From 00ef824dbd3861bf3297ff097f503dbe59934d03 Mon Sep 17 00:00:00 2001 From: S-S-P Date: Sun, 30 Jun 2019 20:17:24 -0400 Subject: [PATCH 1/2] Add support for copying bytes formatted as Python byte-string, Python list, and C/Java array --- .../CodeBrowserClipboardProvider.java | 18 +++++ .../main/java/ghidra/app/util/ByteCopier.java | 69 ++++++++++++++++++- .../ByteViewerClipboardProvider.java | 15 ++++ 3 files changed, 101 insertions(+), 1 deletion(-) diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/clipboard/CodeBrowserClipboardProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/clipboard/CodeBrowserClipboardProvider.java index 0d4317b0f5..aef4821816 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/clipboard/CodeBrowserClipboardProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/clipboard/CodeBrowserClipboardProvider.java @@ -77,6 +77,9 @@ public class CodeBrowserClipboardProvider extends ByteCopier list.add(COMMENTS_TYPE); list.add(BYTE_STRING_TYPE); list.add(BYTE_STRING_NO_SPACE_TYPE); + list.add(PYTHON_BYTE_STRING_TYPE); + list.add(PYTHON_LIST_TYPE); + list.add(CPP_BYTE_ARRAY_TYPE); list.add(ADDRESS_TEXT_TYPE); return list; @@ -239,6 +242,21 @@ public class CodeBrowserClipboardProvider extends ByteCopier String byteString = copyBytesAsString(getSelectedAddresses(), false, monitor); return new ByteViewerTransferable(byteString); } + else if (copyType == PYTHON_BYTE_STRING_TYPE) { + String byteString = "b'" + + copyBytesAsString(currentSelection, true, monitor).replaceAll(" ", "\\x") + "'"; + return new ByteViewerTransferable(byteString); + } + else if (copyType == PYTHON_LIST_TYPE) { + String byteString = "[ " + + copyBytesAsString(currentSelection, true, monitor).replaceAll(" ", ", 0x") + " ]"; + return new ByteViewerTransferable(byteString); + } + else if (copyType == CPP_BYTE_ARRAY_TYPE) { + String byteString = "{ " + + copyBytesAsString(currentSelection, true, monitor).replaceAll(" ", ", 0x") + " }"; + return new ByteViewerTransferable(byteString); + } return null; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/ByteCopier.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/ByteCopier.java index 7bec236ec0..9e0d8a4971 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/ByteCopier.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/ByteCopier.java @@ -46,12 +46,23 @@ public abstract class ByteCopier { public static DataFlavor BYTE_STRING_FLAVOR = createByteStringLocalDataTypeFlavor(); public static DataFlavor BYTE_STRING_NO_SPACES_FLAVOR = createByteStringNoSpacesLocalDataTypeFlavor(); + public static DataFlavor PYTHON_BYTE_STRING_FLAVOR = + createPythonByteStringLocalDataTypeFlavor(); + public static DataFlavor PYTHON_LIST_FLAVOR = createPythonListLocalDataTypeFlavor(); + public static DataFlavor CPP_BYTE_ARRAY_FLAVOR = + createCppByteArrayLocalDataTypeFlavor(); protected static final List EMPTY_LIST = Collections.emptyList(); public static final ClipboardType BYTE_STRING_TYPE = new ClipboardType(BYTE_STRING_FLAVOR, "Byte String"); public static final ClipboardType BYTE_STRING_NO_SPACE_TYPE = new ClipboardType(BYTE_STRING_NO_SPACES_FLAVOR, "Byte String (No Spaces)"); + public static final ClipboardType PYTHON_BYTE_STRING_TYPE = + new ClipboardType(PYTHON_BYTE_STRING_FLAVOR, "Python Byte String"); + public static final ClipboardType PYTHON_LIST_TYPE = + new ClipboardType(PYTHON_LIST_FLAVOR, "Python List"); + public static final ClipboardType CPP_BYTE_ARRAY_TYPE = + new ClipboardType(CPP_BYTE_ARRAY_FLAVOR, "C Array"); private static DataFlavor createByteStringLocalDataTypeFlavor() { @@ -83,6 +94,51 @@ public abstract class ByteCopier { return null; } + private static DataFlavor createPythonByteStringLocalDataTypeFlavor() { + + try { + return new GenericDataFlavor( + DataFlavor.javaJVMLocalObjectMimeType + "; class=java.lang.String", + "Local flavor--Python byte string"); + } + catch (Exception e) { + Msg.showError(ByteCopier.class, null, "Could Not Create Data Flavor", + "Unexpected exception creating data flavor for Python byte string", e); + } + + return null; + } + + private static DataFlavor createPythonListLocalDataTypeFlavor() { + + try { + return new GenericDataFlavor( + DataFlavor.javaJVMLocalObjectMimeType + "; class=java.lang.String", + "Local flavor--Python list"); + } + catch (Exception e) { + Msg.showError(ByteCopier.class, null, "Could Not Create Data Flavor", + "Unexpected exception creating data flavor for Python list", e); + } + + return null; + } + + private static DataFlavor createCppByteArrayLocalDataTypeFlavor() { + + try { + return new GenericDataFlavor( + DataFlavor.javaJVMLocalObjectMimeType + "; class=java.lang.String", + "Local flavor--C++ array"); + } + catch (Exception e) { + Msg.showError(ByteCopier.class, null, "Could Not Create Data Flavor", + "Unexpected exception creating data flavor for C array", e); + } + + return null; + } + protected PluginTool tool; protected Program currentProgram; protected ProgramSelection currentSelection; @@ -392,7 +448,9 @@ public abstract class ByteCopier { public static class ByteViewerTransferable implements Transferable { private final DataFlavor[] flavors = { BYTE_STRING_NO_SPACE_TYPE.getFlavor(), - BYTE_STRING_TYPE.getFlavor(), DataFlavor.stringFlavor }; + BYTE_STRING_TYPE.getFlavor(), PYTHON_BYTE_STRING_TYPE.getFlavor(), + PYTHON_LIST_TYPE.getFlavor(), CPP_BYTE_ARRAY_TYPE.getFlavor(), + DataFlavor.stringFlavor }; private final List flavorList = Arrays.asList(flavors); private final String byteString; @@ -423,6 +481,15 @@ public abstract class ByteCopier { if (flavor.equals(BYTE_STRING_NO_SPACE_TYPE.getFlavor())) { return byteString; } + if (flavor.equals(PYTHON_BYTE_STRING_TYPE.getFlavor())) { + return byteString; + } + if (flavor.equals(PYTHON_LIST_TYPE.getFlavor())) { + return byteString; + } + if (flavor.equals(CPP_BYTE_ARRAY_TYPE.getFlavor())) { + return byteString; + } throw new UnsupportedFlavorException(flavor); } diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerClipboardProvider.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerClipboardProvider.java index d14d14e97e..6b778b4974 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerClipboardProvider.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerClipboardProvider.java @@ -43,6 +43,9 @@ public class ByteViewerClipboardProvider extends ByteCopier List copyTypesList = new LinkedList<>(); copyTypesList.add(BYTE_STRING_TYPE); copyTypesList.add(BYTE_STRING_NO_SPACE_TYPE); + copyTypesList.add(PYTHON_BYTE_STRING_TYPE); + copyTypesList.add(PYTHON_LIST_TYPE); + copyTypesList.add(CPP_BYTE_ARRAY_TYPE); return copyTypesList; } @@ -117,6 +120,18 @@ public class ByteViewerClipboardProvider extends ByteCopier else if (copyType == BYTE_STRING_NO_SPACE_TYPE) { byteString = copyBytesAsString(currentSelection, false, monitor); } + else if (copyType == PYTHON_BYTE_STRING_TYPE) { + byteString = "b'" + + copyBytesAsString(currentSelection, true, monitor).replaceAll(" ", "\\x") + "'"; + } + else if (copyType == PYTHON_LIST_TYPE) { + byteString = "[ " + + copyBytesAsString(currentSelection, true, monitor).replaceAll(" ", ", 0x") + " ]"; + } + else if (copyType == CPP_BYTE_ARRAY_TYPE) { + byteString = "{ " + + copyBytesAsString(currentSelection, true, monitor).replaceAll(" ", ", 0x") + " }"; + } else { return null; } From ccedc6627a7b1a215a4695965e1b1815ce0f958c Mon Sep 17 00:00:00 2001 From: dragonmacher <48328597+dragonmacher@users.noreply.github.com> Date: Wed, 18 Nov 2020 17:21:00 -0500 Subject: [PATCH 2/2] GP-210 - Added new 'Copy Special' items to make copying from Ghidra to Python and C++ easier --- .../help/topics/ClipboardPlugin/Clipboard.htm | 20 + .../ClipboardPlugin/images/CopySpecial.png | Bin 9522 -> 11975 bytes .../images/CopySpecialAgain.png | Bin 7353 -> 7996 bytes .../core/clipboard/ClipboardPlugin.java | 5 +- .../CodeBrowserClipboardProvider.java | 173 +++----- .../ClipboardContentProviderService.java | 28 +- .../main/java/ghidra/app/util/ByteCopier.java | 420 ++++++++++++------ .../java/ghidra/app/util/ClipboardType.java | 16 +- .../CodeBrowserClipboardProviderTest.java | 224 ++++++++++ .../core/clipboard/CopyPasteCommentsTest.java | 80 ++-- .../clipboard/CopyPasteFunctionInfoTest.java | 94 ++-- .../ByteViewerClipboardProvider.java | 50 +-- .../ByteViewerClipboardProviderTest.java | 239 ++++++++++ .../ClipboardPluginScreenShots.java | 55 ++- .../core/clipboard/CopyPasteTestSuite.java | 36 ++ 15 files changed, 1024 insertions(+), 416 deletions(-) create mode 100644 Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/clipboard/CodeBrowserClipboardProviderTest.java create mode 100644 Ghidra/Features/ByteViewer/src/test/java/ghidra/app/plugin/core/byteviewer/ByteViewerClipboardProviderTest.java create mode 100644 Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/app/plugin/core/clipboard/CopyPasteTestSuite.java diff --git a/Ghidra/Features/Base/src/main/help/help/topics/ClipboardPlugin/Clipboard.htm b/Ghidra/Features/Base/src/main/help/help/topics/ClipboardPlugin/Clipboard.htm index 5594229414..ee5ba50b20 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/ClipboardPlugin/Clipboard.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/ClipboardPlugin/Clipboard.htm @@ -165,6 +165,15 @@
  • Byte String (No Spaces) - This is the same as the Byte String format, except there are no delimiting spaces between bytes.
  • +
  • Python Byte String - Copies the bytes into the Python byte string format, for + example: b'\x41\xec\x49\x89'.
  • + +
  • Python List String - Copies the bytes into the Python list format, for + example: [ 0x66, 0x2e, 0x0f, 0x1f, 0x84 ].
  • + +
  • C Array String - Copies the bytes into the C array format, for + example: { 0x66, 0x2e, 0x0f, 0x1f, 0x84 }.
  • +
  • Address - Copies the address at the top of the selection.
  • @@ -208,6 +217,17 @@
  • Byte String (No Spaces) - This is the same as the Byte String format, except there are no delimiting spaces between bytes.
  • + + +
  • Python Byte String - Copies the bytes into the Python byte string format, for + example: b'\x41\xec\x49\x89'.
  • + +
  • Python List String - Copies the bytes into the Python list format, for + example: [ 0x66, 0x2e, 0x0f, 0x1f, 0x84 ].
  • + +
  • C Array String - Copies the bytes into the C array format, for + example: { 0x66, 0x2e, 0x0f, 0x1f, 0x84 }.
  • +

    The Byte Viewer Bytes In Memory window can paste the following formats:

    diff --git a/Ghidra/Features/Base/src/main/help/help/topics/ClipboardPlugin/images/CopySpecial.png b/Ghidra/Features/Base/src/main/help/help/topics/ClipboardPlugin/images/CopySpecial.png index 9e6ccc72b5df7c5834c7bbf08245db13599e4183..8cd9f185703bcd711041ce8b0bb0149d05626880 100644 GIT binary patch literal 11975 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 BR~G;P diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/clipboard/ClipboardPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/clipboard/ClipboardPlugin.java index 805a18aada..0734f7e6ca 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/clipboard/ClipboardPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/clipboard/ClipboardPlugin.java @@ -377,7 +377,6 @@ public class ClipboardPlugin extends ProgramPlugin implements ClipboardOwner, Cl if (!clipboardService.isValidContext(context)) { return false; } - return clipboardService.canCopy(); } @@ -413,7 +412,8 @@ public class ClipboardPlugin extends ProgramPlugin implements ClipboardOwner, Cl } Clipboard systemClipboard = getSystemClipboard(); - return clipboardService.canPaste(getAvailableDataFlavors(systemClipboard)); + DataFlavor[] flavors = getAvailableDataFlavors(systemClipboard); + return clipboardService.canPaste(flavors); } @Override @@ -444,7 +444,6 @@ public class ClipboardPlugin extends ProgramPlugin implements ClipboardOwner, Cl if (!clipboardService.isValidContext(context)) { return false; } - return clipboardService.canCopySpecial(); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/clipboard/CodeBrowserClipboardProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/clipboard/CodeBrowserClipboardProvider.java index aef4821816..eb3e5a4c09 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/clipboard/CodeBrowserClipboardProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/clipboard/CodeBrowserClipboardProvider.java @@ -48,6 +48,7 @@ import ghidra.program.model.symbol.*; import ghidra.program.util.*; import ghidra.util.Msg; import ghidra.util.task.TaskMonitor; +import util.CollectionUtils; public class CodeBrowserClipboardProvider extends ByteCopier implements ClipboardContentProviderService { @@ -85,20 +86,6 @@ public class CodeBrowserClipboardProvider extends ByteCopier return list; } - private static final List PASTE_TYPES = createPasteTypesList(); - - private static List createPasteTypesList() { - List list = new LinkedList<>(); - - list.add(LABELS_COMMENTS_TYPE); - list.add(LABELS_TYPE); - list.add(COMMENTS_TYPE); - list.add(BYTE_STRING_TYPE); - list.add(BYTE_STRING_NO_SPACE_TYPE); - - return list; - } - protected boolean copyFromSelectionEnabled; protected ComponentProvider componentProvider; private ListingModel model; @@ -157,10 +144,6 @@ public class CodeBrowserClipboardProvider extends ByteCopier else if (element.equals(COMMENTS_TYPE.getFlavor())) { return pasteLabelsComments(pasteData, false, true); } - else if (element.equals(BYTE_STRING_FLAVOR)) { - String data = (String) pasteData.getTransferData(BYTE_STRING_FLAVOR); - return pasteByteString(data); - } else if (element.equals(LabelStringTransferable.labelStringFlavor)) { return pasteLabelString(pasteData); } @@ -169,10 +152,8 @@ public class CodeBrowserClipboardProvider extends ByteCopier } } - // last ditch effort, try to paste as a byte string - String string = (String) pasteData.getTransferData(DataFlavor.stringFlavor); - if (string != null) { - return pasteByteString(string); + if (super.pasteBytes(pasteData)) { + return true; } tool.setStatusInfo("Paste failed: unsupported data type", true); @@ -181,36 +162,14 @@ public class CodeBrowserClipboardProvider extends ByteCopier String msg = e.getMessage(); if (msg == null) { msg = e.toString(); - Msg.error(this, "Unexpected Exception: " + e.getMessage(), e); } + + Msg.error(this, "Unexpected Exception: " + msg, e); tool.setStatusInfo("Paste failed: " + msg, true); } return false; } - @Override - protected boolean supportsPasteTransferable(Transferable transferable) { - DataFlavor[] flavors = transferable.getTransferDataFlavors(); - for (DataFlavor element : flavors) { - if (isPasteFlavorMatch(element, transferable)) { - return true; - } - } - return false; - } - - private boolean isPasteFlavorMatch(DataFlavor flavor, Transferable transferable) { - for (ClipboardType type : PASTE_TYPES) { - if (flavor.equals(type.getFlavor())) { - if (type == BYTE_STRING_TYPE) { // our parent handles validating bytes - return isValidBytesTransferable(transferable); - } - return true; - } - } - return false; - } - @Override public List getCurrentCopyTypes() { return COPY_TYPES; @@ -234,45 +193,8 @@ public class CodeBrowserClipboardProvider extends ByteCopier else if (copyType == COMMENTS_TYPE) { return copyLabelsComments(false, true); } - else if (copyType == BYTE_STRING_TYPE) { - String byteString = copyBytesAsString(getSelectedAddresses(), true, monitor); - return new ByteViewerTransferable(byteString); - } - else if (copyType == BYTE_STRING_NO_SPACE_TYPE) { - String byteString = copyBytesAsString(getSelectedAddresses(), false, monitor); - return new ByteViewerTransferable(byteString); - } - else if (copyType == PYTHON_BYTE_STRING_TYPE) { - String byteString = "b'" - + copyBytesAsString(currentSelection, true, monitor).replaceAll(" ", "\\x") + "'"; - return new ByteViewerTransferable(byteString); - } - else if (copyType == PYTHON_LIST_TYPE) { - String byteString = "[ " - + copyBytesAsString(currentSelection, true, monitor).replaceAll(" ", ", 0x") + " ]"; - return new ByteViewerTransferable(byteString); - } - else if (copyType == CPP_BYTE_ARRAY_TYPE) { - String byteString = "{ " - + copyBytesAsString(currentSelection, true, monitor).replaceAll(" ", ", 0x") + " }"; - return new ByteViewerTransferable(byteString); - } - return null; - } - - private AddressSetView getSelectedAddresses() { - AddressSetView addressSet = currentSelection; - if (addressSet == null || addressSet.isEmpty()) { - return new AddressSet(currentLocation.getAddress()); - } - return currentSelection; - } - - public void setSelection(ProgramSelection selection) { - currentSelection = selection; - copyFromSelectionEnabled = selection != null && !selection.isEmpty(); - notifyStateChanged(); + return copyBytes(copyType, monitor); } public void setStringContent(String text) { @@ -287,6 +209,12 @@ public class CodeBrowserClipboardProvider extends ByteCopier currentLocation = location; } + public void setSelection(ProgramSelection selection) { + currentSelection = selection; + copyFromSelectionEnabled = selection != null && !selection.isEmpty(); + notifyStateChanged(); + } + public void setProgram(Program p) { currentProgram = p; currentLocation = null; @@ -323,11 +251,6 @@ public class CodeBrowserClipboardProvider extends ByteCopier } else if (currentLocation instanceof BytesFieldLocation) { // bytes are special--let them get copied and pasted as normal -// AddressSet addressSet = new AddressSet(currentProgram.getAddressFactory(), address); -// String byteString = -// copyBytesAsString(addressSet.getAddressRanges(), false, -// TaskMonitorAdapter.DUMMY_MONITOR); -// return new ByteViewerTransferable(byteString); return copyByteString(address); } else if (currentLocation instanceof OperandFieldLocation) { @@ -439,7 +362,7 @@ public class CodeBrowserClipboardProvider extends ByteCopier private Transferable copyByteString(Address address) { AddressSet set = new AddressSet(address); - return copyBytes(set, false, TaskMonitor.DUMMY); + return createStringTransferable(copyBytesAsString(set, false, TaskMonitor.DUMMY)); } private CodeUnitInfoTransferable copyLabelsComments(boolean copyLabels, boolean copyComments) { @@ -451,13 +374,13 @@ public class CodeBrowserClipboardProvider extends ByteCopier return new CodeUnitInfoTransferable(list); } - @SuppressWarnings("unchecked") // assumed correct data; handled in exception case private boolean pasteLabelsComments(Transferable pasteData, boolean pasteLabels, boolean pasteComments) { try { - List list = (List) pasteData.getTransferData( + List list = (List) pasteData.getTransferData( CodeUnitInfoTransferable.localDataTypeFlavor); - Command cmd = new CodeUnitInfoPasteCmd(currentLocation.getAddress(), list, pasteLabels, + List infos = CollectionUtils.asList(list, CodeUnitInfo.class); + Command cmd = new CodeUnitInfoPasteCmd(currentLocation.getAddress(), infos, pasteLabels, pasteComments); return tool.execute(cmd, currentProgram); } @@ -494,36 +417,43 @@ public class CodeBrowserClipboardProvider extends ByteCopier return tool.execute(cmd, currentProgram); } else if (currentLocation instanceof OperandFieldLocation) { - OperandFieldLocation operandLocation = (OperandFieldLocation) currentLocation; - int opIndex = operandLocation.getOperandIndex(); - Listing listing = currentProgram.getListing(); - Instruction instruction = listing.getInstructionAt(operandLocation.getAddress()); - if (instruction == null) { - return false; - } + return pasteOperandField((OperandFieldLocation) currentLocation, labelName); + } - Reference reference = instruction.getPrimaryReference(opIndex); - if (reference == null) { - return false; - } + // try pasting onto something that is not a label + return maybePasteNonLabelString(labelName); + } - Variable var = currentProgram.getReferenceManager().getReferencedVariable(reference); - if (var != null) { - SetVariableNameCmd cmd = - new SetVariableNameCmd(var, labelName, SourceType.USER_DEFINED); - return tool.execute(cmd, currentProgram); - } + private boolean pasteOperandField(OperandFieldLocation operandLocation, String labelName) { - SymbolTable symbolTable = currentProgram.getSymbolTable(); - Symbol symbol = symbolTable.getSymbol(reference); - if ((symbol instanceof CodeSymbol) || (symbol instanceof FunctionSymbol)) { - String oldName = symbol.getName(); - Namespace namespace = symbol.getParentNamespace(); - Address symbolAddress = symbol.getAddress(); - RenameLabelCmd cmd = new RenameLabelCmd(symbolAddress, oldName, labelName, - namespace, SourceType.USER_DEFINED); - return tool.execute(cmd, currentProgram); - } + int opIndex = operandLocation.getOperandIndex(); + Listing listing = currentProgram.getListing(); + Instruction instruction = listing.getInstructionAt(operandLocation.getAddress()); + if (instruction == null) { + return false; + } + + Reference reference = instruction.getPrimaryReference(opIndex); + if (reference == null) { + return false; + } + + Variable var = currentProgram.getReferenceManager().getReferencedVariable(reference); + if (var != null) { + SetVariableNameCmd cmd = + new SetVariableNameCmd(var, labelName, SourceType.USER_DEFINED); + return tool.execute(cmd, currentProgram); + } + + SymbolTable symbolTable = currentProgram.getSymbolTable(); + Symbol symbol = symbolTable.getSymbol(reference); + if ((symbol instanceof CodeSymbol) || (symbol instanceof FunctionSymbol)) { + String oldName = symbol.getName(); + Namespace namespace = symbol.getParentNamespace(); + Address symbolAddress = symbol.getAddress(); + RenameLabelCmd cmd = new RenameLabelCmd(symbolAddress, oldName, labelName, + namespace, SourceType.USER_DEFINED); + return tool.execute(cmd, currentProgram); } // try pasting onto something that is not a label @@ -707,8 +637,7 @@ public class CodeBrowserClipboardProvider extends ByteCopier return true; } if (flavor.equals(DataFlavor.stringFlavor)) { - // TODO: check if it is a valid hex string... - return true; + return true; // check if it is a valid hex string? } } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/services/ClipboardContentProviderService.java b/Ghidra/Features/Base/src/main/java/ghidra/app/services/ClipboardContentProviderService.java index 1d407a6775..5c55a2c314 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/services/ClipboardContentProviderService.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/services/ClipboardContentProviderService.java @@ -15,9 +15,6 @@ */ package ghidra.app.services; -import ghidra.app.util.ClipboardType; -import ghidra.util.task.TaskMonitor; - import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; import java.util.List; @@ -26,21 +23,26 @@ import javax.swing.event.ChangeListener; import docking.ActionContext; import docking.ComponentProvider; +import ghidra.app.util.ClipboardType; +import ghidra.util.task.TaskMonitor; /** - * ClipboardContentProvider determines what types of - * transfer data can be placed on the clipboard, and cut, copy, and paste. + * Determines what types of transfer data can be placed on the clipboard, as well as if + * cut, copy, and paste operations are supported */ public interface ClipboardContentProviderService { /** - * Returns the component provider associated with this - * ClipboardContentProviderService. + * Returns the component provider associated with this service + * @return the provider */ public ComponentProvider getComponentProvider(); /** * Triggers the default copy operation + * @param monitor monitor that shows progress of the copy to clipboard, and + * may be canceled + * @return the created transferable; null if the copy was unsuccessful */ public Transferable copy(TaskMonitor monitor); @@ -49,44 +51,49 @@ public interface ClipboardContentProviderService { * @param copyType contains the data flavor of the clipboard contents * @param monitor monitor that shows progress of the copy to clipboard, and * may be canceled + * @return the created transferable; null if the copy was unsuccessful */ public Transferable copySpecial(ClipboardType copyType, TaskMonitor monitor); /** * Triggers the default paste operation for the given transferable + * @param pasteData the paste transferable + * @return true of the paste was successful */ public boolean paste(Transferable pasteData); /** * Gets the currently active ClipboardTypes for copying with the current context + * @return the types */ public List getCurrentCopyTypes(); /** * Return whether the given context is valid for actions on popup menus. * @param context the context of where the popup menu will be positioned. + * @return true if valid */ public boolean isValidContext(ActionContext context); - // TODO needs updating. Assumed copyAddToPopup became copy(TaskMonitor monitor) /** * Returns true if copy should be enabled; false if it should be disabled. This method can * be used in conjunction with {@link #copy(TaskMonitor)} in order to add menu items to * popup menus but to have them enabled when appropriate. + * @return true if copy should be enabled */ public boolean enableCopy(); /** * Returns true if copySpecial actions should be enabled; - * @return + * @return true if copySpecial actions should be enabled; */ public boolean enableCopySpecial(); - // TODO needs updating. Assumed pasteAddToPopup became paste(Transferable pasteData) /** * Returns true if paste should be enabled; false if it should be disabled. This method can * be used in conjunction with {@link #paste(Transferable)} in order to add menu items to * popup menus but to have them enabled when appropriate. + * @return true if paste should be enabled */ public boolean enablePaste(); @@ -130,6 +137,7 @@ public interface ClipboardContentProviderService { /** * Returns true if the given service provider can currently perform a 'copy special' * operation. + * @return true if copy special is enabled */ public boolean canCopySpecial(); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/ByteCopier.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/ByteCopier.java index 9e0d8a4971..691249e6e2 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/ByteCopier.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/ByteCopier.java @@ -18,6 +18,8 @@ package ghidra.app.util; import java.awt.datatransfer.*; import java.io.IOException; import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import docking.dnd.GenericDataFlavor; import docking.dnd.StringTransferable; @@ -25,8 +27,7 @@ import docking.widgets.OptionDialog; import ghidra.framework.cmd.Command; import ghidra.framework.model.DomainObject; import ghidra.framework.plugintool.PluginTool; -import ghidra.program.model.address.Address; -import ghidra.program.model.address.AddressSetView; +import ghidra.program.model.address.*; import ghidra.program.model.listing.*; import ghidra.program.model.mem.Memory; import ghidra.program.model.mem.MemoryAccessException; @@ -64,6 +65,19 @@ public abstract class ByteCopier { public static final ClipboardType CPP_BYTE_ARRAY_TYPE = new ClipboardType(CPP_BYTE_ARRAY_FLAVOR, "C Array"); + private static final Map PROGRAMMING_PATTERNS_BY_FLAVOR = + Map.of( + PYTHON_BYTE_STRING_FLAVOR, Pattern.compile("b'(.*)'"), + PYTHON_LIST_FLAVOR, Pattern.compile("\\[(.*)\\]"), + CPP_BYTE_ARRAY_FLAVOR, Pattern.compile("\\{(.*)\\}")); + + /** + * Pattern to recognize bytes that have been encoded during a copy operation using one of this + * class's programming copy types + */ + private static final Pattern PROGRAMMING_BYTES_PATTERN = + Pattern.compile("(?:\\\\x|0x)([a-fA-F0-9]{2})"); + private static DataFlavor createByteStringLocalDataTypeFlavor() { try { @@ -72,7 +86,7 @@ public abstract class ByteCopier { "Local flavor--byte string with spaces"); } catch (Exception e) { - Msg.showError(ByteCopier.class, null, "Could Not Create Data Flavor", + Msg.error(ByteCopier.class, "Unexpected exception creating data flavor for byte string", e); } @@ -87,7 +101,7 @@ public abstract class ByteCopier { "Local flavor--byte string with NO spaces"); } catch (Exception e) { - Msg.showError(ByteCopier.class, null, "Could Not Create Data Flavor", + Msg.error(ByteCopier.class, "Unexpected exception creating data flavor for byte string with no spaces", e); } @@ -102,7 +116,7 @@ public abstract class ByteCopier { "Local flavor--Python byte string"); } catch (Exception e) { - Msg.showError(ByteCopier.class, null, "Could Not Create Data Flavor", + Msg.error(ByteCopier.class, "Unexpected exception creating data flavor for Python byte string", e); } @@ -117,7 +131,7 @@ public abstract class ByteCopier { "Local flavor--Python list"); } catch (Exception e) { - Msg.showError(ByteCopier.class, null, "Could Not Create Data Flavor", + Msg.error(ByteCopier.class, "Unexpected exception creating data flavor for Python list", e); } @@ -132,7 +146,7 @@ public abstract class ByteCopier { "Local flavor--C++ array"); } catch (Exception e) { - Msg.showError(ByteCopier.class, null, "Could Not Create Data Flavor", + Msg.error(ByteCopier.class, "Unexpected exception creating data flavor for C array", e); } @@ -148,8 +162,12 @@ public abstract class ByteCopier { // limit construction } - protected Transferable copyBytes(boolean includeSpaces, TaskMonitor monitor) { - return copyBytes(currentSelection, includeSpaces, monitor); + protected AddressSetView getSelectedAddresses() { + AddressSetView addressSet = currentSelection; + if (addressSet == null || addressSet.isEmpty()) { + return new AddressSet(currentLocation.getAddress()); + } + return currentSelection; } protected Transferable copyBytes(AddressSetView addresses, boolean includeSpaces, @@ -160,42 +178,18 @@ public abstract class ByteCopier { protected String copyBytesAsString(AddressSetView addresses, boolean includeSpaces, TaskMonitor monitor) { - Memory memory = currentProgram.getMemory(); String delimiter = includeSpaces ? " " : ""; + return copyBytesAsString(addresses, delimiter, monitor); + } + + protected String copyBytesAsString(AddressSetView addresses, String delimiter, + TaskMonitor monitor) { + + Memory memory = currentProgram.getMemory(); ByteIterator bytes = new ByteIterator(addresses, memory); return NumericUtilities.convertBytesToString(bytes, delimiter); } - protected boolean supportsPasteTransferable(Transferable transferable) { - return isValidBytesTransferable(transferable); - } - - protected boolean isValidBytesTransferable(Transferable transferable) { - - DataFlavor[] flavors = transferable.getTransferDataFlavors(); - for (DataFlavor element : flavors) { - - try { - Object object = transferable.getTransferData(element); - if (object instanceof String) { - String string = (String) object; - if (!isOnlyAsciiBytes(string)) { - tool.setStatusInfo("Paste string contains non-text ascii bytes. " + - "Only the ascii text will be pasted.", true); - - string = keepOnlyAsciiBytes(string); - } - return (getBytes(string) != null); - } - } - catch (Exception e) { - // don't care; try the next one - } - } - - return false; - } - private byte[] getBytes(String transferString) { byte[] bytes = getHexBytes(transferString); @@ -271,27 +265,121 @@ public abstract class ByteCopier { return null; } + protected Transferable copyBytes(ClipboardType copyType, TaskMonitor monitor) { + + if (copyType == BYTE_STRING_TYPE) { + String byteString = copyBytesAsString(getSelectedAddresses(), true, monitor); + return new ByteStringTransferable(byteString); + } + else if (copyType == BYTE_STRING_NO_SPACE_TYPE) { + String byteString = copyBytesAsString(getSelectedAddresses(), false, monitor); + return new ByteStringTransferable(byteString); + } + else if (copyType == PYTHON_BYTE_STRING_TYPE) { + String prefix = "\\x"; + String bs = copyBytesAsString(getSelectedAddresses(), prefix, monitor); + String fixed = "b'" + prefix + bs + "'"; + return new ProgrammingByteStringTransferable(fixed, copyType.getFlavor()); + } + else if (copyType == PYTHON_LIST_TYPE) { + String prefix = "0x"; + String bs = copyBytesAsString(getSelectedAddresses(), ", " + prefix, monitor); + String fixed = "[ " + prefix + bs + " ]"; + return new ProgrammingByteStringTransferable(fixed, copyType.getFlavor()); + } + else if (copyType == CPP_BYTE_ARRAY_TYPE) { + String prefix = "0x"; + String bs = copyBytesAsString(getSelectedAddresses(), ", " + prefix, monitor); + String byteString = "{ " + prefix + bs + " }"; + return new ProgrammingByteStringTransferable(byteString, copyType.getFlavor()); + } + + return null; + } + protected boolean pasteBytes(Transferable pasteData) throws UnsupportedFlavorException, IOException { - if (!supportsPasteTransferable(pasteData)) { - tool.setStatusInfo("Paste failed: No valid data on clipboard", true); + + DataFlavor[] flavors = pasteData.getTransferDataFlavors(); + DataFlavor byteStringFlavor = getByteStringFlavor(flavors); + if (byteStringFlavor != null) { + String data = (String) pasteData.getTransferData(byteStringFlavor); + return pasteByteString(data); + } + + DataFlavor programmingFlavor = getProgrammingFlavor(flavors); + if (programmingFlavor != null) { + String data = (String) pasteData.getTransferData(programmingFlavor); + String byteString = extractProgrammingBytes(programmingFlavor, data); + if (byteString != null) { + return pasteByteString(byteString); + } + } + + if (!pasteData.isDataFlavorSupported(DataFlavor.stringFlavor)) { + tool.setStatusInfo("Paste failed: unsupported data type", true); return false; } - if (pasteData.isDataFlavorSupported(BYTE_STRING_FLAVOR)) { - String data = (String) pasteData.getTransferData(BYTE_STRING_FLAVOR); - return pasteByteString(data); - } - - if (pasteData.isDataFlavorSupported(BYTE_STRING_NO_SPACES_FLAVOR)) { - String data = (String) pasteData.getTransferData(BYTE_STRING_NO_SPACES_FLAVOR); - return pasteByteString(data); - } - + // see if the pasted data is similar to other known programming formats String string = (String) pasteData.getTransferData(DataFlavor.stringFlavor); + if (string == null) { + tool.setStatusInfo("Paste failed: no string data", true); + return false; + } + return pasteByteString(string); } + private DataFlavor getProgrammingFlavor(DataFlavor[] flavors) { + for (DataFlavor flavor : flavors) { + if (flavor.equals(PYTHON_BYTE_STRING_FLAVOR) || + flavor.equals(PYTHON_LIST_FLAVOR) || + flavor.equals(CPP_BYTE_ARRAY_FLAVOR)) { + return flavor; + } + } + return null; + } + + private DataFlavor getByteStringFlavor(DataFlavor[] flavors) { + + for (DataFlavor flavor : flavors) { + if (flavor.equals(BYTE_STRING_FLAVOR) || + flavor.equals(BYTE_STRING_NO_SPACES_FLAVOR)) { + return flavor; + } + } + + return null; + } + + private String extractProgrammingBytes(DataFlavor flavor, String data) { + + Pattern pattern = PROGRAMMING_PATTERNS_BY_FLAVOR.get(flavor); + Matcher matcher = pattern.matcher(data); + if (!matcher.matches()) { + return null; + } + + String bytes = matcher.group(1); + if (bytes == null) { + return null; + } + + Matcher bytesMatcher = PROGRAMMING_BYTES_PATTERN.matcher(bytes); + if (!bytesMatcher.find()) { + return null; + } + + StringBuilder buffy = new StringBuilder(); + buffy.append(bytesMatcher.group(1)); + while (bytesMatcher.find()) { + buffy.append(bytesMatcher.group(1)); + } + return buffy.toString(); + } + protected boolean pasteByteString(final String string) { Command cmd = new Command() { @@ -299,75 +387,100 @@ public abstract class ByteCopier { @Override public boolean applyTo(DomainObject domainObject) { - if (domainObject instanceof Program) { - String validString = string; - if (!isOnlyAsciiBytes(string)) { - tool.setStatusInfo("Pasted string contained non-text ascii bytes. " + - "Only the ascii text was pasted.", true); + if (!(domainObject instanceof Program)) { + return false; + } - validString = keepOnlyAsciiBytes(string); + String validString = string; + if (!isOnlyAsciiBytes(string)) { + tool.setStatusInfo("Pasted string contained non-text ascii bytes. " + + "Only the ascii will be used.", true); + validString = keepOnlyAsciiBytes(string); + } + + byte[] bytes = getBytes(validString); + if (bytes == null) { + status = "Improper data format. Expected sequence of hex bytes"; + tool.beep(); + return false; + } + + // Ensure that we are not writing over instructions + Program program = (Program) domainObject; + Address address = currentLocation.getAddress(); + if (!hasEnoughSpace(program, address, bytes.length)) { + status = + "Not enough space to paste all bytes. Encountered data or instructions."; + tool.beep(); + return false; + } + + // Ask the user before pasting a string into the program. Since having a string in + // the clipboard is so common, this is to prevent an accidental paste. + if (!confirmPaste(validString)) { + return true; // the user cancelled; the command is successful + } + + boolean pastedAllBytes = pasteBytes(program, bytes); + if (!pastedAllBytes) { + tool.setStatusInfo("Not all bytes were pasted due to memory access issues", + true); + } + + return true; + } + + private boolean pasteBytes(Program program, byte[] bytes) { + + // note: loop one byte at a time here, since Memory will validate all addresses + // before pasting any bytes + boolean foundError = false; + Address address = currentLocation.getAddress(); + Memory memory = program.getMemory(); + for (byte element : bytes) { + try { + memory.setByte(address, element); } + catch (MemoryAccessException e) { + // Keep trying the remaining bytes. Should we just stop in this case? + foundError = true; + } + address = address.next(); + } + return foundError; + } - byte[] bytes = getBytes(validString); - if (bytes == null) { - status = "Improper data format (expected sequence of hex bytes)"; + private boolean confirmPaste(String validString) { + + // create a truncated version of the string to show in the dialog + String partialText = validString.length() < 40 ? validString + : validString.substring(0, 40) + " ..."; + int result = OptionDialog.showYesNoDialog(null, "Paste String Into Program?", + "Are you sure you want to paste the string \"" + partialText + + "\"\n into the program's memory?"); + + return result != OptionDialog.NO_OPTION; + } + + private boolean hasEnoughSpace(Program program, Address address, int byteCount) { + Listing listing = program.getListing(); + for (int i = 0; i < byteCount;) { + if (address == null) { + status = "Not enough addresses to paste bytes"; tool.beep(); return false; } - - // Ensure that we are not writing over instructions - Program curProgram = (Program) domainObject; - Listing listing = curProgram.getListing(); - Address curAddr = currentLocation.getAddress(); - int byteCount = bytes.length; - for (int i = 0; i < byteCount;) { - if (curAddr == null) { - status = "Not enough addresses to paste bytes"; - tool.beep(); - return false; - } - CodeUnit curCodeUnit = listing.getCodeUnitContaining(curAddr); - if (!(curCodeUnit instanceof Data) || ((Data) curCodeUnit).isDefined()) { - status = "Cannot paste on top of defined instructions/data"; - tool.beep(); - return false; - } - int length = curCodeUnit.getLength(); - i += length; - curAddr = curCodeUnit.getMaxAddress().next(); + CodeUnit codeUnit = listing.getCodeUnitContaining(address); + if (!(codeUnit instanceof Data) || ((Data) codeUnit).isDefined()) { + status = "Cannot paste on top of defined instructions/data"; + tool.beep(); + return false; } - - // Per SCR 11212, ask the user before pasting a string into the program. - // Since having a string in the clipboard is so common, this is to prevent - // an accidental paste. - - // create a truncated version of the string to show in the dialog - String partialText = validString.length() < 40 ? validString - : validString.substring(0, 40) + " ..."; - - int result = OptionDialog.showYesNoDialog(null, "Paste String Into Program?", - "Are you sure you want to paste the string \"" + partialText + - "\"\n into the program's memory?"); - - if (result == OptionDialog.NO_OPTION) { - return true; - } - - // Write data - curAddr = currentLocation.getAddress(); - for (byte element : bytes) { - try { - curProgram.getMemory().setByte(curAddr, element); - } - catch (MemoryAccessException e1) { - // handle below - } - curAddr = curAddr.next(); - } - - return true; + int length = codeUnit.getLength(); + i += length; + address = codeUnit.getMaxAddress().next(); } - return false; + return true; } @Override @@ -445,33 +558,69 @@ public abstract class ByteCopier { } } - public static class ByteViewerTransferable implements Transferable { + public static class ProgrammingByteStringTransferable implements Transferable { - private final DataFlavor[] flavors = { BYTE_STRING_NO_SPACE_TYPE.getFlavor(), - BYTE_STRING_TYPE.getFlavor(), PYTHON_BYTE_STRING_TYPE.getFlavor(), - PYTHON_LIST_TYPE.getFlavor(), CPP_BYTE_ARRAY_TYPE.getFlavor(), - DataFlavor.stringFlavor }; - private final List flavorList = Arrays.asList(flavors); + private List flavorList; + private DataFlavor[] flavors; + private DataFlavor programmingFlavor; + private String byteString; - private final String byteString; - - private final String byteViewerRepresentation; - - public ByteViewerTransferable(String byteString) { - this(byteString, null); - } - - public ByteViewerTransferable(String byteString, String byteViewerRepresentation) { + public ProgrammingByteStringTransferable(String byteString, DataFlavor flavor) { this.byteString = byteString; - this.byteViewerRepresentation = byteViewerRepresentation; + this.programmingFlavor = flavor; + this.flavors = new DataFlavor[] { flavor, DataFlavor.stringFlavor }; + this.flavorList = Arrays.asList(flavors); } @Override public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException { if (flavor.equals(DataFlavor.stringFlavor)) { - if (byteViewerRepresentation != null) { - return byteViewerRepresentation; + return byteString; // just default to the byte string when no 'special' string data + } + if (flavor.equals(programmingFlavor)) { + return byteString; + } + throw new UnsupportedFlavorException(flavor); + } + + @Override + public DataFlavor[] getTransferDataFlavors() { + return flavors; + } + + @Override + public boolean isDataFlavorSupported(DataFlavor flavor) { + return flavorList.contains(flavor); + } + } + + public static class ByteStringTransferable implements Transferable { + + private final DataFlavor[] flavors = { + BYTE_STRING_NO_SPACE_TYPE.getFlavor(), + BYTE_STRING_TYPE.getFlavor(), + DataFlavor.stringFlavor }; + private final List flavorList = Arrays.asList(flavors); + + private final String byteString; + private final String stringRepresentation; + + public ByteStringTransferable(String byteString) { + this(byteString, null); + } + + public ByteStringTransferable(String byteString, String stringRepresentation) { + this.byteString = byteString; + this.stringRepresentation = stringRepresentation; + } + + @Override + public Object getTransferData(DataFlavor flavor) + throws UnsupportedFlavorException, IOException { + if (flavor.equals(DataFlavor.stringFlavor)) { + if (stringRepresentation != null) { + return stringRepresentation; } return byteString; // just default to the byte string when no 'special' string data } @@ -481,15 +630,6 @@ public abstract class ByteCopier { if (flavor.equals(BYTE_STRING_NO_SPACE_TYPE.getFlavor())) { return byteString; } - if (flavor.equals(PYTHON_BYTE_STRING_TYPE.getFlavor())) { - return byteString; - } - if (flavor.equals(PYTHON_LIST_TYPE.getFlavor())) { - return byteString; - } - if (flavor.equals(CPP_BYTE_ARRAY_TYPE.getFlavor())) { - return byteString; - } throw new UnsupportedFlavorException(flavor); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/ClipboardType.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/ClipboardType.java index 63506dff8f..83baa67dfa 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/ClipboardType.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/ClipboardType.java @@ -1,6 +1,5 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +21,7 @@ import java.awt.datatransfer.DataFlavor; * Defines a "type" for items in the Clipboard */ public class ClipboardType { - + private DataFlavor flavor; private String typeName; @@ -35,26 +34,25 @@ public class ClipboardType { this.flavor = flavor; this.typeName = typeName; } - + /** - * Returns the DataFlavor for this ClipboardType + * Returns the DataFlavor for this type + * @return the flavor */ public DataFlavor getFlavor() { return flavor; } /** - * Returns the name of this Clipboard Type. + * Returns the name of this type + * @return the name */ public String getTypeName() { return typeName; } - /** - * @see java.lang.Object#toString() - */ @Override - public String toString() { + public String toString() { return typeName; } } diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/clipboard/CodeBrowserClipboardProviderTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/clipboard/CodeBrowserClipboardProviderTest.java new file mode 100644 index 0000000000..8d6d698144 --- /dev/null +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/clipboard/CodeBrowserClipboardProviderTest.java @@ -0,0 +1,224 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.clipboard; + +import static org.hamcrest.core.IsInstanceOf.*; +import static org.junit.Assert.*; + +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; + +import org.junit.Before; +import org.junit.Test; + +import docking.dnd.StringTransferable; +import docking.widgets.OptionDialog; +import ghidra.app.util.ByteCopier; +import ghidra.app.util.ByteCopier.ProgrammingByteStringTransferable; +import ghidra.app.util.ClipboardType; +import ghidra.framework.cmd.Command; +import ghidra.framework.model.DomainObject; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.database.ProgramBuilder; +import ghidra.program.model.address.*; +import ghidra.program.model.data.DataType; +import ghidra.program.model.listing.*; +import ghidra.program.model.mem.Memory; +import ghidra.program.model.mem.MemoryAccessException; +import ghidra.program.model.symbol.RefType; +import ghidra.program.model.symbol.SourceType; +import ghidra.program.util.ProgramLocation; +import ghidra.program.util.ProgramSelection; +import ghidra.test.AbstractGhidraHeadedIntegrationTest; +import ghidra.test.DummyTool; +import ghidra.util.NumericUtilities; +import ghidra.util.exception.AssertException; +import ghidra.util.task.TaskMonitor; + +public class CodeBrowserClipboardProviderTest extends AbstractGhidraHeadedIntegrationTest { + + private Program program; + private CodeBrowserClipboardProvider clipboardProvider; + + @Before + public void setUp() throws Exception { + + program = createProgram(); + PluginTool tool = new DummyTool() { + @Override + public boolean execute(Command command, DomainObject obj) { + boolean result = command.applyTo(obj); + if (!result) { + throw new AssertException("Failed to write bytes"); + } + return true; + } + }; + + clipboardProvider = new CodeBrowserClipboardProvider(tool, null); + clipboardProvider.setProgram(program); + } + + private Program createProgram() throws Exception { + ProgramBuilder builder = new ProgramBuilder("default", ProgramBuilder._TOY, this); + + builder.createMemory("test", "0x01001050", 20000); + + builder.setBytes("0x01001050", + "0e 5e f4 77 33 58 f4 77 91 45 f4 77 88 7c f4 77 8d 70 f5 77 05 62 f4 77 f0 a3 " + + "f4 77 09 56 f4 77 10 17 f4 77 f7 29 f6 77 02 59 f4 77"); + + builder.setBytes("0x01002050", "00 00 00 00 00 00 00 00 00 00 00 00 00"); + + builder.createMemoryReference("0x01002cc0", "0x01002cf0", RefType.DATA, + SourceType.USER_DEFINED); + builder.createMemoryReference("0x01002d04", "0x01002d0f", RefType.DATA, + SourceType.USER_DEFINED); + + DataType dt = DataType.DEFAULT; + Parameter p = new ParameterImpl(null, dt, builder.getProgram()); + builder.createEmptyFunction("ghidra", "0x01002cf5", 1, dt, p); + builder.createEmptyFunction("sscanf", "0x0100415a", 1, dt, p); + + builder.setBytes("0x0100418c", "ff 15 08 10 00 01"); + builder.disassemble("0x0100418c", 6); + + return builder.getProgram(); + } + + @Test + public void testCopyPasteSpecial_PythonByteString() throws Exception { + + int length = 4; + clipboardProvider.setSelection(selection("0x01001050", length)); + ClipboardType type = ByteCopier.PYTHON_BYTE_STRING_TYPE; + Transferable transferable = clipboardProvider.copySpecial(type, TaskMonitor.DUMMY); + assertThat(transferable, instanceOf(ProgrammingByteStringTransferable.class)); + + String byteString = (String) transferable.getTransferData(DataFlavor.stringFlavor); + assertEquals("b'\\x0e\\x5e\\xf4\\x77'", byteString); + + String pasteAddress = "0x01002050"; + paste(pasteAddress, transferable); + assertBytesAt(pasteAddress, "0e 5e f4 77", length); + } + + @Test + public void testCopyPasteSpecial_PythonListString() throws Exception { + + int length = 4; + clipboardProvider.setSelection(selection("0x01001050", 4)); + ClipboardType type = ByteCopier.PYTHON_LIST_TYPE; + Transferable transferable = clipboardProvider.copySpecial(type, TaskMonitor.DUMMY); + assertThat(transferable, instanceOf(ProgrammingByteStringTransferable.class)); + + String byteString = (String) transferable.getTransferData(DataFlavor.stringFlavor); + assertEquals("[ 0x0e, 0x5e, 0xf4, 0x77 ]", byteString); + + String pasteAddress = "0x01002050"; + paste(pasteAddress, transferable); + assertBytesAt(pasteAddress, "0e 5e f4 77", length); + } + + @Test + public void testCopyPasteSpecial_CppByteArray() throws Exception { + + int length = 4; + clipboardProvider.setSelection(selection("0x01001050", 4)); + ClipboardType type = ByteCopier.CPP_BYTE_ARRAY_TYPE; + Transferable transferable = clipboardProvider.copySpecial(type, TaskMonitor.DUMMY); + assertThat(transferable, instanceOf(ProgrammingByteStringTransferable.class)); + + String byteString = (String) transferable.getTransferData(DataFlavor.stringFlavor); + assertEquals("{ 0x0e, 0x5e, 0xf4, 0x77 }", byteString); + + String pasteAddress = "0x01002050"; + paste(pasteAddress, transferable); + assertBytesAt(pasteAddress, "0e 5e f4 77", length); + } + + @Test + public void testCopyPaste_ByteString() throws Exception { + + String byteString = "0e 5e f4 77"; + StringTransferable transferable = new StringTransferable(byteString); + + String pasteAddress = "0x01002050"; + paste(pasteAddress, transferable); + assertBytesAt(pasteAddress, byteString, 4); + } + + @Test + public void testCopyPaste_ByteString_MixedWithNonAscii() throws Exception { + + // the byte string contains ascii and non-ascii + String byteString = + "0e " + ((char) 0x80) + " 5e " + ((char) 0x81 + " f4 " + ((char) 0x82)) + " 77"; + String asciiByteString = "0e 5e f4 77"; + StringTransferable transferable = new StringTransferable(byteString); + + String pasteAddress = "0x01002050"; + paste(pasteAddress, transferable); + assertBytesAt(pasteAddress, asciiByteString, 4); + } + +//================================================================================================== +// Private Methods +//================================================================================================== + + private void paste(String address, Transferable transferable) { + + tx(program, () -> { + doPaste(address, transferable); + }); + } + + private void doPaste(String address, Transferable transferable) { + clipboardProvider.setLocation(location(address)); + runSwing(() -> clipboardProvider.paste(transferable), false); + + OptionDialog confirmDialog = waitForDialogComponent(OptionDialog.class); + pressButtonByText(confirmDialog, "Yes"); + + waitForTasks(); + program.flushEvents(); + waitForSwing(); + } + + private void assertBytesAt(String address, String bytes, int length) + throws MemoryAccessException { + Memory memory = program.getMemory(); + byte[] memoryBytes = new byte[length]; + memory.getBytes(addr(address), memoryBytes, 0, length); + + String memoryByteString = NumericUtilities.convertBytesToString(memoryBytes, " "); + assertEquals(bytes, memoryByteString); + } + + private ProgramSelection selection(String addressString, int n) { + Address address = addr(addressString); + AddressSetView addresses = new AddressSet(address, address.add(n - 1)); + return new ProgramSelection(addresses); + } + + private Address addr(String addr) { + return program.getAddressFactory().getAddress(addr); + } + + private ProgramLocation location(String addressString) { + return new ProgramLocation(program, addr(addressString)); + } +} diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/clipboard/CopyPasteCommentsTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/clipboard/CopyPasteCommentsTest.java index 394290918f..bfe29d65b0 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/clipboard/CopyPasteCommentsTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/clipboard/CopyPasteCommentsTest.java @@ -132,6 +132,7 @@ public class CopyPasteCommentsTest extends AbstractProgramBasedTest { pmTwo.openProgram(df2); programTwo = (ProgramDB) pmTwo.getCurrentProgram(); }); + } @Test @@ -145,7 +146,8 @@ public class CopyPasteCommentsTest extends AbstractProgramBasedTest { goTo(toolOne, 0x32a); ClipboardPlugin plugin = getPlugin(toolOne, ClipboardPlugin.class); - DockingActionIf pasteAction = getAction(plugin, "Paste"); + ClipboardContentProviderService service = getClipboardService(plugin); + DockingActionIf pasteAction = getLocalAction(service, "Paste", plugin); assertEnabled(pasteAction, cb.getProvider()); } @@ -194,8 +196,9 @@ public class CopyPasteCommentsTest extends AbstractProgramBasedTest { // in Program One, add MyLabel at 032a int transactionID = programOne.startTransaction("test"); - programOne.getSymbolTable().createLabel(addr(programOne, 0x032a), "MyLabel", - SourceType.USER_DEFINED); + programOne.getSymbolTable() + .createLabel(addr(programOne, 0x032a), "MyLabel", + SourceType.USER_DEFINED); programOne.endTransaction(transactionID, true); goTo(toolTwo, 0x0326); @@ -385,8 +388,9 @@ public class CopyPasteCommentsTest extends AbstractProgramBasedTest { programOne.getSymbolTable().getSymbol("LAB_0331", addr(programOne, 0x0331), null); // in Browser(1) change default label at 331 to JUNK int transactionID = programOne.startTransaction("test"); - programOne.getSymbolTable().createLabel(addr(programOne, 0x0331), "JUNK", - SourceType.USER_DEFINED); + programOne.getSymbolTable() + .createLabel(addr(programOne, 0x0331), "JUNK", + SourceType.USER_DEFINED); programOne.endTransaction(transactionID, true); // // in Browser(1) go to 331 @@ -426,8 +430,9 @@ public class CopyPasteCommentsTest extends AbstractProgramBasedTest { public void testPasteAtMultipleLabels() throws Exception { // in program 2, create a second label, JUNK2, at 0331 int transactionID = programOne.startTransaction("test"); - programOne.getSymbolTable().createLabel(addr(programOne, 0x331), "JUNK2", - SourceType.USER_DEFINED); + programOne.getSymbolTable() + .createLabel(addr(programOne, 0x331), "JUNK2", + SourceType.USER_DEFINED); programOne.endTransaction(transactionID, true); // in Browser(2) select 331 through 334, contains "RSR10" @@ -474,8 +479,9 @@ public class CopyPasteCommentsTest extends AbstractProgramBasedTest { @Test public void testPasteWhereUserLabelExists() throws Exception { int transactionID = programOne.startTransaction("test"); - programOne.getSymbolTable().createLabel(addr(programOne, 0x331), "JUNK2", - SourceType.USER_DEFINED); + programOne.getSymbolTable() + .createLabel(addr(programOne, 0x331), "JUNK2", + SourceType.USER_DEFINED); programOne.endTransaction(transactionID, true); // in Browser(2) select 331 through 334, contains "RSR10" @@ -586,13 +592,19 @@ public class CopyPasteCommentsTest extends AbstractProgramBasedTest { cb.goToField(addr(programOne, 0x0331), LabelFieldFactory.FIELD_NAME, 0, 0); f = (ListingTextField) cb.getCurrentField(); - assertEquals(programOne.getSymbolTable().getSymbol("LAB_00000331", addr(programOne, 0x0331), - null).getName(), f.getText()); + assertEquals(programOne.getSymbolTable() + .getSymbol("LAB_00000331", addr(programOne, 0x0331), + null) + .getName(), + f.getText()); cb.goToField(addr(programOne, 0x031b), LabelFieldFactory.FIELD_NAME, 0, 0); f = (ListingTextField) cb.getCurrentField(); - assertEquals(programOne.getSymbolTable().getSymbol("LAB_0000031b", addr(programOne, 0x031b), - null).getName(), f.getText()); + assertEquals(programOne.getSymbolTable() + .getSymbol("LAB_0000031b", addr(programOne, 0x031b), + null) + .getName(), + f.getText()); redo(programOne); @@ -616,8 +628,9 @@ public class CopyPasteCommentsTest extends AbstractProgramBasedTest { // create a function over the range 0x31b through 0x0343. int transactionID = programOne.startTransaction("test"); String name = SymbolUtilities.getDefaultFunctionName(min); - programOne.getListing().createFunction(name, min, new AddressSet(min, max), - SourceType.USER_DEFINED); + programOne.getListing() + .createFunction(name, min, new AddressSet(min, max), + SourceType.USER_DEFINED); programOne.endTransaction(transactionID, true); programOne.flushEvents(); waitForSwing(); @@ -710,19 +723,23 @@ public class CopyPasteCommentsTest extends AbstractProgramBasedTest { private void copyToolTwoLabels() { ClipboardPlugin plugin = getPlugin(toolTwo, ClipboardPlugin.class); ClipboardContentProviderService service = - getCodeBrowserClipboardContentProviderService(plugin); + getClipboardService(plugin); DockingAction action = getLocalAction(service, "Copy Special", plugin); assertNotNull(action); assertEnabled(action, cb2.getProvider()); - plugin.copySpecial(service, CodeBrowserClipboardProvider.LABELS_COMMENTS_TYPE); + runSwing( + () -> plugin.copySpecial(service, CodeBrowserClipboardProvider.LABELS_COMMENTS_TYPE)); } private void pasteToolOne() { + ClipboardPlugin plugin = getPlugin(toolOne, ClipboardPlugin.class); - DockingActionIf pasteAction = getAction(plugin, "Paste"); + ClipboardContentProviderService service = getClipboardService(plugin); + DockingActionIf pasteAction = getLocalAction(service, "Paste", plugin); assertEnabled(pasteAction, cb.getProvider()); performAction(pasteAction, true); + waitForSwing(); } private void setupTool(PluginTool tool) throws Exception { @@ -809,13 +826,13 @@ public class CopyPasteCommentsTest extends AbstractProgramBasedTest { waitForSwing(); } - private ClipboardContentProviderService getCodeBrowserClipboardContentProviderService( + private ClipboardContentProviderService getClipboardService( ClipboardPlugin clipboardPlugin) { Map serviceMap = (Map) getInstanceField("serviceActionMap", clipboardPlugin); Set keySet = serviceMap.keySet(); for (Object name : keySet) { ClipboardContentProviderService service = (ClipboardContentProviderService) name; - if (service instanceof CodeBrowserClipboardProvider) { + if (service.getClass().equals(CodeBrowserClipboardProvider.class)) { return service; } } @@ -825,17 +842,12 @@ public class CopyPasteCommentsTest extends AbstractProgramBasedTest { @SuppressWarnings("unchecked") private DockingAction getLocalAction(ClipboardContentProviderService service, String actionName, ClipboardPlugin clipboardPlugin) { - Map serviceMap = (Map) getInstanceField("serviceActionMap", clipboardPlugin); - Set keySet = serviceMap.keySet(); - for (Object name : keySet) { - ClipboardContentProviderService currentService = (ClipboardContentProviderService) name; - if (currentService == service) { - List actionList = (List) serviceMap.get(service); - for (DockingAction pluginAction : actionList) { - if (pluginAction.getName().equals(actionName)) { - return pluginAction; - } - } + Map actionsByService = + (Map) getInstanceField("serviceActionMap", clipboardPlugin); + List actionList = (List) actionsByService.get(service); + for (DockingAction pluginAction : actionList) { + if (pluginAction.getName().equals(actionName)) { + return pluginAction; } } @@ -844,7 +856,9 @@ public class CopyPasteCommentsTest extends AbstractProgramBasedTest { private void assertEnabled(DockingActionIf action, ComponentProvider provider) { boolean isEnabled = - runSwing(() -> action.isEnabledForContext(provider.getActionContext(null))); - assertTrue(isEnabled); + runSwing(() -> { + return action.isEnabledForContext(provider.getActionContext(null)); + }); + assertTrue("Action was not enabled when it should be", isEnabled); } } diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/clipboard/CopyPasteFunctionInfoTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/clipboard/CopyPasteFunctionInfoTest.java index 88926fe381..71afa91362 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/clipboard/CopyPasteFunctionInfoTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/clipboard/CopyPasteFunctionInfoTest.java @@ -15,8 +15,7 @@ */ package ghidra.app.plugin.core.clipboard; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; import java.awt.Point; import java.awt.event.MouseEvent; @@ -26,6 +25,7 @@ import javax.swing.SwingUtilities; import org.junit.*; +import docking.action.DockingAction; import docking.action.DockingActionIf; import docking.widgets.fieldpanel.FieldPanel; import ghidra.app.cmd.function.SetVariableCommentCmd; @@ -53,11 +53,8 @@ import ghidra.program.util.ProgramSelection; import ghidra.test.*; /** - * Test copy/paste function information. - * - * + * Test copy/paste function information */ - public class CopyPasteFunctionInfoTest extends AbstractGhidraHeadedIntegrationTest { private TestEnv env; @@ -72,14 +69,6 @@ public class CopyPasteFunctionInfoTest extends AbstractGhidraHeadedIntegrationTe private Options fieldOptions2; private CodeBrowserPlugin cb1; - /** - * Constructor for CopyPasteFunctionInfoTest. - * @param arg0 - */ - public CopyPasteFunctionInfoTest() { - super(); - } - private Program buildNotepad(String name) throws Exception { ToyProgramBuilder builder = new ToyProgramBuilder(name, true, ProgramBuilder._TOY); builder.createMemory("test1", "0x01001000", 0x8000); @@ -133,9 +122,6 @@ public class CopyPasteFunctionInfoTest extends AbstractGhidraHeadedIntegrationTe resetOptions(); } - /* - * @see TestCase#tearDown() - */ @After public void tearDown() throws Exception { env.dispose(); @@ -174,12 +160,7 @@ public class CopyPasteFunctionInfoTest extends AbstractGhidraHeadedIntegrationTe goToAddr(toolTwo, 0x1004700); click2(); - // paste - plugin = getPlugin(toolTwo, ClipboardPlugin.class); - DockingActionIf pasteAction = getAction(plugin, "Paste"); - assertEnabled(pasteAction); - performAction(pasteAction, true); - waitForSwing(); + paste(toolTwo); // function FUN_01004700 should be renamed to "ghidra" CodeBrowserPlugin cb = getPlugin(toolTwo, CodeBrowserPlugin.class); @@ -219,12 +200,7 @@ public class CopyPasteFunctionInfoTest extends AbstractGhidraHeadedIntegrationTe goToAddr(toolTwo, entryAddr); click2(); - // paste - plugin = getPlugin(toolTwo, ClipboardPlugin.class); - DockingActionIf pasteAction = getAction(plugin, "Paste"); - assertEnabled(pasteAction); - performAction(pasteAction, true); - waitForSwing(); + paste(toolTwo); CodeBrowserPlugin cb = getPlugin(toolTwo, CodeBrowserPlugin.class); cb.goToField(entryAddr, PlateFieldFactory.FIELD_NAME, 0, 0); @@ -283,12 +259,7 @@ public class CopyPasteFunctionInfoTest extends AbstractGhidraHeadedIntegrationTe goToAddr(toolTwo, addr); click2(); - // paste - plugin = getPlugin(toolTwo, ClipboardPlugin.class); - DockingActionIf pasteAction = getAction(plugin, "Paste"); - assertEnabled(pasteAction); - performAction(pasteAction, true); - waitForSwing(); + paste(toolTwo); // verify the code browser field shows the comment func = programTwo.getListing().getFunctionAt(addr); @@ -338,12 +309,8 @@ public class CopyPasteFunctionInfoTest extends AbstractGhidraHeadedIntegrationTe addr = getAddr(programTwo, 0x01004260); goToAddr(toolTwo, addr); click2(); - // paste - plugin = getPlugin(toolTwo, ClipboardPlugin.class); - DockingActionIf pasteAction = getAction(plugin, "Paste"); - assertEnabled(pasteAction); - performAction(pasteAction, true); - waitForSwing(); + + paste(toolTwo); // verify the code browser field shows the comment func = programTwo.getListing().getFunctionAt(addr); @@ -416,12 +383,7 @@ public class CopyPasteFunctionInfoTest extends AbstractGhidraHeadedIntegrationTe goToAddr(toolTwo, 0x0100176f); click2(); - // paste - plugin = getPlugin(toolTwo, ClipboardPlugin.class); - DockingActionIf pasteAction = getAction(plugin, "Paste"); - assertEnabled(pasteAction); - performAction(pasteAction, true); - waitForSwing(); + paste(toolTwo); addr = getAddr(programTwo, 0x0100176f); // nothing should happen with the stack variable comments @@ -433,7 +395,36 @@ public class CopyPasteFunctionInfoTest extends AbstractGhidraHeadedIntegrationTe assertEquals(1, f.getNumRows()); } - ///////////////////////////////////////////////////////////////////////// +//================================================================================================== +// Private Methods +//================================================================================================== + + private void paste(PluginTool tool) { + + ClipboardPlugin plugin = getPlugin(tool, ClipboardPlugin.class); + ClipboardContentProviderService service = + getCodeBrowserClipboardContentProviderService(plugin); + DockingActionIf pasteAction = getClipboardAction(plugin, service, "Paste"); + assertEnabled(pasteAction); + performAction(pasteAction, true); + waitForSwing(); + } + + private DockingActionIf getClipboardAction(ClipboardPlugin plugin, + ClipboardContentProviderService service, String actionName) { + + @SuppressWarnings("unchecked") + Map> map = + (Map>) getInstanceField( + "serviceActionMap", plugin); + List list = map.get(service); + for (DockingAction pluginAction : list) { + if (pluginAction.getName().equals(actionName)) { + return pluginAction; + } + } + return null; + } private void setupTool(PluginTool tool) throws Exception { tool.addPlugin(ClipboardPlugin.class.getName()); @@ -511,10 +502,9 @@ public class CopyPasteFunctionInfoTest extends AbstractGhidraHeadedIntegrationTe ClipboardPlugin clipboardPlugin) { Map serviceMap = (Map) getInstanceField("serviceActionMap", clipboardPlugin); Set keySet = serviceMap.keySet(); - for (Object name : keySet) { - ClipboardContentProviderService service = (ClipboardContentProviderService) name; - if (service instanceof CodeBrowserClipboardProvider) { - return service; + for (Object service : keySet) { + if (service.getClass().equals(CodeBrowserClipboardProvider.class)) { + return (ClipboardContentProviderService) service; } } return null; diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerClipboardProvider.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerClipboardProvider.java index 6b778b4974..0c664cf7ff 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerClipboardProvider.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerClipboardProvider.java @@ -58,7 +58,6 @@ public class ByteViewerClipboardProvider extends ByteCopier PluginTool tool) { this.provider = provider; this.tool = tool; - currentProgram = provider.getProgram(); } @Override @@ -80,11 +79,6 @@ public class ByteViewerClipboardProvider extends ByteCopier @Override public boolean paste(Transferable pasteData) { - if (!supportsPasteTransferable(pasteData)) { - tool.setStatusInfo("Paste failed: No valid data on clipboard", true); - return false; - } - try { // try the default paste return pasteBytes(pasteData); @@ -106,42 +100,17 @@ public class ByteViewerClipboardProvider extends ByteCopier @Override public Transferable copy(TaskMonitor monitor) { String byteString = copyBytesAsString(currentSelection, true, monitor); - String textSelection = provider.getCurrentTextSelection(); - return new ByteViewerTransferable(byteString, textSelection); + String textSelection = getTextSelection(); + return new ByteStringTransferable(byteString, textSelection); + } + + protected String getTextSelection() { + return provider.getCurrentTextSelection(); } @Override public Transferable copySpecial(ClipboardType copyType, TaskMonitor monitor) { - - String byteString = null; - if (copyType == BYTE_STRING_TYPE) { - byteString = copyBytesAsString(currentSelection, true, monitor); - } - else if (copyType == BYTE_STRING_NO_SPACE_TYPE) { - byteString = copyBytesAsString(currentSelection, false, monitor); - } - else if (copyType == PYTHON_BYTE_STRING_TYPE) { - byteString = "b'" - + copyBytesAsString(currentSelection, true, monitor).replaceAll(" ", "\\x") + "'"; - } - else if (copyType == PYTHON_LIST_TYPE) { - byteString = "[ " - + copyBytesAsString(currentSelection, true, monitor).replaceAll(" ", ", 0x") + " ]"; - } - else if (copyType == CPP_BYTE_ARRAY_TYPE) { - byteString = "{ " - + copyBytesAsString(currentSelection, true, monitor).replaceAll(" ", ", 0x") + " }"; - } - else { - return null; - } - - return new ByteViewerTransferable(byteString); - } - - void setSelection(ProgramSelection selection) { - currentSelection = selection; - updateEnablement(); + return copyBytes(copyType, monitor); } private void updateEnablement() { @@ -153,6 +122,11 @@ public class ByteViewerClipboardProvider extends ByteCopier currentLocation = location; } + void setSelection(ProgramSelection selection) { + currentSelection = selection; + updateEnablement(); + } + void setProgram(Program p) { currentProgram = p; currentLocation = null; diff --git a/Ghidra/Features/ByteViewer/src/test/java/ghidra/app/plugin/core/byteviewer/ByteViewerClipboardProviderTest.java b/Ghidra/Features/ByteViewer/src/test/java/ghidra/app/plugin/core/byteviewer/ByteViewerClipboardProviderTest.java new file mode 100644 index 0000000000..6618d4dfb8 --- /dev/null +++ b/Ghidra/Features/ByteViewer/src/test/java/ghidra/app/plugin/core/byteviewer/ByteViewerClipboardProviderTest.java @@ -0,0 +1,239 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.byteviewer; + +import static org.hamcrest.core.IsInstanceOf.*; +import static org.junit.Assert.*; + +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; + +import org.junit.Before; +import org.junit.Test; + +import docking.widgets.OptionDialog; +import ghidra.app.util.ByteCopier; +import ghidra.app.util.ByteCopier.ByteStringTransferable; +import ghidra.app.util.ByteCopier.ProgrammingByteStringTransferable; +import ghidra.app.util.ClipboardType; +import ghidra.framework.cmd.Command; +import ghidra.framework.model.DomainObject; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.database.ProgramBuilder; +import ghidra.program.model.address.*; +import ghidra.program.model.data.DataType; +import ghidra.program.model.listing.*; +import ghidra.program.model.mem.Memory; +import ghidra.program.model.mem.MemoryAccessException; +import ghidra.program.model.symbol.RefType; +import ghidra.program.model.symbol.SourceType; +import ghidra.program.util.ProgramLocation; +import ghidra.program.util.ProgramSelection; +import ghidra.test.AbstractGhidraHeadedIntegrationTest; +import ghidra.test.DummyTool; +import ghidra.util.NumericUtilities; +import ghidra.util.exception.AssertException; +import ghidra.util.task.TaskMonitor; + +public class ByteViewerClipboardProviderTest extends AbstractGhidraHeadedIntegrationTest { + + private Program program; + private ByteViewerClipboardProvider clipboardProvider; + + @Before + public void setUp() throws Exception { + + program = createProgram(); + PluginTool tool = new DummyTool() { + @Override + public boolean execute(Command command, DomainObject obj) { + boolean result = command.applyTo(obj); + if (!result) { + throw new AssertException("Failed to write bytes"); + } + return true; + } + }; + + clipboardProvider = new ByteViewerClipboardProvider(null, tool) { + + // overridden to stub this method, since we don't have a real provider + @Override + protected String getTextSelection() { + return null; + } + }; + clipboardProvider.setProgram(program); + } + + private Program createProgram() throws Exception { + ProgramBuilder builder = new ProgramBuilder("default", ProgramBuilder._TOY, this); + + builder.createMemory("test", "0x01001050", 20000); + + builder.setBytes("0x01001050", + "0e 5e f4 77 33 58 f4 77 91 45 f4 77 88 7c f4 77 8d 70 f5 77 05 62 f4 77 f0 a3 " + + "f4 77 09 56 f4 77 10 17 f4 77 f7 29 f6 77 02 59 f4 77"); + + builder.setBytes("0x01002050", "00 00 00 00 00 00 00 00 00 00 00 00 00"); + + builder.createMemoryReference("0x01002cc0", "0x01002cf0", RefType.DATA, + SourceType.USER_DEFINED); + builder.createMemoryReference("0x01002d04", "0x01002d0f", RefType.DATA, + SourceType.USER_DEFINED); + + DataType dt = DataType.DEFAULT; + Parameter p = new ParameterImpl(null, dt, builder.getProgram()); + builder.createEmptyFunction("ghidra", "0x01002cf5", 1, dt, p); + builder.createEmptyFunction("sscanf", "0x0100415a", 1, dt, p); + + builder.setBytes("0x0100418c", "ff 15 08 10 00 01"); + builder.disassemble("0x0100418c", 6); + + return builder.getProgram(); + } + + @Test + public void testCopyPasteSpecial_ByteString() throws Exception { + int length = 4; + clipboardProvider.setSelection(selection("0x01001050", length)); + ClipboardType type = ByteCopier.BYTE_STRING_TYPE; + Transferable transferable = clipboardProvider.copySpecial(type, TaskMonitor.DUMMY); + assertThat(transferable, instanceOf(ByteStringTransferable.class)); + + String byteString = (String) transferable.getTransferData(DataFlavor.stringFlavor); + assertEquals("0e 5e f4 77", byteString); + + String pasteAddress = "0x01002050"; + paste(pasteAddress, transferable); + assertBytesAt(pasteAddress, "0e 5e f4 77", length); + } + + @Test + public void testCopyPasteSpecial_ByteStringNoSpaces() throws Exception { + int length = 4; + clipboardProvider.setSelection(selection("0x01001050", length)); + ClipboardType type = ByteCopier.BYTE_STRING_NO_SPACE_TYPE; + Transferable transferable = clipboardProvider.copySpecial(type, TaskMonitor.DUMMY); + assertThat(transferable, instanceOf(ByteStringTransferable.class)); + + String byteString = (String) transferable.getTransferData(DataFlavor.stringFlavor); + assertEquals("0e5ef477", byteString); + + String pasteAddress = "0x01002050"; + paste(pasteAddress, transferable); + assertBytesAt(pasteAddress, "0e 5e f4 77", length); + } + + @Test + public void testCopyPasteSpecial_PythonByteString() throws Exception { + + int length = 4; + clipboardProvider.setSelection(selection("0x01001050", length)); + ClipboardType type = ByteCopier.PYTHON_BYTE_STRING_TYPE; + Transferable transferable = clipboardProvider.copySpecial(type, TaskMonitor.DUMMY); + assertThat(transferable, instanceOf(ProgrammingByteStringTransferable.class)); + + String byteString = (String) transferable.getTransferData(DataFlavor.stringFlavor); + assertEquals("b'\\x0e\\x5e\\xf4\\x77'", byteString); + + String pasteAddress = "0x01002050"; + paste(pasteAddress, transferable); + assertBytesAt(pasteAddress, "0e 5e f4 77", length); + } + + @Test + public void testCopyPasteSpecial_PythonListString() throws Exception { + + int length = 4; + clipboardProvider.setSelection(selection("0x01001050", 4)); + ClipboardType type = ByteCopier.PYTHON_LIST_TYPE; + Transferable transferable = clipboardProvider.copySpecial(type, TaskMonitor.DUMMY); + assertThat(transferable, instanceOf(ProgrammingByteStringTransferable.class)); + + String byteString = (String) transferable.getTransferData(DataFlavor.stringFlavor); + assertEquals("[ 0x0e, 0x5e, 0xf4, 0x77 ]", byteString); + + String pasteAddress = "0x01002050"; + paste(pasteAddress, transferable); + assertBytesAt(pasteAddress, "0e 5e f4 77", length); + } + + @Test + public void testCopyPasteSpecial_CppByteArray() throws Exception { + + int length = 4; + clipboardProvider.setSelection(selection("0x01001050", 4)); + ClipboardType type = ByteCopier.CPP_BYTE_ARRAY_TYPE; + Transferable transferable = clipboardProvider.copySpecial(type, TaskMonitor.DUMMY); + assertThat(transferable, instanceOf(ProgrammingByteStringTransferable.class)); + + String byteString = (String) transferable.getTransferData(DataFlavor.stringFlavor); + assertEquals("{ 0x0e, 0x5e, 0xf4, 0x77 }", byteString); + + String pasteAddress = "0x01002050"; + paste(pasteAddress, transferable); + assertBytesAt(pasteAddress, "0e 5e f4 77", length); + } + +//================================================================================================== +// Private Methods +//================================================================================================== + + private void paste(String address, Transferable transferable) { + + tx(program, () -> { + doPaste(address, transferable); + }); + } + + private void doPaste(String address, Transferable transferable) { + clipboardProvider.setLocation(location(address)); + runSwing(() -> clipboardProvider.paste(transferable), false); + + OptionDialog confirmDialog = waitForDialogComponent(OptionDialog.class); + pressButtonByText(confirmDialog, "Yes"); + + waitForTasks(); + program.flushEvents(); + waitForSwing(); + } + + private void assertBytesAt(String address, String bytes, int length) + throws MemoryAccessException { + Memory memory = program.getMemory(); + byte[] memoryBytes = new byte[length]; + memory.getBytes(addr(address), memoryBytes, 0, length); + + String memoryByteString = NumericUtilities.convertBytesToString(memoryBytes, " "); + assertEquals(bytes, memoryByteString); + } + + private ProgramSelection selection(String addressString, int n) { + Address address = addr(addressString); + AddressSetView addresses = new AddressSet(address, address.add(n - 1)); + return new ProgramSelection(addresses); + } + + private Address addr(String addr) { + return program.getAddressFactory().getAddress(addr); + } + + private ProgramLocation location(String addressString) { + return new ProgramLocation(program, addr(addressString)); + } + +} diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/ClipboardPluginScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/ClipboardPluginScreenShots.java index eb95b5b2f8..12cf132b93 100644 --- a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/ClipboardPluginScreenShots.java +++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/ClipboardPluginScreenShots.java @@ -16,16 +16,22 @@ package help.screenshot; import java.awt.*; +import java.util.List; +import java.util.Map; +import java.util.Set; import javax.swing.*; import org.junit.Test; import docking.DialogComponentProvider; +import docking.action.DockingAction; +import docking.action.DockingActionIf; import docking.widgets.fieldpanel.FieldPanel; -import ghidra.app.plugin.core.clipboard.CopyPasteSpecialDialog; +import ghidra.app.plugin.core.clipboard.*; import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin; import ghidra.app.plugin.core.codebrowser.CodeViewerProvider; +import ghidra.app.services.ClipboardContentProviderService; import ghidra.app.util.viewer.field.MnemonicFieldFactory; import ghidra.program.util.ProgramSelection; import ghidra.util.Msg; @@ -33,10 +39,6 @@ import ghidra.util.exception.AssertException; public class ClipboardPluginScreenShots extends GhidraScreenShotGenerator { - public ClipboardPluginScreenShots() { - super(); - } - @Test public void testCaptureCopySpecial() { @@ -47,9 +49,12 @@ public class ClipboardPluginScreenShots extends GhidraScreenShotGenerator { makeSelection(0x401000, 0x401000); sel = cb.getCurrentSelection(); - Msg.debug(this, "selection: " + sel); - showCopySpecialDialog(); + CopyPasteSpecialDialog dialog = showCopySpecialDialog(); + Window window = SwingUtilities.windowForComponent(dialog.getComponent()); + + Dimension size = window.getSize(); + setWindowSize(window, size.width, 330); captureDialog(); } @@ -184,7 +189,39 @@ public class ClipboardPluginScreenShots extends GhidraScreenShotGenerator { crop(imageBounds); } - private void showCopySpecialDialog() { - performAction("Copy Special", "ClipboardPlugin", false); + private CopyPasteSpecialDialog showCopySpecialDialog() { + ClipboardPlugin plugin = getPlugin(tool, ClipboardPlugin.class); + ClipboardContentProviderService service = getClipboardService(plugin); + DockingActionIf pasteAction = getLocalAction(service, "Copy Special", plugin); + performAction(pasteAction, false); + return waitForDialogComponent(CopyPasteSpecialDialog.class); + } + + private ClipboardContentProviderService getClipboardService( + ClipboardPlugin clipboardPlugin) { + Map serviceMap = (Map) getInstanceField("serviceActionMap", clipboardPlugin); + Set keySet = serviceMap.keySet(); + for (Object name : keySet) { + ClipboardContentProviderService service = (ClipboardContentProviderService) name; + if (service.getClass().equals(CodeBrowserClipboardProvider.class)) { + return service; + } + } + return null; + } + + @SuppressWarnings("unchecked") + private DockingAction getLocalAction(ClipboardContentProviderService service, String actionName, + ClipboardPlugin clipboardPlugin) { + Map actionsByService = + (Map) getInstanceField("serviceActionMap", clipboardPlugin); + List actionList = (List) actionsByService.get(service); + for (DockingAction pluginAction : actionList) { + if (pluginAction.getName().equals(actionName)) { + return pluginAction; + } + } + + return null; } } diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/app/plugin/core/clipboard/CopyPasteTestSuite.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/app/plugin/core/clipboard/CopyPasteTestSuite.java new file mode 100644 index 0000000000..487a5cecd1 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/app/plugin/core/clipboard/CopyPasteTestSuite.java @@ -0,0 +1,36 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.clipboard; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.junit.runners.Suite.SuiteClasses; + +import ghidra.app.plugin.core.byteviewer.ByteViewerClipboardProviderTest; + +//@formatter:off +@RunWith(Suite.class) +@SuiteClasses({ + ClipboardPluginTest.class, + ByteViewerClipboardProviderTest.class, + CodeBrowserClipboardProviderTest.class, + CopyPasteCommentsTest.class, + CopyPasteFunctionInfoTest.class +}) +public class CopyPasteTestSuite { + // in the annotation +} +//@formatter:on