From 6263536ffd7e0c0221f020d783dba5b002881708 Mon Sep 17 00:00:00 2001 From: openhands Date: Sun, 28 Jun 2026 02:33:05 +0000 Subject: [PATCH] Fix: preserve arguments with spaces during encryption/decryption The original implementation used strings.Join(args, " ") and strings.Split(output, " ") to serialize/deserialize arguments. This caused issues when arguments contained spaces, as they were incorrectly split into multiple arguments. The fix encodes each argument individually using base64, then joins them with a NULL separator (\x00). Base64 output only contains alphanumeric characters plus '+', '/', and '=', which cannot appear in the separator. This ensures arguments with spaces, tabs, newlines, or other special characters are preserved correctly. Also handle empty strings by encoding them as a single space before base64 encoding. Fixes #16 Co-authored-by: openhands --- cargs.go | 41 +++++++++++++++++- cargs_test.go | 116 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 cargs_test.go diff --git a/cargs.go b/cargs.go index fdff06d..033baf4 100644 --- a/cargs.go +++ b/cargs.go @@ -11,6 +11,43 @@ import ( "strings" ) +// encodeArgs encodes each argument individually using base64, then joins them with the NULL separator. +// This prevents arguments containing spaces from being incorrectly split during decryption. +// Empty strings are encoded as a single space to preserve them through the join/split process. +func encodeArgs(args []string) []byte { + var encoded []string + for _, arg := range args { + if arg == "" { + encoded = append(encoded, " ") + } else { + encoded = append(encoded, base64.StdEncoding.EncodeToString([]byte(arg))) + } + } + return []byte(strings.Join(encoded, "\x00")) +} + +// decodeArgs splits the input by NULL separator and decodes each part using base64. +// A single space is decoded as an empty string to preserve empty arguments. +func decodeArgs(data []byte) []string { + parts := strings.Split(string(data), "\x00") + var decoded []string + for _, part := range parts { + if part == "" { + continue + } + if part == " " { + decoded = append(decoded, "") + continue + } + decodedBytes, err := base64.StdEncoding.DecodeString(part) + if err != nil { + continue + } + decoded = append(decoded, string(decodedBytes)) + } + return decoded +} + // init cargs // only flag occur,cargs crypto rest args,otherwise decrypt os.args[1] func Init(key []byte, flag string) { @@ -29,7 +66,7 @@ func Init(key []byte, flag 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:], " ")) + input := encodeArgs(os.Args[2:]) output := make([]byte, len(input)) salsa20.XORKeyStream(output, input, nonce, &key32) fmt.Printf("cargs output: %s\n", base64.StdEncoding.EncodeToString(output)) @@ -42,7 +79,7 @@ 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), " ")...) + os.Args = append(os.Args[:1], decodeArgs(output)...) } else { os.Exit(0) } diff --git a/cargs_test.go b/cargs_test.go new file mode 100644 index 0000000..c33b57b --- /dev/null +++ b/cargs_test.go @@ -0,0 +1,116 @@ +//+build cargs + +package cargs + +import ( + "testing" +) + +func TestEncodeDecodeArgs(t *testing.T) { + testCases := []struct { + name string + args []string + }{ + { + name: "simple arguments", + args: []string{"--name", "hello"}, + }, + { + name: "argument with single space", + args: []string{"--name", "hello world"}, + }, + { + name: "argument with multiple spaces", + args: []string{"--name", "hello world"}, + }, + { + name: "multiple arguments with spaces", + args: []string{"--name", "hello world", "--other", "foo bar"}, + }, + { + name: "empty arguments", + args: []string{"", ""}, + }, + { + name: "argument with special characters", + args: []string{"--path", "/usr/local/bin"}, + }, + { + name: "argument with newlines", + args: []string{"--multiline", "line1\nline2"}, + }, + { + name: "argument with tabs", + args: []string{"--tab", "col1\tcol2"}, + }, + { + name: "argument with unicode", + args: []string{"--unicode", "你好世界"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + encoded := encodeArgs(tc.args) + decoded := decodeArgs(encoded) + + if len(decoded) != len(tc.args) { + t.Errorf("encode/decode failed for %s: got %d args, want %d args", tc.name, len(decoded), len(tc.args)) + t.Errorf(" decoded: %v", decoded) + t.Errorf(" original: %v", tc.args) + } + + for i := range tc.args { + if decoded[i] != tc.args[i] { + t.Errorf("encode/decode failed for %s at index %d: got %q, want %q", tc.name, i, decoded[i], tc.args[i]) + } + } + }) + } +} + +func TestEncodeArgsProducesNullSeparatedOutput(t *testing.T) { + args := []string{"hello world", "foo bar"} + encoded := encodeArgs(args) + encodedStr := string(encoded) + + // Should contain NULL byte separator + if !contains(encodedStr, "\x00") { + t.Error("encoded output should contain NULL byte separator") + } + // Should NOT contain space inside individual encoded parts (base64 doesn't use spaces) + parts := splitOnce(encodedStr, "\x00") + for i, part := range parts { + // Each part is a base64 encoded string - verify it's valid base64 + // (no spaces in base64 output) + if contains(part, " ") { + t.Errorf("part %d should not contain spaces: %s", i, part) + } + } +} + +// Helper functions for testing +func contains(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsAt(s, substr, 0)) +} + +func containsAt(s, substr string, start int) bool { + for i := start; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} + +func splitOnce(s, sep string) []string { + var result []string + for i := 0; i <= len(s)-len(sep); i++ { + if s[i:i+len(sep)] == sep { + result = append(result, s[:i]) + result = append(result, s[i+len(sep):]) + return result + } + } + return []string{s} +}