Library overview
main.zig contains only argument handling. Packing and reading live in the
zpack module, so a game can depend on it directly and never shell out to the
binary.
Adding the dependency
Section titled “Adding the dependency”zig fetch --save git+https://github.com/masonschafercodes/zpack#v0.0.2Then wire the module into your build.zig:
const zpack = b.dependency("zpack", .{ .target = target, .optimize = optimize });exe.root_module.addImport("zpack", zpack.module("zpack"));const zpack = @import("zpack");zpack targets Zig 0.16.0 and has no dependencies of its own.
The shape of the API
Section titled “The shape of the API”const std = @import("std");const zpack = @import("zpack");
pub fn main(init: std.process.Init) !void { const gpa = init.gpa; const io = init.io;
var archive = try zpack.Archive.open(gpa, io, .cwd(), "game.zpak"); defer archive.deinit();
// Metadata lookup, no I/O. `entry.size` says how much room a read needs. const entry = archive.find("textures/player.png") orelse return error.MissingAsset;
// Read into memory you already own. Nothing is allocated, and the bytes // are checked against the stored hash. var scratch: [32 * 1024]u8 = undefined; const png = try archive.read(entry, &scratch); std.log.info("{s} is {d} bytes", .{ entry.path, png.len });
// Or stream it, 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 magic: [4]u8 = undefined; try entry_reader.reader().readSliceAll(&magic);
// Or let zpack allocate, when the caller wants to own the bytes. const copy = try archive.readAlloc(gpa, "textures/player.png"); defer gpa.free(copy);}The split between find and read is the design’s centre of gravity: find
returns metadata and touches no I/O, so entry.size is known before any read.
That is what makes read usable without an allocator - the caller sizes its own
buffer.
What the module exports
Section titled “What the module exports”// Namespacespub const format = @import("format.zig");pub const Archive = @import("Archive.zig");pub const EntryReader = @import("EntryReader.zig");pub const StoredReader = @import("StoredReader.zig");pub const Ignore = @import("ignore.zig");
// The one free functionpub const pack = @import("pack.zig").pack;
// Re-exported for conveniencepub const Entry = format.Entry;pub const Method = format.Method;
// Buffer sizespub const copy_buffer_len = 64 * 1024;pub const stream_buffer_len = flate.max_window_len + 4096;
pub const Stats = struct { file_count: u32, total_bytes: u64, /// Bytes the entries occupy in the archive, after compression. stored_bytes: u64,};Io and Dir
Section titled “Io and Dir”Zig 0.16 performs all filesystem work through std.Io, so every entry point
takes an Io and a std.Io.Dir rather than a bare path string:
pub fn main(init: std.process.Init) !void { const io = init.io; var archive = try zpack.Archive.open(gpa, io, .cwd(), "game.zpak");}.cwd() is the usual Dir, but any handle works. That is what lets pack
write into a directory it is also reading from, and what confines unpack to a
destination it cannot escape.
Buffer sizes
Section titled “Buffer sizes”Two constants, and the difference between them matters:
| Constant | Value | Use |
|---|---|---|
copy_buffer_len |
64 KiB | Bulk copies while packing and extracting |
stream_buffer_len |
flate.max_window_len + 4096 |
The minimum buffer entryReader accepts for a deflated entry |
A deflated entry needs a full history window plus room for the compressed bytes,
which is why stream_buffer_len is what it is. A stored entry accepts any
buffer size, including one smaller than the entry itself.
Passing a buffer below stream_buffer_len for a deflated entry returns
error.BufferTooSmall. If you do not know an entry’s method up front, size for
stream_buffer_len and it works either way.
Thread safety
Section titled “Thread safety”An Archive is safe to read from multiple threads at once, provided nobody
mutates it:
findandfindIdonly read a hash map that is fixed afteropenreaduses positional reads, so concurrent reads do not race on a shared file cursor- Each
entryReaderowns its own buffer and position
open, deinit, and unpack are not concurrent operations. entryReader
returns a value that must not be copied after reader() has been called on it -
see EntryReader.