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
, andt
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