diff --git a/common/hexutil/hexutil.libevm.go b/common/hexutil/hexutil.libevm.go new file mode 100644 index 00000000000..5dbd228e71e --- /dev/null +++ b/common/hexutil/hexutil.libevm.go @@ -0,0 +1,44 @@ +// Copyright 2025 the libevm authors. +// +// The libevm additions to go-ethereum are free software: you can redistribute +// them and/or modify them under the terms of the GNU Lesser General Public License +// as published by the Free Software Foundation, either version 3 of the License, +// or (at your option) any later version. +// +// The libevm additions are distributed in the hope that they will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +// General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see +// . + +package hexutil + +import "strconv" + +var ErrUint16Range = &decError{"hex number > 16 bits"} + +// DecodeUint16 decodes a hex string with 0x prefix as a quantity. +func DecodeUint16(input string) (uint16, error) { + raw, err := checkNumber(input) + if err != nil { + return 0, err + } + dec, err := strconv.ParseUint(raw, 16, 16) + if err != nil { + err = mapError(err) + if err == ErrUint64Range { + return 0, ErrUint16Range + } + } + return uint16(dec), err //nolint:gosec // G115 won't overflow uint16 as ParseUint uses 16 bits +} + +// EncodeUint16 encodes i as a hex string with 0x prefix. +func EncodeUint16(i uint16) string { + enc := make([]byte, 2, 6) + copy(enc, "0x") + return string(strconv.AppendUint(enc, uint64(i), 16)) +} diff --git a/common/hexutil/hexutil.libevm_test.go b/common/hexutil/hexutil.libevm_test.go new file mode 100644 index 00000000000..febfd8e9c5d --- /dev/null +++ b/common/hexutil/hexutil.libevm_test.go @@ -0,0 +1,76 @@ +// Copyright 2025 the libevm authors. +// +// The libevm additions to go-ethereum are free software: you can redistribute +// them and/or modify them under the terms of the GNU Lesser General Public License +// as published by the Free Software Foundation, either version 3 of the License, +// or (at your option) any later version. +// +// The libevm additions are distributed in the hope that they will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +// General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see +// . + +package hexutil + +import "testing" + +var ( + encodeUint16Tests = []marshalTest{ + {uint16(0), "0x0"}, + {uint16(1), "0x1"}, + {uint16(0xff), "0xff"}, + {uint16(0x1122), "0x1122"}, + } + + decodeUint16Tests = []unmarshalTest{ + // invalid + {input: `0`, wantErr: ErrMissingPrefix}, + {input: `0x`, wantErr: ErrEmptyNumber}, + {input: `0x01`, wantErr: ErrLeadingZero}, + {input: `0xfffff`, wantErr: ErrUint16Range}, + {input: `0xz1`, wantErr: ErrSyntax}, + // valid + {input: `0x0`, want: uint16(0)}, + {input: `0x2`, want: uint16(0x2)}, + {input: `0x2F2`, want: uint16(0x2f2)}, + {input: `0X2F2`, want: uint16(0x2f2)}, + {input: `0xff`, want: uint16(0xff)}, + {input: `0x12af`, want: uint16(0x12af)}, + {input: `0xbbb`, want: uint16(0xbbb)}, + {input: `0xffff`, want: uint16(0xffff)}, + } +) + +func TestEncodeUint16(t *testing.T) { + for _, test := range encodeUint16Tests { + input, ok := test.input.(uint16) + if !ok { + t.Errorf("input %v: not a uint16", test.input) + } + enc := EncodeUint16(input) + if enc != test.want { + t.Errorf("input %x: wrong encoding %s", test.input, enc) + } + } +} + +func TestDecodeUint16(t *testing.T) { + for _, test := range decodeUint16Tests { + dec, err := DecodeUint16(test.input) + if !checkError(t, test.input, err, test.wantErr) { + continue + } + want, ok := test.want.(uint16) + if !ok { + t.Errorf("want %v: not a uint16", test.want) + } + if dec != want { + t.Errorf("input %s: value mismatch: got %x, want %x", test.input, dec, want) + continue + } + } +} diff --git a/common/hexutil/json.libevm.go b/common/hexutil/json.libevm.go new file mode 100644 index 00000000000..0dcdd33f821 --- /dev/null +++ b/common/hexutil/json.libevm.go @@ -0,0 +1,72 @@ +// Copyright 2025 the libevm authors. +// +// The libevm additions to go-ethereum are free software: you can redistribute +// them and/or modify them under the terms of the GNU Lesser General Public License +// as published by the Free Software Foundation, either version 3 of the License, +// or (at your option) any later version. +// +// The libevm additions are distributed in the hope that they will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +// General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see +// . + +package hexutil + +import ( + "reflect" + "strconv" +) + +var uint16T = reflect.TypeOf(Uint16(0)) + +// Uint16 marshals/unmarshals as a JSON string with 0x prefix. +// The zero value marshals as "0x0". +type Uint16 uint16 + +// MarshalText implements encoding.TextMarshaler. +func (b Uint16) MarshalText() ([]byte, error) { + buf := make([]byte, 2, 6) + copy(buf, `0x`) + buf = strconv.AppendUint(buf, uint64(b), 16) + return buf, nil +} + +// UnmarshalJSON implements json.Unmarshaler. +func (b *Uint16) UnmarshalJSON(input []byte) error { + if !isString(input) { + return errNonString(uint16T) + } + return wrapTypeError(b.UnmarshalText(input[1:len(input)-1]), uint16T) +} + +// UnmarshalText implements encoding.TextUnmarshaler. +func (b *Uint16) UnmarshalText(input []byte) error { + raw, err := checkNumberText(input) + if err != nil { + return err + } + if len(raw) > 4 { + return ErrUint16Range + } + var dec uint16 + for _, byte := range raw { + nib := decodeNibble(byte) + if nib == badNibble { + return ErrSyntax + } + dec *= 16 + dec += uint16(nib) //nolint:gosec // G115 won't overflow uint16 as decodeNibble uses 4 bits + } + + *b = Uint16(dec) + return nil +} + +// String returns the hex encoding of b. +func (b Uint16) String() string { + return EncodeUint16(uint16(b)) +} diff --git a/common/hexutil/json.libevm_test.go b/common/hexutil/json.libevm_test.go new file mode 100644 index 00000000000..6cf4f53658a --- /dev/null +++ b/common/hexutil/json.libevm_test.go @@ -0,0 +1,103 @@ +// Copyright 2025 the libevm authors. +// +// The libevm additions to go-ethereum are free software: you can redistribute +// them and/or modify them under the terms of the GNU Lesser General Public License +// as published by the Free Software Foundation, either version 3 of the License, +// or (at your option) any later version. +// +// The libevm additions are distributed in the hope that they will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +// General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see +// . + +package hexutil + +import ( + "encoding/json" + "testing" +) + +var unmarshalUint16Tests = []unmarshalTest{ + // invalid encoding + {input: "", wantErr: errJSONEOF}, + {input: "null", wantErr: errNonString(uint16T)}, + {input: "10", wantErr: errNonString(uint16T)}, + {input: `"0"`, wantErr: wrapTypeError(ErrMissingPrefix, uint16T)}, + {input: `"0x"`, wantErr: wrapTypeError(ErrEmptyNumber, uint16T)}, + {input: `"0x01"`, wantErr: wrapTypeError(ErrLeadingZero, uint16T)}, + {input: `"0x10000"`, wantErr: wrapTypeError(ErrUint16Range, uint16T)}, + {input: `"0xx"`, wantErr: wrapTypeError(ErrSyntax, uint16T)}, + {input: `"0xz1"`, wantErr: wrapTypeError(ErrSyntax, uint16T)}, + + // valid encoding + {input: `""`, want: uint16(0)}, + {input: `"0x0"`, want: uint16(0)}, + {input: `"0x2"`, want: uint16(0x2)}, + {input: `"0x2F2"`, want: uint16(0x2f2)}, + {input: `"0X2F2"`, want: uint16(0x2f2)}, + {input: `"0x1122"`, want: uint16(0x1122)}, + {input: `"0xbbb"`, want: uint16(0xbbb)}, + {input: `"0xffff"`, want: uint16(0xffff)}, +} + +func TestUnmarshalUint16(t *testing.T) { + for _, test := range unmarshalUint16Tests { + var v Uint16 + err := json.Unmarshal([]byte(test.input), &v) + if !checkError(t, test.input, err, test.wantErr) { + continue + } + want, ok := test.want.(uint16) + if !ok { + t.Errorf("want %v: not a uint16", test.want) + } + if uint16(v) != want { + t.Errorf("input %s: value mismatch: got %d, want %d", test.input, v, want) + continue + } + } +} + +func BenchmarkUnmarshalUint16(b *testing.B) { + input := []byte(`"0x1234"`) + for i := 0; i < b.N; i++ { + var v Uint16 + err := v.UnmarshalJSON(input) + if err != nil { + b.Errorf("error unmarshalling: %v", err) + } + } +} + +func TestMarshalUint16(t *testing.T) { + tests := []struct { + input uint16 + want string + }{ + {0, "0x0"}, + {1, "0x1"}, + {0xff, "0xff"}, + {0x1122, "0x1122"}, + {0xffff, "0xffff"}, + } + + for _, test := range tests { + out, err := json.Marshal(Uint16(test.input)) + if err != nil { + t.Errorf("%d: %v", test.input, err) + continue + } + if want := `"` + test.want + `"`; string(out) != want { + t.Errorf("%d: MarshalJSON output mismatch: got %q, want %q", test.input, out, want) + continue + } + if out := Uint16(test.input).String(); out != test.want { + t.Errorf("%d: String mismatch: got %q, want %q", test.input, out, test.want) + continue + } + } +}