diff --git a/pkg/compiler/native_test.go b/pkg/compiler/native_test.go index af7e9ba54..7afd7418f 100644 --- a/pkg/compiler/native_test.go +++ b/pkg/compiler/native_test.go @@ -229,6 +229,9 @@ func TestNativeHelpersCompile(t *testing.T) { {"atoi", []string{`"4"`, "10"}}, {"atoi10", []string{`"4"`}}, {"memoryCompare", []string{"[]byte{1}", "[]byte{2}"}}, + {"memorySearch", []string{"[]byte{1}", "[]byte{2}"}}, + {"memorySearchIndex", []string{"[]byte{1}", "[]byte{2}", "3"}}, + {"memorySearchLastIndex", []string{"[]byte{1}", "[]byte{2}", "3"}}, }) } @@ -243,14 +246,21 @@ func runNativeTestCases(t *testing.T, ctr interop.ContractMD, name string, testC } func getMethod(t *testing.T, ctr interop.ContractMD, name string, params []string) interop.MethodAndPrice { + paramLen := len(params) + switch { case name == "itoa10" || name == "atoi10": name = name[:4] + case strings.HasPrefix(name, "memorySearch"): + if strings.HasSuffix(name, "LastIndex") { + paramLen += 1 // true should be appended inside of an interop + } + name = "memorySearch" default: name = strings.TrimSuffix(name, "WithData") } - md, ok := ctr.GetMethod(name, len(params)) + md, ok := ctr.GetMethod(name, paramLen) require.True(t, ok) return md } diff --git a/pkg/core/native/std.go b/pkg/core/native/std.go index 253b91037..0acaf63d1 100644 --- a/pkg/core/native/std.go +++ b/pkg/core/native/std.go @@ -111,6 +111,27 @@ func newStd() *Std { md = newMethodAndPrice(s.memoryCompare, 1<<5, callflag.NoneFlag) s.AddMethod(md, desc) + desc = newDescriptor("memorySearch", smartcontract.IntegerType, + manifest.NewParameter("mem", smartcontract.ByteArrayType), + manifest.NewParameter("value", smartcontract.ByteArrayType)) + md = newMethodAndPrice(s.memorySearch2, 1<<6, callflag.NoneFlag) + s.AddMethod(md, desc) + + desc = newDescriptor("memorySearch", smartcontract.IntegerType, + manifest.NewParameter("mem", smartcontract.ByteArrayType), + manifest.NewParameter("value", smartcontract.ByteArrayType), + manifest.NewParameter("start", smartcontract.IntegerType)) + md = newMethodAndPrice(s.memorySearch3, 1<<6, callflag.NoneFlag) + s.AddMethod(md, desc) + + desc = newDescriptor("memorySearch", smartcontract.IntegerType, + manifest.NewParameter("mem", smartcontract.ByteArrayType), + manifest.NewParameter("value", smartcontract.ByteArrayType), + manifest.NewParameter("start", smartcontract.IntegerType), + manifest.NewParameter("backward", smartcontract.BoolType)) + md = newMethodAndPrice(s.memorySearch4, 1<<6, callflag.NoneFlag) + s.AddMethod(md, desc) + return s } @@ -291,6 +312,49 @@ func (s *Std) memoryCompare(_ *interop.Context, args []stackitem.Item) stackitem return stackitem.NewBigInteger(big.NewInt(int64(bytes.Compare(s1, s2)))) } +func (s *Std) memorySearch2(_ *interop.Context, args []stackitem.Item) stackitem.Item { + mem := s.toLimitedBytes(args[0]) + val := s.toLimitedBytes(args[1]) + index := s.memorySearchAux(mem, val, 0, false) + return stackitem.NewBigInteger(big.NewInt(int64(index))) +} + +func (s *Std) memorySearch3(_ *interop.Context, args []stackitem.Item) stackitem.Item { + mem := s.toLimitedBytes(args[0]) + val := s.toLimitedBytes(args[1]) + start := toUint32(args[2]) + index := s.memorySearchAux(mem, val, int(start), false) + return stackitem.NewBigInteger(big.NewInt(int64(index))) +} + +func (s *Std) memorySearch4(_ *interop.Context, args []stackitem.Item) stackitem.Item { + mem := s.toLimitedBytes(args[0]) + val := s.toLimitedBytes(args[1]) + start := toUint32(args[2]) + backward, err := args[3].TryBool() + if err != nil { + panic(err) + } + + index := s.memorySearchAux(mem, val, int(start), backward) + return stackitem.NewBigInteger(big.NewInt(int64(index))) +} + +func (s *Std) memorySearchAux(mem, val []byte, start int, backward bool) int { + if backward { + if start > len(mem) { // panic in case if cap(mem) > len(mem) for some reasons + panic("invalid start index") + } + return bytes.LastIndex(mem[:start], val) + } + + index := bytes.Index(mem[start:], val) + if index < 0 { + return -1 + } + return index + start +} + // Metadata implements Contract interface. func (s *Std) Metadata() *interop.ContractMD { return &s.ContractMD diff --git a/pkg/core/native/std_test.go b/pkg/core/native/std_test.go index 7640c81cd..91d355975 100644 --- a/pkg/core/native/std_test.go +++ b/pkg/core/native/std_test.go @@ -389,3 +389,95 @@ func TestMemoryCompare(t *testing.T) { func() { s.memoryCompare(ic, []stackitem.Item{s2, s1}) }) }) } + +func TestMemorySearch(t *testing.T) { + s := newStd() + ic := &interop.Context{VM: vm.New()} + + check := func(t *testing.T, result int64, args ...interface{}) { + items := make([]stackitem.Item, len(args)) + for i := range args { + items[i] = stackitem.Make(args[i]) + } + + var actual stackitem.Item + switch len(items) { + case 2: + actual = s.memorySearch2(ic, items) + case 3: + actual = s.memorySearch3(ic, items) + case 4: + actual = s.memorySearch4(ic, items) + default: + panic("invalid args length") + } + require.Equal(t, big.NewInt(result), actual.Value()) + } + + t.Run("C# compatibility", func(t *testing.T) { + // These tests are taken from C# node. + check(t, 2, "abc", "c", 0) + check(t, 2, "abc", "c", 1) + check(t, 2, "abc", "c", 2) + check(t, -1, "abc", "c", 3) + check(t, -1, "abc", "d", 0) + + check(t, 2, "abc", "c", 0, false) + check(t, 2, "abc", "c", 1, false) + check(t, 2, "abc", "c", 2, false) + check(t, -1, "abc", "c", 3, false) + check(t, -1, "abc", "d", 0, false) + + check(t, -1, "abc", "c", 0, true) + check(t, -1, "abc", "c", 1, true) + check(t, -1, "abc", "c", 2, true) + check(t, 2, "abc", "c", 3, true) + check(t, -1, "abc", "d", 0, true) + }) + + t.Run("boundary indices", func(t *testing.T) { + arg := stackitem.Make("aaa") + require.Panics(t, func() { + s.memorySearch3(ic, []stackitem.Item{arg, arg, stackitem.Make(-1)}) + }) + require.Panics(t, func() { + s.memorySearch3(ic, []stackitem.Item{arg, arg, stackitem.Make(4)}) + }) + t.Run("still in capacity", func(t *testing.T) { + require.Panics(t, func() { + arr := stackitem.NewByteArray(make([]byte, 5, 10)) + s.memorySearch3(ic, []stackitem.Item{arr, arg, stackitem.Make(7)}) + }) + require.Panics(t, func() { + arr := stackitem.NewByteArray(make([]byte, 5, 10)) + s.memorySearch4(ic, []stackitem.Item{arr, arg, + stackitem.Make(7), stackitem.Make(true)}) + }) + }) + }) + + t.Run("big arguments", func(t *testing.T) { + s1 := stackitem.Make(strings.Repeat("x", stdMaxInputLength+1)) + s2 := stackitem.Make("xxx") + start := stackitem.Make(1) + b := stackitem.Make(true) + + require.PanicsWithError(t, ErrTooBigInput.Error(), + func() { s.memorySearch2(ic, []stackitem.Item{s1, s2}) }) + + require.PanicsWithError(t, ErrTooBigInput.Error(), + func() { s.memorySearch2(ic, []stackitem.Item{s2, s1}) }) + + require.PanicsWithError(t, ErrTooBigInput.Error(), + func() { s.memorySearch3(ic, []stackitem.Item{s1, s2, start}) }) + + require.PanicsWithError(t, ErrTooBigInput.Error(), + func() { s.memorySearch3(ic, []stackitem.Item{s2, s1, start}) }) + + require.PanicsWithError(t, ErrTooBigInput.Error(), + func() { s.memorySearch4(ic, []stackitem.Item{s1, s2, start, b}) }) + + require.PanicsWithError(t, ErrTooBigInput.Error(), + func() { s.memorySearch4(ic, []stackitem.Item{s2, s1, start, b}) }) + }) +} diff --git a/pkg/interop/native/std/std.go b/pkg/interop/native/std/std.go index f405a24ec..abadf8d8a 100644 --- a/pkg/interop/native/std/std.go +++ b/pkg/interop/native/std/std.go @@ -121,3 +121,24 @@ func MemoryCompare(s1, s2 []byte) int { return contract.Call(interop.Hash160(Hash), "memoryCompare", contract.NoneFlag, s1, s2).(int) } + +// MemorySearch returns index of the first occurrence of val in mem. +// If not found, -1 is returned. It uses `memorySearch` method of StdLib native contract. +func MemorySearch(mem, pattern []byte) int { + return contract.Call(interop.Hash160(Hash), "memorySearch", contract.NoneFlag, + mem, pattern).(int) +} + +// MemorySearchIndex returns index of the first occurrence of val in mem starting from start. +// If not found, -1 is returned. It uses `memorySearch` method of StdLib native contract. +func MemorySearchIndex(mem, pattern []byte, start int) int { + return contract.Call(interop.Hash160(Hash), "memorySearch", contract.NoneFlag, + mem, pattern, start).(int) +} + +// MemorySearchLastIndex returns index of the last occurrence of val in mem ending before start. +// If not found, -1 is returned. It uses `memorySearch` method of StdLib native contract. +func MemorySearchLastIndex(mem, pattern []byte, start int) int { + return contract.Call(interop.Hash160(Hash), "memorySearch", contract.NoneFlag, + mem, pattern, start, true).(int) +}