Browse Source

Typed debug-directory child nodes

Assisted-by: Claude:claude-opus-4-7:Claude Code

Assisted-by: Claude:claude-opus-4-7:Claude Code
pull/3755/head
Siegfried Pammer 2 months ago
parent
commit
d30ef0f183
  1. 133
      ILSpy.Tests/Metadata/DebugDirectoryChildNodesTests.cs
  2. 125
      ILSpy/Metadata/DebugDirectoryChildNodes.cs
  3. 60
      ILSpy/Metadata/DebugDirectoryTreeNode.cs

133
ILSpy.Tests/Metadata/DebugDirectoryChildNodesTests.cs

@ -0,0 +1,133 @@ @@ -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);
}
}

125
ILSpy/Metadata/DebugDirectoryChildNodes.cs

@ -0,0 +1,125 @@ @@ -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}");
}
}
}

60
ILSpy/Metadata/DebugDirectoryTreeNode.cs

@ -77,22 +77,52 @@ namespace ILSpy.Metadata @@ -77,22 +77,52 @@ namespace ILSpy.Metadata
{
foreach (var entry in module.Reader.ReadDebugDirectory())
{
if (entry.Type != DebugDirectoryEntryType.EmbeddedPortablePdb)
continue;
try
switch (entry.Type)
{
var provider = module.Reader.ReadEmbeddedPortablePdbDebugDirectoryData(entry);
var pdbMetadata = new MetadataFile(
MetadataFile.MetadataFileKind.ProgramDebugDatabase,
module.FileName,
provider,
isEmbedded: true);
Children.Add(new MetadataTreeNode(pdbMetadata, "Debug Metadata (Embedded)"));
}
catch (BadImageFormatException)
{
// A corrupt embedded PDB shouldn't take the whole tree down — skip it
// silently; the entry is still visible in the grid view above.
case DebugDirectoryEntryType.CodeView:
try
{
var cv = module.Reader.ReadCodeViewDebugDirectoryData(entry);
Children.Add(new CodeViewTreeNode(module, entry, cv));
}
catch (BadImageFormatException)
{
Children.Add(new DebugDirectoryEntryTreeNode(module, entry));
}
break;
case DebugDirectoryEntryType.PdbChecksum:
try
{
var sum = module.Reader.ReadPdbChecksumDebugDirectoryData(entry);
Children.Add(new PdbChecksumTreeNode(module, entry, sum));
}
catch (BadImageFormatException)
{
Children.Add(new DebugDirectoryEntryTreeNode(module, entry));
}
break;
case DebugDirectoryEntryType.EmbeddedPortablePdb:
try
{
var provider = module.Reader.ReadEmbeddedPortablePdbDebugDirectoryData(entry);
var pdbMetadata = new MetadataFile(
MetadataFile.MetadataFileKind.ProgramDebugDatabase,
module.FileName,
provider,
isEmbedded: true);
Children.Add(new MetadataTreeNode(pdbMetadata, "Debug Metadata (Embedded)"));
}
catch (BadImageFormatException)
{
// A corrupt embedded PDB shouldn't take the whole tree down — skip it
// silently; the entry is still visible in the grid view above.
}
break;
default:
// Reproducible, Vendor-specific, Mpx, ExtendedDllCharacteristics, …
// The generic fallback shows the type + raw hex dump in Decompile.
Children.Add(new DebugDirectoryEntryTreeNode(module, entry));
break;
}
}
}

Loading…
Cancel
Save