From f5f4d1a2f5cf2d5559b0ddf789416cd99fcfbef1 Mon Sep 17 00:00:00 2001 From: Team Humaki Date: Thu, 17 Sep 2026 07:14:09 -0700 Subject: [PATCH] parser: treat even trailing backslashes as literal Fixes #347 --- parser.go | 21 ++++++++++++++++----- parser_test.go | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/parser.go b/parser.go index 513b19e..fe65286 100644 --- a/parser.go +++ b/parser.go @@ -184,7 +184,7 @@ func (p *parser) readMultilines(line, val, valQuote string) (string, error) { if pos > -1 { // Check if the line ends with backslash continuation after the quote restOfLine := strings.TrimRight(next[pos+len(valQuote):], "\r\n") - if !p.options.IgnoreContinuation && strings.HasSuffix(strings.TrimSpace(restOfLine), `\`) { + if !p.options.IgnoreContinuation && isLineContinuation(strings.TrimSpace(restOfLine)) { val += next continue } @@ -217,7 +217,7 @@ func (p *parser) readContinuationLines(val string) (string, error) { break } val += next - if val[len(val)-1] != '\\' { + if !isLineContinuation(val) { break } val = val[:len(val)-1] @@ -225,6 +225,17 @@ func (p *parser) readContinuationLines(val string) (string, error) { return val, nil } +// isLineContinuation reports whether s ends with an unescaped continuation +// backslash. An even number of trailing backslashes is a literal value (for +// example a Windows path Y:\\_1\\); only an odd count continues the next line. +func isLineContinuation(s string) bool { + n := 0 + for i := len(s) - 1; i >= 0 && s[i] == '\\'; i-- { + n++ + } + return n%2 == 1 +} + // hasSurroundedQuote check if and only if the first and last characters // are quotes \" or \'. // It returns false if any other parts also contain same kind of quotes. @@ -269,10 +280,10 @@ func (p *parser) readValue(in []byte, bufferSize int) (string, error) { lastChar := line[len(line)-1] // Won't be able to reach here if value only contains whitespace line = strings.TrimSpace(line) - trimmedLastChar := line[len(line)-1] - // Check continuation lines when desired - if !p.options.IgnoreContinuation && trimmedLastChar == '\\' { + // Check continuation lines when desired. Only an odd number of trailing + // backslashes is a continuation; even counts are a literal trailing '\'. + if !p.options.IgnoreContinuation && isLineContinuation(line) { return p.readContinuationLines(line[:len(line)-1]) } diff --git a/parser_test.go b/parser_test.go index 7016d67..20a535c 100644 --- a/parser_test.go +++ b/parser_test.go @@ -42,6 +42,43 @@ func TestBOM(t *testing.T) { }) } +func TestEvenTrailingBackslashIsNotContinuation(t *testing.T) { + t.Run("windows path ending with even backslashes keeps the next key", func(t *testing.T) { + f, err := Load([]byte(`[dirs] +cert = Y:\\_1\\ +serial = 12345678 +`)) + require.NoError(t, err) + require.NotNil(t, f) + + assert.Equal(t, `Y:\\_1\\`, f.Section("dirs").Key("cert").String()) + assert.Equal(t, "12345678", f.Section("dirs").Key("serial").String()) + }) + + t.Run("odd trailing backslash still continues onto the next line", func(t *testing.T) { + f, err := Load([]byte(` +key1=a\b\ +key2=c\d\ +`)) + require.NoError(t, err) + require.NotNil(t, f) + + assert.Equal(t, `a\bkey2=c\d`, f.Section("").Key("key1").String()) + }) + + t.Run("single trailing backslash continues", func(t *testing.T) { + f, err := Load([]byte(`key=hello \ +world +next=ok +`)) + require.NoError(t, err) + require.NotNil(t, f) + + assert.Equal(t, "hello world", f.Section("").Key("key").String()) + assert.Equal(t, "ok", f.Section("").Key("next").String()) + }) +} + func TestBadLoad(t *testing.T) { t.Run("load with bad data", func(t *testing.T) { t.Run("bad section name", func(t *testing.T) {