diff options
author | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2018-11-19 13:48:19 -0500 |
---|---|---|
committer | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2018-11-19 13:48:19 -0500 |
commit | 593723b7940ba72a786fc4c7366c56f9813d977b (patch) | |
tree | 4d23fbef5bc5a20115f10ca04ae3379df78cc8e1 /src/SMAPI/Framework/ModLoading | |
parent | 4f28ea33bd7cc65485402c5e85259083e86b49e1 (diff) | |
parent | 3dc27a5681dcfc4ae30e95570d9966f2e14a4dd7 (diff) | |
download | SMAPI-593723b7940ba72a786fc4c7366c56f9813d977b.tar.gz SMAPI-593723b7940ba72a786fc4c7366c56f9813d977b.tar.bz2 SMAPI-593723b7940ba72a786fc4c7366c56f9813d977b.zip |
Merge branch 'develop' into stable
Diffstat (limited to 'src/SMAPI/Framework/ModLoading')
6 files changed, 70 insertions, 112 deletions
diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index 37b1a378..fdbfdd8d 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -45,6 +45,7 @@ namespace StardewModdingAPI.Framework.ModLoading this.AssemblyMap = this.TrackForDisposal(Constants.GetAssemblyMap(targetPlatform)); this.AssemblyDefinitionResolver = this.TrackForDisposal(new AssemblyDefinitionResolver()); this.AssemblyDefinitionResolver.AddSearchDirectory(Constants.ExecutionPath); + this.AssemblyDefinitionResolver.AddSearchDirectory(Constants.InternalFilesPath); // generate type => assembly lookup for types which should be rewritten this.TypeAssemblies = new Dictionary<string, Assembly>(); @@ -349,6 +350,16 @@ namespace StardewModdingAPI.Framework.ModLoading mod.SetWarning(ModWarning.UsesDynamic); break; + case InstructionHandleResult.DetectedFilesystemAccess: + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected filesystem access ({handler.NounPhrase}) in assembly {filename}."); + mod.SetWarning(ModWarning.AccessesFilesystem); + break; + + case InstructionHandleResult.DetectedShellAccess: + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected shell or process access ({handler.NounPhrase}) in assembly {filename}."); + mod.SetWarning(ModWarning.AccessesShell); + break; + case InstructionHandleResult.None: break; diff --git a/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs b/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs index cfa23d08..f3555c2d 100644 --- a/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs +++ b/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs @@ -24,6 +24,12 @@ namespace StardewModdingAPI.Framework.ModLoading DetectedDynamic, /// <summary>The instruction is compatible, but references <see cref="SpecialisedEvents.UnvalidatedUpdateTick"/> which may impact stability.</summary> - DetectedUnvalidatedUpdateTick + DetectedUnvalidatedUpdateTick, + + /// <summary>The instruction accesses the filesystem directly.</summary> + DetectedFilesystemAccess, + + /// <summary>The instruction accesses the OS shell or processes directly.</summary> + DetectedShellAccess } } diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 585debb4..0cb62a75 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using System.Linq; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; using StardewModdingAPI.Toolkit.Framework.ModData; +using StardewModdingAPI.Toolkit.Framework.UpdateData; namespace StardewModdingAPI.Framework.ModLoading { @@ -17,6 +19,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// <summary>The mod's full directory path.</summary> public string DirectoryPath { get; } + /// <summary>The <see cref="IModMetadata.DirectoryPath"/> relative to the game's Mods folder.</summary> + public string RelativeDirectoryPath { get; } + /// <summary>The mod manifest.</summary> public IManifest Manifest { get; } @@ -32,6 +37,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// <summary>The reason the metadata is invalid, if any.</summary> public string Error { get; private set; } + /// <summary>Whether the mod folder should be ignored. This is <c>true</c> if it was found within a folder whose name starts with a dot.</summary> + public bool IsIgnored { get; } + /// <summary>The mod instance (if loaded and <see cref="IsContentPack"/> is false).</summary> public IMod Mod { get; private set; } @@ -57,14 +65,18 @@ namespace StardewModdingAPI.Framework.ModLoading /// <summary>Construct an instance.</summary> /// <param name="displayName">The mod's display name.</param> /// <param name="directoryPath">The mod's full directory path.</param> + /// <param name="relativeDirectoryPath">The <paramref name="directoryPath"/> relative to the game's Mods folder.</param> /// <param name="manifest">The mod manifest.</param> /// <param name="dataRecord">Metadata about the mod from SMAPI's internal data (if any).</param> - public ModMetadata(string displayName, string directoryPath, IManifest manifest, ModDataRecordVersionedFields dataRecord) + /// <param name="isIgnored">Whether the mod folder should be ignored. This should be <c>true</c> if it was found within a folder whose name starts with a dot.</param> + public ModMetadata(string displayName, string directoryPath, string relativeDirectoryPath, IManifest manifest, ModDataRecordVersionedFields dataRecord, bool isIgnored) { this.DisplayName = displayName; this.DirectoryPath = directoryPath; + this.RelativeDirectoryPath = relativeDirectoryPath; this.Manifest = manifest; this.DataRecord = dataRecord; + this.IsIgnored = isIgnored; } /// <summary>Set the mod status.</summary> @@ -141,13 +153,31 @@ namespace StardewModdingAPI.Framework.ModLoading && !string.IsNullOrWhiteSpace(this.Manifest.UniqueID); } - /// <summary>Whether the mod has at least one update key set.</summary> - public bool HasUpdateKeys() + /// <summary>Whether the mod has the given ID.</summary> + /// <param name="id">The mod ID to check.</param> + public bool HasID(string id) { return - this.HasManifest() - && this.Manifest.UpdateKeys != null - && this.Manifest.UpdateKeys.Any(key => !string.IsNullOrWhiteSpace(key)); + this.HasID() + && string.Equals(this.Manifest.UniqueID.Trim(), id?.Trim(), StringComparison.InvariantCultureIgnoreCase); + } + + /// <summary>Get the defined update keys.</summary> + /// <param name="validOnly">Only return valid update keys.</param> + public IEnumerable<UpdateKey> GetUpdateKeys(bool validOnly = false) + { + foreach (string rawKey in this.Manifest?.UpdateKeys ?? new string[0]) + { + UpdateKey updateKey = UpdateKey.Parse(rawKey); + if (updateKey.LooksValid || !validOnly) + yield return updateKey; + } + } + + /// <summary>Whether the mod has at least one valid update key set.</summary> + public bool HasValidUpdateKeys() + { + return this.GetUpdateKeys(validOnly: true).Any(); } } } diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 9ac95fd4..ace84054 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text.RegularExpressions; using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Framework.ModScanning; @@ -31,13 +30,6 @@ namespace StardewModdingAPI.Framework.ModLoading // parse internal data record (if any) ModDataRecordVersionedFields dataRecord = modDatabase.Get(manifest?.UniqueID)?.GetVersionedFields(manifest); - // get display name - string displayName = manifest?.Name; - if (string.IsNullOrWhiteSpace(displayName)) - displayName = dataRecord?.DisplayName; - if (string.IsNullOrWhiteSpace(displayName)) - displayName = PathUtilities.GetRelativePath(rootPath, folder.ActualDirectory?.FullName ?? folder.SearchDirectory.FullName); - // apply defaults if (manifest != null && dataRecord != null) { @@ -46,10 +38,13 @@ namespace StardewModdingAPI.Framework.ModLoading } // build metadata - ModMetadataStatus status = folder.ManifestParseError == null + ModMetadataStatus status = folder.ManifestParseError == null || !folder.ShouldBeLoaded ? ModMetadataStatus.Found : ModMetadataStatus.Failed; - yield return new ModMetadata(displayName, folder.ActualDirectory?.FullName, manifest, dataRecord).SetStatus(status, folder.ManifestParseError); + string relativePath = PathUtilities.GetRelativePath(rootPath, folder.Directory.FullName); + + yield return new ModMetadata(folder.DisplayName, folder.Directory.FullName, relativePath, manifest, dataRecord, isIgnored: !folder.ShouldBeLoaded) + .SetStatus(status, !folder.ShouldBeLoaded ? "disabled by dot convention" : folder.ManifestParseError); } } @@ -92,7 +87,7 @@ namespace StardewModdingAPI.Framework.ModLoading updateUrls.Add(mod.DataRecord.AlternativeUrl); // default update URL - updateUrls.Add("https://smapi.io/compat"); + updateUrls.Add("https://mods.smapi.io"); // build error string error = $"{reasonPhrase}. Please check for a "; @@ -181,7 +176,7 @@ namespace StardewModdingAPI.Framework.ModLoading } // validate ID format - if (Regex.IsMatch(mod.Manifest.UniqueID, "[^a-z0-9_.-]", RegexOptions.IgnoreCase)) + if (!PathUtilities.IsSlug(mod.Manifest.UniqueID)) mod.SetStatus(ModMetadataStatus.Failed, "its manifest specifies an invalid ID (IDs must only contain letters, numbers, underscores, periods, or hyphens)."); } @@ -196,7 +191,7 @@ namespace StardewModdingAPI.Framework.ModLoading { if (mod.Status == ModMetadataStatus.Failed) continue; // don't replace metadata error - mod.SetStatus(ModMetadataStatus.Failed, $"its unique ID '{mod.Manifest.UniqueID}' is used by multiple mods ({string.Join(", ", group.Select(p => p.DisplayName))})."); + mod.SetStatus(ModMetadataStatus.Failed, $"you have multiple copies of this mod installed ({string.Join(", ", group.Select(p => p.RelativeDirectoryPath).OrderBy(p => p))})."); } } } @@ -386,7 +381,7 @@ namespace StardewModdingAPI.Framework.ModLoading /// <param name="loadedMods">The loaded mods.</param> private IEnumerable<ModDependency> GetDependenciesFrom(IManifest manifest, IModMetadata[] loadedMods) { - IModMetadata FindMod(string id) => loadedMods.FirstOrDefault(m => string.Equals(m.Manifest?.UniqueID, id, StringComparison.InvariantCultureIgnoreCase)); + IModMetadata FindMod(string id) => loadedMods.FirstOrDefault(m => m.HasID(id)); // yield dependencies if (manifest.Dependencies != null) diff --git a/src/SMAPI/Framework/ModLoading/ModWarning.cs b/src/SMAPI/Framework/ModLoading/ModWarning.cs index 0e4b2570..c62199b2 100644 --- a/src/SMAPI/Framework/ModLoading/ModWarning.cs +++ b/src/SMAPI/Framework/ModLoading/ModWarning.cs @@ -26,6 +26,12 @@ namespace StardewModdingAPI.Framework.ModLoading UsesUnvalidatedUpdateTick = 16, /// <summary>The mod has no update keys set.</summary> - NoUpdateKeys = 32 + NoUpdateKeys = 32, + + /// <summary>Uses .NET APIs for filesystem access.</summary> + AccessesFilesystem = 64, + + /// <summary>Uses .NET APIs for shell or process access.</summary> + AccessesShell = 128 } } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/VirtualEntryCallRemover.cs b/src/SMAPI/Framework/ModLoading/Rewriters/VirtualEntryCallRemover.cs deleted file mode 100644 index 322a7df1..00000000 --- a/src/SMAPI/Framework/ModLoading/Rewriters/VirtualEntryCallRemover.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using Mono.Cecil; -using Mono.Cecil.Cil; - -namespace StardewModdingAPI.Framework.ModLoading.Rewriters -{ - /// <summary>Rewrites virtual calls to the <see cref="Mod.Entry"/> method.</summary> - internal class VirtualEntryCallRemover : IInstructionHandler - { - /********* - ** Properties - *********/ - /// <summary>The type containing the method.</summary> - private readonly Type ToType; - - /// <summary>The name of the method.</summary> - private readonly string MethodName; - - - /********* - ** Accessors - *********/ - /// <summary>A brief noun phrase indicating what the instruction finder matches.</summary> - public string NounPhrase { get; } - - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - public VirtualEntryCallRemover() - { - this.ToType = typeof(Mod); - this.MethodName = nameof(Mod.Entry); - this.NounPhrase = $"{this.ToType.Name}::{this.MethodName}"; - } - - /// <summary>Perform the predefined logic for a method if applicable.</summary> - /// <param name="module">The assembly module containing the instruction.</param> - /// <param name="method">The method definition containing the instruction.</param> - /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> - /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - public InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return InstructionHandleResult.None; - } - - /// <summary>Perform the predefined logic for an instruction if applicable.</summary> - /// <param name="module">The assembly module containing the instruction.</param> - /// <param name="cil">The CIL processor.</param> - /// <param name="instruction">The instruction to handle.</param> - /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> - /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - public InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - if (!this.IsMatch(instruction)) - return InstructionHandleResult.None; - - // get instructions comprising method call - int index = cil.Body.Instructions.IndexOf(instruction); - Instruction loadArg0 = cil.Body.Instructions[index - 2]; - Instruction loadArg1 = cil.Body.Instructions[index - 1]; - if (loadArg0.OpCode != OpCodes.Ldarg_0) - throw new InvalidOperationException($"Unexpected instruction sequence while removing virtual {this.ToType.Name}.{this.MethodName} call: found {loadArg0.OpCode.Name} instead of {OpCodes.Ldarg_0.Name}"); - if (loadArg1.OpCode != OpCodes.Ldarg_1) - throw new InvalidOperationException($"Unexpected instruction sequence while removing virtual {this.ToType.Name}.{this.MethodName} call: found {loadArg1.OpCode.Name} instead of {OpCodes.Ldarg_1.Name}"); - - // remove method call - cil.Remove(loadArg0); - cil.Remove(loadArg1); - cil.Remove(instruction); - return InstructionHandleResult.Rewritten; - } - - - /********* - ** Protected methods - *********/ - /// <summary>Get whether a CIL instruction matches.</summary> - /// <param name="instruction">The IL instruction.</param> - protected bool IsMatch(Instruction instruction) - { - MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); - return - methodRef != null - && methodRef.DeclaringType.FullName == this.ToType.FullName - && methodRef.Name == this.MethodName; - } - } -} |