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 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
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 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).
86 */
87 U_CAPI const char* U_EXPORT2
uprv_detectWindowsTimeZone()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             // 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);
133             }
134         }
135     }
136 
137     // 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
257     }
258 
259     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 tzListLen = 0;
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, &tzListLen, &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", &tzListLen, &status);
295     }
296 
297     // Note: We want the first entry in the string returned by ures_getStringByKey.
298     // However this string can be a space delimited list of timezones:
299     //  Ex: "America/New_York America/Detroit America/Indiana/Petersburg ..."
300     // We need to stop at the first space, so we pass tzLen (instead of tzListLen) to appendInvariantChars below.
301     int32_t tzLen = 0;
302     if (tzListLen > 0) {
303         while (!(icuTZ16[tzLen] == u'\0' || icuTZ16[tzLen] == u' ')) {
304             tzLen++;
305         }
306     }
307 
308     // Note: cloneData returns nullptr if the status is a failure, so this
309     // will return nullptr if the above look-up fails.
310     CharString icuTZStr;
311     return icuTZStr.appendInvariantChars(icuTZ16, tzLen, status).cloneData(status);
312 }
313 
314 U_NAMESPACE_END
315 #endif /* U_PLATFORM_USES_ONLY_WIN32_API  */
316