Skip to content
zit Artifact [c28bf21017]
Login

Artifact [c28bf21017]

Artifact c28bf210176eec4ca7232f256187f00d273c3ecafe7ed2271eaca3bd15b748ba:


//! Seeded, parameterized fuzz targets for the artifact format
//! (draft-fossil-artifact-format-00). Registered in fuzz_tests.zig.
//!
//! Contract for every target here, matching the module's own "every
//! rejection reason collapses to null/false/an error" posture: no panic, no
//! UB -- only a declared error, a null/false, or a value whose invariants
//! this file checks. `parseGarbage` drives the real §4/§5 `parse()`
//! pipeline (framing through classification) on arbitrary bytes;
//! `parseStructured` instead reaches the sixteen `parseXCard` entry points
//! directly, exercising each card's own §4.7.x grammar in isolation rather
//! than through the card-loop's dispatch.

const std = @import("std");
const zit = @import("zit");
const artifact = zit.artifact;

/// Fill `buf` with a mix of plausible card bytes (letters, digits, the
/// grammar's punctuation) and pure noise, biased toward reaching past the
/// first gate of whatever parser sees it.
fn fillGarbage(rand: std.Random, buf: []u8) void {
    const plausible = "ABCDEFGHIJKLMNPQRTUWZ 0123456789abcdef\n+-.:T";
    for (buf) |*b| b.* = if (rand.uintLessThan(u8, 3) == 0)
        plausible[rand.uintLessThan(usize, plausible.len)]
    else
        rand.int(u8);
}

// ---------------------------------------------------------------------------
// parseGarbage — §4 top-level entry point.
// ---------------------------------------------------------------------------

/// `parse` on arbitrary bytes never crashes: only a declared `ParseError` or
/// a `Manifest` value comes back.
///
/// No reachability assertion: the outcome space does not partition here. Random
/// bytes never satisfy §4 framing/syntax, so `parse` returns `SyntaxError`
/// every time — measured 20_000/20_000 SyntaxError at seed 1, zero accepts and
/// zero classification-stage (`ClassifyError`) rejections. The accept arm and
/// the classify variants are reached where structure exists: `createRoundTrip`
/// for accept, the deterministic §5 classify tests for the rest.
pub fn parseGarbage(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 buf: [512]u8 = undefined;
        const len = rand.uintLessThan(usize, buf.len + 1);
        fillGarbage(rand, buf[0..len]);
        if (artifact.parse(gpa, buf[0..len])) |m| {
            var mm = m;
            mm.deinit();
        } else |_| {}
    }
}

// ---------------------------------------------------------------------------
// parseStructured — the sixteen §4.7.x parseXCard entry points.
// ---------------------------------------------------------------------------

/// The card letters that have a `parseXCard` entry point today (the 21-letter
/// closed alphabet minus R/T/U/W, not yet implemented, and Z, which has no
/// per-card parser at all).
const parseable_letters = "ABCDEFGHIJKLMNPQ";
comptime {
    std.debug.assert(parseable_letters.len == 16);
}

/// One SP-joined token: a structured draw (hash-length hex, a timestamp
/// literal, a `q-sign` byte) or pure noise. Returns the written length,
/// capped to `buf.len`.
fn genOneToken(rand: std.Random, buf: []u8) usize {
    if (buf.len == 0) return 0;
    return switch (rand.uintLessThan(u8, 4)) {
        0 => blk: {
            const want: usize = if (rand.boolean()) 40 else 64;
            const n = @min(want, buf.len);
            for (buf[0..n]) |*b| b.* = "0123456789abcdef"[rand.uintLessThan(usize, 16)];
            break :blk n;
        },
        1 => blk: {
            const lit = timestamp_literals[rand.uintLessThan(usize, timestamp_literals.len)];
            const n = @min(lit.len, buf.len);
            @memcpy(buf[0..n], lit[0..n]);
            break :blk n;
        },
        2 => blk: {
            buf[0] = if (rand.boolean()) '+' else '-';
            break :blk 1;
        },
        else => blk: {
            const n = rand.uintLessThan(usize, @min(buf.len, 24) + 1);
            fillGarbage(rand, buf[0..n]);
            break :blk n;
        },
    };
}

const timestamp_literals = [_][]const u8{
    "2017-12-06T15:36:24.287", "2017-12-06", "15:36:24", "now", "2451545",
};

/// Fill `buf` with 0-4 SP-joined tokens (`genOneToken`). Returns the written
/// length.
fn genTokens(rand: std.Random, buf: []u8) usize {
    const token_count = rand.uintLessThan(usize, 5);
    var w: usize = 0;
    var t: usize = 0;
    while (t < token_count and w < buf.len) : (t += 1) {
        if (t != 0) {
            buf[w] = ' ';
            w += 1;
            if (w >= buf.len) break;
        }
        w += genOneToken(rand, buf[w..]);
    }
    return w;
}

/// Call the one `parseXCard` matching `card.letter` -- every parser's sole
/// caller precondition beyond argument shape (`std.debug.assert(card.letter
/// == '<X>')`), upheld here since `letter` is always drawn from
/// `parseable_letters`. `decode_buf` sized to the raw args length is always
/// sufficient: every parser's decoded fields are a subset of the same raw
/// bytes (decodeEscapes never grows), so no per-letter sizing is needed.
fn dispatchParse(card: artifact.CardLine, decode_buf: []u8) void {
    switch (card.letter) {
        'A' => _ = artifact.parseACard(card, decode_buf),
        'B' => _ = artifact.parseBCard(card),
        'C' => _ = artifact.parseCCard(card, decode_buf),
        'D' => _ = artifact.parseDCard(card),
        'E' => _ = artifact.parseECard(card),
        'F' => _ = artifact.parseFCard(card, decode_buf),
        'G' => _ = artifact.parseGCard(card),
        'H' => _ = artifact.parseHCard(card, decode_buf),
        'I' => _ = artifact.parseICard(card),
        'J' => _ = artifact.parseJCard(card, decode_buf),
        'K' => _ = artifact.parseKCard(card),
        'L' => _ = artifact.parseLCard(card, decode_buf),
        'M' => _ = artifact.parseMCard(card),
        'N' => _ = artifact.parseNCard(card, decode_buf),
        'P' => _ = artifact.parsePCard(card),
        'Q' => _ = artifact.parseQCard(card),
        else => unreachable, // letter always drawn from parseable_letters
    }
}

/// Every `parseXCard` on a structured-or-garbage card line never crashes:
/// only a declared per-card struct or null comes back (§4.7.2-§4.7.14).
///
/// No reachability assertion: this is sixteen heterogeneous per-card parsers
/// (optionals and error unions over distinct struct types), not one binary
/// outcome — there is no single accept/reject split to require, and each
/// card's accept and reject arms are pinned by its own deterministic §4.7.x
/// test in artifact.zig.
pub fn parseStructured(gpa: std.mem.Allocator, seed: u64, rounds: usize) !void {
    _ = gpa;
    var prng = std.Random.DefaultPrng.init(seed);
    const rand = prng.random();
    var round: usize = 0;
    while (round < rounds) : (round += 1) {
        var argbuf: [256]u8 = undefined;
        var decode_buf: [256]u8 = undefined;
        const letter = parseable_letters[rand.uintLessThan(usize, parseable_letters.len)];
        const args: ?[]const u8 = if (rand.boolean()) null else argbuf[0..genTokens(rand, &argbuf)];
        const card: artifact.CardLine = .{ .letter = letter, .args = args };
        dispatchParse(card, decode_buf[0..if (args) |a| a.len else 0]);
    }
}

// ---------------------------------------------------------------------------
// cardLine — §4.2 tokenizeCardLine.
// ---------------------------------------------------------------------------

/// Fill `buf` with a well-formed line for `tokenizeCardLine`'s precondition
/// (non-empty, no embedded LF, terminated by exactly one LF); content before
/// the LF is otherwise arbitrary. Returns the written length.
fn genLine(rand: std.Random, buf: []u8) usize {
    const body_len = 1 + rand.uintLessThan(usize, buf.len - 1);
    for (buf[0 .. body_len - 1]) |*b| {
        b.* = rand.int(u8);
        if (b.* == '\n') b.* = ' ';
    }
    buf[body_len - 1] = '\n';
    return body_len;
}

/// `tokenizeCardLine` on an arbitrary well-formed line never crashes, and
/// any returned `args` is a subslice of the input (§4.2).
///
/// No reachability assertion: `tokenize` needs a valid card letter in a
/// specific shape that a random first byte rarely forms — measured 38/20_000
/// tokenized at seed 1 — so the accept arm is too thin to floor robustly, and
/// it is pinned by the deterministic §4.2 tokenizer tests.
pub fn cardLine(gpa: std.mem.Allocator, seed: u64, rounds: usize) !void {
    _ = gpa;
    var prng = std.Random.DefaultPrng.init(seed);
    const rand = prng.random();
    var round: usize = 0;
    while (round < rounds) : (round += 1) {
        var buf: [128]u8 = undefined;
        const len = genLine(rand, &buf);
        const line = buf[0..len];
        if (artifact.tokenizeCardLine(line)) |card| {
            try std.testing.expect(artifact.isCardLetter(card.letter));
            if (card.args) |a| try std.testing.expect(a.len <= line.len);
        }
    }
}

// ---------------------------------------------------------------------------
// escapes — §4.3 decodeEscapes.
// ---------------------------------------------------------------------------

/// `decodeEscapes` on an arbitrary token never crashes; the decoded length
/// never exceeds the token's, and a backslash-free token decodes to itself
/// (§4.3).
///
/// No reachability assertion: `decodeEscapes` is total — it always returns a
/// decoded slice, there is no accept/reject arm to partition; the invariant is
/// the length/identity bound.
pub fn escapes(gpa: std.mem.Allocator, seed: u64, rounds: usize) !void {
    _ = gpa;
    var prng = std.Random.DefaultPrng.init(seed);
    const rand = prng.random();
    var round: usize = 0;
    while (round < rounds) : (round += 1) {
        var tok_buf: [128]u8 = undefined;
        var out_buf: [128]u8 = undefined;
        const len = rand.uintLessThan(usize, tok_buf.len + 1);
        fillGarbage(rand, tok_buf[0..len]);
        const token = tok_buf[0..len];
        const out = artifact.decodeEscapes(token, out_buf[0..len]);
        try std.testing.expect(out.len <= token.len);
        if (std.mem.indexOfScalar(u8, token, '\\') == null) {
            try std.testing.expectEqualSlices(u8, token, out);
        }
    }
}

// ---------------------------------------------------------------------------
// timestamp — §4.6.4 classifyTimestamp.
// ---------------------------------------------------------------------------

/// `classifyTimestamp` on arbitrary bytes never crashes (§4.6.4).
///
/// No reachability assertion: of the five `TimestampForm`s only `.julian` (a
/// bare digit run) is reachable from random bytes — measured 29 julian /
/// 19_971 null per 20_000 at seed 1, and zero date/time/date_time/relative,
/// which need `-`/`:`/`T` structure random input essentially never forms. One
/// thin arm plus null is not a partition worth flooring; the five forms are
/// pinned by the deterministic §4.6.4 classify tests.
pub fn timestamp(gpa: std.mem.Allocator, seed: u64, rounds: usize) !void {
    _ = gpa;
    var prng = std.Random.DefaultPrng.init(seed);
    const rand = prng.random();
    var round: usize = 0;
    while (round < rounds) : (round += 1) {
        var buf: [64]u8 = undefined;
        const len = rand.uintLessThan(usize, buf.len + 1);
        fillGarbage(rand, buf[0..len]);
        _ = artifact.classifyTimestamp(buf[0..len]);
    }
}

// ---------------------------------------------------------------------------
// framing — §4.1/§4.1.1/§6.2 frame, stripSignatureWrapper, verifyZCard.
// ---------------------------------------------------------------------------

/// The framing trio on arbitrary bytes never crashes; `stripSignatureWrapper`
/// and `frame` never grow their input (§4.1.1, §4.1).
///
/// No reachability assertion: random bytes never form a §4.1-framable body —
/// measured `frame` returned null 20_000/20_000 at seed 1 — and `verifyZCard`
/// only ever reaches `.absent` (plus a 2/20_000 incidental `.mismatch`), never
/// `.match`, which needs a real Z card. The accept and match arms are pinned by
/// `createRoundTrip` and the deterministic §6.2 verify tests.
pub fn framing(gpa: std.mem.Allocator, seed: u64, rounds: usize) !void {
    _ = gpa;
    var prng = std.Random.DefaultPrng.init(seed);
    const rand = prng.random();
    var round: usize = 0;
    while (round < rounds) : (round += 1) {
        var buf: [256]u8 = undefined;
        const len = rand.uintLessThan(usize, buf.len + 1);
        fillGarbage(rand, buf[0..len]);
        const bytes = buf[0..len];

        const stripped = artifact.stripSignatureWrapper(bytes);
        try std.testing.expect(stripped.len <= bytes.len);
        if (artifact.frame(bytes)) |framed| try std.testing.expect(framed.len <= bytes.len);
        _ = artifact.verifyZCard(bytes);
    }
}

// ---------------------------------------------------------------------------
// predicates — §4.5.3/§4.6 compare*, isSimplePathname, isWikiName, isReservedFilename.
// ---------------------------------------------------------------------------

fn reverseOrder(o: std.math.Order) std.math.Order {
    return switch (o) {
        .lt => .gt,
        .gt => .lt,
        .eq => .eq,
    };
}

/// The pathname/wiki-name/reserved-name/compare predicates on arbitrary
/// bytes never crash; `compareBytes` is reflexive and its swap is exactly
/// order-reversing (§4.5.3, §4.6.1-§4.6.3).
///
/// Reachability, asserted rather than assumed (the `serve_fuzz` idea): a run
/// must reach BOTH accept and reject for `isSimplePathname` and `isWikiName`,
/// or the generator has drifted so a predicate's true (or false) arm is never
/// exercised. Measured at seed 1, 20_000 rounds: simple-pathname 435 reject /
/// 19_565 accept, wiki-name 17_007 reject / 2_993 accept; at 512 rounds the
/// thinnest arm (simple-pathname reject) holds ≥6 across seeds 1-3, the floor.
/// `isReservedFilename` is deliberately excluded: it is true only for a fixed
/// name set random bytes never hit (measured 0/20_000 accept), so its accept
/// arm is unreachable here and is pinned by the deterministic §4.6.3 tests.
const predicates_min_rounds: usize = 512;
pub fn predicates(gpa: std.mem.Allocator, seed: u64, rounds: usize) !void {
    _ = gpa;
    var prng = std.Random.DefaultPrng.init(seed);
    const rand = prng.random();
    var sp = [_]usize{0} ** 2;
    var wiki = [_]usize{0} ** 2;
    var round: usize = 0;
    while (round < rounds) : (round += 1) {
        var abuf: [64]u8 = undefined;
        var bbuf: [64]u8 = undefined;
        const alen = rand.uintLessThan(usize, abuf.len + 1);
        const blen = rand.uintLessThan(usize, bbuf.len + 1);
        fillGarbage(rand, abuf[0..alen]);
        fillGarbage(rand, bbuf[0..blen]);
        const a = abuf[0..alen];
        const b = bbuf[0..blen];

        sp[@intFromBool(artifact.isSimplePathname(a))] += 1;
        wiki[@intFromBool(artifact.isWikiName(a))] += 1;
        _ = artifact.isReservedFilename(a);

        try std.testing.expectEqual(std.math.Order.eq, artifact.compareBytes(a, a));
        try std.testing.expectEqual(artifact.compareBytes(a, b), reverseOrder(artifact.compareBytes(b, a)));
    }
    if (rounds >= predicates_min_rounds and
        (sp[0] == 0 or sp[1] == 0 or wiki[0] == 0 or wiki[1] == 0))
    {
        std.debug.print(
            "artifact_predicates: {d} rounds reached simple-pathname[reject={d} accept={d}] " ++
                "wiki-name[reject={d} accept={d}] — a predicate no longer sees both arms\n",
            .{ rounds, sp[0], sp[1], wiki[0], wiki[1] },
        );
        return error.OutcomeUnreached;
    }
}

// ---------------------------------------------------------------------------
// createRoundTrip — §6/§4 create(), the writer.
// ---------------------------------------------------------------------------

const hex_chars = "0123456789abcdef";

/// Fill `buf` with random lowercase hex.
fn fillHex(rand: std.Random, buf: []u8) void {
    for (buf) |*b| b.* = hex_chars[rand.uintLessThan(usize, 16)];
}

/// Assemble a small, always-parseable MANIFEST-type artifact from a bounded
/// pool of random-but-valid card choices, with a correctly computed Z
/// checksum -- structured generation, not blind noise, so `create`'s
/// per-card writers actually get exercised (pure noise essentially never
/// satisfies the Z-checksum gate, so `parseGarbage`'s approach would drive
/// almost no traffic through `create` at all). Always emits at least one F
/// card, pinning classification to MANIFEST deterministically (§5.3) so the
/// rest of the pool (C/P/R/T/U) stays free to vary without touching §5.4's
/// per-type semantic checks.
fn genManifestCandidate(rand: std.Random, out: *std.ArrayList(u8), allocator: std.mem.Allocator) !void {
    out.clearRetainingCapacity();
    if (rand.boolean()) try out.appendSlice(allocator, "C generated\\stest\\scase\n");
    try out.appendSlice(allocator, "D 2026-07-02T11:18:42.940\n");

    const f_count = rand.uintLessThan(usize, 4) + 1; // >=1: keeps classification deterministic
    var i: usize = 0;
    while (i < f_count) : (i += 1) {
        var hbuf: [64]u8 = undefined;
        const hlen: usize = if (rand.boolean()) 40 else 64;
        fillHex(rand, hbuf[0..hlen]);
        try out.print(allocator, "F f{d:0>3}.txt {s}\n", .{ i, hbuf[0..hlen] });
    }

    if (rand.boolean()) {
        try out.appendSlice(allocator, "P\n");
    } else {
        var pbuf: [64]u8 = undefined;
        const plen: usize = if (rand.boolean()) 40 else 64;
        fillHex(rand, pbuf[0..plen]);
        try out.print(allocator, "P {s}\n", .{pbuf[0..plen]});
    }

    if (rand.boolean()) {
        var rbuf: [32]u8 = undefined;
        fillHex(rand, &rbuf);
        try out.print(allocator, "R {s}\n", .{rbuf});
    }

    if (rand.boolean()) {
        var tbuf: [64]u8 = undefined;
        const tlen: usize = if (rand.boolean()) 40 else 64;
        fillHex(rand, tbuf[0..tlen]);
        try out.print(allocator, "T +tag {s}\n", .{tbuf[0..tlen]});
    }

    if (rand.boolean()) try out.appendSlice(allocator, "U tester\n");

    var digest: [32]u8 = undefined;
    _ = artifact.selfChecksum(out.items, &digest);
    try out.print(allocator, "Z {s}\n", .{digest});
}

/// `create` on every generated-parseable candidate never crashes or leaks,
/// and its output is always itself re-parseable (§6/§4 writer round-trip)
/// with a matching self-checksum (§6.2), and a second `create` over the
/// re-parsed data is byte-identical to the first (`create` is a pure
/// function of the Manifest's data, §6.3.4).
///
/// No reachability assertion: every candidate is generated parseable, so a
/// successful create/parse/create round-trip is the only outcome — there is no
/// reject arm to reach (a candidate that fails to parse is skipped, not an
/// outcome the target is testing).
pub fn createRoundTrip(gpa: std.mem.Allocator, seed: u64, rounds: usize) !void {
    var prng = std.Random.DefaultPrng.init(seed);
    const rand = prng.random();
    var gen: std.ArrayList(u8) = .empty;
    defer gen.deinit(gpa);

    var round: usize = 0;
    while (round < rounds) : (round += 1) {
        try genManifestCandidate(rand, &gen, gpa);
        var m = artifact.parse(gpa, gen.items) catch continue;
        defer m.deinit();

        const out1 = try artifact.create(&m, gpa);
        defer gpa.free(out1);
        try std.testing.expectEqual(artifact.ZVerdict.match, artifact.verifyZCard(out1));

        var m1 = try artifact.parse(gpa, out1);
        defer m1.deinit();

        const out2 = try artifact.create(&m1, gpa);
        defer gpa.free(out2);
        try std.testing.expectEqualSlices(u8, out1, out2);
    }
}