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