summaryrefslogtreecommitdiff
path: root/src/SMAPI/Framework/ModLoading
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2018-11-19 13:48:19 -0500
committerJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2018-11-19 13:48:19 -0500
commit593723b7940ba72a786fc4c7366c56f9813d977b (patch)
tree4d23fbef5bc5a20115f10ca04ae3379df78cc8e1 /src/SMAPI/Framework/ModLoading
parent4f28ea33bd7cc65485402c5e85259083e86b49e1 (diff)
parent3dc27a5681dcfc4ae30e95570d9966f2e14a4dd7 (diff)
downloadSMAPI-593723b7940ba72a786fc4c7366c56f9813d977b.tar.gz
SMAPI-593723b7940ba72a786fc4c7366c56f9813d977b.tar.bz2
SMAPI-593723b7940ba72a786fc4c7366c56f9813d977b.zip
Merge branch 'develop' into stable
Diffstat (limited to 'src/SMAPI/Framework/ModLoading')
-rw-r--r--src/SMAPI/Framework/ModLoading/AssemblyLoader.cs11
-rw-r--r--src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs8
-rw-r--r--src/SMAPI/Framework/ModLoading/ModMetadata.cs42
-rw-r--r--src/SMAPI/Framework/ModLoading/ModResolver.cs23
-rw-r--r--src/SMAPI/Framework/ModLoading/ModWarning.cs8
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/VirtualEntryCallRemover.cs90
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;
- }
- }
-}