diff --git a/ICSharpCode.Decompiler/Metadata/UniversalAssemblyResolver.cs b/ICSharpCode.Decompiler/Metadata/UniversalAssemblyResolver.cs
index afee1ed2b7..acb7c0d08d 100644
--- a/ICSharpCode.Decompiler/Metadata/UniversalAssemblyResolver.cs
+++ b/ICSharpCode.Decompiler/Metadata/UniversalAssemblyResolver.cs
@@ -238,8 +238,7 @@ internal static (TargetFrameworkIdentifier, Version) ParseTargetFramework(string
try
{
- FileStream stream = new FileStream(fileName, FileMode.Open, FileAccess.Read);
- return new PEFile(fileName, stream, streamOptions, metadataOptions);
+ return LoadModuleFromFile(fileName);
}
catch (BadImageFormatException ex)
{
@@ -254,6 +253,19 @@ internal static (TargetFrameworkIdentifier, Version) ParseTargetFramework(string
return null;
}
+ ///
+ /// Loads the module stored at , once the file for a reference
+ /// has been found. Override to accept file formats other than a plain PE image. Throws
+ /// or for a file that is
+ /// not a loadable module; the caller turns that into null or a
+ /// according to the throwOnError setting.
+ ///
+ protected virtual MetadataFile LoadModuleFromFile(string fileName)
+ {
+ FileStream stream = new FileStream(fileName, FileMode.Open, FileAccess.Read);
+ return new PEFile(fileName, stream, streamOptions, metadataOptions);
+ }
+
public Task ResolveAsync(IAssemblyReference name)
{
return Task.Run(() => Resolve(name));
diff --git a/ICSharpCode.ILSpyCmd.Tests/XamarinCompressedInputTests.cs b/ICSharpCode.ILSpyCmd.Tests/XamarinCompressedInputTests.cs
new file mode 100644
index 0000000000..fe3acff157
--- /dev/null
+++ b/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
+{
+ ///
+ /// 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.
+ ///
+ [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);
+ }
+
+ ///
+ /// Writes the XALZ form of into the temp directory under
+ /// the same file name, so references resolve by name next to the input, and returns it.
+ ///
+ 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)"));
+ }
+
+ ///
+ /// 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.
+ ///
+ [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;
+ }
+ }
+}
diff --git a/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs b/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs
index 19974fbd8b..b311cc883a 100644
--- a/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs
+++ b/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs
@@ -557,16 +557,26 @@ PEFile LoadInputModule(string assemblyFileName, bool applyWinRTProjections = tru
return InputFileLoader.Load(assemblyFileName, BundleEntryName, applyWinRTProjections);
}
- CSharpDecompiler GetDecompiler(string assemblyFileName) => GetDecompiler(assemblyFileName, out _);
-
- CSharpDecompiler GetDecompiler(string assemblyFileName, out DecompilerSettings settings)
+ ///
+ /// Resolves the references of from next to the input file, the
+ /// -r directories, and the usual framework locations.
+ ///
+ UniversalAssemblyResolver CreateResolver(string assemblyFileName, PEFile module)
{
- var module = LoadInputModule(assemblyFileName);
- var resolver = new UniversalAssemblyResolver(assemblyFileName, false, module.Metadata.DetectTargetFrameworkId());
+ var resolver = new FileLoaderAssemblyResolver(assemblyFileName, module.Metadata.DetectTargetFrameworkId());
foreach (var path in (ReferencePaths ?? Array.Empty()))
{
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);
if (!settings.ApplyWindowsRuntimeProjections)
{
@@ -617,9 +627,7 @@ int ExtractResource(string assemblyFileName, string resourceName, TextWriter out
bool isBaml = resourceName.EndsWith(".baml", StringComparison.OrdinalIgnoreCase);
if (isBaml && value is byte[] bamlBytes)
{
- var resolver = new UniversalAssemblyResolver(assemblyFileName, false, module.Metadata.DetectTargetFrameworkId());
- foreach (var path in (ReferencePaths ?? Array.Empty()))
- resolver.AddSearchDirectory(path);
+ var resolver = CreateResolver(assemblyFileName, module);
var bamlSettings = new BamlDecompilerSettings {
ThrowOnAssemblyResolveErrors = GetSettings(module).ThrowOnAssemblyResolveErrors
};
@@ -788,11 +796,7 @@ int ExitCodeForDecompilationErrors()
ProjectId DecompileAsProject(string assemblyFileName, string projectFileName)
{
var module = LoadInputModule(assemblyFileName);
- var resolver = new UniversalAssemblyResolver(assemblyFileName, false, module.Metadata.DetectTargetFrameworkId());
- foreach (var path in (ReferencePaths ?? Array.Empty()))
- {
- resolver.AddSearchDirectory(path);
- }
+ var resolver = CreateResolver(assemblyFileName, module);
var settings = GetSettings(module);
var debugInfo = TryLoadPDB(module);
WholeProjectDecompiler decompiler;
diff --git a/ICSharpCode.ILSpyCmd/InputFileLoader.cs b/ICSharpCode.ILSpyCmd/InputFileLoader.cs
index 91be93d359..d95a189695 100644
--- a/ICSharpCode.ILSpyCmd/InputFileLoader.cs
+++ b/ICSharpCode.ILSpyCmd/InputFileLoader.cs
@@ -79,7 +79,7 @@ public static PEFile Load(string fileName, string entryName, bool applyWinRTProj
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);
foreach (var loader in loaders.RegisteredLoaders)
@@ -222,4 +222,25 @@ static MetadataReaderOptions MetadataOptions(FileLoadContext context)
: MetadataReaderOptions.None;
}
}
+
+ ///
+ /// 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.
+ ///
+ 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);
+ }
+ }
}