Dnspy/Extensions/dnSpy.AsmEditor/Hex/MDTableContextMenuCommands.cs
2021-09-20 18:20:01 +02:00

424 lines
18 KiB
C#

/*
Copyright (C) 2014-2019 de4dot@gmail.com
This file is part of dnSpy
dnSpy is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
dnSpy is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with dnSpy. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.ComponentModel.Composition;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using dnSpy.AsmEditor.Hex.Nodes;
using dnSpy.AsmEditor.Hex.PE;
using dnSpy.AsmEditor.Properties;
using dnSpy.AsmEditor.Utilities;
using dnSpy.Contracts.App;
using dnSpy.Contracts.Decompiler;
using dnSpy.Contracts.Documents.Tabs;
using dnSpy.Contracts.Images;
using dnSpy.Contracts.Menus;
using dnSpy.Contracts.MVVM;
using dnSpy.Contracts.Text;
using dnSpy.Contracts.Utilities;
namespace dnSpy.AsmEditor.Hex {
[Export(typeof(IInitializeDataTemplate))]
sealed class InitializeMDTableKeyboardShortcuts : IInitializeDataTemplate {
readonly IDocumentTabService documentTabService;
[ImportingConstructor]
InitializeMDTableKeyboardShortcuts(IDocumentTabService documentTabService) => this.documentTabService = documentTabService;
public void Initialize(DependencyObject d) {
var lv = d as ListView;
if (lv is null)
return;
if (!(lv.DataContext is MetadataTableVM))
return;
lv.InputBindings.Add(new KeyBinding(new CtxMenuMDTableCommandProxy(documentTabService, new SortMDTableCommand.TheMenuMDTableCommand()), Key.T, ModifierKeys.Shift | ModifierKeys.Control));
lv.InputBindings.Add(new KeyBinding(new CtxMenuMDTableCommandProxy(documentTabService, new CopyAsTextMDTableCommand.TheMenuMDTableCommand()), Key.C, ModifierKeys.Shift | ModifierKeys.Control));
lv.InputBindings.Add(new KeyBinding(new CtxMenuMDTableCommandProxy(documentTabService, new GoToRidMDTableCommand.TheMenuMDTableCommand()), Key.G, ModifierKeys.Control));
lv.InputBindings.Add(new KeyBinding(new CtxMenuMDTableCommandProxy(documentTabService, new ShowInHexEditorMDTableCommand.TheMenuMDTableCommand(documentTabService)), Key.X, ModifierKeys.Control));
Add(lv, ApplicationCommands.Copy, new CtxMenuMDTableCommandProxy(documentTabService, new CopyMDTableCommand.TheMenuMDTableCommand()));
Add(lv, ApplicationCommands.Paste, new CtxMenuMDTableCommandProxy(documentTabService, new PasteMDTableCommand.TheMenuMDTableCommand()));
}
static void Add(UIElement elem, ICommand cmd, ICommand realCmd) => elem.CommandBindings.Add(new CommandBinding(cmd, (s, e) => realCmd.Execute(e.Parameter), (s, e) => e.CanExecute = realCmd.CanExecute(e.Parameter)));
}
sealed class MDTableContext {
public ListView ListView { get; }
public MetadataTableVM MetadataTableVM { get; }
public MetadataTableNode Node { get; }
public MetadataTableRecordVM[] Records { get; }
public bool IsContextMenu { get; }
public MDTableContext(ListView listView, MetadataTableVM mdVM, MetadataTableNode mdNode, bool isContextMenu) {
ListView = listView;
MetadataTableVM = mdVM;
Node = mdNode;
Records = listView.SelectedItems.Cast<MetadataTableRecordVM>().OrderBy(a => a.Span.Start).ToArray();
IsContextMenu = isContextMenu;
}
public bool ContiguousRecords() {
if (Records.Length <= 1)
return true;
for (int i = 1; i < Records.Length; i++) {
if (Records[i - 1].Span.End != Records[i].Span.Start)
return false;
}
return true;
}
}
sealed class CtxMenuMDTableCommandProxy : ICommand {
readonly IDocumentTabService documentTabService;
readonly MenuItemBase<MDTableContext> cmd;
public CtxMenuMDTableCommandProxy(IDocumentTabService documentTabService, MenuItemBase<MDTableContext> cmd) {
this.documentTabService = documentTabService;
this.cmd = cmd;
}
MDTableContext? CreateMDTableContext() {
var tab = documentTabService.ActiveTab;
if (tab is not null) {
var listView = FindListView(tab);
if (listView is not null && UIUtils.HasSelectedChildrenFocus(listView))
return MenuMDTableCommand.ToMDTableContext(listView, false);
}
return null;
}
static ListView? FindListView(IDocumentTab tab) {
var o = tab.UIContext.UIObject as DependencyObject;
while (o is not null) {
if (o is ListView lv && InitDataTemplateAP.GetInitialize(lv))
return lv;
var children = UIUtils.GetChildren(o).ToArray();
if (children.Length != 1)
return null;
o = children[0];
}
return null;
}
event EventHandler? ICommand.CanExecuteChanged {
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
bool ICommand.CanExecute(object? parameter) {
var ctx = CreateMDTableContext();
return ctx is not null && cmd.IsVisible(ctx) && cmd.IsEnabled(ctx);
}
void ICommand.Execute(object? parameter) {
var ctx = CreateMDTableContext();
if (ctx is not null)
cmd.Execute(ctx);
}
}
abstract class CtxMenuMDTableCommand : MenuItemBase<MDTableContext> {
protected sealed override object CachedContextKey => ContextKey;
static readonly object ContextKey = new object();
protected sealed override MDTableContext? CreateContext(IMenuItemContext context) => MenuMDTableCommand.ToMDTableContext(context.CreatorObject.Object, true);
}
abstract class MenuMDTableCommand : MenuItemBase<MDTableContext> {
protected sealed override object CachedContextKey => ContextKey;
static readonly object ContextKey = new object();
protected sealed override MDTableContext? CreateContext(IMenuItemContext context) => ToMDTableContext(context.CreatorObject.Object, false);
internal static MDTableContext? ToMDTableContext(object? obj, bool isContextMenu) => ToMDTableContext(obj as ListView, isContextMenu);
internal static MDTableContext? ToMDTableContext(ListView? listView, bool isContextMenu) {
if (listView is null)
return null;
var mdVM = listView.DataContext as MetadataTableVM;
if (mdVM is null)
return null;
return new MDTableContext(listView, mdVM, (MetadataTableNode)mdVM.Owner!, isContextMenu);
}
}
static class SortMDTableCommand {
[ExportMenuItem(Header = "res:SortMetadataTableCommand", InputGestureText = "res:ShortCutKeyCtrlShiftT", Group = MenuConstants.GROUP_CTX_DOCVIEWER_HEX_MD, Order = 0)]
sealed class TheCtxMenuMDTableCommand : CtxMenuMDTableCommand {
public override void Execute(MDTableContext context) => ExecuteInternal(context);
public override bool IsEnabled(MDTableContext context) => IsEnabledInternal(context);
}
[ExportMenuItem(OwnerGuid = MenuConstants.APP_MENU_EDIT_GUID, Header = "res:SortMetadataTableCommand", Group = MenuConstants.GROUP_APP_MENU_EDIT_HEX_MD, InputGestureText = "res:ShortCutKeyCtrlShiftT", Order = 0)]
internal sealed class TheMenuMDTableCommand : MenuMDTableCommand {
public override void Execute(MDTableContext context) => ExecuteInternal(context);
public override bool IsEnabled(MDTableContext context) => IsEnabledInternal(context);
}
static void ExecuteInternal(MDTableContext context) =>
SortTable(context.MetadataTableVM, 1, context.MetadataTableVM.Rows);
internal static void SortTable(MetadataTableVM mdTblVM, uint rid, uint count) {
var buffer = mdTblVM.Buffer;
int len = (int)count * mdTblVM.TableInfo.RowSize;
var data = new byte[len];
var startOffset = mdTblVM.Span.Start + (rid - 1) * (ulong)mdTblVM.TableInfo.RowSize;
buffer.ReadBytes(startOffset, data);
TableSorter.Sort(mdTblVM.TableInfo, data);
HexBufferWriterHelper.Write(buffer, startOffset, data);
}
static bool IsEnabledInternal(MDTableContext context) => TableSorter.CanSort(context.MetadataTableVM.TableInfo);
}
static class SortSelectionMDTableCommand {
[ExportMenuItem(Header = "res:SortSelectionCommand", Group = MenuConstants.GROUP_CTX_DOCVIEWER_HEX_MD, Order = 10)]
sealed class TheCtxMenuMDTableCommand : CtxMenuMDTableCommand {
public override void Execute(MDTableContext context) => ExecuteInternal(context);
public override bool IsEnabled(MDTableContext context) => IsEnabledInternal(context);
}
[ExportMenuItem(OwnerGuid = MenuConstants.APP_MENU_EDIT_GUID, Header = "res:SortSelectionCommand", Group = MenuConstants.GROUP_APP_MENU_EDIT_HEX_MD, Order = 10)]
sealed class TheMenuMDTableCommand : MenuMDTableCommand {
public override void Execute(MDTableContext context) => ExecuteInternal(context);
public override bool IsEnabled(MDTableContext context) => IsEnabledInternal(context);
}
static void ExecuteInternal(MDTableContext context) {
uint rid = context.Records[0].Token.Rid;
uint count = (uint)context.Records.Length;
SortMDTableCommand.SortTable(context.MetadataTableVM, rid, count);
}
static bool IsEnabledInternal(MDTableContext context) =>
TableSorter.CanSort(context.MetadataTableVM.TableInfo) &&
context.Records.Length > 1 &&
context.ContiguousRecords();
}
static class GoToRidMDTableCommand {
[ExportMenuItem(Header = "res:GoToRowIdentifierCommand", InputGestureText = "res:ShortCutKeyCtrlG", Group = MenuConstants.GROUP_CTX_DOCVIEWER_HEX_MD, Order = 20)]
sealed class TheCtxMenuMDTableCommand : CtxMenuMDTableCommand {
public override void Execute(MDTableContext context) => ExecuteInternal(context);
}
[ExportMenuItem(OwnerGuid = MenuConstants.APP_MENU_EDIT_GUID, Header = "res:GoToRowIdentifierCommand", InputGestureText = "res:ShortCutKeyCtrlG", Group = MenuConstants.GROUP_APP_MENU_EDIT_HEX_MD, Order = 20)]
internal sealed class TheMenuMDTableCommand : MenuMDTableCommand {
public override void Execute(MDTableContext context) => ExecuteInternal(context);
}
static void ExecuteInternal(MDTableContext context) {
var recVM = Ask(dnSpy_AsmEditor_Resources.GoToRowIdentifier_Title, context);
if (recVM is not null)
UIUtils.ScrollSelectAndSetFocus(context.ListView, recVM);
}
static MetadataTableRecordVM? Ask(string title, MDTableContext context) => MsgBox.Instance.Ask(dnSpy_AsmEditor_Resources.GoToMetaDataTableRow_RID, null, title, s => {
uint rid = SimpleTypeConverter.ParseUInt32(s, 1, context.MetadataTableVM.Rows, out var error);
if (!string.IsNullOrEmpty(error))
return null;
return context.MetadataTableVM.Get((int)(rid - 1));
}, s => {
uint rid = SimpleTypeConverter.ParseUInt32(s, 1, context.MetadataTableVM.Rows, out var error);
if (!string.IsNullOrEmpty(error))
return error;
if (rid == 0 || rid > context.MetadataTableVM.Rows)
return string.Format(dnSpy_AsmEditor_Resources.GoToRowIdentifier_InvalidRowIdentifier, rid);
return string.Empty;
});
}
static class ShowInHexEditorMDTableCommand {
[ExportMenuItem(Header = "res:ShowInHexEditorCommand", Icon = DsImagesAttribute.Binary, InputGestureText = "res:ShortCutKeyCtrlX", Group = MenuConstants.GROUP_CTX_DOCVIEWER_HEX_MD, Order = 30)]
sealed class TheCtxMenuMDTableCommand : CtxMenuMDTableCommand {
readonly IDocumentTabService documentTabService;
[ImportingConstructor]
TheCtxMenuMDTableCommand(IDocumentTabService documentTabService) => this.documentTabService = documentTabService;
public override void Execute(MDTableContext context) => ExecuteInternal(documentTabService, context);
public override bool IsEnabled(MDTableContext context) => IsEnabledInternal(context);
}
[ExportMenuItem(OwnerGuid = MenuConstants.APP_MENU_EDIT_GUID, Header = "res:ShowInHexEditorCommand", Icon = DsImagesAttribute.Binary, InputGestureText = "res:ShortCutKeyCtrlX", Group = MenuConstants.GROUP_APP_MENU_EDIT_HEX_MD, Order = 30)]
internal sealed class TheMenuMDTableCommand : MenuMDTableCommand {
readonly IDocumentTabService documentTabService;
[ImportingConstructor]
public TheMenuMDTableCommand(IDocumentTabService documentTabService) => this.documentTabService = documentTabService;
public override void Execute(MDTableContext context) => ExecuteInternal(documentTabService, context);
public override bool IsEnabled(MDTableContext context) => IsEnabledInternal(context);
}
static void ExecuteInternal(IDocumentTabService documentTabService, MDTableContext context) {
var @ref = GetAddressReference(context);
if (@ref is not null)
documentTabService.FollowReference(@ref);
}
static bool IsEnabledInternal(MDTableContext context) => GetAddressReference(context) is not null;
static AddressReference? GetAddressReference(MDTableContext context) {
if (context.Records.Length == 0)
return null;
if (!context.ContiguousRecords())
return null;
var start = context.Records[0].Span.Start;
var end = context.Records[context.Records.Length - 1].Span.End;
return new AddressReference(context.MetadataTableVM.Buffer.Name, false, start.ToUInt64(), (end - start).ToUInt64());
}
}
static class CopyAsTextMDTableCommand {
[ExportMenuItem(Header = "res:CopyAsTextCommand2", InputGestureText = "res:ShortCutKeyCtrlShiftC", Group = MenuConstants.GROUP_CTX_DOCVIEWER_HEX_COPY, Order = 0)]
sealed class TheCtxMenuMDTableCommand : CtxMenuMDTableCommand {
public override void Execute(MDTableContext context) => ExecuteInternal(context);
public override bool IsEnabled(MDTableContext context) => IsEnabledInternal(context);
}
[ExportMenuItem(OwnerGuid = MenuConstants.APP_MENU_EDIT_GUID, Header = "res:CopyAsTextCommand2", InputGestureText = "res:ShortCutKeyCtrlShiftC", Group = MenuConstants.GROUP_APP_MENU_EDIT_HEX_COPY, Order = 0)]
internal sealed class TheMenuMDTableCommand : MenuMDTableCommand {
public override void Execute(MDTableContext context) => ExecuteInternal(context);
public override bool IsEnabled(MDTableContext context) => IsEnabledInternal(context);
}
static void ExecuteInternal(MDTableContext context) {
var output = new StringBuilderTextColorOutput();
var output2 = TextColorWriterToDecompilerOutput.Create(output);
context.Node.WriteHeader(output2);
foreach (var rec in context.Records)
context.Node.Write(output2, rec);
var s = output.ToString();
if (s.Length > 0) {
try {
Clipboard.SetText(s);
}
catch (ExternalException) { }
}
}
static bool IsEnabledInternal(MDTableContext context) => context.Records.Length > 0;
}
static class CopyMDTableCommand {
[ExportMenuItem(Header = "res:CopyCommand", Icon = DsImagesAttribute.Copy, InputGestureText = "res:ShortCutKeyCtrlC", Group = MenuConstants.GROUP_CTX_DOCVIEWER_HEX_COPY, Order = 10)]
sealed class TheCtxMenuMDTableCommand : CtxMenuMDTableCommand {
public override void Execute(MDTableContext context) => ExecuteInternal(context);
public override bool IsEnabled(MDTableContext context) => IsEnabledInternal(context);
}
[ExportMenuItem(OwnerGuid = MenuConstants.APP_MENU_EDIT_GUID, Header = "res:CopyCommand", Icon = DsImagesAttribute.Copy, InputGestureText = "res:ShortCutKeyCtrlC", Group = MenuConstants.GROUP_APP_MENU_EDIT_HEX_COPY, Order = 10)]
internal sealed class TheMenuMDTableCommand : MenuMDTableCommand {
public override void Execute(MDTableContext context) => ExecuteInternal(context);
public override bool IsEnabled(MDTableContext context) => IsEnabledInternal(context);
}
static void ExecuteInternal(MDTableContext context) {
var buffer = context.MetadataTableVM.Buffer;
ulong totalSize = (ulong)context.MetadataTableVM.TableInfo.RowSize * (ulong)context.Records.Length * 2;
if (totalSize >= int.MaxValue) {
MsgBox.Instance.Show(dnSpy_AsmEditor_Resources.TooManyBytesSelected);
return;
}
var sb = new StringBuilder((int)totalSize);
var recData = new byte[context.MetadataTableVM.TableInfo.RowSize];
foreach (var rec in context.Records) {
buffer.ReadBytes(rec.Span.Start, recData);
foreach (var b in recData)
sb.Append(b.ToString("X2"));
}
var s = sb.ToString();
if (s.Length > 0) {
try {
Clipboard.SetText(s);
}
catch (ExternalException) { }
}
}
static bool IsEnabledInternal(MDTableContext context) => context.Records.Length > 0;
}
static class PasteMDTableCommand {
[ExportMenuItem(Header = "res:PasteCommand", Icon = DsImagesAttribute.Paste, InputGestureText = "res:ShortCutKeyCtrlV", Group = MenuConstants.GROUP_CTX_DOCVIEWER_HEX_COPY, Order = 20)]
sealed class TheCtxMenuMDTableCommand : CtxMenuMDTableCommand {
public override void Execute(MDTableContext context) => ExecuteInternal(context);
public override bool IsEnabled(MDTableContext context) => IsEnabledInternal(context);
public override string? GetHeader(MDTableContext context) => GetHeaderInternal(context);
}
[ExportMenuItem(OwnerGuid = MenuConstants.APP_MENU_EDIT_GUID, Header = "res:PasteCommand", Icon = DsImagesAttribute.Paste, InputGestureText = "res:ShortCutKeyCtrlV", Group = MenuConstants.GROUP_APP_MENU_EDIT_HEX_COPY, Order = 20)]
internal sealed class TheMenuMDTableCommand : MenuMDTableCommand {
public override void Execute(MDTableContext context) => ExecuteInternal(context);
public override bool IsEnabled(MDTableContext context) => IsEnabledInternal(context);
public override string? GetHeader(MDTableContext context) => GetHeaderInternal(context);
}
static void ExecuteInternal(MDTableContext context) {
var data = GetPasteData(context);
if (data is null)
return;
var buffer = context.MetadataTableVM.Buffer;
int recs = data.Length / context.MetadataTableVM.TableInfo.RowSize;
HexBufferWriterHelper.Write(buffer, context.Records[0].Span.Start, data);
}
static bool IsEnabledInternal(MDTableContext context) => GetPasteData(context) is not null;
static byte[]? GetPasteData(MDTableContext context) {
if (context.Records.Length == 0)
return null;
var data = ClipboardUtils.GetData(canBeEmpty: false);
if (data is null || data.Length == 0)
return null;
if (data.Length % context.MetadataTableVM.TableInfo.RowSize != 0)
return null;
int recs = data.Length / context.MetadataTableVM.TableInfo.RowSize;
if ((uint)context.Records[0].Index + (uint)recs > context.MetadataTableVM.Rows)
return null;
return data;
}
static string? GetHeaderInternal(MDTableContext context) {
var data = GetPasteData(context);
if (data is null)
return null;
int recs = data.Length / context.MetadataTableVM.TableInfo.RowSize;
if (recs <= 1)
return null;
return string.Format(dnSpy_AsmEditor_Resources.PasteRecordsCommand, recs, context.Records[0].Span.Start.ToUInt64(), context.Records[0].Token.Rid);
}
}
}