Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.
/ corefx Public archive

Commit

Permalink
Handle binary formatted payloads with ResX mangled generic type names (
Browse files Browse the repository at this point in the history
…#42102) (#42209)

* Handle binary formatted payloads with ResX mangled generic type names

ResXSerializationBinder on desktop corrupted type names for generic parameters
in the binary formatted payload.  It would also undo this in the reader,
but we don't benefit from that in .NETCore since we don't deserialize
during build: we just copy the preserialized payload into the resources.

To handle this, we use a serialization binder to un-mangle the type names
in a way similar to ResXSerializationBinder.  We do it slightly differently
so that we only do it when needed, since relying on the binder to resolve
the type bypasses the type cache in BinaryFormatter.

* Respond to feedback.

release/3.1 - regenerate TestData.resources
  • Loading branch information
ericstj authored Oct 30, 2019
1 parent 9d99231 commit 11b1ec3
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#nullable enable
using System.ComponentModel;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;

namespace System.Resources.Extensions
Expand Down Expand Up @@ -38,12 +39,71 @@ private object ReadBinaryFormattedObject()
{
if (_formatter == null)
{
_formatter = new BinaryFormatter();
_formatter = new BinaryFormatter()
{
Binder = new UndoTruncatedTypeNameSerializationBinder()
};
}

return _formatter.Deserialize(_store.BaseStream);
}


internal class UndoTruncatedTypeNameSerializationBinder : SerializationBinder
{
public override Type BindToType(string assemblyName, string typeName)
{
Type type = null;

// determine if we have a mangled generic type name
if (typeName != null && assemblyName != null && !AreBracketsBalanced(typeName))
{
// undo the mangling that may have happened with .NETFramework's
// incorrect ResXSerialization binder.
typeName = typeName + ", " + assemblyName;

type = Type.GetType(typeName, throwOnError: false, ignoreCase:false);
}

// if type is null we'll fall back to the default type binder which is preferable
// since it is backed by a cache
return type;
}

private static bool AreBracketsBalanced(string typeName)
{
// make sure brackets are balanced
int firstBracket = typeName.IndexOf('[');

if (firstBracket == -1)
{
return true;
}

int brackets = 1;
for (int i = firstBracket + 1; i < typeName.Length; i++)
{
if (typeName[i] == '[')
{
brackets++;
}
else if (typeName[i] == ']')
{
brackets--;

if (brackets < 0)
{
// unbalanced, closing bracket without opening
break;
}
}
}

return brackets == 0;
}

}

private object DeserializeObject(int typeIndex)
{
Type type = FindType(typeIndex);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,12 +155,14 @@ public void AddBinaryFormattedResource(string name, byte[] value, string? typeNa
// and reserializing when writing the resources. We don't want to do that so instead
// we just omit the type.
typeName = UnknownObjectTypeName;

// ResourceReader will validate the type so we must use the new reader.
_requiresDeserializingResourceReader = true;
}

AddResourceData(name, typeName, new ResourceDataRecord(SerializationFormat.BinaryFormatter, value));

// Even though ResourceReader can handle BinaryFormatted resources, the resource may contain
// type names that were mangled by the ResXWriter's SerializationBinder, which we need to fix

_requiresDeserializingResourceReader = true;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,23 +267,7 @@ public static void PrimitiveResourcesAsStrings()
public static void BinaryFormattedResources()
{
var values = TestData.BinaryFormatted;
byte[] writerBuffer, binaryWriterBuffer;
using (MemoryStream ms = new MemoryStream())
using (ResourceWriter writer = new ResourceWriter(ms))
{
BinaryFormatter binaryFormatter = new BinaryFormatter();

foreach (var pair in values)
{
using (MemoryStream memoryStream = new MemoryStream())
{
binaryFormatter.Serialize(memoryStream, pair.Value);
writer.AddResourceData(pair.Key, TestData.GetSerializationTypeName(pair.Value.GetType()), memoryStream.ToArray());
}
}
writer.Generate();
writerBuffer = ms.ToArray();
}
byte[] binaryWriterBuffer;

using (MemoryStream ms = new MemoryStream())
using (PreserializedResourceWriter writer = new PreserializedResourceWriter(ms))
Expand All @@ -302,24 +286,8 @@ public static void BinaryFormattedResources()
binaryWriterBuffer = ms.ToArray();
}

// PreserializedResourceWriter should write ResourceWriter/ResourceReader format
Assert.Equal(writerBuffer, binaryWriterBuffer);

using (MemoryStream ms = new MemoryStream(writerBuffer, false))
using (ResourceReader reader = new ResourceReader(ms))
{
typeof(ResourceReader).GetField("_permitDeserialization", BindingFlags.Instance | BindingFlags.NonPublic)?.SetValue(reader, true);

IDictionaryEnumerator dictEnum = reader.GetEnumerator();

while (dictEnum.MoveNext())
{
ResourceValueEquals(values[(string)dictEnum.Key], dictEnum.Value);
}
}

// DeserializingResourceReader can read ResourceReader format
using (MemoryStream ms = new MemoryStream(writerBuffer, false))
// DeserializingResourceReader can read BinaryFormatted resources with type names.
using (MemoryStream ms = new MemoryStream(binaryWriterBuffer, false))
using (DeserializingResourceReader reader = new DeserializingResourceReader(ms))
{
IDictionaryEnumerator dictEnum = reader.GetEnumerator();
Expand Down Expand Up @@ -510,7 +478,13 @@ public static void EmbeddedResourcesAreUpToDate()
{
TestData.WriteResourcesStream(actualData);
resourcesStream.CopyTo(expectedData);
Assert.Equal(expectedData.ToArray(), actualData.ToArray());

if (!PlatformDetection.IsFullFramework)
{
// Some types rely on SerializationInfo.SetType on .NETCore
// which result in a different binary format
Assert.Equal(expectedData.ToArray(), actualData.ToArray());
}
}
}

Expand Down
129 changes: 118 additions & 11 deletions src/System.Resources.Extensions/tests/TestData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@

using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;

namespace System.Resources.Extensions.Tests
{
Expand Down Expand Up @@ -49,7 +52,15 @@ public static IReadOnlyDictionary<string, object> BinaryFormatted
new Dictionary<string, object>()
{
["enum_bin"] = DayOfWeek.Friday,
["point_bin"] = new Point(4, 8)
["point_bin"] = new Point(4, 8),
["array_int_bin"] = new int[] { 1, 2, 3, 4, 5, 6 },
["list_int_bin"] = new List<int>() { 1, 2, 3, 4, 5, 6 },
["stack_Point_bin"] = new Stack<Point>(new [] { new Point(4, 8), new Point(2, 5) }),
["dict_string_string_bin"] = new Dictionary<string, string>()
{
{ "key1", "value1" },
{ "key2", "value2" }
}
};

public static Dictionary<string, object> BinaryFormattedWithoutDrawingNoType { get; } =
Expand Down Expand Up @@ -142,21 +153,75 @@ public static string GetStringValue(object value)
return converter.ConvertToInvariantString(value);
}

public static string GetSerializationTypeName(Type runtimeType)

// Copied from FormatterServices.cs
internal static string GetClrTypeFullName(Type type)
{
return type.IsArray ?
GetClrTypeFullNameForArray(type) :
GetClrTypeFullNameForNonArrayTypes(type);
}

private static string GetClrTypeFullNameForArray(Type type)
{
int rank = type.GetArrayRank();
Debug.Assert(rank >= 1);
string typeName = GetClrTypeFullName(type.GetElementType());
return rank == 1 ?
typeName + "[]" :
typeName + "[" + new string(',', rank - 1) + "]";
}

private static string GetClrTypeFullNameForNonArrayTypes(Type type)
{
if (!type.IsGenericType)
{
return type.FullName;
}

var builder = new StringBuilder(type.GetGenericTypeDefinition().FullName).Append("[");

foreach (Type genericArgument in type.GetGenericArguments())
{
builder.Append("[").Append(GetClrTypeFullName(genericArgument)).Append(", ");
builder.Append(GetClrAssemblyName(genericArgument)).Append("],");
}

//remove the last comma and close typename for generic with a close bracket
return builder.Remove(builder.Length - 1, 1).Append("]").ToString();
}

private static string GetClrAssemblyName(Type type)
{
object[] typeAttributes = runtimeType.GetCustomAttributes(typeof(TypeForwardedFromAttribute), false);
if (typeAttributes != null && typeAttributes.Length > 0)
if (type == null)
{
throw new ArgumentNullException(nameof(type));
}

// Special case types like arrays
Type attributedType = type;
while (attributedType.HasElementType)
{
TypeForwardedFromAttribute typeForwardedFromAttribute = (TypeForwardedFromAttribute)typeAttributes[0];
return $"{runtimeType.FullName}, {typeForwardedFromAttribute.AssemblyFullName}";
attributedType = attributedType.GetElementType();
}
else if (runtimeType.Assembly == typeof(object).Assembly)

foreach (Attribute first in attributedType.GetCustomAttributes(typeof(TypeForwardedFromAttribute), false))
{
// no attribute and in corelib. Strip the assembly name and hope its in CoreLib on other frameworks
return runtimeType.FullName;
return ((TypeForwardedFromAttribute)first).AssemblyFullName;
}

return runtimeType.AssemblyQualifiedName;
return type.Assembly.FullName;
}

public static string GetSerializationTypeName(Type runtimeType)
{
string typeName = GetClrTypeFullName(runtimeType);
if (runtimeType.Assembly == typeof(object).Assembly)
{
// In corelib. Strip the assembly name and hope its in CoreLib on other frameworks
return typeName;
}
return $"{typeName}, {GetClrAssemblyName(runtimeType)}";
}

public static void WriteResources(string file)
Expand All @@ -178,7 +243,11 @@ public static void WriteResourcesStream(Stream stream)
writer.AddResource(pair.Key, GetStringValue(pair.Value), GetSerializationTypeName(pair.Value.GetType()));
}

var formatter = new BinaryFormatter();
var formatter = new BinaryFormatter()
{
Binder = new TypeNameManglingSerializationBinder()
};

foreach (var pair in BinaryFormattedWithoutDrawing)
{
using (MemoryStream memoryStream = new MemoryStream())
Expand Down Expand Up @@ -217,5 +286,43 @@ public static void WriteResourcesStream(Stream stream)
writer.Generate();
}
}

/// <summary>
/// An approximation of ResXSerializationBinder's behavior (without retargeting)
/// </summary>
internal class TypeNameManglingSerializationBinder : SerializationBinder
{
static readonly string s_coreAssemblyName = typeof(object).Assembly.FullName;
static readonly string s_mscorlibAssemblyName = "mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089";

public override void BindToName(Type serializedType, out string assemblyName, out string typeName)
{
typeName = null;
// Apply type-forwarded from here, so that we mimic what would happen in ResXWriter which runs in VS on desktop
string assemblyQualifiedTypeName = GetSerializationTypeName(serializedType);

// workaround for https://github.com/dotnet/corefx/issues/42092
assemblyQualifiedTypeName = assemblyQualifiedTypeName.Replace(s_coreAssemblyName, s_mscorlibAssemblyName);

int pos = assemblyQualifiedTypeName.IndexOf(',');
if (pos > 0 && pos < assemblyQualifiedTypeName.Length - 1)
{
assemblyName = assemblyQualifiedTypeName.Substring(pos + 1).TrimStart();
string newTypeName = assemblyQualifiedTypeName.Substring(0, pos);
if (!string.Equals(newTypeName, serializedType.FullName, StringComparison.InvariantCulture))
{
typeName = newTypeName;
}
return;
}
base.BindToName(serializedType, out assemblyName, out typeName);
}

public override Type BindToType(string assemblyName, string typeName)
{
// We should never be using this binder during Deserialization
throw new NotSupportedException($"{nameof(TypeNameManglingSerializationBinder)}.{nameof(BindToType)} should not be used during testing.");
}
}
}
}
Binary file modified src/System.Resources.Extensions/tests/TestData.resources
Binary file not shown.

0 comments on commit 11b1ec3

Please sign in to comment.