From 4e0a71c24c8a087ba71150b1cdb4a3c4e5a4e5d1 Mon Sep 17 00:00:00 2001 From: mo Date: Sun, 16 Aug 2026 19:48:19 +1200 Subject: [PATCH] Add non-allocating hex.decode_into_buffer Also minor fixes to documentation of hex.decode(). Adds tests of the new procedure with buffers the correct size, larger, and too small. --- core/encoding/hex/hex.odin | 38 ++++++++++++++- tests/core/encoding/hex/test_core_hex.odin | 56 +++++++++++++++++++++- 2 files changed, 91 insertions(+), 3 deletions(-) diff --git a/core/encoding/hex/hex.odin b/core/encoding/hex/hex.odin index c4726d9e9..f7a39a081 100644 --- a/core/encoding/hex/hex.odin +++ b/core/encoding/hex/hex.odin @@ -95,12 +95,12 @@ Decodes a hex sequence into a byte slice *Allocates Using Provided Allocator* Inputs: -- dst: The hex sequence decoded into bytes - src: The `[]byte` to be hex-decoded - allocator: (default: context.allocator) - loc: The caller location for debugging purposes (default: #caller_location) Returns: +- dst: The hex sequence decoded into bytes - ok: A bool, `true` if decoding succeeded, `false` otherwise */ decode :: proc(src: []byte, allocator := context.allocator, loc := #caller_location) -> (dst: []byte, ok: bool) { @@ -123,6 +123,40 @@ decode :: proc(src: []byte, allocator := context.allocator, loc := #caller_locat return dst, true } +/* +Decodes a hex sequence into a byte slice + +Inputs: +- src: The `[]byte` to be hex-decoded +- buf: A buffer large enough to hold the decoded sequence + +Returns: +- dst: The hex sequence decoded into bytes +- ok: A bool, `true` if decoding succeeded, `false` otherwise +*/ +decode_into_buffer :: proc(src: []byte, buf: []byte) -> (dst: []byte, ok: bool) #optional_ok { + if len(src) % 2 == 1 { + return + } + dst_len := len(src) / 2 + if len(buf) < dst_len { + return + } + + #no_bounds_check for i, j := 0, 1; j < len(src); j += 2 { + p := src[j-1] + q := src[j] + + a := hex_digit(p) or_return + b := hex_digit(q) or_return + + buf[i] = (a << 4) | b + i += 1 + } + + return buf[:dst_len], true +} + /* Decodes the first byte in a hex sequence to a byte @@ -173,4 +207,4 @@ hex_digit :: proc(char: byte) -> (u8, bool) { case 'A' ..= 'F': return char - 'A' + 10, true case: return 0, false } -} \ No newline at end of file +} diff --git a/tests/core/encoding/hex/test_core_hex.odin b/tests/core/encoding/hex/test_core_hex.odin index 6a00c9705..fdb3a2734 100644 --- a/tests/core/encoding/hex/test_core_hex.odin +++ b/tests/core/encoding/hex/test_core_hex.odin @@ -47,6 +47,60 @@ hex_decode :: proc(t: ^testing.T) { } } +@(test) +hex_decode_into_buffer :: proc(t: ^testing.T) { + for test in CASES { + buffer := make([]u8, len(test[1]) / 2) + defer delete(buffer) + decoded, ok := hex.decode_into_buffer(transmute([]byte)test[1], buffer) + testing.expect(t, ok, "decode_into_buffer: not ok") + testing.expectf( + t, + ok, + "decode: %q not ok", + test[1], + ) + testing.expectf( + t, + string(decoded) == test[0], + "decode: %q -> %q (should be: %q)", + test[1], + string(decoded), + test[0], + ) + } + + // destination buffer is larger + for test in CASES { + buffer := make([]u8, len(test[1]) / 2 + 1) + defer delete(buffer) + decoded, ok := hex.decode_into_buffer(transmute([]byte)test[1], buffer) + testing.expect(t, ok, "decode_into_buffer: not ok") + testing.expectf( + t, + ok, + "decode: %q not ok", + test[1], + ) + testing.expectf( + t, + string(decoded) == test[0], + "decode: %q -> %q (should be: %q)", + test[1], + string(decoded), + test[0], + ) + } + + // destination buffer is too small + for test in CASES { + buffer := make([]u8, len(test[1]) / 2 - 1) + defer delete(buffer) + _, ok := hex.decode_into_buffer(transmute([]byte)test[1], buffer) + testing.expect(t, !ok, "decode_into_buffer: should not be ok") + } +} + @(test) hex_decode_sequence :: proc(t: ^testing.T) { b, ok := hex.decode_sequence("0x23") @@ -83,4 +137,4 @@ hex_decode_sequence :: proc(t: ^testing.T) { _, ok = hex.decode_sequence("123") testing.expect(t, !ok, "decode_sequence: 123 should be too long") -} \ No newline at end of file +}