|
@@ -36,17 +36,58 @@
|
|
|
|
|
|
U_NAMESPACE_BEGIN
|
|
|
|
|
|
+// Note these constants and the struct are only used when dealing with the fallback path for RDP sesssions.
|
|
|
+
|
|
|
+// This is the location of the time zones in the registry on Vista+ systems.
|
|
|
+// See: https://docs.microsoft.com/windows/win32/api/timezoneapi/ns-timezoneapi-dynamic_time_zone_information
|
|
|
+#define WINDOWS_TIMEZONES_REG_KEY_PATH L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones"
|
|
|
+
|
|
|
+// Max length for a registry key is 255. +1 for null.
|
|
|
+// See: https://docs.microsoft.com/windows/win32/sysinfo/registry-element-size-limits
|
|
|
+#define WINDOWS_MAX_REG_KEY_LENGTH 256
|
|
|
+
|
|
|
+#if U_PLATFORM_HAS_WINUWP_API == 0
|
|
|
+
|
|
|
+// This is the layout of the TZI binary value in the registry.
|
|
|
+// See: https://docs.microsoft.com/windows/win32/api/timezoneapi/ns-timezoneapi-time_zone_information
|
|
|
+typedef struct _REG_TZI_FORMAT {
|
|
|
+ LONG Bias;
|
|
|
+ LONG StandardBias;
|
|
|
+ LONG DaylightBias;
|
|
|
+ SYSTEMTIME StandardDate;
|
|
|
+ SYSTEMTIME DaylightDate;
|
|
|
+} REG_TZI_FORMAT;
|
|
|
+
|
|
|
+#endif // U_PLATFORM_HAS_WINUWP_API
|
|
|
+
|
|
|
/**
|
|
|
-* Main Windows time zone detection function.
|
|
|
-* Returns the Windows time zone converted to an ICU time zone as a heap-allocated buffer, or nullptr upon failure.
|
|
|
+* This is main Windows time zone detection function.
|
|
|
+*
|
|
|
+* It returns the Windows time zone converted to an ICU time zone as a heap-allocated buffer, or nullptr upon failure.
|
|
|
*
|
|
|
-* Note: We use the Win32 API GetDynamicTimeZoneInformation (available since Vista+) to get the current time zone info.
|
|
|
-* This API returns a non-localized time zone name, which is mapped to an ICU time zone ID (~ Olsen ID).
|
|
|
+* We use the Win32 API GetDynamicTimeZoneInformation (which is available since Vista) to get the current time zone info,
|
|
|
+* as this API returns a non-localized time zone name which can be then mapped to an ICU time zone.
|
|
|
+*
|
|
|
+* However, in some RDP/terminal services situations, this struct isn't always fully complete, and the TimeZoneKeyName
|
|
|
+* field of the struct might be NULL. This can happen with some 3rd party RDP clients, and also when using older versions
|
|
|
+* of the RDP protocol, which don't send the newer TimeZoneKeyNamei information and only send the StandardName and DaylightName.
|
|
|
+*
|
|
|
+* Since these 3rd party clients and older RDP clients only send the pre-Vista time zone information to the server, this means that we
|
|
|
+* need to fallback on using the pre-Vista methods to determine the time zone. This unfortunately requires examining the registry directly
|
|
|
+* in order to try and determine the current time zone.
|
|
|
+*
|
|
|
+* Note that this can however still fail in some cases though if the client and server are using different languages, as the StandardName
|
|
|
+* 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
|
|
|
+* are localized in registry using the server's language. Despite that, this is the best we can do.
|
|
|
+*
|
|
|
+* Note: This fallback method won't work for the UWP version though, as we can't use the registry APIs in UWP.
|
|
|
+*
|
|
|
+* Once we have the current Windows time zone, then we can then map it to an ICU time zone ID (~ Olsen ID).
|
|
|
*/
|
|
|
U_CAPI const char* U_EXPORT2
|
|
|
uprv_detectWindowsTimeZone()
|
|
|
{
|
|
|
- // Obtain the DYNAMIC_TIME_ZONE_INFORMATION info to get the non-localized time zone name.
|
|
|
+ // We first try to obtain the time zone directly by using the TimeZoneKeyName field of the DYNAMIC_TIME_ZONE_INFORMATION struct.
|
|
|
DYNAMIC_TIME_ZONE_INFORMATION dynamicTZI;
|
|
|
uprv_memset(&dynamicTZI, 0, sizeof(dynamicTZI));
|
|
|
SYSTEMTIME systemTimeAllZero;
|
|
@@ -86,22 +127,138 @@ uprv_detectWindowsTimeZone()
|
|
|
// Note '-' before 'utcOffsetMin'. The timezone ID's sign convention
|
|
|
// is that a timezone ahead of UTC is Etc/GMT-<offset> and a timezone
|
|
|
// behind UTC is Etc/GMT+<offset>.
|
|
|
- int ret = snprintf(gmtOffsetTz, UPRV_LENGTHOF(gmtOffsetTz), "Etc/GMT%+d", -utcOffsetMins / 60);
|
|
|
+ int ret = snprintf(gmtOffsetTz, UPRV_LENGTHOF(gmtOffsetTz), "Etc/GMT%+ld", -utcOffsetMins / 60);
|
|
|
if (ret > 0 && ret < UPRV_LENGTHOF(gmtOffsetTz)) {
|
|
|
return uprv_strdup(gmtOffsetTz);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // If DST is NOT disabled, but we have an empty TimeZoneKeyName, then it is unclear
|
|
|
- // what we should do as this should not happen.
|
|
|
+ // If DST is NOT disabled, but the TimeZoneKeyName field of the struct is NULL, then we may be dealing with a
|
|
|
+ // RDP/terminal services session where the 'Time Zone Redirection' feature is enabled. However, either the RDP
|
|
|
+ // client sent the server incomplete info (some 3rd party RDP clients only send the StandardName and DaylightName,
|
|
|
+ // but do not send the important TimeZoneKeyName), or if the RDP server has not appropriately populated the struct correctly.
|
|
|
+ //
|
|
|
+ // In this case we unfortunately have no choice but to fallback to using the pre-Vista method of determining the
|
|
|
+ // time zone, which requires examining the registry directly.
|
|
|
+ //
|
|
|
+ // Note that this can however still fail though if the client and server are using different languages, as the StandardName
|
|
|
+ // 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 are *localized* in registry using the server's language.
|
|
|
+ //
|
|
|
+ // One other note is that this fallback method doesn't work for the UWP version, as we can't use the registry APIs.
|
|
|
+
|
|
|
+ // windowsTimeZoneName will point at timezoneSubKeyName if we had to fallback to using the registry, and we found a match.
|
|
|
+ WCHAR timezoneSubKeyName[WINDOWS_MAX_REG_KEY_LENGTH];
|
|
|
+ WCHAR *windowsTimeZoneName = dynamicTZI.TimeZoneKeyName;
|
|
|
+
|
|
|
if (dynamicTZI.TimeZoneKeyName[0] == 0) {
|
|
|
+
|
|
|
+// We can't use the registry APIs in the UWP version.
|
|
|
+#if U_PLATFORM_HAS_WINUWP_API == 1
|
|
|
+ (void)timezoneSubKeyName; // suppress unused variable warnings.
|
|
|
return nullptr;
|
|
|
+#else
|
|
|
+ // Open the path to the time zones in the Windows registry.
|
|
|
+ LONG ret;
|
|
|
+ HKEY hKeyAllTimeZones = nullptr;
|
|
|
+ ret = RegOpenKeyExW(HKEY_LOCAL_MACHINE, WINDOWS_TIMEZONES_REG_KEY_PATH, 0, KEY_READ,
|
|
|
+ reinterpret_cast<PHKEY>(&hKeyAllTimeZones));
|
|
|
+
|
|
|
+ if (ret != ERROR_SUCCESS) {
|
|
|
+ // If we can't open the key, then we can't do much, so fail.
|
|
|
+ return nullptr;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Read the number of subkeys under the time zone registry path.
|
|
|
+ DWORD numTimeZoneSubKeys;
|
|
|
+ ret = RegQueryInfoKeyW(hKeyAllTimeZones, nullptr, nullptr, nullptr, &numTimeZoneSubKeys,
|
|
|
+ nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr);
|
|
|
+
|
|
|
+ if (ret != ERROR_SUCCESS) {
|
|
|
+ RegCloseKey(hKeyAllTimeZones);
|
|
|
+ return nullptr;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Examine each of the subkeys to try and find a match for the localized standard name ("Std").
|
|
|
+ //
|
|
|
+ // Note: The name of the time zone subkey itself is not localized, but the "Std" name is localized. This means
|
|
|
+ // that we could fail to find a match if the RDP client and RDP server are using different languages, but unfortunately
|
|
|
+ // there isn't much we can do about it.
|
|
|
+ HKEY hKeyTimeZoneSubKey = nullptr;
|
|
|
+ ULONG registryValueType;
|
|
|
+ WCHAR registryStandardName[WINDOWS_MAX_REG_KEY_LENGTH];
|
|
|
+
|
|
|
+ for (DWORD i = 0; i < numTimeZoneSubKeys; i++) {
|
|
|
+ // Note: RegEnumKeyExW wants the size of the buffer in characters.
|
|
|
+ DWORD size = UPRV_LENGTHOF(timezoneSubKeyName);
|
|
|
+ ret = RegEnumKeyExW(hKeyAllTimeZones, i, timezoneSubKeyName, &size, nullptr, nullptr, nullptr, nullptr);
|
|
|
+
|
|
|
+ if (ret != ERROR_SUCCESS) {
|
|
|
+ RegCloseKey(hKeyAllTimeZones);
|
|
|
+ return nullptr;
|
|
|
+ }
|
|
|
+
|
|
|
+ ret = RegOpenKeyExW(hKeyAllTimeZones, timezoneSubKeyName, 0, KEY_READ,
|
|
|
+ reinterpret_cast<PHKEY>(&hKeyTimeZoneSubKey));
|
|
|
+
|
|
|
+ if (ret != ERROR_SUCCESS) {
|
|
|
+ RegCloseKey(hKeyAllTimeZones);
|
|
|
+ return nullptr;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Note: RegQueryValueExW wants the size of the buffer in bytes.
|
|
|
+ size = sizeof(registryStandardName);
|
|
|
+ ret = RegQueryValueExW(hKeyTimeZoneSubKey, L"Std", nullptr, ®istryValueType,
|
|
|
+ reinterpret_cast<LPBYTE>(registryStandardName), &size);
|
|
|
+
|
|
|
+ if (ret != ERROR_SUCCESS || registryValueType != REG_SZ) {
|
|
|
+ RegCloseKey(hKeyTimeZoneSubKey);
|
|
|
+ RegCloseKey(hKeyAllTimeZones);
|
|
|
+ return nullptr;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Note: wcscmp does an ordinal (byte) comparison.
|
|
|
+ if (wcscmp(reinterpret_cast<WCHAR *>(registryStandardName), dynamicTZI.StandardName) == 0) {
|
|
|
+ // Since we are comparing the *localized* time zone name, it's possible that some languages might use
|
|
|
+ // the same string for more than one time zone. Thus we need to examine the TZI data in the registry to
|
|
|
+ // compare the GMT offset (the bias), and the DST transition dates, to ensure it's the same time zone
|
|
|
+ // as the currently reported one.
|
|
|
+ REG_TZI_FORMAT registryTziValue;
|
|
|
+ uprv_memset(®istryTziValue, 0, sizeof(registryTziValue));
|
|
|
+
|
|
|
+ // Note: RegQueryValueExW wants the size of the buffer in bytes.
|
|
|
+ DWORD timezoneTziValueSize = sizeof(registryTziValue);
|
|
|
+ ret = RegQueryValueExW(hKeyTimeZoneSubKey, L"TZI", nullptr, ®istryValueType,
|
|
|
+ reinterpret_cast<LPBYTE>(®istryTziValue), &timezoneTziValueSize);
|
|
|
+
|
|
|
+ if (ret == ERROR_SUCCESS) {
|
|
|
+ if ((dynamicTZI.Bias == registryTziValue.Bias) &&
|
|
|
+ (memcmp((const void *)&dynamicTZI.StandardDate, (const void *)®istryTziValue.StandardDate, sizeof(SYSTEMTIME)) == 0) &&
|
|
|
+ (memcmp((const void *)&dynamicTZI.DaylightDate, (const void *)®istryTziValue.DaylightDate, sizeof(SYSTEMTIME)) == 0))
|
|
|
+ {
|
|
|
+ // We found a matching time zone.
|
|
|
+ windowsTimeZoneName = timezoneSubKeyName;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ RegCloseKey(hKeyTimeZoneSubKey);
|
|
|
+ hKeyTimeZoneSubKey = nullptr;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (hKeyTimeZoneSubKey != nullptr) {
|
|
|
+ RegCloseKey(hKeyTimeZoneSubKey);
|
|
|
+ }
|
|
|
+ if (hKeyAllTimeZones != nullptr) {
|
|
|
+ RegCloseKey(hKeyAllTimeZones);
|
|
|
+ }
|
|
|
+#endif // U_PLATFORM_HAS_WINUWP_API
|
|
|
}
|
|
|
|
|
|
CharString winTZ;
|
|
|
UErrorCode status = U_ZERO_ERROR;
|
|
|
- winTZ.appendInvariantChars(UnicodeString(TRUE, dynamicTZI.TimeZoneKeyName, -1), status);
|
|
|
+ winTZ.appendInvariantChars(UnicodeString(TRUE, windowsTimeZoneName, -1), status);
|
|
|
|
|
|
// Map Windows Timezone name (non-localized) to ICU timezone ID (~ Olson timezone id).
|
|
|
StackUResourceBundle winTZBundle;
|
|
@@ -123,18 +280,29 @@ uprv_detectWindowsTimeZone()
|
|
|
int regionCodeLen = GetGeoInfoW(geoId, GEO_ISO2, regionCodeW, UPRV_LENGTHOF(regionCodeW), 0);
|
|
|
|
|
|
const UChar *icuTZ16 = nullptr;
|
|
|
- int32_t tzLen;
|
|
|
+ int32_t tzListLen = 0;
|
|
|
|
|
|
if (regionCodeLen != 0) {
|
|
|
for (int i = 0; i < UPRV_LENGTHOF(regionCodeW); i++) {
|
|
|
regionCode[i] = static_cast<char>(regionCodeW[i]);
|
|
|
}
|
|
|
- icuTZ16 = ures_getStringByKey(winTZBundle.getAlias(), regionCode, &tzLen, &status);
|
|
|
+ icuTZ16 = ures_getStringByKey(winTZBundle.getAlias(), regionCode, &tzListLen, &status);
|
|
|
}
|
|
|
if (regionCodeLen == 0 || U_FAILURE(status)) {
|
|
|
// fallback to default "001" (world)
|
|
|
status = U_ZERO_ERROR;
|
|
|
- icuTZ16 = ures_getStringByKey(winTZBundle.getAlias(), "001", &tzLen, &status);
|
|
|
+ icuTZ16 = ures_getStringByKey(winTZBundle.getAlias(), "001", &tzListLen, &status);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Note: We want the first entry in the string returned by ures_getStringByKey.
|
|
|
+ // However this string can be a space delimited list of timezones:
|
|
|
+ // Ex: "America/New_York America/Detroit America/Indiana/Petersburg ..."
|
|
|
+ // We need to stop at the first space, so we pass tzLen (instead of tzListLen) to appendInvariantChars below.
|
|
|
+ int32_t tzLen = 0;
|
|
|
+ if (tzListLen > 0) {
|
|
|
+ while (!(icuTZ16[tzLen] == u'\0' || icuTZ16[tzLen] == u' ')) {
|
|
|
+ tzLen++;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// Note: cloneData returns nullptr if the status is a failure, so this
|