From 7e767a6d50c8fbb75ba784709e740acaa94b67f3 Mon Sep 17 00:00:00 2001 From: AdamMagued Date: Fri, 2 Oct 2026 14:50:35 +0000 Subject: [PATCH] fix: preserve comment characters inside quoted values --- parser.go | 60 +++++++++++++++++++++++------ parser_test.go | 101 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 12 deletions(-) diff --git a/parser.go b/parser.go index 513b19e..2237da0 100644 --- a/parser.go +++ b/parser.go @@ -23,6 +23,7 @@ import ( "strconv" "strings" "unicode" + "unicode/utf8" ) const minReaderBufferSize = 4096 @@ -233,6 +234,52 @@ func hasSurroundedQuote(in string, quote byte) bool { strings.IndexByte(in[1:], quote) == len(in)-2 } +func isAlphaNum(s string, i int) bool { + if i <= 0 { + return false + } + r, _ := utf8.DecodeLastRuneInString(s[:i]) + return unicode.IsLetter(r) || unicode.IsDigit(r) +} + +// findInlineComment finds the index of the inline comment delimiter in line. +// It skips comment delimiters that occur inside matched quotes (' or "). +func findInlineComment(line string, spaceBefore bool) int { + for i := 0; i < len(line); i++ { + if (line[i] == '\'' || line[i] == '"') && !isAlphaNum(line, i) { + q := line[i] + matched := false + for j := i + 1; j < len(line); j++ { + if line[j] == q { + backslashes := 0 + for k := j - 1; k >= i+1 && line[k] == '\\'; k-- { + backslashes++ + } + if backslashes%2 == 0 { + i = j + matched = true + break + } + } + } + if matched { + continue + } + } + + if spaceBefore { + if strings.HasPrefix(line[i:], " #") || strings.HasPrefix(line[i:], " ;") { + return i + } + } else { + if line[i] == '#' || line[i] == ';' { + return i + } + } + } + return -1 +} + func (p *parser) readValue(in []byte, bufferSize int) (string, error) { line := strings.TrimLeftFunc(string(in), unicode.IsSpace) @@ -278,22 +325,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 := findInlineComment(line, p.options.SpaceBeforeInlineComment) if i > -1 { p.comment.WriteString(line[i:]) line = strings.TrimSpace(line[:i]) } - } // Trim single and double quotes diff --git a/parser_test.go b/parser_test.go index 7016d67..ca89253 100644 --- a/parser_test.go +++ b/parser_test.go @@ -75,3 +75,104 @@ func TestBadLoad(t *testing.T) { }) }) } + +func TestCommentInsideQuotes(t *testing.T) { + t.Run("preserve comment characters inside single quoted values", func(t *testing.T) { + f, err := Load([]byte(` +[background] +primary-color = '#e18477' +secondary-color = '#000000' +`)) + require.NoError(t, err) + require.NotNil(t, f) + + assert.Equal(t, "#e18477", f.Section("background").Key("primary-color").String()) + assert.Equal(t, "#000000", f.Section("background").Key("secondary-color").String()) + }) + + t.Run("preserve comment characters inside double quoted values", func(t *testing.T) { + f, err := Load([]byte(` +[background] +primary-color = "#e18477" +secondary-color = "#000000" +`)) + require.NoError(t, err) + require.NotNil(t, f) + + assert.Equal(t, "#e18477", f.Section("background").Key("primary-color").String()) + assert.Equal(t, "#000000", f.Section("background").Key("secondary-color").String()) + }) + + t.Run("quoted values with trailing inline comments", func(t *testing.T) { + f, err := Load([]byte(` +[background] +primary-color = '#e18477' # inline comment +secondary-color = "#000000" ; another comment +`)) + require.NoError(t, err) + require.NotNil(t, f) + + assert.Equal(t, "#e18477", f.Section("background").Key("primary-color").String()) + assert.Equal(t, "# inline comment", f.Section("background").Key("primary-color").Comment) + assert.Equal(t, "#000000", f.Section("background").Key("secondary-color").String()) + assert.Equal(t, "; another comment", f.Section("background").Key("secondary-color").Comment) + }) + + t.Run("preserve surrounded quotes with comment characters inside", func(t *testing.T) { + f, err := LoadSources(LoadOptions{ + PreserveSurroundedQuote: true, + }, []byte(` +[background] +primary-color = '#e18477' +secondary-color = "#000000" +`)) + require.NoError(t, err) + require.NotNil(t, f) + + assert.Equal(t, "'#e18477'", f.Section("background").Key("primary-color").String()) + assert.Equal(t, "\"#000000\"", f.Section("background").Key("secondary-color").String()) + }) + + t.Run("space before inline comment with comment characters inside quotes", func(t *testing.T) { + f, err := LoadSources(LoadOptions{ + SpaceBeforeInlineComment: true, + }, []byte(` +[background] +primary-color = '#e18477' # comment +with-space = 'color # 1' # comment 2 +`)) + require.NoError(t, err) + require.NotNil(t, f) + + assert.Equal(t, "#e18477", f.Section("background").Key("primary-color").String()) + assert.Equal(t, "# comment", f.Section("background").Key("primary-color").Comment) + assert.Equal(t, "color # 1", f.Section("background").Key("with-space").String()) + assert.Equal(t, "# comment 2", f.Section("background").Key("with-space").Comment) + }) + + t.Run("url containing hash symbol inside quotes", func(t *testing.T) { + f, err := Load([]byte(` +[links] +docs = 'https://example.com/page#overview' +issue = "https://github.com/go-ini/ini/issues#393" ; see issue +`)) + require.NoError(t, err) + require.NotNil(t, f) + + assert.Equal(t, "https://example.com/page#overview", f.Section("links").Key("docs").String()) + assert.Equal(t, "https://github.com/go-ini/ini/issues#393", f.Section("links").Key("issue").String()) + assert.Equal(t, "; see issue", f.Section("links").Key("issue").Comment) + }) + + t.Run("unquoted contraction with inline comment", func(t *testing.T) { + f, err := Load([]byte(` +[general] +action = don't # comment +`)) + require.NoError(t, err) + require.NotNil(t, f) + + assert.Equal(t, "don't", f.Section("general").Key("action").String()) + assert.Equal(t, "# comment", f.Section("general").Key("action").Comment) + }) +}