cli: unify parameters parsing

Share parameters parsing code between 'contract invokefunction' and
'vm run' commands. It allows VM CLI to parse more complicated parameter
types including arrays and file-backed bytestrings.
This commit is contained in:
Anna Shaleva 2022-10-10 14:00:26 +03:00
parent 3fba4e4f17
commit 7eb87afab8
8 changed files with 167 additions and 147 deletions

View file

@ -24,6 +24,76 @@ const (
ArrayEndSeparator = "]"
)
const (
// ParamsParsingDoc is a documentation for parameters parsing.
ParamsParsingDoc = ` Arguments always do have regular Neo smart contract parameter types, either
specified explicitly or being inferred from the value. To specify the type
manually use "type:value" syntax where the type is one of the following:
'signature', 'bool', 'int', 'hash160', 'hash256', 'bytes', 'key' or 'string'.
Array types are also supported: use special space-separated '[' and ']'
symbols around array values to denote array bounds. Nested arrays are also
supported.
There is ability to provide an argument of 'bytearray' type via file. Use a
special 'filebytes' argument type for this with a filepath specified after
the colon, e.g. 'filebytes:my_file.txt'.
Given values are type-checked against given types with the following
restrictions applied:
* 'signature' type values should be hex-encoded and have a (decoded)
length of 64 bytes.
* 'bool' type values are 'true' and 'false'.
* 'int' values are decimal integers that can be successfully converted
from the string.
* 'hash160' values are Neo addresses and hex-encoded 20-bytes long (after
decoding) strings.
* 'hash256' type values should be hex-encoded and have a (decoded)
length of 32 bytes.
* 'bytes' type values are any hex-encoded things.
* 'filebytes' type values are filenames with the argument value inside.
* 'key' type values are hex-encoded marshalled public keys.
* 'string' type values are any valid UTF-8 strings. In the value's part of
the string the colon looses it's special meaning as a separator between
type and value and is taken literally.
If no type is explicitly specified, it is inferred from the value using the
following logic:
- anything that can be interpreted as a decimal integer gets
an 'int' type
- 'true' and 'false' strings get 'bool' type
- valid Neo addresses and 20 bytes long hex-encoded strings get 'hash160'
type
- valid hex-encoded public keys get 'key' type
- 32 bytes long hex-encoded values get 'hash256' type
- 64 bytes long hex-encoded values get 'signature' type
- any other valid hex-encoded values get 'bytes' type
- anything else is a 'string'
Backslash character is used as an escape character and allows to use colon in
an implicitly typed string. For any other characters it has no special
meaning, to get a literal backslash in the string use the '\\' sequence.
Examples:
* 'int:42' is an integer with a value of 42
* '42' is an integer with a value of 42
* 'bad' is a string with a value of 'bad'
* 'dead' is a byte array with a value of 'dead'
* 'string:dead' is a string with a value of 'dead'
* 'filebytes:my_data.txt' is bytes decoded from a content of my_data.txt
* 'AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y' is a hash160 with a value
of '23ba2703c53263e8d6e522dc32203339dcd8eee9'
* '\4\2' is an integer with a value of 42
* '\\4\2' is a string with a value of '\42'
* 'string:string' is a string with a value of 'string'
* 'string\:string' is a string with a value of 'string:string'
* '03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c' is a
key with a value of '03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c'
* '[ a b c ]' is an array with strings values 'a', 'b' and 'c'
* '[ a b [ c d ] e ]' is an array with 4 values: string 'a', string 'b',
array of two strings 'c' and 'd', string 'e'
* '[ ]' is an empty array`
)
// GetSignersFromContext returns signers parsed from context args starting
// from the specified offset.
func GetSignersFromContext(ctx *cli.Context, offset int) ([]transaction.Signer, *cli.ExitError) {

View file

@ -209,72 +209,7 @@ func NewCommands() []cli.Command {
follow the regular convention of smart contract arguments (method string and
an array of other arguments).
Arguments always do have regular Neo smart contract parameter types, either
specified explicitly or being inferred from the value. To specify the type
manually use "type:value" syntax where the type is one of the following:
'signature', 'bool', 'int', 'hash160', 'hash256', 'bytes', 'key' or 'string'.
Array types are also supported: use special space-separated '[' and ']'
symbols around array values to denote array bounds. Nested arrays are also
supported.
There is ability to provide an argument of 'bytearray' type via file. Use a
special 'filebytes' argument type for this with a filepath specified after
the colon, e.g. 'filebytes:my_file.txt'.
Given values are type-checked against given types with the following
restrictions applied:
* 'signature' type values should be hex-encoded and have a (decoded)
length of 64 bytes.
* 'bool' type values are 'true' and 'false'.
* 'int' values are decimal integers that can be successfully converted
from the string.
* 'hash160' values are Neo addresses and hex-encoded 20-bytes long (after
decoding) strings.
* 'hash256' type values should be hex-encoded and have a (decoded)
length of 32 bytes.
* 'bytes' type values are any hex-encoded things.
* 'filebytes' type values are filenames with the argument value inside.
* 'key' type values are hex-encoded marshalled public keys.
* 'string' type values are any valid UTF-8 strings. In the value's part of
the string the colon looses it's special meaning as a separator between
type and value and is taken literally.
If no type is explicitly specified, it is inferred from the value using the
following logic:
- anything that can be interpreted as a decimal integer gets
an 'int' type
- 'true' and 'false' strings get 'bool' type
- valid Neo addresses and 20 bytes long hex-encoded strings get 'hash160'
type
- valid hex-encoded public keys get 'key' type
- 32 bytes long hex-encoded values get 'hash256' type
- 64 bytes long hex-encoded values get 'signature' type
- any other valid hex-encoded values get 'bytes' type
- anything else is a 'string'
Backslash character is used as an escape character and allows to use colon in
an implicitly typed string. For any other characters it has no special
meaning, to get a literal backslash in the string use the '\\' sequence.
Examples:
* 'int:42' is an integer with a value of 42
* '42' is an integer with a value of 42
* 'bad' is a string with a value of 'bad'
* 'dead' is a byte array with a value of 'dead'
* 'string:dead' is a string with a value of 'dead'
* 'filebytes:my_data.txt' is bytes decoded from a content of my_data.txt
* 'AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y' is a hash160 with a value
of '23ba2703c53263e8d6e522dc32203339dcd8eee9'
* '\4\2' is an integer with a value of 42
* '\\4\2' is a string with a value of '\42'
* 'string:string' is a string with a value of 'string'
* 'string\:string' is a string with a value of 'string:string'
* '03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c' is a
key with a value of '03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c'
* '[ a b c ]' is an array with strings values 'a', 'b' and 'c'
* '[ a b [ c d ] e ]' is an array with 4 values: string 'a', string 'b',
array of two strings 'c' and 'd', string 'e'
* '[ ]' is an empty array
` + cmdargs.ParamsParsingDoc + `
Signers represent a set of Uint160 hashes with witness scopes and are used
to verify hashes in System.Runtime.CheckWitness syscall. First signer is treated

View file

@ -18,6 +18,7 @@ import (
"github.com/chzyer/readline"
"github.com/kballard/go-shellquote"
"github.com/nspcc-dev/neo-go/cli/cmdargs"
"github.com/nspcc-dev/neo-go/cli/flags"
"github.com/nspcc-dev/neo-go/cli/options"
"github.com/nspcc-dev/neo-go/cli/paramcontext"
@ -52,11 +53,6 @@ const (
exitFuncKey = "exitFunc"
readlineInstanceKey = "readlineKey"
printLogoKey = "printLogoKey"
boolType = "bool"
boolFalse = "false"
boolTrue = "true"
intType = "int"
stringType = "string"
)
// Various flag names.
@ -221,18 +217,12 @@ and converted to other formats. Strings are escaped and output in quotes.`,
<method> is a contract method, specified in manifest. It can be '_' which will push
parameters onto the stack and execute from the current offset.
<parameter> is a parameter (can be repeated multiple times) that can be specified
as <type>:<value>, where type can be:
'` + boolType + `': supports '` + boolFalse + `' and '` + boolTrue + `' values
'` + intType + `': supports integers as values
'` + stringType + `': supports strings as values (that are pushed as a byte array
values to the stack)
or can be just <value>, for which the type will be detected automatically
following these rules: '` + boolTrue + `' and '` + boolFalse + `' are treated as respective
boolean values, everything that can be converted to integer is treated as
integer and everything else is treated like a string.
using the same rules as for 'contract testinvokefunction' command:
` + cmdargs.ParamsParsingDoc + `
Example:
> run put ` + stringType + `:"Something to put"`,
> run put string:"Something to put"`,
Action: handleRun,
},
{
@ -862,9 +852,16 @@ func handleRun(c *cli.Context) error {
runCurrent = args[0] != "_"
)
params, err = parseArgs(args[1:])
_, scParams, err := cmdargs.ParseParams(args[1:], true)
if err != nil {
return err
return fmt.Errorf("%w: %v", ErrInvalidParameter, err)
}
params = make([]stackitem.Item, len(scParams))
for i := range scParams {
params[i], err = scParams[i].ToStackItem()
if err != nil {
return fmt.Errorf("failed to convert parameter #%d to stackitem: %w", i, err)
}
}
if runCurrent {
if m == nil {
@ -1265,48 +1262,6 @@ func Parse(args []string) (string, error) {
return buf.String(), nil
}
func parseArgs(args []string) ([]stackitem.Item, error) {
items := make([]stackitem.Item, len(args))
for i, arg := range args {
var typ, value string
typeAndVal := strings.Split(arg, ":")
if len(typeAndVal) < 2 {
if typeAndVal[0] == boolFalse || typeAndVal[0] == boolTrue {
typ = boolType
} else if _, err := strconv.Atoi(typeAndVal[0]); err == nil {
typ = intType
} else {
typ = stringType
}
value = typeAndVal[0]
} else {
typ = typeAndVal[0]
value = typeAndVal[1]
}
switch typ {
case boolType:
if value == boolFalse {
items[i] = stackitem.NewBool(false)
} else if value == boolTrue {
items[i] = stackitem.NewBool(true)
} else {
return nil, fmt.Errorf("%w: invalid bool value", ErrInvalidParameter)
}
case intType:
val, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return nil, fmt.Errorf("%w: invalid integer value", ErrInvalidParameter)
}
items[i] = stackitem.NewBigInteger(big.NewInt(val))
case stringType:
items[i] = stackitem.NewByteArray([]byte(value))
}
}
return items, nil
}
const logo = `
_ ____________ __________ _ ____ ___
/ | / / ____/ __ \ / ____/ __ \ | | / / |/ /

View file

@ -8,6 +8,7 @@ import (
"errors"
"fmt"
gio "io"
"math/big"
"os"
"path/filepath"
"strings"
@ -428,6 +429,9 @@ func TestRunWithDifferentArguments(t *testing.T) {
}
func GetString(arg string) string {
return arg
}
func GetArr(arg []interface{}) []interface{}{
return arg
}`
tmpDir := t.TempDir()
@ -449,6 +453,7 @@ func TestRunWithDifferentArguments(t *testing.T) {
"run _ 1 2",
"loadbase64 "+base64.StdEncoding.EncodeToString([]byte{byte(opcode.MUL)}),
"run _ 21 2",
"loadgo "+filename, "run getArr [ 1 2 3 ]",
)
e.checkNextLine(t, "READY: loaded \\d.* instructions")
@ -480,6 +485,13 @@ func TestRunWithDifferentArguments(t *testing.T) {
e.checkNextLine(t, "READY: loaded \\d.* instructions")
e.checkStack(t, 42)
e.checkNextLine(t, "READY: loaded \\d.* instructions")
e.checkStack(t, []stackitem.Item{
stackitem.NewBigInteger(big.NewInt(1)),
stackitem.NewBigInteger(big.NewInt(2)),
stackitem.NewBigInteger(big.NewInt(3)),
})
}
func TestPrintOps(t *testing.T) {

View file

@ -403,3 +403,12 @@ func ExpandParameterToEmitable(param Parameter) (interface{}, error) {
return param.Value, nil
}
}
// ToStackItem converts smartcontract parameter to stackitem.Item.
func (p *Parameter) ToStackItem() (stackitem.Item, error) {
e, err := ExpandParameterToEmitable(*p)
if err != nil {
return nil, err
}
return stackitem.Make(e), nil
}

View file

@ -9,6 +9,8 @@ import (
"strings"
"testing"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/util"
@ -446,47 +448,57 @@ func hexToBase64(s string) string {
return base64.StdEncoding.EncodeToString(b)
}
func TestExpandParameterToEmitable(t *testing.T) {
func TestExpandParameterToEmitableToStackitem(t *testing.T) {
pk, _ := keys.NewPrivateKey()
testCases := []struct {
In Parameter
Expected interface{}
ExpectedStackitem stackitem.Item
}{
{
In: Parameter{Type: BoolType, Value: true},
Expected: true,
ExpectedStackitem: stackitem.NewBool(true),
},
{
In: Parameter{Type: IntegerType, Value: big.NewInt(123)},
Expected: big.NewInt(123),
ExpectedStackitem: stackitem.NewBigInteger(big.NewInt(123)),
},
{
In: Parameter{Type: ByteArrayType, Value: []byte{1, 2, 3}},
Expected: []byte{1, 2, 3},
ExpectedStackitem: stackitem.NewByteArray([]byte{1, 2, 3}),
},
{
In: Parameter{Type: StringType, Value: "writing's on the wall"},
Expected: "writing's on the wall",
ExpectedStackitem: stackitem.NewByteArray([]byte("writing's on the wall")),
},
{
In: Parameter{Type: Hash160Type, Value: util.Uint160{1, 2, 3}},
Expected: util.Uint160{1, 2, 3},
ExpectedStackitem: stackitem.NewByteArray(util.Uint160{1, 2, 3}.BytesBE()),
},
{
In: Parameter{Type: Hash256Type, Value: util.Uint256{1, 2, 3}},
Expected: util.Uint256{1, 2, 3},
ExpectedStackitem: stackitem.NewByteArray(util.Uint256{1, 2, 3}.BytesBE()),
},
{
In: Parameter{Type: PublicKeyType, Value: pk.PublicKey().Bytes()},
Expected: pk.PublicKey().Bytes(),
ExpectedStackitem: stackitem.NewByteArray(pk.PublicKey().Bytes()),
},
{
In: Parameter{Type: SignatureType, Value: []byte{1, 2, 3}},
Expected: []byte{1, 2, 3},
ExpectedStackitem: stackitem.NewByteArray([]byte{1, 2, 3}),
},
{
In: Parameter{Type: AnyType},
Expected: nil,
ExpectedStackitem: stackitem.Null{},
},
{
In: Parameter{Type: ArrayType, Value: []Parameter{
@ -509,6 +521,13 @@ func TestExpandParameterToEmitable(t *testing.T) {
},
}},
Expected: []interface{}{big.NewInt(123), []byte{1, 2, 3}, []interface{}{true}},
ExpectedStackitem: stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(123)),
stackitem.NewByteArray([]byte{1, 2, 3}),
stackitem.NewArray([]stackitem.Item{
stackitem.NewBool(true),
}),
}),
},
}
bw := io.NewBufBinWriter()
@ -519,6 +538,10 @@ func TestExpandParameterToEmitable(t *testing.T) {
emit.Array(bw.BinWriter, actual)
require.NoError(t, bw.Err)
actualSI, err := testCase.In.ToStackItem()
require.NoError(t, err)
require.Equal(t, testCase.ExpectedStackitem, actualSI)
}
errCases := []Parameter{
{Type: UnknownType},

View file

@ -122,6 +122,18 @@ func Make(v interface{}) Item {
a = append(a, Make(i))
}
return Make(a)
case []interface{}:
res := make([]Item, len(val))
for i := range val {
res[i] = Make(val[i])
}
return Make(res)
case util.Uint160:
return Make(val.BytesBE())
case util.Uint256:
return Make(val.BytesBE())
case nil:
return Null{}
default:
i64T := reflect.TypeOf(int64(0))
if reflect.TypeOf(val).ConvertibleTo(i64T) {

View file

@ -77,13 +77,17 @@ var makeStackItemTestCases = []struct {
input: []int{1, 2, 3},
result: &Array{value: []Item{(*BigInteger)(big.NewInt(1)), (*BigInteger)(big.NewInt(2)), (*BigInteger)(big.NewInt(3))}},
},
{
input: nil,
result: Null{},
},
}
var makeStackItemErrorCases = []struct {
input interface{}
}{
{
input: nil,
input: map[int]int{1: 2},
},
}