// 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 /// 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) { return 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 readonly FileReference File; /// /// The line number containing this element /// public readonly int LineNumber; /// /// Constructor /// public XmlConfigFileElement(FileReference InFile, int InLineNumber, string Prefix, string LocalName, string? NamespaceUri, XmlConfigFile ConfigFile) : base(Prefix, LocalName, NamespaceUri, ConfigFile) { File = InFile; LineNumber = InLineNumber; } } }