Skip to content

Commit ee0d6af

Browse files
github-actions[bot]AkashKumar7902Watson1978
authored
Backport(v1.19): config: accept empty lines in quoted strings (#5448) (#5478)
<!-- Thank you for contributing to Fluentd! Your commits need to follow DCO: https://probot.github.io/apps/dco/ And please provide the following information to help us make the most of your pull request: --> **Which issue(s) this PR fixes**: Backport #5448 Fixes #4691 **What this PR does / why we need it**: The quoted configuration values previously handled a physical line ending together with its preceding character. When the parser was already positioned at a line ending—such as on an empty line or after an escaped literal backslash—it instead reported an unexpected end of file. Handle backslash line continuations and preserved line endings explicitly. This accepts empty lines, preserves LF and CRLF, and retains the existing single-backslash continuation behavior. **Docs Changes**: None. The existing multiline-string documentation already describes the intended behavior. **Release Note**: config: accept empty lines in quoted strings. **Testing**: Automated: - `bundle exec rake test TEST=test/config/test_literal_parser.rb` — 220 tests, 223 assertions, 0 failures, 0 errors - `TEST_ENV_NUMBER=focus4691 bundle exec rake test TEST=test/config/test_config_parser.rb` — 56 tests, 106 assertions, 0 failures, 0 errors - `bundle exec rake test` (two independent runs) — 4,343 tests, 15,829 and 15,830 assertions respectively; 0 failures, 0 errors, 3 pendings, and 36 omissions in both - `rubocop` — 459 files inspected, no offenses - `ruby -c` for all three changed Ruby files and `git diff --check` Manual (Ruby 4.0.6): - Built and separately installed gems from baseline `f380d996bc828b5cd488b578018c06826ebcbf7e` and candidate `7d017869a55e179da4d195806f26d33dc485fc9d`. - The baseline built gem rejected the reproducing configuration with a false unexpected-EOF error; the candidate built gem accepted it with `fluentd --dry-run`; a genuinely unterminated double-quoted value remained rejected. - Ran the candidate built gem as a separate `fluentd --no-supervisor` process through `dummy` → `record_transformer` → `stdout` and externally parsed its emitted JSON as `{"message":"seed","blank":"world\n\n","literal_backslash":"left\\\nright","continued":"leftright"}`. Signed-off-by: Akash Kumar <meakash7902@gmail.com> Signed-off-by: Shizuo Fujita <fujita@clear-code.com> Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Akash Kumar <91385321+AkashKumar7902@users.noreply.github.com> Co-authored-by: Shizuo Fujita <fujita@clear-code.com>
1 parent 577cbc7 commit ee0d6af

4 files changed

Lines changed: 31 additions & 6 deletions

File tree

lib/fluent/config/basic_parser.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ def initialize(strscan)
2828
SPACING = /(?:[ \t\r\n]|\z|\#.*?(?:\z|[\r\n]))+/
2929
ZERO_OR_MORE_SPACING = /(?:[ \t\r\n]|\z|\#.*?(?:\z|[\r\n]))*/
3030
SPACING_WITHOUT_COMMENT = /(?:[ \t\r\n]|\z)+/
31-
LINE_END_WITHOUT_SPACING_AND_COMMENT = /(?:\z|[\r\n])/
3231

3332
module ClassMethods
3433
def symbol(string)

lib/fluent/config/literal_parser.rb

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@
2727
module Fluent
2828
module Config
2929
class LiteralParser < BasicParser
30+
# A physical line break (LF, CR, or CRLF) inside a quoted string.
31+
# CRLF is normalized to LF so that the same config text does not produce a
32+
# different value depending on whether the file was saved with LF or CRLF.
33+
# A lone CR is kept as-is, and an escaped "\r\n" still produces CRLF.
34+
LINE_BREAK = /\r\n|[\r\n]/
35+
# A backslash immediately followed by a physical line break.
36+
# It works as a line continuation, so both are stripped from the value.
37+
LINE_CONTINUATION = /\\#{LINE_BREAK}/o
38+
3039
def self.unescape_char(c)
3140
case c
3241
when '"'
@@ -99,11 +108,10 @@ def scan_double_quoted_string
99108
else
100109
return string.join
101110
end
102-
elsif check(/[^"]#{LINE_END_WITHOUT_SPACING_AND_COMMENT}/o)
103-
if s = check(/[^\\]#{LINE_END_WITHOUT_SPACING_AND_COMMENT}/o)
104-
string << s
105-
end
106-
skip(/[^"]#{LINE_END_WITHOUT_SPACING_AND_COMMENT}/o)
111+
elsif skip(LINE_CONTINUATION)
112+
next
113+
elsif s = scan(LINE_BREAK)
114+
string << (s == "\r\n" ? "\n" : s)
107115
elsif s = scan(/\\./)
108116
string << eval_escape_char(s[1,1])
109117
elsif skip(/\#\{/)
@@ -126,6 +134,8 @@ def scan_single_quoted_string
126134
string << "'"
127135
elsif s = scan(/\\\\/)
128136
string << "\\"
137+
elsif s = scan(LINE_BREAK)
138+
string << (s == "\r\n" ? "\n" : s)
129139
elsif s = scan(/./)
130140
string << s
131141
else

test/config/test_config_parser.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ def parse_text(text)
149149
end
150150

151151
test "support multiline string" do
152+
assert_text_parsed_as(e('ROOT', '', {"k1" => "world\n\n"}), "k1 \"world\n\n\"")
152153
assert_text_parsed_as(e('ROOT', '',
153154
{"k1" => %[line1
154155
line2]

test/config/test_literal_parser.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,14 @@ def test_falseX
111111
test('"t') { assert_parse_error('"t') } # non-terminated quoted character
112112
test("\"t\nt\"") { assert_text_parsed_as("t\nt", "\"t\nt\"" ) } # multiline string
113113
test("\"t\\\nt\"") { assert_text_parsed_as("tt", "\"t\\\nt\"" ) } # multiline string
114+
test("\"t\n\nt\"") { assert_text_parsed_as("t\n\nt", "\"t\n\nt\"") }
115+
test("\"\nt\"") { assert_text_parsed_as("\nt", "\"\nt\"") }
116+
test("\"t\\\\\nt\"") { assert_text_parsed_as("t\\\nt", "\"t\\\\\nt\"") }
117+
test("\"t\r\nt\"") { assert_text_parsed_as("t\nt", "\"t\r\nt\"") }
118+
test("\"t\rt\"") { assert_text_parsed_as("t\rt", "\"t\rt\"") }
119+
test("\"t\\\r\nt\"") { assert_text_parsed_as("tt", "\"t\\\r\nt\"") }
120+
test("\"t\n") { assert_parse_error("\"t\n") }
121+
test("\"t\\\n") { assert_parse_error("\"t\\\n") }
114122
test('t"') { assert_text_parsed_as('t"', 't"') }
115123
test('"."') { assert_text_parsed_as('.', '"."') }
116124
test('"*"') { assert_text_parsed_as('*', '"*"') }
@@ -138,6 +146,13 @@ def test_falseX
138146
test("'\\0'") { assert_text_parsed_as('\0', "'\\0'") }
139147
test("'\\1'") { assert_text_parsed_as('\1', "'\\1'") }
140148
test("'t") { assert_parse_error("'t") } # non-terminated quoted character
149+
test("'t\nt'") { assert_text_parsed_as("t\nt", "'t\nt'") }
150+
test("'t\n\nt'") { assert_text_parsed_as("t\n\nt", "'t\n\nt'") }
151+
test("'\nt'") { assert_text_parsed_as("\nt", "'\nt'") }
152+
test("'t\r\nt'") { assert_text_parsed_as("t\nt", "'t\r\nt'") }
153+
test("'t\rt'") { assert_text_parsed_as("t\rt", "'t\rt'") }
154+
test("'t\\\nt'") { assert_text_parsed_as("t\\\nt", "'t\\\nt'") }
155+
test("'t\n") { assert_parse_error("'t\n") }
141156
test("t'") { assert_text_parsed_as("t'", "t'") }
142157
test("'.'") { assert_text_parsed_as('.', "'.'") }
143158
test("'*'") { assert_text_parsed_as('*', "'*'") }

0 commit comments

Comments
 (0)