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.

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.