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