using System; using System.Collections.Generic; using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Threading.Tasks; using System.Xml; using System.Xml.Linq; using Microsoft.Win32; using Mvp.Xml.XInclude; namespace HashCalculator.GUI { public class ModXml { 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 bool SdkNotFound { get; } public IReadOnlyCollection Errors => _errors; private readonly ConcurrentDictionary _processed = new ConcurrentDictionary(); private readonly ConcurrentDictionary _assets = new ConcurrentDictionary(); private readonly ConcurrentQueue _errors = new ConcurrentQueue(); private readonly ModUriResolver _uriResolver; private readonly string _xmlFullPath; public ModXml(string xmlPath) { var baseDirectory = new DirectoryInfo(FindBaseDirectory(xmlPath)); var allMods = baseDirectory.Parent; if (!allMods.Name.Equals("Mods", StringComparison.OrdinalIgnoreCase)) { throw new DirectoryNotFoundException(); } var allModsParent = allMods.Parent; using var hklm = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32); if (!(hklm.GetValue(RegistryPath) is string sdkRootPath)) { SdkNotFound = true; 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); } public Task> ProcessDocument() { if (_processed.Any() || _assets.Any() || _errors.Any()) { throw new InvalidOperationException(); } return Task.Run(() => { var includes = ProcessDocumentInternal(_xmlFullPath).AsParallel(); while (includes.Any()) { includes = from include in includes from newInclude in ProcessDocumentInternal(include) select newInclude; } return _assets.Keys; }); } private string[] ProcessDocumentInternal(string fullPath) { if (!_processed.TryAdd(fullPath.ToUpperInvariant(), default)) { return Array.Empty(); } var document = GetFile(fullPath); if (document.Root.Name != EalaAsset + "AssetDeclaration" && document.Root.Name != "AssetDeclaration") { throw new NotSupportedException(); } var items = from element in document.Root.Elements() where element.Attribute("id") != null select new AssetEntry(element); foreach (var item in items) { if (!_assets.TryAdd(item, default)) { _errors.Enqueue($"Attempted to add item `{item.Type}:{item.Name}` multiple times"); } } var includes = from include in document.Root.Elements(EalaAsset + "Includes").Elements() let includePath = GetIncludePath(include, fullPath) where includePath != null select includePath; return includes.ToArray(); } 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) { _errors.Enqueue(error.Message); return null; } return includedSource; } private XDocument GetFile(string normalizedPath) { using var reader = new XIncludingReader(normalizedPath, _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); } private static string FindBaseDirectory(string currentPath) { var file = new FileInfo(currentPath); if (File.Exists(Path.Combine(file.DirectoryName, "Data", "mod.xml"))) { return file.DirectoryName; } return FindBaseDirectory(file.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)} is null"); } if (ofObjectToReturn != null && ofObjectToReturn != typeof(Stream)) { throw new NotImplementedException(); } return File.OpenRead(absoluteUri.LocalPath); } [SuppressMessage("Globalization", "CA1303:请不要将文本作为本地化参数传递", Justification = "<挂起>")] 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)} is null"); } return new Uri(Path.GetFullPath(ResolvePath(relativeUri, Path.GetDirectoryName(baseUri.LocalPath)))); } public override Task GetEntityAsync(Uri absoluteUri, string role, Type ofObjectToReturn) { return base.GetEntityAsync(absoluteUri, role, ofObjectToReturn); } public override bool SupportsType(Uri absoluteUri, Type type) { return base.SupportsType(absoluteUri, type); } [SuppressMessage("Globalization", "CA1303:请不要将文本作为本地化参数传递", Justification = "<挂起>")] 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; } } }