TimeZoneInfo.Win32.cs 42 KB

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