Revision control
++ // Copyright (C) 2026 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
++ // Qt-Security score:critical reason:data-parser
++ #include "private/qtparseqttemporalformat_p.h"
++
++ #include "private/qlocale_p.h"
++ #include "private/qstringiterator_p.h"
++
++ QT_BEGIN_NAMESPACE
++
++ using namespace Qt::StringLiterals;
++
++ namespace QtParseQtTemporalFormat {
++
++ inline constexpr char timeFormats[] = "Hhmsz"; // Omits [aA][pP]? deliberately.
++ inline constexpr char dateFormats[] = "Mdy";
++
++ /*!
++ \internal
++ Parse a prefix of \a pattern as a format for the data indicated by \a form.
++
++ Parses the longest prefix of \a pattern that constitutes a valid Qt temporal
++ format string. Only the field indicator characters for data indicated in \a
++ form are recognized as such; everything else (including what other forms
++ might accept as field indicators) is treated as literal text. The only text
++ that can be invalid is thus one containing an odd number of single quote
++ characters; the portion up to the last of these will be parsed, in that
++ case, and the return's \c endIndex will indicate the location of the
++ unmatched single quote. Otherwise, \c endIndex shall equal pattern.size().
++ The return's \c fields is a list of \l {QtTemporalPattern::} {TemporalField}
++ structures representing what was parsed, each with \c options matching the
++ pre-6.12 semantics of the Qt formats for temporal data, with some tweaks to
++ enable desired new features, notably years with (a leading sign and) more
++ than four digits.
++
++ \sa QtTemporalPattern, QDate::fromString(), QTime::fromString(), QDateTime::fromString()
++ */
++
++ ParsedDateTimeFormat prefix(QStringView pattern, QtTemporalPattern::DateTimeParts form)
++ {
++ using namespace QtTemporalPattern;
++
++ ParsedDateTimeFormat result;
++ constexpr char32_t Invalid = ~char32_t(0);
++ static_assert(Invalid > QChar::LastValidCodePoint);
++ const bool includeDate = form.testFlag(DateTimePart::Date);
++ const bool includeTime = form.testFlag(DateTimePart::Time);
++ const bool includeZone = form.testFlag(DateTimePart::Zone);
++
++ QStringIterator iter(pattern);
++ char32_t pending = 0;
++ const auto countRepeats = [&pending, &iter, &result](char32_t first, qsizetype bound) {
++ // Consumes all repeats of \a first, returns min(bound, number of repeats).
++ Q_ASSERT(!QChar::requiresSurrogates(first)); // It's always an ASCII format char
++ Q_ASSERT(pending == 0);
++ qsizetype count = 1; // We've already seen first
++ result.endIndex = iter.index(); // ... and tacitly consumed it.
++ while (iter.hasNext() && count < bound) {
++ const auto read = iter.next(Invalid);
++ if (read > QChar::LastValidCodePoint) {
++ pending = Invalid;
++ break;
++ }
++ if (read != first) {
++ pending = read;
++ break;
++ }
++ ++count;
++ result.endIndex = iter.index();
++ }
++ return count;
++ };
++
++ constexpr char32_t SingleQuote = U'\'';
++ static constexpr auto matchTimeFormats = QtPrivate::makeCharacterSetMatch<timeFormats>();
++ static constexpr auto matchDateFormats = QtPrivate::makeCharacterSetMatch<dateFormats>();
++ const auto isFormatChar = [includeDate, includeTime, includeZone](char32_t ch) {
++ if (ch >= 0x80)
++ return false;
++ if (includeTime) {
++ if (matchTimeFormats.matches(uchar(ch)) || ch == U'A' || ch == U'a')
++ return true;
++ }
++ if (includeZone && ch == U't')
++ return true;
++ return includeDate && matchDateFormats.matches(uchar(ch));
++ };
++
++ constexpr auto formatCategory = [](uchar ch, int count) {
++ using Cat = TemporalFieldCategory;
++ switch (ch) {
++ case 'A': // case 'P':
++ case 'a': // case 'p':
++ return Cat::PeriodInDay;
++ case 'd': return count < 3 ? Cat::DayOfMonth : Cat::DayOfWeek;
++ case 'H': return Cat::Hour;
++ case 'h': return Cat::HourMod12;
++ case 'M': return Cat::Month;
++ case 'm': return Cat::Minute;
++ case 's': return Cat::Second;
++ case 't': return Cat::TimeZone;
++ case 'y': return count < 3 ? Cat::YearWithinCentury : Cat::Year;
++ case 'z': return Cat::SecondFraction;
++ }
++ // Should only be called with a ch that would pass an isFormatChar() check.
++ Q_UNREACHABLE_RETURN(Cat::Literal);
++ };
++ constexpr auto formatFlags = [](uchar ch, int count) -> TemporalFieldFlags {
++ using F = TemporalFieldFlag;
++ constexpr TemporalFieldFlags TextCommon = F::IgnoreCase;
++ switch (ch) {
++ case 'A': // case 'P':
++ return count ? F::UpperCase | TextCommon : TextCommon;
++ case 'a': // case 'p':
++ return count ? F::LowerCase | TextCommon : TextCommon;
++ case 'd': case 'M': // Day and Month share a pattern:
++ switch (count) {
++ case 2: return F::Numeric | F::ZeroPad;
++ case 3: return F::Verbal | F::Abbreviated | TextCommon;
++ default:
++ return count < 2 ? F::Numeric : F::Verbal | F::Wide | TextCommon;
++ };
++ Q_UNREACHABLE();
++ break;
++ case 'H': case 'h': case 'm': case 's': // Shared pattern:
++ return count > 1 ? F::Numeric | F::ZeroPad : F::Numeric;
++ case 't':
++ switch (count) {
++ case 1: // 't': matches everything, serializes as abbreviation
++ return F::AllowZSuffix | F::LocalTimeName;
++ // The next two forms aren't localized - should they be ?
++ // The issue is that they're documented to differ in whether
++ // separators are used, but we don't control that (nor should
++ // we, or the format author) for localized forms.
++ case 2: // 'tt': offset (no-prefix, no separator)
++ return F::Iso8601 | F::Numeric | F::ZeroPad;
++ case 3: // 'ttt': offset (no-prefix, separator)
++ return F::Iso8601 | F::Verbal | F::ZeroPad;
++ default: // 'tttt': long name, IANA ID or LocalTime name
++ return F::LocalizedZone | F::Verbal | F::Standalone | F::Wide | F::Short
++ | F::LocalTimeName;
++ // This includes both metazone and exemplar city versions of long name.
++ }
++ Q_UNREACHABLE();
++ break;
++ case 'y':
++ if (count > 2)
++ return F::Numeric | F::ZeroPad | F::YearSignIso8601;
++ return F::Numeric | F::ZeroPad;
++ case 'z':
++ if (count > 2)
++ return F::Numeric | F::ZeroPad;
++ return F::Numeric;
++ }
++ // Should only be called by branches that passed an isFormatChar() check.
++ Q_UNREACHABLE_RETURN({});
++ };
++
++ const auto store = [&result](QString &&literal, qsizetype count,
++ TemporalFieldFlags flags,
++ TemporalFieldCategory category) {
++ result.fields.append(TemporalField{std::move(literal), count, flags, category});
++ };
++
++ bool seenDayPeriod = false, seenHourMod12 = false; // See post-processing.
++ while (pending <= QChar::LastValidCodePoint && (pending || iter.hasNext())) {
++ char32_t ch;
++ if (pending) {
++ ch = std::exchange(pending, 0);
++ } else {
++ result.endIndex = iter.index();
++ ch = iter.next(Invalid);
++ }
++ if (ch > QChar::LastValidCodePoint)
++ break;
++
++ if (ch < 0x80 && includeTime) {
++ if (matchTimeFormats.matches(uchar(ch))) {
++ qsizetype count = countRepeats(ch, ch == U'z' ? 3 : 2);
++ if (ch == U'z' && count == 2) // Backwards compatibility
++ count = 1; // (but we still consume both 'z' characters from the format)
++ store(QString(), count, formatFlags(uchar(ch), count),
++ formatCategory(uchar(ch), count));
++ if (ch == U'h')
++ seenHourMod12 = true;
++ continue;
++ }
++ if (ch == U'A' || ch == U'a') {
++ // Follow old QDTP (for now, at least) in using count to represent case choice.
++ qsizetype count = ch == U'a' ? 1 : 2;
++ // AP or ap are just the same as A or a; but Ap or aP selects
++ // locale-appropriate case:
++ result.endIndex = iter.index();
++ const auto read = iter.hasNext() ? iter.next(Invalid) : Invalid;
++ if (read > QChar::LastValidCodePoint) {
++ pending = Invalid;
++ } else if (read == U'P') {
++ if (ch == U'a')
++ count = 0;
++ result.endIndex = iter.index();
++ } else if (read == U'p') {
++ if (ch == U'A')
++ count = 0;
++ result.endIndex = iter.index();
++ } else {
++ pending = read;
++ }
++ store(QString(), count, formatFlags(uchar(ch), count),
++ formatCategory(uchar(ch), count));
++ seenDayPeriod = true;
++ continue;
++ }
++ }
++ // Date and Zone fields are more straightforward, except for 'y':
++ if (ch == U'y' && includeDate) {
++ // For 'y', a pair is a year-within-century, double that for a full
++ // year; beyond that, evenly many more are more of those but an odd
++ // 'y' is a literal. We thus need to only consume 2 or 4 'y' tokens,
++ // so can't use countRepeat() with its simple maximum. We need to
++ // leave the odd 'y', if present, for a later iteration to consume
++ // or, if it's all there is, for use as a literal - in which case we
++ // mustn't have set pending. Fortunately 'y' is ASCII so we don't
++ // have to worry about surrogates:
++ qsizetype count = 1;
++ QStringView tail = pattern.sliced(iter.index() - 1);
++ if (tail.size() > 4)
++ tail = tail.first(4);
++ while (count < tail.size() && char32_t(tail[count].unicode()) == ch)
++ ++count;
++ if (count == 3)
++ --count;
++ if (count > 1) {
++ Q_ASSERT(count == 2 || count == 4);
++ // Advance iter over what we've accepted:
++ iter.setPosition(iter.position() - 1 + count);
++ store(QString(), count, formatFlags(uchar(ch), count),
++ formatCategory(uchar(ch), count));
++ result.endIndex = iter.index();
++ continue;
++ }
++ // else: fall through to treat the lone 'y' as a literal.
++ } else if (ch < 0x80 && ((includeDate && matchDateFormats.matches(uchar(ch)))
++ || (includeZone && ch == U't'))) {
++ qsizetype count = countRepeats(ch, 4);
++ store(QString(), count, formatFlags(uchar(ch), count),
++ formatCategory(uchar(ch), count));
++ continue;
++ }
++ Q_ASSERT(pending == 0); // Everything that might set it has continue;d
++
++ // Not a field indicator, so parse as a literal:
++ QString literal;
++ QString quote; // If non-null: unfinished quote, to be appended to literal when closed.
++ if (ch == SingleQuote) { // Defer it to first iteration of loop below.
++ pending = ch;
++ } else {
++ literal = QString(QStringView(QChar::fromUcs4(ch)));
++ result.endIndex = iter.index();
++ }
++ while (pending <= QChar::LastValidCodePoint && (pending || iter.hasNext())) {
++ if (pending) {
++ ch = std::exchange(pending, 0);
++ } else {
++ if (quote.isNull()) // i.e. we're not in an incomplete quote
++ result.endIndex = iter.index();
++ ch = iter.next(Invalid);
++ }
++ if (ch > QChar::LastValidCodePoint)
++ break;
++
++ if (ch == SingleQuote) {
++ if (quote.isNull()) { // Provisionally start a quote
++ quote = u""_s; // empty is not null
++ } else {
++ // Even if this is the first quote of a pair, denoting a
++ // single quote within the quote, there's a valid parse that
++ // ends at it, adding the quote-so-far to literal.
++ literal += quote;
++ result.endIndex = iter.index();
++ quote = QString(); // Set back to null
++ }
++ ch = iter.hasNext() ? iter.next(Invalid) : Invalid;
++ if (ch == SingleQuote) {
++ // Paired single quote denotes a single quote:
++ if (quote.isNull()) {
++ // The quote we thought was ending actually continues:
++ // the continuation starts with a literal single quote.
++ quote = u"'"_s;
++ } else {
++ // Our provisionally-started quote was actually the
++ // first half of an pair of quotes not inside others.
++ Q_ASSERT(quote.isEmpty()); // We just set it.
++ quote = QString();
++ literal.append(u'\'');
++ result.endIndex = iter.index();
++ }
++ continue;
++ }
++
++ if (ch > QChar::LastValidCodePoint) {
++ pending = ch;
++ break;
++ }
++ }
++
++ Q_ASSERT(ch != SingleQuote);
++ if (quote.isNull() && isFormatChar(ch)) {
++ pending = ch;
++ break;
++ }
++ if (ch <= QChar::LastValidCodePoint) {
++ if (quote.isNull()) {
++ result.endIndex = iter.index();
++ literal.append(QStringView(QChar::fromUcs4(ch)));
++ } else {
++ quote.append(QStringView(QChar::fromUcs4(ch)));
++ }
++ }
++ }
++ // Even if we truncated due to an unclosed quote, we have a literal to
++ // include in the prefix we can parse as a pattern:
++ if (!literal.isEmpty())
++ store(std::move(literal), 0, {}, TemporalFieldCategory::Literal);
++
++ // If we're in an unclosed quote, we cleared pending or marked it invalid:
++ Q_ASSERT(quote.isNull() || !pending || pending > QChar::LastValidCodePoint);
++ }
++
++ // Post-process to deal with a quirk of the legacy format: if there's no
++ // AM/PM field, then 'h' format is read as 'H' format.
++ if (seenHourMod12 && !seenDayPeriod) {
++ for (TemporalField &field : result.fields) {
++ if (field.category == TemporalFieldCategory::HourMod12)
++ field.category = TemporalFieldCategory::Hour;
++ }
++ }
++
++ return result;
++ }
++
++ } // QtParseQtTemporalFormat
++
++ QT_END_NAMESPACE