AssemblyPatcherLoader.cs 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  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. namespace BepInEx.Bootstrap
  10. {
  11. /// <summary>
  12. /// Delegate used in patching assemblies.
  13. /// </summary>
  14. /// <param name="assembly">The assembly that is being patched.</param>
  15. public delegate void AssemblyPatcherDelegate(ref AssemblyDefinition assembly);
  16. public class AssemblyPatcher
  17. {
  18. public IEnumerable<string> TargetDLLs { get; set; } = null;
  19. public Action Initializer { get; set; } = null;
  20. public Action Finalizer { get; set; } = null;
  21. public AssemblyPatcherDelegate Patcher { get; set; } = null;
  22. public string Name { get; set; } = string.Empty;
  23. }
  24. /// <summary>
  25. /// Worker class which is used for loading and patching entire folders of assemblies, or alternatively patching and loading assemblies one at a time.
  26. /// </summary>
  27. public static class AssemblyPatcherLoader
  28. {
  29. private static List<AssemblyPatcher> patchers = new List<AssemblyPatcher>();
  30. /// <summary>
  31. /// Configuration value of whether assembly dumping is enabled or not.
  32. /// </summary>
  33. private static bool DumpingEnabled => Utility.SafeParseBool(Config.GetEntry("dump-assemblies", "false", "Preloader"));
  34. public static void AddPatcher(AssemblyPatcher patcher)
  35. {
  36. patchers.Add(patcher);
  37. }
  38. public static void AddPatchersFromDirectory(string directory, Func<Assembly, List<AssemblyPatcher>> patcherLocator)
  39. {
  40. if (!Directory.Exists(directory))
  41. return;
  42. var sortedPatchers = new SortedDictionary<string, AssemblyPatcher>();
  43. foreach (string assemblyPath in Directory.GetFiles(directory, "*.dll"))
  44. try
  45. {
  46. var assembly = Assembly.LoadFrom(assemblyPath);
  47. foreach (var patcher in patcherLocator(assembly))
  48. sortedPatchers.Add(patcher.Name, patcher);
  49. }
  50. catch (BadImageFormatException) { } //unmanaged DLL
  51. catch (ReflectionTypeLoadException) { } //invalid references
  52. foreach (var patcher in sortedPatchers)
  53. AddPatcher(patcher.Value);
  54. }
  55. private static void InitializePatchers()
  56. {
  57. foreach (var assemblyPatcher in patchers)
  58. assemblyPatcher.Initializer?.Invoke();
  59. }
  60. private static void FinalizePatching()
  61. {
  62. foreach (var assemblyPatcher in patchers)
  63. assemblyPatcher.Finalizer?.Invoke();
  64. }
  65. public static void DisposePatchers()
  66. {
  67. patchers.Clear();
  68. }
  69. public static void PatchAndLoad(string directory)
  70. {
  71. // First, load patchable assemblies into Cecil
  72. Dictionary<string, AssemblyDefinition> assemblies = new Dictionary<string, AssemblyDefinition>();
  73. foreach (string assemblyPath in Directory.GetFiles(directory, "*.dll"))
  74. {
  75. var assembly = AssemblyDefinition.ReadAssembly(assemblyPath);
  76. //NOTE: this is special cased here because the dependency handling for System.dll is a bit wonky
  77. //System has an assembly reference to itself, and it also has a reference to Mono.Security causing a circular dependency
  78. //It's also generally dangerous to change system.dll since so many things rely on it,
  79. // and it's already loaded into the appdomain since this loader references it, so we might as well skip it
  80. if (assembly.Name.Name == "System"
  81. || assembly.Name.Name == "mscorlib") //mscorlib is already loaded into the appdomain so it can't be patched
  82. {
  83. assembly.Dispose();
  84. continue;
  85. }
  86. if (PatchedAssemblyResolver.AssemblyLocations.ContainsKey(assembly.FullName))
  87. {
  88. Logger.Log(LogLevel.Warning, $"Tried to load duplicate assembly {Path.GetFileName(assemblyPath)} from Managed folder! Skipping...");
  89. continue;
  90. }
  91. assemblies.Add(Path.GetFileName(assemblyPath), assembly);
  92. PatchedAssemblyResolver.AssemblyLocations.Add(assembly.FullName, Path.GetFullPath(assemblyPath));
  93. }
  94. // Next, initialize all the patchers
  95. InitializePatchers();
  96. // Then, perform the actual patching
  97. HashSet<string> patchedAssemblies = new HashSet<string>();
  98. foreach (var assemblyPatcher in patchers)
  99. {
  100. foreach (string targetDll in assemblyPatcher.TargetDLLs)
  101. {
  102. if (assemblies.TryGetValue(targetDll, out var assembly))
  103. {
  104. assemblyPatcher.Patcher?.Invoke(ref assembly);
  105. assemblies[targetDll] = assembly;
  106. patchedAssemblies.Add(targetDll);
  107. }
  108. }
  109. }
  110. // Finally, load all assemblies into memory
  111. foreach (var kv in assemblies)
  112. {
  113. string filename = kv.Key;
  114. var assembly = kv.Value;
  115. if (DumpingEnabled && patchedAssemblies.Contains(filename))
  116. {
  117. using (MemoryStream mem = new MemoryStream())
  118. {
  119. string dirPath = Path.Combine(Paths.PluginPath, "DumpedAssemblies");
  120. if (!Directory.Exists(dirPath))
  121. Directory.CreateDirectory(dirPath);
  122. assembly.Write(mem);
  123. File.WriteAllBytes(Path.Combine(dirPath, filename), mem.ToArray());
  124. }
  125. }
  126. Load(assembly);
  127. assembly.Dispose();
  128. }
  129. // Apply assembly location resolver patch
  130. PatchedAssemblyResolver.ApplyPatch();
  131. //run all finalizers
  132. FinalizePatching();
  133. }
  134. /// <summary>
  135. /// Loads an individual assembly defintion into the CLR.
  136. /// </summary>
  137. /// <param name="assembly">The assembly to load.</param>
  138. public static void Load(AssemblyDefinition assembly)
  139. {
  140. using (MemoryStream assemblyStream = new MemoryStream())
  141. {
  142. assembly.Write(assemblyStream);
  143. Assembly.Load(assemblyStream.ToArray());
  144. }
  145. }
  146. }
  147. internal static class PatchedAssemblyResolver
  148. {
  149. public static HarmonyInstance HarmonyInstance { get; } = HarmonyInstance.Create("com.bepis.bepinex.asmlocationfix");
  150. public static Dictionary<string, string> AssemblyLocations { get; } = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
  151. public static void ApplyPatch()
  152. {
  153. HarmonyWrapper.PatchAll(typeof(PatchedAssemblyResolver), HarmonyInstance);
  154. }
  155. [HarmonyPostfix, HarmonyPatch(typeof(Assembly), nameof(Assembly.Location), MethodType.Getter)]
  156. public static void GetLocation(ref string __result, Assembly __instance)
  157. {
  158. if (AssemblyLocations.TryGetValue(__instance.FullName, out string location))
  159. __result = location;
  160. }
  161. [HarmonyPostfix, HarmonyPatch(typeof(Assembly), nameof(Assembly.CodeBase), MethodType.Getter)]
  162. public static void GetCodeBase(ref string __result, Assembly __instance)
  163. {
  164. if (AssemblyLocations.TryGetValue(__instance.FullName, out string location))
  165. __result = $"file://{location.Replace('\\', '/')}";
  166. }
  167. }
  168. }