diff --git a/src/AsmResolver.DotNet/AssemblyDefinition.cs b/src/AsmResolver.DotNet/AssemblyDefinition.cs index 814fe9f9e..942da8039 100644 --- a/src/AsmResolver.DotNet/AssemblyDefinition.cs +++ b/src/AsmResolver.DotNet/AssemblyDefinition.cs @@ -33,8 +33,17 @@ public class AssemblyDefinition : AssemblyDescriptor, IModuleProvider, IHasSecur /// The raw contents of the executable file to load. /// The module. /// Occurs when the image does not contain a valid .NET metadata directory. - public static AssemblyDefinition FromBytes(byte[] buffer) => - FromImage(PEImage.FromBytes(buffer)); + public static AssemblyDefinition FromBytes(byte[] buffer) => FromBytes(buffer, new ModuleReaderParameters()); + + /// + /// Reads a .NET assembly from the provided input buffer. + /// + /// The raw contents of the executable file to load. + /// The parameters to use while reading the assembly. + /// The module. + /// Occurs when the image does not contain a valid .NET metadata directory. + public static AssemblyDefinition FromBytes(byte[] buffer, ModuleReaderParameters readerParameters) => + FromImage(PEImage.FromBytes(buffer, readerParameters.PEReaderParameters), readerParameters); /// /// Reads a .NET assembly from the provided input file. @@ -42,8 +51,17 @@ public static AssemblyDefinition FromBytes(byte[] buffer) => /// The file path to the input executable to load. /// The module. /// Occurs when the image does not contain a valid .NET metadata directory. - public static AssemblyDefinition FromFile(string filePath) => - FromImage(PEImage.FromFile(filePath), new ModuleReaderParameters(Path.GetDirectoryName(filePath))); + public static AssemblyDefinition FromFile(string filePath) => FromFile(filePath, new ModuleReaderParameters(Path.GetDirectoryName(filePath))); + + /// + /// Reads a .NET assembly from the provided input file. + /// + /// The file path to the input executable to load. + /// The parameters to use while reading the assembly. + /// The module. + /// Occurs when the image does not contain a valid .NET metadata directory. + public static AssemblyDefinition FromFile(string filePath, ModuleReaderParameters readerParameters) => + FromImage(PEImage.FromFile(filePath, readerParameters.PEReaderParameters), readerParameters); /// /// Reads a .NET assembly from the provided input file. @@ -51,15 +69,36 @@ public static AssemblyDefinition FromFile(string filePath) => /// The portable executable file to load. /// The module. /// Occurs when the image does not contain a valid .NET metadata directory. - public static AssemblyDefinition FromFile(PEFile file) => FromImage(PEImage.FromFile(file)); + public static AssemblyDefinition FromFile(PEFile file) => FromFile(file, new ModuleReaderParameters()); /// /// Reads a .NET assembly from the provided input file. /// /// The portable executable file to load. + /// The parameters to use while reading the assembly. + /// The module. + /// Occurs when the image does not contain a valid .NET metadata directory. + public static AssemblyDefinition FromFile(PEFile file, ModuleReaderParameters readerParameters) => + FromImage(PEImage.FromFile(file, readerParameters.PEReaderParameters), readerParameters); + + /// + /// Reads a .NET assembly from the provided input file. + /// + /// The portable executable file to load. + /// The module. + /// Occurs when the image does not contain a valid .NET metadata directory. + public static AssemblyDefinition FromFile(IInputFile file) => + FromFile(file, new ModuleReaderParameters(Path.GetDirectoryName(file.FilePath))); + + /// + /// Reads a .NET assembly from the provided input file. + /// + /// The portable executable file to load. + /// The parameters to use while reading the assembly. /// The module. /// Occurs when the image does not contain a valid .NET metadata directory. - public static AssemblyDefinition FromFile(IInputFile file) => FromImage(PEImage.FromFile(file)); + public static AssemblyDefinition FromFile(IInputFile file, ModuleReaderParameters readerParameters) => + FromImage(PEImage.FromFile(file, readerParameters.PEReaderParameters), readerParameters); /// /// Reads a .NET assembly from an input stream. diff --git a/src/AsmResolver.DotNet/AssemblyResolverBase.cs b/src/AsmResolver.DotNet/AssemblyResolverBase.cs index 00c0f9ce9..bc48ed75d 100644 --- a/src/AsmResolver.DotNet/AssemblyResolverBase.cs +++ b/src/AsmResolver.DotNet/AssemblyResolverBase.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; +using AsmResolver.DotNet.Serialized; using AsmResolver.DotNet.Signatures; using AsmResolver.IO; @@ -16,7 +17,7 @@ public abstract class AssemblyResolverBase : IAssemblyResolver private static readonly string[] BinaryFileExtensions = {".dll", ".exe"}; private static readonly SignatureComparer Comparer = new(SignatureComparisonFlags.AcceptNewerVersions); - private readonly ConcurrentDictionary _cache = new(new SignatureComparer()); + private readonly ConcurrentDictionary _cache = new(SignatureComparer.Default); /// /// Initializes the base of an assembly resolver. @@ -24,13 +25,27 @@ public abstract class AssemblyResolverBase : IAssemblyResolver /// The service to use for reading files from the disk. protected AssemblyResolverBase(IFileService fileService) { - FileService = fileService ?? throw new ArgumentNullException(nameof(fileService)); + ReaderParameters = new ModuleReaderParameters(fileService); + } + + /// + /// Initializes the base of an assembly resolver. + /// + /// The reader parameters used for reading new resolved assemblies. + protected AssemblyResolverBase(ModuleReaderParameters readerParameters) + { + ReaderParameters = readerParameters; } /// /// Gets the file service that is used for reading files from the disk. /// - public IFileService FileService + public IFileService FileService => ReaderParameters.PEReaderParameters.FileService; + + /// + /// Gets the reader parameters used for reading new resolved assemblies. + /// + public ModuleReaderParameters ReaderParameters { get; } @@ -128,7 +143,7 @@ public void AddToCache(AssemblyDescriptor descriptor, AssemblyDefinition definit /// The assembly. protected virtual AssemblyDefinition LoadAssemblyFromFile(string path) { - return AssemblyDefinition.FromFile(FileService.OpenFile(path)); + return AssemblyDefinition.FromFile(FileService.OpenFile(path), ReaderParameters); } /// diff --git a/src/AsmResolver.DotNet/Bundles/BundleAssemblyResolver.cs b/src/AsmResolver.DotNet/Bundles/BundleAssemblyResolver.cs new file mode 100644 index 000000000..7d58c167d --- /dev/null +++ b/src/AsmResolver.DotNet/Bundles/BundleAssemblyResolver.cs @@ -0,0 +1,90 @@ +using System.Collections.Concurrent; +using System.IO; +using AsmResolver.DotNet.Serialized; +using AsmResolver.DotNet.Signatures; + +namespace AsmResolver.DotNet.Bundles; + +/// +/// Provides an implementation of an assembly resolver that prefers assemblies embedded in single-file-host executable. +/// +public class BundleAssemblyResolver : IAssemblyResolver +{ + private readonly BundleManifest _manifest; + private readonly DotNetCoreAssemblyResolver _baseResolver; + private readonly ConcurrentDictionary _embeddedFilesCache = new(SignatureComparer.Default); + + internal BundleAssemblyResolver(BundleManifest manifest, ModuleReaderParameters readerParameters) + { + _manifest = manifest; + + // Bundles are .NET core 3.1+ only -> we can always default to .NET Core assembly resolution. + _baseResolver = new DotNetCoreAssemblyResolver(readerParameters, manifest.GetTargetRuntime().Version); + } + + /// + public AssemblyDefinition? Resolve(AssemblyDescriptor assembly) + { + // Prefer embedded files before we forward to the default assembly resolution algorithm. + if (TryResolveFromEmbeddedFiles(assembly, out var resolved)) + return resolved; + + return _baseResolver.Resolve(assembly); + } + + private bool TryResolveFromEmbeddedFiles(AssemblyDescriptor assembly, out AssemblyDefinition? resolved) + { + if (_embeddedFilesCache.TryGetValue(assembly, out resolved)) + return true; + + try + { + for (int i = 0; i < _manifest.Files.Count; i++) + { + var file = _manifest.Files[i]; + if (file.Type != BundleFileType.Assembly) + continue; + + if (Path.GetFileNameWithoutExtension(file.RelativePath) == assembly.Name) + { + resolved = AssemblyDefinition.FromBytes(file.GetData(), _baseResolver.ReaderParameters); + _embeddedFilesCache.TryAdd(assembly, resolved); + return true; + } + } + } + catch + { + // Ignore any reader errors. + } + + resolved = null; + return false; + } + + /// + public void AddToCache(AssemblyDescriptor descriptor, AssemblyDefinition definition) + { + _baseResolver.AddToCache(descriptor, definition); + } + + /// + public bool RemoveFromCache(AssemblyDescriptor descriptor) + { + // Note: This is intentionally not an or-else (||) construction. + return _embeddedFilesCache.TryRemove(descriptor, out _) | _baseResolver.RemoveFromCache(descriptor); + } + + /// + public bool HasCached(AssemblyDescriptor descriptor) + { + return _embeddedFilesCache.ContainsKey(descriptor) || _baseResolver.HasCached(descriptor); + } + + /// + public void ClearCache() + { + _embeddedFilesCache.Clear(); + _baseResolver.ClearCache(); + } +} diff --git a/src/AsmResolver.DotNet/Bundles/BundleManifest.cs b/src/AsmResolver.DotNet/Bundles/BundleManifest.cs index 58e82defb..0885f3d82 100644 --- a/src/AsmResolver.DotNet/Bundles/BundleManifest.cs +++ b/src/AsmResolver.DotNet/Bundles/BundleManifest.cs @@ -6,10 +6,10 @@ using System.Text; using System.Threading; using AsmResolver.Collections; +using AsmResolver.DotNet.Config.Json; using AsmResolver.IO; using AsmResolver.PE.File; using AsmResolver.PE.File.Headers; -using AsmResolver.PE.Win32Resources; using AsmResolver.PE.Win32Resources.Builder; namespace AsmResolver.DotNet.Bundles @@ -287,6 +287,60 @@ public string GenerateDeterministicBundleID() .Replace('/', '_'); } + /// + /// Determines the runtime that the assemblies in the bundle are targeting. + /// + /// The runtime. + /// Occurs when the runtime could not be determined. + public DotNetRuntimeInfo GetTargetRuntime() + { + return TryGetTargetRuntime(out var runtime) + ? runtime + : throw new ArgumentException("Could not determine the target runtime for the bundle"); + } + + /// + /// Attempts to determine the runtime that the assemblies in the bundle are targeting. + /// + /// When the method returns true, contains the target runtime. + /// true if the runtime could be determined, false otherwise. + public bool TryGetTargetRuntime(out DotNetRuntimeInfo targetRuntime) + { + // Try find the runtimeconfig.json file. + for (int i = 0; i < Files.Count; i++) + { + var file = Files[i]; + if (file.Type == BundleFileType.RuntimeConfigJson) + { + var config = RuntimeConfiguration.FromJson(Encoding.UTF8.GetString(file.GetData())); + if (config is not {RuntimeOptions.TargetFrameworkMoniker: { } tfm}) + continue; + + if (DotNetRuntimeInfo.TryParseMoniker(tfm, out targetRuntime)) + return true; + } + } + + // If it is not present, make a best effort guess based on the bundle file format version. + switch (MajorVersion) + { + case 1: + targetRuntime = new DotNetRuntimeInfo(DotNetRuntimeInfo.NetCoreApp, new Version(3, 1)); + return true; + + case 2: + targetRuntime = new DotNetRuntimeInfo(DotNetRuntimeInfo.NetCoreApp, new Version(5, 0)); + return true; + + case 6: + targetRuntime = new DotNetRuntimeInfo(DotNetRuntimeInfo.NetCoreApp, new Version(6, 0)); + return true; + } + + targetRuntime = default; + return false; + } + /// /// Constructs a new application host file based on the bundle manifest. /// diff --git a/src/AsmResolver.DotNet/DotNetCoreAssemblyResolver.cs b/src/AsmResolver.DotNet/DotNetCoreAssemblyResolver.cs index 2bf0ed76d..b30c35585 100644 --- a/src/AsmResolver.DotNet/DotNetCoreAssemblyResolver.cs +++ b/src/AsmResolver.DotNet/DotNetCoreAssemblyResolver.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using AsmResolver.DotNet.Config.Json; +using AsmResolver.DotNet.Serialized; using AsmResolver.IO; namespace AsmResolver.DotNet @@ -35,6 +36,17 @@ public DotNetCoreAssemblyResolver(IFileService fileService, Version runtimeVersi { } + /// + /// Creates a new .NET Core assembly resolver, by attempting to autodetect the current .NET or .NET Core + /// installation directory. + /// + /// The parameters to use while reading the assembly. + /// The version of .NET to target. + public DotNetCoreAssemblyResolver(ModuleReaderParameters readerParameters, Version runtimeVersion) + : this(readerParameters, null, runtimeVersion, DotNetCorePathProvider.Default) + { + } + /// /// Creates a new .NET Core assembly resolver, by attempting to autodetect the current .NET or .NET Core /// installation directory. @@ -81,7 +93,23 @@ public DotNetCoreAssemblyResolver( RuntimeConfiguration? configuration, Version? fallbackVersion, DotNetCorePathProvider pathProvider) - : base(fileService) + : this(new ModuleReaderParameters(fileService), configuration, fallbackVersion, pathProvider) + { + } + + /// + /// Creates a new .NET Core assembly resolver. + /// + /// The parameters to use while reading the assembly. + /// The runtime configuration to use, or null if no configuration is available. + /// The version of .NET or .NET Core to use when no (valid) configuration is provided. + /// The installation directory of .NET Core. + public DotNetCoreAssemblyResolver( + ModuleReaderParameters readerParameters, + RuntimeConfiguration? configuration, + Version? fallbackVersion, + DotNetCorePathProvider pathProvider) + : base(readerParameters) { if (fallbackVersion is null) throw new ArgumentNullException(nameof(fallbackVersion)); diff --git a/src/AsmResolver.DotNet/DotNetFrameworkAssemblyResolver.cs b/src/AsmResolver.DotNet/DotNetFrameworkAssemblyResolver.cs index 6bd043431..ce9ce3425 100644 --- a/src/AsmResolver.DotNet/DotNetFrameworkAssemblyResolver.cs +++ b/src/AsmResolver.DotNet/DotNetFrameworkAssemblyResolver.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using AsmResolver.DotNet.Serialized; using AsmResolver.IO; namespace AsmResolver.DotNet @@ -25,7 +26,15 @@ public DotNetFrameworkAssemblyResolver() /// /// The service to use for reading files from the disk. public DotNetFrameworkAssemblyResolver(IFileService fileService) - : base(fileService) + : this(new ModuleReaderParameters(fileService)) + { + } + + /// + /// Creates a new default assembly resolver. + /// + public DotNetFrameworkAssemblyResolver(ModuleReaderParameters readerParameters) + : base(readerParameters) { DetectGacDirectories(); } @@ -92,7 +101,12 @@ private void DetectMonoGacDirectories() .FirstOrDefault(); if (mostRecentMonoDirectory is not null) + { SearchDirectories.Add(mostRecentMonoDirectory); + string facadesDirectory = Path.Combine(mostRecentMonoDirectory, "Facades"); + if (Directory.Exists(facadesDirectory)) + SearchDirectories.Add(facadesDirectory); + } } private void AddGacDirectories(string windowsGac, string? prefix) diff --git a/src/AsmResolver.DotNet/DotNetRuntimeInfo.cs b/src/AsmResolver.DotNet/DotNetRuntimeInfo.cs index 00d733f67..28d6e36bb 100644 --- a/src/AsmResolver.DotNet/DotNetRuntimeInfo.cs +++ b/src/AsmResolver.DotNet/DotNetRuntimeInfo.cs @@ -7,7 +7,7 @@ namespace AsmResolver.DotNet /// /// Provides information about a target runtime. /// - public readonly struct DotNetRuntimeInfo + public readonly struct DotNetRuntimeInfo : IEquatable { /// /// The target framework name used by applications targeting .NET and .NET Core. @@ -26,6 +26,14 @@ public readonly struct DotNetRuntimeInfo private static readonly Regex FormatRegex = new(@"([a-zA-Z.]+)\s*,\s*Version=v(\d+\.\d+)"); + private static readonly Regex NetFxMonikerRegex = new(@"net(\d)(\d)(\d?)"); + + private static readonly Regex NetCoreAppMonikerRegex = new(@"netcoreapp(\d)\.(\d)"); + + private static readonly Regex NetStandardMonikerRegex = new(@"netstandard(\d)\.(\d)"); + + private static readonly Regex NetMonikerRegex = new(@"net(\d+)\.(\d+)"); + /// /// Creates a new instance of the structure. /// @@ -99,6 +107,45 @@ public static bool TryParse(string frameworkName, out DotNetRuntimeInfo info) return true; } + /// + /// Parses the target framework moniker as provided in a .runtimeconfig.json file. + /// + /// The moniker + /// The parsed version info. + public static DotNetRuntimeInfo ParseMoniker(string moniker) + { + return TryParseMoniker(moniker, out var info) ? info : throw new FormatException(); + } + + /// + /// Attempts to parse the target framework moniker as provided in a .runtimeconfig.json file. + /// + /// The moniker + /// The parsed version info. + /// true if the provided name was in the correct format, false otherwise. + public static bool TryParseMoniker(string moniker, out DotNetRuntimeInfo info) + { + info = default; + string runtime; + + Match match; + if ((match = NetMonikerRegex.Match(moniker)).Success) + runtime = NetCoreApp; + else if ((match = NetCoreAppMonikerRegex.Match(moniker)).Success) + runtime = NetCoreApp; + else if ((match = NetStandardMonikerRegex.Match(moniker)).Success) + runtime = NetStandard; + else if ((match = NetFxMonikerRegex.Match(moniker)).Success) + runtime = NetFramework; + else + return false; + + var version = new Version(int.Parse(match.Groups[1].Value), int.Parse(match.Groups[2].Value)); + + info = new DotNetRuntimeInfo(runtime, version); + return true; + } + /// /// Obtains a reference to the default core lib reference of this runtime. /// @@ -108,5 +155,26 @@ public static bool TryParse(string frameworkName, out DotNetRuntimeInfo info) /// public override string ToString() => $"{Name},Version=v{Version}"; + + /// + public bool Equals(DotNetRuntimeInfo other) + { + return Name == other.Name && Version.Equals(other.Version); + } + + /// + public override bool Equals(object? obj) + { + return obj is DotNetRuntimeInfo other && Equals(other); + } + + /// + public override int GetHashCode() + { + unchecked + { + return (Name.GetHashCode() * 397) ^ Version.GetHashCode(); + } + } } } diff --git a/src/AsmResolver.DotNet/ModuleDefinition.cs b/src/AsmResolver.DotNet/ModuleDefinition.cs index 3330de4d0..975497cb4 100644 --- a/src/AsmResolver.DotNet/ModuleDefinition.cs +++ b/src/AsmResolver.DotNet/ModuleDefinition.cs @@ -4,7 +4,6 @@ using System.IO; using System.Linq; using System.Reflection; -using System.Runtime.InteropServices; using System.Threading; using AsmResolver.Collections; using AsmResolver.DotNet.Builder; @@ -62,7 +61,7 @@ public class ModuleDefinition : /// The module. /// Occurs when the image does not contain a valid .NET metadata directory. public static ModuleDefinition FromBytes(byte[] buffer) => - FromImage(PEImage.FromBytes(buffer)); + FromBytes(buffer, new ModuleReaderParameters()); /// /// Reads a .NET module from the provided input buffer. @@ -99,15 +98,25 @@ public static ModuleDefinition FromFile(string filePath, ModuleReaderParameters /// The portable executable file to load. /// The module. /// Occurs when the image does not contain a valid .NET metadata directory. - public static ModuleDefinition FromFile(IInputFile file) => FromImage(PEImage.FromFile(file)); + public static ModuleDefinition FromFile(IInputFile file) => FromFile(file, new ModuleReaderParameters()); /// /// Reads a .NET module from the provided input file. /// /// The portable executable file to load. + /// The parameters to use while reading the module. /// The module. /// Occurs when the image does not contain a valid .NET metadata directory. - public static ModuleDefinition FromFile(IPEFile file) => FromImage(PEImage.FromFile(file)); + public static ModuleDefinition FromFile(IInputFile file, ModuleReaderParameters readerParameters) => + FromImage(PEImage.FromFile(file, readerParameters.PEReaderParameters), readerParameters); + + /// + /// Reads a .NET module from the provided input file. + /// + /// The portable executable file to load. + /// The module. + /// Occurs when the image does not contain a valid .NET metadata directory. + public static ModuleDefinition FromFile(IPEFile file) => FromFile(file, new ModuleReaderParameters()); /// /// Reads a .NET module from the provided input file. @@ -250,8 +259,9 @@ public static ModuleDefinition FromImage(IPEImage peImage) public static ModuleDefinition FromImage(IPEImage peImage, ModuleReaderParameters readerParameters) => new SerializedModuleDefinition(peImage, readerParameters); - // Disable non-nullable property initialization warnings for the CorLibTypeFactory and MetadataResolver - // properties. These are expected to be initialized by constructors that use this base constructor. + // Disable non-nullable property initialization warnings for the CorLibTypeFactory, RuntimeContext and + // MetadataResolver properties. These are expected to be initialized by constructors that use this base + // constructor. #pragma warning disable 8618 /// @@ -295,7 +305,9 @@ public ModuleDefinition(Utf8String? name) Name = name; CorLibTypeFactory = CorLibTypeFactory.CreateMscorlib40TypeFactory(this); - MetadataResolver = new DefaultMetadataResolver(new DotNetFrameworkAssemblyResolver()); + OriginalTargetRuntime = DetectTargetRuntime(); + RuntimeContext = new RuntimeContext(OriginalTargetRuntime); + MetadataResolver = new DefaultMetadataResolver(RuntimeContext.AssemblyResolver); TopLevelTypes.Add(new TypeDefinition(null, TypeDefinition.ModuleTypeName, 0)); } @@ -310,13 +322,10 @@ public ModuleDefinition(string? name, AssemblyReference corLib) { Name = name; - var importer = new ReferenceImporter(this); - corLib = (AssemblyReference) importer.ImportScope(corLib); - - CorLibTypeFactory = new CorLibTypeFactory(corLib); - + CorLibTypeFactory = new CorLibTypeFactory(corLib.ImportWith(DefaultImporter)); OriginalTargetRuntime = DetectTargetRuntime(); - MetadataResolver = new DefaultMetadataResolver(CreateAssemblyResolver(UncachedFileService.Instance)); + RuntimeContext = new RuntimeContext(OriginalTargetRuntime); + MetadataResolver = new DefaultMetadataResolver(RuntimeContext.AssemblyResolver); TopLevelTypes.Add(new TypeDefinition(null, TypeDefinition.ModuleTypeName, 0)); } @@ -342,7 +351,16 @@ public virtual IDotNetDirectory? DotNetDirectory } = null; /// - /// Gets the runtime that this module targeted upon creation or reading. + /// Gets the object describing the current active runtime context the module is loaded in. + /// + public RuntimeContext RuntimeContext + { + get; + protected set; + } + + /// + /// Gets the runtime that this module was targeted for at compile-time. /// public DotNetRuntimeInfo OriginalTargetRuntime { @@ -1203,48 +1221,6 @@ protected DotNetRuntimeInfo DetectTargetRuntime() : CorLibTypeFactory.ExtractDotNetRuntimeInfo(); } - /// - /// Creates an assembly resolver based on the corlib reference. - /// - /// The resolver. - protected IAssemblyResolver CreateAssemblyResolver(IFileService fileService) - { - string? directory = !string.IsNullOrEmpty(FilePath) - ? Path.GetDirectoryName(FilePath) - : null; - - var runtime = OriginalTargetRuntime; - - AssemblyResolverBase resolver; - switch (runtime.Name) - { - case DotNetRuntimeInfo.NetFramework: - case DotNetRuntimeInfo.NetStandard - when string.IsNullOrEmpty(DotNetCorePathProvider.DefaultInstallationPath): - resolver = new DotNetFrameworkAssemblyResolver(fileService); - break; - - case DotNetRuntimeInfo.NetStandard - when DotNetCorePathProvider.Default.TryGetLatestStandardCompatibleVersion( - runtime.Version, out var coreVersion): - resolver = new DotNetCoreAssemblyResolver(fileService, coreVersion); - break; - - case DotNetRuntimeInfo.NetCoreApp: - resolver = new DotNetCoreAssemblyResolver(fileService, runtime.Version); - break; - - default: - resolver = new DotNetFrameworkAssemblyResolver(fileService); - break; - } - - if (!string.IsNullOrEmpty(directory)) - resolver.SearchDirectories.Add(directory!); - - return resolver; - } - /// public override string ToString() => Name ?? string.Empty; diff --git a/src/AsmResolver.DotNet/RuntimeContext.cs b/src/AsmResolver.DotNet/RuntimeContext.cs new file mode 100644 index 000000000..77435698c --- /dev/null +++ b/src/AsmResolver.DotNet/RuntimeContext.cs @@ -0,0 +1,113 @@ +using System; +using System.Reflection; +using AsmResolver.DotNet.Bundles; +using AsmResolver.DotNet.Serialized; +using AsmResolver.IO; + +namespace AsmResolver.DotNet +{ + /// + /// Describes a context in which a .NET runtime is active. + /// + public class RuntimeContext + { + /// + /// Creates a new runtime context. + /// + /// The target runtime version. + public RuntimeContext(DotNetRuntimeInfo targetRuntime) + : this(targetRuntime, new ModuleReaderParameters(new ByteArrayFileService())) + { + } + + /// + /// Creates a new runtime context. + /// + /// The target runtime version. + /// The parameters to use when reading modules in this context. + public RuntimeContext(DotNetRuntimeInfo targetRuntime, ModuleReaderParameters readerParameters) + { + TargetRuntime = targetRuntime; + DefaultReaderParameters = new ModuleReaderParameters(readerParameters) {RuntimeContext = this}; + AssemblyResolver = CreateAssemblyResolver(targetRuntime, DefaultReaderParameters); + } + + /// + /// Creates a new runtime context. + /// + /// The target runtime version. + /// The assembly resolver to use when resolving assemblies into this context. + public RuntimeContext(DotNetRuntimeInfo targetRuntime, IAssemblyResolver assemblyResolver) + { + TargetRuntime = targetRuntime; + DefaultReaderParameters = new ModuleReaderParameters(new ByteArrayFileService()) {RuntimeContext = this}; + AssemblyResolver = assemblyResolver; + } + + /// + /// Creates a new runtime context for the provided bundled application. + /// + /// The bundle to create the runtime context for. + public RuntimeContext(BundleManifest manifest) + : this(manifest, new ModuleReaderParameters(new ByteArrayFileService())) + { + } + + /// + /// Creates a new runtime context. + /// + /// The bundle to create the runtime context for. + /// The parameters to use when reading modules in this context. + public RuntimeContext(BundleManifest manifest, ModuleReaderParameters readerParameters) + { + TargetRuntime = manifest.GetTargetRuntime(); + DefaultReaderParameters = new ModuleReaderParameters(readerParameters) {RuntimeContext = this}; + AssemblyResolver = new BundleAssemblyResolver(manifest, readerParameters); + } + + /// + /// Gets the runtime version this context is targeting. + /// + public DotNetRuntimeInfo TargetRuntime + { + get; + } + + /// + /// Gets the default parameters that are used for reading .NET modules in the context. + /// + public ModuleReaderParameters DefaultReaderParameters + { + get; + } + + /// + /// Gets the assembly resolver that the context uses to resolve assemblies. + /// + public IAssemblyResolver AssemblyResolver + { + get; + } + + private static IAssemblyResolver CreateAssemblyResolver( + DotNetRuntimeInfo runtime, + ModuleReaderParameters readerParameters) + { + switch (runtime.Name) + { + case DotNetRuntimeInfo.NetFramework: + case DotNetRuntimeInfo.NetStandard when string.IsNullOrEmpty(DotNetCorePathProvider.DefaultInstallationPath): + return new DotNetFrameworkAssemblyResolver(readerParameters); + + case DotNetRuntimeInfo.NetStandard when DotNetCorePathProvider.Default.TryGetLatestStandardCompatibleVersion(runtime.Version, out var coreVersion): + return new DotNetCoreAssemblyResolver(readerParameters, coreVersion); + + case DotNetRuntimeInfo.NetCoreApp: + return new DotNetCoreAssemblyResolver(readerParameters, runtime.Version); + + default: + return new DotNetFrameworkAssemblyResolver(readerParameters); + } + } + } +} diff --git a/src/AsmResolver.DotNet/Serialized/DefaultMethodBodyReader.cs b/src/AsmResolver.DotNet/Serialized/DefaultMethodBodyReader.cs index 07dbb19ea..39d7fb846 100644 --- a/src/AsmResolver.DotNet/Serialized/DefaultMethodBodyReader.cs +++ b/src/AsmResolver.DotNet/Serialized/DefaultMethodBodyReader.cs @@ -12,6 +12,11 @@ namespace AsmResolver.DotNet.Serialized /// public class DefaultMethodBodyReader : IMethodBodyReader { + /// + /// Gets the singleton instance of the class. + /// + public static DefaultMethodBodyReader Instance { get; } = new(); + /// public virtual MethodBody? ReadMethodBody(ModuleReaderContext context, MethodDefinition owner, in MethodDefinitionRow row) { diff --git a/src/AsmResolver.DotNet/Serialized/ModuleReaderContext.cs b/src/AsmResolver.DotNet/Serialized/ModuleReaderContext.cs index 737eab418..92e109817 100644 --- a/src/AsmResolver.DotNet/Serialized/ModuleReaderContext.cs +++ b/src/AsmResolver.DotNet/Serialized/ModuleReaderContext.cs @@ -25,7 +25,7 @@ public ModuleReaderContext(IPEImage image, SerializedModuleDefinition parentModu { Image = image ?? throw new ArgumentNullException(nameof(image)); ParentModule = parentModule ?? throw new ArgumentNullException(nameof(parentModule)); - Parameters = parameters ?? throw new ArgumentNullException(nameof(parameters)); + Parameters = new ModuleReaderParameters(parameters) ?? throw new ArgumentNullException(nameof(parameters)); // Both CLR and CoreCLR implement a slightly different loading procedure for EnC metadata. // While the difference is very subtle, it has a slight effect on which streams are selected diff --git a/src/AsmResolver.DotNet/Serialized/ModuleReaderParameters.cs b/src/AsmResolver.DotNet/Serialized/ModuleReaderParameters.cs index 1696903a8..29a30dcc9 100644 --- a/src/AsmResolver.DotNet/Serialized/ModuleReaderParameters.cs +++ b/src/AsmResolver.DotNet/Serialized/ModuleReaderParameters.cs @@ -1,4 +1,4 @@ -using System; +using AsmResolver.IO; using AsmResolver.PE; using AsmResolver.PE.DotNet.Metadata; @@ -13,6 +13,28 @@ public class ModuleReaderParameters /// Initializes the default module read parameters. /// public ModuleReaderParameters() + { + MethodBodyReader = DefaultMethodBodyReader.Instance; + FieldRvaDataReader = AsmResolver.PE.DotNet.Metadata.FieldRvaDataReader.Instance; + PEReaderParameters = new PEReaderParameters(); + } + + /// + /// Initializes the module read parameters with a file service. + /// + /// The context the module should be read in. + public ModuleReaderParameters(RuntimeContext context) + : this(context.DefaultReaderParameters) + { + RuntimeContext = context; + } + + /// + /// Initializes the module read parameters with a file service. + /// + /// The file service to use when reading the file and dependencies. + public ModuleReaderParameters(IFileService fileService) + : this(null, new PEReaderParameters {FileService = fileService}) { } @@ -40,6 +62,16 @@ public ModuleReaderParameters(string? workingDirectory) /// The working directory of the modules to read. /// The object responsible for recording parser errors. public ModuleReaderParameters(string? workingDirectory, IErrorListener errorListener) + : this(workingDirectory, new PEReaderParameters(errorListener)) + { + } + + /// + /// Initializes the module read parameters with a working directory. + /// + /// The working directory of the modules to read. + /// The parameters to use while reading the assembly and its dependencies. + public ModuleReaderParameters(string? workingDirectory, PEReaderParameters readerParameters) { if (workingDirectory is not null) { @@ -47,7 +79,23 @@ public ModuleReaderParameters(string? workingDirectory, IErrorListener errorList ModuleResolver = new DirectoryNetModuleResolver(workingDirectory, this); } - PEReaderParameters.ErrorListener = errorListener; + MethodBodyReader = DefaultMethodBodyReader.Instance; + FieldRvaDataReader = AsmResolver.PE.DotNet.Metadata.FieldRvaDataReader.Instance; + PEReaderParameters = readerParameters; + } + + /// + /// Clones the provided module reader parameters. + /// + /// The parameters to clone. + public ModuleReaderParameters(ModuleReaderParameters readerParameters) + { + WorkingDirectory = readerParameters.WorkingDirectory; + ModuleResolver = readerParameters.ModuleResolver; + MethodBodyReader = readerParameters.MethodBodyReader; + FieldRvaDataReader = readerParameters.FieldRvaDataReader; + PEReaderParameters = readerParameters.PEReaderParameters; + RuntimeContext = readerParameters.RuntimeContext; } /// @@ -74,7 +122,7 @@ public IMethodBodyReader MethodBodyReader { get; set; - } = new DefaultMethodBodyReader(); + } /// /// Gets or sets the field initial value reader. @@ -83,7 +131,7 @@ public IFieldRvaDataReader FieldRvaDataReader { get; set; - } = new FieldRvaDataReader(); + } /// /// Gets or sets the parameters used for parsing a PE file into a PE image. @@ -95,6 +143,15 @@ public PEReaderParameters PEReaderParameters { get; set; - } = new(); + } + + /// + /// Gets or sets the runtime context to load the module in, or null if a new context is to be created. + /// + public RuntimeContext? RuntimeContext + { + get; + set; + } } } diff --git a/src/AsmResolver.DotNet/Serialized/SerializedModuleDefinition.cs b/src/AsmResolver.DotNet/Serialized/SerializedModuleDefinition.cs index 4d5ff6282..d76fb01a9 100644 --- a/src/AsmResolver.DotNet/Serialized/SerializedModuleDefinition.cs +++ b/src/AsmResolver.DotNet/Serialized/SerializedModuleDefinition.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.IO; using AsmResolver.DotNet.Collections; using AsmResolver.DotNet.Signatures.Types; using AsmResolver.PE; @@ -70,15 +71,34 @@ public SerializedModuleDefinition(IPEImage peImage, ModuleReaderParameters reade OriginalTargetRuntime = DetectTargetRuntime(); // Initialize metadata resolution engines. - var resolver = CreateAssemblyResolver(readerParameters.PEReaderParameters.FileService); - if (!string.IsNullOrEmpty(readerParameters.WorkingDirectory) - && resolver is AssemblyResolverBase resolverBase - && !resolverBase.SearchDirectories.Contains(readerParameters.WorkingDirectory!)) + if (readerParameters.RuntimeContext is { } runtimeContext) { - resolverBase.SearchDirectories.Add(readerParameters.WorkingDirectory!); + RuntimeContext = runtimeContext; + } + else + { + RuntimeContext = new RuntimeContext(OriginalTargetRuntime, readerParameters); + + if (RuntimeContext.AssemblyResolver is AssemblyResolverBase resolver) + { + // Add current file's directory as a search directory (if present). + if (!string.IsNullOrEmpty(peImage.FilePath) + && Path.GetDirectoryName(peImage.FilePath) is { } directory + && !resolver.SearchDirectories.Contains(directory)) + { + resolver.SearchDirectories.Add(directory); + } + + // Add current working directory as a search directory (if present). + if (!string.IsNullOrEmpty(readerParameters.WorkingDirectory) + && !resolver.SearchDirectories.Contains(readerParameters.WorkingDirectory!)) + { + resolver.SearchDirectories.Add(readerParameters.WorkingDirectory!); + } + } } - MetadataResolver = new DefaultMetadataResolver(resolver); + MetadataResolver = new DefaultMetadataResolver(RuntimeContext.AssemblyResolver); // Prepare lazy RID lists. _fieldLists = new LazyRidListRelation(metadata, TableIndex.Field, TableIndex.TypeDef, @@ -271,7 +291,7 @@ protected override IList GetExportedTypes() } /// - protected override string GetRuntimeVersion() => ReaderContext.Metadata!.VersionString; + protected override string GetRuntimeVersion() => ReaderContext.Metadata.VersionString; /// protected override IManagedEntryPoint? GetManagedEntryPoint() diff --git a/src/AsmResolver.PE/Certificates/DefaultCertificateReader.cs b/src/AsmResolver.PE/Certificates/DefaultCertificateReader.cs index 60992e9ab..f20240465 100644 --- a/src/AsmResolver.PE/Certificates/DefaultCertificateReader.cs +++ b/src/AsmResolver.PE/Certificates/DefaultCertificateReader.cs @@ -8,6 +8,11 @@ namespace AsmResolver.PE.Certificates /// public class DefaultCertificateReader : ICertificateReader { + /// + /// Gets the singleton instance of the class. + /// + public static DefaultCertificateReader Instance { get; } = new(); + /// public AttributeCertificate ReadCertificate( PEReaderContext context, diff --git a/src/AsmResolver.PE/Debug/DefaultDebugDataReader.cs b/src/AsmResolver.PE/Debug/DefaultDebugDataReader.cs index 6af91f361..219aad8b0 100644 --- a/src/AsmResolver.PE/Debug/DefaultDebugDataReader.cs +++ b/src/AsmResolver.PE/Debug/DefaultDebugDataReader.cs @@ -8,6 +8,11 @@ namespace AsmResolver.PE.Debug /// public class DefaultDebugDataReader : IDebugDataReader { + /// + /// Gets the singleton instance of the class. + /// + public static DefaultDebugDataReader Instance { get; } = new(); + /// public IDebugDataSegment? ReadDebugData(PEReaderContext context, DebugDataType type, ref BinaryStreamReader reader) diff --git a/src/AsmResolver.PE/DotNet/Metadata/FieldRvaDataReader.cs b/src/AsmResolver.PE/DotNet/Metadata/FieldRvaDataReader.cs index 07cb6d674..e51018fe7 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/FieldRvaDataReader.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/FieldRvaDataReader.cs @@ -13,6 +13,11 @@ namespace AsmResolver.PE.DotNet.Metadata /// public class FieldRvaDataReader : IFieldRvaDataReader { + /// + /// Gets the singleton instance of the class. + /// + public static FieldRvaDataReader Instance { get; } = new(); + /// public ISegment? ResolveFieldData( IErrorListener listener, diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/DefaultReadyToRunSectionReader.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/DefaultReadyToRunSectionReader.cs index f1e95f821..6e6062b9e 100644 --- a/src/AsmResolver.PE/DotNet/ReadyToRun/DefaultReadyToRunSectionReader.cs +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/DefaultReadyToRunSectionReader.cs @@ -9,6 +9,11 @@ namespace AsmResolver.PE.DotNet.ReadyToRun /// public class DefaultReadyToRunSectionReader : IReadyToRunSectionReader { + /// + /// Gets the singleton instance of the class. + /// + public static DefaultReadyToRunSectionReader Instance { get; } = new(); + /// public IReadyToRunSection ReadSection(PEReaderContext context, ReadyToRunSectionType type, ref BinaryStreamReader reader) { diff --git a/src/AsmResolver.PE/PEImage.cs b/src/AsmResolver.PE/PEImage.cs index 7f2b49481..dc1eadcf6 100644 --- a/src/AsmResolver.PE/PEImage.cs +++ b/src/AsmResolver.PE/PEImage.cs @@ -145,8 +145,17 @@ public static IPEImage FromReader(in BinaryStreamReader reader, PEMappingMode mo /// The file representing the PE. /// The PE image that was opened. /// Occurs when the file does not follow the PE file format. - public static IPEImage FromFile(IInputFile inputFile) => - FromFile(PEFile.FromFile(inputFile), new PEReaderParameters()); + public static IPEImage FromFile(IInputFile inputFile) => FromFile(inputFile, new PEReaderParameters()); + + /// + /// Opens a PE image from an input file object. + /// + /// The file representing the PE. + /// The parameters to use while reading the PE image. + /// The PE image that was opened. + /// Occurs when the file does not follow the PE file format. + public static IPEImage FromFile(IInputFile inputFile, PEReaderParameters readerParameters) => + FromFile(PEFile.FromFile(inputFile), readerParameters); /// /// Opens a PE image from a PE file object. diff --git a/src/AsmResolver.PE/PEReaderParameters.cs b/src/AsmResolver.PE/PEReaderParameters.cs index 32a48c4ad..145cd11e4 100644 --- a/src/AsmResolver.PE/PEReaderParameters.cs +++ b/src/AsmResolver.PE/PEReaderParameters.cs @@ -26,10 +26,11 @@ public PEReaderParameters() /// The object responsible for recording parser errors. public PEReaderParameters(IErrorListener errorListener) { - MetadataStreamReader = new DefaultMetadataStreamReader(); - DebugDataReader = new DefaultDebugDataReader(); - CertificateReader = new DefaultCertificateReader(); - ReadyToRunSectionReader = new DefaultReadyToRunSectionReader(); + MetadataStreamReader = DefaultMetadataStreamReader.Instance; + DebugDataReader = DefaultDebugDataReader.Instance; + CertificateReader = DefaultCertificateReader.Instance; + FileService = UncachedFileService.Instance; + ReadyToRunSectionReader = DefaultReadyToRunSectionReader.Instance; ErrorListener = errorListener ?? throw new ArgumentNullException(nameof(errorListener)); } @@ -77,7 +78,7 @@ public IFileService FileService { get; set; - } = UncachedFileService.Instance; + } /// /// Gets or sets the object to use for reading ReadyToRun metadata sections from the disk while reading the diff --git a/src/AsmResolver/IO/ByteArrayFileService.cs b/src/AsmResolver/IO/ByteArrayFileService.cs index 02816d600..4551bed50 100644 --- a/src/AsmResolver/IO/ByteArrayFileService.cs +++ b/src/AsmResolver/IO/ByteArrayFileService.cs @@ -22,6 +22,17 @@ public IInputFile OpenFile(string filePath) return _files.GetOrAdd(filePath, x => new ByteArrayInputFile(x)); } + /// + /// Assigns a file path to a byte array and opens it. + /// + /// The file path. + /// The contents of the file. + /// The opened file. + public IInputFile OpenBytesAsFile(string filePath, byte[] contents) + { + return _files.GetOrAdd(filePath, p => new ByteArrayInputFile(p, contents, 0)); + } + /// public void InvalidateFile(string filePath) => _files.TryRemove(filePath, out _); diff --git a/test/AsmResolver.Benchmarks/AsmResolver.Benchmarks.csproj b/test/AsmResolver.Benchmarks/AsmResolver.Benchmarks.csproj index 0f275278b..fc1b5225f 100644 --- a/test/AsmResolver.Benchmarks/AsmResolver.Benchmarks.csproj +++ b/test/AsmResolver.Benchmarks/AsmResolver.Benchmarks.csproj @@ -12,11 +12,13 @@ - - - - - + + + + + + + diff --git a/test/AsmResolver.Benchmarks/TypeResolutionBenchmark.cs b/test/AsmResolver.Benchmarks/TypeResolutionBenchmark.cs new file mode 100644 index 000000000..bd612489d --- /dev/null +++ b/test/AsmResolver.Benchmarks/TypeResolutionBenchmark.cs @@ -0,0 +1,32 @@ +using AsmResolver.DotNet; +using AsmResolver.DotNet.Serialized; +using AsmResolver.DotNet.TestCases.Methods; +using AsmResolver.DotNet.TestCases.Types; +using AsmResolver.IO; +using BenchmarkDotNet.Attributes; + +namespace AsmResolver.Benchmarks; + +[MemoryDiagnoser] +public class TypeResolutionBenchmark +{ + [Params(false, true)] + public bool UsingRuntimeContext { get; set; } + + [Benchmark] + public void ResolveSystemObjectInTwoAssemblies() + { + var service = new ByteArrayFileService(); + var parameters = new ModuleReaderParameters(service); + + var module1 = ModuleDefinition.FromFile(typeof(Class).Assembly.Location, parameters); + + if (UsingRuntimeContext) + parameters.RuntimeContext = module1.RuntimeContext; + + var module2 = ModuleDefinition.FromFile(typeof(SingleMethod).Assembly.Location, parameters); + + _ = module1.CorLibTypeFactory.Object.Resolve(); + _ = module2.CorLibTypeFactory.Object.Resolve(); + } +} diff --git a/test/AsmResolver.DotNet.Tests/Bundles/BundleManifestTest.cs b/test/AsmResolver.DotNet.Tests/Bundles/BundleManifestTest.cs index 6cf4bc39e..812f76f6e 100644 --- a/test/AsmResolver.DotNet.Tests/Bundles/BundleManifestTest.cs +++ b/test/AsmResolver.DotNet.Tests/Bundles/BundleManifestTest.cs @@ -5,6 +5,7 @@ using System.Runtime.InteropServices; using System.Text; using AsmResolver.DotNet.Bundles; +using AsmResolver.DotNet.Serialized; using AsmResolver.IO; using AsmResolver.PE; using AsmResolver.PE.DotNet.Cil; @@ -94,6 +95,46 @@ public void WriteBundleManifestV6Windows() "Hello, World!\n"); } + [Fact] + public void DetectNetCoreApp31Bundle() + { + var manifest = BundleManifest.FromBytes(Properties.Resources.HelloWorld_SingleFile_V1); + Assert.Equal( + new DotNetRuntimeInfo(DotNetRuntimeInfo.NetCoreApp, new Version(3, 1)), + manifest.GetTargetRuntime() + ); + } + + [Fact] + public void DetectNet50Bundle() + { + var manifest = BundleManifest.FromBytes(Properties.Resources.HelloWorld_SingleFile_V2); + Assert.Equal( + new DotNetRuntimeInfo(DotNetRuntimeInfo.NetCoreApp, new Version(5, 0)), + manifest.GetTargetRuntime() + ); + } + + [Fact] + public void DetectNet60Bundle() + { + var manifest = BundleManifest.FromBytes(Properties.Resources.HelloWorld_SingleFile_V6); + Assert.Equal( + new DotNetRuntimeInfo(DotNetRuntimeInfo.NetCoreApp, new Version(6, 0)), + manifest.GetTargetRuntime() + ); + } + + [Fact] + public void DetectNet80Bundle() + { + var manifest = BundleManifest.FromBytes(Properties.Resources.HelloWorld_SingleFile_V6_WithDependency); + Assert.Equal( + new DotNetRuntimeInfo(DotNetRuntimeInfo.NetCoreApp, new Version(8, 0)), + manifest.GetTargetRuntime() + ); + } + [SkippableFact] public void MarkFilesAsCompressed() { @@ -359,5 +400,19 @@ private static void AssertBundlesAreEqual(BundleManifest manifest, BundleManifes Assert.Equal(file.GetData(), newFile.GetData()); } } + + [Fact] + public void BundleRuntimeContext() + { + var manifest = BundleManifest.FromBytes(Properties.Resources.HelloWorld_SingleFile_V6_WithDependency); + var context = new RuntimeContext(manifest); + + var module = ModuleDefinition.FromBytes( + manifest.Files.First(x => x.RelativePath == "MainApp.dll").GetData(), + new ModuleReaderParameters(context)); + + var resolved = module.AssemblyReferences.First(x => x.Name == "Library").Resolve(); + Assert.NotNull(resolved); + } } } diff --git a/test/AsmResolver.DotNet.Tests/DotNetRuntimeInfoTest.cs b/test/AsmResolver.DotNet.Tests/DotNetRuntimeInfoTest.cs index 206a808cf..1a1b60272 100644 --- a/test/AsmResolver.DotNet.Tests/DotNetRuntimeInfoTest.cs +++ b/test/AsmResolver.DotNet.Tests/DotNetRuntimeInfoTest.cs @@ -1,3 +1,4 @@ +using System; using System.Reflection; using AsmResolver.DotNet.Signatures; using Xunit; @@ -6,6 +7,22 @@ namespace AsmResolver.DotNet.Tests { public class DotNetRuntimeInfoTest { + [Theory] + [InlineData(".NETFramework,Version=v2.0", DotNetRuntimeInfo.NetFramework, 2, 0)] + [InlineData(".NETFramework,Version=v3.5", DotNetRuntimeInfo.NetFramework, 3, 5)] + [InlineData(".NETFramework,Version=v4.0", DotNetRuntimeInfo.NetFramework, 4, 0)] + [InlineData(".NETStandard,Version=v1.0", DotNetRuntimeInfo.NetStandard, 1, 0)] + [InlineData(".NETStandard,Version=v2.0", DotNetRuntimeInfo.NetStandard, 2, 0)] + [InlineData(".NETCoreApp,Version=v2.0", DotNetRuntimeInfo.NetCoreApp, 2, 0)] + [InlineData(".NETCoreApp,Version=v5.0", DotNetRuntimeInfo.NetCoreApp, 5, 0)] + public void Parse(string name, string expectedFramework, int major, int minor) + { + Assert.Equal( + new DotNetRuntimeInfo(expectedFramework, new Version(major, minor)), + DotNetRuntimeInfo.Parse(name) + ); + } + [Theory] [InlineData(".NETFramework,Version=v2.0", "mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")] [InlineData(".NETFramework,Version=v3.5", "mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")] @@ -19,7 +36,26 @@ public void DefaultCorLib(string name, string expectedCorLib) Assert.Equal( new ReflectionAssemblyDescriptor(new AssemblyName(expectedCorLib)), (AssemblyDescriptor) DotNetRuntimeInfo.Parse(name).GetDefaultCorLib(), - SignatureComparer.Default); + SignatureComparer.Default + ); + } + + [Theory] + [InlineData("net20", DotNetRuntimeInfo.NetFramework, 2, 0)] + [InlineData("net35", DotNetRuntimeInfo.NetFramework, 3, 5)] + [InlineData("net40", DotNetRuntimeInfo.NetFramework, 4, 0)] + [InlineData("net47", DotNetRuntimeInfo.NetFramework, 4, 7)] + [InlineData("net472", DotNetRuntimeInfo.NetFramework, 4, 7)] + [InlineData("netstandard2.0", DotNetRuntimeInfo.NetStandard, 2, 0)] + [InlineData("netcoreapp2.1", DotNetRuntimeInfo.NetCoreApp, 2,1)] + [InlineData("net5.0", DotNetRuntimeInfo.NetCoreApp, 5, 0)] + [InlineData("net8.0", DotNetRuntimeInfo.NetCoreApp, 8, 0)] + public void ParseMoniker(string tfm, string expectedFramework, int major, int minor) + { + Assert.Equal( + new DotNetRuntimeInfo(expectedFramework, new Version(major, minor)), + DotNetRuntimeInfo.ParseMoniker(tfm) + ); } } } diff --git a/test/AsmResolver.DotNet.Tests/Properties/Resources.Designer.cs b/test/AsmResolver.DotNet.Tests/Properties/Resources.Designer.cs index e36534bf9..cd22ef17d 100644 --- a/test/AsmResolver.DotNet.Tests/Properties/Resources.Designer.cs +++ b/test/AsmResolver.DotNet.Tests/Properties/Resources.Designer.cs @@ -164,6 +164,13 @@ public static byte[] HelloWorld_SingleFile_V6_WithResources { } } + public static byte[] HelloWorld_SingleFile_V6_WithDependency { + get { + object obj = ResourceManager.GetObject("HelloWorld_SingleFile_V6_WithDependency", resourceCulture); + return ((byte[])(obj)); + } + } + public static byte[] HelloWorld_UnusualNestedTypeRefOrder { get { object obj = ResourceManager.GetObject("HelloWorld_UnusualNestedTypeRefOrder", resourceCulture); diff --git a/test/AsmResolver.DotNet.Tests/Properties/Resources.resx b/test/AsmResolver.DotNet.Tests/Properties/Resources.resx index d0c0c7189..f7b1908b2 100644 --- a/test/AsmResolver.DotNet.Tests/Properties/Resources.resx +++ b/test/AsmResolver.DotNet.Tests/Properties/Resources.resx @@ -69,6 +69,9 @@ ..\Resources\HelloWorld.SingleFile.v6.WithResources.exe;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + ..\Resources\HelloWorld.SingleFile.v6.WithDependency.exe;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + ..\Resources\HelloWorld.UnusualNestedTypeRefOrder.exe;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 diff --git a/test/AsmResolver.DotNet.Tests/Resources/HelloWorld.SingleFile.v6.WithDependency.exe b/test/AsmResolver.DotNet.Tests/Resources/HelloWorld.SingleFile.v6.WithDependency.exe new file mode 100755 index 000000000..19fb20328 Binary files /dev/null and b/test/AsmResolver.DotNet.Tests/Resources/HelloWorld.SingleFile.v6.WithDependency.exe differ diff --git a/test/AsmResolver.DotNet.Tests/RuntimeContextTest.cs b/test/AsmResolver.DotNet.Tests/RuntimeContextTest.cs new file mode 100644 index 000000000..19f855181 --- /dev/null +++ b/test/AsmResolver.DotNet.Tests/RuntimeContextTest.cs @@ -0,0 +1,100 @@ +using System; +using AsmResolver.DotNet.Serialized; +using AsmResolver.DotNet.TestCases.Methods; +using AsmResolver.DotNet.TestCases.Types; +using AsmResolver.IO; +using Xunit; + +namespace AsmResolver.DotNet.Tests +{ + public class RuntimeContextTest + { + [Fact] + public void ResolveDependencyShouldUseSameRuntimeContext() + { + var main = ModuleDefinition.FromBytes(Properties.Resources.HelloWorld); + var dependency = main.CorLibTypeFactory.CorLibScope.GetAssembly()!.Resolve()!.ManifestModule!; + + Assert.Same(main.RuntimeContext, dependency.RuntimeContext); + } + + [Fact] + public void ResolveDependencyShouldUseSameFileService() + { + var service = new ByteArrayFileService(); + service.OpenBytesAsFile("HelloWorld.dll", Properties.Resources.HelloWorld); + + var main = ModuleDefinition.FromFile("HelloWorld.dll", new ModuleReaderParameters(service)); + var dependency = main.CorLibTypeFactory.CorLibScope.GetAssembly()!.Resolve()!.ManifestModule!; + + Assert.Contains(main.FilePath, service.GetOpenedFiles()); + Assert.Contains(dependency.FilePath, service.GetOpenedFiles()); + } + + [Fact] + public void DetectNetFrameworkContext() + { + var module = ModuleDefinition.FromBytes(Properties.Resources.HelloWorld); + Assert.Equal( + new DotNetRuntimeInfo(DotNetRuntimeInfo.NetFramework, new Version(4, 0)), + module.RuntimeContext.TargetRuntime + ); + } + + [Fact] + public void DetectNetCoreAppContext() + { + var module = ModuleDefinition.FromBytes(Properties.Resources.HelloWorld_NetCore); + Assert.Equal( + new DotNetRuntimeInfo(DotNetRuntimeInfo.NetCoreApp, new Version(2, 2)), + module.RuntimeContext.TargetRuntime + ); + } + + [Fact] + public void ForceNetFXLoadAsNetCore() + { + var context = new RuntimeContext(new DotNetRuntimeInfo(DotNetRuntimeInfo.NetCoreApp, new Version(3, 1))); + var module = ModuleDefinition.FromBytes(Properties.Resources.HelloWorld, new ModuleReaderParameters(context)); + + Assert.Equal(context.TargetRuntime, module.RuntimeContext.TargetRuntime); + Assert.IsAssignableFrom(module.MetadataResolver.AssemblyResolver); + } + + [Fact] + public void ForceNetStandardLoadAsNetFx() + { + var context = new RuntimeContext(new DotNetRuntimeInfo(DotNetRuntimeInfo.NetFramework, new Version(4, 8))); + var module = ModuleDefinition.FromFile(typeof(Class).Assembly.Location, new ModuleReaderParameters(context)); + + Assert.Equal(context.TargetRuntime, module.RuntimeContext.TargetRuntime); + Assert.Equal("mscorlib", module.CorLibTypeFactory.Object.Resolve()?.Module?.Assembly?.Name); + } + + [Fact] + public void ForceNetStandardLoadAsNetCore() + { + var context = new RuntimeContext(new DotNetRuntimeInfo(DotNetRuntimeInfo.NetCoreApp, new Version(3, 1))); + var module = ModuleDefinition.FromFile(typeof(Class).Assembly.Location, new ModuleReaderParameters(context)); + + Assert.Equal(context.TargetRuntime, module.RuntimeContext.TargetRuntime); + Assert.Equal("System.Private.CoreLib", module.CorLibTypeFactory.Object.Resolve()?.Module?.Assembly?.Name); + } + + [Fact] + public void ResolveSameDependencyInSameContextShouldResultInSameAssembly() + { + var module1 = ModuleDefinition.FromFile(typeof(Class).Assembly.Location); + var module2 = ModuleDefinition.FromFile(typeof(SingleMethod).Assembly.Location, new ModuleReaderParameters + { + RuntimeContext = module1.RuntimeContext + }); + + var object1 = module1.CorLibTypeFactory.Object.Resolve(); + var object2 = module2.CorLibTypeFactory.Object.Resolve(); + + Assert.NotNull(object1); + Assert.Same(object1, object2); + } + } +}