From 49063c34d6314c59ec9529550e0639832796372e Mon Sep 17 00:00:00 2001 From: Edward Welbourne Date: Mon, 18 Nov 2019 13:51:01 +0100 Subject: [PATCH] Fix crash when a date-time has an invalid time-zone QDateTime is a friend of QTimeZone, so can access its internals; but it must check the zone is valid before doing so. Expanded tst_QDateTime::invalid() and made it data-driven to catch the failure cases. Commented on a test-case that caught a mistake in my first attempt at this, and on QDateTimeParser's surprising reliance on a quirk of QDateTime::toMSecsSinceEpoch()'s behavior. Fixes: QTBUG-80146 Change-Id: I24856e19ff9bf402152d17d71f83be84e366faad Reviewed-by: Alex Blasche --- src/corelib/time/qdatetime.cpp | 32 +++++++---- src/corelib/time/qdatetimeparser.cpp | 2 +- .../corelib/time/qdatetime/tst_qdatetime.cpp | 54 +++++++++++++------ 3 files changed, 62 insertions(+), 26 deletions(-) diff --git a/src/corelib/time/qdatetime.cpp b/src/corelib/time/qdatetime.cpp index 9b0bc688c9..8de4f7caf8 100644 --- a/src/corelib/time/qdatetime.cpp +++ b/src/corelib/time/qdatetime.cpp @@ -3410,6 +3410,7 @@ inline qint64 QDateTimePrivate::zoneMSecsToEpochMSecs(qint64 zoneMSecs, const QT DaylightStatus hint, QDate *zoneDate, QTime *zoneTime) { + Q_ASSERT(zone.isValid()); // Get the effective data from QTimeZone QTimeZonePrivate::Data data = zone.d->dataForLocalTime(zoneMSecs, int(hint)); // Docs state any time before 1970-01-01 will *not* have any DST applied @@ -3799,8 +3800,9 @@ QTimeZone QDateTime::timeZone() const case Qt::OffsetFromUTC: return QTimeZone(d->m_offsetFromUtc); case Qt::TimeZone: - Q_ASSERT(d->m_timeZone.isValid()); - return d->m_timeZone; + if (d->m_timeZone.isValid()) + return d->m_timeZone; + break; case Qt::LocalTime: return QTimeZone::systemTimeZone(); } @@ -3884,6 +3886,7 @@ QString QDateTime::timeZoneAbbreviation() const #if !QT_CONFIG(timezone) break; #else + Q_ASSERT(d->m_timeZone.isValid()); return d->m_timeZone.d->abbreviation(toMSecsSinceEpoch()); #endif // timezone case Qt::LocalTime: { @@ -3920,6 +3923,7 @@ bool QDateTime::isDaylightTime() const #if !QT_CONFIG(timezone) break; #else + Q_ASSERT(d->m_timeZone.isValid()); return d->m_timeZone.d->isDaylightTime(toMSecsSinceEpoch()); #endif // timezone case Qt::LocalTime: { @@ -4044,6 +4048,10 @@ void QDateTime::setTimeZone(const QTimeZone &toZone) */ qint64 QDateTime::toMSecsSinceEpoch() const { + // Note: QDateTimeParser relies on this producing a useful result, even when + // !isValid(), at least when the invalidity is a time in a fall-back (that + // we'll have adjusted to lie outside it, but marked invalid because it's + // not what was asked for). Other things may be doing similar. switch (getSpec(d)) { case Qt::UTC: return getMSecs(d); @@ -4058,12 +4066,13 @@ qint64 QDateTime::toMSecsSinceEpoch() const } case Qt::TimeZone: -#if !QT_CONFIG(timezone) - return 0; -#else - return QDateTimePrivate::zoneMSecsToEpochMSecs(d->m_msecs, d->m_timeZone, - extractDaylightStatus(getStatus(d))); +#if QT_CONFIG(timezone) + if (d->m_timeZone.isValid()) { + return QDateTimePrivate::zoneMSecsToEpochMSecs(d->m_msecs, d->m_timeZone, + extractDaylightStatus(getStatus(d))); + } #endif + return 0; } Q_UNREACHABLE(); return 0; @@ -4158,9 +4167,11 @@ void QDateTime::setMSecsSinceEpoch(qint64 msecs) case Qt::TimeZone: Q_ASSERT(!d.isShort()); #if QT_CONFIG(timezone) + d.detach(); + if (!d->m_timeZone.isValid()) + break; // Docs state any LocalTime before 1970-01-01 will *not* have any DST applied // but all affected times afterwards will have DST applied. - d.detach(); if (msecs >= 0) { status = mergeDaylightStatus(status, d->m_timeZone.d->isDaylightTime(msecs) @@ -4433,7 +4444,7 @@ static inline void massageAdjustedDateTime(const QDateTimeData &d, QDate *date, QDateTimePrivate::DaylightStatus status = QDateTimePrivate::UnknownDaylightTime; localMSecsToEpochMSecs(timeToMSecs(*date, *time), &status, date, time); #if QT_CONFIG(timezone) - } else if (spec == Qt::TimeZone) { + } else if (spec == Qt::TimeZone && d->m_timeZone.isValid()) { QDateTimePrivate::zoneMSecsToEpochMSecs(timeToMSecs(*date, *time), d->m_timeZone, QDateTimePrivate::UnknownDaylightTime, @@ -5094,7 +5105,8 @@ QDateTime QDateTime::fromMSecsSinceEpoch(qint64 msecs, const QTimeZone &timeZone { QDateTime dt; dt.setTimeZone(timeZone); - dt.setMSecsSinceEpoch(msecs); + if (timeZone.isValid()) + dt.setMSecsSinceEpoch(msecs); return dt; } diff --git a/src/corelib/time/qdatetimeparser.cpp b/src/corelib/time/qdatetimeparser.cpp index 74db2bdfd5..61214b0c7e 100644 --- a/src/corelib/time/qdatetimeparser.cpp +++ b/src/corelib/time/qdatetimeparser.cpp @@ -1363,7 +1363,7 @@ QDateTimeParser::scanString(const QDateTime &defaultValue, // given date (which might be a spring-forward, skipping an hour). if (parserType == QVariant::DateTime && !(isSet & HourSectionMask) && !when.isValid()) { qint64 msecs = when.toMSecsSinceEpoch(); - // Fortunately, that gets a useful answer ... + // Fortunately, that gets a useful answer, even though when is invalid ... const QDateTime replace = #if QT_CONFIG(timezone) tspec == Qt::TimeZone diff --git a/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp b/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp index 7f13fd0aa5..c628d2b241 100644 --- a/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp +++ b/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp @@ -150,6 +150,7 @@ private slots: void timeZones() const; void systemTimeZoneChange() const; + void invalid_data() const; void invalid() const; void range() const; @@ -2458,6 +2459,7 @@ void tst_QDateTime::fromStringStringFormat_data() if (southBrazil.isValid()) { QTest::newRow("spring-forward-midnight") << QString("2008-10-19 23:45.678 America/Sao_Paulo") << QString("yyyy-MM-dd mm:ss.zzz t") + // That's in the hour skipped - expect the matching time after the spring-forward, in DST: << QDateTime(QDate(2008, 10, 19), QTime(1, 23, 45, 678), southBrazil); } #endif @@ -3359,6 +3361,9 @@ void tst_QDateTime::timeZones() const QCOMPARE(utc.date(), utcDst.date()); QCOMPARE(utc.time(), utcDst.time()); + // Crash test, QTBUG-80146: + QVERIFY(!nzStd.toTimeZone(QTimeZone()).isValid()); + // Sydney is 2 hours behind New Zealand QTimeZone ausTz = QTimeZone("Australia/Sydney"); QDateTime aus = nzStd.toTimeZone(ausTz); @@ -3528,23 +3533,42 @@ void tst_QDateTime::systemTimeZoneChange() const QCOMPARE(tzDate.toMSecsSinceEpoch(), tzMsecs); } +void tst_QDateTime::invalid_data() const +{ + QTest::addColumn("when"); + QTest::addColumn("spec"); + QTest::addColumn("goodZone"); + QTest::newRow("default") << QDateTime() << Qt::LocalTime << true; + + QDateTime invalidDate = QDateTime(QDate(0, 0, 0), QTime(-1, -1, -1)); + QTest::newRow("simple") << invalidDate << Qt::LocalTime << true; + QTest::newRow("UTC") << invalidDate.toUTC() << Qt::UTC << true; + QTest::newRow("offset") + << invalidDate.toOffsetFromUtc(3600) << Qt::OffsetFromUTC << true; + QTest::newRow("CET") + << invalidDate.toTimeZone(QTimeZone("Europe/Oslo")) << Qt::TimeZone << true; + + // Crash tests, QTBUG-80146: + QTest::newRow("nozone+construct") + << QDateTime(QDate(1970, 1, 1), QTime(12, 0), QTimeZone()) << Qt::TimeZone << false; + QTest::newRow("nozone+fromMSecs") + << QDateTime::fromMSecsSinceEpoch(42, QTimeZone()) << Qt::TimeZone << false; + QDateTime valid(QDate(1970, 1, 1), QTime(12, 0), Qt::UTC); + QTest::newRow("tonozone") << valid.toTimeZone(QTimeZone()) << Qt::TimeZone << false; +} + void tst_QDateTime::invalid() const { - QDateTime invalidDate = QDateTime(QDate(0, 0, 0), QTime(-1, -1, -1)); - QCOMPARE(invalidDate.isValid(), false); - QCOMPARE(invalidDate.timeSpec(), Qt::LocalTime); - - QDateTime utcDate = invalidDate.toUTC(); - QCOMPARE(utcDate.isValid(), false); - QCOMPARE(utcDate.timeSpec(), Qt::UTC); - - QDateTime offsetDate = invalidDate.toOffsetFromUtc(3600); - QCOMPARE(offsetDate.isValid(), false); - QCOMPARE(offsetDate.timeSpec(), Qt::OffsetFromUTC); - - QDateTime tzDate = invalidDate.toTimeZone(QTimeZone("Europe/Oslo")); - QCOMPARE(tzDate.isValid(), false); - QCOMPARE(tzDate.timeSpec(), Qt::TimeZone); + QFETCH(QDateTime, when); + QFETCH(Qt::TimeSpec, spec); + QFETCH(bool, goodZone); + QVERIFY(!when.isValid()); + QCOMPARE(when.timeSpec(), spec); + QCOMPARE(when.timeZoneAbbreviation(), QString()); + if (!goodZone) + QCOMPARE(when.toMSecsSinceEpoch(), 0); + QVERIFY(!when.isDaylightTime()); + QCOMPARE(when.timeZone().isValid(), goodZone); } void tst_QDateTime::range() const