File: XmlTaskUtility.cs
Web Access
Project: src\msbuild\src\Tasks\Microsoft.Build.Tasks.csproj (Microsoft.Build.Tasks.Core)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.IO;
using System.Xml;

using Microsoft.Build.Framework;

#nullable disable

namespace Microsoft.Build.Tasks
{
    /// <summary>
    /// Shared XML helper methods used by XML-related tasks.
    /// </summary>
    internal static class XmlTaskUtility
    {
        /// <summary>
        /// The DOCTYPE declaration marker to search for in XML content.
        /// </summary>
        private const string DoctypeMarker = "<!DOCTYPE";

        /// <summary>
        /// Determines whether an exception is likely caused by prohibited DTD processing.
        /// </summary>
        /// <param name="exception">The thrown exception to inspect.</param>
        /// <param name="prohibitDtd">Whether DTDs were prohibited for the operation.</param>
        /// <param name="containsDtd">Whether the input contains a DTD.</param>
        /// <returns>True when the failure is consistent with DTD prohibition.</returns>
        internal static bool IsDtdProhibitedException(Exception exception, bool prohibitDtd, bool containsDtd)
        {
            return prohibitDtd && ContainsXmlException(exception) && containsDtd;
        }

        /// <summary>
        /// Determines whether an exception is likely caused by prohibited DTD processing.
        /// </summary>
        /// <param name="exception">The thrown exception to inspect.</param>
        /// <param name="prohibitDtd">Whether DTDs were prohibited for the operation.</param>
        /// <param name="containsDtd">Callback that determines whether the input contains a DTD.</param>
        /// <returns>True when the failure is consistent with DTD prohibition.</returns>
        internal static bool IsDtdProhibitedException(Exception exception, bool prohibitDtd, Func<bool> containsDtd)
        {
            if (!prohibitDtd || !ContainsXmlException(exception))
            {
                return false;
            }

            try
            {
                return containsDtd();
            }
            catch (Exception ex) when (!ExceptionHandling.IsCriticalException(ex))
            {
                // If we cannot re-read/inspect input, don't classify as DTD-prohibited.
                return false;
            }
        }

        /// <summary>
        /// Determines whether XML text contains a DOCTYPE declaration using text-based search.
        /// </summary>
        /// <param name="xml">The XML text to inspect.</param>
        /// <returns>True when a DOCTYPE declaration is present.</returns>
        internal static bool ContainsDtd(string xml)
        {
            if (string.IsNullOrEmpty(xml))
            {
                return false;
            }

            // Simple text-based detection - no XML parsing required.
            // XML markup declarations are case-sensitive.
            return xml.Contains(DoctypeMarker, StringComparison.Ordinal);
        }

        /// <summary>
        /// Determines whether the XML file contains a DOCTYPE declaration using text-based search.
        /// </summary>
        /// <param name="filePath">The XML file path to inspect.</param>
        /// <returns>True when a DOCTYPE declaration is present.</returns>
        internal static bool ContainsDtd(AbsolutePath filePath)
        {
            try
            {
                // Read only the beginning of the file - DOCTYPE must appear before the root element.
                // 8KB should be more than enough to find it in any reasonable XML file.
                using var stream = File.OpenRead(filePath.Value);
                using var reader = new StreamReader(stream);
                var buffer = new char[8192];
                int charsRead = reader.Read(buffer, 0, buffer.Length);
                var content = new string(buffer, 0, charsRead);
                return content.Contains(DoctypeMarker, StringComparison.Ordinal);
            }
            catch
            {
                // If we cannot read the file, don't classify as containing DTD.
                return false;
            }
        }

        /// <summary>
        /// Walks the exception chain and checks whether an <see cref="XmlException"/> is present.
        /// </summary>
        /// <param name="exception">The exception to inspect.</param>
        /// <returns>True when an XML exception is found in the chain.</returns>
        private static bool ContainsXmlException(Exception exception)
        {
            for (Exception current = exception; current is not null; current = current.InnerException)
            {
                if (current is XmlException)
                {
                    return true;
                }
            }

            return false;
        }
    }
}