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

Add methods to convert between hexadecimal strings and bytes #37546

Merged
merged 13 commits into from
Jul 18, 2020
Merged
40 changes: 40 additions & 0 deletions src/libraries/Common/src/System/HexConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace System
Expand Down Expand Up @@ -149,5 +150,44 @@ public static char ToCharLower(int value)

return (char)value;
}

public static bool TryDecodeFromUtf16(ReadOnlySpan<char> chars, Span<byte> bytes)
{
tkp1n marked this conversation as resolved.
Show resolved Hide resolved
Debug.Assert(chars.Length / 2 == bytes.Length);
Debug.Assert(chars.Length % 2 == 0);

int i = 0;
int j = 0;

ReadOnlySpan<byte> charToHexLookup = CharToHexLookup;
while (j < bytes.Length)
{
int byteHi;
int byteLo;
int charHi = chars[i++];
int charLo = chars[i++];

if (charHi >= charToHexLookup.Length || (byteHi = charToHexLookup[charHi]) == 0xFF)
return false;
if (charLo >= charToHexLookup.Length || (byteLo = charToHexLookup[charLo]) == 0xFF)
return false;

bytes[j++] = (byte)((byteHi << 4) | byteLo);
}

return true;
}

/// <summary>Map from an ASCII char to its hex value, e.g. arr['b'] == 11. 0xFF means it's not a hex digit.</summary>
internal static ReadOnlySpan<byte> CharToHexLookup => new byte[]
{
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 15
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 31
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 47
0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 63
0xFF, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 79
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 95
0xFF, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf // 102
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2224,6 +2224,12 @@
<data name="Format_NoFormatSpecifier" xml:space="preserve">
<value>No format specifiers were provided.</value>
</data>
<data name="Format_BadHexChar" xml:space="preserve">
<value>The input is not a valid hex string as it contains a non-hex character.</value>
</data>
<data name="Format_BadHexLength" xml:space="preserve">
<value>The input is not a valid hex string as its length is not a multiple of 2.</value>
</data>
<data name="Format_BadQuote" xml:space="preserve">
<value>Cannot find a matching quote character for the character '{0}'.</value>
</data>
Expand Down
92 changes: 92 additions & 0 deletions src/libraries/System.Private.CoreLib/src/System/Convert.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2838,5 +2838,97 @@ private static unsafe int FromBase64_ComputeResultLength(char* inputPtr, int inp
// Done:
return (usefulInputLength / 4) * 3 + padding;
}

/// <summary>
/// Converts the specified string, which encodes binary data as hex characters, to an equivalent 8-bit unsigned integer array.
/// </summary>
/// <param name="s">The string to convert.</param>
/// <returns>An array of 8-bit unsigned integers that is equivalent to <paramref name="s"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="s"/> is <code>null</code>.</exception>
/// <exception cref="FormatException">The length of <paramref name="s"/>, is not zero or a multiple of 2.</exception>
/// <exception cref="FormatException">The format of <paramref name="s"/> is invalid. <paramref name="s"/> contains a non-hex character.</exception>
public static byte[] FromHexString(string s)
{
if (s == null)
throw new ArgumentNullException(nameof(s));

return FromHexString(s.AsSpan());
}

/// <summary>
/// Converts the span, which encodes binary data as hex characters, to an equivalent 8-bit unsigned integer array.
/// </summary>
/// <param name="chars">The span to convert.</param>
/// <returns>An array of 8-bit unsigned integers that is equivalent to <paramref name="chars"/>.</returns>
/// <exception cref="FormatException">The length of <paramref name="chars"/>, is not zero or a multiple of 2.</exception>
/// <exception cref="FormatException">The format of <paramref name="chars"/> is invalid. <paramref name="chars"/> contains a non-hex character.</exception>
public static byte[] FromHexString(ReadOnlySpan<char> chars)
{
if (chars.Length == 0)
return Array.Empty<byte>();
if ((uint)chars.Length % 2 != 0)
throw new FormatException(SR.Format_BadHexLength);

byte[] result = GC.AllocateUninitializedArray<byte>(chars.Length >> 1);

if (!HexConverter.TryDecodeFromUtf16(chars, result))
throw new FormatException(SR.Format_BadHexChar);

return result;
}

/// <summary>
/// Converts an array of 8-bit unsigned integers to its equivalent string representation that is encoded with hex characters.
/// </summary>
/// <param name="inArray">An array of 8-bit unsigned integers.</param>
/// <returns>The string representation in hex of the elements in <paramref name="inArray"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="inArray"/> is <code>null</code>.</exception>
public static string ToHexString(byte[] inArray)
{
if (inArray == null)
throw new ArgumentNullException(nameof(inArray));

return ToHexString(new ReadOnlySpan<byte>(inArray));
}

/// <summary>
/// Converts a subset of an array of 8-bit unsigned integers to its equivalent string representation that is encoded with hex characters.
/// Parameters specify the subset as an offset in the input array and the number of elements in the array to convert.
/// </summary>
/// <param name="inArray">An array of 8-bit unsigned integers.</param>
/// <param name="offset">An offset in <paramref name="inArray"/>.</param>
/// <param name="length">The number of elements of <paramref name="inArray"/> to convert.</param>
/// <returns>The string representation in hex of <paramref name="length"/> elements of <paramref name="inArray"/>, starting at position <paramref name="offset"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="inArray"/> is <code>null</code>.</exception>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="offset"/> or <paramref name="length"/> is negative.</exception>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="offset"/> plus <paramref name="length"/> is greater than the length of <paramref name="inArray"/>.</exception>
public static string ToHexString(byte[] inArray, int offset, int length)
{
if (inArray == null)
throw new ArgumentNullException(nameof(inArray));
if (length < 0)
throw new ArgumentOutOfRangeException(nameof(length), SR.ArgumentOutOfRange_Index);
if (offset < 0)
throw new ArgumentOutOfRangeException(nameof(offset), SR.ArgumentOutOfRange_GenericPositive);
if (offset > (inArray.Length - length))
throw new ArgumentOutOfRangeException(nameof(offset), SR.ArgumentOutOfRange_OffsetLength);

return ToHexString(new ReadOnlySpan<byte>(inArray, offset, length));
}

/// <summary>
/// Converts a span of 8-bit unsigned integers to its equivalent string representation that is encoded with hex characters.
/// </summary>
/// <param name="bytes">A span of 8-bit unsigned integers.</param>
/// <returns>The string representation in hex of the elements in <paramref name="bytes"/>.</returns>
tkp1n marked this conversation as resolved.
Show resolved Hide resolved
tkp1n marked this conversation as resolved.
Show resolved Hide resolved
public static string ToHexString(ReadOnlySpan<byte> bytes)
{
if (bytes.Length == 0)
return string.Empty;
if (bytes.Length > int.MaxValue / 2)
throw new OutOfMemoryException();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really an OutOfMemoryException?

The exception that is thrown when there is not enough memory to continue the execution of a program.

(Source)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ArgumentOutOfRangeException seems more appropriate. OOM should only be thrown by the CLR or interop code reinterpreting a native error.

7.3.7 OutOfMemoryException

  • DO NOT explicitly throw OutOfMemoryException. This exception is to be thrown only by the CLR infrastructure.

(Framework Design Guidelines, 3rd edition)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the OOM for consistency with the Base64 methods here:

// If we overflow an int then we cannot allocate enough
// memory to output the value so throw
if (outlen > int.MaxValue)
throw new OutOfMemoryException();

I'll gladly change that to an AOOR though...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched to AOOR with a new exception message (no existing one seemed to fit).
Let me know if you like it better this way. See de2085b


return HexConverter.ToString(bytes, HexConverter.Casing.Upper);
}
} // class Convert
} // namespace
2 changes: 1 addition & 1 deletion src/libraries/System.Private.CoreLib/src/System/Guid.cs
Original file line number Diff line number Diff line change
Expand Up @@ -664,7 +664,7 @@ private static bool TryParseHex(ReadOnlySpan<char> guidString, out uint result,
for (; i < guidString.Length && guidString[i] == '0'; i++) ;

int processedDigits = 0;
ReadOnlySpan<byte> charToHexLookup = Number.CharToHexLookup;
ReadOnlySpan<byte> charToHexLookup = HexConverter.CharToHexLookup;
uint tmp = 0;
for (; i < guidString.Length; i++)
{
Expand Down
16 changes: 2 additions & 14 deletions src/libraries/System.Private.CoreLib/src/System/Number.Parsing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,6 @@ internal static partial class Number
private const int HalfMaxExponent = 5;
private const int HalfMinExponent = -8;

/// <summary>Map from an ASCII char to its hex value, e.g. arr['b'] == 11. 0xFF means it's not a hex digit.</summary>
internal static ReadOnlySpan<byte> CharToHexLookup => new byte[]
{
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 15
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 31
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 47
0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 63
0xFF, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 79
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 95
0xFF, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf // 102
};

private static unsafe bool TryNumberToInt32(ref NumberBuffer number, ref int value)
{
number.CheckConsistency();
Expand Down Expand Up @@ -1134,7 +1122,7 @@ private static ParsingStatus TryParseUInt32HexNumberStyle(ReadOnlySpan<char> val

bool overflow = false;
uint answer = 0;
ReadOnlySpan<byte> charToHexLookup = CharToHexLookup;
ReadOnlySpan<byte> charToHexLookup = HexConverter.CharToHexLookup;

if ((uint)num < (uint)charToHexLookup.Length && charToHexLookup[num] != 0xFF)
{
Expand Down Expand Up @@ -1463,7 +1451,7 @@ private static ParsingStatus TryParseUInt64HexNumberStyle(ReadOnlySpan<char> val

bool overflow = false;
ulong answer = 0;
ReadOnlySpan<byte> charToHexLookup = CharToHexLookup;
ReadOnlySpan<byte> charToHexLookup = HexConverter.CharToHexLookup;

if ((uint)num < (uint)charToHexLookup.Length && charToHexLookup[num] != 0xFF)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefineConstants Condition="'$(TargetsUnix)' == 'true'">$(DefineConstants);Unix</DefineConstants>
Expand Down Expand Up @@ -39,6 +39,7 @@
<Compile Include="System\BitConverter.cs" />
<Compile Include="System\Convert.BoxedObjectCheck.cs" />
<Compile Include="System\Convert.FromBase64.cs" />
<Compile Include="System\Convert.FromHexString.cs" />
<Compile Include="System\Convert.TestBase.cs" />
<Compile Include="System\Convert.ToBase64CharArray.cs" />
<Compile Include="System\Convert.ToBase64String.cs" />
Expand All @@ -48,6 +49,7 @@
<Compile Include="System\Convert.ToDateTime.cs" />
<Compile Include="System\Convert.ToDecimal.cs" />
<Compile Include="System\Convert.ToDouble.cs" />
<Compile Include="System\Convert.ToHexString.cs" />
<Compile Include="System\Convert.ToInt16.cs" />
<Compile Include="System\Convert.ToInt32.cs" />
<Compile Include="System\Convert.ToInt64.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using System.Text;
using Xunit;

namespace System.Tests
{
public class ConvertFromHexStringTests
{
[Theory]
[InlineData("000102FDFEFF")]
[InlineData("000102fdfeff")]
[InlineData("000102fDfEfF")]
[InlineData("000102FdFeFf")]
[InlineData("000102FDfeFF")]
public static void KnownByteSequence(string value)
{
byte[] knownSequence = {0x00, 0x01, 0x02, 0xFD, 0xFE, 0xFF};
TestSequence(knownSequence, value);
}

[Fact]
public static void CompleteValueRange()
{
byte[] values = new byte[256];
StringBuilder sb = new StringBuilder(256);
for (int i = 0; i < values.Length; i++)
{
values[i] = (byte)i;
sb.Append(i.ToString("X2"));
}

TestSequence(values, sb.ToString());
TestSequence(values, sb.ToString().ToLower());
}

private static void TestSequence(byte[] expected, string actual)
{
Assert.Equal(expected, Convert.FromHexString(actual));
}

[Fact]
public static void InvalidInputString_Null()
{
AssertExtensions.Throws<ArgumentNullException>("s", () => Convert.FromHexString(null));
}

[Fact]
public static void InvalidInputString_HalfByte()
{
Assert.Throws<FormatException>(() => Convert.FromHexString("ABC"));
}

[Fact]
public static void InvalidInputString_BadFirstCharacter()
{
Assert.Throws<FormatException>(() => Convert.FromHexString("x0"));
}

[Fact]
public static void InvalidInputString_BadSecondCharacter()
{
Assert.Throws<FormatException>(() => Convert.FromHexString("0x"));
}

[Fact]
public static void InvalidInputString_NonAsciiCharacter()
{
Assert.Throws<FormatException>(() => Convert.FromHexString("0\u0308"));
}

[Fact]
public static void InvalidInputString_ZeroWidthSpace()
{
Assert.Throws<FormatException>(() => Convert.FromHexString("\u200B 000102FDFEFF"));
}

[Fact]
public static void InvalidInputString_LeadingWhiteSpace()
{
Assert.Throws<FormatException>(() => Convert.FromHexString(" 000102FDFEFF"));
}

[Fact]
public static void InvalidInputString_TrailingWhiteSpace()
{
Assert.Throws<FormatException>(() => Convert.FromHexString("000102FDFEFF "));
}

[Fact]
public static void InvalidInputString_WhiteSpace()
{
Assert.Throws<FormatException>(() => Convert.FromHexString("00 01 02FD FE FF"));
}

[Fact]
public static void InvalidInputString_Dash()
{
Assert.Throws<FormatException>(() => Convert.FromHexString("01-02-FD-FE-FF"));
}

[Fact]
public static void ZeroLength()
{
Assert.Same(Array.Empty<byte>(), Convert.FromHexString(string.Empty));
}
}
}
Loading