From 5f2813f5f7b2f12e8a637643acd7466e34e1afa5 Mon Sep 17 00:00:00 2001 From: Alex Collins Date: Sat, 22 Nov 2025 11:25:37 -0800 Subject: [PATCH 1/3] feat: add ParseParams function with quote handling Implement ParseParams function that correctly parses key=value pairs with support for: - Double and single quoted values - Escaped quotes within quoted values - Empty values - Whitespace handling - Multiple pairs in a single string --- pkg/codingcontext/param_map_test.go | 91 +++++++++++++++++++++++ pkg/codingcontext/params.go | 111 ++++++++++++++++++++++++++++ 2 files changed, 202 insertions(+) diff --git a/pkg/codingcontext/param_map_test.go b/pkg/codingcontext/param_map_test.go index 18627839..829f0826 100644 --- a/pkg/codingcontext/param_map_test.go +++ b/pkg/codingcontext/param_map_test.go @@ -95,3 +95,94 @@ func TestParams_SetMultiple(t *testing.T) { t.Errorf("Params[key2] = %q, want %q", p["key2"], "value2") } } + +func TestParseParams(t *testing.T) { + tests := []struct { + name string + input string + expected Params + }{ + { + name: "empty string", + input: "", + expected: Params{}, + }, + { + name: "single key=value", + input: "key=value", + expected: Params{"key": "value"}, + }, + { + name: "multiple key=value pairs", + input: "key1=value1 key2=value2 key3=value3", + expected: Params{"key1": "value1", "key2": "value2", "key3": "value3"}, + }, + { + name: "double-quoted value with spaces", + input: `key1="value with spaces" key2=value2`, + expected: Params{"key1": "value with spaces", "key2": "value2"}, + }, + { + name: "single-quoted value with spaces", + input: `key1='value with spaces' key2=value2`, + expected: Params{"key1": "value with spaces", "key2": "value2"}, + }, + { + name: "escaped double quotes", + input: `key1="value with \"escaped\" quotes"`, + expected: Params{"key1": `value with "escaped" quotes`}, + }, + { + name: "escaped single quotes", + input: `key1='value with \'escaped\' quotes'`, + expected: Params{"key1": `value with 'escaped' quotes`}, + }, + { + name: "mixed quoted and unquoted", + input: `key1="quoted value" key2=unquoted key3='single quoted'`, + expected: Params{"key1": "quoted value", "key2": "unquoted", "key3": "single quoted"}, + }, + { + name: "value with equals sign in quotes", + input: `key1="value=with=equals" key2=normal`, + expected: Params{"key1": "value=with=equals", "key2": "normal"}, + }, + { + name: "empty quoted value", + input: `key1="" key2=value2`, + expected: Params{"key1": "", "key2": "value2"}, + }, + { + name: "whitespace around equals", + input: "key1 = value1 key2=value2", + expected: Params{"key1": "value1", "key2": "value2"}, + }, + { + name: "quoted value with spaces and equals", + input: `key1="value with spaces and = signs"`, + expected: Params{"key1": "value with spaces and = signs"}, + }, + { + name: "key with trailing equals and empty value", + input: "key1= key2=value2", + expected: Params{"key1": "", "key2": "value2"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ParseParams(tt.input) + + if len(result) != len(tt.expected) { + t.Errorf("ParseParams() length = %d, want %d", len(result), len(tt.expected)) + return + } + + for k, v := range tt.expected { + if result[k] != v { + t.Errorf("ParseParams()[%q] = %q, want %q", k, result[k], v) + } + } + }) + } +} diff --git a/pkg/codingcontext/params.go b/pkg/codingcontext/params.go index 8388a8c0..51a4b7a8 100644 --- a/pkg/codingcontext/params.go +++ b/pkg/codingcontext/params.go @@ -25,3 +25,114 @@ func (p *Params) Set(value string) error { (*p)[kv[0]] = kv[1] return nil } + +// ParseParams parses a string containing key=value pairs separated by spaces. +// Values can be quoted with single or double quotes, and quotes can be escaped. +// Examples: +// - "key1=value1 key2=value2" +// - `key1="value with spaces" key2=value2` +// - `key1='value with spaces' key2=value2` +// - `key1="value with \"escaped\" quotes"` +func ParseParams(s string) Params { + params := make(Params) + if s == "" { + return params + } + + s = strings.TrimSpace(s) + var i int + for i < len(s) { + // Skip whitespace + for i < len(s) && (s[i] == ' ' || s[i] == '\t') { + i++ + } + if i >= len(s) { + break + } + + // Find the key (until '=') + keyStart := i + for i < len(s) && s[i] != '=' { + i++ + } + if i >= len(s) { + break + } + key := strings.TrimSpace(s[keyStart:i]) + if key == "" { + i++ + continue + } + + // Skip '=' + i++ + + // Skip whitespace after '=' + for i < len(s) && (s[i] == ' ' || s[i] == '\t') { + i++ + } + if i >= len(s) { + params[key] = "" + break + } + + // Parse the value + var value strings.Builder + if s[i] == '"' { + // Double-quoted value + i++ // skip opening quote + for i < len(s) { + if s[i] == '\\' && i+1 < len(s) && s[i+1] == '"' { + value.WriteByte('"') + i += 2 + } else if s[i] == '"' { + i++ // skip closing quote + break + } else { + value.WriteByte(s[i]) + i++ + } + } + } else if s[i] == '\'' { + // Single-quoted value + i++ // skip opening quote + for i < len(s) { + if s[i] == '\\' && i+1 < len(s) && s[i+1] == '\'' { + value.WriteByte('\'') + i += 2 + } else if s[i] == '\'' { + i++ // skip closing quote + break + } else { + value.WriteByte(s[i]) + i++ + } + } + } else { + // Check if we're at the start of a new key-value pair (look for '=' ahead) + // This handles cases like "key1= key2=value2" + j := i + for j < len(s) && s[j] != '=' && s[j] != ' ' && s[j] != '\t' { + j++ + } + isNewKeyValuePair := j < len(s) && s[j] == '=' && j > i + + if isNewKeyValuePair { + // Empty value, next token is a new key-value pair + params[key] = "" + continue + } + + // Unquoted value (until space or end of string) + valueStart := i + for i < len(s) && s[i] != ' ' && s[i] != '\t' { + i++ + } + value.WriteString(s[valueStart:i]) + } + + params[key] = value.String() + } + + return params +} From f8c86e4483658ad87fdd42a3b7d24a4032535272 Mon Sep 17 00:00:00 2001 From: Alex Collins Date: Sat, 22 Nov 2025 11:26:37 -0800 Subject: [PATCH 2/3] refactor: remove single-quote support from ParseParams Only double-quotes are now supported for quoted values. Single quotes are treated as regular characters in unquoted values. --- pkg/codingcontext/param_map_test.go | 17 ++++++----------- pkg/codingcontext/params.go | 18 +----------------- 2 files changed, 7 insertions(+), 28 deletions(-) diff --git a/pkg/codingcontext/param_map_test.go b/pkg/codingcontext/param_map_test.go index 829f0826..e2d17228 100644 --- a/pkg/codingcontext/param_map_test.go +++ b/pkg/codingcontext/param_map_test.go @@ -122,25 +122,20 @@ func TestParseParams(t *testing.T) { input: `key1="value with spaces" key2=value2`, expected: Params{"key1": "value with spaces", "key2": "value2"}, }, - { - name: "single-quoted value with spaces", - input: `key1='value with spaces' key2=value2`, - expected: Params{"key1": "value with spaces", "key2": "value2"}, - }, { name: "escaped double quotes", input: `key1="value with \"escaped\" quotes"`, expected: Params{"key1": `value with "escaped" quotes`}, }, { - name: "escaped single quotes", - input: `key1='value with \'escaped\' quotes'`, - expected: Params{"key1": `value with 'escaped' quotes`}, + name: "mixed quoted and unquoted", + input: `key1="quoted value" key2=unquoted`, + expected: Params{"key1": "quoted value", "key2": "unquoted"}, }, { - name: "mixed quoted and unquoted", - input: `key1="quoted value" key2=unquoted key3='single quoted'`, - expected: Params{"key1": "quoted value", "key2": "unquoted", "key3": "single quoted"}, + name: "single quote treated as unquoted", + input: `key1='value' key2=value2`, + expected: Params{"key1": "'value'", "key2": "value2"}, }, { name: "value with equals sign in quotes", diff --git a/pkg/codingcontext/params.go b/pkg/codingcontext/params.go index 51a4b7a8..921d9471 100644 --- a/pkg/codingcontext/params.go +++ b/pkg/codingcontext/params.go @@ -27,11 +27,10 @@ func (p *Params) Set(value string) error { } // ParseParams parses a string containing key=value pairs separated by spaces. -// Values can be quoted with single or double quotes, and quotes can be escaped. +// Values can be quoted with double quotes, and quotes can be escaped. // Examples: // - "key1=value1 key2=value2" // - `key1="value with spaces" key2=value2` -// - `key1='value with spaces' key2=value2` // - `key1="value with \"escaped\" quotes"` func ParseParams(s string) Params { params := make(Params) @@ -93,21 +92,6 @@ func ParseParams(s string) Params { i++ } } - } else if s[i] == '\'' { - // Single-quoted value - i++ // skip opening quote - for i < len(s) { - if s[i] == '\\' && i+1 < len(s) && s[i+1] == '\'' { - value.WriteByte('\'') - i += 2 - } else if s[i] == '\'' { - i++ // skip closing quote - break - } else { - value.WriteByte(s[i]) - i++ - } - } } else { // Check if we're at the start of a new key-value pair (look for '=' ahead) // This handles cases like "key1= key2=value2" From 35bb4b0a08a4aae5bd660578801be7a79ce7284c Mon Sep 17 00:00:00 2001 From: Alex Collins Date: Sat, 22 Nov 2025 11:29:59 -0800 Subject: [PATCH 3/3] refactor: require quoted values in ParseParams ParseParams now requires all values to be double-quoted. Unquoted values are treated as an error. Function signature changed to return (Params, error) to support error handling. --- pkg/codingcontext/param_map_test.go | 121 +++++++++++++++++++--------- pkg/codingcontext/params.go | 73 +++++++---------- 2 files changed, 111 insertions(+), 83 deletions(-) diff --git a/pkg/codingcontext/param_map_test.go b/pkg/codingcontext/param_map_test.go index e2d17228..46a8297b 100644 --- a/pkg/codingcontext/param_map_test.go +++ b/pkg/codingcontext/param_map_test.go @@ -1,6 +1,7 @@ package codingcontext import ( + "strings" "testing" ) @@ -98,75 +99,115 @@ func TestParams_SetMultiple(t *testing.T) { func TestParseParams(t *testing.T) { tests := []struct { - name string - input string - expected Params + name string + input string + expected Params + wantError bool + errorMsg string }{ { - name: "empty string", - input: "", - expected: Params{}, + name: "empty string", + input: "", + expected: Params{}, + wantError: false, }, { - name: "single key=value", - input: "key=value", - expected: Params{"key": "value"}, + name: "single quoted key=value", + input: `key="value"`, + expected: Params{"key": "value"}, + wantError: false, }, { - name: "multiple key=value pairs", - input: "key1=value1 key2=value2 key3=value3", - expected: Params{"key1": "value1", "key2": "value2", "key3": "value3"}, + name: "multiple quoted key=value pairs", + input: `key1="value1" key2="value2" key3="value3"`, + expected: Params{"key1": "value1", "key2": "value2", "key3": "value3"}, + wantError: false, }, { - name: "double-quoted value with spaces", - input: `key1="value with spaces" key2=value2`, - expected: Params{"key1": "value with spaces", "key2": "value2"}, + name: "double-quoted value with spaces", + input: `key1="value with spaces" key2="value2"`, + expected: Params{"key1": "value with spaces", "key2": "value2"}, + wantError: false, }, { - name: "escaped double quotes", - input: `key1="value with \"escaped\" quotes"`, - expected: Params{"key1": `value with "escaped" quotes`}, + name: "escaped double quotes", + input: `key1="value with \"escaped\" quotes"`, + expected: Params{"key1": `value with "escaped" quotes`}, + wantError: false, }, { - name: "mixed quoted and unquoted", - input: `key1="quoted value" key2=unquoted`, - expected: Params{"key1": "quoted value", "key2": "unquoted"}, + name: "value with equals sign in quotes", + input: `key1="value=with=equals" key2="normal"`, + expected: Params{"key1": "value=with=equals", "key2": "normal"}, + wantError: false, }, { - name: "single quote treated as unquoted", - input: `key1='value' key2=value2`, - expected: Params{"key1": "'value'", "key2": "value2"}, + name: "empty quoted value", + input: `key1="" key2="value2"`, + expected: Params{"key1": "", "key2": "value2"}, + wantError: false, }, { - name: "value with equals sign in quotes", - input: `key1="value=with=equals" key2=normal`, - expected: Params{"key1": "value=with=equals", "key2": "normal"}, + name: "whitespace around equals", + input: `key1 = "value1" key2="value2"`, + expected: Params{"key1": "value1", "key2": "value2"}, + wantError: false, }, { - name: "empty quoted value", - input: `key1="" key2=value2`, - expected: Params{"key1": "", "key2": "value2"}, + name: "quoted value with spaces and equals", + input: `key1="value with spaces and = signs"`, + expected: Params{"key1": "value with spaces and = signs"}, + wantError: false, }, { - name: "whitespace around equals", - input: "key1 = value1 key2=value2", - expected: Params{"key1": "value1", "key2": "value2"}, + name: "unquoted value - error", + input: `key1=value1`, + wantError: true, + errorMsg: "unquoted value", }, { - name: "quoted value with spaces and equals", - input: `key1="value with spaces and = signs"`, - expected: Params{"key1": "value with spaces and = signs"}, + name: "mixed quoted and unquoted - error", + input: `key1="quoted value" key2=unquoted`, + wantError: true, + errorMsg: "unquoted value", }, { - name: "key with trailing equals and empty value", - input: "key1= key2=value2", - expected: Params{"key1": "", "key2": "value2"}, + name: "unclosed quote - error", + input: `key1="value with spaces`, + wantError: true, + errorMsg: "unclosed quote", + }, + { + name: "missing value after equals - error", + input: `key1= key2="value2"`, + wantError: true, + errorMsg: "unquoted value", + }, + { + name: "single quote not supported - error", + input: `key1='value'`, + wantError: true, + errorMsg: "unquoted value", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := ParseParams(tt.input) + result, err := ParseParams(tt.input) + + if (err != nil) != tt.wantError { + t.Errorf("ParseParams() error = %v, wantError %v", err, tt.wantError) + return + } + + if tt.wantError { + if err != nil && tt.errorMsg != "" { + if !strings.Contains(err.Error(), tt.errorMsg) { + t.Errorf("ParseParams() error = %v, want error containing %q", err, tt.errorMsg) + } + } + return + } if len(result) != len(tt.expected) { t.Errorf("ParseParams() length = %d, want %d", len(result), len(tt.expected)) diff --git a/pkg/codingcontext/params.go b/pkg/codingcontext/params.go index 921d9471..49022f52 100644 --- a/pkg/codingcontext/params.go +++ b/pkg/codingcontext/params.go @@ -27,15 +27,16 @@ func (p *Params) Set(value string) error { } // ParseParams parses a string containing key=value pairs separated by spaces. -// Values can be quoted with double quotes, and quotes can be escaped. +// Values must be quoted with double quotes, and quotes can be escaped. +// Unquoted values are treated as an error. // Examples: -// - "key1=value1 key2=value2" -// - `key1="value with spaces" key2=value2` +// - `key1="value1" key2="value2"` +// - `key1="value with spaces" key2="value2"` // - `key1="value with \"escaped\" quotes"` -func ParseParams(s string) Params { +func ParseParams(s string) (Params, error) { params := make(Params) if s == "" { - return params + return params, nil } s = strings.TrimSpace(s) @@ -71,52 +72,38 @@ func ParseParams(s string) Params { i++ } if i >= len(s) { - params[key] = "" - break + return nil, fmt.Errorf("missing quoted value for key %q", key) } - // Parse the value - var value strings.Builder - if s[i] == '"' { - // Double-quoted value - i++ // skip opening quote - for i < len(s) { - if s[i] == '\\' && i+1 < len(s) && s[i+1] == '"' { - value.WriteByte('"') - i += 2 - } else if s[i] == '"' { - i++ // skip closing quote - break - } else { - value.WriteByte(s[i]) - i++ - } - } - } else { - // Check if we're at the start of a new key-value pair (look for '=' ahead) - // This handles cases like "key1= key2=value2" - j := i - for j < len(s) && s[j] != '=' && s[j] != ' ' && s[j] != '\t' { - j++ - } - isNewKeyValuePair := j < len(s) && s[j] == '=' && j > i - - if isNewKeyValuePair { - // Empty value, next token is a new key-value pair - params[key] = "" - continue - } + // Values must be quoted + if s[i] != '"' { + return nil, fmt.Errorf("unquoted value for key %q: values must be double-quoted", key) + } - // Unquoted value (until space or end of string) - valueStart := i - for i < len(s) && s[i] != ' ' && s[i] != '\t' { + // Parse the double-quoted value + var value strings.Builder + i++ // skip opening quote + quoted := false + for i < len(s) { + if s[i] == '\\' && i+1 < len(s) && s[i+1] == '"' { + value.WriteByte('"') + i += 2 + } else if s[i] == '"' { + i++ // skip closing quote + quoted = true + break + } else { + value.WriteByte(s[i]) i++ } - value.WriteString(s[valueStart:i]) + } + + if !quoted { + return nil, fmt.Errorf("unclosed quote for key %q", key) } params[key] = value.String() } - return params + return params, nil }