Skip to content

Commit a26a6ef

Browse files
Extract multiple package entries
1 parent 96cba4a commit a26a6ef

File tree

4 files changed

+191
-31
lines changed

4 files changed

+191
-31
lines changed

ILSpy/Commands/ExtractPackageEntryContextMenuEntry.cs

Lines changed: 171 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
using System.IO;
2323
using System.Linq;
2424
using System.Threading.Tasks;
25+
using System.Windows;
2526

2627
using ICSharpCode.Decompiler;
2728
using ICSharpCode.Decompiler.CSharp.ProjectDecompiler;
@@ -30,9 +31,12 @@
3031
using ICSharpCode.ILSpy.TextView;
3132
using ICSharpCode.ILSpy.TreeNodes;
3233
using ICSharpCode.ILSpyX;
34+
using ICSharpCode.ILSpyX.TreeView;
3335

3436
using Microsoft.Win32;
3537

38+
using static ICSharpCode.ILSpyX.LoadedPackage;
39+
3640
namespace ICSharpCode.ILSpy
3741
{
3842
[ExportContextMenuEntry(Header = nameof(Resources.ExtractPackageEntry), Category = nameof(Resources.Save), Icon = "Images/Save")]
@@ -41,52 +45,184 @@ sealed class ExtractPackageEntryContextMenuEntry(DockWorkspace dockWorkspace) :
4145
{
4246
public void Execute(TextViewContext context)
4347
{
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();
48+
var selectedNodes = Array.FindAll(context.SelectedTreeNodes, x => x is AssemblyTreeNode { PackageEntry: { } } or PackageFolderTreeNode);
4749
// Get root assembly to infer the initial directory for the save dialog.
4850
var bundleNode = selectedNodes.FirstOrDefault()?.Ancestors().OfType<AssemblyTreeNode>()
4951
.FirstOrDefault(asm => asm.PackageEntry == null);
5052
if (bundleNode == null)
5153
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);
54+
if (selectedNodes is [AssemblyTreeNode { PackageEntry: { } assembly }])
55+
{
56+
SaveFileDialog dlg = new SaveFileDialog();
57+
dlg.FileName = Path.GetFileName(WholeProjectDecompiler.SanitizeFileName(assembly.Name));
58+
dlg.Filter = ".NET assemblies|*.dll;*.exe;*.winmd" + Resources.AllFiles;
59+
dlg.InitialDirectory = Path.GetDirectoryName(bundleNode.LoadedAssembly.FileName);
60+
if (dlg.ShowDialog() != true)
61+
return;
62+
63+
string fileName = dlg.FileName;
64+
dockWorkspace.RunWithCancellation(ct => Task<AvalonEditTextOutput>.Factory.StartNew(() => {
65+
AvalonEditTextOutput output = new AvalonEditTextOutput();
66+
Stopwatch stopwatch = Stopwatch.StartNew();
67+
SaveEntry(output, assembly, fileName);
68+
stopwatch.Stop();
69+
output.WriteLine(Resources.GenerationCompleteInSeconds, stopwatch.Elapsed.TotalSeconds.ToString("F1"));
70+
output.WriteLine();
71+
output.AddButton(null, Resources.OpenExplorer, delegate { Process.Start("explorer", "/select,\"" + fileName + "\""); });
72+
output.WriteLine();
73+
return output;
74+
}, ct)).Then(dockWorkspace.ShowText).HandleExceptions();
75+
}
76+
else
77+
{
78+
OpenFolderDialog dlg = new OpenFolderDialog();
79+
dlg.InitialDirectory = Path.GetDirectoryName(bundleNode.LoadedAssembly.FileName);
80+
if (dlg.ShowDialog() != true)
81+
return;
82+
83+
string folderName = dlg.FolderName;
84+
if (Directory.EnumerateFileSystemEntries(folderName).Any())
85+
{
86+
var result = MessageBox.Show(
87+
Resources.AssemblySaveCodeDirectoryNotEmpty,
88+
Resources.AssemblySaveCodeDirectoryNotEmptyTitle,
89+
MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No);
90+
if (result == MessageBoxResult.No)
91+
return;
92+
}
93+
94+
dockWorkspace.RunWithCancellation(ct => Task<AvalonEditTextOutput>.Factory.StartNew(() => {
95+
AvalonEditTextOutput output = new AvalonEditTextOutput();
96+
Stopwatch stopwatch = Stopwatch.StartNew();
97+
foreach (var selectedNode in selectedNodes)
98+
{
99+
if (selectedNode is AssemblyTreeNode { PackageEntry: { } assembly })
100+
{
101+
string fileName = Path.Combine(folderName, GetFullPath(selectedNode.Parent) + WholeProjectDecompiler.SanitizeFileName(assembly.Name));
102+
SaveEntry(output, assembly, fileName);
103+
}
104+
else if (selectedNode is PackageFolderTreeNode)
105+
{
106+
selectedNode.EnsureLazyChildren();
107+
foreach (var node in selectedNode.DescendantsAndSelf())
108+
{
109+
if (node is AssemblyTreeNode { PackageEntry: { } asm })
110+
{
111+
string fileName = Path.Combine(folderName, GetFullPath(node.Parent) + WholeProjectDecompiler.SanitizeFileName(asm.Name));
112+
SaveEntry(output, asm, fileName);
113+
}
114+
else if (node is PackageFolderTreeNode)
115+
{
116+
Directory.CreateDirectory(Path.Combine(folderName, GetFullPath(node)));
117+
}
118+
}
119+
}
120+
}
121+
stopwatch.Stop();
122+
output.WriteLine(Resources.GenerationCompleteInSeconds, stopwatch.Elapsed.TotalSeconds.ToString("F1"));
123+
output.WriteLine();
124+
output.AddButton(null, Resources.OpenExplorer, delegate { Process.Start("explorer", "\"" + folderName + "\""); });
125+
output.WriteLine();
126+
return output;
127+
}, ct)).Then(dockWorkspace.ShowText).HandleExceptions();
128+
}
129+
}
130+
131+
static string GetFullPath(SharpTreeNode node)
132+
{
133+
if (node is PackageFolderTreeNode)
134+
{
135+
string name = node.Text + "\\";
136+
if (GetFullPath(node.Parent) is string parent)
137+
return parent + "\\" + name;
138+
return name;
139+
}
140+
return null;
141+
}
142+
143+
void SaveEntry(ITextOutput output, PackageEntry entry, string targetFileName)
144+
{
145+
output.Write(entry.Name + ": ");
146+
using Stream stream = entry.TryOpenStream();
147+
if (stream == null)
148+
{
149+
output.WriteLine("Could not open stream!");
150+
return;
151+
}
152+
153+
stream.Position = 0;
154+
using FileStream fileStream = new FileStream(targetFileName, FileMode.OpenOrCreate);
155+
stream.CopyTo(fileStream);
156+
output.WriteLine("Written to " + targetFileName);
157+
}
158+
159+
public bool IsEnabled(TextViewContext context) => true;
160+
161+
public bool IsVisible(TextViewContext context) =>
162+
context.SelectedTreeNodes?.Any(x => x is AssemblyTreeNode { PackageEntry: { } } or PackageFolderTreeNode) == true;
163+
}
164+
165+
[ExportContextMenuEntry(Header = nameof(Resources.ExtractAllPackageEntries), Category = nameof(Resources.Save), Icon = "Images/Save")]
166+
[Shared]
167+
sealed class ExtractAllPackageEntriesContextMenuEntry(DockWorkspace dockWorkspace) : IContextMenuEntry
168+
{
169+
public void Execute(TextViewContext context)
170+
{
171+
if (context.SelectedTreeNodes is not [AssemblyTreeNode { PackageEntry: null } asm])
172+
return;
173+
OpenFolderDialog dlg = new OpenFolderDialog();
174+
dlg.InitialDirectory = Path.GetDirectoryName(asm.LoadedAssembly.FileName);
57175
if (dlg.ShowDialog() != true)
58176
return;
59177

60-
string fileName = dlg.FileName;
61-
string outputFolderOrFileName = fileName;
62-
if (selectedNodes.Length > 1)
63-
outputFolderOrFileName = Path.GetDirectoryName(outputFolderOrFileName);
178+
string folderName = dlg.FolderName;
179+
if (Directory.EnumerateFileSystemEntries(folderName).Any())
180+
{
181+
var result = MessageBox.Show(
182+
Resources.AssemblySaveCodeDirectoryNotEmpty,
183+
Resources.AssemblySaveCodeDirectoryNotEmptyTitle,
184+
MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No);
185+
if (result == MessageBoxResult.No)
186+
return;
187+
}
64188

65189
dockWorkspace.RunWithCancellation(ct => Task<AvalonEditTextOutput>.Factory.StartNew(() => {
66190
AvalonEditTextOutput output = new AvalonEditTextOutput();
67191
Stopwatch stopwatch = Stopwatch.StartNew();
68-
stopwatch.Stop();
69-
70-
if (selectedNodes.Length == 1)
192+
asm.EnsureLazyChildren();
193+
foreach (var node in asm.Descendants())
71194
{
72-
SaveEntry(output, selectedNodes[0].PackageEntry, outputFolderOrFileName);
73-
}
74-
else
75-
{
76-
foreach (var node in selectedNodes)
195+
if (node is AssemblyTreeNode { PackageEntry: { } assembly })
196+
{
197+
string fileName = Path.Combine(folderName, GetFullPath(node.Parent) + WholeProjectDecompiler.SanitizeFileName(assembly.Name));
198+
SaveEntry(output, assembly, fileName);
199+
}
200+
else if (node is PackageFolderTreeNode)
77201
{
78-
var fileName = Path.GetFileName(WholeProjectDecompiler.SanitizeFileName(node.PackageEntry.Name));
79-
SaveEntry(output, node.PackageEntry, Path.Combine(outputFolderOrFileName, fileName));
202+
Directory.CreateDirectory(Path.Combine(folderName, GetFullPath(node)));
80203
}
81204
}
205+
stopwatch.Stop();
82206
output.WriteLine(Resources.GenerationCompleteInSeconds, stopwatch.Elapsed.TotalSeconds.ToString("F1"));
83207
output.WriteLine();
84-
output.AddButton(null, Resources.OpenExplorer, delegate { Process.Start("explorer", "/select,\"" + fileName + "\""); });
208+
output.AddButton(null, Resources.OpenExplorer, delegate { Process.Start("explorer", "\"" + folderName + "\""); });
85209
output.WriteLine();
86210
return output;
87211
}, ct)).Then(dockWorkspace.ShowText).HandleExceptions();
88212
}
89213

214+
static string GetFullPath(SharpTreeNode node)
215+
{
216+
if (node is PackageFolderTreeNode)
217+
{
218+
string name = node.Text + "\\";
219+
if (GetFullPath(node.Parent) is string parent)
220+
return parent + "\\" + name;
221+
return name;
222+
}
223+
return null;
224+
}
225+
90226
void SaveEntry(ITextOutput output, PackageEntry entry, string targetFileName)
91227
{
92228
output.Write(entry.Name + ": ");
@@ -107,9 +243,18 @@ void SaveEntry(ITextOutput output, PackageEntry entry, string targetFileName)
107243

108244
public bool IsVisible(TextViewContext context)
109245
{
110-
var selectedNodes = context.SelectedTreeNodes?.OfType<AssemblyTreeNode>()
111-
.Where(asm => asm.PackageEntry != null) ?? Enumerable.Empty<AssemblyTreeNode>();
112-
return selectedNodes.Any();
246+
if (context.SelectedTreeNodes is [AssemblyTreeNode { PackageEntry: null } asm])
247+
{
248+
try
249+
{
250+
if (asm.LoadedAssembly.GetLoadResultAsync().GetAwaiter().GetResult().Package is { Kind: PackageKind.Bundle })
251+
return true;
252+
}
253+
catch
254+
{
255+
}
256+
}
257+
return false;
113258
}
114259
}
115260
}

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
@@ -624,6 +624,9 @@ Are you sure you want to continue?</value>
624624
<data name="ExpandUsingDeclarationsAfterDecompilation" xml:space="preserve">
625625
<value>Expand using declarations after decompilation</value>
626626
</data>
627+
<data name="ExtractAllPackageEntries" xml:space="preserve">
628+
<value>Extract all package entries</value>
629+
</data>
627630
<data name="ExtractPackageEntry" xml:space="preserve">
628631
<value>Extract package entry</value>
629632
</data>

ILSpy/Properties/Resources.zh-Hans.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,9 @@
582582
<data name="ExpandUsingDeclarationsAfterDecompilation" xml:space="preserve">
583583
<value>反编译后展开引用和声明</value>
584584
</data>
585+
<data name="ExtractAllPackageEntries" xml:space="preserve">
586+
<value>提取所有包条目</value>
587+
</data>
585588
<data name="ExtractPackageEntry" xml:space="preserve">
586589
<value>提取包条目</value>
587590
</data>

0 commit comments

Comments
 (0)