// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // ReSharper disable RedundantCast #nullable disable using System.Diagnostics; using System.Runtime.CompilerServices; namespace System { internal static class HexConverter { public enum Casing : uint { // Output [ '0' .. '9' ] and [ 'A' .. 'F' ]. Upper = 0, // Output [ '0' .. '9' ] and [ 'a' .. 'f' ]. // This works because values in the range [ 0x30 .. 0x39 ] ([ '0' .. '9' ]) // already have the 0x20 bit set, so ORing them with 0x20 is a no-op, // while outputs in the range [ 0x41 .. 0x46 ] ([ 'A' .. 'F' ]) // don't have the 0x20 bit set, so ORing them maps to // [ 0x61 .. 0x66 ] ([ 'a' .. 'f' ]), which is what we want. Lower = 0x2020U, } // We want to pack the incoming byte into a single integer [ 0000 HHHH 0000 LLLL ], // where HHHH and LLLL are the high and low nibbles of the incoming byte. Then // subtract this integer from a constant minuend as shown below. // // [ 1000 1001 1000 1001 ] // - [ 0000 HHHH 0000 LLLL ] // ========================= // [ *YYY **** *ZZZ **** ] // // The end result of this is that YYY is 0b000 if HHHH <= 9, and YYY is 0b111 if HHHH >= 10. // Similarly, ZZZ is 0b000 if LLLL <= 9, and ZZZ is 0b111 if LLLL >= 10. // (We don't care about the value of asterisked bits.) // // To turn a nibble in the range [ 0 .. 9 ] into hex, we calculate hex := nibble + 48 (ascii '0'). // To turn a nibble in the range [ 10 .. 15 ] into hex, we calculate hex := nibble - 10 + 65 (ascii 'A'). // => hex := nibble + 55. // The difference in the starting ASCII offset is (55 - 48) = 7, depending on whether the nibble is <= 9 or >= 10. // Since 7 is 0b111, this conveniently matches the YYY or ZZZ value computed during the earlier subtraction. // The commented out code below is code that directly implements the logic described above. // uint packedOriginalValues = (((uint)value & 0xF0U) << 4) + ((uint)value & 0x0FU); // uint difference = 0x8989U - packedOriginalValues; // uint add7Mask = (difference & 0x7070U) >> 4; // line YYY and ZZZ back up with the packed values // uint packedResult = packedOriginalValues + add7Mask + 0x3030U /* ascii '0' */; // The code below is equivalent to the commented out code above but has been tweaked // to allow codegen to make some extra optimizations. // The low byte of the packed result contains the hex representation of the incoming byte's low nibble. // The adjacent byte of the packed result contains the hex representation of the incoming byte's high nibble. // Finally, write to the output buffer starting with the *highest* index so that codegen can // elide all but the first bounds check. (This only works if 'startingIndex' is a compile-time constant.) // The JIT can elide bounds checks if 'startingIndex' is constant and if the caller is // writing to a span of known length (or the caller has already checked the bounds of the // furthest access). [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ToBytesBuffer(byte value, Span buffer, int startingIndex = 0, Casing casing = Casing.Upper) { uint difference = (((uint)value & 0xF0U) << 4) + ((uint)value & 0x0FU) - 0x8989U; uint packedResult = ((((uint)(-(int)difference) & 0x7070U) >> 4) + difference + 0xB9B9U) | (uint)casing; buffer[startingIndex + 1] = (byte)packedResult; buffer[startingIndex] = (byte)(packedResult >> 8); } #if ALLOW_PARTIALLY_TRUSTED_CALLERS [System.Security.SecuritySafeCriticalAttribute] #endif [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ToCharsBuffer(byte value, Span buffer, int startingIndex = 0, Casing casing = Casing.Upper) { uint difference = (((uint)value & 0xF0U) << 4) + ((uint)value & 0x0FU) - 0x8989U; uint packedResult = ((((uint)(-(int)difference) & 0x7070U) >> 4) + difference + 0xB9B9U) | (uint)casing; buffer[startingIndex + 1] = (char)(packedResult & 0xFF); buffer[startingIndex] = (char)(packedResult >> 8); } public static void EncodeToUtf16(ReadOnlySpan bytes, Span chars, Casing casing = Casing.Upper) { Debug.Assert(chars.Length >= bytes.Length * 2); for (int pos = 0; pos < bytes.Length; ++pos) { ToCharsBuffer(bytes[pos], chars, pos * 2, casing); } } #if !UNITY_NETFRAMEWORK #if ALLOW_PARTIALLY_TRUSTED_CALLERS [System.Security.SecuritySafeCriticalAttribute] #endif public static unsafe string ToString(ReadOnlySpan bytes, Casing casing = Casing.Upper) { #if NETFRAMEWORK || NETSTANDARD1_0 || NETSTANDARD1_3 || NETSTANDARD2_0 Span result = stackalloc char[0]; if (bytes.Length > 16) { var array = new char[bytes.Length * 2]; result = array.AsSpan(); } else { result = stackalloc char[bytes.Length * 2]; } int pos = 0; foreach (byte b in bytes) { ToCharsBuffer(b, result, pos, casing); pos += 2; } return result.ToString(); #else fixed (byte* bytesPtr = bytes) { return string.Create(bytes.Length * 2, (Ptr: (IntPtr)bytesPtr, bytes.Length, casing), static (chars, args) => { var ros = new ReadOnlySpan((byte*)args.Ptr, args.Length); EncodeToUtf16(ros, chars, args.casing); }); } #endif } #endif [MethodImpl(MethodImplOptions.AggressiveInlining)] public static char ToCharUpper(int value) { value &= 0xF; value += '0'; if (value > '9') { value += ('A' - ('9' + 1)); } return (char)value; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static char ToCharLower(int value) { value &= 0xF; value += '0'; if (value > '9') { value += ('a' - ('9' + 1)); } return (char)value; } public static bool TryDecodeFromUtf16(ReadOnlySpan chars, Span bytes) { return TryDecodeFromUtf16(chars, bytes, out _); } public static bool TryDecodeFromUtf16(ReadOnlySpan chars, Span bytes, out int charsProcessed) { Debug.Assert(chars.Length % 2 == 0, "Un-even number of characters provided"); Debug.Assert(chars.Length / 2 == bytes.Length, "Target buffer not right-sized for provided characters"); int i = 0; int j = 0; int byteLo = 0; int byteHi = 0; while (j < bytes.Length) { byteLo = FromChar(chars[i + 1]); byteHi = FromChar(chars[i]); // byteHi hasn't been shifted to the high half yet, so the only way the bitwise or produces this pattern // is if either byteHi or byteLo was not a hex character. if ((byteLo | byteHi) == 0xFF) break; bytes[j++] = (byte)((byteHi << 4) | byteLo); i += 2; } if (byteLo == 0xFF) i++; charsProcessed = i; return (byteLo | byteHi) != 0xFF; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int FromChar(int c) { return c >= CharToHexLookup.Length ? 0xFF : CharToHexLookup[c]; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int FromUpperChar(int c) { return c > 71 ? 0xFF : CharToHexLookup[c]; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int FromLowerChar(int c) { if ((uint)(c - '0') <= '9' - '0') return c - '0'; if ((uint)(c - 'a') <= 'f' - 'a') return c - 'a' + 10; return 0xFF; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsHexChar(int c) { return FromChar(c) != 0xFF; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsHexUpperChar(int c) { return (uint)(c - '0') <= 9 || (uint)(c - 'A') <= ('F' - 'A'); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsHexLowerChar(int c) { return (uint)(c - '0') <= 9 || (uint)(c - 'a') <= ('f' - 'a'); } /// Map from an ASCII char to its hex value, e.g. arr['b'] == 11. 0xFF means it's not a hex digit. public static ReadOnlySpan CharToHexLookup => new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 15 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 31 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 47 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 63 0xFF, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 79 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 95 0xFF, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 111 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 127 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 143 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 159 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 175 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 191 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 207 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 223 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 239 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF // 255 }; } }