diff --git a/pkg/vm/stackitem/json.go b/pkg/vm/stackitem/json.go index cc588ddaa..3980edaab 100644 --- a/pkg/vm/stackitem/json.go +++ b/pkg/vm/stackitem/json.go @@ -264,3 +264,107 @@ func toJSONWithTypes(item Item, seen map[Item]bool) (interface{}, error) { } 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"` + } +) + +// 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, errors.New("invalid type") + } + switch typ { + case AnyT: + return Null{}, nil + case PointerT: + var pos int + if err := json.Unmarshal(raw.Value, &pos); err != nil { + return nil, err + } + return NewPointer(pos, nil), nil + case BooleanT: + var b bool + if err := json.Unmarshal(raw.Value, &b); err != nil { + return nil, err + } + return NewBool(b), nil + case IntegerT: + var s string + if err := json.Unmarshal(raw.Value, &s); err != nil { + return nil, err + } + val, ok := new(big.Int).SetString(s, 10) + if !ok { + return nil, errors.New("invalid integer") + } + return NewBigInteger(val), nil + case ByteArrayT, BufferT: + var s string + if err := json.Unmarshal(raw.Value, &s); err != nil { + return nil, err + } + val, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return nil, 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, 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, err + } + m := NewMap() + for i := range arr { + key, err := FromJSONWithTypes(arr[i].Key) + if err != nil { + return nil, err + } else if !IsValidMapKey(key) { + return nil, fmt.Errorf("invalid map key of type %s", key.Type()) + } + 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, errors.New("unexpected type") + } +} diff --git a/pkg/vm/stackitem/json_test.go b/pkg/vm/stackitem/json_test.go index e2eb00c5c..bb9e17acd 100644 --- a/pkg/vm/stackitem/json_test.go +++ b/pkg/vm/stackitem/json_test.go @@ -155,12 +155,19 @@ func TestToJSONWithTypes(t *testing.T) { {"BoolFalse", NewBool(false), `{"type":"Boolean","value":false}`}, {"Struct", NewStruct([]Item{Make(11)}), `{"type":"Struct","value":[{"type":"Integer","value":"11"}]}`}, + {"Map", NewMapWithValue([]MapElement{{Key: NewBigInteger(big.NewInt(42)), Value: NewBool(false)}}), + `{"type":"Map","value":[{"key":{"type":"Integer","value":"42"},` + + `"value":{"type":"Boolean","value":false}}]}`}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { s, err := ToJSONWithTypes(tc.item) require.NoError(t, err) require.Equal(t, tc.result, string(s)) + + item, err := FromJSONWithTypes(s) + require.NoError(t, err) + require.Equal(t, tc.item, item) }) } @@ -182,3 +189,52 @@ func TestToJSONWithTypes(t *testing.T) { }) }) } + +func TestFromJSONWithTypes(t *testing.T) { + testCases := []struct { + name string + json string + item Item + }{ + {"Pointer", `{"type":"Pointer","value":3}`, NewPointer(3, nil)}, + {"Interop", `{"type":"Interop"}`, NewInterop(nil)}, + {"Null", `{"type":"Any"}`, Null{}}, + {"Array", `{"type":"Array","value":[{"type":"Any"}]}`, NewArray([]Item{Null{}})}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + item, err := FromJSONWithTypes([]byte(tc.json)) + require.NoError(t, err) + require.Equal(t, tc.item, item) + }) + } + + t.Run("Invalid", func(t *testing.T) { + errCases := []struct { + name string + json string + }{ + {"InvalidType", `{"type":int,"value":"4"`}, + {"UnexpectedType", `{"type":"int","value":"4"}`}, + {"IntegerValue1", `{"type":"Integer","value": 4}`}, + {"IntegerValue2", `{"type":"Integer","value": "a"}`}, + {"BoolValue", `{"type":"Boolean","value": "str"}`}, + {"PointerValue", `{"type":"Pointer","value": "str"}`}, + {"BufferValue1", `{"type":"Buffer","value":"not a base 64"}`}, + {"BufferValue2", `{"type":"Buffer","value":123}`}, + {"ArrayValue", `{"type":"Array","value":3}`}, + {"ArrayElement", `{"type":"Array","value":[3]}`}, + {"MapValue", `{"type":"Map","value":3}`}, + {"MapElement", `{"type":"Map","value":[{"key":"value"}]}`}, + {"MapElementKeyNotPrimitive", `{"type":"Map","value":[{"key":{"type":"Any"}}]}`}, + {"MapElementValue", `{"type":"Map","value":[` + + `{"key":{"type":"Integer","value":"3"},"value":3}]}`}, + } + for _, tc := range errCases { + t.Run(tc.name, func(t *testing.T) { + _, err := FromJSONWithTypes([]byte(tc.json)) + require.Error(t, err) + }) + } + }) +}