using System; using System.Collections.Generic; using System.IO; using System.Reflection; using BepInEx.Harmony; using Harmony; using Mono.Cecil; namespace BepInEx.Bootstrap { /// /// Delegate used in patching assemblies. /// /// The assembly that is being patched. public delegate void AssemblyPatcherDelegate(ref AssemblyDefinition assembly); /// /// Worker class which is used for loading and patching entire folders of assemblies, or alternatively patching and loading assemblies one at a time. /// public static class AssemblyPatcher { /// /// Configuration value of whether assembly dumping is enabled or not. /// private static bool DumpingEnabled => Utility.SafeParseBool(Config.GetEntry("dump-assemblies", "false", "Preloader")); /// /// Patches and loads an entire directory of assemblies. /// /// The directory to load assemblies from. /// The dictionary of patchers and their targeted assembly filenames which they are patching. /// List of initializers to run before any patching starts /// List of finalizers to run before returning public static void PatchAll(string directory, IDictionary> patcherMethodDictionary, IEnumerable initializers = null, IEnumerable finalizers = null) { //run all initializers if (initializers != null) foreach (Action init in initializers) init.Invoke(); //load all the requested assemblies Dictionary assemblies = new Dictionary(); foreach (string assemblyPath in Directory.GetFiles(directory, "*.dll")) { var assembly = AssemblyDefinition.ReadAssembly(assemblyPath); //NOTE: this is special cased here because the dependency handling for System.dll is a bit wonky //System has an assembly reference to itself, and it also has a reference to Mono.Security causing a circular dependency //It's also generally dangerous to change system.dll since so many things rely on it, // and it's already loaded into the appdomain since this loader references it, so we might as well skip it if (assembly.Name.Name == "System" || assembly.Name.Name == "mscorlib") //mscorlib is already loaded into the appdomain so it can't be patched { assembly.Dispose(); continue; } assemblies.Add(Path.GetFileName(assemblyPath), assembly); PatchedAssemblyResolver.AssemblyLocations.Add(assembly.FullName, Path.GetFullPath(assemblyPath)); } HashSet patchedAssemblies = new HashSet(); //call the patchers on the assemblies foreach (var patcherMethod in patcherMethodDictionary) { foreach (string assemblyFilename in patcherMethod.Value) { if (assemblies.TryGetValue(assemblyFilename, out var assembly)) { Patch(ref assembly, patcherMethod.Key); assemblies[assemblyFilename] = assembly; patchedAssemblies.Add(assemblyFilename); } } } // Finally, load all assemblies into memory foreach (var kv in assemblies) { string filename = kv.Key; var assembly = kv.Value; if (DumpingEnabled && patchedAssemblies.Contains(filename)) { using (MemoryStream mem = new MemoryStream()) { string dirPath = Path.Combine(Paths.PluginPath, "DumpedAssemblies"); if (!Directory.Exists(dirPath)) Directory.CreateDirectory(dirPath); assembly.Write(mem); File.WriteAllBytes(Path.Combine(dirPath, filename), mem.ToArray()); } } Load(assembly); assembly.Dispose(); } // Patch Assembly.Location and Assembly.CodeBase only if the assemblies were loaded from memory PatchedAssemblyResolver.ApplyPatch(); //run all finalizers if (finalizers != null) foreach (Action finalizer in finalizers) finalizer.Invoke(); } /// /// Patches an individual assembly, without loading it. /// /// The assembly definition to apply the patch to. /// The patcher to use to patch the assembly definition. public static void Patch(ref AssemblyDefinition assembly, AssemblyPatcherDelegate patcherMethod) { patcherMethod.Invoke(ref assembly); } /// /// Loads an individual assembly defintion into the CLR. /// /// The assembly to load. public static void Load(AssemblyDefinition assembly) { using (MemoryStream assemblyStream = new MemoryStream()) { assembly.Write(assemblyStream); Assembly.Load(assemblyStream.ToArray()); } } } internal static class PatchedAssemblyResolver { public static HarmonyInstance HarmonyInstance { get; } = HarmonyInstance.Create("com.bepis.bepinex.asmlocationfix"); public static Dictionary AssemblyLocations { get; } = new Dictionary(StringComparer.InvariantCultureIgnoreCase); public static void ApplyPatch() { HarmonyWrapper.PatchAll(typeof(PatchedAssemblyResolver), HarmonyInstance); } [HarmonyPostfix, HarmonyPatch(typeof(Assembly), nameof(Assembly.Location), MethodType.Getter)] public static void GetLocation(ref string __result, Assembly __instance) { if (AssemblyLocations.TryGetValue(__instance.FullName, out string location)) __result = location; } [HarmonyPostfix, HarmonyPatch(typeof(Assembly), nameof(Assembly.CodeBase), MethodType.Getter)] public static void GetCodeBase(ref string __result, Assembly __instance) { if (AssemblyLocations.TryGetValue(__instance.FullName, out string location)) __result = $"file://{location.Replace('\\', '/')}"; } } }