diff --git a/ILSpy.Tests/Metadata/DebugDirectoryChildNodesTests.cs b/ILSpy.Tests/Metadata/DebugDirectoryChildNodesTests.cs
new file mode 100644
index 000000000..d466349f5
--- /dev/null
+++ b/ILSpy.Tests/Metadata/DebugDirectoryChildNodesTests.cs
@@ -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;
+
+///
+/// Pins the typed-children breakdown of : CodeView
+/// + PdbChecksum entries get specialised tree nodes that surface their fields, other
+/// entry types fall back to a generic node with a hex-dump Decompile. 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.
+///
+[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().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().Should().NotBeEmpty(
+ "modern CoreLib builds ship one or more PdbChecksum debug-directory entries");
+ var sum = debugDir.Children.OfType().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()
+ .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 GetDebugDirectoryForCoreLib()
+ {
+ var window = AppComposition.Current.GetExport();
+ 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(coreLibName);
+ assemblyNode.EnsureLazyChildren();
+ var metadataNode = assemblyNode.Children.OfType().Single();
+ metadataNode.EnsureLazyChildren();
+ var debugDir = metadataNode.Children.OfType().Single();
+ debugDir.EnsureLazyChildren();
+ return debugDir;
+ }
+
+ static async Task<(DebugDirectoryTreeNode node, int rawCount)> GetDebugDirectoryAndRawCountForCoreLib()
+ {
+ var window = AppComposition.Current.GetExport();
+ 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(coreLibName);
+ var module = (ICSharpCode.Decompiler.Metadata.PEFile)assemblyNode.LoadedAssembly.GetMetadataFileOrNull()!;
+ int rawCount = module.Reader.ReadDebugDirectory().Length;
+
+ assemblyNode.EnsureLazyChildren();
+ var metadataNode = assemblyNode.Children.OfType().Single();
+ metadataNode.EnsureLazyChildren();
+ var debugDir = metadataNode.Children.OfType().Single();
+ debugDir.EnsureLazyChildren();
+ return (debugDir, rawCount);
+ }
+}
diff --git a/ILSpy/Metadata/DebugDirectoryChildNodes.cs b/ILSpy/Metadata/DebugDirectoryChildNodes.cs
new file mode 100644
index 000000000..f24025e0b
--- /dev/null
+++ b/ILSpy/Metadata/DebugDirectoryChildNodes.cs
@@ -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
+{
+ ///
+ /// Per-entry tree node under . Specialised typed
+ /// subclasses (, )
+ /// 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).
+ ///
+ 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)");
+ }
+ }
+ }
+
+ ///
+ /// CodeView debug-directory entry — names the associated PDB. Surfaces the GUID,
+ /// age, and original symbol-file path.
+ ///
+ 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}");
+ }
+ }
+
+ ///
+ /// 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.
+ ///
+ 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}");
+ }
+ }
+}
diff --git a/ILSpy/Metadata/DebugDirectoryTreeNode.cs b/ILSpy/Metadata/DebugDirectoryTreeNode.cs
index cce0ddd25..098dd17ac 100644
--- a/ILSpy/Metadata/DebugDirectoryTreeNode.cs
+++ b/ILSpy/Metadata/DebugDirectoryTreeNode.cs
@@ -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;
}
}
}