File: AssemblyDependency\AssemblyInformation.cs
Web Access
Project: src\msbuild\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.Runtime.InteropServices;
using Microsoft.Build.Tasks.Metadata;
using Microsoft.Build.Utilities;
#endif
using System.Reflection;
using System.Runtime.Versioning;
using System.Text;
using Microsoft.Build.Shared;
using Microsoft.Build.Shared.FileSystem;
#if !FEATURE_ASSEMBLYLOADCONTEXT
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Com;
#endif

#if FEATURE_ASSEMBLYLOADCONTEXT
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
#else
using Microsoft.Build.Framework;
#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 unsafe class AssemblyInformation : DisposableBase
    {
        private AssemblyNameExtension[] _assemblyDependencies;
        private string[] _assemblyFiles;
#if !FEATURE_ASSEMBLYLOADCONTEXT
        // COM pointers stored thread-agile via the GIT. Disposed in DisposeManagedResources.
        // The CLR metadata object returned by IMetaDataDispenser::OpenScope implements
        // all three of IMetaDataImport, IMetaDataImport2, and IMetaDataAssemblyImport;
        // we QueryInterface for the two we actually call. The dispenser itself is only
        // needed during construction and is released as soon as OpenScope returns.
        private readonly AgileComPointer<IMetaDataAssemblyImport> _assemblyImport;
        private readonly AgileComPointer<IMetaDataImport2> _import2;
#endif
        private readonly string _sourceFile;
        private FrameworkName _frameworkName;

#if FEATURE_ASSEMBLYLOADCONTEXT
        private bool _metadataRead;
#endif

#if !FEATURE_ASSEMBLYLOADCONTEXT
        private const 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
            // net472-only = inherently Windows. CsWin32 types used directly.
            // Activate the dispenser and ask OpenScope directly for IMetaDataImport2 — the
            // underlying CLR RegMeta coclass implements every IMetaData* interface, so we save a
            // QueryInterface round-trip vs. asking for the base IMetaDataImport. We still need a
            // single QI for IMetaDataAssemblyImport since OpenScope only returns one pointer.
            // Each ComScope releases at end of method; AgileComPointer (takeOwnership: false)
            // AddRefs through GIT registration so the field retains the only persistent reference.
            Guid clsid = CorMetadata.CLSID_CorMetaDataDispenser;
            Guid dispenserIid = IID.Get<IMetaDataDispenser>();
            using ComScope<IMetaDataDispenser> dispenser = new();
            PInvoke.CoCreateInstance(&clsid, null, CLSCTX.CLSCTX_INPROC_SERVER, &dispenserIid, dispenser)
                .ThrowOnFailure();

            Guid import2Iid = IMetaDataImport2.IID_IMetaDataImport2;
            using ComScope<IMetaDataImport2> import2 = new();
            fixed (char* pPath = sourceFile)
            {
                dispenser.Pointer->OpenScope(pPath, CorOpenFlags.ofRead, &import2Iid, import2).ThrowOnFailure();
            }
            _import2 = new AgileComPointer<IMetaDataImport2>(import2.Pointer, takeOwnership: false);

            Guid asmIid = IMetaDataAssemblyImport.IID_IMetaDataAssemblyImport;
            using ComScope<IMetaDataAssemblyImport> asmImport = new();
            import2.Pointer->QueryInterface(&asmIid, asmImport).ThrowOnFailure();
            _assemblyImport = new AgileComPointer<IMetaDataAssemblyImport>(asmImport.Pointer, takeOwnership: false);
#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()
        {
            using ComScope<IMetaDataAssemblyImport> asmImport = _assemblyImport.GetInterface();
            using ComScope<IMetaDataImport2> import2 = _import2.GetInterface();

            MdAssembly assemblyScope;
            asmImport.Pointer->GetAssemblyFromScope(&assemblyScope).ThrowOnFailure();

            // get the assembly, if there is no assembly, it is a module reference
            if (assemblyScope.IsNil)
            {
                return null;
            }

            AssemblyAttributes assemblyAttributes = new()
            {
                AssemblyFullPath = _sourceFile,
                IsAssembly = true,
            };

            // Stack-allocate everything GetAssemblyProps needs to fill in: the name buffer, the
            // locale buffer pointed at by ASSEMBLYMETADATA.szLocale, and the struct itself. The
            // struct is blittable so we pass &asmMeta directly — no Marshal allocation/copy.
            // rProcessor / rOS are left null because we don't request that data.
            using BufferScope<char> nameBuffer = new(GENMAN_STRING_BUF_SIZE);
            char* localeBuffer = stackalloc char[GENMAN_LOCALE_BUF_SIZE];
            ASSEMBLYMETADATA asmMeta = new()
            {
                szLocale = localeBuffer,
                cbLocale = GENMAN_LOCALE_BUF_SIZE,
            };

            void* publicKeyPtr;
            uint publicKeyLength;
            uint hashAlgorithmId;
            uint nameLength;
            CorAssemblyFlags flags;
            fixed (char* pNameBuf = nameBuffer)
            {
                asmImport.Pointer->GetAssemblyProps(
                    assemblyScope,
                    &publicKeyPtr,
                    &publicKeyLength,
                    &hashAlgorithmId,
                    pNameBuf,
                    GENMAN_STRING_BUF_SIZE,
                    &nameLength,
                    &asmMeta,
                    &flags).ThrowOnFailure();
            }

            assemblyAttributes.AssemblyName = nameBuffer.Slice(0, (int)nameLength - 1).ToString();
            assemblyAttributes.DefaultAlias = assemblyAttributes.AssemblyName;

            assemblyAttributes.MajorVersion = asmMeta.usMajorVersion;
            assemblyAttributes.MinorVersion = asmMeta.usMinorVersion;
            assemblyAttributes.RevisionNumber = asmMeta.usRevisionNumber;
            assemblyAttributes.BuildNumber = asmMeta.usBuildNumber;
            // szLocale is null-terminated; new string(char*) reads to the terminator.
            assemblyAttributes.Culture = asmMeta.szLocale.Value is null ? null : new string(asmMeta.szLocale);

            byte[] publicKey = new byte[publicKeyLength];
            Marshal.Copy((IntPtr)publicKeyPtr, publicKey, 0, (int)publicKeyLength);
            assemblyAttributes.PublicHexKey = BitConverter.ToString(publicKey).Replace("-", string.Empty);

            assemblyAttributes.Description = GetStringCustomAttribute(import2.Pointer, assemblyScope, "System.Reflection.AssemblyDescriptionAttribute");
            assemblyAttributes.TargetFrameworkMoniker = GetStringCustomAttribute(import2.Pointer, assemblyScope, "System.Runtime.Versioning.TargetFrameworkAttribute");
            var guid = GetStringCustomAttribute(import2.Pointer, assemblyScope, "System.Runtime.InteropServices.GuidAttribute");
            if (!string.IsNullOrEmpty(guid))
            {
                string importedFromTypeLibString = GetStringCustomAttribute(import2.Pointer, assemblyScope, "System.Runtime.InteropServices.ImportedFromTypeLibAttribute");
                if (!string.IsNullOrEmpty(importedFromTypeLibString))
                {
                    assemblyAttributes.IsImportedFromTypeLib = true;
                }
                else
                {
                    string primaryInteropAssemblyString = GetStringCustomAttribute(import2.Pointer, assemblyScope, "System.Runtime.InteropServices.PrimaryInteropAssemblyAttribute");
                    assemblyAttributes.IsImportedFromTypeLib = !string.IsNullOrEmpty(primaryInteropAssemblyString);
                }
            }

            assemblyAttributes.RuntimeVersion = GetRuntimeVersion(_sourceFile);

            uint peKind;
            uint machine;
            import2.Pointer->GetPEKind(&peKind, &machine).ThrowOnFailure();
            assemblyAttributes.PeKind = peKind;

            return assemblyAttributes;
        }

        // Takes a borrowed IMetaDataImport2* so callers in a hot path (GetAssemblyMetadata's
        // 4+ attribute lookups) can reuse a single GIT round-trip instead of paying one per call.
        private string GetStringCustomAttribute(IMetaDataImport2* import2, MdToken assemblyScope, string attributeName)
        {
            HRESULT hr;
            void* data = null;
            uint valueLen = 0;
            fixed (char* pName = attributeName)
            {
                hr = import2->GetCustomAttributeByName(assemblyScope, pName, &data, &valueLen);
            }

            if (hr == HRESULT.S_OK)
            {
                // if a custom attribute exists, parse the contents of the blob
                if (NativeMethods.TryReadMetadataString(_sourceFile, (IntPtr)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
            // net472-only = inherently Windows.
            FrameworkName frameworkAttribute = null;
            try
            {
                MdAssembly assemblyScope;
                using (ComScope<IMetaDataAssemblyImport> asmImport = _assemblyImport.GetInterface())
                {
                    asmImport.Pointer->GetAssemblyFromScope(&assemblyScope).ThrowOnFailure();
                }

                using ComScope<IMetaDataImport2> import2 = _import2.GetInterface();
                string frameworkNameAttribute = GetStringCustomAttribute(import2.Pointer, 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 DisposeManagedResources()
        {
            _import2?.Dispose();
            _assemblyImport?.Dispose();
        }
#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 unsafe string GetRuntimeVersion(string path)
        {
#if FEATURE_MSCOREE
            // net472-only = inherently Windows. CsWin32 types used directly.
            {
#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
                using BufferScope<char> buffer = new(stackalloc char[bufferLength]);

                fixed (char* bufferPtr = buffer)
                {
                    fixed (char* pathPtr = path)
                    {
                        // Run GetFileVersion, this should succeed using the initial buffer.
                        // It also returns the dwLength which is used if there is insufficient buffer.
                        uint dwLength = 0;
                        HRESULT hresult = Windows.Win32.PInvoke.GetFileVersion(pathPtr, bufferPtr, (uint)bufferLength, &dwLength);

                        if (hresult == (HRESULT)WIN32_ERROR.ERROR_INSUFFICIENT_BUFFER)
                        {
                            // Allocate new buffer based on the returned length.
                            buffer.EnsureCapacity((int)dwLength);
                            fixed (char* newBufferPtr = buffer)
                            {
                                // Run GetFileVersion again, this should succeed using the new buffer.
                                hresult = Windows.Win32.PInvoke.GetFileVersion(pathPtr, newBufferPtr, dwLength, &dwLength);
                            }
                        }

                        return hresult == HRESULT.S_OK ? buffer.Slice(0, (int)dwLength - 1).ToString() : string.Empty;
                    }
                }
            }
#else
            return ManagedRuntimeVersionReader.GetRuntimeVersion(path);
#endif
        }

        /// <summary>
        /// Import assembly dependencies.
        /// </summary>
        /// <returns>The array of assembly dependencies.</returns>
        private AssemblyNameExtension[] ImportAssemblyDependencies()
        {
#if !FEATURE_ASSEMBLYLOADCONTEXT
            // net472-only = inherently Windows.
            var asmRefs = new List<AssemblyNameExtension>();

            IntPtr asmRefEnum = IntPtr.Zero;
            var asmRefTokens = new MdAssemblyRef[GENMAN_ENUM_TOKEN_BUF_SIZE];
            using ComScope<IMetaDataAssemblyImport> asmImport = _assemblyImport.GetInterface();
            // Ensure the enum handle is closed.
            try
            {
                // Enum chunks of refs in 16-ref blocks until we run out.
                uint fetched;
                // Stack-allocate the locale buffer once and reuse it across iterations. The buffer
                // is overwritten on each GetAssemblyRefProps call and ConstructAssemblyName copies
                // the locale out into a managed string before the next iteration, so reuse is safe.
                // 64 wide chars = 128 bytes — trivially fine for the stack.
                char* localeBuffer = stackalloc char[GENMAN_LOCALE_BUF_SIZE];
                do
                {
                    fixed (MdAssemblyRef* pTokens = asmRefTokens)
                    {
                        asmImport.Pointer->EnumAssemblyRefs(
                            &asmRefEnum,
                            pTokens,
                            (uint)asmRefTokens.Length,
                            &fetched).ThrowOnFailure();
                    }

                    for (uint i = 0; i < fetched; i++)
                    {
                        // Determine the length of the string to contain the name first.
                        void* pubKeyPtr;
                        uint pubKeyBytes;
                        uint asmNameLength;
                        CorAssemblyFlags flags;
                        asmImport.Pointer->GetAssemblyRefProps(
                            asmRefTokens[i],
                            &pubKeyPtr,
                            &pubKeyBytes,
                            null,
                            0,
                            &asmNameLength,
                            null,
                            null,
                            null,
                            &flags).ThrowOnFailure();

                        // Allocate assembly name buffer.
                        var asmNameBuf = new char[asmNameLength + 1];

                        // ASSEMBLYMETADATA is blittable; pass &asmMeta directly. rProcessor / rOS
                        // stay null — RAR does not consume them. Reset cbLocale every iteration
                        // since the previous call may have shrunk it to the actual length.
                        ASSEMBLYMETADATA asmMeta = new()
                        {
                            szLocale = localeBuffer,
                            cbLocale = GENMAN_LOCALE_BUF_SIZE,
                        };

                        // Retrieve the assembly reference properties.
                        fixed (char* pNameBuf = asmNameBuf)
                        {
                            asmImport.Pointer->GetAssemblyRefProps(
                                asmRefTokens[i],
                                &pubKeyPtr,
                                &pubKeyBytes,
                                pNameBuf,
                                (uint)asmNameBuf.Length,
                                &asmNameLength,
                                &asmMeta,
                                null,
                                null,
                                &flags).ThrowOnFailure();
                        }

                        // Construct the assembly name from the populated struct.
                        AssemblyNameExtension asmName = ConstructAssemblyName(
                            in asmMeta,
                            asmNameBuf,
                            asmNameLength,
                            (IntPtr)pubKeyPtr,
                            pubKeyBytes,
                            flags);
                        // Add the assembly name to the reference list.
                        asmRefs.Add(asmName);
                    }
                } while (fetched > 0);
            }
            finally
            {
                if (asmRefEnum != IntPtr.Zero)
                {
                    asmImport.Pointer->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 MdFile[GENMAN_ENUM_TOKEN_BUF_SIZE];
            var fileNameBuf = new char[GENMAN_STRING_BUF_SIZE];
            using ComScope<IMetaDataAssemblyImport> asmImport = _assemblyImport.GetInterface();

            // Ensure the enum handle is closed.
            try
            {
                // Enum chunks of files until we run out.
                uint fetched;
                do
                {
                    fixed (MdFile* pTokens = fileTokens)
                    {
                        asmImport.Pointer->EnumFiles(&fileEnum, pTokens, (uint)fileTokens.Length, &fetched).ThrowOnFailure();
                    }

                    for (uint i = 0; i < fetched; i++)
                    {
                        // Retrieve file properties.
                        uint fileNameLength;
                        void* hashValue;
                        uint hashSize;
                        uint fileFlags;
                        fixed (char* pFileNameBuf = fileNameBuf)
                        {
                            asmImport.Pointer->GetFileProps(
                                fileTokens[i],
                                pFileNameBuf,
                                (uint)fileNameBuf.Length,
                                &fileNameLength,
                                &hashValue,
                                &hashSize,
                                &fileFlags).ThrowOnFailure();
                        }

                        // 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)
                {
                    asmImport.Pointer->CloseEnum(fileEnum);
                }
            }

            return files.ToArray();
#else
            CorePopulateMetadata();
            return _assemblyFiles;
#endif
        }

#if !FEATURE_ASSEMBLYLOADCONTEXT
        /// <summary>
        /// Construct assembly name.
        /// </summary>
        /// <param name="asmMeta">Assembly metadata populated by GetAssemblyRefProps.</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(in ASSEMBLYMETADATA asmMeta, char[] asmNameBuf, uint asmNameLength, IntPtr pubKeyPtr, uint pubKeyBytes, CorAssemblyFlags flags)
        {
            // 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. szLocale is null-terminated; new string(char*) reads to the terminator.
            string locale = asmMeta.szLocale.Value is null ? string.Empty : new string(asmMeta.szLocale);
            assemblyName.CultureInfo = locale.Length > 0
                ? CultureInfo.CreateSpecificCulture(locale)
                : CultureInfo.CreateSpecificCulture(string.Empty);

            // Set public key or PKT.
            var publicKey = new byte[pubKeyBytes];
            Marshal.Copy(pubKeyPtr, publicKey, 0, (int)pubKeyBytes);
            if ((flags & CorAssemblyFlags.afPublicKey) != 0)
            {
                assemblyName.SetPublicKey(publicKey);
            }
            else
            {
                assemblyName.SetPublicKeyToken(publicKey);
            }

            assemblyName.Flags = (AssemblyNameFlags)(uint)flags;
            return new AssemblyNameExtension(assemblyName);
        }
#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;
        }
    }
}