Skip to content
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

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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
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
Expand Up @@ -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>
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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>
Expand All @@ -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;
}
}
}
Loading