BrowserDebugProxy: unify debug metadata reading for PE and Webcil (#81099)
authorAleksey Kliger (λgeek) <alklig@microsoft.com>
Thu, 26 Jan 2023 02:33:00 +0000 (21:33 -0500)
committerGitHub <noreply@github.com>
Thu, 26 Jan 2023 02:33:00 +0000 (21:33 -0500)
* DebugStore: factor common PE and Webcil reading logic

* Move common logic to a MetadataDebugSummary class

Also switch from cascade of 'if's to a 'switch' when looking at debug entries

* Implement PDB checksum reader for WebcilReader

* Move WebcilReader reflection to a helper; add lazy initialization

Co-authored-by: Ankit Jain <radical@gmail.com>
src/libraries/Microsoft.NET.WebAssembly.Webcil/src/Webcil/WebcilReader.Reflection.cs [new file with mode: 0644]
src/libraries/Microsoft.NET.WebAssembly.Webcil/src/Webcil/WebcilReader.cs
src/mono/wasm/debugger/BrowserDebugProxy/DebugStore.cs
src/mono/wasm/debugger/BrowserDebugProxy/IDebugMetadataProvider.cs [new file with mode: 0644]
src/mono/wasm/debugger/BrowserDebugProxy/MetadataDebugSummary.cs [new file with mode: 0644]
src/mono/wasm/debugger/BrowserDebugProxy/PortableExecutableDebugMetadataProvider.cs [new file with mode: 0644]
src/mono/wasm/debugger/BrowserDebugProxy/WebcilDebugMetadataProvider.cs [new file with mode: 0644]

diff --git a/src/libraries/Microsoft.NET.WebAssembly.Webcil/src/Webcil/WebcilReader.Reflection.cs b/src/libraries/Microsoft.NET.WebAssembly.Webcil/src/Webcil/WebcilReader.Reflection.cs
new file mode 100644 (file)
index 0000000..56eae84
--- /dev/null
@@ -0,0 +1,59 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Immutable;
+using System.IO;
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+using System.Reflection.Metadata;
+using System.Reflection.PortableExecutable;
+
+namespace Microsoft.NET.WebAssembly.Webcil;
+
+
+public sealed partial class WebcilReader
+{
+
+    // Helpers to call into System.Reflection.Metadata internals
+    internal static class Reflection
+    {
+        private static readonly Lazy<MethodInfo> s_readUtf8NullTerminated = new Lazy<MethodInfo>(() =>
+        {
+            var mi = typeof(BlobReader).GetMethod("ReadUtf8NullTerminated", BindingFlags.NonPublic | BindingFlags.Instance);
+            if (mi == null)
+            {
+                throw new InvalidOperationException("Could not find BlobReader.ReadUtf8NullTerminated");
+            }
+            return mi;
+        });
+
+        internal static string? ReadUtf8NullTerminated(BlobReader reader) => (string?)s_readUtf8NullTerminated.Value.Invoke(reader, null);
+
+        private static readonly Lazy<ConstructorInfo> s_codeViewDebugDirectoryDataCtor = new Lazy<ConstructorInfo>(() =>
+        {
+            var types = new Type[] { typeof(Guid), typeof(int), typeof(string) };
+            var mi = typeof(CodeViewDebugDirectoryData).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, types, null);
+            if (mi == null)
+            {
+                throw new InvalidOperationException("Could not find CodeViewDebugDirectoryData constructor");
+            }
+            return mi;
+        });
+
+        internal static CodeViewDebugDirectoryData MakeCodeViewDebugDirectoryData(Guid guid, int age, string path) => (CodeViewDebugDirectoryData)s_codeViewDebugDirectoryDataCtor.Value.Invoke(new object[] { guid, age, path });
+
+        private static readonly Lazy<ConstructorInfo> s_pdbChecksumDebugDirectoryDataCtor = new Lazy<ConstructorInfo>(() =>
+        {
+            var types = new Type[] { typeof(string), typeof(ImmutableArray<byte>) };
+            var mi = typeof(PdbChecksumDebugDirectoryData).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, types, null);
+            if (mi == null)
+            {
+                throw new InvalidOperationException("Could not find PdbChecksumDebugDirectoryData constructor");
+            }
+            return mi;
+        });
+        internal static PdbChecksumDebugDirectoryData MakePdbChecksumDebugDirectoryData(string algorithmName, ImmutableArray<byte> checksum) => (PdbChecksumDebugDirectoryData)s_pdbChecksumDebugDirectoryDataCtor.Value.Invoke(new object[] { algorithmName, checksum });
+    }
+}
index 6782ebf..39d5813 100644 (file)
@@ -13,7 +13,7 @@ using System.Reflection.PortableExecutable;
 namespace Microsoft.NET.WebAssembly.Webcil;
 
 
-public sealed class WebcilReader : IDisposable
+public sealed partial class WebcilReader : IDisposable
 {
     // WISH:
     // This should be implemented in terms of System.Reflection.Internal.MemoryBlockProvider like the PEReader,
@@ -219,26 +219,11 @@ public sealed class WebcilReader : IDisposable
         return MakeCodeViewDebugDirectoryData(guid, age, path);
     }
 
-    private static string? ReadUtf8NullTerminated(BlobReader reader)
-    {
-        var mi = typeof(BlobReader).GetMethod("ReadUtf8NullTerminated", BindingFlags.NonPublic | BindingFlags.Instance);
-        if (mi == null)
-        {
-            throw new InvalidOperationException("Could not find BlobReader.ReadUtf8NullTerminated");
-        }
-        return (string?)mi.Invoke(reader, null);
-    }
+    private static string? ReadUtf8NullTerminated(BlobReader reader) => Reflection.ReadUtf8NullTerminated(reader);
 
-    private static CodeViewDebugDirectoryData MakeCodeViewDebugDirectoryData(Guid guid, int age, string path)
-    {
-        var types = new Type[] { typeof(Guid), typeof(int), typeof(string) };
-        var mi = typeof(CodeViewDebugDirectoryData).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, types, null);
-        if (mi == null)
-        {
-            throw new InvalidOperationException("Could not find CodeViewDebugDirectoryData constructor");
-        }
-        return (CodeViewDebugDirectoryData)mi.Invoke(new object[] { guid, age, path });
-    }
+    private static CodeViewDebugDirectoryData MakeCodeViewDebugDirectoryData(Guid guid, int age, string path) => Reflection.MakeCodeViewDebugDirectoryData(guid, age, path);
+
+    private static PdbChecksumDebugDirectoryData MakePdbChecksumDebugDirectoryData(string algorithmName, ImmutableArray<byte> checksum) => Reflection.MakePdbChecksumDebugDirectoryData(algorithmName, checksum);
 
     public MetadataReaderProvider ReadEmbeddedPortablePdbDebugDirectoryData(DebugDirectoryEntry entry)
     {
@@ -297,6 +282,44 @@ public sealed class WebcilReader : IDisposable
 
     }
 
+    public PdbChecksumDebugDirectoryData ReadPdbChecksumDebugDirectoryData(DebugDirectoryEntry entry)
+    {
+        if (entry.Type != DebugDirectoryEntryType.PdbChecksum)
+        {
+            throw new ArgumentException($"expected debug directory entry type {nameof(DebugDirectoryEntryType.PdbChecksum)}", nameof(entry));
+        }
+
+        var pos = entry.DataPointer;
+        var buffer = new byte[entry.DataSize];
+        if (_stream.Seek(pos, SeekOrigin.Begin) != pos)
+        {
+            throw new BadImageFormatException("Could not seek to CodeView debug directory data", nameof(_stream));
+        }
+        if (_stream.Read(buffer, 0, buffer.Length) != buffer.Length)
+        {
+            throw new BadImageFormatException("Could not read CodeView debug directory data", nameof(_stream));
+        }
+        unsafe
+        {
+            fixed (byte* p = buffer)
+            {
+                return DecodePdbChecksumDebugDirectoryData(new BlobReader(p, buffer.Length));
+            }
+        }
+    }
+
+    private static PdbChecksumDebugDirectoryData DecodePdbChecksumDebugDirectoryData(BlobReader reader)
+    {
+        var algorithmName = ReadUtf8NullTerminated(reader);
+        byte[]? checksum = reader.ReadBytes(reader.RemainingBytes);
+        if (string.IsNullOrEmpty(algorithmName) || checksum == null || checksum.Length == 0)
+        {
+            throw new BadImageFormatException("Invalid PdbChecksum data format");
+        }
+
+        return MakePdbChecksumDebugDirectoryData(algorithmName, ImmutableArray.Create(checksum));
+    }
+
     private long TranslateRVA(uint rva)
     {
         if (_sections == null)
index 06ddcb0..8596ca4 100644 (file)
@@ -894,100 +894,27 @@ namespace Microsoft.WebAssembly.Diagnostics
             this.id = Interlocked.Increment(ref next_id);
             this.logger = logger;
         }
-
         private static AssemblyInfo FromPEReader(MonoProxy monoProxy, SessionId sessionId, PEReader peReader, byte[] pdb, ILogger logger, CancellationToken token)
         {
-            var entries = peReader.ReadDebugDirectory();
-            CodeViewDebugDirectoryData? codeViewData = null;
-            var isPortableCodeView = false;
-            List<PdbChecksum> pdbChecksums = new();
-            foreach (var entry in peReader.ReadDebugDirectory())
-            {
-                if (entry.Type == DebugDirectoryEntryType.CodeView)
-                {
-                    codeViewData = peReader.ReadCodeViewDebugDirectoryData(entry);
-                    if (entry.IsPortableCodeView)
-                        isPortableCodeView = true;
-                }
-                if (entry.Type == DebugDirectoryEntryType.PdbChecksum)
-                {
-                    var checksum = peReader.ReadPdbChecksumDebugDirectoryData(entry);
-                    pdbChecksums.Add(new PdbChecksum(checksum.AlgorithmName, checksum.Checksum.ToArray()));
-                }
-            }
+
+            var debugProvider = new PortableExecutableDebugMetadataProvider(peReader);
+
             var asmMetadataReader = PEReaderExtensions.GetMetadataReader(peReader);
             string name = ReadAssemblyName(asmMetadataReader);
+            var summary = MetadataDebugSummary.Create(monoProxy, sessionId, name, debugProvider, pdb, token);
 
-            MetadataReader pdbMetadataReader = null;
-            if (pdb != null)
-            {
-                var pdbStream = new MemoryStream(pdb);
-                try
-                {
-                    // MetadataReaderProvider.FromPortablePdbStream takes ownership of the stream
-                    pdbMetadataReader = MetadataReaderProvider.FromPortablePdbStream(pdbStream).GetMetadataReader();
-                }
-                catch (BadImageFormatException)
-                {
-                    monoProxy.SendLog(sessionId, $"Warning: Unable to read debug information of: {name} (use DebugType=Portable/Embedded)", token);
-                }
-            }
-            else
-            {
-                var embeddedPdbEntry = entries.FirstOrDefault(e => e.Type == DebugDirectoryEntryType.EmbeddedPortablePdb);
-                if (embeddedPdbEntry.DataSize != 0)
-                {
-                    pdbMetadataReader = peReader.ReadEmbeddedPortablePdbDebugDirectoryData(embeddedPdbEntry).GetMetadataReader();
-                }
-            }
-
-            var assemblyInfo = new AssemblyInfo(peReader, name, asmMetadataReader, codeViewData, pdbChecksums.ToArray(), isPortableCodeView, pdbMetadataReader, logger);
+            var assemblyInfo = new AssemblyInfo(peReader, name, asmMetadataReader, summary, logger);
             return assemblyInfo;
         }
-
         private static AssemblyInfo FromWebcilReader(MonoProxy monoProxy, SessionId sessionId, WebcilReader wcReader, byte[] pdb, ILogger logger, CancellationToken token)
         {
-            var entries = wcReader.ReadDebugDirectory();
-            CodeViewDebugDirectoryData? codeViewData = null;
-            var isPortableCodeView = false;
-            List<PdbChecksum> pdbChecksums = new();
-            foreach (var entry in entries)
-            {
-                var codeView = entries[0];
-                if (codeView.Type == DebugDirectoryEntryType.CodeView)
-                {
-                    codeViewData = wcReader.ReadCodeViewDebugDirectoryData(codeView);
-                    if (codeView.IsPortableCodeView)
-                        isPortableCodeView = true;
-                }
-            }
+            var debugProvider = new WebcilDebugMetadataProvider(wcReader);
             var asmMetadataReader = wcReader.GetMetadataReader();
             string name = ReadAssemblyName(asmMetadataReader);
 
-            MetadataReader pdbMetadataReader = null;
-            if (pdb != null)
-            {
-                var pdbStream = new MemoryStream(pdb);
-                try
-                {
-                    // MetadataReaderProvider.FromPortablePdbStream takes ownership of the stream
-                    pdbMetadataReader = MetadataReaderProvider.FromPortablePdbStream(pdbStream).GetMetadataReader();
-                }
-                catch (BadImageFormatException)
-                {
-                    monoProxy.SendLog(sessionId, $"Warning: Unable to read debug information of: {name} (use DebugType=Portable/Embedded)", token);
-                }
-            }
-            else
-            {
-                var embeddedPdbEntry = entries.FirstOrDefault(e => e.Type == DebugDirectoryEntryType.EmbeddedPortablePdb);
-                if (embeddedPdbEntry.DataSize != 0)
-                {
-                    pdbMetadataReader = wcReader.ReadEmbeddedPortablePdbDebugDirectoryData(embeddedPdbEntry).GetMetadataReader();
-                }
-            }
+            var summary = MetadataDebugSummary.Create(monoProxy, sessionId, name, debugProvider, pdb, token);
 
-            var assemblyInfo = new AssemblyInfo(wcReader, name, asmMetadataReader, codeViewData, pdbChecksums.ToArray(), isPortableCodeView, pdbMetadataReader, logger);
+            var assemblyInfo = new AssemblyInfo(wcReader, name, asmMetadataReader, summary, logger);
             return assemblyInfo;
         }
 
@@ -997,10 +924,11 @@ namespace Microsoft.WebAssembly.Diagnostics
             return asmDef.GetAssemblyName().Name + ".dll";
         }
 
-        private unsafe AssemblyInfo(IDisposable owningReader, string name, MetadataReader asmMetadataReader, CodeViewDebugDirectoryData? codeViewData, PdbChecksum[] pdbChecksums, bool isPortableCodeView, MetadataReader pdbMetadataReader, ILogger logger)
+        private unsafe AssemblyInfo(IDisposable owningReader, string name, MetadataReader asmMetadataReader, MetadataDebugSummary summary, ILogger logger)
             : this(logger)
         {
             peReaderOrWebcilReader = owningReader;
+            var codeViewData = summary.CodeViewData;
             if (codeViewData != null)
             {
                 PdbAge = codeViewData.Value.Age;
@@ -1008,12 +936,12 @@ namespace Microsoft.WebAssembly.Diagnostics
                 PdbName = codeViewData.Value.Path;
                 CodeViewInformationAvailable = true;
             }
-            IsPortableCodeView = isPortableCodeView;
-            PdbChecksums = pdbChecksums;
+            IsPortableCodeView = summary.IsPortableCodeView;
+            PdbChecksums = summary.PdbChecksums;
             this.asmMetadataReader = asmMetadataReader;
             Name = name;
             logger.LogTrace($"Info: loading AssemblyInfo with name {Name}");
-            this.pdbMetadataReader = pdbMetadataReader;
+            this.pdbMetadataReader = summary.PdbMetadataReader;
             Populate();
         }
 
diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/IDebugMetadataProvider.cs b/src/mono/wasm/debugger/BrowserDebugProxy/IDebugMetadataProvider.cs
new file mode 100644 (file)
index 0000000..413a85c
--- /dev/null
@@ -0,0 +1,24 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable enable
+
+using System;
+using System.Collections.Immutable;
+using System.Reflection.Metadata;
+using System.Reflection.PortableExecutable;
+
+namespace Microsoft.WebAssembly.Diagnostics;
+
+/// <summary>
+///   An adapter on top of MetadataReader and WebcilReader for DebugStore compensating
+///   for the lack of a common base class on those two types.
+/// </summary>
+public interface IDebugMetadataProvider
+{
+    public ImmutableArray<DebugDirectoryEntry> ReadDebugDirectory();
+    public CodeViewDebugDirectoryData ReadCodeViewDebugDirectoryData(DebugDirectoryEntry entry);
+    public PdbChecksumDebugDirectoryData ReadPdbChecksumDebugDirectoryData(DebugDirectoryEntry entry);
+
+    public MetadataReaderProvider ReadEmbeddedPortablePdbDebugDirectoryData(DebugDirectoryEntry entry);
+}
diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MetadataDebugSummary.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MetadataDebugSummary.cs
new file mode 100644 (file)
index 0000000..714f0e9
--- /dev/null
@@ -0,0 +1,89 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.IO;
+using System.Linq;
+using System.Reflection.Metadata;
+using System.Reflection.PortableExecutable;
+using System.Threading;
+using Microsoft.FileFormats.PE;
+
+namespace Microsoft.WebAssembly.Diagnostics;
+
+/// <summary>
+///   Information we can extract directly from the assembly image using metadata readers
+/// </summary>
+internal sealed class MetadataDebugSummary
+{
+    internal MetadataReader? PdbMetadataReader { get; private init; }
+    internal bool IsPortableCodeView { get; private init; }
+    internal PdbChecksum[] PdbChecksums { get; private init; }
+
+    internal CodeViewDebugDirectoryData? CodeViewData { get; private init; }
+
+    private MetadataDebugSummary(MetadataReader? pdbMetadataReader, bool isPortableCodeView, PdbChecksum[] pdbChecksums, CodeViewDebugDirectoryData? codeViewData)
+    {
+        PdbMetadataReader = pdbMetadataReader;
+        IsPortableCodeView = isPortableCodeView;
+        PdbChecksums = pdbChecksums;
+        CodeViewData = codeViewData;
+    }
+
+    internal static MetadataDebugSummary Create(MonoProxy monoProxy, SessionId sessionId, string name, IDebugMetadataProvider provider, byte[]? pdb, CancellationToken token)
+    {
+        var entries = provider.ReadDebugDirectory();
+        CodeViewDebugDirectoryData? codeViewData = null;
+        bool isPortableCodeView = false;
+        List<PdbChecksum> pdbChecksums = new();
+        DebugDirectoryEntry? embeddedPdbEntry = null;
+        foreach (var entry in entries)
+        {
+            switch (entry.Type)
+            {
+                case DebugDirectoryEntryType.CodeView:
+                    codeViewData = provider.ReadCodeViewDebugDirectoryData(entry);
+                    if (entry.IsPortableCodeView)
+                        isPortableCodeView = true;
+                    break;
+                case DebugDirectoryEntryType.PdbChecksum:
+                    var checksum = provider.ReadPdbChecksumDebugDirectoryData(entry);
+                    pdbChecksums.Add(new PdbChecksum(checksum.AlgorithmName, checksum.Checksum.ToArray()));
+                    break;
+                case DebugDirectoryEntryType.EmbeddedPortablePdb:
+                    embeddedPdbEntry = entry;
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        MetadataReader? pdbMetadataReader = null;
+        if (pdb != null)
+        {
+            var pdbStream = new MemoryStream(pdb);
+            try
+            {
+                // MetadataReaderProvider.FromPortablePdbStream takes ownership of the stream
+                pdbMetadataReader = MetadataReaderProvider.FromPortablePdbStream(pdbStream).GetMetadataReader();
+            }
+            catch (BadImageFormatException)
+            {
+                monoProxy.SendLog(sessionId, $"Warning: Unable to read debug information of: {name} (use DebugType=Portable/Embedded)", token);
+            }
+        }
+        else
+        {
+            if (embeddedPdbEntry != null && embeddedPdbEntry.Value.DataSize != 0)
+            {
+                pdbMetadataReader = provider.ReadEmbeddedPortablePdbDebugDirectoryData(embeddedPdbEntry.Value).GetMetadataReader();
+            }
+        }
+
+        return new MetadataDebugSummary(pdbMetadataReader, isPortableCodeView, pdbChecksums.ToArray(), codeViewData);
+    }
+}
diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/PortableExecutableDebugMetadataProvider.cs b/src/mono/wasm/debugger/BrowserDebugProxy/PortableExecutableDebugMetadataProvider.cs
new file mode 100644 (file)
index 0000000..94aebb3
--- /dev/null
@@ -0,0 +1,27 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable enable
+
+using System;
+using System.Collections.Immutable;
+using System.Reflection.Metadata;
+using System.Reflection.PortableExecutable;
+
+namespace Microsoft.WebAssembly.Diagnostics;
+
+public class PortableExecutableDebugMetadataProvider : IDebugMetadataProvider
+{
+    private readonly PEReader _peReader;
+    public PortableExecutableDebugMetadataProvider(PEReader peReader)
+    {
+        _peReader = peReader;
+    }
+    public ImmutableArray<DebugDirectoryEntry> ReadDebugDirectory() => _peReader.ReadDebugDirectory();
+
+    public CodeViewDebugDirectoryData ReadCodeViewDebugDirectoryData(DebugDirectoryEntry entry) => _peReader.ReadCodeViewDebugDirectoryData(entry);
+
+    public PdbChecksumDebugDirectoryData ReadPdbChecksumDebugDirectoryData(DebugDirectoryEntry entry) => _peReader.ReadPdbChecksumDebugDirectoryData(entry);
+
+    public MetadataReaderProvider ReadEmbeddedPortablePdbDebugDirectoryData(DebugDirectoryEntry entry) => _peReader.ReadEmbeddedPortablePdbDebugDirectoryData(entry);
+}
diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/WebcilDebugMetadataProvider.cs b/src/mono/wasm/debugger/BrowserDebugProxy/WebcilDebugMetadataProvider.cs
new file mode 100644 (file)
index 0000000..993bbb7
--- /dev/null
@@ -0,0 +1,29 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable enable
+
+using System;
+using System.Collections.Immutable;
+using System.Reflection.Metadata;
+using System.Reflection.PortableExecutable;
+using Microsoft.NET.WebAssembly.Webcil;
+
+namespace Microsoft.WebAssembly.Diagnostics;
+
+public class WebcilDebugMetadataProvider : IDebugMetadataProvider
+{
+    private readonly WebcilReader _webcilReader;
+
+    public WebcilDebugMetadataProvider(WebcilReader webcilReader)
+    {
+        _webcilReader = webcilReader;
+    }
+    public ImmutableArray<DebugDirectoryEntry> ReadDebugDirectory() => _webcilReader.ReadDebugDirectory();
+
+    public CodeViewDebugDirectoryData ReadCodeViewDebugDirectoryData(DebugDirectoryEntry entry) => _webcilReader.ReadCodeViewDebugDirectoryData(entry);
+
+    public PdbChecksumDebugDirectoryData ReadPdbChecksumDebugDirectoryData(DebugDirectoryEntry entry) => _webcilReader.ReadPdbChecksumDebugDirectoryData(entry);
+
+    public MetadataReaderProvider ReadEmbeddedPortablePdbDebugDirectoryData(DebugDirectoryEntry entry) => _webcilReader.ReadEmbeddedPortablePdbDebugDirectoryData(entry);
+}