Archive
An opened archive: its index held in memory, its contents left on disk.
var archive = try zpack.Archive.open(gpa, io, .cwd(), "game.zpak");defer archive.deinit();open validates the whole index up front, so every later lookup and read works
from data that has already been bounds checked.
Fields
Section titled “Fields”gpa: Allocator,io: Io,file: File,file_size: u64,/// Sorted by path, owned by this `Archive`.entries: []Entry,/// Asset id to index.ids: std.AutoHashMapUnmanaged(u64, u32),entries is public and in sorted path order - iterate it directly to list an
archive’s contents without a helper.
pub fn open(gpa: Allocator, io: Io, dir: Dir, sub_path: []const u8) !ArchiveOpens the file and reads the entire index into memory. The data region is left
on disk and read on demand, so open on a 4 GB archive costs the same as on a
4 KB one.
The file stays open for the lifetime of the Archive. Call deinit to close it
and free the index.
Every structural check happens here - see validation for
the full list and errors for what each one means. Contents are
not read, so HashMismatch never comes from open.
deinit
Section titled “deinit”pub fn deinit(self: *Archive) voidFrees every entry path, the entry array, and the id map, then closes the file.
The Archive is left undefined - do not use it afterwards.
pub fn find(self: *const Archive, path: []const u8) ?EntryExact lookup by path. No I/O: this reads a hash map and returns metadata.
const entry = archive.find("textures/player.png") orelse return error.MissingAsset;std.log.info("{d} bytes, stored as {t}", .{ entry.size, entry.method });The path is compared after the id matches, so a path that is not in the archive can never resolve to a colliding entry.
findId
Section titled “findId”pub fn findId(self: *const Archive, id: u64) ?EntryLookup by the u64 handle format.assetId produces, for
callers holding a generated id rather than a path:
const Asset = @import("assets.zig").Asset;const entry = archive.findId(@intFromEnum(Asset.@"textures/player.png")).?;open rejects archives whose paths collide, so an id from this archive is
unambiguous. See asset handles.
pub fn read(self: *const Archive, entry: Entry, buffer: []u8) ![]u8Writes entry’s original bytes into buffer and returns the filled prefix.
Nothing is allocated, and the contents are checked against the stored hash
before returning.
var scratch: [64 * 1024]u8 = undefined;const bytes = try archive.read(entry, &scratch);buffer must hold at least entry.size bytes, which find told you before any
I/O happened. A shorter buffer returns error.BufferTooSmall - including when
entry.size does not fit in a usize, which only arises on 32-bit targets.
Decompression happens straight into buffer when the entry is deflated; the
decompressor is given a zero-length window so it needs no scratch space of its
own.
| Error | Means |
|---|---|
BufferTooSmall |
buffer.len < entry.size, or entry.size exceeds usize |
HashMismatch |
The bytes do not match the hash recorded at pack time |
CorruptArchive |
The entry produced fewer bytes than the index promised, or the deflate stream is malformed |
readAlloc
Section titled “readAlloc”pub fn readAlloc(self: *Archive, gpa: Allocator, path: []const u8) ![]u8Finds, allocates, and reads in one call. The caller owns the result:
const bytes = try archive.readAlloc(gpa, "levels/level-01.json");defer gpa.free(bytes);Returns error.FileNotFound when the path is not in the archive, and
error.OutOfMemory if the size does not fit in a usize. Otherwise the same
errors as read.
Convenient for one-off reads and configuration files. For a hot asset-loading
path, prefer find plus read into a buffer you already own.
entryReader
Section titled “entryReader”pub fn entryReader(self: *const Archive, entry: Entry, buffer: []u8) error{BufferTooSmall}!EntryReaderReturns a reader over one entry’s original bytes, decompressing as it goes, for assets too large to hold at once.
const buffer = try gpa.alloc(u8, zpack.stream_buffer_len);defer gpa.free(buffer);
var entry_reader = try archive.entryReader(entry, buffer);var header: [4]u8 = undefined;try entry_reader.reader().readSliceAll(&header);For a deflated entry, buffer must be at least zpack.stream_buffer_len. For a
stored entry any size works, including smaller than the entry.
Full details, including the aliasing rule, in EntryReader.
verify
Section titled “verify”pub fn verify(self: *Archive) !voidRehashes every entry’s contents and returns error.HashMismatch at the first
one that does not match. Writes nothing.
archive.verify() catch |err| { std.log.err("archive is damaged: {t}", .{err}); return err;};Each entry is read with a limit of one byte past its declared size, so an entry that produces too many bytes is caught as a length mismatch rather than read to exhaustion. Both the length and the hash must match.
Allocates one stream_buffer_len buffer for the pass and frees it on return.
Memory stays flat regardless of archive size.
unpack
Section titled “unpack”pub fn unpack(self: *Archive, dest: Dir) !StatsExtracts every entry into dest, creating parent directories as needed, and
checks contents against their hashes as they stream.
const dest = try Dir.cwd().createDirPathOpen(io, "extracted", .{});defer dest.close(io);
const stats = try archive.unpack(dest);std.log.info("{d} files, {d} bytes", .{ stats.file_count, stats.total_bytes });Entries are sorted, so files sharing a directory are contiguous and the same
parent is never created twice. Paths are converted from / to the host
separator on the way out.
Extraction is not transactional: entries written before a failure stay on disk. See unpack.
writeManifest
Section titled “writeManifest”pub fn writeManifest(self: *const Archive, w: *Io.Writer) (Io.Writer.Error || format.Error)!voidWrites the index as ZON to any std.Io.Writer - what the archive holds, without
its contents:
var buffer: [4096]u8 = undefined;var out: Io.File.Writer = .initStreaming(.stdout(), io, &buffer);try archive.writeManifest(&out.interface);try out.interface.flush();Derived from the archive rather than written alongside it, so it cannot fall out
of sync. Returns error.CorruptArchive if the entry sizes sum past u64, which
is reported rather than trapped.
See manifests for the output shape and how to parse it back.
Entry and Stats
Section titled “Entry and Stats”pub const Entry = struct { /// Relative to the archive root, `/`-separated, UTF-8. Never escapes the root. path: []const u8, /// Absolute byte offset of the stored bytes, from the start of the archive. offset: u64, /// Size of the original file. size: u64, /// Bytes this entry occupies in the archive. Equals `size` when stored. stored_size: u64, /// `Hasher` digest of the original bytes. hash: u64, method: Method,};
pub const Stats = struct { file_count: u32, total_bytes: u64, /// Bytes the entries occupy in the archive, after compression. stored_bytes: u64,};An Entry is a value, copied out of the archive on lookup. Its path borrows
from the Archive and is invalidated by deinit.