neo-go/pkg/vm/stackitem/json.go
Evgeniy Stratonikov a8d2df874f stackitem: limit JSON size in ToJSONWithTypes
Also do not limit depth. It was introduced in e34fa2e915 as a simple
solution to OOM problem. In this commit we do exactly the refactoring
described there. Maximum size is the same as stack item size and
can be changed if needed withouth significat refactoring.
`1 MiB` seems sufficient, though.

Signed-off-by: Evgeniy Stratonikov <evgeniy@nspcc.ru>
2022-03-09 10:29:23 +03:00

486 lines
11 KiB
Go

package stackitem
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
gio "io"
"math"
"math/big"
"strconv"
)
// decoder is a wrapper around json.Decoder helping to mimic C# json decoder behaviour.
type decoder struct {
json.Decoder
count int
depth int
}
// MaxAllowedInteger is the maximum integer allowed to be encoded.
const MaxAllowedInteger = 2<<53 - 1
// 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:
// ByteArray -> base64 string
// BigInteger -> number
// Bool -> bool
// Null -> null
// Array, Struct -> array
// Map -> map with keys as UTF-8 bytes
func ToJSON(item Item) ([]byte, error) {
seen := make(map[Item]sliceNoPointer, typicalNumOfItems)
return toJSON(nil, seen, item)
}
// sliceNoPointer represents sub-slice of a known slice.
// It doesn't contain pointer and uses less memory than `[]byte`.
type sliceNoPointer struct {
start, end int
}
func toJSON(data []byte, seen map[Item]sliceNoPointer, item Item) ([]byte, error) {
if len(data) > MaxSize {
return nil, errTooBigSize
}
if old, ok := seen[item]; ok {
if len(data)+old.end-old.start > MaxSize {
return nil, errTooBigSize
}
return append(data, data[old.start:old.end]...), nil
}
start := len(data)
var err error
switch it := item.(type) {
case *Array, *Struct:
var items []Item
if a, ok := it.(*Array); ok {
items = a.value
} else {
items = it.(*Struct).value
}
data = append(data, '[')
for i, v := range items {
data, err = toJSON(data, seen, v)
if err != nil {
return nil, err
}
if i < len(items)-1 {
data = append(data, ',')
}
}
data = append(data, ']')
seen[item] = sliceNoPointer{start, len(data)}
case *Map:
data = append(data, '{')
for i := range it.value {
// map key can always be converted to []byte
// but are not always a valid UTF-8.
raw, err := itemToJSONString(it.value[i].Key)
if err != nil {
return nil, err
}
data = append(data, raw...)
data = append(data, ':')
data, err = toJSON(data, seen, it.value[i].Value)
if err != nil {
return nil, err
}
if i < len(it.value)-1 {
data = append(data, ',')
}
}
data = append(data, '}')
seen[item] = sliceNoPointer{start, len(data)}
case *BigInteger:
if it.Big().CmpAbs(big.NewInt(MaxAllowedInteger)) == 1 {
return nil, fmt.Errorf("%w (MaxAllowedInteger)", ErrInvalidValue)
}
data = append(data, it.Big().String()...)
case *ByteArray, *Buffer:
raw, err := itemToJSONString(it)
if err != nil {
return nil, err
}
data = append(data, raw...)
case Bool:
if it {
data = append(data, "true"...)
} else {
data = append(data, "false"...)
}
case Null:
data = append(data, "null"...)
default:
return nil, fmt.Errorf("%w: %s", ErrUnserializable, it.String())
}
if len(data) > MaxSize {
return nil, errTooBigSize
}
return data, nil
}
// itemToJSONString converts it to string
// surrounded in quotes with control characters escaped.
func itemToJSONString(it Item) ([]byte, error) {
s, err := ToString(it)
if err != nil {
return nil, err
}
data, _ := json.Marshal(s) // error never occurs because `ToString` checks for validity
// ref https://github.com/neo-project/neo-modules/issues/375 and https://github.com/dotnet/runtime/issues/35281
return bytes.Replace(data, []byte{'+'}, []byte("\\u002B"), -1), nil
}
// FromJSON decodes Item from JSON.
// It behaves as following:
// string -> ByteArray from base64
// number -> BigInteger
// bool -> Bool
// null -> Null
// array -> Array
// map -> Map, keys are UTF-8
func FromJSON(data []byte, maxCount int) (Item, error) {
d := decoder{
Decoder: *json.NewDecoder(bytes.NewReader(data)),
count: maxCount,
}
if item, err := d.decode(); err != nil {
return nil, err
} else if _, err := d.Token(); err != gio.EOF {
return nil, fmt.Errorf("%w: unexpected items", ErrInvalidValue)
} else {
return item, nil
}
}
func (d *decoder) decode() (Item, error) {
tok, err := d.Token()
if err != nil {
return nil, err
}
d.count--
if d.count < 0 && tok != json.Delim('}') && tok != json.Delim(']') {
return nil, errTooBigElements
}
switch t := tok.(type) {
case json.Delim:
switch t {
case json.Delim('{'), json.Delim('['):
if d.depth == MaxJSONDepth {
return nil, ErrTooDeep
}
d.depth++
var item Item
if t == json.Delim('{') {
item, err = d.decodeMap()
} else {
item, err = d.decodeArray()
}
d.depth--
return item, err
default:
d.count++
// no error above means corresponding closing token
// was encountered for map or array respectively
return nil, nil
}
case string:
return NewByteArray([]byte(t)), nil
case float64:
if math.Floor(t) != t {
return nil, fmt.Errorf("%w (real value for int)", ErrInvalidValue)
}
return NewBigInteger(big.NewInt(int64(t))), nil
case bool:
return NewBool(t), nil
default:
// it can be only `nil`
return Null{}, nil
}
}
func (d *decoder) decodeArray() (*Array, error) {
items := []Item{}
for {
item, err := d.decode()
if err != nil {
return nil, err
}
if item == nil {
return NewArray(items), nil
}
items = append(items, item)
}
}
func (d *decoder) decodeMap() (*Map, error) {
m := NewMap()
for {
key, err := d.Token()
if err != nil {
return nil, err
}
k, ok := key.(string)
if !ok {
return m, nil
}
d.count--
if d.count < 0 {
return nil, errTooBigElements
}
val, err := d.decode()
if err != nil {
return nil, err
}
m.Add(NewByteArray([]byte(k)), val)
}
}
// ToJSONWithTypes serializes any stackitem to JSON in a lossless way.
func ToJSONWithTypes(item Item) ([]byte, error) {
return toJSONWithTypes(nil, item, make(map[Item]sliceNoPointer, typicalNumOfItems))
}
func toJSONWithTypes(data []byte, item Item, seen map[Item]sliceNoPointer) ([]byte, error) {
if item == nil {
return nil, fmt.Errorf("%w: nil", ErrUnserializable)
}
if old, ok := seen[item]; ok {
if old.end == 0 {
// Compound item marshaling which has not yet finished.
return nil, ErrRecursive
}
if len(data)+old.end-old.start > MaxSize {
return nil, errTooBigSize
}
return append(data, data[old.start:old.end]...), nil
}
var val string
var hasValue bool
switch item.(type) {
case Null:
val = `{"type":"Any"}`
case *Interop:
val = `{"type":"Interop"}`
default:
val = `{"type":"` + item.Type().String() + `","value":`
hasValue = true
}
if len(data)+len(val) > MaxSize {
return nil, errTooBigSize
}
start := len(data)
data = append(data, val...)
if !hasValue {
return data, nil
}
// Primitive stack items are appended after the switch
// to reduce the amount of size checks.
var primitive string
var isBuffer bool
var err error
switch it := item.(type) {
case *Array, *Struct:
seen[item] = sliceNoPointer{}
data = append(data, '[')
for i, elem := range it.Value().([]Item) {
if i != 0 {
data = append(data, ',')
}
data, err = toJSONWithTypes(data, elem, seen)
if err != nil {
return nil, err
}
}
case Bool:
if it {
primitive = "true"
} else {
primitive = "false"
}
case *ByteArray:
primitive = `"` + base64.StdEncoding.EncodeToString(it.Value().([]byte)) + `"`
case *Buffer:
isBuffer = true
primitive = `"` + base64.StdEncoding.EncodeToString(it.Value().([]byte)) + `"`
case *BigInteger:
primitive = `"` + it.Big().String() + `"`
case *Map:
seen[item] = sliceNoPointer{}
data = append(data, '[')
for i := range it.value {
if i != 0 {
data = append(data, ',')
}
data = append(data, `{"key":`...)
data, err = toJSONWithTypes(data, it.value[i].Key, seen)
if err != nil {
return nil, err
}
data = append(data, `,"value":`...)
data, err = toJSONWithTypes(data, it.value[i].Value, seen)
if err != nil {
return nil, err
}
data = append(data, '}')
}
case *Pointer:
primitive = strconv.Itoa(it.pos)
}
if len(primitive) != 0 {
if len(data)+len(primitive)+1 > MaxSize {
return nil, errTooBigSize
}
data = append(data, primitive...)
data = append(data, '}')
if isBuffer {
seen[item] = sliceNoPointer{start, len(data)}
}
} else {
if len(data)+2 > MaxSize { // also take care of '}'
return nil, errTooBigSize
}
data = append(data, ']', '}')
seen[item] = sliceNoPointer{start, len(data)}
}
return data, nil
}
type (
rawItem struct {
Type string `json:"type"`
Value json.RawMessage `json:"value,omitempty"`
}
rawMapElement struct {
Key json.RawMessage `json:"key"`
Value json.RawMessage `json:"value"`
}
)
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)
if err := json.Unmarshal(data, raw); err != nil {
return nil, err
}
typ, err := FromString(raw.Type)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrInvalidType, raw.Type)
}
switch typ {
case AnyT:
return Null{}, nil
case PointerT:
var pos int
if err := json.Unmarshal(raw.Value, &pos); err != nil {
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, mkErrValue(err)
}
return NewBool(b), nil
case IntegerT:
var s string
if err := json.Unmarshal(raw.Value, &s); err != nil {
return nil, mkErrValue(err)
}
val, ok := new(big.Int).SetString(s, 10)
if !ok {
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, mkErrValue(err)
}
val, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return nil, mkErrValue(err)
}
if typ == ByteArrayT {
return NewByteArray(val), nil
}
return NewBuffer(val), nil
case ArrayT, StructT:
var arr []json.RawMessage
if err := json.Unmarshal(raw.Value, &arr); err != nil {
return nil, mkErrValue(err)
}
items := make([]Item, len(arr))
for i := range arr {
it, err := FromJSONWithTypes(arr[i])
if err != nil {
return nil, err
}
items[i] = it
}
if typ == ArrayT {
return NewArray(items), nil
}
return NewStruct(items), nil
case MapT:
var arr []rawMapElement
if err := json.Unmarshal(raw.Value, &arr); err != nil {
return nil, mkErrValue(err)
}
m := NewMap()
for i := range arr {
key, err := FromJSONWithTypes(arr[i].Key)
if err != nil {
return nil, err
} else if err = IsValidMapKey(key); err != nil {
return nil, err
}
value, err := FromJSONWithTypes(arr[i].Value)
if err != nil {
return nil, err
}
m.Add(key, value)
}
return m, nil
case InteropT:
return NewInterop(nil), nil
default:
return nil, fmt.Errorf("%w: %v", ErrInvalidType, typ)
}
}