// Copyright (c) 2020 Siegfried Pammer // // Permission is hereby granted, free of charge, to any person obtaining a copy of this // software and associated documentation files (the "Software"), to deal in the Software // without restriction, including without limitation the rights to use, copy, modify, merge, // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons // to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection.PortableExecutable; using System.Xml; using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.Util; namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler { /// /// A implementation that creates the projects in the SDK style format. /// sealed class ProjectFileWriterSdkStyle : IProjectFileWriter { const string AspNetCorePrefix = "Microsoft.AspNetCore"; const string PresentationFrameworkName = "PresentationFramework"; const string WindowsFormsName = "System.Windows.Forms"; const string TrueString = "True"; const string FalseString = "False"; const string AnyCpuString = "AnyCPU"; static readonly HashSet ImplicitReferences = new HashSet { "mscorlib", "netstandard", "PresentationFramework", "System", "System.Diagnostics.Debug", "System.Diagnostics.Tools", "System.Drawing", "System.Runtime", "System.Runtime.Extensions", "System.Windows.Forms", "System.Xaml", }; enum ProjectType { Default, WinForms, Wpf, Web } /// /// Creates a new instance of the class. /// /// A new instance of the class. public static IProjectFileWriter Create() => new ProjectFileWriterSdkStyle(); /// public void Write( TextWriter target, IProjectInfoProvider project, IEnumerable files, MetadataFile module) { using (XmlTextWriter xmlWriter = new XmlTextWriter(target)) { xmlWriter.Formatting = Formatting.Indented; Write(xmlWriter, project, files, module); } } static void Write(XmlTextWriter xml, IProjectInfoProvider project, IEnumerable files, MetadataFile module) { xml.WriteStartElement("Project"); var projectType = GetProjectType(module); xml.WriteAttributeString("Sdk", GetSdkString(projectType)); PlaceIntoTag("PropertyGroup", xml, () => WriteAssemblyInfo(xml, module, project, projectType)); PlaceIntoTag("PropertyGroup", xml, () => WriteProjectInfo(xml, project)); PlaceIntoTag("PropertyGroup", xml, () => WriteMiscellaneousPropertyGroup(xml, files)); PlaceIntoTag("ItemGroup", xml, () => WriteResources(xml, files)); PlaceIntoTag("ItemGroup", xml, () => WriteReferences(xml, module, project, projectType)); xml.WriteEndElement(); } static void PlaceIntoTag(string tagName, XmlTextWriter xml, Action content) { xml.WriteStartElement(tagName); try { content(); } finally { xml.WriteEndElement(); } } static void WriteAssemblyInfo(XmlTextWriter xml, MetadataFile module, IProjectInfoProvider project, ProjectType projectType) { xml.WriteElementString("AssemblyName", module.Name); // Since we create AssemblyInfo.cs manually, we need to disable the auto-generation xml.WriteElementString("GenerateAssemblyInfo", FalseString); string platformName; CorFlags flags; if (module is PEFile { Reader.PEHeaders: var headers } peFile) { WriteOutputType(xml, headers.IsDll, headers.PEHeader.Subsystem, projectType); platformName = TargetServices.GetPlatformName(peFile); flags = headers.CorHeader.Flags; } else { WriteOutputType(xml, isDll: true, Subsystem.Unknown, projectType); platformName = AnyCpuString; flags = 0; } WriteDesktopExtensions(xml, projectType); var targetFramework = TargetServices.DetectTargetFramework(module); if (targetFramework.Identifier == ".NETFramework" && targetFramework.VersionNumber == 200) targetFramework = TargetServices.DetectTargetFrameworkNET20(module, project.AssemblyResolver, targetFramework); if (targetFramework.Moniker == null) { throw new NotSupportedException($"Cannot decompile this assembly to a SDK style project. Use default project format instead."); } xml.WriteElementString("TargetFramework", targetFramework.Moniker); // 'AnyCPU' is default, so only need to specify platform if it differs if (platformName != AnyCpuString) { xml.WriteElementString("PlatformTarget", platformName); } if (platformName == AnyCpuString && (flags & CorFlags.Prefers32Bit) != 0) { xml.WriteElementString("Prefer32Bit", TrueString); } } static void WriteOutputType(XmlTextWriter xml, bool isDll, Subsystem moduleSubsystem, ProjectType projectType) { if (!isDll) { switch (moduleSubsystem) { case Subsystem.WindowsGui: xml.WriteElementString("OutputType", "WinExe"); break; case Subsystem.WindowsCui: xml.WriteElementString("OutputType", "Exe"); break; } } else { // 'Library' is default, so only need to specify output type for executables (excludes ProjectType.Web) if (projectType == ProjectType.Web) { xml.WriteElementString("OutputType", "Library"); } } } static void WriteDesktopExtensions(XmlTextWriter xml, ProjectType projectType) { if (projectType == ProjectType.Wpf) { xml.WriteElementString("UseWPF", TrueString); } else if (projectType == ProjectType.WinForms) { xml.WriteElementString("UseWindowsForms", TrueString); } } static void WriteProjectInfo(XmlTextWriter xml, IProjectInfoProvider project) { xml.WriteElementString("LangVersion", project.LanguageVersion.ToString().Replace("CSharp", "").Replace('_', '.')); xml.WriteElementString("AllowUnsafeBlocks", TrueString); if (project.StrongNameKeyFile != null) { xml.WriteElementString("SignAssembly", TrueString); xml.WriteElementString("AssemblyOriginatorKeyFile", Path.GetFileName(project.StrongNameKeyFile)); } } static void WriteMiscellaneousPropertyGroup(XmlTextWriter xml, IEnumerable files) { var (itemType, fileName) = files.FirstOrDefault(t => t.ItemType == "ApplicationIcon"); if (fileName != null) xml.WriteElementString("ApplicationIcon", fileName); (itemType, fileName) = files.FirstOrDefault(t => t.ItemType == "ApplicationManifest"); if (fileName != null) xml.WriteElementString("ApplicationManifest", fileName); if (files.Any(t => t.ItemType == "EmbeddedResource")) xml.WriteElementString("RootNamespace", string.Empty); // TODO: We should add CustomToolNamespace for resources, otherwise we should add empty RootNamespace } static void WriteResources(XmlTextWriter xml, IEnumerable files) { // remove phase foreach (var item in files.Where(t => t.ItemType == "EmbeddedResource")) { string buildAction = Path.GetExtension(item.FileName).ToUpperInvariant() switch { ".CS" => "Compile", ".RESX" => "EmbeddedResource", _ => "None" }; if (buildAction == "EmbeddedResource") continue; xml.WriteStartElement(buildAction); xml.WriteAttributeString("Remove", item.FileName); xml.WriteEndElement(); } // include phase foreach (var item in files.Where(t => t.ItemType == "EmbeddedResource")) { if (Path.GetExtension(item.FileName) == ".resx") continue; xml.WriteStartElement("EmbeddedResource"); xml.WriteAttributeString("Include", item.FileName); if (item.AdditionalProperties != null) { foreach (var (key, value) in item.AdditionalProperties) xml.WriteAttributeString(key, value); } xml.WriteEndElement(); } } static void WriteReferences(XmlTextWriter xml, MetadataFile module, IProjectInfoProvider project, ProjectType projectType) { bool isNetCoreApp = TargetServices.DetectTargetFramework(module).Identifier == ".NETCoreApp"; var targetPacks = new HashSet(); if (isNetCoreApp) { targetPacks.Add("Microsoft.NETCore.App"); switch (projectType) { case ProjectType.WinForms: case ProjectType.Wpf: targetPacks.Add("Microsoft.WindowsDesktop.App"); break; case ProjectType.Web: targetPacks.Add("Microsoft.AspNetCore.App"); targetPacks.Add("Microsoft.AspNetCore.All"); break; } } foreach (var reference in module.AssemblyReferences.Where(r => !ImplicitReferences.Contains(r.Name))) { if (isNetCoreApp && project.AssemblyReferenceClassifier.IsSharedAssembly(reference, out string runtimePack) && targetPacks.Contains(runtimePack)) { continue; } xml.WriteStartElement("Reference"); xml.WriteAttributeString("Include", reference.Name); var asembly = project.AssemblyResolver.Resolve(reference); if (asembly != null && !project.AssemblyReferenceClassifier.IsGacAssembly(reference)) { xml.WriteElementString("HintPath", FileUtility.GetRelativePath(project.TargetDirectory, asembly.FileName)); } xml.WriteEndElement(); } } static string GetSdkString(ProjectType projectType) { switch (projectType) { case ProjectType.WinForms: case ProjectType.Wpf: return "Microsoft.NET.Sdk.WindowsDesktop"; case ProjectType.Web: return "Microsoft.NET.Sdk.Web"; default: return "Microsoft.NET.Sdk"; } } static ProjectType GetProjectType(MetadataFile module) { foreach (var referenceName in module.AssemblyReferences.Select(r => r.Name)) { if (referenceName.StartsWith(AspNetCorePrefix, StringComparison.Ordinal)) { return ProjectType.Web; } if (referenceName == PresentationFrameworkName) { return ProjectType.Wpf; } if (referenceName == WindowsFormsName) { return ProjectType.WinForms; } } return ProjectType.Default; } } }