From 4a741df757cf6b1d1b4c0e8c875c34b650f46167 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Fri, 23 Jun 2017 10:50:49 +0200 Subject: [PATCH] common/hexutil: wrap errors in json.UnmarshalTypeError This adds type and struct field context to error messages. Instead of "hex string of odd length" users will now see "json: cannot unmarshal hex string of odd length into Go struct field SendTxArgs.from of type common.Address". --- common/hexutil/hexutil.go | 25 +++++++++------ common/hexutil/json.go | 47 ++++++++++++++++++++------- common/hexutil/json_test.go | 64 ++++++++++++++++++------------------- common/types.go | 15 +++++++++ common/types_test.go | 10 +++--- 5 files changed, 102 insertions(+), 59 deletions(-) diff --git a/common/hexutil/hexutil.go b/common/hexutil/hexutil.go index 6b128ae360..582a67c22c 100644 --- a/common/hexutil/hexutil.go +++ b/common/hexutil/hexutil.go @@ -32,7 +32,6 @@ package hexutil import ( "encoding/hex" - "errors" "fmt" "math/big" "strconv" @@ -41,17 +40,23 @@ import ( const uintBits = 32 << (uint64(^uint(0)) >> 63) var ( - ErrEmptyString = errors.New("empty hex string") - ErrMissingPrefix = errors.New("missing 0x prefix for hex data") - ErrSyntax = errors.New("invalid hex") - ErrEmptyNumber = errors.New("hex number has no digits after 0x") - ErrLeadingZero = errors.New("hex number has leading zero digits after 0x") - ErrOddLength = errors.New("hex string has odd length") - ErrUint64Range = errors.New("hex number does not fit into 64 bits") - ErrUintRange = fmt.Errorf("hex number does not fit into %d bits", uintBits) - ErrBig256Range = errors.New("hex number does not fit into 256 bits") + ErrEmptyString = &decError{"empty hex string"} + ErrSyntax = &decError{"invalid hex string"} + ErrMissingPrefix = &decError{"hex string without 0x prefix"} + ErrOddLength = &decError{"hex string of odd length"} + ErrEmptyNumber = &decError{"hex string \"0x\""} + ErrLeadingZero = &decError{"hex number with leading zero digits"} + ErrUint64Range = &decError{"hex number > 64 bits"} + ErrUintRange = &decError{fmt.Sprintf("hex number > %d bits", uintBits)} + ErrBig256Range = &decError{"hex number > 256 bits"} ) +type decError struct{ msg string } + +func (err decError) Error() string { + return string(err.msg) +} + // Decode decodes a hex string with 0x prefix. func Decode(input string) ([]byte, error) { if len(input) == 0 { diff --git a/common/hexutil/json.go b/common/hexutil/json.go index 1bc1d014c7..943288fad3 100644 --- a/common/hexutil/json.go +++ b/common/hexutil/json.go @@ -18,15 +18,19 @@ package hexutil import ( "encoding/hex" - "errors" + "encoding/json" "fmt" "math/big" + "reflect" "strconv" ) var ( - textZero = []byte(`0x0`) - errNonString = errors.New("cannot unmarshal non-string as hex data") + textZero = []byte(`0x0`) + bytesT = reflect.TypeOf(Bytes(nil)) + bigT = reflect.TypeOf((*Big)(nil)) + uintT = reflect.TypeOf(Uint(0)) + uint64T = reflect.TypeOf(Uint64(0)) ) // Bytes marshals/unmarshals as a JSON string with 0x prefix. @@ -44,9 +48,9 @@ func (b Bytes) MarshalText() ([]byte, error) { // UnmarshalJSON implements json.Unmarshaler. func (b *Bytes) UnmarshalJSON(input []byte) error { if !isString(input) { - return errNonString + return errNonString(bytesT) } - return b.UnmarshalText(input[1 : len(input)-1]) + return wrapTypeError(b.UnmarshalText(input[1:len(input)-1]), bytesT) } // UnmarshalText implements encoding.TextUnmarshaler. @@ -69,6 +73,16 @@ func (b Bytes) String() string { return Encode(b) } +// UnmarshalFixedJSON 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. +func UnmarshalFixedJSON(typ reflect.Type, input, out []byte) error { + if !isString(input) { + return errNonString(typ) + } + return wrapTypeError(UnmarshalFixedText(typ.String(), input[1:len(input)-1], out), typ) +} + // 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 // UnmarshalText method for fixed-size types. @@ -127,9 +141,9 @@ func (b Big) MarshalText() ([]byte, error) { // UnmarshalJSON implements json.Unmarshaler. func (b *Big) UnmarshalJSON(input []byte) error { if !isString(input) { - return errNonString + return errNonString(bigT) } - return b.UnmarshalText(input[1 : len(input)-1]) + return wrapTypeError(b.UnmarshalText(input[1:len(input)-1]), bigT) } // UnmarshalText implements encoding.TextUnmarshaler @@ -189,9 +203,9 @@ func (b Uint64) MarshalText() ([]byte, error) { // UnmarshalJSON implements json.Unmarshaler. func (b *Uint64) UnmarshalJSON(input []byte) error { if !isString(input) { - return errNonString + return errNonString(uint64T) } - return b.UnmarshalText(input[1 : len(input)-1]) + return wrapTypeError(b.UnmarshalText(input[1:len(input)-1]), uint64T) } // UnmarshalText implements encoding.TextUnmarshaler @@ -233,9 +247,9 @@ func (b Uint) MarshalText() ([]byte, error) { // UnmarshalJSON implements json.Unmarshaler. func (b *Uint) UnmarshalJSON(input []byte) error { if !isString(input) { - return errNonString + return errNonString(uintT) } - return b.UnmarshalText(input[1 : len(input)-1]) + return wrapTypeError(b.UnmarshalText(input[1:len(input)-1]), uintT) } // UnmarshalText implements encoding.TextUnmarshaler. @@ -295,3 +309,14 @@ func checkNumberText(input []byte) (raw []byte, err error) { } return input, nil } + +func wrapTypeError(err error, typ reflect.Type) error { + if _, ok := err.(*decError); ok { + return &json.UnmarshalTypeError{Value: err.Error(), Type: typ} + } + return err +} + +func errNonString(typ reflect.Type) error { + return &json.UnmarshalTypeError{Value: "non-string", Type: typ} +} diff --git a/common/hexutil/json_test.go b/common/hexutil/json_test.go index e4e8274915..8a6b8643a1 100644 --- a/common/hexutil/json_test.go +++ b/common/hexutil/json_test.go @@ -62,12 +62,12 @@ var errJSONEOF = errors.New("unexpected end of JSON input") var unmarshalBytesTests = []unmarshalTest{ // invalid encoding {input: "", wantErr: errJSONEOF}, - {input: "null", wantErr: errNonString}, - {input: "10", wantErr: errNonString}, - {input: `"0"`, wantErr: ErrMissingPrefix}, - {input: `"0x0"`, wantErr: ErrOddLength}, - {input: `"0xxx"`, wantErr: ErrSyntax}, - {input: `"0x01zz01"`, wantErr: ErrSyntax}, + {input: "null", wantErr: errNonString(bytesT)}, + {input: "10", wantErr: errNonString(bytesT)}, + {input: `"0"`, wantErr: wrapTypeError(ErrMissingPrefix, bytesT)}, + {input: `"0x0"`, wantErr: wrapTypeError(ErrOddLength, bytesT)}, + {input: `"0xxx"`, wantErr: wrapTypeError(ErrSyntax, bytesT)}, + {input: `"0x01zz01"`, wantErr: wrapTypeError(ErrSyntax, bytesT)}, // valid encoding {input: `""`, want: referenceBytes("")}, @@ -127,16 +127,16 @@ func TestMarshalBytes(t *testing.T) { var unmarshalBigTests = []unmarshalTest{ // invalid encoding {input: "", wantErr: errJSONEOF}, - {input: "null", wantErr: errNonString}, - {input: "10", wantErr: errNonString}, - {input: `"0"`, wantErr: ErrMissingPrefix}, - {input: `"0x"`, wantErr: ErrEmptyNumber}, - {input: `"0x01"`, wantErr: ErrLeadingZero}, - {input: `"0xx"`, wantErr: ErrSyntax}, - {input: `"0x1zz01"`, wantErr: ErrSyntax}, + {input: "null", wantErr: errNonString(bigT)}, + {input: "10", wantErr: errNonString(bigT)}, + {input: `"0"`, wantErr: wrapTypeError(ErrMissingPrefix, bigT)}, + {input: `"0x"`, wantErr: wrapTypeError(ErrEmptyNumber, bigT)}, + {input: `"0x01"`, wantErr: wrapTypeError(ErrLeadingZero, bigT)}, + {input: `"0xx"`, wantErr: wrapTypeError(ErrSyntax, bigT)}, + {input: `"0x1zz01"`, wantErr: wrapTypeError(ErrSyntax, bigT)}, { input: `"0x10000000000000000000000000000000000000000000000000000000000000000"`, - wantErr: ErrBig256Range, + wantErr: wrapTypeError(ErrBig256Range, bigT), }, // valid encoding @@ -208,14 +208,14 @@ func TestMarshalBig(t *testing.T) { var unmarshalUint64Tests = []unmarshalTest{ // invalid encoding {input: "", wantErr: errJSONEOF}, - {input: "null", wantErr: errNonString}, - {input: "10", wantErr: errNonString}, - {input: `"0"`, wantErr: ErrMissingPrefix}, - {input: `"0x"`, wantErr: ErrEmptyNumber}, - {input: `"0x01"`, wantErr: ErrLeadingZero}, - {input: `"0xfffffffffffffffff"`, wantErr: ErrUint64Range}, - {input: `"0xx"`, wantErr: ErrSyntax}, - {input: `"0x1zz01"`, wantErr: ErrSyntax}, + {input: "null", wantErr: errNonString(uint64T)}, + {input: "10", wantErr: errNonString(uint64T)}, + {input: `"0"`, wantErr: wrapTypeError(ErrMissingPrefix, uint64T)}, + {input: `"0x"`, wantErr: wrapTypeError(ErrEmptyNumber, uint64T)}, + {input: `"0x01"`, wantErr: wrapTypeError(ErrLeadingZero, uint64T)}, + {input: `"0xfffffffffffffffff"`, wantErr: wrapTypeError(ErrUint64Range, uint64T)}, + {input: `"0xx"`, wantErr: wrapTypeError(ErrSyntax, uint64T)}, + {input: `"0x1zz01"`, wantErr: wrapTypeError(ErrSyntax, uint64T)}, // valid encoding {input: `""`, want: uint64(0)}, @@ -298,15 +298,15 @@ var ( var unmarshalUintTests = []unmarshalTest{ // invalid encoding {input: "", wantErr: errJSONEOF}, - {input: "null", wantErr: errNonString}, - {input: "10", wantErr: errNonString}, - {input: `"0"`, wantErr: ErrMissingPrefix}, - {input: `"0x"`, wantErr: ErrEmptyNumber}, - {input: `"0x01"`, wantErr: ErrLeadingZero}, - {input: `"0x100000000"`, want: uint(maxUint33bits), wantErr32bit: ErrUintRange}, - {input: `"0xfffffffffffffffff"`, wantErr: ErrUintRange}, - {input: `"0xx"`, wantErr: ErrSyntax}, - {input: `"0x1zz01"`, wantErr: ErrSyntax}, + {input: "null", wantErr: errNonString(uintT)}, + {input: "10", wantErr: errNonString(uintT)}, + {input: `"0"`, wantErr: wrapTypeError(ErrMissingPrefix, uintT)}, + {input: `"0x"`, wantErr: wrapTypeError(ErrEmptyNumber, uintT)}, + {input: `"0x01"`, wantErr: wrapTypeError(ErrLeadingZero, uintT)}, + {input: `"0x100000000"`, want: uint(maxUint33bits), wantErr32bit: wrapTypeError(ErrUintRange, uintT)}, + {input: `"0xfffffffffffffffff"`, wantErr: wrapTypeError(ErrUintRange, uintT)}, + {input: `"0xx"`, wantErr: wrapTypeError(ErrSyntax, uintT)}, + {input: `"0x1zz01"`, wantErr: wrapTypeError(ErrSyntax, uintT)}, // valid encoding {input: `""`, want: uint(0)}, @@ -317,7 +317,7 @@ var unmarshalUintTests = []unmarshalTest{ {input: `"0x1122aaff"`, want: uint(0x1122aaff)}, {input: `"0xbbb"`, want: uint(0xbbb)}, {input: `"0xffffffff"`, want: uint(0xffffffff)}, - {input: `"0xffffffffffffffff"`, want: uint(maxUint64bits), wantErr32bit: ErrUintRange}, + {input: `"0xffffffffffffffff"`, want: uint(maxUint64bits), wantErr32bit: wrapTypeError(ErrUintRange, uintT)}, } func TestUnmarshalUint(t *testing.T) { diff --git a/common/types.go b/common/types.go index 05288bf463..8037266349 100644 --- a/common/types.go +++ b/common/types.go @@ -31,6 +31,11 @@ const ( AddressLength = 20 ) +var ( + hashT = reflect.TypeOf(Hash{}) + addressT = reflect.TypeOf(Address{}) +) + // Hash represents the 32 byte Keccak256 hash of arbitrary data. type Hash [HashLength]byte @@ -72,6 +77,11 @@ func (h *Hash) UnmarshalText(input []byte) error { return hexutil.UnmarshalFixedText("Hash", input, h[:]) } +// UnmarshalJSON parses a hash in hex syntax. +func (h *Hash) UnmarshalJSON(input []byte) error { + return hexutil.UnmarshalFixedJSON(hashT, input, h[:]) +} + // MarshalText returns the hex representation of h. func (h Hash) MarshalText() ([]byte, error) { return hexutil.Bytes(h[:]).MarshalText() @@ -194,6 +204,11 @@ func (a *Address) UnmarshalText(input []byte) error { return hexutil.UnmarshalFixedText("Address", input, a[:]) } +// UnmarshalJSON parses a hash in hex syntax. +func (a *Address) UnmarshalJSON(input []byte) error { + return hexutil.UnmarshalFixedJSON(addressT, input, a[:]) +} + // UnprefixedHash allows marshaling an Address without 0x prefix. type UnprefixedAddress Address diff --git a/common/types_test.go b/common/types_test.go index 9f9d8b767c..154c330632 100644 --- a/common/types_test.go +++ b/common/types_test.go @@ -21,8 +21,6 @@ import ( "math/big" "strings" "testing" - - "github.com/ethereum/go-ethereum/common/hexutil" ) func TestBytesConversion(t *testing.T) { @@ -43,10 +41,10 @@ func TestHashJsonValidation(t *testing.T) { Size int Error string }{ - {"", 62, hexutil.ErrMissingPrefix.Error()}, - {"0x", 66, "hex string has length 66, want 64 for Hash"}, - {"0x", 63, hexutil.ErrOddLength.Error()}, - {"0x", 0, "hex string has length 0, want 64 for Hash"}, + {"", 62, "json: cannot unmarshal hex string without 0x prefix into Go value of type common.Hash"}, + {"0x", 66, "hex string has length 66, want 64 for common.Hash"}, + {"0x", 63, "json: cannot unmarshal hex string of odd length into Go value of type common.Hash"}, + {"0x", 0, "hex string has length 0, want 64 for common.Hash"}, {"0x", 64, ""}, {"0X", 64, ""}, }