-
-
Notifications
You must be signed in to change notification settings - Fork 453
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support for saving files in bundle #287
base: feature/single-file-bundles
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
using System.Text; | ||
using System; | ||
using System.IO; | ||
using System.Collections.Generic; | ||
|
||
namespace dnSpy.AsmEditor.Bundle { | ||
/// <summary> | ||
/// Cleans bundle entry name | ||
/// </summary> | ||
public static class BundleNameCleaner { | ||
static readonly HashSet<char> invalidFileNameChar = new HashSet<char>(); | ||
static BundleNameCleaner() { | ||
foreach (var c in Path.GetInvalidFileNameChars()) | ||
invalidFileNameChar.Add(c); | ||
foreach (var c in Path.GetInvalidPathChars()) | ||
invalidFileNameChar.Add(c); | ||
} | ||
|
||
public static string GetCleanedPath(string s, bool useSubDirs) { | ||
if (!useSubDirs) | ||
return FixFileNamePart(GetFileName(s)); | ||
|
||
string res = string.Empty; | ||
foreach (var part in s.Replace('/', '\\').Split('\\')) | ||
res = Path.Combine(res, FixFileNamePart(part)); | ||
return res; | ||
} | ||
|
||
public static string GetFileName(string s) { | ||
int index = Math.Max(s.LastIndexOf('/'), s.LastIndexOf('\\')); | ||
if (index < 0) | ||
return s; | ||
return s.Substring(index + 1); | ||
} | ||
|
||
public static string FixFileNamePart(string s) { | ||
var sb = new StringBuilder(s.Length); | ||
|
||
foreach (var c in s) { | ||
if (invalidFileNameChar.Contains(c)) | ||
sb.Append('_'); | ||
else | ||
sb.Append(c); | ||
} | ||
|
||
return sb.ToString(); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Diagnostics; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Windows; | ||
using System.Windows.Threading; | ||
using dnSpy.AsmEditor.Properties; | ||
using dnSpy.Contracts.App; | ||
using dnSpy.Contracts.Bundles; | ||
using dnSpy.Contracts.MVVM; | ||
using dnSpy.Contracts.MVVM.Dialogs; | ||
using Ookii.Dialogs.Wpf; | ||
using WF = System.Windows.Forms; | ||
|
||
namespace dnSpy.AsmEditor.Bundle { | ||
/// <summary> | ||
/// For saving bundle entries | ||
/// </summary> | ||
public static class SaveBundle { | ||
|
||
/// <summary> | ||
Comment on lines
+20
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unnecessary empty line here |
||
/// Gets the save path of each files in bundle | ||
/// </summary> | ||
/// <param name="infos"></param> | ||
/// <param name="useSubDirs"></param> | ||
/// <returns></returns> | ||
Comment on lines
+25
to
+27
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing XML documentation |
||
static IEnumerable<(BundleEntry data, string filename)> GetFiles(BundleEntry[] infos, bool useSubDirs) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The name chosen for the first parameter, |
||
if (infos.Length == 1) { | ||
var info = infos[0]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
var name = BundleNameCleaner.FixFileNamePart(BundleNameCleaner.GetFileName(info.FileName)); | ||
var dlg = new WF.SaveFileDialog { | ||
Filter = PickFilenameConstants.AnyFilenameFilter, | ||
RestoreDirectory = true, | ||
ValidateNames = true, | ||
FileName = name, | ||
}; | ||
var ext = Path.GetExtension(name); | ||
dlg.DefaultExt = string.IsNullOrEmpty(ext) ? string.Empty : ext.Substring(1); | ||
if (dlg.ShowDialog() != WF.DialogResult.OK) | ||
yield break; | ||
yield return (info, dlg.FileName); | ||
} | ||
else { | ||
var dlg = new VistaFolderBrowserDialog(); | ||
if (dlg.ShowDialog() != true) | ||
yield break; | ||
string baseDir = dlg.SelectedPath; | ||
foreach (var info in infos) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
var name = BundleNameCleaner.GetCleanedPath(info.FileName, useSubDirs); | ||
var pathName = Path.Combine(baseDir, name); | ||
yield return (info, pathName); | ||
} | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Saves the bundle entry nodes | ||
/// </summary> | ||
/// <param name="entries">Nodes</param> | ||
Comment on lines
+57
to
+60
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update XML documentation, we are not dealing with nodes here! |
||
/// <param name="title">true to create sub directories, false to dump everything in the same folder</param> | ||
public static void Save(BundleEntry[] entries, string title) { | ||
if (entries is null) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This check is unnecessary, the |
||
return; | ||
|
||
(BundleEntry bundleEntry, string filename)[] bundleSaveInfo; | ||
try { | ||
bundleSaveInfo = GetFiles(entries, true).ToArray(); | ||
} | ||
catch (Exception ex) { | ||
MsgBox.Instance.Show(ex); | ||
return; | ||
} | ||
if (bundleSaveInfo.Length == 0) | ||
return; | ||
|
||
var data = new ProgressVM(Dispatcher.CurrentDispatcher, new BundleSaver(bundleSaveInfo)); | ||
var win = new ProgressDlg(); | ||
win.DataContext = data; | ||
win.Owner = Application.Current.MainWindow; | ||
win.Title = title; | ||
var res = win.ShowDialog(); | ||
if (res != true) | ||
return; | ||
if (!data.WasError) | ||
return; | ||
MsgBox.Instance.Show(string.Format(dnSpy_AsmEditor_Resources.SaveBundleError, data.ErrorMessage)); | ||
} | ||
|
||
sealed class BundleSaver : IProgressTask { | ||
public bool IsIndeterminate => false; | ||
public double ProgressMinimum => 0; | ||
public double ProgressMaximum => bundleSaveInfo.Length; | ||
|
||
readonly (BundleEntry bundleEntry, string filename)[] bundleSaveInfo; | ||
|
||
public BundleSaver((BundleEntry bundleEntry, string filename)[] bundleSaveInfo) => this.bundleSaveInfo = bundleSaveInfo; | ||
|
||
public void Execute(IProgress progress) { | ||
for (int i = 0; i < bundleSaveInfo.Length; i++) { | ||
progress.ThrowIfCancellationRequested(); | ||
var saveInfo = bundleSaveInfo[i]; | ||
progress.SetDescription(saveInfo.filename); | ||
progress.SetTotalProgress(i); | ||
Directory.CreateDirectory(Path.GetDirectoryName(saveInfo.filename)!); | ||
try { | ||
byte[]? data = saveInfo.bundleEntry.GetEntryData(); | ||
Debug2.Assert(data != null); | ||
File.WriteAllBytes(saveInfo.filename, data); | ||
} | ||
catch { | ||
try { File.Delete(saveInfo.filename); } | ||
catch { } | ||
throw; | ||
} | ||
} | ||
progress.SetTotalProgress(bundleSaveInfo.Length); | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,85 @@ | ||||||||
using System.ComponentModel.Composition; | ||||||||
using System.Diagnostics; | ||||||||
using System.Linq; | ||||||||
using dnSpy.AsmEditor.Commands; | ||||||||
using dnSpy.AsmEditor.Properties; | ||||||||
using dnSpy.Contracts.Documents; | ||||||||
using dnSpy.Contracts.Documents.TreeView; | ||||||||
using dnSpy.Contracts.Menus; | ||||||||
using dnSpy.Contracts.TreeView; | ||||||||
|
||||||||
namespace dnSpy.AsmEditor.Bundle { | ||||||||
sealed class SaveBundleContentsCommand { | ||||||||
[ExportMenuItem(Header = "res:SaveBundleContents", Group = MenuConstants.GROUP_CTX_DOCUMENTS_ASMED_BUNDLE, Order = 0)] | ||||||||
sealed class DocumentsCommand : DocumentsContextMenuHandler { | ||||||||
public override bool IsVisible(AsmEditorContext context) => SaveBundleContentsCommand.CanExecute(context); | ||||||||
public override void Execute(AsmEditorContext context) => SaveBundleContentsCommand.Execute(context); | ||||||||
} | ||||||||
|
||||||||
[ExportMenuItem(OwnerGuid = MenuConstants.APP_MENU_EDIT_GUID, Header = "res:SaveBundleContents", Group = MenuConstants.GROUP_APP_MENU_EDIT_ASMED_BUNDLE, Order = 0)] | ||||||||
sealed class EditMenuCommand : EditMenuHandler { | ||||||||
[ImportingConstructor] | ||||||||
public EditMenuCommand(IAppService appService) : base(appService.DocumentTreeView) { | ||||||||
} | ||||||||
public override bool IsVisible(AsmEditorContext context) => SaveBundleContentsCommand.CanExecute(context); | ||||||||
Comment on lines
+23
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. New line between constructor and methods with statement bodies |
||||||||
public override void Execute(AsmEditorContext context) => SaveBundleContentsCommand.Execute(context); | ||||||||
} | ||||||||
|
||||||||
static bool IsSingleFileBundle(AsmEditorContext context) => context.Nodes.Length == 1 && context.Nodes[0] is BundleDocumentNode; | ||||||||
static bool CanExecute(AsmEditorContext context) => SaveBundleContentsCommand.IsSingleFileBundle(context); | ||||||||
static void Execute(AsmEditorContext context) { | ||||||||
Comment on lines
+29
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. New line between method with full body and method between statement body |
||||||||
var docNode = context.Nodes[0].GetDocumentNode(); | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
We can just safe cast it as the |
||||||||
var bundleDoc = docNode!.Document as DsBundleDocument; | ||||||||
Debug2.Assert(bundleDoc != null); | ||||||||
Debug2.Assert(bundleDoc.SingleFileBundle != null); | ||||||||
Comment on lines
+33
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use |
||||||||
SaveBundle.Save(bundleDoc.SingleFileBundle.Entries.ToArray(), dnSpy_AsmEditor_Resources.SaveBundleContents); | ||||||||
} | ||||||||
} | ||||||||
|
||||||||
sealed class SaveRawEntryCommand { | ||||||||
[ExportMenuItem(Header = "res:SaveRawEntry", Group = MenuConstants.GROUP_CTX_DOCUMENTS_ASMED_BUNDLE, Order = 1)] | ||||||||
sealed class DocumentsCommand : DocumentsContextMenuHandler { | ||||||||
public override bool IsVisible(AsmEditorContext context) => SaveRawEntryCommand.CanExecute(context); | ||||||||
public override void Execute(AsmEditorContext context) => SaveRawEntryCommand.Execute(context); | ||||||||
} | ||||||||
|
||||||||
[ExportMenuItem(OwnerGuid = MenuConstants.APP_MENU_EDIT_GUID, Header = "res:SaveRawEntry", Group = MenuConstants.GROUP_APP_MENU_EDIT_ASMED_BUNDLE, Order = 1)] | ||||||||
sealed class EditMenuCommand : EditMenuHandler { | ||||||||
[ImportingConstructor] | ||||||||
public EditMenuCommand(IAppService appService) : base(appService.DocumentTreeView) { | ||||||||
} | ||||||||
public override bool IsVisible(AsmEditorContext context) => SaveRawEntryCommand.CanExecute(context); | ||||||||
Comment on lines
+50
to
+51
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. New line between constructor and methods with statement bodies |
||||||||
public override void Execute(AsmEditorContext context) => SaveRawEntryCommand.Execute(context); | ||||||||
} | ||||||||
|
||||||||
static bool IsBundleSingleSelection(AsmEditorContext context) => context.Nodes.Length == 1 && context.Nodes[0] is IBundleEntryNode; | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Check whether the |
||||||||
static bool CanExecute(AsmEditorContext context) => SaveRawEntryCommand.IsBundleSingleSelection(context); | ||||||||
static void Execute(AsmEditorContext context) { | ||||||||
Comment on lines
+56
to
+57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. New line between method with full body and method between statement body |
||||||||
var bundleEntryNode = (IBundleEntryNode)context.Nodes[0]; | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Use safe cast here and debug assert. Furthermore, please assert that |
||||||||
SaveBundle.Save([bundleEntryNode.BundleEntry!], dnSpy_AsmEditor_Resources.SaveRawEntry); | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please use the old |
||||||||
} | ||||||||
} | ||||||||
|
||||||||
class SaveRawEntriesCommand { | ||||||||
[ExportMenuItem(Header = "res:SaveRawEntries", Group = MenuConstants.GROUP_CTX_DOCUMENTS_ASMED_BUNDLE, Order = 2)] | ||||||||
sealed class DocumentsCommand : DocumentsContextMenuHandler { | ||||||||
public override bool IsVisible(AsmEditorContext context) => SaveRawEntriesCommand.CanExecute(context); | ||||||||
public override void Execute(AsmEditorContext context) => SaveRawEntriesCommand.Execute(context); | ||||||||
} | ||||||||
|
||||||||
[ExportMenuItem(OwnerGuid = MenuConstants.APP_MENU_EDIT_GUID, Header = "res:SaveRawEntries", Group = MenuConstants.GROUP_APP_MENU_EDIT_ASMED_BUNDLE, Order = 2)] | ||||||||
sealed class EditMenuCommand : EditMenuHandler { | ||||||||
[ImportingConstructor] | ||||||||
public EditMenuCommand(IAppService appService) : base(appService.DocumentTreeView) { | ||||||||
} | ||||||||
public override bool IsVisible(AsmEditorContext context) => SaveRawEntriesCommand.CanExecute(context); | ||||||||
Comment on lines
+74
to
+75
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. New line between constructor and methods with statement bodies |
||||||||
public override void Execute(AsmEditorContext context) => SaveRawEntriesCommand.Execute(context); | ||||||||
} | ||||||||
private static bool IsBundleMultipleSelection(AsmEditorContext context) => context.Nodes.Length > 1 && context.Nodes.All(node => node is IBundleEntryNode); | ||||||||
Comment on lines
+77
to
+78
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. New line between method and class declarations.
|
||||||||
static bool CanExecute(AsmEditorContext context) => SaveRawEntriesCommand.IsBundleMultipleSelection(context); | ||||||||
static void Execute(AsmEditorContext context) { | ||||||||
Comment on lines
+79
to
+80
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. New line between method with full body and method between statement body |
||||||||
var bundleEntries = context.Nodes.Select(x => ((IBundleEntryNode)x).BundleEntry!); | ||||||||
SaveBundle.Save(bundleEntries.ToArray(), dnSpy_AsmEditor_Resources.SaveRawEntries); | ||||||||
} | ||||||||
} | ||||||||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New empty line between method definition and field definition