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