Skip to content

Commit 0a837d3

Browse files
Convert.Try{From/To}HexString (#86556)
Co-authored-by: Adam Sitnik <adam.sitnik@gmail.com>
1 parent f45c788 commit 0a837d3

4 files changed

Lines changed: 198 additions & 64 deletions

File tree

src/libraries/Common/src/System/HexConverter.cs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -227,22 +227,22 @@ public static char ToCharLower(int value)
227227
return (char)value;
228228
}
229229

230-
public static bool TryDecodeFromUtf16(ReadOnlySpan<char> chars, Span<byte> bytes)
230+
public static bool TryDecodeFromUtf16(ReadOnlySpan<char> chars, Span<byte> bytes, out int charsProcessed)
231231
{
232232
#if SYSTEM_PRIVATE_CORELIB
233233
if (BitConverter.IsLittleEndian && (Ssse3.IsSupported || AdvSimd.Arm64.IsSupported) &&
234234
chars.Length >= Vector128<ushort>.Count * 2)
235235
{
236-
return TryDecodeFromUtf16_Vector128(chars, bytes);
236+
return TryDecodeFromUtf16_Vector128(chars, bytes, out charsProcessed);
237237
}
238238
#endif
239-
return TryDecodeFromUtf16(chars, bytes, out _);
239+
return TryDecodeFromUtf16_Scalar(chars, bytes, out charsProcessed);
240240
}
241241

242242
#if SYSTEM_PRIVATE_CORELIB
243243
[CompExactlyDependsOn(typeof(AdvSimd.Arm64))]
244244
[CompExactlyDependsOn(typeof(Ssse3))]
245-
public static bool TryDecodeFromUtf16_Vector128(ReadOnlySpan<char> chars, Span<byte> bytes)
245+
public static bool TryDecodeFromUtf16_Vector128(ReadOnlySpan<char> chars, Span<byte> bytes, out int charsProcessed)
246246
{
247247
Debug.Assert(Ssse3.IsSupported || AdvSimd.Arm64.IsSupported);
248248
Debug.Assert(chars.Length <= bytes.Length * 2);
@@ -309,6 +309,7 @@ public static bool TryDecodeFromUtf16_Vector128(ReadOnlySpan<char> chars, Span<b
309309
offset += (nuint)Vector128<ushort>.Count * 2;
310310
if (offset == (nuint)chars.Length)
311311
{
312+
charsProcessed = chars.Length;
312313
return true;
313314
}
314315
// Overlap with the current chunk for trailing elements
@@ -320,11 +321,13 @@ public static bool TryDecodeFromUtf16_Vector128(ReadOnlySpan<char> chars, Span<b
320321
while (true);
321322

322323
// Fall back to the scalar routine in case of invalid input.
323-
return TryDecodeFromUtf16(chars.Slice((int)offset), bytes.Slice((int)(offset / 2)), out _);
324+
bool fallbackResult = TryDecodeFromUtf16_Scalar(chars.Slice((int)offset), bytes.Slice((int)(offset / 2)), out int fallbackProcessed);
325+
charsProcessed = (int)offset + fallbackProcessed;
326+
return fallbackResult;
324327
}
325328
#endif
326329

327-
public static bool TryDecodeFromUtf16(ReadOnlySpan<char> chars, Span<byte> bytes, out int charsProcessed)
330+
private static bool TryDecodeFromUtf16_Scalar(ReadOnlySpan<char> chars, Span<byte> bytes, out int charsProcessed)
328331
{
329332
Debug.Assert(chars.Length % 2 == 0, "Un-even number of characters provided");
330333
Debug.Assert(chars.Length / 2 == bytes.Length, "Target buffer not right-sized for provided characters");

src/libraries/System.Private.CoreLib/src/System/Convert.cs

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2949,12 +2949,82 @@ public static byte[] FromHexString(ReadOnlySpan<char> chars)
29492949

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

2952-
if (!HexConverter.TryDecodeFromUtf16(chars, result))
2952+
if (!HexConverter.TryDecodeFromUtf16(chars, result, out _))
29532953
throw new FormatException(SR.Format_BadHexChar);
29542954

29552955
return result;
29562956
}
29572957

2958+
/// <summary>
2959+
/// Converts the string, which encodes binary data as hex characters, to an equivalent 8-bit unsigned integer span.
2960+
/// </summary>
2961+
/// <param name="source">The string to convert.</param>
2962+
/// <param name="destination">
2963+
/// The span in which to write the converted 8-bit unsigned integers. When this method returns value different than <see cref="OperationStatus.Done"/>,
2964+
/// either the span remains unmodified or contains an incomplete conversion of <paramref name="source"/>,
2965+
/// up to the last valid character.
2966+
/// </param>
2967+
/// <param name="bytesWritten">When this method returns, contains the number of bytes that were written to <paramref name="destination"/>.</param>
2968+
/// <param name="charsConsumed">When this method returns, contains the number of characters that were consumed from <paramref name="source"/>.</param>
2969+
/// <returns>An <see cref="OperationStatus"/> describing the result of the operation.</returns>
2970+
/// <exception cref="ArgumentNullException">Passed string <paramref name="source"/> is null.</exception>
2971+
public static OperationStatus FromHexString(string source, Span<byte> destination, out int charsConsumed, out int bytesWritten)
2972+
{
2973+
ArgumentNullException.ThrowIfNull(source);
2974+
2975+
return FromHexString(source.AsSpan(), destination, out charsConsumed, out bytesWritten);
2976+
}
2977+
2978+
/// <summary>
2979+
/// Converts the span of chars, which encodes binary data as hex characters, to an equivalent 8-bit unsigned integer span.
2980+
/// </summary>
2981+
/// <param name="source">The span to convert.</param>
2982+
/// <param name="destination">
2983+
/// The span in which to write the converted 8-bit unsigned integers. When this method returns value different than <see cref="OperationStatus.Done"/>,
2984+
/// either the span remains unmodified or contains an incomplete conversion of <paramref name="source"/>,
2985+
/// up to the last valid character.
2986+
/// </param>
2987+
/// <param name="bytesWritten">When this method returns, contains the number of bytes that were written to <paramref name="destination"/>.</param>
2988+
/// <param name="charsConsumed">When this method returns, contains the number of characters that were consumed from <paramref name="source"/>.</param>
2989+
/// <returns>An <see cref="OperationStatus"/> describing the result of the operation.</returns>
2990+
public static OperationStatus FromHexString(ReadOnlySpan<char> source, Span<byte> destination, out int charsConsumed, out int bytesWritten)
2991+
{
2992+
(int quotient, int remainder) = Math.DivRem(source.Length, 2);
2993+
2994+
if (quotient == 0)
2995+
{
2996+
charsConsumed = 0;
2997+
bytesWritten = 0;
2998+
2999+
return remainder == 1 ? OperationStatus.NeedMoreData : OperationStatus.Done;
3000+
}
3001+
3002+
var result = OperationStatus.Done;
3003+
3004+
if (destination.Length < quotient)
3005+
{
3006+
source = source.Slice(0, destination.Length * 2);
3007+
quotient = destination.Length;
3008+
result = OperationStatus.DestinationTooSmall;
3009+
}
3010+
else if (remainder == 1)
3011+
{
3012+
source = source.Slice(0, source.Length - 1);
3013+
destination = destination.Slice(0, destination.Length - 1);
3014+
result = OperationStatus.NeedMoreData;
3015+
}
3016+
3017+
if (!HexConverter.TryDecodeFromUtf16(source, destination, out charsConsumed))
3018+
{
3019+
bytesWritten = charsConsumed / 2;
3020+
return OperationStatus.InvalidData;
3021+
}
3022+
3023+
bytesWritten = quotient;
3024+
charsConsumed = source.Length;
3025+
return result;
3026+
}
3027+
29583028
/// <summary>
29593029
/// Converts an array of 8-bit unsigned integers to its equivalent string representation that is encoded with uppercase hex characters.
29603030
/// </summary>
@@ -3006,5 +3076,31 @@ public static string ToHexString(ReadOnlySpan<byte> bytes)
30063076

30073077
return HexConverter.ToString(bytes, HexConverter.Casing.Upper);
30083078
}
3079+
3080+
3081+
/// <summary>
3082+
/// Converts a span of 8-bit unsigned integers to its equivalent span representation that is encoded with uppercase hex characters.
3083+
/// </summary>
3084+
/// <param name="source">A span of 8-bit unsigned integers.</param>
3085+
/// <param name="destination">The span representation in hex of the elements in <paramref name="source"/>.</param>
3086+
/// <param name="charsWritten">When this method returns, contains the number of chars that were written in <paramref name="destination"/>.</param>
3087+
/// <returns>true if the conversion was successful; otherwise, false.</returns>
3088+
public static bool TryToHexString(ReadOnlySpan<byte> source, Span<char> destination, out int charsWritten)
3089+
{
3090+
if (source.Length == 0)
3091+
{
3092+
charsWritten = 0;
3093+
return true;
3094+
}
3095+
else if (source.Length > int.MaxValue / 2 || destination.Length > source.Length * 2)
3096+
{
3097+
charsWritten = 0;
3098+
return false;
3099+
}
3100+
3101+
HexConverter.EncodeToUtf16(source, destination);
3102+
charsWritten = source.Length * 2;
3103+
return true;
3104+
}
30093105
} // class Convert
30103106
} // namespace
Lines changed: 89 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Text;
1+
using System.Buffers;
2+
using System.Text;
23
using Xunit;
34

45
namespace System.Tests
@@ -34,93 +35,124 @@ public static void CompleteValueRange()
3435

3536
private static void TestSequence(byte[] expected, string actual)
3637
{
37-
Assert.Equal(expected, Convert.FromHexString(actual));
38+
byte[] fromResult = Convert.FromHexString(actual);
39+
Assert.Equal(expected, fromResult);
40+
41+
Span<byte> tryResult = new byte[actual.Length / 2];
42+
Assert.Equal(OperationStatus.Done, Convert.FromHexString(actual, tryResult, out int consumed, out int written));
43+
Assert.Equal(fromResult.Length, written);
44+
Assert.Equal(actual.Length, consumed);
45+
AssertExtensions.SequenceEqual(expected, tryResult);
3846
}
3947

4048
[Fact]
4149
public static void InvalidInputString_Null()
4250
{
4351
AssertExtensions.Throws<ArgumentNullException>("s", () => Convert.FromHexString(null));
52+
AssertExtensions.Throws<ArgumentNullException>("source", () => Convert.FromHexString(null, default, out _, out _));
4453
}
4554

46-
[Fact]
47-
public static void InvalidInputString_HalfByte()
55+
[Theory]
56+
[InlineData("01-02-FD-FE-FF")]
57+
[InlineData("00 01 02FD FE FF")]
58+
[InlineData("000102FDFEFF ")]
59+
[InlineData(" 000102FDFEFF")]
60+
[InlineData("\u200B 000102FDFEFF")]
61+
[InlineData("0\u0308")]
62+
[InlineData("0x")]
63+
[InlineData("x0")]
64+
public static void InvalidInputString_FormatException_Or_FalseResult(string invalidInput)
4865
{
49-
Assert.Throws<FormatException>(() => Convert.FromHexString("ABC"));
50-
}
66+
Assert.Throws<FormatException>(() => Convert.FromHexString(invalidInput));
5167

52-
[Fact]
53-
public static void InvalidInputString_BadFirstCharacter()
54-
{
55-
Assert.Throws<FormatException>(() => Convert.FromHexString("x0"));
68+
Span<byte> buffer = stackalloc byte[invalidInput.Length / 2];
69+
Assert.Equal(OperationStatus.InvalidData, Convert.FromHexString(invalidInput.AsSpan(), buffer, out _, out _));
5670
}
5771

5872
[Fact]
59-
public static void InvalidInputString_BadSecondCharacter()
73+
public static void ZeroLength()
6074
{
61-
Assert.Throws<FormatException>(() => Convert.FromHexString("0x"));
62-
}
75+
Assert.Same(Array.Empty<byte>(), Convert.FromHexString(string.Empty));
6376

64-
[Fact]
65-
public static void InvalidInputString_NonAsciiCharacter()
66-
{
67-
Assert.Throws<FormatException>(() => Convert.FromHexString("0\u0308"));
68-
}
77+
OperationStatus convertResult = Convert.FromHexString(string.Empty, Span<byte>.Empty, out int consumed, out int written);
6978

70-
[Fact]
71-
public static void InvalidInputString_ZeroWidthSpace()
72-
{
73-
Assert.Throws<FormatException>(() => Convert.FromHexString("\u200B 000102FDFEFF"));
79+
Assert.Equal(OperationStatus.Done, convertResult);
80+
Assert.Equal(0, written);
81+
Assert.Equal(0, consumed);
7482
}
7583

7684
[Fact]
77-
public static void InvalidInputString_LeadingWhiteSpace()
85+
public static void ToHexFromHexRoundtrip()
7886
{
79-
Assert.Throws<FormatException>(() => Convert.FromHexString(" 000102FDFEFF"));
80-
}
87+
const int loopCount = 50;
88+
Span<char> buffer = stackalloc char[loopCount * 2];
89+
for (int i = 1; i < loopCount; i++)
90+
{
91+
byte[] data = Security.Cryptography.RandomNumberGenerator.GetBytes(i);
92+
string hex = Convert.ToHexString(data);
8193

82-
[Fact]
83-
public static void InvalidInputString_TrailingWhiteSpace()
84-
{
85-
Assert.Throws<FormatException>(() => Convert.FromHexString("000102FDFEFF "));
86-
}
94+
Span<char> currentBuffer = buffer.Slice(0, i * 2);
95+
bool tryHex = Convert.TryToHexString(data, currentBuffer, out int written);
96+
Assert.True(tryHex);
97+
AssertExtensions.SequenceEqual(hex.AsSpan(), currentBuffer);
98+
Assert.Equal(hex.Length, written);
8799

88-
[Fact]
89-
public static void InvalidInputString_WhiteSpace()
90-
{
91-
Assert.Throws<FormatException>(() => Convert.FromHexString("00 01 02FD FE FF"));
92-
}
100+
TestSequence(data, hex);
101+
TestSequence(data, hex.ToLowerInvariant());
102+
TestSequence(data, hex.ToUpperInvariant());
93103

94-
[Fact]
95-
public static void InvalidInputString_Dash()
96-
{
97-
Assert.Throws<FormatException>(() => Convert.FromHexString("01-02-FD-FE-FF"));
104+
string mixedCase1 = hex.Substring(0, hex.Length / 2).ToUpperInvariant() +
105+
hex.Substring(hex.Length / 2).ToLowerInvariant();
106+
string mixedCase2 = hex.Substring(0, hex.Length / 2).ToLowerInvariant() +
107+
hex.Substring(hex.Length / 2).ToUpperInvariant();
108+
109+
TestSequence(data, mixedCase1);
110+
TestSequence(data, mixedCase2);
111+
112+
Assert.Throws<FormatException>(() => Convert.FromHexString(hex + " "));
113+
Assert.Throws<FormatException>(() => Convert.FromHexString("\uAAAA" + hex));
114+
}
98115
}
99116

100117
[Fact]
101-
public static void ZeroLength()
118+
public static void TooShortDestination()
102119
{
103-
Assert.Same(Array.Empty<byte>(), Convert.FromHexString(string.Empty));
120+
const int destinationSize = 10;
121+
Span<byte> destination = stackalloc byte[destinationSize];
122+
byte[] data = Security.Cryptography.RandomNumberGenerator.GetBytes(destinationSize * 2 + 1);
123+
string hex = Convert.ToHexString(data);
124+
125+
OperationStatus result = Convert.FromHexString(hex, destination, out int charsConsumed, out int bytesWritten);
126+
127+
Assert.Equal(OperationStatus.DestinationTooSmall, result);
128+
Assert.Equal(destinationSize * 2, charsConsumed);
129+
Assert.Equal(destinationSize, bytesWritten);
104130
}
105131

106132
[Fact]
107-
public static void ToHexFromHexRoundtrip()
133+
public static void NeedMoreData_OrFormatException()
108134
{
109-
for (int i = 1; i < 50; i++)
110-
{
111-
byte[] data = System.Security.Cryptography.RandomNumberGenerator.GetBytes(i);
112-
string hex = Convert.ToHexString(data);
113-
Assert.Equal(data, Convert.FromHexString(hex.ToLowerInvariant()));
114-
Assert.Equal(data, Convert.FromHexString(hex.ToUpperInvariant()));
115-
string mixedCase1 = hex.Substring(0, hex.Length / 2).ToUpperInvariant() +
116-
hex.Substring(hex.Length / 2).ToLowerInvariant();
117-
string mixedCase2 = hex.Substring(0, hex.Length / 2).ToLowerInvariant() +
118-
hex.Substring(hex.Length / 2).ToUpperInvariant();
119-
Assert.Equal(data, Convert.FromHexString(mixedCase1));
120-
Assert.Equal(data, Convert.FromHexString(mixedCase2));
121-
Assert.Throws<FormatException>(() => Convert.FromHexString(hex + " "));
122-
Assert.Throws<FormatException>(() => Convert.FromHexString("\uAAAA" + hex));
123-
}
135+
const int destinationSize = 10;
136+
byte[] data = Security.Cryptography.RandomNumberGenerator.GetBytes(destinationSize);
137+
Span<byte> destination = stackalloc byte[destinationSize];
138+
var hex = Convert.ToHexString(data);
139+
140+
var spanHex = hex.AsSpan(0, 1);
141+
var singeResult = Convert.FromHexString(spanHex, destination, out int consumed, out int written);
142+
143+
Assert.Throws<FormatException>(() => Convert.FromHexString(hex.Substring(0, 1)));
144+
Assert.Equal(OperationStatus.NeedMoreData, singeResult);
145+
Assert.Equal(0, consumed);
146+
Assert.Equal(0, written);
147+
148+
spanHex = hex.AsSpan(0, hex.Length - 1);
149+
150+
var oneOffResult = Convert.FromHexString(spanHex, destination, out consumed, out written);
151+
152+
Assert.Throws<FormatException>(() => Convert.FromHexString(hex.Substring(0, hex.Length - 1)));
153+
Assert.Equal(OperationStatus.NeedMoreData, oneOffResult);
154+
Assert.Equal(spanHex.Length - 1, consumed);
155+
Assert.Equal((spanHex.Length - 1) / 2, written);
124156
}
125157
}
126158
}

src/libraries/System.Runtime/ref/System.Runtime.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1136,7 +1136,9 @@ public static partial class Convert
11361136
public static byte[] FromBase64CharArray(char[] inArray, int offset, int length) { throw null; }
11371137
public static byte[] FromBase64String(string s) { throw null; }
11381138
public static byte[] FromHexString(System.ReadOnlySpan<char> chars) { throw null; }
1139+
public static System.Buffers.OperationStatus FromHexString(System.ReadOnlySpan<char> source, Span<byte> destination, out int charsConsumed, out int bytesWritten) { throw null; }
11391140
public static byte[] FromHexString(string s) { throw null; }
1141+
public static System.Buffers.OperationStatus FromHexString(string source, Span<byte> destination, out int charsConsumed, out int bytesWritten) { throw null; }
11401142
public static System.TypeCode GetTypeCode(object? value) { throw null; }
11411143
public static bool IsDBNull([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] object? value) { throw null; }
11421144
public static int ToBase64CharArray(byte[] inArray, int offsetIn, int length, char[] outArray, int offsetOut) { throw null; }
@@ -1282,6 +1284,7 @@ public static partial class Convert
12821284
public static string ToHexString(byte[] inArray) { throw null; }
12831285
public static string ToHexString(byte[] inArray, int offset, int length) { throw null; }
12841286
public static string ToHexString(System.ReadOnlySpan<byte> bytes) { throw null; }
1287+
public static bool TryToHexString(System.ReadOnlySpan<byte> source, System.Span<char> destination, out int charsWritten) { throw null; }
12851288
public static short ToInt16(bool value) { throw null; }
12861289
public static short ToInt16(byte value) { throw null; }
12871290
public static short ToInt16(char value) { throw null; }

0 commit comments

Comments
 (0)