using Microsoft.Win32; 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; using System.Xml.Linq; namespace HashCalculator.GUI { public class ModXml : IModXml { protected enum DocumentOption { Normal, IsEntryPoint } const string RegistryPath = @"Microsoft\Windows\CurrentVersion\Uninstall\{F6A3F605-7B10-4939-8D3D-4594332C1649}"; public static readonly IEnumerable NotSupportedAttributes = new[] { "fragid", "set-xml-id", "encoding", "accept", "accept-language" }; public static XNamespace XInclude { get; } = "http://www.w3.org/2001/XInclude"; public static XNamespace EalaAsset { get; } = "uri:ea.com:eala:asset"; public DirectoryInfo BaseDirectory { get; } public int TotalFilesProcessed => _processed.Count; public int TotalAssets => _assets.Count; private readonly ConcurrentDictionary _processed = new ConcurrentDictionary(); private readonly ConcurrentDictionary _assets = new ConcurrentDictionary(); private readonly ModUriResolver _uriResolver; private readonly string _xmlFullPath; private readonly CancellationToken _token; private readonly BufferBlock _entries; public ModXml(string xmlPath, string? sdkRootPath, CancellationToken token) { BaseDirectory = new DirectoryInfo(FindBaseDirectory(xmlPath)); var allMods = BaseDirectory.Parent ?? throw new ArgumentException($"{nameof(xmlPath)}'s {nameof(BaseDirectory)} doesn't have a parent"); if (!allMods.Name.Equals("Mods", StringComparison.OrdinalIgnoreCase)) { throw new DirectoryNotFoundException(); } var allModsParent = allMods.Parent ?? throw new ArgumentException($"SDK Mods folder doesn't have a parent"); sdkRootPath ??= allModsParent.FullName; IReadOnlyCollection GetPaths(string name) => new string[] { sdkRootPath, allModsParent.FullName, Path.Combine(BaseDirectory.FullName, name), Path.Combine(sdkRootPath, "Mods"), allMods.FullName, Path.Combine(sdkRootPath, name.Equals("Data", StringComparison.OrdinalIgnoreCase) ? "SageXml" : name) }; _uriResolver = new ModUriResolver(new Dictionary>(StringComparer.OrdinalIgnoreCase) { { "Art", GetPaths("Art") }, { "Audio", GetPaths("Audio") }, { "Data", GetPaths("Data") }, }); _xmlFullPath = Path.GetFullPath(xmlPath); _token = token; _entries = new BufferBlock(); } public static string? LocateSdkFromRegistry() { using var hklm = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32); return hklm.GetValue(RegistryPath) as string; } 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 != EalaAsset + "AssetDeclaration" && rootName != "AssetDeclaration") { throw new NotSupportedException(); } var includes = from include in document.Root.Elements(EalaAsset + "Includes").Elements() 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() where element.Attribute("id") != null let entry = AssetEntry.TryParse(element) where entry != null select entry; foreach (var item in items) { if (_assets.TryAdd(item, fullPath)) { _entries.Post(item); } else { var previousPath = _assets[item]; if(!fullPath.Contains("SageXml")) { TracerListener.WriteLine($"[ModXml] `{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 (include.Name != EalaAsset + "Include") { throw new InvalidDataException(); } if (include.Attribute("type")?.Value == "reference") { return null; } var source = include.Attribute("source")?.Value; if (source == null) { throw new InvalidDataException(); } string? includedSource; try { includedSource = _uriResolver.ResolveUri(new Uri(includerPath), source).LocalPath; } catch (FileNotFoundException error) { if (!(error.FileName?.StartsWith("ART:", StringComparison.OrdinalIgnoreCase) is true)) { TracerListener.WriteLine($"[ModXml]: {error}"); } return null; } if (!File.Exists(includedSource)) { TracerListener.WriteLine($"[ModXml]: 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, _uriResolver) { // I dont know why but looks like it need to be set again // Otherwise it wont work XmlResolver = _uriResolver }; reader.MoveToContent(); return XDocument.Load(reader); } catch (Exception e) { throw new InvalidDataException($"Failed to process XML file: {normalizedPath} - {e.Message}", e); } } private static string FindBaseDirectory(string currentPath) { var file = new FileInfo(currentPath); var directoryName = file.DirectoryName ?? throw new ArgumentException($"{nameof(currentPath)} doens't have a directory"); if (File.Exists(Path.Combine(directoryName, "Data", "mod.xml"))) { return directoryName; } return FindBaseDirectory(directoryName); } } public class ModUriResolver : XmlResolver { protected IReadOnlyDictionary> Paths { get; } public ModUriResolver(IReadOnlyDictionary> paths) { Paths = paths; } public override object GetEntity(Uri? absoluteUri, string? role, Type? ofObjectToReturn) { if (absoluteUri == null) { throw new ArgumentNullException(nameof(absoluteUri)); } if (ofObjectToReturn != null && ofObjectToReturn != typeof(Stream)) { throw new NotImplementedException(); } return File.OpenRead(absoluteUri.LocalPath); } public override Uri ResolveUri(Uri? baseUri, string? relativeUri) { if (baseUri == null) { if (!Path.IsPathRooted(relativeUri)) { throw new ArgumentException($"{nameof(baseUri)} is null and {nameof(relativeUri)} is not rooted"); } return new Uri(relativeUri); } if (relativeUri == null) { throw new ArgumentNullException(nameof(relativeUri)); } var localPath = Path.GetDirectoryName(baseUri.LocalPath) ?? throw new ArgumentException($"{nameof(baseUri)} doesn't have a local path"); return new Uri(Path.GetFullPath(ResolvePath(relativeUri, localPath))); } private string ResolvePath(string href, string currentDirectory) { var splitted = href.Split(new[] { ':' }, 2); if (splitted.Length > 1) { var prefix = splitted[0]; var path = splitted[1]; if (Paths.TryGetValue(prefix, out var collection)) { foreach (var directory in collection) { if (prefix.Equals("Art", StringComparison.OrdinalIgnoreCase)) { var artPath = Path.Combine(directory, path.Substring(0, 2), path); if (File.Exists(artPath)) { return artPath; } } var realPath = Path.Combine(directory, path); if (File.Exists(realPath)) { return realPath; } } throw new FileNotFoundException("Cannot find the file specified", href); } throw new FileNotFoundException($"Unknown prefix: {prefix}", href); } var relativePath = Path.Combine(currentDirectory, href); if (!File.Exists(relativePath)) { throw new FileNotFoundException("Cannot find the file specified", href); } return relativePath; } } }