From 37324ca171f33deb8febbdc0c0f949aa7c52e39d Mon Sep 17 00:00:00 2001 From: team-humaki Date: Sun, 13 Sep 2026 08:40:23 -0700 Subject: [PATCH] parser: keep # and ; inside quoted values Fixes #393 --- ini_test.go | 26 ++++++++++++++++++ parser.go | 77 ++++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 91 insertions(+), 12 deletions(-) diff --git a/ini_test.go b/ini_test.go index a3d3b4c..493ccb2 100644 --- a/ini_test.go +++ b/ini_test.go @@ -102,6 +102,32 @@ func TestLoad(t *testing.T) { assert.Equal(t, "U", githubValue) }) + t.Run("keep comment symbols inside quoted values", func(t *testing.T) { + f, err := Load([]byte(` +[background] +primary-color = '#e18477' +secondary-color = "#000000" +hash-in-backticks = ` + "`" + `#e18477` + "`" + ` +semi-single = 'foo;bar' +semi-double = "foo;bar" +quoted-then-comment = '#e18477' ; trailing +double-then-comment = "#000000" # trailing +`)) + require.NoError(t, err) + require.NotNil(t, f) + + sec := f.Section("background") + assert.Equal(t, "#e18477", sec.Key("primary-color").String()) + assert.Equal(t, "#000000", sec.Key("secondary-color").String()) + assert.Equal(t, "#e18477", sec.Key("hash-in-backticks").String()) + assert.Equal(t, "foo;bar", sec.Key("semi-single").String()) + assert.Equal(t, "foo;bar", sec.Key("semi-double").String()) + assert.Equal(t, "#e18477", sec.Key("quoted-then-comment").String()) + assert.Equal(t, "; trailing", sec.Key("quoted-then-comment").Comment) + assert.Equal(t, "#000000", sec.Key("double-then-comment").String()) + assert.Equal(t, "# trailing", sec.Key("double-then-comment").Comment) + }) + t.Run("cannot parse small python-compatible INI files", func(t *testing.T) { f, err := Load([]byte(` [long] diff --git a/parser.go b/parser.go index 513b19e..26684ca 100644 --- a/parser.go +++ b/parser.go @@ -233,6 +233,70 @@ func hasSurroundedQuote(in string, quote byte) bool { strings.IndexByte(in[1:], quote) == len(in)-2 } +func isValueQuote(c byte) bool { + return c == '\'' || c == '"' || c == '`' +} + +// indexOutsideQuotes is strings.Index for sep, skipping bytes inside quotes. +func indexOutsideQuotes(line, sep string) int { + if sep == "" { + return 0 + } + inQuote := byte(0) + n := len(sep) + for i := 0; i <= len(line)-n; i++ { + c := line[i] + if inQuote != 0 { + if c == inQuote { + inQuote = 0 + } + continue + } + if isValueQuote(c) { + inQuote = c + continue + } + if line[i:i+n] == sep { + return i + } + } + return -1 +} + +func indexAnyOutsideQuotes(line, chars string) int { + inQuote := byte(0) + for i := 0; i < len(line); i++ { + c := line[i] + if inQuote != 0 { + if c == inQuote { + inQuote = 0 + } + continue + } + if isValueQuote(c) { + inQuote = c + continue + } + if strings.IndexByte(chars, c) >= 0 { + return i + } + } + return -1 +} + +// indexInlineComment finds an inline comment, ignoring # and ; inside quotes. +// SpaceBeforeInlineComment keeps the existing search order: " #" then " ;". +func indexInlineComment(line string, spaceBefore bool) int { + if spaceBefore { + i := indexOutsideQuotes(line, " #") + if i == -1 { + i = indexOutsideQuotes(line, " ;") + } + return i + } + return indexAnyOutsideQuotes(line, "#;") +} + func (p *parser) readValue(in []byte, bufferSize int) (string, error) { line := strings.TrimLeftFunc(string(in), unicode.IsSpace) @@ -278,22 +342,11 @@ func (p *parser) readValue(in []byte, bufferSize int) (string, error) { // Check if ignore inline comment if !p.options.IgnoreInlineComment { - var i int - if p.options.SpaceBeforeInlineComment { - i = strings.Index(line, " #") - if i == -1 { - i = strings.Index(line, " ;") - } - - } else { - i = strings.IndexAny(line, "#;") - } - + i := indexInlineComment(line, p.options.SpaceBeforeInlineComment) if i > -1 { p.comment.WriteString(line[i:]) line = strings.TrimSpace(line[:i]) } - } // Trim single and double quotes