Browse Source

Fix #3154: accept single-file bundles as ilspycmd input

Every mode loaded the input file with its own `new PEFile(fileName)` call, so a
bundle - which the UI opens fine, because LoadedAssembly goes through the file
loader registry - failed identically in all of them. The five call sites now
share one load helper that runs the same registry, which also makes archives
work and keeps one detection point for both front ends.

Which assembly inside a bundle is meant is not something to guess, so a package
without --bundle-entry lists its managed entries and exits non-zero. The entry
point is only annotated in that listing: the manifest has no marker for it, so
it is derived the way the host does it, from the runtime-config entry's name,
and stays unannotated where that does not resolve.

Assisted-by: Claude:claude-opus-5:Claude Code
pull/4101/head
Siegfried Pammer 2 weeks ago
parent
commit
71ac2b9ca9
  1. 234
      ICSharpCode.ILSpyCmd.Tests/BundleEntryOptionTests.cs
  2. 47
      ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs
  3. 225
      ICSharpCode.ILSpyCmd/InputFileLoader.cs
  4. 5
      ICSharpCode.ILSpyCmd/MetadataTableDumper.cs
  5. 3
      ICSharpCode.ILSpyCmd/README.md

234
ICSharpCode.ILSpyCmd.Tests/BundleEntryOptionTests.cs

@ -0,0 +1,234 @@ @@ -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
{
/// <summary>
/// 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.
/// </summary>
[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);
}
/// <summary>
/// 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.
/// </summary>
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)"));
}
/// <summary>
/// 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.
/// </summary>
[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)"));
}
/// <summary>
/// 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.
/// </summary>
[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"));
}
}
}

47
ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs

@ -189,6 +189,9 @@ Examples: @@ -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 <name>", "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: @@ -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: @@ -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: @@ -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: @@ -538,18 +548,33 @@ Examples:
return decompilerSettings;
}
/// <summary>
/// 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.
/// </summary>
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<string>()))
{
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: @@ -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: @@ -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: @@ -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: @@ -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<string>()))
{
@ -1167,10 +1192,8 @@ Examples: @@ -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))
{

225
ICSharpCode.ILSpyCmd/InputFileLoader.cs

@ -0,0 +1,225 @@ @@ -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
{
/// <summary>
/// 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.
/// </summary>
public sealed class PackageEntryRequiredException : Exception
{
public PackageEntryRequiredException(string message, int exitCode)
: base(message)
{
this.ExitCode = exitCode;
}
public int ExitCode { get; }
}
/// <summary>
/// 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.
/// </summary>
static class InputFileLoader
{
static readonly FileLoaderRegistry loaders = new FileLoaderRegistry();
/// <summary>
/// Loads <paramref name="fileName"/>, or the entry named by <paramref name="entryName"/>
/// if the file turns out to be a package.
/// </summary>
/// <exception cref="PackageEntryRequiredException">
/// The file is a package and <paramref name="entryName"/> is null or unknown.
/// </exception>
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 <name>.", 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));
}
/// <summary>
/// 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.
/// </summary>
static IReadOnlyList<PackageEntry> 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<string> GetAssemblyEntryNames(LoadedPackage package)
{
var names = new HashSet<string>(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;
}
/// <summary>
/// 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".
/// </summary>
static string FindEntryPoint(LoadedPackage package, IReadOnlyList<PackageEntry> 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<PackageEntry> 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;
}
}
}

5
ICSharpCode.ILSpyCmd/MetadataTableDumper.cs

@ -71,14 +71,13 @@ namespace ICSharpCode.ILSpyCmd @@ -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
{

3
ICSharpCode.ILSpyCmd/README.md

@ -64,6 +64,9 @@ Options: @@ -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 <name> 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.

Loading…
Cancel
Save