// dnlib: See LICENSE.txt for more info using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml; using dnlib.Threading; namespace dnlib.DotNet { /// /// Resolves assemblies /// public class AssemblyResolver : IAssemblyResolver { static readonly ModuleDef nullModule = new ModuleDefUser(); // DLL files are searched before EXE files static readonly string[] assemblyExtensions = new string[] { ".dll", ".exe" }; static readonly string[] winMDAssemblyExtensions = new string[] { ".winmd" }; static readonly List gacInfos; static readonly string[] extraMonoPaths; static readonly string[] monoVerDirs = new string[] { // The "-api" dirs are reference assembly dirs. "4.5", @"4.5\Facades", "4.5-api", @"4.5-api\Facades", "4.0", "4.0-api", "3.5", "3.5-api", "3.0", "3.0-api", "2.0", "2.0-api", "1.1", "1.0", }; ModuleContext defaultModuleContext; readonly Dictionary> moduleSearchPaths = new Dictionary>(); readonly Dictionary cachedAssemblies = new Dictionary(StringComparer.OrdinalIgnoreCase); readonly List preSearchPaths = new List(); readonly List postSearchPaths = new List(); bool findExactMatch; bool enableFrameworkRedirect; bool enableTypeDefCache = true; bool useGac = true; #if THREAD_SAFE readonly Lock theLock = Lock.Create(); #endif sealed class GacInfo { public readonly int Version; public readonly string Path; public readonly string Prefix; public readonly string[] SubDirs; public GacInfo(int version, string prefix, string path, string[] subDirs) { Version = version; Prefix = prefix; Path = path; SubDirs = subDirs; } } static AssemblyResolver() { gacInfos = new List(); if (Type.GetType("Mono.Runtime") is not null) { var dirs = new Dictionary(StringComparer.OrdinalIgnoreCase); var extraMonoPathsList = new List(); foreach (var prefix in FindMonoPrefixes()) { var dir = Path.Combine(Path.Combine(Path.Combine(prefix, "lib"), "mono"), "gac"); if (dirs.ContainsKey(dir)) continue; dirs[dir] = true; if (Directory.Exists(dir)) { gacInfos.Add(new GacInfo(-1, "", Path.GetDirectoryName(dir), new string[] { Path.GetFileName(dir) })); } dir = Path.GetDirectoryName(dir); foreach (var verDir in monoVerDirs) { var dir2 = dir; foreach (var d in verDir.Split(new char[] { '\\' })) dir2 = Path.Combine(dir2, d); if (Directory.Exists(dir2)) extraMonoPathsList.Add(dir2); } } var paths = Environment.GetEnvironmentVariable("MONO_PATH"); if (paths is not null) { foreach (var tmp in paths.Split(Path.PathSeparator)) { var path = tmp.Trim(); if (path != string.Empty && Directory.Exists(path)) extraMonoPathsList.Add(path); } } extraMonoPaths = extraMonoPathsList.ToArray(); } else { var windir = Environment.GetEnvironmentVariable("WINDIR"); if (!string.IsNullOrEmpty(windir)) { string path; // .NET Framework 1.x and 2.x path = Path.Combine(windir, "assembly"); if (Directory.Exists(path)) { gacInfos.Add(new GacInfo(2, "", path, new string[] { "GAC_32", "GAC_64", "GAC_MSIL", "GAC" })); } // .NET Framework 4.x path = Path.Combine(Path.Combine(windir, "Microsoft.NET"), "assembly"); if (Directory.Exists(path)) { gacInfos.Add(new GacInfo(4, "v4.0_", path, new string[] { "GAC_32", "GAC_64", "GAC_MSIL" })); } } } } static string GetCurrentMonoPrefix() { var path = typeof(object).Module.FullyQualifiedName; for (int i = 0; i < 4; i++) path = Path.GetDirectoryName(path); return path; } static IEnumerable FindMonoPrefixes() { yield return GetCurrentMonoPrefix(); var prefixes = Environment.GetEnvironmentVariable("MONO_GAC_PREFIX"); if (!string.IsNullOrEmpty(prefixes)) { foreach (var tmp in prefixes.Split(Path.PathSeparator)) { var prefix = tmp.Trim(); if (prefix != string.Empty) yield return prefix; } } } /// /// Gets/sets the default /// public ModuleContext DefaultModuleContext { get => defaultModuleContext; set => defaultModuleContext = value; } /// /// true if should find an assembly that matches exactly. /// false if it first tries to match exactly, and if that fails, it picks an /// assembly that is closest to the requested assembly. /// public bool FindExactMatch { get => findExactMatch; set => findExactMatch = value; } /// /// true if resolved .NET framework assemblies can be redirected to the source /// module's framework assembly version. Eg. if a resolved .NET Framework 3.5 assembly can be /// redirected to a .NET Framework 4.0 assembly if the source module is a .NET Framework 4.0 assembly. This is /// ignored if is true. /// public bool EnableFrameworkRedirect { get => enableFrameworkRedirect; set => enableFrameworkRedirect = value; } /// /// If true, all modules in newly resolved assemblies will have their /// property set to true. This is /// enabled by default since these modules shouldn't be modified by the user. /// public bool EnableTypeDefCache { get => enableTypeDefCache; set => enableTypeDefCache = value; } /// /// true to search the Global Assembly Cache. Default value is true. /// public bool UseGAC { get => useGac; set => useGac = value; } /// /// Gets paths searched before trying the standard locations /// public IList PreSearchPaths => preSearchPaths; /// /// Gets paths searched after trying the standard locations /// public IList PostSearchPaths => postSearchPaths; /// /// Default constructor /// public AssemblyResolver() : this(null) { } /// /// Constructor /// /// Module context for all resolved assemblies public AssemblyResolver(ModuleContext defaultModuleContext) { this.defaultModuleContext = defaultModuleContext; enableFrameworkRedirect = true; } /// public AssemblyDef Resolve(IAssembly assembly, ModuleDef sourceModule) { if (assembly is null) return null; if (EnableFrameworkRedirect && !FindExactMatch) FrameworkRedirect.ApplyFrameworkRedirect(ref assembly, sourceModule); #if THREAD_SAFE theLock.EnterWriteLock(); try { #endif var resolvedAssembly = Resolve2(assembly, sourceModule); if (resolvedAssembly is null) { string asmName = UTF8String.ToSystemStringOrEmpty(assembly.Name); string asmNameTrimmed = asmName.Trim(); if (asmName != asmNameTrimmed) { assembly = new AssemblyNameInfo { Name = asmNameTrimmed, Version = assembly.Version, PublicKeyOrToken = assembly.PublicKeyOrToken, Culture = assembly.Culture, }; resolvedAssembly = Resolve2(assembly, sourceModule); } } if (resolvedAssembly is null) { // Make sure we don't search for this assembly again. This speeds up callers who // keep asking for this assembly when trying to resolve many different TypeRefs cachedAssemblies[GetAssemblyNameKey(assembly)] = null; return null; } var key1 = GetAssemblyNameKey(resolvedAssembly); var key2 = GetAssemblyNameKey(assembly); cachedAssemblies.TryGetValue(key1, out var asm1); cachedAssemblies.TryGetValue(key2, out var asm2); if (asm1 != resolvedAssembly && asm2 != resolvedAssembly) { // This assembly was just resolved if (enableTypeDefCache) { var modules = resolvedAssembly.Modules; int count = modules.Count; for (int i = 0; i < count; i++) { var module = modules[i]; if (module is not null) module.EnableTypeDefFindCache = true; } } } bool inserted = false; if (!cachedAssemblies.ContainsKey(key1)) { cachedAssemblies.Add(key1, resolvedAssembly); inserted = true; } if (!cachedAssemblies.ContainsKey(key2)) { cachedAssemblies.Add(key2, resolvedAssembly); inserted = true; } if (inserted || asm1 == resolvedAssembly || asm2 == resolvedAssembly) return resolvedAssembly; // Dupe assembly. Don't insert it. var dupeModule = resolvedAssembly.ManifestModule; if (dupeModule is not null) dupeModule.Dispose(); return asm1 ?? asm2; #if THREAD_SAFE } finally { theLock.ExitWriteLock(); } #endif } /// /// Add a module's assembly to the assembly cache /// /// The module whose assembly should be cached /// true if 's assembly is cached, false /// if it's not cached because some other assembly with the exact same full name has /// already been cached or if or its assembly is null. public bool AddToCache(ModuleDef module) => module is not null && AddToCache(module.Assembly); /// /// Add an assembly to the assembly cache /// /// The assembly /// true if is cached, false if it's not /// cached because some other assembly with the exact same full name has already been /// cached or if is null. public bool AddToCache(AssemblyDef asm) { if (asm is null) return false; var asmKey = GetAssemblyNameKey(asm); #if THREAD_SAFE theLock.EnterWriteLock(); try { #endif if (cachedAssemblies.TryGetValue(asmKey, out var cachedAsm) && cachedAsm is not null) return asm == cachedAsm; if (enableTypeDefCache) { var modules = asm.Modules; int count = modules.Count; for (int i = 0; i < count; i++) { var module = modules[i]; if (module is not null) module.EnableTypeDefFindCache = true; } } cachedAssemblies[asmKey] = asm; return true; #if THREAD_SAFE } finally { theLock.ExitWriteLock(); } #endif } /// /// Removes a module's assembly from the cache /// /// The module /// true if its assembly was removed, false if it wasn't removed /// since it wasn't in the cache, it has no assembly, or was /// null public bool Remove(ModuleDef module) => module is not null && Remove(module.Assembly); /// /// Removes the assembly from the cache /// /// The assembly /// true if it was removed, false if it wasn't removed since it /// wasn't in the cache or if was null public bool Remove(AssemblyDef asm) { if (asm is null) return false; var asmKey = GetAssemblyNameKey(asm); #if THREAD_SAFE theLock.EnterWriteLock(); try { #endif return cachedAssemblies.Remove(asmKey); #if THREAD_SAFE } finally { theLock.ExitWriteLock(); } #endif } /// /// Clears the cache and calls on each cached module. /// Use to remove any assemblies you added yourself /// using before calling this method if you don't want /// them disposed. /// public void Clear() { List asms; #if THREAD_SAFE theLock.EnterWriteLock(); try { #endif asms = new List(cachedAssemblies.Values); cachedAssemblies.Clear(); #if THREAD_SAFE } finally { theLock.ExitWriteLock(); } #endif foreach (var asm in asms) { if (asm is null) continue; foreach (var mod in asm.Modules) mod.Dispose(); } } /// /// Gets the cached assemblies in this resolver. /// /// The cached assemblies. public IEnumerable GetCachedAssemblies() { AssemblyDef[] assemblies; #if THREAD_SAFE theLock.EnterReadLock(); try { #endif assemblies = cachedAssemblies.Values.ToArray(); #if THREAD_SAFE } finally { theLock.ExitReadLock(); } #endif return assemblies; } static string GetAssemblyNameKey(IAssembly asmName) { // Make sure the name contains PublicKeyToken= and not PublicKey= return asmName.FullNameToken; } AssemblyDef Resolve2(IAssembly assembly, ModuleDef sourceModule) { if (cachedAssemblies.TryGetValue(GetAssemblyNameKey(assembly), out var resolvedAssembly)) return resolvedAssembly; var moduleContext = defaultModuleContext; if (moduleContext is null && sourceModule is not null) moduleContext = sourceModule.Context; resolvedAssembly = FindExactAssembly(assembly, PreFindAssemblies(assembly, sourceModule, true), moduleContext) ?? FindExactAssembly(assembly, FindAssemblies(assembly, sourceModule, true), moduleContext) ?? FindExactAssembly(assembly, PostFindAssemblies(assembly, sourceModule, true), moduleContext); if (resolvedAssembly is not null) return resolvedAssembly; if (!findExactMatch) { resolvedAssembly = FindClosestAssembly(assembly); resolvedAssembly = FindClosestAssembly(assembly, resolvedAssembly, PreFindAssemblies(assembly, sourceModule, false), moduleContext); resolvedAssembly = FindClosestAssembly(assembly, resolvedAssembly, FindAssemblies(assembly, sourceModule, false), moduleContext); resolvedAssembly = FindClosestAssembly(assembly, resolvedAssembly, PostFindAssemblies(assembly, sourceModule, false), moduleContext); } return resolvedAssembly; } /// /// Finds an assembly that exactly matches the requested assembly /// /// Assembly to find /// Search paths or null if none /// Module context /// An instance or null if an exact match /// couldn't be found. AssemblyDef FindExactAssembly(IAssembly assembly, IEnumerable paths, ModuleContext moduleContext) { if (paths is null) return null; var asmComparer = AssemblyNameComparer.CompareAll; foreach (var path in paths) { ModuleDefMD mod = null; try { mod = ModuleDefMD.Load(path, moduleContext); var asm = mod.Assembly; if (asm is not null && asmComparer.Equals(assembly, asm)) { mod = null; return asm; } } catch { } finally { if (mod is not null) mod.Dispose(); } } return null; } /// /// Finds the closest assembly from the already cached assemblies /// /// Assembly to find /// The closest or null if none found AssemblyDef FindClosestAssembly(IAssembly assembly) { AssemblyDef closest = null; var asmComparer = AssemblyNameComparer.CompareAll; foreach (var kv in cachedAssemblies) { var asm = kv.Value; if (asm is null) continue; if (asmComparer.CompareClosest(assembly, closest, asm) == 1) closest = asm; } return closest; } AssemblyDef FindClosestAssembly(IAssembly assembly, AssemblyDef closest, IEnumerable paths, ModuleContext moduleContext) { if (paths is null) return closest; var asmComparer = AssemblyNameComparer.CompareAll; foreach (var path in paths) { ModuleDefMD mod = null; try { mod = ModuleDefMD.Load(path, moduleContext); var asm = mod.Assembly; if (asm is not null && asmComparer.CompareClosest(assembly, closest, asm) == 1) { if (!IsCached(closest) && closest is not null) { var closeMod = closest.ManifestModule; if (closeMod is not null) closeMod.Dispose(); } closest = asm; mod = null; } } catch { } finally { if (mod is not null) mod.Dispose(); } } return closest; } /// /// Returns true if is inserted in /// /// Assembly to check bool IsCached(AssemblyDef asm) { if (asm is null) return false; return cachedAssemblies.TryGetValue(GetAssemblyNameKey(asm), out var cachedAsm) && cachedAsm == asm; } IEnumerable FindAssemblies2(IAssembly assembly, IEnumerable paths) { if (paths is not null) { var asmSimpleName = UTF8String.ToSystemStringOrEmpty(assembly.Name); var exts = assembly.IsContentTypeWindowsRuntime ? winMDAssemblyExtensions : assemblyExtensions; foreach (var ext in exts) { foreach (var path in paths) { string fullPath; try { fullPath = Path.Combine(path, asmSimpleName + ext); } catch (ArgumentException) { // Invalid path chars yield break; } if (File.Exists(fullPath)) yield return fullPath; } } } } /// /// Called before /// /// Assembly to find /// The module that needs to resolve an assembly or null /// We're trying to find an exact match /// null or an enumerable of full paths to try protected virtual IEnumerable PreFindAssemblies(IAssembly assembly, ModuleDef sourceModule, bool matchExactly) { foreach (var path in FindAssemblies2(assembly, preSearchPaths)) yield return path; } /// /// Called after (if it fails) /// /// Assembly to find /// The module that needs to resolve an assembly or null /// We're trying to find an exact match /// null or an enumerable of full paths to try protected virtual IEnumerable PostFindAssemblies(IAssembly assembly, ModuleDef sourceModule, bool matchExactly) { foreach (var path in FindAssemblies2(assembly, postSearchPaths)) yield return path; } /// /// Called after (if it fails) /// /// Assembly to find /// The module that needs to resolve an assembly or null /// We're trying to find an exact match /// null or an enumerable of full paths to try protected virtual IEnumerable FindAssemblies(IAssembly assembly, ModuleDef sourceModule, bool matchExactly) { if (assembly.IsContentTypeWindowsRuntime) { string path; try { path = Path.Combine(Path.Combine(Environment.SystemDirectory, "WinMetadata"), assembly.Name + ".winmd"); } catch (ArgumentException) { // Invalid path chars path = null; } if (File.Exists(path)) yield return path; } else { if (UseGAC) { foreach (var path in FindAssembliesGac(assembly, sourceModule, matchExactly)) yield return path; } } foreach (var path in FindAssembliesModuleSearchPaths(assembly, sourceModule, matchExactly)) yield return path; } IEnumerable FindAssembliesGac(IAssembly assembly, ModuleDef sourceModule, bool matchExactly) { if (matchExactly) return FindAssembliesGacExactly(assembly, sourceModule); return FindAssembliesGacAny(assembly, sourceModule); } IEnumerable GetGacInfos(ModuleDef sourceModule) { int version = sourceModule is null ? int.MinValue : sourceModule.IsClr40 ? 4 : 2; // Try the correct GAC first (eg. GAC4 if it's a .NET Framework 4 assembly) foreach (var gacInfo in gacInfos) { if (gacInfo.Version == version) yield return gacInfo; } foreach (var gacInfo in gacInfos) { if (gacInfo.Version != version) yield return gacInfo; } } IEnumerable FindAssembliesGacExactly(IAssembly assembly, ModuleDef sourceModule) { foreach (var gacInfo in GetGacInfos(sourceModule)) { foreach (var path in FindAssembliesGacExactly(gacInfo, assembly, sourceModule)) yield return path; } if (extraMonoPaths is not null) { foreach (var path in GetExtraMonoPaths(assembly, sourceModule)) yield return path; } } static IEnumerable GetExtraMonoPaths(IAssembly assembly, ModuleDef sourceModule) { if (extraMonoPaths is not null) { foreach (var dir in extraMonoPaths) { string file; try { file = Path.Combine(dir, assembly.Name + ".dll"); } catch (ArgumentException) { // Invalid path chars break; } if (File.Exists(file)) yield return file; } } } IEnumerable FindAssembliesGacExactly(GacInfo gacInfo, IAssembly assembly, ModuleDef sourceModule) { var pkt = PublicKeyBase.ToPublicKeyToken(assembly.PublicKeyOrToken); if (gacInfo is not null && pkt is not null) { string pktString = pkt.ToString(); string verString = Utils.CreateVersionWithNoUndefinedValues(assembly.Version).ToString(); var cultureString = UTF8String.ToSystemStringOrEmpty(assembly.Culture); if (cultureString.Equals("neutral", StringComparison.OrdinalIgnoreCase)) cultureString = string.Empty; var asmSimpleName = UTF8String.ToSystemStringOrEmpty(assembly.Name); foreach (var subDir in gacInfo.SubDirs) { var baseDir = Path.Combine(gacInfo.Path, subDir); try { baseDir = Path.Combine(baseDir, asmSimpleName); } catch (ArgumentException) { // Invalid path chars break; } baseDir = Path.Combine(baseDir, $"{gacInfo.Prefix}{verString}_{cultureString}_{pktString}"); var pathName = Path.Combine(baseDir, asmSimpleName + ".dll"); if (File.Exists(pathName)) yield return pathName; } } } IEnumerable FindAssembliesGacAny(IAssembly assembly, ModuleDef sourceModule) { foreach (var gacInfo in GetGacInfos(sourceModule)) { foreach (var path in FindAssembliesGacAny(gacInfo, assembly, sourceModule)) yield return path; } if (extraMonoPaths is not null) { foreach (var path in GetExtraMonoPaths(assembly, sourceModule)) yield return path; } } IEnumerable FindAssembliesGacAny(GacInfo gacInfo, IAssembly assembly, ModuleDef sourceModule) { if (gacInfo is not null) { var asmSimpleName = UTF8String.ToSystemStringOrEmpty(assembly.Name); foreach (var subDir in gacInfo.SubDirs) { var baseDir = Path.Combine(gacInfo.Path, subDir); try { baseDir = Path.Combine(baseDir, asmSimpleName); } catch (ArgumentException) { // Invalid path chars break; } foreach (var dir in GetDirs(baseDir)) { var pathName = Path.Combine(dir, asmSimpleName + ".dll"); if (File.Exists(pathName)) yield return pathName; } } } } IEnumerable GetDirs(string baseDir) { if (!Directory.Exists(baseDir)) return Array2.Empty(); var dirs = new List(); try { foreach (var di in new DirectoryInfo(baseDir).GetDirectories()) dirs.Add(di.FullName); } catch { } return dirs; } IEnumerable FindAssembliesModuleSearchPaths(IAssembly assembly, ModuleDef sourceModule, bool matchExactly) { string asmSimpleName = UTF8String.ToSystemStringOrEmpty(assembly.Name); var searchPaths = GetSearchPaths(sourceModule); var exts = assembly.IsContentTypeWindowsRuntime ? winMDAssemblyExtensions : assemblyExtensions; foreach (var ext in exts) { foreach (var path in searchPaths) { for (int i = 0; i < 2; i++) { string path2; try { if (i == 0) path2 = Path.Combine(path, asmSimpleName + ext); else path2 = Path.Combine(Path.Combine(path, asmSimpleName), asmSimpleName + ext); } catch (ArgumentException) { // Invalid path chars yield break; } if (File.Exists(path2)) yield return path2; } } } } /// /// Gets all search paths to use for this module /// /// The module or null if unknown /// A list of all search paths to use for this module IEnumerable GetSearchPaths(ModuleDef module) { var keyModule = module; if (keyModule is null) keyModule = nullModule; if (moduleSearchPaths.TryGetValue(keyModule, out var searchPaths)) return searchPaths; moduleSearchPaths[keyModule] = searchPaths = new List(GetModuleSearchPaths(module)); return searchPaths; } /// /// Gets all module search paths. This is usually empty unless its assembly has /// a .config file specifying any additional private search paths in a /// <probing/> element. /// /// The module or null if unknown /// A list of search paths protected virtual IEnumerable GetModuleSearchPaths(ModuleDef module) => GetModulePrivateSearchPaths(module); /// /// Gets all private assembly search paths as found in the module's .config file. /// /// The module or null if unknown /// A list of search paths protected IEnumerable GetModulePrivateSearchPaths(ModuleDef module) { if (module is null) return Array2.Empty(); var asm = module.Assembly; if (asm is null) return Array2.Empty(); module = asm.ManifestModule; if (module is null) return Array2.Empty(); // Should never happen string baseDir = null; try { var imageName = module.Location; if (imageName != string.Empty) { var directoryInfo = Directory.GetParent(imageName); if (directoryInfo is not null) { baseDir = directoryInfo.FullName; var configName = imageName + ".config"; if (File.Exists(configName)) return GetPrivatePaths(baseDir, configName); } } } catch { } if (baseDir is not null) return new List { baseDir }; return Array2.Empty(); } IEnumerable GetPrivatePaths(string baseDir, string configFileName) { var searchPaths = new List(); try { var dirName = Path.GetDirectoryName(Path.GetFullPath(configFileName)); searchPaths.Add(dirName); using (var xmlStream = new FileStream(configFileName, FileMode.Open, FileAccess.Read, FileShare.Read)) { var doc = new XmlDocument(); doc.Load(XmlReader.Create(xmlStream)); foreach (var tmp in doc.GetElementsByTagName("probing")) { var probingElem = tmp as XmlElement; if (probingElem is null) continue; var privatePath = probingElem.GetAttribute("privatePath"); if (string.IsNullOrEmpty(privatePath)) continue; foreach (var tmp2 in privatePath.Split(';')) { var path = tmp2.Trim(); if (path == "") continue; var newPath = Path.GetFullPath(Path.Combine(dirName, path.Replace('\\', Path.DirectorySeparatorChar))); if (Directory.Exists(newPath) && newPath.StartsWith(baseDir + Path.DirectorySeparatorChar)) searchPaths.Add(newPath); } } } } catch (ArgumentException) { } catch (IOException) { } catch (XmlException) { } return searchPaths; } } }