File: System\Configuration\XmlUtil.cs
Project: ndp\fx\src\Configuration\System.Configuration.csproj (System.Configuration)
//------------------------------------------------------------------------------
// <copyright file="XmlUtil.cs" company="Microsoft">
//     Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
//------------------------------------------------------------------------------
 
namespace System.Configuration {
    using System.Configuration.Internal;
    using System.Collections;
    using System.Collections.Specialized;
    using System.Collections.Generic;
    using System.Configuration;
    using System.Diagnostics.CodeAnalysis;
    using System.Globalization;
    using System.IO;
    using System.Runtime.InteropServices;
    using System.Security;
    using System.Security.Permissions;
    using System.Text;
    using System.Xml;
    using System.Net;
 
    //
    // XmlTextReader Helper class.
    // 
    // Provides the following services:
    //
    //      * Reader methods that verify restrictions on the XML that can be contained in a config file.
    //      * Methods to copy the reader stream to a writer stream.
    //      * Method to copy a configuration section to a string.
    //      * Methods to format a string of XML.
    //
    // Errors found during read are accumlated in a ConfigurationSchemaErrors object.
    //
    internal sealed class XmlUtil : IDisposable, IConfigErrorInfo
    {
        private const int   MAX_LINE_WIDTH=60;
 
        // Offset from where the reader reports the LinePosition of an Xml Node to
        // the start of that representation in text.
        static readonly int[] s_positionOffset = {
            0,      // None,                  
            1,      // Element,                 <elem
            -1,     // Attribute,               N/A
            0,      // Text,                    
            9,      // CDATA,                   <![CDATA[
            1,      // EntityReference,         &lt
            -1,     // Entity,                  N/A
            2,      // ProcessingInstruction,   <?pi
            4,      // Comment,                 <!--
            -1,     // Document,                N/A
            10,     // DocumentType,            <!DOCTYPE
            -1,     // DocumentFragment,        N/A
            -1,     // Notation,                N/A
            0,      // Whitespace,              
            0,      // SignificantWhitespace,   
            2,      // EndElement,              />
            -1,     // EndEntity,               N/A
            2,      // XmlDeclaration           <?xml
        };
 
        private static int GetPositionOffset(XmlNodeType nodeType) {
            return s_positionOffset[(int)nodeType];
        }
 
        private Stream                      _stream;                    // the stream to read
        private string                      _streamName;                // name of the stream, typically a file name
        private XmlTextReader               _reader;                    // the XmlTextReader over the stream
        private StringWriter                _cachedStringWriter;        // cached string writer used by CopySection()
        private ConfigurationSchemaErrors   _schemaErrors;              // accumulated errors
        private int                         _lastLineNumber;            // last line number after a call to CopyXmlNode()
        private int                         _lastLinePosition;          // last line position after a call to CopyXmlNode()
 
        internal XmlUtil(Stream stream, string name, bool readToFirstElement) :
            this(stream, name, readToFirstElement, new ConfigurationSchemaErrors()) {}
 
        [SuppressMessage("Microsoft.Security.Xml", "CA3054:DoNotAllowDtdOnXmlTextReader", Justification="Reading trusted input")]
        internal XmlUtil(Stream stream, string name, bool readToFirstElement, ConfigurationSchemaErrors schemaErrors) {
            try {
                _streamName = name;
                _stream = stream;
                _reader = new XmlTextReader(_stream);
 
                // config reads never require a resolver
                _reader.XmlResolver = null;
 
                _schemaErrors = schemaErrors;
                _lastLineNumber = 1;
                _lastLinePosition = 1;
 
                //
                // When parsing config that we don't intend to copy, skip all content
                // before the first element.
                //
                if (readToFirstElement) {
                    _reader.WhitespaceHandling = WhitespaceHandling.None;
 
                    bool done = false;
                    while (!done && _reader.Read()) {
                        switch (_reader.NodeType) {
                            case XmlNodeType.XmlDeclaration:
                            case XmlNodeType.Comment:
                            case XmlNodeType.DocumentType:
                                break;
 
                            case XmlNodeType.Element:
                                done = true;
                                break;
 
                            default:
                                throw new ConfigurationErrorsException(SR.GetString(SR.Config_base_unrecognized_element), this);
                        }
                    }
                }
            }
            catch {
                ReleaseResources();
                throw;
            }
        }
 
        private void ReleaseResources() {
            if (_reader != null) {
                // closing _reader will also close underlying _stream
                _reader.Close();
                _reader = null;
            }
            else if (_stream != null) {
                _stream.Close();
            }
 
            _stream = null;
 
            if (_cachedStringWriter != null) {
                _cachedStringWriter.Close();
                _cachedStringWriter = null;
            }
        }
 
        public void Dispose() {
            ReleaseResources();
        }
 
        public string Filename {
            get { return _streamName; } 
        }
 
        public int LineNumber {
            get { return Reader.LineNumber; }
        }
 
        //
        // Return the line position of the reader, compensating for the reader's offset
        // for nodes such as an XmlElement.
        //
        internal int TrueLinePosition {
            get { 
                int trueLinePosition = Reader.LinePosition - GetPositionOffset(Reader.NodeType); 
                Debug.Assert(trueLinePosition > 0, "trueLinePosition > 0");
                return trueLinePosition;
            }
        }
 
        internal XmlTextReader Reader {
            get { return _reader; }
        }
 
        internal ConfigurationSchemaErrors SchemaErrors {
            get { return _schemaErrors; }
        }
 
        //
        // Read until the Next Element element, or we hit
        // the end of the file.
        //
        internal void ReadToNextElement() {
            while (_reader.Read()) {
                if (_reader.MoveToContent() == XmlNodeType.Element) {
                    // We found an element, so return
                    return;
                }
            }
 
            // We must of hit end of file
        }
 
        //
        // Skip this element and its children, then read to next start element,
        // or until we hit end of file.
        //
        internal void SkipToNextElement() {
            _reader.Skip();
            _reader.MoveToContent();
 
            while (!_reader.EOF && _reader.NodeType != XmlNodeType.Element) {
                _reader.Read();
                _reader.MoveToContent();
            }
        }
 
        //
        // Read to the next start element, and verify that all XML nodes read are permissible.
        //
        internal void StrictReadToNextElement(ExceptionAction action) {
            while (_reader.Read()) {
                // optimize for the common case
                if (_reader.NodeType == XmlNodeType.Element) {
                    return;
                }
 
                VerifyIgnorableNodeType(action);
            }
        }
 
        //
        // Skip this element and its children, then read to next start element,
        // or until we hit end of file. Verify that nodes that are read after the
        // skipped element are permissible.
        //
        internal void StrictSkipToNextElement(ExceptionAction action) {
            _reader.Skip();
 
            while (!_reader.EOF && _reader.NodeType != XmlNodeType.Element) {
                VerifyIgnorableNodeType(action);
                _reader.Read();
            }
        }
 
        //
        // StrictSkipToOurParentsEndElement
        //
        // Skip until we hit the end element for our parent, and verify
        // that nodes at the parent level are permissible.
        //
        internal void StrictSkipToOurParentsEndElement(ExceptionAction action) {
            int currentDepth = _reader.Depth;
 
            // Skip everything at out current level
            while (_reader.Depth >= currentDepth) {
                _reader.Skip();
            }
 
            while (!_reader.EOF && _reader.NodeType != XmlNodeType.EndElement) {
                VerifyIgnorableNodeType(action);
                _reader.Read();
            }
        }
 
        //
        // Add an error if the node type is not permitted by the configuration schema.
        //
        internal void VerifyIgnorableNodeType(ExceptionAction action) {
            XmlNodeType nodeType = _reader.NodeType;
            
            if (nodeType != XmlNodeType.Comment && nodeType != XmlNodeType.EndElement) {
                ConfigurationException ex = new ConfigurationErrorsException(
                    SR.GetString(SR.Config_base_unrecognized_element), 
                    this);
                                
                SchemaErrors.AddError(ex, action);
            }
        }
 
        //
        // Add an error if there are attributes that have not been examined,
        // and are therefore unrecognized.
        //
        internal void VerifyNoUnrecognizedAttributes(ExceptionAction action) {
            if (_reader.MoveToNextAttribute()) {
                AddErrorUnrecognizedAttribute(action);
            }
        }
 
        //
        // Add an error if the retrieved attribute is null, 
        // and therefore not present.
        //
        internal bool VerifyRequiredAttribute(
                object o, string attrName, ExceptionAction action) {
 
            if (o == null) {
                AddErrorRequiredAttribute(attrName, action);
                return false;
            }
            else {
                return true;
            }
        }
 
        // 
        // Functions to handle parsing errors
        //
 
        internal void AddErrorUnrecognizedAttribute(ExceptionAction action) {
            ConfigurationErrorsException ex = new ConfigurationErrorsException(
                SR.GetString(SR.Config_base_unrecognized_attribute, _reader.Name), 
                this);
 
            SchemaErrors.AddError(ex, action);
        }
 
        internal void AddErrorRequiredAttribute(string attrib, ExceptionAction action) {
            ConfigurationErrorsException ex = new ConfigurationErrorsException(
                SR.GetString(SR.Config_missing_required_attribute, attrib, _reader.Name),
                this);
 
            SchemaErrors.AddError(ex, action);
        }
 
        internal void AddErrorReservedAttribute(ExceptionAction action) {
            ConfigurationErrorsException ex = new ConfigurationErrorsException(
                SR.GetString(SR.Config_reserved_attribute, _reader.Name),
                this);
 
            SchemaErrors.AddError(ex, action);
        }
 
        internal void AddErrorUnrecognizedElement(ExceptionAction action) {
            ConfigurationErrorsException ex = new ConfigurationErrorsException(
                SR.GetString(SR.Config_base_unrecognized_element),
                this);
 
            SchemaErrors.AddError(ex, action);
        }
 
        internal void VerifyAndGetNonEmptyStringAttribute(ExceptionAction action, out string newValue) {
            if (!String.IsNullOrEmpty(_reader.Value)) {
                newValue = _reader.Value;
            }
            else {
                newValue = null;
 
                ConfigurationException ex = new ConfigurationErrorsException(
                    SR.GetString(SR.Empty_attribute, _reader.Name),
                    this);
 
                SchemaErrors.AddError(ex, action);
            }
        }
 
        // VerifyAndGetBooleanAttribute
        //
        // Verify and Retrieve the Boolean Attribute.  If it is not
        // a valid value then log an error and set the value to a given default.
        //
        internal void VerifyAndGetBooleanAttribute(
                ExceptionAction action, bool defaultValue, out bool newValue) {
 
            if (_reader.Value == "true") {
                newValue = true;
            }
            else if (_reader.Value == "false") {
                newValue = false;
            }
            else {
                // Unrecognized value
                newValue = defaultValue;
 
                ConfigurationErrorsException ex = new ConfigurationErrorsException(
                    SR.GetString(SR.Config_invalid_boolean_attribute, _reader.Name), 
                    this);
 
                SchemaErrors.AddError(ex, action);
            }
        }
 
        //
        // Copy an XML element, then continue copying until we've hit the next element
        // or exited this depth.
        //
        internal bool CopyOuterXmlToNextElement(XmlUtilWriter utilWriter, bool limitDepth) {
            CopyElement(utilWriter);
 
            // Copy until reaching the next element, or if limitDepth == true until we've exited this depth.
            return CopyReaderToNextElement(utilWriter, limitDepth);
        }
 
        //
        // Copy an XML element but skip all its child elements, then continue copying until we've hit the next element.
        //
        internal bool SkipChildElementsAndCopyOuterXmlToNextElement(XmlUtilWriter utilWriter) {
            bool    isEmptyElement = _reader.IsEmptyElement;
            int     startingLine = _reader.LineNumber;
#if DBG
            int     depth = _reader.Depth;
#endif
 
            Debug.Assert(_reader.NodeType == XmlNodeType.Element, "_reader.NodeType == XmlNodeType.Element");
 
            CopyXmlNode(utilWriter);
 
            // See if we need to skip any child element
            if (!isEmptyElement) {
                while (_reader.NodeType != XmlNodeType.EndElement) {
                    
                    // Skip all the inner child elements
                    if (_reader.NodeType == XmlNodeType.Element) {
                        _reader.Skip();
 
                        // We need to skip all the whitespaces following a skipped element.
                        // - If the whitespaces don't contain /r/n, then it's okay to skip them 
                        //   as part of the element.
                        // - If the whitespaces contain /r/n, not skipping them will result
                        //   in a redundant emtpy line being copied.
                        if (_reader.NodeType == XmlNodeType.Whitespace) {
                            _reader.Skip();
                        }
                    }
                    else {
                        // We want to preserve other content, e.g. comments.
                        CopyXmlNode(utilWriter);
                    }
                }
 
                if (_reader.LineNumber != startingLine) {
                    // The whitespace in front of the EndElement was skipped above.
                    // We need to append spaces to compensate for that.
                    utilWriter.AppendSpacesToLinePosition(TrueLinePosition);
                }
 
 
#if DBG                
                Debug.Assert(_reader.Depth == depth, "We should be at the same depth as the opening Element");
#endif
                    
                // Copy the end element.
                CopyXmlNode(utilWriter);
            }                
 
            return CopyReaderToNextElement(utilWriter, true);
        }
 
        //
        // Copy the reader until we hit an element, or we've exited the current depth.
        //
        internal bool CopyReaderToNextElement(XmlUtilWriter utilWriter, bool limitDepth) {
            bool moreToRead = true;
 
            // Set the depth if we limit copying to this depth
            int depth;
            if (limitDepth) {
                // there is nothing in the element
                if (_reader.NodeType == XmlNodeType.EndElement)
                    return true;
 
                depth = _reader.Depth;
            }
            else {
                depth = 0;
            }
 
            // Copy nodes until we've reached the desired depth, or until we hit an element.
            do {
                if (_reader.NodeType == XmlNodeType.Element)
                    break;
 
                if (_reader.Depth < depth) {
                    break;
                }
 
                moreToRead = CopyXmlNode(utilWriter);
            } while (moreToRead);
 
            return moreToRead;
        }
 
        //
        // Skip over the current element and copy until the next element.
        // This function removes the one blank line that would otherwise
        // be inserted by simply skipping and copying to the next element
        // in a situation like this:
        //
        //      <!-- end of previous configSection -->
        //      <configSectionToDelete>
        //          <content />
        //          <moreContent />
        //      </configSectionToDelete>
        //      <!-- end of configSectionToDelete -->
        //      <nextConfigSection />
        //
        internal bool SkipAndCopyReaderToNextElement(XmlUtilWriter utilWriter, bool limitDepth) {
            Debug.Assert(_reader.NodeType == XmlNodeType.Element, "_reader.NodeType == XmlNodeType.Element");
 
            // If the last line before the element is not blank, then we do not have to
            // remove the blank line.
            if (!utilWriter.IsLastLineBlank) {
                _reader.Skip();
                return CopyReaderToNextElement(utilWriter, limitDepth);
            }
 
            // Set the depth if we limit copying to this depth
            int depth;
            if (limitDepth) {
                depth = _reader.Depth;
            }
            else {
                depth = 0;
            }
 
            // Skip over the element
            _reader.Skip();
 
            int lineNumberOfEndElement = _reader.LineNumber;
 
            // Read until we hit a a non-whitespace node or reach the end
            while (!_reader.EOF) {
                if (_reader.NodeType != XmlNodeType.Whitespace) {
                    //
                    // If the next non-whitepace node is on another line,
                    // seek back to the beginning of the current blank line,
                    // skip a blank line of whitespace, and copy the remaining whitespace.
                    //
                    if (_reader.LineNumber > lineNumberOfEndElement) {
                        utilWriter.SeekToLineStart();
                        utilWriter.AppendWhiteSpace(lineNumberOfEndElement + 1, 1, LineNumber, TrueLinePosition);
                    }
 
                    break;
                }
 
                _reader.Read();
            }
 
            // Copy nodes until we've reached the desired depth, or until we hit an element.
            while (!_reader.EOF) {
                if (_reader.NodeType == XmlNodeType.Element)
                    break;
 
                if (_reader.Depth < depth) {
                    break;
                }
 
                CopyXmlNode(utilWriter);
            };
 
            return !_reader.EOF;
        }
 
        //
        // Copy an XML element and its children, up to and including the end element.
        //
        private void CopyElement(XmlUtilWriter utilWriter) {
            Debug.Assert(_reader.NodeType== XmlNodeType.Element, "_reader.NodeType== XmlNodeType.Element");
 
            int depth = _reader.Depth;
            bool isEmptyElement = _reader.IsEmptyElement;
 
            // Copy current node
            CopyXmlNode(utilWriter);
 
            // Copy nodes while the depth is greater than the current depth.
            while (_reader.Depth > depth) {
                CopyXmlNode(utilWriter);
            }
 
            // Copy the end element.
            if (!isEmptyElement) {
                CopyXmlNode(utilWriter);
            }
        }
 
        //
        // Copy a single XML node, attempting to preserve whitespace.
        // A side effect of this method is to advance the reader to the next node.
        //
        // PERFORMANCE NOTE: this function is used at runtime to copy a configuration section,
        // and at designtime to copy an entire XML document.
        //
        // At designtime, this function needs to be able to copy a <!DOCTYPE declaration.
        // Copying a <!DOCTYPE declaration is expensive, because due to limitations of the 
        // XmlReader API, we must track the position of the writer to accurately format it. 
        // Tracking the position of the writer is expensive, as it requires examining every
        // character that is written for newline characters, and maintaining the seek position
        // of the underlying stream at each new line, which in turn requires a stream flush.
        //
        // This function must NEVER require tracking the writer position to copy the Xml nodes
        // that are used in a configuration section.
        // 
        internal bool CopyXmlNode(XmlUtilWriter utilWriter) {
            //
            // For nodes that have a closing string, such as "<element  >"
            // the XmlReader API does not give us the location of the closing string, e.g. ">".
            // To correctly determine the location of the closing part, we advance the reader,
            // determine the position of the next node, then work backwards to add whitespace
            // and add the closing string.
            //
            string close = null;
            int lineNumber = -1;
            int linePosition = -1;
 
            int readerLineNumber = 0;
            int readerLinePosition = 0;
            int writerLineNumber = 0;
            int writerLinePosition = 0;
            if (utilWriter.TrackPosition) {
                readerLineNumber = _reader.LineNumber;
                readerLinePosition = _reader.LinePosition;
                writerLineNumber = utilWriter.LineNumber;
                writerLinePosition = utilWriter.LinePosition;
            }
 
            // We test the node type in the likely order of decreasing occurrence.
            XmlNodeType nodeType = _reader.NodeType;
            if (nodeType == XmlNodeType.Whitespace) {
                utilWriter.Write(_reader.Value);
            }
            else if (nodeType == XmlNodeType.Element) {
                close = (_reader.IsEmptyElement) ? "/>" : ">";
 
                // get the line position after the element declaration:
                //      <element    attr="value"
                //              ^
                //              linePosition
                //
                lineNumber = _reader.LineNumber;
                linePosition = _reader.LinePosition + _reader.Name.Length;
 
                utilWriter.Write('<');
                utilWriter.Write(_reader.Name);
 
                //
                // Note that there is no way to get spacing between attribute name and value
                // For example:
                //
                //          <elem attr="value" />
                //
                // is reported with the same position as
                //
                //          <elem attr = "value" />
                //
                // The first example has no spaces around '=', the second example does.
                //
                while (_reader.MoveToNextAttribute()) {
                    // get line position of the attribute declaration
                    //      <element attr="value"
                    //               ^
                    //               attrLinePosition
                    //
                    int attrLineNumber = _reader.LineNumber;
                    int attrLinePosition = _reader.LinePosition;
 
                    // Write the whitespace before the attribute
                    utilWriter.AppendRequiredWhiteSpace(lineNumber, linePosition, attrLineNumber, attrLinePosition);
 
                    // Write the attribute and value
                    int charactersWritten = utilWriter.Write(_reader.Name);
                    charactersWritten += utilWriter.Write('=');
                    charactersWritten += utilWriter.AppendAttributeValue(_reader);
 
                    // Update position. Note that the attribute value is escaped to always be on a single line.
                    lineNumber = attrLineNumber;
                    linePosition = attrLinePosition + charactersWritten;
                }
            }
            else if (nodeType == XmlNodeType.EndElement) {
                close = ">";
 
                // get line position after the end element declaration:
                //      </element    >
                //               ^
                //               linePosition
                //
                lineNumber = _reader.LineNumber;
                linePosition = _reader.LinePosition + _reader.Name.Length;
 
                utilWriter.Write("</");
                utilWriter.Write(_reader.Name);
            }
            else if (nodeType == XmlNodeType.Comment) {
                utilWriter.AppendComment(_reader.Value);
            }
            else if (nodeType == XmlNodeType.Text) {
                utilWriter.AppendEscapeTextString(_reader.Value);
            }
            else if (nodeType == XmlNodeType.XmlDeclaration) {
                close = "?>";
 
                // get line position after the xml declaration:
                //      <?xml    version="1.0"
                //           ^
                //           linePosition
                //
                lineNumber = _reader.LineNumber;
                linePosition = _reader.LinePosition + 3;
 
                utilWriter.Write("<?xml");
 
                //
                // Note that there is no way to get spacing between attribute name and value
                // For example:
                //
                //          <?xml attr="value" ?>
                //
                // is reported with the same position as
                //
                //          <?xml attr = "value" ?>
                //
                // The first example has no spaces around '=', the second example does.
                //
                while (_reader.MoveToNextAttribute()) {
                    // get line position of the attribute declaration
                    //      <?xml    version="1.0"
                    //               ^
                    //               attrLinePosition
                    //
                    int attrLineNumber = _reader.LineNumber;
                    int attrLinePosition = _reader.LinePosition;
 
                    // Write the whitespace before the attribute
                    utilWriter.AppendRequiredWhiteSpace(lineNumber, linePosition, attrLineNumber, attrLinePosition);
 
                    // Write the attribute and value
                    int charactersWritten = utilWriter.Write(_reader.Name);
                    charactersWritten += utilWriter.Write('=');
                    charactersWritten += utilWriter.AppendAttributeValue(_reader);
 
                    // Update position. Note that the attribute value is escaped to always be on a single line.
                    lineNumber = attrLineNumber;
                    linePosition = attrLinePosition + charactersWritten;
                }
 
                // Position reader at beginning of node
                _reader.MoveToElement();
            }
            else if (nodeType == XmlNodeType.SignificantWhitespace) {
                utilWriter.Write(_reader.Value);
            }
            else if (nodeType == XmlNodeType.ProcessingInstruction) {
                //
                // Note that there is no way to get spacing between attribute name and value
                // For example:
                //
                //          <?pi "value" ?>
                //
                // is reported with the same position as
                //
                //          <?pi    "value" ?>
                //
                // The first example has one space between 'pi' and "value", the second has multiple spaces.
                //
                utilWriter.AppendProcessingInstruction(_reader.Name, _reader.Value);
            }
            else if (nodeType == XmlNodeType.EntityReference) {
                utilWriter.AppendEntityRef(_reader.Name);
            }
            else if (nodeType == XmlNodeType.CDATA) {
                utilWriter.AppendCData(_reader.Value);
            }
            else if (nodeType == XmlNodeType.DocumentType) {
                // 
                // XmlNodeType.DocumentType has the following format:
                //
                //      <!DOCTYPE rootElementName {(SYSTEM uriRef)|(PUBLIC id uriRef)} {[ dtdDecls ]} >
                //
                // The reader only gives us the position of 'rootElementName', so we must track what was
                // written before "<!DOCTYPE" in order to correctly determine the position of the
                // <!DOCTYPE tag
                //
                Debug.Assert(utilWriter.TrackPosition, "utilWriter.TrackPosition");
                int c = utilWriter.Write("<!DOCTYPE");
 
                // Write the space between <!DOCTYPE and the rootElementName
                utilWriter.AppendRequiredWhiteSpace(_lastLineNumber, _lastLinePosition + c, _reader.LineNumber, _reader.LinePosition);
 
                // Write the rootElementName
                utilWriter.Write(_reader.Name);
 
                // Get the dtd declarations, if any
                string dtdValue = null;
                if (_reader.HasValue) {
                    dtdValue = _reader.Value;
                }
 
                // get line position after the !DOCTYPE declaration:
                //      <!DOCTYPE  rootElement     SYSTEM rootElementDtdUri >
                //                            ^
                //                            linePosition
                lineNumber = _reader.LineNumber;
                linePosition = _reader.LinePosition + _reader.Name.Length;
 
                // Note that there is no way to get the spacing after PUBLIC or SYSTEM attributes and their values
                if (_reader.MoveToFirstAttribute()) {
                    // Write the space before SYSTEM or PUBLIC
                    utilWriter.AppendRequiredWhiteSpace(lineNumber, linePosition, _reader.LineNumber, _reader.LinePosition); 
 
                    // Write SYSTEM or PUBLIC and the 1st value of the attribute
                    string attrName = _reader.Name;
                    utilWriter.Write(attrName);
                    utilWriter.AppendSpace();
                    utilWriter.AppendAttributeValue(_reader);
                    _reader.MoveToAttribute(0);
                    
                    // If PUBLIC, write the second value of the attribute
                    if (attrName == "PUBLIC") {
                        _reader.MoveToAttribute(1);
                        utilWriter.AppendSpace();
                        utilWriter.AppendAttributeValue(_reader);
                        _reader.MoveToAttribute(1);
                    }
                }
 
                // If there is a dtd, write it
                if (dtdValue != null && dtdValue.Length > 0) {
                    utilWriter.Write(" [");
                    utilWriter.Write(dtdValue);
                    utilWriter.Write(']');
                }
 
                utilWriter.Write('>');
            }
 
            // Advance the _reader so we can get the position of the next node.
            bool moreToRead = _reader.Read();
            nodeType = _reader.NodeType;
 
            // Close the node we are copying.
            if (close != null) {
                //
                // Find the position of the close string, for example:
                //
                //          <element      >  <subElement />
                //                        ^
                //                        closeLinePosition
                //
                int startOffset = GetPositionOffset(nodeType);
                int closeLineNumber = _reader.LineNumber;
                int closeLinePosition = _reader.LinePosition - startOffset - close.Length;
 
                // Add whitespace up to the position of the close string
                utilWriter.AppendWhiteSpace(lineNumber, linePosition, closeLineNumber, closeLinePosition);
 
                // Write the close string
                utilWriter.Write(close);
            }
 
            //
            // Track the position of the reader based on the position of the reader
            // before we copied this node and what we have written in copying the node.
            // This allows us to determine the position of the <!DOCTYPE tag.
            //
            if (utilWriter.TrackPosition) {
                _lastLineNumber = (readerLineNumber - writerLineNumber) + utilWriter.LineNumber;
 
                if (writerLineNumber == utilWriter.LineNumber) {
                    _lastLinePosition = (readerLinePosition - writerLinePosition) + utilWriter.LinePosition;
                }
                else {
                    _lastLinePosition = utilWriter.LinePosition;
                }
            }
 
            return moreToRead;
        }
 
        // RetrieveFullOpenElementTag
        //
        // Asuming that we are at an element, retrieve the text for that element
        // and attributes that can be serialized to an xml file.
        //
        private string RetrieveFullOpenElementTag() {
            StringBuilder element;
 
            Debug.Assert(_reader.NodeType == XmlNodeType.Element, 
                         "_reader.NodeType == NodeType.Element");
 
            // Start with element tag name
            element = new StringBuilder(64);
            element.Append("<");
            element.Append(_reader.Name);
 
            // Add attributes
            while (_reader.MoveToNextAttribute()) {
 
                element.Append(" ");
                element.Append(_reader.Name);
                element.Append("=");
                element.Append('\"');
                element.Append(_reader.Value);
                element.Append('\"');
            }
 
            // Now close the element tag
            element.Append(">");
 
            return element.ToString();
        }
        
        //
        // Copy or replace an element node.
        // If the element is an empty element, replace it with a formatted start element if either:
        //   * The contents of the start element string need updating.
        //   * The element needs to contain child elements.
        //
        // If the element is empty and is replaced with a start/end element pair, return a 
        // end element string with whitespace formatting; otherwise return null.
        //
        internal string UpdateStartElement(XmlUtilWriter utilWriter, string updatedStartElement, bool needsChildren, int linePosition, int indent) {
            Debug.Assert(_reader.NodeType == XmlNodeType.Element, "_reader.NodeType == NodeType.Element");
 
            string  endElement = null;
            bool    needsEndElement = false;
            string  elementName;
 
            elementName = _reader.Name;
 
            // If the element is empty, determine if a new end element is needed.
            if (_reader.IsEmptyElement) {
                if (updatedStartElement == null && needsChildren) {
                    updatedStartElement = RetrieveFullOpenElementTag();
                }
 
                needsEndElement = (updatedStartElement != null);
            }
 
            if (updatedStartElement == null) {
                //
                // If no changes to the start element are required, just copy it.
                //
                CopyXmlNode(utilWriter);
            }
            else {
                //
                // Format a new start element/end element pair
                //
                string updatedEndElement = "</" + elementName + ">";
                string updatedElement = updatedStartElement + updatedEndElement;
                string formattedElement = FormatXmlElement(updatedElement, linePosition, indent, true);
 
                //
                // Get the start and end element strings from the formatted element.
                //
                int iEndElement = formattedElement.LastIndexOf('\n') + 1;
                string startElement;
                if (needsEndElement) {
                    endElement = formattedElement.Substring(iEndElement);
 
                    // Include a newline in the start element as we are expanding an empty element.
                    startElement = formattedElement.Substring(0, iEndElement);
                }
                else {
                    // Omit the newline from the start element.
                    startElement = formattedElement.Substring(0, iEndElement - 2);
                }
 
                // Write the new start element.
                utilWriter.Write(startElement);
 
                // Skip over the existing start element.
                _reader.Read();
            }
 
            return endElement;
        }
 
        //
        // Create the cached string writer if it does not exist, 
        // otherwise reuse the existing buffer.
        // 
        private void ResetCachedStringWriter() {
            if (_cachedStringWriter == null) {
                _cachedStringWriter = new StringWriter(new StringBuilder(64), CultureInfo.InvariantCulture);
            }
            else {
                _cachedStringWriter.GetStringBuilder().Length = 0;
            }
        }
 
        //
        // Copy a configuration section to a string, and advance the reader.
        //
        internal string CopySection() {
            ResetCachedStringWriter();
 
            // Preserve whitespace for sections for backcompat
            WhitespaceHandling originalHandling = _reader.WhitespaceHandling;
            _reader.WhitespaceHandling = WhitespaceHandling.All;
 
            // Create string writer to write to
            XmlUtilWriter utilWriter = new XmlUtilWriter(_cachedStringWriter, false);
 
            // Copy the element
            CopyElement(utilWriter);
 
            // Reset whitespace handling
            _reader.WhitespaceHandling = originalHandling;
 
            if ((originalHandling == WhitespaceHandling.None) &&
                 (Reader.NodeType  == XmlNodeType.Whitespace))  {
                // If we were previously suppose to skip whitespace, and now we
                // are at it, then lets jump to the next item
                _reader.Read();
            }
 
            utilWriter.Flush();
            string s = ((StringWriter)utilWriter.Writer).ToString();
            return s;
        }
 
        // Format an Xml element to be written to the config file.
        // Params:
        //   xmlElement      - the element
        //   linePosition    - start position of the element
        //   indent          - indent for each depth
        //   skipFirstIndent - skip indent for the first element?
        //
        [SuppressMessage("Microsoft.Security.Xml", "CA3054:DoNotAllowDtdOnXmlTextReader", Justification="Reading trusted input")]
        static internal string FormatXmlElement(string xmlElement, int linePosition, int indent, bool skipFirstIndent) {
 
            XmlParserContext context = new XmlParserContext(null, null, null, XmlSpace.Default, Encoding.Unicode);
            XmlTextReader reader = new XmlTextReader(xmlElement, XmlNodeType.Element, context);
 
            StringWriter stringWriter = new StringWriter(new StringBuilder(64), CultureInfo.InvariantCulture);
            XmlUtilWriter utilWriter = new XmlUtilWriter(stringWriter, false);
 
            // append newline before indent?
            bool newLine = false;
 
            // last node visited was text?
            bool lastWasText = false;
 
            // width of line from end of indentation
            int lineWidth;
 
            // length of the stringbuilder after last indent with newline
            int sbLengthLastNewLine = 0;
 
            while (reader.Read()) {
                XmlNodeType nodeType = reader.NodeType;
 
                if (lastWasText) {
                    utilWriter.Flush();
                    lineWidth = sbLengthLastNewLine - ((StringWriter)utilWriter.Writer).GetStringBuilder().Length;
                }
                else {
                    lineWidth = 0;
                }
 
                switch (nodeType) {
                    case XmlNodeType.CDATA:
                    case XmlNodeType.Element:
                    case XmlNodeType.EndElement:
                    case XmlNodeType.Comment:
                        // Do not indent if the last node was text - doing so would add whitespace
                        // that is included as part of the text.
                        if (!skipFirstIndent && !lastWasText) {
                            utilWriter.AppendIndent(linePosition, indent, reader.Depth, newLine);
 
                            if (newLine) {
                                utilWriter.Flush();
                                sbLengthLastNewLine = ((StringWriter)utilWriter.Writer).GetStringBuilder().Length;
                            }
                        }
                        break;
 
                    default:
                        break;
                }
 
                lastWasText = false;
                switch (nodeType) {
                    case XmlNodeType.Whitespace:
                        break;
 
                    case XmlNodeType.SignificantWhitespace:
                        utilWriter.Write(reader.Value);
                        break;
 
                    case XmlNodeType.CDATA:
                        utilWriter.AppendCData(reader.Value);
                        break;
 
                    case XmlNodeType.ProcessingInstruction:
                        utilWriter.AppendProcessingInstruction(reader.Name, reader.Value);
                        break;
 
                    case XmlNodeType.Comment:
                        utilWriter.AppendComment(reader.Value);
                        break;
 
                    case XmlNodeType.Text:
                        utilWriter.AppendEscapeTextString(reader.Value);
                        lastWasText = true;
                        break;
 
                    case XmlNodeType.Element:
                        {
                            // Write "<elem"
                            utilWriter.Write('<');
                            utilWriter.Write(reader.Name);
 
                            lineWidth += reader.Name.Length + 2;
 
                            int c = reader.AttributeCount;
                            for (int i = 0; i < c; i++) {
                                // Add new line if we've exceeded the line width
                                bool writeSpace;
                                if (lineWidth > MAX_LINE_WIDTH) {
                                    utilWriter.AppendIndent(linePosition, indent, reader.Depth - 1, true);
                                    lineWidth = indent;
                                    writeSpace = false;
                                    utilWriter.Flush();
                                    sbLengthLastNewLine = ((StringWriter)utilWriter.Writer).GetStringBuilder().Length;
                                }
                                else {
                                    writeSpace = true;
                                }
 
                                // Write the attribute
                                reader.MoveToNextAttribute();
                                utilWriter.Flush();
                                int startLength = ((StringWriter)utilWriter.Writer).GetStringBuilder().Length;
                                if (writeSpace) {
                                    utilWriter.AppendSpace();
                                }
 
                                utilWriter.Write(reader.Name);
                                utilWriter.Write('=');
                                utilWriter.AppendAttributeValue(reader);
                                utilWriter.Flush();
                                lineWidth += ((StringWriter)utilWriter.Writer).GetStringBuilder().Length - startLength;
                            }
                        }
 
                        // position reader back on element
                        reader.MoveToElement();
 
                        // write closing tag
                        if (reader.IsEmptyElement) {
                            utilWriter.Write(" />");
                        }
                        else {
                            utilWriter.Write('>');
                        }
 
                        break;
 
                    case XmlNodeType.EndElement:
                        utilWriter.Write("</");
                        utilWriter.Write(reader.Name);
                        utilWriter.Write('>');
                        break;
 
                    case XmlNodeType.EntityReference:
                        utilWriter.AppendEntityRef(reader.Name);
                        break;
 
                    // Ignore <?xml and <!DOCTYPE nodes
                    case XmlNodeType.XmlDeclaration:
                    case XmlNodeType.DocumentType:
                    default:
                        break;
                }
 
                // put each new element on a new line
                newLine = true;
 
                // do not skip any more indents
                skipFirstIndent = false;
            }
 
            utilWriter.Flush();
            string s = ((StringWriter)utilWriter.Writer).ToString();
            return s;
        }
    }
}