diff --git a/LiteDB.Tests/Internals/BufferWriter_Tests.cs b/LiteDB.Tests/Internals/BufferWriter_Tests.cs index da5d3366f..09d324485 100644 --- a/LiteDB.Tests/Internals/BufferWriter_Tests.cs +++ b/LiteDB.Tests/Internals/BufferWriter_Tests.cs @@ -275,6 +275,70 @@ public void Buffer_Write_Types() p += PageAddress.SIZE; } + [Fact] + public void Buffer_Write_Guid_ObjectId_Across_Segments() + { + var guid = new Guid("01020304-0506-0708-090A-0B0C0D0E0F10"); + var objectId = new ObjectId(0x11223344, 0x556677, 0x6677, 0xAABBCC); + + var slices = new[] + { + new BufferSlice(new byte[8], 0, 8), + new BufferSlice(new byte[10], 0, 10), + new BufferSlice(new byte[12], 0, 12) + }; + + using (var writer = new BufferWriter(slices)) + { + writer.Write(guid); + writer.Write(objectId); + } + + var expectedGuidBytes = guid.ToByteArray(); + var actualGuidBytes = new byte[16]; + + Buffer.BlockCopy(slices[0].Array, slices[0].Offset, actualGuidBytes, 0, slices[0].Count); + Buffer.BlockCopy(slices[1].Array, slices[1].Offset, actualGuidBytes, slices[0].Count, 16 - slices[0].Count); + + actualGuidBytes.Should().Equal(expectedGuidBytes); + + var expectedObjectIdBytes = objectId.ToByteArray(); + var actualObjectIdBytes = new byte[12]; + + Buffer.BlockCopy(slices[1].Array, slices[1].Offset + 16 - slices[0].Count, actualObjectIdBytes, 0, slices[1].Count - (16 - slices[0].Count)); + Buffer.BlockCopy(slices[2].Array, slices[2].Offset, actualObjectIdBytes, slices[1].Count - (16 - slices[0].Count), 12 - (slices[1].Count - (16 - slices[0].Count))); + + actualObjectIdBytes.Should().Equal(expectedObjectIdBytes); + + using (var reader = new BufferReader(slices)) + { + reader.ReadGuid().Should().Be(guid); + reader.ReadObjectId().Should().Be(objectId); + } + } + + [Fact] + public void BufferSlice_Span_Based_Guid_ObjectId_Should_Preserve_Endianness() + { + var buffer = new BufferSlice(new byte[64], 4, 40); + var guid = new Guid("0F0E0D0C-0B0A-0908-0706-050403020100"); + var objectId = new ObjectId(0x0A0B0C0D, 0x010203, 0x0405, 0x060708); + + buffer.Write(guid, 3); + buffer.Write(objectId, 21); + + buffer.ReadGuid(3).Should().Be(guid); + buffer.ReadObjectId(21).Should().Be(objectId); + + var guidBytes = new byte[16]; + Buffer.BlockCopy(buffer.Array, buffer.Offset + 3, guidBytes, 0, 16); + guidBytes.Should().Equal(guid.ToByteArray()); + + var objectIdBytes = new byte[12]; + Buffer.BlockCopy(buffer.Array, buffer.Offset + 21, objectIdBytes, 0, 12); + objectIdBytes.Should().Equal(objectId.ToByteArray()); + } + [Fact] public void Buffer_Write_Overflow() { diff --git a/LiteDB/Engine/Disk/Serializer/BufferReader.cs b/LiteDB/Engine/Disk/Serializer/BufferReader.cs index 980ff2542..05f541beb 100644 --- a/LiteDB/Engine/Disk/Serializer/BufferReader.cs +++ b/LiteDB/Engine/Disk/Serializer/BufferReader.cs @@ -2,6 +2,7 @@ using System.Buffers; using System.Collections.Generic; using System.IO; +using System.Runtime.InteropServices; using static LiteDB.Constants; namespace LiteDB.Engine @@ -124,6 +125,28 @@ public int Read(byte[] buffer, int offset, int count) return bufferPosition; } + private void Read(Span destination) + { + var written = 0; + + while (written < destination.Length) + { + var bytesLeft = _current.Count - _currentPosition; + var bytesToCopy = Math.Min(destination.Length - written, bytesLeft); + + new ReadOnlySpan(_current.Array, _current.Offset + _currentPosition, bytesToCopy) + .CopyTo(destination.Slice(written, bytesToCopy)); + + written += bytesToCopy; + + this.MoveForward(bytesToCopy); + + if (_isEOF) break; + } + + ENSURE(written == destination.Length, "current value must fit inside defined buffer"); + } + /// /// Skip bytes (same as Read but with no array copy) /// @@ -245,8 +268,11 @@ public Guid ReadGuid() } else { - // can't use _tempoBuffer because Guid validate 16 bytes array length - value = new Guid(this.ReadBytes(16)); + Span buffer = stackalloc byte[16]; + + this.Read(buffer); + + value = MemoryMarshal.Read(buffer); } return value; @@ -261,19 +287,17 @@ public ObjectId ReadObjectId() if (_currentPosition + 12 <= _current.Count) { - value = new ObjectId(_current.Array, _current.Offset + _currentPosition); + value = BufferSliceExtensions.ReadObjectId(new ReadOnlySpan(_current.Array, _current.Offset + _currentPosition, 12)); this.MoveForward(12); } else { - var buffer = _bufferPool.Rent(12); + Span buffer = stackalloc byte[12]; - this.Read(buffer, 0, 12); + this.Read(buffer); - value = new ObjectId(buffer, 0); - - _bufferPool.Return(buffer, true); + value = BufferSliceExtensions.ReadObjectId(buffer); } return value; diff --git a/LiteDB/Engine/Disk/Serializer/BufferWriter.cs b/LiteDB/Engine/Disk/Serializer/BufferWriter.cs index 83c6c26f8..8a3850a83 100644 --- a/LiteDB/Engine/Disk/Serializer/BufferWriter.cs +++ b/LiteDB/Engine/Disk/Serializer/BufferWriter.cs @@ -1,6 +1,8 @@ -using System; +using System; using System.Buffers; using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using static LiteDB.Constants; namespace LiteDB.Engine @@ -125,6 +127,28 @@ public int Write(byte[] buffer, int offset, int count) /// public int Write(byte[] buffer) => this.Write(buffer, 0, buffer.Length); + private void Write(ReadOnlySpan source) + { + var written = 0; + + while (written < source.Length) + { + var bytesLeft = _current.Count - _currentPosition; + var bytesToCopy = Math.Min(source.Length - written, bytesLeft); + + source.Slice(written, bytesToCopy) + .CopyTo(new Span(_current.Array, _current.Offset + _currentPosition, bytesToCopy)); + + written += bytesToCopy; + + this.MoveForward(bytesToCopy); + + if (_isEOF) break; + } + + ENSURE(written == source.Length, "current value must fit inside defined buffer"); + } + /// /// Skip bytes (same as Write but with no array copy) /// @@ -217,10 +241,36 @@ public void Write(DateTime value) /// public void Write(Guid value) { - // there is no avaiable value.TryWriteBytes (TODO: implement conditional compile)? - var bytes = value.ToByteArray(); + if (_currentPosition + 16 <= _current.Count) + { + var span = new Span(_current.Array, _current.Offset + _currentPosition, 16); - this.Write(bytes, 0, 16); +#if NET8_0_OR_GREATER + if (!value.TryWriteBytes(span)) + { + throw new InvalidOperationException("Failed to write Guid into span."); + } +#else + Unsafe.WriteUnaligned(ref MemoryMarshal.GetReference(span), value); +#endif + + this.MoveForward(16); + } + else + { + Span buffer = stackalloc byte[16]; + +#if NET8_0_OR_GREATER + if (!value.TryWriteBytes(buffer)) + { + throw new InvalidOperationException("Failed to write Guid into span."); + } +#else + Unsafe.WriteUnaligned(ref MemoryMarshal.GetReference(buffer), value); +#endif + + this.Write(buffer); + } } /// @@ -230,19 +280,19 @@ public void Write(ObjectId value) { if (_currentPosition + 12 <= _current.Count) { - value.ToByteArray(_current.Array, _current.Offset + _currentPosition); + var span = new Span(_current.Array, _current.Offset + _currentPosition, 12); + + BufferSliceExtensions.Write(span, value); this.MoveForward(12); } else { - var buffer = _bufferPool.Rent(12); + Span buffer = stackalloc byte[12]; - value.ToByteArray(buffer, 0); + BufferSliceExtensions.Write(buffer, value); - this.Write(buffer, 0, 12); - - _bufferPool.Return(buffer, true); + this.Write(buffer); } } @@ -444,3 +494,4 @@ public void Dispose() } } } + diff --git a/LiteDB/LiteDB.csproj b/LiteDB/LiteDB.csproj index c1a6f2070..07e04de7e 100644 --- a/LiteDB/LiteDB.csproj +++ b/LiteDB/LiteDB.csproj @@ -1,4 +1,4 @@ - + netstandard2.0;net8.0 @@ -63,9 +63,10 @@ - + + diff --git a/LiteDB/Utils/Extensions/BufferSliceExtensions.cs b/LiteDB/Utils/Extensions/BufferSliceExtensions.cs index 049ce931f..e4539f39a 100644 --- a/LiteDB/Utils/Extensions/BufferSliceExtensions.cs +++ b/LiteDB/Utils/Extensions/BufferSliceExtensions.cs @@ -2,6 +2,8 @@ using System; using System.Linq; using System.Text; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using static LiteDB.Constants; namespace LiteDB @@ -66,12 +68,48 @@ public static Decimal ReadDecimal(this BufferSlice buffer, int offset) public static ObjectId ReadObjectId(this BufferSlice buffer, int offset) { - return new ObjectId(buffer.Array, buffer.Offset + offset); + var span = new ReadOnlySpan(buffer.Array, buffer.Offset + offset, 12); + + return ReadObjectId(span); } public static Guid ReadGuid(this BufferSlice buffer, int offset) { - return new Guid(buffer.ReadBytes(offset, 16)); + var span = new ReadOnlySpan(buffer.Array, buffer.Offset + offset, 16); + + return ReadGuid(span); + } + + internal static ObjectId ReadObjectId(ReadOnlySpan span) + { + ENSURE(span.Length >= 12, "span must contain at least 12 bytes"); + + var timestamp = + (span[0] << 24) | + (span[1] << 16) | + (span[2] << 8) | + span[3]; + + var machine = + (span[4] << 16) | + (span[5] << 8) | + span[6]; + + var pid = (short)((span[7] << 8) | span[8]); + + var increment = + (span[9] << 16) | + (span[10] << 8) | + span[11]; + + return new ObjectId(timestamp, machine, pid, increment); + } + + internal static Guid ReadGuid(ReadOnlySpan span) + { + ENSURE(span.Length >= 16, "span must contain at least 16 bytes"); + + return MemoryMarshal.Read(span); } public static byte[] ReadBytes(this BufferSlice buffer, int offset, int count) @@ -256,7 +294,16 @@ public static void Write(this BufferSlice buffer, PageAddress value, int offset) public static void Write(this BufferSlice buffer, Guid value, int offset) { - buffer.Write(value.ToByteArray(), offset); + var span = new Span(buffer.Array, buffer.Offset + offset, 16); + +#if NET8_0_OR_GREATER + if (!value.TryWriteBytes(span)) + { + throw new InvalidOperationException("Failed to write Guid into span."); + } +#else + Unsafe.WriteUnaligned(ref MemoryMarshal.GetReference(span), value); +#endif } public static void Write(this BufferSlice buffer, float[] value, int offset) @@ -272,7 +319,30 @@ public static void Write(this BufferSlice buffer, float[] value, int offset) public static void Write(this BufferSlice buffer, ObjectId value, int offset) { - value.ToByteArray(buffer.Array, buffer.Offset + offset); + var span = new Span(buffer.Array, buffer.Offset + offset, 12); + + Write(span, value); + } + + internal static void Write(Span destination, ObjectId value) + { + ENSURE(destination.Length >= 12, "span must contain at least 12 bytes"); + + destination[0] = (byte)(value.Timestamp >> 24); + destination[1] = (byte)(value.Timestamp >> 16); + destination[2] = (byte)(value.Timestamp >> 8); + destination[3] = (byte)(value.Timestamp); + + destination[4] = (byte)(value.Machine >> 16); + destination[5] = (byte)(value.Machine >> 8); + destination[6] = (byte)(value.Machine); + + destination[7] = (byte)(value.Pid >> 8); + destination[8] = (byte)(value.Pid); + + destination[9] = (byte)(value.Increment >> 16); + destination[10] = (byte)(value.Increment >> 8); + destination[11] = (byte)(value.Increment); } public static void Write(this BufferSlice buffer, byte[] value, int offset)