Extend time_t-based handling all the way to the end of time_t

At least some modern 64-bit systems have widened time_t to 64 bits
fixing the "Unix time" problem. (This is even the default on MS-Win,
although the system functions artificially limit the accepted range to
1970 through 3000.) Even the 32-bit range extends into January 2038
but the code was artificially cutting this off at the end of 2037.
This is a preparation for using the same also all the way back to the
start of time_t.

In the process, simplify and tidy up the logic of the existing code,
update the docs (this includes correcting some misinformation) and
revise some tests.

Fixes: QTBUG-73225
Change-Id: Ib8001b5a982386c747eda3dea2b5a26eedd499ad
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: Thiago Macieira <thiago.macieira@intel.com>
This commit is contained in:
Edward Welbourne 2021-02-18 17:24:18 +01:00
parent 455994c2ee
commit b4a875544b
2 changed files with 112 additions and 109 deletions

View File

@ -84,7 +84,7 @@ enum : qint64 {
SECS_PER_MIN = 60,
MSECS_PER_MIN = 60000,
MSECS_PER_SEC = 1000,
TIME_T_MAX = 2145916799, // int maximum 2037-12-31T23:59:59 UTC
TIME_T_MAX = std::numeric_limits<time_t>::max(),
JULIAN_DAY_FOR_EPOCH = 2440588 // result of julianDayFromDate(1970, 1, 1)
};
@ -2430,15 +2430,15 @@ int QDateTimeParser::startsWithLocalTimeZone(QStringView name)
}
#endif // datetimeparser
// Calls the platform variant of mktime for the given date, time and daylightStatus,
// and updates the date, time, daylightStatus and abbreviation with the returned values
// If the date falls outside the 1970 to 2037 range supported by mktime / time_t
// then null date/time will be returned, you should adjust the date first if
// you need a guaranteed result.
// Calls the platform variant of mktime for the given date, time and
// daylightStatus, and updates the date, time, daylightStatus and abbreviation
// with the returned values. If the date falls outside the time_t range
// supported by mktime, then date/time will not be updated and *ok is set false.
static qint64 qt_mktime(QDate *date, QTime *time, QDateTimePrivate::DaylightStatus *daylightStatus,
QString *abbreviation, bool *ok = nullptr)
QString *abbreviation, bool *ok)
{
const qint64 msec = time->msec();
Q_ASSERT(ok);
qint64 msec = time->msec();
int yy, mm, dd;
date->getDate(&yy, &mm, &dd);
@ -2505,14 +2505,21 @@ static qint64 qt_mktime(QDate *date, QTime *time, QDateTimePrivate::DaylightStat
*daylightStatus = QDateTimePrivate::UnknownDaylightTime;
if (abbreviation)
*abbreviation = QString();
if (ok)
*ok = false;
*ok = false;
return 0;
}
if (ok)
*ok = true;
if (secsSinceEpoch < 0 && msec > 0) {
secsSinceEpoch++;
msec -= MSECS_PER_SEC;
}
qint64 millis;
const bool overflow =
mul_overflow(qint64(secsSinceEpoch),
std::integral_constant<qint64, MSECS_PER_SEC>(), &millis)
|| add_overflow(millis, msec, &msec);
*ok = !overflow;
return qint64(secsSinceEpoch) * MSECS_PER_SEC + msec;
return msec;
}
// Calls the platform variant of localtime for the given msecs, and updates
@ -2602,6 +2609,34 @@ static qint64 timeToMSecs(QDate date, QTime time)
+ time.msecsSinceStartOfDay();
}
/*!
\internal
Tests whether system functions can handle a given time.
On MS-systems (where time_t is 64-bit by default), the system functions only
work for dates up to the end of year 3000 (for mktime(); for _localtime64_s
it's 18 days later, but we ignore that here). On Unix the supported range
is as many seconds after the epoch as time_t can represent.
This second-range is then mapped to a millisecond range; if \a slack is
passed, the range is extended by this many milliseconds at each end. The
function returns true precisely if \a millis is within the resulting range.
*/
static inline bool millisInSystemRange(qint64 millis, qint64 slack = 0)
{
#ifdef Q_OS_WIN
const qint64 msecsMax = Q_INT64_C(32535215999999);
return millis <= msecsMax + slack;
#else
if constexpr (std::numeric_limits<qint64>::max() / MSECS_PER_SEC > TIME_T_MAX) {
const qint64 msecsMax = TIME_T_MAX * MSECS_PER_SEC;
return millis <= msecsMax + slack;
} else {
return true;
}
#endif
}
// Convert an MSecs Since Epoch into Local Time
static bool epochMSecsToLocalTime(qint64 msecs, QDate *localDate, QTime *localTime,
QDateTimePrivate::DaylightStatus *daylightStatus = nullptr)
@ -2614,9 +2649,11 @@ static bool epochMSecsToLocalTime(qint64 msecs, QDate *localDate, QTime *localTi
if (daylightStatus)
*daylightStatus = QDateTimePrivate::StandardTime;
return true;
} else if (msecs > TIME_T_MAX * MSECS_PER_SEC) {
// Docs state any LocalTime after 2037-12-31 *will* have any DST applied
// but this may fall outside the supported time_t range, so need to fake it.
}
if (!millisInSystemRange(msecs)) {
// Docs state any LocalTime after 2038-01-18 *will* have any DST applied.
// When this falls outside the supported range, we need to fake it.
// Use existing method to fake the conversion, but this is deeply flawed as it may
// apply the conversion from the wrong day number, e.g. if rule is last Sunday of month
// TODO Use QTimeZone when available to apply the future rule correctly
@ -2633,10 +2670,10 @@ static bool epochMSecsToLocalTime(qint64 msecs, QDate *localDate, QTime *localTi
bool res = qt_localtime(fakeMsecs, localDate, localTime, daylightStatus);
*localDate = localDate->addDays(fakeDate.daysTo(utcDate));
return res;
} else {
// Falls inside time_t suported range so can use localtime
return qt_localtime(msecs, localDate, localTime, daylightStatus);
}
// Falls inside time_t supported range so can use localtime
return qt_localtime(msecs, localDate, localTime, daylightStatus);
}
// Convert a LocalTime expressed in local msecs encoding and the corresponding
@ -2651,31 +2688,31 @@ static qint64 localMSecsToEpochMSecs(qint64 localMsecs,
QTime tm;
msecsToTime(localMsecs, &dt, &tm);
const qint64 msecsMax = TIME_T_MAX * MSECS_PER_SEC;
// First, if localMsecs is within +/- 1 day of viable range, try mktime() in
// case it does fall in the range and gets proper DST conversion:
if (localMsecs >= -MSECS_PER_DAY && millisInSystemRange(localMsecs, MSECS_PER_DAY)) {
bool valid;
const qint64 utcMsecs = qt_mktime(&dt, &tm, daylightStatus, abbreviation, &valid);
if (valid && utcMsecs >= 0 && millisInSystemRange(utcMsecs)) {
// mktime worked and falls in valid range, so use it
if (localDate)
*localDate = dt;
if (localTime)
*localTime = tm;
return utcMsecs;
}
// Restore dt and tm, after qt_mktime() stomped them:
msecsToTime(localMsecs, &dt, &tm);
} else {
// If we don't call mktime then we need to call tzset to set up local zone data:
qTzSet();
}
if (localMsecs <= MSECS_PER_DAY) {
// Would have been caught above if after UTC epoch, so is before.
// Docs state any LocalTime before 1970-01-01 will *not* have any DST applied
// First, if localMsecs is within +/- 1 day of minimum time_t try mktime in case it does
// fall after minimum and needs proper DST conversion
if (localMsecs >= -MSECS_PER_DAY) {
bool valid;
qint64 utcMsecs = qt_mktime(&dt, &tm, daylightStatus, abbreviation, &valid);
if (valid && utcMsecs >= 0) {
// mktime worked and falls in valid range, so use it
if (localDate)
*localDate = dt;
if (localTime)
*localTime = tm;
return utcMsecs;
}
} else {
// If we don't call mktime then need to call tzset to get offset
qTzSet();
}
// Time is clearly before 1970-01-01 so just use standard offset to convert
qint64 utcMsecs = localMsecs + qt_timezone() * MSECS_PER_SEC;
const qint64 utcMsecs = localMsecs + qt_timezone() * MSECS_PER_SEC;
if (localDate || localTime)
msecsToTime(localMsecs, localDate, localTime);
if (daylightStatus)
@ -2683,59 +2720,30 @@ static qint64 localMSecsToEpochMSecs(qint64 localMsecs,
if (abbreviation)
*abbreviation = qt_tzname(QDateTimePrivate::StandardTime);
return utcMsecs;
} else if (localMsecs >= msecsMax - MSECS_PER_DAY) {
// Docs state any LocalTime after 2037-12-31 *will* have any DST applied
// but this may fall outside the supported time_t range, so need to fake it.
// First, if localMsecs is within +/- 1 day of maximum time_t try mktime in case it does
// fall before maximum and can use proper DST conversion
if (localMsecs <= msecsMax + MSECS_PER_DAY) {
bool valid;
qint64 utcMsecs = qt_mktime(&dt, &tm, daylightStatus, abbreviation, &valid);
if (valid && utcMsecs <= msecsMax) {
// mktime worked and falls in valid range, so use it
if (localDate)
*localDate = dt;
if (localTime)
*localTime = tm;
return utcMsecs;
}
}
// Use existing method to fake the conversion, but this is deeply flawed as it may
// apply the conversion from the wrong day number, e.g. if rule is last Sunday of month
// TODO Use QTimeZone when available to apply the future rule correctly
int year, month, day;
dt.getDate(&year, &month, &day);
// 2037 is not a leap year, so make sure date isn't Feb 29
if (month == 2 && day == 29)
--day;
QDate fakeDate(2037, month, day);
qint64 fakeDiff = fakeDate.daysTo(dt);
qint64 utcMsecs = qt_mktime(&fakeDate, &tm, daylightStatus, abbreviation);
if (localDate)
*localDate = fakeDate.addDays(fakeDiff);
if (localTime)
*localTime = tm;
QDate utcDate;
QTime utcTime;
msecsToTime(utcMsecs, &utcDate, &utcTime);
utcDate = utcDate.addDays(fakeDiff);
utcMsecs = timeToMSecs(utcDate, utcTime);
return utcMsecs;
} else {
// Clearly falls inside 1970-2037 suported range so can use mktime
qint64 utcMsecs = qt_mktime(&dt, &tm, daylightStatus, abbreviation);
if (localDate)
*localDate = dt;
if (localTime)
*localTime = tm;
return utcMsecs;
}
// Otherwise, after the end of the system range.
// Use existing method to fake the conversion, but this is deeply flawed as it may
// apply the conversion from the wrong day number, e.g. if rule is last Sunday of month
// TODO Use QTimeZone when available to apply the future rule correctly
int year, month, day;
dt.getDate(&year, &month, &day);
// 2037 is not a leap year, so make sure date isn't Feb 29
if (month == 2 && day == 29)
--day;
bool ok;
QDate fakeDate(2037, month, day);
const qint64 fakeDiff = fakeDate.daysTo(dt);
const qint64 utcMsecs = qt_mktime(&fakeDate, &tm, daylightStatus, abbreviation, &ok);
Q_ASSERT(ok);
if (localDate)
*localDate = fakeDate.addDays(fakeDiff);
if (localTime)
*localTime = tm;
QDate utcDate;
QTime utcTime;
msecsToTime(utcMsecs, &utcDate, &utcTime);
return timeToMSecs(utcDate.addDays(fakeDiff), utcTime);
}
static inline bool specCanBeSmall(Qt::TimeSpec spec)
@ -3350,14 +3358,13 @@ inline qint64 QDateTimePrivate::zoneMSecsToEpochMSecs(qint64 zoneMSecs, const QT
result. For example, adding one minute to 01:59:59 will get 03:00:00.
The range of valid dates taking DST into account is 1970-01-01 to the
present, and rules are in place for handling DST correctly until 2037-12-31,
but these could change. For dates after 2037, QDateTime makes a \e{best
guess} using the rules for year 2037, but we can't guarantee accuracy;
indeed, for \e{any} future date, the time-zone may change its rules before
that date comes around. For dates before 1970, QDateTime doesn't take DST
changes into account, even if the system's time zone database provides that
information, although it does take into account changes to the time-zone's
standard offset, where this information is available.
present, and rules are in place for handling DST correctly until 2038-01-18
(or the end of the \c time_t range, if this is later). For dates after the
end of this range, QDateTime makes a \e{best guess} using the rules for year
2037, but we can't guarantee accuracy; indeed, for \e{any} future date, the
time-zone may change its rules before that date comes around. For dates
before 1970, QDateTime uses the current abbreviation and offset of local
time's standad time.
\section2 Offsets From UTC

View File

@ -615,7 +615,7 @@ void tst_QDateTime::setMSecsSinceEpoch_data()
<< Q_INT64_C(-123456789)
<< QDateTime(QDate(1969, 12, 30), QTime(13, 42, 23, 211), Qt::UTC)
<< QDateTime(QDate(1969, 12, 30), QTime(14, 42, 23, 211), Qt::LocalTime);
QTest::newRow("non-time_t")
QTest::newRow("post-32-bit-time_t")
<< (Q_INT64_C(1000) << 32)
<< QDateTime(QDate(2106, 2, 7), QTime(6, 28, 16), Qt::UTC)
<< QDateTime(QDate(2106, 2, 7), QTime(7, 28, 16));
@ -713,10 +713,7 @@ void tst_QDateTime::setMSecsSinceEpoch()
}
QCOMPARE(dt.toMSecsSinceEpoch(), msecs);
if (quint64(msecs / 1000) < 0xFFFFFFFF) {
QCOMPARE(qint64(dt.toSecsSinceEpoch()), msecs / 1000);
}
QCOMPARE(qint64(dt.toSecsSinceEpoch()), msecs / 1000);
QDateTime reference(QDate(1970, 1, 1), QTime(0, 0), Qt::UTC);
QCOMPARE(dt, reference.addMSecs(msecs));
@ -766,11 +763,10 @@ void tst_QDateTime::fromMSecsSinceEpoch()
QCOMPARE(dtUtc.toMSecsSinceEpoch(), msecs);
QCOMPARE(dtOffset.toMSecsSinceEpoch(), msecs);
if (quint64(msecs / 1000) < 0xFFFFFFFF) {
if (!localOverflow)
QCOMPARE(qint64(dtLocal.toSecsSinceEpoch()), msecs / 1000);
QCOMPARE(qint64(dtUtc.toSecsSinceEpoch()), msecs / 1000);
QCOMPARE(qint64(dtOffset.toSecsSinceEpoch()), msecs / 1000);
}
QCOMPARE(qint64(dtUtc.toSecsSinceEpoch()), msecs / 1000);
QCOMPARE(qint64(dtOffset.toSecsSinceEpoch()), msecs / 1000);
QDateTime reference(QDate(1970, 1, 1), QTime(0, 0), Qt::UTC);
if (!localOverflow)