From d5c384ec6785138259d71549475c4501b6ad66d7 Mon Sep 17 00:00:00 2001 From: openhands Date: Sun, 28 Jun 2026 03:35:55 +0000 Subject: [PATCH] Fix argument encoding to preserve spaces in argument values Previously, cargs.Init used strings.Join/Split with space as delimiter, which broke arguments containing spaces (e.g. '--name "hello world"' would become '--name', 'hello', 'world' after encrypt/decrypt). Now each argument is individually base64-encoded, then joined with newline - a character never produced by base64 encoding. On decrypt, arguments are split by newline and individually decoded. Fixes #19 Co-authored-by: openhands --- cargs.go | 21 ++++++++--- cargs_test.go | 78 +++++++++++++++++++++++++++++++++++++++++ testdata/helper/main.go | 13 +++++++ 3 files changed, 107 insertions(+), 5 deletions(-) create mode 100644 cargs_test.go create mode 100644 testdata/helper/main.go diff --git a/cargs.go b/cargs.go index fdff06d..fda50d0 100644 --- a/cargs.go +++ b/cargs.go @@ -25,11 +25,13 @@ func Init(key []byte, flag string) { fmt.Println("error: empty flag!") os.Exit(1) } - var newArgs []string - newArgs = append(newArgs, os.Args[0]) if len(os.Args) > 2 && os.Args[1] == flag { - // encode args - input := []byte(strings.Join(os.Args[2:], " ")) + // encode args: base64 each arg, then join with newline + var encodedArgs []string + for _, arg := range os.Args[2:] { + encodedArgs = append(encodedArgs, base64.StdEncoding.EncodeToString([]byte(arg))) + } + input := []byte(strings.Join(encodedArgs, "\n")) output := make([]byte, len(input)) salsa20.XORKeyStream(output, input, nonce, &key32) fmt.Printf("cargs output: %s\n", base64.StdEncoding.EncodeToString(output)) @@ -42,7 +44,16 @@ func Init(key []byte, flag string) { } output := make([]byte, len(input)) salsa20.XORKeyStream(output, input, nonce, &key32) - os.Args = append(os.Args[:1], strings.Split(string(output), " ")...) + encodedArgs := strings.Split(string(output), "\n") + var decodedArgs []string + for _, enc := range encodedArgs { + decoded, err := base64.StdEncoding.DecodeString(enc) + if err != nil { + os.Exit(1) + } + decodedArgs = append(decodedArgs, string(decoded)) + } + os.Args = append(os.Args[:1], decodedArgs...) } else { os.Exit(0) } diff --git a/cargs_test.go b/cargs_test.go new file mode 100644 index 0000000..ecafb1f --- /dev/null +++ b/cargs_test.go @@ -0,0 +1,78 @@ +// +build cargs + +package cargs + +import ( + "fmt" + "os" + "os/exec" + "testing" +) + +// TestArgsWithSpaces verifies that arguments containing spaces survive the +// encrypt/decrypt round-trip without being split into multiple arguments. +func TestArgsWithSpaces(t *testing.T) { + // Build the test helper + testBin := "./cargs_test_helper" + cmd := exec.Command("go", "build", "-tags", "cargs", "-o", testBin, "./testdata/helper") + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("build test helper: %v\n%s", err, out) + } + defer os.Remove(testBin) + + // Step 1: encrypt an argument with a space in it + encOut, err := exec.Command(testBin, "getarg", "--name", "hello world").CombinedOutput() + if err != nil { + t.Fatalf("encrypt: %v\n%s", err, encOut) + } + // Extract the base64-encoded ciphertext from stdout + var ciphertext string + _, err = fmt.Sscanf(string(encOut), "cargs output: %s\n", &ciphertext) + if err != nil { + t.Fatalf("parse encrypted output: %v\n%s", err, encOut) + } + if ciphertext == "" { + t.Fatal("empty ciphertext") + } + + // Step 2: decrypt and check that the arg with space is preserved + decOut, err := exec.Command(testBin, ciphertext).CombinedOutput() + if err != nil { + t.Fatalf("decrypt: %v\n%s", err, decOut) + } + expected := "args = []string{\"--name\", \"hello world\"}\n" + if string(decOut) != expected { + t.Errorf("unexpected args after decrypt:\n got: %q\n want: %q", string(decOut), expected) + } +} + +// TestArgsWithConsecutiveSpaces verifies that arguments with multiple +// consecutive spaces are handled correctly. +func TestArgsWithConsecutiveSpaces(t *testing.T) { + testBin := "./cargs_test_helper" + cmd := exec.Command("go", "build", "-tags", "cargs", "-o", testBin, "./testdata/helper") + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("build test helper: %v\n%s", err, out) + } + defer os.Remove(testBin) + + // Encrypt + encOut, err := exec.Command(testBin, "getarg", "a", "b c", "d").CombinedOutput() + if err != nil { + t.Fatalf("encrypt: %v\n%s", err, encOut) + } + var ciphertext string + fmt.Sscanf(string(encOut), "cargs output: %s\n", &ciphertext) + + // Decrypt + decOut, err := exec.Command(testBin, ciphertext).CombinedOutput() + if err != nil { + t.Fatalf("decrypt: %v\n%s", err, decOut) + } + expected := "args = []string{\"a\", \"b c\", \"d\"}\n" + if string(decOut) != expected { + t.Errorf("unexpected args after decrypt:\n got: %q\n want: %q", string(decOut), expected) + } +} diff --git a/testdata/helper/main.go b/testdata/helper/main.go new file mode 100644 index 0000000..692d0aa --- /dev/null +++ b/testdata/helper/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "fmt" + "os" + + "github.com/12end/cargs" +) + +func main() { + cargs.Init([]byte("cargsRandomKey"), "getarg") + fmt.Printf("args = %#v\n", os.Args[1:]) +}