From 0bc92c01e364b89250b7d23ec3078563548609f8 Mon Sep 17 00:00:00 2001 From: Edward Welbourne Date: Fri, 15 Jul 2022 15:29:39 +0200 Subject: [PATCH] Support parsing time-zone specifiers more selectively [ChangeLog][QtCore][QDateTime] The 't' format used in fromString() can now be repeated to restrict parsing to particular forms. Thus 'tt' now matches the [+-]hhmm offset format (no colon), 'ttt' the [+-]hh:mm offset format (with colon), and 'tttt' matches an actual zone name. When used singly, 't' still matches anything the parser knows how to interpret as a zone specifier. [ChangeLog][QtCore][QLocale] The 't' format in toDateTime() now has repeated forms, as for QDateTime::fromString(). Task-number: QTBUG-95966 Change-Id: I73753145cb66a56bc25a5c2dd5cb051ba982fa2c Reviewed-by: Thiago Macieira --- src/corelib/time/qdatetime.cpp | 13 ++++- src/corelib/time/qdatetimeparser.cpp | 50 ++++++++++++------- src/corelib/time/qdatetimeparser_p.h | 4 +- .../corelib/time/qdatetime/tst_qdatetime.cpp | 28 +++++++++-- 4 files changed, 71 insertions(+), 24 deletions(-) diff --git a/src/corelib/time/qdatetime.cpp b/src/corelib/time/qdatetime.cpp index ca1dac0cb4..eca8f7c18a 100644 --- a/src/corelib/time/qdatetime.cpp +++ b/src/corelib/time/qdatetime.cpp @@ -5045,7 +5045,18 @@ QDateTime QDateTime::fromString(QStringView string, Qt::DateFormat format) \table \header \li Expression \li Output - \row \li t \li the timezone (for example "CEST") + \row \li t + \li the timezone (offset, name, "Z" or offset with "UTC" prefix) + \row \li tt + \li the timezone in offset format with no colon between hours and + minutes (for example "+0200") + \row \li ttt + \li the timezone in offset format with a colon between hours and + minutes (for example "+02:00") + \row \li tttt + \li the timezone name (for example "Europe/Berlin"). The name + recognized are those known to \l QTimeZone, which may depend on the + operating system in use. \endtable If no 't' format specifier is present, the system's local time-zone is used. diff --git a/src/corelib/time/qdatetimeparser.cpp b/src/corelib/time/qdatetimeparser.cpp index 25d64c00a6..a90fb46046 100644 --- a/src/corelib/time/qdatetimeparser.cpp +++ b/src/corelib/time/qdatetimeparser.cpp @@ -567,10 +567,8 @@ bool QDateTimeParser::parseFormat(QStringView newFormat) break; case 't': if (parserType == QMetaType::QDateTime) { - // TODO (in qlocale.cpp's serialization, too) QTBUG-95966: - // decide what different lengths of 't' format should do, - // instead of repetition ! - const SectionNode sn = { TimeZoneSection, i - add, 1, 0 }; + const SectionNode sn + = { TimeZoneSection, i - add, countRepeat(newFormat, i, 4), 0 }; newSectionNodes.append(sn); appendSeparator(&newSeparators, newFormat, index, i - index, lastQuote); i += sn.count - 1; @@ -830,7 +828,7 @@ QDateTimeParser::parseSection(const QDateTime ¤tValue, int sectionIndex, i case TimeZoneSection: result = findTimeZone(sectionTextRef, defaultValue, absoluteMax(sectionIndex), - absoluteMin(sectionIndex)); + absoluteMin(sectionIndex), sn.count); break; case MonthSection: case DayOfWeekSectionShort: @@ -1697,12 +1695,17 @@ int QDateTimeParser::findDay(const QString &str1, int startDay, int sectionIndex Return's .value is UTC offset in seconds. The caller must verify that the offset is within a valid range. + The mode is 1 for permissive parsing, 2 and 3 for strict offset-only format + (no UTC prefix) with no colon for 2 and a colon for 3. */ -QDateTimeParser::ParsedSection QDateTimeParser::findUtcOffset(QStringView str) const +QDateTimeParser::ParsedSection QDateTimeParser::findUtcOffset(QStringView str, int mode) const { + Q_ASSERT(mode > 0 && mode < 4); const bool startsWithUtc = str.startsWith("UTC"_L1); - // Get rid of UTC prefix if it exists + // Deal with UTC prefix if present: if (startsWithUtc) { + if (mode != 1) + return ParsedSection(); str = str.sliced(3); if (str.isEmpty()) return ParsedSection(Acceptable, 0, 3); @@ -1736,6 +1739,8 @@ QDateTimeParser::ParsedSection QDateTimeParser::findUtcOffset(QStringView str) c i = hoursLength; hasColon = false; } + if (mode == (hasColon ? 2 : 3)) + return ParsedSection(); str.truncate(i); // The rest of the string is not part of the UTC offset bool isInt = false; @@ -1820,17 +1825,26 @@ QDateTimeParser::findTimeZoneName(QStringView str, const QDateTime &when) const Return's .value is zone's offset, zone time - UTC time, in seconds. See QTimeZonePrivate::isValidId() for the format of zone names. - */ + + The mode is the number of 't' characters in the field specifier: + * 1: any recognized format + * 2: only the simple offset format, without colon + * 3: only the simple offset format, with colon + * 4: only a zone name +*/ QDateTimeParser::ParsedSection QDateTimeParser::findTimeZone(QStringView str, const QDateTime &when, - int maxVal, int minVal) const + int maxVal, int minVal, int mode) const { + Q_ASSERT(mode > 0 && mode <= 4); // Short-cut Zulu suffix when it's all there is (rather than a prefix match): - if (str == u'Z') + if (mode == 1 && str == u'Z') return ParsedSection(Acceptable, 0, 1); - ParsedSection section = findUtcOffset(str); - if (section.used <= 0) // if nothing used, try time zone parsing + ParsedSection section; + if (mode != 4) + section = findUtcOffset(str, mode); + if (mode != 2 && mode != 3 && section.used <= 0) // if nothing used, try time zone parsing section = findTimeZoneName(str, when); // It can be a well formed time zone specifier, but with value out of range if (section.state == Acceptable && (section.value < minVal || section.value > maxVal)) @@ -1838,11 +1852,13 @@ QDateTimeParser::findTimeZone(QStringView str, const QDateTime &when, if (section.used > 0) return section; - // Check if string is UTC or alias to UTC, after all other options - if (str.startsWith("UTC"_L1)) - return ParsedSection(Acceptable, 0, 3); - if (str.startsWith(u'Z')) - return ParsedSection(Acceptable, 0, 1); + if (mode == 1) { + // Check if string is UTC or alias to UTC, after all other options + if (str.startsWith("UTC"_L1)) + return ParsedSection(Acceptable, 0, 3); + if (str.startsWith(u'Z')) + return ParsedSection(Acceptable, 0, 1); + } return ParsedSection(); } diff --git a/src/corelib/time/qdatetimeparser_p.h b/src/corelib/time/qdatetimeparser_p.h index 2bc118ca49..4c39f4b951 100644 --- a/src/corelib/time/qdatetimeparser_p.h +++ b/src/corelib/time/qdatetimeparser_p.h @@ -173,10 +173,10 @@ private: int year, QString *monthName = nullptr, int *used = nullptr) const; int findDay(const QString &str1, int intDaystart, int sectionIndex, QString *dayName = nullptr, int *used = nullptr) const; - ParsedSection findUtcOffset(QStringView str) const; + ParsedSection findUtcOffset(QStringView str, int mode) const; ParsedSection findTimeZoneName(QStringView str, const QDateTime &when) const; ParsedSection findTimeZone(QStringView str, const QDateTime &when, - int maxVal, int minVal) const; + int maxVal, int minVal, int mode) const; // Implemented in qlocaltime.cpp: static int startsWithLocalTimeZone(const QStringView name); diff --git a/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp b/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp index a527b328e9..d00ddb254d 100644 --- a/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp +++ b/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp @@ -2937,6 +2937,17 @@ void tst_QDateTime::fromStringStringFormat_data() QTest::newRow("integer overflow found by fuzzer") << QStringLiteral("EEE1200000MUB") << QStringLiteral("t") << QDateTime(); + + // Rich time-zone specifiers (QTBUG-95966): + QTest::newRow("timezone-tt-with-offset:+0300") + << QString("2008-10-13 +0300 11.50") << QString("yyyy-MM-dd tt hh.mm") + << QDateTime(QDate(2008, 10, 13), QTime(11, 50), Qt::OffsetFromUTC, 10800); + QTest::newRow("timezone-ttt-with-offset:+03:00") + << QString("2008-10-13 +03:00 11.50") << QString("yyyy-MM-dd ttt hh.mm") + << QDateTime(QDate(2008, 10, 13), QTime(11, 50), Qt::OffsetFromUTC, 10800); + QTest::newRow("timezone-tttt-with-offset:+03:00") + << QString("2008-10-13 +03:00 11.50") << QString("yyyy-MM-dd tttt hh.mm") + << QDateTime(); // Offset not valid when zone name expected. } void tst_QDateTime::fromStringStringFormat() @@ -2978,14 +2989,23 @@ void tst_QDateTime::fromStringStringFormat_localTimeZone_data() QTimeZone etcGmtWithOffset("Etc/GMT+3"); if (etcGmtWithOffset.isValid()) { lacksRows = false; - QTest::newRow("local-timezone-with-offset:Etc/GMT+3") << QByteArrayLiteral("GMT") + QTest::newRow("local-timezone-t-with-zone:Etc/GMT+3") + << QByteArrayLiteral("GMT") << QString("2008-10-13 Etc/GMT+3 11.50") << QString("yyyy-MM-dd t hh.mm") << QDateTime(QDate(2008, 10, 13), QTime(11, 50), etcGmtWithOffset); - // TODO QTBUG-95966: find better ways to use repeated 't' - QTest::newRow("double-timezone-with-offset:Etc/GMT+3") << QByteArrayLiteral("GMT") - << QString("2008-10-13 Etc/GMT+3Etc/GMT+3 11.50") << QString("yyyy-MM-dd tt hh.mm") + QTest::newRow("local-timezone-tttt-with-zone:Etc/GMT+3") + << QByteArrayLiteral("GMT") + << QString("2008-10-13 Etc/GMT+3 11.50") << QString("yyyy-MM-dd tttt hh.mm") << QDateTime(QDate(2008, 10, 13), QTime(11, 50), etcGmtWithOffset); } + QTest::newRow("local-timezone-tt-with-zone:Etc/GMT+3") + << QByteArrayLiteral("GMT") + << QString("2008-10-13 Etc/GMT+3 11.50") << QString("yyyy-MM-dd tt hh.mm") + << QDateTime(); // Zone name not valid when offset expected + QTest::newRow("local-timezone-ttt-with-zone:Etc/GMT+3") + << QByteArrayLiteral("GMT") + << QString("2008-10-13 Etc/GMT+3 11.50") << QString("yyyy-MM-dd ttt hh.mm") + << QDateTime(); // Zone name not valid when offset expected QTimeZone gmtWithOffset("GMT-2"); if (gmtWithOffset.isValid()) { lacksRows = false;