// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! Inputs for the fuzz tests of the format parsers. //! //! A corpus entry is a file exactly as git wrote it, with no framing of its //! own, so when the test runner replays one (`smith.in` is set) the whole of //! it is the input. Only when the fuzzer is driving is the input asked of //! the `Smith`, which reads a length before the bytes. const std = @import("std"); const builtin = @import("builtin"); const Smith = std.testing.Smith; /// `std.testing.fuzz`, but in a test run with `ZIG_GIT_TEST_NO_FUZZ` set /// the corpus is only replayed, as `std.testing.fuzz` replays it in a run /// that is not fuzzing, without the test being declared a fuzz test. /// /// `zig build test` runs the test binary twice, the second time with /// SHA-256 repositories, and `zig build test --fuzz` fuzzes every run that /// declared a fuzz test: both runs of the one binary at once, sharing its /// corpus and its coverage, which ends in a failure that is the tooling's /// and not a finding. The second run sets this, so that it still replays /// every corpus entry and only the first is fuzzed. pub fn fuzz( context: anytype, comptime testOne: fn (context: @TypeOf(context), smith: *Smith) anyerror!void, options: std.testing.FuzzInputOptions, ) anyerror!void { if (!builtin.fuzz and replayOnly()) { for (options.corpus) |input| { var smith: Smith = .{ .in = input }; try testOne(context, &smith); } var smith: Smith = .{ .in = "" }; return testOne(context, &smith); } return std.testing.fuzz(context, testOne, options); } fn replayOnly() bool { const gpa = std.testing.allocator; const value = std.testing.environ.getAlloc(gpa, "ZIG_GIT_TEST_NO_FUZZ") catch return false; defer gpa.free(value); return value.len != 0; } /// The bytes to parse: the corpus entry being replayed, or up to /// `buffer.len` bytes from the fuzzer. pub fn bytes(smith: *Smith, buffer: []u8) []const u8 { if (smith.in) |in| { smith.in = in[in.len..]; return in; } return buffer[0..smith.slice(buffer)]; } /// An allocator for a fuzz test, passing everything on to `backing` but /// refusing, as out of memory, any one allocation bigger than `limit`, and /// remembering that it did. The inputs a fuzz test is given are a few /// kilobytes, so an allocation of many megabytes means the library took a /// size from the input and believed it. Under `std.testing.allocator`, /// which fills what is freed, such an allocation is not merely reserved /// but written to end to end: one input claiming fifty gigabytes did that, /// and the fuzzer was killed for the memory, taking the machine's other /// work with it. Here it is a test failure instead. pub const Bounded = struct { backing: std.mem.Allocator, limit: usize, /// The size of the first allocation refused, if any was. refused: ?usize = null, pub fn allocator(b: *Bounded) std.mem.Allocator { return .{ .ptr = b, .vtable = &.{ .alloc = alloc, .resize = resize, .remap = remap, .free = free, } }; } /// Fails the test if anything was refused. pub fn check(b: *const Bounded) error{TestUnexpectedResult}!void { if (b.refused) |n| { std.debug.print("an input of a few kilobytes asked for an allocation of {d} bytes\n", .{n}); return error.TestUnexpectedResult; } } fn refuse(b: *Bounded, len: usize) bool { if (len <= b.limit) return false; if (b.refused == null) b.refused = len; return true; } fn alloc(ptr: *anyopaque, len: usize, alignment: std.mem.Alignment, ret_addr: usize) ?[*]u8 { const b: *Bounded = @ptrCast(@alignCast(ptr)); if (b.refuse(len)) return null; return b.backing.rawAlloc(len, alignment, ret_addr); } fn resize(ptr: *anyopaque, memory: []u8, alignment: std.mem.Alignment, new_len: usize, ret_addr: usize) bool { const b: *Bounded = @ptrCast(@alignCast(ptr)); if (b.refuse(new_len)) return false; return b.backing.rawResize(memory, alignment, new_len, ret_addr); } fn remap(ptr: *anyopaque, memory: []u8, alignment: std.mem.Alignment, new_len: usize, ret_addr: usize) ?[*]u8 { const b: *Bounded = @ptrCast(@alignCast(ptr)); if (b.refuse(new_len)) return null; return b.backing.rawRemap(memory, alignment, new_len, ret_addr); } fn free(ptr: *anyopaque, memory: []u8, alignment: std.mem.Alignment, ret_addr: usize) void { const b: *Bounded = @ptrCast(@alignCast(ptr)); b.backing.rawFree(memory, alignment, ret_addr); } };