File: DocumentationComments\XmlDocumentationCommentTextReader.XmlStream.cs
Web Access
Project: src\roslyn\src\Compilers\Core\Portable\Microsoft.CodeAnalysis.csproj (Microsoft.CodeAnalysis)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

using System;
using System.Diagnostics;
using System.IO;
using System.Xml;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis
{
    internal partial class XmlDocumentationCommentTextReader
    {
        internal sealed class Reader : TextReader
        {
            /// <summary>
            /// Current text to validate.
            /// </summary>
            private string _text;

            private int _position;

            /// <summary>
            /// We use <see cref="XmlReader"/> to validate XML doc comments. Unfortunately it cannot be reset and thus can't be pooled. 
            /// Each time we need to validate a fragment of XML we "append" it to the underlying text reader, implemented by this class, 
            /// and advance the reader. By the end of the fragment validation, we keep the reader open in a state 
            /// that is ready for the next fragment validation unless the fragment was invalid, in which case we need to create a new XmlReader.
            /// That is why <see cref="Read(char[], int, int) "/> pretends that the stream has extra <see cref="maxReadsPastTheEnd"/> spaces
            /// at the end. That should be sufficient for <see cref="XmlReader"/> to not reach the end of this reader before the next 
            /// fragment is appended, unless the current fragment is malformed in one way or another. 
            /// </summary>
            private const int maxReadsPastTheEnd = 100;
            private int _readsPastTheEnd;

            // Base the root element name on a GUID to avoid accidental (or intentional) collisions. An underscore is
            // prefixed because element names must not start with a number.
            private static readonly string s_rootElementName = "_" + Guid.NewGuid().ToString("N");
            private static readonly string s_currentElementName = "_" + Guid.NewGuid().ToString("N");

            // internal for testing
            internal static readonly string RootStart = "<" + s_rootElementName + ">";
            internal static readonly string CurrentStart = "<" + s_currentElementName + ">";
            internal static readonly string CurrentEnd = "</" + s_currentElementName + ">";

            public void Reset()
            {
                _text = null;
                _position = 0;
                _readsPastTheEnd = 0;
            }

            public void SetText(string text)
            {
                _text = text;
                _readsPastTheEnd = 0;

                // The first read shall read the <root>, 
                // the subsequents reads shall start with <current> element
                if (_position > 0)
                {
                    _position = RootStart.Length;
                }
            }

            // for testing
            internal int Position
            {
                get { return _position; }
            }

            public static bool ReachedEnd(XmlReader reader)
            {
                return reader.Depth == 1
                    && reader.NodeType == XmlNodeType.EndElement
                    && reader.Name == s_currentElementName;
            }

            public bool Eof
            {
                get
                {
                    return _readsPastTheEnd >= maxReadsPastTheEnd;
                }
            }

            public override int Read(char[] buffer, int index, int count)
            {
                if (count == 0 || Eof)
                {
                    return 0;
                }

                // The stream synthesizes an XML document with:
                // 1. A root element start tag
                // 2. Current element start tag
                // 3. The user text (xml fragments)
                // 4. Current element end tag

                int initialCount = count;

                // <root>
                _position += EncodeAndAdvance(RootStart, _position, buffer, ref index, ref count);

                // <current>
                _position += EncodeAndAdvance(CurrentStart, _position - RootStart.Length, buffer, ref index, ref count);

                // text
                _position += EncodeAndAdvance(_text, _position - RootStart.Length - CurrentStart.Length, buffer, ref index, ref count);

                // </current>
                _position += EncodeAndAdvance(CurrentEnd, _position - RootStart.Length - CurrentStart.Length - _text.Length, buffer, ref index, ref count);

                // Pretend that the stream doesn't end right away
                if (initialCount == count)
                {
                    _readsPastTheEnd++;
                    buffer[index] = ' ';
                    count--;
                }

                return initialCount - count;
            }

            private static int EncodeAndAdvance(string src, int srcIndex, char[] dest, ref int destIndex, ref int destCount)
            {
                if (destCount == 0 || srcIndex < 0 || srcIndex >= src.Length)
                {
                    return 0;
                }

                int charCount = Math.Min(src.Length - srcIndex, destCount);
                Debug.Assert(charCount > 0);
                src.CopyTo(srcIndex, dest, destIndex, charCount);

                destIndex += charCount;
                destCount -= charCount;
                Debug.Assert(destCount >= 0);
                return charCount;
            }

            public override int Read()
            {
                // XmlReader does not call this API
                throw ExceptionUtilities.Unreachable();
            }

            public override int Peek()
            {
                // XmlReader does not call this API
                throw ExceptionUtilities.Unreachable();
            }
        }
    }
}