TimeZoneInfo.Win32.cs 42 KB

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