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