From 279e3a479d79eeaf2d912abe205f54c10960d9fa Mon Sep 17 00:00:00 2001 From: Adrian Kokot <35179235+AdrianKokot@users.noreply.github.com> Date: Tue, 5 Mar 2024 16:17:16 +0100 Subject: [PATCH] Fix ResXNullRef type handling (#10) * Fix ResXNullRef type handling * Clarify the exclusivity of cases in winforms type mapping * Test vs editor compatibility for ResXNullRef node writes Also fixed a misunderstanding in the round trip test --------- Co-authored-by: Spencer Farley <2847259+farlee2121@users.noreply.github.com> --- System.Resources.NetStandard/ResXDataNode.cs | 19 +- System.Resources.NetStandard/ResxConstants.cs | 3 + .../WinformsTypeMappers.cs | 15 +- Tests/Example.Designer.cs | 265 +++++++++-------- Tests/Example.resx | 273 +++++++++--------- Tests/ResxDataNodeTests.cs | 93 +++++- Tests/ResxWithNullRef.resx | 65 +++++ Tests/System.Resources.Tests.csproj | 4 + 8 files changed, 465 insertions(+), 272 deletions(-) create mode 100644 Tests/ResxWithNullRef.resx diff --git a/System.Resources.NetStandard/ResXDataNode.cs b/System.Resources.NetStandard/ResXDataNode.cs index 1cb93d8..3398191 100644 --- a/System.Resources.NetStandard/ResXDataNode.cs +++ b/System.Resources.NetStandard/ResXDataNode.cs @@ -158,6 +158,11 @@ private void InitializeDataNode(string basePath) } } } + + if (nodeType != null && nodeType.Equals(typeof(ResXNullRef))) + { + value = new ResXNullRef(); + } } /// @@ -619,11 +624,16 @@ public string GetValueTypeName(AssemblyName[] names) /// public object GetValue(ITypeResolutionService typeResolver) { + if (value is ResXNullRef) + { + return null; + } + if (value != null) { return value; } - + object result = null; if (FileRefFullPath != null) { @@ -962,6 +972,13 @@ public Type GetType(string name, bool throwOnError, bool ignoreCase) result = typeof(ResXFileRef); return result; } + + // Replace the WinForms ResXNullRef with the copy in this library + if (name.StartsWith(ResXConstants.ResXNullRef_TypeNameAndAssembly, StringComparison.Ordinal)) + { + result = typeof(ResXNullRef); + return result; + } // Missed in cache, try to resolve the type from the reference assemblies. if (name.IndexOf(',') != -1) diff --git a/System.Resources.NetStandard/ResxConstants.cs b/System.Resources.NetStandard/ResxConstants.cs index 6274ed7..73720dd 100644 --- a/System.Resources.NetStandard/ResxConstants.cs +++ b/System.Resources.NetStandard/ResxConstants.cs @@ -10,5 +10,8 @@ internal class ResXConstants 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"; + + public const string ResxNullRefTypeInfo = "System.Resources.ResXNullRef, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"; + public const string ResXNullRef_TypeNameAndAssembly = "System.Resources.ResXNullRef, System.Windows.Forms"; } } \ No newline at end of file diff --git a/System.Resources.NetStandard/WinformsTypeMappers.cs b/System.Resources.NetStandard/WinformsTypeMappers.cs index 235a94e..b30380c 100644 --- a/System.Resources.NetStandard/WinformsTypeMappers.cs +++ b/System.Resources.NetStandard/WinformsTypeMappers.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace System.Resources.NetStandard +namespace System.Resources.NetStandard { internal static class WinformsTypeMappers { @@ -12,13 +8,14 @@ public static Func InterceptWinformsTypes(Func typeNa { if (type.AssemblyQualifiedName == typeof(ResXFileRef).AssemblyQualifiedName) { - return NetStandard.ResXConstants.ResxFileRefTypeInfo; + return ResXConstants.ResxFileRefTypeInfo; } - else + else if (type.AssemblyQualifiedName == typeof(ResXNullRef).AssemblyQualifiedName) { - if (typeNameConverter != null) return typeNameConverter(type); - else return null; + return ResXConstants.ResxNullRefTypeInfo; } + else if (typeNameConverter != null) return typeNameConverter(type); + else return null; }; } diff --git a/Tests/Example.Designer.cs b/Tests/Example.Designer.cs index 3222955..3f12d7b 100644 --- a/Tests/Example.Designer.cs +++ b/Tests/Example.Designer.cs @@ -1,123 +1,142 @@ -//------------------------------------------------------------------------------ -// -// 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); - } - } - } -} +//------------------------------------------------------------------------------ +// +// 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 <?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 ResxWithNullRef { + get { + return ResourceManager.GetString("ResxWithNullRef", 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 index e74bc50..0d40358 100644 --- a/Tests/Example.resx +++ b/Tests/Example.resx @@ -1,136 +1,139 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + ResxWithNullRef.resx;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + \ No newline at end of file diff --git a/Tests/ResxDataNodeTests.cs b/Tests/ResxDataNodeTests.cs index 1e0beac..634239c 100644 --- a/Tests/ResxDataNodeTests.cs +++ b/Tests/ResxDataNodeTests.cs @@ -6,13 +6,10 @@ 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 { @@ -105,7 +102,7 @@ private List ReaderToNodes(ResXResourceReader reader) } [Fact] - public void ResxDataNode_ResXFileRefsWrittenBackWithSameAssemblyInfo() + public void ResxDataNode_ResXFileRefs_WrittenBackWithSameAssemblyInfo() { // This test ensures compatibility with tooling like Visual Studio's visual ResX editor @@ -157,5 +154,93 @@ string customTypeConverter(Type type) Assert.Equal(expected, actual); } + + [Fact] + public void ResxDataNode_CreateForResXNullRef() + { + // Simulate an XML file stored in resources, which uses a ResXNullRef. The actual resx XML looks like: + /* + + + + + */ + + var nodeInfo = new DataNodeInfo + { + Name = "Test", + ReaderPosition = new Point(1, 2), + TypeName = "System.Resources.ResXNullRef, System.Windows.Forms", + ValueData = "", + }; + var dataNode = new ResXDataNode(nodeInfo, null); + var typeResolver = new AssemblyNamesTypeResolutionService(Array.Empty()); + Assert.Equal("Test", dataNode.Name); + Assert.Null(dataNode.GetValue(typeResolver)); + + StringBuilder resxOutput = new StringBuilder(); + using (ResXResourceWriter resx = new ResXResourceWriter(new StringWriter(resxOutput))) + { + resx.AddResource(dataNode); + } + + Assert.Contains("", resxOutput.ToString()); + } + + [Fact] + public void ResxDataNode_ResXNullRef_RoundTrip() + { + object referencedFileContent = null; + var nodeInfo = new DataNodeInfo + { + Name = "ResxWithNullRef", + ReaderPosition = new Point(1, 2), + TypeName = "System.Resources.ResXNullRef, System.Windows.Forms", + ValueData = "" + }; + var dataNode = new ResXDataNode(nodeInfo, null); + + 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)); + } + } + + [Fact] + public void ResxDataNode_ResXNullRefs_WrittenBackWithSameAssemblyInfo() + { + // This test ensures compatibility with tooling like Visual Studio's visual ResX editor + + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + + string originalResx = Example.ResxWithNullRef; + 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()); + } } } diff --git a/Tests/ResxWithNullRef.resx b/Tests/ResxWithNullRef.resx new file mode 100644 index 0000000..749ff9b --- /dev/null +++ b/Tests/ResxWithNullRef.resx @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + + + + \ No newline at end of file diff --git a/Tests/System.Resources.Tests.csproj b/Tests/System.Resources.Tests.csproj index 09d3baf..ae2ef37 100644 --- a/Tests/System.Resources.Tests.csproj +++ b/Tests/System.Resources.Tests.csproj @@ -26,9 +26,13 @@ + + + Designer +