// Copyright Epic Games, Inc. All Rights Reserved. using System.Diagnostics.CodeAnalysis; using System.Xml; using System.Xml.Schema; using EpicGames.Core; using Microsoft.Extensions.Logging; namespace UnrealBuildTool { /// /// Implementation of XmlDocument which preserves line numbers for its elements /// class XmlConfigFile : XmlDocument { /// /// Root element for the XML document /// public const string RootElementName = "Configuration"; /// /// Namespace for the XML schema /// public const string SchemaNamespaceURI = "https://www.unrealengine.com/BuildConfiguration"; /// /// The file being read /// readonly FileReference _file; /// /// Interface to the LineInfo on the active XmlReader /// IXmlLineInfo _lineInfo = null!; /// /// Set to true if the reader encounters an error /// bool _bHasErrors; /// /// Private constructor. Use XmlConfigFile.TryRead to read an XML config file. /// private XmlConfigFile(FileReference inFile) { _file = inFile; } /// /// Overrides XmlDocument.CreateElement() to construct ScriptElements rather than XmlElements /// public override XmlElement CreateElement(string? prefix, string localName, string? namespaceUri) => new XmlConfigFileElement(_file, _lineInfo.LineNumber, prefix!, localName, namespaceUri, this); /// /// Loads a script document from the given file /// /// The file to load /// The schema to validate against /// Logger for output /// If successful, the document that was read /// True if the document could be read, false otherwise public static bool TryRead(FileReference file, XmlSchema schema, ILogger logger, [NotNullWhen(true)] out XmlConfigFile? outConfigFile) { XmlConfigFile configFile = new XmlConfigFile(file); XmlReaderSettings settings = new XmlReaderSettings(); settings.Schemas.Add(schema); settings.ValidationType = ValidationType.Schema; settings.ValidationEventHandler += configFile.ValidationEvent; using (XmlReader reader = XmlReader.Create(file.FullName, settings)) { // Read the document configFile._lineInfo = (IXmlLineInfo)reader; try { configFile.Load(reader); } catch (XmlException ex) { if (!configFile._bHasErrors) { Log.TraceErrorTask(file, ex.LineNumber, "{0}", ex.Message); configFile._bHasErrors = true; } } // If we hit any errors while parsing if (configFile._bHasErrors) { outConfigFile = null; return false; } // Check that the root element is valid. If not, we didn't actually validate against the schema. if (configFile.DocumentElement?.Name != RootElementName) { logger.LogError("Script does not have a root element called '{RootElementName}'", RootElementName); outConfigFile = null; return false; } if (configFile.DocumentElement.NamespaceURI != SchemaNamespaceURI) { logger.LogError("Script root element is not in the '{NamespaceUri}' namespace (add the xmlns=\"{NamespaceUri2}\" attribute)", SchemaNamespaceURI, SchemaNamespaceURI); outConfigFile = null; return false; } } outConfigFile = configFile; return true; } /// /// Callback for validation errors in the document /// /// Standard argument for ValidationEventHandler /// Standard argument for ValidationEventHandler void ValidationEvent(object? sender, ValidationEventArgs args) => Log.TraceWarningTask(_file, args.Exception.LineNumber, "{0}", args.Message); } /// /// Implementation of XmlElement which preserves line numbers /// class XmlConfigFileElement : XmlElement { /// /// The file containing this element /// public FileReference File { get; init; } /// /// The line number containing this element /// public int LineNumber { get; init; } /// /// Constructor /// public XmlConfigFileElement(FileReference inFile, int inLineNumber, string prefix, string localName, string? namespaceUri, XmlConfigFile configFile) : base(prefix, localName, namespaceUri, configFile) { File = inFile; LineNumber = inLineNumber; } } }