wintz.cpp (16418B)
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 */