TimeZoneInfo.StringSerializer.cs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625
  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.Globalization;
  6. using System.Runtime.Serialization;
  7. using System.Text;
  8. namespace System
  9. {
  10. public sealed partial class TimeZoneInfo
  11. {
  12. /// <summary>
  13. /// Used to serialize and deserialize TimeZoneInfo objects based on the custom string serialization format.
  14. /// </summary>
  15. private struct StringSerializer
  16. {
  17. private enum State
  18. {
  19. Escaped = 0,
  20. NotEscaped = 1,
  21. StartOfToken = 2,
  22. EndOfLine = 3
  23. }
  24. private readonly string _serializedText;
  25. private int _currentTokenStartIndex;
  26. private State _state;
  27. // the majority of the strings contained in the OS time zones fit in 64 chars
  28. private const int InitialCapacityForString = 64;
  29. private const char Esc = '\\';
  30. private const char Sep = ';';
  31. private const char Lhs = '[';
  32. private const char Rhs = ']';
  33. private const string DateTimeFormat = "MM:dd:yyyy";
  34. private const string TimeOfDayFormat = "HH:mm:ss.FFF";
  35. /// <summary>
  36. /// Creates the custom serialized string representation of a TimeZoneInfo instance.
  37. /// </summary>
  38. public static string GetSerializedString(TimeZoneInfo zone)
  39. {
  40. StringBuilder serializedText = StringBuilderCache.Acquire();
  41. //
  42. // <_id>;<_baseUtcOffset>;<_displayName>;<_standardDisplayName>;<_daylightDispayName>
  43. //
  44. SerializeSubstitute(zone.Id, serializedText);
  45. serializedText.Append(Sep);
  46. serializedText.Append(zone.BaseUtcOffset.TotalMinutes.ToString(CultureInfo.InvariantCulture));
  47. serializedText.Append(Sep);
  48. SerializeSubstitute(zone.DisplayName, serializedText);
  49. serializedText.Append(Sep);
  50. SerializeSubstitute(zone.StandardName, serializedText);
  51. serializedText.Append(Sep);
  52. SerializeSubstitute(zone.DaylightName, serializedText);
  53. serializedText.Append(Sep);
  54. AdjustmentRule[] rules = zone.GetAdjustmentRules();
  55. foreach (AdjustmentRule rule in rules)
  56. {
  57. serializedText.Append(Lhs);
  58. serializedText.Append(rule.DateStart.ToString(DateTimeFormat, DateTimeFormatInfo.InvariantInfo));
  59. serializedText.Append(Sep);
  60. serializedText.Append(rule.DateEnd.ToString(DateTimeFormat, DateTimeFormatInfo.InvariantInfo));
  61. serializedText.Append(Sep);
  62. serializedText.Append(rule.DaylightDelta.TotalMinutes.ToString(CultureInfo.InvariantCulture));
  63. serializedText.Append(Sep);
  64. // serialize the TransitionTime's
  65. SerializeTransitionTime(rule.DaylightTransitionStart, serializedText);
  66. serializedText.Append(Sep);
  67. SerializeTransitionTime(rule.DaylightTransitionEnd, serializedText);
  68. serializedText.Append(Sep);
  69. if (rule.BaseUtcOffsetDelta != TimeSpan.Zero)
  70. {
  71. // Serialize it only when BaseUtcOffsetDelta has a value to reduce the impact of adding rule.BaseUtcOffsetDelta
  72. serializedText.Append(rule.BaseUtcOffsetDelta.TotalMinutes.ToString(CultureInfo.InvariantCulture));
  73. serializedText.Append(Sep);
  74. }
  75. if (rule.NoDaylightTransitions)
  76. {
  77. // Serialize it only when NoDaylightTransitions is true to reduce the impact of adding rule.NoDaylightTransitions
  78. serializedText.Append('1');
  79. serializedText.Append(Sep);
  80. }
  81. serializedText.Append(Rhs);
  82. }
  83. serializedText.Append(Sep);
  84. return StringBuilderCache.GetStringAndRelease(serializedText);
  85. }
  86. /// <summary>
  87. /// Instantiates a TimeZoneInfo from a custom serialized string.
  88. /// </summary>
  89. public static TimeZoneInfo GetDeserializedTimeZoneInfo(string source)
  90. {
  91. StringSerializer s = new StringSerializer(source);
  92. string id = s.GetNextStringValue();
  93. TimeSpan baseUtcOffset = s.GetNextTimeSpanValue();
  94. string displayName = s.GetNextStringValue();
  95. string standardName = s.GetNextStringValue();
  96. string daylightName = s.GetNextStringValue();
  97. AdjustmentRule[] rules = s.GetNextAdjustmentRuleArrayValue();
  98. try
  99. {
  100. return new TimeZoneInfo(id, baseUtcOffset, displayName, standardName, daylightName, rules, disableDaylightSavingTime: false);
  101. }
  102. catch (ArgumentException ex)
  103. {
  104. throw new SerializationException(SR.Serialization_InvalidData, ex);
  105. }
  106. catch (InvalidTimeZoneException ex)
  107. {
  108. throw new SerializationException(SR.Serialization_InvalidData, ex);
  109. }
  110. }
  111. private StringSerializer(string str)
  112. {
  113. _serializedText = str;
  114. _currentTokenStartIndex = 0;
  115. _state = State.StartOfToken;
  116. }
  117. /// <summary>
  118. /// Appends the String to the StringBuilder with all of the reserved chars escaped.
  119. ///
  120. /// ";" -> "\;"
  121. /// "[" -> "\["
  122. /// "]" -> "\]"
  123. /// "\" -> "\\"
  124. /// </summary>
  125. private static void SerializeSubstitute(string text, StringBuilder serializedText)
  126. {
  127. foreach (char c in text)
  128. {
  129. if (c == Esc || c == Lhs || c == Rhs || c == Sep)
  130. {
  131. serializedText.Append('\\');
  132. }
  133. serializedText.Append(c);
  134. }
  135. }
  136. /// <summary>
  137. /// Helper method to serialize a TimeZoneInfo.TransitionTime object.
  138. /// </summary>
  139. private static void SerializeTransitionTime(TransitionTime time, StringBuilder serializedText)
  140. {
  141. serializedText.Append(Lhs);
  142. serializedText.Append(time.IsFixedDateRule ? '1' : '0');
  143. serializedText.Append(Sep);
  144. serializedText.Append(time.TimeOfDay.ToString(TimeOfDayFormat, DateTimeFormatInfo.InvariantInfo));
  145. serializedText.Append(Sep);
  146. serializedText.Append(time.Month.ToString(CultureInfo.InvariantCulture));
  147. serializedText.Append(Sep);
  148. if (time.IsFixedDateRule)
  149. {
  150. serializedText.Append(time.Day.ToString(CultureInfo.InvariantCulture));
  151. serializedText.Append(Sep);
  152. }
  153. else
  154. {
  155. serializedText.Append(time.Week.ToString(CultureInfo.InvariantCulture));
  156. serializedText.Append(Sep);
  157. serializedText.Append(((int)time.DayOfWeek).ToString(CultureInfo.InvariantCulture));
  158. serializedText.Append(Sep);
  159. }
  160. serializedText.Append(Rhs);
  161. }
  162. /// <summary>
  163. /// Helper function to determine if the passed in string token is allowed to be preceded by an escape sequence token.
  164. /// </summary>
  165. private static void VerifyIsEscapableCharacter(char c)
  166. {
  167. if (c != Esc && c != Sep && c != Lhs && c != Rhs)
  168. {
  169. throw new SerializationException(SR.Format(SR.Serialization_InvalidEscapeSequence, c));
  170. }
  171. }
  172. /// <summary>
  173. /// Helper function that reads past "v.Next" data fields. Receives a "depth" parameter indicating the
  174. /// current relative nested bracket depth that _currentTokenStartIndex is at. The function ends
  175. /// successfully when "depth" returns to zero (0).
  176. /// </summary>
  177. private void SkipVersionNextDataFields(int depth /* starting depth in the nested brackets ('[', ']')*/)
  178. {
  179. if (_currentTokenStartIndex < 0 || _currentTokenStartIndex >= _serializedText.Length)
  180. {
  181. throw new SerializationException(SR.Serialization_InvalidData);
  182. }
  183. State tokenState = State.NotEscaped;
  184. // walk the serialized text, building up the token as we go...
  185. for (int i = _currentTokenStartIndex; i < _serializedText.Length; i++)
  186. {
  187. if (tokenState == State.Escaped)
  188. {
  189. VerifyIsEscapableCharacter(_serializedText[i]);
  190. tokenState = State.NotEscaped;
  191. }
  192. else if (tokenState == State.NotEscaped)
  193. {
  194. switch (_serializedText[i])
  195. {
  196. case Esc:
  197. tokenState = State.Escaped;
  198. break;
  199. case Lhs:
  200. depth++;
  201. break;
  202. case Rhs:
  203. depth--;
  204. if (depth == 0)
  205. {
  206. _currentTokenStartIndex = i + 1;
  207. if (_currentTokenStartIndex >= _serializedText.Length)
  208. {
  209. _state = State.EndOfLine;
  210. }
  211. else
  212. {
  213. _state = State.StartOfToken;
  214. }
  215. return;
  216. }
  217. break;
  218. case '\0':
  219. // invalid character
  220. throw new SerializationException(SR.Serialization_InvalidData);
  221. default:
  222. break;
  223. }
  224. }
  225. }
  226. throw new SerializationException(SR.Serialization_InvalidData);
  227. }
  228. /// <summary>
  229. /// Helper function that reads a string token from the serialized text. The function
  230. /// updates <see cref="_currentTokenStartIndex"/> to point to the next token on exit.
  231. /// Also <see cref="_state"/> is set to either <see cref="State.StartOfToken"/> or
  232. /// <see cref="State.EndOfLine"/> on exit.
  233. /// </summary>
  234. private string GetNextStringValue()
  235. {
  236. // first verify the internal state of the object
  237. if (_state == State.EndOfLine)
  238. {
  239. throw new SerializationException(SR.Serialization_InvalidData);
  240. }
  241. if (_currentTokenStartIndex < 0 || _currentTokenStartIndex >= _serializedText.Length)
  242. {
  243. throw new SerializationException(SR.Serialization_InvalidData);
  244. }
  245. State tokenState = State.NotEscaped;
  246. StringBuilder token = StringBuilderCache.Acquire(InitialCapacityForString);
  247. // walk the serialized text, building up the token as we go...
  248. for (int i = _currentTokenStartIndex; i < _serializedText.Length; i++)
  249. {
  250. if (tokenState == State.Escaped)
  251. {
  252. VerifyIsEscapableCharacter(_serializedText[i]);
  253. token.Append(_serializedText[i]);
  254. tokenState = State.NotEscaped;
  255. }
  256. else if (tokenState == State.NotEscaped)
  257. {
  258. switch (_serializedText[i])
  259. {
  260. case Esc:
  261. tokenState = State.Escaped;
  262. break;
  263. case Lhs:
  264. // '[' is an unexpected character
  265. throw new SerializationException(SR.Serialization_InvalidData);
  266. case Rhs:
  267. // ']' is an unexpected character
  268. throw new SerializationException(SR.Serialization_InvalidData);
  269. case Sep:
  270. _currentTokenStartIndex = i + 1;
  271. if (_currentTokenStartIndex >= _serializedText.Length)
  272. {
  273. _state = State.EndOfLine;
  274. }
  275. else
  276. {
  277. _state = State.StartOfToken;
  278. }
  279. return StringBuilderCache.GetStringAndRelease(token);
  280. case '\0':
  281. // invalid character
  282. throw new SerializationException(SR.Serialization_InvalidData);
  283. default:
  284. token.Append(_serializedText[i]);
  285. break;
  286. }
  287. }
  288. }
  289. //
  290. // we are at the end of the line
  291. //
  292. if (tokenState == State.Escaped)
  293. {
  294. // we are at the end of the serialized text but we are in an escaped state
  295. throw new SerializationException(SR.Format(SR.Serialization_InvalidEscapeSequence, string.Empty));
  296. }
  297. throw new SerializationException(SR.Serialization_InvalidData);
  298. }
  299. /// <summary>
  300. /// Helper function to read a DateTime token.
  301. /// </summary>
  302. private DateTime GetNextDateTimeValue(string format)
  303. {
  304. string token = GetNextStringValue();
  305. DateTime time;
  306. if (!DateTime.TryParseExact(token, format, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None, out time))
  307. {
  308. throw new SerializationException(SR.Serialization_InvalidData);
  309. }
  310. return time;
  311. }
  312. /// <summary>
  313. /// Helper function to read a TimeSpan token.
  314. /// </summary>
  315. private TimeSpan GetNextTimeSpanValue()
  316. {
  317. int token = GetNextInt32Value();
  318. try
  319. {
  320. return new TimeSpan(hours: 0, minutes: token, seconds: 0);
  321. }
  322. catch (ArgumentOutOfRangeException e)
  323. {
  324. throw new SerializationException(SR.Serialization_InvalidData, e);
  325. }
  326. }
  327. /// <summary>
  328. /// Helper function to read an Int32 token.
  329. /// </summary>
  330. private int GetNextInt32Value()
  331. {
  332. string token = GetNextStringValue();
  333. int value;
  334. if (!int.TryParse(token, NumberStyles.AllowLeadingSign /* "[sign]digits" */, CultureInfo.InvariantCulture, out value))
  335. {
  336. throw new SerializationException(SR.Serialization_InvalidData);
  337. }
  338. return value;
  339. }
  340. /// <summary>
  341. /// Helper function to read an AdjustmentRule[] token.
  342. /// </summary>
  343. private AdjustmentRule[] GetNextAdjustmentRuleArrayValue()
  344. {
  345. List<AdjustmentRule> rules = new List<AdjustmentRule>(1);
  346. int count = 0;
  347. // individual AdjustmentRule array elements do not require semicolons
  348. AdjustmentRule rule = GetNextAdjustmentRuleValue();
  349. while (rule != null)
  350. {
  351. rules.Add(rule);
  352. count++;
  353. rule = GetNextAdjustmentRuleValue();
  354. }
  355. // the AdjustmentRule array must end with a separator
  356. if (_state == State.EndOfLine)
  357. {
  358. throw new SerializationException(SR.Serialization_InvalidData);
  359. }
  360. if (_currentTokenStartIndex < 0 || _currentTokenStartIndex >= _serializedText.Length)
  361. {
  362. throw new SerializationException(SR.Serialization_InvalidData);
  363. }
  364. return count != 0 ? rules.ToArray() : null;
  365. }
  366. /// <summary>
  367. /// Helper function to read an AdjustmentRule token.
  368. /// </summary>
  369. private AdjustmentRule GetNextAdjustmentRuleValue()
  370. {
  371. // first verify the internal state of the object
  372. if (_state == State.EndOfLine)
  373. {
  374. return null;
  375. }
  376. if (_currentTokenStartIndex < 0 || _currentTokenStartIndex >= _serializedText.Length)
  377. {
  378. throw new SerializationException(SR.Serialization_InvalidData);
  379. }
  380. // check to see if the very first token we see is the separator
  381. if (_serializedText[_currentTokenStartIndex] == Sep)
  382. {
  383. return null;
  384. }
  385. // verify the current token is a left-hand-side marker ("[")
  386. if (_serializedText[_currentTokenStartIndex] != Lhs)
  387. {
  388. throw new SerializationException(SR.Serialization_InvalidData);
  389. }
  390. _currentTokenStartIndex++;
  391. DateTime dateStart = GetNextDateTimeValue(DateTimeFormat);
  392. DateTime dateEnd = GetNextDateTimeValue(DateTimeFormat);
  393. TimeSpan daylightDelta = GetNextTimeSpanValue();
  394. TransitionTime daylightStart = GetNextTransitionTimeValue();
  395. TransitionTime daylightEnd = GetNextTransitionTimeValue();
  396. TimeSpan baseUtcOffsetDelta = TimeSpan.Zero;
  397. int noDaylightTransitions = 0;
  398. // verify that the string is now at the right-hand-side marker ("]") ...
  399. if (_state == State.EndOfLine || _currentTokenStartIndex >= _serializedText.Length)
  400. {
  401. throw new SerializationException(SR.Serialization_InvalidData);
  402. }
  403. // Check if we have baseUtcOffsetDelta in the serialized string and then deserialize it
  404. if ((_serializedText[_currentTokenStartIndex] >= '0' && _serializedText[_currentTokenStartIndex] <= '9') ||
  405. _serializedText[_currentTokenStartIndex] == '-' || _serializedText[_currentTokenStartIndex] == '+')
  406. {
  407. baseUtcOffsetDelta = GetNextTimeSpanValue();
  408. }
  409. // Check if we have NoDaylightTransitions in the serialized string and then deserialize it
  410. if ((_serializedText[_currentTokenStartIndex] >= '0' && _serializedText[_currentTokenStartIndex] <= '1'))
  411. {
  412. noDaylightTransitions = GetNextInt32Value();
  413. }
  414. if (_state == State.EndOfLine || _currentTokenStartIndex >= _serializedText.Length)
  415. {
  416. throw new SerializationException(SR.Serialization_InvalidData);
  417. }
  418. if (_serializedText[_currentTokenStartIndex] != Rhs)
  419. {
  420. // skip ahead of any "v.Next" data at the end of the AdjustmentRule
  421. //
  422. // FUTURE: if the serialization format is extended in the future then this
  423. // code section will need to be changed to read the new fields rather
  424. // than just skipping the data at the end of the [AdjustmentRule].
  425. SkipVersionNextDataFields(1);
  426. }
  427. else
  428. {
  429. _currentTokenStartIndex++;
  430. }
  431. // create the AdjustmentRule from the deserialized fields ...
  432. AdjustmentRule rule;
  433. try
  434. {
  435. rule = AdjustmentRule.CreateAdjustmentRule(dateStart, dateEnd, daylightDelta, daylightStart, daylightEnd, baseUtcOffsetDelta, noDaylightTransitions > 0);
  436. }
  437. catch (ArgumentException e)
  438. {
  439. throw new SerializationException(SR.Serialization_InvalidData, e);
  440. }
  441. // finally set the state to either EndOfLine or StartOfToken for the next caller
  442. if (_currentTokenStartIndex >= _serializedText.Length)
  443. {
  444. _state = State.EndOfLine;
  445. }
  446. else
  447. {
  448. _state = State.StartOfToken;
  449. }
  450. return rule;
  451. }
  452. /// <summary>
  453. /// Helper function to read a TransitionTime token.
  454. /// </summary>
  455. private TransitionTime GetNextTransitionTimeValue()
  456. {
  457. // first verify the internal state of the object
  458. if (_state == State.EndOfLine ||
  459. (_currentTokenStartIndex < _serializedText.Length && _serializedText[_currentTokenStartIndex] == Rhs))
  460. {
  461. //
  462. // we are at the end of the line or we are starting at a "]" character
  463. //
  464. throw new SerializationException(SR.Serialization_InvalidData);
  465. }
  466. if (_currentTokenStartIndex < 0 || _currentTokenStartIndex >= _serializedText.Length)
  467. {
  468. throw new SerializationException(SR.Serialization_InvalidData);
  469. }
  470. // verify the current token is a left-hand-side marker ("[")
  471. if (_serializedText[_currentTokenStartIndex] != Lhs)
  472. {
  473. throw new SerializationException(SR.Serialization_InvalidData);
  474. }
  475. _currentTokenStartIndex++;
  476. int isFixedDate = GetNextInt32Value();
  477. if (isFixedDate != 0 && isFixedDate != 1)
  478. {
  479. throw new SerializationException(SR.Serialization_InvalidData);
  480. }
  481. TransitionTime transition;
  482. DateTime timeOfDay = GetNextDateTimeValue(TimeOfDayFormat);
  483. timeOfDay = new DateTime(1, 1, 1, timeOfDay.Hour, timeOfDay.Minute, timeOfDay.Second, timeOfDay.Millisecond);
  484. int month = GetNextInt32Value();
  485. if (isFixedDate == 1)
  486. {
  487. int day = GetNextInt32Value();
  488. try
  489. {
  490. transition = TransitionTime.CreateFixedDateRule(timeOfDay, month, day);
  491. }
  492. catch (ArgumentException e)
  493. {
  494. throw new SerializationException(SR.Serialization_InvalidData, e);
  495. }
  496. }
  497. else
  498. {
  499. int week = GetNextInt32Value();
  500. int dayOfWeek = GetNextInt32Value();
  501. try
  502. {
  503. transition = TransitionTime.CreateFloatingDateRule(timeOfDay, month, week, (DayOfWeek)dayOfWeek);
  504. }
  505. catch (ArgumentException e)
  506. {
  507. throw new SerializationException(SR.Serialization_InvalidData, e);
  508. }
  509. }
  510. // verify that the string is now at the right-hand-side marker ("]") ...
  511. if (_state == State.EndOfLine || _currentTokenStartIndex >= _serializedText.Length)
  512. {
  513. throw new SerializationException(SR.Serialization_InvalidData);
  514. }
  515. if (_serializedText[_currentTokenStartIndex] != Rhs)
  516. {
  517. // skip ahead of any "v.Next" data at the end of the AdjustmentRule
  518. //
  519. // FUTURE: if the serialization format is extended in the future then this
  520. // code section will need to be changed to read the new fields rather
  521. // than just skipping the data at the end of the [TransitionTime].
  522. SkipVersionNextDataFields(1);
  523. }
  524. else
  525. {
  526. _currentTokenStartIndex++;
  527. }
  528. // check to see if the string is now at the separator (";") ...
  529. bool sepFound = false;
  530. if (_currentTokenStartIndex < _serializedText.Length &&
  531. _serializedText[_currentTokenStartIndex] == Sep)
  532. {
  533. // handle the case where we ended on a ";"
  534. _currentTokenStartIndex++;
  535. sepFound = true;
  536. }
  537. if (!sepFound)
  538. {
  539. // we MUST end on a separator
  540. throw new SerializationException(SR.Serialization_InvalidData);
  541. }
  542. // finally set the state to either EndOfLine or StartOfToken for the next caller
  543. if (_currentTokenStartIndex >= _serializedText.Length)
  544. {
  545. _state = State.EndOfLine;
  546. }
  547. else
  548. {
  549. _state = State.StartOfToken;
  550. }
  551. return transition;
  552. }
  553. }
  554. }
  555. }