Skip to content
zit Artifact [11022df8a0]
Login

Artifact [11022df8a0]

Artifact 11022df8a07c00113c94b4b4695060b2487c7a4623562d9895e616759fc6d1fc:


//! Seeded, parameterized fuzz targets for the framed, content-addressed log
//! (storage-track-plan.md §C/§E) over a deterministic in-memory backend. Each
//! target takes its seed and round count from the caller (`zig build fuzz --
//! <target> [--seed N] [--rounds N]`); a fixed-seed smoke `test` per target
//! (below) delegates to the same pub fn so the same invariant also runs under
//! `zig build test`. Registered in fuzz_tests.zig.
//!
//! The disk is a zero-fault sim/FaultyFile (the healthy-disk default config, so
//! nothing tears or flips), which makes the round-trip invariants deterministic
//! and filesystem-free. delta is reached through the public `zit` module: a
//! relative import would pull delta.zig into this compilation root a second
//! time, colliding with the copy logstore.zig already holds.

const std = @import("std");
const logstore = @import("zit").logstore;
const faulty = @import("sim/faulty_file.zig");
const delta = @import("zit").delta;

/// A fresh in-memory backend with the healthy-disk default config — the
/// precondition every round-trip below depends on.
fn healthyFile(gpa: std.mem.Allocator, rand: std.Random) faulty.FaultyFile {
    const file = faulty.FaultyFile.init(gpa, rand);
    std.debug.assert(std.meta.eql(file.cfg, faulty.FaultCfg{}));
    return file;
}

/// A random `min_hash_len..max_hash_len` byte hash into `buf`. Hashes are
/// opaque here (never verified against content), so arbitrary bytes are fine.
fn randomHash(rand: std.Random, buf: *[logstore.max_hash_len]u8) []const u8 {
    const span = logstore.max_hash_len - logstore.min_hash_len + 1;
    const len = logstore.min_hash_len + rand.uintLessThan(usize, span);
    for (buf[0..len]) |*b| b.* = rand.int(u8);
    return buf[0..len];
}

/// `open` never crashes on garbage appended after a valid header: it recovers a
/// store or fails with one of its declared errors (storage-track-plan.md §C
/// torn-tail recovery).
pub fn openGarbage(gpa: std.mem.Allocator, seed: u64, rounds: usize) !void {
    var prng = std.Random.DefaultPrng.init(seed);
    const rand = prng.random();
    var round: usize = 0;
    while (round < rounds) : (round += 1) {
        var backend = healthyFile(gpa, rand);
        defer backend.deinit();
        const file = backend.storageFile();

        var seeded = try logstore.LogStore.open(gpa, file, 0);
        seeded.deinit(gpa);

        var garbage: [256]u8 = undefined;
        const n = rand.uintLessThan(usize, garbage.len + 1);
        rand.bytes(garbage[0..n]);
        try file.append(garbage[0..n]);

        if (logstore.LogStore.open(gpa, file, 0)) |store| {
            var opened = store;
            opened.deinit(gpa);
        } else |err| switch (err) {
            error.OutOfMemory, error.Storage, error.Corrupt => {},
        }
    }
}

/// §C/sync §4.1 — putZlib then content round-trips the uncompressed bytes: the
/// zlib container the store writes inflates back to exactly what went in.
pub fn zlibRoundTrip(gpa: std.mem.Allocator, seed: u64, rounds: usize) !void {
    var prng = std.Random.DefaultPrng.init(seed);
    const rand = prng.random();
    const max_raw: usize = 128;
    var hash_buf: [logstore.max_hash_len]u8 = undefined;
    var raw_buf: [max_raw]u8 = undefined;
    var round: usize = 0;
    while (round < rounds) : (round += 1) {
        var backend = healthyFile(gpa, rand);
        defer backend.deinit();
        var store = try logstore.LogStore.open(gpa, backend.storageFile(), 0);
        defer store.deinit(gpa);

        const hash = randomHash(rand, &hash_buf);
        const rlen = rand.uintLessThan(usize, max_raw + 1);
        for (raw_buf[0..rlen]) |*b| b.* = rand.int(u8);
        const raw = raw_buf[0..rlen];

        const rid = try store.putZlib(hash, raw);
        const got = (try store.content(gpa, rid)).?;
        defer gpa.free(got);
        try std.testing.expectEqualSlices(u8, raw, got);
    }
}

/// §C/delta §4 — putDelta then content applies the delta and round-trips the
/// target: a delta frame reconstructs its target by walking to the base frame
/// and applying via delta.apply.
pub fn deltaRoundTrip(gpa: std.mem.Allocator, seed: u64, rounds: usize) !void {
    var prng = std.Random.DefaultPrng.init(seed);
    const rand = prng.random();
    const max_len: usize = 128;
    var base_buf: [max_len]u8 = undefined;
    var target_buf: [max_len]u8 = undefined;
    var base_hash: [20]u8 = undefined;
    var target_hash: [20]u8 = undefined;
    var round: usize = 0;
    while (round < rounds) : (round += 1) {
        var backend = healthyFile(gpa, rand);
        defer backend.deinit();
        var store = try logstore.LogStore.open(gpa, backend.storageFile(), 0);
        defer store.deinit(gpa);

        const blen = 1 + rand.uintLessThan(usize, max_len);
        for (base_buf[0..blen]) |*b| b.* = rand.int(u8);
        const base = base_buf[0..blen];
        const tlen = 1 + rand.uintLessThan(usize, max_len);
        for (target_buf[0..tlen]) |*b| b.* = rand.int(u8);
        const target = target_buf[0..tlen];

        // Distinct hashes: put keys on the hash, so a base/target collision
        // would alias the two frames and defeat the round-trip.
        for (&base_hash) |*b| b.* = rand.int(u8);
        for (&target_hash) |*b| b.* = rand.int(u8);
        base_hash[0] = 0;
        target_hash[0] = 1;

        const d = try delta.create(gpa, base, target);
        defer gpa.free(d);
        const base_rid = try store.put(&base_hash, base);
        const target_rid = try store.putDelta(&target_hash, base_rid, d);
        const got = (try store.content(gpa, target_rid)).?;
        defer gpa.free(got);
        try std.testing.expectEqualSlices(u8, target, got);
    }
}

/// §5 — pickBase/putArtifact over random content and random candidate sets
/// preserves the invariant "storage is a cache of the log": whatever base
/// policy chooses, `content` returns the exact bytes that went in. And any
/// delta pickBase accepts both reconstructs its target and clears the 75%
/// accept bar, against an eligible (non-phantom, under-cap) base.
pub fn pickBaseRoundTrip(gpa: std.mem.Allocator, seed: u64, rounds: usize) !void {
    var prng = std.Random.DefaultPrng.init(seed);
    const rand = prng.random();
    const max_len: usize = 160;
    var buf: [max_len]u8 = undefined;
    var hash_buf: [logstore.max_hash_len]u8 = undefined;
    var thash_buf: [logstore.max_hash_len]u8 = undefined;
    var round: usize = 0;
    while (round < rounds) : (round += 1) {
        var backend = healthyFile(gpa, rand);
        defer backend.deinit();
        var store = try logstore.LogStore.open(gpa, backend.storageFile(), 0);
        defer store.deinit(gpa);

        // Seed a handful of content frames — some sharing a long prefix so
        // profitable deltas exist, some pure noise — each put through the full
        // best-of-N write path with a random (possibly ineligible) base hint.
        // Every stored frame must round-trip regardless of the chosen encoding.
        const seed_frames = 1 + rand.uintLessThan(usize, 8);
        var rids: [8]logstore.Rid = undefined;
        var nr: usize = 0;
        while (nr < seed_frames) : (nr += 1) {
            const len = 1 + rand.uintLessThan(usize, max_len);
            for (buf[0..len]) |*b| b.* = rand.int(u8);
            if (nr > 0 and rand.boolean()) @memset(buf[0..@min(len, 48)], 0x5A);
            const hash = randomHash(rand, &hash_buf);
            // Content addressing: distinct content ⇒ distinct hash. `randomHash`
            // can emit a 1-byte hash that collides across seed frames, which
            // would (correctly) hit `putArtifact`'s idempotent short-circuit and
            // alias two different contents to one rid — a harness artifact, not a
            // store bug (real hashes never collide). Stamp the frame index into
            // byte 0 (`seed_frames <= 8`, and `hash.len >= 1`) so every seed
            // frame is a distinct key, mirroring the deltaRoundTrip guard above.
            hash_buf[0] = @intCast(nr);
            const hint: ?logstore.Rid =
                if (nr > 0 and rand.boolean()) rids[rand.uintLessThan(usize, nr)] else null;
            const rid = try store.putArtifact(hash, buf[0..len], hint);
            const got = (try store.content(gpa, rid)).?;
            defer gpa.free(got);
            try std.testing.expectEqualSlices(u8, buf[0..len], got);
            rids[nr] = rid;
        }

        // Now probe pickBase directly with a random candidate multiset (may
        // over-offer past max_base_candidates, repeat rids, or be empty).
        var cands: [12]logstore.Rid = undefined;
        const cn = rand.uintLessThan(usize, @min(nr, cands.len) + 1);
        for (cands[0..cn]) |*c| c.* = rids[rand.uintLessThan(usize, nr)];

        const tlen = 1 + rand.uintLessThan(usize, max_len);
        for (buf[0..tlen]) |*b| b.* = rand.int(u8);
        if (rand.boolean()) @memset(buf[0..@min(tlen, 48)], 0x5A);
        const target = buf[0..tlen];
        const thash = randomHash(rand, &thash_buf);

        if (try store.pickBase(gpa, thash, target, cands[0..cn])) |pick| {
            defer gpa.free(pick.delta);
            // Accepted ⇒ strictly under 75% of the uncompressed artifact.
            try std.testing.expect(@as(u64, pick.delta.len) * 4 < @as(u64, target.len) * 3);
            // Chosen base is eligible: content-bearing and under the write cap.
            try std.testing.expect(!store.isPhantom(pick.base));
            try std.testing.expect(store.depthOf(pick.base) < logstore.delta_chain_write_max);
            // …and the delta reconstructs the target byte-for-byte.
            const base_bytes = (try store.content(gpa, pick.base)).?;
            defer gpa.free(base_bytes);
            const rebuilt = try delta.apply(gpa, base_bytes, pick.delta);
            defer gpa.free(rebuilt);
            try std.testing.expectEqualSlices(u8, target, rebuilt);
        }
    }
}

/// §E — putConfig then readConfig round-trips hash_policy, branch, and project
/// id. readConfig drives the file-private parseConfig; config strings are
/// length-prefixed, so arbitrary bytes survive the round-trip.
pub fn parseConfig(gpa: std.mem.Allocator, seed: u64, rounds: usize) !void {
    var prng = std.Random.DefaultPrng.init(seed);
    const rand = prng.random();
    const max_str: usize = 32; // <= logstore.max_config_str
    var branch_buf: [max_str]u8 = undefined;
    var pid_buf: [max_str]u8 = undefined;
    var round: usize = 0;
    while (round < rounds) : (round += 1) {
        var backend = healthyFile(gpa, rand);
        defer backend.deinit();
        var store = try logstore.LogStore.open(gpa, backend.storageFile(), 0);
        defer store.deinit(gpa);

        const blen = rand.uintLessThan(usize, max_str + 1);
        for (branch_buf[0..blen]) |*b| b.* = rand.int(u8);
        const plen = rand.uintLessThan(usize, max_str + 1);
        for (pid_buf[0..plen]) |*b| b.* = rand.int(u8);
        const cfg: logstore.Config = .{
            .hash_policy = rand.int(u8),
            .default_branch = branch_buf[0..blen],
            .project_id = pid_buf[0..plen],
        };

        try store.putConfig(cfg);
        var oc = (try store.readConfig(gpa)).?;
        defer oc.deinit(gpa);
        try std.testing.expectEqual(cfg.hash_policy, oc.config.hash_policy);
        try std.testing.expectEqualSlices(u8, cfg.default_branch, oc.config.default_branch);
        try std.testing.expectEqualSlices(u8, cfg.project_id, oc.config.project_id);
    }
}

// Fixed-seed smoke tests: each delegates to its pub fn at a small round count so
// the invariant runs under `zig build test`; deep runs pass `--rounds`.

test "logstore fuzz: open never crashes on trailing garbage (smoke)" {
    try openGarbage(std.testing.allocator, 0xC0_FFEE, 32);
}

test "logstore §C/sync §4.1 fuzz: putZlib content round-trip (smoke)" {
    try zlibRoundTrip(std.testing.allocator, 0x21B_2117, 32);
}

test "logstore §C/delta §4 fuzz: putDelta content round-trip (smoke)" {
    try deltaRoundTrip(std.testing.allocator, 0xDE17A_04, 32);
}

test "logstore §5 fuzz: pickBase/putArtifact content round-trip + accept invariants (smoke)" {
    try pickBaseRoundTrip(std.testing.allocator, 0xB43E_5EED, 64);
}

test "logstore §E fuzz: putConfig/readConfig round-trip (smoke)" {
    try parseConfig(std.testing.allocator, 0xC0_F1_60, 32);
}