//! 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.
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).
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)));
}
}