blob: d0ed404d36b51dd66618394bf626f13a51f01c5a [file] [log] [blame]
Jungshik Shin87232d82017-05-13 21:10:13 -07001// © 2016 and later: Unicode, Inc. and others.
Jungshik Shin5feb9ad2016-10-21 12:52:48 -07002// License & terms of use: http://www.unicode.org/copyright.html
jshin@chromium.org6f31ac32014-03-26 22:15:14 +00003/*
4********************************************************************************
Jungshik Shin70f82502016-01-29 00:32:36 -08005* Copyright (C) 2005-2015, International Business Machines
jshin@chromium.org6f31ac32014-03-26 22:15:14 +00006* Corporation and others. All Rights Reserved.
7********************************************************************************
8*
9* File WINTZ.CPP
10*
11********************************************************************************
12*/
13
14#include "unicode/utypes.h"
15
Jungshik Shin42d50272018-10-24 01:22:09 -070016#if U_PLATFORM_USES_ONLY_WIN32_API
jshin@chromium.org6f31ac32014-03-26 22:15:14 +000017
18#include "wintz.h"
Frank Tangf90543d2020-10-30 19:02:04 -070019#include "charstr.h"
jshin@chromium.org6f31ac32014-03-26 22:15:14 +000020#include "cmemory.h"
21#include "cstring.h"
22
jshin@chromium.org6f31ac32014-03-26 22:15:14 +000023#include "unicode/ures.h"
Frank Tangf90543d2020-10-30 19:02:04 -070024#include "unicode/unistr.h"
Jungshik Shinccad4472018-10-09 00:22:00 -070025#include "uresimp.h"
jshin@chromium.org6f31ac32014-03-26 22:15:14 +000026
Jungshik Shin87232d82017-05-13 21:10:13 -070027#ifndef WIN32_LEAN_AND_MEAN
jshin@chromium.org6f31ac32014-03-26 22:15:14 +000028# define WIN32_LEAN_AND_MEAN
Jungshik Shin87232d82017-05-13 21:10:13 -070029#endif
jshin@chromium.org6f31ac32014-03-26 22:15:14 +000030# define VC_EXTRALEAN
31# define NOUSER
32# define NOSERVICE
33# define NOIME
34# define NOMCX
35#include <windows.h>
36
Jungshik Shinccad4472018-10-09 00:22:00 -070037U_NAMESPACE_BEGIN
jshin@chromium.org6f31ac32014-03-26 22:15:14 +000038
Frank Tangf90543d2020-10-30 19:02:04 -070039// Note these constants and the struct are only used when dealing with the fallback path for RDP sesssions.
40
41// This is the location of the time zones in the registry on Vista+ systems.
42// See: https://docs.microsoft.com/windows/win32/api/timezoneapi/ns-timezoneapi-dynamic_time_zone_information
43#define WINDOWS_TIMEZONES_REG_KEY_PATH L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones"
44
45// Max length for a registry key is 255. +1 for null.
46// See: https://docs.microsoft.com/windows/win32/sysinfo/registry-element-size-limits
47#define WINDOWS_MAX_REG_KEY_LENGTH 256
48
49#if U_PLATFORM_HAS_WINUWP_API == 0
50
51// This is the layout of the TZI binary value in the registry.
52// See: https://docs.microsoft.com/windows/win32/api/timezoneapi/ns-timezoneapi-time_zone_information
53typedef struct _REG_TZI_FORMAT {
54 LONG Bias;
55 LONG StandardBias;
56 LONG DaylightBias;
57 SYSTEMTIME StandardDate;
58 SYSTEMTIME DaylightDate;
59} REG_TZI_FORMAT;
60
61#endif // U_PLATFORM_HAS_WINUWP_API
jshin@chromium.org6f31ac32014-03-26 22:15:14 +000062
63/**
Frank Tangf90543d2020-10-30 19:02:04 -070064* This is main Windows time zone detection function.
65*
66* It returns the Windows time zone converted to an ICU time zone as a heap-allocated buffer, or nullptr upon failure.
67*
68* We use the Win32 API GetDynamicTimeZoneInformation (which is available since Vista) to get the current time zone info,
69* as this API returns a non-localized time zone name which can be then mapped to an ICU time zone.
70*
71* However, in some RDP/terminal services situations, this struct isn't always fully complete, and the TimeZoneKeyName
72* field of the struct might be NULL. This can happen with some 3rd party RDP clients, and also when using older versions
73* of the RDP protocol, which don't send the newer TimeZoneKeyNamei information and only send the StandardName and DaylightName.
74*
75* Since these 3rd party clients and older RDP clients only send the pre-Vista time zone information to the server, this means that we
76* need to fallback on using the pre-Vista methods to determine the time zone. This unfortunately requires examining the registry directly
77* in order to try and determine the current time zone.
78*
79* Note that this can however still fail in some cases though if the client and server are using different languages, as the StandardName
80* that is sent by client is localized in the client's language. However, we must compare this to the names that are on the server, which
81* are localized in registry using the server's language. Despite that, this is the best we can do.
82*
83* Note: This fallback method won't work for the UWP version though, as we can't use the registry APIs in UWP.
84*
85* Once we have the current Windows time zone, then we can then map it to an ICU time zone ID (~ Olsen ID).
jshin@chromium.org6f31ac32014-03-26 22:15:14 +000086*/
Frank Tangf90543d2020-10-30 19:02:04 -070087U_CAPI const char* U_EXPORT2
Jungshik Shinccad4472018-10-09 00:22:00 -070088uprv_detectWindowsTimeZone()
Jungshik Shin87232d82017-05-13 21:10:13 -070089{
Frank Tangf90543d2020-10-30 19:02:04 -070090 // We first try to obtain the time zone directly by using the TimeZoneKeyName field of the DYNAMIC_TIME_ZONE_INFORMATION struct.
Jungshik Shinccad4472018-10-09 00:22:00 -070091 DYNAMIC_TIME_ZONE_INFORMATION dynamicTZI;
92 uprv_memset(&dynamicTZI, 0, sizeof(dynamicTZI));
Frank Tangf90543d2020-10-30 19:02:04 -070093 SYSTEMTIME systemTimeAllZero;
94 uprv_memset(&systemTimeAllZero, 0, sizeof(systemTimeAllZero));
jshin@chromium.org6f31ac32014-03-26 22:15:14 +000095
Frank Tangf90543d2020-10-30 19:02:04 -070096 if (GetDynamicTimeZoneInformation(&dynamicTZI) == TIME_ZONE_ID_INVALID) {
Jungshik Shinccad4472018-10-09 00:22:00 -070097 return nullptr;
98 }
jshin@chromium.org6f31ac32014-03-26 22:15:14 +000099
Frank Tangf90543d2020-10-30 19:02:04 -0700100 // If the DST setting has been turned off in the Control Panel, then return "Etc/GMT<offset>".
101 //
102 // Note: This logic is based on how the Control Panel itself determines if DST is 'off' on Windows.
103 // The code is somewhat convoluted; in a sort of pseudo-code it looks like this:
104 //
105 // IF (GetDynamicTimeZoneInformation != TIME_ZONE_ID_INVALID) && (DynamicDaylightTimeDisabled != 0) &&
106 // (StandardDate == DaylightDate) &&
107 // (
108 // (TimeZoneKeyName != Empty && StandardDate == 0) ||
109 // (TimeZoneKeyName == Empty && StandardDate != 0)
110 // )
111 // THEN
112 // DST setting is "Disabled".
113 //
114 if (dynamicTZI.DynamicDaylightTimeDisabled != 0 &&
115 uprv_memcmp(&dynamicTZI.StandardDate, &dynamicTZI.DaylightDate, sizeof(dynamicTZI.StandardDate)) == 0 &&
116 ((dynamicTZI.TimeZoneKeyName[0] != L'\0' && uprv_memcmp(&dynamicTZI.StandardDate, &systemTimeAllZero, sizeof(systemTimeAllZero)) == 0) ||
117 (dynamicTZI.TimeZoneKeyName[0] == L'\0' && uprv_memcmp(&dynamicTZI.StandardDate, &systemTimeAllZero, sizeof(systemTimeAllZero)) != 0)))
118 {
119 LONG utcOffsetMins = dynamicTZI.Bias;
120 if (utcOffsetMins == 0) {
121 return uprv_strdup("Etc/UTC");
122 }
jshin@chromium.org6f31ac32014-03-26 22:15:14 +0000123
Frank Tangf90543d2020-10-30 19:02:04 -0700124 // No way to support when DST is turned off and the offset in minutes is not a multiple of 60.
125 if (utcOffsetMins % 60 == 0) {
126 char gmtOffsetTz[11] = {}; // "Etc/GMT+dd" is 11-char long with a terminal null.
127 // Note '-' before 'utcOffsetMin'. The timezone ID's sign convention
128 // is that a timezone ahead of UTC is Etc/GMT-<offset> and a timezone
129 // behind UTC is Etc/GMT+<offset>.
130 int ret = snprintf(gmtOffsetTz, UPRV_LENGTHOF(gmtOffsetTz), "Etc/GMT%+ld", -utcOffsetMins / 60);
131 if (ret > 0 && ret < UPRV_LENGTHOF(gmtOffsetTz)) {
132 return uprv_strdup(gmtOffsetTz);
Jungshik Shin87232d82017-05-13 21:10:13 -0700133 }
jshin@chromium.org6f31ac32014-03-26 22:15:14 +0000134 }
Jungshik Shin (jungshik at google)0f8746a2015-01-08 15:46:45 -0800135 }
136
Frank Tangf90543d2020-10-30 19:02:04 -0700137 // If DST is NOT disabled, but the TimeZoneKeyName field of the struct is NULL, then we may be dealing with a
138 // RDP/terminal services session where the 'Time Zone Redirection' feature is enabled. However, either the RDP
139 // client sent the server incomplete info (some 3rd party RDP clients only send the StandardName and DaylightName,
140 // but do not send the important TimeZoneKeyName), or if the RDP server has not appropriately populated the struct correctly.
141 //
142 // In this case we unfortunately have no choice but to fallback to using the pre-Vista method of determining the
143 // time zone, which requires examining the registry directly.
144 //
145 // Note that this can however still fail though if the client and server are using different languages, as the StandardName
146 // that is sent by client is *localized* in the client's language. However, we must compare this to the names that are
147 // on the server, which are *localized* in registry using the server's language.
148 //
149 // One other note is that this fallback method doesn't work for the UWP version, as we can't use the registry APIs.
150
151 // windowsTimeZoneName will point at timezoneSubKeyName if we had to fallback to using the registry, and we found a match.
152 WCHAR timezoneSubKeyName[WINDOWS_MAX_REG_KEY_LENGTH];
153 WCHAR *windowsTimeZoneName = dynamicTZI.TimeZoneKeyName;
154
155 if (dynamicTZI.TimeZoneKeyName[0] == 0) {
156
157// We can't use the registry APIs in the UWP version.
158#if U_PLATFORM_HAS_WINUWP_API == 1
159 (void)timezoneSubKeyName; // suppress unused variable warnings.
160 return nullptr;
161#else
162 // Open the path to the time zones in the Windows registry.
163 LONG ret;
164 HKEY hKeyAllTimeZones = nullptr;
165 ret = RegOpenKeyExW(HKEY_LOCAL_MACHINE, WINDOWS_TIMEZONES_REG_KEY_PATH, 0, KEY_READ,
166 reinterpret_cast<PHKEY>(&hKeyAllTimeZones));
167
168 if (ret != ERROR_SUCCESS) {
169 // If we can't open the key, then we can't do much, so fail.
170 return nullptr;
171 }
172
173 // Read the number of subkeys under the time zone registry path.
174 DWORD numTimeZoneSubKeys;
175 ret = RegQueryInfoKeyW(hKeyAllTimeZones, nullptr, nullptr, nullptr, &numTimeZoneSubKeys,
176 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr);
177
178 if (ret != ERROR_SUCCESS) {
179 RegCloseKey(hKeyAllTimeZones);
180 return nullptr;
181 }
182
183 // Examine each of the subkeys to try and find a match for the localized standard name ("Std").
184 //
185 // Note: The name of the time zone subkey itself is not localized, but the "Std" name is localized. This means
186 // that we could fail to find a match if the RDP client and RDP server are using different languages, but unfortunately
187 // there isn't much we can do about it.
188 HKEY hKeyTimeZoneSubKey = nullptr;
189 ULONG registryValueType;
190 WCHAR registryStandardName[WINDOWS_MAX_REG_KEY_LENGTH];
191
192 for (DWORD i = 0; i < numTimeZoneSubKeys; i++) {
193 // Note: RegEnumKeyExW wants the size of the buffer in characters.
194 DWORD size = UPRV_LENGTHOF(timezoneSubKeyName);
195 ret = RegEnumKeyExW(hKeyAllTimeZones, i, timezoneSubKeyName, &size, nullptr, nullptr, nullptr, nullptr);
196
197 if (ret != ERROR_SUCCESS) {
198 RegCloseKey(hKeyAllTimeZones);
199 return nullptr;
200 }
201
202 ret = RegOpenKeyExW(hKeyAllTimeZones, timezoneSubKeyName, 0, KEY_READ,
203 reinterpret_cast<PHKEY>(&hKeyTimeZoneSubKey));
204
205 if (ret != ERROR_SUCCESS) {
206 RegCloseKey(hKeyAllTimeZones);
207 return nullptr;
208 }
209
210 // Note: RegQueryValueExW wants the size of the buffer in bytes.
211 size = sizeof(registryStandardName);
212 ret = RegQueryValueExW(hKeyTimeZoneSubKey, L"Std", nullptr, &registryValueType,
213 reinterpret_cast<LPBYTE>(registryStandardName), &size);
214
215 if (ret != ERROR_SUCCESS || registryValueType != REG_SZ) {
216 RegCloseKey(hKeyTimeZoneSubKey);
217 RegCloseKey(hKeyAllTimeZones);
218 return nullptr;
219 }
220
221 // Note: wcscmp does an ordinal (byte) comparison.
222 if (wcscmp(reinterpret_cast<WCHAR *>(registryStandardName), dynamicTZI.StandardName) == 0) {
223 // Since we are comparing the *localized* time zone name, it's possible that some languages might use
224 // the same string for more than one time zone. Thus we need to examine the TZI data in the registry to
225 // compare the GMT offset (the bias), and the DST transition dates, to ensure it's the same time zone
226 // as the currently reported one.
227 REG_TZI_FORMAT registryTziValue;
228 uprv_memset(&registryTziValue, 0, sizeof(registryTziValue));
229
230 // Note: RegQueryValueExW wants the size of the buffer in bytes.
231 DWORD timezoneTziValueSize = sizeof(registryTziValue);
232 ret = RegQueryValueExW(hKeyTimeZoneSubKey, L"TZI", nullptr, &registryValueType,
233 reinterpret_cast<LPBYTE>(&registryTziValue), &timezoneTziValueSize);
234
235 if (ret == ERROR_SUCCESS) {
236 if ((dynamicTZI.Bias == registryTziValue.Bias) &&
237 (memcmp((const void *)&dynamicTZI.StandardDate, (const void *)&registryTziValue.StandardDate, sizeof(SYSTEMTIME)) == 0) &&
238 (memcmp((const void *)&dynamicTZI.DaylightDate, (const void *)&registryTziValue.DaylightDate, sizeof(SYSTEMTIME)) == 0))
239 {
240 // We found a matching time zone.
241 windowsTimeZoneName = timezoneSubKeyName;
242 break;
243 }
244 }
245 }
246 RegCloseKey(hKeyTimeZoneSubKey);
247 hKeyTimeZoneSubKey = nullptr;
248 }
249
250 if (hKeyTimeZoneSubKey != nullptr) {
251 RegCloseKey(hKeyTimeZoneSubKey);
252 }
253 if (hKeyAllTimeZones != nullptr) {
254 RegCloseKey(hKeyAllTimeZones);
255 }
256#endif // U_PLATFORM_HAS_WINUWP_API
jshin@chromium.org6f31ac32014-03-26 22:15:14 +0000257 }
258
Frank Tangf90543d2020-10-30 19:02:04 -0700259 CharString winTZ;
260 UErrorCode status = U_ZERO_ERROR;
261 winTZ.appendInvariantChars(UnicodeString(TRUE, windowsTimeZoneName, -1), status);
262
263 // Map Windows Timezone name (non-localized) to ICU timezone ID (~ Olson timezone id).
264 StackUResourceBundle winTZBundle;
265 ures_openDirectFillIn(winTZBundle.getAlias(), nullptr, "windowsZones", &status);
266 ures_getByKey(winTZBundle.getAlias(), "mapTimezones", winTZBundle.getAlias(), &status);
267 ures_getByKey(winTZBundle.getAlias(), winTZ.data(), winTZBundle.getAlias(), &status);
268
269 if (U_FAILURE(status)) {
270 return nullptr;
271 }
272
273 // Note: Since the ISO 3166 country/region codes are all invariant ASCII chars, we can
274 // directly downcast from wchar_t to do the conversion.
275 // We could call the A version of the GetGeoInfo API, but that would be slightly slower than calling the W API,
276 // as the A version of the API will end up calling MultiByteToWideChar anyways internally.
277 wchar_t regionCodeW[3] = {};
278 char regionCode[3] = {}; // 2 letter ISO 3166 country/region code made entirely of invariant chars.
279 int geoId = GetUserGeoID(GEOCLASS_NATION);
280 int regionCodeLen = GetGeoInfoW(geoId, GEO_ISO2, regionCodeW, UPRV_LENGTHOF(regionCodeW), 0);
281
282 const UChar *icuTZ16 = nullptr;
283 int32_t tzLen;
284
285 if (regionCodeLen != 0) {
286 for (int i = 0; i < UPRV_LENGTHOF(regionCodeW); i++) {
287 regionCode[i] = static_cast<char>(regionCodeW[i]);
288 }
289 icuTZ16 = ures_getStringByKey(winTZBundle.getAlias(), regionCode, &tzLen, &status);
290 }
291 if (regionCodeLen == 0 || U_FAILURE(status)) {
292 // fallback to default "001" (world)
293 status = U_ZERO_ERROR;
294 icuTZ16 = ures_getStringByKey(winTZBundle.getAlias(), "001", &tzLen, &status);
295 }
296
297 // Note: cloneData returns nullptr if the status is a failure, so this
298 // will return nullptr if the above look-up fails.
299 CharString icuTZStr;
300 return icuTZStr.appendInvariantChars(icuTZ16, tzLen, status).cloneData(status);
jshin@chromium.org6f31ac32014-03-26 22:15:14 +0000301}
302
Jungshik Shinccad4472018-10-09 00:22:00 -0700303U_NAMESPACE_END
Jungshik Shin42d50272018-10-24 01:22:09 -0700304#endif /* U_PLATFORM_USES_ONLY_WIN32_API */