| 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 | }
|
|---|