diff --git a/src/coreclr/System.Private.CoreLib/src/System/Reflection/TypeNameParser.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/Reflection/TypeNameParser.CoreCLR.cs index 2af4bb792d4588..35fa0b5718e387 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/Reflection/TypeNameParser.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/Reflection/TypeNameParser.CoreCLR.cs @@ -4,11 +4,9 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Loader; -using System.Text; using System.Threading; namespace System.Reflection @@ -57,7 +55,13 @@ internal partial struct TypeNameParser return null; } - return new TypeNameParser(typeName) + Metadata.TypeName? parsed = Metadata.TypeNameParser.Parse(typeName, throwOnError: throwOnError); + if (parsed is null) + { + return null; + } + + return new TypeNameParser() { _assemblyResolver = assemblyResolver, _typeResolver = typeResolver, @@ -65,7 +69,7 @@ internal partial struct TypeNameParser _ignoreCase = ignoreCase, _extensibleParser = extensibleParser, _requestingAssembly = requestingAssembly - }.Parse(); + }.Resolve(parsed); } [RequiresUnreferencedCode("The type might be removed")] @@ -75,13 +79,24 @@ internal partial struct TypeNameParser bool ignoreCase, Assembly topLevelAssembly) { - return new TypeNameParser(typeName) + Metadata.TypeName? parsed = Metadata.TypeNameParser.Parse(typeName, throwOnError); + + if (parsed is null) + { + return null; + } + else if (topLevelAssembly is not null && parsed.AssemblyName is not null) + { + return throwOnError ? throw new ArgumentException(SR.Argument_AssemblyGetTypeCannotSpecifyAssembly) : null; + } + + return new TypeNameParser() { _throwOnError = throwOnError, _ignoreCase = ignoreCase, _topLevelAssembly = topLevelAssembly, _requestingAssembly = topLevelAssembly - }.Parse(); + }.Resolve(parsed); } // Resolve type name referenced by a custom attribute metadata. @@ -95,12 +110,13 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName, RuntimeAssembly requestingAssembly = scope.GetRuntimeAssembly(); - RuntimeType? type = (RuntimeType?)new TypeNameParser(typeName) + Metadata.TypeName parsed = Metadata.TypeName.Parse(typeName); + RuntimeType? type = (RuntimeType?)new TypeNameParser() { _throwOnError = true, _suppressContextualReflectionContext = true, _requestingAssembly = requestingAssembly - }.Parse(); + }.Resolve(parsed); Debug.Assert(type != null); @@ -124,13 +140,19 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName, return null; } - RuntimeType? type = (RuntimeType?)new TypeNameParser(typeName) + Metadata.TypeName? parsed = Metadata.TypeNameParser.Parse(typeName, throwOnError); + if (parsed is null) + { + return null; + } + + RuntimeType? type = (RuntimeType?)new TypeNameParser() { _requestingAssembly = requestingAssembly, _throwOnError = throwOnError, _suppressContextualReflectionContext = true, _requireAssemblyQualifiedName = requireAssemblyQualifiedName, - }.Parse(); + }.Resolve(parsed); if (type != null) RuntimeTypeHandle.RegisterCollectibleTypeDependency(type, requestingAssembly); @@ -138,23 +160,12 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName, return type; } - private bool CheckTopLevelAssemblyQualifiedName() - { - if (_topLevelAssembly is not null) - { - if (_throwOnError) - throw new ArgumentException(SR.Argument_AssemblyGetTypeCannotSpecifyAssembly); - return false; - } - return true; - } - - private Assembly? ResolveAssembly(string assemblyName) + private Assembly? ResolveAssembly(AssemblyName assemblyName) { Assembly? assembly; if (_assemblyResolver is not null) { - assembly = _assemblyResolver(new AssemblyName(assemblyName)); + assembly = _assemblyResolver(assemblyName); if (assembly is null && _throwOnError) { throw new FileNotFoundException(SR.Format(SR.FileNotFound_ResolveAssembly, assemblyName)); @@ -162,7 +173,7 @@ private bool CheckTopLevelAssemblyQualifiedName() } else { - assembly = RuntimeAssembly.InternalLoad(new AssemblyName(assemblyName), ref Unsafe.NullRef(), + assembly = RuntimeAssembly.InternalLoad(assemblyName, ref Unsafe.NullRef(), _suppressContextualReflectionContext ? null : AssemblyLoadContext.CurrentContextualReflectionContext, requestingAssembly: (RuntimeAssembly?)_requestingAssembly, throwOnFileNotFound: _throwOnError); } @@ -173,13 +184,14 @@ private bool CheckTopLevelAssemblyQualifiedName() Justification = "TypeNameParser.GetType is marked as RequiresUnreferencedCode.")] [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075:UnrecognizedReflectionPattern", Justification = "TypeNameParser.GetType is marked as RequiresUnreferencedCode.")] - private Type? GetType(string typeName, ReadOnlySpan nestedTypeNames, string? assemblyNameIfAny) + private Type? GetType(string escapedTypeName, // For nested types, it's Name. For other types it's FullName + ReadOnlySpan nestedTypeNames, Metadata.TypeName parsedName) { Assembly? assembly; - if (assemblyNameIfAny is not null) + if (parsedName.AssemblyName is not null) { - assembly = ResolveAssembly(assemblyNameIfAny); + assembly = ResolveAssembly(parsedName.AssemblyName.ToAssemblyName()); if (assembly is null) return null; } @@ -193,8 +205,6 @@ private bool CheckTopLevelAssemblyQualifiedName() // Resolve the top level type. if (_typeResolver is not null) { - string escapedTypeName = EscapeTypeName(typeName); - type = _typeResolver(assembly, escapedTypeName, _ignoreCase); if (type is null) @@ -216,28 +226,29 @@ private bool CheckTopLevelAssemblyQualifiedName() { if (_throwOnError) { - throw new TypeLoadException(SR.Format(SR.TypeLoad_ResolveType, EscapeTypeName(typeName))); + throw new TypeLoadException(SR.Format(SR.TypeLoad_ResolveType, escapedTypeName)); } return null; } - return GetTypeFromDefaultAssemblies(typeName, nestedTypeNames); + return GetTypeFromDefaultAssemblies(UnescapeTypeName(escapedTypeName), nestedTypeNames, parsedName); } if (assembly is RuntimeAssembly runtimeAssembly) { + string unescapedTypeName = UnescapeTypeName(escapedTypeName); // Compat: Non-extensible parser allows ambiguous matches with ignore case lookup if (!_extensibleParser || !_ignoreCase) { - return runtimeAssembly.GetTypeCore(typeName, nestedTypeNames, throwOnError: _throwOnError, ignoreCase: _ignoreCase); + return runtimeAssembly.GetTypeCore(unescapedTypeName, nestedTypeNames, throwOnError: _throwOnError, ignoreCase: _ignoreCase); } - type = runtimeAssembly.GetTypeCore(typeName, default, throwOnError: _throwOnError, ignoreCase: _ignoreCase); + type = runtimeAssembly.GetTypeCore(unescapedTypeName, default, throwOnError: _throwOnError, ignoreCase: _ignoreCase); } else { // This is a third-party Assembly object. Emulate GetTypeCore() by calling the public GetType() // method. This is wasteful because it'll probably reparse a type string that we've already parsed // but it can't be helped. - type = assembly.GetType(EscapeTypeName(typeName), throwOnError: _throwOnError, ignoreCase: _ignoreCase); + type = assembly.GetType(escapedTypeName, throwOnError: _throwOnError, ignoreCase: _ignoreCase); } if (type is null) @@ -257,7 +268,7 @@ private bool CheckTopLevelAssemblyQualifiedName() if (_throwOnError) { throw new TypeLoadException(SR.Format(SR.TypeLoad_ResolveNestedType, - nestedTypeNames[i], (i > 0) ? nestedTypeNames[i - 1] : typeName)); + nestedTypeNames[i], (i > 0) ? nestedTypeNames[i - 1] : escapedTypeName)); } return null; } @@ -266,12 +277,12 @@ private bool CheckTopLevelAssemblyQualifiedName() return type; } - private Type? GetTypeFromDefaultAssemblies(string typeName, ReadOnlySpan nestedTypeNames) + private Type? GetTypeFromDefaultAssemblies(string typeName, ReadOnlySpan nestedTypeNames, Metadata.TypeName parsedName) { RuntimeAssembly? requestingAssembly = (RuntimeAssembly?)_requestingAssembly; if (requestingAssembly is not null) { - Type? type = ((RuntimeAssembly)requestingAssembly).GetTypeCore(typeName, nestedTypeNames, throwOnError: false, ignoreCase: _ignoreCase); + Type? type = requestingAssembly.GetTypeCore(typeName, nestedTypeNames, throwOnError: false, ignoreCase: _ignoreCase); if (type is not null) return type; } @@ -279,12 +290,12 @@ private bool CheckTopLevelAssemblyQualifiedName() RuntimeAssembly coreLib = (RuntimeAssembly)typeof(object).Assembly; if (requestingAssembly != coreLib) { - Type? type = ((RuntimeAssembly)coreLib).GetTypeCore(typeName, nestedTypeNames, throwOnError: false, ignoreCase: _ignoreCase); + Type? type = coreLib.GetTypeCore(typeName, nestedTypeNames, throwOnError: false, ignoreCase: _ignoreCase); if (type is not null) return type; } - RuntimeAssembly? resolvedAssembly = AssemblyLoadContext.OnTypeResolve(requestingAssembly, EscapeTypeName(typeName, nestedTypeNames)); + RuntimeAssembly? resolvedAssembly = AssemblyLoadContext.OnTypeResolve(requestingAssembly, parsedName.FullName); if (resolvedAssembly is not null) { Type? type = resolvedAssembly.GetTypeCore(typeName, nestedTypeNames, throwOnError: false, ignoreCase: _ignoreCase); @@ -293,7 +304,7 @@ private bool CheckTopLevelAssemblyQualifiedName() } if (_throwOnError) - throw new TypeLoadException(SR.Format(SR.TypeLoad_ResolveTypeFromAssembly, EscapeTypeName(typeName), (requestingAssembly ?? coreLib).FullName)); + throw new TypeLoadException(SR.Format(SR.TypeLoad_ResolveTypeFromAssembly, parsedName.FullName, (requestingAssembly ?? coreLib).FullName)); return null; } diff --git a/src/coreclr/System.Private.CoreLib/src/System/Type.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/Type.CoreCLR.cs index 03a92b709eb1a5..81caecb09c99aa 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/Type.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/Type.CoreCLR.cs @@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.CompilerServices; -using System.Runtime.Versioning; using System.Security; using StackCrawlMark = System.Threading.StackCrawlMark; diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/RuntimeAssemblyName.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/RuntimeAssemblyName.cs index ee76c4a5e302dd..5e855eb58565d6 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/RuntimeAssemblyName.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/RuntimeAssemblyName.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Reflection.Metadata; namespace System.Reflection { @@ -129,6 +130,11 @@ public AssemblyName ToAssemblyName() return assemblyName; } + internal static RuntimeAssemblyName FromAssemblyNameInfo(AssemblyNameInfo source) + { + return new(source.Name, source.Version, source.CultureName, source._flags, source.PublicKeyOrToken); + } + // // Copies a RuntimeAssemblyName into a freshly allocated AssemblyName with no data aliasing to any other object. // @@ -142,10 +148,10 @@ public void CopyToAssemblyName(AssemblyName blank) // Our "Flags" contain both the classic flags and the ProcessorArchitecture + ContentType bits. The public AssemblyName has separate properties for // these. The setters for these properties quietly mask out any bits intended for the other one, so we needn't do that ourselves.. - blank.Flags = ExtractAssemblyNameFlags(this.Flags); - blank.ContentType = ExtractAssemblyContentType(this.Flags); + blank.Flags = AssemblyNameInfo.ExtractAssemblyNameFlags(this.Flags); + blank.ContentType = AssemblyNameInfo.ExtractAssemblyContentType(this.Flags); #pragma warning disable SYSLIB0037 // AssemblyName.ProcessorArchitecture is obsolete - blank.ProcessorArchitecture = ExtractProcessorArchitecture(this.Flags); + blank.ProcessorArchitecture = AssemblyNameInfo.ExtractProcessorArchitecture(this.Flags); #pragma warning restore SYSLIB0037 if (this.PublicKeyOrToken != null) @@ -168,17 +174,8 @@ public string FullName get { byte[]? pkt = (0 != (Flags & AssemblyNameFlags.PublicKey)) ? AssemblyNameHelpers.ComputePublicKeyToken(PublicKeyOrToken) : PublicKeyOrToken; - return AssemblyNameFormatter.ComputeDisplayName(Name, Version, CultureName, pkt, ExtractAssemblyNameFlags(Flags), ExtractAssemblyContentType(Flags)); + return AssemblyNameFormatter.ComputeDisplayName(Name, Version, CultureName, pkt, AssemblyNameInfo.ExtractAssemblyNameFlags(Flags), AssemblyNameInfo.ExtractAssemblyContentType(Flags)); } } - - private static AssemblyNameFlags ExtractAssemblyNameFlags(AssemblyNameFlags combinedFlags) - => combinedFlags & unchecked((AssemblyNameFlags)0xFFFFF10F); - - private static AssemblyContentType ExtractAssemblyContentType(AssemblyNameFlags flags) - => (AssemblyContentType)((((int)flags) >> 9) & 0x7); - - private static ProcessorArchitecture ExtractProcessorArchitecture(AssemblyNameFlags flags) - => (ProcessorArchitecture)((((int)flags) >> 4) & 0x7); } } diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/TypeNameParser.NativeAot.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/TypeNameParser.NativeAot.cs index e370cb4eed0a83..2149003ddcfb4c 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/TypeNameParser.NativeAot.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/TypeNameParser.NativeAot.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Reflection.Runtime.Assemblies; @@ -53,7 +51,13 @@ internal partial struct TypeNameParser return null; } - return new TypeNameParser(typeName) + Metadata.TypeName? parsed = Metadata.TypeNameParser.Parse(typeName, throwOnError); + if (parsed is null) + { + return null; + } + + return new TypeNameParser() { _assemblyResolver = assemblyResolver, _typeResolver = typeResolver, @@ -61,7 +65,7 @@ internal partial struct TypeNameParser _ignoreCase = ignoreCase, _extensibleParser = extensibleParser, _defaultAssemblyName = defaultAssemblyName - }.Parse(); + }.Resolve(parsed); } internal static Type? GetType( @@ -70,35 +74,35 @@ internal partial struct TypeNameParser bool ignoreCase, Assembly topLevelAssembly) { - return new TypeNameParser(typeName) + Metadata.TypeName? parsed = Metadata.TypeNameParser.Parse(typeName, throwOnError); + + if (parsed is null) + { + return null; + } + else if (topLevelAssembly is not null && parsed.AssemblyName is not null) + { + return throwOnError ? throw new ArgumentException(SR.Argument_AssemblyGetTypeCannotSpecifyAssembly) : null; + } + + return new TypeNameParser() { _throwOnError = throwOnError, _ignoreCase = ignoreCase, _topLevelAssembly = topLevelAssembly, - }.Parse(); + }.Resolve(parsed); } - private bool CheckTopLevelAssemblyQualifiedName() - { - if (_topLevelAssembly is not null) - { - if (_throwOnError) - throw new ArgumentException(SR.Argument_AssemblyGetTypeCannotSpecifyAssembly); - return false; - } - return true; - } - - private Assembly? ResolveAssembly(string assemblyName) + private Assembly? ResolveAssembly(Metadata.AssemblyNameInfo assemblyName) { Assembly? assembly; if (_assemblyResolver is not null) { - assembly = _assemblyResolver(new AssemblyName(assemblyName)); + assembly = _assemblyResolver(assemblyName.ToAssemblyName()); } else { - assembly = RuntimeAssemblyInfo.GetRuntimeAssemblyIfExists(RuntimeAssemblyName.Parse(assemblyName)); + assembly = RuntimeAssemblyInfo.GetRuntimeAssemblyIfExists(RuntimeAssemblyName.FromAssemblyNameInfo(assemblyName)); } if (assembly is null && _throwOnError) @@ -113,13 +117,13 @@ private bool CheckTopLevelAssemblyQualifiedName() Justification = "GetType APIs are marked as RequiresUnreferencedCode.")] [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075:UnrecognizedReflectionPattern", Justification = "GetType APIs are marked as RequiresUnreferencedCode.")] - private Type? GetType(string typeName, ReadOnlySpan nestedTypeNames, string? assemblyNameIfAny) + private Type? GetType(string escapedTypeName, ReadOnlySpan nestedTypeNames, Metadata.TypeName parsedName) { Assembly? assembly; - if (assemblyNameIfAny is not null) + if (parsedName.AssemblyName is not null) { - assembly = ResolveAssembly(assemblyNameIfAny); + assembly = ResolveAssembly(parsedName.AssemblyName); if (assembly is null) return null; } @@ -133,8 +137,6 @@ private bool CheckTopLevelAssemblyQualifiedName() // Resolve the top level type. if (_typeResolver is not null) { - string escapedTypeName = EscapeTypeName(typeName); - type = _typeResolver(assembly, escapedTypeName, _ignoreCase); if (type is null) @@ -154,14 +156,14 @@ private bool CheckTopLevelAssemblyQualifiedName() { if (assembly is RuntimeAssemblyInfo runtimeAssembly) { - type = runtimeAssembly.GetTypeCore(typeName, throwOnError: _throwOnError, ignoreCase: _ignoreCase); + type = runtimeAssembly.GetTypeCore(UnescapeTypeName(escapedTypeName), throwOnError: _throwOnError, ignoreCase: _ignoreCase); } else { // This is a third-party Assembly object. We can emulate GetTypeCore() by calling the public GetType() // method. This is wasteful because it'll probably reparse a type string that we've already parsed // but it can't be helped. - type = assembly.GetType(EscapeTypeName(typeName), throwOnError: _throwOnError, ignoreCase: _ignoreCase); + type = assembly.GetType(escapedTypeName, throwOnError: _throwOnError, ignoreCase: _ignoreCase); } if (type is null) @@ -169,13 +171,15 @@ private bool CheckTopLevelAssemblyQualifiedName() } else { + string? unescapedTypeName = UnescapeTypeName(escapedTypeName); + RuntimeAssemblyInfo? defaultAssembly = null; if (_defaultAssemblyName != null) { defaultAssembly = RuntimeAssemblyInfo.GetRuntimeAssemblyIfExists(RuntimeAssemblyName.Parse(_defaultAssemblyName)); if (defaultAssembly != null) { - type = defaultAssembly.GetTypeCore(typeName, throwOnError: false, ignoreCase: _ignoreCase); + type = defaultAssembly.GetTypeCore(unescapedTypeName, throwOnError: false, ignoreCase: _ignoreCase); } } @@ -185,7 +189,7 @@ private bool CheckTopLevelAssemblyQualifiedName() coreLib = (RuntimeAssemblyInfo)typeof(object).Assembly; if (coreLib != assembly) { - type = coreLib.GetTypeCore(typeName, throwOnError: false, ignoreCase: _ignoreCase); + type = coreLib.GetTypeCore(unescapedTypeName, throwOnError: false, ignoreCase: _ignoreCase); } } @@ -193,7 +197,7 @@ private bool CheckTopLevelAssemblyQualifiedName() { if (_throwOnError) { - throw Helpers.CreateTypeLoadException(typeName, (defaultAssembly ?? coreLib).FullName); + throw Helpers.CreateTypeLoadException(unescapedTypeName, (defaultAssembly ?? coreLib).FullName); } return null; } @@ -214,10 +218,9 @@ private bool CheckTopLevelAssemblyQualifiedName() if (type is null && _ignoreCase && !_extensibleParser) { // Return the first name that matches. Which one gets returned on a multiple match is an implementation detail. - string lowerName = nestedTypeNames[i].ToLowerInvariant(); foreach (Type nt in declaringType.GetNestedTypes(BindingFlags.NonPublic | BindingFlags.Public)) { - if (nt.Name.ToLowerInvariant() == lowerName) + if (nt.Name.Equals(nestedTypeNames[i], StringComparison.InvariantCultureIgnoreCase)) { type = nt; break; @@ -230,7 +233,7 @@ private bool CheckTopLevelAssemblyQualifiedName() if (_throwOnError) { throw new TypeLoadException(SR.Format(SR.TypeLoad_ResolveNestedType, - nestedTypeNames[i], (i > 0) ? nestedTypeNames[i - 1] : typeName)); + nestedTypeNames[i], (i > 0) ? nestedTypeNames[i - 1] : escapedTypeName)); } return null; } diff --git a/src/coreclr/tools/Common/TypeSystem/Common/Utilities/CustomAttributeTypeNameParser.cs b/src/coreclr/tools/Common/TypeSystem/Common/Utilities/CustomAttributeTypeNameParser.cs index a52a68f0028a48..72779375f4697a 100644 --- a/src/coreclr/tools/Common/TypeSystem/Common/Utilities/CustomAttributeTypeNameParser.cs +++ b/src/coreclr/tools/Common/TypeSystem/Common/Utilities/CustomAttributeTypeNameParser.cs @@ -2,8 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections.Generic; -using System.Text; +using System.Reflection.Metadata; using Internal.TypeSystem; @@ -37,12 +36,17 @@ internal partial struct TypeNameParser public static TypeDesc ResolveType(ModuleDesc module, string name, bool throwIfNotFound, Func canonResolver) { - return new TypeNameParser(name.AsSpan()) + if (!TypeName.TryParse(name.AsSpan(), out TypeName parsed)) + { + ThrowHelper.ThrowTypeLoadException(name, module); + } + + return new TypeNameParser() { _module = module, _throwIfNotFound = throwIfNotFound, _canonResolver = canonResolver - }.Parse()?.Value; + }.Resolve(parsed)?.Value; } private sealed class Type @@ -64,12 +68,10 @@ public Type MakeGenericType(Type[] typeArguments) } } - private static bool CheckTopLevelAssemblyQualifiedName() => true; - - private Type GetType(string typeName, ReadOnlySpan nestedTypeNames, string assemblyNameIfAny) + private Type GetType(string typeName, ReadOnlySpan nestedTypeNames, TypeName parsedName) { - ModuleDesc module = (assemblyNameIfAny == null) ? _module : - _module.Context.ResolveAssembly(new AssemblyName(assemblyNameIfAny), throwIfNotFound: _throwIfNotFound); + ModuleDesc module = (parsedName.AssemblyName == null) ? _module : + _module.Context.ResolveAssembly(parsedName.AssemblyName.ToAssemblyName(), throwIfNotFound: _throwIfNotFound); if (_canonResolver != null && nestedTypeNames.IsEmpty) { @@ -86,7 +88,7 @@ private Type GetType(string typeName, ReadOnlySpan nestedTypeNames, stri } // If it didn't resolve and wasn't assembly-qualified, we also try core library - if (assemblyNameIfAny == null) + if (parsedName.AssemblyName == null) { Type type = GetTypeCore(module.Context.SystemModule, typeName, nestedTypeNames); if (type != null) @@ -94,7 +96,7 @@ private Type GetType(string typeName, ReadOnlySpan nestedTypeNames, stri } if (_throwIfNotFound) - ThrowHelper.ThrowTypeLoadException(EscapeTypeName(typeName, nestedTypeNames), module); + ThrowHelper.ThrowTypeLoadException(parsedName.FullName, module); return null; } @@ -115,10 +117,5 @@ private static Type GetTypeCore(ModuleDesc module, string typeName, ReadOnlySpan return new Type(type); } - - private void ParseError() - { - ThrowHelper.ThrowTypeLoadException(_input.ToString(), _module); - } } } diff --git a/src/coreclr/tools/ILVerification/ILVerification.projitems b/src/coreclr/tools/ILVerification/ILVerification.projitems index 81ce9f65d1139a..3b2c30ddef1f18 100644 --- a/src/coreclr/tools/ILVerification/ILVerification.projitems +++ b/src/coreclr/tools/ILVerification/ILVerification.projitems @@ -66,12 +66,45 @@ Utilities\CustomAttributeTypeNameParser.cs - + + Utilities\HexConverter.cs + + + Utilities\AssemblyNameFormatter.cs + + + Utilities\AssemblyNameParser.cs + + + Utilities\Metadata\AssemblyNameInfo.cs + + + Utilities\TypeName.cs + + + Utilities\TypeNameParserOptions.cs + + Utilities\TypeNameParser.cs + + Utilities\TypeNameParserHelpers.cs + + + System\Diagnostics\CodeAnalysis\UnconditionalSuppressMessageAttribute.cs + + + System\Diagnostics\CodeAnalysis\NullableAttributes.cs + + + Utilities\CustomAttributeTypeNameParser.Helpers + Utilities\ValueStringBuilder.cs + + Utilities\ValueStringBuilder.AppendSpanFormattable.cs + Utilities\LockFreeReaderHashtable.cs diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/Dataflow/TypeNameParser.Dataflow.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/Dataflow/TypeNameParser.Dataflow.cs index 90b63f39c9f026..5b0cfdb12b4445 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/Dataflow/TypeNameParser.Dataflow.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/Dataflow/TypeNameParser.Dataflow.cs @@ -2,10 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Reflection; - +using System.Reflection.Metadata; using Internal.TypeSystem; namespace System.Reflection @@ -20,14 +17,20 @@ internal partial struct TypeNameParser public static TypeDesc ResolveType(string name, ModuleDesc callingModule, TypeSystemContext context, List referencedModules, out bool typeWasNotFoundInAssemblyNorBaseLibrary) { - var parser = new TypeNameParser(name) + if (!TypeName.TryParse(name, out TypeName parsed)) + { + typeWasNotFoundInAssemblyNorBaseLibrary = false; + return null; + } + + var parser = new TypeNameParser() { _context = context, _callingModule = callingModule, _referencedModules = referencedModules }; - TypeDesc result = parser.Parse()?.Value; + TypeDesc result = parser.Resolve(parsed)?.Value; typeWasNotFoundInAssemblyNorBaseLibrary = parser._typeWasNotFoundInAssemblyNorBaseLibrary; return result; @@ -52,16 +55,13 @@ public Type MakeGenericType(Type[] typeArguments) } } - private static bool CheckTopLevelAssemblyQualifiedName() => true; - - private Type GetType(string typeName, ReadOnlySpan nestedTypeNames, string assemblyNameIfAny) + private Type GetType(string typeName, ReadOnlySpan nestedTypeNames, TypeName parsedName) { ModuleDesc module; - if (assemblyNameIfAny != null) + if (parsedName.AssemblyName != null) { - module = (TryParseAssemblyName(assemblyNameIfAny) is AssemblyName an) ? - _context.ResolveAssembly(an, throwIfNotFound: false) : null; + module = _context.ResolveAssembly(parsedName.AssemblyName.ToAssemblyName(), throwIfNotFound: false); } else { @@ -79,7 +79,7 @@ private Type GetType(string typeName, ReadOnlySpan nestedTypeNames, stri } // If it didn't resolve and wasn't assembly-qualified, we also try core library - if (assemblyNameIfAny == null) + if (parsedName.AssemblyName == null) { Type type = GetTypeCore(_context.SystemModule, typeName, nestedTypeNames); if (type != null) @@ -94,22 +94,6 @@ private Type GetType(string typeName, ReadOnlySpan nestedTypeNames, stri return null; } - private static AssemblyName TryParseAssemblyName(string assemblyName) - { - try - { - return new AssemblyName(assemblyName); - } - catch (FileLoadException) - { - return null; - } - catch (ArgumentException) - { - return null; - } - } - private static Type GetTypeCore(ModuleDesc module, string typeName, ReadOnlySpan nestedTypeNames) { (string typeNamespace, string name) = SplitFullTypeName(typeName); @@ -127,9 +111,5 @@ private static Type GetTypeCore(ModuleDesc module, string typeName, ReadOnlySpan return new Type(type); } - - private static void ParseError() - { - } } } diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj b/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj index 37c8c2108d9172..a895dbe1726fa3 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj @@ -397,8 +397,8 @@ - - Compiler\Dataflow\TypeNameParser.cs + + Compiler\Dataflow\TypeNameParser.Helpers.cs Utilities\ValueStringBuilder.cs diff --git a/src/coreclr/tools/aot/ILCompiler.TypeSystem/ILCompiler.TypeSystem.csproj b/src/coreclr/tools/aot/ILCompiler.TypeSystem/ILCompiler.TypeSystem.csproj index c46d5fecbfb76c..23afa3780b1abc 100644 --- a/src/coreclr/tools/aot/ILCompiler.TypeSystem/ILCompiler.TypeSystem.csproj +++ b/src/coreclr/tools/aot/ILCompiler.TypeSystem/ILCompiler.TypeSystem.csproj @@ -198,12 +198,39 @@ Utilities\CustomAttributeTypeNameParser.cs - + + Utilities\HexConverter.cs + + + Utilities\AssemblyNameFormatter.cs + + + Utilities\AssemblyNameParser.cs + + + Utilities\AssemblyNameInfo.cs + + + Utilities\TypeName.cs + + Utilities\TypeNameParser.cs + + Utilities\TypeNameParserHelpers.cs + + + Utilities\TypeNameParserOptions.cs + + + Utilities\CustomAttributeTypeNameParser.Helpers + Utilities\ValueStringBuilder.cs + + Utilities\ValueStringBuilder.AppendSpanFormattable.cs + Utilities\GCPointerMap.Algorithm.cs diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/AssemblyNameFormatter.cs b/src/libraries/Common/src/System/Reflection/AssemblyNameFormatter.cs similarity index 92% rename from src/libraries/System.Private.CoreLib/src/System/Reflection/AssemblyNameFormatter.cs rename to src/libraries/Common/src/System/Reflection/AssemblyNameFormatter.cs index c1a810db507557..9d5eeebc85a6d0 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/AssemblyNameFormatter.cs +++ b/src/libraries/Common/src/System/Reflection/AssemblyNameFormatter.cs @@ -6,6 +6,8 @@ using System.Globalization; using System.Text; +#nullable enable + namespace System.Reflection { internal static class AssemblyNameFormatter @@ -91,7 +93,8 @@ private static void AppendQuoted(this ref ValueStringBuilder vsb, string s) // App-compat: You can use double or single quotes to quote a name, and Fusion (or rather the IdentityAuthority) picks one // by some algorithm. Rather than guess at it, we use double quotes consistently. - if (s != s.Trim() || s.Contains('\"') || s.Contains('\'')) + ReadOnlySpan span = s.AsSpan(); + if (s.Length != span.Trim().Length || span.IndexOfAny('\"', '\'') >= 0) needsQuoting = true; if (needsQuoting) @@ -125,5 +128,12 @@ private static void AppendQuoted(this ref ValueStringBuilder vsb, string s) if (needsQuoting) vsb.Append(quoteChar); } + +#if !NETCOREAPP + private static void AppendSpanFormattable(this ref ValueStringBuilder vsb, ushort value) + { + vsb.Append(value.ToString()); + } +#endif } } diff --git a/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs b/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs new file mode 100644 index 00000000000000..bfd91d781a4ecc --- /dev/null +++ b/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs @@ -0,0 +1,506 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; + +#nullable enable + +namespace System.Reflection +{ + /// + /// Parses an assembly name. + /// + internal ref partial struct AssemblyNameParser + { + public readonly struct AssemblyNameParts + { + public AssemblyNameParts(string name, Version? version, string? cultureName, AssemblyNameFlags flags, byte[]? publicKeyOrToken) + { + _name = name; + _version = version; + _cultureName = cultureName; + _flags = flags; + _publicKeyOrToken = publicKeyOrToken; + } + + public readonly string _name; + public readonly Version? _version; + public readonly string? _cultureName; + public readonly AssemblyNameFlags _flags; + public readonly byte[]? _publicKeyOrToken; + } + + /// + /// Token categories for the lexer. + /// + private enum Token + { + Equals = 1, + Comma = 2, + String = 3, + End = 4, + } + + private enum AttributeKind + { + Version = 1, + Culture = 2, + PublicKeyOrToken = 4, + ProcessorArchitecture = 8, + Retargetable = 16, + ContentType = 32 + } + + private readonly ReadOnlySpan _input; + private int _index; + + private AssemblyNameParser(ReadOnlySpan input) + { +#if SYSTEM_PRIVATE_CORELIB + if (input.Length == 0) + throw new ArgumentException(SR.Format_StringZeroLength); +#else + Debug.Assert(input.Length > 0); +#endif + + _input = input; + _index = 0; + } + +#if SYSTEM_PRIVATE_CORELIB + public static AssemblyNameParts Parse(string name) => Parse(name.AsSpan()); + + public static AssemblyNameParts Parse(ReadOnlySpan name) + { + AssemblyNameParser parser = new(name); + AssemblyNameParts result = default; + if (parser.TryParse(ref result)) + { + return result; + } + throw new FileLoadException(SR.InvalidAssemblyName, name.ToString()); + } +#endif + + internal static bool TryParse(ReadOnlySpan name, ref AssemblyNameParts parts) + { + AssemblyNameParser parser = new(name); + return parser.TryParse(ref parts); + } + + private static bool TryRecordNewSeen(scoped ref AttributeKind seenAttributes, AttributeKind newAttribute) + { + if ((seenAttributes & newAttribute) != 0) + { + return false; + } + seenAttributes |= newAttribute; + return true; + } + + private bool TryParse(ref AssemblyNameParts result) + { + // Name must come first. + if (!TryGetNextToken(out string name, out Token token) || token != Token.String || string.IsNullOrEmpty(name)) + return false; + + Version? version = null; + string? cultureName = null; + byte[]? pkt = null; + AssemblyNameFlags flags = 0; + + AttributeKind alreadySeen = default; + if (!TryGetNextToken(out _, out token)) + return false; + + while (token != Token.End) + { + if (token != Token.Comma) + return false; + + if (!TryGetNextToken(out string attributeName, out token) || token != Token.String) + return false; + + if (!TryGetNextToken(out _, out token) || token != Token.Equals) + return false; + + if (!TryGetNextToken(out string attributeValue, out token) || token != Token.String) + return false; + + if (attributeName == string.Empty) + return false; + + if (IsAttribute(attributeName, "Version")) + { + if (!TryRecordNewSeen(ref alreadySeen, AttributeKind.Version)) + { + return false; + } + if (!TryParseVersion(attributeValue, ref version)) + { + return false; + } + } + else if (IsAttribute(attributeName, "Culture")) + { + if (!TryRecordNewSeen(ref alreadySeen, AttributeKind.Culture)) + { + return false; + } + if (!TryParseCulture(attributeValue, out cultureName)) + { + return false; + } + } + else if (IsAttribute(attributeName, "PublicKeyToken")) + { + if (!TryRecordNewSeen(ref alreadySeen, AttributeKind.PublicKeyOrToken)) + { + return false; + } + if (!TryParsePKT(attributeValue, isToken: true, out pkt)) + { + return false; + } + } + else if (IsAttribute(attributeName, "PublicKey")) + { + if (!TryRecordNewSeen(ref alreadySeen, AttributeKind.PublicKeyOrToken)) + { + return false; + } + if (!TryParsePKT(attributeValue, isToken: false, out pkt)) + { + return false; + } + flags |= AssemblyNameFlags.PublicKey; + } + else if (IsAttribute(attributeName, "ProcessorArchitecture")) + { + if (!TryRecordNewSeen(ref alreadySeen, AttributeKind.ProcessorArchitecture)) + { + return false; + } + if (!TryParseProcessorArchitecture(attributeValue, out ProcessorArchitecture arch)) + { + return false; + } + flags |= (AssemblyNameFlags)(((int)arch) << 4); + } + else if (IsAttribute(attributeName, "Retargetable")) + { + if (!TryRecordNewSeen(ref alreadySeen, AttributeKind.Retargetable)) + { + return false; + } + + if (attributeValue.Equals("Yes", StringComparison.OrdinalIgnoreCase)) + { + flags |= AssemblyNameFlags.Retargetable; + } + else if (attributeValue.Equals("No", StringComparison.OrdinalIgnoreCase)) + { + // nothing to do + } + else + { + return false; + } + } + else if (IsAttribute(attributeName, "ContentType")) + { + if (!TryRecordNewSeen(ref alreadySeen, AttributeKind.ContentType)) + { + return false; + } + + if (attributeValue.Equals("WindowsRuntime", StringComparison.OrdinalIgnoreCase)) + { + flags |= (AssemblyNameFlags)(((int)AssemblyContentType.WindowsRuntime) << 9); + } + else + { + return false; + } + } + else + { + // Desktop compat: If we got here, the attribute name is unknown to us. Ignore it. + } + + if (!TryGetNextToken(out _, out token)) + { + return false; + } + } + + result = new AssemblyNameParts(name, version, cultureName, flags, pkt); + return true; + } + + private static bool IsAttribute(string candidate, string attributeKind) + => candidate.Equals(attributeKind, StringComparison.OrdinalIgnoreCase); + + private static bool TryParseVersion(string attributeValue, ref Version? version) + { +#if NET8_0_OR_GREATER + ReadOnlySpan attributeValueSpan = attributeValue; + Span parts = stackalloc Range[5]; + parts = parts.Slice(0, attributeValueSpan.Split(parts, '.')); +#else + string[] parts = attributeValue.Split('.'); +#endif + if (parts.Length is < 2 or > 4) + { + return false; + } + + Span versionNumbers = stackalloc ushort[4] { ushort.MaxValue, ushort.MaxValue, ushort.MaxValue, ushort.MaxValue }; + for (int i = 0; i < parts.Length; i++) + { + if (!ushort.TryParse( +#if NET8_0_OR_GREATER + attributeValueSpan[parts[i]], +#else + parts[i], +#endif + NumberStyles.None, NumberFormatInfo.InvariantInfo, out versionNumbers[i])) + { + return false; + } + } + + if (versionNumbers[0] == ushort.MaxValue || + versionNumbers[1] == ushort.MaxValue) + { + return false; + } + + version = + versionNumbers[2] == ushort.MaxValue ? new Version(versionNumbers[0], versionNumbers[1]) : + versionNumbers[3] == ushort.MaxValue ? new Version(versionNumbers[0], versionNumbers[1], versionNumbers[2]) : + new Version(versionNumbers[0], versionNumbers[1], versionNumbers[2], versionNumbers[3]); + + return true; + } + + private static bool TryParseCulture(string attributeValue, out string? result) + { + if (attributeValue.Equals("Neutral", StringComparison.OrdinalIgnoreCase)) + { + result = ""; + return true; + } + + result = attributeValue; + return true; + } + + private static bool TryParsePKT(string attributeValue, bool isToken, out byte[]? result) + { + if (attributeValue.Equals("null", StringComparison.OrdinalIgnoreCase) || attributeValue == string.Empty) + { + result = Array.Empty(); + return true; + } + + if (attributeValue.Length % 2 != 0 || (isToken && attributeValue.Length != 8 * 2)) + { + result = null; + return false; + } + + byte[] pkt = new byte[attributeValue.Length / 2]; + if (!HexConverter.TryDecodeFromUtf16(attributeValue.AsSpan(), pkt, out int _)) + { + result = null; + return false; + } + + result = pkt; + return true; + } + + private static bool TryParseProcessorArchitecture(string attributeValue, out ProcessorArchitecture result) + { + result = attributeValue switch + { + _ when attributeValue.Equals("msil", StringComparison.OrdinalIgnoreCase) => ProcessorArchitecture.MSIL, + _ when attributeValue.Equals("x86", StringComparison.OrdinalIgnoreCase) => ProcessorArchitecture.X86, + _ when attributeValue.Equals("ia64", StringComparison.OrdinalIgnoreCase) => ProcessorArchitecture.IA64, + _ when attributeValue.Equals("amd64", StringComparison.OrdinalIgnoreCase) => ProcessorArchitecture.Amd64, + _ when attributeValue.Equals("arm", StringComparison.OrdinalIgnoreCase) => ProcessorArchitecture.Arm, + _ when attributeValue.Equals("msil", StringComparison.OrdinalIgnoreCase) => ProcessorArchitecture.MSIL, + _ => ProcessorArchitecture.None + }; + return result != ProcessorArchitecture.None; + } + + private static bool IsWhiteSpace(char ch) + => ch is '\n' or '\r' or ' ' or '\t'; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool TryGetNextChar(out char ch) + { + if (_index < _input.Length) + { + ch = _input[_index++]; + if (ch == '\0') + { + return false; + } + } + else + { + ch = '\0'; + } + + return true; + } + + // + // Return the next token in assembly name. If the result is Token.String, + // sets "tokenString" to the tokenized string. + // + private bool TryGetNextToken(out string tokenString, out Token token) + { + tokenString = string.Empty; + char c; + + while (true) + { + if (!TryGetNextChar(out c)) + { + token = default; + return false; + } + + switch (c) + { + case ',': + { + token = Token.Comma; + return true; + } + case '=': + { + token = Token.Equals; + return true; + } + case '\0': + { + token = Token.End; + return true; + } + } + + if (!IsWhiteSpace(c)) + { + break; + } + } + + using ValueStringBuilder sb = new ValueStringBuilder(stackalloc char[64]); + + char quoteChar = '\0'; + if (c is '\'' or '\"') + { + quoteChar = c; + if (!TryGetNextChar(out c)) + { + token = default; + return false; + } + } + + for (; ; ) + { + if (c == 0) + { + if (quoteChar != 0) + { + // EOS and unclosed quotes is an error + token = default; + return false; + } + // Reached end of input and therefore of string + break; + } + + if (quoteChar != 0 && c == quoteChar) + break; // Terminate: Found closing quote of quoted string. + + if (quoteChar == 0 && (c is ',' or '=')) + { + _index--; + break; // Terminate: Found start of a new ',' or '=' token. + } + + if (quoteChar == 0 && (c is '\'' or '\"')) + { + token = default; + return false; + } + + if (c is '\\') + { + if (!TryGetNextChar(out c)) + { + token = default; + return false; + } + + switch (c) + { + case '\\': + case ',': + case '=': + case '\'': + case '"': + sb.Append(c); + break; + case 't': + sb.Append('\t'); + break; + case 'r': + sb.Append('\r'); + break; + case 'n': + sb.Append('\n'); + break; + default: + token = default; + return false; + } + } + else + { + sb.Append(c); + } + + if (!TryGetNextChar(out c)) + { + token = default; + return false; + } + } + + + int length = sb.Length; + if (quoteChar == 0) + { + while (length > 0 && IsWhiteSpace(sb[length - 1])) + length--; + } + + tokenString = sb.AsSpan(0, length).ToString(); + token = Token.String; + return true; + } + } +} diff --git a/src/libraries/Common/src/System/Reflection/Metadata/AssemblyNameInfo.cs b/src/libraries/Common/src/System/Reflection/Metadata/AssemblyNameInfo.cs new file mode 100644 index 00000000000000..ee586e3b57a8c0 --- /dev/null +++ b/src/libraries/Common/src/System/Reflection/Metadata/AssemblyNameInfo.cs @@ -0,0 +1,219 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Text; + +#if !SYSTEM_PRIVATE_CORELIB +using System.Collections.Immutable; +using System.Linq; +#endif + +namespace System.Reflection.Metadata +{ + /// + /// Describes an assembly. + /// + /// + /// It's a more lightweight, immutable version of that does not pre-allocate instances. + /// + [DebuggerDisplay("{FullName}")] +#if SYSTEM_PRIVATE_CORELIB + internal +#else + public +#endif + sealed class AssemblyNameInfo + { + internal readonly AssemblyNameFlags _flags; + private string? _fullName; + +#if !SYSTEM_PRIVATE_CORELIB + /// + /// Initializes a new instance of the AssemblyNameInfo class. + /// + /// The simple name of the assembly. + /// The version of the assembly. + /// The name of the culture associated with the assembly. + /// The attributes of the assembly. + /// The public key or its token. Set to when it's public key. + /// is null. + public AssemblyNameInfo(string name, Version? version = null, string? cultureName = null, AssemblyNameFlags flags = AssemblyNameFlags.None, ImmutableArray publicKeyOrToken = default) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + Version = version; + CultureName = cultureName; + _flags = flags; + PublicKeyOrToken = publicKeyOrToken; + } +#endif + + internal AssemblyNameInfo(AssemblyNameParser.AssemblyNameParts parts) + { + Name = parts._name; + Version = parts._version; + CultureName = parts._cultureName; + _flags = parts._flags; +#if SYSTEM_PRIVATE_CORELIB + PublicKeyOrToken = parts._publicKeyOrToken; +#else + PublicKeyOrToken = parts._publicKeyOrToken is null ? default : parts._publicKeyOrToken.Length == 0 + ? ImmutableArray.Empty + #if NET8_0_OR_GREATER + : Runtime.InteropServices.ImmutableCollectionsMarshal.AsImmutableArray(parts._publicKeyOrToken); + #else + : ImmutableArray.Create(parts._publicKeyOrToken); + #endif +#endif + } + + /// + /// Gets the simple name of the assembly. + /// + public string Name { get; } + + /// + /// Gets the version of the assembly. + /// + public Version? Version { get; } + + /// + /// Gets the name of the culture associated with the assembly. + /// + public string? CultureName { get; } + + /// + /// Gets the attributes of the assembly. + /// + public AssemblyNameFlags Flags => _flags; + + /// + /// Gets the public key or the public key token of the assembly. + /// + /// Check for flag to see whether it's public key or its token. +#if SYSTEM_PRIVATE_CORELIB + public byte[]? PublicKeyOrToken { get; } +#else + public ImmutableArray PublicKeyOrToken { get; } +#endif + + /// + /// Gets the full name of the assembly, also known as the display name. + /// + /// In contrary to it does not validate public key token neither computes it based on the provided public key. + public string FullName + { + get + { + if (_fullName is null) + { + byte[]? publicKeyToken = ((Flags & AssemblyNameFlags.PublicKey) != 0) ? null : +#if SYSTEM_PRIVATE_CORELIB + PublicKeyOrToken; +#elif NET8_0_OR_GREATER + !PublicKeyOrToken.IsDefault ? Runtime.InteropServices.ImmutableCollectionsMarshal.AsArray(PublicKeyOrToken) : null; +#else + !PublicKeyOrToken.IsDefault ? PublicKeyOrToken.ToArray() : null; +#endif + _fullName = AssemblyNameFormatter.ComputeDisplayName(Name, Version, CultureName, publicKeyToken, + ExtractAssemblyNameFlags(_flags), ExtractAssemblyContentType(_flags)); + } + + return _fullName; + } + } + + /// + /// Initializes a new instance of the class based on the stored information. + /// + public AssemblyName ToAssemblyName() + { + AssemblyName assemblyName = new(); + assemblyName.Name = Name; + assemblyName.CultureName = CultureName; + assemblyName.Version = Version; + assemblyName.Flags = Flags; + assemblyName.ContentType = ExtractAssemblyContentType(_flags); +#pragma warning disable SYSLIB0037 // Type or member is obsolete + assemblyName.ProcessorArchitecture = ExtractProcessorArchitecture(_flags); +#pragma warning restore SYSLIB0037 // Type or member is obsolete + +#if SYSTEM_PRIVATE_CORELIB + if (PublicKeyOrToken is not null) + { + if ((Flags & AssemblyNameFlags.PublicKey) != 0) + { + assemblyName.SetPublicKey(PublicKeyOrToken); + } + else + { + assemblyName.SetPublicKeyToken(PublicKeyOrToken); + } + } +#else + if (!PublicKeyOrToken.IsDefault) + { +#pragma warning disable RS0030 // TypeSystem does not allow System.Linq, but we need to use System.Linq.ImmutableArrayExtensions.ToArray + // A copy of the array needs to be created, as AssemblyName allows for the mutation of provided array. + if ((Flags & AssemblyNameFlags.PublicKey) != 0) + { + assemblyName.SetPublicKey(PublicKeyOrToken.ToArray()); + } + else + { + assemblyName.SetPublicKeyToken(PublicKeyOrToken.ToArray()); + } +#pragma warning restore RS0030 + } +#endif + + return assemblyName; + } + + /// + /// Parses a span of characters into a assembly name. + /// + /// A span containing the characters representing the assembly name to parse. + /// Parsed type name. + /// Provided assembly name was invalid. + public static AssemblyNameInfo Parse(ReadOnlySpan assemblyName) + => TryParse(assemblyName, out AssemblyNameInfo? result) + ? result! +#if SYSTEM_REFLECTION_METADATA || SYSTEM_PRIVATE_CORELIB + : throw new ArgumentException(SR.InvalidAssemblyName, nameof(assemblyName)); +#else // tools that reference this file as a link + : throw new ArgumentException("The given assembly name was invalid.", nameof(assemblyName)); +#endif + + /// + /// Tries to parse a span of characters into an assembly name. + /// + /// A span containing the characters representing the assembly name to parse. + /// Contains the result when parsing succeeds. + /// true if assembly name was converted successfully, otherwise, false. + public static bool TryParse(ReadOnlySpan assemblyName, [NotNullWhen(true)] out AssemblyNameInfo? result) + { + AssemblyNameParser.AssemblyNameParts parts = default; + if (AssemblyNameParser.TryParse(assemblyName, ref parts)) + { + result = new(parts); + return true; + } + + result = null; + return false; + } + + internal static AssemblyNameFlags ExtractAssemblyNameFlags(AssemblyNameFlags combinedFlags) + => combinedFlags & unchecked((AssemblyNameFlags)0xFFFFF10F); + + internal static AssemblyContentType ExtractAssemblyContentType(AssemblyNameFlags flags) + => (AssemblyContentType)((((int)flags) >> 9) & 0x7); + + internal static ProcessorArchitecture ExtractProcessorArchitecture(AssemblyNameFlags flags) + => (ProcessorArchitecture)((((int)flags) >> 4) & 0x7); + } +} diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs new file mode 100644 index 00000000000000..2fac2f8ffd74db --- /dev/null +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs @@ -0,0 +1,398 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using System.Text; + +#if !SYSTEM_PRIVATE_CORELIB +using System.Collections.Immutable; +#endif + +namespace System.Reflection.Metadata +{ + [DebuggerDisplay("{AssemblyQualifiedName}")] +#if SYSTEM_PRIVATE_CORELIB + internal +#else + public +#endif + sealed class TypeName + { + /// + /// Positive value is array rank. + /// Negative value is modifier encoded using constants defined in . + /// + private readonly sbyte _rankOrModifier; + /// + /// To avoid the need of allocating a string for all declaring types (example: A+B+C+D+E+F+G), + /// length of the name is stored and the fullName passed in ctor represents the full name of the nested type. + /// So when the name is needed, a substring is being performed. + /// + private readonly int _nestedNameLength; + private readonly TypeName? _elementOrGenericType; + private readonly TypeName? _declaringType; +#if SYSTEM_PRIVATE_CORELIB + private readonly List? _genericArguments; +#else + private readonly ImmutableArray _genericArguments; +#endif + private string? _name, _fullName, _assemblyQualifiedName; + + internal TypeName(string? fullName, + AssemblyNameInfo? assemblyName, + TypeName? elementOrGenericType = default, + TypeName? declaringType = default, +#if SYSTEM_PRIVATE_CORELIB + List? genericTypeArguments = default, +#else + ImmutableArray.Builder? genericTypeArguments = default, +#endif + sbyte rankOrModifier = default, + int nestedNameLength = -1) + { + _fullName = fullName; + AssemblyName = assemblyName; + _rankOrModifier = rankOrModifier; + _elementOrGenericType = elementOrGenericType; + _declaringType = declaringType; + _nestedNameLength = nestedNameLength; + +#if SYSTEM_PRIVATE_CORELIB + _genericArguments = genericTypeArguments; +#else + _genericArguments = genericTypeArguments is null + ? ImmutableArray.Empty + : genericTypeArguments.Count == genericTypeArguments.Capacity ? genericTypeArguments.MoveToImmutable() : genericTypeArguments.ToImmutableArray(); +#endif + } + + /// + /// The assembly-qualified name of the type; e.g., "System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089". + /// + /// + /// If returns null, simply returns . + /// + public string AssemblyQualifiedName + => _assemblyQualifiedName ??= AssemblyName is null ? FullName : $"{FullName}, {AssemblyName.FullName}"; + + /// + /// Returns assembly name which contains this type, or null if this was not + /// created from a fully-qualified name. + /// + public AssemblyNameInfo? AssemblyName { get; } + + /// + /// If this type is a nested type (see ), gets + /// the declaring type. If this type is not a nested type, throws. + /// + /// + /// For example, given "Namespace.Declaring+Nested", unwraps the outermost type and returns "Namespace.Declaring". + /// + /// The current type is not a nested type. + public TypeName DeclaringType + { + get + { + if (_declaringType is null) + { + TypeNameParserHelpers.ThrowInvalidOperation_NotNestedType(); + } + + return _declaringType; + } + } + + /// + /// The full name of this type, including namespace, but without the assembly name; e.g., "System.Int32". + /// Nested types are represented with a '+'; e.g., "MyNamespace.MyType+NestedType". + /// + /// + /// For constructed generic types, the type arguments will be listed using their fully qualified + /// names. For example, given "List<int>", the property will return + /// "System.Collections.Generic.List`1[[System.Int32, mscorlib, ...]]". + /// For open generic types, the convention is to use a backtick ("`") followed by + /// the arity of the generic type. For example, given "Dictionary<,>", the + /// property will return "System.Collections.Generic.Dictionary`2". Given "Dictionary<,>.Enumerator", + /// the property will return "System.Collections.Generic.Dictionary`2+Enumerator". + /// See ECMA-335, Sec. I.10.7.2 (Type names and arity encoding) for more information. + /// + public string FullName + { + get + { + if (_fullName is null) + { + if (IsConstructedGenericType) + { + _fullName = TypeNameParserHelpers.GetGenericTypeFullName(GetGenericTypeDefinition().FullName.AsSpan(), +#if SYSTEM_PRIVATE_CORELIB + CollectionsMarshal.AsSpan(_genericArguments)); +#else + _genericArguments.AsSpan()); +#endif + } + else if (IsArray || IsPointer || IsByRef) + { + ValueStringBuilder builder = new(stackalloc char[128]); + builder.Append(GetElementType().FullName); + _fullName = TypeNameParserHelpers.GetRankOrModifierStringRepresentation(_rankOrModifier, ref builder); + } + else + { + Debug.Fail("Pre-allocated full name should have been provided in the ctor"); + } + } + else if (_nestedNameLength > 0 && _fullName.Length > _nestedNameLength) // Declaring types + { + // Stored fullName represents the full name of the nested type. + // Example: Namespace.Declaring+Nested + _fullName = _fullName.Substring(0, _nestedNameLength); + } + + return _fullName!; + } + } + + /// + /// Returns true if this type represents any kind of array, regardless of the array's + /// rank or its bounds. + /// + public bool IsArray => _rankOrModifier == TypeNameParserHelpers.SZArray || _rankOrModifier > 0; + + /// + /// Returns true if this type represents a constructed generic type (e.g., "List<int>"). + /// + /// + /// Returns false for open generic types (e.g., "Dictionary<,>"). + /// + public bool IsConstructedGenericType => +#if SYSTEM_PRIVATE_CORELIB + _genericArguments is not null; +#else + _genericArguments.Length > 0; +#endif + + /// + /// Returns true if this is a "plain" type; that is, not an array, not a pointer, not a reference, and + /// not a constructed generic type. Examples of elemental types are "System.Int32", + /// "System.Uri", and "YourNamespace.YourClass". + /// + /// + /// This property returning true doesn't mean that the type is a primitive like string + /// or int; it just means that there's no underlying type. + /// This property will return true for generic type definitions (e.g., "Dictionary<,>"). + /// This is because determining whether a type truly is a generic type requires loading the type + /// and performing a runtime check. + /// + public bool IsSimple => _elementOrGenericType is null; + + /// + /// Returns true if this is a managed pointer type (e.g., "ref int"). + /// Managed pointer types are sometimes called byref types () + /// + public bool IsByRef => _rankOrModifier == TypeNameParserHelpers.ByRef; + + /// + /// Returns true if this is a nested type (e.g., "Namespace.Declaring+Nested"). + /// For nested types returns their declaring type. + /// + public bool IsNested => _declaringType is not null; + + /// + /// Returns true if this type represents a single-dimensional, zero-indexed array (e.g., "int[]"). + /// + public bool IsSZArray => _rankOrModifier == TypeNameParserHelpers.SZArray; + + /// + /// Returns true if this type represents an unmanaged pointer (e.g., "int*" or "void*"). + /// Unmanaged pointer types are often just called pointers () + /// + public bool IsPointer => _rankOrModifier == TypeNameParserHelpers.Pointer; + + /// + /// Returns true if this type represents a variable-bound array; that is, an array of rank greater + /// than 1 (e.g., "int[,]") or a single-dimensional array which isn't necessarily zero-indexed. + /// + public bool IsVariableBoundArrayType => _rankOrModifier >= 1; + + /// + /// The name of this type, without the namespace and the assembly name; e.g., "Int32". + /// Nested types are represented without a '+'; e.g., "MyNamespace.MyType+NestedType" is just "NestedType". + /// + public string Name + { + get + { + if (_name is null) + { + if (IsConstructedGenericType) + { + _name = TypeNameParserHelpers.GetName(GetGenericTypeDefinition().FullName.AsSpan()).ToString(); + } + else if (IsPointer || IsByRef || IsArray) + { + ValueStringBuilder builder = new(stackalloc char[64]); + builder.Append(GetElementType().Name); + _name = TypeNameParserHelpers.GetRankOrModifierStringRepresentation(_rankOrModifier, ref builder); + } + else if (_nestedNameLength > 0 && _fullName is not null) + { + _name = TypeNameParserHelpers.GetName(_fullName.AsSpan(0, _nestedNameLength)).ToString(); + } + else + { + _name = TypeNameParserHelpers.GetName(FullName.AsSpan()).ToString(); + } + } + + return _name; + } + } + + /// + /// Represents the total number of instances that are used to describe + /// this instance, including any generic arguments or underlying types. + /// + /// + /// This value is computed every time this method gets called, it's not cached. + /// There's not really a parallel concept to this in reflection. Think of it + /// as the total number of instances that would be created if + /// you were to totally deconstruct this instance and visit each intermediate + /// that occurs as part of deconstruction. + /// "int" and "Person" each have complexities of 1 because they're standalone types. + /// "int[]" has a node count of 2 because to fully inspect it involves inspecting the + /// array type itself, plus unwrapping the underlying type ("int") and inspecting that. + /// + /// "Dictionary<string, List<int[][]>>" has node count 8 because fully visiting it + /// involves inspecting 8 instances total: + /// + /// Dictionary<string, List<int[][]>> (the original type) + /// Dictionary`2 (the generic type definition) + /// string (a type argument of Dictionary) + /// List<int[][]> (a type argument of Dictionary) + /// List`1 (the generic type definition) + /// int[][] (a type argument of List) + /// int[] (the underlying type of int[][]) + /// int (the underlying type of int[]) + /// + /// + /// + public int GetNodeCount() + { + int result = 1; + + if (IsNested) + { + result += DeclaringType.GetNodeCount(); + } + else if (IsConstructedGenericType) + { + result++; + } + else if (IsArray || IsPointer || IsByRef) + { + result += GetElementType().GetNodeCount(); + } + + foreach (TypeName genericArgument in GetGenericArguments()) + { + result += genericArgument.GetNodeCount(); + } + + return result; + } + + /// + /// The TypeName of the object encompassed or referred to by the current array, pointer, or reference type. + /// + /// + /// For example, given "int[][]", unwraps the outermost array and returns "int[]". + /// + /// The current type is not an array, pointer or reference. + public TypeName GetElementType() + { + if (!(IsArray || IsPointer || IsByRef)) + { + TypeNameParserHelpers.ThrowInvalidOperation_NoElement(); + } + + return _elementOrGenericType!; + } + + /// + /// Returns a TypeName object that represents a generic type name definition from which the current generic type name can be constructed. + /// + /// + /// Given "Dictionary<string, int>", returns the generic type definition "Dictionary<,>". + /// + /// The current type is not a generic type. + public TypeName GetGenericTypeDefinition() + { + if (!IsConstructedGenericType) + { + TypeNameParserHelpers.ThrowInvalidOperation_NotGenericType(); + } + + return _elementOrGenericType!; + } + + /// + /// Parses a span of characters into a type name. + /// + /// A span containing the characters representing the type name to parse. + /// An object that describes optional parameters to use. + /// Parsed type name. + /// Provided type name was invalid. + /// Parsing has exceeded the limit set by . + public static TypeName Parse(ReadOnlySpan typeName, TypeNameParseOptions? options = default) + => TypeNameParser.Parse(typeName, throwOnError: true, options)!; + + /// + /// Tries to parse a span of characters into a type name. + /// + /// A span containing the characters representing the type name to parse. + /// An object that describes optional parameters to use. + /// Contains the result when parsing succeeds. + /// true if type name was converted successfully, otherwise, false. + public static bool TryParse(ReadOnlySpan typeName, [NotNullWhen(true)] out TypeName? result, TypeNameParseOptions? options = default) + { + result = TypeNameParser.Parse(typeName, throwOnError: false, options); + return result is not null; + } + + /// + /// Gets the number of dimensions in an array. + /// + /// An integer that contains the number of dimensions in the current type. + /// The current type is not an array. + public int GetArrayRank() + { + if (!(_rankOrModifier == TypeNameParserHelpers.SZArray || _rankOrModifier > 0)) + { + TypeNameParserHelpers.ThrowInvalidOperation_HasToBeArrayClass(); + } + + return _rankOrModifier == TypeNameParserHelpers.SZArray ? 1 : _rankOrModifier; + } + + /// + /// If this represents a constructed generic type, returns an array + /// of all the generic arguments. Otherwise it returns an empty array. + /// + /// + /// For example, given "Dictionary<string, int>", returns a 2-element array containing + /// string and int. + /// + public +#if SYSTEM_PRIVATE_CORELIB + IReadOnlyList GetGenericArguments() => _genericArguments is null ? Array.Empty() : _genericArguments; +#else + ImmutableArray GetGenericArguments() => _genericArguments; +#endif + } +} diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs new file mode 100644 index 00000000000000..7c75f3ee844d6d --- /dev/null +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs @@ -0,0 +1,260 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; + +#if !SYSTEM_PRIVATE_CORELIB +using System.Collections.Immutable; +#endif + +using static System.Reflection.Metadata.TypeNameParserHelpers; + +#nullable enable + +namespace System.Reflection.Metadata +{ + [DebuggerDisplay("{_inputString}")] + internal ref struct TypeNameParser + { + private static readonly TypeNameParseOptions _defaults = new(); + private readonly bool _throwOnError; + private readonly TypeNameParseOptions _parseOptions; + private ReadOnlySpan _inputString; + + private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParseOptions? options) : this() + { + _inputString = name; + _throwOnError = throwOnError; + _parseOptions = options ?? _defaults; + } + + internal static TypeName? Parse(ReadOnlySpan typeName, bool throwOnError, TypeNameParseOptions? options = default) + { + ReadOnlySpan trimmedName = typeName.TrimStart(); // whitespaces at beginning are always OK + if (trimmedName.IsEmpty) + { + if (throwOnError) + { + ThrowArgumentException_InvalidTypeName(errorIndex: 0); // whitespace input needs to report the error index as 0 + } + + return null; + } + + int recursiveDepth = 0; + TypeNameParser parser = new(trimmedName, throwOnError, options); + TypeName? parsedName = parser.ParseNextTypeName(allowFullyQualifiedName: true, ref recursiveDepth); + + if (parsedName is null || !parser._inputString.IsEmpty) // unconsumed input == error + { + if (throwOnError) + { + if (recursiveDepth >= parser._parseOptions.MaxNodes) + { + ThrowInvalidOperation_MaxNodesExceeded(parser._parseOptions.MaxNodes); + } + + int errorIndex = typeName.Length - parser._inputString.Length; + ThrowArgumentException_InvalidTypeName(errorIndex); + } + + return null; + } + + return parsedName; + } + + // this method should return null instead of throwing, so the caller can get errorIndex and include it in error msg + private TypeName? ParseNextTypeName(bool allowFullyQualifiedName, ref int recursiveDepth) + { + if (!TryDive(ref recursiveDepth)) + { + return null; + } + + List? nestedNameLengths = null; + if (!TryGetTypeNameInfo(ref _inputString, ref nestedNameLengths, out int fullTypeNameLength)) + { + return null; + } + + ReadOnlySpan fullTypeName = _inputString.Slice(0, fullTypeNameLength); + _inputString = _inputString.Slice(fullTypeNameLength); + + // Don't allocate now, as it may be an open generic type like "Name`1" +#if SYSTEM_PRIVATE_CORELIB + List? genericArgs = null; +#else + ImmutableArray.Builder? genericArgs = null; +#endif + + // Are there any captured generic args? We'll look for "[[" and "[". + // There are no spaces allowed before the first '[', but spaces are allowed + // after that. The check slices _inputString, so we'll capture it into + // a local so we can restore it later if needed. + ReadOnlySpan capturedBeforeProcessing = _inputString; + if (IsBeginningOfGenericArgs(ref _inputString, out bool doubleBrackets)) + { + ParseAnotherGenericArg: + + // Namespace.Type`2[[GenericArgument1, AssemblyName1],[GenericArgument2, AssemblyName2]] - double square bracket syntax allows for fully qualified type names + // Namespace.Type`2[GenericArgument1,GenericArgument2] - single square bracket syntax is legal only for non-fully qualified type names + // Namespace.Type`2[[GenericArgument1, AssemblyName1], GenericArgument2] - mixed mode + // Namespace.Type`2[GenericArgument1, [GenericArgument2, AssemblyName2]] - mixed mode + TypeName? genericArg = ParseNextTypeName(allowFullyQualifiedName: doubleBrackets, ref recursiveDepth); + if (genericArg is null) // parsing failed + { + return null; + } + + // For [[, there had better be a ']' after the type name. + if (doubleBrackets && !TryStripFirstCharAndTrailingSpaces(ref _inputString, ']')) + { + return null; + } + + if (genericArgs is null) + { +#if SYSTEM_PRIVATE_CORELIB + genericArgs = new List(2); +#else + genericArgs = ImmutableArray.CreateBuilder(2); +#endif + } + genericArgs.Add(genericArg); + + // Is there a ',[' indicating fully qualified generic type arg? + // Is there a ',' indicating non-fully qualified generic type arg? + if (TryStripFirstCharAndTrailingSpaces(ref _inputString, ',')) + { + doubleBrackets = TryStripFirstCharAndTrailingSpaces(ref _inputString, '['); + + goto ParseAnotherGenericArg; + } + + // The only other allowable character is ']', indicating the end of + // the generic type arg list. + if (!TryStripFirstCharAndTrailingSpaces(ref _inputString, ']')) + { + return null; + } + } + + // If there was an error stripping the generic args, back up to + // before we started processing them, and let the decorator + // parser try handling it. + if (genericArgs is null) + { + _inputString = capturedBeforeProcessing; + } + + int previousDecorator = default; + // capture the current state so we can reprocess it again once we know the AssemblyName + capturedBeforeProcessing = _inputString; + // iterate over the decorators to ensure there are no illegal combinations + while (TryParseNextDecorator(ref _inputString, out int parsedDecorator)) + { + if (!TryDive(ref recursiveDepth)) + { + return null; + } + + // Currently it's illegal for managed reference to be followed by any other decorator, + // but this is a runtime-specific behavior and the parser is not enforcing that rule. + previousDecorator = parsedDecorator; + } + + AssemblyNameInfo? assemblyName = null; + if (allowFullyQualifiedName && !TryParseAssemblyName(ref assemblyName)) + { +#if SYSTEM_PRIVATE_CORELIB + // backward compat: throw FileLoadException for non-empty invalid strings + if (_throwOnError || !_inputString.TrimStart().StartsWith(",")) + { + throw new IO.FileLoadException(SR.InvalidAssemblyName, _inputString.ToString()); + } +#else + return null; +#endif + } + + // No matter what was parsed, the full name string is allocated only once. + // In case of generic, nested, array, pointer and byref types the full name is allocated + // when needed for the first time . + string fullName = fullTypeName.ToString(); + + TypeName? declaringType = GetDeclaringType(fullName, nestedNameLengths, assemblyName); + TypeName result = new(fullName, assemblyName, declaringType: declaringType); + if (genericArgs is not null) + { + result = new(fullName: null, assemblyName, elementOrGenericType: result, declaringType, genericArgs); + } + + if (previousDecorator != default) // some decorators were recognized + { + while (TryParseNextDecorator(ref capturedBeforeProcessing, out int parsedModifier)) + { + result = new(fullName: null, assemblyName, elementOrGenericType: result, rankOrModifier: (sbyte)parsedModifier); + } + } + + return result; + } + + /// false means the input was invalid and parsing has failed. Empty input is valid and returns true. + private bool TryParseAssemblyName(ref AssemblyNameInfo? assemblyName) + { + ReadOnlySpan capturedBeforeProcessing = _inputString; + if (TryStripFirstCharAndTrailingSpaces(ref _inputString, ',')) + { + if (_inputString.IsEmpty) + { + _inputString = capturedBeforeProcessing; // restore the state + return false; + } + + ReadOnlySpan candidate = GetAssemblyNameCandidate(_inputString); + if (!AssemblyNameInfo.TryParse(candidate, out assemblyName)) + { + return false; + } + + _inputString = _inputString.Slice(candidate.Length); + return true; + } + + return true; + } + + private static TypeName? GetDeclaringType(string fullTypeName, List? nestedNameLengths, AssemblyNameInfo? assemblyName) + { + if (nestedNameLengths is null) + { + return null; + } + + TypeName? declaringType = null; + int nameOffset = 0; + foreach (int nestedNameLength in nestedNameLengths) + { + Debug.Assert(nestedNameLength > 0, "TryGetTypeNameInfo should return error on zero lengths"); + int fullNameLength = nameOffset + nestedNameLength; + declaringType = new(fullTypeName, assemblyName, declaringType: declaringType, nestedNameLength: fullNameLength); + nameOffset += nestedNameLength + 1; // include the '+' that was skipped in name + } + + return declaringType; + } + + private bool TryDive(ref int depth) + { + if (depth >= _parseOptions.MaxNodes) + { + return false; + } + depth++; + return true; + } + } +} diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs new file mode 100644 index 00000000000000..3783f56c73d4b7 --- /dev/null +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs @@ -0,0 +1,383 @@ +// 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.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Text; + +#nullable enable + +namespace System.Reflection.Metadata +{ + internal static class TypeNameParserHelpers + { + internal const sbyte SZArray = -1; + internal const sbyte Pointer = -2; + internal const sbyte ByRef = -3; + private const char EscapeCharacter = '\\'; +#if NET8_0_OR_GREATER + private static readonly SearchValues s_endOfFullTypeNameDelimitersSearchValues = SearchValues.Create("[]&*,+\\"); +#endif + + internal static string GetGenericTypeFullName(ReadOnlySpan fullTypeName, ReadOnlySpan genericArgs) + { + Debug.Assert(genericArgs.Length > 0); + + ValueStringBuilder result = new(stackalloc char[128]); + result.Append(fullTypeName); + + result.Append('['); + foreach (TypeName genericArg in genericArgs) + { + result.Append('['); + result.Append(genericArg.AssemblyQualifiedName); + result.Append(']'); + result.Append(','); + } + result[result.Length - 1] = ']'; // replace ',' with ']' + + return result.ToString(); + } + + /// Positive length or negative value for invalid name + internal static int GetFullTypeNameLength(ReadOnlySpan input, out bool isNestedType) + { + isNestedType = false; + + // NET 6+ guarantees that MemoryExtensions.IndexOfAny has worst-case complexity + // O(m * i) if a match is found, or O(m * n) if a match is not found, where: + // i := index of match position + // m := number of needles + // n := length of search space (haystack) + // + // Downlevel versions of .NET do not make this guarantee, instead having a + // worst-case complexity of O(m * n) even if a match occurs at the beginning of + // the search space. Since we're running this in a loop over untrusted user + // input, that makes the total loop complexity potentially O(m * n^2), where + // 'n' is adversary-controlled. To avoid DoS issues here, we'll loop manually. + +#if NET8_0_OR_GREATER + int offset = input.IndexOfAny(s_endOfFullTypeNameDelimitersSearchValues); + if (offset < 0) + { + return input.Length; // no type name end chars were found, the whole input is the type name + } + + if (input[offset] == EscapeCharacter) // this is very rare (IL Emit or pure IL) + { + offset = GetUnescapedOffset(input, startOffset: offset); // this is slower, but very rare so acceptable + } +#else + int offset = GetUnescapedOffset(input, startOffset: 0); +#endif + isNestedType = offset > 0 && offset < input.Length && input[offset] == '+'; + return offset; + + static int GetUnescapedOffset(ReadOnlySpan input, int startOffset) + { + int offset = startOffset; + for (; offset < input.Length; offset++) + { + char c = input[offset]; + if (c == EscapeCharacter) + { + offset++; // skip the escaped char + + if (offset == input.Length || // invalid name that ends with escape character + !NeedsEscaping(input[offset])) // invalid name, escapes a char that does not need escaping + { + return -1; + } + } + else if (NeedsEscaping(c)) + { + break; + } + } + return offset; + } + + static bool NeedsEscaping(char c) => c is '[' or ']' or '&' or '*' or ',' or '+' or EscapeCharacter; + } + + internal static ReadOnlySpan GetName(ReadOnlySpan fullName) + { + int offset = fullName.LastIndexOfAny('.', '+'); + + if (offset > 0 && fullName[offset - 1] == EscapeCharacter) // this should be very rare (IL Emit & pure IL) + { + offset = GetUnescapedOffset(fullName, startIndex: offset); + } + + return offset < 0 ? fullName : fullName.Slice(offset + 1); + + static int GetUnescapedOffset(ReadOnlySpan fullName, int startIndex) + { + int offset = startIndex; + for (; offset >= 0; offset--) + { + if (fullName[offset] is '.' or '+') + { + if (offset == 0 || fullName[offset - 1] != EscapeCharacter) + { + break; + } + offset--; // skip the escaping character + } + } + return offset; + } + } + + // this method handles escaping of the ] just to let the AssemblyNameParser fail for the right input + internal static ReadOnlySpan GetAssemblyNameCandidate(ReadOnlySpan input) + { + // The only delimiter which can terminate an assembly name is ']'. + // Otherwise EOL serves as the terminator. + int offset = input.IndexOf(']'); + + if (offset > 0 && input[offset - 1] == EscapeCharacter) // this should be very rare (IL Emit & pure IL) + { + offset = GetUnescapedOffset(input, startIndex: offset); + } + + return offset < 0 ? input : input.Slice(0, offset); + + static int GetUnescapedOffset(ReadOnlySpan input, int startIndex) + { + int offset = startIndex; + for (; offset < input.Length; offset++) + { + if (input[offset] is ']') + { + if (input[offset - 1] != EscapeCharacter) + { + break; + } + } + } + return offset; + } + } + + internal static string GetRankOrModifierStringRepresentation(int rankOrModifier, ref ValueStringBuilder builder) + { + if (rankOrModifier == ByRef) + { + builder.Append('&'); + } + else if (rankOrModifier == Pointer) + { + builder.Append('*'); + } + else if (rankOrModifier == SZArray) + { + builder.Append("[]"); + } + else if (rankOrModifier == 1) + { + builder.Append("[*]"); + } + else + { + Debug.Assert(rankOrModifier >= 2); + + builder.Append('['); + builder.Append(',', rankOrModifier - 1); + builder.Append(']'); + } + + return builder.ToString(); + } + + /// + /// Are there any captured generic args? We'll look for "[[" and "[" that is not followed by "]", "*" and ",". + /// + internal static bool IsBeginningOfGenericArgs(ref ReadOnlySpan span, out bool doubleBrackets) + { + doubleBrackets = false; + + if (!span.IsEmpty && span[0] == '[') + { + // There are no spaces allowed before the first '[', but spaces are allowed after that. + ReadOnlySpan trimmed = span.Slice(1).TrimStart(); + if (!trimmed.IsEmpty) + { + if (trimmed[0] == '[') + { + doubleBrackets = true; + span = trimmed.Slice(1).TrimStart(); + return true; + } + if (!(trimmed[0] is ',' or '*' or ']')) // [] or [*] or [,] or [,,,, ...] + { + span = trimmed; + return true; + } + } + } + + return false; + } + + internal static bool TryGetTypeNameInfo(ref ReadOnlySpan input, ref List? nestedNameLengths, out int totalLength) + { + bool isNestedType; + totalLength = 0; + do + { + int length = GetFullTypeNameLength(input.Slice(totalLength), out isNestedType); + if (length <= 0) + { + // invalid type names: + // -1: invalid escaping + // 0: pair of unescaped "++" characters + return false; + } + +#if SYSTEM_PRIVATE_CORELIB + // Compat: Ignore leading '.' for type names without namespace. .NET Framework historically ignored leading '.' here. It is likely + // that code out there depends on this behavior. For example, type names formed by concatenating namespace and name, without checking for + // empty namespace (bug), are going to have superfluous leading '.'. + // This behavior means that types that start with '.' are not round-trippable via type name. + if (length > 1 && input[0] == '.' && input.Slice(0, length).LastIndexOf('.') == 0) + { + input = input.Slice(1); + length--; + } +#endif + if (isNestedType) + { + (nestedNameLengths ??= new()).Add(length); + totalLength += 1; // skip the '+' sign in next search + } + totalLength += length; + } while (isNestedType); + + return true; + } + + internal static bool TryParseNextDecorator(ref ReadOnlySpan input, out int rankOrModifier) + { + // Then try pulling a single decorator. + // Whitespace cannot precede the decorator, but it can follow the decorator. + + ReadOnlySpan originalInput = input; // so we can restore on 'false' return + + if (TryStripFirstCharAndTrailingSpaces(ref input, '*')) + { + rankOrModifier = Pointer; + return true; + } + + if (TryStripFirstCharAndTrailingSpaces(ref input, '&')) + { + rankOrModifier = ByRef; + return true; + } + + if (TryStripFirstCharAndTrailingSpaces(ref input, '[')) + { + // SZArray := [] + // MDArray := [*] or [,] or [,,,, ...] + + int rank = 1; + bool hasSeenAsterisk = false; + + ReadNextArrayToken: + + if (TryStripFirstCharAndTrailingSpaces(ref input, ']')) + { + // End of array marker + rankOrModifier = rank == 1 && !hasSeenAsterisk ? SZArray : rank; + return true; + } + + if (!hasSeenAsterisk) + { + if (rank == 1 && TryStripFirstCharAndTrailingSpaces(ref input, '*')) + { + // [*] + hasSeenAsterisk = true; + goto ReadNextArrayToken; + } + else if (TryStripFirstCharAndTrailingSpaces(ref input, ',')) + { + // [,,, ...] + checked { rank++; } + goto ReadNextArrayToken; + } + } + + // Don't know what this token is. + // Fall through to 'return false' statement. + } + + input = originalInput; // ensure 'ref input' not mutated + rankOrModifier = 0; + return false; + } + + internal static bool TryStripFirstCharAndTrailingSpaces(ref ReadOnlySpan span, char value) + { + if (!span.IsEmpty && span[0] == value) + { + span = span.Slice(1).TrimStart(); + return true; + } + return false; + } + + [DoesNotReturn] + internal static void ThrowInvalidOperation_MaxNodesExceeded(int limit) => throw +#if SYSTEM_REFLECTION_METADATA + new InvalidOperationException(SR.Format(SR.InvalidOperation_MaxNodesExceeded, limit)); +#else // corelib and tools that reference this file as a link + new InvalidOperationException(); +#endif + + [DoesNotReturn] + internal static void ThrowArgumentException_InvalidTypeName(int errorIndex) => throw +#if SYSTEM_PRIVATE_CORELIB + new ArgumentException(SR.Arg_ArgumentException, $"typeName@{errorIndex}"); +#elif SYSTEM_REFLECTION_METADATA + new ArgumentException(SR.Argument_InvalidTypeName, $"typeName@{errorIndex}"); +#else // tools that reference this file as a link + new ArgumentException(); +#endif + + [DoesNotReturn] + internal static void ThrowInvalidOperation_NotGenericType() => throw +#if SYSTEM_REFLECTION_METADATA || SYSTEM_PRIVATE_CORELIB + new InvalidOperationException(SR.InvalidOperation_NotGenericType); +#else // tools that reference this file as a link + new InvalidOperationException(); +#endif + + [DoesNotReturn] + internal static void ThrowInvalidOperation_NotNestedType() => throw +#if SYSTEM_REFLECTION_METADATA + new InvalidOperationException(SR.InvalidOperation_NotNestedType); +#else // corelib and tools that reference this file as a link + new InvalidOperationException(); +#endif + + [DoesNotReturn] + internal static void ThrowInvalidOperation_NoElement() => throw +#if SYSTEM_REFLECTION_METADATA + new InvalidOperationException(SR.InvalidOperation_NoElement); +#else // corelib and tools that reference this file as a link + new InvalidOperationException(); +#endif + + [DoesNotReturn] + internal static void ThrowInvalidOperation_HasToBeArrayClass() => throw +#if SYSTEM_REFLECTION_METADATA || SYSTEM_PRIVATE_CORELIB + new InvalidOperationException(SR.Argument_HasToBeArrayClass); +#else // tools that reference this file as a link + new InvalidOperationException(); +#endif + } +} diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs new file mode 100644 index 00000000000000..66350c9a44cac7 --- /dev/null +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Reflection.Metadata +{ +#if SYSTEM_PRIVATE_CORELIB + internal +#else + public +#endif + sealed class TypeNameParseOptions + { + private int _maxNodes = +#if SYSTEM_PRIVATE_CORELIB + int.MaxValue; // CoreLib has never introduced any limits +#else + 20; +#endif + + /// + /// Limits the maximum value of node count that parser can handle. + /// + public int MaxNodes + { + get => _maxNodes; + set + { +#if NET8_0_OR_GREATER + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(value, 0, nameof(value)); +#else + if (value <= 0) + { + throw new ArgumentOutOfRangeException(paramName: nameof(value)); + } +#endif + + _maxNodes = value; + } + } + } +} diff --git a/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs new file mode 100644 index 00000000000000..bcb2125d3f4ea3 --- /dev/null +++ b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs @@ -0,0 +1,192 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Reflection.Metadata; +using System.Text; + +#nullable enable + +namespace System.Reflection +{ + internal partial struct TypeNameParser + { +#if !MONO // Mono never needs unescaped names + private const char EscapeCharacter = '\\'; + + /// + /// Removes escape characters from the string (if there were any found). + /// + private static string UnescapeTypeName(string name) + { + int indexOfEscapeCharacter = name.IndexOf(EscapeCharacter); + if (indexOfEscapeCharacter < 0) + { + return name; + } + + return Unescape(name, indexOfEscapeCharacter); + + static string Unescape(string name, int indexOfEscapeCharacter) + { + // this code path is executed very rarely (IL Emit or pure IL with chars not allowed in C# or F#) + var sb = new ValueStringBuilder(stackalloc char[64]); + sb.EnsureCapacity(name.Length); + sb.Append(name.AsSpan(0, indexOfEscapeCharacter)); + + for (int i = indexOfEscapeCharacter; i < name.Length;) + { + char c = name[i++]; + + if (c != EscapeCharacter) + { + sb.Append(c); + } + else if (i < name.Length && name[i] == EscapeCharacter) // escaped escape character ;) + { + sb.Append(c); + // Consume the escaped escape character, it's important for edge cases + // like escaped escape character followed by another escaped char (example: "\\\\\\+") + i++; + } + } + + return sb.ToString(); + } + } +#endif + + private static (string typeNamespace, string name) SplitFullTypeName(string typeName) + { + string typeNamespace, name; + + // Matches algorithm from ns::FindSep in src\coreclr\utilcode\namespaceutil.cpp + // This could result in the type name beginning with a '.' character. + int separator = typeName.LastIndexOf('.'); + if (separator <= 0) + { + typeNamespace = ""; + name = typeName; + } + else + { + if (typeName[separator - 1] == '.') + separator--; + typeNamespace = typeName.Substring(0, separator); + name = typeName.Substring(separator + 1); + } + + return (typeNamespace, name); + } + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050:RequiresDynamicCode", + Justification = "Used to implement resolving types from strings.")] + private Type? Resolve(TypeName typeName) + { + if (typeName.IsNested) + { + TypeName? current = typeName; + int nestingDepth = 0; + while (current is not null && current.IsNested) + { + nestingDepth++; + current = current.DeclaringType; + } + + // We're performing real type resolution, it is assumed that the caller has already validated the correctness + // of this TypeName object against their own policies, so there is no need for this method to perform any further checks. + string[] nestedTypeNames = new string[nestingDepth]; + current = typeName; + while (current is not null && current.IsNested) + { +#if MONO + nestedTypeNames[--nestingDepth] = current.Name; +#else // CLR, NativeAOT and tools require unescaped nested type names + nestedTypeNames[--nestingDepth] = UnescapeTypeName(current.Name); +#endif + current = current.DeclaringType; + } +#if SYSTEM_PRIVATE_CORELIB + string nonNestedParentName = current!.FullName; +#else // the tools require unescaped names + string nonNestedParentName = UnescapeTypeName(current!.FullName); +#endif + Type? type = GetType(nonNestedParentName, nestedTypeNames, typeName); + return type is null || !typeName.IsConstructedGenericType ? type : MakeGenericType(type, typeName); + } + else if (typeName.IsConstructedGenericType) + { + Type? type = Resolve(typeName.GetGenericTypeDefinition()); + return type is null ? null : MakeGenericType(type, typeName); + } + else if (typeName.IsArray || typeName.IsPointer || typeName.IsByRef) + { + Type? type = Resolve(typeName.GetElementType()); + if (type is null) + { + return null; + } + + if (typeName.IsByRef) + { + return type.MakeByRefType(); + } + else if (typeName.IsPointer) + { + return type.MakePointerType(); + } + else if (typeName.IsSZArray) + { + return type.MakeArrayType(); + } + else + { + Debug.Assert(typeName.IsVariableBoundArrayType); + + return type.MakeArrayType(rank: typeName.GetArrayRank()); + } + } + else + { + Debug.Assert(typeName.IsSimple); + + Type? type = GetType( +#if SYSTEM_PRIVATE_CORELIB + typeName.FullName, +#else // the tools require unescaped names + UnescapeTypeName(typeName.FullName), +#endif + nestedTypeNames: ReadOnlySpan.Empty, typeName); + + return type; + } + } + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2055:UnrecognizedReflectionPattern", + Justification = "Used to implement resolving types from strings.")] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050:RequiresDynamicCode", + Justification = "Used to implement resolving types from strings.")] + private Type? MakeGenericType(Type type, TypeName typeName) + { + var genericArgs = typeName.GetGenericArguments(); +#if SYSTEM_PRIVATE_CORELIB + int size = genericArgs.Count; +#else + int size = genericArgs.Length; +#endif + Type[] genericTypes = new Type[size]; + for (int i = 0; i < size; i++) + { + Type? genericArg = Resolve(genericArgs[i]); + if (genericArg is null) + { + return null; + } + genericTypes[i] = genericArg; + } + + return type.MakeGenericType(genericTypes); + } + } +} diff --git a/src/libraries/Common/src/System/Reflection/TypeNameParser.cs b/src/libraries/Common/src/System/Reflection/TypeNameParser.cs deleted file mode 100644 index 0b69e4e18aaae4..00000000000000 --- a/src/libraries/Common/src/System/Reflection/TypeNameParser.cs +++ /dev/null @@ -1,701 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; -using System.Text; - -#nullable enable - -namespace System.Reflection -{ - // - // Parser for type names passed to GetType() apis. - // - [StructLayout(LayoutKind.Auto)] - internal ref partial struct TypeNameParser - { - private ReadOnlySpan _input; - private int _index; - private int _errorIndex; // Position for error reporting - - private TypeNameParser(ReadOnlySpan name) - { - _input = name; - _errorIndex = _index = 0; - } - - // - // Parses a type name. The type name may be optionally postpended with a "," followed by a legal assembly name. - // - private Type? Parse() - { - TypeName? typeName = ParseNonQualifiedTypeName(); - if (typeName is null) - return null; - - string? assemblyName = null; - - TokenType token = GetNextToken(); - if (token != TokenType.End) - { - if (token != TokenType.Comma) - { - ParseError(); - return null; - } - - if (!CheckTopLevelAssemblyQualifiedName()) - return null; - - assemblyName = GetNextAssemblyName(); - if (assemblyName is null) - return null; - Debug.Assert(Peek == TokenType.End); - } - - return typeName.ResolveType(ref this, assemblyName); - } - - // - // Parses a type name without any assembly name qualification. - // - private TypeName? ParseNonQualifiedTypeName() - { - // Parse the named type or constructed generic type part first. - TypeName? typeName = ParseNamedOrConstructedGenericTypeName(); - if (typeName is null) - return null; - - // Iterate through any "has-element" qualifiers ([], &, *). - while (true) - { - TokenType token = Peek; - if (token == TokenType.End) - break; - if (token == TokenType.Asterisk) - { - Skip(); - typeName = new ModifierTypeName(typeName, ModifierTypeName.Pointer); - } - else if (token == TokenType.Ampersand) - { - Skip(); - typeName = new ModifierTypeName(typeName, ModifierTypeName.ByRef); - } - else if (token == TokenType.OpenSqBracket) - { - Skip(); - token = GetNextToken(); - if (token == TokenType.Asterisk) - { - typeName = new ModifierTypeName(typeName, 1); - token = GetNextToken(); - } - else - { - int rank = 1; - while (token == TokenType.Comma) - { - token = GetNextToken(); - rank++; - } - if (rank == 1) - typeName = new ModifierTypeName(typeName, ModifierTypeName.Array); - else - typeName = new ModifierTypeName(typeName, rank); - } - if (token != TokenType.CloseSqBracket) - { - ParseError(); - return null; - } - } - else - { - break; - } - } - return typeName; - } - - // - // Foo or Foo+Inner or Foo[String] or Foo+Inner[String] - // - private TypeName? ParseNamedOrConstructedGenericTypeName() - { - TypeName? namedType = ParseNamedTypeName(); - if (namedType is null) - return null; - - // Because "[" is used both for generic arguments and array indexes, we must peek two characters deep. - if (!(Peek is TokenType.OpenSqBracket && (PeekSecond is TokenType.Other or TokenType.OpenSqBracket))) - return namedType; - - Skip(); - - TypeName[] typeArguments = new TypeName[2]; - int typeArgumentsCount = 0; - while (true) - { - TypeName? typeArgument = ParseGenericTypeArgument(); - if (typeArgument is null) - return null; - if (typeArgumentsCount >= typeArguments.Length) - Array.Resize(ref typeArguments, 2 * typeArgumentsCount); - typeArguments[typeArgumentsCount++] = typeArgument; - TokenType token = GetNextToken(); - if (token == TokenType.CloseSqBracket) - break; - if (token != TokenType.Comma) - { - ParseError(); - return null; - } - } - - return new GenericTypeName(namedType, typeArguments, typeArgumentsCount); - } - - // - // Foo or Foo+Inner - // - private TypeName? ParseNamedTypeName() - { - string? fullName = GetNextIdentifier(); - if (fullName is null) - return null; - - fullName = ApplyLeadingDotCompatQuirk(fullName); - - if (Peek == TokenType.Plus) - { - string[] nestedNames = new string[1]; - int nestedNamesCount = 0; - - do - { - Skip(); - - string? nestedName = GetNextIdentifier(); - if (nestedName is null) - return null; - - nestedName = ApplyLeadingDotCompatQuirk(nestedName); - - if (nestedNamesCount >= nestedNames.Length) - Array.Resize(ref nestedNames, 2 * nestedNamesCount); - nestedNames[nestedNamesCount++] = nestedName; - } - while (Peek == TokenType.Plus); - - return new NestedNamespaceTypeName(fullName, nestedNames, nestedNamesCount); - } - else - { - return new NamespaceTypeName(fullName); - } - - // Compat: Ignore leading '.' for type names without namespace. .NET Framework historically ignored leading '.' here. It is likely - // that code out there depends on this behavior. For example, type names formed by concatenating namespace and name, without checking for - // empty namespace (bug), are going to have superfluous leading '.'. - // This behavior means that types that start with '.' are not round-trippable via type name. - static string ApplyLeadingDotCompatQuirk(string typeName) - { -#if NETCOREAPP - return (typeName.StartsWith('.') && !typeName.AsSpan(1).Contains('.')) ? typeName.Substring(1) : typeName; -#else - return ((typeName.Length > 0) && (typeName[0] == '.') && typeName.LastIndexOf('.') == 0) ? typeName.Substring(1) : typeName; -#endif - } - } - - // - // Parse a generic argument. In particular, generic arguments can take the special form [,]. - // - private TypeName? ParseGenericTypeArgument() - { - TokenType token = GetNextToken(); - if (token == TokenType.Other) - { - return ParseNonQualifiedTypeName(); - } - if (token != TokenType.OpenSqBracket) - { - ParseError(); - return null; - } - string? assemblyName = null; - TypeName? typeName = ParseNonQualifiedTypeName(); - if (typeName is null) - return null; - - token = GetNextToken(); - if (token == TokenType.Comma) - { - assemblyName = GetNextEmbeddedAssemblyName(); - token = GetNextToken(); - } - if (token != TokenType.CloseSqBracket) - { - ParseError(); - return null; - } - - return (assemblyName != null) ? new AssemblyQualifiedTypeName(typeName, assemblyName) : typeName; - } - - // - // String tokenizer for type names passed to the GetType() APIs. - // - - private TokenType Peek - { - get - { - SkipWhiteSpace(); - char c = (_index < _input.Length) ? _input[_index] : '\0'; - return CharToToken(c); - } - } - - private TokenType PeekSecond - { - get - { - SkipWhiteSpace(); - int index = _index + 1; - while (index < _input.Length && char.IsWhiteSpace(_input[index])) - index++; - char c = (index < _input.Length) ? _input[index] : '\0'; - return CharToToken(c); - } - } - - private void Skip() - { - SkipWhiteSpace(); - if (_index < _input.Length) - _index++; - } - - // Return the next token and skip index past it unless already at end of string - // or the token is not a reserved token. - private TokenType GetNextToken() - { - _errorIndex = _index; - - TokenType tokenType = Peek; - if (tokenType == TokenType.End || tokenType == TokenType.Other) - return tokenType; - Skip(); - return tokenType; - } - - // - // Lex the next segment as part of a type name. (Do not use for assembly names.) - // - // Note that unescaped "."'s do NOT terminate the identifier, but unescaped "+"'s do. - // - // Terminated by the first non-escaped reserved character ('[', ']', '+', '&', '*' or ',') - // - private string? GetNextIdentifier() - { - SkipWhiteSpace(); - - ValueStringBuilder sb = new ValueStringBuilder(stackalloc char[64]); - - int src = _index; - while (true) - { - if (src >= _input.Length) - break; - char c = _input[src]; - TokenType token = CharToToken(c); - if (token != TokenType.Other) - break; - src++; - if (c == '\\') // Check for escaped character - { - // Update error location - _errorIndex = src - 1; - - c = (src < _input.Length) ? _input[src++] : '\0'; - - if (!NeedsEscapingInTypeName(c)) - { - // If we got here, a backslash was used to escape a character that is not legal to escape inside a type name. - ParseError(); - return null; - } - } - sb.Append(c); - } - _index = src; - - if (sb.Length == 0) - { - // The identifier has to be non-empty - _errorIndex = src; - ParseError(); - return null; - } - - return sb.ToString(); - } - - // - // Lex the next segment as the assembly name at the end of an assembly-qualified type name. (Do not use for - // assembly names embedded inside generic type arguments.) - // - private string? GetNextAssemblyName() - { - if (!StartAssemblyName()) - return null; - - string assemblyName = _input.Slice(_index).ToString(); - _index = _input.Length; - return assemblyName; - } - - // - // Lex the next segment as an assembly name embedded inside a generic argument type. - // - // Terminated by an unescaped ']'. - // - private string? GetNextEmbeddedAssemblyName() - { - if (!StartAssemblyName()) - return null; - - ValueStringBuilder sb = new ValueStringBuilder(stackalloc char[64]); - - int src = _index; - while (true) - { - if (src >= _input.Length) - { - ParseError(); - return null; - } - char c = _input[src]; - if (c == ']') - break; - src++; - - // Backslash can be used to escape a ']' - any other backslash character is left alone (along with the backslash) - // for the AssemblyName parser to handle. - if (c == '\\' && (src < _input.Length) && _input[src] == ']') - { - c = _input[src++]; - } - sb.Append(c); - } - _index = src; - - if (sb.Length == 0) - { - // The assembly name has to be non-empty - _errorIndex = src; - ParseError(); - return null; - } - - return sb.ToString(); - } - - private bool StartAssemblyName() - { - // Compat: Treat invalid starting token of assembly name as type name parsing error instead of assembly name parsing error. This only affects - // exception returned by the parser. - if (Peek is TokenType.End or TokenType.Comma) - { - ParseError(); - return false; - } - return true; - } - - // - // Classify a character as a TokenType. (Fortunately, all tokens in type name strings other than identifiers are single-character tokens.) - // - private static TokenType CharToToken(char c) - { - return c switch - { - '\0' => TokenType.End, - '[' => TokenType.OpenSqBracket, - ']' => TokenType.CloseSqBracket, - ',' => TokenType.Comma, - '+' => TokenType.Plus, - '*' => TokenType.Asterisk, - '&' => TokenType.Ampersand, - _ => TokenType.Other, - }; - } - - // - // The type name parser has a strange attitude towards whitespace. It throws away whitespace between punctuation tokens and whitespace - // preceding identifiers or assembly names (and this cannot be escaped away). But whitespace between the end of an identifier - // and the punctuation that ends it is *not* ignored. - // - // In other words, GetType(" Foo") searches for "Foo" but GetType("Foo ") searches for "Foo ". - // - // Whitespace between the end of an assembly name and the punction mark that ends it is also not ignored by this parser, - // but this is irrelevant since the assembly name is then turned over to AssemblyName for parsing, which *does* ignore trailing whitespace. - // - private void SkipWhiteSpace() - { - while (_index < _input.Length && char.IsWhiteSpace(_input[_index])) - _index++; - } - - private enum TokenType - { - End = 0, //At end of string - OpenSqBracket = 1, //'[' - CloseSqBracket = 2, //']' - Comma = 3, //',' - Plus = 4, //'+' - Asterisk = 5, //'*' - Ampersand = 6, //'&' - Other = 7, //Type identifier, AssemblyName or embedded AssemblyName. - } - - // - // The TypeName class is the base class for a family of types that represent the nodes in a parse tree for - // assembly-qualified type names. - // - private abstract class TypeName - { - /// - /// Helper for the Type.GetType() family of APIs. "containingAssemblyIsAny" is the assembly to search for (as determined - /// by a qualifying assembly string in the original type string passed to Type.GetType(). If null, it means the type stream - /// didn't specify an assembly name. How to respond to that is up to the type resolver delegate in getTypeOptions - this class - /// is just a middleman. - /// - public abstract Type? ResolveType(ref TypeNameParser parser, string? containingAssemblyIfAny); - } - - // - // Represents a parse of a type name qualified by an assembly name. - // - private sealed class AssemblyQualifiedTypeName : TypeName - { - private readonly string _assemblyName; - private readonly TypeName _nonQualifiedTypeName; - - public AssemblyQualifiedTypeName(TypeName nonQualifiedTypeName, string assemblyName) - { - _nonQualifiedTypeName = nonQualifiedTypeName; - _assemblyName = assemblyName; - } - - public override Type? ResolveType(ref TypeNameParser parser, string? containingAssemblyIfAny) - { - return _nonQualifiedTypeName.ResolveType(ref parser, _assemblyName); - } - } - - // - // Non-nested named type. The full name is the namespace-qualified name. For example, the FullName for - // System.Collections.Generic.IList<> is "System.Collections.Generic.IList`1". - // - private sealed partial class NamespaceTypeName : TypeName - { - private readonly string _fullName; - public NamespaceTypeName(string fullName) - { - _fullName = fullName; - } - - public override Type? ResolveType(ref TypeNameParser parser, string? containingAssemblyIfAny) - { - return parser.GetType(_fullName, default, containingAssemblyIfAny); - } - } - - // - // Nested type name. - // - private sealed partial class NestedNamespaceTypeName : TypeName - { - private readonly string _fullName; - private readonly string[] _nestedNames; - private readonly int _nestedNamesCount; - - public NestedNamespaceTypeName(string fullName, string[] nestedNames, int nestedNamesCount) - { - _fullName = fullName; - _nestedNames = nestedNames; - _nestedNamesCount = nestedNamesCount; - } - - public override Type? ResolveType(ref TypeNameParser parser, string? containingAssemblyIfAny) - { - return parser.GetType(_fullName, _nestedNames.AsSpan(0, _nestedNamesCount), containingAssemblyIfAny); - } - } - - // - // Array, byref or pointer type name. - // - private sealed class ModifierTypeName : TypeName - { - private readonly TypeName _elementTypeName; - - // Positive value is multi-dimensional array rank. - // Negative value is modifier encoded using constants below. - private readonly int _rankOrModifier; - - public const int Array = -1; - public const int Pointer = -2; - public const int ByRef = -3; - - public ModifierTypeName(TypeName elementTypeName, int rankOrModifier) - { - _elementTypeName = elementTypeName; - _rankOrModifier = rankOrModifier; - } - -#if NETCOREAPP - [UnconditionalSuppressMessage("AotAnalysis", "IL3050:AotUnfriendlyApi", - Justification = "Used to implement resolving types from strings.")] -#endif - public override Type? ResolveType(ref TypeNameParser parser, string? containingAssemblyIfAny) - { - Type? elementType = _elementTypeName.ResolveType(ref parser, containingAssemblyIfAny); - if (elementType is null) - return null; - - return _rankOrModifier switch - { - Array => elementType.MakeArrayType(), - Pointer => elementType.MakePointerType(), - ByRef => elementType.MakeByRefType(), - _ => elementType.MakeArrayType(_rankOrModifier) - }; - } - } - - // - // Constructed generic type name. - // - private sealed class GenericTypeName : TypeName - { - private readonly TypeName _typeDefinition; - private readonly TypeName[] _typeArguments; - private readonly int _typeArgumentsCount; - - public GenericTypeName(TypeName genericTypeDefinition, TypeName[] typeArguments, int typeArgumentsCount) - { - _typeDefinition = genericTypeDefinition; - _typeArguments = typeArguments; - _typeArgumentsCount = typeArgumentsCount; - } - -#if NETCOREAPP - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2055:UnrecognizedReflectionPattern", - Justification = "Used to implement resolving types from strings.")] - [UnconditionalSuppressMessage("AotAnalysis", "IL3050:AotUnfriendlyApi", - Justification = "Used to implement resolving types from strings.")] -#endif - public override Type? ResolveType(ref TypeNameParser parser, string? containingAssemblyIfAny) - { - Type? typeDefinition = _typeDefinition.ResolveType(ref parser, containingAssemblyIfAny); - if (typeDefinition is null) - return null; - - Type[] arguments = new Type[_typeArgumentsCount]; - for (int i = 0; i < arguments.Length; i++) - { - Type? typeArgument = _typeArguments[i].ResolveType(ref parser, null); - if (typeArgument is null) - return null; - arguments[i] = typeArgument; - } - - return typeDefinition.MakeGenericType(arguments); - } - } - - // - // Type name escaping helpers - // - -#if NETCOREAPP - private static ReadOnlySpan CharsToEscape => "\\[]+*&,"; - - private static bool NeedsEscapingInTypeName(char c) - => CharsToEscape.Contains(c); -#else - private static char[] CharsToEscape { get; } = "\\[]+*&,".ToCharArray(); - - private static bool NeedsEscapingInTypeName(char c) - => Array.IndexOf(CharsToEscape, c) >= 0; -#endif - - private static string EscapeTypeName(string name) - { - if (name.AsSpan().IndexOfAny(CharsToEscape) < 0) - return name; - - var sb = new ValueStringBuilder(stackalloc char[64]); - foreach (char c in name) - { - if (NeedsEscapingInTypeName(c)) - sb.Append('\\'); - sb.Append(c); - } - - return sb.ToString(); - } - - private static string EscapeTypeName(string typeName, ReadOnlySpan nestedTypeNames) - { - string fullName = EscapeTypeName(typeName); - if (nestedTypeNames.Length > 0) - { - var sb = new StringBuilder(fullName); - for (int i = 0; i < nestedTypeNames.Length; i++) - { - sb.Append('+'); - sb.Append(EscapeTypeName(nestedTypeNames[i])); - } - fullName = sb.ToString(); - } - return fullName; - } - - private static (string typeNamespace, string name) SplitFullTypeName(string typeName) - { - string typeNamespace, name; - - // Matches algorithm from ns::FindSep in src\coreclr\utilcode\namespaceutil.cpp - int separator = typeName.LastIndexOf('.'); - if (separator <= 0) - { - typeNamespace = ""; - name = typeName; - } - else - { - if (typeName[separator - 1] == '.') - separator--; - typeNamespace = typeName.Substring(0, separator); - name = typeName.Substring(separator + 1); - } - - return (typeNamespace, name); - } - -#if SYSTEM_PRIVATE_CORELIB - private void ParseError() - { - if (_throwOnError) - throw new ArgumentException(SR.Arg_ArgumentException, $"typeName@{_errorIndex}"); - } -#endif - } -} diff --git a/src/libraries/Common/src/System/Text/ValueStringBuilder.AppendSpanFormattable.cs b/src/libraries/Common/src/System/Text/ValueStringBuilder.AppendSpanFormattable.cs index 04e9fcbdb55240..3e9cf2c56d3ea6 100644 --- a/src/libraries/Common/src/System/Text/ValueStringBuilder.AppendSpanFormattable.cs +++ b/src/libraries/Common/src/System/Text/ValueStringBuilder.AppendSpanFormattable.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#nullable enable + namespace System.Text { internal ref partial struct ValueStringBuilder diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index b9f4be64bc9a74..4dd47c701586ca 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -649,9 +649,7 @@ - - @@ -1473,8 +1471,29 @@ Common\System\IO\PathInternal.CaseSensitivity.cs - - Common\System\Reflection\TypeNameParser.cs + + Common\System\Reflection\TypeNameParser.Helpers + + + Common\System\Reflection\AssemblyNameParser.cs + + + Common\System\Reflection\AssemblyNameFormatter.cs + + + Common\System\Reflection\Metadata\AssemblyNameInfo.cs + + + Common\System\Reflection\Metadata\TypeName.cs + + + Common\System\Reflection\Metadata\TypeNameParser.cs + + + Common\System\Reflection\Metadata\TypeNameParserHelpers.cs + + + Common\System\Reflection\Metadata\TypeNameParserOptions.cs Common\System\Runtime\Versioning\NonVersionableAttribute.cs diff --git a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/UnconditionalSuppressMessageAttribute.cs b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/UnconditionalSuppressMessageAttribute.cs index 2d82ed0c0e7f50..5ee6c949bc97e5 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/UnconditionalSuppressMessageAttribute.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/UnconditionalSuppressMessageAttribute.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#nullable enable + namespace System.Diagnostics.CodeAnalysis { /// diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/AssemblyNameParser.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/AssemblyNameParser.cs deleted file mode 100644 index dbaaefd4d8c9dd..00000000000000 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/AssemblyNameParser.cs +++ /dev/null @@ -1,438 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.IO; -using System.Runtime.CompilerServices; -using System.Text; - -namespace System.Reflection -{ - // - // Parses an assembly name. - // - internal ref struct AssemblyNameParser - { - public readonly struct AssemblyNameParts - { - public AssemblyNameParts(string name, Version? version, string? cultureName, AssemblyNameFlags flags, byte[]? publicKeyOrToken) - { - _name = name; - _version = version; - _cultureName = cultureName; - _flags = flags; - _publicKeyOrToken = publicKeyOrToken; - } - - public readonly string _name; - public readonly Version? _version; - public readonly string? _cultureName; - public readonly AssemblyNameFlags _flags; - public readonly byte[]? _publicKeyOrToken; - } - - // Token categories for the lexer. - private enum Token - { - Equals = 1, - Comma = 2, - String = 3, - End = 4, - } - - private enum AttributeKind - { - Version = 1, - Culture = 2, - PublicKeyOrToken = 4, - ProcessorArchitecture = 8, - Retargetable = 16, - ContentType = 32 - } - - private readonly ReadOnlySpan _input; - private int _index; - - private AssemblyNameParser(ReadOnlySpan input) - { - if (input.Length == 0) - throw new ArgumentException(SR.Format_StringZeroLength); - - _input = input; - _index = 0; - } - - public static AssemblyNameParts Parse(string name) - { - return new AssemblyNameParser(name).Parse(); - } - - public static AssemblyNameParts Parse(ReadOnlySpan name) - { - return new AssemblyNameParser(name).Parse(); - } - - private void RecordNewSeenOrThrow(scoped ref AttributeKind seenAttributes, AttributeKind newAttribute) - { - if ((seenAttributes & newAttribute) != 0) - { - ThrowInvalidAssemblyName(); - } - seenAttributes |= newAttribute; - } - - private AssemblyNameParts Parse() - { - // Name must come first. - Token token = GetNextToken(out string name); - if (token != Token.String) - ThrowInvalidAssemblyName(); - - if (string.IsNullOrEmpty(name)) - ThrowInvalidAssemblyName(); - - Version? version = null; - string? cultureName = null; - byte[]? pkt = null; - AssemblyNameFlags flags = 0; - - AttributeKind alreadySeen = default; - token = GetNextToken(); - while (token != Token.End) - { - if (token != Token.Comma) - ThrowInvalidAssemblyName(); - - token = GetNextToken(out string attributeName); - if (token != Token.String) - ThrowInvalidAssemblyName(); - - token = GetNextToken(); - if (token != Token.Equals) - ThrowInvalidAssemblyName(); - - token = GetNextToken(out string attributeValue); - if (token != Token.String) - ThrowInvalidAssemblyName(); - - if (attributeName == string.Empty) - ThrowInvalidAssemblyName(); - - if (attributeName.Equals("Version", StringComparison.OrdinalIgnoreCase)) - { - RecordNewSeenOrThrow(ref alreadySeen, AttributeKind.Version); - version = ParseVersion(attributeValue); - } - - if (attributeName.Equals("Culture", StringComparison.OrdinalIgnoreCase)) - { - RecordNewSeenOrThrow(ref alreadySeen, AttributeKind.Culture); - cultureName = ParseCulture(attributeValue); - } - - if (attributeName.Equals("PublicKey", StringComparison.OrdinalIgnoreCase)) - { - RecordNewSeenOrThrow(ref alreadySeen, AttributeKind.PublicKeyOrToken); - pkt = ParsePKT(attributeValue, isToken: false); - flags |= AssemblyNameFlags.PublicKey; - } - - if (attributeName.Equals("PublicKeyToken", StringComparison.OrdinalIgnoreCase)) - { - RecordNewSeenOrThrow(ref alreadySeen, AttributeKind.PublicKeyOrToken); - pkt = ParsePKT(attributeValue, isToken: true); - } - - if (attributeName.Equals("ProcessorArchitecture", StringComparison.OrdinalIgnoreCase)) - { - RecordNewSeenOrThrow(ref alreadySeen, AttributeKind.ProcessorArchitecture); - flags |= (AssemblyNameFlags)(((int)ParseProcessorArchitecture(attributeValue)) << 4); - } - - if (attributeName.Equals("Retargetable", StringComparison.OrdinalIgnoreCase)) - { - RecordNewSeenOrThrow(ref alreadySeen, AttributeKind.Retargetable); - if (attributeValue.Equals("Yes", StringComparison.OrdinalIgnoreCase)) - { - flags |= AssemblyNameFlags.Retargetable; - } - else if (attributeValue.Equals("No", StringComparison.OrdinalIgnoreCase)) - { - // nothing to do - } - else - { - ThrowInvalidAssemblyName(); - } - } - - if (attributeName.Equals("ContentType", StringComparison.OrdinalIgnoreCase)) - { - RecordNewSeenOrThrow(ref alreadySeen, AttributeKind.ContentType); - if (attributeValue.Equals("WindowsRuntime", StringComparison.OrdinalIgnoreCase)) - { - flags |= (AssemblyNameFlags)(((int)AssemblyContentType.WindowsRuntime) << 9); - } - else - { - ThrowInvalidAssemblyName(); - } - } - - // Desktop compat: If we got here, the attribute name is unknown to us. Ignore it. - token = GetNextToken(); - } - - return new AssemblyNameParts(name, version, cultureName, flags, pkt); - } - - private Version ParseVersion(string attributeValue) - { - ReadOnlySpan attributeValueSpan = attributeValue; - Span parts = stackalloc Range[5]; - parts = parts.Slice(0, attributeValueSpan.Split(parts, '.')); - if (parts.Length is < 2 or > 4) - { - ThrowInvalidAssemblyName(); - } - - Span versionNumbers = stackalloc ushort[4]; - for (int i = 0; i < versionNumbers.Length; i++) - { - if ((uint)i >= (uint)parts.Length) - { - versionNumbers[i] = ushort.MaxValue; - break; - } - - if (!ushort.TryParse(attributeValueSpan[parts[i]], NumberStyles.None, NumberFormatInfo.InvariantInfo, out versionNumbers[i])) - { - ThrowInvalidAssemblyName(); - } - } - - if (versionNumbers[0] == ushort.MaxValue || - versionNumbers[1] == ushort.MaxValue) - { - ThrowInvalidAssemblyName(); - } - - return - versionNumbers[2] == ushort.MaxValue ? new Version(versionNumbers[0], versionNumbers[1]) : - versionNumbers[3] == ushort.MaxValue ? new Version(versionNumbers[0], versionNumbers[1], versionNumbers[2]) : - new Version(versionNumbers[0], versionNumbers[1], versionNumbers[2], versionNumbers[3]); - } - - private static string ParseCulture(string attributeValue) - { - if (attributeValue.Equals("Neutral", StringComparison.OrdinalIgnoreCase)) - { - return ""; - } - - return attributeValue; - } - - private byte[] ParsePKT(string attributeValue, bool isToken) - { - if (attributeValue.Equals("null", StringComparison.OrdinalIgnoreCase) || attributeValue == string.Empty) - return Array.Empty(); - - if (isToken && attributeValue.Length != 8 * 2) - ThrowInvalidAssemblyName(); - - byte[] pkt = new byte[attributeValue.Length / 2]; - int srcIndex = 0; - for (int i = 0; i < pkt.Length; i++) - { - char hi = attributeValue[srcIndex++]; - char lo = attributeValue[srcIndex++]; - pkt[i] = (byte)((ParseHexNybble(hi) << 4) | ParseHexNybble(lo)); - } - return pkt; - } - - private ProcessorArchitecture ParseProcessorArchitecture(string attributeValue) - { - if (attributeValue.Equals("msil", StringComparison.OrdinalIgnoreCase)) - return ProcessorArchitecture.MSIL; - if (attributeValue.Equals("x86", StringComparison.OrdinalIgnoreCase)) - return ProcessorArchitecture.X86; - if (attributeValue.Equals("ia64", StringComparison.OrdinalIgnoreCase)) - return ProcessorArchitecture.IA64; - if (attributeValue.Equals("amd64", StringComparison.OrdinalIgnoreCase)) - return ProcessorArchitecture.Amd64; - if (attributeValue.Equals("arm", StringComparison.OrdinalIgnoreCase)) - return ProcessorArchitecture.Arm; - ThrowInvalidAssemblyName(); - return default; // unreachable - } - - private byte ParseHexNybble(char c) - { - int value = HexConverter.FromChar(c); - if (value == 0xFF) - { - ThrowInvalidAssemblyName(); - } - return (byte)value; - } - - // - // Return the next token in assembly name. If you expect the result to be Token.String, - // use GetNext(out String) instead. - // - private Token GetNextToken() - { - return GetNextToken(out _); - } - - private static bool IsWhiteSpace(char ch) - { - switch (ch) - { - case '\n': - case '\r': - case ' ': - case '\t': - return true; - default: - return false; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private char GetNextChar() - { - char ch; - if (_index < _input.Length) - { - ch = _input[_index++]; - if (ch == '\0') - { - ThrowInvalidAssemblyName(); - } - } - else - { - ch = '\0'; - } - - return ch; - } - - // - // Return the next token in assembly name. If the result is Token.String, - // sets "tokenString" to the tokenized string. - // - private Token GetNextToken(out string tokenString) - { - tokenString = string.Empty; - char c; - - while (true) - { - c = GetNextChar(); - switch (c) - { - case ',': - return Token.Comma; - case '=': - return Token.Equals; - case '\0': - return Token.End; - } - - if (!IsWhiteSpace(c)) - { - break; - } - } - - ValueStringBuilder sb = new ValueStringBuilder(stackalloc char[64]); - - char quoteChar = '\0'; - if (c == '\'' || c == '\"') - { - quoteChar = c; - c = GetNextChar(); - } - - for (; ; ) - { - if (c == 0) - { - if (quoteChar != 0) - { - // EOS and unclosed quotes is an error - ThrowInvalidAssemblyName(); - } - // Reached end of input and therefore of string - break; - } - - if (quoteChar != 0 && c == quoteChar) - break; // Terminate: Found closing quote of quoted string. - - if (quoteChar == 0 && (c == ',' || c == '=')) - { - _index--; - break; // Terminate: Found start of a new ',' or '=' token. - } - - if (quoteChar == 0 && (c == '\'' || c == '\"')) - ThrowInvalidAssemblyName(); - - if (c == '\\') - { - c = GetNextChar(); - - switch (c) - { - case '\\': - case ',': - case '=': - case '\'': - case '"': - sb.Append(c); - break; - case 't': - sb.Append('\t'); - break; - case 'r': - sb.Append('\r'); - break; - case 'n': - sb.Append('\n'); - break; - default: - ThrowInvalidAssemblyName(); - break; //unreachable - } - } - else - { - sb.Append(c); - } - - c = GetNextChar(); - } - - - if (quoteChar == 0) - { - while (sb.Length > 0 && IsWhiteSpace(sb[sb.Length - 1])) - sb.Length--; - } - - tokenString = sb.ToString(); - return Token.String; - } - - [DoesNotReturn] - private void ThrowInvalidAssemblyName() - => throw new FileLoadException(SR.InvalidAssemblyName, _input.ToString()); - } -} diff --git a/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs b/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs index d656738565288c..6eaf4ea70e61ee 100644 --- a/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs +++ b/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs @@ -2408,6 +2408,49 @@ public readonly partial struct TypeLayout public int PackingSize { get { throw null; } } public int Size { get { throw null; } } } + public sealed partial class AssemblyNameInfo + { + public AssemblyNameInfo(string name, System.Version? version = null, string? cultureName = null, System.Reflection.AssemblyNameFlags flags = AssemblyNameFlags.None, + Collections.Immutable.ImmutableArray publicKeyOrToken = default) { } + public string Name { get { throw null; } } + public string? CultureName { get { throw null; } } + public string FullName { get { throw null; } } + public System.Version? Version { get { throw null; } } + public System.Reflection.AssemblyNameFlags Flags { get { throw null; } } + public System.Collections.Immutable.ImmutableArray PublicKeyOrToken { get { throw null; } } + public static System.Reflection.Metadata.AssemblyNameInfo Parse(System.ReadOnlySpan assemblyName) { throw null; } + public static bool TryParse(System.ReadOnlySpan assemblyName, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Reflection.Metadata.AssemblyNameInfo? result) { throw null; } + public System.Reflection.AssemblyName ToAssemblyName() { throw null; } + } + public sealed partial class TypeName + { + internal TypeName() { } + public string AssemblyQualifiedName { get { throw null; } } + public AssemblyNameInfo? AssemblyName { get { throw null; } } + public System.Reflection.Metadata.TypeName? DeclaringType { get { throw null; } } + public string FullName { get { throw null; } } + public bool IsArray { get { throw null; } } + public bool IsByRef { get { throw null; } } + public bool IsConstructedGenericType { get { throw null; } } + public bool IsNested { get { throw null; } } + public bool IsPointer { get { throw null; } } + public bool IsSimple { get { throw null; } } + public bool IsSZArray { get { throw null; } } + public bool IsVariableBoundArrayType { get { throw null; } } + public string Name { get { throw null; } } + public static System.Reflection.Metadata.TypeName Parse(System.ReadOnlySpan typeName, System.Reflection.Metadata.TypeNameParseOptions? options = null) { throw null; } + public static bool TryParse(System.ReadOnlySpan typeName, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Reflection.Metadata.TypeName? result, System.Reflection.Metadata.TypeNameParseOptions? options = null) { throw null; } + public int GetArrayRank() { throw null; } + public System.Collections.Immutable.ImmutableArray GetGenericArguments() { throw null; } + public System.Reflection.Metadata.TypeName GetGenericTypeDefinition() { throw null; } + public System.Reflection.Metadata.TypeName GetElementType() { throw null; } + public int GetNodeCount() { throw null; } + } + public sealed partial class TypeNameParseOptions + { + public TypeNameParseOptions() { } + public int MaxNodes { get { throw null; } set { } } + } public readonly partial struct TypeReference { private readonly object _dummy; diff --git a/src/libraries/System.Reflection.Metadata/src/Resources/Strings.resx b/src/libraries/System.Reflection.Metadata/src/Resources/Strings.resx index c035a3efee098f..963e4d0af9f8a3 100644 --- a/src/libraries/System.Reflection.Metadata/src/Resources/Strings.resx +++ b/src/libraries/System.Reflection.Metadata/src/Resources/Strings.resx @@ -411,4 +411,25 @@ The SwitchInstructionEncoder.Branch method was invoked too many times. + + The name of the type is invalid. + + + Maximum node count of {0} exceeded. + + + Must be an array type. + + + This operation is only valid on generic types. + + + This operation is only valid on nested types. + + + This operation is only valid on arrays, pointers and references. + + + The given assembly name was invalid. + \ No newline at end of file diff --git a/src/libraries/System.Reflection.Metadata/src/System.Reflection.Metadata.csproj b/src/libraries/System.Reflection.Metadata/src/System.Reflection.Metadata.csproj index e713d6210651f9..266d4c6f55fddd 100644 --- a/src/libraries/System.Reflection.Metadata/src/System.Reflection.Metadata.csproj +++ b/src/libraries/System.Reflection.Metadata/src/System.Reflection.Metadata.csproj @@ -16,6 +16,7 @@ The System.Reflection.Metadata library is built-in as part of the shared framewo $(DefineConstants);FEATURE_CER + $(DefineConstants);SYSTEM_REFLECTION_METADATA @@ -250,7 +251,17 @@ The System.Reflection.Metadata library is built-in as part of the shared framewo + + + + + + + + + + diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/AssemblyNameInfoTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/AssemblyNameInfoTests.cs new file mode 100644 index 00000000000000..230e051ab045cb --- /dev/null +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/AssemblyNameInfoTests.cs @@ -0,0 +1,105 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace System.Reflection.Metadata.Tests.Metadata +{ + public class AssemblyNameInfoTests + { + [Theory] + [InlineData("MyAssemblyName, Version=1.0.0.0, PublicKeyToken=b77a5c561934e089")] + public void WithPublicTokenKey(string fullName) + { + AssemblyName assemblyName = new AssemblyName(fullName); + + AssemblyNameInfo assemblyNameInfo = AssemblyNameInfo.Parse(fullName.AsSpan()); + + Assert.Equal(fullName, assemblyName.FullName); + Assert.Equal(fullName, assemblyNameInfo.FullName); + + Roundtrip(assemblyName); + } + + [Fact] + public void NoPublicKeyOrToken() + { + AssemblyName source = new AssemblyName(); + source.Name = "test"; + source.Version = new Version(1, 2, 3, 4); + source.CultureName = "en-US"; + + Roundtrip(source); + } + + [Theory] + [InlineData(ProcessorArchitecture.MSIL)] + [InlineData(ProcessorArchitecture.X86)] + [InlineData(ProcessorArchitecture.IA64)] + [InlineData(ProcessorArchitecture.Amd64)] + [InlineData(ProcessorArchitecture.Arm)] + public void ProcessorArchitectureIsPropagated(ProcessorArchitecture architecture) + { + string input = $"Abc, ProcessorArchitecture={architecture}"; + AssemblyNameInfo assemblyNameInfo = AssemblyNameInfo.Parse(input.AsSpan()); + + AssemblyName assemblyName = assemblyNameInfo.ToAssemblyName(); + + Assert.Equal(architecture, assemblyName.ProcessorArchitecture); + Assert.Equal(AssemblyContentType.Default, assemblyName.ContentType); + // By design (desktop compat) AssemblyName.FullName and ToString() do not include ProcessorArchitecture. + Assert.Equal(assemblyName.FullName, assemblyNameInfo.FullName); + Assert.DoesNotContain("ProcessorArchitecture", assemblyNameInfo.FullName); + } + + [Fact] + public void AssemblyContentTypeIsPropagated() + { + const string input = "Abc, ContentType=WindowsRuntime"; + AssemblyNameInfo assemblyNameInfo = AssemblyNameInfo.Parse(input.AsSpan()); + + AssemblyName assemblyName = assemblyNameInfo.ToAssemblyName(); + + Assert.Equal(AssemblyContentType.WindowsRuntime, assemblyName.ContentType); + Assert.Equal(ProcessorArchitecture.None, assemblyName.ProcessorArchitecture); + Assert.Equal(input, assemblyNameInfo.FullName); + Assert.Equal(assemblyName.FullName, assemblyNameInfo.FullName); + } + + [Fact] + public void RetargetableIsPropagated() + { + const string input = "Abc, Retargetable=Yes"; + AssemblyNameInfo assemblyNameInfo = AssemblyNameInfo.Parse(input.AsSpan()); + Assert.True((assemblyNameInfo.Flags & AssemblyNameFlags.Retargetable) != 0); + + AssemblyName assemblyName = assemblyNameInfo.ToAssemblyName(); + + Assert.True((assemblyName.Flags & AssemblyNameFlags.Retargetable) != 0); + Assert.Equal(AssemblyContentType.Default, assemblyName.ContentType); + Assert.Equal(ProcessorArchitecture.None, assemblyName.ProcessorArchitecture); + Assert.Equal(input, assemblyNameInfo.FullName); + Assert.Equal(assemblyName.FullName, assemblyNameInfo.FullName); + } + + [Fact] + public void EscapedSquareBracketIsNotAllowedInTheName() + => Assert.False(AssemblyNameInfo.TryParse("Esc\\[aped".AsSpan(), out _)); + + static void Roundtrip(AssemblyName source) + { + AssemblyNameInfo parsed = AssemblyNameInfo.Parse(source.FullName.AsSpan()); + Assert.Equal(source.Name, parsed.Name); + Assert.Equal(source.Version, parsed.Version); + Assert.Equal(source.CultureName, parsed.CultureName); + Assert.Equal(source.FullName, parsed.FullName); + + AssemblyName fromParsed = parsed.ToAssemblyName(); + Assert.Equal(source.Name, fromParsed.Name); + Assert.Equal(source.Version, fromParsed.Version); + Assert.Equal(source.CultureName, fromParsed.CultureName); + Assert.Equal(source.FullName, fromParsed.FullName); + Assert.Equal(source.GetPublicKeyToken(), fromParsed.GetPublicKeyToken()); + } + } +} diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs new file mode 100644 index 00000000000000..b9d23a2be15244 --- /dev/null +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs @@ -0,0 +1,173 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Xunit; + +namespace System.Reflection.Metadata.Tests +{ + public class TypeNameParserHelpersTests + { + [Theory] + [InlineData("A[]", 1, false)] + [InlineData("AB[a,b]", 2, false)] + [InlineData("AB[[a, b],[c,d]]", 2, false)] + [InlineData("12]]", 2, false)] + [InlineData("ABC&", 3, false)] + [InlineData("ABCD*", 4, false)] + [InlineData("ABCDE,otherType]]", 5, false)] + [InlineData("Containing+Nested", 10, true)] + [InlineData("NoSpecial.Characters", 20, false)] + [InlineData("Requires\\+Escaping", 18, false)] + [InlineData("Requires\\[Escaping+Nested", 18, true)] + [InlineData("Worst\\[\\]\\&\\*\\,\\+Case", 21, false)] + [InlineData("EscapingSthThatShouldNotBeEscaped\\A", -1 , false)] + [InlineData("EndsWithEscaping\\", -1, false)] + public void GetFullTypeNameLengthReturnsExpectedValue(string input, int expected, bool expectedIsNested) + { + Assert.Equal(expected, TypeNameParserHelpers.GetFullTypeNameLength(input.AsSpan(), out bool isNested)); + Assert.Equal(expectedIsNested, isNested); + + string withNamespace = $"Namespace1.Namespace2.Namespace3.{input}"; + int expectedWithNamespace = expected < 0 ? expected : expected + withNamespace.Length - input.Length; + Assert.Equal(expectedWithNamespace, TypeNameParserHelpers.GetFullTypeNameLength(withNamespace.AsSpan(), out isNested)); + Assert.Equal(expectedIsNested, isNested); + } + + [Theory] + [InlineData("JustTypeName", "JustTypeName")] + [InlineData("Namespace.TypeName", "TypeName")] + [InlineData("Namespace1.Namespace2.TypeName", "TypeName")] + [InlineData("Namespace.NotNamespace\\.TypeName", "NotNamespace\\.TypeName")] + [InlineData("Namespace1.Namespace2.Containing+Nested", "Nested")] + [InlineData("Namespace1.Namespace2.Not\\+Nested", "Not\\+Nested")] + [InlineData("NotNamespace1\\.NotNamespace2\\.TypeName", "NotNamespace1\\.NotNamespace2\\.TypeName")] + [InlineData("NotNamespace1\\.NotNamespace2\\.Not\\+Nested", "NotNamespace1\\.NotNamespace2\\.Not\\+Nested")] + public void GetNameReturnsJustName(string fullName, string expected) + => Assert.Equal(expected, TypeNameParserHelpers.GetName(fullName.AsSpan()).ToString()); + + [Theory] + [InlineData("simple", "simple")] + [InlineData("simple]", "simple")] + [InlineData("esc\\]aped", "esc\\]aped")] + [InlineData("esc\\]aped]", "esc\\]aped")] + public void GetAssemblyNameCandidateReturnsExpectedValue(string input, string expected) + => Assert.Equal(expected, TypeNameParserHelpers.GetAssemblyNameCandidate(input.AsSpan()).ToString()); + + [Theory] + [InlineData(TypeNameParserHelpers.SZArray, "[]")] + [InlineData(TypeNameParserHelpers.Pointer, "*")] + [InlineData(TypeNameParserHelpers.ByRef, "&")] + [InlineData(1, "[*]")] + [InlineData(2, "[,]")] + [InlineData(3, "[,,]")] + [InlineData(4, "[,,,]")] + public void AppendRankOrModifierStringRepresentationAppendsExpectedString(int input, string expected) + { + ValueStringBuilder builder = new ValueStringBuilder(initialCapacity: 10); + Assert.Equal(expected, TypeNameParserHelpers.GetRankOrModifierStringRepresentation(input, ref builder)); + } + + [Theory] + [InlineData(typeof(List))] + [InlineData(typeof(int?))] + [InlineData(typeof(List))] + [InlineData(typeof(Dictionary))] + [InlineData(typeof(ValueTuple))] + [InlineData(typeof(ValueTuple))] + public void GetGenericTypeFullNameReturnsSameStringAsTypeAPI(Type genericType) + { + TypeName openGenericTypeName = TypeName.Parse(genericType.GetGenericTypeDefinition().FullName.AsSpan()); + ReadOnlySpan genericArgNames = genericType.GetGenericArguments().Select(arg => TypeName.Parse(arg.AssemblyQualifiedName.AsSpan())).ToArray(); + + Assert.Equal(genericType.FullName, TypeNameParserHelpers.GetGenericTypeFullName(openGenericTypeName.FullName.AsSpan(), genericArgNames)); + } + + [Theory] + [InlineData("", false, false, "")] + [InlineData("[", false, false, "[")] // too little to be able to tell + [InlineData("[[", true, true, "")] + [InlineData("[[A],[B]]", true, true, "A],[B]]")] + [InlineData("[ [ A],[B]]", true, true, "A],[B]]")] + [InlineData("[\t[\t \r\nA],[B]]", true, true, "A],[B]]")] // whitespaces other than ' ' + [InlineData("[A,B]", true, false, "A,B]")] + [InlineData("[ A,B]", true, false, "A,B]")] + [InlineData("[]", false, false, "[]")] + [InlineData("[*]", false, false, "[*]")] + [InlineData("[,]", false, false, "[,]")] + [InlineData("[,,]", false, false, "[,,]")] + public void IsBeginningOfGenericAgsHandlesAllCasesProperly(string input, bool expectedResult, bool expectedDoubleBrackets, string expectedConsumedInput) + { + ReadOnlySpan inputSpan = input.AsSpan(); + + Assert.Equal(expectedResult, TypeNameParserHelpers.IsBeginningOfGenericArgs(ref inputSpan, out bool doubleBrackets)); + Assert.Equal(expectedDoubleBrackets, doubleBrackets); + Assert.Equal(expectedConsumedInput, inputSpan.ToString()); + } + + [Theory] + [InlineData("A.B.C", true, null, 5)] + [InlineData("A.B.C\\", false, null, 0)] // invalid type name: ends with escape character + [InlineData("A.B.C\\DoeNotNeedEscaping", false, null, 0)] // invalid type name: escapes non-special character + [InlineData("A.B+C", true, new int[] { 3 }, 5)] + [InlineData("A.B++C", false, null, 0)] // invalid type name: two following, unescaped + + [InlineData("A.B`1", true, null, 5)] + [InlineData("A+B`1+C1`2+DD2`3+E", true, new int[] { 1, 3, 4, 5 }, 18)] + [InlineData("Integer`2147483646+NoOverflow`1", true, new int[] { 18 }, 31)] + [InlineData("Integer`2147483647+Overflow`1", true, new int[] { 18 }, 29)] + public void TryGetTypeNameInfoGetsAllTheInfo(string input, bool expectedResult, int[] expectedNestedNameLengths, int expectedTotalLength) + { + List? nestedNameLengths = null; + ReadOnlySpan span = input.AsSpan(); + bool result = TypeNameParserHelpers.TryGetTypeNameInfo(ref span, ref nestedNameLengths, out int totalLength); + + Assert.Equal(expectedResult, result); + + if (expectedResult) + { + Assert.Equal(expectedNestedNameLengths, nestedNameLengths?.ToArray()); + Assert.Equal(expectedTotalLength, totalLength); + } + } + + [Theory] + [InlineData("*", true, TypeNameParserHelpers.Pointer, "")] + [InlineData(" *", false, default(int), " *")] // Whitespace cannot precede the decorator + [InlineData("* *", true, TypeNameParserHelpers.Pointer, "*")] // but it can follow the decorator. + [InlineData("&", true, TypeNameParserHelpers.ByRef, "")] + [InlineData("\t&", false, default(int), "\t&")] + [InlineData("&\t\r\n[]", true, TypeNameParserHelpers.ByRef, "[]")] + [InlineData("[]", true, TypeNameParserHelpers.SZArray, "")] + [InlineData("\r\n[]", false, default(int), "\r\n[]")] + [InlineData("[] []", true, TypeNameParserHelpers.SZArray, "[]")] + [InlineData("[,]", true, 2, "")] + [InlineData(" [,,,]", false, default(int), " [,,,]")] + [InlineData("[,,,,] *[]", true, 5, "*[]")] + public void TryParseNextDecoratorParsesTheDecoratorAndConsumesFollowingWhitespaces( + string input, bool expectedResult, int expectedModifier, string expectedConsumedInput) + { + ReadOnlySpan inputSpan = input.AsSpan(); + + Assert.Equal(expectedResult, TypeNameParserHelpers.TryParseNextDecorator(ref inputSpan, out int parsedModifier)); + Assert.Equal(expectedModifier, parsedModifier); + Assert.Equal(expectedConsumedInput, inputSpan.ToString()); + } + + [Theory] + [InlineData(" , ", ',', false, " , ")] // it can not start with a whitespace + [InlineData("AB", ',', false, "AB")] // does not start with given character + [InlineData(", ", ',', true, "")] // trimming + [InlineData(",[AB]", ',', true, "[AB]")] // nothing to trim + public void TryStripFirstCharAndTrailingSpacesWorksAsExpected( + string input, char argument, bool expectedResult, string expectedConsumedInput) + { + ReadOnlySpan inputSpan = input.AsSpan(); + + Assert.Equal(expectedResult, TypeNameParserHelpers.TryStripFirstCharAndTrailingSpaces(ref inputSpan, argument)); + Assert.Equal(expectedConsumedInput, inputSpan.ToString()); + } + } +} diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs new file mode 100644 index 00000000000000..0c48cdc2641d97 --- /dev/null +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs @@ -0,0 +1,255 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters.Binary; +using Xunit; + +namespace System.Reflection.Metadata.Tests +{ + public class TypeNameParserSamples + { + internal sealed class SampleSerializationBinder : SerializationBinder + { + private static TypeNameParseOptions _options; + + // we could use Frozen collections here ;) + private readonly static Dictionary _alwaysAllowed = new() + { + { typeof(string).FullName, typeof(string) }, + { typeof(int).FullName, typeof(int) }, + { typeof(uint).FullName, typeof(uint) }, + { typeof(long).FullName, typeof(long) }, + { typeof(ulong).FullName, typeof(ulong) }, + { typeof(double).FullName, typeof(double) }, + { typeof(float).FullName, typeof(float) }, + { typeof(bool).FullName, typeof(bool) }, + { typeof(short).FullName, typeof(short) }, + { typeof(ushort).FullName, typeof(ushort) }, + { typeof(byte).FullName, typeof(byte) }, + { typeof(char).FullName, typeof(char) }, + { typeof(DateTime).FullName, typeof(DateTime) }, + { typeof(TimeSpan).FullName, typeof(TimeSpan) }, + { typeof(Guid).FullName, typeof(Guid) }, + { typeof(Uri).FullName, typeof(Uri) }, + { typeof(DateTimeOffset).FullName, typeof(DateTimeOffset) }, + { typeof(Version).FullName, typeof(Version) }, + { typeof(Nullable).FullName, typeof(Nullable) }, // Nullable is generic! + }; + + private readonly Dictionary? _userDefined; + + public SampleSerializationBinder(Type[]? allowedTypes = null) + => _userDefined = allowedTypes?.ToDictionary(type => type.FullName); + + public override Type? BindToType(string assemblyName, string typeName) + { + // Fast path for common primitive type names and user-defined type names + // that use the same syntax and casing as System.Type.FullName API. + if (TryGetTypeFromFullName(typeName, out Type type)) + { + return type; + } + + _options ??= new TypeNameParseOptions() // there is no need for lazy initialization, I just wanted to have everything important in one method + { + // To prevent from unbounded recursion, we set the max depth for parser options. + // By ensuring that the max depth limit is enforced, we can safely use recursion in + // GetTypeFromParsedTypeName to get arrays of arrays and generics of generics. + MaxNodes = 10 + }; + + if (!TypeName.TryParse(typeName.AsSpan(), out TypeName parsed, _options)) + { + // we can throw any exception, log the information etc + throw new InvalidOperationException($"Invalid type name: '{typeName}'"); + } + + if (parsed.AssemblyName is not null) + { + // The attackers may create such a payload, + // where "typeName" passed to BindToType contains the assembly name + // and "assemblyName" passed to this method contains something else + // (some garbage or a different assembly name). Example: + // typeName: System.Int32, MyHackyDll.dll + // assemblyName: mscorlib.dll + throw new InvalidOperationException($"Type name '{typeName}' contained assembly name."); + } + + return GetTypeFromParsedTypeName(parsed); + } + + private Type? GetTypeFromParsedTypeName(TypeName parsed) + { + if (TryGetTypeFromFullName(parsed.FullName, out Type type)) + { + return type; + } + else if (parsed.IsArray) + { + TypeName arrayElementTypeName = parsed.GetElementType(); + Type arrayElementType = GetTypeFromParsedTypeName(arrayElementTypeName); // recursive call allows for creating arrays of arrays etc + + return parsed.IsSZArray + ? arrayElementType.MakeArrayType() + : arrayElementType.MakeArrayType(parsed.GetArrayRank()); + } + else if (parsed.IsConstructedGenericType) + { + TypeName genericTypeDefinitionName = parsed.GetGenericTypeDefinition(); + Type genericTypeDefinition = GetTypeFromParsedTypeName(genericTypeDefinitionName); + Debug.Assert(genericTypeDefinition.IsGenericTypeDefinition); + + ImmutableArray genericArgs = parsed.GetGenericArguments(); + Type[] typeArguments = new Type[genericArgs.Length]; + for (int i = 0; i < genericArgs.Length; i++) + { + typeArguments[i] = GetTypeFromParsedTypeName(genericArgs[i]); // recursive call allows for generics of generics like "List" + } + return genericTypeDefinition.MakeGenericType(typeArguments); + } + + throw new ArgumentException($"{parsed.FullName} is not on the allow list."); + } + + private bool TryGetTypeFromFullName(string fullName, out Type? type) + => _alwaysAllowed.TryGetValue(fullName, out type) + || (_userDefined is not null && _userDefined.TryGetValue(fullName, out type)); + } + + [Serializable] + public class CustomUserDefinedType + { + public int Integer { get; set; } + public string Text { get; set; } + public List ListOfDates { get; set; } + public CustomUserDefinedType[] ArrayOfCustomUserDefinedTypes { get; set; } + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsBinaryFormatterSupported))] + public void CanDeserializeCustomUserDefinedType() + { + CustomUserDefinedType parent = new() + { + Integer = 1, + Text = "parent", + ListOfDates = new List() + { + DateTime.Parse("02/06/2024") + }, + ArrayOfCustomUserDefinedTypes = new [] + { + new CustomUserDefinedType() + { + Integer = 2, + Text = "child" + } + } + }; + SampleSerializationBinder binder = new( + allowedTypes: + [ + typeof(CustomUserDefinedType), + typeof(List<>) // using List would require using type forwarding info in dictionary + ]); + + CustomUserDefinedType deserialized = SerializeDeserialize(parent, binder); + + Assert.Equal(parent.Integer, deserialized.Integer); + Assert.Equal(parent.Text, deserialized.Text); + Assert.Equal(parent.ListOfDates.Count, deserialized.ListOfDates.Count); + for (int i = 0; i < deserialized.ListOfDates.Count; i++) + { + Assert.Equal(parent.ListOfDates[i], deserialized.ListOfDates[i]); + } + Assert.Equal(parent.ArrayOfCustomUserDefinedTypes.Length, deserialized.ArrayOfCustomUserDefinedTypes.Length); + for (int i = 0; i < deserialized.ArrayOfCustomUserDefinedTypes.Length; i++) + { + Assert.Equal(parent.ArrayOfCustomUserDefinedTypes[i].Integer, deserialized.ArrayOfCustomUserDefinedTypes[i].Integer); + Assert.Equal(parent.ArrayOfCustomUserDefinedTypes[i].Text, deserialized.ArrayOfCustomUserDefinedTypes[i].Text); + } + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsBinaryFormatterSupported))] + public void CanDeserializeDictionaryUsingNonPublicComparerType() + { + Dictionary dictionary = new(StringComparer.CurrentCulture) + { + { "test", 1 } + }; + SampleSerializationBinder binder = new( + allowedTypes: + [ + typeof(Dictionary<,>), // this could be Dictionary to be more strict + StringComparer.CurrentCulture.GetType(), // this type is not public, this is all this test is about + typeof(Globalization.CompareOptions), + typeof(Globalization.CompareInfo), + typeof(KeyValuePair<,>), // this could be KeyValuePair to be more strict + ]); + + Dictionary deserialized = SerializeDeserialize(dictionary, binder); + + Assert.Equal(dictionary, deserialized); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsBinaryFormatterSupported))] + public void CanDeserializeArraysOfArrays() + { + int[][] arrayOfArrays = new int[10][]; + for (int i = 0; i < arrayOfArrays.Length; i++) + { + arrayOfArrays[i] = Enumerable.Repeat(i, 10).ToArray(); + } + + SampleSerializationBinder binder = new(); + int[][] deserialized = SerializeDeserialize(arrayOfArrays, binder); + + Assert.Equal(arrayOfArrays.Length, deserialized.Length); + for (int i = 0; i < arrayOfArrays.Length; i++) + { + Assert.Equal(arrayOfArrays[i], deserialized[i]); + } + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsBinaryFormatterSupported))] + public void CanDeserializeListOfListOfInt() + { + List> listOfListOfInts = new(10); + for (int i = 0; i < listOfListOfInts.Count; i++) + { + listOfListOfInts[i] = Enumerable.Repeat(i, 10).ToList(); + } + + SampleSerializationBinder binder = new(allowedTypes: + [ + typeof(List<>) + ]); + List> deserialized = SerializeDeserialize(listOfListOfInts, binder); + + Assert.Equal(listOfListOfInts.Count, deserialized.Count); + for (int i = 0; i < listOfListOfInts.Count; i++) + { + Assert.Equal(listOfListOfInts[i], deserialized[i]); + } + } + + static T SerializeDeserialize(T instance, SerializationBinder binder) + { + using MemoryStream bfStream = new(); + BinaryFormatter bf = new() + { + Binder = binder + }; + + bf.Serialize(bfStream, instance); + bfStream.Position = 0; + + return (T)bf.Deserialize(bfStream); + } + } +} diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs new file mode 100644 index 00000000000000..53f8e43821bc61 --- /dev/null +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs @@ -0,0 +1,748 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection.Emit; +using Xunit; + +namespace System.Reflection.Metadata.Tests +{ + public class TypeNameTests + { + [Theory] + [InlineData(" System.Int32", "System.Int32", "Int32")] + [InlineData(" MyNamespace.MyType+NestedType", "MyNamespace.MyType+NestedType", "NestedType")] + public void SpacesAtTheBeginningAreOK(string input, string expectedFullName, string expectedName) + { + TypeName parsed = TypeName.Parse(input.AsSpan()); + + Assert.Equal(expectedName, parsed.Name); + Assert.Equal(expectedFullName, parsed.FullName); + Assert.Equal(expectedFullName, parsed.AssemblyQualifiedName); + } + + [Fact] + public void LeadingDotIsNotConsumedForFullTypeNamesWithoutNamespace() + { + // This is true only for the public API. + // The internal CoreLib implementation consumes the leading dot for backward compat. + TypeName parsed = TypeName.Parse(".NoNamespace".AsSpan()); + + Assert.Equal("NoNamespace", parsed.Name); + Assert.Equal(".NoNamespace", parsed.FullName); + Assert.Equal(".NoNamespace", parsed.AssemblyQualifiedName); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(" ")] + public void EmptyStringsAreNotAllowed(string input) + { + Assert.Throws(() => TypeName.Parse(input.AsSpan())); + + Assert.False(TypeName.TryParse(input.AsSpan(), out _)); + } + + [Theory] + [InlineData("Namespace.Containing++Nested")] // a pair of '++' + [InlineData("TypeNameFollowedBySome[] unconsumedCharacters")] + [InlineData("MissingAssemblyName, ")] + [InlineData("ExtraComma, ,")] + [InlineData("ExtraComma, , System.Runtime")] + [InlineData("UsingGenericSyntaxButNotProvidingGenericArgs[[]]")] + [InlineData("ExtraCommaAfterFirstGenericArg`1[[type1, assembly1],]")] + [InlineData("MissingClosingSquareBrackets`1[[type1, assembly1")] // missing ]] + [InlineData("MissingClosingSquareBracket`1[[type1, assembly1]")] // missing ] + [InlineData("MissingClosingSquareBracketsMixedMode`2[[type1, assembly1], type2")] // missing ] + [InlineData("MissingClosingSquareBrackets`2[[type1, assembly1], [type2, assembly2")] // missing ] + [InlineData("MissingClosingSquareBracketsMixedMode`2[type1, [type2, assembly2")] // missing ]] + [InlineData("MissingClosingSquareBracketsMixedMode`2[type1, [type2, assembly2]")] // missing ] + [InlineData("EscapeCharacterAtTheEnd\\")] + [InlineData("EscapeNonSpecialChar\\a")] + [InlineData("EscapeNonSpecialChar\\0")] + [InlineData("DoubleNestingChar++Bla")] + public void InvalidTypeNamesAreNotAllowed(string input) + { + Assert.Throws(() => TypeName.Parse(input.AsSpan())); + + Assert.False(TypeName.TryParse(input.AsSpan(), out _)); + } + + [Theory] + [InlineData("System.Int32&&")] // by-ref to by-ref is currently not supported by CLR + [InlineData("System.Int32[,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,]")] // more than max array rank (32) + public void ParserIsNotEnforcingRuntimeSpecificRules(string input) + { + Assert.True(TypeName.TryParse(input.AsSpan(), out _)); + + if (PlatformDetection.IsNotMonoRuntime) // https://github.com/dotnet/runtime/issues/45033 + { +#if NETCOREAPP + Assert.Throws(() => Type.GetType(input)); +#endif + } + } + + [Theory] + [InlineData("Namespace.Kość", "Namespace.Kość")] + public void UnicodeCharactersAreAllowedByDefault(string input, string expectedFullName) + => Assert.Equal(expectedFullName, TypeName.Parse(input.AsSpan()).FullName); + + [Theory] + [InlineData(typeof(int))] + [InlineData(typeof(Dictionary))] + [InlineData(typeof(int[][]))] + [InlineData(typeof(Assert))] // xUnit assembly + [InlineData(typeof(TypeNameTests))] // test assembly + [InlineData(typeof(NestedGeneric_0.NestedGeneric_1.NestedGeneric_2.NestedNonGeneric_3))] + public void TypeNameCanContainAssemblyName(Type type) + { + AssemblyName expectedAssemblyName = new(type.Assembly.FullName); + + Verify(type, expectedAssemblyName, TypeName.Parse(type.AssemblyQualifiedName.AsSpan())); + + static void Verify(Type type, AssemblyName expectedAssemblyName, TypeName parsed) + { + Assert.Equal(type.AssemblyQualifiedName, parsed.AssemblyQualifiedName); + Assert.Equal(type.FullName, parsed.FullName); + Assert.Equal(type.Name, parsed.Name); + + AssemblyNameInfo parsedAssemblyName = parsed.AssemblyName; + Assert.NotNull(parsedAssemblyName); + + Assert.Equal(expectedAssemblyName.Name, parsedAssemblyName.Name); + Assert.Equal(expectedAssemblyName.Version, parsedAssemblyName.Version); + Assert.Equal(expectedAssemblyName.CultureName, parsedAssemblyName.CultureName); + Assert.Equal(expectedAssemblyName.FullName, parsedAssemblyName.FullName); + + Assert.Equal(default, parsedAssemblyName.Flags); + } + } + + [Theory] + [InlineData(10, "*")] // pointer to pointer + [InlineData(10, "[]")] // array of arrays + [InlineData(100, "*")] + [InlineData(100, "[]")] + public void MaxNodesIsRespected_TooManyDecorators(int maxDepth, string decorator) + { + TypeNameParseOptions options = new() + { + MaxNodes = maxDepth + }; + + string notTooMany = $"System.Int32{string.Join("", Enumerable.Repeat(decorator, maxDepth - 1))}"; + string tooMany = $"System.Int32{string.Join("", Enumerable.Repeat(decorator, maxDepth))}"; + + Assert.Throws(() => TypeName.Parse(tooMany.AsSpan(), options)); + Assert.False(TypeName.TryParse(tooMany.AsSpan(), out _, options)); + + TypeName parsed = TypeName.Parse(notTooMany.AsSpan(), options); + ValidateElementType(maxDepth, parsed, decorator); + + Assert.True(TypeName.TryParse(notTooMany.AsSpan(), out parsed, options)); + ValidateElementType(maxDepth, parsed, decorator); + + static void ValidateElementType(int maxDepth, TypeName parsed, string decorator) + { + for (int i = 0; i < maxDepth - 1; i++) + { + Assert.Equal(decorator == "*", parsed.IsPointer); + Assert.Equal(decorator == "[]", parsed.IsSZArray); + Assert.False(parsed.IsConstructedGenericType); + + parsed = parsed.GetElementType(); + } + } + } + + [Theory] + [InlineData(10)] + [InlineData(100)] + public void MaxNodesIsRespected_TooDeepGenerics(int maxDepth) + { + TypeNameParseOptions options = new() + { + MaxNodes = maxDepth + }; + + string tooDeep = GetName(maxDepth); + string notTooDeep = GetName(maxDepth - 1); + + Assert.Throws(() => TypeName.Parse(tooDeep.AsSpan(), options)); + Assert.False(TypeName.TryParse(tooDeep.AsSpan(), out _, options)); + + TypeName parsed = TypeName.Parse(notTooDeep.AsSpan(), options); + Validate(maxDepth, parsed); + + Assert.True(TypeName.TryParse(notTooDeep.AsSpan(), out parsed, options)); + Validate(maxDepth, parsed); + + static string GetName(int depth) + { + // MakeGenericType is not used here, as it crashes for larger depths + string coreLibName = typeof(object).Assembly.FullName; + string fullName = typeof(int).AssemblyQualifiedName!; + for (int i = 0; i < depth; i++) + { + fullName = $"System.Collections.Generic.List`1[[{fullName}]], {coreLibName}"; + } + return fullName; + } + + static void Validate(int maxDepth, TypeName parsed) + { + for (int i = 0; i < maxDepth - 1; i++) + { + Assert.True(parsed.IsConstructedGenericType); + parsed = parsed.GetGenericArguments()[0]; + } + } + } + + [Theory] + [InlineData(10)] + [InlineData(100)] + public void MaxNodesIsRespected_TooManyGenericArguments(int maxDepth) + { + TypeNameParseOptions options = new() + { + MaxNodes = maxDepth + }; + + string tooMany = GetName(maxDepth); + string notTooMany = GetName(maxDepth - 1); + + Assert.Throws(() => TypeName.Parse(tooMany.AsSpan(), options)); + Assert.False(TypeName.TryParse(tooMany.AsSpan(), out _, options)); + + TypeName parsed = TypeName.Parse(notTooMany.AsSpan(), options); + Validate(parsed, maxDepth); + + Assert.True(TypeName.TryParse(notTooMany.AsSpan(), out parsed, options)); + Validate(parsed, maxDepth); + + static string GetName(int depth) + => $"Some.GenericType`{depth}[{string.Join(",", Enumerable.Repeat("System.Int32", depth))}]"; + + static void Validate(TypeName parsed, int maxDepth) + { + Assert.True(parsed.IsConstructedGenericType); + ImmutableArray genericArgs = parsed.GetGenericArguments(); + Assert.Equal(maxDepth - 1, genericArgs.Length); + Assert.All(genericArgs, arg => Assert.False(arg.IsConstructedGenericType)); + } + } + + public static IEnumerable GenericArgumentsAreSupported_Arguments() + { + yield return new object[] + { + "Generic`1[[A]]", + "Generic`1", + "Generic`1[[A]]", + new string[] { "A" }, + null + }; + yield return new object[] + { + "Generic`1[A]", + "Generic`1", + "Generic`1[[A]]", + new string[] { "A" }, + null + }; + yield return new object[] + { + "Generic`3[[A],[B],[C]]", + "Generic`3", + "Generic`3[[A],[B],[C]]", + new string[] { "A", "B", "C" }, + null + }; + yield return new object[] + { + "Generic`3[A,B,C]", + "Generic`3", + "Generic`3[[A],[B],[C]]", + new string[] { "A", "B", "C" }, + null + }; + yield return new object[] + { + "Generic`1[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", + "Generic`1", + "Generic`1[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", + new string[] { "System.Int32" }, + new AssemblyName[] { new AssemblyName("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") } + }; + yield return new object[] + { + "Generic`2[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.Boolean, mscorlib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", + "Generic`2", + "Generic`2[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.Boolean, mscorlib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", + new string[] { "System.Int32", "System.Boolean" }, + new AssemblyName[] + { + new AssemblyName("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"), + new AssemblyName("mscorlib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + } + }; + yield return new object[] + { + "Generic`2[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089], System.Boolean]", + "Generic`2", + "Generic`2[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.Boolean]]", + new string[] { "System.Int32", "System.Boolean" }, + new AssemblyName[] + { + new AssemblyName("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"), + null + } + }; + yield return new object[] + { + "Generic`2[System.Boolean, [System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", + "Generic`2", + "Generic`2[[System.Boolean],[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", + new string[] { "System.Boolean", "System.Int32" }, + new AssemblyName[] + { + null, + new AssemblyName("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + } + }; + yield return new object[] + { + "Generic`3[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089], System.Boolean, [System.Byte, other, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", + "Generic`3", + "Generic`3[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.Boolean],[System.Byte, other, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", + new string[] { "System.Int32", "System.Boolean", "System.Byte" }, + new AssemblyName[] + { + new AssemblyName("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"), + null, + new AssemblyName("other, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + } + }; + yield return new object[] + { + "Generic`3[System.Boolean, [System.Byte, other, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089], System.Int32]", + "Generic`3", + "Generic`3[[System.Boolean],[System.Byte, other, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.Int32]]", + new string[] { "System.Boolean", "System.Byte", "System.Int32" }, + new AssemblyName[] + { + null, + new AssemblyName("other, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"), + null + } + }; + } + + [Theory] + [MemberData(nameof(GenericArgumentsAreSupported_Arguments))] + public void GenericArgumentsAreSupported(string input, string name, string fullName, string[] genericTypesFullNames, AssemblyName[]? assemblyNames) + { + TypeName parsed = TypeName.Parse(input.AsSpan()); + + Assert.Equal(name, parsed.Name); + Assert.Equal(fullName, parsed.FullName); + Assert.True(parsed.IsConstructedGenericType); + Assert.NotNull(parsed.GetGenericTypeDefinition()); + Assert.False(parsed.IsSimple); + + ImmutableArray typeNames = parsed.GetGenericArguments(); + for (int i = 0; i < genericTypesFullNames.Length; i++) + { + TypeName genericArg = typeNames[i]; + Assert.Equal(genericTypesFullNames[i], genericArg.FullName); + Assert.True(genericArg.IsSimple); + Assert.False(genericArg.IsConstructedGenericType); + Assert.Throws(genericArg.GetGenericTypeDefinition); + + if (assemblyNames is not null) + { + if (assemblyNames[i] is null) + { + Assert.Null(genericArg.AssemblyName); + } + else + { + Assert.Equal(assemblyNames[i].FullName, genericArg.AssemblyName.FullName); + Assert.Equal(assemblyNames[i].Name, genericArg.AssemblyName.Name); + } + } + } + } + + public static IEnumerable DecoratorsAreSupported_Arguments() + { + yield return new object[] + { + "TypeName*", "TypeName", false, false, -1, false, true + }; + yield return new object[] + { + "TypeName&", "TypeName", false, false, -1, true, false + }; + yield return new object[] + { + "TypeName[]", "TypeName", true, true, 1, false, false + }; + yield return new object[] + { + "TypeName[*]", "TypeName", true, false, 1, false, false + }; + yield return new object[] + { + "TypeName[,,,]", "TypeName", true, false, 4, false, false + }; + } + + [Theory] + [MemberData(nameof(DecoratorsAreSupported_Arguments))] + public void DecoratorsAreSupported(string input, string typeNameWithoutDecorators, bool isArray, bool isSzArray, int arrayRank, bool isByRef, bool isPointer) + { + TypeName parsed = TypeName.Parse(input.AsSpan()); + + Assert.Equal(input, parsed.FullName); + Assert.Equal(isArray, parsed.IsArray); + Assert.Equal(isSzArray, parsed.IsSZArray); + if (isArray) + { + Assert.Equal(arrayRank, parsed.GetArrayRank()); + } + else + { + Assert.Throws(() => parsed.GetArrayRank()); + } + Assert.Equal(isByRef, parsed.IsByRef); + Assert.Equal(isPointer, parsed.IsPointer); + Assert.False(parsed.IsSimple); + + TypeName elementType = parsed.GetElementType(); + Assert.NotNull(elementType); + Assert.Equal(typeNameWithoutDecorators, elementType.FullName); + Assert.True(elementType.IsSimple); + Assert.False(elementType.IsArray); + Assert.False(elementType.IsSZArray); + Assert.False(elementType.IsByRef); + Assert.False(elementType.IsPointer); + Assert.Throws(elementType.GetElementType); + } + + public static IEnumerable GetAdditionalConstructedTypeData() + { + yield return new object[] { typeof(Dictionary[,], List>[]), 16 }; + + // "Dictionary[,], List>[]" breaks down to complexity 16 like so: + // + // 01: Dictionary[,], List>[] + // 02: `- Dictionary[,], List> + // 03: +- Dictionary`2 + // 04: +- List[,] + // 05: | `- List + // 06: | +- List`1 + // 07: | `- int[] + // 08: | `- int + // 09: `- List + // 10: +- List`1 + // 11: `- int?[][][,] + // 12: `- int?[][] + // 13: `- int?[] + // 14: `- int? + // 15: +- Nullable`1 + // 16: `- int + + yield return new object[] { typeof(int[]).MakePointerType().MakeByRefType(), 4 }; // int[]*& + yield return new object[] { typeof(long).MakeArrayType(31), 2 }; // long[,,,,,,,...] + yield return new object[] { typeof(long).Assembly.GetType("System.Int64[*]"), 2 }; // long[*] + } + + [Theory] + [InlineData(typeof(TypeName), 1)] + [InlineData(typeof(TypeNameTests), 1)] + [InlineData(typeof(object), 1)] + [InlineData(typeof(Assert), 1)] // xunit + [InlineData(typeof(int[]), 2)] + [InlineData(typeof(int[,][]), 3)] + [InlineData(typeof(Nullable<>), 1)] // open generic type treated as elemental + [InlineData(typeof(NestedNonGeneric_0), 2)] // declaring and nested + [InlineData(typeof(NestedGeneric_0), 3)] // declaring, nested and generic arg + [InlineData(typeof(NestedNonGeneric_0.NestedNonGeneric_1), 3)] // declaring, nested 0 and nested 1 + // TypeNameTests+NestedGeneric_0`1+NestedGeneric_1`2[[Int32],[String],[Boolean]] (simplified for brevity) + [InlineData(typeof(NestedGeneric_0.NestedGeneric_1), 6)] // declaring, nested 0 and nested 1 and 3 generic args + [MemberData(nameof(GetAdditionalConstructedTypeData))] + public void GetNodeCountReturnsExpectedValue(Type type, int expected) + { + TypeName parsed = TypeName.Parse(type.AssemblyQualifiedName.AsSpan()); + + Assert.Equal(expected, parsed.GetNodeCount()); + + Assert.Equal(type.Name, parsed.Name); + Assert.Equal(type.FullName, parsed.FullName); + Assert.Equal(type.AssemblyQualifiedName, parsed.AssemblyQualifiedName); + } + + [Fact] + public void IsSimpleReturnsTrueForNestedNonGenericTypes() + { + Assert.True(TypeName.Parse("Containing+Nested".AsSpan()).IsSimple); + Assert.False(TypeName.Parse(typeof(NestedGeneric_0).FullName.AsSpan()).IsSimple); + } + + [Fact] + public void DeclaringTypeThrowsForNonNestedTypes() + { + TypeName nested = TypeName.Parse("Containing+Nested".AsSpan()); + Assert.True(nested.IsNested); + Assert.Equal("Containing", nested.DeclaringType.Name); + + TypeName notNested = TypeName.Parse("NotNested".AsSpan()); + Assert.False(notNested.IsNested); + Assert.Throws(() => notNested.DeclaringType); + } + + [Theory] + [InlineData("SingleDimensionNonZeroIndexed[*]", true)] + [InlineData("SingleDimensionZeroIndexed[]", false)] + [InlineData("MultiDimensional[,,,,,,]", true)] + public void IsVariableBoundArrayTypeReturnsTrueForNonSZArrays(string typeName, bool expected) + { + TypeName parsed = TypeName.Parse(typeName.AsSpan()); + + Assert.True(parsed.IsArray); + Assert.Equal(expected, parsed.IsVariableBoundArrayType); + Assert.NotEqual(expected, parsed.IsSZArray); + Assert.InRange(parsed.GetArrayRank(), 1, 32); + } + + public static IEnumerable GetTypesThatRequireEscaping() + { + if (PlatformDetection.IsReflectionEmitSupported + && !PlatformDetection.IsMonoRuntime) // Mono does not escape Type.Name + { + AssemblyBuilder assembly = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("TypesThatRequireEscaping"), AssemblyBuilderAccess.Run); + ModuleBuilder module = assembly.DefineDynamicModule("TypesThatRequireEscapingModule"); + + yield return new object[] { module.DefineType("TypeNameWith+ThatIsNotNestedType").CreateType() }; + yield return new object[] { module.DefineType("TypeNameWith\\TheEscapingCharacter").CreateType() }; + yield return new object[] { module.DefineType("TypeNameWith&Ampersand").CreateType() }; + yield return new object[] { module.DefineType("TypeNameWith*Asterisk").CreateType() }; + yield return new object[] { module.DefineType("TypeNameWith[OpeningSquareBracket").CreateType() }; + yield return new object[] { module.DefineType("TypeNameWith]ClosingSquareBracket").CreateType() }; + yield return new object[] { module.DefineType("TypeNameWith[]BothSquareBrackets").CreateType() }; + yield return new object[] { module.DefineType("TypeNameWith[[]]NestedSquareBrackets").CreateType() }; + yield return new object[] { module.DefineType("TypeNameWith,Comma").CreateType() }; + yield return new object[] { module.DefineType("TypeNameWith\\[]+*&,AllSpecialCharacters").CreateType() }; + + TypeBuilder containingType = module.DefineType("ContainingTypeWithA+Plus"); + _ = containingType.CreateType(); // containing type must exist! + yield return new object[] { containingType.DefineNestedType("NoSpecialCharacters").CreateType() }; + yield return new object[] { containingType.DefineNestedType("Contains+Plus").CreateType() }; + } + } + + [Theory] + [InlineData(typeof(List))] + [InlineData(typeof(List>))] + [InlineData(typeof(Dictionary))] + [InlineData(typeof(Dictionary>))] + [InlineData(typeof(NestedGeneric_0.NestedGeneric_1))] + [InlineData(typeof(NestedGeneric_0.NestedGeneric_1.NestedGeneric_2))] + [InlineData(typeof(NestedGeneric_0.NestedGeneric_1.NestedGeneric_2.NestedNonGeneric_3))] + [MemberData(nameof(GetTypesThatRequireEscaping))] + public void ParsedNamesMatchSystemTypeNames(Type type) + { + TypeName parsed = TypeName.Parse(type.AssemblyQualifiedName.AsSpan()); + + Assert.Equal(type.Name, parsed.Name); + Assert.Equal(type.FullName, parsed.FullName); + Assert.Equal(type.AssemblyQualifiedName, parsed.AssemblyQualifiedName); + + if (type.IsGenericType) + { + Type genericType = type.GetGenericTypeDefinition(); + TypeName genericTypeName = parsed.GetGenericTypeDefinition(); + Assert.Equal(genericType.Name, genericTypeName.Name); + Assert.Equal(genericType.FullName, genericTypeName.FullName); + Assert.Equal(genericType.AssemblyQualifiedName, genericTypeName.AssemblyQualifiedName); + } + } + + [Theory] + [InlineData("Name`2[[int], [bool]]", "Name`2")] // match + [InlineData("Name`1[[int], [bool]]", "Name`1")] // less than expected + [InlineData("Name`3[[int], [bool]]", "Name`3")] // more than expected + [InlineData("Name[[int], [bool]]", "Name")] // no backtick at all! + public void TheNumberAfterBacktickDoesNotEnforceGenericArgCount(string input, string expectedName) + { + TypeName parsed = TypeName.Parse(input.AsSpan()); + + Assert.True(parsed.IsConstructedGenericType); + Assert.Equal(expectedName, parsed.Name); + Assert.Equal($"{expectedName}[[int],[bool]]", parsed.FullName); + Assert.Equal("int", parsed.GetGenericArguments()[0].Name); + Assert.Equal("bool", parsed.GetGenericArguments()[1].Name); + } + + [Theory] + [InlineData(typeof(int))] + [InlineData(typeof(int?))] + [InlineData(typeof(int[]))] + [InlineData(typeof(int[,]))] + [InlineData(typeof(int[,,,]))] + [InlineData(typeof(List))] + [InlineData(typeof(List>))] + [InlineData(typeof(Dictionary))] + [InlineData(typeof(Dictionary>))] + [InlineData(typeof(NestedNonGeneric_0))] + [InlineData(typeof(NestedNonGeneric_0.NestedNonGeneric_1))] + [InlineData(typeof(NestedGeneric_0))] + [InlineData(typeof(NestedGeneric_0.NestedGeneric_1))] + [InlineData(typeof(NestedGeneric_0.NestedGeneric_1.NestedGeneric_2))] + [InlineData(typeof(NestedGeneric_0.NestedGeneric_1.NestedGeneric_2.NestedNonGeneric_3))] + public void CanImplementGetTypeUsingPublicAPIs_Roundtrip(Type type) + { + Test(type); + Test(type.MakePointerType()); + Test(type.MakePointerType().MakePointerType()); + Test(type.MakeByRefType()); + + if (!type.IsArray) + { + Test(type.MakeArrayType()); // [] + Test(type.MakeArrayType(1)); // [*] + Test(type.MakeArrayType(2)); // [,] + } + + static void Test(Type type) + { + TypeName parsed = TypeName.Parse(type.AssemblyQualifiedName.AsSpan()); + + // ensure that Name, FullName and AssemblyQualifiedName match reflection APIs!! + Assert.Equal(type.Name, parsed.Name); + Assert.Equal(type.FullName, parsed.FullName); + Assert.Equal(type.AssemblyQualifiedName, parsed.AssemblyQualifiedName); + // now load load the type from name + Verify(type, parsed, ignoreCase: false); +#if NETCOREAPP // something weird is going on here + // load using lowercase name + Verify(type, TypeName.Parse(type.AssemblyQualifiedName.ToLower().AsSpan()), ignoreCase: true); + // load using uppercase name + Verify(type, TypeName.Parse(type.AssemblyQualifiedName.ToUpper().AsSpan()), ignoreCase: true); +#endif + + static void Verify(Type type, TypeName typeName, bool ignoreCase) + { + Type afterRoundtrip = GetType(typeName, throwOnError: true, ignoreCase: ignoreCase); + + Assert.NotNull(afterRoundtrip); + Assert.Equal(type, afterRoundtrip); + } + } + +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode("The type might be removed")] + [RequiresDynamicCode("Required by MakeArrayType")] +#else +#pragma warning disable IL2055, IL2057, IL2075, IL2096 +#endif + static Type? GetType(TypeName typeName, bool throwOnError = true, bool ignoreCase = false) + { + if (typeName.IsNested) + { + BindingFlags flagsCopiedFromClr = BindingFlags.NonPublic | BindingFlags.Public; + if (ignoreCase) + { + flagsCopiedFromClr |= BindingFlags.IgnoreCase; + } + return Make(GetType(typeName.DeclaringType, throwOnError, ignoreCase)?.GetNestedType(typeName.Name, flagsCopiedFromClr)); + } + else if (typeName.IsConstructedGenericType) + { + return Make(GetType(typeName.GetGenericTypeDefinition(), throwOnError, ignoreCase)); + } + else if(typeName.IsArray || typeName.IsPointer || typeName.IsByRef) + { + return Make(GetType(typeName.GetElementType(), throwOnError, ignoreCase)); + } + else + { + Assert.True(typeName.IsSimple); + + AssemblyName? assemblyName = typeName.AssemblyName.ToAssemblyName(); + Type? type = assemblyName is null + ? Type.GetType(typeName.FullName, throwOnError, ignoreCase) + : Assembly.Load(assemblyName).GetType(typeName.FullName, throwOnError, ignoreCase); + + return Make(type); + } + + Type? Make(Type? type) + { + if (type is null || typeName.IsSimple) + { + return type; + } + else if (typeName.IsConstructedGenericType) + { + ImmutableArray genericArgs = typeName.GetGenericArguments(); + Type[] genericTypes = new Type[genericArgs.Length]; + for (int i = 0; i < genericArgs.Length; i++) + { + Type? genericArg = GetType(genericArgs[i], throwOnError, ignoreCase); + if (genericArg is null) + { + return null; + } + genericTypes[i] = genericArg; + } + + return type.MakeGenericType(genericTypes); + } + else if (typeName.IsByRef) + { + return type.MakeByRefType(); + } + else if (typeName.IsPointer) + { + return type.MakePointerType(); + } + else if (typeName.IsSZArray) + { + return type.MakeArrayType(); + } + else + { + Assert.True(typeName.IsVariableBoundArrayType); + + return type.MakeArrayType(rank: typeName.GetArrayRank()); + } + } + } +#pragma warning restore IL2055, IL2057, IL2075, IL2096 + } + + public class NestedNonGeneric_0 + { + public class NestedNonGeneric_1 { } + } + + public class NestedGeneric_0 + { + public class NestedGeneric_1 + { + public class NestedGeneric_2 + { + public class NestedNonGeneric_3 { } + } + } + } + } +} diff --git a/src/libraries/System.Reflection.Metadata/tests/System.Reflection.Metadata.Tests.csproj b/src/libraries/System.Reflection.Metadata/tests/System.Reflection.Metadata.Tests.csproj index fa48b37d26ab82..f739ac0789ff81 100644 --- a/src/libraries/System.Reflection.Metadata/tests/System.Reflection.Metadata.Tests.csproj +++ b/src/libraries/System.Reflection.Metadata/tests/System.Reflection.Metadata.Tests.csproj @@ -25,6 +25,9 @@ Link="Common\Microsoft\Win32\SafeHandles\SafeLibraryHandle.cs" /> + + @@ -36,6 +39,9 @@ + + + diff --git a/src/mono/System.Private.CoreLib/src/System/Reflection/TypeNameParser.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Reflection/TypeNameParser.Mono.cs index 3622b4e27626ac..22a7361d9814e0 100644 --- a/src/mono/System.Private.CoreLib/src/System/Reflection/TypeNameParser.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Reflection/TypeNameParser.Mono.cs @@ -39,32 +39,31 @@ internal unsafe ref partial struct TypeNameParser return null; } - return new TypeNameParser(typeName) + Metadata.TypeName? parsed = Metadata.TypeNameParser.Parse(typeName, throwOnError: throwOnError); + if (parsed is null) + { + return null; + } + + return new TypeNameParser() { _assemblyResolver = assemblyResolver, _typeResolver = typeResolver, _throwOnError = throwOnError, _ignoreCase = ignoreCase, _stackMark = Unsafe.AsPointer(ref stackMark) - }.Parse(); + }.Resolve(parsed); } - private static bool CheckTopLevelAssemblyQualifiedName() + private Assembly? ResolveAssembly(AssemblyName name) { - return true; - } - - private Assembly? ResolveAssembly(string assemblyName) - { - var name = new AssemblyName(assemblyName); - Assembly? assembly; if (_assemblyResolver is not null) { assembly = _assemblyResolver(name); if (assembly is null && _throwOnError) { - throw new FileNotFoundException(SR.Format(SR.FileNotFound_ResolveAssembly, assemblyName)); + throw new FileNotFoundException(SR.Format(SR.FileNotFound_ResolveAssembly, name.FullName)); } } else @@ -96,13 +95,11 @@ private static bool CheckTopLevelAssemblyQualifiedName() Justification = "TypeNameParser.GetType is marked as RequiresUnreferencedCode.")] [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "TypeNameParser.GetType is marked as RequiresUnreferencedCode.")] - private Type? GetType(string typeName, ReadOnlySpan nestedTypeNames, string? assemblyNameIfAny) + private Type? GetType(string escapedTypeName, ReadOnlySpan nestedTypeNames, Metadata.TypeName parsedName) { - Assembly? assembly = (assemblyNameIfAny is not null) ? ResolveAssembly(assemblyNameIfAny) : null; + Assembly? assembly = (parsedName.AssemblyName is not null) ? ResolveAssembly(parsedName.AssemblyName.ToAssemblyName()) : null; // Both the external type resolver and the default type resolvers expect escaped type names - string escapedTypeName = EscapeTypeName(typeName); - Type? type; // Resolve the top level type. @@ -153,7 +150,7 @@ private static bool CheckTopLevelAssemblyQualifiedName() if (_throwOnError) { throw new TypeLoadException(SR.Format(SR.TypeLoad_ResolveNestedType, - nestedTypeNames[i], (i > 0) ? nestedTypeNames[i - 1] : typeName)); + nestedTypeNames[i], (i > 0) ? nestedTypeNames[i - 1] : escapedTypeName)); } return null; }