using System; using System.IO; using System.Linq; using System.Reflection; using System.Security.Cryptography; using Mono.Cecil; namespace StardewModdingAPI.Framework { /// Preprocesses and loads mod assemblies. internal class ModAssemblyLoader { /********* ** Properties *********/ /// The directory in which to cache data. private readonly string CacheDirPath; /// Encapsulates monitoring and logging for a given module. private readonly IMonitor Monitor; /********* ** Public methods *********/ /// Construct an instance. /// The cache directory. /// Encapsulates monitoring and logging for a given module. public ModAssemblyLoader(string cacheDirPath, IMonitor monitor) { this.CacheDirPath = cacheDirPath; this.Monitor = monitor; } /// Preprocess an assembly and cache the modified version. /// The assembly file path. public void ProcessAssembly(string assemblyPath) { // read assembly data 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($"Preprocessing new assembly {assemblyPath}..."); // read assembly definition AssemblyDefinition definition; using (Stream readStream = new MemoryStream(assemblyBytes)) definition = AssemblyDefinition.ReadAssembly(readStream); // write cache using (MemoryStream outStream = new MemoryStream()) { definition.Write(outStream); byte[] outBytes = outStream.ToArray(); Directory.CreateDirectory(cachePaths.Directory); File.WriteAllBytes(cachePaths.Assembly, outBytes); File.WriteAllBytes(cachePaths.Hash, hash); } } } /// 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); } /********* ** 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); } /********* ** Private objects *********/ /// Contains the paths for an assembly's cached data. private struct CachePaths { /********* ** Accessors *********/ /// The directory path which contains the assembly. public string Directory { get; } /// The file path of the assembly file. public string Assembly { get; } /// The file path containing the MD5 hash for the assembly. public string Hash { get; } /********* ** Public methods *********/ /// Construct an instance. /// The directory path which contains the assembly. /// The file path of the assembly file. /// The file path containing the MD5 hash for the assembly. public CachePaths(string directory, string assembly, string hash) { this.Directory = directory; this.Assembly = assembly; this.Hash = hash; } } } }