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

source code of qtbase/src/corelib/time/qtimezoneprivate_tz.cpp