diff --git a/src/MongoDB.Bson/IO/BinaryPrimitivesCompat.cs b/src/MongoDB.Bson/IO/BinaryPrimitivesCompat.cs index 323649f6d08..87ea9d48fb8 100644 --- a/src/MongoDB.Bson/IO/BinaryPrimitivesCompat.cs +++ b/src/MongoDB.Bson/IO/BinaryPrimitivesCompat.cs @@ -15,6 +15,7 @@ using System; using System.Buffers.Binary; +using System.Runtime.InteropServices; namespace MongoDB.Bson.IO { @@ -31,5 +32,55 @@ public static void WriteDoubleLittleEndian(Span destination, double value) { BinaryPrimitives.WriteInt64LittleEndian(destination, BitConverter.DoubleToInt64Bits(value)); } + + public static float ReadSingleLittleEndian(ReadOnlySpan source) + { +#if NET6_0_OR_GREATER + return BinaryPrimitives.ReadSingleLittleEndian(source); +#else + if (source.Length < 4) + { + throw new ArgumentOutOfRangeException(nameof(source), "Source span is too small to contain a float."); + } + + // Constructs a 32-bit float from 4 Little Endian bytes in a platform-agnostic way. + // Ensures correct bit pattern regardless of system endianness. + int intValue = + source[0] | + (source[1] << 8) | + (source[2] << 16) | + (source[3] << 24); + + // This struct emulates BitConverter.Int32BitsToSingle for platforms like net472. + return new FloatIntUnion { IntValue = intValue }.FloatValue; +#endif + } + + public static void WriteSingleLittleEndian(Span destination, float value) + { +#if NET6_0_OR_GREATER + BinaryPrimitives.WriteSingleLittleEndian(destination, value); +#else + if (destination.Length < 4) + { + throw new ArgumentOutOfRangeException(nameof(destination), "Destination span is too small to hold a float."); + } + + // This struct emulates BitConverter.SingleToInt32Bits for platforms like net472. + int intValue = new FloatIntUnion { FloatValue = value }.IntValue; + + destination[0] = (byte)(intValue); + destination[1] = (byte)(intValue >> 8); + destination[2] = (byte)(intValue >> 16); + destination[3] = (byte)(intValue >> 24); +#endif + } + + [StructLayout(LayoutKind.Explicit)] + private struct FloatIntUnion + { + [FieldOffset(0)] public float FloatValue; + [FieldOffset(0)] public int IntValue; + } } } diff --git a/src/MongoDB.Bson/Serialization/BinaryVectorReader.cs b/src/MongoDB.Bson/Serialization/BinaryVectorReader.cs index ef83c201091..3b751f3c9d6 100644 --- a/src/MongoDB.Bson/Serialization/BinaryVectorReader.cs +++ b/src/MongoDB.Bson/Serialization/BinaryVectorReader.cs @@ -17,6 +17,7 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; +using MongoDB.Bson.IO; namespace MongoDB.Bson.Serialization { @@ -41,21 +42,8 @@ public static (TItem[] Items, byte Padding, BinaryVectorDataType VectorDataType) switch (vectorDataType) { case BinaryVectorDataType.Float32: - - if ((vectorDataBytes.Span.Length & 3) != 0) - { - throw new FormatException("Data length of binary vector of type Float32 must be a multiple of 4 bytes."); - } - - if (BitConverter.IsLittleEndian) - { - var singles = MemoryMarshal.Cast(vectorDataBytes.Span); - items = (TItem[])(object)singles.ToArray(); - } - else - { - throw new NotSupportedException("Binary vector data is not supported on Big Endian architecture yet."); - } + var floatArray = ReadSinglesArrayLittleEndian(vectorDataBytes.Span); + items = (TItem[])(object)floatArray; break; case BinaryVectorDataType.Int8: var itemsSpan = MemoryMarshal.Cast(vectorDataBytes.Span); @@ -123,6 +111,30 @@ TExpectedItem[] AsTypedArrayOrThrow() return result; } } + + private static float[] ReadSinglesArrayLittleEndian(ReadOnlySpan span) + { + if ((span.Length & 3) != 0) + { + throw new FormatException("Data length of binary vector of type Float32 must be a multiple of 4 bytes."); + } + + float[] result; + if (BitConverter.IsLittleEndian) + { + result = MemoryMarshal.Cast(span).ToArray(); + } + else + { + var count = span.Length / 4; + result = new float[count]; + for (int i = 0; i < count; i++) + { + result[i] = BinaryPrimitivesCompat.ReadSingleLittleEndian(span.Slice(i * 4, 4)); + } + } + return result; + } public static void ValidateItemType(BinaryVectorDataType binaryVectorDataType) { diff --git a/src/MongoDB.Bson/Serialization/BinaryVectorWriter.cs b/src/MongoDB.Bson/Serialization/BinaryVectorWriter.cs index 0e9d5e74f6d..9f1ba73b075 100644 --- a/src/MongoDB.Bson/Serialization/BinaryVectorWriter.cs +++ b/src/MongoDB.Bson/Serialization/BinaryVectorWriter.cs @@ -15,6 +15,7 @@ using System; using System.Runtime.InteropServices; +using MongoDB.Bson.IO; namespace MongoDB.Bson.Serialization { @@ -35,15 +36,39 @@ public static byte[] WriteToBytes(BinaryVector binaryVector) public static byte[] WriteToBytes(ReadOnlySpan vectorData, BinaryVectorDataType binaryVectorDataType, byte padding) where TItem : struct { - if (!BitConverter.IsLittleEndian) + switch (binaryVectorDataType) { - throw new NotSupportedException("Binary vector data is not supported on Big Endian architecture yet."); - } + case BinaryVectorDataType.Float32: + var length = vectorData.Length * 4; + var result = new byte[2 + length]; + result[0] = (byte)binaryVectorDataType; + result[1] = padding; + + var floatSpan = MemoryMarshal.Cast(vectorData); + var floatOutput = result.AsSpan(2); + + if (BitConverter.IsLittleEndian) + { + MemoryMarshal.Cast(floatSpan).CopyTo(floatOutput); + } + else + { + for (int i = 0; i < floatSpan.Length; i++) + { + BinaryPrimitivesCompat.WriteSingleLittleEndian(floatOutput.Slice(i * 4, 4), floatSpan[i]); + } + } - var vectorDataBytes = MemoryMarshal.Cast(vectorData); - byte[] result = [(byte)binaryVectorDataType, padding, .. vectorDataBytes]; + return result; - return result; + case BinaryVectorDataType.Int8: + case BinaryVectorDataType.PackedBit: + var vectorDataBytes = MemoryMarshal.Cast(vectorData); + return [(byte)binaryVectorDataType, padding, .. vectorDataBytes]; + + default: + throw new NotSupportedException($"Binary vector serialization is not supported for {binaryVectorDataType}."); + } } } } diff --git a/tests/MongoDB.Bson.Tests/IO/BinaryPrimitivesCompatTests.cs b/tests/MongoDB.Bson.Tests/IO/BinaryPrimitivesCompatTests.cs new file mode 100644 index 00000000000..03ff78e467c --- /dev/null +++ b/tests/MongoDB.Bson.Tests/IO/BinaryPrimitivesCompatTests.cs @@ -0,0 +1,89 @@ +/* Copyright 2010-present MongoDB Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System; +using Xunit; +using FluentAssertions; +using MongoDB.Bson.IO; + +namespace MongoDB.Bson.Tests.IO +{ + public class BinaryPrimitivesCompatTests + { + [Fact] + public void ReadSingleLittleEndian_should_read_correctly() + { + var bytes = new byte[] { 0x00, 0x00, 0x80, 0x3F }; // 1.0f in little endian + var result = BinaryPrimitivesCompat.ReadSingleLittleEndian(bytes); + result.Should().Be(1.0f); + } + + [Fact] + public void ReadSingleLittleEndian_should_throw_on_insufficient_length() + { + var shortBuffer = new byte[3]; + var exception = Record.Exception(() => + BinaryPrimitivesCompat.ReadSingleLittleEndian(shortBuffer)); + + var e = exception.Should().BeOfType().Subject; + e.ParamName.Should().Be("length"); + } + + [Fact] + public void WriteSingleLittleEndian_should_throw_on_insufficient_length() + { + var shortBuffer = new byte[3]; + var exception = Record.Exception(() => + BinaryPrimitivesCompat.WriteSingleLittleEndian(shortBuffer, 1.23f)); + + var e = exception.Should().BeOfType().Subject; + e.ParamName.Should().Be("length"); + } + + [Fact] + public void WriteSingleLittleEndian_should_write_correctly() + { + Span buffer = new byte[4]; + BinaryPrimitivesCompat.WriteSingleLittleEndian(buffer, 1.0f); + buffer.ToArray().Should().Equal(0x00, 0x00, 0x80, 0x3F); // 1.0f little-endian + } + + [Theory] + [InlineData(0f)] + [InlineData(1.0f)] + [InlineData(-1.5f)] + [InlineData(float.MaxValue)] + [InlineData(float.MinValue)] + [InlineData(float.NaN)] + [InlineData(float.PositiveInfinity)] + [InlineData(float.NegativeInfinity)] + public void WriteAndReadSingleLittleEndian_should_roundtrip_correctly(float value) + { + Span buffer = new byte[4]; + + BinaryPrimitivesCompat.WriteSingleLittleEndian(buffer, value); + float result = BinaryPrimitivesCompat.ReadSingleLittleEndian(buffer); + + if (float.IsNaN(value)) + { + Assert.True(float.IsNaN(result)); + } + else + { + Assert.Equal(value, result); + } + } + } +} diff --git a/tests/MongoDB.Bson.Tests/Serialization/Serializers/BinaryVectorSerializerTests.cs b/tests/MongoDB.Bson.Tests/Serialization/Serializers/BinaryVectorSerializerTests.cs index 4394c626c76..274e93de373 100644 --- a/tests/MongoDB.Bson.Tests/Serialization/Serializers/BinaryVectorSerializerTests.cs +++ b/tests/MongoDB.Bson.Tests/Serialization/Serializers/BinaryVectorSerializerTests.cs @@ -365,10 +365,16 @@ private BsonBinaryData SerializeToBinaryData(TCollection collection private static (T[], byte[] VectorBson) GetTestData(BinaryVectorDataType dataType, int elementsCount, byte bitsPadding) where T : struct { - var elementsSpan = new ReadOnlySpan(Enumerable.Range(0, elementsCount).Select(i => Convert.ChangeType(i, typeof(T)).As()).ToArray()); - byte[] vectorBsonData = [(byte)dataType, bitsPadding, .. MemoryMarshal.Cast(elementsSpan)]; - - return (elementsSpan.ToArray(), vectorBsonData); + var elementsSpan = new ReadOnlySpan( + Enumerable.Range(0, elementsCount) + .Select(i => Convert.ChangeType(i, typeof(T)).As()) + .ToArray()); + var elementsBytesLittleEndian = BitConverter.IsLittleEndian + ? MemoryMarshal.Cast(elementsSpan) + : BigEndianToLittleEndian(elementsSpan, dataType); + + byte[] vectorBsonData = [(byte)dataType, bitsPadding, .. elementsBytesLittleEndian]; + return (elementsSpan.ToArray(), vectorBsonData); } private static (BinaryVector, byte[] VectorBson) GetTestDataBinaryVector(BinaryVectorDataType dataType, int elementsCount, byte bitsPadding) @@ -409,6 +415,27 @@ private static IBsonSerializer CreateBinaryVectorSerializer(BinaryVectorDataT return serializer; } + private static byte[] BigEndianToLittleEndian(ReadOnlySpan span, BinaryVectorDataType dataType) where T : struct + { + // Types that do NOT need conversion safe on BE + if (dataType == BinaryVectorDataType.Int8 || dataType == BinaryVectorDataType.PackedBit) + { + return MemoryMarshal.Cast(span).ToArray(); + } + + var elementSize = Marshal.SizeOf(); + byte[] result = new byte[span.Length * elementSize]; + + for (int i = 0; i < span.Length; i++) + { + byte[] bytes = BitConverter.GetBytes((dynamic)span[i]); + Array.Reverse(bytes); // Ensure LE order + Buffer.BlockCopy(bytes, 0, result, i * elementSize, elementSize); + } + + return result; + } + public class BinaryVectorNoAttributeHolder { public BinaryVectorInt8 ValuesInt8 { get; set; }