diff --git a/src/System.Resources.Extensions/src/System/Resources/Extensions/DeserializingResourceReader.cs b/src/System.Resources.Extensions/src/System/Resources/Extensions/DeserializingResourceReader.cs index f6a8210dff9a..057ace872c9d 100644 --- a/src/System.Resources.Extensions/src/System/Resources/Extensions/DeserializingResourceReader.cs +++ b/src/System.Resources.Extensions/src/System/Resources/Extensions/DeserializingResourceReader.cs @@ -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 @@ -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); diff --git a/src/System.Resources.Extensions/src/System/Resources/Extensions/PreserializedResourceWriter.cs b/src/System.Resources.Extensions/src/System/Resources/Extensions/PreserializedResourceWriter.cs index 267043ca845a..23c284ca9adc 100644 --- a/src/System.Resources.Extensions/src/System/Resources/Extensions/PreserializedResourceWriter.cs +++ b/src/System.Resources.Extensions/src/System/Resources/Extensions/PreserializedResourceWriter.cs @@ -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; } /// diff --git a/src/System.Resources.Extensions/tests/BinaryResourceWriterUnitTest.cs b/src/System.Resources.Extensions/tests/BinaryResourceWriterUnitTest.cs index 68e60c911438..0bd7b4c0a9e6 100644 --- a/src/System.Resources.Extensions/tests/BinaryResourceWriterUnitTest.cs +++ b/src/System.Resources.Extensions/tests/BinaryResourceWriterUnitTest.cs @@ -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)) @@ -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(); @@ -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()); + } } } diff --git a/src/System.Resources.Extensions/tests/TestData.cs b/src/System.Resources.Extensions/tests/TestData.cs index 0ddef55b607b..e83d0f31ab13 100644 --- a/src/System.Resources.Extensions/tests/TestData.cs +++ b/src/System.Resources.Extensions/tests/TestData.cs @@ -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 { @@ -49,7 +52,15 @@ public static IReadOnlyDictionary BinaryFormatted new Dictionary() { ["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() { 1, 2, 3, 4, 5, 6 }, + ["stack_Point_bin"] = new Stack(new [] { new Point(4, 8), new Point(2, 5) }), + ["dict_string_string_bin"] = new Dictionary() + { + { "key1", "value1" }, + { "key2", "value2" } + } }; public static Dictionary BinaryFormattedWithoutDrawingNoType { get; } = @@ -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) @@ -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()) @@ -217,5 +286,43 @@ public static void WriteResourcesStream(Stream stream) writer.Generate(); } } + + /// + /// An approximation of ResXSerializationBinder's behavior (without retargeting) + /// + 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."); + } + } } } diff --git a/src/System.Resources.Extensions/tests/TestData.resources b/src/System.Resources.Extensions/tests/TestData.resources index e946372c3748..8ea7850e4ff6 100644 Binary files a/src/System.Resources.Extensions/tests/TestData.resources and b/src/System.Resources.Extensions/tests/TestData.resources differ