/* 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.Diagnostics; using System.Linq; using System.Text; using dnlib.DotNet; using dnlib.DotNet.Emit; namespace dnSpy.Decompiler.MSBuild { sealed class ResourceNameCreator { readonly ModuleDef module; readonly FilenameCreator filenameCreator; public ResourceNameCreator(ModuleDef module, FilenameCreator filenameCreator) { this.module = module; this.filenameCreator = filenameCreator; } public string GetResxFilename(string resourceName, out string typeFullName) { const string RESOURCES_EXT = ".resources"; const string RESX_EXT = ".resx"; var n = resourceName; if (n.EndsWith(RESOURCES_EXT, StringComparison.OrdinalIgnoreCase)) n = n.Substring(0, n.Length - RESOURCES_EXT.Length); var type = module.Find(n, true); if (type is not null && DotNetUtils.IsWinForm(type)) { typeFullName = type.ReflectionFullName; return filenameCreator.CreateFromNamespaceName(RESX_EXT, type.Namespace, type.Name); } var resXType = GetResXType(type, n); if (resXType is not null) { typeFullName = resXType.ReflectionFullName; return filenameCreator.CreateFromNamespaceName(RESX_EXT, resXType.ReflectionNamespace, GetResxDesignerFilename(resXType.ReflectionNamespace, n)); } typeFullName = n; return filenameCreator.Create(RESX_EXT, n); } string GetResxDesignerFilename(string ns, string name) { if (name.StartsWith(ns + ".", StringComparison.Ordinal)) return name.Substring(ns.Length + 1); Debug.Fail("Weird name"); return name; } TypeDef? GetResXType(TypeDef? type, string name) { if (type is not null && IsResXType(type, name)) return type; return FindResXType(name); } TypeDef? FindResXType(string name) { if (resXNameToType is null) { var dict = new Dictionary(StringComparer.Ordinal); foreach (var t in module.Types) { var s = GetResXString(t); if (s is not null) dict[s] = t; } resXNameToType = dict; } resXNameToType.TryGetValue(name, out var type); return type; } Dictionary? resXNameToType; static string? GetResXString(TypeDef type) { if (!type.Fields.Any(a => a.IsStatic && a.FieldType is not null && a.FieldType.ToString() == "System.Globalization.CultureInfo")) return null; if (!type.Fields.Any(a => a.IsStatic && a.FieldType is not null && a.FieldType.ToString() == "System.Resources.ResourceManager")) return null; foreach (var m in type.Methods) { var body = m.Body; if (body is null) continue; var instrs = body.Instructions; for (int i = 0; i + 2 < instrs.Count; i++) { if (instrs[i].OpCode.Code != Code.Ldstr) continue; if (instrs[i + 1].OpCode.Code != Code.Ldtoken) continue; if (instrs[i + 2].OpCode.Code != Code.Call) continue; var s = instrs[i].Operand as string; if (s is null) continue; var cm = instrs[i + 2].Operand as IMethod; if (cm is null || cm.FullName != "System.Type System.Type::GetTypeFromHandle(System.RuntimeTypeHandle)") continue; return s; } } return null; } bool IsResXType(TypeDef type, string name) { foreach (var m in type.Methods) { var body = m.Body; if (body is null) continue; bool b = body.Instructions.Any(a => a.Operand is string && name.Equals((string)a.Operand)); if (b) return true; } return false; } public string GetXamlResourceFilename(string resourceName) => GetBamlResourceName(resourceName); string GetBamlResourceName(string resourceName) { if (namespaces is null) Initialize(); Debug2.Assert(partialNamespaceMap is not null); Debug2.Assert(partialTypeToFullNameMap is not null); Debug2.Assert(typeToFullNameMap is not null); Debug2.Assert(lowerCaseNsToReal is not null); Debug2.Assert(namespaces is not null); var ext = FileUtils.GetExtension(resourceName); var nameNoExt = resourceName.Substring(0, resourceName.Length - ext.Length); var ns = GetNamespace(resourceName); if (partialNamespaceMap.TryGetValue(ns, out var fixedNs)) nameNoExt = fixedNs.Replace('.', '/') + "/" + nameNoExt.Substring(ns.Length + 1); return filenameCreator.CreateFromRelativePath(nameNoExt, ext); } static string GetNamespace(string name) { // name can contain illegal chars so don't use Path methods int i = name.LastIndexOf('/'); if (i < 0) return string.Empty; return name.Substring(0, i).Replace('/', '.'); } public string GetBamlResourceName(string resourceName, out string typeFullName) { if (namespaces is null) Initialize(); Debug2.Assert(partialNamespaceMap is not null); Debug2.Assert(partialTypeToFullNameMap is not null); Debug2.Assert(typeToFullNameMap is not null); Debug2.Assert(lowerCaseNsToReal is not null); Debug2.Assert(namespaces is not null); Debug.Assert(resourceName.EndsWith(".baml", StringComparison.OrdinalIgnoreCase)); var name = resourceName.Substring(0, resourceName.Length - ".baml".Length); var nameNoExt = name; name = name.Replace('/', '.'); typeFullName = GetFullName(name) ?? string.Empty; if (!string.IsNullOrEmpty(typeFullName)) return filenameCreator.Create(".xaml", typeFullName); return GetBamlResourceName(nameNoExt + ".xaml"); } string? GetFullName(string partialName) { Debug2.Assert(partialTypeToFullNameMap is not null); Debug2.Assert(typeToFullNameMap is not null); var name = partialName; if (!string.IsNullOrEmpty(filenameCreator.DefaultNamespace)) name = filenameCreator.DefaultNamespace + "." + name; if (typeToFullNameMap.TryGetValue(name, out var fullName)) return fullName; partialTypeToFullNameMap.TryGetValue(partialName, out fullName); return fullName; } public string GetResourceFilename(string resourceName) { if (namespaces is null) Initialize(); Debug2.Assert(partialNamespaceMap is not null); Debug2.Assert(partialTypeToFullNameMap is not null); Debug2.Assert(typeToFullNameMap is not null); Debug2.Assert(lowerCaseNsToReal is not null); Debug2.Assert(namespaces is not null); string[] parts = resourceName.Split(new char[] { '.' }); var possibleNamespaces = new List(parts.Length); var sb = new StringBuilder(resourceName.Length); for (int i = 0; i < parts.Length - 1; i++) { if (sb.Length > 0) sb.Append("."); sb.Append(parts[i]); var ns = sb.ToString(); lowerCaseNsToReal.TryGetValue(ns, out var realNs); possibleNamespaces.Add(realNs ?? ns); } for (int i = possibleNamespaces.Count - 1; i >= 0; i--) { var ns = possibleNamespaces[i]; if (namespaces.Contains(ns)) { var filename = resourceName.Substring(ns.Length + 1); return filenameCreator.CreateFromNamespaceFilename(ns, filename); } } return filenameCreator.CreateName(resourceName); } void Initialize() { if (namespaces is not null) return; // Only include actual used namespaces, eg. if "ns1.ns2.Type1" is used, but there's no // "ns1.TypeX", don't include "ns1" as a valid namespace. var hash = new HashSet(module.Types.Select(a => UTF8String.ToSystemStringOrEmpty(a.Namespace))); hash.Remove(string.Empty); var sb = new StringBuilder(); var dict = new Dictionary(StringComparer.InvariantCultureIgnoreCase); foreach (var ns in hash) { sb.Clear(); foreach (var n in ns.Split('.')) { if (sb.Length > 0) sb.Append('.'); sb.Append(n); var ns2 = sb.ToString(); dict[ns2] = ns2; } } // XAML resources only include the last part of types, eg. BaseNS.NS1.Type1 => ns1/type1 var pmap2 = new Dictionary(StringComparer.InvariantCultureIgnoreCase); var pmap3 = new Dictionary(StringComparer.InvariantCultureIgnoreCase); var pnsmap = new Dictionary(StringComparer.InvariantCultureIgnoreCase); var stringCache = new Dictionary(StringComparer.Ordinal); // Don't include nested types foreach (var t in module.Types) { string fullName; var nsu = t.Namespace; if (UTF8String.IsNullOrEmpty(nsu)) fullName = (t.Name ?? UTF8String.Empty).String; else fullName = nsu!.String + "." + (t.Name ?? UTF8String.Empty).String; pmap3[fullName] = fullName; var name = fullName; while (name.Length > 0) { pmap2[name] = fullName; int index = name.IndexOf('.'); if (index < 0) break; name = name.Substring(index + 1); } var ns = (t.Namespace ?? UTF8String.Empty).String; while (ns.Length > 0) { if (stringCache.TryGetValue(ns, out var tmp)) ns = tmp; else stringCache[ns] = ns; pnsmap[ns] = ns; int index = ns.IndexOf('.'); if (index < 0) break; ns = ns.Substring(index + 1); } } partialNamespaceMap = pnsmap; partialTypeToFullNameMap = pmap2; typeToFullNameMap = pmap3; lowerCaseNsToReal = dict; namespaces = hash; } HashSet? namespaces; Dictionary? lowerCaseNsToReal; Dictionary? partialNamespaceMap; Dictionary? partialTypeToFullNameMap; Dictionary? typeToFullNameMap; } }