diff --git a/ILSpy/Commands/ExtractPackageEntryContextMenuEntry.cs b/ILSpy/Commands/ExtractPackageEntryContextMenuEntry.cs index cf715759d..26016a2b4 100644 --- a/ILSpy/Commands/ExtractPackageEntryContextMenuEntry.cs +++ b/ILSpy/Commands/ExtractPackageEntryContextMenuEntry.cs @@ -17,11 +17,13 @@ // DEALINGS IN THE SOFTWARE. using System; +using System.Collections.Generic; using System.Composition; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; +using System.Windows; using ICSharpCode.Decompiler; using ICSharpCode.Decompiler.CSharp.ProjectDecompiler; @@ -30,9 +32,12 @@ using ICSharpCode.ILSpy.Properties; using ICSharpCode.ILSpy.TextView; using ICSharpCode.ILSpy.TreeNodes; using ICSharpCode.ILSpyX; +using ICSharpCode.ILSpyX.TreeView; using Microsoft.Win32; +using static ICSharpCode.ILSpyX.LoadedPackage; + namespace ICSharpCode.ILSpy { [ExportContextMenuEntry(Header = nameof(Resources.ExtractPackageEntry), Category = nameof(Resources.Save), Icon = "Images/Save")] @@ -41,55 +46,115 @@ namespace ICSharpCode.ILSpy { 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(); + var selectedNodes = Array.FindAll(context.SelectedTreeNodes, IsBundleItem); // 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(WholeProjectDecompiler.SanitizeFileName(assembly.Name)); - dlg.Filter = ".NET assemblies|*.dll;*.exe;*.winmd" + Resources.AllFiles; - dlg.InitialDirectory = Path.GetDirectoryName(bundleNode.LoadedAssembly.FileName); - if (dlg.ShowDialog() != true) - return; + if (selectedNodes is [AssemblyTreeNode { PackageEntry: { } assembly }]) + { + SaveFileDialog dlg = new SaveFileDialog(); + dlg.FileName = Path.GetFileName(WholeProjectDecompiler.SanitizeFileName(assembly.Name)); + dlg.Filter = ".NET assemblies|*.dll;*.exe;*.winmd" + Resources.AllFiles; + dlg.InitialDirectory = Path.GetDirectoryName(bundleNode.LoadedAssembly.FileName); + if (dlg.ShowDialog() == true) + Save(dockWorkspace, selectedNodes, dlg.FileName, true); + } + else if (selectedNodes is [ResourceTreeNode { Resource: { } resource }]) + { + SaveFileDialog dlg = new SaveFileDialog(); + dlg.FileName = Path.GetFileName(WholeProjectDecompiler.SanitizeFileName(resource.Name)); + dlg.Filter = Resources.AllFiles[1..]; + dlg.InitialDirectory = Path.GetDirectoryName(bundleNode.LoadedAssembly.FileName); + if (dlg.ShowDialog() == true) + Save(dockWorkspace, selectedNodes, dlg.FileName, true); + } + else + { + OpenFolderDialog dlg = new OpenFolderDialog(); + dlg.InitialDirectory = Path.GetDirectoryName(bundleNode.LoadedAssembly.FileName); + if (dlg.ShowDialog() != true) + return; + + string folderName = dlg.FolderName; + if (Directory.EnumerateFileSystemEntries(folderName).Any()) + { + var result = MessageBox.Show( + Resources.AssemblySaveCodeDirectoryNotEmpty, + Resources.AssemblySaveCodeDirectoryNotEmptyTitle, + MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No); + if (result == MessageBoxResult.No) + return; + } - string fileName = dlg.FileName; - string outputFolderOrFileName = fileName; - if (selectedNodes.Length > 1) - outputFolderOrFileName = Path.GetDirectoryName(outputFolderOrFileName); + Save(dockWorkspace, selectedNodes, folderName, false); + } + } + static string GetPackageFolderPath(SharpTreeNode node) + { + string name = ""; + while (node is PackageFolderTreeNode) + { + name = Path.Combine(node.Text.ToString(), name); + node = node.Parent; + } + return name; + } + + internal static void Save(DockWorkspace dockWorkspace, IEnumerable nodes, string path, bool isFile) + { dockWorkspace.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 nodes) { - foreach (var node in selectedNodes) + if (node is AssemblyTreeNode { PackageEntry: { } assembly }) + { + string fileName = isFile ? path : Path.Combine(path, GetPackageFolderPath(node.Parent), assembly.Name); + SaveEntry(output, assembly, fileName); + } + else if (node is ResourceTreeNode { Resource: PackageEntry { } resource }) { - var fileName = Path.GetFileName(WholeProjectDecompiler.SanitizeFileName(node.PackageEntry.Name)); - SaveEntry(output, node.PackageEntry, Path.Combine(outputFolderOrFileName, fileName)); + string fileName = isFile ? path : Path.Combine(path, GetPackageFolderPath(node.Parent), resource.Name); + SaveEntry(output, resource, fileName); + } + else if (node is PackageFolderTreeNode) + { + node.EnsureLazyChildren(); + foreach (var item in node.DescendantsAndSelf()) + { + if (item is AssemblyTreeNode { PackageEntry: { } asm }) + { + string fileName = Path.Combine(path, GetPackageFolderPath(item.Parent), asm.Name); + SaveEntry(output, asm, fileName); + } + else if (item is ResourceTreeNode { Resource: PackageEntry { } entry }) + { + string fileName = Path.Combine(path, GetPackageFolderPath(item.Parent), entry.Name); + SaveEntry(output, entry, fileName); + } + else if (item is PackageFolderTreeNode) + { + Directory.CreateDirectory(Path.Combine(path, GetPackageFolderPath(item))); + } + } } } + stopwatch.Stop(); output.WriteLine(Resources.GenerationCompleteInSeconds, stopwatch.Elapsed.TotalSeconds.ToString("F1")); output.WriteLine(); - output.AddButton(null, Resources.OpenExplorer, delegate { Process.Start("explorer", "/select,\"" + fileName + "\""); }); + output.AddButton(null, Resources.OpenExplorer, delegate { Process.Start("explorer", isFile ? $"/select,\"{path}\"" : $"\"{path}\""); }); output.WriteLine(); return output; }, ct)).Then(dockWorkspace.ShowText).HandleExceptions(); } - void SaveEntry(ITextOutput output, PackageEntry entry, string targetFileName) + static void SaveEntry(ITextOutput output, PackageEntry entry, string targetFileName) { output.Write(entry.Name + ": "); + targetFileName = WholeProjectDecompiler.SanitizeFileName(targetFileName); using Stream stream = entry.TryOpenStream(); if (stream == null) { @@ -105,11 +170,58 @@ namespace ICSharpCode.ILSpy public bool IsEnabled(TextViewContext context) => true; + public bool IsVisible(TextViewContext context) => context.SelectedTreeNodes?.Any(IsBundleItem) == true; + + static bool IsBundleItem(SharpTreeNode node) + { + if (node is AssemblyTreeNode { PackageEntry: { } } or PackageFolderTreeNode) + return true; + if (node is ResourceTreeNode { Resource: PackageEntry { } resource } && resource.FullName.StartsWith("bundle://")) + return true; + return false; + } + } + + [ExportContextMenuEntry(Header = nameof(Resources.ExtractAllPackageEntries), Category = nameof(Resources.Save), Icon = "Images/Save")] + [Shared] + sealed class ExtractAllPackageEntriesContextMenuEntry(DockWorkspace dockWorkspace) : IContextMenuEntry + { + public void Execute(TextViewContext context) + { + if (context.SelectedTreeNodes is not [AssemblyTreeNode { PackageEntry: null } asm]) + return; + OpenFolderDialog dlg = new OpenFolderDialog(); + dlg.InitialDirectory = Path.GetDirectoryName(asm.LoadedAssembly.FileName); + if (dlg.ShowDialog() != true) + return; + + string folderName = dlg.FolderName; + if (Directory.EnumerateFileSystemEntries(folderName).Any()) + { + var result = MessageBox.Show( + Resources.AssemblySaveCodeDirectoryNotEmpty, + Resources.AssemblySaveCodeDirectoryNotEmptyTitle, + MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No); + if (result == MessageBoxResult.No) + return; + } + + asm.EnsureLazyChildren(); + ExtractPackageEntryContextMenuEntry.Save(dockWorkspace, asm.Descendants(), folderName, false); + } + + 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(); + if (context.SelectedTreeNodes is [AssemblyTreeNode { LoadedAssembly.IsLoaded: true, LoadedAssembly.HasLoadError: false, PackageEntry: null } asm]) + { + // Using .GetAwaiter().GetResult() is no problem here, since we already checked IsLoaded and HasLoadError. + var loadResult = asm.LoadedAssembly.GetLoadResultAsync().GetAwaiter().GetResult(); + if (loadResult.Package is { Kind: PackageKind.Bundle }) + return true; + } + return false; } } } diff --git a/ILSpy/Properties/Resources.Designer.cs b/ILSpy/Properties/Resources.Designer.cs index 285319bdd..e6308d83e 100644 --- a/ILSpy/Properties/Resources.Designer.cs +++ b/ILSpy/Properties/Resources.Designer.cs @@ -1829,11 +1829,20 @@ namespace ICSharpCode.ILSpy.Properties { return ResourceManager.GetString("ExpandUsingDeclarationsAfterDecompilation", resourceCulture); } } - - /// - /// Looks up a localized string similar to Extract package entry. - /// - public static string ExtractPackageEntry { + + /// + /// Looks up a localized string similar to Extract all package entries. + /// + public static string ExtractAllPackageEntries { + get { + return ResourceManager.GetString("ExtractAllPackageEntries", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Extract package entry. + /// + public static string ExtractPackageEntry { get { return ResourceManager.GetString("ExtractPackageEntry", resourceCulture); } diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx index f53e0ca4f..65d7471aa 100644 --- a/ILSpy/Properties/Resources.resx +++ b/ILSpy/Properties/Resources.resx @@ -627,6 +627,9 @@ Are you sure you want to continue? Expand using declarations after decompilation + + Extract all package entries + Extract package entry diff --git a/ILSpy/Properties/Resources.zh-Hans.resx b/ILSpy/Properties/Resources.zh-Hans.resx index 471a79dad..bbdc942ce 100644 --- a/ILSpy/Properties/Resources.zh-Hans.resx +++ b/ILSpy/Properties/Resources.zh-Hans.resx @@ -585,6 +585,9 @@ 反编译后展开引用和声明 + + 提取所有包条目 + 提取包条目