1 | // © 2016 and later: Unicode, Inc. and others. |
2 | // License & terms of use: http://www.unicode.org/copyright.html |
3 | /* |
4 | ******************************************************************************** |
5 | * Copyright (C) 2005-2015, International Business Machines |
6 | * Corporation and others. All Rights Reserved. |
7 | ******************************************************************************** |
8 | * |
9 | * File WINTZ.CPP |
10 | * |
11 | ******************************************************************************** |
12 | */ |
13 | |
14 | #include "unicode/utypes.h" |
15 | |
16 | #if U_PLATFORM_USES_ONLY_WIN32_API |
17 | |
18 | #include "wintz.h" |
19 | #include "charstr.h" |
20 | #include "cmemory.h" |
21 | #include "cstring.h" |
22 | |
23 | #include "unicode/ures.h" |
24 | #include "unicode/unistr.h" |
25 | #include "uresimp.h" |
26 | |
27 | #ifndef WIN32_LEAN_AND_MEAN |
28 | # define WIN32_LEAN_AND_MEAN |
29 | #endif |
30 | # define VC_EXTRALEAN |
31 | # define NOUSER |
32 | # define NOSERVICE |
33 | # define NOIME |
34 | # define NOMCX |
35 | #include <windows.h> |
36 | |
37 | U_NAMESPACE_BEGIN |
38 | |
39 | // Note these constants and the struct are only used when dealing with the fallback path for RDP sessions. |
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 |
53 | typedef 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 |
62 | |
63 | /** |
64 | * 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 nullptr. 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). |
86 | */ |
87 | U_CAPI const char* U_EXPORT2 |
88 | uprv_detectWindowsTimeZone() |
89 | { |
90 | // We first try to obtain the time zone directly by using the TimeZoneKeyName field of the DYNAMIC_TIME_ZONE_INFORMATION struct. |
91 | DYNAMIC_TIME_ZONE_INFORMATION dynamicTZI; |
92 | uprv_memset(&dynamicTZI, 0, sizeof(dynamicTZI)); |
93 | SYSTEMTIME systemTimeAllZero; |
94 | uprv_memset(&systemTimeAllZero, 0, sizeof(systemTimeAllZero)); |
95 | |
96 | if (GetDynamicTimeZoneInformation(&dynamicTZI) == TIME_ZONE_ID_INVALID) { |
97 | return nullptr; |
98 | } |
99 | |
100 | // 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 | } |
123 | |
124 | // 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 | // Important note on the sign convention for zones: |
128 | // |
129 | // From https://en.wikipedia.org/wiki/Tz_database#Area |
130 | // "In order to conform with the POSIX style, those zone names beginning with "Etc/GMT" have their sign reversed |
131 | // from the standard ISO 8601 convention. In the "Etc" area, zones west of GMT have a positive sign and those |
132 | // east have a negative sign in their name (e.g "Etc/GMT-14" is 14 hours ahead of GMT)." |
133 | // |
134 | // Regarding the POSIX style, from https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html |
135 | // "The offset specifies the time value you must add to the local time to get a Coordinated Universal Time value." |
136 | // |
137 | // However, the Bias value in DYNAMIC_TIME_ZONE_INFORMATION *already* follows the POSIX convention. |
138 | // |
139 | // From https://docs.microsoft.com/en-us/windows/win32/api/timezoneapi/ns-timezoneapi-dynamic_time_zone_information |
140 | // "The bias is the difference, in minutes, between Coordinated Universal Time (UTC) and |
141 | // local time. All translations between UTC and local time are based on the following formula: |
142 | // UTC = local time + bias" |
143 | // |
144 | // For example, a time zone that is 3 hours ahead of UTC (UTC+03:00) would have a Bias value of -180, and the |
145 | // corresponding time zone ID would be "Etc/GMT-3". (So there is no need to negate utcOffsetMins below.) |
146 | int ret = snprintf(gmtOffsetTz, sizeof(gmtOffsetTz), "Etc/GMT%+ld" , utcOffsetMins / 60); |
147 | if (ret > 0 && ret < UPRV_LENGTHOF(gmtOffsetTz)) { |
148 | return uprv_strdup(gmtOffsetTz); |
149 | } |
150 | } |
151 | } |
152 | |
153 | // If DST is NOT disabled, but the TimeZoneKeyName field of the struct is nullptr, then we may be dealing with a |
154 | // RDP/terminal services session where the 'Time Zone Redirection' feature is enabled. However, either the RDP |
155 | // client sent the server incomplete info (some 3rd party RDP clients only send the StandardName and DaylightName, |
156 | // but do not send the important TimeZoneKeyName), or if the RDP server has not appropriately populated the struct correctly. |
157 | // |
158 | // In this case we unfortunately have no choice but to fallback to using the pre-Vista method of determining the |
159 | // time zone, which requires examining the registry directly. |
160 | // |
161 | // Note that this can however still fail though if the client and server are using different languages, as the StandardName |
162 | // that is sent by client is *localized* in the client's language. However, we must compare this to the names that are |
163 | // on the server, which are *localized* in registry using the server's language. |
164 | // |
165 | // One other note is that this fallback method doesn't work for the UWP version, as we can't use the registry APIs. |
166 | |
167 | // windowsTimeZoneName will point at timezoneSubKeyName if we had to fallback to using the registry, and we found a match. |
168 | WCHAR timezoneSubKeyName[WINDOWS_MAX_REG_KEY_LENGTH]; |
169 | WCHAR *windowsTimeZoneName = dynamicTZI.TimeZoneKeyName; |
170 | |
171 | if (dynamicTZI.TimeZoneKeyName[0] == 0) { |
172 | |
173 | // We can't use the registry APIs in the UWP version. |
174 | #if U_PLATFORM_HAS_WINUWP_API == 1 |
175 | (void)timezoneSubKeyName; // suppress unused variable warnings. |
176 | return nullptr; |
177 | #else |
178 | // Open the path to the time zones in the Windows registry. |
179 | LONG ret; |
180 | HKEY hKeyAllTimeZones = nullptr; |
181 | ret = RegOpenKeyExW(HKEY_LOCAL_MACHINE, WINDOWS_TIMEZONES_REG_KEY_PATH, 0, KEY_READ, |
182 | reinterpret_cast<PHKEY>(&hKeyAllTimeZones)); |
183 | |
184 | if (ret != ERROR_SUCCESS) { |
185 | // If we can't open the key, then we can't do much, so fail. |
186 | return nullptr; |
187 | } |
188 | |
189 | // Read the number of subkeys under the time zone registry path. |
190 | DWORD numTimeZoneSubKeys; |
191 | ret = RegQueryInfoKeyW(hKeyAllTimeZones, nullptr, nullptr, nullptr, &numTimeZoneSubKeys, |
192 | nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr); |
193 | |
194 | if (ret != ERROR_SUCCESS) { |
195 | RegCloseKey(hKeyAllTimeZones); |
196 | return nullptr; |
197 | } |
198 | |
199 | // Examine each of the subkeys to try and find a match for the localized standard name ("Std"). |
200 | // |
201 | // Note: The name of the time zone subkey itself is not localized, but the "Std" name is localized. This means |
202 | // that we could fail to find a match if the RDP client and RDP server are using different languages, but unfortunately |
203 | // there isn't much we can do about it. |
204 | HKEY hKeyTimeZoneSubKey = nullptr; |
205 | ULONG registryValueType; |
206 | WCHAR registryStandardName[WINDOWS_MAX_REG_KEY_LENGTH]; |
207 | |
208 | for (DWORD i = 0; i < numTimeZoneSubKeys; i++) { |
209 | // Note: RegEnumKeyExW wants the size of the buffer in characters. |
210 | DWORD size = UPRV_LENGTHOF(timezoneSubKeyName); |
211 | ret = RegEnumKeyExW(hKeyAllTimeZones, i, timezoneSubKeyName, &size, nullptr, nullptr, nullptr, nullptr); |
212 | |
213 | if (ret != ERROR_SUCCESS) { |
214 | RegCloseKey(hKeyAllTimeZones); |
215 | return nullptr; |
216 | } |
217 | |
218 | ret = RegOpenKeyExW(hKeyAllTimeZones, timezoneSubKeyName, 0, KEY_READ, |
219 | reinterpret_cast<PHKEY>(&hKeyTimeZoneSubKey)); |
220 | |
221 | if (ret != ERROR_SUCCESS) { |
222 | RegCloseKey(hKeyAllTimeZones); |
223 | return nullptr; |
224 | } |
225 | |
226 | // Note: RegQueryValueExW wants the size of the buffer in bytes. |
227 | size = sizeof(registryStandardName); |
228 | ret = RegQueryValueExW(hKeyTimeZoneSubKey, L"Std" , nullptr, ®istryValueType, |
229 | reinterpret_cast<LPBYTE>(registryStandardName), &size); |
230 | |
231 | if (ret != ERROR_SUCCESS || registryValueType != REG_SZ) { |
232 | RegCloseKey(hKeyTimeZoneSubKey); |
233 | RegCloseKey(hKeyAllTimeZones); |
234 | return nullptr; |
235 | } |
236 | |
237 | // Note: wcscmp does an ordinal (byte) comparison. |
238 | if (wcscmp(reinterpret_cast<WCHAR *>(registryStandardName), dynamicTZI.StandardName) == 0) { |
239 | // Since we are comparing the *localized* time zone name, it's possible that some languages might use |
240 | // the same string for more than one time zone. Thus we need to examine the TZI data in the registry to |
241 | // compare the GMT offset (the bias), and the DST transition dates, to ensure it's the same time zone |
242 | // as the currently reported one. |
243 | REG_TZI_FORMAT registryTziValue; |
244 | uprv_memset(®istryTziValue, 0, sizeof(registryTziValue)); |
245 | |
246 | // Note: RegQueryValueExW wants the size of the buffer in bytes. |
247 | DWORD timezoneTziValueSize = sizeof(registryTziValue); |
248 | ret = RegQueryValueExW(hKeyTimeZoneSubKey, L"TZI" , nullptr, ®istryValueType, |
249 | reinterpret_cast<LPBYTE>(®istryTziValue), &timezoneTziValueSize); |
250 | |
251 | if (ret == ERROR_SUCCESS) { |
252 | if ((dynamicTZI.Bias == registryTziValue.Bias) && |
253 | (memcmp((const void *)&dynamicTZI.StandardDate, (const void *)®istryTziValue.StandardDate, sizeof(SYSTEMTIME)) == 0) && |
254 | (memcmp((const void *)&dynamicTZI.DaylightDate, (const void *)®istryTziValue.DaylightDate, sizeof(SYSTEMTIME)) == 0)) |
255 | { |
256 | // We found a matching time zone. |
257 | windowsTimeZoneName = timezoneSubKeyName; |
258 | break; |
259 | } |
260 | } |
261 | } |
262 | RegCloseKey(hKeyTimeZoneSubKey); |
263 | hKeyTimeZoneSubKey = nullptr; |
264 | } |
265 | |
266 | if (hKeyTimeZoneSubKey != nullptr) { |
267 | RegCloseKey(hKeyTimeZoneSubKey); |
268 | } |
269 | if (hKeyAllTimeZones != nullptr) { |
270 | RegCloseKey(hKeyAllTimeZones); |
271 | } |
272 | #endif // U_PLATFORM_HAS_WINUWP_API |
273 | } |
274 | |
275 | CharString winTZ; |
276 | UErrorCode status = U_ZERO_ERROR; |
277 | winTZ.appendInvariantChars(UnicodeString(true, windowsTimeZoneName, -1), status); |
278 | |
279 | // Map Windows Timezone name (non-localized) to ICU timezone ID (~ Olson timezone id). |
280 | StackUResourceBundle winTZBundle; |
281 | ures_openDirectFillIn(winTZBundle.getAlias(), nullptr, "windowsZones" , &status); |
282 | ures_getByKey(winTZBundle.getAlias(), "mapTimezones" , winTZBundle.getAlias(), &status); |
283 | ures_getByKey(winTZBundle.getAlias(), winTZ.data(), winTZBundle.getAlias(), &status); |
284 | |
285 | if (U_FAILURE(status)) { |
286 | return nullptr; |
287 | } |
288 | |
289 | // Note: Since the ISO 3166 country/region codes are all invariant ASCII chars, we can |
290 | // directly downcast from wchar_t to do the conversion. |
291 | // We could call the A version of the GetGeoInfo API, but that would be slightly slower than calling the W API, |
292 | // as the A version of the API will end up calling MultiByteToWideChar anyways internally. |
293 | wchar_t regionCodeW[3] = {}; |
294 | char regionCode[3] = {}; // 2 letter ISO 3166 country/region code made entirely of invariant chars. |
295 | int geoId = GetUserGeoID(GEOCLASS_NATION); |
296 | int regionCodeLen = GetGeoInfoW(geoId, GEO_ISO2, regionCodeW, UPRV_LENGTHOF(regionCodeW), 0); |
297 | |
298 | const char16_t *icuTZ16 = nullptr; |
299 | int32_t tzListLen = 0; |
300 | |
301 | if (regionCodeLen != 0) { |
302 | for (int i = 0; i < UPRV_LENGTHOF(regionCodeW); i++) { |
303 | regionCode[i] = static_cast<char>(regionCodeW[i]); |
304 | } |
305 | icuTZ16 = ures_getStringByKey(winTZBundle.getAlias(), regionCode, &tzListLen, &status); |
306 | } |
307 | if (regionCodeLen == 0 || U_FAILURE(status)) { |
308 | // fallback to default "001" (world) |
309 | status = U_ZERO_ERROR; |
310 | icuTZ16 = ures_getStringByKey(winTZBundle.getAlias(), "001" , &tzListLen, &status); |
311 | } |
312 | |
313 | // Note: We want the first entry in the string returned by ures_getStringByKey. |
314 | // However this string can be a space delimited list of timezones: |
315 | // Ex: "America/New_York America/Detroit America/Indiana/Petersburg ..." |
316 | // We need to stop at the first space, so we pass tzLen (instead of tzListLen) to appendInvariantChars below. |
317 | int32_t tzLen = 0; |
318 | if (tzListLen > 0) { |
319 | while (!(icuTZ16[tzLen] == u'\0' || icuTZ16[tzLen] == u' ')) { |
320 | tzLen++; |
321 | } |
322 | } |
323 | |
324 | // Note: cloneData returns nullptr if the status is a failure, so this |
325 | // will return nullptr if the above look-up fails. |
326 | CharString icuTZStr; |
327 | return icuTZStr.appendInvariantChars(icuTZ16, tzLen, status).cloneData(status); |
328 | } |
329 | |
330 | U_NAMESPACE_END |
331 | #endif /* U_PLATFORM_USES_ONLY_WIN32_API */ |
332 | |