< Summary

Information
Class: System.HexConverter
Assembly: System.Net.Http
File(s): D:\runner\runtime\src\libraries\Common\src\System\HexConverter.cs
Line coverage
70%
Covered lines: 21
Uncovered lines: 9
Coverable lines: 30
Total lines: 616
Line coverage: 70%
Branch coverage
50%
Covered branches: 2
Total branches: 4
Branch coverage: 50%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
ToCharUpper(...)0%220%
FromUpperChar(...)100%22100%

File(s)

D:\runner\runtime\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
 86        {
 87            uint difference = (((uint)value & 0xF0U) << 4) + ((uint)value & 0x0FU) - 0x8989U;
 88            uint packedResult = ((((uint)(-(int)difference) & 0x7070U) >> 4) + difference + 0xB9B9U) | (uint)casing;
 89
 90            buffer[startingIndex + 1] = (char)(packedResult & 0xFF);
 91            buffer[startingIndex] = (char)(packedResult >> 8);
 92        }
 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        internal static (Vector128<byte>, Vector128<byte>) AsciiToHexVector128(Vector128<byte> src, Vector128<byte> hexM
 100        {
 101            Debug.Assert(Ssse3.IsSupported || AdvSimd.Arm64.IsSupported);
 102
 103            // The algorithm is simple: a single srcVec (contains the whole 16b Guid) is converted
 104            // into nibbles and then, via hexMap, converted into a HEX representation via
 105            // Shuffle(nibbles, srcVec). ASCII is then expanded to UTF-16.
 106            Vector128<byte> shiftedSrc = Vector128.ShiftRightLogical(src.AsUInt64(), 4).AsByte();
 107            Vector128<byte> lowNibbles = Vector128.UnpackLow(shiftedSrc, src);
 108            Vector128<byte> highNibbles = Vector128.UnpackHigh(shiftedSrc, src);
 109
 110            return (
 111                Vector128.ShuffleNative(hexMap, lowNibbles & Vector128.Create((byte)0xF)),
 112                Vector128.ShuffleNative(hexMap, highNibbles & Vector128.Create((byte)0xF))
 113            );
 114        }
 115
 116        [CompExactlyDependsOn(typeof(Ssse3))]
 117        [CompExactlyDependsOn(typeof(AdvSimd.Arm64))]
 118        private static void EncodeTo_Vector128<TChar>(ReadOnlySpan<byte> source, Span<TChar> destination, Casing casing)
 119        {
 120            Debug.Assert(source.Length >= (Vector128<TChar>.Count / 2));
 121
 122            ref byte srcRef = ref MemoryMarshal.GetReference(source);
 123            ref TChar destRef = ref MemoryMarshal.GetReference(destination);
 124
 125            Vector128<byte> hexMap = casing == Casing.Upper ?
 126                Vector128.Create((byte)'0', (byte)'1', (byte)'2', (byte)'3',
 127                                 (byte)'4', (byte)'5', (byte)'6', (byte)'7',
 128                                 (byte)'8', (byte)'9', (byte)'A', (byte)'B',
 129                                 (byte)'C', (byte)'D', (byte)'E', (byte)'F') :
 130                Vector128.Create((byte)'0', (byte)'1', (byte)'2', (byte)'3',
 131                                 (byte)'4', (byte)'5', (byte)'6', (byte)'7',
 132                                 (byte)'8', (byte)'9', (byte)'a', (byte)'b',
 133                                 (byte)'c', (byte)'d', (byte)'e', (byte)'f');
 134
 135            nuint pos = 0;
 136            nuint lengthSubVector128 = (nuint)source.Length - (nuint)(Vector128<TChar>.Count / 2);
 137            do
 138            {
 139                // This implementation processes 4 or 8 bytes of input at once, it can be easily modified
 140                // to support 16 bytes at once, but that didn't demonstrate noticeable wins
 141                // for Converter.ToHexString (around 8% faster for large inputs) so
 142                // it focuses on small inputs instead.
 143
 144                Vector128<byte> vec;
 145
 146                if (typeof(TChar) == typeof(byte))
 147                {
 148                    vec = Vector128.CreateScalar(Unsafe.ReadUnaligned<ulong>(ref Unsafe.Add(ref srcRef, pos))).AsByte();
 149                }
 150                else
 151                {
 152                    Debug.Assert(typeof(TChar) == typeof(ushort));
 153                    vec = Vector128.CreateScalar(Unsafe.ReadUnaligned<uint>(ref Unsafe.Add(ref srcRef, pos))).AsByte();
 154                }
 155
 156                // JIT is expected to eliminate all unused calculations
 157                (Vector128<byte> hexLow, _) = AsciiToHexVector128(vec, hexMap);
 158
 159                if (typeof(TChar) == typeof(byte))
 160                {
 161                    hexLow.As<byte, TChar>().StoreUnsafe(ref destRef, pos * 2);
 162                }
 163                else
 164                {
 165                    Debug.Assert(typeof(TChar) == typeof(ushort));
 166                    Vector128.WidenLower(hexLow).As<ushort, TChar>().StoreUnsafe(ref destRef, pos * 2);
 167                }
 168
 169                pos += (nuint)(Vector128<TChar>.Count / 2);
 170                if (pos == (nuint)source.Length)
 171                {
 172                    return;
 173                }
 174
 175                // Overlap with the current chunk for trailing elements
 176                if (pos > lengthSubVector128)
 177                {
 178                    pos = lengthSubVector128;
 179                }
 180
 181            } while (true);
 182        }
 183#endif
 184
 185        public static void EncodeToUtf8(ReadOnlySpan<byte> source, Span<byte> utf8Destination, Casing casing = Casing.Up
 186        {
 187            Debug.Assert(utf8Destination.Length >= (source.Length * 2));
 188
 189#if SYSTEM_PRIVATE_CORELIB
 190            if ((AdvSimd.Arm64.IsSupported || Ssse3.IsSupported) && (source.Length >= (Vector128<byte>.Count / 2)))
 191            {
 192                EncodeTo_Vector128(source, utf8Destination, casing);
 193                return;
 194            }
 195#endif
 196            for (int pos = 0; pos < source.Length; pos++)
 197            {
 198                ToBytesBuffer(source[pos], utf8Destination, pos * 2, casing);
 199            }
 200        }
 201
 202        public static void EncodeToUtf16(ReadOnlySpan<byte> source, Span<char> destination, Casing casing = Casing.Upper
 203        {
 204            Debug.Assert(destination.Length >= (source.Length * 2));
 205
 206#if SYSTEM_PRIVATE_CORELIB
 207            if ((AdvSimd.Arm64.IsSupported || Ssse3.IsSupported) && (source.Length >= (Vector128<ushort>.Count / 2)))
 208            {
 209                EncodeTo_Vector128(source, Unsafe.BitCast<Span<char>, Span<ushort>>(destination), casing);
 210                return;
 211            }
 212#endif
 213            for (int pos = 0; pos < source.Length; pos++)
 214            {
 215                ToCharsBuffer(source[pos], destination, pos * 2, casing);
 216            }
 217        }
 218
 219        public static string ToString(ReadOnlySpan<byte> bytes, Casing casing = Casing.Upper)
 220        {
 221#if NET
 222            SpanCasingPair args = new() { Bytes = bytes, Casing = casing };
 223            return string.Create(bytes.Length * 2, args, static (chars, args) =>
 224                EncodeToUtf16(args.Bytes, chars, args.Casing));
 225#else
 226            Span<char> result = (bytes.Length > 16) ?
 227                new char[bytes.Length * 2].AsSpan() :
 228                stackalloc char[bytes.Length * 2];
 229
 230            int pos = 0;
 231            foreach (byte b in bytes)
 232            {
 233                ToCharsBuffer(b, result, pos, casing);
 234                pos += 2;
 235            }
 236            return result.ToString();
 237#endif
 238        }
 239
 240        private ref struct SpanCasingPair
 241        {
 242            public ReadOnlySpan<byte> Bytes { get; set; }
 243            public Casing Casing { get; set; }
 244        }
 245
 246        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 247        public static char ToCharUpper(int value)
 0248        {
 0249            value &= 0xF;
 0250            value += '0';
 251
 0252            if (value > '9')
 0253            {
 0254                value += ('A' - ('9' + 1));
 0255            }
 256
 0257            return (char)value;
 0258        }
 259
 260        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 261        public static char ToCharLower(int value)
 262        {
 263            value &= 0xF;
 264            value += '0';
 265
 266            if (value > '9')
 267            {
 268                value += ('a' - ('9' + 1));
 269            }
 270
 271            return (char)value;
 272        }
 273
 274        public static bool TryDecodeFromUtf8(ReadOnlySpan<byte> utf8Source, Span<byte> destination, out int bytesProcess
 275        {
 276#if SYSTEM_PRIVATE_CORELIB
 277            if (BitConverter.IsLittleEndian && (Ssse3.IsSupported || AdvSimd.Arm64.IsSupported || PackedSimd.IsSupported
 278                (utf8Source.Length >= Vector128<byte>.Count))
 279            {
 280                return TryDecodeFrom_Vector128(utf8Source, destination, out bytesProcessed);
 281            }
 282#endif
 283            return TryDecodeFromUtf8_Scalar(utf8Source, destination, out bytesProcessed);
 284        }
 285
 286        public static bool TryDecodeFromUtf16(ReadOnlySpan<char> source, Span<byte> destination, out int charsProcessed)
 287        {
 288#if SYSTEM_PRIVATE_CORELIB
 289            if (BitConverter.IsLittleEndian && (Ssse3.IsSupported || AdvSimd.Arm64.IsSupported || PackedSimd.IsSupported
 290                (source.Length >= (Vector128<ushort>.Count * 2)))
 291            {
 292                return TryDecodeFrom_Vector128(Unsafe.BitCast<ReadOnlySpan<char>, ReadOnlySpan<ushort>>(source), destina
 293            }
 294#endif
 295            return TryDecodeFromUtf16_Scalar(source, destination, out charsProcessed);
 296        }
 297
 298#if SYSTEM_PRIVATE_CORELIB
 299        [CompExactlyDependsOn(typeof(AdvSimd.Arm64))]
 300        [CompExactlyDependsOn(typeof(Ssse3))]
 301        [CompExactlyDependsOn(typeof(PackedSimd))]
 302        public static bool TryDecodeFrom_Vector128<TChar>(ReadOnlySpan<TChar> source, Span<byte> destination, out int el
 303        {
 304            Debug.Assert(Ssse3.IsSupported || AdvSimd.Arm64.IsSupported || PackedSimd.IsSupported);
 305            Debug.Assert(source.Length <= (destination.Length * 2));
 306            Debug.Assert((source.Length % 2) == 0);
 307
 308            int elementsReadPerIteration;
 309
 310            if (typeof(TChar) == typeof(byte))
 311            {
 312                elementsReadPerIteration = Vector128<byte>.Count;
 313            }
 314            else
 315            {
 316                Debug.Assert(typeof(TChar) == typeof(ushort));
 317                elementsReadPerIteration = Vector128<ushort>.Count * 2;
 318            }
 319            Debug.Assert(source.Length >= elementsReadPerIteration);
 320
 321            nuint offset = 0;
 322            nuint lengthSubElementsReadPerIteration = (nuint)source.Length - (nuint)elementsReadPerIteration;
 323
 324            ref TChar srcRef = ref MemoryMarshal.GetReference(source);
 325            ref byte destRef = ref MemoryMarshal.GetReference(destination);
 326
 327            do
 328            {
 329                // The algorithm is UTF8 so we'll be loading two UTF-16 vectors to narrow them into a
 330                // single UTF8 ASCII vector - the implementation can be shared with UTF8 paths.
 331                Vector128<byte> vec;
 332
 333                if (typeof(TChar) == typeof(byte))
 334                {
 335                    vec = Vector128.LoadUnsafe(ref srcRef, offset).AsByte();
 336
 337                    if (!Utf8Utility.AllBytesInVector128AreAscii(vec))
 338                    {
 339                        // Input is non-ASCII
 340                        break;
 341                    }
 342                }
 343                else
 344                {
 345                    Debug.Assert(typeof(TChar) == typeof(ushort));
 346
 347                    Vector128<ushort> vec1 = Vector128.LoadUnsafe(ref srcRef, offset).AsUInt16();
 348                    Vector128<ushort> vec2 = Vector128.LoadUnsafe(ref srcRef, offset + (nuint)Vector128<ushort>.Count).A
 349
 350                    vec = Ascii.ExtractAsciiVector(vec1, vec2);
 351
 352                    if (!Utf16Utility.AllCharsInVectorAreAscii(vec1 | vec2))
 353                    {
 354                        // Input is non-ASCII
 355                        break;
 356                    }
 357                }
 358
 359                // Based on "Algorithm #3" https://github.com/WojciechMula/toys/blob/master/simd-parse-hex/geoff_algorit
 360                // by Geoff Langdale and Wojciech Mula
 361                // Move digits '0'..'9' into range 0xf6..0xff.
 362                Vector128<byte> t1 = vec + Vector128.Create<byte>(0xFF - '9');
 363
 364                // And then correct the range to 0xf0..0xf9.
 365                // All other bytes become less than 0xf0.
 366                Vector128<byte> t2 = Vector128.SubtractSaturate(t1, Vector128.Create<byte>(6));
 367
 368                // Convert into uppercase 'a'..'f' => 'A'..'F' and
 369                // move hex letter 'A'..'F' into range 0..5.
 370                Vector128<byte> t3 = (vec & Vector128.Create<byte>(0xDF)) - Vector128.Create((byte)'A');
 371
 372                // And correct the range into 10..15.
 373                // The non-hex letters bytes become greater than 0x0f.
 374                Vector128<byte> t4 = Vector128.AddSaturate(t3, Vector128.Create<byte>(10));
 375
 376                // Convert '0'..'9' into nibbles 0..9. Non-digit bytes become
 377                // greater than 0x0f. Finally choose the result: either valid nibble (0..9/10..15)
 378                // or some byte greater than 0x0f.
 379                Vector128<byte> nibbles = Vector128.Min(t2 - Vector128.Create<byte>(0xF0), t4);
 380
 381                // Any high bit is a sign that input is not a valid hex data
 382                if (Vector128.AddSaturate(nibbles, Vector128.Create<byte>(127 - 15)).ExtractMostSignificantBits() != 0)
 383                {
 384                    // Input is invalid hex data
 385                    break;
 386                }
 387
 388                Vector128<byte> output;
 389                if (Ssse3.IsSupported)
 390                {
 391                    output = Ssse3.MultiplyAddAdjacent(nibbles, Vector128.Create<short>(0x0110).AsSByte()).AsByte();
 392                }
 393                else if (AdvSimd.Arm64.IsSupported)
 394                {
 395                    // Workaround for missing MultiplyAddAdjacent on ARM
 396                    Vector128<short> even = AdvSimd.Arm64.TransposeEven(nibbles, Vector128<byte>.Zero).AsInt16();
 397                    Vector128<short> odd = AdvSimd.Arm64.TransposeOdd(nibbles, Vector128<byte>.Zero).AsInt16();
 398
 399                    even = (even << 4).AsInt16();
 400                    output = AdvSimd.AddSaturate(even, odd).AsByte();
 401                }
 402                else if (PackedSimd.IsSupported)
 403                {
 404                    Vector128<byte> shiftedNibbles = nibbles << 4;
 405                    Vector128<byte> zipped = PackedSimd.BitwiseSelect(nibbles, shiftedNibbles, Vector128.Create<ushort>(
 406                    output = PackedSimd.AddPairwiseWidening(zipped).AsByte();
 407                }
 408                else
 409                {
 410                    // We explicitly recheck each IsSupported query to ensure that the trimmer can see which paths are l
 411                    ThrowHelper.ThrowUnreachableException();
 412                    output = default;
 413                }
 414
 415                // Accumulate output in lower INT64 half and take care about endianness
 416                output = Vector128.Shuffle(output, Vector128.Create((byte)0, 2, 4, 6, 8, 10, 12, 14, 0, 0, 0, 0, 0, 0, 0
 417
 418                // Store 8 bytes in dest by given offset
 419                Unsafe.WriteUnaligned(ref Unsafe.Add(ref destRef, offset / 2), output.AsUInt64().ToScalar());
 420
 421                offset += (nuint)elementsReadPerIteration;
 422                if (offset == (nuint)source.Length)
 423                {
 424                    elementsProcessed = source.Length;
 425                    return true;
 426                }
 427
 428                // Overlap with the current chunk for trailing elements
 429                if (offset > lengthSubElementsReadPerIteration)
 430                {
 431                    offset = lengthSubElementsReadPerIteration;
 432                }
 433            }
 434            while (true);
 435
 436            // Fall back to the scalar routine in case of invalid input.
 437            bool fallbackResult;
 438
 439            if (typeof(TChar) == typeof(byte))
 440            {
 441                fallbackResult = TryDecodeFromUtf8_Scalar(Unsafe.BitCast<ReadOnlySpan<TChar>, ReadOnlySpan<byte>>(source
 442            }
 443            else
 444            {
 445                Debug.Assert(typeof(TChar) == typeof(ushort));
 446                fallbackResult = TryDecodeFromUtf16_Scalar(Unsafe.BitCast<ReadOnlySpan<TChar>, ReadOnlySpan<char>>(sourc
 447            }
 448
 449            elementsProcessed = (int)offset + elementsProcessed;
 450            return fallbackResult;
 451        }
 452#endif
 453
 454        private static bool TryDecodeFromUtf8_Scalar(ReadOnlySpan<byte> utf8Source, Span<byte> destination, out int byte
 455        {
 456            Debug.Assert((utf8Source.Length % 2) == 0, "Un-even number of characters provided");
 457            Debug.Assert((utf8Source.Length / 2) == destination.Length, "Target buffer not right-sized for provided char
 458
 459            int i = 0;
 460            int j = 0;
 461            int byteLo = 0;
 462            int byteHi = 0;
 463
 464            while (j < destination.Length)
 465            {
 466                byteLo = FromChar(utf8Source[i + 1]);
 467                byteHi = FromChar(utf8Source[i]);
 468
 469                // byteHi hasn't been shifted to the high half yet, so the only way the bitwise or produces this pattern
 470                // is if either byteHi or byteLo was not a hex character.
 471                if ((byteLo | byteHi) == 0xFF)
 472                {
 473                    break;
 474                }
 475
 476                destination[j++] = (byte)((byteHi << 4) | byteLo);
 477                i += 2;
 478            }
 479
 480            if (byteLo == 0xFF)
 481            {
 482                i++;
 483            }
 484
 485            bytesProcessed = i;
 486            return (byteLo | byteHi) != 0xFF;
 487        }
 488
 489        private static bool TryDecodeFromUtf16_Scalar(ReadOnlySpan<char> source, Span<byte> destination, out int charsPr
 490        {
 491            Debug.Assert((source.Length % 2) == 0, "Un-even number of characters provided");
 492            Debug.Assert((source.Length / 2) == destination.Length, "Target buffer not right-sized for provided characte
 493
 494            int i = 0;
 495            int j = 0;
 496            int byteLo = 0;
 497            int byteHi = 0;
 498
 499            while (j < destination.Length)
 500            {
 501                byteLo = FromChar(source[i + 1]);
 502                byteHi = FromChar(source[i]);
 503
 504                // byteHi hasn't been shifted to the high half yet, so the only way the bitwise or produces this pattern
 505                // is if either byteHi or byteLo was not a hex character.
 506                if ((byteLo | byteHi) == 0xFF)
 507                {
 508                    break;
 509                }
 510
 511                destination[j++] = (byte)((byteHi << 4) | byteLo);
 512                i += 2;
 513            }
 514
 515            if (byteLo == 0xFF)
 516            {
 517                i++;
 518            }
 519
 520            charsProcessed = i;
 521            return (byteLo | byteHi) != 0xFF;
 522        }
 523
 524        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 525        public static int FromChar(int c)
 526        {
 527            return (c >= CharToHexLookup.Length) ? 0xFF : CharToHexLookup[c];
 528        }
 529
 530        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 531        public static int FromUpperChar(int c)
 4354532        {
 4354533            return (c > 71) ? 0xFF : CharToHexLookup[c];
 4354534        }
 535
 536        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 537        public static int FromLowerChar(int c)
 538        {
 539            if ((uint)(c - '0') <= ('9' - '0'))
 540            {
 541                return c - '0';
 542            }
 543
 544            if ((uint)(c - 'a') <= ('f' - 'a'))
 545            {
 546                return c - 'a' + 10;
 547            }
 548
 549            return 0xFF;
 550        }
 551
 552        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 553        public static bool IsHexChar(int c)
 554        {
 555            if (IntPtr.Size == 8)
 556            {
 557                // This code path, when used, has no branches and doesn't depend on cache hits,
 558                // so it's faster and does not vary in speed depending on input data distribution.
 559                // We only use this logic on 64-bit systems, as using 64 bit values would otherwise
 560                // be much slower than just using the lookup table anyway (no hardware support).
 561                // The magic constant 18428868213665201664 is a 64 bit value containing 1s at the
 562                // indices corresponding to all the valid hex characters (ie. "0123456789ABCDEFabcdef")
 563                // minus 48 (ie. '0'), and backwards (so from the most significant bit and downwards).
 564                // The offset of 48 for each bit is necessary so that the entire range fits in 64 bits.
 565                // First, we subtract '0' to the input digit (after casting to uint to account for any
 566                // negative inputs). Note that even if this subtraction underflows, this happens before
 567                // the result is zero-extended to ulong, meaning that `i` will always have upper 32 bits
 568                // equal to 0. We then left shift the constant with this offset, and apply a bitmask that
 569                // has the highest bit set (the sign bit) if and only if `c` is in the ['0', '0' + 64) range.
 570                // Then we only need to check whether this final result is less than 0: this will only be
 571                // the case if both `i` was in fact the index of a set bit in the magic constant, and also
 572                // `c` was in the allowed range (this ensures that false positive bit shifts are ignored).
 573                ulong i = (uint)c - '0';
 574                ulong shift = 18428868213665201664UL << (int)i;
 575                ulong mask = i - 64;
 576
 577                return (long)(shift & mask) < 0 ? true : false;
 578            }
 579
 580            return FromChar(c) != 0xFF;
 581        }
 582
 583        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 584        public static bool IsHexUpperChar(int c)
 585        {
 586            return ((uint)(c - '0') <= 9) || ((uint)(c - 'A') <= ('F' - 'A'));
 587        }
 588
 589        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 590        public static bool IsHexLowerChar(int c)
 591        {
 592            return ((uint)(c - '0') <= 9) || ((uint)(c - 'a') <= ('f' - 'a'));
 593        }
 594
 595        /// <summary>Map from an ASCII char to its hex value, e.g. arr['b'] == 11. 0xFF means it's not a hex digit.</sum
 596        public static ReadOnlySpan<byte> CharToHexLookup =>
 4348597        [
 4348598            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 15
 4348599            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 31
 4348600            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 47
 4348601            0x0,  0x1,  0x2,  0x3,  0x4,  0x5,  0x6,  0x7,  0x8,  0x9,  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 63
 4348602            0xFF, 0xA,  0xB,  0xC,  0xD,  0xE,  0xF,  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 79
 4348603            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 95
 4348604            0xFF, 0xa,  0xb,  0xc,  0xd,  0xe,  0xf,  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 111
 4348605            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 127
 4348606            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 143
 4348607            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 159
 4348608            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 175
 4348609            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 191
 4348610            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 207
 4348611            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 223
 4348612            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 239
 4348613            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF  // 255
 4348614        ];
 615    }
 616}