Skip to content

Commit f7882fe

Browse files
authored
Overflow when calling Calendar.date(from:) (#690)
Validate the given `DateComponents` values up front to align with the supported Calendar calculation date range, defined as `Date.validCalendarRange`. We could alternatively guard all arithmetic operations with `...reportingOverflow`, but there are too many operations, and so it seems untenable. I opted for a more realistic approach instead. `_CalendarICU` unconditionally truncates values to `Int32`, so the results for `Calendar.date(from:)` have always been incorrect for distant dates anyways. Fixed 129782208
1 parent 544aa64 commit f7882fe

File tree

3 files changed

+70
-2
lines changed

3 files changed

+70
-2
lines changed

Sources/FoundationEssentials/Calendar/Calendar_Gregorian.swift

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1586,11 +1586,48 @@ internal final class _CalendarGregorian: _CalendarProtocol, @unchecked Sendable
15861586

15871587
// MARK:
15881588

1589+
static func isComponentsInSupportedRange(_ components: DateComponents) -> Bool {
1590+
// `Date.validCalendarRange` supports approximately from year -4713 to year 506713. These valid ranges were chosen as if representing the entire supported date range in one calendar unit.
1591+
let validEra = -10...10
1592+
let validYear = -4714...506714
1593+
let validQuarter = -4714*4...506714*4
1594+
let validWeek = -4714*52...506714*52
1595+
let validWeekday = -4714*52*7...506714*52*7
1596+
let validMonth = -4714*12...506714*12
1597+
let validDayOfYear = -4714*365...506714*365
1598+
let validDayOfMonth = -4714*12*31...506714*12*31
1599+
let validHour = -4714*8760...Int(Int32.max)
1600+
let validMinute = Int(Int32.min)...Int(Int32.max)
1601+
let validSecond = Int(Int32.min)...Int(Int32.max)
1602+
1603+
if let value = components.era { guard validEra.contains(value) else { return false } }
1604+
if let value = components.year { guard validYear.contains(value) else { return false } }
1605+
if let value = components.quarter { guard validQuarter.contains(value) else { return false } }
1606+
if let value = components.weekOfYear { guard validWeek.contains(value) else { return false } }
1607+
if let value = components.weekOfMonth { guard validWeek.contains(value) else { return false } }
1608+
if let value = components.yearForWeekOfYear { guard validYear.contains(value) else { return false } }
1609+
if let value = components.weekday { guard validWeekday.contains(value) else { return false } }
1610+
if let value = components.weekdayOrdinal { guard validWeek.contains(value) else { return false } }
1611+
if let value = components.month { guard validMonth.contains(value) else { return false } }
1612+
if let value = components.dayOfYear { guard validDayOfYear.contains(value) else { return false } }
1613+
if let value = components.day { guard validDayOfMonth.contains(value) else { return false } }
1614+
if let value = components.hour { guard validHour.contains(value) else { return false } }
1615+
if let value = components.minute { guard validMinute.contains(value) else { return false } }
1616+
if let value = components.second { guard validSecond.contains(value) else { return false } }
1617+
return true
1618+
}
1619+
15891620
func date(from components: DateComponents) -> Date? {
1621+
guard _CalendarGregorian.isComponentsInSupportedRange(components) else {
1622+
1623+
// One or more values exceeds supported date range
1624+
return nil
1625+
}
1626+
15901627
// If the components specifies a new time zone, perform this calculation using the specified timezone
15911628
// If the date falls into the skipped time frame when transitioning into DST (e.g. 1:00 - 3:00 AM for PDT), we want to treat it as if DST hasn't happened yet. So, use .former for dstRepeatedTimePolicy.
15921629
// If the date falls into the repeated time frame when DST ends (e.g. 1:00 - 2:00 AM for PDT), we want the first instance, i.e. the instance before turning back the clock. So, use .former for dstSkippedTimePolicy.
1593-
date(from: components, inTimeZone: components.timeZone ?? timeZone, dstRepeatedTimePolicy: .former, dstSkippedTimePolicy: .former)
1630+
return date(from: components, inTimeZone: components.timeZone ?? timeZone, dstRepeatedTimePolicy: .former, dstSkippedTimePolicy: .former)
15941631
}
15951632

15961633
// Returns the weekday with reference to `firstWeekday`, in the range of 0...6

Sources/FoundationEssentials/Date.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ extension Date : _CustomPlaygroundQuickLookable {
383383
#endif // FOUNDATION_FRAMEWORK
384384

385385
extension Date {
386-
// Julian day 0 (-4713-01-01 12:00:00 +0000) in CFAbsoluteTime to 50000-01-01 00:00:00 +0000, smaller than the max time ICU supported.
386+
// Julian day 0 (-4713-01-01 12:00:00 +0000) in CFAbsoluteTime to 506713-02-07 00:00:00 +0000, smaller than the max time ICU supported.
387387
package static let validCalendarRange = Date(timeIntervalSinceReferenceDate: TimeInterval(-211845067200.0))...Date(timeIntervalSinceReferenceDate: TimeInterval(15927175497600.0))
388388

389389
// aka __CFCalendarValidateAndCapTimeRange

Tests/FoundationInternationalizationTests/CalendarTests.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1122,6 +1122,37 @@ final class CalendarTests : XCTestCase {
11221122
// 2024-03-03T02:34:36-0800, 2024-03-11T02:34:36-0700
11231123
try test(Date(timeIntervalSinceReferenceDate: 731154876), Date(timeIntervalSinceReferenceDate: 731842476))
11241124
}
1125+
1126+
#if !os(watchOS) // This test assumes Int is Int64
1127+
func test_dateFromComponentsOverflow() {
1128+
let calendar = Calendar(identifier: .gregorian)
1129+
1130+
do {
1131+
let components = DateComponents(year: -1157442765409226769, month: -1157442765409226769, day: -1157442765409226769)
1132+
let date = calendar.date(from: components)
1133+
XCTAssertNil(date)
1134+
}
1135+
1136+
do {
1137+
let components = DateComponents(year: -8935141660703064064, month: -8897841259083430780, day: -8897841259083430780)
1138+
let date = calendar.date(from: components)
1139+
XCTAssertNil(date)
1140+
}
1141+
1142+
do {
1143+
let components = DateComponents(era: 3475652213542486016, year: -1, month: 72056757140062316, day: 7812738666521952255)
1144+
let date = calendar.date(from: components)
1145+
XCTAssertNil(date)
1146+
}
1147+
1148+
do {
1149+
let components = DateComponents(weekOfYear: -5280832742222096118, yearForWeekOfYear: 182)
1150+
let date = calendar.date(from: components)
1151+
XCTAssertNil(date)
1152+
}
1153+
1154+
}
1155+
#endif
11251156
}
11261157

11271158
// MARK: - Bridging Tests

0 commit comments

Comments
 (0)