diff --git a/src/Microsoft.Extensions.Testing.Abstractions/FullPdbReader.cs b/src/Microsoft.Extensions.Testing.Abstractions/FullPdbReader.cs index b4c08e599..d5339096b 100644 --- a/src/Microsoft.Extensions.Testing.Abstractions/FullPdbReader.cs +++ b/src/Microsoft.Extensions.Testing.Abstractions/FullPdbReader.cs @@ -51,25 +51,32 @@ namespace Microsoft.Extensions.Testing.Abstractions private static ISymUnmanagedReader3 CreateNativeSymReader(Stream pdbStream) { - object symReader = null; - var guid = default(Guid); - if (IntPtr.Size == 4) + try { - CreateSymReader32(ref guid, out symReader); + object symReader = null; + var guid = default(Guid); + if (IntPtr.Size == 4) + { + CreateSymReader32(ref guid, out symReader); + } + else + { + CreateSymReader64(ref guid, out symReader); + } + var reader = (ISymUnmanagedReader3)symReader; + var hr = reader.Initialize(new DummyMetadataImport(), null, null, new ComStreamWrapper(pdbStream)); + SymUnmanagedReaderExtensions.ThrowExceptionForHR(hr); + return reader; } - else + catch (Exception e) { - CreateSymReader64(ref guid, out symReader); + throw new IOException(e.Message, e); } - var reader = (ISymUnmanagedReader3)symReader; - var hr = reader.Initialize(new DummyMetadataImport(), null, null, new ComStreamWrapper(pdbStream)); - SymUnmanagedReaderExtensions.ThrowExceptionForHR(hr); - return reader; } public void Dispose() { - ((ISymUnmanagedDispose) _symReader).Destroy(); + ((ISymUnmanagedDispose)_symReader).Destroy(); } } diff --git a/src/Microsoft.Extensions.Testing.Abstractions/IPdbReaderFactory.cs b/src/Microsoft.Extensions.Testing.Abstractions/IPdbReaderFactory.cs index e3ee8b53f..d6f7a23e3 100644 --- a/src/Microsoft.Extensions.Testing.Abstractions/IPdbReaderFactory.cs +++ b/src/Microsoft.Extensions.Testing.Abstractions/IPdbReaderFactory.cs @@ -1,12 +1,21 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; using System.IO; namespace Microsoft.Extensions.Testing.Abstractions { public interface IPdbReaderFactory { + /// + /// Creates for given file. + /// + /// + /// Path to the .pdb file or a PE file that refers to the .pdb file in its Debug Directory Table. + /// + /// File does not exist or can't be read. + /// is null. IPdbReader Create(string pdbPath); } } diff --git a/src/Microsoft.Extensions.Testing.Abstractions/MissingPdbReader.cs b/src/Microsoft.Extensions.Testing.Abstractions/MissingPdbReader.cs new file mode 100644 index 000000000..c4b860670 --- /dev/null +++ b/src/Microsoft.Extensions.Testing.Abstractions/MissingPdbReader.cs @@ -0,0 +1,12 @@ +using System.Reflection; + +namespace Microsoft.Extensions.Testing.Abstractions +{ + internal sealed class MissingPdbReader : IPdbReader + { + public static readonly MissingPdbReader Instance = new MissingPdbReader(); + + public SourceInformation GetSourceInformation(MethodInfo methodInfo) => null; + public void Dispose() { } + } +} diff --git a/src/Microsoft.Extensions.Testing.Abstractions/PdbReaderFactory.cs b/src/Microsoft.Extensions.Testing.Abstractions/PdbReaderFactory.cs index 24e3eb119..8c05227fe 100644 --- a/src/Microsoft.Extensions.Testing.Abstractions/PdbReaderFactory.cs +++ b/src/Microsoft.Extensions.Testing.Abstractions/PdbReaderFactory.cs @@ -3,31 +3,103 @@ using System; using System.IO; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; namespace Microsoft.Extensions.Testing.Abstractions { public class PdbReaderFactory : IPdbReaderFactory { + /// + /// Creates for given file. + /// + /// + /// Path to the .pdb file or a PE file that refers to the .pdb file in its Debug Directory Table. + /// + /// File does not exist or can't be read. + /// is null. public IPdbReader Create(string pdbPath) { - var pdbStream = new FileStream(pdbPath, FileMode.Open, FileAccess.Read); - - if (IsPortable(pdbStream)) + if (pdbPath == null) { - return new PortablePdbReader(pdbStream); + throw new ArgumentNullException(nameof(pdbPath)); } - else + + Stream stream = OpenRead(pdbPath); + + if (IsPE(stream)) { - return new FullPdbReader(pdbStream); + return CreateFromPortableExecutable(stream, pdbPath); + } + + if (IsPortable(stream)) + { + return new PortablePdbReader(stream); + } + + return new FullPdbReader(stream); + } + + private static bool IsPortable(Stream stream) + { + bool result = stream.ReadByte() == 'B' && stream.ReadByte() == 'S' && stream.ReadByte() == 'J' && stream.ReadByte() == 'B'; + stream.Position = 0; + return result; + } + + private static bool IsPE(Stream stream) + { + bool result = stream.ReadByte() == 'M' && stream.ReadByte() == 'Z'; + stream.Position = 0; + return result; + } + + private IPdbReader CreateFromPortableExecutable(Stream peStream, string pePath) + { + using (var peReader = new PEReader(peStream)) + { + MetadataReaderProvider pdbProvider; + string pdbPath; + if (peReader.TryOpenAssociatedPortablePdb(pePath, TryOpenRead, out pdbProvider, out pdbPath)) + { + return new PortablePdbReader(pdbProvider); + } + + return MissingPdbReader.Instance; } } - private static bool IsPortable(Stream pdbStream) + private static Stream OpenRead(string path) { - return pdbStream.ReadByte() == 'B' && - pdbStream.ReadByte() == 'S' && - pdbStream.ReadByte() == 'J' && - pdbStream.ReadByte() == 'B'; + try + { + return File.OpenRead(path); + } + catch (Exception e) when (!(e is IOException)) + { + throw new IOException(e.Message, e); + } + } + + private static Stream TryOpenRead(string path) + { + if (!File.Exists(path)) + { + return null; + } + + try + { + return File.OpenRead(path); + } + catch (FileNotFoundException) + { + return null; + } + catch (Exception e) when (!(e is IOException)) + { + throw new IOException(e.Message, e); + } } } } diff --git a/src/Microsoft.Extensions.Testing.Abstractions/PortablePdbReader.cs b/src/Microsoft.Extensions.Testing.Abstractions/PortablePdbReader.cs index 80ea95cf2..7b6d11220 100644 --- a/src/Microsoft.Extensions.Testing.Abstractions/PortablePdbReader.cs +++ b/src/Microsoft.Extensions.Testing.Abstractions/PortablePdbReader.cs @@ -6,27 +6,23 @@ using System.IO; using System.Linq; using System.Reflection; using System.Reflection.Metadata; -using System.Runtime.InteropServices; namespace Microsoft.Extensions.Testing.Abstractions { public class PortablePdbReader : IPdbReader { private MetadataReader _reader; - private GCHandle _gcHandle; + private MetadataReaderProvider _provider; - public PortablePdbReader(Stream pdbStream) + public PortablePdbReader(Stream stream) + : this(MetadataReaderProvider.FromPortablePdbStream(stream)) { - pdbStream.Position = 0; - var buffer = new byte[pdbStream.Length]; - pdbStream.Read(buffer, 0, buffer.Length); - - _gcHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned); - unsafe - { - var address = _gcHandle.AddrOfPinnedObject(); - _reader = new MetadataReader((byte*)address.ToPointer(), buffer.Length); - } + } + + internal PortablePdbReader(MetadataReaderProvider provider) + { + _provider = provider; + _reader = provider.GetMetadataReader(); } public SourceInformation GetSourceInformation(MethodInfo methodInfo) @@ -43,6 +39,11 @@ namespace Microsoft.Extensions.Testing.Abstractions private SourceInformation GetSourceInformation(MethodDebugInformationHandle handle) { + if (_reader == null) + { + throw new ObjectDisposedException(nameof(PortablePdbReader)); + } + SourceInformation sourceInformation = null; try { @@ -81,7 +82,9 @@ namespace Microsoft.Extensions.Testing.Abstractions public void Dispose() { - _gcHandle.Free(); + _provider?.Dispose(); + _provider = null; + _reader = null; } } } diff --git a/src/Microsoft.Extensions.Testing.Abstractions/SourceInformationProvider.cs b/src/Microsoft.Extensions.Testing.Abstractions/SourceInformationProvider.cs index 1e018cc0e..eadfaa743 100644 --- a/src/Microsoft.Extensions.Testing.Abstractions/SourceInformationProvider.cs +++ b/src/Microsoft.Extensions.Testing.Abstractions/SourceInformationProvider.cs @@ -10,23 +10,44 @@ namespace Microsoft.Extensions.Testing.Abstractions { public class SourceInformationProvider : ISourceInformationProvider { - private readonly string _pdbPath; + private readonly string _filePath; private readonly IPdbReader _pdbReader; + /// + /// Creates a source info provide from a specified file path. + /// + /// + /// Path to the .pdb file or a PE file that refers to the .pdb file in its Debug Directory Table. + /// public SourceInformationProvider(string pdbPath) : this(pdbPath, new PdbReaderFactory()) { } + /// + /// Creates a source info provide from a specified file path. + /// + /// + /// Path to the .pdb file or a PE file that refers to the .pdb file in its Debug Directory Table. + /// + /// + /// Factory that creates instance used to read the PDB. + /// + /// File does not exist or can't be read. + /// or is null. public SourceInformationProvider(string pdbPath, IPdbReaderFactory pdbReaderFactory) { - if (string.IsNullOrWhiteSpace(pdbPath) || !File.Exists(pdbPath)) + if (pdbPath == null) { - throw new ArgumentException($"The file '{pdbPath}' does not exist.", nameof(pdbPath)); + throw new ArgumentNullException(nameof(pdbPath)); } - _pdbPath = pdbPath; + if (pdbReaderFactory == null) + { + throw new ArgumentNullException(nameof(pdbReaderFactory)); + } + _filePath = pdbPath; _pdbReader = pdbReaderFactory.Create(pdbPath); } diff --git a/src/Microsoft.Extensions.Testing.Abstractions/project.json b/src/Microsoft.Extensions.Testing.Abstractions/project.json index 5f1ccc646..649f67315 100644 --- a/src/Microsoft.Extensions.Testing.Abstractions/project.json +++ b/src/Microsoft.Extensions.Testing.Abstractions/project.json @@ -13,7 +13,7 @@ }, "Microsoft.DiaSymReader": "1.0.8", "Microsoft.DiaSymReader.Native": "1.4.0-rc2", - "System.Reflection.Metadata": "1.4.1-beta-24410-02" + "System.Reflection.Metadata": "1.4.1-beta-24426-02" }, "frameworks": { "net451": {},