mirror of https://github.com/icsharpcode/ILSpy.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
225 lines
8.0 KiB
225 lines
8.0 KiB
// 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; |
|
} |
|
} |
|
}
|
|
|