From 401d0d72c9c8b87df84a0e9f1f72deb73f10d3ff Mon Sep 17 00:00:00 2001 From: Jeancarlo Barrios Date: Thu, 6 Apr 2023 10:13:55 -0600 Subject: [PATCH] feat(autocli): add support for binary file hex encoding (#15504) Co-authored-by: Marko --- client/v2/autocli/flag/binary.go | 63 ++++++++++++++++ client/v2/autocli/flag/field.go | 2 + client/v2/autocli/flag/simple.go | 3 - client/v2/autocli/query_test.go | 74 +++++++++++++++++++ client/v2/autocli/testdata/file.test | 1 + .../testdata/help-deprecated-msg.golden | 4 +- .../autocli/testdata/help-deprecated.golden | 4 +- .../v2/autocli/testdata/help-echo-msg.golden | 4 +- client/v2/autocli/testdata/help-echo.golden | 4 +- 9 files changed, 148 insertions(+), 11 deletions(-) create mode 100644 client/v2/autocli/flag/binary.go create mode 100644 client/v2/autocli/testdata/file.test diff --git a/client/v2/autocli/flag/binary.go b/client/v2/autocli/flag/binary.go new file mode 100644 index 0000000000..55bf1675ad --- /dev/null +++ b/client/v2/autocli/flag/binary.go @@ -0,0 +1,63 @@ +package flag + +import ( + "context" + "encoding/base64" + "encoding/hex" + "github.com/cockroachdb/errors" + "google.golang.org/protobuf/reflect/protoreflect" + "os" +) + +type binaryType struct{} + +var _ Value = (*fileBinaryValue)(nil) + +func (f binaryType) NewValue(_ context.Context, _ *Builder) Value { + return &fileBinaryValue{} +} + +func (f binaryType) DefaultValue() string { + return "" +} + +// fileBinaryValue is a Value that holds a binary file. +type fileBinaryValue struct { + value []byte +} + +func (f *fileBinaryValue) Get(protoreflect.Value) (protoreflect.Value, error) { + return protoreflect.ValueOfBytes(f.value), nil +} + +func (f *fileBinaryValue) String() string { + return string(f.value) +} + +// Set implements the flag.Value interface for binary files, with exceptions. +// If the input string is a valid file path, the value will be the content of that file. +// If the input string is a valid hex or base64 string, the value will be the decoded form of that string. +// If the input string is not a valid file path, hex string, or base64 string, Set will return an error. +func (f *fileBinaryValue) Set(s string) error { + if data, err := os.ReadFile(s); err == nil { + f.value = data + return nil + } + + if data, err := hex.DecodeString(s); err == nil { + f.value = data + return nil + } + + if data, err := base64.StdEncoding.DecodeString(s); err == nil { + f.value = data + return nil + } + + return errors.New("input string is neither a valid file path, hex, or base64 encoded") + +} + +func (f *fileBinaryValue) Type() string { + return "binary" +} diff --git a/client/v2/autocli/flag/field.go b/client/v2/autocli/flag/field.go index b2c385ac95..40577e4ee7 100644 --- a/client/v2/autocli/flag/field.go +++ b/client/v2/autocli/flag/field.go @@ -98,6 +98,8 @@ func (b *Builder) resolveFlagTypeBasic(field protoreflect.FieldDescriptor) Type } switch field.Kind() { + case protoreflect.BytesKind: + return binaryType{} case protoreflect.EnumKind: return enumType{enum: field.Enum()} case protoreflect.MessageKind: diff --git a/client/v2/autocli/flag/simple.go b/client/v2/autocli/flag/simple.go index d4ed6d4464..a64c565e22 100644 --- a/client/v2/autocli/flag/simple.go +++ b/client/v2/autocli/flag/simple.go @@ -7,9 +7,6 @@ import ( func bindSimpleFlag(flagSet *pflag.FlagSet, kind protoreflect.Kind, name, shorthand, usage string) HasValue { switch kind { - case protoreflect.BytesKind: - val := flagSet.BytesBase64P(name, shorthand, nil, usage) - return newSimpleValue(val, protoreflect.ValueOfBytes) case protoreflect.StringKind: val := flagSet.StringP(name, shorthand, "", usage) return newSimpleValue(val, protoreflect.ValueOfString) diff --git a/client/v2/autocli/query_test.go b/client/v2/autocli/query_test.go index 34f59bac07..3dafa05dfa 100644 --- a/client/v2/autocli/query_test.go +++ b/client/v2/autocli/query_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "os" "strings" "testing" @@ -78,6 +79,9 @@ var testCmdDesc = &autocliv1.ServiceCommandDescriptor{ "duration": { Usage: "some random duration", }, + "bz": { + Usage: "some bytes", + }, }, }, }, @@ -196,6 +200,75 @@ func TestOptions(t *testing.T) { assert.Equal(t, uint64(5), lastReq.U64) // no opt default value got set } +func TestBinaryFlag(t *testing.T) { + // Create a temporary file with some content + tempFile, err := os.Open("testdata/file.test") + if err != nil { + t.Fatal(err) + } + content := []byte("this is just a test file") + if err := tempFile.Close(); err != nil { + t.Fatal(err) + } + + // Test cases + tests := []struct { + name string + input string + expected []byte + hasError bool + err string + }{ + { + name: "Valid file path with extension", + input: tempFile.Name(), + expected: content, + hasError: false, + err: "", + }, + { + name: "Valid hex-encoded string", + input: "68656c6c6f20776f726c64", + expected: []byte("hello world"), + hasError: false, + err: "", + }, + { + name: "Valid base64-encoded string", + input: "SGVsbG8gV29ybGQ=", + expected: []byte("Hello World"), + hasError: false, + err: "", + }, + { + name: "Invalid input (not a file path or encoded string)", + input: "not a file or encoded string", + expected: nil, + hasError: true, + err: "input string is neither a valid file path, hex, or base64 encoded", + }, + } + + // Run test cases + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + conn := testExecCommon(t, buildModuleQueryCommand, + "echo", + "1", "abc", `{"denom":"foo","amount":"1"}`, + "--bz", tc.input, + ) + errorOut := conn.errorOut.String() + if errorOut == "" { + lastReq := conn.lastRequest.(*testpb.EchoRequest) + assert.DeepEqual(t, tc.expected, lastReq.Bz) + } else { + assert.Assert(t, strings.Contains(conn.errorOut.String(), tc.err)) + } + }) + + } +} + func TestAddressValidation(t *testing.T) { conn := testExecCommon(t, buildModuleQueryCommand, "echo", @@ -217,6 +290,7 @@ func TestAddressValidation(t *testing.T) { "--an-address", "cosmps1BAD_ENCODING", ) assert.Assert(t, strings.Contains(conn.errorOut.String(), "Error: invalid argument")) + } func TestOutputFormat(t *testing.T) { diff --git a/client/v2/autocli/testdata/file.test b/client/v2/autocli/testdata/file.test new file mode 100644 index 0000000000..2808f24d9d --- /dev/null +++ b/client/v2/autocli/testdata/file.test @@ -0,0 +1 @@ +this is just a test file \ No newline at end of file diff --git a/client/v2/autocli/testdata/help-deprecated-msg.golden b/client/v2/autocli/testdata/help-deprecated-msg.golden index d4364ef72c..810a532a60 100644 --- a/client/v2/autocli/testdata/help-deprecated-msg.golden +++ b/client/v2/autocli/testdata/help-deprecated-msg.golden @@ -14,7 +14,7 @@ Flags: --aux Generate aux signer data instead of sending a tx --bools bools (default []) -b, --broadcast-mode string Transaction broadcasting mode (sync|async) (default "sync") - --bz bytesBase64 + --bz binary --chain-id string The network chain ID --deprecated-field string --dry-run ignore the --gas flag and perform a simulation of a transaction, but don't broadcast it (when enabled, the local Keybase is not accessible) @@ -41,7 +41,7 @@ Flags: --offline Offline mode (does not allow any online functionality) -o, --output string Output format (text|json) (default "json") --page-count-total - --page-key bytesBase64 + --page-key binary --page-limit uint --page-offset uint --page-reverse diff --git a/client/v2/autocli/testdata/help-deprecated.golden b/client/v2/autocli/testdata/help-deprecated.golden index 00c78a1352..963da0986b 100644 --- a/client/v2/autocli/testdata/help-deprecated.golden +++ b/client/v2/autocli/testdata/help-deprecated.golden @@ -9,7 +9,7 @@ Flags: --an-address bech32 account address key name --an-enum Enum (unspecified | one | two | five | neg-three) (default unspecified) --bools bools (default []) - --bz bytesBase64 + --bz binary --deprecated-field string --duration duration --durations duration (repeated) @@ -24,7 +24,7 @@ Flags: --node string : to CometBFT RPC interface for this chain (default "tcp://localhost:26657") -o, --output string Output format (text|json) (default "text") --page-count-total - --page-key bytesBase64 + --page-key binary --page-limit uint --page-offset uint --page-reverse diff --git a/client/v2/autocli/testdata/help-echo-msg.golden b/client/v2/autocli/testdata/help-echo-msg.golden index 9ec3a64be8..b029bd2efe 100644 --- a/client/v2/autocli/testdata/help-echo-msg.golden +++ b/client/v2/autocli/testdata/help-echo-msg.golden @@ -19,7 +19,7 @@ Flags: --aux Generate aux signer data instead of sending a tx --bools bools (default []) -b, --broadcast-mode string Transaction broadcasting mode (sync|async) (default "sync") - --bz bytesBase64 + --bz binary --chain-id string The network chain ID --deprecated-field string (DEPRECATED: don't use this) --dry-run ignore the --gas flag and perform a simulation of a transaction, but don't broadcast it (when enabled, the local Keybase is not accessible) @@ -45,7 +45,7 @@ Flags: --offline Offline mode (does not allow any online functionality) -o, --output string Output format (text|json) (default "json") --page-count-total - --page-key bytesBase64 + --page-key binary --page-limit uint --page-offset uint --page-reverse diff --git a/client/v2/autocli/testdata/help-echo.golden b/client/v2/autocli/testdata/help-echo.golden index 627c9e9474..9e5f52f6da 100644 --- a/client/v2/autocli/testdata/help-echo.golden +++ b/client/v2/autocli/testdata/help-echo.golden @@ -16,7 +16,7 @@ Flags: --an-address bech32 account address key name --an-enum Enum (unspecified | one | two | five | neg-three) (default unspecified) --bools bools (default []) - --bz bytesBase64 + --bz binary some bytes --deprecated-field string (DEPRECATED: don't use this) --duration duration some random duration --durations duration (repeated) @@ -30,7 +30,7 @@ Flags: --node string : to CometBFT RPC interface for this chain (default "tcp://localhost:26657") -o, --output string Output format (text|json) (default "text") --page-count-total - --page-key bytesBase64 + --page-key binary --page-limit uint --page-offset uint --page-reverse