diff --git a/pkg/vm/stackitem/item.go b/pkg/vm/stackitem/item.go index 63c407948..324086dfa 100644 --- a/pkg/vm/stackitem/item.go +++ b/pkg/vm/stackitem/item.go @@ -1019,3 +1019,66 @@ func (i *Buffer) Convert(typ Type) (Item, error) { func (i *Buffer) Len() int { return len(i.value) } + +// DeepCopy returns new deep copy of the provided item. +// Values of Interop items are not deeply copied. +// It does preserve duplicates only for non-primitive types. +func DeepCopy(item Item) Item { + seen := make(map[Item]Item) + return deepCopy(item, seen) +} + +func deepCopy(item Item, seen map[Item]Item) Item { + if it := seen[item]; it != nil { + return it + } + switch it := item.(type) { + case Null: + return Null{} + case *Array: + arr := NewArray(make([]Item, len(it.value))) + seen[item] = arr + for i := range it.value { + arr.value[i] = deepCopy(it.value[i], seen) + } + return arr + case *Struct: + arr := NewStruct(make([]Item, len(it.value))) + seen[item] = arr + for i := range it.value { + arr.value[i] = deepCopy(it.value[i], seen) + } + return arr + case *Map: + m := NewMap() + seen[item] = m + for i := range it.value { + key := deepCopy(it.value[i].Key, seen) + value := deepCopy(it.value[i].Value, seen) + m.Add(key, value) + } + return m + case *BigInteger: + bi := new(big.Int).SetBytes(it.value.Bytes()) + if it.value.Sign() == -1 { + bi.Neg(bi) + } + return NewBigInteger(bi) + case *ByteArray: + val := make([]byte, len(it.value)) + copy(val, it.value) + return NewByteArray(val) + case *Buffer: + val := make([]byte, len(it.value)) + copy(val, it.value) + return NewBuffer(val) + case *Bool: + return NewBool(it.value) + case *Pointer: + return NewPointer(it.pos, it.script) + case *Interop: + return NewInterop(it.value) + default: + return nil + } +} diff --git a/pkg/vm/stackitem/item_test.go b/pkg/vm/stackitem/item_test.go index 723d0a489..43c2a03dd 100644 --- a/pkg/vm/stackitem/item_test.go +++ b/pkg/vm/stackitem/item_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var makeStackItemTestCases = []struct { @@ -412,3 +413,61 @@ func TestMarshalJSON(t *testing.T) { assert.Equal(t, testCase.result, actual) } } + +func TestDeepCopy(t *testing.T) { + testCases := []struct { + name string + item Item + }{ + {"Integer", NewBigInteger(big.NewInt(1))}, + {"ByteArray", NewByteArray([]byte{1, 2, 3})}, + {"Buffer", NewBuffer([]byte{1, 2, 3})}, + {"Bool", NewBool(true)}, + {"Pointer", NewPointer(1, []byte{1, 2, 3})}, + {"Interop", NewInterop(&[]byte{1, 2})}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual := DeepCopy(tc.item) + require.Equal(t, tc.item, actual) + require.False(t, actual == tc.item) + }) + } + + t.Run("Null", func(t *testing.T) { + require.Equal(t, Null{}, DeepCopy(Null{})) + }) + + t.Run("Array", func(t *testing.T) { + arr := NewArray(make([]Item, 2)) + arr.value[0] = NewBool(true) + arr.value[1] = arr + + actual := DeepCopy(arr) + require.Equal(t, arr, actual) + require.False(t, arr == actual) + require.True(t, actual == actual.(*Array).value[1]) + }) + + t.Run("Struct", func(t *testing.T) { + arr := NewStruct(make([]Item, 2)) + arr.value[0] = NewBool(true) + arr.value[1] = arr + + actual := DeepCopy(arr) + require.Equal(t, arr, actual) + require.False(t, arr == actual) + require.True(t, actual == actual.(*Struct).value[1]) + }) + + t.Run("Map", func(t *testing.T) { + m := NewMapWithValue(make([]MapElement, 2)) + m.value[0] = MapElement{Key: NewBool(true), Value: m} + m.value[1] = MapElement{Key: NewBigInteger(big.NewInt(1)), Value: NewByteArray([]byte{1, 2, 3})} + + actual := DeepCopy(m) + require.Equal(t, m, actual) + require.False(t, m == actual) + require.True(t, actual == actual.(*Map).value[0].Value) + }) +}