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