From 40d1dd0e0d1bf2625cf18353108be01a69d0be4e Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Fri, 30 Apr 2021 17:00:29 +0300 Subject: [PATCH] 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) +}