Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(txtar): handle quote for gnokey #1745

Merged
merged 9 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions gno.land/pkg/integration/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
// - Supports most of the common commands.
// - `--remote`, `--insecure-password-stdin`, and `--home` flags are set automatically to
// communicate with the gnoland node.
// - In order to handle escape sequences like `\n` within arguments, you can enclose the argument
// in `"`
//
// 3. `adduser`:
// - Must be run before `gnoland start`.
Expand Down
46 changes: 46 additions & 0 deletions gno.land/pkg/integration/integration_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,57 @@
package integration

import (
"strings"
"testing"

"github.com/stretchr/testify/assert"
deelawn marked this conversation as resolved.
Show resolved Hide resolved
"github.com/stretchr/testify/require"
)

func TestTestdata(t *testing.T) {
t.Parallel()

RunGnolandTestscripts(t, "testdata")
}

func TestUnquote(t *testing.T) {
t.Parallel()

cases := []struct {
Input string
Expected []string
ShouldFail bool
}{
{"", []string{""}, false},
{"g", []string{"g"}, false},
{"Hello Gno", []string{"Hello", "Gno"}, false},
{`"Hello" "Gno"`, []string{"Hello", "Gno"}, false},
{`"Hel lo" "Gno"`, []string{"Hel lo", "Gno"}, false},
{`"H e l l o\n" \nGno`, []string{"H e l l o\n", "\\nGno"}, false},
{`"Hel\n"\nlo " ""G"n"o"`, []string{"Hel\n\\nlo", " Gno"}, false},
{`"He said, \"Hello\"" "Gno"`, []string{`He said, "Hello"`, "Gno"}, false},
{`"\n \t" \n\t`, []string{"\n \t", "\\n\\t"}, false},
{`"Hel\\n"\t\\nlo " ""\\nGno"`, []string{"Hel\\n\\t\\\\nlo", " \\nGno"}, false},
// errors:
{`"Hello Gno`, []string{}, true}, // unfinished quote
{`"Hello\e Gno"`, []string{}, true}, // unhandled escape sequence
}

for _, tc := range cases {
tc := tc
t.Run(tc.Input, func(t *testing.T) {
t.Parallel()

// split by whitespace to simulate command-line arguments
args := strings.Split(tc.Input, " ")
unquotedArgs, err := unquote(args)
if tc.ShouldFail {
require.Error(t, err)
return
}

require.NoError(t, err)
assert.Equal(t, tc.Expected, unquotedArgs)
})
}
}
69 changes: 67 additions & 2 deletions gno.land/pkg/integration/testing_integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,12 @@ func setupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params {
logger := ts.Value(envKeyLogger).(*slog.Logger) // grab logger
sid := ts.Getenv("SID") // grab session id

// Unquote args enclosed in `"` to correctly handle `\n` or similar escapes.
args, err := unquote(args)
if err != nil {
tsValidateError(ts, "gnokey", neg, err)
}

// Setup IO command
io := commands.NewTestIO()
io.SetOut(commands.WriteNopCloser(ts.Stdout()))
Expand Down Expand Up @@ -250,8 +256,7 @@ func setupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params {
// user provided.
args = append(defaultArgs, args...)

err := cmd.ParseAndRun(context.Background(), args)

err = cmd.ParseAndRun(context.Background(), args)
tsValidateError(ts, "gnokey", neg, err)
},
// adduser commands must be executed before starting the node; it errors out otherwise.
Expand Down Expand Up @@ -326,6 +331,66 @@ func setupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params {
}
}

// `unquote` processes a string slice as one block string. handling quoted
// phrases and escape sequences.
func unquote(args []string) ([]string, error) {
const quote = '"'

parts := []string{}
inQuote := false
gfanton marked this conversation as resolved.
Show resolved Hide resolved

var part strings.Builder
for _, arg := range args {
escaped := false
gfanton marked this conversation as resolved.
Show resolved Hide resolved
for _, c := range arg {
// If this character should be escaped, unquote it
if escaped {
uc, err := strconv.Unquote(`"\` + string(c) + `"`)
gfanton marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, fmt.Errorf("unhandled escape sequence `\\%c`: %w", c, err)
}

part.WriteString(uc)
escaped = false
continue
}

// If we are inside a quoted string and encounter an escape character,
// flag the next character as `escaped`
if inQuote && c == '\\' {
escaped = true
continue
}

// Detect quote and toggle inQuote state
if c == quote {
inQuote = !inQuote
continue
}

// Handle regular character
part.WriteRune(c)
}

// If we're inside a quote, add a single space
gfanton marked this conversation as resolved.
Show resolved Hide resolved
if inQuote {
part.WriteRune(' ')
continue
}

// Finalize part, add to parts, and reset for next part
parts = append(parts, part.String())
part.Reset()
}

// Check if a quote is left open
if inQuote {
return nil, errors.New("unfinished quote")
}

return parts, nil
}

func getNodeSID(ts *testscript.TestScript) string {
return ts.Getenv("SID")
}
Expand Down
Loading