Browse Source

Implement decompilation of multiple selected assemblies icsharpcode#972

pull/1550/head
dymanoid 6 years ago
parent
commit
4ceb8f62d0
  1. 1
      ILSpy/ILSpy.csproj
  2. 29
      ILSpy/MainWindow.xaml.cs
  3. 198
      ILSpy/SolutionWriter.cs
  4. 53
      ILSpy/TreeNodes/AssemblyTreeNode.cs
  5. 9
      ILSpy/TreeNodes/ILSpyTreeNode.cs

1
ILSpy/ILSpy.csproj

@ -204,6 +204,7 @@ @@ -204,6 +204,7 @@
</Compile>
<Compile Include="Commands\SimpleCommand.cs" />
<Compile Include="Search\AbstractSearchStrategy.cs" />
<Compile Include="SolutionWriter.cs" />
<Compile Include="TaskHelper.cs" />
<Compile Include="TextView\EditorCommands.cs" />
<Compile Include="TextView\FoldingCommands.cs" />

29
ILSpy/MainWindow.xaml.cs

@ -907,19 +907,34 @@ namespace ICSharpCode.ILSpy @@ -907,19 +907,34 @@ namespace ICSharpCode.ILSpy
e.CanExecute = true;
return;
}
var selectedNodes = SelectedNodes.ToArray();
e.CanExecute = selectedNodes.Length == 1 || Array.TrueForAll(selectedNodes, n => n is AssemblyTreeNode);
var selectedNodes = SelectedNodes.ToList();
e.CanExecute = selectedNodes.Count == 1 || selectedNodes.TrueForAll(n => n is AssemblyTreeNode);
}
void SaveCommandExecuted(object sender, ExecutedRoutedEventArgs e)
{
if (this.SelectedNodes.Count() == 1) {
if (this.SelectedNodes.Single().Save(this.TextView))
var selectedNodes = SelectedNodes.ToList();
if (selectedNodes.Count > 1) {
var assemblyNodes = selectedNodes
.OfType<AssemblyTreeNode>()
.Where(n => n.Language is CSharpLanguage)
.ToList();
if (assemblyNodes.Count == selectedNodes.Count) {
var initialPath = Path.GetDirectoryName(assemblyNodes[0].LoadedAssembly.FileName);
var selectedPath = SolutionWriter.SelectSolutionFile(initialPath);
if (!string.IsNullOrEmpty(selectedPath)) {
SolutionWriter.CreateSolution(TextView, selectedPath, assemblyNodes);
}
return;
}
}
if (selectedNodes.Count != 1 || !selectedNodes[0].Save(TextView)) {
var options = new DecompilationOptions() { FullDecompilation = true };
TextView.SaveToDisk(CurrentLanguage, selectedNodes, options);
}
this.TextView.SaveToDisk(this.CurrentLanguage,
this.SelectedNodes,
new DecompilationOptions() { FullDecompilation = true });
}
public void RefreshDecompiledView()

198
ILSpy/SolutionWriter.cs

@ -0,0 +1,198 @@ @@ -0,0 +1,198 @@
// 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.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Security;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using ICSharpCode.Decompiler;
using ICSharpCode.ILSpy.TextView;
using ICSharpCode.ILSpy.TreeNodes;
using Microsoft.Win32;
namespace ICSharpCode.ILSpy
{
/// <summary>
/// An utility class that creates a Visual Studio solution containing projects for the
/// decompiled assemblies.
/// </summary>
internal static class SolutionWriter
{
private const string SolutionExtension = ".sln";
private const string DefaultSolutionName = "Solution";
/// <summary>
/// Shows a File Selection dialog where the user can select the target file for the solution.
/// </summary>
/// <param name="path">The initial path to show in the dialog. If not specified, the 'Documents' directory
/// will be used.</param>
///
/// <returns>The full path of the selected target file, or <c>null</c> if the user canceled.</returns>
public static string SelectSolutionFile(string path)
{
if (string.IsNullOrWhiteSpace(path)) {
path = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
}
SaveFileDialog dlg = new SaveFileDialog();
dlg.InitialDirectory = path;
dlg.FileName = Path.Combine(path, DefaultSolutionName + SolutionExtension);
dlg.Filter = "Visual Studio Solution file|*" + SolutionExtension;
bool targetInvalid;
do {
if (dlg.ShowDialog() != true) {
return null;
}
string selectedPath = Path.GetDirectoryName(dlg.FileName);
try {
targetInvalid = Directory.EnumerateFileSystemEntries(selectedPath).Any();
} catch (Exception e) when (e is IOException || e is UnauthorizedAccessException || e is SecurityException) {
MessageBox.Show(
"The directory cannot be accessed. Please ensure it exists and you have sufficient rights to access it.",
"Solution directory not accessible",
MessageBoxButton.OK, MessageBoxImage.Error);
targetInvalid = true;
continue;
}
if (targetInvalid) {
MessageBox.Show(
"The directory is not empty. Please select an empty directory.",
"Solution directory not empty",
MessageBoxButton.OK, MessageBoxImage.Warning);
}
} while (targetInvalid);
return dlg.FileName;
}
/// <summary>
/// Creates a Visual Studio solution that contains projects with decompiled code
/// of the specified <paramref name="assemblyNodes"/>. The solution file will be saved
/// to the <paramref name="solutionFilePath"/>. The directory of this file must either
/// be empty or not exist.
/// </summary>
/// <param name="textView">A reference to the <see cref="DecompilerTextView"/> instance.</param>
/// <param name="solutionFilePath">The target file path of the solution file.</param>
/// <param name="assemblyNodes">The assembly nodes to decompile.</param>
///
/// <exception cref="ArgumentException">Thrown when <paramref name="solutionFilePath"/> is null,
/// an empty or a whitespace string.</exception>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="textView"/>> or
/// <paramref name="assemblyNodes"/> is null.</exception>
public static void CreateSolution(DecompilerTextView textView, string solutionFilePath, IEnumerable<AssemblyTreeNode> assemblyNodes)
{
if (textView == null) {
throw new ArgumentNullException(nameof(textView));
}
if (string.IsNullOrWhiteSpace(solutionFilePath)) {
throw new ArgumentException("The solution file path cannot be null or empty.", nameof(solutionFilePath));
}
if (assemblyNodes == null) {
throw new ArgumentNullException(nameof(assemblyNodes));
}
textView
.RunWithCancellation(ct => CreateSolution(solutionFilePath, assemblyNodes, ct))
.Then(output => textView.ShowText(output))
.HandleExceptions();
}
private static async Task<AvalonEditTextOutput> CreateSolution(
string solutionFilePath,
IEnumerable<AssemblyTreeNode> assemblyNodes,
CancellationToken ct)
{
var solutionDirectory = Path.GetDirectoryName(solutionFilePath);
var statusOutput = new ConcurrentBag<string>();
var result = new AvalonEditTextOutput();
var duplicates = new HashSet<string>();
if (assemblyNodes.Any(n => !duplicates.Add(n.LoadedAssembly.ShortName))) {
result.WriteLine("Duplicate assembly names selected, cannot generate a solution.");
return result;
}
Stopwatch stopwatch = Stopwatch.StartNew();
await Task.Run(() => Parallel.ForEach(assemblyNodes, n => WriteProject(n, solutionDirectory, statusOutput, ct)))
.ConfigureAwait(false);
foreach (var item in statusOutput) {
result.WriteLine(item);
}
if (statusOutput.Count == 0) {
result.WriteLine("Successfully decompiled the following assemblies to a Visual Studio Solution:");
foreach (var item in assemblyNodes.Select(n => n.Text.ToString())) {
result.WriteLine(item);
}
result.WriteLine();
result.WriteLine("Elapsed time: " + stopwatch.Elapsed.TotalSeconds.ToString("F1") + " seconds.");
result.WriteLine();
result.AddButton(null, "Open Explorer", delegate { Process.Start("explorer", "/select,\"" + solutionFilePath + "\""); });
}
return result;
}
private static void WriteProject(AssemblyTreeNode assemblyNode, string targetDirectory, ConcurrentBag<string> statusOutput, CancellationToken ct)
{
var loadedAssembly = assemblyNode.LoadedAssembly;
targetDirectory = Path.Combine(targetDirectory, loadedAssembly.ShortName);
string projectFileName = Path.Combine(targetDirectory, loadedAssembly.ShortName + assemblyNode.Language.ProjectFileExtension);
if (!Directory.Exists(targetDirectory)) {
try {
Directory.CreateDirectory(targetDirectory);
} catch (Exception e) {
statusOutput.Add($"Failed to create a directory '{targetDirectory}':{Environment.NewLine}{e}");
return;
}
}
try {
using (var projectFileWriter = new StreamWriter(projectFileName)) {
var projectFileOutput = new PlainTextOutput(projectFileWriter);
var options = new DecompilationOptions() {
FullDecompilation = true,
CancellationToken = ct,
SaveAsProjectDirectory = targetDirectory };
assemblyNode.Decompile(assemblyNode.Language, projectFileOutput, options);
}
} catch (Exception e) {
statusOutput.Add($"Failed to decompile the assembly '{loadedAssembly.FileName}':{Environment.NewLine}{e}");
return;
}
}
}
}

53
ILSpy/TreeNodes/AssemblyTreeNode.cs

@ -279,31 +279,44 @@ namespace ICSharpCode.ILSpy.TreeNodes @@ -279,31 +279,44 @@ namespace ICSharpCode.ILSpy.TreeNodes
public override bool Save(DecompilerTextView textView)
{
Language language = this.Language;
if (string.IsNullOrEmpty(language.ProjectFileExtension))
if (string.IsNullOrEmpty(language.ProjectFileExtension)) {
return false;
}
SaveFileDialog dlg = new SaveFileDialog();
dlg.FileName = DecompilerTextView.CleanUpName(LoadedAssembly.ShortName) + language.ProjectFileExtension;
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;
}
}
dlg.Filter = language.Name + " project|*" + language.ProjectFileExtension;
if (dlg.ShowDialog() != true) {
return true;
}
var targetDirectory = Path.GetDirectoryName(dlg.FileName);
var existingFiles = Directory.GetFileSystemEntries(targetDirectory);
if (existingFiles.Any(e => !string.Equals(e, 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
}
textView.SaveToDisk(language, new[] { this }, options, dlg.FileName);
}
Save(textView, dlg.FileName);
return true;
}
public override bool Save(DecompilerTextView textView, string fileName)
{
var targetDirectory = Path.GetDirectoryName(fileName);
DecompilationOptions options = new DecompilationOptions {
FullDecompilation = true,
SaveAsProjectDirectory = targetDirectory
};
textView.SaveToDisk(Language, new[] { this }, options, fileName);
return true;
}

9
ILSpy/TreeNodes/ILSpyTreeNode.cs

@ -79,6 +79,15 @@ namespace ICSharpCode.ILSpy.TreeNodes @@ -79,6 +79,15 @@ namespace ICSharpCode.ILSpy.TreeNodes
return false;
}
/// <summary>
/// Saves the content this node represents to the specified <paramref name="fileName"/>.
/// The file will be silently overwritten.
/// </summary>
/// <param name="textView">A reference to a <see cref="TextView.DecompilerTextView"/> instance.</param>
/// <param name="fileName">The target full path to save the content to.</param>
/// <returns><c>true</c> on success; otherwise, <c>false</c>.</returns>
public virtual bool Save(TextView.DecompilerTextView textView, string fileName) => Save(textView);
protected override void OnChildrenChanged(NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null) {

Loading…
Cancel
Save