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