From 8d6885632052d672e425eb06463e7fc33d94cd7b Mon Sep 17 00:00:00 2001 From: Paul Hebble Date: Sat, 26 Mar 2022 12:37:51 -0500 Subject: [PATCH] Detect manually installed mods via cfg file :FOR[identifier] clauses --- Core/GameInstance.cs | 48 ++-------- Core/Games/IGame.cs | 3 + Core/Games/KerbalSpaceProgram.cs | 90 +++++++++++++++++++ Core/Registry/Registry.cs | 48 +++------- Tests/Core/GameInstance.cs | 2 +- Tests/Core/Registry/Registry.cs | 5 +- .../Relationships/RelationshipResolver.cs | 8 +- 7 files changed, 123 insertions(+), 81 deletions(-) diff --git a/Core/GameInstance.cs b/Core/GameInstance.cs index 360662a739..2344c599e7 100644 --- a/Core/GameInstance.cs +++ b/Core/GameInstance.cs @@ -371,16 +371,19 @@ public bool Scan() // GameData *twice*. // // The least evil is to walk it once, and filter it ourselves. - IEnumerable files = Directory + var files = Directory .EnumerateFiles(game.PrimaryModDirectory(this), "*", SearchOption.AllDirectories) - .Where(file => file.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase)) .Select(CKANPathUtils.NormalizePath) - .Where(absPath => !game.StockFolders.Any(f => - ToRelativeGameDir(absPath).StartsWith($"{f}/"))); + .Select(ToRelativeGameDir) + .Where(relPath => !game.StockFolders.Any(f => relPath.StartsWith($"{f}/")) + && manager.registry.FileOwner(relPath) == null); - foreach (string dll in files) + foreach (string relativePath in files) { - manager.registry.RegisterDll(this, dll); + foreach (var identifier in game.IdentifiersFromFileName(this, relativePath)) + { + manager.registry.RegisterFile(relativePath, identifier); + } } var newDlls = manager.registry.InstalledDlls.ToHashSet(); bool dllChanged = !oldDlls.SetEquals(newDlls); @@ -419,39 +422,6 @@ public string ToAbsoluteGameDir(string path) return CKANPathUtils.ToAbsolute(path, GameDir()); } - /// - /// https://xkcd.com/208/ - /// This regex matches things like GameData/Foo/Foo.1.2.dll - /// - private static readonly Regex dllPattern = new Regex( - @" - ^(?:.*/)? # Directories (ending with /) - (?[^.]+) # Our DLL name, up until the first dot. - .*\.dll$ # Everything else, ending in dll - ", - RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled - ); - - /// - /// Find the identifier associated with a manually installed DLL - /// - /// Path of the DLL relative to game root - /// - /// Identifier if found otherwise null - /// - public string DllPathToIdentifier(string relative_path) - { - if (!relative_path.StartsWith($"{game.PrimaryModDirectoryRelative}/", StringComparison.CurrentCultureIgnoreCase)) - { - // DLLs only live in the primary mod directory - return null; - } - Match match = dllPattern.Match(relative_path); - return match.Success - ? Identifier.Sanitize(match.Groups["identifier"].Value) - : null; - } - public override string ToString() { return $"{game.ShortName} Install: {gameDir}"; diff --git a/Core/Games/IGame.cs b/Core/Games/IGame.cs index 5cfb4e9027..4935a6beb9 100644 --- a/Core/Games/IGame.cs +++ b/Core/Games/IGame.cs @@ -34,6 +34,9 @@ public interface IGame string CompatibleVersionsFile { get; } string[] BuildIDFiles { get; } + // Manually installed file handling + string[] IdentifiersFromFileName(GameInstance inst, string absolutePath); + // How to get metadata Uri DefaultRepositoryURL { get; } Uri RepositoryListURL { get; } diff --git a/Core/Games/KerbalSpaceProgram.cs b/Core/Games/KerbalSpaceProgram.cs index 4baf4bcb42..a2593ee94b 100644 --- a/Core/Games/KerbalSpaceProgram.cs +++ b/Core/Games/KerbalSpaceProgram.cs @@ -2,8 +2,12 @@ using System.Linq; using System.IO; using System.Collections.Generic; +using System.Text.RegularExpressions; + using Autofac; using log4net; +using ParsecSharp; + using CKAN.GameVersionProviders; using CKAN.Versioning; @@ -328,6 +332,92 @@ private string[] filterCmdLineArgs(string[] args, GameVersion installedVersion, return args; } + /// + /// https://xkcd.com/208/ + /// This regex matches things like GameData/Foo/Foo.1.2.dll + /// + private static readonly Regex dllPattern = new Regex( + @" + ^(?:.*/)? # Directories (ending with /) + (?[^.]+) # Our DLL name, up until the first dot. + .*\.dll$ # Everything else, ending in dll + ", + RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled + ); + + /// + /// Find the identifier associated with a manually installed DLL + /// + /// Path of the DLL relative to game root + /// + /// Identifier if found otherwise null + /// + private string[] DllPathToIdentifiers(string relativePath) + { + if (!relativePath.StartsWith($"{PrimaryModDirectoryRelative}/", + Platform.IsWindows + ? StringComparison.CurrentCultureIgnoreCase + : StringComparison.CurrentCulture)) + { + // DLLs only live in the primary mod directory + return new string[] { }; + } + Match match = dllPattern.Match(relativePath); + return match.Success + ? new string[] { Identifier.Sanitize(match.Groups["identifier"].Value) } + : new string[] { }; + } + + public static IEnumerable IdentifiersFromConfigNodes(IEnumerable nodes) + => nodes + .Select(node => node.For) + .Where(ident => !string.IsNullOrEmpty(ident)) + .Concat(nodes.SelectMany(node => IdentifiersFromConfigNodes(node.Children))) + .Distinct(); + + /// + /// Find the identifiers associated with a .cfg file string + /// using ModuleManager's :FOR[identifier] pattern + /// + /// Game instance containing the files + /// Absolute path of the .cfg file + /// Contents of the cfg file + /// + /// Array of identifiers, if any found + /// + private static string[] CfgContentsToIdentifiers(GameInstance inst, string absolutePath, string cfgContents) + => KSPConfigParser.ConfigFile.Parse(cfgContents) + .CaseOf(failure => + { + log.InfoFormat("{0}:{1}:{2}: {3}", + inst.ToRelativeGameDir(absolutePath), + failure.State.Position.Line, + failure.State.Position.Column, + failure.Message); + return Enumerable.Empty(); + }, + success => IdentifiersFromConfigNodes(success.Value)) + .ToArray(); + + /// + /// Find the identifiers associated with a manually installed .cfg file + /// using ModuleManager's :FOR[identifier] pattern + /// + /// Game instance containing the files + /// Absolute path of the .cfg file + /// + /// Array of identifiers, if any found + /// + private static string[] CfgPathToIdentifiers(GameInstance inst, string absolutePath) + => CfgContentsToIdentifiers(inst, absolutePath, File.ReadAllText(absolutePath)); + + public string[] IdentifiersFromFileName(GameInstance inst, string relativePath) + => relativePath.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase) + ? DllPathToIdentifiers(relativePath) + : relativePath.EndsWith(".cfg", StringComparison.CurrentCultureIgnoreCase) + ? CfgPathToIdentifiers(inst, inst.ToAbsoluteGameDir(relativePath)) + : new string[] { }; + private static readonly ILog log = LogManager.GetLogger(typeof(KerbalSpaceProgram)); } } diff --git a/Core/Registry/Registry.cs b/Core/Registry/Registry.cs index 2aa9d53fc0..8de0232968 100644 --- a/Core/Registry/Registry.cs +++ b/Core/Registry/Registry.cs @@ -105,7 +105,10 @@ [JsonIgnore] public IEnumerable InstalledDlls /// public string DllPath(string identifier) { - return installed_dlls.TryGetValue(identifier, out string path) ? path : null; + return (installed_dlls.TryGetValue(identifier, out string path) + && path.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase)) + ? path + : null; } /// @@ -620,7 +623,7 @@ public GameVersion LatestCompatibleKSP(string identifier) /// Return parameter for the highest game version public static void GetMinMaxVersions(IEnumerable modVersions, out ModuleVersion minMod, out ModuleVersion maxMod, - out GameVersion minKsp, out GameVersion maxKsp) + out GameVersion minKsp, out GameVersion maxKsp) { minMod = maxMod = null; minKsp = maxKsp = null; @@ -839,41 +842,14 @@ public void DeregisterModule(GameInstance ksp, string module) installed_modules.Remove(module); } - /// - /// Registers the given DLL as having been installed. This provides some support - /// for pre-CKAN modules. - /// - /// Does nothing if the DLL is already part of an installed module. - /// - public void RegisterDll(GameInstance ksp, string absolute_path) + public void RegisterFile(string relativePath, string identifier) { - log.DebugFormat("Registering DLL {0}", absolute_path); - string relative_path = ksp.ToRelativeGameDir(absolute_path); - - string dllIdentifier = ksp.DllPathToIdentifier(relative_path); - if (dllIdentifier == null) - { - log.WarnFormat("Attempted to index {0} which is not a DLL", relative_path); - return; - } - - string owner; - if (installed_files.TryGetValue(relative_path, out owner)) + if (!installed_dlls.ContainsKey(identifier)) { - log.InfoFormat( - "Not registering {0}, it belongs to {1}", - relative_path, - owner - ); - return; + EnlistWithTransaction(); + log.InfoFormat("Registering {0} from {1}", identifier, relativePath); + installed_dlls[identifier] = relativePath; } - - EnlistWithTransaction(); - - log.InfoFormat("Registering {0} from {1}", dllIdentifier, relative_path); - - // We're fine if we overwrite an existing key. - installed_dlls[dllIdentifier] = relative_path; } /// @@ -1048,9 +1024,9 @@ public CkanModule GetInstalledVersion(string mod_identifier) /// Returns the module which owns this file, or null if not known. /// Throws a PathErrorKraken if an absolute path is provided. /// - public string FileOwner(string file) + public string FileOwner(string relativePath) { - file = CKANPathUtils.NormalizePath(file); + var file = CKANPathUtils.NormalizePath(relativePath); if (Path.IsPathRooted(file)) { diff --git a/Tests/Core/GameInstance.cs b/Tests/Core/GameInstance.cs index fb57d1c99e..dc57630026 100644 --- a/Tests/Core/GameInstance.cs +++ b/Tests/Core/GameInstance.cs @@ -99,7 +99,7 @@ public void ScanDlls() ksp.Scan(); - Assert.IsTrue(registry.IsInstalled("NewMod")); + Assert.IsTrue(registry.IsInstalled("NewMod"), "NewMod installed"); } [Test] diff --git a/Tests/Core/Registry/Registry.cs b/Tests/Core/Registry/Registry.cs index cf7730a29f..e81863361b 100644 --- a/Tests/Core/Registry/Registry.cs +++ b/Tests/Core/Registry/Registry.cs @@ -236,8 +236,9 @@ public void HasUpdate_WithUpgradeableManuallyInstalledMod_ReturnsTrue() }"); registry.AddAvailable(mod); GameInstance gameInst = gameInstWrapper.KSP; - registry.RegisterDll(gameInst, Path.Combine( - gameInst.GameDir(), "GameData", $"{mod.identifier}.dll")); + registry.RegisterFile( + Path.Combine("GameData", $"{mod.identifier}.dll"), + mod.identifier); GameVersionCriteria crit = new GameVersionCriteria(mod.ksp_version); // Act diff --git a/Tests/Core/Relationships/RelationshipResolver.cs b/Tests/Core/Relationships/RelationshipResolver.cs index 10f73c5dc0..50f6ee7e2c 100644 --- a/Tests/Core/Relationships/RelationshipResolver.cs +++ b/Tests/Core/Relationships/RelationshipResolver.cs @@ -892,9 +892,11 @@ public void ReasonFor_WithTreeOfMods_GivesCorrectParents() [Test] public void AutodetectedCanSatisfyRelationships() { - using (var ksp = new DisposableKSP ()) + using (var ksp = new DisposableKSP()) { - registry.RegisterDll(ksp.KSP, Path.Combine(ksp.KSP.game.PrimaryModDirectory(ksp.KSP), "ModuleManager.dll")); + registry.RegisterFile( + Path.Combine(ksp.KSP.game.PrimaryModDirectoryRelative, "ModuleManager.dll"), + "ModuleManager"); var depends = new List(); depends.Add(new CKAN.ModuleRelationshipDescriptor { name = "ModuleManager" }); @@ -906,7 +908,7 @@ public void AutodetectedCanSatisfyRelationships() null, RelationshipResolver.DefaultOpts(), registry, - new GameVersionCriteria (GameVersion.Parse("1.0.0")) + new GameVersionCriteria(GameVersion.Parse("1.0.0")) ); } }