From 22d131eb5ceed492ddf37d3668013474fd3532c5 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 23 Jan 2024 21:09:01 +0100 Subject: [PATCH 01/48] TypeNameParser first ref and configurable input validation --- .../ref/System.Reflection.Metadata.cs | 25 +++ .../src/System.Reflection.Metadata.csproj | 1 + .../Reflection/Metadata/TypeNameParser.cs | 146 ++++++++++++++++++ .../tests/Metadata/TypeNameParserTests.cs | 52 +++++++ .../System.Reflection.Metadata.Tests.csproj | 1 + 5 files changed, 225 insertions(+) create mode 100644 src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs create mode 100644 src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs 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 d656738565288..8f6b01d5a4dc5 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,31 @@ public readonly partial struct TypeLayout public int PackingSize { get { throw null; } } public int Size { get { throw null; } } } + public readonly partial struct TypeName + { + public string? AssemblyQualifiedName { get { throw null; } } + public bool ContainsGenericParameters { get { throw null; } } + public string? FullName { get { throw null; } } + public System.Reflection.Metadata.TypeName[] GenericTypeArguments { get { throw null; } } + public bool IsArray { get { throw null; } } + public bool IsManagedPointerType { get { throw null; } } + public bool IsNested { get { throw null; } } + public bool IsUnmanagedPointerType { get { throw null; } } + public bool IsVariableBoundArrayType { get { throw null; } } + public string Name { get { throw null; } } + public string? Namespace { get { throw null; } } + public int GetArrayRank() { throw null; } + } + public ref partial struct TypeNameParser + { + public static System.Reflection.Metadata.TypeName Parse(System.ReadOnlySpan name, System.Reflection.Metadata.TypeNameParserOptions? options = null) { throw null; } + } + public partial class TypeNameParserOptions + { + public TypeNameParserOptions() { } + public int MaxRecursiveDepth { get { throw null; } set { } } + public virtual void ValidateIdentifier(string candidate) { } + } public readonly partial struct TypeReference { private readonly object _dummy; 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 e713d6210651f..7a6fe87ef4286 100644 --- a/src/libraries/System.Reflection.Metadata/src/System.Reflection.Metadata.csproj +++ b/src/libraries/System.Reflection.Metadata/src/System.Reflection.Metadata.csproj @@ -77,6 +77,7 @@ The System.Reflection.Metadata library is built-in as part of the shared framewo + diff --git a/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs new file mode 100644 index 0000000000000..008a1330271da --- /dev/null +++ b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs @@ -0,0 +1,146 @@ +// 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; + +namespace System.Reflection.Metadata +{ + public ref struct TypeNameParser + { + private const string EndOfTypeNameDelimiters = "[]&*,"; // TODO: Roslyn is using '+' here as well +#if NET8_0_OR_GREATER + private static readonly SearchValues _endOfTypeNameDelimitersSearchValues = SearchValues.Create(EndOfTypeNameDelimiters); +#endif + + private readonly TypeNameParserOptions _parseOptions; + private ReadOnlySpan _inputString; + + private TypeNameParser(ReadOnlySpan name, TypeNameParserOptions? options) : this() + { + _inputString = name; + _parseOptions = options ?? new(); + } + + public static TypeName Parse(ReadOnlySpan name, TypeNameParserOptions? options = default) + { + TypeNameParser parser = new(name, options); + TypeName typeName = parser.Parse(); + // TODO: throw for non-consumed input like trailing whitespaces + return typeName; + } + + private TypeName Parse() + { + _inputString = _inputString.TrimStart(' '); // spaces at beginning are ok, BTW Roslyn does not need that as their input comes already trimmed + + int offset = GetOffsetOfEndOfTypeName(_inputString); + + string candidate = _inputString.Slice(0, offset).ToString(); + + _parseOptions.ValidateIdentifier(candidate); + + _inputString = _inputString.Slice(offset); + + return new (candidate); + } + + // Normalizes "not found" to input length, since caller is expected to slice. + private static int GetOffsetOfEndOfTypeName(ReadOnlySpan input) + { + // 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(_endOfTypeNameDelimitersSearchValues); +#elif NET6_0_OR_GREATER + int offset = input.IndexOfAny(EndOfTypeNameDelimiters); +#else + int offset; + for (offset = 0; offset < input.Length; offset++) + { + if (EndOfTypeNameDelimiters.IndexOf(input[offset]) >= 0) { break; } + } +#endif + + return (int)Math.Min((uint)offset, (uint)input.Length); + } + } + + public readonly struct TypeName + { + internal TypeName(string? fullName) : this() => FullName = Name = fullName!; + + public string? AssemblyQualifiedName { get; } + public bool ContainsGenericParameters { get; } + public TypeName[] GenericTypeArguments => Array.Empty(); + public string? FullName { get; } + public bool IsArray { get; } + public bool IsVariableBoundArrayType { get; } + public bool IsManagedPointerType { get; } // inconsistent with Type.IsByRef + public bool IsUnmanagedPointerType { get; } // inconsistent with Type.IsPointer + public bool IsNested { get; } // ?? not needed right now? + public string Name { get; } + public string? Namespace { get; } + public int GetArrayRank() => 0; + } + + public class TypeNameParserOptions + { + private int _maxRecursiveDepth = int.MaxValue; + + public int MaxRecursiveDepth + { + get => _maxRecursiveDepth; + set + { +#if NET8_0_OR_GREATER + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(value, 0, nameof(value)); +#endif + + _maxRecursiveDepth = value; + } + } + + public virtual void ValidateIdentifier(string candidate) + { +#if NET8_0_OR_GREATER + ArgumentNullException.ThrowIfNullOrEmpty(candidate, nameof(candidate)); +#endif + } + } + + internal class SafeTypeNameParserOptions : TypeNameParserOptions + { + public SafeTypeNameParserOptions(bool allowNonAsciiIdentifiers) + { + AllowNonAsciiIdentifiers = allowNonAsciiIdentifiers; + MaxRecursiveDepth = 10; + } + + public bool AllowNonAsciiIdentifiers { get; set; } + + public override void ValidateIdentifier(string candidate) + { + base.ValidateIdentifier(candidate); + + // allow specific ASCII chars + } + } + + internal class RoslynTypeNameParserOptions : TypeNameParserOptions + { + public override void ValidateIdentifier(string candidate) + { + // it seems that Roslyn is not performing any kind of validation + } + } +} diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs new file mode 100644 index 0000000000000..dcb8a8b5fee75 --- /dev/null +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using System.Text; +using Xunit; + +namespace System.Reflection.Metadata.Tests.Metadata +{ + public class TypeNameParserTests + { + [Theory] + [InlineData(" System.Int32", "System.Int32")] + public void SpacesAtTheBeginningAreOK(string input, string expectedName) + => Assert.Equal(expectedName, TypeNameParser.Parse(input.AsSpan()).FullName); + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(" ")] + public void EmptyStringsAreNotAllowed(string input) + => Assert.Throws(() => TypeNameParser.Parse(input.AsSpan())); + + [Theory] + [InlineData("Namespace.Kość", "Namespace.Kość")] + public void UnicodeCharactersAreAllowedByDefault(string input, string expectedName) + => Assert.Equal(expectedName, TypeNameParser.Parse(input.AsSpan()).FullName); + + [Theory] + [InlineData("Namespace.Kość")] + public void UsersCanCustomizeIdentifierValidation(string input) + => Assert.Throws(() => TypeNameParser.Parse(input.AsSpan(), new NonAsciiNotAllowed())); + + internal sealed class NonAsciiNotAllowed : TypeNameParserOptions + { + public override void ValidateIdentifier(string candidate) + { + base.ValidateIdentifier(candidate); + +#if NET8_0_OR_GREATER + if (!Ascii.IsValid(candidate)) +#else + if (candidate.Any(c => c >= 128)) +#endif + { + throw new ArgumentException("Non ASCII char found"); + } + } + } + + } +} 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 fa48b37d26ab8..ad8437ac3fcce 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 @@ -36,6 +36,7 @@ + From 5de952627ed2bb1963702355cf602561e1a667ca Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 24 Jan 2024 22:09:22 +0100 Subject: [PATCH 02/48] assembly name parsing --- .../ref/System.Reflection.Metadata.cs | 5 +- .../Reflection/Metadata/TypeNameParser.cs | 48 ++++++++++++++--- .../tests/Metadata/TypeNameParserTests.cs | 51 +++++++++++++++++-- 3 files changed, 90 insertions(+), 14 deletions(-) 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 8f6b01d5a4dc5..1ab3aa78c605a 100644 --- a/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs +++ b/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs @@ -2411,8 +2411,8 @@ public readonly partial struct TypeLayout public readonly partial struct TypeName { public string? AssemblyQualifiedName { get { throw null; } } + public System.Reflection.AssemblyName? AssemblyName { get { throw null; } } public bool ContainsGenericParameters { get { throw null; } } - public string? FullName { get { throw null; } } public System.Reflection.Metadata.TypeName[] GenericTypeArguments { get { throw null; } } public bool IsArray { get { throw null; } } public bool IsManagedPointerType { get { throw null; } } @@ -2420,12 +2420,11 @@ public readonly partial struct TypeName public bool IsUnmanagedPointerType { get { throw null; } } public bool IsVariableBoundArrayType { get { throw null; } } public string Name { get { throw null; } } - public string? Namespace { get { throw null; } } public int GetArrayRank() { throw null; } } public ref partial struct TypeNameParser { - public static System.Reflection.Metadata.TypeName Parse(System.ReadOnlySpan name, System.Reflection.Metadata.TypeNameParserOptions? options = null) { throw null; } + public static System.Reflection.Metadata.TypeName Parse(System.ReadOnlySpan name, bool allowFullyQualifiedName = true, System.Reflection.Metadata.TypeNameParserOptions? options = null) { throw null; } } public partial class TypeNameParserOptions { diff --git a/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs index 008a1330271da..ca6bb24e06c68 100644 --- a/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs +++ b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs @@ -21,15 +21,15 @@ private TypeNameParser(ReadOnlySpan name, TypeNameParserOptions? options) _parseOptions = options ?? new(); } - public static TypeName Parse(ReadOnlySpan name, TypeNameParserOptions? options = default) + public static TypeName Parse(ReadOnlySpan name, bool allowFullyQualifiedName = true, TypeNameParserOptions? options = default) { TypeNameParser parser = new(name, options); - TypeName typeName = parser.Parse(); + TypeName typeName = parser.Parse(allowFullyQualifiedName); // TODO: throw for non-consumed input like trailing whitespaces return typeName; } - private TypeName Parse() + private TypeName Parse(bool allowFullyQualifiedName) { _inputString = _inputString.TrimStart(' '); // spaces at beginning are ok, BTW Roslyn does not need that as their input comes already trimmed @@ -41,7 +41,9 @@ private TypeName Parse() _inputString = _inputString.Slice(offset); - return new (candidate); + AssemblyName? assemblyName = allowFullyQualifiedName ? ParseAssemblyName() : null; + + return new (candidate, assemblyName); } // Normalizes "not found" to input length, since caller is expected to slice. @@ -73,23 +75,53 @@ private static int GetOffsetOfEndOfTypeName(ReadOnlySpan input) return (int)Math.Min((uint)offset, (uint)input.Length); } + + private AssemblyName? ParseAssemblyName() + { + if (!_inputString.IsEmpty && _inputString[0] == ',') + { + _inputString = _inputString.Slice(1).TrimStart(' '); + + // The only delimiter which can terminate an assembly name is ']'. + // Otherwise EOL serves as the terminator. + int assemblyNameLength = (int)Math.Min((uint)_inputString.IndexOf(']'), (uint)_inputString.Length); + + string candidate = _inputString.Slice(0, assemblyNameLength).ToString(); + _inputString = _inputString.Slice(assemblyNameLength); + // we may want to consider throwing a different exception for an empty string here + // TODO: make sure the parsing below is safe for untrusted input + return new AssemblyName(candidate); + } + + return null; + } } public readonly struct TypeName { - internal TypeName(string? fullName) : this() => FullName = Name = fullName!; + internal TypeName(string name, AssemblyName? assemblyName) : this() + { + Name = name; + AssemblyName = assemblyName; + AssemblyQualifiedName = assemblyName is null ? name : $"{name}, {assemblyName.FullName}"; + } - public string? AssemblyQualifiedName { get; } + public string AssemblyQualifiedName { get; } // TODO: do we really need that? + public AssemblyName? AssemblyName { get; } // TODO: AssemblyName is mutable, are we fine with that? Does it not offer too much? public bool ContainsGenericParameters { get; } public TypeName[] GenericTypeArguments => Array.Empty(); - public string? FullName { get; } public bool IsArray { get; } public bool IsVariableBoundArrayType { get; } public bool IsManagedPointerType { get; } // inconsistent with Type.IsByRef public bool IsUnmanagedPointerType { get; } // inconsistent with Type.IsPointer public bool IsNested { get; } // ?? not needed right now? + + /// + /// The 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". + /// public string Name { get; } - public string? Namespace { get; } + public int GetArrayRank() => 0; } diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs index dcb8a8b5fee75..f5e5b84b1a215 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs @@ -1,6 +1,7 @@ // 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.Linq; using System.Text; using Xunit; @@ -12,7 +13,7 @@ public class TypeNameParserTests [Theory] [InlineData(" System.Int32", "System.Int32")] public void SpacesAtTheBeginningAreOK(string input, string expectedName) - => Assert.Equal(expectedName, TypeNameParser.Parse(input.AsSpan()).FullName); + => Assert.Equal(expectedName, TypeNameParser.Parse(input.AsSpan()).Name); [Theory] [InlineData("")] @@ -24,12 +25,56 @@ public void EmptyStringsAreNotAllowed(string input) [Theory] [InlineData("Namespace.Kość", "Namespace.Kość")] public void UnicodeCharactersAreAllowedByDefault(string input, string expectedName) - => Assert.Equal(expectedName, TypeNameParser.Parse(input.AsSpan()).FullName); + => Assert.Equal(expectedName, TypeNameParser.Parse(input.AsSpan()).Name); [Theory] [InlineData("Namespace.Kość")] public void UsersCanCustomizeIdentifierValidation(string input) - => Assert.Throws(() => TypeNameParser.Parse(input.AsSpan(), new NonAsciiNotAllowed())); + => Assert.Throws(() => TypeNameParser.Parse(input.AsSpan(), true, new NonAsciiNotAllowed())); + + public static IEnumerable TypeNamesWithAssemblyNames() + { + yield return new object[] + { + "System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", + "System.Int32", + "mscorlib", + new Version(4, 0, 0, 0), + "b77a5c561934e089" + }; + } + + [Theory] + [MemberData(nameof(TypeNamesWithAssemblyNames))] + public void TypeNameCanContainAssemblyName(string input, string typeName, string assemblyName, Version assemblyVersion, string assemblyPublicKeyToken) + { + TypeName parsed = TypeNameParser.Parse(input.AsSpan(), allowFullyQualifiedName: true); + + Assert.Equal(typeName, parsed.Name); + Assert.NotNull(parsed.AssemblyName); + Assert.Equal(assemblyName, parsed.AssemblyName.Name); + Assert.Equal(assemblyVersion, parsed.AssemblyName.Version); + Assert.Equal(GetPublicKeyToken(assemblyPublicKeyToken), parsed.AssemblyName.GetPublicKeyToken()); + + static byte[] GetPublicKeyToken(string assemblyPublicKeyToken) + { + byte[] pkt = new byte[assemblyPublicKeyToken.Length / 2]; + int srcIndex = 0; + for (int i = 0; i < pkt.Length; i++) + { + char hi = assemblyPublicKeyToken[srcIndex++]; + char lo = assemblyPublicKeyToken[srcIndex++]; + pkt[i] = (byte)((FromHexChar(hi) << 4) | FromHexChar(lo)); + } + return pkt; + } + + static byte FromHexChar(char hex) + { + if (hex >= '0' && hex <= '9') return (byte)(hex - '0'); + else return (byte)(hex - 'a' + 10); + } + } internal sealed class NonAsciiNotAllowed : TypeNameParserOptions { From 6dbcd59d0f990b8d3e6bf142ae97bd35f0153fc5 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Thu, 25 Jan 2024 13:41:41 +0100 Subject: [PATCH 03/48] initial generic type info parsing --- .../ref/System.Reflection.Metadata.cs | 12 +- .../Reflection/Metadata/TypeNameParser.cs | 232 ++++++++++++++++-- .../tests/Metadata/TypeNameParserTests.cs | 33 ++- 3 files changed, 253 insertions(+), 24 deletions(-) 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 1ab3aa78c605a..66adf9216ead2 100644 --- a/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs +++ b/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs @@ -2408,19 +2408,21 @@ public readonly partial struct TypeLayout public int PackingSize { get { throw null; } } public int Size { get { throw null; } } } - public readonly partial struct TypeName + public sealed class TypeName { - public string? AssemblyQualifiedName { get { throw null; } } + public string AssemblyQualifiedName { get { throw null; } } public System.Reflection.AssemblyName? AssemblyName { get { throw null; } } - public bool ContainsGenericParameters { get { throw null; } } - public System.Reflection.Metadata.TypeName[] GenericTypeArguments { get { throw null; } } public bool IsArray { get { throw null; } } + public bool IsConstructedGenericType { get { throw null; } } + public bool IsElementalType { get { throw null; } } public bool IsManagedPointerType { get { throw null; } } - public bool IsNested { get { throw null; } } public bool IsUnmanagedPointerType { get { throw null; } } public bool IsVariableBoundArrayType { get { throw null; } } public string Name { get { throw null; } } + public TypeName? UnderlyingType { get { throw null; } } public int GetArrayRank() { throw null; } + public System.Reflection.Metadata.TypeName[] GetGenericArguments() { throw null; } + } public ref partial struct TypeNameParser { diff --git a/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs index ca6bb24e06c68..dbd237b5da2aa 100644 --- a/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs +++ b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; namespace System.Reflection.Metadata { @@ -24,14 +26,26 @@ private TypeNameParser(ReadOnlySpan name, TypeNameParserOptions? options) public static TypeName Parse(ReadOnlySpan name, bool allowFullyQualifiedName = true, TypeNameParserOptions? options = default) { TypeNameParser parser = new(name, options); - TypeName typeName = parser.Parse(allowFullyQualifiedName); - // TODO: throw for non-consumed input like trailing whitespaces + + int recursiveDepth = 0; + TypeName typeName = parser.ParseNextTypeName(allowFullyQualifiedName, ref recursiveDepth); + if (!parser._inputString.IsEmpty) + { + ThrowInvalidTypeName(); + } + return typeName; } - private TypeName Parse(bool allowFullyQualifiedName) + public override string ToString() => _inputString.ToString(); + + private TypeName ParseNextTypeName(bool allowFullyQualifiedName, ref int recursiveDepth) { - _inputString = _inputString.TrimStart(' '); // spaces at beginning are ok, BTW Roslyn does not need that as their input comes already trimmed + System.Diagnostics.Debugger.Launch(); + + Dive(ref recursiveDepth); + + _inputString = _inputString.TrimStart(' '); // spaces at beginning are always OK int offset = GetOffsetOfEndOfTypeName(_inputString); @@ -41,9 +55,78 @@ private TypeName Parse(bool allowFullyQualifiedName) _inputString = _inputString.Slice(offset); + List? genericArgs = null; + ReadOnlySpan capturedBeforeGenericProcessing = _inputString; + if (_inputString.Length > 2 && _inputString[0] == '[') + { + // Are there any captured generic args? We'll look for "[[". + // 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. + _inputString = _inputString.Slice(1).TrimStart(' '); + + if (_inputString.Length > 1 && _inputString[0] == '[') + { + _inputString = _inputString.Slice(1); // the next call to ParseNextTypeName is going to trim the starting spaces + + int startingRecursionCheck = recursiveDepth; + int maxObservedRecursionCheck = recursiveDepth; + + ParseAnotherGenericArg: + + recursiveDepth = startingRecursionCheck; + TypeName genericArg = ParseNextTypeName(allowFullyQualifiedName: true, ref recursiveDepth); + if (recursiveDepth > maxObservedRecursionCheck) + { + maxObservedRecursionCheck = recursiveDepth; + } + + // There had better be a ']' after the type name. + if (_inputString.IsEmpty || _inputString[0] != ']') + { + ThrowInvalidTypeName(); + } + + (genericArgs ??= new()).Add(genericArg); + + // Is there a ',[' indicating another generic type arg? + if (!_inputString.IsEmpty && _inputString[0] == ',') + { + _inputString = _inputString.TrimStart(' '); + if (_inputString.IsEmpty || _inputString[0] != '[') + { + ThrowInvalidTypeName(); + } + + goto ParseAnotherGenericArg; + } + + // The only other allowable character is ']', indicating the end of + // the generic type arg list. + if (_inputString.IsEmpty || _inputString[0] != ']') + { + ThrowInvalidTypeName(); + } + + // And now that we're at the end, restore the max observed recursion count. + recursiveDepth = maxObservedRecursionCheck; + } + } + + // 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 = capturedBeforeGenericProcessing; + } + + // Strip off decorators one at a time, bumping the recursive depth each time. + // TODO + AssemblyName? assemblyName = allowFullyQualifiedName ? ParseAssemblyName() : null; - return new (candidate, assemblyName); + return new(candidate, assemblyName, 0, null, genericArgs?.ToArray()); } // Normalizes "not found" to input length, since caller is expected to slice. @@ -87,34 +170,120 @@ private static int GetOffsetOfEndOfTypeName(ReadOnlySpan input) int assemblyNameLength = (int)Math.Min((uint)_inputString.IndexOf(']'), (uint)_inputString.Length); string candidate = _inputString.Slice(0, assemblyNameLength).ToString(); - _inputString = _inputString.Slice(assemblyNameLength); + // we may want to consider throwing a different exception for an empty string here // TODO: make sure the parsing below is safe for untrusted input - return new AssemblyName(candidate); + + try + { + AssemblyName result = new(candidate); + _inputString = _inputString.Slice(assemblyNameLength); + return result; + } + catch (Exception) // TODO: handle invalid assembly names without exceptions + { + return null; + } } return null; } + + private void Dive(ref int depth) + { + if (depth >= _parseOptions.MaxRecursiveDepth) + { + Throw(); + } + depth++; + + [DoesNotReturn] + static void Throw() => throw new InvalidOperationException("SR.RecursionCheck_MaxDepthExceeded"); + } + + [DoesNotReturn] + private static void ThrowInvalidTypeName() => throw new ArgumentException("SR.Argument_InvalidTypeName"); } - public readonly struct TypeName + public sealed class TypeName { - internal TypeName(string name, AssemblyName? assemblyName) : this() + internal const int Pointer = -2; + internal const int ByRef = -3; + + // Positive value is array rank. + // Negative value is modifier encoded using constants above. + private readonly int _rankOrModifier; + private readonly TypeName[]? _genericArguments; + + internal TypeName(string name, AssemblyName? assemblyName, int rankOrModifier, TypeName? underlyingType = default, TypeName[]? genericTypeArguments = null) { Name = name; AssemblyName = assemblyName; + _rankOrModifier = rankOrModifier; + UnderlyingType = underlyingType; + _genericArguments = genericTypeArguments; AssemblyQualifiedName = assemblyName is null ? name : $"{name}, {assemblyName.FullName}"; } - public string AssemblyQualifiedName { get; } // TODO: do we really need that? + /// + /// The assembly-qualified name of the type; e.g., "System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089". + /// + /// + /// If is null, simply returns . + /// + public string AssemblyQualifiedName { get; } + + /// + /// The assembly which contains this type, or null if this was not + /// created from a fully-qualified name. + /// public AssemblyName? AssemblyName { get; } // TODO: AssemblyName is mutable, are we fine with that? Does it not offer too much? - public bool ContainsGenericParameters { get; } - public TypeName[] GenericTypeArguments => Array.Empty(); - public bool IsArray { get; } - public bool IsVariableBoundArrayType { get; } - public bool IsManagedPointerType { get; } // inconsistent with Type.IsByRef - public bool IsUnmanagedPointerType { get; } // inconsistent with Type.IsPointer - public bool IsNested { get; } // ?? not needed right now? + + /// + /// Returns true if this type represents any kind of array, regardless of the array's + /// rank or its bounds. + /// + public bool IsArray => _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 => _genericArguments is not null; + + /// + /// Returns true if this is a "plain" type; that is, not an array, not a pointer, 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 ( returns null). + /// 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 IsElementalType => UnderlyingType is null; + + /// + /// 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; + + /// + /// Returns true if this is a managed pointer type (e.g., "ref int"). + /// Managed pointer types are sometimes called byref types () + /// + public bool IsManagedPointerType => _rankOrModifier == ByRef; // name inconsistent with Type.IsByRef + + /// + /// Returns true if this type represents an unmanaged pointer (e.g., "int*" or "void*"). + /// Unmanaged pointer types are often just called pointers () + /// + public bool IsUnmanagedPointerType => _rankOrModifier == Pointer;// name inconsistent with Type.IsPointer /// /// The name of this type, including namespace, but without the assembly name; e.g., "System.Int32". @@ -122,7 +291,34 @@ internal TypeName(string name, AssemblyName? assemblyName) : this() /// public string Name { get; } - public int GetArrayRank() => 0; + /// + /// If this type is not an elemental type (see ), gets + /// the underlying type. If this type is an elemental type, returns null. + /// + /// + /// For example, given "int[][]", unwraps the outermost array and returns "int[]". + /// Given "Dictionary<string, int>", returns the generic type definition "Dictionary<,>". + /// + public TypeName? UnderlyingType { get; } + + public int GetArrayRank() + => _rankOrModifier > 0 + ? _rankOrModifier + : throw new ArgumentException("SR.Argument_HasToBeArrayClass"); // TODO: use actual resource (used by Type.GetArrayRank) + + /// + /// 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. + /// The caller controls the returned array and may mutate it freely. + /// + public TypeName[] GetGenericArguments() + => _genericArguments is not null + ? (TypeName[])_genericArguments.Clone() // we return a copy on purpose, to not allow for mutations. TODO: consider returning a ROS + : Array.Empty(); // TODO: should we throw (Levi's parser throws InvalidOperationException in such case), Type.GetGenericArguments just returns an empty array } public class TypeNameParserOptions diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs index f5e5b84b1a215..249d4cb04a2d4 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs @@ -40,13 +40,14 @@ public static IEnumerable TypeNamesWithAssemblyNames() "System.Int32", "mscorlib", new Version(4, 0, 0, 0), + "", "b77a5c561934e089" }; } [Theory] [MemberData(nameof(TypeNamesWithAssemblyNames))] - public void TypeNameCanContainAssemblyName(string input, string typeName, string assemblyName, Version assemblyVersion, string assemblyPublicKeyToken) + public void TypeNameCanContainAssemblyName(string input, string typeName, string assemblyName, Version assemblyVersion, string assemblyCulture, string assemblyPublicKeyToken) { TypeName parsed = TypeNameParser.Parse(input.AsSpan(), allowFullyQualifiedName: true); @@ -54,6 +55,7 @@ public void TypeNameCanContainAssemblyName(string input, string typeName, string Assert.NotNull(parsed.AssemblyName); Assert.Equal(assemblyName, parsed.AssemblyName.Name); Assert.Equal(assemblyVersion, parsed.AssemblyName.Version); + Assert.Equal(assemblyCulture, parsed.AssemblyName.CultureName); Assert.Equal(GetPublicKeyToken(assemblyPublicKeyToken), parsed.AssemblyName.GetPublicKeyToken()); static byte[] GetPublicKeyToken(string assemblyPublicKeyToken) @@ -76,6 +78,35 @@ static byte FromHexChar(char hex) } } + public static IEnumerable SimpleGenericTypes() + { + yield return new object[] + { + "System.Collections.Generic.List[[System.Int32,System.UInt32,System.Boolean]]", + "System.Collections.Generic.List", + new string[] { "System.Int32", "System.UInt32", "System.Boolean" } + }; + } + + [Theory] + [MemberData(nameof(SimpleGenericTypes))] + public void GenericArgumentsAreSupported(string input, string typeName, string[] typeNames) + { + TypeName parsed = TypeNameParser.Parse(input.AsSpan(), allowFullyQualifiedName: true); + + Assert.Equal(typeName, parsed.Name); + Assert.True(parsed.IsConstructedGenericType); + Assert.False(parsed.IsElementalType); + + for (int i = 0; i < typeNames.Length; i++) + { + TypeName genericArg = parsed.GetGenericArguments()[i]; + Assert.Equal(typeName, genericArg.Name); + Assert.True(genericArg.IsElementalType); + Assert.False(genericArg.IsConstructedGenericType); + } + } + internal sealed class NonAsciiNotAllowed : TypeNameParserOptions { public override void ValidateIdentifier(string candidate) From 6f1f1611ad8941df7d809b2d5963fea6a1d15c94 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 26 Jan 2024 12:57:10 +0100 Subject: [PATCH 04/48] decorator parsing --- .../Reflection/Metadata/TypeNameParser.cs | 250 +++++++++++++----- .../tests/Metadata/TypeNameParserTests.cs | 88 +++++- 2 files changed, 258 insertions(+), 80 deletions(-) diff --git a/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs index dbd237b5da2aa..30ff4d3f84c12 100644 --- a/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs +++ b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs @@ -4,6 +4,7 @@ using System.Buffers; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Text; namespace System.Reflection.Metadata { @@ -19,7 +20,7 @@ public ref struct TypeNameParser private TypeNameParser(ReadOnlySpan name, TypeNameParserOptions? options) : this() { - _inputString = name; + _inputString = name.TrimStart(' '); // spaces at beginning are always OK; _parseOptions = options ?? new(); } @@ -37,80 +38,70 @@ public static TypeName Parse(ReadOnlySpan name, bool allowFullyQualifiedNa return typeName; } - public override string ToString() => _inputString.ToString(); + public override string ToString() => _inputString.ToString(); // TODO: add proper debugger display stuff private TypeName ParseNextTypeName(bool allowFullyQualifiedName, ref int recursiveDepth) { - System.Diagnostics.Debugger.Launch(); - Dive(ref recursiveDepth); - _inputString = _inputString.TrimStart(' '); // spaces at beginning are always OK - int offset = GetOffsetOfEndOfTypeName(_inputString); - string candidate = _inputString.Slice(0, offset).ToString(); + string typeName = _inputString.Slice(0, offset).ToString(); - _parseOptions.ValidateIdentifier(candidate); + _parseOptions.ValidateIdentifier(typeName); _inputString = _inputString.Slice(offset); - List? genericArgs = null; - ReadOnlySpan capturedBeforeGenericProcessing = _inputString; - if (_inputString.Length > 2 && _inputString[0] == '[') + List? genericArgs = null; // TODO: use some stack-based list in CoreLib + + // Are there any captured generic args? We'll look for "[[". + // 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 (TryStripFirstCharAndTrailingSpaces(ref _inputString, '[') + && TryStripFirstCharAndTrailingSpaces(ref _inputString, '[')) { - // Are there any captured generic args? We'll look for "[[". - // 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. - _inputString = _inputString.Slice(1).TrimStart(' '); + int startingRecursionCheck = recursiveDepth; + int maxObservedRecursionCheck = recursiveDepth; - if (_inputString.Length > 1 && _inputString[0] == '[') - { - _inputString = _inputString.Slice(1); // the next call to ParseNextTypeName is going to trim the starting spaces + ParseAnotherGenericArg: - int startingRecursionCheck = recursiveDepth; - int maxObservedRecursionCheck = recursiveDepth; + recursiveDepth = startingRecursionCheck; + TypeName genericArg = ParseNextTypeName(allowFullyQualifiedName: true, ref recursiveDepth); // generic args always allow AQNs + if (recursiveDepth > maxObservedRecursionCheck) + { + maxObservedRecursionCheck = recursiveDepth; + } - ParseAnotherGenericArg: + // There had better be a ']' after the type name. + if (!TryStripFirstCharAndTrailingSpaces(ref _inputString, ']')) + { + ThrowInvalidTypeName(); + } - recursiveDepth = startingRecursionCheck; - TypeName genericArg = ParseNextTypeName(allowFullyQualifiedName: true, ref recursiveDepth); - if (recursiveDepth > maxObservedRecursionCheck) - { - maxObservedRecursionCheck = recursiveDepth; - } + (genericArgs ??= new()).Add(genericArg); - // There had better be a ']' after the type name. - if (_inputString.IsEmpty || _inputString[0] != ']') + // Is there a ',[' indicating another generic type arg? + if (TryStripFirstCharAndTrailingSpaces(ref _inputString, ',')) + { + if (!TryStripFirstCharAndTrailingSpaces(ref _inputString, '[')) { ThrowInvalidTypeName(); } - (genericArgs ??= new()).Add(genericArg); - - // Is there a ',[' indicating another generic type arg? - if (!_inputString.IsEmpty && _inputString[0] == ',') - { - _inputString = _inputString.TrimStart(' '); - if (_inputString.IsEmpty || _inputString[0] != '[') - { - ThrowInvalidTypeName(); - } - - goto ParseAnotherGenericArg; - } - - // The only other allowable character is ']', indicating the end of - // the generic type arg list. - if (_inputString.IsEmpty || _inputString[0] != ']') - { - ThrowInvalidTypeName(); - } + goto ParseAnotherGenericArg; + } - // And now that we're at the end, restore the max observed recursion count. - recursiveDepth = maxObservedRecursionCheck; + // The only other allowable character is ']', indicating the end of + // the generic type arg list. + if (!TryStripFirstCharAndTrailingSpaces(ref _inputString, ']')) + { + ThrowInvalidTypeName(); } + + // And now that we're at the end, restore the max observed recursion count. + recursiveDepth = maxObservedRecursionCheck; } // If there was an error stripping the generic args, back up to @@ -118,15 +109,46 @@ private TypeName ParseNextTypeName(bool allowFullyQualifiedName, ref int recursi // parser try handling it. if (genericArgs is null) { - _inputString = capturedBeforeGenericProcessing; + _inputString = capturedBeforeProcessing; } - // Strip off decorators one at a time, bumping the recursive depth each time. - // TODO + 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)) + { + Dive(ref recursiveDepth); + + if (previousDecorator == TypeName.ByRef) // it's illegal for managed reference to be followed by any other decorator + { + ThrowInvalidTypeName(); + } + previousDecorator = parsedDecorator; + } AssemblyName? assemblyName = allowFullyQualifiedName ? ParseAssemblyName() : null; - return new(candidate, assemblyName, 0, null, genericArgs?.ToArray()); + TypeName result = new(typeName, assemblyName, rankOrModifier: 0, underlyingType: null, genericArgs?.ToArray()); + + if (previousDecorator != default) // some decorators were recognized + { + while (TryParseNextDecorator(ref capturedBeforeProcessing, out int parsedModifier)) + { + // we are not reusing the input string, as it could have contain whitespaces that we want to exclude + string trimmedModifier = parsedModifier switch + { + TypeName.ByRef => "&", + TypeName.Pointer => "*", + 1 => "[]", + _ => ArrayRankToString(parsedModifier) + }; ; + typeName += trimmedModifier; + result = new(typeName, assemblyName, parsedModifier, underlyingType: result); + } + } + + return result; } // Normalizes "not found" to input length, since caller is expected to slice. @@ -161,29 +183,17 @@ private static int GetOffsetOfEndOfTypeName(ReadOnlySpan input) private AssemblyName? ParseAssemblyName() { - if (!_inputString.IsEmpty && _inputString[0] == ',') + if (TryStripFirstCharAndTrailingSpaces(ref _inputString, ',')) { - _inputString = _inputString.Slice(1).TrimStart(' '); - // The only delimiter which can terminate an assembly name is ']'. // Otherwise EOL serves as the terminator. int assemblyNameLength = (int)Math.Min((uint)_inputString.IndexOf(']'), (uint)_inputString.Length); string candidate = _inputString.Slice(0, assemblyNameLength).ToString(); - + _inputString = _inputString.Slice(assemblyNameLength); // we may want to consider throwing a different exception for an empty string here // TODO: make sure the parsing below is safe for untrusted input - - try - { - AssemblyName result = new(candidate); - _inputString = _inputString.Slice(assemblyNameLength); - return result; - } - catch (Exception) // TODO: handle invalid assembly names without exceptions - { - return null; - } + return new AssemblyName(candidate); } return null; @@ -203,6 +213,100 @@ private void Dive(ref int depth) [DoesNotReturn] private static void ThrowInvalidTypeName() => throw new ArgumentException("SR.Argument_InvalidTypeName"); + + private static bool TryStripFirstCharAndTrailingSpaces(ref ReadOnlySpan span, char value) + { + if (!span.IsEmpty && span[0] == value) + { + span = span.Slice(1).Trim(' '); + return true; + } + return false; + } + + private 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 = TypeName.Pointer; + return true; + } + + if (TryStripFirstCharAndTrailingSpaces(ref input, '&')) + { + rankOrModifier = TypeName.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; + //return (rank == 1 && !hasSeenAsterisk) + // ? TypeIdDecorator.SzArray + // : TypeIdDecorator.MdArray(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; + } + + private static string ArrayRankToString(int arrayRank) + { +#if NET8_0_OR_GREATER + return string.Create(2 + arrayRank - 1, arrayRank, (buffer, rank) => + { + buffer[0] = '['; + for (int i = 1; i < arrayRank; i++) + buffer[i] = ','; + buffer[^1] = ']'; + }); +#else + StringBuilder sb = new(2 + arrayRank - 1); + sb.Append('['); + for (int i = 1; i < arrayRank; i++) + sb.Append(','); + sb.Append(']'); + return sb.ToString(); +#endif + } } public sealed class TypeName @@ -215,7 +319,7 @@ public sealed class TypeName private readonly int _rankOrModifier; private readonly TypeName[]? _genericArguments; - internal TypeName(string name, AssemblyName? assemblyName, int rankOrModifier, TypeName? underlyingType = default, TypeName[]? genericTypeArguments = null) + internal TypeName(string name, AssemblyName? assemblyName, int rankOrModifier, TypeName? underlyingType = default, TypeName[]? genericTypeArguments = default) { Name = name; AssemblyName = assemblyName; @@ -265,7 +369,7 @@ internal TypeName(string name, AssemblyName? assemblyName, int rankOrModifier, T /// This is because determining whether a type truly is a generic type requires loading the type /// and performing a runtime check. /// - public bool IsElementalType => UnderlyingType is null; + public bool IsElementalType => UnderlyingType is null && !IsConstructedGenericType; /// /// Returns true if this type represents a variable-bound array; that is, an array of rank greater diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs index 249d4cb04a2d4..3619fa0912868 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs @@ -78,19 +78,45 @@ static byte FromHexChar(char hex) } } - public static IEnumerable SimpleGenericTypes() + public static IEnumerable GenericArgumentsAreSupported_Arguments() { yield return new object[] { - "System.Collections.Generic.List[[System.Int32,System.UInt32,System.Boolean]]", - "System.Collections.Generic.List", - new string[] { "System.Int32", "System.UInt32", "System.Boolean" } + "Generic`1[[A]]", + "Generic`1", + new string[] { "A" }, + null + }; + yield return new object[] + { + "Generic`3[[A],[B],[C]]", + "Generic`3", + 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", + 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", + 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") + } }; } [Theory] - [MemberData(nameof(SimpleGenericTypes))] - public void GenericArgumentsAreSupported(string input, string typeName, string[] typeNames) + [MemberData(nameof(GenericArgumentsAreSupported_Arguments))] + public void GenericArgumentsAreSupported(string input, string typeName, string[] typeNames, AssemblyName[]? assemblyNames) { TypeName parsed = TypeNameParser.Parse(input.AsSpan(), allowFullyQualifiedName: true); @@ -101,12 +127,60 @@ public void GenericArgumentsAreSupported(string input, string typeName, string[] for (int i = 0; i < typeNames.Length; i++) { TypeName genericArg = parsed.GetGenericArguments()[i]; - Assert.Equal(typeName, genericArg.Name); + Assert.Equal(typeNames[i], genericArg.Name); Assert.True(genericArg.IsElementalType); Assert.False(genericArg.IsConstructedGenericType); + + if (assemblyNames is not null) + { + Assert.Equal(assemblyNames[i].FullName, genericArg.AssemblyName.FullName); + } } } + public static IEnumerable DecoratorsAreSupported_Arguments() + { + yield return new object[] + { + "TypeName*", "TypeName", false, -1, false, true + }; + yield return new object[] + { + "TypeName&", "TypeName", false, -1, true, false + }; + yield return new object[] + { + "TypeName[]", "TypeName", true, 1, false, false + }; + yield return new object[] + { + "TypeName[,,,]", "TypeName", true, 4, false, false + }; + } + + [Theory] + [MemberData(nameof(DecoratorsAreSupported_Arguments))] + public void DecoratorsAreSupported(string input, string typeNameWithoutDecorators, bool isArray, int arrayRank, bool isByRef, bool isPointer) + { + TypeName parsed = TypeNameParser.Parse(input.AsSpan(), allowFullyQualifiedName: true); + + Assert.Equal(input, parsed.Name); + Assert.Equal(isArray, parsed.IsArray); + if (isArray) Assert.Equal(arrayRank, parsed.GetArrayRank()); + Assert.Equal(isByRef, parsed.IsManagedPointerType); + Assert.Equal(isPointer, parsed.IsUnmanagedPointerType); + Assert.False(parsed.IsElementalType); + + TypeName underlyingType = parsed.UnderlyingType; + Assert.NotNull(underlyingType); + Assert.Equal(typeNameWithoutDecorators, underlyingType.Name); + Assert.True(underlyingType.IsElementalType); + Assert.False(underlyingType.IsArray); + Assert.False(underlyingType.IsManagedPointerType); + Assert.False(underlyingType.IsUnmanagedPointerType); + Assert.Null(underlyingType.UnderlyingType); + } + internal sealed class NonAsciiNotAllowed : TypeNameParserOptions { public override void ValidateIdentifier(string candidate) From 4ec4ea5474aca26b2fa8dde46a3ac64a75c6c78f Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Mon, 29 Jan 2024 10:21:22 +0100 Subject: [PATCH 05/48] Handle single dimensional array indexing --- .../ref/System.Reflection.Metadata.cs | 1 + .../Reflection/Metadata/TypeNameParser.cs | 39 +++++++++++-------- .../tests/Metadata/TypeNameParserTests.cs | 16 +++++--- 3 files changed, 35 insertions(+), 21 deletions(-) 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 66adf9216ead2..100fbd96417af 100644 --- a/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs +++ b/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs @@ -2416,6 +2416,7 @@ public sealed class TypeName public bool IsConstructedGenericType { get { throw null; } } public bool IsElementalType { get { throw null; } } public bool IsManagedPointerType { get { throw null; } } + public bool IsSzArrayType { get { throw null; } } public bool IsUnmanagedPointerType { get { throw null; } } public bool IsVariableBoundArrayType { get { throw null; } } public string Name { get { throw null; } } diff --git a/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs index 30ff4d3f84c12..36580c054e747 100644 --- a/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs +++ b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs @@ -140,7 +140,8 @@ private TypeName ParseNextTypeName(bool allowFullyQualifiedName, ref int recursi { TypeName.ByRef => "&", TypeName.Pointer => "*", - 1 => "[]", + TypeName.SZArray => "[]", + 1 => "[*]", _ => ArrayRankToString(parsedModifier) }; ; typeName += trimmedModifier; @@ -256,10 +257,7 @@ private static bool TryParseNextDecorator(ref ReadOnlySpan input, out int if (TryStripFirstCharAndTrailingSpaces(ref input, ']')) { // End of array marker - rankOrModifier = rank; - //return (rank == 1 && !hasSeenAsterisk) - // ? TypeIdDecorator.SzArray - // : TypeIdDecorator.MdArray(rank)); + rankOrModifier = rank == 1 && !hasSeenAsterisk ? TypeName.SZArray : rank; return true; } @@ -294,7 +292,7 @@ private static string ArrayRankToString(int arrayRank) return string.Create(2 + arrayRank - 1, arrayRank, (buffer, rank) => { buffer[0] = '['; - for (int i = 1; i < arrayRank; i++) + for (int i = 1; i < rank; i++) buffer[i] = ','; buffer[^1] = ']'; }); @@ -311,6 +309,7 @@ private static string ArrayRankToString(int arrayRank) public sealed class TypeName { + internal const int SZArray = -1; internal const int Pointer = -2; internal const int ByRef = -3; @@ -347,7 +346,7 @@ internal TypeName(string name, AssemblyName? assemblyName, int rankOrModifier, T /// Returns true if this type represents any kind of array, regardless of the array's /// rank or its bounds. /// - public bool IsArray => _rankOrModifier > 0; + public bool IsArray => _rankOrModifier == SZArray || _rankOrModifier > 0; /// /// Returns true if this type represents a constructed generic type (e.g., "List<int>"). @@ -371,24 +370,29 @@ internal TypeName(string name, AssemblyName? assemblyName, int rankOrModifier, T /// public bool IsElementalType => UnderlyingType is null && !IsConstructedGenericType; - /// - /// 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; - /// /// Returns true if this is a managed pointer type (e.g., "ref int"). /// Managed pointer types are sometimes called byref types () /// public bool IsManagedPointerType => _rankOrModifier == ByRef; // name inconsistent with Type.IsByRef + /// + /// Returns true if this type represents a single-dimensional, zero-indexed array (e.g., "int[]"). + /// + public bool IsSzArrayType => _rankOrModifier == SZArray; // name could be more user-friendly + /// /// Returns true if this type represents an unmanaged pointer (e.g., "int*" or "void*"). /// Unmanaged pointer types are often just called pointers () /// public bool IsUnmanagedPointerType => _rankOrModifier == Pointer;// name inconsistent with Type.IsPointer + /// + /// 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, including namespace, but without the assembly name; e.g., "System.Int32". /// Nested types are represented with a '+'; e.g., "MyNamespace.MyType+NestedType". @@ -406,9 +410,12 @@ internal TypeName(string name, AssemblyName? assemblyName, int rankOrModifier, T public TypeName? UnderlyingType { get; } public int GetArrayRank() - => _rankOrModifier > 0 - ? _rankOrModifier - : throw new ArgumentException("SR.Argument_HasToBeArrayClass"); // TODO: use actual resource (used by Type.GetArrayRank) + => _rankOrModifier switch + { + SZArray => 1, + _ when _rankOrModifier > 0 => _rankOrModifier, + _ => throw new ArgumentException("SR.Argument_HasToBeArrayClass") // TODO: use actual resource (used by Type.GetArrayRank) + }; /// /// If this represents a constructed generic type, returns an array diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs index 3619fa0912868..fb038ce86ab8b 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs @@ -142,30 +142,35 @@ public static IEnumerable DecoratorsAreSupported_Arguments() { yield return new object[] { - "TypeName*", "TypeName", false, -1, false, true + "TypeName*", "TypeName", false, false, -1, false, true }; yield return new object[] { - "TypeName&", "TypeName", false, -1, true, false + "TypeName&", "TypeName", false, false, -1, true, false }; yield return new object[] { - "TypeName[]", "TypeName", true, 1, false, false + "TypeName[]", "TypeName", true, true, 1, false, false }; yield return new object[] { - "TypeName[,,,]", "TypeName", true, 4, false, false + "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, int arrayRank, bool isByRef, bool isPointer) + public void DecoratorsAreSupported(string input, string typeNameWithoutDecorators, bool isArray, bool isSzArray, int arrayRank, bool isByRef, bool isPointer) { TypeName parsed = TypeNameParser.Parse(input.AsSpan(), allowFullyQualifiedName: true); Assert.Equal(input, parsed.Name); Assert.Equal(isArray, parsed.IsArray); + Assert.Equal(isSzArray, parsed.IsSzArrayType); if (isArray) Assert.Equal(arrayRank, parsed.GetArrayRank()); Assert.Equal(isByRef, parsed.IsManagedPointerType); Assert.Equal(isPointer, parsed.IsUnmanagedPointerType); @@ -176,6 +181,7 @@ public void DecoratorsAreSupported(string input, string typeNameWithoutDecorator Assert.Equal(typeNameWithoutDecorators, underlyingType.Name); Assert.True(underlyingType.IsElementalType); Assert.False(underlyingType.IsArray); + Assert.False(underlyingType.IsSzArrayType); Assert.False(underlyingType.IsManagedPointerType); Assert.False(underlyingType.IsUnmanagedPointerType); Assert.Null(underlyingType.UnderlyingType); From f5a8716b8d685e5911c2a440725fc707d2695887 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Mon, 29 Jan 2024 18:02:48 +0100 Subject: [PATCH 06/48] implement TypeName.GetType --- .../ref/System.Reflection.Metadata.cs | 2 +- .../Reflection/Metadata/TypeNameParser.cs | 65 +++++++++++++++++++ .../tests/Metadata/TypeNameParserTests.cs | 19 ++++++ 3 files changed, 85 insertions(+), 1 deletion(-) 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 100fbd96417af..7ca7eb6c6d10a 100644 --- a/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs +++ b/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs @@ -2423,7 +2423,7 @@ public sealed class TypeName public TypeName? UnderlyingType { get { throw null; } } public int GetArrayRank() { throw null; } public System.Reflection.Metadata.TypeName[] GetGenericArguments() { throw null; } - + public System.Type? GetType(bool throwOnError = true) { throw null; } } public ref partial struct TypeNameParser { diff --git a/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs index 36580c054e747..828a2426c8b7d 100644 --- a/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs +++ b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs @@ -430,6 +430,71 @@ public TypeName[] GetGenericArguments() => _genericArguments is not null ? (TypeName[])_genericArguments.Clone() // we return a copy on purpose, to not allow for mutations. TODO: consider returning a ROS : Array.Empty(); // TODO: should we throw (Levi's parser throws InvalidOperationException in such case), Type.GetGenericArguments just returns an empty array + +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode("The type might be removed")] + [RequiresDynamicCode("Required by MakeArrayType")] +#endif + public Type? GetType(bool throwOnError = true) + { + if (UnderlyingType is null) + { + Type? type; + if (AssemblyName is null) + { +#pragma warning disable IL2057 // Unrecognized value passed to the parameter of method. It's not possible to guarantee the availability of the target type. + type = Type.GetType(Name, throwOnError); +#pragma warning restore IL2057 // Unrecognized value passed to the parameter of method. It's not possible to guarantee the availability of the target type. + } + else + { + Assembly assembly = Assembly.Load(AssemblyName); + type = assembly.GetType(Name, throwOnError); + } + + if (IsElementalType || type is null) + { + return type; + } + + TypeName[] genericArgs = GetGenericArguments(); + Type[] genericTypes = new Type[genericArgs.Length]; + for (int i = 0; i < genericArgs.Length; i++) + { + Type? genericArg = genericArgs[i].GetType(throwOnError); + if (genericArg is null) + { + return null; + } + genericTypes[i] = genericArg; + } + +#pragma warning disable IL2055 // Either the type on which the MakeGenericType is called can't be statically determined, or the type parameters to be used for generic arguments can't be statically determined. + return type.MakeGenericType(genericTypes); +#pragma warning restore IL2055 // Either the type on which the MakeGenericType is called can't be statically determined, or the type parameters to be used for generic arguments can't be statically determined. + } + + Type? underlyingType = UnderlyingType.GetType(throwOnError); + if (underlyingType is null) + { + return null; + } + + if (IsManagedPointerType) + { + return underlyingType.MakeByRefType(); + } + else if (IsUnmanagedPointerType) + { + return underlyingType.MakePointerType(); + } + else if (IsSzArrayType) + { + return underlyingType.MakeArrayType(); + } + + return underlyingType.MakeArrayType(rank: GetArrayRank()); + } } public class TypeNameParserOptions diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs index fb038ce86ab8b..03d47363eb35b 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs @@ -187,6 +187,25 @@ public void DecoratorsAreSupported(string input, string typeNameWithoutDecorator Assert.Null(underlyingType.UnderlyingType); } + [Theory] + [InlineData(typeof(int))] + [InlineData(typeof(int*))] + [InlineData(typeof(int***))] + [InlineData(typeof(int[]))] + [InlineData(typeof(int[,]))] + [InlineData(typeof(int[,,,]))] + [InlineData(typeof(List))] + [InlineData(typeof(Dictionary))] + public void GetType_Roundtrip(Type type) + { + TypeName typeName = TypeNameParser.Parse(type.FullName.AsSpan(), allowFullyQualifiedName: true); + + Type afterRoundtrip = typeName.GetType(throwOnError: true); + + Assert.NotNull(afterRoundtrip); + Assert.Equal(type, afterRoundtrip); + } + internal sealed class NonAsciiNotAllowed : TypeNameParserOptions { public override void ValidateIdentifier(string candidate) From 52cb3519c8b7d2743d75c772ef7439abb039ed1c Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 30 Jan 2024 15:10:04 +0100 Subject: [PATCH 07/48] nested types support --- .../ref/System.Reflection.Metadata.cs | 4 +- .../Reflection/Metadata/TypeNameParser.cs | 206 ++++++++++++------ .../tests/Metadata/TypeNameParserTests.cs | 53 ++++- 3 files changed, 189 insertions(+), 74 deletions(-) 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 7ca7eb6c6d10a..c631fc5ade864 100644 --- a/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs +++ b/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs @@ -2412,10 +2412,12 @@ public sealed class TypeName { public string AssemblyQualifiedName { get { throw null; } } public System.Reflection.AssemblyName? AssemblyName { get { throw null; } } + public TypeName? ContainingType { get { throw null; } } public bool IsArray { get { throw null; } } public bool IsConstructedGenericType { get { throw null; } } public bool IsElementalType { get { throw null; } } public bool IsManagedPointerType { get { throw null; } } + public bool IsNestedType { get { throw null; } } public bool IsSzArrayType { get { throw null; } } public bool IsUnmanagedPointerType { get { throw null; } } public bool IsVariableBoundArrayType { get { throw null; } } @@ -2433,7 +2435,7 @@ public partial class TypeNameParserOptions { public TypeNameParserOptions() { } public int MaxRecursiveDepth { get { throw null; } set { } } - public virtual void ValidateIdentifier(string candidate) { } + public virtual void ValidateIdentifier(System.ReadOnlySpan candidate) { } } public readonly partial struct TypeReference { diff --git a/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs index 828a2426c8b7d..137c882ba61a5 100644 --- a/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs +++ b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text; @@ -10,7 +11,7 @@ namespace System.Reflection.Metadata { public ref struct TypeNameParser { - private const string EndOfTypeNameDelimiters = "[]&*,"; // TODO: Roslyn is using '+' here as well + private const string EndOfTypeNameDelimiters = "[]&*,+"; #if NET8_0_OR_GREATER private static readonly SearchValues _endOfTypeNameDelimitersSearchValues = SearchValues.Create(EndOfTypeNameDelimiters); #endif @@ -44,13 +45,14 @@ private TypeName ParseNextTypeName(bool allowFullyQualifiedName, ref int recursi { Dive(ref recursiveDepth); - int offset = GetOffsetOfEndOfTypeName(_inputString); + List? nestedNameLengths = null; + int typeNameLength = GetTypeNameLengthWithNestedNameLengths(_inputString, ref nestedNameLengths); - string typeName = _inputString.Slice(0, offset).ToString(); + ReadOnlySpan typeName = _inputString.Slice(0, typeNameLength); _parseOptions.ValidateIdentifier(typeName); - _inputString = _inputString.Slice(offset); + _inputString = _inputString.Slice(typeNameLength); List? genericArgs = null; // TODO: use some stack-based list in CoreLib @@ -129,10 +131,20 @@ private TypeName ParseNextTypeName(bool allowFullyQualifiedName, ref int recursi AssemblyName? assemblyName = allowFullyQualifiedName ? ParseAssemblyName() : null; - TypeName result = new(typeName, assemblyName, rankOrModifier: 0, underlyingType: null, genericArgs?.ToArray()); + TypeName? containingType = GetContainingType(ref typeName, nestedNameLengths, assemblyName); + TypeName result = new(typeName.ToString(), assemblyName, rankOrModifier: 0, underlyingType: null, containingType, genericArgs?.ToArray()); if (previousDecorator != default) // some decorators were recognized { + StringBuilder sb = new StringBuilder(typeName.Length + 4); +#if NET8_0_OR_GREATER + sb.Append(typeName); +#else + for (int i = 0; i < typeName.Length; i++) + { + sb.Append(typeName[i]); + } +#endif while (TryParseNextDecorator(ref capturedBeforeProcessing, out int parsedModifier)) { // we are not reusing the input string, as it could have contain whitespaces that we want to exclude @@ -143,17 +155,38 @@ private TypeName ParseNextTypeName(bool allowFullyQualifiedName, ref int recursi TypeName.SZArray => "[]", 1 => "[*]", _ => ArrayRankToString(parsedModifier) - }; ; - typeName += trimmedModifier; - result = new(typeName, assemblyName, parsedModifier, underlyingType: result); + }; + sb.Append(trimmedModifier); + result = new(sb.ToString(), assemblyName, parsedModifier, underlyingType: result); } } return result; } + private static int GetTypeNameLengthWithNestedNameLengths(ReadOnlySpan input, ref List? nestedNameLengths) + { + bool isNestedType; + int totalLength = 0; + do + { + int length = GetTypeNameLength(input.Slice(totalLength), out isNestedType); + Debug.Assert(length > 0, "GetTypeNameLength should never return a negative value"); + + if (isNestedType) + { + // do not validate the type name now, it will be validated as a whole nested type name later + (nestedNameLengths ??= new()).Add(length); + totalLength += 1; // skip the '+' sign in next search + } + totalLength += length; + } while (isNestedType); + + return totalLength; + } + // Normalizes "not found" to input length, since caller is expected to slice. - private static int GetOffsetOfEndOfTypeName(ReadOnlySpan input) + private static int GetTypeNameLength(ReadOnlySpan input, out bool isNestedType) { // 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: @@ -178,6 +211,7 @@ private static int GetOffsetOfEndOfTypeName(ReadOnlySpan input) if (EndOfTypeNameDelimiters.IndexOf(input[offset]) >= 0) { break; } } #endif + isNestedType = offset > 0 && input[offset] == '+'; return (int)Math.Min((uint)offset, (uint)input.Length); } @@ -200,6 +234,23 @@ private static int GetOffsetOfEndOfTypeName(ReadOnlySpan input) return null; } + private static TypeName? GetContainingType(ref ReadOnlySpan typeName, List? nestedNameLengths, AssemblyName? assemblyName) + { + if (nestedNameLengths is null) + { + return null; + } + + TypeName? containingType = null; + foreach (int nestedNameLength in nestedNameLengths) + { + containingType = new(typeName.Slice(0, nestedNameLength).ToString(), assemblyName, rankOrModifier: 0, null, containingType: containingType, null); + typeName = typeName.Slice(nestedNameLength + 1); // don't include the `+` in type name + } + + return containingType; + } + private void Dive(ref int depth) { if (depth >= _parseOptions.MaxRecursiveDepth) @@ -219,7 +270,7 @@ private static bool TryStripFirstCharAndTrailingSpaces(ref ReadOnlySpan sp { if (!span.IsEmpty && span[0] == value) { - span = span.Slice(1).Trim(' '); + span = span.Slice(1).TrimStart(' '); return true; } return false; @@ -318,12 +369,16 @@ public sealed class TypeName private readonly int _rankOrModifier; private readonly TypeName[]? _genericArguments; - internal TypeName(string name, AssemblyName? assemblyName, int rankOrModifier, TypeName? underlyingType = default, TypeName[]? genericTypeArguments = default) + internal TypeName(string name, AssemblyName? assemblyName, int rankOrModifier, + TypeName? underlyingType = default, + TypeName? containingType = default, + TypeName[]? genericTypeArguments = default) { Name = name; AssemblyName = assemblyName; _rankOrModifier = rankOrModifier; UnderlyingType = underlyingType; + ContainingType = containingType; _genericArguments = genericTypeArguments; AssemblyQualifiedName = assemblyName is null ? name : $"{name}, {assemblyName.FullName}"; } @@ -376,6 +431,12 @@ internal TypeName(string name, AssemblyName? assemblyName, int rankOrModifier, T /// public bool IsManagedPointerType => _rankOrModifier == ByRef; // name inconsistent with Type.IsByRef + /// + /// Returns true if this is a nested type (e.g., "Namespace.Containing+Nested"). + /// For nested types returns their containing type. + /// + public bool IsNestedType => ContainingType is not null; + /// /// Returns true if this type represents a single-dimensional, zero-indexed array (e.g., "int[]"). /// @@ -393,6 +454,15 @@ internal TypeName(string name, AssemblyName? assemblyName, int rankOrModifier, T /// public bool IsVariableBoundArrayType => _rankOrModifier > 1; + /// + /// If this type is a nested type (see ), gets + /// the containing type. If this type is not a nested type, returns null. + /// + /// + /// For example, given "Namespace.Containing+Nested", unwraps the outermost type and returns "Namespace.Containing". + /// + public TypeName? ContainingType { get; } + /// /// The 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". @@ -434,68 +504,69 @@ public TypeName[] GetGenericArguments() #if NET8_0_OR_GREATER [RequiresUnreferencedCode("The type might be removed")] [RequiresDynamicCode("Required by MakeArrayType")] +#else +#pragma warning disable IL2075, IL2057, IL2055 #endif public Type? GetType(bool throwOnError = true) { - if (UnderlyingType is null) + if (ContainingType is not null) // nested type { - Type? type; - if (AssemblyName is null) - { -#pragma warning disable IL2057 // Unrecognized value passed to the parameter of method. It's not possible to guarantee the availability of the target type. - type = Type.GetType(Name, throwOnError); -#pragma warning restore IL2057 // Unrecognized value passed to the parameter of method. It's not possible to guarantee the availability of the target type. - } - else - { - Assembly assembly = Assembly.Load(AssemblyName); - type = assembly.GetType(Name, throwOnError); - } + const BindingFlags flagsCopiedFromClr = BindingFlags.NonPublic | BindingFlags.Public; + return Make(ContainingType.GetType(throwOnError)?.GetNestedType(Name, flagsCopiedFromClr)); + } + else if (UnderlyingType is null) + { + Type? type = AssemblyName is null + ? Type.GetType(Name, throwOnError) + : Assembly.Load(AssemblyName).GetType(Name, throwOnError); + + return Make(type); + } + + return Make(UnderlyingType.GetType(throwOnError)); - if (IsElementalType || type is null) + Type? Make(Type? type) + { + if (type is null || IsElementalType) { return type; } - - TypeName[] genericArgs = GetGenericArguments(); - Type[] genericTypes = new Type[genericArgs.Length]; - for (int i = 0; i < genericArgs.Length; i++) + else if (IsConstructedGenericType) { - Type? genericArg = genericArgs[i].GetType(throwOnError); - if (genericArg is null) + TypeName[] genericArgs = GetGenericArguments(); + Type[] genericTypes = new Type[genericArgs.Length]; + for (int i = 0; i < genericArgs.Length; i++) { - return null; + Type? genericArg = genericArgs[i].GetType(throwOnError); + if (genericArg is null) + { + return null; + } + genericTypes[i] = genericArg; } - genericTypes[i] = genericArg; - } - -#pragma warning disable IL2055 // Either the type on which the MakeGenericType is called can't be statically determined, or the type parameters to be used for generic arguments can't be statically determined. - return type.MakeGenericType(genericTypes); -#pragma warning restore IL2055 // Either the type on which the MakeGenericType is called can't be statically determined, or the type parameters to be used for generic arguments can't be statically determined. - } - Type? underlyingType = UnderlyingType.GetType(throwOnError); - if (underlyingType is null) - { - return null; - } - - if (IsManagedPointerType) - { - return underlyingType.MakeByRefType(); - } - else if (IsUnmanagedPointerType) - { - return underlyingType.MakePointerType(); - } - else if (IsSzArrayType) - { - return underlyingType.MakeArrayType(); + return type.MakeGenericType(genericTypes); + } + else if (IsManagedPointerType) + { + return type.MakeByRefType(); + } + else if (IsUnmanagedPointerType) + { + return type.MakePointerType(); + } + else if (IsSzArrayType) + { + return type.MakeArrayType(); + } + else + { + return type.MakeArrayType(rank: GetArrayRank()); + } } - - return underlyingType.MakeArrayType(rank: GetArrayRank()); } } +#pragma warning restore IL2075, IL2057, IL2055 public class TypeNameParserOptions { @@ -514,11 +585,18 @@ public int MaxRecursiveDepth } } - public virtual void ValidateIdentifier(string candidate) + internal bool AllowSpacesOnly { get; set; } + + internal bool AllowEscaping { get; set; } + + internal bool StrictValidation { get; set; } + + public virtual void ValidateIdentifier(ReadOnlySpan candidate) { -#if NET8_0_OR_GREATER - ArgumentNullException.ThrowIfNullOrEmpty(candidate, nameof(candidate)); -#endif + if (candidate.IsEmpty) + { + throw new ArgumentException("TODO"); + } } } @@ -532,7 +610,7 @@ public SafeTypeNameParserOptions(bool allowNonAsciiIdentifiers) public bool AllowNonAsciiIdentifiers { get; set; } - public override void ValidateIdentifier(string candidate) + public override void ValidateIdentifier(ReadOnlySpan candidate) { base.ValidateIdentifier(candidate); @@ -542,7 +620,7 @@ public override void ValidateIdentifier(string candidate) internal class RoslynTypeNameParserOptions : TypeNameParserOptions { - public override void ValidateIdentifier(string candidate) + public override void ValidateIdentifier(ReadOnlySpan candidate) { // it seems that Roslyn is not performing any kind of validation } diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs index 03d47363eb35b..0f1f1db99bda6 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs @@ -6,7 +6,7 @@ using System.Text; using Xunit; -namespace System.Reflection.Metadata.Tests.Metadata +namespace System.Reflection.Metadata.Tests { public class TypeNameParserTests { @@ -189,33 +189,68 @@ public void DecoratorsAreSupported(string input, string typeNameWithoutDecorator [Theory] [InlineData(typeof(int))] - [InlineData(typeof(int*))] - [InlineData(typeof(int***))] [InlineData(typeof(int[]))] [InlineData(typeof(int[,]))] [InlineData(typeof(int[,,,]))] [InlineData(typeof(List))] [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 GetType_Roundtrip(Type type) { - TypeName typeName = TypeNameParser.Parse(type.FullName.AsSpan(), allowFullyQualifiedName: true); + Test(type); + Test(type.MakePointerType()); + Test(type.MakePointerType().MakePointerType()); + Test(type.MakeByRefType()); - Type afterRoundtrip = typeName.GetType(throwOnError: true); + if (!type.IsArray) + { + Test(type.MakeArrayType()); // [] + Test(type.MakeArrayType(1)); // [*] + Test(type.MakeArrayType(2)); // [,] + } + + static void Test(Type type) + { + TypeName typeName = TypeNameParser.Parse(type.AssemblyQualifiedName.AsSpan(), allowFullyQualifiedName: true); + + Type afterRoundtrip = typeName.GetType(throwOnError: true); + + Assert.NotNull(afterRoundtrip); + Assert.Equal(type, afterRoundtrip); + } + } - Assert.NotNull(afterRoundtrip); - Assert.Equal(type, afterRoundtrip); + public class NestedNonGeneric_0 + { + public class NestedNonGeneric_1 { } + } + + public class NestedGeneric_0 + { + public class NestedGeneric_1 + { + public class NestedGeneric_2 + { + public class NestedNonGeneric_3 { } + } + } } internal sealed class NonAsciiNotAllowed : TypeNameParserOptions { - public override void ValidateIdentifier(string candidate) + public override void ValidateIdentifier(ReadOnlySpan candidate) { base.ValidateIdentifier(candidate); #if NET8_0_OR_GREATER if (!Ascii.IsValid(candidate)) #else - if (candidate.Any(c => c >= 128)) + if (candidate.ToArray().Any(c => c >= 128)) #endif { throw new ArgumentException("Non ASCII char found"); From 60ad6f0df35293dc27e47b8998ba14c88e8e88f2 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 31 Jan 2024 08:52:50 +0100 Subject: [PATCH 08/48] support ignore case --- .../ref/System.Reflection.Metadata.cs | 2 +- .../Reflection/Metadata/TypeNameParser.cs | 24 +++++++++++-------- .../tests/Metadata/TypeNameParserTests.cs | 16 ++++++++++--- 3 files changed, 28 insertions(+), 14 deletions(-) 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 c631fc5ade864..f762dd3fa0522 100644 --- a/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs +++ b/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs @@ -2425,7 +2425,7 @@ public sealed class TypeName public TypeName? UnderlyingType { get { throw null; } } public int GetArrayRank() { throw null; } public System.Reflection.Metadata.TypeName[] GetGenericArguments() { throw null; } - public System.Type? GetType(bool throwOnError = true) { throw null; } + public System.Type? GetType(bool throwOnError = true, bool ignoreCase = false) { throw null; } } public ref partial struct TypeNameParser { diff --git a/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs index 137c882ba61a5..d08be692d5a60 100644 --- a/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs +++ b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs @@ -211,7 +211,7 @@ private static int GetTypeNameLength(ReadOnlySpan input, out bool isNested if (EndOfTypeNameDelimiters.IndexOf(input[offset]) >= 0) { break; } } #endif - isNestedType = offset > 0 && input[offset] == '+'; + isNestedType = offset > 0 && offset < input.Length && input[offset] == '+'; return (int)Math.Min((uint)offset, (uint)input.Length); } @@ -505,25 +505,29 @@ public TypeName[] GetGenericArguments() [RequiresUnreferencedCode("The type might be removed")] [RequiresDynamicCode("Required by MakeArrayType")] #else -#pragma warning disable IL2075, IL2057, IL2055 +#pragma warning disable IL2055, IL2057, IL2075, IL2096 #endif - public Type? GetType(bool throwOnError = true) + public Type? GetType(bool throwOnError = true, bool ignoreCase = false) { if (ContainingType is not null) // nested type { - const BindingFlags flagsCopiedFromClr = BindingFlags.NonPublic | BindingFlags.Public; - return Make(ContainingType.GetType(throwOnError)?.GetNestedType(Name, flagsCopiedFromClr)); + BindingFlags flagsCopiedFromClr = BindingFlags.NonPublic | BindingFlags.Public; + if (ignoreCase) + { + flagsCopiedFromClr |= BindingFlags.IgnoreCase; + } + return Make(ContainingType.GetType(throwOnError, ignoreCase)?.GetNestedType(Name, flagsCopiedFromClr)); } else if (UnderlyingType is null) { Type? type = AssemblyName is null - ? Type.GetType(Name, throwOnError) - : Assembly.Load(AssemblyName).GetType(Name, throwOnError); + ? Type.GetType(Name, throwOnError, ignoreCase) + : Assembly.Load(AssemblyName).GetType(Name, throwOnError, ignoreCase); return Make(type); } - return Make(UnderlyingType.GetType(throwOnError)); + return Make(UnderlyingType.GetType(throwOnError, ignoreCase)); Type? Make(Type? type) { @@ -537,7 +541,7 @@ public TypeName[] GetGenericArguments() Type[] genericTypes = new Type[genericArgs.Length]; for (int i = 0; i < genericArgs.Length; i++) { - Type? genericArg = genericArgs[i].GetType(throwOnError); + Type? genericArg = genericArgs[i].GetType(throwOnError, ignoreCase); if (genericArg is null) { return null; @@ -566,7 +570,7 @@ public TypeName[] GetGenericArguments() } } } -#pragma warning restore IL2075, IL2057, IL2055 +#pragma warning restore IL2055, IL2057, IL2075, IL2096 public class TypeNameParserOptions { diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs index 0f1f1db99bda6..9027314d9438f 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs @@ -217,11 +217,21 @@ public void GetType_Roundtrip(Type type) static void Test(Type type) { TypeName typeName = TypeNameParser.Parse(type.AssemblyQualifiedName.AsSpan(), allowFullyQualifiedName: true); + Verify(type, typeName, ignoreCase: false); - Type afterRoundtrip = typeName.GetType(throwOnError: true); + typeName = TypeNameParser.Parse(type.AssemblyQualifiedName.ToLower().AsSpan(), allowFullyQualifiedName: true); + Verify(type, typeName, ignoreCase: true); - Assert.NotNull(afterRoundtrip); - Assert.Equal(type, afterRoundtrip); + typeName = TypeNameParser.Parse(type.AssemblyQualifiedName.ToUpper().AsSpan(), allowFullyQualifiedName: true); + Verify(type, typeName, ignoreCase: true); + + static void Verify(Type type, TypeName typeName, bool ignoreCase) + { + Type afterRoundtrip = typeName.GetType(throwOnError: true, ignoreCase: ignoreCase); + + Assert.NotNull(afterRoundtrip); + Assert.Equal(type, afterRoundtrip); + } } } From b5349bd4db3f8d72332e4ad8cf2a0dd8e347ccd9 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 31 Jan 2024 09:06:47 +0100 Subject: [PATCH 09/48] integrate with System.Private.CoreLib: - allow ignoring errors (return null) - assembly name parsing --- .../System.Private.CoreLib.csproj | 3 +- .../src/System/Reflection/TypeNameResolver.cs | 382 +++++++++++ .../src/System/Type.CoreCLR.cs | 8 +- .../System/Reflection/AssemblyNameParser.cs | 279 +++++--- .../System/Reflection/Metadata/TypeName.cs | 228 +++++++ .../Reflection/Metadata/TypeNameParser.cs | 420 ++++++++++++ .../Metadata/TypeNameParserOptions.cs | 57 ++ .../src/System/Reflection/TypeNameParser.cs | 4 +- .../System.Private.CoreLib.Shared.projitems | 13 +- .../ref/System.Reflection.Metadata.cs | 4 +- .../src/System.Reflection.Metadata.csproj | 6 +- .../Reflection/Metadata/TypeNameParser.cs | 632 ------------------ .../tests/Metadata/TypeNameParserTests.cs | 39 +- 13 files changed, 1331 insertions(+), 744 deletions(-) create mode 100644 src/coreclr/System.Private.CoreLib/src/System/Reflection/TypeNameResolver.cs rename src/libraries/{System.Private.CoreLib => Common}/src/System/Reflection/AssemblyNameParser.cs (57%) create mode 100644 src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs create mode 100644 src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs create mode 100644 src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs delete mode 100644 src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs diff --git a/src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj b/src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj index 08f6699e0a02d..1214a877c9234 100644 --- a/src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj +++ b/src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj @@ -1,4 +1,4 @@ - + false @@ -202,6 +202,7 @@ + diff --git a/src/coreclr/System.Private.CoreLib/src/System/Reflection/TypeNameResolver.cs b/src/coreclr/System.Private.CoreLib/src/System/Reflection/TypeNameResolver.cs new file mode 100644 index 0000000000000..13947a3fde214 --- /dev/null +++ b/src/coreclr/System.Private.CoreLib/src/System/Reflection/TypeNameResolver.cs @@ -0,0 +1,382 @@ +// 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.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Loader; +using System.Threading; + +namespace System.Reflection +{ + internal struct TypeNameResolver + { + private Func? _assemblyResolver; + private Func? _typeResolver; + private bool _throwOnError; + private bool _ignoreCase; + private bool _extensibleParser; + private bool _requireAssemblyQualifiedName; + private bool _suppressContextualReflectionContext; + private Assembly? _requestingAssembly; + private Assembly? _topLevelAssembly; + + [RequiresUnreferencedCode("The type might be removed")] + internal static Type? GetType( + string typeName, + Assembly requestingAssembly, + bool throwOnError = false, + bool ignoreCase = false) + { + return GetType(typeName, assemblyResolver: null, typeResolver: null, requestingAssembly: requestingAssembly, + throwOnError: throwOnError, ignoreCase: ignoreCase, extensibleParser: false); + } + + [RequiresUnreferencedCode("The type might be removed")] + internal static Type? GetType( + string typeName, + Func? assemblyResolver, + Func? typeResolver, + Assembly? requestingAssembly, + bool throwOnError = false, + bool ignoreCase = false, + bool extensibleParser = true) + { + ArgumentNullException.ThrowIfNull(typeName); + + // Compat: Empty name throws TypeLoadException instead of + // the natural ArgumentException + if (typeName.Length == 0) + { + if (throwOnError) + throw new TypeLoadException(SR.Arg_TypeLoadNullStr); + return null; + } + + var parsed = Metadata.TypeNameParser.Parse(typeName, throwOnError: throwOnError); + if (parsed is null) + { + return null; + } + + return new TypeNameResolver() + { + _assemblyResolver = assemblyResolver, + _typeResolver = typeResolver, + _throwOnError = throwOnError, + _ignoreCase = ignoreCase, + _extensibleParser = extensibleParser, + _requestingAssembly = requestingAssembly + }.Resolve(parsed); + } + + [RequiresUnreferencedCode("The type might be removed")] + internal static Type? GetType( + string typeName, + bool throwOnError, + bool ignoreCase, + Assembly topLevelAssembly) + { + var parsed = Metadata.TypeNameParser.Parse(typeName, + allowFullyQualifiedName: true, // let it get parsed, but throw when topLevelAssembly was specified + throwOnError: throwOnError); + + if (parsed is null) + { + return null; + } + else if (parsed.AssemblyName is not null && topLevelAssembly is not null) + { + return throwOnError ? throw new ArgumentException(SR.Argument_AssemblyGetTypeCannotSpecifyAssembly) : null; + } + + return new TypeNameResolver() + { + _throwOnError = throwOnError, + _ignoreCase = ignoreCase, + _topLevelAssembly = topLevelAssembly, + _requestingAssembly = topLevelAssembly + }.Resolve(parsed); + } + + // Resolve type name referenced by a custom attribute metadata. + // It uses the standard Type.GetType(typeName, throwOnError: true) algorithm with the following modifications: + // - ContextualReflectionContext is not taken into account + // - The dependency between the returned type and the requesting assembly is recorded for the purpose of + // lifetime tracking of collectible types. + [RequiresUnreferencedCode("TODO: introduce dedicated overload that does not use forbidden API")] + internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName, RuntimeModule scope) + { + ArgumentException.ThrowIfNullOrEmpty(typeName); + + RuntimeAssembly requestingAssembly = scope.GetRuntimeAssembly(); + + var parsed = Metadata.TypeNameParser.Parse(typeName, allowFullyQualifiedName: requestingAssembly is null, throwOnError: true)!; // adsitnik allowFullyQualifiedName part might be wrong + RuntimeType? type = (RuntimeType?)new TypeNameResolver() + { + _throwOnError = true, + _suppressContextualReflectionContext = true, + _requestingAssembly = requestingAssembly + }.Resolve(parsed); + + Debug.Assert(type != null); + + RuntimeTypeHandle.RegisterCollectibleTypeDependency(type, requestingAssembly); + + return type; + } + + // Used by VM + [RequiresUnreferencedCode("TODO: introduce dedicated overload that does not use forbidden API")] + internal static unsafe RuntimeType? GetTypeHelper(char* pTypeName, RuntimeAssembly? requestingAssembly, + bool throwOnError, bool requireAssemblyQualifiedName) + { + ReadOnlySpan typeName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pTypeName); + + // Compat: Empty name throws TypeLoadException instead of + // the natural ArgumentException + if (typeName.Length == 0) + { + if (throwOnError) + throw new TypeLoadException(SR.Arg_TypeLoadNullStr); + return null; + } + + var parsed = Metadata.TypeNameParser.Parse(typeName, + allowFullyQualifiedName: true, + throwOnError: throwOnError); + + if (parsed is null) + { + return null; + } + + RuntimeType? type = (RuntimeType?)new TypeNameResolver() + { + _requestingAssembly = requestingAssembly, + _throwOnError = throwOnError, + _suppressContextualReflectionContext = true, + _requireAssemblyQualifiedName = requireAssemblyQualifiedName, + }.Resolve(parsed); + + if (type != null) + RuntimeTypeHandle.RegisterCollectibleTypeDependency(type, requestingAssembly); + + return type; + } + + private Assembly? ResolveAssembly(AssemblyName assemblyName) + { + Assembly? assembly; + if (_assemblyResolver is not null) + { + assembly = _assemblyResolver(assemblyName); + if (assembly is null && _throwOnError) + { + throw new FileNotFoundException(SR.Format(SR.FileNotFound_ResolveAssembly, assemblyName)); + } + } + else + { + assembly = RuntimeAssembly.InternalLoad(assemblyName, ref Unsafe.NullRef(), + _suppressContextualReflectionContext ? null : AssemblyLoadContext.CurrentContextualReflectionContext, + requestingAssembly: (RuntimeAssembly?)_requestingAssembly, throwOnFileNotFound: _throwOnError); + } + return assembly; + } + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", + Justification = "TypeNameParser.GetType is marked as RequiresUnreferencedCode.")] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075:UnrecognizedReflectionPattern", + Justification = "TypeNameParser.GetType is marked as RequiresUnreferencedCode.")] + private Type? GetType(Metadata.TypeName typeName) + { + Assembly? assembly; + + if (typeName.AssemblyName is not null) + { + assembly = ResolveAssembly(typeName.AssemblyName); + if (assembly is null) + return null; + } + else + { + assembly = _topLevelAssembly; + } + + Type? type; + + // Resolve the top level type. + if (_typeResolver is not null) + { + string escapedTypeName = TypeNameParser.EscapeTypeName(typeName.Name); + + type = _typeResolver(assembly, escapedTypeName, _ignoreCase); + + if (type is null) + { + if (_throwOnError) + { + throw new TypeLoadException(assembly is null ? + SR.Format(SR.TypeLoad_ResolveType, escapedTypeName) : + SR.Format(SR.TypeLoad_ResolveTypeFromAssembly, escapedTypeName, assembly.FullName)); + } + return null; + } + } + else + { + if (assembly is null) + { + if (_requireAssemblyQualifiedName) + { + if (_throwOnError) + { + throw new TypeLoadException(SR.Format(SR.TypeLoad_ResolveType, TypeNameParser.EscapeTypeName(typeName.Name))); + } + return null; + } + return GetTypeFromDefaultAssemblies(typeName.Name, nestedTypeNames: ReadOnlySpan.Empty); + } + + if (assembly is RuntimeAssembly runtimeAssembly) + { + // Compat: Non-extensible parser allows ambiguous matches with ignore case lookup + if (!_extensibleParser || !_ignoreCase) + { + return runtimeAssembly.GetTypeCore(typeName.Name, nestedTypeNames: ReadOnlySpan.Empty, throwOnError: _throwOnError, ignoreCase: _ignoreCase); + } + type = runtimeAssembly.GetTypeCore(typeName.Name, 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(TypeNameParser.EscapeTypeName(typeName.Name), throwOnError: _throwOnError, ignoreCase: _ignoreCase); + } + + if (type is null) + return null; + } + + return type; + } + + private Type? GetTypeFromDefaultAssemblies(string typeName, ReadOnlySpan nestedTypeNames) + { + RuntimeAssembly? requestingAssembly = (RuntimeAssembly?)_requestingAssembly; + if (requestingAssembly is not null) + { + Type? type = ((RuntimeAssembly)requestingAssembly).GetTypeCore(typeName, nestedTypeNames, throwOnError: false, ignoreCase: _ignoreCase); + if (type is not null) + return type; + } + + RuntimeAssembly coreLib = (RuntimeAssembly)typeof(object).Assembly; + if (requestingAssembly != coreLib) + { + Type? type = ((RuntimeAssembly)coreLib).GetTypeCore(typeName, nestedTypeNames, throwOnError: false, ignoreCase: _ignoreCase); + if (type is not null) + return type; + } + + RuntimeAssembly? resolvedAssembly = AssemblyLoadContext.OnTypeResolve(requestingAssembly, TypeNameParser.EscapeTypeName(typeName, nestedTypeNames)); + if (resolvedAssembly is not null) + { + Type? type = resolvedAssembly.GetTypeCore(typeName, nestedTypeNames, throwOnError: false, ignoreCase: _ignoreCase); + if (type is not null) + return type; + } + + if (_throwOnError) + throw new TypeLoadException(SR.Format(SR.TypeLoad_ResolveTypeFromAssembly, TypeNameParser.EscapeTypeName(typeName), (requestingAssembly ?? coreLib).FullName)); + + return null; + } + + [RequiresUnreferencedCode("The type might be removed")] + private Type? Resolve(Metadata.TypeName typeName) + { + if (typeName.ContainingType is not null) // nested type + { + BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Public; + if (_ignoreCase) + { + flags |= BindingFlags.IgnoreCase; + } + Type? containingType = Resolve(typeName.ContainingType); + if (containingType is null) + { + return null; + } + Type? nestedType = containingType.GetNestedType(typeName.Name, flags); + if (nestedType is null) + { + if (_throwOnError) + { + throw new TypeLoadException(SR.Format(SR.TypeLoad_ResolveNestedType, typeName.Name, typeName.ContainingType.Name)); + } + return null; + } + + return Make(nestedType, typeName); + } + else if (typeName.UnderlyingType is null) + { + Type? type = GetType(typeName); + + return Make(type, typeName); + } + + return Make(Resolve(typeName.UnderlyingType), typeName); + } + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2055:UnrecognizedReflectionPattern", + Justification = "Used to implement resolving types from strings.")] + [UnconditionalSuppressMessage("AotAnalysis", "IL3050:AotUnfriendlyApi", + Justification = "Used to implement resolving types from strings.")] + [RequiresUnreferencedCode("The type might be removed")] + private Type? Make(Type? type, Metadata.TypeName typeName) + { + if (type is null || typeName.IsElementalType) + { + return type; + } + else if (typeName.IsConstructedGenericType) + { + Metadata.TypeName[] genericArgs = typeName.GetGenericArguments(); + Type[] genericTypes = new Type[genericArgs.Length]; + for (int i = 0; i < genericArgs.Length; i++) + { + Type? genericArg = Resolve(genericArgs[i]); + if (genericArg is null) + { + return null; + } + genericTypes[i] = genericArg; + } + + return type.MakeGenericType(genericTypes); + } + else if (typeName.IsManagedPointerType) + { + return type.MakeByRefType(); + } + else if (typeName.IsUnmanagedPointerType) + { + return type.MakePointerType(); + } + else if (typeName.IsSzArrayType) + { + return type.MakeArrayType(); + } + else + { + return type.MakeArrayType(rank: typeName.GetArrayRank()); + } + } + } +} 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 03a92b709eb1a..99d0a211e8f5c 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/Type.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/Type.CoreCLR.cs @@ -35,7 +35,7 @@ public abstract partial class Type : MemberInfo, IReflect public static Type? GetType(string typeName) { StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller; - return TypeNameParser.GetType(typeName, Assembly.GetExecutingAssembly(ref stackMark)); + return TypeNameResolver.GetType(typeName, Assembly.GetExecutingAssembly(ref stackMark)); } [RequiresUnreferencedCode("The type might be removed")] @@ -46,7 +46,7 @@ public abstract partial class Type : MemberInfo, IReflect Func? typeResolver) { StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller; - return TypeNameParser.GetType(typeName, assemblyResolver, typeResolver, + return TypeNameResolver.GetType(typeName, assemblyResolver, typeResolver, ((assemblyResolver != null) && (typeResolver != null)) ? null : Assembly.GetExecutingAssembly(ref stackMark)); } @@ -59,7 +59,7 @@ public abstract partial class Type : MemberInfo, IReflect bool throwOnError) { StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller; - return TypeNameParser.GetType(typeName, assemblyResolver, typeResolver, + return TypeNameResolver.GetType(typeName, assemblyResolver, typeResolver, ((assemblyResolver != null) && (typeResolver != null)) ? null : Assembly.GetExecutingAssembly(ref stackMark), throwOnError: throwOnError); } @@ -74,7 +74,7 @@ public abstract partial class Type : MemberInfo, IReflect bool ignoreCase) { StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller; - return TypeNameParser.GetType(typeName, assemblyResolver, typeResolver, + return TypeNameResolver.GetType(typeName, assemblyResolver, typeResolver, ((assemblyResolver != null) && (typeResolver != null)) ? null : Assembly.GetExecutingAssembly(ref stackMark), throwOnError: throwOnError, ignoreCase: ignoreCase); } diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/AssemblyNameParser.cs b/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs similarity index 57% rename from src/libraries/System.Private.CoreLib/src/System/Reflection/AssemblyNameParser.cs rename to src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs index dbaaefd4d8c9d..04e626f25c4dd 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/AssemblyNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs @@ -57,41 +57,53 @@ private enum AttributeKind 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; } - public static AssemblyNameParts Parse(string name) +#if SYSTEM_PRIVATE_CORELIB + public static AssemblyNameParts Parse(string name) => Parse(name.AsSpan()); + + public static AssemblyNameParts Parse(ReadOnlySpan name) { - return new AssemblyNameParser(name).Parse(); + AssemblyNameParser parser = new(name); + AssemblyNameParts result = default; + if (parser.TryParse(ref result)) + { + return result; + } + throw new FileLoadException(SR.InvalidAssemblyName, name.ToString()); } +#endif - public static AssemblyNameParts Parse(ReadOnlySpan name) + internal static bool TryParse(ReadOnlySpan name, ref AssemblyNameParts parts) { - return new AssemblyNameParser(name).Parse(); + AssemblyNameParser parser = new(name); + return parser.TryParse(ref parts); } - private void RecordNewSeenOrThrow(scoped ref AttributeKind seenAttributes, AttributeKind newAttribute) + private static bool TryRecordNewSeen(scoped ref AttributeKind seenAttributes, AttributeKind newAttribute) { if ((seenAttributes & newAttribute) != 0) { - ThrowInvalidAssemblyName(); + return false; } seenAttributes |= newAttribute; + return true; } - private AssemblyNameParts Parse() + private bool TryParse(ref AssemblyNameParts result) { // Name must come first. - Token token = GetNextToken(out string name); - if (token != Token.String) - ThrowInvalidAssemblyName(); - - if (string.IsNullOrEmpty(name)) - ThrowInvalidAssemblyName(); + if (!TryGetNextToken(out string name, out Token token) || token != Token.String || string.IsNullOrEmpty(name)) + return false; Version? version = null; string? cultureName = null; @@ -99,61 +111,92 @@ private AssemblyNameParts Parse() AssemblyNameFlags flags = 0; AttributeKind alreadySeen = default; - token = GetNextToken(); + if (!TryGetNextToken(out _, out token)) + return false; + while (token != Token.End) { if (token != Token.Comma) - ThrowInvalidAssemblyName(); + return false; - token = GetNextToken(out string attributeName); - if (token != Token.String) - ThrowInvalidAssemblyName(); + if (!TryGetNextToken(out string attributeName, out token) || token != Token.String) + return false; - token = GetNextToken(); - if (token != Token.Equals) - ThrowInvalidAssemblyName(); + if (!TryGetNextToken(out _, out token) || token != Token.Equals) + return false; - token = GetNextToken(out string attributeValue); - if (token != Token.String) - ThrowInvalidAssemblyName(); + if (!TryGetNextToken(out string attributeValue, out token) || token != Token.String) + return false; if (attributeName == string.Empty) - ThrowInvalidAssemblyName(); + return false; if (attributeName.Equals("Version", StringComparison.OrdinalIgnoreCase)) { - RecordNewSeenOrThrow(ref alreadySeen, AttributeKind.Version); - version = ParseVersion(attributeValue); + if (!TryRecordNewSeen(ref alreadySeen, AttributeKind.Version)) + { + return false; + } + if (!TryParseVersion(attributeValue, ref version)) + { + return false; + } } if (attributeName.Equals("Culture", StringComparison.OrdinalIgnoreCase)) { - RecordNewSeenOrThrow(ref alreadySeen, AttributeKind.Culture); + if (!TryRecordNewSeen(ref alreadySeen, AttributeKind.Culture)) + { + return false; + } cultureName = ParseCulture(attributeValue); } if (attributeName.Equals("PublicKey", StringComparison.OrdinalIgnoreCase)) { - RecordNewSeenOrThrow(ref alreadySeen, AttributeKind.PublicKeyOrToken); - pkt = ParsePKT(attributeValue, isToken: false); + if (!TryRecordNewSeen(ref alreadySeen, AttributeKind.PublicKeyOrToken)) + { + return false; + } + if (!TryParsePKT(attributeValue, isToken: false, ref pkt)) + { + return false; + } flags |= AssemblyNameFlags.PublicKey; } if (attributeName.Equals("PublicKeyToken", StringComparison.OrdinalIgnoreCase)) { - RecordNewSeenOrThrow(ref alreadySeen, AttributeKind.PublicKeyOrToken); - pkt = ParsePKT(attributeValue, isToken: true); + if (!TryRecordNewSeen(ref alreadySeen, AttributeKind.PublicKeyOrToken)) + { + return false; + } + if (!TryParsePKT(attributeValue, isToken: true, ref pkt)) + { + return false; + } } if (attributeName.Equals("ProcessorArchitecture", StringComparison.OrdinalIgnoreCase)) { - RecordNewSeenOrThrow(ref alreadySeen, AttributeKind.ProcessorArchitecture); - flags |= (AssemblyNameFlags)(((int)ParseProcessorArchitecture(attributeValue)) << 4); + if (!TryRecordNewSeen(ref alreadySeen, AttributeKind.ProcessorArchitecture)) + { + return false; + } + if (!TryParseProcessorArchitecture(attributeValue, out ProcessorArchitecture arch)) + { + return false; + } + flags |= (AssemblyNameFlags)(((int)arch) << 4); } if (attributeName.Equals("Retargetable", StringComparison.OrdinalIgnoreCase)) { - RecordNewSeenOrThrow(ref alreadySeen, AttributeKind.Retargetable); + if (!TryRecordNewSeen(ref alreadySeen, AttributeKind.Retargetable)) + { + return false; + } + if (attributeValue.Equals("Yes", StringComparison.OrdinalIgnoreCase)) { flags |= AssemblyNameFlags.Retargetable; @@ -164,38 +207,46 @@ private AssemblyNameParts Parse() } else { - ThrowInvalidAssemblyName(); + return false; } } if (attributeName.Equals("ContentType", StringComparison.OrdinalIgnoreCase)) { - RecordNewSeenOrThrow(ref alreadySeen, AttributeKind.ContentType); + if (!TryRecordNewSeen(ref alreadySeen, AttributeKind.ContentType)) + { + return false; + } + if (attributeValue.Equals("WindowsRuntime", StringComparison.OrdinalIgnoreCase)) { flags |= (AssemblyNameFlags)(((int)AssemblyContentType.WindowsRuntime) << 9); } else { - ThrowInvalidAssemblyName(); + return false; } } // Desktop compat: If we got here, the attribute name is unknown to us. Ignore it. - token = GetNextToken(); + if (!TryGetNextToken(out _, out token)) + { + return false; + } } - return new AssemblyNameParts(name, version, cultureName, flags, pkt); + result = new AssemblyNameParts(name, version, cultureName, flags, pkt); + return true; } - private Version ParseVersion(string attributeValue) + private bool TryParseVersion(string attributeValue, ref Version? version) { ReadOnlySpan attributeValueSpan = attributeValue; Span parts = stackalloc Range[5]; parts = parts.Slice(0, attributeValueSpan.Split(parts, '.')); if (parts.Length is < 2 or > 4) { - ThrowInvalidAssemblyName(); + return false; } Span versionNumbers = stackalloc ushort[4]; @@ -209,20 +260,22 @@ private Version ParseVersion(string attributeValue) if (!ushort.TryParse(attributeValueSpan[parts[i]], NumberStyles.None, NumberFormatInfo.InvariantInfo, out versionNumbers[i])) { - ThrowInvalidAssemblyName(); + return false; } } if (versionNumbers[0] == ushort.MaxValue || versionNumbers[1] == ushort.MaxValue) { - ThrowInvalidAssemblyName(); + return false; } - return + 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 string ParseCulture(string attributeValue) @@ -235,13 +288,18 @@ private static string ParseCulture(string attributeValue) return attributeValue; } - private byte[] ParsePKT(string attributeValue, bool isToken) + private static bool TryParsePKT(string attributeValue, bool isToken, ref byte[]? result) { if (attributeValue.Equals("null", StringComparison.OrdinalIgnoreCase) || attributeValue == string.Empty) - return Array.Empty(); + { + result = Array.Empty(); + return true; + } if (isToken && attributeValue.Length != 8 * 2) - ThrowInvalidAssemblyName(); + { + return false; + } byte[] pkt = new byte[attributeValue.Length / 2]; int srcIndex = 0; @@ -249,44 +307,43 @@ private byte[] ParsePKT(string attributeValue, bool isToken) { char hi = attributeValue[srcIndex++]; char lo = attributeValue[srcIndex++]; - pkt[i] = (byte)((ParseHexNybble(hi) << 4) | ParseHexNybble(lo)); + + if (!TryParseHexNybble(hi, out byte parsedHi) || !TryParseHexNybble(lo, out byte parsedLo)) + { + return false; + } + + pkt[i] = (byte)((parsedHi << 4) | parsedLo); } - return pkt; + result = pkt; + return true; } - private ProcessorArchitecture ParseProcessorArchitecture(string attributeValue) + private static bool TryParseProcessorArchitecture(string attributeValue, out ProcessorArchitecture result) { - 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 + 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 byte ParseHexNybble(char c) + private static bool TryParseHexNybble(char c, out byte parsed) { int value = HexConverter.FromChar(c); if (value == 0xFF) { - ThrowInvalidAssemblyName(); + parsed = 0; + return false; } - 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 _); + parsed = (byte)value; + return true; } private static bool IsWhiteSpace(char ch) @@ -304,15 +361,14 @@ private static bool IsWhiteSpace(char ch) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private char GetNextChar() + private bool TryGetNextChar(out char ch) { - char ch; if (_index < _input.Length) { ch = _input[_index++]; if (ch == '\0') { - ThrowInvalidAssemblyName(); + return false; } } else @@ -320,29 +376,43 @@ private char GetNextChar() ch = '\0'; } - return ch; + return true; } // // 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) + private bool TryGetNextToken(out string tokenString, out Token token) { tokenString = string.Empty; char c; while (true) { - c = GetNextChar(); + if (!TryGetNextChar(out c)) + { + token = default; + return false; + } + switch (c) { case ',': - return Token.Comma; + { + token = Token.Comma; + return true; + } case '=': - return Token.Equals; + { + token = Token.Equals; + return true; + } case '\0': - return Token.End; + { + token = Token.End; + return true; + } } if (!IsWhiteSpace(c)) @@ -351,13 +421,21 @@ private Token GetNextToken(out string tokenString) } } +#if SYSTEM_PRIVATE_CORELIB ValueStringBuilder sb = new ValueStringBuilder(stackalloc char[64]); +#else + StringBuilder sb = new(64); +#endif char quoteChar = '\0'; if (c == '\'' || c == '\"') { quoteChar = c; - c = GetNextChar(); + if (!TryGetNextChar(out c)) + { + token = default; + return false; + } } for (; ; ) @@ -367,7 +445,8 @@ private Token GetNextToken(out string tokenString) if (quoteChar != 0) { // EOS and unclosed quotes is an error - ThrowInvalidAssemblyName(); + token = default; + return false; } // Reached end of input and therefore of string break; @@ -383,11 +462,18 @@ private Token GetNextToken(out string tokenString) } if (quoteChar == 0 && (c == '\'' || c == '\"')) - ThrowInvalidAssemblyName(); + { + token = default; + return false; + } if (c == '\\') { - c = GetNextChar(); + if (!TryGetNextChar(out c)) + { + token = default; + return false; + } switch (c) { @@ -408,8 +494,8 @@ private Token GetNextToken(out string tokenString) sb.Append('\n'); break; default: - ThrowInvalidAssemblyName(); - break; //unreachable + token = default; + return false; //unreachable } } else @@ -417,7 +503,11 @@ private Token GetNextToken(out string tokenString) sb.Append(c); } - c = GetNextChar(); + if (!TryGetNextChar(out c)) + { + token = default; + return false; + } } @@ -428,11 +518,8 @@ private Token GetNextToken(out string tokenString) } tokenString = sb.ToString(); - return Token.String; + token = Token.String; + return true; } - - [DoesNotReturn] - private void ThrowInvalidAssemblyName() - => throw new FileLoadException(SR.InvalidAssemblyName, _input.ToString()); } } 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 0000000000000..9a4f8bfd3a5cd --- /dev/null +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs @@ -0,0 +1,228 @@ +// 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.CodeAnalysis; + +namespace System.Reflection.Metadata +{ +#if SYSTEM_PRIVATE_CORELIB + internal +#else + public +#endif + sealed class TypeName + { + internal const int SZArray = -1; + internal const int Pointer = -2; + internal const int ByRef = -3; + + // Positive value is array rank. + // Negative value is modifier encoded using constants above. + private readonly int _rankOrModifier; + private readonly TypeName[]? _genericArguments; + + internal TypeName(string name, AssemblyName? assemblyName, int rankOrModifier, + TypeName? underlyingType = default, + TypeName? containingType = default, + TypeName[]? genericTypeArguments = default) + { + Name = name; + AssemblyName = assemblyName; + _rankOrModifier = rankOrModifier; + UnderlyingType = underlyingType; + ContainingType = containingType; + _genericArguments = genericTypeArguments; + AssemblyQualifiedName = assemblyName is null ? name : $"{name}, {assemblyName.FullName}"; + } + + /// + /// The assembly-qualified name of the type; e.g., "System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089". + /// + /// + /// If is null, simply returns . + /// + public string AssemblyQualifiedName { get; } + + /// + /// The assembly which contains this type, or null if this was not + /// created from a fully-qualified name. + /// + public AssemblyName? AssemblyName { get; } // TODO: AssemblyName is mutable, are we fine with that? Does it not offer too much? + + /// + /// Returns true if this type represents any kind of array, regardless of the array's + /// rank or its bounds. + /// + public bool IsArray => _rankOrModifier == 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 => _genericArguments is not null; + + /// + /// Returns true if this is a "plain" type; that is, not an array, not a pointer, 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 ( returns null). + /// 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 IsElementalType => UnderlyingType is null && !IsConstructedGenericType; + + /// + /// Returns true if this is a managed pointer type (e.g., "ref int"). + /// Managed pointer types are sometimes called byref types () + /// + public bool IsManagedPointerType => _rankOrModifier == ByRef; // name inconsistent with Type.IsByRef + + /// + /// Returns true if this is a nested type (e.g., "Namespace.Containing+Nested"). + /// For nested types returns their containing type. + /// + public bool IsNestedType => ContainingType is not null; + + /// + /// Returns true if this type represents a single-dimensional, zero-indexed array (e.g., "int[]"). + /// + public bool IsSzArrayType => _rankOrModifier == SZArray; // name could be more user-friendly + + /// + /// Returns true if this type represents an unmanaged pointer (e.g., "int*" or "void*"). + /// Unmanaged pointer types are often just called pointers () + /// + public bool IsUnmanagedPointerType => _rankOrModifier == Pointer;// name inconsistent with Type.IsPointer + + /// + /// 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; + + /// + /// If this type is a nested type (see ), gets + /// the containing type. If this type is not a nested type, returns null. + /// + /// + /// For example, given "Namespace.Containing+Nested", unwraps the outermost type and returns "Namespace.Containing". + /// + public TypeName? ContainingType { get; } + + /// + /// The 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". + /// + public string Name { get; } + + /// + /// If this type is not an elemental type (see ), gets + /// the underlying type. If this type is an elemental type, returns null. + /// + /// + /// For example, given "int[][]", unwraps the outermost array and returns "int[]". + /// Given "Dictionary<string, int>", returns the generic type definition "Dictionary<,>". + /// + public TypeName? UnderlyingType { get; } + + public int GetArrayRank() + => _rankOrModifier switch + { + SZArray => 1, + _ when _rankOrModifier > 0 => _rankOrModifier, + _ => throw new ArgumentException("SR.Argument_HasToBeArrayClass") // TODO: use actual resource (used by Type.GetArrayRank) + }; + + /// + /// 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. + /// The caller controls the returned array and may mutate it freely. + /// + public TypeName[] GetGenericArguments() + => _genericArguments is not null + ? (TypeName[])_genericArguments.Clone() // we return a copy on purpose, to not allow for mutations. TODO: consider returning a ROS + : Array.Empty(); // TODO: should we throw (Levi's parser throws InvalidOperationException in such case), Type.GetGenericArguments just returns an empty array + +#if !SYSTEM_PRIVATE_CORELIB +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode("The type might be removed")] + [RequiresDynamicCode("Required by MakeArrayType")] +#else +#pragma warning disable IL2055, IL2057, IL2075, IL2096 +#endif + public Type? GetType(bool throwOnError = true, bool ignoreCase = false) + { + if (ContainingType is not null) // nested type + { + BindingFlags flagsCopiedFromClr = BindingFlags.NonPublic | BindingFlags.Public; + if (ignoreCase) + { + flagsCopiedFromClr |= BindingFlags.IgnoreCase; + } + return Make(ContainingType.GetType(throwOnError, ignoreCase)?.GetNestedType(Name, flagsCopiedFromClr)); + } + else if (UnderlyingType is null) + { + Type? type = AssemblyName is null + ? Type.GetType(Name, throwOnError, ignoreCase) + : Assembly.Load(AssemblyName).GetType(Name, throwOnError, ignoreCase); + + return Make(type); + } + + return Make(UnderlyingType.GetType(throwOnError, ignoreCase)); + + Type? Make(Type? type) + { + if (type is null || IsElementalType) + { + return type; + } + else if (IsConstructedGenericType) + { + TypeName[] genericArgs = GetGenericArguments(); + Type[] genericTypes = new Type[genericArgs.Length]; + for (int i = 0; i < genericArgs.Length; i++) + { + Type? genericArg = genericArgs[i].GetType(throwOnError, ignoreCase); + if (genericArg is null) + { + return null; + } + genericTypes[i] = genericArg; + } + + return type.MakeGenericType(genericTypes); + } + else if (IsManagedPointerType) + { + return type.MakeByRefType(); + } + else if (IsUnmanagedPointerType) + { + return type.MakePointerType(); + } + else if (IsSzArrayType) + { + return type.MakeArrayType(); + } + else + { + return type.MakeArrayType(rank: GetArrayRank()); + } + } + } +#pragma warning restore IL2055, IL2057, IL2075, IL2096 +#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 0000000000000..8aeb885fb5beb --- /dev/null +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs @@ -0,0 +1,420 @@ +// 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; + +namespace System.Reflection.Metadata +{ +#if SYSTEM_PRIVATE_CORELIB + internal +#else + public +#endif + ref struct TypeNameParser + { + private const string EndOfTypeNameDelimiters = "[]&*,+"; +#if NET8_0_OR_GREATER + private static readonly SearchValues _endOfTypeNameDelimitersSearchValues = SearchValues.Create(EndOfTypeNameDelimiters); +#endif + private readonly bool _throwOnError; + private readonly TypeNameParserOptions _parseOptions; + private ReadOnlySpan _inputString; + + private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParserOptions? options) : this() + { + _inputString = name.TrimStart(' '); // spaces at beginning are always OK + _throwOnError = throwOnError; + _parseOptions = options ?? new(); + } + + public static TypeName? Parse(ReadOnlySpan name, bool allowFullyQualifiedName = true, bool throwOnError = true, TypeNameParserOptions? options = default) + { + TypeNameParser parser = new(name, throwOnError, options); + + int recursiveDepth = 0; + TypeName? typeName = parser.ParseNextTypeName(allowFullyQualifiedName, ref recursiveDepth); + if (typeName is not null && !parser._inputString.IsEmpty) + { + return ThrowInvalidTypeNameOrReturnNull(throwOnError); + } + + return typeName; + } + + public override string ToString() => _inputString.ToString(); // TODO: add proper debugger display stuff + + private TypeName? ParseNextTypeName(bool allowFullyQualifiedName, ref int recursiveDepth) + { + if (!Dive(ref recursiveDepth)) + { + return null; + } + + List? nestedNameLengths = null; + if (!TryGetTypeNameLengthWithNestedNameLengths(_inputString, ref nestedNameLengths, out int typeNameLength)) + { + return ThrowInvalidTypeNameOrReturnNull(_throwOnError); + } + + ReadOnlySpan typeName = _inputString.Slice(0, typeNameLength); + if (!_parseOptions.ValidateIdentifier(typeName, _throwOnError)) + { + return null; + } + _inputString = _inputString.Slice(typeNameLength); + + List? genericArgs = null; // TODO: use some stack-based list in CoreLib + + // Are there any captured generic args? We'll look for "[[". + // 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 (TryStripFirstCharAndTrailingSpaces(ref _inputString, '[') + && TryStripFirstCharAndTrailingSpaces(ref _inputString, '[')) + { + int startingRecursionCheck = recursiveDepth; + int maxObservedRecursionCheck = recursiveDepth; + + ParseAnotherGenericArg: + + recursiveDepth = startingRecursionCheck; + TypeName? genericArg = ParseNextTypeName(allowFullyQualifiedName: true, ref recursiveDepth); // generic args always allow AQNs + if (genericArg is null) // parsing failed, but not thrown due to _throwOnError being true + { + return null; + } + + if (recursiveDepth > maxObservedRecursionCheck) + { + maxObservedRecursionCheck = recursiveDepth; + } + + // There had better be a ']' after the type name. + if (!TryStripFirstCharAndTrailingSpaces(ref _inputString, ']')) + { + return ThrowInvalidTypeNameOrReturnNull(_throwOnError); + } + + (genericArgs ??= new()).Add(genericArg); + + // Is there a ',[' indicating another generic type arg? + if (TryStripFirstCharAndTrailingSpaces(ref _inputString, ',')) + { + if (!TryStripFirstCharAndTrailingSpaces(ref _inputString, '[')) + { + return ThrowInvalidTypeNameOrReturnNull(_throwOnError); + } + + goto ParseAnotherGenericArg; + } + + // The only other allowable character is ']', indicating the end of + // the generic type arg list. + if (!TryStripFirstCharAndTrailingSpaces(ref _inputString, ']')) + { + return ThrowInvalidTypeNameOrReturnNull(_throwOnError); + } + + // And now that we're at the end, restore the max observed recursion count. + recursiveDepth = maxObservedRecursionCheck; + } + + // 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 (!Dive(ref recursiveDepth)) + { + return null; + } + + if (previousDecorator == TypeName.ByRef) // it's illegal for managed reference to be followed by any other decorator + { + return ThrowInvalidTypeNameOrReturnNull(_throwOnError); + } + previousDecorator = parsedDecorator; + } + + AssemblyName? 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(",")) // TODO: refactor + { + return null; + } + throw new IO.FileLoadException(SR.InvalidAssemblyName, _inputString.ToString()); +#else + return ThrowInvalidTypeNameOrReturnNull(_throwOnError); +#endif + } + + TypeName? containingType = GetContainingType(ref typeName, nestedNameLengths, assemblyName); + TypeName result = new(typeName.ToString(), assemblyName, rankOrModifier: 0, underlyingType: null, containingType, genericArgs?.ToArray()); + + if (previousDecorator != default) // some decorators were recognized + { + StringBuilder sb = new StringBuilder(typeName.Length + 4); +#if NET8_0_OR_GREATER + sb.Append(typeName); +#else + for (int i = 0; i < typeName.Length; i++) + { + sb.Append(typeName[i]); + } +#endif + while (TryParseNextDecorator(ref capturedBeforeProcessing, out int parsedModifier)) + { + // we are not reusing the input string, as it could have contain whitespaces that we want to exclude + string trimmedModifier = parsedModifier switch + { + TypeName.ByRef => "&", + TypeName.Pointer => "*", + TypeName.SZArray => "[]", + 1 => "[*]", + _ => ArrayRankToString(parsedModifier) + }; + sb.Append(trimmedModifier); + result = new(sb.ToString(), assemblyName, parsedModifier, underlyingType: result); + } + } + + return result; + } + + private static bool TryGetTypeNameLengthWithNestedNameLengths(ReadOnlySpan input, ref List? nestedNameLengths, out int totalLength) + { + bool isNestedType; + totalLength = 0; + do + { + int length = GetTypeNameLength(input.Slice(totalLength), out isNestedType); + if (length <= 0) // it's possible only for a pair of unescaped '+' characters + { + return false; + } + + if (isNestedType) + { + // do not validate the type name now, it will be validated as a whole nested type name later + (nestedNameLengths ??= new()).Add(length); + totalLength += 1; // skip the '+' sign in next search + } + totalLength += length; + } while (isNestedType); + + return true; + } + + // Normalizes "not found" to input length, since caller is expected to slice. + private static int GetTypeNameLength(ReadOnlySpan input, out bool isNestedType) + { + // 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(_endOfTypeNameDelimitersSearchValues); +#elif NET6_0_OR_GREATER + int offset = input.IndexOfAny(EndOfTypeNameDelimiters); +#else + int offset; + for (offset = 0; offset < input.Length; offset++) + { + if (EndOfTypeNameDelimiters.IndexOf(input[offset]) >= 0) { break; } + } +#endif + isNestedType = offset > 0 && offset < input.Length && input[offset] == '+'; + + return (int)Math.Min((uint)offset, (uint)input.Length); + } + + /// false means the input was invalid and parsing has failed. Empty input is valid and returns true. + private bool TryParseAssemblyName(ref AssemblyName? assemblyName) + { + ReadOnlySpan capturedBeforeProcessing = _inputString; + if (TryStripFirstCharAndTrailingSpaces(ref _inputString, ',')) + { + if (_inputString.IsEmpty) + { + _inputString = capturedBeforeProcessing; // restore the state + return false; + } + + // The only delimiter which can terminate an assembly name is ']'. + // Otherwise EOL serves as the terminator. + int assemblyNameLength = (int)Math.Min((uint)_inputString.IndexOf(']'), (uint)_inputString.Length); + ReadOnlySpan candidate = _inputString.Slice(0, assemblyNameLength); + AssemblyNameParser.AssemblyNameParts parts = default; + // TODO: make sure the parsing below is safe for untrusted input + if (!AssemblyNameParser.TryParse(candidate, ref parts)) + { + return false; + } + + // TODO: fix the perf and avoid doing it twice (missing public ctors for System.Reflection.Metadata) + assemblyName = new(candidate.ToString()); + _inputString = _inputString.Slice(assemblyNameLength); + return true; + } + + return true; + } + + private static TypeName? GetContainingType(ref ReadOnlySpan typeName, List? nestedNameLengths, AssemblyName? assemblyName) + { + if (nestedNameLengths is null) + { + return null; + } + + TypeName? containingType = null; + foreach (int nestedNameLength in nestedNameLengths) + { + Debug.Assert(nestedNameLength > 0, "TryGetTypeNameLengthWithNestedNameLengths should throw on zero lengths"); + containingType = new(typeName.Slice(0, nestedNameLength).ToString(), assemblyName, rankOrModifier: 0, null, containingType: containingType, null); + typeName = typeName.Slice(nestedNameLength + 1); // don't include the `+` in type name + } + + return containingType; + } + + private bool Dive(ref int depth) + { + if (depth >= _parseOptions.MaxRecursiveDepth) + { + if (_throwOnError) + { + return false; + } + else + { + Throw(); + } + } + depth++; + return true; + + [DoesNotReturn] + static void Throw() => throw new InvalidOperationException("SR.RecursionCheck_MaxDepthExceeded"); + } + + private static TypeName? ThrowInvalidTypeNameOrReturnNull(bool throwOnError) + => throwOnError ? throw new ArgumentException("SR.Argument_InvalidTypeName") : null; + + private static bool TryStripFirstCharAndTrailingSpaces(ref ReadOnlySpan span, char value) + { + if (!span.IsEmpty && span[0] == value) + { + span = span.Slice(1).TrimStart(' '); + return true; + } + return false; + } + + private 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 = TypeName.Pointer; + return true; + } + + if (TryStripFirstCharAndTrailingSpaces(ref input, '&')) + { + rankOrModifier = TypeName.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 ? TypeName.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; + } + + private static string ArrayRankToString(int arrayRank) + { +#if NET8_0_OR_GREATER + return string.Create(2 + arrayRank - 1, arrayRank, (buffer, rank) => + { + buffer[0] = '['; + for (int i = 1; i < rank; i++) + buffer[i] = ','; + buffer[^1] = ']'; + }); +#else + StringBuilder sb = new(2 + arrayRank - 1); + sb.Append('['); + for (int i = 1; i < arrayRank; i++) + sb.Append(','); + sb.Append(']'); + return sb.ToString(); +#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 0000000000000..b3d1f9be617dc --- /dev/null +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs @@ -0,0 +1,57 @@ +// 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; + +namespace System.Reflection.Metadata +{ +#if SYSTEM_PRIVATE_CORELIB + internal sealed +#else + public +#endif + class TypeNameParserOptions + { + private int _maxRecursiveDepth = int.MaxValue; + + public int MaxRecursiveDepth + { + get => _maxRecursiveDepth; + set + { +#if NET8_0_OR_GREATER + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(value, 0, nameof(value)); +#endif + + _maxRecursiveDepth = value; + } + } + + internal bool AllowSpacesOnly { get; set; } + + internal bool AllowEscaping { get; set; } + + internal bool StrictValidation { get; set; } + +#if SYSTEM_PRIVATE_CORELIB + internal +#else + public virtual +#endif + bool ValidateIdentifier(ReadOnlySpan candidate, bool throwOnError) + { + Debug.Assert(!StrictValidation, "TODO (ignoring the compiler warning)"); + + if (candidate.IsEmpty) + { + if (throwOnError) + { + throw new ArgumentException("TODO"); + } + return false; + } + + return true; + } + } +} diff --git a/src/libraries/Common/src/System/Reflection/TypeNameParser.cs b/src/libraries/Common/src/System/Reflection/TypeNameParser.cs index 0b69e4e18aaae..182f0fa229f3e 100644 --- a/src/libraries/Common/src/System/Reflection/TypeNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/TypeNameParser.cs @@ -636,7 +636,7 @@ private static bool NeedsEscapingInTypeName(char c) => Array.IndexOf(CharsToEscape, c) >= 0; #endif - private static string EscapeTypeName(string name) + internal static string EscapeTypeName(string name) { if (name.AsSpan().IndexOfAny(CharsToEscape) < 0) return name; @@ -652,7 +652,7 @@ private static string EscapeTypeName(string name) return sb.ToString(); } - private static string EscapeTypeName(string typeName, ReadOnlySpan nestedTypeNames) + internal static string EscapeTypeName(string typeName, ReadOnlySpan nestedTypeNames) { string fullName = EscapeTypeName(typeName); if (nestedTypeNames.Length > 0) 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 e46bd04f7d695..2e190d63d92d0 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 @@ -644,7 +644,6 @@ - @@ -1463,6 +1462,18 @@ Common\System\Reflection\TypeNameParser.cs + + Common\System\Reflection\AssemblyNameParser.cs + + + Common\System\Reflection\Metadata\TypeName.cs + + + Common\System\Reflection\Metadata\TypeNameParser.cs + + + Common\System\Reflection\Metadata\TypeNameParserOptions.cs + Common\System\Runtime\Versioning\NonVersionableAttribute.cs 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 f762dd3fa0522..2da3bd6cdc2ea 100644 --- a/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs +++ b/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs @@ -2429,13 +2429,13 @@ public sealed class TypeName } public ref partial struct TypeNameParser { - public static System.Reflection.Metadata.TypeName Parse(System.ReadOnlySpan name, bool allowFullyQualifiedName = true, System.Reflection.Metadata.TypeNameParserOptions? options = null) { throw null; } + public static System.Reflection.Metadata.TypeName? Parse(System.ReadOnlySpan name, bool allowFullyQualifiedName = true, bool throwOnError = true, System.Reflection.Metadata.TypeNameParserOptions? options = null) { throw null; } } public partial class TypeNameParserOptions { public TypeNameParserOptions() { } public int MaxRecursiveDepth { get { throw null; } set { } } - public virtual void ValidateIdentifier(System.ReadOnlySpan candidate) { } + public virtual bool ValidateIdentifier(System.ReadOnlySpan candidate, bool throwOnError) { throw null; } } public readonly partial struct TypeReference { 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 7a6fe87ef4286..2348f979e262c 100644 --- a/src/libraries/System.Reflection.Metadata/src/System.Reflection.Metadata.csproj +++ b/src/libraries/System.Reflection.Metadata/src/System.Reflection.Metadata.csproj @@ -77,7 +77,6 @@ The System.Reflection.Metadata library is built-in as part of the shared framewo - @@ -251,7 +250,12 @@ The System.Reflection.Metadata library is built-in as part of the shared framewo + + + + + diff --git a/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs deleted file mode 100644 index d08be692d5a60..0000000000000 --- a/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParser.cs +++ /dev/null @@ -1,632 +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.Buffers; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Text; - -namespace System.Reflection.Metadata -{ - public ref struct TypeNameParser - { - private const string EndOfTypeNameDelimiters = "[]&*,+"; -#if NET8_0_OR_GREATER - private static readonly SearchValues _endOfTypeNameDelimitersSearchValues = SearchValues.Create(EndOfTypeNameDelimiters); -#endif - - private readonly TypeNameParserOptions _parseOptions; - private ReadOnlySpan _inputString; - - private TypeNameParser(ReadOnlySpan name, TypeNameParserOptions? options) : this() - { - _inputString = name.TrimStart(' '); // spaces at beginning are always OK; - _parseOptions = options ?? new(); - } - - public static TypeName Parse(ReadOnlySpan name, bool allowFullyQualifiedName = true, TypeNameParserOptions? options = default) - { - TypeNameParser parser = new(name, options); - - int recursiveDepth = 0; - TypeName typeName = parser.ParseNextTypeName(allowFullyQualifiedName, ref recursiveDepth); - if (!parser._inputString.IsEmpty) - { - ThrowInvalidTypeName(); - } - - return typeName; - } - - public override string ToString() => _inputString.ToString(); // TODO: add proper debugger display stuff - - private TypeName ParseNextTypeName(bool allowFullyQualifiedName, ref int recursiveDepth) - { - Dive(ref recursiveDepth); - - List? nestedNameLengths = null; - int typeNameLength = GetTypeNameLengthWithNestedNameLengths(_inputString, ref nestedNameLengths); - - ReadOnlySpan typeName = _inputString.Slice(0, typeNameLength); - - _parseOptions.ValidateIdentifier(typeName); - - _inputString = _inputString.Slice(typeNameLength); - - List? genericArgs = null; // TODO: use some stack-based list in CoreLib - - // Are there any captured generic args? We'll look for "[[". - // 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 (TryStripFirstCharAndTrailingSpaces(ref _inputString, '[') - && TryStripFirstCharAndTrailingSpaces(ref _inputString, '[')) - { - int startingRecursionCheck = recursiveDepth; - int maxObservedRecursionCheck = recursiveDepth; - - ParseAnotherGenericArg: - - recursiveDepth = startingRecursionCheck; - TypeName genericArg = ParseNextTypeName(allowFullyQualifiedName: true, ref recursiveDepth); // generic args always allow AQNs - if (recursiveDepth > maxObservedRecursionCheck) - { - maxObservedRecursionCheck = recursiveDepth; - } - - // There had better be a ']' after the type name. - if (!TryStripFirstCharAndTrailingSpaces(ref _inputString, ']')) - { - ThrowInvalidTypeName(); - } - - (genericArgs ??= new()).Add(genericArg); - - // Is there a ',[' indicating another generic type arg? - if (TryStripFirstCharAndTrailingSpaces(ref _inputString, ',')) - { - if (!TryStripFirstCharAndTrailingSpaces(ref _inputString, '[')) - { - ThrowInvalidTypeName(); - } - - goto ParseAnotherGenericArg; - } - - // The only other allowable character is ']', indicating the end of - // the generic type arg list. - if (!TryStripFirstCharAndTrailingSpaces(ref _inputString, ']')) - { - ThrowInvalidTypeName(); - } - - // And now that we're at the end, restore the max observed recursion count. - recursiveDepth = maxObservedRecursionCheck; - } - - // 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)) - { - Dive(ref recursiveDepth); - - if (previousDecorator == TypeName.ByRef) // it's illegal for managed reference to be followed by any other decorator - { - ThrowInvalidTypeName(); - } - previousDecorator = parsedDecorator; - } - - AssemblyName? assemblyName = allowFullyQualifiedName ? ParseAssemblyName() : null; - - TypeName? containingType = GetContainingType(ref typeName, nestedNameLengths, assemblyName); - TypeName result = new(typeName.ToString(), assemblyName, rankOrModifier: 0, underlyingType: null, containingType, genericArgs?.ToArray()); - - if (previousDecorator != default) // some decorators were recognized - { - StringBuilder sb = new StringBuilder(typeName.Length + 4); -#if NET8_0_OR_GREATER - sb.Append(typeName); -#else - for (int i = 0; i < typeName.Length; i++) - { - sb.Append(typeName[i]); - } -#endif - while (TryParseNextDecorator(ref capturedBeforeProcessing, out int parsedModifier)) - { - // we are not reusing the input string, as it could have contain whitespaces that we want to exclude - string trimmedModifier = parsedModifier switch - { - TypeName.ByRef => "&", - TypeName.Pointer => "*", - TypeName.SZArray => "[]", - 1 => "[*]", - _ => ArrayRankToString(parsedModifier) - }; - sb.Append(trimmedModifier); - result = new(sb.ToString(), assemblyName, parsedModifier, underlyingType: result); - } - } - - return result; - } - - private static int GetTypeNameLengthWithNestedNameLengths(ReadOnlySpan input, ref List? nestedNameLengths) - { - bool isNestedType; - int totalLength = 0; - do - { - int length = GetTypeNameLength(input.Slice(totalLength), out isNestedType); - Debug.Assert(length > 0, "GetTypeNameLength should never return a negative value"); - - if (isNestedType) - { - // do not validate the type name now, it will be validated as a whole nested type name later - (nestedNameLengths ??= new()).Add(length); - totalLength += 1; // skip the '+' sign in next search - } - totalLength += length; - } while (isNestedType); - - return totalLength; - } - - // Normalizes "not found" to input length, since caller is expected to slice. - private static int GetTypeNameLength(ReadOnlySpan input, out bool isNestedType) - { - // 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(_endOfTypeNameDelimitersSearchValues); -#elif NET6_0_OR_GREATER - int offset = input.IndexOfAny(EndOfTypeNameDelimiters); -#else - int offset; - for (offset = 0; offset < input.Length; offset++) - { - if (EndOfTypeNameDelimiters.IndexOf(input[offset]) >= 0) { break; } - } -#endif - isNestedType = offset > 0 && offset < input.Length && input[offset] == '+'; - - return (int)Math.Min((uint)offset, (uint)input.Length); - } - - private AssemblyName? ParseAssemblyName() - { - if (TryStripFirstCharAndTrailingSpaces(ref _inputString, ',')) - { - // The only delimiter which can terminate an assembly name is ']'. - // Otherwise EOL serves as the terminator. - int assemblyNameLength = (int)Math.Min((uint)_inputString.IndexOf(']'), (uint)_inputString.Length); - - string candidate = _inputString.Slice(0, assemblyNameLength).ToString(); - _inputString = _inputString.Slice(assemblyNameLength); - // we may want to consider throwing a different exception for an empty string here - // TODO: make sure the parsing below is safe for untrusted input - return new AssemblyName(candidate); - } - - return null; - } - - private static TypeName? GetContainingType(ref ReadOnlySpan typeName, List? nestedNameLengths, AssemblyName? assemblyName) - { - if (nestedNameLengths is null) - { - return null; - } - - TypeName? containingType = null; - foreach (int nestedNameLength in nestedNameLengths) - { - containingType = new(typeName.Slice(0, nestedNameLength).ToString(), assemblyName, rankOrModifier: 0, null, containingType: containingType, null); - typeName = typeName.Slice(nestedNameLength + 1); // don't include the `+` in type name - } - - return containingType; - } - - private void Dive(ref int depth) - { - if (depth >= _parseOptions.MaxRecursiveDepth) - { - Throw(); - } - depth++; - - [DoesNotReturn] - static void Throw() => throw new InvalidOperationException("SR.RecursionCheck_MaxDepthExceeded"); - } - - [DoesNotReturn] - private static void ThrowInvalidTypeName() => throw new ArgumentException("SR.Argument_InvalidTypeName"); - - private static bool TryStripFirstCharAndTrailingSpaces(ref ReadOnlySpan span, char value) - { - if (!span.IsEmpty && span[0] == value) - { - span = span.Slice(1).TrimStart(' '); - return true; - } - return false; - } - - private 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 = TypeName.Pointer; - return true; - } - - if (TryStripFirstCharAndTrailingSpaces(ref input, '&')) - { - rankOrModifier = TypeName.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 ? TypeName.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; - } - - private static string ArrayRankToString(int arrayRank) - { -#if NET8_0_OR_GREATER - return string.Create(2 + arrayRank - 1, arrayRank, (buffer, rank) => - { - buffer[0] = '['; - for (int i = 1; i < rank; i++) - buffer[i] = ','; - buffer[^1] = ']'; - }); -#else - StringBuilder sb = new(2 + arrayRank - 1); - sb.Append('['); - for (int i = 1; i < arrayRank; i++) - sb.Append(','); - sb.Append(']'); - return sb.ToString(); -#endif - } - } - - public sealed class TypeName - { - internal const int SZArray = -1; - internal const int Pointer = -2; - internal const int ByRef = -3; - - // Positive value is array rank. - // Negative value is modifier encoded using constants above. - private readonly int _rankOrModifier; - private readonly TypeName[]? _genericArguments; - - internal TypeName(string name, AssemblyName? assemblyName, int rankOrModifier, - TypeName? underlyingType = default, - TypeName? containingType = default, - TypeName[]? genericTypeArguments = default) - { - Name = name; - AssemblyName = assemblyName; - _rankOrModifier = rankOrModifier; - UnderlyingType = underlyingType; - ContainingType = containingType; - _genericArguments = genericTypeArguments; - AssemblyQualifiedName = assemblyName is null ? name : $"{name}, {assemblyName.FullName}"; - } - - /// - /// The assembly-qualified name of the type; e.g., "System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089". - /// - /// - /// If is null, simply returns . - /// - public string AssemblyQualifiedName { get; } - - /// - /// The assembly which contains this type, or null if this was not - /// created from a fully-qualified name. - /// - public AssemblyName? AssemblyName { get; } // TODO: AssemblyName is mutable, are we fine with that? Does it not offer too much? - - /// - /// Returns true if this type represents any kind of array, regardless of the array's - /// rank or its bounds. - /// - public bool IsArray => _rankOrModifier == 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 => _genericArguments is not null; - - /// - /// Returns true if this is a "plain" type; that is, not an array, not a pointer, 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 ( returns null). - /// 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 IsElementalType => UnderlyingType is null && !IsConstructedGenericType; - - /// - /// Returns true if this is a managed pointer type (e.g., "ref int"). - /// Managed pointer types are sometimes called byref types () - /// - public bool IsManagedPointerType => _rankOrModifier == ByRef; // name inconsistent with Type.IsByRef - - /// - /// Returns true if this is a nested type (e.g., "Namespace.Containing+Nested"). - /// For nested types returns their containing type. - /// - public bool IsNestedType => ContainingType is not null; - - /// - /// Returns true if this type represents a single-dimensional, zero-indexed array (e.g., "int[]"). - /// - public bool IsSzArrayType => _rankOrModifier == SZArray; // name could be more user-friendly - - /// - /// Returns true if this type represents an unmanaged pointer (e.g., "int*" or "void*"). - /// Unmanaged pointer types are often just called pointers () - /// - public bool IsUnmanagedPointerType => _rankOrModifier == Pointer;// name inconsistent with Type.IsPointer - - /// - /// 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; - - /// - /// If this type is a nested type (see ), gets - /// the containing type. If this type is not a nested type, returns null. - /// - /// - /// For example, given "Namespace.Containing+Nested", unwraps the outermost type and returns "Namespace.Containing". - /// - public TypeName? ContainingType { get; } - - /// - /// The 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". - /// - public string Name { get; } - - /// - /// If this type is not an elemental type (see ), gets - /// the underlying type. If this type is an elemental type, returns null. - /// - /// - /// For example, given "int[][]", unwraps the outermost array and returns "int[]". - /// Given "Dictionary<string, int>", returns the generic type definition "Dictionary<,>". - /// - public TypeName? UnderlyingType { get; } - - public int GetArrayRank() - => _rankOrModifier switch - { - SZArray => 1, - _ when _rankOrModifier > 0 => _rankOrModifier, - _ => throw new ArgumentException("SR.Argument_HasToBeArrayClass") // TODO: use actual resource (used by Type.GetArrayRank) - }; - - /// - /// 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. - /// The caller controls the returned array and may mutate it freely. - /// - public TypeName[] GetGenericArguments() - => _genericArguments is not null - ? (TypeName[])_genericArguments.Clone() // we return a copy on purpose, to not allow for mutations. TODO: consider returning a ROS - : Array.Empty(); // TODO: should we throw (Levi's parser throws InvalidOperationException in such case), Type.GetGenericArguments just returns an empty array - -#if NET8_0_OR_GREATER - [RequiresUnreferencedCode("The type might be removed")] - [RequiresDynamicCode("Required by MakeArrayType")] -#else -#pragma warning disable IL2055, IL2057, IL2075, IL2096 -#endif - public Type? GetType(bool throwOnError = true, bool ignoreCase = false) - { - if (ContainingType is not null) // nested type - { - BindingFlags flagsCopiedFromClr = BindingFlags.NonPublic | BindingFlags.Public; - if (ignoreCase) - { - flagsCopiedFromClr |= BindingFlags.IgnoreCase; - } - return Make(ContainingType.GetType(throwOnError, ignoreCase)?.GetNestedType(Name, flagsCopiedFromClr)); - } - else if (UnderlyingType is null) - { - Type? type = AssemblyName is null - ? Type.GetType(Name, throwOnError, ignoreCase) - : Assembly.Load(AssemblyName).GetType(Name, throwOnError, ignoreCase); - - return Make(type); - } - - return Make(UnderlyingType.GetType(throwOnError, ignoreCase)); - - Type? Make(Type? type) - { - if (type is null || IsElementalType) - { - return type; - } - else if (IsConstructedGenericType) - { - TypeName[] genericArgs = GetGenericArguments(); - Type[] genericTypes = new Type[genericArgs.Length]; - for (int i = 0; i < genericArgs.Length; i++) - { - Type? genericArg = genericArgs[i].GetType(throwOnError, ignoreCase); - if (genericArg is null) - { - return null; - } - genericTypes[i] = genericArg; - } - - return type.MakeGenericType(genericTypes); - } - else if (IsManagedPointerType) - { - return type.MakeByRefType(); - } - else if (IsUnmanagedPointerType) - { - return type.MakePointerType(); - } - else if (IsSzArrayType) - { - return type.MakeArrayType(); - } - else - { - return type.MakeArrayType(rank: GetArrayRank()); - } - } - } - } -#pragma warning restore IL2055, IL2057, IL2075, IL2096 - - public class TypeNameParserOptions - { - private int _maxRecursiveDepth = int.MaxValue; - - public int MaxRecursiveDepth - { - get => _maxRecursiveDepth; - set - { -#if NET8_0_OR_GREATER - ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(value, 0, nameof(value)); -#endif - - _maxRecursiveDepth = value; - } - } - - internal bool AllowSpacesOnly { get; set; } - - internal bool AllowEscaping { get; set; } - - internal bool StrictValidation { get; set; } - - public virtual void ValidateIdentifier(ReadOnlySpan candidate) - { - if (candidate.IsEmpty) - { - throw new ArgumentException("TODO"); - } - } - } - - internal class SafeTypeNameParserOptions : TypeNameParserOptions - { - public SafeTypeNameParserOptions(bool allowNonAsciiIdentifiers) - { - AllowNonAsciiIdentifiers = allowNonAsciiIdentifiers; - MaxRecursiveDepth = 10; - } - - public bool AllowNonAsciiIdentifiers { get; set; } - - public override void ValidateIdentifier(ReadOnlySpan candidate) - { - base.ValidateIdentifier(candidate); - - // allow specific ASCII chars - } - } - - internal class RoslynTypeNameParserOptions : TypeNameParserOptions - { - public override void ValidateIdentifier(ReadOnlySpan candidate) - { - // it seems that Roslyn is not performing any kind of validation - } - } -} diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs index 9027314d9438f..1bfadab14eca6 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs @@ -20,7 +20,24 @@ public void SpacesAtTheBeginningAreOK(string input, string expectedName) [InlineData(" ")] [InlineData(" ")] public void EmptyStringsAreNotAllowed(string input) - => Assert.Throws(() => TypeNameParser.Parse(input.AsSpan())); + { + Assert.Throws(() => TypeNameParser.Parse(input.AsSpan(), throwOnError: true)); + + Assert.Null(TypeNameParser.Parse(input.AsSpan(), throwOnError: false)); + } + + [Theory] + [InlineData("Namespace.Containing++Nested")] // a pair of '++' + [InlineData("TypeNameFollowedBySome[] crap")] // unconsumed characters + [InlineData("MissingAssemblyName, ")] + [InlineData("ExtraComma, ,")] + [InlineData("ExtraComma, , System.Runtime")] + public void InvalidTypeNamesAreNotAllowed(string input) + { + Assert.Throws(() => TypeNameParser.Parse(input.AsSpan(), throwOnError: true)); + + Assert.Null(TypeNameParser.Parse(input.AsSpan(), throwOnError: false)); + } [Theory] [InlineData("Namespace.Kość", "Namespace.Kość")] @@ -30,7 +47,11 @@ public void UnicodeCharactersAreAllowedByDefault(string input, string expectedNa [Theory] [InlineData("Namespace.Kość")] public void UsersCanCustomizeIdentifierValidation(string input) - => Assert.Throws(() => TypeNameParser.Parse(input.AsSpan(), true, new NonAsciiNotAllowed())); + { + Assert.Throws(() => TypeNameParser.Parse(input.AsSpan(), true, throwOnError: true, new NonAsciiNotAllowed())); + + Assert.Null(TypeNameParser.Parse(input.AsSpan(), true, throwOnError: false, new NonAsciiNotAllowed())); + } public static IEnumerable TypeNamesWithAssemblyNames() { @@ -253,9 +274,12 @@ public class NestedNonGeneric_3 { } internal sealed class NonAsciiNotAllowed : TypeNameParserOptions { - public override void ValidateIdentifier(ReadOnlySpan candidate) + public override bool ValidateIdentifier(ReadOnlySpan candidate, bool throwOnError) { - base.ValidateIdentifier(candidate); + if (!base.ValidateIdentifier(candidate, throwOnError)) + { + return false; + } #if NET8_0_OR_GREATER if (!Ascii.IsValid(candidate)) @@ -263,8 +287,13 @@ public override void ValidateIdentifier(ReadOnlySpan candidate) if (candidate.ToArray().Any(c => c >= 128)) #endif { - throw new ArgumentException("Non ASCII char found"); + if (throwOnError) + { + throw new ArgumentException("Non ASCII char found"); + } + return false; } + return true; } } From 2dbd091e9033dd79289c5066ab147c2eca162219 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Thu, 1 Feb 2024 08:14:07 +0100 Subject: [PATCH 10/48] integrate with System.Private.CoreLib: - report errorIndex in the ex message - fix Full Framework build - fix nested types support - implement NativeAOT part --- .../System.Private.CoreLib.csproj | 1 - .../Reflection/TypeNameParser.CoreCLR.cs | 66 +-- .../src/System/Reflection/TypeNameResolver.cs | 382 ------------------ .../src/System/Type.CoreCLR.cs | 9 +- .../Reflection/TypeNameParser.NativeAot.cs | 48 ++- .../System/Reflection/AssemblyNameParser.cs | 12 +- .../Reflection/Metadata/TypeNameParser.cs | 136 +++++-- .../Reflection/TypeNameParser.Helpers.cs | 133 ++++++ .../System.Private.CoreLib.Shared.projitems | 4 +- .../ref/System.Reflection.Metadata.cs | 2 +- .../tests/Metadata/TypeNameParserTests.cs | 1 + 11 files changed, 316 insertions(+), 478 deletions(-) delete mode 100644 src/coreclr/System.Private.CoreLib/src/System/Reflection/TypeNameResolver.cs create mode 100644 src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs diff --git a/src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj b/src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj index 1214a877c9234..bf34c30dfb454 100644 --- a/src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj +++ b/src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj @@ -202,7 +202,6 @@ - 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 2af4bb792d458..432e109a4391a 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) + var 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,26 @@ internal partial struct TypeNameParser bool ignoreCase, Assembly topLevelAssembly) { - return new TypeNameParser(typeName) + var parsed = Metadata.TypeNameParser.Parse(typeName, + allowFullyQualifiedName: true, // let it get parsed, but throw when topLevelAssembly was specified + throwOnError: throwOnError); + + if (parsed is null) + { + return null; + } + else if (parsed.AssemblyName is not null && topLevelAssembly 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 +112,13 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName, RuntimeAssembly requestingAssembly = scope.GetRuntimeAssembly(); - RuntimeType? type = (RuntimeType?)new TypeNameParser(typeName) + var parsed = Metadata.TypeNameParser.Parse(typeName, allowFullyQualifiedName: true, throwOnError: true)!; + RuntimeType? type = (RuntimeType?)new TypeNameParser() { _throwOnError = true, _suppressContextualReflectionContext = true, _requestingAssembly = requestingAssembly - }.Parse(); + }.Resolve(parsed); Debug.Assert(type != null); @@ -124,13 +142,22 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName, return null; } - RuntimeType? type = (RuntimeType?)new TypeNameParser(typeName) + var parsed = Metadata.TypeNameParser.Parse(typeName, + allowFullyQualifiedName: true, + throwOnError: 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 +165,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 +178,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,7 +189,7 @@ 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 typeName, ReadOnlySpan nestedTypeNames, AssemblyName? assemblyNameIfAny) { Assembly? assembly; diff --git a/src/coreclr/System.Private.CoreLib/src/System/Reflection/TypeNameResolver.cs b/src/coreclr/System.Private.CoreLib/src/System/Reflection/TypeNameResolver.cs deleted file mode 100644 index 13947a3fde214..0000000000000 --- a/src/coreclr/System.Private.CoreLib/src/System/Reflection/TypeNameResolver.cs +++ /dev/null @@ -1,382 +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.IO; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Loader; -using System.Threading; - -namespace System.Reflection -{ - internal struct TypeNameResolver - { - private Func? _assemblyResolver; - private Func? _typeResolver; - private bool _throwOnError; - private bool _ignoreCase; - private bool _extensibleParser; - private bool _requireAssemblyQualifiedName; - private bool _suppressContextualReflectionContext; - private Assembly? _requestingAssembly; - private Assembly? _topLevelAssembly; - - [RequiresUnreferencedCode("The type might be removed")] - internal static Type? GetType( - string typeName, - Assembly requestingAssembly, - bool throwOnError = false, - bool ignoreCase = false) - { - return GetType(typeName, assemblyResolver: null, typeResolver: null, requestingAssembly: requestingAssembly, - throwOnError: throwOnError, ignoreCase: ignoreCase, extensibleParser: false); - } - - [RequiresUnreferencedCode("The type might be removed")] - internal static Type? GetType( - string typeName, - Func? assemblyResolver, - Func? typeResolver, - Assembly? requestingAssembly, - bool throwOnError = false, - bool ignoreCase = false, - bool extensibleParser = true) - { - ArgumentNullException.ThrowIfNull(typeName); - - // Compat: Empty name throws TypeLoadException instead of - // the natural ArgumentException - if (typeName.Length == 0) - { - if (throwOnError) - throw new TypeLoadException(SR.Arg_TypeLoadNullStr); - return null; - } - - var parsed = Metadata.TypeNameParser.Parse(typeName, throwOnError: throwOnError); - if (parsed is null) - { - return null; - } - - return new TypeNameResolver() - { - _assemblyResolver = assemblyResolver, - _typeResolver = typeResolver, - _throwOnError = throwOnError, - _ignoreCase = ignoreCase, - _extensibleParser = extensibleParser, - _requestingAssembly = requestingAssembly - }.Resolve(parsed); - } - - [RequiresUnreferencedCode("The type might be removed")] - internal static Type? GetType( - string typeName, - bool throwOnError, - bool ignoreCase, - Assembly topLevelAssembly) - { - var parsed = Metadata.TypeNameParser.Parse(typeName, - allowFullyQualifiedName: true, // let it get parsed, but throw when topLevelAssembly was specified - throwOnError: throwOnError); - - if (parsed is null) - { - return null; - } - else if (parsed.AssemblyName is not null && topLevelAssembly is not null) - { - return throwOnError ? throw new ArgumentException(SR.Argument_AssemblyGetTypeCannotSpecifyAssembly) : null; - } - - return new TypeNameResolver() - { - _throwOnError = throwOnError, - _ignoreCase = ignoreCase, - _topLevelAssembly = topLevelAssembly, - _requestingAssembly = topLevelAssembly - }.Resolve(parsed); - } - - // Resolve type name referenced by a custom attribute metadata. - // It uses the standard Type.GetType(typeName, throwOnError: true) algorithm with the following modifications: - // - ContextualReflectionContext is not taken into account - // - The dependency between the returned type and the requesting assembly is recorded for the purpose of - // lifetime tracking of collectible types. - [RequiresUnreferencedCode("TODO: introduce dedicated overload that does not use forbidden API")] - internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName, RuntimeModule scope) - { - ArgumentException.ThrowIfNullOrEmpty(typeName); - - RuntimeAssembly requestingAssembly = scope.GetRuntimeAssembly(); - - var parsed = Metadata.TypeNameParser.Parse(typeName, allowFullyQualifiedName: requestingAssembly is null, throwOnError: true)!; // adsitnik allowFullyQualifiedName part might be wrong - RuntimeType? type = (RuntimeType?)new TypeNameResolver() - { - _throwOnError = true, - _suppressContextualReflectionContext = true, - _requestingAssembly = requestingAssembly - }.Resolve(parsed); - - Debug.Assert(type != null); - - RuntimeTypeHandle.RegisterCollectibleTypeDependency(type, requestingAssembly); - - return type; - } - - // Used by VM - [RequiresUnreferencedCode("TODO: introduce dedicated overload that does not use forbidden API")] - internal static unsafe RuntimeType? GetTypeHelper(char* pTypeName, RuntimeAssembly? requestingAssembly, - bool throwOnError, bool requireAssemblyQualifiedName) - { - ReadOnlySpan typeName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pTypeName); - - // Compat: Empty name throws TypeLoadException instead of - // the natural ArgumentException - if (typeName.Length == 0) - { - if (throwOnError) - throw new TypeLoadException(SR.Arg_TypeLoadNullStr); - return null; - } - - var parsed = Metadata.TypeNameParser.Parse(typeName, - allowFullyQualifiedName: true, - throwOnError: throwOnError); - - if (parsed is null) - { - return null; - } - - RuntimeType? type = (RuntimeType?)new TypeNameResolver() - { - _requestingAssembly = requestingAssembly, - _throwOnError = throwOnError, - _suppressContextualReflectionContext = true, - _requireAssemblyQualifiedName = requireAssemblyQualifiedName, - }.Resolve(parsed); - - if (type != null) - RuntimeTypeHandle.RegisterCollectibleTypeDependency(type, requestingAssembly); - - return type; - } - - private Assembly? ResolveAssembly(AssemblyName assemblyName) - { - Assembly? assembly; - if (_assemblyResolver is not null) - { - assembly = _assemblyResolver(assemblyName); - if (assembly is null && _throwOnError) - { - throw new FileNotFoundException(SR.Format(SR.FileNotFound_ResolveAssembly, assemblyName)); - } - } - else - { - assembly = RuntimeAssembly.InternalLoad(assemblyName, ref Unsafe.NullRef(), - _suppressContextualReflectionContext ? null : AssemblyLoadContext.CurrentContextualReflectionContext, - requestingAssembly: (RuntimeAssembly?)_requestingAssembly, throwOnFileNotFound: _throwOnError); - } - return assembly; - } - - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", - Justification = "TypeNameParser.GetType is marked as RequiresUnreferencedCode.")] - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075:UnrecognizedReflectionPattern", - Justification = "TypeNameParser.GetType is marked as RequiresUnreferencedCode.")] - private Type? GetType(Metadata.TypeName typeName) - { - Assembly? assembly; - - if (typeName.AssemblyName is not null) - { - assembly = ResolveAssembly(typeName.AssemblyName); - if (assembly is null) - return null; - } - else - { - assembly = _topLevelAssembly; - } - - Type? type; - - // Resolve the top level type. - if (_typeResolver is not null) - { - string escapedTypeName = TypeNameParser.EscapeTypeName(typeName.Name); - - type = _typeResolver(assembly, escapedTypeName, _ignoreCase); - - if (type is null) - { - if (_throwOnError) - { - throw new TypeLoadException(assembly is null ? - SR.Format(SR.TypeLoad_ResolveType, escapedTypeName) : - SR.Format(SR.TypeLoad_ResolveTypeFromAssembly, escapedTypeName, assembly.FullName)); - } - return null; - } - } - else - { - if (assembly is null) - { - if (_requireAssemblyQualifiedName) - { - if (_throwOnError) - { - throw new TypeLoadException(SR.Format(SR.TypeLoad_ResolveType, TypeNameParser.EscapeTypeName(typeName.Name))); - } - return null; - } - return GetTypeFromDefaultAssemblies(typeName.Name, nestedTypeNames: ReadOnlySpan.Empty); - } - - if (assembly is RuntimeAssembly runtimeAssembly) - { - // Compat: Non-extensible parser allows ambiguous matches with ignore case lookup - if (!_extensibleParser || !_ignoreCase) - { - return runtimeAssembly.GetTypeCore(typeName.Name, nestedTypeNames: ReadOnlySpan.Empty, throwOnError: _throwOnError, ignoreCase: _ignoreCase); - } - type = runtimeAssembly.GetTypeCore(typeName.Name, 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(TypeNameParser.EscapeTypeName(typeName.Name), throwOnError: _throwOnError, ignoreCase: _ignoreCase); - } - - if (type is null) - return null; - } - - return type; - } - - private Type? GetTypeFromDefaultAssemblies(string typeName, ReadOnlySpan nestedTypeNames) - { - RuntimeAssembly? requestingAssembly = (RuntimeAssembly?)_requestingAssembly; - if (requestingAssembly is not null) - { - Type? type = ((RuntimeAssembly)requestingAssembly).GetTypeCore(typeName, nestedTypeNames, throwOnError: false, ignoreCase: _ignoreCase); - if (type is not null) - return type; - } - - RuntimeAssembly coreLib = (RuntimeAssembly)typeof(object).Assembly; - if (requestingAssembly != coreLib) - { - Type? type = ((RuntimeAssembly)coreLib).GetTypeCore(typeName, nestedTypeNames, throwOnError: false, ignoreCase: _ignoreCase); - if (type is not null) - return type; - } - - RuntimeAssembly? resolvedAssembly = AssemblyLoadContext.OnTypeResolve(requestingAssembly, TypeNameParser.EscapeTypeName(typeName, nestedTypeNames)); - if (resolvedAssembly is not null) - { - Type? type = resolvedAssembly.GetTypeCore(typeName, nestedTypeNames, throwOnError: false, ignoreCase: _ignoreCase); - if (type is not null) - return type; - } - - if (_throwOnError) - throw new TypeLoadException(SR.Format(SR.TypeLoad_ResolveTypeFromAssembly, TypeNameParser.EscapeTypeName(typeName), (requestingAssembly ?? coreLib).FullName)); - - return null; - } - - [RequiresUnreferencedCode("The type might be removed")] - private Type? Resolve(Metadata.TypeName typeName) - { - if (typeName.ContainingType is not null) // nested type - { - BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Public; - if (_ignoreCase) - { - flags |= BindingFlags.IgnoreCase; - } - Type? containingType = Resolve(typeName.ContainingType); - if (containingType is null) - { - return null; - } - Type? nestedType = containingType.GetNestedType(typeName.Name, flags); - if (nestedType is null) - { - if (_throwOnError) - { - throw new TypeLoadException(SR.Format(SR.TypeLoad_ResolveNestedType, typeName.Name, typeName.ContainingType.Name)); - } - return null; - } - - return Make(nestedType, typeName); - } - else if (typeName.UnderlyingType is null) - { - Type? type = GetType(typeName); - - return Make(type, typeName); - } - - return Make(Resolve(typeName.UnderlyingType), typeName); - } - - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2055:UnrecognizedReflectionPattern", - Justification = "Used to implement resolving types from strings.")] - [UnconditionalSuppressMessage("AotAnalysis", "IL3050:AotUnfriendlyApi", - Justification = "Used to implement resolving types from strings.")] - [RequiresUnreferencedCode("The type might be removed")] - private Type? Make(Type? type, Metadata.TypeName typeName) - { - if (type is null || typeName.IsElementalType) - { - return type; - } - else if (typeName.IsConstructedGenericType) - { - Metadata.TypeName[] genericArgs = typeName.GetGenericArguments(); - Type[] genericTypes = new Type[genericArgs.Length]; - for (int i = 0; i < genericArgs.Length; i++) - { - Type? genericArg = Resolve(genericArgs[i]); - if (genericArg is null) - { - return null; - } - genericTypes[i] = genericArg; - } - - return type.MakeGenericType(genericTypes); - } - else if (typeName.IsManagedPointerType) - { - return type.MakeByRefType(); - } - else if (typeName.IsUnmanagedPointerType) - { - return type.MakePointerType(); - } - else if (typeName.IsSzArrayType) - { - return type.MakeArrayType(); - } - else - { - return type.MakeArrayType(rank: typeName.GetArrayRank()); - } - } - } -} 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 99d0a211e8f5c..81caecb09c99a 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; @@ -35,7 +34,7 @@ public abstract partial class Type : MemberInfo, IReflect public static Type? GetType(string typeName) { StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller; - return TypeNameResolver.GetType(typeName, Assembly.GetExecutingAssembly(ref stackMark)); + return TypeNameParser.GetType(typeName, Assembly.GetExecutingAssembly(ref stackMark)); } [RequiresUnreferencedCode("The type might be removed")] @@ -46,7 +45,7 @@ public abstract partial class Type : MemberInfo, IReflect Func? typeResolver) { StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller; - return TypeNameResolver.GetType(typeName, assemblyResolver, typeResolver, + return TypeNameParser.GetType(typeName, assemblyResolver, typeResolver, ((assemblyResolver != null) && (typeResolver != null)) ? null : Assembly.GetExecutingAssembly(ref stackMark)); } @@ -59,7 +58,7 @@ public abstract partial class Type : MemberInfo, IReflect bool throwOnError) { StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller; - return TypeNameResolver.GetType(typeName, assemblyResolver, typeResolver, + return TypeNameParser.GetType(typeName, assemblyResolver, typeResolver, ((assemblyResolver != null) && (typeResolver != null)) ? null : Assembly.GetExecutingAssembly(ref stackMark), throwOnError: throwOnError); } @@ -74,7 +73,7 @@ public abstract partial class Type : MemberInfo, IReflect bool ignoreCase) { StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller; - return TypeNameResolver.GetType(typeName, assemblyResolver, typeResolver, + return TypeNameParser.GetType(typeName, assemblyResolver, typeResolver, ((assemblyResolver != null) && (typeResolver != null)) ? null : Assembly.GetExecutingAssembly(ref stackMark), throwOnError: throwOnError, ignoreCase: ignoreCase); } 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 e370cb4eed0a8..b18ada21d85bf 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) + var parsed = Metadata.TypeNameParser.Parse(typeName, throwOnError: 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,37 @@ internal partial struct TypeNameParser bool ignoreCase, Assembly topLevelAssembly) { - return new TypeNameParser(typeName) + var parsed = Metadata.TypeNameParser.Parse(typeName, + allowFullyQualifiedName: true, // let it get parsed, but throw when topLevelAssembly was specified + throwOnError: throwOnError); + + if (parsed is null) + { + return null; + } + else if (parsed.AssemblyName is not null && topLevelAssembly is not null) + { + return throwOnError ? throw new ArgumentException(SR.Argument_AssemblyGetTypeCannotSpecifyAssembly) : null; + } + + return new TypeNameParser() { _throwOnError = throwOnError, _ignoreCase = ignoreCase, _topLevelAssembly = topLevelAssembly, - }.Parse(); - } - - private bool CheckTopLevelAssemblyQualifiedName() - { - if (_topLevelAssembly is not null) - { - if (_throwOnError) - throw new ArgumentException(SR.Argument_AssemblyGetTypeCannotSpecifyAssembly); - return false; - } - return true; + }.Resolve(parsed); } - 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); } else { - assembly = RuntimeAssemblyInfo.GetRuntimeAssemblyIfExists(RuntimeAssemblyName.Parse(assemblyName)); + assembly = RuntimeAssemblyInfo.GetRuntimeAssemblyIfExists(RuntimeAssemblyName.Parse(assemblyName.FullName)); // TODO adsitnik: remove the redundant parsing } if (assembly is null && _throwOnError) @@ -113,7 +119,7 @@ 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 typeName, ReadOnlySpan nestedTypeNames, AssemblyName? assemblyNameIfAny) { Assembly? assembly; diff --git a/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs b/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs index 04e626f25c4dd..948dbe175e385 100644 --- a/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs @@ -241,9 +241,13 @@ private bool TryParse(ref AssemblyNameParts result) private 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; @@ -258,7 +262,13 @@ private bool TryParseVersion(string attributeValue, ref Version? version) break; } - if (!ushort.TryParse(attributeValueSpan[parts[i]], NumberStyles.None, NumberFormatInfo.InvariantInfo, out versionNumbers[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; } diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs index 8aeb885fb5beb..9c40a57129136 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs @@ -1,10 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#if SYSTEM_PRIVATE_CORELIB +#define NET8_0_OR_GREATER +#endif using System.Buffers; using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Text; namespace System.Reflection.Metadata @@ -26,30 +28,64 @@ ref struct TypeNameParser private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParserOptions? options) : this() { - _inputString = name.TrimStart(' '); // spaces at beginning are always OK + _inputString = name; _throwOnError = throwOnError; _parseOptions = options ?? new(); } - public static TypeName? Parse(ReadOnlySpan name, bool allowFullyQualifiedName = true, bool throwOnError = true, TypeNameParserOptions? options = default) + public static TypeName? Parse(ReadOnlySpan typeName, bool allowFullyQualifiedName = true, bool throwOnError = true, TypeNameParserOptions? options = default) { - TypeNameParser parser = new(name, throwOnError, options); + ReadOnlySpan trimmedName = TrimStart(typeName); // whitespaces at beginning are always OK + if (trimmedName.IsEmpty) + { + // whitespace input needs to report the error index as 0 + return ThrowInvalidTypeNameOrReturnNull(throwOnError, 0); + } int recursiveDepth = 0; - TypeName? typeName = parser.ParseNextTypeName(allowFullyQualifiedName, ref recursiveDepth); - if (typeName is not null && !parser._inputString.IsEmpty) + TypeNameParser parser = new(trimmedName, throwOnError, options); + TypeName? parsedName = parser.ParseNextTypeName(allowFullyQualifiedName, ref recursiveDepth); + + if (parsedName is not null && parser._inputString.IsEmpty) // unconsumed input == error + { + return parsedName; + } + else if (!throwOnError) + { + return null; + } + + // there was an error and we need to throw +#if !SYSTEM_PRIVATE_CORELIB + if (recursiveDepth >= parser._parseOptions.MaxRecursiveDepth) { - return ThrowInvalidTypeNameOrReturnNull(throwOnError); + throw new InvalidOperationException("SR.RecursionCheck_MaxDepthExceeded"); } +#endif + int errorIndex = typeName.Length - parser._inputString.Length; + return ThrowInvalidTypeNameOrReturnNull(throwOnError, errorIndex); - return typeName; + static TypeName? ThrowInvalidTypeNameOrReturnNull(bool throwOnError, int errorIndex = 0) + { + if (!throwOnError) + { + return null; + } + +#if SYSTEM_PRIVATE_CORELIB + throw new ArgumentException(SR.Arg_ArgumentException, $"typeName@{errorIndex}"); +#else + throw new ArgumentException("SR.Argument_InvalidTypeName"); +#endif + } } public override string ToString() => _inputString.ToString(); // TODO: add proper debugger display stuff + // 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 (!Dive(ref recursiveDepth)) + if (!TryDive(ref recursiveDepth)) { return null; } @@ -57,7 +93,7 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse List? nestedNameLengths = null; if (!TryGetTypeNameLengthWithNestedNameLengths(_inputString, ref nestedNameLengths, out int typeNameLength)) { - return ThrowInvalidTypeNameOrReturnNull(_throwOnError); + return null; } ReadOnlySpan typeName = _inputString.Slice(0, typeNameLength); @@ -69,13 +105,12 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse List? genericArgs = null; // TODO: use some stack-based list in CoreLib - // Are there any captured generic args? We'll look for "[[". + // 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 (TryStripFirstCharAndTrailingSpaces(ref _inputString, '[') - && TryStripFirstCharAndTrailingSpaces(ref _inputString, '[')) + if (IsBeginningOfGenericAgs(ref _inputString, out bool doubleBrackets)) { int startingRecursionCheck = recursiveDepth; int maxObservedRecursionCheck = recursiveDepth; @@ -83,8 +118,10 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse ParseAnotherGenericArg: recursiveDepth = startingRecursionCheck; - TypeName? genericArg = ParseNextTypeName(allowFullyQualifiedName: true, ref recursiveDepth); // generic args always allow AQNs - if (genericArg is null) // parsing failed, but not thrown due to _throwOnError being true + // Namespace.Type`1[[GenericArgument1, AssemblyName1],[GenericArgument2, AssemblyName2]] - double square bracket syntax allows for fully qualified type names + // Namespace.Type`1[GenericArgument1,GenericArgument2] - single square bracket syntax is legal only for non-fully qualified type names + TypeName? genericArg = ParseNextTypeName(allowFullyQualifiedName: doubleBrackets, ref recursiveDepth); + if (genericArg is null) // parsing failed { return null; } @@ -94,20 +131,21 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse maxObservedRecursionCheck = recursiveDepth; } - // There had better be a ']' after the type name. - if (!TryStripFirstCharAndTrailingSpaces(ref _inputString, ']')) + // For [[, there had better be a ']' after the type name. + if (doubleBrackets && !TryStripFirstCharAndTrailingSpaces(ref _inputString, ']')) { - return ThrowInvalidTypeNameOrReturnNull(_throwOnError); + return null; } (genericArgs ??= new()).Add(genericArg); - // Is there a ',[' indicating another generic type arg? if (TryStripFirstCharAndTrailingSpaces(ref _inputString, ',')) { - if (!TryStripFirstCharAndTrailingSpaces(ref _inputString, '[')) + // For [[, is there a ',[' indicating another generic type arg? + // For [, it's just a ',' + if (doubleBrackets && !TryStripFirstCharAndTrailingSpaces(ref _inputString, '[')) { - return ThrowInvalidTypeNameOrReturnNull(_throwOnError); + return null; } goto ParseAnotherGenericArg; @@ -117,7 +155,7 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse // the generic type arg list. if (!TryStripFirstCharAndTrailingSpaces(ref _inputString, ']')) { - return ThrowInvalidTypeNameOrReturnNull(_throwOnError); + return null; } // And now that we're at the end, restore the max observed recursion count. @@ -138,14 +176,14 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse // iterate over the decorators to ensure there are no illegal combinations while (TryParseNextDecorator(ref _inputString, out int parsedDecorator)) { - if (!Dive(ref recursiveDepth)) + if (!TryDive(ref recursiveDepth)) { return null; } if (previousDecorator == TypeName.ByRef) // it's illegal for managed reference to be followed by any other decorator { - return ThrowInvalidTypeNameOrReturnNull(_throwOnError); + return null; } previousDecorator = parsedDecorator; } @@ -161,7 +199,7 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse } throw new IO.FileLoadException(SR.InvalidAssemblyName, _inputString.ToString()); #else - return ThrowInvalidTypeNameOrReturnNull(_throwOnError); + return null; #endif } @@ -285,6 +323,9 @@ private bool TryParseAssemblyName(ref AssemblyName? assemblyName) return true; } + private static ReadOnlySpan TrimStart(ReadOnlySpan input) + => input.TrimStart(' '); // TODO: the CLR parser should trim all whitespaces, but there seems to be no test coverage + private static TypeName? GetContainingType(ref ReadOnlySpan typeName, List? nestedNameLengths, AssemblyName? assemblyName) { if (nestedNameLengths is null) @@ -303,34 +344,49 @@ private bool TryParseAssemblyName(ref AssemblyName? assemblyName) return containingType; } - private bool Dive(ref int depth) + private bool TryDive(ref int depth) { if (depth >= _parseOptions.MaxRecursiveDepth) { - if (_throwOnError) - { - return false; - } - else - { - Throw(); - } + return false; } depth++; return true; - - [DoesNotReturn] - static void Throw() => throw new InvalidOperationException("SR.RecursionCheck_MaxDepthExceeded"); } - private static TypeName? ThrowInvalidTypeNameOrReturnNull(bool throwOnError) - => throwOnError ? throw new ArgumentException("SR.Argument_InvalidTypeName") : null; + // Are there any captured generic args? We'll look for "[[" and "[" that is not followed by "]", "*" and ",". + private static bool IsBeginningOfGenericAgs(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 = TrimStart(span.Slice(1)); + if (!trimmed.IsEmpty) + { + if (trimmed[0] == '[') + { + doubleBrackets = true; + span = TrimStart(trimmed.Slice(1)); + return true; + } + if (!(trimmed[0] is ',' or '*' or ']')) // [] or [*] or [,] or [,,,, ...] + { + span = trimmed; + return true; + } + } + } + + return false; + } private static bool TryStripFirstCharAndTrailingSpaces(ref ReadOnlySpan span, char value) { if (!span.IsEmpty && span[0] == value) { - span = span.Slice(1).TrimStart(' '); + span = TrimStart(span.Slice(1)); return true; } return false; 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 0000000000000..9eb84d21d9fcd --- /dev/null +++ b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs @@ -0,0 +1,133 @@ +// 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.CodeAnalysis; +using System.Text; + +#nullable enable + +namespace System.Reflection +{ + internal partial struct TypeNameParser + { +#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 Type? Resolve(Metadata.TypeName typeName) + { + if (typeName.IsNestedType) + { + Metadata.TypeName? current = typeName; + int nestingDepth = 0; + while (current is not null && current.IsNestedType) + { + nestingDepth++; + current = current.ContainingType; + } + + string[] nestedTypeNames = new string[nestingDepth]; + current = typeName; + while (current is not null && current.IsNestedType) + { + nestedTypeNames[--nestingDepth] = current.Name; + current = current.ContainingType; + } + string nonNestedParentName = current!.Name; + + Type? type = GetType(nonNestedParentName, nestedTypeNames, typeName.AssemblyName); + return Make(type, typeName); + } + else if (typeName.UnderlyingType is null) + { + Type? type = GetType(typeName.Name, nestedTypeNames: ReadOnlySpan.Empty, typeName.AssemblyName); + + return Make(type, typeName); + } + + return Make(Resolve(typeName.UnderlyingType), typeName); + } + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2055:UnrecognizedReflectionPattern", + Justification = "Used to implement resolving types from strings.")] + private Type? Make(Type? type, Metadata.TypeName typeName) + { + if (type is null || typeName.IsElementalType) + { + return type; + } + else if (typeName.IsConstructedGenericType) + { + Metadata.TypeName[] genericArgs = typeName.GetGenericArguments(); + Type[] genericTypes = new Type[genericArgs.Length]; + for (int i = 0; i < genericArgs.Length; i++) + { + Type? genericArg = Resolve(genericArgs[i]); + if (genericArg is null) + { + return null; + } + genericTypes[i] = genericArg; + } + + return type.MakeGenericType(genericTypes); + } + else if (typeName.IsManagedPointerType) + { + return type.MakeByRefType(); + } + else if (typeName.IsUnmanagedPointerType) + { + return type.MakePointerType(); + } + else if (typeName.IsSzArrayType) + { + return type.MakeArrayType(); + } + else + { + return type.MakeArrayType(rank: typeName.GetArrayRank()); + } + } + } +} 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 2e190d63d92d0..68fd8a957d85a 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 @@ -1459,8 +1459,8 @@ Common\System\IO\PathInternal.CaseSensitivity.cs - - Common\System\Reflection\TypeNameParser.cs + + Common\System\Reflection\TypeNameParser.Helpers Common\System\Reflection\AssemblyNameParser.cs 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 2da3bd6cdc2ea..2a1b8b63b931a 100644 --- a/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs +++ b/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs @@ -2429,7 +2429,7 @@ public sealed class TypeName } public ref partial struct TypeNameParser { - public static System.Reflection.Metadata.TypeName? Parse(System.ReadOnlySpan name, bool allowFullyQualifiedName = true, bool throwOnError = true, System.Reflection.Metadata.TypeNameParserOptions? options = null) { throw null; } + public static System.Reflection.Metadata.TypeName? Parse(System.ReadOnlySpan typeName, bool allowFullyQualifiedName = true, bool throwOnError = true, System.Reflection.Metadata.TypeNameParserOptions? options = null) { throw null; } } public partial class TypeNameParserOptions { diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs index 1bfadab14eca6..8c8e3fe603c07 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs @@ -210,6 +210,7 @@ public void DecoratorsAreSupported(string input, string typeNameWithoutDecorator [Theory] [InlineData(typeof(int))] + [InlineData(typeof(int?))] [InlineData(typeof(int[]))] [InlineData(typeof(int[,]))] [InlineData(typeof(int[,,,]))] From bd78637c438215cda700e2e0faef5b0cd7dc329d Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 2 Feb 2024 12:17:42 +0100 Subject: [PATCH 11/48] integrate with System.Private.CoreLib for Mono and clr tools, improve perf --- .../CustomAttributeTypeNameParser.cs | 21 +- .../ILVerification/ILVerification.projitems | 17 +- .../Dataflow/TypeNameParser.Dataflow.cs | 42 +- .../ILCompiler.Compiler.csproj | 4 +- .../ILCompiler.TypeSystem.csproj | 17 +- .../System/Reflection/AssemblyNameParser.cs | 2 + .../System/Reflection/Metadata/TypeName.cs | 7 +- .../Reflection/Metadata/TypeNameParser.cs | 10 +- .../Reflection/TypeNameParser.Helpers.cs | 24 + .../src/System/Reflection/TypeNameParser.cs | 701 ------------------ .../src/System/Reflection/AssemblyName.cs | 6 +- .../System/Reflection/TypeNameParser.Mono.cs | 23 +- 12 files changed, 111 insertions(+), 763 deletions(-) delete mode 100644 src/libraries/Common/src/System/Reflection/TypeNameParser.cs diff --git a/src/coreclr/tools/Common/TypeSystem/Common/Utilities/CustomAttributeTypeNameParser.cs b/src/coreclr/tools/Common/TypeSystem/Common/Utilities/CustomAttributeTypeNameParser.cs index a52a68f0028a4..164f5b6b94e96 100644 --- a/src/coreclr/tools/Common/TypeSystem/Common/Utilities/CustomAttributeTypeNameParser.cs +++ b/src/coreclr/tools/Common/TypeSystem/Common/Utilities/CustomAttributeTypeNameParser.cs @@ -37,12 +37,18 @@ internal partial struct TypeNameParser public static TypeDesc ResolveType(ModuleDesc module, string name, bool throwIfNotFound, Func canonResolver) { - return new TypeNameParser(name.AsSpan()) + var parsed = Metadata.TypeNameParser.Parse(name.AsSpan(), throwOnError: false); + if (parsed is null) + { + ThrowHelper.ThrowTypeLoadException(name, module); + } + + return new TypeNameParser() { _module = module, _throwIfNotFound = throwIfNotFound, _canonResolver = canonResolver - }.Parse()?.Value; + }.Resolve(parsed)?.Value; } private sealed class Type @@ -64,12 +70,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, AssemblyName assemblyNameIfAny) { ModuleDesc module = (assemblyNameIfAny == null) ? _module : - _module.Context.ResolveAssembly(new AssemblyName(assemblyNameIfAny), throwIfNotFound: _throwIfNotFound); + _module.Context.ResolveAssembly(assemblyNameIfAny, throwIfNotFound: _throwIfNotFound); if (_canonResolver != null && nestedTypeNames.IsEmpty) { @@ -115,10 +119,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 58c2204c79510..e072d2e593cff 100644 --- a/src/coreclr/tools/ILVerification/ILVerification.projitems +++ b/src/coreclr/tools/ILVerification/ILVerification.projitems @@ -66,9 +66,24 @@ Utilities\CustomAttributeTypeNameParser.cs - + + Utilities\HexConverter.cs + + + Utilities\AssemblyNameParser.cs + + + Utilities\TypeName.cs + + + Utilities\TypeNameParserOptions.cs + + Utilities\TypeNameParser.cs + + Utilities\CustomAttributeTypeNameParser.Helpers + Utilities\ValueStringBuilder.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 90b63f39c9f02..bc7df41cebd15 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,6 @@ // 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 Internal.TypeSystem; namespace System.Reflection @@ -20,14 +16,21 @@ internal partial struct TypeNameParser public static TypeDesc ResolveType(string name, ModuleDesc callingModule, TypeSystemContext context, List referencedModules, out bool typeWasNotFoundInAssemblyNorBaseLibrary) { - var parser = new TypeNameParser(name) + var parsed = Metadata.TypeNameParser.Parse(name, throwOnError: false); + if (parsed is null) // TODO adsitnik: verify that this is desired + { + typeWasNotFoundInAssemblyNorBaseLibrary = true; + 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, AssemblyName assemblyNameIfAny) { ModuleDesc module; if (assemblyNameIfAny != null) { - module = (TryParseAssemblyName(assemblyNameIfAny) is AssemblyName an) ? - _context.ResolveAssembly(an, throwIfNotFound: false) : null; + module = _context.ResolveAssembly(assemblyNameIfAny, throwIfNotFound: false); } else { @@ -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 91276048b08e0..f122f87f2104f 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj @@ -399,8 +399,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 c528fb3c67340..d93115e3a87aa 100644 --- a/src/coreclr/tools/aot/ILCompiler.TypeSystem/ILCompiler.TypeSystem.csproj +++ b/src/coreclr/tools/aot/ILCompiler.TypeSystem/ILCompiler.TypeSystem.csproj @@ -184,9 +184,24 @@ Utilities\CustomAttributeTypeNameParser.cs - + + Utilities\HexConverter.cs + + + Utilities\AssemblyNameParser.cs + + + Utilities\TypeName.cs + + + Utilities\TypeNameParserOptions.cs + + Utilities\TypeNameParser.cs + + Utilities\CustomAttributeTypeNameParser.Helpers + Utilities\ValueStringBuilder.cs diff --git a/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs b/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs index 948dbe175e385..0f6bd64bb7b0d 100644 --- a/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs @@ -8,6 +8,8 @@ using System.Runtime.CompilerServices; using System.Text; +#nullable enable + namespace System.Reflection { // diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs index 9a4f8bfd3a5cd..da5cca3891925 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs @@ -3,6 +3,8 @@ using System.Diagnostics.CodeAnalysis; +#nullable enable + namespace System.Reflection.Metadata { #if SYSTEM_PRIVATE_CORELIB @@ -20,6 +22,7 @@ sealed class TypeName // Negative value is modifier encoded using constants above. private readonly int _rankOrModifier; private readonly TypeName[]? _genericArguments; + private string? _assemblyQualifiedName; internal TypeName(string name, AssemblyName? assemblyName, int rankOrModifier, TypeName? underlyingType = default, @@ -32,7 +35,6 @@ internal TypeName(string name, AssemblyName? assemblyName, int rankOrModifier, UnderlyingType = underlyingType; ContainingType = containingType; _genericArguments = genericTypeArguments; - AssemblyQualifiedName = assemblyName is null ? name : $"{name}, {assemblyName.FullName}"; } /// @@ -41,7 +43,8 @@ internal TypeName(string name, AssemblyName? assemblyName, int rankOrModifier, /// /// If is null, simply returns . /// - public string AssemblyQualifiedName { get; } + public string AssemblyQualifiedName + => _assemblyQualifiedName ??= AssemblyName is null ? Name : $"{Name}, {AssemblyName.FullName}"; /// /// The assembly which contains this type, or null if this was not diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs index 9c40a57129136..407888261a5bc 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs @@ -9,6 +9,8 @@ using System.Diagnostics; using System.Text; +#nullable enable + namespace System.Reflection.Metadata { #if SYSTEM_PRIVATE_CORELIB @@ -22,6 +24,7 @@ ref struct TypeNameParser #if NET8_0_OR_GREATER private static readonly SearchValues _endOfTypeNameDelimitersSearchValues = SearchValues.Create(EndOfTypeNameDelimiters); #endif + private static readonly TypeNameParserOptions _defaults = new(); private readonly bool _throwOnError; private readonly TypeNameParserOptions _parseOptions; private ReadOnlySpan _inputString; @@ -30,7 +33,7 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse { _inputString = name; _throwOnError = throwOnError; - _parseOptions = options ?? new(); + _parseOptions = options ?? _defaults; } public static TypeName? Parse(ReadOnlySpan typeName, bool allowFullyQualifiedName = true, bool throwOnError = true, TypeNameParserOptions? options = default) @@ -314,8 +317,13 @@ private bool TryParseAssemblyName(ref AssemblyName? assemblyName) return false; } +#if SYSTEM_PRIVATE_CORELIB + assemblyName = new(); + assemblyName.Init(parts); +#else // TODO: fix the perf and avoid doing it twice (missing public ctors for System.Reflection.Metadata) assemblyName = new(candidate.ToString()); +#endif _inputString = _inputString.Slice(assemblyNameLength); return true; } diff --git a/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs index 9eb84d21d9fcd..1bb98ab76f88e 100644 --- a/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs +++ b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs @@ -54,6 +54,28 @@ private static string EscapeTypeName(string typeName, ReadOnlySpan neste 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); + } + private Type? Resolve(Metadata.TypeName typeName) { if (typeName.IsNestedType) @@ -88,8 +110,10 @@ private static string EscapeTypeName(string typeName, ReadOnlySpan neste return Make(Resolve(typeName.UnderlyingType), typeName); } +#if !NETSTANDARD2_0 // needed for ILVerification project [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2055:UnrecognizedReflectionPattern", Justification = "Used to implement resolving types from strings.")] +#endif private Type? Make(Type? type, Metadata.TypeName typeName) { if (type is null || typeName.IsElementalType) 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 182f0fa229f3e..0000000000000 --- 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 - - internal 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(); - } - - internal 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/System.Private.CoreLib/src/System/Reflection/AssemblyName.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/AssemblyName.cs index 7440e1fae8dac..ec96aa8bc07b5 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/AssemblyName.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/AssemblyName.cs @@ -33,7 +33,11 @@ public AssemblyName(string assemblyName) if (assemblyName[0] == '\0') throw new ArgumentException(SR.Format_StringZeroLength); - AssemblyNameParser.AssemblyNameParts parts = AssemblyNameParser.Parse(assemblyName); + Init(AssemblyNameParser.Parse(assemblyName)); + } + + internal void Init(AssemblyNameParser.AssemblyNameParts parts) + { _name = parts._name; _version = parts._version; _flags = parts._flags; 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 3622b4e27626a..bd88f12846774 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) + var 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,7 +95,7 @@ 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 typeName, ReadOnlySpan nestedTypeNames, AssemblyName? assemblyNameIfAny) { Assembly? assembly = (assemblyNameIfAny is not null) ? ResolveAssembly(assemblyNameIfAny) : null; From 8fda94a52ea2433bc59f46bc8b5cb1c2bafed887 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 2 Feb 2024 15:18:09 +0100 Subject: [PATCH 12/48] build fix 1/n --- .../System/Reflection/Metadata/TypeName.cs | 74 ------------------ .../Reflection/Metadata/TypeNameParser.cs | 3 +- .../ref/System.Reflection.Metadata.cs | 12 +-- .../tests/Metadata/TypeNameParserTests.cs | 75 ++++++++++++++++++- 4 files changed, 81 insertions(+), 83 deletions(-) diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs index da5cca3891925..70fffbca2b02e 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.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.Diagnostics.CodeAnalysis; - #nullable enable namespace System.Reflection.Metadata @@ -155,77 +153,5 @@ public TypeName[] GetGenericArguments() => _genericArguments is not null ? (TypeName[])_genericArguments.Clone() // we return a copy on purpose, to not allow for mutations. TODO: consider returning a ROS : Array.Empty(); // TODO: should we throw (Levi's parser throws InvalidOperationException in such case), Type.GetGenericArguments just returns an empty array - -#if !SYSTEM_PRIVATE_CORELIB -#if NET8_0_OR_GREATER - [RequiresUnreferencedCode("The type might be removed")] - [RequiresDynamicCode("Required by MakeArrayType")] -#else -#pragma warning disable IL2055, IL2057, IL2075, IL2096 -#endif - public Type? GetType(bool throwOnError = true, bool ignoreCase = false) - { - if (ContainingType is not null) // nested type - { - BindingFlags flagsCopiedFromClr = BindingFlags.NonPublic | BindingFlags.Public; - if (ignoreCase) - { - flagsCopiedFromClr |= BindingFlags.IgnoreCase; - } - return Make(ContainingType.GetType(throwOnError, ignoreCase)?.GetNestedType(Name, flagsCopiedFromClr)); - } - else if (UnderlyingType is null) - { - Type? type = AssemblyName is null - ? Type.GetType(Name, throwOnError, ignoreCase) - : Assembly.Load(AssemblyName).GetType(Name, throwOnError, ignoreCase); - - return Make(type); - } - - return Make(UnderlyingType.GetType(throwOnError, ignoreCase)); - - Type? Make(Type? type) - { - if (type is null || IsElementalType) - { - return type; - } - else if (IsConstructedGenericType) - { - TypeName[] genericArgs = GetGenericArguments(); - Type[] genericTypes = new Type[genericArgs.Length]; - for (int i = 0; i < genericArgs.Length; i++) - { - Type? genericArg = genericArgs[i].GetType(throwOnError, ignoreCase); - if (genericArg is null) - { - return null; - } - genericTypes[i] = genericArg; - } - - return type.MakeGenericType(genericTypes); - } - else if (IsManagedPointerType) - { - return type.MakeByRefType(); - } - else if (IsUnmanagedPointerType) - { - return type.MakePointerType(); - } - else if (IsSzArrayType) - { - return type.MakeArrayType(); - } - else - { - return type.MakeArrayType(rank: GetArrayRank()); - } - } - } -#pragma warning restore IL2055, IL2057, IL2075, IL2096 -#endif } } diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs index 407888261a5bc..973e797dfffc4 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs @@ -13,6 +13,7 @@ namespace System.Reflection.Metadata { + // TODO: add proper debugger display stuff #if SYSTEM_PRIVATE_CORELIB internal #else @@ -83,8 +84,6 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse } } - public override string ToString() => _inputString.ToString(); // TODO: add proper debugger display stuff - // 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) { 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 2a1b8b63b931a..3c3ee929e82c1 100644 --- a/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs +++ b/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs @@ -2408,11 +2408,12 @@ public readonly partial struct TypeLayout public int PackingSize { get { throw null; } } public int Size { get { throw null; } } } - public sealed class TypeName + public sealed partial class TypeName { - public string AssemblyQualifiedName { get { throw null; } } + internal TypeName() { } public System.Reflection.AssemblyName? AssemblyName { get { throw null; } } - public TypeName? ContainingType { get { throw null; } } + public string AssemblyQualifiedName { get { throw null; } } + public System.Reflection.Metadata.TypeName? ContainingType { get { throw null; } } public bool IsArray { get { throw null; } } public bool IsConstructedGenericType { get { throw null; } } public bool IsElementalType { get { throw null; } } @@ -2422,13 +2423,14 @@ public sealed class TypeName public bool IsUnmanagedPointerType { get { throw null; } } public bool IsVariableBoundArrayType { get { throw null; } } public string Name { get { throw null; } } - public TypeName? UnderlyingType { get { throw null; } } + public System.Reflection.Metadata.TypeName? UnderlyingType { get { throw null; } } public int GetArrayRank() { throw null; } public System.Reflection.Metadata.TypeName[] GetGenericArguments() { throw null; } - public System.Type? GetType(bool throwOnError = true, bool ignoreCase = false) { throw null; } } public ref partial struct TypeNameParser { + private object _dummy; + private int _dummyPrimitive; public static System.Reflection.Metadata.TypeName? Parse(System.ReadOnlySpan typeName, bool allowFullyQualifiedName = true, bool throwOnError = true, System.Reflection.Metadata.TypeNameParserOptions? options = null) { throw null; } } public partial class TypeNameParserOptions diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs index 8c8e3fe603c07..6bfd8021eeafe 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; using Xunit; @@ -222,7 +223,7 @@ public void DecoratorsAreSupported(string input, string typeNameWithoutDecorator [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 GetType_Roundtrip(Type type) + public void CanImplementGetTypeUsingPublicAPIs_Roundtrip(Type type) { Test(type); Test(type.MakePointerType()); @@ -249,12 +250,82 @@ static void Test(Type type) static void Verify(Type type, TypeName typeName, bool ignoreCase) { - Type afterRoundtrip = typeName.GetType(throwOnError: true, ignoreCase: 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.ContainingType is not null) // nested type + { + BindingFlags flagsCopiedFromClr = BindingFlags.NonPublic | BindingFlags.Public; + if (ignoreCase) + { + flagsCopiedFromClr |= BindingFlags.IgnoreCase; + } + return Make(GetType(typeName.ContainingType, throwOnError, ignoreCase)?.GetNestedType(typeName.Name, flagsCopiedFromClr)); + } + else if (typeName.UnderlyingType is null) + { + Type? type = typeName.AssemblyName is null + ? Type.GetType(typeName.Name, throwOnError, ignoreCase) + : Assembly.Load(typeName.AssemblyName).GetType(typeName.Name, throwOnError, ignoreCase); + + return Make(type); + } + + return Make(GetType(typeName.UnderlyingType, throwOnError, ignoreCase)); + + Type? Make(Type? type) + { + if (type is null || typeName.IsElementalType) + { + return type; + } + else if (typeName.IsConstructedGenericType) + { + TypeName[] 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.IsManagedPointerType) + { + return type.MakeByRefType(); + } + else if (typeName.IsUnmanagedPointerType) + { + return type.MakePointerType(); + } + else if (typeName.IsSzArrayType) + { + return type.MakeArrayType(); + } + else + { + return type.MakeArrayType(rank: typeName.GetArrayRank()); + } + } + } +#pragma warning restore IL2055, IL2057, IL2075, IL2096 } public class NestedNonGeneric_0 From 745e7bb66ba68121b81a4894123024869f168f97 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 7 Feb 2024 10:15:31 +0100 Subject: [PATCH 13/48] make TypeNameParser internal, extend TypeName with Parse and TryParse methods, move "allowFullyQualifiedName" to Options bag --- .../Reflection/TypeNameParser.CoreCLR.cs | 11 ++------ .../Reflection/TypeNameParser.NativeAot.cs | 6 ++-- .../CustomAttributeTypeNameParser.cs | 6 ++-- .../ILVerification/ILVerification.csproj | 2 +- .../Dataflow/TypeNameParser.Dataflow.cs | 6 ++-- .../System/Reflection/Metadata/TypeName.cs | 15 ++++++++++ .../Reflection/Metadata/TypeNameParser.cs | 11 ++------ .../Metadata/TypeNameParserOptions.cs | 2 ++ .../ref/System.Reflection.Metadata.cs | 9 ++---- .../tests/Metadata/TypeNameParserTests.cs | 28 +++++++++---------- 10 files changed, 48 insertions(+), 48 deletions(-) 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 432e109a4391a..320ee674b507b 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 @@ -79,9 +79,7 @@ internal partial struct TypeNameParser bool ignoreCase, Assembly topLevelAssembly) { - var parsed = Metadata.TypeNameParser.Parse(typeName, - allowFullyQualifiedName: true, // let it get parsed, but throw when topLevelAssembly was specified - throwOnError: throwOnError); + var parsed = Metadata.TypeNameParser.Parse(typeName, throwOnError); if (parsed is null) { @@ -112,7 +110,7 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName, RuntimeAssembly requestingAssembly = scope.GetRuntimeAssembly(); - var parsed = Metadata.TypeNameParser.Parse(typeName, allowFullyQualifiedName: true, throwOnError: true)!; + var parsed = Metadata.TypeNameParser.Parse(typeName, throwOnError: true)!; RuntimeType? type = (RuntimeType?)new TypeNameParser() { _throwOnError = true, @@ -142,10 +140,7 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName, return null; } - var parsed = Metadata.TypeNameParser.Parse(typeName, - allowFullyQualifiedName: true, - throwOnError: throwOnError); - + var parsed = Metadata.TypeNameParser.Parse(typeName, throwOnError); if (parsed is null) { return null; 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 b18ada21d85bf..0ce5358f7128d 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 @@ -51,7 +51,7 @@ internal partial struct TypeNameParser return null; } - var parsed = Metadata.TypeNameParser.Parse(typeName, throwOnError: throwOnError); + var parsed = Metadata.TypeNameParser.Parse(typeName, throwOnError); if (parsed is null) { return null; @@ -74,9 +74,7 @@ internal partial struct TypeNameParser bool ignoreCase, Assembly topLevelAssembly) { - var parsed = Metadata.TypeNameParser.Parse(typeName, - allowFullyQualifiedName: true, // let it get parsed, but throw when topLevelAssembly was specified - throwOnError: throwOnError); + var parsed = Metadata.TypeNameParser.Parse(typeName, throwOnError); if (parsed is null) { diff --git a/src/coreclr/tools/Common/TypeSystem/Common/Utilities/CustomAttributeTypeNameParser.cs b/src/coreclr/tools/Common/TypeSystem/Common/Utilities/CustomAttributeTypeNameParser.cs index 164f5b6b94e96..093901ce502ec 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,8 +36,7 @@ internal partial struct TypeNameParser public static TypeDesc ResolveType(ModuleDesc module, string name, bool throwIfNotFound, Func canonResolver) { - var parsed = Metadata.TypeNameParser.Parse(name.AsSpan(), throwOnError: false); - if (parsed is null) + if (!TypeName.TryParse(name.AsSpan(), out TypeName parsed)) { ThrowHelper.ThrowTypeLoadException(name, module); } diff --git a/src/coreclr/tools/ILVerification/ILVerification.csproj b/src/coreclr/tools/ILVerification/ILVerification.csproj index 4ac252c3e0c44..b8a3b75bb0d5c 100644 --- a/src/coreclr/tools/ILVerification/ILVerification.csproj +++ b/src/coreclr/tools/ILVerification/ILVerification.csproj @@ -10,7 +10,7 @@ true Open false - NETSTANDARD2_0 + NETSTANDARD2_0;INTERNAL_NULLABLE_ANNOTATIONS 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 bc7df41cebd15..df435c0e3204d 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,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Reflection.Metadata; using Internal.TypeSystem; namespace System.Reflection @@ -16,11 +17,10 @@ internal partial struct TypeNameParser public static TypeDesc ResolveType(string name, ModuleDesc callingModule, TypeSystemContext context, List referencedModules, out bool typeWasNotFoundInAssemblyNorBaseLibrary) { - var parsed = Metadata.TypeNameParser.Parse(name, throwOnError: false); - if (parsed is null) // TODO adsitnik: verify that this is desired + if (!TypeName.TryParse(name, out TypeName parsed)) { typeWasNotFoundInAssemblyNorBaseLibrary = true; - return null; + return null; // TODO adsitnik: verify that this is desired } var parser = new TypeNameParser() diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs index 70fffbca2b02e..2b9ae9fe79a3c 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs @@ -3,6 +3,8 @@ #nullable enable +using System.Diagnostics.CodeAnalysis; + namespace System.Reflection.Metadata { #if SYSTEM_PRIVATE_CORELIB @@ -132,6 +134,19 @@ public string AssemblyQualifiedName /// public TypeName? UnderlyingType { get; } + public static TypeName Parse(ReadOnlySpan typeName, TypeNameParserOptions? options = default) + => TypeNameParser.Parse(typeName, throwOnError: true, options)!; + + public static bool TryParse(ReadOnlySpan typeName, +#if !INTERNAL_NULLABLE_ANNOTATIONS // remove along with the define from ILVerification.csproj when SystemReflectionMetadataVersion points to new version with the new types + [NotNullWhen(true)] +#endif + out TypeName? result, TypeNameParserOptions? options = default) + { + result = TypeNameParser.Parse(typeName, throwOnError: false, options); + return result is not null; + } + public int GetArrayRank() => _rankOrModifier switch { diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs index 973e797dfffc4..6b1086c92f4ad 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs @@ -14,12 +14,7 @@ namespace System.Reflection.Metadata { // TODO: add proper debugger display stuff -#if SYSTEM_PRIVATE_CORELIB - internal -#else - public -#endif - ref struct TypeNameParser + internal ref struct TypeNameParser { private const string EndOfTypeNameDelimiters = "[]&*,+"; #if NET8_0_OR_GREATER @@ -37,7 +32,7 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse _parseOptions = options ?? _defaults; } - public static TypeName? Parse(ReadOnlySpan typeName, bool allowFullyQualifiedName = true, bool throwOnError = true, TypeNameParserOptions? options = default) + internal static TypeName? Parse(ReadOnlySpan typeName, bool throwOnError, TypeNameParserOptions? options = default) { ReadOnlySpan trimmedName = TrimStart(typeName); // whitespaces at beginning are always OK if (trimmedName.IsEmpty) @@ -48,7 +43,7 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse int recursiveDepth = 0; TypeNameParser parser = new(trimmedName, throwOnError, options); - TypeName? parsedName = parser.ParseNextTypeName(allowFullyQualifiedName, ref recursiveDepth); + TypeName? parsedName = parser.ParseNextTypeName(parser._parseOptions.AllowFullyQualifiedName, ref recursiveDepth); if (parsedName is not null && parser._inputString.IsEmpty) // unconsumed input == error { diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs index b3d1f9be617dc..4f4bf728da0b5 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs @@ -14,6 +14,8 @@ class TypeNameParserOptions { private int _maxRecursiveDepth = int.MaxValue; + public bool AllowFullyQualifiedName { get; set; } = true; + public int MaxRecursiveDepth { get => _maxRecursiveDepth; 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 3c3ee929e82c1..569f29e68053b 100644 --- a/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs +++ b/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs @@ -2424,18 +2424,15 @@ internal TypeName() { } public bool IsVariableBoundArrayType { get { throw null; } } public string Name { get { throw null; } } public System.Reflection.Metadata.TypeName? UnderlyingType { get { throw null; } } + public static System.Reflection.Metadata.TypeName Parse(System.ReadOnlySpan typeName, System.Reflection.Metadata.TypeNameParserOptions? 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.TypeNameParserOptions? options = null) { throw null; } public int GetArrayRank() { throw null; } public System.Reflection.Metadata.TypeName[] GetGenericArguments() { throw null; } } - public ref partial struct TypeNameParser - { - private object _dummy; - private int _dummyPrimitive; - public static System.Reflection.Metadata.TypeName? Parse(System.ReadOnlySpan typeName, bool allowFullyQualifiedName = true, bool throwOnError = true, System.Reflection.Metadata.TypeNameParserOptions? options = null) { throw null; } - } public partial class TypeNameParserOptions { public TypeNameParserOptions() { } + public bool AllowFullyQualifiedName { get { throw null; } set { } } public int MaxRecursiveDepth { get { throw null; } set { } } public virtual bool ValidateIdentifier(System.ReadOnlySpan candidate, bool throwOnError) { throw null; } } diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs index 6bfd8021eeafe..c8a649c6d9445 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs @@ -14,7 +14,7 @@ public class TypeNameParserTests [Theory] [InlineData(" System.Int32", "System.Int32")] public void SpacesAtTheBeginningAreOK(string input, string expectedName) - => Assert.Equal(expectedName, TypeNameParser.Parse(input.AsSpan()).Name); + => Assert.Equal(expectedName, TypeName.Parse(input.AsSpan()).Name); [Theory] [InlineData("")] @@ -22,9 +22,9 @@ public void SpacesAtTheBeginningAreOK(string input, string expectedName) [InlineData(" ")] public void EmptyStringsAreNotAllowed(string input) { - Assert.Throws(() => TypeNameParser.Parse(input.AsSpan(), throwOnError: true)); + Assert.Throws(() => TypeName.Parse(input.AsSpan())); - Assert.Null(TypeNameParser.Parse(input.AsSpan(), throwOnError: false)); + Assert.False(TypeName.TryParse(input.AsSpan(), out _)); } [Theory] @@ -35,23 +35,23 @@ public void EmptyStringsAreNotAllowed(string input) [InlineData("ExtraComma, , System.Runtime")] public void InvalidTypeNamesAreNotAllowed(string input) { - Assert.Throws(() => TypeNameParser.Parse(input.AsSpan(), throwOnError: true)); + Assert.Throws(() => TypeName.Parse(input.AsSpan())); - Assert.Null(TypeNameParser.Parse(input.AsSpan(), throwOnError: false)); + Assert.False(TypeName.TryParse(input.AsSpan(), out _)); } [Theory] [InlineData("Namespace.Kość", "Namespace.Kość")] public void UnicodeCharactersAreAllowedByDefault(string input, string expectedName) - => Assert.Equal(expectedName, TypeNameParser.Parse(input.AsSpan()).Name); + => Assert.Equal(expectedName, TypeName.Parse(input.AsSpan()).Name); [Theory] [InlineData("Namespace.Kość")] public void UsersCanCustomizeIdentifierValidation(string input) { - Assert.Throws(() => TypeNameParser.Parse(input.AsSpan(), true, throwOnError: true, new NonAsciiNotAllowed())); + Assert.Throws(() => TypeName.Parse(input.AsSpan(), new NonAsciiNotAllowed())); - Assert.Null(TypeNameParser.Parse(input.AsSpan(), true, throwOnError: false, new NonAsciiNotAllowed())); + Assert.False(TypeName.TryParse(input.AsSpan(), out _, new NonAsciiNotAllowed())); } public static IEnumerable TypeNamesWithAssemblyNames() @@ -71,7 +71,7 @@ public static IEnumerable TypeNamesWithAssemblyNames() [MemberData(nameof(TypeNamesWithAssemblyNames))] public void TypeNameCanContainAssemblyName(string input, string typeName, string assemblyName, Version assemblyVersion, string assemblyCulture, string assemblyPublicKeyToken) { - TypeName parsed = TypeNameParser.Parse(input.AsSpan(), allowFullyQualifiedName: true); + TypeName parsed = TypeName.Parse(input.AsSpan()); Assert.Equal(typeName, parsed.Name); Assert.NotNull(parsed.AssemblyName); @@ -140,7 +140,7 @@ public static IEnumerable GenericArgumentsAreSupported_Arguments() [MemberData(nameof(GenericArgumentsAreSupported_Arguments))] public void GenericArgumentsAreSupported(string input, string typeName, string[] typeNames, AssemblyName[]? assemblyNames) { - TypeName parsed = TypeNameParser.Parse(input.AsSpan(), allowFullyQualifiedName: true); + TypeName parsed = TypeName.Parse(input.AsSpan()); Assert.Equal(typeName, parsed.Name); Assert.True(parsed.IsConstructedGenericType); @@ -188,7 +188,7 @@ public static IEnumerable DecoratorsAreSupported_Arguments() [MemberData(nameof(DecoratorsAreSupported_Arguments))] public void DecoratorsAreSupported(string input, string typeNameWithoutDecorators, bool isArray, bool isSzArray, int arrayRank, bool isByRef, bool isPointer) { - TypeName parsed = TypeNameParser.Parse(input.AsSpan(), allowFullyQualifiedName: true); + TypeName parsed = TypeName.Parse(input.AsSpan()); Assert.Equal(input, parsed.Name); Assert.Equal(isArray, parsed.IsArray); @@ -239,13 +239,13 @@ public void CanImplementGetTypeUsingPublicAPIs_Roundtrip(Type type) static void Test(Type type) { - TypeName typeName = TypeNameParser.Parse(type.AssemblyQualifiedName.AsSpan(), allowFullyQualifiedName: true); + TypeName typeName = TypeName.Parse(type.AssemblyQualifiedName.AsSpan()); Verify(type, typeName, ignoreCase: false); - typeName = TypeNameParser.Parse(type.AssemblyQualifiedName.ToLower().AsSpan(), allowFullyQualifiedName: true); + typeName = TypeName.Parse(type.AssemblyQualifiedName.ToLower().AsSpan()); Verify(type, typeName, ignoreCase: true); - typeName = TypeNameParser.Parse(type.AssemblyQualifiedName.ToUpper().AsSpan(), allowFullyQualifiedName: true); + typeName = TypeName.Parse(type.AssemblyQualifiedName.ToUpper().AsSpan()); Verify(type, typeName, ignoreCase: true); static void Verify(Type type, TypeName typeName, bool ignoreCase) From ee43e71f721be3351219bcc26ad561b97da91ed1 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 7 Feb 2024 16:40:51 +0100 Subject: [PATCH 14/48] introduce TotalComplexity --- .../System/Reflection/Metadata/TypeName.cs | 78 ++++++++++++++++--- .../ref/System.Reflection.Metadata.cs | 1 + .../tests/Metadata/TypeNameParserTests.cs | 44 +++++++++++ 3 files changed, 114 insertions(+), 9 deletions(-) diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs index 2b9ae9fe79a3c..dcb0a78589eb9 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs @@ -35,6 +35,7 @@ internal TypeName(string name, AssemblyName? assemblyName, int rankOrModifier, UnderlyingType = underlyingType; ContainingType = containingType; _genericArguments = genericTypeArguments; + TotalComplexity = GetTotalComplexity(underlyingType, containingType, genericTypeArguments); } /// @@ -52,6 +53,15 @@ public string AssemblyQualifiedName /// public AssemblyName? AssemblyName { get; } // TODO: AssemblyName is mutable, are we fine with that? Does it not offer too much? + /// + /// If this type is a nested type (see ), gets + /// the containing type. If this type is not a nested type, returns null. + /// + /// + /// For example, given "Namespace.Containing+Nested", unwraps the outermost type and returns "Namespace.Containing". + /// + public TypeName? ContainingType { get; } + /// /// Returns true if this type represents any kind of array, regardless of the array's /// rank or its bounds. @@ -110,19 +120,39 @@ public string AssemblyQualifiedName public bool IsVariableBoundArrayType => _rankOrModifier > 1; /// - /// If this type is a nested type (see ), gets - /// the containing type. If this type is not a nested type, returns null. + /// 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". /// - /// - /// For example, given "Namespace.Containing+Nested", unwraps the outermost type and returns "Namespace.Containing". - /// - public TypeName? ContainingType { get; } + public string Name { get; } /// - /// The 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". + /// Represents the total amount of work that needs to be performed to fully inspect + /// this instance, including any generic arguments or underlying types. /// - public string Name { get; } + /// + /// 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 complexity 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 complexity 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 TotalComplexity { get; } /// /// If this type is not an elemental type (see ), gets @@ -168,5 +198,35 @@ public TypeName[] GetGenericArguments() => _genericArguments is not null ? (TypeName[])_genericArguments.Clone() // we return a copy on purpose, to not allow for mutations. TODO: consider returning a ROS : Array.Empty(); // TODO: should we throw (Levi's parser throws InvalidOperationException in such case), Type.GetGenericArguments just returns an empty array + + private static int GetTotalComplexity(TypeName? underlyingType, TypeName? containingType, TypeName[]? genericTypeArguments) + { + int result = 1; + + if (underlyingType is not null) + { + result = checked(result + underlyingType.TotalComplexity); + } + + if (containingType is not null) + { + result = checked(result + containingType.TotalComplexity); + } + + if (genericTypeArguments is not null) + { + // New total complexity will be the sum of the cumulative args' complexity + 2: + // - one for the generic type definition "MyGeneric`x" + // - one for the constructed type definition "MyGeneric`x[[...]]" + // - and the cumulative complexity of all the arguments + result = checked(result + 1); + foreach (TypeName genericArgument in genericTypeArguments) + { + result = checked(result + genericArgument.TotalComplexity); + } + } + + return result; + } } } 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 569f29e68053b..c6a8d2865e24f 100644 --- a/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs +++ b/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs @@ -2423,6 +2423,7 @@ internal TypeName() { } public bool IsUnmanagedPointerType { get { throw null; } } public bool IsVariableBoundArrayType { get { throw null; } } public string Name { get { throw null; } } + public int TotalComplexity { get; } public System.Reflection.Metadata.TypeName? UnderlyingType { get { throw null; } } public static System.Reflection.Metadata.TypeName Parse(System.ReadOnlySpan typeName, System.Reflection.Metadata.TypeNameParserOptions? 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.TypeNameParserOptions? options = null) { throw null; } diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs index c8a649c6d9445..980a882275d22 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs @@ -209,6 +209,50 @@ public void DecoratorsAreSupported(string input, string typeNameWithoutDecorator Assert.Null(underlyingType.UnderlyingType); } + 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(TypeNameParserTests), 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 + [MemberData(nameof(GetAdditionalConstructedTypeData))] + public void TotalComplexityReturnsExpectedValue(Type type, int expectedComplexity) + { + TypeName parsed = TypeName.Parse(type.AssemblyQualifiedName.AsSpan()); + + Assert.Equal(expectedComplexity, parsed.TotalComplexity); + } + [Theory] [InlineData(typeof(int))] [InlineData(typeof(int?))] From a3a7f266c216c71f04b31e04c0d75054e5620a3f Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 7 Feb 2024 16:50:18 +0100 Subject: [PATCH 15/48] introduce FullName, so we have Name, FullName and AssemblyQualifiedName and they are consistent with Sytem.Type APIs --- .../System/Reflection/Metadata/TypeName.cs | 32 ++++- .../Reflection/Metadata/TypeNameParser.cs | 116 ++++++++++++++---- .../Reflection/TypeNameParser.Helpers.cs | 4 +- .../tests/Metadata/TypeNameParserTests.cs | 98 +++++++++++---- 4 files changed, 189 insertions(+), 61 deletions(-) diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs index dcb0a78589eb9..a9b7d51d94d16 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs @@ -3,10 +3,12 @@ #nullable enable +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; namespace System.Reflection.Metadata { + [DebuggerDisplay("{AssemblyQualifiedName}")] #if SYSTEM_PRIVATE_CORELIB internal #else @@ -24,12 +26,15 @@ sealed class TypeName private readonly TypeName[]? _genericArguments; private string? _assemblyQualifiedName; - internal TypeName(string name, AssemblyName? assemblyName, int rankOrModifier, + internal TypeName(string name, string fullName, + AssemblyName? assemblyName, + int rankOrModifier = default, TypeName? underlyingType = default, TypeName? containingType = default, TypeName[]? genericTypeArguments = default) { Name = name; + FullName = fullName; AssemblyName = assemblyName; _rankOrModifier = rankOrModifier; UnderlyingType = underlyingType; @@ -42,10 +47,10 @@ internal TypeName(string name, AssemblyName? assemblyName, int rankOrModifier, /// The assembly-qualified name of the type; e.g., "System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089". /// /// - /// If is null, simply returns . + /// If is null, simply returns . /// public string AssemblyQualifiedName - => _assemblyQualifiedName ??= AssemblyName is null ? Name : $"{Name}, {AssemblyName.FullName}"; + => _assemblyQualifiedName ??= AssemblyName is null ? FullName : $"{FullName}, {AssemblyName.FullName}"; /// /// The assembly which contains this type, or null if this was not @@ -62,6 +67,22 @@ public string AssemblyQualifiedName /// public TypeName? ContainingType { get; } + /// + /// 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; } + /// /// Returns true if this type represents any kind of array, regardless of the array's /// rank or its bounds. @@ -88,7 +109,7 @@ public string AssemblyQualifiedName /// This is because determining whether a type truly is a generic type requires loading the type /// and performing a runtime check. /// - public bool IsElementalType => UnderlyingType is null && !IsConstructedGenericType; + public bool IsElementalType => UnderlyingType is null; /// /// Returns true if this is a managed pointer type (e.g., "ref int"). @@ -111,7 +132,7 @@ public string AssemblyQualifiedName /// Returns true if this type represents an unmanaged pointer (e.g., "int*" or "void*"). /// Unmanaged pointer types are often just called pointers () /// - public bool IsUnmanagedPointerType => _rankOrModifier == Pointer;// name inconsistent with Type.IsPointer + public bool IsUnmanagedPointerType => _rankOrModifier == Pointer; // name inconsistent with Type.IsPointer /// /// Returns true if this type represents a variable-bound array; that is, an array of rank greater @@ -219,7 +240,6 @@ private static int GetTotalComplexity(TypeName? underlyingType, TypeName? contai // - one for the generic type definition "MyGeneric`x" // - one for the constructed type definition "MyGeneric`x[[...]]" // - and the cumulative complexity of all the arguments - result = checked(result + 1); foreach (TypeName genericArgument in genericTypeArguments) { result = checked(result + genericArgument.TotalComplexity); diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs index 6b1086c92f4ad..22389e3f20a54 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs @@ -7,18 +7,25 @@ using System.Buffers; using System.Collections.Generic; using System.Diagnostics; -using System.Text; + +#if SYSTEM_PRIVATE_CORELIB +using StringBuilder = System.Text.ValueStringBuilder; +#else +using StringBuilder = System.Text.StringBuilder; +#endif #nullable enable namespace System.Reflection.Metadata { - // TODO: add proper debugger display stuff + [DebuggerDisplay("{_inputString}")] internal ref struct TypeNameParser { - private const string EndOfTypeNameDelimiters = "[]&*,+"; + private const string EndOfTypeNameDelimiters = ".+"; + private const string EndOfFullTypeNameDelimiters = "[]&*,+"; #if NET8_0_OR_GREATER private static readonly SearchValues _endOfTypeNameDelimitersSearchValues = SearchValues.Create(EndOfTypeNameDelimiters); + private static readonly SearchValues _endOfFullTypeNameDelimitersSearchValues = SearchValues.Create(EndOfFullTypeNameDelimiters); #endif private static readonly TypeNameParserOptions _defaults = new(); private readonly bool _throwOnError; @@ -88,17 +95,17 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse } List? nestedNameLengths = null; - if (!TryGetTypeNameLengthWithNestedNameLengths(_inputString, ref nestedNameLengths, out int typeNameLength)) + if (!TryGetTypeNameLengthWithNestedNameLengths(_inputString, ref nestedNameLengths, out int fullTypeNameLength)) { return null; } - ReadOnlySpan typeName = _inputString.Slice(0, typeNameLength); - if (!_parseOptions.ValidateIdentifier(typeName, _throwOnError)) + ReadOnlySpan fullTypeName = _inputString.Slice(0, fullTypeNameLength); + if (!_parseOptions.ValidateIdentifier(fullTypeName, _throwOnError)) { return null; } - _inputString = _inputString.Slice(typeNameLength); + _inputString = _inputString.Slice(fullTypeNameLength); List? genericArgs = null; // TODO: use some stack-based list in CoreLib @@ -200,20 +207,19 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse #endif } - TypeName? containingType = GetContainingType(ref typeName, nestedNameLengths, assemblyName); - TypeName result = new(typeName.ToString(), assemblyName, rankOrModifier: 0, underlyingType: null, containingType, genericArgs?.ToArray()); + TypeName? containingType = GetContainingType(fullTypeName, nestedNameLengths, assemblyName); + string name = GetName(fullTypeName).ToString(); + TypeName? underlyingType = genericArgs is null ? null : new(name, fullTypeName.ToString(), assemblyName, containingType: containingType); + string genericTypeFullName = GetGenericTypeFullName(fullTypeName, genericArgs); + TypeName result = new(name, genericTypeFullName, assemblyName, rankOrModifier: 0, underlyingType, containingType, genericArgs?.ToArray()); if (previousDecorator != default) // some decorators were recognized { - StringBuilder sb = new StringBuilder(typeName.Length + 4); -#if NET8_0_OR_GREATER - sb.Append(typeName); -#else - for (int i = 0; i < typeName.Length; i++) - { - sb.Append(typeName[i]); - } -#endif + StringBuilder fullNameSb = new(genericTypeFullName.Length + 4); + fullNameSb.Append(genericTypeFullName); + StringBuilder nameSb = new(name.Length + 4); + nameSb.Append(name); + while (TryParseNextDecorator(ref capturedBeforeProcessing, out int parsedModifier)) { // we are not reusing the input string, as it could have contain whitespaces that we want to exclude @@ -225,8 +231,10 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse 1 => "[*]", _ => ArrayRankToString(parsedModifier) }; - sb.Append(trimmedModifier); - result = new(sb.ToString(), assemblyName, parsedModifier, underlyingType: result); + nameSb.Append(trimmedModifier); + fullNameSb.Append(trimmedModifier); + + result = new(nameSb.ToString(), fullNameSb.ToString(), assemblyName, parsedModifier, underlyingType: result); } } @@ -239,7 +247,7 @@ private static bool TryGetTypeNameLengthWithNestedNameLengths(ReadOnlySpan totalLength = 0; do { - int length = GetTypeNameLength(input.Slice(totalLength), out isNestedType); + int length = GetFullTypeNameLength(input.Slice(totalLength), out isNestedType); if (length <= 0) // it's possible only for a pair of unescaped '+' characters { return false; @@ -258,7 +266,7 @@ private static bool TryGetTypeNameLengthWithNestedNameLengths(ReadOnlySpan } // Normalizes "not found" to input length, since caller is expected to slice. - private static int GetTypeNameLength(ReadOnlySpan input, out bool isNestedType) + private static int GetFullTypeNameLength(ReadOnlySpan input, out bool isNestedType) { // 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: @@ -273,14 +281,14 @@ private static int GetTypeNameLength(ReadOnlySpan input, out bool isNested // 'n' is adversary-controlled. To avoid DoS issues here, we'll loop manually. #if NET8_0_OR_GREATER - int offset = input.IndexOfAny(_endOfTypeNameDelimitersSearchValues); + int offset = input.IndexOfAny(_endOfFullTypeNameDelimitersSearchValues); #elif NET6_0_OR_GREATER int offset = input.IndexOfAny(EndOfTypeNameDelimiters); #else int offset; for (offset = 0; offset < input.Length; offset++) { - if (EndOfTypeNameDelimiters.IndexOf(input[offset]) >= 0) { break; } + if (EndOfFullTypeNameDelimiters.IndexOf(input[offset]) >= 0) { break; } } #endif isNestedType = offset > 0 && offset < input.Length && input[offset] == '+'; @@ -328,7 +336,7 @@ private bool TryParseAssemblyName(ref AssemblyName? assemblyName) private static ReadOnlySpan TrimStart(ReadOnlySpan input) => input.TrimStart(' '); // TODO: the CLR parser should trim all whitespaces, but there seems to be no test coverage - private static TypeName? GetContainingType(ref ReadOnlySpan typeName, List? nestedNameLengths, AssemblyName? assemblyName) + private static TypeName? GetContainingType(ReadOnlySpan fullTypeName, List? nestedNameLengths, AssemblyName? assemblyName) { if (nestedNameLengths is null) { @@ -336,16 +344,35 @@ private static ReadOnlySpan TrimStart(ReadOnlySpan input) } TypeName? containingType = null; + int nameOffset = 0; foreach (int nestedNameLength in nestedNameLengths) { Debug.Assert(nestedNameLength > 0, "TryGetTypeNameLengthWithNestedNameLengths should throw on zero lengths"); - containingType = new(typeName.Slice(0, nestedNameLength).ToString(), assemblyName, rankOrModifier: 0, null, containingType: containingType, null); - typeName = typeName.Slice(nestedNameLength + 1); // don't include the `+` in type name + ReadOnlySpan fullName = fullTypeName.Slice(0, nameOffset + nestedNameLength); + ReadOnlySpan name = GetName(fullName); + containingType = new(name.ToString(), fullName.ToString(), assemblyName, containingType: containingType); + nameOffset += nestedNameLength + 1; // include the '+' that was skipped in name } return containingType; } + private static ReadOnlySpan GetName(ReadOnlySpan fullName) + { +#if NET8_0_OR_GREATER + int offset = fullName.LastIndexOfAny(_endOfTypeNameDelimitersSearchValues); +#elif NET6_0_OR_GREATER + int offset = fullName.LastIndexOfAny(EndOfTypeNameDelimiters); +#else + int offset = fullName.Length - 1; + for (; offset >= 0; offset--) + { + if (EndOfTypeNameDelimiters.IndexOf(fullName[offset]) >= 0) { break; } + } +#endif + return offset < 0 ? fullName : fullName.Slice(offset + 1); + } + private bool TryDive(ref int depth) { if (depth >= _parseOptions.MaxRecursiveDepth) @@ -474,5 +501,40 @@ private static string ArrayRankToString(int arrayRank) return sb.ToString(); #endif } + + private static string GetGenericTypeFullName(ReadOnlySpan fullTypeName, List? genericArgs) + { + if (genericArgs is null) + { + return fullTypeName.ToString(); + } + + int size = fullTypeName.Length + 1; + for (int i = 0; i < genericArgs.Count; i++) + { + size += 3 + genericArgs[i].AssemblyQualifiedName.Length; + } + + StringBuilder result = new(size); +#if NET8_0_OR_GREATER + result.Append(fullTypeName); +#else + for (int i = 0; i < fullTypeName.Length; i++) + { + result.Append(fullTypeName[i]); + } +#endif + result.Append('['); + for (int i = 0; i < genericArgs.Count; i++) + { + result.Append('['); + result.Append(genericArgs[i].AssemblyQualifiedName); + result.Append(']'); + result.Append(','); + } + result[result.Length - 1] = ']'; // replace ',' with ']' + + return result.ToString(); + } } } diff --git a/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs index 1bb98ab76f88e..4ed21c25093c0 100644 --- a/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs +++ b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs @@ -95,14 +95,14 @@ private static (string typeNamespace, string name) SplitFullTypeName(string type nestedTypeNames[--nestingDepth] = current.Name; current = current.ContainingType; } - string nonNestedParentName = current!.Name; + string nonNestedParentName = current!.FullName; Type? type = GetType(nonNestedParentName, nestedTypeNames, typeName.AssemblyName); return Make(type, typeName); } else if (typeName.UnderlyingType is null) { - Type? type = GetType(typeName.Name, nestedTypeNames: ReadOnlySpan.Empty, typeName.AssemblyName); + Type? type = GetType(typeName.FullName, nestedTypeNames: ReadOnlySpan.Empty, typeName.AssemblyName); return Make(type, typeName); } diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs index 980a882275d22..d0849b915f550 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs @@ -12,9 +12,16 @@ namespace System.Reflection.Metadata.Tests public class TypeNameParserTests { [Theory] - [InlineData(" System.Int32", "System.Int32")] - public void SpacesAtTheBeginningAreOK(string input, string expectedName) - => Assert.Equal(expectedName, TypeName.Parse(input.AsSpan()).Name); + [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); + } [Theory] [InlineData("")] @@ -42,8 +49,8 @@ public void InvalidTypeNamesAreNotAllowed(string input) [Theory] [InlineData("Namespace.Kość", "Namespace.Kość")] - public void UnicodeCharactersAreAllowedByDefault(string input, string expectedName) - => Assert.Equal(expectedName, TypeName.Parse(input.AsSpan()).Name); + public void UnicodeCharactersAreAllowedByDefault(string input, string expectedFullName) + => Assert.Equal(expectedFullName, TypeName.Parse(input.AsSpan()).FullName); [Theory] [InlineData("Namespace.Kość")] @@ -60,6 +67,7 @@ public static IEnumerable TypeNamesWithAssemblyNames() { "System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", "System.Int32", + "Int32", "mscorlib", new Version(4, 0, 0, 0), "", @@ -69,11 +77,14 @@ public static IEnumerable TypeNamesWithAssemblyNames() [Theory] [MemberData(nameof(TypeNamesWithAssemblyNames))] - public void TypeNameCanContainAssemblyName(string input, string typeName, string assemblyName, Version assemblyVersion, string assemblyCulture, string assemblyPublicKeyToken) + public void TypeNameCanContainAssemblyName(string assemblyQualifiedName, string fullName, string name, string assemblyName, + Version assemblyVersion, string assemblyCulture, string assemblyPublicKeyToken) { - TypeName parsed = TypeName.Parse(input.AsSpan()); + TypeName parsed = TypeName.Parse(assemblyQualifiedName.AsSpan()); - Assert.Equal(typeName, parsed.Name); + Assert.Equal(assemblyQualifiedName, parsed.AssemblyQualifiedName); + Assert.Equal(fullName, parsed.FullName); + Assert.Equal(name, parsed.Name); Assert.NotNull(parsed.AssemblyName); Assert.Equal(assemblyName, parsed.AssemblyName.Name); Assert.Equal(assemblyVersion, parsed.AssemblyName.Version); @@ -125,7 +136,7 @@ public static IEnumerable GenericArgumentsAreSupported_Arguments() }; 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[[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", new string[] { "System.Int32", "System.Boolean" }, new AssemblyName[] @@ -138,18 +149,19 @@ public static IEnumerable GenericArgumentsAreSupported_Arguments() [Theory] [MemberData(nameof(GenericArgumentsAreSupported_Arguments))] - public void GenericArgumentsAreSupported(string input, string typeName, string[] typeNames, AssemblyName[]? assemblyNames) + public void GenericArgumentsAreSupported(string input, string name, string[] genericTypesFullNames, AssemblyName[]? assemblyNames) { TypeName parsed = TypeName.Parse(input.AsSpan()); - Assert.Equal(typeName, parsed.Name); + Assert.Equal(name, parsed.Name); + Assert.Equal(input, parsed.FullName); Assert.True(parsed.IsConstructedGenericType); Assert.False(parsed.IsElementalType); - for (int i = 0; i < typeNames.Length; i++) + for (int i = 0; i < genericTypesFullNames.Length; i++) { TypeName genericArg = parsed.GetGenericArguments()[i]; - Assert.Equal(typeNames[i], genericArg.Name); + Assert.Equal(genericTypesFullNames[i], genericArg.FullName); Assert.True(genericArg.IsElementalType); Assert.False(genericArg.IsConstructedGenericType); @@ -190,7 +202,7 @@ public void DecoratorsAreSupported(string input, string typeNameWithoutDecorator { TypeName parsed = TypeName.Parse(input.AsSpan()); - Assert.Equal(input, parsed.Name); + Assert.Equal(input, parsed.FullName); Assert.Equal(isArray, parsed.IsArray); Assert.Equal(isSzArray, parsed.IsSzArrayType); if (isArray) Assert.Equal(arrayRank, parsed.GetArrayRank()); @@ -200,7 +212,7 @@ public void DecoratorsAreSupported(string input, string typeNameWithoutDecorator TypeName underlyingType = parsed.UnderlyingType; Assert.NotNull(underlyingType); - Assert.Equal(typeNameWithoutDecorators, underlyingType.Name); + Assert.Equal(typeNameWithoutDecorators, underlyingType.FullName); Assert.True(underlyingType.IsElementalType); Assert.False(underlyingType.IsArray); Assert.False(underlyingType.IsSzArrayType); @@ -251,6 +263,32 @@ public void TotalComplexityReturnsExpectedValue(Type type, int expectedComplexit TypeName parsed = TypeName.Parse(type.AssemblyQualifiedName.AsSpan()); Assert.Equal(expectedComplexity, parsed.TotalComplexity); + + Assert.Equal(type.Name, parsed.Name); + Assert.Equal(type.FullName, parsed.FullName); + Assert.Equal(type.AssemblyQualifiedName, parsed.AssemblyQualifiedName); + } + + [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))] + 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); + + Type genericType = type.GetGenericTypeDefinition(); + Assert.Equal(genericType.Name, parsed.UnderlyingType.Name); + Assert.Equal(genericType.FullName, parsed.UnderlyingType.FullName); + Assert.Equal(genericType.AssemblyQualifiedName, parsed.UnderlyingType.AssemblyQualifiedName); } [Theory] @@ -260,7 +298,9 @@ public void TotalComplexityReturnsExpectedValue(Type type, int expectedComplexit [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))] @@ -283,14 +323,20 @@ public void CanImplementGetTypeUsingPublicAPIs_Roundtrip(Type type) static void Test(Type type) { - TypeName typeName = TypeName.Parse(type.AssemblyQualifiedName.AsSpan()); - Verify(type, typeName, ignoreCase: false); - - typeName = TypeName.Parse(type.AssemblyQualifiedName.ToLower().AsSpan()); - Verify(type, typeName, ignoreCase: true); - - typeName = TypeName.Parse(type.AssemblyQualifiedName.ToUpper().AsSpan()); - Verify(type, typeName, ignoreCase: true); + 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) { @@ -318,11 +364,11 @@ static void Verify(Type type, TypeName typeName, bool ignoreCase) } return Make(GetType(typeName.ContainingType, throwOnError, ignoreCase)?.GetNestedType(typeName.Name, flagsCopiedFromClr)); } - else if (typeName.UnderlyingType is null) + else if (typeName.UnderlyingType is null) // elemental { Type? type = typeName.AssemblyName is null - ? Type.GetType(typeName.Name, throwOnError, ignoreCase) - : Assembly.Load(typeName.AssemblyName).GetType(typeName.Name, throwOnError, ignoreCase); + ? Type.GetType(typeName.FullName, throwOnError, ignoreCase) + : Assembly.Load(typeName.AssemblyName).GetType(typeName.FullName, throwOnError, ignoreCase); return Make(type); } From a2296d0ecfc2219ddd89e4610a49530d07858e3e Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 9 Feb 2024 08:49:18 +0100 Subject: [PATCH 16/48] back tick error handling --- .../Reflection/Metadata/TypeNameParser.cs | 74 ++++++++++++++++--- .../tests/Metadata/TypeNameParserTests.cs | 8 ++ 2 files changed, 72 insertions(+), 10 deletions(-) diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs index 22389e3f20a54..da817670258cb 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs @@ -95,7 +95,7 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse } List? nestedNameLengths = null; - if (!TryGetTypeNameLengthWithNestedNameLengths(_inputString, ref nestedNameLengths, out int fullTypeNameLength)) + if (!TryGetTypeNameLengthWithNestedNameLengths(_inputString, ref nestedNameLengths, out int fullTypeNameLength, out int genericArgCount)) { return null; } @@ -107,7 +107,9 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse } _inputString = _inputString.Slice(fullTypeNameLength); - List? genericArgs = null; // TODO: use some stack-based list in CoreLib + int genericArgIndex = 0; + // Don't allocate now, as it may be an open generic type like "List`1" + TypeName[]? genericArgs = null; // Are there any captured generic args? We'll look for "[[" and "[". // There are no spaces allowed before the first '[', but spaces are allowed @@ -121,6 +123,15 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse ParseAnotherGenericArg: + // Invalid generic argument count provided after backtick. + // Examples: + // - too many: List`1[[a], [b]] + // - not expected: NoBacktick[[a]] + if (genericArgIndex >= genericArgCount) + { + return null; + } + recursiveDepth = startingRecursionCheck; // Namespace.Type`1[[GenericArgument1, AssemblyName1],[GenericArgument2, AssemblyName2]] - double square bracket syntax allows for fully qualified type names // Namespace.Type`1[GenericArgument1,GenericArgument2] - single square bracket syntax is legal only for non-fully qualified type names @@ -141,7 +152,7 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse return null; } - (genericArgs ??= new()).Add(genericArg); + (genericArgs ??= new TypeName[genericArgCount])[genericArgIndex++] = genericArg; if (TryStripFirstCharAndTrailingSpaces(ref _inputString, ',')) { @@ -162,6 +173,13 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse return null; } + // We have reached the end of generic arguments, but parsed fewer than expected. + // Example: A`2[[b]] + if (genericArgIndex != genericArgCount) + { + return null; + } + // And now that we're at the end, restore the max observed recursion count. recursiveDepth = maxObservedRecursionCheck; } @@ -211,7 +229,7 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse string name = GetName(fullTypeName).ToString(); TypeName? underlyingType = genericArgs is null ? null : new(name, fullTypeName.ToString(), assemblyName, containingType: containingType); string genericTypeFullName = GetGenericTypeFullName(fullTypeName, genericArgs); - TypeName result = new(name, genericTypeFullName, assemblyName, rankOrModifier: 0, underlyingType, containingType, genericArgs?.ToArray()); + TypeName result = new(name, genericTypeFullName, assemblyName, rankOrModifier: 0, underlyingType, containingType, genericArgs); if (previousDecorator != default) // some decorators were recognized { @@ -241,10 +259,12 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse return result; } - private static bool TryGetTypeNameLengthWithNestedNameLengths(ReadOnlySpan input, ref List? nestedNameLengths, out int totalLength) + private static bool TryGetTypeNameLengthWithNestedNameLengths(ReadOnlySpan input, ref List? nestedNameLengths, + out int totalLength, out int genericArgCount) { bool isNestedType; totalLength = 0; + genericArgCount = 0; do { int length = GetFullTypeNameLength(input.Slice(totalLength), out isNestedType); @@ -253,6 +273,8 @@ private static bool TryGetTypeNameLengthWithNestedNameLengths(ReadOnlySpan return false; } + genericArgCount += GetGenericArgumentCount(input.Slice(totalLength, length)); + if (isNestedType) { // do not validate the type name now, it will be validated as a whole nested type name later @@ -502,7 +524,7 @@ private static string ArrayRankToString(int arrayRank) #endif } - private static string GetGenericTypeFullName(ReadOnlySpan fullTypeName, List? genericArgs) + private static string GetGenericTypeFullName(ReadOnlySpan fullTypeName, TypeName[]? genericArgs) { if (genericArgs is null) { @@ -510,9 +532,9 @@ private static string GetGenericTypeFullName(ReadOnlySpan fullTypeName, Li } int size = fullTypeName.Length + 1; - for (int i = 0; i < genericArgs.Count; i++) + foreach (TypeName genericArg in genericArgs) { - size += 3 + genericArgs[i].AssemblyQualifiedName.Length; + size += 3 + genericArg.AssemblyQualifiedName.Length; } StringBuilder result = new(size); @@ -525,10 +547,10 @@ private static string GetGenericTypeFullName(ReadOnlySpan fullTypeName, Li } #endif result.Append('['); - for (int i = 0; i < genericArgs.Count; i++) + foreach (TypeName genericArg in genericArgs) { result.Append('['); - result.Append(genericArgs[i].AssemblyQualifiedName); + result.Append(genericArg.AssemblyQualifiedName); result.Append(']'); result.Append(','); } @@ -536,5 +558,37 @@ private static string GetGenericTypeFullName(ReadOnlySpan fullTypeName, Li return result.ToString(); } + + private static int GetGenericArgumentCount(ReadOnlySpan fullTypeName) + { + const int ShortestPossibleGenericTypeName = 3; // single letter followed by a backtick and one digit + if (fullTypeName.Length < ShortestPossibleGenericTypeName || !IsAsciiDigit(fullTypeName[fullTypeName.Length - 1])) + { + return 0; + } + + int backtickIndex = fullTypeName.Length - 2; // we already know it's true for the last one + for (; backtickIndex >= 0; backtickIndex--) + { + if (fullTypeName[backtickIndex] == '`') + return int.Parse(fullTypeName.Slice(backtickIndex + 1) +#if NET8_0_OR_GREATER + ); +#else + .ToString()); +#endif + else if (!IsAsciiDigit(fullTypeName[backtickIndex])) + break; + } + + return 0; + + static bool IsAsciiDigit(char ch) => +#if NET8_0_OR_GREATER + char.IsAsciiDigit(ch); +#else + ch >= '0' && ch <= '9'; +#endif + } } } diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs index d0849b915f550..4eaf7ead4852d 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs @@ -40,6 +40,14 @@ public void EmptyStringsAreNotAllowed(string input) [InlineData("MissingAssemblyName, ")] [InlineData("ExtraComma, ,")] [InlineData("ExtraComma, , System.Runtime")] + [InlineData("TooManyGenericArgumentsDoubleSquareBracket'1[[a],[b]]")] + [InlineData("TooManyGenericArgumentsSingleSquareBracket'1[a,b]")] + [InlineData("TooManyGenericArgumentsDoubleSquareBracketTwoDigits'10[[1],[2],[3],[4],[5],[6],[7],[8],[9],[10],[11]]")] + [InlineData("TooManyGenericArgumentsSingleSquareBracketTwoDigits'10[1,2,3,4,5,6,7,8,9,10,11]")] + [InlineData("TooFewGenericArgumentsDoubleSquareBracket'3[[a],[b]]")] + [InlineData("TooFewGenericArgumentsDoubleSquareBracket'3[a,b]")] + [InlineData("TooFewGenericArgumentsDoubleSquareBracketTwoDigits'10[[1],[2],[3],[4],[5],[6],[7],[8],[9]]")] + [InlineData("TooFewGenericArgumentsSingleSquareBracketTwoDigits'10[1,2,3,4,5,6,7,8,9]")] public void InvalidTypeNamesAreNotAllowed(string input) { Assert.Throws(() => TypeName.Parse(input.AsSpan())); From 4c8a6f73c617759fecc20bc81263f24c583321ae Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 9 Feb 2024 10:05:42 +0100 Subject: [PATCH 17/48] move helper methods to a standalone helper type, include it as a link in test project and cover with tests, fix edge case bugs --- .../ILVerification/ILVerification.projitems | 3 + .../ILCompiler.TypeSystem.csproj | 9 +- .../System/Reflection/Metadata/TypeName.cs | 20 +- .../Reflection/Metadata/TypeNameParser.cs | 239 +-------------- .../Metadata/TypeNameParserHelpers.cs | 289 ++++++++++++++++++ .../System.Private.CoreLib.Shared.projitems | 3 + .../src/System.Reflection.Metadata.csproj | 1 + .../Metadata/TypeNameParserHelpersTests.cs | 159 ++++++++++ .../tests/Metadata/TypeNameParserTests.cs | 9 + .../System.Reflection.Metadata.Tests.csproj | 3 + 10 files changed, 497 insertions(+), 238 deletions(-) create mode 100644 src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs create mode 100644 src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs diff --git a/src/coreclr/tools/ILVerification/ILVerification.projitems b/src/coreclr/tools/ILVerification/ILVerification.projitems index e072d2e593cff..2d260c9f45037 100644 --- a/src/coreclr/tools/ILVerification/ILVerification.projitems +++ b/src/coreclr/tools/ILVerification/ILVerification.projitems @@ -81,6 +81,9 @@ Utilities\TypeNameParser.cs + + Utilities\TypeNameParserHelpers.cs + Utilities\CustomAttributeTypeNameParser.Helpers diff --git a/src/coreclr/tools/aot/ILCompiler.TypeSystem/ILCompiler.TypeSystem.csproj b/src/coreclr/tools/aot/ILCompiler.TypeSystem/ILCompiler.TypeSystem.csproj index d93115e3a87aa..72bc53d2e2ad1 100644 --- a/src/coreclr/tools/aot/ILCompiler.TypeSystem/ILCompiler.TypeSystem.csproj +++ b/src/coreclr/tools/aot/ILCompiler.TypeSystem/ILCompiler.TypeSystem.csproj @@ -193,12 +193,15 @@ Utilities\TypeName.cs - - Utilities\TypeNameParserOptions.cs - Utilities\TypeNameParser.cs + + Utilities\TypeNameParserHelpers.cs + + + Utilities\TypeNameParserOptions.cs + Utilities\CustomAttributeTypeNameParser.Helpers diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs index a9b7d51d94d16..e0e13e4edc006 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs @@ -16,12 +16,10 @@ namespace System.Reflection.Metadata #endif sealed class TypeName { - internal const int SZArray = -1; - internal const int Pointer = -2; - internal const int ByRef = -3; - - // Positive value is array rank. - // Negative value is modifier encoded using constants above. + /// + /// Positive value is array rank. + /// Negative value is modifier encoded using constants defined in . + /// private readonly int _rankOrModifier; private readonly TypeName[]? _genericArguments; private string? _assemblyQualifiedName; @@ -87,7 +85,7 @@ public string AssemblyQualifiedName /// Returns true if this type represents any kind of array, regardless of the array's /// rank or its bounds. /// - public bool IsArray => _rankOrModifier == SZArray || _rankOrModifier > 0; + public bool IsArray => _rankOrModifier == TypeNameParserHelpers.SZArray || _rankOrModifier > 0; /// /// Returns true if this type represents a constructed generic type (e.g., "List<int>"). @@ -115,7 +113,7 @@ public string AssemblyQualifiedName /// Returns true if this is a managed pointer type (e.g., "ref int"). /// Managed pointer types are sometimes called byref types () /// - public bool IsManagedPointerType => _rankOrModifier == ByRef; // name inconsistent with Type.IsByRef + public bool IsManagedPointerType => _rankOrModifier == TypeNameParserHelpers.ByRef; // name inconsistent with Type.IsByRef /// /// Returns true if this is a nested type (e.g., "Namespace.Containing+Nested"). @@ -126,13 +124,13 @@ public string AssemblyQualifiedName /// /// Returns true if this type represents a single-dimensional, zero-indexed array (e.g., "int[]"). /// - public bool IsSzArrayType => _rankOrModifier == SZArray; // name could be more user-friendly + public bool IsSzArrayType => _rankOrModifier == TypeNameParserHelpers.SZArray; // name could be more user-friendly /// /// Returns true if this type represents an unmanaged pointer (e.g., "int*" or "void*"). /// Unmanaged pointer types are often just called pointers () /// - public bool IsUnmanagedPointerType => _rankOrModifier == Pointer; // name inconsistent with Type.IsPointer + public bool IsUnmanagedPointerType => _rankOrModifier == TypeNameParserHelpers.Pointer; // name inconsistent with Type.IsPointer /// /// Returns true if this type represents a variable-bound array; that is, an array of rank greater @@ -201,7 +199,7 @@ public static bool TryParse(ReadOnlySpan typeName, public int GetArrayRank() => _rankOrModifier switch { - SZArray => 1, + TypeNameParserHelpers.SZArray => 1, _ when _rankOrModifier > 0 => _rankOrModifier, _ => throw new ArgumentException("SR.Argument_HasToBeArrayClass") // TODO: use actual resource (used by Type.GetArrayRank) }; diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs index da817670258cb..722e689934a95 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs @@ -4,7 +4,6 @@ #if SYSTEM_PRIVATE_CORELIB #define NET8_0_OR_GREATER #endif -using System.Buffers; using System.Collections.Generic; using System.Diagnostics; @@ -14,6 +13,8 @@ using StringBuilder = System.Text.StringBuilder; #endif +using static System.Reflection.Metadata.TypeNameParserHelpers; + #nullable enable namespace System.Reflection.Metadata @@ -21,12 +22,7 @@ namespace System.Reflection.Metadata [DebuggerDisplay("{_inputString}")] internal ref struct TypeNameParser { - private const string EndOfTypeNameDelimiters = ".+"; - private const string EndOfFullTypeNameDelimiters = "[]&*,+"; -#if NET8_0_OR_GREATER - private static readonly SearchValues _endOfTypeNameDelimitersSearchValues = SearchValues.Create(EndOfTypeNameDelimiters); - private static readonly SearchValues _endOfFullTypeNameDelimitersSearchValues = SearchValues.Create(EndOfFullTypeNameDelimiters); -#endif + private const int MaxArrayRank = 32; private static readonly TypeNameParserOptions _defaults = new(); private readonly bool _throwOnError; private readonly TypeNameParserOptions _parseOptions; @@ -95,7 +91,7 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse } List? nestedNameLengths = null; - if (!TryGetTypeNameLengthWithNestedNameLengths(_inputString, ref nestedNameLengths, out int fullTypeNameLength, out int genericArgCount)) + if (!TryGetTypeNameInfo(_inputString, ref nestedNameLengths, out int fullTypeNameLength, out int genericArgCount)) { return null; } @@ -152,6 +148,7 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse return null; } + // TODO adsitnik: don't allocate an array that would require reaching the max depth limit (genericArgs ??= new TypeName[genericArgCount])[genericArgIndex++] = genericArg; if (TryStripFirstCharAndTrailingSpaces(ref _inputString, ',')) @@ -203,7 +200,11 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse return null; } - if (previousDecorator == TypeName.ByRef) // it's illegal for managed reference to be followed by any other decorator + if (previousDecorator == ByRef) // it's illegal for managed reference to be followed by any other decorator + { + return null; + } + else if (parsedDecorator > MaxArrayRank) { return null; } @@ -241,14 +242,7 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse while (TryParseNextDecorator(ref capturedBeforeProcessing, out int parsedModifier)) { // we are not reusing the input string, as it could have contain whitespaces that we want to exclude - string trimmedModifier = parsedModifier switch - { - TypeName.ByRef => "&", - TypeName.Pointer => "*", - TypeName.SZArray => "[]", - 1 => "[*]", - _ => ArrayRankToString(parsedModifier) - }; + string trimmedModifier = GetRankOrModifierStringRepresentation(parsedModifier); nameSb.Append(trimmedModifier); fullNameSb.Append(trimmedModifier); @@ -259,65 +253,6 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse return result; } - private static bool TryGetTypeNameLengthWithNestedNameLengths(ReadOnlySpan input, ref List? nestedNameLengths, - out int totalLength, out int genericArgCount) - { - bool isNestedType; - totalLength = 0; - genericArgCount = 0; - do - { - int length = GetFullTypeNameLength(input.Slice(totalLength), out isNestedType); - if (length <= 0) // it's possible only for a pair of unescaped '+' characters - { - return false; - } - - genericArgCount += GetGenericArgumentCount(input.Slice(totalLength, length)); - - if (isNestedType) - { - // do not validate the type name now, it will be validated as a whole nested type name later - (nestedNameLengths ??= new()).Add(length); - totalLength += 1; // skip the '+' sign in next search - } - totalLength += length; - } while (isNestedType); - - return true; - } - - // Normalizes "not found" to input length, since caller is expected to slice. - private static int GetFullTypeNameLength(ReadOnlySpan input, out bool isNestedType) - { - // 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(_endOfFullTypeNameDelimitersSearchValues); -#elif NET6_0_OR_GREATER - int offset = input.IndexOfAny(EndOfTypeNameDelimiters); -#else - int offset; - for (offset = 0; offset < input.Length; offset++) - { - if (EndOfFullTypeNameDelimiters.IndexOf(input[offset]) >= 0) { break; } - } -#endif - isNestedType = offset > 0 && offset < input.Length && input[offset] == '+'; - - return (int)Math.Min((uint)offset, (uint)input.Length); - } - /// false means the input was invalid and parsing has failed. Empty input is valid and returns true. private bool TryParseAssemblyName(ref AssemblyName? assemblyName) { @@ -355,9 +290,6 @@ private bool TryParseAssemblyName(ref AssemblyName? assemblyName) return true; } - private static ReadOnlySpan TrimStart(ReadOnlySpan input) - => input.TrimStart(' '); // TODO: the CLR parser should trim all whitespaces, but there seems to be no test coverage - private static TypeName? GetContainingType(ReadOnlySpan fullTypeName, List? nestedNameLengths, AssemblyName? assemblyName) { if (nestedNameLengths is null) @@ -369,7 +301,7 @@ private static ReadOnlySpan TrimStart(ReadOnlySpan input) int nameOffset = 0; foreach (int nestedNameLength in nestedNameLengths) { - Debug.Assert(nestedNameLength > 0, "TryGetTypeNameLengthWithNestedNameLengths should throw on zero lengths"); + Debug.Assert(nestedNameLength > 0, "TryGetTypeNameInfo should return error on zero lengths"); ReadOnlySpan fullName = fullTypeName.Slice(0, nameOffset + nestedNameLength); ReadOnlySpan name = GetName(fullName); containingType = new(name.ToString(), fullName.ToString(), assemblyName, containingType: containingType); @@ -379,22 +311,6 @@ private static ReadOnlySpan TrimStart(ReadOnlySpan input) return containingType; } - private static ReadOnlySpan GetName(ReadOnlySpan fullName) - { -#if NET8_0_OR_GREATER - int offset = fullName.LastIndexOfAny(_endOfTypeNameDelimitersSearchValues); -#elif NET6_0_OR_GREATER - int offset = fullName.LastIndexOfAny(EndOfTypeNameDelimiters); -#else - int offset = fullName.Length - 1; - for (; offset >= 0; offset--) - { - if (EndOfTypeNameDelimiters.IndexOf(fullName[offset]) >= 0) { break; } - } -#endif - return offset < 0 ? fullName : fullName.Slice(offset + 1); - } - private bool TryDive(ref int depth) { if (depth >= _parseOptions.MaxRecursiveDepth) @@ -405,44 +321,6 @@ private bool TryDive(ref int depth) return true; } - // Are there any captured generic args? We'll look for "[[" and "[" that is not followed by "]", "*" and ",". - private static bool IsBeginningOfGenericAgs(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 = TrimStart(span.Slice(1)); - if (!trimmed.IsEmpty) - { - if (trimmed[0] == '[') - { - doubleBrackets = true; - span = TrimStart(trimmed.Slice(1)); - return true; - } - if (!(trimmed[0] is ',' or '*' or ']')) // [] or [*] or [,] or [,,,, ...] - { - span = trimmed; - return true; - } - } - } - - return false; - } - - private static bool TryStripFirstCharAndTrailingSpaces(ref ReadOnlySpan span, char value) - { - if (!span.IsEmpty && span[0] == value) - { - span = TrimStart(span.Slice(1)); - return true; - } - return false; - } - private static bool TryParseNextDecorator(ref ReadOnlySpan input, out int rankOrModifier) { // Then try pulling a single decorator. @@ -452,13 +330,13 @@ private static bool TryParseNextDecorator(ref ReadOnlySpan input, out int if (TryStripFirstCharAndTrailingSpaces(ref input, '*')) { - rankOrModifier = TypeName.Pointer; + rankOrModifier = TypeNameParserHelpers.Pointer; return true; } if (TryStripFirstCharAndTrailingSpaces(ref input, '&')) { - rankOrModifier = TypeName.ByRef; + rankOrModifier = ByRef; return true; } @@ -475,7 +353,7 @@ private static bool TryParseNextDecorator(ref ReadOnlySpan input, out int if (TryStripFirstCharAndTrailingSpaces(ref input, ']')) { // End of array marker - rankOrModifier = rank == 1 && !hasSeenAsterisk ? TypeName.SZArray : rank; + rankOrModifier = rank == 1 && !hasSeenAsterisk ? SZArray : rank; return true; } @@ -503,92 +381,5 @@ private static bool TryParseNextDecorator(ref ReadOnlySpan input, out int rankOrModifier = 0; return false; } - - private static string ArrayRankToString(int arrayRank) - { -#if NET8_0_OR_GREATER - return string.Create(2 + arrayRank - 1, arrayRank, (buffer, rank) => - { - buffer[0] = '['; - for (int i = 1; i < rank; i++) - buffer[i] = ','; - buffer[^1] = ']'; - }); -#else - StringBuilder sb = new(2 + arrayRank - 1); - sb.Append('['); - for (int i = 1; i < arrayRank; i++) - sb.Append(','); - sb.Append(']'); - return sb.ToString(); -#endif - } - - private static string GetGenericTypeFullName(ReadOnlySpan fullTypeName, TypeName[]? genericArgs) - { - if (genericArgs is null) - { - return fullTypeName.ToString(); - } - - int size = fullTypeName.Length + 1; - foreach (TypeName genericArg in genericArgs) - { - size += 3 + genericArg.AssemblyQualifiedName.Length; - } - - StringBuilder result = new(size); -#if NET8_0_OR_GREATER - result.Append(fullTypeName); -#else - for (int i = 0; i < fullTypeName.Length; i++) - { - result.Append(fullTypeName[i]); - } -#endif - 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(); - } - - private static int GetGenericArgumentCount(ReadOnlySpan fullTypeName) - { - const int ShortestPossibleGenericTypeName = 3; // single letter followed by a backtick and one digit - if (fullTypeName.Length < ShortestPossibleGenericTypeName || !IsAsciiDigit(fullTypeName[fullTypeName.Length - 1])) - { - return 0; - } - - int backtickIndex = fullTypeName.Length - 2; // we already know it's true for the last one - for (; backtickIndex >= 0; backtickIndex--) - { - if (fullTypeName[backtickIndex] == '`') - return int.Parse(fullTypeName.Slice(backtickIndex + 1) -#if NET8_0_OR_GREATER - ); -#else - .ToString()); -#endif - else if (!IsAsciiDigit(fullTypeName[backtickIndex])) - break; - } - - return 0; - - static bool IsAsciiDigit(char ch) => -#if NET8_0_OR_GREATER - char.IsAsciiDigit(ch); -#else - ch >= '0' && ch <= '9'; -#endif - } } } 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 0000000000000..2a17bd6ab5ee6 --- /dev/null +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs @@ -0,0 +1,289 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if SYSTEM_PRIVATE_CORELIB +#define NET8_0_OR_GREATER +#endif +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics; + +#if SYSTEM_PRIVATE_CORELIB +using StringBuilder = System.Text.ValueStringBuilder; +#else +using StringBuilder = System.Text.StringBuilder; +#endif + +using static System.Array; +using static System.Char; +using static System.Int32; + +#nullable enable + +namespace System.Reflection.Metadata +{ + internal static class TypeNameParserHelpers + { + internal const int SZArray = -1; + internal const int Pointer = -2; + internal const int ByRef = -3; + private const char EscapeCharacter = '\\'; + private const string EndOfTypeNameDelimiters = ".+"; + private const string EndOfFullTypeNameDelimiters = "[]&*,+"; +#if NET8_0_OR_GREATER + private static readonly SearchValues _endOfTypeNameDelimitersSearchValues = SearchValues.Create(EndOfTypeNameDelimiters); + private static readonly SearchValues _endOfFullTypeNameDelimitersSearchValues = SearchValues.Create(EndOfFullTypeNameDelimiters); +#endif + + /// + /// Negative value for invalid type names. + /// Zero for valid non-generic type names. + /// Positive value for valid generic type names. + /// + internal static int GetGenericArgumentCount(ReadOnlySpan fullTypeName) + { + const int ShortestInvalidTypeName = 2; // Back tick and one digit. Example: "`1" + if (fullTypeName.Length < ShortestInvalidTypeName || !IsAsciiDigit(fullTypeName[fullTypeName.Length - 1])) + { + return 0; + } + + int backtickIndex = fullTypeName.Length - 2; // we already know it's true for the last one + for (; backtickIndex >= 0; backtickIndex--) + { + if (fullTypeName[backtickIndex] == '`') + { + if (backtickIndex == 0) + { + return -1; // illegal name, example "`1" + } + else if (fullTypeName[backtickIndex - 1] == EscapeCharacter) + { + return 0; // legal name, but not a generic type definition. Example: "Escaped\\`1" + } + else if (TryParse(fullTypeName.Slice(backtickIndex + 1), out int value)) + { + // From C# 2.0 language spec: 8.16.3 Multiple type parameters Generic type declarations can have any number of type parameters. + if (value > MaxLength) + { + // But.. it's impossible to create a type with more than Array.MaxLength. + // OOM is also not welcomed in the parser! + return -1; + } + + // the value can still be negative, but it's fine as the caller should treat that as an error + return value; + } + + // most likely the value was too large to be parsed as an int + return -1; + } + else if (!IsAsciiDigit(fullTypeName[backtickIndex]) && fullTypeName[backtickIndex] != '-') + { + break; + } + } + + return 0; + } + + internal static string GetGenericTypeFullName(ReadOnlySpan fullTypeName, TypeName[]? genericArgs) + { + if (genericArgs is null) + { + return fullTypeName.ToString(); + } + + int size = fullTypeName.Length + 1; + foreach (TypeName genericArg in genericArgs) + { + size += 3 + genericArg.AssemblyQualifiedName.Length; + } + + StringBuilder result = new(size); +#if NET8_0_OR_GREATER + result.Append(fullTypeName); +#else + for (int i = 0; i < fullTypeName.Length; i++) + { + result.Append(fullTypeName[i]); + } +#endif + 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(); + } + + // Normalizes "not found" to input length, since caller is expected to slice. + internal static int GetFullTypeNameLength(ReadOnlySpan input, out bool isNestedType) + { + // 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(_endOfFullTypeNameDelimitersSearchValues); +#elif NET6_0_OR_GREATER + int offset = input.IndexOfAny(EndOfTypeNameDelimiters); +#else + int offset; + for (offset = 0; offset < input.Length; offset++) + { + if (EndOfFullTypeNameDelimiters.IndexOf(input[offset]) >= 0) { break; } + } +#endif + isNestedType = offset > 0 && offset < input.Length && input[offset] == '+'; + + return (int)Math.Min((uint)offset, (uint)input.Length); + } + + internal static ReadOnlySpan GetName(ReadOnlySpan fullName) + { +#if NET8_0_OR_GREATER + int offset = fullName.LastIndexOfAny(_endOfTypeNameDelimitersSearchValues); +#elif NET6_0_OR_GREATER + int offset = fullName.LastIndexOfAny(EndOfTypeNameDelimiters); +#else + int offset = fullName.Length - 1; + for (; offset >= 0; offset--) + { + if (EndOfTypeNameDelimiters.IndexOf(fullName[offset]) >= 0) { break; } + } +#endif + return offset < 0 ? fullName : fullName.Slice(offset + 1); + } + + internal static string GetRankOrModifierStringRepresentation(int rankOrModifier) + { + return rankOrModifier switch + { + ByRef => "&", + Pointer => "*", + SZArray => "[]", + 1 => "[*]", + _ => ArrayRankToString(rankOrModifier) + }; + + static string ArrayRankToString(int arrayRank) + { + Debug.Assert(arrayRank >= 2 && arrayRank <= 32); + +#if NET8_0_OR_GREATER + return string.Create(2 + arrayRank - 1, arrayRank, (buffer, rank) => + { + buffer[0] = '['; + for (int i = 1; i < rank; i++) + buffer[i] = ','; + buffer[^1] = ']'; + }); +#else + StringBuilder sb = new(2 + arrayRank - 1); + sb.Append('['); + for (int i = 1; i < arrayRank; i++) + sb.Append(','); + sb.Append(']'); + return sb.ToString(); +#endif + } + } + + /// + /// Are there any captured generic args? We'll look for "[[" and "[" that is not followed by "]", "*" and ",". + /// + internal static bool IsBeginningOfGenericAgs(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 = TrimStart(span.Slice(1)); + if (!trimmed.IsEmpty) + { + if (trimmed[0] == '[') + { + doubleBrackets = true; + span = TrimStart(trimmed.Slice(1)); + return true; + } + if (!(trimmed[0] is ',' or '*' or ']')) // [] or [*] or [,] or [,,,, ...] + { + span = trimmed; + return true; + } + } + } + + return false; + } + + internal static ReadOnlySpan TrimStart(ReadOnlySpan input) => input.TrimStart(); + + internal static bool TryGetTypeNameInfo(ReadOnlySpan input, ref List? nestedNameLengths, + out int totalLength, out int genericArgCount) + { + bool isNestedType; + totalLength = 0; + genericArgCount = 0; + do + { + int length = GetFullTypeNameLength(input.Slice(totalLength), out isNestedType); + if (length <= 0) // it's possible only for a pair of unescaped '+' characters + { + return false; + } + + int generics = GetGenericArgumentCount(input.Slice(totalLength, length)); + if (generics < 0) + { + return false; // invalid type name detected! + } + genericArgCount += generics; + + if (isNestedType) + { + // do not validate the type name now, it will be validated as a whole nested type name later + (nestedNameLengths ??= new()).Add(length); + totalLength += 1; // skip the '+' sign in next search + } + totalLength += length; + } while (isNestedType); + + return true; + } + + internal static bool TryStripFirstCharAndTrailingSpaces(ref ReadOnlySpan span, char value) + { + if (!span.IsEmpty && span[0] == value) + { + span = TrimStart(span.Slice(1)); + return true; + } + return false; + } + +#if !NETCOREAPP + private const int MaxLength = 2147483591; + + private static bool TryParse(ReadOnlySpan input, out int value) => int.TryParse(input.ToString(), out value); + + private static bool IsAsciiDigit(char ch) => ch >= '0' && ch <= '9'; +#endif + } +} 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 68fd8a957d85a..17eba2c383d48 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 @@ -1471,6 +1471,9 @@ Common\System\Reflection\Metadata\TypeNameParser.cs + + Common\System\Reflection\Metadata\TypeNameParserHelpers.cs + Common\System\Reflection\Metadata\TypeNameParserOptions.cs 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 2348f979e262c..4964985ffb4fd 100644 --- a/src/libraries/System.Reflection.Metadata/src/System.Reflection.Metadata.csproj +++ b/src/libraries/System.Reflection.Metadata/src/System.Reflection.Metadata.csproj @@ -255,6 +255,7 @@ The System.Reflection.Metadata library is built-in as part of the shared framewo + 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 0000000000000..3cf5c95c41a0c --- /dev/null +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs @@ -0,0 +1,159 @@ +// 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.Linq; +using Xunit; + +namespace System.Reflection.Metadata.Tests +{ + public class TypeNameParserHelpersTests + { + public static IEnumerable GetGenericArgumentCountReturnsExpectedValue_Args() + { + int maxArrayLength = +#if NETCOREAPP + Array.MaxLength; +#else + 2147483591; +#endif + + yield return new object[] { $"TooLargeForInt`{long.MaxValue}", -1 }; + yield return new object[] { $"TooLargeForInt`{(long)int.MaxValue + 1}", -1 }; + yield return new object[] { $"TooLargeForInt`{(long)uint.MaxValue + 1}", -1 }; + yield return new object[] { $"MaxArrayLength`{maxArrayLength}", maxArrayLength }; + yield return new object[] { $"TooLargeForAnArray`{maxArrayLength + 1}", -1 }; + } + + [Theory] + [InlineData("", 0)] // empty input + [InlineData("1", 0)] // short, valid + [InlineData("`1", -1)] // short, back tick as first char + [InlineData("`111", -1)] // long, back tick as first char + [InlineData("\\`111", 0)] // long enough, escaped back tick as first char + [InlineData("NoBackTick2", 0)] // no backtick, single digit + [InlineData("NoBackTick123", 0)] // no backtick, few digits + [InlineData("a`1", 1)] // valid, single digit + [InlineData("a`666", 666)] // valid, few digits + [InlineData("DigitBeforeBackTick1`7", 7)] // valid, single digit + [InlineData("DigitBeforeBackTick123`321", 321)] // valid, few digits + [InlineData("EscapedBacktick\\`1", 0)] // escaped backtick, single digit + [InlineData("EscapedBacktick\\`123", 0)] // escaped backtick, few digits + [InlineData("NegativeValue`-1", -1)] // negative value, single digit + [InlineData("NegativeValue`-222", -222)] // negative value, few digits + [InlineData("EscapedBacktickNegativeValue\\`-1", 0)] // negative value, single digit + [InlineData("EscapedBacktickNegativeValue\\`-222", 0)] // negative value, few digits + [MemberData(nameof(GetGenericArgumentCountReturnsExpectedValue_Args))] + public void GetGenericArgumentCountReturnsExpectedValue(string input, int expected) + => Assert.Equal(expected, TypeNameParserHelpers.GetGenericArgumentCount(input.AsSpan())); + + + [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)] + // TODO adsitnik: add escaping handling + 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}"; + Assert.Equal(expected + withNamespace.Length - input.Length, TypeNameParserHelpers.GetFullTypeNameLength(withNamespace.AsSpan(), out isNested)); + Assert.Equal(expectedIsNested, isNested); + } + + [Theory] + [InlineData(TypeNameParserHelpers.SZArray, "[]")] + [InlineData(TypeNameParserHelpers.Pointer, "*")] + [InlineData(TypeNameParserHelpers.ByRef, "&")] + [InlineData(1, "[*]")] + [InlineData(2, "[,]")] + [InlineData(3, "[,,]")] + [InlineData(4, "[,,,]")] + public void GetRankOrModifierStringRepresentationReturnsExpectedString(int input, string expected) + => Assert.Equal(expected, TypeNameParserHelpers.GetRankOrModifierStringRepresentation(input)); + + [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()); + TypeName[] 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.IsBeginningOfGenericAgs(ref inputSpan, out bool doubleBrackets)); + Assert.Equal(expectedDoubleBrackets, doubleBrackets); + Assert.Equal(expectedConsumedInput, inputSpan.ToString()); + } + + [Theory] + [InlineData(" \t\r\nA.B.C", "A.B.C")] + [InlineData(" A.B.C\t\r\n", "A.B.C\t\r\n")] // don't trim the end + public void TrimStartTrimsAllWhitespaces(string input, string expectedResult) + { + ReadOnlySpan inputSpan = input.AsSpan(); + + Assert.Equal(expectedResult, TypeNameParserHelpers.TrimStart(inputSpan).ToString()); + } + + [Theory] + [InlineData("A.B.C", true, null, 5, 0)] + [InlineData("A+B`1+C1`2+DD2`3+E", true, new int[] { 1, 3, 4, 5 }, 18, 6)] + // TODO adsitnik: add escaping handling and more test cases + public void TryGetTypeNameInfoGetsAllTheInfo(string input, bool expectedResult, int[] expectedNestedNameLengths, + int expectedTotalLength, int expectedGenericArgCount) + { + List? nestedNameLengths = null; + bool result = TypeNameParserHelpers.TryGetTypeNameInfo(input.AsSpan(), ref nestedNameLengths, out int totalLength, out int genericArgCount); + + Assert.Equal(expectedResult, result); + Assert.Equal(expectedNestedNameLengths, nestedNameLengths?.ToArray()); + Assert.Equal(expectedTotalLength, totalLength); + Assert.Equal(expectedGenericArgCount, genericArgCount); + } + + [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/TypeNameParserTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs index 4eaf7ead4852d..0e2e1ec08db41 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs @@ -48,6 +48,15 @@ public void EmptyStringsAreNotAllowed(string input) [InlineData("TooFewGenericArgumentsDoubleSquareBracket'3[a,b]")] [InlineData("TooFewGenericArgumentsDoubleSquareBracketTwoDigits'10[[1],[2],[3],[4],[5],[6],[7],[8],[9]]")] [InlineData("TooFewGenericArgumentsSingleSquareBracketTwoDigits'10[1,2,3,4,5,6,7,8,9]")] + [InlineData("`1")] // back tick as first char followed by numbers (short) + [InlineData("`111")] // back tick as first char followed by numbers (longer) + [InlineData("MoreThanMaxArrayLength`2147483592")] + [InlineData("NegativeGenericArgumentCount`-123")] + [InlineData("MoreThanMaxArrayRank[,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,]")] + [InlineData("NonGenericTypeUsingGenericSyntax[[type1], [type2]]")] + [InlineData("NonGenericTypeUsingGenericSyntax[[type1, assembly1], [type2, assembly2]]")] + [InlineData("NonGenericTypeUsingGenericSyntax[type1,type2]")] + [InlineData("NonGenericTypeUsingGenericSyntax[[]]")] public void InvalidTypeNamesAreNotAllowed(string input) { Assert.Throws(() => TypeName.Parse(input.AsSpan())); 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 ad8437ac3fcce..f77bd3926e700 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,8 @@ Link="Common\Microsoft\Win32\SafeHandles\SafeLibraryHandle.cs" /> + @@ -36,6 +38,7 @@ + From eadf9704fad273c2e50f44e8cdeb03cc9a54a164 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Mon, 12 Feb 2024 08:32:45 +0100 Subject: [PATCH 18/48] increase test coverage, improve edge case handling --- .../Reflection/Metadata/TypeNameParser.cs | 82 +++--------- .../Metadata/TypeNameParserHelpers.cs | 61 +++++++++ .../Metadata/TypeNameParserHelpersTests.cs | 26 +++- .../tests/Metadata/TypeNameParserTests.cs | 119 ++++++++++++++++++ 4 files changed, 221 insertions(+), 67 deletions(-) diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs index 722e689934a95..3197d9c03b5ec 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs @@ -148,8 +148,19 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse return null; } - // TODO adsitnik: don't allocate an array that would require reaching the max depth limit - (genericArgs ??= new TypeName[genericArgCount])[genericArgIndex++] = genericArg; + if (genericArgs is null) + { + // Parsing the rest would hit the limit. + // -1 because the first generic arg has been already parsed. + if (maxObservedRecursionCheck + genericArgCount - 1 > _parseOptions.MaxRecursiveDepth) + { + recursiveDepth = _parseOptions.MaxRecursiveDepth; + return null; + } + + genericArgs = new TypeName[genericArgCount]; + } + genericArgs[genericArgIndex++] = genericArg; if (TryStripFirstCharAndTrailingSpaces(ref _inputString, ',')) { @@ -216,7 +227,7 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse { #if SYSTEM_PRIVATE_CORELIB // backward compat: throw FileLoadException for non-empty invalid strings - if (!_throwOnError && _inputString.TrimStart().StartsWith(",")) // TODO: refactor + if (!_throwOnError && _inputString.TrimStart().StartsWith(",")) // TODO adsitnik: refactor { return null; } @@ -270,7 +281,7 @@ private bool TryParseAssemblyName(ref AssemblyName? assemblyName) int assemblyNameLength = (int)Math.Min((uint)_inputString.IndexOf(']'), (uint)_inputString.Length); ReadOnlySpan candidate = _inputString.Slice(0, assemblyNameLength); AssemblyNameParser.AssemblyNameParts parts = default; - // TODO: make sure the parsing below is safe for untrusted input + // TODO adsitnik: make sure the parsing below is safe for untrusted input if (!AssemblyNameParser.TryParse(candidate, ref parts)) { return false; @@ -280,7 +291,7 @@ private bool TryParseAssemblyName(ref AssemblyName? assemblyName) assemblyName = new(); assemblyName.Init(parts); #else - // TODO: fix the perf and avoid doing it twice (missing public ctors for System.Reflection.Metadata) + // TODO adsitnik: fix the perf and avoid doing it twice (missing public ctors for System.Reflection.Metadata) assemblyName = new(candidate.ToString()); #endif _inputString = _inputString.Slice(assemblyNameLength); @@ -320,66 +331,5 @@ private bool TryDive(ref int depth) depth++; return true; } - - private 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 = TypeNameParserHelpers.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; - } } } diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs index 2a17bd6ab5ee6..1208f48dc9e1b 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs @@ -268,6 +268,67 @@ internal static bool TryGetTypeNameInfo(ReadOnlySpan input, ref List? 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 = TypeNameParserHelpers.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) diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs index 3cf5c95c41a0c..600d7dc6e98e6 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs @@ -143,12 +143,36 @@ public void TryGetTypeNameInfoGetsAllTheInfo(string input, bool expectedResult, Assert.Equal(expectedGenericArgCount, genericArgCount); } + [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) + public void TryStripFirstCharAndTrailingSpacesWorksAsExpected( + string input, char argument, bool expectedResult, string expectedConsumedInput) { ReadOnlySpan inputSpan = input.AsSpan(); diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs index 0e2e1ec08db41..0452c0c49bc8a 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs @@ -57,6 +57,10 @@ public void EmptyStringsAreNotAllowed(string input) [InlineData("NonGenericTypeUsingGenericSyntax[[type1, assembly1], [type2, assembly2]]")] [InlineData("NonGenericTypeUsingGenericSyntax[type1,type2]")] [InlineData("NonGenericTypeUsingGenericSyntax[[]]")] + [InlineData("ExtraCommaAfterFirstGenericArg`1[[type1, assembly1],]")] + [InlineData("MissingClosingSquareBrackets`1[[type1, assembly1")] // missing ]] + [InlineData("MissingClosingSquareBracket`1[[type1, assembly1]")] // missing ] + [InlineData("CantMakeByRefToByRef&&")] public void InvalidTypeNamesAreNotAllowed(string input) { Assert.Throws(() => TypeName.Parse(input.AsSpan())); @@ -128,6 +132,121 @@ static byte FromHexChar(char hex) } } + [Theory] + [InlineData(10, "*")] // pointer to pointer + [InlineData(10, "[]")] // array of arrays + [InlineData(100, "*")] + [InlineData(100, "[]")] + public void MaxRecursiveDepthIsRespected_TooManyDecorators(int maxDepth, string decorator) + { + TypeNameParserOptions options = new() + { + MaxRecursiveDepth = 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); + ValidateUnderlyingType(maxDepth, parsed, decorator); + + Assert.True(TypeName.TryParse(notTooMany.AsSpan(), out parsed, options)); + ValidateUnderlyingType(maxDepth, parsed, decorator); + + static void ValidateUnderlyingType(int maxDepth, TypeName parsed, string decorator) + { + for (int i = 0; i < maxDepth - 1; i++) + { + Assert.Equal(decorator == "*", parsed.IsUnmanagedPointerType); + Assert.Equal(decorator == "[]", parsed.IsSzArrayType); + Assert.False(parsed.IsConstructedGenericType); + + parsed = parsed.UnderlyingType; + } + } + } + + [Theory] + [InlineData(10)] + [InlineData(100)] + public void MaxRecursiveDepthIsRespected_TooDeepGenerics(int maxDepth) + { + TypeNameParserOptions options = new() + { + MaxRecursiveDepth = 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 MaxRecursiveDepthIsRespected_TooManyGenericArguments(int maxDepth) + { + TypeNameParserOptions options = new() + { + MaxRecursiveDepth = 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); + TypeName[] 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[] From abf7543f8d6f29a8162dfdf1d38bd1dbc766d546 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Mon, 12 Feb 2024 22:20:00 +0100 Subject: [PATCH 19/48] sample SerializationBinder that uses the new APIs --- .../tests/Metadata/TypeNameParserSamples.cs | 216 ++++++++++++++++++ .../System.Reflection.Metadata.Tests.csproj | 1 + 2 files changed, 217 insertions(+) create mode 100644 src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs 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 0000000000000..ca0b471bda73e --- /dev/null +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs @@ -0,0 +1,216 @@ +// 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.IO; +using System.Linq; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters.Binary; +using Xunit; + +namespace System.Reflection.Metadata.Tests +{ + public class TypeNameParserSamples + { + [Fact] + public void CanImplementSerializationBinder() + { + SerializableClass parent = new() + { + Integer = 1, + Text = "parent", + Dates = new List() + { + DateTime.Parse("02/06/2024") + } + }; + SerializableClass child = new() + { + Integer = 2, + Text = "child" + }; + parent.Array = new SerializableClass[] { child }; + + SampleSerializationBinder binder = new( + allowedCustomElementalTypes: new HashSet(new Type[] + { + typeof(SerializableClass), + })); + + BinaryFormatter bf = new() + { + Binder = binder + }; + using MemoryStream bfStream = new(); + bf.Serialize(bfStream, parent); + bfStream.Position = 0; + + SerializableClass deserialized = (SerializableClass)bf.Deserialize(bfStream); + + Assert.Equal(parent.Integer, deserialized.Integer); + Assert.Equal(parent.Text, deserialized.Text); + Assert.Equal(parent.Dates.Count, deserialized.Dates.Count); + for (int i = 0; i < deserialized.Dates.Count; i++) + { + Assert.Equal(parent.Dates[i], deserialized.Dates[i]); + } + Assert.Equal(parent.Array.Length, deserialized.Array.Length); + for (int i = 0; i < deserialized.Array.Length; i++) + { + Assert.Equal(parent.Array[i].Integer, deserialized.Array[i].Integer); + Assert.Equal(parent.Array[i].Text, deserialized.Array[i].Text); + } + } + + internal sealed class SampleSerializationBinder : SerializationBinder + { + private static readonly TypeNameParserOptions _options = new() + { + AllowFullyQualifiedName = false // we parse only type names + }; + + // we could use Frozen collections here! + private readonly static Dictionary _alwaysAllowedElementalTypes = 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) }, + }; + + private static readonly Dictionary _alwaysAllowedOpenGenericTypes = new() + { + { typeof(Nullable).FullName, typeof(Nullable) }, + { typeof(List<>).FullName, typeof(List<>) }, + }; + + private readonly Dictionary? _allowedCustomElementalTypes, _allowedCustomOpenGenericTypes; + + public SampleSerializationBinder( + HashSet? allowedCustomElementalTypes, + HashSet? allowedCustomOpenGenericTypes = null) + { + if (allowedCustomElementalTypes is not null) + { + foreach (var type in allowedCustomElementalTypes) + { + if (type.IsGenericType || type.IsByRef || type.IsPointer || type.IsArray) + { + throw new ArgumentException($"{type.FullName} is not an elemental type"); + } + } + } + + if (allowedCustomOpenGenericTypes is not null) + { + foreach (var type in allowedCustomOpenGenericTypes) + { + if (!type.IsGenericTypeDefinition || type.IsByRef || type.IsPointer || type.IsArray) + { + throw new ArgumentException($"{type.FullName} is not an open generic type"); + } + } + } + + _allowedCustomElementalTypes = allowedCustomElementalTypes?.ToDictionary(type => type.FullName); + _allowedCustomOpenGenericTypes = allowedCustomOpenGenericTypes?.ToDictionary(type => type.FullName); + } + + public override Type? BindToType(string assemblyName, string typeName) + { + // fast path for common primitive types like int, bool and string + if (_alwaysAllowedElementalTypes.TryGetValue(typeName, out Type type)) + { + return type; + } + + 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.IsElementalType) // not a pointer, generic, array or managed reference + { + if (TryGetElementalTypeFromFullName(parsed.FullName, out type)) + { + return type; + } + + throw new ArgumentException($"{parsed.FullName} is not on the allow list."); + } + else if (parsed.IsArray) + { + TypeName arrayElementType = parsed.UnderlyingType; + if (TryGetElementalTypeFromFullName(arrayElementType.FullName, out type)) + { + return arrayElementType.IsSzArrayType + ? type.MakeArrayType() + : type.MakeArrayType(parsed.GetArrayRank()); + } + + throw new ArgumentException($"{parsed.FullName} (array) is not on the allow list."); + } + else if (parsed.IsConstructedGenericType) + { + TypeName genericTypeDefinition = parsed.UnderlyingType; + if (TryGetOpenGenericTypeFromFullName(genericTypeDefinition.FullName, out type)) + { + TypeName[] genericArguments = parsed.GetGenericArguments(); + Type[] types = new Type[genericArguments.Length]; + for (int i = 0; i < genericArguments.Length; i++ ) + { + if (!TryGetElementalTypeFromFullName(genericArguments[i].FullName, out types[i])) + { + throw new ArgumentException($"{genericArguments[i].FullName} (generic argument) is not on the allow list."); + } + } + return type.MakeGenericType(types); + } + throw new ArgumentException($"{parsed.FullName} (generic) is not on the allow list."); + } + + throw new ArgumentException($"{parsed.FullName} is not on the allow list."); + } + + private bool TryGetElementalTypeFromFullName(string fullName, out Type? type) + { + type = null; + + return (_alwaysAllowedElementalTypes is not null && _alwaysAllowedElementalTypes.TryGetValue(fullName, out type)) + || (_allowedCustomElementalTypes is not null && _allowedCustomElementalTypes.TryGetValue(fullName, out type)); + } + + private bool TryGetOpenGenericTypeFromFullName(string fullName, out Type? type) + { + type = null; + + return (_alwaysAllowedOpenGenericTypes is not null && _alwaysAllowedOpenGenericTypes.TryGetValue(fullName, out type)) + || (_allowedCustomOpenGenericTypes is not null && _allowedCustomOpenGenericTypes.TryGetValue(fullName, out type)); + } + } + + [Serializable] + public class SerializableClass + { + public int Integer { get; set; } + public string Text { get; set; } + public List Dates { get; set; } + public SerializableClass[] Array { get; set; } + } + } +} 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 f77bd3926e700..88f300b3d30d9 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 @@ -40,6 +40,7 @@ + From 281c4f32e73591b03d805a0809626efda19bd4a2 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 13 Feb 2024 17:19:47 +0100 Subject: [PATCH 20/48] cover more serialization binder scenarios with the tests to ensure the API is complete --- .../tests/Metadata/TypeNameParserSamples.cs | 306 ++++++++++-------- 1 file changed, 170 insertions(+), 136 deletions(-) diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs index ca0b471bda73e..288a8776803bf 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs @@ -2,6 +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.Linq; using System.Runtime.Serialization; @@ -12,65 +13,12 @@ namespace System.Reflection.Metadata.Tests { public class TypeNameParserSamples { - [Fact] - public void CanImplementSerializationBinder() - { - SerializableClass parent = new() - { - Integer = 1, - Text = "parent", - Dates = new List() - { - DateTime.Parse("02/06/2024") - } - }; - SerializableClass child = new() - { - Integer = 2, - Text = "child" - }; - parent.Array = new SerializableClass[] { child }; - - SampleSerializationBinder binder = new( - allowedCustomElementalTypes: new HashSet(new Type[] - { - typeof(SerializableClass), - })); - - BinaryFormatter bf = new() - { - Binder = binder - }; - using MemoryStream bfStream = new(); - bf.Serialize(bfStream, parent); - bfStream.Position = 0; - - SerializableClass deserialized = (SerializableClass)bf.Deserialize(bfStream); - - Assert.Equal(parent.Integer, deserialized.Integer); - Assert.Equal(parent.Text, deserialized.Text); - Assert.Equal(parent.Dates.Count, deserialized.Dates.Count); - for (int i = 0; i < deserialized.Dates.Count; i++) - { - Assert.Equal(parent.Dates[i], deserialized.Dates[i]); - } - Assert.Equal(parent.Array.Length, deserialized.Array.Length); - for (int i = 0; i < deserialized.Array.Length; i++) - { - Assert.Equal(parent.Array[i].Integer, deserialized.Array[i].Integer); - Assert.Equal(parent.Array[i].Text, deserialized.Array[i].Text); - } - } - internal sealed class SampleSerializationBinder : SerializationBinder { - private static readonly TypeNameParserOptions _options = new() - { - AllowFullyQualifiedName = false // we parse only type names - }; + private static TypeNameParserOptions _options; - // we could use Frozen collections here! - private readonly static Dictionary _alwaysAllowedElementalTypes = new() + // we could use Frozen collections here ;) + private readonly static Dictionary _alwaysAllowed = new() { { typeof(string).FullName, typeof(string) }, { typeof(int).FullName, typeof(int) }, @@ -90,127 +38,213 @@ internal sealed class SampleSerializationBinder : SerializationBinder { typeof(Uri).FullName, typeof(Uri) }, { typeof(DateTimeOffset).FullName, typeof(DateTimeOffset) }, { typeof(Version).FullName, typeof(Version) }, + { typeof(Nullable).FullName, typeof(Nullable) }, // Nullable is generic! }; - private static readonly Dictionary _alwaysAllowedOpenGenericTypes = new() - { - { typeof(Nullable).FullName, typeof(Nullable) }, - { typeof(List<>).FullName, typeof(List<>) }, - }; - - private readonly Dictionary? _allowedCustomElementalTypes, _allowedCustomOpenGenericTypes; - - public SampleSerializationBinder( - HashSet? allowedCustomElementalTypes, - HashSet? allowedCustomOpenGenericTypes = null) - { - if (allowedCustomElementalTypes is not null) - { - foreach (var type in allowedCustomElementalTypes) - { - if (type.IsGenericType || type.IsByRef || type.IsPointer || type.IsArray) - { - throw new ArgumentException($"{type.FullName} is not an elemental type"); - } - } - } - - if (allowedCustomOpenGenericTypes is not null) - { - foreach (var type in allowedCustomOpenGenericTypes) - { - if (!type.IsGenericTypeDefinition || type.IsByRef || type.IsPointer || type.IsArray) - { - throw new ArgumentException($"{type.FullName} is not an open generic type"); - } - } - } + private readonly Dictionary? _userDefined; - _allowedCustomElementalTypes = allowedCustomElementalTypes?.ToDictionary(type => type.FullName); - _allowedCustomOpenGenericTypes = allowedCustomOpenGenericTypes?.ToDictionary(type => type.FullName); - } + public SampleSerializationBinder(Type[]? allowedTypes = null) + => _userDefined = allowedTypes?.ToDictionary(type => type.FullName); public override Type? BindToType(string assemblyName, string typeName) { - // fast path for common primitive types like int, bool and string - if (_alwaysAllowedElementalTypes.TryGetValue(typeName, out Type type)) + // 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() // there is no need for lazy initialization, I just wanted to have everything important in one method + { + // We parse only type names, because 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 + AllowFullyQualifiedName = false, + // 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. + MaxRecursiveDepth = 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.IsElementalType) // not a pointer, generic, array or managed reference - { - if (TryGetElementalTypeFromFullName(parsed.FullName, out type)) - { - return type; - } + return GetTypeFromParsedTypeName(parsed); + } - throw new ArgumentException($"{parsed.FullName} is not on the allow list."); + private Type? GetTypeFromParsedTypeName(TypeName parsed) + { + if (TryGetTypeFromFullName(parsed.FullName, out Type type)) + { + return type; } else if (parsed.IsArray) { - TypeName arrayElementType = parsed.UnderlyingType; - if (TryGetElementalTypeFromFullName(arrayElementType.FullName, out type)) - { - return arrayElementType.IsSzArrayType - ? type.MakeArrayType() - : type.MakeArrayType(parsed.GetArrayRank()); - } + TypeName arrayElementTypeName = parsed.UnderlyingType; // equivalent of type.GetElementType() + Type arrayElementType = GetTypeFromParsedTypeName(arrayElementTypeName); // recursive call allows for creating arrays of arrays etc - throw new ArgumentException($"{parsed.FullName} (array) is not on the allow list."); + return parsed.IsSzArrayType + ? arrayElementType.MakeArrayType() + : arrayElementType.MakeArrayType(parsed.GetArrayRank()); } else if (parsed.IsConstructedGenericType) { - TypeName genericTypeDefinition = parsed.UnderlyingType; - if (TryGetOpenGenericTypeFromFullName(genericTypeDefinition.FullName, out type)) + TypeName genericTypeDefinitionName = parsed.UnderlyingType; // equivalent of type.GetGenericTypeDefinition() + Type genericTypeDefinition = GetTypeFromParsedTypeName(genericTypeDefinitionName); + Debug.Assert(genericTypeDefinition.IsGenericTypeDefinition); + + TypeName[] genericArgs = parsed.GetGenericArguments(); + Type[] typeArguments = new Type[genericArgs.Length]; + for (int i = 0; i < genericArgs.Length; i++) { - TypeName[] genericArguments = parsed.GetGenericArguments(); - Type[] types = new Type[genericArguments.Length]; - for (int i = 0; i < genericArguments.Length; i++ ) - { - if (!TryGetElementalTypeFromFullName(genericArguments[i].FullName, out types[i])) - { - throw new ArgumentException($"{genericArguments[i].FullName} (generic argument) is not on the allow list."); - } - } - return type.MakeGenericType(types); + typeArguments[i] = GetTypeFromParsedTypeName(genericArgs[i]); // recursive call allows for generics of generics like "List" } - throw new ArgumentException($"{parsed.FullName} (generic) is not on the allow list."); + return genericTypeDefinition.MakeGenericType(typeArguments); } throw new ArgumentException($"{parsed.FullName} is not on the allow list."); } - private bool TryGetElementalTypeFromFullName(string fullName, out Type? type) + 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; } + } + + [Fact] + 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<>) // TODO adsitnik: make it work for List too (currently does not work due to type forwarding) + ]); + + 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); + } + } + + [Fact] + public void CanDeserializeDictionaryUsingNonPublicComparerType() + { + Dictionary dictionary = new(StringComparer.CurrentCulture) { - type = null; + { "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); + } - return (_alwaysAllowedElementalTypes is not null && _alwaysAllowedElementalTypes.TryGetValue(fullName, out type)) - || (_allowedCustomElementalTypes is not null && _allowedCustomElementalTypes.TryGetValue(fullName, out type)); + [Fact] + public void CanDeserializeArraysOfArrays() + { + int[][] arrayOfArrays = new int[10][]; + for (int i = 0; i < arrayOfArrays.Length; i++) + { + arrayOfArrays[i] = Enumerable.Repeat(i, 10).ToArray(); } - private bool TryGetOpenGenericTypeFromFullName(string fullName, out Type? type) + 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]); + } + } + + [Fact] + public void CanDeserializeListOfListOfInt() + { + List> listOfListOfInts = new(10); + for (int i = 0; i < listOfListOfInts.Count; i++) { - type = null; + listOfListOfInts[i] = Enumerable.Repeat(i, 10).ToList(); + } - return (_alwaysAllowedOpenGenericTypes is not null && _alwaysAllowedOpenGenericTypes.TryGetValue(fullName, out type)) - || (_allowedCustomOpenGenericTypes is not null && _allowedCustomOpenGenericTypes.TryGetValue(fullName, out type)); + 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]); } } - [Serializable] - public class SerializableClass + static T SerializeDeserialize(T instance, SerializationBinder binder) { - public int Integer { get; set; } - public string Text { get; set; } - public List Dates { get; set; } - public SerializableClass[] Array { get; set; } + using MemoryStream bfStream = new(); + BinaryFormatter bf = new() + { + Binder = binder + }; + + bf.Serialize(bfStream, instance); + bfStream.Position = 0; + + return (T)bf.Deserialize(bfStream); } } } From dc58cce04bed829637f5fd5fab4e7a3bc63b9c4b Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Mon, 19 Feb 2024 16:54:17 +0100 Subject: [PATCH 21/48] strict mode parsing: type names --- .../Reflection/Metadata/TypeNameParser.cs | 3 +- .../Metadata/TypeNameParserHelpers.cs | 168 ++++++++++++++++++ .../Metadata/TypeNameParserOptions.cs | 41 ++--- .../ref/System.Reflection.Metadata.cs | 4 +- .../Metadata/TypeNameParserHelpersTests.cs | 46 +++++ .../tests/Metadata/TypeNameParserTests.cs | 36 ---- 6 files changed, 229 insertions(+), 69 deletions(-) diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs index 3197d9c03b5ec..6a0b5674c4fe2 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs @@ -97,7 +97,8 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse } ReadOnlySpan fullTypeName = _inputString.Slice(0, fullTypeNameLength); - if (!_parseOptions.ValidateIdentifier(fullTypeName, _throwOnError)) + int invalidCharIndex = GetIndexOfFirstInvalidCharacter(fullTypeName, _parseOptions.StrictValidation); + if (invalidCharIndex >= 0) { return null; } diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs index 1208f48dc9e1b..e3c47d8a656b1 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs @@ -153,6 +153,174 @@ internal static int GetFullTypeNameLength(ReadOnlySpan input, out bool isN return (int)Math.Min((uint)offset, (uint)input.Length); } + internal static int GetIndexOfFirstInvalidCharacter(ReadOnlySpan input, bool strictMode) + { + if (input.IsEmpty) + { + return 0; + } + + if (strictMode) + { + ReadOnlySpan allowedAsciiCharsMap = GetAsciiCharsAllowMap(); + Debug.Assert(allowedAsciiCharsMap.Length == 128); + + for (int i = 0; i < input.Length; i++) + { + char c = input[i]; + if (c < (uint)allowedAsciiCharsMap.Length) + { + // ASCII - fast track + if (!allowedAsciiCharsMap[c]) + { + return i; + } + } + else + { + if (IsControl(c) || IsWhiteSpace(c)) + { + return i; + } + } + } + } + + return -1; + + static ReadOnlySpan GetAsciiCharsAllowMap() => + [ + false, // U+0000 (NUL) + false, // U+0001 (SOH) + false, // U+0002 (STX) + false, // U+0003 (ETX) + false, // U+0004 (EOT) + false, // U+0005 (ENQ) + false, // U+0006 (ACK) + false, // U+0007 (BEL) + false, // U+0008 (BS) + false, // U+0009 (TAB) + false, // U+000A (LF) + false, // U+000B (VT) + false, // U+000C (FF) + false, // U+000D (CR) + false, // U+000E (SO) + false, // U+000F (SI) + false, // U+0010 (DLE) + false, // U+0011 (DC1) + false, // U+0012 (DC2) + false, // U+0013 (DC3) + false, // U+0014 (DC4) + false, // U+0015 (NAK) + false, // U+0016 (SYN) + false, // U+0017 (ETB) + false, // U+0018 (CAN) + false, // U+0019 (EM) + false, // U+001A (SUB) + false, // U+001B (ESC) + false, // U+001C (FS) + false, // U+001D (GS) + false, // U+001E (RS) + false, // U+001F (US) + false, // U+0020 ' ' + true, // U+0021 '!' + false, // U+0022 '"' + true, // U+0023 '#' + true, // U+0024 '$' + true, // U+0025 '%' + false, // U+0026 '&' + false, // U+0027 ''' + true, // U+0028 '(' + true, // U+0029 ')' + false, // U+002A '*' + true, // U+002B '+' + false, // U+002C ',' + true, // U+002D '-' + true, // U+002E '.' + false, // U+002F '/' + true, // U+0030 '0' + true, // U+0031 '1' + true, // U+0032 '2' + true, // U+0033 '3' + true, // U+0034 '4' + true, // U+0035 '5' + true, // U+0036 '6' + true, // U+0037 '7' + true, // U+0038 '8' + true, // U+0039 '9' + false, // U+003A ':' + false, // U+003B ';' + true, // U+003C '<' + true, // U+003D '=' + true, // U+003E '>' + false, // U+003F '?' + true, // U+0040 '@' + true, // U+0041 'A' + true, // U+0042 'B' + true, // U+0043 'C' + true, // U+0044 'D' + true, // U+0045 'E' + true, // U+0046 'F' + true, // U+0047 'G' + true, // U+0048 'H' + true, // U+0049 'I' + true, // U+004A 'J' + true, // U+004B 'K' + true, // U+004C 'L' + true, // U+004D 'M' + true, // U+004E 'N' + true, // U+004F 'O' + true, // U+0050 'P' + true, // U+0051 'Q' + true, // U+0052 'R' + true, // U+0053 'S' + true, // U+0054 'T' + true, // U+0055 'U' + true, // U+0056 'V' + true, // U+0057 'W' + true, // U+0058 'X' + true, // U+0059 'Y' + true, // U+005A 'Z' + false, // U+005B '[' + false, // U+005C '\' + false, // U+005D ']' + true, // U+005E '^' + true, // U+005F '_' + true, // U+0060 '`' + true, // U+0061 'a' + true, // U+0062 'b' + true, // U+0063 'c' + true, // U+0064 'd' + true, // U+0065 'e' + true, // U+0066 'f' + true, // U+0067 'g' + true, // U+0068 'h' + true, // U+0069 'i' + true, // U+006A 'j' + true, // U+006B 'k' + true, // U+006C 'l' + true, // U+006D 'm' + true, // U+006E 'n' + true, // U+006F 'o' + true, // U+0070 'p' + true, // U+0071 'q' + true, // U+0072 'r' + true, // U+0073 's' + true, // U+0074 't' + true, // U+0075 'u' + true, // U+0076 'v' + true, // U+0077 'w' + true, // U+0078 'x' + true, // U+0079 'y' + true, // U+007A 'z' + true, // U+007B '{' + true, // U+007C '|' + true, // U+007D '}' + true, // U+007E '~' + false, // U+007F (DEL) + ]; + } + internal static ReadOnlySpan GetName(ReadOnlySpan fullName) { #if NET8_0_OR_GREATER diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs index 4f4bf728da0b5..078cc3c5af043 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs @@ -1,16 +1,14 @@ // 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; - namespace System.Reflection.Metadata { #if SYSTEM_PRIVATE_CORELIB - internal sealed + internal #else public #endif - class TypeNameParserOptions + sealed class TypeNameParserOptions { private int _maxRecursiveDepth = int.MaxValue; @@ -23,37 +21,20 @@ public int MaxRecursiveDepth { #if NET8_0_OR_GREATER ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(value, 0, nameof(value)); +#else + if (value <= 0) + { + throw new ArgumentOutOfRangeException(paramName: nameof(value)); + } #endif _maxRecursiveDepth = value; } } - internal bool AllowSpacesOnly { get; set; } - - internal bool AllowEscaping { get; set; } - - internal bool StrictValidation { get; set; } - -#if SYSTEM_PRIVATE_CORELIB - internal -#else - public virtual -#endif - bool ValidateIdentifier(ReadOnlySpan candidate, bool throwOnError) - { - Debug.Assert(!StrictValidation, "TODO (ignoring the compiler warning)"); - - if (candidate.IsEmpty) - { - if (throwOnError) - { - throw new ArgumentException("TODO"); - } - return false; - } - - return true; - } + /// + /// Extends ECMA-335 standard limitations with a set of opinionated rules based on most up-to-date security knowledge. + /// + public bool StrictValidation { get; set; } } } 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 c6a8d2865e24f..2ece60ea39966 100644 --- a/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs +++ b/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs @@ -2430,12 +2430,12 @@ internal TypeName() { } public int GetArrayRank() { throw null; } public System.Reflection.Metadata.TypeName[] GetGenericArguments() { throw null; } } - public partial class TypeNameParserOptions + public sealed partial class TypeNameParserOptions { public TypeNameParserOptions() { } public bool AllowFullyQualifiedName { get { throw null; } set { } } public int MaxRecursiveDepth { get { throw null; } set { } } - public virtual bool ValidateIdentifier(System.ReadOnlySpan candidate, bool throwOnError) { throw null; } + public bool StrictValidation { get; set; } } public readonly partial struct TypeReference { diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs index 600d7dc6e98e6..f7af885f27be4 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs @@ -69,6 +69,52 @@ public void GetFullTypeNameLengthReturnsExpectedValue(string input, int expected Assert.Equal(expectedIsNested, isNested); } + [Theory] + [InlineData("", 0)] + [InlineData("\0NullCharacterIsNotAllowed", 0)] + [InlineData("Null\0CharacterIsNotAllowed", 4)] + [InlineData("NullCharacterIsNotAllowed\0", 25)] + [InlineData("\bBackspaceIsNotAllowed", 0)] + [InlineData("EscapingIsNotAllowed\\", 20)] + [InlineData("EscapingIsNotAllowed\\\\", 20)] + [InlineData("EscapingIsNotAllowed\\*", 20)] + [InlineData("EscapingIsNotAllowed\\&", 20)] + [InlineData("EscapingIsNotAllowed\\+", 20)] + [InlineData("EscapingIsNotAllowed\\[", 20)] + [InlineData("EscapingIsNotAllowed\\]", 20)] + [InlineData("Slash/IsNotAllowed", 5)] + [InlineData("Whitespaces AreNotAllowed", 11)] + [InlineData("WhitespacesAre\tNotAllowed", 14)] + [InlineData("WhitespacesAreNot\r\nAllowed", 17)] + [InlineData("Question?MarkIsNotAllowed", 8)] + [InlineData("Quotes\"AreNotAllowed", 6)] + [InlineData("Quote'IsNotAllowed", 5)] + [InlineData("abcdefghijklmnopqrstuvwxyz", -1)] + [InlineData("ABCDEFGHIJKLMNOPQRSTUVWXYZ", -1)] + [InlineData("0123456789", -1)] + [InlineData("!@#$%^()-_={}|<>.~", -1)] + [InlineData("BacktickIsOk`1", -1)] + public void GetIndexOfFirstInvalidCharacter_ReturnsFirstInvalidCharacter(string input, int expected) + { + Assert.Equal(expected, TypeNameParserHelpers.GetIndexOfFirstInvalidCharacter(input.AsSpan(), strictMode: true)); + + TypeNameParserOptions strictOptions = new() + { + StrictValidation = true + }; + + if (expected >= 0) + { + Assert.False(TypeName.TryParse(input.AsSpan(), out _, strictOptions)); + Assert.Throws(() => TypeName.Parse(input.AsSpan(), strictOptions)); + } + else + { + Assert.True(TypeName.TryParse(input.AsSpan(), out TypeName parsed, strictOptions)); + Assert.Equal(input, parsed.FullName); + } + } + [Theory] [InlineData(TypeNameParserHelpers.SZArray, "[]")] [InlineData(TypeNameParserHelpers.Pointer, "*")] diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs index 0452c0c49bc8a..1bf2a0a372d7a 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Text; using Xunit; namespace System.Reflection.Metadata.Tests @@ -73,15 +72,6 @@ public void InvalidTypeNamesAreNotAllowed(string input) public void UnicodeCharactersAreAllowedByDefault(string input, string expectedFullName) => Assert.Equal(expectedFullName, TypeName.Parse(input.AsSpan()).FullName); - [Theory] - [InlineData("Namespace.Kość")] - public void UsersCanCustomizeIdentifierValidation(string input) - { - Assert.Throws(() => TypeName.Parse(input.AsSpan(), new NonAsciiNotAllowed())); - - Assert.False(TypeName.TryParse(input.AsSpan(), out _, new NonAsciiNotAllowed())); - } - public static IEnumerable TypeNamesWithAssemblyNames() { yield return new object[] @@ -569,31 +559,5 @@ public class NestedNonGeneric_3 { } } } } - - internal sealed class NonAsciiNotAllowed : TypeNameParserOptions - { - public override bool ValidateIdentifier(ReadOnlySpan candidate, bool throwOnError) - { - if (!base.ValidateIdentifier(candidate, throwOnError)) - { - return false; - } - -#if NET8_0_OR_GREATER - if (!Ascii.IsValid(candidate)) -#else - if (candidate.ToArray().Any(c => c >= 128)) -#endif - { - if (throwOnError) - { - throw new ArgumentException("Non ASCII char found"); - } - return false; - } - return true; - } - } - } } From b9479775c3c6d1014e1f315ec443aef5aa9e9f58 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 20 Feb 2024 08:03:31 +0100 Subject: [PATCH 22/48] strict mode parsing: assembly names --- .../System/Reflection/AssemblyNameParser.cs | 83 ++-- .../AssemblyNameParser.netstandard.cs | 72 +++ .../Reflection/Metadata/TypeNameParser.cs | 26 +- .../Metadata/TypeNameParserHelpers.cs | 434 ++++++++++++------ .../Metadata/TypeNameParserOptions.cs | 4 + .../src/System.Reflection.Metadata.csproj | 3 + .../Metadata/TypeNameParserHelpersTests.cs | 83 +++- .../tests/Metadata/TypeNameParserTests.cs | 97 ++-- 8 files changed, 559 insertions(+), 243 deletions(-) create mode 100644 src/libraries/Common/src/System/Reflection/AssemblyNameParser.netstandard.cs diff --git a/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs b/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs index 0f6bd64bb7b0d..e8891ed17a59f 100644 --- a/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs @@ -15,7 +15,7 @@ namespace System.Reflection // // Parses an assembly name. // - internal ref struct AssemblyNameParser + internal ref partial struct AssemblyNameParser { public readonly struct AssemblyNameParts { @@ -55,9 +55,10 @@ private enum AttributeKind } private readonly ReadOnlySpan _input; + private readonly bool _strict; private int _index; - private AssemblyNameParser(ReadOnlySpan input) + private AssemblyNameParser(ReadOnlySpan input, bool strict = false) { #if SYSTEM_PRIVATE_CORELIB if (input.Length == 0) @@ -67,6 +68,7 @@ private AssemblyNameParser(ReadOnlySpan input) #endif _input = input; + _strict = strict; _index = 0; } @@ -85,9 +87,9 @@ public static AssemblyNameParts Parse(ReadOnlySpan name) } #endif - internal static bool TryParse(ReadOnlySpan name, ref AssemblyNameParts parts) + internal static bool TryParse(ReadOnlySpan name, bool strict, ref AssemblyNameParts parts) { - AssemblyNameParser parser = new(name); + AssemblyNameParser parser = new(name, strict); return parser.TryParse(ref parts); } @@ -133,7 +135,7 @@ private bool TryParse(ref AssemblyNameParts result) if (attributeName == string.Empty) return false; - if (attributeName.Equals("Version", StringComparison.OrdinalIgnoreCase)) + if (IsAttribute(attributeName, "Version")) { if (!TryRecordNewSeen(ref alreadySeen, AttributeKind.Version)) { @@ -144,42 +146,46 @@ private bool TryParse(ref AssemblyNameParts result) return false; } } - - if (attributeName.Equals("Culture", StringComparison.OrdinalIgnoreCase)) + else if (IsAttribute(attributeName, "Culture")) { if (!TryRecordNewSeen(ref alreadySeen, AttributeKind.Culture)) { return false; } - cultureName = ParseCulture(attributeValue); + if (!TryParseCulture(attributeValue, out cultureName)) + { + return false; + } } - - if (attributeName.Equals("PublicKey", StringComparison.OrdinalIgnoreCase)) + else if (IsAttribute(attributeName, "PublicKeyToken")) { if (!TryRecordNewSeen(ref alreadySeen, AttributeKind.PublicKeyOrToken)) { return false; } - if (!TryParsePKT(attributeValue, isToken: false, ref pkt)) + if (!TryParsePKT(attributeValue, isToken: true, ref pkt)) { return false; } - flags |= AssemblyNameFlags.PublicKey; } - - if (attributeName.Equals("PublicKeyToken", StringComparison.OrdinalIgnoreCase)) + else if (_strict) + { + // it's either unrecognized or not on the allow list (Version, Culture and PublicKeyToken) + return false; + } + else if (IsAttribute(attributeName, "PublicKey")) { if (!TryRecordNewSeen(ref alreadySeen, AttributeKind.PublicKeyOrToken)) { return false; } - if (!TryParsePKT(attributeValue, isToken: true, ref pkt)) + if (!TryParsePKT(attributeValue, isToken: false, ref pkt)) { return false; } + flags |= AssemblyNameFlags.PublicKey; } - - if (attributeName.Equals("ProcessorArchitecture", StringComparison.OrdinalIgnoreCase)) + else if (IsAttribute(attributeName, "ProcessorArchitecture")) { if (!TryRecordNewSeen(ref alreadySeen, AttributeKind.ProcessorArchitecture)) { @@ -191,8 +197,7 @@ private bool TryParse(ref AssemblyNameParts result) } flags |= (AssemblyNameFlags)(((int)arch) << 4); } - - if (attributeName.Equals("Retargetable", StringComparison.OrdinalIgnoreCase)) + else if (IsAttribute(attributeName, "Retargetable")) { if (!TryRecordNewSeen(ref alreadySeen, AttributeKind.Retargetable)) { @@ -212,8 +217,7 @@ private bool TryParse(ref AssemblyNameParts result) return false; } } - - if (attributeName.Equals("ContentType", StringComparison.OrdinalIgnoreCase)) + else if (IsAttribute(attributeName, "ContentType")) { if (!TryRecordNewSeen(ref alreadySeen, AttributeKind.ContentType)) { @@ -229,8 +233,11 @@ private bool TryParse(ref AssemblyNameParts result) return false; } } + else + { + // Desktop compat: If we got here, the attribute name is unknown to us. Ignore it. + } - // Desktop compat: If we got here, the attribute name is unknown to us. Ignore it. if (!TryGetNextToken(out _, out token)) { return false; @@ -241,7 +248,10 @@ private bool TryParse(ref AssemblyNameParts result) return true; } - private bool TryParseVersion(string attributeValue, ref Version? version) + private bool IsAttribute(string candidate, string attributeKind) + => candidate.Equals(attributeKind, _strict ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase); + + private static bool TryParseVersion(string attributeValue, ref Version? version) { #if NET8_0_OR_GREATER ReadOnlySpan attributeValueSpan = attributeValue; @@ -290,14 +300,21 @@ private bool TryParseVersion(string attributeValue, ref Version? version) return true; } - private static string ParseCulture(string attributeValue) + private bool TryParseCulture(string attributeValue, out string? result) { if (attributeValue.Equals("Neutral", StringComparison.OrdinalIgnoreCase)) { - return ""; + result = ""; + return true; + } + else if (_strict && !IsPredefinedCulture(attributeValue)) + { + result = null; + return false; } - return attributeValue; + result = attributeValue; + return true; } private static bool TryParsePKT(string attributeValue, bool isToken, ref byte[]? result) @@ -533,5 +550,19 @@ private bool TryGetNextToken(out string tokenString, out Token token) token = Token.String; return true; } + +#if NET8_0_OR_GREATER + private static bool IsPredefinedCulture(string cultureName) + { + try + { + return CultureInfo.GetCultureInfo(cultureName, predefinedOnly: true) is not null; + } + catch (CultureNotFoundException) + { + return false; + } + } +#endif } } diff --git a/src/libraries/Common/src/System/Reflection/AssemblyNameParser.netstandard.cs b/src/libraries/Common/src/System/Reflection/AssemblyNameParser.netstandard.cs new file mode 100644 index 0000000000000..71cffe67b4b5e --- /dev/null +++ b/src/libraries/Common/src/System/Reflection/AssemblyNameParser.netstandard.cs @@ -0,0 +1,72 @@ +// 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.Globalization; + +namespace System.Reflection +{ + internal ref partial struct AssemblyNameParser + { + private static HashSet? _predefinedCultureNames; + private static readonly object _predefinedCultureNamesLock = new object(); + + private static bool IsPredefinedCulture(string cultureName) + { + if (_predefinedCultureNames is null) + { + lock (_predefinedCultureNamesLock) + { + _predefinedCultureNames ??= GetPredefinedCultureNames(); + } + } + + return _predefinedCultureNames.Contains(AnsiToLower(cultureName)); + + static HashSet GetPredefinedCultureNames() + { + HashSet result = new(StringComparer.Ordinal); + foreach (CultureInfo culture in CultureInfo.GetCultures(CultureTypes.AllCultures)) + { + result.Add(AnsiToLower(culture.Name)); + } + return result; + } + + // Like CultureInfo, only maps [A-Z] -> [a-z]. + // All non-ASCII characters are left alone. + static string AnsiToLower(string input) + { + if (input is null) + { + return null; + } + + int idx; + for (idx = 0; idx < input.Length; idx++) + { + if (input[idx] is >= 'A' and <= 'Z') + { + break; + } + } + + if (idx == input.Length) + { + return input; // no characters to change. + } + + char[] chars = input.ToCharArray(); + for (; idx < chars.Length; idx++) + { + char c = chars[idx]; + if (input[idx] is >= 'A' and <= 'Z') + { + chars[idx] = (char)(c | 0x20); + } + } + return new string(chars); + } + } + } +} diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs index 6a0b5674c4fe2..f36f2ab322778 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs @@ -97,7 +97,7 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse } ReadOnlySpan fullTypeName = _inputString.Slice(0, fullTypeNameLength); - int invalidCharIndex = GetIndexOfFirstInvalidCharacter(fullTypeName, _parseOptions.StrictValidation); + int invalidCharIndex = GetIndexOfFirstInvalidTypeNameCharacter(fullTypeName, _parseOptions.StrictValidation); if (invalidCharIndex >= 0) { return null; @@ -282,18 +282,32 @@ private bool TryParseAssemblyName(ref AssemblyName? assemblyName) int assemblyNameLength = (int)Math.Min((uint)_inputString.IndexOf(']'), (uint)_inputString.Length); ReadOnlySpan candidate = _inputString.Slice(0, assemblyNameLength); AssemblyNameParser.AssemblyNameParts parts = default; - // TODO adsitnik: make sure the parsing below is safe for untrusted input - if (!AssemblyNameParser.TryParse(candidate, ref parts)) + + if (GetIndexOfFirstInvalidAssemblyNameCharacter(candidate, _parseOptions.StrictValidation) >= 0 + || !AssemblyNameParser.TryParse(candidate, _parseOptions.StrictValidation, ref parts)) { return false; } -#if SYSTEM_PRIVATE_CORELIB assemblyName = new(); +#if SYSTEM_PRIVATE_CORELIB assemblyName.Init(parts); #else - // TODO adsitnik: fix the perf and avoid doing it twice (missing public ctors for System.Reflection.Metadata) - assemblyName = new(candidate.ToString()); + assemblyName.Name = parts._name; + assemblyName.CultureName = parts._cultureName; + assemblyName.Version = parts._version; + + if (parts._publicKeyOrToken is not null) + { + if ((parts._flags & AssemblyNameFlags.PublicKey) != 0) + { + assemblyName.SetPublicKey(parts._publicKeyOrToken); + } + else + { + assemblyName.SetPublicKeyToken(parts._publicKeyOrToken); + } + } #endif _inputString = _inputString.Slice(assemblyNameLength); return true; diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs index e3c47d8a656b1..8a0501abdfe40 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs @@ -153,171 +153,313 @@ internal static int GetFullTypeNameLength(ReadOnlySpan input, out bool isN return (int)Math.Min((uint)offset, (uint)input.Length); } - internal static int GetIndexOfFirstInvalidCharacter(ReadOnlySpan input, bool strictMode) + private static int GetIndexOfFirstInvalidCharacter(ReadOnlySpan input, bool strictMode, ReadOnlySpan allowedAsciiCharsMap) { if (input.IsEmpty) { return 0; } - - if (strictMode) + else if (!strictMode) { - ReadOnlySpan allowedAsciiCharsMap = GetAsciiCharsAllowMap(); - Debug.Assert(allowedAsciiCharsMap.Length == 128); + return -1; + } + + Debug.Assert(allowedAsciiCharsMap.Length == 128); - for (int i = 0; i < input.Length; i++) + for (int i = 0; i < input.Length; i++) + { + char c = input[i]; + if (c < (uint)allowedAsciiCharsMap.Length) { - char c = input[i]; - if (c < (uint)allowedAsciiCharsMap.Length) + // ASCII - fast track + if (!allowedAsciiCharsMap[c]) { - // ASCII - fast track - if (!allowedAsciiCharsMap[c]) - { - return i; - } + return i; } - else + } + else + { + if (IsControl(c) || IsWhiteSpace(c)) { - if (IsControl(c) || IsWhiteSpace(c)) - { - return i; - } + return i; } } } return -1; + } + + internal static int GetIndexOfFirstInvalidAssemblyNameCharacter(ReadOnlySpan input, bool strictMode) + { + return GetIndexOfFirstInvalidCharacter(input, strictMode, GetAsciiCharsAllowMap()); + + static ReadOnlySpan GetAsciiCharsAllowMap() => + [ + false, // U+0000 (NUL) + false, // U+0001 (SOH) + false, // U+0002 (STX) + false, // U+0003 (ETX) + false, // U+0004 (EOT) + false, // U+0005 (ENQ) + false, // U+0006 (ACK) + false, // U+0007 (BEL) + false, // U+0008 (BS) + false, // U+0009 (TAB) + false, // U+000A (LF) + false, // U+000B (VT) + false, // U+000C (FF) + false, // U+000D (CR) + false, // U+000E (SO) + false, // U+000F (SI) + false, // U+0010 (DLE) + false, // U+0011 (DC1) + false, // U+0012 (DC2) + false, // U+0013 (DC3) + false, // U+0014 (DC4) + false, // U+0015 (NAK) + false, // U+0016 (SYN) + false, // U+0017 (ETB) + false, // U+0018 (CAN) + false, // U+0019 (EM) + false, // U+001A (SUB) + false, // U+001B (ESC) + false, // U+001C (FS) + false, // U+001D (GS) + false, // U+001E (RS) + false, // U+001F (US) + true, // U+0020 ' ' + true, // U+0021 '!' + false, // U+0022 '"' + true, // U+0023 '#' + true, // U+0024 '$' + true, // U+0025 '%' + true, // U+0026 '&' + false, // U+0027 ''' + true, // U+0028 '(' + true, // U+0029 ')' + false, // U+002A '*' + true, // U+002B '+' + true, // U+002C ',' + true, // U+002D '-' + true, // U+002E '.' + false, // U+002F '/' + true, // U+0030 '0' + true, // U+0031 '1' + true, // U+0032 '2' + true, // U+0033 '3' + true, // U+0034 '4' + true, // U+0035 '5' + true, // U+0036 '6' + true, // U+0037 '7' + true, // U+0038 '8' + true, // U+0039 '9' + false, // U+003A ':' + true, // U+003B ';' + true, // U+003C '<' + true, // U+003D '=' + true, // U+003E '>' + false, // U+003F '?' + true, // U+0040 '@' + true, // U+0041 'A' + true, // U+0042 'B' + true, // U+0043 'C' + true, // U+0044 'D' + true, // U+0045 'E' + true, // U+0046 'F' + true, // U+0047 'G' + true, // U+0048 'H' + true, // U+0049 'I' + true, // U+004A 'J' + true, // U+004B 'K' + true, // U+004C 'L' + true, // U+004D 'M' + true, // U+004E 'N' + true, // U+004F 'O' + true, // U+0050 'P' + true, // U+0051 'Q' + true, // U+0052 'R' + true, // U+0053 'S' + true, // U+0054 'T' + true, // U+0055 'U' + true, // U+0056 'V' + true, // U+0057 'W' + true, // U+0058 'X' + true, // U+0059 'Y' + true, // U+005A 'Z' + false, // U+005B '[' + false, // U+005C '\' + false, // U+005D ']' + true, // U+005E '^' + true, // U+005F '_' + true, // U+0060 '`' + true, // U+0061 'a' + true, // U+0062 'b' + true, // U+0063 'c' + true, // U+0064 'd' + true, // U+0065 'e' + true, // U+0066 'f' + true, // U+0067 'g' + true, // U+0068 'h' + true, // U+0069 'i' + true, // U+006A 'j' + true, // U+006B 'k' + true, // U+006C 'l' + true, // U+006D 'm' + true, // U+006E 'n' + true, // U+006F 'o' + true, // U+0070 'p' + true, // U+0071 'q' + true, // U+0072 'r' + true, // U+0073 's' + true, // U+0074 't' + true, // U+0075 'u' + true, // U+0076 'v' + true, // U+0077 'w' + true, // U+0078 'x' + true, // U+0079 'y' + true, // U+007A 'z' + true, // U+007B '{' + true, // U+007C '|' + true, // U+007D '}' + true, // U+007E '~' + false, // U+007F (DEL) + ]; + } + + internal static int GetIndexOfFirstInvalidTypeNameCharacter(ReadOnlySpan input, bool strictMode) + { + return GetIndexOfFirstInvalidCharacter(input, strictMode, GetAsciiCharsAllowMap()); static ReadOnlySpan GetAsciiCharsAllowMap() => [ - false, // U+0000 (NUL) - false, // U+0001 (SOH) - false, // U+0002 (STX) - false, // U+0003 (ETX) - false, // U+0004 (EOT) - false, // U+0005 (ENQ) - false, // U+0006 (ACK) - false, // U+0007 (BEL) - false, // U+0008 (BS) - false, // U+0009 (TAB) - false, // U+000A (LF) - false, // U+000B (VT) - false, // U+000C (FF) - false, // U+000D (CR) - false, // U+000E (SO) - false, // U+000F (SI) - false, // U+0010 (DLE) - false, // U+0011 (DC1) - false, // U+0012 (DC2) - false, // U+0013 (DC3) - false, // U+0014 (DC4) - false, // U+0015 (NAK) - false, // U+0016 (SYN) - false, // U+0017 (ETB) - false, // U+0018 (CAN) - false, // U+0019 (EM) - false, // U+001A (SUB) - false, // U+001B (ESC) - false, // U+001C (FS) - false, // U+001D (GS) - false, // U+001E (RS) - false, // U+001F (US) - false, // U+0020 ' ' - true, // U+0021 '!' - false, // U+0022 '"' - true, // U+0023 '#' - true, // U+0024 '$' - true, // U+0025 '%' - false, // U+0026 '&' - false, // U+0027 ''' - true, // U+0028 '(' - true, // U+0029 ')' - false, // U+002A '*' - true, // U+002B '+' - false, // U+002C ',' - true, // U+002D '-' - true, // U+002E '.' - false, // U+002F '/' - true, // U+0030 '0' - true, // U+0031 '1' - true, // U+0032 '2' - true, // U+0033 '3' - true, // U+0034 '4' - true, // U+0035 '5' - true, // U+0036 '6' - true, // U+0037 '7' - true, // U+0038 '8' - true, // U+0039 '9' - false, // U+003A ':' - false, // U+003B ';' - true, // U+003C '<' - true, // U+003D '=' - true, // U+003E '>' - false, // U+003F '?' - true, // U+0040 '@' - true, // U+0041 'A' - true, // U+0042 'B' - true, // U+0043 'C' - true, // U+0044 'D' - true, // U+0045 'E' - true, // U+0046 'F' - true, // U+0047 'G' - true, // U+0048 'H' - true, // U+0049 'I' - true, // U+004A 'J' - true, // U+004B 'K' - true, // U+004C 'L' - true, // U+004D 'M' - true, // U+004E 'N' - true, // U+004F 'O' - true, // U+0050 'P' - true, // U+0051 'Q' - true, // U+0052 'R' - true, // U+0053 'S' - true, // U+0054 'T' - true, // U+0055 'U' - true, // U+0056 'V' - true, // U+0057 'W' - true, // U+0058 'X' - true, // U+0059 'Y' - true, // U+005A 'Z' - false, // U+005B '[' - false, // U+005C '\' - false, // U+005D ']' - true, // U+005E '^' - true, // U+005F '_' - true, // U+0060 '`' - true, // U+0061 'a' - true, // U+0062 'b' - true, // U+0063 'c' - true, // U+0064 'd' - true, // U+0065 'e' - true, // U+0066 'f' - true, // U+0067 'g' - true, // U+0068 'h' - true, // U+0069 'i' - true, // U+006A 'j' - true, // U+006B 'k' - true, // U+006C 'l' - true, // U+006D 'm' - true, // U+006E 'n' - true, // U+006F 'o' - true, // U+0070 'p' - true, // U+0071 'q' - true, // U+0072 'r' - true, // U+0073 's' - true, // U+0074 't' - true, // U+0075 'u' - true, // U+0076 'v' - true, // U+0077 'w' - true, // U+0078 'x' - true, // U+0079 'y' - true, // U+007A 'z' - true, // U+007B '{' - true, // U+007C '|' - true, // U+007D '}' - true, // U+007E '~' - false, // U+007F (DEL) + false, // U+0000 (NUL) + false, // U+0001 (SOH) + false, // U+0002 (STX) + false, // U+0003 (ETX) + false, // U+0004 (EOT) + false, // U+0005 (ENQ) + false, // U+0006 (ACK) + false, // U+0007 (BEL) + false, // U+0008 (BS) + false, // U+0009 (TAB) + false, // U+000A (LF) + false, // U+000B (VT) + false, // U+000C (FF) + false, // U+000D (CR) + false, // U+000E (SO) + false, // U+000F (SI) + false, // U+0010 (DLE) + false, // U+0011 (DC1) + false, // U+0012 (DC2) + false, // U+0013 (DC3) + false, // U+0014 (DC4) + false, // U+0015 (NAK) + false, // U+0016 (SYN) + false, // U+0017 (ETB) + false, // U+0018 (CAN) + false, // U+0019 (EM) + false, // U+001A (SUB) + false, // U+001B (ESC) + false, // U+001C (FS) + false, // U+001D (GS) + false, // U+001E (RS) + false, // U+001F (US) + false, // U+0020 ' ' + true, // U+0021 '!' + false, // U+0022 '"' + true, // U+0023 '#' + true, // U+0024 '$' + true, // U+0025 '%' + false, // U+0026 '&' + false, // U+0027 ''' + true, // U+0028 '(' + true, // U+0029 ')' + false, // U+002A '*' + true, // U+002B '+' + false, // U+002C ',' + true, // U+002D '-' + true, // U+002E '.' + false, // U+002F '/' + true, // U+0030 '0' + true, // U+0031 '1' + true, // U+0032 '2' + true, // U+0033 '3' + true, // U+0034 '4' + true, // U+0035 '5' + true, // U+0036 '6' + true, // U+0037 '7' + true, // U+0038 '8' + true, // U+0039 '9' + false, // U+003A ':' + false, // U+003B ';' + true, // U+003C '<' + true, // U+003D '=' + true, // U+003E '>' + false, // U+003F '?' + true, // U+0040 '@' + true, // U+0041 'A' + true, // U+0042 'B' + true, // U+0043 'C' + true, // U+0044 'D' + true, // U+0045 'E' + true, // U+0046 'F' + true, // U+0047 'G' + true, // U+0048 'H' + true, // U+0049 'I' + true, // U+004A 'J' + true, // U+004B 'K' + true, // U+004C 'L' + true, // U+004D 'M' + true, // U+004E 'N' + true, // U+004F 'O' + true, // U+0050 'P' + true, // U+0051 'Q' + true, // U+0052 'R' + true, // U+0053 'S' + true, // U+0054 'T' + true, // U+0055 'U' + true, // U+0056 'V' + true, // U+0057 'W' + true, // U+0058 'X' + true, // U+0059 'Y' + true, // U+005A 'Z' + false, // U+005B '[' + false, // U+005C '\' + false, // U+005D ']' + true, // U+005E '^' + true, // U+005F '_' + true, // U+0060 '`' + true, // U+0061 'a' + true, // U+0062 'b' + true, // U+0063 'c' + true, // U+0064 'd' + true, // U+0065 'e' + true, // U+0066 'f' + true, // U+0067 'g' + true, // U+0068 'h' + true, // U+0069 'i' + true, // U+006A 'j' + true, // U+006B 'k' + true, // U+006C 'l' + true, // U+006D 'm' + true, // U+006E 'n' + true, // U+006F 'o' + true, // U+0070 'p' + true, // U+0071 'q' + true, // U+0072 'r' + true, // U+0073 's' + true, // U+0074 't' + true, // U+0075 'u' + true, // U+0076 'v' + true, // U+0077 'w' + true, // U+0078 'x' + true, // U+0079 'y' + true, // U+007A 'z' + true, // U+007B '{' + true, // U+007C '|' + true, // U+007D '}' + true, // U+007E '~' + false, // U+007F (DEL) ]; } diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs index 078cc3c5af043..8c66ce61bac1d 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs @@ -35,6 +35,10 @@ public int MaxRecursiveDepth /// /// Extends ECMA-335 standard limitations with a set of opinionated rules based on most up-to-date security knowledge. /// + /// + /// When parsing AssemblyName, only Version, Culture and PublicKeyToken attributes are allowed. + /// The comparison is also case-sensitive (in contrary to constructor). + /// public bool StrictValidation { get; set; } } } 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 4964985ffb4fd..ee149cad89c6e 100644 --- a/src/libraries/System.Reflection.Metadata/src/System.Reflection.Metadata.csproj +++ b/src/libraries/System.Reflection.Metadata/src/System.Reflection.Metadata.csproj @@ -253,6 +253,9 @@ The System.Reflection.Metadata library is built-in as part of the shared framewo + diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs index f7af885f27be4..8e9b463a63bc5 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs @@ -69,34 +69,67 @@ public void GetFullTypeNameLengthReturnsExpectedValue(string input, int expected Assert.Equal(expectedIsNested, isNested); } + public static IEnumerable InvalidNamesArguments() + { + yield return new object[] { "", 0 }; + yield return new object[] { "\0NullCharacterIsNotAllowed", 0 }; + yield return new object[] { "Null\0CharacterIsNotAllowed", 4 }; + yield return new object[] { "NullCharacterIsNotAllowed\0", 25 }; + yield return new object[] { "\bBackspaceIsNotAllowed", 0 }; + yield return new object[] { "EscapingIsNotAllowed\\", 20 }; + yield return new object[] { "EscapingIsNotAllowed\\\\", 20 }; + yield return new object[] { "EscapingIsNotAllowed\\*", 20 }; + yield return new object[] { "EscapingIsNotAllowed\\&", 20 }; + yield return new object[] { "EscapingIsNotAllowed\\+", 20 }; + yield return new object[] { "EscapingIsNotAllowed\\[", 20 }; + yield return new object[] { "EscapingIsNotAllowed\\]", 20 }; + yield return new object[] { "Slash/IsNotAllowed", 5 }; + yield return new object[] { "WhitespacesAre\tNotAllowed", 14 }; + yield return new object[] { "WhitespacesAreNot\r\nAllowed", 17 }; + yield return new object[] { "Question?MarkIsNotAllowed", 8 }; + yield return new object[] { "Quotes\"AreNotAllowed", 6 }; + yield return new object[] { "Quote'IsNotAllowed", 5 }; + yield return new object[] { "abcdefghijklmnopqrstuvwxyz", -1 }; + yield return new object[] { "ABCDEFGHIJKLMNOPQRSTUVWXYZ", -1 }; + yield return new object[] { "0123456789", -1 }; + yield return new object[] { "BacktickIsOk`1", -1 }; + } + + + [Theory] + [MemberData(nameof(InvalidNamesArguments))] + [InlineData("Spaces AreAllowed", -1)] + [InlineData("!@#$%^()-_{}|<>.~&;", -1)] + public void GetIndexOfFirstInvalidAssemblyNameCharacter_ReturnsFirstInvalidCharacter(string input, int expected) + { + Assert.Equal(expected, TypeNameParserHelpers.GetIndexOfFirstInvalidAssemblyNameCharacter(input.AsSpan(), strictMode: true)); + + TypeNameParserOptions strictOptions = new() + { + StrictValidation = true + }; + + string assemblyQualifiedName = $"Namespace.CorrectTypeName, {input}"; + + if (expected >= 0) + { + Assert.False(TypeName.TryParse(assemblyQualifiedName.AsSpan(), out _, strictOptions)); + Assert.Throws(() => TypeName.Parse(assemblyQualifiedName.AsSpan(), strictOptions)); + } + else + { + Assert.True(TypeName.TryParse(assemblyQualifiedName.AsSpan(), out TypeName parsed, strictOptions)); + Assert.Equal(assemblyQualifiedName, parsed.AssemblyQualifiedName); + } + } + [Theory] - [InlineData("", 0)] - [InlineData("\0NullCharacterIsNotAllowed", 0)] - [InlineData("Null\0CharacterIsNotAllowed", 4)] - [InlineData("NullCharacterIsNotAllowed\0", 25)] - [InlineData("\bBackspaceIsNotAllowed", 0)] - [InlineData("EscapingIsNotAllowed\\", 20)] - [InlineData("EscapingIsNotAllowed\\\\", 20)] - [InlineData("EscapingIsNotAllowed\\*", 20)] - [InlineData("EscapingIsNotAllowed\\&", 20)] - [InlineData("EscapingIsNotAllowed\\+", 20)] - [InlineData("EscapingIsNotAllowed\\[", 20)] - [InlineData("EscapingIsNotAllowed\\]", 20)] - [InlineData("Slash/IsNotAllowed", 5)] - [InlineData("Whitespaces AreNotAllowed", 11)] - [InlineData("WhitespacesAre\tNotAllowed", 14)] - [InlineData("WhitespacesAreNot\r\nAllowed", 17)] - [InlineData("Question?MarkIsNotAllowed", 8)] - [InlineData("Quotes\"AreNotAllowed", 6)] - [InlineData("Quote'IsNotAllowed", 5)] - [InlineData("abcdefghijklmnopqrstuvwxyz", -1)] - [InlineData("ABCDEFGHIJKLMNOPQRSTUVWXYZ", -1)] - [InlineData("0123456789", -1)] + [MemberData(nameof(InvalidNamesArguments))] + [InlineData("Spaces AreNotAllowed", 6)] [InlineData("!@#$%^()-_={}|<>.~", -1)] - [InlineData("BacktickIsOk`1", -1)] - public void GetIndexOfFirstInvalidCharacter_ReturnsFirstInvalidCharacter(string input, int expected) + public void GetIndexOfFirstInvalidTypeNameCharacter_ReturnsFirstInvalidCharacter(string input, int expected) { - Assert.Equal(expected, TypeNameParserHelpers.GetIndexOfFirstInvalidCharacter(input.AsSpan(), strictMode: true)); + Assert.Equal(expected, TypeNameParserHelpers.GetIndexOfFirstInvalidTypeNameCharacter(input.AsSpan(), strictMode: true)); TypeNameParserOptions strictOptions = new() { diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs index 1bf2a0a372d7a..a42ac3e9cabda 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs @@ -72,54 +72,71 @@ public void InvalidTypeNamesAreNotAllowed(string input) public void UnicodeCharactersAreAllowedByDefault(string input, string expectedFullName) => Assert.Equal(expectedFullName, TypeName.Parse(input.AsSpan()).FullName); - public static IEnumerable TypeNamesWithAssemblyNames() - { - yield return new object[] - { - "System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", - "System.Int32", - "Int32", - "mscorlib", - new Version(4, 0, 0, 0), - "", - "b77a5c561934e089" - }; - } - [Theory] - [MemberData(nameof(TypeNamesWithAssemblyNames))] - public void TypeNameCanContainAssemblyName(string assemblyQualifiedName, string fullName, string name, string assemblyName, - Version assemblyVersion, string assemblyCulture, string assemblyPublicKeyToken) + [InlineData(typeof(int))] + [InlineData(typeof(Dictionary))] + [InlineData(typeof(int[][]))] + [InlineData(typeof(Assert))] // xUnit assembly + [InlineData(typeof(TypeNameParserTests))] // test assembly + [InlineData(typeof(NestedGeneric_0.NestedGeneric_1.NestedGeneric_2.NestedNonGeneric_3))] + public void TypeNameCanContainAssemblyName(Type type) { - TypeName parsed = TypeName.Parse(assemblyQualifiedName.AsSpan()); + AssemblyName expectedAssemblyName = new(type.Assembly.FullName); - Assert.Equal(assemblyQualifiedName, parsed.AssemblyQualifiedName); - Assert.Equal(fullName, parsed.FullName); - Assert.Equal(name, parsed.Name); - Assert.NotNull(parsed.AssemblyName); - Assert.Equal(assemblyName, parsed.AssemblyName.Name); - Assert.Equal(assemblyVersion, parsed.AssemblyName.Version); - Assert.Equal(assemblyCulture, parsed.AssemblyName.CultureName); - Assert.Equal(GetPublicKeyToken(assemblyPublicKeyToken), parsed.AssemblyName.GetPublicKeyToken()); + Verify(type, expectedAssemblyName, TypeName.Parse(type.AssemblyQualifiedName.AsSpan())); + Verify(type, expectedAssemblyName, TypeName.Parse(type.AssemblyQualifiedName.AsSpan(), new TypeNameParserOptions() { StrictValidation = true })); - static byte[] GetPublicKeyToken(string assemblyPublicKeyToken) + static void Verify(Type type, AssemblyName expectedAssemblyName, TypeName parsed) { - byte[] pkt = new byte[assemblyPublicKeyToken.Length / 2]; - int srcIndex = 0; - for (int i = 0; i < pkt.Length; i++) - { - char hi = assemblyPublicKeyToken[srcIndex++]; - char lo = assemblyPublicKeyToken[srcIndex++]; - pkt[i] = (byte)((FromHexChar(hi) << 4) | FromHexChar(lo)); - } - return pkt; + Assert.Equal(type.AssemblyQualifiedName, parsed.AssemblyQualifiedName); + Assert.Equal(type.FullName, parsed.FullName); + Assert.Equal(type.Name, parsed.Name); + Assert.NotNull(parsed.AssemblyName); + + Assert.Equal(expectedAssemblyName.Name, parsed.AssemblyName.Name); + Assert.Equal(expectedAssemblyName.Version, parsed.AssemblyName.Version); + Assert.Equal(expectedAssemblyName.CultureName, parsed.AssemblyName.CultureName); + Assert.Equal(expectedAssemblyName.GetPublicKeyToken(), parsed.AssemblyName.GetPublicKeyToken()); + Assert.Equal(expectedAssemblyName.FullName, parsed.AssemblyName.FullName); + + Assert.Equal(default, parsed.AssemblyName.ContentType); + Assert.Equal(default, parsed.AssemblyName.Flags); + Assert.Equal(default, parsed.AssemblyName.ProcessorArchitecture); } + } - static byte FromHexChar(char hex) + [Theory] + [InlineData("Hello,")] // trailing comma + [InlineData("Hello, ")] // trailing comma + [InlineData("Hello, ./../PathToA.dll")] // path to a file! + [InlineData("Hello, .\\..\\PathToA.dll")] // path to a file! + [InlineData("Hello, AssemblyName, Version=1.2\0.3.4")] // embedded null in Version (the Version class normally allows this) + [InlineData("Hello, AssemblyName, Version=1.2 .3.4")] // extra space in Version (the Version class normally allows this) + [InlineData("Hello, AssemblyName, Version=1.2.3.4, Version=1.2.3.4")] // duplicate Versions specified + [InlineData("Hello, AssemblyName, Culture=neutral, Culture=neutral")] // duplicate Culture specified + [InlineData("Hello, AssemblyName, PublicKeyToken=b77a5c561934e089, PublicKeyToken=b77a5c561934e089")] // duplicate PublicKeyToken specified + [InlineData("Hello, AssemblyName, PublicKeyToken=bad")] // invalid PKT + [InlineData("Hello, AssemblyName, Culture=en-US_XYZ")] // invalid culture + [InlineData("Hello, AssemblyName, \r\nCulture=en-US")] // disallowed whitespace + [InlineData("Hello, AssemblyName, Version=1.2.3.4,")] // another trailing comma + [InlineData("Hello, AssemblyName, Version=1.2.3.4, =")] // malformed key=token pair + [InlineData("Hello, AssemblyName, Version=1.2.3.4, Architecture=x86")] // Architecture disallowed + [InlineData("Hello, AssemblyName, CodeBase=file://blah")] // CodeBase disallowed (and illegal path chars) + [InlineData("Hello, AssemblyName, CodeBase=legalChars")] // CodeBase disallowed + [InlineData("Hello, AssemblyName, Unrecognized=some")] // not on the allow list? disallowed + [InlineData("Hello, AssemblyName, version=1.2.3.4")] // wrong case (Version) + [InlineData("Hello, AssemblyName, culture=neutral")] // wrong case (Culture) + [InlineData("Hello, AssemblyName, publicKeyToken=b77a5c561934e089")] // wrong case (PKT) + public void CanNotParseTypeWithInvalidAssemblyName(string fullName) + { + TypeNameParserOptions options = new() { - if (hex >= '0' && hex <= '9') return (byte)(hex - '0'); - else return (byte)(hex - 'a' + 10); - } + StrictValidation = true, + AllowFullyQualifiedName = true + }; + + Assert.False(TypeName.TryParse(fullName.AsSpan(), out _, options)); + Assert.Throws(() => TypeName.Parse(fullName.AsSpan(), options)); } [Theory] From c63f3a865bb8024cc115d338ec858c67e5f41229 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Thu, 22 Feb 2024 15:06:55 +0100 Subject: [PATCH 23/48] fix the build and apply some design changes --- .../Reflection/TypeNameParser.CoreCLR.cs | 2 +- .../ILVerification/ILVerification.projitems | 3 ++ .../AssemblyNameParser.netstandard.cs | 17 ++++--- .../System/Reflection/Metadata/TypeName.cs | 45 ++++++++++++------- .../Reflection/Metadata/TypeNameParser.cs | 2 +- .../Metadata/TypeNameParserHelpers.cs | 28 ++++++------ .../Reflection/TypeNameParser.Helpers.cs | 6 +-- .../ref/System.Reflection.Metadata.cs | 4 +- .../tests/Metadata/TypeNameParserSamples.cs | 2 +- .../tests/Metadata/TypeNameParserTests.cs | 32 +++++++------ 10 files changed, 82 insertions(+), 59 deletions(-) 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 320ee674b507b..d3871f336157b 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 @@ -85,7 +85,7 @@ internal partial struct TypeNameParser { return null; } - else if (parsed.AssemblyName is not null && topLevelAssembly is not null) + else if (parsed.GetAssemblyName() is not null && topLevelAssembly is not null) { return throwOnError ? throw new ArgumentException(SR.Argument_AssemblyGetTypeCannotSpecifyAssembly) : null; } diff --git a/src/coreclr/tools/ILVerification/ILVerification.projitems b/src/coreclr/tools/ILVerification/ILVerification.projitems index 2d260c9f45037..43dff62427488 100644 --- a/src/coreclr/tools/ILVerification/ILVerification.projitems +++ b/src/coreclr/tools/ILVerification/ILVerification.projitems @@ -72,6 +72,9 @@ Utilities\AssemblyNameParser.cs + + Utilities\AssemblyNameParser.netstandard.cs + Utilities\TypeName.cs diff --git a/src/libraries/Common/src/System/Reflection/AssemblyNameParser.netstandard.cs b/src/libraries/Common/src/System/Reflection/AssemblyNameParser.netstandard.cs index 71cffe67b4b5e..8787f6c6d27e3 100644 --- a/src/libraries/Common/src/System/Reflection/AssemblyNameParser.netstandard.cs +++ b/src/libraries/Common/src/System/Reflection/AssemblyNameParser.netstandard.cs @@ -4,6 +4,8 @@ using System.Collections.Generic; using System.Globalization; +#nullable enable + namespace System.Reflection { internal ref partial struct AssemblyNameParser @@ -13,6 +15,11 @@ internal ref partial struct AssemblyNameParser private static bool IsPredefinedCulture(string cultureName) { + if (cultureName is null) + { + return false; + } + if (_predefinedCultureNames is null) { lock (_predefinedCultureNamesLock) @@ -28,7 +35,10 @@ static HashSet GetPredefinedCultureNames() HashSet result = new(StringComparer.Ordinal); foreach (CultureInfo culture in CultureInfo.GetCultures(CultureTypes.AllCultures)) { - result.Add(AnsiToLower(culture.Name)); + if (culture.Name is not null) + { + result.Add(AnsiToLower(culture.Name)); + } } return result; } @@ -37,11 +47,6 @@ static HashSet GetPredefinedCultureNames() // All non-ASCII characters are left alone. static string AnsiToLower(string input) { - if (input is null) - { - return null; - } - int idx; for (idx = 0; idx < input.Length; idx++) { diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs index e0e13e4edc006..92af60924dc0c 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs @@ -22,6 +22,7 @@ sealed class TypeName /// private readonly int _rankOrModifier; private readonly TypeName[]? _genericArguments; + private readonly AssemblyName? _assemblyName; private string? _assemblyQualifiedName; internal TypeName(string name, string fullName, @@ -33,7 +34,7 @@ internal TypeName(string name, string fullName, { Name = name; FullName = fullName; - AssemblyName = assemblyName; + _assemblyName = assemblyName; _rankOrModifier = rankOrModifier; UnderlyingType = underlyingType; ContainingType = containingType; @@ -45,16 +46,10 @@ internal TypeName(string name, string fullName, /// The assembly-qualified name of the type; e.g., "System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089". /// /// - /// If is null, simply returns . + /// If returns null, simply returns . /// public string AssemblyQualifiedName - => _assemblyQualifiedName ??= AssemblyName is null ? FullName : $"{FullName}, {AssemblyName.FullName}"; - - /// - /// The assembly which contains this type, or null if this was not - /// created from a fully-qualified name. - /// - public AssemblyName? AssemblyName { get; } // TODO: AssemblyName is mutable, are we fine with that? Does it not offer too much? + => _assemblyQualifiedName ??= _assemblyName is null ? FullName : $"{FullName}, {_assemblyName.FullName}"; /// /// If this type is a nested type (see ), gets @@ -205,18 +200,34 @@ public int GetArrayRank() }; /// - /// If this represents a constructed generic type, returns an array - /// of all the generic arguments. Otherwise it returns an empty array. + /// Returns assembly name which contains this type, or null if this was not + /// created from a fully-qualified name. + /// + /// Since is mutable, this method returns a copy of it. + public AssemblyName? GetAssemblyName() + { + if (_assemblyName is null) + { + return null; + } + +#if SYSTEM_PRIVATE_CORELIB + return _assemblyName; // no need for a copy in CoreLib +#else + return (AssemblyName)_assemblyName.Clone(); +#endif + } + + /// + /// If this represents a constructed generic type, returns a span + /// of all the generic arguments. Otherwise it returns an empty span. /// /// - /// For example, given "Dictionary<string, int>", returns a 2-element array containing + /// For example, given "Dictionary<string, int>", returns a 2-element span containing /// string and int. - /// The caller controls the returned array and may mutate it freely. /// - public TypeName[] GetGenericArguments() - => _genericArguments is not null - ? (TypeName[])_genericArguments.Clone() // we return a copy on purpose, to not allow for mutations. TODO: consider returning a ROS - : Array.Empty(); // TODO: should we throw (Levi's parser throws InvalidOperationException in such case), Type.GetGenericArguments just returns an empty array + public ReadOnlySpan GetGenericArguments() + => _genericArguments is null ? ReadOnlySpan.Empty : _genericArguments.AsSpan(); private static int GetTotalComplexity(TypeName? underlyingType, TypeName? containingType, TypeName[]? genericTypeArguments) { diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs index f36f2ab322778..049b735f0e63e 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs @@ -41,7 +41,7 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse if (trimmedName.IsEmpty) { // whitespace input needs to report the error index as 0 - return ThrowInvalidTypeNameOrReturnNull(throwOnError, 0); + return ThrowInvalidTypeNameOrReturnNull(throwOnError, errorIndex: 0); } int recursiveDepth = 0; diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs index 8a0501abdfe40..de05a9bc0e51c 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs @@ -153,7 +153,7 @@ internal static int GetFullTypeNameLength(ReadOnlySpan input, out bool isN return (int)Math.Min((uint)offset, (uint)input.Length); } - private static int GetIndexOfFirstInvalidCharacter(ReadOnlySpan input, bool strictMode, ReadOnlySpan allowedAsciiCharsMap) + private static int GetIndexOfFirstInvalidCharacter(ReadOnlySpan input, bool strictMode, bool assemblyName) { if (input.IsEmpty) { @@ -164,6 +164,10 @@ private static int GetIndexOfFirstInvalidCharacter(ReadOnlySpan input, boo return -1; } + ReadOnlySpan allowedAsciiCharsMap = assemblyName + ? GetAssemblyNameAsciiCharsAllowMap() + : GetTypeNameAsciiCharsAllowMap(); + Debug.Assert(allowedAsciiCharsMap.Length == 128); for (int i = 0; i < input.Length; i++) @@ -187,13 +191,8 @@ private static int GetIndexOfFirstInvalidCharacter(ReadOnlySpan input, boo } return -1; - } - internal static int GetIndexOfFirstInvalidAssemblyNameCharacter(ReadOnlySpan input, bool strictMode) - { - return GetIndexOfFirstInvalidCharacter(input, strictMode, GetAsciiCharsAllowMap()); - - static ReadOnlySpan GetAsciiCharsAllowMap() => + static ReadOnlySpan GetAssemblyNameAsciiCharsAllowMap() => [ false, // U+0000 (NUL) false, // U+0001 (SOH) @@ -324,13 +323,8 @@ static ReadOnlySpan GetAsciiCharsAllowMap() => true, // U+007E '~' false, // U+007F (DEL) ]; - } - internal static int GetIndexOfFirstInvalidTypeNameCharacter(ReadOnlySpan input, bool strictMode) - { - return GetIndexOfFirstInvalidCharacter(input, strictMode, GetAsciiCharsAllowMap()); - - static ReadOnlySpan GetAsciiCharsAllowMap() => + static ReadOnlySpan GetTypeNameAsciiCharsAllowMap() => [ false, // U+0000 (NUL) false, // U+0001 (SOH) @@ -463,6 +457,12 @@ static ReadOnlySpan GetAsciiCharsAllowMap() => ]; } + internal static int GetIndexOfFirstInvalidAssemblyNameCharacter(ReadOnlySpan input, bool strictMode) + => GetIndexOfFirstInvalidCharacter(input, strictMode, assemblyName: true); + + internal static int GetIndexOfFirstInvalidTypeNameCharacter(ReadOnlySpan input, bool strictMode) + => GetIndexOfFirstInvalidCharacter(input, strictMode, assemblyName: false); + internal static ReadOnlySpan GetName(ReadOnlySpan fullName) { #if NET8_0_OR_GREATER @@ -587,7 +587,7 @@ internal static bool TryParseNextDecorator(ref ReadOnlySpan input, out int if (TryStripFirstCharAndTrailingSpaces(ref input, '*')) { - rankOrModifier = TypeNameParserHelpers.Pointer; + rankOrModifier = Pointer; return true; } diff --git a/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs index 4ed21c25093c0..4211e934ac093 100644 --- a/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs +++ b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs @@ -97,12 +97,12 @@ private static (string typeNamespace, string name) SplitFullTypeName(string type } string nonNestedParentName = current!.FullName; - Type? type = GetType(nonNestedParentName, nestedTypeNames, typeName.AssemblyName); + Type? type = GetType(nonNestedParentName, nestedTypeNames, typeName.GetAssemblyName()); return Make(type, typeName); } else if (typeName.UnderlyingType is null) { - Type? type = GetType(typeName.FullName, nestedTypeNames: ReadOnlySpan.Empty, typeName.AssemblyName); + Type? type = GetType(typeName.FullName, nestedTypeNames: ReadOnlySpan.Empty, typeName.GetAssemblyName()); return Make(type, typeName); } @@ -122,7 +122,7 @@ private static (string typeNamespace, string name) SplitFullTypeName(string type } else if (typeName.IsConstructedGenericType) { - Metadata.TypeName[] genericArgs = typeName.GetGenericArguments(); + ReadOnlySpan genericArgs = typeName.GetGenericArguments(); Type[] genericTypes = new Type[genericArgs.Length]; for (int i = 0; i < genericArgs.Length; i++) { 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 2ece60ea39966..9f7794f616541 100644 --- a/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs +++ b/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs @@ -2411,7 +2411,6 @@ public readonly partial struct TypeLayout public sealed partial class TypeName { internal TypeName() { } - public System.Reflection.AssemblyName? AssemblyName { get { throw null; } } public string AssemblyQualifiedName { get { throw null; } } public System.Reflection.Metadata.TypeName? ContainingType { get { throw null; } } public bool IsArray { get { throw null; } } @@ -2428,7 +2427,8 @@ internal TypeName() { } public static System.Reflection.Metadata.TypeName Parse(System.ReadOnlySpan typeName, System.Reflection.Metadata.TypeNameParserOptions? 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.TypeNameParserOptions? options = null) { throw null; } public int GetArrayRank() { throw null; } - public System.Reflection.Metadata.TypeName[] GetGenericArguments() { throw null; } + public System.Reflection.AssemblyName? GetAssemblyName() { throw null; } + public System.ReadOnlySpan GetGenericArguments() { throw null; } } public sealed partial class TypeNameParserOptions { diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs index 288a8776803bf..e8219e288ed9f 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs @@ -100,7 +100,7 @@ public SampleSerializationBinder(Type[]? allowedTypes = null) Type genericTypeDefinition = GetTypeFromParsedTypeName(genericTypeDefinitionName); Debug.Assert(genericTypeDefinition.IsGenericTypeDefinition); - TypeName[] genericArgs = parsed.GetGenericArguments(); + ReadOnlySpan genericArgs = parsed.GetGenericArguments(); Type[] typeArguments = new Type[genericArgs.Length]; for (int i = 0; i < genericArgs.Length; i++) { diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs index a42ac3e9cabda..ace591cce4e41 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs @@ -91,17 +91,19 @@ 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); - Assert.NotNull(parsed.AssemblyName); - Assert.Equal(expectedAssemblyName.Name, parsed.AssemblyName.Name); - Assert.Equal(expectedAssemblyName.Version, parsed.AssemblyName.Version); - Assert.Equal(expectedAssemblyName.CultureName, parsed.AssemblyName.CultureName); - Assert.Equal(expectedAssemblyName.GetPublicKeyToken(), parsed.AssemblyName.GetPublicKeyToken()); - Assert.Equal(expectedAssemblyName.FullName, parsed.AssemblyName.FullName); + AssemblyName parsedAssemblyName = parsed.GetAssemblyName(); + Assert.NotNull(parsedAssemblyName); - Assert.Equal(default, parsed.AssemblyName.ContentType); - Assert.Equal(default, parsed.AssemblyName.Flags); - Assert.Equal(default, parsed.AssemblyName.ProcessorArchitecture); + Assert.Equal(expectedAssemblyName.Name, parsedAssemblyName.Name); + Assert.Equal(expectedAssemblyName.Version, parsedAssemblyName.Version); + Assert.Equal(expectedAssemblyName.CultureName, parsedAssemblyName.CultureName); + Assert.Equal(expectedAssemblyName.GetPublicKeyToken(), parsedAssemblyName.GetPublicKeyToken()); + Assert.Equal(expectedAssemblyName.FullName, parsedAssemblyName.FullName); + + Assert.Equal(default, parsedAssemblyName.ContentType); + Assert.Equal(default, parsedAssemblyName.Flags); + Assert.Equal(default, parsedAssemblyName.ProcessorArchitecture); } } @@ -248,7 +250,7 @@ static string GetName(int depth) static void Validate(TypeName parsed, int maxDepth) { Assert.True(parsed.IsConstructedGenericType); - TypeName[] genericArgs = parsed.GetGenericArguments(); + TypeName[] genericArgs = parsed.GetGenericArguments().ToArray(); Assert.Equal(maxDepth - 1, genericArgs.Length); Assert.All(genericArgs, arg => Assert.False(arg.IsConstructedGenericType)); } @@ -310,7 +312,7 @@ public void GenericArgumentsAreSupported(string input, string name, string[] gen if (assemblyNames is not null) { - Assert.Equal(assemblyNames[i].FullName, genericArg.AssemblyName.FullName); + Assert.Equal(assemblyNames[i].FullName, genericArg.GetAssemblyName().FullName); } } } @@ -509,9 +511,11 @@ static void Verify(Type type, TypeName typeName, bool ignoreCase) } else if (typeName.UnderlyingType is null) // elemental { - Type? type = typeName.AssemblyName is null + AssemblyName? assemblyName = typeName.GetAssemblyName(); + + Type? type = assemblyName is null ? Type.GetType(typeName.FullName, throwOnError, ignoreCase) - : Assembly.Load(typeName.AssemblyName).GetType(typeName.FullName, throwOnError, ignoreCase); + : Assembly.Load(assemblyName).GetType(typeName.FullName, throwOnError, ignoreCase); return Make(type); } @@ -526,7 +530,7 @@ static void Verify(Type type, TypeName typeName, bool ignoreCase) } else if (typeName.IsConstructedGenericType) { - TypeName[] genericArgs = typeName.GetGenericArguments(); + ReadOnlySpan genericArgs = typeName.GetGenericArguments(); Type[] genericTypes = new Type[genericArgs.Length]; for (int i = 0; i < genericArgs.Length; i++) { From 83cdd1b66a4aeffe66581e851a877ef772d393a7 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Thu, 22 Feb 2024 16:06:18 +0100 Subject: [PATCH 24/48] add escaping support --- .../Reflection/TypeNameParser.CoreCLR.cs | 42 +++++---- .../CustomAttributeTypeNameParser.cs | 4 +- .../Metadata/TypeNameParserHelpers.cs | 89 +++++++++++++++---- .../Reflection/TypeNameParser.Helpers.cs | 69 ++++++++++---- .../Metadata/TypeNameParserHelpersTests.cs | 27 +++++- .../tests/Metadata/TypeNameParserTests.cs | 4 + 6 files changed, 178 insertions(+), 57 deletions(-) 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 d3871f336157b..eabc9b433e628 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 @@ -184,7 +184,8 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName, 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, AssemblyName? assemblyNameIfAny) + private Type? GetType(string escapedTypeName, // For nested types, it's Name. For other types it's FullName + ReadOnlySpan nestedTypeNames, AssemblyName? assemblyNameIfAny, string fullEscapedName) { Assembly? assembly; @@ -204,8 +205,6 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName, // Resolve the top level type. if (_typeResolver is not null) { - string escapedTypeName = EscapeTypeName(typeName); - type = _typeResolver(assembly, escapedTypeName, _ignoreCase); if (type is null) @@ -227,11 +226,11 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName, { 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(escapedTypeName, nestedTypeNames, fullEscapedName); } if (assembly is RuntimeAssembly runtimeAssembly) @@ -239,16 +238,20 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName, // Compat: Non-extensible parser allows ambiguous matches with ignore case lookup if (!_extensibleParser || !_ignoreCase) { - return runtimeAssembly.GetTypeCore(typeName, nestedTypeNames, throwOnError: _throwOnError, ignoreCase: _ignoreCase); + string[]? unescapedNestedNames = null; + UnescapeTypeNames(nestedTypeNames, ref unescapedNestedNames); + return runtimeAssembly.GetTypeCore(UnescapeTypeName(escapedTypeName), + unescapedNestedNames is not null ? unescapedNestedNames : nestedTypeNames, + throwOnError: _throwOnError, ignoreCase: _ignoreCase); } - type = runtimeAssembly.GetTypeCore(typeName, default, throwOnError: _throwOnError, ignoreCase: _ignoreCase); + type = runtimeAssembly.GetTypeCore(UnescapeTypeName(escapedTypeName), 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) @@ -261,14 +264,14 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName, if (_ignoreCase) bindingFlags |= BindingFlags.IgnoreCase; - type = type.GetNestedType(nestedTypeNames[i], bindingFlags); + type = type.GetNestedType(UnescapeTypeName(nestedTypeNames[i]), bindingFlags); if (type is null) { 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; } @@ -277,12 +280,17 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName, return type; } - private Type? GetTypeFromDefaultAssemblies(string typeName, ReadOnlySpan nestedTypeNames) + private Type? GetTypeFromDefaultAssemblies(string escapedTypeName, ReadOnlySpan nestedTypeNames, string fullEscapedName) { + string unescapedTypeName = UnescapeTypeName(escapedTypeName); + string[]? unescapedNestedNames = null; + UnescapeTypeNames(nestedTypeNames, ref unescapedNestedNames); + RuntimeAssembly? requestingAssembly = (RuntimeAssembly?)_requestingAssembly; if (requestingAssembly is not null) { - Type? type = ((RuntimeAssembly)requestingAssembly).GetTypeCore(typeName, nestedTypeNames, throwOnError: false, ignoreCase: _ignoreCase); + Type? type = ((RuntimeAssembly)requestingAssembly).GetTypeCore(unescapedTypeName, + unescapedNestedNames is null ? nestedTypeNames : unescapedNestedNames, throwOnError: false, ignoreCase: _ignoreCase); if (type is not null) return type; } @@ -290,21 +298,23 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName, RuntimeAssembly coreLib = (RuntimeAssembly)typeof(object).Assembly; if (requestingAssembly != coreLib) { - Type? type = ((RuntimeAssembly)coreLib).GetTypeCore(typeName, nestedTypeNames, throwOnError: false, ignoreCase: _ignoreCase); + Type? type = ((RuntimeAssembly)coreLib).GetTypeCore(unescapedTypeName, + unescapedNestedNames is null ? nestedTypeNames : unescapedNestedNames, throwOnError: false, ignoreCase: _ignoreCase); if (type is not null) return type; } - RuntimeAssembly? resolvedAssembly = AssemblyLoadContext.OnTypeResolve(requestingAssembly, EscapeTypeName(typeName, nestedTypeNames)); + RuntimeAssembly? resolvedAssembly = AssemblyLoadContext.OnTypeResolve(requestingAssembly, fullEscapedName); if (resolvedAssembly is not null) { - Type? type = resolvedAssembly.GetTypeCore(typeName, nestedTypeNames, throwOnError: false, ignoreCase: _ignoreCase); + Type? type = resolvedAssembly.GetTypeCore(unescapedTypeName, + unescapedNestedNames is null ? nestedTypeNames : unescapedNestedNames, throwOnError: false, ignoreCase: _ignoreCase); if (type is not null) return type; } if (_throwOnError) - throw new TypeLoadException(SR.Format(SR.TypeLoad_ResolveTypeFromAssembly, EscapeTypeName(typeName), (requestingAssembly ?? coreLib).FullName)); + throw new TypeLoadException(SR.Format(SR.TypeLoad_ResolveTypeFromAssembly, escapedTypeName, (requestingAssembly ?? coreLib).FullName)); 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 093901ce502ec..49345ea00ea39 100644 --- a/src/coreclr/tools/Common/TypeSystem/Common/Utilities/CustomAttributeTypeNameParser.cs +++ b/src/coreclr/tools/Common/TypeSystem/Common/Utilities/CustomAttributeTypeNameParser.cs @@ -68,7 +68,7 @@ public Type MakeGenericType(Type[] typeArguments) } } - private Type GetType(string typeName, ReadOnlySpan nestedTypeNames, AssemblyName assemblyNameIfAny) + private Type GetType(string typeName, ReadOnlySpan nestedTypeNames, AssemblyName assemblyNameIfAny, string fullEscapedName) { ModuleDesc module = (assemblyNameIfAny == null) ? _module : _module.Context.ResolveAssembly(assemblyNameIfAny, throwIfNotFound: _throwIfNotFound); @@ -96,7 +96,7 @@ private Type GetType(string typeName, ReadOnlySpan nestedTypeNames, Asse } if (_throwIfNotFound) - ThrowHelper.ThrowTypeLoadException(EscapeTypeName(typeName, nestedTypeNames), module); + ThrowHelper.ThrowTypeLoadException(fullEscapedName, module); return null; } diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs index de05a9bc0e51c..554096c0e969b 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs @@ -28,11 +28,9 @@ internal static class TypeNameParserHelpers internal const int Pointer = -2; internal const int ByRef = -3; private const char EscapeCharacter = '\\'; - private const string EndOfTypeNameDelimiters = ".+"; - private const string EndOfFullTypeNameDelimiters = "[]&*,+"; #if NET8_0_OR_GREATER - private static readonly SearchValues _endOfTypeNameDelimitersSearchValues = SearchValues.Create(EndOfTypeNameDelimiters); - private static readonly SearchValues _endOfFullTypeNameDelimitersSearchValues = SearchValues.Create(EndOfFullTypeNameDelimiters); + private static readonly SearchValues _endOfTypeNameDelimitersSearchValues = SearchValues.Create(".+"); + private static readonly SearchValues _endOfFullTypeNameDelimitersSearchValues = SearchValues.Create("[]&*,+\\"); #endif /// @@ -122,9 +120,11 @@ internal static string GetGenericTypeFullName(ReadOnlySpan fullTypeName, T return result.ToString(); } - // Normalizes "not found" to input length, since caller is expected to slice. + /// 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 @@ -139,20 +139,49 @@ internal static int GetFullTypeNameLength(ReadOnlySpan input, out bool isN #if NET8_0_OR_GREATER int offset = input.IndexOfAny(_endOfFullTypeNameDelimitersSearchValues); -#elif NET6_0_OR_GREATER - int offset = input.IndexOfAny(EndOfTypeNameDelimiters); -#else - int offset; - for (offset = 0; offset < input.Length; offset++) + 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) { - if (EndOfFullTypeNameDelimiters.IndexOf(input[offset]) >= 0) { break; } + 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 - return (int)Math.Min((uint)offset, (uint)input.Length); + 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; } + // this method checks for a single banned char, not for invalid combinations of characters like invalid escaping private static int GetIndexOfFirstInvalidCharacter(ReadOnlySpan input, bool strictMode, bool assemblyName) { if (input.IsEmpty) @@ -467,16 +496,33 @@ internal static ReadOnlySpan GetName(ReadOnlySpan fullName) { #if NET8_0_OR_GREATER int offset = fullName.LastIndexOfAny(_endOfTypeNameDelimitersSearchValues); -#elif NET6_0_OR_GREATER - int offset = fullName.LastIndexOfAny(EndOfTypeNameDelimiters); -#else - int offset = fullName.Length - 1; - for (; offset >= 0; offset--) + Debug.Assert(offset != 0, "The provided full name must be valid"); + + if (offset > 0 && fullName[offset - 1] == EscapeCharacter) // this should be very rare (IL Emit & pure IL) { - if (EndOfTypeNameDelimiters.IndexOf(fullName[offset]) >= 0) { break; } + offset = GetUnescapedOffset(fullName, startIndex: offset); } +#else + int offset = GetUnescapedOffset(fullName, startIndex: fullName.Length - 1); #endif 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; + } } internal static string GetRankOrModifierStringRepresentation(int rankOrModifier) @@ -554,8 +600,13 @@ internal static bool TryGetTypeNameInfo(ReadOnlySpan input, ref List? do { int length = GetFullTypeNameLength(input.Slice(totalLength), out isNestedType); - if (length <= 0) // it's possible only for a pair of unescaped '+' characters + if (length <= 0) { + // invalid type names: + // -1: invalid escaping + // 0: pair of unescaped "++" characters + nestedNameLengths = null; + totalLength = genericArgCount = 0; return false; } diff --git a/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs index 4211e934ac093..e20faf4937183 100644 --- a/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs +++ b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs @@ -10,6 +10,8 @@ namespace System.Reflection { internal partial struct TypeNameParser { + private const char EscapeCharacter = '\\'; + #if NETCOREAPP private static ReadOnlySpan CharsToEscape => "\\[]+*&,"; @@ -22,36 +24,69 @@ private static bool NeedsEscapingInTypeName(char c) => Array.IndexOf(CharsToEscape, c) >= 0; #endif - private static string EscapeTypeName(string name) + private static string UnescapeTypeName(string name) { - if (name.AsSpan().IndexOfAny(CharsToEscape) < 0) + int indexOfEscapeCharacter = name.IndexOf(EscapeCharacter); + if (indexOfEscapeCharacter < 0) + { return name; + } + // 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]); - foreach (char c in name) + sb.Append(name.AsSpan(0, indexOfEscapeCharacter)); + + for (int i = indexOfEscapeCharacter; i < name.Length;) { - if (NeedsEscapingInTypeName(c)) - sb.Append('\\'); - sb.Append(c); + char c = name[i++]; + + if (c != EscapeCharacter) + { + sb.Append(c); + } + else if (i < name.Length && name[i] == EscapeCharacter) // escaped escape character ;) + { + sb.Append(c); + i++; // escaped escape character followed by another escaped char like "\\\\\\+" + } } return sb.ToString(); } - private static string EscapeTypeName(string typeName, ReadOnlySpan nestedTypeNames) + /// + /// Initializes only when some unescaping of nested names is required. + /// + private static void UnescapeTypeNames(ReadOnlySpan names, ref string[]? unescapedNames) { - string fullName = EscapeTypeName(typeName); - if (nestedTypeNames.Length > 0) + if (names.IsEmpty) // nothing to check + { + return; + } + + int i = 0; + for (; i < names.Length; i++) { - var sb = new StringBuilder(fullName); - for (int i = 0; i < nestedTypeNames.Length; i++) + if (names[i].Contains(EscapeCharacter)) { - sb.Append('+'); - sb.Append(EscapeTypeName(nestedTypeNames[i])); + break; } - fullName = sb.ToString(); } - return fullName; + + if (i == names.Length) // nothing to escape + { + return; + } + + unescapedNames = new string[names.Length]; + for (int j = 0; j < i; j++) + { + unescapedNames[j] = names[j]; // copy what not needed escaping + } + for (; i < names.Length; i++) + { + unescapedNames[i] = UnescapeTypeName(names[i]); // escape the rest + } } private static (string typeNamespace, string name) SplitFullTypeName(string typeName) @@ -97,12 +132,12 @@ private static (string typeNamespace, string name) SplitFullTypeName(string type } string nonNestedParentName = current!.FullName; - Type? type = GetType(nonNestedParentName, nestedTypeNames, typeName.GetAssemblyName()); + Type? type = GetType(nonNestedParentName, nestedTypeNames, typeName.GetAssemblyName(), typeName.FullName); return Make(type, typeName); } else if (typeName.UnderlyingType is null) { - Type? type = GetType(typeName.FullName, nestedTypeNames: ReadOnlySpan.Empty, typeName.GetAssemblyName()); + Type? type = GetType(typeName.FullName, nestedTypeNames: ReadOnlySpan.Empty, typeName.GetAssemblyName(), typeName.FullName); return Make(type, typeName); } diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs index 8e9b463a63bc5..64cf3073c4469 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs @@ -58,14 +58,19 @@ public void GetGenericArgumentCountReturnsExpectedValue(string input, int expect [InlineData("ABCDE,otherType]]", 5, false)] [InlineData("Containing+Nested", 10, true)] [InlineData("NoSpecial.Characters", 20, false)] - // TODO adsitnik: add escaping handling + [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}"; - Assert.Equal(expected + withNamespace.Length - input.Length, TypeNameParserHelpers.GetFullTypeNameLength(withNamespace.AsSpan(), out isNested)); + int expectedWithNamespace = expected < 0 ? expected : expected + withNamespace.Length - input.Length; + Assert.Equal(expectedWithNamespace, TypeNameParserHelpers.GetFullTypeNameLength(withNamespace.AsSpan(), out isNested)); Assert.Equal(expectedIsNested, isNested); } @@ -148,6 +153,18 @@ public void GetIndexOfFirstInvalidTypeNameCharacter_ReturnsFirstInvalidCharacter } } + [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(TypeNameParserHelpers.SZArray, "[]")] [InlineData(TypeNameParserHelpers.Pointer, "*")] @@ -208,8 +225,12 @@ public void TrimStartTrimsAllWhitespaces(string input, string expectedResult) [Theory] [InlineData("A.B.C", true, null, 5, 0)] + [InlineData("A.B.C\\", false, null, 0, 0)] // invalid type name: ends with escape character + [InlineData("A.B.C\\DoeNotNeedEscaping", false, null, 0, 0)] // invalid type name: escapes non-special character + [InlineData("A.B+C", true, new int[] { 3 }, 5, 0)] + [InlineData("A.B++C", false, null, 0, 0)] // invalid type name: two following, unescaped + + [InlineData("A.B`1", true, null, 5, 1)] [InlineData("A+B`1+C1`2+DD2`3+E", true, new int[] { 1, 3, 4, 5 }, 18, 6)] - // TODO adsitnik: add escaping handling and more test cases public void TryGetTypeNameInfoGetsAllTheInfo(string input, bool expectedResult, int[] expectedNestedNameLengths, int expectedTotalLength, int expectedGenericArgCount) { diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs index ace591cce4e41..f7e32b823cfd8 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs @@ -60,6 +60,10 @@ public void EmptyStringsAreNotAllowed(string input) [InlineData("MissingClosingSquareBrackets`1[[type1, assembly1")] // missing ]] [InlineData("MissingClosingSquareBracket`1[[type1, assembly1]")] // missing ] [InlineData("CantMakeByRefToByRef&&")] + [InlineData("EscapeCharacterAtTheEnd\\")] + [InlineData("EscapeNonSpecialChar\\a")] + [InlineData("EscapeNonSpecialChar\\0")] + [InlineData("DoubleNestingChar++Bla")] public void InvalidTypeNamesAreNotAllowed(string input) { Assert.Throws(() => TypeName.Parse(input.AsSpan())); From 23ad44f22c6bd9aee9fd25b71b6fc156dd3f64f6 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 23 Feb 2024 09:52:08 +0100 Subject: [PATCH 25/48] fix the last failing tests, increase test coverage, fix the perf, fix the build --- .../Reflection/TypeNameParser.CoreCLR.cs | 13 +- .../Reflection/TypeNameParser.NativeAot.cs | 18 +-- .../Dataflow/TypeNameParser.Dataflow.cs | 2 +- .../Reflection/Metadata/TypeNameParser.cs | 43 +++-- .../Metadata/TypeNameParserHelpers.cs | 42 ++--- .../Metadata/TypeNameParserOptions.cs | 11 +- .../Reflection/TypeNameParser.Helpers.cs | 31 ++-- .../ref/System.Reflection.Metadata.cs | 3 +- .../src/System.Reflection.Metadata.csproj | 1 + .../Metadata/TypeNameParserHelpersTests.cs | 3 +- .../tests/Metadata/TypeNameParserSamples.cs | 2 +- .../tests/Metadata/TypeNameParserTests.cs | 149 ++++++++++++++++-- .../System/Type/TypeTests.cs | 18 +-- .../System/Reflection/TypeNameParser.Mono.cs | 6 +- 14 files changed, 222 insertions(+), 120 deletions(-) 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 eabc9b433e628..7d54706b89cbf 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 @@ -238,10 +238,8 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName, // Compat: Non-extensible parser allows ambiguous matches with ignore case lookup if (!_extensibleParser || !_ignoreCase) { - string[]? unescapedNestedNames = null; - UnescapeTypeNames(nestedTypeNames, ref unescapedNestedNames); return runtimeAssembly.GetTypeCore(UnescapeTypeName(escapedTypeName), - unescapedNestedNames is not null ? unescapedNestedNames : nestedTypeNames, + UnescapeTypeNames(nestedTypeNames) ?? nestedTypeNames, throwOnError: _throwOnError, ignoreCase: _ignoreCase); } type = runtimeAssembly.GetTypeCore(UnescapeTypeName(escapedTypeName), default, throwOnError: _throwOnError, ignoreCase: _ignoreCase); @@ -283,14 +281,13 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName, private Type? GetTypeFromDefaultAssemblies(string escapedTypeName, ReadOnlySpan nestedTypeNames, string fullEscapedName) { string unescapedTypeName = UnescapeTypeName(escapedTypeName); - string[]? unescapedNestedNames = null; - UnescapeTypeNames(nestedTypeNames, ref unescapedNestedNames); + string[]? unescapedNestedNames = UnescapeTypeNames(nestedTypeNames); RuntimeAssembly? requestingAssembly = (RuntimeAssembly?)_requestingAssembly; if (requestingAssembly is not null) { Type? type = ((RuntimeAssembly)requestingAssembly).GetTypeCore(unescapedTypeName, - unescapedNestedNames is null ? nestedTypeNames : unescapedNestedNames, throwOnError: false, ignoreCase: _ignoreCase); + unescapedNestedNames ?? nestedTypeNames, throwOnError: false, ignoreCase: _ignoreCase); if (type is not null) return type; } @@ -299,7 +296,7 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName, if (requestingAssembly != coreLib) { Type? type = ((RuntimeAssembly)coreLib).GetTypeCore(unescapedTypeName, - unescapedNestedNames is null ? nestedTypeNames : unescapedNestedNames, throwOnError: false, ignoreCase: _ignoreCase); + unescapedNestedNames ?? nestedTypeNames, throwOnError: false, ignoreCase: _ignoreCase); if (type is not null) return type; } @@ -308,7 +305,7 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName, if (resolvedAssembly is not null) { Type? type = resolvedAssembly.GetTypeCore(unescapedTypeName, - unescapedNestedNames is null ? nestedTypeNames : unescapedNestedNames, throwOnError: false, ignoreCase: _ignoreCase); + unescapedNestedNames ?? nestedTypeNames, throwOnError: false, ignoreCase: _ignoreCase); if (type is not null) return type; } 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 0ce5358f7128d..80af56bbedc9e 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 @@ -80,7 +80,7 @@ internal partial struct TypeNameParser { return null; } - else if (parsed.AssemblyName is not null && topLevelAssembly is not null) + else if (parsed.GetAssemblyName() is not null && topLevelAssembly is not null) { return throwOnError ? throw new ArgumentException(SR.Argument_AssemblyGetTypeCannotSpecifyAssembly) : null; } @@ -117,7 +117,7 @@ internal partial struct TypeNameParser 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, AssemblyName? assemblyNameIfAny) + private Type? GetType(string escapedTypeName, ReadOnlySpan nestedTypeNames, AssemblyName? assemblyNameIfAny, string _) { Assembly? assembly; @@ -137,8 +137,6 @@ internal partial struct TypeNameParser // Resolve the top level type. if (_typeResolver is not null) { - string escapedTypeName = EscapeTypeName(typeName); - type = _typeResolver(assembly, escapedTypeName, _ignoreCase); if (type is null) @@ -158,14 +156,14 @@ internal partial struct TypeNameParser { 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) @@ -179,7 +177,7 @@ internal partial struct TypeNameParser defaultAssembly = RuntimeAssemblyInfo.GetRuntimeAssemblyIfExists(RuntimeAssemblyName.Parse(_defaultAssemblyName)); if (defaultAssembly != null) { - type = defaultAssembly.GetTypeCore(typeName, throwOnError: false, ignoreCase: _ignoreCase); + type = defaultAssembly.GetTypeCore(UnescapeTypeName(escapedTypeName), throwOnError: false, ignoreCase: _ignoreCase); } } @@ -189,7 +187,7 @@ internal partial struct TypeNameParser coreLib = (RuntimeAssemblyInfo)typeof(object).Assembly; if (coreLib != assembly) { - type = coreLib.GetTypeCore(typeName, throwOnError: false, ignoreCase: _ignoreCase); + type = coreLib.GetTypeCore(UnescapeTypeName(escapedTypeName), throwOnError: false, ignoreCase: _ignoreCase); } } @@ -197,7 +195,7 @@ internal partial struct TypeNameParser { if (_throwOnError) { - throw Helpers.CreateTypeLoadException(typeName, (defaultAssembly ?? coreLib).FullName); + throw Helpers.CreateTypeLoadException(UnescapeTypeName(escapedTypeName), (defaultAssembly ?? coreLib).FullName); } return null; } @@ -234,7 +232,7 @@ internal partial struct TypeNameParser 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/aot/ILCompiler.Compiler/Compiler/Dataflow/TypeNameParser.Dataflow.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/Dataflow/TypeNameParser.Dataflow.cs index df435c0e3204d..231873cf303e5 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 @@ -55,7 +55,7 @@ public Type MakeGenericType(Type[] typeArguments) } } - private Type GetType(string typeName, ReadOnlySpan nestedTypeNames, AssemblyName assemblyNameIfAny) + private Type GetType(string typeName, ReadOnlySpan nestedTypeNames, AssemblyName assemblyNameIfAny, string _) { ModuleDesc module; diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs index 049b735f0e63e..c0b3a0cd4b90b 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs @@ -1,17 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if SYSTEM_PRIVATE_CORELIB -#define NET8_0_OR_GREATER -#endif using System.Collections.Generic; using System.Diagnostics; - -#if SYSTEM_PRIVATE_CORELIB -using StringBuilder = System.Text.ValueStringBuilder; -#else -using StringBuilder = System.Text.StringBuilder; -#endif +using System.Text; using static System.Reflection.Metadata.TypeNameParserHelpers; @@ -59,7 +51,7 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse // there was an error and we need to throw #if !SYSTEM_PRIVATE_CORELIB - if (recursiveDepth >= parser._parseOptions.MaxRecursiveDepth) + if (recursiveDepth >= parser._parseOptions.MaxTotalComplexity) { throw new InvalidOperationException("SR.RecursionCheck_MaxDepthExceeded"); } @@ -91,7 +83,7 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse } List? nestedNameLengths = null; - if (!TryGetTypeNameInfo(_inputString, ref nestedNameLengths, out int fullTypeNameLength, out int genericArgCount)) + if (!TryGetTypeNameInfo(ref _inputString, ref nestedNameLengths, out int fullTypeNameLength, out int genericArgCount)) { return null; } @@ -132,6 +124,8 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse recursiveDepth = startingRecursionCheck; // Namespace.Type`1[[GenericArgument1, AssemblyName1],[GenericArgument2, AssemblyName2]] - double square bracket syntax allows for fully qualified type names // Namespace.Type`1[GenericArgument1,GenericArgument2] - single square bracket syntax is legal only for non-fully qualified type names + // Namespace.Type`1[[GenericArgument1, AssemblyName1], GenericArgument2] - mixed mode + // Namespace.Type`1[GenericArgument1, [GenericArgument2, AssemblyName2]] - mixed mode TypeName? genericArg = ParseNextTypeName(allowFullyQualifiedName: doubleBrackets, ref recursiveDepth); if (genericArg is null) // parsing failed { @@ -153,9 +147,9 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse { // Parsing the rest would hit the limit. // -1 because the first generic arg has been already parsed. - if (maxObservedRecursionCheck + genericArgCount - 1 > _parseOptions.MaxRecursiveDepth) + if (maxObservedRecursionCheck + genericArgCount - 1 > _parseOptions.MaxTotalComplexity) { - recursiveDepth = _parseOptions.MaxRecursiveDepth; + recursiveDepth = _parseOptions.MaxTotalComplexity; return null; } @@ -163,14 +157,11 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse } genericArgs[genericArgIndex++] = genericArg; + // Is there a ',[' indicating fully qualified generic type arg? + // Is there a ',' indicating non-fully qualified generic type arg? if (TryStripFirstCharAndTrailingSpaces(ref _inputString, ',')) { - // For [[, is there a ',[' indicating another generic type arg? - // For [, it's just a ',' - if (doubleBrackets && !TryStripFirstCharAndTrailingSpaces(ref _inputString, '[')) - { - return null; - } + doubleBrackets = TryStripFirstCharAndTrailingSpaces(ref _inputString, '['); goto ParseAnotherGenericArg; } @@ -246,9 +237,10 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse if (previousDecorator != default) // some decorators were recognized { - StringBuilder fullNameSb = new(genericTypeFullName.Length + 4); + ValueStringBuilder fullNameSb = new(stackalloc char[128]); fullNameSb.Append(genericTypeFullName); - StringBuilder nameSb = new(name.Length + 4); + + ValueStringBuilder nameSb = new(stackalloc char[32]); nameSb.Append(name); while (TryParseNextDecorator(ref capturedBeforeProcessing, out int parsedModifier)) @@ -258,8 +250,13 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse nameSb.Append(trimmedModifier); fullNameSb.Append(trimmedModifier); - result = new(nameSb.ToString(), fullNameSb.ToString(), assemblyName, parsedModifier, underlyingType: result); + result = new(nameSb.AsSpan().ToString(), fullNameSb.AsSpan().ToString(), assemblyName, parsedModifier, underlyingType: result); } + + // The code above is not calling ValueStringBuilder.ToString() directly, + // because it calls Dispose and we want to reuse the builder content until we are done with all decorators. + fullNameSb.Dispose(); + nameSb.Dispose(); } return result; @@ -339,7 +336,7 @@ private bool TryParseAssemblyName(ref AssemblyName? assemblyName) private bool TryDive(ref int depth) { - if (depth >= _parseOptions.MaxRecursiveDepth) + if (depth >= _parseOptions.MaxTotalComplexity) { return false; } diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs index 554096c0e969b..389b930452bb1 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs @@ -1,18 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if SYSTEM_PRIVATE_CORELIB -#define NET8_0_OR_GREATER -#endif using System.Buffers; using System.Collections.Generic; using System.Diagnostics; - -#if SYSTEM_PRIVATE_CORELIB -using StringBuilder = System.Text.ValueStringBuilder; -#else -using StringBuilder = System.Text.StringBuilder; -#endif +using System.Text; using static System.Array; using static System.Char; @@ -92,21 +84,9 @@ internal static string GetGenericTypeFullName(ReadOnlySpan fullTypeName, T return fullTypeName.ToString(); } - int size = fullTypeName.Length + 1; - foreach (TypeName genericArg in genericArgs) - { - size += 3 + genericArg.AssemblyQualifiedName.Length; - } - - StringBuilder result = new(size); -#if NET8_0_OR_GREATER + ValueStringBuilder result = new(stackalloc char[128]); result.Append(fullTypeName); -#else - for (int i = 0; i < fullTypeName.Length; i++) - { - result.Append(fullTypeName[i]); - } -#endif + result.Append('['); foreach (TypeName genericArg in genericArgs) { @@ -549,7 +529,7 @@ static string ArrayRankToString(int arrayRank) buffer[^1] = ']'; }); #else - StringBuilder sb = new(2 + arrayRank - 1); + ValueStringBuilder sb = new(stackalloc char[16]); sb.Append('['); for (int i = 1; i < arrayRank; i++) sb.Append(','); @@ -591,7 +571,7 @@ internal static bool IsBeginningOfGenericAgs(ref ReadOnlySpan span, out bo internal static ReadOnlySpan TrimStart(ReadOnlySpan input) => input.TrimStart(); - internal static bool TryGetTypeNameInfo(ReadOnlySpan input, ref List? nestedNameLengths, + internal static bool TryGetTypeNameInfo(ref ReadOnlySpan input, ref List? nestedNameLengths, out int totalLength, out int genericArgCount) { bool isNestedType; @@ -610,6 +590,18 @@ internal static bool TryGetTypeNameInfo(ReadOnlySpan input, ref List? 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 + int generics = GetGenericArgumentCount(input.Slice(totalLength, length)); if (generics < 0) { diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs index 8c66ce61bac1d..c957817144768 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs @@ -10,13 +10,16 @@ namespace System.Reflection.Metadata #endif sealed class TypeNameParserOptions { - private int _maxRecursiveDepth = int.MaxValue; + private int _maxComplexity = int.MaxValue; public bool AllowFullyQualifiedName { get; set; } = true; - public int MaxRecursiveDepth + /// + /// Limits the maximum value of that parser can handle. + /// + public int MaxTotalComplexity { - get => _maxRecursiveDepth; + get => _maxComplexity; set { #if NET8_0_OR_GREATER @@ -28,7 +31,7 @@ public int MaxRecursiveDepth } #endif - _maxRecursiveDepth = value; + _maxComplexity = value; } } diff --git a/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs index e20faf4937183..292a9481ef3ed 100644 --- a/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs +++ b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs @@ -12,18 +12,6 @@ internal partial struct TypeNameParser { private const char EscapeCharacter = '\\'; -#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 UnescapeTypeName(string name) { int indexOfEscapeCharacter = name.IndexOf(EscapeCharacter); @@ -47,7 +35,9 @@ private static string UnescapeTypeName(string name) else if (i < name.Length && name[i] == EscapeCharacter) // escaped escape character ;) { sb.Append(c); - i++; // escaped escape character followed by another escaped char like "\\\\\\+" + // Consume the escaped escape character, it's important for edge cases + // like escaped escape character followed by another escaped char (example: "\\\\\\+") + i++; } } @@ -55,19 +45,23 @@ private static string UnescapeTypeName(string name) } /// - /// Initializes only when some unescaping of nested names is required. + /// Returns non-null array when some unescaping of nested names was required. /// - private static void UnescapeTypeNames(ReadOnlySpan names, ref string[]? unescapedNames) + private static string[]? UnescapeTypeNames(ReadOnlySpan names) { if (names.IsEmpty) // nothing to check { - return; + return null; } int i = 0; for (; i < names.Length; i++) { +#if NETCOREAPP if (names[i].Contains(EscapeCharacter)) +#else + if (names[i].IndexOf(EscapeCharacter) >= 0) +#endif { break; } @@ -75,10 +69,10 @@ private static void UnescapeTypeNames(ReadOnlySpan names, ref string[]? if (i == names.Length) // nothing to escape { - return; + return null; } - unescapedNames = new string[names.Length]; + string[] unescapedNames = new string[names.Length]; for (int j = 0; j < i; j++) { unescapedNames[j] = names[j]; // copy what not needed escaping @@ -87,6 +81,7 @@ private static void UnescapeTypeNames(ReadOnlySpan names, ref string[]? { unescapedNames[i] = UnescapeTypeName(names[i]); // escape the rest } + return unescapedNames; } private static (string typeNamespace, string name) SplitFullTypeName(string typeName) 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 9f7794f616541..42712f97cfcbc 100644 --- a/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs +++ b/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs @@ -2413,6 +2413,7 @@ public sealed partial class TypeName internal TypeName() { } public string AssemblyQualifiedName { get { throw null; } } public System.Reflection.Metadata.TypeName? ContainingType { get { throw null; } } + public string FullName { get { throw null; } } public bool IsArray { get { throw null; } } public bool IsConstructedGenericType { get { throw null; } } public bool IsElementalType { get { throw null; } } @@ -2434,7 +2435,7 @@ public sealed partial class TypeNameParserOptions { public TypeNameParserOptions() { } public bool AllowFullyQualifiedName { get { throw null; } set { } } - public int MaxRecursiveDepth { get { throw null; } set { } } + public int MaxTotalComplexity { get { throw null; } set { } } public bool StrictValidation { get; set; } } public readonly partial struct TypeReference 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 ee149cad89c6e..4de74c3b0da6c 100644 --- a/src/libraries/System.Reflection.Metadata/src/System.Reflection.Metadata.csproj +++ b/src/libraries/System.Reflection.Metadata/src/System.Reflection.Metadata.csproj @@ -260,6 +260,7 @@ The System.Reflection.Metadata library is built-in as part of the shared framewo + diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs index 64cf3073c4469..9bd263544e394 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs @@ -235,7 +235,8 @@ public void TryGetTypeNameInfoGetsAllTheInfo(string input, bool expectedResult, int expectedTotalLength, int expectedGenericArgCount) { List? nestedNameLengths = null; - bool result = TypeNameParserHelpers.TryGetTypeNameInfo(input.AsSpan(), ref nestedNameLengths, out int totalLength, out int genericArgCount); + ReadOnlySpan span = input.AsSpan(); + bool result = TypeNameParserHelpers.TryGetTypeNameInfo(ref span, ref nestedNameLengths, out int totalLength, out int genericArgCount); Assert.Equal(expectedResult, result); Assert.Equal(expectedNestedNameLengths, nestedNameLengths?.ToArray()); diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs index e8219e288ed9f..b8b0a86838ae7 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs @@ -67,7 +67,7 @@ public SampleSerializationBinder(Type[]? allowedTypes = null) // 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. - MaxRecursiveDepth = 10 + MaxTotalComplexity = 10 }; if (!TypeName.TryParse(typeName.AsSpan(), out TypeName parsed, _options)) diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs index f7e32b823cfd8..be9d4bd7b5ff1 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Reflection.Emit; using Xunit; namespace System.Reflection.Metadata.Tests @@ -22,6 +23,18 @@ public void SpacesAtTheBeginningAreOK(string input, string expectedFullName, str 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(" ")] @@ -59,6 +72,10 @@ public void EmptyStringsAreNotAllowed(string input) [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("CantMakeByRefToByRef&&")] [InlineData("EscapeCharacterAtTheEnd\\")] [InlineData("EscapeNonSpecialChar\\a")] @@ -150,11 +167,11 @@ public void CanNotParseTypeWithInvalidAssemblyName(string fullName) [InlineData(10, "[]")] // array of arrays [InlineData(100, "*")] [InlineData(100, "[]")] - public void MaxRecursiveDepthIsRespected_TooManyDecorators(int maxDepth, string decorator) + public void MaxTotalComplexityIsRespected_TooManyDecorators(int maxDepth, string decorator) { TypeNameParserOptions options = new() { - MaxRecursiveDepth = maxDepth + MaxTotalComplexity = maxDepth }; string notTooMany = $"System.Int32{string.Join("", Enumerable.Repeat(decorator, maxDepth - 1))}"; @@ -185,11 +202,11 @@ static void ValidateUnderlyingType(int maxDepth, TypeName parsed, string decorat [Theory] [InlineData(10)] [InlineData(100)] - public void MaxRecursiveDepthIsRespected_TooDeepGenerics(int maxDepth) + public void MaxTotalComplexityIsRespected_TooDeepGenerics(int maxDepth) { TypeNameParserOptions options = new() { - MaxRecursiveDepth = maxDepth + MaxTotalComplexity = maxDepth }; string tooDeep = GetName(maxDepth); @@ -229,11 +246,11 @@ static void Validate(int maxDepth, TypeName parsed) [Theory] [InlineData(10)] [InlineData(100)] - public void MaxRecursiveDepthIsRespected_TooManyGenericArguments(int maxDepth) + public void MaxTotalComplexityIsRespected_TooManyGenericArguments(int maxDepth) { TypeNameParserOptions options = new() { - MaxRecursiveDepth = maxDepth + MaxTotalComplexity = maxDepth }; string tooMany = GetName(maxDepth); @@ -266,6 +283,15 @@ public static IEnumerable GenericArgumentsAreSupported_Arguments() { "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 }; @@ -273,6 +299,15 @@ public static IEnumerable GenericArgumentsAreSupported_Arguments() { "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 }; @@ -280,6 +315,7 @@ public static IEnumerable GenericArgumentsAreSupported_Arguments() { "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") } }; @@ -287,6 +323,7 @@ public static IEnumerable GenericArgumentsAreSupported_Arguments() { "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[] { @@ -294,16 +331,66 @@ public static IEnumerable GenericArgumentsAreSupported_Arguments() 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[] genericTypesFullNames, AssemblyName[]? assemblyNames) + 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(input, parsed.FullName); + Assert.Equal(fullName, parsed.FullName); Assert.True(parsed.IsConstructedGenericType); Assert.False(parsed.IsElementalType); @@ -316,7 +403,14 @@ public void GenericArgumentsAreSupported(string input, string name, string[] gen if (assemblyNames is not null) { - Assert.Equal(assemblyNames[i].FullName, genericArg.GetAssemblyName().FullName); + if (assemblyNames[i] is null) + { + Assert.Null(genericArg.GetAssemblyName()); + } + else + { + Assert.Equal(assemblyNames[i].FullName, genericArg.GetAssemblyName().FullName); + } } } } @@ -418,6 +512,31 @@ public void TotalComplexityReturnsExpectedValue(Type type, int expectedComplexit Assert.Equal(type.AssemblyQualifiedName, parsed.AssemblyQualifiedName); } + public static IEnumerable GetTypesThatRequireEscaping() + { + if (PlatformDetection.IsReflectionEmitSupported) + { + 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>))] @@ -426,6 +545,7 @@ public void TotalComplexityReturnsExpectedValue(Type type, int expectedComplexit [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()); @@ -434,10 +554,13 @@ public void ParsedNamesMatchSystemTypeNames(Type type) Assert.Equal(type.FullName, parsed.FullName); Assert.Equal(type.AssemblyQualifiedName, parsed.AssemblyQualifiedName); - Type genericType = type.GetGenericTypeDefinition(); - Assert.Equal(genericType.Name, parsed.UnderlyingType.Name); - Assert.Equal(genericType.FullName, parsed.UnderlyingType.FullName); - Assert.Equal(genericType.AssemblyQualifiedName, parsed.UnderlyingType.AssemblyQualifiedName); + if (type.IsGenericType) + { + Type genericType = type.GetGenericTypeDefinition(); + Assert.Equal(genericType.Name, parsed.UnderlyingType.Name); + Assert.Equal(genericType.FullName, parsed.UnderlyingType.FullName); + Assert.Equal(genericType.AssemblyQualifiedName, parsed.UnderlyingType.AssemblyQualifiedName); + } } [Theory] diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Type/TypeTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Type/TypeTests.cs index 13fac33e4eb39..b1d27bae01ed2 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Type/TypeTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Type/TypeTests.cs @@ -515,21 +515,17 @@ public void GetTypeByName_ValidType_ReturnsExpected(string typeName, Type expect public static IEnumerable GetTypeByName_InvalidElementType() { - Type expectedException = PlatformDetection.IsMonoRuntime - ? typeof(ArgumentException) // https://github.com/dotnet/runtime/issues/45033 - : typeof(TypeLoadException); - - yield return new object[] { "System.Int32&&", expectedException, true }; - yield return new object[] { "System.Int32&*", expectedException, true }; - yield return new object[] { "System.Int32&[]", expectedException, true }; - yield return new object[] { "System.Int32&[*]", expectedException, true }; - yield return new object[] { "System.Int32&[,]", expectedException, true }; + yield return new object[] { "System.Int32&&", typeof(ArgumentException), true }; + yield return new object[] { "System.Int32&*", typeof(ArgumentException), true }; + yield return new object[] { "System.Int32&[]", typeof(ArgumentException), true }; + yield return new object[] { "System.Int32&[*]", typeof(ArgumentException), true }; + yield return new object[] { "System.Int32&[,]", typeof(ArgumentException), true }; // https://github.com/dotnet/runtime/issues/45033 if (!PlatformDetection.IsMonoRuntime) { - yield return new object[] { "System.Void[]", expectedException, true }; - yield return new object[] { "System.TypedReference[]", expectedException, true }; + yield return new object[] { "System.Void[]", typeof(TypeLoadException), true }; + yield return new object[] { "System.TypedReference[]", typeof(TypeLoadException), true }; } } 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 bd88f12846774..7ebf7082578a6 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 @@ -95,13 +95,11 @@ internal unsafe ref partial struct TypeNameParser 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, AssemblyName? assemblyNameIfAny) + private Type? GetType(string escapedTypeName, ReadOnlySpan nestedTypeNames, AssemblyName? assemblyNameIfAny, string _) { Assembly? assembly = (assemblyNameIfAny is not null) ? ResolveAssembly(assemblyNameIfAny) : 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. @@ -152,7 +150,7 @@ internal unsafe ref partial struct TypeNameParser 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; } From c8014fdbdc7b4a2f464c7238ea422f7b3679fd7e Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 19 Mar 2024 14:15:51 +0100 Subject: [PATCH 26/48] apply changes based on the 1st API Design Review --- .../System/Reflection/Metadata/TypeName.cs | 68 ++++++++++------- .../Metadata/TypeNameParserOptions.cs | 2 +- .../Reflection/TypeNameParser.Helpers.cs | 38 ++++++---- .../ref/System.Reflection.Metadata.cs | 17 +++-- .../tests/Metadata/TypeNameParserSamples.cs | 8 +- .../tests/Metadata/TypeNameParserTests.cs | 76 ++++++++++--------- 6 files changed, 122 insertions(+), 87 deletions(-) diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs index 92af60924dc0c..c7e3500b3fa1b 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs @@ -23,6 +23,7 @@ sealed class TypeName private readonly int _rankOrModifier; private readonly TypeName[]? _genericArguments; private readonly AssemblyName? _assemblyName; + private readonly TypeName? _underlyingType; private string? _assemblyQualifiedName; internal TypeName(string name, string fullName, @@ -36,10 +37,13 @@ internal TypeName(string name, string fullName, FullName = fullName; _assemblyName = assemblyName; _rankOrModifier = rankOrModifier; - UnderlyingType = underlyingType; - ContainingType = containingType; + _underlyingType = underlyingType; + DeclaringType = containingType; _genericArguments = genericTypeArguments; - TotalComplexity = GetTotalComplexity(underlyingType, containingType, genericTypeArguments); + Complexity = GetComplexity(underlyingType, containingType, genericTypeArguments); + + Debug.Assert(!(IsArray || IsPointer || IsByRef) || _underlyingType is not null); + Debug.Assert(_genericArguments is null || _underlyingType is not null); } /// @@ -52,13 +56,13 @@ public string AssemblyQualifiedName => _assemblyQualifiedName ??= _assemblyName is null ? FullName : $"{FullName}, {_assemblyName.FullName}"; /// - /// If this type is a nested type (see ), gets - /// the containing type. If this type is not a nested type, returns null. + /// If this type is a nested type (see ), gets + /// the declaring type. If this type is not a nested type, returns null. /// /// - /// For example, given "Namespace.Containing+Nested", unwraps the outermost type and returns "Namespace.Containing". + /// For example, given "Namespace.Declaring+Nested", unwraps the outermost type and returns "Namespace.Declaring". /// - public TypeName? ContainingType { get; } + public TypeName? DeclaringType { get; } /// /// The full name of this type, including namespace, but without the assembly name; e.g., "System.Int32". @@ -97,35 +101,35 @@ public string AssemblyQualifiedName /// /// /// 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 ( returns null). + /// 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 IsElementalType => UnderlyingType is null; + public bool IsElementalType => _underlyingType 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 IsManagedPointerType => _rankOrModifier == TypeNameParserHelpers.ByRef; // name inconsistent with Type.IsByRef + public bool IsByRef => _rankOrModifier == TypeNameParserHelpers.ByRef; /// /// Returns true if this is a nested type (e.g., "Namespace.Containing+Nested"). - /// For nested types returns their containing type. + /// For nested types returns their containing type. /// - public bool IsNestedType => ContainingType is not null; + public bool IsNested => DeclaringType is not null; /// /// Returns true if this type represents a single-dimensional, zero-indexed array (e.g., "int[]"). /// - public bool IsSzArrayType => _rankOrModifier == TypeNameParserHelpers.SZArray; // name could be more user-friendly + 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 IsUnmanagedPointerType => _rankOrModifier == TypeNameParserHelpers.Pointer; // name inconsistent with Type.IsPointer + public bool IsPointer => _rankOrModifier == TypeNameParserHelpers.Pointer; /// /// Returns true if this type represents a variable-bound array; that is, an array of rank greater @@ -166,17 +170,29 @@ public string AssemblyQualifiedName /// /// /// - public int TotalComplexity { get; } + public int Complexity { get; } /// - /// If this type is not an elemental type (see ), gets - /// the underlying type. If this type is an elemental type, returns null. + /// 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[]". + /// + public TypeName GetElementType() + => IsArray || IsPointer || IsByRef + ? _underlyingType! + : throw new InvalidOperationException(); + + /// + /// 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<,>". /// - public TypeName? UnderlyingType { get; } + public TypeName GetGenericTypeDefinition() + => IsConstructedGenericType + ? _underlyingType! + : throw new InvalidOperationException("SR.InvalidOperation_NotGenericType"); // TODO: use actual resource public static TypeName Parse(ReadOnlySpan typeName, TypeNameParserOptions? options = default) => TypeNameParser.Parse(typeName, throwOnError: true, options)!; @@ -219,28 +235,28 @@ public int GetArrayRank() } /// - /// If this represents a constructed generic type, returns a span - /// of all the generic arguments. Otherwise it returns an empty span. + /// If this represents a constructed generic type, returns a buffer + /// of all the generic arguments. Otherwise it returns an empty buffer. /// /// /// For example, given "Dictionary<string, int>", returns a 2-element span containing /// string and int. /// - public ReadOnlySpan GetGenericArguments() - => _genericArguments is null ? ReadOnlySpan.Empty : _genericArguments.AsSpan(); + public ReadOnlyMemory GetGenericArguments() + => _genericArguments is null ? ReadOnlyMemory.Empty : _genericArguments.AsMemory(); - private static int GetTotalComplexity(TypeName? underlyingType, TypeName? containingType, TypeName[]? genericTypeArguments) + private static int GetComplexity(TypeName? underlyingType, TypeName? containingType, TypeName[]? genericTypeArguments) { int result = 1; if (underlyingType is not null) { - result = checked(result + underlyingType.TotalComplexity); + result = checked(result + underlyingType.Complexity); } if (containingType is not null) { - result = checked(result + containingType.TotalComplexity); + result = checked(result + containingType.Complexity); } if (genericTypeArguments is not null) @@ -251,7 +267,7 @@ private static int GetTotalComplexity(TypeName? underlyingType, TypeName? contai // - and the cumulative complexity of all the arguments foreach (TypeName genericArgument in genericTypeArguments) { - result = checked(result + genericArgument.TotalComplexity); + result = checked(result + genericArgument.Complexity); } } diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs index c957817144768..382164d21bac9 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs @@ -15,7 +15,7 @@ sealed class TypeNameParserOptions public bool AllowFullyQualifiedName { get; set; } = true; /// - /// Limits the maximum value of that parser can handle. + /// Limits the maximum value of that parser can handle. /// public int MaxTotalComplexity { diff --git a/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs index 292a9481ef3ed..ab3eb5bbc41fd 100644 --- a/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs +++ b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs @@ -108,36 +108,42 @@ private static (string typeNamespace, string name) SplitFullTypeName(string type private Type? Resolve(Metadata.TypeName typeName) { - if (typeName.IsNestedType) + if (typeName.IsNested) { Metadata.TypeName? current = typeName; int nestingDepth = 0; - while (current is not null && current.IsNestedType) + while (current is not null && current.IsNested) { nestingDepth++; - current = current.ContainingType; + current = current.DeclaringType; } string[] nestedTypeNames = new string[nestingDepth]; current = typeName; - while (current is not null && current.IsNestedType) + while (current is not null && current.IsNested) { nestedTypeNames[--nestingDepth] = current.Name; - current = current.ContainingType; + current = current.DeclaringType; } string nonNestedParentName = current!.FullName; Type? type = GetType(nonNestedParentName, nestedTypeNames, typeName.GetAssemblyName(), typeName.FullName); return Make(type, typeName); } - else if (typeName.UnderlyingType is null) + else if (typeName.IsConstructedGenericType) + { + return Make(Resolve(typeName.GetGenericTypeDefinition()), typeName); + } + else if (typeName.IsArray || typeName.IsPointer || typeName.IsByRef) + { + return Make(Resolve(typeName.GetElementType()), typeName); + } + else { Type? type = GetType(typeName.FullName, nestedTypeNames: ReadOnlySpan.Empty, typeName.GetAssemblyName(), typeName.FullName); return Make(type, typeName); } - - return Make(Resolve(typeName.UnderlyingType), typeName); } #if !NETSTANDARD2_0 // needed for ILVerification project @@ -146,13 +152,13 @@ private static (string typeNamespace, string name) SplitFullTypeName(string type #endif private Type? Make(Type? type, Metadata.TypeName typeName) { - if (type is null || typeName.IsElementalType) + if (type is null) { return type; } else if (typeName.IsConstructedGenericType) { - ReadOnlySpan genericArgs = typeName.GetGenericArguments(); + ReadOnlySpan genericArgs = typeName.GetGenericArguments().Span; Type[] genericTypes = new Type[genericArgs.Length]; for (int i = 0; i < genericArgs.Length; i++) { @@ -166,22 +172,26 @@ private static (string typeNamespace, string name) SplitFullTypeName(string type return type.MakeGenericType(genericTypes); } - else if (typeName.IsManagedPointerType) + else if (typeName.IsByRef) { return type.MakeByRefType(); } - else if (typeName.IsUnmanagedPointerType) + else if (typeName.IsPointer) { return type.MakePointerType(); } - else if (typeName.IsSzArrayType) + else if (typeName.IsSZArray) { return type.MakeArrayType(); } - else + else if(typeName.IsArray) { return type.MakeArrayType(rank: typeName.GetArrayRank()); } + else + { + return type; + } } } } 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 42712f97cfcbc..e7b597e1a39a2 100644 --- a/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs +++ b/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs @@ -2412,24 +2412,25 @@ public sealed partial class TypeName { internal TypeName() { } public string AssemblyQualifiedName { get { throw null; } } - public System.Reflection.Metadata.TypeName? ContainingType { 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 IsConstructedGenericType { get { throw null; } } public bool IsElementalType { get { throw null; } } - public bool IsManagedPointerType { get { throw null; } } - public bool IsNestedType { get { throw null; } } - public bool IsSzArrayType { get { throw null; } } - public bool IsUnmanagedPointerType { get { throw null; } } + public bool IsByRef { get { throw null; } } + public bool IsNested { get { throw null; } } + public bool IsSZArray { get { throw null; } } + public bool IsPointer { get { throw null; } } public bool IsVariableBoundArrayType { get { throw null; } } public string Name { get { throw null; } } - public int TotalComplexity { get; } - public System.Reflection.Metadata.TypeName? UnderlyingType { get { throw null; } } + public int Complexity { get; } public static System.Reflection.Metadata.TypeName Parse(System.ReadOnlySpan typeName, System.Reflection.Metadata.TypeNameParserOptions? 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.TypeNameParserOptions? options = null) { throw null; } public int GetArrayRank() { throw null; } public System.Reflection.AssemblyName? GetAssemblyName() { throw null; } - public System.ReadOnlySpan GetGenericArguments() { throw null; } + public System.ReadOnlyMemory GetGenericArguments() { throw null; } + public System.Reflection.Metadata.TypeName GetGenericTypeDefinition() { throw null; } + public System.Reflection.Metadata.TypeName GetElementType() { throw null; } } public sealed partial class TypeNameParserOptions { diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs index b8b0a86838ae7..13a3c8c371199 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs @@ -87,20 +87,20 @@ public SampleSerializationBinder(Type[]? allowedTypes = null) } else if (parsed.IsArray) { - TypeName arrayElementTypeName = parsed.UnderlyingType; // equivalent of type.GetElementType() + TypeName arrayElementTypeName = parsed.GetElementType(); Type arrayElementType = GetTypeFromParsedTypeName(arrayElementTypeName); // recursive call allows for creating arrays of arrays etc - return parsed.IsSzArrayType + return parsed.IsSZArray ? arrayElementType.MakeArrayType() : arrayElementType.MakeArrayType(parsed.GetArrayRank()); } else if (parsed.IsConstructedGenericType) { - TypeName genericTypeDefinitionName = parsed.UnderlyingType; // equivalent of type.GetGenericTypeDefinition() + TypeName genericTypeDefinitionName = parsed.GetGenericTypeDefinition(); Type genericTypeDefinition = GetTypeFromParsedTypeName(genericTypeDefinitionName); Debug.Assert(genericTypeDefinition.IsGenericTypeDefinition); - ReadOnlySpan genericArgs = parsed.GetGenericArguments(); + ReadOnlySpan genericArgs = parsed.GetGenericArguments().Span; Type[] typeArguments = new Type[genericArgs.Length]; for (int i = 0; i < genericArgs.Length; i++) { diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs index be9d4bd7b5ff1..c4f575c2df9a0 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs @@ -181,20 +181,20 @@ public void MaxTotalComplexityIsRespected_TooManyDecorators(int maxDepth, string Assert.False(TypeName.TryParse(tooMany.AsSpan(), out _, options)); TypeName parsed = TypeName.Parse(notTooMany.AsSpan(), options); - ValidateUnderlyingType(maxDepth, parsed, decorator); + ValidateElementType(maxDepth, parsed, decorator); Assert.True(TypeName.TryParse(notTooMany.AsSpan(), out parsed, options)); - ValidateUnderlyingType(maxDepth, parsed, decorator); + ValidateElementType(maxDepth, parsed, decorator); - static void ValidateUnderlyingType(int maxDepth, TypeName parsed, string decorator) + static void ValidateElementType(int maxDepth, TypeName parsed, string decorator) { for (int i = 0; i < maxDepth - 1; i++) { - Assert.Equal(decorator == "*", parsed.IsUnmanagedPointerType); - Assert.Equal(decorator == "[]", parsed.IsSzArrayType); + Assert.Equal(decorator == "*", parsed.IsPointer); + Assert.Equal(decorator == "[]", parsed.IsSZArray); Assert.False(parsed.IsConstructedGenericType); - parsed = parsed.UnderlyingType; + parsed = parsed.GetElementType(); } } } @@ -238,7 +238,7 @@ static void Validate(int maxDepth, TypeName parsed) for (int i = 0; i < maxDepth - 1; i++) { Assert.True(parsed.IsConstructedGenericType); - parsed = parsed.GetGenericArguments()[0]; + parsed = parsed.GetGenericArguments().Span[0]; } } } @@ -396,7 +396,7 @@ public void GenericArgumentsAreSupported(string input, string name, string fullN for (int i = 0; i < genericTypesFullNames.Length; i++) { - TypeName genericArg = parsed.GetGenericArguments()[i]; + TypeName genericArg = parsed.GetGenericArguments().Span[i]; Assert.Equal(genericTypesFullNames[i], genericArg.FullName); Assert.True(genericArg.IsElementalType); Assert.False(genericArg.IsConstructedGenericType); @@ -447,21 +447,21 @@ public void DecoratorsAreSupported(string input, string typeNameWithoutDecorator Assert.Equal(input, parsed.FullName); Assert.Equal(isArray, parsed.IsArray); - Assert.Equal(isSzArray, parsed.IsSzArrayType); + Assert.Equal(isSzArray, parsed.IsSZArray); if (isArray) Assert.Equal(arrayRank, parsed.GetArrayRank()); - Assert.Equal(isByRef, parsed.IsManagedPointerType); - Assert.Equal(isPointer, parsed.IsUnmanagedPointerType); + Assert.Equal(isByRef, parsed.IsByRef); + Assert.Equal(isPointer, parsed.IsPointer); Assert.False(parsed.IsElementalType); - TypeName underlyingType = parsed.UnderlyingType; - Assert.NotNull(underlyingType); - Assert.Equal(typeNameWithoutDecorators, underlyingType.FullName); - Assert.True(underlyingType.IsElementalType); - Assert.False(underlyingType.IsArray); - Assert.False(underlyingType.IsSzArrayType); - Assert.False(underlyingType.IsManagedPointerType); - Assert.False(underlyingType.IsUnmanagedPointerType); - Assert.Null(underlyingType.UnderlyingType); + TypeName elementType = parsed.GetElementType(); + Assert.NotNull(elementType); + Assert.Equal(typeNameWithoutDecorators, elementType.FullName); + Assert.True(elementType.IsElementalType); + Assert.False(elementType.IsArray); + Assert.False(elementType.IsSZArray); + Assert.False(elementType.IsByRef); + Assert.False(elementType.IsPointer); + Assert.Throws(elementType.GetElementType); } public static IEnumerable GetAdditionalConstructedTypeData() @@ -505,7 +505,7 @@ public void TotalComplexityReturnsExpectedValue(Type type, int expectedComplexit { TypeName parsed = TypeName.Parse(type.AssemblyQualifiedName.AsSpan()); - Assert.Equal(expectedComplexity, parsed.TotalComplexity); + Assert.Equal(expectedComplexity, parsed.Complexity); Assert.Equal(type.Name, parsed.Name); Assert.Equal(type.FullName, parsed.FullName); @@ -557,9 +557,10 @@ public void ParsedNamesMatchSystemTypeNames(Type type) if (type.IsGenericType) { Type genericType = type.GetGenericTypeDefinition(); - Assert.Equal(genericType.Name, parsed.UnderlyingType.Name); - Assert.Equal(genericType.FullName, parsed.UnderlyingType.FullName); - Assert.Equal(genericType.AssemblyQualifiedName, parsed.UnderlyingType.AssemblyQualifiedName); + TypeName genericTypeName = parsed.GetGenericTypeDefinition(); + Assert.Equal(genericType.Name, genericTypeName.Name); + Assert.Equal(genericType.FullName, genericTypeName.FullName); + Assert.Equal(genericType.AssemblyQualifiedName, genericTypeName.AssemblyQualifiedName); } } @@ -627,19 +628,28 @@ static void Verify(Type type, TypeName typeName, bool ignoreCase) #endif static Type? GetType(TypeName typeName, bool throwOnError = true, bool ignoreCase = false) { - if (typeName.ContainingType is not null) // nested type + if (typeName.IsNested) { BindingFlags flagsCopiedFromClr = BindingFlags.NonPublic | BindingFlags.Public; if (ignoreCase) { flagsCopiedFromClr |= BindingFlags.IgnoreCase; } - return Make(GetType(typeName.ContainingType, throwOnError, ignoreCase)?.GetNestedType(typeName.Name, flagsCopiedFromClr)); + return Make(GetType(typeName.DeclaringType!, throwOnError, ignoreCase)?.GetNestedType(typeName.Name, flagsCopiedFromClr)); } - else if (typeName.UnderlyingType is null) // elemental + else if (typeName.IsConstructedGenericType) { - AssemblyName? assemblyName = typeName.GetAssemblyName(); + return Make(GetType(typeName.GetGenericTypeDefinition(), throwOnError, ignoreCase)); + } + else if(typeName.IsArray || typeName.IsPointer || typeName.IsByRef) + { + return Make(GetType(typeName.GetElementType(), throwOnError, ignoreCase)); + } + else + { + Assert.Equal(1, typeName.Complexity); + AssemblyName? assemblyName = typeName.GetAssemblyName(); Type? type = assemblyName is null ? Type.GetType(typeName.FullName, throwOnError, ignoreCase) : Assembly.Load(assemblyName).GetType(typeName.FullName, throwOnError, ignoreCase); @@ -647,8 +657,6 @@ static void Verify(Type type, TypeName typeName, bool ignoreCase) return Make(type); } - return Make(GetType(typeName.UnderlyingType, throwOnError, ignoreCase)); - Type? Make(Type? type) { if (type is null || typeName.IsElementalType) @@ -657,7 +665,7 @@ static void Verify(Type type, TypeName typeName, bool ignoreCase) } else if (typeName.IsConstructedGenericType) { - ReadOnlySpan genericArgs = typeName.GetGenericArguments(); + ReadOnlySpan genericArgs = typeName.GetGenericArguments().Span; Type[] genericTypes = new Type[genericArgs.Length]; for (int i = 0; i < genericArgs.Length; i++) { @@ -671,15 +679,15 @@ static void Verify(Type type, TypeName typeName, bool ignoreCase) return type.MakeGenericType(genericTypes); } - else if (typeName.IsManagedPointerType) + else if (typeName.IsByRef) { return type.MakeByRefType(); } - else if (typeName.IsUnmanagedPointerType) + else if (typeName.IsPointer) { return type.MakePointerType(); } - else if (typeName.IsSzArrayType) + else if (typeName.IsSZArray) { return type.MakeArrayType(); } From 1c76366c7bcc8e2f877e5d5a62a38d3c5c9f1c59 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 20 Mar 2024 12:22:49 +0100 Subject: [PATCH 27/48] apply changes based on the final API Design Review --- .../System/Reflection/Metadata/TypeName.cs | 102 ++++++++++-------- .../Reflection/Metadata/TypeNameParser.cs | 10 +- .../Metadata/TypeNameParserOptions.cs | 14 ++- .../Reflection/TypeNameParser.Helpers.cs | 10 +- .../ref/System.Reflection.Metadata.cs | 11 +- .../tests/Metadata/TypeNameParserSamples.cs | 23 ++-- .../tests/Metadata/TypeNameParserTests.cs | 58 +++++++--- 7 files changed, 132 insertions(+), 96 deletions(-) diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs index c7e3500b3fa1b..e8465c83c3b92 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs @@ -24,6 +24,7 @@ sealed class TypeName private readonly TypeName[]? _genericArguments; private readonly AssemblyName? _assemblyName; private readonly TypeName? _underlyingType; + private readonly TypeName? _declaringType; private string? _assemblyQualifiedName; internal TypeName(string name, string fullName, @@ -38,9 +39,8 @@ internal TypeName(string name, string fullName, _assemblyName = assemblyName; _rankOrModifier = rankOrModifier; _underlyingType = underlyingType; - DeclaringType = containingType; + _declaringType = containingType; _genericArguments = genericTypeArguments; - Complexity = GetComplexity(underlyingType, containingType, genericTypeArguments); Debug.Assert(!(IsArray || IsPointer || IsByRef) || _underlyingType is not null); Debug.Assert(_genericArguments is null || _underlyingType is not null); @@ -55,14 +55,22 @@ internal TypeName(string name, string fullName, public string AssemblyQualifiedName => _assemblyQualifiedName ??= _assemblyName is null ? FullName : $"{FullName}, {_assemblyName.FullName}"; + /// + /// Returns the name of the assembly (not the full name>). + /// + /// + /// If returns null, simply returns null. + /// + public string? AssemblySimpleName => _assemblyName?.Name; + /// /// If this type is a nested type (see ), gets - /// the declaring type. If this type is not a nested type, returns null. + /// 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". /// - public TypeName? DeclaringType { get; } + public TypeName DeclaringType => _declaringType is not null ? _declaringType : throw new InvalidOperationException(); /// /// The full name of this type, including namespace, but without the assembly name; e.g., "System.Int32". @@ -95,7 +103,7 @@ public string AssemblyQualifiedName public bool IsConstructedGenericType => _genericArguments is not null; /// - /// Returns true if this is a "plain" type; that is, not an array, not a pointer, and + /// 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". /// @@ -106,7 +114,7 @@ public string AssemblyQualifiedName /// This is because determining whether a type truly is a generic type requires loading the type /// and performing a runtime check. /// - public bool IsElementalType => _underlyingType is null; + public bool IsSimple => _underlyingType is null; /// /// Returns true if this is a managed pointer type (e.g., "ref int"). @@ -115,10 +123,10 @@ public string AssemblyQualifiedName public bool IsByRef => _rankOrModifier == TypeNameParserHelpers.ByRef; /// - /// Returns true if this is a nested type (e.g., "Namespace.Containing+Nested"). - /// For nested types returns their containing type. + /// 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; + public bool IsNested => _declaringType is not null; /// /// Returns true if this type represents a single-dimensional, zero-indexed array (e.g., "int[]"). @@ -135,7 +143,7 @@ public string AssemblyQualifiedName /// 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; + public bool IsVariableBoundArrayType => _rankOrModifier >= 1; /// /// The name of this type, without the namespace and the assembly name; e.g., "Int32". @@ -144,19 +152,20 @@ public string AssemblyQualifiedName public string Name { get; } /// - /// Represents the total amount of work that needs to be performed to fully inspect + /// 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 complexity of 2 because to fully inspect it involves inspecting the + /// "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 complexity 8 because fully visiting it + /// "Dictionary<string, List<int[][]>>" has node count 8 because fully visiting it /// involves inspecting 8 instances total: /// /// Dictionary<string, List<int[][]>> (the original type) @@ -170,7 +179,30 @@ public string AssemblyQualifiedName /// /// /// - public int Complexity { get; } + public int GetNodeCount() + { + int result = 1; + + if (_underlyingType is not null) + { + result = checked(result + _underlyingType.GetNodeCount()); + } + + if (_declaringType is not null) + { + result = checked(result + _declaringType.GetNodeCount()); + } + + if (_genericArguments is not null) + { + foreach (TypeName genericArgument in _genericArguments) + { + result = checked(result + genericArgument.GetNodeCount()); + } + } + + return result; + } /// /// The TypeName of the object encompassed or referred to by the current array, pointer, or reference type. @@ -242,36 +274,16 @@ public int GetArrayRank() /// For example, given "Dictionary<string, int>", returns a 2-element span containing /// string and int. /// - public ReadOnlyMemory GetGenericArguments() - => _genericArguments is null ? ReadOnlyMemory.Empty : _genericArguments.AsMemory(); - - private static int GetComplexity(TypeName? underlyingType, TypeName? containingType, TypeName[]? genericTypeArguments) - { - int result = 1; - - if (underlyingType is not null) - { - result = checked(result + underlyingType.Complexity); - } - - if (containingType is not null) - { - result = checked(result + containingType.Complexity); - } - - if (genericTypeArguments is not null) - { - // New total complexity will be the sum of the cumulative args' complexity + 2: - // - one for the generic type definition "MyGeneric`x" - // - one for the constructed type definition "MyGeneric`x[[...]]" - // - and the cumulative complexity of all the arguments - foreach (TypeName genericArgument in genericTypeArguments) - { - result = checked(result + genericArgument.Complexity); - } - } - - return result; - } +#if SYSTEM_PRIVATE_CORELIB + public TypeName[] GetGenericArguments() => _genericArguments ?? Array.Empty(); +#else + public Collections.Immutable.ImmutableArray GetGenericArguments() + => _genericArguments is null ? Collections.Immutable.ImmutableArray.Empty : +#if NET8_0_OR_GREATER + Runtime.InteropServices.ImmutableCollectionsMarshal.AsImmutableArray(_genericArguments); +#else + Collections.Immutable.ImmutableArray.Create(_genericArguments); + #endif +#endif } } diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs index c0b3a0cd4b90b..212eb86763067 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs @@ -38,7 +38,7 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse int recursiveDepth = 0; TypeNameParser parser = new(trimmedName, throwOnError, options); - TypeName? parsedName = parser.ParseNextTypeName(parser._parseOptions.AllowFullyQualifiedName, ref recursiveDepth); + TypeName? parsedName = parser.ParseNextTypeName(allowFullyQualifiedName: true, ref recursiveDepth); if (parsedName is not null && parser._inputString.IsEmpty) // unconsumed input == error { @@ -51,7 +51,7 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse // there was an error and we need to throw #if !SYSTEM_PRIVATE_CORELIB - if (recursiveDepth >= parser._parseOptions.MaxTotalComplexity) + if (recursiveDepth >= parser._parseOptions.MaxNodes) { throw new InvalidOperationException("SR.RecursionCheck_MaxDepthExceeded"); } @@ -147,9 +147,9 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse { // Parsing the rest would hit the limit. // -1 because the first generic arg has been already parsed. - if (maxObservedRecursionCheck + genericArgCount - 1 > _parseOptions.MaxTotalComplexity) + if (maxObservedRecursionCheck + genericArgCount - 1 > _parseOptions.MaxNodes) { - recursiveDepth = _parseOptions.MaxTotalComplexity; + recursiveDepth = _parseOptions.MaxNodes; return null; } @@ -336,7 +336,7 @@ private bool TryParseAssemblyName(ref AssemblyName? assemblyName) private bool TryDive(ref int depth) { - if (depth >= _parseOptions.MaxTotalComplexity) + if (depth >= _parseOptions.MaxNodes) { return false; } diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs index 382164d21bac9..40b4e82d3b553 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs @@ -10,16 +10,14 @@ namespace System.Reflection.Metadata #endif sealed class TypeNameParserOptions { - private int _maxComplexity = int.MaxValue; - - public bool AllowFullyQualifiedName { get; set; } = true; + private int _maxNodes = int.MaxValue; // TODO: choose the right default based on facts /// - /// Limits the maximum value of that parser can handle. + /// Limits the maximum value of node count that parser can handle. /// - public int MaxTotalComplexity + public int MaxNodes { - get => _maxComplexity; + get => _maxNodes; set { #if NET8_0_OR_GREATER @@ -31,7 +29,7 @@ public int MaxTotalComplexity } #endif - _maxComplexity = value; + _maxNodes = value; } } @@ -42,6 +40,6 @@ public int MaxTotalComplexity /// When parsing AssemblyName, only Version, Culture and PublicKeyToken attributes are allowed. /// The comparison is also case-sensitive (in contrary to constructor). /// - public bool StrictValidation { get; set; } + internal bool StrictValidation { get; set; } // it's internal for now, will very soon be changed after we have full requirements and the API gets approved } } diff --git a/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs index ab3eb5bbc41fd..5bc412f4974c5 100644 --- a/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs +++ b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs @@ -152,13 +152,13 @@ private static (string typeNamespace, string name) SplitFullTypeName(string type #endif private Type? Make(Type? type, Metadata.TypeName typeName) { - if (type is null) + if (type is null || typeName.IsSimple) { return type; } else if (typeName.IsConstructedGenericType) { - ReadOnlySpan genericArgs = typeName.GetGenericArguments().Span; + var genericArgs = typeName.GetGenericArguments(); Type[] genericTypes = new Type[genericArgs.Length]; for (int i = 0; i < genericArgs.Length; i++) { @@ -184,13 +184,9 @@ private static (string typeNamespace, string name) SplitFullTypeName(string type { return type.MakeArrayType(); } - else if(typeName.IsArray) - { - return type.MakeArrayType(rank: typeName.GetArrayRank()); - } else { - return type; + return type.MakeArrayType(rank: typeName.GetArrayRank()); } } } 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 e7b597e1a39a2..e80cf762b9e8a 100644 --- a/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs +++ b/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs @@ -2412,15 +2412,17 @@ public sealed partial class TypeName { internal TypeName() { } public string AssemblyQualifiedName { get { throw null; } } + public string? AssemblySimpleName { 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 IsElementalType { get { throw null; } } - public bool IsByRef { get { throw null; } } public bool IsNested { get { throw null; } } - public bool IsSZArray { 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 int Complexity { get; } @@ -2431,13 +2433,12 @@ internal TypeName() { } public System.ReadOnlyMemory 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 TypeNameParserOptions { public TypeNameParserOptions() { } - public bool AllowFullyQualifiedName { get { throw null; } set { } } - public int MaxTotalComplexity { get { throw null; } set { } } - public bool StrictValidation { get; set; } + public int MaxNodes { get { throw null; } set { } } } public readonly partial struct TypeReference { diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs index 13a3c8c371199..712a34571ff39 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs @@ -2,6 +2,7 @@ // 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; @@ -57,17 +58,10 @@ public SampleSerializationBinder(Type[]? allowedTypes = null) _options ??= new() // there is no need for lazy initialization, I just wanted to have everything important in one method { - // We parse only type names, because 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 - AllowFullyQualifiedName = false, // 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. - MaxTotalComplexity = 10 + MaxNodes = 10 }; if (!TypeName.TryParse(typeName.AsSpan(), out TypeName parsed, _options)) @@ -76,6 +70,17 @@ public SampleSerializationBinder(Type[]? allowedTypes = null) throw new InvalidOperationException($"Invalid type name: '{typeName}'"); } + if (parsed.GetAssemblyName() 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); } @@ -100,7 +105,7 @@ public SampleSerializationBinder(Type[]? allowedTypes = null) Type genericTypeDefinition = GetTypeFromParsedTypeName(genericTypeDefinitionName); Debug.Assert(genericTypeDefinition.IsGenericTypeDefinition); - ReadOnlySpan genericArgs = parsed.GetGenericArguments().Span; + ImmutableArray genericArgs = parsed.GetGenericArguments(); Type[] typeArguments = new Type[genericArgs.Length]; for (int i = 0; i < genericArgs.Length; i++) { diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs index c4f575c2df9a0..7244ec1b71dca 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs @@ -2,6 +2,7 @@ // 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; @@ -117,6 +118,7 @@ static void Verify(Type type, AssemblyName expectedAssemblyName, TypeName parsed Assert.NotNull(parsedAssemblyName); Assert.Equal(expectedAssemblyName.Name, parsedAssemblyName.Name); + Assert.Equal(expectedAssemblyName.Name, parsed.AssemblySimpleName); Assert.Equal(expectedAssemblyName.Version, parsedAssemblyName.Version); Assert.Equal(expectedAssemblyName.CultureName, parsedAssemblyName.CultureName); Assert.Equal(expectedAssemblyName.GetPublicKeyToken(), parsedAssemblyName.GetPublicKeyToken()); @@ -155,7 +157,6 @@ public void CanNotParseTypeWithInvalidAssemblyName(string fullName) TypeNameParserOptions options = new() { StrictValidation = true, - AllowFullyQualifiedName = true }; Assert.False(TypeName.TryParse(fullName.AsSpan(), out _, options)); @@ -171,7 +172,7 @@ public void MaxTotalComplexityIsRespected_TooManyDecorators(int maxDepth, string { TypeNameParserOptions options = new() { - MaxTotalComplexity = maxDepth + MaxNodes = maxDepth }; string notTooMany = $"System.Int32{string.Join("", Enumerable.Repeat(decorator, maxDepth - 1))}"; @@ -206,7 +207,7 @@ public void MaxTotalComplexityIsRespected_TooDeepGenerics(int maxDepth) { TypeNameParserOptions options = new() { - MaxTotalComplexity = maxDepth + MaxNodes = maxDepth }; string tooDeep = GetName(maxDepth); @@ -238,7 +239,7 @@ static void Validate(int maxDepth, TypeName parsed) for (int i = 0; i < maxDepth - 1; i++) { Assert.True(parsed.IsConstructedGenericType); - parsed = parsed.GetGenericArguments().Span[0]; + parsed = parsed.GetGenericArguments()[0]; } } } @@ -250,7 +251,7 @@ public void MaxTotalComplexityIsRespected_TooManyGenericArguments(int maxDepth) { TypeNameParserOptions options = new() { - MaxTotalComplexity = maxDepth + MaxNodes = maxDepth }; string tooMany = GetName(maxDepth); @@ -271,7 +272,7 @@ static string GetName(int depth) static void Validate(TypeName parsed, int maxDepth) { Assert.True(parsed.IsConstructedGenericType); - TypeName[] genericArgs = parsed.GetGenericArguments().ToArray(); + ImmutableArray genericArgs = parsed.GetGenericArguments(); Assert.Equal(maxDepth - 1, genericArgs.Length); Assert.All(genericArgs, arg => Assert.False(arg.IsConstructedGenericType)); } @@ -392,13 +393,14 @@ public void GenericArgumentsAreSupported(string input, string name, string fullN Assert.Equal(name, parsed.Name); Assert.Equal(fullName, parsed.FullName); Assert.True(parsed.IsConstructedGenericType); - Assert.False(parsed.IsElementalType); + Assert.False(parsed.IsSimple); + ImmutableArray typeNames = parsed.GetGenericArguments(); for (int i = 0; i < genericTypesFullNames.Length; i++) { - TypeName genericArg = parsed.GetGenericArguments().Span[i]; + TypeName genericArg = typeNames[i]; Assert.Equal(genericTypesFullNames[i], genericArg.FullName); - Assert.True(genericArg.IsElementalType); + Assert.True(genericArg.IsSimple); Assert.False(genericArg.IsConstructedGenericType); if (assemblyNames is not null) @@ -410,6 +412,7 @@ public void GenericArgumentsAreSupported(string input, string name, string fullN else { Assert.Equal(assemblyNames[i].FullName, genericArg.GetAssemblyName().FullName); + Assert.Equal(assemblyNames[i].Name, genericArg.AssemblySimpleName); } } } @@ -451,12 +454,12 @@ public void DecoratorsAreSupported(string input, string typeNameWithoutDecorator if (isArray) Assert.Equal(arrayRank, parsed.GetArrayRank()); Assert.Equal(isByRef, parsed.IsByRef); Assert.Equal(isPointer, parsed.IsPointer); - Assert.False(parsed.IsElementalType); + Assert.False(parsed.IsSimple); TypeName elementType = parsed.GetElementType(); Assert.NotNull(elementType); Assert.Equal(typeNameWithoutDecorators, elementType.FullName); - Assert.True(elementType.IsElementalType); + Assert.True(elementType.IsSimple); Assert.False(elementType.IsArray); Assert.False(elementType.IsSZArray); Assert.False(elementType.IsByRef); @@ -501,17 +504,38 @@ public static IEnumerable GetAdditionalConstructedTypeData() [InlineData(typeof(int[,][]), 3)] [InlineData(typeof(Nullable<>), 1)] // open generic type treated as elemental [MemberData(nameof(GetAdditionalConstructedTypeData))] - public void TotalComplexityReturnsExpectedValue(Type type, int expectedComplexity) + public void GetNodeCountReturnsExpectedValue(Type type, int expected) { TypeName parsed = TypeName.Parse(type.AssemblyQualifiedName.AsSpan()); - Assert.Equal(expectedComplexity, parsed.Complexity); + 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); + } + + [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) @@ -635,7 +659,7 @@ static void Verify(Type type, TypeName typeName, bool ignoreCase) { flagsCopiedFromClr |= BindingFlags.IgnoreCase; } - return Make(GetType(typeName.DeclaringType!, throwOnError, ignoreCase)?.GetNestedType(typeName.Name, flagsCopiedFromClr)); + return Make(GetType(typeName.DeclaringType, throwOnError, ignoreCase)?.GetNestedType(typeName.Name, flagsCopiedFromClr)); } else if (typeName.IsConstructedGenericType) { @@ -647,7 +671,7 @@ static void Verify(Type type, TypeName typeName, bool ignoreCase) } else { - Assert.Equal(1, typeName.Complexity); + Assert.True(typeName.IsSimple); AssemblyName? assemblyName = typeName.GetAssemblyName(); Type? type = assemblyName is null @@ -659,13 +683,13 @@ static void Verify(Type type, TypeName typeName, bool ignoreCase) Type? Make(Type? type) { - if (type is null || typeName.IsElementalType) + if (type is null || typeName.IsSimple) { return type; } else if (typeName.IsConstructedGenericType) { - ReadOnlySpan genericArgs = typeName.GetGenericArguments().Span; + ImmutableArray genericArgs = typeName.GetGenericArguments(); Type[] genericTypes = new Type[genericArgs.Length]; for (int i = 0; i < genericArgs.Length; i++) { From 8cd205ac73e7652750c30160ef4db286a63e5b4f Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 20 Mar 2024 15:10:16 +0100 Subject: [PATCH 28/48] solve the TODOs --- .../Reflection/TypeNameParser.CoreCLR.cs | 4 ++-- .../System/Reflection/RuntimeAssemblyName.cs | 9 +++++++++ .../Reflection/TypeNameParser.NativeAot.cs | 4 ++-- .../Dataflow/TypeNameParser.Dataflow.cs | 4 ++-- .../System/Reflection/Metadata/TypeName.cs | 17 ++++++++++------- .../Reflection/Metadata/TypeNameParser.cs | 19 +++++++++---------- .../src/Resources/Strings.resx | 3 +++ .../src/System/Reflection/AssemblyName.cs | 2 +- .../ref/System.Reflection.Metadata.cs | 4 +--- .../src/Resources/Strings.resx | 12 ++++++++++++ .../Metadata/TypeNameParserHelpersTests.cs | 2 -- .../tests/Metadata/TypeNameParserSamples.cs | 2 +- .../tests/Metadata/TypeNameParserTests.cs | 11 ++++++++--- .../System/Type/TypeTests.cs | 18 +++++++++++------- 14 files changed, 71 insertions(+), 40 deletions(-) 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 7d54706b89cbf..4bd8077986602 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 @@ -85,7 +85,7 @@ internal partial struct TypeNameParser { return null; } - else if (parsed.GetAssemblyName() is not null && topLevelAssembly is not null) + else if (topLevelAssembly is not null && parsed.GetAssemblyName() is not null) { return throwOnError ? throw new ArgumentException(SR.Argument_AssemblyGetTypeCannotSpecifyAssembly) : null; } @@ -110,7 +110,7 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName, RuntimeAssembly requestingAssembly = scope.GetRuntimeAssembly(); - var parsed = Metadata.TypeNameParser.Parse(typeName, throwOnError: true)!; + var parsed = Metadata.TypeName.Parse(typeName); RuntimeType? type = (RuntimeType?)new TypeNameParser() { _throwOnError = true, 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 ee76c4a5e302d..95c80a5df9c3b 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 @@ -129,6 +129,15 @@ public AssemblyName ToAssemblyName() return assemblyName; } + internal static RuntimeAssemblyName FromAssemblyName(AssemblyName source) + { + byte[]? publicKeyOrToken = (source._flags & AssemblyNameFlags.PublicKey) != 0 + ? source.GetPublicKey() + : source.GetPublicKeyToken(); + + return new(source.Name, source.Version, source.CultureName, source._flags, publicKeyOrToken); + } + // // Copies a RuntimeAssemblyName into a freshly allocated AssemblyName with no data aliasing to any other object. // 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 80af56bbedc9e..28d024c929456 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 @@ -80,7 +80,7 @@ internal partial struct TypeNameParser { return null; } - else if (parsed.GetAssemblyName() is not null && topLevelAssembly is not null) + else if (topLevelAssembly is not null && parsed.GetAssemblyName() is not null) { return throwOnError ? throw new ArgumentException(SR.Argument_AssemblyGetTypeCannotSpecifyAssembly) : null; } @@ -102,7 +102,7 @@ internal partial struct TypeNameParser } else { - assembly = RuntimeAssemblyInfo.GetRuntimeAssemblyIfExists(RuntimeAssemblyName.Parse(assemblyName.FullName)); // TODO adsitnik: remove the redundant parsing + assembly = RuntimeAssemblyInfo.GetRuntimeAssemblyIfExists(RuntimeAssemblyName.FromAssemblyName(assemblyName)); } if (assembly is null && _throwOnError) 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 231873cf303e5..1e617a1f1faba 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 @@ -19,8 +19,8 @@ public static TypeDesc ResolveType(string name, ModuleDesc callingModule, { if (!TypeName.TryParse(name, out TypeName parsed)) { - typeWasNotFoundInAssemblyNorBaseLibrary = true; - return null; // TODO adsitnik: verify that this is desired + typeWasNotFoundInAssemblyNorBaseLibrary = false; + return null; } var parser = new TypeNameParser() diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs index e8465c83c3b92..7a93abe03a01b 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs @@ -183,14 +183,17 @@ public int GetNodeCount() { int result = 1; - if (_underlyingType is not null) + if (IsNested) { - result = checked(result + _underlyingType.GetNodeCount()); + result = checked(result + DeclaringType.GetNodeCount()); } - - if (_declaringType is not null) + else if (IsConstructedGenericType) + { + result = checked(result + 1); + } + else if (IsArray || IsPointer || IsByRef) { - result = checked(result + _declaringType.GetNodeCount()); + result = checked(result + GetElementType().GetNodeCount()); } if (_genericArguments is not null) @@ -224,7 +227,7 @@ public TypeName GetElementType() public TypeName GetGenericTypeDefinition() => IsConstructedGenericType ? _underlyingType! - : throw new InvalidOperationException("SR.InvalidOperation_NotGenericType"); // TODO: use actual resource + : throw new InvalidOperationException(SR.InvalidOperation_NotGenericType); public static TypeName Parse(ReadOnlySpan typeName, TypeNameParserOptions? options = default) => TypeNameParser.Parse(typeName, throwOnError: true, options)!; @@ -244,7 +247,7 @@ public int GetArrayRank() { TypeNameParserHelpers.SZArray => 1, _ when _rankOrModifier > 0 => _rankOrModifier, - _ => throw new ArgumentException("SR.Argument_HasToBeArrayClass") // TODO: use actual resource (used by Type.GetArrayRank) + _ => throw new InvalidOperationException(SR.Argument_HasToBeArrayClass) }; /// diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs index 212eb86763067..98f3d94efe78a 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs @@ -49,13 +49,11 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse return null; } - // there was an error and we need to throw -#if !SYSTEM_PRIVATE_CORELIB if (recursiveDepth >= parser._parseOptions.MaxNodes) { - throw new InvalidOperationException("SR.RecursionCheck_MaxDepthExceeded"); + throw new InvalidOperationException(SR.Format(SR.MaxNodesExceeded, parser._parseOptions.MaxNodes)); } -#endif + int errorIndex = typeName.Length - parser._inputString.Length; return ThrowInvalidTypeNameOrReturnNull(throwOnError, errorIndex); @@ -69,7 +67,7 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse #if SYSTEM_PRIVATE_CORELIB throw new ArgumentException(SR.Arg_ArgumentException, $"typeName@{errorIndex}"); #else - throw new ArgumentException("SR.Argument_InvalidTypeName"); + throw new ArgumentException(SR.Argument_InvalidTypeName); #endif } } @@ -203,13 +201,14 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse return null; } - if (previousDecorator == ByRef) // it's illegal for managed reference to be followed by any other decorator - { - return null; - } - else if (parsedDecorator > MaxArrayRank) + if (previousDecorator == ByRef // it's illegal for managed reference to be followed by any other decorator + || parsedDecorator > MaxArrayRank) { +#if SYSTEM_PRIVATE_CORELIB + throw new TypeLoadException(); // CLR throws TypeLoadException on purpose +#else return null; +#endif } previousDecorator = parsedDecorator; } diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index 2f58fae3471e9..600a8a1870160 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -4307,4 +4307,7 @@ Blocking wait is not supported on the JS interop threads. + + Maximum node count of {0} exceeded. + \ No newline at end of file diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/AssemblyName.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/AssemblyName.cs index ec96aa8bc07b5..85b07fdf28377 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/AssemblyName.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/AssemblyName.cs @@ -24,7 +24,7 @@ public sealed partial class AssemblyName : ICloneable, IDeserializationCallback, private AssemblyHashAlgorithm _hashAlgorithm; private AssemblyVersionCompatibility _versionCompatibility; - private AssemblyNameFlags _flags; + internal AssemblyNameFlags _flags; public AssemblyName(string assemblyName) : this() 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 e80cf762b9e8a..d656922076f93 100644 --- a/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs +++ b/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs @@ -2418,19 +2418,17 @@ internal TypeName() { } public bool IsArray { get { throw null; } } public bool IsByRef { get { throw null; } } public bool IsConstructedGenericType { get { throw null; } } - public bool IsElementalType { 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 int Complexity { get; } public static System.Reflection.Metadata.TypeName Parse(System.ReadOnlySpan typeName, System.Reflection.Metadata.TypeNameParserOptions? 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.TypeNameParserOptions? options = null) { throw null; } public int GetArrayRank() { throw null; } public System.Reflection.AssemblyName? GetAssemblyName() { throw null; } - public System.ReadOnlyMemory GetGenericArguments() { 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; } diff --git a/src/libraries/System.Reflection.Metadata/src/Resources/Strings.resx b/src/libraries/System.Reflection.Metadata/src/Resources/Strings.resx index c035a3efee098..6c09fb986b692 100644 --- a/src/libraries/System.Reflection.Metadata/src/Resources/Strings.resx +++ b/src/libraries/System.Reflection.Metadata/src/Resources/Strings.resx @@ -411,4 +411,16 @@ 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. + \ No newline at end of file diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs index 9bd263544e394..e2323eb5c3867 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs @@ -47,7 +47,6 @@ public static IEnumerable GetGenericArgumentCountReturnsExpectedValue_ public void GetGenericArgumentCountReturnsExpectedValue(string input, int expected) => Assert.Equal(expected, TypeNameParserHelpers.GetGenericArgumentCount(input.AsSpan())); - [Theory] [InlineData("A[]", 1, false)] [InlineData("AB[a,b]", 2, false)] @@ -100,7 +99,6 @@ public static IEnumerable InvalidNamesArguments() yield return new object[] { "BacktickIsOk`1", -1 }; } - [Theory] [MemberData(nameof(InvalidNamesArguments))] [InlineData("Spaces AreAllowed", -1)] diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs index 712a34571ff39..e4e747fb20405 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs @@ -155,7 +155,7 @@ public void CanDeserializeCustomUserDefinedType() allowedTypes: [ typeof(CustomUserDefinedType), - typeof(List<>) // TODO adsitnik: make it work for List too (currently does not work due to type forwarding) + typeof(List<>) // using List would require using type forwarding info in dictionary ]); CustomUserDefinedType deserialized = SerializeDeserialize(parent, binder); diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs index 7244ec1b71dca..4d8e707738d03 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs @@ -168,7 +168,7 @@ public void CanNotParseTypeWithInvalidAssemblyName(string fullName) [InlineData(10, "[]")] // array of arrays [InlineData(100, "*")] [InlineData(100, "[]")] - public void MaxTotalComplexityIsRespected_TooManyDecorators(int maxDepth, string decorator) + public void MaxNodesIsRespected_TooManyDecorators(int maxDepth, string decorator) { TypeNameParserOptions options = new() { @@ -203,7 +203,7 @@ static void ValidateElementType(int maxDepth, TypeName parsed, string decorator) [Theory] [InlineData(10)] [InlineData(100)] - public void MaxTotalComplexityIsRespected_TooDeepGenerics(int maxDepth) + public void MaxNodesIsRespected_TooDeepGenerics(int maxDepth) { TypeNameParserOptions options = new() { @@ -247,7 +247,7 @@ static void Validate(int maxDepth, TypeName parsed) [Theory] [InlineData(10)] [InlineData(100)] - public void MaxTotalComplexityIsRespected_TooManyGenericArguments(int maxDepth) + public void MaxNodesIsRespected_TooManyGenericArguments(int maxDepth) { TypeNameParserOptions options = new() { @@ -503,6 +503,11 @@ public static IEnumerable GetAdditionalConstructedTypeData() [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 + // TypeNameParserTests+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) { diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Type/TypeTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Type/TypeTests.cs index ac78c6ac6cc70..ab1d6c66c1279 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Type/TypeTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Type/TypeTests.cs @@ -515,17 +515,21 @@ public void GetTypeByName_ValidType_ReturnsExpected(string typeName, Type expect public static IEnumerable GetTypeByName_InvalidElementType() { - yield return new object[] { "System.Int32&&", typeof(ArgumentException), true }; - yield return new object[] { "System.Int32&*", typeof(ArgumentException), true }; - yield return new object[] { "System.Int32&[]", typeof(ArgumentException), true }; - yield return new object[] { "System.Int32&[*]", typeof(ArgumentException), true }; - yield return new object[] { "System.Int32&[,]", typeof(ArgumentException), true }; + Type expectedException = PlatformDetection.IsMonoRuntime + ? typeof(ArgumentException) // https://github.com/dotnet/runtime/issues/45033 + : typeof(TypeLoadException); + + yield return new object[] { "System.Int32&&", expectedException, true }; + yield return new object[] { "System.Int32&*", expectedException, true }; + yield return new object[] { "System.Int32&[]", expectedException, true }; + yield return new object[] { "System.Int32&[*]", expectedException, true }; + yield return new object[] { "System.Int32&[,]", expectedException, true }; // https://github.com/dotnet/runtime/issues/45033 if (!PlatformDetection.IsMonoRuntime) { - yield return new object[] { "System.Void[]", typeof(TypeLoadException), true }; - yield return new object[] { "System.TypedReference[]", typeof(TypeLoadException), true }; + yield return new object[] { "System.Void[]", expectedException, true }; + yield return new object[] { "System.TypedReference[]", expectedException, true }; } } From ae514ecc872cc5afd92c2fd0b5ec6c7c0b1163c0 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Thu, 21 Mar 2024 10:07:33 +0100 Subject: [PATCH 29/48] implement IEquatable, add missing exception messages, set default MaxNode value --- .../ILVerification/ILVerification.csproj | 2 +- .../System/Reflection/Metadata/TypeName.cs | 65 ++++++++++++++---- .../Reflection/Metadata/TypeNameParser.cs | 32 +++------ .../Metadata/TypeNameParserHelpers.cs | 44 +++++++++++++ .../Metadata/TypeNameParserOptions.cs | 9 ++- .../src/Resources/Strings.resx | 8 ++- .../ref/System.Reflection.Metadata.cs | 13 ++-- .../src/Resources/Strings.resx | 8 ++- .../src/System.Reflection.Metadata.csproj | 1 + .../Metadata/TypeNameParserHelpersTests.cs | 4 +- .../tests/Metadata/TypeNameParserSamples.cs | 2 +- ...ypeNameParserTests.cs => TypeNameTests.cs} | 66 ++++++++++++++++--- .../System.Reflection.Metadata.Tests.csproj | 2 +- 13 files changed, 195 insertions(+), 61 deletions(-) rename src/libraries/System.Reflection.Metadata/tests/Metadata/{TypeNameParserTests.cs => TypeNameTests.cs} (92%) diff --git a/src/coreclr/tools/ILVerification/ILVerification.csproj b/src/coreclr/tools/ILVerification/ILVerification.csproj index b8a3b75bb0d5c..4ac252c3e0c44 100644 --- a/src/coreclr/tools/ILVerification/ILVerification.csproj +++ b/src/coreclr/tools/ILVerification/ILVerification.csproj @@ -10,7 +10,7 @@ true Open false - NETSTANDARD2_0;INTERNAL_NULLABLE_ANNOTATIONS + NETSTANDARD2_0 diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs index 7a93abe03a01b..c09d59ce8cdf7 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs @@ -14,7 +14,7 @@ namespace System.Reflection.Metadata #else public #endif - sealed class TypeName + sealed class TypeName : IEquatable { /// /// Positive value is array rank. @@ -56,7 +56,7 @@ public string AssemblyQualifiedName => _assemblyQualifiedName ??= _assemblyName is null ? FullName : $"{FullName}, {_assemblyName.FullName}"; /// - /// Returns the name of the assembly (not the full name>). + /// Returns the name of the assembly (not the full name). /// /// /// If returns null, simply returns null. @@ -70,7 +70,10 @@ public string AssemblyQualifiedName /// /// For example, given "Namespace.Declaring+Nested", unwraps the outermost type and returns "Namespace.Declaring". /// - public TypeName DeclaringType => _declaringType is not null ? _declaringType : throw new InvalidOperationException(); + /// The current type is not a nested type. + public TypeName DeclaringType => _declaringType is not null + ? _declaringType + : throw TypeNameParserHelpers.InvalidOperation_NotNestedType(); /// /// The full name of this type, including namespace, but without the assembly name; e.g., "System.Int32". @@ -151,6 +154,18 @@ public string AssemblyQualifiedName /// public string Name { get; } + public bool Equals(TypeName? other) + => other is not null + && other._rankOrModifier == _rankOrModifier + // try to prevent from allocations if possible (AssemblyQualifiedName can allocate) + && ((other._assemblyName is null && _assemblyName is null) + || (other._assemblyName is not null && _assemblyName is not null)) + && other.AssemblyQualifiedName == AssemblyQualifiedName; + + public override bool Equals(object? obj) => Equals(obj as TypeName); + + public override int GetHashCode() => AssemblyQualifiedName.GetHashCode(); + /// /// Represents the total number of instances that are used to describe /// this instance, including any generic arguments or underlying types. @@ -213,10 +228,11 @@ public int GetNodeCount() /// /// For example, given "int[][]", unwraps the outermost array and returns "int[]". /// + /// The current type is not an array, pointer or reference. public TypeName GetElementType() => IsArray || IsPointer || IsByRef ? _underlyingType! - : throw new InvalidOperationException(); + : throw TypeNameParserHelpers.InvalidOperation_NoElement(); /// /// Returns a TypeName object that represents a generic type name definition from which the current generic type name can be constructed. @@ -224,30 +240,51 @@ public TypeName GetElementType() /// /// Given "Dictionary<string, int>", returns the generic type definition "Dictionary<,>". /// + /// The current type is not a generic type. public TypeName GetGenericTypeDefinition() => IsConstructedGenericType ? _underlyingType! - : throw new InvalidOperationException(SR.InvalidOperation_NotGenericType); + : throw TypeNameParserHelpers.InvalidOperation_NotGenericType(); - public static TypeName Parse(ReadOnlySpan typeName, TypeNameParserOptions? options = default) + /// + /// 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, -#if !INTERNAL_NULLABLE_ANNOTATIONS // remove along with the define from ILVerification.csproj when SystemReflectionMetadataVersion points to new version with the new types +#if SYSTEM_REFLECTION_METADATA || SYSTEM_PRIVATE_CORELIB // required by some tools that include this file but don't include the attribute [NotNullWhen(true)] #endif - out TypeName? result, TypeNameParserOptions? options = default) + 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() => _rankOrModifier switch { TypeNameParserHelpers.SZArray => 1, _ when _rankOrModifier > 0 => _rankOrModifier, - _ => throw new InvalidOperationException(SR.Argument_HasToBeArrayClass) + _ => throw TypeNameParserHelpers.InvalidOperation_HasToBeArrayClass() }; /// @@ -270,11 +307,11 @@ public int GetArrayRank() } /// - /// If this represents a constructed generic type, returns a buffer - /// of all the generic arguments. Otherwise it returns an empty buffer. + /// 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 span containing + /// For example, given "Dictionary<string, int>", returns a 2-element array containing /// string and int. /// #if SYSTEM_PRIVATE_CORELIB @@ -282,9 +319,9 @@ public int GetArrayRank() #else public Collections.Immutable.ImmutableArray GetGenericArguments() => _genericArguments is null ? Collections.Immutable.ImmutableArray.Empty : -#if NET8_0_OR_GREATER + #if NET8_0_OR_GREATER Runtime.InteropServices.ImmutableCollectionsMarshal.AsImmutableArray(_genericArguments); -#else + #else Collections.Immutable.ImmutableArray.Create(_genericArguments); #endif #endif diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs index 98f3d94efe78a..7686d93ea25fb 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs @@ -15,25 +15,25 @@ namespace System.Reflection.Metadata internal ref struct TypeNameParser { private const int MaxArrayRank = 32; - private static readonly TypeNameParserOptions _defaults = new(); + private static readonly TypeNameParseOptions _defaults = new(); private readonly bool _throwOnError; - private readonly TypeNameParserOptions _parseOptions; + private readonly TypeNameParseOptions _parseOptions; private ReadOnlySpan _inputString; - private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParserOptions? options) : this() + private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParseOptions? options) : this() { _inputString = name; _throwOnError = throwOnError; _parseOptions = options ?? _defaults; } - internal static TypeName? Parse(ReadOnlySpan typeName, bool throwOnError, TypeNameParserOptions? options = default) + internal static TypeName? Parse(ReadOnlySpan typeName, bool throwOnError, TypeNameParseOptions? options = default) { ReadOnlySpan trimmedName = TrimStart(typeName); // whitespaces at beginning are always OK if (trimmedName.IsEmpty) { // whitespace input needs to report the error index as 0 - return ThrowInvalidTypeNameOrReturnNull(throwOnError, errorIndex: 0); + return throwOnError ? throw ArgumentException_InvalidTypeName(errorIndex: 0) : null; } int recursiveDepth = 0; @@ -51,25 +51,11 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse if (recursiveDepth >= parser._parseOptions.MaxNodes) { - throw new InvalidOperationException(SR.Format(SR.MaxNodesExceeded, parser._parseOptions.MaxNodes)); + throw InvalidOperation_MaxNodesExceeded(parser._parseOptions.MaxNodes); } int errorIndex = typeName.Length - parser._inputString.Length; - return ThrowInvalidTypeNameOrReturnNull(throwOnError, errorIndex); - - static TypeName? ThrowInvalidTypeNameOrReturnNull(bool throwOnError, int errorIndex = 0) - { - if (!throwOnError) - { - return null; - } - -#if SYSTEM_PRIVATE_CORELIB - throw new ArgumentException(SR.Arg_ArgumentException, $"typeName@{errorIndex}"); -#else - throw new ArgumentException(SR.Argument_InvalidTypeName); -#endif - } + throw ArgumentException_InvalidTypeName(errorIndex); } // this method should return null instead of throwing, so the caller can get errorIndex and include it in error msg @@ -205,7 +191,7 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse || parsedDecorator > MaxArrayRank) { #if SYSTEM_PRIVATE_CORELIB - throw new TypeLoadException(); // CLR throws TypeLoadException on purpose + throw new TypeLoadException(); // CLR throws TypeLoadException for invalid decorators #else return null; #endif @@ -218,7 +204,7 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse { #if SYSTEM_PRIVATE_CORELIB // backward compat: throw FileLoadException for non-empty invalid strings - if (!_throwOnError && _inputString.TrimStart().StartsWith(",")) // TODO adsitnik: refactor + if (!_throwOnError && _inputString.TrimStart().StartsWith(",")) { return null; } diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs index 389b930452bb1..67b07af8f77ba 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs @@ -692,6 +692,50 @@ internal static bool TryStripFirstCharAndTrailingSpaces(ref ReadOnlySpan s return false; } + internal static InvalidOperationException InvalidOperation_MaxNodesExceeded(int limit) => +#if SYSTEM_REFLECTION_METADATA || SYSTEM_PRIVATE_CORELIB + new InvalidOperationException(SR.Format(SR.InvalidOperation_MaxNodesExceeded, limit)); +#else // tools that reference this file as a link + new InvalidOperationException(); +#endif + + internal static ArgumentException ArgumentException_InvalidTypeName(int errorIndex) => +#if SYSTEM_PRIVATE_CORELIB + new ArgumentException(SR.Arg_ArgumentException, $"typeName@{errorIndex}"); +#elif SYSTEM_REFLECTION_METADATA + new ArgumentException(SR.Argument_InvalidTypeName); +#else // tools that reference this file as a link + new ArgumentException(); +#endif + + internal static InvalidOperationException InvalidOperation_NotGenericType() => +#if SYSTEM_REFLECTION_METADATA || SYSTEM_PRIVATE_CORELIB + new InvalidOperationException(SR.InvalidOperation_NotGenericType); +#else // tools that reference this file as a link + new InvalidOperationException(); +#endif + + internal static InvalidOperationException InvalidOperation_NotNestedType() => +#if SYSTEM_REFLECTION_METADATA || SYSTEM_PRIVATE_CORELIB + new InvalidOperationException(SR.InvalidOperation_NotNestedType); +#else // tools that reference this file as a link + new InvalidOperationException(); +#endif + + internal static InvalidOperationException InvalidOperation_NoElement() => +#if SYSTEM_REFLECTION_METADATA || SYSTEM_PRIVATE_CORELIB + new InvalidOperationException(SR.InvalidOperation_NoElement); +#else // tools that reference this file as a link + new InvalidOperationException(); +#endif + + internal static InvalidOperationException InvalidOperation_HasToBeArrayClass() => +#if SYSTEM_REFLECTION_METADATA || SYSTEM_PRIVATE_CORELIB + new InvalidOperationException(SR.Argument_HasToBeArrayClass); +#else // tools that reference this file as a link + new InvalidOperationException(); +#endif + #if !NETCOREAPP private const int MaxLength = 2147483591; diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs index 40b4e82d3b553..0efcac029bf93 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs @@ -8,9 +8,14 @@ namespace System.Reflection.Metadata #else public #endif - sealed class TypeNameParserOptions + sealed class TypeNameParseOptions { - private int _maxNodes = int.MaxValue; // TODO: choose the right default based on facts + 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. diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index 600a8a1870160..3c7dc276f9845 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -4307,7 +4307,13 @@ Blocking wait is not supported on the JS interop threads. - + Maximum node count of {0} exceeded. + + This operation is only valid on nested types. + + + This operation is only valid on arrays, pointers and references. + \ No newline at end of file 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 d656922076f93..2cc2644e2c8ae 100644 --- a/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs +++ b/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs @@ -2408,7 +2408,7 @@ public readonly partial struct TypeLayout public int PackingSize { get { throw null; } } public int Size { get { throw null; } } } - public sealed partial class TypeName + public sealed partial class TypeName : System.IEquatable { internal TypeName() { } public string AssemblyQualifiedName { get { throw null; } } @@ -2424,8 +2424,11 @@ internal TypeName() { } 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.TypeNameParserOptions? 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.TypeNameParserOptions? options = null) { 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 override bool Equals(object? obj) { throw null; } + public bool Equals(System.Reflection.Metadata.TypeName? other) { throw null; } + public override int GetHashCode() { throw null; } public int GetArrayRank() { throw null; } public System.Reflection.AssemblyName? GetAssemblyName() { throw null; } public System.Collections.Immutable.ImmutableArray GetGenericArguments() { throw null; } @@ -2433,9 +2436,9 @@ internal TypeName() { } public System.Reflection.Metadata.TypeName GetElementType() { throw null; } public int GetNodeCount() { throw null; } } - public sealed partial class TypeNameParserOptions + public sealed partial class TypeNameParseOptions { - public TypeNameParserOptions() { } + public TypeNameParseOptions() { } public int MaxNodes { get { throw null; } set { } } } public readonly partial struct TypeReference diff --git a/src/libraries/System.Reflection.Metadata/src/Resources/Strings.resx b/src/libraries/System.Reflection.Metadata/src/Resources/Strings.resx index 6c09fb986b692..bcc8499ec5f47 100644 --- a/src/libraries/System.Reflection.Metadata/src/Resources/Strings.resx +++ b/src/libraries/System.Reflection.Metadata/src/Resources/Strings.resx @@ -414,7 +414,7 @@ The name of the type is invalid. - + Maximum node count of {0} exceeded. @@ -423,4 +423,10 @@ 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. + \ 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 4de74c3b0da6c..f1571582cf873 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 diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs index e2323eb5c3867..982d0a0c58f09 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs @@ -107,7 +107,7 @@ public void GetIndexOfFirstInvalidAssemblyNameCharacter_ReturnsFirstInvalidChara { Assert.Equal(expected, TypeNameParserHelpers.GetIndexOfFirstInvalidAssemblyNameCharacter(input.AsSpan(), strictMode: true)); - TypeNameParserOptions strictOptions = new() + TypeNameParseOptions strictOptions = new() { StrictValidation = true }; @@ -134,7 +134,7 @@ public void GetIndexOfFirstInvalidTypeNameCharacter_ReturnsFirstInvalidCharacter { Assert.Equal(expected, TypeNameParserHelpers.GetIndexOfFirstInvalidTypeNameCharacter(input.AsSpan(), strictMode: true)); - TypeNameParserOptions strictOptions = new() + TypeNameParseOptions strictOptions = new() { StrictValidation = true }; diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs index e4e747fb20405..91fe3d53f8912 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs @@ -16,7 +16,7 @@ public class TypeNameParserSamples { internal sealed class SampleSerializationBinder : SerializationBinder { - private static TypeNameParserOptions _options; + private static TypeNameParseOptions _options; // we could use Frozen collections here ;) private readonly static Dictionary _alwaysAllowed = new() diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs similarity index 92% rename from src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs rename to src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs index 4d8e707738d03..8f9bd6a1f3bd4 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs @@ -10,7 +10,7 @@ namespace System.Reflection.Metadata.Tests { - public class TypeNameParserTests + public class TypeNameTests { [Theory] [InlineData(" System.Int32", "System.Int32", "Int32")] @@ -99,14 +99,14 @@ public void UnicodeCharactersAreAllowedByDefault(string input, string expectedFu [InlineData(typeof(Dictionary))] [InlineData(typeof(int[][]))] [InlineData(typeof(Assert))] // xUnit assembly - [InlineData(typeof(TypeNameParserTests))] // test 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())); - Verify(type, expectedAssemblyName, TypeName.Parse(type.AssemblyQualifiedName.AsSpan(), new TypeNameParserOptions() { StrictValidation = true })); + Verify(type, expectedAssemblyName, TypeName.Parse(type.AssemblyQualifiedName.AsSpan(), new TypeNameParseOptions() { StrictValidation = true })); static void Verify(Type type, AssemblyName expectedAssemblyName, TypeName parsed) { @@ -154,7 +154,7 @@ static void Verify(Type type, AssemblyName expectedAssemblyName, TypeName parsed [InlineData("Hello, AssemblyName, publicKeyToken=b77a5c561934e089")] // wrong case (PKT) public void CanNotParseTypeWithInvalidAssemblyName(string fullName) { - TypeNameParserOptions options = new() + TypeNameParseOptions options = new() { StrictValidation = true, }; @@ -170,7 +170,7 @@ public void CanNotParseTypeWithInvalidAssemblyName(string fullName) [InlineData(100, "[]")] public void MaxNodesIsRespected_TooManyDecorators(int maxDepth, string decorator) { - TypeNameParserOptions options = new() + TypeNameParseOptions options = new() { MaxNodes = maxDepth }; @@ -205,7 +205,7 @@ static void ValidateElementType(int maxDepth, TypeName parsed, string decorator) [InlineData(100)] public void MaxNodesIsRespected_TooDeepGenerics(int maxDepth) { - TypeNameParserOptions options = new() + TypeNameParseOptions options = new() { MaxNodes = maxDepth }; @@ -249,7 +249,7 @@ static void Validate(int maxDepth, TypeName parsed) [InlineData(100)] public void MaxNodesIsRespected_TooManyGenericArguments(int maxDepth) { - TypeNameParserOptions options = new() + TypeNameParseOptions options = new() { MaxNodes = maxDepth }; @@ -393,6 +393,7 @@ public void GenericArgumentsAreSupported(string input, string name, string fullN 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(); @@ -402,6 +403,7 @@ public void GenericArgumentsAreSupported(string input, string name, string fullN Assert.Equal(genericTypesFullNames[i], genericArg.FullName); Assert.True(genericArg.IsSimple); Assert.False(genericArg.IsConstructedGenericType); + Assert.Throws(genericArg.GetGenericTypeDefinition); if (assemblyNames is not null) { @@ -451,7 +453,14 @@ public void DecoratorsAreSupported(string input, string typeNameWithoutDecorator Assert.Equal(input, parsed.FullName); Assert.Equal(isArray, parsed.IsArray); Assert.Equal(isSzArray, parsed.IsSZArray); - if (isArray) Assert.Equal(arrayRank, parsed.GetArrayRank()); + 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); @@ -497,7 +506,7 @@ public static IEnumerable GetAdditionalConstructedTypeData() [Theory] [InlineData(typeof(TypeName), 1)] - [InlineData(typeof(TypeNameParserTests), 1)] + [InlineData(typeof(TypeNameTests), 1)] [InlineData(typeof(object), 1)] [InlineData(typeof(Assert), 1)] // xunit [InlineData(typeof(int[]), 2)] @@ -506,7 +515,7 @@ public static IEnumerable GetAdditionalConstructedTypeData() [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 - // TypeNameParserTests+NestedGeneric_0`1+NestedGeneric_1`2[[Int32],[String],[Boolean]] (simplified for brevity) + // 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) @@ -527,6 +536,18 @@ public void IsSimpleReturnsTrueForNestedNonGenericTypes() 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)] @@ -593,6 +614,31 @@ public void ParsedNamesMatchSystemTypeNames(Type type) } } + [Theory] + [InlineData("name", "name", true)] + [InlineData("Name", "Name", true)] + [InlineData("name", "Name", false)] + [InlineData("Name", "name", false)] + [InlineData("type, assembly", "type, assembly", true)] + [InlineData("Type, Assembly", "Type, Assembly", true)] + [InlineData("Type, Assembly", "type, assembly", false)] + [InlineData("Type, assembly", "type, Assembly", false)] + [InlineData("name[]", "name[]", true)] + [InlineData("name[]", "name[*]", false)] + [InlineData("name[]", "name[,]", false)] + [InlineData("name*", "name*", true)] + [InlineData("name&", "name&", true)] + [InlineData("name*", "name&", false)] + [InlineData("generic`1[[int]]", "generic`1[[int]]", true)] // exactly the same + [InlineData("generic`1[[int]]", "generic`1[int]", true)] // different generic args syntax describing same type + [InlineData("generic`2[[int],[bool]]", "generic`2[int,bool]", true)] + public void Equality(string left, string right, bool expected) + { + Assert.Equal(expected, TypeName.Parse(left.AsSpan()).Equals(TypeName.Parse(right.AsSpan()))); + Assert.Equal(TypeName.Parse(left.AsSpan()), TypeName.Parse(left.AsSpan())); + Assert.Equal(TypeName.Parse(right.AsSpan()), TypeName.Parse(right.AsSpan())); + } + [Theory] [InlineData(typeof(int))] [InlineData(typeof(int?))] 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 88f300b3d30d9..134c82d238b6a 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 @@ -39,7 +39,7 @@ - + From c7c67c87f4fa01a027733f00ddec112955b621ce Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 22 Mar 2024 09:40:40 +0100 Subject: [PATCH 30/48] address code review feedback, make some tests conditional, remove invalid assert --- .../Reflection/TypeNameParser.CoreCLR.cs | 8 ++-- .../Reflection/TypeNameParser.NativeAot.cs | 4 +- .../ILVerification/ILVerification.projitems | 3 ++ .../System/Reflection/AssemblyNameParser.cs | 27 ++++--------- .../System/Reflection/Metadata/TypeName.cs | 16 ++++---- .../Reflection/Metadata/TypeNameParser.cs | 3 +- .../Metadata/TypeNameParserHelpers.cs | 40 ++++++------------- .../Reflection/TypeNameParser.Helpers.cs | 2 - .../UnconditionalSuppressMessageAttribute.cs | 2 + .../Metadata/TypeNameParserHelpersTests.cs | 10 ----- .../tests/Metadata/TypeNameParserSamples.cs | 8 ++-- .../tests/Metadata/TypeNameTests.cs | 5 ++- .../System/Reflection/TypeNameParser.Mono.cs | 2 +- 13 files changed, 48 insertions(+), 82 deletions(-) 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 4bd8077986602..55544a6f8ecc8 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 @@ -55,7 +55,7 @@ internal partial struct TypeNameParser return null; } - var parsed = Metadata.TypeNameParser.Parse(typeName, throwOnError: throwOnError); + Metadata.TypeName? parsed = Metadata.TypeNameParser.Parse(typeName, throwOnError: throwOnError); if (parsed is null) { return null; @@ -79,7 +79,7 @@ internal partial struct TypeNameParser bool ignoreCase, Assembly topLevelAssembly) { - var parsed = Metadata.TypeNameParser.Parse(typeName, throwOnError); + Metadata.TypeName? parsed = Metadata.TypeNameParser.Parse(typeName, throwOnError); if (parsed is null) { @@ -110,7 +110,7 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName, RuntimeAssembly requestingAssembly = scope.GetRuntimeAssembly(); - var parsed = Metadata.TypeName.Parse(typeName); + Metadata.TypeName parsed = Metadata.TypeName.Parse(typeName); RuntimeType? type = (RuntimeType?)new TypeNameParser() { _throwOnError = true, @@ -140,7 +140,7 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName, return null; } - var parsed = Metadata.TypeNameParser.Parse(typeName, throwOnError); + Metadata.TypeName? parsed = Metadata.TypeNameParser.Parse(typeName, throwOnError); if (parsed is null) { return null; 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 28d024c929456..fd497ff511620 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 @@ -51,7 +51,7 @@ internal partial struct TypeNameParser return null; } - var parsed = Metadata.TypeNameParser.Parse(typeName, throwOnError); + Metadata.TypeName? parsed = Metadata.TypeNameParser.Parse(typeName, throwOnError); if (parsed is null) { return null; @@ -74,7 +74,7 @@ internal partial struct TypeNameParser bool ignoreCase, Assembly topLevelAssembly) { - var parsed = Metadata.TypeNameParser.Parse(typeName, throwOnError); + Metadata.TypeName? parsed = Metadata.TypeNameParser.Parse(typeName, throwOnError); if (parsed is null) { diff --git a/src/coreclr/tools/ILVerification/ILVerification.projitems b/src/coreclr/tools/ILVerification/ILVerification.projitems index c51a35f3f3510..0264eeeec5de1 100644 --- a/src/coreclr/tools/ILVerification/ILVerification.projitems +++ b/src/coreclr/tools/ILVerification/ILVerification.projitems @@ -87,6 +87,9 @@ Utilities\TypeNameParserHelpers.cs + + System\Diagnostics\CodeAnalysis\UnconditionalSuppressMessageAttribute.cs + Utilities\CustomAttributeTypeNameParser.Helpers diff --git a/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs b/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs index e8891ed17a59f..b9a9562817c7a 100644 --- a/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs @@ -12,9 +12,9 @@ namespace System.Reflection { - // - // Parses an assembly name. - // + /// + /// Parses an assembly name. + /// internal ref partial struct AssemblyNameParser { public readonly struct AssemblyNameParts @@ -35,7 +35,9 @@ public AssemblyNameParts(string name, Version? version, string? cultureName, Ass public readonly byte[]? _publicKeyOrToken; } - // Token categories for the lexer. + /// + /// Token categories for the lexer. + /// private enum Token { Equals = 1, @@ -376,18 +378,7 @@ private static bool TryParseHexNybble(char c, out byte parsed) } private static bool IsWhiteSpace(char ch) - { - switch (ch) - { - case '\n': - case '\r': - case ' ': - case '\t': - return true; - default: - return false; - } - } + => ch is '\n' or '\r' or ' ' or '\t'; [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool TryGetNextChar(out char ch) @@ -450,11 +441,7 @@ private bool TryGetNextToken(out string tokenString, out Token token) } } -#if SYSTEM_PRIVATE_CORELIB ValueStringBuilder sb = new ValueStringBuilder(stackalloc char[64]); -#else - StringBuilder sb = new(64); -#endif char quoteChar = '\0'; if (c == '\'' || c == '\"') diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs index c09d59ce8cdf7..cd76de8870f59 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs @@ -23,7 +23,7 @@ sealed class TypeName : IEquatable private readonly int _rankOrModifier; private readonly TypeName[]? _genericArguments; private readonly AssemblyName? _assemblyName; - private readonly TypeName? _underlyingType; + private readonly TypeName? _elementOrGenericType; private readonly TypeName? _declaringType; private string? _assemblyQualifiedName; @@ -38,12 +38,12 @@ internal TypeName(string name, string fullName, FullName = fullName; _assemblyName = assemblyName; _rankOrModifier = rankOrModifier; - _underlyingType = underlyingType; + _elementOrGenericType = underlyingType; _declaringType = containingType; _genericArguments = genericTypeArguments; - Debug.Assert(!(IsArray || IsPointer || IsByRef) || _underlyingType is not null); - Debug.Assert(_genericArguments is null || _underlyingType is not null); + Debug.Assert(!(IsArray || IsPointer || IsByRef) || _elementOrGenericType is not null); + Debug.Assert(_genericArguments is null || _elementOrGenericType is not null); } /// @@ -117,7 +117,7 @@ public string AssemblyQualifiedName /// This is because determining whether a type truly is a generic type requires loading the type /// and performing a runtime check. /// - public bool IsSimple => _underlyingType is null; + public bool IsSimple => _elementOrGenericType is null; /// /// Returns true if this is a managed pointer type (e.g., "ref int"). @@ -231,7 +231,7 @@ public int GetNodeCount() /// The current type is not an array, pointer or reference. public TypeName GetElementType() => IsArray || IsPointer || IsByRef - ? _underlyingType! + ? _elementOrGenericType! : throw TypeNameParserHelpers.InvalidOperation_NoElement(); /// @@ -243,7 +243,7 @@ public TypeName GetElementType() /// The current type is not a generic type. public TypeName GetGenericTypeDefinition() => IsConstructedGenericType - ? _underlyingType! + ? _elementOrGenericType! : throw TypeNameParserHelpers.InvalidOperation_NotGenericType(); /// @@ -300,7 +300,7 @@ public int GetArrayRank() } #if SYSTEM_PRIVATE_CORELIB - return _assemblyName; // no need for a copy in CoreLib + return _assemblyName; // no need for a copy in CoreLib (it's internal) #else return (AssemblyName)_assemblyName.Clone(); #endif diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs index 7686d93ea25fb..a8a94ddf96f16 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs @@ -14,7 +14,6 @@ namespace System.Reflection.Metadata [DebuggerDisplay("{_inputString}")] internal ref struct TypeNameParser { - private const int MaxArrayRank = 32; private static readonly TypeNameParseOptions _defaults = new(); private readonly bool _throwOnError; private readonly TypeNameParseOptions _parseOptions; @@ -29,7 +28,7 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse internal static TypeName? Parse(ReadOnlySpan typeName, bool throwOnError, TypeNameParseOptions? options = default) { - ReadOnlySpan trimmedName = TrimStart(typeName); // whitespaces at beginning are always OK + ReadOnlySpan trimmedName = typeName.TrimStart(); // whitespaces at beginning are always OK if (trimmedName.IsEmpty) { // whitespace input needs to report the error index as 0 diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs index 67b07af8f77ba..92632ed6c02be 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs @@ -16,12 +16,12 @@ namespace System.Reflection.Metadata { internal static class TypeNameParserHelpers { + internal const int MaxArrayRank = 32; internal const int SZArray = -1; internal const int Pointer = -2; internal const int ByRef = -3; private const char EscapeCharacter = '\\'; #if NET8_0_OR_GREATER - private static readonly SearchValues _endOfTypeNameDelimitersSearchValues = SearchValues.Create(".+"); private static readonly SearchValues _endOfFullTypeNameDelimitersSearchValues = SearchValues.Create("[]&*,+\\"); #endif @@ -474,17 +474,13 @@ internal static int GetIndexOfFirstInvalidTypeNameCharacter(ReadOnlySpan i internal static ReadOnlySpan GetName(ReadOnlySpan fullName) { -#if NET8_0_OR_GREATER - int offset = fullName.LastIndexOfAny(_endOfTypeNameDelimitersSearchValues); - Debug.Assert(offset != 0, "The provided full name must be valid"); + 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); } -#else - int offset = GetUnescapedOffset(fullName, startIndex: fullName.Length - 1); -#endif + return offset < 0 ? fullName : fullName.Slice(offset + 1); static int GetUnescapedOffset(ReadOnlySpan fullName, int startIndex) @@ -520,22 +516,14 @@ static string ArrayRankToString(int arrayRank) { Debug.Assert(arrayRank >= 2 && arrayRank <= 32); -#if NET8_0_OR_GREATER - return string.Create(2 + arrayRank - 1, arrayRank, (buffer, rank) => - { - buffer[0] = '['; - for (int i = 1; i < rank; i++) - buffer[i] = ','; - buffer[^1] = ']'; - }); -#else - ValueStringBuilder sb = new(stackalloc char[16]); - sb.Append('['); + Span buffer = stackalloc char[2 + MaxArrayRank - 1]; + buffer[0] = '['; for (int i = 1; i < arrayRank; i++) - sb.Append(','); - sb.Append(']'); - return sb.ToString(); -#endif + { + buffer[i] = ','; + } + buffer[arrayRank] = ']'; + return buffer.Slice(0, arrayRank + 1).ToString(); } } @@ -549,13 +537,13 @@ internal static bool IsBeginningOfGenericAgs(ref ReadOnlySpan span, out bo if (!span.IsEmpty && span[0] == '[') { // There are no spaces allowed before the first '[', but spaces are allowed after that. - ReadOnlySpan trimmed = TrimStart(span.Slice(1)); + ReadOnlySpan trimmed = span.Slice(1).TrimStart(); if (!trimmed.IsEmpty) { if (trimmed[0] == '[') { doubleBrackets = true; - span = TrimStart(trimmed.Slice(1)); + span = trimmed.Slice(1).TrimStart(); return true; } if (!(trimmed[0] is ',' or '*' or ']')) // [] or [*] or [,] or [,,,, ...] @@ -569,8 +557,6 @@ internal static bool IsBeginningOfGenericAgs(ref ReadOnlySpan span, out bo return false; } - internal static ReadOnlySpan TrimStart(ReadOnlySpan input) => input.TrimStart(); - internal static bool TryGetTypeNameInfo(ref ReadOnlySpan input, ref List? nestedNameLengths, out int totalLength, out int genericArgCount) { @@ -686,7 +672,7 @@ internal static bool TryStripFirstCharAndTrailingSpaces(ref ReadOnlySpan s { if (!span.IsEmpty && span[0] == value) { - span = TrimStart(span.Slice(1)); + span = span.Slice(1).TrimStart(); return true; } return false; diff --git a/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs index 5bc412f4974c5..8e3949becb3d7 100644 --- a/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs +++ b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs @@ -146,10 +146,8 @@ private static (string typeNamespace, string name) SplitFullTypeName(string type } } -#if !NETSTANDARD2_0 // needed for ILVerification project [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2055:UnrecognizedReflectionPattern", Justification = "Used to implement resolving types from strings.")] -#endif private Type? Make(Type? type, Metadata.TypeName typeName) { if (type is null || typeName.IsSimple) 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 2d82ed0c0e7f5..5ee6c949bc97e 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.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs index 982d0a0c58f09..b0685926c0071 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs @@ -211,16 +211,6 @@ public void IsBeginningOfGenericAgsHandlesAllCasesProperly(string input, bool ex Assert.Equal(expectedConsumedInput, inputSpan.ToString()); } - [Theory] - [InlineData(" \t\r\nA.B.C", "A.B.C")] - [InlineData(" A.B.C\t\r\n", "A.B.C\t\r\n")] // don't trim the end - public void TrimStartTrimsAllWhitespaces(string input, string expectedResult) - { - ReadOnlySpan inputSpan = input.AsSpan(); - - Assert.Equal(expectedResult, TypeNameParserHelpers.TrimStart(inputSpan).ToString()); - } - [Theory] [InlineData("A.B.C", true, null, 5, 0)] [InlineData("A.B.C\\", false, null, 0, 0)] // invalid type name: ends with escape character diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs index 91fe3d53f8912..f1ee08958d0a5 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs @@ -131,7 +131,7 @@ public class CustomUserDefinedType public CustomUserDefinedType[] ArrayOfCustomUserDefinedTypes { get; set; } } - [Fact] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsBinaryFormatterSupported))] public void CanDeserializeCustomUserDefinedType() { CustomUserDefinedType parent = new() @@ -175,7 +175,7 @@ public void CanDeserializeCustomUserDefinedType() } } - [Fact] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsBinaryFormatterSupported))] public void CanDeserializeDictionaryUsingNonPublicComparerType() { Dictionary dictionary = new(StringComparer.CurrentCulture) @@ -197,7 +197,7 @@ public void CanDeserializeDictionaryUsingNonPublicComparerType() Assert.Equal(dictionary, deserialized); } - [Fact] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsBinaryFormatterSupported))] public void CanDeserializeArraysOfArrays() { int[][] arrayOfArrays = new int[10][]; @@ -216,7 +216,7 @@ public void CanDeserializeArraysOfArrays() } } - [Fact] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsBinaryFormatterSupported))] public void CanDeserializeListOfListOfInt() { List> listOfListOfInts = new(10); diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs index 8f9bd6a1f3bd4..cd1a414ed8704 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs @@ -49,7 +49,7 @@ public void EmptyStringsAreNotAllowed(string input) [Theory] [InlineData("Namespace.Containing++Nested")] // a pair of '++' - [InlineData("TypeNameFollowedBySome[] crap")] // unconsumed characters + [InlineData("TypeNameFollowedBySome[] unconsumedCharacters")] [InlineData("MissingAssemblyName, ")] [InlineData("ExtraComma, ,")] [InlineData("ExtraComma, , System.Runtime")] @@ -564,7 +564,8 @@ public void IsVariableBoundArrayTypeReturnsTrueForNonSZArrays(string typeName, b public static IEnumerable GetTypesThatRequireEscaping() { - if (PlatformDetection.IsReflectionEmitSupported) + 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"); 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 7ebf7082578a6..8193358316cd9 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,7 +39,7 @@ internal unsafe ref partial struct TypeNameParser return null; } - var parsed = Metadata.TypeNameParser.Parse(typeName, throwOnError: throwOnError); + Metadata.TypeName? parsed = Metadata.TypeNameParser.Parse(typeName, throwOnError: throwOnError); if (parsed is null) { return null; From 87804e96b299962ab8e3f30fa7a96ced8b43e4c1 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 22 Mar 2024 12:24:50 +0100 Subject: [PATCH 31/48] Apply suggestions from code review Co-authored-by: Jan Kotas --- .../Common/src/System/Reflection/Metadata/TypeNameParser.cs | 2 +- .../tests/Metadata/TypeNameParserSamples.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs index a8a94ddf96f16..836b43af1ecc4 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs @@ -270,7 +270,7 @@ private bool TryParseAssemblyName(ref AssemblyName? assemblyName) return false; } - assemblyName = new(); + assemblyName = new AssemblyName(); #if SYSTEM_PRIVATE_CORELIB assemblyName.Init(parts); #else diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs index f1ee08958d0a5..3176d6e8196f0 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs @@ -56,7 +56,7 @@ public SampleSerializationBinder(Type[]? allowedTypes = null) return type; } - _options ??= new() // there is no need for lazy initialization, I just wanted to have everything important in one method + _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 From 97aad27f1e0ce2900d005f00752304c96921b1e8 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 22 Mar 2024 13:16:56 +0100 Subject: [PATCH 32/48] supress IL3050:RequiresDynamicCode --- .../Common/src/System/Reflection/TypeNameParser.Helpers.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs index 8e3949becb3d7..e46fa95e19046 100644 --- a/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs +++ b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs @@ -148,6 +148,8 @@ private static (string typeNamespace, string name) SplitFullTypeName(string 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? Make(Type? type, Metadata.TypeName typeName) { if (type is null || typeName.IsSimple) From f9407232f6a4582d853e869082cd41cb051c1f5e Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 22 Mar 2024 16:41:35 +0100 Subject: [PATCH 33/48] remove everything related to strict parsing (it will come back in a similar form but with a different name) --- .../ILVerification/ILVerification.projitems | 3 - .../System/Reflection/AssemblyNameParser.cs | 38 +-- .../AssemblyNameParser.netstandard.cs | 77 ----- .../Reflection/Metadata/TypeNameParser.cs | 8 +- .../Metadata/TypeNameParserHelpers.cs | 311 ------------------ .../src/System.Reflection.Metadata.csproj | 3 - .../Metadata/TypeNameParserHelpersTests.cs | 78 ----- .../tests/Metadata/TypeNameTests.cs | 34 -- 8 files changed, 7 insertions(+), 545 deletions(-) delete mode 100644 src/libraries/Common/src/System/Reflection/AssemblyNameParser.netstandard.cs diff --git a/src/coreclr/tools/ILVerification/ILVerification.projitems b/src/coreclr/tools/ILVerification/ILVerification.projitems index 0264eeeec5de1..7c205fbbf7188 100644 --- a/src/coreclr/tools/ILVerification/ILVerification.projitems +++ b/src/coreclr/tools/ILVerification/ILVerification.projitems @@ -72,9 +72,6 @@ Utilities\AssemblyNameParser.cs - - Utilities\AssemblyNameParser.netstandard.cs - Utilities\TypeName.cs diff --git a/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs b/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs index b9a9562817c7a..8c903bf24fe9c 100644 --- a/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs @@ -57,10 +57,9 @@ private enum AttributeKind } private readonly ReadOnlySpan _input; - private readonly bool _strict; private int _index; - private AssemblyNameParser(ReadOnlySpan input, bool strict = false) + private AssemblyNameParser(ReadOnlySpan input) { #if SYSTEM_PRIVATE_CORELIB if (input.Length == 0) @@ -70,7 +69,6 @@ private AssemblyNameParser(ReadOnlySpan input, bool strict = false) #endif _input = input; - _strict = strict; _index = 0; } @@ -89,9 +87,9 @@ public static AssemblyNameParts Parse(ReadOnlySpan name) } #endif - internal static bool TryParse(ReadOnlySpan name, bool strict, ref AssemblyNameParts parts) + internal static bool TryParse(ReadOnlySpan name, ref AssemblyNameParts parts) { - AssemblyNameParser parser = new(name, strict); + AssemblyNameParser parser = new(name); return parser.TryParse(ref parts); } @@ -170,11 +168,6 @@ private bool TryParse(ref AssemblyNameParts result) return false; } } - else if (_strict) - { - // it's either unrecognized or not on the allow list (Version, Culture and PublicKeyToken) - return false; - } else if (IsAttribute(attributeName, "PublicKey")) { if (!TryRecordNewSeen(ref alreadySeen, AttributeKind.PublicKeyOrToken)) @@ -250,8 +243,8 @@ private bool TryParse(ref AssemblyNameParts result) return true; } - private bool IsAttribute(string candidate, string attributeKind) - => candidate.Equals(attributeKind, _strict ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase); + private static bool IsAttribute(string candidate, string attributeKind) + => candidate.Equals(attributeKind, StringComparison.OrdinalIgnoreCase); private static bool TryParseVersion(string attributeValue, ref Version? version) { @@ -302,18 +295,13 @@ private static bool TryParseVersion(string attributeValue, ref Version? version) return true; } - private bool TryParseCulture(string attributeValue, out string? result) + private static bool TryParseCulture(string attributeValue, out string? result) { if (attributeValue.Equals("Neutral", StringComparison.OrdinalIgnoreCase)) { result = ""; return true; } - else if (_strict && !IsPredefinedCulture(attributeValue)) - { - result = null; - return false; - } result = attributeValue; return true; @@ -537,19 +525,5 @@ private bool TryGetNextToken(out string tokenString, out Token token) token = Token.String; return true; } - -#if NET8_0_OR_GREATER - private static bool IsPredefinedCulture(string cultureName) - { - try - { - return CultureInfo.GetCultureInfo(cultureName, predefinedOnly: true) is not null; - } - catch (CultureNotFoundException) - { - return false; - } - } -#endif } } diff --git a/src/libraries/Common/src/System/Reflection/AssemblyNameParser.netstandard.cs b/src/libraries/Common/src/System/Reflection/AssemblyNameParser.netstandard.cs deleted file mode 100644 index 8787f6c6d27e3..0000000000000 --- a/src/libraries/Common/src/System/Reflection/AssemblyNameParser.netstandard.cs +++ /dev/null @@ -1,77 +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.Collections.Generic; -using System.Globalization; - -#nullable enable - -namespace System.Reflection -{ - internal ref partial struct AssemblyNameParser - { - private static HashSet? _predefinedCultureNames; - private static readonly object _predefinedCultureNamesLock = new object(); - - private static bool IsPredefinedCulture(string cultureName) - { - if (cultureName is null) - { - return false; - } - - if (_predefinedCultureNames is null) - { - lock (_predefinedCultureNamesLock) - { - _predefinedCultureNames ??= GetPredefinedCultureNames(); - } - } - - return _predefinedCultureNames.Contains(AnsiToLower(cultureName)); - - static HashSet GetPredefinedCultureNames() - { - HashSet result = new(StringComparer.Ordinal); - foreach (CultureInfo culture in CultureInfo.GetCultures(CultureTypes.AllCultures)) - { - if (culture.Name is not null) - { - result.Add(AnsiToLower(culture.Name)); - } - } - return result; - } - - // Like CultureInfo, only maps [A-Z] -> [a-z]. - // All non-ASCII characters are left alone. - static string AnsiToLower(string input) - { - int idx; - for (idx = 0; idx < input.Length; idx++) - { - if (input[idx] is >= 'A' and <= 'Z') - { - break; - } - } - - if (idx == input.Length) - { - return input; // no characters to change. - } - - char[] chars = input.ToCharArray(); - for (; idx < chars.Length; idx++) - { - char c = chars[idx]; - if (input[idx] is >= 'A' and <= 'Z') - { - chars[idx] = (char)(c | 0x20); - } - } - return new string(chars); - } - } - } -} diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs index 836b43af1ecc4..0f479503df230 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs @@ -72,11 +72,6 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse } ReadOnlySpan fullTypeName = _inputString.Slice(0, fullTypeNameLength); - int invalidCharIndex = GetIndexOfFirstInvalidTypeNameCharacter(fullTypeName, _parseOptions.StrictValidation); - if (invalidCharIndex >= 0) - { - return null; - } _inputString = _inputString.Slice(fullTypeNameLength); int genericArgIndex = 0; @@ -264,8 +259,7 @@ private bool TryParseAssemblyName(ref AssemblyName? assemblyName) ReadOnlySpan candidate = _inputString.Slice(0, assemblyNameLength); AssemblyNameParser.AssemblyNameParts parts = default; - if (GetIndexOfFirstInvalidAssemblyNameCharacter(candidate, _parseOptions.StrictValidation) >= 0 - || !AssemblyNameParser.TryParse(candidate, _parseOptions.StrictValidation, ref parts)) + if (!AssemblyNameParser.TryParse(candidate, ref parts)) { return false; } diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs index 92632ed6c02be..26e9c00276751 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs @@ -161,317 +161,6 @@ static int GetUnescapedOffset(ReadOnlySpan input, int startOffset) static bool NeedsEscaping(char c) => c is '[' or ']' or '&' or '*' or ',' or '+' or EscapeCharacter; } - // this method checks for a single banned char, not for invalid combinations of characters like invalid escaping - private static int GetIndexOfFirstInvalidCharacter(ReadOnlySpan input, bool strictMode, bool assemblyName) - { - if (input.IsEmpty) - { - return 0; - } - else if (!strictMode) - { - return -1; - } - - ReadOnlySpan allowedAsciiCharsMap = assemblyName - ? GetAssemblyNameAsciiCharsAllowMap() - : GetTypeNameAsciiCharsAllowMap(); - - Debug.Assert(allowedAsciiCharsMap.Length == 128); - - for (int i = 0; i < input.Length; i++) - { - char c = input[i]; - if (c < (uint)allowedAsciiCharsMap.Length) - { - // ASCII - fast track - if (!allowedAsciiCharsMap[c]) - { - return i; - } - } - else - { - if (IsControl(c) || IsWhiteSpace(c)) - { - return i; - } - } - } - - return -1; - - static ReadOnlySpan GetAssemblyNameAsciiCharsAllowMap() => - [ - false, // U+0000 (NUL) - false, // U+0001 (SOH) - false, // U+0002 (STX) - false, // U+0003 (ETX) - false, // U+0004 (EOT) - false, // U+0005 (ENQ) - false, // U+0006 (ACK) - false, // U+0007 (BEL) - false, // U+0008 (BS) - false, // U+0009 (TAB) - false, // U+000A (LF) - false, // U+000B (VT) - false, // U+000C (FF) - false, // U+000D (CR) - false, // U+000E (SO) - false, // U+000F (SI) - false, // U+0010 (DLE) - false, // U+0011 (DC1) - false, // U+0012 (DC2) - false, // U+0013 (DC3) - false, // U+0014 (DC4) - false, // U+0015 (NAK) - false, // U+0016 (SYN) - false, // U+0017 (ETB) - false, // U+0018 (CAN) - false, // U+0019 (EM) - false, // U+001A (SUB) - false, // U+001B (ESC) - false, // U+001C (FS) - false, // U+001D (GS) - false, // U+001E (RS) - false, // U+001F (US) - true, // U+0020 ' ' - true, // U+0021 '!' - false, // U+0022 '"' - true, // U+0023 '#' - true, // U+0024 '$' - true, // U+0025 '%' - true, // U+0026 '&' - false, // U+0027 ''' - true, // U+0028 '(' - true, // U+0029 ')' - false, // U+002A '*' - true, // U+002B '+' - true, // U+002C ',' - true, // U+002D '-' - true, // U+002E '.' - false, // U+002F '/' - true, // U+0030 '0' - true, // U+0031 '1' - true, // U+0032 '2' - true, // U+0033 '3' - true, // U+0034 '4' - true, // U+0035 '5' - true, // U+0036 '6' - true, // U+0037 '7' - true, // U+0038 '8' - true, // U+0039 '9' - false, // U+003A ':' - true, // U+003B ';' - true, // U+003C '<' - true, // U+003D '=' - true, // U+003E '>' - false, // U+003F '?' - true, // U+0040 '@' - true, // U+0041 'A' - true, // U+0042 'B' - true, // U+0043 'C' - true, // U+0044 'D' - true, // U+0045 'E' - true, // U+0046 'F' - true, // U+0047 'G' - true, // U+0048 'H' - true, // U+0049 'I' - true, // U+004A 'J' - true, // U+004B 'K' - true, // U+004C 'L' - true, // U+004D 'M' - true, // U+004E 'N' - true, // U+004F 'O' - true, // U+0050 'P' - true, // U+0051 'Q' - true, // U+0052 'R' - true, // U+0053 'S' - true, // U+0054 'T' - true, // U+0055 'U' - true, // U+0056 'V' - true, // U+0057 'W' - true, // U+0058 'X' - true, // U+0059 'Y' - true, // U+005A 'Z' - false, // U+005B '[' - false, // U+005C '\' - false, // U+005D ']' - true, // U+005E '^' - true, // U+005F '_' - true, // U+0060 '`' - true, // U+0061 'a' - true, // U+0062 'b' - true, // U+0063 'c' - true, // U+0064 'd' - true, // U+0065 'e' - true, // U+0066 'f' - true, // U+0067 'g' - true, // U+0068 'h' - true, // U+0069 'i' - true, // U+006A 'j' - true, // U+006B 'k' - true, // U+006C 'l' - true, // U+006D 'm' - true, // U+006E 'n' - true, // U+006F 'o' - true, // U+0070 'p' - true, // U+0071 'q' - true, // U+0072 'r' - true, // U+0073 's' - true, // U+0074 't' - true, // U+0075 'u' - true, // U+0076 'v' - true, // U+0077 'w' - true, // U+0078 'x' - true, // U+0079 'y' - true, // U+007A 'z' - true, // U+007B '{' - true, // U+007C '|' - true, // U+007D '}' - true, // U+007E '~' - false, // U+007F (DEL) - ]; - - static ReadOnlySpan GetTypeNameAsciiCharsAllowMap() => - [ - false, // U+0000 (NUL) - false, // U+0001 (SOH) - false, // U+0002 (STX) - false, // U+0003 (ETX) - false, // U+0004 (EOT) - false, // U+0005 (ENQ) - false, // U+0006 (ACK) - false, // U+0007 (BEL) - false, // U+0008 (BS) - false, // U+0009 (TAB) - false, // U+000A (LF) - false, // U+000B (VT) - false, // U+000C (FF) - false, // U+000D (CR) - false, // U+000E (SO) - false, // U+000F (SI) - false, // U+0010 (DLE) - false, // U+0011 (DC1) - false, // U+0012 (DC2) - false, // U+0013 (DC3) - false, // U+0014 (DC4) - false, // U+0015 (NAK) - false, // U+0016 (SYN) - false, // U+0017 (ETB) - false, // U+0018 (CAN) - false, // U+0019 (EM) - false, // U+001A (SUB) - false, // U+001B (ESC) - false, // U+001C (FS) - false, // U+001D (GS) - false, // U+001E (RS) - false, // U+001F (US) - false, // U+0020 ' ' - true, // U+0021 '!' - false, // U+0022 '"' - true, // U+0023 '#' - true, // U+0024 '$' - true, // U+0025 '%' - false, // U+0026 '&' - false, // U+0027 ''' - true, // U+0028 '(' - true, // U+0029 ')' - false, // U+002A '*' - true, // U+002B '+' - false, // U+002C ',' - true, // U+002D '-' - true, // U+002E '.' - false, // U+002F '/' - true, // U+0030 '0' - true, // U+0031 '1' - true, // U+0032 '2' - true, // U+0033 '3' - true, // U+0034 '4' - true, // U+0035 '5' - true, // U+0036 '6' - true, // U+0037 '7' - true, // U+0038 '8' - true, // U+0039 '9' - false, // U+003A ':' - false, // U+003B ';' - true, // U+003C '<' - true, // U+003D '=' - true, // U+003E '>' - false, // U+003F '?' - true, // U+0040 '@' - true, // U+0041 'A' - true, // U+0042 'B' - true, // U+0043 'C' - true, // U+0044 'D' - true, // U+0045 'E' - true, // U+0046 'F' - true, // U+0047 'G' - true, // U+0048 'H' - true, // U+0049 'I' - true, // U+004A 'J' - true, // U+004B 'K' - true, // U+004C 'L' - true, // U+004D 'M' - true, // U+004E 'N' - true, // U+004F 'O' - true, // U+0050 'P' - true, // U+0051 'Q' - true, // U+0052 'R' - true, // U+0053 'S' - true, // U+0054 'T' - true, // U+0055 'U' - true, // U+0056 'V' - true, // U+0057 'W' - true, // U+0058 'X' - true, // U+0059 'Y' - true, // U+005A 'Z' - false, // U+005B '[' - false, // U+005C '\' - false, // U+005D ']' - true, // U+005E '^' - true, // U+005F '_' - true, // U+0060 '`' - true, // U+0061 'a' - true, // U+0062 'b' - true, // U+0063 'c' - true, // U+0064 'd' - true, // U+0065 'e' - true, // U+0066 'f' - true, // U+0067 'g' - true, // U+0068 'h' - true, // U+0069 'i' - true, // U+006A 'j' - true, // U+006B 'k' - true, // U+006C 'l' - true, // U+006D 'm' - true, // U+006E 'n' - true, // U+006F 'o' - true, // U+0070 'p' - true, // U+0071 'q' - true, // U+0072 'r' - true, // U+0073 's' - true, // U+0074 't' - true, // U+0075 'u' - true, // U+0076 'v' - true, // U+0077 'w' - true, // U+0078 'x' - true, // U+0079 'y' - true, // U+007A 'z' - true, // U+007B '{' - true, // U+007C '|' - true, // U+007D '}' - true, // U+007E '~' - false, // U+007F (DEL) - ]; - } - - internal static int GetIndexOfFirstInvalidAssemblyNameCharacter(ReadOnlySpan input, bool strictMode) - => GetIndexOfFirstInvalidCharacter(input, strictMode, assemblyName: true); - - internal static int GetIndexOfFirstInvalidTypeNameCharacter(ReadOnlySpan input, bool strictMode) - => GetIndexOfFirstInvalidCharacter(input, strictMode, assemblyName: false); - internal static ReadOnlySpan GetName(ReadOnlySpan fullName) { int offset = fullName.LastIndexOfAny('.', '+'); 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 f1571582cf873..7c683a07db249 100644 --- a/src/libraries/System.Reflection.Metadata/src/System.Reflection.Metadata.csproj +++ b/src/libraries/System.Reflection.Metadata/src/System.Reflection.Metadata.csproj @@ -254,9 +254,6 @@ The System.Reflection.Metadata library is built-in as part of the shared framewo - diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs index b0685926c0071..e5edf182c22f4 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs @@ -73,84 +73,6 @@ public void GetFullTypeNameLengthReturnsExpectedValue(string input, int expected Assert.Equal(expectedIsNested, isNested); } - public static IEnumerable InvalidNamesArguments() - { - yield return new object[] { "", 0 }; - yield return new object[] { "\0NullCharacterIsNotAllowed", 0 }; - yield return new object[] { "Null\0CharacterIsNotAllowed", 4 }; - yield return new object[] { "NullCharacterIsNotAllowed\0", 25 }; - yield return new object[] { "\bBackspaceIsNotAllowed", 0 }; - yield return new object[] { "EscapingIsNotAllowed\\", 20 }; - yield return new object[] { "EscapingIsNotAllowed\\\\", 20 }; - yield return new object[] { "EscapingIsNotAllowed\\*", 20 }; - yield return new object[] { "EscapingIsNotAllowed\\&", 20 }; - yield return new object[] { "EscapingIsNotAllowed\\+", 20 }; - yield return new object[] { "EscapingIsNotAllowed\\[", 20 }; - yield return new object[] { "EscapingIsNotAllowed\\]", 20 }; - yield return new object[] { "Slash/IsNotAllowed", 5 }; - yield return new object[] { "WhitespacesAre\tNotAllowed", 14 }; - yield return new object[] { "WhitespacesAreNot\r\nAllowed", 17 }; - yield return new object[] { "Question?MarkIsNotAllowed", 8 }; - yield return new object[] { "Quotes\"AreNotAllowed", 6 }; - yield return new object[] { "Quote'IsNotAllowed", 5 }; - yield return new object[] { "abcdefghijklmnopqrstuvwxyz", -1 }; - yield return new object[] { "ABCDEFGHIJKLMNOPQRSTUVWXYZ", -1 }; - yield return new object[] { "0123456789", -1 }; - yield return new object[] { "BacktickIsOk`1", -1 }; - } - - [Theory] - [MemberData(nameof(InvalidNamesArguments))] - [InlineData("Spaces AreAllowed", -1)] - [InlineData("!@#$%^()-_{}|<>.~&;", -1)] - public void GetIndexOfFirstInvalidAssemblyNameCharacter_ReturnsFirstInvalidCharacter(string input, int expected) - { - Assert.Equal(expected, TypeNameParserHelpers.GetIndexOfFirstInvalidAssemblyNameCharacter(input.AsSpan(), strictMode: true)); - - TypeNameParseOptions strictOptions = new() - { - StrictValidation = true - }; - - string assemblyQualifiedName = $"Namespace.CorrectTypeName, {input}"; - - if (expected >= 0) - { - Assert.False(TypeName.TryParse(assemblyQualifiedName.AsSpan(), out _, strictOptions)); - Assert.Throws(() => TypeName.Parse(assemblyQualifiedName.AsSpan(), strictOptions)); - } - else - { - Assert.True(TypeName.TryParse(assemblyQualifiedName.AsSpan(), out TypeName parsed, strictOptions)); - Assert.Equal(assemblyQualifiedName, parsed.AssemblyQualifiedName); - } - } - - [Theory] - [MemberData(nameof(InvalidNamesArguments))] - [InlineData("Spaces AreNotAllowed", 6)] - [InlineData("!@#$%^()-_={}|<>.~", -1)] - public void GetIndexOfFirstInvalidTypeNameCharacter_ReturnsFirstInvalidCharacter(string input, int expected) - { - Assert.Equal(expected, TypeNameParserHelpers.GetIndexOfFirstInvalidTypeNameCharacter(input.AsSpan(), strictMode: true)); - - TypeNameParseOptions strictOptions = new() - { - StrictValidation = true - }; - - if (expected >= 0) - { - Assert.False(TypeName.TryParse(input.AsSpan(), out _, strictOptions)); - Assert.Throws(() => TypeName.Parse(input.AsSpan(), strictOptions)); - } - else - { - Assert.True(TypeName.TryParse(input.AsSpan(), out TypeName parsed, strictOptions)); - Assert.Equal(input, parsed.FullName); - } - } - [Theory] [InlineData("JustTypeName", "JustTypeName")] [InlineData("Namespace.TypeName", "TypeName")] diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs index cd1a414ed8704..62785cbaa1314 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs @@ -106,7 +106,6 @@ public void TypeNameCanContainAssemblyName(Type type) AssemblyName expectedAssemblyName = new(type.Assembly.FullName); Verify(type, expectedAssemblyName, TypeName.Parse(type.AssemblyQualifiedName.AsSpan())); - Verify(type, expectedAssemblyName, TypeName.Parse(type.AssemblyQualifiedName.AsSpan(), new TypeNameParseOptions() { StrictValidation = true })); static void Verify(Type type, AssemblyName expectedAssemblyName, TypeName parsed) { @@ -130,39 +129,6 @@ static void Verify(Type type, AssemblyName expectedAssemblyName, TypeName parsed } } - [Theory] - [InlineData("Hello,")] // trailing comma - [InlineData("Hello, ")] // trailing comma - [InlineData("Hello, ./../PathToA.dll")] // path to a file! - [InlineData("Hello, .\\..\\PathToA.dll")] // path to a file! - [InlineData("Hello, AssemblyName, Version=1.2\0.3.4")] // embedded null in Version (the Version class normally allows this) - [InlineData("Hello, AssemblyName, Version=1.2 .3.4")] // extra space in Version (the Version class normally allows this) - [InlineData("Hello, AssemblyName, Version=1.2.3.4, Version=1.2.3.4")] // duplicate Versions specified - [InlineData("Hello, AssemblyName, Culture=neutral, Culture=neutral")] // duplicate Culture specified - [InlineData("Hello, AssemblyName, PublicKeyToken=b77a5c561934e089, PublicKeyToken=b77a5c561934e089")] // duplicate PublicKeyToken specified - [InlineData("Hello, AssemblyName, PublicKeyToken=bad")] // invalid PKT - [InlineData("Hello, AssemblyName, Culture=en-US_XYZ")] // invalid culture - [InlineData("Hello, AssemblyName, \r\nCulture=en-US")] // disallowed whitespace - [InlineData("Hello, AssemblyName, Version=1.2.3.4,")] // another trailing comma - [InlineData("Hello, AssemblyName, Version=1.2.3.4, =")] // malformed key=token pair - [InlineData("Hello, AssemblyName, Version=1.2.3.4, Architecture=x86")] // Architecture disallowed - [InlineData("Hello, AssemblyName, CodeBase=file://blah")] // CodeBase disallowed (and illegal path chars) - [InlineData("Hello, AssemblyName, CodeBase=legalChars")] // CodeBase disallowed - [InlineData("Hello, AssemblyName, Unrecognized=some")] // not on the allow list? disallowed - [InlineData("Hello, AssemblyName, version=1.2.3.4")] // wrong case (Version) - [InlineData("Hello, AssemblyName, culture=neutral")] // wrong case (Culture) - [InlineData("Hello, AssemblyName, publicKeyToken=b77a5c561934e089")] // wrong case (PKT) - public void CanNotParseTypeWithInvalidAssemblyName(string fullName) - { - TypeNameParseOptions options = new() - { - StrictValidation = true, - }; - - Assert.False(TypeName.TryParse(fullName.AsSpan(), out _, options)); - Assert.Throws(() => TypeName.Parse(fullName.AsSpan(), options)); - } - [Theory] [InlineData(10, "*")] // pointer to pointer [InlineData(10, "[]")] // array of arrays From b1ed2503033b2fa902f54eb83088b69a443cf7cb Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 22 Mar 2024 17:00:19 +0100 Subject: [PATCH 34/48] remove special handling for Array.MaxLength-many generic args --- .../Reflection/Metadata/TypeNameParserHelpers.cs | 14 +++----------- .../tests/Metadata/TypeNameParserHelpersTests.cs | 9 --------- .../tests/Metadata/TypeNameTests.cs | 1 - 3 files changed, 3 insertions(+), 21 deletions(-) diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs index 26e9c00276751..698f38d7fa6ac 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs @@ -6,7 +6,6 @@ using System.Diagnostics; using System.Text; -using static System.Array; using static System.Char; using static System.Int32; @@ -54,14 +53,9 @@ internal static int GetGenericArgumentCount(ReadOnlySpan fullTypeName) else if (TryParse(fullTypeName.Slice(backtickIndex + 1), out int value)) { // From C# 2.0 language spec: 8.16.3 Multiple type parameters Generic type declarations can have any number of type parameters. - if (value > MaxLength) - { - // But.. it's impossible to create a type with more than Array.MaxLength. - // OOM is also not welcomed in the parser! - return -1; - } - - // the value can still be negative, but it's fine as the caller should treat that as an error + // There is no special treatment for values larger than Array.MaxLength, + // as the parser should simply prevent from parsing that many nodes. + // The value can still be negative, but it's fine as the caller should treat that as an error. return value; } @@ -412,8 +406,6 @@ internal static InvalidOperationException InvalidOperation_HasToBeArrayClass() = #endif #if !NETCOREAPP - private const int MaxLength = 2147483591; - private static bool TryParse(ReadOnlySpan input, out int value) => int.TryParse(input.ToString(), out value); private static bool IsAsciiDigit(char ch) => ch >= '0' && ch <= '9'; diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs index e5edf182c22f4..05337c2422520 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs @@ -11,18 +11,9 @@ public class TypeNameParserHelpersTests { public static IEnumerable GetGenericArgumentCountReturnsExpectedValue_Args() { - int maxArrayLength = -#if NETCOREAPP - Array.MaxLength; -#else - 2147483591; -#endif - yield return new object[] { $"TooLargeForInt`{long.MaxValue}", -1 }; yield return new object[] { $"TooLargeForInt`{(long)int.MaxValue + 1}", -1 }; yield return new object[] { $"TooLargeForInt`{(long)uint.MaxValue + 1}", -1 }; - yield return new object[] { $"MaxArrayLength`{maxArrayLength}", maxArrayLength }; - yield return new object[] { $"TooLargeForAnArray`{maxArrayLength + 1}", -1 }; } [Theory] diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs index 62785cbaa1314..4d72468d80fd6 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs @@ -63,7 +63,6 @@ public void EmptyStringsAreNotAllowed(string input) [InlineData("TooFewGenericArgumentsSingleSquareBracketTwoDigits'10[1,2,3,4,5,6,7,8,9]")] [InlineData("`1")] // back tick as first char followed by numbers (short) [InlineData("`111")] // back tick as first char followed by numbers (longer) - [InlineData("MoreThanMaxArrayLength`2147483592")] [InlineData("NegativeGenericArgumentCount`-123")] [InlineData("MoreThanMaxArrayRank[,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,]")] [InlineData("NonGenericTypeUsingGenericSyntax[[type1], [type2]]")] From 90a5582dcc887ddf3ec5083d13255ba8311fc498 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 22 Mar 2024 18:32:26 +0100 Subject: [PATCH 35/48] remove the unused property, use the new names for non-public APIs as well --- .../src/System/Reflection/Metadata/TypeName.cs | 8 ++++---- .../System/Reflection/Metadata/TypeNameParser.cs | 16 ++++++++-------- .../Reflection/Metadata/TypeNameParserOptions.cs | 9 --------- 3 files changed, 12 insertions(+), 21 deletions(-) diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs index cd76de8870f59..a6b71f0c0c5f8 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs @@ -30,16 +30,16 @@ sealed class TypeName : IEquatable internal TypeName(string name, string fullName, AssemblyName? assemblyName, int rankOrModifier = default, - TypeName? underlyingType = default, - TypeName? containingType = default, + TypeName? elementOrGenericType = default, + TypeName? declaringType = default, TypeName[]? genericTypeArguments = default) { Name = name; FullName = fullName; _assemblyName = assemblyName; _rankOrModifier = rankOrModifier; - _elementOrGenericType = underlyingType; - _declaringType = containingType; + _elementOrGenericType = elementOrGenericType; + _declaringType = declaringType; _genericArguments = genericTypeArguments; Debug.Assert(!(IsArray || IsPointer || IsByRef) || _elementOrGenericType is not null); diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs index 0f479503df230..e2f7f41b4fe18 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs @@ -208,11 +208,11 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse #endif } - TypeName? containingType = GetContainingType(fullTypeName, nestedNameLengths, assemblyName); + TypeName? declaringType = GetDeclaringType(fullTypeName, nestedNameLengths, assemblyName); string name = GetName(fullTypeName).ToString(); - TypeName? underlyingType = genericArgs is null ? null : new(name, fullTypeName.ToString(), assemblyName, containingType: containingType); + TypeName? underlyingType = genericArgs is null ? null : new(name, fullTypeName.ToString(), assemblyName, declaringType: declaringType); string genericTypeFullName = GetGenericTypeFullName(fullTypeName, genericArgs); - TypeName result = new(name, genericTypeFullName, assemblyName, rankOrModifier: 0, underlyingType, containingType, genericArgs); + TypeName result = new(name, genericTypeFullName, assemblyName, rankOrModifier: 0, underlyingType, declaringType, genericArgs); if (previousDecorator != default) // some decorators were recognized { @@ -229,7 +229,7 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse nameSb.Append(trimmedModifier); fullNameSb.Append(trimmedModifier); - result = new(nameSb.AsSpan().ToString(), fullNameSb.AsSpan().ToString(), assemblyName, parsedModifier, underlyingType: result); + result = new(nameSb.AsSpan().ToString(), fullNameSb.AsSpan().ToString(), assemblyName, parsedModifier, elementOrGenericType: result); } // The code above is not calling ValueStringBuilder.ToString() directly, @@ -291,25 +291,25 @@ private bool TryParseAssemblyName(ref AssemblyName? assemblyName) return true; } - private static TypeName? GetContainingType(ReadOnlySpan fullTypeName, List? nestedNameLengths, AssemblyName? assemblyName) + private static TypeName? GetDeclaringType(ReadOnlySpan fullTypeName, List? nestedNameLengths, AssemblyName? assemblyName) { if (nestedNameLengths is null) { return null; } - TypeName? containingType = null; + TypeName? declaringType = null; int nameOffset = 0; foreach (int nestedNameLength in nestedNameLengths) { Debug.Assert(nestedNameLength > 0, "TryGetTypeNameInfo should return error on zero lengths"); ReadOnlySpan fullName = fullTypeName.Slice(0, nameOffset + nestedNameLength); ReadOnlySpan name = GetName(fullName); - containingType = new(name.ToString(), fullName.ToString(), assemblyName, containingType: containingType); + declaringType = new(name.ToString(), fullName.ToString(), assemblyName, declaringType: declaringType); nameOffset += nestedNameLength + 1; // include the '+' that was skipped in name } - return containingType; + return declaringType; } private bool TryDive(ref int depth) diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs index 0efcac029bf93..66350c9a44cac 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserOptions.cs @@ -37,14 +37,5 @@ public int MaxNodes _maxNodes = value; } } - - /// - /// Extends ECMA-335 standard limitations with a set of opinionated rules based on most up-to-date security knowledge. - /// - /// - /// When parsing AssemblyName, only Version, Culture and PublicKeyToken attributes are allowed. - /// The comparison is also case-sensitive (in contrary to constructor). - /// - internal bool StrictValidation { get; set; } // it's internal for now, will very soon be changed after we have full requirements and the API gets approved } } From 9a5d01b3d3a8593759f806673a6c54303dd45cb7 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 2 Apr 2024 18:11:29 +0200 Subject: [PATCH 36/48] make the allocations happen when they are actually needed for the first time --- .../System/Reflection/Metadata/TypeName.cs | 56 +++++++++++++++++-- .../Reflection/Metadata/TypeNameParser.cs | 36 +++++------- .../Metadata/TypeNameParserHelpers.cs | 48 ++++++++-------- .../Metadata/TypeNameParserHelpersTests.cs | 8 ++- 4 files changed, 94 insertions(+), 54 deletions(-) diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs index a6b71f0c0c5f8..582bf74afca36 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Text; namespace System.Reflection.Metadata { @@ -25,17 +26,16 @@ sealed class TypeName : IEquatable private readonly AssemblyName? _assemblyName; private readonly TypeName? _elementOrGenericType; private readonly TypeName? _declaringType; - private string? _assemblyQualifiedName; + private string? _name, _fullName, _assemblyQualifiedName; - internal TypeName(string name, string fullName, + internal TypeName(string? fullName, AssemblyName? assemblyName, int rankOrModifier = default, TypeName? elementOrGenericType = default, TypeName? declaringType = default, TypeName[]? genericTypeArguments = default) { - Name = name; - FullName = fullName; + _fullName = fullName; _assemblyName = assemblyName; _rankOrModifier = rankOrModifier; _elementOrGenericType = elementOrGenericType; @@ -89,7 +89,27 @@ public string AssemblyQualifiedName /// 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; } + public string FullName + { + get + { + if (_fullName is null) + { + if (_genericArguments is not null) + { + _fullName = TypeNameParserHelpers.GetGenericTypeFullName(GetGenericTypeDefinition().FullName.AsSpan(), _genericArguments); + } + else if (IsArray || IsPointer || IsByRef) + { + ValueStringBuilder builder = new(stackalloc char[128]); + builder.Append(GetElementType().FullName); + _fullName = TypeNameParserHelpers.GetRankOrModifierStringRepresentation(_rankOrModifier, builder); + } + } + + return _fullName!; + } + } /// /// Returns true if this type represents any kind of array, regardless of the array's @@ -152,7 +172,31 @@ public string AssemblyQualifiedName /// 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; } + 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, builder); + } + else // Nested and Simple types + { + _name = TypeNameParserHelpers.GetName(FullName.AsSpan()).ToString(); + } + } + + return _name; + } + } public bool Equals(TypeName? other) => other is not null diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs index e2f7f41b4fe18..21029003bba32 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs @@ -209,33 +209,24 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse } TypeName? declaringType = GetDeclaringType(fullTypeName, nestedNameLengths, assemblyName); - string name = GetName(fullTypeName).ToString(); - TypeName? underlyingType = genericArgs is null ? null : new(name, fullTypeName.ToString(), assemblyName, declaringType: declaringType); - string genericTypeFullName = GetGenericTypeFullName(fullTypeName, genericArgs); - TypeName result = new(name, genericTypeFullName, assemblyName, rankOrModifier: 0, underlyingType, declaringType, genericArgs); - if (previousDecorator != default) // some decorators were recognized + TypeName? result; + if (genericArgs is null) { - ValueStringBuilder fullNameSb = new(stackalloc char[128]); - fullNameSb.Append(genericTypeFullName); - - ValueStringBuilder nameSb = new(stackalloc char[32]); - nameSb.Append(name); + result = new(fullTypeName.ToString(), assemblyName, rankOrModifier: 0, elementOrGenericType: null, declaringType, genericArgs); + } + else + { + TypeName genericTypeDefinition = new(fullTypeName.ToString(), assemblyName, declaringType: declaringType); + result = new(fullName: null, assemblyName, rankOrModifier: 0, genericTypeDefinition, declaringType, genericArgs); + } + if (previousDecorator != default) // some decorators were recognized + { while (TryParseNextDecorator(ref capturedBeforeProcessing, out int parsedModifier)) { - // we are not reusing the input string, as it could have contain whitespaces that we want to exclude - string trimmedModifier = GetRankOrModifierStringRepresentation(parsedModifier); - nameSb.Append(trimmedModifier); - fullNameSb.Append(trimmedModifier); - - result = new(nameSb.AsSpan().ToString(), fullNameSb.AsSpan().ToString(), assemblyName, parsedModifier, elementOrGenericType: result); + result = new(fullName: null, assemblyName, parsedModifier, elementOrGenericType: result); } - - // The code above is not calling ValueStringBuilder.ToString() directly, - // because it calls Dispose and we want to reuse the builder content until we are done with all decorators. - fullNameSb.Dispose(); - nameSb.Dispose(); } return result; @@ -304,8 +295,7 @@ private bool TryParseAssemblyName(ref AssemblyName? assemblyName) { Debug.Assert(nestedNameLength > 0, "TryGetTypeNameInfo should return error on zero lengths"); ReadOnlySpan fullName = fullTypeName.Slice(0, nameOffset + nestedNameLength); - ReadOnlySpan name = GetName(fullName); - declaringType = new(name.ToString(), fullName.ToString(), assemblyName, declaringType: declaringType); + declaringType = new(fullName.ToString(), assemblyName, declaringType: declaringType); nameOffset += nestedNameLength + 1; // include the '+' that was skipped in name } diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs index 698f38d7fa6ac..fc82e95b472f9 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs @@ -71,13 +71,8 @@ internal static int GetGenericArgumentCount(ReadOnlySpan fullTypeName) return 0; } - internal static string GetGenericTypeFullName(ReadOnlySpan fullTypeName, TypeName[]? genericArgs) + internal static string GetGenericTypeFullName(ReadOnlySpan fullTypeName, TypeName[] genericArgs) { - if (genericArgs is null) - { - return fullTypeName.ToString(); - } - ValueStringBuilder result = new(stackalloc char[128]); result.Append(fullTypeName); @@ -184,30 +179,37 @@ static int GetUnescapedOffset(ReadOnlySpan fullName, int startIndex) } } - internal static string GetRankOrModifierStringRepresentation(int rankOrModifier) + internal static string GetRankOrModifierStringRepresentation(int rankOrModifier, ValueStringBuilder builder) { - return rankOrModifier switch + if (rankOrModifier == ByRef) { - ByRef => "&", - Pointer => "*", - SZArray => "[]", - 1 => "[*]", - _ => ArrayRankToString(rankOrModifier) - }; - - static string ArrayRankToString(int arrayRank) + builder.Append('&'); + } + else if (rankOrModifier == Pointer) + { + builder.Append('*'); + } + else if (rankOrModifier == SZArray) { - Debug.Assert(arrayRank >= 2 && arrayRank <= 32); + builder.Append("[]"); + } + else if (rankOrModifier == 1) + { + builder.Append("[*]"); + } + else + { + Debug.Assert(rankOrModifier >= 2 && rankOrModifier <= 32); - Span buffer = stackalloc char[2 + MaxArrayRank - 1]; - buffer[0] = '['; - for (int i = 1; i < arrayRank; i++) + builder.Append('['); + for (int i = 1; i < rankOrModifier; i++) { - buffer[i] = ','; + builder.Append(','); } - buffer[arrayRank] = ']'; - return buffer.Slice(0, arrayRank + 1).ToString(); + builder.Append(']'); } + + return builder.ToString(); } /// diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs index 05337c2422520..beb47dc066bb2 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using System.Text; using Xunit; namespace System.Reflection.Metadata.Tests @@ -84,8 +85,11 @@ public void GetNameReturnsJustName(string fullName, string expected) [InlineData(2, "[,]")] [InlineData(3, "[,,]")] [InlineData(4, "[,,,]")] - public void GetRankOrModifierStringRepresentationReturnsExpectedString(int input, string expected) - => Assert.Equal(expected, TypeNameParserHelpers.GetRankOrModifierStringRepresentation(input)); + public void AppendRankOrModifierStringRepresentationAppendsExpectedString(int input, string expected) + { + ValueStringBuilder builder = new ValueStringBuilder(initialCapacity: 10); + Assert.Equal(expected, TypeNameParserHelpers.GetRankOrModifierStringRepresentation(input, builder)); + } [Theory] [InlineData(typeof(List))] From cfb221611d25599168a3fbaaa08fdf22c2af9efe Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 3 Apr 2024 11:21:11 +0200 Subject: [PATCH 37/48] don't pre-allocate full names for all declaring types, just store the full name of final type and the length of the substring --- .../System/Reflection/Metadata/TypeName.cs | 32 ++++++++++++++++--- .../Reflection/Metadata/TypeNameParser.cs | 27 ++++++++-------- .../Metadata/TypeNameParserHelpers.cs | 6 ++-- 3 files changed, 43 insertions(+), 22 deletions(-) diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs index 582bf74afca36..cb3b6dada7d75 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs @@ -21,7 +21,13 @@ sealed class TypeName : IEquatable /// Positive value is array rank. /// Negative value is modifier encoded using constants defined in . /// - private readonly int _rankOrModifier; + 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[]? _genericArguments; private readonly AssemblyName? _assemblyName; private readonly TypeName? _elementOrGenericType; @@ -30,10 +36,11 @@ sealed class TypeName : IEquatable internal TypeName(string? fullName, AssemblyName? assemblyName, - int rankOrModifier = default, TypeName? elementOrGenericType = default, TypeName? declaringType = default, - TypeName[]? genericTypeArguments = default) + TypeName[]? genericTypeArguments = default, + sbyte rankOrModifier = default, + int nestedNameLength = -1) { _fullName = fullName; _assemblyName = assemblyName; @@ -41,6 +48,7 @@ internal TypeName(string? fullName, _elementOrGenericType = elementOrGenericType; _declaringType = declaringType; _genericArguments = genericTypeArguments; + _nestedNameLength = nestedNameLength; Debug.Assert(!(IsArray || IsPointer || IsByRef) || _elementOrGenericType is not null); Debug.Assert(_genericArguments is null || _elementOrGenericType is not null); @@ -105,9 +113,19 @@ public string FullName builder.Append(GetElementType().FullName); _fullName = TypeNameParserHelpers.GetRankOrModifierStringRepresentation(_rankOrModifier, 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!; + return _fullName; } } @@ -188,7 +206,11 @@ public string Name builder.Append(GetElementType().Name); _name = TypeNameParserHelpers.GetRankOrModifierStringRepresentation(_rankOrModifier, builder); } - else // Nested and Simple types + else if (_nestedNameLength > 0 && _fullName is not null) + { + _name = TypeNameParserHelpers.GetName(_fullName.AsSpan(0, _nestedNameLength)).ToString(); + } + else { _name = TypeNameParserHelpers.GetName(FullName.AsSpan()).ToString(); } diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs index 21029003bba32..923ad243187e6 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs @@ -208,24 +208,23 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse #endif } - TypeName? declaringType = GetDeclaringType(fullTypeName, nestedNameLengths, assemblyName); - - TypeName? result; - if (genericArgs is null) - { - result = new(fullTypeName.ToString(), assemblyName, rankOrModifier: 0, elementOrGenericType: null, declaringType, genericArgs); - } - else + // 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) { - TypeName genericTypeDefinition = new(fullTypeName.ToString(), assemblyName, declaringType: declaringType); - result = new(fullName: null, assemblyName, rankOrModifier: 0, genericTypeDefinition, declaringType, genericArgs); + 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, parsedModifier, elementOrGenericType: result); + result = new(fullName: null, assemblyName, elementOrGenericType: result, rankOrModifier: (sbyte)parsedModifier); } } @@ -282,7 +281,7 @@ private bool TryParseAssemblyName(ref AssemblyName? assemblyName) return true; } - private static TypeName? GetDeclaringType(ReadOnlySpan fullTypeName, List? nestedNameLengths, AssemblyName? assemblyName) + private static TypeName? GetDeclaringType(string fullTypeName, List? nestedNameLengths, AssemblyName? assemblyName) { if (nestedNameLengths is null) { @@ -294,8 +293,8 @@ private bool TryParseAssemblyName(ref AssemblyName? assemblyName) foreach (int nestedNameLength in nestedNameLengths) { Debug.Assert(nestedNameLength > 0, "TryGetTypeNameInfo should return error on zero lengths"); - ReadOnlySpan fullName = fullTypeName.Slice(0, nameOffset + nestedNameLength); - declaringType = new(fullName.ToString(), assemblyName, declaringType: declaringType); + int fullNameLength = nameOffset + nestedNameLength; + declaringType = new(fullTypeName, assemblyName, declaringType: declaringType, nestedNameLength: fullNameLength); nameOffset += nestedNameLength + 1; // include the '+' that was skipped in name } diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs index fc82e95b472f9..6144bcd2396b0 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs @@ -16,9 +16,9 @@ namespace System.Reflection.Metadata internal static class TypeNameParserHelpers { internal const int MaxArrayRank = 32; - internal const int SZArray = -1; - internal const int Pointer = -2; - internal const int ByRef = -3; + 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 _endOfFullTypeNameDelimitersSearchValues = SearchValues.Create("[]&*,+\\"); From d63365fb7293b497a0bc7f17592c3d6439f1d451 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 3 Apr 2024 15:44:23 +0200 Subject: [PATCH 38/48] build fix --- src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs index cb3b6dada7d75..53d46071523fe 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs @@ -125,7 +125,7 @@ public string FullName _fullName = _fullName.Substring(0, _nestedNameLength); } - return _fullName; + return _fullName!; } } From 04731fba62eda36df59a47fd9ca9e46ef351cf1f Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 9 Apr 2024 15:59:10 +0200 Subject: [PATCH 39/48] address part of the code review feedback --- .../System/Reflection/AssemblyNameParser.cs | 19 +++++++------------ .../Reflection/Metadata/TypeNameParser.cs | 2 +- .../Metadata/TypeNameParserHelpers.cs | 11 +++-------- .../Reflection/TypeNameParser.Helpers.cs | 7 ++++++- .../Metadata/TypeNameParserHelpersTests.cs | 14 ++++++++++---- .../tests/Metadata/TypeNameTests.cs | 1 + 6 files changed, 28 insertions(+), 26 deletions(-) diff --git a/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs b/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs index 8c903bf24fe9c..7274d2ed5364a 100644 --- a/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs @@ -260,15 +260,9 @@ private static bool TryParseVersion(string attributeValue, ref Version? version) return false; } - Span versionNumbers = stackalloc ushort[4]; - for (int i = 0; i < versionNumbers.Length; i++) + Span versionNumbers = stackalloc ushort[4] { ushort.MaxValue, ushort.MaxValue, ushort.MaxValue, ushort.MaxValue }; + for (int i = 0; i < parts.Length; i++) { - if ((uint)i >= (uint)parts.Length) - { - versionNumbers[i] = ushort.MaxValue; - break; - } - if (!ushort.TryParse( #if NET8_0_OR_GREATER attributeValueSpan[parts[i]], @@ -429,7 +423,7 @@ private bool TryGetNextToken(out string tokenString, out Token token) } } - ValueStringBuilder sb = new ValueStringBuilder(stackalloc char[64]); + using ValueStringBuilder sb = new ValueStringBuilder(stackalloc char[64]); char quoteChar = '\0'; if (c == '\'' || c == '\"') @@ -515,13 +509,14 @@ private bool TryGetNextToken(out string tokenString, out Token token) } + int length = sb.Length; if (quoteChar == 0) { - while (sb.Length > 0 && IsWhiteSpace(sb[sb.Length - 1])) - sb.Length--; + while (length > 0 && IsWhiteSpace(sb[length - 1])) + length--; } - tokenString = sb.ToString(); + tokenString = sb.AsSpan(0, length).ToString(); token = Token.String; return true; } diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs index 923ad243187e6..602a78255ffd3 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs @@ -83,7 +83,7 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse // 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 (IsBeginningOfGenericAgs(ref _inputString, out bool doubleBrackets)) + if (IsBeginningOfGenericArgs(ref _inputString, out bool doubleBrackets)) { int startingRecursionCheck = recursiveDepth; int maxObservedRecursionCheck = recursiveDepth; diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs index 6144bcd2396b0..75d80df78adb9 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs @@ -202,10 +202,7 @@ internal static string GetRankOrModifierStringRepresentation(int rankOrModifier, Debug.Assert(rankOrModifier >= 2 && rankOrModifier <= 32); builder.Append('['); - for (int i = 1; i < rankOrModifier; i++) - { - builder.Append(','); - } + builder.Append(',', rankOrModifier - 1); builder.Append(']'); } @@ -215,7 +212,7 @@ internal static string GetRankOrModifierStringRepresentation(int rankOrModifier, /// /// Are there any captured generic args? We'll look for "[[" and "[" that is not followed by "]", "*" and ",". /// - internal static bool IsBeginningOfGenericAgs(ref ReadOnlySpan span, out bool doubleBrackets) + internal static bool IsBeginningOfGenericArgs(ref ReadOnlySpan span, out bool doubleBrackets) { doubleBrackets = false; @@ -256,8 +253,6 @@ internal static bool TryGetTypeNameInfo(ref ReadOnlySpan input, ref List input, ref List 0 && ((long)genericArgCount + generics > int.MaxValue))) { return false; // invalid type name detected! } diff --git a/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs index e46fa95e19046..e99e7150ff415 100644 --- a/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs +++ b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs @@ -1,6 +1,7 @@ // 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.Text; @@ -184,10 +185,14 @@ private static (string typeNamespace, string name) SplitFullTypeName(string type { return type.MakeArrayType(); } - else + else if (typeName.IsVariableBoundArrayType) { return type.MakeArrayType(rank: typeName.GetArrayRank()); } + else + { + throw new UnreachableException(); + } } } } diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs index beb47dc066bb2..998b1c5adc477 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs @@ -123,7 +123,7 @@ public void IsBeginningOfGenericAgsHandlesAllCasesProperly(string input, bool ex { ReadOnlySpan inputSpan = input.AsSpan(); - Assert.Equal(expectedResult, TypeNameParserHelpers.IsBeginningOfGenericAgs(ref inputSpan, out bool doubleBrackets)); + Assert.Equal(expectedResult, TypeNameParserHelpers.IsBeginningOfGenericArgs(ref inputSpan, out bool doubleBrackets)); Assert.Equal(expectedDoubleBrackets, doubleBrackets); Assert.Equal(expectedConsumedInput, inputSpan.ToString()); } @@ -136,6 +136,8 @@ public void IsBeginningOfGenericAgsHandlesAllCasesProperly(string input, bool ex [InlineData("A.B++C", false, null, 0, 0)] // invalid type name: two following, unescaped + [InlineData("A.B`1", true, null, 5, 1)] [InlineData("A+B`1+C1`2+DD2`3+E", true, new int[] { 1, 3, 4, 5 }, 18, 6)] + [InlineData("Integer`2147483646+NoOverflow`1", true, new int[] { 18 }, 31, 2147483647)] + [InlineData("Integer`2147483647+Overflow`1", false, null, 0, 0)] // integer overflow for generic args count public void TryGetTypeNameInfoGetsAllTheInfo(string input, bool expectedResult, int[] expectedNestedNameLengths, int expectedTotalLength, int expectedGenericArgCount) { @@ -144,9 +146,13 @@ public void TryGetTypeNameInfoGetsAllTheInfo(string input, bool expectedResult, bool result = TypeNameParserHelpers.TryGetTypeNameInfo(ref span, ref nestedNameLengths, out int totalLength, out int genericArgCount); Assert.Equal(expectedResult, result); - Assert.Equal(expectedNestedNameLengths, nestedNameLengths?.ToArray()); - Assert.Equal(expectedTotalLength, totalLength); - Assert.Equal(expectedGenericArgCount, genericArgCount); + + if (expectedResult) + { + Assert.Equal(expectedNestedNameLengths, nestedNameLengths?.ToArray()); + Assert.Equal(expectedTotalLength, totalLength); + Assert.Equal(expectedGenericArgCount, genericArgCount); + } } [Theory] diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs index 4d72468d80fd6..bbd8344367e01 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs @@ -81,6 +81,7 @@ public void EmptyStringsAreNotAllowed(string input) [InlineData("EscapeNonSpecialChar\\a")] [InlineData("EscapeNonSpecialChar\\0")] [InlineData("DoubleNestingChar++Bla")] + [InlineData("Integer`2147483647+Overflow`1")] public void InvalidTypeNamesAreNotAllowed(string input) { Assert.Throws(() => TypeName.Parse(input.AsSpan())); From 113a87fc6dfc0dbcd280a24513dc2d5728a369b9 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Thu, 11 Apr 2024 18:01:32 +0200 Subject: [PATCH 40/48] fix the build --- .../src/System/Reflection/TypeNameParser.Helpers.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs index e99e7150ff415..be5a784a40f75 100644 --- a/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs +++ b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs @@ -185,13 +185,11 @@ private static (string typeNamespace, string name) SplitFullTypeName(string type { return type.MakeArrayType(); } - else if (typeName.IsVariableBoundArrayType) - { - return type.MakeArrayType(rank: typeName.GetArrayRank()); - } else { - throw new UnreachableException(); + Debug.Assert(typeName.IsVariableBoundArrayType); + + return type.MakeArrayType(rank: typeName.GetArrayRank()); } } } From 70051642bc937b9dffd1ef0f91148f30d7d4169d Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Mon, 15 Apr 2024 16:16:54 +0200 Subject: [PATCH 41/48] AssemblyNameInfo --- .../Reflection/TypeNameParser.CoreCLR.cs | 2 +- .../Reflection/TypeNameParser.NativeAot.cs | 2 +- .../ILVerification/ILVerification.projitems | 9 + .../ILCompiler.TypeSystem.csproj | 9 + .../Reflection/AssemblyNameFormatter.cs | 12 +- .../Reflection/Metadata/AssemblyNameInfo.cs | 285 ++++++++++++++++++ .../System/Reflection/Metadata/TypeName.cs | 40 +-- .../Reflection/Metadata/TypeNameParser.cs | 29 +- .../Reflection/TypeNameParser.Helpers.cs | 4 +- ...alueStringBuilder.AppendSpanFormattable.cs | 2 + .../System.Private.CoreLib.Shared.projitems | 7 +- .../ref/System.Reflection.Metadata.cs | 17 ++ .../src/System.Reflection.Metadata.csproj | 3 + .../tests/Metadata/AssemblyNameInfoTests.cs | 50 +++ .../tests/Metadata/TypeNameParserSamples.cs | 2 +- .../tests/Metadata/TypeNameTests.cs | 14 +- .../System.Reflection.Metadata.Tests.csproj | 1 + 17 files changed, 416 insertions(+), 72 deletions(-) rename src/libraries/{System.Private.CoreLib => Common}/src/System/Reflection/AssemblyNameFormatter.cs (92%) create mode 100644 src/libraries/Common/src/System/Reflection/Metadata/AssemblyNameInfo.cs create mode 100644 src/libraries/System.Reflection.Metadata/tests/Metadata/AssemblyNameInfoTests.cs 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 55544a6f8ecc8..1a49aecab6d7f 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 @@ -85,7 +85,7 @@ internal partial struct TypeNameParser { return null; } - else if (topLevelAssembly is not null && parsed.GetAssemblyName() is not null) + else if (topLevelAssembly is not null && parsed.AssemblyName is not null) { return throwOnError ? throw new ArgumentException(SR.Argument_AssemblyGetTypeCannotSpecifyAssembly) : null; } 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 fd497ff511620..d9329ebc84f7f 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 @@ -80,7 +80,7 @@ internal partial struct TypeNameParser { return null; } - else if (topLevelAssembly is not null && parsed.GetAssemblyName() is not null) + else if (topLevelAssembly is not null && parsed.AssemblyName is not null) { return throwOnError ? throw new ArgumentException(SR.Argument_AssemblyGetTypeCannotSpecifyAssembly) : null; } diff --git a/src/coreclr/tools/ILVerification/ILVerification.projitems b/src/coreclr/tools/ILVerification/ILVerification.projitems index 7c205fbbf7188..75b7e614adcf5 100644 --- a/src/coreclr/tools/ILVerification/ILVerification.projitems +++ b/src/coreclr/tools/ILVerification/ILVerification.projitems @@ -69,9 +69,15 @@ Utilities\HexConverter.cs + + Utilities\AssemblyNameFormatter.cs + Utilities\AssemblyNameParser.cs + + Utilities\Metadata\AssemblyNameInfo.cs + Utilities\TypeName.cs @@ -93,6 +99,9 @@ Utilities\ValueStringBuilder.cs + + Utilities\ValueStringBuilder.AppendSpanFormattable.cs + Utilities\LockFreeReaderHashtable.cs diff --git a/src/coreclr/tools/aot/ILCompiler.TypeSystem/ILCompiler.TypeSystem.csproj b/src/coreclr/tools/aot/ILCompiler.TypeSystem/ILCompiler.TypeSystem.csproj index 6091f03b4acbc..23afa3780b1ab 100644 --- a/src/coreclr/tools/aot/ILCompiler.TypeSystem/ILCompiler.TypeSystem.csproj +++ b/src/coreclr/tools/aot/ILCompiler.TypeSystem/ILCompiler.TypeSystem.csproj @@ -201,9 +201,15 @@ Utilities\HexConverter.cs + + Utilities\AssemblyNameFormatter.cs + Utilities\AssemblyNameParser.cs + + Utilities\AssemblyNameInfo.cs + Utilities\TypeName.cs @@ -222,6 +228,9 @@ 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 c1a810db50755..9d5eeebc85a6d 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/Metadata/AssemblyNameInfo.cs b/src/libraries/Common/src/System/Reflection/Metadata/AssemblyNameInfo.cs new file mode 100644 index 0000000000000..d4b21bd7f906f --- /dev/null +++ b/src/libraries/Common/src/System/Reflection/Metadata/AssemblyNameInfo.cs @@ -0,0 +1,285 @@ +// 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; + +namespace System.Reflection.Metadata +{ + [DebuggerDisplay("{FullName}")] +#if SYSTEM_PRIVATE_CORELIB + internal +#else + public +#endif + sealed class AssemblyNameInfo : IEquatable + { + private string? _fullName; + +#if !SYSTEM_PRIVATE_CORELIB + public AssemblyNameInfo(string name, Version? version = null, string? cultureName = null, AssemblyNameFlags flags = AssemblyNameFlags.None, Collections.Immutable.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 + ? Collections.Immutable.ImmutableArray.Empty + #if NET8_0_OR_GREATER + : Runtime.InteropServices.ImmutableCollectionsMarshal.AsImmutableArray(parts._publicKeyOrToken); + #else + : Collections.Immutable.ImmutableArray.Create(parts._publicKeyOrToken); + #endif +#endif + } + + public string Name { get; } + public Version? Version { get; } + public string? CultureName { get; } + public AssemblyNameFlags Flags { get; } + +#if SYSTEM_PRIVATE_CORELIB + public byte[]? PublicKeyOrToken { get; } +#else + public Collections.Immutable.ImmutableArray PublicKeyOrToken { get; } +#endif + + 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 + ToArray(PublicKeyOrToken); +#endif + _fullName = AssemblyNameFormatter.ComputeDisplayName(Name, Version, CultureName, publicKeyToken); + } + + return _fullName; + } + } + + public bool Equals(AssemblyNameInfo? other) + { + if (other is null || Flags != other.Flags || !Name.Equals(other.Name) || !string.Equals(CultureName, other.CultureName)) + { + return false; + } + + if (Version is null) + { + if (other.Version is not null) + { + return false; + } + } + else + { + if (!Version.Equals(other.Version)) + { + return false; + } + } + + return SequenceEqual(PublicKeyOrToken, other.PublicKeyOrToken); + +#if SYSTEM_PRIVATE_CORELIB + static bool SequenceEqual(byte[]? left, byte[]? right) + { + if (left is null) + { + if (right is not null) + { + return false; + } + } + else if (right is null) + { + return false; + } + else if (left.Length != right.Length) + { + return false; + } + else + { + for (int i = 0; i < left.Length; i++) + { + if (left[i] != right[i]) + { + return false; + } + } + } + + return true; + } +#else + static bool SequenceEqual(Collections.Immutable.ImmutableArray left, Collections.Immutable.ImmutableArray right) + { + int leftLength = left.IsDefaultOrEmpty ? 0 : left.Length; + int rightLength = right.IsDefaultOrEmpty ? 0 : right.Length; + + if (leftLength != rightLength) + { + return false; + } + else if (leftLength > 0) + { + for (int i = 0; i < leftLength; i++) + { + if (left[i] != right[i]) + { + return false; + } + } + } + + return true; + } +#endif + } + + public override bool Equals(object? obj) => Equals(obj as AssemblyNameInfo); + + public override int GetHashCode() + { +#if NETCOREAPP + HashCode hashCode = default; + hashCode.Add(Name); + hashCode.Add(Flags); + hashCode.Add(Version); + + #if SYSTEM_PRIVATE_CORELIB + if (PublicKeyOrToken is not null) + { + hashCode.AddBytes(PublicKeyOrToken); + } + #else + if (!PublicKeyOrToken.IsDefaultOrEmpty) + { + hashCode.AddBytes(Runtime.InteropServices.ImmutableCollectionsMarshal.AsArray(PublicKeyOrToken)); + } + #endif + return hashCode.ToHashCode(); +#else + return FullName.GetHashCode(); +#endif + } + + public AssemblyName ToAssemblyName() + { + AssemblyName assemblyName = new(); + assemblyName.Name = Name; + assemblyName.CultureName = CultureName; + assemblyName.Version = Version; + +#if SYSTEM_PRIVATE_CORELIB + assemblyName._flags = Flags; + + if (PublicKeyOrToken is not null) + { + if ((Flags & AssemblyNameFlags.PublicKey) != 0) + { + assemblyName.SetPublicKey(PublicKeyOrToken); + } + else + { + assemblyName.SetPublicKeyToken(PublicKeyOrToken); + } + } +#else + assemblyName.Flags = Flags; + + if (!PublicKeyOrToken.IsDefault) + { + if ((Flags & AssemblyNameFlags.PublicKey) != 0) + { + assemblyName.SetPublicKey(ToArray(PublicKeyOrToken)); + } + else + { + assemblyName.SetPublicKeyToken(ToArray(PublicKeyOrToken)); + } + } +#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! + : throw new ArgumentException("TODO_adsitnik_add_or_reuse_resource"); + + /// + /// 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, +#if SYSTEM_REFLECTION_METADATA || SYSTEM_PRIVATE_CORELIB // required by some tools that include this file but don't include the attribute + [NotNullWhen(true)] +#endif + out AssemblyNameInfo? result) + { + AssemblyNameParser.AssemblyNameParts parts = default; + if (AssemblyNameParser.TryParse(assemblyName, ref parts)) + { + result = new(parts); + return true; + } + + result = null; + return false; + } + +#if !SYSTEM_PRIVATE_CORELIB + private static byte[]? ToArray(Collections.Immutable.ImmutableArray input) + { + // not using System.Linq.ImmutableArrayExtensions.ToArray as TypeSystem does not allow System.Linq + if (input.IsDefault) + { + return null; + } + else if (input.IsEmpty) + { + return Array.Empty(); + } + + byte[] result = new byte[input.Length]; + input.CopyTo(result, 0); + return result; + } +#endif + } +} diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs index 53d46071523fe..3be55a528b37e 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs @@ -29,13 +29,12 @@ sealed class TypeName : IEquatable /// private readonly int _nestedNameLength; private readonly TypeName[]? _genericArguments; - private readonly AssemblyName? _assemblyName; private readonly TypeName? _elementOrGenericType; private readonly TypeName? _declaringType; private string? _name, _fullName, _assemblyQualifiedName; internal TypeName(string? fullName, - AssemblyName? assemblyName, + AssemblyNameInfo? assemblyName, TypeName? elementOrGenericType = default, TypeName? declaringType = default, TypeName[]? genericTypeArguments = default, @@ -43,7 +42,7 @@ internal TypeName(string? fullName, int nestedNameLength = -1) { _fullName = fullName; - _assemblyName = assemblyName; + AssemblyName = assemblyName; _rankOrModifier = rankOrModifier; _elementOrGenericType = elementOrGenericType; _declaringType = declaringType; @@ -58,18 +57,16 @@ internal TypeName(string? fullName, /// 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 . + /// If returns null, simply returns . /// public string AssemblyQualifiedName - => _assemblyQualifiedName ??= _assemblyName is null ? FullName : $"{FullName}, {_assemblyName.FullName}"; + => _assemblyQualifiedName ??= AssemblyName is null ? FullName : $"{FullName}, {AssemblyName.FullName}"; /// - /// Returns the name of the assembly (not the full name). + /// Returns assembly name which contains this type, or null if this was not + /// created from a fully-qualified name. /// - /// - /// If returns null, simply returns null. - /// - public string? AssemblySimpleName => _assemblyName?.Name; + public AssemblyNameInfo? AssemblyName { get; } /// /// If this type is a nested type (see ), gets @@ -224,8 +221,8 @@ public bool Equals(TypeName? other) => other is not null && other._rankOrModifier == _rankOrModifier // try to prevent from allocations if possible (AssemblyQualifiedName can allocate) - && ((other._assemblyName is null && _assemblyName is null) - || (other._assemblyName is not null && _assemblyName is not null)) + && ((other.AssemblyName is null && AssemblyName is null) + || (other.AssemblyName is not null && AssemblyName is not null)) && other.AssemblyQualifiedName == AssemblyQualifiedName; public override bool Equals(object? obj) => Equals(obj as TypeName); @@ -353,25 +350,6 @@ public int GetArrayRank() _ => throw TypeNameParserHelpers.InvalidOperation_HasToBeArrayClass() }; - /// - /// Returns assembly name which contains this type, or null if this was not - /// created from a fully-qualified name. - /// - /// Since is mutable, this method returns a copy of it. - public AssemblyName? GetAssemblyName() - { - if (_assemblyName is null) - { - return null; - } - -#if SYSTEM_PRIVATE_CORELIB - return _assemblyName; // no need for a copy in CoreLib (it's internal) -#else - return (AssemblyName)_assemblyName.Clone(); -#endif - } - /// /// If this represents a constructed generic type, returns an array /// of all the generic arguments. Otherwise it returns an empty array. diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs index 602a78255ffd3..84a6147e154ce 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs @@ -193,7 +193,7 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse previousDecorator = parsedDecorator; } - AssemblyName? assemblyName = null; + AssemblyNameInfo? assemblyName = null; if (allowFullyQualifiedName && !TryParseAssemblyName(ref assemblyName)) { #if SYSTEM_PRIVATE_CORELIB @@ -232,7 +232,7 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse } /// false means the input was invalid and parsing has failed. Empty input is valid and returns true. - private bool TryParseAssemblyName(ref AssemblyName? assemblyName) + private bool TryParseAssemblyName(ref AssemblyNameInfo? assemblyName) { ReadOnlySpan capturedBeforeProcessing = _inputString; if (TryStripFirstCharAndTrailingSpaces(ref _inputString, ',')) @@ -247,33 +247,12 @@ private bool TryParseAssemblyName(ref AssemblyName? assemblyName) // Otherwise EOL serves as the terminator. int assemblyNameLength = (int)Math.Min((uint)_inputString.IndexOf(']'), (uint)_inputString.Length); ReadOnlySpan candidate = _inputString.Slice(0, assemblyNameLength); - AssemblyNameParser.AssemblyNameParts parts = default; - if (!AssemblyNameParser.TryParse(candidate, ref parts)) + if (!AssemblyNameInfo.TryParse(candidate, out assemblyName)) { return false; } - assemblyName = new AssemblyName(); -#if SYSTEM_PRIVATE_CORELIB - assemblyName.Init(parts); -#else - assemblyName.Name = parts._name; - assemblyName.CultureName = parts._cultureName; - assemblyName.Version = parts._version; - - if (parts._publicKeyOrToken is not null) - { - if ((parts._flags & AssemblyNameFlags.PublicKey) != 0) - { - assemblyName.SetPublicKey(parts._publicKeyOrToken); - } - else - { - assemblyName.SetPublicKeyToken(parts._publicKeyOrToken); - } - } -#endif _inputString = _inputString.Slice(assemblyNameLength); return true; } @@ -281,7 +260,7 @@ private bool TryParseAssemblyName(ref AssemblyName? assemblyName) return true; } - private static TypeName? GetDeclaringType(string fullTypeName, List? nestedNameLengths, AssemblyName? assemblyName) + private static TypeName? GetDeclaringType(string fullTypeName, List? nestedNameLengths, AssemblyNameInfo? assemblyName) { if (nestedNameLengths is null) { diff --git a/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs index be5a784a40f75..6fae810b6bd0e 100644 --- a/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs +++ b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs @@ -128,7 +128,7 @@ private static (string typeNamespace, string name) SplitFullTypeName(string type } string nonNestedParentName = current!.FullName; - Type? type = GetType(nonNestedParentName, nestedTypeNames, typeName.GetAssemblyName(), typeName.FullName); + Type? type = GetType(nonNestedParentName, nestedTypeNames, typeName.AssemblyName?.ToAssemblyName(), typeName.FullName); return Make(type, typeName); } else if (typeName.IsConstructedGenericType) @@ -141,7 +141,7 @@ private static (string typeNamespace, string name) SplitFullTypeName(string type } else { - Type? type = GetType(typeName.FullName, nestedTypeNames: ReadOnlySpan.Empty, typeName.GetAssemblyName(), typeName.FullName); + Type? type = GetType(typeName.FullName, nestedTypeNames: ReadOnlySpan.Empty, typeName.AssemblyName?.ToAssemblyName(), typeName.FullName); return Make(type, typeName); } diff --git a/src/libraries/Common/src/System/Text/ValueStringBuilder.AppendSpanFormattable.cs b/src/libraries/Common/src/System/Text/ValueStringBuilder.AppendSpanFormattable.cs index 04e9fcbdb5524..3e9cf2c56d3ea 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 c534b2f299207..5225546861cfe 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,7 +649,6 @@ - @@ -1477,6 +1476,12 @@ Common\System\Reflection\AssemblyNameParser.cs + + Common\System\Reflection\AssemblyNameFormatter.cs + + + Common\System\Reflection\Metadata\AssemblyNameInfo.cs + Common\System\Reflection\Metadata\TypeName.cs 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 2cc2644e2c8ae..54299224cd861 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,23 @@ public readonly partial struct TypeLayout public int PackingSize { get { throw null; } } public int Size { get { throw null; } } } + public sealed partial class AssemblyNameInfo : System.IEquatable + { + 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 override bool Equals(object? obj) { throw null; } + public bool Equals(System.Reflection.Metadata.AssemblyNameInfo? other) { throw null; } + public override int GetHashCode() { throw null; } + public System.Reflection.AssemblyName ToAssemblyName() { throw null; } + } public sealed partial class TypeName : System.IEquatable { internal TypeName() { } 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 7c683a07db249..266d4c6f55fdd 100644 --- a/src/libraries/System.Reflection.Metadata/src/System.Reflection.Metadata.csproj +++ b/src/libraries/System.Reflection.Metadata/src/System.Reflection.Metadata.csproj @@ -253,12 +253,15 @@ 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 0000000000000..5642173544841 --- /dev/null +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/AssemblyNameInfoTests.cs @@ -0,0 +1,50 @@ +// 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", "MyAssemblyName, Version=1.0.0.0, PublicKeyToken=b77a5c561934e089")] + public void WithPublicTokenKey(string name, string expectedName) + { + AssemblyName assemblyName = new AssemblyName(name); + + AssemblyNameInfo assemblyNameInfo = AssemblyNameInfo.Parse(name.AsSpan()); + + Assert.Equal(expectedName, assemblyName.FullName); + Assert.Equal(expectedName, 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); + } + + 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); + } + } +} diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs index 3176d6e8196f0..0c48cdc2641d9 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserSamples.cs @@ -70,7 +70,7 @@ public SampleSerializationBinder(Type[]? allowedTypes = null) throw new InvalidOperationException($"Invalid type name: '{typeName}'"); } - if (parsed.GetAssemblyName() is not null) + if (parsed.AssemblyName is not null) { // The attackers may create such a payload, // where "typeName" passed to BindToType contains the assembly name diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs index bbd8344367e01..34201b15ee5be 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs @@ -113,19 +113,15 @@ static void Verify(Type type, AssemblyName expectedAssemblyName, TypeName parsed Assert.Equal(type.FullName, parsed.FullName); Assert.Equal(type.Name, parsed.Name); - AssemblyName parsedAssemblyName = parsed.GetAssemblyName(); + AssemblyNameInfo parsedAssemblyName = parsed.AssemblyName; Assert.NotNull(parsedAssemblyName); Assert.Equal(expectedAssemblyName.Name, parsedAssemblyName.Name); - Assert.Equal(expectedAssemblyName.Name, parsed.AssemblySimpleName); Assert.Equal(expectedAssemblyName.Version, parsedAssemblyName.Version); Assert.Equal(expectedAssemblyName.CultureName, parsedAssemblyName.CultureName); - Assert.Equal(expectedAssemblyName.GetPublicKeyToken(), parsedAssemblyName.GetPublicKeyToken()); Assert.Equal(expectedAssemblyName.FullName, parsedAssemblyName.FullName); - Assert.Equal(default, parsedAssemblyName.ContentType); Assert.Equal(default, parsedAssemblyName.Flags); - Assert.Equal(default, parsedAssemblyName.ProcessorArchitecture); } } @@ -375,12 +371,12 @@ public void GenericArgumentsAreSupported(string input, string name, string fullN { if (assemblyNames[i] is null) { - Assert.Null(genericArg.GetAssemblyName()); + Assert.Null(genericArg.AssemblyName); } else { - Assert.Equal(assemblyNames[i].FullName, genericArg.GetAssemblyName().FullName); - Assert.Equal(assemblyNames[i].Name, genericArg.AssemblySimpleName); + Assert.Equal(assemblyNames[i].FullName, genericArg.AssemblyName.FullName); + Assert.Equal(assemblyNames[i].Name, genericArg.AssemblyName.Name); } } } @@ -691,7 +687,7 @@ static void Verify(Type type, TypeName typeName, bool ignoreCase) { Assert.True(typeName.IsSimple); - AssemblyName? assemblyName = typeName.GetAssemblyName(); + AssemblyName? assemblyName = typeName.AssemblyName.ToAssemblyName(); Type? type = assemblyName is null ? Type.GetType(typeName.FullName, throwOnError, ignoreCase) : Assembly.Load(assemblyName).GetType(typeName.FullName, throwOnError, ignoreCase); 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 134c82d238b6a..f739ac0789ff8 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 @@ -27,6 +27,7 @@ Link="Common\System\IO\TempFile.cs" /> + From cd8a9c29a1e589ab417ebc8dd055fa48fd184c29 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 16 Apr 2024 15:52:34 +0200 Subject: [PATCH 42/48] don't enforce the back tick convention --- .../System/Reflection/Metadata/TypeName.cs | 55 +++++++++------ .../Reflection/Metadata/TypeNameParser.cs | 67 ++++++------------- .../Metadata/TypeNameParserHelpers.cs | 61 +---------------- .../Reflection/TypeNameParser.Helpers.cs | 9 ++- .../ref/System.Reflection.Metadata.cs | 3 +- .../Metadata/TypeNameParserHelpersTests.cs | 53 +++------------ .../tests/Metadata/TypeNameTests.cs | 35 +++++----- 7 files changed, 96 insertions(+), 187 deletions(-) diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs index 3be55a528b37e..3d99ba71f95d3 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs @@ -3,10 +3,15 @@ #nullable enable +using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text; +#if !SYSTEM_PRIVATE_CORELIB +using System.Collections.Immutable; +#endif + namespace System.Reflection.Metadata { [DebuggerDisplay("{AssemblyQualifiedName}")] @@ -28,16 +33,24 @@ sealed class TypeName : IEquatable /// So when the name is needed, a substring is being performed. /// private readonly int _nestedNameLength; - private readonly TypeName[]? _genericArguments; private readonly TypeName? _elementOrGenericType; private readonly TypeName? _declaringType; +#if SYSTEM_PRIVATE_CORELIB + private readonly IReadOnlyList _genericArguments; +#else + private readonly ImmutableArray _genericArguments; +#endif private string? _name, _fullName, _assemblyQualifiedName; internal TypeName(string? fullName, AssemblyNameInfo? assemblyName, TypeName? elementOrGenericType = default, TypeName? declaringType = default, - TypeName[]? genericTypeArguments = default, +#if SYSTEM_PRIVATE_CORELIB + List? genericTypeArguments = default, +#else + ImmutableArray.Builder? genericTypeArguments = default, +#endif sbyte rankOrModifier = default, int nestedNameLength = -1) { @@ -46,11 +59,15 @@ internal TypeName(string? fullName, _rankOrModifier = rankOrModifier; _elementOrGenericType = elementOrGenericType; _declaringType = declaringType; - _genericArguments = genericTypeArguments; _nestedNameLength = nestedNameLength; - Debug.Assert(!(IsArray || IsPointer || IsByRef) || _elementOrGenericType is not null); - Debug.Assert(_genericArguments is null || _elementOrGenericType is not null); +#if SYSTEM_PRIVATE_CORELIB + _genericArguments = genericTypeArguments is not null ? genericTypeArguments : Array.Empty(); +#else + _genericArguments = genericTypeArguments is null + ? ImmutableArray.Empty + : genericTypeArguments.Count == genericTypeArguments.Capacity ? genericTypeArguments.MoveToImmutable() : genericTypeArguments.ToImmutableArray(); +#endif } /// @@ -100,7 +117,7 @@ public string FullName { if (_fullName is null) { - if (_genericArguments is not null) + if (IsConstructedGenericType) { _fullName = TypeNameParserHelpers.GetGenericTypeFullName(GetGenericTypeDefinition().FullName.AsSpan(), _genericArguments); } @@ -138,7 +155,12 @@ public string FullName /// /// Returns false for open generic types (e.g., "Dictionary<,>"). /// - public bool IsConstructedGenericType => _genericArguments is not null; + public bool IsConstructedGenericType => +#if SYSTEM_PRIVATE_CORELIB + _genericArguments.Count > 0; +#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 @@ -274,12 +296,9 @@ public int GetNodeCount() result = checked(result + GetElementType().GetNodeCount()); } - if (_genericArguments is not null) + foreach (TypeName genericArgument in _genericArguments) { - foreach (TypeName genericArgument in _genericArguments) - { - result = checked(result + genericArgument.GetNodeCount()); - } + result = checked(result + genericArgument.GetNodeCount()); } return result; @@ -358,16 +377,12 @@ public int GetArrayRank() /// For example, given "Dictionary<string, int>", returns a 2-element array containing /// string and int. /// + public #if SYSTEM_PRIVATE_CORELIB - public TypeName[] GetGenericArguments() => _genericArguments ?? Array.Empty(); + IReadOnlyList #else - public Collections.Immutable.ImmutableArray GetGenericArguments() - => _genericArguments is null ? Collections.Immutable.ImmutableArray.Empty : - #if NET8_0_OR_GREATER - Runtime.InteropServices.ImmutableCollectionsMarshal.AsImmutableArray(_genericArguments); - #else - Collections.Immutable.ImmutableArray.Create(_genericArguments); - #endif + ImmutableArray #endif + GetGenericArguments() => _genericArguments; } } diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs index 84a6147e154ce..d989199eddbbe 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs @@ -5,6 +5,10 @@ using System.Diagnostics; using System.Text; +#if !SYSTEM_PRIVATE_CORELIB +using System.Collections.Immutable; +#endif + using static System.Reflection.Metadata.TypeNameParserHelpers; #nullable enable @@ -66,7 +70,7 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse } List? nestedNameLengths = null; - if (!TryGetTypeNameInfo(ref _inputString, ref nestedNameLengths, out int fullTypeNameLength, out int genericArgCount)) + if (!TryGetTypeNameInfo(ref _inputString, ref nestedNameLengths, out int fullTypeNameLength)) { return null; } @@ -74,9 +78,12 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse ReadOnlySpan fullTypeName = _inputString.Slice(0, fullTypeNameLength); _inputString = _inputString.Slice(fullTypeNameLength); - int genericArgIndex = 0; - // Don't allocate now, as it may be an open generic type like "List`1" - TypeName[]? genericArgs = null; + // 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 @@ -85,36 +92,18 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse ReadOnlySpan capturedBeforeProcessing = _inputString; if (IsBeginningOfGenericArgs(ref _inputString, out bool doubleBrackets)) { - int startingRecursionCheck = recursiveDepth; - int maxObservedRecursionCheck = recursiveDepth; - ParseAnotherGenericArg: - // Invalid generic argument count provided after backtick. - // Examples: - // - too many: List`1[[a], [b]] - // - not expected: NoBacktick[[a]] - if (genericArgIndex >= genericArgCount) - { - return null; - } - - recursiveDepth = startingRecursionCheck; - // Namespace.Type`1[[GenericArgument1, AssemblyName1],[GenericArgument2, AssemblyName2]] - double square bracket syntax allows for fully qualified type names - // Namespace.Type`1[GenericArgument1,GenericArgument2] - single square bracket syntax is legal only for non-fully qualified type names - // Namespace.Type`1[[GenericArgument1, AssemblyName1], GenericArgument2] - mixed mode - // Namespace.Type`1[GenericArgument1, [GenericArgument2, AssemblyName2]] - mixed mode + // 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; } - if (recursiveDepth > maxObservedRecursionCheck) - { - maxObservedRecursionCheck = recursiveDepth; - } - // For [[, there had better be a ']' after the type name. if (doubleBrackets && !TryStripFirstCharAndTrailingSpaces(ref _inputString, ']')) { @@ -123,17 +112,13 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse if (genericArgs is null) { - // Parsing the rest would hit the limit. - // -1 because the first generic arg has been already parsed. - if (maxObservedRecursionCheck + genericArgCount - 1 > _parseOptions.MaxNodes) - { - recursiveDepth = _parseOptions.MaxNodes; - return null; - } - - genericArgs = new TypeName[genericArgCount]; +#if SYSTEM_PRIVATE_CORELIB + genericArgs = new List(2); +#else + genericArgs = ImmutableArray.CreateBuilder(2); +#endif } - genericArgs[genericArgIndex++] = genericArg; + genericArgs.Add(genericArg); // Is there a ',[' indicating fully qualified generic type arg? // Is there a ',' indicating non-fully qualified generic type arg? @@ -150,16 +135,6 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse { return null; } - - // We have reached the end of generic arguments, but parsed fewer than expected. - // Example: A`2[[b]] - if (genericArgIndex != genericArgCount) - { - return null; - } - - // And now that we're at the end, restore the max observed recursion count. - recursiveDepth = maxObservedRecursionCheck; } // If there was an error stripping the generic args, back up to diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs index 75d80df78adb9..c3f94bfc40210 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs @@ -24,54 +24,7 @@ internal static class TypeNameParserHelpers private static readonly SearchValues _endOfFullTypeNameDelimitersSearchValues = SearchValues.Create("[]&*,+\\"); #endif - /// - /// Negative value for invalid type names. - /// Zero for valid non-generic type names. - /// Positive value for valid generic type names. - /// - internal static int GetGenericArgumentCount(ReadOnlySpan fullTypeName) - { - const int ShortestInvalidTypeName = 2; // Back tick and one digit. Example: "`1" - if (fullTypeName.Length < ShortestInvalidTypeName || !IsAsciiDigit(fullTypeName[fullTypeName.Length - 1])) - { - return 0; - } - - int backtickIndex = fullTypeName.Length - 2; // we already know it's true for the last one - for (; backtickIndex >= 0; backtickIndex--) - { - if (fullTypeName[backtickIndex] == '`') - { - if (backtickIndex == 0) - { - return -1; // illegal name, example "`1" - } - else if (fullTypeName[backtickIndex - 1] == EscapeCharacter) - { - return 0; // legal name, but not a generic type definition. Example: "Escaped\\`1" - } - else if (TryParse(fullTypeName.Slice(backtickIndex + 1), out int value)) - { - // From C# 2.0 language spec: 8.16.3 Multiple type parameters Generic type declarations can have any number of type parameters. - // There is no special treatment for values larger than Array.MaxLength, - // as the parser should simply prevent from parsing that many nodes. - // The value can still be negative, but it's fine as the caller should treat that as an error. - return value; - } - - // most likely the value was too large to be parsed as an int - return -1; - } - else if (!IsAsciiDigit(fullTypeName[backtickIndex]) && fullTypeName[backtickIndex] != '-') - { - break; - } - } - - return 0; - } - - internal static string GetGenericTypeFullName(ReadOnlySpan fullTypeName, TypeName[] genericArgs) + internal static string GetGenericTypeFullName(ReadOnlySpan fullTypeName, IReadOnlyList genericArgs) { ValueStringBuilder result = new(stackalloc char[128]); result.Append(fullTypeName); @@ -239,12 +192,10 @@ internal static bool IsBeginningOfGenericArgs(ref ReadOnlySpan span, out b return false; } - internal static bool TryGetTypeNameInfo(ref ReadOnlySpan input, ref List? nestedNameLengths, - out int totalLength, out int genericArgCount) + internal static bool TryGetTypeNameInfo(ref ReadOnlySpan input, ref List? nestedNameLengths, out int totalLength) { bool isNestedType; totalLength = 0; - genericArgCount = 0; do { int length = GetFullTypeNameLength(input.Slice(totalLength), out isNestedType); @@ -267,14 +218,6 @@ internal static bool TryGetTypeNameInfo(ref ReadOnlySpan input, ref List 0 && ((long)genericArgCount + generics > int.MaxValue))) - { - return false; // invalid type name detected! - } - genericArgCount += generics; - if (isNestedType) { // do not validate the type name now, it will be validated as a whole nested type name later diff --git a/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs index 6fae810b6bd0e..00134fa63e473 100644 --- a/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs +++ b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs @@ -160,8 +160,13 @@ private static (string typeNamespace, string name) SplitFullTypeName(string type else if (typeName.IsConstructedGenericType) { var genericArgs = typeName.GetGenericArguments(); - Type[] genericTypes = new Type[genericArgs.Length]; - for (int i = 0; i < genericArgs.Length; i++) +#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) 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 54299224cd861..3ffed6f3c7198 100644 --- a/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs +++ b/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs @@ -2429,7 +2429,7 @@ public sealed partial class TypeName : System.IEquatable GetGenericArguments() { throw null; } public System.Reflection.Metadata.TypeName GetGenericTypeDefinition() { throw null; } public System.Reflection.Metadata.TypeName GetElementType() { throw null; } diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs index 998b1c5adc477..fafa51571f999 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs @@ -10,35 +10,6 @@ namespace System.Reflection.Metadata.Tests { public class TypeNameParserHelpersTests { - public static IEnumerable GetGenericArgumentCountReturnsExpectedValue_Args() - { - yield return new object[] { $"TooLargeForInt`{long.MaxValue}", -1 }; - yield return new object[] { $"TooLargeForInt`{(long)int.MaxValue + 1}", -1 }; - yield return new object[] { $"TooLargeForInt`{(long)uint.MaxValue + 1}", -1 }; - } - - [Theory] - [InlineData("", 0)] // empty input - [InlineData("1", 0)] // short, valid - [InlineData("`1", -1)] // short, back tick as first char - [InlineData("`111", -1)] // long, back tick as first char - [InlineData("\\`111", 0)] // long enough, escaped back tick as first char - [InlineData("NoBackTick2", 0)] // no backtick, single digit - [InlineData("NoBackTick123", 0)] // no backtick, few digits - [InlineData("a`1", 1)] // valid, single digit - [InlineData("a`666", 666)] // valid, few digits - [InlineData("DigitBeforeBackTick1`7", 7)] // valid, single digit - [InlineData("DigitBeforeBackTick123`321", 321)] // valid, few digits - [InlineData("EscapedBacktick\\`1", 0)] // escaped backtick, single digit - [InlineData("EscapedBacktick\\`123", 0)] // escaped backtick, few digits - [InlineData("NegativeValue`-1", -1)] // negative value, single digit - [InlineData("NegativeValue`-222", -222)] // negative value, few digits - [InlineData("EscapedBacktickNegativeValue\\`-1", 0)] // negative value, single digit - [InlineData("EscapedBacktickNegativeValue\\`-222", 0)] // negative value, few digits - [MemberData(nameof(GetGenericArgumentCountReturnsExpectedValue_Args))] - public void GetGenericArgumentCountReturnsExpectedValue(string input, int expected) - => Assert.Equal(expected, TypeNameParserHelpers.GetGenericArgumentCount(input.AsSpan())); - [Theory] [InlineData("A[]", 1, false)] [InlineData("AB[a,b]", 2, false)] @@ -129,21 +100,20 @@ public void IsBeginningOfGenericAgsHandlesAllCasesProperly(string input, bool ex } [Theory] - [InlineData("A.B.C", true, null, 5, 0)] - [InlineData("A.B.C\\", false, null, 0, 0)] // invalid type name: ends with escape character - [InlineData("A.B.C\\DoeNotNeedEscaping", false, null, 0, 0)] // invalid type name: escapes non-special character - [InlineData("A.B+C", true, new int[] { 3 }, 5, 0)] - [InlineData("A.B++C", false, null, 0, 0)] // invalid type name: two following, unescaped + - [InlineData("A.B`1", true, null, 5, 1)] - [InlineData("A+B`1+C1`2+DD2`3+E", true, new int[] { 1, 3, 4, 5 }, 18, 6)] - [InlineData("Integer`2147483646+NoOverflow`1", true, new int[] { 18 }, 31, 2147483647)] - [InlineData("Integer`2147483647+Overflow`1", false, null, 0, 0)] // integer overflow for generic args count - public void TryGetTypeNameInfoGetsAllTheInfo(string input, bool expectedResult, int[] expectedNestedNameLengths, - int expectedTotalLength, int expectedGenericArgCount) + [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, out int genericArgCount); + bool result = TypeNameParserHelpers.TryGetTypeNameInfo(ref span, ref nestedNameLengths, out int totalLength); Assert.Equal(expectedResult, result); @@ -151,7 +121,6 @@ public void TryGetTypeNameInfoGetsAllTheInfo(string input, bool expectedResult, { Assert.Equal(expectedNestedNameLengths, nestedNameLengths?.ToArray()); Assert.Equal(expectedTotalLength, totalLength); - Assert.Equal(expectedGenericArgCount, genericArgCount); } } diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs index 34201b15ee5be..e19a87c2bffb8 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs @@ -53,22 +53,8 @@ public void EmptyStringsAreNotAllowed(string input) [InlineData("MissingAssemblyName, ")] [InlineData("ExtraComma, ,")] [InlineData("ExtraComma, , System.Runtime")] - [InlineData("TooManyGenericArgumentsDoubleSquareBracket'1[[a],[b]]")] - [InlineData("TooManyGenericArgumentsSingleSquareBracket'1[a,b]")] - [InlineData("TooManyGenericArgumentsDoubleSquareBracketTwoDigits'10[[1],[2],[3],[4],[5],[6],[7],[8],[9],[10],[11]]")] - [InlineData("TooManyGenericArgumentsSingleSquareBracketTwoDigits'10[1,2,3,4,5,6,7,8,9,10,11]")] - [InlineData("TooFewGenericArgumentsDoubleSquareBracket'3[[a],[b]]")] - [InlineData("TooFewGenericArgumentsDoubleSquareBracket'3[a,b]")] - [InlineData("TooFewGenericArgumentsDoubleSquareBracketTwoDigits'10[[1],[2],[3],[4],[5],[6],[7],[8],[9]]")] - [InlineData("TooFewGenericArgumentsSingleSquareBracketTwoDigits'10[1,2,3,4,5,6,7,8,9]")] - [InlineData("`1")] // back tick as first char followed by numbers (short) - [InlineData("`111")] // back tick as first char followed by numbers (longer) - [InlineData("NegativeGenericArgumentCount`-123")] [InlineData("MoreThanMaxArrayRank[,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,]")] - [InlineData("NonGenericTypeUsingGenericSyntax[[type1], [type2]]")] - [InlineData("NonGenericTypeUsingGenericSyntax[[type1, assembly1], [type2, assembly2]]")] - [InlineData("NonGenericTypeUsingGenericSyntax[type1,type2]")] - [InlineData("NonGenericTypeUsingGenericSyntax[[]]")] + [InlineData("UsingGenericSyntaxButNotProvidingGenericArgs[[]]")] [InlineData("ExtraCommaAfterFirstGenericArg`1[[type1, assembly1],]")] [InlineData("MissingClosingSquareBrackets`1[[type1, assembly1")] // missing ]] [InlineData("MissingClosingSquareBracket`1[[type1, assembly1]")] // missing ] @@ -81,7 +67,6 @@ public void EmptyStringsAreNotAllowed(string input) [InlineData("EscapeNonSpecialChar\\a")] [InlineData("EscapeNonSpecialChar\\0")] [InlineData("DoubleNestingChar++Bla")] - [InlineData("Integer`2147483647+Overflow`1")] public void InvalidTypeNamesAreNotAllowed(string input) { Assert.Throws(() => TypeName.Parse(input.AsSpan())); @@ -602,6 +587,22 @@ public void Equality(string left, string right, bool expected) Assert.Equal(TypeName.Parse(right.AsSpan()), TypeName.Parse(right.AsSpan())); } + [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?))] @@ -731,6 +732,8 @@ static void Verify(Type type, TypeName typeName, bool ignoreCase) } else { + Assert.True(typeName.IsVariableBoundArrayType); + return type.MakeArrayType(rank: typeName.GetArrayRank()); } } From afdbc5fdeeadabdd4a7d59900886f37a2656917b Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 17 Apr 2024 09:09:13 +0200 Subject: [PATCH 43/48] address API and code review feedback: - remove IEquatable Implementation from TypeName and AssemblyNameInfo (and GetHashCode too) - parser should not enforce runtime-specific rules like illegal type decorators (ByRef to ByRef etc) - add XML docs for the new AssemblyNameInfo type - propagate ProcessorArchitecture and ContentType from AssemblyNameInfo to the created AssemblyName - reject PublicKeyToken of odd length - remove checked arithmetic for getting node count: it's impossible to parse a name that would cause int overflow - add test case for escaped closing square bracket in the assembly name - remove unused code - address TODOs - add clarifications for ECMA divergence - remove one step from AssemblyNameInfo -> AssemblyName -> RuntimeAssemblyName conversion for NativeAOT --- .../Reflection/TypeNameParser.CoreCLR.cs | 4 +- .../System/Reflection/RuntimeAssemblyName.cs | 26 +-- .../Reflection/TypeNameParser.NativeAot.cs | 8 +- .../CustomAttributeTypeNameParser.cs | 4 +- .../Dataflow/TypeNameParser.Dataflow.cs | 4 +- .../System/Reflection/AssemblyNameParser.cs | 2 +- .../Reflection/Metadata/AssemblyNameInfo.cs | 185 +++++++----------- .../System/Reflection/Metadata/TypeName.cs | 22 +-- .../Reflection/Metadata/TypeNameParser.cs | 19 +- .../Metadata/TypeNameParserHelpers.cs | 44 +++-- .../Reflection/TypeNameParser.Helpers.cs | 23 ++- .../src/System/Reflection/AssemblyName.cs | 2 +- .../ref/System.Reflection.Metadata.cs | 10 +- .../src/Resources/Strings.resx | 3 + .../tests/Metadata/AssemblyNameInfoTests.cs | 67 ++++++- .../Metadata/TypeNameParserHelpersTests.cs | 8 + .../tests/Metadata/TypeNameTests.cs | 44 ++--- .../System/Reflection/TypeNameParser.Mono.cs | 4 +- 18 files changed, 241 insertions(+), 238 deletions(-) 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 1a49aecab6d7f..921908c95c33a 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 @@ -185,13 +185,13 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName, [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075:UnrecognizedReflectionPattern", Justification = "TypeNameParser.GetType is marked as RequiresUnreferencedCode.")] private Type? GetType(string escapedTypeName, // For nested types, it's Name. For other types it's FullName - ReadOnlySpan nestedTypeNames, AssemblyName? assemblyNameIfAny, string fullEscapedName) + ReadOnlySpan nestedTypeNames, Metadata.AssemblyNameInfo? assemblyNameIfAny, string fullEscapedName) { Assembly? assembly; if (assemblyNameIfAny is not null) { - assembly = ResolveAssembly(assemblyNameIfAny); + assembly = ResolveAssembly(assemblyNameIfAny.ToAssemblyName()); if (assembly is null) return null; } 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 95c80a5df9c3b..5e855eb58565d 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,13 +130,9 @@ public AssemblyName ToAssemblyName() return assemblyName; } - internal static RuntimeAssemblyName FromAssemblyName(AssemblyName source) + internal static RuntimeAssemblyName FromAssemblyNameInfo(AssemblyNameInfo source) { - byte[]? publicKeyOrToken = (source._flags & AssemblyNameFlags.PublicKey) != 0 - ? source.GetPublicKey() - : source.GetPublicKeyToken(); - - return new(source.Name, source.Version, source.CultureName, source._flags, publicKeyOrToken); + return new(source.Name, source.Version, source.CultureName, source._flags, source.PublicKeyOrToken); } // @@ -151,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) @@ -177,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 d9329ebc84f7f..e5750f30fecee 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 @@ -93,16 +93,16 @@ internal partial struct TypeNameParser }.Resolve(parsed); } - private Assembly? ResolveAssembly(AssemblyName assemblyName) + private Assembly? ResolveAssembly(Metadata.AssemblyNameInfo assemblyName) { Assembly? assembly; if (_assemblyResolver is not null) { - assembly = _assemblyResolver(assemblyName); + assembly = _assemblyResolver(assemblyName.ToAssemblyName()); } else { - assembly = RuntimeAssemblyInfo.GetRuntimeAssemblyIfExists(RuntimeAssemblyName.FromAssemblyName(assemblyName)); + assembly = RuntimeAssemblyInfo.GetRuntimeAssemblyIfExists(RuntimeAssemblyName.FromAssemblyNameInfo(assemblyName)); } if (assembly is null && _throwOnError) @@ -117,7 +117,7 @@ internal partial struct TypeNameParser Justification = "GetType APIs are marked as RequiresUnreferencedCode.")] [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075:UnrecognizedReflectionPattern", Justification = "GetType APIs are marked as RequiresUnreferencedCode.")] - private Type? GetType(string escapedTypeName, ReadOnlySpan nestedTypeNames, AssemblyName? assemblyNameIfAny, string _) + private Type? GetType(string escapedTypeName, ReadOnlySpan nestedTypeNames, Metadata.AssemblyNameInfo? assemblyNameIfAny, string _) { Assembly? assembly; diff --git a/src/coreclr/tools/Common/TypeSystem/Common/Utilities/CustomAttributeTypeNameParser.cs b/src/coreclr/tools/Common/TypeSystem/Common/Utilities/CustomAttributeTypeNameParser.cs index 49345ea00ea39..e4be278066efe 100644 --- a/src/coreclr/tools/Common/TypeSystem/Common/Utilities/CustomAttributeTypeNameParser.cs +++ b/src/coreclr/tools/Common/TypeSystem/Common/Utilities/CustomAttributeTypeNameParser.cs @@ -68,10 +68,10 @@ public Type MakeGenericType(Type[] typeArguments) } } - private Type GetType(string typeName, ReadOnlySpan nestedTypeNames, AssemblyName assemblyNameIfAny, string fullEscapedName) + private Type GetType(string typeName, ReadOnlySpan nestedTypeNames, AssemblyNameInfo assemblyNameIfAny, string fullEscapedName) { ModuleDesc module = (assemblyNameIfAny == null) ? _module : - _module.Context.ResolveAssembly(assemblyNameIfAny, throwIfNotFound: _throwIfNotFound); + _module.Context.ResolveAssembly(assemblyNameIfAny.ToAssemblyName(), throwIfNotFound: _throwIfNotFound); if (_canonResolver != null && nestedTypeNames.IsEmpty) { 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 1e617a1f1faba..7a1c70574568b 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 @@ -55,13 +55,13 @@ public Type MakeGenericType(Type[] typeArguments) } } - private Type GetType(string typeName, ReadOnlySpan nestedTypeNames, AssemblyName assemblyNameIfAny, string _) + private Type GetType(string typeName, ReadOnlySpan nestedTypeNames, AssemblyNameInfo assemblyNameIfAny, string _) { ModuleDesc module; if (assemblyNameIfAny != null) { - module = _context.ResolveAssembly(assemblyNameIfAny, throwIfNotFound: false); + module = _context.ResolveAssembly(assemblyNameIfAny.ToAssemblyName(), throwIfNotFound: false); } else { diff --git a/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs b/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs index 7274d2ed5364a..90b7ec273e991 100644 --- a/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs @@ -309,7 +309,7 @@ private static bool TryParsePKT(string attributeValue, bool isToken, ref byte[]? return true; } - if (isToken && attributeValue.Length != 8 * 2) + if (attributeValue.Length % 2 != 0 || (isToken && attributeValue.Length != 8 * 2)) { return false; } diff --git a/src/libraries/Common/src/System/Reflection/Metadata/AssemblyNameInfo.cs b/src/libraries/Common/src/System/Reflection/Metadata/AssemblyNameInfo.cs index d4b21bd7f906f..08785c362ea5d 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/AssemblyNameInfo.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/AssemblyNameInfo.cs @@ -9,23 +9,39 @@ 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 : IEquatable + 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, Collections.Immutable.ImmutableArray publicKeyOrToken = default) { Name = name ?? throw new ArgumentNullException(nameof(name)); Version = version; CultureName = cultureName; - Flags = flags; + _flags = flags; PublicKeyOrToken = publicKeyOrToken; } #endif @@ -35,7 +51,7 @@ internal AssemblyNameInfo(AssemblyNameParser.AssemblyNameParts parts) Name = parts._name; Version = parts._version; CultureName = parts._cultureName; - Flags = parts._flags; + _flags = parts._flags; #if SYSTEM_PRIVATE_CORELIB PublicKeyOrToken = parts._publicKeyOrToken; #else @@ -49,17 +65,40 @@ internal AssemblyNameInfo(AssemblyNameParser.AssemblyNameParts parts) #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; } - public AssemblyNameFlags Flags { get; } + /// + /// Gets the attributes of the assembly. + /// + public AssemblyNameFlags Flags => ExtractAssemblyNameFlags(_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 Collections.Immutable.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 @@ -74,131 +113,29 @@ public string FullName #else ToArray(PublicKeyOrToken); #endif - _fullName = AssemblyNameFormatter.ComputeDisplayName(Name, Version, CultureName, publicKeyToken); + _fullName = AssemblyNameFormatter.ComputeDisplayName(Name, Version, CultureName, publicKeyToken, Flags, ExtractAssemblyContentType(_flags)); } return _fullName; } } - public bool Equals(AssemblyNameInfo? other) - { - if (other is null || Flags != other.Flags || !Name.Equals(other.Name) || !string.Equals(CultureName, other.CultureName)) - { - return false; - } - - if (Version is null) - { - if (other.Version is not null) - { - return false; - } - } - else - { - if (!Version.Equals(other.Version)) - { - return false; - } - } - - return SequenceEqual(PublicKeyOrToken, other.PublicKeyOrToken); - -#if SYSTEM_PRIVATE_CORELIB - static bool SequenceEqual(byte[]? left, byte[]? right) - { - if (left is null) - { - if (right is not null) - { - return false; - } - } - else if (right is null) - { - return false; - } - else if (left.Length != right.Length) - { - return false; - } - else - { - for (int i = 0; i < left.Length; i++) - { - if (left[i] != right[i]) - { - return false; - } - } - } - - return true; - } -#else - static bool SequenceEqual(Collections.Immutable.ImmutableArray left, Collections.Immutable.ImmutableArray right) - { - int leftLength = left.IsDefaultOrEmpty ? 0 : left.Length; - int rightLength = right.IsDefaultOrEmpty ? 0 : right.Length; - - if (leftLength != rightLength) - { - return false; - } - else if (leftLength > 0) - { - for (int i = 0; i < leftLength; i++) - { - if (left[i] != right[i]) - { - return false; - } - } - } - - return true; - } -#endif - } - - public override bool Equals(object? obj) => Equals(obj as AssemblyNameInfo); - - public override int GetHashCode() - { -#if NETCOREAPP - HashCode hashCode = default; - hashCode.Add(Name); - hashCode.Add(Flags); - hashCode.Add(Version); - - #if SYSTEM_PRIVATE_CORELIB - if (PublicKeyOrToken is not null) - { - hashCode.AddBytes(PublicKeyOrToken); - } - #else - if (!PublicKeyOrToken.IsDefaultOrEmpty) - { - hashCode.AddBytes(Runtime.InteropServices.ImmutableCollectionsMarshal.AsArray(PublicKeyOrToken)); - } - #endif - return hashCode.ToHashCode(); -#else - return FullName.GetHashCode(); -#endif - } - + /// + /// 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 - assemblyName._flags = Flags; - if (PublicKeyOrToken is not null) { if ((Flags & AssemblyNameFlags.PublicKey) != 0) @@ -211,10 +148,9 @@ public AssemblyName ToAssemblyName() } } #else - assemblyName.Flags = Flags; - if (!PublicKeyOrToken.IsDefault) { + // 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(ToArray(PublicKeyOrToken)); @@ -238,7 +174,11 @@ public AssemblyName ToAssemblyName() public static AssemblyNameInfo Parse(ReadOnlySpan assemblyName) => TryParse(assemblyName, out AssemblyNameInfo? result) ? result! - : throw new ArgumentException("TODO_adsitnik_add_or_reuse_resource"); +#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. @@ -263,6 +203,15 @@ public static bool TryParse(ReadOnlySpan assemblyName, 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); + #if !SYSTEM_PRIVATE_CORELIB private static byte[]? ToArray(Collections.Immutable.ImmutableArray input) { diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs index 3d99ba71f95d3..f7205e1076315 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs @@ -20,7 +20,7 @@ namespace System.Reflection.Metadata #else public #endif - sealed class TypeName : IEquatable + sealed class TypeName { /// /// Positive value is array rank. @@ -239,18 +239,6 @@ public string Name } } - public bool Equals(TypeName? other) - => other is not null - && other._rankOrModifier == _rankOrModifier - // try to prevent from allocations if possible (AssemblyQualifiedName can allocate) - && ((other.AssemblyName is null && AssemblyName is null) - || (other.AssemblyName is not null && AssemblyName is not null)) - && other.AssemblyQualifiedName == AssemblyQualifiedName; - - public override bool Equals(object? obj) => Equals(obj as TypeName); - - public override int GetHashCode() => AssemblyQualifiedName.GetHashCode(); - /// /// Represents the total number of instances that are used to describe /// this instance, including any generic arguments or underlying types. @@ -285,20 +273,20 @@ public int GetNodeCount() if (IsNested) { - result = checked(result + DeclaringType.GetNodeCount()); + result += DeclaringType.GetNodeCount(); } else if (IsConstructedGenericType) { - result = checked(result + 1); + result++; } else if (IsArray || IsPointer || IsByRef) { - result = checked(result + GetElementType().GetNodeCount()); + result += GetElementType().GetNodeCount(); } foreach (TypeName genericArgument in _genericArguments) { - result = checked(result + genericArgument.GetNodeCount()); + result += genericArgument.GetNodeCount(); } return result; diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs index d989199eddbbe..49128313f175a 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs @@ -156,15 +156,8 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse return null; } - if (previousDecorator == ByRef // it's illegal for managed reference to be followed by any other decorator - || parsedDecorator > MaxArrayRank) - { -#if SYSTEM_PRIVATE_CORELIB - throw new TypeLoadException(); // CLR throws TypeLoadException for invalid decorators -#else - return null; -#endif - } + // 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; } @@ -218,17 +211,13 @@ private bool TryParseAssemblyName(ref AssemblyNameInfo? assemblyName) return false; } - // The only delimiter which can terminate an assembly name is ']'. - // Otherwise EOL serves as the terminator. - int assemblyNameLength = (int)Math.Min((uint)_inputString.IndexOf(']'), (uint)_inputString.Length); - ReadOnlySpan candidate = _inputString.Slice(0, assemblyNameLength); - + ReadOnlySpan candidate = GetAssemblyNameCandidate(_inputString); if (!AssemblyNameInfo.TryParse(candidate, out assemblyName)) { return false; } - _inputString = _inputString.Slice(assemblyNameLength); + _inputString = _inputString.Slice(candidate.Length); return true; } diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs index c3f94bfc40210..a9d986126cef7 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs @@ -6,16 +6,12 @@ using System.Diagnostics; using System.Text; -using static System.Char; -using static System.Int32; - #nullable enable namespace System.Reflection.Metadata { internal static class TypeNameParserHelpers { - internal const int MaxArrayRank = 32; internal const sbyte SZArray = -1; internal const sbyte Pointer = -2; internal const sbyte ByRef = -3; @@ -100,6 +96,7 @@ static int GetUnescapedOffset(ReadOnlySpan input, int startOffset) return offset; } + // This is not a strict implementation of ECMA, so far all .NET Runtimes were forbidding to escape other characters. static bool NeedsEscaping(char c) => c is '[' or ']' or '&' or '*' or ',' or '+' or EscapeCharacter; } @@ -132,6 +129,37 @@ static int GetUnescapedOffset(ReadOnlySpan fullName, int startIndex) } } + // 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, ValueStringBuilder builder) { if (rankOrModifier == ByRef) @@ -152,7 +180,7 @@ internal static string GetRankOrModifierStringRepresentation(int rankOrModifier, } else { - Debug.Assert(rankOrModifier >= 2 && rankOrModifier <= 32); + Debug.Assert(rankOrModifier >= 2); builder.Append('['); builder.Append(',', rankOrModifier - 1); @@ -344,11 +372,5 @@ internal static InvalidOperationException InvalidOperation_HasToBeArrayClass() = #else // tools that reference this file as a link new InvalidOperationException(); #endif - -#if !NETCOREAPP - private static bool TryParse(ReadOnlySpan input, out int value) => int.TryParse(input.ToString(), out value); - - private static bool IsAsciiDigit(char ch) => ch >= '0' && ch <= '9'; -#endif } } diff --git a/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs index 00134fa63e473..30baa7e1cd01e 100644 --- a/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs +++ b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs @@ -13,6 +13,9 @@ internal partial struct TypeNameParser { 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); @@ -90,6 +93,7 @@ private static (string typeNamespace, string name) SplitFullTypeName(string type 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) { @@ -119,6 +123,8 @@ private static (string typeNamespace, string name) SplitFullTypeName(string type 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) @@ -128,7 +134,7 @@ private static (string typeNamespace, string name) SplitFullTypeName(string type } string nonNestedParentName = current!.FullName; - Type? type = GetType(nonNestedParentName, nestedTypeNames, typeName.AssemblyName?.ToAssemblyName(), typeName.FullName); + Type? type = GetType(nonNestedParentName, nestedTypeNames, typeName.AssemblyName, typeName.FullName); return Make(type, typeName); } else if (typeName.IsConstructedGenericType) @@ -137,11 +143,22 @@ private static (string typeNamespace, string name) SplitFullTypeName(string type } else if (typeName.IsArray || typeName.IsPointer || typeName.IsByRef) { - return Make(Resolve(typeName.GetElementType()), typeName); + Metadata.TypeName elementType = typeName.GetElementType(); + + if (elementType.IsByRef || (typeName.IsVariableBoundArrayType && typeName.GetArrayRank() > 32)) + { +#if SYSTEM_PRIVATE_CORELIB + throw new TypeLoadException(); // CLR throws TypeLoadException for invalid decorators +#else + return null; +#endif + } + + return Make(Resolve(elementType), typeName); } else { - Type? type = GetType(typeName.FullName, nestedTypeNames: ReadOnlySpan.Empty, typeName.AssemblyName?.ToAssemblyName(), typeName.FullName); + Type? type = GetType(typeName.FullName, nestedTypeNames: ReadOnlySpan.Empty, typeName.AssemblyName, typeName.FullName); return Make(type, typeName); } diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/AssemblyName.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/AssemblyName.cs index 85b07fdf28377..ec96aa8bc07b5 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/AssemblyName.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/AssemblyName.cs @@ -24,7 +24,7 @@ public sealed partial class AssemblyName : ICloneable, IDeserializationCallback, private AssemblyHashAlgorithm _hashAlgorithm; private AssemblyVersionCompatibility _versionCompatibility; - internal AssemblyNameFlags _flags; + private AssemblyNameFlags _flags; public AssemblyName(string assemblyName) : this() 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 3ffed6f3c7198..6eaf4ea70e61e 100644 --- a/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs +++ b/src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs @@ -2408,7 +2408,7 @@ public readonly partial struct TypeLayout public int PackingSize { get { throw null; } } public int Size { get { throw null; } } } - public sealed partial class AssemblyNameInfo : System.IEquatable + 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) { } @@ -2420,12 +2420,9 @@ public AssemblyNameInfo(string name, System.Version? version = null, string? cul 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 override bool Equals(object? obj) { throw null; } - public bool Equals(System.Reflection.Metadata.AssemblyNameInfo? other) { throw null; } - public override int GetHashCode() { throw null; } public System.Reflection.AssemblyName ToAssemblyName() { throw null; } } - public sealed partial class TypeName : System.IEquatable + public sealed partial class TypeName { internal TypeName() { } public string AssemblyQualifiedName { get { throw null; } } @@ -2443,9 +2440,6 @@ internal TypeName() { } 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 override bool Equals(object? obj) { throw null; } - public bool Equals(System.Reflection.Metadata.TypeName? other) { throw null; } - public override int GetHashCode() { throw null; } public int GetArrayRank() { throw null; } public System.Collections.Immutable.ImmutableArray GetGenericArguments() { throw null; } public System.Reflection.Metadata.TypeName GetGenericTypeDefinition() { throw null; } diff --git a/src/libraries/System.Reflection.Metadata/src/Resources/Strings.resx b/src/libraries/System.Reflection.Metadata/src/Resources/Strings.resx index bcc8499ec5f47..963e4d0af9f8a 100644 --- a/src/libraries/System.Reflection.Metadata/src/Resources/Strings.resx +++ b/src/libraries/System.Reflection.Metadata/src/Resources/Strings.resx @@ -429,4 +429,7 @@ 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/tests/Metadata/AssemblyNameInfoTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/AssemblyNameInfoTests.cs index 5642173544841..230e051ab045c 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/AssemblyNameInfoTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/AssemblyNameInfoTests.cs @@ -8,15 +8,15 @@ namespace System.Reflection.Metadata.Tests.Metadata public class AssemblyNameInfoTests { [Theory] - [InlineData("MyAssemblyName, Version=1.0.0.0, PublicKeyToken=b77a5c561934e089", "MyAssemblyName, Version=1.0.0.0, PublicKeyToken=b77a5c561934e089")] - public void WithPublicTokenKey(string name, string expectedName) + [InlineData("MyAssemblyName, Version=1.0.0.0, PublicKeyToken=b77a5c561934e089")] + public void WithPublicTokenKey(string fullName) { - AssemblyName assemblyName = new AssemblyName(name); + AssemblyName assemblyName = new AssemblyName(fullName); - AssemblyNameInfo assemblyNameInfo = AssemblyNameInfo.Parse(name.AsSpan()); + AssemblyNameInfo assemblyNameInfo = AssemblyNameInfo.Parse(fullName.AsSpan()); - Assert.Equal(expectedName, assemblyName.FullName); - Assert.Equal(expectedName, assemblyNameInfo.FullName); + Assert.Equal(fullName, assemblyName.FullName); + Assert.Equal(fullName, assemblyNameInfo.FullName); Roundtrip(assemblyName); } @@ -32,6 +32,60 @@ public void NoPublicKeyOrToken() 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()); @@ -45,6 +99,7 @@ static void Roundtrip(AssemblyName source) 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 index fafa51571f999..d9b071e6e929c 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs @@ -48,6 +48,14 @@ public void GetFullTypeNameLengthReturnsExpectedValue(string input, int expected 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, "*")] diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs index e19a87c2bffb8..8658dccd4486e 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs @@ -53,7 +53,6 @@ public void EmptyStringsAreNotAllowed(string input) [InlineData("MissingAssemblyName, ")] [InlineData("ExtraComma, ,")] [InlineData("ExtraComma, , System.Runtime")] - [InlineData("MoreThanMaxArrayRank[,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,]")] [InlineData("UsingGenericSyntaxButNotProvidingGenericArgs[[]]")] [InlineData("ExtraCommaAfterFirstGenericArg`1[[type1, assembly1],]")] [InlineData("MissingClosingSquareBrackets`1[[type1, assembly1")] // missing ]] @@ -62,7 +61,6 @@ public void EmptyStringsAreNotAllowed(string input) [InlineData("MissingClosingSquareBrackets`2[[type1, assembly1], [type2, assembly2")] // missing ] [InlineData("MissingClosingSquareBracketsMixedMode`2[type1, [type2, assembly2")] // missing ]] [InlineData("MissingClosingSquareBracketsMixedMode`2[type1, [type2, assembly2]")] // missing ] - [InlineData("CantMakeByRefToByRef&&")] [InlineData("EscapeCharacterAtTheEnd\\")] [InlineData("EscapeNonSpecialChar\\a")] [InlineData("EscapeNonSpecialChar\\0")] @@ -74,6 +72,23 @@ public void InvalidTypeNamesAreNotAllowed(string input) Assert.False(TypeName.TryParse(input.AsSpan(), out _)); } + [Theory] + [InlineData("int&&")] // by-ref to by-ref is currently not supported by CLR + [InlineData("int[,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,]")] // 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)); +#elif NETFRAMEWORK + Assert.Null(Type.GetType(input)); +#endif + } + } + [Theory] [InlineData("Namespace.Kość", "Namespace.Kość")] public void UnicodeCharactersAreAllowedByDefault(string input, string expectedFullName) @@ -562,31 +577,6 @@ public void ParsedNamesMatchSystemTypeNames(Type type) } } - [Theory] - [InlineData("name", "name", true)] - [InlineData("Name", "Name", true)] - [InlineData("name", "Name", false)] - [InlineData("Name", "name", false)] - [InlineData("type, assembly", "type, assembly", true)] - [InlineData("Type, Assembly", "Type, Assembly", true)] - [InlineData("Type, Assembly", "type, assembly", false)] - [InlineData("Type, assembly", "type, Assembly", false)] - [InlineData("name[]", "name[]", true)] - [InlineData("name[]", "name[*]", false)] - [InlineData("name[]", "name[,]", false)] - [InlineData("name*", "name*", true)] - [InlineData("name&", "name&", true)] - [InlineData("name*", "name&", false)] - [InlineData("generic`1[[int]]", "generic`1[[int]]", true)] // exactly the same - [InlineData("generic`1[[int]]", "generic`1[int]", true)] // different generic args syntax describing same type - [InlineData("generic`2[[int],[bool]]", "generic`2[int,bool]", true)] - public void Equality(string left, string right, bool expected) - { - Assert.Equal(expected, TypeName.Parse(left.AsSpan()).Equals(TypeName.Parse(right.AsSpan()))); - Assert.Equal(TypeName.Parse(left.AsSpan()), TypeName.Parse(left.AsSpan())); - Assert.Equal(TypeName.Parse(right.AsSpan()), TypeName.Parse(right.AsSpan())); - } - [Theory] [InlineData("Name`2[[int], [bool]]", "Name`2")] // match [InlineData("Name`1[[int], [bool]]", "Name`1")] // less than expected 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 8193358316cd9..da5a7c6f2e80c 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 @@ -95,9 +95,9 @@ internal unsafe ref partial struct TypeNameParser Justification = "TypeNameParser.GetType is marked as RequiresUnreferencedCode.")] [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "TypeNameParser.GetType is marked as RequiresUnreferencedCode.")] - private Type? GetType(string escapedTypeName, ReadOnlySpan nestedTypeNames, AssemblyName? assemblyNameIfAny, string _) + private Type? GetType(string escapedTypeName, ReadOnlySpan nestedTypeNames, Metadata.AssemblyNameInfo? assemblyNameIfAny, string _) { - Assembly? assembly = (assemblyNameIfAny is not null) ? ResolveAssembly(assemblyNameIfAny) : null; + Assembly? assembly = (assemblyNameIfAny is not null) ? ResolveAssembly(assemblyNameIfAny.ToAssemblyName()) : null; // Both the external type resolver and the default type resolvers expect escaped type names Type? type; From 413be9ed932ff18b8692a742667038e0d0079189 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 17 Apr 2024 13:46:52 +0200 Subject: [PATCH 44/48] minor tweaks after reading the code again --- .../System/Reflection/Metadata/AssemblyNameInfo.cs | 14 +++++++++----- .../Reflection/Metadata/TypeNameParserHelpers.cs | 1 - .../src/System/Reflection/AssemblyName.cs | 6 +----- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/libraries/Common/src/System/Reflection/Metadata/AssemblyNameInfo.cs b/src/libraries/Common/src/System/Reflection/Metadata/AssemblyNameInfo.cs index 08785c362ea5d..3e571eef99f03 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/AssemblyNameInfo.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/AssemblyNameInfo.cs @@ -7,6 +7,10 @@ using System.Diagnostics.CodeAnalysis; using System.Text; +#if !SYSTEM_PRIVATE_CORELIB +using System.Collections.Immutable; +#endif + namespace System.Reflection.Metadata { /// @@ -36,7 +40,7 @@ sealed class AssemblyNameInfo /// 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, Collections.Immutable.ImmutableArray publicKeyOrToken = default) + 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; @@ -56,11 +60,11 @@ internal AssemblyNameInfo(AssemblyNameParser.AssemblyNameParts parts) PublicKeyOrToken = parts._publicKeyOrToken; #else PublicKeyOrToken = parts._publicKeyOrToken is null ? default : parts._publicKeyOrToken.Length == 0 - ? Collections.Immutable.ImmutableArray.Empty + ? ImmutableArray.Empty #if NET8_0_OR_GREATER : Runtime.InteropServices.ImmutableCollectionsMarshal.AsImmutableArray(parts._publicKeyOrToken); #else - : Collections.Immutable.ImmutableArray.Create(parts._publicKeyOrToken); + : ImmutableArray.Create(parts._publicKeyOrToken); #endif #endif } @@ -92,7 +96,7 @@ internal AssemblyNameInfo(AssemblyNameParser.AssemblyNameParts parts) #if SYSTEM_PRIVATE_CORELIB public byte[]? PublicKeyOrToken { get; } #else - public Collections.Immutable.ImmutableArray PublicKeyOrToken { get; } + public ImmutableArray PublicKeyOrToken { get; } #endif /// @@ -213,7 +217,7 @@ internal static ProcessorArchitecture ExtractProcessorArchitecture(AssemblyNameF => (ProcessorArchitecture)((((int)flags) >> 4) & 0x7); #if !SYSTEM_PRIVATE_CORELIB - private static byte[]? ToArray(Collections.Immutable.ImmutableArray input) + private static byte[]? ToArray(ImmutableArray input) { // not using System.Linq.ImmutableArrayExtensions.ToArray as TypeSystem does not allow System.Linq if (input.IsDefault) diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs index a9d986126cef7..4f3b57676e3a3 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs @@ -96,7 +96,6 @@ static int GetUnescapedOffset(ReadOnlySpan input, int startOffset) return offset; } - // This is not a strict implementation of ECMA, so far all .NET Runtimes were forbidding to escape other characters. static bool NeedsEscaping(char c) => c is '[' or ']' or '&' or '*' or ',' or '+' or EscapeCharacter; } diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/AssemblyName.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/AssemblyName.cs index ec96aa8bc07b5..7440e1fae8dac 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/AssemblyName.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/AssemblyName.cs @@ -33,11 +33,7 @@ public AssemblyName(string assemblyName) if (assemblyName[0] == '\0') throw new ArgumentException(SR.Format_StringZeroLength); - Init(AssemblyNameParser.Parse(assemblyName)); - } - - internal void Init(AssemblyNameParser.AssemblyNameParts parts) - { + AssemblyNameParser.AssemblyNameParts parts = AssemblyNameParser.Parse(assemblyName); _name = parts._name; _version = parts._version; _flags = parts._flags; From b424d76096b8d581ded219386fefbcd4acdb82cd Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Thu, 18 Apr 2024 15:35:31 +0200 Subject: [PATCH 45/48] try to improve the escaping handling --- .../Reflection/TypeNameParser.CoreCLR.cs | 27 ++++----- .../Reflection/TypeNameParser.NativeAot.cs | 11 ++-- .../Reflection/TypeNameParser.Helpers.cs | 57 ++++++------------- 3 files changed, 33 insertions(+), 62 deletions(-) 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 921908c95c33a..24462d72ce53e 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 @@ -230,19 +230,18 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName, } return null; } - return GetTypeFromDefaultAssemblies(escapedTypeName, nestedTypeNames, fullEscapedName); + return GetTypeFromDefaultAssemblies(UnescapeTypeName(escapedTypeName), nestedTypeNames, fullEscapedName); } 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(UnescapeTypeName(escapedTypeName), - UnescapeTypeNames(nestedTypeNames) ?? nestedTypeNames, - throwOnError: _throwOnError, ignoreCase: _ignoreCase); + return runtimeAssembly.GetTypeCore(unescapedTypeName, nestedTypeNames, throwOnError: _throwOnError, ignoreCase: _ignoreCase); } - type = runtimeAssembly.GetTypeCore(UnescapeTypeName(escapedTypeName), default, throwOnError: _throwOnError, ignoreCase: _ignoreCase); + type = runtimeAssembly.GetTypeCore(unescapedTypeName, default, throwOnError: _throwOnError, ignoreCase: _ignoreCase); } else { @@ -262,7 +261,7 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName, if (_ignoreCase) bindingFlags |= BindingFlags.IgnoreCase; - type = type.GetNestedType(UnescapeTypeName(nestedTypeNames[i]), bindingFlags); + type = type.GetNestedType(nestedTypeNames[i], bindingFlags); if (type is null) { @@ -278,16 +277,12 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName, return type; } - private Type? GetTypeFromDefaultAssemblies(string escapedTypeName, ReadOnlySpan nestedTypeNames, string fullEscapedName) + private Type? GetTypeFromDefaultAssemblies(string typeName, ReadOnlySpan nestedTypeNames, string fullEscapedName) { - string unescapedTypeName = UnescapeTypeName(escapedTypeName); - string[]? unescapedNestedNames = UnescapeTypeNames(nestedTypeNames); - RuntimeAssembly? requestingAssembly = (RuntimeAssembly?)_requestingAssembly; if (requestingAssembly is not null) { - Type? type = ((RuntimeAssembly)requestingAssembly).GetTypeCore(unescapedTypeName, - unescapedNestedNames ?? nestedTypeNames, throwOnError: false, ignoreCase: _ignoreCase); + Type? type = requestingAssembly.GetTypeCore(typeName, nestedTypeNames, throwOnError: false, ignoreCase: _ignoreCase); if (type is not null) return type; } @@ -295,8 +290,7 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName, RuntimeAssembly coreLib = (RuntimeAssembly)typeof(object).Assembly; if (requestingAssembly != coreLib) { - Type? type = ((RuntimeAssembly)coreLib).GetTypeCore(unescapedTypeName, - unescapedNestedNames ?? nestedTypeNames, throwOnError: false, ignoreCase: _ignoreCase); + Type? type = coreLib.GetTypeCore(typeName, nestedTypeNames, throwOnError: false, ignoreCase: _ignoreCase); if (type is not null) return type; } @@ -304,14 +298,13 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName, RuntimeAssembly? resolvedAssembly = AssemblyLoadContext.OnTypeResolve(requestingAssembly, fullEscapedName); if (resolvedAssembly is not null) { - Type? type = resolvedAssembly.GetTypeCore(unescapedTypeName, - unescapedNestedNames ?? nestedTypeNames, throwOnError: false, ignoreCase: _ignoreCase); + Type? type = resolvedAssembly.GetTypeCore(typeName, nestedTypeNames, throwOnError: false, ignoreCase: _ignoreCase); if (type is not null) return type; } if (_throwOnError) - throw new TypeLoadException(SR.Format(SR.TypeLoad_ResolveTypeFromAssembly, escapedTypeName, (requestingAssembly ?? coreLib).FullName)); + throw new TypeLoadException(SR.Format(SR.TypeLoad_ResolveTypeFromAssembly, fullEscapedName, (requestingAssembly ?? coreLib).FullName)); return null; } 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 e5750f30fecee..dcebd79718276 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 @@ -171,13 +171,15 @@ internal partial struct TypeNameParser } else { + string? unescapedTypeName = UnescapeTypeName(escapedTypeName); + RuntimeAssemblyInfo? defaultAssembly = null; if (_defaultAssemblyName != null) { defaultAssembly = RuntimeAssemblyInfo.GetRuntimeAssemblyIfExists(RuntimeAssemblyName.Parse(_defaultAssemblyName)); if (defaultAssembly != null) { - type = defaultAssembly.GetTypeCore(UnescapeTypeName(escapedTypeName), throwOnError: false, ignoreCase: _ignoreCase); + type = defaultAssembly.GetTypeCore(unescapedTypeName, throwOnError: false, ignoreCase: _ignoreCase); } } @@ -187,7 +189,7 @@ internal partial struct TypeNameParser coreLib = (RuntimeAssemblyInfo)typeof(object).Assembly; if (coreLib != assembly) { - type = coreLib.GetTypeCore(UnescapeTypeName(escapedTypeName), throwOnError: false, ignoreCase: _ignoreCase); + type = coreLib.GetTypeCore(unescapedTypeName, throwOnError: false, ignoreCase: _ignoreCase); } } @@ -195,7 +197,7 @@ internal partial struct TypeNameParser { if (_throwOnError) { - throw Helpers.CreateTypeLoadException(UnescapeTypeName(escapedTypeName), (defaultAssembly ?? coreLib).FullName); + throw Helpers.CreateTypeLoadException(unescapedTypeName, (defaultAssembly ?? coreLib).FullName); } return null; } @@ -216,10 +218,9 @@ internal partial struct TypeNameParser 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; diff --git a/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs index 30baa7e1cd01e..8a98d381a8360 100644 --- a/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs +++ b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; using System.Text; #nullable enable @@ -11,6 +12,7 @@ namespace System.Reflection { internal partial struct TypeNameParser { +#if !MONO // Mono never needs unescaped names private const char EscapeCharacter = '\\'; /// @@ -47,46 +49,7 @@ private static string UnescapeTypeName(string name) return sb.ToString(); } - - /// - /// Returns non-null array when some unescaping of nested names was required. - /// - private static string[]? UnescapeTypeNames(ReadOnlySpan names) - { - if (names.IsEmpty) // nothing to check - { - return null; - } - - int i = 0; - for (; i < names.Length; i++) - { -#if NETCOREAPP - if (names[i].Contains(EscapeCharacter)) -#else - if (names[i].IndexOf(EscapeCharacter) >= 0) #endif - { - break; - } - } - - if (i == names.Length) // nothing to escape - { - return null; - } - - string[] unescapedNames = new string[names.Length]; - for (int j = 0; j < i; j++) - { - unescapedNames[j] = names[j]; // copy what not needed escaping - } - for (; i < names.Length; i++) - { - unescapedNames[i] = UnescapeTypeName(names[i]); // escape the rest - } - return unescapedNames; - } private static (string typeNamespace, string name) SplitFullTypeName(string typeName) { @@ -129,10 +92,18 @@ private static (string typeNamespace, string name) SplitFullTypeName(string type 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.AssemblyName, typeName.FullName); return Make(type, typeName); @@ -158,7 +129,13 @@ private static (string typeNamespace, string name) SplitFullTypeName(string type } else { - Type? type = GetType(typeName.FullName, nestedTypeNames: ReadOnlySpan.Empty, typeName.AssemblyName, typeName.FullName); + Type? type = GetType( +#if SYSTEM_PRIVATE_CORELIB + typeName.FullName, +#else // the tools require unescaped names + UnescapeTypeName(typeName.FullName), +#endif + nestedTypeNames: ReadOnlySpan.Empty, typeName.AssemblyName, typeName.FullName); return Make(type, typeName); } From da203c0a6da07f68fce2b75ca5f302ea7b5fc15f Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 23 Apr 2024 18:24:23 +0200 Subject: [PATCH 46/48] address code review feedback: - unify syntax for character checks - remove false statement about unreachable code from the AssemblyNameParser - fix whitespace formatting - reduce code duplication and supress RS0030 for TypeSystem (we need to use System.Linq.ImmutableArrayExtensions.ToArray) - use hungarian notation for static fields - remove boxing - pass VSB by reference to avoid resource leaks - use HexConverter.TryDecodeFromUtf16 --- .../System/Reflection/AssemblyNameParser.cs | 36 +++++-------------- .../Reflection/Metadata/AssemblyNameInfo.cs | 30 ++++------------ .../System/Reflection/Metadata/TypeName.cs | 25 +++++++------ .../Reflection/Metadata/TypeNameParser.cs | 5 ++- .../Metadata/TypeNameParserHelpers.cs | 10 +++--- .../Metadata/TypeNameParserHelpersTests.cs | 5 +-- 6 files changed, 41 insertions(+), 70 deletions(-) diff --git a/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs b/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs index 90b7ec273e991..19ac86bd15f71 100644 --- a/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs @@ -315,19 +315,11 @@ private static bool TryParsePKT(string attributeValue, bool isToken, ref byte[]? } byte[] pkt = new byte[attributeValue.Length / 2]; - int srcIndex = 0; - for (int i = 0; i < pkt.Length; i++) + if (!HexConverter.TryDecodeFromUtf16(attributeValue.AsSpan(), pkt, out int _)) { - char hi = attributeValue[srcIndex++]; - char lo = attributeValue[srcIndex++]; - - if (!TryParseHexNybble(hi, out byte parsedHi) || !TryParseHexNybble(lo, out byte parsedLo)) - { - return false; - } - - pkt[i] = (byte)((parsedHi << 4) | parsedLo); + return false; } + result = pkt; return true; } @@ -347,18 +339,6 @@ _ when attributeValue.Equals("msil", StringComparison.OrdinalIgnoreCase) => Proc return result != ProcessorArchitecture.None; } - private static bool TryParseHexNybble(char c, out byte parsed) - { - int value = HexConverter.FromChar(c); - if (value == 0xFF) - { - parsed = 0; - return false; - } - parsed = (byte)value; - return true; - } - private static bool IsWhiteSpace(char ch) => ch is '\n' or '\r' or ' ' or '\t'; @@ -426,7 +406,7 @@ private bool TryGetNextToken(out string tokenString, out Token token) using ValueStringBuilder sb = new ValueStringBuilder(stackalloc char[64]); char quoteChar = '\0'; - if (c == '\'' || c == '\"') + if (c is '\'' or '\"') { quoteChar = c; if (!TryGetNextChar(out c)) @@ -453,19 +433,19 @@ private bool TryGetNextToken(out string tokenString, out Token token) if (quoteChar != 0 && c == quoteChar) break; // Terminate: Found closing quote of quoted string. - if (quoteChar == 0 && (c == ',' || c == '=')) + if (quoteChar == 0 && (c is ',' or '=')) { _index--; break; // Terminate: Found start of a new ',' or '=' token. } - if (quoteChar == 0 && (c == '\'' || c == '\"')) + if (quoteChar == 0 && (c is '\'' or '\"')) { token = default; return false; } - if (c == '\\') + if (c is '\\') { if (!TryGetNextChar(out c)) { @@ -493,7 +473,7 @@ private bool TryGetNextToken(out string tokenString, out Token token) break; default: token = default; - return false; //unreachable + return false; } } else diff --git a/src/libraries/Common/src/System/Reflection/Metadata/AssemblyNameInfo.cs b/src/libraries/Common/src/System/Reflection/Metadata/AssemblyNameInfo.cs index 3e571eef99f03..2e03a78e221ce 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/AssemblyNameInfo.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/AssemblyNameInfo.cs @@ -9,6 +9,7 @@ #if !SYSTEM_PRIVATE_CORELIB using System.Collections.Immutable; +using System.Linq; #endif namespace System.Reflection.Metadata @@ -115,7 +116,7 @@ public string FullName #elif NET8_0_OR_GREATER !PublicKeyOrToken.IsDefault ? Runtime.InteropServices.ImmutableCollectionsMarshal.AsArray(PublicKeyOrToken) : null; #else - ToArray(PublicKeyOrToken); + !PublicKeyOrToken.IsDefault ? PublicKeyOrToken.ToArray() : null; #endif _fullName = AssemblyNameFormatter.ComputeDisplayName(Name, Version, CultureName, publicKeyToken, Flags, ExtractAssemblyContentType(_flags)); } @@ -154,15 +155,17 @@ public AssemblyName ToAssemblyName() #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(ToArray(PublicKeyOrToken)); + assemblyName.SetPublicKey(PublicKeyOrToken.ToArray()); } else { - assemblyName.SetPublicKeyToken(ToArray(PublicKeyOrToken)); + assemblyName.SetPublicKeyToken(PublicKeyOrToken.ToArray()); } +#pragma warning restore RS0030 } #endif @@ -194,7 +197,7 @@ public static bool TryParse(ReadOnlySpan assemblyName, #if SYSTEM_REFLECTION_METADATA || SYSTEM_PRIVATE_CORELIB // required by some tools that include this file but don't include the attribute [NotNullWhen(true)] #endif - out AssemblyNameInfo? result) + out AssemblyNameInfo? result) { AssemblyNameParser.AssemblyNameParts parts = default; if (AssemblyNameParser.TryParse(assemblyName, ref parts)) @@ -215,24 +218,5 @@ internal static AssemblyContentType ExtractAssemblyContentType(AssemblyNameFlags internal static ProcessorArchitecture ExtractProcessorArchitecture(AssemblyNameFlags flags) => (ProcessorArchitecture)((((int)flags) >> 4) & 0x7); - -#if !SYSTEM_PRIVATE_CORELIB - private static byte[]? ToArray(ImmutableArray input) - { - // not using System.Linq.ImmutableArrayExtensions.ToArray as TypeSystem does not allow System.Linq - if (input.IsDefault) - { - return null; - } - else if (input.IsEmpty) - { - return Array.Empty(); - } - - byte[] result = new byte[input.Length]; - input.CopyTo(result, 0); - return result; - } -#endif } } diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs index f7205e1076315..b263a893dd130 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; using System.Text; #if !SYSTEM_PRIVATE_CORELIB @@ -36,7 +37,7 @@ sealed class TypeName private readonly TypeName? _elementOrGenericType; private readonly TypeName? _declaringType; #if SYSTEM_PRIVATE_CORELIB - private readonly IReadOnlyList _genericArguments; + private readonly List? _genericArguments; #else private readonly ImmutableArray _genericArguments; #endif @@ -62,7 +63,7 @@ internal TypeName(string? fullName, _nestedNameLength = nestedNameLength; #if SYSTEM_PRIVATE_CORELIB - _genericArguments = genericTypeArguments is not null ? genericTypeArguments : Array.Empty(); + _genericArguments = genericTypeArguments; #else _genericArguments = genericTypeArguments is null ? ImmutableArray.Empty @@ -119,13 +120,18 @@ public string FullName { if (IsConstructedGenericType) { - _fullName = TypeNameParserHelpers.GetGenericTypeFullName(GetGenericTypeDefinition().FullName.AsSpan(), _genericArguments); + _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, builder); + _fullName = TypeNameParserHelpers.GetRankOrModifierStringRepresentation(_rankOrModifier, ref builder); } else { @@ -157,7 +163,7 @@ public string FullName /// public bool IsConstructedGenericType => #if SYSTEM_PRIVATE_CORELIB - _genericArguments.Count > 0; + _genericArguments is not null; #else _genericArguments.Length > 0; #endif @@ -223,7 +229,7 @@ public string Name { ValueStringBuilder builder = new(stackalloc char[64]); builder.Append(GetElementType().Name); - _name = TypeNameParserHelpers.GetRankOrModifierStringRepresentation(_rankOrModifier, builder); + _name = TypeNameParserHelpers.GetRankOrModifierStringRepresentation(_rankOrModifier, ref builder); } else if (_nestedNameLength > 0 && _fullName is not null) { @@ -284,7 +290,7 @@ public int GetNodeCount() result += GetElementType().GetNodeCount(); } - foreach (TypeName genericArgument in _genericArguments) + foreach (TypeName genericArgument in GetGenericArguments()) { result += genericArgument.GetNodeCount(); } @@ -367,10 +373,9 @@ public int GetArrayRank() /// public #if SYSTEM_PRIVATE_CORELIB - IReadOnlyList + IReadOnlyList GetGenericArguments() => _genericArguments is null ? Array.Empty() : _genericArguments; #else - ImmutableArray + ImmutableArray GetGenericArguments() => _genericArguments; #endif - GetGenericArguments() => _genericArguments; } } diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs index 49128313f175a..93a632e13b7f3 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs @@ -166,11 +166,10 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse { #if SYSTEM_PRIVATE_CORELIB // backward compat: throw FileLoadException for non-empty invalid strings - if (!_throwOnError && _inputString.TrimStart().StartsWith(",")) + if (_throwOnError || !_inputString.TrimStart().StartsWith(",")) { - return null; + throw new IO.FileLoadException(SR.InvalidAssemblyName, _inputString.ToString()); } - throw new IO.FileLoadException(SR.InvalidAssemblyName, _inputString.ToString()); #else return null; #endif diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs index 4f3b57676e3a3..5cf62673d525a 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs @@ -17,11 +17,13 @@ internal static class TypeNameParserHelpers internal const sbyte ByRef = -3; private const char EscapeCharacter = '\\'; #if NET8_0_OR_GREATER - private static readonly SearchValues _endOfFullTypeNameDelimitersSearchValues = SearchValues.Create("[]&*,+\\"); + private static readonly SearchValues s_endOfFullTypeNameDelimitersSearchValues = SearchValues.Create("[]&*,+\\"); #endif - internal static string GetGenericTypeFullName(ReadOnlySpan fullTypeName, IReadOnlyList genericArgs) + internal static string GetGenericTypeFullName(ReadOnlySpan fullTypeName, ReadOnlySpan genericArgs) { + Debug.Assert(genericArgs.Length > 0); + ValueStringBuilder result = new(stackalloc char[128]); result.Append(fullTypeName); @@ -56,7 +58,7 @@ internal static int GetFullTypeNameLength(ReadOnlySpan input, out bool isN // 'n' is adversary-controlled. To avoid DoS issues here, we'll loop manually. #if NET8_0_OR_GREATER - int offset = input.IndexOfAny(_endOfFullTypeNameDelimitersSearchValues); + 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 @@ -159,7 +161,7 @@ static int GetUnescapedOffset(ReadOnlySpan input, int startIndex) } } - internal static string GetRankOrModifierStringRepresentation(int rankOrModifier, ValueStringBuilder builder) + internal static string GetRankOrModifierStringRepresentation(int rankOrModifier, ref ValueStringBuilder builder) { if (rankOrModifier == ByRef) { diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs index d9b071e6e929c..b9d23a2be1524 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs @@ -2,6 +2,7 @@ // 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; @@ -67,7 +68,7 @@ public void GetAssemblyNameCandidateReturnsExpectedValue(string input, string ex public void AppendRankOrModifierStringRepresentationAppendsExpectedString(int input, string expected) { ValueStringBuilder builder = new ValueStringBuilder(initialCapacity: 10); - Assert.Equal(expected, TypeNameParserHelpers.GetRankOrModifierStringRepresentation(input, builder)); + Assert.Equal(expected, TypeNameParserHelpers.GetRankOrModifierStringRepresentation(input, ref builder)); } [Theory] @@ -80,7 +81,7 @@ public void AppendRankOrModifierStringRepresentationAppendsExpectedString(int in public void GetGenericTypeFullNameReturnsSameStringAsTypeAPI(Type genericType) { TypeName openGenericTypeName = TypeName.Parse(genericType.GetGenericTypeDefinition().FullName.AsSpan()); - TypeName[] genericArgNames = genericType.GetGenericArguments().Select(arg => TypeName.Parse(arg.AssemblyQualifiedName.AsSpan())).ToArray(); + ReadOnlySpan genericArgNames = genericType.GetGenericArguments().Select(arg => TypeName.Parse(arg.AssemblyQualifiedName.AsSpan())).ToArray(); Assert.Equal(genericType.FullName, TypeNameParserHelpers.GetGenericTypeFullName(openGenericTypeName.FullName.AsSpan(), genericArgNames)); } From 7979e271261ee197fc5f203d2a3e0ce2fe46907d Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 24 Apr 2024 13:04:51 +0200 Subject: [PATCH 47/48] address code review feedback: - don't throw TypeLoadException in the parser, let loader do that - add Debug.Assert(typeName.IsSimple) for clarity - always include invalid character index in the error message - remove resources added to CoreLib (they are never going to be used) - use [DoesNotReturn] and implement proper throw helper pattern - remove outdated comment about name validation - AssemblyNameInfo.Flags should return the exact value provided in ctor - don't allocate FullName until it's really needed - refactor Make and Resovle methods into one method - fix the test: specify namespace to make sure the type loader loads System.Int32 and later throws TypeLoadException for byref to byref --- .../Reflection/TypeNameParser.CoreCLR.cs | 14 +-- .../Reflection/TypeNameParser.NativeAot.cs | 6 +- .../CustomAttributeTypeNameParser.cs | 10 +- .../ILVerification/ILVerification.projitems | 3 + .../Dataflow/TypeNameParser.Dataflow.cs | 8 +- .../System/Reflection/AssemblyNameParser.cs | 8 +- .../Reflection/Metadata/AssemblyNameInfo.cs | 11 +-- .../System/Reflection/Metadata/TypeName.cs | 55 +++++++---- .../Reflection/Metadata/TypeNameParser.cs | 34 ++++--- .../Metadata/TypeNameParserHelpers.cs | 34 ++++--- .../Reflection/TypeNameParser.Helpers.cs | 99 +++++++++---------- .../src/Resources/Strings.resx | 9 -- .../tests/Metadata/TypeNameTests.cs | 6 +- .../System/Reflection/TypeNameParser.Mono.cs | 4 +- 14 files changed, 155 insertions(+), 146 deletions(-) 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 24462d72ce53e..35fa0b5718e38 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 @@ -185,13 +185,13 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName, [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075:UnrecognizedReflectionPattern", Justification = "TypeNameParser.GetType is marked as RequiresUnreferencedCode.")] private Type? GetType(string escapedTypeName, // For nested types, it's Name. For other types it's FullName - ReadOnlySpan nestedTypeNames, Metadata.AssemblyNameInfo? assemblyNameIfAny, string fullEscapedName) + ReadOnlySpan nestedTypeNames, Metadata.TypeName parsedName) { Assembly? assembly; - if (assemblyNameIfAny is not null) + if (parsedName.AssemblyName is not null) { - assembly = ResolveAssembly(assemblyNameIfAny.ToAssemblyName()); + assembly = ResolveAssembly(parsedName.AssemblyName.ToAssemblyName()); if (assembly is null) return null; } @@ -230,7 +230,7 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName, } return null; } - return GetTypeFromDefaultAssemblies(UnescapeTypeName(escapedTypeName), nestedTypeNames, fullEscapedName); + return GetTypeFromDefaultAssemblies(UnescapeTypeName(escapedTypeName), nestedTypeNames, parsedName); } if (assembly is RuntimeAssembly runtimeAssembly) @@ -277,7 +277,7 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName, return type; } - private Type? GetTypeFromDefaultAssemblies(string typeName, ReadOnlySpan nestedTypeNames, string fullEscapedName) + private Type? GetTypeFromDefaultAssemblies(string typeName, ReadOnlySpan nestedTypeNames, Metadata.TypeName parsedName) { RuntimeAssembly? requestingAssembly = (RuntimeAssembly?)_requestingAssembly; if (requestingAssembly is not null) @@ -295,7 +295,7 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName, return type; } - RuntimeAssembly? resolvedAssembly = AssemblyLoadContext.OnTypeResolve(requestingAssembly, fullEscapedName); + RuntimeAssembly? resolvedAssembly = AssemblyLoadContext.OnTypeResolve(requestingAssembly, parsedName.FullName); if (resolvedAssembly is not null) { Type? type = resolvedAssembly.GetTypeCore(typeName, nestedTypeNames, throwOnError: false, ignoreCase: _ignoreCase); @@ -304,7 +304,7 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName, } if (_throwOnError) - throw new TypeLoadException(SR.Format(SR.TypeLoad_ResolveTypeFromAssembly, fullEscapedName, (requestingAssembly ?? coreLib).FullName)); + throw new TypeLoadException(SR.Format(SR.TypeLoad_ResolveTypeFromAssembly, parsedName.FullName, (requestingAssembly ?? coreLib).FullName)); return null; } 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 dcebd79718276..2149003ddcfb4 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 @@ -117,13 +117,13 @@ internal partial struct TypeNameParser Justification = "GetType APIs are marked as RequiresUnreferencedCode.")] [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075:UnrecognizedReflectionPattern", Justification = "GetType APIs are marked as RequiresUnreferencedCode.")] - private Type? GetType(string escapedTypeName, ReadOnlySpan nestedTypeNames, Metadata.AssemblyNameInfo? assemblyNameIfAny, string _) + 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; } diff --git a/src/coreclr/tools/Common/TypeSystem/Common/Utilities/CustomAttributeTypeNameParser.cs b/src/coreclr/tools/Common/TypeSystem/Common/Utilities/CustomAttributeTypeNameParser.cs index e4be278066efe..72779375f4697 100644 --- a/src/coreclr/tools/Common/TypeSystem/Common/Utilities/CustomAttributeTypeNameParser.cs +++ b/src/coreclr/tools/Common/TypeSystem/Common/Utilities/CustomAttributeTypeNameParser.cs @@ -68,10 +68,10 @@ public Type MakeGenericType(Type[] typeArguments) } } - private Type GetType(string typeName, ReadOnlySpan nestedTypeNames, AssemblyNameInfo assemblyNameIfAny, string fullEscapedName) + private Type GetType(string typeName, ReadOnlySpan nestedTypeNames, TypeName parsedName) { - ModuleDesc module = (assemblyNameIfAny == null) ? _module : - _module.Context.ResolveAssembly(assemblyNameIfAny.ToAssemblyName(), throwIfNotFound: _throwIfNotFound); + ModuleDesc module = (parsedName.AssemblyName == null) ? _module : + _module.Context.ResolveAssembly(parsedName.AssemblyName.ToAssemblyName(), throwIfNotFound: _throwIfNotFound); if (_canonResolver != null && nestedTypeNames.IsEmpty) { @@ -88,7 +88,7 @@ private Type GetType(string typeName, ReadOnlySpan nestedTypeNames, Asse } // 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) @@ -96,7 +96,7 @@ private Type GetType(string typeName, ReadOnlySpan nestedTypeNames, Asse } if (_throwIfNotFound) - ThrowHelper.ThrowTypeLoadException(fullEscapedName, module); + ThrowHelper.ThrowTypeLoadException(parsedName.FullName, module); return null; } diff --git a/src/coreclr/tools/ILVerification/ILVerification.projitems b/src/coreclr/tools/ILVerification/ILVerification.projitems index 75b7e614adcf5..3b2c30ddef1f1 100644 --- a/src/coreclr/tools/ILVerification/ILVerification.projitems +++ b/src/coreclr/tools/ILVerification/ILVerification.projitems @@ -93,6 +93,9 @@ System\Diagnostics\CodeAnalysis\UnconditionalSuppressMessageAttribute.cs + + System\Diagnostics\CodeAnalysis\NullableAttributes.cs + Utilities\CustomAttributeTypeNameParser.Helpers 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 7a1c70574568b..5b0cfdb12b444 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 @@ -55,13 +55,13 @@ public Type MakeGenericType(Type[] typeArguments) } } - private Type GetType(string typeName, ReadOnlySpan nestedTypeNames, AssemblyNameInfo assemblyNameIfAny, string _) + private Type GetType(string typeName, ReadOnlySpan nestedTypeNames, TypeName parsedName) { ModuleDesc module; - if (assemblyNameIfAny != null) + if (parsedName.AssemblyName != null) { - module = _context.ResolveAssembly(assemblyNameIfAny.ToAssemblyName(), throwIfNotFound: false); + module = _context.ResolveAssembly(parsedName.AssemblyName.ToAssemblyName(), throwIfNotFound: false); } else { @@ -79,7 +79,7 @@ private Type GetType(string typeName, ReadOnlySpan nestedTypeNames, Asse } // 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) diff --git a/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs b/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs index 19ac86bd15f71..bfd91d781a4ec 100644 --- a/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/AssemblyNameParser.cs @@ -163,7 +163,7 @@ private bool TryParse(ref AssemblyNameParts result) { return false; } - if (!TryParsePKT(attributeValue, isToken: true, ref pkt)) + if (!TryParsePKT(attributeValue, isToken: true, out pkt)) { return false; } @@ -174,7 +174,7 @@ private bool TryParse(ref AssemblyNameParts result) { return false; } - if (!TryParsePKT(attributeValue, isToken: false, ref pkt)) + if (!TryParsePKT(attributeValue, isToken: false, out pkt)) { return false; } @@ -301,7 +301,7 @@ private static bool TryParseCulture(string attributeValue, out string? result) return true; } - private static bool TryParsePKT(string attributeValue, bool isToken, ref byte[]? result) + private static bool TryParsePKT(string attributeValue, bool isToken, out byte[]? result) { if (attributeValue.Equals("null", StringComparison.OrdinalIgnoreCase) || attributeValue == string.Empty) { @@ -311,12 +311,14 @@ private static bool TryParsePKT(string attributeValue, bool isToken, ref byte[]? 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; } diff --git a/src/libraries/Common/src/System/Reflection/Metadata/AssemblyNameInfo.cs b/src/libraries/Common/src/System/Reflection/Metadata/AssemblyNameInfo.cs index 2e03a78e221ce..ee586e3b57a8c 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/AssemblyNameInfo.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/AssemblyNameInfo.cs @@ -88,7 +88,7 @@ internal AssemblyNameInfo(AssemblyNameParser.AssemblyNameParts parts) /// /// Gets the attributes of the assembly. /// - public AssemblyNameFlags Flags => ExtractAssemblyNameFlags(_flags); + public AssemblyNameFlags Flags => _flags; /// /// Gets the public key or the public key token of the assembly. @@ -118,7 +118,8 @@ public string FullName #else !PublicKeyOrToken.IsDefault ? PublicKeyOrToken.ToArray() : null; #endif - _fullName = AssemblyNameFormatter.ComputeDisplayName(Name, Version, CultureName, publicKeyToken, Flags, ExtractAssemblyContentType(_flags)); + _fullName = AssemblyNameFormatter.ComputeDisplayName(Name, Version, CultureName, publicKeyToken, + ExtractAssemblyNameFlags(_flags), ExtractAssemblyContentType(_flags)); } return _fullName; @@ -193,11 +194,7 @@ public static AssemblyNameInfo Parse(ReadOnlySpan assemblyName) /// 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, -#if SYSTEM_REFLECTION_METADATA || SYSTEM_PRIVATE_CORELIB // required by some tools that include this file but don't include the attribute - [NotNullWhen(true)] -#endif - out AssemblyNameInfo? result) + public static bool TryParse(ReadOnlySpan assemblyName, [NotNullWhen(true)] out AssemblyNameInfo? result) { AssemblyNameParser.AssemblyNameParts parts = default; if (AssemblyNameParser.TryParse(assemblyName, ref parts)) diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs index b263a893dd130..2fac2f8ffd74d 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs @@ -94,9 +94,18 @@ public string AssemblyQualifiedName /// 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 => _declaringType is not null - ? _declaringType - : throw TypeNameParserHelpers.InvalidOperation_NotNestedType(); + 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". @@ -306,9 +315,14 @@ public int GetNodeCount() /// /// The current type is not an array, pointer or reference. public TypeName GetElementType() - => IsArray || IsPointer || IsByRef - ? _elementOrGenericType! - : throw TypeNameParserHelpers.InvalidOperation_NoElement(); + { + 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. @@ -318,9 +332,14 @@ public TypeName GetElementType() /// /// The current type is not a generic type. public TypeName GetGenericTypeDefinition() - => IsConstructedGenericType - ? _elementOrGenericType! - : throw TypeNameParserHelpers.InvalidOperation_NotGenericType(); + { + if (!IsConstructedGenericType) + { + TypeNameParserHelpers.ThrowInvalidOperation_NotGenericType(); + } + + return _elementOrGenericType!; + } /// /// Parses a span of characters into a type name. @@ -340,11 +359,7 @@ public static TypeName Parse(ReadOnlySpan typeName, TypeNameParseOptions? /// 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, -#if SYSTEM_REFLECTION_METADATA || SYSTEM_PRIVATE_CORELIB // required by some tools that include this file but don't include the attribute - [NotNullWhen(true)] -#endif - out TypeName? result, TypeNameParseOptions? options = default) + 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; @@ -356,12 +371,14 @@ public static bool TryParse(ReadOnlySpan typeName, /// An integer that contains the number of dimensions in the current type. /// The current type is not an array. public int GetArrayRank() - => _rankOrModifier switch + { + if (!(_rankOrModifier == TypeNameParserHelpers.SZArray || _rankOrModifier > 0)) { - TypeNameParserHelpers.SZArray => 1, - _ when _rankOrModifier > 0 => _rankOrModifier, - _ => throw TypeNameParserHelpers.InvalidOperation_HasToBeArrayClass() - }; + TypeNameParserHelpers.ThrowInvalidOperation_HasToBeArrayClass(); + } + + return _rankOrModifier == TypeNameParserHelpers.SZArray ? 1 : _rankOrModifier; + } /// /// If this represents a constructed generic type, returns an array diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs index 93a632e13b7f3..7c75f3ee844d6 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParser.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Diagnostics; -using System.Text; #if !SYSTEM_PRIVATE_CORELIB using System.Collections.Immutable; @@ -35,30 +34,35 @@ private TypeNameParser(ReadOnlySpan name, bool throwOnError, TypeNameParse ReadOnlySpan trimmedName = typeName.TrimStart(); // whitespaces at beginning are always OK if (trimmedName.IsEmpty) { - // whitespace input needs to report the error index as 0 - return throwOnError ? throw ArgumentException_InvalidTypeName(errorIndex: 0) : null; + 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 not null && parser._inputString.IsEmpty) // unconsumed input == error + if (parsedName is null || !parser._inputString.IsEmpty) // unconsumed input == error { - return parsedName; - } - else if (!throwOnError) - { - return null; - } + if (throwOnError) + { + if (recursiveDepth >= parser._parseOptions.MaxNodes) + { + ThrowInvalidOperation_MaxNodesExceeded(parser._parseOptions.MaxNodes); + } - if (recursiveDepth >= parser._parseOptions.MaxNodes) - { - throw InvalidOperation_MaxNodesExceeded(parser._parseOptions.MaxNodes); + int errorIndex = typeName.Length - parser._inputString.Length; + ThrowArgumentException_InvalidTypeName(errorIndex); + } + + return null; } - int errorIndex = typeName.Length - parser._inputString.Length; - throw ArgumentException_InvalidTypeName(errorIndex); + return parsedName; } // this method should return null instead of throwing, so the caller can get errorIndex and include it in error msg diff --git a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs index 5cf62673d525a..3783f56c73d4b 100644 --- a/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs +++ b/src/libraries/Common/src/System/Reflection/Metadata/TypeNameParserHelpers.cs @@ -4,6 +4,7 @@ using System.Buffers; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Text; #nullable enable @@ -249,7 +250,6 @@ internal static bool TryGetTypeNameInfo(ref ReadOnlySpan input, ref List s return false; } - internal static InvalidOperationException InvalidOperation_MaxNodesExceeded(int limit) => -#if SYSTEM_REFLECTION_METADATA || SYSTEM_PRIVATE_CORELIB + [DoesNotReturn] + internal static void ThrowInvalidOperation_MaxNodesExceeded(int limit) => throw +#if SYSTEM_REFLECTION_METADATA new InvalidOperationException(SR.Format(SR.InvalidOperation_MaxNodesExceeded, limit)); -#else // tools that reference this file as a link +#else // corelib and tools that reference this file as a link new InvalidOperationException(); #endif - internal static ArgumentException ArgumentException_InvalidTypeName(int errorIndex) => + [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); + new ArgumentException(SR.Argument_InvalidTypeName, $"typeName@{errorIndex}"); #else // tools that reference this file as a link new ArgumentException(); #endif - internal static InvalidOperationException InvalidOperation_NotGenericType() => + [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 - internal static InvalidOperationException InvalidOperation_NotNestedType() => -#if SYSTEM_REFLECTION_METADATA || SYSTEM_PRIVATE_CORELIB + [DoesNotReturn] + internal static void ThrowInvalidOperation_NotNestedType() => throw +#if SYSTEM_REFLECTION_METADATA new InvalidOperationException(SR.InvalidOperation_NotNestedType); -#else // tools that reference this file as a link +#else // corelib and tools that reference this file as a link new InvalidOperationException(); #endif - internal static InvalidOperationException InvalidOperation_NoElement() => -#if SYSTEM_REFLECTION_METADATA || SYSTEM_PRIVATE_CORELIB + [DoesNotReturn] + internal static void ThrowInvalidOperation_NoElement() => throw +#if SYSTEM_REFLECTION_METADATA new InvalidOperationException(SR.InvalidOperation_NoElement); -#else // tools that reference this file as a link +#else // corelib and tools that reference this file as a link new InvalidOperationException(); #endif - internal static InvalidOperationException InvalidOperation_HasToBeArrayClass() => + [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 diff --git a/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs index 8a98d381a8360..3f547d2b258e8 100644 --- a/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs +++ b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs @@ -3,7 +3,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; +using System.Reflection.Metadata; using System.Text; #nullable enable @@ -74,11 +74,13 @@ private static (string typeNamespace, string name) SplitFullTypeName(string type return (typeNamespace, name); } - private Type? Resolve(Metadata.TypeName typeName) + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050:RequiresDynamicCode", + Justification = "Used to implement resolving types from strings.")] + private Type? Resolve(TypeName typeName) { if (typeName.IsNested) { - Metadata.TypeName? current = typeName; + TypeName? current = typeName; int nestingDepth = 0; while (current is not null && current.IsNested) { @@ -104,40 +106,54 @@ private static (string typeNamespace, string name) SplitFullTypeName(string type #else // the tools require unescaped names string nonNestedParentName = UnescapeTypeName(current!.FullName); #endif - - Type? type = GetType(nonNestedParentName, nestedTypeNames, typeName.AssemblyName, typeName.FullName); - return Make(type, typeName); + Type? type = GetType(nonNestedParentName, nestedTypeNames, typeName); + return type is null || !typeName.IsConstructedGenericType ? type : MakeGenericType(type, typeName); } else if (typeName.IsConstructedGenericType) { - return Make(Resolve(typeName.GetGenericTypeDefinition()), typeName); + Type? type = Resolve(typeName.GetGenericTypeDefinition()); + return type is null ? null : MakeGenericType(type, typeName); } else if (typeName.IsArray || typeName.IsPointer || typeName.IsByRef) { - Metadata.TypeName elementType = typeName.GetElementType(); - - if (elementType.IsByRef || (typeName.IsVariableBoundArrayType && typeName.GetArrayRank() > 32)) + Type? type = Resolve(typeName.GetElementType()); + if (type is null) { -#if SYSTEM_PRIVATE_CORELIB - throw new TypeLoadException(); // CLR throws TypeLoadException for invalid decorators -#else return null; -#endif } - return Make(Resolve(elementType), typeName); + 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.AssemblyName, typeName.FullName); + nestedTypeNames: ReadOnlySpan.Empty, typeName); - return Make(type, typeName); + return type; } } @@ -145,51 +161,26 @@ private static (string typeNamespace, string name) SplitFullTypeName(string type Justification = "Used to implement resolving types from strings.")] [UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050:RequiresDynamicCode", Justification = "Used to implement resolving types from strings.")] - private Type? Make(Type? type, Metadata.TypeName typeName) + private Type? MakeGenericType(Type type, TypeName typeName) { - if (type is null || typeName.IsSimple) - { - return type; - } - else if (typeName.IsConstructedGenericType) - { - var genericArgs = typeName.GetGenericArguments(); + var genericArgs = typeName.GetGenericArguments(); #if SYSTEM_PRIVATE_CORELIB - int size = genericArgs.Count; + int size = genericArgs.Count; #else - int size = genericArgs.Length; + int size = genericArgs.Length; #endif - Type[] genericTypes = new Type[size]; - for (int i = 0; i < size; i++) + Type[] genericTypes = new Type[size]; + for (int i = 0; i < size; i++) + { + Type? genericArg = Resolve(genericArgs[i]); + if (genericArg is null) { - Type? genericArg = Resolve(genericArgs[i]); - if (genericArg is null) - { - return null; - } - genericTypes[i] = genericArg; + return null; } - - return type.MakeGenericType(genericTypes); + genericTypes[i] = genericArg; } - else 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()); - } + return type.MakeGenericType(genericTypes); } } } diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index ec34570d0d178..c4504f140527a 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -4319,13 +4319,4 @@ Emitting debug info is not supported for this member. - - Maximum node count of {0} exceeded. - - - This operation is only valid on nested types. - - - This operation is only valid on arrays, pointers and references. - diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs index 8658dccd4486e..53f8e43821bc6 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameTests.cs @@ -73,8 +73,8 @@ public void InvalidTypeNamesAreNotAllowed(string input) } [Theory] - [InlineData("int&&")] // by-ref to by-ref is currently not supported by CLR - [InlineData("int[,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,]")] // more than max array rank (32) + [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 _)); @@ -83,8 +83,6 @@ public void ParserIsNotEnforcingRuntimeSpecificRules(string input) { #if NETCOREAPP Assert.Throws(() => Type.GetType(input)); -#elif NETFRAMEWORK - Assert.Null(Type.GetType(input)); #endif } } 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 da5a7c6f2e80c..22a7361d9814e 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 @@ -95,9 +95,9 @@ internal unsafe ref partial struct TypeNameParser Justification = "TypeNameParser.GetType is marked as RequiresUnreferencedCode.")] [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "TypeNameParser.GetType is marked as RequiresUnreferencedCode.")] - private Type? GetType(string escapedTypeName, ReadOnlySpan nestedTypeNames, Metadata.AssemblyNameInfo? assemblyNameIfAny, string _) + private Type? GetType(string escapedTypeName, ReadOnlySpan nestedTypeNames, Metadata.TypeName parsedName) { - Assembly? assembly = (assemblyNameIfAny is not null) ? ResolveAssembly(assemblyNameIfAny.ToAssemblyName()) : 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 Type? type; From 22de7617bab80e1cdf10eca06c4915c8258b4c77 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 24 Apr 2024 17:05:42 +0200 Subject: [PATCH 48/48] Apply suggestions from code review Co-authored-by: Jan Kotas --- .../Reflection/TypeNameParser.Helpers.cs | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs index 3f547d2b258e8..bcb2125d3f4ea 100644 --- a/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs +++ b/src/libraries/Common/src/System/Reflection/TypeNameParser.Helpers.cs @@ -26,28 +26,34 @@ private static string UnescapeTypeName(string name) return name; } - // 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.Append(name.AsSpan(0, indexOfEscapeCharacter)); + return Unescape(name, indexOfEscapeCharacter); - for (int i = indexOfEscapeCharacter; i < name.Length;) + static string Unescape(string name, int indexOfEscapeCharacter) { - char c = name[i++]; + // 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)); - if (c != EscapeCharacter) + for (int i = indexOfEscapeCharacter; i < name.Length;) { - sb.Append(c); + 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++; + } } - 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(); + return sb.ToString(); + } } #endif