From 199b7bf15142a0f26d18dd2423c2b95610776680 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sat, 1 Aug 2026 21:33:18 +0200 Subject: [PATCH] Add --dump-table metadata dump mode to ilspycmd The GUI has the metadata-tables view; the CLI had nothing, so checking e.g. which MethodSemantics rows reference a Property row required a hand-written System.Reflection.Metadata script. --dump-table prints every row of a table (RID, token, resolved names, heap offsets, coded indexes) as an aligned text table, or as JSON with --json, for the same 39 Cor tables the GUI shows. Row enumeration for tables without public SRM row access lives in MetadataExtensions next to the existing GetMethodSemantics helper, so the GUI's raw-reading table nodes can be folded onto the shared readers later. Every table's columns are spelled out explicitly in ECMA-335 declaration order: reflecting over the SRM row structs would tie the output (and its column order) to runtime internals, and deterministic output is the point of the feature. JSON uses System.Text.Json from the shared framework, so no new package reference is needed. Assisted-by: Claude:claude-fable-5:Claude Code --- .../Metadata/MetadataExtensions.cs | 277 +++++++++ ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs | 33 ++ ICSharpCode.ILSpyCmd/MetadataTableDumper.cs | 544 ++++++++++++++++++ ICSharpCode.ILSpyCmd/README.md | 5 + 4 files changed, 859 insertions(+) create mode 100644 ICSharpCode.ILSpyCmd/MetadataTableDumper.cs diff --git a/ICSharpCode.Decompiler/Metadata/MetadataExtensions.cs b/ICSharpCode.Decompiler/Metadata/MetadataExtensions.cs index 7685cad7d..16366050e 100644 --- a/ICSharpCode.Decompiler/Metadata/MetadataExtensions.cs +++ b/ICSharpCode.Decompiler/Metadata/MetadataExtensions.cs @@ -357,6 +357,7 @@ namespace ICSharpCode.Decompiler.Metadata int methodSize = SimpleIndexSize(metadata, TableIndex.MethodDef); // HasSemantics coded index: 1 tag bit over Event (tag 0) and Property (tag 1). int assocSize = CodedIndexSize(metadata, 1, TableIndex.Event, TableIndex.Property); + CheckRowSize(metadata, TableIndex.MethodSemantics, 2 + methodSize + assocSize); for (int rid = 1; rid <= rowCount; rid++) { var semantics = (MethodSemanticsAttributes)reader.ReadUInt16(); @@ -400,6 +401,282 @@ namespace ICSharpCode.Decompiler.Metadata return 2; } + /// + /// Size in bytes of the FieldList/MethodList/ParamList member-range columns. SRM's rule + /// (MetadataReader.InitializeTableReaders): the column is wide when the *Ptr table itself + /// is large; otherwise the member table's row count governs the width, whether or not the + /// indirection is present. + /// + static int ListColumnSize(MetadataReader metadata, TableIndex ptrTable, TableIndex memberTable) + { + return SimpleIndexSize(metadata, ptrTable) > 2 ? 4 : SimpleIndexSize(metadata, memberTable); + } + + /// + /// Size in bytes of a #Strings heap index column. The width is declared by the HeapSizes + /// flags in the tables-stream header, not derived from the heap's size (a producer may set + /// the large-heap flag over a small heap). SRM does not expose the flags, but the ModuleRef + /// row consists of a single String column, so its computed row size is exactly this width. + /// + static int StringIndexSize(MetadataReader metadata) + { + return metadata.GetTableRowSize(TableIndex.ModuleRef); + } + + /// + /// Size in bytes of a #Blob heap index column (see ; the + /// TypeSpec row consists of a single Blob column). + /// + static int BlobIndexSize(MetadataReader metadata) + { + return metadata.GetTableRowSize(TableIndex.TypeSpec); + } + + /// + /// Verifies that the computed column widths sum to the row size SRM derived from the + /// tables-stream header. A width bug reads plausible-looking garbage rather than failing, + /// and some width rules are not reproducible through public API at all (EnC minimal-delta + /// metadata forces every table and coded index to 4 bytes), so fail loudly on mismatch. + /// + static void CheckRowSize(MetadataReader metadata, TableIndex table, int computedRowSize) + { + int actualRowSize = metadata.GetTableRowSize(table); + if (computedRowSize != actualRowSize) + throw new BadImageFormatException($"Unexpected {table} row size: computed {computedRowSize}, actual {actualRowSize}."); + } + + public static IEnumerable<(int PackingSize, uint ClassSize, TypeDefinitionHandle Parent)> GetClassLayouts(this MetadataReader metadata) + { + int rowCount = metadata.GetTableRowCount(TableIndex.ClassLayout); + var reader = metadata.AsBlobReader(); + reader.Offset = metadata.GetTableMetadataOffset(TableIndex.ClassLayout); + int typeDefSize = SimpleIndexSize(metadata, TableIndex.TypeDef); + CheckRowSize(metadata, TableIndex.ClassLayout, 2 + 4 + typeDefSize); + for (int rid = 1; rid <= rowCount; rid++) + { + ushort packingSize = reader.ReadUInt16(); + uint classSize = reader.ReadUInt32(); + int parentRow = typeDefSize == 2 ? reader.ReadUInt16() : reader.ReadInt32(); + yield return (packingSize, classSize, MetadataTokens.TypeDefinitionHandle(parentRow)); + } + } + + public static IEnumerable<(int Offset, FieldDefinitionHandle Field)> GetFieldLayoutRows(this MetadataReader metadata) + { + int rowCount = metadata.GetTableRowCount(TableIndex.FieldLayout); + var reader = metadata.AsBlobReader(); + reader.Offset = metadata.GetTableMetadataOffset(TableIndex.FieldLayout); + int fieldSize = SimpleIndexSize(metadata, TableIndex.Field); + CheckRowSize(metadata, TableIndex.FieldLayout, 4 + fieldSize); + for (int rid = 1; rid <= rowCount; rid++) + { + int offset = reader.ReadInt32(); + int fieldRow = fieldSize == 2 ? reader.ReadUInt16() : reader.ReadInt32(); + yield return (offset, MetadataTokens.FieldDefinitionHandle(fieldRow)); + } + } + + public static IEnumerable<(TypeDefinitionHandle Parent, EventDefinitionHandle EventList)> GetEventMaps(this MetadataReader metadata) + { + int rowCount = metadata.GetTableRowCount(TableIndex.EventMap); + var reader = metadata.AsBlobReader(); + reader.Offset = metadata.GetTableMetadataOffset(TableIndex.EventMap); + int typeDefSize = SimpleIndexSize(metadata, TableIndex.TypeDef); + int eventListSize = ListColumnSize(metadata, TableIndex.EventPtr, TableIndex.Event); + CheckRowSize(metadata, TableIndex.EventMap, typeDefSize + eventListSize); + for (int rid = 1; rid <= rowCount; rid++) + { + int parentRow = typeDefSize == 2 ? reader.ReadUInt16() : reader.ReadInt32(); + int eventListRow = eventListSize == 2 ? reader.ReadUInt16() : reader.ReadInt32(); + yield return (MetadataTokens.TypeDefinitionHandle(parentRow), MetadataTokens.EventDefinitionHandle(eventListRow)); + } + } + + public static IEnumerable<(TypeDefinitionHandle Parent, PropertyDefinitionHandle PropertyList)> GetPropertyMaps(this MetadataReader metadata) + { + int rowCount = metadata.GetTableRowCount(TableIndex.PropertyMap); + var reader = metadata.AsBlobReader(); + reader.Offset = metadata.GetTableMetadataOffset(TableIndex.PropertyMap); + int typeDefSize = SimpleIndexSize(metadata, TableIndex.TypeDef); + int propertyListSize = ListColumnSize(metadata, TableIndex.PropertyPtr, TableIndex.Property); + CheckRowSize(metadata, TableIndex.PropertyMap, typeDefSize + propertyListSize); + for (int rid = 1; rid <= rowCount; rid++) + { + int parentRow = typeDefSize == 2 ? reader.ReadUInt16() : reader.ReadInt32(); + int propertyListRow = propertyListSize == 2 ? reader.ReadUInt16() : reader.ReadInt32(); + yield return (MetadataTokens.TypeDefinitionHandle(parentRow), MetadataTokens.PropertyDefinitionHandle(propertyListRow)); + } + } + + public static IEnumerable<(TypeDefinitionHandle NestedClass, TypeDefinitionHandle EnclosingClass)> GetNestedClasses(this MetadataReader metadata) + { + int rowCount = metadata.GetTableRowCount(TableIndex.NestedClass); + var reader = metadata.AsBlobReader(); + reader.Offset = metadata.GetTableMetadataOffset(TableIndex.NestedClass); + int typeDefSize = SimpleIndexSize(metadata, TableIndex.TypeDef); + CheckRowSize(metadata, TableIndex.NestedClass, 2 * typeDefSize); + for (int rid = 1; rid <= rowCount; rid++) + { + int nestedRow = typeDefSize == 2 ? reader.ReadUInt16() : reader.ReadInt32(); + int enclosingRow = typeDefSize == 2 ? reader.ReadUInt16() : reader.ReadInt32(); + yield return (MetadataTokens.TypeDefinitionHandle(nestedRow), MetadataTokens.TypeDefinitionHandle(enclosingRow)); + } + } + + public static IEnumerable<(int RelativeVirtualAddress, FieldDefinitionHandle Field)> GetFieldRvas(this MetadataReader metadata) + { + int rowCount = metadata.GetTableRowCount(TableIndex.FieldRva); + var reader = metadata.AsBlobReader(); + reader.Offset = metadata.GetTableMetadataOffset(TableIndex.FieldRva); + int fieldSize = SimpleIndexSize(metadata, TableIndex.Field); + CheckRowSize(metadata, TableIndex.FieldRva, 4 + fieldSize); + for (int rid = 1; rid <= rowCount; rid++) + { + int rva = reader.ReadInt32(); + int fieldRow = fieldSize == 2 ? reader.ReadUInt16() : reader.ReadInt32(); + yield return (rva, MetadataTokens.FieldDefinitionHandle(fieldRow)); + } + } + + public static IEnumerable<(EntityHandle Parent, BlobHandle NativeType)> GetFieldMarshals(this MetadataReader metadata) + { + int rowCount = metadata.GetTableRowCount(TableIndex.FieldMarshal); + var reader = metadata.AsBlobReader(); + reader.Offset = metadata.GetTableMetadataOffset(TableIndex.FieldMarshal); + // HasFieldMarshal coded index: 1 tag bit over Field (tag 0) and Param (tag 1). + int parentSize = CodedIndexSize(metadata, 1, TableIndex.Field, TableIndex.Param); + int blobSize = BlobIndexSize(metadata); + CheckRowSize(metadata, TableIndex.FieldMarshal, parentSize + blobSize); + for (int rid = 1; rid <= rowCount; rid++) + { + int parentTag = parentSize == 2 ? reader.ReadUInt16() : reader.ReadInt32(); + int blobOffset = blobSize == 2 ? reader.ReadUInt16() : reader.ReadInt32(); + EntityHandle parent = (parentTag & 0x1) == 1 + ? MetadataTokens.ParameterHandle(parentTag >> 1) + : MetadataTokens.FieldDefinitionHandle(parentTag >> 1); + yield return (parent, MetadataTokens.BlobHandle(blobOffset)); + } + } + + public static IEnumerable<(System.Reflection.MethodImportAttributes MappingFlags, EntityHandle MemberForwarded, StringHandle ImportName, ModuleReferenceHandle ImportScope)> GetImplMaps(this MetadataReader metadata) + { + int rowCount = metadata.GetTableRowCount(TableIndex.ImplMap); + var reader = metadata.AsBlobReader(); + reader.Offset = metadata.GetTableMetadataOffset(TableIndex.ImplMap); + // MemberForwarded coded index: 1 tag bit over Field (tag 0) and MethodDef (tag 1). + int memberForwardedSize = CodedIndexSize(metadata, 1, TableIndex.Field, TableIndex.MethodDef); + int stringSize = StringIndexSize(metadata); + int moduleRefSize = SimpleIndexSize(metadata, TableIndex.ModuleRef); + CheckRowSize(metadata, TableIndex.ImplMap, 2 + memberForwardedSize + stringSize + moduleRefSize); + for (int rid = 1; rid <= rowCount; rid++) + { + var mappingFlags = (System.Reflection.MethodImportAttributes)reader.ReadUInt16(); + int memberForwardedTag = memberForwardedSize == 2 ? reader.ReadUInt16() : reader.ReadInt32(); + int importNameOffset = stringSize == 2 ? reader.ReadUInt16() : reader.ReadInt32(); + int importScopeRow = moduleRefSize == 2 ? reader.ReadUInt16() : reader.ReadInt32(); + EntityHandle memberForwarded = (memberForwardedTag & 0x1) == 1 + ? MetadataTokens.MethodDefinitionHandle(memberForwardedTag >> 1) + : MetadataTokens.FieldDefinitionHandle(memberForwardedTag >> 1); + yield return (mappingFlags, memberForwarded, MetadataTokens.StringHandle(importNameOffset), MetadataTokens.ModuleReferenceHandle(importScopeRow)); + } + } + + public static IEnumerable<(TypeDefinitionHandle Class, EntityHandle Interface)> GetInterfaceImplRows(this MetadataReader metadata) + { + int rowCount = metadata.GetTableRowCount(TableIndex.InterfaceImpl); + var reader = metadata.AsBlobReader(); + reader.Offset = metadata.GetTableMetadataOffset(TableIndex.InterfaceImpl); + int typeDefSize = SimpleIndexSize(metadata, TableIndex.TypeDef); + // TypeDefOrRef coded index: 2 tag bits over TypeDef (0), TypeRef (1) and TypeSpec (2). + int interfaceSize = CodedIndexSize(metadata, 2, TableIndex.TypeDef, TableIndex.TypeRef, TableIndex.TypeSpec); + CheckRowSize(metadata, TableIndex.InterfaceImpl, typeDefSize + interfaceSize); + for (int rid = 1; rid <= rowCount; rid++) + { + int classRow = typeDefSize == 2 ? reader.ReadUInt16() : reader.ReadInt32(); + int interfaceTag = interfaceSize == 2 ? reader.ReadUInt16() : reader.ReadInt32(); + EntityHandle iface = (interfaceTag & 0x3) switch { + 0 => MetadataTokens.TypeDefinitionHandle(interfaceTag >> 2), + 1 => MetadataTokens.TypeReferenceHandle(interfaceTag >> 2), + _ => MetadataTokens.TypeSpecificationHandle(interfaceTag >> 2), + }; + yield return (MetadataTokens.TypeDefinitionHandle(classRow), iface); + } + } + + /// + /// Enumerates one of the five *Ptr indirection tables (EventPtr, FieldPtr, MethodPtr, + /// ParamPtr, PropertyPtr), yielding the referenced entity of each row. + /// + public static IEnumerable GetPtrRows(this MetadataReader metadata, TableIndex ptrTable) + { + TableIndex referencedTable = ptrTable switch { + TableIndex.EventPtr => TableIndex.Event, + TableIndex.FieldPtr => TableIndex.Field, + TableIndex.MethodPtr => TableIndex.MethodDef, + TableIndex.ParamPtr => TableIndex.Param, + TableIndex.PropertyPtr => TableIndex.Property, + _ => throw new ArgumentOutOfRangeException(nameof(ptrTable)), + }; + int rowCount = metadata.GetTableRowCount(ptrTable); + var reader = metadata.AsBlobReader(); + reader.Offset = metadata.GetTableMetadataOffset(ptrTable); + int handleSize = SimpleIndexSize(metadata, referencedTable); + CheckRowSize(metadata, ptrTable, handleSize); + for (int rid = 1; rid <= rowCount; rid++) + { + int row = handleSize == 2 ? reader.ReadUInt16() : reader.ReadInt32(); + yield return MetadataTokens.EntityHandle(((int)referencedTable << 24) | row); + } + } + + /// + /// Reads the FieldList/MethodList columns of the TypeDef table, which SRM does not expose: + /// the stored value is the running list position (the next type's first member row, or one + /// past the member table's end), never 0. The columns sit at the end of the row, so they are + /// read relative to the row end to avoid re-deriving the widths of the preceding columns. + /// + public static IEnumerable<(TypeDefinitionHandle Type, int FieldList, int MethodList)> GetTypeDefListColumns(this MetadataReader metadata) + { + int fieldListSize = ListColumnSize(metadata, TableIndex.FieldPtr, TableIndex.Field); + int methodListSize = ListColumnSize(metadata, TableIndex.MethodPtr, TableIndex.MethodDef); + // Flags + Name + Namespace + Extends (TypeDefOrRef coded index) precede the list columns. + CheckRowSize(metadata, TableIndex.TypeDef, + 4 + 2 * StringIndexSize(metadata) + + CodedIndexSize(metadata, 2, TableIndex.TypeDef, TableIndex.TypeRef, TableIndex.TypeSpec) + + fieldListSize + methodListSize); + int rowSize = metadata.GetTableRowSize(TableIndex.TypeDef); + int tableOffset = metadata.GetTableMetadataOffset(TableIndex.TypeDef); + var reader = metadata.AsBlobReader(); + foreach (var handle in metadata.TypeDefinitions) + { + reader.Offset = tableOffset + rowSize * MetadataTokens.GetRowNumber(handle) - fieldListSize - methodListSize; + int fieldList = fieldListSize == 2 ? reader.ReadUInt16() : reader.ReadInt32(); + int methodList = methodListSize == 2 ? reader.ReadUInt16() : reader.ReadInt32(); + yield return (handle, fieldList, methodList); + } + } + + /// + /// Reads the ParamList column of the MethodDef table (see + /// for why SRM cannot provide it). + /// + public static IEnumerable<(MethodDefinitionHandle Method, int ParamList)> GetMethodDefParamLists(this MetadataReader metadata) + { + int paramListSize = ListColumnSize(metadata, TableIndex.ParamPtr, TableIndex.Param); + // RVA + ImplFlags + Flags + Name + Signature precede the ParamList column. + CheckRowSize(metadata, TableIndex.MethodDef, + 4 + 2 + 2 + StringIndexSize(metadata) + BlobIndexSize(metadata) + paramListSize); + int rowSize = metadata.GetTableRowSize(TableIndex.MethodDef); + int tableOffset = metadata.GetTableMetadataOffset(TableIndex.MethodDef); + var reader = metadata.AsBlobReader(); + foreach (var handle in metadata.MethodDefinitions) + { + reader.Offset = tableOffset + rowSize * MetadataTokens.GetRowNumber(handle) - paramListSize; + int paramList = paramListSize == 2 ? reader.ReadUInt16() : reader.ReadInt32(); + yield return (handle, paramList); + } + } + public static IEnumerable GetFieldLayouts(this MetadataReader metadata) { var rowCount = metadata.GetTableRowCount(TableIndex.FieldLayout); diff --git a/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs b/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs index 5968c22d4..7400f798e 100644 --- a/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs +++ b/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs @@ -136,6 +136,12 @@ Examples: [Option("--decompile-baml", "When used with -p, decompile BAML resources to XAML files (Page items) instead of leaving them as raw byte streams.", CommandOptionType.NoValue)] public bool DecompileBamlFlag { get; } + [Option("--dump-table ", "Dump a metadata table: prints RID, token, names, heap offsets and coded indexes of every row.
is the ECMA-335 table name (e.g. TypeDef, Property, MethodSemantics; case-insensitive) or table number (decimal or 0x-prefixed hex, e.g. 0x17).", CommandOptionType.SingleValue)] + public string DumpTableName { get; } + + [Option("--json", "Output as JSON. Currently only supported together with --dump-table.", CommandOptionType.NoValue)] + public bool JsonOutputFlag { get; } + public string DecompilerVersion => "ilspycmd: " + typeof(ILSpyCmdProgram).Assembly.GetName().Version.ToString() + Environment.NewLine + "ICSharpCode.Decompiler: " + @@ -234,6 +240,12 @@ Examples: Directory.CreateDirectory(outputDirectory); } + if (JsonOutputFlag && DumpTableName == null) + { + app.Error.WriteLine("The --json option is currently only supported together with --dump-table."); + return ProgramExitCodes.EX_USAGE; + } + try { if (CreateCompilableProjectFlag) @@ -363,6 +375,27 @@ Examples: { return ExtractResource(fileName, ResourceName, output, outputDirectory, app); } + else if (DumpTableName != null) + { + if (!MetadataTableDumper.TryParseTableName(DumpTableName, out var table)) + { + app.Error.WriteLine($"Unknown metadata table '{DumpTableName}'."); + app.Error.WriteLine($"Supported tables: {MetadataTableDumper.SupportedTableNames}"); + return ProgramExitCodes.EX_USAGE; + } + + if (outputDirectory != null) + { + // per-file writer, disposed here: the shared 'output' is only closed once + // at the end of the run, which would lose the buffered tail of every file + // but the last when dumping multiple assemblies + string outputName = Path.GetFileNameWithoutExtension(fileName); + using var tableOutput = File.CreateText(Path.Combine(outputDirectory, outputName) + $".{table}.{(JsonOutputFlag ? "json" : "txt")}"); + return MetadataTableDumper.DumpTable(fileName, tableOutput, table, JsonOutputFlag); + } + + return MetadataTableDumper.DumpTable(fileName, output, table, JsonOutputFlag); + } else { if (MemberIdString != null && TypeName != null) diff --git a/ICSharpCode.ILSpyCmd/MetadataTableDumper.cs b/ICSharpCode.ILSpyCmd/MetadataTableDumper.cs new file mode 100644 index 000000000..6e7666819 --- /dev/null +++ b/ICSharpCode.ILSpyCmd/MetadataTableDumper.cs @@ -0,0 +1,544 @@ +// Copyright (c) 2026 Siegfried Pammer +// +// 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.IO; +using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Text.Json; + +using ICSharpCode.Decompiler.Metadata; + +namespace ICSharpCode.ILSpyCmd +{ + /// + /// Implements the --dump-table mode: prints every row of a metadata table (RID, token, + /// resolved names, heap offsets and coded indexes) as an aligned console table or as JSON. + /// The columns of every table are spelled out explicitly (in ECMA-335 declaration order) + /// so the output stays deterministic across runtime versions. + /// + internal static class MetadataTableDumper + { + // The Cor tables the GUI's metadata view supports; EnC and Portable-PDB tables are out of scope. + static readonly TableIndex[] supportedTables = { + TableIndex.Module, TableIndex.TypeRef, TableIndex.TypeDef, TableIndex.FieldPtr, + TableIndex.Field, TableIndex.MethodPtr, TableIndex.MethodDef, TableIndex.ParamPtr, + TableIndex.Param, TableIndex.InterfaceImpl, TableIndex.MemberRef, TableIndex.Constant, + TableIndex.CustomAttribute, TableIndex.FieldMarshal, TableIndex.DeclSecurity, + TableIndex.ClassLayout, TableIndex.FieldLayout, TableIndex.StandAloneSig, + TableIndex.EventMap, TableIndex.EventPtr, TableIndex.Event, TableIndex.PropertyMap, + TableIndex.PropertyPtr, TableIndex.Property, TableIndex.MethodSemantics, + TableIndex.MethodImpl, TableIndex.ModuleRef, TableIndex.TypeSpec, TableIndex.ImplMap, + TableIndex.FieldRva, TableIndex.Assembly, TableIndex.AssemblyRef, TableIndex.File, + TableIndex.ExportedType, TableIndex.ManifestResource, TableIndex.NestedClass, + TableIndex.GenericParam, TableIndex.MethodSpec, TableIndex.GenericParamConstraint, + }; + + public static string SupportedTableNames => string.Join(", ", + supportedTables.Select(t => $"{t} (0x{(int)t:X2})")); + + public static bool TryParseTableName(string name, out TableIndex table) + { + // accept the ECMA-335 table number, decimal or 0x-prefixed hex, as an + // alternative to the table name + bool isNumber = name.StartsWith("0x", StringComparison.OrdinalIgnoreCase) + ? int.TryParse(name.AsSpan(2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int number) + : int.TryParse(name, NumberStyles.None, CultureInfo.InvariantCulture, out number); + if (isNumber) + { + table = (TableIndex)number; + return supportedTables.Contains(table); + } + return Enum.TryParse(name, ignoreCase: true, out table) + && supportedTables.Contains(table); + } + + public static int DumpTable(string assemblyFileName, TextWriter output, TableIndex table, bool asJson) + { + using var module = new PEFile(assemblyFileName); + var metadata = module.Metadata; + var rows = LoadRows(metadata, table); + if (asJson) + { + WriteJson(output, assemblyFileName, table, rows); + } + else + { + WriteConsoleTable(output, rows); + } + return 0; + } + + /// + /// A row is an ordered list of (column, value) pairs; values are either int (RID) + /// or already-formatted strings, so the console and JSON writers stay in sync. + /// + static List> LoadRows(MetadataReader metadata, TableIndex table) + { + var rows = new List>(); + int rid = 0; + + switch (table) + { + case TableIndex.Module: + if (metadata.GetTableRowCount(TableIndex.Module) > 0) + { + var module = metadata.GetModuleDefinition(); + rows.Add(Row(metadata, 1, MetadataTokens.EntityHandle(TableIndex.Module, 1), + ("Generation", module.Generation), + ("Name", module.Name), + ("Mvid", module.Mvid), + ("GenerationId", module.GenerationId), + ("BaseGenerationId", module.BaseGenerationId))); + } + break; + case TableIndex.TypeRef: + foreach (var h in metadata.TypeReferences) + { + var typeRef = metadata.GetTypeReference(h); + rows.Add(Row(metadata, MetadataTokens.GetRowNumber(h), h, + ("ResolutionScope", typeRef.ResolutionScope), + ("Name", typeRef.Name), + ("Namespace", typeRef.Namespace))); + } + break; + case TableIndex.TypeDef: + var listColumns = metadata.GetTypeDefListColumns().ToDictionary(r => r.Type, r => (r.FieldList, r.MethodList)); + foreach (var h in metadata.TypeDefinitions) + { + var typeDef = metadata.GetTypeDefinition(h); + rows.Add(Row(metadata, MetadataTokens.GetRowNumber(h), h, + ("Attributes", typeDef.Attributes), + ("Name", typeDef.Name), + ("Namespace", typeDef.Namespace), + ("BaseType", typeDef.BaseType), + ("FieldList", FormatRid(listColumns[h].FieldList)), + ("MethodList", FormatRid(listColumns[h].MethodList)))); + } + break; + case TableIndex.Field: + foreach (var h in metadata.FieldDefinitions) + { + var field = metadata.GetFieldDefinition(h); + rows.Add(Row(metadata, MetadataTokens.GetRowNumber(h), h, + ("Attributes", field.Attributes), + ("Name", field.Name), + ("Signature", field.Signature))); + } + break; + case TableIndex.MethodDef: + var paramLists = metadata.GetMethodDefParamLists().ToDictionary(r => r.Method, r => r.ParamList); + foreach (var h in metadata.MethodDefinitions) + { + var method = metadata.GetMethodDefinition(h); + rows.Add(Row(metadata, MetadataTokens.GetRowNumber(h), h, + ("RVA", FormatHex(method.RelativeVirtualAddress)), + ("ImplAttributes", method.ImplAttributes), + ("Attributes", method.Attributes), + ("Name", method.Name), + ("Signature", method.Signature), + ("ParamList", FormatRid(paramLists[h])))); + } + break; + case TableIndex.Param: + foreach (int r in RidRange(metadata, table)) + { + var param = metadata.GetParameter(MetadataTokens.ParameterHandle(r)); + rows.Add(Row(metadata, r, MetadataTokens.EntityHandle(table, r), + ("Attributes", param.Attributes), + ("SequenceNumber", param.SequenceNumber), + ("Name", param.Name))); + } + break; + case TableIndex.InterfaceImpl: + foreach (var (cls, iface) in metadata.GetInterfaceImplRows()) + rows.Add(Row(metadata, ++rid, table, ("Class", cls), ("Interface", iface))); + break; + case TableIndex.MemberRef: + foreach (var h in metadata.MemberReferences) + { + var memberRef = metadata.GetMemberReference(h); + rows.Add(Row(metadata, MetadataTokens.GetRowNumber(h), h, + ("Parent", memberRef.Parent), + ("Name", memberRef.Name), + ("Signature", memberRef.Signature))); + } + break; + case TableIndex.Constant: + foreach (int r in RidRange(metadata, table)) + { + var constant = metadata.GetConstant(MetadataTokens.ConstantHandle(r)); + rows.Add(Row(metadata, r, MetadataTokens.EntityHandle(table, r), + ("TypeCode", constant.TypeCode), + ("Parent", constant.Parent), + ("Value", constant.Value))); + } + break; + case TableIndex.CustomAttribute: + foreach (var h in metadata.CustomAttributes) + { + var attribute = metadata.GetCustomAttribute(h); + rows.Add(Row(metadata, MetadataTokens.GetRowNumber(h), h, + ("Parent", attribute.Parent), + ("Constructor", attribute.Constructor), + ("Value", attribute.Value))); + } + break; + case TableIndex.FieldMarshal: + foreach (var (parent, nativeType) in metadata.GetFieldMarshals()) + rows.Add(Row(metadata, ++rid, table, ("Parent", parent), ("NativeType", nativeType))); + break; + case TableIndex.DeclSecurity: + foreach (var h in metadata.DeclarativeSecurityAttributes) + { + var declSecurity = metadata.GetDeclarativeSecurityAttribute(h); + rows.Add(Row(metadata, MetadataTokens.GetRowNumber(h), h, + ("Action", declSecurity.Action), + ("Parent", declSecurity.Parent), + ("PermissionSet", declSecurity.PermissionSet))); + } + break; + case TableIndex.ClassLayout: + foreach (var (packingSize, classSize, parent) in metadata.GetClassLayouts()) + rows.Add(Row(metadata, ++rid, table, ("PackingSize", packingSize), ("ClassSize", classSize), ("Parent", parent))); + break; + case TableIndex.FieldLayout: + foreach (var (offset, field) in metadata.GetFieldLayoutRows()) + rows.Add(Row(metadata, ++rid, table, ("Offset", offset), ("Field", field))); + break; + case TableIndex.StandAloneSig: + foreach (int r in RidRange(metadata, table)) + { + var signature = metadata.GetStandaloneSignature(MetadataTokens.StandaloneSignatureHandle(r)); + rows.Add(Row(metadata, r, MetadataTokens.EntityHandle(table, r), + ("Signature", signature.Signature))); + } + break; + case TableIndex.EventMap: + foreach (var (parent, eventList) in metadata.GetEventMaps()) + rows.Add(Row(metadata, ++rid, table, ("Parent", parent), ("EventList", eventList))); + break; + case TableIndex.Event: + foreach (var h in metadata.EventDefinitions) + { + var eventDef = metadata.GetEventDefinition(h); + rows.Add(Row(metadata, MetadataTokens.GetRowNumber(h), h, + ("Attributes", eventDef.Attributes), + ("Name", eventDef.Name), + ("Type", eventDef.Type))); + } + break; + case TableIndex.PropertyMap: + foreach (var (parent, propertyList) in metadata.GetPropertyMaps()) + rows.Add(Row(metadata, ++rid, table, ("Parent", parent), ("PropertyList", propertyList))); + break; + case TableIndex.Property: + foreach (var h in metadata.PropertyDefinitions) + { + var property = metadata.GetPropertyDefinition(h); + rows.Add(Row(metadata, MetadataTokens.GetRowNumber(h), h, + ("Attributes", property.Attributes), + ("Name", property.Name), + ("Signature", property.Signature))); + } + break; + case TableIndex.MethodSemantics: + foreach (var (_, semantics, method, association) in metadata.GetMethodSemantics()) + rows.Add(Row(metadata, ++rid, table, ("Semantics", semantics), ("Method", method), ("Association", association))); + break; + case TableIndex.MethodImpl: + foreach (int r in RidRange(metadata, table)) + { + var methodImpl = metadata.GetMethodImplementation(MetadataTokens.MethodImplementationHandle(r)); + rows.Add(Row(metadata, r, MetadataTokens.EntityHandle(table, r), + ("Class", methodImpl.Type), + ("MethodBody", methodImpl.MethodBody), + ("MethodDeclaration", methodImpl.MethodDeclaration))); + } + break; + case TableIndex.ModuleRef: + foreach (var h in metadata.GetModuleReferences()) + { + var moduleRef = metadata.GetModuleReference(h); + rows.Add(Row(metadata, MetadataTokens.GetRowNumber(h), h, + ("Name", moduleRef.Name))); + } + break; + case TableIndex.TypeSpec: + foreach (var h in metadata.GetTypeSpecifications()) + { + var typeSpec = metadata.GetTypeSpecification(h); + rows.Add(Row(metadata, MetadataTokens.GetRowNumber(h), h, + ("Signature", typeSpec.Signature))); + } + break; + case TableIndex.ImplMap: + foreach (var (mappingFlags, memberForwarded, importName, importScope) in metadata.GetImplMaps()) + rows.Add(Row(metadata, ++rid, table, ("MappingFlags", mappingFlags), ("MemberForwarded", memberForwarded), ("ImportName", importName), ("ImportScope", importScope))); + break; + case TableIndex.FieldRva: + foreach (var (rva, field) in metadata.GetFieldRvas()) + rows.Add(Row(metadata, ++rid, table, ("RVA", FormatHex(rva)), ("Field", field))); + break; + case TableIndex.Assembly: + if (metadata.IsAssembly) + { + var assembly = metadata.GetAssemblyDefinition(); + rows.Add(Row(metadata, 1, MetadataTokens.EntityHandle(TableIndex.Assembly, 1), + ("HashAlgorithm", assembly.HashAlgorithm), + ("Version", assembly.Version), + ("Flags", assembly.Flags), + ("PublicKey", assembly.PublicKey), + ("Name", assembly.Name), + ("Culture", assembly.Culture))); + } + break; + case TableIndex.AssemblyRef: + foreach (var h in metadata.AssemblyReferences) + { + var assemblyRef = metadata.GetAssemblyReference(h); + rows.Add(Row(metadata, MetadataTokens.GetRowNumber(h), h, + ("Version", assemblyRef.Version), + ("Flags", assemblyRef.Flags), + ("PublicKeyOrToken", assemblyRef.PublicKeyOrToken), + ("Name", assemblyRef.Name), + ("Culture", assemblyRef.Culture), + ("HashValue", assemblyRef.HashValue))); + } + break; + case TableIndex.File: + foreach (var h in metadata.AssemblyFiles) + { + var file = metadata.GetAssemblyFile(h); + rows.Add(Row(metadata, MetadataTokens.GetRowNumber(h), h, + ("ContainsMetadata", file.ContainsMetadata), + ("Name", file.Name), + ("HashValue", file.HashValue))); + } + break; + case TableIndex.ExportedType: + foreach (var h in metadata.ExportedTypes) + { + var exportedType = metadata.GetExportedType(h); + rows.Add(Row(metadata, MetadataTokens.GetRowNumber(h), h, + ("Attributes", exportedType.Attributes), + ("Name", exportedType.Name), + ("Namespace", exportedType.Namespace), + ("Implementation", exportedType.Implementation), + ("IsForwarder", exportedType.IsForwarder))); + } + break; + case TableIndex.ManifestResource: + foreach (var h in metadata.ManifestResources) + { + var resource = metadata.GetManifestResource(h); + rows.Add(Row(metadata, MetadataTokens.GetRowNumber(h), h, + ("Offset", resource.Offset), + ("Attributes", resource.Attributes), + ("Name", resource.Name), + ("Implementation", resource.Implementation))); + } + break; + case TableIndex.NestedClass: + foreach (var (nested, enclosing) in metadata.GetNestedClasses()) + rows.Add(Row(metadata, ++rid, table, ("NestedClass", nested), ("EnclosingClass", enclosing))); + break; + case TableIndex.GenericParam: + foreach (int r in RidRange(metadata, table)) + { + var genericParam = metadata.GetGenericParameter(MetadataTokens.GenericParameterHandle(r)); + rows.Add(Row(metadata, r, MetadataTokens.EntityHandle(table, r), + ("Number", genericParam.Index), + ("Attributes", genericParam.Attributes), + ("Owner", genericParam.Parent), + ("Name", genericParam.Name))); + } + break; + case TableIndex.MethodSpec: + foreach (var h in metadata.GetMethodSpecifications()) + { + var methodSpec = metadata.GetMethodSpecification(h); + rows.Add(Row(metadata, MetadataTokens.GetRowNumber(h), h, + ("Method", methodSpec.Method), + ("Instantiation", methodSpec.Signature))); + } + break; + case TableIndex.GenericParamConstraint: + foreach (int r in RidRange(metadata, table)) + { + var constraint = metadata.GetGenericParameterConstraint(MetadataTokens.GenericParameterConstraintHandle(r)); + rows.Add(Row(metadata, r, MetadataTokens.EntityHandle(table, r), + ("Owner", (EntityHandle)constraint.Parameter), + ("Constraint", constraint.Type))); + } + break; + case TableIndex.EventPtr: + case TableIndex.FieldPtr: + case TableIndex.MethodPtr: + case TableIndex.ParamPtr: + case TableIndex.PropertyPtr: + foreach (var target in metadata.GetPtrRows(table)) + rows.Add(Row(metadata, ++rid, table, ("Target", target))); + break; + default: + throw new InvalidOperationException($"Table {table} is listed as supported but has no loader."); + } + return rows; + } + + static IEnumerable RidRange(MetadataReader metadata, TableIndex table) + { + return Enumerable.Range(1, metadata.GetTableRowCount(table)); + } + + static List<(string, object)> Row(MetadataReader metadata, int rid, EntityHandle handle, params (string Column, object Value)[] columns) + { + return BuildRow(metadata, rid, MetadataTokens.GetToken(handle), columns); + } + + static List<(string, object)> Row(MetadataReader metadata, int rid, TableIndex table, params (string Column, object Value)[] columns) + { + // tables without an SRM handle type get their token synthesized from table id and RID + return BuildRow(metadata, rid, ((int)table << 24) | rid, columns); + } + + static List<(string, object)> BuildRow(MetadataReader metadata, int rid, int token, (string Column, object Value)[] columns) + { + var row = new List<(string, object)> { + ("RID", rid), + ("Token", FormatHex(token)), + }; + foreach (var (column, value) in columns) + { + row.Add((column, FormatValue(metadata, value))); + } + return row; + } + + static string FormatValue(MetadataReader metadata, object value) + { + switch (value) + { + case null: + return ""; + case string s: + return s; + case StringHandle sh: + return sh.IsNil ? "" : metadata.GetString(sh); + case BlobHandle bh: + return bh.IsNil ? "nil" : FormatHex(MetadataTokens.GetHeapOffset(bh)); + case GuidHandle gh: + return gh.IsNil ? "nil" : metadata.GetGuid(gh).ToString(); + case EntityHandle eh: + return eh.IsNil ? "nil" : FormatHex(MetadataTokens.GetToken(eh)); + // The concrete handle cases below are not dead code: pattern matching tests the + // runtime type of the boxed value and ignores SRM's user-defined conversions, so + // a boxed MethodDefinitionHandle does not match the EntityHandle case above. + case MethodDefinitionHandle mdh: + return FormatValue(metadata, (EntityHandle)mdh); + case FieldDefinitionHandle fdh: + return FormatValue(metadata, (EntityHandle)fdh); + case TypeDefinitionHandle tdh: + return FormatValue(metadata, (EntityHandle)tdh); + case EventDefinitionHandle edh: + return FormatValue(metadata, (EntityHandle)edh); + case PropertyDefinitionHandle pdh: + return FormatValue(metadata, (EntityHandle)pdh); + case ModuleReferenceHandle mrh: + return FormatValue(metadata, (EntityHandle)mrh); + case Enum e: + return e.ToString(); + case bool b: + return b ? "true" : "false"; + case Version v: + return v.ToString(); + default: + // Reject unlisted handle types instead of letting Convert.ToString render + // them as their type name; cast handles to EntityHandle at the call site. + if (value.GetType().Namespace == "System.Reflection.Metadata") + throw new InvalidOperationException($"Unhandled metadata handle type {value.GetType().Name}."); + return Convert.ToString(value, CultureInfo.InvariantCulture) ?? ""; + } + } + + static string FormatHex(int value) + { + return "0x" + value.ToString("X8", CultureInfo.InvariantCulture); + } + + static string FormatRid(int value) + { + return value.ToString(CultureInfo.InvariantCulture); + } + + static void WriteConsoleTable(TextWriter output, List> rows) + { + if (rows.Count == 0) + { + output.WriteLine("0 rows"); + return; + } + var columns = rows[0].Select(c => c.Column).ToArray(); + int[] widths = columns.Select(c => c.Length).ToArray(); + foreach (var row in rows) + { + for (int i = 0; i < row.Count; i++) + { + widths[i] = Math.Max(widths[i], Convert.ToString(row[i].Value, CultureInfo.InvariantCulture)!.Length); + } + } + output.WriteLine(string.Join(" ", columns.Select((c, i) => c.PadRight(widths[i]))).TrimEnd()); + output.WriteLine(string.Join(" ", widths.Select(w => new string('-', w)))); + foreach (var row in rows) + { + output.WriteLine(string.Join(" ", row.Select((c, i) => Convert.ToString(c.Value, CultureInfo.InvariantCulture)!.PadRight(widths[i]))).TrimEnd()); + } + } + + static void WriteJson(TextWriter output, string assemblyFileName, TableIndex table, List> rows) + { + using var stream = new MemoryStream(); + using (var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true })) + { + writer.WriteStartObject(); + writer.WriteString("assembly", assemblyFileName); + writer.WriteString("table", table.ToString()); + writer.WriteNumber("rowCount", rows.Count); + writer.WriteStartArray("rows"); + foreach (var row in rows) + { + writer.WriteStartObject(); + foreach (var (column, value) in row) + { + if (value is int i) + writer.WriteNumber(column, i); + else + writer.WriteString(column, (string)value); + } + writer.WriteEndObject(); + } + writer.WriteEndArray(); + writer.WriteEndObject(); + } + output.WriteLine(System.Text.Encoding.UTF8.GetString(stream.ToArray())); + } + } +} diff --git a/ICSharpCode.ILSpyCmd/README.md b/ICSharpCode.ILSpyCmd/README.md index b93761c27..c32cef8b0 100644 --- a/ICSharpCode.ILSpyCmd/README.md +++ b/ICSharpCode.ILSpyCmd/README.md @@ -41,6 +41,11 @@ Options: whose name ends with '.baml' are decompiled to XAML. --decompile-baml When used with -p, decompile BAML resources to XAML files (Page items) instead of leaving them as raw byte streams. + --dump-table
Dump a metadata table: prints RID, token, names, heap offsets and coded + indexes of every row.
is the ECMA-335 table name (e.g. TypeDef, + Property, MethodSemantics; case-insensitive) or table number (decimal or + 0x-prefixed hex, e.g. 0x17). + --json Output as JSON. Currently only supported together with --dump-table. -lv|--languageversion C# Language version: CSharp1, CSharp2, CSharp3, CSharp4, CSharp5, CSharp6, CSharp7, CSharp7_1, CSharp7_2, CSharp7_3, CSharp8_0, CSharp9_0, CSharp10_0, CSharp11_0, CSharp12_0, CSharp13_0, Preview or Latest