File: AxTlbBaseReference.cs
Web Access
Project: ..\..\..\src\Tasks\Microsoft.Build.Tasks.csproj (Microsoft.Build.Tasks.Core)
// 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.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;
 
using Microsoft.Build.Framework;
using Microsoft.Build.Shared.FileSystem;
using Microsoft.Build.Utilities;
 
#nullable disable
 
namespace Microsoft.Build.Tasks
{
    /// <summary>
    /// Common abstract base for aximp and tlbimp COM reference wrapper classes.
    /// They share the resolution method and only differ in constructing the wrapper file name.
    /// </summary>
    internal abstract class AxTlbBaseReference : ComReference
    {
        #region Constructors
        /// <summary>
        /// internal constructor
        /// </summary>
        /// <param name="taskLoggingHelper">task logger instance used for logging</param>
        /// <param name="silent">true if this task should log only errors, no warnings or messages; false otherwise</param>
        /// <param name="resolverCallback">callback interface for resolving dependent COM refs/NET assemblies</param>
        /// <param name="referenceInfo">cached reference information (typelib pointer, original task item, typelib name etc.)</param>
        /// <param name="itemName">reference name (for better logging experience)</param>
        /// <param name="outputDirectory">directory we should write the wrapper to</param>
        /// <param name="delaySign">delay sign wrappers?</param>
        /// <param name="keyFile">file containing public/private keys</param>
        /// <param name="keyContainer">container name for public/private keys</param>
        /// <param name="includeTypeLibVersionInName">True if the interop name should include the typelib's version</param>
        /// <param name="executeAsTool">True if GenerateWrapper() should generate the wrapper out-of-proc using aximp.exe or tlbimp.exe</param>
        /// <param name="toolPath">Path to the SDK tools directory where aximp.exe or tlbimp.exe can be found</param>
        /// <param name="buildEngine">BuildEngine of parent task; needed for logging purposes when generating wrapper out-of-proc</param>
        /// <param name="environmentVariables">Array of equals-separated pairs of environment variables that should be passed to the spawned executable, in addition to (or selectively overriding) the regular environment block.</param>
        internal AxTlbBaseReference(TaskLoggingHelper taskLoggingHelper, bool silent, IComReferenceResolver resolverCallback, ComReferenceInfo referenceInfo, string itemName, string outputDirectory, bool delaySign, string keyFile, string keyContainer, bool includeTypeLibVersionInName, bool executeAsTool, string toolPath, IBuildEngine buildEngine, string[] environmentVariables)
            : base(taskLoggingHelper, silent, referenceInfo, itemName)
        {
            ResolverCallback = resolverCallback;
            OutputDirectory = outputDirectory;
            IncludeTypeLibVersionInName = includeTypeLibVersionInName;
 
            BuildEngine = buildEngine;
            EnvironmentVariables = environmentVariables;
            DelaySign = delaySign;
            ExecuteAsTool = executeAsTool;
            KeyFile = keyFile;
            KeyContainer = keyContainer;
            ToolPath = toolPath;
        }
 
        #endregion
 
        #region Properties
 
        /// <summary>
        /// directory we should write the wrapper to
        /// </summary>
        protected virtual string OutputDirectory { get; }
 
        /// <summary>
        /// callback interface for resolving dependent COM refs/NET assemblies
        /// </summary>
        protected IComReferenceResolver ResolverCallback { get; }
 
        /// <summary>
        /// container name for public/private keys
        /// </summary>
        protected string KeyContainer { get; set; }
 
        /// <summary>
        /// file containing public/private keys
        /// </summary>
        protected string KeyFile { get; set; }
 
        /// <summary>
        /// True if generated wrappers should be delay signed
        /// </summary>
        protected bool DelaySign { get; set; }
 
        /// <summary>
        /// Property to allow multitargeting of ResolveComReferences:  If true, tlbimp.exe and
        /// aximp.exe from the appropriate target framework will be run out-of-proc to generate
        /// the necessary wrapper assemblies.
        /// </summary>
        protected bool ExecuteAsTool { get; set; }
 
        /// <summary>
        /// The BuildEngine of the ResolveComReference instance that created this instance
        /// of the class:  necessary for passing to the AxImp or TlbImp task that is spawned
        /// when ExecuteAsTool is set to true
        /// </summary>
        protected IBuildEngine BuildEngine { get; set; }
 
        /// <summary>
        /// Environment variables to pass to the tool.
        /// </summary>
        protected string[] EnvironmentVariables { get; set; }
 
        /// <summary>
        /// If ExecuteAsTool is true, this must be set to the SDK
        /// tools path for the framework version being targeted.
        /// </summary>
        protected string ToolPath { get; set; }
 
        /// <summary>
        /// When true, we include the typelib version number in the name.
        /// </summary>
        protected bool IncludeTypeLibVersionInName { get; set; }
 
        #endregion
 
        #region Methods
 
        /// <summary>
        /// Checks if there's a preexisting wrapper for this reference.
        /// </summary>
        internal override bool FindExistingWrapper(out ComReferenceWrapperInfo wrapperInfo, DateTime componentTimestamp)
        {
            wrapperInfo = null;
 
            string wrapperPath = GetWrapperPath();
 
            // now see if the wrapper assembly actually exists
            if (!FileSystems.Default.FileExists(wrapperPath))
            {
                return false;
            }
 
            wrapperInfo = new ComReferenceWrapperInfo { path = wrapperPath };
 
            return IsWrapperUpToDate(wrapperInfo, componentTimestamp);
        }
 
        /// <summary>
        /// Checks if the existing wrapper is up to date.
        /// </summary>
        protected virtual bool IsWrapperUpToDate(ComReferenceWrapperInfo wrapperInfo, DateTime componentTimestamp)
        {
            Debug.Assert(!string.IsNullOrEmpty(ReferenceInfo.strippedTypeLibPath), "ReferenceInfo.path should be valid if we got here");
            if (string.IsNullOrEmpty(ReferenceInfo.strippedTypeLibPath))
            {
                throw new ComReferenceResolutionException();
            }
 
            // if wrapper doesn't exist, wrapper is obviously not up to date
            if (!FileSystems.Default.FileExists(wrapperInfo.path))
            {
                return false;
            }
 
            // if typelib file has a DIFFERENT last write time, wrapper is not up to date
            // the reason we're comparing write times in an unusual way is that type libraries are unusual
            // "source files" for wrappers. If you upgrade/downgrade a system component, its write
            // time may be earlier than before but we should still regenerate the wrapper.
            if (DateTime.Compare(File.GetLastWriteTime(ReferenceInfo.strippedTypeLibPath), componentTimestamp) != 0)
            {
                return false;
            }
 
            // Compare our the existing wrapper's strong name state to the one we are requesting.
            if (!SigningRequirementsMatchExistingWrapper(wrapperInfo))
            {
                return false;
            }
 
            // ok, everything's looking fine, now just verify the assembly file is valid
            try
            {
                wrapperInfo.assembly = Assembly.UnsafeLoadFrom(wrapperInfo.path);
            }
            catch (BadImageFormatException)
            {
                // ouch, this assembly is malformed... need to regenerate the wrapper.
                wrapperInfo.assembly = null;
            }
 
            return wrapperInfo.assembly != null;
        }
 
        /// <summary>
        /// Constructs the wrapper file path.
        /// </summary>
        internal string GetWrapperPath()
        {
            // combine with the specified output directory
            return Path.Combine(OutputDirectory, GetWrapperFileName());
        }
 
        /// <summary>
        /// Helper method for constructing wrapper file name.
        /// </summary>
        internal string GetWrapperFileName()
        {
            return GetWrapperFileNameInternal(ReferenceInfo.typeLibName);
        }
 
        /*
         * Method:  GetWrapperFileName
         *
         *
         */
        /// <summary>
        /// Constructs the wrapper file name from a type library name. Specialized wrappers must override it if
        /// they want to use the Resolve method from this class.
        /// </summary>
        protected abstract string GetWrapperFileNameInternal(string typeLibName);
 
        /// <summary>
        /// Static version of GetWrapperFileName for use when calling from the outside.
        /// This version need only be used if the interop DLL needs to include the typelib version in the name
        /// Default implementation
        /// </summary>
        /// <param name="interopDllHeader">XXX, when the interop DLL is of the form XXX.typeLibName.[Y.Z.]dll</param>
        /// <param name="typeLibName">The typelib to generate the wrapper name for</param>
        /// <param name="includeTypeLibVersionInName">True if the interop name should include the typelib's version</param>
        /// <param name="majorVerNum">Major version number to append to the interop DLL's name</param>
        /// <param name="minorVerNum">Minor version number to append to the interop DLL's name</param>
        internal static string GetWrapperFileName(string interopDllHeader, string typeLibName, bool includeTypeLibVersionInName, short majorVerNum, short minorVerNum)
        {
            // create wrapper name of the format XXX.YYY[.Z.W].dll, where
            // XXX = the header ("Interop." or the like)
            // YYY = typeLibName
            // Z.W = optional TLB version number
            var builder = new StringBuilder(interopDllHeader);
            builder.Append(typeLibName);
 
            if (includeTypeLibVersionInName)
            {
                builder.Append('.');
                builder.Append(majorVerNum);
                builder.Append('.');
                builder.Append(minorVerNum);
            }
 
            builder.Append(".dll");
 
            return builder.ToString();
        }
 
        /// <summary>
        /// Given our KeyFile, KeyContainer, and DelaySign parameters, generate the public / private
        /// key pair and validate that it exists to the extent needed.
        /// </summary>
        internal void GetAndValidateStrongNameKey(out StrongNameKeyPair keyPair, out byte[] publicKey)
        {
 
            // get key pair/public key
            StrongNameUtils.GetStrongNameKey(Log, KeyFile, KeyContainer, out keyPair, out publicKey);
 
            // make sure we give as much data to the typelib converter as necessary but not more, or we might end up
            // with something we didn't want
            if (DelaySign)
            {
                keyPair = null;
 
                if (publicKey == null)
                {
                    Log.LogErrorWithCodeFromResources(null, ReferenceInfo.SourceItemSpec, 0, 0, 0, 0, "StrongNameUtils.NoPublicKeySpecified");
                    throw new StrongNameException();
                }
            }
            else
            {
                publicKey = null;
 
                // If the user did not specify delay sign and we didn't get a public/private
                // key pair then we have an error since a public key by itself is not enough
                // to fully sign the assembly. (only if either KeyContainer or KeyFile was specified though)
                if (keyPair == null)
                {
                    if (!string.IsNullOrEmpty(KeyContainer))
                    {
                        Log.LogErrorWithCodeFromResources(null, ReferenceInfo.SourceItemSpec, 0, 0, 0, 0, "ResolveComReference.StrongNameUtils.NoKeyPairInContainer", KeyContainer);
                        throw new StrongNameException();
                    }
                    if (!string.IsNullOrEmpty(KeyFile))
                    {
                        Log.LogErrorWithCodeFromResources(null, ReferenceInfo.SourceItemSpec, 0, 0, 0, 0, "ResolveComReference.StrongNameUtils.NoKeyPairInFile", KeyFile);
                        throw new StrongNameException();
                    }
                }
            }
        }
 
        /// <summary>
        /// Compare the strong name signing state of the existing wrapper to the signing
        /// state we are requesting in this run of the task. Return true if they match (e.g.
        /// from a signing perspective, the wrapper is up-to-date) or false otherwise.
        /// </summary>
        private bool SigningRequirementsMatchExistingWrapper(ComReferenceWrapperInfo wrapperInfo)
        {
            StrongNameLevel desiredStrongNameLevel = StrongNameLevel.None;
 
            if (!string.IsNullOrEmpty(KeyFile) || !string.IsNullOrEmpty(KeyContainer))
            {
                desiredStrongNameLevel = DelaySign ? StrongNameLevel.DelaySigned : StrongNameLevel.FullySigned;
            }
 
            // ...and see what we have already
            StrongNameLevel currentStrongNameLevel = StrongNameUtils.GetAssemblyStrongNameLevel(wrapperInfo.path);
 
            // if not matching, need to regenerate wrapper
            if (desiredStrongNameLevel != currentStrongNameLevel)
            {
                return false;
            }
 
            // if the wrapper needs a strong name, see if the public keys match
            if (desiredStrongNameLevel == StrongNameLevel.DelaySigned ||
                desiredStrongNameLevel == StrongNameLevel.FullySigned)
            {
                // get desired public key
                StrongNameUtils.GetStrongNameKey(Log, KeyFile, KeyContainer, out _, out byte[] desiredPublicKey);
 
                // get current public key
                AssemblyName assemblyName = AssemblyName.GetAssemblyName(wrapperInfo.path);
 
                if (assemblyName == null)
                {
                    return false;
                }
 
                byte[] currentPublicKey = assemblyName.GetPublicKey();
 
                if (currentPublicKey.Length != desiredPublicKey.Length)
                {
                    return false;
                }
 
                // compare public keys byte by byte
                for (int i = 0; i < currentPublicKey.Length; i++)
                {
                    if (currentPublicKey[i] != desiredPublicKey[i])
                    {
                        return false;
                    }
                }
            }
 
            return true;
        }
 
        #endregion
    }
}