From 5a9efcc6543dec421404e5c2aa0b9ff5f8723815 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 7 Jul 2021 00:18:00 +0300 Subject: [PATCH] stackitem: rework error handling Return good named errors everywhere, export appropriate constants, make errors.Is() work. --- pkg/vm/stackitem/item.go | 86 +++++++++++++++----------- pkg/vm/stackitem/json.go | 60 +++++++++++------- pkg/vm/stackitem/serialization.go | 18 ++++-- pkg/vm/stackitem/serialization_test.go | 6 +- pkg/vm/stackitem/type.go | 5 +- 5 files changed, 108 insertions(+), 67 deletions(-) diff --git a/pkg/vm/stackitem/item.go b/pkg/vm/stackitem/item.go index a8af99573..db2c1cac4 100644 --- a/pkg/vm/stackitem/item.go +++ b/pkg/vm/stackitem/item.go @@ -53,10 +53,28 @@ type Item interface { } var ( - errInvalidConversion = errors.New("invalid conversion type") - errExceedingMaxComparableSize = errors.New("the operand exceeds the maximum comparable size") + // ErrInvalidConversion is returned on attempt to make an incorrect + // conversion between item types. + ErrInvalidConversion = errors.New("invalid conversion") + + // ErrTooBig is returned when item exceeds some size constraints like + // maximum allowed integer value of number of elements in array. It + // can also be returned by serialization functions if resulting + // value exceeds MaxSize. + ErrTooBig = errors.New("too big") + + errTooBigComparable = fmt.Errorf("%w: uncomparable", ErrTooBig) + errTooBigInteger = fmt.Errorf("%w: integer", ErrTooBig) + errTooBigKey = fmt.Errorf("%w: map key", ErrTooBig) + errTooBigSize = fmt.Errorf("%w: size", ErrTooBig) ) +// mkInvConversion creates conversion error with additional metadata (from and +// to types). +func mkInvConversion(from Item, to Type) error { + return fmt.Errorf("%w: %s/%s", ErrInvalidConversion, from, to) +} + // Make tries to make appropriate stack item from provided value. // It will panic if it's not possible. func Make(v interface{}) Item { @@ -138,7 +156,7 @@ func ToString(item Item) (string, error) { return "", err } if !utf8.Valid(bs) { - return "", errors.New("not a valid UTF-8") + return "", fmt.Errorf("%w: not UTF-8", ErrInvalidValue) } return string(bs), nil } @@ -174,7 +192,7 @@ func convertPrimitive(item Item, typ Type) (Item, error) { } return NewBool(b), nil default: - return nil, errInvalidConversion + return nil, mkInvConversion(item, typ) } } @@ -232,12 +250,12 @@ func (i *Struct) TryBool() (bool, error) { return true, nil } // TryBytes implements Item interface. func (i *Struct) TryBytes() ([]byte, error) { - return nil, errors.New("can't convert Struct to ByteString") + return nil, mkInvConversion(i, ByteArrayT) } // TryInteger implements Item interface. func (i *Struct) TryInteger() (*big.Int, error) { - return nil, errors.New("can't convert Struct to Integer") + return nil, mkInvConversion(i, IntegerT) } // Equals implements Item interface. @@ -274,7 +292,7 @@ func (i *Struct) Convert(typ Type) (Item, error) { case BooleanT: return NewBool(true), nil default: - return nil, errInvalidConversion + return nil, mkInvConversion(i, typ) } } @@ -318,12 +336,12 @@ func (i Null) TryBool() (bool, error) { return false, nil } // TryBytes implements Item interface. func (i Null) TryBytes() ([]byte, error) { - return nil, errors.New("can't convert Null to ByteString") + return nil, mkInvConversion(i, ByteArrayT) } // TryInteger implements Item interface. func (i Null) TryInteger() (*big.Int, error) { - return nil, errors.New("can't convert Null to Integer") + return nil, mkInvConversion(i, IntegerT) } // Equals implements Item interface. @@ -338,7 +356,7 @@ func (i Null) Type() Type { return AnyT } // Convert implements Item interface. func (i Null) Convert(typ Type) (Item, error) { if typ == AnyT || !typ.IsValid() { - return nil, errInvalidConversion + return nil, mkInvConversion(i, typ) } return i, nil } @@ -350,18 +368,16 @@ type BigInteger struct { // NewBigInteger returns an new BigInteger object. func NewBigInteger(value *big.Int) *BigInteger { - const tooBigErrMsg = "integer is too big" - // There are 2 cases, when `BitLen` differs from actual size: // 1. Positive integer with highest bit on byte boundary = 1. // 2. Negative integer with highest bit on byte boundary = 1 // minus some value. (-0x80 -> 0x80, -0x7F -> 0x81, -0x81 -> 0x7FFF). sz := value.BitLen() if sz > MaxBigIntegerSizeBits { - panic(tooBigErrMsg) + panic(errTooBigInteger) } else if sz == MaxBigIntegerSizeBits { if value.Sign() == 1 || value.TrailingZeroBits() != MaxBigIntegerSizeBits-1 { - panic(tooBigErrMsg) + panic(errTooBigInteger) } } return &BigInteger{ @@ -531,7 +547,7 @@ func (i *ByteArray) String() string { // TryBool implements Item interface. func (i *ByteArray) TryBool() (bool, error) { if len(i.value) > MaxBigIntegerSizeBits/8 { - return false, errors.New("too big byte string") + return false, errTooBigInteger } for _, b := range i.value { if b != 0 { @@ -549,7 +565,7 @@ func (i *ByteArray) TryBytes() ([]byte, error) { // TryInteger implements Item interface. func (i *ByteArray) TryInteger() (*big.Int, error) { if len(i.value) > MaxBigIntegerSizeBits/8 { - return nil, errors.New("integer is too big") + return nil, errTooBigInteger } return bigint.FromBytes(i.value), nil } @@ -557,7 +573,7 @@ func (i *ByteArray) TryInteger() (*big.Int, error) { // Equals implements Item interface. func (i *ByteArray) Equals(s Item) bool { if len(i.value) > MaxByteArrayComparableSize { - panic(errExceedingMaxComparableSize) + panic(errTooBigComparable) } if i == s { return true @@ -569,7 +585,7 @@ func (i *ByteArray) Equals(s Item) bool { return false } if len(val.value) > MaxByteArrayComparableSize { - panic(errExceedingMaxComparableSize) + panic(errTooBigComparable) } return bytes.Equal(i.value, val.value) } @@ -641,12 +657,12 @@ func (i *Array) TryBool() (bool, error) { return true, nil } // TryBytes implements Item interface. func (i *Array) TryBytes() ([]byte, error) { - return nil, errors.New("can't convert Array to ByteString") + return nil, mkInvConversion(i, ByteArrayT) } // TryInteger implements Item interface. func (i *Array) TryInteger() (*big.Int, error) { - return nil, errors.New("can't convert Array to Integer") + return nil, mkInvConversion(i, IntegerT) } // Equals implements Item interface. @@ -675,7 +691,7 @@ func (i *Array) Convert(typ Type) (Item, error) { case BooleanT: return NewBool(true), nil default: - return nil, errInvalidConversion + return nil, mkInvConversion(i, typ) } } @@ -731,12 +747,12 @@ func (i *Map) TryBool() (bool, error) { return true, nil } // TryBytes implements Item interface. func (i *Map) TryBytes() ([]byte, error) { - return nil, errors.New("can't convert Map to ByteString") + return nil, mkInvConversion(i, ByteArrayT) } // TryInteger implements Item interface. func (i *Map) TryInteger() (*big.Int, error) { - return nil, errors.New("can't convert Map to Integer") + return nil, mkInvConversion(i, IntegerT) } // Equals implements Item interface. @@ -780,7 +796,7 @@ func (i *Map) Convert(typ Type) (Item, error) { case BooleanT: return NewBool(true), nil default: - return nil, errInvalidConversion + return nil, mkInvConversion(i, typ) } } @@ -812,11 +828,11 @@ func IsValidMapKey(key Item) error { case *ByteArray: size := len(key.Value().([]byte)) if size > MaxKeySize { - return fmt.Errorf("invalid map key size: %d", size) + return errTooBigKey } return nil default: - return fmt.Errorf("invalid map key of type %s", key.Type()) + return fmt.Errorf("%w: %s map key", ErrInvalidType, key.Type()) } } @@ -853,12 +869,12 @@ func (i *Interop) TryBool() (bool, error) { return true, nil } // TryBytes implements Item interface. func (i *Interop) TryBytes() ([]byte, error) { - return nil, errors.New("can't convert Interop to ByteString") + return nil, mkInvConversion(i, ByteArrayT) } // TryInteger implements Item interface. func (i *Interop) TryInteger() (*big.Int, error) { - return nil, errors.New("can't convert Interop to Integer") + return nil, mkInvConversion(i, IntegerT) } // Equals implements Item interface. @@ -883,7 +899,7 @@ func (i *Interop) Convert(typ Type) (Item, error) { case BooleanT: return NewBool(true), nil default: - return nil, errInvalidConversion + return nil, mkInvConversion(i, typ) } } @@ -946,12 +962,12 @@ func (p *Pointer) TryBool() (bool, error) { // TryBytes implements Item interface. func (p *Pointer) TryBytes() ([]byte, error) { - return nil, errors.New("can't convert Pointer to ByteString") + return nil, mkInvConversion(p, ByteArrayT) } // TryInteger implements Item interface. func (p *Pointer) TryInteger() (*big.Int, error) { - return nil, errors.New("can't convert Pointer to Integer") + return nil, mkInvConversion(p, IntegerT) } // Equals implements Item interface. @@ -976,7 +992,7 @@ func (p *Pointer) Convert(typ Type) (Item, error) { case BooleanT: return NewBool(true), nil default: - return nil, errInvalidConversion + return nil, mkInvConversion(p, typ) } } @@ -1024,7 +1040,7 @@ func (i *Buffer) TryBytes() ([]byte, error) { // TryInteger implements Item interface. func (i *Buffer) TryInteger() (*big.Int, error) { - return nil, errors.New("can't convert Buffer to Integer") + return nil, mkInvConversion(i, IntegerT) } // Equals implements Item interface. @@ -1058,11 +1074,11 @@ func (i *Buffer) Convert(typ Type) (Item, error) { return NewByteArray(val), nil case IntegerT: if len(i.value) > MaxBigIntegerSizeBits/8 { - return nil, errInvalidConversion + return nil, errTooBigInteger } return NewBigInteger(bigint.FromBytes(i.value)), nil default: - return nil, errInvalidConversion + return nil, mkInvConversion(i, typ) } } diff --git a/pkg/vm/stackitem/json.go b/pkg/vm/stackitem/json.go index 3d6abe9ed..a431d73b9 100644 --- a/pkg/vm/stackitem/json.go +++ b/pkg/vm/stackitem/json.go @@ -23,8 +23,16 @@ type decoder struct { // MaxAllowedInteger is the maximum integer allowed to be encoded. const MaxAllowedInteger = 2<<53 - 1 -// maxJSONDepth is a maximum allowed depth-level of decoded JSON. -const maxJSONDepth = 10 +// MaxJSONDepth is the maximum allowed nesting level of encoded/decoded JSON. +const MaxJSONDepth = 10 + +// ErrInvalidValue is returned when item value doesn't fit some constraints +// during serialization or deserialization. +var ErrInvalidValue = errors.New("invalid value") + +// ErrTooDeep is returned when JSON encoder/decoder goes beyond MaxJSONDepth in +// its processing. +var ErrTooDeep = errors.New("too deep") // ToJSON encodes Item to JSON. // It behaves as following: @@ -48,7 +56,7 @@ func toJSON(buf *io.BufBinWriter, item Item) { if w.Err != nil { return } else if buf.Len() > MaxSize { - w.Err = errors.New("item is too big") + w.Err = errTooBigSize } switch it := item.(type) { case *Array, *Struct: @@ -76,7 +84,7 @@ func toJSON(buf *io.BufBinWriter, item Item) { w.WriteB('}') case *BigInteger: if it.value.CmpAbs(big.NewInt(MaxAllowedInteger)) == 1 { - w.Err = errors.New("too big integer") + w.Err = fmt.Errorf("%w (MaxAllowedInteger)", ErrInvalidValue) return } w.WriteBytes([]byte(it.value.String())) @@ -91,11 +99,11 @@ func toJSON(buf *io.BufBinWriter, item Item) { case Null: w.WriteBytes([]byte("null")) default: - w.Err = fmt.Errorf("invalid item: %s", it.String()) + w.Err = fmt.Errorf("%w: %s", ErrUnserializable, it.String()) return } if w.Err == nil && buf.Len() > MaxSize { - w.Err = errors.New("item is too big") + w.Err = errTooBigSize } } @@ -131,7 +139,7 @@ func FromJSON(data []byte) (Item, error) { if item, err := d.decode(); err != nil { return nil, err } else if _, err := d.Token(); err != gio.EOF { - return nil, errors.New("unexpected items") + return nil, fmt.Errorf("%w: unexpected items", ErrInvalidValue) } else { return item, nil } @@ -146,8 +154,8 @@ func (d *decoder) decode() (Item, error) { case json.Delim: switch t { case json.Delim('{'), json.Delim('['): - if d.depth == maxJSONDepth { - return nil, errors.New("JSON depth limit exceeded") + if d.depth == MaxJSONDepth { + return nil, ErrTooDeep } d.depth++ var item Item @@ -167,7 +175,7 @@ func (d *decoder) decode() (Item, error) { return NewByteArray([]byte(t)), nil case float64: if math.Floor(t) != t { - return nil, fmt.Errorf("real value is not allowed: %v", t) + return nil, fmt.Errorf("%w (real value for int)", ErrInvalidValue) } return NewBigInteger(big.NewInt(int64(t))), nil case bool: @@ -221,8 +229,8 @@ func ToJSONWithTypes(item Item) ([]byte, error) { } func toJSONWithTypes(item Item, seen map[Item]bool) (interface{}, error) { - if len(seen) > maxJSONDepth { - return "", errors.New("too deep structure") + if len(seen) > MaxJSONDepth { + return "", ErrTooDeep } typ := item.Type() result := map[string]interface{}{ @@ -232,7 +240,7 @@ func toJSONWithTypes(item Item, seen map[Item]bool) (interface{}, error) { switch it := item.(type) { case *Array, *Struct: if seen[item] { - return "", errors.New("recursive structures can't be serialized to json") + return "", ErrRecursive } seen[item] = true arr := []interface{}{} @@ -253,7 +261,7 @@ func toJSONWithTypes(item Item, seen map[Item]bool) (interface{}, error) { value = it.value.String() case *Map: if seen[item] { - return "", errors.New("recursive structures can't be serialized to json") + return "", ErrRecursive } seen[item] = true arr := []interface{}{} @@ -292,6 +300,10 @@ type ( } ) +func mkErrValue(err error) error { + return fmt.Errorf("%w: %v", ErrInvalidValue, err) +} + // FromJSONWithTypes deserializes an item from typed-json representation. func FromJSONWithTypes(data []byte) (Item, error) { raw := new(rawItem) @@ -300,7 +312,7 @@ func FromJSONWithTypes(data []byte) (Item, error) { } typ, err := FromString(raw.Type) if err != nil { - return nil, errors.New("invalid type") + return nil, fmt.Errorf("%w: %v", ErrInvalidType, raw.Type) } switch typ { case AnyT: @@ -308,33 +320,33 @@ func FromJSONWithTypes(data []byte) (Item, error) { case PointerT: var pos int if err := json.Unmarshal(raw.Value, &pos); err != nil { - return nil, err + return nil, mkErrValue(err) } return NewPointer(pos, nil), nil case BooleanT: var b bool if err := json.Unmarshal(raw.Value, &b); err != nil { - return nil, err + return nil, mkErrValue(err) } return NewBool(b), nil case IntegerT: var s string if err := json.Unmarshal(raw.Value, &s); err != nil { - return nil, err + return nil, mkErrValue(err) } val, ok := new(big.Int).SetString(s, 10) if !ok { - return nil, errors.New("invalid integer") + return nil, mkErrValue(errors.New("not an integer")) } return NewBigInteger(val), nil case ByteArrayT, BufferT: var s string if err := json.Unmarshal(raw.Value, &s); err != nil { - return nil, err + return nil, mkErrValue(err) } val, err := base64.StdEncoding.DecodeString(s) if err != nil { - return nil, err + return nil, mkErrValue(err) } if typ == ByteArrayT { return NewByteArray(val), nil @@ -343,7 +355,7 @@ func FromJSONWithTypes(data []byte) (Item, error) { case ArrayT, StructT: var arr []json.RawMessage if err := json.Unmarshal(raw.Value, &arr); err != nil { - return nil, err + return nil, mkErrValue(err) } items := make([]Item, len(arr)) for i := range arr { @@ -360,7 +372,7 @@ func FromJSONWithTypes(data []byte) (Item, error) { case MapT: var arr []rawMapElement if err := json.Unmarshal(raw.Value, &arr); err != nil { - return nil, err + return nil, mkErrValue(err) } m := NewMap() for i := range arr { @@ -380,6 +392,6 @@ func FromJSONWithTypes(data []byte) (Item, error) { case InteropT: return NewInterop(nil), nil default: - return nil, errors.New("unexpected type") + return nil, fmt.Errorf("%w: %v", ErrInvalidType, typ) } } diff --git a/pkg/vm/stackitem/serialization.go b/pkg/vm/stackitem/serialization.go index f668522a6..cbc54b4b0 100644 --- a/pkg/vm/stackitem/serialization.go +++ b/pkg/vm/stackitem/serialization.go @@ -9,6 +9,14 @@ import ( "github.com/nspcc-dev/neo-go/pkg/io" ) +// ErrRecursive is returned on attempts to serialize some recursive stack item +// (like array including an item with reference to the same array). +var ErrRecursive = errors.New("recursive item") + +// ErrUnserializable is returned on attempt to serialize some item that can't +// be serialized (like Interop item or Pointer). +var ErrUnserializable = errors.New("unserializable") + // serContext is an internal serialization context. type serContext struct { *io.BinWriter @@ -71,7 +79,7 @@ func (w *serContext) serialize(item Item) { return } if w.seen[item] { - w.Err = errors.New("recursive structures can't be serialized") + w.Err = ErrRecursive return } @@ -92,7 +100,7 @@ func (w *serContext) serialize(item Item) { if w.allowInvalid { w.WriteBytes([]byte{byte(InteropT)}) } else { - w.Err = errors.New("interop item can't be serialized") + w.Err = fmt.Errorf("%w: Interop", ErrUnserializable) } case *Array, *Struct: w.seen[item] = true @@ -126,12 +134,12 @@ func (w *serContext) serialize(item Item) { if w.allowInvalid { w.WriteBytes([]byte{byte(InvalidT)}) } else { - w.Err = errors.New("invalid stack item") + w.Err = fmt.Errorf("%w: nil", ErrUnserializable) } } if w.Err == nil && w.buf != nil && w.buf.Len() > MaxSize { - w.Err = errors.New("too big item") + w.Err = errTooBigSize } } @@ -213,7 +221,7 @@ func decodeBinary(r *io.BinReader, allowInvalid bool) Item { if t == InvalidT && allowInvalid { return nil } - r.Err = fmt.Errorf("unknown type: %v", t) + r.Err = fmt.Errorf("%w: %v", ErrInvalidType, t) return nil } } diff --git a/pkg/vm/stackitem/serialization_test.go b/pkg/vm/stackitem/serialization_test.go index f3d58aa96..02b07cb24 100644 --- a/pkg/vm/stackitem/serialization_test.go +++ b/pkg/vm/stackitem/serialization_test.go @@ -1,6 +1,7 @@ package stackitem import ( + "errors" "testing" "github.com/stretchr/testify/require" @@ -10,12 +11,13 @@ func TestSerializationMaxErr(t *testing.T) { base := make([]byte, MaxSize/2+1) item := Make(base) - arr := []Item{item, item.Dup()} + // Pointer is unserializable, but we specifically want to catch ErrTooBig. + arr := []Item{item, item.Dup(), NewPointer(0, []byte{})} aitem := Make(arr) _, err := Serialize(item) require.NoError(t, err) _, err = Serialize(aitem) - require.Error(t, err) + require.True(t, errors.Is(err, ErrTooBig), err) } diff --git a/pkg/vm/stackitem/type.go b/pkg/vm/stackitem/type.go index b28df9ebe..30dee93fd 100644 --- a/pkg/vm/stackitem/type.go +++ b/pkg/vm/stackitem/type.go @@ -2,6 +2,9 @@ package stackitem import "errors" +// ErrInvalidType is returned on attempts to deserialize some unknown item type. +var ErrInvalidType = errors.New("invalid type") + // Type represents type of the stack item. type Type byte @@ -82,6 +85,6 @@ func FromString(s string) (Type, error) { case "Interop": return InteropT, nil default: - return 0xFF, errors.New("invalid type") + return 0xFF, ErrInvalidType } }