diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj index f1b1e5777..3f60f4548 100644 --- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj +++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj @@ -130,7 +130,6 @@ - @@ -356,7 +355,6 @@ - diff --git a/ICSharpCode.Decompiler/Util/ResXResourceWriter.cs b/ICSharpCode.Decompiler/Util/ResXResourceWriter.cs index 7d18ef551..80d712353 100644 --- a/ICSharpCode.Decompiler/Util/ResXResourceWriter.cs +++ b/ICSharpCode.Decompiler/Util/ResXResourceWriter.cs @@ -36,6 +36,8 @@ using System.IO; using System.Text; using System.Xml; +using ICSharpCode.Decompiler.Metadata; + namespace ICSharpCode.Decompiler.Util { #if INSIDE_SYSTEM_WEB @@ -150,7 +152,7 @@ namespace ICSharpCode.Decompiler.Util { writer.WriteAttributeString("mimetype", BinSerializedObjectMimeType); writer.WriteStartElement("value"); - writer.WriteBase64(value, offset, length); + WriteNiceBase64(value, offset, length); } writer.WriteEndElement(); @@ -274,7 +276,7 @@ namespace ICSharpCode.Decompiler.Util break; case ResourceSerializedObject rso: var bytes = rso.GetBytes(); - WriteBytes(name, null, bytes, 0, bytes.Length, comment); + WriteBytes(name, rso.TypeName, bytes, 0, bytes.Length, comment); break; default: throw new NotSupportedException($"Value '{value}' of type {value.GetType().FullName} is not supported by this version of ResXResourceWriter. Use byte arrays or streams instead."); diff --git a/ICSharpCode.Decompiler/Util/ResourcesFile.cs b/ICSharpCode.Decompiler/Util/ResourcesFile.cs index 988fa01a2..5ef294a1f 100644 --- a/ICSharpCode.Decompiler/Util/ResourcesFile.cs +++ b/ICSharpCode.Decompiler/Util/ResourcesFile.cs @@ -76,12 +76,21 @@ namespace ICSharpCode.Decompiler.Util StartOfUserTypes = 0x40 } + enum SerializationFormat + { + BinaryFormatter = 1, + TypeConverterByteArray = 2, + TypeConverterString = 3, + ActivatorStream = 4 + } + /// Holds the number used to identify resource files. public const int MagicNumber = unchecked((int)0xBEEFCACE); const int ResourceSetVersion = 2; readonly MyBinaryReader reader; readonly int version; + readonly bool usesSerializationFormat; readonly int numResources; readonly string[] typeTable; readonly int[] namePositions; @@ -94,7 +103,7 @@ namespace ICSharpCode.Decompiler.Util /// Creates a new ResourcesFile. /// /// Input stream. - /// Whether the stream should be help open when the ResourcesFile is disposed. + /// Whether the stream should be held open when the ResourcesFile is disposed. /// /// The stream is must be held open while the ResourcesFile is in use. /// The stream must be seekable; any operation using the ResourcesFile will end up seeking the stream. @@ -130,7 +139,8 @@ namespace ICSharpCode.Decompiler.Util // We don't care about numBytesToSkip; read the rest of the header // readerType: - reader.ReadString(); + string readerType = reader.ReadString(); + usesSerializationFormat = readerType == "System.Resources.Extensions.DeserializingResourceReader, System.Resources.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51"; // resourceSetType: reader.ReadString(); } @@ -369,7 +379,7 @@ namespace ICSharpCode.Decompiler.Util bits[i] = reader.ReadInt32(); return new decimal(bits); default: - return new ResourceSerializedObject(FindType(typeIndex), this, reader.BaseStream.Position); + return new ResourceSerializedObject(FindType(typeIndex), this, reader.BaseStream.Position, usesSerializationFormat); } } @@ -461,7 +471,7 @@ namespace ICSharpCode.Decompiler.Util { throw new BadImageFormatException("Invalid typeCode"); } - return new ResourceSerializedObject(FindType(typeCode - ResourceTypeCode.StartOfUserTypes), this, reader.BaseStream.Position); + return new ResourceSerializedObject(FindType(typeCode - ResourceTypeCode.StartOfUserTypes), this, reader.BaseStream.Position, usesSerializationFormat); } } @@ -509,7 +519,7 @@ namespace ICSharpCode.Decompiler.Util } } - internal byte[] GetBytesForSerializedObject(long pos) + internal byte[] GetBytesForSerializedObject(long pos, bool usesSerializationFormat) { long[] positions = GetStartPositions(); int i = Array.BinarySearch(positions, pos); @@ -535,6 +545,12 @@ namespace ICSharpCode.Decompiler.Util } int len = (int)(endPos - pos); reader.Seek(pos, SeekOrigin.Begin); + if (usesSerializationFormat) + { + int kind = reader.Read7BitEncodedInt(); + Debug.Assert(Enum.IsDefined(typeof(SerializationFormat), kind)); + len = reader.Read7BitEncodedInt(); + } return reader.ReadBytes(len); } } @@ -545,12 +561,14 @@ namespace ICSharpCode.Decompiler.Util public string? TypeName { get; } readonly ResourcesFile file; readonly long position; + readonly bool usesSerializationFormat; - internal ResourceSerializedObject(string? typeName, ResourcesFile file, long position) + internal ResourceSerializedObject(string? typeName, ResourcesFile file, long position, bool usesSerializationFormat) { - this.TypeName = typeName; + this.TypeName = usesSerializationFormat ? typeName : null; this.file = file; this.position = position; + this.usesSerializationFormat = usesSerializationFormat; } /// @@ -558,7 +576,7 @@ namespace ICSharpCode.Decompiler.Util /// public Stream GetStream() { - return new MemoryStream(file.GetBytesForSerializedObject(position), writable: false); + return new MemoryStream(file.GetBytesForSerializedObject(position, usesSerializationFormat), writable: false); } /// @@ -566,7 +584,7 @@ namespace ICSharpCode.Decompiler.Util /// public byte[] GetBytes() { - return file.GetBytesForSerializedObject(position); + return file.GetBytesForSerializedObject(position, usesSerializationFormat); } } } diff --git a/ILSpy.Tests/BitmapContainer.resources b/ILSpy.Tests/BitmapContainer.resources new file mode 100644 index 000000000..d50495a74 Binary files /dev/null and b/ILSpy.Tests/BitmapContainer.resources differ diff --git a/ILSpy.Tests/ILSpy.Tests.csproj b/ILSpy.Tests/ILSpy.Tests.csproj index 0f44621f6..8340c2b46 100644 --- a/ILSpy.Tests/ILSpy.Tests.csproj +++ b/ILSpy.Tests/ILSpy.Tests.csproj @@ -41,6 +41,19 @@ + + + + + + + + ResXCodeGenerator + + + + + diff --git a/ILSpy.Tests/Icon.ico b/ILSpy.Tests/Icon.ico new file mode 100644 index 000000000..5d06b9f28 Binary files /dev/null and b/ILSpy.Tests/Icon.ico differ diff --git a/ILSpy.Tests/IconContainer.resx b/ILSpy.Tests/IconContainer.resx new file mode 100644 index 000000000..842140e25 --- /dev/null +++ b/ILSpy.Tests/IconContainer.resx @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + + Icon.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/ICSharpCode.Decompiler.Tests/Util/ResourceReaderWriterTests.cs b/ILSpy.Tests/ResourceReaderWriterTests.cs similarity index 63% rename from ICSharpCode.Decompiler.Tests/Util/ResourceReaderWriterTests.cs rename to ILSpy.Tests/ResourceReaderWriterTests.cs index 59f8aadce..4ade2b91a 100644 --- a/ICSharpCode.Decompiler.Tests/Util/ResourceReaderWriterTests.cs +++ b/ILSpy.Tests/ResourceReaderWriterTests.cs @@ -17,11 +17,10 @@ // DEALINGS IN THE SOFTWARE. using System; +using System.Drawing; using System.Globalization; using System.IO; using System.Linq; -using System.Resources; -using System.Runtime.CompilerServices; using System.Xml.Linq; using System.Xml.XPath; @@ -30,13 +29,16 @@ using ICSharpCode.Decompiler.Util; using NUnit.Framework; using NUnit.Framework.Internal; -namespace ICSharpCode.Decompiler.Tests.Util +using TomsToolbox.Essentials; + +namespace ICSharpCode.ILSpy.Tests { [TestFixture] public class ResourceReaderWriterTests { - const string WinFormsAssemblyName = ", System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"; - const string MSCorLibAssemblyName = ", mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"; + const string winFormsAssemblyName = ", System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"; + const string msCorLibAssemblyName = ", mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"; + const string drawingAssemblyName = ", System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"; [Serializable] public class SerializableClass @@ -45,22 +47,27 @@ namespace ICSharpCode.Decompiler.Tests.Util public int Age { get; set; } } - static readonly object[][] TestWriteCases = { - new object[] { "Decimal", 1.0m, "1.0", "System.Decimal" + MSCorLibAssemblyName }, - new object[] { "TimeSpan", TimeSpan.FromSeconds(42), "00:00:42", "System.TimeSpan" + MSCorLibAssemblyName }, - new object[] { "DateTime", DateTime.Parse("06/18/2023 21:36:30", CultureInfo.InvariantCulture), "06/18/2023 21:36:30", "System.DateTime" + MSCorLibAssemblyName }, + static readonly object[][] testWriteCases = { + new object[] { "Decimal", 1.0m, "1.0", "System.Decimal" + msCorLibAssemblyName }, + new object[] { "TimeSpan", TimeSpan.FromSeconds(42), "00:00:42", "System.TimeSpan" + msCorLibAssemblyName }, + new object[] { "DateTime", DateTime.Parse("06/18/2023 21:36:30", CultureInfo.InvariantCulture), "06/18/2023 21:36:30", "System.DateTime" + msCorLibAssemblyName }, }; - static readonly object[][] TestReadCases = { + static readonly object[][] testReadCases = { new object[] { "Decimal", 1.0m }, new object[] { "TimeSpan", TimeSpan.FromSeconds(42) }, new object[] { "DateTime", DateTime.Parse("06/18/2023 21:36:30", CultureInfo.InvariantCulture) }, }; + static Stream GetResource(string fileName) + { + return typeof(ResourceReaderWriterTests).Assembly.GetManifestResourceStream(typeof(ResourceReaderWriterTests).Namespace + "." + fileName); + } + static MemoryStream ProduceResourcesTestFile(string name, T value) { var ms = new MemoryStream(); - var writer = new ResourceWriter(ms); + var writer = new System.Resources.ResourceWriter(ms); writer.AddResource(name, value); writer.Generate(); ms.Position = 0; @@ -70,7 +77,7 @@ namespace ICSharpCode.Decompiler.Tests.Util static XElement ProduceResXTest(string name, T value) { using var ms = new MemoryStream(); - var writer = new ResXResourceWriter(ms); + var writer = new Decompiler.Util.ResXResourceWriter(ms); writer.AddResource(name, value); writer.Generate(); ms.Position = 0; @@ -94,7 +101,7 @@ namespace ICSharpCode.Decompiler.Tests.Util [TestCase("Single", 1.0f)] [TestCase("Double", 1.0d)] [TestCase("Bytes", new byte[] { 42, 43, 44 })] - [TestCaseSource(nameof(TestReadCases))] + [TestCaseSource(nameof(testReadCases))] public void Read(string name, object value) { using var testFile = ProduceResourcesTestFile(name, value); @@ -105,22 +112,22 @@ namespace ICSharpCode.Decompiler.Tests.Util Assert.That(items[0].Value, Is.EqualTo(value)); } - [TestCase("Null", null, null, "System.Resources.ResXNullRef" + WinFormsAssemblyName)] + [TestCase("Null", null, null, "System.Resources.ResXNullRef" + winFormsAssemblyName)] [TestCase("String", "Hello World!", "Hello World!", null)] - [TestCase("Bool", true, "True", "System.Boolean" + MSCorLibAssemblyName)] - [TestCase("Bool", false, "False", "System.Boolean" + MSCorLibAssemblyName)] - [TestCase("Char", 'A', "A", "System.Char" + MSCorLibAssemblyName)] - [TestCase("Byte", (byte)1, "1", "System.Byte" + MSCorLibAssemblyName)] - [TestCase("SByte", (sbyte)-1, "-1", "System.SByte" + MSCorLibAssemblyName)] - [TestCase("Int16", (short)1, "1", "System.Int16" + MSCorLibAssemblyName)] - [TestCase("UInt16", (ushort)1, "1", "System.UInt16" + MSCorLibAssemblyName)] - [TestCase("Int32", 1, "1", "System.Int32" + MSCorLibAssemblyName)] - [TestCase("UInt32", (uint)1, "1", "System.UInt32" + MSCorLibAssemblyName)] - [TestCase("Int64", (long)1, "1", "System.Int64" + MSCorLibAssemblyName)] - [TestCase("UInt64", (ulong)1, "1", "System.UInt64" + MSCorLibAssemblyName)] - [TestCase("Single", 1.0f, "1", "System.Single" + MSCorLibAssemblyName)] - [TestCase("Double", 1.0d, "1", "System.Double" + MSCorLibAssemblyName)] - [TestCaseSource(nameof(TestWriteCases))] + [TestCase("Bool", true, "True", "System.Boolean" + msCorLibAssemblyName)] + [TestCase("Bool", false, "False", "System.Boolean" + msCorLibAssemblyName)] + [TestCase("Char", 'A', "A", "System.Char" + msCorLibAssemblyName)] + [TestCase("Byte", (byte)1, "1", "System.Byte" + msCorLibAssemblyName)] + [TestCase("SByte", (sbyte)-1, "-1", "System.SByte" + msCorLibAssemblyName)] + [TestCase("Int16", (short)1, "1", "System.Int16" + msCorLibAssemblyName)] + [TestCase("UInt16", (ushort)1, "1", "System.UInt16" + msCorLibAssemblyName)] + [TestCase("Int32", 1, "1", "System.Int32" + msCorLibAssemblyName)] + [TestCase("UInt32", (uint)1, "1", "System.UInt32" + msCorLibAssemblyName)] + [TestCase("Int64", (long)1, "1", "System.Int64" + msCorLibAssemblyName)] + [TestCase("UInt64", (ulong)1, "1", "System.UInt64" + msCorLibAssemblyName)] + [TestCase("Single", 1.0f, "1", "System.Single" + msCorLibAssemblyName)] + [TestCase("Double", 1.0d, "1", "System.Double" + msCorLibAssemblyName)] + [TestCaseSource(nameof(testWriteCases))] public void Write(string name, object value, string serializedValue, string typeName) { var element = ProduceResXTest(name, value); @@ -153,6 +160,8 @@ namespace ICSharpCode.Decompiler.Tests.Util var item = items.FirstOrDefault(i => i.Key == "Bitmap"); Assert.That(item.Key, Is.Not.Null); Assert.That(item.Value, Is.InstanceOf()); + var rso = (ResourceSerializedObject)item.Value; + Assert.That(rso.TypeName, Is.Null); } [Test] @@ -185,5 +194,39 @@ namespace ICSharpCode.Decompiler.Tests.Util Assert.That(item.Key, Is.Not.Null); Assert.That(item.Value, Is.InstanceOf()); } + + [Test] + public void IconDataCanBeDeserializedFromResX() + { + // Uses new serialization format + var resourcesStream = GetResource("IconContainer.resources"); + var reader = new ResourcesFile(resourcesStream); + var item = reader.Single(); + Assert.That(item.Key, Is.EqualTo("Icon")); + Assert.That(item.Value, Is.InstanceOf()); + var rso = (ResourceSerializedObject)item.Value; + var xml = ProduceResXTest("Icon", rso); + Assert.That(xml.GetAttribute("name"), Is.EqualTo("Icon")); + Assert.That(xml.GetAttribute("type"), Is.EqualTo("System.Drawing.Icon" + drawingAssemblyName)); + Assert.That(xml.GetAttribute("mimetype"), Is.EqualTo("application/x-microsoft.net.object.bytearray.base64")); + var base64Icon = xml.Element("value").Value; + using var memory = new MemoryStream(Convert.FromBase64String(base64Icon)); + new Icon(memory); + } + + [Test] + public void BitmapDataCanBeDeserializedFromResX() + { + // Uses old serialization format + var resourcesStream = GetResource("BitmapContainer.resources"); + var reader = new ResourcesFile(resourcesStream); + var item = reader.Single(x => x.Key == "Image1"); + Assert.That(item.Value, Is.InstanceOf()); + var rso = (ResourceSerializedObject)item.Value; + var xml = ProduceResXTest("Image1", rso); + Assert.That(xml.GetAttribute("name"), Is.EqualTo("Image1")); + Assert.That(xml.GetAttribute("type"), Is.Null); + Assert.That(xml.GetAttribute("mimetype"), Is.EqualTo("application/x-microsoft.net.object.binary.base64")); + } } } \ No newline at end of file diff --git a/ICSharpCode.Decompiler.Tests/Util/Test.resources b/ILSpy.Tests/Test.resources similarity index 100% rename from ICSharpCode.Decompiler.Tests/Util/Test.resources rename to ILSpy.Tests/Test.resources