diff --git a/ICSharpCode.ILSpyCmd.Tests/BundleEntryOptionTests.cs b/ICSharpCode.ILSpyCmd.Tests/BundleEntryOptionTests.cs new file mode 100644 index 000000000..f9d30acf5 --- /dev/null +++ b/ICSharpCode.ILSpyCmd.Tests/BundleEntryOptionTests.cs @@ -0,0 +1,234 @@ +// Copyright (c) 2026 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.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; + +using ICSharpCode.Decompiler; + +using NUnit.Framework; + +using static ICSharpCode.ILSpyCmd.Tests.CliTestRunner; + +namespace ICSharpCode.ILSpyCmd.Tests +{ + /// + /// A single-file bundle is a host executable with a payload appended to it: the embedded + /// files, a manifest, and - as the last bytes of the file - the manifest offset followed by + /// the bundle signature. Only that trailer and the manifest are read to detect a bundle and + /// find its entries, so these tests build the payload around a real assembly and leave the + /// host stub empty, rather than publishing a self-contained app at test time. + /// + [TestFixture] + public class ILSpyCmdBundleEntryOptionTests + { + static readonly string testAssemblyPath = typeof(ILSpyCmdBundleEntryOptionTests).Assembly.Location; + + // The 32-byte bundle signature, as written by the .NET bundler. + static readonly byte[] bundleSignature = { + 0x8b, 0x12, 0x02, 0xb9, 0x6a, 0x61, 0x20, 0x38, + 0x72, 0x7b, 0x93, 0x02, 0x14, 0xd7, 0xa0, 0x32, + 0x13, 0xf5, 0xb9, 0xe6, 0xef, 0xae, 0x33, 0x18, + 0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae + }; + + string tempDirectory; + string bundlePath; + string bundleWithoutRuntimeConfigPath; + + [OneTimeSetUp] + public void CreateBundles() + { + tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(tempDirectory); + bundlePath = Path.Combine(tempDirectory, "SampleApp.exe"); + bundleWithoutRuntimeConfigPath = Path.Combine(tempDirectory, "SampleAppV1.exe"); + WriteBundle(bundlePath, withRuntimeConfig: true); + WriteBundle(bundleWithoutRuntimeConfigPath, withRuntimeConfig: false); + } + + [OneTimeTearDown] + public void DeleteBundles() + { + if (tempDirectory != null && Directory.Exists(tempDirectory)) + Directory.Delete(tempDirectory, recursive: true); + } + + /// + /// Writes a bundle containing two managed assemblies (both copies of this test assembly) + /// and, optionally, the runtime-config entry that identifies "Sample.dll" as the app. + /// + static void WriteBundle(string path, bool withRuntimeConfig) + { + byte[] assemblyBytes = File.ReadAllBytes(testAssemblyPath); + var entries = new List<(string Name, SingleFileBundle.FileType Type, long Offset, long Size)>(); + + using var stream = File.Create(path); + using var writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true); + + // Stand-in for the host executable the payload is appended to. Bundle detection only + // requires that the signature is not within the first eight bytes of the file. + writer.Write(new byte[64]); + + void WriteEntry(string name, SingleFileBundle.FileType type, byte[] contents) + { + long offset = stream.Position; + writer.Write(contents); + entries.Add((name, type, offset, contents.Length)); + } + + WriteEntry("Sample.dll", SingleFileBundle.FileType.Assembly, assemblyBytes); + WriteEntry("Helper.dll", SingleFileBundle.FileType.Assembly, assemblyBytes); + if (withRuntimeConfig) + { + WriteEntry("Sample.runtimeconfig.json", SingleFileBundle.FileType.RuntimeConfigJson, + Encoding.UTF8.GetBytes("{ \"runtimeOptions\": { } }")); + } + + long headerOffset = stream.Position; + writer.Write((uint)6); // MajorVersion: entries carry a compressed size + writer.Write((uint)0); // MinorVersion + writer.Write(entries.Count); + writer.Write("bundle-id"); + writer.Write(0L); // DepsJsonOffset + writer.Write(0L); // DepsJsonSize + writer.Write(0L); // RuntimeConfigJsonOffset + writer.Write(0L); // RuntimeConfigJsonSize + writer.Write(0UL); // Flags + foreach (var entry in entries) + { + writer.Write(entry.Offset); + writer.Write(entry.Size); + writer.Write(0L); // CompressedSize: entries are stored uncompressed + writer.Write((byte)entry.Type); + writer.Write(entry.Name); + } + + writer.Write(headerOffset); + writer.Write(bundleSignature); + } + + [Test] + public async Task BundleWithoutEntryListsManagedEntries() + { + var result = await RunAsync(bundlePath, "--disable-updatecheck"); + + Assert.That(result.ExitCode, Is.EqualTo(ProgramExitCodes.EX_USAGE)); + Assert.That(result.Error, Does.Contain("--bundle-entry")); + Assert.That(result.Error, Does.Contain("Sample.dll")); + Assert.That(result.Error, Does.Contain("Helper.dll")); + // The manifest entry that is not an assembly is not something to decompile. + Assert.That(result.Error, Does.Not.Contain("Sample.runtimeconfig.json")); + } + + [Test] + public async Task BundleWithoutEntryMarksEntryPoint() + { + var result = await RunAsync(bundlePath, "--disable-updatecheck"); + + Assert.That(result.ExitCode, Is.EqualTo(ProgramExitCodes.EX_USAGE)); + Assert.That(result.Error, Does.Contain("Sample.dll (entry point)")); + Assert.That(result.Error, Does.Not.Contain("Helper.dll (entry point)")); + } + + /// + /// Without a runtime-config entry the app assembly cannot be derived. The listing is + /// still the answer to the question asked, it just carries no annotation. + /// + [Test] + public async Task BundleWithoutRuntimeConfigListsEntriesUnmarked() + { + var result = await RunAsync(bundleWithoutRuntimeConfigPath, "--disable-updatecheck"); + + Assert.That(result.ExitCode, Is.EqualTo(ProgramExitCodes.EX_USAGE)); + Assert.That(result.Error, Does.Contain("Sample.dll")); + Assert.That(result.Error, Does.Contain("Helper.dll")); + Assert.That(result.Error, Does.Not.Contain("(entry point)")); + } + + [Test] + public async Task NamedEntryIsDecompiled() + { + var result = await RunAsync(bundlePath, "--disable-updatecheck", "--bundle-entry", "Sample.dll", + "-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)")); + } + + [Test] + public async Task EntryNameIsMatchedCaseInsensitively() + { + var result = await RunAsync(bundlePath, "--disable-updatecheck", "--bundle-entry", "sample.DLL", + "-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)")); + } + + /// + /// The five call sites that loaded the input file separately are the reason every mode + /// failed on a bundle; -il is one of the modes that never reaches the default path. + /// + [Test] + public async Task NamedEntryWorksInILMode() + { + var result = await RunAsync(bundlePath, "--disable-updatecheck", "--bundle-entry", "Sample.dll", "-il"); + + Assert.That(result.ExitCode, Is.EqualTo(0), result.Error); + Assert.That(result.Output, Does.Contain(".class")); + Assert.That(result.Output, Does.Contain("MemberOptionSample")); + } + + [Test] + public async Task UnknownEntryNameListsValidEntries() + { + var result = await RunAsync(bundlePath, "--disable-updatecheck", "--bundle-entry", "NotThere.dll"); + + Assert.That(result.ExitCode, Is.EqualTo(ProgramExitCodes.EX_DATAERR)); + Assert.That(result.Error, Does.Contain("NotThere.dll")); + Assert.That(result.Error, Does.Contain("Sample.dll")); + Assert.That(result.Error, Does.Contain("Helper.dll")); + } + + [Test] + public async Task DumpPackageStillWorksWithoutBundleEntry() + { + string outputDir = Path.Combine(tempDirectory, Path.GetRandomFileName()); + + var result = await RunAsync(bundlePath, "--disable-updatecheck", "-d", "-o", outputDir); + + Assert.That(result.ExitCode, Is.EqualTo(0), result.Error); + Assert.That(File.Exists(Path.Combine(outputDir, "Sample.dll")), Is.True); + Assert.That(File.Exists(Path.Combine(outputDir, "Helper.dll")), Is.True); + } + + [Test] + public async Task OrdinaryAssemblyIsUnaffected() + { + var result = await RunAsync(testAssemblyPath, "--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)")); + Assert.That(result.Error, Does.Not.Contain("--bundle-entry")); + } + } +} diff --git a/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs b/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs index 82c1208a8..19974fbd8 100644 --- a/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs +++ b/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs @@ -189,6 +189,9 @@ Examples: [Option("-d|--dump-package", "Dump package assemblies into a folder. This requires the output directory option.", CommandOptionType.NoValue)] public bool DumpPackageFlag { get; } + [Option("--bundle-entry ", "The assembly inside a single-file bundle (or other package) to work on, as printed when such a file is passed without this option. Ignored for input files that are not packages.", CommandOptionType.SingleValue)] + public string BundleEntryName { get; } + [Option("--nested-directories", "Use nested directories for namespaces.", CommandOptionType.NoValue)] public bool NestedDirectories { get; } @@ -315,6 +318,11 @@ Examples: return ExitCodeForDecompilationErrors(); } } + catch (PackageEntryRequiredException ex) + { + app.Error.WriteLine(ex.Message); + return ex.ExitCode; + } catch (Exception ex) { app.Error.WriteLine(ex.ToString()); @@ -417,6 +425,8 @@ Examples: return ProgramExitCodes.EX_USAGE; } + using var tableModule = LoadInputModule(fileName); + if (outputDirectory != null) { // per-file writer, disposed here: the shared 'output' is only closed once @@ -424,10 +434,10 @@ Examples: // but the last when dumping multiple assemblies string outputName = Path.GetFileNameWithoutExtension(fileName); using var tableOutput = File.CreateText(Path.Combine(outputDirectory, outputName) + $".{table}.{(JsonOutputFlag ? "json" : "txt")}"); - return MetadataTableDumper.DumpTable(fileName, tableOutput, table, JsonOutputFlag); + return MetadataTableDumper.DumpTable(tableModule, tableOutput, table, JsonOutputFlag); } - return MetadataTableDumper.DumpTable(fileName, output, table, JsonOutputFlag); + return MetadataTableDumper.DumpTable(tableModule, output, table, JsonOutputFlag); } else { @@ -538,18 +548,33 @@ Examples: return decompilerSettings; } + /// + /// Loads the module to work on. A package (single-file bundle, archive) is not an + /// assembly: the entry to use must be named with --bundle-entry. + /// + PEFile LoadInputModule(string assemblyFileName, bool applyWinRTProjections = true) + { + return InputFileLoader.Load(assemblyFileName, BundleEntryName, applyWinRTProjections); + } + CSharpDecompiler GetDecompiler(string assemblyFileName) => GetDecompiler(assemblyFileName, out _); CSharpDecompiler GetDecompiler(string assemblyFileName, out DecompilerSettings settings) { - var module = new PEFile(assemblyFileName); + var module = LoadInputModule(assemblyFileName); var resolver = new UniversalAssemblyResolver(assemblyFileName, false, module.Metadata.DetectTargetFrameworkId()); foreach (var path in (ReferencePaths ?? Array.Empty())) { resolver.AddSearchDirectory(path); } settings = GetSettings(module); - return new CSharpDecompiler(assemblyFileName, resolver, settings) { + if (!settings.ApplyWindowsRuntimeProjections) + { + // Whether the projections are wanted is only known once the settings have been + // read, which needs the module: load it again to get the metadata as stored. + module = LoadInputModule(assemblyFileName, applyWinRTProjections: false); + } + return new CSharpDecompiler(module, resolver, settings) { DebugInfoProvider = TryLoadPDB(module) }; } @@ -569,7 +594,7 @@ Examples: int ListResources(string assemblyFileName, TextWriter output) { - var module = new PEFile(assemblyFileName); + var module = LoadInputModule(assemblyFileName); foreach (var path in ResourceExtensions.EnumerateResourcePaths(module)) { output.WriteLine(path); @@ -579,7 +604,7 @@ Examples: int ExtractResource(string assemblyFileName, string resourceName, TextWriter output, string outputDirectory, CommandLineApplication app) { - var module = new PEFile(assemblyFileName); + var module = LoadInputModule(assemblyFileName); if (!ResourceExtensions.TryGetResource(module, resourceName, out object value)) { app.Error.WriteLine($"Resource '{resourceName}' not found."); @@ -648,7 +673,7 @@ Examples: int ShowIL(string assemblyFileName, TextWriter output) { - var module = new PEFile(assemblyFileName); + var module = LoadInputModule(assemblyFileName); output.WriteLine($"// IL code: {module.Name}"); var disassembler = new ReflectionDisassembler(new PlainTextOutput(output), CancellationToken.None) { DebugInfo = TryLoadPDB(module), @@ -762,7 +787,7 @@ Examples: ProjectId DecompileAsProject(string assemblyFileName, string projectFileName) { - var module = new PEFile(assemblyFileName); + var module = LoadInputModule(assemblyFileName); var resolver = new UniversalAssemblyResolver(assemblyFileName, false, module.Metadata.DetectTargetFrameworkId()); foreach (var path in (ReferencePaths ?? Array.Empty())) { @@ -1167,10 +1192,8 @@ Examples: int GeneratePdbForAssembly(string assemblyFileName, string pdbFileName, CommandLineApplication app) { - var module = new PEFile(assemblyFileName, - new FileStream(assemblyFileName, FileMode.Open, FileAccess.Read), - PEStreamOptions.PrefetchEntireImage, - metadataOptions: MetadataReaderOptions.None); + // PDB generation works on the metadata as it is stored, so no WinRT projections here. + var module = LoadInputModule(assemblyFileName, applyWinRTProjections: false); if (!PortablePdbWriter.HasCodeViewDebugDirectoryEntry(module)) { diff --git a/ICSharpCode.ILSpyCmd/InputFileLoader.cs b/ICSharpCode.ILSpyCmd/InputFileLoader.cs new file mode 100644 index 000000000..91be93d35 --- /dev/null +++ b/ICSharpCode.ILSpyCmd/InputFileLoader.cs @@ -0,0 +1,225 @@ +// Copyright (c) 2026 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; +using System.IO; +using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using System.Text; + +using ICSharpCode.Decompiler; +using ICSharpCode.Decompiler.Metadata; +using ICSharpCode.ILSpyX; +using ICSharpCode.ILSpyX.FileLoaders; + +namespace ICSharpCode.ILSpyCmd +{ + /// + /// Thrown when the input file is a package (a single-file bundle, an archive) and the entry + /// to work on was either not named or names nothing in the package. The message is the + /// complete text to print, including the entries to choose from. + /// + public sealed class PackageEntryRequiredException : Exception + { + public PackageEntryRequiredException(string message, int exitCode) + : base(message) + { + this.ExitCode = exitCode; + } + + public int ExitCode { get; } + } + + /// + /// Turns an input file name into the module to work on. Every mode of the tool loads its + /// input through here, so a package is recognized the same way in all of them. + /// + static class InputFileLoader + { + static readonly FileLoaderRegistry loaders = new FileLoaderRegistry(); + + /// + /// Loads , or the entry named by + /// if the file turns out to be a package. + /// + /// + /// The file is a package and is null or unknown. + /// + public static PEFile Load(string fileName, string entryName, bool applyWinRTProjections = true) + { + var context = new FileLoadContext(applyWinRTProjections, null); + var result = LoadFile(fileName, context); + if (result?.Package is { } package) + { + return LoadPackageEntry(package, entryName, context); + } + if (result?.MetadataFile is PEFile module) + { + return module; + } + // Not a file any loader recognized: let PEFile report what is wrong with it, which is + // the error the tool has always produced for such input. + return new PEFile(fileName, metadataOptions: MetadataOptions(context)); + } + + static LoadResult LoadFile(string fileName, FileLoadContext context) + { + using var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read); + foreach (var loader in loaders.RegisteredLoaders) + { + stream.Position = 0; + LoadResult result; + try + { + // The loaders are synchronous in fact; only their signature is not. + result = loader.Load(fileName, stream, context).GetAwaiter().GetResult(); + } + catch (Exception) + { + // A loader that chokes on a file it does not own says nothing about the file. + // If no other loader claims it either, the caller reloads it as a PE file and + // reports the failure from there. + continue; + } + if (result?.IsSuccess == true) + return result; + } + return null; + } + + static PEFile LoadPackageEntry(LoadedPackage package, string entryName, FileLoadContext context) + { + var managedEntries = GetManagedEntries(package); + string kind = package.Kind == LoadedPackage.PackageKind.Bundle ? "single-file bundle" : "package"; + + if (string.IsNullOrEmpty(entryName)) + { + throw new PackageEntryRequiredException( + BuildListing($"error: {kind}; name an entry with --bundle-entry .", package, managedEntries), + ProgramExitCodes.EX_USAGE); + } + + var entry = managedEntries.FirstOrDefault(e => string.Equals(entryName, e.Name, StringComparison.OrdinalIgnoreCase)); + if (entry == null) + { + throw new PackageEntryRequiredException( + BuildListing($"error: '{entryName}' is not a managed entry of this {kind}.", package, managedEntries), + ProgramExitCodes.EX_DATAERR); + } + + using var stream = entry.TryOpenStream(); + if (stream == null) + { + throw new PackageEntryRequiredException( + $"error: entry '{entry.Name}' could not be read from the {kind}.", + ProgramExitCodes.EX_DATAERR); + } + stream.Position = 0; + return new PEFile(entry.Name, stream, + PEStreamOptions.PrefetchEntireImage | PEStreamOptions.LeaveOpen, + MetadataOptions(context)); + } + + /// + /// The entries worth decompiling. A bundle manifest states the type of each entry; an + /// archive - and a bundle whose entries are all typed as unknown - is filtered by + /// extension instead. + /// + static IReadOnlyList GetManagedEntries(LoadedPackage package) + { + var assemblyNames = GetAssemblyEntryNames(package); + if (assemblyNames.Count > 0) + { + return package.Entries.Where(e => assemblyNames.Contains(e.Name)).ToList(); + } + return package.Entries + .Where(e => e.Name.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) + || e.Name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) + .ToList(); + } + + static HashSet GetAssemblyEntryNames(LoadedPackage package) + { + var names = new HashSet(StringComparer.OrdinalIgnoreCase); + var entries = package.BundleHeader.Entries; + if (!entries.IsDefaultOrEmpty) + { + foreach (var entry in entries) + { + if (entry.Type == SingleFileBundle.FileType.Assembly) + names.Add(entry.RelativePath); + } + } + return names; + } + + /// + /// The name of the application assembly, or null if it cannot be determined. The manifest + /// carries no entry-point marker, so it is derived the way the host does it: from the + /// runtime-config entry, whose name is the application name plus ".runtimeconfig.json". + /// + static string FindEntryPoint(LoadedPackage package, IReadOnlyList managedEntries) + { + const string suffix = ".runtimeconfig.json"; + var entries = package.BundleHeader.Entries; + if (entries.IsDefaultOrEmpty) + return null; + foreach (var entry in entries) + { + if (entry.Type != SingleFileBundle.FileType.RuntimeConfigJson) + continue; + if (!entry.RelativePath.EndsWith(suffix, StringComparison.OrdinalIgnoreCase)) + continue; + string candidate = entry.RelativePath[..^suffix.Length] + ".dll"; + var match = managedEntries.FirstOrDefault(e => string.Equals(candidate, e.Name, StringComparison.OrdinalIgnoreCase)); + if (match != null) + return match.Name; + } + return null; + } + + static string BuildListing(string message, LoadedPackage package, IReadOnlyList managedEntries) + { + var text = new StringBuilder(message); + if (managedEntries.Count == 0) + { + text.Append(" It contains no managed entries."); + return text.ToString(); + } + string entryPoint = FindEntryPoint(package, managedEntries); + text.AppendLine(" Managed entries:"); + foreach (var entry in managedEntries) + { + text.Append(" ").Append(entry.Name); + if (entry.Name == entryPoint) + text.Append(" (entry point)"); + text.AppendLine(); + } + return text.ToString().TrimEnd(); + } + + static MetadataReaderOptions MetadataOptions(FileLoadContext context) + { + return context.ApplyWinRTProjections + ? MetadataReaderOptions.ApplyWindowsRuntimeProjections + : MetadataReaderOptions.None; + } + } +} diff --git a/ICSharpCode.ILSpyCmd/MetadataTableDumper.cs b/ICSharpCode.ILSpyCmd/MetadataTableDumper.cs index 6e7666819..31cff0df6 100644 --- a/ICSharpCode.ILSpyCmd/MetadataTableDumper.cs +++ b/ICSharpCode.ILSpyCmd/MetadataTableDumper.cs @@ -71,14 +71,13 @@ namespace ICSharpCode.ILSpyCmd && supportedTables.Contains(table); } - public static int DumpTable(string assemblyFileName, TextWriter output, TableIndex table, bool asJson) + public static int DumpTable(PEFile module, TextWriter output, TableIndex table, bool asJson) { - using var module = new PEFile(assemblyFileName); var metadata = module.Metadata; var rows = LoadRows(metadata, table); if (asJson) { - WriteJson(output, assemblyFileName, table, rows); + WriteJson(output, module.FileName, table, rows); } else { diff --git a/ICSharpCode.ILSpyCmd/README.md b/ICSharpCode.ILSpyCmd/README.md index c821eb643..10dda270a 100644 --- a/ICSharpCode.ILSpyCmd/README.md +++ b/ICSharpCode.ILSpyCmd/README.md @@ -64,6 +64,9 @@ Options: --no-dead-stores Remove dead stores. -d|--dump-package Dump package assemblies into a folder. This requires the output directory option. + --bundle-entry The assembly inside a single-file bundle (or other package) to work on, as + printed when such a file is passed without this option. Ignored for input + files that are not packages. --nested-directories Use nested directories for namespaces. --disable-updatecheck If using ilspycmd in a tight loop or fully automated scenario, you might want to disable the automatic update check.