TimeZoneInfo.Win32.cs 43 KB

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