Skip to main content
Code Review

Return to Revisions

2 of 4
various bug fixes related to null or empty values
Kittoes0124
  • 2k
  • 2
  • 18
  • 24

handling hexadecimal strings & byte arrays

I recently ran into a problem that requires me to work with hexadecimal strings and wanted to learn how the encoding/decoding process works. Using a bunch of help from posts on SO I was able to cobble together a Hexadecimal struct with the static methods GetBytes and GetString.

Usage:

var x = System.Text.Encoding.ASCII.GetBytes("Hello world!");
var y = Hexadecimal.GetString(x, "0x"); // value: "0x48656C6C6F20776F726C6421"
var z = Hexadecimal.GetBytes(y, 2); // value: new byte[] { 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x77, 0x6F, 0x72, 0x6C, 0x64, 0x21 }

Notes:

  • The most important thing is that the methods properly handle all possible inputs.
  • I wanted to avoid lookup tables of hard-coded "magic" values and calculate them instead.
  • Performance must suffer as little as possible while trying accomplish any other goals.
  • Normally unacceptable variables names such as m, s, and t are used because I personally believe that it makes this particular code easier to reason about.

Code:

/// <summary>
/// Represents a single byte in hexadecimal notation.
/// </summary>
[StructLayout(LayoutKind.Explicit, Pack = 2, Size = 4)]
public struct Hexadecimal
{
 private static readonly uint[] m_byteToHexMap = Enumerable
 .Range(0, 256)
 .Select(a => (((uint)(GetChar((byte)(a & 0x0000000F)) << 16)) | GetChar((byte)(a >> 4))))
 .ToArray();
 private static readonly unsafe Hexadecimal* m_byteToHexMapPointer = ((Hexadecimal*)GCHandle.Alloc(m_byteToHexMap, GCHandleType.Pinned).AddrOfPinnedObject());
 private static readonly byte[] m_hexToByteMap = Enumerable
 .Range(0, 103)
 .Select(a => ("0123456789ABCDEFabcdef".Contains((char)a) ? GetNybble((char)a) : ((byte)0xFF)))
 .ToArray();
 private static readonly unsafe byte* m_hexToByteMapPointer = ((byte*)GCHandle.Alloc(m_hexToByteMap, GCHandleType.Pinned).AddrOfPinnedObject());
 [FieldOffset((sizeof(char) * 0))]
 private readonly char m_high;
 [FieldOffset((sizeof(char) * 1))]
 private readonly char m_low;
 /// <summary>
 /// Returns the left symbol of this <see cref="Hexadecimal"/> value.
 /// </summary>
 public char High => m_high;
 /// <summary>
 /// Returns the right symbol of this <see cref="Hexadecimal"/> value.
 /// </summary>
 public char Low => m_low;
 /// <summary>
 /// Converts a hexadecimal string into an array of bytes.
 /// </summary>
 /// <param name="value">The hexadecimal string that will be converted.</param>
 /// <param name="prefixLength">The length of the prefix to be skipped over.</param>
 public static unsafe byte[] GetBytes(string value, int prefixLength) {
 if (prefixLength < 0) {
 throw new ArgumentOutOfRangeException(message: "prefix length cannot be less than zero", paramName: nameof(prefixLength));
 }
 if (value == null) {
 return null;
 }
 var stringLength = checked(value.Length - prefixLength);
 if (stringLength < 1) {
#if (NET40 || NET45 || NET451 || NET452)
 return new byte[0];
#else
 return Array.Empty<byte>();
#endif
 }
 if (stringLength.IsOdd()) {
 throw new ArgumentOutOfRangeException(message: "hexadecimal string cannot have an odd length", paramName: nameof(value));
 }
 var count = (stringLength >> 1);
 var result = new byte[count];
 fixed (char* source = value)
 fixed (byte* target = &result[0]) {
 var m = m_hexToByteMapPointer;
 var s = ((Hexadecimal*)(source + prefixLength));
 var t = target;
 while (0 < count--) {
 var high = (*s).High;
 var low = (*s).Low;
 if (((high < 0x67) && (low < 0x67)) && ((m[high] < 0xFF) && (m[low] < 0xFF))) {
 *t = ((byte)(((uint)(m[high] << 4)) | m[low]));
 }
 else {
 throw new IndexOutOfRangeException(message: ("invalid hexadecimal string encountered: " + high + low));
 }
 s++;
 t++;
 }
 }
 return result;
 }
 /// <summary>
 /// Converts a hexadecimal string into an array of bytes.
 /// </summary>
 /// <param name="value">The hexadecimal string that will be converted.</param>
#if !(NET40)
 [MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
 public static byte[] GetBytes(string value) {
 return GetBytes(value, 0);
 }
 /// <summary>
 /// Converts an array of bytes into a hexadecimal string.
 /// </summary>
 /// <param name="value">The array of bytes that will be converted.</param>
 /// <param name="prefix">The prefix that will be appended to the hexadecimal string.</param>
 public static unsafe string GetString(byte[] value, string prefix) {
 if (value == null) {
 return null;
 }
 prefix = (prefix ?? string.Empty);
 if (value.Length == 0) {
 return prefix;
 }
 var count = value.Length;
 var result = new string('☠', ((count << 1) + prefix.Length));
 fixed (byte* source = &value[0])
 fixed (char* target = result) {
 for (var i = 0; i < prefix.Length; i++) {
 *(target + i) = prefix[i];
 }
 var m = m_byteToHexMapPointer;
 var s = source;
 var t = ((Hexadecimal*)(target + prefix.Length));
 while (0 < count--) {
 *t = m[*s];
 s++;
 t++;
 }
 }
 return result;
 }
 /// <summary>
 /// Converts an array of bytes into a hexadecimal string.
 /// </summary>
 /// <param name="value">The array of bytes that will be converted.</param>
#if !(NET40)
 [MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
 public static unsafe string GetString(byte[] value) {
 return GetString(value, string.Empty);
 }
#if !(NET40)
 [MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
 private static char GetChar(byte value) { // https://stackoverflow.com/a/14333437
 return ((char)((55 + value) + (((value - 10) >> 31) & 0xFFFFFFF9)));
 }
#if !(NET40)
 [MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
 private static byte GetNybble(char value) { // https://stackoverflow.com/a/20695932
 return ((byte)(((value - 48) + (((57 - value) >> 31) & 0xFFFFFFF9)) & 0x0000000F));
 }
}
Kittoes0124
  • 2k
  • 2
  • 18
  • 24
lang-cs

AltStyle によって変換されたページ (->オリジナル) /