Skip to content

Commit b52544d

Browse files
Merge pull request #3508 from CreateAndInject/ExtractMultiplePackageEntries
Extract multiple package entries
2 parents 844f5b4 + 247d075 commit b52544d

File tree

4 files changed

+161
-34
lines changed

4 files changed

+161
-34
lines changed

ILSpy/Commands/ExtractPackageEntryContextMenuEntry.cs

Lines changed: 141 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@
1717
// DEALINGS IN THE SOFTWARE.
1818

1919
using System;
20+
using System.Collections.Generic;
2021
using System.Composition;
2122
using System.Diagnostics;
2223
using System.IO;
2324
using System.Linq;
2425
using System.Threading.Tasks;
26+
using System.Windows;
2527

2628
using ICSharpCode.Decompiler;
2729
using ICSharpCode.Decompiler.CSharp.ProjectDecompiler;
@@ -30,9 +32,12 @@
3032
using ICSharpCode.ILSpy.TextView;
3133
using ICSharpCode.ILSpy.TreeNodes;
3234
using ICSharpCode.ILSpyX;
35+
using ICSharpCode.ILSpyX.TreeView;
3336

3437
using Microsoft.Win32;
3538

39+
using static ICSharpCode.ILSpyX.LoadedPackage;
40+
3641
namespace ICSharpCode.ILSpy
3742
{
3843
[ExportContextMenuEntry(Header = nameof(Resources.ExtractPackageEntry), Category = nameof(Resources.Save), Icon = "Images/Save")]
@@ -41,55 +46,115 @@ sealed class ExtractPackageEntryContextMenuEntry(DockWorkspace dockWorkspace) :
4146
{
4247
public void Execute(TextViewContext context)
4348
{
44-
// Get all assemblies in the selection that are stored inside a package.
45-
var selectedNodes = context.SelectedTreeNodes.OfType<AssemblyTreeNode>()
46-
.Where(asm => asm.PackageEntry != null).ToArray();
49+
var selectedNodes = Array.FindAll(context.SelectedTreeNodes, IsBundleItem);
4750
// Get root assembly to infer the initial directory for the save dialog.
4851
var bundleNode = selectedNodes.FirstOrDefault()?.Ancestors().OfType<AssemblyTreeNode>()
4952
.FirstOrDefault(asm => asm.PackageEntry == null);
5053
if (bundleNode == null)
5154
return;
52-
var assembly = selectedNodes[0].PackageEntry;
53-
SaveFileDialog dlg = new SaveFileDialog();
54-
dlg.FileName = Path.GetFileName(WholeProjectDecompiler.SanitizeFileName(assembly.Name));
55-
dlg.Filter = ".NET assemblies|*.dll;*.exe;*.winmd" + Resources.AllFiles;
56-
dlg.InitialDirectory = Path.GetDirectoryName(bundleNode.LoadedAssembly.FileName);
57-
if (dlg.ShowDialog() != true)
58-
return;
55+
if (selectedNodes is [AssemblyTreeNode { PackageEntry: { } assembly }])
56+
{
57+
SaveFileDialog dlg = new SaveFileDialog();
58+
dlg.FileName = Path.GetFileName(WholeProjectDecompiler.SanitizeFileName(assembly.Name));
59+
dlg.Filter = ".NET assemblies|*.dll;*.exe;*.winmd" + Resources.AllFiles;
60+
dlg.InitialDirectory = Path.GetDirectoryName(bundleNode.LoadedAssembly.FileName);
61+
if (dlg.ShowDialog() == true)
62+
Save(dockWorkspace, selectedNodes, dlg.FileName, true);
63+
}
64+
else if (selectedNodes is [ResourceTreeNode { Resource: { } resource }])
65+
{
66+
SaveFileDialog dlg = new SaveFileDialog();
67+
dlg.FileName = Path.GetFileName(WholeProjectDecompiler.SanitizeFileName(resource.Name));
68+
dlg.Filter = Resources.AllFiles[1..];
69+
dlg.InitialDirectory = Path.GetDirectoryName(bundleNode.LoadedAssembly.FileName);
70+
if (dlg.ShowDialog() == true)
71+
Save(dockWorkspace, selectedNodes, dlg.FileName, true);
72+
}
73+
else
74+
{
75+
OpenFolderDialog dlg = new OpenFolderDialog();
76+
dlg.InitialDirectory = Path.GetDirectoryName(bundleNode.LoadedAssembly.FileName);
77+
if (dlg.ShowDialog() != true)
78+
return;
79+
80+
string folderName = dlg.FolderName;
81+
if (Directory.EnumerateFileSystemEntries(folderName).Any())
82+
{
83+
var result = MessageBox.Show(
84+
Resources.AssemblySaveCodeDirectoryNotEmpty,
85+
Resources.AssemblySaveCodeDirectoryNotEmptyTitle,
86+
MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No);
87+
if (result == MessageBoxResult.No)
88+
return;
89+
}
5990

60-
string fileName = dlg.FileName;
61-
string outputFolderOrFileName = fileName;
62-
if (selectedNodes.Length > 1)
63-
outputFolderOrFileName = Path.GetDirectoryName(outputFolderOrFileName);
91+
Save(dockWorkspace, selectedNodes, folderName, false);
92+
}
93+
}
6494

95+
static string GetPackageFolderPath(SharpTreeNode node)
96+
{
97+
string name = "";
98+
while (node is PackageFolderTreeNode)
99+
{
100+
name = Path.Combine(node.Text.ToString(), name);
101+
node = node.Parent;
102+
}
103+
return name;
104+
}
105+
106+
internal static void Save(DockWorkspace dockWorkspace, IEnumerable<SharpTreeNode> nodes, string path, bool isFile)
107+
{
65108
dockWorkspace.RunWithCancellation(ct => Task<AvalonEditTextOutput>.Factory.StartNew(() => {
66109
AvalonEditTextOutput output = new AvalonEditTextOutput();
67110
Stopwatch stopwatch = Stopwatch.StartNew();
68-
stopwatch.Stop();
69-
70-
if (selectedNodes.Length == 1)
71-
{
72-
SaveEntry(output, selectedNodes[0].PackageEntry, outputFolderOrFileName);
73-
}
74-
else
111+
foreach (var node in nodes)
75112
{
76-
foreach (var node in selectedNodes)
113+
if (node is AssemblyTreeNode { PackageEntry: { } assembly })
114+
{
115+
string fileName = isFile ? path : Path.Combine(path, GetPackageFolderPath(node.Parent), assembly.Name);
116+
SaveEntry(output, assembly, fileName);
117+
}
118+
else if (node is ResourceTreeNode { Resource: PackageEntry { } resource })
77119
{
78-
var fileName = Path.GetFileName(WholeProjectDecompiler.SanitizeFileName(node.PackageEntry.Name));
79-
SaveEntry(output, node.PackageEntry, Path.Combine(outputFolderOrFileName, fileName));
120+
string fileName = isFile ? path : Path.Combine(path, GetPackageFolderPath(node.Parent), resource.Name);
121+
SaveEntry(output, resource, fileName);
122+
}
123+
else if (node is PackageFolderTreeNode)
124+
{
125+
node.EnsureLazyChildren();
126+
foreach (var item in node.DescendantsAndSelf())
127+
{
128+
if (item is AssemblyTreeNode { PackageEntry: { } asm })
129+
{
130+
string fileName = Path.Combine(path, GetPackageFolderPath(item.Parent), asm.Name);
131+
SaveEntry(output, asm, fileName);
132+
}
133+
else if (item is ResourceTreeNode { Resource: PackageEntry { } entry })
134+
{
135+
string fileName = Path.Combine(path, GetPackageFolderPath(item.Parent), entry.Name);
136+
SaveEntry(output, entry, fileName);
137+
}
138+
else if (item is PackageFolderTreeNode)
139+
{
140+
Directory.CreateDirectory(Path.Combine(path, GetPackageFolderPath(item)));
141+
}
142+
}
80143
}
81144
}
145+
stopwatch.Stop();
82146
output.WriteLine(Resources.GenerationCompleteInSeconds, stopwatch.Elapsed.TotalSeconds.ToString("F1"));
83147
output.WriteLine();
84-
output.AddButton(null, Resources.OpenExplorer, delegate { Process.Start("explorer", "/select,\"" + fileName + "\""); });
148+
output.AddButton(null, Resources.OpenExplorer, delegate { Process.Start("explorer", isFile ? $"/select,\"{path}\"" : $"\"{path}\""); });
85149
output.WriteLine();
86150
return output;
87151
}, ct)).Then(dockWorkspace.ShowText).HandleExceptions();
88152
}
89153

90-
void SaveEntry(ITextOutput output, PackageEntry entry, string targetFileName)
154+
static void SaveEntry(ITextOutput output, PackageEntry entry, string targetFileName)
91155
{
92156
output.Write(entry.Name + ": ");
157+
targetFileName = WholeProjectDecompiler.SanitizeFileName(targetFileName);
93158
using Stream stream = entry.TryOpenStream();
94159
if (stream == null)
95160
{
@@ -105,11 +170,58 @@ void SaveEntry(ITextOutput output, PackageEntry entry, string targetFileName)
105170

106171
public bool IsEnabled(TextViewContext context) => true;
107172

173+
public bool IsVisible(TextViewContext context) => context.SelectedTreeNodes?.Any(IsBundleItem) == true;
174+
175+
static bool IsBundleItem(SharpTreeNode node)
176+
{
177+
if (node is AssemblyTreeNode { PackageEntry: { } } or PackageFolderTreeNode)
178+
return true;
179+
if (node is ResourceTreeNode { Resource: PackageEntry { } resource } && resource.FullName.StartsWith("bundle://"))
180+
return true;
181+
return false;
182+
}
183+
}
184+
185+
[ExportContextMenuEntry(Header = nameof(Resources.ExtractAllPackageEntries), Category = nameof(Resources.Save), Icon = "Images/Save")]
186+
[Shared]
187+
sealed class ExtractAllPackageEntriesContextMenuEntry(DockWorkspace dockWorkspace) : IContextMenuEntry
188+
{
189+
public void Execute(TextViewContext context)
190+
{
191+
if (context.SelectedTreeNodes is not [AssemblyTreeNode { PackageEntry: null } asm])
192+
return;
193+
OpenFolderDialog dlg = new OpenFolderDialog();
194+
dlg.InitialDirectory = Path.GetDirectoryName(asm.LoadedAssembly.FileName);
195+
if (dlg.ShowDialog() != true)
196+
return;
197+
198+
string folderName = dlg.FolderName;
199+
if (Directory.EnumerateFileSystemEntries(folderName).Any())
200+
{
201+
var result = MessageBox.Show(
202+
Resources.AssemblySaveCodeDirectoryNotEmpty,
203+
Resources.AssemblySaveCodeDirectoryNotEmptyTitle,
204+
MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No);
205+
if (result == MessageBoxResult.No)
206+
return;
207+
}
208+
209+
asm.EnsureLazyChildren();
210+
ExtractPackageEntryContextMenuEntry.Save(dockWorkspace, asm.Descendants(), folderName, false);
211+
}
212+
213+
public bool IsEnabled(TextViewContext context) => true;
214+
108215
public bool IsVisible(TextViewContext context)
109216
{
110-
var selectedNodes = context.SelectedTreeNodes?.OfType<AssemblyTreeNode>()
111-
.Where(asm => asm.PackageEntry != null) ?? Enumerable.Empty<AssemblyTreeNode>();
112-
return selectedNodes.Any();
217+
if (context.SelectedTreeNodes is [AssemblyTreeNode { LoadedAssembly.IsLoaded: true, LoadedAssembly.HasLoadError: false, PackageEntry: null } asm])
218+
{
219+
// Using .GetAwaiter().GetResult() is no problem here, since we already checked IsLoaded and HasLoadError.
220+
var loadResult = asm.LoadedAssembly.GetLoadResultAsync().GetAwaiter().GetResult();
221+
if (loadResult.Package is { Kind: PackageKind.Bundle })
222+
return true;
223+
}
224+
return false;
113225
}
114226
}
115227
}

ILSpy/Properties/Resources.Designer.cs

Lines changed: 14 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ILSpy/Properties/Resources.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,9 @@ Are you sure you want to continue?</value>
627627
<data name="ExpandUsingDeclarationsAfterDecompilation" xml:space="preserve">
628628
<value>Expand using declarations after decompilation</value>
629629
</data>
630+
<data name="ExtractAllPackageEntries" xml:space="preserve">
631+
<value>Extract all package entries</value>
632+
</data>
630633
<data name="ExtractPackageEntry" xml:space="preserve">
631634
<value>Extract package entry</value>
632635
</data>

ILSpy/Properties/Resources.zh-Hans.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,9 @@
585585
<data name="ExpandUsingDeclarationsAfterDecompilation" xml:space="preserve">
586586
<value>反编译后展开引用和声明</value>
587587
</data>
588+
<data name="ExtractAllPackageEntries" xml:space="preserve">
589+
<value>提取所有包条目</value>
590+
</data>
588591
<data name="ExtractPackageEntry" xml:space="preserve">
589592
<value>提取包条目</value>
590593
</data>

0 commit comments

Comments
 (0)