Skip to content

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.

Terminal window
zig fetch --save git+https://github.com/masonschafercodes/zpack#v0.0.2

Then 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.

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.

// Namespaces
pub 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 function
pub const pack = @import("pack.zig").pack;
// Re-exported for convenience
pub const Entry = format.Entry;
pub const Method = format.Method;
// Buffer sizes
pub 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,
};

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.

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.

An Archive is safe to read from multiple threads at once, provided nobody mutates it:

  • find and findId only read a hash map that is fixed after open
  • read uses positional reads, so concurrent reads do not race on a shared file cursor
  • Each entryReader owns 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.