# 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`](https://crates.io/crates/snapdir-ffi) crate on crates.io, then
`zig fetch --save` the wrapper (Zig 0.13.0):
```sh
# 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#
```
See [Install snapdir for Zig](/install/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:
```zig
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 STATUSPATH 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:
```zig
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.
| Platform | Supported |
| --- | --- |
| Linux glibc (x64 + arm64) | yes |
| Linux musl / Alpine (x64 + arm64) | yes |
| macOS | yes |
| Windows | no |
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.