using System; using System.IO; using System.Linq; using System.Reflection; using System.Security.Cryptography; using Mono.Cecil; using StardewModdingAPI.Framework.AssemblyRewriting; namespace StardewModdingAPI.Framework { /// Preprocesses and loads mod assemblies. internal class ModAssemblyLoader { /********* ** Properties *********/ /// The directory in which to cache data. private readonly string CacheDirPath; /// Metadata for mapping assemblies to the current . private readonly PlatformAssemblyMap AssemblyMap; /// Rewrites assembly types to match the current platform. private readonly AssemblyTypeRewriter AssemblyTypeRewriter; /// Encapsulates monitoring and logging. private readonly IMonitor Monitor; /********* ** Public methods *********/ /// Construct an instance. /// The cache directory. /// The current game platform. /// Encapsulates monitoring and logging. public ModAssemblyLoader(string cacheDirPath, Platform targetPlatform, IMonitor monitor) { this.CacheDirPath = cacheDirPath; this.Monitor = monitor; this.AssemblyMap = Constants.GetAssemblyMap(targetPlatform); this.AssemblyTypeRewriter = new AssemblyTypeRewriter(this.AssemblyMap, monitor); } /// Preprocess an assembly and cache the modified version. /// The assembly file path. public void ProcessAssembly(string assemblyPath) { // read assembly data string assemblyFileName = Path.GetFileName(assemblyPath); string assemblyDir = Path.GetDirectoryName(assemblyPath); byte[] assemblyBytes = File.ReadAllBytes(assemblyPath); byte[] hash = MD5.Create().ComputeHash(assemblyBytes); // check cache CachePaths cachePaths = this.GetCacheInfo(assemblyPath); bool canUseCache = File.Exists(cachePaths.Assembly) && File.Exists(cachePaths.Hash) && hash.SequenceEqual(File.ReadAllBytes(cachePaths.Hash)); // process assembly if not cached if (!canUseCache) { this.Monitor.Log($"Loading {assemblyFileName} for the first time; preprocessing..."); // read assembly definition AssemblyDefinition assembly; using (Stream readStream = new MemoryStream(assemblyBytes)) assembly = AssemblyDefinition.ReadAssembly(readStream); // rewrite assembly to match platform this.AssemblyTypeRewriter.RewriteAssembly(assembly); // write cache using (MemoryStream outStream = new MemoryStream()) { // get assembly bytes assembly.Write(outStream); byte[] outBytes = outStream.ToArray(); // write assembly data Directory.CreateDirectory(cachePaths.Directory); File.WriteAllBytes(cachePaths.Assembly, outBytes); File.WriteAllBytes(cachePaths.Hash, hash); // copy any mdb/pdb files foreach (string path in Directory.GetFiles(assemblyDir, "*.mdb").Concat(Directory.GetFiles(assemblyDir, "*.pdb"))) { string filename = Path.GetFileName(path); File.Copy(path, Path.Combine(cachePaths.Directory, filename), overwrite: true); } } } } /// Load a preprocessed assembly. /// The assembly file path. public Assembly LoadCachedAssembly(string assemblyPath) { CachePaths cachePaths = this.GetCacheInfo(assemblyPath); if (!File.Exists(cachePaths.Assembly)) throw new InvalidOperationException($"The assembly {assemblyPath} doesn't exist in the preprocessed cache."); return Assembly.UnsafeLoadFrom(cachePaths.Assembly); // unsafe load allows DLLs downloaded from the Internet without the user needing to 'unblock' them } /// Resolve an assembly from its name. /// The assembly name. public Assembly ResolveAssembly(string name) { string shortName = name.Split(new[] { ',' }, 2).First(); return this.AssemblyMap.Targets.FirstOrDefault(p => p.GetName().Name == shortName); } /********* ** Private methods *********/ /// Get the cache details for an assembly. /// The assembly file path. private CachePaths GetCacheInfo(string assemblyPath) { string key = Path.GetFileNameWithoutExtension(assemblyPath); string dirPath = Path.Combine(this.CacheDirPath, new DirectoryInfo(Path.GetDirectoryName(assemblyPath)).Name); string cacheAssemblyPath = Path.Combine(dirPath, $"{key}.dll"); string cacheHashPath = Path.Combine(dirPath, $"{key}.hash"); return new CachePaths(dirPath, cacheAssemblyPath, cacheHashPath); } } }