Skip to content
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

Open
wants to merge 1 commit into
base: feature/single-file-bundles
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions Extensions/dnSpy.AsmEditor/Bundle/BundleNameCleaner.cs
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() {
Comment on lines +11 to +12
Copy link
Member

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

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();
}
}
}
121 changes: 121 additions & 0 deletions Extensions/dnSpy.AsmEditor/Bundle/SaveBundle.cs
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
Copy link
Member

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The 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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name chosen for the first parameter, infos, is not really descriptive of the actual parameter. Consider using entries instead.

if (infos.Length == 1) {
var info = infos[0];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

info -> entry

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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

info -> entry

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
Copy link
Member

Choose a reason for hiding this comment

The 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check is unnecessary, the BundleEntry[] entries parameter is already marked as non-nullable. If you belive this method should accept null too, then add the appropriate nullable annotation to the type. (BundleEntry[]?)

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);
}
}
}
}
85 changes: 85 additions & 0 deletions Extensions/dnSpy.AsmEditor/Bundle/SaveBundleContentsCommands.cs
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
Copy link
Member

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The 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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var docNode = context.Nodes[0].GetDocumentNode();
var docNode = context.Nodes[0] as BundleDocumentNode;

We can just safe cast it as the CanExecute method has already checked whether the node is a BundleDocumentNode

var bundleDoc = docNode!.Document as DsBundleDocument;
Debug2.Assert(bundleDoc != null);
Debug2.Assert(bundleDoc.SingleFileBundle != null);
Comment on lines +33 to +34
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use is not null instead of != null

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
Copy link
Member

Choose a reason for hiding this comment

The 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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check whether the IBundleEntryNode.BundleEntry is not null. Implementing this interface does not guarantee that the node has an entry!

static bool CanExecute(AsmEditorContext context) => SaveRawEntryCommand.IsBundleSingleSelection(context);
static void Execute(AsmEditorContext context) {
Comment on lines +56 to +57
Copy link
Member

Choose a reason for hiding this comment

The 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];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var bundleEntryNode = (IBundleEntryNode)context.Nodes[0];
var bundleEntryNode = context.Nodes[0] as IBundleEntryNode;
Debug2.Assert(bundleEntryNode is not null);

Use safe cast here and debug assert. Furthermore, please assert that bundleEntryNode.BundleEntry is not null

SaveBundle.Save([bundleEntryNode.BundleEntry!], dnSpy_AsmEditor_Resources.SaveRawEntry);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the old new BundleEntry[] { bundleEntryNode.BundleEntry } syntax instead

}
}

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
Copy link
Member

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New line between method and class declarations.

context.Nodes.All(node => node is IBundleEntryNode) should check whether the IBundleEntryNode.BundleEntry property is not null too.

static bool CanExecute(AsmEditorContext context) => SaveRawEntriesCommand.IsBundleMultipleSelection(context);
static void Execute(AsmEditorContext context) {
Comment on lines +79 to +80
Copy link
Member

Choose a reason for hiding this comment

The 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.

Loading