File: CommandLine\AssemblyMetadataProvider.cs
Web Access
Project: src\src\vstest\src\vstest.console\vstest.console.csproj (vstest.console)
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.IO;
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Runtime.Versioning;
using System.Text;

using Microsoft.VisualStudio.TestPlatform.ObjectModel;

using Microsoft.VisualStudio.TestPlatform.Utilities.Helpers;
using Microsoft.VisualStudio.TestPlatform.Utilities.Helpers.Interfaces;

namespace Microsoft.VisualStudio.TestPlatform.CommandLine.Processors.Utilities;

internal class AssemblyMetadataProvider : IAssemblyMetadataProvider
{
    private static AssemblyMetadataProvider? s_instance;
    private readonly IFileHelper _fileHelper;

    /// <summary>
    /// Gets the instance.
    /// </summary>
    public static AssemblyMetadataProvider Instance => s_instance ??= new AssemblyMetadataProvider(new FileHelper());

    internal AssemblyMetadataProvider(IFileHelper fileHelper)
    {
        _fileHelper = fileHelper;
    }

    /// <inheritdoc />
    public FrameworkName GetFrameworkName(string filePath)
    {
        FrameworkName frameworkName = new(Framework.DefaultFramework.Name);
        try
        {
            using var assemblyStream = _fileHelper.GetStream(filePath, FileMode.Open, FileAccess.Read);
            frameworkName = GetFrameworkNameFromAssemblyMetadata(assemblyStream);
        }
        catch (Exception ex)
        {
            EqtTrace.Warning("AssemblyMetadataProvider.GetFrameworkName: failed to determine TargetFrameworkVersion exception: {0} for assembly: {1}", ex, filePath);
        }

        EqtTrace.Info("AssemblyMetadataProvider.GetFrameworkName: Determined framework:'{0}' for source: '{1}'", frameworkName, filePath);

        return frameworkName;
    }

    /// <inheritdoc />
    public Architecture GetArchitecture(string assemblyPath)
    {
        Architecture archType = Architecture.AnyCPU;
        try
        {
            // AssemblyName won't load the assembly into current domain.
            var assemblyName = AssemblyName.GetAssemblyName(assemblyPath);

            var processorArchitecture =
#if NET
                // AssemblyName doesn't include ProcessorArchitecture in net7.
                // It will always be ProcessorArchitecture.None.
                ProcessorArchitecture.None;
#else
                assemblyName.ProcessorArchitecture;
#endif

            archType = MapToArchitecture(processorArchitecture, assemblyPath);
        }
        catch (Exception ex)
        {
            // AssemblyName will throw Exception if assembly contains native code or no manifest.

            EqtTrace.Verbose("AssemblyMetadataProvider.GetArchitecture: Failed get ProcessorArchitecture using AssemblyName API with exception: {0}", ex);

            try
            {
                archType = GetArchitectureForSource(assemblyPath);
            }
            catch (Exception e)
            {
                EqtTrace.Info("AssemblyMetadataProvider.GetArchitecture: Failed to determine Assembly Architecture with exception: {0}", e);
            }
        }

        EqtTrace.Info("AssemblyMetadataProvider.GetArchitecture: Determined architecture:{0} info for assembly: {1}", archType,
            assemblyPath);

        return archType;
    }

    private Architecture GetArchitectureFromAssemblyMetadata(string path)
    {
        Architecture arch = Architecture.AnyCPU;
        using (Stream stream = _fileHelper.GetStream(path, FileMode.Open, FileAccess.Read))
        using (PEReader peReader = new(stream))
        {
            switch (peReader.PEHeaders.CoffHeader.Machine)
            {
                case Machine.Amd64:
                case Machine.IA64:
                    return Architecture.X64;
                case Machine.Arm64:
                    return Architecture.ARM64;
                case Machine.Arm:
                    return Architecture.ARM;
                case Machine.I386:
                    // We can distinguish AnyCPU only from the set of CorFlags.Requires32Bit, but in case of Ready
                    // to Run image that flag is not "updated" and ignored. So we check if the module is IL only or not.
                    // If it's not IL only it means that is a R2R (Ready to Run) and we're already in the correct architecture x86.
                    // In all other cases the architecture will end inside the correct switch branch.
                    var corflags = peReader.PEHeaders.CorHeader?.Flags;
                    return (corflags & CorFlags.Requires32Bit) != 0 || (corflags & CorFlags.ILOnly) == 0
                        ? Architecture.X86 : Architecture.AnyCPU;
                default:
                    {
                        EqtTrace.Error($"AssemblyMetadataProvider.GetArchitecture: Unhandled architecture '{peReader.PEHeaders.CoffHeader.Machine}'.");
                        break;
                    }
            }
        }

        return arch;
    }

    private static FrameworkName GetFrameworkNameFromAssemblyMetadata(Stream assemblyStream)
    {
        FrameworkName frameworkName = new(Framework.DefaultFramework.Name);
        using (var peReader = new PEReader(assemblyStream))
        {
            var metadataReader = peReader.GetMetadataReader();

            foreach (var customAttributeHandle in metadataReader.CustomAttributes)
            {
                var attr = metadataReader.GetCustomAttribute(customAttributeHandle);
                var result = Encoding.UTF8.GetString(metadataReader.GetBlobBytes(attr.Value));
                if (result.Contains(".NET") && result.Contains(",Version="))
                {
                    var fxStartIndex = result.IndexOf(".NET", StringComparison.Ordinal);
                    var fxEndIndex = result.IndexOf("\u0001", fxStartIndex, StringComparison.Ordinal);
                    if (fxStartIndex > -1 && fxEndIndex > fxStartIndex)
                    {
                        // Using -3 because custom attribute values separated by unicode characters.
                        result = result.Substring(fxStartIndex, fxEndIndex - 3);
                        frameworkName = new FrameworkName(result);
                        break;
                    }
                }
            }
        }

        return frameworkName;
    }

    private Architecture MapToArchitecture(ProcessorArchitecture processorArchitecture, string assemblyPath)
    {
        Architecture arch = Architecture.AnyCPU;
        // Mapping to Architecture based on https://msdn.microsoft.com/en-us/library/system.reflection.processorarchitecture(v=vs.110).aspx

        if (processorArchitecture.Equals(ProcessorArchitecture.Amd64)
            || processorArchitecture.Equals(ProcessorArchitecture.IA64))
        {
            arch = Architecture.X64;
        }
        else if (processorArchitecture.Equals(ProcessorArchitecture.X86))
        {
            arch = Architecture.X86;
        }
        else if (processorArchitecture.Equals(ProcessorArchitecture.MSIL))
        {
            arch = Architecture.AnyCPU;
        }
        else if (processorArchitecture.Equals(ProcessorArchitecture.Arm))
        {
            arch = Architecture.ARM;
        }
        else if (processorArchitecture.Equals(ProcessorArchitecture.None))
        {
            // In case of None we fallback to PEReader
            // We don't use only PEReader for back compatibility.
            // An AnyCPU returned by AssemblyName.GetAssemblyName(assemblyPath) will result in a I386 for PEReader.
            // Also MSIL processor architecture is missing with PEReader.
            // For now it should fix the issue for the missing ARM64 architecture.
            arch = GetArchitectureFromAssemblyMetadata(assemblyPath);
        }
        else
        {
            EqtTrace.Info("Unable to map to Architecture, using platform: {0}", arch);
        }

        return arch;
    }

    public Architecture GetArchitectureForSource(string imagePath)
    {
        // For details refer to below code available on MSDN.
        //https://code.msdn.microsoft.com/windowsapps/CSCheckExeType-aab06100#content

        var archType = Architecture.AnyCPU;
        ushort machine = 0;

        uint peHeader;
        const int imageFileMachineAmd64 = 0x8664;
        const int imageFileMachineIa64 = 0x200;
        const int imageFileMachineI386 = 0x14c;
        const int imageFileMachineArm = 0x01c0; // ARM Little-Endian
        const int imageFileMachineThumb = 0x01c2; // ARM Thumb/Thumb-2 Little-Endian
        const int imageFileMachineArmnt = 0x01c4; // ARM Thumb-2 Little-Endian
        const int imageFileMachineArm64 = 0xAA64; // ARM64 Little-Endian

        try
        {
            //get the input stream
            using Stream fs = _fileHelper.GetStream(imagePath, FileMode.Open, FileAccess.Read);
            using var reader = new BinaryReader(fs);
            var validImage = true;

            //PE Header starts @ 0x3C (60). Its a 4 byte header.
            fs.Position = 0x3C;
            peHeader = reader.ReadUInt32();

            // Check if the offset is invalid
            if (peHeader > fs.Length - 5)
            {
                validImage = false;
            }

            if (validImage)
            {
                //Moving to PE Header start location...
                fs.Position = peHeader;

                var signature = reader.ReadUInt32(); //peHeaderSignature
                // 0x00004550 is the letters "PE" followed by two terminating zeros.
                if (signature != 0x00004550)
                {
                    validImage = false;
                }

                if (validImage)
                {
                    //Read the image file header.
                    machine = reader.ReadUInt16();
                    reader.ReadUInt16(); //NumberOfSections
                    reader.ReadUInt32(); //TimeDateStamp
                    reader.ReadUInt32(); //PointerToSymbolTable
                    reader.ReadUInt32(); //NumberOfSymbols
                    reader.ReadUInt16(); //SizeOfOptionalHeader
                    reader.ReadUInt16(); //Characteristics

                    // magic number.32bit or 64bit assembly.
                    var magic = reader.ReadUInt16();
                    if (magic is not 0x010B and not 0x020B)
                    {
                        validImage = false;
                    }
                }

                if (validImage)
                {
                    switch (machine)
                    {
                        case imageFileMachineI386:
                            archType = Architecture.X86;
                            break;

                        case imageFileMachineAmd64:
                        case imageFileMachineIa64:
                            archType = Architecture.X64;
                            break;

                        case imageFileMachineArm:
                        case imageFileMachineThumb:
                        case imageFileMachineArmnt:
                            archType = Architecture.ARM;
                            break;

                        case imageFileMachineArm64:
                            archType = Architecture.ARM64;
                            break;
                    }
                }
                else
                {
                    EqtTrace.Info(
                        "GetArchitectureForSource: Source path {0} is not a valid image path. Returning default proc arch type: {1}.",
                        imagePath, archType);
                }
            }
        }
        catch (Exception ex)
        {
            //Ignore all exception
            EqtTrace.Info(
                "GetArchitectureForSource: Returning default:{0}. Unhandled exception: {1}.",
                archType, ex);
        }

        return archType;
    }
}