File: System\Runtime\Serialization\Json\XmlJsonWriter.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;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Runtime;
using System.Runtime.Serialization;
using System.Text;
using System.Xml;
 
namespace System.Runtime.Serialization.Json
{
    internal sealed class XmlJsonWriter : XmlDictionaryWriter, IXmlJsonWriterInitializer
    {
        private const char BACK_SLASH = '\\';
        private const char FORWARD_SLASH = '/';
 
        private const char HIGH_SURROGATE_START = (char)0xd800;
        private const char LOW_SURROGATE_END = (char)0xdfff;
        private const char MAX_CHAR = (char)0xfffe;
        private const char WHITESPACE = ' ';
        private const char CARRIAGE_RETURN = '\r';
        private const char NEWLINE = '\n';
        private const string xmlNamespace = "http://www.w3.org/XML/1998/namespace";
        private const string xmlnsNamespace = "http://www.w3.org/2000/xmlns/";
 
        // This array was part of a perf improvement for escaping characters < WHITESPACE.
        private static readonly string[] s_escapedJsonStringTable =
        {
            "\\u0000",
            "\\u0001",
            "\\u0002",
            "\\u0003",
            "\\u0004",
            "\\u0005",
            "\\u0006",
            "\\u0007",
            "\\b",
            "\\t",
            "\\n",
            "\\u000b",
            "\\f",
            "\\r",
            "\\u000e",
            "\\u000f",
            "\\u0010",
            "\\u0011",
            "\\u0012",
            "\\u0013",
            "\\u0014",
            "\\u0015",
            "\\u0016",
            "\\u0017",
            "\\u0018",
            "\\u0019",
            "\\u001a",
            "\\u001b",
            "\\u001c",
            "\\u001d",
            "\\u001e",
            "\\u001f"
        };
 
        private string? _attributeText;
        private JsonDataType _dataType;
        private int _depth;
        private bool _endElementBuffer;
        private bool _isWritingDataTypeAttribute;
        private bool _isWritingServerTypeAttribute;
        private bool _isWritingXmlnsAttribute;
        private bool _isWritingXmlnsAttributeDefaultNs;
        private NameState _nameState;
        private JsonNodeType _nodeType;
        private JsonNodeWriter _nodeWriter = null!; // initialized in SetOutput
        private JsonNodeType[]? _scopes;
        private string? _serverTypeValue;
        // Do not use this field's value anywhere other than the WriteState property.
        // It's OK to set this field's value anywhere and then change the WriteState property appropriately.
        // If it's necessary to check the WriteState outside WriteState, use the WriteState property.
        private WriteState _writeState;
        private bool _wroteServerTypeAttribute;
        private readonly bool _indent;
        private readonly string? _indentChars;
        private int _indentLevel;
 
        public XmlJsonWriter() : this(false, null) { }
 
        public XmlJsonWriter(bool indent, string? indentChars)
        {
            _indent = indent;
            if (indent)
            {
                ArgumentNullException.ThrowIfNull(indentChars);
                _indentChars = indentChars;
            }
            InitializeWriter();
        }
 
        private enum JsonDataType
        {
            None,
            Null,
            Boolean,
            Number,
            String,
            Object,
            Array
        };
 
        [Flags]
        private enum NameState
        {
            None = 0,
            IsWritingNameWithMapping = 1,
            IsWritingNameAttribute = 2,
            WrittenNameWithMapping = 4,
        }
 
        public override XmlWriterSettings? Settings
        {
            // The XmlWriterSettings object used to create this writer instance.
            // If this writer was not created using the Create method, this property
            // returns a null reference.
            get { return null; }
        }
 
        public override WriteState WriteState
        {
            get
            {
                if (_writeState == WriteState.Closed)
                {
                    return WriteState.Closed;
                }
                if (HasOpenAttribute)
                {
                    return WriteState.Attribute;
                }
                switch (_nodeType)
                {
                    case JsonNodeType.None:
                        return WriteState.Start;
                    case JsonNodeType.Element:
                        return WriteState.Element;
                    case JsonNodeType.QuotedText:
                    case JsonNodeType.StandaloneText:
                    case JsonNodeType.EndElement:
                        return WriteState.Content;
                    default:
                        return WriteState.Error;
                }
            }
        }
 
        public override string? XmlLang
        {
            get { return null; }
        }
 
        public override XmlSpace XmlSpace
        {
            get { return XmlSpace.None; }
        }
 
        private bool HasOpenAttribute => (_isWritingDataTypeAttribute || _isWritingServerTypeAttribute || IsWritingNameAttribute || _isWritingXmlnsAttribute);
 
        private bool IsClosed => (WriteState == WriteState.Closed);
 
        private bool IsWritingCollection => (_depth > 0) && (_scopes![_depth] == JsonNodeType.Collection);
 
        private bool IsWritingNameAttribute => (_nameState & NameState.IsWritingNameAttribute) == NameState.IsWritingNameAttribute;
 
        private bool IsWritingNameWithMapping => (_nameState & NameState.IsWritingNameWithMapping) == NameState.IsWritingNameWithMapping;
 
        private bool WrittenNameWithMapping => (_nameState & NameState.WrittenNameWithMapping) == NameState.WrittenNameWithMapping;
 
        public override void Close()
        {
            if (!IsClosed)
            {
                try
                {
                    WriteEndDocument();
                }
                finally
                {
                    try
                    {
                        _nodeWriter.Flush();
                        _nodeWriter.Close();
                    }
                    finally
                    {
                        _writeState = WriteState.Closed;
                        if (_depth != 0)
                        {
                            _depth = 0;
                        }
                    }
                }
            }
 
            base.Close();
        }
 
        public override void Flush()
        {
            if (IsClosed)
            {
                ThrowClosed();
            }
            _nodeWriter.Flush();
        }
 
        public override string? LookupPrefix(string ns)
        {
            ArgumentNullException.ThrowIfNull(ns);
 
            if (ns == Globals.XmlnsNamespace)
            {
                return Globals.XmlnsPrefix;
            }
            if (ns == xmlNamespace)
            {
                return JsonGlobals.xmlPrefix;
            }
            if (ns.Length == 0)
            {
                return string.Empty;
            }
            return null;
        }
 
        public void SetOutput(Stream stream, Encoding encoding, bool ownsStream)
        {
            ArgumentNullException.ThrowIfNull(stream);
            ArgumentNullException.ThrowIfNull(encoding);
 
            if (encoding.WebName != Encoding.UTF8.WebName)
            {
                stream = new JsonEncodingStreamWrapper(stream, encoding, false);
            }
            else
            {
                encoding = null!;
            }
 
            _nodeWriter ??= new JsonNodeWriter();
 
            _nodeWriter.SetOutput(stream, ownsStream, encoding);
            InitializeWriter();
        }
 
        public override void WriteArray(string? prefix, string localName, string? namespaceUri, bool[] array, int offset, int count)
        {
            throw new NotSupportedException(SR.JsonWriteArrayNotSupported);
        }
 
        public override void WriteArray(string? prefix, string localName, string? namespaceUri, short[] array, int offset, int count)
        {
            throw new NotSupportedException(SR.JsonWriteArrayNotSupported);
        }
 
        public override void WriteArray(string? prefix, string localName, string? namespaceUri, int[] array, int offset, int count)
        {
            throw new NotSupportedException(SR.JsonWriteArrayNotSupported);
        }
 
        public override void WriteArray(string? prefix, string localName, string? namespaceUri, long[] array, int offset, int count)
        {
            throw new NotSupportedException(SR.JsonWriteArrayNotSupported);
        }
 
        public override void WriteArray(string? prefix, string localName, string? namespaceUri, float[] array, int offset, int count)
        {
            throw new NotSupportedException(SR.JsonWriteArrayNotSupported);
        }
 
        public override void WriteArray(string? prefix, string localName, string? namespaceUri, double[] array, int offset, int count)
        {
            throw new NotSupportedException(SR.JsonWriteArrayNotSupported);
        }
 
        public override void WriteArray(string? prefix, string localName, string? namespaceUri, decimal[] array, int offset, int count)
        {
            throw new NotSupportedException(SR.JsonWriteArrayNotSupported);
        }
 
        public override void WriteArray(string? prefix, string localName, string? namespaceUri, DateTime[] array, int offset, int count)
        {
            throw new NotSupportedException(SR.JsonWriteArrayNotSupported);
        }
 
        public override void WriteArray(string? prefix, string localName, string? namespaceUri, Guid[] array, int offset, int count)
        {
            throw new NotSupportedException(SR.JsonWriteArrayNotSupported);
        }
 
        public override void WriteArray(string? prefix, string localName, string? namespaceUri, TimeSpan[] array, int offset, int count)
        {
            throw new NotSupportedException(SR.JsonWriteArrayNotSupported);
        }
 
        public override void WriteArray(string? prefix, XmlDictionaryString localName, XmlDictionaryString? namespaceUri, bool[] array, int offset, int count)
        {
            throw new NotSupportedException(SR.JsonWriteArrayNotSupported);
        }
 
        public override void WriteArray(string? prefix, XmlDictionaryString localName, XmlDictionaryString? namespaceUri, decimal[] array, int offset, int count)
        {
            throw new NotSupportedException(SR.JsonWriteArrayNotSupported);
        }
 
        public override void WriteArray(string? prefix, XmlDictionaryString localName, XmlDictionaryString? namespaceUri, double[] array, int offset, int count)
        {
            throw new NotSupportedException(SR.JsonWriteArrayNotSupported);
        }
 
        public override void WriteArray(string? prefix, XmlDictionaryString localName, XmlDictionaryString? namespaceUri, float[] array, int offset, int count)
        {
            throw new NotSupportedException(SR.JsonWriteArrayNotSupported);
        }
 
        public override void WriteArray(string? prefix, XmlDictionaryString localName, XmlDictionaryString? namespaceUri, int[] array, int offset, int count)
        {
            throw new NotSupportedException(SR.JsonWriteArrayNotSupported);
        }
 
        public override void WriteArray(string? prefix, XmlDictionaryString localName, XmlDictionaryString? namespaceUri, long[] array, int offset, int count)
        {
            throw new NotSupportedException(SR.JsonWriteArrayNotSupported);
        }
 
        public override void WriteArray(string? prefix, XmlDictionaryString localName, XmlDictionaryString? namespaceUri, short[] array, int offset, int count)
        {
            throw new NotSupportedException(SR.JsonWriteArrayNotSupported);
        }
 
        public override void WriteArray(string? prefix, XmlDictionaryString localName, XmlDictionaryString? namespaceUri, DateTime[] array, int offset, int count)
        {
            throw new NotSupportedException(SR.JsonWriteArrayNotSupported);
        }
 
        public override void WriteArray(string? prefix, XmlDictionaryString localName, XmlDictionaryString? namespaceUri, Guid[] array, int offset, int count)
        {
            throw new NotSupportedException(SR.JsonWriteArrayNotSupported);
        }
 
        public override void WriteArray(string? prefix, XmlDictionaryString localName, XmlDictionaryString? namespaceUri, TimeSpan[] array, int offset, int count)
        {
            throw new NotSupportedException(SR.JsonWriteArrayNotSupported);
        }
 
        public override void WriteBase64(byte[] buffer, int index, int count)
        {
            ArgumentNullException.ThrowIfNull(buffer);
 
            // Not checking upper bound because it will be caught by "count".  This is what XmlTextWriter does.
            ArgumentOutOfRangeException.ThrowIfNegative(index);
            ArgumentOutOfRangeException.ThrowIfNegative(count);
            if (count > buffer.Length - index)
            {
                throw new ArgumentOutOfRangeException(nameof(count), SR.Format(SR.JsonSizeExceedsRemainingBufferSpace, buffer.Length - index));
            }
 
            StartText();
            _nodeWriter.WriteBase64Text(buffer, 0, buffer, index, count);
        }
 
        public override void WriteBinHex(byte[] buffer, int index, int count)
        {
            ArgumentNullException.ThrowIfNull(buffer);
 
            // Not checking upper bound because it will be caught by "count".  This is what XmlTextWriter does.
            ArgumentOutOfRangeException.ThrowIfNegative(index);
            ArgumentOutOfRangeException.ThrowIfNegative(count);
            if (count > buffer.Length - index)
            {
                throw new ArgumentOutOfRangeException(nameof(count), SR.Format(SR.JsonSizeExceedsRemainingBufferSpace, buffer.Length - index));
            }
 
            StartText();
            WriteEscapedJsonString(DataContractSerializer.BinHexEncoding.GetString(buffer, index, count));
        }
 
        public override void WriteCData(string? text)
        {
            WriteString(text);
        }
 
        public override void WriteCharEntity(char ch)
        {
            WriteString(ch.ToString());
        }
 
        public override void WriteChars(char[] buffer, int index, int count)
        {
            ArgumentNullException.ThrowIfNull(buffer);
 
            // Not checking upper bound because it will be caught by "count".  This is what XmlTextWriter does.
            ArgumentOutOfRangeException.ThrowIfNegative(index);
            ArgumentOutOfRangeException.ThrowIfNegative(count);
            if (count > buffer.Length - index)
            {
                throw new ArgumentOutOfRangeException(nameof(count), SR.Format(SR.JsonSizeExceedsRemainingBufferSpace, buffer.Length - index));
            }
 
            WriteString(new string(buffer, index, count));
        }
 
        public override void WriteComment(string? text)
        {
            throw new NotSupportedException(SR.Format(SR.JsonMethodNotSupported, "WriteComment"));
        }
 
        public override void WriteDocType(string name, string? pubid, string? sysid, string? subset)
        {
            throw new NotSupportedException(SR.Format(SR.JsonMethodNotSupported, "WriteDocType"));
        }
 
        public override void WriteEndAttribute()
        {
            if (IsClosed)
            {
                ThrowClosed();
            }
            if (!HasOpenAttribute)
            {
                throw new XmlException(SR.JsonNoMatchingStartAttribute);
            }
 
            Debug.Assert(!(_isWritingDataTypeAttribute && _isWritingServerTypeAttribute),
                "Can not write type attribute and __type attribute at the same time.");
 
            if (_isWritingDataTypeAttribute)
            {
                switch (_attributeText)
                {
                    case JsonGlobals.numberString:
                        {
                            ThrowIfServerTypeWritten(JsonGlobals.numberString);
                            _dataType = JsonDataType.Number;
                            break;
                        }
                    case JsonGlobals.stringString:
                        {
                            ThrowIfServerTypeWritten(JsonGlobals.stringString);
                            _dataType = JsonDataType.String;
                            break;
                        }
                    case JsonGlobals.arrayString:
                        {
                            ThrowIfServerTypeWritten(JsonGlobals.arrayString);
                            _dataType = JsonDataType.Array;
                            break;
                        }
                    case JsonGlobals.objectString:
                        {
                            _dataType = JsonDataType.Object;
                            break;
                        }
                    case JsonGlobals.nullString:
                        {
                            ThrowIfServerTypeWritten(JsonGlobals.nullString);
                            _dataType = JsonDataType.Null;
                            break;
                        }
                    case JsonGlobals.booleanString:
                        {
                            ThrowIfServerTypeWritten(JsonGlobals.booleanString);
                            _dataType = JsonDataType.Boolean;
                            break;
                        }
                    default:
                        throw new XmlException(SR.Format(SR.JsonUnexpectedAttributeValue, _attributeText));
                }
 
                _attributeText = null;
                _isWritingDataTypeAttribute = false;
 
                if (!IsWritingNameWithMapping || WrittenNameWithMapping)
                {
                    WriteDataTypeServerType();
                }
            }
            else if (_isWritingServerTypeAttribute)
            {
                _serverTypeValue = _attributeText;
                _attributeText = null;
                _isWritingServerTypeAttribute = false;
 
                // we are writing __type after type="object" (enforced by WSE)
                if ((!IsWritingNameWithMapping || WrittenNameWithMapping) && _dataType == JsonDataType.Object)
                {
                    WriteServerTypeAttribute();
                }
            }
            else if (IsWritingNameAttribute)
            {
                WriteJsonElementName(_attributeText!);
                _attributeText = null;
                _nameState = NameState.IsWritingNameWithMapping | NameState.WrittenNameWithMapping;
                WriteDataTypeServerType();
            }
            else if (_isWritingXmlnsAttribute)
            {
                if (!string.IsNullOrEmpty(_attributeText) && _isWritingXmlnsAttributeDefaultNs)
                {
                    throw new ArgumentException(SR.Format(SR.JsonNamespaceMustBeEmpty, _attributeText));
                }
 
                _attributeText = null;
                _isWritingXmlnsAttribute = false;
                _isWritingXmlnsAttributeDefaultNs = false;
            }
        }
 
        public override void WriteEndDocument()
        {
            if (IsClosed)
            {
                ThrowClosed();
            }
            if (_nodeType != JsonNodeType.None)
            {
                while (_depth > 0)
                {
                    WriteEndElement();
                }
            }
        }
 
        public override void WriteEndElement()
        {
            if (IsClosed)
            {
                ThrowClosed();
            }
 
            if (_depth == 0)
            {
                throw new XmlException(SR.JsonEndElementNoOpenNodes);
            }
            if (HasOpenAttribute)
            {
                throw new XmlException(SR.Format(SR.JsonOpenAttributeMustBeClosedFirst, "WriteEndElement"));
            }
 
            _endElementBuffer = false;
 
            JsonNodeType token = ExitScope();
            if (token == JsonNodeType.Collection)
            {
                _indentLevel--;
                if (_indent)
                {
                    if (_nodeType == JsonNodeType.Element)
                    {
                        _nodeWriter.WriteText(WHITESPACE);
                    }
                    else
                    {
                        WriteNewLine();
                        WriteIndent();
                    }
                }
                _nodeWriter.WriteText(JsonGlobals.EndCollectionChar);
                token = ExitScope();
            }
            else if (_nodeType == JsonNodeType.QuotedText)
            {
                // For writing "
                WriteJsonQuote();
            }
            else if (_nodeType == JsonNodeType.Element)
            {
                if ((_dataType == JsonDataType.None) && (_serverTypeValue != null))
                {
                    throw new XmlException(SR.Format(SR.JsonMustSpecifyDataType, JsonGlobals.typeString, JsonGlobals.objectString, JsonGlobals.serverTypeString));
                }
 
                if (IsWritingNameWithMapping && !WrittenNameWithMapping)
                {
                    // Ending </item> without writing item attribute
                    // Not providing a better error message because localization deadline has passed.
                    throw new XmlException(SR.Format(SR.JsonMustSpecifyDataType, JsonGlobals.itemString, string.Empty, JsonGlobals.itemString));
                }
 
                // the element is empty, it does not have any content,
                if ((_dataType == JsonDataType.None) ||
                    (_dataType == JsonDataType.String))
                {
                    _nodeWriter.WriteText(JsonGlobals.QuoteChar);
                    _nodeWriter.WriteText(JsonGlobals.QuoteChar);
                }
            }
            else
            {
                // Assert on only StandaloneText and EndElement because preceding if
                //    conditions take care of checking for QuotedText and Element.
                Debug.Assert((_nodeType == JsonNodeType.StandaloneText) || (_nodeType == JsonNodeType.EndElement),
                    "nodeType has invalid value " + _nodeType + ". Expected it to be QuotedText, Element, StandaloneText, or EndElement.");
            }
            if (_depth != 0)
            {
                if (token == JsonNodeType.Element)
                {
                    _endElementBuffer = true;
                }
                else if (token == JsonNodeType.Object)
                {
                    _indentLevel--;
                    if (_indent)
                    {
                        if (_nodeType == JsonNodeType.Element)
                        {
                            _nodeWriter.WriteText(WHITESPACE);
                        }
                        else
                        {
                            WriteNewLine();
                            WriteIndent();
                        }
                    }
                    _nodeWriter.WriteText(JsonGlobals.EndObjectChar);
                    if ((_depth > 0) && _scopes![_depth] == JsonNodeType.Element)
                    {
                        ExitScope();
                        _endElementBuffer = true;
                    }
                }
            }
 
            _dataType = JsonDataType.None;
            _nodeType = JsonNodeType.EndElement;
            _nameState = NameState.None;
            _wroteServerTypeAttribute = false;
        }
 
        public override void WriteEntityRef(string name)
        {
            throw new NotSupportedException(SR.Format(SR.JsonMethodNotSupported, "WriteEntityRef"));
        }
 
        public override void WriteFullEndElement()
        {
            WriteEndElement();
        }
 
        public override void WriteProcessingInstruction(string name, string? text)
        {
            if (IsClosed)
            {
                ThrowClosed();
            }
 
            if (!name.Equals("xml", StringComparison.OrdinalIgnoreCase))
            {
                throw new ArgumentException(SR.JsonXmlProcessingInstructionNotSupported, nameof(name));
            }
 
            if (WriteState != WriteState.Start)
            {
                throw new XmlException(SR.JsonXmlInvalidDeclaration);
            }
        }
 
        public override void WriteQualifiedName(string localName, string? ns)
        {
            ArgumentNullException.ThrowIfNull(localName);
 
            if (localName.Length == 0)
            {
                throw new ArgumentException(SR.JsonInvalidLocalNameEmpty, nameof(localName));
            }
 
            base.WriteQualifiedName(localName, ns ?? string.Empty);
        }
 
        public override void WriteRaw(string data)
        {
            WriteString(data);
        }
 
        public override void WriteRaw(char[] buffer, int index, int count)
        {
            ArgumentNullException.ThrowIfNull(buffer);
 
            // Not checking upper bound because it will be caught by "count".  This is what XmlTextWriter does.
            ArgumentOutOfRangeException.ThrowIfNegative(index);
            ArgumentOutOfRangeException.ThrowIfNegative(count);
            if (count > buffer.Length - index)
            {
                throw new ArgumentOutOfRangeException(nameof(count), SR.Format(SR.JsonSizeExceedsRemainingBufferSpace, buffer.Length - index));
            }
 
            WriteString(new string(buffer, index, count));
        }
 
        public override void WriteStartAttribute(string? prefix, string localName, string? ns)
        {
            if (IsClosed)
            {
                ThrowClosed();
            }
            if (!string.IsNullOrEmpty(prefix))
            {
                if (IsWritingNameWithMapping && prefix == JsonGlobals.xmlnsPrefix)
                {
                    if (ns != null && ns != xmlnsNamespace)
                    {
                        throw new ArgumentException(SR.Format(SR.XmlPrefixBoundToNamespace, "xmlns", xmlnsNamespace, ns), nameof(ns));
                    }
                }
                else
                {
                    throw new ArgumentException(SR.Format(SR.JsonPrefixMustBeNullOrEmpty, prefix), nameof(prefix));
                }
            }
            else
            {
                if (IsWritingNameWithMapping && ns == xmlnsNamespace && localName != JsonGlobals.xmlnsPrefix)
                {
                    prefix = JsonGlobals.xmlnsPrefix;
                }
            }
            if (!string.IsNullOrEmpty(ns))
            {
                if (IsWritingNameWithMapping && ns == xmlnsNamespace)
                {
                    prefix = JsonGlobals.xmlnsPrefix;
                }
                else if (string.IsNullOrEmpty(prefix) && localName == JsonGlobals.xmlnsPrefix && ns == xmlnsNamespace)
                {
                    prefix = JsonGlobals.xmlnsPrefix;
                    _isWritingXmlnsAttributeDefaultNs = true;
                }
                else
                {
                    throw new ArgumentException(SR.Format(SR.JsonNamespaceMustBeEmpty, ns), nameof(ns));
                }
            }
            ArgumentNullException.ThrowIfNull(localName);
            if (localName.Length == 0)
            {
                throw new ArgumentException(SR.JsonInvalidLocalNameEmpty, nameof(localName));
            }
            if ((_nodeType != JsonNodeType.Element) && !_wroteServerTypeAttribute)
            {
                throw new XmlException(SR.JsonAttributeMustHaveElement);
            }
            if (HasOpenAttribute)
            {
                throw new XmlException(SR.Format(SR.JsonOpenAttributeMustBeClosedFirst, "WriteStartAttribute"));
            }
            if (prefix == JsonGlobals.xmlnsPrefix)
            {
                _isWritingXmlnsAttribute = true;
            }
            else if (localName == JsonGlobals.typeString)
            {
                if (_dataType != JsonDataType.None)
                {
                    throw new XmlException(SR.Format(SR.JsonAttributeAlreadyWritten, JsonGlobals.typeString));
                }
 
                _isWritingDataTypeAttribute = true;
            }
            else if (localName == JsonGlobals.serverTypeString)
            {
                if (_serverTypeValue != null)
                {
                    throw new XmlException(SR.Format(SR.JsonAttributeAlreadyWritten, JsonGlobals.serverTypeString));
                }
 
                if ((_dataType != JsonDataType.None) && (_dataType != JsonDataType.Object))
                {
                    throw new XmlException(SR.Format(SR.JsonServerTypeSpecifiedForInvalidDataType,
                        JsonGlobals.serverTypeString, JsonGlobals.typeString, _dataType.ToString().ToLowerInvariant(), JsonGlobals.objectString));
                }
 
                _isWritingServerTypeAttribute = true;
            }
            else if (localName == JsonGlobals.itemString)
            {
                if (WrittenNameWithMapping)
                {
                    throw new XmlException(SR.Format(SR.JsonAttributeAlreadyWritten, JsonGlobals.itemString));
                }
 
                if (!IsWritingNameWithMapping)
                {
                    // Don't write attribute with local name "item" if <item> element is not open.
                    // Not providing a better error message because localization deadline has passed.
                    throw new XmlException(SR.JsonEndElementNoOpenNodes);
                }
 
                _nameState |= NameState.IsWritingNameAttribute;
            }
            else
            {
                throw new ArgumentException(SR.Format(SR.JsonUnexpectedAttributeLocalName, localName), nameof(localName));
            }
        }
 
        public override void WriteStartDocument(bool standalone)
        {
            // In XML, writes the XML declaration with the version "1.0" and the standalone attribute.
            WriteStartDocument();
        }
 
        public override void WriteStartDocument()
        {
            // In XML, writes the XML declaration with the version "1.0".
            if (IsClosed)
            {
                ThrowClosed();
            }
            if (WriteState != WriteState.Start)
            {
                throw new XmlException(SR.Format(SR.JsonInvalidWriteState, "WriteStartDocument", WriteState.ToString()));
            }
        }
 
        public override void WriteStartElement(string? prefix, string localName, string? ns)
        {
            ArgumentNullException.ThrowIfNull(localName);
 
            if (localName.Length == 0)
            {
                throw new ArgumentException(SR.JsonInvalidLocalNameEmpty, nameof(localName));
            }
            if (!string.IsNullOrEmpty(prefix))
            {
                if (string.IsNullOrEmpty(ns) || !TrySetWritingNameWithMapping(localName, ns))
                {
                    throw new ArgumentException(SR.Format(SR.JsonPrefixMustBeNullOrEmpty, prefix), nameof(prefix));
                }
            }
            if (!string.IsNullOrEmpty(ns))
            {
                if (!TrySetWritingNameWithMapping(localName, ns))
                {
                    throw new ArgumentException(SR.Format(SR.JsonNamespaceMustBeEmpty, ns), nameof(ns));
                }
            }
            if (IsClosed)
            {
                ThrowClosed();
            }
            if (HasOpenAttribute)
            {
                throw new XmlException(SR.Format(SR.JsonOpenAttributeMustBeClosedFirst, "WriteStartElement"));
            }
            if ((_nodeType != JsonNodeType.None) && _depth == 0)
            {
                throw new XmlException(SR.JsonMultipleRootElementsNotAllowedOnWriter);
            }
 
            switch (_nodeType)
            {
                case JsonNodeType.None:
                    {
                        if (!localName.Equals(JsonGlobals.rootString))
                        {
                            throw new XmlException(SR.Format(SR.JsonInvalidRootElementName, localName, JsonGlobals.rootString));
                        }
                        EnterScope(JsonNodeType.Element);
                        break;
                    }
                case JsonNodeType.Element:
                    {
                        if ((_dataType != JsonDataType.Array) && (_dataType != JsonDataType.Object))
                        {
                            throw new XmlException(SR.JsonNodeTypeArrayOrObjectNotSpecified);
                        }
                        if (_indent)
                        {
                            WriteNewLine();
                            WriteIndent();
                        }
                        if (!IsWritingCollection)
                        {
                            if (_nameState != NameState.IsWritingNameWithMapping)
                            {
                                WriteJsonElementName(localName);
                            }
                        }
                        else if (!localName.Equals(JsonGlobals.itemString))
                        {
                            throw new XmlException(SR.Format(SR.JsonInvalidItemNameForArrayElement, localName, JsonGlobals.itemString));
                        }
                        EnterScope(JsonNodeType.Element);
                        break;
                    }
                case JsonNodeType.EndElement:
                    {
                        if (_endElementBuffer)
                        {
                            _nodeWriter.WriteText(JsonGlobals.MemberSeparatorChar);
                        }
                        if (_indent)
                        {
                            WriteNewLine();
                            WriteIndent();
                        }
                        if (!IsWritingCollection)
                        {
                            if (_nameState != NameState.IsWritingNameWithMapping)
                            {
                                WriteJsonElementName(localName);
                            }
                        }
                        else if (!localName.Equals(JsonGlobals.itemString))
                        {
                            throw new XmlException(SR.Format(SR.JsonInvalidItemNameForArrayElement, localName, JsonGlobals.itemString));
                        }
                        EnterScope(JsonNodeType.Element);
                        break;
                    }
                default:
                    throw new XmlException(SR.JsonInvalidStartElementCall);
            }
 
            _isWritingDataTypeAttribute = false;
            _isWritingServerTypeAttribute = false;
            _isWritingXmlnsAttribute = false;
            _wroteServerTypeAttribute = false;
            _serverTypeValue = null;
            _dataType = JsonDataType.None;
            _nodeType = JsonNodeType.Element;
        }
 
        public override void WriteString(string? text)
        {
            if (HasOpenAttribute && (text != null))
            {
                _attributeText += text;
            }
            else
            {
                text ??= string.Empty;
 
                // do work only when not indenting whitespace
                if (!((_dataType == JsonDataType.Array || _dataType == JsonDataType.Object || _nodeType == JsonNodeType.EndElement) && XmlConverter.IsWhitespace(text)))
                {
                    StartText();
                    WriteEscapedJsonString(text);
                }
            }
        }
 
        public override void WriteSurrogateCharEntity(char lowChar, char highChar)
        {
            WriteString(new string([highChar, lowChar]));
        }
 
        public override void WriteValue(bool value)
        {
            StartText();
            _nodeWriter.WriteBoolText(value);
        }
 
        public override void WriteValue(decimal value)
        {
            StartText();
            _nodeWriter.WriteDecimalText(value);
        }
 
        public override void WriteValue(double value)
        {
            StartText();
            _nodeWriter.WriteDoubleText(value);
        }
 
        public override void WriteValue(float value)
        {
            StartText();
            _nodeWriter.WriteFloatText(value);
        }
 
        public override void WriteValue(int value)
        {
            StartText();
            _nodeWriter.WriteInt32Text(value);
        }
 
        public override void WriteValue(long value)
        {
            StartText();
            _nodeWriter.WriteInt64Text(value);
        }
 
        public override void WriteValue(Guid value)
        {
            StartText();
            _nodeWriter.WriteGuidText(value);
        }
 
        public override void WriteValue(DateTime value)
        {
            StartText();
            _nodeWriter.WriteDateTimeText(value);
        }
 
        public override void WriteValue(string? value)
        {
            WriteString(value);
        }
 
        public override void WriteValue(TimeSpan value)
        {
            StartText();
            _nodeWriter.WriteTimeSpanText(value);
        }
 
        public override void WriteValue(UniqueId value)
        {
            ArgumentNullException.ThrowIfNull(value);
 
            StartText();
            _nodeWriter.WriteUniqueIdText(value);
        }
 
        public override void WriteValue(object value)
        {
            ArgumentNullException.ThrowIfNull(value);
 
            if (IsClosed)
            {
                ThrowClosed();
            }
 
            if (value is Array)
            {
                WriteValue((Array)value);
            }
            else if (value is IStreamProvider)
            {
                WriteValue((IStreamProvider)value);
            }
            else
            {
                WritePrimitiveValue(value);
            }
        }
 
        public override void WriteWhitespace(string? ws)
        {
            if (IsClosed)
            {
                ThrowClosed();
            }
            ArgumentNullException.ThrowIfNull(ws);
 
            int pos = ws.AsSpan().IndexOfAnyExcept(" \t\r\n");
            if (pos >= 0)
            {
                throw new ArgumentException(SR.Format(SR.JsonOnlyWhitespace, ws[pos].ToString(), "WriteWhitespace"), nameof(ws));
            }
 
            WriteString(ws);
        }
 
        public override void WriteXmlAttribute(string localName, string? value)
        {
            throw new NotSupportedException(SR.Format(SR.JsonMethodNotSupported, "WriteXmlAttribute"));
        }
 
        public override void WriteXmlAttribute(XmlDictionaryString localName, XmlDictionaryString? value)
        {
            throw new NotSupportedException(SR.Format(SR.JsonMethodNotSupported, "WriteXmlAttribute"));
        }
 
        public override void WriteXmlnsAttribute(string? prefix, string namespaceUri)
        {
            if (!IsWritingNameWithMapping)
            {
                throw new NotSupportedException(SR.Format(SR.JsonMethodNotSupported, "WriteXmlnsAttribute"));
            }
        }
 
        public override void WriteXmlnsAttribute(string? prefix, XmlDictionaryString namespaceUri)
        {
            if (!IsWritingNameWithMapping)
            {
                throw new NotSupportedException(SR.Format(SR.JsonMethodNotSupported, "WriteXmlnsAttribute"));
            }
        }
 
        internal static bool CharacterNeedsEscaping(char ch)
        {
            return (ch == FORWARD_SLASH || ch == JsonGlobals.QuoteChar || ch < WHITESPACE || ch == BACK_SLASH
                || (ch >= HIGH_SURROGATE_START && (ch <= LOW_SURROGATE_END || ch >= MAX_CHAR)));
        }
 
 
        private static void ThrowClosed()
        {
            throw new InvalidOperationException(SR.JsonWriterClosed);
        }
 
        private void CheckText(JsonNodeType nextNodeType)
        {
            if (IsClosed)
            {
                ThrowClosed();
            }
            if (_depth == 0)
            {
                throw new InvalidOperationException(SR.XmlIllegalOutsideRoot);
            }
 
            if ((nextNodeType == JsonNodeType.StandaloneText) &&
                (_nodeType == JsonNodeType.QuotedText))
            {
                throw new XmlException(SR.JsonCannotWriteStandaloneTextAfterQuotedText);
            }
        }
 
        private void EnterScope(JsonNodeType currentNodeType)
        {
            _depth++;
            if (_scopes == null)
            {
                _scopes = new JsonNodeType[4];
            }
            else if (_scopes.Length == _depth)
            {
                JsonNodeType[] newScopes = new JsonNodeType[_depth * 2];
                Array.Copy(_scopes, newScopes, _depth);
                _scopes = newScopes;
            }
            _scopes[_depth] = currentNodeType;
        }
 
        private JsonNodeType ExitScope()
        {
            JsonNodeType nodeTypeToReturn = _scopes![_depth];
            _scopes[_depth] = JsonNodeType.None;
            _depth--;
            return nodeTypeToReturn;
        }
 
        private void InitializeWriter()
        {
            _nodeType = JsonNodeType.None;
            _dataType = JsonDataType.None;
            _isWritingDataTypeAttribute = false;
            _wroteServerTypeAttribute = false;
            _isWritingServerTypeAttribute = false;
            _serverTypeValue = null;
            _attributeText = null;
 
            if (_depth != 0)
            {
                _depth = 0;
            }
            if ((_scopes != null) && (_scopes.Length > JsonGlobals.maxScopeSize))
            {
                _scopes = null;
            }
 
            // Can't let writeState be at Closed if reinitializing.
            _writeState = WriteState.Start;
            _endElementBuffer = false;
            _indentLevel = 0;
        }
 
        private static bool IsUnicodeNewlineCharacter(char c)
        {
            // Newline characters in JSON strings need to be encoded on the way out (DevDiv #665974)
            // See Unicode 6.2, Table 5-1 (http://www.unicode.org/versions/Unicode6.2.0/ch05.pdf]) for the full list.
 
            // We only care about NEL, LS, and PS, since the other newline characters are all
            // control characters so are already encoded.
            return (c == '\u0085' || c == '\u2028' || c == '\u2029');
        }
 
        private void StartText()
        {
            if (HasOpenAttribute)
            {
                throw new InvalidOperationException(SR.JsonMustUseWriteStringForWritingAttributeValues);
            }
 
            if ((_dataType == JsonDataType.None) && (_serverTypeValue != null))
            {
                throw new XmlException(SR.Format(SR.JsonMustSpecifyDataType, JsonGlobals.typeString, JsonGlobals.objectString, JsonGlobals.serverTypeString));
            }
 
            if (IsWritingNameWithMapping && !WrittenNameWithMapping)
            {
                // Don't write out any text content unless the local name has been written.
                // Not providing a better error message because localization deadline has passed.
                throw new XmlException(SR.Format(SR.JsonMustSpecifyDataType, JsonGlobals.itemString, string.Empty, JsonGlobals.itemString));
            }
 
            if ((_dataType == JsonDataType.String) ||
                (_dataType == JsonDataType.None))
            {
                CheckText(JsonNodeType.QuotedText);
                if (_nodeType != JsonNodeType.QuotedText)
                {
                    WriteJsonQuote();
                }
                _nodeType = JsonNodeType.QuotedText;
            }
            else if ((_dataType == JsonDataType.Number) ||
                (_dataType == JsonDataType.Boolean))
            {
                CheckText(JsonNodeType.StandaloneText);
                _nodeType = JsonNodeType.StandaloneText;
            }
            else
            {
                ThrowInvalidAttributeContent();
            }
        }
 
        private void ThrowIfServerTypeWritten(string dataTypeSpecified)
        {
            if (_serverTypeValue != null)
            {
                throw new XmlException(SR.Format(SR.JsonInvalidDataTypeSpecifiedForServerType, JsonGlobals.typeString, dataTypeSpecified, JsonGlobals.serverTypeString, JsonGlobals.objectString));
            }
        }
 
        private void ThrowInvalidAttributeContent()
        {
            if (HasOpenAttribute)
            {
                throw new XmlException(SR.JsonInvalidMethodBetweenStartEndAttribute);
            }
            else
            {
                throw new XmlException(SR.Format(SR.JsonCannotWriteTextAfterNonTextAttribute, _dataType.ToString().ToLowerInvariant()));
            }
        }
 
        private bool TrySetWritingNameWithMapping(string localName, string ns)
        {
            if (localName.Equals(JsonGlobals.itemString) && ns.Equals(JsonGlobals.itemString))
            {
                _nameState = NameState.IsWritingNameWithMapping;
                return true;
            }
            return false;
        }
 
        private void WriteDataTypeServerType()
        {
            if (_dataType != JsonDataType.None)
            {
                switch (_dataType)
                {
                    case JsonDataType.Array:
                        {
                            EnterScope(JsonNodeType.Collection);
                            _nodeWriter.WriteText(JsonGlobals.CollectionChar);
                            _indentLevel++;
                            break;
                        }
                    case JsonDataType.Object:
                        {
                            EnterScope(JsonNodeType.Object);
                            _nodeWriter.WriteText(JsonGlobals.ObjectChar);
                            _indentLevel++;
                            break;
                        }
                    case JsonDataType.Null:
                        {
                            _nodeWriter.WriteText(JsonGlobals.nullString);
                            break;
                        }
                    default:
                        break;
                }
 
                if (_serverTypeValue != null)
                {
                    // dataType must be object because we throw in all other case.
                    WriteServerTypeAttribute();
                }
            }
        }
 
        private unsafe void WriteEscapedJsonString(string str)
        {
            fixed (char* chars = str)
            {
                int i = 0;
                int j;
                for (j = 0; j < str.Length; j++)
                {
                    char ch = chars[j];
                    if (ch <= FORWARD_SLASH)
                    {
                        if (ch == FORWARD_SLASH || ch == JsonGlobals.QuoteChar)
                        {
                            _nodeWriter.WriteChars(chars + i, j - i);
                            _nodeWriter.WriteText(BACK_SLASH);
                            _nodeWriter.WriteText(ch);
                            i = j + 1;
                        }
                        else if (ch < WHITESPACE)
                        {
                            _nodeWriter.WriteChars(chars + i, j - i);
                            _nodeWriter.WriteText(s_escapedJsonStringTable[ch]);
                            i = j + 1;
                        }
                    }
                    else if (ch == BACK_SLASH)
                    {
                        _nodeWriter.WriteChars(chars + i, j - i);
                        _nodeWriter.WriteText(BACK_SLASH);
                        _nodeWriter.WriteText(ch);
                        i = j + 1;
                    }
                    else if ((ch >= HIGH_SURROGATE_START && (ch <= LOW_SURROGATE_END || ch >= MAX_CHAR)) || IsUnicodeNewlineCharacter(ch))
                    {
                        _nodeWriter.WriteChars(chars + i, j - i);
                        _nodeWriter.WriteText(BACK_SLASH);
                        _nodeWriter.WriteText('u');
                        _nodeWriter.WriteText($"{(int)ch:x4}");
                        i = j + 1;
                    }
                }
                if (i < j)
                {
                    _nodeWriter.WriteChars(chars + i, j - i);
                }
            }
        }
 
        private void WriteIndent()
        {
            for (int i = 0; i < _indentLevel; i++)
            {
                _nodeWriter.WriteText(_indentChars!);
            }
        }
 
        private void WriteNewLine()
        {
            _nodeWriter.WriteText(CARRIAGE_RETURN);
            _nodeWriter.WriteText(NEWLINE);
        }
 
        private void WriteJsonElementName(string localName)
        {
            WriteJsonQuote();
            WriteEscapedJsonString(localName);
            WriteJsonQuote();
            _nodeWriter.WriteText(JsonGlobals.NameValueSeparatorChar);
            if (_indent)
            {
                _nodeWriter.WriteText(WHITESPACE);
            }
        }
 
        private void WriteJsonQuote()
        {
            _nodeWriter.WriteText(JsonGlobals.QuoteChar);
        }
 
        private void WritePrimitiveValue(object value)
        {
            ArgumentNullException.ThrowIfNull(value);
 
            if (IsClosed)
            {
                ThrowClosed();
            }
 
            if (value is ulong)
            {
                WriteValue((ulong)value);
            }
            else if (value is string)
            {
                WriteValue((string)value);
            }
            else if (value is int)
            {
                WriteValue((int)value);
            }
            else if (value is long)
            {
                WriteValue((long)value);
            }
            else if (value is bool)
            {
                WriteValue((bool)value);
            }
            else if (value is double)
            {
                WriteValue((double)value);
            }
            else if (value is DateTime)
            {
                WriteValue((DateTime)value);
            }
            else if (value is float)
            {
                WriteValue((float)value);
            }
            else if (value is decimal)
            {
                WriteValue((decimal)value);
            }
            else if (value is XmlDictionaryString)
            {
                WriteValue((XmlDictionaryString)value);
            }
            else if (value is UniqueId)
            {
                WriteValue((UniqueId)value);
            }
            else if (value is Guid)
            {
                WriteValue((Guid)value);
            }
            else if (value is TimeSpan)
            {
                WriteValue((TimeSpan)value);
            }
            else if (value.GetType().IsArray)
            {
                throw new ArgumentException(SR.JsonNestedArraysNotSupported, nameof(value));
            }
            else
            {
                base.WriteValue(value);
            }
        }
 
        private void WriteServerTypeAttribute()
        {
            string? value = _serverTypeValue;
            JsonDataType oldDataType = _dataType;
            NameState oldNameState = _nameState;
            WriteStartElement(JsonGlobals.serverTypeString);
            WriteValue(value);
            WriteEndElement();
            _dataType = oldDataType;
            _nameState = oldNameState;
            _wroteServerTypeAttribute = true;
        }
 
        private void WriteValue(ulong value)
        {
            StartText();
            _nodeWriter.WriteUInt64Text(value);
        }
 
        private void WriteValue(Array array)
        {
            // This method is called only if WriteValue(object) is called with an array
            // The contract for XmlWriter.WriteValue(object) requires that this object array be written out as a string.
            // E.g. WriteValue(new int[] { 1, 2, 3}) should be equivalent to WriteString("1 2 3").
            JsonDataType oldDataType = _dataType;
            // Set attribute mode to String because WritePrimitiveValue might write numerical text.
            //  Calls to methods that write numbers can't be mixed with calls that write quoted text unless the attribute mode is explicitly string.
            _dataType = JsonDataType.String;
            StartText();
            for (int i = 0; i < array.Length; i++)
            {
                if (i != 0)
                {
                    _nodeWriter.WriteText(JsonGlobals.WhitespaceChar);
                }
                WritePrimitiveValue(array.GetValue(i)!);
            }
            _dataType = oldDataType;
        }
 
        private sealed class JsonNodeWriter : XmlUTF8NodeWriter
        {
            internal unsafe void WriteChars(char* chars, int charCount)
            {
                base.UnsafeWriteUTF8Chars(chars, charCount);
            }
        }
    }
}