From 6480996c4ce099fdd428b4ce894be79126752b29 Mon Sep 17 00:00:00 2001 From: Team Humaki Date: Thu, 17 Sep 2026 02:59:17 -0700 Subject: [PATCH 1/2] struct: escape delimiter in string slices on ReflectFrom Fixes #348 --- key.go | 10 ++++++++++ struct.go | 2 +- struct_test.go | 43 ++++++++++++++++++++++++++++++++++++++----- 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/key.go b/key.go index 72f0de4..46bf21e 100644 --- a/key.go +++ b/key.go @@ -516,6 +516,16 @@ func (k *Key) RangeTime(defaultVal, min, max time.Time) time.Time { return k.RangeTimeFormat(time.RFC3339, defaultVal, min, max) } +// escapeDelimited prepares a slice element for joining with delim. +// Backslashes are escaped first, then occurrences of delim, matching Strings(). +func escapeDelimited(val, delim string) string { + if delim == "" { + return val + } + val = strings.ReplaceAll(val, `\`, `\\`) + return strings.ReplaceAll(val, delim, `\`+delim) +} + // Strings returns list of string divided by given delimiter. func (k *Key) Strings(delim string) []string { str := k.String() diff --git a/struct.go b/struct.go index c8bff94..7e09863 100644 --- a/struct.go +++ b/struct.go @@ -533,7 +533,7 @@ func reflectSliceWithProperType(key *Key, field reflect.Value, delim string, all for i := 0; i < field.Len(); i++ { switch sliceOf { case reflect.String: - buf.WriteString(slice.Index(i).String()) + buf.WriteString(escapeDelimited(slice.Index(i).String(), delim)) case reflect.Int, reflect.Int64: fmt.Fprint(&buf, slice.Index(i).Int()) case reflect.Uint, reflect.Uint64: diff --git a/struct_test.go b/struct_test.go index 3fe9491..2deffd6 100644 --- a/struct_test.go +++ b/struct_test.go @@ -671,12 +671,12 @@ func Test_ReflectFromStructNonUniqueSections(t *testing.T) { { PublicKey: "pubClientKey", PresharedKey: "psKey", - AllowedIPs: []string{"10.2.0.2/32,fd00:2::2/128"}, + AllowedIPs: []string{"10.2.0.2/32", "fd00:2::2/128"}, }, { PublicKey: "pubClientKey2", PresharedKey: "psKey2", - AllowedIPs: []string{"10.2.0.3/32,fd00:2::3/128"}, + AllowedIPs: []string{"10.2.0.3/32", "fd00:2::3/128"}, }, }, } @@ -697,12 +697,12 @@ func Test_ReflectFromStructNonUniqueSections(t *testing.T) { { PublicKey: "pubClientKey3", PresharedKey: "psKey3", - AllowedIPs: []string{"10.2.0.4/32,fd00:2::4/128"}, + AllowedIPs: []string{"10.2.0.4/32", "fd00:2::4/128"}, }, { PublicKey: "pubClientKey4", PresharedKey: "psKey4", - AllowedIPs: []string{"10.2.0.5/32,fd00:2::5/128"}, + AllowedIPs: []string{"10.2.0.5/32", "fd00:2::5/128"}, }, }) @@ -733,7 +733,7 @@ AllowedIPs = 10.2.0.5/32,fd00:2::5/128 err = cfg.Section("Peer").ReflectFrom(&testPeer{ PublicKey: "pubClientKey5", PresharedKey: "psKey5", - AllowedIPs: []string{"10.2.0.6/32,fd00:2::6/128"}, + AllowedIPs: []string{"10.2.0.6/32", "fd00:2::6/128"}, }) require.NoError(t, err) @@ -929,6 +929,39 @@ func Test_Duration(t *testing.T) { }) } +// https://github.com/go-ini/ini/issues/348 +func Test_ReflectFromStringSliceWithComma(t *testing.T) { + type C struct { + Scale float32 + History []string + } + in := C{ + Scale: 1.5, + History: []string{ + "Test One", + "Test Two", + "Test with comma, Three", + "Test Four", + }, + } + + f := Empty() + require.NoError(t, f.ReflectFrom(&in)) + + var buf bytes.Buffer + _, err := f.WriteTo(&buf) + require.NoError(t, err) + assert.Contains(t, buf.String(), `Test with comma\, Three`) + + got := f.Section("").Key("History").Strings(",") + assert.Equal(t, in.History, got) + + var out C + require.NoError(t, MapTo(&out, buf.Bytes())) + assert.Equal(t, in.History, out.History) + assert.Equal(t, float32(1.5), out.Scale) +} + type Employer struct { Name string Title string From a395deb83e197440b41dead0174b1377ea36c70b Mon Sep 17 00:00:00 2001 From: Joe Chen Date: Wed, 11 Nov 2026 20:22:53 +0000 Subject: [PATCH 2/2] Escape slice elements only with the EscapeSliceDelimiters option Escaping changes what ReflectFrom writes for elements that contain the delimiter, such as WireGuard AllowedIPs, which other readers don't unescape. Default output stays as before. --- ini.go | 5 +++++ struct.go | 6 +++++- struct_test.go | 43 ++++++++++++++++++++++++++----------------- 3 files changed, 36 insertions(+), 18 deletions(-) diff --git a/ini.go b/ini.go index 5eb9d5a..35a91ac 100644 --- a/ini.go +++ b/ini.go @@ -130,6 +130,11 @@ type LoadOptions struct { // empty assignment to reset prior values. Empty values are otherwise // omitted. PreserveEmptyShadowValues bool + // EscapeSliceDelimiters indicates whether ReflectFrom escapes backslashes + // and the delimiter in the elements of a string slice, so Key.Strings and + // MapTo read back the same elements. Other INI readers may not unescape + // them, so it is off by default. + EscapeSliceDelimiters bool } // DebugFunc is the type of function called to log parse events. diff --git a/struct.go b/struct.go index 7e09863..7c77e7b 100644 --- a/struct.go +++ b/struct.go @@ -533,7 +533,11 @@ func reflectSliceWithProperType(key *Key, field reflect.Value, delim string, all for i := 0; i < field.Len(); i++ { switch sliceOf { case reflect.String: - buf.WriteString(escapeDelimited(slice.Index(i).String(), delim)) + val := slice.Index(i).String() + if key.s.f.options.EscapeSliceDelimiters { + val = escapeDelimited(val, delim) + } + buf.WriteString(val) case reflect.Int, reflect.Int64: fmt.Fprint(&buf, slice.Index(i).Int()) case reflect.Uint, reflect.Uint64: diff --git a/struct_test.go b/struct_test.go index 2deffd6..057c4d7 100644 --- a/struct_test.go +++ b/struct_test.go @@ -671,12 +671,12 @@ func Test_ReflectFromStructNonUniqueSections(t *testing.T) { { PublicKey: "pubClientKey", PresharedKey: "psKey", - AllowedIPs: []string{"10.2.0.2/32", "fd00:2::2/128"}, + AllowedIPs: []string{"10.2.0.2/32,fd00:2::2/128"}, }, { PublicKey: "pubClientKey2", PresharedKey: "psKey2", - AllowedIPs: []string{"10.2.0.3/32", "fd00:2::3/128"}, + AllowedIPs: []string{"10.2.0.3/32,fd00:2::3/128"}, }, }, } @@ -697,12 +697,12 @@ func Test_ReflectFromStructNonUniqueSections(t *testing.T) { { PublicKey: "pubClientKey3", PresharedKey: "psKey3", - AllowedIPs: []string{"10.2.0.4/32", "fd00:2::4/128"}, + AllowedIPs: []string{"10.2.0.4/32,fd00:2::4/128"}, }, { PublicKey: "pubClientKey4", PresharedKey: "psKey4", - AllowedIPs: []string{"10.2.0.5/32", "fd00:2::5/128"}, + AllowedIPs: []string{"10.2.0.5/32,fd00:2::5/128"}, }, }) @@ -733,7 +733,7 @@ AllowedIPs = 10.2.0.5/32,fd00:2::5/128 err = cfg.Section("Peer").ReflectFrom(&testPeer{ PublicKey: "pubClientKey5", PresharedKey: "psKey5", - AllowedIPs: []string{"10.2.0.6/32", "fd00:2::6/128"}, + AllowedIPs: []string{"10.2.0.6/32,fd00:2::6/128"}, }) require.NoError(t, err) @@ -941,25 +941,34 @@ func Test_ReflectFromStringSliceWithComma(t *testing.T) { "Test One", "Test Two", "Test with comma, Three", + `C:\dir\`, "Test Four", }, } - f := Empty() - require.NoError(t, f.ReflectFrom(&in)) + t.Run("escaped with EscapeSliceDelimiters", func(t *testing.T) { + f := Empty(LoadOptions{EscapeSliceDelimiters: true}) + require.NoError(t, f.ReflectFrom(&in)) - var buf bytes.Buffer - _, err := f.WriteTo(&buf) - require.NoError(t, err) - assert.Contains(t, buf.String(), `Test with comma\, Three`) + var buf bytes.Buffer + _, err := f.WriteTo(&buf) + require.NoError(t, err) + assert.Contains(t, buf.String(), `Test with comma\, Three`) - got := f.Section("").Key("History").Strings(",") - assert.Equal(t, in.History, got) + got := f.Section("").Key("History").Strings(",") + assert.Equal(t, in.History, got) - var out C - require.NoError(t, MapTo(&out, buf.Bytes())) - assert.Equal(t, in.History, out.History) - assert.Equal(t, float32(1.5), out.Scale) + var out C + require.NoError(t, MapTo(&out, buf.Bytes())) + assert.Equal(t, in.History, out.History) + assert.Equal(t, float32(1.5), out.Scale) + }) + + t.Run("written as is by default", func(t *testing.T) { + f := Empty() + require.NoError(t, f.ReflectFrom(&in)) + assert.Equal(t, `Test One,Test Two,Test with comma, Three,C:\dir\,Test Four`, f.Section("").Key("History").String()) + }) } type Employer struct {