Browse Source

Merge pull request #3747 from fowl2/customDebugInfoEmbdeddedSource

CustomDebugInformation: decode EmbeddedSource, split columns
pull/3767/head
Siegfried Pammer 4 weeks ago committed by GitHub
parent
commit
89f7d680ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 64
      ILSpy.Tests/Metadata/CustomDebugInformationRowDetailsTests.cs
  2. 79
      ILSpy/Metadata/DebugTables/CustomDebugInformationTableTreeNode.cs

64
ILSpy.Tests/Metadata/CustomDebugInformationRowDetailsTests.cs

@ -21,6 +21,7 @@ using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.IO; using System.IO;
using System.IO.Compression;
using System.Linq; using System.Linq;
using System.Reflection.Metadata; using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335; using System.Reflection.Metadata.Ecma335;
@ -48,14 +49,16 @@ namespace ICSharpCode.ILSpy.Tests.Metadata;
public class CustomDebugInformationRowDetailsTests public class CustomDebugInformationRowDetailsTests
{ {
const string SourceLinkJson = /*lang=json,strict*/ """{"documents":{"/src/*":"https://example.invalid/*"}}"""; const string SourceLinkJson = /*lang=json,strict*/ """{"documents":{"/src/*":"https://example.invalid/*"}}""";
const string EmbeddedSourceText = "class Embedded { void M() { } }";
static readonly Guid UnknownKindGuid = new Guid("1c46e7c9-0631-44eb-a78a-f8e8e1e0c0a1"); static readonly Guid UnknownKindGuid = new Guid("1c46e7c9-0631-44eb-a78a-f8e8e1e0c0a1");
static readonly Guid ReferenceMvid = new Guid("8e2c021c-1f4e-4a40-a3a8-9785b2a99f9c"); static readonly Guid ReferenceMvid = new Guid("8e2c021c-1f4e-4a40-a3a8-9785b2a99f9c");
static readonly byte[] EmbeddedSourceDeflateBlob = BuildEmbeddedSourceDeflateBlob(EmbeddedSourceText);
static MetadataFile BuildPdbFixture() static MetadataFile BuildPdbFixture()
{ {
// Synthesize a standalone portable PDB whose CustomDebugInformation table holds one // Synthesize a standalone portable PDB whose CustomDebugInformation table holds one
// row per row-details scenario: a text kind (Source Link), an unrecognized kind // row per row-details scenario: a text kind (Source Link), an unrecognized kind
// GUID, an embedded-source blob (opaque at this layer), and the four structured // GUID, embedded source in both formats (raw and DEFLATE), and the four structured
// kinds that parse into typed rows. Parents use ascending MethodDef tokens so the // kinds that parse into typed rows. Parents use ascending MethodDef tokens so the
// (parent-sorted) table preserves this row order. // (parent-sorted) table preserves this row order.
var builder = new MetadataBuilder(); var builder = new MetadataBuilder();
@ -68,9 +71,10 @@ public class CustomDebugInformationRowDetailsTests
AddRow(6, KnownGuids.CompilationMetadataReferences, MetadataReferenceBlob( AddRow(6, KnownGuids.CompilationMetadataReferences, MetadataReferenceBlob(
"System.Runtime.dll", "global", flags: 1, timestamp: 0x12345678, fileSize: 1024, ReferenceMvid)); "System.Runtime.dll", "global", flags: 1, timestamp: 0x12345678, fileSize: 1024, ReferenceMvid));
AddRow(7, KnownGuids.TupleElementNames, NullTerminatedStrings("Item1", "Name")); AddRow(7, KnownGuids.TupleElementNames, NullTerminatedStrings("Item1", "Name"));
AddRow(8, KnownGuids.EmbeddedSource, EmbeddedSourceDeflateBlob);
var rowCounts = new int[MetadataTokens.TableCount]; var rowCounts = new int[MetadataTokens.TableCount];
rowCounts[(int)TableIndex.MethodDef] = 7; rowCounts[(int)TableIndex.MethodDef] = 8;
var pdbBuilder = new PortablePdbBuilder(builder, rowCounts.ToImmutableArray(), entryPoint: default); var pdbBuilder = new PortablePdbBuilder(builder, rowCounts.ToImmutableArray(), entryPoint: default);
var blob = new BlobBuilder(); var blob = new BlobBuilder();
pdbBuilder.Serialize(blob); pdbBuilder.Serialize(blob);
@ -87,6 +91,20 @@ public class CustomDebugInformationRowDetailsTests
} }
} }
static byte[] BuildEmbeddedSourceDeflateBlob(string text)
{
// Embedded-source blob: an int32 format header, then the document bytes. A positive
// header is the uncompressed size and marks the payload as DEFLATE-compressed.
var content = Encoding.UTF8.GetBytes(text);
using var ms = new MemoryStream();
ms.Write(BitConverter.GetBytes(content.Length), 0, 4);
using (var deflate = new DeflateStream(ms, CompressionMode.Compress, leaveOpen: true))
{
deflate.Write(content, 0, content.Length);
}
return ms.ToArray();
}
static byte[] HoistedScopesBlob(params (uint StartOffset, uint Length)[] scopes) static byte[] HoistedScopesBlob(params (uint StartOffset, uint Length)[] scopes)
{ {
// State-machine hoisted local scopes: a sequence of (start offset, length) uint32 pairs. // State-machine hoisted local scopes: a sequence of (start offset, length) uint32 pairs.
@ -139,22 +157,26 @@ public class CustomDebugInformationRowDetailsTests
.ToList(); .ToList();
[Test] [Test]
public void Offset_And_Friendly_Kind_Columns_Match_The_Tables_Conventions() public void Offset_And_Split_Kind_Columns_Match_The_Tables_Conventions()
{ {
// The Kind column carries the GUID heap offset, the decoded friendly name, and the // The kind surfaces as three columns — the GUID heap offset (Kind), the raw GUID
// raw GUID in one cell; Offset positions the row within the metadata stream like // (KindGUID), and the decoded friendly name (KindString) — so each is independently
// sortable and filterable. Offset positions the row within the metadata stream like
// every other table's Offset column. // every other table's Offset column.
var metadataFile = BuildPdbFixture(); var metadataFile = BuildPdbFixture();
var entries = LoadEntries(metadataFile); var entries = LoadEntries(metadataFile);
entries[0].Kind.Should().MatchRegex("^[0-9A-F]{8} - Source Link \\(C# / VB\\) \\[cc110556-a091-4d38-9fec-25ab9a351a6a\\]$"); entries[0].Kind.Should().BePositive("the Kind column is the 1-based GUID heap offset");
entries[1].Kind.Should().EndWith($"- Unknown [{UnknownKindGuid}]"); entries[0].KindGUID.Should().Be(KnownGuids.SourceLink);
entries[3].Kind.Should().Contain("State Machine Hoisted Local Scopes (C# / VB)"); entries[0].KindString.Should().Be("Source Link (C# / VB)");
entries[1].KindGUID.Should().Be(UnknownKindGuid);
entries[1].KindString.Should().Be("Unknown");
entries[3].KindString.Should().Be("State Machine Hoisted Local Scopes (C# / VB)");
int rowSize = metadataFile.Metadata.GetTableRowSize(TableIndex.CustomDebugInformation); int rowSize = metadataFile.Metadata.GetTableRowSize(TableIndex.CustomDebugInformation);
entries[0].Offset.Should().BePositive("the table lives at a real offset inside the PDB metadata"); entries[0].Offset.Should().BePositive("the table lives at a real offset inside the PDB metadata");
entries.Select(e => e.Offset).Should().BeInAscendingOrder() entries.Select(e => e.Offset).Should().BeInAscendingOrder()
.And.HaveCount(7); .And.HaveCount(8);
(entries[1].Offset - entries[0].Offset).Should().Be(rowSize); (entries[1].Offset - entries[0].Offset).Should().Be(rowSize);
} }
@ -192,8 +214,28 @@ public class CustomDebugInformationRowDetailsTests
entries[0].RowDetails.Should().Be(SourceLinkJson, "source link blobs are UTF-8 JSON"); entries[0].RowDetails.Should().Be(SourceLinkJson, "source link blobs are UTF-8 JSON");
entries[1].RowDetails.Should().Be("01-02-03", "unrecognized kinds degrade to a hex dump"); entries[1].RowDetails.Should().Be("01-02-03", "unrecognized kinds degrade to a hex dump");
entries[2].RowDetails.Should().Be("00-00-00-00-41", }
"embedded source is an opaque payload to the row-details parser and dumps as hex");
[Test]
public void RowDetails_Decodes_Embedded_Source_To_The_Document_Text()
{
var metadataFile = BuildPdbFixture();
var entries = LoadEntries(metadataFile);
entries[2].RowDetails.Should().Be("A", "a zero format header means the document bytes follow uncompressed");
entries[7].RowDetails.Should().Be(EmbeddedSourceText, "a positive format header means the document is DEFLATE-compressed");
}
[Test]
public void Info_Summarizes_The_Embedded_Source_Format()
{
var metadataFile = BuildPdbFixture();
var entries = LoadEntries(metadataFile);
entries[0].Info.Should().BeNull("only embedded source carries a format header to summarize");
entries[2].Info.Should().Be("Raw, 5 bytes");
entries[7].Info.Should().Be(
$"DEFLATE, {EmbeddedSourceDeflateBlob.Length} bytes, {Encoding.UTF8.GetByteCount(EmbeddedSourceText)} uncompressed");
} }
[AvaloniaTest] [AvaloniaTest]

79
ILSpy/Metadata/DebugTables/CustomDebugInformationTableTreeNode.cs

@ -18,6 +18,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Reflection.Metadata; using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335; using System.Reflection.Metadata.Ecma335;
@ -45,9 +46,9 @@ namespace ILSpy.Metadata.DebugTables
/// <summary> /// <summary>
/// View of the CustomDebugInformation table — extensible per-entity payloads used for /// View of the CustomDebugInformation table — extensible per-entity payloads used for
/// async/iterator state-machine info, embedded source, source-link JSON, and similar /// async/iterator state-machine info, embedded source, source-link JSON, and similar
/// debug-time data. Each row carries a Parent token, a Kind GUID (decoded to a friendly /// debug-time data. Each row carries a Parent token, a Kind GUID (surfaced as heap
/// name in the Kind column), and an opaque Value blob whose parsed contents show as the /// offset, raw GUID, and decoded friendly name in the Kind / KindGUID / KindString
/// row's details. /// columns), and an opaque Value blob whose parsed contents show as the row's details.
/// </summary> /// </summary>
public sealed class CustomDebugInformationTableTreeNode : MetadataTableTreeNode<CustomDebugInformationTableTreeNode.CustomDebugInformationEntry> public sealed class CustomDebugInformationTableTreeNode : MetadataTableTreeNode<CustomDebugInformationTableTreeNode.CustomDebugInformationEntry>
{ {
@ -131,37 +132,59 @@ namespace ILSpy.Metadata.DebugTables
(KnownGuids.TypeDefinitionDocuments, "Type Definition Documents (C# / VB)"), (KnownGuids.TypeDefinitionDocuments, "Type Definition Documents (C# / VB)"),
}; };
[ColumnInfo("X2", Kind = ColumnKind.HeapOffset)]
public int Kind => MetadataTokens.GetHeapOffset(debugInfo.Kind);
[ColumnInfo("D")]
public Guid KindGUID => metadataFile.Metadata.GetGuid(debugInfo.Kind);
string? kindString; string? kindString;
/// <summary> public string KindString {
/// One cell carrying the GUID heap offset, the decoded friendly name of well-known
/// kind GUIDs, and the raw GUID, e.g.
/// <c>0000000B - Source Link (C# / VB) [cc110556-...]</c>. Plain text (instead of a
/// bare heap offset plus tooltip) so the column filter matches the friendly names.
/// </summary>
public string Kind {
get { get {
if (kindString != null) if (kindString != null)
return kindString; return kindString;
if (debugInfo.Kind.IsNil) if (debugInfo.Kind.IsNil)
return kindString = ""; return kindString = "";
var guid = metadataFile.Metadata.GetGuid(debugInfo.Kind); var guid = metadataFile.Metadata.GetGuid(debugInfo.Kind);
string name = "Unknown";
foreach (var (knownGuid, knownName) in knownKindNames) foreach (var (knownGuid, knownName) in knownKindNames)
{ {
if (guid == knownGuid) if (guid == knownGuid)
{ return kindString = knownName;
name = knownName;
break;
}
} }
return kindString = $"{MetadataTokens.GetHeapOffset(debugInfo.Kind):X8} - {name} [{guid}]"; return kindString = "Unknown";
} }
} }
public string? Info => !debugInfo.Kind.IsNil && KindGUID == KnownGuids.EmbeddedSource
? GetEmbeddedSourceFormat()
: null;
string GetEmbeddedSourceFormat()
{
if (debugInfo.Value.IsNil)
return "{nil blob}";
var reader = metadataFile.Metadata.GetBlobReader(debugInfo.Value);
if (reader.RemainingBytes < 4)
return "{blob too short}";
var format = reader.ReadInt32();
return format switch {
< 0 => $"Unknown format '{format}', {reader.Length} bytes",
0 => $"Raw, {reader.Length} bytes",
> 0 => $"DEFLATE, {reader.Length} bytes, {format} uncompressed",
};
}
[ColumnInfo("X8", Kind = ColumnKind.HeapOffset)] [ColumnInfo("X8", Kind = ColumnKind.HeapOffset)]
public int Value => MetadataTokens.GetHeapOffset(debugInfo.Value); public int Value => MetadataTokens.GetHeapOffset(debugInfo.Value);
[ColumnInfo("X8")]
public int ValueLength => metadataFile.Metadata.GetBlobReader(debugInfo.Value).Length;
public string ValueTooltip { public string ValueTooltip {
get { get {
if (debugInfo.Value.IsNil) if (debugInfo.Value.IsNil)
@ -174,9 +197,9 @@ namespace ILSpy.Metadata.DebugTables
/// <summary> /// <summary>
/// Parsed view of the Value blob for the row-details area. Structured kinds become /// Parsed view of the Value blob for the row-details area. Structured kinds become
/// typed row lists, source-link blobs the decoded JSON text, everything else /// typed row lists, source-link blobs the decoded JSON text, embedded source the
/// (including malformed structured blobs) a hex dump. Cached — the details area /// (decompressed) document text, everything else (including malformed blobs) a hex
/// re-requests it on every selection change. /// dump. Cached — the details area re-requests it on every selection change.
/// </summary> /// </summary>
public object? RowDetails { public object? RowDetails {
get { get {
@ -190,7 +213,7 @@ namespace ILSpy.Metadata.DebugTables
{ {
return rowDetails = ParseRowDetails(ref reader); return rowDetails = ParseRowDetails(ref reader);
} }
catch (BadImageFormatException) catch (Exception ex) when (ex is BadImageFormatException or InvalidDataException)
{ {
return rowDetails = metadataFile.Metadata.GetBlobReader(debugInfo.Value).ToHexString(); return rowDetails = metadataFile.Metadata.GetBlobReader(debugInfo.Value).ToHexString();
} }
@ -209,6 +232,22 @@ namespace ILSpy.Metadata.DebugTables
} }
if (kind == KnownGuids.SourceLink) if (kind == KnownGuids.SourceLink)
return reader.ReadUTF8(reader.RemainingBytes); return reader.ReadUTF8(reader.RemainingBytes);
if (kind == KnownGuids.EmbeddedSource)
{
var embeddedSourceFormat = reader.ReadInt32();
if (embeddedSourceFormat < 0) // unknown format, show raw data as hex
return reader.ToHexString();
var embeddedSourceBytes = reader.ReadBytes(reader.RemainingBytes);
Stream embeddedSourceByteStream = new MemoryStream(embeddedSourceBytes);
if (embeddedSourceFormat > 0) // positive length means the data is compressed using DEFLATE
embeddedSourceByteStream = new System.IO.Compression.DeflateStream(embeddedSourceByteStream, System.IO.Compression.CompressionMode.Decompress);
var textReader = new StreamReader(embeddedSourceByteStream, detectEncodingFromByteOrderMarks: true);
return textReader.ReadToEnd();
}
if (kind == KnownGuids.CompilationOptions) if (kind == KnownGuids.CompilationOptions)
{ {
var list = new List<CompilationOptionDetail>(); var list = new List<CompilationOptionDetail>();

Loading…
Cancel
Save