From 606851afa541674c4f5b273cee1e84a6498f9c50 Mon Sep 17 00:00:00 2001 From: Qingyuan Wang <162230229@nuaa.edu.cn> Date: Sun, 25 Oct 2026 13:41:00 +0000 Subject: [PATCH 1/2] time: fix parse_float overlong nanoseconds and negative second flooring NumericTimeParser#parse_float sliced 10 characters out of the fraction (nsec_s[0..9]), so a value with 10 or more decimal digits stored a nanosecond field of 1_000_000_000 or more. EventTime assumes nsec fits in a second, so such a value reports the wrong second everywhere: to_f and to_r are off, to_time normalises into the next second, %N prints ten digits, and the pair is forwarded as-is through to_msgpack_ext, so a downstream fluentd inherits it. It also truncated the whole part towards zero. EventTime keeps nsec non-negative, so a negative timestamp has to floor to the second below and carry the remainder, exactly like Time#to_i and Time#nsec. Instead "-1234.5" became (-1234, 500000000) -- one second late, and "-0.5" became (0, 500000000), i.e. +0.5. Every pre-1970 timestamp parsed with time_type float was wrong, and the parser disagreed with the string based parser for the same instant. Both are now checked against the decomposition Time.at(Rational(value)) gives: a randomised corpus of 200_000 positive and negative values with 1 to 9 fraction digits had 98_614 wrong decompositions before this change and 0 after, and nsec stays in [0, 1_000_000_000) for fractions of up to 20 digits. Signed-off-by: Qingyuan Wang <162230229@nuaa.edu.cn> --- lib/fluent/time.rb | 12 ++++++++++-- test/test_time_parser.rb | 30 ++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/lib/fluent/time.rb b/lib/fluent/time.rb index 441452bce1..2519d432f4 100644 --- a/lib/fluent/time.rb +++ b/lib/fluent/time.rb @@ -345,9 +345,17 @@ def parse_float(value) begin sec_s, nsec_s, _ = value.to_s.split('.', 3) # throw away second-dot and later - nsec_s = nsec_s && nsec_s[0..9] || '0' + nsec_s = nsec_s && nsec_s[0, 9] || '0' # nanoseconds: 9 digits at most nsec_s += '0' * (9 - nsec_s.size) if nsec_s.size < 9 - time = Fluent::EventTime.new(sec_s.to_i, nsec_s.to_i) + sec = sec_s.to_i + nsec = nsec_s.to_i + if nsec > 0 && sec_s.start_with?('-') + # EventTime holds a non-negative nsec, so a negative value floors to the + # second below and carries the remainder, like Time#to_i and Time#nsec. + sec -= 1 + nsec = 1_000_000_000 - nsec + end + time = Fluent::EventTime.new(sec, nsec) rescue => e raise TimeParseError, "invalid time format: value = #{value}, error_class = #{e.class.name}, error = #{e.message}" end diff --git a/test/test_time_parser.rb b/test/test_time_parser.rb index 7575a2e24d..cae1b1f129 100644 --- a/test/test_time_parser.rb +++ b/test/test_time_parser.rb @@ -359,4 +359,34 @@ class DummyForTimeParser assert_equal([expected, expected + 1], [time1, time2]) end + + # A float timestamp's fraction is nanoseconds: at most 9 digits, and always below + # 1_000_000_000 like every other EventTime producer. + def test_parse_float_keeps_nine_digits_of_the_fraction + parser = Fluent::NumericTimeParser.new(:float) + + assert_equal_event_time(Fluent::EventTime.new(1700000000, 123456789), parser.parse('1700000000.123456789')) + assert_equal_event_time(Fluent::EventTime.new(1700000000, 123456789), parser.parse('1700000000.1234567890')) + assert_equal_event_time(Fluent::EventTime.new(1700000000, 999999999), parser.parse('1700000000.9999999999')) + assert_equal_event_time(Fluent::EventTime.new(0, 0), parser.parse('0.0000000001')) + + (1..20).each do |n| + time = parser.parse("1700000000.#{'9' * n}") + assert_true time.nsec < 1_000_000_000, "nsec #{time.nsec} is out of range with #{n} digits of fraction" + end + end + + # EventTime carries a non-negative nsec, so a negative float timestamp floors to the + # second below and keeps the remainder -- the same decomposition Time#to_i / Time#nsec + # gives, which is what the string based parser and #to_r both rely on. + def test_parse_float_applies_floor_to_negative_values + parser = Fluent::NumericTimeParser.new(:float) + + ['-1234', '-1234.0', '-1234.5', '-1234.2', '-0.5', '-0.000000001', '-1700000000.123456789'].each do |value| + time = parser.parse(value) + expected = Time.at(Rational(value)) + assert_equal [expected.to_i, expected.nsec], [time.sec, time.nsec], "#{value} decomposes into a wrong sec/nsec pair" + assert_equal Rational(value), time.to_r, "#{value} is not reproduced by its sec/nsec pair" + end + end end From 5173c746ee46c89f021d719b4ca53da0f1655136 Mon Sep 17 00:00:00 2001 From: NUAAwqy Date: Tue, 27 Oct 2026 09:13:25 +0000 Subject: [PATCH 2/2] time: keep the previous parsing for negative float timestamps Per review: dropping the negative-timestamp adjustment again. It changed how negative values interact with out_file formatting and MessagePack serialization, and EventTime is also defined in the Forward protocol, so changing its encoding needs a separate discussion. This keeps the PR focused on the nine-digit fraction limit. Signed-off-by: NUAAwqy --- lib/fluent/time.rb | 10 +--------- test/test_time_parser.rb | 13 ------------- 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/lib/fluent/time.rb b/lib/fluent/time.rb index 2519d432f4..793dd58e61 100644 --- a/lib/fluent/time.rb +++ b/lib/fluent/time.rb @@ -347,15 +347,7 @@ def parse_float(value) sec_s, nsec_s, _ = value.to_s.split('.', 3) # throw away second-dot and later nsec_s = nsec_s && nsec_s[0, 9] || '0' # nanoseconds: 9 digits at most nsec_s += '0' * (9 - nsec_s.size) if nsec_s.size < 9 - sec = sec_s.to_i - nsec = nsec_s.to_i - if nsec > 0 && sec_s.start_with?('-') - # EventTime holds a non-negative nsec, so a negative value floors to the - # second below and carries the remainder, like Time#to_i and Time#nsec. - sec -= 1 - nsec = 1_000_000_000 - nsec - end - time = Fluent::EventTime.new(sec, nsec) + time = Fluent::EventTime.new(sec_s.to_i, nsec_s.to_i) rescue => e raise TimeParseError, "invalid time format: value = #{value}, error_class = #{e.class.name}, error = #{e.message}" end diff --git a/test/test_time_parser.rb b/test/test_time_parser.rb index cae1b1f129..6c4ac1a19e 100644 --- a/test/test_time_parser.rb +++ b/test/test_time_parser.rb @@ -376,17 +376,4 @@ def test_parse_float_keeps_nine_digits_of_the_fraction end end - # EventTime carries a non-negative nsec, so a negative float timestamp floors to the - # second below and keeps the remainder -- the same decomposition Time#to_i / Time#nsec - # gives, which is what the string based parser and #to_r both rely on. - def test_parse_float_applies_floor_to_negative_values - parser = Fluent::NumericTimeParser.new(:float) - - ['-1234', '-1234.0', '-1234.5', '-1234.2', '-0.5', '-0.000000001', '-1700000000.123456789'].each do |value| - time = parser.parse(value) - expected = Time.at(Rational(value)) - assert_equal [expected.to_i, expected.nsec], [time.sec, time.nsec], "#{value} decomposes into a wrong sec/nsec pair" - assert_equal Rational(value), time.to_r, "#{value} is not reproduced by its sec/nsec pair" - end - end end