Concepts
Five terms recur throughout these docs. They are worth ten minutes up front.
Archive
Section titled “Archive”A single .zpak file. It has two parts: an index at the head describing
what the archive holds, and a data region after it holding the bytes.
Opening an archive reads and validates only the index. The data region stays on
disk and is read on demand, one entry at a time. That is why
Archive.open on a 4 GB archive costs the same as on a 4 KB one, and why the
whole thing is never resident in memory.
The header plus one entry record per file, in sorted path order. Its size is
fixed once the set of paths is known, which is what lets pack reserve it,
stream all the file data, and come back to fill it in with offsets and sizes
that describe what was actually written rather than what stat predicted.
Because the index comes first, list and find never touch the data region.
One file’s metadata:
pub const Entry = struct { path: []const u8, // relative, `/`-separated, UTF-8 offset: u64, // absolute, from the start of the archive size: u64, // the original file stored_size: u64, // what the archive holds hash: u64, // XxHash64 of the original bytes method: Method, // .store or .deflate};find returns this and reads nothing. The separation matters: because
entry.size is known before any I/O, a caller can size its own buffer and
read needs no allocator at all.
Method: store or deflate
Section titled “Method: store or deflate”Each file is compressed with raw deflate at level 6. If the result is not
smaller than the original, pack rewinds and writes the file verbatim instead,
recording method = .store.
That fallback is why an archive of PNGs, MP3s, and video never grows. Those
formats are already compressed; deflating them costs CPU and adds bytes.
A mixed asset tree ends up with both methods in one archive, and reading does
not care which - read and entryReader
handle both.
When an entry is stored, stored_size == size. An archive whose index claims
otherwise for a stored entry is rejected as corrupt.
Asset id
Section titled “Asset id”A u64 handle for a path: XxHash64 of the path bytes under a fixed seed.
const id = zpack.format.assetId("textures/player.png");The same path always yields the same id, so an id survives a repack and can be
computed anywhere - nothing about it is stored in the archive. That is what
makes zpack ids work: it emits a Zig enum of these
handles, so a mistyped asset path becomes a compile error rather than a null
at load time.
Both pack and Archive.open reject an archive in which two different paths
hash alike, so a collision is a build failure rather than a wrong asset served
at runtime.
Io and Dir
Section titled “Io and Dir”Zig 0.16 routes all filesystem work through std.Io. Every zpack entry point
takes an Io and a std.Io.Dir rather than a bare path string:
var archive = try zpack.Archive.open(gpa, io, .cwd(), "game.zpak");.cwd() is the usual Dir, but any directory handle works - which is what lets
pack write into a directory it also reads from, and lets unpack be confined
to a destination it cannot escape.