import { readdirSync, readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { assertProperty, fc } from "./property.ts"; export { fc }; /** Hostile strings: arbitrary UTF-16 units, control chars, long runs. */ export const hostileString: fc.Arbitrary = fc.string({ unit: "binary", maxLength: 8192, }); export interface FuzzOptions { seed?: number; numRuns?: number; /** Documented error types that are acceptable rejections, not findings. */ allow?: ReadonlyArray Error>; } /** * Fuzz a parser over hostile string input. Any throw outside `allow` is a * finding: fast-check shrinks it to a minimal input and `assertProperty` stores * it under `/regressions/`, where the paired replay test picks it up. */ export function fuzz( targetFile: string, name: string, exercise: (input: string) => void, options: FuzzOptions = {}, ): void { const allow = options.allow ?? []; assertProperty( targetFile, name, fc.property(hostileString, (input) => { try { exercise(input); return true; } catch (error) { return ( error instanceof Error && allow.some((kind) => error instanceof kind) ); } }), { ...(options.seed !== undefined ? { seed: options.seed } : {}), numRuns: options.numRuns ?? 2000, }, ); } /** Minimal inputs stored by earlier fuzz runs beside `targetFile`. */ export function fuzzRegressions(targetFile: string): string[] { const dir = join(dirname(fileURLToPath(targetFile)), "regressions"); try { return readdirSync(dir) .filter((file) => file.endsWith(".json")) .map((file) => { const record = JSON.parse(readFileSync(join(dir, file), "utf8")) as { counterexample?: unknown[]; }; return record.counterexample?.[0]; }) .filter((input): input is string => typeof input === "string"); } catch { return []; } } /** Re-run a stored input; rethrow only errors outside the documented `allow` set. */ export function replay( exercise: (input: string) => void, allow: ReadonlyArray Error>, input: string, ): void { try { exercise(input); } catch (error) { if ( !(error instanceof Error && allow.some((kind) => error instanceof kind)) ) throw error; } } /** True when run directly as `node .fuzz.ts`, not when imported. */ export function isMain(importMetaUrl: string): boolean { const entry = process.argv[1]; return entry ? importMetaUrl === `file://${entry}` : false; }