Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qtimezoneprivate_win.cpp
Go to the documentation of this file.
1// Copyright (C) 2022 The Qt Company Ltd.
2// Copyright (C) 2013 John Layt <jlayt@kde.org>
3// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
4// Qt-Security score:significant reason:default
5
6#include "qtimezone.h"
8
9#include "qdatetime.h"
10#include "qdebug.h"
11#include <private/qnumeric_p.h>
12#include <private/wcharhelpers_win_p.h>
13
14#include <algorithm>
15
16#include <private/qwinregistry_p.h>
17
19
20QT_BEGIN_NAMESPACE
21
22using namespace Qt::StringLiterals;
23
24/*
25 Private
26
27 Windows system implementation
28*/
29
30#define MAX_KEY_LENGTH 255
31
32// MSDN home page for Time support
33// http://msdn.microsoft.com/en-us/library/windows/desktop/ms724962%28v=vs.85%29.aspx
34
35// For Windows XP and later refer to MSDN docs on TIME_ZONE_INFORMATION structure
36// http://msdn.microsoft.com/en-gb/library/windows/desktop/ms725481%28v=vs.85%29.aspx
37
38// Vista introduced support for historic data, see MSDN docs on DYNAMIC_TIME_ZONE_INFORMATION
39// http://msdn.microsoft.com/en-gb/library/windows/desktop/ms724253%28v=vs.85%29.aspx
40static const wchar_t tzRegPath[] = LR"(SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones)";
41static const wchar_t currTzRegPath[] = LR"(SYSTEM\CurrentControlSet\Control\TimeZoneInformation)";
42
43constexpr qint64 MSECS_PER_DAY = 86400000LL;
44constexpr qint64 JULIAN_DAY_FOR_EPOCH = 2440588LL; // result of julianDayFromDate(1970, 1, 1)
45
46/* Ignore any claims of DST before 1900.
47
48 Daylight-Saving time adjustments were first proposed in 1895 (George Vernon
49 Hudson in New Zealand) and 1905 (William Willett in the UK) and first adopted
50 in 1908 (one town in Ontario, Canada) and 1916 (Germany). Since MS's data
51 tends to pretend the rules in force in 1970ish (or later) had always been in
52 effect, which presents difficulties for the code that selects correct data
53 (for a time close to the earliest we can represent), always ignore any claim
54 a first rule may make of DST starting any time before 1900.
55
56 For southern-hemisphere zones, this implies that a rule claiming 1900 started
57 in DST is overruled to merely start DST later in 1900, having spent the whole
58 part of 1900 prior to that in standard time. This erases 1900's earlier
59 transition out of daylight-saving time so as to prevent a fake change of
60 offset at the start of the year, since 1899 shall be treated as observing
61 standard time throughout.
62
63 In the unlikely event of MS supplying a change in standard time before 1900,
64 however, that should be faithfully represented. If that ever happens, trust
65 that MS gets the start year of any subsequend DST right.
66
67 See:
68 * https://www.timeanddate.com/time/dst/history.html
69 * https://en.wikipedia.org/wiki/Daylight_saving_time#History
70*/
71constexpr int FIRST_DST_YEAR = 1900;
72
73// Copied from MSDN, see above for link
82
83namespace {
84
85// Fast and reliable conversion from msecs to date for all values
86// Adapted from QDateTime msecsToDate
87QDate msecsToDate(qint64 msecs)
88{
89 qint64 jd = JULIAN_DAY_FOR_EPOCH;
90 // Corner case: don't use qAbs() because msecs may be numeric_limits<qint64>::min()
91 if (msecs >= MSECS_PER_DAY || msecs <= -MSECS_PER_DAY) {
92 jd += msecs / MSECS_PER_DAY;
93 msecs %= MSECS_PER_DAY;
94 }
95
96 if (msecs < 0) {
97 Q_ASSERT(msecs > -MSECS_PER_DAY);
98 --jd;
99 }
100
101 return QDate::fromJulianDay(jd);
102}
103
104bool equalSystemtime(const SYSTEMTIME &t1, const SYSTEMTIME &t2)
105{
106 return (t1.wYear == t2.wYear
107 && t1.wMonth == t2.wMonth
108 && t1.wDay == t2.wDay
109 && t1.wDayOfWeek == t2.wDayOfWeek
110 && t1.wHour == t2.wHour
111 && t1.wMinute == t2.wMinute
112 && t1.wSecond == t2.wSecond
113 && t1.wMilliseconds == t2.wMilliseconds);
114}
115
116bool equalTzi(const TIME_ZONE_INFORMATION &tzi1, const TIME_ZONE_INFORMATION &tzi2)
117{
118 return(tzi1.Bias == tzi2.Bias
119 && tzi1.StandardBias == tzi2.StandardBias
120 && equalSystemtime(tzi1.StandardDate, tzi2.StandardDate)
121 && wcscmp(tzi1.StandardName, tzi2.StandardName) == 0
122 && tzi1.DaylightBias == tzi2.DaylightBias
123 && equalSystemtime(tzi1.DaylightDate, tzi2.DaylightDate)
124 && wcscmp(tzi1.DaylightName, tzi2.DaylightName) == 0);
125}
126
127QWinTimeZonePrivate::QWinTransitionRule readRegistryRule(const HKEY &key,
128 const wchar_t *value, bool *ok)
129{
130 *ok = false;
131 QWinTimeZonePrivate::QWinTransitionRule rule;
132 REG_TZI_FORMAT tzi;
133 DWORD tziSize = sizeof(tzi);
134 if (RegQueryValueEx(key, value, nullptr, nullptr, reinterpret_cast<BYTE *>(&tzi), &tziSize)
135 == ERROR_SUCCESS) {
136 rule.startYear = 0;
137 rule.standardTimeBias = tzi.Bias + tzi.StandardBias;
138 rule.daylightTimeBias = tzi.Bias + tzi.DaylightBias - rule.standardTimeBias;
139 rule.standardTimeRule = tzi.StandardDate;
140 rule.daylightTimeRule = tzi.DaylightDate;
141 *ok = true;
142 }
143 return rule;
144}
145
146TIME_ZONE_INFORMATION getRegistryTzi(const QByteArray &windowsId, bool *ok)
147{
148 *ok = false;
149 TIME_ZONE_INFORMATION tzi;
150 REG_TZI_FORMAT regTzi;
151 DWORD regTziSize = sizeof(regTzi);
152 const QString tziKeyPath = QString::fromWCharArray(tzRegPath) + u'\\'
153 + QString::fromUtf8(windowsId);
154
155 QWinRegistryKey key(HKEY_LOCAL_MACHINE, tziKeyPath);
156 if (key.isValid()) {
157 DWORD size = sizeof(tzi.DaylightName);
158 RegQueryValueEx(key, L"Dlt", nullptr, nullptr, reinterpret_cast<LPBYTE>(tzi.DaylightName), &size);
159
160 size = sizeof(tzi.StandardName);
161 RegQueryValueEx(key, L"Std", nullptr, nullptr, reinterpret_cast<LPBYTE>(tzi.StandardName), &size);
162
163 if (RegQueryValueEx(key, L"TZI", nullptr, nullptr, reinterpret_cast<BYTE *>(&regTzi), &regTziSize)
164 == ERROR_SUCCESS) {
165 tzi.Bias = regTzi.Bias;
166 tzi.StandardBias = regTzi.StandardBias;
167 tzi.DaylightBias = regTzi.DaylightBias;
168 tzi.StandardDate = regTzi.StandardDate;
169 tzi.DaylightDate = regTzi.DaylightDate;
170 *ok = true;
171 }
172 }
173
174 return tzi;
175}
176
177bool isSameRule(const QWinTimeZonePrivate::QWinTransitionRule &last,
178 const QWinTimeZonePrivate::QWinTransitionRule &rule)
179{
180 // In particular, when this is true and either wYear is 0, so is the other;
181 // so if one rule is recurrent and they're equal, so is the other. If
182 // either rule *isn't* recurrent, it has non-0 wYear which shall be
183 // different from the other's. Note that we don't compare .startYear, since
184 // that will always be different.
185 return equalSystemtime(last.standardTimeRule, rule.standardTimeRule)
186 && equalSystemtime(last.daylightTimeRule, rule.daylightTimeRule)
187 && last.standardTimeBias == rule.standardTimeBias
188 && last.daylightTimeBias == rule.daylightTimeBias;
189}
190
191QList<QByteArray> availableWindowsIds()
192{
193 static const QList<QByteArray> cache = [] {
194 QList<QByteArray> list;
195 QWinRegistryKey key(HKEY_LOCAL_MACHINE, tzRegPath);
196 if (key.isValid()) {
197 DWORD idCount = 0;
198 if (RegQueryInfoKey(key, 0, 0, 0, &idCount, 0, 0, 0, 0, 0, 0, 0) == ERROR_SUCCESS
199 && idCount > 0) {
200 for (DWORD i = 0; i < idCount; ++i) {
201 DWORD maxLen = MAX_KEY_LENGTH;
202 TCHAR buffer[MAX_KEY_LENGTH];
203 if (RegEnumKeyEx(key, i, buffer, &maxLen, 0, 0, 0, 0) == ERROR_SUCCESS)
204 list.append(QString::fromWCharArray(buffer).toUtf8());
205 }
206 }
207 }
208 return list;
209 }();
210 return cache;
211}
212
213QByteArray windowsSystemZoneId()
214{
215 // On Vista and later is held in the value TimeZoneKeyName in key currTzRegPath
216 const QString id = QWinRegistryKey(HKEY_LOCAL_MACHINE, currTzRegPath)
217 .stringValue(L"TimeZoneKeyName");
218 if (!id.isEmpty())
219 return id.toUtf8();
220
221 // On XP we have to iterate over the zones until we find a match on
222 // names/offsets with the current data
223 TIME_ZONE_INFORMATION sysTzi;
224 GetTimeZoneInformation(&sysTzi);
225 bool ok = false;
226 const auto winIds = availableWindowsIds();
227 for (const QByteArray &winId : winIds) {
228 if (equalTzi(getRegistryTzi(winId, &ok), sysTzi))
229 return winId;
230 }
231
232 // If we can't determine the current ID use UTC
233 return QTimeZonePrivate::utcQByteArray();
234}
235
236QDate calculateTransitionLocalDate(const SYSTEMTIME &rule, int year)
237{
238 // If month is 0 then there is no date
239 if (rule.wMonth == 0)
240 return QDate();
241
242 // Interpret SYSTEMTIME according to the slightly quirky rules in:
243 // https://msdn.microsoft.com/en-us/library/windows/desktop/ms725481(v=vs.85).aspx
244
245 // If the year is set, the rule gives an absolute date:
246 if (rule.wYear)
247 return QDate(rule.wYear, rule.wMonth, rule.wDay);
248
249 // Otherwise, the rule date is annual and relative:
250 const int dayOfWeek = rule.wDayOfWeek == 0 ? 7 : rule.wDayOfWeek;
251 QDate date(year, rule.wMonth, 1);
252 Q_ASSERT(date.isValid());
253 // How many days before was last dayOfWeek before target month ?
254 int adjust = dayOfWeek - date.dayOfWeek(); // -6 <= adjust < 7
255 if (adjust >= 0) // Ensure -7 <= adjust < 0:
256 adjust -= 7;
257 // Normally, wDay is day-within-month; but here it is 1 for the first
258 // of the given dayOfWeek in the month, through 4 for the fourth or ...
259 adjust += (rule.wDay < 1 ? 1 : rule.wDay > 4 ? 5 : rule.wDay) * 7;
260 date = date.addDays(adjust);
261 // ... 5 for the last; so back up by weeks to get within the month:
262 if (date.month() != rule.wMonth) {
263 Q_ASSERT(rule.wDay > 4);
264 // (Note that, with adjust < 0, date <= 28th of our target month
265 // is guaranteed when wDay <= 4, or after our first -7 here.)
266 date = date.addDays(-7);
267 Q_ASSERT(date.month() == rule.wMonth);
268 }
269 return date;
270}
271
272// Converts a date/time value into msecs, returns true on overflow:
273inline bool timeToMSecs(QDate date, QTime time, qint64 *msecs)
274{
275 qint64 dayms = 0;
276 qint64 daySinceEpoch = date.toJulianDay() - JULIAN_DAY_FOR_EPOCH;
277 qint64 msInDay = time.msecsSinceStartOfDay();
278 if (daySinceEpoch < 0 && msInDay > 0) {
279 // In the earliest day with representable parts, take care to not
280 // underflow before an addition that would have fixed it.
281 ++daySinceEpoch;
282 msInDay -= MSECS_PER_DAY;
283 }
284 return qMulOverflow(daySinceEpoch, std::integral_constant<qint64, MSECS_PER_DAY>(), &dayms)
285 || qAddOverflow(dayms, msInDay, msecs);
286}
287
288qint64 calculateTransitionForYear(const SYSTEMTIME &rule, int year, int bias)
289{
290 // TODO Consider caching the calculated values - i.e. replace SYSTEMTIME in
291 // WinTransitionRule; do this in init() once and store the results.
292 Q_ASSERT(year);
293 const QDate date = calculateTransitionLocalDate(rule, year);
294 const QTime time = QTime(rule.wHour, rule.wMinute, rule.wSecond);
295 qint64 msecs = 0;
296 if (date.isValid() && time.isValid() && !timeToMSecs(date, time, &msecs)) {
297 // If bias pushes us outside the representable range, clip to range
298 // (overflow went past the end bias pushed us towards; and
299 // invalidMSecs() is a representable value less than minMSecs()):
300 return bias && qAddOverflow(msecs, qint64(bias) * 60000, &msecs)
301 ? (bias < 0 ? QTimeZonePrivate::minMSecs() : QTimeZonePrivate::maxMSecs())
302 : qMax(QTimeZonePrivate::minMSecs(), msecs);
303 }
304 return QTimeZonePrivate::invalidMSecs();
305}
306
307// True precisely if transition represents the start of the year.
308bool isAtStartOfYear(const SYSTEMTIME &transition, int year)
309{
310 /*
311 Note that, here, wDay identifies an instance of a given day-of-week in the
312 month, with 5 meaning last. (December 31st is, incidentally, always the
313 fifth instance of its day of the week in its month. But we aren't testing
314 that - see below.)
315
316 QDate represents Sunday by 7, SYSTEMTIME by 0; so compare day of the week
317 by taking difference mod 7.
318 */
319 return transition.wMonth == 1 && transition.wDay == 1
320 && (QDate(year, 1, 1).dayOfWeek() - transition.wDayOfWeek) % 7 == 0
321 && transition.wHour == 0 && transition.wMinute == 0 && transition.wSecond == 0;
322}
323
324struct TransitionTimePair
325{
326 // Transition times, in ms:
327 qint64 std, dst;
328 // If either is invalidMSecs(), which shall then be < the other, there is no
329 // DST and the other describes a change in actual standard offset.
330 bool fakesDst = false;
331
332 TransitionTimePair(const QWinTimeZonePrivate::QWinTransitionRule &rule,
333 int year, int oldYearOffset)
334 // The local time in Daylight Time of the switch to Standard Time
335 : std(calculateTransitionForYear(rule.standardTimeRule, year,
336 rule.standardTimeBias + rule.daylightTimeBias)),
337 // The local time in Standard Time of the switch to Daylight Time
338 dst(calculateTransitionForYear(rule.daylightTimeRule, year, rule.standardTimeBias))
339 {
340 /*
341 Check for potential "fake DST", used by MS's APIs because the
342 TIME_ZONE_INFORMATION spec either expresses no transitions in the
343 year, or expresses a transition of each kind, even if standard time
344 did change in a year with no DST. We've seen year-start fake-DST
345 (whose offset matches prior standard offset, in which the previous
346 year ended).
347
348 It is possible there might also be year-end fake-DST but Bangladesh
349 toyed with DST from 2009-06-19 (a Friday) at 23:00 until, according to
350 the Olson database, 2009-12-32 24:00; however, MS represents that by
351 the last millisecond of the year, technically a millisecond early. (MS
352 falsely claims Bhutan did the same.) So we do not attempt to detect an
353 end-of-year fake transition; nor is there any reason to suppose MS
354 would need to do that, as anything it could implement thereby could
355 equally be implemented by a start-of-year fake.
356
357 A fake transition at the start of the year tells us what the offset at
358 the start of the year is; if this doesn't match the offset in effect
359 at the end of the previous year, then it's a real transition. If it
360 does match, then we have a fake transition. (A fake transition of one
361 kind at the end of the year would be paired with a real transition,
362 allegedly of the other kind, part way through the year; that would be
363 a transition away from the offset that would nominally be restored by
364 the fake so, again, the year would have started with the post-fake
365 offset in effect.)
366
367 Either the alleged standardTimeRule or the alleged daylightTimeRule
368 may be faked; either way, the transition is actually a change to the
369 current standard offset; but the unfaked half of the rule contains the
370 useful bias data, so we have to go along with its lies. Clients of
371 this class should still use DaylightTime and StandardTime as if the
372 fake were not a lie, selecting which side of the real transition to
373 use the data for, and ruleToData() will take care of extracting the
374 right offset based on that, while tagging the resulting Data as
375 standard time.
376
377 Example: Russia/Moscow
378 Format: -bias +( -stdBias, stdDate | -dstBias, dstDate ) notes
379 Last year of DST, 2010: 180 +( 0, 0-10-5 3:0 | 60, 0-3-5 2:0 ) normal DST
380 Zone change in 2011: 180 +( 0, 0-1-1 0:0 | 60 0-3-5 2:0 ) fake DST at transition
381 Fixed standard in 2012: 240 +( 0, 0-0-0 0:0 | 60, 0-0-0 0:0 ) standard time years
382 Zone change in 2014: 180 +( 0, 0-10-5 2:0 | 60, 0-1-1 0:0 ) fake DST at year-start
383 The last of these is missing on Win7 VMs (too old to know about it).
384 */
385 if (rule.standardTimeBias + rule.daylightTimeBias == oldYearOffset
386 && isAtStartOfYear(rule.daylightTimeRule, year)) {
387 dst = QTimeZonePrivate::invalidMSecs();
388 fakesDst = true;
389 }
390 if (rule.standardTimeBias == oldYearOffset
391 && isAtStartOfYear(rule.standardTimeRule, year)) {
392 Q_ASSERT_X(!fakesDst, "TransitionTimePair",
393 "Year with (DST bias zero and) both transitions fake !");
394 std = QTimeZonePrivate::invalidMSecs();
395 fakesDst = true;
396 }
397 }
398
399 bool startsInDst() const
400 {
401 // Year starts in daylightTimeRule iff it has a valid transition out of
402 // DST with no earlier valid transition into it.
403 return std != QTimeZonePrivate::invalidMSecs()
404 && (std < dst || dst == QTimeZonePrivate::invalidMSecs());
405 }
406
407 // Returns true if (assuming this pair was derived from the first rule, and
408 // that has non-zero wMonth values, so is a DST-recurrence or faking it) the
409 // given millis, presumed to be in the given year, is before the first
410 // transition into DST.
411 bool beforeInitialDst(int year, qint64 millis) const
412 {
413 return !fakesDst && (year == FIRST_DST_YEAR ? millis < dst : year < FIRST_DST_YEAR);
414 }
415
416 QTimeZonePrivate::Data ruleToData(const QWinTimeZonePrivate::QWinTransitionRule &rule,
417 const QWinTimeZonePrivate *tzp, bool isDst) const
418 {
419 const auto type = isDst ? QTimeZone::DaylightTime : QTimeZone::StandardTime;
420 auto time = isDst ? dst : std;
421 // The isDst we're asked for may be set to the valid one of dst and
422 // std, when fake, but not always - so make sure:
423 if (fakesDst && time == QTimeZonePrivate::invalidMSecs())
424 time = isDst ? std : dst;
425 return tzp->ruleToData(rule, time, type, fakesDst);
426 }
427};
428
429int yearEndOffset(const QWinTimeZonePrivate::QWinTransitionRule &rule, int year)
430{
431 Q_ASSERT(year);
432 int offset = rule.standardTimeBias;
433 // Only needed to help another TransitionTimePair work out year + 1's start
434 // offset; and the oldYearOffset we use only affects an alleged transition
435 // at the *start* of this year, so it doesn't matter if we guess wrong here:
436 TransitionTimePair pair(rule, year, offset);
437 if (pair.dst > pair.std)
438 offset += rule.daylightTimeBias;
439 return offset;
440}
441
442QLocale::Territory userTerritory()
443{
444 const GEOID id = GetUserGeoID(GEOCLASS_NATION);
445 wchar_t code[3];
446 const int size = GetGeoInfo(id, GEO_ISO2, code, 3, 0);
447 return (size == 3) ? QLocalePrivate::codeToTerritory(QStringView(code, size))
448 : QLocale::AnyTerritory;
449}
450
451// Index of last rule in rules with .startYear <= year, or 0 if none satisfies that:
452int ruleIndexForYear(const QList<QWinTimeZonePrivate::QWinTransitionRule> &rules, int year)
453{
454 if (rules.last().startYear <= year)
455 return rules.count() - 1;
456 // We don't have a rule for before the first, but the first is the best we can offer:
457 if (rules.first().startYear > year)
458 return 0;
459
460 // Otherwise, use binary chop:
461 int lo = 0, hi = rules.count();
462 // invariant: rules[i].startYear <= year < rules[hi].startYear
463 // subject to treating rules[rules.count()] as "off the end of time"
464 while (lo + 1 < hi) {
465 const int mid = (lo + hi) / 2;
466 // lo + 2 <= hi, so lo + 1 <= mid <= hi - 1, so lo < mid < hi
467 // In particular, mid < rules.count()
468 const int midYear = rules.at(mid).startYear;
469 if (midYear > year)
470 hi = mid;
471 else if (midYear < year)
472 lo = mid;
473 else // No two rules have the same startYear:
474 return mid;
475 }
476 return lo;
477}
478
479} // anonymous namespace
480
481// Create the system default time zone
482QWinTimeZonePrivate::QWinTimeZonePrivate()
483 : QTimeZonePrivate()
484{
485 init(QByteArray());
486}
487
488// Create a named time zone
489QWinTimeZonePrivate::QWinTimeZonePrivate(const QByteArray &ianaId)
490 : QTimeZonePrivate()
491{
492 init(ianaId);
493}
494
495QWinTimeZonePrivate::~QWinTimeZonePrivate()
496{
497}
498
499QWinTimeZonePrivate *QWinTimeZonePrivate::clone() const
500{
501 return new QWinTimeZonePrivate(*this);
502}
503
504void QWinTimeZonePrivate::init(const QByteArray &ianaId)
505{
506 if (ianaId.isEmpty()) {
507 m_windowsId = windowsSystemZoneId();
508 m_id = systemTimeZoneId();
509 } else {
510 m_windowsId = ianaIdToWindowsId(ianaId);
511 m_id = ianaId;
512 }
513 const auto initialYear = [](const QWinTransitionRule &rule) {
514 // Only applicable to the first rule, and only if not faking DST.
515 // The rule starts in FIRST_DST_YEAR if it is a DST recurrence (with
516 // non-zero wMonth fields), otherwise read as a constant
517 // offset rule dating back to the start of time.
518 return (rule.standardTimeRule.wMonth > 0 || rule.daylightTimeRule.wMonth > 0
519 ? FIRST_DST_YEAR : int(QDateTime::YearRange::First));
520 };
521
522 bool badMonth = false; // Only warn once per zone, if at all.
523 if (!m_windowsId.isEmpty()) {
524 // Open the base TZI for the time zone
525 const QString baseKeyPath = QString::fromWCharArray(tzRegPath) + u'\\'
526 + QString::fromUtf8(m_windowsId);
527 QWinRegistryKey baseKey(HKEY_LOCAL_MACHINE, baseKeyPath);
528 if (baseKey.isValid()) {
529 // Load the localized names
530 m_displayName = baseKey.stringValue(L"Display");
531 m_standardName = baseKey.stringValue(L"Std");
532 m_daylightName = baseKey.stringValue(L"Dlt");
533 // On Vista and later the optional dynamic key holds historic data
534 const QString dynamicKeyPath = baseKeyPath + "\\Dynamic DST"_L1;
535 QWinRegistryKey dynamicKey(HKEY_LOCAL_MACHINE, dynamicKeyPath);
536 if (dynamicKey.isValid()) {
537 // Find out the start and end years stored, then iterate over them
538 const int startYear = dynamicKey.value<int>(L"FirstEntry").value_or(0);
539 const int endYear = dynamicKey.value<int>(L"LastEntry").value_or(0);
540 for (int year = startYear; year <= endYear; ++year) {
541 bool ruleOk;
542 QWinTransitionRule rule = readRegistryRule(dynamicKey,
543 qt_castToWchar(QString::number(year)),
544 &ruleOk);
545 if (ruleOk
546 // Don't repeat a recurrent rule:
547 && (m_tranRules.isEmpty()
548 || !isSameRule(m_tranRules.last(), rule))) {
549 if (!badMonth
550 && (rule.standardTimeRule.wMonth == 0)
551 != (rule.daylightTimeRule.wMonth == 0)) {
552 badMonth = true;
553 qWarning("MS registry TZ API violated its wMonth constraint;"
554 "this may cause mistakes for %s from %d",
555 ianaId.constData(), year);
556 }
557 const TransitionTimePair pair(rule, year, rule.standardTimeBias);
558 // First rule may be a standard offset change, for which fakesDst is true.
559 rule.startYear
560 = m_tranRules.size() || pair.fakesDst ? year : initialYear(rule);
561 m_tranRules.append(rule);
562 }
563 }
564 } else {
565 // No dynamic data so use the base data
566 bool ruleOk;
567 QWinTransitionRule rule = readRegistryRule(baseKey, L"TZI", &ruleOk);
568 if (ruleOk) {
569 rule.startYear = initialYear(rule);
570 m_tranRules.append(rule);
571 }
572 }
573 }
574 }
575
576 // If there are no rules then we failed to find a windowsId or any tzi info
577 if (m_tranRules.size() == 0) {
578 m_id.clear();
579 m_windowsId.clear();
580 m_displayName.clear();
581 } else if (m_id.isEmpty()) {
582 m_id = m_standardName.toUtf8();
583 }
584}
585
586QString QWinTimeZonePrivate::comment() const
587{
588 return m_displayName;
589}
590
591QString QWinTimeZonePrivate::displayName(QTimeZone::TimeType timeType,
592 QTimeZone::NameType nameType,
593 const QLocale &locale) const
594{
595 // Registry gave us long names for the system locale:
596 if (nameType == QTimeZone::LongName && locale == QLocale::system()) {
597 switch (timeType) {
598 case QTimeZone::DaylightTime :
599 return m_daylightName;
600 case QTimeZone::GenericTime :
601 return m_displayName;
602 case QTimeZone::StandardTime :
603 return m_standardName;
604 }
605 }
606 // Fall back to base class for everything else.
607 return QTimeZonePrivate::displayName(timeType, nameType, locale);;
608}
609
610QString QWinTimeZonePrivate::abbreviation(qint64 atMSecsSinceEpoch) const
611{
612 return data(atMSecsSinceEpoch).abbreviation;
613}
614
615int QWinTimeZonePrivate::offsetFromUtc(qint64 atMSecsSinceEpoch) const
616{
617 return data(atMSecsSinceEpoch).offsetFromUtc;
618}
619
620int QWinTimeZonePrivate::standardTimeOffset(qint64 atMSecsSinceEpoch) const
621{
622 return data(atMSecsSinceEpoch).standardTimeOffset;
623}
624
625int QWinTimeZonePrivate::daylightTimeOffset(qint64 atMSecsSinceEpoch) const
626{
627 return data(atMSecsSinceEpoch).daylightTimeOffset;
628}
629
630bool QWinTimeZonePrivate::hasDaylightTime() const
631{
632 return hasTransitions();
633}
634
635bool QWinTimeZonePrivate::isDaylightTime(qint64 atMSecsSinceEpoch) const
636{
637 return (data(atMSecsSinceEpoch).daylightTimeOffset != 0);
638}
639
640QTimeZonePrivate::Data QWinTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const
641{
642 int year = msecsToDate(forMSecsSinceEpoch).year();
643 for (int ruleIndex = ruleIndexForYear(m_tranRules, year);
644 ruleIndex >= 0; --ruleIndex) {
645 const QWinTransitionRule &rule = m_tranRules.at(ruleIndex);
646 Q_ASSERT(ruleIndex == 0 || year >= rule.startYear);
647 if (year < rule.startYear
648 || !(rule.standardTimeRule.wMonth > 0 || rule.daylightTimeRule.wMonth > 0)) {
649 // No transition (or before first rule), no DST, use the rule's standard time.
650 return ruleToData(rule, forMSecsSinceEpoch, QTimeZone::StandardTime);
651 }
652
653 int prior = year == 1 ? -1 : year - 1; // No year 0.
654 const int endYear = qMax(rule.startYear, prior);
655 while (year >= endYear) {
656 const int newYearOffset = (prior < rule.startYear && ruleIndex > 0)
657 ? yearEndOffset(m_tranRules.at(ruleIndex - 1), prior)
658 : yearEndOffset(rule, prior);
659 const TransitionTimePair pair(rule, year, newYearOffset);
660 bool isDst = false;
661 if (ruleIndex == 0 && pair.beforeInitialDst(year, forMSecsSinceEpoch)) {
662 // We're before DST first started and have no earlier rule that
663 // might give better data on this year, so just extrapolate
664 // standard time backwards.
665 } else if (pair.std != invalidMSecs() && pair.std <= forMSecsSinceEpoch) {
666 isDst = pair.std < pair.dst && pair.dst <= forMSecsSinceEpoch;
667 } else if (pair.dst != invalidMSecs() && pair.dst <= forMSecsSinceEpoch) {
668 isDst = true;
669 } else {
670 year = prior; // Try an earlier year for this rule (once).
671 prior = year == 1 ? -1 : year - 1; // No year 0.
672 continue;
673 }
674 return ruleToData(rule, forMSecsSinceEpoch,
675 isDst ? QTimeZone::DaylightTime : QTimeZone::StandardTime,
676 pair.fakesDst);
677 }
678 // We can only fall off the end of that loop if endYear is rule.startYear:
679 Q_ASSERT(year < rule.startYear);
680 // Fell off start of rule, try previous rule.
681 }
682 // We don't have relevant data :-(
683 return {};
684}
685
686bool QWinTimeZonePrivate::hasTransitions() const
687{
688 for (const QWinTransitionRule &rule : m_tranRules) {
689 if (rule.standardTimeRule.wMonth > 0 && rule.daylightTimeRule.wMonth > 0)
690 return true;
691 }
692 return false;
693}
694
695QTimeZonePrivate::Data QWinTimeZonePrivate::nextTransition(qint64 afterMSecsSinceEpoch) const
696{
697 int year = msecsToDate(afterMSecsSinceEpoch).year();
698 int newYearOffset = invalidSeconds();
699 for (int ruleIndex = ruleIndexForYear(m_tranRules, year);
700 ruleIndex < m_tranRules.count(); ++ruleIndex) {
701 const QWinTransitionRule &rule = m_tranRules.at(ruleIndex);
702 // Does this rule's period include any transition at all ?
703 if (rule.standardTimeRule.wMonth > 0 || rule.daylightTimeRule.wMonth > 0) {
704 int prior = year == 1 ? -1 : year - 1; // No year 0.
705 if (newYearOffset == invalidSeconds()) {
706 // First rule tried. (Will revise newYearOffset before any
707 // fall-back to a later rule.)
708 newYearOffset = (prior < rule.startYear && ruleIndex > 0)
709 ? yearEndOffset(m_tranRules.at(ruleIndex - 1), prior)
710 : yearEndOffset(rule, prior);
711 }
712 if (year < rule.startYear) {
713 // Either before first rule's start, or we fell off the end of
714 // the rule for year because afterMSecsSinceEpoch is after any
715 // transitions in it. Find first transition in this rule.
716 TransitionTimePair pair(rule, rule.startYear, newYearOffset);
717 // First transition is to DST precisely if the year started in
718 // standard time. If the year is FIRST_DST_YEAR or earlier, it
719 // definitely started in standard time.
720 return pair.ruleToData(rule, this, !(year > FIRST_DST_YEAR && pair.startsInDst()));
721 }
722 const int endYear = ruleIndex + 1 < m_tranRules.count()
723 ? qMin(m_tranRules.at(ruleIndex + 1).startYear, year + 2) : (year + 2);
724 while (year < endYear) {
725 const TransitionTimePair pair(rule, year, newYearOffset);
726 bool isDst = false;
727 Q_ASSERT(invalidMSecs() <= afterMSecsSinceEpoch); // invalid is min qint64
728 if (ruleIndex == 0 && pair.beforeInitialDst(year, afterMSecsSinceEpoch)) {
729 // This is an initial recurrence rule, whose startYear
730 // (which we know is <= year) is FIRST_DST_YEAR:
731 Q_ASSERT(year == FIRST_DST_YEAR);
732 // This year's DST transition is the first ever DST
733 // transition, and we're before it. The transition back to
734 // standard time is a lie unless the DST one comes before
735 // it; either way, the DST one is next.
736 isDst = true;
737 } else if (pair.std > afterMSecsSinceEpoch) {
738 isDst = pair.std > pair.dst && pair.dst > afterMSecsSinceEpoch;
739 } else if (pair.dst > afterMSecsSinceEpoch) {
740 isDst = true;
741 } else {
742 newYearOffset = rule.standardTimeBias;
743 if (pair.dst > pair.std)
744 newYearOffset += rule.daylightTimeBias;
745 // Try a later year for this rule (once).
746 prior = year;
747 year = year == -1 ? 1 : year + 1; // No year 0
748 continue;
749 }
750
751 return pair.ruleToData(rule, this, isDst);
752 }
753 // Fell off end of rule, try next rule.
754 } else {
755 // No transition during rule's period. If this is our first rule,
756 // record its standard time as newYearOffset for the next rule;
757 // otherwise, it should be consistent with what we have.
758 if (newYearOffset == invalidSeconds())
759 newYearOffset = rule.standardTimeBias;
760 else
761 Q_ASSERT(newYearOffset == rule.standardTimeBias);
762 }
763 }
764 // Apparently no transition after the given time:
765 return {};
766}
767
768QTimeZonePrivate::Data QWinTimeZonePrivate::previousTransition(qint64 beforeMSecsSinceEpoch) const
769{
770 if (beforeMSecsSinceEpoch <= minMSecs())
771 return {};
772
773 int year = msecsToDate(beforeMSecsSinceEpoch).year();
774 for (int ruleIndex = ruleIndexForYear(m_tranRules, year);
775 ruleIndex >= 0; --ruleIndex) {
776 const QWinTransitionRule &rule = m_tranRules.at(ruleIndex);
777 Q_ASSERT(ruleIndex == 0 || year >= rule.startYear);
778 // Does this rule's period include any transition at all ?
779 if (year >= rule.startYear
780 && (rule.standardTimeRule.wMonth > 0 || rule.daylightTimeRule.wMonth > 0)) {
781 int prior = year == 1 ? -1 : year - 1; // No year 0.
782 const int endYear = qMax(rule.startYear, prior);
783 while (year >= endYear) {
784 const int newYearOffset = (prior < rule.startYear && ruleIndex > 0)
785 ? yearEndOffset(m_tranRules.at(ruleIndex - 1), prior)
786 : yearEndOffset(rule, prior);
787 const TransitionTimePair pair(rule, year, newYearOffset);
788 // A recurrent DST rule, before DST first started, is a lie:
789 // fake a first transition at the start of time, as for the
790 // other (ruleIndex == 0) case below. Same applies to first
791 // instant of DST; there is no prior (real) transition.
792 if (ruleIndex == 0 && pair.beforeInitialDst(year, beforeMSecsSinceEpoch - 1))
793 return ruleToData(rule, minMSecs(), QTimeZone::StandardTime, false);
794
795 bool isDst = false;
796 if (pair.std != invalidMSecs() && pair.std < beforeMSecsSinceEpoch) {
797 isDst = pair.std < pair.dst && pair.dst < beforeMSecsSinceEpoch;
798 } else if (pair.dst != invalidMSecs() && pair.dst < beforeMSecsSinceEpoch) {
799 isDst = true;
800 } else {
801 year = prior; // Try an earlier year for this rule (once).
802 prior = year == 1 ? -1 : year - 1; // No year 0.
803 continue;
804 }
805 return pair.ruleToData(rule, this, isDst);
806 }
807 // Fell off start of rule, try previous rule.
808 } else if (ruleIndex == 0) {
809 // Describe time before the first transition in terms of a fictional
810 // transition at the start of time, so that a scan through all rules
811 // *does* see a first rule that supplies the offset for such times:
812 return ruleToData(rule, minMSecs(), QTimeZone::StandardTime, false);
813 } // else: no transition during rule's period
814 if (year >= rule.startYear) {
815 year = rule.startYear - 1; // Seek last transition in new rule
816 if (!year)
817 --year;
818 }
819 }
820 // Apparently no transition before the given time:
821 return {};
822}
823
824QByteArray QWinTimeZonePrivate::systemTimeZoneId() const
825{
826 const QLocale::Territory territory = userTerritory();
827 const QByteArray windowsId = windowsSystemZoneId();
828 QByteArray ianaId;
829 // If we have a real territory, then try get a specific match for that territory
830 if (territory != QLocale::AnyTerritory)
831 ianaId = windowsIdToDefaultIanaId(windowsId, territory);
832 // If we don't have a real territory, or there wasn't a specific match, try the global default
833 if (ianaId.isEmpty())
834 ianaId = windowsIdToDefaultIanaId(windowsId);
835 return ianaId;
836}
837
838QList<QByteArray> QWinTimeZonePrivate::availableTimeZoneIds() const
839{
840 static const QList<QByteArray> cache = [] {
841 QList<QByteArray> result;
842 const auto winIds = availableWindowsIds();
843 for (const QByteArray &winId : winIds)
844 result += windowsIdToIanaIds(winId);
845 std::sort(result.begin(), result.end());
846 result.erase(std::unique(result.begin(), result.end()), result.end());
847 return result;
848 }();
849 return cache;
850}
851
852QTimeZonePrivate::Data QWinTimeZonePrivate::ruleToData(const QWinTransitionRule &rule,
853 qint64 atMSecsSinceEpoch,
854 QTimeZone::TimeType type,
855 bool fakeDst) const
856{
857 Data tran;
858 tran.atMSecsSinceEpoch = atMSecsSinceEpoch;
859 tran.standardTimeOffset = rule.standardTimeBias * -60;
860 if (fakeDst) {
861 tran.daylightTimeOffset = 0;
862 // Rule may claim we're in DST when it's actually a standard time change:
863 if (type == QTimeZone::DaylightTime)
864 tran.standardTimeOffset += rule.daylightTimeBias * -60;
865 } else if (type == QTimeZone::DaylightTime) {
866 tran.daylightTimeOffset = rule.daylightTimeBias * -60;
867 } else {
868 tran.daylightTimeOffset = 0;
869 }
870 tran.offsetFromUtc = tran.standardTimeOffset + tran.daylightTimeOffset;
871 tran.abbreviation = localeName(atMSecsSinceEpoch, tran.offsetFromUtc,
872 type, QTimeZone::ShortName, QLocale::system());
873 return tran;
874}
875
876QT_END_NAMESPACE
\inmodule QtCore \reentrant
Definition qdatetime.h:30
QT_REQUIRE_CONFIG(timezone_locale)
#define MAX_KEY_LENGTH
constexpr qint64 JULIAN_DAY_FOR_EPOCH
constexpr int FIRST_DST_YEAR
static const wchar_t tzRegPath[]
static const wchar_t currTzRegPath[]
constexpr qint64 MSECS_PER_DAY