using Mvp.Xml.XInclude; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; using System.Xml.Linq; namespace HashCalculator.GUI { public class ModXsd : IModXml { protected enum DocumentOption { Normal, IsEntryPoint } public static readonly IEnumerable NotSupportedAttributes = new[] { "fragid", "set-xml-id", "encoding", "accept", "accept-language" }; public static XNamespace Schema = "http://www.w3.org/2001/XMLSchema"; public int TotalFilesProcessed => _processed.Count; public int TotalAssets => _assets.Count; private readonly ConcurrentDictionary _processed = new(); private readonly ConcurrentDictionary _assets = new(); private readonly BufferBlock _entries = new(); private readonly string _xmlFullPath; private readonly CancellationToken _token; public ModXsd(string xmlPath, CancellationToken token) { _xmlFullPath = Path.GetFullPath(xmlPath); _token = token; } public async IAsyncEnumerable ProcessDocument() { if (_processed.Any() || _assets.Any()) { throw new InvalidOperationException(); } var task = Task.Run(async () => { try { await ProcessDocumentInternal(_xmlFullPath); } catch { if (!_token.IsCancellationRequested) { throw; } } finally { _entries.Complete(); } }); while (await _entries.OutputAvailableAsync()) { yield return _entries.Receive(); } await task; } private async Task ProcessDocumentInternal(string fullPath) { _token.ThrowIfCancellationRequested(); if (!_processed.TryAdd(fullPath.ToUpperInvariant(), default)) { return; } var document = await GetFileAsync(fullPath); var rootName = document.Root?.Name ?? throw new InvalidDataException("Document doesn't have a root"); if (rootName != Schema + "schema") { throw new NotSupportedException(); } var includes = from include in document.Root.Elements(Schema + "include") let includePath = GetIncludePath(include, fullPath) where includePath != null select Task.Run(() => ProcessDocumentInternal(includePath), _token); var includesTasks = includes.ToArray(); var items = from element in document.Root.Elements() let name = element.Name where name == Schema + "simpleType" || name == Schema + "complexType" let entry = AssetEntry.TryParseXsd(element) where entry != null select entry; foreach (var item in items) { if (_assets.TryAdd(item, fullPath)) { _entries.Post(item); } else { var previousPath = _assets[item]; TracerListener.WriteLine($"[ModXsd] `{fullPath}`: Attempted to add item `{item.Type}:{item.Name}` multiple times - already processed in `{previousPath}`"); } } await Task.WhenAll(includesTasks); } private string? GetIncludePath(XElement include, string includerPath) { if (Path.GetDirectoryName(includerPath) is not { } currentDirectory) { throw new InvalidOperationException(); } if (include.Name != Schema + "include") { throw new InvalidDataException(); } var source = include.Attribute("schemaLocation")?.Value; if (source is null) { throw new InvalidDataException(); } var includedSource = Path.GetFullPath(source, currentDirectory); if (!File.Exists(includedSource)) { TracerListener.WriteLine($"[ModXsd]: Warning, include path does not exist! It is: {includedSource}"); return null; } return includedSource; } private async Task GetFileAsync(string normalizedPath) { try { var xml = await File.ReadAllBytesAsync(normalizedPath, _token); using var xmlStream = new MemoryStream(xml); using var reader = new XIncludingReader(normalizedPath, xmlStream); reader.MoveToContent(); return XDocument.Load(reader); } catch (Exception e) { throw new InvalidDataException($"Failed to process XML file: {normalizedPath} - {e.Message}", e); } } } }