/* 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.IO; using System.Linq; using dnlib.DotNet; using dnlib.DotNet.Emit; using dnlib.DotNet.Resources; using dnlib.PE; using dnSpy.Contracts.Decompiler; using dnSpy.Decompiler.Properties; namespace dnSpy.Decompiler.MSBuild { sealed class Project { public ProjectModuleOptions Options { get; } public string DefaultNamespace { get; } public string AssemblyName { get; } public ModuleDef Module => Options.Module; public List Files { get; } public Guid Guid => Options.ProjectGuid; public Guid LanguageGuid { get; } public string Filename { get; } public string Directory { get; } public string? Platform { get; set; } public HashSet ProjectTypeGuids { get; } public HashSet ExtraAssemblyReferences { get; } public string? StartupObject { get; private set; } public bool AllowUnsafeBlocks { get; private set; } public string PropertiesFolder { get; } public ApplicationIcon? ApplicationIcon => applicationIcon; public ApplicationManifest? ApplicationManifest => applicationManifest; ApplicationIcon? applicationIcon; ApplicationManifest? applicationManifest; readonly SatelliteAssemblyFinder satelliteAssemblyFinder; readonly Func createDecompilerOutput; public Project(ProjectModuleOptions options, string projDir, SatelliteAssemblyFinder satelliteAssemblyFinder, Func createDecompilerOutput) { Options = options ?? throw new ArgumentNullException(nameof(options)); Directory = projDir; this.satelliteAssemblyFinder = satelliteAssemblyFinder; this.createDecompilerOutput = createDecompilerOutput; Files = new List(); DefaultNamespace = new DefaultNamespaceFinder(options.Module).Find(); Filename = Path.Combine(projDir, Path.GetFileName(projDir) + options.Decompiler.ProjectFileExtension); AssemblyName = options.Module.Assembly is null ? string.Empty : options.Module.Assembly.Name.String; ProjectTypeGuids = new HashSet(); PropertiesFolder = CalculatePropertiesFolder(); ExtraAssemblyReferences = new HashSet(); LanguageGuid = CalculateLanguageGuid(options.Decompiler); } static Guid CalculateLanguageGuid(IDecompiler decompiler) { if (decompiler.GenericGuid == DecompilerConstants.LANGUAGE_VISUALBASIC) return new Guid("F184B08F-C81C-45F6-A57F-5ABD9991F28F"); Debug.Assert(decompiler.GenericGuid == DecompilerConstants.LANGUAGE_CSHARP); return new Guid("FAE04EC0-301F-11D3-BF4B-00C04F79EFBC"); } string CalculatePropertiesFolder() { if (Options.Decompiler.GenericGuid == DecompilerConstants.LANGUAGE_VISUALBASIC) return "My Project"; return "Properties"; } public void CreateProjectFiles(DecompileContext ctx) { var filenameCreator = new FilenameCreator(Directory, DefaultNamespace); var resourceNameCreator = new ResourceNameCreator(Options.Module, filenameCreator); AllowUnsafeBlocks = DotNetUtils.IsUnsafe(Options.Module); InitializeSplashScreen(); if (Options.Decompiler.CanDecompile(DecompilationType.AssemblyInfo)) { var filename = filenameCreator.CreateFromRelativePath(Path.Combine(PropertiesFolder, "AssemblyInfo"), Options.Decompiler.FileExtension); Files.Add(new AssemblyInfoProjectFile(Options.Module, filename, Options.DecompilationContext, Options.Decompiler, createDecompilerOutput)); } var ep = Options.Module.EntryPoint; if (ep is not null && ep.DeclaringType is not null) StartupObject = ep.DeclaringType.ReflectionFullName; applicationManifest = ApplicationManifest.TryCreate(Options.Module.Win32Resources, filenameCreator); if (ApplicationManifest is not null) Files.Add(new ApplicationManifestProjectFile(ApplicationManifest.Filename)); foreach (var rsrc in Options.Module.Resources) { ctx.CancellationToken.ThrowIfCancellationRequested(); switch (rsrc.ResourceType) { case ResourceType.Embedded: foreach (var file in CreateEmbeddedResourceFiles(Options.Module, resourceNameCreator, (EmbeddedResource)rsrc)) { Files.Add(file); Files.AddRange(CreateSatelliteFiles(rsrc.Name, filenameCreator, file)); } break; case ResourceType.AssemblyLinked: //TODO: What should be created here? break; case ResourceType.Linked: //TODO: What should be created here? break; default: break; } } InitializeXaml(); InitializeResX(); foreach (var type in Options.Module.Types) { ctx.CancellationToken.ThrowIfCancellationRequested(); if (!DecompileType(type)) continue; Files.Add(CreateTypeProjectFile(type, filenameCreator)); } CreateEmptyAppXamlFile(); var existingAppConfig = Options.Module.Location + ".config"; if (File.Exists(existingAppConfig)) Files.Add(new AppConfigProjectFile(filenameCreator.CreateName("app.config"), existingAppConfig)); applicationIcon = ApplicationIcon.TryCreate(Options.Module.Win32Resources, Path.GetFileName(Directory), filenameCreator); var dirs = new HashSet(Files.Select(a => GetDirectoryName(a.Filename)).OfType(), StringComparer.OrdinalIgnoreCase); int errors = 0; foreach (var dir in dirs) { ctx.CancellationToken.ThrowIfCancellationRequested(); try { System.IO.Directory.CreateDirectory(dir); } catch (Exception ex) { if (errors++ < 20) ctx.Logger.Error(string.Format(dnSpy_Decompiler_Resources.MSBuild_CouldNotCreateDirectory2, dir, ex.Message)); } } } static string? GetDirectoryName(string s) { try { return Path.GetDirectoryName(s); } catch (ArgumentException) { } catch (PathTooLongException) { } return null; } void InitializeSplashScreen() { var ep = Options.Module.EntryPoint; if (ep is null || ep.Body is null) return; var instrs = ep.Body.Instructions; for (int i = 0; i + 1 < instrs.Count; i++) { var newobj = instrs[i + 1]; if (newobj.OpCode.Code != Code.Newobj) continue; var s = instrs[i].Operand as string; if (s is null) continue; var ctor = newobj.Operand as IMethod; if (ctor is null || ctor.MethodSig is null) continue; if (ctor.FullName != "System.Void System.Windows.SplashScreen::.ctor(System.String)" && ctor.FullName != "System.Void System.Windows.SplashScreen::.ctor(System.Reflection.Assembly,System.String)") continue; splashScreenImageName = s; break; } } string? splashScreenImageName; ProjectFile CreateTypeProjectFile(TypeDef type, FilenameCreator filenameCreator) { var bamlFile = TryGetBamlFile(type); if (bamlFile is not null) { var filename = filenameCreator.Create(GetTypeExtension(type), type.FullName); TypeProjectFile newFile; var isAppType = DotNetUtils.IsSystemWindowsApplication(type); if (!Options.Decompiler.CanDecompile(DecompilationType.PartialType)) newFile = new TypeProjectFile(type, filename, Options.DecompilationContext, Options.Decompiler, createDecompilerOutput); else newFile = new XamlTypeProjectFile(type, filename, Options.DecompilationContext, Options.Decompiler, createDecompilerOutput); newFile.DependentUpon = bamlFile; if (isAppType && DotNetUtils.IsStartUpClass(type)) { bamlFile.IsAppDef = true; StartupObject = null; } if (isAppType) appTypeProjFile = newFile; return newFile; } const string DESIGNER = ".Designer"; var resxFile = TryGetResXFile(type); if (DotNetUtils.IsWinForm(type)) { var fname = resxFile is not null ? Path.GetFileNameWithoutExtension(resxFile.Filename) : type.Name.String; var filename = filenameCreator.CreateFromNamespaceName(GetTypeExtension(type), type.ReflectionNamespace, fname); var dname = filenameCreator.CreateFromNamespaceName(GetTypeExtension(type), type.ReflectionNamespace, fname + DESIGNER); var newFile = new WinFormsProjectFile(type, filename, Options.DecompilationContext, Options.Decompiler, createDecompilerOutput); if (resxFile is not null) resxFile.DependentUpon = newFile; var winFormsDesignerFile = new WinFormsDesignerProjectFile(newFile, dname, createDecompilerOutput); winFormsDesignerFile.DependentUpon = newFile; Files.Add(winFormsDesignerFile); return newFile; } else if (resxFile is not null) { var filename = filenameCreator.CreateFromNamespaceName(GetTypeExtension(type), type.ReflectionNamespace, Path.GetFileNameWithoutExtension(resxFile.Filename) + DESIGNER); var newFile = new TypeProjectFile(type, filename, Options.DecompilationContext, Options.Decompiler, createDecompilerOutput); newFile.DependentUpon = resxFile; newFile.AutoGen = true; newFile.DesignTime = true; resxFile.Generator = type.IsPublic ? "PublicResXFileCodeGenerator" : "ResXFileCodeGenerator"; resxFile.LastGenOutput = newFile; return newFile; } var bt = type.BaseType; if (bt is not null && bt.FullName == "System.Configuration.ApplicationSettingsBase") { var designerFilename = filenameCreator.Create(DESIGNER + GetTypeExtension(type), type.FullName); var settingsFilename = filenameCreator.Create(".settings", type.FullName); ProjectFile designerTypeFile; if (Options.Decompiler.CanDecompile(DecompilationType.PartialType)) { var typeFilename = filenameCreator.Create(GetTypeExtension(type), type.FullName); var settingsTypeFile = new SettingsTypeProjectFile(type, typeFilename, Options.DecompilationContext, Options.Decompiler, createDecompilerOutput); designerTypeFile = new SettingsDesignerTypeProjectFile(settingsTypeFile, designerFilename, createDecompilerOutput); Files.Add(settingsTypeFile); } else designerTypeFile = new TypeProjectFile(type, designerFilename, Options.DecompilationContext, Options.Decompiler, createDecompilerOutput); var settingsFile = new SettingsProjectFile(type, settingsFilename); designerTypeFile.DependentUpon = settingsFile; designerTypeFile.AutoGen = true; designerTypeFile.DesignTimeSharedInput = true; settingsFile.Generator = type.IsPublic ? "PublicSettingsSingleFileGenerator" : "SettingsSingleFileGenerator"; settingsFile.LastGenOutput = designerTypeFile; Files.Add(settingsFile); return designerTypeFile; } var newFilename = filenameCreator.Create(GetTypeExtension(type), type.FullName); return new TypeProjectFile(type, newFilename, Options.DecompilationContext, Options.Decompiler, createDecompilerOutput); } void CreateEmptyAppXamlFile() { if (!hasXamlClasses || appTypeProjFile is not null) return; if ((Options.Module.Characteristics & Characteristics.Dll) != 0) return; var file = Files.OfType().Where(a => DotNetUtils.IsSystemWindowsApplication(a.Type)).FirstOrDefault(); Debug2.Assert(file is not null); if (file is null) return; Debug2.Assert(file.DependentUpon is null); if (file.DependentUpon is not null) return; Files.Remove(file); var filename = file.Filename; var name = Path.GetFileNameWithoutExtension(file.Filename); filename = Path.Combine(Path.GetDirectoryName(filename)!, name + ".xaml"); var newFile = new XamlTypeProjectFile(file.Type, filename + Options.Decompiler.FileExtension, Options.DecompilationContext, Options.Decompiler, createDecompilerOutput); Files.Add(newFile); var bamlFile = new AppBamlResourceProjectFile(filename, file.Type, Options.Decompiler); newFile.DependentUpon = bamlFile; Files.Add(bamlFile); } TypeProjectFile? appTypeProjFile; void InitializeXaml() { typeFullNameToBamlFile = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var xamlFile in Files.OfType()) { hasXamlClasses = true; if (!string.IsNullOrEmpty(xamlFile.TypeFullName) && !xamlFile.IsSatelliteFile) typeFullNameToBamlFile[xamlFile.TypeFullName] = xamlFile; } if (hasXamlClasses) { ExtraAssemblyReferences.Add("WindowsBase"); ExtraAssemblyReferences.Add("PresentationCore"); ExtraAssemblyReferences.Add("PresentationFramework"); if (!Options.Module.IsClr1x && !Options.Module.IsClr20) ExtraAssemblyReferences.Add("System.Xaml"); } if (hasXamlClasses || ReferencesWPFClasses()) { ProjectTypeGuids.Add(new Guid("60DC8134-EBA5-43B8-BCC9-BB4BC16C2548")); if (Options.Decompiler.GenericGuid == DecompilerConstants.LANGUAGE_VISUALBASIC) ProjectTypeGuids.Add(new Guid("F184B08F-C81C-45F6-A57F-5ABD9991F28F")); else if (Options.Decompiler.GenericGuid == DecompilerConstants.LANGUAGE_CSHARP) ProjectTypeGuids.Add(new Guid("FAE04EC0-301F-11D3-BF4B-00C04F79EFBC")); } } Dictionary? typeFullNameToBamlFile; bool hasXamlClasses; bool ReferencesWPFClasses() { foreach (var asmRef in Options.Module.GetAssemblyRefs()) { switch (asmRef.Name) { case "WindowsBase": case "PresentationCore": case "PresentationFramework": return true; } } return false; } BamlResourceProjectFile? TryGetBamlFile(TypeDef type) { Debug2.Assert(typeFullNameToBamlFile is not null); typeFullNameToBamlFile.TryGetValue(type.FullName, out var bamlFile); return bamlFile; } void InitializeResX() { typeFullNameToResXFile = new Dictionary(StringComparer.Ordinal); foreach (var resxFile in Files.OfType()) { if (!string.IsNullOrEmpty(resxFile.TypeFullName) && !resxFile.IsSatelliteFile) typeFullNameToResXFile[resxFile.TypeFullName] = resxFile; } } Dictionary? typeFullNameToResXFile; ResXProjectFile? TryGetResXFile(TypeDef type) { Debug2.Assert(typeFullNameToResXFile is not null); typeFullNameToResXFile.TryGetValue(type.FullName, out var resxFile); return resxFile; } string GetTypeExtension(TypeDef type) { Debug2.Assert(typeFullNameToBamlFile is not null); if (typeFullNameToBamlFile.TryGetValue(type.FullName, out var bamlFile)) return ".xaml" + Options.Decompiler.FileExtension; return Options.Decompiler.FileExtension; } IEnumerable CreateEmbeddedResourceFiles(ModuleDef module, ResourceNameCreator resourceNameCreator, EmbeddedResource er) { if (!Options.UnpackResources) { yield return CreateRawEmbeddedResourceProjectFile(module, resourceNameCreator, er); yield break; } if (ResourceReader.CouldBeResourcesFile(er.CreateReader())) { var files = TryCreateResourceFiles(module, resourceNameCreator, er); if (files is not null) { foreach (var file in files) yield return file; yield break; } } yield return CreateRawEmbeddedResourceProjectFile(module, resourceNameCreator, er); } List? TryCreateResourceFiles(ModuleDef module, ResourceNameCreator resourceNameCreator, EmbeddedResource er) { ResourceElementSet set; try { set = ResourceReader.Read(module, er.CreateReader()); } catch { return null; } if (IsXamlResource(module, er.Name, set)) return CreateXamlResourceFiles(module, resourceNameCreator, set).ToList(); if (Options.CreateResX) { string filename = resourceNameCreator.GetResxFilename(er.Name, out string typeFullName); return new List() { CreateResXFile(module, er, set, filename, typeFullName, false) }; } return null; } bool IsXamlResource(ModuleDef module, string name, ResourceElementSet set) { var asm = module.Assembly; if (asm is null || !module.IsManifestModule) return false; string culture = UTF8String.IsNullOrEmpty(asm.Culture) ? string.Empty : "." + asm.Culture; if (!StringComparer.OrdinalIgnoreCase.Equals(asm.Name + ".g" + culture + ".resources", name)) return false; var elems = set.ResourceElements.ToArray(); if (elems.Length == 0) return false; foreach (var e in elems) { if (!(e.ResourceData.Code == ResourceTypeCode.ByteArray || e.ResourceData.Code == ResourceTypeCode.Stream)) return false; } return true; } IEnumerable CreateXamlResourceFiles(ModuleDef module, ResourceNameCreator resourceNameCreator, ResourceElementSet set) { bool decompileBaml = Options.DecompileXaml && Options.DecompileBaml is not null; foreach (var e in set.ResourceElements) { Debug.Assert(e.ResourceData.Code == ResourceTypeCode.ByteArray || e.ResourceData.Code == ResourceTypeCode.Stream); var data = (byte[])((BuiltInResourceData)e.ResourceData).Data; var rsrcName = Uri.UnescapeDataString(e.Name); if (decompileBaml && rsrcName.EndsWith(".baml", StringComparison.OrdinalIgnoreCase)) { var filename = resourceNameCreator.GetBamlResourceName(rsrcName, out string typeFullName); yield return new BamlResourceProjectFile(filename, data, typeFullName, (bamlData, stream) => Options.DecompileBaml!(module, bamlData, Options.DecompilationContext.CancellationToken, stream)); } else if (StringComparer.InvariantCultureIgnoreCase.Equals(splashScreenImageName, e.Name)) { var filename = resourceNameCreator.GetXamlResourceFilename(rsrcName); yield return new SplashScreenProjectFile(filename, data, e.Name); } else { var filename = resourceNameCreator.GetXamlResourceFilename(rsrcName); yield return new ResourceProjectFile(filename, data, e.Name); } } } ResXProjectFile CreateResXFile(ModuleDef module, EmbeddedResource er, ResourceElementSet set, string filename, string typeFullName, bool isSatellite) { Debug.Assert(Options.CreateResX); if (!Options.CreateResX) throw new InvalidOperationException(); return new ResXProjectFile(module, filename, typeFullName, er) { IsSatelliteFile = isSatellite, }; } RawEmbeddedResourceProjectFile CreateRawEmbeddedResourceProjectFile(ModuleDef module, ResourceNameCreator resourceNameCreator, EmbeddedResource er) => new RawEmbeddedResourceProjectFile(resourceNameCreator.GetResourceFilename(er.Name), er); bool DecompileType(TypeDef type) { if (!Options.Decompiler.ShowMember(type)) return false; if (type.IsGlobalModuleType && type.Methods.Count == 0 && type.Fields.Count == 0 && type.Properties.Count == 0 && type.Events.Count == 0 && type.NestedTypes.Count == 0) { return false; } if (type.Namespace == "XamlGeneratedNamespace" && type.Name == "GeneratedInternalTypeHelper") return false; return true; } IEnumerable CreateSatelliteFiles(string rsrcName, FilenameCreator filenameCreator, ProjectFile nonSatFile) { foreach (var satMod in satelliteAssemblyFinder.GetSatelliteAssemblies(Options.Module)) { var satFile = TryCreateSatelliteFile(satMod, rsrcName, filenameCreator, nonSatFile); if (satFile is not null) yield return satFile; } } ProjectFile? TryCreateSatelliteFile(ModuleDef module, string rsrcName, FilenameCreator filenameCreator, ProjectFile nonSatFile) { if (!Options.CreateResX) return null; var asm = module.Assembly; Debug2.Assert(asm is not null && !UTF8String.IsNullOrEmpty(asm.Culture)); if (asm is null || UTF8String.IsNullOrEmpty(asm.Culture)) return null; var name = FileUtils.RemoveExtension(rsrcName); var ext = FileUtils.GetExtension(rsrcName); var locName = name + "." + asm.Culture + ext; var er = module.Resources.OfType().FirstOrDefault(a => StringComparer.Ordinal.Equals(a.Name, locName)); var set = TryCreateResourceElementSet(module, er); if (set is null) return null; Debug2.Assert(er is not null); var dirName = Path.GetDirectoryName(nonSatFile.Filename)!; var dir = Directory.Length + 1 > dirName.Length ? string.Empty : dirName.Substring(Directory.Length + 1); name = Path.GetFileNameWithoutExtension(nonSatFile.Filename); ext = Path.GetExtension(nonSatFile.Filename); var filename = filenameCreator.CreateFromRelativePath(Path.Combine(dir, name) + "." + asm.Culture, ext); return CreateResXFile(module, er, set, filename, string.Empty, true); } static ResourceElementSet? TryCreateResourceElementSet(ModuleDef module, EmbeddedResource? er) { if (er is null) return null; if (!ResourceReader.CouldBeResourcesFile(er.CreateReader())) return null; try { return ResourceReader.Read(module, er.CreateReader()); } catch { return null; } } public IEnumerable GetJobs() { if (ApplicationIcon is not null) yield return ApplicationIcon; if (ApplicationManifest is not null) yield return ApplicationManifest; foreach (var f in Files) yield return f; } public void OnWrite() { string asmName = Options.Module.Assembly is not null && Options.Module.IsManifestModule ? Options.Module.Assembly.Name : null; foreach (var bamlFile in Files.OfType()) { foreach (var asmRef in bamlFile.AssemblyReferences) { if (asmName is not null && !StringComparer.Ordinal.Equals(asmName, asmRef.Name)) ExtraAssemblyReferences.Add(asmRef.Name); } } } } }