mirror of https://github.com/icsharpcode/ILSpy.git
Browse Source
Assisted-by: Claude:claude-opus-4-7:Claude Code Assisted-by: Claude:claude-opus-4-7:Claude Codepull/3755/head
3 changed files with 303 additions and 15 deletions
@ -0,0 +1,133 @@ |
|||||||
|
// 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.Linq; |
||||||
|
using System.Threading.Tasks; |
||||||
|
|
||||||
|
using Avalonia.Headless.NUnit; |
||||||
|
|
||||||
|
using AwesomeAssertions; |
||||||
|
|
||||||
|
using ILSpy.AppEnv; |
||||||
|
using ILSpy.Metadata; |
||||||
|
using ILSpy.TreeNodes; |
||||||
|
using ILSpy.ViewModels; |
||||||
|
using ILSpy.Views; |
||||||
|
|
||||||
|
using NUnit.Framework; |
||||||
|
|
||||||
|
namespace ICSharpCode.ILSpy.Tests.Metadata; |
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pins the typed-children breakdown of <see cref="DebugDirectoryTreeNode"/>: CodeView
|
||||||
|
/// + PdbChecksum entries get specialised tree nodes that surface their fields, other
|
||||||
|
/// entry types fall back to a generic node with a hex-dump <c>Decompile</c>. CoreLib's
|
||||||
|
/// PE always ships at least a CodeView entry plus a Reproducible entry, which lets us
|
||||||
|
/// validate both the typed and the fallback paths against a real assembly.
|
||||||
|
/// </summary>
|
||||||
|
[TestFixture] |
||||||
|
public class DebugDirectoryChildNodesTests |
||||||
|
{ |
||||||
|
[AvaloniaTest] |
||||||
|
public async Task DebugDirectoryTreeNode_Surfaces_CodeViewTreeNode_For_The_CodeView_Entry() |
||||||
|
{ |
||||||
|
var debugDir = await GetDebugDirectoryForCoreLib(); |
||||||
|
|
||||||
|
// CoreLib ships at least one CodeView entry (private + public PDB descriptions
|
||||||
|
// can produce multiple); the contract is "every CodeView entry surfaces as a
|
||||||
|
// typed child", not a specific count.
|
||||||
|
debugDir.Children.OfType<CodeViewTreeNode>().Should().NotBeEmpty( |
||||||
|
"every System.Private.CoreLib build ships at least one CodeView debug-directory entry"); |
||||||
|
} |
||||||
|
|
||||||
|
[AvaloniaTest] |
||||||
|
public async Task DebugDirectoryTreeNode_Surfaces_PdbChecksumTreeNode_For_PdbChecksum_Entries() |
||||||
|
{ |
||||||
|
var debugDir = await GetDebugDirectoryForCoreLib(); |
||||||
|
|
||||||
|
debugDir.Children.OfType<PdbChecksumTreeNode>().Should().NotBeEmpty( |
||||||
|
"modern CoreLib builds ship one or more PdbChecksum debug-directory entries"); |
||||||
|
var sum = debugDir.Children.OfType<PdbChecksumTreeNode>().First(); |
||||||
|
sum.AlgorithmName.Should().NotBeNullOrEmpty(); |
||||||
|
sum.Checksum.Should().NotBeNullOrEmpty(); |
||||||
|
} |
||||||
|
|
||||||
|
[AvaloniaTest] |
||||||
|
public async Task DebugDirectoryTreeNode_Falls_Back_To_Generic_Entry_For_Unspecialised_Types() |
||||||
|
{ |
||||||
|
// Reproducible entries are zero-payload and have no specialised viewer; they should
|
||||||
|
// still surface as DebugDirectoryEntryTreeNode children so the user can see them in
|
||||||
|
// the tree alongside the typed entries.
|
||||||
|
var debugDir = await GetDebugDirectoryForCoreLib(); |
||||||
|
|
||||||
|
var generic = debugDir.Children.OfType<DebugDirectoryEntryTreeNode>() |
||||||
|
.Where(n => n.GetType() == typeof(DebugDirectoryEntryTreeNode)) |
||||||
|
.ToList(); |
||||||
|
generic.Should().NotBeEmpty("expected at least one fallback entry (e.g. Reproducible)"); |
||||||
|
} |
||||||
|
|
||||||
|
[AvaloniaTest] |
||||||
|
public async Task Every_Entry_In_The_PE_Debug_Directory_Becomes_A_Child_Node() |
||||||
|
{ |
||||||
|
// Sanity check: no entry is silently dropped. The child count must match the raw
|
||||||
|
// directory's entry count exactly (modulo embedded-PDB BadImageFormat skips, which
|
||||||
|
// don't happen on healthy CoreLib).
|
||||||
|
var (debugDir, peEntryCount) = await GetDebugDirectoryAndRawCountForCoreLib(); |
||||||
|
|
||||||
|
debugDir.Children.Count.Should().Be(peEntryCount); |
||||||
|
} |
||||||
|
|
||||||
|
// --- helpers ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static async Task<DebugDirectoryTreeNode> GetDebugDirectoryForCoreLib() |
||||||
|
{ |
||||||
|
var window = AppComposition.Current.GetExport<MainWindow>(); |
||||||
|
window.Show(); |
||||||
|
var vm = (MainWindowViewModel)window.DataContext!; |
||||||
|
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); |
||||||
|
|
||||||
|
var coreLibName = typeof(object).Assembly.GetName().Name!; |
||||||
|
var assemblyNode = vm.AssemblyTreeModel.FindNode<AssemblyTreeNode>(coreLibName); |
||||||
|
assemblyNode.EnsureLazyChildren(); |
||||||
|
var metadataNode = assemblyNode.Children.OfType<MetadataTreeNode>().Single(); |
||||||
|
metadataNode.EnsureLazyChildren(); |
||||||
|
var debugDir = metadataNode.Children.OfType<DebugDirectoryTreeNode>().Single(); |
||||||
|
debugDir.EnsureLazyChildren(); |
||||||
|
return debugDir; |
||||||
|
} |
||||||
|
|
||||||
|
static async Task<(DebugDirectoryTreeNode node, int rawCount)> GetDebugDirectoryAndRawCountForCoreLib() |
||||||
|
{ |
||||||
|
var window = AppComposition.Current.GetExport<MainWindow>(); |
||||||
|
window.Show(); |
||||||
|
var vm = (MainWindowViewModel)window.DataContext!; |
||||||
|
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); |
||||||
|
|
||||||
|
var coreLibName = typeof(object).Assembly.GetName().Name!; |
||||||
|
var assemblyNode = vm.AssemblyTreeModel.FindNode<AssemblyTreeNode>(coreLibName); |
||||||
|
var module = (ICSharpCode.Decompiler.Metadata.PEFile)assemblyNode.LoadedAssembly.GetMetadataFileOrNull()!; |
||||||
|
int rawCount = module.Reader.ReadDebugDirectory().Length; |
||||||
|
|
||||||
|
assemblyNode.EnsureLazyChildren(); |
||||||
|
var metadataNode = assemblyNode.Children.OfType<MetadataTreeNode>().Single(); |
||||||
|
metadataNode.EnsureLazyChildren(); |
||||||
|
var debugDir = metadataNode.Children.OfType<DebugDirectoryTreeNode>().Single(); |
||||||
|
debugDir.EnsureLazyChildren(); |
||||||
|
return (debugDir, rawCount); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,125 @@ |
|||||||
|
// 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.Reflection.PortableExecutable; |
||||||
|
|
||||||
|
using ICSharpCode.Decompiler; |
||||||
|
using ICSharpCode.Decompiler.Metadata; |
||||||
|
|
||||||
|
using ILSpy.Languages; |
||||||
|
using ILSpy.TreeNodes; |
||||||
|
|
||||||
|
namespace ILSpy.Metadata |
||||||
|
{ |
||||||
|
/// <summary>
|
||||||
|
/// Per-entry tree node under <see cref="DebugDirectoryTreeNode"/>. Specialised typed
|
||||||
|
/// subclasses (<see cref="CodeViewTreeNode"/>, <see cref="PdbChecksumTreeNode"/>)
|
||||||
|
/// surface their fields as a tab page; this fallback shows the raw entry bytes for
|
||||||
|
/// types we don't decode further (Reproducible, EmbeddedPortablePdb, vendor entries).
|
||||||
|
/// </summary>
|
||||||
|
public class DebugDirectoryEntryTreeNode : ILSpyTreeNode |
||||||
|
{ |
||||||
|
readonly PEFile module; |
||||||
|
readonly DebugDirectoryEntry entry; |
||||||
|
|
||||||
|
public DebugDirectoryEntryTreeNode(PEFile module, DebugDirectoryEntry entry) |
||||||
|
{ |
||||||
|
this.module = module ?? throw new ArgumentNullException(nameof(module)); |
||||||
|
this.entry = entry; |
||||||
|
} |
||||||
|
|
||||||
|
public override object Text => entry.Type.ToString(); |
||||||
|
public override object Icon => Images.Images.MetadataTable; |
||||||
|
public override string ToString() => entry.Type.ToString(); |
||||||
|
|
||||||
|
protected DebugDirectoryEntry Entry => entry; |
||||||
|
protected PEFile Module => module; |
||||||
|
|
||||||
|
public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) |
||||||
|
{ |
||||||
|
language.WriteCommentLine(output, entry.Type.ToString()); |
||||||
|
if (entry.DataSize > 0) |
||||||
|
{ |
||||||
|
language.WriteCommentLine(output, $"Raw Data ({entry.DataSize}):"); |
||||||
|
int dataOffset = module.Reader.IsLoadedImage |
||||||
|
? entry.DataRelativeVirtualAddress |
||||||
|
: entry.DataPointer; |
||||||
|
var data = module.Reader.GetEntireImage().GetContent(dataOffset, entry.DataSize); |
||||||
|
language.WriteCommentLine(output, data.ToHexString(data.Length)); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
language.WriteCommentLine(output, "(no data)"); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// CodeView debug-directory entry — names the associated PDB. Surfaces the GUID,
|
||||||
|
/// age, and original symbol-file path.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CodeViewTreeNode : DebugDirectoryEntryTreeNode |
||||||
|
{ |
||||||
|
readonly CodeViewDebugDirectoryData data; |
||||||
|
|
||||||
|
public CodeViewTreeNode(PEFile module, DebugDirectoryEntry entry, CodeViewDebugDirectoryData data) |
||||||
|
: base(module, entry) |
||||||
|
{ |
||||||
|
this.data = data; |
||||||
|
} |
||||||
|
|
||||||
|
public override object Text => nameof(DebugDirectoryEntryType.CodeView); |
||||||
|
|
||||||
|
public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) |
||||||
|
{ |
||||||
|
language.WriteCommentLine(output, Text.ToString()!); |
||||||
|
language.WriteCommentLine(output, $"GUID: {data.Guid}"); |
||||||
|
language.WriteCommentLine(output, $"Age: {data.Age}"); |
||||||
|
language.WriteCommentLine(output, $"Path: {data.Path}"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PdbChecksum debug-directory entry — a cryptographic hash of the symbol file's
|
||||||
|
/// contents, used to validate that a PDB matches its PE/COFF parent. Multiple entries
|
||||||
|
/// can coexist when the build emits both public and private PDBs.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PdbChecksumTreeNode : DebugDirectoryEntryTreeNode |
||||||
|
{ |
||||||
|
readonly PdbChecksumDebugDirectoryData data; |
||||||
|
|
||||||
|
public PdbChecksumTreeNode(PEFile module, DebugDirectoryEntry entry, PdbChecksumDebugDirectoryData data) |
||||||
|
: base(module, entry) |
||||||
|
{ |
||||||
|
this.data = data; |
||||||
|
} |
||||||
|
|
||||||
|
public override object Text => nameof(DebugDirectoryEntryType.PdbChecksum); |
||||||
|
|
||||||
|
public string AlgorithmName => data.AlgorithmName; |
||||||
|
public string Checksum => data.Checksum.ToHexString(data.Checksum.Length); |
||||||
|
|
||||||
|
public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) |
||||||
|
{ |
||||||
|
language.WriteCommentLine(output, Text.ToString()!); |
||||||
|
language.WriteCommentLine(output, $"AlgorithmName: {AlgorithmName}"); |
||||||
|
language.WriteCommentLine(output, $"Checksum: {Checksum}"); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue