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 = '/'; };