1 | // Licensed to the .NET Foundation under one or more agreements.
|
---|
2 | // The .NET Foundation licenses this file to you under the MIT license.
|
---|
3 |
|
---|
4 | // ReSharper disable RedundantCast
|
---|
5 |
|
---|
6 | #nullable disable
|
---|
7 | using System.Diagnostics;
|
---|
8 | using System.Runtime.CompilerServices;
|
---|
9 |
|
---|
10 | namespace System
|
---|
11 | {
|
---|
12 | internal static class HexConverter
|
---|
13 | {
|
---|
14 | public enum Casing : uint
|
---|
15 | {
|
---|
16 | // Output [ '0' .. '9' ] and [ 'A' .. 'F' ].
|
---|
17 | Upper = 0,
|
---|
18 |
|
---|
19 | // Output [ '0' .. '9' ] and [ 'a' .. 'f' ].
|
---|
20 | // This works because values in the range [ 0x30 .. 0x39 ] ([ '0' .. '9' ])
|
---|
21 | // already have the 0x20 bit set, so ORing them with 0x20 is a no-op,
|
---|
22 | // while outputs in the range [ 0x41 .. 0x46 ] ([ 'A' .. 'F' ])
|
---|
23 | // don't have the 0x20 bit set, so ORing them maps to
|
---|
24 | // [ 0x61 .. 0x66 ] ([ 'a' .. 'f' ]), which is what we want.
|
---|
25 | Lower = 0x2020U,
|
---|
26 | }
|
---|
27 |
|
---|
28 | // We want to pack the incoming byte into a single integer [ 0000 HHHH 0000 LLLL ],
|
---|
29 | // where HHHH and LLLL are the high and low nibbles of the incoming byte. Then
|
---|
30 | // subtract this integer from a constant minuend as shown below.
|
---|
31 | //
|
---|
32 | // [ 1000 1001 1000 1001 ]
|
---|
33 | // - [ 0000 HHHH 0000 LLLL ]
|
---|
34 | // =========================
|
---|
35 | // [ *YYY **** *ZZZ **** ]
|
---|
36 | //
|
---|
37 | // The end result of this is that YYY is 0b000 if HHHH <= 9, and YYY is 0b111 if HHHH >= 10.
|
---|
38 | // Similarly, ZZZ is 0b000 if LLLL <= 9, and ZZZ is 0b111 if LLLL >= 10.
|
---|
39 | // (We don't care about the value of asterisked bits.)
|
---|
40 | //
|
---|
41 | // To turn a nibble in the range [ 0 .. 9 ] into hex, we calculate hex := nibble + 48 (ascii '0').
|
---|
42 | // To turn a nibble in the range [ 10 .. 15 ] into hex, we calculate hex := nibble - 10 + 65 (ascii 'A').
|
---|
43 | // => hex := nibble + 55.
|
---|
44 | // The difference in the starting ASCII offset is (55 - 48) = 7, depending on whether the nibble is <= 9 or >= 10.
|
---|
45 | // Since 7 is 0b111, this conveniently matches the YYY or ZZZ value computed during the earlier subtraction.
|
---|
46 |
|
---|
47 | // The commented out code below is code that directly implements the logic described above.
|
---|
48 |
|
---|
49 | // uint packedOriginalValues = (((uint)value & 0xF0U) << 4) + ((uint)value & 0x0FU);
|
---|
50 | // uint difference = 0x8989U - packedOriginalValues;
|
---|
51 | // uint add7Mask = (difference & 0x7070U) >> 4; // line YYY and ZZZ back up with the packed values
|
---|
52 | // uint packedResult = packedOriginalValues + add7Mask + 0x3030U /* ascii '0' */;
|
---|
53 |
|
---|
54 | // The code below is equivalent to the commented out code above but has been tweaked
|
---|
55 | // to allow codegen to make some extra optimizations.
|
---|
56 |
|
---|
57 | // The low byte of the packed result contains the hex representation of the incoming byte's low nibble.
|
---|
58 | // The adjacent byte of the packed result contains the hex representation of the incoming byte's high nibble.
|
---|
59 |
|
---|
60 | // Finally, write to the output buffer starting with the *highest* index so that codegen can
|
---|
61 | // elide all but the first bounds check. (This only works if 'startingIndex' is a compile-time constant.)
|
---|
62 |
|
---|
63 | // The JIT can elide bounds checks if 'startingIndex' is constant and if the caller is
|
---|
64 | // writing to a span of known length (or the caller has already checked the bounds of the
|
---|
65 | // furthest access).
|
---|
66 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
|
---|
67 | public static void ToBytesBuffer(byte value, Span<byte> buffer, int startingIndex = 0, Casing casing = Casing.Upper)
|
---|
68 | {
|
---|
69 | uint difference = (((uint)value & 0xF0U) << 4) + ((uint)value & 0x0FU) - 0x8989U;
|
---|
70 | uint packedResult = ((((uint)(-(int)difference) & 0x7070U) >> 4) + difference + 0xB9B9U) | (uint)casing;
|
---|
71 |
|
---|
72 | buffer[startingIndex + 1] = (byte)packedResult;
|
---|
73 | buffer[startingIndex] = (byte)(packedResult >> 8);
|
---|
74 | }
|
---|
75 |
|
---|
76 | #if ALLOW_PARTIALLY_TRUSTED_CALLERS
|
---|
77 | [System.Security.SecuritySafeCriticalAttribute]
|
---|
78 | #endif
|
---|
79 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
|
---|
80 | public static void ToCharsBuffer(byte value, Span<char> buffer, int startingIndex = 0, Casing casing = Casing.Upper)
|
---|
81 | {
|
---|
82 | uint difference = (((uint)value & 0xF0U) << 4) + ((uint)value & 0x0FU) - 0x8989U;
|
---|
83 | uint packedResult = ((((uint)(-(int)difference) & 0x7070U) >> 4) + difference + 0xB9B9U) | (uint)casing;
|
---|
84 |
|
---|
85 | buffer[startingIndex + 1] = (char)(packedResult & 0xFF);
|
---|
86 | buffer[startingIndex] = (char)(packedResult >> 8);
|
---|
87 | }
|
---|
88 |
|
---|
89 | public static void EncodeToUtf16(ReadOnlySpan<byte> bytes, Span<char> chars, Casing casing = Casing.Upper)
|
---|
90 | {
|
---|
91 | Debug.Assert(chars.Length >= bytes.Length * 2);
|
---|
92 |
|
---|
93 | for (int pos = 0; pos < bytes.Length; ++pos)
|
---|
94 | {
|
---|
95 | ToCharsBuffer(bytes[pos], chars, pos * 2, casing);
|
---|
96 | }
|
---|
97 | }
|
---|
98 |
|
---|
99 | #if !UNITY_NETFRAMEWORK
|
---|
100 | #if ALLOW_PARTIALLY_TRUSTED_CALLERS
|
---|
101 | [System.Security.SecuritySafeCriticalAttribute]
|
---|
102 | #endif
|
---|
103 | public static unsafe string ToString(ReadOnlySpan<byte> bytes, Casing casing = Casing.Upper)
|
---|
104 | {
|
---|
105 | #if NETFRAMEWORK || NETSTANDARD1_0 || NETSTANDARD1_3 || NETSTANDARD2_0
|
---|
106 | Span<char> result = stackalloc char[0];
|
---|
107 | if (bytes.Length > 16)
|
---|
108 | {
|
---|
109 | var array = new char[bytes.Length * 2];
|
---|
110 | result = array.AsSpan();
|
---|
111 | }
|
---|
112 | else
|
---|
113 | {
|
---|
114 | result = stackalloc char[bytes.Length * 2];
|
---|
115 | }
|
---|
116 |
|
---|
117 | int pos = 0;
|
---|
118 | foreach (byte b in bytes)
|
---|
119 | {
|
---|
120 | ToCharsBuffer(b, result, pos, casing);
|
---|
121 | pos += 2;
|
---|
122 | }
|
---|
123 | return result.ToString();
|
---|
124 | #else
|
---|
125 | fixed (byte* bytesPtr = bytes)
|
---|
126 | {
|
---|
127 | return string.Create(bytes.Length * 2, (Ptr: (IntPtr)bytesPtr, bytes.Length, casing), static (chars, args) =>
|
---|
128 | {
|
---|
129 | var ros = new ReadOnlySpan<byte>((byte*)args.Ptr, args.Length);
|
---|
130 | EncodeToUtf16(ros, chars, args.casing);
|
---|
131 | });
|
---|
132 | }
|
---|
133 | #endif
|
---|
134 | }
|
---|
135 | #endif
|
---|
136 |
|
---|
137 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
|
---|
138 | public static char ToCharUpper(int value)
|
---|
139 | {
|
---|
140 | value &= 0xF;
|
---|
141 | value += '0';
|
---|
142 |
|
---|
143 | if (value > '9')
|
---|
144 | {
|
---|
145 | value += ('A' - ('9' + 1));
|
---|
146 | }
|
---|
147 |
|
---|
148 | return (char)value;
|
---|
149 | }
|
---|
150 |
|
---|
151 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
|
---|
152 | public static char ToCharLower(int value)
|
---|
153 | {
|
---|
154 | value &= 0xF;
|
---|
155 | value += '0';
|
---|
156 |
|
---|
157 | if (value > '9')
|
---|
158 | {
|
---|
159 | value += ('a' - ('9' + 1));
|
---|
160 | }
|
---|
161 |
|
---|
162 | return (char)value;
|
---|
163 | }
|
---|
164 |
|
---|
165 | public static bool TryDecodeFromUtf16(ReadOnlySpan<char> chars, Span<byte> bytes)
|
---|
166 | {
|
---|
167 | return TryDecodeFromUtf16(chars, bytes, out _);
|
---|
168 | }
|
---|
169 |
|
---|
170 | public static bool TryDecodeFromUtf16(ReadOnlySpan<char> chars, Span<byte> bytes, out int charsProcessed)
|
---|
171 | {
|
---|
172 | Debug.Assert(chars.Length % 2 == 0, "Un-even number of characters provided");
|
---|
173 | Debug.Assert(chars.Length / 2 == bytes.Length, "Target buffer not right-sized for provided characters");
|
---|
174 |
|
---|
175 | int i = 0;
|
---|
176 | int j = 0;
|
---|
177 | int byteLo = 0;
|
---|
178 | int byteHi = 0;
|
---|
179 | while (j < bytes.Length)
|
---|
180 | {
|
---|
181 | byteLo = FromChar(chars[i + 1]);
|
---|
182 | byteHi = FromChar(chars[i]);
|
---|
183 |
|
---|
184 | // byteHi hasn't been shifted to the high half yet, so the only way the bitwise or produces this pattern
|
---|
185 | // is if either byteHi or byteLo was not a hex character.
|
---|
186 | if ((byteLo | byteHi) == 0xFF)
|
---|
187 | break;
|
---|
188 |
|
---|
189 | bytes[j++] = (byte)((byteHi << 4) | byteLo);
|
---|
190 | i += 2;
|
---|
191 | }
|
---|
192 |
|
---|
193 | if (byteLo == 0xFF)
|
---|
194 | i++;
|
---|
195 |
|
---|
196 | charsProcessed = i;
|
---|
197 | return (byteLo | byteHi) != 0xFF;
|
---|
198 | }
|
---|
199 |
|
---|
200 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
|
---|
201 | public static int FromChar(int c)
|
---|
202 | {
|
---|
203 | return c >= CharToHexLookup.Length ? 0xFF : CharToHexLookup[c];
|
---|
204 | }
|
---|
205 |
|
---|
206 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
|
---|
207 | public static int FromUpperChar(int c)
|
---|
208 | {
|
---|
209 | return c > 71 ? 0xFF : CharToHexLookup[c];
|
---|
210 | }
|
---|
211 |
|
---|
212 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
|
---|
213 | public static int FromLowerChar(int c)
|
---|
214 | {
|
---|
215 | if ((uint)(c - '0') <= '9' - '0')
|
---|
216 | return c - '0';
|
---|
217 |
|
---|
218 | if ((uint)(c - 'a') <= 'f' - 'a')
|
---|
219 | return c - 'a' + 10;
|
---|
220 |
|
---|
221 | return 0xFF;
|
---|
222 | }
|
---|
223 |
|
---|
224 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
|
---|
225 | public static bool IsHexChar(int c)
|
---|
226 | {
|
---|
227 | return FromChar(c) != 0xFF;
|
---|
228 | }
|
---|
229 |
|
---|
230 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
|
---|
231 | public static bool IsHexUpperChar(int c)
|
---|
232 | {
|
---|
233 | return (uint)(c - '0') <= 9 || (uint)(c - 'A') <= ('F' - 'A');
|
---|
234 | }
|
---|
235 |
|
---|
236 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
|
---|
237 | public static bool IsHexLowerChar(int c)
|
---|
238 | {
|
---|
239 | return (uint)(c - '0') <= 9 || (uint)(c - 'a') <= ('f' - 'a');
|
---|
240 | }
|
---|
241 |
|
---|
242 | /// <summary>Map from an ASCII char to its hex value, e.g. arr['b'] == 11. 0xFF means it's not a hex digit.</summary>
|
---|
243 | public static ReadOnlySpan<byte> CharToHexLookup => new byte[]
|
---|
244 | {
|
---|
245 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 15
|
---|
246 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 31
|
---|
247 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 47
|
---|
248 | 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 63
|
---|
249 | 0xFF, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 79
|
---|
250 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 95
|
---|
251 | 0xFF, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 111
|
---|
252 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 127
|
---|
253 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 143
|
---|
254 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 159
|
---|
255 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 175
|
---|
256 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 191
|
---|
257 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 207
|
---|
258 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 223
|
---|
259 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 239
|
---|
260 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF // 255
|
---|
261 | };
|
---|
262 | }
|
---|
263 | }
|
---|