From 65e30c0bd68924560e45d001caabe41235917057 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sun, 31 Mar 2024 10:33:56 +0200 Subject: [PATCH] Add IFileLoader API --- ICSharpCode.ILSpyX/AssemblyList.cs | 7 +- ICSharpCode.ILSpyX/AssemblyListManager.cs | 3 + ICSharpCode.ILSpyX/AssemblyListSnapshot.cs | 3 +- .../FileLoaders/ArchiveFileLoader.cs | 40 ++++ .../FileLoaders/BundleFileLoader.cs | 33 +++ .../FileLoaders/FileLoaderRegistry.cs | 49 +++++ ICSharpCode.ILSpyX/FileLoaders/LoadResult.cs | 40 ++++ .../FileLoaders/MetadataFileLoader.cs | 48 ++++ .../FileLoaders/WebCilFileLoader.cs | 40 ++++ .../XamarinCompressedFileLoader.cs | 73 +++++++ ICSharpCode.ILSpyX/LoadedAssembly.cs | 206 ++++++------------ ICSharpCode.ILSpyX/LoadedPackage.cs | 1 + ILSpy/MainWindow.xaml.cs | 15 ++ ILSpy/TreeNodes/AssemblyTreeNode.cs | 15 +- 14 files changed, 418 insertions(+), 155 deletions(-) create mode 100644 ICSharpCode.ILSpyX/FileLoaders/ArchiveFileLoader.cs create mode 100644 ICSharpCode.ILSpyX/FileLoaders/BundleFileLoader.cs create mode 100644 ICSharpCode.ILSpyX/FileLoaders/FileLoaderRegistry.cs create mode 100644 ICSharpCode.ILSpyX/FileLoaders/LoadResult.cs create mode 100644 ICSharpCode.ILSpyX/FileLoaders/MetadataFileLoader.cs create mode 100644 ICSharpCode.ILSpyX/FileLoaders/WebCilFileLoader.cs create mode 100644 ICSharpCode.ILSpyX/FileLoaders/XamarinCompressedFileLoader.cs diff --git a/ICSharpCode.ILSpyX/AssemblyList.cs b/ICSharpCode.ILSpyX/AssemblyList.cs index d7070498ba..b52e791173 100644 --- a/ICSharpCode.ILSpyX/AssemblyList.cs +++ b/ICSharpCode.ILSpyX/AssemblyList.cs @@ -30,6 +30,7 @@ using System.Xml.Linq; using ICSharpCode.ILSpyX.Extensions; +using ICSharpCode.ILSpyX.FileLoaders; namespace ICSharpCode.ILSpyX { @@ -128,6 +129,7 @@ public event NotifyCollectionChangedEventHandler CollectionChanged { public bool ApplyWinRTProjections { get; set; } public bool UseDebugSymbols { get; set; } + public FileLoaderRegistry LoaderRegistry => this.manager.LoaderRegistry; /// /// Gets the loaded assemblies. This method is thread-safe. @@ -279,7 +281,7 @@ public LoadedAssembly OpenAssembly(string file, bool isAutoLoaded = false) { file = Path.GetFullPath(file); return OpenAssembly(file, () => { - var newAsm = new LoadedAssembly(this, file, applyWinRTProjections: ApplyWinRTProjections, useDebugSymbols: UseDebugSymbols); + var newAsm = new LoadedAssembly(this, file, fileLoaders: manager?.LoaderRegistry, applyWinRTProjections: ApplyWinRTProjections, useDebugSymbols: UseDebugSymbols); newAsm.IsAutoLoaded = isAutoLoaded; return newAsm; }); @@ -293,6 +295,7 @@ public LoadedAssembly OpenAssembly(string file, Stream? stream, bool isAutoLoade file = Path.GetFullPath(file); return OpenAssembly(file, () => { var newAsm = new LoadedAssembly(this, file, stream: Task.FromResult(stream), + fileLoaders: manager?.LoaderRegistry, applyWinRTProjections: ApplyWinRTProjections, useDebugSymbols: UseDebugSymbols); newAsm.IsAutoLoaded = isAutoLoaded; return newAsm; @@ -345,6 +348,7 @@ LoadedAssembly OpenAssembly(string file, Func load) return null; var newAsm = new LoadedAssembly(this, file, stream: Task.FromResult(stream), + fileLoaders: manager?.LoaderRegistry, applyWinRTProjections: ApplyWinRTProjections, useDebugSymbols: UseDebugSymbols); newAsm.IsAutoLoaded = target.IsAutoLoaded; @@ -374,6 +378,7 @@ LoadedAssembly OpenAssembly(string file, Func load) if (index < 0) return null; var newAsm = new LoadedAssembly(this, target.FileName, pdbFileName: target.PdbFileName, + fileLoaders: manager?.LoaderRegistry, applyWinRTProjections: ApplyWinRTProjections, useDebugSymbols: UseDebugSymbols); newAsm.IsAutoLoaded = target.IsAutoLoaded; lock (lockObj) diff --git a/ICSharpCode.ILSpyX/AssemblyListManager.cs b/ICSharpCode.ILSpyX/AssemblyListManager.cs index 5791ee8d08..83f264f6d0 100644 --- a/ICSharpCode.ILSpyX/AssemblyListManager.cs +++ b/ICSharpCode.ILSpyX/AssemblyListManager.cs @@ -23,6 +23,7 @@ using System.Xml.Linq; using ICSharpCode.Decompiler.Metadata; +using ICSharpCode.ILSpyX.FileLoaders; using ICSharpCode.ILSpyX.Settings; namespace ICSharpCode.ILSpyX @@ -59,6 +60,8 @@ public AssemblyListManager(ISettingsProvider settingsProvider) public ObservableCollection AssemblyLists { get; } = new ObservableCollection(); + public FileLoaderRegistry LoaderRegistry { get; } = new FileLoaderRegistry(); + /// /// Loads an assembly list from the ILSpySettings. /// If no list with the specified name is found, the default list is loaded instead. diff --git a/ICSharpCode.ILSpyX/AssemblyListSnapshot.cs b/ICSharpCode.ILSpyX/AssemblyListSnapshot.cs index 7daf2d24c5..485a52a08b 100644 --- a/ICSharpCode.ILSpyX/AssemblyListSnapshot.cs +++ b/ICSharpCode.ILSpyX/AssemblyListSnapshot.cs @@ -27,6 +27,7 @@ using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.Util; using ICSharpCode.ILSpyX.Extensions; +using ICSharpCode.ILSpyX.FileLoaders; namespace ICSharpCode.ILSpyX { @@ -156,7 +157,7 @@ public async Task> GetAllAssembliesAsync() foreach (var asm in assemblies) { - LoadedAssembly.LoadResult result; + LoadResult result; try { result = await asm.GetLoadResultAsync().ConfigureAwait(false); diff --git a/ICSharpCode.ILSpyX/FileLoaders/ArchiveFileLoader.cs b/ICSharpCode.ILSpyX/FileLoaders/ArchiveFileLoader.cs new file mode 100644 index 0000000000..6d6cda73a2 --- /dev/null +++ b/ICSharpCode.ILSpyX/FileLoaders/ArchiveFileLoader.cs @@ -0,0 +1,40 @@ +// Copyright (c) 2024 Siegfried Pammer +// +// 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.IO; +using System.Threading.Tasks; + +namespace ICSharpCode.ILSpyX.FileLoaders +{ + public sealed class ArchiveFileLoader : IFileLoader + { + public Task Load(string fileName, Stream stream, FileLoadSettings settings) + { + try + { + var zip = LoadedPackage.FromZipFile(fileName); + var result = zip != null ? new LoadResult { Package = zip } : null; + return Task.FromResult(result); + } + catch (InvalidDataException) + { + return Task.FromResult(null); + } + } + } +} diff --git a/ICSharpCode.ILSpyX/FileLoaders/BundleFileLoader.cs b/ICSharpCode.ILSpyX/FileLoaders/BundleFileLoader.cs new file mode 100644 index 0000000000..4f0ec5bfef --- /dev/null +++ b/ICSharpCode.ILSpyX/FileLoaders/BundleFileLoader.cs @@ -0,0 +1,33 @@ +// Copyright (c) 2024 Siegfried Pammer +// +// 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.IO; +using System.Threading.Tasks; + +namespace ICSharpCode.ILSpyX.FileLoaders +{ + public sealed class BundleFileLoader : IFileLoader + { + public Task Load(string fileName, Stream stream, FileLoadSettings settings) + { + var bundle = LoadedPackage.FromBundle(fileName); + var result = bundle != null ? new LoadResult { Package = bundle } : null; + return Task.FromResult(result); + } + } +} diff --git a/ICSharpCode.ILSpyX/FileLoaders/FileLoaderRegistry.cs b/ICSharpCode.ILSpyX/FileLoaders/FileLoaderRegistry.cs new file mode 100644 index 0000000000..8117b4b940 --- /dev/null +++ b/ICSharpCode.ILSpyX/FileLoaders/FileLoaderRegistry.cs @@ -0,0 +1,49 @@ +// Copyright (c) 2024 Siegfried Pammer +// +// 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.Collections.Generic; + +namespace ICSharpCode.ILSpyX.FileLoaders +{ + public sealed class FileLoaderRegistry + { + readonly List registeredLoaders = new List(); + + public IReadOnlyList RegisteredLoaders => registeredLoaders; + + public void Register(IFileLoader loader) + { + if (loader is null) + { + throw new ArgumentNullException(nameof(loader)); + } + + registeredLoaders.Add(loader); + } + + public FileLoaderRegistry() + { + Register(new XamarinCompressedFileLoader()); + Register(new WebCilFileLoader()); + Register(new MetadataFileLoader()); + Register(new BundleFileLoader()); + Register(new ArchiveFileLoader()); + } + } +} diff --git a/ICSharpCode.ILSpyX/FileLoaders/LoadResult.cs b/ICSharpCode.ILSpyX/FileLoaders/LoadResult.cs new file mode 100644 index 0000000000..73c771a8d6 --- /dev/null +++ b/ICSharpCode.ILSpyX/FileLoaders/LoadResult.cs @@ -0,0 +1,40 @@ +// Copyright (c) 2024 Siegfried Pammer +// +// 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.Threading.Tasks; + +using ICSharpCode.Decompiler.Metadata; + +namespace ICSharpCode.ILSpyX.FileLoaders +{ + public sealed class LoadResult + { + public MetadataFile? MetadataFile { get; init; } + public Exception? FileLoadException { get; init; } + public LoadedPackage? Package { get; init; } + } + + public record FileLoadSettings(bool ApplyWinRTProjections); + + public interface IFileLoader + { + Task Load(string fileName, Stream stream, FileLoadSettings settings); + } +} diff --git a/ICSharpCode.ILSpyX/FileLoaders/MetadataFileLoader.cs b/ICSharpCode.ILSpyX/FileLoaders/MetadataFileLoader.cs new file mode 100644 index 0000000000..c7de1fb8dd --- /dev/null +++ b/ICSharpCode.ILSpyX/FileLoaders/MetadataFileLoader.cs @@ -0,0 +1,48 @@ +// Copyright (c) 2024 Siegfried Pammer +// +// 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.Reflection.Metadata; +using System.Threading.Tasks; + +using ICSharpCode.Decompiler.Metadata; + +using static ICSharpCode.Decompiler.Metadata.MetadataFile; + +namespace ICSharpCode.ILSpyX.FileLoaders +{ + public sealed class MetadataFileLoader : IFileLoader + { + public Task Load(string fileName, Stream stream, FileLoadSettings settings) + { + try + { + var kind = Path.GetExtension(fileName).Equals(".pdb", StringComparison.OrdinalIgnoreCase) + ? MetadataFileKind.ProgramDebugDatabase : MetadataFileKind.Metadata; + var metadata = MetadataReaderProvider.FromMetadataStream(stream, MetadataStreamOptions.PrefetchMetadata | MetadataStreamOptions.LeaveOpen); + var metadataFile = new MetadataFile(kind, fileName, metadata); + return Task.FromResult(new LoadResult { MetadataFile = metadataFile }); + } + catch (BadImageFormatException) + { + return Task.FromResult(null); + } + } + } +} diff --git a/ICSharpCode.ILSpyX/FileLoaders/WebCilFileLoader.cs b/ICSharpCode.ILSpyX/FileLoaders/WebCilFileLoader.cs new file mode 100644 index 0000000000..441ff10fbe --- /dev/null +++ b/ICSharpCode.ILSpyX/FileLoaders/WebCilFileLoader.cs @@ -0,0 +1,40 @@ +// Copyright (c) 2024 Siegfried Pammer +// +// 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.IO; +using System.Reflection.Metadata; +using System.Threading.Tasks; + +using ICSharpCode.Decompiler.Metadata; + +namespace ICSharpCode.ILSpyX.FileLoaders +{ + public sealed class WebCilFileLoader : IFileLoader + { + public Task Load(string fileName, Stream stream, FileLoadSettings settings) + { + MetadataReaderOptions options = settings.ApplyWinRTProjections + ? MetadataReaderOptions.ApplyWindowsRuntimeProjections + : MetadataReaderOptions.None; + + var wasm = WebCilFile.FromStream(fileName, options); + var result = wasm != null ? new LoadResult { MetadataFile = wasm } : null; + return Task.FromResult(result); + } + } +} diff --git a/ICSharpCode.ILSpyX/FileLoaders/XamarinCompressedFileLoader.cs b/ICSharpCode.ILSpyX/FileLoaders/XamarinCompressedFileLoader.cs new file mode 100644 index 0000000000..bce1c8918e --- /dev/null +++ b/ICSharpCode.ILSpyX/FileLoaders/XamarinCompressedFileLoader.cs @@ -0,0 +1,73 @@ +// Copyright (c) 2024 Siegfried Pammer +// +// 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.Buffers; +using System.IO; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using System.Text; +using System.Threading.Tasks; + +using ICSharpCode.Decompiler.Metadata; + +using K4os.Compression.LZ4; + +namespace ICSharpCode.ILSpyX.FileLoaders +{ + public sealed class XamarinCompressedFileLoader : IFileLoader + { + public async Task Load(string fileName, Stream stream, FileLoadSettings settings) + { + const uint CompressedDataMagic = 0x5A4C4158; // Magic used for Xamarin compressed module header ('XALZ', little-endian) + using var fileReader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true); + // Read compressed file header + var magic = fileReader.ReadUInt32(); + if (magic != CompressedDataMagic) + return null; + _ = fileReader.ReadUInt32(); // skip index into descriptor table, unused + int uncompressedLength = (int)fileReader.ReadUInt32(); + int compressedLength = (int)stream.Length; // Ensure we read all of compressed data + ArrayPool pool = ArrayPool.Shared; + var src = pool.Rent(compressedLength); + var dst = pool.Rent(uncompressedLength); + try + { + // fileReader stream position is now at compressed module data + await stream.ReadAsync(src, 0, compressedLength).ConfigureAwait(false); + // Decompress + LZ4Codec.Decode(src, 0, compressedLength, dst, 0, uncompressedLength); + // Load module from decompressed data buffer + using (var uncompressedStream = new MemoryStream(dst, writable: false)) + { + MetadataReaderOptions options = settings.ApplyWinRTProjections + ? MetadataReaderOptions.ApplyWindowsRuntimeProjections + : MetadataReaderOptions.None; + + return new LoadResult { + MetadataFile = new PEFile(fileName, stream, PEStreamOptions.PrefetchEntireImage, metadataOptions: options) + }; + } + } + finally + { + pool.Return(dst); + pool.Return(src); + } + } + } +} diff --git a/ICSharpCode.ILSpyX/LoadedAssembly.cs b/ICSharpCode.ILSpyX/LoadedAssembly.cs index 19509df007..cee8561412 100644 --- a/ICSharpCode.ILSpyX/LoadedAssembly.cs +++ b/ICSharpCode.ILSpyX/LoadedAssembly.cs @@ -17,7 +17,6 @@ // DEALINGS IN THE SOFTWARE. using System; -using System.Buffers; using System.Diagnostics; using System.IO; using System.Reflection.Metadata; @@ -31,12 +30,9 @@ using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.Decompiler.TypeSystem.Implementation; using ICSharpCode.Decompiler.Util; +using ICSharpCode.ILSpyX.FileLoaders; using ICSharpCode.ILSpyX.PdbProvider; -using K4os.Compression.LZ4; - -using static ICSharpCode.Decompiler.Metadata.MetadataFile; - #nullable enable @@ -65,47 +61,29 @@ public sealed class LoadedAssembly /// internal static readonly ConditionalWeakTable loadedAssemblies = new ConditionalWeakTable(); - public sealed class LoadResult - { - public MetadataFile? MetadataFile { get; } - public PEFile? PEFile => MetadataFile as PEFile; - public Exception? FileLoadException { get; } - public LoadedPackage? Package { get; } - - public LoadResult(PEFile peFile) - { - this.MetadataFile = peFile ?? throw new ArgumentNullException(nameof(peFile)); - } - public LoadResult(Exception fileLoadException, LoadedPackage package) - { - this.FileLoadException = fileLoadException ?? throw new ArgumentNullException(nameof(fileLoadException)); - this.Package = package ?? throw new ArgumentNullException(nameof(package)); - } - public LoadResult(Exception fileLoadException, MetadataFile metadataFile) - { - this.FileLoadException = fileLoadException ?? throw new ArgumentNullException(nameof(fileLoadException)); - this.MetadataFile = metadataFile ?? throw new ArgumentNullException(nameof(metadataFile)); - } - } - readonly Task loadingTask; readonly AssemblyList assemblyList; readonly string fileName; readonly string shortName; readonly IAssemblyResolver? providedAssemblyResolver; + readonly FileLoaderRegistry? fileLoaders; readonly bool applyWinRTProjections; readonly bool useDebugSymbols; public LoadedAssembly? ParentBundle { get; } public LoadedAssembly(AssemblyList assemblyList, string fileName, - Task? stream = null, IAssemblyResolver? assemblyResolver = null, string? pdbFileName = null, + Task? stream = null, + FileLoaderRegistry? fileLoaders = null, + IAssemblyResolver? assemblyResolver = null, + string? pdbFileName = null, bool applyWinRTProjections = false, bool useDebugSymbols = false) { this.assemblyList = assemblyList ?? throw new ArgumentNullException(nameof(assemblyList)); this.fileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); this.PdbFileName = pdbFileName; this.providedAssemblyResolver = assemblyResolver; + this.fileLoaders = fileLoaders; this.applyWinRTProjections = applyWinRTProjections; this.useDebugSymbols = useDebugSymbols; @@ -114,9 +92,10 @@ public LoadedAssembly(AssemblyList assemblyList, string fileName, } public LoadedAssembly(LoadedAssembly bundle, string fileName, Task? stream, + FileLoaderRegistry? fileLoaders = null, IAssemblyResolver? assemblyResolver = null, bool applyWinRTProjections = false, bool useDebugSymbols = false) - : this(bundle.assemblyList, fileName, stream, assemblyResolver, null, + : this(bundle.assemblyList, fileName, stream, fileLoaders, assemblyResolver, null, applyWinRTProjections, useDebugSymbols) { this.ParentBundle = bundle; @@ -180,7 +159,7 @@ public async Task GetMetadataFileAsync() if (loadResult.MetadataFile != null) return loadResult.MetadataFile; else - throw loadResult.FileLoadException!; + throw loadResult.FileLoadException ?? new MetadataFileNotSupportedException(); } /// @@ -334,145 +313,84 @@ public bool IsLoadedAsValidAssembly { async Task LoadAsync(Task? streamTask) { - // runs on background thread - var stream = streamTask != null ? await streamTask.ConfigureAwait(false) : null; - if (stream != null) + using var stream = await PrepareStream(); + FileLoadSettings settings = new FileLoadSettings(applyWinRTProjections); + + LoadResult? result = null; + + if (fileLoaders != null) { - // Read the module from a precrafted stream - if (!stream.CanSeek) + foreach (var loader in fileLoaders.RegisteredLoaders) { - var memoryStream = new MemoryStream(); - stream.CopyTo(memoryStream); - stream.Close(); - memoryStream.Position = 0; - stream = memoryStream; + stream.Position = 0; + result = await loader.Load(fileName, stream, settings).ConfigureAwait(false); + if (result != null) + { + break; + } } - var streamOptions = stream is MemoryStream ? PEStreamOptions.PrefetchEntireImage : PEStreamOptions.Default; - return LoadAssembly(stream, streamOptions, applyWinRTProjections); } - // Read the module from disk - Exception loadAssemblyException; - try + + if (result == null) { - using (var fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read)) + stream.Position = 0; + try { - return LoadAssembly(fileStream, PEStreamOptions.PrefetchEntireImage, applyWinRTProjections); + MetadataReaderOptions options = applyWinRTProjections + ? MetadataReaderOptions.ApplyWindowsRuntimeProjections + : MetadataReaderOptions.None; + + PEFile module = new PEFile(fileName, stream, PEStreamOptions.PrefetchEntireImage, metadataOptions: options); + result = new LoadResult { MetadataFile = module }; + } + catch (Exception ex) + { + result = new LoadResult { FileLoadException = ex }; } } - catch (MetadataFileNotSupportedException ex) - { - loadAssemblyException = ex; - } - catch (BadImageFormatException ex) - { - loadAssemblyException = ex; - } - // Maybe its a compressed Xamarin/Mono assembly, see https://github.com/xamarin/xamarin-android/pull/4686 - try - { - return LoadCompressedAssembly(fileName); - } - catch (InvalidDataException) - { - // Not a compressed module, try other options below - } - // If it's not a .NET module, maybe it's a single-file bundle - var bundle = LoadedPackage.FromBundle(fileName); - if (bundle != null) - { - bundle.LoadedAssembly = this; - return new LoadResult(loadAssemblyException, bundle); - } - // If it's not a .NET module, maybe it's a WASM module - var wasm = WebCilFile.FromStream(fileName); - if (wasm != null) + + if (result.MetadataFile != null) { lock (loadedAssemblies) { - loadedAssemblies.Add(wasm, this); + loadedAssemblies.Add(result.MetadataFile, this); } - return new LoadResult(loadAssemblyException, wasm); - } - // If it's not a .NET module, maybe it's a zip archive (e.g. .nupkg) - try - { - var zip = LoadedPackage.FromZipFile(fileName); - zip.LoadedAssembly = this; - return new LoadResult(loadAssemblyException, zip); - } - catch (InvalidDataException) - { - // Not a compressed module, try other options below - } - // or it could be a standalone portable PDB - try - { - using (var fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read)) + + if (result.MetadataFile is PEFile module) { - var kind = Path.GetExtension(fileName).Equals(".pdb", StringComparison.OrdinalIgnoreCase) ? MetadataFileKind.ProgramDebugDatabase : MetadataFileKind.Metadata; - var metadata = MetadataReaderProvider.FromMetadataStream(fileStream, MetadataStreamOptions.PrefetchMetadata); - var metadataFile = new MetadataFile(kind, fileName, metadata); - lock (loadedAssemblies) - { - loadedAssemblies.Add(metadataFile, this); - } - return new LoadResult(loadAssemblyException, metadataFile); + debugInfoProvider = LoadDebugInfo(module); } } - catch (Exception) + else if (result.Package != null) { - throw loadAssemblyException; + result.Package.LoadedAssembly = this; } - } - - LoadResult LoadAssembly(Stream stream, PEStreamOptions streamOptions, bool applyWinRTProjections) - { - MetadataReaderOptions options = applyWinRTProjections - ? MetadataReaderOptions.ApplyWindowsRuntimeProjections - : MetadataReaderOptions.None; - - PEFile module = new PEFile(fileName, stream, streamOptions, metadataOptions: options); - - debugInfoProvider = LoadDebugInfo(module); - lock (loadedAssemblies) + else if (result.FileLoadException != null) { - loadedAssemblies.Add(module, this); + throw result.FileLoadException; } - return new LoadResult(module); - } + return result; - LoadResult LoadCompressedAssembly(string fileName) - { - const uint CompressedDataMagic = 0x5A4C4158; // Magic used for Xamarin compressed module header ('XALZ', little-endian) - using (var fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read)) - using (var fileReader = new BinaryReader(fileStream)) + async Task PrepareStream() { - // Read compressed file header - var magic = fileReader.ReadUInt32(); - if (magic != CompressedDataMagic) - throw new InvalidDataException($"Xamarin compressed module header magic {magic} does not match expected {CompressedDataMagic}"); - _ = fileReader.ReadUInt32(); // skip index into descriptor table, unused - int uncompressedLength = (int)fileReader.ReadUInt32(); - int compressedLength = (int)fileStream.Length; // Ensure we read all of compressed data - ArrayPool pool = ArrayPool.Shared; - var src = pool.Rent(compressedLength); - var dst = pool.Rent(uncompressedLength); - try + // runs on background thread + var stream = streamTask != null ? await streamTask.ConfigureAwait(false) : null; + if (stream != null) { - // fileReader stream position is now at compressed module data - fileStream.Read(src, 0, compressedLength); - // Decompress - LZ4Codec.Decode(src, 0, compressedLength, dst, 0, uncompressedLength); - // Load module from decompressed data buffer - using (var uncompressedStream = new MemoryStream(dst, writable: false)) + // Read the module from a precrafted stream + if (!stream.CanSeek) { - return LoadAssembly(uncompressedStream, PEStreamOptions.PrefetchEntireImage, applyWinRTProjections); + var memoryStream = new MemoryStream(); + stream.CopyTo(memoryStream); + stream.Close(); + memoryStream.Position = 0; + stream = memoryStream; } + return stream; } - finally + else { - pool.Return(dst); - pool.Return(src); + return new FileStream(fileName, FileMode.Open, FileAccess.Read); } } } diff --git a/ICSharpCode.ILSpyX/LoadedPackage.cs b/ICSharpCode.ILSpyX/LoadedPackage.cs index d434d08273..47e80b962d 100644 --- a/ICSharpCode.ILSpyX/LoadedPackage.cs +++ b/ICSharpCode.ILSpyX/LoadedPackage.cs @@ -339,6 +339,7 @@ internal PackageFolder(LoadedPackage package, PackageFolder? parent, string name { asm = new LoadedAssembly( package.LoadedAssembly, entry.Name, + fileLoaders: package.LoadedAssembly.AssemblyList.LoaderRegistry, assemblyResolver: this, stream: Task.Run(entry.TryOpenStream), applyWinRTProjections: package.LoadedAssembly.AssemblyList.ApplyWinRTProjections, diff --git a/ILSpy/MainWindow.xaml.cs b/ILSpy/MainWindow.xaml.cs index 4f21809535..999abdca36 100644 --- a/ILSpy/MainWindow.xaml.cs +++ b/ILSpy/MainWindow.xaml.cs @@ -54,6 +54,7 @@ using ICSharpCode.ILSpy.Updates; using ICSharpCode.ILSpy.ViewModels; using ICSharpCode.ILSpyX; +using ICSharpCode.ILSpyX.FileLoaders; using ICSharpCode.ILSpyX.Settings; using ICSharpCode.TreeView; @@ -152,6 +153,7 @@ public MainWindow() InitMainMenu(); InitWindowMenu(); InitToolbar(); + InitFileLoaders(); ContextMenuProvider.Add(AssemblyTreeView); this.Loaded += MainWindow_Loaded; @@ -579,6 +581,19 @@ public void ShowInBottomPane(string title, object content) } #endregion + #region File Loader extensibility + + void InitFileLoaders() + { + // TODO + foreach (var loader in App.ExportProvider.GetExportedValues()) + { + + } + } + + #endregion + #region Message Hook protected override void OnSourceInitialized(EventArgs e) { diff --git a/ILSpy/TreeNodes/AssemblyTreeNode.cs b/ILSpy/TreeNodes/AssemblyTreeNode.cs index 04b179a3dc..aec26fcc08 100644 --- a/ILSpy/TreeNodes/AssemblyTreeNode.cs +++ b/ILSpy/TreeNodes/AssemblyTreeNode.cs @@ -33,6 +33,7 @@ using ICSharpCode.ILSpy.Properties; using ICSharpCode.ILSpy.ViewModels; using ICSharpCode.ILSpyX; +using ICSharpCode.ILSpyX.FileLoaders; using ICSharpCode.ILSpyX.PdbProvider; using ICSharpCode.TreeView; @@ -188,7 +189,7 @@ async void Init() protected override void LoadChildren() { - LoadedAssembly.LoadResult loadResult; + LoadResult loadResult; try { loadResult = LoadedAssembly.GetLoadResultAsync().GetAwaiter().GetResult(); @@ -205,7 +206,7 @@ protected override void LoadChildren() switch (loadResult.MetadataFile.Kind) { case MetadataFile.MetadataFileKind.PortableExecutable: - LoadChildrenForPEFile(loadResult.PEFile); + LoadChildrenForPEFile(loadResult.MetadataFile); break; case MetadataFile.MetadataFileKind.WebCIL: LoadChildrenForWebCilFile((WebCilFile)loadResult.MetadataFile); @@ -462,7 +463,7 @@ void HandleException(Exception ex, string message) try { var loadResult = LoadedAssembly.GetLoadResultAsync().GetAwaiter().GetResult(); - if (loadResult.PEFile != null) + if (loadResult.MetadataFile != null) { language.DecompileAssembly(LoadedAssembly, output, options); } @@ -471,13 +472,9 @@ void HandleException(Exception ex, string message) output.WriteLine("// " + LoadedAssembly.FileName); DecompilePackage(loadResult.Package, output); } - else if (loadResult.MetadataFile != null) - { - output.WriteLine("// " + LoadedAssembly.FileName); - } - else + else if (loadResult.FileLoadException != null) { - LoadedAssembly.GetMetadataFileOrNullAsync().GetAwaiter().GetResult(); + HandleException(loadResult.FileLoadException, loadResult.FileLoadException.Message); } } catch (BadImageFormatException badImage)