From e4b34833da5ef498f0d102b58fe6ebfb049c89d6 Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Fri, 30 Apr 2021 11:22:00 +0300 Subject: [PATCH 1/5] native/std: add overloads for `itoa` and `atoi` --- pkg/compiler/native_test.go | 19 ++++++++++++++++--- pkg/core/native/std.go | 35 ++++++++++++++++++++++++++++++----- pkg/core/native/std_test.go | 12 ++++++++++++ pkg/interop/native/std/std.go | 14 ++++++++++++++ 4 files changed, 72 insertions(+), 8 deletions(-) diff --git a/pkg/compiler/native_test.go b/pkg/compiler/native_test.go index 4f7ae97e2..23f6aa037 100644 --- a/pkg/compiler/native_test.go +++ b/pkg/compiler/native_test.go @@ -225,7 +225,9 @@ func TestNativeHelpersCompile(t *testing.T) { {"base58Encode", []string{"[]byte{1, 2, 3}"}}, {"base58Decode", []string{"[]byte{1, 2, 3}"}}, {"itoa", []string{"4", "10"}}, + {"itoa10", []string{"4"}}, {"atoi", []string{`"4"`, "10"}}, + {"atoi10", []string{`"4"`}}, }) } @@ -239,10 +241,21 @@ func runNativeTestCases(t *testing.T, ctr interop.ContractMD, name string, testC }) } -func runNativeTestCase(t *testing.T, ctr interop.ContractMD, name, method string, params ...string) { - md, ok := ctr.GetMethod(strings.TrimSuffix(method, "WithData"), len(params)) - require.True(t, ok) +func getMethod(t *testing.T, ctr interop.ContractMD, name string, params []string) interop.MethodAndPrice { + switch { + case name == "itoa10" || name == "atoi10": + name = name[:4] + default: + name = strings.TrimSuffix(name, "WithData") + } + md, ok := ctr.GetMethod(name, len(params)) + require.True(t, ok) + return md +} + +func runNativeTestCase(t *testing.T, ctr interop.ContractMD, name, method string, params ...string) { + md := getMethod(t, ctr, method, params) isVoid := md.MD.ReturnType == smartcontract.VoidType srcTmpl := `package foo import "github.com/nspcc-dev/neo-go/pkg/interop/native/%s" diff --git a/pkg/core/native/std.go b/pkg/core/native/std.go index ed559000f..8c386c4c9 100644 --- a/pkg/core/native/std.go +++ b/pkg/core/native/std.go @@ -61,12 +61,22 @@ func newStd() *Std { md = newMethodAndPrice(s.itoa, 1<<12, callflag.NoneFlag) s.AddMethod(md, desc) + desc = newDescriptor("itoa", smartcontract.StringType, + manifest.NewParameter("value", smartcontract.IntegerType)) + md = newMethodAndPrice(s.itoa10, 1<<12, callflag.NoneFlag) + s.AddMethod(md, desc) + desc = newDescriptor("atoi", smartcontract.IntegerType, manifest.NewParameter("value", smartcontract.StringType), manifest.NewParameter("base", smartcontract.IntegerType)) md = newMethodAndPrice(s.atoi, 1<<12, callflag.NoneFlag) s.AddMethod(md, desc) + desc = newDescriptor("atoi", smartcontract.IntegerType, + manifest.NewParameter("value", smartcontract.StringType)) + md = newMethodAndPrice(s.atoi10, 1<<12, callflag.NoneFlag) + s.AddMethod(md, desc) + desc = newDescriptor("base64Encode", smartcontract.StringType, manifest.NewParameter("data", smartcontract.ByteArrayType)) md = newMethodAndPrice(s.base64Encode, 1<<12, callflag.NoneFlag) @@ -142,6 +152,11 @@ func (s *Std) jsonDeserialize(_ *interop.Context, args []stackitem.Item) stackit return item } +func (s *Std) itoa10(_ *interop.Context, args []stackitem.Item) stackitem.Item { + num := toBigInt(args[0]) + return stackitem.NewByteArray([]byte(num.Text(10))) +} + func (s *Std) itoa(_ *interop.Context, args []stackitem.Item) stackitem.Item { num := toBigInt(args[0]) base := toBigInt(args[1]) @@ -170,6 +185,20 @@ func (s *Std) itoa(_ *interop.Context, args []stackitem.Item) stackitem.Item { return stackitem.NewByteArray([]byte(str)) } +func (s *Std) atoi10(_ *interop.Context, args []stackitem.Item) stackitem.Item { + num := toString(args[0]) + res := s.atoi10Aux(num) + return stackitem.NewBigInteger(res) +} + +func (s *Std) atoi10Aux(num string) *big.Int { + bi, ok := new(big.Int).SetString(num, 10) + if !ok { + panic(ErrInvalidFormat) + } + return bi +} + func (s *Std) atoi(_ *interop.Context, args []stackitem.Item) stackitem.Item { num := toString(args[0]) base := toBigInt(args[1]) @@ -179,11 +208,7 @@ func (s *Std) atoi(_ *interop.Context, args []stackitem.Item) stackitem.Item { var bi *big.Int switch b := base.Int64(); b { case 10: - var ok bool - bi, ok = new(big.Int).SetString(num, int(b)) - if !ok { - panic(ErrInvalidFormat) - } + bi = s.atoi10Aux(num) case 16: changed := len(num)%2 != 0 if changed { diff --git a/pkg/core/native/std_test.go b/pkg/core/native/std_test.go index f5a3bf15c..bfe4e3f43 100644 --- a/pkg/core/native/std_test.go +++ b/pkg/core/native/std_test.go @@ -49,6 +49,18 @@ func TestStdLibItoaAtoi(t *testing.T) { actual = s.atoi(ic, []stackitem.Item{stackitem.Make(tc.result), stackitem.Make(tc.base)}) }) require.Equal(t, stackitem.Make(tc.num), actual) + + if tc.base.Int64() == 10 { + require.NotPanics(t, func() { + actual = s.itoa10(ic, []stackitem.Item{stackitem.Make(tc.num)}) + }) + require.Equal(t, stackitem.Make(tc.result), actual) + + require.NotPanics(t, func() { + actual = s.atoi10(ic, []stackitem.Item{stackitem.Make(tc.result)}) + }) + require.Equal(t, stackitem.Make(tc.num), actual) + } } t.Run("-1", func(t *testing.T) { diff --git a/pkg/interop/native/std/std.go b/pkg/interop/native/std/std.go index fa5f4dadc..e20ce2219 100644 --- a/pkg/interop/native/std/std.go +++ b/pkg/interop/native/std/std.go @@ -93,9 +93,23 @@ func Itoa(num int, base int) string { num, base).(string) } +// Itoa10 converts num in a base 10 to string. +// It uses `itoa` method of StdLib native contract. +func Itoa10(num int) string { + return contract.Call(interop.Hash160(Hash), "itoa", contract.NoneFlag, + num).(string) +} + // Atoi converts string to a number in a given base. Base should be either 10 or 16. // It uses `atoi` method of StdLib native contract. func Atoi(s string, base int) int { return contract.Call(interop.Hash160(Hash), "atoi", contract.NoneFlag, s, base).(int) } + +// Atoi10 converts string to a number in a base 10. +// It uses `atoi` method of StdLib native contract. +func Atoi10(s string) int { + return contract.Call(interop.Hash160(Hash), "atoi", contract.NoneFlag, + s).(int) +} From 82a6c3266c34bd2c5adab81177863dc29f42f35b Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Fri, 30 Apr 2021 11:44:06 +0300 Subject: [PATCH 2/5] native/std: limit input size for some methods Also fix prices accordingly. --- pkg/core/native/std.go | 58 +++++++++++++++++++++++++------------ pkg/core/native/std_test.go | 16 ++++++++++ 2 files changed, 55 insertions(+), 19 deletions(-) diff --git a/pkg/core/native/std.go b/pkg/core/native/std.go index 8c386c4c9..96bd3bc78 100644 --- a/pkg/core/native/std.go +++ b/pkg/core/native/std.go @@ -22,13 +22,20 @@ type Std struct { interop.ContractMD } -const stdContractID = -2 +const ( + stdContractID = -2 + + // stdMaxInputLength is the maximum input length for string-related methods. + stdMaxInputLength = 1024 +) var ( // ErrInvalidBase is returned when base is invalid. ErrInvalidBase = errors.New("invalid base") // ErrInvalidFormat is returned when string is not a number. ErrInvalidFormat = errors.New("invalid format") + // ErrTooBigInput is returned when input exceeds size limit. + ErrTooBigInput = errors.New("input is too big") ) func newStd() *Std { @@ -69,32 +76,32 @@ func newStd() *Std { desc = newDescriptor("atoi", smartcontract.IntegerType, manifest.NewParameter("value", smartcontract.StringType), manifest.NewParameter("base", smartcontract.IntegerType)) - md = newMethodAndPrice(s.atoi, 1<<12, callflag.NoneFlag) + md = newMethodAndPrice(s.atoi, 1<<6, callflag.NoneFlag) s.AddMethod(md, desc) desc = newDescriptor("atoi", smartcontract.IntegerType, manifest.NewParameter("value", smartcontract.StringType)) - md = newMethodAndPrice(s.atoi10, 1<<12, callflag.NoneFlag) + md = newMethodAndPrice(s.atoi10, 1<<6, callflag.NoneFlag) s.AddMethod(md, desc) desc = newDescriptor("base64Encode", smartcontract.StringType, manifest.NewParameter("data", smartcontract.ByteArrayType)) - md = newMethodAndPrice(s.base64Encode, 1<<12, callflag.NoneFlag) + md = newMethodAndPrice(s.base64Encode, 1<<5, callflag.NoneFlag) s.AddMethod(md, desc) desc = newDescriptor("base64Decode", smartcontract.ByteArrayType, manifest.NewParameter("s", smartcontract.StringType)) - md = newMethodAndPrice(s.base64Decode, 1<<12, callflag.NoneFlag) + md = newMethodAndPrice(s.base64Decode, 1<<5, callflag.NoneFlag) s.AddMethod(md, desc) desc = newDescriptor("base58Encode", smartcontract.StringType, manifest.NewParameter("data", smartcontract.ByteArrayType)) - md = newMethodAndPrice(s.base58Encode, 1<<12, callflag.NoneFlag) + md = newMethodAndPrice(s.base58Encode, 1<<13, callflag.NoneFlag) s.AddMethod(md, desc) desc = newDescriptor("base58Decode", smartcontract.ByteArrayType, manifest.NewParameter("s", smartcontract.StringType)) - md = newMethodAndPrice(s.base58Decode, 1<<12, callflag.NoneFlag) + md = newMethodAndPrice(s.base58Decode, 1<<10, callflag.NoneFlag) s.AddMethod(md, desc) return s @@ -186,7 +193,7 @@ func (s *Std) itoa(_ *interop.Context, args []stackitem.Item) stackitem.Item { } func (s *Std) atoi10(_ *interop.Context, args []stackitem.Item) stackitem.Item { - num := toString(args[0]) + num := s.toLimitedString(args[0]) res := s.atoi10Aux(num) return stackitem.NewBigInteger(res) } @@ -200,7 +207,7 @@ func (s *Std) atoi10Aux(num string) *big.Int { } func (s *Std) atoi(_ *interop.Context, args []stackitem.Item) stackitem.Item { - num := toString(args[0]) + num := s.toLimitedString(args[0]) base := toBigInt(args[1]) if !base.IsInt64() { panic(ErrInvalidBase) @@ -238,17 +245,14 @@ func reverse(b []byte) { } func (s *Std) base64Encode(_ *interop.Context, args []stackitem.Item) stackitem.Item { - src, err := args[0].TryBytes() - if err != nil { - panic(err) - } + src := s.toLimitedBytes(args[0]) result := base64.StdEncoding.EncodeToString(src) return stackitem.NewByteArray([]byte(result)) } func (s *Std) base64Decode(_ *interop.Context, args []stackitem.Item) stackitem.Item { - src := toString(args[0]) + src := s.toLimitedString(args[0]) result, err := base64.StdEncoding.DecodeString(src) if err != nil { panic(err) @@ -258,17 +262,14 @@ func (s *Std) base64Decode(_ *interop.Context, args []stackitem.Item) stackitem. } func (s *Std) base58Encode(_ *interop.Context, args []stackitem.Item) stackitem.Item { - src, err := args[0].TryBytes() - if err != nil { - panic(err) - } + src := s.toLimitedBytes(args[0]) result := base58.Encode(src) return stackitem.NewByteArray([]byte(result)) } func (s *Std) base58Decode(_ *interop.Context, args []stackitem.Item) stackitem.Item { - src := toString(args[0]) + src := s.toLimitedString(args[0]) result, err := base58.Decode(src) if err != nil { panic(err) @@ -296,3 +297,22 @@ func (s *Std) OnPersist(ic *interop.Context) error { func (s *Std) PostPersist(ic *interop.Context) error { return nil } + +func (s *Std) toLimitedBytes(item stackitem.Item) []byte { + src, err := item.TryBytes() + if err != nil { + panic(err) + } + if len(src) > stdMaxInputLength { + panic(ErrTooBigInput) + } + return src +} + +func (s *Std) toLimitedString(item stackitem.Item) string { + src := toString(item) + if len(src) > stdMaxInputLength { + panic(ErrTooBigInput) + } + return src +} diff --git a/pkg/core/native/std_test.go b/pkg/core/native/std_test.go index bfe4e3f43..49a24b37f 100644 --- a/pkg/core/native/std_test.go +++ b/pkg/core/native/std_test.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "math" "math/big" + "strings" "testing" "github.com/mr-tron/base58" @@ -102,6 +103,7 @@ func TestStdLibItoaAtoi(t *testing.T) { {"1_000", big.NewInt(10), ErrInvalidFormat}, {"FE", big.NewInt(10), ErrInvalidFormat}, {"XD", big.NewInt(16), ErrInvalidFormat}, + {strings.Repeat("0", stdMaxInputLength+1), big.NewInt(10), ErrTooBigInput}, } for _, tc := range testCases { @@ -164,18 +166,28 @@ func TestStdLibEncodeDecode(t *testing.T) { ic := &interop.Context{VM: vm.New()} var actual stackitem.Item + bigInputArgs := []stackitem.Item{stackitem.Make(strings.Repeat("6", stdMaxInputLength+1))} + t.Run("Encode64", func(t *testing.T) { require.NotPanics(t, func() { actual = s.base64Encode(ic, []stackitem.Item{stackitem.Make(original)}) }) require.Equal(t, stackitem.Make(encoded64), actual) }) + t.Run("Encode64/error", func(t *testing.T) { + require.PanicsWithError(t, ErrTooBigInput.Error(), + func() { s.base64Encode(ic, bigInputArgs) }) + }) t.Run("Encode58", func(t *testing.T) { require.NotPanics(t, func() { actual = s.base58Encode(ic, []stackitem.Item{stackitem.Make(original)}) }) require.Equal(t, stackitem.Make(encoded58), actual) }) + t.Run("Encode58/error", func(t *testing.T) { + require.PanicsWithError(t, ErrTooBigInput.Error(), + func() { s.base58Encode(ic, bigInputArgs) }) + }) t.Run("Decode64/positive", func(t *testing.T) { require.NotPanics(t, func() { actual = s.base64Decode(ic, []stackitem.Item{stackitem.Make(encoded64)}) @@ -189,6 +201,8 @@ func TestStdLibEncodeDecode(t *testing.T) { require.Panics(t, func() { _ = s.base64Decode(ic, []stackitem.Item{stackitem.NewInterop(nil)}) }) + require.PanicsWithError(t, ErrTooBigInput.Error(), + func() { s.base64Decode(ic, bigInputArgs) }) }) t.Run("Decode58/positive", func(t *testing.T) { require.NotPanics(t, func() { @@ -203,6 +217,8 @@ func TestStdLibEncodeDecode(t *testing.T) { require.Panics(t, func() { _ = s.base58Decode(ic, []stackitem.Item{stackitem.NewInterop(nil)}) }) + require.PanicsWithError(t, ErrTooBigInput.Error(), + func() { s.base58Decode(ic, bigInputArgs) }) }) } From 978f4dfbc5e613fa4f883c81860a0417885988b5 Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Fri, 30 Apr 2021 15:02:44 +0300 Subject: [PATCH 3/5] native/std: add `memoryCompare` method --- pkg/compiler/native_test.go | 1 + pkg/core/native/std.go | 13 +++++++++++++ pkg/core/native/std_test.go | 35 +++++++++++++++++++++++++++++++++++ pkg/interop/native/std/std.go | 8 ++++++++ 4 files changed, 57 insertions(+) diff --git a/pkg/compiler/native_test.go b/pkg/compiler/native_test.go index 23f6aa037..af7e9ba54 100644 --- a/pkg/compiler/native_test.go +++ b/pkg/compiler/native_test.go @@ -228,6 +228,7 @@ func TestNativeHelpersCompile(t *testing.T) { {"itoa10", []string{"4"}}, {"atoi", []string{`"4"`, "10"}}, {"atoi10", []string{`"4"`}}, + {"memoryCompare", []string{"[]byte{1}", "[]byte{2}"}}, }) } diff --git a/pkg/core/native/std.go b/pkg/core/native/std.go index 96bd3bc78..253b91037 100644 --- a/pkg/core/native/std.go +++ b/pkg/core/native/std.go @@ -1,6 +1,7 @@ package native import ( + "bytes" "encoding/base64" "encoding/hex" "errors" @@ -104,6 +105,12 @@ func newStd() *Std { md = newMethodAndPrice(s.base58Decode, 1<<10, callflag.NoneFlag) s.AddMethod(md, desc) + desc = newDescriptor("memoryCompare", smartcontract.IntegerType, + manifest.NewParameter("str1", smartcontract.ByteArrayType), + manifest.NewParameter("str2", smartcontract.ByteArrayType)) + md = newMethodAndPrice(s.memoryCompare, 1<<5, callflag.NoneFlag) + s.AddMethod(md, desc) + return s } @@ -278,6 +285,12 @@ func (s *Std) base58Decode(_ *interop.Context, args []stackitem.Item) stackitem. return stackitem.NewByteArray(result) } +func (s *Std) memoryCompare(_ *interop.Context, args []stackitem.Item) stackitem.Item { + s1 := s.toLimitedBytes(args[0]) + s2 := s.toLimitedBytes(args[1]) + return stackitem.NewBigInteger(big.NewInt(int64(bytes.Compare(s1, s2)))) +} + // 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 49a24b37f..7640c81cd 100644 --- a/pkg/core/native/std_test.go +++ b/pkg/core/native/std_test.go @@ -354,3 +354,38 @@ func TestStdLibSerializeDeserialize(t *testing.T) { }) }) } + +func TestMemoryCompare(t *testing.T) { + s := newStd() + ic := &interop.Context{VM: vm.New()} + + check := func(t *testing.T, result int64, s1, s2 string) { + actual := s.memoryCompare(ic, []stackitem.Item{stackitem.Make(s1), stackitem.Make(s2)}) + require.Equal(t, big.NewInt(result), actual.Value()) + } + + check(t, -1, "a", "ab") + check(t, 1, "ab", "a") + check(t, 0, "ab", "ab") + check(t, -1, "", "a") + check(t, 0, "", "") + + t.Run("C# compatibility", func(t *testing.T) { + // These tests are taken from C# node. + check(t, -1, "abc", "c") + check(t, -1, "abc", "d") + check(t, 0, "abc", "abc") + check(t, -1, "abc", "abcd") + }) + + t.Run("big arguments", func(t *testing.T) { + s1 := stackitem.Make(strings.Repeat("x", stdMaxInputLength+1)) + s2 := stackitem.Make("xxx") + + require.PanicsWithError(t, ErrTooBigInput.Error(), + func() { s.memoryCompare(ic, []stackitem.Item{s1, s2}) }) + + require.PanicsWithError(t, ErrTooBigInput.Error(), + func() { s.memoryCompare(ic, []stackitem.Item{s2, s1}) }) + }) +} diff --git a/pkg/interop/native/std/std.go b/pkg/interop/native/std/std.go index e20ce2219..f405a24ec 100644 --- a/pkg/interop/native/std/std.go +++ b/pkg/interop/native/std/std.go @@ -113,3 +113,11 @@ func Atoi10(s string) int { return contract.Call(interop.Hash160(Hash), "atoi", contract.NoneFlag, s).(int) } + +// MemoryCompare is similar to bytes.Compare: +// The result will be 0 if a==b, -1 if a < b, and +1 if a > b. +// It uses `memoryCompare` method of StdLib native contract. +func MemoryCompare(s1, s2 []byte) int { + return contract.Call(interop.Hash160(Hash), "memoryCompare", contract.NoneFlag, + s1, s2).(int) +} From dadfe2b9abc8dc4b2180171802cdf57ed0514515 Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Fri, 30 Apr 2021 15:33:07 +0300 Subject: [PATCH 4/5] native/std: add `memorySearch` method --- pkg/compiler/native_test.go | 12 ++++- pkg/core/native/std.go | 64 ++++++++++++++++++++++++ pkg/core/native/std_test.go | 92 +++++++++++++++++++++++++++++++++++ pkg/interop/native/std/std.go | 21 ++++++++ 4 files changed, 188 insertions(+), 1 deletion(-) 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) +} From 40d1dd0e0d1bf2625cf18353108be01a69d0be4e Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Fri, 30 Apr 2021 17:00:29 +0300 Subject: [PATCH 5/5] native/std: add `stringSplit` method --- pkg/compiler/native_test.go | 7 ++++++ pkg/core/native/std.go | 43 ++++++++++++++++++++++++++++++++++ pkg/core/native/std_test.go | 44 +++++++++++++++++++++++++++++++++++ pkg/interop/native/std/std.go | 14 +++++++++++ 4 files changed, 108 insertions(+) diff --git a/pkg/compiler/native_test.go b/pkg/compiler/native_test.go index 7afd7418f..03c3fc77b 100644 --- a/pkg/compiler/native_test.go +++ b/pkg/compiler/native_test.go @@ -232,6 +232,8 @@ func TestNativeHelpersCompile(t *testing.T) { {"memorySearch", []string{"[]byte{1}", "[]byte{2}"}}, {"memorySearchIndex", []string{"[]byte{1}", "[]byte{2}", "3"}}, {"memorySearchLastIndex", []string{"[]byte{1}", "[]byte{2}", "3"}}, + {"stringSplit", []string{`"a,b"`, `","`}}, + {"stringSplitNonEmpty", []string{`"a,b"`, `","`}}, }) } @@ -256,6 +258,11 @@ func getMethod(t *testing.T, ctr interop.ContractMD, name string, params []strin paramLen += 1 // true should be appended inside of an interop } name = "memorySearch" + case strings.HasPrefix(name, "stringSplit"): + if strings.HasSuffix(name, "NonEmpty") { + paramLen += 1 // true should be appended inside of an interop + } + name = "stringSplit" default: name = strings.TrimSuffix(name, "WithData") } diff --git a/pkg/core/native/std.go b/pkg/core/native/std.go index 0acaf63d1..243624753 100644 --- a/pkg/core/native/std.go +++ b/pkg/core/native/std.go @@ -132,6 +132,19 @@ func newStd() *Std { md = newMethodAndPrice(s.memorySearch4, 1<<6, callflag.NoneFlag) s.AddMethod(md, desc) + desc = newDescriptor("stringSplit", smartcontract.ArrayType, + manifest.NewParameter("str", smartcontract.StringType), + manifest.NewParameter("separator", smartcontract.StringType)) + md = newMethodAndPrice(s.stringSplit2, 1<<8, callflag.NoneFlag) + s.AddMethod(md, desc) + + desc = newDescriptor("stringSplit", smartcontract.ArrayType, + manifest.NewParameter("str", smartcontract.StringType), + manifest.NewParameter("separator", smartcontract.StringType), + manifest.NewParameter("removeEmptyEntries", smartcontract.BoolType)) + md = newMethodAndPrice(s.stringSplit3, 1<<8, callflag.NoneFlag) + s.AddMethod(md, desc) + return s } @@ -355,6 +368,36 @@ func (s *Std) memorySearchAux(mem, val []byte, start int, backward bool) int { return index + start } +func (s *Std) stringSplit2(_ *interop.Context, args []stackitem.Item) stackitem.Item { + str := s.toLimitedString(args[0]) + sep := toString(args[1]) + return stackitem.NewArray(s.stringSplitAux(str, sep, false)) +} + +func (s *Std) stringSplit3(_ *interop.Context, args []stackitem.Item) stackitem.Item { + str := s.toLimitedString(args[0]) + sep := toString(args[1]) + removeEmpty, err := args[2].TryBool() + if err != nil { + panic(err) + } + + return stackitem.NewArray(s.stringSplitAux(str, sep, removeEmpty)) +} + +func (s *Std) stringSplitAux(str, sep string, removeEmpty bool) []stackitem.Item { + var result []stackitem.Item + + arr := strings.Split(str, sep) + for i := range arr { + if !removeEmpty || len(arr[i]) != 0 { + result = append(result, stackitem.Make(arr[i])) + } + } + + return result +} + // 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 91d355975..68021c665 100644 --- a/pkg/core/native/std_test.go +++ b/pkg/core/native/std_test.go @@ -481,3 +481,47 @@ func TestMemorySearch(t *testing.T) { func() { s.memorySearch4(ic, []stackitem.Item{s2, s1, start, b}) }) }) } + +func TestStringSplit(t *testing.T) { + s := newStd() + ic := &interop.Context{VM: vm.New()} + + check := func(t *testing.T, result []string, str, sep string, remove interface{}) { + args := []stackitem.Item{stackitem.Make(str), stackitem.Make(sep)} + var actual stackitem.Item + if remove == nil { + actual = s.stringSplit2(ic, args) + } else { + args = append(args, stackitem.NewBool(remove.(bool))) + actual = s.stringSplit3(ic, args) + } + + arr, ok := actual.Value().([]stackitem.Item) + require.True(t, ok) + require.Equal(t, len(result), len(arr)) + for i := range result { + require.Equal(t, stackitem.Make(result[i]), arr[i]) + } + } + + check(t, []string{"a", "b", "c"}, "abc", "", nil) + check(t, []string{"a", "b", "c"}, "abc", "", true) + check(t, []string{"a", "c", "", "", "d"}, "abcbbbd", "b", nil) + check(t, []string{"a", "c", "", "", "d"}, "abcbbbd", "b", false) + check(t, []string{"a", "c", "d"}, "abcbbbd", "b", true) + check(t, []string{""}, "", "abc", nil) + check(t, []string{}, "", "abc", true) + + t.Run("C# compatibility", func(t *testing.T) { + // These tests are taken from C# node. + check(t, []string{"a", "b"}, "a,b", ",", nil) + }) + + t.Run("big arguments", func(t *testing.T) { + s1 := stackitem.Make(strings.Repeat("x", stdMaxInputLength+1)) + s2 := stackitem.Make("xxx") + + require.PanicsWithError(t, ErrTooBigInput.Error(), + func() { s.stringSplit2(ic, []stackitem.Item{s1, s2}) }) + }) +} diff --git a/pkg/interop/native/std/std.go b/pkg/interop/native/std/std.go index abadf8d8a..af263c8ba 100644 --- a/pkg/interop/native/std/std.go +++ b/pkg/interop/native/std/std.go @@ -142,3 +142,17 @@ func MemorySearchLastIndex(mem, pattern []byte, start int) int { return contract.Call(interop.Hash160(Hash), "memorySearch", contract.NoneFlag, mem, pattern, start, true).(int) } + +// StringSplit splits s by occurrences of sep. +// It uses `stringSplit` method of StdLib native contract. +func StringSplit(s, sep string) []string { + return contract.Call(interop.Hash160(Hash), "stringSplit", contract.NoneFlag, + s, sep).([]string) +} + +// StringSplitNonEmpty splits s by occurrences of sep and returns a list of non-empty items. +// It uses `stringSplit` method of StdLib native contract. +func StringSplitNonEmpty(s, sep string) []string { + return contract.Call(interop.Hash160(Hash), "stringSplit", contract.NoneFlag, + s, sep, true).([]string) +}