const ona = @import("ona"); const std = @import("std"); /// /// Thin file-wrapper and in-memory layout cache of an OAR archive file. /// pub const Archive = struct { file_access: ona.io.FileAccess, index_cache: IndexCache, /// /// [OpenError.EntryNotFound] happens when an entry could not be found. /// pub const FindError = ona.io.FileAccess.Error || error { EntryNotFound, }; /// /// See [std.mem.Allocator.Error]. /// pub const InitError = std.mem.Allocator.Error; /// /// In-memory archive layout cache. /// /// As the archive is queried via [find], the cache is lazily assembled with the absolute /// offsets of each queried file. /// const IndexCache = ona.table.Hashed([]const u8, u64, ona.table.string_context); /// /// Deinitializes the index cache of `archive`, freeing all associated memory. /// /// **Note** that this does nothing to the [FileAccess] value that was provided as part of /// [init]. /// pub fn deint(archive: *Archive) void { archive.index_cache.deinit(); } /// /// Finds an entry matching `entry_path` in `archive`. /// /// The found [Entry] value is returned or a [FindError] if it failed to be found. /// pub fn find(archive: *Archive, entry_path: []const u8) FindError!Entry { return Entry{ .header = find_header: { var header = Entry.Header{ .revision = 0, .path = Path.empty, .file_size = 0, .absolute_offset = 0 }; const header_size = @sizeOf(Entry.Header); if (archive.index_cache.lookup(entry_path)) |cursor| { try archive.file_access.seek(cursor); if ((try archive.file_access.read(std.mem.asBytes(&header))) != header_size) { std.debug.assert(archive.index_cache.remove(entry_path) != null); return error.EntryNotFound; } break: find_header header; } else { const mem = std.mem; // Start from beginning of archive. try archive.file_access.seek(0); // Read first entry. while ((try archive.file_access.read(mem.asBytes(&header))) == header_size) { if (mem.eql(u8, entry_path, header.path.buffer[0 .. header.path.length])) { // If caching fails... oh well... archive.index_cache.insert(entry_path, header.absolute_offset) catch {}; break: find_header header; } // Move over file data following the entry. var to_skip = header.file_size; while (to_skip != 0) { const math = std.math; const skipped = math.min(to_skip, math.maxInt(i64)); try archive.file_access.skip(@intCast(i64, skipped)); to_skip -= skipped; } } } return error.EntryNotFound; }, .owner = &archive.file_access, .cursor = 0, }; } /// /// Attempts to initialize a new [Archive] with `cache_allocator` as the allocator for managing /// the in-memory archive layout caches and `archive_file_access` as the currently open archive /// file. /// /// **Note** that `archive_file_access` does nothing to manage the lifetime of the open file. /// pub fn init(cache_allocator: std.mem.Allocator, archive_file_access: ona.io.FileAccess) InitError!Archive { return Archive{ .index_cache = try IndexCache.init(cache_allocator), .file_access = archive_file_access, }; } }; /// /// Handles the state of an opened archive entry. /// pub const Entry = struct { owner: ?*ona.io.FileAccess, cursor: u64, header: Header, /// /// An entry block of an Oar archive file. /// /// Typically, following the block in memory is the file data it holds the meta-information for. /// pub const Header = extern struct { signature: [signature_magic.len]u8 = signature_magic, revision: u8, path: Path, file_size: u64, absolute_offset: u64, padding: [232]u8 = std.mem.zeroes([232]u8), comptime { const entry_size = @sizeOf(Header); if (entry_size != 512) @compileError("Entry is " ++ std.fmt.comptimePrint("{d}", .{entry_size}) ++ " bytes"); } /// /// Magic identifier used to validate [Entry] data. /// pub const signature_magic = [3]u8{'o', 'a', 'r'}; }; }; /// /// /// pub const Path = extern struct { buffer: [255]u8, length: u8, /// /// /// pub const empty = std.mem.zeroes(Path); };