diff --git a/System.Resources.NetStandard/ResXDataNode.cs b/System.Resources.NetStandard/ResXDataNode.cs index cfe1239..efc7b75 100644 --- a/System.Resources.NetStandard/ResXDataNode.cs +++ b/System.Resources.NetStandard/ResXDataNode.cs @@ -47,13 +47,13 @@ public sealed class ResXDataNode : ISerializable // No public property to force using constructors for the following reasons: // 1. one of the constructors needs this field (if used) to initialize the object, make it consistent with the other ctrs to avoid errors. // 2. once the object is constructed the delegate should not be changed to avoid getting inconsistent results. - private Func typeNameConverter; + private Func typeNameConverter = WinformsTypeMappers.InterceptWinformsTypes(null); private ResXDataNode() { } - internal ResXDataNode DeepClone() + public ResXDataNode DeepClone() { return new ResXDataNode { @@ -87,7 +87,7 @@ public ResXDataNode(string name, object value, Func typeNameConver throw (new ArgumentException(nameof(name))); } - this.typeNameConverter = typeNameConverter; + this.typeNameConverter = WinformsTypeMappers.InterceptWinformsTypes(typeNameConverter); Type valueType = (value == null) ? typeof(object) : value.GetType(); if (value != null && !valueType.IsSerializable) @@ -116,7 +116,7 @@ public ResXDataNode(string name, ResXFileRef fileRef, Func typeNam this.name = name; this.fileRef = fileRef ?? throw new ArgumentNullException(nameof(fileRef)); - this.typeNameConverter = typeNameConverter; + this.typeNameConverter = WinformsTypeMappers.InterceptWinformsTypes(typeNameConverter); } internal ResXDataNode(DataNodeInfo nodeInfo, string basePath) @@ -864,8 +864,8 @@ internal class AssemblyNamesTypeResolutionService : ITypeResolutionService private Hashtable cachedAssemblies; private Hashtable cachedTypes; - private static readonly string s_dotNetPath = Path.Combine(Environment.GetEnvironmentVariable("ProgramFiles"), "dotnet\\shared"); - private static readonly string s_dotNetPathX86 = Path.Combine(Environment.GetEnvironmentVariable("ProgramFiles(x86)"), "dotnet\\shared"); + private static readonly string s_dotNetPath = Environment.GetEnvironmentVariable("ProgramFiles") != null ? Path.Combine(Environment.GetEnvironmentVariable("ProgramFiles"), "dotnet", "shared") : null; + private static readonly string s_dotNetPathX86 = Environment.GetEnvironmentVariable("ProgramFiles(x86)") != null ? Path.Combine(Environment.GetEnvironmentVariable("ProgramFiles(x86)"), "dotnet", "shared") : null; internal AssemblyNamesTypeResolutionService(AssemblyName[] names) { @@ -956,6 +956,13 @@ public Type GetType(string name, bool throwOnError, bool ignoreCase) return result; } + // Replace the WinForms ResXFileRef with the copy in this library + if (name.StartsWith(ResXConstants.ResxFileRef_TypeNameAndAssembly, StringComparison.Ordinal)) + { + result = typeof(ResXFileRef); + return result; + } + // Missed in cache, try to resolve the type from the reference assemblies. if (name.IndexOf(',') != -1) { @@ -1047,7 +1054,7 @@ public Type GetType(string name, bool throwOnError, bool ignoreCase) /// private bool IsDotNetAssembly(string assemblyPath) { - return assemblyPath != null && (assemblyPath.StartsWith(s_dotNetPath, StringComparison.OrdinalIgnoreCase) || assemblyPath.StartsWith(s_dotNetPathX86, StringComparison.OrdinalIgnoreCase)); + return assemblyPath != null && s_dotNetPath != null && (assemblyPath.StartsWith(s_dotNetPath, StringComparison.OrdinalIgnoreCase) || assemblyPath.StartsWith(s_dotNetPathX86, StringComparison.OrdinalIgnoreCase)); } public void ReferenceAssembly(AssemblyName name) diff --git a/System.Resources.NetStandard/ResXResourceWriter.cs b/System.Resources.NetStandard/ResXResourceWriter.cs index 7458495..aa552f1 100644 --- a/System.Resources.NetStandard/ResXResourceWriter.cs +++ b/System.Resources.NetStandard/ResXResourceWriter.cs @@ -102,7 +102,7 @@ public class ResXResourceWriter : IResourceWriter bool hasBeenSaved; bool initialized; - private readonly Func typeNameConverter; // no public property to be consistent with ResXDataNode class. + private readonly Func typeNameConverter = WinformsTypeMappers.InterceptWinformsTypes(null); // no public property to be consistent with ResXDataNode class. /// /// Base Path for ResXFileRefs. @@ -119,7 +119,7 @@ public ResXResourceWriter(string fileName) public ResXResourceWriter(string fileName, Func typeNameConverter) { this.fileName = fileName; - this.typeNameConverter = typeNameConverter; + this.typeNameConverter = WinformsTypeMappers.InterceptWinformsTypes(typeNameConverter); } /// @@ -132,7 +132,7 @@ public ResXResourceWriter(Stream stream) public ResXResourceWriter(Stream stream, Func typeNameConverter) { this.stream = stream; - this.typeNameConverter = typeNameConverter; + this.typeNameConverter = WinformsTypeMappers.InterceptWinformsTypes(typeNameConverter); } /// @@ -145,7 +145,7 @@ public ResXResourceWriter(TextWriter textWriter) public ResXResourceWriter(TextWriter textWriter, Func typeNameConverter) { this.textWriter = textWriter; - this.typeNameConverter = typeNameConverter; + this.typeNameConverter = WinformsTypeMappers.InterceptWinformsTypes(typeNameConverter); } ~ResXResourceWriter() diff --git a/System.Resources.NetStandard/ResxConstants.cs b/System.Resources.NetStandard/ResxConstants.cs index 240d6cf..6274ed7 100644 --- a/System.Resources.NetStandard/ResxConstants.cs +++ b/System.Resources.NetStandard/ResxConstants.cs @@ -6,5 +6,9 @@ internal class ResXConstants public static string ResHeaderReaderTypeName => ResHeaderReader.Split(',')[0].Trim(); public static string ResHeaderWriterTypeName => ResHeaderWriter.Split(',')[0].Trim(); + + + public const string ResxFileRefTypeInfo = "System.Resources.ResXFileRef, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"; + public const string ResxFileRef_TypeNameAndAssembly = "System.Resources.ResXFileRef, System.Windows.Forms"; } } \ No newline at end of file diff --git a/System.Resources.NetStandard/WinformsTypeMappers.cs b/System.Resources.NetStandard/WinformsTypeMappers.cs new file mode 100644 index 0000000..235a94e --- /dev/null +++ b/System.Resources.NetStandard/WinformsTypeMappers.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace System.Resources.NetStandard +{ + internal static class WinformsTypeMappers + { + public static Func InterceptWinformsTypes(Func typeNameConverter) + { + return (Type type) => + { + if (type.AssemblyQualifiedName == typeof(ResXFileRef).AssemblyQualifiedName) + { + return NetStandard.ResXConstants.ResxFileRefTypeInfo; + } + else + { + if (typeNameConverter != null) return typeNameConverter(type); + else return null; + } + }; + } + + } +} diff --git a/Tests/Example.Designer.cs b/Tests/Example.Designer.cs new file mode 100644 index 0000000..3222955 --- /dev/null +++ b/Tests/Example.Designer.cs @@ -0,0 +1,123 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace System.Resources.Tests { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Example { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Example() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("System.Resources.Tests.Example", typeof(Example).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon Error { + get { + object obj = ResourceManager.GetObject("Error", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap ErrorControl { + get { + object obj = ResourceManager.GetObject("ErrorControl", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized string similar to <?xml version="1.0" encoding="utf-8" ?> + ///<Root> + /// <Element>Text</Element> + ///</Root>. + /// + internal static string FileRef { + get { + return ResourceManager.GetString("FileRef", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to <?xml version="1.0" encoding="utf-8"?> + ///<root> + /// <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> + /// <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> + /// <xsd:element name="root" msdata:IsDataSet="true"> + /// <xsd:complexType> + /// <xsd:choice maxOccurs="unbounded"> + /// <xsd:element name="metadata"> + /// <xsd:complexType> + /// <xsd:sequence> + /// <xsd:element name="va [rest of string was truncated]";. + /// + internal static string ResxWithFileRef { + get { + return ResourceManager.GetString("ResxWithFileRef", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Text. + /// + internal static string text_ansi { + get { + return ResourceManager.GetString("text_ansi", resourceCulture); + } + } + } +} diff --git a/Tests/Example.resx b/Tests/Example.resx new file mode 100644 index 0000000..e74bc50 --- /dev/null +++ b/Tests/Example.resx @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + TestResources\Files\Error.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + TestResources\Files\ErrorControl.bmp;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + TestResources\Files\FileRef.xml;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ResxWithFileRef.resx;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + TestResources\Files\text.ansi.txt;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252 + + \ No newline at end of file diff --git a/Tests/Resources/Error.ico b/Tests/Resources/Error.ico new file mode 100644 index 0000000..5d06b9f Binary files /dev/null and b/Tests/Resources/Error.ico differ diff --git a/Tests/ResxDataNodeTests.cs b/Tests/ResxDataNodeTests.cs index f0fde86..1e0beac 100644 --- a/Tests/ResxDataNodeTests.cs +++ b/Tests/ResxDataNodeTests.cs @@ -2,7 +2,17 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Collections.Generic; +using System.Collections; +using System.Drawing; +using System.Reflection; +using System.Reflection.PortableExecutable; +using System.Resources.Tests; using Xunit; +using System.IO; +using System.Linq; +using System.Text; +using System.ComponentModel.Design; namespace System.Resources.NetStandard.Tests { @@ -18,5 +28,134 @@ public void ResxDataNode_ResXFileRefConstructor() Assert.Equal(nodeName, dataNode.Name); Assert.Same(fileRef, dataNode.FileRef); } + + [Fact] + public void ResxDataNode_CreateForResXFileRef() + { + // Simulate an XML file stored in resources, which uses a ResXFileRef. The actual resx XML looks like: + /* + + + Test.xml;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + */ + + var nodeInfo = new DataNodeInfo + { + Name = "Test", + ReaderPosition = new Point(1, 2), + TypeName = "System.Resources.ResXFileRef, System.Windows.Forms", + ValueData = "TestResources/Files/FileRef.xml;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8", + }; + var dataNode = new ResXDataNode(nodeInfo, null); + var typeResolver = new AssemblyNamesTypeResolutionService(Array.Empty()); + Assert.Equal("Test", dataNode.Name); + Assert.Equal(Example.FileRef, dataNode.GetValue(typeResolver)); + + StringBuilder resxOutput = new StringBuilder(); + using (ResXResourceWriter resx = new ResXResourceWriter(new StringWriter(resxOutput))) + { + resx.AddResource(dataNode); + } + } + + [Fact] + public void ResxDataNode_ResXFileRef_RoundTrip() + { + var referencedFileContent = Example.FileRef; + var nodeInfo = new DataNodeInfo + { + Name = "Test", + ReaderPosition = new Point(1, 2), + TypeName = "System.Resources.ResXFileRef, System.Windows.Forms", + ValueData = "TestResources/Files/FileRef.xml;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8", + }; + var dataNode = new ResXDataNode(nodeInfo, null); + var typeResolver = new AssemblyNamesTypeResolutionService(Array.Empty()); + + StringBuilder resxOutput = new StringBuilder(); + using (ResXResourceWriter writer = new ResXResourceWriter(new StringWriter(resxOutput))) + { + writer.AddResource(dataNode); + } + using (ResXResourceReader reader = new ResXResourceReader(new StringReader(resxOutput.ToString()))) + { + var dictionary = new Dictionary(); + IDictionaryEnumerator dictionaryEnumerator = reader.GetEnumerator(); + while (dictionaryEnumerator.MoveNext()) + { + dictionary.Add(dictionaryEnumerator.Key, dictionaryEnumerator.Value); + } + + Assert.Equal(referencedFileContent, dictionary.GetValueOrDefault(nodeInfo.Name)); + } + } + + private List ReaderToNodes(ResXResourceReader reader) + { + List nodes = new List(); + + IDictionaryEnumerator dict = reader.GetEnumerator(); + while (dict.MoveNext()) + { + nodes.Add((ResXDataNode)dict.Value); + } + + return nodes; + } + + [Fact] + public void ResxDataNode_ResXFileRefsWrittenBackWithSameAssemblyInfo() + { + // This test ensures compatibility with tooling like Visual Studio's visual ResX editor + + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + + string originalResx = Example.ResxWithFileRef; + StringBuilder writerOutput = new StringBuilder(); + + using (var reader = new ResXResourceReader(new StringReader(originalResx))) + { + reader.UseResXDataNodes = true; + var dataNodes = ReaderToNodes(reader); + + using (ResXResourceWriter writer = new ResXResourceWriter(new StringWriter(writerOutput))) + { + dataNodes.ForEach(writer.AddResource); + writer.Generate(); + } + } + + Assert.Equal(originalResx, writerOutput.ToString()); + } + + [Fact] + public void ResxDataNode_CustomTypeConvertersDontOverwriteDefaultConverters() + { + string expectedIntTypeName = "some-text"; + string customTypeConverter(Type type) + { + if (type == typeof(int)) return expectedIntTypeName; + else return null; + } + + var intNode = new ResXDataNode("int-node", 1, customTypeConverter); + + var fileRefNode = new ResXDataNode("file-node", new ResXFileRef("i-am-file.txt", typeof(string).AssemblyQualifiedName), customTypeConverter); + + + var expected = + ( + ("int-node", expectedIntTypeName), + ("file-node", NetStandard.ResXConstants.ResxFileRefTypeInfo) + ); + var actual = + ( + ("int-node", intNode.GetDataNodeInfo().TypeName), + ("file-node", fileRefNode.GetDataNodeInfo().TypeName) + ); + + Assert.Equal(expected, actual); + } } } diff --git a/Tests/ResxWithFileRef.resx b/Tests/ResxWithFileRef.resx new file mode 100644 index 0000000..eea66f9 --- /dev/null +++ b/Tests/ResxWithFileRef.resx @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + + + + TestResources\Files\text.ansi.txt;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;windows-1252 + + \ No newline at end of file diff --git a/Tests/System.Resources.Tests.csproj b/Tests/System.Resources.Tests.csproj index 899be4f..854ad7f 100644 --- a/Tests/System.Resources.Tests.csproj +++ b/Tests/System.Resources.Tests.csproj @@ -17,11 +17,34 @@ + PreserveNewest + + + + + + + + + True + True + Example.resx + + + + + ResXFileCodeGenerator + Example.Designer.cs + + + + + diff --git a/Tests/TestResources/Files/FileRef.xml b/Tests/TestResources/Files/FileRef.xml new file mode 100644 index 0000000..8e9ab91 --- /dev/null +++ b/Tests/TestResources/Files/FileRef.xml @@ -0,0 +1,4 @@ + + + Text + \ No newline at end of file