diff --git a/types/primitives/util.go b/types/primitives/util.go index 86cf204..6661a30 100644 --- a/types/primitives/util.go +++ b/types/primitives/util.go @@ -6,6 +6,26 @@ import ( var hextable = "0123456789abcdef" +const nonHexMarker = 100 + +var reverseHexTable [256]byte + +func init() { + // Pre-fill the reverseHexTable + for i := 0; i < len(reverseHexTable); i++ { + switch { + case i >= '0' && i <= '9': + reverseHexTable[i] = byte(i) - '0' + case i >= 'a' && i <= 'f': + reverseHexTable[i] = byte(i) - 'a' + 10 + case i >= 'A' && i <= 'F': + reverseHexTable[i] = byte(i) - 'A' + 10 + default: + reverseHexTable[i] = nonHexMarker + } + } +} + func alignBytes(b []byte, length int, bigEndian bool) []byte { if length < 0 { if b == nil { @@ -72,8 +92,22 @@ func hexToBytes(s string) ([]byte, error) { if len(s) >= 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X') { s = s[2:] } - if len(s)%2 == 1 { - s = "0" + s + + if len(s)%2 == 0 { + return hex.DecodeString(s) } - return hex.DecodeString(s) + + dst := make([]byte, hex.DecodedLen(1+len(s))) + + if reverseHexTable[s[0]] == nonHexMarker { + return nil, hex.InvalidByteError(s[0]) + } + dst[0] = reverseHexTable[s[0]] + + // Allocation-free string -> []byte conversion there (compiler will optimize it out) + if _, err := hex.Decode(dst[1:], []byte(s[1:])); err != nil { + return nil, err + } + + return dst, nil } diff --git a/types/primitives/util_test.go b/types/primitives/util_test.go index 0430493..2b54ee0 100644 --- a/types/primitives/util_test.go +++ b/types/primitives/util_test.go @@ -27,3 +27,96 @@ func TestBytesToHex(t *testing.T) { assert.Equal(t, "0x0000000010", string(writeDataHex(nil, []byte{0, 0, 0, 0, 16}))) assert.Equal(t, "0x10", string(writeQuantityHex(nil, []byte{0, 0, 0, 0, 16}))) } + +func TestHexToBytes(t *testing.T) { + testCases := []struct { + name string + in string + out []byte + err bool + }{{ + name: "empty", + in: "", + out: []byte{}, + }, { + name: "empty with prefix", + in: "0x", + out: []byte{}, + }, { + name: "zero without prefix", + in: "00", + out: []byte{0x00}, + }, { + name: "even length without prefix", + in: "1a", + out: []byte{0x1a}, + }, { + name: "uppercase without prefix", + in: "FC", + out: []byte{0xfc}, + }, { + name: "longer, without prefix", + in: "0a1b2c", + out: []byte{0x0a, 0x1b, 0x2c}, + }, { + name: "zero with prefix", + in: "0x00", + out: []byte{0x00}, + }, { + name: "even length with prefix", + in: "0x1a", + out: []byte{0x1a}, + }, { + name: "uppercase with prefix", + in: "0XFC", + out: []byte{0xfc}, + }, { + name: "zero with odd length", + in: "0x0", + out: []byte{0x00}, + }, { + name: "odd length without prefix", + in: "a", + out: []byte{0x0a}, + }, { + name: "uppercase with odd length", + in: "0XFC1", + out: []byte{0x0f, 0xc1}, + }, { + name: "longer, with odd length", + in: "0xa1b2c", + out: []byte{0x0a, 0x1b, 0x2c}, + }, { + name: "only invalid characters", + in: "0xgg", + err: true, + }, { + name: "only invalid characters without prefix", + in: "gg", + err: true, + }, { + name: "mixed invalid characters, with odd length", + in: "0x1b0gg", + err: true, + }, { + name: "with whitespace", + in: "0x1 a", + err: true, + }, { + name: "non-ascii characters", + in: "0x(╯°□°)╯ ┻━┻", + err: true, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + res, err := hexToBytes(tc.in) + if tc.err { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.out, res) + } + }) + } +}