1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
4 
5 using System.Configuration.Internal;
6 using System.Diagnostics;
7 using System.Globalization;
8 using System.IO;
9 using System.Text;
10 using System.Xml;
11 
12 namespace System.Configuration
13 {
14     // XmlTextReader Helper class.
15     //
16     // Provides the following services:
17     //
18     //      * Reader methods that verify restrictions on the XML that can be contained in a config file.
19     //      * Methods to copy the reader stream to a writer stream.
20     //      * Method to copy a configuration section to a string.
21     //      * Methods to format a string of XML.
22     //
23     // Errors found during read are accumulated in a ConfigurationSchemaErrors object.
24     internal sealed class XmlUtil : IDisposable, IConfigErrorInfo
25     {
26         private const int MaxLineWidth = 60;
27 
28         // Offset from where the reader reports the LinePosition of an Xml Node to
29         // the start of that representation in text.
30         private static readonly int[] s_positionOffset =
31         {
32             0,  // None,
33             1,  // Element,                 <elem
34             -1, // Attribute,               N/A
35             0,  // Text,
36             9,  // CDATA,                   <![CDATA[
37             1,  // EntityReference,         &lt
38             -1, // Entity,                  N/A
39             2,  // ProcessingInstruction,   <?pi
40             4,  // Comment,                 <!--
41             -1, // Document,                N/A
42             10, // DocumentType,            <!DOCTYPE
43             -1, // DocumentFragment,        N/A
44             -1, // Notation,                N/A
45             0,  // Whitespace,
46             0,  // SignificantWhitespace,
47             2,  // EndElement,              />
48             -1, // EndEntity,               N/A
49             2,  // XmlDeclaration           <?xml
50         };
51 
52         private StringWriter _cachedStringWriter;
53         private int _lastLineNumber;
54         private int _lastLinePosition;
55 
56         private Stream _stream;
57 
XmlUtil(Stream stream, string name, bool readToFirstElement)58         internal XmlUtil(Stream stream, string name, bool readToFirstElement) :
59             this(stream, name, readToFirstElement, new ConfigurationSchemaErrors())
60         { }
61 
XmlUtil(Stream stream, string name, bool readToFirstElement, ConfigurationSchemaErrors schemaErrors)62         internal XmlUtil(Stream stream, string name, bool readToFirstElement, ConfigurationSchemaErrors schemaErrors)
63         {
64             try
65             {
66                 Filename = name;
67                 _stream = stream;
68                 Reader = new XmlTextReader(_stream) { XmlResolver = null };
69 
70                 // config reads never require a resolver
71 
72                 SchemaErrors = schemaErrors;
73                 _lastLineNumber = 1;
74                 _lastLinePosition = 1;
75 
76                 // When parsing config that we don't intend to copy, skip all content
77                 // before the first element.
78                 if (!readToFirstElement) return;
79                 Reader.WhitespaceHandling = WhitespaceHandling.None;
80 
81                 bool done = false;
82                 while (!done && Reader.Read())
83                     switch (Reader.NodeType)
84                     {
85                         case XmlNodeType.XmlDeclaration:
86                         case XmlNodeType.Comment:
87                         case XmlNodeType.DocumentType:
88                             break;
89                         case XmlNodeType.Element:
90                             done = true;
91                             break;
92                         default:
93                             throw new ConfigurationErrorsException(SR.Config_base_unrecognized_element, this);
94                     }
95             }
96             catch
97             {
98                 ReleaseResources();
99                 throw;
100             }
101         }
102 
103         // Return the line position of the reader, compensating for the reader's offset
104         // for nodes such as an XmlElement.
105         internal int TrueLinePosition
106         {
107             get
108             {
109                 int trueLinePosition = Reader.LinePosition - GetPositionOffset(Reader.NodeType);
110                 Debug.Assert(trueLinePosition > 0, "trueLinePosition > 0");
111                 return trueLinePosition;
112             }
113         }
114 
115         internal XmlTextReader Reader { get; private set; }
116 
117         internal ConfigurationSchemaErrors SchemaErrors { get; }
118 
119         public string Filename { get; }
120 
121         public int LineNumber => Reader.LineNumber;
122 
Dispose()123         public void Dispose()
124         {
125             ReleaseResources();
126         }
127 
GetPositionOffset(XmlNodeType nodeType)128         private static int GetPositionOffset(XmlNodeType nodeType)
129         {
130             return s_positionOffset[(int)nodeType];
131         }
132 
ReleaseResources()133         private void ReleaseResources()
134         {
135             if (Reader != null)
136             {
137                 // closing _reader will also close underlying _stream
138                 Reader.Close();
139                 Reader = null;
140             }
141             else
142                 _stream?.Close();
143 
144             _stream = null;
145 
146             if (_cachedStringWriter != null)
147             {
148                 _cachedStringWriter.Close();
149                 _cachedStringWriter = null;
150             }
151         }
152 
153         // Read until the Next Element element, or we hit
154         // the end of the file.
ReadToNextElement()155         internal void ReadToNextElement()
156         {
157             while (Reader.Read())
158                 if (Reader.MoveToContent() == XmlNodeType.Element)
159                 {
160                     // We found an element, so return
161                     return;
162                 }
163             // We must of hit end of file
164         }
165 
166         /// <summary>
167         /// Skip this element and its children, then read to next start element,
168         /// or until we hit end of file.
169         /// </summary>
SkipToNextElement()170         internal void SkipToNextElement()
171         {
172             Reader.Skip();
173             Reader.MoveToContent();
174 
175             while (!Reader.EOF && (Reader.NodeType != XmlNodeType.Element))
176             {
177                 Reader.Read();
178                 Reader.MoveToContent();
179             }
180         }
181 
182         /// <summary>
183         /// Read to the next start element, and verify that all XML nodes read are permissible.
184         /// </summary>
StrictReadToNextElement(ExceptionAction action)185         internal void StrictReadToNextElement(ExceptionAction action)
186         {
187             while (Reader.Read())
188             {
189                 // optimize for the common case
190                 if (Reader.NodeType == XmlNodeType.Element) return;
191 
192                 VerifyIgnorableNodeType(action);
193             }
194         }
195 
196         /// <summary>
197         /// Skip this element and its children, then read to next start element, or until we hit
198         /// end of file. Verify that nodes that are read after the skipped element are permissible.
199         /// </summary>
StrictSkipToNextElement(ExceptionAction action)200         internal void StrictSkipToNextElement(ExceptionAction action)
201         {
202             Reader.Skip();
203 
204             while (!Reader.EOF && (Reader.NodeType != XmlNodeType.Element))
205             {
206                 VerifyIgnorableNodeType(action);
207                 Reader.Read();
208             }
209         }
210 
211         /// <summary>
212         /// Skip until we hit the end element for our parent, and verify that nodes at the
213         /// parent level are permissible.
214         /// </summary>
StrictSkipToOurParentsEndElement(ExceptionAction action)215         internal void StrictSkipToOurParentsEndElement(ExceptionAction action)
216         {
217             int currentDepth = Reader.Depth;
218 
219             // Skip everything at out current level
220             while (Reader.Depth >= currentDepth) Reader.Skip();
221 
222             while (!Reader.EOF && (Reader.NodeType != XmlNodeType.EndElement))
223             {
224                 VerifyIgnorableNodeType(action);
225                 Reader.Read();
226             }
227         }
228 
229         /// <summary>
230         /// Add an error if the node type is not permitted by the configuration schema.
231         /// </summary>
VerifyIgnorableNodeType(ExceptionAction action)232         internal void VerifyIgnorableNodeType(ExceptionAction action)
233         {
234             XmlNodeType nodeType = Reader.NodeType;
235 
236             if ((nodeType != XmlNodeType.Comment) && (nodeType != XmlNodeType.EndElement))
237             {
238                 ConfigurationException ex = new ConfigurationErrorsException(
239                     SR.Config_base_unrecognized_element,
240                     this);
241 
242                 SchemaErrors.AddError(ex, action);
243             }
244         }
245 
246         /// <summary>
247         /// Add an error if there are attributes that have not been examined, and are therefore unrecognized.
248         /// </summary>
VerifyNoUnrecognizedAttributes(ExceptionAction action)249         internal void VerifyNoUnrecognizedAttributes(ExceptionAction action)
250         {
251             if (Reader.MoveToNextAttribute())
252                 AddErrorUnrecognizedAttribute(action);
253         }
254 
255         /// <summary>
256         /// Add an error if the retrieved attribute is null, and therefore not present.
257         /// </summary>
VerifyRequiredAttribute(object requiredAttribute, string attrName, ExceptionAction action)258         internal bool VerifyRequiredAttribute(object requiredAttribute, string attrName, ExceptionAction action)
259         {
260             if (requiredAttribute == null)
261             {
262                 AddErrorRequiredAttribute(attrName, action);
263                 return false;
264             }
265             else
266             {
267                 return true;
268             }
269         }
270 
AddErrorUnrecognizedAttribute(ExceptionAction action)271         internal void AddErrorUnrecognizedAttribute(ExceptionAction action)
272         {
273             ConfigurationErrorsException ex = new ConfigurationErrorsException(
274                 string.Format(SR.Config_base_unrecognized_attribute, Reader.Name),
275                 this);
276 
277             SchemaErrors.AddError(ex, action);
278         }
279 
AddErrorRequiredAttribute(string attrib, ExceptionAction action)280         internal void AddErrorRequiredAttribute(string attrib, ExceptionAction action)
281         {
282             ConfigurationErrorsException ex = new ConfigurationErrorsException(
283                 string.Format(SR.Config_missing_required_attribute, attrib, Reader.Name),
284                 this);
285 
286             SchemaErrors.AddError(ex, action);
287         }
288 
AddErrorReservedAttribute(ExceptionAction action)289         internal void AddErrorReservedAttribute(ExceptionAction action)
290         {
291             ConfigurationErrorsException ex = new ConfigurationErrorsException(
292                 string.Format(SR.Config_reserved_attribute, Reader.Name),
293                 this);
294 
295             SchemaErrors.AddError(ex, action);
296         }
297 
AddErrorUnrecognizedElement(ExceptionAction action)298         internal void AddErrorUnrecognizedElement(ExceptionAction action)
299         {
300             ConfigurationErrorsException ex = new ConfigurationErrorsException(
301                 SR.Config_base_unrecognized_element,
302                 this);
303 
304             SchemaErrors.AddError(ex, action);
305         }
306 
VerifyAndGetNonEmptyStringAttribute(ExceptionAction action, out string newValue)307         internal void VerifyAndGetNonEmptyStringAttribute(ExceptionAction action, out string newValue)
308         {
309             if (!string.IsNullOrEmpty(Reader.Value)) newValue = Reader.Value;
310             else
311             {
312                 newValue = null;
313 
314                 ConfigurationException ex = new ConfigurationErrorsException(
315                     string.Format(SR.Empty_attribute, Reader.Name),
316                     this);
317 
318                 SchemaErrors.AddError(ex, action);
319             }
320         }
321 
322         /// <summary>
323         /// Verify and Retrieve the Boolean Attribute.  If it is not
324         /// a valid value then log an error and set the value to a given default.
325         /// </summary>
VerifyAndGetBooleanAttribute( ExceptionAction action, bool defaultValue, out bool newValue)326         internal void VerifyAndGetBooleanAttribute(
327             ExceptionAction action, bool defaultValue, out bool newValue)
328         {
329             switch (Reader.Value)
330             {
331                 case "true":
332                     newValue = true;
333                     break;
334                 case "false":
335                     newValue = false;
336                     break;
337                 default:
338                     newValue = defaultValue;
339                     SchemaErrors.AddError(
340                         new ConfigurationErrorsException(string.Format(SR.Config_invalid_boolean_attribute, Reader.Name), this),
341                         action);
342                     break;
343             }
344         }
345 
346         // Copy an XML element, then continue copying until we've hit the next element
347         // or exited this depth.
CopyOuterXmlToNextElement(XmlUtilWriter utilWriter, bool limitDepth)348         internal bool CopyOuterXmlToNextElement(XmlUtilWriter utilWriter, bool limitDepth)
349         {
350             CopyElement(utilWriter);
351 
352             // Copy until reaching the next element, or if limitDepth == true until we've exited this depth.
353             return CopyReaderToNextElement(utilWriter, limitDepth);
354         }
355 
356         // Copy an XML element but skip all its child elements, then continue copying until we've hit the next element.
SkipChildElementsAndCopyOuterXmlToNextElement(XmlUtilWriter utilWriter)357         internal bool SkipChildElementsAndCopyOuterXmlToNextElement(XmlUtilWriter utilWriter)
358         {
359             bool isEmptyElement = Reader.IsEmptyElement;
360             int startingLine = Reader.LineNumber;
361 #if DEBUG
362             int depth = Reader.Depth;
363 #endif
364 
365             Debug.Assert(Reader.NodeType == XmlNodeType.Element, "Reader.NodeType == XmlNodeType.Element");
366 
367             CopyXmlNode(utilWriter);
368 
369             // See if we need to skip any child element
370             if (!isEmptyElement)
371             {
372                 while (Reader.NodeType != XmlNodeType.EndElement)
373                     if (Reader.NodeType == XmlNodeType.Element)
374                     {
375                         Reader.Skip();
376 
377                         // We need to skip all the whitespace following a skipped element.
378                         // - If the whitespace doesn't contain /r/n, then it's okay to skip them
379                         //   as part of the element.
380                         // - If the whitespace contains /r/n, not skipping them will result
381                         //   in a redundant emtpy line being copied.
382                         if (Reader.NodeType == XmlNodeType.Whitespace) Reader.Skip();
383                     }
384                     else
385                     {
386                         // We want to preserve other content, e.g. comments.
387                         CopyXmlNode(utilWriter);
388                     }
389 
390                 if (Reader.LineNumber != startingLine)
391                 {
392                     // The whitespace in front of the EndElement was skipped above.
393                     // We need to append spaces to compensate for that.
394                     utilWriter.AppendSpacesToLinePosition(TrueLinePosition);
395                 }
396 
397 #if DEBUG
398                 Debug.Assert(Reader.Depth == depth, "We should be at the same depth as the opening Element");
399 #endif
400 
401                 // Copy the end element.
402                 CopyXmlNode(utilWriter);
403             }
404 
405             return CopyReaderToNextElement(utilWriter, true);
406         }
407 
408         // Copy the reader until we hit an element, or we've exited the current depth.
CopyReaderToNextElement(XmlUtilWriter utilWriter, bool limitDepth)409         internal bool CopyReaderToNextElement(XmlUtilWriter utilWriter, bool limitDepth)
410         {
411             bool moreToRead = true;
412 
413             // Set the depth if we limit copying to this depth
414             int depth;
415             if (limitDepth)
416             {
417                 // there is nothing in the element
418                 if (Reader.NodeType == XmlNodeType.EndElement)
419                     return true;
420 
421                 depth = Reader.Depth;
422             }
423             else depth = 0;
424 
425             // Copy nodes until we've reached the desired depth, or until we hit an element.
426             do
427             {
428                 if (Reader.NodeType == XmlNodeType.Element)
429                     break;
430 
431                 if (Reader.Depth < depth) break;
432 
433                 moreToRead = CopyXmlNode(utilWriter);
434             } while (moreToRead);
435 
436             return moreToRead;
437         }
438 
439         // Skip over the current element and copy until the next element.
440         // This function removes the one blank line that would otherwise
441         // be inserted by simply skipping and copying to the next element
442         // in a situation like this:
443         //
444         //      <!-- end of previous configSection -->
445         //      <configSectionToDelete>
446         //          <content />
447         //          <moreContent />
448         //      </configSectionToDelete>
449         //      <!-- end of configSectionToDelete -->
450         //      <nextConfigSection />
SkipAndCopyReaderToNextElement(XmlUtilWriter utilWriter, bool limitDepth)451         internal bool SkipAndCopyReaderToNextElement(XmlUtilWriter utilWriter, bool limitDepth)
452         {
453             Debug.Assert(Reader.NodeType == XmlNodeType.Element, "_reader.NodeType == XmlNodeType.Element");
454 
455             // If the last line before the element is not blank, then we do not have to
456             // remove the blank line.
457             if (!utilWriter.IsLastLineBlank)
458             {
459                 Reader.Skip();
460                 return CopyReaderToNextElement(utilWriter, limitDepth);
461             }
462 
463             // Set the depth if we limit copying to this depth
464             int depth = limitDepth ? Reader.Depth : 0;
465 
466             // Skip over the element
467             Reader.Skip();
468 
469             int lineNumberOfEndElement = Reader.LineNumber;
470 
471             // Read until we hit a non-whitespace node or reach the end
472             while (!Reader.EOF)
473             {
474                 if (Reader.NodeType != XmlNodeType.Whitespace)
475                 {
476                     // If the next non-whitepace node is on another line,
477                     // seek back to the beginning of the current blank line,
478                     // skip a blank line of whitespace, and copy the remaining whitespace.
479                     if (Reader.LineNumber > lineNumberOfEndElement)
480                     {
481                         utilWriter.SeekToLineStart();
482                         utilWriter.AppendWhiteSpace(lineNumberOfEndElement + 1, 1, LineNumber, TrueLinePosition);
483                     }
484 
485                     break;
486                 }
487 
488                 Reader.Read();
489             }
490 
491             // Copy nodes until we've reached the desired depth, or until we hit an element.
492             while (!Reader.EOF)
493             {
494                 if (Reader.NodeType == XmlNodeType.Element)
495                     break;
496 
497                 if (Reader.Depth < depth) break;
498 
499                 CopyXmlNode(utilWriter);
500             }
501 
502             return !Reader.EOF;
503         }
504 
505         // Copy an XML element and its children, up to and including the end element.
CopyElement(XmlUtilWriter utilWriter)506         private void CopyElement(XmlUtilWriter utilWriter)
507         {
508             Debug.Assert(Reader.NodeType == XmlNodeType.Element, "_reader.NodeType== XmlNodeType.Element");
509 
510             int depth = Reader.Depth;
511             bool isEmptyElement = Reader.IsEmptyElement;
512 
513             // Copy current node
514             CopyXmlNode(utilWriter);
515 
516             // Copy nodes while the depth is greater than the current depth.
517             while (Reader.Depth > depth) CopyXmlNode(utilWriter);
518 
519             // Copy the end element.
520             if (!isEmptyElement) CopyXmlNode(utilWriter);
521         }
522 
523         // Copy a single XML node, attempting to preserve whitespace.
524         // A side effect of this method is to advance the reader to the next node.
525         //
526         // PERFORMANCE NOTE: this function is used at runtime to copy a configuration section,
527         // and at designtime to copy an entire XML document.
528         //
529         // At designtime, this function needs to be able to copy a <!DOCTYPE declaration.
530         // Copying a <!DOCTYPE declaration is expensive, because due to limitations of the
531         // XmlReader API, we must track the position of the writer to accurately format it.
532         // Tracking the position of the writer is expensive, as it requires examining every
533         // character that is written for newline characters, and maintaining the seek position
534         // of the underlying stream at each new line, which in turn requires a stream flush.
535         //
536         // This function must NEVER require tracking the writer position to copy the Xml nodes
537         // that are used in a configuration section.
CopyXmlNode(XmlUtilWriter utilWriter)538         internal bool CopyXmlNode(XmlUtilWriter utilWriter)
539         {
540             // For nodes that have a closing string, such as "<element  >"
541             // the XmlReader API does not give us the location of the closing string, e.g. ">".
542             // To correctly determine the location of the closing part, we advance the reader,
543             // determine the position of the next node, then work backwards to add whitespace
544             // and add the closing string.
545             string close = null;
546             int lineNumber = -1;
547             int linePosition = -1;
548 
549             int readerLineNumber = 0;
550             int readerLinePosition = 0;
551             int writerLineNumber = 0;
552             int writerLinePosition = 0;
553             if (utilWriter.TrackPosition)
554             {
555                 readerLineNumber = Reader.LineNumber;
556                 readerLinePosition = Reader.LinePosition;
557                 writerLineNumber = utilWriter.LineNumber;
558                 writerLinePosition = utilWriter.LinePosition;
559             }
560 
561             // We test the node type in the likely order of decreasing occurrence.
562             XmlNodeType nodeType = Reader.NodeType;
563             if (nodeType == XmlNodeType.Whitespace) utilWriter.Write(Reader.Value);
564             else
565             {
566                 if (nodeType == XmlNodeType.Element)
567                 {
568                     close = Reader.IsEmptyElement ? "/>" : ">";
569 
570                     // get the line position after the element declaration:
571                     //      <element    attr="value"
572                     //              ^
573                     //              linePosition
574                     lineNumber = Reader.LineNumber;
575                     linePosition = Reader.LinePosition + Reader.Name.Length;
576 
577                     utilWriter.Write('<');
578                     utilWriter.Write(Reader.Name);
579 
580                     // Note that there is no way to get spacing between attribute name and value
581                     // For example:
582                     //
583                     //          <elem attr="value" />
584                     //
585                     // is reported with the same position as
586                     //
587                     //          <elem attr = "value" />
588                     //
589                     // The first example has no spaces around '=', the second example does.
590                     while (Reader.MoveToNextAttribute())
591                     {
592                         // get line position of the attribute declaration
593                         //      <element attr="value"
594                         //               ^
595                         //               attrLinePosition
596                         int attrLineNumber = Reader.LineNumber;
597                         int attrLinePosition = Reader.LinePosition;
598 
599                         // Write the whitespace before the attribute
600                         utilWriter.AppendRequiredWhiteSpace(lineNumber, linePosition, attrLineNumber, attrLinePosition);
601 
602                         // Write the attribute and value
603                         int charactersWritten = utilWriter.Write(Reader.Name);
604                         charactersWritten += utilWriter.Write('=');
605                         charactersWritten += utilWriter.AppendAttributeValue(Reader);
606 
607                         // Update position. Note that the attribute value is escaped to always be on a single line.
608                         lineNumber = attrLineNumber;
609                         linePosition = attrLinePosition + charactersWritten;
610                     }
611                 }
612                 else
613                 {
614                     if (nodeType == XmlNodeType.EndElement)
615                     {
616                         close = ">";
617 
618                         // get line position after the end element declaration:
619                         //      </element    >
620                         //               ^
621                         //               linePosition
622                         lineNumber = Reader.LineNumber;
623                         linePosition = Reader.LinePosition + Reader.Name.Length;
624 
625                         utilWriter.Write("</");
626                         utilWriter.Write(Reader.Name);
627                     }
628                     else
629                     {
630                         if (nodeType == XmlNodeType.Comment) utilWriter.AppendComment(Reader.Value);
631                         else
632                         {
633                             if (nodeType == XmlNodeType.Text) utilWriter.AppendEscapeTextString(Reader.Value);
634                             else
635                             {
636                                 if (nodeType == XmlNodeType.XmlDeclaration)
637                                 {
638                                     close = "?>";
639 
640                                     // get line position after the xml declaration:
641                                     //      <?xml    version="1.0"
642                                     //           ^
643                                     //           linePosition
644                                     lineNumber = Reader.LineNumber;
645                                     linePosition = Reader.LinePosition + 3;
646 
647                                     utilWriter.Write("<?xml");
648 
649                                     // Note that there is no way to get spacing between attribute name and value
650                                     // For example:
651                                     //
652                                     //          <?xml attr="value" ?>
653                                     //
654                                     // is reported with the same position as
655                                     //
656                                     //          <?xml attr = "value" ?>
657                                     //
658                                     // The first example has no spaces around '=', the second example does.
659                                     while (Reader.MoveToNextAttribute())
660                                     {
661                                         // get line position of the attribute declaration
662                                         //      <?xml    version="1.0"
663                                         //               ^
664                                         //               attrLinePosition
665                                         int attrLineNumber = Reader.LineNumber;
666                                         int attrLinePosition = Reader.LinePosition;
667 
668                                         // Write the whitespace before the attribute
669                                         utilWriter.AppendRequiredWhiteSpace(lineNumber, linePosition, attrLineNumber,
670                                             attrLinePosition);
671 
672                                         // Write the attribute and value
673                                         int charactersWritten = utilWriter.Write(Reader.Name);
674                                         charactersWritten += utilWriter.Write('=');
675                                         charactersWritten += utilWriter.AppendAttributeValue(Reader);
676 
677                                         // Update position. Note that the attribute value is escaped to always be on a single line.
678                                         lineNumber = attrLineNumber;
679                                         linePosition = attrLinePosition + charactersWritten;
680                                     }
681 
682                                     // Position reader at beginning of node
683                                     Reader.MoveToElement();
684                                 }
685                                 else
686                                 {
687                                     if (nodeType == XmlNodeType.SignificantWhitespace) utilWriter.Write(Reader.Value);
688                                     else
689                                     {
690                                         if (nodeType == XmlNodeType.ProcessingInstruction)
691                                         {
692                                             // Note that there is no way to get spacing between attribute name and value
693                                             // For example:
694                                             //
695                                             //          <?pi "value" ?>
696                                             //
697                                             // is reported with the same position as
698                                             //
699                                             //          <?pi    "value" ?>
700                                             //
701                                             // The first example has one space between 'pi' and "value", the second has multiple spaces.
702                                             utilWriter.AppendProcessingInstruction(Reader.Name, Reader.Value);
703                                         }
704                                         else
705                                         {
706                                             if (nodeType == XmlNodeType.EntityReference)
707                                                 utilWriter.AppendEntityRef(Reader.Name);
708                                             else
709                                             {
710                                                 if (nodeType == XmlNodeType.CDATA)
711                                                     utilWriter.AppendCData(Reader.Value);
712                                                 else
713                                                 {
714                                                     if (nodeType == XmlNodeType.DocumentType)
715                                                     {
716                                                         // XmlNodeType.DocumentType has the following format:
717                                                         //
718                                                         //      <!DOCTYPE rootElementName {(SYSTEM uriRef)|(PUBLIC id uriRef)} {[ dtdDecls ]} >
719                                                         //
720                                                         // The reader only gives us the position of 'rootElementName', so we must track what was
721                                                         // written before "<!DOCTYPE" in order to correctly determine the position of the
722                                                         // <!DOCTYPE tag
723                                                         Debug.Assert(utilWriter.TrackPosition,
724                                                             "utilWriter.TrackPosition");
725                                                         int c = utilWriter.Write("<!DOCTYPE");
726 
727                                                         // Write the space between <!DOCTYPE and the rootElementName
728                                                         utilWriter.AppendRequiredWhiteSpace(_lastLineNumber,
729                                                             _lastLinePosition + c, Reader.LineNumber,
730                                                             Reader.LinePosition);
731 
732                                                         // Write the rootElementName
733                                                         utilWriter.Write(Reader.Name);
734 
735                                                         // Get the dtd declarations, if any
736                                                         string dtdValue = null;
737                                                         if (Reader.HasValue) dtdValue = Reader.Value;
738 
739                                                         // get line position after the !DOCTYPE declaration:
740                                                         //      <!DOCTYPE  rootElement     SYSTEM rootElementDtdUri >
741                                                         //                            ^
742                                                         //                            linePosition
743                                                         lineNumber = Reader.LineNumber;
744                                                         linePosition = Reader.LinePosition + Reader.Name.Length;
745 
746                                                         // Note that there is no way to get the spacing after PUBLIC or SYSTEM attributes and their values
747                                                         if (Reader.MoveToFirstAttribute())
748                                                         {
749                                                             // Write the space before SYSTEM or PUBLIC
750                                                             utilWriter.AppendRequiredWhiteSpace(lineNumber, linePosition,
751                                                                 Reader.LineNumber, Reader.LinePosition);
752 
753                                                             // Write SYSTEM or PUBLIC and the 1st value of the attribute
754                                                             string attrName = Reader.Name;
755                                                             utilWriter.Write(attrName);
756                                                             utilWriter.AppendSpace();
757                                                             utilWriter.AppendAttributeValue(Reader);
758                                                             Reader.MoveToAttribute(0);
759 
760                                                             // If PUBLIC, write the second value of the attribute
761                                                             if (attrName == "PUBLIC")
762                                                             {
763                                                                 Reader.MoveToAttribute(1);
764                                                                 utilWriter.AppendSpace();
765                                                                 utilWriter.AppendAttributeValue(Reader);
766                                                                 Reader.MoveToAttribute(1);
767                                                             }
768                                                         }
769 
770                                                         // If there is a dtd, write it
771                                                         if (!string.IsNullOrEmpty(dtdValue))
772                                                         {
773                                                             utilWriter.Write(" [");
774                                                             utilWriter.Write(dtdValue);
775                                                             utilWriter.Write(']');
776                                                         }
777 
778                                                         utilWriter.Write('>');
779                                                     }
780                                                 }
781                                             }
782                                         }
783                                     }
784                                 }
785                             }
786                         }
787                     }
788                 }
789             }
790 
791             // Advance the _reader so we can get the position of the next node.
792             bool moreToRead = Reader.Read();
793             nodeType = Reader.NodeType;
794 
795             // Close the node we are copying.
796             if (close != null)
797             {
798                 // Find the position of the close string, for example:
799                 //          <element      >  <subElement />
800                 //                        ^
801                 //                        closeLinePosition
802                 int startOffset = GetPositionOffset(nodeType);
803                 int closeLineNumber = Reader.LineNumber;
804                 int closeLinePosition = Reader.LinePosition - startOffset - close.Length;
805 
806                 // Add whitespace up to the position of the close string
807                 utilWriter.AppendWhiteSpace(lineNumber, linePosition, closeLineNumber, closeLinePosition);
808 
809                 // Write the close string
810                 utilWriter.Write(close);
811             }
812 
813             // Track the position of the reader based on the position of the reader
814             // before we copied this node and what we have written in copying the node.
815             // This allows us to determine the position of the <!DOCTYPE tag.
816             if (utilWriter.TrackPosition)
817             {
818                 _lastLineNumber = readerLineNumber - writerLineNumber + utilWriter.LineNumber;
819 
820                 if (writerLineNumber == utilWriter.LineNumber)
821                     _lastLinePosition = readerLinePosition - writerLinePosition + utilWriter.LinePosition;
822                 else _lastLinePosition = utilWriter.LinePosition;
823             }
824 
825             return moreToRead;
826         }
827 
828         // Asuming that we are at an element, retrieve the text for that element
829         // and attributes that can be serialized to an xml file.
RetrieveFullOpenElementTag()830         private string RetrieveFullOpenElementTag()
831         {
832             Debug.Assert(Reader.NodeType == XmlNodeType.Element,
833                 "_reader.NodeType == NodeType.Element");
834 
835             // Start with element tag name
836             StringBuilder element = new StringBuilder(64);
837             element.Append("<");
838             element.Append(Reader.Name);
839 
840             // Add attributes
841             while (Reader.MoveToNextAttribute())
842             {
843                 element.Append(" ");
844                 element.Append(Reader.Name);
845                 element.Append("=");
846                 element.Append('\"');
847                 element.Append(Reader.Value);
848                 element.Append('\"');
849             }
850 
851             // Now close the element tag
852             element.Append(">");
853 
854             return element.ToString();
855         }
856 
857         // Copy or replace an element node.
858         // If the element is an empty element, replace it with a formatted start element if either:
859         //   * The contents of the start element string need updating.
860         //   * The element needs to contain child elements.
861         //
862         // If the element is empty and is replaced with a start/end element pair, return a
863         // end element string with whitespace formatting; otherwise return null.
UpdateStartElement(XmlUtilWriter utilWriter, string updatedStartElement, bool needsChildren, int linePosition, int indent)864         internal string UpdateStartElement(XmlUtilWriter utilWriter, string updatedStartElement, bool needsChildren,
865             int linePosition, int indent)
866         {
867             Debug.Assert(Reader.NodeType == XmlNodeType.Element, "_reader.NodeType == NodeType.Element");
868 
869             string endElement = null;
870             bool needsEndElement = false;
871 
872             string elementName = Reader.Name;
873 
874             // If the element is empty, determine if a new end element is needed.
875             if (Reader.IsEmptyElement)
876             {
877                 if ((updatedStartElement == null) && needsChildren) updatedStartElement = RetrieveFullOpenElementTag();
878 
879                 needsEndElement = updatedStartElement != null;
880             }
881 
882             if (updatedStartElement == null)
883             {
884                 // If no changes to the start element are required, just copy it.
885                 CopyXmlNode(utilWriter);
886             }
887             else
888             {
889                 // Format a new start element/end element pair
890                 string updatedEndElement = "</" + elementName + ">";
891                 string updatedElement = updatedStartElement + updatedEndElement;
892                 string formattedElement = FormatXmlElement(updatedElement, linePosition, indent, true);
893 
894                 // Get the start and end element strings from the formatted element.
895                 int iEndElement = formattedElement.LastIndexOf('\n') + 1;
896                 string startElement;
897                 if (needsEndElement)
898                 {
899                     endElement = formattedElement.Substring(iEndElement);
900 
901                     // Include a newline in the start element as we are expanding an empty element.
902                     startElement = formattedElement.Substring(0, iEndElement);
903                 }
904                 else
905                 {
906                     // Omit the newline from the start element.
907                     startElement = formattedElement.Substring(0, iEndElement - 2);
908                 }
909 
910                 // Write the new start element.
911                 utilWriter.Write(startElement);
912 
913                 // Skip over the existing start element.
914                 Reader.Read();
915             }
916 
917             return endElement;
918         }
919 
920         // Create the cached string writer if it does not exist,
921         // otherwise reuse the existing buffer.
ResetCachedStringWriter()922         private void ResetCachedStringWriter()
923         {
924             if (_cachedStringWriter == null)
925                 _cachedStringWriter = new StringWriter(new StringBuilder(64), CultureInfo.InvariantCulture);
926             else _cachedStringWriter.GetStringBuilder().Length = 0;
927         }
928 
929         // Copy a configuration section to a string, and advance the reader.
CopySection()930         internal string CopySection()
931         {
932             ResetCachedStringWriter();
933 
934             // Preserve whitespace for sections for backcompat
935             WhitespaceHandling originalHandling = Reader.WhitespaceHandling;
936             Reader.WhitespaceHandling = WhitespaceHandling.All;
937 
938             // Create string writer to write to
939             XmlUtilWriter utilWriter = new XmlUtilWriter(_cachedStringWriter, false);
940 
941             // Copy the element
942             CopyElement(utilWriter);
943 
944             // Reset whitespace handling
945             Reader.WhitespaceHandling = originalHandling;
946 
947             if ((originalHandling == WhitespaceHandling.None) &&
948                 (Reader.NodeType == XmlNodeType.Whitespace))
949             {
950                 // If we were previously suppose to skip whitespace, and now we
951                 // are at it, then lets jump to the next item
952                 Reader.Read();
953             }
954 
955             utilWriter.Flush();
956             string s = ((StringWriter)utilWriter.Writer).ToString();
957             return s;
958         }
959 
960         /// <summary>
961         /// Format an Xml element to be written to the config file.
962         /// </summary>
963         /// <param name="xmlElement">the element</param>
964         /// <param name="linePosition">start position of the element</param>
965         /// <param name="indent">indent for each depth</param>
966         /// <param name="skipFirstIndent">skip indent for the first element?</param>
967         /// <returns></returns>
FormatXmlElement(string xmlElement, int linePosition, int indent, bool skipFirstIndent)968         internal static string FormatXmlElement(string xmlElement, int linePosition, int indent, bool skipFirstIndent)
969         {
970             XmlParserContext context = new XmlParserContext(null, null, null, XmlSpace.Default, Encoding.Unicode);
971             XmlTextReader reader = new XmlTextReader(xmlElement, XmlNodeType.Element, context);
972 
973             StringWriter stringWriter = new StringWriter(new StringBuilder(64), CultureInfo.InvariantCulture);
974             XmlUtilWriter utilWriter = new XmlUtilWriter(stringWriter, false);
975 
976             // append newline before indent?
977             bool newLine = false;
978 
979             // last node visited was text?
980             bool lastWasText = false;
981 
982             // length of the stringbuilder after last indent with newline
983             int sbLengthLastNewLine = 0;
984 
985             while (reader.Read())
986             {
987                 XmlNodeType nodeType = reader.NodeType;
988 
989                 int lineWidth;
990                 if (lastWasText)
991                 {
992                     utilWriter.Flush();
993                     lineWidth = sbLengthLastNewLine - ((StringWriter)utilWriter.Writer).GetStringBuilder().Length;
994                 }
995                 else lineWidth = 0;
996 
997                 switch (nodeType)
998                 {
999                     case XmlNodeType.CDATA:
1000                     case XmlNodeType.Element:
1001                     case XmlNodeType.EndElement:
1002                     case XmlNodeType.Comment:
1003                         // Do not indent if the last node was text - doing so would add whitespace
1004                         // that is included as part of the text.
1005                         if (!skipFirstIndent && !lastWasText)
1006                         {
1007                             utilWriter.AppendIndent(linePosition, indent, reader.Depth, newLine);
1008 
1009                             if (newLine)
1010                             {
1011                                 utilWriter.Flush();
1012                                 sbLengthLastNewLine = ((StringWriter)utilWriter.Writer).GetStringBuilder().Length;
1013                             }
1014                         }
1015                         break;
1016                 }
1017 
1018                 lastWasText = false;
1019                 switch (nodeType)
1020                 {
1021                     case XmlNodeType.Whitespace:
1022                         break;
1023                     case XmlNodeType.SignificantWhitespace:
1024                         utilWriter.Write(reader.Value);
1025                         break;
1026                     case XmlNodeType.CDATA:
1027                         utilWriter.AppendCData(reader.Value);
1028                         break;
1029                     case XmlNodeType.ProcessingInstruction:
1030                         utilWriter.AppendProcessingInstruction(reader.Name, reader.Value);
1031                         break;
1032                     case XmlNodeType.Comment:
1033                         utilWriter.AppendComment(reader.Value);
1034                         break;
1035                     case XmlNodeType.Text:
1036                         utilWriter.AppendEscapeTextString(reader.Value);
1037                         lastWasText = true;
1038                         break;
1039                     case XmlNodeType.Element:
1040                         {
1041                             // Write "<elem"
1042                             utilWriter.Write('<');
1043                             utilWriter.Write(reader.Name);
1044 
1045                             lineWidth += reader.Name.Length + 2;
1046 
1047                             int c = reader.AttributeCount;
1048                             for (int i = 0; i < c; i++)
1049                             {
1050                                 // Add new line if we've exceeded the line width
1051                                 bool writeSpace;
1052                                 if (lineWidth > MaxLineWidth)
1053                                 {
1054                                     utilWriter.AppendIndent(linePosition, indent, reader.Depth - 1, true);
1055                                     lineWidth = indent;
1056                                     writeSpace = false;
1057                                     utilWriter.Flush();
1058                                     sbLengthLastNewLine = ((StringWriter)utilWriter.Writer).GetStringBuilder().Length;
1059                                 }
1060                                 else writeSpace = true;
1061 
1062                                 // Write the attribute
1063                                 reader.MoveToNextAttribute();
1064                                 utilWriter.Flush();
1065                                 int startLength = ((StringWriter)utilWriter.Writer).GetStringBuilder().Length;
1066                                 if (writeSpace) utilWriter.AppendSpace();
1067 
1068                                 utilWriter.Write(reader.Name);
1069                                 utilWriter.Write('=');
1070                                 utilWriter.AppendAttributeValue(reader);
1071                                 utilWriter.Flush();
1072                                 lineWidth += ((StringWriter)utilWriter.Writer).GetStringBuilder().Length - startLength;
1073                             }
1074                         }
1075 
1076                         // position reader back on element
1077                         reader.MoveToElement();
1078 
1079                         // write closing tag
1080                         if (reader.IsEmptyElement) utilWriter.Write(" />");
1081                         else utilWriter.Write('>');
1082 
1083                         break;
1084                     case XmlNodeType.EndElement:
1085                         utilWriter.Write("</");
1086                         utilWriter.Write(reader.Name);
1087                         utilWriter.Write('>');
1088                         break;
1089                     case XmlNodeType.EntityReference:
1090                         utilWriter.AppendEntityRef(reader.Name);
1091                         break;
1092 
1093                     // Ignore <?xml and <!DOCTYPE nodes
1094                     // case XmlNodeType.XmlDeclaration:
1095                     // case XmlNodeType.DocumentType:
1096                 }
1097 
1098                 // put each new element on a new line
1099                 newLine = true;
1100 
1101                 // do not skip any more indents
1102                 skipFirstIndent = false;
1103             }
1104 
1105             utilWriter.Flush();
1106             string s = ((StringWriter)utilWriter.Writer).ToString();
1107             return s;
1108         }
1109     }
1110 }
1111