From 7a7cf783a49229063175710d18385cb357b19b92 Mon Sep 17 00:00:00 2001 From: JalonSolov Date: Sun, 7 Jun 2026 19:16:36 -0400 Subject: [PATCH] encoding.hex: cleaner code, options for encode (#27378) --- .../internal/edwards25519/extra_test.v | 4 +- .../internal/edwards25519/point_test.v | 6 +- .../internal/edwards25519/scalar_test.v | 15 +-- vlib/encoding/hex/hex.v | 117 ++++++++++++------ vlib/encoding/hex/hex_test.v | 23 +++- 5 files changed, 112 insertions(+), 53 deletions(-) diff --git a/vlib/crypto/ed25519/internal/edwards25519/extra_test.v b/vlib/crypto/ed25519/internal/edwards25519/extra_test.v index 753da166e..2111df5cf 100644 --- a/vlib/crypto/ed25519/internal/edwards25519/extra_test.v +++ b/vlib/crypto/ed25519/internal/edwards25519/extra_test.v @@ -66,14 +66,14 @@ fn test_bytes_montgomery_infinity() { } const loworder_string = '26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc85' -const loworder_bytes = hex.decode(loworder_string) or { panic(err) } +const loworder_bytes = hex.decode(loworder_string) or { []u8{} } fn fn_cofactor(mut data []u8) bool { if data.len != 64 { panic('data.len should be 64') } mut loworder := Point{} - loworder.set_bytes(loworder_bytes) or { panic(err) } + loworder.set_bytes(loworder_bytes) or { return false } mut s := new_scalar() mut p := Point{} diff --git a/vlib/crypto/ed25519/internal/edwards25519/point_test.v b/vlib/crypto/ed25519/internal/edwards25519/point_test.v index 115d1e3bc..cc74fa875 100644 --- a/vlib/crypto/ed25519/internal/edwards25519/point_test.v +++ b/vlib/crypto/ed25519/internal/edwards25519/point_test.v @@ -7,7 +7,7 @@ const zero_point = Point{fe_zero, fe_zero, fe_zero, fe_zero} fn test_invalid_encodings() { // An invalid point, that also happens to have y > p. invalid := 'efffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f' - inv_bytes := hex.decode(invalid) or { panic(err) } + inv_bytes := hex.decode(invalid) or { []u8{} } mut p := new_generator_point() out := p.set_bytes(inv_bytes) or { zero_point } @@ -96,11 +96,11 @@ fn test_non_canonical_points() { // t.Run(tt.name, func(t *testing.T) { // p1, err := new(Point).SetBytes(decodeHex(tt.encoding)) mut p1 := Point{} - p1.set_bytes(hex.decode(tt.encoding)!)! + p1.set_bytes(hex.decode(tt.encoding) or { []u8{} })! // p2, err := new(Point).SetBytes(decodeHex(tt.canonical)) mut p2 := Point{} - p2.set_bytes(hex.decode(tt.canonical)!)! + p2.set_bytes(hex.decode(tt.canonical) or { []u8{} })! assert p1.equal(p2) == 1 assert p1.bytes() == p2.bytes() diff --git a/vlib/crypto/ed25519/internal/edwards25519/scalar_test.v b/vlib/crypto/ed25519/internal/edwards25519/scalar_test.v index 566ca8b6b..d850e20f7 100644 --- a/vlib/crypto/ed25519/internal/edwards25519/scalar_test.v +++ b/vlib/crypto/ed25519/internal/edwards25519/scalar_test.v @@ -20,6 +20,7 @@ fn test_scalar_equal() { assert sc_minus_one.equal(sc_minus_one) != 0 } +@[direct_array_access] fn test_scalar_non_adjacent_form() { mut s := Scalar{ s: [u8(0x1a), 0x0e, 0x97, 0x8a, 0x90, 0xf6, 0x62, 0x2d, 0x37, 0x47, 0x02, 0x3f, 0x8a, 0xd8, @@ -35,7 +36,7 @@ fn test_scalar_non_adjacent_form() { -15, 0, 0, 0, 0, 1, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 15, 0, 0, 0, 0, 15, 0, - 0, 0, 0, 0, 1, 0, 0, 0, 0] + 0, 0, 0, 0, 1, 0, 0, 0, 0]! snaf := s.non_adjacent_form(5) for i := 0; i < 256; i++ { @@ -151,10 +152,10 @@ fn test_scalar_set_bytes_with_clamping() { t.Errorf("random: got %q, want %q", got, want) }*/ random := '633d368491364dc9cd4c1bf891b1d59460face1644813240a313e61f2c88216e' - random_bytes := hex.decode(random) or { panic(err) } + random_bytes := hex.decode(random) or { []u8{} } mut s0 := Scalar{} - s0.set_bytes_with_clamping(random_bytes) or { panic(err) } + s0.set_bytes_with_clamping(random_bytes) or { Scalar{} } mut p0 := Point{} p0.scalar_base_mult(mut s0) @@ -166,8 +167,8 @@ fn test_scalar_set_bytes_with_clamping() { zero := '0000000000000000000000000000000000000000000000000000000000000000' mut s1 := Scalar{} - zero_bytes := hex.decode(zero) or { panic(err) } - s1.set_bytes_with_clamping(zero_bytes) or { panic(err) } + zero_bytes := hex.decode(zero) or { []u8{} } + s1.set_bytes_with_clamping(zero_bytes) or { Scalar{} } mut p1 := Point{} p1.scalar_base_mult(mut s1) @@ -177,8 +178,8 @@ fn test_scalar_set_bytes_with_clamping() { one := 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' mut s2 := Scalar{} - mut one_bytes := hex.decode(one) or { panic(err) } - s2.set_bytes_with_clamping(one_bytes) or { panic(err) } + mut one_bytes := hex.decode(one) or { []u8{} } + s2.set_bytes_with_clamping(one_bytes) or { Scalar{} } mut p2 := Point{} p2.scalar_base_mult(mut s2) diff --git a/vlib/encoding/hex/hex.v b/vlib/encoding/hex/hex.v index 0f2a69721..0ea914b30 100644 --- a/vlib/encoding/hex/hex.v +++ b/vlib/encoding/hex/hex.v @@ -1,62 +1,99 @@ module hex -import strings +const hex_digits = '0123456789abcdef' +const hex_digits_upper = '0123456789ABCDEF' -// decode converts a hex string into an array of bytes. The expected -// input format is 2 ASCII characters for each output byte. If the provided -// string length is not a multiple of 2, an implicit `0` is prepended to it. +// EncodeParams configures optional output formatting for encode. +// +// If `uppercase` is set, `encode` emits `A-F` instead of `a-f`. +// If `with_prefix` is non-empty, `encode` prepends that exact string. +@[params] +pub struct EncodeParams { +pub mut: + uppercase bool + with_prefix string +} + +// decode converts a hex string into an array of bytes. +// The expected input format is 2 ASCII characters for each output byte. +// If the provided string length is not a multiple of 2, the first digit is +// decoded as if an implicit `0` preceded it. An optional `0x` or `0X` prefix +// is accepted. +@[direct_array_access] pub fn decode(s string) ![]u8 { - mut hex_str := s - if hex_str.len >= 2 { + if s.len == 0 { + return []u8{} + } + + mut offset := 0 + mut hex_bytes := if s.len >= 2 { if s[0] == `0` && (s[1] == `x` || s[1] == `X`) { - hex_str = s[2..] + offset = 2 + s[2..].bytes() + } else { + s.bytes() } + } else { + s.bytes() } - if hex_str.len == 0 { + + if hex_bytes.len == 0 { return []u8{} - } else if hex_str.len == 1 { - return [char2nibble(hex_str[0])!] - } else if hex_str.len == 2 { - n1 := char2nibble(hex_str[0])! - n0 := char2nibble(hex_str[1])! - return [(n1 << 4) | n0] - } - // calculate the first byte depending on if hex_str.len is odd - mut val := char2nibble(hex_str[0])! - if hex_str.len & 1 == 0 { - val = (val << 4) | char2nibble(hex_str[1])! - } - // set cap to hex_str.len/2 rounded up - mut bytes := []u8{len: 1, cap: (hex_str.len + 1) >> 1, init: val} - // iterate over every 2 bytes - // the start index depends on if hex_str.len is odd - for i := 2 - (hex_str.len & 1); i < hex_str.len; i += 2 { - n1 := char2nibble(hex_str[i])! - n0 := char2nibble(hex_str[i + 1])! + } + + mut bytes := []u8{cap: (hex_bytes.len + 1) >> 1} + mut start := 0 + if hex_bytes.len & 1 == 1 { + bytes << char2nibble(hex_bytes[0], offset)! + start = 1 + } + + for i := start; i < hex_bytes.len; i += 2 { + n1 := char2nibble(hex_bytes[i], offset + i)! + n0 := char2nibble(hex_bytes[i + 1], offset + i + 1)! bytes << (n1 << 4) | n0 } return bytes } // encode converts an array of bytes into a string of ASCII hex bytes. The -// output will always be a string with length a multiple of 2. -@[manualfree] -pub fn encode(bytes []u8) string { - mut sb := strings.new_builder(bytes.len * 2) - for b in bytes { - sb.write_string(b.hex()) - } - res := sb.str() - unsafe { sb.free() } - return res +// output will always be a string whose length will be a multiple of 2. +// If `EncodeParams.uppercase` is set, the output hex characters are emitted in +// uppercase. +// If `EncodeParams.with_prefix` is non-empty, the output string is prefixed +// with the provided string. +@[direct_array_access] +pub fn encode(bytes []u8, params EncodeParams) string { + if bytes.len == 0 { + return '' + } + mut res := []u8{} + if params.with_prefix != '' { + res << params.with_prefix.bytes() + } + for _, b in bytes { + res << nibble2char(b >> 4, params) + res << nibble2char(b & 0xf, params) + } + return res.bytestr() +} + +// nibble2char converts a 4-bit hex value to its ASCII character +@[inline] +fn nibble2char(nibble u8, params EncodeParams) u8 { + if params.uppercase { + return hex_digits_upper[nibble] + } + return hex_digits[nibble] } -// char2nibble converts an ASCII hex character to it's hex value -fn char2nibble(b u8) !u8 { +// char2nibble converts an ASCII hex character to its hex value +@[inline] +fn char2nibble(b u8, index int) !u8 { match b { `0`...`9` { return b - u8(`0`) } `A`...`F` { return b - u8(`A`) + 10 } `a`...`f` { return b - u8(`a`) + 10 } - else { return error('invalid hex char ${b.ascii_str()}') } + else { return error('invalid hex char ${b.ascii_str()} at index ${index}') } } } diff --git a/vlib/encoding/hex/hex_test.v b/vlib/encoding/hex/hex_test.v index b33240f97..f9bcdbbf0 100644 --- a/vlib/encoding/hex/hex_test.v +++ b/vlib/encoding/hex/hex_test.v @@ -44,11 +44,32 @@ fn test_encode() { assert encode(decode('ABCDEF')!) == 'abcdef' } +fn test_encode_params() { + assert encode([u8(0xab), 0xcd], uppercase: true) == 'ABCD' + assert encode([u8(0xab), 0xcd], with_prefix: '0x') == '0xabcd' + assert encode([u8(0xab), 0xcd], uppercase: true, with_prefix: '0X') == '0XABCD' + assert encode([u8(0xab), 0xcd], with_prefix: 'hex:') == 'hex:abcd' +} + fn test_decode_0x() { - assert decode('0x')! == [] + assert decode('0x') or { []u8{} } == []u8{} assert decode('0x0')! == [u8(0x0)] assert decode('0X1234')! == [u8(0x12), 0x34] assert decode('0x12345')! == [u8(0x1), 0x23, 0x45] assert decode('0x0123456789abcdef')! == [u8(0x01), 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef] assert decode('0X123456789ABCDEF')! == [u8(0x01), 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef] } + +fn test_decode_error_indexes() { + decode('g') or { + assert err.msg() == 'invalid hex char g at index 0' + return + } + assert false + + decode('0xg') or { + assert err.msg() == 'invalid hex char g at index 2' + return + } + assert false +} -- 2.39.5