| /**************************************************************************** |
| ** |
| ** Copyright (C) 2013 John Layt <jlayt@kde.org> |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the QtCore module of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:LGPL$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** GNU Lesser General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU Lesser |
| ** General Public License version 3 as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
| ** packaging of this file. Please review the following information to |
| ** ensure the GNU Lesser General Public License version 3 requirements |
| ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 2.0 or (at your option) the GNU General |
| ** Public license version 3 or any later version approved by the KDE Free |
| ** Qt Foundation. The licenses are as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
| ** https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| #include "qtimezone.h" |
| #include "qtimezoneprivate_p.h" |
| |
| #include "qdatetime.h" |
| |
| #include "qdebug.h" |
| |
| #include <algorithm> |
| |
| #ifndef Q_OS_WINRT |
| #include <private/qwinregistry_p.h> |
| // The registry-based timezone backend is not available on WinRT, which falls back to equivalent APIs. |
| #define QT_USE_REGISTRY_TIMEZONE 1 |
| #endif |
| |
| QT_BEGIN_NAMESPACE |
| |
| /* |
| Private |
| |
| Windows system implementation |
| */ |
| |
| #define MAX_KEY_LENGTH 255 |
| #define FILETIME_UNIX_EPOCH Q_UINT64_C(116444736000000000) |
| |
| // MSDN home page for Time support |
| // http://msdn.microsoft.com/en-us/library/windows/desktop/ms724962%28v=vs.85%29.aspx |
| |
| // For Windows XP and later refer to MSDN docs on TIME_ZONE_INFORMATION structure |
| // http://msdn.microsoft.com/en-gb/library/windows/desktop/ms725481%28v=vs.85%29.aspx |
| |
| // Vista introduced support for historic data, see MSDN docs on DYNAMIC_TIME_ZONE_INFORMATION |
| // http://msdn.microsoft.com/en-gb/library/windows/desktop/ms724253%28v=vs.85%29.aspx |
| #ifdef QT_USE_REGISTRY_TIMEZONE |
| static const wchar_t tzRegPath[] = LR"(SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones)"; |
| static const wchar_t currTzRegPath[] = LR"(SYSTEM\CurrentControlSet\Control\TimeZoneInformation)"; |
| #endif |
| |
| enum { |
| MIN_YEAR = -292275056, |
| MAX_YEAR = 292278994, |
| MSECS_PER_DAY = 86400000, |
| TIME_T_MAX = 2145916799, // int maximum 2037-12-31T23:59:59 UTC |
| JULIAN_DAY_FOR_EPOCH = 2440588 // result of julianDayFromDate(1970, 1, 1) |
| }; |
| |
| // Copied from MSDN, see above for link |
| typedef struct _REG_TZI_FORMAT |
| { |
| LONG Bias; |
| LONG StandardBias; |
| LONG DaylightBias; |
| SYSTEMTIME StandardDate; |
| SYSTEMTIME DaylightDate; |
| } REG_TZI_FORMAT; |
| |
| namespace { |
| |
| // Fast and reliable conversion from msecs to date for all values |
| // Adapted from QDateTime msecsToDate |
| QDate msecsToDate(qint64 msecs) |
| { |
| qint64 jd = JULIAN_DAY_FOR_EPOCH; |
| |
| if (qAbs(msecs) >= MSECS_PER_DAY) { |
| jd += (msecs / MSECS_PER_DAY); |
| msecs %= MSECS_PER_DAY; |
| } |
| |
| if (msecs < 0) { |
| qint64 ds = MSECS_PER_DAY - msecs - 1; |
| jd -= ds / MSECS_PER_DAY; |
| } |
| |
| return QDate::fromJulianDay(jd); |
| } |
| |
| bool equalSystemtime(const SYSTEMTIME &t1, const SYSTEMTIME &t2) |
| { |
| return (t1.wYear == t2.wYear |
| && t1.wMonth == t2.wMonth |
| && t1.wDay == t2.wDay |
| && t1.wDayOfWeek == t2.wDayOfWeek |
| && t1.wHour == t2.wHour |
| && t1.wMinute == t2.wMinute |
| && t1.wSecond == t2.wSecond |
| && t1.wMilliseconds == t2.wMilliseconds); |
| } |
| |
| bool equalTzi(const TIME_ZONE_INFORMATION &tzi1, const TIME_ZONE_INFORMATION &tzi2) |
| { |
| return(tzi1.Bias == tzi2.Bias |
| && tzi1.StandardBias == tzi2.StandardBias |
| && equalSystemtime(tzi1.StandardDate, tzi2.StandardDate) |
| && wcscmp(tzi1.StandardName, tzi2.StandardName) == 0 |
| && tzi1.DaylightBias == tzi2.DaylightBias |
| && equalSystemtime(tzi1.DaylightDate, tzi2.DaylightDate) |
| && wcscmp(tzi1.DaylightName, tzi2.DaylightName) == 0); |
| } |
| |
| #ifdef QT_USE_REGISTRY_TIMEZONE |
| |
| QWinTimeZonePrivate::QWinTransitionRule readRegistryRule(const HKEY &key, |
| const wchar_t *value, bool *ok) |
| { |
| *ok = false; |
| QWinTimeZonePrivate::QWinTransitionRule rule; |
| REG_TZI_FORMAT tzi; |
| DWORD tziSize = sizeof(tzi); |
| if (RegQueryValueEx(key, value, nullptr, nullptr, reinterpret_cast<BYTE *>(&tzi), &tziSize) |
| == ERROR_SUCCESS) { |
| rule.startYear = 0; |
| rule.standardTimeBias = tzi.Bias + tzi.StandardBias; |
| rule.daylightTimeBias = tzi.Bias + tzi.DaylightBias - rule.standardTimeBias; |
| rule.standardTimeRule = tzi.StandardDate; |
| rule.daylightTimeRule = tzi.DaylightDate; |
| *ok = true; |
| } |
| return rule; |
| } |
| |
| TIME_ZONE_INFORMATION getRegistryTzi(const QByteArray &windowsId, bool *ok) |
| { |
| *ok = false; |
| TIME_ZONE_INFORMATION tzi; |
| REG_TZI_FORMAT regTzi; |
| DWORD regTziSize = sizeof(regTzi); |
| const QString tziKeyPath = QString::fromWCharArray(tzRegPath) + QLatin1Char('\\') |
| + QString::fromUtf8(windowsId); |
| |
| QWinRegistryKey key(HKEY_LOCAL_MACHINE, tziKeyPath); |
| if (key.isValid()) { |
| DWORD size = sizeof(tzi.DaylightName); |
| RegQueryValueEx(key, L"Dlt", nullptr, nullptr, reinterpret_cast<LPBYTE>(tzi.DaylightName), &size); |
| |
| size = sizeof(tzi.StandardName); |
| RegQueryValueEx(key, L"Std", nullptr, nullptr, reinterpret_cast<LPBYTE>(tzi.StandardName), &size); |
| |
| if (RegQueryValueEx(key, L"TZI", nullptr, nullptr, reinterpret_cast<BYTE *>(®Tzi), ®TziSize) |
| == ERROR_SUCCESS) { |
| tzi.Bias = regTzi.Bias; |
| tzi.StandardBias = regTzi.StandardBias; |
| tzi.DaylightBias = regTzi.DaylightBias; |
| tzi.StandardDate = regTzi.StandardDate; |
| tzi.DaylightDate = regTzi.DaylightDate; |
| *ok = true; |
| } |
| } |
| |
| return tzi; |
| } |
| #else // QT_USE_REGISTRY_TIMEZONE |
| struct QWinDynamicTimeZone |
| { |
| QString standardName; |
| QString daylightName; |
| QString timezoneName; |
| qint32 bias; |
| bool daylightTime; |
| }; |
| |
| typedef QHash<QByteArray, QWinDynamicTimeZone> QWinRTTimeZoneHash; |
| |
| Q_GLOBAL_STATIC(QWinRTTimeZoneHash, gTimeZones) |
| |
| void enumerateTimeZones() |
| { |
| DYNAMIC_TIME_ZONE_INFORMATION dtzInfo; |
| quint32 index = 0; |
| QString prevTimeZoneKeyName; |
| while (SUCCEEDED(EnumDynamicTimeZoneInformation(index++, &dtzInfo))) { |
| QWinDynamicTimeZone item; |
| item.timezoneName = QString::fromWCharArray(dtzInfo.TimeZoneKeyName); |
| // As soon as key name repeats, break. Some systems continue to always |
| // return the last item independent of index being out of range |
| if (item.timezoneName == prevTimeZoneKeyName) |
| break; |
| item.standardName = QString::fromWCharArray(dtzInfo.StandardName); |
| item.daylightName = QString::fromWCharArray(dtzInfo.DaylightName); |
| item.daylightTime = !dtzInfo.DynamicDaylightTimeDisabled; |
| item.bias = dtzInfo.Bias; |
| gTimeZones->insert(item.timezoneName.toUtf8(), item); |
| prevTimeZoneKeyName = item.timezoneName; |
| } |
| } |
| |
| DYNAMIC_TIME_ZONE_INFORMATION dynamicInfoForId(const QByteArray &windowsId) |
| { |
| DYNAMIC_TIME_ZONE_INFORMATION dtzInfo; |
| quint32 index = 0; |
| QString prevTimeZoneKeyName; |
| while (SUCCEEDED(EnumDynamicTimeZoneInformation(index++, &dtzInfo))) { |
| const QString timeZoneName = QString::fromWCharArray(dtzInfo.TimeZoneKeyName); |
| if (timeZoneName == QLatin1String(windowsId)) |
| break; |
| if (timeZoneName == prevTimeZoneKeyName) |
| break; |
| prevTimeZoneKeyName = timeZoneName; |
| } |
| return dtzInfo; |
| } |
| |
| QWinTimeZonePrivate::QWinTransitionRule |
| readDynamicRule(DYNAMIC_TIME_ZONE_INFORMATION &dtzi, int year, bool *ok) |
| { |
| TIME_ZONE_INFORMATION tzi; |
| QWinTimeZonePrivate::QWinTransitionRule rule; |
| *ok = GetTimeZoneInformationForYear(year, &dtzi, &tzi); |
| if (*ok) { |
| rule.startYear = 0; |
| rule.standardTimeBias = tzi.Bias + tzi.StandardBias; |
| rule.daylightTimeBias = tzi.Bias + tzi.DaylightBias - rule.standardTimeBias; |
| rule.standardTimeRule = tzi.StandardDate; |
| rule.daylightTimeRule = tzi.DaylightDate; |
| } |
| return rule; |
| } |
| #endif // QT_USE_REGISTRY_TIMEZONE |
| |
| bool isSameRule(const QWinTimeZonePrivate::QWinTransitionRule &last, |
| const QWinTimeZonePrivate::QWinTransitionRule &rule) |
| { |
| // In particular, when this is true and either wYear is 0, so is the other; |
| // so if one rule is recurrent and they're equal, so is the other. If |
| // either rule *isn't* recurrent, it has non-0 wYear which shall be |
| // different from the other's. Note that we don't compare .startYear, since |
| // that will always be different. |
| return equalSystemtime(last.standardTimeRule, rule.standardTimeRule) |
| && equalSystemtime(last.daylightTimeRule, rule.daylightTimeRule) |
| && last.standardTimeBias == rule.standardTimeBias |
| && last.daylightTimeBias == rule.daylightTimeBias; |
| } |
| |
| QList<QByteArray> availableWindowsIds() |
| { |
| #ifdef QT_USE_REGISTRY_TIMEZONE |
| // TODO Consider caching results in a global static, very unlikely to change. |
| QList<QByteArray> list; |
| QWinRegistryKey key(HKEY_LOCAL_MACHINE, tzRegPath); |
| if (key.isValid()) { |
| DWORD idCount = 0; |
| if (RegQueryInfoKey(key, 0, 0, 0, &idCount, 0, 0, 0, 0, 0, 0, 0) == ERROR_SUCCESS |
| && idCount > 0) { |
| for (DWORD i = 0; i < idCount; ++i) { |
| DWORD maxLen = MAX_KEY_LENGTH; |
| TCHAR buffer[MAX_KEY_LENGTH]; |
| if (RegEnumKeyEx(key, i, buffer, &maxLen, 0, 0, 0, 0) == ERROR_SUCCESS) |
| list.append(QString::fromWCharArray(buffer).toUtf8()); |
| } |
| } |
| } |
| return list; |
| #else // QT_USE_REGISTRY_TIMEZONE |
| if (gTimeZones->isEmpty()) |
| enumerateTimeZones(); |
| return gTimeZones->keys(); |
| #endif // QT_USE_REGISTRY_TIMEZONE |
| } |
| |
| QByteArray windowsSystemZoneId() |
| { |
| #ifdef QT_USE_REGISTRY_TIMEZONE |
| // On Vista and later is held in the value TimeZoneKeyName in key currTzRegPath |
| const QString id = QWinRegistryKey(HKEY_LOCAL_MACHINE, currTzRegPath) |
| .stringValue(L"TimeZoneKeyName"); |
| if (!id.isEmpty()) |
| return id.toUtf8(); |
| |
| // On XP we have to iterate over the zones until we find a match on |
| // names/offsets with the current data |
| TIME_ZONE_INFORMATION sysTzi; |
| GetTimeZoneInformation(&sysTzi); |
| bool ok = false; |
| const auto winIds = availableWindowsIds(); |
| for (const QByteArray &winId : winIds) { |
| if (equalTzi(getRegistryTzi(winId, &ok), sysTzi)) |
| return winId; |
| } |
| #else // QT_USE_REGISTRY_TIMEZONE |
| DYNAMIC_TIME_ZONE_INFORMATION dtzi; |
| if (SUCCEEDED(GetDynamicTimeZoneInformation(&dtzi))) |
| return QString::fromWCharArray(dtzi.TimeZoneKeyName).toLocal8Bit(); |
| #endif // QT_USE_REGISTRY_TIMEZONE |
| |
| // If we can't determine the current ID use UTC |
| return QTimeZonePrivate::utcQByteArray(); |
| } |
| |
| QDate calculateTransitionLocalDate(const SYSTEMTIME &rule, int year) |
| { |
| // If month is 0 then there is no date |
| if (rule.wMonth == 0) |
| return QDate(); |
| |
| // Interpret SYSTEMTIME according to the slightly quirky rules in: |
| // https://msdn.microsoft.com/en-us/library/windows/desktop/ms725481(v=vs.85).aspx |
| |
| // If the year is set, the rule gives an absolute date: |
| if (rule.wYear) |
| return QDate(rule.wYear, rule.wMonth, rule.wDay); |
| |
| // Otherwise, the rule date is annual and relative: |
| const int dayOfWeek = rule.wDayOfWeek == 0 ? 7 : rule.wDayOfWeek; |
| QDate date(year, rule.wMonth, 1); |
| Q_ASSERT(date.isValid()); |
| // How many days before was last dayOfWeek before target month ? |
| int adjust = dayOfWeek - date.dayOfWeek(); // -6 <= adjust < 7 |
| if (adjust >= 0) // Ensure -7 <= adjust < 0: |
| adjust -= 7; |
| // Normally, wDay is day-within-month; but here it is 1 for the first |
| // of the given dayOfWeek in the month, through 4 for the fourth or ... |
| adjust += (rule.wDay < 1 ? 1 : rule.wDay > 4 ? 5 : rule.wDay) * 7; |
| date = date.addDays(adjust); |
| // ... 5 for the last; so back up by weeks to get within the month: |
| if (date.month() != rule.wMonth) { |
| Q_ASSERT(rule.wDay > 4); |
| // (Note that, with adjust < 0, date <= 28th of our target month |
| // is guaranteed when wDay <= 4, or after our first -7 here.) |
| date = date.addDays(-7); |
| Q_ASSERT(date.month() == rule.wMonth); |
| } |
| return date; |
| } |
| |
| // Converts a date/time value into msecs |
| inline qint64 timeToMSecs(QDate date, QTime time) |
| { |
| return ((date.toJulianDay() - JULIAN_DAY_FOR_EPOCH) * MSECS_PER_DAY) |
| + time.msecsSinceStartOfDay(); |
| } |
| |
| qint64 calculateTransitionForYear(const SYSTEMTIME &rule, int year, int bias) |
| { |
| // TODO Consider caching the calculated values - i.e. replace SYSTEMTIME in |
| // WinTransitionRule; do this in init() once and store the results. |
| Q_ASSERT(year); |
| const QDate date = calculateTransitionLocalDate(rule, year); |
| const QTime time = QTime(rule.wHour, rule.wMinute, rule.wSecond); |
| if (date.isValid() && time.isValid()) |
| return timeToMSecs(date, time) + bias * 60000; |
| return QTimeZonePrivate::invalidMSecs(); |
| } |
| |
| struct TransitionTimePair |
| { |
| // Transition times after the epoch, in ms: |
| qint64 std, dst; |
| // If either is invalidMSecs(), which shall then be < the other, there is no |
| // DST and the other describes a change in actual standard offset. |
| |
| TransitionTimePair(const QWinTimeZonePrivate::QWinTransitionRule &rule, |
| int year, int oldYearOffset) |
| // The local time in Daylight Time of the switch to Standard Time |
| : std(calculateTransitionForYear(rule.standardTimeRule, year, |
| rule.standardTimeBias + rule.daylightTimeBias)), |
| // The local time in Standard Time of the switch to Daylight Time |
| dst(calculateTransitionForYear(rule.daylightTimeRule, year, rule.standardTimeBias)) |
| { |
| /* |
| Check for potential "fake DST", used by MS's APIs because the |
| TIME_ZONE_INFORMATION spec either expresses no transitions in the |
| year, or expresses a transition of each kind, even if standard time |
| did change in a year with no DST. We've seen year-start fake-DST |
| (whose offset matches prior standard offset, in which the previous |
| year ended); and conjecture that similar might be used at a year-end. |
| (This might be used for a southern-hemisphere zone, where the start of |
| the year usually is in DST, when applicable.) Note that, here, wDay |
| identifies an instance of a given day-of-week in the month, with 5 |
| meaning last. |
| |
| Either the alleged standardTimeRule or the alleged daylightTimeRule |
| may be faked; either way, the transition is actually a change to the |
| current standard offset; but the unfaked half of the rule contains the |
| useful bias data, so we have to go along with its lies. |
| |
| Example: Russia/Moscow |
| Format: -bias +( -stdBias, stdDate | -dstBias, dstDate ) notes |
| Last year of DST, 2010: 180 +( 0, 0-10-5 3:0 | 60, 0-3-5 2:0 ) normal DST |
| Zone change in 2011: 180 +( 0, 0-1-1 0:0 | 60 0-3-5 2:0 ) fake DST at transition |
| Fixed standard in 2012: 240 +( 0, 0-0-0 0:0 | 60, 0-0-0 0:0 ) standard time years |
| Zone change in 2014: 180 +( 0, 0-10-5 2:0 | 60, 0-1-1 0:0 ) fake DST at year-start |
| The last of these is missing on Win7 VMs (too old to know about it). |
| */ |
| if (rule.daylightTimeRule.wMonth == 1 && rule.daylightTimeRule.wDay == 1) { |
| // Fake "DST transition" at start of year producing the same offset as |
| // previous year ended in. |
| if (rule.standardTimeBias + rule.daylightTimeBias == oldYearOffset) |
| dst = QTimeZonePrivate::invalidMSecs(); |
| } else if (rule.daylightTimeRule.wMonth == 12 && rule.daylightTimeRule.wDay > 3) { |
| // Similar, conjectured, for end of year, not changing offset. |
| if (rule.daylightTimeBias == 0) |
| dst = QTimeZonePrivate::invalidMSecs(); |
| } |
| if (rule.standardTimeRule.wMonth == 1 && rule.standardTimeRule.wDay == 1) { |
| // Fake "transition out of DST" at start of year producing the same |
| // offset as previous year ended in. |
| if (rule.standardTimeBias == oldYearOffset) |
| std = QTimeZonePrivate::invalidMSecs(); |
| } else if (rule.standardTimeRule.wMonth == 12 && rule.standardTimeRule.wDay > 3) { |
| // Similar, conjectured, for end of year, not changing offset. |
| if (rule.daylightTimeBias == 0) |
| std = QTimeZonePrivate::invalidMSecs(); |
| } |
| } |
| |
| bool fakesDst() const |
| { |
| return std == QTimeZonePrivate::invalidMSecs() |
| || dst == QTimeZonePrivate::invalidMSecs(); |
| } |
| }; |
| |
| int yearEndOffset(const QWinTimeZonePrivate::QWinTransitionRule &rule, int year) |
| { |
| Q_ASSERT(year); |
| int offset = rule.standardTimeBias; |
| // Only needed to help another TransitionTimePair work out year + 1's start |
| // offset; and the oldYearOffset we use only affects an alleged transition |
| // at the *start* of this year, so it doesn't matter if we guess wrong here: |
| TransitionTimePair pair(rule, year, offset); |
| if (pair.dst > pair.std) |
| offset += rule.daylightTimeBias; |
| return offset; |
| } |
| |
| QLocale::Country userCountry() |
| { |
| const GEOID id = GetUserGeoID(GEOCLASS_NATION); |
| wchar_t code[3]; |
| const int size = GetGeoInfo(id, GEO_ISO2, code, 3, 0); |
| return (size == 3) ? QLocalePrivate::codeToCountry(QStringView(code, size)) |
| : QLocale::AnyCountry; |
| } |
| |
| // Index of last rule in rules with .startYear <= year: |
| int ruleIndexForYear(const QList<QWinTimeZonePrivate::QWinTransitionRule> &rules, int year) |
| { |
| if (rules.last().startYear <= year) |
| return rules.count() - 1; |
| // We don't have a rule for before the first, but the first is the best we can offer: |
| if (rules.first().startYear > year) |
| return 0; |
| |
| // Otherwise, use binary chop: |
| int lo = 0, hi = rules.count(); |
| // invariant: rules[i].startYear <= year < rules[hi].startYear |
| // subject to treating rules[rules.count()] as "off the end of time" |
| while (lo + 1 < hi) { |
| const int mid = (lo + hi) / 2; |
| // lo + 2 <= hi, so lo + 1 <= mid <= hi - 1, so lo < mid < hi |
| // In particular, mid < rules.count() |
| const int midYear = rules.at(mid).startYear; |
| if (midYear > year) |
| hi = mid; |
| else if (midYear < year) |
| lo = mid; |
| else // No two rules have the same startYear: |
| return mid; |
| } |
| return lo; |
| } |
| |
| } // anonymous namespace |
| |
| // Create the system default time zone |
| QWinTimeZonePrivate::QWinTimeZonePrivate() |
| : QTimeZonePrivate() |
| { |
| init(QByteArray()); |
| } |
| |
| // Create a named time zone |
| QWinTimeZonePrivate::QWinTimeZonePrivate(const QByteArray &ianaId) |
| : QTimeZonePrivate() |
| { |
| init(ianaId); |
| } |
| |
| QWinTimeZonePrivate::QWinTimeZonePrivate(const QWinTimeZonePrivate &other) |
| : QTimeZonePrivate(other), m_windowsId(other.m_windowsId), |
| m_displayName(other.m_displayName), m_standardName(other.m_standardName), |
| m_daylightName(other.m_daylightName), m_tranRules(other.m_tranRules) |
| { |
| } |
| |
| QWinTimeZonePrivate::~QWinTimeZonePrivate() |
| { |
| } |
| |
| QWinTimeZonePrivate *QWinTimeZonePrivate::clone() const |
| { |
| return new QWinTimeZonePrivate(*this); |
| } |
| |
| void QWinTimeZonePrivate::init(const QByteArray &ianaId) |
| { |
| if (ianaId.isEmpty()) { |
| m_windowsId = windowsSystemZoneId(); |
| m_id = systemTimeZoneId(); |
| } else { |
| m_windowsId = ianaIdToWindowsId(ianaId); |
| m_id = ianaId; |
| } |
| |
| bool badMonth = false; // Only warn once per zone, if at all. |
| if (!m_windowsId.isEmpty()) { |
| #ifdef QT_USE_REGISTRY_TIMEZONE |
| // Open the base TZI for the time zone |
| const QString baseKeyPath = QString::fromWCharArray(tzRegPath) + QLatin1Char('\\') |
| + QString::fromUtf8(m_windowsId); |
| QWinRegistryKey baseKey(HKEY_LOCAL_MACHINE, baseKeyPath); |
| if (baseKey.isValid()) { |
| // Load the localized names |
| m_displayName = baseKey.stringValue(L"Display"); |
| m_standardName = baseKey.stringValue(L"Std"); |
| m_daylightName = baseKey.stringValue(L"Dlt"); |
| // On Vista and later the optional dynamic key holds historic data |
| const QString dynamicKeyPath = baseKeyPath + QLatin1String("\\Dynamic DST"); |
| QWinRegistryKey dynamicKey(HKEY_LOCAL_MACHINE, dynamicKeyPath); |
| if (dynamicKey.isValid()) { |
| // Find out the start and end years stored, then iterate over them |
| const auto startYear = dynamicKey.dwordValue(L"FirstEntry"); |
| const auto endYear = dynamicKey.dwordValue(L"LastEntry"); |
| for (int year = int(startYear.first); year <= int(endYear.first); ++year) { |
| bool ruleOk; |
| QWinTransitionRule rule = readRegistryRule(dynamicKey, |
| reinterpret_cast<LPCWSTR>(QString::number(year).utf16()), |
| &ruleOk); |
| if (ruleOk |
| // Don't repeat a recurrent rule: |
| && (m_tranRules.isEmpty() |
| || !isSameRule(m_tranRules.last(), rule))) { |
| if (!badMonth |
| && (rule.standardTimeRule.wMonth == 0) |
| != (rule.daylightTimeRule.wMonth == 0)) { |
| badMonth = true; |
| qWarning("MS registry TZ API violated its wMonth constraint;" |
| "this may cause mistakes for %s from %d", |
| ianaId.constData(), year); |
| } |
| rule.startYear = m_tranRules.isEmpty() ? MIN_YEAR : year; |
| m_tranRules.append(rule); |
| } |
| } |
| } else { |
| // No dynamic data so use the base data |
| bool ruleOk; |
| QWinTransitionRule rule = readRegistryRule(baseKey, L"TZI", &ruleOk); |
| rule.startYear = MIN_YEAR; |
| if (ruleOk) |
| m_tranRules.append(rule); |
| } |
| } |
| #else // QT_USE_REGISTRY_TIMEZONE |
| if (gTimeZones->isEmpty()) |
| enumerateTimeZones(); |
| QWinRTTimeZoneHash::const_iterator it = gTimeZones->find(m_windowsId); |
| if (it != gTimeZones->constEnd()) { |
| m_displayName = it->timezoneName; |
| m_standardName = it->standardName; |
| m_daylightName = it->daylightName; |
| DWORD firstYear = 0; |
| DWORD lastYear = 0; |
| DYNAMIC_TIME_ZONE_INFORMATION dtzi = dynamicInfoForId(m_windowsId); |
| if (GetDynamicTimeZoneInformationEffectiveYears(&dtzi, &firstYear, &lastYear) |
| == ERROR_SUCCESS && firstYear < lastYear) { |
| for (DWORD year = firstYear; year <= lastYear; ++year) { |
| bool ok = false; |
| QWinTransitionRule rule = readDynamicRule(dtzi, year, &ok); |
| if (ok |
| // Don't repeat a recurrent rule |
| && (m_tranRules.isEmpty() |
| || !isSameRule(m_tranRules.last(), rule))) { |
| if (!badMonth |
| && (rule.standardTimeRule.wMonth == 0) |
| != (rule.daylightTimeRule.wMonth == 0)) { |
| badMonth = true; |
| qWarning("MS dynamic TZ API violated its wMonth constraint;" |
| "this may cause mistakes for %s from %d", |
| ianaId.constData(), year); |
| } |
| rule.startYear = m_tranRules.isEmpty() ? MIN_YEAR : year; |
| m_tranRules.append(rule); |
| } |
| } |
| } else { |
| // At least try to get the non-dynamic data: |
| dtzi.DynamicDaylightTimeDisabled = false; |
| bool ok = false; |
| QWinTransitionRule rule = readDynamicRule(dtzi, 1970, &ok); |
| if (ok) { |
| rule.startYear = MIN_YEAR; |
| m_tranRules.append(rule); |
| } |
| } |
| } |
| #endif // QT_USE_REGISTRY_TIMEZONE |
| } |
| |
| // If there are no rules then we failed to find a windowsId or any tzi info |
| if (m_tranRules.size() == 0) { |
| m_id.clear(); |
| m_windowsId.clear(); |
| m_displayName.clear(); |
| } |
| } |
| |
| QString QWinTimeZonePrivate::comment() const |
| { |
| return m_displayName; |
| } |
| |
| QString QWinTimeZonePrivate::displayName(QTimeZone::TimeType timeType, |
| QTimeZone::NameType nameType, |
| const QLocale &locale) const |
| { |
| // TODO Registry holds MUI keys, should be able to look up translations? |
| Q_UNUSED(locale); |
| |
| if (nameType == QTimeZone::OffsetName) { |
| const QWinTransitionRule &rule = |
| m_tranRules.at(ruleIndexForYear(m_tranRules, QDate::currentDate().year())); |
| int offset = rule.standardTimeBias; |
| if (timeType == QTimeZone::DaylightTime) |
| offset += rule.daylightTimeBias; |
| return isoOffsetFormat(offset * -60); |
| } |
| |
| switch (timeType) { |
| case QTimeZone::DaylightTime : |
| return m_daylightName; |
| case QTimeZone::GenericTime : |
| return m_displayName; |
| case QTimeZone::StandardTime : |
| return m_standardName; |
| } |
| return m_standardName; |
| } |
| |
| QString QWinTimeZonePrivate::abbreviation(qint64 atMSecsSinceEpoch) const |
| { |
| return data(atMSecsSinceEpoch).abbreviation; |
| } |
| |
| int QWinTimeZonePrivate::offsetFromUtc(qint64 atMSecsSinceEpoch) const |
| { |
| return data(atMSecsSinceEpoch).offsetFromUtc; |
| } |
| |
| int QWinTimeZonePrivate::standardTimeOffset(qint64 atMSecsSinceEpoch) const |
| { |
| return data(atMSecsSinceEpoch).standardTimeOffset; |
| } |
| |
| int QWinTimeZonePrivate::daylightTimeOffset(qint64 atMSecsSinceEpoch) const |
| { |
| return data(atMSecsSinceEpoch).daylightTimeOffset; |
| } |
| |
| bool QWinTimeZonePrivate::hasDaylightTime() const |
| { |
| return hasTransitions(); |
| } |
| |
| bool QWinTimeZonePrivate::isDaylightTime(qint64 atMSecsSinceEpoch) const |
| { |
| return (data(atMSecsSinceEpoch).daylightTimeOffset != 0); |
| } |
| |
| QTimeZonePrivate::Data QWinTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const |
| { |
| int year = msecsToDate(forMSecsSinceEpoch).year(); |
| for (int ruleIndex = ruleIndexForYear(m_tranRules, year); |
| ruleIndex >= 0; --ruleIndex) { |
| const QWinTransitionRule &rule = m_tranRules.at(ruleIndex); |
| // Does this rule's period include any transition at all ? |
| if (rule.standardTimeRule.wMonth > 0 || rule.daylightTimeRule.wMonth > 0) { |
| int prior = year == 1 ? -1 : year - 1; // No year 0. |
| const int endYear = qMax(rule.startYear, prior); |
| while (year >= endYear) { |
| const int newYearOffset = (year <= rule.startYear && ruleIndex > 0) |
| ? yearEndOffset(m_tranRules.at(ruleIndex - 1), prior) |
| : yearEndOffset(rule, prior); |
| const TransitionTimePair pair(rule, year, newYearOffset); |
| bool isDst = false; |
| if (pair.std != invalidMSecs() && pair.std <= forMSecsSinceEpoch) { |
| isDst = pair.std < pair.dst && pair.dst <= forMSecsSinceEpoch; |
| } else if (pair.dst != invalidMSecs() && pair.dst <= forMSecsSinceEpoch) { |
| isDst = true; |
| } else { |
| year = prior; // Try an earlier year for this rule (once). |
| prior = year == 1 ? -1 : year - 1; // No year 0. |
| continue; |
| } |
| return ruleToData(rule, forMSecsSinceEpoch, |
| isDst ? QTimeZone::DaylightTime : QTimeZone::StandardTime, |
| pair.fakesDst()); |
| } |
| // Fell off start of rule, try previous rule. |
| } else { |
| // No transition, no DST, use the year's standard time. |
| return ruleToData(rule, forMSecsSinceEpoch, QTimeZone::StandardTime); |
| } |
| if (year >= rule.startYear) { |
| year = rule.startYear - 1; // Seek last transition in new rule. |
| if (!year) |
| --year; |
| } |
| } |
| // We don't have relevant data :-( |
| return invalidData(); |
| } |
| |
| bool QWinTimeZonePrivate::hasTransitions() const |
| { |
| for (const QWinTransitionRule &rule : m_tranRules) { |
| if (rule.standardTimeRule.wMonth > 0 && rule.daylightTimeRule.wMonth > 0) |
| return true; |
| } |
| return false; |
| } |
| |
| QTimeZonePrivate::Data QWinTimeZonePrivate::nextTransition(qint64 afterMSecsSinceEpoch) const |
| { |
| int year = msecsToDate(afterMSecsSinceEpoch).year(); |
| for (int ruleIndex = ruleIndexForYear(m_tranRules, year); |
| ruleIndex < m_tranRules.count(); ++ruleIndex) { |
| const QWinTransitionRule &rule = m_tranRules.at(ruleIndex); |
| // Does this rule's period include any transition at all ? |
| if (rule.standardTimeRule.wMonth > 0 || rule.daylightTimeRule.wMonth > 0) { |
| if (year < rule.startYear) |
| year = rule.startYear; // Seek first transition in this rule. |
| const int endYear = ruleIndex + 1 < m_tranRules.count() |
| ? qMin(m_tranRules.at(ruleIndex + 1).startYear, year + 2) : (year + 2); |
| int prior = year == 1 ? -1 : year - 1; // No year 0. |
| int newYearOffset = (year <= rule.startYear && ruleIndex > 0) |
| ? yearEndOffset(m_tranRules.at(ruleIndex - 1), prior) |
| : yearEndOffset(rule, prior); |
| while (year < endYear) { |
| const TransitionTimePair pair(rule, year, newYearOffset); |
| bool isDst = false; |
| Q_ASSERT(invalidMSecs() <= afterMSecsSinceEpoch); // invalid is min qint64 |
| if (pair.std > afterMSecsSinceEpoch) { |
| isDst = pair.std > pair.dst && pair.dst > afterMSecsSinceEpoch; |
| } else if (pair.dst > afterMSecsSinceEpoch) { |
| isDst = true; |
| } else { |
| newYearOffset = rule.standardTimeBias; |
| if (pair.dst > pair.std) |
| newYearOffset += rule.daylightTimeBias; |
| // Try a later year for this rule (once). |
| prior = year; |
| year = year == -1 ? 1 : year + 1; // No year 0 |
| continue; |
| } |
| |
| if (isDst) |
| return ruleToData(rule, pair.dst, QTimeZone::DaylightTime, pair.fakesDst()); |
| return ruleToData(rule, pair.std, QTimeZone::StandardTime, pair.fakesDst()); |
| } |
| // Fell off end of rule, try next rule. |
| } // else: no transition during rule's period |
| } |
| // Apparently no transition after the given time: |
| return invalidData(); |
| } |
| |
| QTimeZonePrivate::Data QWinTimeZonePrivate::previousTransition(qint64 beforeMSecsSinceEpoch) const |
| { |
| const qint64 startOfTime = invalidMSecs() + 1; |
| if (beforeMSecsSinceEpoch <= startOfTime) |
| return invalidData(); |
| |
| int year = msecsToDate(beforeMSecsSinceEpoch).year(); |
| for (int ruleIndex = ruleIndexForYear(m_tranRules, year); |
| ruleIndex >= 0; --ruleIndex) { |
| const QWinTransitionRule &rule = m_tranRules.at(ruleIndex); |
| // Does this rule's period include any transition at all ? |
| if (rule.standardTimeRule.wMonth > 0 || rule.daylightTimeRule.wMonth > 0) { |
| int prior = year == 1 ? -1 : year - 1; // No year 0. |
| const int endYear = qMax(rule.startYear, prior); |
| while (year >= endYear) { |
| const int newYearOffset = (year <= rule.startYear && ruleIndex > 0) |
| ? yearEndOffset(m_tranRules.at(ruleIndex - 1), prior) |
| : yearEndOffset(rule, prior); |
| const TransitionTimePair pair(rule, year, newYearOffset); |
| bool isDst = false; |
| if (pair.std != invalidMSecs() && pair.std < beforeMSecsSinceEpoch) { |
| isDst = pair.std < pair.dst && pair.dst < beforeMSecsSinceEpoch; |
| } else if (pair.dst != invalidMSecs() && pair.dst < beforeMSecsSinceEpoch) { |
| isDst = true; |
| } else { |
| year = prior; // Try an earlier year for this rule (once). |
| prior = year == 1 ? -1 : year - 1; // No year 0. |
| continue; |
| } |
| if (isDst) |
| return ruleToData(rule, pair.dst, QTimeZone::DaylightTime, pair.fakesDst()); |
| return ruleToData(rule, pair.std, QTimeZone::StandardTime, pair.fakesDst()); |
| } |
| // Fell off start of rule, try previous rule. |
| } else if (ruleIndex == 0) { |
| // Treat a no-transition first rule as a transition at the start of |
| // time, so that a scan through all rules *does* see it as the first |
| // rule: |
| return ruleToData(rule, startOfTime, QTimeZone::StandardTime, false); |
| } // else: no transition during rule's period |
| if (year >= rule.startYear) { |
| year = rule.startYear - 1; // Seek last transition in new rule |
| if (!year) |
| --year; |
| } |
| } |
| // Apparently no transition before the given time: |
| return invalidData(); |
| } |
| |
| QByteArray QWinTimeZonePrivate::systemTimeZoneId() const |
| { |
| const QLocale::Country country = userCountry(); |
| const QByteArray windowsId = windowsSystemZoneId(); |
| QByteArray ianaId; |
| // If we have a real country, then try get a specific match for that country |
| if (country != QLocale::AnyCountry) |
| ianaId = windowsIdToDefaultIanaId(windowsId, country); |
| // If we don't have a real country, or there wasn't a specific match, try the global default |
| if (ianaId.isEmpty()) { |
| ianaId = windowsIdToDefaultIanaId(windowsId); |
| // If no global default then probably an unknown Windows ID so return UTC |
| if (ianaId.isEmpty()) |
| return utcQByteArray(); |
| } |
| return ianaId; |
| } |
| |
| QList<QByteArray> QWinTimeZonePrivate::availableTimeZoneIds() const |
| { |
| QList<QByteArray> result; |
| const auto winIds = availableWindowsIds(); |
| for (const QByteArray &winId : winIds) |
| result += windowsIdToIanaIds(winId); |
| std::sort(result.begin(), result.end()); |
| result.erase(std::unique(result.begin(), result.end()), result.end()); |
| return result; |
| } |
| |
| QTimeZonePrivate::Data QWinTimeZonePrivate::ruleToData(const QWinTransitionRule &rule, |
| qint64 atMSecsSinceEpoch, |
| QTimeZone::TimeType type, |
| bool fakeDst) const |
| { |
| Data tran = invalidData(); |
| tran.atMSecsSinceEpoch = atMSecsSinceEpoch; |
| tran.standardTimeOffset = rule.standardTimeBias * -60; |
| if (fakeDst) { |
| tran.daylightTimeOffset = 0; |
| tran.abbreviation = m_standardName; |
| // Rule may claim we're in DST when it's actually a standard time change: |
| if (type == QTimeZone::DaylightTime) |
| tran.standardTimeOffset += rule.daylightTimeBias * -60; |
| } else if (type == QTimeZone::DaylightTime) { |
| tran.daylightTimeOffset = rule.daylightTimeBias * -60; |
| tran.abbreviation = m_daylightName; |
| } else { |
| tran.daylightTimeOffset = 0; |
| tran.abbreviation = m_standardName; |
| } |
| tran.offsetFromUtc = tran.standardTimeOffset + tran.daylightTimeOffset; |
| return tran; |
| } |
| |
| QT_END_NAMESPACE |