Skip to content

Commit

Permalink
Merge pull request #537 from Washi1337/feature/runtime-contexts
Browse files Browse the repository at this point in the history
Runtime Contexts
  • Loading branch information
Washi1337 authored Apr 15, 2024
2 parents e20aafe + 5985b60 commit e8055ae
Show file tree
Hide file tree
Showing 28 changed files with 851 additions and 96 deletions.
51 changes: 45 additions & 6 deletions src/AsmResolver.DotNet/AssemblyDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,33 +33,72 @@ public class AssemblyDefinition : AssemblyDescriptor, IModuleProvider, IHasSecur
/// <param name="buffer">The raw contents of the executable file to load.</param>
/// <returns>The module.</returns>
/// <exception cref="BadImageFormatException">Occurs when the image does not contain a valid .NET metadata directory.</exception>
public static AssemblyDefinition FromBytes(byte[] buffer) =>
FromImage(PEImage.FromBytes(buffer));
public static AssemblyDefinition FromBytes(byte[] buffer) => FromBytes(buffer, new ModuleReaderParameters());

/// <summary>
/// Reads a .NET assembly from the provided input buffer.
/// </summary>
/// <param name="buffer">The raw contents of the executable file to load.</param>
/// <param name="readerParameters">The parameters to use while reading the assembly.</param>
/// <returns>The module.</returns>
/// <exception cref="BadImageFormatException">Occurs when the image does not contain a valid .NET metadata directory.</exception>
public static AssemblyDefinition FromBytes(byte[] buffer, ModuleReaderParameters readerParameters) =>
FromImage(PEImage.FromBytes(buffer, readerParameters.PEReaderParameters), readerParameters);

/// <summary>
/// Reads a .NET assembly from the provided input file.
/// </summary>
/// <param name="filePath">The file path to the input executable to load.</param>
/// <returns>The module.</returns>
/// <exception cref="BadImageFormatException">Occurs when the image does not contain a valid .NET metadata directory.</exception>
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)));

/// <summary>
/// Reads a .NET assembly from the provided input file.
/// </summary>
/// <param name="filePath">The file path to the input executable to load.</param>
/// <param name="readerParameters">The parameters to use while reading the assembly.</param>
/// <returns>The module.</returns>
/// <exception cref="BadImageFormatException">Occurs when the image does not contain a valid .NET metadata directory.</exception>
public static AssemblyDefinition FromFile(string filePath, ModuleReaderParameters readerParameters) =>
FromImage(PEImage.FromFile(filePath, readerParameters.PEReaderParameters), readerParameters);

/// <summary>
/// Reads a .NET assembly from the provided input file.
/// </summary>
/// <param name="file">The portable executable file to load.</param>
/// <returns>The module.</returns>
/// <exception cref="BadImageFormatException">Occurs when the image does not contain a valid .NET metadata directory.</exception>
public static AssemblyDefinition FromFile(PEFile file) => FromImage(PEImage.FromFile(file));
public static AssemblyDefinition FromFile(PEFile file) => FromFile(file, new ModuleReaderParameters());

/// <summary>
/// Reads a .NET assembly from the provided input file.
/// </summary>
/// <param name="file">The portable executable file to load.</param>
/// <param name="readerParameters">The parameters to use while reading the assembly.</param>
/// <returns>The module.</returns>
/// <exception cref="BadImageFormatException">Occurs when the image does not contain a valid .NET metadata directory.</exception>
public static AssemblyDefinition FromFile(PEFile file, ModuleReaderParameters readerParameters) =>
FromImage(PEImage.FromFile(file, readerParameters.PEReaderParameters), readerParameters);

/// <summary>
/// Reads a .NET assembly from the provided input file.
/// </summary>
/// <param name="file">The portable executable file to load.</param>
/// <returns>The module.</returns>
/// <exception cref="BadImageFormatException">Occurs when the image does not contain a valid .NET metadata directory.</exception>
public static AssemblyDefinition FromFile(IInputFile file) =>
FromFile(file, new ModuleReaderParameters(Path.GetDirectoryName(file.FilePath)));

/// <summary>
/// Reads a .NET assembly from the provided input file.
/// </summary>
/// <param name="file">The portable executable file to load.</param>
/// <param name="readerParameters">The parameters to use while reading the assembly.</param>
/// <returns>The module.</returns>
/// <exception cref="BadImageFormatException">Occurs when the image does not contain a valid .NET metadata directory.</exception>
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);

/// <summary>
/// Reads a .NET assembly from an input stream.
Expand Down
23 changes: 19 additions & 4 deletions src/AsmResolver.DotNet/AssemblyResolverBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -16,21 +17,35 @@ public abstract class AssemblyResolverBase : IAssemblyResolver
private static readonly string[] BinaryFileExtensions = {".dll", ".exe"};
private static readonly SignatureComparer Comparer = new(SignatureComparisonFlags.AcceptNewerVersions);

private readonly ConcurrentDictionary<AssemblyDescriptor, AssemblyDefinition> _cache = new(new SignatureComparer());
private readonly ConcurrentDictionary<AssemblyDescriptor, AssemblyDefinition> _cache = new(SignatureComparer.Default);

/// <summary>
/// Initializes the base of an assembly resolver.
/// </summary>
/// <param name="fileService">The service to use for reading files from the disk.</param>
protected AssemblyResolverBase(IFileService fileService)
{
FileService = fileService ?? throw new ArgumentNullException(nameof(fileService));
ReaderParameters = new ModuleReaderParameters(fileService);
}

/// <summary>
/// Initializes the base of an assembly resolver.
/// </summary>
/// <param name="readerParameters">The reader parameters used for reading new resolved assemblies.</param>
protected AssemblyResolverBase(ModuleReaderParameters readerParameters)
{
ReaderParameters = readerParameters;
}

/// <summary>
/// Gets the file service that is used for reading files from the disk.
/// </summary>
public IFileService FileService
public IFileService FileService => ReaderParameters.PEReaderParameters.FileService;

/// <summary>
/// Gets the reader parameters used for reading new resolved assemblies.
/// </summary>
public ModuleReaderParameters ReaderParameters
{
get;
}
Expand Down Expand Up @@ -128,7 +143,7 @@ public void AddToCache(AssemblyDescriptor descriptor, AssemblyDefinition definit
/// <returns>The assembly.</returns>
protected virtual AssemblyDefinition LoadAssemblyFromFile(string path)
{
return AssemblyDefinition.FromFile(FileService.OpenFile(path));
return AssemblyDefinition.FromFile(FileService.OpenFile(path), ReaderParameters);
}

/// <summary>
Expand Down
90 changes: 90 additions & 0 deletions src/AsmResolver.DotNet/Bundles/BundleAssemblyResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using System.Collections.Concurrent;
using System.IO;
using AsmResolver.DotNet.Serialized;
using AsmResolver.DotNet.Signatures;

namespace AsmResolver.DotNet.Bundles;

/// <summary>
/// Provides an implementation of an assembly resolver that prefers assemblies embedded in single-file-host executable.
/// </summary>
public class BundleAssemblyResolver : IAssemblyResolver
{
private readonly BundleManifest _manifest;
private readonly DotNetCoreAssemblyResolver _baseResolver;
private readonly ConcurrentDictionary<AssemblyDescriptor, AssemblyDefinition> _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);
}

/// <inheritdoc />
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;
}

/// <inheritdoc />
public void AddToCache(AssemblyDescriptor descriptor, AssemblyDefinition definition)
{
_baseResolver.AddToCache(descriptor, definition);
}

/// <inheritdoc />
public bool RemoveFromCache(AssemblyDescriptor descriptor)
{
// Note: This is intentionally not an or-else (||) construction.
return _embeddedFilesCache.TryRemove(descriptor, out _) | _baseResolver.RemoveFromCache(descriptor);
}

/// <inheritdoc />
public bool HasCached(AssemblyDescriptor descriptor)
{
return _embeddedFilesCache.ContainsKey(descriptor) || _baseResolver.HasCached(descriptor);
}

/// <inheritdoc />
public void ClearCache()
{
_embeddedFilesCache.Clear();
_baseResolver.ClearCache();
}
}
56 changes: 55 additions & 1 deletion src/AsmResolver.DotNet/Bundles/BundleManifest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -287,6 +287,60 @@ public string GenerateDeterministicBundleID()
.Replace('/', '_');
}

/// <summary>
/// Determines the runtime that the assemblies in the bundle are targeting.
/// </summary>
/// <returns>The runtime.</returns>
/// <exception cref="ArgumentException">Occurs when the runtime could not be determined.</exception>
public DotNetRuntimeInfo GetTargetRuntime()
{
return TryGetTargetRuntime(out var runtime)
? runtime
: throw new ArgumentException("Could not determine the target runtime for the bundle");
}

/// <summary>
/// Attempts to determine the runtime that the assemblies in the bundle are targeting.
/// </summary>
/// <param name="targetRuntime">When the method returns <c>true</c>, contains the target runtime.</param>
/// <returns><c>true</c> if the runtime could be determined, <c>false</c> otherwise.</returns>
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;
}

/// <summary>
/// Constructs a new application host file based on the bundle manifest.
/// </summary>
Expand Down
30 changes: 29 additions & 1 deletion src/AsmResolver.DotNet/DotNetCoreAssemblyResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -35,6 +36,17 @@ public DotNetCoreAssemblyResolver(IFileService fileService, Version runtimeVersi
{
}

/// <summary>
/// Creates a new .NET Core assembly resolver, by attempting to autodetect the current .NET or .NET Core
/// installation directory.
/// </summary>
/// <param name="readerParameters">The parameters to use while reading the assembly.</param>
/// <param name="runtimeVersion">The version of .NET to target.</param>
public DotNetCoreAssemblyResolver(ModuleReaderParameters readerParameters, Version runtimeVersion)
: this(readerParameters, null, runtimeVersion, DotNetCorePathProvider.Default)
{
}

/// <summary>
/// Creates a new .NET Core assembly resolver, by attempting to autodetect the current .NET or .NET Core
/// installation directory.
Expand Down Expand Up @@ -81,7 +93,23 @@ public DotNetCoreAssemblyResolver(
RuntimeConfiguration? configuration,
Version? fallbackVersion,
DotNetCorePathProvider pathProvider)
: base(fileService)
: this(new ModuleReaderParameters(fileService), configuration, fallbackVersion, pathProvider)
{
}

/// <summary>
/// Creates a new .NET Core assembly resolver.
/// </summary>
/// <param name="readerParameters">The parameters to use while reading the assembly.</param>
/// <param name="configuration">The runtime configuration to use, or <c>null</c> if no configuration is available.</param>
/// <param name="fallbackVersion">The version of .NET or .NET Core to use when no (valid) configuration is provided.</param>
/// <param name="pathProvider">The installation directory of .NET Core.</param>
public DotNetCoreAssemblyResolver(
ModuleReaderParameters readerParameters,
RuntimeConfiguration? configuration,
Version? fallbackVersion,
DotNetCorePathProvider pathProvider)
: base(readerParameters)
{
if (fallbackVersion is null)
throw new ArgumentNullException(nameof(fallbackVersion));
Expand Down
16 changes: 15 additions & 1 deletion src/AsmResolver.DotNet/DotNetFrameworkAssemblyResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using AsmResolver.DotNet.Serialized;
using AsmResolver.IO;

namespace AsmResolver.DotNet
Expand All @@ -25,7 +26,15 @@ public DotNetFrameworkAssemblyResolver()
/// </summary>
/// <param name="fileService">The service to use for reading files from the disk.</param>
public DotNetFrameworkAssemblyResolver(IFileService fileService)
: base(fileService)
: this(new ModuleReaderParameters(fileService))
{
}

/// <summary>
/// Creates a new default assembly resolver.
/// </summary>
public DotNetFrameworkAssemblyResolver(ModuleReaderParameters readerParameters)
: base(readerParameters)
{
DetectGacDirectories();
}
Expand Down Expand Up @@ -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)
Expand Down
Loading

0 comments on commit e8055ae

Please sign in to comment.