// Copyright (c) 2016 Daniel Grunwald // // 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; using System.Collections.Generic; using System.IO; using System.Linq; using System.Resources; using System.Threading.Tasks; using System.Xml; using ICSharpCode.Decompiler.CSharp.OutputVisitor; using ICSharpCode.Decompiler.CSharp.Syntax; using ICSharpCode.Decompiler.CSharp.Transforms; using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.Decompiler.Util; using Mono.Cecil; using System.Threading; namespace ICSharpCode.Decompiler.CSharp { /// /// Decompiles an assembly into a visual studio project file. /// public class WholeProjectDecompiler { #region Settings DecompilerSettings settings = new DecompilerSettings(); public DecompilerSettings Settings { get { return settings; } set { if (value == null) throw new ArgumentNullException(); settings = value; } } /// /// The MSBuild ProjectGuid to use for the new project. /// null to automatically generate a new GUID. /// public Guid? ProjectGuid { get; set; } public int MaxDegreeOfParallelism { get; set; } = Environment.ProcessorCount; #endregion // per-run members HashSet directories = new HashSet(Platform.FileNameComparer); /// /// The target directory that the decompiled files are written to. /// /// /// This field is set by DecompileProject() and protected so that overridden protected members /// can access it. /// protected string targetDirectory; public void DecompileProject(ModuleDefinition moduleDefinition, string targetDirectory, CancellationToken cancellationToken = default(CancellationToken)) { string projectFileName = Path.Combine(targetDirectory, CleanUpFileName(moduleDefinition.Assembly.Name.Name) + ".csproj"); using (var writer = new StreamWriter(projectFileName)) { DecompileProject(moduleDefinition, targetDirectory, writer, cancellationToken); } } public void DecompileProject(ModuleDefinition moduleDefinition, string targetDirectory, TextWriter projectFileWriter, CancellationToken cancellationToken = default(CancellationToken)) { if (string.IsNullOrEmpty(targetDirectory)) { throw new InvalidOperationException("Must set TargetDirectory"); } this.targetDirectory = targetDirectory; directories.Clear(); var files = WriteCodeFilesInProject(moduleDefinition, cancellationToken).ToList(); files.AddRange(WriteResourceFilesInProject(moduleDefinition)); WriteProjectFile(projectFileWriter, files, moduleDefinition); } enum LanguageTargets { None, Portable } #region WriteProjectFile void WriteProjectFile(TextWriter writer, IEnumerable> files, ModuleDefinition module) { const string ns = "http://schemas.microsoft.com/developer/msbuild/2003"; string platformName = GetPlatformName(module); Guid guid = this.ProjectGuid ?? Guid.NewGuid(); using (XmlTextWriter w = new XmlTextWriter(writer)) { w.Formatting = Formatting.Indented; w.WriteStartDocument(); w.WriteStartElement("Project", ns); w.WriteAttributeString("ToolsVersion", "4.0"); w.WriteAttributeString("DefaultTargets", "Build"); w.WriteStartElement("PropertyGroup"); w.WriteElementString("ProjectGuid", guid.ToString("B").ToUpperInvariant()); w.WriteStartElement("Configuration"); w.WriteAttributeString("Condition", " '$(Configuration)' == '' "); w.WriteValue("Debug"); w.WriteEndElement(); // w.WriteStartElement("Platform"); w.WriteAttributeString("Condition", " '$(Platform)' == '' "); w.WriteValue(platformName); w.WriteEndElement(); // switch (module.Kind) { case ModuleKind.Windows: w.WriteElementString("OutputType", "WinExe"); break; case ModuleKind.Console: w.WriteElementString("OutputType", "Exe"); break; default: w.WriteElementString("OutputType", "Library"); break; } w.WriteElementString("AssemblyName", module.Assembly.Name.Name); bool useTargetFrameworkAttribute = false; LanguageTargets languageTargets = LanguageTargets.None; var targetFrameworkAttribute = module.Assembly.CustomAttributes.FirstOrDefault(a => a.AttributeType.FullName == "System.Runtime.Versioning.TargetFrameworkAttribute"); if (targetFrameworkAttribute != null && targetFrameworkAttribute.ConstructorArguments.Any()) { string frameworkName = (string)targetFrameworkAttribute.ConstructorArguments[0].Value; string[] frameworkParts = frameworkName.Split(','); string frameworkIdentifier = frameworkParts.FirstOrDefault(a => !a.StartsWith("Version=", StringComparison.OrdinalIgnoreCase) && !a.StartsWith("Profile=", StringComparison.OrdinalIgnoreCase)); if (frameworkIdentifier != null) { w.WriteElementString("TargetFrameworkIdentifier", frameworkIdentifier); switch (frameworkIdentifier) { case ".NETPortable": languageTargets = LanguageTargets.Portable; break; } } string frameworkVersion = frameworkParts.FirstOrDefault(a => a.StartsWith("Version=", StringComparison.OrdinalIgnoreCase)); if (frameworkVersion != null) { w.WriteElementString("TargetFrameworkVersion", frameworkVersion.Substring("Version=".Length)); useTargetFrameworkAttribute = true; } string frameworkProfile = frameworkParts.FirstOrDefault(a => a.StartsWith("Profile=", StringComparison.OrdinalIgnoreCase)); if (frameworkProfile != null) w.WriteElementString("TargetFrameworkProfile", frameworkProfile.Substring("Profile=".Length)); } if (!useTargetFrameworkAttribute) { switch (module.Runtime) { case TargetRuntime.Net_1_0: w.WriteElementString("TargetFrameworkVersion", "v1.0"); break; case TargetRuntime.Net_1_1: w.WriteElementString("TargetFrameworkVersion", "v1.1"); break; case TargetRuntime.Net_2_0: w.WriteElementString("TargetFrameworkVersion", "v2.0"); // TODO: Detect when .NET 3.0/3.5 is required break; default: w.WriteElementString("TargetFrameworkVersion", "v4.0"); break; } } w.WriteElementString("WarningLevel", "4"); w.WriteElementString("AllowUnsafeBlocks", "True"); w.WriteEndElement(); // w.WriteStartElement("PropertyGroup"); // platform-specific w.WriteAttributeString("Condition", " '$(Platform)' == '" + platformName + "' "); w.WriteElementString("PlatformTarget", platformName); w.WriteEndElement(); // (platform-specific) w.WriteStartElement("PropertyGroup"); // Debug w.WriteAttributeString("Condition", " '$(Configuration)' == 'Debug' "); w.WriteElementString("OutputPath", "bin\\Debug\\"); w.WriteElementString("DebugSymbols", "true"); w.WriteElementString("DebugType", "full"); w.WriteElementString("Optimize", "false"); w.WriteEndElement(); // (Debug) w.WriteStartElement("PropertyGroup"); // Release w.WriteAttributeString("Condition", " '$(Configuration)' == 'Release' "); w.WriteElementString("OutputPath", "bin\\Release\\"); w.WriteElementString("DebugSymbols", "true"); w.WriteElementString("DebugType", "pdbonly"); w.WriteElementString("Optimize", "true"); w.WriteEndElement(); // (Release) w.WriteStartElement("ItemGroup"); // References foreach (AssemblyNameReference r in module.AssemblyReferences) { if (r.Name != "mscorlib") { w.WriteStartElement("Reference"); w.WriteAttributeString("Include", r.Name); var asm = module.AssemblyResolver.Resolve(r); if (!IsGacAssembly(r, asm)) { if (asm != null) { w.WriteElementString("HintPath", asm.MainModule.FileName); } } w.WriteEndElement(); } } w.WriteEndElement(); // (References) foreach (IGrouping gr in (from f in files group f.Item2 by f.Item1 into g orderby g.Key select g)) { w.WriteStartElement("ItemGroup"); foreach (string file in gr.OrderBy(f => f, StringComparer.OrdinalIgnoreCase)) { w.WriteStartElement(gr.Key); w.WriteAttributeString("Include", file); w.WriteEndElement(); } w.WriteEndElement(); } switch (languageTargets) { case LanguageTargets.Portable: w.WriteStartElement("Import"); w.WriteAttributeString("Project", "$(MSBuildExtensionsPath32)\\Microsoft\\Portable\\$(TargetFrameworkVersion)\\Microsoft.Portable.CSharp.targets"); w.WriteEndElement(); break; default: w.WriteStartElement("Import"); w.WriteAttributeString("Project", "$(MSBuildToolsPath)\\Microsoft.CSharp.targets"); w.WriteEndElement(); break; } w.WriteEndDocument(); } } protected virtual bool IsGacAssembly(AssemblyNameReference r, AssemblyDefinition asm) { return false; } #endregion #region WriteCodeFilesInProject protected virtual bool IncludeTypeWhenDecompilingProject(TypeDefinition type) { if (type.Name == "" || CSharpDecompiler.MemberIsHidden(type, settings)) return false; if (type.Namespace == "XamlGeneratedNamespace" && type.Name == "GeneratedInternalTypeHelper") return false; return true; } CSharpDecompiler CreateDecompiler(DecompilerTypeSystem ts) { var decompiler = new CSharpDecompiler(ts, settings); decompiler.AstTransforms.Add(new EscapeInvalidIdentifiers()); decompiler.AstTransforms.Add(new RemoveCLSCompliantAttribute()); return decompiler; } IEnumerable> WriteAssemblyInfo(DecompilerTypeSystem ts, CancellationToken cancellationToken) { var decompiler = CreateDecompiler(ts); decompiler.CancellationToken = cancellationToken; decompiler.AstTransforms.Add(new RemoveCompilerGeneratedAssemblyAttributes()); SyntaxTree syntaxTree = decompiler.DecompileModuleAndAssemblyAttributes(); const string prop = "Properties"; if (directories.Add(prop)) Directory.CreateDirectory(Path.Combine(targetDirectory, prop)); string assemblyInfo = Path.Combine(prop, "AssemblyInfo.cs"); using (StreamWriter w = new StreamWriter(Path.Combine(targetDirectory, assemblyInfo))) { syntaxTree.AcceptVisitor(new CSharpOutputVisitor(w, settings.CSharpFormattingOptions)); } return new Tuple[] { Tuple.Create("Compile", assemblyInfo) }; } IEnumerable> WriteCodeFilesInProject(ModuleDefinition module, CancellationToken cancellationToken) { var files = module.Types.Where(IncludeTypeWhenDecompilingProject).GroupBy( delegate (TypeDefinition type) { string file = CleanUpFileName(type.Name) + ".cs"; if (string.IsNullOrEmpty(type.Namespace)) { return file; } else { string dir = CleanUpFileName(type.Namespace); if (directories.Add(dir)) Directory.CreateDirectory(Path.Combine(targetDirectory, dir)); return Path.Combine(dir, file); } }, StringComparer.OrdinalIgnoreCase).ToList(); DecompilerTypeSystem ts = new DecompilerTypeSystem(module); Parallel.ForEach( files, new ParallelOptions { MaxDegreeOfParallelism = this.MaxDegreeOfParallelism, CancellationToken = cancellationToken }, delegate (IGrouping file) { using (StreamWriter w = new StreamWriter(Path.Combine(targetDirectory, file.Key))) { CSharpDecompiler decompiler = CreateDecompiler(ts); decompiler.CancellationToken = cancellationToken; var syntaxTree = decompiler.DecompileTypes(file.ToArray()); syntaxTree.AcceptVisitor(new CSharpOutputVisitor(w, settings.CSharpFormattingOptions)); } }); return files.Select(f => Tuple.Create("Compile", f.Key)).Concat(WriteAssemblyInfo(ts, cancellationToken)); } #endregion #region WriteResourceFilesInProject protected virtual IEnumerable> WriteResourceFilesInProject(ModuleDefinition module) { foreach (EmbeddedResource r in module.Resources.OfType()) { Stream stream = r.GetResourceStream(); stream.Position = 0; IEnumerable entries; if (r.Name.EndsWith(".resources", StringComparison.OrdinalIgnoreCase)) { if (GetEntries(stream, out entries) && entries.All(e => e.Value is Stream)) { foreach (var pair in entries) { string fileName = Path.Combine(((string)pair.Key).Split('/').Select(p => CleanUpFileName(p)).ToArray()); string dirName = Path.GetDirectoryName(fileName); if (!string.IsNullOrEmpty(dirName) && directories.Add(dirName)) { Directory.CreateDirectory(Path.Combine(targetDirectory, dirName)); } Stream entryStream = (Stream)pair.Value; entryStream.Position = 0; WriteResourceToFile(Path.Combine(targetDirectory, fileName), (string)pair.Key, entryStream); } } else { stream.Position = 0; string fileName = GetFileNameForResource(Path.ChangeExtension(r.Name, ".resource")); WriteResourceToFile(fileName, r.Name, stream); } } else { string fileName = GetFileNameForResource(r.Name); using (FileStream fs = new FileStream(Path.Combine(targetDirectory, fileName), FileMode.Create, FileAccess.Write)) { stream.Position = 0; stream.CopyTo(fs); } yield return Tuple.Create("EmbeddedResource", fileName); } } } protected virtual IEnumerable> WriteResourceToFile(string fileName, string resourceName, Stream entryStream) { using (FileStream fs = new FileStream(fileName, FileMode.Create, FileAccess.Write)) { entryStream.CopyTo(fs); } yield return Tuple.Create("EmbeddedResource", fileName); } string GetFileNameForResource(string fullName) { string[] splitName = fullName.Split('.'); string fileName = CleanUpFileName(fullName); for (int i = splitName.Length - 1; i > 0; i--) { string ns = string.Join(".", splitName, 0, i); if (directories.Contains(ns)) { string name = string.Join(".", splitName, i, splitName.Length - i); fileName = Path.Combine(ns, CleanUpFileName(name)); break; } } return fileName; } bool GetEntries(Stream stream, out IEnumerable entries) { try { entries = new ResourceSet(stream).Cast(); return true; } catch (ArgumentException) { entries = null; return false; } } #endregion /// /// Cleans up a node name for use as a file name. /// public static string CleanUpFileName(string text) { int pos = text.IndexOf(':'); if (pos > 0) text = text.Substring(0, pos); pos = text.IndexOf('`'); if (pos > 0) text = text.Substring(0, pos); text = text.Trim(); foreach (char c in Path.GetInvalidFileNameChars()) text = text.Replace(c, '-'); return text; } public static string GetPlatformName(ModuleDefinition module) { switch (module.Architecture) { case TargetArchitecture.I386: if ((module.Attributes & ModuleAttributes.Preferred32Bit) == ModuleAttributes.Preferred32Bit) return "AnyCPU"; else if ((module.Attributes & ModuleAttributes.Required32Bit) == ModuleAttributes.Required32Bit) return "x86"; else return "AnyCPU"; case TargetArchitecture.AMD64: return "x64"; case TargetArchitecture.IA64: return "Itanium"; default: return module.Architecture.ToString(); } } } }