File: AssemblyDependency\AssemblyInformation.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.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
#if !FEATURE_ASSEMBLYLOADCONTEXT
using System.Linq;
using System.Runtime.InteropServices;
#endif
using System.Reflection;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using Microsoft.Build.Shared;
using Microsoft.Build.Shared.FileSystem;
using static Microsoft.Build.Shared.FileSystem.WindowsNative;
#if FEATURE_ASSEMBLYLOADCONTEXT
using System.Reflection.PortableExecutable;
using System.Reflection.Metadata;
#endif
using Microsoft.Build.Tasks.AssemblyDependency;
 
#nullable disable
 
namespace Microsoft.Build.Tasks
{
    /// <summary>
    /// Collection of methods used to discover assembly metadata.
    /// Primarily stolen from manifestutility.cs AssemblyMetaDataImport class.
    /// </summary>
    internal class AssemblyInformation : DisposableBase
    {
        private AssemblyNameExtension[] _assemblyDependencies;
        private string[] _assemblyFiles;
#if !FEATURE_ASSEMBLYLOADCONTEXT
        private readonly IMetaDataDispenser _metadataDispenser;
        private readonly IMetaDataAssemblyImport _assemblyImport;
        private static Guid s_importerGuid = new Guid(((GuidAttribute)Attribute.GetCustomAttribute(typeof(IMetaDataImport), typeof(GuidAttribute), false)).Value);
        private readonly Assembly _assembly;
#endif
        private readonly string _sourceFile;
        private FrameworkName _frameworkName;
 
#if FEATURE_ASSEMBLYLOADCONTEXT
        private bool _metadataRead;
#endif
 
#if !FEATURE_ASSEMBLYLOADCONTEXT
        private static string s_targetFrameworkAttribute = "System.Runtime.Versioning.TargetFrameworkAttribute";
#endif
#if !FEATURE_ASSEMBLYLOADCONTEXT
        // Borrowed from genman.
        private const int GENMAN_STRING_BUF_SIZE = 1024;
        private const int GENMAN_LOCALE_BUF_SIZE = 64;
        private const int GENMAN_ENUM_TOKEN_BUF_SIZE = 16; // 128 from genman seems too big.
 
        static AssemblyInformation()
        {
            AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += ReflectionOnlyAssemblyResolve;
        }
#endif // FEATURE_ASSEMBLY_LOADFROM
 
        /// <summary>
        /// Construct an instance for a source file.
        /// </summary>
        /// <param name="sourceFile">The assembly.</param>
        internal AssemblyInformation(string sourceFile)
        {
            // Extra checks for PInvoke-destined data.
            ErrorUtilities.VerifyThrowArgumentNull(sourceFile);
            _sourceFile = sourceFile;
 
#if !FEATURE_ASSEMBLYLOADCONTEXT
            if (NativeMethodsShared.IsWindows)
            {
                // Create the metadata dispenser and open scope on the source file.
                _metadataDispenser = (IMetaDataDispenser)new CorMetaDataDispenser();
                _assemblyImport = (IMetaDataAssemblyImport)_metadataDispenser.OpenScope(sourceFile, 0, ref s_importerGuid);
            }
            else
            {
                _assembly = Assembly.ReflectionOnlyLoadFrom(sourceFile);
            }
#endif
        }
 
#if !FEATURE_ASSEMBLYLOADCONTEXT
        private static Assembly ReflectionOnlyAssemblyResolve(object sender, ResolveEventArgs args)
        {
            string[] nameParts = args.Name.Split(MSBuildConstants.CommaChar);
            Assembly assembly = null;
 
            if (args.RequestingAssembly != null && !string.IsNullOrEmpty(args.RequestingAssembly.Location) && nameParts.Length > 0)
            {
                var location = args.RequestingAssembly.Location;
                var newLocation = Path.Combine(Path.GetDirectoryName(location), nameParts[0].Trim() + ".dll");
 
                try
                {
                    if (FileSystems.Default.FileExists(newLocation))
                    {
                        assembly = Assembly.ReflectionOnlyLoadFrom(newLocation);
                    }
                }
                catch
                {
                }
            }
 
            // Let's try to automatically load it
            if (assembly == null)
            {
                try
                {
                    assembly = Assembly.ReflectionOnlyLoad(args.Name);
                }
                catch
                {
                }
            }
 
            return assembly;
        }
#endif
 
        /// <summary>
        /// Get the dependencies.
        /// </summary>
        /// <value></value>
        public AssemblyNameExtension[] Dependencies
        {
            get
            {
                if (_assemblyDependencies == null)
                {
                    lock (this)
                    {
                        if (_assemblyDependencies == null)
                        {
                            _assemblyDependencies = ImportAssemblyDependencies();
                        }
                    }
                }
 
                return _assemblyDependencies;
            }
        }
 
        /// <summary>
        /// Get the scatter files from the assembly metadata.
        /// </summary>
        public string[] Files
        {
            get
            {
                if (_assemblyFiles == null)
                {
                    lock (this)
                    {
                        if (_assemblyFiles == null)
                        {
                            _assemblyFiles = ImportFiles();
                        }
                    }
                }
 
                return _assemblyFiles;
            }
        }
 
        /// <summary>
        /// What was the framework name that the assembly was built against.
        /// </summary>
        public FrameworkName FrameworkNameAttribute
        {
            get
            {
                if (_frameworkName == null)
                {
                    lock (this)
                    {
                        if (_frameworkName == null)
                        {
                            _frameworkName = GetFrameworkName();
                        }
                    }
                }
 
                return _frameworkName;
            }
        }
 
        /// <summary>
        /// Given an assembly name, crack it open and retrieve the list of dependent
        /// assemblies and  the list of scatter files.
        /// </summary>
        /// <param name="path">Path to the assembly.</param>
        /// <param name="assemblyMetadataCache">Cache of pre-extracted assembly metadata.</param>
        /// <param name="dependencies">Receives the list of dependencies.</param>
        /// <param name="scatterFiles">Receives the list of associated scatter files.</param>
        /// <param name="frameworkName">Gets the assembly name.</param>
        internal static void GetAssemblyMetadata(
            string path,
            ConcurrentDictionary<string, AssemblyMetadata> assemblyMetadataCache,
            out AssemblyNameExtension[] dependencies,
            out string[] scatterFiles,
            out FrameworkName frameworkName)
        {
            var import = assemblyMetadataCache?.GetOrAdd(path, p => new AssemblyMetadata(p))
                ?? new AssemblyMetadata(path);
 
            dependencies = import.Dependencies;
            frameworkName = import.FrameworkName;
            scatterFiles = import.ScatterFiles;
        }
 
        /// <summary>
        /// Given an assembly name, crack it open and retrieve the TargetFrameworkAttribute
        /// assemblies and  the list of scatter files.
        /// </summary>
        internal static FrameworkName GetTargetFrameworkAttribute(string path)
        {
            using (var import = new AssemblyInformation(path))
            {
                return import.FrameworkNameAttribute;
            }
        }
 
        /// <summary>
        /// Determine if an file is a winmd file or not.
        /// </summary>
        internal static bool IsWinMDFile(
            string fullPath,
            GetAssemblyRuntimeVersion getAssemblyRuntimeVersion,
            FileExists fileExists,
            out string imageRuntimeVersion,
            out bool isManagedWinmd)
        {
            imageRuntimeVersion = String.Empty;
            isManagedWinmd = false;
 
            if (!NativeMethodsShared.IsWindows)
            {
                return false;
            }
 
            // May be null or empty is the file was never resolved to a path on disk.
            if (!String.IsNullOrEmpty(fullPath) && fileExists(fullPath))
            {
                imageRuntimeVersion = getAssemblyRuntimeVersion(fullPath);
                if (!String.IsNullOrEmpty(imageRuntimeVersion))
                {
                    bool containsWindowsRuntime = imageRuntimeVersion.IndexOf(
                        "WindowsRuntime",
                        StringComparison.OrdinalIgnoreCase) >= 0;
 
                    if (containsWindowsRuntime)
                    {
                        isManagedWinmd = imageRuntimeVersion.IndexOf("CLR", StringComparison.OrdinalIgnoreCase) >= 0;
                        return true;
                    }
                }
            }
 
            return false;
        }
 
#if !FEATURE_ASSEMBLYLOADCONTEXT
        /// <summary>
        /// Collects the metadata and attributes for specified assembly.
        /// The requested properties are used by legacy project system.
        /// </summary>
        internal AssemblyAttributes GetAssemblyMetadata()
        {
            IntPtr asmMetaPtr = IntPtr.Zero;
            ASSEMBLYMETADATA asmMeta = new();
            try
            {
                IMetaDataImport2 import2 = (IMetaDataImport2)_assemblyImport;
                _assemblyImport.GetAssemblyFromScope(out uint assemblyScope);
 
                // get the assembly, if there is no assembly, it is a module reference
                if (assemblyScope == 0)
                {
                    return null;
                }
 
                AssemblyAttributes assemblyAttributes = new()
                {
                    AssemblyFullPath = _sourceFile,
                    IsAssembly = true,
                };
 
                // will be populated with the assembly name
                char[] defaultCharArray = new char[GENMAN_STRING_BUF_SIZE];
                asmMetaPtr = AllocAsmMeta();
                _assemblyImport.GetAssemblyProps(
                    assemblyScope,
                    out IntPtr publicKeyPtr,
                    out uint publicKeyLength,
                    out uint hashAlgorithmId,
                    defaultCharArray,
 
                    // the default buffer size is taken from csproj call
                    GENMAN_STRING_BUF_SIZE,
                    out uint nameLength,
                    asmMetaPtr,
                    out uint flags);
 
                assemblyAttributes.AssemblyName = new string(defaultCharArray, 0, (int)nameLength - 1);
                assemblyAttributes.DefaultAlias = assemblyAttributes.AssemblyName;
 
                asmMeta = (ASSEMBLYMETADATA)Marshal.PtrToStructure(asmMetaPtr, typeof(ASSEMBLYMETADATA));
                assemblyAttributes.MajorVersion = asmMeta.usMajorVersion;
                assemblyAttributes.MinorVersion = asmMeta.usMinorVersion;
                assemblyAttributes.RevisionNumber = asmMeta.usRevisionNumber;
                assemblyAttributes.BuildNumber = asmMeta.usBuildNumber;
                assemblyAttributes.Culture = Marshal.PtrToStringUni(asmMeta.rpLocale);
 
                byte[] publicKey = new byte[publicKeyLength];
                Marshal.Copy(publicKeyPtr, publicKey, 0, (int)publicKeyLength);
                assemblyAttributes.PublicHexKey = BitConverter.ToString(publicKey).Replace("-", string.Empty);
 
                if (import2 != null)
                {
                    assemblyAttributes.Description = GetStringCustomAttribute(import2, assemblyScope, "System.Reflection.AssemblyDescriptionAttribute");
                    assemblyAttributes.TargetFrameworkMoniker = GetStringCustomAttribute(import2, assemblyScope, "System.Runtime.Versioning.TargetFrameworkAttribute");
                    var guid = GetStringCustomAttribute(import2, assemblyScope, "System.Runtime.InteropServices.GuidAttribute");
                    if (!string.IsNullOrEmpty(guid))
                    {
                        string importedFromTypeLibString = GetStringCustomAttribute(import2, assemblyScope, "System.Runtime.InteropServices.ImportedFromTypeLibAttribute");
                        if (!string.IsNullOrEmpty(importedFromTypeLibString))
                        {
                            assemblyAttributes.IsImportedFromTypeLib = true;
                        }
                        else
                        {
                            string primaryInteropAssemblyString = GetStringCustomAttribute(import2, assemblyScope, "System.Runtime.InteropServices.PrimaryInteropAssemblyAttribute");
                            assemblyAttributes.IsImportedFromTypeLib = !string.IsNullOrEmpty(primaryInteropAssemblyString);
                        }
                    }
                }
 
                assemblyAttributes.RuntimeVersion = GetRuntimeVersion(_sourceFile);
 
                import2.GetPEKind(out uint peKind, out _);
                assemblyAttributes.PeKind = peKind;
 
                return assemblyAttributes;
            }
            finally
            {
                FreeAsmMeta(asmMetaPtr, ref asmMeta);
            }
        }
 
        private string GetStringCustomAttribute(IMetaDataImport2 import2, uint assemblyScope, string attributeName)
        {
            int hr = import2.GetCustomAttributeByName(assemblyScope, attributeName, out IntPtr data, out uint valueLen);
 
            if (hr == NativeMethodsShared.S_OK)
            {
                // if an custom attribute exists, parse the contents of the blob
                if (NativeMethods.TryReadMetadataString(_sourceFile, data, valueLen, out string propertyValue))
                {
                    return propertyValue;
                }
            }
 
            return string.Empty;
        }
#endif
 
        /// <summary>
        /// Get the framework name from the assembly.
        /// </summary>
        private FrameworkName GetFrameworkName()
        {
#if !FEATURE_ASSEMBLYLOADCONTEXT
            if (!NativeMethodsShared.IsWindows)
            {
                CustomAttributeData attr = null;
 
                foreach (CustomAttributeData a in _assembly.GetCustomAttributesData())
                {
                    try
                    {
                        if (a.AttributeType == typeof(TargetFrameworkAttribute))
                        {
                            attr = a;
                            break;
                        }
                    }
                    catch
                    {
                    }
                }
 
                string name = null;
                if (attr != null)
                {
                    name = (string)attr.ConstructorArguments[0].Value;
                }
                return name == null ? null : new FrameworkName(name);
            }
 
            FrameworkName frameworkAttribute = null;
            try
            {
                var import2 = (IMetaDataImport2)_assemblyImport;
                _assemblyImport.GetAssemblyFromScope(out uint assemblyScope);
 
                string frameworkNameAttribute = GetStringCustomAttribute(import2, assemblyScope, s_targetFrameworkAttribute);
                if (!string.IsNullOrEmpty(frameworkNameAttribute))
                {
                    frameworkAttribute = new FrameworkName(frameworkNameAttribute);
                }
            }
            catch (Exception e) when (!ExceptionHandling.IsCriticalException(e))
            {
            }
 
            return frameworkAttribute;
#else
            CorePopulateMetadata();
            return _frameworkName;
#endif
        }
 
#if FEATURE_ASSEMBLYLOADCONTEXT
        /// <summary>
        /// Read everything from the assembly in a single stream.
        /// </summary>
        /// <returns></returns>
        private void CorePopulateMetadata()
        {
            if (_metadataRead)
            {
                return;
            }
 
            lock (this)
            {
                if (_metadataRead)
                {
                    return;
                }
 
                using (var stream = File.OpenRead(_sourceFile))
                using (var peFile = new PEReader(stream))
                {
                    bool hasMetadata = false;
                    try
                    {
                        // This can throw if the stream is too small, which means
                        // the assembly doesn't have metadata.
                        hasMetadata = peFile.HasMetadata;
                    }
                    finally
                    {
                        // If the file does not contain PE metadata, throw BadImageFormatException to preserve
                        // behavior from AssemblyName.GetAssemblyName(). RAR will deal with this correctly.
                        if (!hasMetadata)
                        {
                            throw new BadImageFormatException(string.Format(CultureInfo.CurrentCulture,
                                AssemblyResources.GetString("ResolveAssemblyReference.AssemblyDoesNotContainPEMetadata"),
                                _sourceFile));
                        }
                    }
 
                    var metadataReader = peFile.GetMetadataReader();
 
                    var assemblyReferences = metadataReader.AssemblyReferences;
 
                    List<AssemblyNameExtension> ret = new List<AssemblyNameExtension>(assemblyReferences.Count);
 
                    foreach (var handle in assemblyReferences)
                    {
                        var assemblyName = GetAssemblyName(metadataReader, handle);
                        ret.Add(new AssemblyNameExtension(assemblyName));
                    }
 
                    _assemblyDependencies = ret.ToArray();
 
                    foreach (var attrHandle in metadataReader.GetAssemblyDefinition().GetCustomAttributes())
                    {
                        var attr = metadataReader.GetCustomAttribute(attrHandle);
 
                        var ctorHandle = attr.Constructor;
                        if (ctorHandle.Kind != HandleKind.MemberReference)
                        {
                            continue;
                        }
 
                        var container = metadataReader.GetMemberReference((MemberReferenceHandle)ctorHandle).Parent;
                        if (container.Kind != HandleKind.TypeReference)
                        {
                            continue;
                        }
 
                        var name = metadataReader.GetTypeReference((TypeReferenceHandle)container).Name;
                        if (!string.Equals(metadataReader.GetString(name), "TargetFrameworkAttribute"))
                        {
                            continue;
                        }
 
                        var arguments = GetFixedStringArguments(metadataReader, attr);
                        if (arguments.Count == 1)
                        {
                            _frameworkName = new FrameworkName(arguments[0]);
                        }
                    }
 
                    var assemblyFilesCollection = metadataReader.AssemblyFiles;
 
                    List<string> assemblyFiles = new List<string>(assemblyFilesCollection.Count);
 
                    foreach (var fileHandle in assemblyFilesCollection)
                    {
                        assemblyFiles.Add(metadataReader.GetString(metadataReader.GetAssemblyFile(fileHandle).Name));
                    }
 
                    _assemblyFiles = assemblyFiles.ToArray();
                }
 
                _metadataRead = true;
            }
        }
 
        // https://github.com/dotnet/msbuild/issues/4002
        // https://github.com/dotnet/corefx/issues/34008
        //
        // We do not use AssemblyReference.GetAssemblyName() here because its behavior
        // is different from other code paths with respect to neutral culture. We will
        // get unspecified culture instead of explicitly neutral culture. This in turn
        // leads string comparisons of assembly-name-modulo-version in RAR to false
        // negatives that break its conflict resolution and binding redirect generation.
        private static AssemblyName GetAssemblyName(MetadataReader metadataReader, AssemblyReferenceHandle handle)
        {
            var entry = metadataReader.GetAssemblyReference(handle);
 
            var assemblyName = new AssemblyName
            {
                Name = metadataReader.GetString(entry.Name),
                Version = entry.Version,
                CultureName = metadataReader.GetString(entry.Culture)
            };
 
            var publicKeyOrToken = metadataReader.GetBlobBytes(entry.PublicKeyOrToken);
            if (publicKeyOrToken != null)
            {
                if (publicKeyOrToken.Length <= 8)
                {
                    assemblyName.SetPublicKeyToken(publicKeyOrToken);
                }
                else
                {
                    assemblyName.SetPublicKey(publicKeyOrToken);
                }
            }
 
            assemblyName.Flags = (AssemblyNameFlags)(int)entry.Flags;
            return assemblyName;
        }
#endif
 
#if FEATURE_ASSEMBLYLOADCONTEXT
        // This method copied from DNX source: https://github.com/aspnet/dnx/blob/e0726f769aead073af2d8cd9db47b89e1745d574/src/Microsoft.Dnx.Tooling/Utils/LockFileUtils.cs#L385
        //  System.Reflection.Metadata 1.1 is expected to have an API that helps with this.
        /// <summary>
        /// Gets the fixed (required) string arguments of a custom attribute.
        /// Only attributes that have only fixed string arguments.
        /// </summary>
        private static List<string> GetFixedStringArguments(MetadataReader reader, CustomAttribute attribute)
        {
            // TODO: Nick Guerrera (Nick.Guerrera@microsoft.com) hacked this method for temporary use.
            // There is a blob decoder feature in progress but it won't ship in time for our milestone.
            // Replace this method with the blob decoder feature when later it is availale.
 
            var signature = reader.GetMemberReference((MemberReferenceHandle)attribute.Constructor).Signature;
            var signatureReader = reader.GetBlobReader(signature);
            var valueReader = reader.GetBlobReader(attribute.Value);
            var arguments = new List<string>();
 
            var prolog = valueReader.ReadUInt16();
            if (prolog != 1)
            {
                // Invalid custom attribute prolog
                return arguments;
            }
 
            var header = signatureReader.ReadSignatureHeader();
            if (header.Kind != SignatureKind.Method || header.IsGeneric)
            {
                // Invalid custom attribute constructor signature
                return arguments;
            }
 
            int parameterCount;
            if (!signatureReader.TryReadCompressedInteger(out parameterCount))
            {
                // Invalid custom attribute constructor signature
                return arguments;
            }
 
            var returnType = signatureReader.ReadSignatureTypeCode();
            if (returnType != SignatureTypeCode.Void)
            {
                // Invalid custom attribute constructor signature
                return arguments;
            }
 
            for (int i = 0; i < parameterCount; i++)
            {
                var signatureTypeCode = signatureReader.ReadSignatureTypeCode();
                if (signatureTypeCode == SignatureTypeCode.String)
                {
                    // Custom attribute constructor must take only strings
                    arguments.Add(valueReader.ReadSerializedString());
                }
            }
 
            return arguments;
        }
#endif
 
#if !FEATURE_ASSEMBLYLOADCONTEXT
        /// <summary>
        /// Release interface pointers on Dispose().
        /// </summary>
        protected override void DisposeUnmanagedResources()
        {
            if (NativeMethodsShared.IsWindows)
            {
                if (_assemblyImport != null)
                {
                    Marshal.ReleaseComObject(_assemblyImport);
                }
 
                if (_metadataDispenser != null)
                {
                    Marshal.ReleaseComObject(_metadataDispenser);
                }
            }
        }
#endif
 
        /// <summary>
        /// Given a path get the CLR runtime version of the file
        /// </summary>
        /// <param name="path">path to the file</param>
        /// <returns>The CLR runtime version or empty if the path does not exist.</returns>
        internal static string GetRuntimeVersion(string path)
        {
#if FEATURE_MSCOREE
            if (NativeMethodsShared.IsWindows)
            {
#if DEBUG
                // Just to make sure and exercise the code that uses dwLength to allocate the buffer
                // when GetRequestedRuntimeInfo fails due to insufficient buffer size.
                int bufferLength = 1;
#else
                int bufferLength = 11; // 11 is the length of a runtime version and null terminator v2.0.50727/0
#endif
 
                unsafe
                {
                    // Allocate an initial buffer
                    char* runtimeVersion = stackalloc char[bufferLength];
 
                    // Run GetFileVersion, this should succeed using the initial buffer.
                    // It also returns the dwLength which is used if there is insufficient buffer.
                    uint hresult = NativeMethods.GetFileVersion(path, runtimeVersion, bufferLength, out int dwLength);
 
                    if (hresult == NativeMethodsShared.ERROR_INSUFFICIENT_BUFFER)
                    {
                        // Allocate new buffer based on the returned length.
                        char* runtimeVersion2 = stackalloc char[dwLength];
                        runtimeVersion = runtimeVersion2;
 
                        // Get the RuntimeVersion in this second call.
                        bufferLength = dwLength;
                        hresult = NativeMethods.GetFileVersion(path, runtimeVersion, bufferLength, out dwLength);
                    }
 
                    return hresult == NativeMethodsShared.S_OK ? new string(runtimeVersion, 0, dwLength - 1) : string.Empty;
                }
            }
            else
            {
                return ManagedRuntimeVersionReader.GetRuntimeVersion(path);
            }
#else
            return ManagedRuntimeVersionReader.GetRuntimeVersion(path);
#endif
        }
 
        /// <summary>
        /// Import assembly dependencies.
        /// </summary>
        /// <returns>The array of assembly dependencies.</returns>
        private AssemblyNameExtension[] ImportAssemblyDependencies()
        {
#if !FEATURE_ASSEMBLYLOADCONTEXT
            var asmRefs = new List<AssemblyNameExtension>();
 
            if (!NativeMethodsShared.IsWindows)
            {
                return _assembly.GetReferencedAssemblies().Select(a => new AssemblyNameExtension(a)).ToArray();
            }
 
            IntPtr asmRefEnum = IntPtr.Zero;
            var asmRefTokens = new UInt32[GENMAN_ENUM_TOKEN_BUF_SIZE];
            // Ensure the enum handle is closed.
            try
            {
                // Enum chunks of refs in 16-ref blocks until we run out.
                UInt32 fetched;
                do
                {
                    _assemblyImport.EnumAssemblyRefs(
                        ref asmRefEnum,
                        asmRefTokens,
                        (uint)asmRefTokens.Length,
                        out fetched);
 
                    for (uint i = 0; i < fetched; i++)
                    {
                        // Determine the length of the string to contain the name first.
                        _assemblyImport.GetAssemblyRefProps(
                            asmRefTokens[i],
                            out IntPtr pubKeyPtr,
                            out uint pubKeyBytes,
                            null,
                            0,
                            out uint asmNameLength,
                            IntPtr.Zero,
                            out _,
                            out _,
                            out uint flags);
                        // Allocate assembly name buffer.
                        var asmNameBuf = new char[asmNameLength + 1];
                        IntPtr asmMetaPtr = IntPtr.Zero;
                        // Ensure metadata structure is freed.
                        try
                        {
                            // Allocate metadata structure.
                            asmMetaPtr = AllocAsmMeta();
                            // Retrieve the assembly reference properties.
                            _assemblyImport.GetAssemblyRefProps(
                                asmRefTokens[i],
                                out pubKeyPtr,
                                out pubKeyBytes,
                                asmNameBuf,
                                (uint)asmNameBuf.Length,
                                out asmNameLength,
                                asmMetaPtr,
                                out _,
                                out _,
                                out flags);
                            // Construct the assembly name and free metadata structure.
                            AssemblyNameExtension asmName = ConstructAssemblyName(
                                asmMetaPtr,
                                asmNameBuf,
                                asmNameLength,
                                pubKeyPtr,
                                pubKeyBytes,
                                flags);
                            // Add the assembly name to the reference list.
                            asmRefs.Add(asmName);
                        }
                        finally
                        {
                            FreeAsmMeta(asmMetaPtr);
                        }
                    }
                } while (fetched > 0);
            }
            finally
            {
                if (asmRefEnum != IntPtr.Zero)
                {
                    _assemblyImport.CloseEnum(asmRefEnum);
                }
            }
 
            return asmRefs.ToArray();
#else
            CorePopulateMetadata();
            return _assemblyDependencies;
#endif
        }
 
 
        /// <summary>
        /// Import extra files. These are usually consituent members of a scatter assembly.
        /// </summary>
        /// <returns>The extra files of assembly dependencies.</returns>
        private string[] ImportFiles()
        {
#if !FEATURE_ASSEMBLYLOADCONTEXT
            var files = new List<string>();
            IntPtr fileEnum = IntPtr.Zero;
            var fileTokens = new UInt32[GENMAN_ENUM_TOKEN_BUF_SIZE];
            var fileNameBuf = new char[GENMAN_STRING_BUF_SIZE];
 
            // Ensure the enum handle is closed.
            try
            {
                // Enum chunks of files until we run out.
                UInt32 fetched;
                do
                {
                    _assemblyImport.EnumFiles(ref fileEnum, fileTokens, (uint)fileTokens.Length, out fetched);
 
                    for (uint i = 0; i < fetched; i++)
                    {
                        // Retrieve file properties.
                        _assemblyImport.GetFileProps(fileTokens[i],
                            fileNameBuf, (uint)fileNameBuf.Length, out uint fileNameLength,
                            out _, out _, out _);
 
                        // Add file to file list.
                        string file = new string(fileNameBuf, 0, (int)(fileNameLength - 1));
                        files.Add(file);
                    }
                } while (fetched > 0);
            }
            finally
            {
                if (fileEnum != IntPtr.Zero)
                {
                    _assemblyImport.CloseEnum(fileEnum);
                }
            }
 
            return files.ToArray();
#else
            CorePopulateMetadata();
            return _assemblyFiles;
#endif
        }
 
#if !FEATURE_ASSEMBLYLOADCONTEXT
        /// <summary>
        /// Allocate assembly metadata structure buffer.
        /// </summary>
        /// <returns>Pointer to structure</returns>
        private static IntPtr AllocAsmMeta()
        {
            ASSEMBLYMETADATA asmMeta;
            asmMeta.usMajorVersion = asmMeta.usMinorVersion = asmMeta.usBuildNumber = asmMeta.usRevisionNumber = 0;
            asmMeta.cOses = asmMeta.cProcessors = 0;
            asmMeta.rOses = asmMeta.rpProcessors = IntPtr.Zero;
            // Allocate buffer for locale.
            asmMeta.rpLocale = Marshal.AllocCoTaskMem(GENMAN_LOCALE_BUF_SIZE * 2);
            asmMeta.cchLocale = GENMAN_LOCALE_BUF_SIZE;
            // Convert to unmanaged structure.
            int size = Marshal.SizeOf<ASSEMBLYMETADATA>();
            IntPtr asmMetaPtr = Marshal.AllocCoTaskMem(size);
            Marshal.StructureToPtr(asmMeta, asmMetaPtr, false);
 
            return asmMetaPtr;
        }
 
        /// <summary>
        /// Construct assembly name.
        /// </summary>
        /// <param name="asmMetaPtr">Assembly metadata structure</param>
        /// <param name="asmNameBuf">Buffer containing the name</param>
        /// <param name="asmNameLength">Length of that buffer</param>
        /// <param name="pubKeyPtr">Pointer to public key</param>
        /// <param name="pubKeyBytes">Count of bytes in public key.</param>
        /// <param name="flags">Extra flags</param>
        /// <returns>The assembly name.</returns>
        private static AssemblyNameExtension ConstructAssemblyName(IntPtr asmMetaPtr, char[] asmNameBuf, UInt32 asmNameLength, IntPtr pubKeyPtr, UInt32 pubKeyBytes, UInt32 flags)
        {
            // Marshal the assembly metadata back to a managed type.
            ASSEMBLYMETADATA asmMeta = (ASSEMBLYMETADATA)Marshal.PtrToStructure(asmMetaPtr, typeof(ASSEMBLYMETADATA));
 
            // Construct the assembly name. (Note asmNameLength should/must be > 0.)
            var assemblyName = new AssemblyName
            {
                Name = new string(asmNameBuf, 0, (int)asmNameLength - 1),
                Version = new Version(
                    asmMeta.usMajorVersion,
                    asmMeta.usMinorVersion,
                    asmMeta.usBuildNumber,
                    asmMeta.usRevisionNumber)
            };
 
            // Set culture info.
            string locale = Marshal.PtrToStringUni(asmMeta.rpLocale);
            if (locale.Length > 0)
            {
                assemblyName.CultureInfo = CultureInfo.CreateSpecificCulture(locale);
            }
            else
            {
                assemblyName.CultureInfo = CultureInfo.CreateSpecificCulture(String.Empty);
            }
 
            // Set public key or PKT.
            var publicKey = new byte[pubKeyBytes];
            Marshal.Copy(pubKeyPtr, publicKey, 0, (int)pubKeyBytes);
            if ((flags & (uint)CorAssemblyFlags.afPublicKey) != 0)
            {
                assemblyName.SetPublicKey(publicKey);
            }
            else
            {
                assemblyName.SetPublicKeyToken(publicKey);
            }
 
            assemblyName.Flags = (AssemblyNameFlags)flags;
            return new AssemblyNameExtension(assemblyName);
        }
 
        /// <summary>
        /// Free the assembly metadata structure.
        /// </summary>
        /// <param name="asmMetaPtr">The pointer.</param>
        private static void FreeAsmMeta(IntPtr asmMetaPtr)
        {
            if (asmMetaPtr != IntPtr.Zero)
            {
                // Marshal the assembly metadata back to a managed type.
                var asmMeta = (ASSEMBLYMETADATA)Marshal.PtrToStructure(asmMetaPtr, typeof(ASSEMBLYMETADATA));
                FreeAsmMeta(asmMetaPtr, ref asmMeta);
            }
        }
 
        /// <summary>
        /// Free the assembly metadata structure.
        /// </summary>
        /// <param name="asmMetaPtr">The pointer.</param>
        /// <param name="asmMeta">Marshaled assembly metadata to the managed type.</param>
        private static void FreeAsmMeta(IntPtr asmMetaPtr, ref ASSEMBLYMETADATA asmMeta)
        {
            if (asmMetaPtr != IntPtr.Zero)
            {
                // Free unmanaged memory.
                Marshal.FreeCoTaskMem(asmMeta.rpLocale);
                asmMeta.rpLocale = IntPtr.Zero;
                Marshal.DestroyStructure(asmMetaPtr, typeof(ASSEMBLYMETADATA));
                Marshal.FreeCoTaskMem(asmMetaPtr);
            }
        }
#endif
    }
 
    /// <summary>
    /// Managed implementation of a reader for getting the runtime version of an assembly
    /// </summary>
    internal static class ManagedRuntimeVersionReader
    {
        private class HeaderInfo
        {
            public uint VirtualAddress;
            public uint Size;
            public uint FileOffset;
        }
 
        /// <summary>
        /// Given a path get the CLR runtime version of the file.
        /// </summary>
        /// <param name="path">path to the file</param>
        /// <returns>The CLR runtime version or empty if the path does not exist or the file is not an assembly.</returns>
        public static string GetRuntimeVersion(string path)
        {
            if (!FileSystems.Default.FileExists(path))
            {
                return string.Empty;
            }
 
            using Stream stream = File.OpenRead(path);
            using BinaryReader reader = new BinaryReader(stream);
            return GetRuntimeVersion(reader);
        }
 
        /// <summary>
        /// Given a <see cref="BinaryReader"/> get the CLR runtime version of the underlying file.
        /// </summary>
        /// <param name="sr">A <see cref="BinaryReader"/> positioned at the first byte of the file.</param>
        /// <returns>The CLR runtime version or empty if the data does not represent an assembly.</returns>
        internal static string GetRuntimeVersion(BinaryReader sr)
        {
            // This algorithm for getting the runtime version is based on
            // the ECMA Standard 335: The Common Language Infrastructure (CLI)
            // http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-335.pdf
 
            try
            {
                const uint PEHeaderPointerOffset = 0x3c;
                const uint PEHeaderSize = 20;
                const uint OptionalPEHeaderSize = 224;
                const uint OptionalPEPlusHeaderSize = 240;
                const uint SectionHeaderSize = 40;
 
                // The PE file format is specified in section II.25
 
                // A PE image starts with an MS-DOS header followed by a PE signature, followed by the PE file header,
                // and then the PE optional header followed by PE section headers.
                // There must be room for all of that.
 
                if (sr.BaseStream.Length < PEHeaderPointerOffset + 4 + PEHeaderSize + OptionalPEHeaderSize +
                    SectionHeaderSize)
                {
                    return string.Empty;
                }
 
                // The PE format starts with an MS-DOS stub of 128 bytes.
                // At offset 0x3c in the DOS header is a 4-byte unsigned integer offset to the PE
                // signature (shall be “PE\0\0”), immediately followed by the PE file header
 
                sr.BaseStream.Position = PEHeaderPointerOffset;
                var peHeaderOffset = sr.ReadUInt32();
 
                if (peHeaderOffset + 4 + PEHeaderSize + OptionalPEHeaderSize + SectionHeaderSize >=
                    sr.BaseStream.Length)
                {
                    return string.Empty;
                }
 
                // The PE header is specified in section II.25.2
                // Read the PE header signature
 
                sr.BaseStream.Position = peHeaderOffset;
                if (!ReadBytes(sr, (byte)'P', (byte)'E', 0, 0))
                {
                    return string.Empty;
                }
 
                // The PE header immediately follows the signature
                var peHeaderBase = peHeaderOffset + 4;
 
                // At offset 2 of the PE header there is the number of sections
                sr.BaseStream.Position = peHeaderBase + 2;
                var numberOfSections = sr.ReadUInt16();
                if (numberOfSections > 96)
                {
                    return string.Empty; // There can't be more than 96 sections, something is wrong
                }
 
                // Immediately after the PE Header is the PE Optional Header.
                // This header is optional in the general PE spec, but always
                // present in assembly files.
                // From this header we'll get the CLI header RVA, which is
                // at offset 208 for PE32, and at offset 224 for PE32+
 
                var optionalHeaderOffset = peHeaderBase + PEHeaderSize;
 
                uint cliHeaderRvaOffset;
                uint optionalPEHeaderSize;
 
                sr.BaseStream.Position = optionalHeaderOffset;
                var magicNumber = sr.ReadUInt16();
 
                if (magicNumber == 0x10b) // PE32
                {
                    optionalPEHeaderSize = OptionalPEHeaderSize;
                    cliHeaderRvaOffset = optionalHeaderOffset + 208;
                }
                else if (magicNumber == 0x20b) // PE32+
                {
                    optionalPEHeaderSize = OptionalPEPlusHeaderSize;
                    cliHeaderRvaOffset = optionalHeaderOffset + 224;
                }
                else
                {
                    return string.Empty;
                }
 
                // Read the CLI header RVA
 
                sr.BaseStream.Position = cliHeaderRvaOffset;
                var cliHeaderRva = sr.ReadUInt32();
                if (cliHeaderRva == 0)
                {
                    return string.Empty; // No CLI section
                }
 
                // Immediately following the optional header is the Section
                // Table, which contains a number of section headers.
                // Section headers are specified in section II.25.3
 
                // Each section header has the base RVA, size, and file
                // offset of the section. To find the file offset of the
                // CLI header we need to find a section that contains
                // its RVA, and the calculate the file offset using
                // the base file offset of the section.
 
                var sectionOffset = optionalHeaderOffset + optionalPEHeaderSize;
 
                // Read all section headers, we need them to make RVA to
                // offset conversions.
 
                var sections = new HeaderInfo[numberOfSections];
                for (int n = 0; n < numberOfSections; n++)
                {
                    // At offset 8 of the section is the section size
                    // and base RVA. At offset 20 there is the file offset
                    sr.BaseStream.Position = sectionOffset + 8;
                    var sectionSize = sr.ReadUInt32();
                    var sectionRva = sr.ReadUInt32();
                    sr.BaseStream.Position = sectionOffset + 20;
                    var sectionDataOffset = sr.ReadUInt32();
                    sections[n] = new HeaderInfo
                    {
                        VirtualAddress = sectionRva,
                        Size = sectionSize,
                        FileOffset = sectionDataOffset
                    };
                    sectionOffset += SectionHeaderSize;
                }
 
                uint cliHeaderOffset = RvaToOffset(sections, cliHeaderRva);
 
                // CLI section not found
                if (cliHeaderOffset == 0)
                {
                    return string.Empty;
                }
 
                // The CLI header is specified in section II.25.3.3.
                // It contains all of the runtime-specific data entries and other information.
                // From the CLI header we need to get the RVA of the metadata root,
                // which is located at offset 8.
 
                sr.BaseStream.Position = cliHeaderOffset + 8;
                var metadataRva = sr.ReadUInt32();
 
                var metadataOffset = RvaToOffset(sections, metadataRva);
                if (metadataOffset == 0)
                {
                    return string.Empty;
                }
 
                // The metadata root is specified in section II.24.2.1
                // The first 4 bytes contain a signature.
                // The version string is at offset 12.
 
                sr.BaseStream.Position = metadataOffset;
                if (!ReadBytes(sr, 0x42, 0x53, 0x4a, 0x42)) // Metadata root signature
                {
                    return string.Empty;
                }
 
                // Read the version string length
                sr.BaseStream.Position = metadataOffset + 12;
                var length = sr.ReadInt32();
                if (length > 255 || length <= 0 || sr.BaseStream.Position + length >= sr.BaseStream.Length)
                {
                    return string.Empty;
                }
 
                // Read the version string
                var v = Encoding.UTF8.GetString(sr.ReadBytes(length));
 
                // Per II.24.2.1, version string length is rounded up
                // to a multiple of 4. So we may read eg "4.0.30319\0\0"
                // Version.Parse works fine, but it's not pretty in the log.
                int firstNull = v.IndexOf('\0');
                if (firstNull > 0)
                {
                    v = v.Substring(0, firstNull);
                }
 
                return v;
            }
            catch
            {
                // Something went wrong in spite of all checks. Corrupt file?
                return string.Empty;
            }
        }
 
        private static bool ReadBytes(BinaryReader r, params byte[] bytes)
        {
            foreach (byte b in bytes)
            {
                if (b != r.ReadByte())
                {
                    return false;
                }
            }
 
            return true;
        }
 
        private static uint RvaToOffset(HeaderInfo[] sections, uint rva)
        {
            foreach (var s in sections)
            {
                if (rva >= s.VirtualAddress && rva < s.VirtualAddress + s.Size)
                {
                    return s.FileOffset + (rva - s.VirtualAddress);
                }
            }
            return 0;
        }
    }
}