diff --git a/ICSharpCode.ILSpyCmd.Tests/DumpPdbOptionTests.cs b/ICSharpCode.ILSpyCmd.Tests/DumpPdbOptionTests.cs
new file mode 100644
index 000000000..e1968a995
--- /dev/null
+++ b/ICSharpCode.ILSpyCmd.Tests/DumpPdbOptionTests.cs
@@ -0,0 +1,107 @@
+// 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.IO;
+using System.Linq;
+using System.Text.Json;
+using System.Threading.Tasks;
+
+using NUnit.Framework;
+
+using static ICSharpCode.ILSpyCmd.Tests.CliTestRunner;
+
+namespace ICSharpCode.ILSpyCmd.Tests
+{
+ [TestFixture]
+ public class DumpPdbOptionTests
+ {
+ static readonly string testAssemblyPath = typeof(DumpPdbOptionTests).Assembly.Location;
+
+ [Test]
+ public async Task DumpPdbListsDocumentsAndSequencePoints()
+ {
+ var result = await RunAsync(testAssemblyPath, "--disable-updatecheck", "--dump-pdb");
+
+ Assert.That(result.ExitCode, Is.EqualTo(0), result.Error);
+ Assert.That(result.Output, Does.Contain("Documents"));
+ Assert.That(result.Output, Does.Contain("DumpPdbOptionTests.cs"));
+ // the method this assertion lives in must show up with its sequence points
+ Assert.That(result.Output, Does.Contain(nameof(DumpPdbListsDocumentsAndSequencePoints)));
+ Assert.That(result.Output, Does.Contain("IL_"));
+ }
+
+ [Test]
+ public async Task DumpPdbShowsLocalVariableNames()
+ {
+ var result = await RunAsync(testAssemblyPath, "--disable-updatecheck", "--dump-pdb");
+
+ Assert.That(result.ExitCode, Is.EqualTo(0), result.Error);
+ // a local of this test class, only knowable from the PDB
+ Assert.That(result.Output, Does.Contain("scopeProbe"));
+ }
+
+ [Test]
+ public async Task DumpPdbJsonIsParseableAndHasMethods()
+ {
+ var result = await RunAsync(testAssemblyPath, "--disable-updatecheck", "--dump-pdb", "--json");
+
+ Assert.That(result.ExitCode, Is.EqualTo(0), result.Error);
+ using var doc = JsonDocument.Parse(result.Output);
+ var root = doc.RootElement;
+ Assert.That(root.GetProperty("documents").GetArrayLength(), Is.GreaterThan(0));
+ var methods = root.GetProperty("methods");
+ Assert.That(methods.GetArrayLength(), Is.GreaterThan(0));
+ var method = methods.EnumerateArray()
+ .First(m => m.GetProperty("name").GetString() == nameof(ScopeProbe));
+ Assert.That(method.GetProperty("sequencePoints").GetArrayLength(), Is.GreaterThan(0));
+ }
+
+ [Test]
+ public async Task DumpPdbReportsMissingSymbols()
+ {
+ // a reference assembly ships without any PDB next to it
+ string noPdbAssembly = typeof(object).Assembly.Location;
+
+ var result = await RunAsync(noPdbAssembly, "--disable-updatecheck", "--dump-pdb");
+
+ Assert.That(result.ExitCode, Is.Not.EqualTo(0));
+ Assert.That(result.Error, Does.Contain("No debug symbols"));
+ }
+
+ [Test]
+ public async Task DumpPdbAcceptsExplicitPdbPath()
+ {
+ string pdbPath = Path.ChangeExtension(testAssemblyPath, ".pdb");
+ Assert.That(File.Exists(pdbPath), Is.True, pdbPath);
+
+ var result = await RunAsync(testAssemblyPath, "--disable-updatecheck", "--dump-pdb", "-usepdb:" + pdbPath);
+
+ Assert.That(result.ExitCode, Is.EqualTo(0), result.Error);
+ Assert.That(result.Output, Does.Contain("DumpPdbOptionTests.cs"));
+ }
+
+ ///
+ /// Gives a named local to find in the dump.
+ ///
+ public static int ScopeProbe()
+ {
+ int scopeProbe = 42;
+ return scopeProbe;
+ }
+ }
+}
diff --git a/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs b/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs
index 6a39ace1b..51bc97895 100644
--- a/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs
+++ b/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs
@@ -152,7 +152,10 @@ Examples:
[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)]
+ [Option("--dump-pdb", "Dump the debug information of the assembly's PDB: documents, per-method sequence points, the scope tree with its variables, constants and imports, and custom debug information. Reads both Portable and Windows PDBs. Use -usepdb: to point at a PDB that is not next to the assembly.", CommandOptionType.NoValue)]
+ public bool DumpPdbFlag { get; }
+
+ [Option("--json", "Output as JSON. Only supported together with --dump-table or --dump-pdb.", CommandOptionType.NoValue)]
public bool JsonOutputFlag { get; }
public string DecompilerVersion => "ilspycmd: " + typeof(ILSpyCmdProgram).Assembly.GetName().Version.ToString() +
@@ -261,9 +264,9 @@ Examples:
Directory.CreateDirectory(outputDirectory);
}
- if (JsonOutputFlag && DumpTableName == null)
+ if (JsonOutputFlag && DumpTableName == null && !DumpPdbFlag)
{
- app.Error.WriteLine("The --json option is currently only supported together with --dump-table.");
+ app.Error.WriteLine("The --json option is only supported together with --dump-table or --dump-pdb.");
return ProgramExitCodes.EX_USAGE;
}
@@ -441,6 +444,18 @@ Examples:
return MetadataTableDumper.DumpTable(tableModule, output, table, JsonOutputFlag);
}
+ else if (DumpPdbFlag)
+ {
+ if (outputDirectory != null)
+ {
+ // per-file writer, disposed here, for the same reason as in --dump-table
+ string outputName = Path.GetFileNameWithoutExtension(fileName);
+ using var pdbOutput = File.CreateText(Path.Combine(outputDirectory, outputName) + $".pdb.{(JsonOutputFlag ? "json" : "txt")}");
+ return DumpPdb(fileName, pdbOutput, app);
+ }
+
+ return DumpPdb(fileName, output, app);
+ }
else
{
if (MemberIdString != null && TypeName != null)
@@ -1266,6 +1281,20 @@ Examples:
return 0;
}
+ int DumpPdb(string assemblyFileName, TextWriter output, CommandLineApplication app)
+ {
+ try
+ {
+ PdbDumper.Dump(assemblyFileName, InputPDBFile.IsSet ? InputPDBFile.Value : null, output, JsonOutputFlag);
+ }
+ catch (PdbDumper.NoDebugSymbolsException ex)
+ {
+ app.Error.WriteLine(ex.Message);
+ return ProgramExitCodes.EX_NOINPUT;
+ }
+ return 0;
+ }
+
IDebugInfoProvider TryLoadPDB(PEFile module)
{
if (InputPDBFile.IsSet)
diff --git a/ICSharpCode.ILSpyCmd/README.md b/ICSharpCode.ILSpyCmd/README.md
index 3a42ae5be..850741bf3 100644
--- a/ICSharpCode.ILSpyCmd/README.md
+++ b/ICSharpCode.ILSpyCmd/README.md
@@ -45,7 +45,11 @@ Options:
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.
+ --dump-pdb Dump the debug information of the assembly's PDB: documents, per-method
+ sequence points, the scope tree with its variables, constants and imports, and
+ custom debug information. Reads both Portable and Windows PDBs. Use
+ -usepdb: to point at a PDB that is not next to the assembly.
+ --json Output as JSON. Only supported together with --dump-table or --dump-pdb.
-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
diff --git a/ICSharpCode.ILSpyX/Metadata/PdbDumper.cs b/ICSharpCode.ILSpyX/Metadata/PdbDumper.cs
new file mode 100644
index 000000000..fab7d3d4a
--- /dev/null
+++ b/ICSharpCode.ILSpyX/Metadata/PdbDumper.cs
@@ -0,0 +1,431 @@
+// 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.Text;
+using System.Text.Json;
+
+using ICSharpCode.ILSpyX.PdbProvider;
+
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+
+using KnownGuids = ICSharpCode.Decompiler.DebugInfo.KnownGuids;
+
+namespace ICSharpCode.ILSpyX.Metadata
+{
+ ///
+ /// Prints the debug information of an assembly's PDB: documents, per-method sequence points,
+ /// the scope tree with its variables, constants and imports, and custom debug information.
+ ///
+ ///
+ /// Reading goes through Mono.Cecil, whose symbol readers cover both the Portable PDB and the
+ /// Windows (native MSF) format in managed code, so the dump works the same on every OS. The
+ /// raw table view of a Portable PDB is a different thing and belongs to
+ /// ; a Windows PDB has no metadata tables at all.
+ ///
+ public static class PdbDumper
+ {
+ ///
+ /// Thrown when the assembly has no PDB next to it, or the given PDB cannot be read.
+ ///
+ public sealed class NoDebugSymbolsException : Exception
+ {
+ public NoDebugSymbolsException(string message, Exception? innerException = null)
+ : base(message, innerException)
+ {
+ }
+ }
+
+ ///
+ /// Writes the debug information of to .
+ ///
+ ///
+ /// The PDB to read. When null, the symbol file next to the assembly (or embedded in it) is used.
+ ///
+ /// No readable debug symbols were found.
+ public static void Dump(string assemblyFileName, string? pdbFileName, TextWriter output, bool asJson)
+ {
+ using var module = ReadModuleWithSymbols(assemblyFileName, pdbFileName);
+ var methods = EnumerateTypes(module.Types)
+ .SelectMany(t => t.Methods)
+ .Where(m => m.DebugInformation != null && HasContent(m.DebugInformation))
+ .ToList();
+ var documents = CollectDocuments(methods);
+
+ if (asJson)
+ WriteJson(output, module, assemblyFileName, documents, methods);
+ else
+ WriteText(output, module, assemblyFileName, documents, methods);
+ }
+
+ static ModuleDefinition ReadModuleWithSymbols(string assemblyFileName, string? pdbFileName)
+ {
+ var parameters = new ReaderParameters { ReadSymbols = true, InMemory = true };
+ if (pdbFileName != null)
+ {
+ // SymbolStream is only honoured together with an explicit provider, and the
+ // provider must match the format of the file the user named
+ parameters.SymbolStream = File.OpenRead(pdbFileName);
+ parameters.SymbolReaderProvider = IsWindowsPdb(pdbFileName)
+ ? new Mono.Cecil.Pdb.NativePdbReaderProvider()
+ : new Mono.Cecil.Cil.PortablePdbReaderProvider();
+ }
+ ModuleDefinition module;
+ try
+ {
+ module = ModuleDefinition.ReadModule(assemblyFileName, parameters);
+ }
+ catch (Exception ex) when (ex is FileNotFoundException or InvalidOperationException or BadImageFormatException)
+ {
+ throw new NoDebugSymbolsException(
+ $"No debug symbols could be read for '{assemblyFileName}'.", ex);
+ }
+ if (module.SymbolReader == null)
+ {
+ module.Dispose();
+ throw new NoDebugSymbolsException($"No debug symbols found for '{assemblyFileName}'.");
+ }
+ return module;
+ }
+
+ static bool IsWindowsPdb(string pdbFileName)
+ {
+ using var stream = File.OpenRead(pdbFileName);
+ return DebugInfoUtils.IsWindowsPdb(stream);
+ }
+
+ static IEnumerable EnumerateTypes(IEnumerable types)
+ {
+ foreach (var type in types)
+ {
+ yield return type;
+ foreach (var nested in EnumerateTypes(type.NestedTypes))
+ yield return nested;
+ }
+ }
+
+ static bool HasContent(MethodDebugInformation debugInfo)
+ {
+ return debugInfo.HasSequencePoints
+ || debugInfo.Scope != null
+ || debugInfo.StateMachineKickOffMethod != null
+ || debugInfo.HasCustomDebugInformations;
+ }
+
+ ///
+ /// Collects the documents the methods refer to. Neither symbol reader exposes the
+ /// document list itself, so the sequence points are the only way to reach them.
+ ///
+ static List CollectDocuments(List methods)
+ {
+ var documents = new Dictionary(StringComparer.Ordinal);
+ foreach (var method in methods)
+ {
+ if (!method.DebugInformation.HasSequencePoints)
+ continue;
+ foreach (var point in method.DebugInformation.SequencePoints)
+ {
+ if (point.Document != null)
+ documents[point.Document.Url] = point.Document;
+ }
+ }
+ return documents.Values.OrderBy(d => d.Url, StringComparer.Ordinal).ToList();
+ }
+
+ static string FormatToken(MetadataToken token) => "0x" + token.ToUInt32().ToString("X8", CultureInfo.InvariantCulture);
+
+ static string FormatOffset(int offset) => "IL_" + offset.ToString("X4", CultureInfo.InvariantCulture);
+
+ ///
+ /// Reading of an end-of-method offset throws, so
+ /// that case gets its own text.
+ ///
+ static string FormatOffset(InstructionOffset offset)
+ {
+ return offset.IsEndOfMethod ? "end" : FormatOffset(offset.Offset);
+ }
+
+ static string FormatSpan(SequencePoint point)
+ {
+ return point.IsHidden
+ ? "hidden"
+ : $"({point.StartLine},{point.StartColumn})-({point.EndLine},{point.EndColumn})";
+ }
+
+ static string FormatHash(byte[]? hash)
+ {
+ return hash == null ? "" : string.Concat(hash.Select(b => b.ToString("x2", CultureInfo.InvariantCulture)));
+ }
+
+ static string FormatConstantValue(object? value)
+ {
+ return value switch {
+ null => "null",
+ string s => "\"" + s + "\"",
+ IFormattable f => f.ToString(null, CultureInfo.InvariantCulture),
+ _ => value.ToString() ?? "",
+ };
+ }
+
+ static string FormatImport(ImportTarget target)
+ {
+ var text = new StringBuilder(target.Kind.ToString());
+ if (target.Alias != null)
+ text.Append(" ").Append(target.Alias).Append(" =");
+ if (target.Namespace != null)
+ text.Append(" ").Append(target.Namespace);
+ if (target.Type != null)
+ text.Append(" ").Append(target.Type.FullName);
+ if (target.AssemblyReference != null)
+ text.Append(" [").Append(target.AssemblyReference.Name).Append("]");
+ return text.ToString();
+ }
+
+ static string DescribeCustomDebugInformation(CustomDebugInformation info)
+ {
+ return info switch {
+ // Cecil hands every kind it does not model back as raw bytes; name the ones
+ // Roslyn documents so the dump is readable
+ BinaryCustomDebugInformation binary =>
+ $"{KnownGuids.GetCustomDebugInformationKindName(info.Identifier) ?? info.Identifier.ToString()} ({binary.Data?.Length ?? 0} bytes)",
+ StateMachineScopeDebugInformation scopes =>
+ $"{info.Kind} " + string.Join(", ", scopes.Scopes.Select(s => $"{FormatOffset(s.Start)}..{FormatOffset(s.End)}")),
+ AsyncMethodBodyDebugInformation async =>
+ $"{info.Kind} catch handler {FormatOffset(async.CatchHandler)}, {async.Resumes.Count} resume point(s)",
+ EmbeddedSourceDebugInformation embedded => $"{info.Kind} ({embedded.Content?.Length ?? 0} bytes, compressed={embedded.Compress})",
+ SourceLinkDebugInformation sourceLink => $"{info.Kind} {sourceLink.Content}",
+ _ => info.Kind.ToString(),
+ };
+ }
+
+ static void WriteText(TextWriter output, ModuleDefinition module, string assemblyFileName,
+ List documents, List methods)
+ {
+ output.WriteLine($"Assembly: {assemblyFileName}");
+ output.WriteLine($"Symbols: {module.SymbolReader!.GetType().Name}");
+ output.WriteLine();
+
+ if (module.HasCustomDebugInformations)
+ {
+ // SourceLink and the compilation options are recorded against the module, not
+ // against any single method
+ output.WriteLine("Module custom debug information");
+ foreach (var info in module.CustomDebugInformations)
+ {
+ output.WriteLine($" {DescribeCustomDebugInformation(info)}");
+ }
+ output.WriteLine();
+ }
+
+ output.WriteLine($"Documents ({documents.Count})");
+ foreach (var document in documents)
+ {
+ output.WriteLine($" {document.Url}");
+ output.WriteLine($" language={document.Language} hashAlgorithm={document.HashAlgorithm} hash={FormatHash(document.Hash)}");
+ if (document.EmbeddedSource is { Length: > 0 } embedded)
+ output.WriteLine($" embeddedSource={embedded.Length} bytes");
+ }
+ output.WriteLine();
+
+ output.WriteLine($"Methods ({methods.Count})");
+ foreach (var method in methods)
+ {
+ var debugInfo = method.DebugInformation;
+ output.WriteLine($" {FormatToken(method.MetadataToken)} {method.FullName}");
+ if (debugInfo.StateMachineKickOffMethod != null)
+ output.WriteLine($" kickoff: {FormatToken(debugInfo.StateMachineKickOffMethod.MetadataToken)} {debugInfo.StateMachineKickOffMethod.FullName}");
+ if (debugInfo.HasSequencePoints)
+ {
+ output.WriteLine(" Sequence points:");
+ foreach (var point in debugInfo.SequencePoints)
+ {
+ output.WriteLine($" {FormatOffset(point.Offset)} {FormatSpan(point)} {point.Document?.Url}");
+ }
+ }
+ if (debugInfo.Scope != null)
+ WriteScopeText(output, debugInfo.Scope, " ");
+ foreach (var info in debugInfo.CustomDebugInformations)
+ {
+ output.WriteLine($" CDI: {DescribeCustomDebugInformation(info)}");
+ }
+ }
+ }
+
+ static void WriteScopeText(TextWriter output, ScopeDebugInformation scope, string indent)
+ {
+ output.WriteLine($"{indent}Scope {FormatOffset(scope.Start)}..{FormatOffset(scope.End)}");
+ foreach (var variable in scope.Variables)
+ {
+ output.WriteLine($"{indent} [{variable.Index}] {variable.Name}{(variable.IsDebuggerHidden ? " (hidden)" : "")}");
+ }
+ foreach (var constant in scope.Constants)
+ {
+ output.WriteLine($"{indent} const {constant.Name} : {constant.ConstantType?.FullName} = {FormatConstantValue(constant.Value)}");
+ }
+ for (var import = scope.Import; import != null; import = import.Parent)
+ {
+ foreach (var target in import.Targets)
+ {
+ output.WriteLine($"{indent} import {FormatImport(target)}");
+ }
+ }
+ foreach (var nested in scope.Scopes)
+ {
+ WriteScopeText(output, nested, indent + " ");
+ }
+ }
+
+ static void WriteJson(TextWriter output, ModuleDefinition module, string assemblyFileName,
+ List documents, List methods)
+ {
+ using var stream = new MemoryStream();
+ using (var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true }))
+ {
+ writer.WriteStartObject();
+ writer.WriteString("assembly", assemblyFileName);
+ writer.WriteString("symbolReader", module.SymbolReader!.GetType().Name);
+
+ writer.WriteStartArray("moduleCustomDebugInformation");
+ foreach (var info in module.CustomDebugInformations)
+ {
+ writer.WriteStartObject();
+ writer.WriteString("kind", info.Kind.ToString());
+ writer.WriteString("identifier", info.Identifier.ToString());
+ writer.WriteString("description", DescribeCustomDebugInformation(info));
+ writer.WriteEndObject();
+ }
+ writer.WriteEndArray();
+
+ writer.WriteStartArray("documents");
+ foreach (var document in documents)
+ {
+ writer.WriteStartObject();
+ writer.WriteString("url", document.Url);
+ writer.WriteString("language", document.Language.ToString());
+ writer.WriteString("hashAlgorithm", document.HashAlgorithm.ToString());
+ writer.WriteString("hash", FormatHash(document.Hash));
+ writer.WriteNumber("embeddedSourceLength", document.EmbeddedSource?.Length ?? 0);
+ writer.WriteEndObject();
+ }
+ writer.WriteEndArray();
+
+ writer.WriteStartArray("methods");
+ foreach (var method in methods)
+ {
+ var debugInfo = method.DebugInformation;
+ writer.WriteStartObject();
+ writer.WriteString("token", FormatToken(method.MetadataToken));
+ writer.WriteString("name", method.Name);
+ writer.WriteString("fullName", method.FullName);
+ if (debugInfo.StateMachineKickOffMethod != null)
+ writer.WriteString("kickoffMethod", debugInfo.StateMachineKickOffMethod.FullName);
+
+ writer.WriteStartArray("sequencePoints");
+ foreach (var point in debugInfo.SequencePoints)
+ {
+ writer.WriteStartObject();
+ writer.WriteNumber("offset", point.Offset);
+ writer.WriteBoolean("hidden", point.IsHidden);
+ writer.WriteNumber("startLine", point.StartLine);
+ writer.WriteNumber("startColumn", point.StartColumn);
+ writer.WriteNumber("endLine", point.EndLine);
+ writer.WriteNumber("endColumn", point.EndColumn);
+ writer.WriteString("document", point.Document?.Url);
+ writer.WriteEndObject();
+ }
+ writer.WriteEndArray();
+
+ if (debugInfo.Scope != null)
+ {
+ writer.WritePropertyName("scope");
+ WriteScopeJson(writer, debugInfo.Scope);
+ }
+
+ writer.WriteStartArray("customDebugInformation");
+ foreach (var info in debugInfo.CustomDebugInformations)
+ {
+ writer.WriteStartObject();
+ writer.WriteString("kind", info.Kind.ToString());
+ writer.WriteString("identifier", info.Identifier.ToString());
+ writer.WriteString("description", DescribeCustomDebugInformation(info));
+ writer.WriteEndObject();
+ }
+ writer.WriteEndArray();
+
+ writer.WriteEndObject();
+ }
+ writer.WriteEndArray();
+ writer.WriteEndObject();
+ }
+ output.WriteLine(Encoding.UTF8.GetString(stream.ToArray()));
+ }
+
+ static void WriteScopeJson(Utf8JsonWriter writer, ScopeDebugInformation scope)
+ {
+ writer.WriteStartObject();
+ writer.WriteString("start", FormatOffset(scope.Start));
+ writer.WriteString("end", FormatOffset(scope.End));
+
+ writer.WriteStartArray("variables");
+ foreach (var variable in scope.Variables)
+ {
+ writer.WriteStartObject();
+ writer.WriteNumber("index", variable.Index);
+ writer.WriteString("name", variable.Name);
+ writer.WriteBoolean("debuggerHidden", variable.IsDebuggerHidden);
+ writer.WriteEndObject();
+ }
+ writer.WriteEndArray();
+
+ writer.WriteStartArray("constants");
+ foreach (var constant in scope.Constants)
+ {
+ writer.WriteStartObject();
+ writer.WriteString("name", constant.Name);
+ writer.WriteString("type", constant.ConstantType?.FullName);
+ writer.WriteString("value", FormatConstantValue(constant.Value));
+ writer.WriteEndObject();
+ }
+ writer.WriteEndArray();
+
+ writer.WriteStartArray("imports");
+ for (var import = scope.Import; import != null; import = import.Parent)
+ {
+ foreach (var target in import.Targets)
+ {
+ writer.WriteStringValue(FormatImport(target));
+ }
+ }
+ writer.WriteEndArray();
+
+ writer.WriteStartArray("scopes");
+ foreach (var nested in scope.Scopes)
+ {
+ WriteScopeJson(writer, nested);
+ }
+ writer.WriteEndArray();
+
+ writer.WriteEndObject();
+ }
+ }
+}
diff --git a/ICSharpCode.ILSpyX/PdbProvider/DebugInfoUtils.cs b/ICSharpCode.ILSpyX/PdbProvider/DebugInfoUtils.cs
index 98b5abcc2..3ba87d786 100644
--- a/ICSharpCode.ILSpyX/PdbProvider/DebugInfoUtils.cs
+++ b/ICSharpCode.ILSpyX/PdbProvider/DebugInfoUtils.cs
@@ -87,6 +87,17 @@ namespace ICSharpCode.ILSpyX.PdbProvider
const string LegacyPDBPrefix = "Microsoft C/C++ MSF 7.00";
static readonly byte[] buffer = new byte[LegacyPDBPrefix.Length];
+ ///
+ /// True for a Windows (native MSF) PDB, false for a Portable PDB. Reads from the current
+ /// position and leaves the stream positioned after the signature.
+ ///
+ public static bool IsWindowsPdb(Stream stream)
+ {
+ var signature = new byte[LegacyPDBPrefix.Length];
+ return stream.Read(signature, 0, signature.Length) == signature.Length
+ && System.Text.Encoding.ASCII.GetString(signature) == LegacyPDBPrefix;
+ }
+
static bool TryOpenPortablePdb(PEFile module,
[NotNullWhen(true)] out MetadataReaderProvider? provider,
[NotNullWhen(true)] out string? pdbFileName)