Skip to content

Commit

Permalink
Add BundleAssemblyResolver.
Browse files Browse the repository at this point in the history
  • Loading branch information
Washi1337 committed Apr 10, 2024
1 parent d150e58 commit 2fc1872
Show file tree
Hide file tree
Showing 11 changed files with 365 additions and 23 deletions.
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
70 changes: 69 additions & 1 deletion src/AsmResolver.DotNet/DotNetRuntimeInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace AsmResolver.DotNet
/// <summary>
/// Provides information about a target runtime.
/// </summary>
public readonly struct DotNetRuntimeInfo
public readonly struct DotNetRuntimeInfo : IEquatable<DotNetRuntimeInfo>
{
/// <summary>
/// The target framework name used by applications targeting .NET and .NET Core.
Expand All @@ -26,6 +26,14 @@ namespace AsmResolver.DotNet

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+)");

/// <summary>
/// Creates a new instance of the <see cref="DotNetRuntimeInfo"/> structure.
/// </summary>
Expand Down Expand Up @@ -99,6 +107,45 @@ public static bool TryParse(string frameworkName, out DotNetRuntimeInfo info)
return true;
}

/// <summary>
/// Parses the target framework moniker as provided in a <c>.runtimeconfig.json</c> file.
/// </summary>
/// <param name="moniker">The moniker</param>
/// <returns>The parsed version info.</returns>
public static DotNetRuntimeInfo ParseMoniker(string moniker)
{
return TryParseMoniker(moniker, out var info) ? info : throw new FormatException();
}

/// <summary>
/// Attempts to parse the target framework moniker as provided in a <c>.runtimeconfig.json</c> file.
/// </summary>
/// <param name="moniker">The moniker</param>
/// <param name="info">The parsed version info.</param>
/// <returns><c>true</c> if the provided name was in the correct format, <c>false</c> otherwise.</returns>
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;
}

/// <summary>
/// Obtains a reference to the default core lib reference of this runtime.
/// </summary>
Expand All @@ -108,5 +155,26 @@ public static bool TryParse(string frameworkName, out DotNetRuntimeInfo info)

/// <inheritdoc />
public override string ToString() => $"{Name},Version=v{Version}";

/// <inheritdoc />
public bool Equals(DotNetRuntimeInfo other)
{
return Name == other.Name && Version.Equals(other.Version);
}

/// <inheritdoc />
public override bool Equals(object? obj)
{
return obj is DotNetRuntimeInfo other && Equals(other);
}

/// <inheritdoc />
public override int GetHashCode()
{
unchecked
{
return (Name.GetHashCode() * 397) ^ Version.GetHashCode();
}
}
}
}
44 changes: 36 additions & 8 deletions src/AsmResolver.DotNet/RuntimeContext.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using System;
using System.Reflection;
using AsmResolver.DotNet.Bundles;
using AsmResolver.DotNet.Serialized;
using AsmResolver.IO;

Expand All @@ -13,10 +16,7 @@ public class RuntimeContext
/// </summary>
/// <param name="targetRuntime">The target runtime version.</param>
public RuntimeContext(DotNetRuntimeInfo targetRuntime)
: this(targetRuntime, new ModuleReaderParameters
{
PEReaderParameters = {FileService = new ByteArrayFileService()}
})
: this(targetRuntime, new ModuleReaderParameters(new ByteArrayFileService()))
{
}

Expand All @@ -28,10 +28,8 @@ public RuntimeContext(DotNetRuntimeInfo targetRuntime)
public RuntimeContext(DotNetRuntimeInfo targetRuntime, ModuleReaderParameters readerParameters)
{
TargetRuntime = targetRuntime;
AssemblyResolver = CreateAssemblyResolver(targetRuntime, new ModuleReaderParameters(readerParameters)
{
RuntimeContext = this
});
DefaultReaderParameters = new ModuleReaderParameters(readerParameters) {RuntimeContext = this};
AssemblyResolver = CreateAssemblyResolver(targetRuntime, DefaultReaderParameters);
}

/// <summary>
Expand All @@ -42,9 +40,31 @@ public RuntimeContext(DotNetRuntimeInfo targetRuntime, ModuleReaderParameters re
public RuntimeContext(DotNetRuntimeInfo targetRuntime, IAssemblyResolver assemblyResolver)
{
TargetRuntime = targetRuntime;
DefaultReaderParameters = new ModuleReaderParameters(new ByteArrayFileService()) {RuntimeContext = this};
AssemblyResolver = assemblyResolver;
}

/// <summary>
/// Creates a new runtime context for the provided bundled application.
/// </summary>
/// <param name="manifest">The bundle to create the runtime context for.</param>
public RuntimeContext(BundleManifest manifest)
: this(manifest, new ModuleReaderParameters(new ByteArrayFileService()))
{
}

/// <summary>
/// Creates a new runtime context.
/// </summary>
/// <param name="manifest">The bundle to create the runtime context for.</param>
/// <param name="readerParameters">The parameters to use when reading modules in this context.</param>
public RuntimeContext(BundleManifest manifest, ModuleReaderParameters readerParameters)
{
TargetRuntime = manifest.GetTargetRuntime();
DefaultReaderParameters = new ModuleReaderParameters(readerParameters) {RuntimeContext = this};
AssemblyResolver = new BundleAssemblyResolver(manifest, readerParameters);
}

/// <summary>
/// Gets the runtime version this context is targeting.
/// </summary>
Expand All @@ -53,6 +73,14 @@ public DotNetRuntimeInfo TargetRuntime
get;
}

/// <summary>
/// Gets the default parameters that are used for reading .NET modules in the context.
/// </summary>
public ModuleReaderParameters DefaultReaderParameters
{
get;
}

/// <summary>
/// Gets the assembly resolver that the context uses to resolve assemblies.
/// </summary>
Expand Down
10 changes: 10 additions & 0 deletions src/AsmResolver.DotNet/Serialized/ModuleReaderParameters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ public ModuleReaderParameters()
PEReaderParameters = new PEReaderParameters();
}

/// <summary>
/// Initializes the module read parameters with a file service.
/// </summary>
/// <param name="context">The context the module should be read in.</param>
public ModuleReaderParameters(RuntimeContext context)
: this(context.DefaultReaderParameters)
{
RuntimeContext = context;
}

/// <summary>
/// Initializes the module read parameters with a file service.
/// </summary>
Expand Down

0 comments on commit 2fc1872

Please sign in to comment.