TimeZoneInfo.Win32.cs 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994
  1. // Licensed to the .NET Foundation under one or more agreements.
  2. // The .NET Foundation licenses this file to you under the MIT license.
  3. // See the LICENSE file in the project root for more information.
  4. using System.Collections.Generic;
  5. using System.Diagnostics;
  6. using System.Globalization;
  7. using System.IO;
  8. using System.Security;
  9. using System.Text;
  10. using System.Threading;
  11. using Microsoft.Win32.SafeHandles;
  12. using Internal.Win32;
  13. using Internal.Runtime.CompilerServices;
  14. using REG_TZI_FORMAT = Interop.Kernel32.REG_TZI_FORMAT;
  15. using TIME_ZONE_INFORMATION = Interop.Kernel32.TIME_ZONE_INFORMATION;
  16. using TIME_DYNAMIC_ZONE_INFORMATION = Interop.Kernel32.TIME_DYNAMIC_ZONE_INFORMATION;
  17. namespace System
  18. {
  19. public sealed partial class TimeZoneInfo
  20. {
  21. // registry constants for the 'Time Zones' hive
  22. //
  23. private const string TimeZonesRegistryHive = @"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones";
  24. private const string DisplayValue = "Display";
  25. private const string DaylightValue = "Dlt";
  26. private const string StandardValue = "Std";
  27. private const string MuiDisplayValue = "MUI_Display";
  28. private const string MuiDaylightValue = "MUI_Dlt";
  29. private const string MuiStandardValue = "MUI_Std";
  30. private const string TimeZoneInfoValue = "TZI";
  31. private const string FirstEntryValue = "FirstEntry";
  32. private const string LastEntryValue = "LastEntry";
  33. private const int MaxKeyLength = 255;
  34. #pragma warning disable 0420
  35. private sealed partial class CachedData
  36. {
  37. private static TimeZoneInfo GetCurrentOneYearLocal()
  38. {
  39. // load the data from the OS
  40. TIME_ZONE_INFORMATION timeZoneInformation;
  41. uint result = Interop.Kernel32.GetTimeZoneInformation(out timeZoneInformation);
  42. return result == Interop.Kernel32.TIME_ZONE_ID_INVALID ?
  43. CreateCustomTimeZone(LocalId, TimeSpan.Zero, LocalId, LocalId) :
  44. GetLocalTimeZoneFromWin32Data(timeZoneInformation, dstDisabled: false);
  45. }
  46. private volatile OffsetAndRule _oneYearLocalFromUtc;
  47. public OffsetAndRule GetOneYearLocalFromUtc(int year)
  48. {
  49. OffsetAndRule oneYearLocFromUtc = _oneYearLocalFromUtc;
  50. if (oneYearLocFromUtc == null || oneYearLocFromUtc.Year != year)
  51. {
  52. TimeZoneInfo currentYear = GetCurrentOneYearLocal();
  53. AdjustmentRule rule = currentYear._adjustmentRules == null ? null : currentYear._adjustmentRules[0];
  54. oneYearLocFromUtc = new OffsetAndRule(year, currentYear.BaseUtcOffset, rule);
  55. _oneYearLocalFromUtc = oneYearLocFromUtc;
  56. }
  57. return oneYearLocFromUtc;
  58. }
  59. }
  60. #pragma warning restore 0420
  61. private sealed class OffsetAndRule
  62. {
  63. public readonly int Year;
  64. public readonly TimeSpan Offset;
  65. public readonly AdjustmentRule Rule;
  66. public OffsetAndRule(int year, TimeSpan offset, AdjustmentRule rule)
  67. {
  68. Year = year;
  69. Offset = offset;
  70. Rule = rule;
  71. }
  72. }
  73. /// <summary>
  74. /// Returns a cloned array of AdjustmentRule objects
  75. /// </summary>
  76. public AdjustmentRule[] GetAdjustmentRules()
  77. {
  78. if (_adjustmentRules == null)
  79. {
  80. return Array.Empty<AdjustmentRule>();
  81. }
  82. return (AdjustmentRule[])_adjustmentRules.Clone();
  83. }
  84. private static void PopulateAllSystemTimeZones(CachedData cachedData)
  85. {
  86. Debug.Assert(Monitor.IsEntered(cachedData));
  87. using (RegistryKey reg = Registry.LocalMachine.OpenSubKey(TimeZonesRegistryHive, writable: false))
  88. {
  89. if (reg != null)
  90. {
  91. foreach (string keyName in reg.GetSubKeyNames())
  92. {
  93. TimeZoneInfo value;
  94. Exception ex;
  95. TryGetTimeZone(keyName, false, out value, out ex, cachedData); // populate the cache
  96. }
  97. }
  98. }
  99. }
  100. private TimeZoneInfo(in TIME_ZONE_INFORMATION zone, bool dstDisabled)
  101. {
  102. string standardName = zone.GetStandardName();
  103. if (standardName.Length == 0)
  104. {
  105. _id = LocalId; // the ID must contain at least 1 character - initialize _id to "Local"
  106. }
  107. else
  108. {
  109. _id = standardName;
  110. }
  111. _baseUtcOffset = new TimeSpan(0, -(zone.Bias), 0);
  112. if (!dstDisabled)
  113. {
  114. // only create the adjustment rule if DST is enabled
  115. REG_TZI_FORMAT regZone = new REG_TZI_FORMAT(zone);
  116. AdjustmentRule rule = CreateAdjustmentRuleFromTimeZoneInformation(regZone, DateTime.MinValue.Date, DateTime.MaxValue.Date, zone.Bias);
  117. if (rule != null)
  118. {
  119. _adjustmentRules = new[] { rule };
  120. }
  121. }
  122. ValidateTimeZoneInfo(_id, _baseUtcOffset, _adjustmentRules, out _supportsDaylightSavingTime);
  123. _displayName = standardName;
  124. _standardDisplayName = standardName;
  125. _daylightDisplayName = zone.GetDaylightName();
  126. }
  127. /// <summary>
  128. /// Helper function to check if the current TimeZoneInformation struct does not support DST.
  129. /// This check returns true when the DaylightDate == StandardDate.
  130. /// This check is only meant to be used for "Local".
  131. /// </summary>
  132. private static bool CheckDaylightSavingTimeNotSupported(in TIME_ZONE_INFORMATION timeZone) =>
  133. timeZone.DaylightDate.Equals(timeZone.StandardDate);
  134. /// <summary>
  135. /// Converts a REG_TZI_FORMAT struct to an AdjustmentRule.
  136. /// </summary>
  137. private static AdjustmentRule CreateAdjustmentRuleFromTimeZoneInformation(in REG_TZI_FORMAT timeZoneInformation, DateTime startDate, DateTime endDate, int defaultBaseUtcOffset)
  138. {
  139. bool supportsDst = timeZoneInformation.StandardDate.Month != 0;
  140. if (!supportsDst)
  141. {
  142. if (timeZoneInformation.Bias == defaultBaseUtcOffset)
  143. {
  144. // this rule will not contain any information to be used to adjust dates. just ignore it
  145. return null;
  146. }
  147. return AdjustmentRule.CreateAdjustmentRule(
  148. startDate,
  149. endDate,
  150. TimeSpan.Zero, // no daylight saving transition
  151. TransitionTime.CreateFixedDateRule(DateTime.MinValue, 1, 1),
  152. TransitionTime.CreateFixedDateRule(DateTime.MinValue.AddMilliseconds(1), 1, 1),
  153. new TimeSpan(0, defaultBaseUtcOffset - timeZoneInformation.Bias, 0), // Bias delta is all what we need from this rule
  154. noDaylightTransitions: false);
  155. }
  156. //
  157. // Create an AdjustmentRule with TransitionTime objects
  158. //
  159. TransitionTime daylightTransitionStart;
  160. if (!TransitionTimeFromTimeZoneInformation(timeZoneInformation, out daylightTransitionStart, readStartDate: true))
  161. {
  162. return null;
  163. }
  164. TransitionTime daylightTransitionEnd;
  165. if (!TransitionTimeFromTimeZoneInformation(timeZoneInformation, out daylightTransitionEnd, readStartDate: false))
  166. {
  167. return null;
  168. }
  169. if (daylightTransitionStart.Equals(daylightTransitionEnd))
  170. {
  171. // this happens when the time zone does support DST but the OS has DST disabled
  172. return null;
  173. }
  174. return AdjustmentRule.CreateAdjustmentRule(
  175. startDate,
  176. endDate,
  177. new TimeSpan(0, -timeZoneInformation.DaylightBias, 0),
  178. daylightTransitionStart,
  179. daylightTransitionEnd,
  180. new TimeSpan(0, defaultBaseUtcOffset - timeZoneInformation.Bias, 0),
  181. noDaylightTransitions: false);
  182. }
  183. /// <summary>
  184. /// Helper function that searches the registry for a time zone entry
  185. /// that matches the TimeZoneInformation struct.
  186. /// </summary>
  187. private static string FindIdFromTimeZoneInformation(in TIME_ZONE_INFORMATION timeZone, out bool dstDisabled)
  188. {
  189. dstDisabled = false;
  190. using (RegistryKey key = Registry.LocalMachine.OpenSubKey(TimeZonesRegistryHive, writable: false))
  191. {
  192. if (key == null)
  193. {
  194. return null;
  195. }
  196. foreach (string keyName in key.GetSubKeyNames())
  197. {
  198. if (TryCompareTimeZoneInformationToRegistry(timeZone, keyName, out dstDisabled))
  199. {
  200. return keyName;
  201. }
  202. }
  203. }
  204. return null;
  205. }
  206. /// <summary>
  207. /// Helper function for retrieving the local system time zone.
  208. /// May throw COMException, TimeZoneNotFoundException, InvalidTimeZoneException.
  209. /// Assumes cachedData lock is taken.
  210. /// </summary>
  211. /// <returns>A new TimeZoneInfo instance.</returns>
  212. private static TimeZoneInfo GetLocalTimeZone(CachedData cachedData)
  213. {
  214. Debug.Assert(Monitor.IsEntered(cachedData));
  215. //
  216. // Try using the "kernel32!GetDynamicTimeZoneInformation" API to get the "id"
  217. //
  218. var dynamicTimeZoneInformation = new TIME_DYNAMIC_ZONE_INFORMATION();
  219. // call kernel32!GetDynamicTimeZoneInformation...
  220. uint result = Interop.Kernel32.GetDynamicTimeZoneInformation(out dynamicTimeZoneInformation);
  221. if (result == Interop.Kernel32.TIME_ZONE_ID_INVALID)
  222. {
  223. // return a dummy entry
  224. return CreateCustomTimeZone(LocalId, TimeSpan.Zero, LocalId, LocalId);
  225. }
  226. // check to see if we can use the key name returned from the API call
  227. string dynamicTimeZoneKeyName = dynamicTimeZoneInformation.GetTimeZoneKeyName();
  228. if (dynamicTimeZoneKeyName.Length != 0)
  229. {
  230. TimeZoneInfo zone;
  231. Exception ex;
  232. if (TryGetTimeZone(dynamicTimeZoneKeyName, dynamicTimeZoneInformation.DynamicDaylightTimeDisabled != 0, out zone, out ex, cachedData) == TimeZoneInfoResult.Success)
  233. {
  234. // successfully loaded the time zone from the registry
  235. return zone;
  236. }
  237. }
  238. var timeZoneInformation = new TIME_ZONE_INFORMATION(dynamicTimeZoneInformation);
  239. // the key name was not returned or it pointed to a bogus entry - search for the entry ourselves
  240. string id = FindIdFromTimeZoneInformation(timeZoneInformation, out bool dstDisabled);
  241. if (id != null)
  242. {
  243. TimeZoneInfo zone;
  244. Exception ex;
  245. if (TryGetTimeZone(id, dstDisabled, out zone, out ex, cachedData) == TimeZoneInfoResult.Success)
  246. {
  247. // successfully loaded the time zone from the registry
  248. return zone;
  249. }
  250. }
  251. // We could not find the data in the registry. Fall back to using
  252. // the data from the Win32 API
  253. return GetLocalTimeZoneFromWin32Data(timeZoneInformation, dstDisabled);
  254. }
  255. /// <summary>
  256. /// Helper function used by 'GetLocalTimeZone()' - this function wraps a bunch of
  257. /// try/catch logic for handling the TimeZoneInfo private constructor that takes
  258. /// a TIME_ZONE_INFORMATION structure.
  259. /// </summary>
  260. private static TimeZoneInfo GetLocalTimeZoneFromWin32Data(in TIME_ZONE_INFORMATION timeZoneInformation, bool dstDisabled)
  261. {
  262. // first try to create the TimeZoneInfo with the original 'dstDisabled' flag
  263. try
  264. {
  265. return new TimeZoneInfo(timeZoneInformation, dstDisabled);
  266. }
  267. catch (ArgumentException) { }
  268. catch (InvalidTimeZoneException) { }
  269. // if 'dstDisabled' was false then try passing in 'true' as a last ditch effort
  270. if (!dstDisabled)
  271. {
  272. try
  273. {
  274. return new TimeZoneInfo(timeZoneInformation, dstDisabled: true);
  275. }
  276. catch (ArgumentException) { }
  277. catch (InvalidTimeZoneException) { }
  278. }
  279. // the data returned from Windows is completely bogus; return a dummy entry
  280. return CreateCustomTimeZone(LocalId, TimeSpan.Zero, LocalId, LocalId);
  281. }
  282. /// <summary>
  283. /// Helper function for retrieving a TimeZoneInfo object by time_zone_name.
  284. /// This function wraps the logic necessary to keep the private
  285. /// SystemTimeZones cache in working order
  286. ///
  287. /// This function will either return a valid TimeZoneInfo instance or
  288. /// it will throw 'InvalidTimeZoneException' / 'TimeZoneNotFoundException'.
  289. /// </summary>
  290. public static TimeZoneInfo FindSystemTimeZoneById(string id)
  291. {
  292. // Special case for Utc as it will not exist in the dictionary with the rest
  293. // of the system time zones. There is no need to do this check for Local.Id
  294. // since Local is a real time zone that exists in the dictionary cache
  295. if (string.Equals(id, UtcId, StringComparison.OrdinalIgnoreCase))
  296. {
  297. return Utc;
  298. }
  299. if (id == null)
  300. {
  301. throw new ArgumentNullException(nameof(id));
  302. }
  303. if (id.Length == 0 || id.Length > MaxKeyLength || id.Contains('\0'))
  304. {
  305. throw new TimeZoneNotFoundException(SR.Format(SR.TimeZoneNotFound_MissingData, id));
  306. }
  307. TimeZoneInfo value;
  308. Exception e;
  309. TimeZoneInfoResult result;
  310. CachedData cachedData = s_cachedData;
  311. lock (cachedData)
  312. {
  313. result = TryGetTimeZone(id, false, out value, out e, cachedData);
  314. }
  315. if (result == TimeZoneInfoResult.Success)
  316. {
  317. return value;
  318. }
  319. else if (result == TimeZoneInfoResult.InvalidTimeZoneException)
  320. {
  321. throw new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidRegistryData, id), e);
  322. }
  323. else if (result == TimeZoneInfoResult.SecurityException)
  324. {
  325. throw new SecurityException(SR.Format(SR.Security_CannotReadRegistryData, id), e);
  326. }
  327. else
  328. {
  329. throw new TimeZoneNotFoundException(SR.Format(SR.TimeZoneNotFound_MissingData, id), e);
  330. }
  331. }
  332. // DateTime.Now fast path that avoids allocating an historically accurate TimeZoneInfo.Local and just creates a 1-year (current year) accurate time zone
  333. internal static TimeSpan GetDateTimeNowUtcOffsetFromUtc(DateTime time, out bool isAmbiguousLocalDst)
  334. {
  335. bool isDaylightSavings = false;
  336. isAmbiguousLocalDst = false;
  337. TimeSpan baseOffset;
  338. int timeYear = time.Year;
  339. OffsetAndRule match = s_cachedData.GetOneYearLocalFromUtc(timeYear);
  340. baseOffset = match.Offset;
  341. if (match.Rule != null)
  342. {
  343. baseOffset = baseOffset + match.Rule.BaseUtcOffsetDelta;
  344. if (match.Rule.HasDaylightSaving)
  345. {
  346. isDaylightSavings = GetIsDaylightSavingsFromUtc(time, timeYear, match.Offset, match.Rule, null, out isAmbiguousLocalDst, Local);
  347. baseOffset += (isDaylightSavings ? match.Rule.DaylightDelta : TimeSpan.Zero /* FUTURE: rule.StandardDelta */);
  348. }
  349. }
  350. return baseOffset;
  351. }
  352. /// <summary>
  353. /// Converts a REG_TZI_FORMAT struct to a TransitionTime
  354. /// - When the argument 'readStart' is true the corresponding daylightTransitionTimeStart field is read
  355. /// - When the argument 'readStart' is false the corresponding dayightTransitionTimeEnd field is read
  356. /// </summary>
  357. private static bool TransitionTimeFromTimeZoneInformation(in REG_TZI_FORMAT timeZoneInformation, out TransitionTime transitionTime, bool readStartDate)
  358. {
  359. //
  360. // SYSTEMTIME -
  361. //
  362. // If the time zone does not support daylight saving time or if the caller needs
  363. // to disable daylight saving time, the wMonth member in the SYSTEMTIME structure
  364. // must be zero. If this date is specified, the DaylightDate value in the
  365. // TIME_ZONE_INFORMATION structure must also be specified. Otherwise, the system
  366. // assumes the time zone data is invalid and no changes will be applied.
  367. //
  368. bool supportsDst = (timeZoneInformation.StandardDate.Month != 0);
  369. if (!supportsDst)
  370. {
  371. transitionTime = default;
  372. return false;
  373. }
  374. //
  375. // SYSTEMTIME -
  376. //
  377. // * FixedDateRule -
  378. // If the Year member is not zero, the transition date is absolute; it will only occur one time
  379. //
  380. // * FloatingDateRule -
  381. // To select the correct day in the month, set the Year member to zero, the Hour and Minute
  382. // members to the transition time, the DayOfWeek member to the appropriate weekday, and the
  383. // Day member to indicate the occurence of the day of the week within the month (first through fifth).
  384. //
  385. // Using this notation, specify the 2:00a.m. on the first Sunday in April as follows:
  386. // Hour = 2,
  387. // Month = 4,
  388. // DayOfWeek = 0,
  389. // Day = 1.
  390. //
  391. // Specify 2:00a.m. on the last Thursday in October as follows:
  392. // Hour = 2,
  393. // Month = 10,
  394. // DayOfWeek = 4,
  395. // Day = 5.
  396. //
  397. if (readStartDate)
  398. {
  399. //
  400. // read the "daylightTransitionStart"
  401. //
  402. if (timeZoneInformation.DaylightDate.Year == 0)
  403. {
  404. transitionTime = TransitionTime.CreateFloatingDateRule(
  405. new DateTime(1, /* year */
  406. 1, /* month */
  407. 1, /* day */
  408. timeZoneInformation.DaylightDate.Hour,
  409. timeZoneInformation.DaylightDate.Minute,
  410. timeZoneInformation.DaylightDate.Second,
  411. timeZoneInformation.DaylightDate.Milliseconds),
  412. timeZoneInformation.DaylightDate.Month,
  413. timeZoneInformation.DaylightDate.Day, /* Week 1-5 */
  414. (DayOfWeek)timeZoneInformation.DaylightDate.DayOfWeek);
  415. }
  416. else
  417. {
  418. transitionTime = TransitionTime.CreateFixedDateRule(
  419. new DateTime(1, /* year */
  420. 1, /* month */
  421. 1, /* day */
  422. timeZoneInformation.DaylightDate.Hour,
  423. timeZoneInformation.DaylightDate.Minute,
  424. timeZoneInformation.DaylightDate.Second,
  425. timeZoneInformation.DaylightDate.Milliseconds),
  426. timeZoneInformation.DaylightDate.Month,
  427. timeZoneInformation.DaylightDate.Day);
  428. }
  429. }
  430. else
  431. {
  432. //
  433. // read the "daylightTransitionEnd"
  434. //
  435. if (timeZoneInformation.StandardDate.Year == 0)
  436. {
  437. transitionTime = TransitionTime.CreateFloatingDateRule(
  438. new DateTime(1, /* year */
  439. 1, /* month */
  440. 1, /* day */
  441. timeZoneInformation.StandardDate.Hour,
  442. timeZoneInformation.StandardDate.Minute,
  443. timeZoneInformation.StandardDate.Second,
  444. timeZoneInformation.StandardDate.Milliseconds),
  445. timeZoneInformation.StandardDate.Month,
  446. timeZoneInformation.StandardDate.Day, /* Week 1-5 */
  447. (DayOfWeek)timeZoneInformation.StandardDate.DayOfWeek);
  448. }
  449. else
  450. {
  451. transitionTime = TransitionTime.CreateFixedDateRule(
  452. new DateTime(1, /* year */
  453. 1, /* month */
  454. 1, /* day */
  455. timeZoneInformation.StandardDate.Hour,
  456. timeZoneInformation.StandardDate.Minute,
  457. timeZoneInformation.StandardDate.Second,
  458. timeZoneInformation.StandardDate.Milliseconds),
  459. timeZoneInformation.StandardDate.Month,
  460. timeZoneInformation.StandardDate.Day);
  461. }
  462. }
  463. return true;
  464. }
  465. /// <summary>
  466. /// Helper function that takes:
  467. /// 1. A string representing a time_zone_name registry key name.
  468. /// 2. A REG_TZI_FORMAT struct containing the default rule.
  469. /// 3. An AdjustmentRule[] out-parameter.
  470. /// </summary>
  471. private static bool TryCreateAdjustmentRules(string id, in REG_TZI_FORMAT defaultTimeZoneInformation, out AdjustmentRule[] rules, out Exception e, int defaultBaseUtcOffset)
  472. {
  473. rules = null;
  474. e = null;
  475. try
  476. {
  477. // Optional, Dynamic Time Zone Registry Data
  478. // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
  479. //
  480. // HKLM
  481. // Software
  482. // Microsoft
  483. // Windows NT
  484. // CurrentVersion
  485. // Time Zones
  486. // <time_zone_name>
  487. // Dynamic DST
  488. // * "FirstEntry" REG_DWORD "1980"
  489. // First year in the table. If the current year is less than this value,
  490. // this entry will be used for DST boundaries
  491. // * "LastEntry" REG_DWORD "2038"
  492. // Last year in the table. If the current year is greater than this value,
  493. // this entry will be used for DST boundaries"
  494. // * "<year1>" REG_BINARY REG_TZI_FORMAT
  495. // * "<year2>" REG_BINARY REG_TZI_FORMAT
  496. // * "<year3>" REG_BINARY REG_TZI_FORMAT
  497. //
  498. using (RegistryKey dynamicKey = Registry.LocalMachine.OpenSubKey(TimeZonesRegistryHive + "\\" + id + "\\Dynamic DST", writable: false))
  499. {
  500. if (dynamicKey == null)
  501. {
  502. AdjustmentRule rule = CreateAdjustmentRuleFromTimeZoneInformation(
  503. defaultTimeZoneInformation, DateTime.MinValue.Date, DateTime.MaxValue.Date, defaultBaseUtcOffset);
  504. if (rule != null)
  505. {
  506. rules = new[] { rule };
  507. }
  508. return true;
  509. }
  510. //
  511. // loop over all of the "<time_zone_name>\Dynamic DST" hive entries
  512. //
  513. // read FirstEntry {MinValue - (year1, 12, 31)}
  514. // read MiddleEntry {(yearN, 1, 1) - (yearN, 12, 31)}
  515. // read LastEntry {(yearN, 1, 1) - MaxValue }
  516. // read the FirstEntry and LastEntry key values (ex: "1980", "2038")
  517. int first = (int)dynamicKey.GetValue(FirstEntryValue, -1);
  518. int last = (int)dynamicKey.GetValue(LastEntryValue, -1);
  519. if (first == -1 || last == -1 || first > last)
  520. {
  521. return false;
  522. }
  523. // read the first year entry
  524. REG_TZI_FORMAT dtzi;
  525. if (!TryGetTimeZoneEntryFromRegistry(dynamicKey, first.ToString(CultureInfo.InvariantCulture), out dtzi))
  526. {
  527. return false;
  528. }
  529. if (first == last)
  530. {
  531. // there is just 1 dynamic rule for this time zone.
  532. AdjustmentRule rule = CreateAdjustmentRuleFromTimeZoneInformation(dtzi, DateTime.MinValue.Date, DateTime.MaxValue.Date, defaultBaseUtcOffset);
  533. if (rule != null)
  534. {
  535. rules = new[] { rule };
  536. }
  537. return true;
  538. }
  539. List<AdjustmentRule> rulesList = new List<AdjustmentRule>(1);
  540. // there are more than 1 dynamic rules for this time zone.
  541. AdjustmentRule firstRule = CreateAdjustmentRuleFromTimeZoneInformation(
  542. dtzi,
  543. DateTime.MinValue.Date, // MinValue
  544. new DateTime(first, 12, 31), // December 31, <FirstYear>
  545. defaultBaseUtcOffset);
  546. if (firstRule != null)
  547. {
  548. rulesList.Add(firstRule);
  549. }
  550. // read the middle year entries
  551. for (int i = first + 1; i < last; i++)
  552. {
  553. if (!TryGetTimeZoneEntryFromRegistry(dynamicKey, i.ToString(CultureInfo.InvariantCulture), out dtzi))
  554. {
  555. return false;
  556. }
  557. AdjustmentRule middleRule = CreateAdjustmentRuleFromTimeZoneInformation(
  558. dtzi,
  559. new DateTime(i, 1, 1), // January 01, <Year>
  560. new DateTime(i, 12, 31), // December 31, <Year>
  561. defaultBaseUtcOffset);
  562. if (middleRule != null)
  563. {
  564. rulesList.Add(middleRule);
  565. }
  566. }
  567. // read the last year entry
  568. if (!TryGetTimeZoneEntryFromRegistry(dynamicKey, last.ToString(CultureInfo.InvariantCulture), out dtzi))
  569. {
  570. return false;
  571. }
  572. AdjustmentRule lastRule = CreateAdjustmentRuleFromTimeZoneInformation(
  573. dtzi,
  574. new DateTime(last, 1, 1), // January 01, <LastYear>
  575. DateTime.MaxValue.Date, // MaxValue
  576. defaultBaseUtcOffset);
  577. if (lastRule != null)
  578. {
  579. rulesList.Add(lastRule);
  580. }
  581. // convert the List to an AdjustmentRule array
  582. if (rulesList.Count != 0)
  583. {
  584. rules = rulesList.ToArray();
  585. }
  586. } // end of: using (RegistryKey dynamicKey...
  587. }
  588. catch (InvalidCastException ex)
  589. {
  590. // one of the RegistryKey.GetValue calls could not be cast to an expected value type
  591. e = ex;
  592. return false;
  593. }
  594. catch (ArgumentOutOfRangeException ex)
  595. {
  596. e = ex;
  597. return false;
  598. }
  599. catch (ArgumentException ex)
  600. {
  601. e = ex;
  602. return false;
  603. }
  604. return true;
  605. }
  606. private static unsafe bool TryGetTimeZoneEntryFromRegistry(RegistryKey key, string name, out REG_TZI_FORMAT dtzi)
  607. {
  608. if (!(key.GetValue(name, null) is byte[] regValue) || regValue.Length != sizeof(REG_TZI_FORMAT))
  609. {
  610. dtzi = default;
  611. return false;
  612. }
  613. fixed (byte * pBytes = &regValue[0])
  614. dtzi = *(REG_TZI_FORMAT *)pBytes;
  615. return true;
  616. }
  617. /// <summary>
  618. /// Helper function that compares the StandardBias and StandardDate portion a
  619. /// TimeZoneInformation struct to a time zone registry entry.
  620. /// </summary>
  621. private static bool TryCompareStandardDate(in TIME_ZONE_INFORMATION timeZone, in REG_TZI_FORMAT registryTimeZoneInfo) =>
  622. timeZone.Bias == registryTimeZoneInfo.Bias &&
  623. timeZone.StandardBias == registryTimeZoneInfo.StandardBias &&
  624. timeZone.StandardDate.Equals(registryTimeZoneInfo.StandardDate);
  625. /// <summary>
  626. /// Helper function that compares a TimeZoneInformation struct to a time zone registry entry.
  627. /// </summary>
  628. private static bool TryCompareTimeZoneInformationToRegistry(in TIME_ZONE_INFORMATION timeZone, string id, out bool dstDisabled)
  629. {
  630. dstDisabled = false;
  631. using (RegistryKey key = Registry.LocalMachine.OpenSubKey(TimeZonesRegistryHive + "\\" + id, writable: false))
  632. {
  633. if (key == null)
  634. {
  635. return false;
  636. }
  637. REG_TZI_FORMAT registryTimeZoneInfo;
  638. if (!TryGetTimeZoneEntryFromRegistry(key, TimeZoneInfoValue, out registryTimeZoneInfo))
  639. {
  640. return false;
  641. }
  642. //
  643. // first compare the bias and standard date information between the data from the Win32 API
  644. // and the data from the registry...
  645. //
  646. bool result = TryCompareStandardDate(timeZone, registryTimeZoneInfo);
  647. if (!result)
  648. {
  649. return false;
  650. }
  651. result = dstDisabled || CheckDaylightSavingTimeNotSupported(timeZone) ||
  652. //
  653. // since Daylight Saving Time is not "disabled", do a straight comparision between
  654. // the Win32 API data and the registry data ...
  655. //
  656. (timeZone.DaylightBias == registryTimeZoneInfo.DaylightBias &&
  657. timeZone.DaylightDate.Equals(registryTimeZoneInfo.DaylightDate));
  658. // Finally compare the "StandardName" string value...
  659. //
  660. // we do not compare "DaylightName" as this TimeZoneInformation field may contain
  661. // either "StandardName" or "DaylightName" depending on the time of year and current machine settings
  662. //
  663. if (result)
  664. {
  665. string registryStandardName = key.GetValue(StandardValue, string.Empty) as string;
  666. result = string.Equals(registryStandardName, timeZone.GetStandardName(), StringComparison.Ordinal);
  667. }
  668. return result;
  669. }
  670. }
  671. /// <summary>
  672. /// Helper function for retrieving a localized string resource via MUI.
  673. /// The function expects a string in the form: "@resource.dll, -123"
  674. ///
  675. /// "resource.dll" is a language-neutral portable executable (LNPE) file in
  676. /// the %windir%\system32 directory. The OS is queried to find the best-fit
  677. /// localized resource file for this LNPE (ex: %windir%\system32\en-us\resource.dll.mui).
  678. /// If a localized resource file exists, we LoadString resource ID "123" and
  679. /// return it to our caller.
  680. /// </summary>
  681. private static string TryGetLocalizedNameByMuiNativeResource(string resource)
  682. {
  683. if (string.IsNullOrEmpty(resource))
  684. {
  685. return string.Empty;
  686. }
  687. // parse "@tzres.dll, -100"
  688. //
  689. // filePath = "C:\Windows\System32\tzres.dll"
  690. // resourceId = -100
  691. //
  692. string[] resources = resource.Split(',');
  693. if (resources.Length != 2)
  694. {
  695. return string.Empty;
  696. }
  697. string filePath;
  698. int resourceId;
  699. // get the path to Windows\System32
  700. string system32 = Environment.SystemDirectory;
  701. // trim the string "@tzres.dll" => "tzres.dll"
  702. string tzresDll = resources[0].TrimStart('@');
  703. try
  704. {
  705. filePath = Path.Combine(system32, tzresDll);
  706. }
  707. catch (ArgumentException)
  708. {
  709. // there were probably illegal characters in the path
  710. return string.Empty;
  711. }
  712. if (!int.TryParse(resources[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out resourceId))
  713. {
  714. return string.Empty;
  715. }
  716. resourceId = -resourceId;
  717. try
  718. {
  719. unsafe
  720. {
  721. char* fileMuiPath = stackalloc char[Interop.Kernel32.MAX_PATH];
  722. int fileMuiPathLength = Interop.Kernel32.MAX_PATH;
  723. int languageLength = 0;
  724. long enumerator = 0;
  725. bool succeeded = Interop.Kernel32.GetFileMUIPath(
  726. Interop.Kernel32.MUI_PREFERRED_UI_LANGUAGES,
  727. filePath, null /* language */, ref languageLength,
  728. fileMuiPath, ref fileMuiPathLength, ref enumerator);
  729. return succeeded ?
  730. TryGetLocalizedNameByNativeResource(new string(fileMuiPath, 0, fileMuiPathLength), resourceId) :
  731. string.Empty;
  732. }
  733. }
  734. catch (EntryPointNotFoundException)
  735. {
  736. return string.Empty;
  737. }
  738. }
  739. /// <summary>
  740. /// Helper function for retrieving a localized string resource via a native resource DLL.
  741. /// The function expects a string in the form: "C:\Windows\System32\en-us\resource.dll"
  742. ///
  743. /// "resource.dll" is a language-specific resource DLL.
  744. /// If the localized resource DLL exists, LoadString(resource) is returned.
  745. /// </summary>
  746. private static unsafe string TryGetLocalizedNameByNativeResource(string filePath, int resource)
  747. {
  748. using (SafeLibraryHandle handle = Interop.Kernel32.LoadLibraryEx(filePath, IntPtr.Zero, Interop.Kernel32.LOAD_LIBRARY_AS_DATAFILE))
  749. {
  750. if (!handle.IsInvalid)
  751. {
  752. const int LoadStringMaxLength = 500;
  753. char* localizedResource = stackalloc char[LoadStringMaxLength];
  754. int charsWritten = Interop.User32.LoadString(handle, (uint)resource, localizedResource, LoadStringMaxLength);
  755. if (charsWritten != 0)
  756. {
  757. return new string(localizedResource, 0, charsWritten);
  758. }
  759. }
  760. }
  761. return string.Empty;
  762. }
  763. /// <summary>
  764. /// Helper function for retrieving the DisplayName, StandardName, and DaylightName from the registry
  765. ///
  766. /// The function first checks the MUI_ key-values, and if they exist, it loads the strings from the MUI
  767. /// resource dll(s). When the keys do not exist, the function falls back to reading from the standard
  768. /// key-values
  769. /// </summary>
  770. private static void GetLocalizedNamesByRegistryKey(RegistryKey key, out string displayName, out string standardName, out string daylightName)
  771. {
  772. displayName = string.Empty;
  773. standardName = string.Empty;
  774. daylightName = string.Empty;
  775. // read the MUI_ registry keys
  776. string displayNameMuiResource = key.GetValue(MuiDisplayValue, string.Empty) as string;
  777. string standardNameMuiResource = key.GetValue(MuiStandardValue, string.Empty) as string;
  778. string daylightNameMuiResource = key.GetValue(MuiDaylightValue, string.Empty) as string;
  779. // try to load the strings from the native resource DLL(s)
  780. if (!string.IsNullOrEmpty(displayNameMuiResource))
  781. {
  782. displayName = TryGetLocalizedNameByMuiNativeResource(displayNameMuiResource);
  783. }
  784. if (!string.IsNullOrEmpty(standardNameMuiResource))
  785. {
  786. standardName = TryGetLocalizedNameByMuiNativeResource(standardNameMuiResource);
  787. }
  788. if (!string.IsNullOrEmpty(daylightNameMuiResource))
  789. {
  790. daylightName = TryGetLocalizedNameByMuiNativeResource(daylightNameMuiResource);
  791. }
  792. // fallback to using the standard registry keys
  793. if (string.IsNullOrEmpty(displayName))
  794. {
  795. displayName = key.GetValue(DisplayValue, string.Empty) as string;
  796. }
  797. if (string.IsNullOrEmpty(standardName))
  798. {
  799. standardName = key.GetValue(StandardValue, string.Empty) as string;
  800. }
  801. if (string.IsNullOrEmpty(daylightName))
  802. {
  803. daylightName = key.GetValue(DaylightValue, string.Empty) as string;
  804. }
  805. }
  806. /// <summary>
  807. /// Helper function that takes a string representing a time_zone_name registry key name
  808. /// and returns a TimeZoneInfo instance.
  809. /// </summary>
  810. private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachine(string id, out TimeZoneInfo value, out Exception e)
  811. {
  812. e = null;
  813. // Standard Time Zone Registry Data
  814. // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  815. // HKLM
  816. // Software
  817. // Microsoft
  818. // Windows NT
  819. // CurrentVersion
  820. // Time Zones
  821. // <time_zone_name>
  822. // * STD, REG_SZ "Standard Time Name"
  823. // (For OS installed zones, this will always be English)
  824. // * MUI_STD, REG_SZ "@tzres.dll,-1234"
  825. // Indirect string to localized resource for Standard Time,
  826. // add "%windir%\system32\" after "@"
  827. // * DLT, REG_SZ "Daylight Time Name"
  828. // (For OS installed zones, this will always be English)
  829. // * MUI_DLT, REG_SZ "@tzres.dll,-1234"
  830. // Indirect string to localized resource for Daylight Time,
  831. // add "%windir%\system32\" after "@"
  832. // * Display, REG_SZ "Display Name like (GMT-8:00) Pacific Time..."
  833. // * MUI_Display, REG_SZ "@tzres.dll,-1234"
  834. // Indirect string to localized resource for the Display,
  835. // add "%windir%\system32\" after "@"
  836. // * TZI, REG_BINARY REG_TZI_FORMAT
  837. //
  838. using (RegistryKey key = Registry.LocalMachine.OpenSubKey(TimeZonesRegistryHive + "\\" + id, writable: false))
  839. {
  840. if (key == null)
  841. {
  842. value = null;
  843. return TimeZoneInfoResult.TimeZoneNotFoundException;
  844. }
  845. REG_TZI_FORMAT defaultTimeZoneInformation;
  846. if (!TryGetTimeZoneEntryFromRegistry(key, TimeZoneInfoValue, out defaultTimeZoneInformation))
  847. {
  848. // the registry value could not be cast to a byte array
  849. value = null;
  850. return TimeZoneInfoResult.InvalidTimeZoneException;
  851. }
  852. AdjustmentRule[] adjustmentRules;
  853. if (!TryCreateAdjustmentRules(id, defaultTimeZoneInformation, out adjustmentRules, out e, defaultTimeZoneInformation.Bias))
  854. {
  855. value = null;
  856. return TimeZoneInfoResult.InvalidTimeZoneException;
  857. }
  858. GetLocalizedNamesByRegistryKey(key, out string displayName, out string standardName, out string daylightName);
  859. try
  860. {
  861. value = new TimeZoneInfo(
  862. id,
  863. new TimeSpan(0, -(defaultTimeZoneInformation.Bias), 0),
  864. displayName,
  865. standardName,
  866. daylightName,
  867. adjustmentRules,
  868. disableDaylightSavingTime: false);
  869. return TimeZoneInfoResult.Success;
  870. }
  871. catch (ArgumentException ex)
  872. {
  873. // TimeZoneInfo constructor can throw ArgumentException and InvalidTimeZoneException
  874. value = null;
  875. e = ex;
  876. return TimeZoneInfoResult.InvalidTimeZoneException;
  877. }
  878. catch (InvalidTimeZoneException ex)
  879. {
  880. // TimeZoneInfo constructor can throw ArgumentException and InvalidTimeZoneException
  881. value = null;
  882. e = ex;
  883. return TimeZoneInfoResult.InvalidTimeZoneException;
  884. }
  885. }
  886. }
  887. }
  888. }