File: System\Runtime\Serialization\Json\JsonReaderDelegator.cs
Web Access
Project: src\src\libraries\System.Private.DataContractSerialization\src\System.Private.DataContractSerialization.csproj (System.Private.DataContractSerialization)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Runtime.Serialization;
using System.Xml;
 
namespace System.Runtime.Serialization.Json
{
    internal sealed class JsonReaderDelegator : XmlReaderDelegator
    {
        private readonly DateTimeFormat? _dateTimeFormat;
        private DateTimeArrayJsonHelperWithString? _dateTimeArrayHelper;
 
        public JsonReaderDelegator(XmlReader reader)
            : base(reader)
        {
        }
 
        public JsonReaderDelegator(XmlReader reader, DateTimeFormat? dateTimeFormat)
            : this(reader)
        {
            _dateTimeFormat = dateTimeFormat;
        }
 
        internal XmlDictionaryReaderQuotas? ReaderQuotas => dictionaryReader?.Quotas;
 
        private DateTimeArrayJsonHelperWithString DateTimeArrayHelper =>
            _dateTimeArrayHelper ??= new DateTimeArrayJsonHelperWithString(_dateTimeFormat);
 
        internal static XmlQualifiedName ParseQualifiedName(string qname)
        {
            string name, ns;
            if (string.IsNullOrEmpty(qname))
            {
                name = ns = string.Empty;
            }
            else
            {
                qname = qname.Trim();
                int colon = qname.IndexOf(':');
                if (colon >= 0)
                {
                    name = qname.Substring(0, colon);
                    ns = qname.Substring(colon + 1);
                }
                else
                {
                    name = qname;
                    ns = string.Empty;
                }
            }
            return new XmlQualifiedName(name, ns);
        }
 
        internal override char ReadContentAsChar()
        {
            return XmlConvert.ToChar(ReadContentAsString());
        }
 
        internal override XmlQualifiedName ReadContentAsQName()
        {
            return ParseQualifiedName(ReadContentAsString());
        }
 
        internal override char ReadElementContentAsChar()
        {
            return XmlConvert.ToChar(ReadElementContentAsString());
        }
 
        public override byte[] ReadContentAsBase64()
        {
            if (isEndOfEmptyElement)
                return Array.Empty<byte>();
 
            byte[] buffer;
 
            if (dictionaryReader == null)
            {
                XmlDictionaryReader tempDictionaryReader = XmlDictionaryReader.CreateDictionaryReader(reader);
                buffer = ByteArrayHelperWithString.Instance.ReadArray(tempDictionaryReader, JsonGlobals.itemString, string.Empty, tempDictionaryReader.Quotas.MaxArrayLength);
            }
            else
            {
                buffer = ByteArrayHelperWithString.Instance.ReadArray(dictionaryReader, JsonGlobals.itemString, string.Empty, dictionaryReader.Quotas.MaxArrayLength);
            }
            return buffer;
        }
 
        internal override byte[] ReadElementContentAsBase64()
        {
            if (isEndOfEmptyElement)
            {
                throw new XmlException(SR.Format(SR.XmlStartElementExpected, "EndElement"));
            }
 
            bool isEmptyElement = reader.IsStartElement() && reader.IsEmptyElement;
            byte[] buffer;
 
            if (isEmptyElement)
            {
                reader.Read();
                buffer = Array.Empty<byte>();
            }
            else
            {
                reader.ReadStartElement();
                buffer = ReadContentAsBase64();
                reader.ReadEndElement();
            }
 
            return buffer;
        }
 
        internal override DateTime ReadContentAsDateTime()
        {
            return ParseJsonDate(ReadContentAsString(), _dateTimeFormat);
        }
 
        internal static DateTime ParseJsonDate(string originalDateTimeValue, DateTimeFormat? dateTimeFormat)
        {
            if (dateTimeFormat == null)
            {
                return ParseJsonDateInDefaultFormat(originalDateTimeValue);
            }
            else
            {
                return DateTime.ParseExact(originalDateTimeValue, dateTimeFormat.FormatString, dateTimeFormat.FormatProvider, dateTimeFormat.DateTimeStyles);
            }
        }
 
        internal static DateTime ParseJsonDateInDefaultFormat(string originalDateTimeValue)
        {
            // Dates are represented in JSON as "\/Date(number of ticks)\/".
            // The number of ticks is the number of milliseconds since January 1, 1970.
 
            string dateTimeValue;
 
            if (!string.IsNullOrEmpty(originalDateTimeValue))
            {
                dateTimeValue = originalDateTimeValue.Trim();
            }
            else
            {
                dateTimeValue = originalDateTimeValue;
            }
 
            if (string.IsNullOrEmpty(dateTimeValue) ||
                !dateTimeValue.StartsWith(JsonGlobals.DateTimeStartGuardReader, StringComparison.Ordinal) ||
                !dateTimeValue.EndsWith(JsonGlobals.DateTimeEndGuardReader, StringComparison.Ordinal))
            {
                throw new FormatException(SR.Format(SR.JsonInvalidDateTimeString, originalDateTimeValue, JsonGlobals.DateTimeStartGuardWriter, JsonGlobals.DateTimeEndGuardWriter));
            }
 
            ReadOnlySpan<char> ticksvalue = dateTimeValue.AsSpan(6, dateTimeValue.Length - 8);
            long millisecondsSinceUnixEpoch;
            DateTimeKind dateTimeKind = DateTimeKind.Utc;
            int indexOfTimeZoneOffset = ticksvalue.Slice(1).IndexOf('+');
 
            if (indexOfTimeZoneOffset == -1)
            {
                indexOfTimeZoneOffset = ticksvalue.Slice(1).IndexOf('-');
            }
 
            if (indexOfTimeZoneOffset >= 0)
            {
                dateTimeKind = DateTimeKind.Local;
                ticksvalue = ticksvalue.Slice(0, indexOfTimeZoneOffset + 1); // +1 for Slice above
            }
 
            try
            {
                millisecondsSinceUnixEpoch = long.Parse(ticksvalue, CultureInfo.InvariantCulture);
            }
            catch (Exception exception) when (exception is ArgumentException or FormatException or OverflowException)
            {
                throw XmlExceptionHelper.CreateConversionException(ticksvalue.ToString(), "Int64", exception);
            }
 
            // Convert from # milliseconds since epoch to # of 100-nanosecond units, which is what DateTime understands
            long ticks = millisecondsSinceUnixEpoch * 10000 + JsonGlobals.unixEpochTicks;
 
            try
            {
                DateTime dateTime = new DateTime(ticks, DateTimeKind.Utc);
                switch (dateTimeKind)
                {
                    case DateTimeKind.Local:
                        return dateTime.ToLocalTime();
                    case DateTimeKind.Unspecified:
                        return DateTime.SpecifyKind(dateTime.ToLocalTime(), DateTimeKind.Unspecified);
                    case DateTimeKind.Utc:
                    default:
                        return dateTime;
                }
            }
            catch (ArgumentException exception)
            {
                throw XmlExceptionHelper.CreateConversionException(ticksvalue.ToString(), "DateTime", exception);
            }
        }
 
        internal override DateTime ReadElementContentAsDateTime()
        {
            return ParseJsonDate(ReadElementContentAsString(), _dateTimeFormat);
        }
 
        internal override bool TryReadDateTimeArray(XmlObjectSerializerReadContext context,
        XmlDictionaryString itemName, XmlDictionaryString itemNamespace,
            int arrayLength, [NotNullWhen(true)] out DateTime[]? array)
        {
            return TryReadJsonDateTimeArray(context, itemName, itemNamespace, arrayLength, out array);
        }
 
        internal bool TryReadJsonDateTimeArray(XmlObjectSerializerReadContext context,
            XmlDictionaryString itemName, XmlDictionaryString itemNamespace,
            int arrayLength, [NotNullWhen(true)] out DateTime[]? array)
        {
            if ((dictionaryReader == null) || (arrayLength != -1))
            {
                array = null;
                return false;
            }
 
            array = this.DateTimeArrayHelper.ReadArray(dictionaryReader, XmlDictionaryString.GetString(itemName), XmlDictionaryString.GetString(itemNamespace), GetArrayLengthQuota(context));
            context.IncrementItemCount(array.Length);
 
            return true;
        }
 
        private sealed class DateTimeArrayJsonHelperWithString : ArrayHelper<string, DateTime>
        {
            private readonly DateTimeFormat? _dateTimeFormat;
 
            public DateTimeArrayJsonHelperWithString(DateTimeFormat? dateTimeFormat)
            {
                _dateTimeFormat = dateTimeFormat;
            }
 
            protected override int ReadArray(XmlDictionaryReader reader, string localName, string namespaceUri, DateTime[] array, int offset, int count)
            {
                XmlJsonReader.CheckArray(array, offset, count);
                int actual = 0;
                while (actual < count && reader.IsStartElement(JsonGlobals.itemString, string.Empty))
                {
                    array[offset + actual] = JsonReaderDelegator.ParseJsonDate(reader.ReadElementContentAsString(), _dateTimeFormat);
                    actual++;
                }
                return actual;
            }
 
            protected override void WriteArray(XmlDictionaryWriter writer, string prefix, string localName, string namespaceUri, DateTime[] array, int offset, int count)
            {
                throw NotImplemented.ByDesign;
            }
        }
 
        // Overridden because base reader relies on XmlConvert.ToUInt64 for conversion to ulong
        internal override ulong ReadContentAsUnsignedLong()
        {
            string value = reader.ReadContentAsString();
 
            if (string.IsNullOrEmpty(value))
            {
                throw new XmlException(XmlObjectSerializer.TryAddLineInfo(this, SR.Format(SR.XmlInvalidConversion, value, "UInt64")));
            }
 
            try
            {
                return ulong.Parse(value, NumberStyles.Float, NumberFormatInfo.InvariantInfo);
            }
            catch (ArgumentException exception)
            {
                throw XmlExceptionHelper.CreateConversionException(value, "UInt64", exception);
            }
            catch (FormatException exception)
            {
                throw XmlExceptionHelper.CreateConversionException(value, "UInt64", exception);
            }
            catch (OverflowException exception)
            {
                throw XmlExceptionHelper.CreateConversionException(value, "UInt64", exception);
            }
        }
 
        // Overridden because base reader relies on XmlConvert.ToUInt64 for conversion to ulong
        internal override ulong ReadElementContentAsUnsignedLong()
        {
            if (isEndOfEmptyElement)
            {
                throw new XmlException(SR.Format(SR.XmlStartElementExpected, "EndElement"));
            }
 
            string value = reader.ReadElementContentAsString();
 
            if (string.IsNullOrEmpty(value))
            {
                throw new XmlException(XmlObjectSerializer.TryAddLineInfo(this, SR.Format(SR.XmlInvalidConversion, value, "UInt64")));
            }
 
            try
            {
                return ulong.Parse(value, NumberStyles.Float, NumberFormatInfo.InvariantInfo);
            }
            catch (ArgumentException exception)
            {
                throw XmlExceptionHelper.CreateConversionException(value, "UInt64", exception);
            }
            catch (FormatException exception)
            {
                throw XmlExceptionHelper.CreateConversionException(value, "UInt64", exception);
            }
            catch (OverflowException exception)
            {
                throw XmlExceptionHelper.CreateConversionException(value, "UInt64", exception);
            }
        }
    }
}