diff options
author | Edward Welbourne <edward.welbourne@qt.io> | 2021-02-18 17:24:18 +0100 |
---|---|---|
committer | Edward Welbourne <edward.welbourne@qt.io> | 2021-04-16 10:22:27 +0200 |
commit | b4a875544ba8f2d11e183d67f45891d6149203ed (patch) | |
tree | 0aa1ad44a4594e36bbb32b55ced0e8201d49a3dd | |
parent | 455994c2eef28ca4ed6d52103af47364e4145555 (diff) |
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>
-rw-r--r-- | src/corelib/time/qdatetime.cpp | 207 | ||||
-rw-r--r-- | tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp | 14 |
2 files changed, 112 insertions, 109 deletions
diff --git a/src/corelib/time/qdatetime.cpp b/src/corelib/time/qdatetime.cpp index 701df4a06b..a3516ddb67 100644 --- a/src/corelib/time/qdatetime.cpp +++ b/src/corelib/time/qdatetime.cpp @@ -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 diff --git a/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp b/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp index c2f4e82896..166ce260a5 100644 --- a/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp +++ b/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp @@ -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) |