snapdir for Zig
Idiomatic Zig bindings for snapdir. The binding is a thin @cImport wrapper over
the snapdir C ABI (snapdir.h), which itself wraps the canonical
snapdir-api core — so the manifests and snapshot IDs it produces are
bit-identical to the CLI and to every other binding. Snapshot IDs are
64-character lowercase hex.
Install
The Zig binding is build-from-source (there is no prebuilt native blob in a
Zig registry). Build the C ABI from the
snapdir-ffi crate on crates.io, then
zig fetch --save the wrapper (Zig 0.13.0):
# needs a Rust toolchain + cbindgen; on Alpine also: apk add libgcc
cargo build --release # -> libsnapdir_ffi.a
cbindgen --config cbindgen.toml --crate snapdir-ffi --output include/snapdir.h .
zig fetch --save git+https://github.com/snapdir/snapdir#<rev>
See Install snapdir for Zig for the build.zig wiring and the
Alpine/musl libgcc_s note.
Usage
Every allocating function takes a std.mem.Allocator first (use an arena or
std.testing.allocator for leak detection). The id/manifest calls are
synchronous; push/pull/fetch/diff do the transfer. The store URI is
always an argument — file://$PWD/store for a local store, gs://bucket/prefix
for Google Cloud:
const std = @import("std");
const snapdir = @import("snapdir");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const a = gpa.allocator();
// id → 64-char snapshot id ([64]u8 value, no free).
const id = try snapdir.id(a, "./my-dir", .{});
std.debug.print("{s}\n", .{id});
// Full manifest (raw text + parsed entries); free with deinit.
var m = try snapdir.manifest(a, "./my-dir", .{});
defer m.deinit(a);
for (m.entries) |e| {
// e.type (PathType), e.perm (u32), e.checksum ([64]u8), e.size (u64), e.path
}
// snapdir.idFromManifest(m) == id
// Options: exclude a regex, do not follow symlinks.
const filtered = try snapdir.id(a, "./my-dir", .{ .no_follow = true, .exclude = "\\.tmp$" });
_ = filtered;
// Transfer to a store (URI is an argument).
const pushed = try snapdir.push(a, "./my-dir", "file:///…/store", .{});
try snapdir.pull(a, &pushed, "file:///…/store", "./restored", .{});
// fetch caches objects locally; diff compares two store contents
// as STATUS<TAB>PATH entries (A / D / M / =), byte-identical to the CLI.
const changes = try snapdir.diff(a, "file:///…/store-a", "file:///…/store-b", .{});
defer {
for (changes) |c| a.free(c.path);
a.free(changes);
}
for (changes) |c| std.debug.print("{c}\t{s}\n", .{ @intFromEnum(c.status), c.path });
}
To pin a specific snapshot when diffing, use the store@id convention (the last
@ splits the store URI from the 64-hex ID), pull each side into a temp
directory, and diff those — see the canonical examples/zig/app.zig.
Errors
Errors are a Zig error{...} set mapped from the 8 stable C ABI codes, plus
OutOfMemory. catch |err| switch (err) handles them exhaustively:
const id = snapdir.id(a, "/no/such/path", .{}) catch |err| switch (err) {
error.IoError => return, // IO_ERROR — filesystem / network failure
error.HashMismatch => return, // HASH_MISMATCH — content did not verify
error.StoreError => return, // STORE_ERROR — object store rejected the op
error.InFlux => return, // IN_FLUX — tree changed mid-snapshot
error.CatalogError => return, // CATALOG_ERROR — manifest/catalog problem
error.InvalidId => return, // INVALID_ID — malformed snapshot id
error.InvalidStore => return, // INVALID_STORE — unparseable store URI
error.Conflict => return, // CONFLICT — concurrent write conflict
else => return err, // OutOfMemory
};
The same eight codes (IO_ERROR, HASH_MISMATCH, STORE_ERROR, IN_FLUX,
CATALOG_ERROR, INVALID_ID, INVALID_STORE, CONFLICT) appear in every
binding, so error handling ports cleanly across languages.
Platform support
Every binding runs on Linux (glibc and Alpine/musl, x64 and arm64) and macOS; Windows is unsupported. Each release is CI-verified from its live public registry.
The one binding-wide exception is Java, which is glibc-only today (musl support is planned for 1.11.1). The Zig binding links on glibc and musl alike.