From a26a6efd3ea27535f3c7c18e9d3ae15980f777c7 Mon Sep 17 00:00:00 2001 From: CreateAndInject Date: Mon, 7 Jul 2025 18:34:24 +0800 Subject: [PATCH] Extract multiple package entries --- .../ExtractPackageEntryContextMenuEntry.cs | 197 +++++++++++++++--- ILSpy/Properties/Resources.Designer.cs | 19 +- ILSpy/Properties/Resources.resx | 3 + ILSpy/Properties/Resources.zh-Hans.resx | 3 + 4 files changed, 191 insertions(+), 31 deletions(-) diff --git a/ILSpy/Commands/ExtractPackageEntryContextMenuEntry.cs b/ILSpy/Commands/ExtractPackageEntryContextMenuEntry.cs index cf715759d..22366d087 100644 --- a/ILSpy/Commands/ExtractPackageEntryContextMenuEntry.cs +++ b/ILSpy/Commands/ExtractPackageEntryContextMenuEntry.cs @@ -22,6 +22,7 @@ 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 +31,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,52 +45,184 @@ 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, x => x is AssemblyTreeNode { PackageEntry: { } } or PackageFolderTreeNode); // 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 (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) + return; + + string fileName = dlg.FileName; + dockWorkspace.RunWithCancellation(ct => Task.Factory.StartNew(() => { + AvalonEditTextOutput output = new AvalonEditTextOutput(); + Stopwatch stopwatch = Stopwatch.StartNew(); + SaveEntry(output, assembly, fileName); + 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.WriteLine(); + return output; + }, ct)).Then(dockWorkspace.ShowText).HandleExceptions(); + } + 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; + } + + dockWorkspace.RunWithCancellation(ct => Task.Factory.StartNew(() => { + AvalonEditTextOutput output = new AvalonEditTextOutput(); + Stopwatch stopwatch = Stopwatch.StartNew(); + foreach (var selectedNode in selectedNodes) + { + if (selectedNode is AssemblyTreeNode { PackageEntry: { } assembly }) + { + string fileName = Path.Combine(folderName, GetFullPath(selectedNode.Parent) + WholeProjectDecompiler.SanitizeFileName(assembly.Name)); + SaveEntry(output, assembly, fileName); + } + else if (selectedNode is PackageFolderTreeNode) + { + selectedNode.EnsureLazyChildren(); + foreach (var node in selectedNode.DescendantsAndSelf()) + { + if (node is AssemblyTreeNode { PackageEntry: { } asm }) + { + string fileName = Path.Combine(folderName, GetFullPath(node.Parent) + WholeProjectDecompiler.SanitizeFileName(asm.Name)); + SaveEntry(output, asm, fileName); + } + else if (node is PackageFolderTreeNode) + { + Directory.CreateDirectory(Path.Combine(folderName, GetFullPath(node))); + } + } + } + } + stopwatch.Stop(); + output.WriteLine(Resources.GenerationCompleteInSeconds, stopwatch.Elapsed.TotalSeconds.ToString("F1")); + output.WriteLine(); + output.AddButton(null, Resources.OpenExplorer, delegate { Process.Start("explorer", "\"" + folderName + "\""); }); + output.WriteLine(); + return output; + }, ct)).Then(dockWorkspace.ShowText).HandleExceptions(); + } + } + + static string GetFullPath(SharpTreeNode node) + { + if (node is PackageFolderTreeNode) + { + string name = node.Text + "\\"; + if (GetFullPath(node.Parent) is string parent) + return parent + "\\" + name; + return name; + } + return null; + } + + 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) => + context.SelectedTreeNodes?.Any(x => x is AssemblyTreeNode { PackageEntry: { } } or PackageFolderTreeNode) == true; + } + + [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 fileName = dlg.FileName; - string outputFolderOrFileName = fileName; - if (selectedNodes.Length > 1) - outputFolderOrFileName = Path.GetDirectoryName(outputFolderOrFileName); + 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; + } dockWorkspace.RunWithCancellation(ct => Task.Factory.StartNew(() => { AvalonEditTextOutput output = new AvalonEditTextOutput(); Stopwatch stopwatch = Stopwatch.StartNew(); - stopwatch.Stop(); - - if (selectedNodes.Length == 1) + asm.EnsureLazyChildren(); + foreach (var node in asm.Descendants()) { - SaveEntry(output, selectedNodes[0].PackageEntry, outputFolderOrFileName); - } - else - { - foreach (var node in selectedNodes) + if (node is AssemblyTreeNode { PackageEntry: { } assembly }) + { + string fileName = Path.Combine(folderName, GetFullPath(node.Parent) + WholeProjectDecompiler.SanitizeFileName(assembly.Name)); + SaveEntry(output, assembly, fileName); + } + else if (node is PackageFolderTreeNode) { - var fileName = Path.GetFileName(WholeProjectDecompiler.SanitizeFileName(node.PackageEntry.Name)); - SaveEntry(output, node.PackageEntry, Path.Combine(outputFolderOrFileName, fileName)); + Directory.CreateDirectory(Path.Combine(folderName, GetFullPath(node))); } } + 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", "\"" + folderName + "\""); }); output.WriteLine(); return output; }, ct)).Then(dockWorkspace.ShowText).HandleExceptions(); } + static string GetFullPath(SharpTreeNode node) + { + if (node is PackageFolderTreeNode) + { + string name = node.Text + "\\"; + if (GetFullPath(node.Parent) is string parent) + return parent + "\\" + name; + return name; + } + return null; + } + void SaveEntry(ITextOutput output, PackageEntry entry, string targetFileName) { output.Write(entry.Name + ": "); @@ -107,9 +243,18 @@ namespace ICSharpCode.ILSpy 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 { PackageEntry: null } asm]) + { + try + { + if (asm.LoadedAssembly.GetLoadResultAsync().GetAwaiter().GetResult().Package is { Kind: PackageKind.Bundle }) + return true; + } + catch + { + } + } + return false; } } } diff --git a/ILSpy/Properties/Resources.Designer.cs b/ILSpy/Properties/Resources.Designer.cs index d31af0b12..b7b2d6301 100644 --- a/ILSpy/Properties/Resources.Designer.cs +++ b/ILSpy/Properties/Resources.Designer.cs @@ -1820,11 +1820,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 840c92fb1..0384209eb 100644 --- a/ILSpy/Properties/Resources.resx +++ b/ILSpy/Properties/Resources.resx @@ -624,6 +624,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 3973a4805..8d1ce8907 100644 --- a/ILSpy/Properties/Resources.zh-Hans.resx +++ b/ILSpy/Properties/Resources.zh-Hans.resx @@ -582,6 +582,9 @@ 反编译后展开引用和声明 + + 提取所有包条目 + 提取包条目