Revision control

Copy as Markdown

// 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
// Qt-Security score:critical reason:data-parser
#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/QDir>
#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
#include <sys/param.h> // to use MAXSYMLINKS constant
#endif
#include <unistd.h> // to use _SC_SYMLOOP_MAX constant
QT_BEGIN_NAMESPACE
using namespace Qt::StringLiterals;
/*
Private
tz file implementation
*/
struct QTzTimeZone {
// TODO: for zone1970.tab we'll need a set of territories:
QLocale::Territory territory = QLocale::AnyTerritory;
QByteArray comment;
};
// Define as a type as Q_GLOBAL_STATIC doesn't like it
typedef QHash<QByteArray, QTzTimeZone> QTzTimeZoneHash;
static bool isTzFile(const QString &name);
// Open a named file under the zone info directory:
static bool openZoneInfo(const QString &name, QFile *file)
{
// 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(zoneLib + name);
if (file->open(QIODevice::ReadOnly))
return true;
}
return false;
}
// 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; see also QTBUG-64941).
static QTzTimeZoneHash loadTzTimeZones()
{
QFile tzif;
if (!openZoneInfo("zone.tab"_L1, &tzif))
return QTzTimeZoneHash();
QTzTimeZoneHash zonesHash;
QByteArray line;
while (tzif.readLineInto(&line)) {
QByteArrayView text = QByteArrayView(line).trimmed();
if (text.isEmpty() || text.at(0) == '#') // Ignore empty or comment
continue;
// Data rows are tab-separated columns Region, Coordinates, ID, Optional Comments
int cut = text.indexOf('\t');
if (Q_LIKELY(cut > 0)) {
QTzTimeZone 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);
}
}
}
}
QString path = tzif.fileName();
const qsizetype cut = path.lastIndexOf(u'/');
Q_ASSERT(cut > 0);
path.truncate(cut + 1);
const qsizetype prefixLen = path.size();
for (const auto &info : QDirListing(path, QDirListing::IteratorFlag::Recursive)) {
if (!(info.isFile() || info.isSymLink()))
continue;
const QString infoAbsolutePath = info.absoluteFilePath();
const QString name = infoAbsolutePath.sliced(prefixLen);
// 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(infoAbsolutePath))
zonesHash.insert(id, QTzTimeZone());
}
return zonesHash;
}
// Hash of available system tz files as loaded by loadTzTimeZones()
Q_GLOBAL_STATIC(const QTzTimeZoneHash, tzZones, loadTzTimeZones());
/*
The following is copied and modified from tzfile.h which is in the public domain.
Copied as no compatibility guarantee and is never system installed.
*/
#define TZ_MAGIC "TZif"
#define TZ_MAX_TIMES 1200
#define TZ_MAX_TYPES 256 // Limited by what (unsigned char)'s can hold
#define TZ_MAX_CHARS 50 // Maximum number of abbreviation characters
#define TZ_MAX_LEAPS 50 // Maximum number of leap second corrections
struct QTzHeader {
char tzh_magic[4]; // TZ_MAGIC
char tzh_version; // '\0' or '2' as of 2005
char tzh_reserved[15]; // reserved--must be zero
quint32 tzh_ttisgmtcnt; // number of trans. time flags
quint32 tzh_ttisstdcnt; // number of trans. time flags
quint32 tzh_leapcnt; // number of leap seconds
quint32 tzh_timecnt; // number of transition times
quint32 tzh_typecnt; // number of local time types
quint32 tzh_charcnt; // number of abbr. chars
};
struct QTzTransition {
qint64 tz_time; // Transition time
quint8 tz_typeind; // Type Index
};
Q_DECLARE_TYPEINFO(QTzTransition, Q_PRIMITIVE_TYPE);
struct QTzType {
int tz_gmtoff; // UTC offset in seconds
bool tz_isdst; // Is DST
quint8 tz_abbrind; // abbreviation list index
};
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
static QTzHeader parseTzHeader(QDataStream &ds, bool *ok)
{
QTzHeader hdr;
quint8 ch;
*ok = false;
// Parse Magic, 4 bytes
ds.readRawData(hdr.tzh_magic, 4);
if (memcmp(hdr.tzh_magic, TZ_MAGIC, 4) != 0 || ds.status() != QDataStream::Ok)
return hdr;
// Parse Version, 1 byte, before 2005 was '\0', since 2005 a '2', since 2013 a '3'
ds >> ch;
hdr.tzh_version = ch;
if (ds.status() != QDataStream::Ok
|| (hdr.tzh_version != '2' && hdr.tzh_version != '\0' && hdr.tzh_version != '3')) {
return hdr;
}
// Parse reserved space, 15 bytes
ds.readRawData(hdr.tzh_reserved, 15);
if (ds.status() != QDataStream::Ok)
return hdr;
// Parse rest of header, 6 x 4-byte transition counts
ds >> hdr.tzh_ttisgmtcnt >> hdr.tzh_ttisstdcnt >> hdr.tzh_leapcnt >> hdr.tzh_timecnt
>> hdr.tzh_typecnt >> hdr.tzh_charcnt;
// Check defined maximums
if (ds.status() != QDataStream::Ok
|| hdr.tzh_timecnt > TZ_MAX_TIMES
|| hdr.tzh_typecnt > TZ_MAX_TYPES
|| hdr.tzh_charcnt > TZ_MAX_CHARS
|| hdr.tzh_leapcnt > TZ_MAX_LEAPS
|| hdr.tzh_ttisgmtcnt > hdr.tzh_typecnt
|| hdr.tzh_ttisstdcnt > hdr.tzh_typecnt) {
return hdr;
}
*ok = true;
return hdr;
}
static std::optional<QList<QTzTransition>>
parseTzTransitions(QDataStream &ds, int tzh_timecnt, int tzh_typecnt, bool longTran)
{
QList<QTzTransition> transitions(tzh_timecnt);
if (longTran) {
// Parse tzh_timecnt x 8-byte transition times
for (int i = 0; i < tzh_timecnt; ++i) {
ds >> transitions[i].tz_time;
if (ds.status() != QDataStream::Ok)
return {};
}
} else {
// Parse tzh_timecnt x 4-byte transition times
qint32 val;
for (int i = 0; i < tzh_timecnt; ++i) {
ds >> val;
transitions[i].tz_time = val;
if (ds.status() != QDataStream::Ok)
return {};
}
}
// Parse tzh_timecnt x 1-byte transition type index
for (int i = 0; i < tzh_timecnt; ++i) {
quint8 typeind;
ds >> typeind;
if (ds.status() != QDataStream::Ok)
return {};
if (typeind >= tzh_typecnt)
return {}; // Malformed: transition names an undefined type.
transitions[i].tz_typeind = typeind;
}
return transitions;
}
static QList<QTzType> parseTzTypes(QDataStream &ds, int tzh_typecnt)
{
QList<QTzType> types(tzh_typecnt);
// Parse tzh_typecnt x transition types
for (int i = 0; i < tzh_typecnt && ds.status() == QDataStream::Ok; ++i) {
QTzType &type = types[i];
// Parse UTC Offset, 4 bytes
ds >> type.tz_gmtoff;
// Parse Is DST flag, 1 byte
if (ds.status() == QDataStream::Ok)
ds >> type.tz_isdst;
// Parse Abbreviation Array Index, 1 byte
if (ds.status() == QDataStream::Ok)
ds >> type.tz_abbrind;
if (ds.status() != QDataStream::Ok)
types.resize(i);
}
return types;
}
static QMap<int, QByteArray> parseTzAbbreviations(QDataStream &ds, int tzh_charcnt, const QList<QTzType> &types)
{
// Parse the abbreviation list which is tzh_charcnt long with '\0' separated strings. The
// QTzType.tz_abbrind index points to the first char of the abbreviation in the array, not the
// occurrence in the list. It can also point to a partial string so we need to use the actual typeList
// index values when parsing. By using a map with tz_abbrind as ordered key we get both index
// methods in one data structure and can convert the types afterwards.
QMap<int, QByteArray> map;
quint8 ch;
QByteArray input;
// First parse the full abbrev string
for (int i = 0; i < tzh_charcnt && ds.status() == QDataStream::Ok; ++i) {
ds >> ch;
if (ds.status() == QDataStream::Ok)
input.append(char(ch));
else
return map;
}
// Then extract all the substrings pointed to by types
for (const QTzType &type : types) {
const qsizetype end = type.tz_abbrind < input.size()
? input.indexOf('\0', type.tz_abbrind) : -1;
if (end < 0)
return QMap<int, QByteArray>();
// Have reached end of an abbreviation, so add to map
map[type.tz_abbrind] = input.sliced(type.tz_abbrind, end - type.tz_abbrind);
}
return map;
}
static void parseTzLeapSeconds(QDataStream &ds, int tzh_leapcnt, bool longTran)
{
// Parse tzh_leapcnt x pairs of leap seconds
// We don't use leap seconds, so only read and don't store
qint32 val;
if (longTran) {
// v2 file format, each entry is 12 bytes long
qint64 time;
for (int i = 0; i < tzh_leapcnt && ds.status() == QDataStream::Ok; ++i) {
// Parse Leap Occurrence Time, 8 bytes
ds >> time;
// Parse Leap Seconds To Apply, 4 bytes
if (ds.status() == QDataStream::Ok)
ds >> val;
}
} else {
// v0 file format, each entry is 8 bytes long
for (int i = 0; i < tzh_leapcnt && ds.status() == QDataStream::Ok; ++i) {
// Parse Leap Occurrence Time, 4 bytes
ds >> val;
// Parse Leap Seconds To Apply, 4 bytes
if (ds.status() == QDataStream::Ok)
ds >> val;
}
}
}
static QList<QTzType> parseTzIndicators(QDataStream &ds, const QList<QTzType> &types, int tzh_ttisstdcnt,
int tzh_ttisgmtcnt)
{
QList<QTzType> result = types;
bool temp;
/*
Scan and discard indicators.
These indicators are only of use (by the date program) when "handling
POSIX-style time zone environment variables". The flags here say whether
the *specification* of the zone gave the time in UTC, local standard time
or local wall time; but whatever was specified has been digested for us,
already, by the zone-info compiler (zic), so that the tz_time values read
from the file (by parseTzTransitions) are all in UTC.
*/
// Scan tzh_ttisstdcnt x 1-byte standard/wall indicators
for (int i = 0; i < tzh_ttisstdcnt && ds.status() == QDataStream::Ok; ++i)
ds >> temp;
// Scan tzh_ttisgmtcnt x 1-byte UTC/local indicators
for (int i = 0; i < tzh_ttisgmtcnt && ds.status() == QDataStream::Ok; ++i)
ds >> temp;
return result;
}
static QByteArray parseTzPosixRule(QDataStream &ds)
{
// Parse POSIX rule, variable length '\n' enclosed
QByteArray rule;
quint8 ch;
ds >> ch;
if (ch != '\n' || ds.status() != QDataStream::Ok)
return rule;
ds >> ch;
while (ch != '\n' && ds.status() == QDataStream::Ok) {
rule.append((char)ch);
ds >> ch;
}
return rule;
}
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)
date = date.addDays(dayOfWeek - startDow - 7);
else
date = date.addDays(dayOfWeek - startDow);
date = date.addDays(week * 7);
while (date.month() != month)
date = date.addDays(-7);
return date;
}
static QDate calculatePosixDate(QLatin1StringView dateRule, int year)
{
if (dateRule.isEmpty())
return QDate();
bool ok;
// Can start with M, J, or a digit
if (dateRule.at(0) == 'M') {
// nth week in month format "Mmonth.week.dow"
const auto dateParts = dateRule.tokenize(u'.');
auto token = dateParts.begin();
Q_ASSERT(token != dateParts.end());
Q_ASSERT(!token->isEmpty()); // the 'M' is its [0].
const int month = token->sliced(1).toInt(&ok);
if (ok && ++token != dateParts.end()) {
const int week = token->toInt(&ok);
if (ok && ++token != dateParts.end()) {
const int dow = token->toInt(&ok);
if (ok)
return calculateDowDate(year, month, dow, week);
}
}
} else if (dateRule.at(0) == 'J') {
// Day of Year 1...365, ignores Feb 29.
// So March always starts on day 60.
int doy = 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 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
static int parsePosixTime(const char *begin, const char *end)
{
// Format "hh[:mm[:ss]]"
int hour, min = 0, sec = 0;
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 += r.used;
if (begin < end && *begin == ':') {
// minutes
++begin;
r = qstrntoll(begin, end - begin, 10);
min = r.result;
if (!r.ok() || min < 0 || min > 59 || r.used > 2)
return INT_MIN;
begin += r.used;
if (begin < end && *begin == ':') {
// seconds
++begin;
r = qstrntoll(begin, end - begin, 10);
sec = r.result;
if (!r.ok() || sec < 0 || sec > 59 || r.used > 2)
return INT_MIN;
begin += r.used;
}
}
// we must have consumed everything
if (begin != end)
return INT_MIN;
return (hour * 60 + min) * 60 + sec;
}
static int parsePosixTransitionTime(QLatin1StringView timeRule)
{
return parsePosixTime(timeRule.begin(), timeRule.end());
}
static int parsePosixOffset(const char *begin, const char *end)
{
// Format "[+|-]hh[:mm[:ss]]"
// note that the sign is inverted because POSIX counts in hours West of GMT
bool negate = true;
if (*begin == '+') {
++begin;
} else if (*begin == '-') {
negate = false;
++begin;
}
// POSIX says hour is at most 24, minutes and seconds (when present) at most 59.
// We here enforce the stronger condition that the offset is at most 24 hours.
// (This is weaker than QTZ's M(ax|in)UtcOffsetSecs but asserted by QDT.)
int value = parsePosixTime(begin, end);
using namespace QtPrivate::DateTimeConstants;
if (value < -SECS_PER_DAY || value > SECS_PER_DAY)
return INT_MIN;
return negate ? -value : value;
}
static inline bool asciiIsLetter(char ch)
{
ch |= 0x20; // lowercases if it is a letter, otherwise just corrupts ch
return ch >= 'a' && ch <= 'z';
}
// The characters POSIX allows in a <>-quoted zone name:
static inline bool asciiIsQuotedNameChar(char ch)
{
return asciiIsLetter(ch) || (ch >= '0' && ch <= '9') || ch == '+' || ch == '-';
}
namespace {
struct PosixZone // TODO: QTBUG-112006 - make this cross-platform.
{
enum {
InvalidOffset = INT_MIN,
};
QString name;
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 parse(const char *&pos, const char *end);
};
} // unnamed namespace
// Returns the zone name, the offset (in seconds) and advances \a begin to
// where the parsing ended. Returns a zone of INT_MIN in case an offset
// couldn't be read.
PosixZone PosixZone::parse(const char *&pos, const char *end)
{
static const char offsetChars[] = "0123456789:";
const char *nameBegin = pos;
const char *nameEnd;
Q_ASSERT(pos < end);
if (*pos == '<') {
++nameBegin; // skip the '<'
nameEnd = nameBegin;
while (nameEnd < end && asciiIsQuotedNameChar(*nameEnd))
++nameEnd;
if (nameEnd == end || *nameEnd != '>') {
// Unterminated '<':
pos = end;
return {};
}
pos = nameEnd + 1; // skip the '>'
} else {
nameEnd = nameBegin;
while (nameEnd < end && asciiIsLetter(*nameEnd))
++nameEnd;
pos = nameEnd;
}
if (nameEnd - nameBegin < 3)
return {}; // name must be at least 3 characters long
// zone offset, form [+-]hh:mm:ss
const char *zoneBegin = pos;
const char *zoneEnd = pos;
// Sign is optional (implicitly +):
if (zoneEnd < end && (zoneEnd[0] == '+' || zoneEnd[0] == '-'))
++zoneEnd;
while (zoneEnd < end) {
if (strchr(offsetChars, char(*zoneEnd)) == nullptr)
break;
++zoneEnd;
}
QString name = QString::fromUtf8(nameBegin, nameEnd - nameBegin);
const int offset = zoneEnd > zoneBegin ? parsePosixOffset(zoneBegin, zoneEnd) : InvalidOffset;
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 =="UTC"_L1 || name == "GMT"_L1))
return {};
return {std::move(name), offset};
}
/* 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(QByteArrayView posixRule, bool requireOffset = false)
{
// Format is described here:
// See also calculatePosixTransition()'s reference.
QLatin1StringView zoneinfo, startDate, endDate;
int parts = 1;
{
const auto tokens = QLatin1StringView(posixRule).tokenize(u',');
auto token = tokens.begin();
Q_ASSERT(token != tokens.end());
zoneinfo = token->trimmed();
if (++token != tokens.end()) {
++parts;
startDate = *token;
if (++token != tokens.end()) {
++parts;
endDate = *token;
if (++token != tokens.end())
++parts; // So we know if we have too many
}
}
}
const struct { bool isValid, hasDst; } fail{false, false}, good{true, parts > 1};
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 != 3 || startDate.isEmpty() || endDate.isEmpty())
return fail;
for (int i = 0; i < 2; ++i) {
const auto tokens = (i ? endDate : startDate).tokenize(u'/');
auto token = tokens.begin();
Q_ASSERT(token != tokens.end());
if (!calculatePosixDate(*token, 1972).isValid())
return fail;
if (++token != tokens.end() && parsePosixTime(token->begin(), token->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;
// POSIX Format is like "TZ=CST6CDT,M3.2.0/2:00:00,M11.1.0/2:00:00"
// i.e. "std offset dst [offset],start[/time],end[/time]"
// See the section about TZ at the end of
// and the link in validatePosixRule(), above.
const auto tokens = QLatin1String(posixRule).tokenize(u',');
auto token = tokens.begin();
Q_ASSERT(token != tokens.end());
PosixZone stdZone, dstZone;
QLatin1StringView zoneText = *token;
{
QLatin1StringView zoneinfo = zoneText.trimmed();
const char *begin = zoneinfo.begin();
stdZone = PosixZone::parse(begin, zoneinfo.end());
if (!stdZone.hasValidOffset()) {
stdZone.offset = 0; // reset to UTC if we failed to parse
} else if (begin < zoneinfo.end()) {
dstZone = PosixZone::parse(begin, zoneinfo.end());
if (!dstZone.hasValidOffset()) {
// if the dst offset isn't provided, it is 1 hour ahead of the standard offset
dstZone.offset = stdZone.offset + (60 * 60);
}
}
}
// If only the name part, or no DST specified, then no transitions
if (++token == tokens.end() || !dstZone.hasValidOffset()) {
result.emplaceBack(
stdZone.name.isEmpty() ? QString(zoneText) : stdZone.name,
lastTranMSecs, stdZone.offset, stdZone.offset);
return result;
}
QLatin1StringView dstRule = *token;
if (++token == tokens.end() || dstRule.isEmpty() || token->isEmpty())
return result; // Malformed.
QLatin1StringView stdRule = *token;
// Get the std to dst transition details
const int twoOClock = 7200; // Default transition time, when none specified
const auto dstParts = dstRule.tokenize(u'/');
auto subtok = dstParts.begin();
Q_ASSERT(subtok != dstParts.end());
QLatin1StringView dstDateRule = *subtok;
const int dstTime = ++subtok == dstParts.end() ? twoOClock : parsePosixTransitionTime(*subtok);
// Get the dst to std transition details
const auto stdParts = stdRule.tokenize(u'/');
subtok = stdParts.begin();
Q_ASSERT(subtok != stdParts.end());
QLatin1StringView stdDateRule = *subtok;
const int stdTime = ++subtok == stdParts.end() ? twoOClock : parsePosixTransitionTime(*subtok);
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);
const int maxYear = int(QDateTime::YearRange::Last);
startYear = qBound(minYear, startYear, maxYear);
endYear = qBound(minYear, endYear, maxYear);
Q_ASSERT(startYear <= endYear);
for (int year = startYear; year <= endYear; ++year) {
// 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;
}
}
}
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;
}
// Create the system default time zone
QTzTimeZonePrivate::QTzTimeZonePrivate()
: QTzTimeZonePrivate(staticSystemTimeZoneId())
{
}
QTzTimeZonePrivate::~QTzTimeZonePrivate()
{
}
QTzTimeZonePrivate *QTzTimeZonePrivate::clone() const
{
return new QTzTimeZonePrivate(*this);
}
class QTzTimeZoneCache
{
public:
QTzTimeZoneCacheEntry fetchEntry(const QByteArray &ianaId);
// Set max cost to hold whole IANA DB, since some code iterates it !
QTzTimeZoneCache() : m_cache(700) {}
private:
static QTzTimeZoneCacheEntry findEntry(const QByteArray &ianaId);
QCache<QByteArray, QTzTimeZoneCacheEntry> m_cache;
QMutex m_mutex;
};
QTzTimeZoneCacheEntry QTzTimeZoneCache::findEntry(const QByteArray &ianaId)
{
QTzTimeZoneCacheEntry ret;
QFile tzif;
if (ianaId.isEmpty()) {
// Open system tz
tzif.setFileName(QStringLiteral("/etc/localtime"));
if (!tzif.open(QIODevice::ReadOnly))
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;
auto trans = parseTzTransitions(ds, hdr.tzh_timecnt, hdr.tzh_typecnt, false);
if (!trans)
return ret;
Q_ASSERT(ds.status() == QDataStream::Ok);
QList<QTzTransition> tranList = std::move(*trans);
QList<QTzType> typeList = parseTzTypes(ds, hdr.tzh_typecnt);
if (ds.status() != QDataStream::Ok)
return ret;
QMap<int, QByteArray> abbrevMap = parseTzAbbreviations(ds, hdr.tzh_charcnt, typeList);
if (ds.status() != QDataStream::Ok)
return ret;
parseTzLeapSeconds(ds, hdr.tzh_leapcnt, false);
if (ds.status() != QDataStream::Ok)
return ret;
typeList = parseTzIndicators(ds, typeList, hdr.tzh_ttisstdcnt, hdr.tzh_ttisgmtcnt);
if (ds.status() != QDataStream::Ok)
return ret;
// If version 2 then parse the second block of data
if (hdr.tzh_version == '2' || hdr.tzh_version == '3') {
ok = false;
QTzHeader hdr2 = parseTzHeader(ds, &ok);
if (!ok || ds.status() != QDataStream::Ok)
return ret;
trans = parseTzTransitions(ds, hdr2.tzh_timecnt, hdr2.tzh_typecnt, true);
if (!trans)
return ret;
Q_ASSERT(ds.status() == QDataStream::Ok);
tranList = std::move(*trans);
typeList = parseTzTypes(ds, hdr2.tzh_typecnt);
if (ds.status() != QDataStream::Ok)
return ret;
abbrevMap = parseTzAbbreviations(ds, hdr2.tzh_charcnt, typeList);
if (ds.status() != QDataStream::Ok)
return ret;
parseTzLeapSeconds(ds, hdr2.tzh_leapcnt, true);
if (ds.status() != QDataStream::Ok)
return ret;
typeList = parseTzIndicators(ds, typeList, hdr2.tzh_ttisstdcnt, hdr2.tzh_ttisgmtcnt);
if (ds.status() != QDataStream::Ok)
return ret;
posixRule = parseTzPosixRule(ds);
if (ds.status() != QDataStream::Ok)
return ret;
}
// Translate the TZ file's raw data into our internal form:
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
const int size = abbrevMap.size();
ret.m_abbreviations.clear();
ret.m_abbreviations.reserve(size);
QList<int> abbrindList;
abbrindList.reserve(size);
for (auto it = abbrevMap.cbegin(), end = abbrevMap.cend(); it != end; ++it) {
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 = 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;
}
}
// Now for each transition time calculate and store our rule:
const int tranCount = tranList.size();
ret.m_tranTimes.reserve(tranCount);
// The DST offset when in effect: usually stable, usually an hour:
int lastDstOff = 3600;
for (int i = 0; i < tranCount; i++) {
const QTzTransition &tz_tran = tranList.at(i);
QTzTransitionTime tran;
QTzTransitionRule rule;
const QTzType tz_type = typeList.at(tz_tran.tz_typeind);
// Calculate the associated Rule
if (!tz_type.tz_isdst) {
utcOffset = tz_type.tz_gmtoff;
} else if (Q_UNLIKELY(tz_type.tz_gmtoff != utcOffset + lastDstOff)) {
/*
This might be a genuine change in DST offset, but could also be
DST starting at the same time as the standard offset changed. See
if DST's end gives a more plausible utcOffset (i.e. one closer to
the last we saw, or a simple whole hour):
*/
// Standard offset inferred from net offset and expected DST offset:
const int inferStd = tz_type.tz_gmtoff - lastDstOff; // != utcOffset
for (int j = i + 1; j < tranCount; j++) {
const QTzType new_type = typeList.at(tranList.at(j).tz_typeind);
if (!new_type.tz_isdst) {
const int newUtc = new_type.tz_gmtoff;
if (newUtc == utcOffset) {
// DST-end can't help us, avoid lots of messy checks.
// else: See if the end matches the familiar DST offset:
} else if (newUtc == inferStd) {
utcOffset = newUtc;
// else: let either end shift us to one hour as DST offset:
} else if (tz_type.tz_gmtoff - 3600 == utcOffset) {
// Start does it
} else if (tz_type.tz_gmtoff - 3600 == newUtc) {
utcOffset = newUtc; // End does it
// else: prefer whichever end gives DST offset closer to
// last, but consider any offset > 0 "closer" than any <= 0:
} else if (newUtc < tz_type.tz_gmtoff
? (utcOffset >= tz_type.tz_gmtoff
|| qAbs(newUtc - inferStd) < qAbs(utcOffset - inferStd))
: (utcOffset >= tz_type.tz_gmtoff
&& qAbs(newUtc - inferStd) < qAbs(utcOffset - inferStd))) {
utcOffset = newUtc;
}
break;
}
}
lastDstOff = tz_type.tz_gmtoff - utcOffset;
}
rule.stdOffset = utcOffset;
rule.dstOffset = tz_type.tz_gmtoff - utcOffset;
rule.abbreviationIndex = tz_type.tz_abbrind;
// 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);
} else {
tran.ruleIndex = ruleIndex;
}
if (qMulOverflow<1000>(tz_tran.tz_time, &tran.atMSecsSinceEpoch)) {
ret.m_tranTimes.clear();
ret.m_posixRule.clear();
break;
}
ret.m_tranTimes.append(tran);
}
return ret;
}
QTzTimeZoneCacheEntry QTzTimeZoneCache::fetchEntry(const QByteArray &ianaId)
{
QMutexLocker locker(&m_mutex);
// search the cache...
QTzTimeZoneCacheEntry *obj = m_cache.object(ianaId);
if (obj)
return *obj;
// ... or build a new entry from scratch
locker.unlock(); // don't parse files under mutex lock
QTzTimeZoneCacheEntry ret = findEntry(ianaId);
if (ret.m_tranTimes.isEmpty() && ret.m_posixRule.isEmpty())
return ret; // Don't use up cache space with invalid IDs.
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())
return; // Invalid after all !
cached_data = std::move(entry);
m_id = ianaId;
// Avoid empty ID, if we have an abbreviation to use instead
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.
// TODO: use CLDR generic abbreviation for the zone.
m_id = abbreviation(QDateTime::currentMSecsSinceEpoch()).toUtf8();
}
}
QLocale::Territory QTzTimeZonePrivate::territory() const
{
return tzZones->value(m_id).territory;
}
QString QTzTimeZonePrivate::comment() const
{
return QString::fromUtf8(tzZones->value(m_id).comment);
}
QString QTzTimeZonePrivate::displayName(QTimeZone::TimeType timeType,
QTimeZone::NameType nameType,
const QLocale &locale) const
{
// TZ only provides C-locale abbreviations and offset:
if (nameType != QTimeZone::LongName && isDataLocale(locale)) {
Data tran = data(timeType);
if (tran.atMSecsSinceEpoch != invalidMSecs()) {
if (nameType == QTimeZone::ShortName)
return tran.abbreviation;
// Save base class repeating the data(timeType) query:
if (isAnglicLocale(locale))
return isoOffsetFormat(tran.offsetFromUtc);
}
}
// Otherwise, fall back to base class (and qtimezonelocale.cpp):
return QTimeZonePrivate::displayName(timeType, nameType, locale);
}
QString QTzTimeZonePrivate::abbreviation(qint64 atMSecsSinceEpoch) const
{
return data(atMSecsSinceEpoch).abbreviation;
}
int QTzTimeZonePrivate::offsetFromUtc(qint64 atMSecsSinceEpoch) const
{
const Data tran = data(atMSecsSinceEpoch);
return tran.offsetFromUtc; // == tran.standardTimeOffset + tran.daylightTimeOffset
}
int QTzTimeZonePrivate::standardTimeOffset(qint64 atMSecsSinceEpoch) const
{
return data(atMSecsSinceEpoch).standardTimeOffset;
}
int QTzTimeZonePrivate::daylightTimeOffset(qint64 atMSecsSinceEpoch) const
{
return data(atMSecsSinceEpoch).daylightTimeOffset;
}
bool QTzTimeZonePrivate::hasDaylightTime() const
{
return cached_data.m_hasDst;
}
bool QTzTimeZonePrivate::isDaylightTime(qint64 atMSecsSinceEpoch) const
{
return (daylightTimeOffset(atMSecsSinceEpoch) != 0);
}
QTimeZonePrivate::Data QTzTimeZonePrivate::dataForTzTransition(QTzTransitionTime tran) const
{
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, 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);
}
QTimeZonePrivate::Data QTzTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const
{
// If the required time is after the last transition (or there were none)
// and we have a POSIX rule, then use it:
if (!cached_data.m_posixRule.isEmpty()
&& (tranCache().isEmpty() || tranCache().last().atMSecsSinceEpoch < forMSecsSinceEpoch)) {
QList<Data> posixTrans = getPosixTransitions(forMSecsSinceEpoch);
auto it = std::partition_point(posixTrans.cbegin(), posixTrans.cend(),
[forMSecsSinceEpoch] (const Data &at) {
return at.atMSecsSinceEpoch <= forMSecsSinceEpoch;
});
// Use most recent, if any in the past; or the first if we have no other rules:
if (it > posixTrans.cbegin() || (tranCache().isEmpty() && it < posixTrans.cend())) {
Data data = *(it > posixTrans.cbegin() ? it - 1 : it);
data.atMSecsSinceEpoch = forMSecsSinceEpoch;
return data;
}
}
if (tranCache().isEmpty()) // Only possible if !isValid()
return {};
// Otherwise, use the rule for the most recent or first transition:
auto last = std::partition_point(tranCache().cbegin(), tranCache().cend(),
[forMSecsSinceEpoch] (QTzTransitionTime at) {
return at.atMSecsSinceEpoch <= forMSecsSinceEpoch;
});
if (last == tranCache().cbegin())
return dataFromRule(cached_data.m_preZoneRule, forMSecsSinceEpoch);
--last;
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 Data &tran) {
return tran.atMSecsSinceEpoch != invalidMSecs()
&& ((timeType == QTimeZone::DaylightTime) != (tran.daylightTimeOffset == 0));
};
// Get current tran, use if suitable:
const qint64 currentMSecs = QDateTime::currentMSecsSinceEpoch();
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 en-Latn-* / C locale names:
return isAnglicLocale(locale);
}
bool QTzTimeZonePrivate::hasTransitions() const
{
return true;
}
QTimeZonePrivate::Data QTzTimeZonePrivate::nextTransition(qint64 afterMSecsSinceEpoch) const
{
// If the required time is after the last transition (or there were none)
// and we have a POSIX rule, then use it:
if (!cached_data.m_posixRule.isEmpty()
&& (tranCache().isEmpty() || tranCache().last().atMSecsSinceEpoch < afterMSecsSinceEpoch)) {
QList<Data> posixTrans = getPosixTransitions(afterMSecsSinceEpoch);
auto it = std::partition_point(posixTrans.cbegin(), posixTrans.cend(),
[afterMSecsSinceEpoch] (const Data &at) {
return at.atMSecsSinceEpoch <= afterMSecsSinceEpoch;
});
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] (QTzTransitionTime at) {
return at.atMSecsSinceEpoch <= afterMSecsSinceEpoch;
});
return last != tranCache().cend() ? dataForTzTransition(*last) : Data{};
}
QTimeZonePrivate::Data QTzTimeZonePrivate::previousTransition(qint64 beforeMSecsSinceEpoch) const
{
// If the required time is after the last transition (or there were none)
// and we have a POSIX rule, then use it:
if (!cached_data.m_posixRule.isEmpty()
&& (tranCache().isEmpty() || tranCache().last().atMSecsSinceEpoch < beforeMSecsSinceEpoch)) {
QList<Data> posixTrans = getPosixTransitions(beforeMSecsSinceEpoch);
auto it = std::partition_point(posixTrans.cbegin(), posixTrans.cend(),
[beforeMSecsSinceEpoch] (const Data &at) {
return at.atMSecsSinceEpoch < beforeMSecsSinceEpoch;
});
if (it > posixTrans.cbegin())
return *--it;
// It fell between the last transition (if any) and the first of the POSIX rule:
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] (QTzTransitionTime at) {
return at.atMSecsSinceEpoch < beforeMSecsSinceEpoch;
});
return last > tranCache().cbegin() ? dataForTzTransition(*--last) : Data{};
}
bool QTzTimeZonePrivate::isTimeZoneIdAvailable(QByteArrayView ianaId) const
{
// 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
{
return uniqueSortedAliasPadded(tzZones->keys());
}
QList<QByteArray> QTzTimeZonePrivate::availableTimeZoneIds(QLocale::Territory territory) const
{
QList<QByteArray> result;
for (auto it = tzZones->cbegin(), end = tzZones->cend(); it != end; ++it) {
if (it.value().territory == territory)
result << it.key();
// We'll pick up any CLDR-standard names below, so don't try to map here.
}
std::sort(result.begin(), result.end());
// Since zone.tab only knows about one territory per zone, and is somewhat
// incomplete, we may well miss some zones that CLDR associates with the
// territory. So merge with those from CLDR that we do support.
const auto unWantedZone = [territory](QByteArrayView id) {
// We only want to add zones if they are known and we don't already have them:
auto it = tzZones->constFind(id);
return it == tzZones->end() || it->territory == territory;
};
QList<QByteArrayView> cldrViews = matchingTimeZoneIds(territory);
std::sort(cldrViews.begin(), cldrViews.end());
const auto uniqueEnd = std::unique(cldrViews.begin(), cldrViews.end());
const auto prunedEnd = std::remove_if(cldrViews.begin(), uniqueEnd, unWantedZone);
const auto cldrSize = std::distance(cldrViews.begin(), prunedEnd);
if (cldrSize) {
QList<QByteArray> cldrList;
cldrList.reserve(cldrSize);
for (auto it = cldrViews.begin(); it != prunedEnd; ++it)
cldrList.emplace_back(it->toByteArray());
QList<QByteArray> joined;
joined.reserve(result.size() + cldrSize);
std::set_union(result.begin(), result.end(), cldrList.begin(), cldrList.end(),
std::back_inserter(joined));
result = joined;
}
return result;
}
// Getting the system zone's ID:
namespace {
class ZoneNameReader
{
public:
QByteArray name()
{
/* Assumptions:
a) Systems don't change which of localtime and TZ they use without a
reboot.
b) When they change, they use atomic renames, hence a new device and
inode for the new file.
c) If we change which *name* is used for a zone, while referencing
the same final zoneinfo file, we don't care about the change of
name (e.g. if Europe/Oslo and Europe/Berlin are both symlinks to
the same CET file, continuing to use the old name, after
/etc/localtime changes which of the two it points to, is
harmless).
The alternative would be to use a file-system watcher, but they are a
scarce resource.
*/
const StatIdent local = identify("/etc/localtime");
const StatIdent tz = identify("/etc/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()) {
m_last = local;
return m_name;
}
// 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;
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; }
friend constexpr bool operator==(StatIdent lhs, StatIdent rhs)
{ return lhs.m_dev == rhs.m_dev && lhs.m_ino == rhs.m_ino; }
};
StatIdent m_last;
static StatIdent identify(const char *path)
{
QT_STATBUF data;
return QT_STAT(path, &data) == -1 ? StatIdent() : StatIdent(data);
}
static QByteArray etcLocalTime()
{
// On most distros /etc/localtime is a symlink to a real file so extract
// name from the path
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/
// We stop on the first path under /zoneinfo/, even if it is itself a
// symlink, like America/Montreal pointing to America/Toronto
do {
path = QFile::symLinkTarget(path);
// 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 etcContent(const QString &path)
{
QFile zone(path);
if (zone.open(QIODevice::ReadOnly))
return zone.readAll().trimmed();
return QByteArray();
}
// Any chain of symlinks longer than this is assumed to be a loop:
static long getSymloopMax()
{
#ifdef SYMLOOP_MAX
// If defined, at runtime it can only be greater than this, so this is a safe bet:
return SYMLOOP_MAX;
#else
errno = 0;
long result = sysconf(_SC_SYMLOOP_MAX);
if (result >= 0)
return result;
// result is -1, meaning either error or no limit
Q_ASSERT(!errno); // ... but it can't be an error, POSIX mandates _SC_SYMLOOP_MAX
// therefore we can make up our own limit
# ifdef MAXSYMLINKS
return MAXSYMLINKS;
# else
return 8;
# endif
#endif
}
};
}
QByteArray QTzTimeZonePrivate::systemTimeZoneId() const
{
return staticSystemTimeZoneId();
}
QByteArray QTzTimeZonePrivate::staticSystemTimeZoneId()
{
// Check TZ env var first, if not populated try find it
QByteArray ianaId = qgetenv("TZ");
// The TZ value can be ":/etc/localtime" which libc considers
// to be a "default timezone", in which case it will be read
// by one of the blocks below, so unset it here so it is not
// considered as a valid/found ianaId
if (ianaId == ":/etc/localtime")
ianaId.clear();
else if (ianaId.startsWith(':'))
ianaId = ianaId.sliced(1);
if (ianaId.isEmpty()) {
Q_CONSTINIT thread_local static ZoneNameReader reader;
ianaId = reader.name();
}
return ianaId;
}
QT_END_NAMESPACE