From b3cc30cb9d33e1edc5c0e18e4fe9716f44340eab Mon Sep 17 00:00:00 2001 From: "carpentry-heartbeat[bot]" Date: Mon, 27 Jul 2026 07:25:49 +0200 Subject: [PATCH] Fix signed overflow in Datetime.add-seconds for large offsets The non-negative branch computed `(+ s n)` from the datetime's seconds field and the caller's offset. For `n` near `Int.MAX` that sum wraps negative, `(< a 60)` then holds, and the function returned a Datetime with a negative seconds field and an untouched date: `2020-01-01 23:59:59` plus `2147483647` gave `2020-01-01 23:59:-10`. Split `n` into whole days and a sub-day remainder up front. The seconds addition is now bounded by `59 + 86399`, and the day count is folded into the Gregorian ordinal alongside the carry the innermost branch already computes. The maximum day shift is 24856, so the ordinal stays far inside Int; no widening to Long is needed (Long is 32-bit on some targets anyway). The early-exit conditions gain a `(= days 0)` conjunct so a datetime whose smaller units do not carry still gets its date advanced, and so the Maybe-ness of the hours/minutes fields is set exactly as before. The negative branch does not need the same treatment: `s` is in [0, 59] and `n` is negative there, so `(+ s n)` moves toward zero and cannot overflow even at `Int.MIN`. --- test/time.carp | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++ time.carp | 11 +++++----- 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/test/time.carp b/test/time.carp index fb3b58f..4f8cfc3 100644 --- a/test/time.carp +++ b/test/time.carp @@ -682,6 +682,62 @@ (Maybe.Nothing)) 3600)) "add 3600 seconds is one hour") + (assert-equal test + &@"2088-01-20 03:14:06" + &(stamp + &(Datetime.add-seconds + &(Datetime.init 2020 + 1 + 1 + (Maybe.Just 23) + (Maybe.Just 59) + (Maybe.Just 59) + (Maybe.Nothing) + (Maybe.Nothing)) + 2147483647)) + "add Int.MAX seconds to a non-zero seconds field") + (assert-equal test + &@"1951-12-14 20:45:51" + &(stamp + &(Datetime.add-seconds + &(Datetime.init 2020 + 1 + 1 + (Maybe.Just 23) + (Maybe.Just 59) + (Maybe.Just 59) + (Maybe.Nothing) + (Maybe.Nothing)) + -2147483648)) + "add Int.MIN seconds to a non-zero seconds field") + (assert-equal test + &@"2063-05-18 03:33:19" + &(stamp + &(Datetime.add-seconds + &(Datetime.init 1999 + 12 + 31 + (Maybe.Just 23) + (Maybe.Just 59) + (Maybe.Just 59) + (Maybe.Nothing) + (Maybe.Nothing)) + 2000000000)) + "add 2000000000 seconds") + (assert-equal test + &@"2056-09-09 01:46:39" + &(stamp + &(Datetime.add-seconds + &(Datetime.init 2024 + 12 + 31 + (Maybe.Just 23) + (Maybe.Just 59) + (Maybe.Just 59) + (Maybe.Nothing) + (Maybe.Nothing)) + 1000000000)) + "add 1000000000 seconds") (assert-equal test &@"2024-03-15" diff --git a/time.carp b/time.carp index 11ded36..b8dc40c 100644 --- a/time.carp +++ b/time.carp @@ -295,25 +295,26 @@ If `n` is negative, it will be subtracted instead.") (set-year! &nd @(year &nnd)) nd))))))) (let-do [nd @d + days (/ n DAY) s (Maybe.from @(seconds d) 0) - a (+ s n)] + a (+ s (Int.mod n DAY))] (set-seconds! &nd (Maybe.Just (Int.mod a 60))) - (if (< a 60) + (if (and (< a 60) (= days 0)) nd (let-do [m (/ a 60) dm (Maybe.from @(minutes d) 0) ma (+ m dm)] (set-minutes! &nd (Maybe.Just (Int.mod ma 60))) - (if (< ma 60) + (if (and (< ma 60) (= days 0)) nd (let-do [h (/ ma 60) dh (Maybe.from @(hours d) 0) ha (+ h dh)] (set-hours! &nd (Maybe.Just (Int.mod ha 24))) - (if (< ha 24) + (if (and (< ha 24) (= days 0)) nd (let-do [ord (to-ordinal d) - nnd (from-ordinal (+ ord (/ ha 24)))] + nnd (from-ordinal (+ ord (+ days (/ ha 24))))] (set-day! &nd @(day &nnd)) (set-month! &nd @(month &nnd)) (set-year! &nd @(year &nnd))