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.