// Reference: golang/nats-server/server/filestore.go // Go wire format: filestore.go:6720-6724 (writeMsgRecordLocked) // Go decode: filestore.go:8180-8250 (msgFromBufEx) // Go size calc: filestore.go:8770-8777 (fileStoreMsgSizeRaw) // // These tests verify the .NET MessageRecord binary encoder/decoder that matches // the Go message block record format: // [1:flags][varint:subj_len][N:subject][varint:hdr_len][M:headers][varint:payload_len][P:payload][8:sequence_LE][8:checksum] using NATS.Server.JetStream.Storage; using System.Text; namespace NATS.Server.Tests.JetStream.Storage; public sealed class MessageRecordTests { // Go: writeMsgRecordLocked / msgFromBufEx — basic round-trip [Fact] public void RoundTrip_SimpleMessage() { var record = new MessageRecord { Sequence = 42, Subject = "foo.bar", Headers = ReadOnlyMemory.Empty, Payload = Encoding.UTF8.GetBytes("hello world"), Timestamp = 1_700_000_000_000_000_000L, Deleted = false, }; var encoded = MessageRecord.Encode(record); var decoded = MessageRecord.Decode(encoded); decoded.Sequence.ShouldBe(record.Sequence); decoded.Subject.ShouldBe(record.Subject); decoded.Headers.Length.ShouldBe(0); decoded.Payload.ToArray().ShouldBe(Encoding.UTF8.GetBytes("hello world")); decoded.Timestamp.ShouldBe(record.Timestamp); decoded.Deleted.ShouldBe(false); } // Go: writeMsgRecordLocked with headers — hdr_len(4) hdr present in record [Fact] public void RoundTrip_WithHeaders() { var headers = "NATS/1.0\r\nX-Test: value\r\n\r\n"u8.ToArray(); var record = new MessageRecord { Sequence = 99, Subject = "test.headers", Headers = headers, Payload = Encoding.UTF8.GetBytes("payload with headers"), Timestamp = 1_700_000_000_000_000_000L, Deleted = false, }; var encoded = MessageRecord.Encode(record); var decoded = MessageRecord.Decode(encoded); decoded.Sequence.ShouldBe(99UL); decoded.Subject.ShouldBe("test.headers"); decoded.Headers.ToArray().ShouldBe(headers); decoded.Payload.ToArray().ShouldBe(Encoding.UTF8.GetBytes("payload with headers")); decoded.Timestamp.ShouldBe(record.Timestamp); } // Verify that the last 8 bytes of the encoded record contain a nonzero XxHash64 checksum. [Fact] public void Encode_SetsChecksumInTrailer() { var record = new MessageRecord { Sequence = 1, Subject = "checksum.test", Headers = ReadOnlyMemory.Empty, Payload = Encoding.UTF8.GetBytes("data"), Timestamp = 0, Deleted = false, }; var encoded = MessageRecord.Encode(record); // Last 8 bytes are the checksum — should be nonzero for any non-trivial message. var checksumBytes = encoded.AsSpan(encoded.Length - 8); var checksum = BitConverter.ToUInt64(checksumBytes); checksum.ShouldNotBe(0UL); } // Flip a byte in the encoded data and verify decode throws InvalidDataException. [Fact] public void Decode_DetectsCorruptChecksum() { var record = new MessageRecord { Sequence = 7, Subject = "corrupt", Headers = ReadOnlyMemory.Empty, Payload = Encoding.UTF8.GetBytes("will be corrupted"), Timestamp = 0, Deleted = false, }; var encoded = MessageRecord.Encode(record); // Flip a byte in the payload area (not the checksum itself). var corrupted = encoded.ToArray(); corrupted[corrupted.Length / 2] ^= 0xFF; Should.Throw(() => MessageRecord.Decode(corrupted)); } // Go: varint encoding matches protobuf convention — high-bit continuation. [Theory] [InlineData(0UL)] [InlineData(1UL)] [InlineData(127UL)] [InlineData(128UL)] [InlineData(16383UL)] [InlineData(16384UL)] public void Varint_RoundTrip(ulong value) { Span buf = stackalloc byte[10]; var written = MessageRecord.WriteVarint(buf, value); written.ShouldBeGreaterThan(0); var (decoded, bytesRead) = MessageRecord.ReadVarint(buf); decoded.ShouldBe(value); bytesRead.ShouldBe(written); } // Go: ebit (1 << 63) marks deleted/erased messages in the sequence field. [Fact] public void RoundTrip_DeletedFlag() { var record = new MessageRecord { Sequence = 100, Subject = "deleted.msg", Headers = ReadOnlyMemory.Empty, Payload = ReadOnlyMemory.Empty, Timestamp = 0, Deleted = true, }; var encoded = MessageRecord.Encode(record); var decoded = MessageRecord.Decode(encoded); decoded.Deleted.ShouldBe(true); decoded.Sequence.ShouldBe(100UL); decoded.Subject.ShouldBe("deleted.msg"); } // Edge case: empty payload should encode and decode cleanly. [Fact] public void RoundTrip_EmptyPayload() { var record = new MessageRecord { Sequence = 1, Subject = "empty", Headers = ReadOnlyMemory.Empty, Payload = ReadOnlyMemory.Empty, Timestamp = 0, Deleted = false, }; var encoded = MessageRecord.Encode(record); var decoded = MessageRecord.Decode(encoded); decoded.Subject.ShouldBe("empty"); decoded.Payload.Length.ShouldBe(0); decoded.Headers.Length.ShouldBe(0); } // Verify 64KB payload works (large payload stress test). [Fact] public void RoundTrip_LargePayload() { var payload = new byte[64 * 1024]; Random.Shared.NextBytes(payload); var record = new MessageRecord { Sequence = 999_999, Subject = "large.payload.test", Headers = ReadOnlyMemory.Empty, Payload = payload, Timestamp = long.MaxValue, Deleted = false, }; var encoded = MessageRecord.Encode(record); var decoded = MessageRecord.Decode(encoded); decoded.Sequence.ShouldBe(999_999UL); decoded.Subject.ShouldBe("large.payload.test"); decoded.Payload.ToArray().ShouldBe(payload); decoded.Timestamp.ShouldBe(long.MaxValue); } }