From a1cec851a6648aa7f35cee7773bcf717f1a16c89 Mon Sep 17 00:00:00 2001 From: ElektroKill Date: Wed, 31 Jul 2024 13:01:07 +0200 Subject: [PATCH] Fixed incorrect type names and output of stream resources in RESX `BinaryFormatter` is not used directly as it is going to be removed in future .NET versions and no longer works for serializing `MemoryStream` types on .NET 8 --- .../MSBuild/ResXProjectFile.cs | 30 +--- .../MSBuild/ResXResourceFileWriter.cs | 135 +++++++++++++++--- 2 files changed, 124 insertions(+), 41 deletions(-) diff --git a/dnSpy/dnSpy.Decompiler/MSBuild/ResXProjectFile.cs b/dnSpy/dnSpy.Decompiler/MSBuild/ResXProjectFile.cs index 7bd2e817c9..e4d0ded473 100644 --- a/dnSpy/dnSpy.Decompiler/MSBuild/ResXProjectFile.cs +++ b/dnSpy/dnSpy.Decompiler/MSBuild/ResXProjectFile.cs @@ -17,11 +17,7 @@ You should have received a copy of the GNU General Public License along with dnSpy. If not, see . */ -using System; -using System.Collections.Generic; -using System.Linq; using dnlib.DotNet; -using dnlib.DotNet.Emit; using dnlib.DotNet.Resources; using dnSpy.Decompiler.Properties; @@ -29,43 +25,27 @@ namespace dnSpy.Decompiler.MSBuild { sealed class ResXProjectFile : ProjectFile { public override string Description => dnSpy_Decompiler_Resources.MSBuild_CreateResXFile; public override BuildAction BuildAction => BuildAction.EmbeddedResource; - public override string Filename => filename; - readonly string filename; - + public override string Filename { get; } public string TypeFullName { get; } public bool IsSatelliteFile { get; set; } + readonly ModuleDef module; readonly ResourceElementSet resourceElementSet; - readonly Dictionary newToOldAsm; public ResXProjectFile(ModuleDef module, string filename, string typeFullName, ResourceElementSet resourceElementSet) { - this.filename = filename; + this.module = module; + Filename = filename; TypeFullName = typeFullName; this.resourceElementSet = resourceElementSet; - - newToOldAsm = new Dictionary(new AssemblyNameComparer(AssemblyNameComparerFlags.All & ~AssemblyNameComparerFlags.Version)); - foreach (var asmRef in module.GetAssemblyRefs()) - newToOldAsm[asmRef] = asmRef; } public override void Create(DecompileContext ctx) { - using (var writer = new ResXResourceFileWriter(Filename, TypeNameConverter)) { + using (var writer = new ResXResourceFileWriter(Filename, module)) { foreach (var resourceElement in resourceElementSet.ResourceElements) { ctx.CancellationToken.ThrowIfCancellationRequested(); writer.AddResourceData(resourceElement); } } } - - string TypeNameConverter(Type type) { - var newAsm = new AssemblyNameInfo(type.Assembly.GetName()); - if (!newToOldAsm.TryGetValue(newAsm, out var oldAsm)) - return type.AssemblyQualifiedName ?? throw new ArgumentException(); - if (type.IsGenericType) - return type.AssemblyQualifiedName ?? throw new ArgumentException(); - if (AssemblyNameComparer.CompareAll.Equals(oldAsm, newAsm)) - return type.AssemblyQualifiedName ?? throw new ArgumentException(); - return $"{type.FullName}, {oldAsm.FullName}"; - } } } diff --git a/dnSpy/dnSpy.Decompiler/MSBuild/ResXResourceFileWriter.cs b/dnSpy/dnSpy.Decompiler/MSBuild/ResXResourceFileWriter.cs index 818cbe5d42..e0e49fe810 100644 --- a/dnSpy/dnSpy.Decompiler/MSBuild/ResXResourceFileWriter.cs +++ b/dnSpy/dnSpy.Decompiler/MSBuild/ResXResourceFileWriter.cs @@ -18,12 +18,15 @@ You should have received a copy of the GNU General Public License */ using System; +using System.Collections.Generic; using System.Globalization; using System.IO; using System.Resources; using System.Text; using System.Xml; +using dnlib.DotNet; using dnlib.DotNet.Resources; +using dnlib.DotNet.Writer; namespace dnSpy.Decompiler.MSBuild { /// @@ -54,12 +57,48 @@ public ResXResourceInfo(string valueString) { } } - readonly Func typeNameConverter; + private const int LengthPropertyOffset = 196; + private const int CapacityPropertyOffset = 200; + private const int BufferLengthOffset = 214; + private const int BufferOffset = 219; + private static readonly byte[] memoryStreamBinaryFormatterTemplate = [ + 0x00, 0x01, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x00, 0x16, 0x53, + 0x79, 0x73, 0x74, 0x65, 0x6D, 0x2E, 0x49, 0x4F, 0x2E, 0x4D, 0x65, 0x6D, + 0x6F, 0x72, 0x79, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6D, 0x0A, 0x00, 0x00, + 0x00, 0x07, 0x5F, 0x62, 0x75, 0x66, 0x66, 0x65, 0x72, 0x07, 0x5F, 0x6F, + 0x72, 0x69, 0x67, 0x69, 0x6E, 0x09, 0x5F, 0x70, 0x6F, 0x73, 0x69, 0x74, + 0x69, 0x6F, 0x6E, 0x07, 0x5F, 0x6C, 0x65, 0x6E, 0x67, 0x74, 0x68, 0x09, + 0x5F, 0x63, 0x61, 0x70, 0x61, 0x63, 0x69, 0x74, 0x79, 0x0B, 0x5F, 0x65, + 0x78, 0x70, 0x61, 0x6E, 0x64, 0x61, 0x62, 0x6C, 0x65, 0x09, 0x5F, 0x77, + 0x72, 0x69, 0x74, 0x61, 0x62, 0x6C, 0x65, 0x0A, 0x5F, 0x65, 0x78, 0x70, + 0x6F, 0x73, 0x61, 0x62, 0x6C, 0x65, 0x07, 0x5F, 0x69, 0x73, 0x4F, 0x70, + 0x65, 0x6E, 0x1D, 0x4D, 0x61, 0x72, 0x73, 0x68, 0x61, 0x6C, 0x42, 0x79, + 0x52, 0x65, 0x66, 0x4F, 0x62, 0x6A, 0x65, 0x63, 0x74, 0x2B, 0x5F, 0x5F, + 0x69, 0x64, 0x65, 0x6E, 0x74, 0x69, 0x74, 0x79, 0x07, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x02, 0x08, 0x08, 0x08, 0x08, 0x01, + 0x01, 0x01, 0x01, 0x09, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x00, 0x00, // Length + 0x05, 0x00, 0x00, 0x00, // Capacity + 0x00, 0x01, 0x00, 0x01, 0x0A, 0x0F, 0x02, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x00, 0x00, // buffer length + 0x02 + // buffer + // 0x0B + ]; + + readonly ModuleDef module; + readonly Dictionary newToOldAsm; readonly XmlTextWriter writer; bool written; - public ResXResourceFileWriter(string fileName, Func typeNameConverter) { - this.typeNameConverter = typeNameConverter; + public ResXResourceFileWriter(string fileName, ModuleDef module) { + this.module = module; + newToOldAsm = new Dictionary(new AssemblyNameComparer(AssemblyNameComparerFlags.All & ~AssemblyNameComparerFlags.Version)); + foreach (var asmRef in module.GetAssemblyRefs()) + newToOldAsm[asmRef] = asmRef; + writer = new XmlTextWriter(fileName, Encoding.UTF8) { Formatting = Formatting.Indented, Indentation = 2 @@ -94,14 +133,14 @@ void InitializeWriter() { writer.WriteStartElement("resheader"); writer.WriteAttributeString("name", "reader"); writer.WriteStartElement("value"); - writer.WriteString(typeNameConverter(typeof(ResXResourceReader))); + writer.WriteString(GetTypeName(typeof(ResXResourceReader))); writer.WriteEndElement(); writer.WriteEndElement(); writer.WriteStartElement("resheader"); writer.WriteAttributeString("name", "writer"); writer.WriteStartElement("value"); - writer.WriteString(typeNameConverter(typeof(ResXResourceWriter))); + writer.WriteString(GetTypeName(typeof(ResXResourceWriter))); writer.WriteEndElement(); writer.WriteEndElement(); } @@ -135,14 +174,14 @@ ResXResourceInfo GetNodeInfo(IResourceData resourceData) { // Mimic formatting used in ResXDataNode and TypeConverter implementations switch (builtInResourceData.Code) { case ResourceTypeCode.Null: - return new ResXResourceInfo("", typeNameConverter(typeof(ResXDataNode).Assembly.GetType("System.Resources.ResXNullRef")!)); + return new ResXResourceInfo("", GetTypeName(ResourceTypeCode.Null)); case ResourceTypeCode.String: return new ResXResourceInfo((string)builtInResourceData.Data); case ResourceTypeCode.Boolean: - return new ResXResourceInfo(((bool)builtInResourceData.Data).ToString(), typeNameConverter(typeof(bool))); + return new ResXResourceInfo(((bool)builtInResourceData.Data).ToString(), GetTypeName(ResourceTypeCode.Boolean)); case ResourceTypeCode.Char: var c = (char)builtInResourceData.Data; - return new ResXResourceInfo(c == '\0' ? "" : c.ToString(), typeNameConverter(typeof(char))); + return new ResXResourceInfo(c == '\0' ? "" : c.ToString(), GetTypeName(ResourceTypeCode.Char)); case ResourceTypeCode.Byte: case ResourceTypeCode.SByte: case ResourceTypeCode.Int16: @@ -153,15 +192,15 @@ ResXResourceInfo GetNodeInfo(IResourceData resourceData) { case ResourceTypeCode.UInt64: case ResourceTypeCode.Decimal: { var data = (IFormattable)builtInResourceData.Data; - return new ResXResourceInfo(data.ToString("G", CultureInfo.InvariantCulture.NumberFormat), typeNameConverter(builtInResourceData.Data.GetType())); + return new ResXResourceInfo(data.ToString("G", CultureInfo.InvariantCulture.NumberFormat), GetTypeName(builtInResourceData.Code)); } case ResourceTypeCode.Single: case ResourceTypeCode.Double: { var data = (IFormattable)builtInResourceData.Data; - return new ResXResourceInfo(data.ToString("R", CultureInfo.InvariantCulture.NumberFormat), typeNameConverter(builtInResourceData.Data.GetType())); + return new ResXResourceInfo(data.ToString("R", CultureInfo.InvariantCulture.NumberFormat), GetTypeName(builtInResourceData.Code)); } case ResourceTypeCode.TimeSpan: - return new ResXResourceInfo(((IFormattable)builtInResourceData.Data).ToString()!, typeNameConverter(typeof(TimeSpan))); + return new ResXResourceInfo(((IFormattable)builtInResourceData.Data).ToString()!, GetTypeName(ResourceTypeCode.TimeSpan)); case ResourceTypeCode.DateTime: var dateTime = (DateTime)builtInResourceData.Data; string str; @@ -171,11 +210,25 @@ ResXResourceInfo GetNodeInfo(IResourceData resourceData) { str = dateTime.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); else str = dateTime.ToString(CultureInfo.InvariantCulture); - return new ResXResourceInfo(str, typeNameConverter(typeof(DateTime))); + return new ResXResourceInfo(str, GetTypeName(ResourceTypeCode.DateTime)); case ResourceTypeCode.ByteArray: - return new ResXResourceInfo(ToBase64WrappedString((byte[])builtInResourceData.Data), typeNameConverter(typeof(byte[]))); - case ResourceTypeCode.Stream: - return new ResXResourceInfo(ToBase64WrappedString((byte[])builtInResourceData.Data), null, ResXResourceWriter.BinSerializedObjectMimeType); + return new ResXResourceInfo(ToBase64WrappedString((byte[])builtInResourceData.Data), GetTypeName(ResourceTypeCode.ByteArray)); + case ResourceTypeCode.Stream: { + var data = (byte[])builtInResourceData.Data; + var finalBuffer = new byte[memoryStreamBinaryFormatterTemplate.Length + data.Length + 1]; + var bufWriter = new ArrayWriter(finalBuffer); + bufWriter.WriteBytes(memoryStreamBinaryFormatterTemplate); + bufWriter.Position = LengthPropertyOffset; + bufWriter.WriteInt32(data.Length); + bufWriter.Position = CapacityPropertyOffset; + bufWriter.WriteInt32(data.Length); + bufWriter.Position = BufferLengthOffset; + bufWriter.WriteInt32(data.Length); + bufWriter.Position = BufferOffset; + bufWriter.WriteBytes(data); + bufWriter.WriteByte(0x0B); + return new ResXResourceInfo(ToBase64WrappedString(finalBuffer), null, ResXResourceWriter.BinSerializedObjectMimeType); + } default: throw new ArgumentOutOfRangeException(); } @@ -187,7 +240,7 @@ ResXResourceInfo GetNodeInfo(IResourceData resourceData) { case SerializationFormat.TypeConverterByteArray: case SerializationFormat.ActivatorStream: // RESX does not have a way to represent creation of an object using Activator.CreateInstance, - // so we fallback to the same representation as data passed into TypeConverter. + // so we fall back to the same representation as data passed into TypeConverter. return new ResXResourceInfo(ToBase64WrappedString(binaryResourceData.Data), binaryResourceData.TypeName, ResXResourceWriter.ByteArraySerializedObjectMimeType); case SerializationFormat.TypeConverterString: return new ResXResourceInfo(Encoding.UTF8.GetString(binaryResourceData.Data), binaryResourceData.TypeName); @@ -221,6 +274,56 @@ static string ToBase64WrappedString(byte[] data) { return raw; } + string GetTypeName(Type type) { + IAssembly newAsm = new AssemblyNameInfo(type.Assembly.GetName()); + if (!newToOldAsm.TryGetValue(newAsm, out var oldAsm)) + return type.AssemblyQualifiedName ?? throw new ArgumentException(); + if (type.IsGenericType) + return type.AssemblyQualifiedName ?? throw new ArgumentException(); + if (AssemblyNameComparer.CompareAll.Equals(oldAsm, newAsm)) + return type.AssemblyQualifiedName ?? throw new ArgumentException(); + return $"{type.FullName}, {oldAsm.FullName}"; + } + + string GetTypeName(ResourceTypeCode typeCode) { + if (typeCode == ResourceTypeCode.Null) { + var asmRef = GetAssemblyRef("System.Windows.Forms"); + if (asmRef is not null) + return new TypeRefUser(module, "System.Resources", "ResXNullRef", asmRef).AssemblyQualifiedName; + return GetTypeName(typeof(ResXDataNode).Assembly.GetType("System.Resources.ResXNullRef")!); + } + return typeCode switch { + ResourceTypeCode.String => module.CorLibTypes.String.AssemblyQualifiedName, + ResourceTypeCode.Boolean => module.CorLibTypes.Boolean.AssemblyQualifiedName, + ResourceTypeCode.Char => module.CorLibTypes.Char.AssemblyQualifiedName, + ResourceTypeCode.Byte => module.CorLibTypes.Byte.AssemblyQualifiedName, + ResourceTypeCode.SByte => module.CorLibTypes.SByte.AssemblyQualifiedName, + ResourceTypeCode.Int16 => module.CorLibTypes.Int16.AssemblyQualifiedName, + ResourceTypeCode.UInt16 => module.CorLibTypes.UInt16.AssemblyQualifiedName, + ResourceTypeCode.Int32 => module.CorLibTypes.Int32.AssemblyQualifiedName, + ResourceTypeCode.UInt32 => module.CorLibTypes.UInt32.AssemblyQualifiedName, + ResourceTypeCode.Int64 => module.CorLibTypes.Int64.AssemblyQualifiedName, + ResourceTypeCode.UInt64 => module.CorLibTypes.UInt64.AssemblyQualifiedName, + ResourceTypeCode.Single => module.CorLibTypes.Single.AssemblyQualifiedName, + ResourceTypeCode.Double => module.CorLibTypes.Double.AssemblyQualifiedName, + ResourceTypeCode.Decimal => module.CorLibTypes.GetTypeRef("System", "Decimal").AssemblyQualifiedName, + ResourceTypeCode.DateTime => module.CorLibTypes.GetTypeRef("System", "DateTime").AssemblyQualifiedName, + ResourceTypeCode.TimeSpan => module.CorLibTypes.GetTypeRef("System", "TimeSpan").AssemblyQualifiedName, + ResourceTypeCode.ByteArray => new SZArraySig(module.CorLibTypes.Byte).AssemblyQualifiedName, + ResourceTypeCode.Stream => module.CorLibTypes.GetTypeRef("System.IO", "MemoryStream") + .AssemblyQualifiedName, + _ => throw new ArgumentOutOfRangeException(nameof(typeCode), typeCode, null) + }; + } + + AssemblyRef? GetAssemblyRef(string name) { + foreach (var asmRef in module.GetAssemblyRefs()) { + if (asmRef.Name == name) + return asmRef; + } + return null; + } + ~ResXResourceFileWriter() => Dispose(false); public void Dispose() {