Skip to content

Commit abf9a15

Browse files
authored
Native: handle the platform limits better (#24)
* Native (*nix): Fix crash The program used to crash (which couldn't be caught) when the timezone database was queried with large negative instants (earlier than year -32767). * Native (Darwin): improve results outside of supported range Darwin only supports years [1; 4001], and strange things may happen when requesting from it to perform arithmetic on dates outside this range. In particular, when converting NSDateComponents to NSDate, the era is ignored, and so 32000 BC becomes 32000 AD, which is absolutely wrong. Reliance on Darwin auto-adjusting NSDate when converting from NSDateComponents is the core of our solution to acquire the timezone database information, but in this case it is better to completely ignore this API and instead return a plausible-looking result. * Native (Windows): return more plausible results When querying the timezone database outside of the supported range on Windows, the requested value could be too large to fit in the platform-provided range. Then, instead of returning a completely arbitrary result, the call will still return values that could be naturally acquired for some date in the supported range, instead of garbage values. * Native (Darwin): use ISO-8601 calendar This doesn't change the results in the supported ranges, but it is more clean conceptually to use the same calendar here as in the other platforms.
1 parent 6d49559 commit abf9a15

File tree

3 files changed

+57
-13
lines changed

3 files changed

+57
-13
lines changed

core/nativeMain/cinterop/cpp/apple.mm

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@
2828
return [NSTimeZone timeZoneWithName: name];
2929
}
3030

31+
static NSDate * dateWithTimeIntervalSince1970Saturating(int64_t epoch_sec)
32+
{
33+
auto date = [NSDate dateWithTimeIntervalSince1970: epoch_sec];
34+
if ([date timeIntervalSinceDate:[NSDate distantPast]] < 0)
35+
date = [NSDate distantPast];
36+
else if ([date timeIntervalSinceDate:[NSDate distantFuture]] > 0)
37+
date = [NSDate distantFuture];
38+
return date;
39+
}
40+
3141
extern "C" {
3242
#include "cdate.h"
3343
}
@@ -154,7 +164,7 @@ int offset_at_instant(TZID zone_id, int64_t epoch_sec)
154164
{
155165
auto zone = timezone_by_id(zone_id);
156166
if (zone == nil) { return INT_MAX; }
157-
auto date = [NSDate dateWithTimeIntervalSince1970: epoch_sec];
167+
auto date = dateWithTimeIntervalSince1970Saturating(epoch_sec);
158168
return (int32_t)[zone secondsFromGMTForDate: date];
159169
}
160170

@@ -169,27 +179,27 @@ int offset_at_datetime(TZID zone_id, int64_t epoch_sec, int *offset) {
169179
if (zone == nil) { return 0; }
170180
/* a date in an unspecified timezone, defined by the number of seconds since
171181
the start of the epoch in *that* unspecified timezone */
172-
NSDate *date = [NSDate dateWithTimeIntervalSince1970: epoch_sec];
173-
// The Gregorian calendar.
174-
NSCalendar *gregorian = [NSCalendar
175-
calendarWithIdentifier:NSCalendarIdentifierGregorian];
176-
if (gregorian == nil) { return 0; }
182+
auto date = dateWithTimeIntervalSince1970Saturating(epoch_sec);
183+
// The ISO8601 calendar.
184+
NSCalendar *iso8601 = [NSCalendar
185+
calendarWithIdentifier: NSCalendarIdentifierISO8601];
186+
if (iso8601 == nil) { return 0; }
177187
// The UTC time zone
178188
NSTimeZone *utc = [NSTimeZone timeZoneForSecondsFromGMT: 0];
179189
/* Now, we say that the date that we initially meant is `date`, only with
180190
the context of being in a timezone `zone`. */
181-
NSDateComponents *dateComponents = [gregorian
191+
NSDateComponents *dateComponents = [iso8601
182192
componentsInTimeZone: utc
183193
fromDate: date];
184194
dateComponents.timeZone = zone;
185-
NSDate *newDate = [gregorian dateFromComponents:dateComponents];
195+
NSDate *newDate = [iso8601 dateFromComponents:dateComponents];
186196
if (newDate == nil) { return 0; }
187197
// we now know the offset of that timezone at this time.
188198
*offset = (int)[zone secondsFromGMTForDate: newDate];
189199
/* `dateFromComponents` automatically corrects the date to avoid gaps. We
190200
need to learn which adjustments it performed. */
191201
int result = (int)((int64_t)[newDate timeIntervalSince1970] +
192-
(int64_t)*offset - epoch_sec);
202+
(int64_t)*offset - (int64_t)[date timeIntervalSince1970]);
193203
return result;
194204
}
195205

core/nativeMain/cinterop/cpp/cdate.cpp

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,32 @@
1515
using namespace date;
1616
using namespace std::chrono;
1717

18+
static int64_t first_instant_of_year(const year& yr) {
19+
return sys_seconds{sys_days{yr/January/1}}.time_since_epoch().count();
20+
}
21+
/* This constant represents the earliest moment that our system recognizes;
22+
everything earlier than that is considered the same moment.
23+
This doesn't make us lose any precision in computations, as timezone
24+
information doesn't make sense to use at that time, as there were no
25+
timezones, and even the calendars were all different.
26+
The reason for this explicit check is that the years that are considered
27+
valid by the "date" library are [-32767; 32767], and library crashes if
28+
it sees a date in year -32768 or earlier. */
29+
static const int64_t min_available_instant =
30+
first_instant_of_year(++year::min());
31+
// Lack of this check didn't cause any problems yet, but why not add it too?
32+
static const int64_t max_available_instant =
33+
first_instant_of_year(--year::max());
34+
35+
static seconds saturating(int64_t epoch_sec)
36+
{
37+
if (epoch_sec < min_available_instant)
38+
epoch_sec = min_available_instant;
39+
else if (epoch_sec > max_available_instant)
40+
epoch_sec = max_available_instant;
41+
return seconds(epoch_sec);
42+
}
43+
1844
extern "C" {
1945
#include "cdate.h"
2046
}
@@ -94,8 +120,7 @@ int offset_at_instant(TZID zone_id, int64_t epoch_sec)
94120
try {
95121
/* `sys_time` is usually Unix time (UTC, not counting leap seconds).
96122
Starting from C++20, it is specified in the standard. */
97-
auto stime = sys_time<std::chrono::seconds>(
98-
std::chrono::seconds(epoch_sec));
123+
auto stime = sys_time<std::chrono::seconds>(saturating(epoch_sec));
99124
auto zone = zone_by_id(zone_id);
100125
auto info = zone->get_info(stime);
101126
return info.offset.count();
@@ -118,7 +143,7 @@ int offset_at_datetime(TZID zone_id, int64_t epoch_sec, int *offset)
118143
{
119144
try {
120145
auto zone = zone_by_id(zone_id);
121-
local_seconds seconds((std::chrono::seconds(epoch_sec)));
146+
local_seconds seconds(saturating(epoch_sec));
122147
auto info = zone->get_info(seconds);
123148
switch (info.result) {
124149
case local_info::unique:

core/nativeMain/cinterop/cpp/windows.cpp

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,16 @@ int offset_at_datetime(TZID zone_id, int64_t epoch_sec, int *offset)
356356
TzSpecificLocalTimeToSystemTimeEx(&dtzi, &localtime, &utctime);
357357
*offset = offset_at_systime(dtzi, utctime);
358358
SystemTimeToTzSpecificLocalTimeEx(&dtzi, &utctime, &adjusted);
359-
return (int)(systemtime_to_unix_time(adjusted) - epoch_sec);
359+
/* We don't use `epoch_sec` instead of `systemtime_to_unix_time(localtime)
360+
because `unix_time_to_systemtime(epoch_sec, localtime)` above could
361+
overflow the range of instants representable in WinAPI, and then the
362+
difference from `epoch_sec` would be large, potentially causing problems.
363+
If it happened, we don't return an error as we don't really care which
364+
result to return: timezone database information outside of [1970; current
365+
time) is not accurate anyway, and WinAPI supports dates in years [1601;
366+
30827], which should be enough for all practical purposes. */
367+
return (int)(systemtime_to_unix_time(adjusted) -
368+
systemtime_to_unix_time(localtime));
360369
}
361370

362371
}

0 commit comments

Comments
 (0)