Jungshik Shin | 87232d8 | 2017-05-13 21:10:13 -0700 | [diff] [blame] | 1 | // © 2016 and later: Unicode, Inc. and others. |
Jungshik Shin | 5feb9ad | 2016-10-21 12:52:48 -0700 | [diff] [blame] | 2 | // License & terms of use: http://www.unicode.org/copyright.html |
jshin@chromium.org | 6f31ac3 | 2014-03-26 22:15:14 +0000 | [diff] [blame] | 3 | /* |
| 4 | ******************************************************************************** |
Jungshik Shin | 70f8250 | 2016-01-29 00:32:36 -0800 | [diff] [blame] | 5 | * Copyright (C) 2005-2015, International Business Machines |
jshin@chromium.org | 6f31ac3 | 2014-03-26 22:15:14 +0000 | [diff] [blame] | 6 | * Corporation and others. All Rights Reserved. |
| 7 | ******************************************************************************** |
| 8 | * |
| 9 | * File WINTZ.CPP |
| 10 | * |
| 11 | ******************************************************************************** |
| 12 | */ |
| 13 | |
| 14 | #include "unicode/utypes.h" |
| 15 | |
Jungshik Shin | 42d5027 | 2018-10-24 01:22:09 -0700 | [diff] [blame] | 16 | #if U_PLATFORM_USES_ONLY_WIN32_API |
jshin@chromium.org | 6f31ac3 | 2014-03-26 22:15:14 +0000 | [diff] [blame] | 17 | |
| 18 | #include "wintz.h" |
Frank Tang | f90543d | 2020-10-30 19:02:04 -0700 | [diff] [blame] | 19 | #include "charstr.h" |
jshin@chromium.org | 6f31ac3 | 2014-03-26 22:15:14 +0000 | [diff] [blame] | 20 | #include "cmemory.h" |
| 21 | #include "cstring.h" |
| 22 | |
jshin@chromium.org | 6f31ac3 | 2014-03-26 22:15:14 +0000 | [diff] [blame] | 23 | #include "unicode/ures.h" |
Frank Tang | f90543d | 2020-10-30 19:02:04 -0700 | [diff] [blame] | 24 | #include "unicode/unistr.h" |
Jungshik Shin | ccad447 | 2018-10-09 00:22:00 -0700 | [diff] [blame] | 25 | #include "uresimp.h" |
jshin@chromium.org | 6f31ac3 | 2014-03-26 22:15:14 +0000 | [diff] [blame] | 26 | |
Jungshik Shin | 87232d8 | 2017-05-13 21:10:13 -0700 | [diff] [blame] | 27 | #ifndef WIN32_LEAN_AND_MEAN |
jshin@chromium.org | 6f31ac3 | 2014-03-26 22:15:14 +0000 | [diff] [blame] | 28 | # define WIN32_LEAN_AND_MEAN |
Jungshik Shin | 87232d8 | 2017-05-13 21:10:13 -0700 | [diff] [blame] | 29 | #endif |
jshin@chromium.org | 6f31ac3 | 2014-03-26 22:15:14 +0000 | [diff] [blame] | 30 | # define VC_EXTRALEAN |
| 31 | # define NOUSER |
| 32 | # define NOSERVICE |
| 33 | # define NOIME |
| 34 | # define NOMCX |
| 35 | #include <windows.h> |
| 36 | |
Jungshik Shin | ccad447 | 2018-10-09 00:22:00 -0700 | [diff] [blame] | 37 | U_NAMESPACE_BEGIN |
jshin@chromium.org | 6f31ac3 | 2014-03-26 22:15:14 +0000 | [diff] [blame] | 38 | |
Frank Tang | f90543d | 2020-10-30 19:02:04 -0700 | [diff] [blame] | 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 |
jshin@chromium.org | 6f31ac3 | 2014-03-26 22:15:14 +0000 | [diff] [blame] | 62 | |
| 63 | /** |
Frank Tang | f90543d | 2020-10-30 19:02:04 -0700 | [diff] [blame] | 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). |
jshin@chromium.org | 6f31ac3 | 2014-03-26 22:15:14 +0000 | [diff] [blame] | 86 | */ |
Frank Tang | f90543d | 2020-10-30 19:02:04 -0700 | [diff] [blame] | 87 | U_CAPI const char* U_EXPORT2 |
Jungshik Shin | ccad447 | 2018-10-09 00:22:00 -0700 | [diff] [blame] | 88 | uprv_detectWindowsTimeZone() |
Jungshik Shin | 87232d8 | 2017-05-13 21:10:13 -0700 | [diff] [blame] | 89 | { |
Frank Tang | f90543d | 2020-10-30 19:02:04 -0700 | [diff] [blame] | 90 | // We first try to obtain the time zone directly by using the TimeZoneKeyName field of the DYNAMIC_TIME_ZONE_INFORMATION struct. |
Jungshik Shin | ccad447 | 2018-10-09 00:22:00 -0700 | [diff] [blame] | 91 | DYNAMIC_TIME_ZONE_INFORMATION dynamicTZI; |
| 92 | uprv_memset(&dynamicTZI, 0, sizeof(dynamicTZI)); |
Frank Tang | f90543d | 2020-10-30 19:02:04 -0700 | [diff] [blame] | 93 | SYSTEMTIME systemTimeAllZero; |
| 94 | uprv_memset(&systemTimeAllZero, 0, sizeof(systemTimeAllZero)); |
jshin@chromium.org | 6f31ac3 | 2014-03-26 22:15:14 +0000 | [diff] [blame] | 95 | |
Frank Tang | f90543d | 2020-10-30 19:02:04 -0700 | [diff] [blame] | 96 | if (GetDynamicTimeZoneInformation(&dynamicTZI) == TIME_ZONE_ID_INVALID) { |
Jungshik Shin | ccad447 | 2018-10-09 00:22:00 -0700 | [diff] [blame] | 97 | return nullptr; |
| 98 | } |
jshin@chromium.org | 6f31ac3 | 2014-03-26 22:15:14 +0000 | [diff] [blame] | 99 | |
Frank Tang | f90543d | 2020-10-30 19:02:04 -0700 | [diff] [blame] | 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 | } |
jshin@chromium.org | 6f31ac3 | 2014-03-26 22:15:14 +0000 | [diff] [blame] | 123 | |
Frank Tang | f90543d | 2020-10-30 19:02:04 -0700 | [diff] [blame] | 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); |
Jungshik Shin | 87232d8 | 2017-05-13 21:10:13 -0700 | [diff] [blame] | 133 | } |
jshin@chromium.org | 6f31ac3 | 2014-03-26 22:15:14 +0000 | [diff] [blame] | 134 | } |
Jungshik Shin (jungshik at google) | 0f8746a | 2015-01-08 15:46:45 -0800 | [diff] [blame] | 135 | } |
| 136 | |
Frank Tang | f90543d | 2020-10-30 19:02:04 -0700 | [diff] [blame] | 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, ®istryValueType, |
| 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(®istryTziValue, 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, ®istryValueType, |
| 233 | reinterpret_cast<LPBYTE>(®istryTziValue), &timezoneTziValueSize); |
| 234 | |
| 235 | if (ret == ERROR_SUCCESS) { |
| 236 | if ((dynamicTZI.Bias == registryTziValue.Bias) && |
| 237 | (memcmp((const void *)&dynamicTZI.StandardDate, (const void *)®istryTziValue.StandardDate, sizeof(SYSTEMTIME)) == 0) && |
| 238 | (memcmp((const void *)&dynamicTZI.DaylightDate, (const void *)®istryTziValue.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.org | 6f31ac3 | 2014-03-26 22:15:14 +0000 | [diff] [blame] | 257 | } |
| 258 | |
Frank Tang | f90543d | 2020-10-30 19:02:04 -0700 | [diff] [blame] | 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 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.org | 6f31ac3 | 2014-03-26 22:15:14 +0000 | [diff] [blame] | 301 | } |
| 302 | |
Jungshik Shin | ccad447 | 2018-10-09 00:22:00 -0700 | [diff] [blame] | 303 | U_NAMESPACE_END |
Jungshik Shin | 42d5027 | 2018-10-24 01:22:09 -0700 | [diff] [blame] | 304 | #endif /* U_PLATFORM_USES_ONLY_WIN32_API */ |