// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System.Collections.Generic; using System.Globalization; using System.Runtime.Serialization; using System.Text; namespace System { public sealed partial class TimeZoneInfo { /// /// Used to serialize and deserialize TimeZoneInfo objects based on the custom string serialization format. /// private struct StringSerializer { private enum State { Escaped = 0, NotEscaped = 1, StartOfToken = 2, EndOfLine = 3 } private readonly string _serializedText; private int _currentTokenStartIndex; private State _state; // the majority of the strings contained in the OS time zones fit in 64 chars private const int InitialCapacityForString = 64; private const char Esc = '\\'; private const char Sep = ';'; private const char Lhs = '['; private const char Rhs = ']'; private const string DateTimeFormat = "MM:dd:yyyy"; private const string TimeOfDayFormat = "HH:mm:ss.FFF"; /// /// Creates the custom serialized string representation of a TimeZoneInfo instance. /// public static string GetSerializedString(TimeZoneInfo zone) { StringBuilder serializedText = StringBuilderCache.Acquire(); // // <_id>;<_baseUtcOffset>;<_displayName>;<_standardDisplayName>;<_daylightDispayName> // SerializeSubstitute(zone.Id, serializedText); serializedText.Append(Sep); serializedText.Append(zone.BaseUtcOffset.TotalMinutes.ToString(CultureInfo.InvariantCulture)); serializedText.Append(Sep); SerializeSubstitute(zone.DisplayName, serializedText); serializedText.Append(Sep); SerializeSubstitute(zone.StandardName, serializedText); serializedText.Append(Sep); SerializeSubstitute(zone.DaylightName, serializedText); serializedText.Append(Sep); AdjustmentRule[] rules = zone.GetAdjustmentRules(); foreach (AdjustmentRule rule in rules) { serializedText.Append(Lhs); serializedText.Append(rule.DateStart.ToString(DateTimeFormat, DateTimeFormatInfo.InvariantInfo)); serializedText.Append(Sep); serializedText.Append(rule.DateEnd.ToString(DateTimeFormat, DateTimeFormatInfo.InvariantInfo)); serializedText.Append(Sep); serializedText.Append(rule.DaylightDelta.TotalMinutes.ToString(CultureInfo.InvariantCulture)); serializedText.Append(Sep); // serialize the TransitionTime's SerializeTransitionTime(rule.DaylightTransitionStart, serializedText); serializedText.Append(Sep); SerializeTransitionTime(rule.DaylightTransitionEnd, serializedText); serializedText.Append(Sep); if (rule.BaseUtcOffsetDelta != TimeSpan.Zero) { // Serialize it only when BaseUtcOffsetDelta has a value to reduce the impact of adding rule.BaseUtcOffsetDelta serializedText.Append(rule.BaseUtcOffsetDelta.TotalMinutes.ToString(CultureInfo.InvariantCulture)); serializedText.Append(Sep); } if (rule.NoDaylightTransitions) { // Serialize it only when NoDaylightTransitions is true to reduce the impact of adding rule.NoDaylightTransitions serializedText.Append('1'); serializedText.Append(Sep); } serializedText.Append(Rhs); } serializedText.Append(Sep); return StringBuilderCache.GetStringAndRelease(serializedText); } /// /// Instantiates a TimeZoneInfo from a custom serialized string. /// public static TimeZoneInfo GetDeserializedTimeZoneInfo(string source) { StringSerializer s = new StringSerializer(source); string id = s.GetNextStringValue(); TimeSpan baseUtcOffset = s.GetNextTimeSpanValue(); string displayName = s.GetNextStringValue(); string standardName = s.GetNextStringValue(); string daylightName = s.GetNextStringValue(); AdjustmentRule[] rules = s.GetNextAdjustmentRuleArrayValue(); try { return new TimeZoneInfo(id, baseUtcOffset, displayName, standardName, daylightName, rules, disableDaylightSavingTime: false); } catch (ArgumentException ex) { throw new SerializationException(SR.Serialization_InvalidData, ex); } catch (InvalidTimeZoneException ex) { throw new SerializationException(SR.Serialization_InvalidData, ex); } } private StringSerializer(string str) { _serializedText = str; _currentTokenStartIndex = 0; _state = State.StartOfToken; } /// /// Appends the String to the StringBuilder with all of the reserved chars escaped. /// /// ";" -> "\;" /// "[" -> "\[" /// "]" -> "\]" /// "\" -> "\\" /// private static void SerializeSubstitute(string text, StringBuilder serializedText) { foreach (char c in text) { if (c == Esc || c == Lhs || c == Rhs || c == Sep) { serializedText.Append('\\'); } serializedText.Append(c); } } /// /// Helper method to serialize a TimeZoneInfo.TransitionTime object. /// private static void SerializeTransitionTime(TransitionTime time, StringBuilder serializedText) { serializedText.Append(Lhs); serializedText.Append(time.IsFixedDateRule ? '1' : '0'); serializedText.Append(Sep); serializedText.Append(time.TimeOfDay.ToString(TimeOfDayFormat, DateTimeFormatInfo.InvariantInfo)); serializedText.Append(Sep); serializedText.Append(time.Month.ToString(CultureInfo.InvariantCulture)); serializedText.Append(Sep); if (time.IsFixedDateRule) { serializedText.Append(time.Day.ToString(CultureInfo.InvariantCulture)); serializedText.Append(Sep); } else { serializedText.Append(time.Week.ToString(CultureInfo.InvariantCulture)); serializedText.Append(Sep); serializedText.Append(((int)time.DayOfWeek).ToString(CultureInfo.InvariantCulture)); serializedText.Append(Sep); } serializedText.Append(Rhs); } /// /// Helper function to determine if the passed in string token is allowed to be preceded by an escape sequence token. /// private static void VerifyIsEscapableCharacter(char c) { if (c != Esc && c != Sep && c != Lhs && c != Rhs) { throw new SerializationException(SR.Format(SR.Serialization_InvalidEscapeSequence, c)); } } /// /// Helper function that reads past "v.Next" data fields. Receives a "depth" parameter indicating the /// current relative nested bracket depth that _currentTokenStartIndex is at. The function ends /// successfully when "depth" returns to zero (0). /// private void SkipVersionNextDataFields(int depth /* starting depth in the nested brackets ('[', ']')*/) { if (_currentTokenStartIndex < 0 || _currentTokenStartIndex >= _serializedText.Length) { throw new SerializationException(SR.Serialization_InvalidData); } State tokenState = State.NotEscaped; // walk the serialized text, building up the token as we go... for (int i = _currentTokenStartIndex; i < _serializedText.Length; i++) { if (tokenState == State.Escaped) { VerifyIsEscapableCharacter(_serializedText[i]); tokenState = State.NotEscaped; } else if (tokenState == State.NotEscaped) { switch (_serializedText[i]) { case Esc: tokenState = State.Escaped; break; case Lhs: depth++; break; case Rhs: depth--; if (depth == 0) { _currentTokenStartIndex = i + 1; if (_currentTokenStartIndex >= _serializedText.Length) { _state = State.EndOfLine; } else { _state = State.StartOfToken; } return; } break; case '\0': // invalid character throw new SerializationException(SR.Serialization_InvalidData); default: break; } } } throw new SerializationException(SR.Serialization_InvalidData); } /// /// Helper function that reads a string token from the serialized text. The function /// updates to point to the next token on exit. /// Also is set to either or /// on exit. /// private string GetNextStringValue() { // first verify the internal state of the object if (_state == State.EndOfLine) { throw new SerializationException(SR.Serialization_InvalidData); } if (_currentTokenStartIndex < 0 || _currentTokenStartIndex >= _serializedText.Length) { throw new SerializationException(SR.Serialization_InvalidData); } State tokenState = State.NotEscaped; StringBuilder token = StringBuilderCache.Acquire(InitialCapacityForString); // walk the serialized text, building up the token as we go... for (int i = _currentTokenStartIndex; i < _serializedText.Length; i++) { if (tokenState == State.Escaped) { VerifyIsEscapableCharacter(_serializedText[i]); token.Append(_serializedText[i]); tokenState = State.NotEscaped; } else if (tokenState == State.NotEscaped) { switch (_serializedText[i]) { case Esc: tokenState = State.Escaped; break; case Lhs: // '[' is an unexpected character throw new SerializationException(SR.Serialization_InvalidData); case Rhs: // ']' is an unexpected character throw new SerializationException(SR.Serialization_InvalidData); case Sep: _currentTokenStartIndex = i + 1; if (_currentTokenStartIndex >= _serializedText.Length) { _state = State.EndOfLine; } else { _state = State.StartOfToken; } return StringBuilderCache.GetStringAndRelease(token); case '\0': // invalid character throw new SerializationException(SR.Serialization_InvalidData); default: token.Append(_serializedText[i]); break; } } } // // we are at the end of the line // if (tokenState == State.Escaped) { // we are at the end of the serialized text but we are in an escaped state throw new SerializationException(SR.Format(SR.Serialization_InvalidEscapeSequence, string.Empty)); } throw new SerializationException(SR.Serialization_InvalidData); } /// /// Helper function to read a DateTime token. /// private DateTime GetNextDateTimeValue(string format) { string token = GetNextStringValue(); DateTime time; if (!DateTime.TryParseExact(token, format, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None, out time)) { throw new SerializationException(SR.Serialization_InvalidData); } return time; } /// /// Helper function to read a TimeSpan token. /// private TimeSpan GetNextTimeSpanValue() { int token = GetNextInt32Value(); try { return new TimeSpan(hours: 0, minutes: token, seconds: 0); } catch (ArgumentOutOfRangeException e) { throw new SerializationException(SR.Serialization_InvalidData, e); } } /// /// Helper function to read an Int32 token. /// private int GetNextInt32Value() { string token = GetNextStringValue(); int value; if (!int.TryParse(token, NumberStyles.AllowLeadingSign /* "[sign]digits" */, CultureInfo.InvariantCulture, out value)) { throw new SerializationException(SR.Serialization_InvalidData); } return value; } /// /// Helper function to read an AdjustmentRule[] token. /// private AdjustmentRule[] GetNextAdjustmentRuleArrayValue() { List rules = new List(1); int count = 0; // individual AdjustmentRule array elements do not require semicolons AdjustmentRule rule = GetNextAdjustmentRuleValue(); while (rule != null) { rules.Add(rule); count++; rule = GetNextAdjustmentRuleValue(); } // the AdjustmentRule array must end with a separator if (_state == State.EndOfLine) { throw new SerializationException(SR.Serialization_InvalidData); } if (_currentTokenStartIndex < 0 || _currentTokenStartIndex >= _serializedText.Length) { throw new SerializationException(SR.Serialization_InvalidData); } return count != 0 ? rules.ToArray() : null; } /// /// Helper function to read an AdjustmentRule token. /// private AdjustmentRule GetNextAdjustmentRuleValue() { // first verify the internal state of the object if (_state == State.EndOfLine) { return null; } if (_currentTokenStartIndex < 0 || _currentTokenStartIndex >= _serializedText.Length) { throw new SerializationException(SR.Serialization_InvalidData); } // check to see if the very first token we see is the separator if (_serializedText[_currentTokenStartIndex] == Sep) { return null; } // verify the current token is a left-hand-side marker ("[") if (_serializedText[_currentTokenStartIndex] != Lhs) { throw new SerializationException(SR.Serialization_InvalidData); } _currentTokenStartIndex++; DateTime dateStart = GetNextDateTimeValue(DateTimeFormat); DateTime dateEnd = GetNextDateTimeValue(DateTimeFormat); TimeSpan daylightDelta = GetNextTimeSpanValue(); TransitionTime daylightStart = GetNextTransitionTimeValue(); TransitionTime daylightEnd = GetNextTransitionTimeValue(); TimeSpan baseUtcOffsetDelta = TimeSpan.Zero; int noDaylightTransitions = 0; // verify that the string is now at the right-hand-side marker ("]") ... if (_state == State.EndOfLine || _currentTokenStartIndex >= _serializedText.Length) { throw new SerializationException(SR.Serialization_InvalidData); } // Check if we have baseUtcOffsetDelta in the serialized string and then deserialize it if ((_serializedText[_currentTokenStartIndex] >= '0' && _serializedText[_currentTokenStartIndex] <= '9') || _serializedText[_currentTokenStartIndex] == '-' || _serializedText[_currentTokenStartIndex] == '+') { baseUtcOffsetDelta = GetNextTimeSpanValue(); } // Check if we have NoDaylightTransitions in the serialized string and then deserialize it if ((_serializedText[_currentTokenStartIndex] >= '0' && _serializedText[_currentTokenStartIndex] <= '1')) { noDaylightTransitions = GetNextInt32Value(); } if (_state == State.EndOfLine || _currentTokenStartIndex >= _serializedText.Length) { throw new SerializationException(SR.Serialization_InvalidData); } if (_serializedText[_currentTokenStartIndex] != Rhs) { // skip ahead of any "v.Next" data at the end of the AdjustmentRule // // FUTURE: if the serialization format is extended in the future then this // code section will need to be changed to read the new fields rather // than just skipping the data at the end of the [AdjustmentRule]. SkipVersionNextDataFields(1); } else { _currentTokenStartIndex++; } // create the AdjustmentRule from the deserialized fields ... AdjustmentRule rule; try { rule = AdjustmentRule.CreateAdjustmentRule(dateStart, dateEnd, daylightDelta, daylightStart, daylightEnd, baseUtcOffsetDelta, noDaylightTransitions > 0); } catch (ArgumentException e) { throw new SerializationException(SR.Serialization_InvalidData, e); } // finally set the state to either EndOfLine or StartOfToken for the next caller if (_currentTokenStartIndex >= _serializedText.Length) { _state = State.EndOfLine; } else { _state = State.StartOfToken; } return rule; } /// /// Helper function to read a TransitionTime token. /// private TransitionTime GetNextTransitionTimeValue() { // first verify the internal state of the object if (_state == State.EndOfLine || (_currentTokenStartIndex < _serializedText.Length && _serializedText[_currentTokenStartIndex] == Rhs)) { // // we are at the end of the line or we are starting at a "]" character // throw new SerializationException(SR.Serialization_InvalidData); } if (_currentTokenStartIndex < 0 || _currentTokenStartIndex >= _serializedText.Length) { throw new SerializationException(SR.Serialization_InvalidData); } // verify the current token is a left-hand-side marker ("[") if (_serializedText[_currentTokenStartIndex] != Lhs) { throw new SerializationException(SR.Serialization_InvalidData); } _currentTokenStartIndex++; int isFixedDate = GetNextInt32Value(); if (isFixedDate != 0 && isFixedDate != 1) { throw new SerializationException(SR.Serialization_InvalidData); } TransitionTime transition; DateTime timeOfDay = GetNextDateTimeValue(TimeOfDayFormat); timeOfDay = new DateTime(1, 1, 1, timeOfDay.Hour, timeOfDay.Minute, timeOfDay.Second, timeOfDay.Millisecond); int month = GetNextInt32Value(); if (isFixedDate == 1) { int day = GetNextInt32Value(); try { transition = TransitionTime.CreateFixedDateRule(timeOfDay, month, day); } catch (ArgumentException e) { throw new SerializationException(SR.Serialization_InvalidData, e); } } else { int week = GetNextInt32Value(); int dayOfWeek = GetNextInt32Value(); try { transition = TransitionTime.CreateFloatingDateRule(timeOfDay, month, week, (DayOfWeek)dayOfWeek); } catch (ArgumentException e) { throw new SerializationException(SR.Serialization_InvalidData, e); } } // verify that the string is now at the right-hand-side marker ("]") ... if (_state == State.EndOfLine || _currentTokenStartIndex >= _serializedText.Length) { throw new SerializationException(SR.Serialization_InvalidData); } if (_serializedText[_currentTokenStartIndex] != Rhs) { // skip ahead of any "v.Next" data at the end of the AdjustmentRule // // FUTURE: if the serialization format is extended in the future then this // code section will need to be changed to read the new fields rather // than just skipping the data at the end of the [TransitionTime]. SkipVersionNextDataFields(1); } else { _currentTokenStartIndex++; } // check to see if the string is now at the separator (";") ... bool sepFound = false; if (_currentTokenStartIndex < _serializedText.Length && _serializedText[_currentTokenStartIndex] == Sep) { // handle the case where we ended on a ";" _currentTokenStartIndex++; sepFound = true; } if (!sepFound) { // we MUST end on a separator throw new SerializationException(SR.Serialization_InvalidData); } // finally set the state to either EndOfLine or StartOfToken for the next caller if (_currentTokenStartIndex >= _serializedText.Length) { _state = State.EndOfLine; } else { _state = State.StartOfToken; } return transition; } } } }