Skip to content

Commit

Permalink
Utilize TypeName Parser API in ResXSerializationBinder (#11312)
Browse files Browse the repository at this point in the history
* Utilize TypeName Parser API

* clean up

* address feedback
  • Loading branch information
lonitra authored May 6, 2024
1 parent 33233cd commit 0f077e0
Show file tree
Hide file tree
Showing 2 changed files with 139 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel.Design;
using System.Reflection.Metadata;
using System.Runtime.Serialization;

namespace System.Resources;
Expand Down Expand Up @@ -36,45 +37,34 @@ internal class ResXSerializationBinder : SerializationBinder
string assemblyName,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] string typeName)
{
if (_typeResolver is null)
if (_typeResolver is null || !TypeName.TryParse($"{typeName}, {assemblyName}".AsSpan(), out TypeName? parsed))
{
// cs/deserialization/nullbindtotype
return null; // CodeQL [SM04225] : This class is meant to redirect to .NET Framework type names. If this cannot be done, the default binder should be used.
}

// Try the fully-qualified name first.
typeName = $"{typeName}, {assemblyName}";

Type? type = _typeResolver.GetType(typeName);
Type? type = _typeResolver.GetType(parsed.AssemblyQualifiedName);
if (type is not null)
{
return type;
}

string[] typeParts = typeName.Split(',', StringSplitOptions.TrimEntries);

if (typeParts.Length > 2)
if (parsed.AssemblyName is { } assemblyNameInfo)
{
string partialName = typeParts[0];

// Strip out the version.
for (int i = 1; i < typeParts.Length; ++i)
{
string typePart = typeParts[i];
if (!typePart.StartsWith("Version=", StringComparison.Ordinal)
&& !typePart.StartsWith("version=", StringComparison.Ordinal))
{
partialName = $"{partialName}, {typePart}";
}
}

// Try the name without the version.
type = _typeResolver.GetType(partialName);

// If that didn't work, try the simple name.
type ??= _typeResolver.GetType(typeParts[0]);
string fullyQualifiedNameWithoutVersion =
$"{typeName}, {new AssemblyNameInfo(
assemblyNameInfo.Name,
version: null,
assemblyNameInfo.CultureName,
assemblyNameInfo.Flags,
assemblyNameInfo.PublicKeyOrToken).FullName}";
type = _typeResolver.GetType(fullyQualifiedNameWithoutVersion);
}

// If that didn't work, try the simple name.
type ??= _typeResolver.GetType(parsed.FullName);

// Hand back what we found or null to let the default loader take over.
return type;
}
Expand All @@ -88,23 +78,19 @@ public override void BindToName(Type serializedType, out string? assemblyName, o
{
// Allow the specified type name converter to modify the type name.
string? assemblyQualifiedTypeName = MultitargetUtil.GetAssemblyQualifiedName(serializedType, _typeNameConverter);
if (!string.IsNullOrEmpty(assemblyQualifiedTypeName))
if (!string.IsNullOrEmpty(assemblyQualifiedTypeName)
&& TypeName.TryParse(assemblyQualifiedTypeName.AsSpan(), out TypeName? parsed)
&& parsed.AssemblyName is { } assemblyInfo)
{
// Split the assembly name from the type name.
int pos = assemblyQualifiedTypeName.IndexOf(',');
if (pos > 0 && pos < assemblyQualifiedTypeName.Length - 1)
{
// Set the custom assembly name.
assemblyName = assemblyQualifiedTypeName[(pos + 1)..].TrimStart();
// Set the custom assembly name.
assemblyName = assemblyInfo.FullName;

// Customize the type name only if it changed.
string newTypeName = assemblyQualifiedTypeName[..pos];
typeName = string.Equals(newTypeName, serializedType.FullName, StringComparison.Ordinal)
? null
: newTypeName;
// Customize the type name only if it changed.
typeName = string.Equals(parsed.FullName, serializedType.FullName, StringComparison.Ordinal)
? null
: parsed.FullName;

return;
}
return;
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// 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.ComponentModel.Design;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Reflection.Metadata;

namespace System.Resources.Tests;

public class ResXSerializationBinderTests
{
[Fact]
public void ResXSerializationBinder_BindToType_FullyQualifiedName()
{
TrackGetTypePathTypeResolutionService typeResolutionService = new();
ResXSerializationBinder binder = new(typeResolutionService);
binder.BindToType(typeof(Button).Assembly.FullName!, typeof(Button).FullName!).Should().Be(typeof(Button));
typeResolutionService.FullyQualifiedAssemblyNamePath.Should().BeTrue();
typeResolutionService.FullyQualifiedAssemblyNameNoVersionPath.Should().BeFalse();
}

[Fact]
public void ResXSerializationBinder_BindToType_FullName()
{
TrackGetTypePathTypeResolutionService typeResolutionService = new();
ResXSerializationBinder binder = new(typeResolutionService);
binder.BindToType(typeof(MyClass).Assembly.FullName!, typeof(MyClass).FullName!).Should().Be(typeof(MyClass));
typeResolutionService.FullNamePath.Should().BeTrue();
typeResolutionService.FullyQualifiedAssemblyNameNoVersionPath.Should().BeFalse();
}

[Fact]
public void ResXSerializationBinder_BindToType_FullyQualifiedNameNoVersion()
{
TrackGetTypePathTypeResolutionService typeResolutionService = new();
ResXSerializationBinder binder = new(typeResolutionService);
binder.BindToType(typeof(Form).Assembly.FullName!, typeof(Form).FullName!).Should().Be(typeof(Form));
typeResolutionService.FullyQualifiedAssemblyNameNoVersionPath.Should().BeTrue();
typeResolutionService.FullNamePath.Should().BeFalse();
}

[Fact]
public void ResXSerializationBinder_BindToName_TypeNameConverter_NoNameChange()
{
ResXSerializationBinder binder = new(type => type?.AssemblyQualifiedName ?? string.Empty);
binder.BindToName(typeof(Button), out string? assemblyName, out string? typeName);
assemblyName.Should().Be(typeof(Button).Assembly.FullName);
typeName.Should().BeNull();
}

[Fact]
public void ResXSerializationBinder_BindToName_TypeNameConverter_NameChange()
{
string testAssemblyName = "TestAssembly";
string testTypeName = "TestType";
ResXSerializationBinder binder = new(type => $"{testTypeName}, {testAssemblyName}");
binder.BindToName(typeof(Button), out string? assemblyName, out string? typeName);
assemblyName.Should().Be(testAssemblyName);
typeName.Should().Be(typeName);
}

private class TrackGetTypePathTypeResolutionService : ITypeResolutionService
{
public bool FullNamePath { get; private set; }
public bool FullyQualifiedAssemblyNamePath { get; private set; }
public bool FullyQualifiedAssemblyNameNoVersionPath { get; private set; }

public Assembly? GetAssembly(AssemblyName name) => throw new NotImplementedException();
public Assembly? GetAssembly(AssemblyName name, bool throwOnError) => throw new NotImplementedException();
public string? GetPathOfAssembly(AssemblyName name) => throw new NotImplementedException();
public Type? GetType(string typeName)
{
if (typeName == $"{typeof(Button).FullName}, {typeof(Button).Assembly.FullName}")
{
FullyQualifiedAssemblyNamePath = true;
return typeof(Button);
}
else if (typeName == typeof(MyClass).FullName)
{
FullNamePath = true;
return typeof(MyClass);
}
else
{
TypeName parsed = TypeName.Parse($"{typeof(Form).FullName}, {typeof(Form).Assembly.FullName}");
AssemblyNameInfo? assemblyNameInfo = parsed.AssemblyName;
string formNoVersionFullyQualifiedName = $"{typeof(Form).FullName}, {new AssemblyNameInfo(
assemblyNameInfo!.Name,
version: null,
assemblyNameInfo.CultureName,
assemblyNameInfo.Flags,
assemblyNameInfo.PublicKeyOrToken).FullName}";
if (typeName == formNoVersionFullyQualifiedName)
{
FullyQualifiedAssemblyNameNoVersionPath = true;
return typeof(Form);
}
}

return null;
}

[return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
public Type? GetType([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] string name, bool throwOnError) => throw new NotImplementedException();
[return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
public Type? GetType([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] string name, bool throwOnError, bool ignoreCase) => throw new NotImplementedException();
public void ReferenceAssembly(AssemblyName name) => throw new NotImplementedException();
}

private class MyClass { }
}

0 comments on commit 0f077e0

Please sign in to comment.