.NET Decompiler with support for PDB generation, ReadyToRun, Metadata (&more) - cross-platform!
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.
 
 
 

470 lines
16 KiB

// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
//
// 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.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Documents;
using Avalonia.Media;
using ICSharpCode.Decompiler;
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.ILSpyX;
using ICSharpCode.ILSpyX.FileLoaders;
using ICSharpCode.ILSpyX.PdbProvider;
using ILSpy;
using ILSpy.Languages;
namespace ILSpy.TreeNodes
{
sealed class AssemblyTreeNode : ILSpyTreeNode
{
readonly LoadedAssembly assembly;
string? loadError;
MetadataFile? cachedModule;
public LoadedAssembly LoadedAssembly => assembly;
/// <summary>
/// When this node represents a .dll/.exe entry inside a <see cref="LoadedPackage"/>
/// (zip or .NET bundle), this is the package entry it was created from. Null for
/// stand-alone assemblies.
/// </summary>
public PackageEntry? PackageEntry { get; }
public AssemblyTreeNode(LoadedAssembly assembly) : this(assembly, null)
{
}
internal AssemblyTreeNode(LoadedAssembly assembly, PackageEntry? packageEntry)
{
ArgumentNullException.ThrowIfNull(assembly);
this.assembly = assembly;
this.PackageEntry = packageEntry;
LazyLoading = true;
// Observe the load OUTCOME without triggering it — the cooldown sweep and
// explicit user actions (path-restore, expand) are what start the load.
// If we called GetLoadResultAsync here we'd flatten the lazy strategy and
// every assembly in the list would load the moment its tree node is built.
assembly.Loaded += OnAssemblyLoaded;
if (assembly.IsLoaded)
OnAssemblyLoaded(); // already finished before we subscribed — catch up
}
void OnAssemblyLoaded()
{
// The Loaded event fires from a thread-pool ContinueWith. Marshal to the UI
// thread before mutating tree-node state and raising change notifications;
// SharpTreeNode's RaisePropertyChanged is not thread-safe.
global::Avalonia.Threading.Dispatcher.UIThread.Post(InitFromLoadResult,
global::Avalonia.Threading.DispatcherPriority.Background);
}
void InitFromLoadResult()
{
try
{
// GetLoadResultAsync is a no-op now — the load is already complete.
var loadResult = assembly.GetLoadResultAsync().GetAwaiter().GetResult();
cachedModule = loadResult.MetadataFile;
if (cachedModule == null && loadResult.Package == null)
{
loadError = File.Exists(assembly.FileName)
? $"Failed to load '{assembly.FileName}'."
: $"File not found:\n{assembly.FileName}";
}
}
catch (Exception ex)
{
loadError = $"Failed to load '{assembly.FileName}':\n{ex.GetBaseException().Message}";
}
RaisePropertyChanged(nameof(Text));
RaisePropertyChanged(nameof(Icon));
RaisePropertyChanged(nameof(ToolTip));
RaisePropertyChanged(nameof(ShowExpander));
}
// Assemblies nested in NuGet packages can't be unloaded individually — the parent
// package entry owns them.
public override bool CanDelete() => PackageEntry == null;
public override void Delete() => DeleteCore();
public override void DeleteCore() => assembly.AssemblyList.Unload(assembly);
public override object Text => assembly.Text;
// ToString is the stable identity used by SessionSettings.ActiveTreeViewPath — must not
// depend on the active language. The full file path uniquely identifies the assembly.
public override string ToString() => assembly.FileName;
public override object Icon {
get {
if (assembly.HasLoadError || loadError != null)
return Images.Images.AssemblyWarning;
if (!assembly.IsLoaded)
return Images.Images.AssemblyLoading;
var loadResult = assembly.GetLoadResultAsync().GetAwaiter().GetResult();
if (loadResult.Package != null)
{
return loadResult.Package.Kind switch {
LoadedPackage.PackageKind.Zip => Images.Images.NuGet,
_ => Images.Images.Library,
};
}
if (loadResult.MetadataFile != null)
{
return loadResult.MetadataFile.Kind switch {
MetadataFile.MetadataFileKind.PortableExecutable => Images.Images.Assembly,
MetadataFile.MetadataFileKind.ProgramDebugDatabase => Images.Images.ProgramDebugDatabase,
MetadataFile.MetadataFileKind.WebCIL => Images.Images.WebAssemblyFile,
_ => Images.Images.MetadataFile,
};
}
return Images.Images.Assembly;
}
}
public override object? ToolTip {
get {
if (assembly.HasLoadError || loadError != null)
return loadError ?? "Assembly could not be loaded.";
if (!assembly.IsLoaded || cachedModule == null)
return assembly.FileName;
var tb = new TextBlock { TextWrapping = TextWrapping.Wrap };
var metadata = cachedModule.Metadata;
if (metadata?.IsAssembly == true && metadata.TryGetFullAssemblyName(out var assemblyName))
{
tb.Inlines!.Add(BoldRun("Name: "));
tb.Inlines.Add(new Run(assemblyName));
tb.Inlines.Add(new LineBreak());
}
tb.Inlines!.Add(BoldRun("Location: "));
tb.Inlines.Add(new Run(assembly.FileName));
if (cachedModule is PEFile peFile)
{
tb.Inlines.Add(new LineBreak());
tb.Inlines.Add(BoldRun("Architecture: "));
tb.Inlines.Add(new Run(GetPlatformDisplayName(peFile)));
}
string? runtime = GetRuntimeDisplayName(cachedModule);
if (runtime != null)
{
tb.Inlines.Add(new LineBreak());
tb.Inlines.Add(BoldRun("Runtime: "));
tb.Inlines.Add(new Run(runtime));
}
return tb;
}
}
public override bool ShowExpander => !assembly.HasLoadError && loadError == null && base.ShowExpander;
public override bool IsAutoLoaded => assembly.IsAutoLoaded;
/// <summary>
/// Finds the <see cref="NamespaceTreeNode"/> for the given namespace string, or
/// <c>null</c> if no children are loaded yet or the namespace has no top-level types
/// in this assembly.
/// </summary>
public NamespaceTreeNode? FindNamespaceNode(string namespaceName)
{
ArgumentNullException.ThrowIfNull(namespaceName);
EnsureLazyChildren();
return Children.OfType<NamespaceTreeNode>().FirstOrDefault(ns => ns.Name == namespaceName);
}
/// <summary>
/// Finds the <see cref="TypeTreeNode"/> for the given top-level type definition.
/// Walks the assembly's namespaces (loading them as needed) and matches by
/// <see cref="TypeTreeNode.Handle"/>.
/// </summary>
public TypeTreeNode? FindTypeNode(ITypeDefinition type)
{
ArgumentNullException.ThrowIfNull(type);
var ns = FindNamespaceNode(type.Namespace);
if (ns == null)
return null;
ns.EnsureLazyChildren();
var handle = (System.Reflection.Metadata.TypeDefinitionHandle)type.MetadataToken;
return ns.Children.OfType<TypeTreeNode>().FirstOrDefault(n => n.Handle == handle);
}
protected override void LoadChildren()
{
LoadResult loadResult;
try
{
loadResult = assembly.GetLoadResultAsync().GetAwaiter().GetResult();
}
catch
{
return;
}
// Zip / .NET-bundle packages: surface their folder structure instead of trying to
// decompile the package itself.
if (loadResult.Package != null)
{
foreach (var child in PackageFolderTreeNode.LoadChildrenForFolder(loadResult.Package.RootFolder))
Children.Add(child);
return;
}
var module = loadResult.MetadataFile ?? cachedModule;
if (module == null)
return;
if (module.Kind != MetadataFile.MetadataFileKind.PortableExecutable
&& module.Kind != MetadataFile.MetadataFileKind.WebCIL)
{
return;
}
Children.Add(new ILSpy.Metadata.MetadataTreeNode(module, ICSharpCode.ILSpy.Properties.Resources.Metadata));
// Surface portable PDB metadata (embedded or side-by-side) as a top-level sibling
// of the host Metadata folder so PDB browsing is one click away instead of buried
// under Metadata → Debug Directory. Reuses the cached PdbProvider that the rest of
// the app already opened for decompilation — no second parse of the PDB blob.
var debugInfo = assembly.GetDebugInfoOrNull();
if (debugInfo is PortableDebugInfoProvider ppdb
&& ppdb.GetMetadataReader() is not null)
{
var label = $"Debug Metadata ({(ppdb.IsEmbedded ? "Embedded" : "From portable PDB")})";
Children.Add(new ILSpy.Metadata.MetadataTreeNode(ppdb.ToMetadataFile(), label));
}
Children.Add(new ReferenceFolderTreeNode(module, this));
if (module.Resources.Any())
Children.Add(new ResourceListTreeNode(module));
var metadata = module.Metadata;
var namespaces = metadata.TypeDefinitions
.Where(t => metadata.GetTypeDefinition(t).GetDeclaringType().IsNil)
.Select(t => metadata.GetString(metadata.GetTypeDefinition(t).Namespace))
.Where(ns => !string.IsNullOrEmpty(ns))
.Distinct()
.OrderBy(ns => ns, NaturalStringComparer.Instance);
foreach (var ns in namespaces)
Children.Add(new NamespaceTreeNode(ns, module));
var globalTypes = metadata.TypeDefinitions
.Where(t => {
var td = metadata.GetTypeDefinition(t);
return td.GetDeclaringType().IsNil
&& string.IsNullOrEmpty(metadata.GetString(td.Namespace));
});
foreach (var t in globalTypes)
Children.Add(new TypeTreeNode(t, module));
}
public override FilterResult Filter(LanguageSettings settings)
{
if (settings.SearchTermMatches(LoadedAssembly.ShortName))
return FilterResult.Match;
else
return FilterResult.Recurse;
}
public override void Decompile(Language language, ITextOutput output, DecompilationOptions options)
{
void HandleException(Exception ex, string message)
{
language.WriteCommentLine(output, message);
output.WriteLine();
output.MarkFoldStart("Exception details", true);
output.Write(ex.ToString());
output.MarkFoldEnd();
}
try
{
var loadResult = assembly.GetLoadResultAsync().GetAwaiter().GetResult();
if (loadResult.MetadataFile != null)
{
switch (loadResult.MetadataFile.Kind)
{
case MetadataFile.MetadataFileKind.ProgramDebugDatabase:
case MetadataFile.MetadataFileKind.Metadata:
output.WriteLine("// " + assembly.FileName);
break;
default:
language.DecompileAssembly(assembly, output, options);
break;
}
}
else if (loadResult.Package != null)
{
output.WriteLine("// " + assembly.FileName);
DecompilePackage(loadResult.Package, output);
}
else if (loadResult.FileLoadException != null)
{
HandleException(loadResult.FileLoadException, loadResult.FileLoadException.Message);
}
}
catch (BadImageFormatException badImage)
{
HandleException(badImage, "This file does not contain a managed assembly.");
}
catch (FileNotFoundException fileNotFound) when (options.SaveAsProjectDirectory == null)
{
HandleException(fileNotFound, "The file was not found.");
}
catch (DirectoryNotFoundException dirNotFound) when (options.SaveAsProjectDirectory == null)
{
HandleException(dirNotFound, "The directory was not found.");
}
catch (MetadataFileNotSupportedException notSupported)
{
HandleException(notSupported, notSupported.Message);
}
}
static void DecompilePackage(LoadedPackage package, ITextOutput output)
{
switch (package.Kind)
{
case LoadedPackage.PackageKind.Zip:
output.WriteLine("// File format: .zip file");
break;
case LoadedPackage.PackageKind.Bundle:
var header = package.BundleHeader;
output.WriteLine($"// File format: .NET bundle {header.MajorVersion}.{header.MinorVersion}");
break;
}
output.WriteLine();
output.WriteLine("Entries:");
foreach (var entry in package.Entries)
{
output.WriteLine($" {entry.Name} ({entry.TryGetLength()} bytes)");
}
}
static Run BoldRun(string text) => new(text) { FontWeight = FontWeight.Bold };
static string GetPlatformDisplayName(PEFile file)
{
return file.Reader.PEHeaders.CoffHeader.Machine switch {
System.Reflection.PortableExecutable.Machine.I386 => "x86",
System.Reflection.PortableExecutable.Machine.Amd64 => "x64",
System.Reflection.PortableExecutable.Machine.IA64 => "Itanium",
System.Reflection.PortableExecutable.Machine.Arm => "ARM",
System.Reflection.PortableExecutable.Machine.Arm64 => "ARM64",
_ => file.Reader.PEHeaders.CoffHeader.Machine.ToString(),
};
}
static string? GetRuntimeDisplayName(MetadataFile module)
{
return module.Metadata.MetadataKind switch {
System.Reflection.Metadata.MetadataKind.Ecma335 => module.Metadata.MetadataVersion,
System.Reflection.Metadata.MetadataKind.WindowsMetadata => "WinRT",
System.Reflection.Metadata.MetadataKind.ManagedWindowsMetadata => "Managed WinRT",
_ => null,
};
}
// Right-click → "Remove" — unloads every selected assembly from the active list. Only
// visible when the entire selection is assembly nodes, so it doesn't pollute right-click
// menus opened on member / namespace / resource rows. MEF discovers this via the
// [ExportContextMenuEntry] attribute and surfaces it through ContextMenuEntryRegistry.
[ExportContextMenuEntry(Header = nameof(ICSharpCode.ILSpy.Properties.Resources._Remove))]
[System.Composition.Shared]
sealed class RemoveAssembly : IContextMenuEntry
{
public bool IsVisible(TextViewContext context)
{
var nodes = context.SelectedTreeNodes;
return nodes is { Length: > 0 } && nodes.All(n => n is AssemblyTreeNode);
}
public bool IsEnabled(TextViewContext context) => true;
public void Execute(TextViewContext context)
{
if (context.SelectedTreeNodes == null)
return;
// Snapshot before mutation — Unload reshapes the tree and the live selection.
foreach (var node in context.SelectedTreeNodes.OfType<AssemblyTreeNode>().ToArray())
node.Delete();
}
}
// Right-click → "Reload" — re-reads each selected assembly from disk so the user can
// pick up edits made by an external build without restarting ILSpy. Same visibility
// shape as Remove (assembly nodes only). The model's existing path-restoration plumbing
// handles re-selecting equivalent nodes after the reload churns the tree.
[ExportContextMenuEntry(Header = nameof(ICSharpCode.ILSpy.Properties.Resources._Reload))]
[System.Composition.Shared]
[method: System.Composition.ImportingConstructor]
sealed class ReloadAssembly(AssemblyTree.AssemblyTreeModel assemblyTreeModel) : IContextMenuEntry
{
public bool IsVisible(TextViewContext context)
{
var nodes = context.SelectedTreeNodes;
return nodes is { Length: > 0 } && nodes.All(n => n is AssemblyTreeNode);
}
public bool IsEnabled(TextViewContext context) => true;
public void Execute(TextViewContext context)
{
if (context.SelectedTreeNodes == null)
return;
// Snapshot before mutation — ReloadAssembly mutates the AssemblyList and the
// live tree selection.
var nodes = context.SelectedTreeNodes.OfType<AssemblyTreeNode>().ToArray();
var paths = nodes.Select(n => AssemblyTree.AssemblyTreeModel.GetPathForNode(n)).ToArray();
foreach (var node in nodes)
{
var loaded = node.LoadedAssembly;
loaded.AssemblyList.ReloadAssembly(loaded.FileName);
}
// Re-select using the saved paths so the user keeps their position after the
// LoadedAssembly instances are swapped out.
if (paths.Length > 0)
{
var restored = paths
.Select(p => assemblyTreeModel.FindNodeByPath(p, returnBestMatch: true))
.OfType<ICSharpCode.ILSpyX.TreeView.SharpTreeNode>()
.LastOrDefault();
if (restored != null)
assemblyTreeModel.SelectNode(restored);
}
}
}
}
}