-module(thrifty@fuzz_cli). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/thrifty/fuzz_cli.gleam"). -export([main/0]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. -file("src/thrifty/fuzz_cli.gleam", 155). ?DOC(" Attempt to save a failing payload and a small metadata file.\n"). -spec maybe_save_failure( boolean(), binary(), integer(), integer(), bitstring(), thrifty@types:decode_error() ) -> {ok, nil} | {error, binary()}. maybe_save_failure(Enabled, Dir, Seed, Iter, Data, Reason) -> case Enabled of false -> {ok, nil}; true -> thrifty@fuzz_persistence:persist_failure( Dir, Seed, Iter, Data, Reason ) end. -file("src/thrifty/fuzz_cli.gleam", 179). ?DOC( " Core fuzz loop that mutates a single byte and invokes the reader.\n" "\n" " Notes\n" " - This harness is purposely conservative: it uses `with_options` to limit\n" " resources and discards reader results. Extend to persist failing inputs\n" " when required.\n" ). -spec normalize_mod(integer(), integer()) -> integer(). normalize_mod(Value, Modulus) -> Rem = case Modulus of 0 -> 0; Gleam@denominator -> Value rem Gleam@denominator end, case Rem < 0 of true -> Rem + Modulus; false -> Rem end. -file("src/thrifty/fuzz_cli.gleam", 113). ?DOC( " Run deterministic single-byte mutation iterations for a single seed.\n" "\n" " Inputs\n" " - `data`: original payload.\n" " - `size`: payload byte size.\n" " - `seed`: deterministic seed controlling mutation positions/values.\n" " - `iterations`: number of mutations to generate for this seed.\n" ). -spec fuzz_loop(bitstring(), integer(), integer(), integer(), integer()) -> nil. fuzz_loop(Data, Size, Seed, Index, Remaining) -> case Remaining =:= 0 of true -> nil; false -> Position = normalize_mod((Seed * 31) + (Index * 17), Size), Value = normalize_mod((Seed * 97) + (Index * 101), 256), Mutated = thrifty@fuzz_utils:mutate_byte(Data, Position, Value), Reader = thrifty@reader:with_options( Mutated, {reader_options, 32, 4096, 4194304, accept_canonical_only} ), case thrifty@reader:read_struct(Reader) of {ok, _} -> nil; {error, Err} -> _ = maybe_save_failure( false, <<"artifact/fuzz-failures"/utf8>>, Seed, Index, Mutated, Err ), nil end, fuzz_loop(Data, Size, Seed, Index + 1, Remaining - 1) end. -file("src/thrifty/fuzz_cli.gleam", 102). ?DOC( " Prepare fuzzing state for a given payload and spawn seed-based iterations.\n" "\n" " Inputs\n" " - `file`: path to the golden file (used only for logging).\n" " - `data`: payload bytes to mutate.\n" "\n" " Behavior\n" " - Skips empty payloads. Logs size and iterates `seeds` invoking `fuzz_with_seed`.\n" ). -spec fuzz_with_seed(bitstring(), integer(), integer(), integer()) -> nil. fuzz_with_seed(Data, Size, Seed, Iterations) -> fuzz_loop(Data, Size, Seed, 0, Iterations). -file("src/thrifty/fuzz_cli.gleam", 81). ?DOC( " Read a golden file and trigger fuzzing of its payloads.\n" "\n" " Inputs\n" " - `file`: path to a golden binary file.\n" "\n" " Behavior\n" " - On read error prints a message and continues. On success delegates to\n" " `fuzz_payloads/2` to run deterministic mutations.\n" ). -spec fuzz_payloads(binary(), bitstring()) -> nil. fuzz_payloads(File, Data) -> Size = erlang:byte_size(Data), case Size =:= 0 of true -> gleam_stdlib:println( <<"Skipping empty payload: "/utf8, File/binary>> ); false -> gleam_stdlib:println( <<<<<<<<"Fuzzing "/utf8, File/binary>>/binary, " ("/utf8>>/binary, (erlang:integer_to_binary(Size))/binary>>/binary, " bytes)"/utf8>> ), gleam@list:each( [13, 29, 41, 53, 67, 79, 97, 109], fun(Seed) -> fuzz_with_seed(Data, Size, Seed, 200) end ) end. -file("src/thrifty/fuzz_cli.gleam", 66). -spec fuzz_file(binary()) -> nil. fuzz_file(File) -> case thrifty@file_io:read_binary(File) of {error, Error} -> gleam_stdlib:println( <<<<<<"Skipping "/utf8, File/binary>>/binary, ": "/utf8>>/binary, Error/binary>> ); {ok, Data} -> fuzz_payloads(File, Data) end. -file("src/thrifty/fuzz_cli.gleam", 60). ?DOC( " Run a short deterministic fuzz run over the configured golden payloads.\n" "\n" " Purpose\n" " - Convenience entrypoint for manual or CI smoke tests that exercise the\n" " reader against small, mutated variants of canonical golden payloads.\n" "\n" " Behavior\n" " - Iterates the predefined `files` list, reads each binary, and applies a set\n" " of deterministic single-byte mutations controlled by `seeds` and\n" " `iterations_per_seed`.\n" " - Each mutated payload is passed to the reader created with conservative\n" " `ReaderOptions` to verify the reader does not crash on malformed inputs.\n" "\n" " Outputs\n" " - Prints progress to stdout. This function is intended as a smoke-test only\n" " and does not return structured results; callers should inspect logs or\n" " extend the harness to persist failing cases.\n" ). -spec main() -> nil. main() -> gleam_stdlib:println(<<"Starting fuzz run over golden payloads"/utf8>>), gleam@list:each( [<<"artifact/golden/user_profile.bin"/utf8>>, <<"artifact/golden/ping_message.bin"/utf8>>, <<"artifact/golden/complex_struct.bin"/utf8>>, <<"artifact/golden/bool_list.bin"/utf8>>], fun fuzz_file/1 ), gleam_stdlib:println(<<"Fuzz run completed without crashes"/utf8>>).