/* Copyright (C) 2014-2019 de4dot@gmail.com This file is part of dnSpy dnSpy is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. dnSpy is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 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.IO; using System.Reflection; using System.Reflection.Emit; using System.Resources; using dnlib.DotNet; using dnSpy.Decompiler.Properties; namespace dnSpy.Decompiler.MSBuild { sealed class ResXProjectFile : ProjectFile { static ResXProjectFile() { // Mono doesn't support the constructors that we need Type[] paramTypes; ConstructorInfo? ctorInfo; paramTypes = new Type[] { typeof(string), typeof(Func) }; ctorInfo = typeof(ResXResourceWriter).GetConstructor(paramTypes); if (ctorInfo is not null) { var dynMethod = new DynamicMethod("ResXResourceWriter-ctor", typeof(ResXResourceWriter), paramTypes); var ilg = dynMethod.GetILGenerator(); ilg.Emit(OpCodes.Ldarg_0); ilg.Emit(OpCodes.Ldarg_1); ilg.Emit(OpCodes.Newobj, ctorInfo); ilg.Emit(OpCodes.Ret); delegateResXResourceWriterConstructor = (Func, ResXResourceWriter>)dynMethod.CreateDelegate(typeof(Func, ResXResourceWriter>)); } paramTypes = new Type[] { typeof(string), typeof(object), typeof(Func) }; ctorInfo = typeof(ResXDataNode).GetConstructor(paramTypes); if (ctorInfo is not null) { var dynMethod = new DynamicMethod("ResXDataNode-ctor", typeof(ResXDataNode), paramTypes); var ilg = dynMethod.GetILGenerator(); ilg.Emit(OpCodes.Ldarg_0); ilg.Emit(OpCodes.Ldarg_1); ilg.Emit(OpCodes.Ldarg_2); ilg.Emit(OpCodes.Newobj, ctorInfo); ilg.Emit(OpCodes.Ret); delegateResXDataNodeConstructor = (Func, ResXDataNode>)dynMethod.CreateDelegate(typeof(Func, ResXDataNode>)); } } static readonly Func, ResXResourceWriter>? delegateResXResourceWriterConstructor; static readonly Func, ResXDataNode>? delegateResXDataNodeConstructor; public override string Description => dnSpy_Decompiler_Resources.MSBuild_CreateResXFile; public override BuildAction BuildAction => BuildAction.EmbeddedResource; public override string Filename => filename; readonly string filename; public string TypeFullName { get; } public bool IsSatelliteFile { get; set; } readonly EmbeddedResource embeddedResource; readonly Dictionary newToOldAsm; public ResXProjectFile(ModuleDef module, string filename, string typeFullName, EmbeddedResource er) { this.filename = filename; TypeFullName = typeFullName; embeddedResource = er; newToOldAsm = new Dictionary(new AssemblyNameComparer(AssemblyNameComparerFlags.All & ~AssemblyNameComparerFlags.Version)); foreach (var asmRef in module.GetAssemblyRefs()) newToOldAsm[asmRef] = asmRef; } public override void Create(DecompileContext ctx) { var list = ReadResourceEntries(ctx); using (var writer = delegateResXResourceWriterConstructor?.Invoke(Filename, TypeNameConverter) ?? new ResXResourceWriter(Filename)) { foreach (var t in list) { ctx.CancellationToken.ThrowIfCancellationRequested(); writer.AddResource(t); } } } 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}"; } List ReadResourceEntries(DecompileContext ctx) { var list = new List(); int errors = 0; try { using (var reader = new ResourceReader(embeddedResource.CreateReader().AsStream())) { var iter = reader.GetEnumerator(); while (iter.MoveNext()) { ctx.CancellationToken.ThrowIfCancellationRequested(); string? key = null; try { key = iter.Key as string; if (key is null) continue; var value = iter.Value; // ResXDataNode ctor checks if the input is serializable, which this stream isn't. // We have no choice but to create a new stream. if (value is Stream && !value.GetType().IsSerializable) { var stream = (Stream)value; var data = new byte[stream.Length]; if (stream.Read(data, 0, data.Length) != data.Length) throw new IOException("Could not read all bytes"); value = new MemoryStream(data); } //TODO: Some resources, like images, should be saved as separate files. Use ResXFileRef. // Don't do it if it's a satellite assembly. list.Add(delegateResXDataNodeConstructor?.Invoke(key, value, TypeNameConverter) ?? new ResXDataNode(key, value)); } catch (Exception ex) { if (errors++ < 30) ctx.Logger.Error($"Could not add resource '{key}', Message: {ex.Message}"); } } } } catch (Exception ex) { ctx.Logger.Error($"Could not read resources from {embeddedResource.Name}, Message: {ex.Message}"); } return list; } } }