Skip to content

CSHARP-5125 Span support for ObjectId ctor, Parse, TryParse, BsonUtils hex conversion #1465

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 148 additions & 44 deletions src/MongoDB.Bson/BsonUtils.cs
Original file line number Diff line number Diff line change
@@ -72,6 +72,19 @@ public static byte[] ParseHexString(string s)
return bytes;
}

/// <summary>
/// Parses a hex string into its equivalent byte array.
/// </summary>
/// <param name="s">The hex string to parse.</param>
/// <param name="bytes">The output buffer containing the byte equivalent of the hex string.</param>
public static void ParseHexChars(ReadOnlySpan<char> s, Span<byte> bytes)
{
if (!TryParseHexChars(s, bytes))
{
throw new FormatException("String should contain only hexadecimal digits.");
}
}

/// <summary>
/// Converts from number of milliseconds since Unix epoch to DateTime.
/// </summary>
@@ -119,25 +132,63 @@ public static string ToHexString(byte[] bytes)
{
#if NET5_0_OR_GREATER
ArgumentNullException.ThrowIfNull(bytes);
return Convert.ToHexString(bytes).ToLowerInvariant();
#else
if (bytes == null)
{
throw new ArgumentNullException(nameof(bytes));
}
#endif
return ToHexString(bytes.AsMemory());
}

/// <summary>
/// Converts a memory of bytes to a hex string.
/// </summary>
/// <param name="bytes">The memory of bytes.</param>
/// <returns>A hex string.</returns>
public static string ToHexString(ReadOnlyMemory<byte> bytes)
{
#if NET5_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER
return string.Create(bytes.Length * 2, bytes, static (chars, bytes) =>
{
ToHexChars(bytes.Span, chars);
});
#else
return new string(ToHexChars(bytes.Span));
#endif
}

/// <summary>
/// Converts a span of byte to a span of hex characters.
/// </summary>
/// <param name="bytes">The input span of bytes.</param>
/// <returns>An array of hex characters.</returns>
public static char[] ToHexChars(ReadOnlySpan<byte> bytes)
{
var length = bytes.Length;
var c = new char[length * 2];
ToHexChars(bytes, c.AsSpan());
return c;
}

/// <summary>
/// Converts a span of bytes to a span of hex characters.
/// </summary>
/// <param name="bytes">The input span of bytes.</param>
/// <param name="chars">The result span of characters.</param>
public static void ToHexChars(ReadOnlySpan<byte> bytes, Span<char> chars)
{
if (chars.Length != bytes.Length * 2)
{
throw new ArgumentException("Length of character span should be 2 times the length of byte span");
}
int length = bytes.Length;
for (int i = 0, j = 0; i < length; i++)
{
var b = bytes[i];
c[j++] = ToHexChar(b >> 4);
c[j++] = ToHexChar(b & 0x0f);
chars[j++] = ToHexChar(b >> 4);
chars[j++] = ToHexChar(b & 0x0f);
}

return new string(c);
#endif
}

/// <summary>
@@ -203,7 +254,6 @@ public static DateTime ToUniversalTime(DateTime dateTime)
return dateTime.ToUniversalTime();
}
}

/// <summary>
/// Tries to parse a hex string to a byte array.
/// </summary>
@@ -213,69 +263,123 @@ public static DateTime ToUniversalTime(DateTime dateTime)
public static bool TryParseHexString(string s, out byte[] bytes)
{
bytes = null;

if (s == null)
{
return false;
}

var buffer = new byte[(s.Length + 1) / 2];
if (!TryParseHexChars(s.AsSpan(), buffer.AsSpan()))
{
return false;
}
bytes = buffer;
return true;
}

/// <summary>
/// Tries to parse hex characters into a span of bytes.
/// </summary>
/// <param name="s">The span containing hex characters.</param>
/// <param name="bytes">The result byte span.</param>
/// <returns>True if the hex string was successfully parsed.</returns>
public static bool TryParseHexChars(ReadOnlySpan<char> s, Span<byte> bytes)
{
return HexParser.TryParse(s, bytes);
}

var i = 0;
var j = 0;
private static class HexParser
{
private static readonly int s_min = Math.Min(Math.Min('a', 'A'), '0');
private static readonly int s_max = Math.Max(Math.Max('f', 'F'), '9');

if ((s.Length % 2) == 1)
private static readonly byte[] s_lookup = CreateLookup();

private static byte[] CreateLookup()
{
// if s has an odd length assume an implied leading "0"
int y;
if (!TryParseHexChar(s[i++], out y))
var result = new byte[s_max - s_min + 1];
for (var i = 0; i < result.Length; i++)
{
return false;
result[i] = HexToByte((char)(i + s_min));
}
return result;
static byte HexToByte(char ch)
{
if (char.IsDigit(ch))
{
return (byte)(ch - '0');
}
else if ('A' <= ch && ch <= 'F')
{
return (byte)(10 + ch - 'A');
}
else if ('a' <= ch && ch <= 'f')
{
return (byte)(10 + ch - 'a');
}
else
{
return byte.MaxValue;
}
}
buffer[j++] = (byte)y;
}

while (i < s.Length)
public static bool TryParse(ReadOnlySpan<char> chars, Span<byte> bytes)
{
int x, y;
if (!TryParseHexChar(s[i++], out x))
{
if (bytes.Length != (chars.Length + 1) / 2)
return false;
int j = 0;
if ((chars.Length & 1) == 1)
{
// if chars has an odd length assume an implied leading "0"
if (!TryParseChar(chars[0], out byte b))
return false;
bytes[j++] = b;
chars = chars.Slice(1);
}
if (!TryParseHexChar(s[i++], out y))
for (int i = 0; i < chars.Length; i += 2)
{
return false;
if (!TryParseChars(chars.Slice(i, 2), out byte b))
return false;
bytes[j++] = b;
}
buffer[j++] = (byte)((x << 4) | y);
}

bytes = buffer;
return true;
}

// private static methods
private static bool TryParseHexChar(char c, out int value)
{
if (c >= '0' && c <= '9')
{
value = c - '0';
return true;
}

if (c >= 'a' && c <= 'f')
public static bool TryParseChars(ReadOnlySpan<char> chars, out byte value)
{
value = 10 + (c - 'a');
return true;
if (chars.Length == 1)
{
return TryParseChar(chars[0], out value);
}
if (chars.Length >= 2
&& TryParseChar(chars[0], out byte upper)
&& TryParseChar(chars[1], out byte lower))
{
value = (byte)((upper << 4) | lower);
return true;
}
else
{
value = default;
return false;
}
}

if (c >= 'A' && c <= 'F')
public static bool TryParseChar(char ch, out byte result)
{
value = 10 + (c - 'A');
return true;
int index = ch - s_min;
if (0 <= index && index < s_lookup.Length && s_lookup[index] != byte.MaxValue)
{
result = s_lookup[index];
return true;
}
else
{
result = default;
return false;
}
}

value = 0;
return false;
}
}
}
140 changes: 88 additions & 52 deletions src/MongoDB.Bson/ObjectModel/ObjectId.cs
Original file line number Diff line number Diff line change
@@ -14,8 +14,10 @@
*/

using System;
using System.Buffers.Binary;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;

namespace MongoDB.Bson
@@ -41,6 +43,15 @@ public struct ObjectId : IComparable<ObjectId>, IEquatable<ObjectId>, IConvertib
/// </summary>
/// <param name="bytes">The bytes.</param>
public ObjectId(byte[] bytes)
: this(bytes.AsSpan())
{
}
// constructors
/// <summary>
/// Initializes a new instance of the ObjectId class.
/// </summary>
/// <param name="bytes">The bytes.</param>
public ObjectId(ReadOnlySpan<byte> bytes)
{
if (bytes == null)
{
@@ -51,32 +62,42 @@ public ObjectId(byte[] bytes)
throw new ArgumentException("Byte array must be 12 bytes long", "bytes");
}

FromByteArray(bytes, 0, out _a, out _b, out _c);
FromSpan(bytes, out _a, out _b, out _c);
}

/// <summary>
/// Initializes a new instance of the ObjectId class.
/// </summary>
/// <param name="bytes">The bytes.</param>
/// <param name="index">The index into the byte array where the ObjectId starts.</param>
internal ObjectId(byte[] bytes, int index)
internal ObjectId(ReadOnlySpan<byte> bytes, int index)
{
FromByteArray(bytes, index, out _a, out _b, out _c);
FromSpan(bytes.Slice(index), out _a, out _b, out _c);
}

/// <summary>
/// Initializes a new instance of the ObjectId class.
/// </summary>
/// <param name="value">The value.</param>
public ObjectId(string value)
: this(value is null ? throw new ArgumentNullException("value") :value.AsSpan())
{
}

/// <summary>
/// Initializes a new instance of the ObjectId class.
/// </summary>
/// <param name="value">The value.</param>
public ObjectId(ReadOnlySpan<char> value)
{
if (value == null)
{
throw new ArgumentNullException("value");
}

var bytes = BsonUtils.ParseHexString(value);
FromByteArray(bytes, 0, out _a, out _b, out _c);
Span<byte> bytes = stackalloc byte[12];
BsonUtils.ParseHexChars(value, bytes);
FromSpan(bytes, out _a, out _b, out _c);
}

private ObjectId(int a, int b, int c)
@@ -243,10 +264,26 @@ public static ObjectId Parse(string s)
public static bool TryParse(string s, out ObjectId objectId)
{
// don't throw ArgumentNullException if s is null
if (s != null && s.Length == 24)
if (s == null)
{
byte[] bytes;
if (BsonUtils.TryParseHexString(s, out bytes))
objectId = default;
return false;
}
return TryParse(s.AsSpan(), out objectId);
}

/// <summary>
/// Tries to parse a string and create a new ObjectId.
/// </summary>
/// <param name="s">The string value.</param>
/// <param name="objectId">The new ObjectId.</param>
/// <returns>True if the string was parsed successfully.</returns>
public static bool TryParse(ReadOnlySpan<char> s, out ObjectId objectId)
{
if (s.Length == 24)
{
Span<byte> bytes = stackalloc byte[12];
if (BsonUtils.TryParseHexChars(s, bytes))
{
objectId = new ObjectId(bytes);
return true;
@@ -343,11 +380,12 @@ private static int GetTimestampFromDateTime(DateTime timestamp)
return (int)(uint)secondsSinceEpoch;
}

private static void FromByteArray(byte[] bytes, int offset, out int a, out int b, out int c)
private static void FromSpan(ReadOnlySpan<byte> bytes, out int a, out int b, out int c)
{
a = (bytes[offset] << 24) | (bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3];
b = (bytes[offset + 4] << 24) | (bytes[offset + 5] << 16) | (bytes[offset + 6] << 8) | bytes[offset + 7];
c = (bytes[offset + 8] << 24) | (bytes[offset + 9] << 16) | (bytes[offset + 10] << 8) | bytes[offset + 11];
var ints = MemoryMarshal.Cast<byte, int>(bytes);
a = BinaryPrimitives.ReverseEndianness(ints[0]);
b = BinaryPrimitives.ReverseEndianness(ints[1]);
c = BinaryPrimitives.ReverseEndianness(ints[2]);
}

// public methods
@@ -425,28 +463,28 @@ public byte[] ToByteArray()
/// <param name="destination">The destination.</param>
/// <param name="offset">The offset.</param>
public void ToByteArray(byte[] destination, int offset)
{
ToByteSpan(destination.AsSpan().Slice(offset));
}

/// <summary>
/// Writes the ObjectId into a byte span.
/// </summary>
/// <param name="destination">The destination.</param>
public void ToByteSpan(Span<byte> destination)
{
if (destination == null)
{
throw new ArgumentNullException("destination");
}
if (offset + 12 > destination.Length)
if (12 > destination.Length)
{
throw new ArgumentException("Not enough room in destination buffer.", "offset");
}

destination[offset + 0] = (byte)(_a >> 24);
destination[offset + 1] = (byte)(_a >> 16);
destination[offset + 2] = (byte)(_a >> 8);
destination[offset + 3] = (byte)(_a);
destination[offset + 4] = (byte)(_b >> 24);
destination[offset + 5] = (byte)(_b >> 16);
destination[offset + 6] = (byte)(_b >> 8);
destination[offset + 7] = (byte)(_b);
destination[offset + 8] = (byte)(_c >> 24);
destination[offset + 9] = (byte)(_c >> 16);
destination[offset + 10] = (byte)(_c >> 8);
destination[offset + 11] = (byte)(_c);
var intValues = MemoryMarshal.Cast<byte, int>(destination);
intValues[0] = BinaryPrimitives.ReverseEndianness(_a);
intValues[1] = BinaryPrimitives.ReverseEndianness(_b);
intValues[2] = BinaryPrimitives.ReverseEndianness(_c);
}

/// <summary>
@@ -455,32 +493,30 @@ public void ToByteArray(byte[] destination, int offset)
/// <returns>A string representation of the value.</returns>
public override string ToString()
{
var c = new char[24];
c[0] = BsonUtils.ToHexChar((_a >> 28) & 0x0f);
c[1] = BsonUtils.ToHexChar((_a >> 24) & 0x0f);
c[2] = BsonUtils.ToHexChar((_a >> 20) & 0x0f);
c[3] = BsonUtils.ToHexChar((_a >> 16) & 0x0f);
c[4] = BsonUtils.ToHexChar((_a >> 12) & 0x0f);
c[5] = BsonUtils.ToHexChar((_a >> 8) & 0x0f);
c[6] = BsonUtils.ToHexChar((_a >> 4) & 0x0f);
c[7] = BsonUtils.ToHexChar(_a & 0x0f);
c[8] = BsonUtils.ToHexChar((_b >> 28) & 0x0f);
c[9] = BsonUtils.ToHexChar((_b >> 24) & 0x0f);
c[10] = BsonUtils.ToHexChar((_b >> 20) & 0x0f);
c[11] = BsonUtils.ToHexChar((_b >> 16) & 0x0f);
c[12] = BsonUtils.ToHexChar((_b >> 12) & 0x0f);
c[13] = BsonUtils.ToHexChar((_b >> 8) & 0x0f);
c[14] = BsonUtils.ToHexChar((_b >> 4) & 0x0f);
c[15] = BsonUtils.ToHexChar(_b & 0x0f);
c[16] = BsonUtils.ToHexChar((_c >> 28) & 0x0f);
c[17] = BsonUtils.ToHexChar((_c >> 24) & 0x0f);
c[18] = BsonUtils.ToHexChar((_c >> 20) & 0x0f);
c[19] = BsonUtils.ToHexChar((_c >> 16) & 0x0f);
c[20] = BsonUtils.ToHexChar((_c >> 12) & 0x0f);
c[21] = BsonUtils.ToHexChar((_c >> 8) & 0x0f);
c[22] = BsonUtils.ToHexChar((_c >> 4) & 0x0f);
c[23] = BsonUtils.ToHexChar(_c & 0x0f);
return new string(c);
#if NETFRAMEWORK
var chars = new char[24];
#else
Span<char> chars = stackalloc char[24];
#endif
WriteToSpan(chars);
return new string(chars);
}

/// <summary>
/// Writes the string representation of the value into the target span
/// </summary>
/// <param name="span">the target span</param>
public void WriteToSpan(Span<char> span)
{
if (span.Length < 24)
{
throw new ArgumentException("Not enough room in destination span.", "offset");
}
Span<int> intValues = stackalloc int[3];
intValues[0] = BinaryPrimitives.ReverseEndianness(_a);
intValues[1] = BinaryPrimitives.ReverseEndianness(_b);
intValues[2] = BinaryPrimitives.ReverseEndianness(_c);
BsonUtils.ToHexChars(MemoryMarshal.AsBytes(intValues), span);
}

// explicit IConvertible implementation
2 changes: 1 addition & 1 deletion tests/MongoDB.Bson.Tests/ObjectModel/ObjectIdTests.cs
Original file line number Diff line number Diff line change
@@ -346,7 +346,7 @@ public void TestTryParse()
Assert.False(ObjectId.TryParse("102030405060708090a0b0c", out objectId1)); // too short
Assert.False(ObjectId.TryParse("x102030405060708090a0b0c", out objectId1)); // invalid character
Assert.False(ObjectId.TryParse("00102030405060708090a0b0c", out objectId1)); // too long
Assert.False(ObjectId.TryParse(null, out objectId1)); // should return false not throw ArgumentNullException
Assert.False(ObjectId.TryParse(default(string), out objectId1)); // should return false not throw ArgumentNullException
}

[Fact]