diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 251faa0a4..426660621 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -59,6 +59,8 @@ pub const Style = terminal.Style; pub const Terminal = terminal.Terminal; pub const Stream = terminal.Stream; pub const StreamAction = terminal.StreamAction; +pub const ReadonlyStream = terminal.ReadonlyStream; +pub const ReadonlyHandler = terminal.ReadonlyHandler; pub const Cursor = Screen.Cursor; pub const CursorStyle = Screen.CursorStyle; pub const CursorStyleReq = terminal.CursorStyle; diff --git a/test/fuzz-libghostty/.gitattributes b/test/fuzz-libghostty/.gitattributes index de57ad9a7..50dce46fd 100644 --- a/test/fuzz-libghostty/.gitattributes +++ b/test/fuzz-libghostty/.gitattributes @@ -1,6 +1,9 @@ # Hand-written seed corpus: binary files, track as-is corpus/initial/** binary +corpus/vt-stream-initial/** binary # Generated/minimized corpora: binary, mark as generated corpus/vt-parser-cmin/** binary linguist-generated=true corpus/vt-parser-min/** binary linguist-generated=true +corpus/vt-stream-cmin/** binary linguist-generated=true +corpus/vt-stream-min/** binary linguist-generated=true diff --git a/test/fuzz-libghostty/AGENTS.md b/test/fuzz-libghostty/AGENTS.md index 042173a75..daafdb887 100644 --- a/test/fuzz-libghostty/AGENTS.md +++ b/test/fuzz-libghostty/AGENTS.md @@ -1,14 +1,14 @@ # AFL++ Fuzzer for Libghostty -- `ghostty-fuzz` is a binary built with `afl-cc` -- Build `ghostty-fuzz` with `zig build` +- Fuzz targets: `fuzz-vt-parser` and `fuzz-vt-stream` +- Build all targets with `zig build` - After running `afl-cmin`/`afl-tmin`, run `corpus/sanitize-filenames.sh` before committing to replace colons with underscores (colons are invalid on Windows NTFS). ## Important: stdin-based input -The instrumented binary (`afl.c` harness) reads fuzz input from **stdin**, +The instrumented binaries (`afl.c` harness) read fuzz input from **stdin**, not from a file argument. This affects how you invoke AFL++ tools: - **`afl-fuzz`**: Uses shared-memory fuzzing automatically; `@@` works @@ -16,7 +16,7 @@ not from a file argument. This affects how you invoke AFL++ tools: - **`afl-showmap`**: Must pipe input via stdin, **not** `@@`: ```sh - cat testcase | afl-showmap -o map.txt -- zig-out/bin/ghostty-fuzz + cat testcase | afl-showmap -o map.txt -- zig-out/bin/fuzz-vt-stream ``` - **`afl-cmin`**: Do **not** use `@@`. Requires `AFL_NO_FORKSRV=1` with @@ -24,14 +24,14 @@ not from a file argument. This affects how you invoke AFL++ tools: ```sh AFL_NO_FORKSRV=1 /opt/homebrew/Cellar/afl++/4.35c/libexec/afl-cmin.bash \ - -i afl-out/default/queue -o corpus/vt-parser-cmin \ - -- zig-out/bin/ghostty-fuzz + -i afl-out/fuzz-vt-stream/default/queue -o corpus/vt-stream-cmin \ + -- zig-out/bin/fuzz-vt-stream ``` - **`afl-tmin`**: Also requires `AFL_NO_FORKSRV=1`, no `@@`: ```sh - AFL_NO_FORKSRV=1 afl-tmin -i -o -- zig-out/bin/ghostty-fuzz + AFL_NO_FORKSRV=1 afl-tmin -i -o -- zig-out/bin/fuzz-vt-stream ``` If you pass `@@` or a filename argument, `afl-showmap`/`afl-cmin`/`afl-tmin` diff --git a/test/fuzz-libghostty/README.md b/test/fuzz-libghostty/README.md index 64dccd066..4bf9d6676 100644 --- a/test/fuzz-libghostty/README.md +++ b/test/fuzz-libghostty/README.md @@ -1,9 +1,19 @@ # AFL++ Fuzzer for Libghostty -This directory contains an [AFL++](https://aflplus.plus/) fuzzing harness for -libghostty-vt (Zig module). At the time of writing this README, it only -fuzzes the VT parser, but it can be extended to cover other components of -libghostty as well. +This directory contains [AFL++](https://aflplus.plus/) fuzzing harnesses for +libghostty-vt (Zig module). + +## Fuzz Targets + +| Target | Binary | Description | +| ------------------ | ------------------ | ------------------------------------------------------- | +| `fuzz-vt-parser` | `fuzz-vt-parser` | VT parser only (`Parser.next` byte-at-a-time) | +| `fuzz-vt-stream` | `fuzz-vt-stream` | Full terminal stream (`nextSlice` + `next` via handler) | + +The stream target creates a small `Terminal` and exercises the readonly +`Stream` handler, covering printing, CSI dispatch, OSC, DCS, SGR, cursor +movement, scrolling regions, and more. The first byte of each input selects +between the slice path (SIMD fast-path) and the scalar path. ## Prerequisites @@ -21,43 +31,39 @@ From this directory (`test/fuzz-libghostty`): zig build ``` -This compiles a Zig static library (with the fuzz harness in `src/lib.zig`), -emits LLVM bitcode, then links it with `src/main.c` using `afl-cc` to produce -the instrumented binary at `zig-out/bin/ghostty-fuzz`. +This compiles Zig static libraries for each fuzz target, emits LLVM bitcode, +then links each with `afl.c` using `afl-cc` to produce instrumented binaries +at `zig-out/bin/fuzz-vt-parser` and `zig-out/bin/fuzz-vt-stream`. ## Running the Fuzzer -The build system has a convenience step that invokes `afl-fuzz` with the -correct arguments: +Each target has its own run step: ```sh -zig build run +zig build run-fuzz-vt-parser # Run the VT parser fuzzer +zig build run-fuzz-vt-stream # Run the VT stream fuzzer +zig build run # Alias for run-fuzz-vt-parser ``` -This is equivalent to: +Or invoke `afl-fuzz` directly: ```sh -afl-fuzz -i corpus/initial -o afl-out -- zig-out/bin/ghostty-fuzz @@ +afl-fuzz -i corpus/vt-stream-initial -o afl-out/fuzz-vt-stream -- zig-out/bin/fuzz-vt-stream @@ ``` -You may want to run `afl-fuzz` directly with different options -for your own experimentation. - The fuzzer runs indefinitely. Let it run for as long as you like; meaningful coverage is usually reached within a few hours, but longer runs can find deeper bugs. Press `ctrl+c` to stop the fuzzer when you're done. ## Finding Crashes and Hangs -After (or during) a run, results are written to `afl-out/default/`: +After (or during) a run, results are written to `afl-out//default/`: ``` - -afl-out/default/ +afl-out/fuzz-vt-stream/default/ ├── crashes/ # Inputs that triggered crashes -├── hangs/ # Inputs that triggered hangs/timeouts -└── queue/ # All interesting inputs (the evolved corpus) - +├── hangs/ # Inputs that triggered hangs/timeouts +└── queue/ # All interesting inputs (the evolved corpus) ``` Each file in `crashes/` or `hangs/` is a raw byte file that triggered the @@ -69,12 +75,12 @@ issue. The filename encodes metadata about how it was found (e.g. Replay any crashing input by piping it into the harness: ```sh -cat afl-out/default/crashes/ | zig-out/bin/ghostty-fuzz +cat afl-out/fuzz-vt-stream/default/crashes/ | zig-out/bin/fuzz-vt-stream ``` ## Corpus Management -After a fuzzing run, the queue in `afl-out/default/queue/` typically +After a fuzzing run, the queue in `afl-out//default/queue/` typically contains many redundant inputs. Use `afl-cmin` to find the smallest subset that preserves full edge coverage, and `afl-tmin` to shrink individual test cases. @@ -90,9 +96,9 @@ Reduce the evolved queue to a minimal set covering all discovered edges: ```sh AFL_NO_FORKSRV=1 afl-cmin.bash \ - -i afl-out/default/queue \ - -o corpus/vt-parser-cmin \ - -- zig-out/bin/ghostty-fuzz + -i afl-out/fuzz-vt-stream/default/queue \ + -o corpus/vt-stream-cmin \ + -- zig-out/bin/fuzz-vt-stream ``` `AFL_NO_FORKSRV=1` is required because the Python `afl-cmin` wrapper has @@ -105,12 +111,12 @@ Shrink each file in the minimized corpus to the smallest input that preserves its unique coverage: ```sh -mkdir -p corpus/vt-parser-min -for f in corpus/vt-parser-cmin/*; do +mkdir -p corpus/vt-stream-min +for f in corpus/vt-stream-cmin/*; do AFL_NO_FORKSRV=1 afl-tmin \ -i "$f" \ - -o "corpus/vt-parser-min/$(basename "$f")" \ - -- zig-out/bin/ghostty-fuzz + -o "corpus/vt-stream-min/$(basename "$f")" \ + -- zig-out/bin/fuzz-vt-stream done ``` @@ -130,8 +136,8 @@ rename the output files to replace colons with underscores before committing: ### Corpus directories -| Directory | Contents | -| ------------------------ | ----------------------------------------------- | -| `corpus/initial/` | Hand-written seed inputs for `afl-fuzz -i` | -| `corpus/vt-parser-cmin/` | Output of `afl-cmin` (edge-deduplicated corpus) | -| `corpus/vt-parser-min/` | Output of `afl-tmin` (individually minimized) | +| Directory | Contents | +| -------------------------- | ----------------------------------------------- | +| `corpus/initial/` | Hand-written seed inputs for vt-parser | +| `corpus/vt-parser-cmin/` | Output of `afl-cmin` (edge-deduplicated corpus) | +| `corpus/vt-stream-initial/`| Hand-written seed inputs for vt-stream | diff --git a/test/fuzz-libghostty/build.zig b/test/fuzz-libghostty/build.zig index bd6b42615..037f5cae8 100644 --- a/test/fuzz-libghostty/build.zig +++ b/test/fuzz-libghostty/build.zig @@ -1,59 +1,69 @@ const std = @import("std"); const afl = @import("afl"); +const FuzzTarget = struct { + name: []const u8, + source: []const u8, + corpus: []const u8, +}; + +const fuzz_targets = [_]FuzzTarget{ + .{ + .name = "fuzz-vt-parser", + .source = "src/fuzz_vt_parser.zig", + .corpus = "corpus/vt-parser-cmin", + }, + .{ + .name = "fuzz-vt-stream", + .source = "src/fuzz_vt_stream.zig", + .corpus = "corpus/vt-stream-initial", + }, +}; + pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); - const run_step = b.step("run", "Run the fuzzer with afl-fuzz"); + const run_step = b.step("run", "Run the default fuzzer (vt-parser) with afl-fuzz"); + + const ghostty_dep = b.lazyDependency("ghostty", .{ + .simd = false, + }); + + for (fuzz_targets, 0..) |fuzz, i| { + const target_run_step = b.step( + b.fmt("run-{s}", .{fuzz.name}), + b.fmt("Run {s} with afl-fuzz", .{fuzz.name}), + ); - // Create the C ABI library from Zig source that exports the - // API that the `afl-cc` main.c entrypoint can call into. This - // lets us just use standard `afl-cc` to fuzz test our library without - // needing to write any Zig-specific fuzzing harnesses. - const lib = lib: { - // Zig module const lib_mod = b.createModule(.{ - .root_source_file = b.path("src/lib.zig"), + .root_source_file = b.path(fuzz.source), .target = target, .optimize = optimize, }); - if (b.lazyDependency("ghostty", .{ - .simd = false, - })) |dep| { - lib_mod.addImport( - "ghostty-vt", - dep.module("ghostty-vt"), - ); + if (ghostty_dep) |dep| { + lib_mod.addImport("ghostty-vt", dep.module("ghostty-vt")); } - // C lib const lib = b.addLibrary(.{ - .name = "ghostty-fuzz", + .name = fuzz.name, .root_module = lib_mod, }); - - // Required to build properly with afl-cc lib.root_module.stack_check = false; lib.root_module.fuzz = true; - break :lib lib; - }; + const exe = afl.addInstrumentedExe(b, lib); - // Build a C entrypoint with afl-cc that links against the generated - // static Zig library. afl-cc is expected to be on the PATH. - const exe = afl.addInstrumentedExe(b, lib); + const run = afl.addFuzzerRun(b, exe, b.path(fuzz.corpus), b.path(b.fmt("afl-out/{s}", .{fuzz.name}))); - // Runner to simplify running afl-fuzz. - // Use the cmin corpus (edge-deduplicated from prior runs) so that each - // fuzzing session starts from full coverage. Switch to "corpus/initial" - // if you don't have a cmin corpus yet. - const run = afl.addFuzzerRun(b, exe, b.path("corpus/vt-parser-cmin"), b.path("afl-out")); + b.installArtifact(lib); + const exe_install = b.addInstallBinFile(exe, fuzz.name); + b.getInstallStep().dependOn(&exe_install.step); - // Install - b.installArtifact(lib); - const exe_install = b.addInstallBinFile(exe, "ghostty-fuzz"); - b.getInstallStep().dependOn(&exe_install.step); + target_run_step.dependOn(&run.step); - // Run - run_step.dependOn(&run.step); + // Default `zig build run` runs the first target (vt-parser) + if (i == 0) { + run_step.dependOn(&run.step); + } + } } diff --git a/test/fuzz-libghostty/corpus/vt-stream-initial/01-plain-text-slice b/test/fuzz-libghostty/corpus/vt-stream-initial/01-plain-text-slice new file mode 100644 index 000000000..be111345f Binary files /dev/null and b/test/fuzz-libghostty/corpus/vt-stream-initial/01-plain-text-slice differ diff --git a/test/fuzz-libghostty/corpus/vt-stream-initial/02-plain-text-scalar b/test/fuzz-libghostty/corpus/vt-stream-initial/02-plain-text-scalar new file mode 100644 index 000000000..9970aba91 --- /dev/null +++ b/test/fuzz-libghostty/corpus/vt-stream-initial/02-plain-text-scalar @@ -0,0 +1 @@ +Hello, World! diff --git a/test/fuzz-libghostty/corpus/vt-stream-initial/03-csi-cursor-sgr b/test/fuzz-libghostty/corpus/vt-stream-initial/03-csi-cursor-sgr new file mode 100644 index 000000000..3c2e99cc4 Binary files /dev/null and b/test/fuzz-libghostty/corpus/vt-stream-initial/03-csi-cursor-sgr differ diff --git a/test/fuzz-libghostty/corpus/vt-stream-initial/04-csi-erase b/test/fuzz-libghostty/corpus/vt-stream-initial/04-csi-erase new file mode 100644 index 000000000..ce1416a30 Binary files /dev/null and b/test/fuzz-libghostty/corpus/vt-stream-initial/04-csi-erase differ diff --git a/test/fuzz-libghostty/corpus/vt-stream-initial/05-osc-title-bel b/test/fuzz-libghostty/corpus/vt-stream-initial/05-osc-title-bel new file mode 100644 index 000000000..c580d9151 Binary files /dev/null and b/test/fuzz-libghostty/corpus/vt-stream-initial/05-osc-title-bel differ diff --git a/test/fuzz-libghostty/corpus/vt-stream-initial/06-osc-title-st b/test/fuzz-libghostty/corpus/vt-stream-initial/06-osc-title-st new file mode 100644 index 000000000..792485f09 Binary files /dev/null and b/test/fuzz-libghostty/corpus/vt-stream-initial/06-osc-title-st differ diff --git a/test/fuzz-libghostty/corpus/vt-stream-initial/07-dcs-decrqss b/test/fuzz-libghostty/corpus/vt-stream-initial/07-dcs-decrqss new file mode 100644 index 000000000..9e9a56b30 --- /dev/null +++ b/test/fuzz-libghostty/corpus/vt-stream-initial/07-dcs-decrqss @@ -0,0 +1 @@ +P$q"p\ \ No newline at end of file diff --git a/test/fuzz-libghostty/corpus/vt-stream-initial/08-apc b/test/fuzz-libghostty/corpus/vt-stream-initial/08-apc new file mode 100644 index 000000000..7972be99c Binary files /dev/null and b/test/fuzz-libghostty/corpus/vt-stream-initial/08-apc differ diff --git a/test/fuzz-libghostty/corpus/vt-stream-initial/09-mixed-text-csi b/test/fuzz-libghostty/corpus/vt-stream-initial/09-mixed-text-csi new file mode 100644 index 000000000..a01d1ca9e --- /dev/null +++ b/test/fuzz-libghostty/corpus/vt-stream-initial/09-mixed-text-csi @@ -0,0 +1,2 @@ +ABCDhello +world \ No newline at end of file diff --git a/test/fuzz-libghostty/corpus/vt-stream-initial/10-sgr-256-rgb b/test/fuzz-libghostty/corpus/vt-stream-initial/10-sgr-256-rgb new file mode 100644 index 000000000..5439f26a4 Binary files /dev/null and b/test/fuzz-libghostty/corpus/vt-stream-initial/10-sgr-256-rgb differ diff --git a/test/fuzz-libghostty/corpus/vt-stream-initial/11-utf8-multibyte b/test/fuzz-libghostty/corpus/vt-stream-initial/11-utf8-multibyte new file mode 100644 index 000000000..73c122594 Binary files /dev/null and b/test/fuzz-libghostty/corpus/vt-stream-initial/11-utf8-multibyte differ diff --git a/test/fuzz-libghostty/corpus/vt-stream-initial/12-malformed-utf8 b/test/fuzz-libghostty/corpus/vt-stream-initial/12-malformed-utf8 new file mode 100644 index 000000000..8ad0930da --- /dev/null +++ b/test/fuzz-libghostty/corpus/vt-stream-initial/12-malformed-utf8 @@ -0,0 +1 @@ +ÿþ€À¯í € \ No newline at end of file diff --git a/test/fuzz-libghostty/corpus/vt-stream-initial/13-incomplete-csi b/test/fuzz-libghostty/corpus/vt-stream-initial/13-incomplete-csi new file mode 100644 index 000000000..03b80010c Binary files /dev/null and b/test/fuzz-libghostty/corpus/vt-stream-initial/13-incomplete-csi differ diff --git a/test/fuzz-libghostty/corpus/vt-stream-initial/14-decset-decrst b/test/fuzz-libghostty/corpus/vt-stream-initial/14-decset-decrst new file mode 100644 index 000000000..d540d67f5 Binary files /dev/null and b/test/fuzz-libghostty/corpus/vt-stream-initial/14-decset-decrst differ diff --git a/test/fuzz-libghostty/corpus/vt-stream-initial/15-scroll-region b/test/fuzz-libghostty/corpus/vt-stream-initial/15-scroll-region new file mode 100644 index 000000000..79554c7c9 --- /dev/null +++ b/test/fuzz-libghostty/corpus/vt-stream-initial/15-scroll-region @@ -0,0 +1 @@ +DDDM \ No newline at end of file diff --git a/test/fuzz-libghostty/corpus/vt-stream-initial/16-c1-controls b/test/fuzz-libghostty/corpus/vt-stream-initial/16-c1-controls new file mode 100644 index 000000000..aa8ead46e Binary files /dev/null and b/test/fuzz-libghostty/corpus/vt-stream-initial/16-c1-controls differ diff --git a/test/fuzz-libghostty/corpus/vt-stream-initial/17-tab-backspace b/test/fuzz-libghostty/corpus/vt-stream-initial/17-tab-backspace new file mode 100644 index 000000000..9ace2ee6d Binary files /dev/null and b/test/fuzz-libghostty/corpus/vt-stream-initial/17-tab-backspace differ diff --git a/test/fuzz-libghostty/corpus/vt-stream-initial/18-insert-delete b/test/fuzz-libghostty/corpus/vt-stream-initial/18-insert-delete new file mode 100644 index 000000000..3b6ad631b --- /dev/null +++ b/test/fuzz-libghostty/corpus/vt-stream-initial/18-insert-delete @@ -0,0 +1 @@ +[3@ \ No newline at end of file diff --git a/test/fuzz-libghostty/corpus/vt-stream-initial/19-many-params b/test/fuzz-libghostty/corpus/vt-stream-initial/19-many-params new file mode 100644 index 000000000..756ad9d9a Binary files /dev/null and b/test/fuzz-libghostty/corpus/vt-stream-initial/19-many-params differ diff --git a/test/fuzz-libghostty/corpus/vt-stream-initial/20-csi-subparams b/test/fuzz-libghostty/corpus/vt-stream-initial/20-csi-subparams new file mode 100644 index 000000000..571617bc3 Binary files /dev/null and b/test/fuzz-libghostty/corpus/vt-stream-initial/20-csi-subparams differ diff --git a/test/fuzz-libghostty/corpus/vt-stream-initial/21-osc-hyperlink b/test/fuzz-libghostty/corpus/vt-stream-initial/21-osc-hyperlink new file mode 100644 index 000000000..b5f539b6e Binary files /dev/null and b/test/fuzz-libghostty/corpus/vt-stream-initial/21-osc-hyperlink differ diff --git a/test/fuzz-libghostty/corpus/vt-stream-initial/22-osc-clipboard b/test/fuzz-libghostty/corpus/vt-stream-initial/22-osc-clipboard new file mode 100644 index 000000000..9d6a4873c --- /dev/null +++ b/test/fuzz-libghostty/corpus/vt-stream-initial/22-osc-clipboard @@ -0,0 +1 @@ +]52;c;SGVsbG8= \ No newline at end of file diff --git a/test/fuzz-libghostty/corpus/vt-stream-initial/23-empty b/test/fuzz-libghostty/corpus/vt-stream-initial/23-empty new file mode 100644 index 000000000..f76dd238a Binary files /dev/null and b/test/fuzz-libghostty/corpus/vt-stream-initial/23-empty differ diff --git a/test/fuzz-libghostty/corpus/vt-stream-initial/24-esc-misc b/test/fuzz-libghostty/corpus/vt-stream-initial/24-esc-misc new file mode 100644 index 000000000..93809d0d9 --- /dev/null +++ b/test/fuzz-libghostty/corpus/vt-stream-initial/24-esc-misc @@ -0,0 +1 @@ +78c \ No newline at end of file diff --git a/test/fuzz-libghostty/src/lib.zig b/test/fuzz-libghostty/src/fuzz_vt_parser.zig similarity index 100% rename from test/fuzz-libghostty/src/lib.zig rename to test/fuzz-libghostty/src/fuzz_vt_parser.zig diff --git a/test/fuzz-libghostty/src/fuzz_vt_stream.zig b/test/fuzz-libghostty/src/fuzz_vt_stream.zig new file mode 100644 index 000000000..84b50f01e --- /dev/null +++ b/test/fuzz-libghostty/src/fuzz_vt_stream.zig @@ -0,0 +1,71 @@ +const std = @import("std"); +const ghostty_vt = @import("ghostty-vt"); +const Terminal = ghostty_vt.Terminal; +const ReadonlyStream = ghostty_vt.ReadonlyStream; + +/// Fixed-capacity allocator that avoids heap allocation and gives the +/// fuzzer deterministic, bounded memory behaviour. Backed by a single +/// fixed buffer; every `reset()` returns the bump pointer to the start +/// so the same memory is reused across iterations. +const FuzzAllocator = struct { + buf: [mem_size]u8 = undefined, + state: std.heap.FixedBufferAllocator = undefined, + + /// 4 MiB is plenty for a small terminal with a few pages of + /// scrollback, while staying within the resident-set limits + /// that AFL++ expects. + const mem_size = 4 * 1024 * 1024; + + fn init(self: *FuzzAllocator) void { + self.state = std.heap.FixedBufferAllocator.init(&self.buf); + } + + fn allocator(self: *FuzzAllocator) std.mem.Allocator { + return self.state.allocator(); + } + + fn reset(self: *FuzzAllocator) void { + self.state.reset(); + } +}; + +var fuzz_alloc: FuzzAllocator = .{}; + +pub export fn zig_fuzz_init() callconv(.c) void { + fuzz_alloc.init(); +} + +pub export fn zig_fuzz_test( + buf: [*]const u8, + len: usize, +) callconv(.c) void { + fuzz_alloc.reset(); + const alloc = fuzz_alloc.allocator(); + const input = buf[0..@intCast(len)]; + + // Allocate a terminal; if we run out of fixed-buffer space just + // skip this input (not a bug, just a very large allocation). + var term = Terminal.init(alloc, .{ + .cols = 80, + .rows = 24, + .max_scrollback = 100, + }) catch return; + defer term.deinit(alloc); + + var stream: ReadonlyStream = term.vtStream(); + defer stream.deinit(); + + // Use the first byte to decide between the scalar and slice paths + // so both code paths get exercised by the fuzzer. + if (input.len == 0) return; + const mode = input[0]; + const data = input[1..]; + + if (mode & 1 == 0) { + // Slice path — exercises SIMD fast-path + stream.nextSlice(data) catch {}; + } else { + // Scalar path — exercises byte-at-a-time UTF-8 decoding + for (data) |byte| _ = stream.next(byte) catch {}; + } +}