< Summary

Line coverage
77%
Covered lines: 21
Uncovered lines: 6
Coverable lines: 27
Total lines: 619
Line coverage: 77.7%
Branch coverage
50%
Covered branches: 1
Total branches: 2
Branch coverage: 50%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
ToCharsBuffer(...)100%110%
FromChar(...)50%22100%

File(s)

https://raw.githubusercontent.com/dotnet/runtime/811a7eabb75c42db53440e8ba3f60c07511cfd1f/src/libraries/Common/src/System/HexConverter.cs

#LineLine coverage
 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
 4using System.Diagnostics;
 5using System.Runtime.CompilerServices;
 6using System.Numerics;
 7
 8#if SYSTEM_PRIVATE_CORELIB
 9using System.Runtime.InteropServices;
 10using System.Runtime.Intrinsics;
 11using System.Runtime.Intrinsics.Arm;
 12using System.Runtime.Intrinsics.Wasm;
 13using System.Runtime.Intrinsics.X86;
 14using System.Text;
 15using System.Text.Unicode;
 16#endif
 17
 18namespace System
 19{
 20    internal static class HexConverter
 21    {
 22        public enum Casing : uint
 23        {
 24            // Output [ '0' .. '9' ] and [ 'A' .. 'F' ].
 25            Upper = 0,
 26
 27            // Output [ '0' .. '9' ] and [ 'a' .. 'f' ].
 28            // This works because values in the range [ 0x30 .. 0x39 ] ([ '0' .. '9' ])
 29            // already have the 0x20 bit set, so ORing them with 0x20 is a no-op,
 30            // while outputs in the range [ 0x41 .. 0x46 ] ([ 'A' .. 'F' ])
 31            // don't have the 0x20 bit set, so ORing them maps to
 32            // [ 0x61 .. 0x66 ] ([ 'a' .. 'f' ]), which is what we want.
 33            Lower = 0x2020U,
 34        }
 35
 36        // We want to pack the incoming byte into a single integer [ 0000 HHHH 0000 LLLL ],
 37        // where HHHH and LLLL are the high and low nibbles of the incoming byte. Then
 38        // subtract this integer from a constant minuend as shown below.
 39        //
 40        //   [ 1000 1001 1000 1001 ]
 41        // - [ 0000 HHHH 0000 LLLL ]
 42        // =========================
 43        //   [ *YYY **** *ZZZ **** ]
 44        //
 45        // The end result of this is that YYY is 0b000 if HHHH <= 9, and YYY is 0b111 if HHHH >= 10.
 46        // Similarly, ZZZ is 0b000 if LLLL <= 9, and ZZZ is 0b111 if LLLL >= 10.
 47        // (We don't care about the value of asterisked bits.)
 48        //
 49        // To turn a nibble in the range [ 0 .. 9 ] into hex, we calculate hex := nibble + 48 (ascii '0').
 50        // To turn a nibble in the range [ 10 .. 15 ] into hex, we calculate hex := nibble - 10 + 65 (ascii 'A').
 51        //                                                                => hex := nibble + 55.
 52        // The difference in the starting ASCII offset is (55 - 48) = 7, depending on whether the nibble is <= 9 or >= 1
 53        // Since 7 is 0b111, this conveniently matches the YYY or ZZZ value computed during the earlier subtraction.
 54
 55        // The commented out code below is code that directly implements the logic described above.
 56
 57        // uint packedOriginalValues = (((uint)value & 0xF0U) << 4) + ((uint)value & 0x0FU);
 58        // uint difference = 0x8989U - packedOriginalValues;
 59        // uint add7Mask = (difference & 0x7070U) >> 4; // line YYY and ZZZ back up with the packed values
 60        // uint packedResult = packedOriginalValues + add7Mask + 0x3030U /* ascii '0' */;
 61
 62        // The code below is equivalent to the commented out code above but has been tweaked
 63        // to allow codegen to make some extra optimizations.
 64
 65        // The low byte of the packed result contains the hex representation of the incoming byte's low nibble.
 66        // The adjacent byte of the packed result contains the hex representation of the incoming byte's high nibble.
 67
 68        // Finally, write to the output buffer starting with the *highest* index so that codegen can
 69        // elide all but the first bounds check. (This only works if 'startingIndex' is a compile-time constant.)
 70
 71        // The JIT can elide bounds checks if 'startingIndex' is constant and if the caller is
 72        // writing to a span of known length (or the caller has already checked the bounds of the
 73        // furthest access).
 74        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 75        public static void ToBytesBuffer(byte value, Span<byte> buffer, int startingIndex = 0, Casing casing = Casing.Up
 76        {
 77            uint difference = (((uint)value & 0xF0U) << 4) + ((uint)value & 0x0FU) - 0x8989U;
 78            uint packedResult = ((((uint)(-(int)difference) & 0x7070U) >> 4) + difference + 0xB9B9U) | (uint)casing;
 79
 80            buffer[startingIndex + 1] = (byte)packedResult;
 81            buffer[startingIndex] = (byte)(packedResult >> 8);
 82        }
 83
 84        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 85        public static void ToCharsBuffer(byte value, Span<char> buffer, int startingIndex = 0, Casing casing = Casing.Up
 086        {
 087            uint difference = (((uint)value & 0xF0U) << 4) + ((uint)value & 0x0FU) - 0x8989U;
 088            uint packedResult = ((((uint)(-(int)difference) & 0x7070U) >> 4) + difference + 0xB9B9U) | (uint)casing;
 89
 090            buffer[startingIndex + 1] = (char)(packedResult & 0xFF);
 091            buffer[startingIndex] = (char)(packedResult >> 8);
 092        }
 93
 94#if SYSTEM_PRIVATE_CORELIB
 95        // Converts Vector128<byte> into 2xVector128<byte> ASCII Hex representation
 96        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 97        [CompExactlyDependsOn(typeof(Ssse3))]
 98        [CompExactlyDependsOn(typeof(AdvSimd.Arm64))]
 99        [CompExactlyDependsOn(typeof(PackedSimd))]
 100        internal static (Vector128<byte>, Vector128<byte>) AsciiToHexVector128(Vector128<byte> src, Vector128<byte> hexM
 101        {
 102            Debug.Assert(Ssse3.IsSupported || AdvSimd.Arm64.IsSupported || PackedSimd.IsSupported);
 103
 104            // The algorithm is simple: a single srcVec (contains the whole 16b Guid) is converted
 105            // into nibbles and then, via hexMap, converted into a HEX representation via
 106            // Shuffle(nibbles, srcVec). ASCII is then expanded to UTF-16.
 107            Vector128<byte> shiftedSrc = Vector128.ShiftRightLogical(src.AsUInt64(), 4).AsByte();
 108            Vector128<byte> lowNibbles = Vector128.UnpackLow(shiftedSrc, src);
 109            Vector128<byte> highNibbles = Vector128.UnpackHigh(shiftedSrc, src);
 110
 111            return (
 112                Vector128.ShuffleNative(hexMap, lowNibbles & Vector128.Create((byte)0xF)),
 113                Vector128.ShuffleNative(hexMap, highNibbles & Vector128.Create((byte)0xF))
 114            );
 115        }
 116
 117        [CompExactlyDependsOn(typeof(Ssse3))]
 118        [CompExactlyDependsOn(typeof(AdvSimd.Arm64))]
 119        [CompExactlyDependsOn(typeof(PackedSimd))]
 120        private static void EncodeTo_Vector128<TChar>(ReadOnlySpan<byte> source, Span<TChar> destination, Casing casing)
 121        {
 122            Debug.Assert(source.Length >= (Vector128<TChar>.Count / 2));
 123
 124            ref byte srcRef = ref MemoryMarshal.GetReference(source);
 125            ref TChar destRef = ref MemoryMarshal.GetReference(destination);
 126
 127            Vector128<byte> hexMap = casing == Casing.Upper ?
 128                Vector128.Create((byte)'0', (byte)'1', (byte)'2', (byte)'3',
 129                                 (byte)'4', (byte)'5', (byte)'6', (byte)'7',
 130                                 (byte)'8', (byte)'9', (byte)'A', (byte)'B',
 131                                 (byte)'C', (byte)'D', (byte)'E', (byte)'F') :
 132                Vector128.Create((byte)'0', (byte)'1', (byte)'2', (byte)'3',
 133                                 (byte)'4', (byte)'5', (byte)'6', (byte)'7',
 134                                 (byte)'8', (byte)'9', (byte)'a', (byte)'b',
 135                                 (byte)'c', (byte)'d', (byte)'e', (byte)'f');
 136
 137            nuint pos = 0;
 138            nuint lengthSubVector128 = (nuint)source.Length - (nuint)(Vector128<TChar>.Count / 2);
 139            do
 140            {
 141                // This implementation processes 4 or 8 bytes of input at once, it can be easily modified
 142                // to support 16 bytes at once, but that didn't demonstrate noticeable wins
 143                // for Converter.ToHexString (around 8% faster for large inputs) so
 144                // it focuses on small inputs instead.
 145
 146                Vector128<byte> vec;
 147
 148                if (typeof(TChar) == typeof(byte))
 149                {
 150                    vec = Vector128.CreateScalar(Unsafe.ReadUnaligned<ulong>(ref Unsafe.Add(ref srcRef, pos))).AsByte();
 151                }
 152                else
 153                {
 154                    Debug.Assert(typeof(TChar) == typeof(ushort));
 155                    vec = Vector128.CreateScalar(Unsafe.ReadUnaligned<uint>(ref Unsafe.Add(ref srcRef, pos))).AsByte();
 156                }
 157
 158                // JIT is expected to eliminate all unused calculations
 159                (Vector128<byte> hexLow, _) = AsciiToHexVector128(vec, hexMap);
 160
 161                if (typeof(TChar) == typeof(byte))
 162                {
 163                    hexLow.As<byte, TChar>().StoreUnsafe(ref destRef, pos * 2);
 164                }
 165                else
 166                {
 167                    Debug.Assert(typeof(TChar) == typeof(ushort));
 168                    Vector128.WidenLower(hexLow).As<ushort, TChar>().StoreUnsafe(ref destRef, pos * 2);
 169                }
 170
 171                pos += (nuint)(Vector128<TChar>.Count / 2);
 172                if (pos == (nuint)source.Length)
 173                {
 174                    return;
 175                }
 176
 177                // Overlap with the current chunk for trailing elements
 178                if (pos > lengthSubVector128)
 179                {
 180                    pos = lengthSubVector128;
 181                }
 182
 183            } while (true);
 184        }
 185#endif
 186
 187        public static void EncodeToUtf8(ReadOnlySpan<byte> source, Span<byte> utf8Destination, Casing casing = Casing.Up
 188        {
 189            Debug.Assert(utf8Destination.Length >= (source.Length * 2));
 190
 191#if SYSTEM_PRIVATE_CORELIB
 192            if ((AdvSimd.Arm64.IsSupported || Ssse3.IsSupported || PackedSimd.IsSupported) && (source.Length >= (Vector1
 193            {
 194                EncodeTo_Vector128(source, utf8Destination, casing);
 195                return;
 196            }
 197#endif
 198            for (int pos = 0; pos < source.Length; pos++)
 199            {
 200                ToBytesBuffer(source[pos], utf8Destination, pos * 2, casing);
 201            }
 202        }
 203
 204        public static void EncodeToUtf16(ReadOnlySpan<byte> source, Span<char> destination, Casing casing = Casing.Upper
 205        {
 206            Debug.Assert(destination.Length >= (source.Length * 2));
 207
 208#if SYSTEM_PRIVATE_CORELIB
 209            if ((AdvSimd.Arm64.IsSupported || Ssse3.IsSupported || PackedSimd.IsSupported) && (source.Length >= (Vector1
 210            {
 211                EncodeTo_Vector128(source, Unsafe.BitCast<Span<char>, Span<ushort>>(destination), casing);
 212                return;
 213            }
 214#endif
 215            for (int pos = 0; pos < source.Length; pos++)
 216            {
 217                ToCharsBuffer(source[pos], destination, pos * 2, casing);
 218            }
 219        }
 220
 221        public static unsafe string ToString(ReadOnlySpan<byte> bytes, Casing casing = Casing.Upper)
 222        {
 223#if NET
 224            SpanCasingPair args = new() { Bytes = bytes, Casing = casing };
 225            return string.Create(bytes.Length * 2, args, static (chars, args) =>
 226                EncodeToUtf16(args.Bytes, chars, args.Casing));
 227#else
 228            Span<char> result = (bytes.Length > 16) ?
 229                new char[bytes.Length * 2].AsSpan() :
 230                stackalloc char[bytes.Length * 2];
 231
 232            int pos = 0;
 233            foreach (byte b in bytes)
 234            {
 235                ToCharsBuffer(b, result, pos, casing);
 236                pos += 2;
 237            }
 238            return result.ToString();
 239#endif
 240        }
 241
 242        private ref struct SpanCasingPair
 243        {
 244            public ReadOnlySpan<byte> Bytes { get; set; }
 245            public Casing Casing { get; set; }
 246        }
 247
 248        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 249        public static char ToCharUpper(int value)
 250        {
 251            value &= 0xF;
 252            value += '0';
 253
 254            if (value > '9')
 255            {
 256                value += ('A' - ('9' + 1));
 257            }
 258
 259            return (char)value;
 260        }
 261
 262        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 263        public static char ToCharLower(int value)
 264        {
 265            value &= 0xF;
 266            value += '0';
 267
 268            if (value > '9')
 269            {
 270                value += ('a' - ('9' + 1));
 271            }
 272
 273            return (char)value;
 274        }
 275
 276        public static bool TryDecodeFromUtf8(ReadOnlySpan<byte> utf8Source, Span<byte> destination, out int bytesProcess
 277        {
 278#if SYSTEM_PRIVATE_CORELIB
 279            if (BitConverter.IsLittleEndian && (Ssse3.IsSupported || AdvSimd.Arm64.IsSupported || PackedSimd.IsSupported
 280                (utf8Source.Length >= Vector128<byte>.Count))
 281            {
 282                return TryDecodeFrom_Vector128(utf8Source, destination, out bytesProcessed);
 283            }
 284#endif
 285            return TryDecodeFromUtf8_Scalar(utf8Source, destination, out bytesProcessed);
 286        }
 287
 288        public static bool TryDecodeFromUtf16(ReadOnlySpan<char> source, Span<byte> destination, out int charsProcessed)
 289        {
 290#if SYSTEM_PRIVATE_CORELIB
 291            if (BitConverter.IsLittleEndian && (Ssse3.IsSupported || AdvSimd.Arm64.IsSupported || PackedSimd.IsSupported
 292                (source.Length >= (Vector128<ushort>.Count * 2)))
 293            {
 294                return TryDecodeFrom_Vector128(Unsafe.BitCast<ReadOnlySpan<char>, ReadOnlySpan<ushort>>(source), destina
 295            }
 296#endif
 297            return TryDecodeFromUtf16_Scalar(source, destination, out charsProcessed);
 298        }
 299
 300#if SYSTEM_PRIVATE_CORELIB
 301        [CompExactlyDependsOn(typeof(AdvSimd.Arm64))]
 302        [CompExactlyDependsOn(typeof(Ssse3))]
 303        [CompExactlyDependsOn(typeof(PackedSimd))]
 304        public static bool TryDecodeFrom_Vector128<TChar>(ReadOnlySpan<TChar> source, Span<byte> destination, out int el
 305        {
 306            Debug.Assert(Ssse3.IsSupported || AdvSimd.Arm64.IsSupported || PackedSimd.IsSupported);
 307            Debug.Assert(source.Length <= (destination.Length * 2));
 308            Debug.Assert((source.Length % 2) == 0);
 309
 310            int elementsReadPerIteration;
 311
 312            if (typeof(TChar) == typeof(byte))
 313            {
 314                elementsReadPerIteration = Vector128<byte>.Count;
 315            }
 316            else
 317            {
 318                Debug.Assert(typeof(TChar) == typeof(ushort));
 319                elementsReadPerIteration = Vector128<ushort>.Count * 2;
 320            }
 321            Debug.Assert(source.Length >= elementsReadPerIteration);
 322
 323            nuint offset = 0;
 324            nuint lengthSubElementsReadPerIteration = (nuint)source.Length - (nuint)elementsReadPerIteration;
 325
 326            ref TChar srcRef = ref MemoryMarshal.GetReference(source);
 327            ref byte destRef = ref MemoryMarshal.GetReference(destination);
 328
 329            do
 330            {
 331                // The algorithm is UTF8 so we'll be loading two UTF-16 vectors to narrow them into a
 332                // single UTF8 ASCII vector - the implementation can be shared with UTF8 paths.
 333                Vector128<byte> vec;
 334
 335                if (typeof(TChar) == typeof(byte))
 336                {
 337                    vec = Vector128.LoadUnsafe(ref srcRef, offset).AsByte();
 338
 339                    if (!Utf8Utility.AllBytesInVector128AreAscii(vec))
 340                    {
 341                        // Input is non-ASCII
 342                        break;
 343                    }
 344                }
 345                else
 346                {
 347                    Debug.Assert(typeof(TChar) == typeof(ushort));
 348
 349                    Vector128<ushort> vec1 = Vector128.LoadUnsafe(ref srcRef, offset).AsUInt16();
 350                    Vector128<ushort> vec2 = Vector128.LoadUnsafe(ref srcRef, offset + (nuint)Vector128<ushort>.Count).A
 351
 352                    vec = Ascii.ExtractAsciiVector(vec1, vec2);
 353
 354                    if (!Utf16Utility.AllCharsInVectorAreAscii(vec1 | vec2))
 355                    {
 356                        // Input is non-ASCII
 357                        break;
 358                    }
 359                }
 360
 361                // Based on "Algorithm #3" https://github.com/WojciechMula/toys/blob/master/simd-parse-hex/geoff_algorit
 362                // by Geoff Langdale and Wojciech Mula
 363                // Move digits '0'..'9' into range 0xf6..0xff.
 364                Vector128<byte> t1 = vec + Vector128.Create<byte>(0xFF - '9');
 365
 366                // And then correct the range to 0xf0..0xf9.
 367                // All other bytes become less than 0xf0.
 368                Vector128<byte> t2 = Vector128.SubtractSaturate(t1, Vector128.Create<byte>(6));
 369
 370                // Convert into uppercase 'a'..'f' => 'A'..'F' and
 371                // move hex letter 'A'..'F' into range 0..5.
 372                Vector128<byte> t3 = (vec & Vector128.Create<byte>(0xDF)) - Vector128.Create((byte)'A');
 373
 374                // And correct the range into 10..15.
 375                // The non-hex letters bytes become greater than 0x0f.
 376                Vector128<byte> t4 = Vector128.AddSaturate(t3, Vector128.Create<byte>(10));
 377
 378                // Convert '0'..'9' into nibbles 0..9. Non-digit bytes become
 379                // greater than 0x0f. Finally choose the result: either valid nibble (0..9/10..15)
 380                // or some byte greater than 0x0f.
 381                Vector128<byte> nibbles = Vector128.Min(t2 - Vector128.Create<byte>(0xF0), t4);
 382
 383                // Any high bit is a sign that input is not a valid hex data
 384                if (Vector128.AddSaturate(nibbles, Vector128.Create<byte>(127 - 15)).ExtractMostSignificantBits() != 0)
 385                {
 386                    // Input is invalid hex data
 387                    break;
 388                }
 389
 390                Vector128<byte> output;
 391                if (Ssse3.IsSupported)
 392                {
 393                    output = Ssse3.MultiplyAddAdjacent(nibbles, Vector128.Create<short>(0x0110).AsSByte()).AsByte();
 394                }
 395                else if (AdvSimd.Arm64.IsSupported)
 396                {
 397                    // Workaround for missing MultiplyAddAdjacent on ARM
 398                    Vector128<short> even = AdvSimd.Arm64.TransposeEven(nibbles, Vector128<byte>.Zero).AsInt16();
 399                    Vector128<short> odd = AdvSimd.Arm64.TransposeOdd(nibbles, Vector128<byte>.Zero).AsInt16();
 400
 401                    even = (even << 4).AsInt16();
 402                    output = AdvSimd.AddSaturate(even, odd).AsByte();
 403                }
 404                else if (PackedSimd.IsSupported)
 405                {
 406                    Vector128<byte> shiftedNibbles = nibbles << 4;
 407                    Vector128<byte> zipped = PackedSimd.BitwiseSelect(nibbles, shiftedNibbles, Vector128.Create<ushort>(
 408                    output = PackedSimd.AddPairwiseWidening(zipped).AsByte();
 409                }
 410                else
 411                {
 412                    // We explicitly recheck each IsSupported query to ensure that the trimmer can see which paths are l
 413                    ThrowHelper.ThrowUnreachableException();
 414                    output = default;
 415                }
 416
 417                // Accumulate output in lower INT64 half and take care about endianness
 418                output = Vector128.Shuffle(output, Vector128.Create((byte)0, 2, 4, 6, 8, 10, 12, 14, 0, 0, 0, 0, 0, 0, 0
 419
 420                // Store 8 bytes in dest by given offset
 421                Unsafe.WriteUnaligned(ref Unsafe.Add(ref destRef, offset / 2), output.AsUInt64().ToScalar());
 422
 423                offset += (nuint)elementsReadPerIteration;
 424                if (offset == (nuint)source.Length)
 425                {
 426                    elementsProcessed = source.Length;
 427                    return true;
 428                }
 429
 430                // Overlap with the current chunk for trailing elements
 431                if (offset > lengthSubElementsReadPerIteration)
 432                {
 433                    offset = lengthSubElementsReadPerIteration;
 434                }
 435            }
 436            while (true);
 437
 438            // Fall back to the scalar routine in case of invalid input.
 439            bool fallbackResult;
 440
 441            if (typeof(TChar) == typeof(byte))
 442            {
 443                fallbackResult = TryDecodeFromUtf8_Scalar(Unsafe.BitCast<ReadOnlySpan<TChar>, ReadOnlySpan<byte>>(source
 444            }
 445            else
 446            {
 447                Debug.Assert(typeof(TChar) == typeof(ushort));
 448                fallbackResult = TryDecodeFromUtf16_Scalar(Unsafe.BitCast<ReadOnlySpan<TChar>, ReadOnlySpan<char>>(sourc
 449            }
 450
 451            elementsProcessed = (int)offset + elementsProcessed;
 452            return fallbackResult;
 453        }
 454#endif
 455
 456        private static bool TryDecodeFromUtf8_Scalar(ReadOnlySpan<byte> utf8Source, Span<byte> destination, out int byte
 457        {
 458            Debug.Assert((utf8Source.Length % 2) == 0, "Un-even number of characters provided");
 459            Debug.Assert((utf8Source.Length / 2) == destination.Length, "Target buffer not right-sized for provided char
 460
 461            int i = 0;
 462            int j = 0;
 463            int byteLo = 0;
 464            int byteHi = 0;
 465
 466            while (j < destination.Length)
 467            {
 468                byteLo = FromChar(utf8Source[i + 1]);
 469                byteHi = FromChar(utf8Source[i]);
 470
 471                // byteHi hasn't been shifted to the high half yet, so the only way the bitwise or produces this pattern
 472                // is if either byteHi or byteLo was not a hex character.
 473                if ((byteLo | byteHi) == 0xFF)
 474                {
 475                    break;
 476                }
 477
 478                destination[j++] = (byte)((byteHi << 4) | byteLo);
 479                i += 2;
 480            }
 481
 482            if (byteLo == 0xFF)
 483            {
 484                i++;
 485            }
 486
 487            bytesProcessed = i;
 488            return (byteLo | byteHi) != 0xFF;
 489        }
 490
 491        private static bool TryDecodeFromUtf16_Scalar(ReadOnlySpan<char> source, Span<byte> destination, out int charsPr
 492        {
 493            Debug.Assert((source.Length % 2) == 0, "Un-even number of characters provided");
 494            Debug.Assert((source.Length / 2) == destination.Length, "Target buffer not right-sized for provided characte
 495
 496            int i = 0;
 497            int j = 0;
 498            int byteLo = 0;
 499            int byteHi = 0;
 500
 501            while (j < destination.Length)
 502            {
 503                byteLo = FromChar(source[i + 1]);
 504                byteHi = FromChar(source[i]);
 505
 506                // byteHi hasn't been shifted to the high half yet, so the only way the bitwise or produces this pattern
 507                // is if either byteHi or byteLo was not a hex character.
 508                if ((byteLo | byteHi) == 0xFF)
 509                {
 510                    break;
 511                }
 512
 513                destination[j++] = (byte)((byteHi << 4) | byteLo);
 514                i += 2;
 515            }
 516
 517            if (byteLo == 0xFF)
 518            {
 519                i++;
 520            }
 521
 522            charsProcessed = i;
 523            return (byteLo | byteHi) != 0xFF;
 524        }
 525
 526        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 527        public static int FromChar(int c)
 6969528        {
 6969529            return (c >= CharToHexLookup.Length) ? 0xFF : CharToHexLookup[c];
 6969530        }
 531
 532        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 533        public static int FromUpperChar(int c)
 534        {
 535            return (c > 71) ? 0xFF : CharToHexLookup[c];
 536        }
 537
 538        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 539        public static int FromLowerChar(int c)
 540        {
 541            if ((uint)(c - '0') <= ('9' - '0'))
 542            {
 543                return c - '0';
 544            }
 545
 546            if ((uint)(c - 'a') <= ('f' - 'a'))
 547            {
 548                return c - 'a' + 10;
 549            }
 550
 551            return 0xFF;
 552        }
 553
 554        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 555        public static bool IsHexChar(int c)
 556        {
 557            if (IntPtr.Size == 8)
 558            {
 559                // This code path, when used, has no branches and doesn't depend on cache hits,
 560                // so it's faster and does not vary in speed depending on input data distribution.
 561                // We only use this logic on 64-bit systems, as using 64 bit values would otherwise
 562                // be much slower than just using the lookup table anyway (no hardware support).
 563                // The magic constant 18428868213665201664 is a 64 bit value containing 1s at the
 564                // indices corresponding to all the valid hex characters (ie. "0123456789ABCDEFabcdef")
 565                // minus 48 (ie. '0'), and backwards (so from the most significant bit and downwards).
 566                // The offset of 48 for each bit is necessary so that the entire range fits in 64 bits.
 567                // First, we subtract '0' to the input digit (after casting to uint to account for any
 568                // negative inputs). Note that even if this subtraction underflows, this happens before
 569                // the result is zero-extended to ulong, meaning that `i` will always have upper 32 bits
 570                // equal to 0. We then left shift the constant with this offset, and apply a bitmask that
 571                // has the highest bit set (the sign bit) if and only if `c` is in the ['0', '0' + 64) range.
 572                // Then we only need to check whether this final result is less than 0: this will only be
 573                // the case if both `i` was in fact the index of a set bit in the magic constant, and also
 574                // `c` was in the allowed range (this ensures that false positive bit shifts are ignored).
 575                ulong i = (uint)c - '0';
 576                ulong shift = 18428868213665201664UL << (int)i;
 577                ulong mask = i - 64;
 578
 579                return (long)(shift & mask) < 0 ? true : false;
 580            }
 581
 582            return FromChar(c) != 0xFF;
 583        }
 584
 585        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 586        public static bool IsHexUpperChar(int c)
 587        {
 588            return ((uint)(c - '0') <= 9) || ((uint)(c - 'A') <= ('F' - 'A'));
 589        }
 590
 591        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 592        public static bool IsHexLowerChar(int c)
 593        {
 594            return ((uint)(c - '0') <= 9) || ((uint)(c - 'a') <= ('f' - 'a'));
 595        }
 596
 597        /// <summary>Map from an ASCII char to its hex value, e.g. arr['b'] == 11. 0xFF means it's not a hex digit.</sum
 598        public static ReadOnlySpan<byte> CharToHexLookup =>
 13938599        [
 13938600            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 15
 13938601            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 31
 13938602            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 47
 13938603            0x0,  0x1,  0x2,  0x3,  0x4,  0x5,  0x6,  0x7,  0x8,  0x9,  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 63
 13938604            0xFF, 0xA,  0xB,  0xC,  0xD,  0xE,  0xF,  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 79
 13938605            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 95
 13938606            0xFF, 0xa,  0xb,  0xc,  0xd,  0xe,  0xf,  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 111
 13938607            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 127
 13938608            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 143
 13938609            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 159
 13938610            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 175
 13938611            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 191
 13938612            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 207
 13938613            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 223
 13938614            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 239
 13938615            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF  // 255
 13938616        ];
 617    }
 618}
 619