Add --dump-pdb, a cross-platform replacement for Pdb2Xml

The Pdb2Xml command that just left the app was the only way to see a PDB's
contents, and it was Windows-only. Mono.Cecil's symbol readers cover the
Portable and the Windows (native MSF) format in managed code and pick the
right one themselves, so a single dump path serves both on every OS.

Cecil's object model, rather than raw metadata, is what makes this work for
Windows PDBs at all: they have no metadata tables for --dump-table to read.
That also settles the split between the two options - --dump-table stays the
raw row view of a Portable PDB, --dump-pdb is the decoded one.

Neither symbol reader exposes the document list, so the documents are
collected from the sequence points that reference them.

Assisted-by: Claude:claude-opus-5:Claude Code
This commit is contained in:
Siegfried Pammer
2026-10-02 07:44:55 +02:00
parent 58be31306e
commit 30dbce447e
5 changed files with 586 additions and 4 deletions
@@ -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"));
}
/// <summary>
/// Gives <see cref="DumpPdbShowsLocalVariableNames"/> a named local to find in the dump.
/// </summary>
public static int ScopeProbe()
{
int scopeProbe = 42;
return scopeProbe;
}
}
}
+32 -3
View File
@@ -152,7 +152,10 @@ Examples:
[Option("--dump-table <table>", "Dump a metadata table: prints RID, token, names, heap offsets and coded indexes of every row. <table> 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:<file> 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)
+5 -1
View File
@@ -45,7 +45,11 @@ Options:
indexes of every row. <table> 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:<file> 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 <version> 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
+431
View File
@@ -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
{
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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
/// <see cref="MetadataTableDumper"/>; a Windows PDB has no metadata tables at all.
/// </remarks>
public static class PdbDumper
{
/// <summary>
/// Thrown when the assembly has no PDB next to it, or the given PDB cannot be read.
/// </summary>
public sealed class NoDebugSymbolsException : Exception
{
public NoDebugSymbolsException(string message, Exception? innerException = null)
: base(message, innerException)
{
}
}
/// <summary>
/// Writes the debug information of <paramref name="assemblyFileName"/> to <paramref name="output"/>.
/// </summary>
/// <param name="pdbFileName">
/// The PDB to read. When null, the symbol file next to the assembly (or embedded in it) is used.
/// </param>
/// <exception cref="NoDebugSymbolsException">No readable debug symbols were found.</exception>
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<TypeDefinition> EnumerateTypes(IEnumerable<TypeDefinition> 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;
}
/// <summary>
/// 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.
/// </summary>
static List<Document> CollectDocuments(List<MethodDefinition> methods)
{
var documents = new Dictionary<string, Document>(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);
/// <summary>
/// Reading <see cref="InstructionOffset.Offset"/> of an end-of-method offset throws, so
/// that case gets its own text.
/// </summary>
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<Document> documents, List<MethodDefinition> 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<Document> documents, List<MethodDefinition> 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();
}
}
}
@@ -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];
/// <summary>
/// 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.
/// </summary>
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)