Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(primitives): Dropped hex.String #2048

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion mod/primitives/pkg/bytes/b.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ func (b Bytes) MarshalText() ([]byte, error) {

// UnmarshalJSON implements json.Unmarshaler.
func (b *Bytes) UnmarshalJSON(input []byte) error {
return hex.UnmarshalJSONText(input, b)
strippedInput, err := hex.ValidateQuotedString(input)
if err != nil {
return err
}
return b.UnmarshalText(strippedInput)
}

// UnmarshalText implements encoding.TextUnmarshaler.
Expand Down
43 changes: 42 additions & 1 deletion mod/primitives/pkg/bytes/b_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,47 @@ func TestMustFromHex(t *testing.T) {
}
}

func TestBytesUnmarshalJSONText(t *testing.T) {
tests := []struct {
name string
input []byte
expectErr bool
}{
{
name: "Valid JSON text",
input: []byte(`"0x48656c6c6f"`),
expectErr: false,
},
{
name: "Invalid JSON text",
input: []byte(`"invalid"`),
expectErr: true,
},
{
name: "Invalid quoted JSON text",
input: []byte(`"0x`),
expectErr: true,
},
{
name: "Empty JSON text",
input: []byte(`""`),
expectErr: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b := &bytes.Bytes{}
err := b.UnmarshalJSON(tt.input)
if tt.expectErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
abi87 marked this conversation as resolved.
Show resolved Hide resolved

func TestReverseEndianness(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -873,7 +914,7 @@ func TestToBytes48(t *testing.T) {
}
}

func TestUnmarshalJSON(t *testing.T) {
func TestBytes48UnmarshalJSON(t *testing.T) {
tests := []struct {
name string
input string
Expand Down
81 changes: 81 additions & 0 deletions mod/primitives/pkg/encoding/hex/big_int.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// SPDX-License-Identifier: BUSL-1.1
//
// Copyright (C) 2024, Berachain Foundation. All rights reserved.
// Use of this software is governed by the Business Source License included
// in the LICENSE file of this repository and at www.mariadb.com/bsl11.
//
// ANY USE OF THE LICENSED WORK IN VIOLATION OF THIS LICENSE WILL AUTOMATICALLY
// TERMINATE YOUR RIGHTS UNDER THIS LICENSE FOR THE CURRENT AND ALL OTHER
// VERSIONS OF THE LICENSED WORK.
//
// THIS LICENSE DOES NOT GRANT YOU ANY RIGHT IN ANY TRADEMARK OR LOGO OF
// LICENSOR OR ITS AFFILIATES (PROVIDED THAT YOU MAY USE A TRADEMARK OR LOGO OF
// LICENSOR AS EXPRESSLY REQUIRED BY THIS LICENSE).
//
// TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
// AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
// EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
// TITLE.

package hex

import "math/big"

// FromBigInt encodes bigint as a hex string with 0x prefix.
// Precondition: bigint is non-negative.
func FromBigInt(bigint *big.Int) string {
switch sign := bigint.Sign(); {
case sign == 0:
return prefix + "0"
case sign > 0:
return prefix + bigint.Text(hexBase)
default:
// this return should never reach if precondition is met
return prefix + bigint.Text(hexBase)[1:]
}
}
abi87 marked this conversation as resolved.
Show resolved Hide resolved

// ToBigInt decodes a hex string with 0x prefix.
func ToBigInt(hexStr string) (*big.Int, error) {
raw, err := formatAndValidateNumber(hexStr)
if err != nil {
return nil, err
}
if len(raw) > nibblesPer256Bits {
return nil, ErrBig256Range
}
bigWordNibbles, err := getBigWordNibbles()
if err != nil {
return nil, err
}
words := make([]big.Word, len(raw)/bigWordNibbles+1)
end := len(raw)
for i := range words {
start := end - bigWordNibbles
if start < 0 {
start = 0
}
for ri := start; ri < end; ri++ {
nib := decodeNibble(raw[ri])
if nib == badNibble {
return nil, ErrInvalidString
}
words[i] *= 16
words[i] += big.Word(nib)
}
end = start
}
dec := new(big.Int).SetBits(words)
return dec, nil
}
abi87 marked this conversation as resolved.
Show resolved Hide resolved

// MustToBigInt decodes a hex string with 0x prefix.
// It panics for invalid input.
func MustToBigInt(hexStr string) *big.Int {
bi, err := ToBigInt(hexStr)
if err != nil {
panic(err)
}
return bi
}
110 changes: 110 additions & 0 deletions mod/primitives/pkg/encoding/hex/bit_int_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// SPDX-License-Identifier: BUSL-1.1
//
// Copyright (C) 2024, Berachain Foundation. All rights reserved.
// Use of this software is governed by the Business Source License included
// in the LICENSE file of this repository and at www.mariadb.com/bsl11.
//
// ANY USE OF THE LICENSED WORK IN VIOLATION OF THIS LICENSE WILL AUTOMATICALLY
// TERMINATE YOUR RIGHTS UNDER THIS LICENSE FOR THE CURRENT AND ALL OTHER
// VERSIONS OF THE LICENSED WORK.
//
// THIS LICENSE DOES NOT GRANT YOU ANY RIGHT IN ANY TRADEMARK OR LOGO OF
// LICENSOR OR ITS AFFILIATES (PROVIDED THAT YOU MAY USE A TRADEMARK OR LOGO OF
// LICENSOR AS EXPRESSLY REQUIRED BY THIS LICENSE).
//
// TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
// AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
// EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
// TITLE.

//nolint:lll // long strings
package hex_test

import (
"bytes"
"math/big"
"testing"

"github.com/berachain/beacon-kit/mod/primitives/pkg/encoding/hex"
"github.com/stretchr/testify/require"
)

// FromBigInt, then ToBigInt.
func TestBigIntRoundTrip(t *testing.T) {
// assume FromBigInt only called on non-negative big.Int
tests := []struct {
name string
input *big.Int
expected string
}{
{
name: "zero value",
input: big.NewInt(0),
expected: "0x0",
},
{
name: "positive value",
input: big.NewInt(12345),
expected: "0x3039",
},
{
name: "large positive value",
input: new(big.Int).SetBytes(bytes.Repeat([]byte{0xff}, 32)),
expected: "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := hex.FromBigInt(tt.input)
require.Equal(t, tt.expected, result)

_, err := hex.IsValidHex(result)
require.NoError(t, err)

var dec *big.Int

if tt.input.Sign() >= 0 {
dec, err = hex.ToBigInt(result)
} else {
dec, err = hex.ToBigInt(result)
dec = dec.Neg(dec)
}

require.NoError(t, err)
require.Zero(t, dec.Cmp(tt.input))
})
}
}
abi87 marked this conversation as resolved.
Show resolved Hide resolved

func TestString_MustToBigInt(t *testing.T) {
tests := []struct {
name string
input string
expected *big.Int
panics bool
}{
{"Valid hex string", "0x1", big.NewInt(1), false},
{"Another valid hex string", "0x10", big.NewInt(16), false},
{"Large valid hex string", "0x1a", big.NewInt(26), false},
{"Invalid hex string", "0xinvalid", nil, true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var (
res *big.Int
f = func() {
res = hex.MustToBigInt(tt.input)
}
)
if tt.panics {
require.Panics(t, f)
} else {
require.NotPanics(t, f)
require.Equal(t, tt.expected, res)
}
})
}
}
abi87 marked this conversation as resolved.
Show resolved Hide resolved
7 changes: 4 additions & 3 deletions mod/primitives/pkg/encoding/hex/bytes.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,11 @@ func UnmarshalByteText(input []byte) ([]byte, error) {
// of out determines the required input length. This function is commonly used
// to implement the UnmarshalJSON method for fixed-size types.
func DecodeFixedJSON(input, out []byte) error {
if !isQuotedString(input) {
return ErrNonQuotedString
strippedInput, err := ValidateQuotedString(input)
if err != nil {
return err
}
return DecodeFixedText(input[1:len(input)-1], out)
return DecodeFixedText(strippedInput, out)
abi87 marked this conversation as resolved.
Show resolved Hide resolved
}

// DecodeFixedText decodes the input as a string with 0x prefix. The length
Expand Down
5 changes: 5 additions & 0 deletions mod/primitives/pkg/encoding/hex/bytes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ func TestEncodeAndDecodeBytes(t *testing.T) {
decoded, err := hex.ToBytes(result)
require.NoError(t, err)
require.Equal(t, tt.input, decoded)

require.NotPanics(t, func() {
decoded = hex.MustToBytes(result)
})
require.Equal(t, tt.input, decoded)
})
}
}
Expand Down
31 changes: 27 additions & 4 deletions mod/primitives/pkg/encoding/hex/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,34 @@

package hex

import "errors"
import (
"errors"
"strings"
)

// isQuotedString returns true if input has quotes.
func isQuotedString[T []byte | string](input T) bool {
return len(input) >= 2 && input[0] == '"' && input[len(input)-1] == '"'
// IsValidHex performs basic validations that every hex string
// must pass (there may be extra ones depending on the type encoded)
// It returns the suffix (dropping 0x prefix) in the hope to appease nilaway.
func IsValidHex[T ~[]byte | ~string](s T) (T, error) {
if len(s) == 0 {
return *new(T), ErrEmptyString
}
if len(s) < prefixLen {
return *new(T), ErrMissingPrefix
}
if strings.ToLower(string(s[:prefixLen])) != prefix {
return *new(T), ErrMissingPrefix
}
return s[prefixLen:], nil
}

// ValidateQuotedString errs if input has no quotes.
// For convenience it returns the unstrip content if it does not err.
func ValidateQuotedString(input []byte) ([]byte, error) {
if len(input) >= 2 && input[0] == '"' && input[len(input)-1] == '"' {
return input[1 : len(input)-1], nil
}
return nil, ErrNonQuotedString
}

// formatAndValidateText validates the input text for a hex string.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,56 @@
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
// TITLE.

package hex
package hex_test

import (
"encoding"
"testing"

"github.com/berachain/beacon-kit/mod/primitives/pkg/encoding/hex"
"github.com/stretchr/testify/require"
)

// UnmarshalJSONText unmarshals a JSON string with a 0x prefix into a given
// TextUnmarshaler. It validates the input and then removes the surrounding
// quotes before passing the inner content to the UnmarshalText method.
func UnmarshalJSONText(input []byte,
u encoding.TextUnmarshaler,
) error {
if err := ValidateUnmarshalInput(input); err != nil {
return err
func TestIsValidHex(t *testing.T) {
tests := []struct {
name string
input string
wantErr error
}{
{
name: "Valid hex string",
input: "0x48656c6c6f",
wantErr: nil,
},
{
name: "Empty string",
input: "",
wantErr: hex.ErrEmptyString,
},
{
name: "No 0x prefix",
input: "48656c6c6f",
wantErr: hex.ErrMissingPrefix,
},
{
name: "Valid single hex character",
input: "0x0",
wantErr: nil,
},
{
name: "Empty hex string",
input: "0x",
wantErr: nil,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := hex.IsValidHex(test.input)
if test.wantErr != nil {
require.ErrorIs(t, test.wantErr, err)
} else {
require.NoError(t, err)
}
})
abi87 marked this conversation as resolved.
Show resolved Hide resolved
}
return u.UnmarshalText(input[1 : len(input)-1])
}
Loading
Loading