From 275f0f6d21c0c077a9ae46d7850a3c1dee9c84a0 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Sun, 20 Feb 2011 15:21:53 +0100 Subject: [PATCH] Implemented "Save Assembly as C# Project" --- .../Ast/AstMethodBodyBuilder.cs | 26 +++ .../ICSharpCode.Decompiler.csproj | 1 + ICSharpCode.Decompiler/ITextOutput.cs | 1 + ICSharpCode.Decompiler/TextOutputWriter.cs | 55 ++++++ ILSpy/CSharpLanguage.cs | 173 +++++++++++++++++- ILSpy/DecompilationOptions.cs | 5 + ILSpy/Language.cs | 4 + ILSpy/MainWindow.xaml.cs | 2 +- ILSpy/TextView/DecompilerTextView.cs | 8 + ILSpy/TreeNodes/AssemblyTreeNode.cs | 33 ++++ ILSpy/TreeNodes/ILSpyTreeNode.cs | 2 +- ILSpy/TreeNodes/ResourceEntryNode.cs | 4 +- ILSpy/TreeNodes/ResourceListTreeNode.cs | 4 +- 13 files changed, 307 insertions(+), 11 deletions(-) create mode 100644 ICSharpCode.Decompiler/TextOutputWriter.cs diff --git a/ICSharpCode.Decompiler/Ast/AstMethodBodyBuilder.cs b/ICSharpCode.Decompiler/Ast/AstMethodBodyBuilder.cs index 203a68a72..9351fd969 100644 --- a/ICSharpCode.Decompiler/Ast/AstMethodBodyBuilder.cs +++ b/ICSharpCode.Decompiler/Ast/AstMethodBodyBuilder.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -690,8 +691,33 @@ namespace Decompiler return target.Invoke(cecilMethod.Name, ConvertTypeArguments(cecilMethod), methodArgs).WithAnnotation(cecilMethod); } + #if DEBUG + static readonly ConcurrentDictionary unhandledOpcodes = new ConcurrentDictionary(); + #endif + + [Conditional("DEBUG")] + public static void ClearUnhandledOpcodes() + { + #if DEBUG + unhandledOpcodes.Clear(); + #endif + } + + [Conditional("DEBUG")] + public static void PrintNumberOfUnhandledOpcodes() + { + #if DEBUG + foreach (var pair in unhandledOpcodes) { + Debug.WriteLine("AddMethodBodyBuilder unhandled opcode: {1}x {0}", pair.Key, pair.Value); + } + #endif + } + static Expression InlineAssembly(ILExpression byteCode, List args) { + #if DEBUG + unhandledOpcodes.AddOrUpdate(byteCode.Code, c => 1, (c, n) => n+1); + #endif // Output the operand of the unknown IL code as well if (byteCode.Operand != null) { args.Insert(0, new IdentifierExpression(FormatByteCodeOperand(byteCode.Operand))); diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index 8a541de43..d96ec99e9 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj @@ -97,6 +97,7 @@ + diff --git a/ICSharpCode.Decompiler/ITextOutput.cs b/ICSharpCode.Decompiler/ITextOutput.cs index f862d5f0a..b4374d50b 100644 --- a/ICSharpCode.Decompiler/ITextOutput.cs +++ b/ICSharpCode.Decompiler/ITextOutput.cs @@ -17,6 +17,7 @@ // DEALINGS IN THE SOFTWARE. using System; +using System.IO; namespace ICSharpCode.Decompiler { diff --git a/ICSharpCode.Decompiler/TextOutputWriter.cs b/ICSharpCode.Decompiler/TextOutputWriter.cs new file mode 100644 index 000000000..964187a31 --- /dev/null +++ b/ICSharpCode.Decompiler/TextOutputWriter.cs @@ -0,0 +1,55 @@ +// Copyright (c) 2011 AlphaSierraPapa for the SharpDevelop Team +// +// 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.IO; +using System.Text; + +namespace ICSharpCode.Decompiler +{ + public class TextOutputWriter : TextWriter + { + readonly ITextOutput output; + + public TextOutputWriter(ITextOutput output) + { + if (output == null) + throw new ArgumentNullException("output"); + this.output = output; + } + + public override Encoding Encoding { + get { return Encoding.UTF8; } + } + + public override void Write(char value) + { + output.Write(value); + } + + public override void Write(string value) + { + output.Write(value); + } + + public override void WriteLine() + { + output.WriteLine(); + } + } +} diff --git a/ILSpy/CSharpLanguage.cs b/ILSpy/CSharpLanguage.cs index a1eaf8d72..ca17d220c 100644 --- a/ILSpy/CSharpLanguage.cs +++ b/ILSpy/CSharpLanguage.cs @@ -21,6 +21,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; +using System.Threading.Tasks; +using System.Xml; using Decompiler; using Decompiler.Transforms; using ICSharpCode.Decompiler; @@ -70,6 +72,10 @@ namespace ICSharpCode.ILSpy get { return ".cs"; } } + public override string ProjectFileExtension { + get { return ".csproj"; } + } + public override void DecompileMethod(MethodDefinition method, ITextOutput output, DecompilationOptions options) { AstBuilder codeDomBuilder = CreateAstBuilder(options, method.DeclaringType); @@ -108,17 +114,174 @@ namespace ICSharpCode.ILSpy public override void DecompileAssembly(AssemblyDefinition assembly, string fileName, ITextOutput output, DecompilationOptions options) { if (options.FullDecompilation) { - foreach (TypeDefinition type in assembly.MainModule.Types) { - AstBuilder codeDomBuilder = CreateAstBuilder(options, type); - codeDomBuilder.AddType(type); - codeDomBuilder.GenerateCode(output, transformAbortCondition); - output.WriteLine(); + if (options.SaveAsProjectDirectory != null) { + var files = WriteFilesInProject(assembly, options); + WriteProjectFile(new TextOutputWriter(output), files, assembly.MainModule); + } else { + foreach (TypeDefinition type in assembly.MainModule.Types) { + AstBuilder codeDomBuilder = CreateAstBuilder(options, type); + codeDomBuilder.AddType(type); + codeDomBuilder.GenerateCode(output, transformAbortCondition); + output.WriteLine(); + } } } else { base.DecompileAssembly(assembly, fileName, output, options); } } + void WriteProjectFile(TextWriter writer, IEnumerable files, ModuleDefinition module) + { + const string ns = "http://schemas.microsoft.com/developer/msbuild/2003"; + string platformName; + switch (module.Architecture) { + case TargetArchitecture.I386: + if ((module.Attributes & ModuleAttributes.Required32Bit) == ModuleAttributes.Required32Bit) + platformName = "x86"; + else + platformName = "AnyCPU"; + break; + case TargetArchitecture.AMD64: + platformName = "x64"; + break; + case TargetArchitecture.IA64: + platformName = "Itanium"; + break; + default: + throw new NotSupportedException("Invalid value for TargetArchitecture"); + } + 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.NewGuid().ToString().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); + 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"); + // TODO: Detect TargetFrameworkProfile + break; + } + w.WriteElementString("WarningLevel", "4"); + + 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); + // TODO: RequiredTargetFramework + w.WriteEndElement(); + } + } + w.WriteEndElement(); // (References) + + w.WriteStartElement("ItemGroup"); // Code + foreach (string file in files.OrderBy(f => f, StringComparer.OrdinalIgnoreCase)) { + w.WriteStartElement("Compile"); + w.WriteAttributeString("Include", file); + w.WriteEndElement(); + } + w.WriteEndElement(); + + w.WriteStartElement("Import"); + w.WriteAttributeString("Project", "$(MSBuildToolsPath)\\Microsoft.CSharp.targets"); + w.WriteEndElement(); + + w.WriteEndDocument(); + } + } + + IEnumerable WriteFilesInProject(AssemblyDefinition assembly, DecompilationOptions options) + { + var files = assembly.MainModule.Types.Where(t => t.Name != "").GroupBy( + delegate (TypeDefinition type) { + string file = TextView.DecompilerTextView.CleanUpName(type.Name) + this.FileExtension; + if (string.IsNullOrEmpty(type.Namespace)) { + return file; + } else { + string dir = TextView.DecompilerTextView.CleanUpName(type.Namespace); + Directory.CreateDirectory(Path.Combine(options.SaveAsProjectDirectory, dir)); + return Path.Combine(dir, file); + } + }, StringComparer.OrdinalIgnoreCase).ToList(); + + AstMethodBodyBuilder.ClearUnhandledOpcodes(); + Parallel.ForEach( + files, + new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }, + delegate (IGrouping file) { + using (StreamWriter w = new StreamWriter(Path.Combine(options.SaveAsProjectDirectory, file.Key))) { + AstBuilder codeDomBuilder = CreateAstBuilder(options, null); + foreach (TypeDefinition type in file) + codeDomBuilder.AddType(type); + codeDomBuilder.GenerateCode(new PlainTextOutput(w), transformAbortCondition); + } + }); + AstMethodBodyBuilder.PrintNumberOfUnhandledOpcodes(); + return files.Select(f => f.Key); + } + AstBuilder CreateAstBuilder(DecompilationOptions options, TypeDefinition currentType) { return new AstBuilder( diff --git a/ILSpy/DecompilationOptions.cs b/ILSpy/DecompilationOptions.cs index 120583be6..8dc953c6d 100644 --- a/ILSpy/DecompilationOptions.cs +++ b/ILSpy/DecompilationOptions.cs @@ -32,6 +32,11 @@ namespace ICSharpCode.ILSpy /// public bool FullDecompilation { get; set; } + /// + /// Gets/Sets the directory into which the project is saved. + /// + public string SaveAsProjectDirectory { get; set; } + /// /// Gets the cancellation token that is used to abort the decompiler. /// diff --git a/ILSpy/Language.cs b/ILSpy/Language.cs index 5a96521b0..3bce054af 100644 --- a/ILSpy/Language.cs +++ b/ILSpy/Language.cs @@ -40,6 +40,10 @@ namespace ICSharpCode.ILSpy /// public abstract string FileExtension { get; } + public virtual string ProjectFileExtension { + get { return null; } + } + /// /// Gets the syntax highlighting used for this language. /// diff --git a/ILSpy/MainWindow.xaml.cs b/ILSpy/MainWindow.xaml.cs index 7a79cc166..b998739aa 100644 --- a/ILSpy/MainWindow.xaml.cs +++ b/ILSpy/MainWindow.xaml.cs @@ -392,7 +392,7 @@ namespace ICSharpCode.ILSpy { if (treeView.SelectedItems.Count == 1) { ILSpyTreeNode node = treeView.SelectedItem as ILSpyTreeNode; - if (node != null && node.Save()) + if (node != null && node.Save(decompilerTextView)) return; } decompilerTextView.SaveToDisk(sessionSettings.FilterSettings.Language, diff --git a/ILSpy/TextView/DecompilerTextView.cs b/ILSpy/TextView/DecompilerTextView.cs index d8a883bcc..7b628539e 100644 --- a/ILSpy/TextView/DecompilerTextView.cs +++ b/ILSpy/TextView/DecompilerTextView.cs @@ -384,6 +384,11 @@ namespace ICSharpCode.ILSpy.TextView } } + public void SaveToDisk(ILSpy.Language language, IEnumerable treeNodes, DecompilationOptions options, string fileName) + { + SaveToDisk(new DecompilationContext(language, treeNodes.ToArray(), options), fileName); + } + /// /// Starts the decompilation of the given nodes. /// The result will be saved to the given file name. @@ -458,6 +463,9 @@ namespace ICSharpCode.ILSpy.TextView internal static string CleanUpName(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(); diff --git a/ILSpy/TreeNodes/AssemblyTreeNode.cs b/ILSpy/TreeNodes/AssemblyTreeNode.cs index 7b9136717..72708fd20 100644 --- a/ILSpy/TreeNodes/AssemblyTreeNode.cs +++ b/ILSpy/TreeNodes/AssemblyTreeNode.cs @@ -27,7 +27,9 @@ using System.Windows; using System.Windows.Controls; using System.Windows.Threading; using ICSharpCode.Decompiler; +using ICSharpCode.ILSpy.TextView; using ICSharpCode.TreeView; +using Microsoft.Win32; using Mono.Cecil; namespace ICSharpCode.ILSpy.TreeNodes @@ -201,5 +203,36 @@ namespace ICSharpCode.ILSpy.TreeNodes assembly.WaitUntilLoaded(); // necessary so that load errors are passed on to the caller language.DecompileAssembly(assembly.AssemblyDefinition, assembly.FileName, output, options); } + + public override bool Save(DecompilerTextView textView) + { + Language language = this.Language; + if (string.IsNullOrEmpty(language.ProjectFileExtension)) + return false; + SaveFileDialog dlg = new SaveFileDialog(); + dlg.FileName = DecompilerTextView.CleanUpName(assembly.ShortName); + dlg.Filter = language.Name + " project|*" + language.ProjectFileExtension + "|" + language.Name + " single file|*" + language.FileExtension + "|All files|*.*"; + if (dlg.ShowDialog() == true) { + DecompilationOptions options = new DecompilationOptions(); + options.FullDecompilation = true; + if (dlg.FilterIndex == 1) { + options.SaveAsProjectDirectory = Path.GetDirectoryName(dlg.FileName); + foreach (string entry in Directory.GetFileSystemEntries(options.SaveAsProjectDirectory)) { + if (!string.Equals(entry, dlg.FileName, StringComparison.OrdinalIgnoreCase)) { + var result = MessageBox.Show( + "The directory is not empty. File will be overwritten." + Environment.NewLine + + "Are you sure you want to continue?", + "Project Directory not empty", + MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No); + if (result == MessageBoxResult.No) + return true; // don't save, but mark the Save operation as handled + break; + } + } + } + textView.SaveToDisk(language, new[]{this}, options, dlg.FileName); + } + return true; + } } } diff --git a/ILSpy/TreeNodes/ILSpyTreeNode.cs b/ILSpy/TreeNodes/ILSpyTreeNode.cs index 44a9483d3..3aef3bfc5 100644 --- a/ILSpy/TreeNodes/ILSpyTreeNode.cs +++ b/ILSpy/TreeNodes/ILSpyTreeNode.cs @@ -79,7 +79,7 @@ namespace ICSharpCode.ILSpy.TreeNodes /// This method is called on the main thread when only a single item is selected. /// If it returns false, normal decompilation is used to save the item. /// - public virtual bool Save() + public virtual bool Save(TextView.DecompilerTextView textView) { return false; } diff --git a/ILSpy/TreeNodes/ResourceEntryNode.cs b/ILSpy/TreeNodes/ResourceEntryNode.cs index af1c01baf..a167a916d 100644 --- a/ILSpy/TreeNodes/ResourceEntryNode.cs +++ b/ILSpy/TreeNodes/ResourceEntryNode.cs @@ -75,7 +75,7 @@ namespace ICSharpCode.ILSpy.TreeNodes image.EndInit(); output.AddUIElement(() => new Image { Source = image }); output.WriteLine(); - output.AddButton(Images.Save, "Save", delegate { Save(); }); + output.AddButton(Images.Save, "Save", delegate { Save(null); }); } catch (Exception) { return false; } @@ -106,7 +106,7 @@ namespace ICSharpCode.ILSpy.TreeNodes return true; } - public override bool Save() + public override bool Save(DecompilerTextView textView) { SaveFileDialog dlg = new SaveFileDialog(); dlg.FileName = Path.GetFileName(DecompilerTextView.CleanUpName(key)); diff --git a/ILSpy/TreeNodes/ResourceListTreeNode.cs b/ILSpy/TreeNodes/ResourceListTreeNode.cs index cc38111c5..f3d3ed5ca 100644 --- a/ILSpy/TreeNodes/ResourceListTreeNode.cs +++ b/ILSpy/TreeNodes/ResourceListTreeNode.cs @@ -102,7 +102,7 @@ namespace ICSharpCode.ILSpy.TreeNodes ISmartTextOutput smartOutput = output as ISmartTextOutput; if (smartOutput != null && r is EmbeddedResource) { - smartOutput.AddButton(Images.Save, "Save", delegate { Save(); }); + smartOutput.AddButton(Images.Save, "Save", delegate { Save(null); }); output.WriteLine(); } } @@ -132,7 +132,7 @@ namespace ICSharpCode.ILSpy.TreeNodes return false; } - public override bool Save() + public override bool Save(TextView.DecompilerTextView textView) { EmbeddedResource er = r as EmbeddedResource; if (er != null) {