ona/source/oar/oar.zig

231 lines
5.3 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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