File: Compilation.EmitStream.cs
Web Access
Project: src\src\Compilers\Core\Portable\Microsoft.CodeAnalysis.csproj (Microsoft.CodeAnalysis)
// 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.Diagnostics;
using System.IO;
using System.Reflection.PortableExecutable;
using Microsoft.CodeAnalysis.Interop;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis
{
    public abstract partial class Compilation
    {
        /// <summary>
        /// Describes the kind of real signing that is being done during Emit. In the case of public signing
        /// this value will be <see cref="None"/>.
        /// </summary>
        internal enum EmitStreamSignKind
        {
            None,
 
            /// <summary>
            /// This form of signing occurs in memory using the <see cref="PEBuilder"/> APIs. This is the default 
            /// form of signing and will be used when a strong name key is provided in a file on disk.
            /// </summary>
            SignedWithBuilder,
 
            /// <summary>
            /// This form of signing occurs using the <see cref="IClrStrongName"/> COM APIs. This form of signing
            /// requires the unsigned PE to be written to disk before it can be signed (typically by writing it
            /// out to the %TEMP% folder). This signing is used when the key in a key container, the signing 
            /// requires a counter signature or customers opted in via the UseLegacyStrongNameProvider feature 
            /// flag.
            /// </summary>
            SignedWithFile,
        }
 
        /// <summary>
        /// This type abstracts away the legacy COM based signing implementation for PE streams. Under the hood
        /// a temporary file must be created on disk (at the last possible moment), emitted to, signed on disk
        /// and then copied back to the original <see cref="Stream"/>. Only when legacy signing is enabled though.
        /// </summary>
        internal sealed class EmitStream
        {
            private readonly EmitStreamProvider _emitStreamProvider;
            private readonly EmitStreamSignKind _emitStreamSignKind;
            private readonly StrongNameProvider? _strongNameProvider;
            private readonly StrongNameKeys _strongNameKeys;
            private (Stream emitStream, Stream tempStream, string tempFilePath)? _tempInfo;
            private bool _created;
 
            internal EmitStream(
                EmitStreamProvider emitStreamProvider,
                EmitStreamSignKind emitStreamSignKind,
                StrongNameKeys strongNameKeys,
                StrongNameProvider? strongNameProvider)
            {
                RoslynDebug.Assert(emitStreamProvider != null);
                RoslynDebug.Assert(strongNameProvider != null || emitStreamSignKind == EmitStreamSignKind.None);
                _emitStreamProvider = emitStreamProvider;
                _emitStreamSignKind = emitStreamSignKind;
                _strongNameProvider = strongNameProvider;
                _strongNameKeys = strongNameKeys;
            }
 
            internal Func<Stream?> GetCreateStreamFunc(CommonMessageProvider messageProvider, DiagnosticBag diagnostics)
            {
                return () => CreateStream(messageProvider, diagnostics);
            }
 
            internal void Close()
            {
                // The emitStream tuple element is _deliberately_ not disposed here. That is owned by 
                // the EmitStreamProvider not us.
                if (_tempInfo is (Stream _, Stream tempStream, string tempFilePath))
                {
                    _tempInfo = null;
                    try
                    {
                        tempStream.Dispose();
                    }
                    finally
                    {
                        try
                        {
                            File.Delete(tempFilePath);
                        }
                        catch
                        {
                            // Not much to do if we can't delete from the temp directory
                        }
                    }
                }
            }
 
            /// <summary>
            /// Create the stream which should be used for Emit. This should only be called one time.
            /// </summary>
            /// <remarks>
            /// The <see cref="Stream"/> returned here is owned by this type and should not be disposed 
            /// by the caller.
            /// </remarks>
            private Stream? CreateStream(CommonMessageProvider messageProvider, DiagnosticBag diagnostics)
            {
                RoslynDebug.Assert(!_created);
                RoslynDebug.Assert(diagnostics != null);
 
                _created = true;
                if (diagnostics.HasAnyErrors())
                {
                    return null;
                }
 
                if (_emitStreamSignKind == EmitStreamSignKind.SignedWithFile)
                {
                    // Signing is going to be done with on disk files and that requires us to manage 
                    // multiple Stream instances. One for the on disk file and one for the actual emit
                    // stream the final PE should be written to.
                    RoslynDebug.Assert(_strongNameProvider != null);
 
                    var fileSystem = _strongNameProvider.FileSystem;
                    var tempDir = fileSystem.GetSigningTempPath();
                    if (tempDir is null)
                    {
                        diagnostics.Add(StrongNameKeys.GetError(
                            _strongNameKeys.KeyFilePath,
                            _strongNameKeys.KeyContainer,
                            new CodeAnalysisResourcesLocalizableErrorArgument(nameof(CodeAnalysisResources.SigningTempPathUnavailable)),
                            messageProvider));
                        return null;
                    }
 
                    var emitStream = _emitStreamProvider.GetOrCreateStream(diagnostics);
                    if (emitStream is null)
                    {
                        return null;
                    }
 
                    Stream tempStream;
                    string tempFilePath;
                    try
                    {
                        Func<string, Stream> streamConstructor = path => fileSystem.CreateFileStream(path, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite);
 
                        tempFilePath = Path.Combine(tempDir, Guid.NewGuid().ToString("N"));
                        tempStream = FileUtilities.CreateFileStreamChecked(streamConstructor, tempFilePath);
                    }
                    catch (IOException e)
                    {
                        throw new Cci.PeWritingException(e);
                    }
 
                    _tempInfo = (emitStream, tempStream, tempFilePath);
                    return tempStream;
                }
                else
                {
                    // Signing, if it occurs, will be done in memory so we can just return the final 
                    // Stream directly here.
                    return _emitStreamProvider.GetOrCreateStream(diagnostics);
                }
            }
 
            internal bool Complete(CommonMessageProvider messageProvider, DiagnosticBag diagnostics)
            {
                RoslynDebug.Assert(_created);
                RoslynDebug.Assert(_emitStreamSignKind != EmitStreamSignKind.SignedWithFile || _tempInfo.HasValue);
 
                try
                {
                    if (_tempInfo is (Stream emitStream, Stream tempStream, string tempFilePath))
                    {
                        RoslynDebug.Assert(_emitStreamSignKind == EmitStreamSignKind.SignedWithFile);
                        RoslynDebug.Assert(_strongNameProvider is object);
 
                        try
                        {
                            // Dispose the temp stream to ensure all of the contents are written to 
                            // disk.
                            tempStream.Dispose();
 
                            _strongNameProvider.SignFile(_strongNameKeys, tempFilePath);
 
                            using (var tempFileStream = new FileStream(tempFilePath, FileMode.Open))
                            {
                                tempFileStream.CopyTo(emitStream);
                            }
                        }
                        catch (DesktopStrongNameProvider.ClrStrongNameMissingException)
                        {
                            diagnostics.Add(StrongNameKeys.GetError(_strongNameKeys.KeyFilePath, _strongNameKeys.KeyContainer,
                                new CodeAnalysisResourcesLocalizableErrorArgument(nameof(CodeAnalysisResources.AssemblySigningNotSupported)), messageProvider));
                            return false;
                        }
                        catch (IOException ex)
                        {
                            diagnostics.Add(StrongNameKeys.GetError(_strongNameKeys.KeyFilePath, _strongNameKeys.KeyContainer, ex.Message, messageProvider));
                            return false;
                        }
                    }
                }
                finally
                {
                    Close();
                }
 
                return true;
            }
        }
    }
}