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; } } }