// Go ref: filestore.go:2204 (lastChecksum), filestore.go:8180 (validation in msgFromBufEx) // // Tests for per-block last-checksum tracking and read-path validation using XxHash64. // The Go reference implementation tracks the last written checksum in msgBlock.lchk // and validates each record's checksum during reads to detect storage corruption. using NATS.Server.JetStream.Storage; namespace NATS.Server.Tests.JetStream.Storage; public sealed class FileStoreChecksumTests : IDisposable { private readonly DirectoryInfo _dir = Directory.CreateTempSubdirectory("checksum-"); public void Dispose() => _dir.Delete(recursive: true); // Go ref: filestore.go:2204 (msgBlock.lchk — last checksum field) [Fact] public void MsgBlock_tracks_last_checksum() { // Arrange / Act using var block = MsgBlock.Create(1, _dir.FullName, 1024 * 1024); block.Write("test", ReadOnlyMemory.Empty, "hello"u8.ToArray()); // Assert block.LastChecksum.ShouldNotBeNull(); block.LastChecksum!.Length.ShouldBe(8); // XxHash64 = 8 bytes } // Go ref: filestore.go:8180 (msgFromBufEx checksum validation) [Fact] public void MsgBlock_validates_checksum_on_read() { // Arrange using var block = MsgBlock.Create(1, _dir.FullName, 1024 * 1024); block.Write("test", ReadOnlyMemory.Empty, "hello"u8.ToArray()); block.Flush(); block.ClearCache(); // force disk read // Act — read should succeed with valid data var record = block.Read(1); // Assert record.ShouldNotBeNull(); record!.Subject.ShouldBe("test"); record.Payload.ToArray().ShouldBe("hello"u8.ToArray()); } // Go ref: filestore.go:8180 (checksum mismatch → error path) [Fact] public void MsgBlock_detects_corrupted_record_on_disk_read() { // Arrange — write a record, flush, clear cache so next read goes to disk using var block = MsgBlock.Create(1, _dir.FullName, 1024 * 1024); block.Write("test", ReadOnlyMemory.Empty, "hello"u8.ToArray()); block.Flush(); block.ClearCache(); // Corrupt a byte near the end of the block file (in the payload region) var files = Directory.GetFiles(_dir.FullName, "*.blk"); files.Length.ShouldBe(1); var bytes = File.ReadAllBytes(files[0]); // Flip a bit in the payload area (10 bytes from end: past checksum + timestamp) bytes[^10] ^= 0xFF; File.WriteAllBytes(files[0], bytes); // Act / Assert — Decode should throw on checksum mismatch Should.Throw(() => block.Read(1)); } // Go ref: filestore.go:2204 (lchk updated on each write) [Fact] public void MsgBlock_checksum_chain_across_writes() { // Arrange using var block = MsgBlock.Create(1, _dir.FullName, 1024 * 1024); // Act — write three records, capture checksum after each block.Write("a", ReadOnlyMemory.Empty, "one"u8.ToArray()); var checksum1 = block.LastChecksum?.ToArray(); block.Write("b", ReadOnlyMemory.Empty, "two"u8.ToArray()); var checksum2 = block.LastChecksum?.ToArray(); block.Write("c", ReadOnlyMemory.Empty, "three"u8.ToArray()); var checksum3 = block.LastChecksum?.ToArray(); // Assert — each write produces a non-null checksum that changes checksum1.ShouldNotBeNull(); checksum2.ShouldNotBeNull(); checksum3.ShouldNotBeNull(); checksum1.ShouldNotBe(checksum2!); checksum2.ShouldNotBe(checksum3!); } }