From cb0ecd6b6dfaea372d7973e4d78e661deb441540 Mon Sep 17 00:00:00 2001 From: Edward Welbourne Date: Wed, 13 Jun 2018 21:19:18 +0200 Subject: [PATCH] Check value is in range when setting a QDateTime Previously, a QDate representing more than about 0.3 gigayears before or after the epoch would overflow the millisecond count and produce a "valid" date-time that didn't represent the date and time passed to its constructor. Changed to detect such overflow and produce an invalid date-time instead, if it happens. Corrected some tests that wrongly expected to be able to represent extreme date-time values with every time-spec. The (milli)seconds since epoch are from UTC's epoch, so converting to another offset, zone or local time may give a value outside the actual range. Added some tests for the actual exact bounds. Task-number: QTBUG-68855 Change-Id: I866a4974aeb54bba92dbe7eab0a440baf02124f0 Reviewed-by: Thiago Macieira Reviewed-by: Andrei Golubev --- src/corelib/time/qdatetime.cpp | 21 +++++++--- .../corelib/time/qdatetime/tst_qdatetime.cpp | 42 ++++++++++++++----- 2 files changed, 47 insertions(+), 16 deletions(-) diff --git a/src/corelib/time/qdatetime.cpp b/src/corelib/time/qdatetime.cpp index 96932ff129..559b3764a1 100644 --- a/src/corelib/time/qdatetime.cpp +++ b/src/corelib/time/qdatetime.cpp @@ -55,6 +55,7 @@ #include "private/qcore_mac_p.h" #endif #include "private/qgregoriancalendar_p.h" +#include "private/qnumeric_p.h" #include "private/qstringiterator_p.h" #if QT_CONFIG(timezone) #include "private/qtimezoneprivate_p.h" @@ -2977,10 +2978,20 @@ static void setDateTime(QDateTimeData &d, QDate date, QTime time) ds = useTime.msecsSinceStartOfDay(); newStatus |= QDateTimePrivate::ValidTime; } + Q_ASSERT(ds < MSECS_PER_DAY); + // Only the later parts of the very first day are representable - its start + // would overflow - so get ds the same side of 0 as days: + if (days < 0 && ds > 0) { + days++; + ds -= MSECS_PER_DAY; + } - // Set msecs serial value - qint64 msecs = (days * MSECS_PER_DAY) + ds; - if (d.isShort()) { + // Check in representable range: + qint64 msecs = 0; + if (mul_overflow(days, std::integral_constant(), &msecs) + || add_overflow(msecs, qint64(ds), &msecs)) { + newStatus = QDateTimePrivate::StatusFlags{}; + } else if (d.isShort()) { // let's see if we can keep this short if (msecsCanBeSmall(msecs)) { // yes, we can @@ -3905,8 +3916,8 @@ void QDateTime::setMSecsSinceEpoch(qint64 msecs) status = mergeDaylightStatus(status, QDateTimePrivate::StandardTime); d->m_offsetFromUtc = d->m_timeZone.d->standardTimeOffset(msecs); } - msecs = msecs + (d->m_offsetFromUtc * 1000); - status |= QDateTimePrivate::ValidWhenMask; + if (!add_overflow(msecs, qint64(d->m_offsetFromUtc * 1000), &msecs)) + status |= QDateTimePrivate::ValidWhenMask; #endif // timezone break; case Qt::LocalTime: { diff --git a/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp b/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp index 2e2b5fbf24..d66ee5c832 100644 --- a/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp +++ b/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp @@ -755,17 +755,22 @@ void tst_QDateTime::fromMSecsSinceEpoch() void tst_QDateTime::fromSecsSinceEpoch() { const qint64 maxSeconds = std::numeric_limits::max() / 1000; + const QDateTime early = QDateTime::fromSecsSinceEpoch(-maxSeconds, Qt::UTC); + const QDateTime late = QDateTime::fromSecsSinceEpoch(maxSeconds, Qt::UTC); - QVERIFY(QDateTime::fromSecsSinceEpoch(maxSeconds).isValid()); - QVERIFY(!QDateTime::fromSecsSinceEpoch(maxSeconds + 1).isValid()); - QVERIFY(QDateTime::fromSecsSinceEpoch(-maxSeconds).isValid()); - QVERIFY(!QDateTime::fromSecsSinceEpoch(-maxSeconds - 1).isValid()); - - QVERIFY(QDateTime::fromSecsSinceEpoch(maxSeconds, Qt::UTC).isValid()); + QVERIFY(late.isValid()); QVERIFY(!QDateTime::fromSecsSinceEpoch(maxSeconds + 1, Qt::UTC).isValid()); - QVERIFY(QDateTime::fromSecsSinceEpoch(-maxSeconds, Qt::UTC).isValid()); + QVERIFY(early.isValid()); QVERIFY(!QDateTime::fromSecsSinceEpoch(-maxSeconds - 1, Qt::UTC).isValid()); + // Local time: need to adjust for its zone offset + const qint64 last = maxSeconds - qMax(late.addYears(-1).toLocalTime().offsetFromUtc(), 0); + QVERIFY(QDateTime::fromSecsSinceEpoch(last).isValid()); + QVERIFY(!QDateTime::fromSecsSinceEpoch(last + 1).isValid()); + const qint64 first = -maxSeconds - qMin(early.addYears(1).toLocalTime().offsetFromUtc(), 0); + QVERIFY(QDateTime::fromSecsSinceEpoch(first).isValid()); + QVERIFY(!QDateTime::fromSecsSinceEpoch(first - 1).isValid()); + // Use an offset for which .toUTC()'s return would flip the validity: QVERIFY(QDateTime::fromSecsSinceEpoch(maxSeconds, Qt::OffsetFromUTC, 7200).isValid()); QVERIFY(!QDateTime::fromSecsSinceEpoch(maxSeconds + 1, Qt::OffsetFromUTC, -7200).isValid()); @@ -775,10 +780,10 @@ void tst_QDateTime::fromSecsSinceEpoch() #if QT_CONFIG(timezone) // As for offset, use zones each side of UTC: const QTimeZone west("UTC-02:00"), east("UTC+02:00"); - QVERIFY(QDateTime::fromSecsSinceEpoch(maxSeconds, east).isValid()); - QVERIFY(!QDateTime::fromSecsSinceEpoch(maxSeconds + 1, west).isValid()); - QVERIFY(QDateTime::fromSecsSinceEpoch(-maxSeconds, west).isValid()); - QVERIFY(!QDateTime::fromSecsSinceEpoch(-maxSeconds - 1, east).isValid()); + QVERIFY(QDateTime::fromSecsSinceEpoch(maxSeconds, west).isValid()); + QVERIFY(!QDateTime::fromSecsSinceEpoch(maxSeconds + 1, east).isValid()); + QVERIFY(QDateTime::fromSecsSinceEpoch(-maxSeconds, east).isValid()); + QVERIFY(!QDateTime::fromSecsSinceEpoch(-maxSeconds - 1, west).isValid()); #endif // timezone } @@ -3759,6 +3764,21 @@ void tst_QDateTime::range() const int(QDateTime::YearRange::First)); QCOMPARE(QDateTime::fromMSecsSinceEpoch(Bounds::max() - 1, Qt::UTC).date().year(), int(QDateTime::YearRange::Last)); + constexpr qint64 millisPerDay = 24 * 3600 * 1000; + constexpr qint64 wholeDays = Bounds::max() / millisPerDay; + constexpr qint64 millisRemainder = Bounds::max() % millisPerDay; + QVERIFY(QDateTime(QDate(1970, 1, 1).addDays(wholeDays), + QTime::fromMSecsSinceStartOfDay(millisRemainder), + Qt::UTC).isValid()); + QVERIFY(!QDateTime(QDate(1970, 1, 1).addDays(wholeDays), + QTime::fromMSecsSinceStartOfDay(millisRemainder + 1), + Qt::UTC).isValid()); + QVERIFY(QDateTime(QDate(1970, 1, 1).addDays(-wholeDays - 1), + QTime::fromMSecsSinceStartOfDay(3600 * 24000 - millisRemainder - 1), + Qt::UTC).isValid()); + QVERIFY(!QDateTime(QDate(1970, 1, 1).addDays(-wholeDays - 1), + QTime::fromMSecsSinceStartOfDay(3600 * 24000 - millisRemainder - 2), + Qt::UTC).isValid()); } void tst_QDateTime::macTypes()