Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qtimezoneprivate_tz.cpp
Go to the documentation of this file.
1// Copyright (C) 2022 The Qt Company Ltd.
2// Copyright (C) 2019 Crimson AS <info@crimson.no>
3// Copyright (C) 2013 John Layt <jlayt@kde.org>
4// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
5// Qt-Security score:critical reason:data-parser
6
7#include "qtimezone.h"
9#include "private/qlocale_tools_p.h"
10#include "private/qlocking_p.h"
11
12#include <QtCore/QDataStream>
13#include <QtCore/QDateTime>
14#include <QtCore/QDirListing>
15#include <QtCore/QDir>
16#include <QtCore/QFile>
17#include <QtCore/QCache>
18#include <QtCore/QMap>
19#include <QtCore/QMutex>
20
21#include <qdebug.h>
22#include <qplatformdefs.h>
23
24#include <algorithm>
25#include <memory>
26
27#include <errno.h>
28#include <limits.h>
29#ifndef Q_OS_INTEGRITY
30#include <sys/param.h> // to use MAXSYMLINKS constant
31#endif
32#include <unistd.h> // to use _SC_SYMLOOP_MAX constant
33
34QT_BEGIN_NAMESPACE
35
36using namespace Qt::StringLiterals;
37
38/*
39 Private
40
41 tz file implementation
42*/
43
45 // TODO: for zone1970.tab we'll need a set of territories:
48};
49
50// Define as a type as Q_GLOBAL_STATIC doesn't like it
52
53static bool isTzFile(const QString &name);
54
55// Open a named file under the zone info directory:
56static bool openZoneInfo(const QString &name, QFile *file)
57{
58 // At least on Linux / glibc (see man 3 tzset), $TZDIR overrides the system
59 // default location for zone info:
60 const QString tzdir = qEnvironmentVariable("TZDIR");
61 if (!tzdir.isEmpty()) {
62 file->setFileName(QDir(tzdir).filePath(name));
63 if (file->open(QIODevice::ReadOnly))
64 return true;
65 }
66 // Try modern system path first:
67 constexpr auto zoneShare = "/usr/share/zoneinfo/"_L1;
68 if (tzdir != zoneShare && tzdir != zoneShare.chopped(1)) {
69 file->setFileName(zoneShare + name);
70 if (file->open(QIODevice::ReadOnly))
71 return true;
72 }
73 // Fall back to legacy system path:
74 constexpr auto zoneLib = "/usr/lib/zoneinfo/"_L1;
75 if (tzdir != zoneLib && tzdir != zoneLib.chopped(1)) {
76 file->setFileName(zoneLib + name);
77 if (file->open(QIODevice::ReadOnly))
78 return true;
79 }
80 return false;
81}
82
83// Parse zone.tab table for territory information, read directories to ensure we
84// find all installed zones (many are omitted from zone.tab; even more from
85// zone1970.tab; see also QTBUG-64941).
87{
88 QFile tzif;
89 if (!openZoneInfo("zone.tab"_L1, &tzif))
90 return QTzTimeZoneHash();
91
92 QTzTimeZoneHash zonesHash;
93 QByteArray line;
94 while (tzif.readLineInto(&line)) {
95 QByteArrayView text = QByteArrayView(line).trimmed();
96 if (text.isEmpty() || text.at(0) == '#') // Ignore empty or comment
97 continue;
98 // Data rows are tab-separated columns Region, Coordinates, ID, Optional Comments
99 int cut = text.indexOf('\t');
100 if (Q_LIKELY(cut > 0)) {
101 QTzTimeZone zone;
102 // TODO: QLocale & friends could do this look-up without UTF8-conversion:
103 zone.territory = QLocalePrivate::codeToTerritory(QString::fromUtf8(text.first(cut)));
104 text = text.sliced(cut + 1);
105 cut = text.indexOf('\t');
106 if (Q_LIKELY(cut >= 0)) { // Skip over Coordinates, read ID and comment
107 text = text.sliced(cut + 1);
108 cut = text.indexOf('\t'); // < 0 if line has no comment
109 if (Q_LIKELY(cut)) {
110 const QByteArray id = (cut > 0 ? text.first(cut) : text).toByteArray();
111 if (cut > 0)
112 zone.comment = text.sliced(cut + 1).toByteArray();
113 zonesHash.insert(id, zone);
114 }
115 }
116 }
117 }
118
119 QString path = tzif.fileName();
120 const qsizetype cut = path.lastIndexOf(u'/');
121 Q_ASSERT(cut > 0);
122 path.truncate(cut + 1);
123 const qsizetype prefixLen = path.size();
124 for (const auto &info : QDirListing(path, QDirListing::IteratorFlag::Recursive)) {
125 if (!(info.isFile() || info.isSymLink()))
126 continue;
127 const QString infoAbsolutePath = info.absoluteFilePath();
128 const QString name = infoAbsolutePath.sliced(prefixLen);
129 // Two sub-directories containing (more or less) copies of the zoneinfo tree.
130 if (info.isDir() ? name == "posix"_L1 || name == "right"_L1
131 : name.startsWith("posix/"_L1) || name.startsWith("right/"_L1)) {
132 continue;
133 }
134 // We could filter out *.* and leapseconds instead of doing the
135 // isTzFile() check; in practice current (2023) zoneinfo/ contains only
136 // actual zone files and matches to that filter.
137 const QByteArray id = QFile::encodeName(name);
138 if (!zonesHash.contains(id) && isTzFile(infoAbsolutePath))
139 zonesHash.insert(id, QTzTimeZone());
140 }
141 return zonesHash;
142}
143
144// Hash of available system tz files as loaded by loadTzTimeZones()
145Q_GLOBAL_STATIC(const QTzTimeZoneHash, tzZones, loadTzTimeZones());
146
147/*
148 The following is copied and modified from tzfile.h which is in the public domain.
149 Copied as no compatibility guarantee and is never system installed.
150 See https://github.com/eggert/tz/blob/master/tzfile.h
151*/
152
153#define TZ_MAGIC "TZif"
154#define TZ_MAX_TIMES 1200
155#define TZ_MAX_TYPES 256 // Limited by what (unsigned char)'s can hold
156#define TZ_MAX_CHARS 50 // Maximum number of abbreviation characters
157#define TZ_MAX_LEAPS 50 // Maximum number of leap second corrections
158
159struct QTzHeader {
160 char tzh_magic[4]; // TZ_MAGIC
161 char tzh_version; // '\0' or '2' as of 2005
162 char tzh_reserved[15]; // reserved--must be zero
163 quint32 tzh_ttisgmtcnt; // number of trans. time flags
164 quint32 tzh_ttisstdcnt; // number of trans. time flags
165 quint32 tzh_leapcnt; // number of leap seconds
166 quint32 tzh_timecnt; // number of transition times
167 quint32 tzh_typecnt; // number of local time types
168 quint32 tzh_charcnt; // number of abbr. chars
169};
170
172 qint64 tz_time; // Transition time
173 quint8 tz_typeind; // Type Index
174};
176
177struct QTzType {
178 int tz_gmtoff; // UTC offset in seconds
179 bool tz_isdst; // Is DST
180 quint8 tz_abbrind; // abbreviation list index
181};
183
184static bool isTzFile(const QString &name)
185{
186 QFile file(name);
187 return file.open(QFile::ReadOnly) && file.read(strlen(TZ_MAGIC)) == TZ_MAGIC;
188}
189
190// TZ File parsing
191
192static QTzHeader parseTzHeader(QDataStream &ds, bool *ok)
193{
194 QTzHeader hdr;
195 quint8 ch;
196 *ok = false;
197
198 // Parse Magic, 4 bytes
199 ds.readRawData(hdr.tzh_magic, 4);
200
201 if (memcmp(hdr.tzh_magic, TZ_MAGIC, 4) != 0 || ds.status() != QDataStream::Ok)
202 return hdr;
203
204 // Parse Version, 1 byte, before 2005 was '\0', since 2005 a '2', since 2013 a '3'
205 ds >> ch;
206 hdr.tzh_version = ch;
207 if (ds.status() != QDataStream::Ok
208 || (hdr.tzh_version != '2' && hdr.tzh_version != '\0' && hdr.tzh_version != '3')) {
209 return hdr;
210 }
211
212 // Parse reserved space, 15 bytes
213 ds.readRawData(hdr.tzh_reserved, 15);
214 if (ds.status() != QDataStream::Ok)
215 return hdr;
216
217 // Parse rest of header, 6 x 4-byte transition counts
218 ds >> hdr.tzh_ttisgmtcnt >> hdr.tzh_ttisstdcnt >> hdr.tzh_leapcnt >> hdr.tzh_timecnt
219 >> hdr.tzh_typecnt >> hdr.tzh_charcnt;
220
221 // Check defined maximums
222 if (ds.status() != QDataStream::Ok
223 || hdr.tzh_timecnt > TZ_MAX_TIMES
224 || hdr.tzh_typecnt > TZ_MAX_TYPES
225 || hdr.tzh_charcnt > TZ_MAX_CHARS
226 || hdr.tzh_leapcnt > TZ_MAX_LEAPS
227 || hdr.tzh_ttisgmtcnt > hdr.tzh_typecnt
228 || hdr.tzh_ttisstdcnt > hdr.tzh_typecnt) {
229 return hdr;
230 }
231
232 *ok = true;
233 return hdr;
234}
235
236static std::optional<QList<QTzTransition>>
237parseTzTransitions(QDataStream &ds, int tzh_timecnt, int tzh_typecnt, bool longTran)
238{
239 QList<QTzTransition> transitions(tzh_timecnt);
240
241 if (longTran) {
242 // Parse tzh_timecnt x 8-byte transition times
243 for (int i = 0; i < tzh_timecnt; ++i) {
244 ds >> transitions[i].tz_time;
245 if (ds.status() != QDataStream::Ok)
246 return {};
247 }
248 } else {
249 // Parse tzh_timecnt x 4-byte transition times
250 qint32 val;
251 for (int i = 0; i < tzh_timecnt; ++i) {
252 ds >> val;
253 transitions[i].tz_time = val;
254 if (ds.status() != QDataStream::Ok)
255 return {};
256 }
257 }
258
259 // Parse tzh_timecnt x 1-byte transition type index
260 for (int i = 0; i < tzh_timecnt; ++i) {
261 quint8 typeind;
262 ds >> typeind;
263 if (ds.status() != QDataStream::Ok)
264 return {};
265 if (typeind >= tzh_typecnt)
266 return {}; // Malformed: transition names an undefined type.
267 transitions[i].tz_typeind = typeind;
268 }
269
270 return transitions;
271}
272
273static QList<QTzType> parseTzTypes(QDataStream &ds, int tzh_typecnt)
274{
275 QList<QTzType> types(tzh_typecnt);
276
277 // Parse tzh_typecnt x transition types
278 for (int i = 0; i < tzh_typecnt && ds.status() == QDataStream::Ok; ++i) {
279 QTzType &type = types[i];
280 // Parse UTC Offset, 4 bytes
281 ds >> type.tz_gmtoff;
282 // Parse Is DST flag, 1 byte
283 if (ds.status() == QDataStream::Ok)
284 ds >> type.tz_isdst;
285 // Parse Abbreviation Array Index, 1 byte
286 if (ds.status() == QDataStream::Ok)
287 ds >> type.tz_abbrind;
288 if (ds.status() != QDataStream::Ok)
289 types.resize(i);
290 }
291
292 return types;
293}
294
295static QMap<int, QByteArray> parseTzAbbreviations(QDataStream &ds, int tzh_charcnt, const QList<QTzType> &types)
296{
297 // Parse the abbreviation list which is tzh_charcnt long with '\0' separated strings. The
298 // QTzType.tz_abbrind index points to the first char of the abbreviation in the array, not the
299 // occurrence in the list. It can also point to a partial string so we need to use the actual typeList
300 // index values when parsing. By using a map with tz_abbrind as ordered key we get both index
301 // methods in one data structure and can convert the types afterwards.
302 QMap<int, QByteArray> map;
303 quint8 ch;
304 QByteArray input;
305 // First parse the full abbrev string
306 for (int i = 0; i < tzh_charcnt && ds.status() == QDataStream::Ok; ++i) {
307 ds >> ch;
308 if (ds.status() == QDataStream::Ok)
309 input.append(char(ch));
310 else
311 return map;
312 }
313 // Then extract all the substrings pointed to by types
314 for (const QTzType &type : types) {
315 const qsizetype end = type.tz_abbrind < input.size()
316 ? input.indexOf('\0', type.tz_abbrind) : -1;
317 if (end < 0)
318 return QMap<int, QByteArray>();
319 // Have reached end of an abbreviation, so add to map
320 map[type.tz_abbrind] = input.sliced(type.tz_abbrind, end - type.tz_abbrind);
321 }
322 return map;
323}
324
325static void parseTzLeapSeconds(QDataStream &ds, int tzh_leapcnt, bool longTran)
326{
327 // Parse tzh_leapcnt x pairs of leap seconds
328 // We don't use leap seconds, so only read and don't store
329 qint32 val;
330 if (longTran) {
331 // v2 file format, each entry is 12 bytes long
332 qint64 time;
333 for (int i = 0; i < tzh_leapcnt && ds.status() == QDataStream::Ok; ++i) {
334 // Parse Leap Occurrence Time, 8 bytes
335 ds >> time;
336 // Parse Leap Seconds To Apply, 4 bytes
337 if (ds.status() == QDataStream::Ok)
338 ds >> val;
339 }
340 } else {
341 // v0 file format, each entry is 8 bytes long
342 for (int i = 0; i < tzh_leapcnt && ds.status() == QDataStream::Ok; ++i) {
343 // Parse Leap Occurrence Time, 4 bytes
344 ds >> val;
345 // Parse Leap Seconds To Apply, 4 bytes
346 if (ds.status() == QDataStream::Ok)
347 ds >> val;
348 }
349 }
350}
351
352static QList<QTzType> parseTzIndicators(QDataStream &ds, const QList<QTzType> &types, int tzh_ttisstdcnt,
353 int tzh_ttisgmtcnt)
354{
355 QList<QTzType> result = types;
356 bool temp;
357 /*
358 Scan and discard indicators.
359
360 These indicators are only of use (by the date program) when "handling
361 POSIX-style time zone environment variables". The flags here say whether
362 the *specification* of the zone gave the time in UTC, local standard time
363 or local wall time; but whatever was specified has been digested for us,
364 already, by the zone-info compiler (zic), so that the tz_time values read
365 from the file (by parseTzTransitions) are all in UTC.
366 */
367
368 // Scan tzh_ttisstdcnt x 1-byte standard/wall indicators
369 for (int i = 0; i < tzh_ttisstdcnt && ds.status() == QDataStream::Ok; ++i)
370 ds >> temp;
371
372 // Scan tzh_ttisgmtcnt x 1-byte UTC/local indicators
373 for (int i = 0; i < tzh_ttisgmtcnt && ds.status() == QDataStream::Ok; ++i)
374 ds >> temp;
375
376 return result;
377}
378
379static QByteArray parseTzPosixRule(QDataStream &ds)
380{
381 // Parse POSIX rule, variable length '\n' enclosed
382 QByteArray rule;
383
384 quint8 ch;
385 ds >> ch;
386 if (ch != '\n' || ds.status() != QDataStream::Ok)
387 return rule;
388 ds >> ch;
389 while (ch != '\n' && ds.status() == QDataStream::Ok) {
390 rule.append((char)ch);
391 ds >> ch;
392 }
393
394 return rule;
395}
396
397static QDate calculateDowDate(int year, int month, int dayOfWeek, int week)
398{
399 if (dayOfWeek == 0) // Sunday; we represent it as 7, POSIX uses 0
400 dayOfWeek = 7;
401 else if (dayOfWeek & ~7 || month < 1 || month > 12 || week < 1 || week > 5)
402 return QDate();
403
404 QDate date(year, month, 1);
405 int startDow = date.dayOfWeek();
406 if (startDow <= dayOfWeek)
407 date = date.addDays(dayOfWeek - startDow - 7);
408 else
409 date = date.addDays(dayOfWeek - startDow);
410 date = date.addDays(week * 7);
411 while (date.month() != month)
412 date = date.addDays(-7);
413 return date;
414}
415
416static QDate calculatePosixDate(QLatin1StringView dateRule, int year)
417{
418 if (dateRule.isEmpty())
419 return QDate();
420 bool ok;
421 // Can start with M, J, or a digit
422 if (dateRule.at(0) == 'M') {
423 // nth week in month format "Mmonth.week.dow"
424 const auto dateParts = dateRule.tokenize(u'.');
425 auto token = dateParts.begin();
426 Q_ASSERT(token != dateParts.end());
427 Q_ASSERT(!token->isEmpty()); // the 'M' is its [0].
428 const int month = token->sliced(1).toInt(&ok);
429 if (ok && ++token != dateParts.end()) {
430 const int week = token->toInt(&ok);
431 if (ok && ++token != dateParts.end()) {
432 const int dow = token->toInt(&ok);
433 if (ok)
434 return calculateDowDate(year, month, dow, week);
435 }
436 }
437 } else if (dateRule.at(0) == 'J') {
438 // Day of Year 1...365, ignores Feb 29.
439 // So March always starts on day 60.
440 int doy = dateRule.sliced(1).toInt(&ok);
441 if (ok && doy > 0 && doy < 366) {
442 // Subtract 1 because we're adding days *after* the first of
443 // January, unless it's after February in a leap year, when the leap
444 // day cancels that out:
445 if (!QDate::isLeapYear(year) || doy < 60)
446 --doy;
447 return QDate(year, 1, 1).addDays(doy);
448 }
449 } else {
450 // Day of Year 0...365, includes Feb 29
451 int doy = dateRule.toInt(&ok);
452 if (ok && doy >= 0 && doy < 366)
453 return QDate(year, 1, 1).addDays(doy);
454 }
455 return QDate();
456}
457
458// returns the time in seconds, INT_MIN if we failed to parse
459static int parsePosixTime(const char *begin, const char *end)
460{
461 // Format "hh[:mm[:ss]]"
462 int hour, min = 0, sec = 0;
463
464 const int maxHour = 137; // POSIX's extended range.
465 auto r = qstrntoll(begin, end - begin, 10);
466 hour = r.result;
467 if (!r.ok() || hour < -maxHour || hour > maxHour || r.used > 2)
468 return INT_MIN;
469 begin += r.used;
470 if (begin < end && *begin == ':') {
471 // minutes
472 ++begin;
473 r = qstrntoll(begin, end - begin, 10);
474 min = r.result;
475 if (!r.ok() || min < 0 || min > 59 || r.used > 2)
476 return INT_MIN;
477
478 begin += r.used;
479 if (begin < end && *begin == ':') {
480 // seconds
481 ++begin;
482 r = qstrntoll(begin, end - begin, 10);
483 sec = r.result;
484 if (!r.ok() || sec < 0 || sec > 59 || r.used > 2)
485 return INT_MIN;
486 begin += r.used;
487 }
488 }
489
490 // we must have consumed everything
491 if (begin != end)
492 return INT_MIN;
493
494 return (hour * 60 + min) * 60 + sec;
495}
496
497static int parsePosixTransitionTime(QLatin1StringView timeRule)
498{
499 return parsePosixTime(timeRule.begin(), timeRule.end());
500}
501
502static int parsePosixOffset(const char *begin, const char *end)
503{
504 // Format "[+|-]hh[:mm[:ss]]"
505 // note that the sign is inverted because POSIX counts in hours West of GMT
506 bool negate = true;
507 if (*begin == '+') {
508 ++begin;
509 } else if (*begin == '-') {
510 negate = false;
511 ++begin;
512 }
513
514 // POSIX says hour is at most 24, minutes and seconds (when present) at most 59.
515 // We here enforce the stronger condition that the offset is at most 24 hours.
516 // (This is weaker than QTZ's M(ax|in)UtcOffsetSecs but asserted by QDT.)
517 int value = parsePosixTime(begin, end);
518 using namespace QtPrivate::DateTimeConstants;
519 if (value < -SECS_PER_DAY || value > SECS_PER_DAY)
520 return INT_MIN;
521 return negate ? -value : value;
522}
523
524static inline bool asciiIsLetter(char ch)
525{
526 ch |= 0x20; // lowercases if it is a letter, otherwise just corrupts ch
527 return ch >= 'a' && ch <= 'z';
528}
529
530// The characters POSIX allows in a <>-quoted zone name:
531static inline bool asciiIsQuotedNameChar(char ch)
532{
533 return asciiIsLetter(ch) || (ch >= '0' && ch <= '9') || ch == '+' || ch == '-';
534}
535
536namespace {
537
538struct PosixZone // TODO: QTBUG-112006 - make this cross-platform.
539{
540 enum {
541 InvalidOffset = INT_MIN,
542 };
543
544 QString name;
545 int offset = InvalidOffset;
546 bool hasValidOffset() const noexcept { return offset != InvalidOffset; }
547 QTimeZonePrivate::Data dataAt(qint64 when)
548 {
549 Q_ASSERT(hasValidOffset());
550 return QTimeZonePrivate::Data(name, when, offset, offset);
551 }
552 QTimeZonePrivate::Data dataAtOffset(qint64 when, int standard)
553 {
554 Q_ASSERT(hasValidOffset());
555 return QTimeZonePrivate::Data(name, when, offset, standard);
556 }
557
558 static PosixZone parse(const char *&pos, const char *end);
559};
560
561} // unnamed namespace
562
563// Returns the zone name, the offset (in seconds) and advances \a begin to
564// where the parsing ended. Returns a zone of INT_MIN in case an offset
565// couldn't be read.
566PosixZone PosixZone::parse(const char *&pos, const char *end)
567{
568 static const char offsetChars[] = "0123456789:";
569
570 const char *nameBegin = pos;
571 const char *nameEnd;
572 Q_ASSERT(pos < end);
573
574 if (*pos == '<') {
575 ++nameBegin; // skip the '<'
576 nameEnd = nameBegin;
577 while (nameEnd < end && asciiIsQuotedNameChar(*nameEnd))
578 ++nameEnd;
579 if (nameEnd == end || *nameEnd != '>') {
580 // Unterminated '<':
581 pos = end;
582 return {};
583 }
584 pos = nameEnd + 1; // skip the '>'
585 } else {
586 nameEnd = nameBegin;
587 while (nameEnd < end && asciiIsLetter(*nameEnd))
588 ++nameEnd;
589 pos = nameEnd;
590 }
591 if (nameEnd - nameBegin < 3)
592 return {}; // name must be at least 3 characters long
593
594 // zone offset, form [+-]hh:mm:ss
595 const char *zoneBegin = pos;
596 const char *zoneEnd = pos;
597 // Sign is optional (implicitly +):
598 if (zoneEnd < end && (zoneEnd[0] == '+' || zoneEnd[0] == '-'))
599 ++zoneEnd;
600 while (zoneEnd < end) {
601 if (strchr(offsetChars, char(*zoneEnd)) == nullptr)
602 break;
603 ++zoneEnd;
604 }
605
606 QString name = QString::fromUtf8(nameBegin, nameEnd - nameBegin);
607 const int offset = zoneEnd > zoneBegin ? parsePosixOffset(zoneBegin, zoneEnd) : InvalidOffset;
608 pos = zoneEnd;
609 // UTC+hh:mm:ss or GMT+hh:mm:ss should be read as offsets from UTC, not as a
610 // POSIX rule naming a zone as UTC or GMT and specifying a non-zero offset.
611 if (offset != 0 && (name =="UTC"_L1 || name == "GMT"_L1))
612 return {};
613 return {std::move(name), offset};
614}
615
616/* Parse and check a POSIX rule.
617
618 By default a simple zone abbreviation with no offset information is accepted.
619 Set \a requireOffset to \c true to require that there be offset data present.
620*/
621static auto validatePosixRule(QByteArrayView posixRule, bool requireOffset = false)
622{
623 // Format is described here:
624 // https://sourceware.org/glibc/manual/latest/html_node/Proleptic-TZ.html
625 // See also calculatePosixTransition()'s reference.
626 QLatin1StringView zoneinfo, startDate, endDate;
627 int parts = 1;
628 {
629 const auto tokens = QLatin1StringView(posixRule).tokenize(u',');
630 auto token = tokens.begin();
631 Q_ASSERT(token != tokens.end());
632 zoneinfo = token->trimmed();
633 if (++token != tokens.end()) {
634 ++parts;
635 startDate = *token;
636 if (++token != tokens.end()) {
637 ++parts;
638 endDate = *token;
639 if (++token != tokens.end())
640 ++parts; // So we know if we have too many
641 }
642 }
643 }
644 const struct { bool isValid, hasDst; } fail{false, false}, good{true, parts > 1};
645 if (zoneinfo.isEmpty())
646 return fail;
647
648 const char *begin = zoneinfo.begin();
649 {
650 // Updates begin to point after the name and offset it parses:
651 const auto posix = PosixZone::parse(begin, zoneinfo.end());
652 if (posix.name.isEmpty())
653 return fail;
654 if (requireOffset && !posix.hasValidOffset())
655 return fail;
656 }
657
658 if (good.hasDst) {
659 if (begin >= zoneinfo.end())
660 return fail;
661 // Expect a second name (and optional offset) after the first:
662 if (PosixZone::parse(begin, zoneinfo.end()).name.isEmpty())
663 return fail;
664 }
665 if (begin < zoneinfo.end())
666 return fail;
667
668 if (good.hasDst) {
669 if (parts != 3 || startDate.isEmpty() || endDate.isEmpty())
670 return fail;
671 for (int i = 0; i < 2; ++i) {
672 const auto tokens = (i ? endDate : startDate).tokenize(u'/');
673 auto token = tokens.begin();
674 Q_ASSERT(token != tokens.end());
675 if (!calculatePosixDate(*token, 1972).isValid())
676 return fail;
677 if (++token != tokens.end() && parsePosixTime(token->begin(), token->end()) == INT_MIN)
678 return fail;
679 }
680 }
681 return good;
682}
683
684static QList<QTimeZonePrivate::Data> calculatePosixTransitions(const QByteArray &posixRule,
685 int startYear, int endYear,
686 qint64 lastTranMSecs)
687{
688 QList<QTimeZonePrivate::Data> result;
689
690 // POSIX Format is like "TZ=CST6CDT,M3.2.0/2:00:00,M11.1.0/2:00:00"
691 // i.e. "std offset dst [offset],start[/time],end[/time]"
692 // See the section about TZ at the end of
693 // https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html
694 // and the link in validatePosixRule(), above.
695 const auto tokens = QLatin1String(posixRule).tokenize(u',');
696 auto token = tokens.begin();
697 Q_ASSERT(token != tokens.end());
698
699 PosixZone stdZone, dstZone;
700 QLatin1StringView zoneText = *token;
701 {
702 QLatin1StringView zoneinfo = zoneText.trimmed();
703 const char *begin = zoneinfo.begin();
704
705 stdZone = PosixZone::parse(begin, zoneinfo.end());
706 if (!stdZone.hasValidOffset()) {
707 stdZone.offset = 0; // reset to UTC if we failed to parse
708 } else if (begin < zoneinfo.end()) {
709 dstZone = PosixZone::parse(begin, zoneinfo.end());
710 if (!dstZone.hasValidOffset()) {
711 // if the dst offset isn't provided, it is 1 hour ahead of the standard offset
712 dstZone.offset = stdZone.offset + (60 * 60);
713 }
714 }
715 }
716
717 // If only the name part, or no DST specified, then no transitions
718 if (++token == tokens.end() || !dstZone.hasValidOffset()) {
719 result.emplaceBack(
720 stdZone.name.isEmpty() ? QString(zoneText) : stdZone.name,
721 lastTranMSecs, stdZone.offset, stdZone.offset);
722 return result;
723 }
724 QLatin1StringView dstRule = *token;
725 if (++token == tokens.end() || dstRule.isEmpty() || token->isEmpty())
726 return result; // Malformed.
727 QLatin1StringView stdRule = *token;
728
729 // Get the std to dst transition details
730 const int twoOClock = 7200; // Default transition time, when none specified
731 const auto dstParts = dstRule.tokenize(u'/');
732 auto subtok = dstParts.begin();
733 Q_ASSERT(subtok != dstParts.end());
734 QLatin1StringView dstDateRule = *subtok;
735 const int dstTime = ++subtok == dstParts.end() ? twoOClock : parsePosixTransitionTime(*subtok);
736
737 // Get the dst to std transition details
738 const auto stdParts = stdRule.tokenize(u'/');
739 subtok = stdParts.begin();
740 Q_ASSERT(subtok != stdParts.end());
741 QLatin1StringView stdDateRule = *subtok;
742 const int stdTime = ++subtok == stdParts.end() ? twoOClock : parsePosixTransitionTime(*subtok);
743
744 if (dstDateRule.isEmpty() || stdDateRule.isEmpty() || dstTime == INT_MIN || stdTime == INT_MIN)
745 return result; // Malformed.
746
747 // Limit year to the range QDateTime can represent:
748 const int minYear = int(QDateTime::YearRange::First);
749 const int maxYear = int(QDateTime::YearRange::Last);
750 startYear = qBound(minYear, startYear, maxYear);
751 endYear = qBound(minYear, endYear, maxYear);
752 Q_ASSERT(startYear <= endYear);
753
754 for (int year = startYear; year <= endYear; ++year) {
755 // Note: std and dst, despite being QDateTime(,, UTC), have the
756 // date() and time() of the *zone*'s description of the transition
757 // moments; the atMSecsSinceEpoch values computed from them are
758 // correctly offse to be UTC-based.
759
760 // Transition to daylight-saving time:
761 QDateTime dst(calculatePosixDate(dstDateRule, year)
762 .startOfDay(QTimeZone::UTC).addSecs(dstTime));
763 auto saving = dstZone.dataAtOffset(dst.toMSecsSinceEpoch() - stdZone.offset * 1000,
764 stdZone.offset);
765 // Transition to standard time:
766 QDateTime std(calculatePosixDate(stdDateRule, year)
767 .startOfDay(QTimeZone::UTC).addSecs(stdTime));
768 auto standard = stdZone.dataAt(std.toMSecsSinceEpoch() - dstZone.offset * 1000);
769
770 if (year == startYear) {
771 // Handle the special case of fixed state, which may be represented
772 // by fake transitions at start and end of each year:
773 if (saving.atMSecsSinceEpoch < standard.atMSecsSinceEpoch) {
774 if (dst <= QDate(year, 1, 1).startOfDay(QTimeZone::UTC)
775 && std >= QDate(year, 12, 31).endOfDay(QTimeZone::UTC)) {
776 // Permanent DST:
777 saving.atMSecsSinceEpoch = lastTranMSecs;
778 result.emplaceBack(std::move(saving));
779 return result;
780 }
781 } else {
782 if (std <= QDate(year, 1, 1).startOfDay(QTimeZone::UTC)
783 && dst >= QDate(year, 12, 31).endOfDay(QTimeZone::UTC)) {
784 // Permanent Standard time, perversely described:
785 standard.atMSecsSinceEpoch = lastTranMSecs;
786 result.emplaceBack(std::move(standard));
787 return result;
788 }
789 }
790 }
791
792 const bool useStd = std.isValid() && std.date().year() == year && !stdZone.name.isEmpty();
793 const bool useDst = dst.isValid() && dst.date().year() == year && !dstZone.name.isEmpty();
794 if (useStd && useDst) {
795 if (dst < std) {
796 result.emplaceBack(std::move(saving));
797 result.emplaceBack(std::move(standard));
798 } else {
799 result.emplaceBack(std::move(standard));
800 result.emplaceBack(std::move(saving));
801 }
802 } else if (useStd) {
803 result.emplaceBack(std::move(standard));
804 } else if (useDst) {
805 result.emplaceBack(std::move(saving));
806 }
807 }
808 return result;
809}
810
811// Create the system default time zone
812QTzTimeZonePrivate::QTzTimeZonePrivate()
813 : QTzTimeZonePrivate(staticSystemTimeZoneId())
814{
815}
816
817QTzTimeZonePrivate::~QTzTimeZonePrivate()
818{
819}
820
821QTzTimeZonePrivate *QTzTimeZonePrivate::clone() const
822{
823 return new QTzTimeZonePrivate(*this);
824}
825
827{
828public:
829 QTzTimeZoneCacheEntry fetchEntry(const QByteArray &ianaId);
830 // Set max cost to hold whole IANA DB, since some code iterates it !
832
833private:
834 static QTzTimeZoneCacheEntry findEntry(const QByteArray &ianaId);
835 QCache<QByteArray, QTzTimeZoneCacheEntry> m_cache;
836 QMutex m_mutex;
837};
838
839QTzTimeZoneCacheEntry QTzTimeZoneCache::findEntry(const QByteArray &ianaId)
840{
841 QTzTimeZoneCacheEntry ret;
842 QFile tzif;
843 if (ianaId.isEmpty()) {
844 // Open system tz
845 tzif.setFileName(QStringLiteral("/etc/localtime"));
846 if (!tzif.open(QIODevice::ReadOnly))
847 return ret;
848 } else if (!openZoneInfo(QString::fromLocal8Bit(ianaId), &tzif)) {
849 // ianaId may be a POSIX rule, taken from $TZ or /etc/TZ
850 auto check = validatePosixRule(ianaId);
851 if (check.isValid) {
852 ret.m_hasDst = check.hasDst;
853 ret.m_posixRule = ianaId;
854 }
855 return ret;
856 }
857
858 QDataStream ds(&tzif);
859
860 // Parse the old version block of data
861 bool ok = false;
862 QByteArray posixRule;
863 QTzHeader hdr = parseTzHeader(ds, &ok);
864 if (!ok || ds.status() != QDataStream::Ok)
865 return ret;
866
867 auto trans = parseTzTransitions(ds, hdr.tzh_timecnt, hdr.tzh_typecnt, false);
868 if (!trans)
869 return ret;
870 Q_ASSERT(ds.status() == QDataStream::Ok);
871 QList<QTzTransition> tranList = std::move(*trans);
872
873 QList<QTzType> typeList = parseTzTypes(ds, hdr.tzh_typecnt);
874 if (ds.status() != QDataStream::Ok)
875 return ret;
876 QMap<int, QByteArray> abbrevMap = parseTzAbbreviations(ds, hdr.tzh_charcnt, typeList);
877 if (ds.status() != QDataStream::Ok)
878 return ret;
879 parseTzLeapSeconds(ds, hdr.tzh_leapcnt, false);
880 if (ds.status() != QDataStream::Ok)
881 return ret;
882 typeList = parseTzIndicators(ds, typeList, hdr.tzh_ttisstdcnt, hdr.tzh_ttisgmtcnt);
883 if (ds.status() != QDataStream::Ok)
884 return ret;
885
886 // If version 2 then parse the second block of data
887 if (hdr.tzh_version == '2' || hdr.tzh_version == '3') {
888 ok = false;
889 QTzHeader hdr2 = parseTzHeader(ds, &ok);
890 if (!ok || ds.status() != QDataStream::Ok)
891 return ret;
892
893 trans = parseTzTransitions(ds, hdr2.tzh_timecnt, hdr2.tzh_typecnt, true);
894 if (!trans)
895 return ret;
896 Q_ASSERT(ds.status() == QDataStream::Ok);
897 tranList = std::move(*trans);
898
899 typeList = parseTzTypes(ds, hdr2.tzh_typecnt);
900 if (ds.status() != QDataStream::Ok)
901 return ret;
902 abbrevMap = parseTzAbbreviations(ds, hdr2.tzh_charcnt, typeList);
903 if (ds.status() != QDataStream::Ok)
904 return ret;
905 parseTzLeapSeconds(ds, hdr2.tzh_leapcnt, true);
906 if (ds.status() != QDataStream::Ok)
907 return ret;
908 typeList = parseTzIndicators(ds, typeList, hdr2.tzh_ttisstdcnt, hdr2.tzh_ttisgmtcnt);
909 if (ds.status() != QDataStream::Ok)
910 return ret;
911 posixRule = parseTzPosixRule(ds);
912 if (ds.status() != QDataStream::Ok)
913 return ret;
914 }
915 // Translate the TZ file's raw data into our internal form:
916
917 if (!posixRule.isEmpty()) {
918 auto check = validatePosixRule(posixRule);
919 if (!check.isValid) // We got a POSIX rule, but it was malformed:
920 return ret;
921 ret.m_posixRule = posixRule;
922 ret.m_hasDst = check.hasDst;
923 }
924
925 // Translate the array-index-based tz_abbrind into list index
926 const int size = abbrevMap.size();
927 ret.m_abbreviations.clear();
928 ret.m_abbreviations.reserve(size);
929 QList<int> abbrindList;
930 abbrindList.reserve(size);
931 for (auto it = abbrevMap.cbegin(), end = abbrevMap.cend(); it != end; ++it) {
932 ret.m_abbreviations.append(it.value());
933 abbrindList.append(it.key());
934 }
935 // Map tz_abbrind from map's keys (as initially read) to abbrindList's
936 // indices (used hereafter):
937 for (int i = 0; i < typeList.size(); ++i)
938 typeList[i].tz_abbrind = abbrindList.indexOf(typeList.at(i).tz_abbrind);
939
940 // TODO: is typeList[0] always the "before zones" data ? It seems to be ...
941 if (typeList.size())
942 ret.m_preZoneRule = { typeList.at(0).tz_gmtoff, 0, typeList.at(0).tz_abbrind };
943
944 // Offsets are stored as total offset, want to know separate UTC and DST offsets
945 // so find the first non-dst transition to use as base UTC Offset
946 int utcOffset = ret.m_preZoneRule.stdOffset;
947 for (const QTzTransition &tran : std::as_const(tranList)) {
948 if (!typeList.at(tran.tz_typeind).tz_isdst) {
949 utcOffset = typeList.at(tran.tz_typeind).tz_gmtoff;
950 break;
951 }
952 }
953
954 // Now for each transition time calculate and store our rule:
955 const int tranCount = tranList.size();
956 ret.m_tranTimes.reserve(tranCount);
957 // The DST offset when in effect: usually stable, usually an hour:
958 int lastDstOff = 3600;
959 for (int i = 0; i < tranCount; i++) {
960 const QTzTransition &tz_tran = tranList.at(i);
961 QTzTransitionTime tran;
962 QTzTransitionRule rule;
963 const QTzType tz_type = typeList.at(tz_tran.tz_typeind);
964
965 // Calculate the associated Rule
966 if (!tz_type.tz_isdst) {
967 utcOffset = tz_type.tz_gmtoff;
968 } else if (Q_UNLIKELY(tz_type.tz_gmtoff != utcOffset + lastDstOff)) {
969 /*
970 This might be a genuine change in DST offset, but could also be
971 DST starting at the same time as the standard offset changed. See
972 if DST's end gives a more plausible utcOffset (i.e. one closer to
973 the last we saw, or a simple whole hour):
974 */
975 // Standard offset inferred from net offset and expected DST offset:
976 const int inferStd = tz_type.tz_gmtoff - lastDstOff; // != utcOffset
977 for (int j = i + 1; j < tranCount; j++) {
978 const QTzType new_type = typeList.at(tranList.at(j).tz_typeind);
979 if (!new_type.tz_isdst) {
980 const int newUtc = new_type.tz_gmtoff;
981 if (newUtc == utcOffset) {
982 // DST-end can't help us, avoid lots of messy checks.
983 // else: See if the end matches the familiar DST offset:
984 } else if (newUtc == inferStd) {
985 utcOffset = newUtc;
986 // else: let either end shift us to one hour as DST offset:
987 } else if (tz_type.tz_gmtoff - 3600 == utcOffset) {
988 // Start does it
989 } else if (tz_type.tz_gmtoff - 3600 == newUtc) {
990 utcOffset = newUtc; // End does it
991 // else: prefer whichever end gives DST offset closer to
992 // last, but consider any offset > 0 "closer" than any <= 0:
993 } else if (newUtc < tz_type.tz_gmtoff
994 ? (utcOffset >= tz_type.tz_gmtoff
995 || qAbs(newUtc - inferStd) < qAbs(utcOffset - inferStd))
996 : (utcOffset >= tz_type.tz_gmtoff
997 && qAbs(newUtc - inferStd) < qAbs(utcOffset - inferStd))) {
998 utcOffset = newUtc;
999 }
1000 break;
1001 }
1002 }
1003 lastDstOff = tz_type.tz_gmtoff - utcOffset;
1004 }
1005 rule.stdOffset = utcOffset;
1006 rule.dstOffset = tz_type.tz_gmtoff - utcOffset;
1007 rule.abbreviationIndex = tz_type.tz_abbrind;
1008
1009 // If the rule already exist then use that, otherwise add it
1010 int ruleIndex = ret.m_tranRules.indexOf(rule);
1011 if (ruleIndex == -1) {
1012 if (rule.dstOffset != 0)
1013 ret.m_hasDst = true;
1014 tran.ruleIndex = ret.m_tranRules.size();
1015 ret.m_tranRules.append(rule);
1016 } else {
1017 tran.ruleIndex = ruleIndex;
1018 }
1019
1020 if (qMulOverflow<1000>(tz_tran.tz_time, &tran.atMSecsSinceEpoch)) {
1021 ret.m_tranTimes.clear();
1022 ret.m_posixRule.clear();
1023 break;
1024 }
1025 ret.m_tranTimes.append(tran);
1026 }
1027
1028 return ret;
1029}
1030
1032{
1033 QMutexLocker locker(&m_mutex);
1034
1035 // search the cache...
1036 QTzTimeZoneCacheEntry *obj = m_cache.object(ianaId);
1037 if (obj)
1038 return *obj;
1039
1040 // ... or build a new entry from scratch
1041
1042 locker.unlock(); // don't parse files under mutex lock
1043
1044 QTzTimeZoneCacheEntry ret = findEntry(ianaId);
1045 if (ret.m_tranTimes.isEmpty() && ret.m_posixRule.isEmpty())
1046 return ret; // Don't use up cache space with invalid IDs.
1047
1048 auto ptr = std::make_unique<QTzTimeZoneCacheEntry>(ret);
1049
1050 locker.relock();
1051 m_cache.insert(ianaId, ptr.release()); // may overwrite if another thread was faster
1052 locker.unlock();
1053
1054 return ret;
1055}
1056
1057// Create a named time zone
1058QTzTimeZonePrivate::QTzTimeZonePrivate(const QByteArray &ianaId)
1059{
1060 if (!isTimeZoneIdAvailable(ianaId)) // Avoid pointlessly creating cache entries
1061 return;
1062 static QTzTimeZoneCache tzCache;
1063 auto entry = tzCache.fetchEntry(ianaId);
1064 if (entry.m_tranTimes.isEmpty() && entry.m_posixRule.isEmpty())
1065 return; // Invalid after all !
1066
1067 cached_data = std::move(entry);
1068 m_id = ianaId;
1069 // Avoid empty ID, if we have an abbreviation to use instead
1070 if (m_id.isEmpty()) {
1071 // This can only happen for the system zone, when we've read the
1072 // contents of /etc/localtime because it wasn't a symlink.
1073 // TODO: use CLDR generic abbreviation for the zone.
1074 m_id = abbreviation(QDateTime::currentMSecsSinceEpoch()).toUtf8();
1075 }
1076}
1077
1078QLocale::Territory QTzTimeZonePrivate::territory() const
1079{
1080 return tzZones->value(m_id).territory;
1081}
1082
1083QString QTzTimeZonePrivate::comment() const
1084{
1085 return QString::fromUtf8(tzZones->value(m_id).comment);
1086}
1087
1088QString QTzTimeZonePrivate::displayName(QTimeZone::TimeType timeType,
1089 QTimeZone::NameType nameType,
1090 const QLocale &locale) const
1091{
1092 // TZ only provides C-locale abbreviations and offset:
1093 if (nameType != QTimeZone::LongName && isDataLocale(locale)) {
1094 Data tran = data(timeType);
1095 if (tran.atMSecsSinceEpoch != invalidMSecs()) {
1096 if (nameType == QTimeZone::ShortName)
1097 return tran.abbreviation;
1098 // Save base class repeating the data(timeType) query:
1099 if (isAnglicLocale(locale))
1100 return isoOffsetFormat(tran.offsetFromUtc);
1101 }
1102 }
1103 // Otherwise, fall back to base class (and qtimezonelocale.cpp):
1104 return QTimeZonePrivate::displayName(timeType, nameType, locale);
1105}
1106
1107QString QTzTimeZonePrivate::abbreviation(qint64 atMSecsSinceEpoch) const
1108{
1109 return data(atMSecsSinceEpoch).abbreviation;
1110}
1111
1112int QTzTimeZonePrivate::offsetFromUtc(qint64 atMSecsSinceEpoch) const
1113{
1114 const Data tran = data(atMSecsSinceEpoch);
1115 return tran.offsetFromUtc; // == tran.standardTimeOffset + tran.daylightTimeOffset
1116}
1117
1118int QTzTimeZonePrivate::standardTimeOffset(qint64 atMSecsSinceEpoch) const
1119{
1120 return data(atMSecsSinceEpoch).standardTimeOffset;
1121}
1122
1123int QTzTimeZonePrivate::daylightTimeOffset(qint64 atMSecsSinceEpoch) const
1124{
1125 return data(atMSecsSinceEpoch).daylightTimeOffset;
1126}
1127
1128bool QTzTimeZonePrivate::hasDaylightTime() const
1129{
1130 return cached_data.m_hasDst;
1131}
1132
1133bool QTzTimeZonePrivate::isDaylightTime(qint64 atMSecsSinceEpoch) const
1134{
1135 return (daylightTimeOffset(atMSecsSinceEpoch) != 0);
1136}
1137
1138QTimeZonePrivate::Data QTzTimeZonePrivate::dataForTzTransition(QTzTransitionTime tran) const
1139{
1140 return dataFromRule(cached_data.m_tranRules.at(tran.ruleIndex), tran.atMSecsSinceEpoch);
1141}
1142
1143QTimeZonePrivate::Data QTzTimeZonePrivate::dataFromRule(QTzTransitionRule rule,
1144 qint64 msecsSinceEpoch) const
1145{
1146 return Data(QString::fromUtf8(cached_data.m_abbreviations.at(rule.abbreviationIndex)),
1147 msecsSinceEpoch, rule.stdOffset + rule.dstOffset, rule.stdOffset);
1148}
1149
1150QList<QTimeZonePrivate::Data> QTzTimeZonePrivate::getPosixTransitions(qint64 msNear) const
1151{
1152 const int year = QDateTime::fromMSecsSinceEpoch(msNear, QTimeZone::UTC).date().year();
1153 // The Data::atMSecsSinceEpoch of the single entry if zone is constant:
1154 qint64 atTime = tranCache().isEmpty() ? msNear : tranCache().last().atMSecsSinceEpoch;
1155 return calculatePosixTransitions(cached_data.m_posixRule, year - 1, year + 1, atTime);
1156}
1157
1158QTimeZonePrivate::Data QTzTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const
1159{
1160 // If the required time is after the last transition (or there were none)
1161 // and we have a POSIX rule, then use it:
1162 if (!cached_data.m_posixRule.isEmpty()
1163 && (tranCache().isEmpty() || tranCache().last().atMSecsSinceEpoch < forMSecsSinceEpoch)) {
1164 QList<Data> posixTrans = getPosixTransitions(forMSecsSinceEpoch);
1165 auto it = std::partition_point(posixTrans.cbegin(), posixTrans.cend(),
1166 [forMSecsSinceEpoch] (const Data &at) {
1167 return at.atMSecsSinceEpoch <= forMSecsSinceEpoch;
1168 });
1169 // Use most recent, if any in the past; or the first if we have no other rules:
1170 if (it > posixTrans.cbegin() || (tranCache().isEmpty() && it < posixTrans.cend())) {
1171 Data data = *(it > posixTrans.cbegin() ? it - 1 : it);
1172 data.atMSecsSinceEpoch = forMSecsSinceEpoch;
1173 return data;
1174 }
1175 }
1176 if (tranCache().isEmpty()) // Only possible if !isValid()
1177 return {};
1178
1179 // Otherwise, use the rule for the most recent or first transition:
1180 auto last = std::partition_point(tranCache().cbegin(), tranCache().cend(),
1181 [forMSecsSinceEpoch] (QTzTransitionTime at) {
1182 return at.atMSecsSinceEpoch <= forMSecsSinceEpoch;
1183 });
1184 if (last == tranCache().cbegin())
1185 return dataFromRule(cached_data.m_preZoneRule, forMSecsSinceEpoch);
1186
1187 --last;
1188 return dataFromRule(cached_data.m_tranRules.at(last->ruleIndex), forMSecsSinceEpoch);
1189}
1190
1191// Overridden because the final iteration over transitions only needs to look
1192// forward and backwards one transition within the POSIX rule (when there is
1193// one, as is common) to settle the whole period it covers, so we can then skip
1194// all other transitions of the POSIX rule and iterate tranCache() backwards
1195// from its most recent transition.
1196QTimeZonePrivate::Data QTzTimeZonePrivate::data(QTimeZone::TimeType timeType) const
1197{
1198 // True if tran is valid and has the DST-ness to match timeType:
1199 const auto validMatch = [timeType](const Data &tran) {
1200 return tran.atMSecsSinceEpoch != invalidMSecs()
1201 && ((timeType == QTimeZone::DaylightTime) != (tran.daylightTimeOffset == 0));
1202 };
1203
1204 // Get current tran, use if suitable:
1205 const qint64 currentMSecs = QDateTime::currentMSecsSinceEpoch();
1206 Data tran = data(currentMSecs);
1207 if (validMatch(tran))
1208 return tran;
1209
1210 // Otherwise, next tran probably flips DST-ness:
1211 tran = nextTransition(currentMSecs);
1212 if (validMatch(tran))
1213 return tran;
1214
1215 // Failing that, prev (or present, if current MSecs is eactly a transition
1216 // moment) tran defines what data() got us and the one before that probably
1217 // flips DST-ness:
1218 tran = previousTransition(currentMSecs + 1);
1219 if (tran.atMSecsSinceEpoch != invalidMSecs())
1220 tran = previousTransition(tran.atMSecsSinceEpoch);
1221 if (validMatch(tran))
1222 return tran;
1223
1224 // Otherwise, we can look backwards through transitions for a match; if we
1225 // have a POSIX rule, it clearly doesn't do DST (or we'd have hit it by
1226 // now), so we only need to look in the tranCache() up to now.
1227 const auto untilNow = [currentMSecs](QTzTransitionTime at) {
1228 return at.atMSecsSinceEpoch <= currentMSecs;
1229 };
1230 auto it = std::partition_point(tranCache().cbegin(), tranCache().cend(), untilNow);
1231 // That's the end or first future transition; we don't want to look at it,
1232 // but at all those before it.
1233 while (it != tranCache().cbegin()) {
1234 --it;
1235 tran = dataForTzTransition(*it);
1236 if ((timeType == QTimeZone::DaylightTime) != (tran.daylightTimeOffset == 0))
1237 return tran;
1238 }
1239
1240 return {};
1241}
1242
1243bool QTzTimeZonePrivate::isDataLocale(const QLocale &locale) const
1244{
1245 // TZ data uses en-Latn-* / C locale names:
1246 return isAnglicLocale(locale);
1247}
1248
1249bool QTzTimeZonePrivate::hasTransitions() const
1250{
1251 return true;
1252}
1253
1254QTimeZonePrivate::Data QTzTimeZonePrivate::nextTransition(qint64 afterMSecsSinceEpoch) const
1255{
1256 // If the required time is after the last transition (or there were none)
1257 // and we have a POSIX rule, then use it:
1258 if (!cached_data.m_posixRule.isEmpty()
1259 && (tranCache().isEmpty() || tranCache().last().atMSecsSinceEpoch < afterMSecsSinceEpoch)) {
1260 QList<Data> posixTrans = getPosixTransitions(afterMSecsSinceEpoch);
1261 auto it = std::partition_point(posixTrans.cbegin(), posixTrans.cend(),
1262 [afterMSecsSinceEpoch] (const Data &at) {
1263 return at.atMSecsSinceEpoch <= afterMSecsSinceEpoch;
1264 });
1265
1266 return it == posixTrans.cend() ? Data{} : *it;
1267 }
1268
1269 // Otherwise, if we can find a valid tran, use its rule:
1270 auto last = std::partition_point(tranCache().cbegin(), tranCache().cend(),
1271 [afterMSecsSinceEpoch] (QTzTransitionTime at) {
1272 return at.atMSecsSinceEpoch <= afterMSecsSinceEpoch;
1273 });
1274 return last != tranCache().cend() ? dataForTzTransition(*last) : Data{};
1275}
1276
1277QTimeZonePrivate::Data QTzTimeZonePrivate::previousTransition(qint64 beforeMSecsSinceEpoch) const
1278{
1279 // If the required time is after the last transition (or there were none)
1280 // and we have a POSIX rule, then use it:
1281 if (!cached_data.m_posixRule.isEmpty()
1282 && (tranCache().isEmpty() || tranCache().last().atMSecsSinceEpoch < beforeMSecsSinceEpoch)) {
1283 QList<Data> posixTrans = getPosixTransitions(beforeMSecsSinceEpoch);
1284 auto it = std::partition_point(posixTrans.cbegin(), posixTrans.cend(),
1285 [beforeMSecsSinceEpoch] (const Data &at) {
1286 return at.atMSecsSinceEpoch < beforeMSecsSinceEpoch;
1287 });
1288 if (it > posixTrans.cbegin())
1289 return *--it;
1290 // It fell between the last transition (if any) and the first of the POSIX rule:
1291 return tranCache().isEmpty() ? Data{} : dataForTzTransition(tranCache().last());
1292 }
1293
1294 // Otherwise if we can find a valid tran then use its rule
1295 auto last = std::partition_point(tranCache().cbegin(), tranCache().cend(),
1296 [beforeMSecsSinceEpoch] (QTzTransitionTime at) {
1297 return at.atMSecsSinceEpoch < beforeMSecsSinceEpoch;
1298 });
1299 return last > tranCache().cbegin() ? dataForTzTransition(*--last) : Data{};
1300}
1301
1302bool QTzTimeZonePrivate::isTimeZoneIdAvailable(QByteArrayView ianaId) const
1303{
1304 // Allow a POSIX rule as long as it has offset data. (This needs to reject a
1305 // plain abbreviation, without offset, since claiming to support such zones
1306 // would prevent the custom QTimeZone constructor from accepting such a
1307 // name, as it doesn't want a custom zone to over-ride a "real" one.)
1308 return tzZones->contains(ianaId) || validatePosixRule(ianaId, true).isValid;
1309}
1310
1311QList<QByteArray> QTzTimeZonePrivate::availableTimeZoneIds() const
1312{
1313 return uniqueSortedAliasPadded(tzZones->keys());
1314}
1315
1316QList<QByteArray> QTzTimeZonePrivate::availableTimeZoneIds(QLocale::Territory territory) const
1317{
1318 QList<QByteArray> result;
1319 for (auto it = tzZones->cbegin(), end = tzZones->cend(); it != end; ++it) {
1320 if (it.value().territory == territory)
1321 result << it.key();
1322 // We'll pick up any CLDR-standard names below, so don't try to map here.
1323 }
1324 std::sort(result.begin(), result.end());
1325
1326 // Since zone.tab only knows about one territory per zone, and is somewhat
1327 // incomplete, we may well miss some zones that CLDR associates with the
1328 // territory. So merge with those from CLDR that we do support.
1329 const auto unWantedZone = [territory](QByteArrayView id) {
1330 // We only want to add zones if they are known and we don't already have them:
1331 auto it = tzZones->constFind(id);
1332 return it == tzZones->end() || it->territory == territory;
1333 };
1334 QList<QByteArrayView> cldrViews = matchingTimeZoneIds(territory);
1335 std::sort(cldrViews.begin(), cldrViews.end());
1336 const auto uniqueEnd = std::unique(cldrViews.begin(), cldrViews.end());
1337 const auto prunedEnd = std::remove_if(cldrViews.begin(), uniqueEnd, unWantedZone);
1338 const auto cldrSize = std::distance(cldrViews.begin(), prunedEnd);
1339 if (cldrSize) {
1340 QList<QByteArray> cldrList;
1341 cldrList.reserve(cldrSize);
1342 for (auto it = cldrViews.begin(); it != prunedEnd; ++it)
1343 cldrList.emplace_back(it->toByteArray());
1344 QList<QByteArray> joined;
1345 joined.reserve(result.size() + cldrSize);
1346 std::set_union(result.begin(), result.end(), cldrList.begin(), cldrList.end(),
1347 std::back_inserter(joined));
1348 result = joined;
1349 }
1350
1351 return result;
1352}
1353
1354// Getting the system zone's ID:
1355
1356namespace {
1357class ZoneNameReader
1358{
1359public:
1360 QByteArray name()
1361 {
1362 /* Assumptions:
1363 a) Systems don't change which of localtime and TZ they use without a
1364 reboot.
1365 b) When they change, they use atomic renames, hence a new device and
1366 inode for the new file.
1367 c) If we change which *name* is used for a zone, while referencing
1368 the same final zoneinfo file, we don't care about the change of
1369 name (e.g. if Europe/Oslo and Europe/Berlin are both symlinks to
1370 the same CET file, continuing to use the old name, after
1371 /etc/localtime changes which of the two it points to, is
1372 harmless).
1373
1374 The alternative would be to use a file-system watcher, but they are a
1375 scarce resource.
1376 */
1377 const StatIdent local = identify("/etc/localtime");
1378 const StatIdent tz = identify("/etc/TZ");
1379 const StatIdent timezone = identify("/etc/timezone");
1380 if (!m_name.isEmpty() && m_last.isValid()
1381 && (m_last == local || m_last == tz || m_last == timezone)) {
1382 return m_name;
1383 }
1384
1385 m_name = etcLocalTime();
1386 if (!m_name.isEmpty()) {
1387 m_last = local;
1388 return m_name;
1389 }
1390
1391 // Some systems (e.g. uClibc) have a default value for $TZ in /etc/TZ:
1392 m_name = etcContent(QStringLiteral("/etc/TZ"));
1393 if (!m_name.isEmpty()) {
1394 m_last = tz;
1395 return m_name;
1396 }
1397
1398 // Gentoo still (2020, QTBUG-87326) uses this:
1399 m_name = etcContent(QStringLiteral("/etc/timezone"));
1400 m_last = m_name.isEmpty() ? StatIdent() : timezone;
1401 return m_name;
1402 }
1403
1404private:
1405 QByteArray m_name;
1406 struct StatIdent
1407 {
1408 static constexpr unsigned long bad = ~0ul;
1409 unsigned long m_dev, m_ino;
1410 constexpr StatIdent() : m_dev(bad), m_ino(bad) {}
1411 StatIdent(const QT_STATBUF &data) : m_dev(data.st_dev), m_ino(data.st_ino) {}
1412 bool isValid() { return m_dev != bad || m_ino != bad; }
1413 friend constexpr bool operator==(StatIdent lhs, StatIdent rhs)
1414 { return lhs.m_dev == rhs.m_dev && lhs.m_ino == rhs.m_ino; }
1415 };
1416 StatIdent m_last;
1417
1418 static StatIdent identify(const char *path)
1419 {
1420 QT_STATBUF data;
1421 return QT_STAT(path, &data) == -1 ? StatIdent() : StatIdent(data);
1422 }
1423
1424 static QByteArray etcLocalTime()
1425 {
1426 // On most distros /etc/localtime is a symlink to a real file so extract
1427 // name from the path
1428 const QString tzdir = qEnvironmentVariable("TZDIR");
1429 constexpr auto zoneinfo = "/zoneinfo/"_L1;
1430 QString path = QStringLiteral("/etc/localtime");
1431 long iteration = getSymloopMax();
1432 // Symlink may point to another symlink etc. before being under zoneinfo/
1433 // We stop on the first path under /zoneinfo/, even if it is itself a
1434 // symlink, like America/Montreal pointing to America/Toronto
1435 do {
1436 path = QFile::symLinkTarget(path);
1437 // If it's a zoneinfo file, extract the zone name from its path:
1438 int index = tzdir.isEmpty() ? -1 : path.indexOf(tzdir);
1439 if (index >= 0) {
1440 const auto tail = QStringView{ path }.sliced(index + tzdir.size()).toUtf8();
1441 return tail.startsWith(u'/') ? tail.sliced(1) : tail;
1442 }
1443 index = path.indexOf(zoneinfo);
1444 if (index >= 0)
1445 return QStringView{ path }.sliced(index + zoneinfo.size()).toUtf8();
1446 } while (!path.isEmpty() && --iteration > 0);
1447
1448 return QByteArray();
1449 }
1450
1451 static QByteArray etcContent(const QString &path)
1452 {
1453 QFile zone(path);
1454 if (zone.open(QIODevice::ReadOnly))
1455 return zone.readAll().trimmed();
1456
1457 return QByteArray();
1458 }
1459
1460 // Any chain of symlinks longer than this is assumed to be a loop:
1461 static long getSymloopMax()
1462 {
1463#ifdef SYMLOOP_MAX
1464 // If defined, at runtime it can only be greater than this, so this is a safe bet:
1465 return SYMLOOP_MAX;
1466#else
1467 errno = 0;
1468 long result = sysconf(_SC_SYMLOOP_MAX);
1469 if (result >= 0)
1470 return result;
1471 // result is -1, meaning either error or no limit
1472 Q_ASSERT(!errno); // ... but it can't be an error, POSIX mandates _SC_SYMLOOP_MAX
1473
1474 // therefore we can make up our own limit
1475# ifdef MAXSYMLINKS
1476 return MAXSYMLINKS;
1477# else
1478 return 8;
1479# endif
1480#endif
1481 }
1482};
1483}
1484
1485QByteArray QTzTimeZonePrivate::systemTimeZoneId() const
1486{
1487 return staticSystemTimeZoneId();
1488}
1489
1490QByteArray QTzTimeZonePrivate::staticSystemTimeZoneId()
1491{
1492 // Check TZ env var first, if not populated try find it
1493 QByteArray ianaId = qgetenv("TZ");
1494
1495 // The TZ value can be ":/etc/localtime" which libc considers
1496 // to be a "default timezone", in which case it will be read
1497 // by one of the blocks below, so unset it here so it is not
1498 // considered as a valid/found ianaId
1499 if (ianaId == ":/etc/localtime")
1500 ianaId.clear();
1501 else if (ianaId.startsWith(':'))
1502 ianaId = ianaId.sliced(1);
1503
1504 if (ianaId.isEmpty()) {
1505 Q_CONSTINIT thread_local static ZoneNameReader reader;
1506 ianaId = reader.name();
1507 }
1508
1509 return ianaId;
1510}
1511
1512QT_END_NAMESPACE
\inmodule QtCore\reentrant
Definition qdatastream.h:50
Definition qlist.h:82
QTzTimeZoneCacheEntry fetchEntry(const QByteArray &ianaId)
static int parsePosixTime(const char *begin, const char *end)
static bool asciiIsQuotedNameChar(char ch)
static auto validatePosixRule(QByteArrayView posixRule, bool requireOffset=false)
#define TZ_MAGIC
static QMap< int, QByteArray > parseTzAbbreviations(QDataStream &ds, int tzh_charcnt, const QList< QTzType > &types)
static std::optional< QList< QTzTransition > > parseTzTransitions(QDataStream &ds, int tzh_timecnt, int tzh_typecnt, bool longTran)
static QTzTimeZoneHash loadTzTimeZones()
Q_GLOBAL_STATIC(const QTzTimeZoneHash, tzZones, loadTzTimeZones())
QHash< QByteArray, QTzTimeZone > QTzTimeZoneHash
static int parsePosixTransitionTime(QLatin1StringView timeRule)
static QDate calculatePosixDate(QLatin1StringView dateRule, int year)
#define TZ_MAX_CHARS
static bool isTzFile(const QString &name)
static void parseTzLeapSeconds(QDataStream &ds, int tzh_leapcnt, bool longTran)
static QTzHeader parseTzHeader(QDataStream &ds, bool *ok)
static bool asciiIsLetter(char ch)
static QDate calculateDowDate(int year, int month, int dayOfWeek, int week)
#define TZ_MAX_TYPES
static bool openZoneInfo(const QString &name, QFile *file)
Q_DECLARE_TYPEINFO(QTzType, Q_PRIMITIVE_TYPE)
#define TZ_MAX_TIMES
static QList< QTzType > parseTzIndicators(QDataStream &ds, const QList< QTzType > &types, int tzh_ttisstdcnt, int tzh_ttisgmtcnt)
#define TZ_MAX_LEAPS
Q_DECLARE_TYPEINFO(QTzTransition, Q_PRIMITIVE_TYPE)
static QList< QTzType > parseTzTypes(QDataStream &ds, int tzh_typecnt)
static QList< QTimeZonePrivate::Data > calculatePosixTransitions(const QByteArray &posixRule, int startYear, int endYear, qint64 lastTranMSecs)
static int parsePosixOffset(const char *begin, const char *end)
static QByteArray parseTzPosixRule(QDataStream &ds)
QLocale::Territory territory