/*
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 .
*/
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Diagnostics;
using System.Linq;
using dnlib.DotNet;
using dnlib.PE;
using dnSpy.AsmEditor.Commands;
using dnSpy.AsmEditor.Properties;
using dnSpy.AsmEditor.SaveModule;
using dnSpy.AsmEditor.UndoRedo;
using dnSpy.Contracts.Controls;
using dnSpy.Contracts.Documents;
using dnSpy.Contracts.Documents.Tabs;
using dnSpy.Contracts.Documents.TreeView;
using dnSpy.Contracts.Extension;
using dnSpy.Contracts.Images;
using dnSpy.Contracts.Menus;
using dnSpy.Contracts.TreeView;
using dnSpy.Contracts.Utilities;
namespace dnSpy.AsmEditor.Assembly {
[ExportAutoLoaded]
sealed class CommandLoader : IAutoLoaded {
[ImportingConstructor]
CommandLoader(IWpfCommandService wpfCommandService, IDocumentTabService documentTabService, RemoveAssemblyCommand.EditMenuCommand removeCmd, AssemblySettingsCommand.EditMenuCommand settingsCmd) {
wpfCommandService.AddRemoveCommand(removeCmd);
wpfCommandService.AddSettingsCommand(documentTabService, settingsCmd, null);
}
}
[ExportMenuItem(Header = "res:DisableMMapIOCommand", Group = MenuConstants.GROUP_CTX_DOCUMENTS_OTHER, Order = 50)]
sealed class DisableMemoryMappedIOCommand : MenuItemBase {
public override bool IsVisible(IMenuItemContext context) =>
context.CreatorObject.Guid == new Guid(MenuConstants.GUIDOBJ_DOCUMENTS_TREEVIEW_GUID) &&
(context.Find() ?? Array.Empty()).Any(a => GetDocument(a) is not null);
static IDsDocument? GetDocument(TreeNodeData node) {
var fileNode = node as DsDocumentNode;
if (fileNode is null)
return null;
var peImage = fileNode.Document.PEImage;
if (peImage is null)
peImage = (fileNode.Document.ModuleDef as ModuleDefMD)?.Metadata?.PEImage;
return (peImage as IInternalPEImage)?.IsMemoryMappedIO == true ? fileNode.Document : null;
}
public override void Execute(IMenuItemContext context) {
if (context.CreatorObject.Guid != new Guid(MenuConstants.GUIDOBJ_DOCUMENTS_TREEVIEW_GUID))
return;
var asms = new List();
foreach (var node in (context.Find() ?? Array.Empty())) {
var file = GetDocument(node);
if (file is not null)
asms.Add(file);
}
foreach (var asm in asms)
(asm.PEImage as IInternalPEImage)?.UnsafeDisableMemoryMappedIO();
}
}
[DebuggerDisplay("{Description}")]
sealed class RemoveAssemblyCommand : IUndoCommand {
[ExportMenuItem(Header = "res:RemoveAssemblyCommand", Icon = DsImagesAttribute.Cancel, InputGestureText = "res:DeleteCommandKey", Group = MenuConstants.GROUP_CTX_DOCUMENTS_ASMED_DELETE, Order = 0)]
sealed class DocumentsCommand : DocumentsContextMenuHandler {
readonly Lazy undoCommandService;
readonly Lazy documentSaver;
readonly IAppService appService;
[ImportingConstructor]
DocumentsCommand(Lazy undoCommandService, Lazy documentSaver, IAppService appService) {
this.undoCommandService = undoCommandService;
this.documentSaver = documentSaver;
this.appService = appService;
}
public override bool IsVisible(AsmEditorContext context) => RemoveAssemblyCommand.CanExecute(context.Nodes);
public override void Execute(AsmEditorContext context) => RemoveAssemblyCommand.Execute(undoCommandService, documentSaver, appService, context.Nodes);
public override string? GetHeader(AsmEditorContext context) => RemoveAssemblyCommand.GetHeader(context.Nodes);
}
[Export, ExportMenuItem(OwnerGuid = MenuConstants.APP_MENU_EDIT_GUID, Header = "res:RemoveAssemblyCommand", Icon = DsImagesAttribute.Cancel, InputGestureText = "res:DeleteCommandKey", Group = MenuConstants.GROUP_APP_MENU_EDIT_ASMED_DELETE, Order = 0)]
internal sealed class EditMenuCommand : EditMenuHandler {
readonly Lazy undoCommandService;
readonly Lazy documentSaver;
readonly IAppService appService;
[ImportingConstructor]
EditMenuCommand(Lazy undoCommandService, Lazy documentSaver, IAppService appService)
: base(appService.DocumentTreeView) {
this.undoCommandService = undoCommandService;
this.documentSaver = documentSaver;
this.appService = appService;
}
public override bool IsVisible(AsmEditorContext context) => RemoveAssemblyCommand.CanExecute(context.Nodes);
public override void Execute(AsmEditorContext context) => RemoveAssemblyCommand.Execute(undoCommandService, documentSaver, appService, context.Nodes);
public override string? GetHeader(AsmEditorContext context) => RemoveAssemblyCommand.GetHeader(context.Nodes);
}
static string GetHeader(TreeNodeData[] nodes) {
if (nodes.Length == 1)
return string.Format(dnSpy_AsmEditor_Resources.RemoveCommand, UIUtilities.EscapeMenuItemHeader(nodes[0].ToString()!));
return string.Format(dnSpy_AsmEditor_Resources.RemoveAssembliesCommand, nodes.Length);
}
static bool CanExecute(DocumentTreeNodeData[] nodes) =>
nodes.Length > 0 &&
nodes.All(n => n is DsDocumentNode && n.TreeNode.Parent == n.Context.DocumentTreeView.TreeView.Root);
internal static void Execute(Lazy undoCommandService, Lazy documentSaver, IAppService appService, DocumentTreeNodeData[] nodes) {
if (!CanExecute(nodes))
return;
var asmNodes = nodes.Cast().ToArray();
var files = asmNodes.SelectMany(a => a.Document.GetAllChildrenAndSelf());
if (!documentSaver.Value.AskUserToSaveIfModified(files))
return;
var keepNodes = new List();
var freeNodes = new List();
var onlyInRedoHistory = new List();
foreach (var info in GetUndoRedoInfo(undoCommandService.Value, asmNodes)) {
if (!info.IsInUndo && !info.IsInRedo) {
// This asm is safe to remove
freeNodes.Add(info.Node);
}
else if (!info.IsInUndo && info.IsInRedo) {
// If we add a RemoveAssemblyCommand, the redo history will be cleared, so this
// assembly will be cleared from the history and don't need to be kept.
onlyInRedoHistory.Add(info.Node);
}
else {
// The asm is in the undo history, and maybe in the redo history. We must keep it.
keepNodes.Add(info.Node);
}
}
if (keepNodes.Count > 0 || onlyInRedoHistory.Count > 0) {
// We can't free the asm since older commands might reference it so we must record
// it in the history. The user can click Clear History to free everything.
foreach (var node in keepNodes) {
foreach (var f in node.Document.GetAllChildrenAndSelf())
MemoryMappedIOHelper.DisableMemoryMappedIO(f);
}
if (keepNodes.Count != 0)
undoCommandService.Value.Add(new RemoveAssemblyCommand(appService.DocumentTreeView, keepNodes.ToArray()));
else
undoCommandService.Value.ClearRedo();
// Redo history was cleared
FreeAssemblies(onlyInRedoHistory);
}
FreeAssemblies(freeNodes);
}
static void FreeAssemblies(IList nodes) {
if (nodes.Count == 0)
return;
var docTreeView = nodes[0].Context.DocumentTreeView;
if (nodes.Count == docTreeView.TreeView.Root.Children.Count) {
var hash1 = new HashSet(docTreeView.TreeView.Root.Children.Select(a => (DsDocumentNode)a.Data));
var hash2 = new HashSet(nodes);
if (hash1.Count == hash2.Count && hash1.Count == nodes.Count) {
docTreeView.TreeView.SelectItems(Array.Empty());
docTreeView.DocumentService.Clear();
return;
}
}
docTreeView.Remove(nodes);
}
readonly struct UndoRedoInfo {
public readonly bool IsInUndo;
public readonly bool IsInRedo;
public readonly DsDocumentNode Node;
public UndoRedoInfo(DsDocumentNode node, bool isInUndo, bool isInRedo) {
IsInUndo = isInUndo;
IsInRedo = isInRedo;
Node = node;
}
}
static IEnumerable GetUndoRedoInfo(IUndoCommandService undoCommandService, IEnumerable nodes) {
var modifiedUndoAsms = new HashSet(undoCommandService.UndoObjects);
var modifiedRedoAsms = new HashSet(undoCommandService.RedoObjects);
foreach (var node in nodes) {
var uo = undoCommandService.GetUndoObject(node.Document);
bool isInUndo = modifiedUndoAsms.Contains(uo!);
bool isInRedo = modifiedRedoAsms.Contains(uo!);
yield return new UndoRedoInfo(node, isInUndo, isInRedo);
}
}
RootDocumentNodeCreator[] savedStates;
RemoveAssemblyCommand(IDocumentTreeView documentTreeView, DsDocumentNode[] asmNodes) {
savedStates = new RootDocumentNodeCreator[asmNodes.Length];
for (int i = 0; i < savedStates.Length; i++)
savedStates[i] = new RootDocumentNodeCreator(documentTreeView, asmNodes[i]);
}
public string Description => dnSpy_AsmEditor_Resources.RemoveAssemblyCommand;
public void Execute() {
for (int i = 0; i < savedStates.Length; i++)
savedStates[i].Remove();
}
public void Undo() {
for (int i = savedStates.Length - 1; i >= 0; i--)
savedStates[i].Add();
}
public IEnumerable