Files
ILSpy/ICSharpCode.Decompiler/Metadata/UniversalAssemblyResolver.cs
T
Siegfried Pammer 2115bd9028 Read the file system once per reference closure, not per reference
Resolving one assembly resolves its whole reference closure, and every
reference in it asked the same framework directories the same questions.
The worst of it was the scan for the closest version folder of a shared
framework: a directory listing plus a recursive file search, repeated per
reference and per runtime pack - 42 scans for two distinct answers when
decompiling ICSharpCode.ILSpyX.dll.

The scan result is only safe to keep for a bounded time: a runtime can be
installed or removed while ILSpy runs, and reloading an assembly list has
to see that. So it is kept for the length of an explicitly opened scope,
which the type system opens around the closure it resolves and closes
again afterwards; outside a scope the file system is read as before. The
scope owns what was read, so two of them on one resolver do not stack -
the first to end takes it, and the other reads the file system again.

BeginSnapshot is on IAssemblyResolver rather than an interface of its
own: it is core functionality of a resolver, and one implementation is
not an abstraction. This breaks the interface for implementors outside
this repository, who opt out by returning null - which is what the three
resolvers here that hold nothing do.

The remaining probes cost nothing to fix: the preferred runtime pack was
listed among the defaults it already belongs to, so its directory was
scanned twice for every reference that is not in it, and one package
folder was probed once per assembly the package contains.

Measured over 27 references with a fresh resolver each time: 3.3 ms per
assembly before, 3.1 ms without a scope, 1.1 ms with one.

Assisted-by: Claude:claude-opus-5:Claude Code
2026-09-04 19:55:39 +02:00

940 lines
30 KiB
C#

// Copyright (c) 2018 Siegfried Pammer
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
#nullable enable
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace ICSharpCode.Decompiler.Metadata
{
public enum TargetRuntime
{
Unknown,
Net_1_0,
Net_1_1,
Net_2_0,
Net_4_0
}
public enum TargetFrameworkIdentifier
{
NETFramework,
NETCoreApp,
NETStandard,
Silverlight,
NET
}
enum DecompilerRuntime
{
NETFramework,
NETCoreApp,
Mono
}
/// <summary>
/// Used to resolve assemblies referenced by an assembly.
/// </summary>
public class UniversalAssemblyResolver : AssemblyReferenceClassifier, IAssemblyResolver
{
static UniversalAssemblyResolver()
{
// TODO : test whether this works with Mono on *Windows*, not sure if we'll
// ever need this...
if (Type.GetType("Mono.Runtime") != null)
decompilerRuntime = DecompilerRuntime.Mono;
else if (typeof(object).Assembly.GetName().Name == "System.Private.CoreLib")
decompilerRuntime = DecompilerRuntime.NETCoreApp;
else if (Environment.OSVersion.Platform == PlatformID.Unix)
decompilerRuntime = DecompilerRuntime.Mono;
else
decompilerRuntime = DecompilerRuntime.NETFramework;
}
readonly Lazy<DotNetCorePathFinder> dotNetCorePathFinder;
ConcurrentDictionary<string, string>? versionFolders;
readonly bool throwOnError;
readonly PEStreamOptions streamOptions;
readonly MetadataReaderOptions metadataOptions;
readonly string? mainAssemblyFileName;
readonly string? baseDirectory;
readonly List<string?> directories = new List<string?>();
static readonly List<string> gac_paths = GetGacPaths();
static readonly DecompilerRuntime decompilerRuntime;
/// <inheritdoc/>
public IDisposable? BeginSnapshot()
{
versionFolders = new ConcurrentDictionary<string, string>();
return new Snapshot(this);
}
/// <summary>
/// The version folder to use inside <paramref name="basePath"/>, determined by
/// <paramref name="valueFactory"/> once per open scope, and on every call outside one.
/// </summary>
internal string GetOrAddVersionFolder(string basePath, Func<string, string> valueFactory)
{
return versionFolders is { } cache ? cache.GetOrAdd(basePath, valueFactory) : valueFactory(basePath);
}
/// <summary>
/// Holds what the resolver read from the file system until it is disposed. Two of these on
/// one resolver do not stack: the first to end takes the cache with it, and the other reads
/// the file system again.
/// </summary>
sealed class Snapshot : IDisposable
{
readonly UniversalAssemblyResolver resolver;
public Snapshot(UniversalAssemblyResolver resolver)
{
this.resolver = resolver;
}
public void Dispose() => resolver.versionFolders = null;
}
public void AddSearchDirectory(string? directory)
{
directories.Add(directory);
if (dotNetCorePathFinder.IsValueCreated)
{
dotNetCorePathFinder.Value.AddSearchDirectory(directory);
}
}
public void RemoveSearchDirectory(string? directory)
{
directories.Remove(directory);
if (dotNetCorePathFinder.IsValueCreated)
{
dotNetCorePathFinder.Value.RemoveSearchDirectory(directory);
}
}
public string?[] GetSearchDirectories()
{
return directories.ToArray();
}
readonly string targetFramework;
readonly string runtimePack;
readonly TargetFrameworkIdentifier targetFrameworkIdentifier;
readonly Version targetFrameworkVersion;
/// <summary>
/// Creates a new instance of the <see cref="UniversalAssemblyResolver"/>.
/// </summary>
/// <param name="mainAssemblyFileName">
/// The full path to the "main assembly" (i.e., the assembly being decompiled). This is used to
/// resolve assemblies that are located next the main assembly. If no full path is used, the resolver
/// falls back to using <see cref="Environment.CurrentDirectory"/>.
/// </param>
/// <param name="throwOnError">
/// If <see langword="true"/> an <see cref="ResolutionException"/> is thrown, in case the
/// assembly reference cannot be resolved.
/// </param>
/// <param name="targetFramework">
/// The target framework name as used by <see cref="System.Runtime.Versioning.TargetFrameworkAttribute"/>.
/// That is, "{framework},Version={version}": currently it supports ".NETCoreApp", ".NETStandard" and
/// "Silverlight", if the string doesn't match any of these, the resolver falls back to ".NET Framework",
/// which is "classic" .NET &lt;= 4.8.
/// </param>
/// <param name="runtimePack">
/// Identifier of the runtime pack this assembly was compiled for.
/// If omitted, falling back to "Microsoft.NETCore.App" and this is ignored in case of classic .NET</param>
/// <param name="streamOptions">Options used for the <see cref="PEReader"/>.</param>
/// <param name="metadataOptions">Options used for the <see cref="MetadataReader"/>.</param>
public UniversalAssemblyResolver(string? mainAssemblyFileName, bool throwOnError, string? targetFramework,
string? runtimePack = null, PEStreamOptions streamOptions = PEStreamOptions.Default, MetadataReaderOptions metadataOptions = MetadataReaderOptions.Default)
{
this.mainAssemblyFileName = mainAssemblyFileName;
this.throwOnError = throwOnError;
this.streamOptions = streamOptions;
this.metadataOptions = metadataOptions;
this.targetFramework = targetFramework ?? string.Empty;
this.runtimePack = runtimePack ?? "Microsoft.NETCore.App";
(targetFrameworkIdentifier, targetFrameworkVersion) = ParseTargetFramework(this.targetFramework);
this.dotNetCorePathFinder = new Lazy<DotNetCorePathFinder>(InitDotNetCorePathFinder);
if (mainAssemblyFileName != null)
{
this.baseDirectory = Path.GetDirectoryName(mainAssemblyFileName);
if (string.IsNullOrWhiteSpace(this.baseDirectory))
this.baseDirectory = Environment.CurrentDirectory;
AddSearchDirectory(baseDirectory);
}
}
internal static (TargetFrameworkIdentifier, Version) ParseTargetFramework(string targetFramework)
{
if (string.IsNullOrEmpty(targetFramework))
return (TargetFrameworkIdentifier.NETFramework, ZeroVersion);
string[] tokens = targetFramework.Split(',');
TargetFrameworkIdentifier identifier;
switch (tokens[0].Trim().ToUpperInvariant())
{
case ".NETCOREAPP":
identifier = TargetFrameworkIdentifier.NETCoreApp;
break;
case ".NETSTANDARD":
identifier = TargetFrameworkIdentifier.NETStandard;
break;
case "SILVERLIGHT":
identifier = TargetFrameworkIdentifier.Silverlight;
break;
default:
identifier = TargetFrameworkIdentifier.NETFramework;
break;
}
Version? version = null;
for (int i = 1; i < tokens.Length; i++)
{
var pair = tokens[i].Trim().Split('=');
if (pair.Length != 2)
continue;
switch (pair[0].Trim().ToUpperInvariant())
{
case "VERSION":
var versionString = pair[1].TrimStart('v', 'V', ' ', '\t');
if (!Version.TryParse(versionString, out version))
{
version = null;
}
else
{
version = new Version(version.Major, version.Minor, version.Build < 0 ? 0 : version.Build);
}
// .NET 5 or greater still use ".NETCOREAPP" as TargetFrameworkAttribute value...
if (version?.Major >= 5 && identifier == TargetFrameworkIdentifier.NETCoreApp)
identifier = TargetFrameworkIdentifier.NET;
break;
}
}
return (identifier, version ?? ZeroVersion);
}
#if !VSADDIN
public MetadataFile? Resolve(IAssemblyReference name)
{
var file = FindAssemblyFile(name);
return CreatePEFileFromFileName(file, ex => new ResolutionException(name, file, ex));
}
public MetadataFile? ResolveModule(MetadataFile mainModule, string moduleName)
{
string? baseDirectory = Path.GetDirectoryName(mainModule.FileName);
if (baseDirectory == null)
return null;
string moduleFileName = Path.Combine(baseDirectory, moduleName);
return CreatePEFileFromFileName(moduleFileName, ex => new ResolutionException(mainModule.FileName, moduleName, moduleFileName, ex));
}
private MetadataFile? CreatePEFileFromFileName(string? fileName, Func<Exception?, Exception> makeException)
{
if (fileName == null)
{
if (throwOnError)
throw makeException(null);
return null;
}
try
{
FileStream stream = new FileStream(fileName, FileMode.Open, FileAccess.Read);
return new PEFile(fileName, stream, streamOptions, metadataOptions);
}
catch (BadImageFormatException ex)
{
if (throwOnError)
throw makeException(ex);
}
catch (IOException ex)
{
if (throwOnError)
throw makeException(ex);
}
return null;
}
public Task<MetadataFile?> ResolveAsync(IAssemblyReference name)
{
return Task.Run(() => Resolve(name));
}
public Task<MetadataFile?> ResolveModuleAsync(MetadataFile mainModule, string moduleName)
{
return Task.Run(() => ResolveModule(mainModule, moduleName));
}
#endif
public override bool IsSharedAssembly(IAssemblyReference reference, [NotNullWhen(true)] out string? runtimePack)
{
return dotNetCorePathFinder.Value.TryResolveDotNetCoreShared(reference, out runtimePack) != null;
}
public string? FindAssemblyFile(IAssemblyReference name)
{
#if VSADDIN
// The VS add-in compiles this file without the Instrumentation sources.
return FindAssemblyFileCore(name);
#else
Instrumentation.DecompilerEventSource.Log.AssemblyResolveStart(name);
string? resolvedFile = null;
try
{
resolvedFile = FindAssemblyFileCore(name);
return resolvedFile;
}
finally
{
Instrumentation.DecompilerEventSource.Log.AssemblyResolveStop(name, resolvedFile);
}
#endif
}
string? FindAssemblyFileCore(IAssemblyReference name)
{
if (name.IsWindowsRuntime)
{
return FindWindowsMetadataFile(name);
}
string? file;
#if !VSADDIN
file = FindNextToReferencingModule(name);
if (file != null)
return file;
#endif
switch (targetFrameworkIdentifier)
{
case TargetFrameworkIdentifier.NET:
case TargetFrameworkIdentifier.NETCoreApp:
case TargetFrameworkIdentifier.NETStandard:
if (IsZeroOrAllOnes(targetFrameworkVersion))
goto default;
file = dotNetCorePathFinder.Value.TryResolveDotNetCore(name);
if (file != null)
break;
goto default;
case TargetFrameworkIdentifier.Silverlight:
if (IsZeroOrAllOnes(targetFrameworkVersion))
goto default;
file = ResolveSilverlight(name, targetFrameworkVersion);
if (file != null)
break;
goto default;
default:
file = ResolveInternal(name);
break;
}
return file;
}
#if !VSADDIN
/// <summary>
/// Every other probe searches relative to the assembly being decompiled, whichever assembly
/// is asking. A caller that is following a chain of type forwarders needs the opposite: the
/// next assembly in the chain has to come from where the chain currently is, or the chain
/// leaves the framework it reached and can end up going in circles (issue #2054). Only a
/// caller that knows it is repairing such a chain asks for this, by setting
/// <see cref="AssemblyReference.PreferNextToReferencingModule"/>.
/// </summary>
string? FindNextToReferencingModule(IAssemblyReference name)
{
if (name is not AssemblyReference { PreferNextToReferencingModule: true, ReferencingModule: { } referrer })
return null;
// An entry of a package or a single-file bundle carries its path inside the container,
// which would be probed relative to the current working directory.
if (!Path.IsPathRooted(referrer.FileName))
return null;
string? directory = Path.GetDirectoryName(referrer.FileName);
return directory == null ? null : SearchDirectory(name, directory);
}
#endif
DotNetCorePathFinder InitDotNetCorePathFinder()
{
DotNetCorePathFinder dotNetCorePathFinder;
if (mainAssemblyFileName == null)
dotNetCorePathFinder = new DotNetCorePathFinder(targetFrameworkIdentifier, targetFrameworkVersion, runtimePack);
else
dotNetCorePathFinder = new DotNetCorePathFinder(mainAssemblyFileName, targetFramework, runtimePack, targetFrameworkIdentifier, targetFrameworkVersion);
dotNetCorePathFinder.Owner = this;
foreach (var directory in directories)
{
dotNetCorePathFinder.AddSearchDirectory(directory);
}
return dotNetCorePathFinder;
}
string? FindWindowsMetadataFile(IAssemblyReference name)
{
// Finding Windows Metadata (winmd) is currently only supported on Windows.
if (Environment.OSVersion.Platform != PlatformID.Win32NT)
return null;
// TODO : Find a way to detect the base directory for the required Windows SDK.
string? basePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "Windows Kits", "10", "References");
if (!Directory.Exists(basePath))
return FindWindowsMetadataInSystemDirectory(name);
// TODO : Find a way to detect the required Windows SDK version.
var di = new DirectoryInfo(basePath);
basePath = null;
foreach (var versionFolder in di.EnumerateDirectories())
{
basePath = versionFolder.FullName;
}
if (basePath == null)
return FindWindowsMetadataInSystemDirectory(name);
basePath = Path.Combine(basePath, name.Name);
if (!Directory.Exists(basePath))
return FindWindowsMetadataInSystemDirectory(name);
basePath = Path.Combine(basePath, FindClosestVersionDirectory(basePath, name.Version));
if (!Directory.Exists(basePath))
return FindWindowsMetadataInSystemDirectory(name);
string file = Path.Combine(basePath, name.Name + ".winmd");
if (!File.Exists(file))
return FindWindowsMetadataInSystemDirectory(name);
return file;
}
string? FindWindowsMetadataInSystemDirectory(IAssemblyReference name)
{
string file = Path.Combine(Environment.SystemDirectory, "WinMetadata", name.Name + ".winmd");
if (File.Exists(file))
return file;
return null;
}
/// <summary>
/// This only works on Windows
/// </summary>
string? ResolveSilverlight(IAssemblyReference name, Version? version)
{
string[] targetFrameworkSearchPaths = {
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Microsoft Silverlight"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "Microsoft Silverlight")
};
foreach (var baseDirectory in targetFrameworkSearchPaths)
{
if (!Directory.Exists(baseDirectory))
continue;
var versionDirectory = Path.Combine(baseDirectory, FindClosestVersionDirectory(baseDirectory, version));
var file = SearchDirectory(name, versionDirectory);
if (file != null)
return file;
}
return null;
}
string FindClosestVersionDirectory(string basePath, Version? version)
{
string? path = null;
foreach (var folder in new DirectoryInfo(basePath).GetDirectories().Select(DotNetCorePathFinder.ConvertToVersion)
.Where(v => v.Item1 != null).OrderByDescending(v => v.Item1))
{
if (path == null || version == null || folder.Item1 >= version)
path = folder.Item2.Name;
}
return path ?? version?.ToString() ?? ".";
}
string? ResolveInternal(IAssemblyReference name)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
var assembly = SearchDirectory(name, directories);
if (assembly != null)
return assembly;
string[] framework_dirs;
string framework_dir;
switch (decompilerRuntime)
{
case DecompilerRuntime.Mono:
framework_dir = Path.GetDirectoryName(typeof(object).Module.FullyQualifiedName)!;
framework_dirs = new[] { framework_dir, Path.Combine(framework_dir, "Facades") };
break;
case DecompilerRuntime.NETCoreApp:
string windir;
try
{
windir = Environment.GetFolderPath(Environment.SpecialFolder.Windows);
if (string.IsNullOrEmpty(windir))
{
goto default;
}
}
catch (PlatformNotSupportedException)
{
goto default;
}
framework_dir = Path.Combine(windir, "Microsoft.NET", "Framework64", "v4.0.30319");
framework_dirs = new[] { framework_dir };
break;
default:
framework_dir = Path.GetDirectoryName(typeof(object).Module.FullyQualifiedName)!;
framework_dirs = new[] { framework_dir };
break;
}
if (IsSpecialVersionOrRetargetable(name))
{
assembly = SearchDirectory(name, framework_dirs);
if (assembly != null)
return assembly;
}
if (name.Name == "mscorlib")
{
assembly = GetCorlib(name);
if (assembly != null)
return assembly;
}
assembly = GetAssemblyInGac(name);
if (assembly != null)
return assembly;
// when decompiling assemblies that target frameworks prior to 4.0, we can fall back to the 4.0 assemblies in case the target framework is not installed.
// but when looking for Microsoft.Build.Framework, Version=15.0.0.0 we should not use the version 4.0 assembly here so that the LoadedAssembly logic can instead fall back to version 15.1.0.0
if (name.Version <= new Version(4, 0, 0, 0))
{
assembly = SearchDirectory(name, framework_dirs);
if (assembly != null)
return assembly;
}
if (decompilerRuntime == DecompilerRuntime.NETCoreApp)
{
// Hosts without a .NET Framework installation (e.g. Linux, macOS) have no GAC;
// the only system-wide assembly store there is the shared-framework directory
// of the runtime executing the decompiler. Search it as the last resort,
// regardless of the requested version (the runtime itself rolls forward in the
// same way). Without this, e.g. the type-forwards of a netstandard facade
// (pointing to a versioned System.Runtime) are unresolvable on such hosts.
string runtimeDir = Path.GetDirectoryName(typeof(object).Module.FullyQualifiedName)!;
assembly = SearchDirectory(name, runtimeDir);
if (assembly != null)
return assembly;
}
if (throwOnError)
throw new ResolutionException(name, null, null);
return null;
}
#region .NET / mono GAC handling
string? SearchDirectory(IAssemblyReference name, IEnumerable<string?> directories)
{
foreach (var directory in directories)
{
if (directory == null)
continue;
string? file = SearchDirectory(name, directory);
if (file != null)
return file;
}
return null;
}
static bool IsSpecialVersionOrRetargetable(IAssemblyReference reference)
{
return IsZeroOrAllOnes(reference.Version) || reference.IsRetargetable;
}
static readonly string[] assemblyExtensions = { ".dll", ".exe" };
static readonly string[] windowsMetadataExtensions = { ".winmd", ".dll" };
string? SearchDirectory(IAssemblyReference name, string directory)
{
var extensions = name.IsWindowsRuntime ? windowsMetadataExtensions : assemblyExtensions;
foreach (var extension in extensions)
{
string file = Path.Combine(directory, name.Name + extension);
if (!File.Exists(file))
continue;
try
{
return file;
}
catch (BadImageFormatException)
{
continue;
}
}
return null;
}
static bool IsZeroOrAllOnes(Version? version)
{
return version == null
|| (version.Major == 0 && version.Minor == 0 && version.Build == 0 && version.Revision == 0)
|| (version.Major == 65535 && version.Minor == 65535 && version.Build == 65535 && version.Revision == 65535);
}
internal static Version ZeroVersion = new Version(0, 0, 0, 0);
string? GetCorlib(IAssemblyReference reference)
{
var version = reference.Version;
var corlib = typeof(object).Assembly.GetName();
if (decompilerRuntime != DecompilerRuntime.NETCoreApp)
{
if (corlib.Version == version || IsSpecialVersionOrRetargetable(reference))
return typeof(object).Module.FullyQualifiedName;
}
if (reference.PublicKeyToken == null)
return null;
if (version == null)
return null;
string? path;
if (decompilerRuntime == DecompilerRuntime.Mono)
{
path = GetMonoMscorlibBasePath(version);
}
else
{
path = GetMscorlibBasePath(version, reference.PublicKeyToken.ToHexString(8));
}
if (path == null)
return null;
var file = Path.Combine(path, "mscorlib.dll");
if (File.Exists(file))
return file;
return null;
}
string? GetMscorlibBasePath(Version version, string? publicKeyToken)
{
string? GetSubFolderForVersion()
{
switch (version.Major)
{
case 1:
if (version.MajorRevision == 3300)
return "v1.0.3705";
return "v1.1.4322";
case 2:
return "v2.0.50727";
case 4:
return "v4.0.30319";
default:
if (throwOnError)
throw new NotSupportedException("Version not supported: " + version);
return null;
}
}
if (publicKeyToken == "969db8053d3322ac")
{
string programFiles = Environment.Is64BitOperatingSystem ?
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86) :
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
string cfPath = $@"Microsoft.NET\SDK\CompactFramework\v{version.Major}.{version.Minor}\WindowsCE\";
string cfBasePath = Path.Combine(programFiles, cfPath);
if (Directory.Exists(cfBasePath))
return cfBasePath;
}
else
{
string rootPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Microsoft.NET");
string[] frameworkPaths = new[] {
Path.Combine(rootPath, "Framework"),
Path.Combine(rootPath, "Framework64")
};
string? folder = GetSubFolderForVersion();
if (folder != null)
{
foreach (var path in frameworkPaths)
{
var basePath = Path.Combine(path, folder);
if (Directory.Exists(basePath))
return basePath;
}
}
}
if (throwOnError)
throw new NotSupportedException("Version not supported: " + version);
return null;
}
string? GetMonoMscorlibBasePath(Version version)
{
var path = Directory.GetParent(typeof(object).Module.FullyQualifiedName)!.Parent!.FullName;
if (version.Major == 1)
path = Path.Combine(path, "1.0");
else if (version.Major == 2)
{
if (version.MajorRevision == 5)
path = Path.Combine(path, "2.1");
else
path = Path.Combine(path, "2.0");
}
else if (version.Major == 4)
path = Path.Combine(path, "4.0");
else
{
if (throwOnError)
throw new NotSupportedException("Version not supported: " + version);
return null;
}
return path;
}
public static List<string> GetGacPaths()
{
if (decompilerRuntime == DecompilerRuntime.Mono)
return GetDefaultMonoGacPaths();
var paths = new List<string>(2);
var windir = Environment.GetFolderPath(Environment.SpecialFolder.Windows);
if (windir == null)
return paths;
paths.Add(Path.Combine(windir, "assembly"));
paths.Add(Path.Combine(windir, "Microsoft.NET", "assembly"));
return paths;
}
static List<string> GetDefaultMonoGacPaths()
{
var paths = new List<string>(1);
var gac = GetCurrentMonoGac();
if (gac != null)
paths.Add(gac);
var gac_paths_env = Environment.GetEnvironmentVariable("MONO_GAC_PREFIX");
if (string.IsNullOrEmpty(gac_paths_env))
return paths;
var prefixes = gac_paths_env.Split(Path.PathSeparator);
foreach (var prefix in prefixes)
{
if (string.IsNullOrEmpty(prefix))
continue;
var gac_path = Path.Combine(Path.Combine(Path.Combine(prefix, "lib"), "mono"), "gac");
if (Directory.Exists(gac_path) && !paths.Contains(gac_path))
paths.Add(gac_path);
}
return paths;
}
static string GetCurrentMonoGac()
{
return Path.Combine(
Directory.GetParent(
Path.GetDirectoryName(typeof(object).Module.FullyQualifiedName)!)!.FullName,
"gac");
}
public static string? GetAssemblyInGac(IAssemblyReference reference)
{
if (reference.PublicKeyToken == null || reference.PublicKeyToken.Length == 0)
return null;
if (decompilerRuntime == DecompilerRuntime.Mono)
return GetAssemblyInMonoGac(reference);
return GetAssemblyInNetGac(reference);
}
static string? GetAssemblyInMonoGac(IAssemblyReference reference)
{
for (int i = 0; i < gac_paths.Count; i++)
{
var gac_path = gac_paths[i];
var file = GetAssemblyFile(reference, string.Empty, gac_path);
if (File.Exists(file))
return file;
}
return null;
}
static readonly string[] gacFolders = { "GAC_MSIL", "GAC_32", "GAC_64", "GAC" };
static readonly string[] gacFolderPrefixes = { string.Empty, "v4.0_" };
static string? GetAssemblyInNetGac(IAssemblyReference reference)
{
for (int i = 0; i < gac_paths.Count; i++)
{
for (int j = 0; j < gacFolders.Length; j++)
{
var gac = Path.Combine(gac_paths[i], gacFolders[j]);
var file = GetAssemblyFile(reference, gacFolderPrefixes[i], gac);
if (File.Exists(file))
return file;
}
}
// An exact version match is always preferred, so unification is a separate pass over
// the whole GAC rather than a fallback within one folder.
for (int i = 0; i < gac_paths.Count; i++)
{
for (int j = 0; j < gacFolders.Length; j++)
{
var gac = Path.Combine(gac_paths[i], gacFolders[j]);
var file = FindUnifiedAssemblyInGacFolder(reference, gacFolderPrefixes[i], gac);
if (file != null)
return file;
}
}
return null;
}
/// <summary>
/// Finds an assembly whose version differs from the requested one, because the runtime
/// unifies references to in-box assemblies onto whatever version the installed framework
/// carries. The versions genuinely differ: the .NET Framework 4.7.2/4.8 reference
/// assemblies of about a hundred assemblies (System.IO.Compression is 4.2.0.0, System.Runtime
/// is 4.1.2.0, ...) are higher than the 4.0.0.0 implementations in the GAC, which is the
/// only version ever installed there (issue #2080).
///
/// Unification is approximated by the highest installed version that shares the major
/// version and the public key token of the reference. The major version is what separates
/// assemblies that share a name but are different products, e.g. Microsoft.Build.Framework
/// 4.0.0.0 and 15.x.
/// </summary>
internal static string? FindUnifiedAssemblyInGacFolder(IAssemblyReference reference, string prefix, string gac)
{
var requestedVersion = reference.Version;
if (requestedVersion == null || reference.PublicKeyToken == null)
return null;
string assemblyDirectory = Path.Combine(gac, reference.Name);
if (!Directory.Exists(assemblyDirectory))
return null;
// The folder name is "{prefix}{version}_{culture}_{publicKeyToken}"; the culture of a
// non-satellite assembly is empty, which leaves the two underscores adjacent.
string suffix = "__" + reference.PublicKeyToken.ToHexString(8);
string? bestFile = null;
Version? bestVersion = null;
foreach (var candidate in Directory.EnumerateDirectories(assemblyDirectory, prefix + "*" + suffix))
{
string folderName = Path.GetFileName(candidate);
string versionText = folderName.Substring(prefix.Length, folderName.Length - prefix.Length - suffix.Length);
if (!Version.TryParse(versionText, out var version))
continue;
if (version.Major != requestedVersion.Major)
continue;
if (bestVersion != null && version <= bestVersion)
continue;
string file = Path.Combine(candidate, reference.Name + ".dll");
if (!File.Exists(file))
continue;
bestFile = file;
bestVersion = version;
}
return bestFile;
}
static string GetAssemblyFile(IAssemblyReference reference, string prefix, string gac)
{
var gac_folder = new StringBuilder()
.Append(prefix)
.Append(reference.Version);
gac_folder.Append("__");
for (int i = 0; i < reference.PublicKeyToken!.Length; i++)
gac_folder.Append(reference.PublicKeyToken[i].ToString("x2"));
return Path.Combine(gac, reference.Name, gac_folder.ToString(), reference.Name + ".dll");
}
/// <summary>
/// Gets the names of all assemblies in the GAC.
/// </summary>
public static IEnumerable<AssemblyNameReference> EnumerateGac()
{
foreach (var path in GetGacPaths())
{
foreach (var gac in gacFolders)
{
string rootPath = Path.Combine(path, gac);
if (!Directory.Exists(rootPath))
continue;
foreach (var item in new DirectoryInfo(rootPath).EnumerateFiles("*.dll", SearchOption.AllDirectories))
{
// The root of the GAC should only contain folders, but make sure we handle the case where it does NOT in case
// someone has a non-standard layout (e.g. due to a broken installer).
string? assemblyParentPath = Path.GetDirectoryName(item.FullName);
if (assemblyParentPath?.Length > rootPath.Length)
{
string[]? name = assemblyParentPath.Substring(rootPath.Length + 1).Split(new[] { "\\" }, StringSplitOptions.RemoveEmptyEntries);
if (name?.Length != 2)
continue;
var match = Regex.Match(name[1], $"(v4.0_)?(?<version>[^_]+)_(?<culture>[^_]+)?_(?<publicKey>[^_]+)");
if (!match.Success)
continue;
string culture = match.Groups["culture"].Value;
if (string.IsNullOrEmpty(culture))
culture = "neutral";
yield return AssemblyNameReference.Parse(name[0] + ", Version=" + match.Groups["version"].Value + ", Culture=" + culture + ", PublicKeyToken=" + match.Groups["publicKey"].Value);
}
}
}
}
}
#endregion
}
}