diff --git a/ILSpy.Tests/Metadata/PEHeaderTreeTests.cs b/ILSpy.Tests/Metadata/PEHeaderTreeTests.cs new file mode 100644 index 000000000..baeb8b24f --- /dev/null +++ b/ILSpy.Tests/Metadata/PEHeaderTreeTests.cs @@ -0,0 +1,127 @@ +// 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; + +[TestFixture] +public class PEHeaderTreeTests +{ + [AvaloniaTest] + public async Task MetadataTreeNode_Surfaces_Five_PE_Header_Children_For_A_PE_Assembly() + { + // The Metadata folder under a PE assembly should expose five PE-format leaves: + // DOS / COFF / Optional / DataDirectories / DebugDirectory. Order mirrors WPF so a + // user shifting between hosts sees the same tree shape. Each node is the entry point + // to a header / table view; Phase 1 renders text, Phase 2 swaps to a DataGrid. + + 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(); + + metadataNode.Children.OfType().Should().ContainSingle(); + metadataNode.Children.OfType().Should().ContainSingle(); + metadataNode.Children.OfType().Should().ContainSingle(); + metadataNode.Children.OfType().Should().ContainSingle(); + metadataNode.Children.OfType().Should().ContainSingle(); + + var orderedTypes = metadataNode.Children.Take(5).Select(c => c.GetType()).ToList(); + orderedTypes.Should().Equal( + typeof(DosHeaderTreeNode), + typeof(CoffHeaderTreeNode), + typeof(OptionalHeaderTreeNode), + typeof(DataDirectoriesTreeNode), + typeof(DebugDirectoryTreeNode)); + } + + [AvaloniaTest] + public async Task DosHeaderTreeNode_Decompiles_To_A_Table_With_All_31_DOS_Header_Fields() + { + // DOS header is a fixed 64-byte block at offset 0; the 31 fields are well-known and + // stable across every PE file. Selecting the node should produce a text table that + // every reader can scan, with the magic-number row visible by both its raw value and + // its semantic meaning. + + 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 dosNode = metadataNode.Children.OfType().Single(); + + vm.AssemblyTreeModel.SelectNode(dosNode); + var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + + tab.Text.Should().Contain("e_magic"); + tab.Text.Should().Contain("Magic Number (MZ)"); + tab.Text.Should().Contain("e_lfanew"); + tab.Text.Should().Contain("File address of new exe header"); + // All 31 DOS-header field rows should be present — count e_ prefixes on member rows. + var fieldCount = System.Text.RegularExpressions.Regex.Matches(tab.Text, @"\be_[a-z0-9_\[\]]+").Count; + fieldCount.Should().BeGreaterThanOrEqualTo(31); + } + + [AvaloniaTest] + public async Task CoffHeaderTreeNode_Decompiles_To_A_Table_Including_Machine_And_Characteristics() + { + 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 coffNode = metadataNode.Children.OfType().Single(); + + vm.AssemblyTreeModel.SelectNode(coffNode); + var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + + tab.Text.Should().Contain("Machine"); + tab.Text.Should().Contain("Number of Sections"); + tab.Text.Should().Contain("Characteristics"); + } +} diff --git a/ILSpy/Metadata/CoffHeaderTreeNode.cs b/ILSpy/Metadata/CoffHeaderTreeNode.cs new file mode 100644 index 000000000..206f35aab --- /dev/null +++ b/ILSpy/Metadata/CoffHeaderTreeNode.cs @@ -0,0 +1,88 @@ +// 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.Collections.Generic; + +using ICSharpCode.Decompiler; +using ICSharpCode.Decompiler.Metadata; + +using ILSpy.Languages; +using ILSpy.TreeNodes; + +namespace ILSpy.Metadata +{ + /// + /// COFF (Common Object File Format) header — 20 bytes describing target machine, section + /// count, link timestamp, and file-level characteristics. The Characteristics field is a + /// 16-bit flags word; each individual bit is broken out as a row-detail entry. + /// + public sealed class CoffHeaderTreeNode : ILSpyTreeNode + { + readonly PEFile module; + + public CoffHeaderTreeNode(PEFile module) + { + this.module = module ?? throw new ArgumentNullException(nameof(module)); + } + + public override object Text => "COFF Header"; + public override object Icon => Images.Images.MetadataTable; + public override string ToString() => "COFF Header"; + + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + { + language.WriteCommentLine(output, "COFF Header"); + MetadataTextWriter.WriteTable(language, output, BuildEntries()); + } + + IReadOnlyList BuildEntries() + { + var headers = module.Reader.PEHeaders; + var header = headers.CoffHeader; + var linkerDateTime = DateTimeOffset.FromUnixTimeSeconds(unchecked((uint)header.TimeDateStamp)).DateTime; + int characteristics = (int)header.Characteristics; + return new List { + new(headers.CoffHeaderStartOffset, (int)header.Machine, 2, "Machine", header.Machine.ToString()), + new(headers.CoffHeaderStartOffset + 2, (int)header.NumberOfSections, 2, "Number of Sections", "Number of sections; indicates size of the Section Table, which immediately follows the headers."), + new(headers.CoffHeaderStartOffset + 4, header.TimeDateStamp, 4, "Time/Date Stamp", $"{linkerDateTime} (UTC) / {linkerDateTime.ToLocalTime()} - Time and date the file was created in seconds since January 1st 1970 00:00:00 or 0. Note that for deterministic builds this value is meaningless."), + new(headers.CoffHeaderStartOffset + 8, header.PointerToSymbolTable, 4, "Pointer to Symbol Table", "Always 0 in .NET executables."), + new(headers.CoffHeaderStartOffset + 12, header.NumberOfSymbols, 4, "Number of Symbols", "Always 0 in .NET executables."), + new(headers.CoffHeaderStartOffset + 16, (int)header.SizeOfOptionalHeader, 2, "Optional Header Size", "Size of the optional header."), + new(headers.CoffHeaderStartOffset + 18, characteristics, 2, "Characteristics", "Flags indicating attributes of the file.", new BitEntry[] { + new((characteristics & 0x0001) != 0, "<0001> Relocation info stripped from file"), + new((characteristics & 0x0002) != 0, "<0002> File is executable"), + new((characteristics & 0x0004) != 0, "<0004> Line numbers stripped from file"), + new((characteristics & 0x0008) != 0, "<0008> Local symbols stripped from file"), + new((characteristics & 0x0010) != 0, "<0010> Aggressively trim working set"), + new((characteristics & 0x0020) != 0, "<0020> Large address aware"), + new((characteristics & 0x0040) != 0, "<0040> Reserved"), + new((characteristics & 0x0080) != 0, "<0080> Bytes of machine words are reversed (Low)"), + new((characteristics & 0x0100) != 0, "<0100> 32-bit word machine"), + new((characteristics & 0x0200) != 0, "<0200> Debugging info stripped from file in .DBG file"), + new((characteristics & 0x0400) != 0, "<0400> If image is on removable media, copy and run from the swap file"), + new((characteristics & 0x0800) != 0, "<0800> If image is on Net, copy and run from the swap file"), + new((characteristics & 0x1000) != 0, "<1000> System"), + new((characteristics & 0x2000) != 0, "<2000> DLL"), + new((characteristics & 0x4000) != 0, "<4000> File should only be run on a UP machine"), + new((characteristics & 0x8000) != 0, "<8000> Bytes of machine words are reversed (High)"), + }), + }; + } + } +} diff --git a/ILSpy/Metadata/DataDirectoriesTreeNode.cs b/ILSpy/Metadata/DataDirectoriesTreeNode.cs new file mode 100644 index 000000000..7910d857a --- /dev/null +++ b/ILSpy/Metadata/DataDirectoriesTreeNode.cs @@ -0,0 +1,97 @@ +// 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.Collections.Generic; +using System.Reflection.PortableExecutable; + +using ICSharpCode.Decompiler; +using ICSharpCode.Decompiler.Metadata; + +using ILSpy.Languages; +using ILSpy.TreeNodes; + +namespace ILSpy.Metadata +{ + /// + /// 15-row table of (RVA, Size, Containing-Section) triples that lives at the tail of the + /// PE Optional Header. Each entry points at one well-known PE region (Export Table, + /// Import Table, CLI Header, …). + /// + public sealed class DataDirectoriesTreeNode : ILSpyTreeNode + { + readonly PEFile module; + + public DataDirectoriesTreeNode(PEFile module) + { + this.module = module ?? throw new ArgumentNullException(nameof(module)); + } + + public override object Text => "Data Directories"; + public override object Icon => Images.Images.MetadataTable; + public override string ToString() => "Data Directories"; + + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + { + language.WriteCommentLine(output, "Data Directories"); + MetadataTextWriter.WriteTable(language, output, BuildEntries()); + } + + IReadOnlyList BuildEntries() + { + var headers = module.Reader.PEHeaders; + var header = headers.PEHeader!; + return new List { + new(headers, "Export Table", header.ExportTableDirectory), + new(headers, "Import Table", header.ImportTableDirectory), + new(headers, "Resource Table", header.ResourceTableDirectory), + new(headers, "Exception Table", header.ExceptionTableDirectory), + new(headers, "Certificate Table", header.CertificateTableDirectory), + new(headers, "Base Relocation Table", header.BaseRelocationTableDirectory), + new(headers, "Debug Table", header.DebugTableDirectory), + new(headers, "Copyright Table", header.CopyrightTableDirectory), + new(headers, "Global Pointer Table", header.GlobalPointerTableDirectory), + new(headers, "Thread Local Storage Table", header.ThreadLocalStorageTableDirectory), + new(headers, "Load Config", header.LoadConfigTableDirectory), + new(headers, "Bound Import", header.BoundImportTableDirectory), + new(headers, "Import Address Table", header.ImportAddressTableDirectory), + new(headers, "Delay Import Descriptor", header.DelayImportTableDirectory), + new(headers, "CLI Header", header.CorHeaderTableDirectory), + }; + } + + public sealed class DataDirectoryEntry + { + public string Name { get; } + [ColumnInfo("X8")] + public int RVA { get; } + [ColumnInfo("X8")] + public int Size { get; } + public string Section { get; } + + public DataDirectoryEntry(PEHeaders headers, string name, DirectoryEntry entry) + { + Name = name; + RVA = entry.RelativeVirtualAddress; + Size = entry.Size; + int sectionIndex = headers.GetContainingSectionIndex(entry.RelativeVirtualAddress); + Section = sectionIndex >= 0 ? headers.SectionHeaders[sectionIndex].Name : ""; + } + } + } +} diff --git a/ILSpy/Metadata/DebugDirectoryTreeNode.cs b/ILSpy/Metadata/DebugDirectoryTreeNode.cs new file mode 100644 index 000000000..500646bd5 --- /dev/null +++ b/ILSpy/Metadata/DebugDirectoryTreeNode.cs @@ -0,0 +1,94 @@ +// 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.Collections.Generic; +using System.Reflection.PortableExecutable; + +using ICSharpCode.Decompiler; +using ICSharpCode.Decompiler.Metadata; + +using ILSpy.Languages; +using ILSpy.TreeNodes; + +namespace ILSpy.Metadata +{ + /// + /// Lists each entry of the PE debug directory: timestamp, version, type, and the size / + /// addresses of the raw debug payload. Phase 1 renders the entries as text; the + /// per-entry sub-tree (CodeView, embedded portable PDB, PDB checksum) lands with the + /// rest of the metadata-tables work later in Phase 1. + /// + public sealed class DebugDirectoryTreeNode : ILSpyTreeNode + { + readonly PEFile module; + + public DebugDirectoryTreeNode(PEFile module) + { + this.module = module ?? throw new ArgumentNullException(nameof(module)); + } + + public override object Text => "Debug Directory"; + public override object Icon => Images.Images.MetadataTable; + public override string ToString() => "Debug Directory"; + + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + { + language.WriteCommentLine(output, "Debug Directory"); + MetadataTextWriter.WriteTable(language, output, BuildEntries()); + } + + IReadOnlyList BuildEntries() + { + var entries = new List(); + foreach (var entry in module.Reader.ReadDebugDirectory()) + { + int dataOffset = module.Reader.IsLoadedImage ? entry.DataRelativeVirtualAddress : entry.DataPointer; + var data = module.Reader.GetEntireImage().GetContent(dataOffset, entry.DataSize); + entries.Add(new DebugDirectoryEntryView(entry, data.ToHexString(data.Length))); + } + return entries; + } + + public sealed class DebugDirectoryEntryView + { + public uint Timestamp { get; } + public ushort MajorVersion { get; } + public ushort MinorVersion { get; } + public string Type { get; } + public int SizeOfRawData { get; } + [ColumnInfo("X8")] + public int AddressOfRawData { get; } + [ColumnInfo("X8")] + public int PointerToRawData { get; } + public string RawData { get; } + + public DebugDirectoryEntryView(DebugDirectoryEntry entry, string data) + { + Timestamp = entry.Stamp; + MajorVersion = entry.MajorVersion; + MinorVersion = entry.MinorVersion; + Type = entry.Type.ToString(); + SizeOfRawData = entry.DataSize; + AddressOfRawData = entry.DataRelativeVirtualAddress; + PointerToRawData = entry.DataPointer; + RawData = data; + } + } + } +} diff --git a/ILSpy/Metadata/DosHeaderTreeNode.cs b/ILSpy/Metadata/DosHeaderTreeNode.cs new file mode 100644 index 000000000..99ebbf114 --- /dev/null +++ b/ILSpy/Metadata/DosHeaderTreeNode.cs @@ -0,0 +1,92 @@ +// 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.Collections.Generic; + +using ICSharpCode.Decompiler; +using ICSharpCode.Decompiler.Metadata; + +using ILSpy.Languages; +using ILSpy.TreeNodes; + +namespace ILSpy.Metadata +{ + /// + /// Renders the 64-byte legacy DOS header sitting at offset 0 of every PE file. Phase 1 + /// emits a fixed-width text table; Phase 2 swaps to a DataGrid tab. + /// + public sealed class DosHeaderTreeNode : ILSpyTreeNode + { + readonly PEFile module; + + public DosHeaderTreeNode(PEFile module) + { + this.module = module ?? throw new ArgumentNullException(nameof(module)); + } + + public override object Text => "DOS Header"; + public override object Icon => Images.Images.MetadataTable; + public override string ToString() => "DOS Header"; + + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + { + language.WriteCommentLine(output, "DOS Header"); + MetadataTextWriter.WriteTable(language, output, BuildEntries()); + } + + IReadOnlyList BuildEntries() + { + var reader = module.Reader.GetEntireImage().GetReader(0, 64); + var entries = new List(31) { + new(reader.Offset, reader.ReadUInt16(), 2, "e_magic", "Magic Number (MZ)"), + new(reader.Offset, reader.ReadUInt16(), 2, "e_cblp", "Bytes on last page of file"), + new(reader.Offset, reader.ReadUInt16(), 2, "e_cp", "Pages in file"), + new(reader.Offset, reader.ReadUInt16(), 2, "e_crlc", "Relocations"), + new(reader.Offset, reader.ReadUInt16(), 2, "e_cparhdr", "Size of header in paragraphs"), + new(reader.Offset, reader.ReadUInt16(), 2, "e_minalloc", "Minimum extra paragraphs needed"), + new(reader.Offset, reader.ReadUInt16(), 2, "e_maxalloc", "Maximum extra paragraphs needed"), + new(reader.Offset, reader.ReadUInt16(), 2, "e_ss", "Initial (relative) SS value"), + new(reader.Offset, reader.ReadUInt16(), 2, "e_sp", "Initial SP value"), + new(reader.Offset, reader.ReadUInt16(), 2, "e_csum", "Checksum"), + new(reader.Offset, reader.ReadUInt16(), 2, "e_ip", "Initial IP value"), + new(reader.Offset, reader.ReadUInt16(), 2, "e_cs", "Initial (relative) CS value"), + new(reader.Offset, reader.ReadUInt16(), 2, "e_lfarlc", "File address of relocation table"), + new(reader.Offset, reader.ReadUInt16(), 2, "e_ovno", "Overlay number"), + new(reader.Offset, reader.ReadUInt16(), 2, "e_res[0]", "Reserved words"), + new(reader.Offset, reader.ReadUInt16(), 2, "e_res[1]", "Reserved words"), + new(reader.Offset, reader.ReadUInt16(), 2, "e_res[2]", "Reserved words"), + new(reader.Offset, reader.ReadUInt16(), 2, "e_res[3]", "Reserved words"), + new(reader.Offset, reader.ReadUInt16(), 2, "e_oemid", "OEM identifier (for e_oeminfo)"), + new(reader.Offset, reader.ReadUInt16(), 2, "e_oeminfo", "OEM information; e_oemid specific"), + new(reader.Offset, reader.ReadUInt16(), 2, "e_res2[0]", "Reserved words"), + new(reader.Offset, reader.ReadUInt16(), 2, "e_res2[1]", "Reserved words"), + new(reader.Offset, reader.ReadUInt16(), 2, "e_res2[2]", "Reserved words"), + new(reader.Offset, reader.ReadUInt16(), 2, "e_res2[3]", "Reserved words"), + new(reader.Offset, reader.ReadUInt16(), 2, "e_res2[4]", "Reserved words"), + new(reader.Offset, reader.ReadUInt16(), 2, "e_res2[5]", "Reserved words"), + new(reader.Offset, reader.ReadUInt16(), 2, "e_res2[6]", "Reserved words"), + new(reader.Offset, reader.ReadUInt16(), 2, "e_res2[7]", "Reserved words"), + new(reader.Offset, reader.ReadUInt16(), 2, "e_res2[8]", "Reserved words"), + new(reader.Offset, reader.ReadUInt16(), 2, "e_res2[9]", "Reserved words"), + new(reader.Offset, reader.ReadInt32(), 4, "e_lfanew", "File address of new exe header"), + }; + return entries; + } + } +} diff --git a/ILSpy/Metadata/Helpers.cs b/ILSpy/Metadata/Helpers.cs new file mode 100644 index 000000000..ad161638e --- /dev/null +++ b/ILSpy/Metadata/Helpers.cs @@ -0,0 +1,93 @@ +// 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.Collections.Generic; + +namespace ILSpy.Metadata +{ + /// + /// Classifies a column for the DataGrid view (Phase 2+). Heap-offset cells render with a + /// hex-padded width and resolve their string content via the relevant heap reader; token + /// cells render as a hyperlink that navigates to the target table row. The text path + /// (Phase 1) ignores everything except Format. + /// + public enum ColumnKind + { + Other, + HeapOffset, + Token, + } + + /// + /// Annotates a row-shape property with rendering hints. Format is consumed by the + /// text writer; Kind and LinkToTable drive the DataGrid column factory in + /// Phase 2+ and are inert in Phase 1. + /// + [AttributeUsage(AttributeTargets.Property)] + public sealed class ColumnInfoAttribute : Attribute + { + public string Format { get; } + public ColumnKind Kind { get; set; } + public bool LinkToTable { get; set; } + + public ColumnInfoAttribute(string format) + { + Format = format; + } + } + + /// + /// One row in a PE-header / metadata-table dump. Value is rendered hex-formatted + /// at Size * 2 digits when numeric; RowDetails carries optional flag-bit + /// breakdowns that the DataGrid view (Phase 2) uses to populate row details. The text + /// path (Phase 1) ignores RowDetails. + /// + public sealed class Entry + { + public string Member { get; } + public int Offset { get; } + public int Size { get; } + public object? Value { get; } + public string Meaning { get; } + public IList? RowDetails { get; } + + public Entry(int offset, object? value, int size, string member, string meaning, IList? rowDetails = null) + { + Member = member; + Offset = offset; + Size = size; + Value = value; + Meaning = meaning; + RowDetails = rowDetails; + } + } + + /// One bit (or bit-group) entry inside a flags-Entry's row-details strip. + public sealed class BitEntry + { + public bool Value { get; } + public string Meaning { get; } + + public BitEntry(bool value, string meaning) + { + Value = value; + Meaning = meaning; + } + } +} diff --git a/ILSpy/Metadata/MetadataTextWriter.cs b/ILSpy/Metadata/MetadataTextWriter.cs new file mode 100644 index 000000000..fc5ece896 --- /dev/null +++ b/ILSpy/Metadata/MetadataTextWriter.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; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text; + +using ICSharpCode.Decompiler; + +using ILSpy.Languages; + +namespace ILSpy.Metadata +{ + /// + /// Renders a list of row objects to as a fixed-width table — + /// the Phase 1 stand-in for the DataGrid view. Column names come from + /// ; cell formatting respects + /// and, for rows, the + /// per-row hex-width implied by . Each emitted line goes through + /// so the output sits cleanly inside the + /// existing comment-prefixed text pipeline. + /// + public static class MetadataTextWriter + { + public static void WriteTable(Language language, ITextOutput output, IReadOnlyList rows) + { + ArgumentNullException.ThrowIfNull(language); + ArgumentNullException.ThrowIfNull(output); + ArgumentNullException.ThrowIfNull(rows); + + var props = typeof(TRow).GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.GetIndexParameters().Length == 0 + && p.PropertyType != typeof(IList) + && p.Name != nameof(Entry.RowDetails)) + .ToArray(); + if (props.Length == 0) + return; + + var formats = props.Select(p => p.GetCustomAttribute()?.Format).ToArray(); + + // Materialise every cell as a string, so column widths can be computed in one pass. + var cells = new string[rows.Count][]; + for (int r = 0; r < rows.Count; r++) + { + cells[r] = new string[props.Length]; + for (int c = 0; c < props.Length; c++) + cells[r][c] = FormatCell(rows[r], props[c], formats[c]); + } + + var widths = new int[props.Length]; + for (int c = 0; c < props.Length; c++) + { + widths[c] = props[c].Name.Length; + for (int r = 0; r < rows.Count; r++) + widths[c] = Math.Max(widths[c], cells[r][c].Length); + } + + language.WriteCommentLine(output, BuildLine(widths, props.Select(p => p.Name).ToArray())); + language.WriteCommentLine(output, BuildLine(widths, widths.Select(w => new string('-', w)).ToArray())); + for (int r = 0; r < rows.Count; r++) + language.WriteCommentLine(output, BuildLine(widths, cells[r])); + } + + static string BuildLine(int[] widths, string[] cells) + { + var sb = new StringBuilder(); + for (int i = 0; i < cells.Length; i++) + { + if (i > 0) + sb.Append(" "); + sb.Append(cells[i].PadRight(widths[i])); + } + return sb.ToString().TrimEnd(); + } + + static string FormatCell(object? row, PropertyInfo prop, string? format) + { + var value = prop.GetValue(row); + if (value is null) + return ""; + + // Entry-shaped rows get special handling: Offset → 8-digit hex; Value → hex padded + // to (Size * 2) digits. Phase 2 will hoist this onto a converter; Phase 1 keeps it + // inline because the only consumer is this writer. + if (row is Entry entry) + { + if (prop.Name == nameof(Entry.Offset)) + return ((int)value).ToString("X8", CultureInfo.InvariantCulture); + if (prop.Name == nameof(Entry.Value) && entry.Size > 0) + return FormatHex(value, entry.Size * 2); + } + + if (!string.IsNullOrEmpty(format) && value is IFormattable formattable) + return formattable.ToString(format, CultureInfo.InvariantCulture); + + return value.ToString() ?? ""; + } + + static string FormatHex(object value, int width) + { + string spec = "X" + width.ToString(CultureInfo.InvariantCulture); + return value switch { + ulong u => u.ToString(spec, CultureInfo.InvariantCulture), + long l => l.ToString(spec, CultureInfo.InvariantCulture), + uint ui => ui.ToString(spec, CultureInfo.InvariantCulture), + int i => i.ToString(spec, CultureInfo.InvariantCulture), + ushort us => us.ToString(spec, CultureInfo.InvariantCulture), + short s => s.ToString(spec, CultureInfo.InvariantCulture), + byte b => b.ToString(spec, CultureInfo.InvariantCulture), + sbyte sb => sb.ToString(spec, CultureInfo.InvariantCulture), + _ => value.ToString() ?? "", + }; + } + } +} diff --git a/ILSpy/Metadata/MetadataTreeNode.cs b/ILSpy/Metadata/MetadataTreeNode.cs index 235634d05..201ce5cd5 100644 --- a/ILSpy/Metadata/MetadataTreeNode.cs +++ b/ILSpy/Metadata/MetadataTreeNode.cs @@ -93,9 +93,16 @@ namespace ILSpy.Metadata protected override void LoadChildren() { - // PE-header / table / heap children land in subsequent commits — Phase 1 keeps the - // container empty so the navigation surface is stable while we add the leaf nodes - // incrementally without destabilising the suite. + if (metadataFile is PEFile peFile) + { + Children.Add(new DosHeaderTreeNode(peFile)); + Children.Add(new CoffHeaderTreeNode(peFile)); + Children.Add(new OptionalHeaderTreeNode(peFile)); + Children.Add(new DataDirectoriesTreeNode(peFile)); + Children.Add(new DebugDirectoryTreeNode(peFile)); + } + + // Heaps and metadata-tables children land in subsequent commits. } } } diff --git a/ILSpy/Metadata/OptionalHeaderTreeNode.cs b/ILSpy/Metadata/OptionalHeaderTreeNode.cs new file mode 100644 index 000000000..267bdf429 --- /dev/null +++ b/ILSpy/Metadata/OptionalHeaderTreeNode.cs @@ -0,0 +1,114 @@ +// 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.Collections.Generic; +using System.Reflection.PortableExecutable; + +using ICSharpCode.Decompiler; +using ICSharpCode.Decompiler.Metadata; + +using ILSpy.Languages; +using ILSpy.TreeNodes; + +namespace ILSpy.Metadata +{ + /// + /// PE Optional Header — image base, sizes, alignments, subsystem. Field widths flex + /// between PE32 and PE32+ (Image Base / Stack & Heap reserves go from 4 → 8 bytes, + /// Base Of Data drops out on PE32+). + /// + public sealed class OptionalHeaderTreeNode : ILSpyTreeNode + { + readonly PEFile module; + + public OptionalHeaderTreeNode(PEFile module) + { + this.module = module ?? throw new ArgumentNullException(nameof(module)); + } + + public override object Text => "Optional Header"; + public override object Icon => Images.Images.MetadataTable; + public override string ToString() => "Optional Header"; + + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + { + language.WriteCommentLine(output, "Optional Header"); + MetadataTextWriter.WriteTable(language, output, BuildEntries()); + } + + IReadOnlyList BuildEntries() + { + var headers = module.Reader.PEHeaders; + var reader = module.Reader.GetEntireImage().GetReader(headers.PEHeaderStartOffset, 128); + var header = headers.PEHeader!; + bool isPE32Plus = header.Magic == PEMagic.PE32Plus; + + var entries = new List(); + entries.Add(new Entry(headers.PEHeaderStartOffset + reader.Offset, reader.ReadUInt16(), 2, "Magic", header.Magic.ToString())); + entries.Add(new Entry(headers.PEHeaderStartOffset + reader.Offset, reader.ReadByte(), 1, "Major Linker Version", "")); + entries.Add(new Entry(headers.PEHeaderStartOffset + reader.Offset, reader.ReadByte(), 1, "Minor Linker Version", "")); + entries.Add(new Entry(headers.PEHeaderStartOffset + reader.Offset, reader.ReadInt32(), 4, "Code Size", "Size of the code (text) section, or the sum of all code sections if there are multiple sections.")); + entries.Add(new Entry(headers.PEHeaderStartOffset + reader.Offset, reader.ReadInt32(), 4, "Initialized Data Size", "Size of the initialized data section, or the sum of all initialized data sections if there are multiple data sections.")); + entries.Add(new Entry(headers.PEHeaderStartOffset + reader.Offset, reader.ReadInt32(), 4, "Uninitialized Data Size", "Size of the uninitialized data section, or the sum of all uninitialized data sections if there are multiple uninitialized data sections.")); + entries.Add(new Entry(headers.PEHeaderStartOffset + reader.Offset, reader.ReadInt32(), 4, "Entry Point RVA", "RVA of entry point, needs to point to bytes 0xFF 0x25 followed by the RVA in a section marked execute / read for EXEs or 0 for DLLs")); + entries.Add(new Entry(headers.PEHeaderStartOffset + reader.Offset, reader.ReadInt32(), 4, "Base Of Code", "RVA of the code section.")); + entries.Add(new Entry(isPE32Plus ? 0 : headers.PEHeaderStartOffset + reader.Offset, isPE32Plus ? 0UL : reader.ReadUInt32(), isPE32Plus ? 0 : 4, "Base Of Data", "PE32 only (not present in PE32Plus): RVA of the data section, relative to the Image Base.")); + entries.Add(new Entry(headers.PEHeaderStartOffset + reader.Offset, isPE32Plus ? reader.ReadUInt64() : reader.ReadUInt32(), isPE32Plus ? 8 : 4, "Image Base", "Shall be a multiple of 0x10000.")); + entries.Add(new Entry(headers.PEHeaderStartOffset + reader.Offset, reader.ReadInt32(), 4, "Section Alignment", "Shall be greater than File Alignment.")); + entries.Add(new Entry(headers.PEHeaderStartOffset + reader.Offset, reader.ReadInt32(), 4, "File Alignment", "")); + entries.Add(new Entry(headers.PEHeaderStartOffset + reader.Offset, reader.ReadUInt16(), 2, "Major OS Version", "")); + entries.Add(new Entry(headers.PEHeaderStartOffset + reader.Offset, reader.ReadUInt16(), 2, "Minor OS Version", "")); + entries.Add(new Entry(headers.PEHeaderStartOffset + reader.Offset, reader.ReadUInt16(), 2, "Major Image Version", "")); + entries.Add(new Entry(headers.PEHeaderStartOffset + reader.Offset, reader.ReadUInt16(), 2, "Minor Image Version", "")); + entries.Add(new Entry(headers.PEHeaderStartOffset + reader.Offset, reader.ReadUInt16(), 2, "Major Subsystem Version", "")); + entries.Add(new Entry(headers.PEHeaderStartOffset + reader.Offset, reader.ReadUInt16(), 2, "Minor Subsystem Version", "")); + entries.Add(new Entry(headers.PEHeaderStartOffset + reader.Offset, reader.ReadUInt32(), 4, "Win32VersionValue", "")); + entries.Add(new Entry(headers.PEHeaderStartOffset + reader.Offset, reader.ReadInt32(), 4, "Image Size", "Size, in bytes, of image, including all headers and padding; shall be a multiple of Section Alignment.")); + entries.Add(new Entry(headers.PEHeaderStartOffset + reader.Offset, reader.ReadInt32(), 4, "Header Size", "Combined size of MS-DOS Header, PE Header, PE Optional Header and padding; shall be a multiple of the file alignment.")); + entries.Add(new Entry(headers.PEHeaderStartOffset + reader.Offset, reader.ReadInt32(), 4, "File Checksum", "")); + entries.Add(new Entry(headers.PEHeaderStartOffset + reader.Offset, reader.ReadUInt16(), 2, "Subsystem", header.Subsystem.ToString())); + ushort dllCharacteristics = reader.ReadUInt16(); + entries.Add(new Entry(headers.PEHeaderStartOffset + reader.Offset - 2, dllCharacteristics, 2, "DLL Characteristics", header.DllCharacteristics.ToString(), new BitEntry[] { + new((dllCharacteristics & 0x0001) != 0, "<0001> Process Init (Reserved)"), + new((dllCharacteristics & 0x0002) != 0, "<0002> Process Term (Reserved)"), + new((dllCharacteristics & 0x0004) != 0, "<0004> Thread Init (Reserved)"), + new((dllCharacteristics & 0x0008) != 0, "<0008> Thread Term (Reserved)"), + new((dllCharacteristics & 0x0010) != 0, "<0010> Unused"), + new((dllCharacteristics & 0x0020) != 0, "<0020> Image can handle a high entropy 64-bit virtual address space (ASLR)"), + new((dllCharacteristics & 0x0040) != 0, "<0040> DLL can be relocated at load time"), + new((dllCharacteristics & 0x0080) != 0, "<0080> Code integrity checks are enforced"), + new((dllCharacteristics & 0x0100) != 0, "<0100> Image is NX compatible"), + new((dllCharacteristics & 0x0200) != 0, "<0200> Isolation aware, but do not isolate the image"), + new((dllCharacteristics & 0x0400) != 0, "<0400> Does not use structured exception handling (SEH)"), + new((dllCharacteristics & 0x0800) != 0, "<0800> Do not bind the image"), + new((dllCharacteristics & 0x1000) != 0, "<1000> Image must execute in an AppContainer"), + new((dllCharacteristics & 0x2000) != 0, "<2000> Driver is a WDM Driver"), + new((dllCharacteristics & 0x4000) != 0, "<4000> Image supports Control Flow Guard"), + new((dllCharacteristics & 0x8000) != 0, "<8000> Image is Terminal Server aware"), + })); + entries.Add(new Entry(headers.PEHeaderStartOffset + reader.Offset, isPE32Plus ? reader.ReadUInt64() : reader.ReadUInt32(), isPE32Plus ? 8 : 4, "Stack Reserve Size", "")); + entries.Add(new Entry(headers.PEHeaderStartOffset + reader.Offset, isPE32Plus ? reader.ReadUInt64() : reader.ReadUInt32(), isPE32Plus ? 8 : 4, "Stack Commit Size", "")); + entries.Add(new Entry(headers.PEHeaderStartOffset + reader.Offset, isPE32Plus ? reader.ReadUInt64() : reader.ReadUInt32(), isPE32Plus ? 8 : 4, "Heap Reserve Size", "")); + entries.Add(new Entry(headers.PEHeaderStartOffset + reader.Offset, isPE32Plus ? reader.ReadUInt64() : reader.ReadUInt32(), isPE32Plus ? 8 : 4, "Heap Commit Size", "")); + entries.Add(new Entry(headers.PEHeaderStartOffset + reader.Offset, reader.ReadUInt32(), 4, "Loader Flags", "")); + entries.Add(new Entry(headers.PEHeaderStartOffset + reader.Offset, reader.ReadInt32(), 4, "Number of Data Directories", "")); + return entries; + } + } +}