File: Emit\CompilationOutputs.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.IO;
using System.Linq;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Debugging;
 
namespace Microsoft.CodeAnalysis.Emit;
 
/// <summary>
/// Reads compilation outputs such as output assembly and PDB.
/// </summary>
internal abstract class CompilationOutputs
{
    /// <summary>
    /// String describing the assembly to be used in user facing error messages (e.g. file path).
    /// </summary>
    public abstract string? AssemblyDisplayPath { get; }
 
    /// <summary>
    /// String describing the PDB to be used in user facing error messages (e.g. file path).
    /// </summary>
    public abstract string? PdbDisplayPath { get; }
 
    /// <summary>
    /// Opens metadata section of the assembly file produced by the compiler.
    /// </summary>
    /// <param name="prefetch">
    /// True to prefetch all metadata from the assembly and close the underlying stream on return,
    /// otherwise keeps the underlying stream open until the returned <see cref="MetadataReaderProvider"/> is disposed.
    /// </param>
    /// <returns>
    /// Instance of <see cref="MetadataReaderProvider"/>, which owns the opened metadata and must be disposed once the caller is done reading the data, 
    /// or null if the assembly is not available.
    /// </returns>
    /// <exception cref="BadImageFormatException">Invalid format of the assembly data.</exception>
    /// <exception cref="InvalidOperationException">The stream returned by <see cref="OpenAssemblyStreamChecked"/> does not support read and seek operations.</exception>
    /// <exception cref="Exception">Error while reading assembly data.</exception>
    public virtual MetadataReaderProvider? OpenAssemblyMetadata(bool prefetch)
    {
        var peStream = OpenAssemblyStreamChecked();
        if (peStream == null)
        {
            return null;
        }
 
        PEHeaders peHeaders;
        using (var peReader = new PEReader(peStream, PEStreamOptions.LeaveOpen))
        {
            peHeaders = peReader.PEHeaders;
        }
 
        peStream.Position = peHeaders.MetadataStartOffset;
        return MetadataReaderProvider.FromMetadataStream(
            peStream,
            prefetch ? MetadataStreamOptions.PrefetchMetadata : MetadataStreamOptions.Default,
            size: peHeaders.MetadataSize);
    }
 
    /// <summary>
    /// Reads MVID of the output assembly. Overridable for test mocking.
    /// Returns <see cref="Guid.Empty"/> if the assembly is not available.
    /// </summary>
    internal virtual Guid ReadAssemblyModuleVersionId()
    {
        using var metadataProvider = OpenAssemblyMetadata(prefetch: false);
        if (metadataProvider == null)
        {
            return Guid.Empty;
        }
 
        var metadataReader = metadataProvider.GetMetadataReader();
        var mvidHandle = metadataReader.GetModuleDefinition().Mvid;
        return metadataReader.GetGuid(mvidHandle);
    }
 
    /// <summary>
    /// Opens PDB produced by the compiler.
    /// The caller must dispose the returned <see cref="DebugInformationReaderProvider"/>.
    /// </summary>
    /// <returns>
    /// Instance of <see cref="DebugInformationReaderProvider"/>, which owns the opened PDB and must be disposed once the caller is done reading the data,
    /// or null if PDB is not available.
    /// </returns>
    /// <exception cref="BadImageFormatException">Invalid format of the PDB or assembly data.</exception>
    /// <exception cref="InvalidOperationException">The stream returned by <see cref="OpenPdbStreamChecked"/> or <see cref="OpenAssemblyStreamChecked"/> does not support read and seek operations.</exception>
    /// <exception cref="Exception">Error while reading assembly data.</exception>
    /// <remarks>
    /// If a separate PDB stream is not available (<see cref="OpenPdbStreamChecked"/> returns null) opens the PDB embedded in the assembly, if present.
    /// </remarks>
    public virtual DebugInformationReaderProvider? OpenPdb()
    {
        var pdbStream = OpenPdbStreamChecked();
        if (pdbStream != null)
        {
            return DebugInformationReaderProvider.CreateFromStream(pdbStream);
        }
 
        // check for embedded PDB
        var peStream = OpenAssemblyStreamChecked();
        if (peStream != null)
        {
            using var peReader = new PEReader(peStream);
            var embeddedPdbEntry = peReader.ReadDebugDirectory().FirstOrDefault(e => e.Type == DebugDirectoryEntryType.EmbeddedPortablePdb);
            if (embeddedPdbEntry.DataSize != 0)
            {
                return DebugInformationReaderProvider.CreateFromMetadataReader(peReader.ReadEmbeddedPortablePdbDebugDirectoryData(embeddedPdbEntry));
            }
        }
 
        return null;
    }
 
    private static Stream? ValidateStream(Stream? stream, string methodName)
    {
        if (stream != null && (!stream.CanRead || !stream.CanSeek))
        {
            throw new InvalidOperationException(string.Format(FeaturesResources.MethodMustReturnStreamThatSupportsReadAndSeek, methodName));
        }
 
        return stream;
    }
 
    private Stream? OpenPdbStreamChecked()
        => ValidateStream(OpenPdbStream(), nameof(OpenPdbStream));
 
    private Stream? OpenAssemblyStreamChecked()
        => ValidateStream(OpenAssemblyStream(), nameof(OpenAssemblyStream));
 
    /// <summary>
    /// Opens an assembly file produced by the compiler.
    /// </summary>
    /// <remarks>
    /// The stream must be readable and seekable.
    /// </remarks>
    /// <returns>New <see cref="Stream"/> instance or null if the assembly is not available.</returns>
    protected abstract Stream? OpenAssemblyStream();
 
    /// <summary>
    /// Opens a PDB file produced by the compiler.
    /// </summary>
    /// <remarks>
    /// The stream must be readable and seekable.
    /// </remarks>
    /// <returns>New <see cref="Stream"/> instance or null if the compiler generated no PDB (the symbols might be embedded in the assembly).</returns>
    protected abstract Stream? OpenPdbStream();
 
    internal async ValueTask<bool> TryCopyAssemblyToAsync(Stream stream, CancellationToken cancellationToken)
    {
        var peImage = OpenAssemblyStreamChecked();
        if (peImage == null)
        {
            return false;
        }
 
        await peImage.CopyToAsync(stream, bufferSize: 4 * 1024, cancellationToken).ConfigureAwait(false);
        return true;
    }
 
    internal async ValueTask<bool> TryCopyPdbToAsync(Stream stream, CancellationToken cancellationToken)
    {
        using var pdb = OpenPdb();
        if (pdb == null)
        {
            return false;
        }
 
        await pdb.CopyContentToAsync(stream, cancellationToken).ConfigureAwait(false);
        return true;
    }
}