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 <thiago.macieira@intel.com>
Reviewed-by: Andrei Golubev <andrei.golubev@qt.io>
This commit is contained in:
Edward Welbourne 2018-06-13 21:19:18 +02:00
parent 83bff8951a
commit cb0ecd6b6d
2 changed files with 47 additions and 16 deletions

View File

@ -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<qint64, MSECS_PER_DAY>(), &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: {

View File

@ -755,17 +755,22 @@ void tst_QDateTime::fromMSecsSinceEpoch()
void tst_QDateTime::fromSecsSinceEpoch()
{
const qint64 maxSeconds = std::numeric_limits<qint64>::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()