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

Provided by KDAB

Privacy Policy
Learn Advanced QML with KDAB
Find out more

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