diff options
Diffstat (limited to 'src/corelib/time/qtimezoneprivate_tz.cpp')
-rw-r--r-- | src/corelib/time/qtimezoneprivate_tz.cpp | 685 |
1 files changed, 413 insertions, 272 deletions
diff --git a/src/corelib/time/qtimezoneprivate_tz.cpp b/src/corelib/time/qtimezoneprivate_tz.cpp index ace966e15b..f6156fe93e 100644 --- a/src/corelib/time/qtimezoneprivate_tz.cpp +++ b/src/corelib/time/qtimezoneprivate_tz.cpp @@ -1,58 +1,27 @@ -/**************************************************************************** -** -** Copyright (C) 2020 The Qt Company Ltd. -** Copyright (C) 2019 Crimson AS <info@crimson.no> -** Copyright (C) 2013 John Layt <jlayt@kde.org> -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtCore module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2022 The Qt Company Ltd. +// Copyright (C) 2019 Crimson AS <info@crimson.no> +// Copyright (C) 2013 John Layt <jlayt@kde.org> +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qtimezone.h" #include "qtimezoneprivate_p.h" #include "private/qlocale_tools_p.h" +#include "private/qlocking_p.h" #include <QtCore/QDataStream> #include <QtCore/QDateTime> +#include <QtCore/QDirListing> #include <QtCore/QFile> #include <QtCore/QCache> +#include <QtCore/QMap> #include <QtCore/QMutex> #include <qdebug.h> #include <qplatformdefs.h> #include <algorithm> +#include <memory> + #include <errno.h> #include <limits.h> #ifndef Q_OS_INTEGRITY @@ -62,6 +31,12 @@ QT_BEGIN_NAMESPACE +using namespace Qt::StringLiterals; + +#if QT_CONFIG(icu) +Q_CONSTINIT static QBasicMutex s_icu_mutex; +#endif + /* Private @@ -69,45 +44,104 @@ QT_BEGIN_NAMESPACE */ struct QTzTimeZone { - QLocale::Country country; + 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; - // TODO QTextStream inefficient, replace later - QTextStream ts(&tzif); - while (!ts.atEnd()) { - const QString line = ts.readLine(); - // Comment lines are prefixed with a # - if (!line.isEmpty() && line.at(0) != u'#') { - // Data rows are tab-separated columns Region, Coordinates, ID, Optional Comments - const auto parts = QStringView{line}.split(QLatin1Char('\t')); + while (!tzif.atEnd()) { + const QByteArray line = tzif.readLine().trimmed(); + if (line.isEmpty() || line.at(0) == '#') // Ignore empty or comment + continue; + // Data rows are tab-separated columns Region, Coordinates, ID, Optional Comments + QByteArrayView text(line); + int cut = text.indexOf('\t'); + if (Q_LIKELY(cut > 0)) { QTzTimeZone zone; - zone.country = QLocalePrivate::codeToCountry(parts.at(0)); - if (parts.size() > 3) - zone.comment = parts.at(3).toUtf8(); - zonesHash.insert(parts.at(2).toUtf8(), zone); + // TODO: QLocale & friends could do this look-up without UTF8-conversion: + zone.territory = QLocalePrivate::codeToTerritory(QString::fromUtf8(text.first(cut))); + text = text.sliced(cut + 1); + cut = text.indexOf('\t'); + if (Q_LIKELY(cut >= 0)) { // Skip over Coordinates, read ID and comment + text = text.sliced(cut + 1); + cut = text.indexOf('\t'); // < 0 if line has no comment + if (Q_LIKELY(cut)) { + const QByteArray id = (cut > 0 ? text.first(cut) : text).toByteArray(); + if (cut > 0) + zone.comment = text.sliced(cut + 1).toByteArray(); + zonesHash.insert(id, zone); + } + } + } + } + + 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; } // Hash of available system tz files as loaded by loadTzTimeZones() -Q_GLOBAL_STATIC_WITH_ARGS(const QTzTimeZoneHash, tzZones, (loadTzTimeZones())); +Q_GLOBAL_STATIC(const QTzTimeZoneHash, tzZones, loadTzTimeZones()); /* The following is copied and modified from tzfile.h which is in the public domain. @@ -146,6 +180,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 @@ -351,6 +390,11 @@ static QByteArray parseTzPosixRule(QDataStream &ds) static QDate calculateDowDate(int year, int month, int dayOfWeek, int week) { + if (dayOfWeek == 0) // Sunday; we represent it as 7, POSIX uses 0 + dayOfWeek = 7; + else if (dayOfWeek & ~7 || month < 1 || month > 12 || week < 1 || week > 5) + return QDate(); + QDate date(year, month, 1); int startDow = date.dayOfWeek(); if (startDow <= dayOfWeek) @@ -365,28 +409,39 @@ 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('.'); - int month = dateParts.at(0).mid(1).toInt(); - int week = dateParts.at(1).toInt(); - int dow = dateParts.at(2).toInt(); - if (dow == 0) // Sunday; we represent it as 7 - dow = 7; - return calculateDowDate(year, month, dow, week); + if (dateParts.size() > 2) { + 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) + return calculateDowDate(year, month, dow, week); + } } else if (dateRule.at(0) == 'J') { - // Day of Year ignores Feb 29 - int doy = dateRule.mid(1).toInt(); - QDate date = QDate(year, 1, 1).addDays(doy - 1); - if (QDate::isLeapYear(date.year())) - date = date.addDays(-1); - return date; + // Day of Year 1...365, ignores Feb 29. + // So March always starts on day 60. + 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 + // day cancels that out: + if (!QDate::isLeapYear(year) || doy < 60) + --doy; + return QDate(year, 1, 1).addDays(doy); + } } else { - // Day of Year includes Feb 29 - int doy = dateRule.toInt(); - return QDate(year, 1, 1).addDays(doy - 1); + // Day of Year 0...365, includes Feb 29 + int doy = dateRule.toInt(&ok); + if (ok && doy >= 0 && doy < 366) + return QDate(year, 1, 1).addDays(doy); } + return QDate(); } // returns the time in seconds, INT_MIN if we failed to parse @@ -395,34 +450,29 @@ static int parsePosixTime(const char *begin, const char *end) // Format "hh[:mm[:ss]]" int hour, min = 0, sec = 0; - // Note that the calls to qstrtoll do *not* check against the end pointer, - // which means they proceed until they find a non-digit. We check that we're - // still in range at the end, but we may have read past end. It's the - // caller's responsibility to ensure that begin is part of a null-terminated - // string. - - const int maxHour = QTimeZone::MaxUtcOffsetSecs / 3600; - bool ok = false; - const char *cut = begin; - hour = qstrtoll(begin, &cut, 10, &ok); - if (!ok || hour < 0 || hour > maxHour || cut > begin + 2) + const int maxHour = 137; // POSIX's extended range. + auto r = qstrntoll(begin, end - begin, 10); + hour = r.result; + if (!r.ok() || hour < -maxHour || hour > maxHour || r.used > 2) return INT_MIN; - begin = cut; + begin += r.used; if (begin < end && *begin == ':') { // minutes ++begin; - min = qstrtoll(begin, &cut, 10, &ok); - if (!ok || min < 0 || min > 59 || cut > begin + 2) + r = qstrntoll(begin, end - begin, 10); + min = r.result; + if (!r.ok() || min < 0 || min > 59 || r.used > 2) return INT_MIN; - begin = cut; + begin += r.used; if (begin < end && *begin == ':') { // seconds ++begin; - sec = qstrtoll(begin, &cut, 10, &ok); - if (!ok || sec < 0 || sec > 59 || cut > begin + 2) + r = qstrntoll(begin, end - begin, 10); + sec = r.result; + if (!r.ok() || sec < 0 || sec > 59 || r.used > 2) return INT_MIN; - begin = cut; + begin += r.used; } } @@ -433,15 +483,9 @@ static int parsePosixTime(const char *begin, const char *end) return (hour * 60 + min) * 60 + sec; } -static QTime parsePosixTransitionTime(const QByteArray &timeRule) +static int parsePosixTransitionTime(const QByteArray &timeRule) { - // Format "hh[:mm[:ss]]" - int value = parsePosixTime(timeRule.constBegin(), timeRule.constEnd()); - if (value == INT_MIN) { - // if we failed to parse, return 02:00 - return QTime(2, 0, 0); - } - return QTime::fromMSecsSinceStartOfDay(value * 1000); + return parsePosixTime(timeRule.constBegin(), timeRule.constEnd()); } static int parsePosixOffset(const char *begin, const char *end) @@ -477,12 +521,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 @@ -499,7 +551,7 @@ PosixZone PosixZone::parse(const char *&pos, const char *end) Q_ASSERT(pos < end); if (*pos == '<') { - nameBegin = pos + 1; // skip the '<' + ++nameBegin; // skip the '<' nameEnd = nameBegin; while (nameEnd < end && *nameEnd != '>') { // POSIX says only alphanumeric, but we allow anything @@ -507,14 +559,13 @@ PosixZone PosixZone::parse(const char *&pos, const char *end) } pos = nameEnd + 1; // skip the '>' } else { - nameBegin = pos; nameEnd = nameBegin; while (nameEnd < end && asciiIsLetter(*nameEnd)) ++nameEnd; 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; @@ -532,12 +583,66 @@ PosixZone PosixZone::parse(const char *&pos, const char *end) pos = zoneEnd; // 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 == QLatin1String("UTC") || name == QLatin1String("GMT"))) - return invalid(); + if (offset != 0 && (name =="UTC"_L1 || name == "GMT"_L1)) + return {}; return {std::move(name), offset}; } -static QList<QTimeZonePrivate::Data> calculatePosixTransitions(const QByteArray &posixRule, int startYear, int endYear, +/* 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 + // See also calculatePosixTransition()'s reference. + const auto parts = posixRule.split(','); + const struct { bool isValid, hasDst; } fail{false, false}, good{true, parts.size() > 1}; + const QByteArray &zoneinfo = parts.at(0); + if (zoneinfo.isEmpty()) + return fail; + + const char *begin = zoneinfo.begin(); + { + // 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 optional offset) after the first: + if (PosixZone::parse(begin, zoneinfo.end()).name.isEmpty()) + return fail; + } + if (begin < zoneinfo.end()) + return fail; + + if (good.hasDst) { + if (parts.size() != 3 || parts.at(1).isEmpty() || parts.at(2).isEmpty()) + return fail; + for (int i = 1; i < 3; ++i) { + const auto tran = parts.at(i).split('/'); + if (!calculatePosixDate(tran.at(0), 1972).isValid()) + return fail; + if (tran.size() > 1) { + const auto time = tran.at(1); + if (parsePosixTime(time.begin(), time.end()) == INT_MIN) + return fail; + } + } + } + return good; +} + +static QList<QTimeZonePrivate::Data> calculatePosixTransitions(const QByteArray &posixRule, + int startYear, int endYear, qint64 lastTranMSecs) { QList<QTimeZonePrivate::Data> result; @@ -546,9 +651,10 @@ static QList<QTimeZonePrivate::Data> calculatePosixTransitions(const QByteArray // i.e. "std offset dst [offset],start[/time],end[/time]" // See the section about TZ at // http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html + // 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(); @@ -565,36 +671,29 @@ static QList<QTimeZonePrivate::Data> calculatePosixTransitions(const QByteArray } } - // If only the name part then no transitions - if (parts.count() == 1) { - QTimeZonePrivate::Data data; - data.atMSecsSinceEpoch = lastTranMSecs; - data.offsetFromUtc = stdZone.offset; - data.standardTimeOffset = stdZone.offset; - data.daylightTimeOffset = 0; - data.abbreviation = stdZone.name; - result << data; + // If only the name part, or no DST specified, then no transitions + if (parts.size() == 1 || !dstZone.hasValidOffset()) { + 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()) + return result; // Malformed. + // Get the std to dst transition details + const int twoOClock = 7200; // Default transition time, when none specified + const auto dstParts = parts.at(1).split('/'); + const QByteArray dstDateRule = dstParts.at(0); + const int dstTime = dstParts.size() < 2 ? twoOClock : parsePosixTransitionTime(dstParts.at(1)); - // Get the std to dst transtion details - QList<QByteArray> dstParts = parts.at(1).split('/'); - QByteArray dstDateRule = dstParts.at(0); - QTime dstTime; - if (dstParts.count() > 1) - dstTime = parsePosixTransitionTime(dstParts.at(1)); - else - dstTime = QTime(2, 0, 0); - - // Get the dst to std transtion details - QList<QByteArray> stdParts = parts.at(2).split('/'); - QByteArray stdDateRule = stdParts.at(0); - QTime stdTime; - if (stdParts.count() > 1) - stdTime = parsePosixTransitionTime(stdParts.at(1)); - else - stdTime = QTime(2, 0, 0); + // Get the dst to std transition details + const auto stdParts = parts.at(2).split('/'); + const QByteArray stdDateRule = stdParts.at(0); + const int stdTime = stdParts.size() < 2 ? twoOClock : parsePosixTransitionTime(stdParts.at(1)); + + if (dstDateRule.isEmpty() || stdDateRule.isEmpty() || dstTime == INT_MIN || stdTime == INT_MIN) + return result; // Malformed. // Limit year to the range QDateTime can represent: const int minYear = int(QDateTime::YearRange::First); @@ -604,34 +703,57 @@ static QList<QTimeZonePrivate::Data> calculatePosixTransitions(const QByteArray Q_ASSERT(startYear <= endYear); for (int year = startYear; year <= endYear; ++year) { - QTimeZonePrivate::Data dstData; - QDateTime dst(calculatePosixDate(dstDateRule, year), dstTime, Qt::UTC); - 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; - QDateTime std(calculatePosixDate(stdDateRule, year), stdTime, Qt::UTC); - stdData.atMSecsSinceEpoch = std.toMSecsSinceEpoch() - (dstZone.offset * 1000); - stdData.offsetFromUtc = stdZone.offset; - stdData.standardTimeOffset = stdZone.offset; - stdData.daylightTimeOffset = 0; - stdData.abbreviation = stdZone.name; - // Part of maxYear will overflow (likewise for minYear, below): - if (year == maxYear && (dstData.atMSecsSinceEpoch < 0 || stdData.atMSecsSinceEpoch < 0)) { - if (dstData.atMSecsSinceEpoch > 0) { - result << dstData; - } else if (stdData.atMSecsSinceEpoch > 0) { - result << stdData; + // Note: std and dst, despite being QDateTime(,, UTC), have the + // date() and time() of the *zone*'s description of the transition + // moments; the atMSecsSinceEpoch values computed from them are + // correctly offse to be UTC-based. + + // Transition to daylight-saving time: + QDateTime dst(calculatePosixDate(dstDateRule, year) + .startOfDay(QTimeZone::UTC).addSecs(dstTime)); + 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)); + 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 (saving.atMSecsSinceEpoch < standard.atMSecsSinceEpoch) { + if (dst <= QDate(year, 1, 1).startOfDay(QTimeZone::UTC) + && std >= QDate(year, 12, 31).endOfDay(QTimeZone::UTC)) { + // Permanent DST: + 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: + standard.atMSecsSinceEpoch = lastTranMSecs; + result.emplaceBack(std::move(standard)); + return result; + } } - } else if (year < 1970) { // We ignore DST before the epoch. - if (year > minYear || stdData.atMSecsSinceEpoch != QTimeZonePrivate::invalidMSecs()) - result << stdData; - } else if (dst < std) { - result << dstData << stdData; - } else { - result << stdData << dstData; + } + + 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.emplaceBack(std::move(saving)); + result.emplaceBack(std::move(standard)); + } else { + result.emplaceBack(std::move(standard)); + result.emplaceBack(std::move(saving)); + } + } else if (useStd) { + result.emplaceBack(std::move(standard)); + } else if (useDst) { + result.emplaceBack(std::move(saving)); } } return result; @@ -639,14 +761,8 @@ static QList<QTimeZonePrivate::Data> calculatePosixTransitions(const QByteArray // Create the system default time zone QTzTimeZonePrivate::QTzTimeZonePrivate() + : QTzTimeZonePrivate(staticSystemTimeZoneId()) { - init(systemTimeZoneId()); -} - -// Create a named time zone -QTzTimeZonePrivate::QTzTimeZonePrivate(const QByteArray &ianaId) -{ - init(ianaId); } QTzTimeZonePrivate::~QTzTimeZonePrivate() @@ -655,6 +771,9 @@ QTzTimeZonePrivate::~QTzTimeZonePrivate() QTzTimeZonePrivate *QTzTimeZonePrivate::clone() const { +#if QT_CONFIG(icu) + const auto lock = qt_scoped_lock(s_icu_mutex); +#endif return new QTzTimeZonePrivate(*this); } @@ -664,7 +783,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; }; @@ -678,29 +797,21 @@ 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(QLatin1String("/usr/share/zoneinfo/") + QString::fromLocal8Bit(ianaId)); - if (!tzif.open(QIODevice::ReadOnly)) { - tzif.setFileName(QLatin1String("/usr/lib/zoneinfo/") + QString::fromLocal8Bit(ianaId)); - if (!tzif.open(QIODevice::ReadOnly)) { - // ianaId may be a POSIX rule, taken from $TZ or /etc/TZ - const QByteArray zoneInfo = ianaId.split(',').at(0); - const char *begin = zoneInfo.constBegin(); - if (PosixZone::parse(begin, zoneInfo.constEnd()).hasValidOffset() - && (begin == zoneInfo.constEnd() - || PosixZone::parse(begin, zoneInfo.constEnd()).hasValidOffset())) { - 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); // Parse the old version block of data bool ok = false; + QByteArray posixRule; QTzHeader hdr = parseTzHeader(ds, &ok); if (!ok || ds.status() != QDataStream::Ok) return ret; @@ -741,14 +852,21 @@ QTzTimeZoneCacheEntry QTzTimeZoneCache::findEntry(const QByteArray &ianaId) typeList = parseTzIndicators(ds, typeList, hdr2.tzh_ttisstdcnt, hdr2.tzh_ttisgmtcnt); if (ds.status() != QDataStream::Ok) return ret; - ret.m_posixRule = parseTzPosixRule(ds); + posixRule = parseTzPosixRule(ds); if (ds.status() != QDataStream::Ok) return ret; } + // Translate the TZ file's raw data into our internal form: - // Translate the TZ file into internal format + if (!posixRule.isEmpty()) { + auto check = validatePosixRule(posixRule); + if (!check.isValid) // We got a POSIX rule, but it was malformed: + return ret; + ret.m_posixRule = posixRule; + ret.m_hasDst = check.hasDst; + } - // Translate the array index based tz_abbrind into list index + // Translate the array-index-based tz_abbrind into list index const int size = abbrevMap.size(); ret.m_abbreviations.clear(); ret.m_abbreviations.reserve(size); @@ -758,13 +876,19 @@ QTzTimeZoneCacheEntry QTzTimeZoneCache::findEntry(const QByteArray &ianaId) ret.m_abbreviations.append(it.value()); abbrindList.append(it.key()); } + // Map tz_abbrind from map's keys (as initially read) to abbrindList's + // indices (used hereafter): for (int i = 0; i < typeList.size(); ++i) typeList[i].tz_abbrind = abbrindList.indexOf(typeList.at(i).tz_abbrind); + // 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 }; + // 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 - int utcOffset = 0; - for (const QTzTransition &tran : qAsConst(tranList)) { + int utcOffset = ret.m_preZoneRule.stdOffset; + for (const QTzTransition &tran : std::as_const(tranList)) { if (!typeList.at(tran.tz_typeind).tz_isdst) { utcOffset = typeList.at(tran.tz_typeind).tz_gmtoff; break; @@ -772,7 +896,7 @@ QTzTimeZoneCacheEntry QTzTimeZoneCache::findEntry(const QByteArray &ianaId) } // Now for each transition time calculate and store our rule: - const int tranCount = tranList.count();; + const int tranCount = tranList.size(); ret.m_tranTimes.reserve(tranCount); // The DST offset when in effect: usually stable, usually an hour: int lastDstOff = 3600; @@ -829,8 +953,10 @@ QTzTimeZoneCacheEntry QTzTimeZoneCache::findEntry(const QByteArray &ianaId) // If the rule already exist then use that, otherwise add it int ruleIndex = ret.m_tranRules.indexOf(rule); 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; } @@ -852,15 +978,26 @@ 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; } -void QTzTimeZonePrivate::init(const QByteArray &ianaId) +// Create a named time zone +QTzTimeZonePrivate::QTzTimeZonePrivate(const QByteArray &ianaId) { + if (!isTimeZoneIdAvailable(ianaId)) // Avoid pointlessly creating cache entries + return; static QTzTimeZoneCache tzCache; - const auto &entry = tzCache.fetchEntry(ianaId); + auto entry = tzCache.fetchEntry(ianaId); if (entry.m_tranTimes.isEmpty() && entry.m_posixRule.isEmpty()) return; // Invalid after all ! @@ -883,9 +1020,9 @@ void QTzTimeZonePrivate::init(const QByteArray &ianaId) } } -QLocale::Country QTzTimeZonePrivate::country() const +QLocale::Territory QTzTimeZonePrivate::territory() const { - return tzZones->value(m_id).country; + return tzZones->value(m_id).territory; } QString QTzTimeZonePrivate::comment() const @@ -893,42 +1030,27 @@ 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) - 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); -#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 { + // TZ DB lacks localized names (it only has IANA IDs), so delegate to ICU + // for those, when available. #if QT_CONFIG(icu) - 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); + { + auto lock = qt_scoped_lock(s_icu_mutex); + // TODO Some valid TZ names are not valid ICU names, use translation table? + if (!m_icu) + m_icu = new QIcuTimeZonePrivate(m_id); + if (m_icu->isValid()) + return m_icu->displayName(timeType, nameType, locale); + } #else Q_UNUSED(timeType); Q_UNUSED(nameType); Q_UNUSED(locale); #endif - // If no ICU available then have to use abbreviations instead + // If ICU is unavailable, fall back to abbreviations. // Abbreviations don't have GenericTime if (timeType == QTimeZone::GenericTime) timeType = QTimeZone::StandardTime; @@ -1001,12 +1123,7 @@ int QTzTimeZonePrivate::daylightTimeOffset(qint64 atMSecsSinceEpoch) const bool QTzTimeZonePrivate::hasDaylightTime() const { - // TODO Perhaps cache as frequently accessed? - for (const QTzTransitionRule &rule : cached_data.m_tranRules) { - if (rule.dstOffset != 0) - return true; - } - return false; + return cached_data.m_hasDst; } bool QTzTimeZonePrivate::isDaylightTime(qint64 atMSecsSinceEpoch) const @@ -1016,19 +1133,19 @@ bool QTzTimeZonePrivate::isDaylightTime(qint64 atMSecsSinceEpoch) const QTimeZonePrivate::Data QTzTimeZonePrivate::dataForTzTransition(QTzTransitionTime tran) const { - QTimeZonePrivate::Data data; - data.atMSecsSinceEpoch = tran.atMSecsSinceEpoch; - QTzTransitionRule rule = cached_data.m_tranRules.at(tran.ruleIndex); - data.standardTimeOffset = rule.stdOffset; - data.daylightTimeOffset = rule.dstOffset; - data.offsetFromUtc = rule.stdOffset + rule.dstOffset; - data.abbreviation = QString::fromUtf8(cached_data.m_abbreviations.at(rule.abbreviationIndex)); - return data; + return dataFromRule(cached_data.m_tranRules.at(tran.ruleIndex), tran.atMSecsSinceEpoch); +} + +QTimeZonePrivate::Data QTzTimeZonePrivate::dataFromRule(QTzTransitionRule rule, + qint64 msecsSinceEpoch) const +{ + 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 { - const int year = QDateTime::fromMSecsSinceEpoch(msNear, Qt::UTC).date().year(); + const int year = QDateTime::fromMSecsSinceEpoch(msNear, QTimeZone::UTC).date().year(); // The Data::atMSecsSinceEpoch of the single entry if zone is constant: qint64 atTime = tranCache().isEmpty() ? msNear : tranCache().last().atMSecsSinceEpoch; return calculatePosixTransitions(cached_data.m_posixRule, year - 1, year + 1, atTime); @@ -1053,18 +1170,18 @@ 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) { return at.atMSecsSinceEpoch <= forMSecsSinceEpoch; }); - if (last > tranCache().cbegin()) - --last; - Data data = dataForTzTransition(*last); - data.atMSecsSinceEpoch = forMSecsSinceEpoch; - return data; + if (last == tranCache().cbegin()) + return dataFromRule(cached_data.m_preZoneRule, forMSecsSinceEpoch); + + --last; + return dataFromRule(cached_data.m_tranRules.at(last->ruleIndex), forMSecsSinceEpoch); } bool QTzTimeZonePrivate::hasTransitions() const @@ -1084,7 +1201,7 @@ 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: @@ -1092,7 +1209,7 @@ QTimeZonePrivate::Data QTzTimeZonePrivate::nextTransition(qint64 afterMSecsSince [afterMSecsSinceEpoch] (const 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 @@ -1109,7 +1226,7 @@ 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 @@ -1117,12 +1234,16 @@ QTimeZonePrivate::Data QTzTimeZonePrivate::previousTransition(qint64 beforeMSecs [beforeMSecsSinceEpoch] (const 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 @@ -1132,12 +1253,12 @@ QList<QByteArray> QTzTimeZonePrivate::availableTimeZoneIds() const return result; } -QList<QByteArray> QTzTimeZonePrivate::availableTimeZoneIds(QLocale::Country country) const +QList<QByteArray> QTzTimeZonePrivate::availableTimeZoneIds(QLocale::Territory territory) const { - // TODO AnyCountry + // TODO AnyTerritory QList<QByteArray> result; for (auto it = tzZones->cbegin(), end = tzZones->cend(); it != end; ++it) { - if (it.value().country == country) + if (it.value().territory == territory) result << it.key(); } std::sort(result.begin(), result.end()); @@ -1147,7 +1268,7 @@ QList<QByteArray> QTzTimeZonePrivate::availableTimeZoneIds(QLocale::Country coun // Getting the system zone's ID: namespace { -class ZoneNameReader : public QObject +class ZoneNameReader { public: QByteArray name() @@ -1169,8 +1290,11 @@ public: */ const StatIdent local = identify("/etc/localtime"); const StatIdent tz = identify("/etc/TZ"); - if (!m_name.isEmpty() && m_last.isValid() && (m_last == local || m_last == tz)) + const StatIdent timezone = identify("/etc/timezone"); + if (!m_name.isEmpty() && m_last.isValid() + && (m_last == local || m_last == tz || m_last == timezone)) { return m_name; + } m_name = etcLocalTime(); if (!m_name.isEmpty()) { @@ -1178,19 +1302,26 @@ public: return m_name; } - m_name = etcTZ(); - m_last = m_name.isEmpty() ? StatIdent() : tz; + // Some systems (e.g. uClibc) have a default value for $TZ in /etc/TZ: + m_name = etcContent(QStringLiteral("/etc/TZ")); + if (!m_name.isEmpty()) { + m_last = tz; + return m_name; + } + + // Gentoo still (2020, QTBUG-87326) uses this: + m_name = etcContent(QStringLiteral("/etc/timezone")); + m_last = m_name.isEmpty() ? StatIdent() : timezone; return m_name; } - private: QByteArray m_name; struct StatIdent { static constexpr unsigned long bad = ~0ul; unsigned long m_dev, m_ino; - StatIdent() : m_dev(bad), m_ino(bad) {} + 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) @@ -1208,7 +1339,8 @@ private: { // On most distros /etc/localtime is a symlink to a real file so extract // name from the path - const QLatin1String zoneinfo("/zoneinfo/"); + 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/ @@ -1216,18 +1348,22 @@ 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(); } - static QByteArray etcTZ() + static QByteArray etcContent(const QString &path) { - // Some systems (e.g. uClibc) have a default value for $TZ in /etc/TZ: - const QString path = QStringLiteral("/etc/TZ"); QFile zone(path); if (zone.open(QIODevice::ReadOnly)) return zone.readAll().trimmed(); @@ -1262,6 +1398,11 @@ private: QByteArray QTzTimeZonePrivate::systemTimeZoneId() const { + return staticSystemTimeZoneId(); +} + +QByteArray QTzTimeZonePrivate::staticSystemTimeZoneId() +{ // Check TZ env var first, if not populated try find it QByteArray ianaId = qgetenv("TZ"); @@ -1272,10 +1413,10 @@ QByteArray QTzTimeZonePrivate::systemTimeZoneId() const if (ianaId == ":/etc/localtime") ianaId.clear(); else if (ianaId.startsWith(':')) - ianaId = ianaId.mid(1); + ianaId = ianaId.sliced(1); if (ianaId.isEmpty()) { - thread_local static ZoneNameReader reader; + Q_CONSTINIT thread_local static ZoneNameReader reader; ianaId = reader.name(); } |