From eec79d5a5d273ff30a5bc6665e9f79441ede2c38 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sat, 5 Jun 2021 18:26:41 +0200 Subject: [PATCH] Fix #2410: Add "Extract package entry" context menu entry for binaries inside bundles/packages. --- .../ExtractPackageEntryContextMenuEntry.cs | 110 ++++++++++++++++++ ILSpy/Properties/Resources.Designer.cs | 27 +++++ ILSpy/Properties/Resources.resx | 9 ++ ILSpy/TextView/DecompilerTextView.cs | 2 +- 4 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 ILSpy/Commands/ExtractPackageEntryContextMenuEntry.cs diff --git a/ILSpy/Commands/ExtractPackageEntryContextMenuEntry.cs b/ILSpy/Commands/ExtractPackageEntryContextMenuEntry.cs new file mode 100644 index 000000000..b38ad69be --- /dev/null +++ b/ILSpy/Commands/ExtractPackageEntryContextMenuEntry.cs @@ -0,0 +1,110 @@ +// Copyright (c) 2021 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.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +using ICSharpCode.Decompiler; +using ICSharpCode.ILSpy.Properties; +using ICSharpCode.ILSpy.TextView; +using ICSharpCode.ILSpy.TreeNodes; + +using Microsoft.Win32; + +namespace ICSharpCode.ILSpy +{ + [ExportContextMenuEntry(Header = nameof(Resources.ExtractPackageEntry), Category = nameof(Resources.Save), Icon = "Images/Save")] + sealed class ExtractPackageEntryContextMenuEntry : IContextMenuEntry + { + public void Execute(TextViewContext context) + { + // Get all assemblies in the selection that are stored inside a package. + var selectedNodes = context.SelectedTreeNodes.OfType() + .Where(asm => asm.PackageEntry != null).ToArray(); + // Get root assembly to infer the initial directory for the save dialog. + var bundleNode = selectedNodes.FirstOrDefault()?.Ancestors().OfType() + .FirstOrDefault(asm => asm.PackageEntry == null); + if (bundleNode == null) + return; + var assembly = selectedNodes[0].PackageEntry; + SaveFileDialog dlg = new SaveFileDialog(); + dlg.FileName = Path.GetFileName(DecompilerTextView.CleanUpName(assembly.Name)); + dlg.Filter = ".NET assemblies|*.dll;*.exe;*.winmd" + Resources.AllFiles; + dlg.InitialDirectory = Path.GetDirectoryName(bundleNode.LoadedAssembly.FileName); + if (dlg.ShowDialog() != true) + return; + + string fileName = dlg.FileName; + string outputFolderOrFileName = fileName; + if (selectedNodes.Length > 1) + outputFolderOrFileName = Path.GetDirectoryName(outputFolderOrFileName); + + Docking.DockWorkspace.Instance.RunWithCancellation(ct => Task.Factory.StartNew(() => { + AvalonEditTextOutput output = new AvalonEditTextOutput(); + Stopwatch stopwatch = Stopwatch.StartNew(); + stopwatch.Stop(); + + if (selectedNodes.Length == 1) + { + SaveEntry(output, selectedNodes[0].PackageEntry, outputFolderOrFileName); + } + else + { + foreach (var node in selectedNodes) + { + var fileName = Path.GetFileName(DecompilerTextView.CleanUpName(node.PackageEntry.Name)); + SaveEntry(output, node.PackageEntry, Path.Combine(outputFolderOrFileName, fileName)); + } + } + output.WriteLine(Resources.GenerationCompleteInSeconds, stopwatch.Elapsed.TotalSeconds.ToString("F1")); + output.WriteLine(); + output.AddButton(null, Resources.OpenExplorer, delegate { Process.Start("explorer", "/select,\"" + fileName + "\""); }); + output.WriteLine(); + return output; + }, ct)).Then(output => Docking.DockWorkspace.Instance.ShowText(output)).HandleExceptions(); + } + + void SaveEntry(ITextOutput output, PackageEntry entry, string targetFileName) + { + output.Write(entry.Name + ": "); + using Stream stream = entry.TryOpenStream(); + if (stream == null) + { + output.WriteLine("Could not open stream!"); + return; + } + + stream.Position = 0; + using FileStream fileStream = new FileStream(targetFileName, FileMode.OpenOrCreate); + stream.CopyTo(fileStream); + output.WriteLine("Written to " + targetFileName); + } + + public bool IsEnabled(TextViewContext context) => true; + + public bool IsVisible(TextViewContext context) + { + var selectedNodes = context.SelectedTreeNodes.OfType() + .Where(asm => asm.PackageEntry != null) ?? Enumerable.Empty(); + return selectedNodes.Any(); + } + } +} diff --git a/ILSpy/Properties/Resources.Designer.cs b/ILSpy/Properties/Resources.Designer.cs index 4856b4aee..9302e2f9f 100644 --- a/ILSpy/Properties/Resources.Designer.cs +++ b/ILSpy/Properties/Resources.Designer.cs @@ -621,6 +621,15 @@ namespace ICSharpCode.ILSpy.Properties { } } + /// + /// Looks up a localized string similar to Decompilation was cancelled.. + /// + public static string DecompilationWasCancelled { + get { + return ResourceManager.GetString("DecompilationWasCancelled", resourceCulture); + } + } + /// /// Looks up a localized string similar to Decompile. /// @@ -1532,6 +1541,15 @@ namespace ICSharpCode.ILSpy.Properties { } } + /// + /// Looks up a localized string similar to Extract package entry. + /// + public static string ExtractPackageEntry { + get { + return ResourceManager.GetString("ExtractPackageEntry", resourceCulture); + } + } + /// /// Looks up a localized string similar to Folding. /// @@ -1893,6 +1911,15 @@ namespace ICSharpCode.ILSpy.Properties { } } + /// + /// Looks up a localized string similar to Operation was cancelled.. + /// + public static string OperationWasCancelled { + get { + return ResourceManager.GetString("OperationWasCancelled", resourceCulture); + } + } + /// /// Looks up a localized string similar to Options. /// diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx index 049782bcd..4952cb74d 100644 --- a/ILSpy/Properties/Resources.resx +++ b/ILSpy/Properties/Resources.resx @@ -234,6 +234,9 @@ Are you sure you want to continue? Decompilation complete in {0:F1} seconds. + + Decompilation was cancelled. + Decompile @@ -537,6 +540,9 @@ Are you sure you want to continue? Expand using declarations after decompilation + + Extract package entry + Folding @@ -658,6 +664,9 @@ Please disable all filters that might hide the item (i.e. activate "View > Sh _Open + + Operation was cancelled. + Options diff --git a/ILSpy/TextView/DecompilerTextView.cs b/ILSpy/TextView/DecompilerTextView.cs index 25b86f1b1..714f6f9c1 100644 --- a/ILSpy/TextView/DecompilerTextView.cs +++ b/ILSpy/TextView/DecompilerTextView.cs @@ -1090,7 +1090,7 @@ namespace ICSharpCode.ILSpy.TextView catch (OperationCanceledException) { w.WriteLine(); - w.WriteLine("Decompiled was cancelled."); + w.WriteLine(Properties.Resources.DecompilationWasCancelled); throw; } }