Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions PdbLibrary/PdbLibrary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,18 @@ public PDBFile(FileInfo pdbFileInfo)
byte[] bytes = new byte[PdbSignature.signature.Length];
int n = fp.Read(bytes, 0, PdbSignature.signature.Length);

// A Portable PDB (ECMA-335, "BSJB" magic — the format modern .NET Core /
// SDK projects emit, including a .NET app's own assembly PDB) is CLI
// metadata, not an MSF container. Its debug identity is read from the
// metadata "PDB id" instead of the MSF streams; the resulting {GUID}{age}
// key matches the one a dump's CodeView record names, so it indexes into
// the symsrv store the same way an MSF PDB's key does.
if (n >= 4 && bytes[0] == (byte)'B' && bytes[1] == (byte)'S' && bytes[2] == (byte)'J' && bytes[3] == (byte)'B')
{
this.GUID = ReadPortableIdentity(pdbFileInfo);
return;
}

if (n != PdbSignature.signature.Length)
{
throw new Exception("Invalid PDB signature");
Expand Down Expand Up @@ -373,6 +385,55 @@ public PDBFile(FileInfo pdbFileInfo)
}
}
}

#if NET
// Portable PDB (BSJB) debug identity: the 16-byte GUID prefix of the metadata
// "PDB id" (System.Reflection.Metadata's DebugMetadataHeader.Id), with age fixed
// at 1 — the value Roslyn writes into the PE's CodeView RSDS record for portable
// PDBs — so GUID.Value() yields the same {GUID}{age} symbol-server key a dump's
// PdbStoreKey computes. The GUID bytes are laid out exactly as a System.Guid
// constructed from the same 16 bytes formats under "N", matching the store key.
//
// In-box on the net10.0 target; the netstandard2.0 flavor has no
// System.Reflection.Metadata, so portable identity is a .NET-target capability
// (the MSF-PDB / PE key extraction stays pure BCL and cross-platform there).
private static GUID ReadPortableIdentity(FileInfo pdbFileInfo)
{
using (FileStream stream = new FileStream(pdbFileInfo.FullName, FileMode.Open, FileAccess.Read))
using (var provider = System.Reflection.Metadata.MetadataReaderProvider.FromPortablePdbStream(stream))
{
var reader = provider.GetMetadataReader();
var header = reader.DebugMetadataHeader;
if (header == null)
{
throw new Exception("Not a portable PDB: file has no debug metadata header");
}

var idBlob = header.Id; // 20-byte PDB id: 16-byte GUID + 4-byte stamp
if (idBlob.Length < 16)
{
throw new Exception($"Portable PDB id too short: {idBlob.Length} bytes");
}
Comment on lines +406 to +416

byte[] id = new byte[idBlob.Length];
idBlob.CopyTo(id);

uint data1 = BitConverter.ToUInt32(id, 0);
ushort data2 = BitConverter.ToUInt16(id, 4);
ushort data3 = BitConverter.ToUInt16(id, 6);
byte[] data4 = new byte[8];
Array.Copy(id, 8, data4, 0, 8);

const uint PortablePdbAge = 1; // Roslyn's fixed RSDS age for portable PDBs
return new GUID(data1, data2, data3, data4, PortablePdbAge);
}
}
#else
private static GUID ReadPortableIdentity(FileInfo pdbFileInfo)
=> throw new NotSupportedException(
"Portable PDB identity requires the .NET (net10.0) target of PdbLibrary; " +
"the netstandard2.0 flavor reads only MSF/CodeView PDBs.");
#endif
}

public class PEFile
Expand Down
24 changes: 15 additions & 9 deletions PdbLibrary/PdbLibrary.csproj
Original file line number Diff line number Diff line change
@@ -1,27 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">

<!--
Retargeted from the legacy net472 project to an SDK-style netstandard2.0
library (2026-07): the GUID parser is pure BCL (FileStream + BitConverter),
so netstandard2.0 runs unchanged on .NET 10 (BugSplat's cross-platform crash
processor consumes it there) while staying consumable by the existing net472
NuGet package users. Builds on Windows, macOS, and Linux.
Retargeted from the legacy net472 project to an SDK-style library (2026-07).
Multi-targeted netstandard2.0;net10.0:
- netstandard2.0: the MSF-PDB / PE GUID parser is pure BCL (FileStream +
BitConverter), so it runs unchanged on .NET 10 and stays consumable by the
existing net472 NuGet package users. Builds on Windows, macOS, and Linux.
- net10.0: additionally reads PORTABLE PDB (ECMA-335 "BSJB") debug identity via
the in-box System.Reflection.Metadata (see ReadPortableIdentity, #if NET). No
package reference is needed — the net10.0 target ships it in the framework, and
the netstandard2.0 flavor stays pure BCL (portable identity is a .NET-only
capability there).
-->
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFrameworks>netstandard2.0;net10.0</TargetFrameworks>
<RootNamespace>PdbLibrary</RootNamespace>
<AssemblyName>PdbLibrary</AssemblyName>
<LangVersion>latest</LangVersion>
<Nullable>disable</Nullable>
<GenerateDocumentationFile>false</GenerateDocumentationFile>

<!-- NuGet package identity (was in Properties/AssemblyInfo.cs) -->
<!-- NuGet package identity (was in Properties/AssemblyInfo.cs). Published (signed)
manually from a controlled machine; there is no publish CI in this repo. -->
<PackageId>PdbLibrary</PackageId>
<Version>1.1.0</Version>
<Version>1.2.0</Version>
<Authors>BugSplat</Authors>
<Company>BugSplat</Company>
<Product>PdbLibrary</Product>
<Description>Extract GUIDs from Windows Executables and Symbol Files</Description>
<Description>Extract debug identity (GUID+age) from Windows PE images, MSF/CodeView PDBs, and Portable (ECMA-335) PDBs</Description>
<Copyright>Copyright BugSplat, All Rights Reserved</Copyright>
</PropertyGroup>

Expand Down
13 changes: 13 additions & 0 deletions UnitTestPdbLibrary/PdbLibraryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,19 @@ public void TestPDBFileGuid()
Assert.That(guid, Is.EqualTo("0C0E0F8243B54897952E4DB3E538A2361"));
}

[Test]
public void TestPortablePdbGuid()
{
// A Portable PDB (BSJB) — the format a .NET Core / modern SDK app's own
// assembly PDB ships in. Its {GUID}{age} key must equal the one a dump's
// CodeView record names, so the backend indexes it into the symsrv store
// under the same key bugsplat-cdb later looks it up by. This fixture's key
// is the directory it lives under in bugsplat-cdb's data/symbols-core store.
PDBFile pdbFile = new PDBFile(new FileInfo(TestData("MyDotNetCrasher.pdb")));
string guid = pdbFile.GUID.Value();
Assert.That(guid, Is.EqualTo("0023F679780F4F5D8DE2F203E88AB6721"));
}

[Test]
public void TestPEFileGuid()
{
Expand Down
Binary file not shown.