// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! The end of index entries (`EOIE`) and index entry offset table //! (`IEOT`) extensions, read from their bytes and checked against the //! index file they came from. //! //! gitformat-index(5) describes both, which git writes when //! `index.recordEndOfIndexEntries` and `index.recordOffsetTable` say so //! (or `index.threads` asks for more than one thread): `EOIE`, always the //! last extension, is where the entries end and a hash over the //! signatures and sizes of the extensions before it; `IEOT` is a version, //! 1, and for each block of entries the offset of its first and how many //! there are. They are for reading an index faster, in parallel; this //! library reads the entries in one pass and uses them only to check. //! //! git passes over either without a word when it cannot read it. const std = @import("std"); const object = @import("../object.zig"); const Format = object.Format; const Id = object.Id; const file = @import("file.zig"); /// The end of index entries extension. pub const EndOfEntries = struct { /// Where the entries end, from the start of the file. offset: u32, /// The hash of each earlier extension's signature and size. hash: Id, }; /// One block of the index entry offset table. pub const Block = struct { /// Where the block's first entry is, from the start of the file. offset: u32, /// How many entries the block holds. count: u32, }; /// The index entry offset table. pub const OffsetTable = struct { version: u32, /// The table's contents, eight bytes a block; `block` reads one. raw: []const u8, /// How many blocks there are. pub fn blockCount(t: *const OffsetTable) usize { return t.raw.len / 8; } /// The `i`th block. pub fn block(t: *const OffsetTable, i: usize) Block { return .{ .offset = std.mem.readInt(u32, t.raw[i * 8 ..][0..4], .big), .count = std.mem.readInt(u32, t.raw[i * 8 + 4 ..][0..4], .big), }; } }; /// Why either extension could not be read or does not fit its file. pub const Error = error{ MalformedEndOfEntries, MalformedOffsetTable }; /// Reads the `EOIE` extension's contents, in `format`. pub fn parseEndOfEntries(format: Format, data: []const u8) Error!EndOfEntries { const n = format.byteLength(); if (data.len != 4 + n) return error.MalformedEndOfEntries; return .{ .offset = std.mem.readInt(u32, data[0..4], .big), .hash = .fromBytes(format, data[4..][0..n]), }; } /// Checks the end of index entries extension of `f`, read from its /// extension `eoie`: that it is the last, that the entries end where it /// says, and that its hash is right. pub fn checkEndOfEntries(format: Format, f: *const file.File, eoie: file.Extension, e: EndOfEntries) Error!void { const last = f.extensions[f.extensions.len - 1]; if (last.offset != eoie.offset) return error.MalformedEndOfEntries; if (e.offset != f.entries_end) return error.MalformedEndOfEntries; var h: object.Hasher = .init(format); for (f.extensions[0 .. f.extensions.len - 1]) |x| { h.update(&x.signature); var size: [4]u8 = undefined; std.mem.writeInt(u32, &size, @intCast(x.data.len), .big); h.update(&size); } if (!h.final().eql(e.hash)) return error.MalformedEndOfEntries; } /// Reads the `IEOT` extension's contents. pub fn parseOffsetTable(data: []const u8) Error!OffsetTable { if (data.len < 4 or (data.len - 4) % 8 != 0) return error.MalformedOffsetTable; const version = std.mem.readInt(u32, data[0..4], .big); if (version != 1) return error.MalformedOffsetTable; return .{ .version = version, .raw = data[4..] }; } /// Checks the offset table `t` against the entries of `f`: that its /// blocks begin where entries begin, one after another, and cover them /// all. pub fn checkOffsetTable(f: *const file.File, t: OffsetTable) Error!void { var next: u64 = 0; for (0..t.blockCount()) |i| { const b = t.block(i); if (next >= f.entries.len and b.count != 0) return error.MalformedOffsetTable; if (next < f.entries.len and f.entry_offsets[@intCast(next)] != b.offset) return error.MalformedOffsetTable; next += b.count; if (next > f.entries.len) return error.MalformedOffsetTable; } if (next != f.entries.len) return error.MalformedOffsetTable; } // -- tests ----------------------------------------------------------------------- const testing = std.testing; const fuzz_input = @import("../testing/fuzz.zig"); test "fuzz: end of index entries and offset tables" { try fuzz_input.fuzz({}, struct { fn one(_: void, smith: *testing.Smith) anyerror!void { var buffer: [4096]u8 = undefined; const input = @import("../testing/fuzz.zig").bytes(smith, &buffer); var arena: std.heap.ArenaAllocator = .init(testing.allocator); defer arena.deinit(); for ([_]Format{ .sha1, .sha256 }) |format| { const f = file.parse(arena.allocator(), format, input, null) catch continue; for (f.extensions) |x| { if (std.mem.eql(u8, &x.signature, "EOIE")) { const e = parseEndOfEntries(format, x.data) catch continue; checkEndOfEntries(format, &f, x, e) catch {}; } else if (std.mem.eql(u8, &x.signature, "IEOT")) { const t = parseOffsetTable(x.data) catch continue; checkOffsetTable(&f, t) catch {}; } } } } }.one, .{ .corpus = &.{ @embedFile("../fuzz/index-eoie"), @embedFile("../fuzz/index-eoie-v4"), } }); }