fig-sys 3.0.3

FFI bindings and native library for fig (the comment-preserving JSON/YAML/TOML/… config engine). Used by the `fig` crate.
Documentation
//! Fuzz targets for the hand-written tokenizers and parsers.
//!
//! Test-only: this module is reachable solely from `root.zig`'s `test {}` block,
//! so nothing here is analyzed in a library/CLI/wasm build. That is also what
//! makes the canonical oracle below unconditionally available — every call site
//! gates it as `lang_canonical or @import("builtin").is_test` (see
//! `ast/serialize_options.zig`), and `is_test` always holds here, so the target
//! needs no `-Dcanonical=true`.
//!
//! Two targets, deliberately split by what they can prove:
//!
//!   * `detect` — BREADTH. `Language.detect` runs every compiled-in parser over
//!     the same bytes (see its ordering note), so one target sweeps all eleven
//!     grammars for crashes, hangs, and leaks. It cannot check correctness: the
//!     result is just an enum, and each probe throws its Document away.
//!
//!   * canonical round-trip — DEPTH. The canonical form is a bijection with the
//!     AST (`canonical/canonical.zig`), which is the one property strong enough
//!     to catch a SILENT mis-parse — a parser that accepts input and builds the
//!     wrong tree. `detect` would never notice.
//!
//! Neither asserts `parse -> print == original`: printers render from the AST,
//! not the borrowed `Document.source`, so byte-identical output is not a
//! property of this codebase and asserting it would only encode today's
//! formatting. The reprint check below is idempotence — a weaker, true claim.
//!
//! Run: `zig build test` runs both as deterministic smoke tests (the empty-tape
//! case, plus any corpus). Real fuzzing needs the `fuzz` step.
//!
//! On Zig 0.16's fuzz API: `testOne` receives a `*std.testing.Smith` — a
//! structured generator — NOT the input slice older versions passed. The bytes
//! under test are synthesized here, from Smith decisions. The corollary is a
//! trap worth stating outright: `FuzzInputOptions.corpus` entries are Smith
//! DECISION TAPES, not source text, so the fixtures under `testdata/` cannot be
//! seeded as a corpus — they are ordinary files, and the conformance suites
//! already read them as such. Modeled on `std/zig/tokenizer.zig`'s own fuzz test.

const std = @import("std");

const Canonical = @import("canonical/canonical.zig");
const Language = @import("languages/language.zig");

/// Byte weights shaping random input into something the parsers will actually
/// walk into rather than reject on the first token. The first entry must span
/// the full range (`Smith` asserts every later entry falls inside it) and stays
/// at weight 1: it is what still reaches the tokenizers' invalid-UTF-8 and
/// control-byte paths, which are exactly where a hand-written scanner tends to
/// run off the end of its buffer.
///
/// Everything after it is a structural character these grammars branch on,
/// weighted up because a uniform random byte string is almost all rejected at
/// depth 0 and proves nothing about the parser bodies.
const structural_weights: []const std.testing.Smith.Weight = &.{
    .rangeAtMost(u8, 0x00, 0xff, 1), // full span: invalid UTF-8, NUL, control bytes
    .rangeAtMost(u8, 0x20, 0x7e, 8), // printable ASCII: keys, values, bare scalars
    // Newline carries more grammar here than any other byte: INI, dotenv,
    // `.properties`, YAML, NestedText and fig are all line-oriented, and a
    // tokenizer bug at a line boundary is the common case.
    .value(u8, '\n', 6),
    // Indentation. YAML and NestedText derive STRUCTURE from it, so without
    // leading whitespace their block paths are unreachable.
    .value(u8, ' ', 5),
    .value(u8, '\t', 3), // and tabs, which several of those formats reject
    // Assignment/separator: TOML, INI, dotenv, `.properties`, fig, ZON.
    .value(u8, '=', 4),
    .value(u8, ':', 4), // JSON, YAML, NestedText
    // Bracket pairs: JSON/ZON arrays, TOML/INI section headers, fig groups.
    .value(u8, '[', 4),
    .value(u8, ']', 4),
    .value(u8, '{', 4),
    .value(u8, '}', 4),
    .value(u8, '"', 4), // quoted-string paths (escape handling) in every format
    .value(u8, ',', 3), // JSON/ZON separators
    .value(u8, '<', 3), // XML/plist tags
    .value(u8, '>', 3), // and fig's `>` section depth
    .value(u8, '#', 3), // comments: TOML, YAML, INI, dotenv, fig
    .value(u8, '-', 3), // YAML sequence entries
    .value(u8, '.', 2), // TOML dotted keys, ZON's leading `.`
    .value(u8, '&', 2), // YAML anchors
    .value(u8, '*', 2), // YAML aliases, fig elements
    .value(u8, '\\', 2), // escapes, and INI's unquoted `C:\a\b` values
};

/// The canonical grammar is a closed, JSON-like syntax, so it gets its own
/// narrower weights: the line-oriented and XML bytes above are noise here and
/// would spend the budget on input the parser rejects at the first token.
const canonical_weights: []const std.testing.Smith.Weight = &.{
    .rangeAtMost(u8, 0x00, 0xff, 1),
    .rangeAtMost(u8, 0x20, 0x7e, 8),
    .value(u8, '"', 5), // strings, and the escape decoder behind them
    .value(u8, '{', 4),
    .value(u8, '}', 4),
    .value(u8, '[', 4),
    .value(u8, ']', 4),
    .value(u8, ':', 4),
    .value(u8, ',', 4),
    .value(u8, '@', 3), // extended scalars: `@offset_datetime`, `@char_literal`, …
    .value(u8, '&', 2), // anchors
    .value(u8, '*', 2), // aliases
    .value(u8, '!', 2), // tags
    .value(u8, '~', 2), // the kind override (`~f`)
    .value(u8, '\n', 2), // comments terminate on it
    .value(u8, '/', 2), // and start with `//` or `/*`
};

/// Big enough to nest past the parsers' `max_depth` guards (512 for canonical)
/// so the recursion limits themselves get exercised, and to hold a document
/// with real structure rather than a single truncated token. Stack-allocated:
/// the fuzzer reruns `testOne` in a tight loop, so the input buffer should not
/// be a per-iteration heap allocation competing with the parser's own.
const max_input = 2048;

/// Breadth target: `Language.detect` tries every compiled-in parser in turn
/// (`languages/language.zig`), discarding each Document, so this one call
/// exercises all of them. The assertion is everything the test runner does
/// implicitly around it — no crash, no hang, no leak (each iteration gets a
/// fresh `testing.allocator` instance and fails on a leak) — which is why the
/// body ignores the result: `detect` returning null for random bytes is the
/// expected case, not a failure.
fn detectNeverCrashes(_: void, smith: *std.testing.Smith) anyerror!void {
    // The fuzzer must not see this harness's own coverage; only the parsers'.
    @disableInstrumentation();

    var buf: [max_input]u8 = undefined;
    const len = smith.sliceWeightedBytes(&buf, structural_weights);

    // `tryParse` frees every Document it builds, so a leak reported here is a
    // real one inside a parser's error path, not a harness artifact.
    _ = Language.detect(std.testing.allocator, buf[0..len]);
}

test "fuzz: detect never crashes or leaks on arbitrary input" {
    try std.testing.fuzz({}, detectNeverCrashes, .{});
}

/// Depth target: the canonical form claims to be a 1:1 encoding of the AST, so
/// for any input it ACCEPTS, printing and reparsing must land on the same tree.
/// This is the fuzzable generalization of `expectRoundTrip` in
/// `canonical/parser.zig` — same three assertions, arbitrary input instead of
/// hand-written fixtures.
///
/// The property is conditional on a successful parse: a rejected input proves
/// nothing about a bijection, so it returns rather than fails. What it does NOT
/// forgive is the reparse — the printer's output is canonical text by
/// construction, so if it fails to parse, the bijection is broken and that is a
/// bug, not bad input.
fn canonicalRoundTrips(_: void, smith: *std.testing.Smith) anyerror!void {
    @disableInstrumentation();

    const allocator = std.testing.allocator;

    var buf: [max_input]u8 = undefined;
    const len = smith.sliceWeightedBytes(&buf, canonical_weights);

    // Most random input is not canonical text; `NestingTooDeep` from the depth
    // guard lands here too, which is the guard working.
    var ast = Canonical.parseAbstract(allocator, buf[0..len]) catch return;
    defer ast.deinit();

    var first: std.Io.Writer.Allocating = .init(allocator);
    defer first.deinit();
    // Total over every node kind, and the parser's guard matches the printer's,
    // so anything parsed above prints — a failure here is a real defect.
    try Canonical.print(&first.writer, &ast);

    var reparsed = try Canonical.parseAbstract(allocator, first.written());
    defer reparsed.deinit();

    // The bijection itself: same tree in, same tree out.
    try std.testing.expect(ast.eql(reparsed));
    // `eql` deliberately ignores comments, so they need their own check or the
    // canonical form would be free to silently drop them.
    try std.testing.expect(ast.commentsEql(reparsed));

    // Idempotence. Only from the SECOND print on: the first print normalizes
    // whatever spelling the input used (spacing, comment placement), so
    // comparing it to the input would be false. Comparing print(parse(print(x)))
    // to print(x) is the real claim — the canonical form has one spelling per
    // document, so re-rendering a reparsed tree must be byte-identical.
    var second: std.Io.Writer.Allocating = .init(allocator);
    defer second.deinit();
    try Canonical.print(&second.writer, &reparsed);
    try std.testing.expectEqualStrings(first.written(), second.written());
}

test "fuzz: canonical print output reparses to an equal AST" {
    try std.testing.fuzz({}, canonicalRoundTrips, .{});
}