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 <thiago.macieira@intel.com>
bb10
Edward Welbourne 2022-07-15 15:29:39 +02:00
parent 5818dd3cad
commit 0bc92c01e3
4 changed files with 71 additions and 24 deletions

View File

@ -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.

View File

@ -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 &currentValue, 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();
}

View File

@ -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);

View File

@ -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;