File: System\Reflection\TypeLoading\General\Helpers.cs
Web Access
Project: src\src\libraries\System.Reflection.MetadataLoadContext\src\System.Reflection.MetadataLoadContext.csproj (System.Reflection.MetadataLoadContext)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Buffers;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text;
 
namespace System.Reflection.TypeLoading
{
    internal static class Helpers
    {
#if NET8_0_OR_GREATER
        private static readonly SearchValues<char> s_charsToEscape = SearchValues.Create("\\[]+*&,");
#else
        private static ReadOnlySpan<char> s_charsToEscape => "\\[]+*&,".AsSpan();
#endif
 
        [return: NotNullIfNotNull(nameof(original))]
        public static T[]? CloneArray<T>(this T[]? original)
        {
            if (original == null)
                return null;
 
            if (original.Length == 0)
                return Array.Empty<T>();
 
            // We want to return the exact type of T[] even if "original" is a type of T2[] (due to array variance.)
            // The arrays produced by this helper are usually passed directly to app code.
            T[] copy = new T[original.Length];
            Array.Copy(sourceArray: original, sourceIndex: 0, destinationArray: copy, destinationIndex: 0, length: original.Length);
            return copy;
        }
 
        [return: NotNullIfNotNull(nameof(original))]
        // Converts an array of modified types to unmodified types when unmodified are requested.
        // This prevents inconsistencies such as allowing modifiers to be returned.
        // This doesn't affect performance since we need to clone arrays anyway before returning to the caller.
        public static Type[]? CloneArrayToUnmodifiedTypes(this Type[]? original)
        {
            if (original == null)
                return null;
 
            if (original.Length == 0)
                return Type.EmptyTypes;
 
            Type[] copy = new Type[original.Length];
            for (int i = 0; i < original.Length; i++)
            {
                copy[i] = original[i].UnderlyingSystemType;
            }
 
            return copy;
        }
 
        public static ReadOnlyCollection<T> ToReadOnlyCollection<T>(this IEnumerable<T> enumeration)
        {
            List<T> list = new List<T>(enumeration);
            return Array.AsReadOnly(list.ToArray());
        }
 
        public static int GetTokenRowNumber(this int token) => token & 0x00ffffff;
 
        public static RoMethod? FilterInheritedAccessor(this RoMethod accessor)
        {
            if (accessor.ReflectedType == accessor.DeclaringType)
                return accessor;
 
            if (accessor.IsPrivate)
                return null;
 
            // If the accessor is virtual, .NET Framework tries to look for a overriding member starting from ReflectedType - a situation
            // which probably isn't expressible in any known language. Detecting overrides veers into vtable-building business which
            // is something this library tries to avoid. If anyone ever cares about this, welcome to fix.
 
            return accessor;
        }
 
        public static MethodInfo? FilterAccessor(this MethodInfo accessor, bool nonPublic)
        {
            if (nonPublic)
                return accessor;
            if (accessor.IsPublic)
                return accessor;
            return null;
        }
 
        public static string ComputeArraySuffix(int rank, bool multiDim)
        {
            Debug.Assert(rank == 1 || multiDim);
 
            if (!multiDim)
                return "[]";
            if (rank == 1)
                return "[*]";
            return "[" + new string(',', rank - 1) + "]";
        }
 
        // Escape identifiers as described in "Specifying Fully Qualified Type Names" on msdn.
        // Current link is http://msdn.microsoft.com/en-us/library/yfsftwz6(v=vs.110).aspx
        public static string EscapeTypeNameIdentifier(this string identifier)
        {
            // Some characters in a type name need to be escaped
            if (TypeNameContainsTypeParserMetacharacters(identifier))
            {
                StringBuilder sbEscapedName = new StringBuilder(identifier.Length);
                foreach (char c in identifier)
                {
                    if (c.NeedsEscapingInTypeName())
                        sbEscapedName.Append('\\');
 
                    sbEscapedName.Append(c);
                }
                identifier = sbEscapedName.ToString();
            }
            return identifier;
        }
 
        public static bool TypeNameContainsTypeParserMetacharacters(this string identifier)
        {
            return identifier.AsSpan().IndexOfAny(s_charsToEscape) >= 0;
        }
 
        public static bool NeedsEscapingInTypeName(this char c)
        {
#if NET8_0_OR_GREATER
            return s_charsToEscape.Contains(c);
#else
            return s_charsToEscape.IndexOf(c) >= 0;
#endif
        }
 
        public static string UnescapeTypeNameIdentifier(this string identifier)
        {
#if NET
            if (identifier.Contains('\\'))
#else
            if (identifier.IndexOf('\\') != -1)
#endif
            {
                StringBuilder sbUnescapedName = new StringBuilder(identifier.Length);
                for (int i = 0; i < identifier.Length; i++)
                {
                    if (identifier[i] == '\\')
                    {
                        // If we have a trailing '\\', the framework somehow messed up escaping the original identifier. Since that's
                        // unlikely to happen and unactionable, we'll just let the next line IndexOutOfRange if that happens.
                        i++;
                    }
                    sbUnescapedName.Append(identifier[i]);
                }
                identifier = sbUnescapedName.ToString();
            }
            return identifier;
        }
 
        /// <summary>
        /// For AssemblyReferences, convert "unspecified" components from the ECMA format (0xffff) to the in-memory System.Version format (0xffffffff).
        /// </summary>
        public static Version? AdjustForUnspecifiedVersionComponents(this Version v)
        {
            int mask =
                ((v.Revision == ushort.MaxValue) ? 0b0001 : 0) |
                ((v.Build == ushort.MaxValue) ? 0b0010 : 0) |
                ((v.Minor == ushort.MaxValue) ? 0b0100 : 0) |
                ((v.Major == ushort.MaxValue) ? 0b1000 : 0);
 
            return mask switch
            {
                0b0000 => v,
                0b0001 => new Version(v.Major, v.Minor, v.Build),
                0b0010 => new Version(v.Major, v.Minor),
                0b0011 => new Version(v.Major, v.Minor),
                _ => null,
            };
        }
 
        public static byte[]? ComputePublicKeyToken(this byte[] pkt)
        {
            // @TODO - https://github.com/dotnet/corefxlab/issues/2447 - This is not the best way to compute the PKT as AssemblyName
            // throws if the PK isn't a valid PK blob. That's not something we should block a metadata inspection tool for so we
            // should compute the PKT ourselves as soon as we can convince the libraries analyzers to let us use SHA1.
            AssemblyName an = new AssemblyName();
            an.SetPublicKey(pkt);
            return an.GetPublicKeyToken();
        }
 
        public static AssemblyNameFlags ConvertAssemblyFlagsToAssemblyNameFlags(AssemblyFlags assemblyFlags)
        {
            AssemblyNameFlags assemblyNameFlags = AssemblyNameFlags.None;
 
            if ((assemblyFlags & AssemblyFlags.Retargetable) != 0)
            {
                assemblyNameFlags |= AssemblyNameFlags.Retargetable;
            }
 
            return assemblyNameFlags;
        }
 
        //
        // Note that for a top level type, the resulting ns is string.Empty, *not* null.
        // This is a concession to the fact that MetadataReader's fast String equal methods
        // don't accept null.
        //
        public static void SplitTypeName(this string fullName, out string ns, out string name)
        {
            Debug.Assert(fullName != null);
 
            int indexOfLastDot = fullName.LastIndexOf('.');
            if (indexOfLastDot == -1)
            {
                ns = string.Empty;
                name = fullName;
            }
            else
            {
                ns = fullName.Substring(0, indexOfLastDot);
                name = fullName.Substring(indexOfLastDot + 1);
            }
        }
 
        //
        // Rejoin a namespace and type name back into a full name.
        //
        // Note that for a top level type, the namespace is string.Empty, *not* null (as Reflection surfaces it.)
        // This is a concession to the fact that MetadataReader's fast String equal methods don't accept null.
        //
        public static string AppendTypeName(this string ns, string name)
        {
            Debug.Assert(ns != null, "For top level types, the namespace must be string.Empty, not null");
            Debug.Assert(name != null);
 
            return ns.Length == 0 ? name : ns + "." + name;
        }
 
        //
        // Common helper for ConstructorInfo.ToString() and MethodInfo.ToString()
        //
        public static string ToString(this IRoMethodBase roMethodBase, MethodSig<string> methodSigStrings)
        {
            TypeContext typeContext = roMethodBase.TypeContext;
 
            StringBuilder sb = new StringBuilder();
            sb.Append(methodSigStrings[-1]);
            sb.Append(' ');
            sb.Append(roMethodBase.MethodBase.Name);
 
            Type[]? genericMethodArguments = typeContext.GenericMethodArguments;
            int count = genericMethodArguments == null ? 0 : genericMethodArguments.Length;
            if (count != 0)
            {
                sb.Append('[');
                for (int gpi = 0; gpi < count; gpi++)
                {
                    if (gpi != 0)
                        sb.Append(',');
                    sb.Append(genericMethodArguments![gpi].ToString());
                }
                sb.Append(']');
            }
 
            sb.Append('(');
            for (int position = 0; position < methodSigStrings.Parameters.Length; position++)
            {
                if (position != 0)
                    sb.Append(", ");
                sb.Append(methodSigStrings[position]);
            }
            sb.Append(')');
            return sb.ToString();
        }
 
        public static bool HasSameMetadataDefinitionAsCore<M>(this M thisMember, MemberInfo other) where M : MemberInfo
        {
            if (other is null)
                throw new ArgumentNullException(nameof(other));
 
            // Ensure that "other" is one of our MemberInfo objects. Do this check before calling any methods on it!
            if (!(other is M))
                return false;
 
            if (thisMember.MetadataToken != other.MetadataToken)
                return false;
 
            if (!(thisMember.Module.Equals(other.Module)))
                return false;
 
            return true;
        }
 
        public static RoType? LoadTypeFromAssemblyQualifiedName(string name, RoAssembly defaultAssembly, bool ignoreCase, bool throwOnError)
        {
            if (!name.TypeNameContainsTypeParserMetacharacters())
            {
                // Fast-path: the type contains none of the parser metacharacters nor the escape character. Just treat as plain old type name.
                name.SplitTypeName(out string ns, out string simpleName);
                RoType? type = defaultAssembly.GetTypeCore(ns, simpleName, ignoreCase: ignoreCase, out Exception? e);
                if (type != null)
                    return type;
                if (throwOnError)
                    throw e!;
            }
 
            MetadataLoadContext loader = defaultAssembly.Loader;
 
            Func<AssemblyName, Assembly> assemblyResolver =
                loader.LoadFromAssemblyName;
 
            Func<Assembly?, string, bool, Type?> typeResolver =
                delegate (Assembly? assembly, string fullName, bool ignoreCase2)
                {
                    assembly ??= defaultAssembly;
 
                    Debug.Assert(assembly is RoAssembly);
                    RoAssembly roAssembly = (RoAssembly)assembly;
 
                    fullName = fullName.UnescapeTypeNameIdentifier();
                    fullName.SplitTypeName(out string ns, out string simpleName);
                    Type? type = roAssembly.GetTypeCore(ns, simpleName, ignoreCase: ignoreCase2, out Exception? e);
                    if (type != null)
                        return type;
                    if (throwOnError)
                        throw e!;
                    return null;
                };
 
            return (RoType?)Type.GetType(name, assemblyResolver: assemblyResolver, typeResolver: typeResolver, throwOnError: throwOnError, ignoreCase: ignoreCase);
        }
 
        public static RoType SkipTypeWrappers(this RoType type)
        {
            while (type is RoWrappedType roWrappedType)
            {
                type = roWrappedType.UnmodifiedType;
            }
            return type;
        }
 
        public static bool IsVisibleOutsideAssembly(this Type type)
        {
            TypeAttributes visibility = type.Attributes & TypeAttributes.VisibilityMask;
            if (visibility == TypeAttributes.Public)
                return true;
 
            if (visibility == TypeAttributes.NestedPublic)
                return type.DeclaringType!.IsVisibleOutsideAssembly();
 
            return false;
        }
 
        //
        // Converts an AssemblyName to a RoAssemblyName that is free from any future mutations on the AssemblyName.
        //
        public static RoAssemblyName ToRoAssemblyName(this AssemblyName assemblyName)
        {
            if (assemblyName.Name == null)
                throw new ArgumentException();
 
            // AssemblyName's PKT property getters do NOT copy the array before giving it out. Make our own copy
            // as the original is wide open to tampering by anyone.
            byte[]? pkt = assemblyName.GetPublicKeyToken().CloneArray();
 
            return new RoAssemblyName(assemblyName.Name, assemblyName.Version, assemblyName.CultureName, pkt, assemblyName.Flags);
        }
 
        public static byte[] ToUtf8(this string s) => Encoding.UTF8.GetBytes(s);
 
#if NET
        public static string ToUtf16(this ReadOnlySpan<byte> utf8) => Encoding.UTF8.GetString(utf8);
#else
        public static unsafe string ToUtf16(this ReadOnlySpan<byte> utf8)
        {
            if (utf8.IsEmpty)
            {
                return string.Empty;
            }
 
            fixed (byte* ptr = utf8)
            {
                return Encoding.UTF8.GetString(ptr, utf8.Length);
            }
        }
#endif
 
        // Guards ToString() implementations. Sample usage:
        //
        //    public sealed override string ToString() => Loader.GetDisposedString() ?? <your real ToString() code>;"
        //
        public static string? GetDisposedString(this MetadataLoadContext loader) => loader.IsDisposed ? SR.MetadataLoadContextDisposed : null;
 
        public static TypeContext ToTypeContext(this RoType[] instantiation) => new TypeContext(instantiation, null);
    }
}