Browse Source

Surface pe header tree under metadata folder

Adds the five PE-format leaves (DOS / COFF / Optional / DataDirectories
/ DebugDirectory) under each assembly's Metadata folder. Phase 1 renders
them as fixed-width text tables via Decompile; Phase 2 will swap to a
DataGrid tab. Introduces Entry / BitEntry / ColumnInfoAttribute and a
MetadataTextWriter helper that the heap and table nodes reuse.

Assisted-by: Claude:claude-opus-4-7:Claude Code
pull/3755/head
Siegfried Pammer 2 months ago
parent
commit
e86f68a13e
  1. 127
      ILSpy.Tests/Metadata/PEHeaderTreeTests.cs
  2. 88
      ILSpy/Metadata/CoffHeaderTreeNode.cs
  3. 97
      ILSpy/Metadata/DataDirectoriesTreeNode.cs
  4. 94
      ILSpy/Metadata/DebugDirectoryTreeNode.cs
  5. 92
      ILSpy/Metadata/DosHeaderTreeNode.cs
  6. 93
      ILSpy/Metadata/Helpers.cs
  7. 133
      ILSpy/Metadata/MetadataTextWriter.cs
  8. 13
      ILSpy/Metadata/MetadataTreeNode.cs
  9. 114
      ILSpy/Metadata/OptionalHeaderTreeNode.cs

127
ILSpy.Tests/Metadata/PEHeaderTreeTests.cs

@ -0,0 +1,127 @@ @@ -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<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();
metadataNode.Children.OfType<DosHeaderTreeNode>().Should().ContainSingle();
metadataNode.Children.OfType<CoffHeaderTreeNode>().Should().ContainSingle();
metadataNode.Children.OfType<OptionalHeaderTreeNode>().Should().ContainSingle();
metadataNode.Children.OfType<DataDirectoriesTreeNode>().Should().ContainSingle();
metadataNode.Children.OfType<DebugDirectoryTreeNode>().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<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 dosNode = metadataNode.Children.OfType<DosHeaderTreeNode>().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<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 coffNode = metadataNode.Children.OfType<CoffHeaderTreeNode>().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");
}
}

88
ILSpy/Metadata/CoffHeaderTreeNode.cs

@ -0,0 +1,88 @@ @@ -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
{
/// <summary>
/// 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.
/// </summary>
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<Entry> 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<Entry> {
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)"),
}),
};
}
}
}

97
ILSpy/Metadata/DataDirectoriesTreeNode.cs

@ -0,0 +1,97 @@ @@ -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
{
/// <summary>
/// 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, …).
/// </summary>
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<DataDirectoryEntry> BuildEntries()
{
var headers = module.Reader.PEHeaders;
var header = headers.PEHeader!;
return new List<DataDirectoryEntry> {
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 : "";
}
}
}
}

94
ILSpy/Metadata/DebugDirectoryTreeNode.cs

@ -0,0 +1,94 @@ @@ -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
{
/// <summary>
/// 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.
/// </summary>
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<DebugDirectoryEntryView> BuildEntries()
{
var entries = new List<DebugDirectoryEntryView>();
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;
}
}
}
}

92
ILSpy/Metadata/DosHeaderTreeNode.cs

@ -0,0 +1,92 @@ @@ -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
{
/// <summary>
/// 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.
/// </summary>
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<Entry> BuildEntries()
{
var reader = module.Reader.GetEntireImage().GetReader(0, 64);
var entries = new List<Entry>(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;
}
}
}

93
ILSpy/Metadata/Helpers.cs

@ -0,0 +1,93 @@ @@ -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
{
/// <summary>
/// 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 <c>Format</c>.
/// </summary>
public enum ColumnKind
{
Other,
HeapOffset,
Token,
}
/// <summary>
/// Annotates a row-shape property with rendering hints. <c>Format</c> is consumed by the
/// text writer; <c>Kind</c> and <c>LinkToTable</c> drive the DataGrid column factory in
/// Phase 2+ and are inert in Phase 1.
/// </summary>
[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;
}
}
/// <summary>
/// One row in a PE-header / metadata-table dump. <c>Value</c> is rendered hex-formatted
/// at <c>Size * 2</c> digits when numeric; <c>RowDetails</c> carries optional flag-bit
/// breakdowns that the DataGrid view (Phase 2) uses to populate row details. The text
/// path (Phase 1) ignores <c>RowDetails</c>.
/// </summary>
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<BitEntry>? RowDetails { get; }
public Entry(int offset, object? value, int size, string member, string meaning, IList<BitEntry>? rowDetails = null)
{
Member = member;
Offset = offset;
Size = size;
Value = value;
Meaning = meaning;
RowDetails = rowDetails;
}
}
/// <summary>One bit (or bit-group) entry inside a flags-Entry's row-details strip.</summary>
public sealed class BitEntry
{
public bool Value { get; }
public string Meaning { get; }
public BitEntry(bool value, string meaning)
{
Value = value;
Meaning = meaning;
}
}
}

133
ILSpy/Metadata/MetadataTextWriter.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;
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
{
/// <summary>
/// Renders a list of row objects to <see cref="ITextOutput"/> as a fixed-width table —
/// the Phase 1 stand-in for the DataGrid view. Column names come from
/// <see cref="PropertyInfo.Name"/>; cell formatting respects
/// <see cref="ColumnInfoAttribute.Format"/> and, for <see cref="Entry"/> rows, the
/// per-row hex-width implied by <see cref="Entry.Size"/>. Each emitted line goes through
/// <see cref="Language.WriteCommentLine"/> so the output sits cleanly inside the
/// existing comment-prefixed text pipeline.
/// </summary>
public static class MetadataTextWriter
{
public static void WriteTable<TRow>(Language language, ITextOutput output, IReadOnlyList<TRow> 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<BitEntry>)
&& p.Name != nameof(Entry.RowDetails))
.ToArray();
if (props.Length == 0)
return;
var formats = props.Select(p => p.GetCustomAttribute<ColumnInfoAttribute>()?.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() ?? "",
};
}
}
}

13
ILSpy/Metadata/MetadataTreeNode.cs

@ -93,9 +93,16 @@ namespace ILSpy.Metadata @@ -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.
}
}
}

114
ILSpy/Metadata/OptionalHeaderTreeNode.cs

@ -0,0 +1,114 @@ @@ -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
{
/// <summary>
/// PE Optional Header — image base, sizes, alignments, subsystem. Field widths flex
/// between PE32 and PE32+ (Image Base / Stack &amp; Heap reserves go from 4 → 8 bytes,
/// Base Of Data drops out on PE32+).
/// </summary>
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<Entry> 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<Entry>();
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;
}
}
}
Loading…
Cancel
Save