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, < 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