From 198cacb16ee96056f3a5454a7ef35f73eea19056 Mon Sep 17 00:00:00 2001 From: Harshil-1603 Date: Thu, 15 Oct 2026 01:43:32 +0530 Subject: [PATCH 1/2] Reject additional CSV records in StringToString arguments Assisted-by: OpenAI gpt-6.1-sol --- string_to_string.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/string_to_string.go b/string_to_string.go index e3765754..73019c30 100644 --- a/string_to_string.go +++ b/string_to_string.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/csv" "fmt" + "io" "sort" "strings" ) @@ -37,6 +38,9 @@ func (s *stringToStringValue) Set(val string) error { if err != nil { return err } + if _, err := r.Read(); err != io.EOF { + return fmt.Errorf("must be a single CSV record; quote fields containing newlines") + } } out := make(map[string]string, len(ss)) @@ -125,6 +129,16 @@ func (f *FlagSet) GetStringToString(name string) (map[string]string, error) { // // --arg a=1,2 -> map[string]string{ "a": "1,2" } // +// Arguments containing multiple '=' characters are parsed as one CSV record. +// Quote the entire key=value field to preserve commas, quotes, or newlines in +// its value. When invoking a command from a shell, preserve the CSV quotes, +// for example by enclosing the argument in single quotes: +// +// --arg '"a=first,second=value"' -> map[string]string{ "a": "first,second=value" } +// +// Multiple unquoted CSV records in one argument are rejected rather than +// silently discarding mappings after the first record. +// // Returns a pointer to the map which will be updated upon invocation of [FlagSet.Parse], [Flag.Value.Set], and others. func (f *FlagSet) StringToString(name string, value map[string]string, usage string) *map[string]string { p := map[string]string{} From d35d745d623c02c6e49c7b46f4c2ae803df30850 Mon Sep 17 00:00:00 2001 From: Harshil-1603 Date: Thu, 15 Oct 2026 01:43:59 +0530 Subject: [PATCH 2/2] Test rejected extra records and quoted newlines Assisted-by: OpenAI gpt-6.1-sol --- string_to_string_test.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/string_to_string_test.go b/string_to_string_test.go index 3e7659ec..911620ae 100644 --- a/string_to_string_test.go +++ b/string_to_string_test.go @@ -159,6 +159,31 @@ func TestStringToString(t *testing.T) { } } +func TestStringToStringRejectsExtraCSVRecords(t *testing.T) { + f := NewFlagSet("test", ContinueOnError) + values := f.StringToString("arg", map[string]string{"default": "retained"}, "") + if err := f.Parse([]string{"--arg", "a=first\nb=second"}); err == nil { + t.Fatal("expected an error instead of silently dropping the second record") + } + if !reflect.DeepEqual(*values, map[string]string{"default": "retained"}) { + t.Fatalf("failed parsing changed the default map: %v", *values) + } + if f.Lookup("arg").Changed { + t.Fatal("failed parsing marked the flag as changed") + } +} + +func TestStringToStringQuotedNewline(t *testing.T) { + f := NewFlagSet("test", ContinueOnError) + values := f.StringToString("arg", nil, "") + if err := f.Parse([]string{"--arg", "\"a=first\nsecond=value\""}); err != nil { + t.Fatalf("quoted newline should be preserved: %v", err) + } + if !reflect.DeepEqual(*values, map[string]string{"a": "first\nsecond=value"}) { + t.Fatalf("quoted value was not preserved: %v", *values) + } +} + // This test ensures that [FlagSet.GetStringToString] always return the pointers which were given during flag // initialization. //