Skip to content

Commit

Permalink
common/hexutil: implement TextMarshaler, TextUnmarshaler
Browse files Browse the repository at this point in the history
This commit makes the wrapper types more generally applicable.
encoding.TextMarshaler is supported by most codec implementations (e.g.
for yaml).

The tests now ensure that package json actually recognizes the custom
marshaler implementation irrespective of how it is implemented.

The Uint type has new tests, too. These are tricky because uint size
depends on the CPU word size. Turns out that there was one incorrect
case where decoding returned ErrUint64Range instead of ErrUintRange.
  • Loading branch information
fjl committed Mar 2, 2017
1 parent 357d00c commit d304da3
Show file tree
Hide file tree
Showing 8 changed files with 238 additions and 96 deletions.
14 changes: 11 additions & 3 deletions common/hexutil/hexutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ type marshalTest struct {
}

type unmarshalTest struct {
input string
want interface{}
wantErr error
input string
want interface{}
wantErr error // if set, decoding must fail on any platform
wantErr32bit error // if set, decoding must fail on 32bit platforms (used for Uint tests)
}

var (
Expand All @@ -55,6 +56,13 @@ var (
{uint64(0x1122334455667788), "0x1122334455667788"},
}

encodeUintTests = []marshalTest{
{uint(0), "0x0"},
{uint(1), "0x1"},
{uint(0xff), "0xff"},
{uint(0x11223344), "0x11223344"},
}

decodeBytesTests = []unmarshalTest{
// invalid
{input: ``, wantErr: ErrEmptyString},
Expand Down
123 changes: 69 additions & 54 deletions common/hexutil/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ import (
)

var (
jsonNull = []byte("null")
jsonZero = []byte(`"0x0"`)
textZero = []byte(`0x0`)
errNonString = errors.New("cannot unmarshal non-string as hex data")
errNegativeBigInt = errors.New("hexutil.Big: can't marshal negative integer")
)
Expand All @@ -35,18 +34,25 @@ var (
// The empty slice marshals as "0x".
type Bytes []byte

// MarshalJSON implements json.Marshaler.
func (b Bytes) MarshalJSON() ([]byte, error) {
result := make([]byte, len(b)*2+4)
copy(result, `"0x`)
hex.Encode(result[3:], b)
result[len(result)-1] = '"'
// MarshalText implements encoding.TextMarshaler
func (b Bytes) MarshalText() ([]byte, error) {
result := make([]byte, len(b)*2+2)
copy(result, `0x`)
hex.Encode(result[2:], b)
return result, nil
}

// UnmarshalJSON implements json.Unmarshaler.
func (b *Bytes) UnmarshalJSON(input []byte) error {
raw, err := checkJSON(input)
if !isString(input) {
return errNonString
}
return b.UnmarshalText(input[1 : len(input)-1])
}

// UnmarshalText implements encoding.TextUnmarshaler.
func (b *Bytes) UnmarshalText(input []byte) error {
raw, err := checkText(input)
if err != nil {
return err
}
Expand All @@ -64,17 +70,11 @@ func (b Bytes) String() string {
return Encode(b)
}

// UnmarshalJSON decodes input as a JSON string with 0x prefix. The length of out
// UnmarshalFixedText decodes the input as a string with 0x prefix. The length of out
// determines the required input length. This function is commonly used to implement the
// UnmarshalJSON method for fixed-size types:
//
// type Foo [8]byte
//
// func (f *Foo) UnmarshalJSON(input []byte) error {
// return hexutil.UnmarshalJSON("Foo", input, f[:])
// }
func UnmarshalJSON(typname string, input, out []byte) error {
raw, err := checkJSON(input)
// UnmarshalText method for fixed-size types.
func UnmarshalFixedText(typname string, input, out []byte) error {
raw, err := checkText(input)
if err != nil {
return err
}
Expand All @@ -99,26 +99,33 @@ func UnmarshalJSON(typname string, input, out []byte) error {
// marshaled without error.
type Big big.Int

// MarshalJSON implements json.Marshaler.
func (b *Big) MarshalJSON() ([]byte, error) {
if b == nil {
return jsonNull, nil
}
bigint := (*big.Int)(b)
// MarshalText implements encoding.TextMarshaler
func (b Big) MarshalText() ([]byte, error) {
bigint := (big.Int)(b)
if bigint.Sign() == -1 {
return nil, errNegativeBigInt
}
nbits := bigint.BitLen()
if nbits == 0 {
return jsonZero, nil
return textZero, nil
}
enc := fmt.Sprintf(`"0x%x"`, bigint)
return []byte(enc), nil
enc := make([]byte, 2, nbits/4+2)
copy(enc, "0x")
enc = bigint.Append(enc, 16)
return enc, nil
}

// UnmarshalJSON implements json.Unmarshaler.
func (b *Big) UnmarshalJSON(input []byte) error {
raw, err := checkNumberJSON(input)
if !isString(input) {
return errNonString
}
return b.UnmarshalText(input[1 : len(input)-1])
}

// UnmarshalText implements encoding.TextUnmarshaler
func (b *Big) UnmarshalText(input []byte) error {
raw, err := checkNumberText(input)
if err != nil {
return err
}
Expand Down Expand Up @@ -162,18 +169,25 @@ func (b *Big) String() string {
// The zero value marshals as "0x0".
type Uint64 uint64

// MarshalJSON implements json.Marshaler.
func (b Uint64) MarshalJSON() ([]byte, error) {
buf := make([]byte, 3, 12)
copy(buf, `"0x`)
// MarshalText implements encoding.TextMarshaler.
func (b Uint64) MarshalText() ([]byte, error) {
buf := make([]byte, 2, 10)
copy(buf, `0x`)
buf = strconv.AppendUint(buf, uint64(b), 16)
buf = append(buf, '"')
return buf, nil
}

// UnmarshalJSON implements json.Unmarshaler.
func (b *Uint64) UnmarshalJSON(input []byte) error {
raw, err := checkNumberJSON(input)
if !isString(input) {
return errNonString
}
return b.UnmarshalText(input[1 : len(input)-1])
}

// UnmarshalText implements encoding.TextUnmarshaler
func (b *Uint64) UnmarshalText(input []byte) error {
raw, err := checkNumberText(input)
if err != nil {
return err
}
Expand Down Expand Up @@ -202,19 +216,27 @@ func (b Uint64) String() string {
// The zero value marshals as "0x0".
type Uint uint

// MarshalJSON implements json.Marshaler.
func (b Uint) MarshalJSON() ([]byte, error) {
return Uint64(b).MarshalJSON()
// MarshalText implements encoding.TextMarshaler.
func (b Uint) MarshalText() ([]byte, error) {
return Uint64(b).MarshalText()
}

// UnmarshalJSON implements json.Unmarshaler.
func (b *Uint) UnmarshalJSON(input []byte) error {
if !isString(input) {
return errNonString
}
return b.UnmarshalText(input[1 : len(input)-1])
}

// UnmarshalText implements encoding.TextUnmarshaler.
func (b *Uint) UnmarshalText(input []byte) error {
var u64 Uint64
err := u64.UnmarshalJSON(input)
if err != nil {
return err
} else if u64 > Uint64(^uint(0)) {
err := u64.UnmarshalText(input)
if u64 > Uint64(^uint(0)) || err == ErrUint64Range {
return ErrUintRange
} else if err != nil {
return err
}
*b = Uint(u64)
return nil
Expand All @@ -233,28 +255,21 @@ func bytesHave0xPrefix(input []byte) bool {
return len(input) >= 2 && input[0] == '0' && (input[1] == 'x' || input[1] == 'X')
}

func checkJSON(input []byte) (raw []byte, err error) {
if !isString(input) {
return nil, errNonString
}
if len(input) == 2 {
func checkText(input []byte) ([]byte, error) {
if len(input) == 0 {
return nil, nil // empty strings are allowed
}
if !bytesHave0xPrefix(input[1:]) {
if !bytesHave0xPrefix(input) {
return nil, ErrMissingPrefix
}
input = input[3 : len(input)-1]
input = input[2:]
if len(input)%2 != 0 {
return nil, ErrOddLength
}
return input, nil
}

func checkNumberJSON(input []byte) (raw []byte, err error) {
if !isString(input) {
return nil, errNonString
}
input = input[1 : len(input)-1]
func checkNumberText(input []byte) (raw []byte, err error) {
if len(input) == 0 {
return nil, nil // empty strings are allowed
}
Expand Down
45 changes: 45 additions & 0 deletions common/hexutil/json_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it 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 go-ethereum library is distributed in the hope that it 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 <http://www.gnu.org/licenses/>.

package hexutil_test

import (
"encoding/json"
"fmt"

"github.com/ethereum/go-ethereum/common/hexutil"
)

type MyType [5]byte

func (v *MyType) UnmarshalText(input []byte) error {
return hexutil.UnmarshalFixedText("MyType", input, v[:])
}

func (v MyType) String() string {
return hexutil.Bytes(v[:]).String()
}

func ExampleUnmarshalFixedText() {
var v1, v2 MyType
fmt.Println("v1 error:", json.Unmarshal([]byte(`"0x01"`), &v1))
fmt.Println("v2 error:", json.Unmarshal([]byte(`"0x0101010101"`), &v2))
fmt.Println("v2:", v2)
// Output:
// v1 error: hex string has length 2, want 10 for MyType
// v2 error: <nil>
// v2: 0x0101010101
}
Loading

0 comments on commit d304da3

Please sign in to comment.