Browse Source

Merge pull request #4101 from icsharpcode/fix/3154-single-file-bundle-input

Fix #3154: accept single-file bundles as ilspycmd input
pull/4104/head
Siegfried Pammer 2 weeks ago committed by GitHub
parent
commit
3ef547ca2c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  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