using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Security.Cryptography; using Mono.Cecil; using Newtonsoft.Json; using StardewModdingAPI.AssemblyRewriters; using StardewModdingAPI.Framework.AssemblyRewriting; namespace StardewModdingAPI.Framework { /// Preprocesses and loads mod assemblies. internal class ModAssemblyLoader { /********* ** Properties *********/ /// The name of the directory containing a mod's cached data. private readonly string CacheDirName; /// 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 name of the directory containing a mod's cached data. /// The current game platform. /// Encapsulates monitoring and logging. public ModAssemblyLoader(string cacheDirName, Platform targetPlatform, IMonitor monitor) { this.CacheDirName = cacheDirName; this.Monitor = monitor; this.AssemblyMap = Constants.GetAssemblyMap(targetPlatform); this.AssemblyTypeRewriter = new AssemblyTypeRewriter(this.AssemblyMap, monitor); } /// Preprocess an assembly unless the cache is up to date. /// The assembly file path. /// Returns the rewrite metadata for the preprocessed assembly. public RewriteResult ProcessAssemblyUnlessCached(string assemblyPath) { // read assembly data byte[] assemblyBytes = File.ReadAllBytes(assemblyPath); string hash = string.Join("", MD5.Create().ComputeHash(assemblyBytes).Select(p => p.ToString("X2"))); // get cached result if current CachePaths cachePaths = this.GetCachePaths(assemblyPath); { CacheEntry cacheEntry = File.Exists(cachePaths.Metadata) ? JsonConvert.DeserializeObject(File.ReadAllText(cachePaths.Metadata)) : null; if (cacheEntry != null && cacheEntry.IsUpToDate(cachePaths, hash, Constants.ApiVersion)) return new RewriteResult(assemblyPath, cachePaths, assemblyBytes, cacheEntry.Hash, cacheEntry.UseCachedAssembly, isNewerThanCache: false); // no rewrite needed } this.Monitor.Log($"Preprocessing {Path.GetFileName(assemblyPath)} for compatibility...", LogLevel.Trace); // rewrite assembly AssemblyDefinition assembly; using (Stream readStream = new MemoryStream(assemblyBytes)) assembly = AssemblyDefinition.ReadAssembly(readStream); bool modified = this.AssemblyTypeRewriter.RewriteAssembly(assembly); using (MemoryStream outStream = new MemoryStream()) { assembly.Write(outStream); byte[] outBytes = outStream.ToArray(); return new RewriteResult(assemblyPath, cachePaths, outBytes, hash, useCachedAssembly: modified, isNewerThanCache: true); } } /// Write rewritten assembly metadata to the cache for a mod. /// The rewrite results. /// Whether to write all assemblies to the cache, even if they weren't modified. /// There are no results to write, or the results are not all for the same directory. public void WriteCache(IEnumerable results, bool forceCacheAssemblies) { results = results.ToArray(); // get cache directory if (!results.Any()) throw new InvalidOperationException("There are no assemblies to cache."); if (results.Select(p => p.CachePaths.Directory).Distinct().Count() > 1) throw new InvalidOperationException("The assemblies can't be cached together because they have different source directories."); string cacheDir = results.Select(p => p.CachePaths.Directory).First(); // reset cache if (Directory.Exists(cacheDir)) Directory.Delete(cacheDir, recursive: true); Directory.CreateDirectory(cacheDir); // cache all results foreach (RewriteResult result in results) { CacheEntry cacheEntry = new CacheEntry(result.Hash, Constants.ApiVersion.ToString(), forceCacheAssemblies || result.UseCachedAssembly); File.WriteAllText(result.CachePaths.Metadata, JsonConvert.SerializeObject(cacheEntry)); if (forceCacheAssemblies || result.UseCachedAssembly) File.WriteAllBytes(result.CachePaths.Assembly, result.AssemblyBytes); } } /// Resolve an assembly from its name. /// The assembly name. /// /// This implementation returns the first loaded assembly which matches the short form of /// the assembly name, to resolve assembly resolution issues when rewriting /// assemblies (especially with Mono). Since this is meant to be called on , /// the implicit assumption is that loading the exact assembly failed. /// public Assembly ResolveAssembly(string name) { string shortName = name.Split(new[] { ',' }, 2).First(); // get simple name (without version and culture) return AppDomain.CurrentDomain .GetAssemblies() .FirstOrDefault(p => p.GetName().Name == shortName); } /********* ** Private methods *********/ /// Get the cache details for an assembly. /// The assembly file path. private CachePaths GetCachePaths(string assemblyPath) { string fileName = Path.GetFileName(assemblyPath); string dirPath = Path.Combine(Path.GetDirectoryName(assemblyPath), this.CacheDirName); string cacheAssemblyPath = Path.Combine(dirPath, fileName); string metadataPath = Path.Combine(dirPath, $"{fileName}.json"); return new CachePaths(dirPath, cacheAssemblyPath, metadataPath); } } }