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