From 227abfc9af61838cf1412de89a7b3f26042d16c2 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Mon, 6 Jul 2026 12:47:41 -0600 Subject: [PATCH 1/3] Fix `ArgumentNullException` thrown from `Decoder.Convert` When a `ReadOnlySequence` that includes an empty `ReadOnlyMemory` segment is deserialized, the string interning converter may end up throwing `ArgumentNullException`. This fixes that bug. --- .../PolyfillExtensions.cs | 10 +++ .../SequenceBuilder.cs | 65 +++++++++++++++++++ .../StringInterningTests.cs | 9 +++ 3 files changed, 84 insertions(+) create mode 100644 test/Nerdbank.MessagePack.Tests/SequenceBuilder.cs diff --git a/src/Nerdbank.MessagePack/PolyfillExtensions.cs b/src/Nerdbank.MessagePack/PolyfillExtensions.cs index 8a01d19b..2fd3e16c 100644 --- a/src/Nerdbank.MessagePack/PolyfillExtensions.cs +++ b/src/Nerdbank.MessagePack/PolyfillExtensions.cs @@ -90,6 +90,11 @@ internal static unsafe int GetChars(this Encoding encoding, ReadOnlySpan s internal static unsafe int GetChars(this Encoding encoding, ReadOnlySequence source, Span destination) { + if (source.IsEmpty) + { + return 0; + } + if (source.IsSingleSegment) { return GetChars(encoding, source.First.Span, destination); @@ -100,6 +105,11 @@ internal static unsafe int GetChars(this Encoding encoding, ReadOnlySequence sourceSegment in source) { + if (sourceSegment.IsEmpty) + { + continue; + } + fixed (byte* pSource = sourceSegment.Span) { fixed (char* pDestination = destination) diff --git a/test/Nerdbank.MessagePack.Tests/SequenceBuilder.cs b/test/Nerdbank.MessagePack.Tests/SequenceBuilder.cs new file mode 100644 index 00000000..e1d525df --- /dev/null +++ b/test/Nerdbank.MessagePack.Tests/SequenceBuilder.cs @@ -0,0 +1,65 @@ +// Copyright (c) Andrew Arnott. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +/// +/// Helper class for constructing artificial instances for testing purposes. +/// +internal static class SequenceBuilder +{ + /// + /// Creates a from the provided segments. + /// + /// The type of element in the sequence. + /// The segments for the sequence. + /// A representing the concatenated segments. + /// + /// This does not use to construct sequences because that + /// type will not append empty segments, which for some tests is exactly what we need. + /// + internal static ReadOnlySequence Create(params ReadOnlyMemory[] segmentContents) + { + if (segmentContents.Length == 1) + { + return new ReadOnlySequence(segmentContents[0]); + } + + BufferSegment bufferSegment = new(segmentContents[0]); + BufferSegment? last = bufferSegment; + for (int i = 1; i < segmentContents.Length; i++) + { + last = last.Append(segmentContents[i]); + } + + return new ReadOnlySequence(bufferSegment, 0, last!, last!.Memory.Length); + } + + /// + internal static ReadOnlySequence Create(params T[][] segmentContents) + { + ReadOnlyMemory[] memorySegments = new ReadOnlyMemory[segmentContents.Length]; + for (int i = 0; i < segmentContents.Length; i++) + { + memorySegments[i] = segmentContents[i].AsMemory(); + } + + return Create(memorySegments); + } + + private sealed class BufferSegment : ReadOnlySequenceSegment + { + internal BufferSegment(ReadOnlyMemory memory) + { + this.Memory = memory; + } + + internal BufferSegment Append(ReadOnlyMemory memory) + { + var segment = new BufferSegment(memory) + { + RunningIndex = this.RunningIndex + this.Memory.Length, + }; + this.Next = segment; + return segment; + } + } +} diff --git a/test/Nerdbank.MessagePack.Tests/StringInterningTests.cs b/test/Nerdbank.MessagePack.Tests/StringInterningTests.cs index 676f1233..4415a1ed 100644 --- a/test/Nerdbank.MessagePack.Tests/StringInterningTests.cs +++ b/test/Nerdbank.MessagePack.Tests/StringInterningTests.cs @@ -56,6 +56,15 @@ public void Fragmented() Assert.Equal("abc", deserialized); } + [Fact] + public void FragmentedWithEmptySegment() + { + ReadOnlyMemory buffer = this.Serializer.Serialize("abc", TestContext.Current.CancellationToken); + ReadOnlySequence sequence = SequenceBuilder.Create(buffer[..2], ReadOnlyMemory.Empty, buffer[2..^1], buffer[^1..]); + string? deserialized = this.Serializer.Deserialize(sequence, TestContext.Current.CancellationToken); + Assert.Equal("abc", deserialized); + } + [GenerateShapeFor] [GenerateShapeFor] private partial class Witness; From bcf63537276c79416a41786b6f8b6c9875e0b92b Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Mon, 6 Jul 2026 13:10:14 -0600 Subject: [PATCH 2/3] Consolidate test helpers and add more empty sequence segment tests --- .../ArraysOfPrimitivesTests.cs | 10 ++++ .../MessagePackReaderTests.ReadString.cs | 53 ++++++------------- .../MessagePackReaderTests.cs | 28 +++------- 3 files changed, 32 insertions(+), 59 deletions(-) diff --git a/test/Nerdbank.MessagePack.Tests/ArraysOfPrimitivesTests.cs b/test/Nerdbank.MessagePack.Tests/ArraysOfPrimitivesTests.cs index 77773f9d..287efb68 100644 --- a/test/Nerdbank.MessagePack.Tests/ArraysOfPrimitivesTests.cs +++ b/test/Nerdbank.MessagePack.Tests/ArraysOfPrimitivesTests.cs @@ -40,6 +40,16 @@ public void Boolean([CombinatorialMemberData(nameof(GetInterestingLengths), type this.Roundtrip, Witness>(values); } + [Fact] + public void BoolArray_FragmentedWithEmptySegment() + { + ReadOnlyMemory buffer = this.Serializer.Serialize([true, false, true], TestContext.Current.CancellationToken); + ReadOnlySequence sequence = SequenceBuilder.Create(buffer[..2], ReadOnlyMemory.Empty, buffer[2..]); + bool[]? deserialized = this.Serializer.Deserialize(sequence, TestContext.Current.CancellationToken); + Assert.NotNull(deserialized); + Assert.Equal([true, false, true], deserialized); + } + [Theory, PairwiseData] public void Int8([CombinatorialMemberData(nameof(GetInterestingLengths), typeof(sbyte))] int length) => this.Roundtrip, Witness>(GetRandomValues(length)); diff --git a/test/Nerdbank.MessagePack.Tests/MessagePackReaderTests.ReadString.cs b/test/Nerdbank.MessagePack.Tests/MessagePackReaderTests.ReadString.cs index 1fd0d928..59b9b7fb 100644 --- a/test/Nerdbank.MessagePack.Tests/MessagePackReaderTests.ReadString.cs +++ b/test/Nerdbank.MessagePack.Tests/MessagePackReaderTests.ReadString.cs @@ -10,7 +10,7 @@ public partial class MessagePackReaderTests [Fact] public void ReadString_HandlesSingleSegment() { - ReadOnlySequence seq = this.BuildSequence(new[] + ReadOnlySequence seq = SequenceBuilder.Create(new[] { (byte)(MessagePackCode.MinFixStr + 2), (byte)'A', (byte)'B', @@ -24,7 +24,7 @@ public void ReadString_HandlesSingleSegment() [Fact] public void ReadString_HandlesMultipleSegments() { - ReadOnlySequence seq = this.BuildSequence( + ReadOnlySequence seq = SequenceBuilder.Create( new[] { (byte)(MessagePackCode.MinFixStr + 2), (byte)'A' }, new[] { (byte)'B' }); @@ -33,11 +33,24 @@ public void ReadString_HandlesMultipleSegments() Assert.Equal("AB", result); } + [Fact] + public void ReadString_HandlesMultipleSegments_WithEmptySegment() + { + ReadOnlySequence seq = SequenceBuilder.Create( + new[] { (byte)(MessagePackCode.MinFixStr + 2), (byte)'A' }, + [], + new[] { (byte)'B' }); + + var reader = new MessagePackReader(seq); + var result = reader.ReadString(); + Assert.Equal("AB", result); + } + [Fact] [Trait("CWE", "682")] public void ReadString_HandlesMultipleSegments_WithExpectedRemainingStructures() { - ReadOnlySequence seq = this.BuildSequence( + ReadOnlySequence seq = SequenceBuilder.Create( new[] { (byte)(MessagePackCode.MinFixArray + 2), (byte)(MessagePackCode.MinFixStr + 3), (byte)'A' }, new[] { (byte)'B', (byte)'C', (byte)MessagePackCode.Nil }); @@ -66,38 +79,4 @@ private static void AssertExpectedRemainingStructures(ref MessagePackReader read private static extern uint GetExpectedRemainingStructures(ref MessagePackReader reader); #endif - private ReadOnlySequence BuildSequence(params T[][] segmentContents) - { - if (segmentContents.Length == 1) - { - return new ReadOnlySequence(segmentContents[0].AsMemory()); - } - - var bufferSegment = new BufferSegment(segmentContents[0].AsMemory()); - BufferSegment? last = default; - for (var i = 1; i < segmentContents.Length; i++) - { - last = bufferSegment.Append(segmentContents[i]); - } - - return new ReadOnlySequence(bufferSegment, 0, last!, last!.Memory.Length); - } - - internal class BufferSegment : ReadOnlySequenceSegment - { - public BufferSegment(ReadOnlyMemory memory) - { - this.Memory = memory; - } - - public BufferSegment Append(ReadOnlyMemory memory) - { - var segment = new BufferSegment(memory) - { - RunningIndex = this.RunningIndex + this.Memory.Length, - }; - this.Next = segment; - return segment; - } - } } diff --git a/test/Nerdbank.MessagePack.Tests/MessagePackReaderTests.cs b/test/Nerdbank.MessagePack.Tests/MessagePackReaderTests.cs index b6360f3b..3218ba97 100644 --- a/test/Nerdbank.MessagePack.Tests/MessagePackReaderTests.cs +++ b/test/Nerdbank.MessagePack.Tests/MessagePackReaderTests.cs @@ -186,9 +186,9 @@ public void TryReadStringSpan_Fragmented() byte[] expected = [0x1, 0x2, 0x3]; writer.WriteString(expected); writer.Flush(); - ReadOnlySequence fragmentedSequence = BuildSequence( - contiguousSequence.AsReadOnlySequence.First.Slice(0, 2), - contiguousSequence.AsReadOnlySequence.First.Slice(2)); + ReadOnlySequence fragmentedSequence = SequenceBuilder.Create( + contiguousSequence.AsReadOnlySequence.First.Slice(0, 2), + contiguousSequence.AsReadOnlySequence.First.Slice(2)); var reader = new MessagePackReader(fragmentedSequence); Assert.False(reader.TryReadStringSpan(out ReadOnlySpan span)); @@ -253,9 +253,9 @@ public void ReadStringSpan_Fragmented() byte[] expected = [0x1, 0x2, 0x3]; writer.WriteString(expected); writer.Flush(); - ReadOnlySequence fragmentedSequence = BuildSequence( - contiguousSequence.AsReadOnlySequence.First.Slice(0, 2), - contiguousSequence.AsReadOnlySequence.First.Slice(2)); + ReadOnlySequence fragmentedSequence = SequenceBuilder.Create( + contiguousSequence.AsReadOnlySequence.First.Slice(0, 2), + contiguousSequence.AsReadOnlySequence.First.Slice(2)); var reader = new MessagePackReader(fragmentedSequence); ReadOnlySpan span = reader.ReadStringSpan(); @@ -453,22 +453,6 @@ private static ReadOnlySequence Encode(WriterEncoder cb) return sequence.AsReadOnlySequence; } - private static ReadOnlySequence BuildSequence(params ReadOnlyMemory[] memoryChunks) - { - var sequence = new Sequence(new ExactArrayPool()) - { - MinimumSpanLength = -1, - }; - foreach (ReadOnlyMemory chunk in memoryChunks) - { - Span span = sequence.GetSpan(chunk.Length); - chunk.Span.CopyTo(span); - sequence.Advance(chunk.Length); - } - - return sequence; - } - private void AssertCodeRange(RangeChecker predicate, Func isOneByteRepresentation, Func isIntroductoryByte) { bool mismatch = false; From 61f253cb0a69b1be53869d8d67472fac357083ad Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Mon, 6 Jul 2026 13:41:44 -0600 Subject: [PATCH 3/3] Fix secure hashing of `ReadOnlySequence` Previously, it was producing multiple hashes for 'equal' sequences based on where the segment boundaries fell. This change updates `SipHash` so that it properly supports streaming hashing such that the boundaries of each segment do not impact the resulting value. --- src/Nerdbank.MessagePack/Extension.cs | 14 +- .../HashCollisionResistantPrimitives.cs | 37 +- .../SecureHash/SipHash.cs | 342 +++++++++--------- .../StructuralEqualityComparerTests.cs | 21 +- 4 files changed, 186 insertions(+), 228 deletions(-) diff --git a/src/Nerdbank.MessagePack/Extension.cs b/src/Nerdbank.MessagePack/Extension.cs index 0cf1d54c..da457ea3 100644 --- a/src/Nerdbank.MessagePack/Extension.cs +++ b/src/Nerdbank.MessagePack/Extension.cs @@ -39,17 +39,5 @@ public Extension(sbyte typeCode, ReadOnlyMemory data) /// long IStructuralSecureEqualityComparer.GetSecureHashCode() - { - // We don't have an incremental SipHash implementation, so we have to copy the data to a rented buffer. - byte[] rented = ArrayPool.Shared.Rent(checked((int)this.Data.Length)); - try - { - this.Data.CopyTo(rented); - return SipHash.Default.Compute(rented.AsSpan(0, (int)this.Data.Length)) + this.TypeCode; - } - finally - { - ArrayPool.Shared.Return(rented); - } - } + => SipHash.Default.Compute(this.Data) + this.TypeCode; } diff --git a/src/Nerdbank.MessagePack/SecureHash/HashCollisionResistantPrimitives.cs b/src/Nerdbank.MessagePack/SecureHash/HashCollisionResistantPrimitives.cs index 1cd3ceb0..baceabb7 100644 --- a/src/Nerdbank.MessagePack/SecureHash/HashCollisionResistantPrimitives.cs +++ b/src/Nerdbank.MessagePack/SecureHash/HashCollisionResistantPrimitives.cs @@ -287,42 +287,7 @@ private ReadOnlySequenceOfBytesEqualityComparer() public override bool Equals(ReadOnlySequence x, ReadOnlySequence y) => x.SequenceEqual(y); - public override long GetSecureHashCode([DisallowNull] ReadOnlySequence obj) - { - int segmentCount = 0; - foreach (ReadOnlyMemory segment in obj) - { - if (++segmentCount > 64) - { - break; - } - } - - if (segmentCount <= 64) - { - Span hashesSpan = stackalloc long[segmentCount]; - int i = 0; - foreach (ReadOnlyMemory segment in obj) - { - hashesSpan[i++] = SecureHash(segment.Span); - } - - return SipHash.Default.Compute(MemoryMarshal.Cast(hashesSpan)); - } - - List hashes = []; - foreach (ReadOnlyMemory segment in obj) - { - hashes.Add(SecureHash(segment.Span)); - } - -#if NET - Span span = CollectionsMarshal.AsSpan(hashes); -#else - Span span = hashes.ToArray(); -#endif - return SipHash.Default.Compute(MemoryMarshal.Cast(span)); - } + public override long GetSecureHashCode([DisallowNull] ReadOnlySequence obj) => SipHash.Default.Compute(obj); } internal class CollisionResistantEnumHasher(SecureEqualityComparer equalityComparer) : SecureEqualityComparer diff --git a/src/Nerdbank.MessagePack/SecureHash/SipHash.cs b/src/Nerdbank.MessagePack/SecureHash/SipHash.cs index 152ca3c3..e1911283 100644 --- a/src/Nerdbank.MessagePack/SecureHash/SipHash.cs +++ b/src/Nerdbank.MessagePack/SecureHash/SipHash.cs @@ -7,7 +7,6 @@ //// SipHash website: https://131002.net/siphash/ using System.Buffers.Binary; -using System.Runtime.InteropServices; using System.Security.Cryptography; namespace Nerdbank.MessagePack.SecureHash; @@ -71,7 +70,7 @@ public SipHash(ReadOnlySpan key) } /// - /// Gets a 128-bit SipHash key. + /// Gets the 128-bit SipHash key used to construct this instance. /// /// The 16-byte buffer that receives the key originally provided to the constructor. public void GetKey(Span key) @@ -89,200 +88,187 @@ public void GetKey(Span key) /// The byte array for which to compute a SipHash tag. /// Returns 64-bit (8 bytes) SipHash tag. public long Compute(scoped ReadOnlySpan data) + { + IncrementalHasher hasher = this.CreateIncrementalHasher(); + hasher.Append(data); + return hasher.FinalizeHash(); + } + + /// Computes 64-bit SipHash tag for the specified sequence. + /// The byte sequence for which to compute a SipHash tag. + /// Returns 64-bit (8 bytes) SipHash tag. + public long Compute(scoped in ReadOnlySequence data) + { + if (data.IsSingleSegment) + { + return this.Compute(data.First.Span); + } + + IncrementalHasher hasher = this.CreateIncrementalHasher(); + foreach (ReadOnlyMemory segment in data) + { + hasher.Append(segment.Span); + } + + return hasher.FinalizeHash(); + } + + /// + /// Creates a stateful hasher that can accept data in multiple segments. + /// + /// An incremental SipHash hasher. + internal IncrementalHasher CreateIncrementalHasher() => new(this.initialState0, this.initialState1); + + private static void ProcessMessageBlock(ref ulong v0, ref ulong v1, ref ulong v2, ref ulong v3, ulong block) { unchecked { - // SipHash internal state - ulong v0 = this.initialState0; - ulong v1 = this.initialState1; + v3 ^= block; + SipRound(ref v0, ref v1, ref v2, ref v3); + SipRound(ref v0, ref v1, ref v2, ref v3); + v0 ^= block; + } + } - // It is faster to load the initialStateX fields from memory again than to reference v0 and v1: - ulong v2 = 0x1F160A001E161714UL ^ this.initialState0; - ulong v3 = 0x100A160317100A1EUL ^ this.initialState1; + private static void ProcessFinalBlock(ref ulong v0, ref ulong v1, ref ulong v2, ref ulong v3, ulong block) + { + unchecked + { + v3 ^= block; + SipRound(ref v0, ref v1, ref v2, ref v3); + SipRound(ref v0, ref v1, ref v2, ref v3); + v0 ^= block; + v2 ^= 0xff; + } + } - // We process data in 64-bit blocks - ulong block; + private static void FinalizeCore(ref ulong v0, ref ulong v1, ref ulong v2, ref ulong v3) + { + unchecked + { + SipRound(ref v0, ref v1, ref v2, ref v3); + SipRound(ref v0, ref v1, ref v2, ref v3); + SipRound(ref v0, ref v1, ref v2, ref v3); + SipRound(ref v0, ref v1, ref v2, ref v3); + } + } - // The last 64-bit block of data - int finalBlockPosition = data.Length & ~7; + private static void SipRound(ref ulong v0, ref ulong v1, ref ulong v2, ref ulong v3) + { + unchecked + { + v0 += v1; + v2 += v3; + v1 = (v1 << 13) | (v1 >> 51); + v3 = (v3 << 16) | (v3 >> 48); + v1 ^= v0; + v3 ^= v2; + v0 = (v0 << 32) | (v0 >> 32); + v2 += v1; + v0 += v3; + v1 = (v1 << 17) | (v1 >> 47); + v3 = (v3 << 21) | (v3 >> 43); + v1 ^= v2; + v3 ^= v0; + v2 = (v2 << 32) | (v2 >> 32); + } + } - // Process the input data in blocks of 64 bits - for (int blockPosition = 0; blockPosition < finalBlockPosition; blockPosition += sizeof(ulong)) + /// + /// Incrementally computes a SipHash value over multiple segments. + /// + internal struct IncrementalHasher + { + private ulong v0; + private ulong v1; + private ulong v2; + private ulong v3; + private ulong tailBlock; + private int tailLength; + private ulong totalLength; + + /// + /// Initializes a new instance of the struct. + /// + /// The first half of the keyed initial state. + /// The second half of the keyed initial state. + internal IncrementalHasher(ulong initialState0, ulong initialState1) + { + this.v0 = initialState0; + this.v1 = initialState1; + this.v2 = 0x1F160A001E161714UL ^ initialState0; + this.v3 = 0x100A160317100A1EUL ^ initialState1; + this.tailBlock = 0; + this.tailLength = 0; + this.totalLength = 0; + } + + /// + /// Appends data to the hash computation. + /// + /// The data to hash. + internal void Append(ReadOnlySpan data) + { + this.totalLength += (ulong)data.Length; + + if (this.tailLength > 0) { - block = MemoryMarshal.Read(data.Slice(blockPosition)); - - v3 ^= block; - - // Round 1 - v0 += v1; - v2 += v3; - v1 = v1 << 13 | v1 >> 51; - v3 = v3 << 16 | v3 >> 48; - v1 ^= v0; - v3 ^= v2; - v0 = v0 << 32 | v0 >> 32; - v2 += v1; - v0 += v3; - v1 = v1 << 17 | v1 >> 47; - v3 = v3 << 21 | v3 >> 43; - v1 ^= v2; - v3 ^= v0; - v2 = v2 << 32 | v2 >> 32; - - // Round 2 - v0 += v1; - v2 += v3; - v1 = v1 << 13 | v1 >> 51; - v3 = v3 << 16 | v3 >> 48; - v1 ^= v0; - v3 ^= v2; - v0 = v0 << 32 | v0 >> 32; - v2 += v1; - v0 += v3; - v1 = v1 << 17 | v1 >> 47; - v3 = v3 << 21 | v3 >> 43; - v1 ^= v2; - v3 ^= v0; - v2 = v2 << 32 | v2 >> 32; - - v0 ^= block; + int bytesNeeded = sizeof(ulong) - this.tailLength; + int bytesToCopy = Math.Min(bytesNeeded, data.Length); + for (int i = 0; i < bytesToCopy; i++) + { + this.tailBlock |= (ulong)data[i] << ((this.tailLength + i) * 8); + } + + this.tailLength += bytesToCopy; + data = data[bytesToCopy..]; + + if (this.tailLength < sizeof(ulong)) + { + return; + } + + if (this.tailLength == sizeof(ulong)) + { + ProcessMessageBlock(ref this.v0, ref this.v1, ref this.v2, ref this.v3, this.tailBlock); + this.tailBlock = 0; + this.tailLength = 0; + } } - // Load the remaining bytes - block = (ulong)data.Length << 56; - switch (data.Length & 7) + int finalBlockPosition = data.Length & ~7; + for (int blockPosition = 0; blockPosition < finalBlockPosition; blockPosition += sizeof(ulong)) { - case 7: - block |= MemoryMarshal.Read(data.Slice(finalBlockPosition)) | (ulong)MemoryMarshal.Read(data.Slice(finalBlockPosition + 4)) << 32 | (ulong)data[finalBlockPosition + 6] << 48; - break; - case 6: - block |= MemoryMarshal.Read(data.Slice(finalBlockPosition)) | (ulong)MemoryMarshal.Read(data.Slice(finalBlockPosition + 4)) << 32; - break; - case 5: - block |= MemoryMarshal.Read(data.Slice(finalBlockPosition)) | (ulong)data[finalBlockPosition + 4] << 32; - break; - case 4: - block |= MemoryMarshal.Read(data.Slice(finalBlockPosition)); - break; - case 3: - block |= MemoryMarshal.Read(data.Slice(finalBlockPosition)) | (ulong)data[finalBlockPosition + 2] << 16; - break; - case 2: - block |= MemoryMarshal.Read(data.Slice(finalBlockPosition)); - break; - case 1: - block |= data[finalBlockPosition]; - break; + ProcessMessageBlock(ref this.v0, ref this.v1, ref this.v2, ref this.v3, BinaryPrimitives.ReadUInt64LittleEndian(data.Slice(blockPosition))); } - // Process the final block + ReadOnlySpan tail = data[finalBlockPosition..]; + for (int i = 0; i < tail.Length; i++) { - v3 ^= block; - - // Round 1 - v0 += v1; - v2 += v3; - v1 = v1 << 13 | v1 >> 51; - v3 = v3 << 16 | v3 >> 48; - v1 ^= v0; - v3 ^= v2; - v0 = v0 << 32 | v0 >> 32; - v2 += v1; - v0 += v3; - v1 = v1 << 17 | v1 >> 47; - v3 = v3 << 21 | v3 >> 43; - v1 ^= v2; - v3 ^= v0; - v2 = v2 << 32 | v2 >> 32; - - // Round 2 - v0 += v1; - v2 += v3; - v1 = v1 << 13 | v1 >> 51; - v3 = v3 << 16 | v3 >> 48; - v1 ^= v0; - v3 ^= v2; - v0 = v0 << 32 | v0 >> 32; - v2 += v1; - v0 += v3; - v1 = v1 << 17 | v1 >> 47; - v3 = v3 << 21 | v3 >> 43; - v1 ^= v2; - v3 ^= v0; - v2 = v2 << 32 | v2 >> 32; - - v0 ^= block; - v2 ^= 0xff; + this.tailBlock |= (ulong)tail[i] << (i * 8); } - // 4 finalization rounds + this.tailLength = tail.Length; + } + + /// + /// Computes the hash for all data appended so far. + /// + /// The 64-bit SipHash tag. + internal long FinalizeHash() + { + unchecked { - // Round 1 - v0 += v1; - v2 += v3; - v1 = v1 << 13 | v1 >> 51; - v3 = v3 << 16 | v3 >> 48; - v1 ^= v0; - v3 ^= v2; - v0 = v0 << 32 | v0 >> 32; - v2 += v1; - v0 += v3; - v1 = v1 << 17 | v1 >> 47; - v3 = v3 << 21 | v3 >> 43; - v1 ^= v2; - v3 ^= v0; - v2 = v2 << 32 | v2 >> 32; - - // Round 2 - v0 += v1; - v2 += v3; - v1 = v1 << 13 | v1 >> 51; - v3 = v3 << 16 | v3 >> 48; - v1 ^= v0; - v3 ^= v2; - v0 = v0 << 32 | v0 >> 32; - v2 += v1; - v0 += v3; - v1 = v1 << 17 | v1 >> 47; - v3 = v3 << 21 | v3 >> 43; - v1 ^= v2; - v3 ^= v0; - v2 = v2 << 32 | v2 >> 32; - - // Round 3 - v0 += v1; - v2 += v3; - v1 = v1 << 13 | v1 >> 51; - v3 = v3 << 16 | v3 >> 48; - v1 ^= v0; - v3 ^= v2; - v0 = v0 << 32 | v0 >> 32; - v2 += v1; - v0 += v3; - v1 = v1 << 17 | v1 >> 47; - v3 = v3 << 21 | v3 >> 43; - v1 ^= v2; - v3 ^= v0; - v2 = v2 << 32 | v2 >> 32; - - // Round 4 - v0 += v1; - v2 += v3; - v1 = v1 << 13 | v1 >> 51; - v3 = v3 << 16 | v3 >> 48; - v1 ^= v0; - v3 ^= v2; - v0 = v0 << 32 | v0 >> 32; - v2 += v1; - v0 += v3; - v1 = v1 << 17 | v1 >> 47; - v3 = v3 << 21 | v3 >> 43; - v1 ^= v2; - v3 ^= v0; - v2 = v2 << 32 | v2 >> 32; + ulong v0 = this.v0; + ulong v1 = this.v1; + ulong v2 = this.v2; + ulong v3 = this.v3; + ulong finalBlock = this.tailBlock | ((this.totalLength & 0xFFUL) << 56); + + ProcessFinalBlock(ref v0, ref v1, ref v2, ref v3, finalBlock); + FinalizeCore(ref v0, ref v1, ref v2, ref v3); + return (long)(v0 ^ v1 ^ v2 ^ v3); } - - return (long)(v0 ^ v1 ^ v2 ^ v3); } } } diff --git a/test/Nerdbank.MessagePack.Tests/StructuralEqualityComparerTests.cs b/test/Nerdbank.MessagePack.Tests/StructuralEqualityComparerTests.cs index ba1f59e4..ad8638a8 100644 --- a/test/Nerdbank.MessagePack.Tests/StructuralEqualityComparerTests.cs +++ b/test/Nerdbank.MessagePack.Tests/StructuralEqualityComparerTests.cs @@ -61,7 +61,12 @@ public void ReadOnlyMemoryOfByte() => this.AssertEqualityComparerBehavior( [Fact] public void ReadOnlySequenceOfByte() => this.AssertEqualityComparerBehavior( - [new HaveReadOnlySequenceOfByte(new([1, 2])), new HaveReadOnlySequenceOfByte(new([1, 2]))], + [ + new HaveReadOnlySequenceOfByte(new([1, 2])), + new HaveReadOnlySequenceOfByte(new([1, 2])), + new HaveReadOnlySequenceOfByte(SequenceBuilder.Create(new byte[] { 1 }, new byte[] { 2 })), + new HaveReadOnlySequenceOfByte(SequenceBuilder.Create(new byte[] { 1 }, ReadOnlyMemory.Empty, new byte[] { 2 })), + ], [new HaveReadOnlySequenceOfByte(new([1, 3])), new HaveReadOnlySequenceOfByte(new([1, 2, 3]))]); [Fact] @@ -266,6 +271,20 @@ public void Uri() Assert.Equal(comparer.GetHashCode(relativeFirst), comparer.GetHashCode(relativeSecond)); } + [Fact] + public void Extension_IgnoresSegmentBoundaries() + { + IEqualityComparer comparer = this.GetEqualityComparer(); + Extension contiguous = new(5, new byte[] { 1, 2 }); + Extension segmented = new(5, SequenceBuilder.Create(new byte[] { 1 }, new byte[] { 2 })); + Extension segmentedWithEmpty = new(5, SequenceBuilder.Create(new byte[] { 1 }, ReadOnlyMemory.Empty, new byte[] { 2 })); + + Assert.True(comparer.Equals(contiguous, segmented)); + Assert.Equal(comparer.GetHashCode(contiguous), comparer.GetHashCode(segmented)); + Assert.True(comparer.Equals(contiguous, segmentedWithEmpty)); + Assert.Equal(comparer.GetHashCode(contiguous), comparer.GetHashCode(segmentedWithEmpty)); + } + [Fact] public override void CustomHash() {