diff --git a/src/tail/checkpoint/lane.zig b/src/tail/checkpoint/lane.zig index 5a309dca..7c711c8f 100644 --- a/src/tail/checkpoint/lane.zig +++ b/src/tail/checkpoint/lane.zig @@ -365,6 +365,38 @@ test "checkpoint/lane: corrupted snapshot falls back to wal replay" { try testing.expectEqual(@as(?u64, 777), recovered.getOffset(id)); } +test "checkpoint/lane: corrupted snapshot count degrades to wal replay" { + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); + const state_dir = try tmp.dir.realPathFileAlloc(testing.io, ".", testing.allocator); + defer testing.allocator.free(state_dir); + + const id: tail_types.FileIdentity = .{ .dev = 30, .inode = 31, .fingerprint = 32 }; + const value: checkpoint_types.Value = .{ + .identity = id, + .offset = 888, + .last_seen_ns = @intCast(std.Io.Timestamp.now(testing.io, .awake).toNanoseconds()), + }; + + var snap = try snapshot_mod.Snapshot.init(testing.allocator, testing.io, state_dir); + defer snap.deinit(); + var vals: [1]checkpoint_types.Value = .{value}; + try snap.write(vals[0..]); + + const snap_path = try std.fs.path.join(testing.allocator, &.{ state_dir, "checkpoint.snap" }); + defer testing.allocator.free(snap_path); + try corruptByte(testing.io, snap_path, 15); + + var wal = try wal_mod.Wal.init(testing.allocator, testing.io, state_dir); + defer wal.deinit(); + try wal.append(1, value); + try wal.sync(); + + var recovered = try Lane.init(testing.allocator, testing.io, state_dir, 16, 64, 5, 72 * 60 * 60 * 1000, 64, 60_000); + defer recovered.deinit(); + try testing.expectEqual(@as(?u64, 888), recovered.getOffset(id)); +} + test "checkpoint/lane: missing state files initialize cleanly" { var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); diff --git a/src/tail/checkpoint/snapshot.zig b/src/tail/checkpoint/snapshot.zig index f46d28fb..75c155d1 100644 --- a/src/tail/checkpoint/snapshot.zig +++ b/src/tail/checkpoint/snapshot.zig @@ -62,7 +62,14 @@ pub const Snapshot = struct { if (got_header != @sizeOf(SnapshotHeader)) return out; if (header.magic != SNAP_MAGIC or header.version != SNAP_VERSION) return out; - try out.ensureTotalCapacity(allocator, @intCast(header.count)); + const file_size = try file.length(self.io); + const entry_space = if (file_size > @sizeOf(SnapshotHeader)) + file_size - @sizeOf(SnapshotHeader) + else + 0; + const max_entries = entry_space / @sizeOf(SnapshotEntry); + const hint = @min(header.count, max_entries); + try out.ensureTotalCapacity(allocator, @intCast(hint)); var off: u64 = @sizeOf(SnapshotHeader); var i: u64 = 0;