Skip to content

Commit

Permalink
Fixed incorrect type names and output of stream resources in RESX
Browse files Browse the repository at this point in the history
`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
  • Loading branch information
ElektroKill committed Jul 31, 2024
1 parent 05c1b83 commit a1cec85
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 41 deletions.
30 changes: 5 additions & 25 deletions dnSpy/dnSpy.Decompiler/MSBuild/ResXProjectFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,55 +17,35 @@ You should have received a copy of the GNU General Public License
along with dnSpy. If not, see <http://www.gnu.org/licenses/>.
*/

using System;
using System.Collections.Generic;
using System.Linq;
using dnlib.DotNet;
using dnlib.DotNet.Emit;
using dnlib.DotNet.Resources;
using dnSpy.Decompiler.Properties;

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<IAssembly, IAssembly> 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<IAssembly, IAssembly>(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}";
}
}
}
135 changes: 119 additions & 16 deletions dnSpy/dnSpy.Decompiler/MSBuild/ResXResourceFileWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/// <summary>
Expand Down Expand Up @@ -54,12 +57,48 @@ public ResXResourceInfo(string valueString) {
}
}

readonly Func<Type, string> 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<IAssembly, IAssembly> newToOldAsm;
readonly XmlTextWriter writer;
bool written;

public ResXResourceFileWriter(string fileName, Func<Type, string> typeNameConverter) {
this.typeNameConverter = typeNameConverter;
public ResXResourceFileWriter(string fileName, ModuleDef module) {
this.module = module;
newToOldAsm = new Dictionary<IAssembly, IAssembly>(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
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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:
Expand All @@ -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;
Expand All @@ -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();
}
Expand All @@ -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);
Expand Down Expand Up @@ -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() {
Expand Down

0 comments on commit a1cec85

Please sign in to comment.