231 lines
5.3 KiB
Zig
231 lines
5.3 KiB
Zig
|
const coral = @import("coral");
|
|||
|
|
|||
|
const ona = @import("ona");
|
|||
|
|
|||
|
pub const Archive = struct {
|
|||
|
state_table: [state_max]State = [_]State{.{}} ** state_max,
|
|||
|
file_accessor: ona.files.FileAccessor,
|
|||
|
file_path: []const u8,
|
|||
|
|
|||
|
const State = struct {
|
|||
|
readable_file: ?*ona.files.ReadableFile = null,
|
|||
|
data_head: u64 = 0,
|
|||
|
data_size: u64 = 0,
|
|||
|
data_cursor: u64 = 0,
|
|||
|
|
|||
|
fn cast(archived_file: *ArchivedFile) *Archive.State {
|
|||
|
return @ptrCast(*State, @alignCast(@alignOf(State), archived_file));
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
const state_max = 64;
|
|||
|
|
|||
|
pub fn open_archived(self: *Archive, path: Path) OpenError!*ArchivedFile {
|
|||
|
const state_index = find_available_state: {
|
|||
|
var index: usize = 0;
|
|||
|
|
|||
|
while (index < self.state_table.len) : (index += 1) {
|
|||
|
if (self.state_table[index].readable_file == null) break :find_available_state index;
|
|||
|
}
|
|||
|
|
|||
|
return error.TooManyFiles;
|
|||
|
};
|
|||
|
|
|||
|
const archive_file = try self.file_accessor.open_readable(self.file_path);
|
|||
|
|
|||
|
errdefer _ = archive_file.close();
|
|||
|
|
|||
|
var archive_header = Header.empty;
|
|||
|
|
|||
|
if ((try archive_file.read(&archive_header.bytes)) != Header.size) return error.ArchiveInvalid;
|
|||
|
|
|||
|
// Read file table.
|
|||
|
var head: u64 = 0;
|
|||
|
var tail: u64 = archive_header.layout.entry_count - 1;
|
|||
|
const path_hash = path.hash();
|
|||
|
|
|||
|
while (head <= tail) {
|
|||
|
const midpoint = head + ((tail - head) / 2);
|
|||
|
var archive_block = Block.empty;
|
|||
|
|
|||
|
try archive_file.seek(Header.size + archive_header.layout.total_data_size + (Block.size * midpoint));
|
|||
|
|
|||
|
if ((try archive_file.read(&archive_block.bytes)) != Block.size) return error.ArchiveInvalid;
|
|||
|
|
|||
|
const path_hash_comparison = path_hash - archive_block.layout.path_hash;
|
|||
|
|
|||
|
if (path_hash_comparison > 0) {
|
|||
|
head = (midpoint + 1);
|
|||
|
|
|||
|
continue;
|
|||
|
}
|
|||
|
|
|||
|
if (path_hash_comparison < 0) {
|
|||
|
tail = (midpoint - 1);
|
|||
|
|
|||
|
continue;
|
|||
|
}
|
|||
|
|
|||
|
const path_comparison = path.compare(archive_block.layout.path);
|
|||
|
|
|||
|
if (path_comparison > 0) {
|
|||
|
head = (midpoint + 1);
|
|||
|
|
|||
|
continue;
|
|||
|
}
|
|||
|
|
|||
|
if (path_comparison < 0) {
|
|||
|
tail = (midpoint - 1);
|
|||
|
|
|||
|
continue;
|
|||
|
}
|
|||
|
|
|||
|
self.state_table[state_index] = .{
|
|||
|
.readable_file = archive_file,
|
|||
|
.data_head = archive_block.layout.data_head,
|
|||
|
.data_size = archive_block.layout.data_size,
|
|||
|
.data_cursor = 0,
|
|||
|
};
|
|||
|
|
|||
|
return @ptrCast(*ArchivedFile, &(self.state_table[state_index]));
|
|||
|
}
|
|||
|
|
|||
|
return error.FileNotFound;
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
pub const ArchivedFile = opaque {
|
|||
|
pub fn as_reader(self: *ArchivedFile) coral.io.Reader {
|
|||
|
return coral.io.Reader.bind(self, ArchivedFile);
|
|||
|
}
|
|||
|
|
|||
|
pub fn close(self: *ArchivedFile) bool {
|
|||
|
const state = Archive.State.cast(self);
|
|||
|
|
|||
|
if (state.readable_file) |readable_file| {
|
|||
|
defer state.readable_file = null;
|
|||
|
|
|||
|
return readable_file.close();
|
|||
|
}
|
|||
|
|
|||
|
return true;
|
|||
|
}
|
|||
|
|
|||
|
pub fn read(self: *ArchivedFile, buffer: []u8) coral.io.ReadError!usize {
|
|||
|
const state = Archive.State.cast(self);
|
|||
|
|
|||
|
if (state.readable_file) |readable_file| {
|
|||
|
const actual_cursor = coral.math.min(u64,
|
|||
|
state.data_head + state.data_cursor, state.data_head + state.data_size);
|
|||
|
|
|||
|
try readable_file.seek(actual_cursor);
|
|||
|
|
|||
|
const buffer_read = coral.math.min(usize, buffer.len, state.data_size - actual_cursor);
|
|||
|
|
|||
|
defer state.data_cursor += buffer_read;
|
|||
|
|
|||
|
return readable_file.read(buffer[0..buffer_read]);
|
|||
|
}
|
|||
|
|
|||
|
return error.IoUnavailable;
|
|||
|
}
|
|||
|
|
|||
|
pub fn size(self: *ArchivedFile) u64 {
|
|||
|
return Archive.State.cast(self).data_size;
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
const Block = extern union {
|
|||
|
layout: extern struct {
|
|||
|
path: Path,
|
|||
|
path_hash: u64,
|
|||
|
data_head: u64,
|
|||
|
data_size: u64,
|
|||
|
},
|
|||
|
|
|||
|
bytes: [size]u8,
|
|||
|
|
|||
|
const empty = Block{ .bytes = [_]u8{0} ** size };
|
|||
|
|
|||
|
const size = 512;
|
|||
|
};
|
|||
|
|
|||
|
const Header = extern union {
|
|||
|
layout: extern struct {
|
|||
|
signature: [signature_magic.len]u8,
|
|||
|
entry_count: u32,
|
|||
|
total_data_size: u64,
|
|||
|
},
|
|||
|
|
|||
|
bytes: [size]u8,
|
|||
|
|
|||
|
const empty = Header{ .bytes = [_]u8{0} ** size };
|
|||
|
|
|||
|
const signature_magic = [_]u8{ 'o', 'a', 'r', 1 };
|
|||
|
|
|||
|
const size = 16;
|
|||
|
};
|
|||
|
|
|||
|
pub const OpenError = ona.files.OpenError || coral.io.ReadError || error{
|
|||
|
ArchiveInvalid,
|
|||
|
};
|
|||
|
|
|||
|
pub const Path = extern struct {
|
|||
|
buffer: [maximum + 1]u8,
|
|||
|
|
|||
|
pub const DataError = error{
|
|||
|
PathCorrupt,
|
|||
|
};
|
|||
|
|
|||
|
pub const ParseError = error{
|
|||
|
TooLong,
|
|||
|
};
|
|||
|
|
|||
|
pub fn compare(self: Path, other: Path) isize {
|
|||
|
return coral.io.compare(&self.buffer, &other.buffer);
|
|||
|
}
|
|||
|
|
|||
|
pub fn data(self: Path) DataError![:0]const u8 {
|
|||
|
// Verify presence of zero terminator.
|
|||
|
if (self.buffer[self.filled()] != 0) return error.PathCorrupt;
|
|||
|
|
|||
|
return @ptrCast([:0]const u8, self.buffer[0..self.filled()]);
|
|||
|
}
|
|||
|
|
|||
|
pub fn filled(self: Path) usize {
|
|||
|
return maximum - self.remaining();
|
|||
|
}
|
|||
|
|
|||
|
pub fn hash(self: Path) u64 {
|
|||
|
// Fowler–Noll–Vo hash function is used here as it has a lower collision rate for smaller inputs.
|
|||
|
const fnv_prime = 0x100000001b3;
|
|||
|
var hash_code = @as(u64, 0xcbf29ce484222325);
|
|||
|
|
|||
|
for (self.buffer[0..self.filled()]) |byte| {
|
|||
|
hash_code = hash_code ^ byte;
|
|||
|
hash_code = hash_code *% fnv_prime;
|
|||
|
}
|
|||
|
|
|||
|
return hash_code;
|
|||
|
}
|
|||
|
|
|||
|
pub const maximum = 255;
|
|||
|
|
|||
|
pub fn parse(bytes: []const u8) ParseError!Path {
|
|||
|
if (bytes.len > maximum) return error.TooLong;
|
|||
|
|
|||
|
// Int cast is safe as bytes length is confirmed to be smaller than or equal to u8 maximum.
|
|||
|
var parsed_path = Path{ .buffer = ([_]u8{0} ** maximum) ++ [_]u8{maximum - @intCast(u8, bytes.len)} };
|
|||
|
|
|||
|
coral.io.copy(&parsed_path.buffer, bytes);
|
|||
|
|
|||
|
return parsed_path;
|
|||
|
}
|
|||
|
|
|||
|
pub fn remaining(self: Path) usize {
|
|||
|
return self.buffer[maximum];
|
|||
|
}
|
|||
|
|
|||
|
pub const seperator = '/';
|
|||
|
};
|