wintz.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  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. #include "unicode/utypes.h"
  14. #if U_PLATFORM_USES_ONLY_WIN32_API
  15. #include "wintz.h"
  16. #include "charstr.h"
  17. #include "cmemory.h"
  18. #include "cstring.h"
  19. #include "unicode/ures.h"
  20. #include "unicode/unistr.h"
  21. #include "uresimp.h"
  22. #ifndef WIN32_LEAN_AND_MEAN
  23. # define WIN32_LEAN_AND_MEAN
  24. #endif
  25. # define VC_EXTRALEAN
  26. # define NOUSER
  27. # define NOSERVICE
  28. # define NOIME
  29. # define NOMCX
  30. #include <windows.h>
  31. U_NAMESPACE_BEGIN
  32. // Note these constants and the struct are only used when dealing with the fallback path for RDP sessions.
  33. // This is the location of the time zones in the registry on Vista+ systems.
  34. // See: https://docs.microsoft.com/windows/win32/api/timezoneapi/ns-timezoneapi-dynamic_time_zone_information
  35. #define WINDOWS_TIMEZONES_REG_KEY_PATH L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones"
  36. // Max length for a registry key is 255. +1 for null.
  37. // See: https://docs.microsoft.com/windows/win32/sysinfo/registry-element-size-limits
  38. #define WINDOWS_MAX_REG_KEY_LENGTH 256
  39. #if U_PLATFORM_HAS_WINUWP_API == 0
  40. // This is the layout of the TZI binary value in the registry.
  41. // See: https://docs.microsoft.com/windows/win32/api/timezoneapi/ns-timezoneapi-time_zone_information
  42. typedef struct _REG_TZI_FORMAT {
  43. LONG Bias;
  44. LONG StandardBias;
  45. LONG DaylightBias;
  46. SYSTEMTIME StandardDate;
  47. SYSTEMTIME DaylightDate;
  48. } REG_TZI_FORMAT;
  49. #endif // U_PLATFORM_HAS_WINUWP_API
  50. /**
  51. * This is main Windows time zone detection function.
  52. *
  53. * It returns the Windows time zone converted to an ICU time zone as a heap-allocated buffer, or nullptr upon failure.
  54. *
  55. * We use the Win32 API GetDynamicTimeZoneInformation (which is available since Vista) to get the current time zone info,
  56. * as this API returns a non-localized time zone name which can be then mapped to an ICU time zone.
  57. *
  58. * However, in some RDP/terminal services situations, this struct isn't always fully complete, and the TimeZoneKeyName
  59. * field of the struct might be nullptr. This can happen with some 3rd party RDP clients, and also when using older versions
  60. * of the RDP protocol, which don't send the newer TimeZoneKeyNamei information and only send the StandardName and DaylightName.
  61. *
  62. * Since these 3rd party clients and older RDP clients only send the pre-Vista time zone information to the server, this means that we
  63. * need to fallback on using the pre-Vista methods to determine the time zone. This unfortunately requires examining the registry directly
  64. * in order to try and determine the current time zone.
  65. *
  66. * Note that this can however still fail in some cases though if the client and server are using different languages, as the StandardName
  67. * 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
  68. * are localized in registry using the server's language. Despite that, this is the best we can do.
  69. *
  70. * Note: This fallback method won't work for the UWP version though, as we can't use the registry APIs in UWP.
  71. *
  72. * Once we have the current Windows time zone, then we can then map it to an ICU time zone ID (~ Olsen ID).
  73. */
  74. U_CAPI const char* U_EXPORT2
  75. uprv_detectWindowsTimeZone()
  76. {
  77. // We first try to obtain the time zone directly by using the TimeZoneKeyName field of the DYNAMIC_TIME_ZONE_INFORMATION struct.
  78. DYNAMIC_TIME_ZONE_INFORMATION dynamicTZI;
  79. uprv_memset(&dynamicTZI, 0, sizeof(dynamicTZI));
  80. SYSTEMTIME systemTimeAllZero;
  81. uprv_memset(&systemTimeAllZero, 0, sizeof(systemTimeAllZero));
  82. if (GetDynamicTimeZoneInformation(&dynamicTZI) == TIME_ZONE_ID_INVALID) {
  83. return nullptr;
  84. }
  85. // If the DST setting has been turned off in the Control Panel, then return "Etc/GMT<offset>".
  86. //
  87. // Note: This logic is based on how the Control Panel itself determines if DST is 'off' on Windows.
  88. // The code is somewhat convoluted; in a sort of pseudo-code it looks like this:
  89. //
  90. // IF (GetDynamicTimeZoneInformation != TIME_ZONE_ID_INVALID) && (DynamicDaylightTimeDisabled != 0) &&
  91. // (StandardDate == DaylightDate) &&
  92. // (
  93. // (TimeZoneKeyName != Empty && StandardDate == 0) ||
  94. // (TimeZoneKeyName == Empty && StandardDate != 0)
  95. // )
  96. // THEN
  97. // DST setting is "Disabled".
  98. //
  99. if (dynamicTZI.DynamicDaylightTimeDisabled != 0 &&
  100. uprv_memcmp(&dynamicTZI.StandardDate, &dynamicTZI.DaylightDate, sizeof(dynamicTZI.StandardDate)) == 0 &&
  101. ((dynamicTZI.TimeZoneKeyName[0] != L'\0' && uprv_memcmp(&dynamicTZI.StandardDate, &systemTimeAllZero, sizeof(systemTimeAllZero)) == 0) ||
  102. (dynamicTZI.TimeZoneKeyName[0] == L'\0' && uprv_memcmp(&dynamicTZI.StandardDate, &systemTimeAllZero, sizeof(systemTimeAllZero)) != 0)))
  103. {
  104. LONG utcOffsetMins = dynamicTZI.Bias;
  105. if (utcOffsetMins == 0) {
  106. return uprv_strdup("Etc/UTC");
  107. }
  108. // No way to support when DST is turned off and the offset in minutes is not a multiple of 60.
  109. if (utcOffsetMins % 60 == 0) {
  110. char gmtOffsetTz[11] = {}; // "Etc/GMT+dd" is 11-char long with a terminal null.
  111. // Important note on the sign convention for zones:
  112. //
  113. // From https://en.wikipedia.org/wiki/Tz_database#Area
  114. // "In order to conform with the POSIX style, those zone names beginning with "Etc/GMT" have their sign reversed
  115. // from the standard ISO 8601 convention. In the "Etc" area, zones west of GMT have a positive sign and those
  116. // east have a negative sign in their name (e.g "Etc/GMT-14" is 14 hours ahead of GMT)."
  117. //
  118. // Regarding the POSIX style, from https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
  119. // "The offset specifies the time value you must add to the local time to get a Coordinated Universal Time value."
  120. //
  121. // However, the Bias value in DYNAMIC_TIME_ZONE_INFORMATION *already* follows the POSIX convention.
  122. //
  123. // From https://docs.microsoft.com/en-us/windows/win32/api/timezoneapi/ns-timezoneapi-dynamic_time_zone_information
  124. // "The bias is the difference, in minutes, between Coordinated Universal Time (UTC) and
  125. // local time. All translations between UTC and local time are based on the following formula:
  126. // UTC = local time + bias"
  127. //
  128. // For example, a time zone that is 3 hours ahead of UTC (UTC+03:00) would have a Bias value of -180, and the
  129. // corresponding time zone ID would be "Etc/GMT-3". (So there is no need to negate utcOffsetMins below.)
  130. int ret = snprintf(gmtOffsetTz, sizeof(gmtOffsetTz), "Etc/GMT%+ld", utcOffsetMins / 60);
  131. if (ret > 0 && ret < UPRV_LENGTHOF(gmtOffsetTz)) {
  132. return uprv_strdup(gmtOffsetTz);
  133. }
  134. }
  135. }
  136. // If DST is NOT disabled, but the TimeZoneKeyName field of the struct is nullptr, then we may be dealing with a
  137. // RDP/terminal services session where the 'Time Zone Redirection' feature is enabled. However, either the RDP
  138. // client sent the server incomplete info (some 3rd party RDP clients only send the StandardName and DaylightName,
  139. // but do not send the important TimeZoneKeyName), or if the RDP server has not appropriately populated the struct correctly.
  140. //
  141. // In this case we unfortunately have no choice but to fallback to using the pre-Vista method of determining the
  142. // time zone, which requires examining the registry directly.
  143. //
  144. // Note that this can however still fail though if the client and server are using different languages, as the StandardName
  145. // that is sent by client is *localized* in the client's language. However, we must compare this to the names that are
  146. // on the server, which are *localized* in registry using the server's language.
  147. //
  148. // One other note is that this fallback method doesn't work for the UWP version, as we can't use the registry APIs.
  149. // windowsTimeZoneName will point at timezoneSubKeyName if we had to fallback to using the registry, and we found a match.
  150. WCHAR timezoneSubKeyName[WINDOWS_MAX_REG_KEY_LENGTH];
  151. WCHAR *windowsTimeZoneName = dynamicTZI.TimeZoneKeyName;
  152. if (dynamicTZI.TimeZoneKeyName[0] == 0) {
  153. // We can't use the registry APIs in the UWP version.
  154. #if U_PLATFORM_HAS_WINUWP_API == 1
  155. (void)timezoneSubKeyName; // suppress unused variable warnings.
  156. return nullptr;
  157. #else
  158. // Open the path to the time zones in the Windows registry.
  159. LONG ret;
  160. HKEY hKeyAllTimeZones = nullptr;
  161. ret = RegOpenKeyExW(HKEY_LOCAL_MACHINE, WINDOWS_TIMEZONES_REG_KEY_PATH, 0, KEY_READ,
  162. reinterpret_cast<PHKEY>(&hKeyAllTimeZones));
  163. if (ret != ERROR_SUCCESS) {
  164. // If we can't open the key, then we can't do much, so fail.
  165. return nullptr;
  166. }
  167. // Read the number of subkeys under the time zone registry path.
  168. DWORD numTimeZoneSubKeys;
  169. ret = RegQueryInfoKeyW(hKeyAllTimeZones, nullptr, nullptr, nullptr, &numTimeZoneSubKeys,
  170. nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr);
  171. if (ret != ERROR_SUCCESS) {
  172. RegCloseKey(hKeyAllTimeZones);
  173. return nullptr;
  174. }
  175. // Examine each of the subkeys to try and find a match for the localized standard name ("Std").
  176. //
  177. // Note: The name of the time zone subkey itself is not localized, but the "Std" name is localized. This means
  178. // that we could fail to find a match if the RDP client and RDP server are using different languages, but unfortunately
  179. // there isn't much we can do about it.
  180. HKEY hKeyTimeZoneSubKey = nullptr;
  181. ULONG registryValueType;
  182. WCHAR registryStandardName[WINDOWS_MAX_REG_KEY_LENGTH];
  183. for (DWORD i = 0; i < numTimeZoneSubKeys; i++) {
  184. // Note: RegEnumKeyExW wants the size of the buffer in characters.
  185. DWORD size = UPRV_LENGTHOF(timezoneSubKeyName);
  186. ret = RegEnumKeyExW(hKeyAllTimeZones, i, timezoneSubKeyName, &size, nullptr, nullptr, nullptr, nullptr);
  187. if (ret != ERROR_SUCCESS) {
  188. RegCloseKey(hKeyAllTimeZones);
  189. return nullptr;
  190. }
  191. ret = RegOpenKeyExW(hKeyAllTimeZones, timezoneSubKeyName, 0, KEY_READ,
  192. reinterpret_cast<PHKEY>(&hKeyTimeZoneSubKey));
  193. if (ret != ERROR_SUCCESS) {
  194. RegCloseKey(hKeyAllTimeZones);
  195. return nullptr;
  196. }
  197. // Note: RegQueryValueExW wants the size of the buffer in bytes.
  198. size = sizeof(registryStandardName);
  199. ret = RegQueryValueExW(hKeyTimeZoneSubKey, L"Std", nullptr, &registryValueType,
  200. reinterpret_cast<LPBYTE>(registryStandardName), &size);
  201. if (ret != ERROR_SUCCESS || registryValueType != REG_SZ) {
  202. RegCloseKey(hKeyTimeZoneSubKey);
  203. RegCloseKey(hKeyAllTimeZones);
  204. return nullptr;
  205. }
  206. // Note: wcscmp does an ordinal (byte) comparison.
  207. if (wcscmp(reinterpret_cast<WCHAR *>(registryStandardName), dynamicTZI.StandardName) == 0) {
  208. // Since we are comparing the *localized* time zone name, it's possible that some languages might use
  209. // the same string for more than one time zone. Thus we need to examine the TZI data in the registry to
  210. // compare the GMT offset (the bias), and the DST transition dates, to ensure it's the same time zone
  211. // as the currently reported one.
  212. REG_TZI_FORMAT registryTziValue;
  213. uprv_memset(&registryTziValue, 0, sizeof(registryTziValue));
  214. // Note: RegQueryValueExW wants the size of the buffer in bytes.
  215. DWORD timezoneTziValueSize = sizeof(registryTziValue);
  216. ret = RegQueryValueExW(hKeyTimeZoneSubKey, L"TZI", nullptr, &registryValueType,
  217. reinterpret_cast<LPBYTE>(&registryTziValue), &timezoneTziValueSize);
  218. if (ret == ERROR_SUCCESS) {
  219. if ((dynamicTZI.Bias == registryTziValue.Bias) &&
  220. (memcmp((const void *)&dynamicTZI.StandardDate, (const void *)&registryTziValue.StandardDate, sizeof(SYSTEMTIME)) == 0) &&
  221. (memcmp((const void *)&dynamicTZI.DaylightDate, (const void *)&registryTziValue.DaylightDate, sizeof(SYSTEMTIME)) == 0))
  222. {
  223. // We found a matching time zone.
  224. windowsTimeZoneName = timezoneSubKeyName;
  225. break;
  226. }
  227. }
  228. }
  229. RegCloseKey(hKeyTimeZoneSubKey);
  230. hKeyTimeZoneSubKey = nullptr;
  231. }
  232. if (hKeyTimeZoneSubKey != nullptr) {
  233. RegCloseKey(hKeyTimeZoneSubKey);
  234. }
  235. if (hKeyAllTimeZones != nullptr) {
  236. RegCloseKey(hKeyAllTimeZones);
  237. }
  238. #endif // U_PLATFORM_HAS_WINUWP_API
  239. }
  240. CharString winTZ;
  241. UErrorCode status = U_ZERO_ERROR;
  242. winTZ.appendInvariantChars(UnicodeString(true, windowsTimeZoneName, -1), status);
  243. // Map Windows Timezone name (non-localized) to ICU timezone ID (~ Olson timezone id).
  244. StackUResourceBundle winTZBundle;
  245. ures_openDirectFillIn(winTZBundle.getAlias(), nullptr, "windowsZones", &status);
  246. ures_getByKey(winTZBundle.getAlias(), "mapTimezones", winTZBundle.getAlias(), &status);
  247. ures_getByKey(winTZBundle.getAlias(), winTZ.data(), winTZBundle.getAlias(), &status);
  248. if (U_FAILURE(status)) {
  249. return nullptr;
  250. }
  251. // Note: Since the ISO 3166 country/region codes are all invariant ASCII chars, we can
  252. // directly downcast from wchar_t to do the conversion.
  253. // We could call the A version of the GetGeoInfo API, but that would be slightly slower than calling the W API,
  254. // as the A version of the API will end up calling MultiByteToWideChar anyways internally.
  255. wchar_t regionCodeW[3] = {};
  256. char regionCode[3] = {}; // 2 letter ISO 3166 country/region code made entirely of invariant chars.
  257. int geoId = GetUserGeoID(GEOCLASS_NATION);
  258. int regionCodeLen = GetGeoInfoW(geoId, GEO_ISO2, regionCodeW, UPRV_LENGTHOF(regionCodeW), 0);
  259. const char16_t *icuTZ16 = nullptr;
  260. int32_t tzListLen = 0;
  261. if (regionCodeLen != 0) {
  262. for (int i = 0; i < UPRV_LENGTHOF(regionCodeW); i++) {
  263. regionCode[i] = static_cast<char>(regionCodeW[i]);
  264. }
  265. icuTZ16 = ures_getStringByKey(winTZBundle.getAlias(), regionCode, &tzListLen, &status);
  266. }
  267. if (regionCodeLen == 0 || U_FAILURE(status)) {
  268. // fallback to default "001" (world)
  269. status = U_ZERO_ERROR;
  270. icuTZ16 = ures_getStringByKey(winTZBundle.getAlias(), "001", &tzListLen, &status);
  271. }
  272. // Note: We want the first entry in the string returned by ures_getStringByKey.
  273. // However this string can be a space delimited list of timezones:
  274. // Ex: "America/New_York America/Detroit America/Indiana/Petersburg ..."
  275. // We need to stop at the first space, so we pass tzLen (instead of tzListLen) to appendInvariantChars below.
  276. int32_t tzLen = 0;
  277. if (tzListLen > 0) {
  278. while (!(icuTZ16[tzLen] == u'\0' || icuTZ16[tzLen] == u' ')) {
  279. tzLen++;
  280. }
  281. }
  282. // Note: cloneData returns nullptr if the status is a failure, so this
  283. // will return nullptr if the above look-up fails.
  284. CharString icuTZStr;
  285. return icuTZStr.appendInvariantChars(icuTZ16, tzLen, status).cloneData(status);
  286. }
  287. U_NAMESPACE_END
  288. #endif /* U_PLATFORM_USES_ONLY_WIN32_API */