Something went wrong. Try again.
A Zig library that reads git repositories without running git, with log, show and diff output byte-identical to git's.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! One reftable, read from its bytes: refs and reflog entries in git's//! binary format, as the reftable document at//! <https://git-scm.com/docs/reftable> 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"), } });}