Browse Source

Merge 2b21f508e0 into c1abc29765

pull/4107/merge
Christoph Wille 3 days ago committed by GitHub
parent
commit
49ca126fb8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 16
      ICSharpCode.Decompiler/Metadata/UniversalAssemblyResolver.cs
  2. 121
      ICSharpCode.ILSpyCmd.Tests/XamarinCompressedInputTests.cs
  3. 30
      ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs
  4. 23
      ICSharpCode.ILSpyCmd/InputFileLoader.cs

16
ICSharpCode.Decompiler/Metadata/UniversalAssemblyResolver.cs

@ -273,8 +273,7 @@ namespace ICSharpCode.Decompiler.Metadata
try try
{ {
FileStream stream = new FileStream(fileName, FileMode.Open, FileAccess.Read); return LoadModuleFromFile(fileName);
return new PEFile(fileName, stream, streamOptions, metadataOptions);
} }
catch (BadImageFormatException ex) catch (BadImageFormatException ex)
{ {
@ -289,6 +288,19 @@ namespace ICSharpCode.Decompiler.Metadata
return null; return null;
} }
/// <summary>
/// Loads the module stored at <paramref name="fileName"/>, once the file for a reference
/// has been found. Override to accept file formats other than a plain PE image. Throws
/// <see cref="BadImageFormatException"/> or <see cref="IOException"/> for a file that is
/// not a loadable module; the caller turns that into null or a
/// <see cref="ResolutionException"/> according to the throwOnError setting.
/// </summary>
protected virtual MetadataFile LoadModuleFromFile(string fileName)
{
FileStream stream = new FileStream(fileName, FileMode.Open, FileAccess.Read);
return new PEFile(fileName, stream, streamOptions, metadataOptions);
}
public Task<MetadataFile?> ResolveAsync(IAssemblyReference name) public Task<MetadataFile?> ResolveAsync(IAssemblyReference name)
{ {
return Task.Run(() => Resolve(name)); return Task.Run(() => Resolve(name));

121
ICSharpCode.ILSpyCmd.Tests/XamarinCompressedInputTests.cs

@ -0,0 +1,121 @@
// Copyright (c) 2026 Christoph Wille
//
// 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.
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using ICSharpCode.Decompiler.Metadata;
using K4os.Compression.LZ4;
using NUnit.Framework;
using static ICSharpCode.ILSpyCmd.Tests.CliTestRunner;
namespace ICSharpCode.ILSpyCmd.Tests
{
/// <summary>
/// A Xamarin.Android app ships every assembly LZ4-compressed behind a 12-byte "XALZ" header.
/// These tests lay out a directory the way such an app does - the input assembly and the
/// assembly it references, both compressed - because decompiling one file of an app only
/// gives good output when its references next to it load too.
/// </summary>
[TestFixture]
public class ILSpyCmdXamarinCompressedInputTests
{
// Magic used for the Xamarin compressed module header ('XALZ', little-endian).
const uint CompressedDataMagic = 0x5A4C4158;
string tempDirectory;
string compressedInputPath;
[OneTimeSetUp]
public void CreateCompressedAssemblies()
{
tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
Directory.CreateDirectory(tempDirectory);
string inputAssembly = typeof(ILSpyCmdXamarinCompressedInputTests).Assembly.Location;
string referencedAssembly = typeof(TargetFrameworkIdentifier).Assembly.Location;
compressedInputPath = WriteCompressed(inputAssembly);
WriteCompressed(referencedAssembly);
}
[OneTimeTearDown]
public void DeleteCompressedAssemblies()
{
if (tempDirectory != null && Directory.Exists(tempDirectory))
Directory.Delete(tempDirectory, recursive: true);
}
/// <summary>
/// Writes the XALZ form of <paramref name="assemblyPath"/> into the temp directory under
/// the same file name, so references resolve by name next to the input, and returns it.
/// </summary>
string WriteCompressed(string assemblyPath)
{
byte[] original = File.ReadAllBytes(assemblyPath);
byte[] compressed = new byte[LZ4Codec.MaximumOutputSize(original.Length)];
int compressedLength = LZ4Codec.Encode(original, 0, original.Length, compressed, 0, compressed.Length);
string path = Path.Combine(tempDirectory, Path.GetFileName(assemblyPath));
using var stream = File.Create(path);
using var writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true);
writer.Write(CompressedDataMagic);
writer.Write((uint)0); // descriptor table index, unused by the loader
writer.Write((uint)original.Length);
writer.Write(compressed, 0, compressedLength);
return path;
}
[Test]
public async Task CompressedInputIsDecompiled()
{
var result = await RunAsync(compressedInputPath, "--disable-updatecheck",
"-m", "M:ICSharpCode.ILSpyCmd.Tests.MemberOptionSample.Add(System.Int32,System.Int32)");
Assert.That(result.ExitCode, Is.EqualTo(0), result.Error);
Assert.That(result.Output, Does.Contain("int Add(int a, int b)"));
}
/// <summary>
/// An enum from an unresolved reference is an unknown type, so a comparison against one of
/// its members prints as a cast of the raw value. The member name appearing proves the
/// compressed reference was loaded.
/// </summary>
[Test]
public async Task CompressedReferenceIsResolved()
{
var result = await RunAsync(compressedInputPath, "--disable-updatecheck",
"-m", "M:ICSharpCode.ILSpyCmd.Tests.XamarinReferenceSample.IsCore(ICSharpCode.Decompiler.Metadata.TargetFrameworkIdentifier)");
Assert.That(result.ExitCode, Is.EqualTo(0), result.Error);
Assert.That(result.Output, Does.Contain("id == TargetFrameworkIdentifier.NETCoreApp"));
}
}
public static class XamarinReferenceSample
{
public static bool IsCore(TargetFrameworkIdentifier id)
{
return id == TargetFrameworkIdentifier.NETCoreApp;
}
}
}

30
ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs

@ -557,16 +557,26 @@ Examples:
return InputFileLoader.Load(assemblyFileName, BundleEntryName, applyWinRTProjections); return InputFileLoader.Load(assemblyFileName, BundleEntryName, applyWinRTProjections);
} }
CSharpDecompiler GetDecompiler(string assemblyFileName) => GetDecompiler(assemblyFileName, out _); /// <summary>
/// Resolves the references of <paramref name="module"/> from next to the input file, the
CSharpDecompiler GetDecompiler(string assemblyFileName, out DecompilerSettings settings) /// -r directories, and the usual framework locations.
/// </summary>
UniversalAssemblyResolver CreateResolver(string assemblyFileName, PEFile module)
{ {
var module = LoadInputModule(assemblyFileName); var resolver = new FileLoaderAssemblyResolver(assemblyFileName, module.Metadata.DetectTargetFrameworkId());
var resolver = new UniversalAssemblyResolver(assemblyFileName, false, module.Metadata.DetectTargetFrameworkId());
foreach (var path in (ReferencePaths ?? Array.Empty<string>())) foreach (var path in (ReferencePaths ?? Array.Empty<string>()))
{ {
resolver.AddSearchDirectory(path); resolver.AddSearchDirectory(path);
} }
return resolver;
}
CSharpDecompiler GetDecompiler(string assemblyFileName) => GetDecompiler(assemblyFileName, out _);
CSharpDecompiler GetDecompiler(string assemblyFileName, out DecompilerSettings settings)
{
var module = LoadInputModule(assemblyFileName);
var resolver = CreateResolver(assemblyFileName, module);
settings = GetSettings(module); settings = GetSettings(module);
if (!settings.ApplyWindowsRuntimeProjections) if (!settings.ApplyWindowsRuntimeProjections)
{ {
@ -617,9 +627,7 @@ Examples:
bool isBaml = resourceName.EndsWith(".baml", StringComparison.OrdinalIgnoreCase); bool isBaml = resourceName.EndsWith(".baml", StringComparison.OrdinalIgnoreCase);
if (isBaml && value is byte[] bamlBytes) if (isBaml && value is byte[] bamlBytes)
{ {
var resolver = new UniversalAssemblyResolver(assemblyFileName, false, module.Metadata.DetectTargetFrameworkId()); var resolver = CreateResolver(assemblyFileName, module);
foreach (var path in (ReferencePaths ?? Array.Empty<string>()))
resolver.AddSearchDirectory(path);
var bamlSettings = new BamlDecompilerSettings { var bamlSettings = new BamlDecompilerSettings {
ThrowOnAssemblyResolveErrors = GetSettings(module).ThrowOnAssemblyResolveErrors ThrowOnAssemblyResolveErrors = GetSettings(module).ThrowOnAssemblyResolveErrors
}; };
@ -788,11 +796,7 @@ Examples:
ProjectId DecompileAsProject(string assemblyFileName, string projectFileName) ProjectId DecompileAsProject(string assemblyFileName, string projectFileName)
{ {
var module = LoadInputModule(assemblyFileName); var module = LoadInputModule(assemblyFileName);
var resolver = new UniversalAssemblyResolver(assemblyFileName, false, module.Metadata.DetectTargetFrameworkId()); var resolver = CreateResolver(assemblyFileName, module);
foreach (var path in (ReferencePaths ?? Array.Empty<string>()))
{
resolver.AddSearchDirectory(path);
}
var settings = GetSettings(module); var settings = GetSettings(module);
var debugInfo = TryLoadPDB(module); var debugInfo = TryLoadPDB(module);
// A WPF assembly keeps its XAML as BAML, so a project exported without converting it // A WPF assembly keeps its XAML as BAML, so a project exported without converting it

23
ICSharpCode.ILSpyCmd/InputFileLoader.cs

@ -79,7 +79,7 @@ namespace ICSharpCode.ILSpyCmd
return new PEFile(fileName, metadataOptions: MetadataOptions(context)); return new PEFile(fileName, metadataOptions: MetadataOptions(context));
} }
static LoadResult LoadFile(string fileName, FileLoadContext context) internal static LoadResult LoadFile(string fileName, FileLoadContext context)
{ {
using var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read); using var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read);
foreach (var loader in loaders.RegisteredLoaders) foreach (var loader in loaders.RegisteredLoaders)
@ -222,4 +222,25 @@ namespace ICSharpCode.ILSpyCmd
: MetadataReaderOptions.None; : MetadataReaderOptions.None;
} }
} }
/// <summary>
/// Resolves references the way the input file is loaded: a referenced file goes through the
/// file loaders first, so a Xamarin compressed module (or any other format a loader
/// understands) next to the input resolves like a plain assembly. A file no loader claims
/// is loaded as a PE image, as the base resolver does.
/// </summary>
sealed class FileLoaderAssemblyResolver : UniversalAssemblyResolver
{
static readonly FileLoadContext context = new FileLoadContext(ApplyWinRTProjections: true, null);
public FileLoaderAssemblyResolver(string mainAssemblyFileName, string targetFramework)
: base(mainAssemblyFileName, throwOnError: false, targetFramework)
{
}
protected override MetadataFile LoadModuleFromFile(string fileName)
{
return InputFileLoader.LoadFile(fileName, context)?.MetadataFile ?? base.LoadModuleFromFile(fileName);
}
}
} }

Loading…
Cancel
Save