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 all 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)
})
}
}
74 changes: 72 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 @@
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)

Check warning on line 226 in gno.land/pkg/integration/testing_integration.go

View check run for this annotation

Codecov / codecov/patch

gno.land/pkg/integration/testing_integration.go#L226

Added line #L226 was not covered by tests
}

// Setup IO command
io := commands.NewTestIO()
io.SetOut(commands.WriteNopCloser(ts.Stdout()))
Expand Down Expand Up @@ -250,8 +256,7 @@
// 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,71 @@
}
}

// `unquote` takes a slice of strings, resulting from splitting a string block by spaces, and
// processes them. The function handles quoted phrases and escape characters within these strings.
func unquote(args []string) ([]string, error) {
const quote = '"'

parts := []string{}
var inQuote bool

var part strings.Builder
for _, arg := range args {
var escaped bool
for _, c := range arg {
if escaped {
// If the character is meant to be escaped, it is processed with Unquote.
// We use `Unquote` here for two main reasons:
// 1. It will validate that the escape sequence is correct
// 2. It converts the escaped string to its corresponding raw character.
// For example, "\\t" becomes '\t'.
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.
// It reflects one or multiple spaces between args in the original string.
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