neo-go/pkg/vm/stackitem/json.go
Evgeniy Stratonikov c4eb7db8a5 stackitem: access value of Array/Struct directly
```
name      old time/op    new time/op    delta
ToJSON-8    50.3µs ±34%    47.8µs ± 9%     ~     (p=0.971 n=10+10)

name      old alloc/op   new alloc/op   delta
ToJSON-8     396kB ± 0%     396kB ± 0%   -0.10%  (p=0.000 n=6+10)

name      old allocs/op  new allocs/op  delta
ToJSON-8      34.0 ± 0%      18.0 ± 0%  -47.06%  (p=0.000 n=10+10)
```

Signed-off-by: Evgeniy Stratonikov <evgeniy@nspcc.ru>
2021-07-12 14:40:21 +03:00

421 lines
9.7 KiB
Go

package stackitem
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
gio "io"
"math"
"math/big"
)
// decoder is a wrapper around json.Decoder helping to mimic C# json decoder behaviour.
type decoder struct {
json.Decoder
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)
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.value.CmpAbs(big.NewInt(MaxAllowedInteger)) == 1 {
return nil, fmt.Errorf("%w (MaxAllowedInteger)", ErrInvalidValue)
}
data = append(data, it.value.String()...)
case *ByteArray, *Buffer:
raw, err := itemToJSONString(it)
if err != nil {
return nil, err
}
data = append(data, raw...)
case *Bool:
if it.value {
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) (Item, error) {
d := decoder{Decoder: *json.NewDecoder(bytes.NewReader(data))}
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
}
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:
// 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
}
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) {
result, err := toJSONWithTypes(item, make(map[Item]bool))
if err != nil {
return nil, err
}
return json.Marshal(result)
}
func toJSONWithTypes(item Item, seen map[Item]bool) (interface{}, error) {
if len(seen) > MaxJSONDepth {
return "", ErrTooDeep
}
var value interface{}
switch it := item.(type) {
case *Array, *Struct:
if seen[item] {
return "", ErrRecursive
}
seen[item] = true
arr := []interface{}{}
for _, elem := range it.Value().([]Item) {
s, err := toJSONWithTypes(elem, seen)
if err != nil {
return "", err
}
arr = append(arr, s)
}
value = arr
delete(seen, item)
case *Bool:
value = it.value
case *Buffer, *ByteArray:
value = base64.StdEncoding.EncodeToString(it.Value().([]byte))
case *BigInteger:
value = it.value.String()
case *Map:
if seen[item] {
return "", ErrRecursive
}
seen[item] = true
arr := []interface{}{}
for i := range it.value {
// map keys are primitive types and can always be converted to json
key, _ := toJSONWithTypes(it.value[i].Key, seen)
val, err := toJSONWithTypes(it.value[i].Value, seen)
if err != nil {
return "", err
}
arr = append(arr, map[string]interface{}{
"key": key,
"value": val,
})
}
value = arr
delete(seen, item)
case *Pointer:
value = it.pos
case nil:
return "", fmt.Errorf("%w: nil", ErrUnserializable)
}
result := map[string]interface{}{
"type": item.Type().String(),
}
if value != nil {
result["value"] = value
}
return result, 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)
}
}