ona/source/oar/oar.zig

231 lines
5.3 KiB
Zig
Raw Normal View History

2023-04-19 01:25:35 +02:00
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 {
// FowlerNollVo 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 = '/';
};