File: Utilities\MetadataReaderHelper.cs
Web Access
Project: src\src\vstest\src\Microsoft.TestPlatform.Common\Microsoft.TestPlatform.Common.csproj (Microsoft.VisualStudio.TestPlatform.Common)
// 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.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Text;

using Microsoft.VisualStudio.TestPlatform.ObjectModel;

namespace Microsoft.VisualStudio.TestPlatform.Common.Utilities;
/* Expected attribute shape

    namespace Microsoft.VisualStudio.TestPlatform
    {
        using System;

        [AttributeUsage(AttributeTargets.Assembly, Inherited = false, AllowMultiple = true)]
        internal sealed class TestExtensionTypesV2Attribute : Attribute
        {
            public string ExtensionType { get; }
            public string ExtensionIdentifier { get; }
            public Type ExtensionImplementation { get; }
            public int Version { get; }

            public TestExtensionTypesV2Attribute(string extensionType, string extensionIdentifier, Type extensionImplementation, int version)
            {
                ExtensionType = extensionType;
                ExtensionIdentifier = extensionIdentifier;
                ExtensionImplementation = extensionImplementation;
                Version = version;
            }
        }
    }
*/
internal static class MetadataReaderExtensionsHelper
{
    private const string TestExtensionTypesAttributeV2 = "Microsoft.VisualStudio.TestPlatform.TestExtensionTypesV2Attribute";
    private static readonly ConcurrentDictionary<string, Type[]> AssemblyCache = new();
    private static readonly Type[] EmptyTypeArray = [];

    public static Type[] DiscoverTestExtensionTypesV2Attribute(Assembly loadedAssembly, string assemblyFilePath)
        => AssemblyCache.GetOrAdd(assemblyFilePath, DiscoverTestExtensionTypesV2AttributeInternal(loadedAssembly, assemblyFilePath));

    private static Type[] DiscoverTestExtensionTypesV2AttributeInternal(Assembly loadedAssembly, string assemblyFilePath)
    {
        EqtTrace.Verbose($"MetadataReaderExtensionsHelper: Discovering extensions inside assembly '{loadedAssembly.FullName}' file path '{assemblyFilePath}'");

        // We don't cache the load because this method is used by DiscoverTestExtensionTypesV2Attribute that caches the outcome Type[]
        Assembly assemblyToAnalyze;
        try
        {
            assemblyToAnalyze = Assembly.LoadFile(assemblyFilePath);
        }
        catch (Exception ex)
        {
            EqtTrace.Verbose($"MetadataReaderExtensionsHelper: Failure during assembly file load '{assemblyFilePath}', fallback to the loaded assembly.\n{FormatException(ex)}");
            assemblyToAnalyze = loadedAssembly;
        }

        List<Tuple<int, Type>>? extensions = null;
        using (var stream = new FileStream(assemblyFilePath, FileMode.Open, FileAccess.Read))
        using (var reader = new PEReader(stream, PEStreamOptions.Default))
        {
            MetadataReader metadataReader = reader.GetMetadataReader();

            // Search for the custom attribute TestExtensionTypesAttributeV2 - ECMA-335 II.22.10 CustomAttribute : 0x0C
            foreach (var customAttributeHandle in metadataReader.CustomAttributes)
            {
                string? attributeFullName = null;
                try
                {
                    if (customAttributeHandle.IsNil)
                    {
                        continue;
                    }

                    // Read custom attribute metadata row
                    var customAttribute = metadataReader.GetCustomAttribute(customAttributeHandle);

                    // We expect that the attribute is defined inside current assembly by the extension owner
                    // and so ctor should point to the MethodDefinition table - ECMA-335 II.22.26 MethodDef : 0x06
                    // and parent scope should be the current assembly [assembly:...] - ECMA-335 II.22.2 Assembly : 0x20
                    if (customAttribute.Constructor.Kind != HandleKind.MethodDefinition || customAttribute.Parent.Kind != HandleKind.AssemblyDefinition)
                    {
                        continue;
                    }

                    // Read MethodDef metadata row
                    var methodDefinition = metadataReader.GetMethodDefinition((MethodDefinitionHandle)customAttribute.Constructor);

                    // Check that name is .ctor
                    if (metadataReader.GetString(methodDefinition.Name) != ".ctor")
                    {
                        continue;
                    }

                    // Get the custom attribute TypeDef handle
                    var typeDefinitionHandle = methodDefinition.GetDeclaringType();

                    // Read TypeDef metadata row
                    var typeDef = metadataReader.GetTypeDefinition(typeDefinitionHandle);

                    // Check the attribute type full name
                    attributeFullName = $"{metadataReader.GetString(typeDef.Namespace)}.{metadataReader.GetString(typeDef.Name)}";
                    if (attributeFullName == TestExtensionTypesAttributeV2)
                    {
                        // We don't do any signature verification to allow future possibility to add new parameter in a back compat way.
                        // Get signature blob using methodDefinition.Signature index into Blob heap - ECMA-335 II.22.26 MethodDef : 0x06, 'Signature' column
                        // BlobReader signatureReader = metadataReader.GetBlobReader(methodDefinition.Signature);
                        // var decoder = new SignatureDecoder<string, object>(new SignatureDecoder(), metadataReader, genericContext: null);
                        // var ctorDecodedSignature = decoder.DecodeMethodSignature(ref signatureReader);
                        // Log the signature for analysis purpose
                        // EqtTrace.Verbose($"MetadataReaderExtensionsHelper: Find possible good attribute candidate '{attributeFullName}' with ctor signature: '{ctorDecodedSignature.ReturnType}" +
                        //    $" ctor ({(ctorDecodedSignature.ParameterTypes.Length > 0 ? ctorDecodedSignature.ParameterTypes.Aggregate((a, b) => $"{a},{b}").Trim(',') : "(")})'");

                        // Read the ctor signature values - ECMA-335 II.23.3 Custom attributes
                        BlobReader valueReader = metadataReader.GetBlobReader(customAttribute.Value);
                        // Verify the prolog
                        if (valueReader.ReadUInt16() == 1)
                        {
                            // Expected ctor shape ctor(string,string,System.Type,Int32)
                            // [assembly: TestExtensionTypesV2(ExtensionMetadata.ExtensionType, ExtensionMetadata.Uri, typeof(ExtensionImplementation), 1)]

                            // If the parameter kind is string, (middle line in above diagram) then the blob contains
                            // a SerString – a PackedLen count of bytes, followed by the UTF8 characters.If the
                            // string is null, its PackedLen has the value 0xFF(with no following characters).If
                            // the string is empty(“”), then PackedLen has the value 0x00(with no following
                            // characters).
                            string? extension = valueReader.ReadSerializedString();
                            string? extensionIdentifier = valueReader.ReadSerializedString();

                            // If the parameter kind is System.Type, (also, the middle line in above diagram) its
                            // value is stored as a SerString(as defined in the previous paragraph), representing its
                            // canonical name. The canonical name is its full type name, followed optionally by
                            // the assembly where it is defined, its version, culture and public-key-token.If the
                            // assembly name is omitted, the CLI looks first in the current assembly, and then in
                            // the system library(mscorlib); in these two special cases, it is permitted to omit the
                            // assembly-name, version, culture and public-key-token.
                            string? extensionImplementation = valueReader.ReadSerializedString();

                            // If the parameter kind is simple(first line in the above diagram) (bool, char, float32,
                            // float64, int8, int16, int32, int64, unsigned int8, unsigned int16, unsigned int32 or
                            // unsigned int64) then the 'blob' contains its binary value(Val). (A bool is a single
                            // byte with value 0(false) or 1(true); char is a two-byte Unicode character; and the
                            // others have their obvious meaning.) This pattern is also used if the parameter kind is
                            // an enum -- simply store the value of the enum's underlying integer type.
                            int version = valueReader.ReadInt32();
                            try
                            {
                                TPDebug.Assert(extensionImplementation is not null, "extensionImplementation is null");
                                var extensionType = assemblyToAnalyze.GetType(extensionImplementation);
                                if (extensionType is null)
                                {
                                    EqtTrace.Verbose($"MetadataReaderExtensionsHelper: Unable to get extension type for '{extensionImplementation}'");
                                    continue;
                                }

                                extensions ??= new List<Tuple<int, Type>>();
                                extensions.Add(Tuple.Create(version, extensionType));
                                EqtTrace.Verbose($"MetadataReaderExtensionsHelper: Valid extension found: extension type '{extension}' identifier '{extensionIdentifier}' implementation '{extensionType}' version '{version}'");
                            }
                            catch (Exception ex)
                            {
                                EqtTrace.Verbose($"MetadataReaderExtensionsHelper: Failure during type creation, extension full name: '{extensionImplementation}'\n{FormatException(ex)}");
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    EqtTrace.Verbose($"MetadataReaderExtensionsHelper: Failure during custom attribute analysis, attribute full name: {attributeFullName}\n{FormatException(ex)}");
                }
            }
        }

        return extensions?.OrderByDescending(t => t.Item1).Select(t => t.Item2).ToArray() ?? EmptyTypeArray;
    }

    private static string FormatException(Exception ex)
    {
        StringBuilder log = new();
        Exception? current = ex;
        while (current != null)
        {
            log.AppendLine(current.ToString());
            current = current.InnerException;
        }

        return log.ToString();
    }
}