Artifact 2d82d74528460e123999e78a8b20c189dffdc56ed9cec50ef620ffca04eab33a:
- File
src/artifact_fuzz.zig
— part of check-in
[e46e97cb76]
at
2026-07-09 20:02:22
on branch trunk
— Give the artifact card grammar the same seeded fuzz coverage as delta
artifact_fuzz.zig adds seven parameterized targets registered in fuzz_tests.zig: parse() on raw garbage, a structured generator that dispatches through all sixteen parseXCard entry points, tokenizeCardLine, decodeEscapes, classifyTimestamp, the frame/stripSignatureWrapper/ verifyZCard trio, and the pathname/wiki-name/reserved/compare predicates. artifact.zig gains matching fixed-seed inline blocks plus a parse() allocation-failure test, mirroring delta.zig's existing fuzz section. (user: dmestas, size: 12296) [annotate] [blame] [check-ins using] [more...]
//! 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. `parse()` is presently a stub (`ParseError.NotImplemented` //! for any input); `parseStructured` reaches the sixteen `parseXCard` entry //! points directly, since those -- not yet `parse` -- are where the format's //! real per-card logic lives today. Once the §4.2 card-loop lands in `parse`, //! `parseGarbage` starts exercising it directly with no changes needed here. 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. 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]); _ = artifact.parse(gpa, buf[0..len]) catch {}; } } // --------------------------------------------------------------------------- // 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). 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). 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). 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). 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). 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). 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 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]; _ = artifact.isSimplePathname(a); _ = artifact.isWikiName(a); _ = 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))); } }