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)