forked from TrueCloudLab/neoneo-go
203 lines
4.2 KiB
Go
203 lines
4.2 KiB
Go
|
package stackitem
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"encoding/base64"
|
||
|
"encoding/json"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
gio "io"
|
||
|
"math"
|
||
|
"math/big"
|
||
|
|
||
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||
|
)
|
||
|
|
||
|
// 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 a maximum allowed depth-level of decoded JSON.
|
||
|
const maxJSONDepth = 10
|
||
|
|
||
|
// 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) {
|
||
|
buf := io.NewBufBinWriter()
|
||
|
toJSON(buf, item)
|
||
|
if buf.Err != nil {
|
||
|
return nil, buf.Err
|
||
|
}
|
||
|
return buf.Bytes(), nil
|
||
|
}
|
||
|
|
||
|
func toJSON(buf *io.BufBinWriter, item Item) {
|
||
|
w := buf.BinWriter
|
||
|
if w.Err != nil {
|
||
|
return
|
||
|
} else if buf.Len() > MaxSize {
|
||
|
w.Err = errors.New("item is too big")
|
||
|
}
|
||
|
switch it := item.(type) {
|
||
|
case *Array, *Struct:
|
||
|
w.WriteB('[')
|
||
|
items := it.Value().([]Item)
|
||
|
for i, v := range items {
|
||
|
toJSON(buf, v)
|
||
|
if i < len(items)-1 {
|
||
|
w.WriteB(',')
|
||
|
}
|
||
|
}
|
||
|
w.WriteB(']')
|
||
|
case *Map:
|
||
|
w.WriteB('{')
|
||
|
for i := range it.value {
|
||
|
bs, _ := it.value[i].Key.TryBytes() // map key can always be converted to []byte
|
||
|
w.WriteB('"')
|
||
|
w.WriteBytes(bs)
|
||
|
w.WriteBytes([]byte(`":`))
|
||
|
toJSON(buf, it.value[i].Value)
|
||
|
if i < len(it.value)-1 {
|
||
|
w.WriteB(',')
|
||
|
}
|
||
|
}
|
||
|
w.WriteB('}')
|
||
|
case *BigInteger:
|
||
|
if it.value.CmpAbs(big.NewInt(MaxAllowedInteger)) == 1 {
|
||
|
w.Err = errors.New("too big integer")
|
||
|
return
|
||
|
}
|
||
|
w.WriteBytes([]byte(it.value.String()))
|
||
|
case *ByteArray:
|
||
|
w.WriteB('"')
|
||
|
val := it.Value().([]byte)
|
||
|
b := make([]byte, base64.StdEncoding.EncodedLen(len(val)))
|
||
|
base64.StdEncoding.Encode(b, val)
|
||
|
w.WriteBytes(b)
|
||
|
w.WriteB('"')
|
||
|
case *Bool:
|
||
|
if it.value {
|
||
|
w.WriteBytes([]byte("true"))
|
||
|
} else {
|
||
|
w.WriteBytes([]byte("false"))
|
||
|
}
|
||
|
case Null:
|
||
|
w.WriteBytes([]byte("null"))
|
||
|
default:
|
||
|
w.Err = fmt.Errorf("invalid item: %s", it.String())
|
||
|
return
|
||
|
}
|
||
|
if w.Err == nil && buf.Len() > MaxSize {
|
||
|
w.Err = errors.New("item is too big")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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, errors.New("unexpected items")
|
||
|
} 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, errors.New("JSON depth limit exceeded")
|
||
|
}
|
||
|
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:
|
||
|
b, err := base64.StdEncoding.DecodeString(t)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return NewByteArray(b), nil
|
||
|
case float64:
|
||
|
if math.Floor(t) != t {
|
||
|
return nil, fmt.Errorf("real value is not allowed: %v", t)
|
||
|
}
|
||
|
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)
|
||
|
}
|
||
|
}
|