Skip to content

Commit

Permalink
Add fromHexString, fromHexStringAsRange and isHexString
Browse files Browse the repository at this point in the history
Squashed commit of the following:

commit af66b61
Author: Elias Batek
Date:   Sun Nov 3 09:37:51 2024 +0100

    Further cleanup

commit cb122cd
Author: Elias Batek
Date:   Sun Nov 3 09:31:42 2024 +0100

    Add bloat because of questionable Circle CI failure

commit 1ad9e88
Author: Elias Batek
Date:   Sun Nov 3 09:24:37 2024 +0100

    Add cycle test

commit 913cfc7
Author: Elias Batek
Date:   Sun Nov 3 09:13:47 2024 +0100

    Split long lines

commit 4c062cc
Author: Elias Batek
Date:   Sun Nov 3 09:11:38 2024 +0100

    Do not leak implementation details in unittest

commit 66e64de
Author: Elias Batek
Date:   Sun Nov 3 09:09:17 2024 +0100

    Improve unittests

commit 1b95777
Author: Elias Batek
Date:   Sun Nov 3 09:00:16 2024 +0100

    Fix style

commit b1e4c61
Author: Elias Batek
Date:   Sun Nov 3 08:56:02 2024 +0100

    Improve unittests

commit c5aed51
Author: Elias Batek
Date:   Sun Nov 3 08:31:57 2024 +0100

    Fix decoder regression

commit 89f4d9b
Author: Elias Batek
Date:   Sun Nov 3 08:21:52 2024 +0100

    Rename

commit c3a8c52
Author: Elias Batek
Date:   Sun Nov 3 08:11:03 2024 +0100

    Oops

commit aff5b0d
Author: Elias Batek
Date:   Sun Nov 3 08:03:32 2024 +0100

    Fix typo and wstring/dstring bug

commit dbe75e9
Author: Elias Batek
Date:   Sun Nov 3 08:03:23 2024 +0100

    Improve docs

commit 9ff3ac3
Author: Elias Batek
Date:   Sun Nov 3 07:50:35 2024 +0100

    Fix return type typo

commit 4b68416
Author: Elias Batek
Date:   Sun Nov 3 07:49:48 2024 +0100

    Add validation

commit 3d22244
Author: Elias Batek
Date:   Sun Nov 3 07:34:21 2024 +0100

    Remove no longer necessary cast

commit a8ac073
Author: Elias Batek
Date:   Sun Nov 3 07:30:41 2024 +0100

    Improve docs

commit 4efb47b
Author: Elias Batek
Date:   Sun Nov 3 07:27:54 2024 +0100

    Cleanup API

commit d1a3fd0
Author: Elias Batek
Date:   Sun Nov 3 07:15:42 2024 +0100

    Add constraints

commit 5fb9097
Author: Elias Batek
Date:   Sun Nov 3 07:05:17 2024 +0100

    Improve docs

commit 100aa5f
Author: Elias Batek
Date:   Sun Nov 3 06:46:32 2024 +0100

    Refactor

commit 13551c1
Author: Elias Batek
Date:   Sun Nov 3 06:37:33 2024 +0100

    Fix out-of-bounds bug

commit cca538f
Author: Elias Batek
Date:   Sun Nov 3 06:32:52 2024 +0100

    Fix accidental conversion of faulty hex digits

commit c4a367d
Author: Elias Batek
Date:   Sun Nov 3 06:19:19 2024 +0100

    Refactor

commit 538ea23
Author: Elias Batek
Date:   Sun Nov 3 06:15:21 2024 +0100

    Add changelog

commit cb1b131
Author: Elias Batek
Date:   Sun Nov 3 05:17:55 2024 +0100

    Add fromHexString

    Squashed commit of the following:

    commit bc159e9b44c89e68b66dfb3c54bd781229dffa46
    Author: Tynuk <[email protected]>
    Date:   Sat Jan 27 12:14:52 2024 +0200

        update style

    commit 1fc719532fb83b764c95701973a5fddc98520a15
    Author: Tynuk <[email protected]>
    Date:   Sat Jan 27 11:52:19 2024 +0200

        create ByteRange

    commit 27ea722f04d9b0120aff2a405935530dd0174950
    Author: Tynuk <[email protected]>
    Date:   Thu Aug 25 16:19:45 2022 +0300

        rm spaces

    commit 1fdf63ab4baafc1c4d6d9e957767fe64716365ef
    Author: Tynuk <[email protected]>
    Date:   Thu Aug 25 15:55:40 2022 +0300

        Rename

    commit 4b37b8d40d1c251b35e3c14ec64e3b15a388e158
    Author: Tynuk <[email protected]>
    Date:   Thu Aug 25 15:26:10 2022 +0300

        Update std/digest/package.d

    commit 712eb2504371c63bee582fb53b5d2bbae9e6f525
    Author: Tynuk <[email protected]>
    Date:   Thu Aug 25 15:25:51 2022 +0300

        Update std/digest/package.d

    commit 4826317a5d9d1da7ef8cd4b6df87237d8d254bff
    Author: Tynuk <[email protected]>
    Date:   Thu Aug 25 15:25:43 2022 +0300

        Update std/digest/package.d

    commit aedb66bd1ab8bdd1f5acc54f5f63f2c9d2b33b05
    Author: tynuk <[email protected]>
    Date:   Mon Aug 8 15:15:42 2022 +0300

        add toDigest / hexToBytes

Co-authored-by: Elias Batek <[email protected]>
  • Loading branch information
2 people authored and thewilsonator committed Nov 3, 2024
1 parent 2a730ad commit 23ea6b0
Show file tree
Hide file tree
Showing 2 changed files with 324 additions and 0 deletions.
15 changes: 15 additions & 0 deletions changelog/fromhexstring.dd
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Added fromHexString and fromHexStringAsRange functions to std.digest.

This new function enables the converion of a hex string to a range of bytes.
Unlike the template $(REF hexString, std, conv) that was designed to supersede
a language feature, this function is usable with runtime input.

The `std.conv` module lacks facilities to conveniently transform the input
to a series of bytes directly. Both $(REF parse, std, conv) and $(REF to, std,
conv) can only handle the conversion for a single value of the requested target
integer type. Furthermore, said functions would allocate a new buffer for the
result, while `fromHexStringAsRange` operates lazily by implementing a forward
range.

For further convenience, a validation function $(REF isHexString, std, digest)
was added as well.
309 changes: 309 additions & 0 deletions std/digest/package.d
Original file line number Diff line number Diff line change
Expand Up @@ -1212,3 +1212,312 @@ if (isInputRange!R1 && isInputRange!R2 && !isInfinite!R1 && !isInfinite!R2 &&
assert(!secureEqual(hex1, hex2));
}
}

/**
* Validates a hex string.
*
* Checks whether all characters following an optional "0x" suffix
* are valid hexadecimal digits.
*
* Params:
* hex = hexdecimal encoded byte array
* Returns:
* true = if valid
*/
bool isHexString(String)(String hex) @safe pure nothrow @nogc
if (isSomeString!String)
{
import std.ascii : isHexDigit;

if ((hex.length >= 2) && (hex[0 .. 2] == "0x"))
{
hex = hex[2 .. $];
}

foreach (digit; hex)
{
if (!digit.isHexDigit)
{
return false;
}
}

return true;
}

///
@safe unittest
{
assert(isHexString("0x0123456789ABCDEFabcdef"));
assert(isHexString("0123456789ABCDEFabcdef"));
assert(!isHexString("g"));
assert(!isHexString("#"));
}

/**
* Converts a hex text string to a range of bytes.
*
* The input to this function MUST be valid.
* $(REF isHexString, std, digest) can be used to check for this if needed.
*
* Params:
* hex = String representation of a hexdecimal-encoded byte array.
* Returns:
* A forward range of bytes.
*/
auto fromHexStringAsRange(String)(String hex) @safe pure nothrow @nogc
if (isSomeString!String)
{
return HexStringDecoder!String(hex);
}

///
@safe unittest
{
import std.range.primitives : ElementType, isForwardRange;
import std.traits : ReturnType;

// The decoder implements a forward range.
static assert(isForwardRange!(ReturnType!(fromHexStringAsRange!string)));
static assert(isForwardRange!(ReturnType!(fromHexStringAsRange!wstring)));
static assert(isForwardRange!(ReturnType!(fromHexStringAsRange!dstring)));

// The element type of the range is always `ubyte`.
static assert(
is(ElementType!(ReturnType!(fromHexStringAsRange!string)) == ubyte)
);
static assert(
is(ElementType!(ReturnType!(fromHexStringAsRange!wstring)) == ubyte)
);
static assert(
is(ElementType!(ReturnType!(fromHexStringAsRange!dstring)) == ubyte)
);
}

@safe unittest
{
import std.array : staticArray;

// `staticArray` consumes the range returned by `fromHexStringAsRange`.
assert("0x0000ff".fromHexStringAsRange.staticArray!3 == [0, 0, 0xFF]);
assert("0x0000ff"w.fromHexStringAsRange.staticArray!3 == [0, 0, 0xFF]);
assert("0x0000ff"d.fromHexStringAsRange.staticArray!3 == [0, 0, 0xFF]);
assert("0xff12ff".fromHexStringAsRange.staticArray!1 == [0xFF]);
assert("0x12ff".fromHexStringAsRange.staticArray!2 == [0x12, 255]);
assert(
"0x3AaAA".fromHexStringAsRange.staticArray!4 == [0x3, 0xAA, 0xAA, 0x00]
);
}

/**
* Converts a hex text string to a range of bytes.
*
* Params:
* hex = String representation of a hexdecimal-encoded byte array.
* Returns:
* An newly allocated array of bytes.
* Throws:
* Exception on invalid input.
* Example:
* ---
* ubyte[] dby = "0xBA".fromHexString;
* ---
* See_Also:
* $(REF fromHexString, std, digest) for a range version of the function.
*/
ubyte[] fromHexString(String)(String hex) @safe pure
if (isSomeString!String)
{
// This function is trivial, yet necessary for consistency.
// It provides a similar API to its `toHexString` counterpart.
import std.array : array;
import std.conv : text;

if (!hex.isHexString)
{
throw new Exception(
"The provided character sequence `"
~ hex.text
~ "` is not a valid hex string."
);
}

return HexStringDecoder!String(hex).array;
}

///
@safe unittest
{
// Single byte
assert("0xff".fromHexString == [255]);
assert("0xff"w.fromHexString == [255]);
assert("0xff"d.fromHexString == [255]);
assert("0xC0".fromHexString == [192]);
assert("0x00".fromHexString == [0]);

// Nothing
assert("".fromHexString == []);
assert(""w.fromHexString == []);
assert(""d.fromHexString == []);

// Nothing but a prefix
assert("0x".fromHexString == []);
assert("0x"w.fromHexString == []);
assert("0x"d.fromHexString == []);

// Half a byte
assert("0x1".fromHexString == [0x01]);
assert("0x1"w.fromHexString == [0x01]);
assert("0x1"d.fromHexString == [0x01]);

// Mixed case is fine.
assert("0xAf".fromHexString == [0xAF]);
assert("0xaF".fromHexString == [0xAF]);

// Multiple bytes
assert("0xfff".fromHexString == [0x0F, 0xFF]);
assert("0x123AaAa".fromHexString == [0x01, 0x23, 0xAA, 0xAA]);
assert("EBBBBF".fromHexString == [0xEB, 0xBB, 0xBF]);

// md5 sum
assert("d41d8cd98f00b204e9800998ecf8427e".fromHexString == [
0xD4, 0x1D, 0x8C, 0xD9, 0x8F, 0x00, 0xB2, 0x04,
0xE9, 0x80, 0x09, 0x98, 0xEC, 0xF8, 0x42, 0x7E,
]);
}

///
@safe unittest
{
// Cycle self-test
const ubyte[] initial = [0x00, 0x12, 0x34, 0xEB];
assert(initial == initial.toHexString().fromHexString());
}

private ubyte hexDigitToByte(dchar hexDigit) @safe pure nothrow @nogc
{
static int hexDigitToByteImpl(dchar hexDigit)
{
if (hexDigit >= '0' && hexDigit <= '9')
{
return hexDigit - '0';
}
else if (hexDigit >= 'A' && hexDigit <= 'F')
{
return hexDigit - 'A' + 10;
}
else if (hexDigit >= 'a' && hexDigit <= 'f')
{
return hexDigit - 'a' + 10;
}

assert(false, "Cannot convert invalid hex digit.");
}

return hexDigitToByteImpl(hexDigit) & 0xFF;
}

@safe unittest
{
assert(hexDigitToByte('0') == 0x0);
assert(hexDigitToByte('9') == 0x9);
assert(hexDigitToByte('a') == 0xA);
assert(hexDigitToByte('b') == 0xB);
assert(hexDigitToByte('A') == 0xA);
assert(hexDigitToByte('C') == 0xC);
}

private struct HexStringDecoder(String)
if (isSomeString!String)
{
String hex;
ubyte front;
bool empty;

this(String hex)
{
if ((hex.length >= 2) && (hex[0 .. 2] == "0x"))
{
hex = hex[2 .. $];
}

if (hex.length == 0)
{
empty = true;
return;
}

const oddInputLength = (hex.length % 2 == 1);

if (oddInputLength)
{
front = hexDigitToByte(hex[0]);
hex = hex[1 .. $];
}
else
{
front = cast(ubyte)(hexDigitToByte(hex[0]) << 4 | hexDigitToByte(hex[1]));
hex = hex[2 .. $];
}

this.hex = hex;
}

void popFront()
{
if (hex.length == 0)
{
empty = true;
return;
}

front = cast(ubyte)(hexDigitToByte(hex[0]) << 4 | hexDigitToByte(hex[1]));
hex = hex[2 .. $];
}

typeof(this) save()
{
return this;
}
}

@safe unittest
{
auto decoder = HexStringDecoder!string("");
assert(decoder.empty);

decoder = HexStringDecoder!string("0x");
assert(decoder.empty);
}

@safe unittest
{
auto decoder = HexStringDecoder!string("0x0077FF");
assert(!decoder.empty);
assert(decoder.front == 0x00);

decoder.popFront();
assert(!decoder.empty);
assert(decoder.front == 0x77);

decoder.popFront();
assert(!decoder.empty);
assert(decoder.front == 0xFF);

decoder.popFront();
assert(decoder.empty);
}

@safe unittest
{
auto decoder = HexStringDecoder!string("0x7FF");
assert(!decoder.empty);
assert(decoder.front == 0x07);

decoder.popFront();
assert(!decoder.empty);
assert(decoder.front == 0xFF);

decoder.popFront();
assert(decoder.empty);
}

0 comments on commit 23ea6b0

Please sign in to comment.