// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! The fsmonitor cache: the `FSMN` extension of the index, read from its //! bytes as gitformat-index(5) lays it out: a version, then for version 1 //! a time in nanoseconds and for version 2 the monitor's token and a NUL, //! then the size of an EWAH bitmap and the bitmap, whose set bits are the //! index entries the monitor has not vouched for. //! //! What git says of one it cannot read, seen by writing malformed ones, //! is a warning, after which it reads the index without it: //! //! - fewer than nine bytes, or fewer than the version's fields: "corrupt //! fsmonitor extension (too short)" (where git reads past the end of a //! short version 1 or an unterminated token, this says so too); //! - any version but 1 and 2: "bad fsmonitor version N"; //! - a bitmap it cannot read, or one that does not take exactly the size //! given for it: "failed to parse ewah bitmap reading fsmonitor index //! extension", after the bitmap's own complaint if it has one. Bytes //! after the bitmap are passed over. //! //! Which entries the monitor vouches for is not what the extension says //! alone: when `core.fsmonitor` is set, git asks the monitor before //! answering, which this library cannot do. const std = @import("std"); const Diagnostics = @import("../Diagnostics.zig"); const ewah = @import("ewah.zig"); /// What the monitor's answers were last relative to. pub const Since = union(enum) { /// Version 1: nanoseconds since the epoch. time: u64, /// Version 2: the monitor's opaque token. token: []const u8, }; /// The fsmonitor cache. pub const Fsmonitor = struct { since: Since, /// Set for each index entry the monitor has not vouched for. dirty: ewah.Bitmap, /// The extension's version. pub fn version(f: *const Fsmonitor) u32 { return switch (f.since) { .time => 1, .token => 2, }; } }; /// Why an `FSMN` extension could not be read, each a warning in git's /// words: `FsmonitorTooShort`, `BadFsmonitorVersion` (the detail is the /// version) and `BadFsmonitorBitmap` (the detail is the bitmap's own /// complaint, or empty when it has none). pub const Error = error{ FsmonitorTooShort, BadFsmonitorVersion, BadFsmonitorBitmap }; /// Reads the `FSMN` extension's contents. pub fn parse(data: []const u8, diag: ?*Diagnostics) Error!Fsmonitor { // git wants nine bytes before it looks at the version: four of it, // and at least a NUL and a size after. if (data.len < 9) return error.FsmonitorTooShort; const v = std.mem.readInt(u32, data[0..4], .big); var pos: usize = 4; var since: Since = undefined; switch (v) { 1 => { if (data.len - pos < 8) return error.FsmonitorTooShort; since = .{ .time = std.mem.readInt(u64, data[pos..][0..8], .big) }; pos += 8; }, 2 => { const nul = std.mem.findScalar(u8, data[pos..], 0) orelse return error.FsmonitorTooShort; since = .{ .token = data[pos..][0..nul] }; pos += nul + 1; }, else => { if (diag) |d| d.setDetail("{d}", .{v}); return error.BadFsmonitorVersion; }, } if (data.len - pos < 4) return error.FsmonitorTooShort; const size = std.mem.readInt(u32, data[pos..][0..4], .big); pos += 4; const region = data[pos..][0..@min(size, data.len - pos)]; if (diag) |d| d.setDetail("", .{}); const p = ewah.parse(region, diag) catch return error.BadFsmonitorBitmap; if (p.len != size) { if (diag) |d| d.setDetail("", .{}); return error.BadFsmonitorBitmap; } return .{ .since = since, .dirty = p.bitmap }; } // -- tests ----------------------------------------------------------------------- const testing = std.testing; const fuzz_input = @import("../testing/fuzz.zig"); test "fsmonitor extensions are read, and refused in git's words" { const bitmap = "\x00\x00\x00\x00\x00\x00\x00\x01" ++ "\x00\x00\x00\x00\x00\x00\x00\x00" ++ "\x00\x00\x00\x00"; const f = try parse("\x00\x00\x00\x02tok\x00\x00\x00\x00\x14" ++ bitmap, null); try testing.expectEqualStrings("tok", f.since.token); try testing.expectEqual(2, f.version()); const g = try parse("\x00\x00\x00\x01" ++ "\x00\x00\x00\x00\x00\x00\x00" ++ "\x05\x00\x00\x00\x14" ++ bitmap, null); try testing.expectEqual(5, g.since.time); var d: Diagnostics = .{}; try testing.expectError(error.BadFsmonitorVersion, parse("\x00\x00\x00\x03tok\x00\x00", &d)); try testing.expectEqualStrings("3", d.detail()); try testing.expectError(error.FsmonitorTooShort, parse("\x00\x00\x00\x03tok\x00", &d)); try testing.expectError(error.FsmonitorTooShort, parse("\x00\x00\x00\x02tok", &d)); try testing.expectError(error.FsmonitorTooShort, parse("\x00\x00\x00\x01\x00\x00", &d)); try testing.expectError(error.BadFsmonitorBitmap, parse("\x00\x00\x00\x02tok\x00\x00\x00\x00\x15" ++ bitmap, &d)); try testing.expectEqualStrings("", d.detail()); try testing.expectError(error.BadFsmonitorBitmap, parse("\x00\x00\x00\x02tok\x00\x00\x00\x00\x13" ++ bitmap, &d)); try testing.expectEqualStrings("eof before rlw", d.detail()); } test "fuzz: fsmonitor extensions" { try fuzz_input.fuzz({}, struct { fn one(_: void, smith: *testing.Smith) anyerror!void { var buffer: [1024]u8 = undefined; const input = @import("../testing/fuzz.zig").bytes(smith, &buffer); const f = parse(input, null) catch return; _ = f.dirty.anySetFrom(64); } }.one, .{ .corpus = &.{ @embedFile("../fuzz/index-fsmonitor"), } }); }