using System.Xml; using System.Xml.Schema; namespace XSDVisualiser.Core; /// /// Validates an XML document against a compiled XSD schema set and a specific global element (node). /// public static class XmlValidator { public static XmlValidationResult ValidateAgainstElement(string xsdPath, string elementName, string? elementNamespace, string xmlPath) { var set = BuildSchemaSet(xsdPath); return ValidateAgainstElement(set, elementName, elementNamespace, xmlPath); } public static XmlValidationResult ValidateAgainstElement(XmlSchemaSet schemas, string elementName, string? elementNamespace, string xmlPath) { var result = new XmlValidationResult(); // Ensure the requested element exists in the schema set var qname = new XmlQualifiedName(elementName, elementNamespace ?? string.Empty); if (schemas.GlobalElements[qname] is not XmlSchemaElement) { result.AddError($"Element '{qname}' was not found in the compiled schema set."); return result; } // Probe XML root element (string localName, string nsUri)? rootInfo = TryReadRoot(xmlPath); if (rootInfo is null) { result.AddError("Unable to read XML root element."); return result; } var (rootLocal, rootNs) = rootInfo.Value; var matchesRoot = string.Equals(rootLocal, elementName, StringComparison.Ordinal) && string.Equals(rootNs ?? string.Empty, elementNamespace ?? string.Empty, StringComparison.Ordinal); var settings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore, ValidationType = ValidationType.Schema, Schemas = schemas, CloseInput = true, ConformanceLevel = ConformanceLevel.Auto }; settings.ValidationFlags = XmlSchemaValidationFlags.ReportValidationWarnings | XmlSchemaValidationFlags.ProcessIdentityConstraints; void Handler(object? sender, ValidationEventArgs e) { if (e.Severity == XmlSeverityType.Warning) result.AddWarning(e.Message, e.Exception?.LineNumber, e.Exception?.LinePosition); else result.AddError(e.Message, e.Exception?.LineNumber, e.Exception?.LinePosition); } settings.ValidationEventHandler += Handler; if (matchesRoot) { using var reader = XmlReader.Create(xmlPath, settings); try { while (reader.Read()) { // just advance to trigger validation callbacks } } catch (XmlException xe) { result.AddError($"XML parsing error: {xe.Message}", xe.LineNumber, xe.LinePosition); } return result; } else { // Root does not match the selected schema element. Try to locate the first matching subtree and validate only that fragment. // This enables validating an XML file towards a selected node from the XSD. var fragmentSettings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore, ValidationType = ValidationType.Schema, Schemas = schemas, CloseInput = true, ConformanceLevel = ConformanceLevel.Fragment }; fragmentSettings.ValidationFlags = settings.ValidationFlags; fragmentSettings.ValidationEventHandler += Handler; try { var (elementNode, loadError) = FindFirstElementNode(xmlPath, elementName, elementNamespace); if (loadError is not null) { result.AddError(loadError.Value.Message, loadError.Value.LineNumber, loadError.Value.LinePosition); return result; } if (elementNode is null) { result.AddError($"Could not find any element '{{{elementNamespace}}}{elementName}' in the XML document to validate against."); return result; } // Inform as a warning that we validate a subtree instead of the document root result.AddWarning($"Validating against the first occurrence of '{{{elementNamespace}}}{elementName}' found in the document (root is '{{{rootNs}}}{rootLocal}')."); using var nodeReader = new XmlNodeReader(elementNode); using var validatingReader = XmlReader.Create(nodeReader, fragmentSettings); while (validatingReader.Read()) { // advance to trigger validation callbacks for the subtree } } catch (XmlException xe) { result.AddError($"XML parsing error: {xe.Message}", xe.LineNumber, xe.LinePosition); } return result; } } private static XmlSchemaSet BuildSchemaSet(string xsdPath) { var set = new XmlSchemaSet { XmlResolver = new XmlUrlResolver(), CompilationSettings = new XmlSchemaCompilationSettings { EnableUpaCheck = true } }; using var reader = XmlReader.Create(xsdPath, new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore }); var schema = XmlSchema.Read(reader, null); if (schema != null) set.Add(schema); set.Compile(); return set; } private static (string localName, string nsUri)? TryReadRoot(string xmlPath) { using var reader = XmlReader.Create(xmlPath, new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore, IgnoreWhitespace = true, IgnoreComments = true }); try { while (reader.Read()) { if (reader.NodeType == XmlNodeType.Element && reader.Depth == 0) { return (reader.LocalName, reader.NamespaceURI); } } } catch { // ignored; higher level will report XmlException separately } return null; } private static (XmlElement? Node, (string Message, int LineNumber, int LinePosition)? LoadError) FindFirstElementNode(string xmlPath, string elementName, string? elementNamespace) { try { var xr = XmlReader.Create(xmlPath, new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore }); var doc = new XmlDocument(); doc.PreserveWhitespace = false; doc.Load(xr); static XmlElement? Traverse(XmlNode node, string name, string? ns) { if (node is XmlElement el) { if (string.Equals(el.LocalName, name, StringComparison.Ordinal) && string.Equals(el.NamespaceURI ?? string.Empty, ns ?? string.Empty, StringComparison.Ordinal)) return el; } foreach (XmlNode child in node.ChildNodes) { var found = Traverse(child, name, ns); if (found != null) return found; } return null; } var match = Traverse(doc, elementName, elementNamespace); return (match, null); } catch (XmlException xe) { return (null, ($"XML parsing error: {xe.Message}", xe.LineNumber, xe.LinePosition)); } } } public sealed class XmlValidationResult { private readonly List _issues = new(); public bool IsValid => _issues.TrueForAll(i => i.Severity != XmlSeverityType.Error); public IReadOnlyList Issues => _issues; public IEnumerable Errors => _issues.Where(i => i.Severity == XmlSeverityType.Error); public IEnumerable Warnings => _issues.Where(i => i.Severity == XmlSeverityType.Warning); internal void AddError(string message, int? line = null, int? position = null) => _issues.Add(new XmlValidationIssue(XmlSeverityType.Error, message, line ?? 0, position ?? 0)); internal void AddWarning(string message, int? line = null, int? position = null) => _issues.Add(new XmlValidationIssue(XmlSeverityType.Warning, message, line ?? 0, position ?? 0)); } public sealed record XmlValidationIssue(XmlSeverityType Severity, string Message, int LineNumber, int LinePosition);