pack
pub fn pack( gpa: Allocator, io: Io, src: Dir, out_dir: Dir, out_sub_path: []const u8,) !StatsWrites src as a .zpak archive at out_dir/out_sub_path.
const src = try Dir.cwd().openDir(io, "assets", .{ .iterate = true });defer src.close(io);
const stats = try zpack.pack(gpa, io, src, .cwd(), "game.zpak");std.log.info("packed {d} files, {d} -> {d} bytes", .{ stats.file_count, stats.total_bytes, stats.stored_bytes,});What it packs
Section titled “What it packs”Every regular file under src, recursively. Directories are implied by entry
paths, so empty ones are not preserved; symlinks, devices, and sockets are
skipped.
Paths are stored relative to src and normalized to /, so an archive packed
on Windows extracts identically on Linux.
Four things are excluded:
- Anything matching a pattern in
src/.zpackignore - The
.zpackignorefile itself - A file whose relative path equals
out_sub_path- which is what makes packing a directory into itself repeatable - The temporary file
packbuilds into, created after the scan so it cannot appear in its own archive
The out_sub_path comparison is done against the /-separated form the scan
produces, so it matches regardless of which separator the caller wrote.
Atomicity
Section titled “Atomicity”The archive is built in a temporary file beside the destination and moved into
place only once complete. A failure partway - a full disk, a vanished source
file - leaves any existing archive at out_sub_path untouched.
Nothing at the destination changes until the very last step.
Compression
Section titled “Compression”Each file is compressed with raw deflate at level 6. If the result is not
smaller than the original, pack seeks back, re-reads the source, and writes it
verbatim as store.
That rewind costs a second read of the file, which is the price of never producing an archive larger than its input. There is no level parameter.
If the file changed size between the two reads, pack returns
error.SourceChanged rather than writing an entry whose length it cannot
trust.
Reproducibility
Section titled “Reproducibility”Directory walk order is undefined, so paths are sorted before the index is written. The same tree always produces the same bytes.
How it works
Section titled “How it works”The index is fixed-size once the set of paths is known. pack reserves that
much space, streams every file, then seeks back to fill the index in - so
offsets and sizes describe what was actually written, not what stat
predicted.
1. Load src/.zpackignore2. Walk src, pruning ignored directories without entering them3. Sort the paths4. Reserve the index; seek past it5. For each file: hash + compress into place, or rewind and store6. Seek to 0, write the header and the filled-in index7. Truncate to length, then move the temporary into placeStep 2 is selective: an ignored directory is never opened at all, so .git/
costs nothing to skip regardless of its size.
Step 7’s truncate matters because a store rewind can leave discarded compressed bytes past the last entry.
Errors
Section titled “Errors”| Error | Cause |
|---|---|
AssetIdCollision |
Two different paths hash to the same asset id |
DuplicatePath |
The same path was scanned twice |
InvalidPath |
A filename containing \, :, or NUL, or not valid UTF-8 |
PathTooLong |
A path over 65535 bytes |
TooManyFiles |
More than maxInt(u32) files |
TooManyRules |
More than 1024 rules in .zpackignore |
SourceChanged |
A file’s length changed between compression and the store fallback |
Plus whatever the filesystem and allocator return. See errors.
Memory
Section titled “Memory”| Data | Where |
|---|---|
| Directory metadata | An arena, discarded when pack returns |
| File contents | Two reused 64 KiB buffers, one read, one write |
| Deflate window | One flate.max_window_len buffer, reused |
| Deflate compressor | ~230 KB, heap-allocated once and reused for every file |
| The index | The same arena as the metadata |
No file’s contents are ever resident whole. Packing a 40 GB tree uses the same working set as packing a 40 MB one - the peak scales with the number of paths, not their total size.