// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! One reftable, read from its bytes: refs and reflog entries in git's //! binary format, as the reftable document at //! describes it. //! //! header 24 bytes (version 1, SHA-1) or 28 (version 2) //! ref blocks 'r', the first sharing the header's block //! ref index blocks 'i' //! obj blocks 'o', and their index //! log blocks 'g', compressed with zlib //! log index blocks 'i' //! footer the header again, five positions, a CRC-32 //! //! A block is a type byte, a 24-bit length, records, a table of 24-bit //! restart offsets and their 16-bit count. Each record's key shares a //! prefix with the key before it and stores only the rest; a record at a //! restart offset stores the whole key. Ref and obj blocks are padded to //! the block size when the header gives one; log blocks never are, and //! hold their records deflated, the length in their header being the //! inflated length. //! //! Nothing here does I/O: a `Table` reads bytes the caller has, so it can //! be fuzzed. Every length and offset is checked against the bytes there //! are, and anything that does not add up is `MalformedReftable`, with //! the diagnostic saying what and where. const std = @import("std"); const Allocator = std.mem.Allocator; const object = @import("../object.zig"); const Id = object.Id; const Diagnostics = @import("../Diagnostics.zig"); const zlib = @import("../zlib.zig"); /// Why a reftable could not be read: /// /// - `MalformedReftable`: something in it does not add up; the diagnostic /// says what, and the offset where. /// - `UnsupportedReftableVersion`: a version other than 1 or 2. /// - `ReftableChecksumMismatch`: the footer's CRC-32 is not that of the /// footer. /// - `ReftableObjectFormatMismatch`: the table names its objects with a /// different hash from the repository's. pub const Error = Allocator.Error || error{ MalformedReftable, UnsupportedReftableVersion, ReftableChecksumMismatch, ReftableObjectFormatMismatch, }; /// The largest block the format allows. pub const max_block_size = 16777215; /// A ref record's value. pub const RefValue = union(enum) { /// A tombstone: the ref is deleted, whatever older tables say. deletion, /// An object ID. direct: Id, /// An annotated tag's object ID and the object it peels to. peeled: struct { id: Id, peeled: Id }, /// A symbolic ref's target. symbolic: []const u8, }; /// One ref record. The name and target are valid until the iterator that /// produced it moves on. pub const RefRecord = struct { name: []const u8, update_index: u64, value: RefValue, }; /// One log record. The strings are valid until the iterator that /// produced it moves on. pub const LogRecord = struct { /// The ref's name. name: []const u8, /// The transaction it belongs to; higher is newer. update_index: u64, value: union(enum) { /// A tombstone for an older table's record with the same key. deletion, entry: struct { old: Id, new: Id, name: []const u8, email: []const u8, time: u64, /// Minutes east of UTC. tz_offset: i16, message: []const u8, }, }, }; /// A reftable's header and footer, read and checked, and the bytes they /// describe. pub const Table = struct { bytes: []const u8, version: u8, format: object.Format, header_len: usize, footer_len: usize, block_size: u32, min_update_index: u64, max_update_index: u64, ref_index_position: u64, obj_position: u64, obj_id_len: u5, obj_index_position: u64, log_position: u64, log_index_position: u64, /// Reads a table's header and footer, checking the footer's checksum /// and that the table names objects in `format`. pub fn init(bytes: []const u8, format: object.Format, diag: ?*Diagnostics) Error!Table { if (bytes.len < 24 or !std.mem.eql(u8, bytes[0..4], "REFT")) return malformed(diag, 0, "bad magic", .{}); const version = bytes[4]; const header_len: usize = switch (version) { 1 => 24, 2 => 28, else => { if (diag) |d| d.setDetail("{d}", .{version}); return error.UnsupportedReftableVersion; }, }; const footer_len = header_len + 44; if (bytes.len < header_len + footer_len) return malformed(diag, 0, "too short for a header and footer", .{}); const table_format: object.Format = if (version == 1) .sha1 else blk: { const id = bytes[24..28]; if (std.mem.eql(u8, id, "sha1")) break :blk .sha1; if (std.mem.eql(u8, id, "s256")) break :blk .sha256; return malformed(diag, 24, "unknown hash ID", .{}); }; if (table_format != format) { if (diag) |d| d.setDetail("{s}", .{table_format.name()}); return error.ReftableObjectFormatMismatch; } const footer_start = bytes.len - footer_len; const footer = bytes[footer_start..]; if (!std.mem.eql(u8, footer[0..header_len], bytes[0..header_len])) return malformed(diag, footer_start, "footer does not repeat the header", .{}); const crc = std.hash.Crc32.hash(footer[0 .. footer_len - 4]); if (crc != std.mem.readInt(u32, footer[footer_len - 4 ..][0..4], .big)) { if (diag) |d| d.offset = footer_start; return error.ReftableChecksumMismatch; } const f = footer[header_len..]; const obj = std.mem.readInt(u64, f[8..16], .big); const t: Table = .{ .bytes = bytes, .version = version, .format = format, .header_len = header_len, .footer_len = footer_len, .block_size = std.mem.readInt(u24, bytes[5..8], .big), .min_update_index = std.mem.readInt(u64, bytes[8..16], .big), .max_update_index = std.mem.readInt(u64, bytes[16..24], .big), .ref_index_position = std.mem.readInt(u64, f[0..8], .big), .obj_position = obj >> 5, .obj_id_len = @truncate(obj), .obj_index_position = std.mem.readInt(u64, f[16..24], .big), .log_position = std.mem.readInt(u64, f[24..32], .big), .log_index_position = std.mem.readInt(u64, f[32..40], .big), }; for ([_]u64{ t.ref_index_position, t.obj_position, t.obj_index_position, t.log_position, t.log_index_position }) |p| { if (p != 0 and (p < header_len or p >= footer_start)) return malformed(diag, footer_start, "a section position {d} outside the table", .{p}); } if (t.min_update_index > t.max_update_index) return malformed(diag, 8, "update indexes out of order", .{}); return t; } fn footerStart(t: *const Table) usize { return t.bytes.len - t.footer_len; } /// Where the ref blocks end: at the first section after them. fn refsEnd(t: *const Table) usize { var end = t.footerStart(); for ([_]u64{ t.ref_index_position, t.obj_position, t.log_position }) |p| { if (p != 0 and p < end) end = @intCast(p); } return end; } /// Where the ref index ends: at the first section after it. fn indexEnd(t: *const Table) usize { var end = t.footerStart(); for ([_]u64{ t.obj_position, t.log_position }) |p| { if (p > t.ref_index_position and p < end) end = @intCast(p); } return end; } /// Whether the table has any ref blocks: a log-only table's first /// block is a log block, and an empty table has no block at all. fn hasRefs(t: *const Table) bool { return t.footerStart() > t.header_len and t.bytes[t.header_len] == 'r'; } /// Every ref record, in order of name. `gpa` holds the key being /// rebuilt. pub fn refs(t: *const Table, gpa: Allocator) RefIterator { return .{ .table = t, .gpa = gpa, .next_block = if (t.hasRefs()) 0 else null }; } /// The record for `name`, found through the ref index when the table /// has one; null if the table has no record for it. The record's /// strings are in `iterator`, which the caller deinitializes. pub fn findRef(t: *const Table, gpa: Allocator, name: []const u8, iterator: *RefIterator, diag: ?*Diagnostics) Error!?RefRecord { iterator.* = t.refs(gpa); if (t.ref_index_position != 0 and t.hasRefs()) { // Down the index, level by level, to the ref block whose last // key is the first not before the name. git writes an index // too big for one block as consecutive index blocks rather // than as levels, so a level is read on through the blocks // that follow it until a key answers. const index_end = t.indexEnd(); var pos: usize = @intCast(t.ref_index_position); var steps: usize = 0; while (true) { steps += 1; if (steps > 1 << 20) return malformed(diag, pos, "ref index too long", .{}); const block = try Block.at(t, pos, diag); if (block.kind == 'r') { iterator.next_block = pos; iterator.single_block = true; break; } if (block.kind != 'i') return malformed(diag, pos, "ref index names a block of type 0x{x:0>2}", .{block.kind}); var it: RecordReader = .{ .block = block, .key = &iterator.key, .gpa = gpa }; const found: ?u64 = while (try it.next(diag)) |_| { const position = try it.varint(diag); if (std.mem.order(u8, it.key.items, name) != .lt) break position; } else null; iterator.key.clearRetainingCapacity(); if (found) |target| { if (target >= t.footerStart()) return malformed(diag, pos, "ref index points outside the table", .{}); pos = @intCast(target); continue; } // On to the next index block, if there is one. var next_pos = pos + block.data.len; if (t.block_size != 0 and next_pos < index_end) next_pos = std.mem.alignForwardAnyAlign(usize, next_pos, t.block_size); if (next_pos >= index_end or t.bytes[next_pos] != 'i') return null; pos = next_pos; } } while (try iterator.next(diag)) |r| { switch (std.mem.order(u8, r.name, name)) { .lt => continue, .eq => return r, .gt => return null, } } return null; } /// Every log record, in order of name and then newest first. pub fn logs(t: *const Table, gpa: Allocator) LogIterator { return .{ .table = t, .gpa = gpa, .next_block = if (t.log_position != 0) @intCast(t.log_position) else null }; } }; /// One block, its records found but not yet read. pub const Block = struct { /// `r`, `i`, `o` or `g`. kind: u8, /// The block's bytes, from the position its restart offsets count /// from: the start of the file for the first block, and for a log /// block the inflated records behind its four-byte header. data: []const u8, /// Where the records start in `data`. records_start: usize, /// Where they end, and the restart table begins. records_end: usize, /// How many restart points there are. restart_count: u16, /// For a log block, how many stored bytes it took up; otherwise its /// length. stored_len: usize, /// For a log block, the inflated bytes, which the caller frees. allocation: ?[]u8 = null, /// Reads the block whose header is at `data[header_at]`, its offsets /// counted from the start of `data`; `data` may run on past the block. /// A log block is not inflated: use `Block.at`. pub fn parse(data: []const u8, header_at: usize, diag: ?*Diagnostics) Error!Block { if (data.len < header_at + 4) return malformed(diag, header_at, "block header cut short", .{}); const kind = data[header_at]; switch (kind) { 'r', 'i', 'o', 'g' => {}, else => return malformed(diag, header_at, "unknown block type 0x{x:0>2}", .{kind}), } const len = std.mem.readInt(u24, data[header_at + 1 ..][0..3], .big); if (len > data.len or len < header_at + 4 + 2) return malformed(diag, header_at, "block length {d} does not fit", .{len}); return finish(kind, data[0..len], header_at + 4, len, diag); } fn finish(kind: u8, data: []const u8, records_start: usize, stored_len: usize, diag: ?*Diagnostics) Error!Block { const len = data.len; const restart_count = std.mem.readInt(u16, data[len - 2 ..][0..2], .big); if (restart_count == 0) return malformed(diag, len - 2, "no restart points", .{}); const table_len = @as(usize, restart_count) * 3 + 2; if (table_len > len - records_start) return malformed(diag, len - 2, "restart table larger than the block", .{}); const records_end = len - table_len; var prev: usize = 0; for (0..restart_count) |i| { const off = std.mem.readInt(u24, data[records_end + 3 * i ..][0..3], .big); if (off < records_start or off >= records_end or off < prev) return malformed(diag, records_end + 3 * i, "restart offset {d} out of place", .{off}); prev = off; } return .{ .kind = kind, .data = data, .records_start = records_start, .records_end = records_end, .restart_count = restart_count, .stored_len = stored_len, }; } /// The block at `pos` in a table: a ref, index or obj block as it is, /// a log block inflated (the caller frees `allocation`, with the /// allocator given to `inflate`). fn at(t: *const Table, pos: usize, diag: ?*Diagnostics) Error!Block { const header_at: usize = if (pos == 0) t.header_len else 0; const end = t.footerStart(); if (pos >= end) return malformed(diag, pos, "block outside the table", .{}); const block = Block.parse(t.bytes[pos..end], header_at, diag) catch |err| { if (diag) |d| d.offset = pos + (d.offset orelse 0); return err; }; if (block.kind == 'g') return malformed(diag, pos, "log block where a ref, index or obj block belongs", .{}); return block; } /// The log block at `pos`, inflated into memory from `gpa`. fn inflate(t: *const Table, gpa: Allocator, pos: usize, diag: ?*Diagnostics) Error!Block { const end = t.footerStart(); if (pos + 4 > end) return malformed(diag, pos, "log block header cut short", .{}); const bytes = t.bytes[pos..end]; if (bytes[0] != 'g') return malformed(diag, pos, "block of type 0x{x:0>2} among the log blocks", .{bytes[0]}); const len = std.mem.readInt(u24, bytes[1..4], .big); if (len < 4 + 2) return malformed(diag, pos, "log block length {d} too small", .{len}); // Not allocated if what follows could not inflate to it. if (len - 4 > zlib.maxInflatedLen(bytes.len - 4)) return malformed(diag, pos, "log block does not inflate to {d} bytes", .{len}); const buffer = try gpa.alloc(u8, len + 1); errdefer gpa.free(buffer); @memcpy(buffer[0..4], bytes[0..4]); var detail: []const u8 = ""; const consumed = zlib.inflateExactConsumed(bytes[4..], buffer[4..], len - 4, &detail) catch |err| switch (err) { error.InflateFailed => return malformed(diag, pos, "log block: {s}", .{detail}), error.TruncatedStream => return malformed(diag, pos, "log block cut short", .{}), error.StreamTooLong, error.StreamTooShort => return malformed(diag, pos, "log block does not inflate to {d} bytes", .{len}), }; var block = finish('g', buffer[0..len], 4, 4 + consumed, diag) catch |err| { if (diag) |d| d.offset = pos; return err; }; block.allocation = buffer; return block; } /// The records, read in order; `key` holds the key being rebuilt. pub fn records(b: *const Block, gpa: Allocator, key: *std.ArrayList(u8)) RecordReader { return .{ .block = b.*, .key = key, .gpa = gpa }; } }; /// Reads a block's records one at a time: the key, rebuilt from the /// previous one, and the type bits, leaving the cursor at what follows /// them for the caller, who knows what the block's records hold. pub const RecordReader = struct { block: Block, key: *std.ArrayList(u8), gpa: Allocator, pos: usize = 0, started: bool = false, /// Moves to the next record and returns its type bits, or null at /// the end of the records; `key` then holds its key. pub fn next(r: *RecordReader, diag: ?*Diagnostics) Error!?u3 { if (!r.started) { r.pos = r.block.records_start; r.started = true; r.key.clearRetainingCapacity(); } if (r.pos >= r.block.records_end) return null; const record_at = r.pos; const prefix_len = try r.varint(diag); const x = try r.varint(diag); const suffix_len = x >> 3; if (prefix_len > r.key.items.len) return malformed(diag, record_at, "prefix of {d} bytes longer than the key before", .{prefix_len}); if (record_at == r.block.records_start and prefix_len != 0) return malformed(diag, record_at, "the block's first key shares a prefix", .{}); const suffix = try r.take(suffix_len, diag); r.key.shrinkRetainingCapacity(@intCast(prefix_len)); try r.key.appendSlice(r.gpa, suffix); return @truncate(x); } /// Reads a varint, as the reftable document decodes them. pub fn varint(r: *RecordReader, diag: ?*Diagnostics) Error!u64 { const at = r.pos; const end = r.block.records_end; if (r.pos >= end) return malformed(diag, at, "varint cut short", .{}); var b = r.block.data[r.pos]; r.pos += 1; var v: u64 = b & 0x7f; while (b & 0x80 != 0) { if (r.pos >= end) return malformed(diag, at, "varint cut short", .{}); b = r.block.data[r.pos]; r.pos += 1; const plus = std.math.add(u64, v, 1) catch return malformed(diag, at, "varint too large", .{}); if (plus > std.math.maxInt(u64) >> 7) return malformed(diag, at, "varint too large", .{}); v = (plus << 7) | (b & 0x7f); } return v; } /// The next `n` bytes of the record. pub fn take(r: *RecordReader, n: u64, diag: ?*Diagnostics) Error![]const u8 { if (n > r.block.records_end - r.pos) return malformed(diag, r.pos, "record runs past the block's records", .{}); const s = r.block.data[r.pos..][0..@intCast(n)]; r.pos += @intCast(n); return s; } /// The next object ID. pub fn id(r: *RecordReader, format: object.Format, diag: ?*Diagnostics) Error!Id { return Id.fromBytes(format, try r.take(format.byteLength(), diag)); } }; /// Steps through a table's ref records in order. pub const RefIterator = struct { table: *const Table, gpa: Allocator, key: std.ArrayList(u8) = .empty, next_block: ?usize, /// Read only the one block, as a lookup through the index does. single_block: bool = false, reader: ?RecordReader = null, /// Frees the key. pub fn deinit(it: *RefIterator) void { it.key.deinit(it.gpa); it.* = undefined; } /// The next ref record, or null after the last. pub fn next(it: *RefIterator, diag: ?*Diagnostics) Error!?RefRecord { const t = it.table; while (true) { if (it.reader) |*r| { if (try r.next(diag)) |kind| { const delta = try r.varint(diag); const update_index = std.math.add(u64, t.min_update_index, delta) catch return malformed(diag, r.pos, "update index too large", .{}); const value: RefValue = switch (kind) { 0 => .deletion, 1 => .{ .direct = try r.id(t.format, diag) }, 2 => .{ .peeled = .{ .id = try r.id(t.format, diag), .peeled = try r.id(t.format, diag) } }, 3 => .{ .symbolic = try r.take(try r.varint(diag), diag) }, else => return malformed(diag, r.pos, "ref record of reserved type {d}", .{kind}), }; return .{ .name = it.key.items, .update_index = update_index, .value = value }; } it.reader = null; if (it.single_block) it.next_block = null; } const pos = it.next_block orelse return null; const end = t.refsEnd(); if (pos >= end) { it.next_block = null; return null; } const block = try Block.at(t, pos, diag); if (block.kind != 'r') return malformed(diag, pos, "block of type 0x{x:0>2} among the ref blocks", .{block.kind}); var next_pos = pos + block.data.len; if (t.block_size != 0 and next_pos < end) next_pos = std.mem.alignForwardAnyAlign(usize, next_pos, t.block_size); it.next_block = next_pos; it.reader = .{ .block = block, .key = &it.key, .gpa = it.gpa }; } } }; /// Steps through a table's log records in order. pub const LogIterator = struct { table: *const Table, gpa: Allocator, key: std.ArrayList(u8) = .empty, next_block: ?usize, reader: ?RecordReader = null, allocation: ?[]u8 = null, /// Frees the key and the block being read. pub fn deinit(it: *LogIterator) void { it.key.deinit(it.gpa); if (it.allocation) |a| it.gpa.free(a); it.* = undefined; } /// The next log record, or null after the last. pub fn next(it: *LogIterator, diag: ?*Diagnostics) Error!?LogRecord { const t = it.table; while (true) { if (it.reader) |*r| { if (try r.next(diag)) |kind| { const key = it.key.items; if (key.len < 9 or key[key.len - 9] != 0) return malformed(diag, r.pos, "log key without its update index", .{}); const update_index = std.math.maxInt(u64) - std.mem.readInt(u64, key[key.len - 8 ..][0..8], .big); const name = key[0 .. key.len - 9]; switch (kind) { 0 => return .{ .name = name, .update_index = update_index, .value = .deletion }, 1 => { const old = try r.id(t.format, diag); const new = try r.id(t.format, diag); const who = try r.take(try r.varint(diag), diag); const email = try r.take(try r.varint(diag), diag); const time = try r.varint(diag); const tz = std.mem.readInt(i16, (try r.take(2, diag))[0..2], .big); const message = try r.take(try r.varint(diag), diag); return .{ .name = name, .update_index = update_index, .value = .{ .entry = .{ .old = old, .new = new, .name = who, .email = email, .time = time, .tz_offset = tz, .message = message, } } }; }, else => return malformed(diag, r.pos, "log record of reserved type {d}", .{kind}), } } it.reader = null; } const pos = it.next_block orelse return null; const end = if (t.log_index_position != 0 and t.log_index_position > t.log_position) t.log_index_position else t.footerStart(); if (pos >= end) { it.next_block = null; return null; } if (it.allocation) |a| it.gpa.free(a); it.allocation = null; const block = try Block.inflate(t, it.gpa, pos, diag); it.allocation = block.allocation; it.next_block = pos + block.stored_len; it.reader = .{ .block = block, .key = &it.key, .gpa = it.gpa }; } } }; fn malformed(diag: ?*Diagnostics, offset: usize, comptime fmt: []const u8, args: anytype) error{MalformedReftable} { if (diag) |d| { d.offset = offset; d.setDetail(fmt, args); } return error.MalformedReftable; } /// Reads every record of every block in `bytes` taken as one block, of /// whatever kind: the block reader alone, for fuzzing. pub fn readBlock(gpa: Allocator, format: object.Format, bytes: []const u8, diag: ?*Diagnostics) Error!usize { const block = try Block.parse(bytes, 0, diag); var key: std.ArrayList(u8) = .empty; defer key.deinit(gpa); var r = block.records(gpa, &key); var n: usize = 0; while (try r.next(diag)) |kind| { n += 1; switch (block.kind) { 'r' => { _ = try r.varint(diag); switch (kind) { 0 => {}, 1 => _ = try r.id(format, diag), 2 => _ = try r.take(2 * format.byteLength(), diag), 3 => _ = try r.take(try r.varint(diag), diag), else => return malformed(diag, r.pos, "reserved", .{}), } }, 'i' => _ = try r.varint(diag), 'o' => { const count = if (kind == 0) try r.varint(diag) else kind; for (0..@min(count, block.data.len)) |_| _ = try r.varint(diag); }, 'g' => if (kind == 1) { _ = try r.take(2 * format.byteLength(), diag); _ = try r.take(try r.varint(diag), diag); _ = try r.take(try r.varint(diag), diag); _ = try r.varint(diag); _ = try r.take(2, diag); _ = try r.take(try r.varint(diag), diag); }, else => unreachable, } } return n; } // -- tests ----------------------------------------------------------------------- const testing = std.testing; fn collectRefs(gpa: Allocator, t: *const Table) !std.ArrayList([]u8) { var names: std.ArrayList([]u8) = .empty; errdefer { for (names.items) |n| gpa.free(n); names.deinit(gpa); } var it = t.refs(gpa); defer it.deinit(); while (try it.next(null)) |r| try names.append(gpa, try gpa.dupe(u8, r.name)); return names; } const fuzz_input = @import("../testing/fuzz.zig"); test "a reftable git wrote is read" { const gpa = testing.allocator; const t = try Table.init(@embedFile("../fuzz/reftable"), .sha1, null); try testing.expectEqual(1, t.version); var names = try collectRefs(gpa, &t); defer { for (names.items) |n| gpa.free(n); names.deinit(gpa); } try testing.expectEqualStrings("HEAD", names.items[0]); try testing.expectEqualStrings("refs/heads/main", names.items[1]); var it: RefIterator = undefined; const v1 = t.findRef(gpa, "refs/tags/v1", &it, null) catch |err| { it.deinit(); return err; }; it.deinit(); try testing.expect(v1 != null); try testing.expect(try t.findRef(gpa, "refs/tags/nope", &it, null) == null); it.deinit(); const head = (try t.findRef(gpa, "HEAD", &it, null)).?; try testing.expectEqualStrings("refs/heads/main", head.value.symbolic); it.deinit(); var logs = t.logs(gpa); defer logs.deinit(); var n: usize = 0; while (try logs.next(null)) |l| { n += 1; try testing.expect(l.value == .entry); } try testing.expect(n > 0); } test "a large reftable is read through its ref index" { const gpa = testing.allocator; const t = try Table.init(@embedFile("../fuzz/reftable-indexed"), .sha1, null); try testing.expect(t.ref_index_position != 0); var names = try collectRefs(gpa, &t); defer { for (names.items) |n| gpa.free(n); names.deinit(gpa); } for (names.items[1..], 0..) |name, i| try testing.expect(std.mem.lessThan(u8, names.items[i], name)); for (names.items) |name| { var it: RefIterator = undefined; defer it.deinit(); const r = (try t.findRef(gpa, name, &it, null)).?; try testing.expectEqualStrings(name, r.name); } var it: RefIterator = undefined; defer it.deinit(); try testing.expect(try t.findRef(gpa, "refs/heads/branch-99999", &it, null) == null); } test "a corrupt reftable is refused" { const good = @embedFile("../fuzz/reftable"); var bad = good.*; bad[0] = 'X'; try testing.expectError(error.MalformedReftable, Table.init(&bad, .sha1, null)); bad = good.*; bad[bad.len - 1] ^= 1; try testing.expectError(error.ReftableChecksumMismatch, Table.init(&bad, .sha1, null)); bad = good.*; bad[4] = 9; try testing.expectError(error.UnsupportedReftableVersion, Table.init(&bad, .sha1, null)); try testing.expectError(error.ReftableObjectFormatMismatch, Table.init(good, .sha256, null)); try testing.expectError(error.MalformedReftable, Table.init(good[0..50], .sha1, null)); } fn fuzzTable(_: void, smith: *testing.Smith) anyerror!void { const gpa = testing.allocator; var buffer: [8192]u8 = undefined; const input = @import("../testing/fuzz.zig").bytes(smith, &buffer); for ([_]object.Format{ .sha1, .sha256 }) |f| { _ = readBlock(gpa, f, input, null) catch {}; _ = readBlock(gpa, f, input[@min(input.len, 24)..], null) catch {}; const t = Table.init(input, f, null) catch continue; var refs = t.refs(gpa); defer refs.deinit(); while (refs.next(null) catch null) |r| { var it: RefIterator = undefined; const name = try gpa.dupe(u8, r.name); defer gpa.free(name); _ = t.findRef(gpa, name, &it, null) catch {}; it.deinit(); } var logs = t.logs(gpa); defer logs.deinit(); while (logs.next(null) catch null) |_| {} } } test "fuzz: reftables" { try fuzz_input.fuzz({}, fuzzTable, .{ .corpus = &.{ @embedFile("../fuzz/reftable"), @embedFile("../fuzz/reftable-sha256"), @embedFile("../fuzz/reftable-deletion"), @embedFile("../fuzz/reftable-worktree"), } }); }