diff options
Diffstat (limited to 'src/corelib/time/qtimezoneprivate_tz.cpp')
-rw-r--r-- | src/corelib/time/qtimezoneprivate_tz.cpp | 431 |
1 files changed, 242 insertions, 189 deletions
diff --git a/src/corelib/time/qtimezoneprivate_tz.cpp b/src/corelib/time/qtimezoneprivate_tz.cpp index 960a094418..45a6867524 100644 --- a/src/corelib/time/qtimezoneprivate_tz.cpp +++ b/src/corelib/time/qtimezoneprivate_tz.cpp @@ -10,6 +10,7 @@ #include <QtCore/QDataStream> #include <QtCore/QDateTime> +#include <QtCore/QDirListing> #include <QtCore/QFile> #include <QtCore/QCache> #include <QtCore/QMap> @@ -19,6 +20,8 @@ #include <qplatformdefs.h> #include <algorithm> +#include <memory> + #include <errno.h> #include <limits.h> #ifndef Q_OS_INTEGRITY @@ -30,10 +33,6 @@ QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; -#if QT_CONFIG(icu) -Q_CONSTINIT static QBasicMutex s_icu_mutex; -#endif - /* Private @@ -41,22 +40,50 @@ Q_CONSTINIT static QBasicMutex s_icu_mutex; */ struct QTzTimeZone { - QLocale::Territory territory; + QLocale::Territory territory = QLocale::AnyTerritory; QByteArray comment; }; // Define as a type as Q_GLOBAL_STATIC doesn't like it typedef QHash<QByteArray, QTzTimeZone> QTzTimeZoneHash; -// Parse zone.tab table, assume lists all installed zones, if not will need to read directories -static QTzTimeZoneHash loadTzTimeZones() +static bool isTzFile(const QString &name); + +// Open a named file under the zone info directory: +static bool openZoneInfo(const QString &name, QFile *file) { - QString path = QStringLiteral("/usr/share/zoneinfo/zone.tab"); - if (!QFile::exists(path)) - path = QStringLiteral("/usr/lib/zoneinfo/zone.tab"); + // At least on Linux / glibc (see man 3 tzset), $TZDIR overrides the system + // default location for zone info: + const QString tzdir = qEnvironmentVariable("TZDIR"); + if (!tzdir.isEmpty()) { + file->setFileName(QDir(tzdir).filePath(name)); + if (file->open(QIODevice::ReadOnly)) + return true; + } + // Try modern system path first: + constexpr auto zoneShare = "/usr/share/zoneinfo/"_L1; + if (tzdir != zoneShare && tzdir != zoneShare.chopped(1)) { + file->setFileName(zoneShare + name); + if (file->open(QIODevice::ReadOnly)) + return true; + } + // Fall back to legacy system path: + constexpr auto zoneLib = "/usr/lib/zoneinfo/"_L1; + if (tzdir != zoneLib && tzdir != zoneLib.chopped(1)) { + file->setFileName(zoneShare + name); + if (file->open(QIODevice::ReadOnly)) + return true; + } + return false; +} - QFile tzif(path); - if (!tzif.open(QIODevice::ReadOnly)) +// Parse zone.tab table for territory information, read directories to ensure we +// find all installed zones (many are omitted from zone.tab; even more from +// zone1970.tab). +static QTzTimeZoneHash loadTzTimeZones() +{ + QFile tzif; + if (!openZoneInfo("zone.tab"_L1, &tzif)) return QTzTimeZoneHash(); QTzTimeZoneHash zonesHash; @@ -85,6 +112,27 @@ static QTzTimeZoneHash loadTzTimeZones() } } } + + const QString path = tzif.fileName(); + const qsizetype cut = path.lastIndexOf(u'/'); + Q_ASSERT(cut > 0); + const QDir zoneDir = QDir(path.first(cut)); + for (const auto &info : QDirListing(zoneDir, QDirListing::IteratorFlag::Recursive)) { + if (!(info.isFile() || info.isSymLink())) + continue; + const QString name = zoneDir.relativeFilePath(info.filePath()); + // Two sub-directories containing (more or less) copies of the zoneinfo tree. + if (info.isDir() ? name == "posix"_L1 || name == "right"_L1 + : name.startsWith("posix/"_L1) || name.startsWith("right/"_L1)) { + continue; + } + // We could filter out *.* and leapseconds instead of doing the + // isTzFile() check; in practice current (2023) zoneinfo/ contains only + // actual zone files and matches to that filter. + const QByteArray id = QFile::encodeName(name); + if (!zonesHash.contains(id) && isTzFile(zoneDir.absoluteFilePath(name))) + zonesHash.insert(id, QTzTimeZone()); + } return zonesHash; } @@ -128,6 +176,11 @@ struct QTzType { }; Q_DECLARE_TYPEINFO(QTzType, Q_PRIMITIVE_TYPE); +static bool isTzFile(const QString &name) +{ + QFile file(name); + return file.open(QFile::ReadOnly) && file.read(strlen(TZ_MAGIC)) == TZ_MAGIC; +} // TZ File parsing @@ -352,13 +405,15 @@ static QDate calculateDowDate(int year, int month, int dayOfWeek, int week) static QDate calculatePosixDate(const QByteArray &dateRule, int year) { + Q_ASSERT(!dateRule.isEmpty()); bool ok; // Can start with M, J, or a digit if (dateRule.at(0) == 'M') { // nth week in month format "Mmonth.week.dow" QList<QByteArray> dateParts = dateRule.split('.'); if (dateParts.size() > 2) { - int month = dateParts.at(0).mid(1).toInt(&ok); + Q_ASSERT(!dateParts.at(0).isEmpty()); // the 'M' is its [0]. + int month = QByteArrayView{ dateParts.at(0) }.sliced(1).toInt(&ok); int week = ok ? dateParts.at(1).toInt(&ok) : 0; int dow = ok ? dateParts.at(2).toInt(&ok) : 0; if (ok) @@ -367,7 +422,7 @@ static QDate calculatePosixDate(const QByteArray &dateRule, int year) } else if (dateRule.at(0) == 'J') { // Day of Year 1...365, ignores Feb 29. // So March always starts on day 60. - int doy = dateRule.mid(1).toInt(&ok); + int doy = QByteArrayView{ dateRule }.sliced(1).toInt(&ok); if (ok && doy > 0 && doy < 366) { // Subtract 1 because we're adding days *after* the first of // January, unless it's after February in a leap year, when the leap @@ -462,12 +517,20 @@ struct PosixZone }; QString name; - int offset; + int offset = InvalidOffset; + bool hasValidOffset() const noexcept { return offset != InvalidOffset; } + QTimeZonePrivate::Data dataAt(qint64 when) + { + Q_ASSERT(hasValidOffset()); + return QTimeZonePrivate::Data(name, when, offset, offset); + } + QTimeZonePrivate::Data dataAtOffset(qint64 when, int standard) + { + Q_ASSERT(hasValidOffset()); + return QTimeZonePrivate::Data(name, when, offset, standard); + } - static PosixZone invalid() { return {QString(), InvalidOffset}; } static PosixZone parse(const char *&pos, const char *end); - - bool hasValidOffset() const noexcept { return offset != InvalidOffset; } }; } // unnamed namespace @@ -498,7 +561,7 @@ PosixZone PosixZone::parse(const char *&pos, const char *end) pos = nameEnd; } if (nameEnd - nameBegin < 3) - return invalid(); // name must be at least 3 characters long + return {}; // name must be at least 3 characters long // zone offset, form [+-]hh:mm:ss const char *zoneBegin = pos; @@ -517,11 +580,16 @@ PosixZone PosixZone::parse(const char *&pos, const char *end) // UTC+hh:mm:ss or GMT+hh:mm:ss should be read as offsets from UTC, not as a // POSIX rule naming a zone as UTC or GMT and specifying a non-zero offset. if (offset != 0 && (name =="UTC"_L1 || name == "GMT"_L1)) - return invalid(); + return {}; return {std::move(name), offset}; } -static auto validatePosixRule(const QByteArray &posixRule) +/* Parse and check a POSIX rule. + + By default a simple zone abbreviation with no offset information is accepted. + Set \a requireOffset to \c true to require that there be offset data present. +*/ +static auto validatePosixRule(const QByteArray &posixRule, bool requireOffset = false) { // Format is described here: // http://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html @@ -533,15 +601,19 @@ static auto validatePosixRule(const QByteArray &posixRule) return fail; const char *begin = zoneinfo.begin(); - - // Updates begin to point after the name and offset it parses: - if (PosixZone::parse(begin, zoneinfo.end()).name.isEmpty()) - return fail; + { + // Updates begin to point after the name and offset it parses: + const auto posix = PosixZone::parse(begin, zoneinfo.end()); + if (posix.name.isEmpty()) + return fail; + if (requireOffset && !posix.hasValidOffset()) + return fail; + } if (good.hasDst) { if (begin >= zoneinfo.end()) return fail; - // Expect a second name and offset after the first: + // Expect a second name (and optional offset) after the first: if (PosixZone::parse(begin, zoneinfo.end()).name.isEmpty()) return fail; } @@ -578,7 +650,7 @@ static QList<QTimeZonePrivate::Data> calculatePosixTransitions(const QByteArray // and the link in validatePosixRule(), above. QList<QByteArray> parts = posixRule.split(','); - PosixZone stdZone, dstZone = PosixZone::invalid(); + PosixZone stdZone, dstZone; { const QByteArray &zoneinfo = parts.at(0); const char *begin = zoneinfo.constBegin(); @@ -597,13 +669,9 @@ static QList<QTimeZonePrivate::Data> calculatePosixTransitions(const QByteArray // If only the name part, or no DST specified, then no transitions if (parts.size() == 1 || !dstZone.hasValidOffset()) { - QTimeZonePrivate::Data data; - data.atMSecsSinceEpoch = lastTranMSecs; - data.offsetFromUtc = stdZone.offset; - data.standardTimeOffset = stdZone.offset; - data.daylightTimeOffset = 0; - data.abbreviation = stdZone.name.isEmpty() ? QString::fromUtf8(parts.at(0)) : stdZone.name; - result << data; + result.emplaceBack( + stdZone.name.isEmpty() ? QString::fromUtf8(parts.at(0)) : stdZone.name, + lastTranMSecs, stdZone.offset, stdZone.offset); return result; } if (parts.size() < 3 || parts.at(1).isEmpty() || parts.at(2).isEmpty()) @@ -636,40 +704,33 @@ static QList<QTimeZonePrivate::Data> calculatePosixTransitions(const QByteArray // moments; the atMSecsSinceEpoch values computed from them are // correctly offse to be UTC-based. - QTimeZonePrivate::Data dstData; // Transition to DST + // Transition to daylight-saving time: QDateTime dst(calculatePosixDate(dstDateRule, year) .startOfDay(QTimeZone::UTC).addSecs(dstTime)); - dstData.atMSecsSinceEpoch = dst.toMSecsSinceEpoch() - stdZone.offset * 1000; - dstData.offsetFromUtc = dstZone.offset; - dstData.standardTimeOffset = stdZone.offset; - dstData.daylightTimeOffset = dstZone.offset - stdZone.offset; - dstData.abbreviation = dstZone.name; - QTimeZonePrivate::Data stdData; // Transition to standard time + auto saving = dstZone.dataAtOffset(dst.toMSecsSinceEpoch() - stdZone.offset * 1000, + stdZone.offset); + // Transition to standard time: QDateTime std(calculatePosixDate(stdDateRule, year) .startOfDay(QTimeZone::UTC).addSecs(stdTime)); - stdData.atMSecsSinceEpoch = std.toMSecsSinceEpoch() - dstZone.offset * 1000; - stdData.offsetFromUtc = stdZone.offset; - stdData.standardTimeOffset = stdZone.offset; - stdData.daylightTimeOffset = 0; - stdData.abbreviation = stdZone.name; + auto standard = stdZone.dataAt(std.toMSecsSinceEpoch() - dstZone.offset * 1000); if (year == startYear) { // Handle the special case of fixed state, which may be represented // by fake transitions at start and end of each year: - if (dstData.atMSecsSinceEpoch < stdData.atMSecsSinceEpoch) { + if (saving.atMSecsSinceEpoch < standard.atMSecsSinceEpoch) { if (dst <= QDate(year, 1, 1).startOfDay(QTimeZone::UTC) && std >= QDate(year, 12, 31).endOfDay(QTimeZone::UTC)) { // Permanent DST: - dstData.atMSecsSinceEpoch = lastTranMSecs; - result << dstData; + saving.atMSecsSinceEpoch = lastTranMSecs; + result.emplaceBack(std::move(saving)); return result; } } else { if (std <= QDate(year, 1, 1).startOfDay(QTimeZone::UTC) && dst >= QDate(year, 12, 31).endOfDay(QTimeZone::UTC)) { // Permanent Standard time, perversely described: - stdData.atMSecsSinceEpoch = lastTranMSecs; - result << stdData; + standard.atMSecsSinceEpoch = lastTranMSecs; + result.emplaceBack(std::move(standard)); return result; } } @@ -678,14 +739,17 @@ static QList<QTimeZonePrivate::Data> calculatePosixTransitions(const QByteArray const bool useStd = std.isValid() && std.date().year() == year && !stdZone.name.isEmpty(); const bool useDst = dst.isValid() && dst.date().year() == year && !dstZone.name.isEmpty(); if (useStd && useDst) { - if (dst < std) - result << dstData << stdData; - else - result << stdData << dstData; + if (dst < std) { + result.emplaceBack(std::move(saving)); + result.emplaceBack(std::move(standard)); + } else { + result.emplaceBack(std::move(standard)); + result.emplaceBack(std::move(saving)); + } } else if (useStd) { - result << stdData; + result.emplaceBack(std::move(standard)); } else if (useDst) { - result << dstData; + result.emplaceBack(std::move(saving)); } } return result; @@ -703,9 +767,6 @@ QTzTimeZonePrivate::~QTzTimeZonePrivate() QTzTimeZonePrivate *QTzTimeZonePrivate::clone() const { -#if QT_CONFIG(icu) - const auto lock = qt_scoped_lock(s_icu_mutex); -#endif return new QTzTimeZonePrivate(*this); } @@ -715,7 +776,7 @@ public: QTzTimeZoneCacheEntry fetchEntry(const QByteArray &ianaId); private: - QTzTimeZoneCacheEntry findEntry(const QByteArray &ianaId); + static QTzTimeZoneCacheEntry findEntry(const QByteArray &ianaId); QCache<QByteArray, QTzTimeZoneCacheEntry> m_cache; QMutex m_mutex; }; @@ -729,21 +790,14 @@ QTzTimeZoneCacheEntry QTzTimeZoneCache::findEntry(const QByteArray &ianaId) tzif.setFileName(QStringLiteral("/etc/localtime")); if (!tzif.open(QIODevice::ReadOnly)) return ret; - } else { - // Open named tz, try modern path first, if fails try legacy path - tzif.setFileName("/usr/share/zoneinfo/"_L1 + QString::fromLocal8Bit(ianaId)); - if (!tzif.open(QIODevice::ReadOnly)) { - tzif.setFileName("/usr/lib/zoneinfo/"_L1 + QString::fromLocal8Bit(ianaId)); - if (!tzif.open(QIODevice::ReadOnly)) { - // ianaId may be a POSIX rule, taken from $TZ or /etc/TZ - auto check = validatePosixRule(ianaId); - if (check.isValid) { - ret.m_hasDst = check.hasDst; - ret.m_posixRule = ianaId; - } - return ret; - } + } else if (!openZoneInfo(QString::fromLocal8Bit(ianaId), &tzif)) { + // ianaId may be a POSIX rule, taken from $TZ or /etc/TZ + auto check = validatePosixRule(ianaId); + if (check.isValid) { + ret.m_hasDst = check.hasDst; + ret.m_posixRule = ianaId; } + return ret; } QDataStream ds(&tzif); @@ -823,8 +877,6 @@ QTzTimeZoneCacheEntry QTzTimeZoneCache::findEntry(const QByteArray &ianaId) // TODO: is typeList[0] always the "before zones" data ? It seems to be ... if (typeList.size()) ret.m_preZoneRule = { typeList.at(0).tz_gmtoff, 0, typeList.at(0).tz_abbrind }; - else - ret.m_preZoneRule = { 0, 0, 0 }; // Offsets are stored as total offset, want to know separate UTC and DST offsets // so find the first non-dst transition to use as base UTC Offset @@ -837,7 +889,7 @@ QTzTimeZoneCacheEntry QTzTimeZoneCache::findEntry(const QByteArray &ianaId) } // Now for each transition time calculate and store our rule: - const int tranCount = tranList.size();; + const int tranCount = tranList.size(); ret.m_tranTimes.reserve(tranCount); // The DST offset when in effect: usually stable, usually an hour: int lastDstOff = 3600; @@ -896,8 +948,8 @@ QTzTimeZoneCacheEntry QTzTimeZoneCache::findEntry(const QByteArray &ianaId) if (ruleIndex == -1) { if (rule.dstOffset != 0) ret.m_hasDst = true; + tran.ruleIndex = ret.m_tranRules.size(); ret.m_tranRules.append(rule); - tran.ruleIndex = ret.m_tranRules.size() - 1; } else { tran.ruleIndex = ruleIndex; } @@ -919,14 +971,24 @@ QTzTimeZoneCacheEntry QTzTimeZoneCache::fetchEntry(const QByteArray &ianaId) return *obj; // ... or build a new entry from scratch + + locker.unlock(); // don't parse files under mutex lock + QTzTimeZoneCacheEntry ret = findEntry(ianaId); - m_cache.insert(ianaId, new QTzTimeZoneCacheEntry(ret)); + auto ptr = std::make_unique<QTzTimeZoneCacheEntry>(ret); + + locker.relock(); + m_cache.insert(ianaId, ptr.release()); // may overwrite if another thread was faster + locker.unlock(); + return ret; } // Create a named time zone QTzTimeZonePrivate::QTzTimeZonePrivate(const QByteArray &ianaId) { + if (!isTimeZoneIdAvailable(ianaId)) // Avoid pointlessly creating cache entries + return; static QTzTimeZoneCache tzCache; auto entry = tzCache.fetchEntry(ianaId); if (entry.m_tranTimes.isEmpty() && entry.m_posixRule.isEmpty()) @@ -938,15 +1000,7 @@ QTzTimeZonePrivate::QTzTimeZonePrivate(const QByteArray &ianaId) if (m_id.isEmpty()) { // This can only happen for the system zone, when we've read the // contents of /etc/localtime because it wasn't a symlink. -#if QT_CONFIG(icu) - // Use ICU's system zone, if only to avoid using the abbreviation as ID - // (ICU might mis-recognize it) in displayName(). - m_icu = new QIcuTimeZonePrivate(); - // Use its ID, as an alternate source of data: - m_id = m_icu->id(); - if (!m_id.isEmpty()) - return; -#endif + // TODO: use CLDR generic abbreviation for the zone. m_id = abbreviation(QDateTime::currentMSecsSinceEpoch()).toUtf8(); } } @@ -961,93 +1015,23 @@ QString QTzTimeZonePrivate::comment() const return QString::fromUtf8(tzZones->value(m_id).comment); } -QString QTzTimeZonePrivate::displayName(qint64 atMSecsSinceEpoch, - QTimeZone::NameType nameType, - const QLocale &locale) const -{ -#if QT_CONFIG(icu) - auto lock = qt_unique_lock(s_icu_mutex); - if (!m_icu) - m_icu = new QIcuTimeZonePrivate(m_id); - // TODO small risk may not match if tran times differ due to outdated files - // TODO Some valid TZ names are not valid ICU names, use translation table? - if (m_icu->isValid()) - return m_icu->displayName(atMSecsSinceEpoch, nameType, locale); - lock.unlock(); -#else - Q_UNUSED(nameType); - Q_UNUSED(locale); -#endif - // Fall back to base-class: - return QTimeZonePrivate::displayName(atMSecsSinceEpoch, nameType, locale); -} - QString QTzTimeZonePrivate::displayName(QTimeZone::TimeType timeType, QTimeZone::NameType nameType, const QLocale &locale) const { -#if QT_CONFIG(icu) - auto lock = qt_unique_lock(s_icu_mutex); - if (!m_icu) - m_icu = new QIcuTimeZonePrivate(m_id); - // TODO small risk may not match if tran times differ due to outdated files - // TODO Some valid TZ names are not valid ICU names, use translation table? - if (m_icu->isValid()) - return m_icu->displayName(timeType, nameType, locale); - lock.unlock(); -#else - Q_UNUSED(timeType); - Q_UNUSED(nameType); - Q_UNUSED(locale); -#endif - // If no ICU available then have to use abbreviations instead - // Abbreviations don't have GenericTime - if (timeType == QTimeZone::GenericTime) - timeType = QTimeZone::StandardTime; - - // Get current tran, if valid and is what we want, then use it - const qint64 currentMSecs = QDateTime::currentMSecsSinceEpoch(); - QTimeZonePrivate::Data tran = data(currentMSecs); - if (tran.atMSecsSinceEpoch != invalidMSecs() - && ((timeType == QTimeZone::DaylightTime && tran.daylightTimeOffset != 0) - || (timeType == QTimeZone::StandardTime && tran.daylightTimeOffset == 0))) { - return tran.abbreviation; - } - - // Otherwise get next tran and if valid and is what we want, then use it - tran = nextTransition(currentMSecs); - if (tran.atMSecsSinceEpoch != invalidMSecs() - && ((timeType == QTimeZone::DaylightTime && tran.daylightTimeOffset != 0) - || (timeType == QTimeZone::StandardTime && tran.daylightTimeOffset == 0))) { - return tran.abbreviation; - } - - // Otherwise get prev tran and if valid and is what we want, then use it - tran = previousTransition(currentMSecs); - if (tran.atMSecsSinceEpoch != invalidMSecs()) - tran = previousTransition(tran.atMSecsSinceEpoch); - if (tran.atMSecsSinceEpoch != invalidMSecs() - && ((timeType == QTimeZone::DaylightTime && tran.daylightTimeOffset != 0) - || (timeType == QTimeZone::StandardTime && tran.daylightTimeOffset == 0))) { - return tran.abbreviation; - } - - // Otherwise is strange sequence, so work backwards through trans looking for first match, if any - auto it = std::partition_point(tranCache().cbegin(), tranCache().cend(), - [currentMSecs](const QTzTransitionTime &at) { - return at.atMSecsSinceEpoch <= currentMSecs; - }); - - while (it != tranCache().cbegin()) { - --it; - tran = dataForTzTransition(*it); - int offset = tran.daylightTimeOffset; - if ((timeType == QTimeZone::DaylightTime) != (offset == 0)) - return tran.abbreviation; + // TZ only provides C-locale abbreviations and offset: + if (nameType != QTimeZone::LongName && isDataLocale(locale)) { + QTimeZonePrivate::Data tran = data(timeType); + if (tran.atMSecsSinceEpoch != invalidMSecs()) { + if (nameType == QTimeZone::ShortName) + return tran.abbreviation; + // Save base class repeating the data(timeType) query: + if (locale.language() == QLocale::C) + return isoOffsetFormat(tran.offsetFromUtc); + } } - - // Otherwise if no match use current data - return data(currentMSecs).abbreviation; + // Otherwise, fall back to base class (and qtimezonelocale.cpp): + return QTimeZonePrivate::displayName(timeType, nameType, locale); } QString QTzTimeZonePrivate::abbreviation(qint64 atMSecsSinceEpoch) const @@ -1089,8 +1073,8 @@ QTimeZonePrivate::Data QTzTimeZonePrivate::dataForTzTransition(QTzTransitionTime QTimeZonePrivate::Data QTzTimeZonePrivate::dataFromRule(QTzTransitionRule rule, qint64 msecsSinceEpoch) const { - return { QString::fromUtf8(cached_data.m_abbreviations.at(rule.abbreviationIndex)), - msecsSinceEpoch, rule.stdOffset + rule.dstOffset, rule.stdOffset, rule.dstOffset }; + return Data(QString::fromUtf8(cached_data.m_abbreviations.at(rule.abbreviationIndex)), + msecsSinceEpoch, rule.stdOffset + rule.dstOffset, rule.stdOffset); } QList<QTimeZonePrivate::Data> QTzTimeZonePrivate::getPosixTransitions(qint64 msNear) const @@ -1120,11 +1104,11 @@ QTimeZonePrivate::Data QTzTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const } } if (tranCache().isEmpty()) // Only possible if !isValid() - return invalidData(); + return {}; // Otherwise, use the rule for the most recent or first transition: auto last = std::partition_point(tranCache().cbegin(), tranCache().cend(), - [forMSecsSinceEpoch] (const QTzTransitionTime &at) { + [forMSecsSinceEpoch] (QTzTransitionTime at) { return at.atMSecsSinceEpoch <= forMSecsSinceEpoch; }); if (last == tranCache().cbegin()) @@ -1134,6 +1118,64 @@ QTimeZonePrivate::Data QTzTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const return dataFromRule(cached_data.m_tranRules.at(last->ruleIndex), forMSecsSinceEpoch); } +// Overridden because the final iteration over transitions only needs to look +// forward and backwards one transition within the POSIX rule (when there is +// one, as is common) to settle the whole period it covers, so we can then skip +// all other transitions of the POSIX rule and iterate tranCache() backwards +// from its most recent transition. +QTimeZonePrivate::Data QTzTimeZonePrivate::data(QTimeZone::TimeType timeType) const +{ + // True if tran is valid and has the DST-ness to match timeType: + const auto validMatch = [timeType](const QTimeZonePrivate::Data &tran) { + return tran.atMSecsSinceEpoch != invalidMSecs() + && ((timeType == QTimeZone::DaylightTime) != (tran.daylightTimeOffset == 0)); + }; + + // Get current tran, use if suitable: + const qint64 currentMSecs = QDateTime::currentMSecsSinceEpoch(); + QTimeZonePrivate::Data tran = data(currentMSecs); + if (validMatch(tran)) + return tran; + + // Otherwise, next tran probably flips DST-ness: + tran = nextTransition(currentMSecs); + if (validMatch(tran)) + return tran; + + // Failing that, prev (or present, if current MSecs is eactly a transition + // moment) tran defines what data() got us and the one before that probably + // flips DST-ness: + tran = previousTransition(currentMSecs + 1); + if (tran.atMSecsSinceEpoch != invalidMSecs()) + tran = previousTransition(tran.atMSecsSinceEpoch); + if (validMatch(tran)) + return tran; + + // Otherwise, we can look backwards through transitions for a match; if we + // have a POSIX rule, it clearly doesn't do DST (or we'd have hit it by + // now), so we only need to look in the tranCache() up to now. + const auto untilNow = [currentMSecs](QTzTransitionTime at) { + return at.atMSecsSinceEpoch <= currentMSecs; + }; + auto it = std::partition_point(tranCache().cbegin(), tranCache().cend(), untilNow); + // That's the end or first future transition; we don't want to look at it, + // but at all those before it. + while (it != tranCache().cbegin()) { + --it; + tran = dataForTzTransition(*it); + if ((timeType == QTimeZone::DaylightTime) != (tran.daylightTimeOffset == 0)) + return tran; + } + + return {}; +} + +bool QTzTimeZonePrivate::isDataLocale(const QLocale &locale) const +{ + // TZ data uses English / C locale names: + return locale.language() == QLocale::C || locale.language() == QLocale::English; +} + bool QTzTimeZonePrivate::hasTransitions() const { return true; @@ -1151,15 +1193,15 @@ QTimeZonePrivate::Data QTzTimeZonePrivate::nextTransition(qint64 afterMSecsSince return at.atMSecsSinceEpoch <= afterMSecsSinceEpoch; }); - return it == posixTrans.cend() ? invalidData() : *it; + return it == posixTrans.cend() ? Data{} : *it; } // Otherwise, if we can find a valid tran, use its rule: auto last = std::partition_point(tranCache().cbegin(), tranCache().cend(), - [afterMSecsSinceEpoch] (const QTzTransitionTime &at) { + [afterMSecsSinceEpoch] (QTzTransitionTime at) { return at.atMSecsSinceEpoch <= afterMSecsSinceEpoch; }); - return last != tranCache().cend() ? dataForTzTransition(*last) : invalidData(); + return last != tranCache().cend() ? dataForTzTransition(*last) : Data{}; } QTimeZonePrivate::Data QTzTimeZonePrivate::previousTransition(qint64 beforeMSecsSinceEpoch) const @@ -1176,20 +1218,24 @@ QTimeZonePrivate::Data QTzTimeZonePrivate::previousTransition(qint64 beforeMSecs if (it > posixTrans.cbegin()) return *--it; // It fell between the last transition (if any) and the first of the POSIX rule: - return tranCache().isEmpty() ? invalidData() : dataForTzTransition(tranCache().last()); + return tranCache().isEmpty() ? Data{} : dataForTzTransition(tranCache().last()); } // Otherwise if we can find a valid tran then use its rule auto last = std::partition_point(tranCache().cbegin(), tranCache().cend(), - [beforeMSecsSinceEpoch] (const QTzTransitionTime &at) { + [beforeMSecsSinceEpoch] (QTzTransitionTime at) { return at.atMSecsSinceEpoch < beforeMSecsSinceEpoch; }); - return last > tranCache().cbegin() ? dataForTzTransition(*--last) : invalidData(); + return last > tranCache().cbegin() ? dataForTzTransition(*--last) : Data{}; } bool QTzTimeZonePrivate::isTimeZoneIdAvailable(const QByteArray &ianaId) const { - return tzZones->contains(ianaId); + // Allow a POSIX rule as long as it has offset data. (This needs to reject a + // plain abbreviation, without offset, since claiming to support such zones + // would prevent the custom QTimeZone constructor from accepting such a + // name, as it doesn't want a custom zone to over-ride a "real" one.) + return tzZones->contains(ianaId) || validatePosixRule(ianaId, true).isValid; } QList<QByteArray> QTzTimeZonePrivate::availableTimeZoneIds() const @@ -1270,8 +1316,8 @@ private: constexpr StatIdent() : m_dev(bad), m_ino(bad) {} StatIdent(const QT_STATBUF &data) : m_dev(data.st_dev), m_ino(data.st_ino) {} bool isValid() { return m_dev != bad || m_ino != bad; } - bool operator==(const StatIdent &other) - { return other.m_dev == m_dev && other.m_ino == m_ino; } + friend constexpr bool operator==(StatIdent lhs, StatIdent rhs) + { return lhs.m_dev == rhs.m_dev && lhs.m_ino == rhs.m_ino; } }; StatIdent m_last; @@ -1285,7 +1331,8 @@ private: { // On most distros /etc/localtime is a symlink to a real file so extract // name from the path - const auto zoneinfo = "/zoneinfo/"_L1; + const QString tzdir = qEnvironmentVariable("TZDIR"); + constexpr auto zoneinfo = "/zoneinfo/"_L1; QString path = QStringLiteral("/etc/localtime"); long iteration = getSymloopMax(); // Symlink may point to another symlink etc. before being under zoneinfo/ @@ -1293,9 +1340,15 @@ private: // symlink, like America/Montreal pointing to America/Toronto do { path = QFile::symLinkTarget(path); - int index = path.indexOf(zoneinfo); - if (index >= 0) // Found zoneinfo file; extract zone name from path: - return QStringView{ path }.mid(index + zoneinfo.size()).toUtf8(); + // If it's a zoneinfo file, extract the zone name from its path: + int index = tzdir.isEmpty() ? -1 : path.indexOf(tzdir); + if (index >= 0) { + const auto tail = QStringView{ path }.sliced(index + tzdir.size()).toUtf8(); + return tail.startsWith(u'/') ? tail.sliced(1) : tail; + } + index = path.indexOf(zoneinfo); + if (index >= 0) + return QStringView{ path }.sliced(index + zoneinfo.size()).toUtf8(); } while (!path.isEmpty() && --iteration > 0); return QByteArray(); @@ -1352,7 +1405,7 @@ QByteArray QTzTimeZonePrivate::staticSystemTimeZoneId() if (ianaId == ":/etc/localtime") ianaId.clear(); else if (ianaId.startsWith(':')) - ianaId = ianaId.mid(1); + ianaId = ianaId.sliced(1); if (ianaId.isEmpty()) { Q_CONSTINIT thread_local static ZoneNameReader reader; |