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

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