Skip to content

Commit 75585a5

Browse files
authored
Guard against integer overflow (swiftlang#1703)
In a recent change we changed to use integer math for better performance. However, this approach doesn't work if the date offset of the input Date cannot be represented by a Int, which would trigger a swift runtime error. Fix this by checking if the offset is representable as an Int, and fallback to the previous implementation with floating point math if it isn't. 168600685
1 parent 5d12358 commit 75585a5

2 files changed

Lines changed: 38 additions & 9 deletions

File tree

Sources/FoundationEssentials/Calendar/Calendar_Gregorian.swift

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1994,9 +1994,6 @@ internal final class _CalendarGregorian: _CalendarProtocol, @unchecked Sendable
19941994
let timezoneOffset = timeZone.secondsFromGMT(for: d)
19951995
let localDate = d + Double(timezoneOffset)
19961996

1997-
let dateOffsetInSeconds = localDate.timeIntervalSinceReferenceDate.rounded(.down)
1998-
let date = Date(timeIntervalSinceReferenceDate: dateOffsetInSeconds) // Round down the given date to seconds
1999-
20001997
let hour: Int?
20011998
let second: Int?
20021999
let minute: Int?
@@ -2012,13 +2009,31 @@ internal final class _CalendarGregorian: _CalendarProtocol, @unchecked Sendable
20122009
var day: Int?
20132010

20142011
let timeComponents: Calendar.ComponentSet = [.hour, .minute, .second, .nanosecond]
2012+
let dateOffsetInSeconds = localDate.timeIntervalSinceReferenceDate.rounded(.down)
20152013
if !components.isDisjoint(with: timeComponents) {
2016-
let totalSeconds = Int(dateOffsetInSeconds)
2017-
let secondsInDay = (totalSeconds % 86400 + 86400) % 86400
2018-
2019-
hour = secondsInDay / 3600
2020-
minute = (secondsInDay % 3600) / 60
2021-
second = secondsInDay % 60
2014+
let canUseIntegerMath = dateOffsetInSeconds < Double(Int.max) && dateOffsetInSeconds >= Double(Int.min)
2015+
if canUseIntegerMath {
2016+
let totalSeconds = Int(dateOffsetInSeconds)
2017+
let secondsInDay = (totalSeconds % 86400 + 86400) % 86400
2018+
2019+
let tmp: Int
2020+
(hour, tmp) = secondsInDay.quotientAndRemainder(dividingBy: 3600)
2021+
(minute, second) = tmp.quotientAndRemainder(dividingBy: 60)
2022+
} else {
2023+
var timeInDay = dateOffsetInSeconds.remainder(dividingBy: 86400) // this has precision of one second
2024+
if (timeInDay < 0) {
2025+
timeInDay += 86400
2026+
}
2027+
2028+
hour = Int(timeInDay / 3600) // zero-based
2029+
timeInDay = timeInDay.truncatingRemainder(dividingBy: 3600.0)
2030+
2031+
minute = Int(timeInDay / 60)
2032+
timeInDay = timeInDay.truncatingRemainder(dividingBy: 60.0)
2033+
2034+
second = Int(timeInDay)
2035+
}
2036+
20222037
nanosecond = Int((localDate.timeIntervalSinceReferenceDate - dateOffsetInSeconds) * 1_000_000_000)
20232038
} else {
20242039
hour = nil
@@ -2041,6 +2056,7 @@ internal final class _CalendarGregorian: _CalendarProtocol, @unchecked Sendable
20412056
}
20422057

20432058
do {
2059+
let date = Date(timeIntervalSinceReferenceDate: dateOffsetInSeconds) // Round down the given date to seconds
20442060
let useJulianRef = useJulianReference(date)
20452061
let julianDay = try date.julianDay()
20462062
let julianDayYMD = Self.yearMonthDayFromJulianDay(julianDay, useJulianRef: useJulianRef)

Tests/FoundationEssentialsTests/GregorianCalendarTests.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,19 @@ private struct GregorianCalendarTests {
583583
testAdding(.init(month: 2), to: date2, wrap: false, expected: Date(timeIntervalSince1970: 983404800)) // 2001-03-01 00:00:00 UTC, 2001-02-28 16:00:00 PT
584584
}
585585
}
586+
#if _pointerBitWidth(_64)
587+
@Test func testAddLargeValue() throws {
588+
589+
let date = Date(timeIntervalSinceReferenceDate: 656157793.922098)
590+
let calendar = Calendar(identifier: .gregorian)
591+
let value = 578721382704613386
592+
593+
let allComponents: Set<Calendar.Component> = [.era, .year, .month, .day, .hour, .minute, .second, .nanosecond, .weekday, .weekdayOrdinal, .quarter, .weekOfMonth, .weekOfYear, .yearForWeekOfYear]
594+
for component in allComponents {
595+
_ = calendar.date(byAdding: component, value: value, to: date)
596+
}
597+
}
598+
#endif
586599

587600
// MARK: - Ordinality
588601

0 commit comments

Comments
 (0)