/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="fullTypeName">The full type name for which to find references.</param> /// <param name="methodName">The method name for which to find references.</param> /// <param name="result">The result to return for matching instructions.</param> public MethodFinder(string fullTypeName, string methodName, InstructionHandleResult result) : base(defaultPhrase: $"{fullTypeName}.{methodName} method") { this.FullTypeName = fullTypeName; this.MethodName = methodName; this.Result = result; }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="fullTypeName">The full type name for which to find references.</param> /// <param name="eventName">The event name for which to find references.</param> /// <param name="result">The result to return for matching instructions.</param> public EventFinder(string fullTypeName, string eventName, InstructionHandleResult result) : base(defaultPhrase: $"{fullTypeName}.{eventName} event") { this.FullTypeName = fullTypeName; this.EventName = eventName; this.Result = result; }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="fullTypeName">The full type name for which to find references.</param> /// <param name="propertyName">The property name for which to find references.</param> /// <param name="result">The result to return for matching instructions.</param> public PropertyFinder(string fullTypeName, string propertyName, InstructionHandleResult result) : base(defaultPhrase: $"{fullTypeName}.{propertyName} property") { this.FullTypeName = fullTypeName; this.PropertyName = propertyName; this.Result = result; }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="fullTypeName">The full type name for which to find references.</param> /// <param name="methodName">The method name for which to find references.</param> /// <param name="result">The result to return for matching instructions.</param> public MethodFinder(string fullTypeName, string methodName, InstructionHandleResult result) { this.FullTypeName = fullTypeName; this.MethodName = methodName; this.Result = result; this.NounPhrase = $"{fullTypeName}.{methodName} method"; }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="fullTypeName">The full type name to match.</param> /// <param name="result">The result to return for matching instructions.</param> /// <param name="shouldIgnore">A lambda which overrides a matched type.</param> public TypeFinder(string fullTypeName, InstructionHandleResult result, Func <TypeReference, bool> shouldIgnore = null) { this.FullTypeName = fullTypeName; this.Result = result; this.NounPhrase = $"{fullTypeName} type"; this.ShouldIgnore = shouldIgnore ?? (p => false); }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="fullTypeName">The full type name for which to find references.</param> /// <param name="fieldName">The field name for which to find references.</param> /// <param name="result">The result to return for matching instructions.</param> public FieldFinder(string fullTypeName, string fieldName, InstructionHandleResult result) : base(defaultPhrase: $"{fullTypeName}.{fieldName} field") { this.FullTypeName = fullTypeName; this.FieldName = fieldName; this.Result = result; }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="fullTypeName">The full type name for which to find references.</param> /// <param name="fieldNames">The field names for which to find references.</param> /// <param name="result">The result to return for matching instructions.</param> public FieldFinder(string fullTypeName, string[] fieldNames, InstructionHandleResult result) : base(defaultPhrase: $"{string.Join(", ", fieldNames.Select(p => $"{fullTypeName}.{p}"))} field{(fieldNames.Length != 1 ? "s" : "")}") // default phrase should never be used { this.FullTypeName = fullTypeName; this.FieldNames = new HashSet <string>(fieldNames); this.Result = result; }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="fullTypeName">The full type name for which to find references.</param> /// <param name="fieldName">The field name for which to find references.</param> /// <param name="result">The result to return for matching instructions.</param> public FieldFinder(string fullTypeName, string fieldName, InstructionHandleResult result) { this.FullTypeName = fullTypeName; this.FieldName = fieldName; this.Result = result; this.NounPhrase = $"{fullTypeName}.{fieldName} field"; }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="fullTypeName">The full type name for which to find references.</param> /// <param name="propertyName">The property name for which to find references.</param> /// <param name="result">The result to return for matching instructions.</param> public PropertyFinder(string fullTypeName, string propertyName, InstructionHandleResult result) { this.FullTypeName = fullTypeName; this.PropertyName = propertyName; this.Result = result; this.NounPhrase = $"{fullTypeName}.{propertyName} property"; }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="fullTypeName">The full type name for which to find references.</param> /// <param name="eventName">The event name for which to find references.</param> /// <param name="result">The result to return for matching instructions.</param> public EventFinder(string fullTypeName, string eventName, InstructionHandleResult result) { this.FullTypeName = fullTypeName; this.EventName = eventName; this.Result = result; this.NounPhrase = $"{fullTypeName}.{eventName} event"; }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="fullTypeNames">The full type names to match.</param> /// <param name="result">The result to return for matching instructions.</param> /// <param name="shouldIgnore">Get whether a matched type should be ignored.</param> public TypeFinder(string[] fullTypeNames, InstructionHandleResult result, Func <TypeReference, bool> shouldIgnore = null) : base(defaultPhrase: $"{string.Join(", ", fullTypeNames)} type{(fullTypeNames.Length != 1 ? "s" : "")}") // default phrase should never be used { this.FullTypeNames = new HashSet <string>(fullTypeNames); this.Result = result; this.ShouldIgnore = shouldIgnore; }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="fullTypeName">The full type name to match.</param> /// <param name="result">The result to return for matching instructions.</param> /// <param name="shouldIgnore">Get whether a matched type should be ignored.</param> public TypeFinder(string fullTypeName, InstructionHandleResult result, Func <TypeReference, bool> shouldIgnore = null) : base(defaultPhrase: $"{fullTypeName} type") { this.FullTypeName = fullTypeName; this.Result = result; this.ShouldIgnore = shouldIgnore; }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="assemblyName">The full assembly name to which to find references.</param> /// <param name="result">The result to return for matching instructions.</param> /// <param name="shouldIgnore">Get whether a matched type should be ignored.</param> public TypeAssemblyFinder(string assemblyName, InstructionHandleResult result, Func <TypeReference, bool> shouldIgnore = null) : base(defaultPhrase: $"{assemblyName} assembly") { this.AssemblyName = assemblyName; this.Result = result; this.ShouldIgnore = shouldIgnore; }
/// <summary>Raise a result flag.</summary> /// <param name="flag">The result flag to set.</param> /// <param name="resultMessage">The result message to add.</param> /// <returns>Returns true for convenience.</returns> protected bool MarkFlag(InstructionHandleResult flag, string resultMessage = null) { this.Flags.Add(flag); if (resultMessage != null) { this.Phrases.Add(resultMessage); } return(true); }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="fullTypeName">The full type name for which to find references.</param> /// <param name="eventNames">The event names for which to find references.</param> /// <param name="result">The result to return for matching instructions.</param> public EventFinder(string fullTypeName, string[] eventNames, InstructionHandleResult result) : base(defaultPhrase: $"{string.Join(", ", eventNames.Select(p => $"{fullTypeName}.{p}"))} event{(eventNames.Length != 1 ? "s" : "")}") // default phrase should never be used { this.FullTypeName = fullTypeName; this.Result = result; this.MethodNames = new HashSet <string>(); foreach (string name in eventNames) { this.MethodNames.Add($"add_{name}"); this.MethodNames.Add($"remove_{name}"); } }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="fullTypeName">The full type name to match.</param> /// <param name="result">The result to return for matching instructions.</param> public TypeFinder(string fullTypeName, InstructionHandleResult result) { this.FullTypeName = fullTypeName; this.Result = result; this.NounPhrase = $"{fullTypeName} type"; }
/// <summary>Construct an instance.</summary> /// <param name="fullTypeName">The full type name to match.</param> /// <param name="result">The result to return for matching instructions.</param> /// <param name="shouldIgnore">Get whether a matched type should be ignored.</param> public TypeFinder(string fullTypeName, InstructionHandleResult result, Func <TypeReference, bool> shouldIgnore = null) : this(new[] { fullTypeName }, result, shouldIgnore) { }
/// <summary>Construct an instance.</summary> /// <param name="fullTypeName">The full type name for which to find references.</param> /// <param name="eventName">The event name for which to find references.</param> /// <param name="result">The result to return for matching instructions.</param> public EventFinder(string fullTypeName, string eventName, InstructionHandleResult result) : this(fullTypeName, new[] { eventName }, result) { }
/// <summary>Process the result from an instruction handler.</summary> /// <param name="mod">The mod being analysed.</param> /// <param name="handler">The instruction handler.</param> /// <param name="result">The result returned by the handler.</param> /// <param name="loggedMessages">The messages already logged for the current mod.</param> /// <param name="assumeCompatible">Assume the mod is compatible, even if incompatible code is detected.</param> /// <param name="logPrefix">A string to prefix to log messages.</param> /// <param name="filename">The assembly filename for log messages.</param> private void ProcessInstructionHandleResult(IModMetadata mod, IInstructionHandler handler, InstructionHandleResult result, HashSet <string> loggedMessages, string logPrefix, bool assumeCompatible, string filename) { switch (result) { case InstructionHandleResult.Rewritten: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewrote {filename} to fix {handler.NounPhrase}..."); break; case InstructionHandleResult.NotCompatible: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Broken code in {filename}: {handler.NounPhrase}."); if (!assumeCompatible) { throw new IncompatibleInstructionException(handler.NounPhrase, $"Found an incompatible CIL instruction ({handler.NounPhrase}) while loading assembly {filename}."); } mod.SetWarning(ModWarning.BrokenCodeLoaded); break; case InstructionHandleResult.DetectedGamePatch: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected game patcher ({handler.NounPhrase}) in assembly {filename}."); mod.SetWarning(ModWarning.PatchesGame); break; case InstructionHandleResult.DetectedSaveSerialiser: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected possible save serialiser change ({handler.NounPhrase}) in assembly {filename}."); mod.SetWarning(ModWarning.ChangesSaveSerialiser); break; case InstructionHandleResult.DetectedUnvalidatedUpdateTick: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected reference to {handler.NounPhrase} in assembly {filename}."); mod.SetWarning(ModWarning.UsesUnvalidatedUpdateTick); break; case InstructionHandleResult.DetectedDynamic: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected 'dynamic' keyword ({handler.NounPhrase}) in assembly {filename}."); 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; default: throw new NotSupportedException($"Unrecognised instruction handler result '{result}'."); } }
/**** ** Assembly rewriting ****/ /// <summary>Rewrite the types referenced by an assembly.</summary> /// <param name="mod">The mod for which the assembly is being loaded.</param> /// <param name="assembly">The assembly to rewrite.</param> /// <param name="assumeCompatible">Assume the mod is compatible, even if incompatible code is detected.</param> /// <param name="loggedMessages">The messages that have already been logged for this mod.</param> /// <param name="logPrefix">A string to prefix to log messages.</param> /// <returns>Returns whether the assembly was modified.</returns> /// <exception cref="IncompatibleInstructionException">An incompatible CIL instruction was found while rewriting the assembly.</exception> private bool RewriteAssembly(IModMetadata mod, AssemblyDefinition assembly, bool assumeCompatible, HashSet <string> loggedMessages, string logPrefix) { ModuleDefinition module = assembly.MainModule; string filename = $"{assembly.Name.Name}.dll"; // swap assembly references if needed (e.g. XNA => MonoGame) bool platformChanged = false; for (int i = 0; i < module.AssemblyReferences.Count; i++) { // remove old assembly reference if (this.AssemblyMap.RemoveNames.Any(name => module.AssemblyReferences[i].Name == name)) { this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewriting {filename} for OS..."); platformChanged = true; module.AssemblyReferences.RemoveAt(i); i--; } } if (platformChanged) { // add target assembly references foreach (AssemblyNameReference target in this.AssemblyMap.TargetReferences.Values) { module.AssemblyReferences.Add(target); } // rewrite type scopes to use target assemblies IEnumerable <TypeReference> typeReferences = module.GetTypeReferences().OrderBy(p => p.FullName); foreach (TypeReference type in typeReferences) { this.ChangeTypeScope(type); } } // find (and optionally rewrite) incompatible instructions bool anyRewritten = false; IInstructionHandler[] handlers = new InstructionMetadata().GetHandlers(this.ParanoidMode).ToArray(); foreach (MethodDefinition method in this.GetMethods(module)) { // check method definition foreach (IInstructionHandler handler in handlers) { InstructionHandleResult result = handler.Handle(module, method, this.AssemblyMap, platformChanged); this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, assumeCompatible, filename); if (result == InstructionHandleResult.Rewritten) { anyRewritten = true; } } // check CIL instructions ILProcessor cil = method.Body.GetILProcessor(); var instructions = cil.Body.Instructions; // ReSharper disable once ForCanBeConvertedToForeach -- deliberate access by index so each handler sees replacements from previous handlers for (int offset = 0; offset < instructions.Count; offset++) { foreach (IInstructionHandler handler in handlers) { Instruction instruction = instructions[offset]; InstructionHandleResult result = handler.Handle(module, cil, instruction, this.AssemblyMap, platformChanged); this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, assumeCompatible, filename); if (result == InstructionHandleResult.Rewritten) { anyRewritten = true; } } } } return(platformChanged || anyRewritten); }
/// <summary>Process the result from an instruction handler.</summary> /// <param name="mod">The mod being analysed.</param> /// <param name="handler">The instruction handler.</param> /// <param name="result">The result returned by the handler.</param> /// <param name="loggedMessages">The messages already logged for the current mod.</param> /// <param name="assumeCompatible">Assume the mod is compatible, even if incompatible code is detected.</param> /// <param name="logPrefix">A string to prefix to log messages.</param> /// <param name="filename">The assembly filename for log messages.</param> private void ProcessInstructionHandleResult(IModMetadata mod, IInstructionHandler handler, InstructionHandleResult result, HashSet <string> loggedMessages, string logPrefix, bool assumeCompatible, string filename) { switch (result) { case InstructionHandleResult.Rewritten: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewrote {filename} to fix {handler.NounPhrase}..."); break; case InstructionHandleResult.NotCompatible: if (!assumeCompatible) { throw new IncompatibleInstructionException(handler.NounPhrase, $"Found an incompatible CIL instruction ({handler.NounPhrase}) while loading assembly {filename}."); } this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Found an incompatible CIL instruction ({handler.NounPhrase}) while loading assembly {filename}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); break; case InstructionHandleResult.DetectedGamePatch: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected game patcher ({handler.NounPhrase}) in assembly {filename}."); this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} patches the game, which may impact game stability. If you encounter problems, try removing this mod first.", LogLevel.Warn); break; case InstructionHandleResult.DetectedSaveSerialiser: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected possible save serialiser change ({handler.NounPhrase}) in assembly {filename}."); this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} seems to change the save serialiser. It may change your saves in such a way that they won't work without this mod in the future.", LogLevel.Warn); break; case InstructionHandleResult.DetectedDynamic: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected 'dynamic' keyword ({handler.NounPhrase}) in assembly {filename}."); this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} uses the 'dynamic' keyword, which isn't compatible with Stardew Valley on Linux or Mac.", #if SMAPI_FOR_WINDOWS this.IsDeveloperMode ? LogLevel.Warn : LogLevel.Debug #else LogLevel.Warn #endif ); break; case InstructionHandleResult.None: break; default: throw new NotSupportedException($"Unrecognised instruction handler result '{result}'."); } }
/// <summary>Process the result from an instruction handler.</summary> /// <param name="mod">The mod being analyzed.</param> /// <param name="handler">The instruction handler.</param> /// <param name="result">The result returned by the handler.</param> /// <param name="loggedMessages">The messages already logged for the current mod.</param> /// <param name="logPrefix">A string to prefix to log messages.</param> /// <param name="filename">The assembly filename for log messages.</param> private void ProcessInstructionHandleResult(IModMetadata mod, IInstructionHandler handler, InstructionHandleResult result, HashSet <string> loggedMessages, string logPrefix, string filename) { // get message template // ($phrase is replaced with the noun phrase or messages) string template = null; switch (result) { case InstructionHandleResult.Rewritten: template = $"{logPrefix}Rewrote {filename} to fix $phrase..."; break; case InstructionHandleResult.NotCompatible: template = $"{logPrefix}Broken code in {filename}: $phrase."; mod.SetWarning(ModWarning.BrokenCodeLoaded); break; case InstructionHandleResult.DetectedGamePatch: template = $"{logPrefix}Detected game patcher ($phrase) in assembly {filename}."; mod.SetWarning(ModWarning.PatchesGame); break; case InstructionHandleResult.DetectedSaveSerializer: template = $"{logPrefix}Detected possible save serializer change ($phrase) in assembly {filename}."; mod.SetWarning(ModWarning.ChangesSaveSerializer); break; case InstructionHandleResult.DetectedUnvalidatedUpdateTick: template = $"{logPrefix}Detected reference to $phrase in assembly {filename}."; mod.SetWarning(ModWarning.UsesUnvalidatedUpdateTick); break; case InstructionHandleResult.DetectedDynamic: template = $"{logPrefix}Detected 'dynamic' keyword ($phrase) in assembly {filename}."; mod.SetWarning(ModWarning.UsesDynamic); break; case InstructionHandleResult.DetectedConsoleAccess: template = $"{logPrefix}Detected direct console access ($phrase) in assembly {filename}."; mod.SetWarning(ModWarning.AccessesConsole); break; case InstructionHandleResult.DetectedFilesystemAccess: template = $"{logPrefix}Detected filesystem access ($phrase) in assembly {filename}."; mod.SetWarning(ModWarning.AccessesFilesystem); break; case InstructionHandleResult.DetectedShellAccess: template = $"{logPrefix}Detected shell or process access ($phrase) in assembly {filename}."; mod.SetWarning(ModWarning.AccessesShell); break; case InstructionHandleResult.None: break; default: throw new NotSupportedException($"Unrecognized instruction handler result '{result}'."); } if (template == null) { return; } // format messages if (handler.Phrases.Any()) { foreach (string message in handler.Phrases) { this.Monitor.LogOnce(loggedMessages, template.Replace("$phrase", message)); } } else { this.Monitor.LogOnce(loggedMessages, template.Replace("$phrase", handler.DefaultPhrase ?? handler.GetType().Name)); } }
/// <summary>Construct an instance.</summary> /// <param name="fullTypeName">The full type name for which to find references.</param> /// <param name="fieldName">The field name for which to find references.</param> /// <param name="result">The result to return for matching instructions.</param> public FieldFinder(string fullTypeName, string fieldName, InstructionHandleResult result) : this(fullTypeName, new[] { fieldName }, result) { }