AssemblyPatcherLoader.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Reflection;
  5. using BepInEx.Harmony;
  6. using BepInEx.Logging;
  7. using Harmony;
  8. using Mono.Cecil;
  9. using UnityEngine;
  10. namespace BepInEx.Bootstrap
  11. {
  12. /// <summary>
  13. /// Delegate used in patching assemblies.
  14. /// </summary>
  15. /// <param name="assembly">The assembly that is being patched.</param>
  16. public delegate void AssemblyPatcherDelegate(ref AssemblyDefinition assembly);
  17. public class AssemblyPatcher
  18. {
  19. public IEnumerable<string> TargetDLLs { get; set; } = null;
  20. public Action Initializer { get; set; } = null;
  21. public Action Finalizer { get; set; } = null;
  22. public AssemblyPatcherDelegate Patcher { get; set; } = null;
  23. public string Name { get; set; } = string.Empty;
  24. }
  25. /// <summary>
  26. /// Worker class which is used for loading and patching entire folders of assemblies, or alternatively patching and loading assemblies one at a time.
  27. /// </summary>
  28. public static class AssemblyPatcherLoader
  29. {
  30. private static List<AssemblyPatcher> patchers = new List<AssemblyPatcher>();
  31. /// <summary>
  32. /// Configuration value of whether assembly dumping is enabled or not.
  33. /// </summary>
  34. private static bool DumpingEnabled => Utility.SafeParseBool(Config.GetEntry("dump-assemblies", "false", "Preloader"));
  35. public static void AddPatcher(AssemblyPatcher patcher)
  36. {
  37. patchers.Add(patcher);
  38. }
  39. public static void AddPatchersFromDirectory(string directory, Func<Assembly, List<AssemblyPatcher>> patcherLocator)
  40. {
  41. if (!Directory.Exists(directory))
  42. return;
  43. var sortedPatchers = new SortedDictionary<string, AssemblyPatcher>();
  44. foreach (string assemblyPath in Directory.GetFiles(directory, "*.dll"))
  45. try
  46. {
  47. var assembly = Assembly.LoadFrom(assemblyPath);
  48. foreach (var patcher in patcherLocator(assembly))
  49. sortedPatchers.Add(patcher.Name, patcher);
  50. }
  51. catch (BadImageFormatException) { } //unmanaged DLL
  52. catch (ReflectionTypeLoadException) { } //invalid references
  53. foreach (var patcher in sortedPatchers)
  54. AddPatcher(patcher.Value);
  55. }
  56. private static void InitializePatchers()
  57. {
  58. foreach (var assemblyPatcher in patchers)
  59. assemblyPatcher.Initializer?.Invoke();
  60. }
  61. private static void FinalizePatching()
  62. {
  63. foreach (var assemblyPatcher in patchers)
  64. assemblyPatcher.Finalizer?.Invoke();
  65. }
  66. public static void DisposePatchers()
  67. {
  68. patchers.Clear();
  69. }
  70. public static void PatchAndLoad(string directory)
  71. {
  72. // First, load patchable assemblies into Cecil
  73. Dictionary<string, AssemblyDefinition> assemblies = new Dictionary<string, AssemblyDefinition>();
  74. foreach (string assemblyPath in Directory.GetFiles(directory, "*.dll"))
  75. {
  76. var assembly = AssemblyDefinition.ReadAssembly(assemblyPath);
  77. //NOTE: this is special cased here because the dependency handling for System.dll is a bit wonky
  78. //System has an assembly reference to itself, and it also has a reference to Mono.Security causing a circular dependency
  79. //It's also generally dangerous to change system.dll since so many things rely on it,
  80. // and it's already loaded into the appdomain since this loader references it, so we might as well skip it
  81. if (assembly.Name.Name == "System"
  82. || assembly.Name.Name == "mscorlib") //mscorlib is already loaded into the appdomain so it can't be patched
  83. {
  84. assembly.Dispose();
  85. continue;
  86. }
  87. if (PatchedAssemblyResolver.AssemblyLocations.ContainsKey(assembly.FullName))
  88. {
  89. Logger.Log(LogLevel.Warning, $"Tried to load duplicate assembly {Path.GetFileName(assemblyPath)} from Managed folder! Skipping...");
  90. continue;
  91. }
  92. assemblies.Add(Path.GetFileName(assemblyPath), assembly);
  93. PatchedAssemblyResolver.AssemblyLocations.Add(assembly.FullName, Path.GetFullPath(assemblyPath));
  94. }
  95. // Next, initialize all the patchers
  96. InitializePatchers();
  97. // Then, perform the actual patching
  98. HashSet<string> patchedAssemblies = new HashSet<string>();
  99. foreach (var assemblyPatcher in patchers)
  100. {
  101. foreach (string targetDll in assemblyPatcher.TargetDLLs)
  102. {
  103. if (assemblies.TryGetValue(targetDll, out var assembly))
  104. {
  105. assemblyPatcher.Patcher?.Invoke(ref assembly);
  106. assemblies[targetDll] = assembly;
  107. patchedAssemblies.Add(targetDll);
  108. }
  109. }
  110. }
  111. // Finally, load all assemblies into memory
  112. foreach (var kv in assemblies)
  113. {
  114. string filename = kv.Key;
  115. var assembly = kv.Value;
  116. if (DumpingEnabled && patchedAssemblies.Contains(filename))
  117. {
  118. using (MemoryStream mem = new MemoryStream())
  119. {
  120. string dirPath = Path.Combine(Paths.PluginPath, "DumpedAssemblies");
  121. if (!Directory.Exists(dirPath))
  122. Directory.CreateDirectory(dirPath);
  123. assembly.Write(mem);
  124. File.WriteAllBytes(Path.Combine(dirPath, filename), mem.ToArray());
  125. }
  126. }
  127. Load(assembly);
  128. assembly.Dispose();
  129. }
  130. //run all finalizers
  131. FinalizePatching();
  132. }
  133. /// <summary>
  134. /// Patches and loads an entire directory of assemblies.
  135. /// </summary>
  136. /// <param name="directory">The directory to load assemblies from.</param>
  137. /// <param name="patcherMethodDictionary">The dictionary of patchers and their targeted assembly filenames which they are patching.</param>
  138. /// <param name="initializers">List of initializers to run before any patching starts</param>
  139. /// <param name="finalizers">List of finalizers to run before returning</param>
  140. public static void PatchAll(string directory, IDictionary<AssemblyPatcherDelegate, IEnumerable<string>> patcherMethodDictionary, IEnumerable<Action> initializers = null, IEnumerable<Action> finalizers = null)
  141. {
  142. //run all initializers
  143. if (initializers != null)
  144. foreach (Action init in initializers)
  145. init.Invoke();
  146. //load all the requested assemblies
  147. Dictionary<string, AssemblyDefinition> assemblies = new Dictionary<string, AssemblyDefinition>();
  148. foreach (string assemblyPath in Directory.GetFiles(directory, "*.dll"))
  149. {
  150. var assembly = AssemblyDefinition.ReadAssembly(assemblyPath);
  151. //NOTE: this is special cased here because the dependency handling for System.dll is a bit wonky
  152. //System has an assembly reference to itself, and it also has a reference to Mono.Security causing a circular dependency
  153. //It's also generally dangerous to change system.dll since so many things rely on it,
  154. // and it's already loaded into the appdomain since this loader references it, so we might as well skip it
  155. if (assembly.Name.Name == "System"
  156. || assembly.Name.Name == "mscorlib") //mscorlib is already loaded into the appdomain so it can't be patched
  157. {
  158. assembly.Dispose();
  159. continue;
  160. }
  161. assemblies.Add(Path.GetFileName(assemblyPath), assembly);
  162. PatchedAssemblyResolver.AssemblyLocations.Add(assembly.FullName, Path.GetFullPath(assemblyPath));
  163. }
  164. HashSet<string> patchedAssemblies = new HashSet<string>();
  165. //call the patchers on the assemblies
  166. foreach (var patcherMethod in patcherMethodDictionary)
  167. {
  168. foreach (string assemblyFilename in patcherMethod.Value)
  169. {
  170. if (assemblies.TryGetValue(assemblyFilename, out var assembly))
  171. {
  172. Patch(ref assembly, patcherMethod.Key);
  173. assemblies[assemblyFilename] = assembly;
  174. patchedAssemblies.Add(assemblyFilename);
  175. }
  176. }
  177. }
  178. // Finally, load all assemblies into memory
  179. foreach (var kv in assemblies)
  180. {
  181. string filename = kv.Key;
  182. var assembly = kv.Value;
  183. if (DumpingEnabled && patchedAssemblies.Contains(filename))
  184. {
  185. string dirPath = Path.Combine(Paths.PluginPath, "DumpedAssemblies");
  186. if (!Directory.Exists(dirPath))
  187. Directory.CreateDirectory(dirPath);
  188. assembly.Write(Path.Combine(dirPath, filename));
  189. }
  190. Load(assembly);
  191. assembly.Dispose();
  192. }
  193. // Patch Assembly.Location and Assembly.CodeBase only if the assemblies were loaded from memory
  194. PatchedAssemblyResolver.ApplyPatch();
  195. //run all finalizers
  196. if (finalizers != null)
  197. foreach (Action finalizer in finalizers)
  198. finalizer.Invoke();
  199. }
  200. /// <summary>
  201. /// Patches an individual assembly, without loading it.
  202. /// </summary>
  203. /// <param name="assembly">The assembly definition to apply the patch to.</param>
  204. /// <param name="patcherMethod">The patcher to use to patch the assembly definition.</param>
  205. public static void Patch(ref AssemblyDefinition assembly, AssemblyPatcherDelegate patcherMethod)
  206. {
  207. patcherMethod.Invoke(ref assembly);
  208. }
  209. /// <summary>
  210. /// Loads an individual assembly defintion into the CLR.
  211. /// </summary>
  212. /// <param name="assembly">The assembly to load.</param>
  213. public static void Load(AssemblyDefinition assembly)
  214. {
  215. using (MemoryStream assemblyStream = new MemoryStream())
  216. {
  217. assembly.Write(assemblyStream);
  218. Assembly.Load(assemblyStream.ToArray());
  219. }
  220. }
  221. }
  222. internal static class PatchedAssemblyResolver
  223. {
  224. public static HarmonyInstance HarmonyInstance { get; } = HarmonyInstance.Create("com.bepis.bepinex.asmlocationfix");
  225. public static Dictionary<string, string> AssemblyLocations { get; } = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
  226. public static void ApplyPatch()
  227. {
  228. HarmonyWrapper.PatchAll(typeof(PatchedAssemblyResolver), HarmonyInstance);
  229. }
  230. [HarmonyPostfix, HarmonyPatch(typeof(Assembly), nameof(Assembly.Location), MethodType.Getter)]
  231. public static void GetLocation(ref string __result, Assembly __instance)
  232. {
  233. if (AssemblyLocations.TryGetValue(__instance.FullName, out string location))
  234. __result = location;
  235. }
  236. [HarmonyPostfix, HarmonyPatch(typeof(Assembly), nameof(Assembly.CodeBase), MethodType.Getter)]
  237. public static void GetCodeBase(ref string __result, Assembly __instance)
  238. {
  239. if (AssemblyLocations.TryGetValue(__instance.FullName, out string location))
  240. __result = $"file://{location.Replace('\\', '/')}";
  241. }
  242. }
  243. }