DTLib.Demystifier/Internal/PortablePdbReader.cs
2024-09-20 02:23:38 +05:00

113 lines
3.8 KiB
C#

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using System.Reflection.PortableExecutable;
namespace DTLib.Demystifier.Internal;
// Adapted from https://github.com/aspnet/Common/blob/dev/shared/Microsoft.Extensions.StackTrace.Sources/StackFrame/PortablePdbReader.cs
internal class PortablePdbReader : IDisposable
{
private readonly Dictionary<string, MetadataReaderProvider> _cache = new(StringComparer.Ordinal);
public void Dispose()
{
foreach (var entry in _cache) entry.Value.Dispose();
_cache.Clear();
}
public void PopulateStackFrame(StackFrame frameInfo, MethodBase method, int IlOffset, out string fileName,
out int row, out int column)
{
fileName = "";
row = 0;
column = 0;
if (method.Module.Assembly.IsDynamic) return;
var metadataReader = GetMetadataReader(method.Module.Assembly.Location);
if (metadataReader is null) return;
var methodToken = MetadataTokens.Handle(method.MetadataToken);
Debug.Assert(methodToken.Kind == HandleKind.MethodDefinition);
var handle = ((MethodDefinitionHandle)methodToken).ToDebugInformationHandle();
if (!handle.IsNil)
{
var methodDebugInfo = metadataReader.GetMethodDebugInformation(handle);
var sequencePoints = methodDebugInfo.GetSequencePoints();
SequencePoint? bestPointSoFar = null;
foreach (var point in sequencePoints)
{
if (point.Offset > IlOffset) break;
if (point.StartLine != SequencePoint.HiddenLine) bestPointSoFar = point;
}
if (bestPointSoFar.HasValue)
{
row = bestPointSoFar.Value.StartLine;
column = bestPointSoFar.Value.StartColumn;
fileName = metadataReader.GetString(metadataReader.GetDocument(bestPointSoFar.Value.Document).Name);
}
}
}
private MetadataReader? GetMetadataReader(string assemblyPath)
{
if (!_cache.TryGetValue(assemblyPath, out var provider) && provider is not null)
{
var pdbPath = GetPdbPath(assemblyPath);
if (!string.IsNullOrEmpty(pdbPath) && File.Exists(pdbPath) && IsPortable(pdbPath!))
{
var pdbStream = File.OpenRead(pdbPath);
provider = MetadataReaderProvider.FromPortablePdbStream(pdbStream);
}
_cache[assemblyPath] = provider;
}
return provider?.GetMetadataReader();
}
private static string? GetPdbPath(string assemblyPath)
{
if (string.IsNullOrEmpty(assemblyPath)) return null;
if (File.Exists(assemblyPath))
{
var peStream = File.OpenRead(assemblyPath);
using var peReader = new PEReader(peStream);
foreach (var entry in peReader.ReadDebugDirectory())
if (entry.Type == DebugDirectoryEntryType.CodeView)
{
var codeViewData = peReader.ReadCodeViewDebugDirectoryData(entry);
var peDirectory = Path.GetDirectoryName(assemblyPath);
return peDirectory is null
? null
: Path.Combine(peDirectory, Path.GetFileName(codeViewData.Path));
}
}
return null;
}
private static bool IsPortable(string pdbPath)
{
using var pdbStream = File.OpenRead(pdbPath);
return pdbStream.ReadByte() == 'B' &&
pdbStream.ReadByte() == 'S' &&
pdbStream.ReadByte() == 'J' &&
pdbStream.ReadByte() == 'B';
}
}