diff --git a/lib/fluent/command/cat.rb b/lib/fluent/command/cat.rb
index d313c37472..ba0ab9c35a 100644
--- a/lib/fluent/command/cat.rb
+++ b/lib/fluent/command/cat.rb
@@ -326,7 +326,7 @@ def abort_message(time, record)
when 'json'
begin
while line = $stdin.gets
- record = JSON.parse(line, Fluent::DEFAULT_JSON_PARSE_OPTIONS)
+ record = JSON.parse(line, **Fluent::DEFAULT_JSON_PARSE_OPTIONS)
w.write(record)
end
rescue
diff --git a/lib/fluent/compat/exec_util.rb b/lib/fluent/compat/exec_util.rb
index ac0f034459..c83b6da31f 100644
--- a/lib/fluent/compat/exec_util.rb
+++ b/lib/fluent/compat/exec_util.rb
@@ -80,7 +80,7 @@ class JSONParser < Parser
BYTES_TO_READ = 8192
def call(io)
- parser = JSON::ResumableParser.new(Fluent::DEFAULT_JSON_PARSE_OPTIONS)
+ parser = JSON::ResumableParser.new(**Fluent::DEFAULT_JSON_PARSE_OPTIONS)
begin
chunk = +"".b
while io.readpartial(BYTES_TO_READ, chunk)
diff --git a/lib/fluent/config/literal_parser.rb b/lib/fluent/config/literal_parser.rb
index febe6f2bb5..eb6b0710ee 100644
--- a/lib/fluent/config/literal_parser.rb
+++ b/lib/fluent/config/literal_parser.rb
@@ -253,7 +253,7 @@ def scan_json(is_array)
# '{"foo":"bar", #' -> '{"foo":"bar"}' (to check)
parsed = nil
begin
- parsed = JSON.parse(buffer + line_buffer.rstrip.sub(/,$/, '') + (is_array ? "]" : "}"), Fluent::DEFAULT_JSON_PARSE_OPTIONS)
+ parsed = JSON.parse(buffer + line_buffer.rstrip.sub(/,$/, '') + (is_array ? "]" : "}"), **Fluent::DEFAULT_JSON_PARSE_OPTIONS)
rescue JSON::ParserError
# This '#' is in json string literals
end
@@ -288,7 +288,7 @@ def scan_json(is_array)
line_buffer << char
begin
- result = JSON.parse(buffer + line_buffer, Fluent::DEFAULT_JSON_PARSE_OPTIONS)
+ result = JSON.parse(buffer + line_buffer, **Fluent::DEFAULT_JSON_PARSE_OPTIONS)
rescue JSON::ParserError
# Incomplete json string yet
end
diff --git a/lib/fluent/config/types.rb b/lib/fluent/config/types.rb
index 2fe5cd4c8e..26f104e4dd 100644
--- a/lib/fluent/config/types.rb
+++ b/lib/fluent/config/types.rb
@@ -201,7 +201,7 @@ def self.hash_value(val, opts = {}, name = nil)
return nil if val.nil?
param = if val.is_a?(String)
- val.start_with?('{') ? JSON.parse(val, Fluent::DEFAULT_JSON_PARSE_OPTIONS) : Hash[val.strip.split(/\s*,\s*/).map{|v| v.split(':', 2)}]
+ val.start_with?('{') ? JSON.parse(val, **Fluent::DEFAULT_JSON_PARSE_OPTIONS) : Hash[val.strip.split(/\s*,\s*/).map{|v| v.split(':', 2)}]
else
val
end
@@ -228,7 +228,7 @@ def self.array_value(val, opts = {}, name = nil)
return nil if val.nil?
param = if val.is_a?(String)
- val.start_with?('[') ? JSON.parse(val, Fluent::DEFAULT_JSON_PARSE_OPTIONS) : val.strip.split(/\s*,\s*/)
+ val.start_with?('[') ? JSON.parse(val, **Fluent::DEFAULT_JSON_PARSE_OPTIONS) : val.strip.split(/\s*,\s*/)
elsif val.is_a?(Array)
val
elsif val.is_a?(Numeric) || val == true || val == false
diff --git a/lib/fluent/daemon.rb b/lib/fluent/daemon.rb
index ed45758042..b76bbb9da1 100644
--- a/lib/fluent/daemon.rb
+++ b/lib/fluent/daemon.rb
@@ -10,5 +10,5 @@
server_module = Fluent.const_get(ARGV[0])
worker_module = Fluent.const_get(ARGV[1])
-params = JSON.parse(ARGV[2], Fluent::DEFAULT_JSON_PARSE_OPTIONS)
+params = JSON.parse(ARGV[2], **Fluent::DEFAULT_JSON_PARSE_OPTIONS)
ServerEngine::Daemon.run_server(server_module, worker_module) { Fluent::Supervisor.serverengine_config(params) }
diff --git a/lib/fluent/plugin/filter_record_transformer.rb b/lib/fluent/plugin/filter_record_transformer.rb
index 57e4cba260..bccb09299d 100644
--- a/lib/fluent/plugin/filter_record_transformer.rb
+++ b/lib/fluent/plugin/filter_record_transformer.rb
@@ -117,7 +117,7 @@ def filter_stream(tag, es)
def parse_value(value_str)
if value_str.start_with?('{', '[')
- JSON.parse(value_str, Fluent::DEFAULT_JSON_PARSE_OPTIONS)
+ JSON.parse(value_str, **Fluent::DEFAULT_JSON_PARSE_OPTIONS)
else
value_str
end
diff --git a/lib/fluent/plugin/in_forward.rb b/lib/fluent/plugin/in_forward.rb
index ba1c81aaf2..98a959faab 100644
--- a/lib/fluent/plugin/in_forward.rb
+++ b/lib/fluent/plugin/in_forward.rb
@@ -257,7 +257,7 @@ def read_messages(conn, &block)
unless feeder
first = data[0]
if first == '{' || first == '[' # json
- parser = JSON::ResumableParser.new(Fluent::DEFAULT_JSON_PARSE_OPTIONS)
+ parser = JSON::ResumableParser.new(**Fluent::DEFAULT_JSON_PARSE_OPTIONS)
serializer = :to_json.to_proc
feeder = ->(d){
parser << d
diff --git a/lib/fluent/plugin/in_monitor_agent.rb b/lib/fluent/plugin/in_monitor_agent.rb
index 3e743a50cc..e4cd5576ca 100644
--- a/lib/fluent/plugin/in_monitor_agent.rb
+++ b/lib/fluent/plugin/in_monitor_agent.rb
@@ -102,7 +102,7 @@ def render_json(obj, code: 200, pretty_json: nil)
end
def render_ltsv(obj, code: 200)
- normalized = JSON.parse(obj.to_json, Fluent::DEFAULT_JSON_PARSE_OPTIONS)
+ normalized = JSON.parse(obj.to_json, **Fluent::DEFAULT_JSON_PARSE_OPTIONS)
text = ''
normalized.each do |hash|
row = []
diff --git a/lib/fluent/plugin/in_sample.rb b/lib/fluent/plugin/in_sample.rb
index a2b5b3926f..1bdc9821f9 100644
--- a/lib/fluent/plugin/in_sample.rb
+++ b/lib/fluent/plugin/in_sample.rb
@@ -45,7 +45,7 @@ class SampleInput < Input
desc "The sample data to be generated. An array of JSON hashes or a single JSON hash."
config_param :sample, alias: :dummy, default: [{"message" => "sample"}] do |val|
begin
- parsed = JSON.parse(val, Fluent::DEFAULT_JSON_PARSE_OPTIONS)
+ parsed = JSON.parse(val, **Fluent::DEFAULT_JSON_PARSE_OPTIONS)
rescue JSON::ParserError => ex
# Fluent::ConfigParseError, "got incomplete JSON" will be raised
# at literal_parser.rb with --use-v1-config, but I had to
diff --git a/lib/fluent/plugin/in_unix.rb b/lib/fluent/plugin/in_unix.rb
index a9df09cfea..21ed482e07 100644
--- a/lib/fluent/plugin/in_unix.rb
+++ b/lib/fluent/plugin/in_unix.rb
@@ -158,7 +158,7 @@ def on_read(data)
first = data[0]
if first == '{'.freeze || first == '['.freeze
m = method(:on_read_json)
- @parser = JSON::ResumableParser.new(Fluent::DEFAULT_JSON_PARSE_OPTIONS)
+ @parser = JSON::ResumableParser.new(**Fluent::DEFAULT_JSON_PARSE_OPTIONS)
else
m = method(:on_read_msgpack)
@parser = Fluent::MessagePackFactory.msgpack_unpacker
diff --git a/lib/fluent/plugin/parser_json.rb b/lib/fluent/plugin/parser_json.rb
index 8687dc4da2..45fe8607c7 100644
--- a/lib/fluent/plugin/parser_json.rb
+++ b/lib/fluent/plugin/parser_json.rb
@@ -38,7 +38,7 @@ class JSONParser < Parser
# Use a shared proc rather than a per-call lambda so that
# configure_json_parser returns the same object every time.
- JSON_PARSE_PROC = ->(text) { JSON.parse(text, Fluent::DEFAULT_JSON_PARSE_OPTIONS) }
+ JSON_PARSE_PROC = ->(text) { JSON.parse(text, **Fluent::DEFAULT_JSON_PARSE_OPTIONS) }
def configure(conf)
if conf.has_key?('time_format')
@@ -96,7 +96,7 @@ def parser_type
end
def parse_io(io, &block)
- parser = JSON::ResumableParser.new(Fluent::DEFAULT_JSON_PARSE_OPTIONS)
+ parser = JSON::ResumableParser.new(**Fluent::DEFAULT_JSON_PARSE_OPTIONS)
begin
chunk = +"".b
while io.readpartial(@stream_buffer_size, chunk)
diff --git a/lib/fluent/plugin/parser_syslog.rb b/lib/fluent/plugin/parser_syslog.rb
index 2b6fcca479..3938c13ed6 100644
--- a/lib/fluent/plugin/parser_syslog.rb
+++ b/lib/fluent/plugin/parser_syslog.rb
@@ -71,6 +71,7 @@ def initialize
@time_parser_rfc5424 = nil
@space_count_rfc3164 = nil
@space_count_rfc5424 = nil
+ @time_format_starts_with_space_rfc3164 = false
@skip_space_count_rfc3164 = false
@skip_space_count_rfc5424 = false
@time_parser_rfc5424_without_subseconds = nil
@@ -123,10 +124,12 @@ class << self
def setup_time_parser_3164(time_fmt)
@time_parser_rfc3164 = time_parser_create(format: time_fmt)
+ @time_format_starts_with_space_rfc3164 = time_fmt.start_with?(SPLIT_CHAR)
if ['%b %d %H:%M:%S', '%b %d %H:%M:%S.%N'].include?(time_fmt)
@skip_space_count_rfc3164 = true
end
@space_count_rfc3164 = time_fmt.squeeze(' ').count(' ') + 1
+ @space_count_rfc3164 -= 1 if @time_format_starts_with_space_rfc3164
end
def setup_time_parser_5424(time_fmt)
@@ -162,6 +165,8 @@ def parse_auto(text, &block)
end
SPLIT_CHAR = ' '.freeze
+ DOT_CHAR = '.'.freeze
+ RFC3164_PRI_DIGITS_REGEXP = /\A[0-9]{1,3}\z/
def parse_rfc3164_regex(text, &block)
idx = 0
@@ -180,13 +185,26 @@ def parse_rfc3164_regex(text, &block)
i = idx - 1
sq = false
+ first_time_field = true
@space_count_rfc3164.times do
while text[i + 1] == SPLIT_CHAR
+ if first_time_field
+ unless @time_format_starts_with_space_rfc3164
+ idx += 1
+ i += 1
+ break
+ end
+ end
sq = true
i += 1
end
+ first_time_field = false
i = text.index(SPLIT_CHAR, i + 1)
+ unless i
+ yield nil, nil
+ return
+ end
end
time_str = sq ? text.slice(idx, i - idx).squeeze(SPLIT_CHAR) : text.slice(idx, i - idx)
@@ -287,16 +305,30 @@ def parse_rfc3164(text, &block)
end
end
+ if text[cursor] == SPLIT_CHAR
+ cursor = rfc3164_space_cursor(text, cursor)
+ unless cursor
+ yield nil, nil
+ return
+ end
+ end
+
if @skip_space_count_rfc3164
# header part
time_size = 15 # skip Mmm dd hh:mm:ss
- time_end = text[cursor + time_size]
+ time_end_index = cursor + time_size
+ time_end = text[time_end_index]
+
if time_end == SPLIT_CHAR
time_str = text.slice(cursor, time_size)
cursor += 16 # time + ' '
- elsif time_end == '.'.freeze
+ elsif time_end == DOT_CHAR
# support subsecond time
- i = text.index(SPLIT_CHAR, time_size)
+ i = text.index(SPLIT_CHAR, time_end_index)
+ unless i
+ yield nil, nil
+ return
+ end
time_str = text.slice(cursor, i - cursor)
cursor = i + 1
else
@@ -312,14 +344,18 @@ def parse_rfc3164(text, &block)
i += 1
end
i = text.index(SPLIT_CHAR, i + 1)
+ unless i
+ yield nil, nil
+ return
+ end
end
- time_str = sq ? text.slice(idx, i - cursor).squeeze(SPLIT_CHAR) : text.slice(cursor, i - cursor)
+ time_str = sq ? text.slice(cursor, i - cursor).squeeze(SPLIT_CHAR) : text.slice(cursor, i - cursor)
cursor = i + 1
end
i = text.index(SPLIT_CHAR, cursor)
- if i.nil?
+ unless i
yield nil, nil
return
end
@@ -363,12 +399,23 @@ def parse_rfc3164(text, &block)
msg.chomp!
record['message'] = msg
- time = @time_parser_rfc3164.parse(time_str)
+ begin
+ time = @time_parser_rfc3164.parse(time_str)
+ rescue Fluent::TimeParser::TimeParseError
+ yield nil, nil
+ return
+ end
record['time'] = time_str if @keep_time_key
yield time, record
end
+ def rfc3164_space_cursor(text, cursor)
+ return if @with_priority && !RFC3164_PRI_DIGITS_REGEXP.match?(text.slice(1, cursor - 2))
+
+ @time_format_starts_with_space_rfc3164 ? cursor : cursor + 1
+ end
+
NILVALUE = '-'.freeze
def parse_rfc5424(text, &block)
diff --git a/lib/fluent/plugin/sd_file.rb b/lib/fluent/plugin/sd_file.rb
index 0b30541c35..322f7814d0 100644
--- a/lib/fluent/plugin/sd_file.rb
+++ b/lib/fluent/plugin/sd_file.rb
@@ -76,7 +76,7 @@ def parser
-> (v) { YAML.safe_load(v).map }
when :json
require 'json'
- -> (v) { JSON.parse(v, Fluent::DEFAULT_JSON_PARSE_OPTIONS) }
+ -> (v) { JSON.parse(v, **Fluent::DEFAULT_JSON_PARSE_OPTIONS) }
end
end
diff --git a/lib/fluent/plugin/storage_local.rb b/lib/fluent/plugin/storage_local.rb
index bc66c83d2e..8afb3f05eb 100644
--- a/lib/fluent/plugin/storage_local.rb
+++ b/lib/fluent/plugin/storage_local.rb
@@ -90,7 +90,7 @@ def configure(conf)
log.warn "detect empty plugin storage file during startup. Ignored: #{@path}"
return
end
- data = JSON.parse(data, Fluent::DEFAULT_JSON_PARSE_OPTIONS)
+ data = JSON.parse(data, **Fluent::DEFAULT_JSON_PARSE_OPTIONS)
raise Fluent::ConfigError, "Invalid contents (not object) in plugin storage file: '#{@path}'" unless data.is_a?(Hash)
rescue => e
log.error "failed to read data from plugin storage file", path: @path, error: e
@@ -114,7 +114,7 @@ def load
return unless File.exist?(@path)
begin
json_string = File.open(@path, 'r:utf-8:utf-8'){ |io| io.read }
- json = JSON.parse(json_string, Fluent::DEFAULT_JSON_PARSE_OPTIONS)
+ json = JSON.parse(json_string, **Fluent::DEFAULT_JSON_PARSE_OPTIONS)
unless json.is_a?(Hash)
log.error "broken content for plugin storage (Hash required: ignored)", type: json.class
log.debug "broken content", content: json_string
diff --git a/test/plugin/test_in_syslog.rb b/test/plugin/test_in_syslog.rb
index 868e77bb04..9132a7f27f 100755
--- a/test/plugin/test_in_syslog.rb
+++ b/test/plugin/test_in_syslog.rb
@@ -124,6 +124,79 @@ def test_time_format(data)
}
end
+ data(
+ 'regexp/rfc3164/parser priority' => ['regexp', 'rfc3164', true],
+ 'string/rfc3164/parser priority' => ['string', 'rfc3164', true],
+ 'regexp/auto/parser priority' => ['regexp', 'auto', true],
+ 'string/auto/parser priority' => ['string', 'auto', true],
+ 'regexp/rfc3164/input priority' => ['regexp', 'rfc3164', false],
+ 'string/rfc3164/input priority' => ['string', 'rfc3164', false],
+ 'regexp/auto/input priority' => ['regexp', 'auto', false],
+ 'string/auto/input priority' => ['string', 'auto', false],
+ )
+ def test_space_between_rfc3164_priority_and_header(data)
+ parser_engine, message_format, with_priority = data
+ d = create_driver([
+ ipv4_config,
+ 'severity_key severity',
+ 'facility_key facility',
+ '',
+ " parser_engine #{parser_engine}",
+ " message_format #{message_format}",
+ " with_priority #{with_priority}",
+ '',
+ ].join("\n"))
+
+ message = 'Apr 25 16:43:29 PAA-SW1-1 General[procLOG]: main.c(257) 272264 %% Stopping System API application'
+ d.run(expect_emits: 2) do
+ u = UDPSocket.new
+ u.connect('127.0.0.1', @port)
+ u.send("<14>#{message}", 0)
+ u.send("<14> #{message}", 0)
+ end
+
+ assert_equal(2, d.events.size)
+ assert_equal(d.events[0][1], d.events[1][1])
+ assert_equal(d.events[0][2], d.events[1][2])
+ d.events.each do |tag, time, record|
+ assert_equal('syslog.user.info', tag)
+ assert_equal(event_time('Apr 25 16:43:29', format: '%b %d %H:%M:%S'), time)
+ assert_equal('user', record['facility'])
+ assert_equal('info', record['severity'])
+ assert_equal('PAA-SW1-1', record['host'])
+ assert_equal('General', record['ident'])
+ assert_equal('main.c(257) 272264 %% Stopping System API application', record['message'])
+ end
+ end
+
+ data('regexp' => 'regexp', 'string' => 'string')
+ def test_space_after_input_owned_priority_preserves_input_priority_syntax(parser_engine)
+ d = create_driver([
+ ipv4_config,
+ 'emit_unmatched_lines true',
+ '',
+ " parser_engine #{parser_engine}",
+ ' with_priority false',
+ '',
+ ].join("\n"))
+
+ messages = [
+ '<1234>Apr 25 16:43:29 host app: message',
+ '<1234> Apr 25 16:43:29 host app: message',
+ ' Apr 25 16:43:29 host app: message',
+ ]
+ d.run(expect_emits: 3) do
+ u = UDPSocket.new
+ u.connect('127.0.0.1', @port)
+ messages.each { |message| u.send(message, 0) }
+ end
+
+ assert_equal(3, d.events.size)
+ assert_equal(d.events[0], d.events[1])
+ assert_equal('syslog.unmatched', d.events[2][0])
+ assert_equal(messages[2], d.events[2][2]['unmatched_line'])
+ end
+
def test_msg_size
d = create_driver
tests = create_test_case
diff --git a/test/plugin/test_parser_syslog.rb b/test/plugin/test_parser_syslog.rb
index dcdbca8388..f7e8655c45 100644
--- a/test/plugin/test_parser_syslog.rb
+++ b/test/plugin/test_parser_syslog.rb
@@ -15,6 +15,25 @@ def setup
}
end
+ def assert_input_rejected(text, expectation)
+ case expectation
+ when :raises
+ assert_raise(Fluent::TimeParser::TimeParseError) do
+ @parser.instance.parse(text) { |time, record| }
+ end
+ when :rejects
+ result = :not_yielded
+ assert_nothing_raised do
+ @parser.instance.parse(text) do |time, record|
+ result = [time, record]
+ end
+ end
+ assert_equal([nil, nil], result)
+ else
+ flunk("unknown expectation: #{expectation.inspect}")
+ end
+ end
+
data('regexp' => 'regexp', 'string' => 'string')
def test_parse(param)
@parser.configure({'parser_type' => param})
@@ -75,6 +94,297 @@ def test_parse_with_priority(param)
assert_equal("%b %d %H:%M:%S", @parser.instance.patterns['time_format'])
end
+ data(
+ 'regexp/rfc3164/parser priority' => ['regexp', 'rfc3164', true],
+ 'string/rfc3164/parser priority' => ['string', 'rfc3164', true],
+ 'regexp/auto/parser priority' => ['regexp', 'auto', true],
+ 'string/auto/parser priority' => ['string', 'auto', true],
+ 'regexp/rfc3164/without priority' => ['regexp', 'rfc3164', false],
+ 'string/rfc3164/without priority' => ['string', 'rfc3164', false],
+ 'regexp/auto/without priority' => ['regexp', 'auto', false],
+ 'string/auto/without priority' => ['string', 'auto', false],
+ )
+ def test_parse_with_space_between_rfc3164_priority_and_header(data)
+ parser_engine, message_format, with_priority = data
+ @parser.configure(
+ 'parser_engine' => parser_engine,
+ 'message_format' => message_format,
+ 'with_priority' => with_priority,
+ 'keep_time_key' => true,
+ )
+
+ prefix = with_priority ? '<14>' : ''
+ message = 'Apr 25 16:43:29 PAA-SW1-1 General[procLOG]: main.c(257) 272264 %% Stopping System API application'
+ results = ["#{prefix}#{message}", "#{prefix} #{message}"].map do |text|
+ result = nil
+ @parser.instance.parse(text) do |time, record|
+ result = [time, record]
+ end
+ result
+ end
+
+ assert_equal(results[0], results[1])
+ time, record = results[1]
+ assert_equal(event_time('Apr 25 16:43:29', format: '%b %d %H:%M:%S'), time)
+ assert_equal(14, record['pri']) if with_priority
+ assert_equal('Apr 25 16:43:29', record['time'])
+ assert_equal('PAA-SW1-1', record['host'])
+ assert_equal('General', record['ident'])
+ assert_equal('main.c(257) 272264 %% Stopping System API application', record['message'])
+ end
+
+ data(
+ 'regexp/single digit day/with priority' => ['regexp', true, '%b %d %H:%M:%S', 'Apr 5 16:43:29'],
+ 'string/single digit day/with priority' => ['string', true, '%b %d %H:%M:%S', 'Apr 5 16:43:29'],
+ 'regexp/subseconds/with priority' => ['regexp', true, '%b %d %H:%M:%S.%N', 'Apr 25 16:43:29.123456789'],
+ 'string/subseconds/with priority' => ['string', true, '%b %d %H:%M:%S.%N', 'Apr 25 16:43:29.123456789'],
+ 'regexp/custom format/with priority' => ['regexp', true, '%Y-%m-%dT%H:%M:%S', '2026-04-25T16:43:29'],
+ 'string/custom format/with priority' => ['string', true, '%Y-%m-%dT%H:%M:%S', '2026-04-25T16:43:29'],
+ 'regexp/single digit day/without priority' => ['regexp', false, '%b %d %H:%M:%S', 'Apr 5 16:43:29'],
+ 'string/single digit day/without priority' => ['string', false, '%b %d %H:%M:%S', 'Apr 5 16:43:29'],
+ 'regexp/subseconds/without priority' => ['regexp', false, '%b %d %H:%M:%S.%N', 'Apr 25 16:43:29.123456789'],
+ 'string/subseconds/without priority' => ['string', false, '%b %d %H:%M:%S.%N', 'Apr 25 16:43:29.123456789'],
+ 'regexp/custom format/without priority' => ['regexp', false, '%Y-%m-%dT%H:%M:%S', '2026-04-25T16:43:29'],
+ 'string/custom format/without priority' => ['string', false, '%Y-%m-%dT%H:%M:%S', '2026-04-25T16:43:29'],
+ )
+ def test_parse_rfc3164_time_variants_with_space_after_priority(data)
+ parser_engine, with_priority, time_format, timestamp = data
+ @parser.configure(
+ 'parser_engine' => parser_engine,
+ 'time_format' => time_format,
+ 'with_priority' => with_priority,
+ 'keep_time_key' => true,
+ )
+
+ suffix = "#{timestamp} host app[123]: message"
+ prefix = with_priority ? '<14>' : ''
+ results = ["#{prefix}#{suffix}", "#{prefix} #{suffix}"].map do |text|
+ result = nil
+ @parser.instance.parse(text) do |time, record|
+ result = [time, record]
+ end
+ result
+ end
+
+ assert_equal(results[0], results[1])
+ expected_time_key = parser_engine == 'regexp' ? timestamp.squeeze(' ') : timestamp
+ assert_equal(expected_time_key, results[1][1]['time'])
+ assert_equal('host', results[1][1]['host'])
+ assert_equal('app', results[1][1]['ident'])
+ assert_equal('123', results[1][1]['pid'])
+ assert_equal('message', results[1][1]['message'])
+ end
+
+ data(
+ 'regexp/rfc3164/parser priority' => ['regexp', 'rfc3164', true],
+ 'string/rfc3164/parser priority' => ['string', 'rfc3164', true],
+ 'regexp/auto/parser priority' => ['regexp', 'auto', true],
+ 'string/auto/parser priority' => ['string', 'auto', true],
+ 'regexp/rfc3164/input priority' => ['regexp', 'rfc3164', false],
+ 'string/rfc3164/input priority' => ['string', 'rfc3164', false],
+ 'regexp/auto/input priority' => ['regexp', 'auto', false],
+ 'string/auto/input priority' => ['string', 'auto', false],
+ )
+ def test_truncated_subsecond_rfc3164_is_rejected_without_raising(data)
+ parser_engine, message_format, with_priority = data
+ @parser.configure(
+ 'parser_engine' => parser_engine,
+ 'message_format' => message_format,
+ 'with_priority' => with_priority,
+ 'time_format' => '%b %d %H:%M:%S.%N',
+ )
+
+ prefix = with_priority ? '<14> ' : ' '
+ timestamp = 'Apr 25 16:43:29.5'
+ valid_result = nil
+ @parser.instance.parse("#{prefix}#{timestamp} host app: message") do |time, record|
+ valid_result = [time, record]
+ end
+ expected_record = {'host' => 'host', 'ident' => 'app', 'message' => 'message'}
+ expected_record['pri'] = 14 if with_priority
+ assert_equal(
+ [event_time(timestamp, format: '%b %d %H:%M:%S.%N'), expected_record],
+ valid_result,
+ )
+
+ result = :not_yielded
+ assert_nothing_raised do
+ @parser.instance.parse("#{prefix}#{timestamp}") do |time, record|
+ result = [time, record]
+ end
+ end
+ assert_equal([nil, nil], result)
+ end
+
+ data(
+ 'rfc3164/parser priority' => ['rfc3164', true],
+ 'auto/parser priority' => ['auto', true],
+ 'rfc3164/input priority' => ['rfc3164', false],
+ 'auto/input priority' => ['auto', false],
+ )
+ def test_non_ascii_rfc3164_header_is_rejected_without_raising(data)
+ message_format, with_priority = data
+ @parser.configure(
+ 'parser_engine' => 'string',
+ 'message_format' => message_format,
+ 'with_priority' => with_priority,
+ )
+ prefix = with_priority ? '<14>' : ''
+
+ assert_input_rejected("#{prefix}Apr 25 16:43:é host app: message", :rejects)
+ end
+
+ data(
+ 'two boundary spaces' => [nil, '<14> Apr 25 16:43:2 host app: message'],
+ 'subsecond format without subsecond' => ['%b %d %H:%M:%S.%N', '<14> Apr 25 16:43:29 host app: message'],
+ 'subsecond format truncated second' => ['%b %d %H:%M:%S.%N', '<14> Apr 25 16:43:2 host app: message'],
+ )
+ def test_broken_timestamp_is_rejected_without_raising(data)
+ time_format, text = data
+ config = {'parser_engine' => 'string', 'with_priority' => true}
+ config['time_format'] = time_format if time_format
+ @parser.configure(config)
+
+ assert_input_rejected(text, :rejects)
+ end
+
+ data(
+ 'regexp/subsecond/parser priority' => ['regexp', '%b %d %H:%M:%S.%L', 'Apr 25', true],
+ 'string/subsecond/parser priority' => ['string', '%b %d %H:%M:%S.%L', 'Apr 25', true],
+ 'regexp/subsecond/input priority' => ['regexp', '%b %d %H:%M:%S.%L', 'Apr 25', false],
+ 'string/subsecond/input priority' => ['string', '%b %d %H:%M:%S.%L', 'Apr 25', false],
+ 'regexp/iso8601/parser priority' => ['regexp', '%Y-%m-%dT%H:%M:%S', '2026-04-25T16:43:29', true],
+ 'string/iso8601/parser priority' => ['string', '%Y-%m-%dT%H:%M:%S', '2026-04-25T16:43:29', true],
+ 'regexp/iso8601/input priority' => ['regexp', '%Y-%m-%dT%H:%M:%S', '2026-04-25T16:43:29', false],
+ 'string/iso8601/input priority' => ['string', '%Y-%m-%dT%H:%M:%S', '2026-04-25T16:43:29', false],
+ )
+ def test_truncated_rfc3164_header_is_rejected_without_raising(data)
+ parser_engine, time_format, timestamp, with_priority = data
+ @parser.configure(
+ 'parser_engine' => parser_engine,
+ 'with_priority' => with_priority,
+ 'time_format' => time_format,
+ )
+ prefix = with_priority ? '<14> ' : ' '
+
+ assert_input_rejected("#{prefix}#{timestamp}", :rejects)
+ end
+
+ data(
+ 'regexp/parser priority' => ['regexp', true],
+ 'string/parser priority' => ['string', true],
+ 'regexp/input priority' => ['regexp', false],
+ 'string/input priority' => ['string', false],
+ )
+ def test_parse_with_leading_space_time_format(data)
+ parser_engine, with_priority = data
+ @parser.configure(
+ 'parser_engine' => parser_engine,
+ 'time_format' => ' %b %d %H:%M:%S',
+ 'with_priority' => with_priority,
+ 'keep_time_key' => true,
+ )
+
+ prefix = with_priority ? '<14>' : ''
+ result = nil
+ @parser.instance.parse("#{prefix} Apr 25 16:43:29 host app[123]: message") do |time, record|
+ result = [time, record]
+ end
+
+ time, record = result
+ assert_equal(event_time('Apr 25 16:43:29', format: '%b %d %H:%M:%S'), time)
+ assert_equal(14, record['pri']) if with_priority
+ assert_equal(' Apr 25 16:43:29', record['time'])
+ assert_equal(
+ {'host' => 'host', 'ident' => 'app', 'pid' => '123', 'message' => 'message'},
+ record.reject { |key, _| ['pri', 'time'].include?(key) },
+ )
+ end
+
+ data(
+ 'regexp/two spaces' => ['regexp', true, '<14> Apr 25 16:43:29 host app: message', :raises],
+ 'string/two spaces' => ['string', true, '<14> Apr 25 16:43:29 host app: message', :rejects],
+ 'regexp/tab' => ['regexp', true, "<14>\tApr 25 16:43:29 host app: message", :raises],
+ 'string/tab' => ['string', true, "<14>\tApr 25 16:43:29 host app: message", :rejects],
+ 'regexp/two leading spaces' => ['regexp', false, ' Apr 25 16:43:29 host app: message', :raises],
+ 'string/two leading spaces' => ['string', false, ' Apr 25 16:43:29 host app: message', :rejects],
+ 'regexp/leading tab' => ['regexp', false, "\tApr 25 16:43:29 host app: message", :raises],
+ 'string/leading tab' => ['string', false, "\tApr 25 16:43:29 host app: message", :rejects],
+ 'regexp/long priority' => ['regexp', true, '<1234> Apr 25 16:43:29 host app: message', :rejects],
+ 'string/long priority' => ['string', true, '<1234> Apr 25 16:43:29 host app: message', :rejects],
+ 'regexp/non-numeric priority' => ['regexp', true, ' Apr 25 16:43:29 host app: message', :rejects],
+ 'string/non-numeric priority' => ['string', true, ' Apr 25 16:43:29 host app: message', :rejects],
+ )
+ def test_parse_does_not_broaden_rfc3164_priority_separator(data)
+ parser_engine, with_priority, text, expectation = data
+ @parser.configure('parser_engine' => parser_engine, 'with_priority' => with_priority)
+
+ assert_input_rejected(text, expectation)
+ end
+
+ data(
+ 'zero' => ['<0> Apr 25 16:43:29 host app: message', 0],
+ 'two digits with leading zero' => ['<01> Apr 25 16:43:29 host app: message', 1],
+ 'three digits with leading zeros' => ['<001> Apr 25 16:43:29 host app: message', 1],
+ )
+ def test_string_parser_accepts_spaced_numeric_priority_with_leading_zeros(data)
+ text, expected_priority = data
+ @parser.configure('parser_engine' => 'string', 'with_priority' => true)
+ result = nil
+ @parser.instance.parse(text) do |time, record|
+ result = [time, record]
+ end
+
+ time, record = result
+ assert_not_nil(time)
+ assert_equal(expected_priority, record['pri'])
+ assert_equal('host', record['host'])
+ assert_equal('app', record['ident'])
+ assert_equal('message', record['message'])
+ end
+
+ data(
+ 'long priority' => '<1234> Apr 25 16:43:29 host app: message',
+ 'non-numeric priority' => ' Apr 25 16:43:29 host app: message',
+ 'partially numeric priority' => '<1a> Apr 25 16:43:29 host app: message',
+ 'non-ASCII priority' => '<é> Apr 25 16:43:29 host app: message',
+ )
+ def test_leading_space_time_format_does_not_broaden_priority_syntax(text)
+ @parser.configure(
+ 'parser_engine' => 'string',
+ 'time_format' => ' %b %d %H:%M:%S',
+ 'with_priority' => true,
+ )
+ parsed = false
+ @parser.instance.parse(text) do |time, record|
+ parsed = !!(time && record)
+ end
+
+ assert_false(parsed)
+ end
+
+ data(
+ 'regexp/rfc5424/with priority' => ['regexp', 'rfc5424', true, :rejects],
+ 'string/rfc5424/with priority' => ['string', 'rfc5424', true, :raises],
+ 'regexp/auto/with priority' => ['regexp', 'auto', true, :raises],
+ 'string/auto/with priority' => ['string', 'auto', true, :rejects],
+ 'regexp/rfc5424/without priority' => ['regexp', 'rfc5424', false, :raises],
+ 'string/rfc5424/without priority' => ['string', 'rfc5424', false, :raises],
+ 'regexp/auto/without priority' => ['regexp', 'auto', false, :raises],
+ 'string/auto/without priority' => ['string', 'auto', false, :rejects],
+ )
+ def test_parse_does_not_accept_space_after_rfc5424_priority(data)
+ parser_engine, message_format, with_priority, expectation = data
+ @parser.configure(
+ 'parser_engine' => parser_engine,
+ 'message_format' => message_format,
+ 'with_priority' => with_priority,
+ )
+ text = with_priority ? '<14> 1 2026-04-25T16:43:29Z host app 123 ID - message' : ' 1 2026-04-25T16:43:29Z host app 123 ID - message'
+
+ assert_input_rejected(text, expectation)
+ end
+
data('regexp' => 'regexp', 'string' => 'string')
def test_parse_rfc5452_with_priority(param)
@parser.configure('with_priority' => true, 'parser_type' => param, 'message_format' => 'rfc5424')