File: System\IO\Packaging\XmlCompatibilityReader.cs
Web Access
Project: src\src\libraries\System.IO.Packaging\src\System.IO.Packaging.csproj (System.IO.Packaging)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Threading;
using System.Xml;
 
namespace System.IO.Packaging
{
    // <returns>
    // true if xmlNamespace is recognized
    // </returns>
    // <param name=xmlNamespace>
    // the namespace to be checked
    // </param>
    // <param name=newXmlNamespace>
    // if the passed in namespace is subsumed, then newXmlNamespace returns the subsuming namespace.
    // </param>
    internal delegate bool IsXmlNamespaceSupportedCallback(string xmlNamespace, out string newXmlNamespace);
    internal delegate void HandleElementCallback(int elementDepth, ref bool more);
    internal delegate void HandleAttributeCallback(int elementDepth);
 
    internal sealed class XmlCompatibilityReader : XmlWrappingReader
    {
        #region Construction
        public XmlCompatibilityReader(XmlReader baseReader)
            : base(baseReader)
        {
            _compatibilityScope = new CompatibilityScope(null, -1, this);
 
            foreach (string xmlNamespace in s_predefinedNamespaces)
            {
                AddKnownNamespace(xmlNamespace);
                _namespaceMap[xmlNamespace] = xmlNamespace;
                Reader.NameTable.Add(xmlNamespace);
            }
 
            _elementHandler.Add(AlternateContent, new HandleElementCallback(HandleAlternateContent));
            _elementHandler.Add(Choice, new HandleElementCallback(HandleChoice));
            _elementHandler.Add(Fallback, new HandleElementCallback(HandleFallback));
 
            _attributeHandler.Add(Ignorable, new HandleAttributeCallback(HandleIgnorable));
            _attributeHandler.Add(MustUnderstand, new HandleAttributeCallback(HandleMustUnderstand));
            _attributeHandler.Add(ProcessContent, new HandleAttributeCallback(HandleProcessContent));
            _attributeHandler.Add(PreserveElements, new HandleAttributeCallback(HandlePreserveElements));
            _attributeHandler.Add(PreserveAttributes, new HandleAttributeCallback(HandlePreserveAttributes));
        }
 
        public XmlCompatibilityReader(XmlReader baseReader,
            IsXmlNamespaceSupportedCallback? isXmlNamespaceSupported)
            : this(baseReader)
        {
            _namespaceCallback = isXmlNamespaceSupported;
        }
 
        public XmlCompatibilityReader(XmlReader baseReader,
            IsXmlNamespaceSupportedCallback? isXmlNamespaceSupported,
            IEnumerable<string> supportedNamespaces)
            : this(baseReader, isXmlNamespaceSupported)
        {
            foreach (string xmlNamespace in supportedNamespaces)
            {
                AddKnownNamespace(xmlNamespace);
                _namespaceMap[xmlNamespace] = xmlNamespace;
            }
        }
 
#if !PBTCOMPILER
        public XmlCompatibilityReader(XmlReader baseReader,
            IEnumerable<string> supportedNamespaces)
            : this(baseReader, null, supportedNamespaces)
        {
        }
#endif
        #endregion Construction
 
        #region Public Methods
        /// <summary>
        /// replaces all future references of namespace URI 'oldNamespace' with 'newNamespace'
        /// </summary>
        /// <param name="newNamespace">
        /// the namespace to subsume with
        /// </param>
        /// <param name="oldNamespace">
        /// the namespace to be subsumed
        /// </param>
        public void DeclareNamespaceCompatibility(string newNamespace, string oldNamespace)
        {
            if (newNamespace != oldNamespace)
            {
                // indicate that newNamespace subsumes another namespace
                AddSubsumingNamespace(newNamespace);
 
                // If newNamespace is mapped to a namespace,
                if (_namespaceMap.TryGetValue(newNamespace, out string? tempNamespace))
                {
                    // If we have mapped newNamespace already get the newest name.
                    // We don't have to do this recursively because of the code below
                    // ensures the map always refers to the newest namespace.
                    newNamespace = tempNamespace;
                }
 
                if (IsSubsumingNamespace(oldNamespace))
                {
                    // if we are mapping what was used as a new namespace to a newer name,
                    // scan the _newNamespaces dictionary and update the entries. We collect
                    // a list to avoid updating the dictionary during enumeration.
                    List<string> keysToUpdate = new List<string>();
 
                    foreach (KeyValuePair<string, string> pair in _namespaceMap)
                    {
                        if (pair.Value == oldNamespace)
                        {
                            keysToUpdate.Add(pair.Key);
                        }
                    }
 
                    foreach (string key in keysToUpdate)
                    {
                        _namespaceMap[key] = newNamespace;
                    }
                }
            }
 
            _namespaceMap[oldNamespace] = newNamespace;
        }
 
        /// <summary>
        /// Reads the next node from the stream.
        /// </summary>
        /// <returns>
        /// true if the next node was read successfully; false if there are no more nodes to read.
        /// </returns>
        public override bool Read()
        {
            // Previous element was an empty element. So if we pushed a scope, then get rid of the scope first.
            if (_isPreviousElementEmpty)
            {
                _isPreviousElementEmpty = false;
                ScanForEndCompatibility(_previousElementDepth);
            }
 
            bool more = Reader.Read(); //passed as ref arg to ReadStartElement and ReadEndElement
            bool result = false;
 
            while (more)
            {
                switch (Reader.NodeType)
                {
                    case XmlNodeType.Element:
                        {
                            // if the element read should be ignored, read the next element
                            if (!ReadStartElement(ref more))
                            {
                                continue;
                            }
                            break;
                        }
                    case XmlNodeType.EndElement:
                        {
                            // if the element read should be ignored, read the next element
                            if (!ReadEndElement(ref more))
                            {
                                continue;
                            }
                            break;
                        }
                }
 
                // if the element was read successfully and was not ignored, break and return true
                result = true;
                break;
            }
 
            return result;
        }
 
        /// <summary>
        /// Used to handle 'start element' tags.  These are actually
        /// just called 'element' tags, the 'start' is just for clarity
        /// </summary>
        /// <param name="more">
        /// is set to true if there is the document contains more elements, false if the end of the
        /// document has been reached.
        /// </param>
        /// <returns>
        /// true if an element was read that should not be ignored
        /// false if the element read should be ignored or the end of document has been reached
        /// </returns>
        private bool ReadStartElement(ref bool more)
        {
            // when processing elements, the Reader may advance to another element or attribute,
            // so we save the values of the current element here
            int elementDepth = Reader.Depth;
            int depthOffset = _depthOffset;
            bool isEmpty = Reader.IsEmptyElement;
            string namespaceName = NamespaceURI;
            bool result = false;
 
            if (object.ReferenceEquals(namespaceName, CompatibilityUri))
            {
                // if the element is a markup-compatibility element, we get the appropriate handler for
                // the element type, and call the appropriate delegate.  If the element is not recognized
                // we throw an exception.
                string elementName = Reader.LocalName;
                HandleElementCallback? elementCB;
                if (!_elementHandler.TryGetValue(elementName, out elementCB))
                {
                    Error(SR.XCRUnknownCompatElement, elementName);
                }
                elementCB(elementDepth, ref more);
            }
            // handle non-markup-compatibility elements
            else
            {
                // check for markup-compatibility attributes and namespaces that should be ignored
                ScanForCompatibility(elementDepth);
 
                if (ShouldIgnoreNamespace(namespaceName))
                {
                    if (Scope.ShouldProcessContent(namespaceName, Reader.LocalName))
                    {
                        // if the current element is unknown and has been marked Ignorable and ProcessContent,
                        // then read the next element, and increase depth offset
                        if (Scope.Depth == elementDepth)
                        {
                            // if the current element pushed a scope, mark the scope as InProcessContent to
                            // note that for certain logic this scope's parent should be checked
                            Scope.InProcessContent = true;
                        }
                        _depthOffset++;
                        more = Reader.Read();
                    }
                    else
                    {
                        // if element should be ignored but not processed, check to see if scope must be popped,
                        // then skip to the next element after the end tag of the current element
                        ScanForEndCompatibility(elementDepth);
                        Reader.Skip();
                    }
                }
                else
                {
                    if (Scope.InAlternateContent)
                    {
                        // if this element is the child of an AlternateContent element, then throw an exception.
                        Error(SR.XCRInvalidACChild, Reader.Name);
                    }
 
                    result = true;
                }
            }
 
            // if the element is empty (e.g. "<a ... />" and we pushed a scope then we need to set a flag
            // to get rid of the scope when we hit the next element.
            // We also need to store the current elementDepth.
            if (isEmpty)
            {
                _isPreviousElementEmpty = true;
                _previousElementDepth = elementDepth;
                _depthOffset = depthOffset;
            }
 
            return result;
        }
 
        /// <summary>
        /// Used to handle any end element tag
        /// </summary>
        /// <param name="more">
        /// is set to true if there is the document contains more elements, false if the end of the
        /// document has been reached.
        /// </param>
        /// <returns>
        /// true if an element was read that should not be ignored
        /// false if the element read should be ignored or the end of document has been reached
        /// </returns>
        private bool ReadEndElement(ref bool more)
        {
            // when reading attributes, the reader's depth increases, so for consistency
            // we store the depth before reading any attributes
            int elementDepth = Reader.Depth;
            string namespaceName = NamespaceURI;
            bool result = false;  // return value
 
            if (object.ReferenceEquals(namespaceName, CompatibilityUri))
            {
                // if the element is a markup-compatibility element, pop a scope, decrement the
                // depth offset and read the next element.
                string elementName = Reader.LocalName;
                if (object.ReferenceEquals(elementName, AlternateContent))
                {
                    if (!Scope.ChoiceSeen)
                    {
                        // if the current element was a </mc:AlternateContent>, without any Choice
                        // element children, throw an exception
                        Error(SR.XCRChoiceNotFound);
                    }
                }
                _depthOffset--;
                PopScope();  //we know we can pop, so no need to scan
                more = Reader.Read();
            }
            else
            {
                if (ShouldIgnoreNamespace(namespaceName))
                {
                    // if current element is Ignorable, then to be on it, it must have been marked
                    // ProcessContent.  Pop a scope if the corresponding start element pushed a scope a
                    // scope, decrement the depth offset and read the next element.
                    Debug.Assert(Scope.ShouldProcessContent(namespaceName, Reader.LocalName));
                    ScanForEndCompatibility(elementDepth);
                    _depthOffset--;
                    more = Reader.Read();
                }
                else
                {
                    ScanForEndCompatibility(elementDepth);
                    result = true;
                }
            }
 
            return result;
        }
 
        /// <summary>
        /// Gets the value of the attribute with the specified index.
        /// </summary>
        /// <param name="i">
        /// The index of the attribute. The index is zero-based. (The first attribute has index 0.)
        /// </param>
        /// <returns>
        /// The value of the specified attribute. If the attribute is not found, a null reference is returned.
        /// </returns>
        public override string GetAttribute(int i)
        {
            string? result;
 
            if (_ignoredAttributeCount == 0)
            {
                // if the current element should not ignored any of its attributes, skip extra logic
                result = Reader.GetAttribute(i);
            }
            else
            {
                SaveReaderPosition();
 
                // move to 'i'th attribute, get its value
                MoveToAttribute(i);
                result = Reader.Value;
 
                RestoreReaderPosition();
            }
 
            return result;
        }
 
        /// <summary>
        /// Gets the value of the attribute with the specified name.
        /// </summary>
        /// <param name="name">
        /// The qualified name of the attribute.
        /// </param>
        /// <returns>
        /// The value of the specified attribute. If the attribute is not found, a null reference is returned.
        /// </returns>
        public override string? GetAttribute(string name)
        {
            string? result = null;
 
            if (_ignoredAttributeCount == 0)
            {
                // if the current element should not ignored any attributes, call Reader method
                result = Reader.GetAttribute(name);
            }
            else
            {
                SaveReaderPosition();
 
                // move to "name" attribute
                if (MoveToAttribute(name))
                {
                    result = Reader.Value;
                    RestoreReaderPosition();
                }
            }
 
            return result;
        }
 
        /// <summary>
        /// Gets the value of the attribute with the specified local name and namespace URI.
        /// </summary>
        /// <param name="localName">
        /// The local name of the attribute.
        /// </param>
        /// <param name="namespaceURI">
        /// The namespace URI of the attribute.
        /// </param>
        /// <returns>
        /// The value of the specified attribute. If the attribute is not found, a null reference is returned.
        /// </returns>
        public override string? GetAttribute(string localName, string? namespaceURI)
        {
            string? result = null;
 
            if (_ignoredAttributeCount == 0 || !ShouldIgnoreNamespace(namespaceURI!))
            {
                // if the current element does not have any attributes that should be ignored or
                // the namespace provided is not ignorable, call Reader method
                result = Reader.GetAttribute(localName, namespaceURI);
            }
 
            return result;
        }
 
        /// <summary>
        /// Gets the value of the attribute with the specified index.
        /// </summary>
        /// <param name="i">
        /// The index of the attribute. The index is zero-based. (The first attribute has index 0.)
        /// </param>
        public override void MoveToAttribute(int i)
        {
            if (_ignoredAttributeCount == 0)
            {
                // if the current element should not ignored any attributes, call Reader method
                Reader.MoveToAttribute(i);
            }
            else if (i < 0 || i >= AttributeCount)
            {
                throw new ArgumentOutOfRangeException(nameof(i));
            }
            else
            {
                // move Reader to first attribute and iterate until 'i'th element found
                Reader.MoveToFirstAttribute();
 
                while (true)
                {
                    if (!ShouldIgnoreNamespace(NamespaceURI))
                    {
                        // if attribute should not be ignored, decrement 'i', if i == 0 we've found element
                        if (i-- == 0)
                        {
                            break;
                        }
                    }
 
                    Reader.MoveToNextAttribute();
                }
            }
        }
 
        /// <summary>
        /// Moves to the attribute with the specified name.
        /// </summary>
        /// <param name="name">
        /// The qualified name of the attribute.
        /// </param>
        /// <returns>
        /// true if the attribute is found; otherwise, false. If false, the reader's position does not change.
        /// </returns>
        public override bool MoveToAttribute(string name)
        {
            bool result;
 
            if (_ignoredAttributeCount == 0)
            {
                // if the current element should not ignored any attributes, call Reader method
                result = Reader.MoveToAttribute(name);
            }
            else
            {
                SaveReaderPosition();
 
                result = Reader.MoveToAttribute(name);
                if (result && ShouldIgnoreNamespace(NamespaceURI))
                {
                    // if attribute should be ignored, return false and restore state
                    result = false;
                    RestoreReaderPosition();
                }
            }
 
            return result;
        }
 
        /// <summary>
        /// Moves to the attribute with the specified local name and namespace URI.
        /// </summary>
        /// <param name="localName">
        /// The local name of the attribute.
        /// </param>
        /// <param name="namespaceURI">
        /// The namespace URI of the attribute.
        /// </param>
        /// <returns>
        /// true if the attribute is found; otherwise, false. If false, the reader's position does not change.
        /// </returns>
        public override bool MoveToAttribute(string localName, string? namespaceURI)
        {
            bool result;
 
            if (_ignoredAttributeCount == 0)
            {
                // if the current element should not ignored any attributes, call Reader method
                result = Reader.MoveToAttribute(localName, namespaceURI);
            }
            else
            {
                SaveReaderPosition();
 
                result = Reader.MoveToAttribute(localName, namespaceURI);
 
                if (result && ShouldIgnoreNamespace(namespaceURI!))
                {
                    result = false;
                    RestoreReaderPosition();
                }
            }
            return result;
        }
 
        /// <summary>
        /// Moves to the first attribute.
        /// </summary>
        /// <returns>
        /// true if an attribute exists (the reader moves to the first attribute);
        /// otherwise, false (the position of the reader does not change).
        /// </returns>
        public override bool MoveToFirstAttribute()
        {
            bool result = HasAttributes;
 
            if (result)
            {
                MoveToAttribute(0);
            }
 
            return result;
        }
 
        /// <summary>
        /// Moves to the next attribute.
        /// </summary>
        /// <returns>
        /// true if there is a next attribute; false if there are no more attributes.
        /// </returns>
        public override bool MoveToNextAttribute()
        {
            bool result;
 
            if (_ignoredAttributeCount == 0)
            {
                // if the current element should not ignored any attributes, call Reader method
                result = Reader.MoveToNextAttribute();
            }
            else
            {
                SaveReaderPosition();
 
                result = Reader.MoveToNextAttribute();
 
                if (result)
                {
                    result = SkipToKnownAttribute();
 
                    if (!result)
                    {
                        // if no more attributes exist that should not be ignored, return false and restore state
                        RestoreReaderPosition();
                    }
                }
            }
 
            return result;
        }
 
        /// <summary>
        /// Resolves a namespace prefix in the current element's scope.
        /// </summary>
        /// <param name="prefix">
        /// The prefix whose namespace URI you want to resolve. To match the default namespace,
        /// pass an empty string. This string does not have to be atomized.
        /// </param>
        /// <returns>
        /// The namespace URI to which the prefix maps or a null reference if no matching prefix is found.
        /// </returns>
        public override string? LookupNamespace(string prefix)
        {
            string? namespaceName = Reader.LookupNamespace(prefix);
 
            if (namespaceName != null)
            {
                namespaceName = GetMappedNamespace(namespaceName);
            }
 
            return namespaceName;
        }
 
        #endregion Public Methods
 
        #region Public Properties
 
        /// <summary>
        /// This override is to ensure that the value
        /// for the xmlns attribute reflects all the
        /// compatibility (subsuming) rules.
        /// </summary>
#pragma warning disable CS8764 // Nullability of return type doesn't match overridden member
        public override string? Value
        {
            get
            {
                // Look for xmlns
                if (string.Equals(XmlnsDeclaration, Reader.LocalName, StringComparison.Ordinal))
                {
                    return LookupNamespace(string.Empty);
                }
                // Look for xmlns: ...
                else if (string.Equals(XmlnsDeclaration, Reader.Prefix, StringComparison.Ordinal))
                {
                    return LookupNamespace(Reader.LocalName);
                }
 
                return Reader.Value;
            }
        }
#pragma warning restore CS8764
 
        /// <summary>
        /// Gets the namespace URI (as defined in the W3C Namespace specification) of the node
        /// on which the reader is positioned.
        /// </summary>
        public override string NamespaceURI
        {
            get
            {
                return GetMappedNamespace(Reader.NamespaceURI);
            }
        }
 
        /// <summary>
        /// Gets the depth of the current node in the XML document.
        /// </summary>
        public override int Depth
        {
            get
            {
                return Reader.Depth - _depthOffset;
            }
        }
 
        /// <summary>
        /// Gets a value indicating whether the current node has any attributes
        /// </summary>
        public override bool HasAttributes
        {
            get
            {
                return AttributeCount != 0;
            }
        }
 
        /// <summary>
        /// Gets the number of attributes on the current node.
        /// </summary>
        public override int AttributeCount
        {
            get
            {
                return Reader.AttributeCount - _ignoredAttributeCount;
            }
        }
 
        #endregion Public Properties
 
        #region Private Methods
 
        private void SaveReaderPosition()
        {
            // Save current state so we can go back to the same spot if this fails
            _inAttribute = (Reader.NodeType == XmlNodeType.Attribute);
            _currentName = Reader.Name;
        }
 
        private void RestoreReaderPosition()
        {
            // Restore reader state from SaveReaderPosition
            if (_inAttribute)
            {
                Reader.MoveToAttribute(_currentName!);
            }
            else
            {
                Reader.MoveToElement();
            }
        }
 
        /// <summary>
        /// Retrieves the correctly mapped namespace from the namespace provided
        /// </summary>
        /// <param name="namespaceName">
        /// The name of the namespace to retrieve the mapping of
        /// </param>
        /// <returns>
        /// The name of the mapped namespace.
        /// </returns>
        private string GetMappedNamespace(string namespaceName)
        {
            string? mappedNamespace;
 
            // if the namespace is not null, get the mapped namespace (which may be itself)
            if (!_namespaceMap.TryGetValue(namespaceName, out mappedNamespace))
            {
                // if the namespace has not yet been mapped, map it
                mappedNamespace = MapNewNamespace(namespaceName);
            }
            else
            {
                // if the mapped namespace is null, then the namespace was not supported, just return
                // the given namespace
                mappedNamespace ??= namespaceName;
            }
 
            return mappedNamespace;
        }
 
        /// <summary>
        /// Adds the namespace to the namespace map.  The default is to map the namespace to itself.
        /// The namespace is mapped to the value returned by the callback, if a callback exists and the
        /// callback returns a subsuming namespace.
        /// </summary>
        /// <param name="namespaceName">
        /// The name of the namespace to be mapped.
        /// </param>
        /// <returns>
        /// The name of the mapped namespace.
        /// </returns>
        private string MapNewNamespace(string namespaceName)
        {
            if (_namespaceCallback != null)
            {
                string mappedNamespace;
 
                // the callback returns whether the namespace is supported, and mappedNamespace is the
                // namespace subsuming the namespace passed in.
                bool isSupported = _namespaceCallback(namespaceName, out mappedNamespace);
 
                if (isSupported)
                {
                    AddKnownNamespace(namespaceName);
 
                    if (string.IsNullOrEmpty(mappedNamespace) || namespaceName == mappedNamespace)
                    {
                        _namespaceMap[namespaceName] = namespaceName;
                    }
                    else
                    {
                        // subsume namespace with mappedNamespace.
                        string? tempNamespace;
 
                        if (!_namespaceMap.TryGetValue(mappedNamespace, out tempNamespace))
                        {
                            // If the namespace is known, but doesn't have a map, that means we're
                            // already in the process of calling MapNewNamespace on it, i.e. we have
                            // a cycle
                            if (IsNamespaceKnown(mappedNamespace))
                            {
                                Error(SR.XCRCompatCycle, mappedNamespace);
                            }
 
                            // mappedNamespace has not been mapped, so map it
                            tempNamespace = MapNewNamespace(mappedNamespace);
                        }
 
                        DeclareNamespaceCompatibility(tempNamespace, namespaceName);
                        namespaceName = tempNamespace;
                    }
                }
                else
                {
                    // if the namespace is not supported, we enter null into the namespaceMap as a placeholder
                    // so that we do not call the callback again on this namespace.
                    _namespaceMap[namespaceName] = null!;
                }
            }
 
            return namespaceName;
        }
 
        /// <summary>
        /// Used to determine whether a given namespace subsumes another namespace
        /// </summary>
        /// <param name="namespaceName">
        /// The name of the namespace to be checked.
        /// </param>
        /// <returns>
        /// true if the namespace subsumes another namespace; false otherwise
        /// </returns>
        private bool IsSubsumingNamespace(string namespaceName)
        {
            return (_subsumingNamespaces == null ? false : _subsumingNamespaces.ContainsKey(namespaceName));
        }
 
        /// <summary>
        /// Used to specify that a namespace subsumes another namespace
        /// </summary>
        /// <param name="namespaceName">
        /// The name of the namespace to be added.
        /// </param>
        private void AddSubsumingNamespace(string namespaceName)
        {
            _subsumingNamespaces ??= new Dictionary<string, object?>();
            _subsumingNamespaces[namespaceName] = null;
        }
 
        /// <summary>
        /// Used to determine whether a given namespace is known/supported
        /// </summary>
        /// <param name="namespaceName">
        /// The name of the namespace to be checked.
        /// </param>
        /// <returns>
        /// true if the namespace is known/supported; false otherwise
        /// </returns>
        private bool IsNamespaceKnown(string namespaceName)
        {
            return (_knownNamespaces == null ? false : _knownNamespaces.ContainsKey(namespaceName));
        }
 
        /// <summary>
        /// Used to specify that a namespace is known or supported
        /// </summary>
        /// <param name="namespaceName">
        /// The name of the namespace to be added.
        /// </param>
        private void AddKnownNamespace(string namespaceName)
        {
            _knownNamespaces ??= new Dictionary<string, object?>();
            _knownNamespaces[namespaceName] = null;
        }
 
        /// <summary>
        /// Used to determine whether a given namespace should be ignored.  A namespace should be ignored if:
        /// EITHER
        /// a) the namespace is not known/supported and has been marked Ignorable
        /// OR
        /// b) the namespace is the markup-compatibility namespace
        /// </summary>
        /// <param name="namespaceName">
        /// The name of the prefix to be checked.
        /// </param>
        /// <returns>
        /// true if the namespace should be ignored; false otherwise
        /// </returns>
        private bool ShouldIgnoreNamespace(string namespaceName)
        {
            bool result;
            if (IsNamespaceKnown(namespaceName))
            {
                result = object.ReferenceEquals(namespaceName, CompatibilityUri);
            }
            else
            {
                result = Scope.CanIgnore(namespaceName);
            }
            return result;
        }
 
        /// <summary>
        /// breaks up a space-delineated string into namespace/element pairs
        /// </summary>
        /// <param name="content">
        /// the string to be parsed
        /// </param>
        /// <param name="callerContext">
        /// The calling element, used in case of an error
        /// </param>
        /// <returns>
        /// the list of namespace/element pairs
        /// </returns>
        private IEnumerable<NamespaceElementPair> ParseContentToNamespaceElementPair(string content, string? callerContext)
        {
            foreach (string pair in content.Trim().Split(' '))
            {
                // check each non-null, non-empty space-delineated namespace/element pair
                if (!string.IsNullOrEmpty(pair))
                {
                    int colonIndex = pair.IndexOf(':');
                    int length = pair.Length;
 
                    if (colonIndex <= 0 || colonIndex >= length - 1 || colonIndex != pair.LastIndexOf(':'))
                    {
                        // if string does not have a ':', if the last character in the string is a ':'
                        // or if the string contains more than one ':', throw an exception
                        Error(SR.XCRInvalidFormat, callerContext);
                    }
 
                    string prefix = pair.Substring(0, colonIndex);
                    string elementName = pair.Substring(colonIndex + 1, length - 1 - colonIndex);
                    string? namespaceName = LookupNamespace(prefix);
 
                    if (namespaceName == null)
                    {
                        // if a prefix does not map to a namespace, throw an exception
                        Error(SR.XCRUndefinedPrefix, prefix);
                    }
                    else if (elementName != "*" && !IsName(elementName))
                    {
                        // if the element's name is not valid XML, throw an exception
                        Error(SR.XCRInvalidXMLName, pair);
                    }
                    else
                    {
                        yield return new NamespaceElementPair(namespaceName, elementName);
                    }
                }
            }
        }
 
        /// <summary>
        /// converts a string of space-delineated prefixes into a list of namespaces
        /// </summary>
        /// <param name="prefixes">
        /// the string to be parsed
        /// </param>
        /// <returns>
        /// the list of namespace/element pairs
        /// </returns>
        private IEnumerable<string> PrefixesToNamespaces(string prefixes)
        {
            foreach (string prefix in prefixes.Trim().Split(' '))
            {
                // check each non-null, non-empty space-delineated prefix
                if (!string.IsNullOrEmpty(prefix))
                {
                    string? namespaceUri = LookupNamespace(prefix);
 
                    if (namespaceUri == null)
                    {
                        // if a prefix does not map to a namespace, throw an exception
                        Error(SR.XCRUndefinedPrefix, prefix);
                    }
                    else
                    {
                        yield return namespaceUri;
                    }
                }
            }
        }
 
        /// <summary>
        /// advances the reader to the next known namespace/attribute pair
        /// </summary>
        /// <returns>
        /// true if a known namespace/attribute pair was found
        /// </returns>
        private bool SkipToKnownAttribute()
        {
            bool result = true;
            while (result && ShouldIgnoreNamespace(NamespaceURI))
            {
                result = Reader.MoveToNextAttribute();
            }
            return result;
        }
 
        /// <summary>
        /// Scans the current element for compatibility attributes.  Pushes a new
        /// scope onto the stack under the following conditions:
        /// 1) Ignorable or MustUnderstand attribute read
        /// 2) current element has not previously declared an Ignorable or
        ///    MustUnderstand attribute
        ///
        /// However, if a last condition is not fulfilled, then the scope is popped off
        /// before the function returns
        /// 3) current element is not empty
        ///
        /// stores in _ignoredAttributeCount the number of attributes on the current element
        /// that should be ignored, for the sake of improving perf in attribute-related
        /// methods/properties
        /// </summary>
        /// <param name="elementDepth">
        /// the depth of the Reader at the element currently being processed
        /// </param>
        private void ScanForCompatibility(int elementDepth)
        {
            bool onAttribute = Reader.MoveToFirstAttribute();
 
            _ignoredAttributeCount = 0;
 
            if (onAttribute)
            {
                _attributePosition = 0; // we count the attribute index in case we see Ignorable
 
                do
                {
                    string namespaceName = NamespaceURI;
 
                    if (ShouldIgnoreNamespace(namespaceName))
                    {
                        // check each attribute's namespace to see if it should be ignored
                        if (object.ReferenceEquals(namespaceName, CompatibilityUri))
                        {
                            // if the attribute is in the markup-compatibility namespace
                            // find and call the appropriate attribute handler callback.
                            string attributeName = Reader.LocalName;
                            HandleAttributeCallback? attributeCB;
                            if (!_attributeHandler.TryGetValue(attributeName, out attributeCB))
                            {
                                Error(SR.XCRUnknownCompatAttrib, attributeName);
                            }
                            attributeCB(elementDepth);
                        }
 
                        _ignoredAttributeCount++;
                    }
 
                    onAttribute = Reader.MoveToNextAttribute();
                    _attributePosition++; // we count the attribute index in case we see Ignorable
                } while (onAttribute);
 
                if (Scope.Depth == elementDepth)
                {
                    // if this element pushed a scope, then we need to do a sanity check
                    Scope.Verify();
                }
 
                // move the reader back to the element for the client
                Reader.MoveToElement();
            }
        }
 
 
        /// <summary>
        /// pops a scope if the end of a compatibility region.
        /// </summary>
        /// <param name="elementDepth">
        /// the depth of the Reader at the element currently being processed
        /// </param>
        private void ScanForEndCompatibility(int elementDepth)
        {
            if (elementDepth == Scope.Depth)
            {
                // if the current element's depth equals the depth of the top-level scope, then pop
                PopScope();
            }
        }
 
        /// <summary>
        /// pushes a new scope onto the stack with a depth passed as an arg.
        /// PushScope does not push a scope if the top scope on the stack is not a lower depth.
        /// </summary>
        /// <param name="elementDepth">
        /// the depth of the Reader at the element currently being processed
        /// </param>
        private void PushScope(int elementDepth)
        {
            if (_compatibilityScope.Depth < elementDepth)
            {
                // if the current element has already pushed a scope, then don't push another one
                _compatibilityScope = new CompatibilityScope(_compatibilityScope, elementDepth, this);
            }
        }
 
        /// <summary>
        /// pops a scope off the top of the stack.
        /// PopScope *always* pops, it does not check the depth before doing so
        /// </summary>
        private void PopScope()
        {
            _compatibilityScope = _compatibilityScope.Previous!;
        }
 
        /// <summary>
        /// handles mc:AlternateContent element
        ///
        /// a good way to think of AlternateContent blocks is as a switch/case
        /// statement.  The AlternateContent tag is like switch, Choice is like
        /// case, and Fallback is like default.
        /// </summary>
        /// <param name="elementDepth">
        /// the depth of the Reader at the element currently being processed
        /// </param>
        /// <param name="more">
        /// returns whether the Reader has more to be read
        /// </param>
        private void HandleAlternateContent(int elementDepth, ref bool more)
        {
            if (Scope.InAlternateContent)
            {
                // the only valid tags within <AlternateContent> ... </> are
                // Choice and Fallback
                Error(SR.Format(SR.XCRInvalidACChild, Reader.Name));
            }
            if (Reader.IsEmptyElement)
            {
                // AlternateContent blocks must have a Choice, so they can't be empty
                Error(SR.XCRChoiceNotFound);
            }
 
            // check for markup-compatibility attributes, then push an AlternateContent scope
            ScanForCompatibility(elementDepth);
            PushScope(elementDepth);
 
            Scope.InAlternateContent = true;
            _depthOffset++;
            more = Reader.Read();
        }
 
        /// <summary>
        /// handles mc:Choice element
        ///
        /// a good way to think of AlternateContent blocks is as a switch/case
        /// statement.  The AlternateContent tag is like switch, Choice is like
        /// case, and Fallback is like default.
        /// </summary>
        /// <param name="elementDepth">
        /// the depth of the Reader at the element currently being processed
        /// </param>
        /// <param name="more">
        /// returns whether the Reader has more to be read
        /// </param>
        private void HandleChoice(int elementDepth, ref bool more)
        {
            if (!Scope.InAlternateContent)
            {
                // Choice must be the child of AlternateContent
                Error(SR.XCRChoiceOnlyInAC);
            }
            if (Scope.FallbackSeen)
            {
                // Choice cannot occur after Fallback
                Error(SR.XCRChoiceAfterFallback);
            }
 
            string? requiresValue = Reader.GetAttribute(Requires);
 
            if (requiresValue == null)
            {
                // Choice must have a requires attribute
                Error(SR.XCRRequiresAttribNotFound);
            }
            if (string.IsNullOrEmpty(requiresValue))
            {
                // Requires attribute may not be empty
                Error(SR.XCRInvalidRequiresAttribute);
            }
 
            CompatibilityScope scope = Scope;
 
            // check for markup-compatibility attributes
            ScanForCompatibility(elementDepth);
 
            if (AttributeCount != 1)
            {
                // Choice may not have any attribute that should not be ignored other than Requires
                // get first non-markup-compatibility, non-Requires attribute
                MoveToFirstAttribute();
                if (Reader.LocalName == Requires)
                {
                    MoveToNextAttribute();
                }
                string attributeName = Reader.LocalName;
                MoveToElement();
 
                Error(SR.XCRInvalidAttribInElement, attributeName, Choice);
            }
 
            if (scope.ChoiceTaken)
            {
                // a previous choice was valid, so pop any scope pushed and
                // skip to next attribute after </mc:Choice>
                ScanForEndCompatibility(elementDepth);
                Reader.Skip();
            }
            else
            {
                // mark AlternateContent as having seen a choice
                scope.ChoiceSeen = true;
 
                bool allKnown = true;
                bool somethingSeen = false;
 
                foreach (string namespaceUri in PrefixesToNamespaces(requiresValue))
                {
                    somethingSeen = true;
                    if (!IsNamespaceKnown(namespaceUri))
                    {
                        // if any attribute in the Requires value is unknown, then do not take this choice
                        allKnown = false;
                        break;
                    }
                }
 
                if (!somethingSeen)
                {
                    // if the Requires value does not contain a valid prefix/namespace, throw an exception
                    Error(SR.XCRInvalidRequiresAttribute);
                }
 
                if (allKnown)
                {
                    // if all namespace in the Requires value are known, then this is the Choice taken.
                    // Mark AlternateContent scope as having taken a choice
                    scope.ChoiceTaken = true;
 
                    // we push a scope here as a place holder, because AlternateContent
                    // scopes do not allow child elements other than Choice and Fallback
                    PushScope(elementDepth);
                    _depthOffset++;
                    more = Reader.Read();
                }
                else
                {
                    // this is not the choice taken, so pop any scope pushed and
                    // skip to next attribute after </mc:Choice>
                    ScanForEndCompatibility(elementDepth);
                    Reader.Skip();
                }
            }
        }
 
        /// <summary>
        /// handles mc:Fallback element
        ///
        /// a good way to think of AlternateContent blocks is as a switch/case
        /// statement.  The AlternateContent tag is like switch, Choice is like
        /// case, and Fallback is like default.
        /// </summary>
        /// <param name="elementDepth">
        /// the depth of the Reader at the element currently being processed
        /// </param>
        /// <param name="more">
        /// returns whether the Reader has more to be read
        /// </param>
        private void HandleFallback(int elementDepth, ref bool more)
        {
            if (!Scope.InAlternateContent)
            {
                // Fallback must be the child of AlternateContent
                Error(SR.XCRFallbackOnlyInAC);
            }
            if (!Scope.ChoiceSeen)
            {
                // AlternateContent block must contain a Choice element
                Error(SR.XCRChoiceNotFound);
            }
            if (Scope.FallbackSeen)
            {
                // AlternateContent block may only contain one Fallback child
                Error(SR.XCRMultipleFallbackFound);
            }
 
            // mark scope as having a fallback
            Scope.FallbackSeen = true;
            bool choiceTaken = Scope.ChoiceTaken;
 
            // check for markup-compatibility attributes
            ScanForCompatibility(elementDepth);
 
            if (AttributeCount != 0)
            {
                // Fallback may not have any attribute that should not be ignored
                // get first non-markup-compatibility attribute
                MoveToFirstAttribute();
                string attributeName = Reader.LocalName;
                MoveToElement();
 
                Error(SR.XCRInvalidAttribInElement, attributeName, Fallback);
            }
 
            if (choiceTaken)
            {
                // a choice was valid, so ignore contents
                ScanForEndCompatibility(elementDepth);
                Reader.Skip();
            }
            else
            {
                // this is the content that will be used, so push a scope
                if (!Reader.IsEmptyElement)
                {
                    // we push a scope here as a place holder, because AlternateContent
                    // scopes do not allow child elements other than Choice and Fallback
                    PushScope(elementDepth);
                    _depthOffset++;
                }
                more = Reader.Read();
            }
        }
 
        /// <summary>
        /// handles mc:Ignorable="foo" attribute
        ///
        /// Ignorable is used to indicate that the namespace the prefix is mapped to can
        /// be ignored, i.e. when the namespace/element or namespace/attribute occurs it
        /// is not returned by the reader.
        /// </summary>
        private void HandleIgnorable(int elementDepth)
        {
            PushScope(elementDepth);
 
            foreach (string namespaceUri in PrefixesToNamespaces(Reader.Value))
            {
                Scope.Ignorable(namespaceUri);
            }
 
            // Just in case one of the namespaces that preceded the Ignorable declaration
            // was an ignorable namespace, we have to recompute _ignoredAttributeCount.
            // No need to check if we haven't yet had any non-ignored attributes.
            if (_ignoredAttributeCount < _attributePosition)
            {
                _ignoredAttributeCount = 0;
                Reader.MoveToFirstAttribute();
 
                for (int i = 0; i < _attributePosition; i++)
                {
                    if (ShouldIgnoreNamespace(Reader.NamespaceURI))
                    {
                        _ignoredAttributeCount++;
                    }
 
                    Reader.MoveToNextAttribute();
                }
            }
        }
 
        /// <summary>
        /// handles mc:MustUnderstand="foo" attribute
        ///
        /// MustUnderstand is used to indicate that the namespace the prefix is mapped to
        /// cannot be handled, and if it is not understood an exception is thrown
        /// </summary>
        private void HandleMustUnderstand(int elementDepth)
        {
            foreach (string namespaceUri in PrefixesToNamespaces(Reader.Value))
            {
                if (!IsNamespaceKnown(namespaceUri))
                {
                    Error(SR.XCRMustUnderstandFailed, namespaceUri);
                }
            }
        }
 
        /// <summary>
        /// handles mc:ProcessContent="foo:bar" attribute
        ///
        /// ProcessContent is used to indicate that an ignorable namespace has some
        /// elements that should be skipped, but contain child elements that should be processed.
        ///
        /// The wildcard token ("foo:*") indicates that the children of any element in that
        /// namespace should be processed.
        /// </summary>
        private void HandleProcessContent(int elementDepth)
        {
            PushScope(elementDepth);
 
            foreach (NamespaceElementPair pair in ParseContentToNamespaceElementPair(Reader.Value, _processContent))
            {
                Scope.ProcessContent(pair.namespaceName, pair.itemName);
            }
        }
 
        /// <summary>
        /// handles mc:PreserveElements="foo:bar" attribute
        ///
        /// functionality is supported, but not implemented
        /// </summary>
        private void HandlePreserveElements(int elementDepth)
        {
            PushScope(elementDepth);
 
            foreach (NamespaceElementPair pair in ParseContentToNamespaceElementPair(Reader.Value, _preserveElements))
            {
                Scope.PreserveElement(pair.namespaceName, pair.itemName);
            }
        }
 
        /// <summary>
        /// handles mc:PreserveAttributes="foo:bar" attribute
        ///
        /// functionality is supported, but not implemented
        /// </summary>
        private void HandlePreserveAttributes(int elementDepth)
        {
            PushScope(elementDepth);
 
            foreach (NamespaceElementPair pair in ParseContentToNamespaceElementPair(Reader.Value, _preserveAttributes))
            {
                Scope.PreserveAttribute(pair.namespaceName, pair.itemName);
            }
        }
 
        /// <summary>
        /// helper method to generate an exception
        /// </summary>
        [DoesNotReturn]
        private void Error(string message, params object?[] args)
        {
            IXmlLineInfo? info = Reader as IXmlLineInfo;
            throw new XmlException(string.Format(CultureInfo.InvariantCulture, message, args), null, info == null ? 1 : info.LineNumber,
                info == null ? 1 : info.LinePosition);
        }
        #endregion Private Methods
 
        #region Private Properties
        private CompatibilityScope Scope
        {
            get
            {
                return _compatibilityScope;
            }
        }
 
        private string AlternateContent => _alternateContent ??= Reader.NameTable.Add("AlternateContent");
 
        private string Choice => _choice ??= Reader.NameTable.Add("Choice");
 
        private string Fallback => _fallback ??= Reader.NameTable.Add("Fallback");
 
        private string Requires => _requires ??= Reader.NameTable.Add("Requires");
 
        private string Ignorable => _ignorable ??= Reader.NameTable.Add("Ignorable");
 
        private string MustUnderstand => _mustUnderstand ??= Reader.NameTable.Add("MustUnderstand");
 
        private string ProcessContent => _processContent ??= Reader.NameTable.Add("ProcessContent");
 
        private string PreserveElements => _preserveElements ??= Reader.NameTable.Add("PreserveElements");
 
        private string PreserveAttributes => _preserveAttributes ??= Reader.NameTable.Add("PreserveAttributes");
 
        private string CompatibilityUri => _compatibilityUri ??= Reader.NameTable.Add(MarkupCompatibilityURI);
        #endregion Private Properties
        #region Nested Classes
        private readonly struct NamespaceElementPair
        {
            public readonly string namespaceName;
            public readonly string itemName;
 
            public NamespaceElementPair(string namespaceName, string itemName)
            {
                this.namespaceName = namespaceName;
                this.itemName = itemName;
            }
        }
 
        /// <summary>
        /// CompatibilityScopes are used to handle markup-compatibility elements and attributes.
        /// Each scope stores the "previous" or parent scope, its depth, and an associated XmlCompatibilityReader.
        /// At a particular Reader depth, only one scope should be pushed.
        /// </summary>
        private sealed class CompatibilityScope
        {
            private readonly CompatibilityScope? _previous;
            private readonly int _depth;
            private bool _fallbackSeen;
            private bool _inAlternateContent;
            private bool _inProcessContent;
            private bool _choiceTaken;
            private bool _choiceSeen;
            private readonly XmlCompatibilityReader _reader;
            private Dictionary<string, object?>? _ignorables;
            private Dictionary<string, ProcessContentSet>? _processContents;
            private Dictionary<string, PreserveItemSet>? _preserveElements;
            private Dictionary<string, PreserveItemSet>? _preserveAttributes;
 
            public CompatibilityScope(CompatibilityScope? previous, int depth, XmlCompatibilityReader reader)
            {
                _previous = previous;
                _depth = depth;
                _reader = reader;
            }
 
            public CompatibilityScope? Previous
            {
                get
                {
                    return _previous;
                }
            }
 
            public int Depth
            {
                get
                {
                    return _depth;
                }
            }
 
            public bool FallbackSeen
            {
                get
                {
                    bool result;
                    if (_inProcessContent && _previous != null)
                    {
                        result = _previous.FallbackSeen;
                    }
                    else
                    {
                        result = _fallbackSeen;
                    }
                    return result;
                }
                set
                {
                    if (_inProcessContent && _previous != null)
                    {
                        _previous.FallbackSeen = value;
                    }
                    else
                    {
                        _fallbackSeen = value;
                    }
                }
            }
 
            public bool InAlternateContent
            {
                get
                {
                    bool result;
                    if (_inProcessContent && _previous != null)
                    {
                        result = _previous.InAlternateContent;
                    }
                    else
                    {
                        result = _inAlternateContent;
                    }
                    return result;
                }
                set
                {
                    _inAlternateContent = value;
                }
            }
 
            public bool InProcessContent
            {
                set
                {
                    _inProcessContent = value;
                }
            }
 
            public bool ChoiceTaken
            {
                get
                {
                    bool result;
                    if (_inProcessContent && _previous != null)
                    {
                        result = _previous.ChoiceTaken;
                    }
                    else
                    {
                        result = _choiceTaken;
                    }
                    return result;
                }
                set
                {
                    if (_inProcessContent && _previous != null)
                    {
                        _previous.ChoiceTaken = value;
                    }
                    else
                    {
                        _choiceTaken = value;
                    }
                }
            }
 
            public bool ChoiceSeen
            {
                get
                {
                    bool result;
                    if (_inProcessContent && _previous != null)
                    {
                        result = _previous.ChoiceSeen;
                    }
                    else
                    {
                        result = _choiceSeen;
                    }
                    return result;
                }
                set
                {
                    if (_inProcessContent && _previous != null)
                    {
                        _previous.ChoiceSeen = value;
                    }
                    else
                    {
                        _choiceSeen = value;
                    }
                }
            }
 
            public bool CanIgnore(string namespaceName)
            {
                bool result = IsIgnorableAtCurrentScope(namespaceName);
 
                if (!result && _previous != null)
                {
                    result = _previous.CanIgnore(namespaceName);
                }
 
                return result;
            }
 
            public bool IsIgnorableAtCurrentScope(string namespaceName)
            {
                return _ignorables != null && _ignorables.ContainsKey(namespaceName);
            }
 
            public bool ShouldProcessContent(string namespaceName, string elementName)
            {
                bool result = false;
                ProcessContentSet? set;
                if (_processContents != null && _processContents.TryGetValue(namespaceName, out set))
                {
                    result = set.ShouldProcessContent(elementName);
                }
                else if (_previous != null)
                {
                    result = _previous.ShouldProcessContent(namespaceName, elementName);
                }
 
                return result;
            }
 
            public void Ignorable(string namespaceName)
            {
                _ignorables ??= new Dictionary<string, object?>();
                _ignorables[namespaceName] = null; // we don't care about value, just key
            }
 
            public void ProcessContent(string namespaceName, string elementName)
            {
                _processContents ??= new Dictionary<string, ProcessContentSet>();
                ProcessContentSet? processContentSet;
                if (!_processContents.TryGetValue(namespaceName, out processContentSet))
                {
                    processContentSet = new ProcessContentSet(namespaceName, _reader);
                    _processContents.Add(namespaceName, processContentSet);
                }
                processContentSet.Add(elementName);
            }
 
            public void PreserveElement(string namespaceName, string elementName)
            {
                _preserveElements ??= new Dictionary<string, PreserveItemSet>();
                PreserveItemSet? preserveElementSet;
                if (!_preserveElements.TryGetValue(namespaceName, out preserveElementSet))
                {
                    preserveElementSet = new PreserveItemSet(namespaceName, _reader);
                    _preserveElements.Add(namespaceName, preserveElementSet);
                }
                preserveElementSet.Add(elementName);
            }
 
            public void PreserveAttribute(string namespaceName, string attributeName)
            {
                _preserveAttributes ??= new Dictionary<string, PreserveItemSet>();
                PreserveItemSet? preserveAttributeSet;
                if (!_preserveAttributes.TryGetValue(namespaceName, out preserveAttributeSet))
                {
                    preserveAttributeSet = new PreserveItemSet(namespaceName, _reader);
                    _preserveAttributes.Add(namespaceName, preserveAttributeSet);
                }
                preserveAttributeSet.Add(attributeName);
            }
 
            public void Verify()
            {
                // Check process content
                if (_processContents != null)
                {
                    foreach (string key in _processContents.Keys)
                    {
                        if (!IsIgnorableAtCurrentScope(key))
                        {
                            _reader.Error(SR.XCRNSProcessContentNotIgnorable, key);
                        }
                    }
                }
                // Check preserve elements
                if (_preserveElements != null)
                {
                    foreach (string key in _preserveElements.Keys)
                    {
                        if (!IsIgnorableAtCurrentScope(key))
                        {
                            _reader.Error(SR.XCRNSPreserveNotIgnorable, key);
                        }
                    }
                }
                // Check preserve attributes
                if (_preserveAttributes != null)
                {
                    foreach (string key in _preserveAttributes.Keys)
                    {
                        if (!IsIgnorableAtCurrentScope(key))
                        {
                            _reader.Error(SR.XCRNSPreserveNotIgnorable, key);
                        }
                    }
                }
            }
        }
 
        private sealed class ProcessContentSet
        {
            private bool _all;
            private readonly string _namespaceName;
            private readonly XmlCompatibilityReader _reader;
            private HashSet<string>? _names;
 
            public ProcessContentSet(string namespaceName, XmlCompatibilityReader reader)
            {
                _namespaceName = namespaceName;
                _reader = reader;
            }
 
            public bool ShouldProcessContent(string elementName)
            {
                return _all || (_names != null && _names.Contains(elementName));
            }
 
            public void Add(string elementName)
            {
                if (ShouldProcessContent(elementName))
                {
                    if (elementName == "*")
                    {
                        _reader.Error(SR.XCRDuplicateWildcardProcessContent, _namespaceName);
                    }
                    else
                    {
                        _reader.Error(SR.XCRDuplicateProcessContent, _namespaceName, elementName);
                    }
                }
 
                if (elementName == "*")
                {
                    if (_names != null)
                    {
                        _reader.Error(SR.XCRInvalidProcessContent, _namespaceName);
                    }
                    else
                    {
                        _all = true;
                    }
                }
                else
                {
                    _names ??= new HashSet<string>();
                    _names.Add(elementName);
                }
            }
        }
 
        private sealed class PreserveItemSet
        {
            private bool _all;
            private readonly string _namespaceName;
            private readonly XmlCompatibilityReader _reader;
            private HashSet<string>? _names;
 
            public PreserveItemSet(string namespaceName, XmlCompatibilityReader reader)
            {
                _namespaceName = namespaceName;
                _reader = reader;
            }
 
            public bool ShouldPreserveItem(string itemName)
            {
                return _all || (_names != null && _names.Contains(itemName));
            }
 
            public void Add(string itemName)
            {
                if (ShouldPreserveItem(itemName))
                {
                    if (itemName == "*")
                    {
                        _reader.Error(SR.XCRDuplicateWildcardPreserve, _namespaceName);
                    }
                    else
                    {
                        _reader.Error(SR.XCRDuplicatePreserve, itemName, _namespaceName);
                    }
                }
 
                if (itemName == "*")
                {
                    if (_names != null)
                    {
                        _reader.Error(SR.XCRInvalidPreserve, _namespaceName);
                    }
                    else
                    {
                        _all = true;
                    }
                }
                else
                {
                    _names ??= new HashSet<string>();
                    _names.Add(itemName);
                }
            }
        }
        #endregion Nested Classes
 
        #region Private Fields
        private bool _inAttribute; // for Save/Restore ReaderPosition
        private string? _currentName; // for Save/Restore ReaderPosition
        private readonly IsXmlNamespaceSupportedCallback? _namespaceCallback;
        private Dictionary<string, object?>? _knownNamespaces;
        private readonly Dictionary<string, string> _namespaceMap = new Dictionary<string, string>();
        private Dictionary<string, object?>? _subsumingNamespaces;
        private readonly Dictionary<string, HandleElementCallback> _elementHandler = new Dictionary<string, HandleElementCallback>();
        private readonly Dictionary<string, HandleAttributeCallback> _attributeHandler = new Dictionary<string, HandleAttributeCallback>();
        private int _depthOffset; // offset for Depth method, to account for elements that should be ignored by client
        private int _ignoredAttributeCount;
        private int _attributePosition; // used for ScanForCompatibility / HandleIgnorable
        private string? _compatibilityUri;
        private string? _alternateContent;
        private string? _choice;
        private string? _fallback;
        private string? _requires;
        private string? _ignorable;
        private string? _mustUnderstand;
        private string? _processContent;
        private string? _preserveElements;
        private string? _preserveAttributes;
        private CompatibilityScope _compatibilityScope;
 
        private bool _isPreviousElementEmpty;
        private int _previousElementDepth;
 
        private const string XmlnsDeclaration = "xmlns";
        private const string MarkupCompatibilityURI = "http://schemas.openxmlformats.org/markup-compatibility/2006";
 
        private static readonly string[] s_predefinedNamespaces = new string[4] {
            "http://www.w3.org/2000/xmlns/",
            "http://www.w3.org/XML/1998/namespace",
            "http://www.w3.org/2001/XMLSchema-instance",
            MarkupCompatibilityURI
        };
        #endregion Private Fields
    }
}