From 2e7f6a16a91f84f7e5dc3d7bad02707ba688afd4 Mon Sep 17 00:00:00 2001 From: Team Humaki Date: Thu, 17 Sep 2026 01:38:05 -0700 Subject: [PATCH] file: quote keys using the write delimiter Do not wrap key names in backticks just because they contain a parse-time extra delimiter such as ':'. Samba-style keys like fruit:metadata are valid when the write delimiter is '='. Fixes #355 --- file.go | 34 ++++++++++++++++++++++------------ file_test.go | 20 ++++++++++++++++++++ 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/file.go b/file.go index 58beabc..2d7aa8e 100644 --- a/file.go +++ b/file.go @@ -332,6 +332,25 @@ func (f *File) Append(source interface{}, others ...interface{}) error { return f.Reload() } +// quoteKeyNameOnWrite surrounds a key if writing it raw would be ambiguous +// given the write-side delimiter. Parse-time extra delimiters (':' in the +// default "=:" set) must not force quoting — keys like Samba's +// "fruit:metadata" are valid when the write delimiter is "=". +func (f *File) quoteKeyNameOnWrite(kname string) string { + writeDelim := f.options.KeyValueDelimiterOnWrite + if writeDelim == "" { + writeDelim = "=" + } + switch { + case strings.Contains(kname, `"`) || strings.ContainsAny(kname, writeDelim): + return "`" + kname + "`" + case strings.Contains(kname, "`"): + return `"""` + kname + `"""` + default: + return kname + } +} + func (f *File) writeToBuffer(indent string) (*bytes.Buffer, error) { equalSign := DefaultFormatLeft + f.options.KeyValueDelimiterOnWrite + DefaultFormatRight @@ -392,14 +411,7 @@ func (f *File) writeToBuffer(indent string) (*bytes.Buffer, error) { alignLength := 0 if PrettyFormat { for _, kname := range sec.keyList { - keyLength := len(kname) - // First case will surround key by ` and second by """ - if strings.Contains(kname, "\"") || strings.ContainsAny(kname, f.options.KeyValueDelimiters) { - keyLength += 2 - } else if strings.Contains(kname, "`") { - keyLength += 6 - } - + keyLength := len(f.quoteKeyNameOnWrite(kname)) if keyLength > alignLength { alignLength = keyLength } @@ -437,10 +449,8 @@ func (f *File) writeToBuffer(indent string) (*bytes.Buffer, error) { switch { case key.isAutoIncrement: kname = "-" - case strings.Contains(kname, "\"") || strings.ContainsAny(kname, f.options.KeyValueDelimiters): - kname = "`" + kname + "`" - case strings.Contains(kname, "`"): - kname = `"""` + kname + `"""` + default: + kname = f.quoteKeyNameOnWrite(kname) } writeKeyValue := func(val string) (bool, error) { diff --git a/file_test.go b/file_test.go index 306c0c8..32bbb97 100644 --- a/file_test.go +++ b/file_test.go @@ -519,6 +519,26 @@ Note -> Hello world! assert.Equal(t, expected, actual.Bytes()) } +func TestFile_WriteKeyWithColonInName(t *testing.T) { + f := Empty() + sec, err := f.NewSection("global") + require.NoError(t, err) + _, err = sec.NewKey("vfs objects", "catia fruit streams_xattr") + require.NoError(t, err) + _, err = sec.NewKey("fruit:metadata", "stream") + require.NoError(t, err) + _, err = sec.NewKey("a=b", "quoted-because-write-delim") + require.NoError(t, err) + + var buf bytes.Buffer + _, err = f.WriteTo(&buf) + require.NoError(t, err) + out := buf.String() + assert.Contains(t, out, "fruit:metadata") + assert.NotContains(t, out, "`fruit:metadata`") + assert.Contains(t, out, "`a=b`") +} + // Inspired by https://github.com/go-ini/ini/issues/207 func TestReloadAfterShadowLoad(t *testing.T) { f, err := ShadowLoad([]byte(`