//! Seeded fuzz-target runner.
//!
//! `zig build fuzz -- <target> [--seed N] [--rounds N]`
//!
//! One binary; targets are named-tuple fields dispatched through
//! `std.meta.FieldEnum`, so a module registers a new target by adding one
//! field to `targets` below. The seed is explicit (`--seed`, decimal or
//! `0x`-prefixed) or drawn from the process's csprng; either way it is
//! always printed as `reproduce with seed=0x...` before the target runs, so
//! a failure replays exactly. `--rounds` bounds the run explicitly (default
//! 10_000); an out-of-band runner sweeps higher counts.
const std = @import("std");
const delta_fuzz = @import("delta_fuzz.zig");
const artifact_fuzz = @import("artifact_fuzz.zig");
/// One field per fuzz target: `pub fn(allocator, seed: u64, rounds: usize) !void`.
/// The field name is the argv target name.
const targets = .{
.delta_apply_garbage = delta_fuzz.applyGarbage,
.delta_round_trip = delta_fuzz.roundTrip,
.delta_decode_int_bounded = delta_fuzz.decodeIntBounded,
.delta_target_size_garbage = delta_fuzz.targetSizeGarbage,
.delta_create_large_sizes = delta_fuzz.createLargeSizes,
.artifact_parse_garbage = artifact_fuzz.parseGarbage,
.artifact_parse_structured = artifact_fuzz.parseStructured,
.artifact_card_line = artifact_fuzz.cardLine,
.artifact_escapes = artifact_fuzz.escapes,
.artifact_timestamp = artifact_fuzz.timestamp,
.artifact_framing = artifact_fuzz.framing,
.artifact_predicates = artifact_fuzz.predicates,
};
const Target = std.meta.FieldEnum(@TypeOf(targets));
const default_rounds: usize = 10_000;
pub fn main(init: std.process.Init) !void {
const gpa = init.gpa;
const args = try init.minimal.args.toSlice(init.arena.allocator());
var stderr_buffer: [1024]u8 = undefined;
var stderr_writer: std.Io.File.Writer = .init(.stderr(), init.io, &stderr_buffer);
const err = &stderr_writer.interface;
if (args.len < 2) {
try usage(err);
try err.flush();
std.process.exit(2);
}
const target = std.meta.stringToEnum(Target, args[1]) orelse {
try err.print("fuzz: unknown target '{s}'\n\n", .{args[1]});
try usage(err);
try err.flush();
std.process.exit(2);
};
var seed: u64 = undefined;
init.io.random(std.mem.asBytes(&seed));
var rounds: usize = default_rounds;
try parseFlags(args[2..], &seed, &rounds);
try err.print("reproduce with seed=0x{x}\n", .{seed});
try err.flush();
switch (target) {
inline else => |tag| try @field(targets, @tagName(tag))(gpa, seed, rounds),
}
}
/// Parse `[--seed N] [--rounds N]` in any order, overwriting the defaults
/// already in `seed`/`rounds`. Each flag consumes exactly one value; an
/// unknown flag or a missing value is an explicit error, never ignored.
fn parseFlags(args: []const []const u8, seed: *u64, rounds: *usize) !void {
var i: usize = 0;
while (i < args.len) : (i += 1) {
if (std.mem.eql(u8, args[i], "--seed")) {
i += 1;
if (i >= args.len) return error.MissingSeedValue;
seed.* = try std.fmt.parseInt(u64, args[i], 0);
} else if (std.mem.eql(u8, args[i], "--rounds")) {
i += 1;
if (i >= args.len) return error.MissingRoundsValue;
rounds.* = try std.fmt.parseInt(usize, args[i], 0);
} else {
return error.UnknownArgument;
}
}
}
fn usage(w: *std.Io.Writer) !void {
try w.writeAll("usage: zig build fuzz -- <target> [--seed N] [--rounds N]\n\ntargets:\n");
inline for (@typeInfo(Target).@"enum".fields) |f| {
try w.print(" {s}\n", .{f.name});
}
}