Merge remote-tracking branch 'origin/GP-1684_Dan_patchDataAction--SQUASHED'

This commit is contained in:
ghidra1
2022-01-26 17:35:28 -05:00
18 changed files with 1721 additions and 655 deletions
@@ -899,7 +899,6 @@ src/main/resources/help/Dummy_HelpSet.hs||GHIDRA||reviewed||END|
src/main/resources/help/empty.htm||GHIDRA||||END|
src/main/resources/images/2leftarrow.png||Nuvola Icons - LGPL 2.1|||Nuvola icon set|END|
src/main/resources/images/2rightarrow.png||Nuvola Icons - LGPL 2.1|||Nuvola icon set|END|
src/main/resources/images/ArmedMarkSelection.png||GHIDRA||||END|
src/main/resources/images/Array.png||GHIDRA||||END|
src/main/resources/images/B.gif||GHIDRA||||END|
src/main/resources/images/BookShelf.png||GHIDRA||||END|
@@ -9,109 +9,99 @@
<TITLE>Assembler</TITLE>
<LINK rel="stylesheet" type="text/css" href="../../shared/Frontpage.css">
<META name="generator" content="Microsoft FrontPage 4.0">
<STYLE>
<META name="generator" content="Microsoft FrontPage 4.0"><STYLE type="text/css">
ol.ratings li {
margin: 0 0 10px 0;
margin: 0 0 10px 0;
}
</STYLE>
</HEAD>
<BODY>
<H1><A name="AssembleAction"></A>Assembler</H1>
<H1><A name="plugin"></A>Assembler</H1>
<P>The <B>Patch Instruction</B> action will allow you to edit
the current assembly instruction in the listing. The first time you use the action, it may take
a moment to prepare the assembler for your processor. You can then edit the text of the
instruction, optionally replacing it altogether. As you edit, a content assists will provide
completion suggestions. It will present a list of byte sequences when the text comprises a
complete instruction. Activating a completion suggestion will input that text at your cursor.
Activating a byte sequence will complete the action, replacing the instruction at your cursor.
Pressing ESC or clicking outside of the assembly editor will cancel the action.</P>
<P align="center"><IMG border="1" src="images/Assembler.png"></P>
<P align="center"><IMG alt="" border="1" src="images/Assembler.png"></P>
<P align="center">Assembly Editor</P>
<BLOCKQUOTE>
<P>To edit assembly, select <B>Patch Instruction</B> from the
<A href="../CodeBrowserPlugin/CodeBrowser.htm#Code_Browser">Listing View</A> context menu or
press CTRL-SHIFT-G on the instruction to modify. Click the plus button below the content
assist to exhaust the undefined bits.</P>
</BLOCKQUOTE>
<P>The Assembler plugin provides actions for modifying program bytes by inputing mnemonic
assembly and data values.</P>
<H2>Actions</H2>
<P>There are two actions: One for instructions and one for data.</P>
<H3><A name="patch_instruction">Patch Instruction</A></H3>
<P>This action is available at any initialized memory address. It allows you to edit the
current assembly instruction in the listing. The first time you use the action, it may take a
moment to prepare the assembler for the current program's processor language. You can then edit
the text of the instruction, optionally replacing it altogether. As you edit, a content assist
will provide completion suggestions. When the text comprises a valid instruction, it will also
display assembled byte sequences. Activating a suggestion will input that text at your cursor.
Activating a byte sequence will finish the action, replacing the instruction at the current
address. Pressing ESC or clicking outside of the assembly editor will cancel the action.</P>
<P>At times, the assembler will generate undefined bits. Typically, these bits are actually
reserved, and so by default, the assembler fills them with 0s. The toggle below the content
assist will cause the assembler to fill them will all possible patterns of 1s and 0s. This
toggle should be used with care, since some languages may generate many undefined bits, and
each bit grows the list by a factor of 2.</P>
<P>Ghidra's assembler is based on the same SLEIGH modeling that powers the disassembler. This
offers some nice benefits:</P>
<UL>
<LI>There is no need for an external tool chain.
<LI>The assembler and disassembler share the same mnemonic syntax.
<LI>Most Ghidra-supported processors are also supported by the assembler.
<LI>Processors added to Ghidra automatically get an assembler.
<LI>There is no need for an external tool chain.</LI>
<LI>The assembler and disassembler share the same mnemonic syntax.</LI>
<LI>Most Ghidra-supported processors are also supported by the assembler.</LI>
<LI>Processors added to Ghidra automatically get an assembler (most of the time).</LI>
</UL>
<P>Keep in mind, the above list is in an ideal world. We are in the process of improving the
assembly engine and processor modules to eventually support assembly for all of Ghidra's
processors. In the meantime, we test several popular processors and assign a performance rating
to each. The possible ratings are:</P>
<P>Keep in mind, the above list is in an ideal world. The assembler essentially "solves" the
disassembly routine for its input given a desired output. While the process works well in
general, some languages require special attention. Thus, we periodically test our processor
languages for assembly support and assign each a performance rating. The possible ratings
are:</P>
<OL class="ratings">
<LI><B>Platinum:</B> Our automated tests did not find any errors. This offers the best
possible user experience.</LI>
<LI><B>Gold:</B> You will rarely encounter an error. You will find it very useful.</LI>
<LI><B>Silver:</B> You may occasionally encounter an error, but the assembler is still
usable. You will likely find it useful with occasional frustration.</LI>
<LI><B>Bronze:</B> You are likely to encounter errors, but there are enough working
instructions that the assembler is useful. You may find it useful, but it will probably be
frustrating.</LI>
<LI><B>Poor:</B> You are likely to encounter severe errors, and there are few instructions
that assemble. You may or may not find it useful, but we consider it unusable.</LI>
<LI><B>Unrated:</B> The processor is not tested, or the test failed before a rating could be
assigned. You might get lucky, but don't count on it.</LI>
<LI><B>Gold:</B> Our automated tests found a couple of small errors. You should rarely
encounter issues.</LI>
<LI><B>Silver:</B> Our automated tests found a handful of errors, or perhaps one large class
of errors. You will likely encounter one, but still find the assembler useful.</LI>
<LI><B>Bronze:</B> Our automated tests found many errors, or perhaps a few large classes of
errors. You are very likely to encounter them, but still may find the assembler useful, if
not just a bit frustrating.</LI>
<LI><B>Poor:</B> Our automated tests found many severe errors, and/or several large classes
of errors, or the tests could not complete. You will likely encounter an error the first time
you try using it. A few instructions may assemble correctly, but it'll be more frustrating
than useful.</LI>
<LI><B>Unrated:</B> The processor is not tested. Your experience will depend on the
complexity of the processor language. In general, the more SLEIGH context operations,
especially those preceding the main instruction table, the more issues that may arise.</LI>
</OL>
<P>As of this release, our tested processors fall under Platinum, Gold, or Poor.</P>
<UL>
<LI>Platinum</LI>
<UL>
<LI>68000:BE:32:default</LI>
<LI>AARCH64:BE:64:v8A</LI>
<LI>AARCH64:LE:64:v8A</LI>
<LI>ARM:BE:32:v7</LI>
<LI>ARM:LE:32:v7</LI>
<LI>avr8:LE:16:extended</LI>
<LI>MIPS:BE:32:default</LI>
<LI>MIPS:BE:32:micro</LI>
<LI>MIPS:BE:32:R6</LI>
<LI>MIPS:BE:64:64-32addr</LI>
<LI>MIPS:BE:64:R6</LI>
<LI>MIPS:BE:64:default</LI>
<LI>pa-risc:BE:32:default</LI>
<LI>PowerPC:BE:32:default</LI>
<LI>PowerPC:BE:64:A2-32addr</LI>
<LI>PowerPC:BE:64:A2ALT-32addr</LI>
<LI>PowerPC:BE:64:default</LI>
<LI>sparc:BE:32:default</LI>
<LI>sparc:BE:64:default</LI>
<LI>SuperH4:BE:32:default</LI>
<LI>SuperH4:LE:32:default</LI>
<LI>TI_MSP430X:LE:32:default</LI>
<LI>x86:LE:32:default</LI>
</UL>
<LI>Gold</LI>
<UL>
<LI>x86:LE:64:default</LI>
</UL>
<LI>Bronze</LI>
<UL>
<LI>avr32:BE:32:default</LI>
</UL>
<LI>Poor</LI>
<UL>
<LI>dsPIC30F:LE:24:default</LI>
</UL>
</UL>
<P>If the current processor scored anything less than <B>Platinum</B> you will receive a
friendly warning reminding you what to expect.</P>
<H3><A name="patch_data"></A>Patch Data</H3>
<P>This action is available at initialized memory addresses having a data unit with a type that
provides an encoder. It allows you to edit the current data unit by typing its string
representation. There is (currently) no content assist, and generally only primitive types and
strings are supported. <B>NOTE:</B> Strings must be enclosed in double quotes, since that is
how they are displayed in the listing. Pressing ENTER attempts to encode the data and place it
at the current address. If this fails, the error is displayed in Ghidra's status bar, and the
input fields remain on screen.</P>
<P class="providedbyplugin">Provided by: <I>Assembler</I> plugin</P>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 76 KiB

@@ -0,0 +1,384 @@
/* ###
* 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.assembler;
import java.awt.*;
import java.awt.event.*;
import java.math.BigInteger;
import docking.ActionContext;
import docking.ComponentProvider;
import docking.action.DockingAction;
import docking.widgets.fieldpanel.*;
import docking.widgets.fieldpanel.support.FieldLocation;
import ghidra.GhidraOptions;
import ghidra.app.context.ListingActionContext;
import ghidra.app.plugin.core.codebrowser.CodeViewerProvider;
import ghidra.app.util.viewer.field.ListingField;
import ghidra.app.util.viewer.listingpanel.ListingModelAdapter;
import ghidra.app.util.viewer.listingpanel.ListingPanel;
import ghidra.framework.options.ToolOptions;
import ghidra.framework.plugintool.Plugin;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.CodeUnit;
import ghidra.program.model.listing.Program;
import ghidra.program.model.mem.MemoryBlock;
import ghidra.program.util.ProgramLocation;
/**
* An abstract action for patching
*
* <p>
* This handles most of the field placement, but relies on quite a few callbacks.
*/
public abstract class AbstractPatchAction extends DockingAction {
protected static final String MENU_GROUP = "Disassembly";
protected final PluginTool tool;
private FieldPanelOverLayoutManager fieldLayoutManager;
private CodeViewerProvider codeViewerProvider;
private FieldPanel fieldPanel;
private ListingPanel listingPanel;
private Program program;
private Address address;
private final KeyListener listenerForKeys = new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.isConsumed()) {
return;
}
switch (e.getKeyCode()) {
case KeyEvent.VK_ESCAPE:
cancel();
e.consume();
break;
case KeyEvent.VK_ENTER:
accept();
e.consume();
break;
}
}
};
private final FocusListener listenerForFocusLost = new FocusAdapter() {
@Override
public void focusLost(FocusEvent e) {
cancel();
}
};
/**
* Create a new action owned by the given plugin, having the given name
*
* @param owner the plugin owning the action
* @param name the name of the action
*/
public AbstractPatchAction(Plugin owner, String name) {
super(name, owner.getName());
tool = owner.getTool();
}
/**
* Initialize the action, post construction
*/
protected void init() {
addInputFocusListener(listenerForFocusLost);
addInputKeyListener(listenerForKeys);
}
/**
* Add the given focus listener to your input field(s)
*
* <p>
* The action uses this to know when those fields have lost focus, so it can cancel the action.
*
* @param listener the listener
*/
protected abstract void addInputFocusListener(FocusListener listener);
/**
* Add the given key listener to your input field(s)
*
* <p>
* The listener handles Escape and Enter, canceling or accepting the input, respectively.
*
* @param listener the listener
*/
protected abstract void addInputKeyListener(KeyListener listener);
/**
* If needed, add your layout listeners to this action's layout manager
*
* <p>
* If there are additional components that need to move, e.g., when the panel is scrolled, then
* you need a layout listener. If this is overridden, then
* {@link #removeLayoutListeners(FieldPanelOverLayoutManager)} must also be overridden.
*
* @param fieldLayoutManager the layout manager
*/
protected void addLayoutListeners(FieldPanelOverLayoutManager fieldLayoutManager) {
// Extension point
}
/**
* Remove your layout listeners from this action's layout manager
*
* @see #addLayoutListeners(FieldPanelOverLayoutManager)
*/
protected void removeLayoutListeners(FieldPanelOverLayoutManager fieldLayoutManager) {
// Extension point
}
/**
* Set your input field(s) font to the given one
*
* <p>
* This ensures your field's font matches the listing over which it is placed.
*
* @param font the listing's base font
*/
protected abstract void setInputFont(Font font);
/**
* Get the program on which this action was invoked
*
* @return the current program
*/
protected Program getProgram() {
return program;
}
/**
* Get the address at which this action was invoked
*
* @return the current address
*/
protected Address getAddress() {
return address;
}
@Override
public void dispose() {
super.dispose();
if (fieldLayoutManager != null) {
fieldPanel.setLayout(null);
fieldLayoutManager.unregister();
removeLayoutListeners(fieldLayoutManager);
}
}
private void prepareLayout(ListingActionContext context) {
ComponentProvider contextProvider = context.getComponentProvider();
if (codeViewerProvider == contextProvider) {
return;
}
if (codeViewerProvider != null) {
fieldPanel.setLayout(null);
fieldLayoutManager.unregister();
removeLayoutListeners(fieldLayoutManager);
}
codeViewerProvider = (CodeViewerProvider) contextProvider;
listingPanel = codeViewerProvider.getListingPanel();
fieldPanel = listingPanel.getFieldPanel();
fieldLayoutManager = new FieldPanelOverLayoutManager(fieldPanel);
fieldPanel.setLayout(fieldLayoutManager);
addLayoutListeners(fieldLayoutManager);
}
/**
* Invoked when the user presses Enter
*
* <p>
* This should validate the user's input and complete the action. If the action is completed
* successfully, then call {@link #hide()}. Note that the Enter key can be ignored by doing
* nothing, since the input field(s) will remain visible. In that case, you must provide another
* mechanism for completing the action.
*/
public abstract void accept();
/**
* Hide the input field(s)
*
* <p>
* This removes any components added to the listing's field panel, usually via
* {@link #showInputs(FieldPanel)}, and returns focus to the listing. If other components were
* added elsewhere, you must override this and hide them, too.
*/
protected void hide() {
fieldPanel.removeAll();
fieldLayoutManager.layoutContainer(fieldPanel);
fieldPanel.requestFocusInWindow();
}
/**
* Cancel the current patch action
*
* <p>
* This hides the input field(s) without completing the action.
*/
public void cancel() {
hide();
}
/**
* Locate a listing field by name and address
*
* <p>
* Generally, this is used in {@link #showInputs(FieldPanel)} to find constraints suitable for
* use in {@link Container#add(Component, Object)} on the passed {@code fieldPanel}. Likely, the
* address should be obtained from {@link #getAddress()}.
*
* @param address the address for the line (row) in the listing
* @param fieldName the column name for the field in the listing
* @return if found, the field location, or null
*/
protected FieldLocation findFieldLocation(Address address, String fieldName) {
Layout layout = listingPanel.getLayout(address);
ListingModelAdapter adapter = (ListingModelAdapter) fieldPanel.getLayoutModel();
BigInteger index = adapter.getAddressIndexMap().getIndex(address);
int count = layout.getNumFields();
for (int i = 0; i < count; i++) {
ListingField field = (ListingField) layout.getField(i);
if (field.getFieldFactory().getFieldName().equals(fieldName)) {
return new FieldLocation(index, i);
}
}
return null;
}
/**
* Check if the action is applicable to the given code unit
*
* @param unit the code unit at the user's cursor
* @return true if applicable, false if not
*/
protected abstract boolean isApplicableToUnit(CodeUnit unit);
@Override
public boolean isAddToPopup(ActionContext context) {
if (!(context instanceof ListingActionContext)) {
return false;
}
ListingActionContext lac = (ListingActionContext) context;
if (!isApplicableToUnit(lac.getCodeUnit())) {
return false;
}
ComponentProvider provider = lac.getComponentProvider();
if (!(provider instanceof CodeViewerProvider)) {
return false;
}
CodeViewerProvider codeViewer = (CodeViewerProvider) provider;
if (codeViewer.isReadOnly()) {
return false;
}
Program program = lac.getProgram();
if (program == null) {
return false;
}
Address address = lac.getAddress();
if (address == null) {
return false;
}
MemoryBlock block = program.getMemory().getBlock(address);
if (block == null || !block.isInitialized()) {
return false;
}
return true;
}
/**
* Perform preparation and save any information needed later in {@link #accept()}.
*
* @param unit the code unit at the user's cursor
*/
protected abstract void prepare(CodeUnit unit);
/**
* Put the input fields in their place, show them, and place focus appropriately
*
* <p>
* Use {{@link #findFieldLocation(Address, String)} to locate fields in the listing and place
* your inputs over them.
*
* @param address the address of the user's cursor
* @param fieldPanel the currently-focused listing field panel
* @return false if inputs could not be placed and shown
*/
protected abstract boolean showInputs(FieldPanel fieldPanel);
/**
* Pre-fill the input fields and place the caret appropriately (usually at the end)
*
* @param unit the code unit at the user's cursor
*/
protected abstract void fillInputs(CodeUnit unit);
@Override
public void actionPerformed(ActionContext context) {
if (!(context instanceof ListingActionContext)) {
return;
}
ListingActionContext lac = (ListingActionContext) context;
prepareLayout(lac);
if (codeViewerProvider.isReadOnly()) {
return;
}
CodeUnit cu = lac.getCodeUnit();
if (!isApplicableToUnit(cu)) {
return;
}
ProgramLocation cur = lac.getLocation();
program = cur.getProgram();
address = cur.getAddress();
if (address == null) {
return;
}
MemoryBlock block = program.getMemory().getBlock(address);
if (block == null || !block.isInitialized()) {
return;
}
prepare(cu);
ToolOptions displayOptions = tool.getOptions(GhidraOptions.CATEGORY_BROWSER_DISPLAY);
Font font = displayOptions.getFont(GhidraOptions.OPTION_BASE_FONT, null);
if (font != null) {
setInputFont(font);
}
fieldPanel.removeAll();
if (!showInputs(fieldPanel)) {
return;
}
fillInputs(cu);
fieldLayoutManager.layoutContainer(fieldPanel);
}
}
@@ -1,403 +0,0 @@
/* ###
* 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.assembler;
import java.awt.Color;
import java.awt.Font;
import java.awt.event.*;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.Map;
import javax.swing.BorderFactory;
import javax.swing.KeyStroke;
import org.apache.commons.collections4.map.DefaultedMap;
import org.apache.commons.collections4.map.LazyMap;
import docking.*;
import docking.action.*;
import docking.widgets.autocomplete.*;
import docking.widgets.fieldpanel.*;
import docking.widgets.fieldpanel.support.FieldLocation;
import ghidra.GhidraOptions;
import ghidra.app.context.ListingActionContext;
import ghidra.app.plugin.assembler.Assembler;
import ghidra.app.plugin.assembler.Assemblers;
import ghidra.app.plugin.core.assembler.AssemblyDualTextField.*;
import ghidra.app.plugin.core.codebrowser.CodeViewerProvider;
import ghidra.app.util.viewer.field.ListingField;
import ghidra.app.util.viewer.listingpanel.ListingModelAdapter;
import ghidra.app.util.viewer.listingpanel.ListingPanel;
import ghidra.framework.options.ToolOptions;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.database.util.ProgramTransaction;
import ghidra.program.model.address.Address;
import ghidra.program.model.lang.Language;
import ghidra.program.model.listing.*;
import ghidra.program.model.mem.MemoryAccessException;
import ghidra.program.model.mem.MemoryBlock;
import ghidra.program.util.ProgramLocation;
import ghidra.util.HelpLocation;
import ghidra.util.Msg;
import ghidra.util.task.CachingSwingWorker;
import ghidra.util.task.TaskMonitor;
/**
* A context menu action to assemble an instruction at the current address
*/
public class AssembleDockingAction extends DockingAction {
private static final String ASSEMBLY_RATING = "assemblyRating";
private static final String ASSEMBLY_MESSAGE = "assemblyMessage";
private static final KeyStroke KEYBIND_ASSEMBLE = KeyStroke.getKeyStroke(KeyEvent.VK_G,
DockingUtils.CONTROL_KEY_MODIFIER_MASK | InputEvent.SHIFT_DOWN_MASK);
//private PluginTool tool;
private FieldPanelOverLayoutManager fieldLayoutManager;
private CodeViewerProvider cv;
private FieldPanel codepane;
private ListingPanel listpane;
private Map<Language, CachingSwingWorker<Assembler>> cache =
LazyMap.lazyMap(new HashMap<>(), language -> new AssemblerConstructorWorker(language));
private Map<Language, Boolean> shownWarning = DefaultedMap.defaultedMap(new HashMap<>(), false);
private final AssemblyDualTextField input = new AssemblyDualTextField();
private Program prog;
private Address addr;
private Language lang;
private Assembler assembler;
private final MyListener listener = new MyListener();
private final PluginTool tool;
// Callback to keep the autocompleter positioned under the fields
private FieldPanelOverLayoutListener autoCompleteMover = (FieldPanelOverLayoutEvent ev) -> {
TextFieldAutocompleter<AssemblyCompletion> autocompleter = input.getAutocompleter();
if (autocompleter.isCompletionListVisible()) {
autocompleter.updateDisplayLocation();
}
};
// To build the assembler in the background if it takes a while
private static class AssemblerConstructorWorker extends CachingSwingWorker<Assembler> {
private Language lang;
public AssemblerConstructorWorker(Language lang) {
super("Assemble", false);
this.lang = lang;
}
@Override
protected Assembler runInBackground(TaskMonitor monitor) {
monitor.setMessage("Constructing assembler for " + lang);
return Assemblers.getAssembler(lang);
}
}
/*
* A class for all my callbacks
*
* For autocompletion, this causes activation of an assembled instruction to actually patch the
* instruction in.
*
* For keyboard, it causes the escape key, if not already consumed by the autocompleter, to
* cancel the assembly action altogether.
*/
private class MyListener implements AutocompletionListener<AssemblyCompletion>, KeyListener {
@Override
public void completionActivated(AutocompletionEvent<AssemblyCompletion> ev) {
if (ev.getSelection() instanceof AssemblyInstruction) {
AssemblyInstruction ins = (AssemblyInstruction) ev.getSelection();
try (ProgramTransaction trans =
ProgramTransaction.open(prog, "Assemble @" + addr + ": " + input.getText())) {
assembler.patchProgram(ins.getData(), addr);
trans.commit();
cancel(); // Not really, since I've committed. Just hides the editors.
return;
}
catch (MemoryAccessException e) {
Msg.showError(assembler, input.getMnemonicField().getRootPane(), "Assemble",
"Could not patch selected instruction", e);
}
}
}
@Override
public void keyTyped(KeyEvent e) {
// Blank
}
@Override
public void keyPressed(KeyEvent e) {
if (e.isConsumed()) {
return;
}
if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
cancel();
e.consume();
}
}
@Override
public void keyReleased(KeyEvent e) {
// Blank
}
}
public AssembleDockingAction(PluginTool tool, String name, String owner) {
super(name, owner);
this.tool = tool;
String group = "Disassembly";
setPopupMenuData(new MenuData(new String[] { "Patch Instruction" }, group));
setKeyBindingData(new KeyBindingData(KEYBIND_ASSEMBLE));
setHelpLocation(new HelpLocation("AssemblerPlugin", "AssembleAction"));
// If I lose focus, cancel the assembly
input.addFocusListener(new FocusListener() {
@Override
public void focusGained(FocusEvent e) {
// Blank
}
@Override
public void focusLost(FocusEvent e) {
cancel();
}
});
input.getMnemonicField().setBorder(BorderFactory.createLineBorder(Color.RED, 2));
input.getOperandsField().setBorder(BorderFactory.createLineBorder(Color.RED, 2));
input.getAssemblyField().setBorder(BorderFactory.createLineBorder(Color.RED, 2));
input.getAutocompleter().addAutocompletionListener(listener);
input.addKeyListener(listener);
}
@Override
public void dispose() {
super.dispose();
input.dispose();
}
protected void prepareLayout(ActionContext context) {
ComponentProvider prov = context.getComponentProvider();
if (cv == prov) {
return;
}
if (cv != null) {
codepane.setLayout(null);
fieldLayoutManager.removeLayoutListener(autoCompleteMover);
}
// we are only added to the popup for a ListingActionContext that has a CodeViewerProvider
cv = (CodeViewerProvider) prov;
listpane = cv.getListingPanel();
codepane = listpane.getFieldPanel();
fieldLayoutManager = new FieldPanelOverLayoutManager(codepane);
codepane.setLayout(fieldLayoutManager);
fieldLayoutManager.addLayoutListener(autoCompleteMover);
}
/**
* Cancel the current assembly action
*/
public void cancel() {
codepane.removeAll();
//codepane.repaint();
fieldLayoutManager.layoutContainer(codepane);
codepane.requestFocusInWindow();
}
/**
* Retrieve the location in the code viewer's {@link FieldPanel} for the field at the given
* address having the given header text
*
* @param address the address
* @param fieldName the name of the field
* @return if found, the {@link FieldLocation}, otherwise {@code null}
*/
protected FieldLocation findFieldLocation(Address address, String fieldName) {
Layout layout = listpane.getLayout(address);
ListingModelAdapter adapter = (ListingModelAdapter) codepane.getLayoutModel();
BigInteger index = adapter.getAddressIndexMap().getIndex(address);
int count = layout.getNumFields();
for (int i = 0; i < count; i++) {
ListingField field = (ListingField) layout.getField(i);
if (field.getFieldFactory().getFieldName().equals(fieldName)) {
return new FieldLocation(index, i);
}
}
return null;
}
static enum AssemblyRating {
UNRATED("This processor has not been tested with the assembler." +
" If you are really lucky, the assembler will work on this language." +
" Please contact the Ghidra team if you'd like us to test, rate, and/or improve this language."),
POOR("This processor received a rating of POOR during testing." +
" Please contact the Ghidra team if you'd like to assemble for this language." +
" Until then, we DO NOT recommend trying to assemble."),
BRONZE("This processor received a rating of BRONZE during testing." +
" Please contact the Ghidra team if you'd like to assemble for this language." +
" A fair number of instruction may assemble, but we DO NOT recommend trying to assemble."),
SILVER("This processor received a rating of SILVER during testing." +
" Most instruction should work, but you will likely encounter a few errors." +
" Please contact the Ghidra team if you'd like certain instruction improved."),
GOLD("This processor received a rating of GOLD during testing." +
" You should rarely encounter an error, but please let us know if you do."),
PLATINUM("This processor received a rating of PLATINUM during testing.");
final String message;
private AssemblyRating(String message) {
this.message = message;
}
}
@Override
public void actionPerformed(ActionContext context) {
if (!(context instanceof ListingActionContext)) {
return;
}
prepareLayout(context);
if (cv.isReadOnly()) {
return;
}
ListingActionContext lac = (ListingActionContext) context;
ProgramLocation cur = lac.getLocation();
prog = cur.getProgram();
addr = cur.getAddress();
MemoryBlock block = prog.getMemory().getBlock(addr);
if (block == null || !block.isInitialized()) {
return;
}
lang = prog.getLanguage();
AssemblyRating rating =
AssemblyRating.valueOf(lang.getProperty(ASSEMBLY_RATING + ":" + lang.getLanguageID(),
AssemblyRating.UNRATED.name()));
if (AssemblyRating.PLATINUM != rating) {
String message =
lang.getProperty(ASSEMBLY_MESSAGE + ":" + lang.getLanguageID(), rating.message);
if (!shownWarning.get(lang)) {
Msg.showWarn(this, cv.getComponent(), "Assembler Rating",
"<html><body><p style='width: 300px;'>" + message + "</p></body></html>");
shownWarning.put(lang, true);
}
}
cache.get(lang).get(null);
assembler = Assemblers.getAssembler(prog);
ToolOptions displayOptions = tool.getOptions(GhidraOptions.CATEGORY_BROWSER_DISPLAY);
Font font = displayOptions.getFont(GhidraOptions.OPTION_BASE_FONT, null);
if (font != null) {
input.setFont(font);
}
input.setProgramLocation(cur);
FieldLocation locMnem = findFieldLocation(addr, "Mnemonic");
if (null == locMnem) {
Msg.showError(this, codepane, "Assemble",
"The mnemonic field must be present to assemble");
return;
}
FieldLocation locOpns = findFieldLocation(addr, "Operands");
codepane.removeAll();
if (null == locOpns) {
// There is no operands field. Use the single-box variant
codepane.add(input.getAssemblyField(), locMnem);
input.setVisible(VisibilityMode.SINGLE_VISIBLE);
}
else {
// Use the split-field variant
codepane.add(input.getMnemonicField(), locMnem);
codepane.add(input.getOperandsField(), locOpns);
input.setVisible(VisibilityMode.DUAL_VISIBLE);
}
// Set the default text, only if it's currently an instruction
CodeUnit cu = prog.getListing().getCodeUnitAt(addr);
if (cu instanceof Instruction) {
Instruction ins = (Instruction) cu;
String instr = ins.toString();
if (ins.isInDelaySlot()) {
assert instr.startsWith("_");
instr = instr.substring(1).trim();
}
input.setText(instr);
input.setCaretPosition(instr.length());
if (null == locOpns) {
input.getAssemblyField().grabFocus();
}
else if (instr.contains(" ")) {
input.getOperandsField().grabFocus();
}
else {
input.getMnemonicField().grabFocus();
}
}
else {
input.setText("");
input.setCaretPosition(0);
if (null == locOpns) {
input.getAssemblyField().grabFocus();
}
else {
input.getMnemonicField().grabFocus();
}
}
fieldLayoutManager.layoutContainer(codepane);
//JTextField opns = dual.getOperandsField();
//opns.grabFocus();
//opns.setCaretPosition(opns.getText().length());
}
@Override
public boolean isAddToPopup(ActionContext context) {
// currently only works on a listing that has a CodeViewerProvider
if (!(context instanceof ListingActionContext)) {
return false;
}
ListingActionContext lac = (ListingActionContext) context;
ComponentProvider cp = lac.getComponentProvider();
if (!(cp instanceof CodeViewerProvider)) {
return false;
}
CodeViewerProvider codeViewer = (CodeViewerProvider) cp;
if (codeViewer.isReadOnly()) {
return false;
}
Program program = lac.getProgram();
if (program == null) {
return false;
}
MemoryBlock block = program.getMemory().getBlock(lac.getAddress());
if (block == null || !block.isInitialized()) {
return false;
}
return true;
}
}
@@ -15,42 +15,46 @@
*/
package ghidra.app.plugin.core.assembler;
import docking.action.DockingAction;
import ghidra.app.CorePluginPackage;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.plugin.ProgramPlugin;
import ghidra.app.plugin.assembler.Assemblers;
import ghidra.docking.settings.Settings;
import ghidra.framework.plugintool.PluginInfo;
import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.program.model.data.DataType;
import ghidra.program.model.mem.MemBuffer;
/**
* A plugin for assembly
*
* This plugin currently provides a single action: {@link AssembleDockingAction}, which allows the
* user to assemble an instruction at the current address.
* <p>
* This plugin currently provides two actions: {@link PatchInstructionAction}, which allows the user
* to assemble an instruction at the current address; and {@link PatchDataAction}, which allows the
* user to "assemble" data at the current address.
*
* The API for assembly is available from {@link Assemblers}.
* <p>
* The API for instruction assembly is available from {@link Assemblers}. For data assembly, the API
* is in {@link DataType#encodeRepresentation(String, MemBuffer, Settings, int)}.
*/
//@formatter:off
@PluginInfo(
status = PluginStatus.RELEASED,
packageName = CorePluginPackage.NAME,
category = PluginCategoryNames.PATCHING,
shortDescription = "Assembler",
description = "This plugin provides functionality for assembly patching. " +
"The assembler supports most processor languages also supported by the " +
"disassembler. Depending on the particular processor, your mileage may vary. " +
"We are in the process of testing and improving support for all our processors. " +
"You can access the assembler by pressing Ctrl-Shift-G, and then modifying the " +
"instruction in place. As you type, a content assist will guide you and provide " +
"assembled bytes when you have a complete instruction."
)
//@formatter:on
"The assembler supports most processor languages also supported by the " +
"disassembler. Depending on the particular processor, your mileage may vary. " +
"We are in the process of testing and improving support for all our processors. " +
"You can access the assembler by pressing Ctrl-Shift-G, and then modifying the " +
"instruction in place. As you type, a content assist will guide you and provide " +
"assembled bytes when you have a complete instruction.")
public class AssemblerPlugin extends ProgramPlugin {
public static final String ASSEMBLER_NAME = "Assembler";
private DockingAction assembleAction;
/*test*/ PatchInstructionAction patchInstructionAction;
/*test*/ PatchDataAction patchDataAction;
public AssemblerPlugin(PluginTool tool) {
super(tool, false, false, false);
@@ -58,14 +62,16 @@ public class AssemblerPlugin extends ProgramPlugin {
}
private void createActions() {
assembleAction = new AssembleDockingAction(tool, "Assemble", getName());
assembleAction.setEnabled(true);
tool.addAction(assembleAction);
patchInstructionAction = new PatchInstructionAction(this, "Patch Instruction");
tool.addAction(patchInstructionAction);
patchDataAction = new PatchDataAction(this, "Patch Data");
tool.addAction(patchDataAction);
}
@Override
protected void dispose() {
assembleAction.dispose();
patchInstructionAction.dispose();
patchDataAction.dispose();
}
}
@@ -73,7 +73,7 @@ public class AssemblyDualTextField {
protected Program program;
protected Assembler assembler;
protected Address addr;
protected Address address;
protected Instruction existing;
protected boolean exhaustUndefined = false;
@@ -415,6 +415,13 @@ public class AssemblyDualTextField {
auto.dispose();
}
/**
* @see #setProgramLocation(Program, Address)
*/
public void setProgramLocation(ProgramLocation loc) {
setProgramLocation(loc.getProgram(), loc.getAddress());
}
/**
* Set the current program location
*
@@ -422,12 +429,13 @@ public class AssemblyDualTextField {
* This may cause the construction of a new assembler, if one suitable for the given program's
* language has not yet been built.
*
* @param loc the location
* @param program the program
* @param address the non-null address
*/
public void setProgramLocation(ProgramLocation loc) {
this.program = loc.getProgram();
this.addr = loc.getAddress();
this.existing = program.getListing().getInstructionAt(addr);
public void setProgramLocation(Program program, Address address) {
this.program = program;
this.address = Objects.requireNonNull(address);
this.existing = program.getListing().getInstructionAt(address);
this.assembler = Assemblers.getAssembler(program);
}
@@ -438,9 +446,9 @@ public class AssemblyDualTextField {
* @param lang the language
* @param addr the address
*/
public void setLanguageLocation(Language lang, long addr) {
public void setLanguageLocation(Language lang, Address addr) {
this.program = null;
this.addr = lang.getDefaultSpace().getAddress(addr);
this.address = addr;
this.existing = null;
this.assembler = Assemblers.getAssembler(lang);
@@ -514,7 +522,7 @@ public class AssemblyDualTextField {
/**
* Set the visibility of the text box(es)
*
* @param visibility the VisibilityMode to set.
* @param visibility the {@link VisibilityMode} to set.
*/
public void setVisible(VisibilityMode visibility) {
switch (visibility) {
@@ -533,6 +541,35 @@ public class AssemblyDualTextField {
}
}
/**
* Get the visibility of the text box(es)
*
* <p>
* <b>NOTE:</b> This method assumes nothing else changes the visibility of the text boxes. If
* anything else does, then it should be sure to maintain a configuration consistent with one of
* the {@link VisibilityMode}s.
*
* @return the current mode
*/
public VisibilityMode getVisible() {
if (linker.isVisible()) {
if (assembly.isVisible()) {
throw new AssertionError();
}
else {
return VisibilityMode.DUAL_VISIBLE;
}
}
else {
if (assembly.isVisible()) {
return VisibilityMode.SINGLE_VISIBLE;
}
else {
return VisibilityMode.INVISIBLE;
}
}
}
/**
* Set the font for all text fields
*
@@ -699,7 +736,7 @@ public class AssemblyDualTextField {
* @return the collection of completion items
*/
protected Collection<AssemblyCompletion> computeCompletions(String text) {
final AssemblyPatternBlock ctx = assembler.getContextAt(addr);
final AssemblyPatternBlock ctx = assembler.getContextAt(address);
Set<AssemblyCompletion> result = new TreeSet<>();
Collection<AssemblyParseResult> parses = assembler.parseLine(text);
@@ -720,7 +757,7 @@ public class AssemblyDualTextField {
parses = assembler.parseLine(fullText);
for (AssemblyParseResult parse : parses) {
if (!parse.isError()) {
AssemblyResolutionResults sems = assembler.resolveTree(parse, addr);
AssemblyResolutionResults sems = assembler.resolveTree(parse, address);
for (AssemblyResolution ar : sems) {
if (ar.isError()) {
//result.add(new AssemblyError("", ar.toString()));
@@ -749,7 +786,7 @@ public class AssemblyDualTextField {
public class AssemblyDualTextFieldDemo implements GhidraLaunchable {
public final LanguageID DEMO_LANG_ID = new LanguageID("x86:LE:64:default");
public final String ADDR_FORMAT = "@%08x:";
long curAddr = 0;
Address curAddr;
@Override
public void launch(GhidraApplicationLayout layout, String[] args) throws Exception {
@@ -768,6 +805,7 @@ public class AssemblyDualTextField {
SleighLanguageProvider provider = new SleighLanguageProvider();
SleighLanguage lang = (SleighLanguage) provider.getLanguage(DEMO_LANG_ID);
curAddr = lang.getDefaultSpace().getAddress(0);
input.setLanguageLocation(lang, curAddr);
@@ -788,7 +826,7 @@ public class AssemblyDualTextField {
String data = NumericUtilities.convertBytesToString(ins.getData());
asm.setText(asm.getText() + data);
input.clear();
curAddr += ins.getData().length;
curAddr = curAddr.addWrap(ins.getData().length);
input.setLanguageLocation(lang, curAddr);
addrlabel.setText(String.format(ADDR_FORMAT, curAddr));
}
@@ -0,0 +1,153 @@
/* ###
* 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.assembler;
import java.awt.Color;
import java.awt.Font;
import java.awt.event.FocusListener;
import java.awt.event.KeyListener;
import javax.swing.*;
import docking.action.KeyBindingData;
import docking.action.MenuData;
import docking.widgets.fieldpanel.FieldPanel;
import docking.widgets.fieldpanel.support.FieldLocation;
import ghidra.framework.plugintool.Plugin;
import ghidra.program.database.util.ProgramTransaction;
import ghidra.program.model.address.*;
import ghidra.program.model.data.*;
import ghidra.program.model.listing.*;
import ghidra.program.model.mem.MemoryAccessException;
import ghidra.program.model.util.CodeUnitInsertionException;
import ghidra.util.HelpLocation;
import ghidra.util.Msg;
/**
* A context menu action to patch data at the current address
*/
public class PatchDataAction extends AbstractPatchAction {
private static final KeyStroke KEYBIND_PATCH_DATA = KeyStroke.getKeyStroke("ctrl shift H");
/*test*/ final JTextField input = new JTextField();
private Data data;
public PatchDataAction(Plugin owner, String name) {
super(owner, name);
setPopupMenuData(new MenuData(new String[] { "Patch Data" }, MENU_GROUP));
setKeyBindingData(new KeyBindingData(KEYBIND_PATCH_DATA));
setHelpLocation(new HelpLocation(owner.getName(), "patch_data"));
input.setBorder(BorderFactory.createLineBorder(Color.RED, 2));
init();
}
@Override
protected void addInputFocusListener(FocusListener listener) {
input.addFocusListener(listener);
}
@Override
protected void addInputKeyListener(KeyListener listener) {
input.addKeyListener(listener);
}
@Override
protected boolean isApplicableToUnit(CodeUnit cu) {
if (!(cu instanceof Data)) {
return false;
}
Data data = (Data) cu;
if (!data.getBaseDataType().isEncodable()) {
return false;
}
return true;
}
@Override
protected void prepare(CodeUnit unit) {
data = (Data) unit;
}
@Override
protected void setInputFont(Font font) {
input.setFont(font);
}
@Override
protected boolean showInputs(FieldPanel fieldPanel) {
FieldLocation locOpns = findFieldLocation(getAddress(), "Operands");
if (locOpns == null) {
Msg.showError(this, fieldPanel, getName(),
"The Operands field must be present to patch data");
return false;
}
fieldPanel.add(input, locOpns);
input.setVisible(true);
input.grabFocus();
return true;
}
@Override
protected void fillInputs(CodeUnit unit) {
String repr = data.getDefaultValueRepresentation();
input.setText(repr);
input.setCaretPosition(repr.length());
}
@Override
public void accept() {
Program program = getProgram();
Address address = getAddress();
DataType dt = data.getBaseDataType();
/**
* Do as much outside the transaction as possible. The tool tends to steal focus away upon
* restoring the database, and that causes the input fields to disappear.
*/
byte[] encoded;
AddressRange rng;
try {
encoded = dt.encodeRepresentation(input.getText(), data, data, data.getLength());
rng = new AddressRangeImpl(address, encoded.length);
}
catch (DataTypeEncodeException | AddressOverflowException e) {
tool.setStatusInfo(e.getMessage(), true);
return;
}
try (ProgramTransaction trans =
ProgramTransaction.open(program, "Patch Data @" + address + ": " + input.getText())) {
int oldLength = data.getLength();
if (encoded.length != oldLength) {
program.getListing().clearCodeUnits(address, rng.getMaxAddress(), false);
}
program.getMemory().setBytes(address, encoded);
if (encoded.length != oldLength) {
program.getListing().createData(address, dt, encoded.length);
}
trans.commit();
hide();
}
catch (MemoryAccessException e) {
Msg.showError(this, null, "Patch Failure", e.getMessage(), e);
}
catch (CodeUnitInsertionException | DataTypeConflictException e) {
throw new AssertionError(); // Should have been cleared first
}
}
}
@@ -0,0 +1,290 @@
/* ###
* 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.assembler;
import java.awt.Color;
import java.awt.Font;
import java.awt.event.FocusListener;
import java.awt.event.KeyListener;
import java.util.HashMap;
import java.util.Map;
import javax.swing.BorderFactory;
import javax.swing.KeyStroke;
import org.apache.commons.collections4.map.DefaultedMap;
import org.apache.commons.collections4.map.LazyMap;
import docking.action.KeyBindingData;
import docking.action.MenuData;
import docking.widgets.autocomplete.*;
import docking.widgets.fieldpanel.*;
import docking.widgets.fieldpanel.support.FieldLocation;
import ghidra.app.plugin.assembler.Assembler;
import ghidra.app.plugin.assembler.Assemblers;
import ghidra.app.plugin.core.assembler.AssemblyDualTextField.*;
import ghidra.framework.plugintool.Plugin;
import ghidra.program.database.util.ProgramTransaction;
import ghidra.program.model.address.Address;
import ghidra.program.model.lang.Language;
import ghidra.program.model.listing.*;
import ghidra.program.model.mem.MemoryAccessException;
import ghidra.util.HelpLocation;
import ghidra.util.Msg;
import ghidra.util.task.CachingSwingWorker;
import ghidra.util.task.TaskMonitor;
/**
* A context menu action to assemble an instruction at the current address
*/
public class PatchInstructionAction extends AbstractPatchAction {
/**
* Enumerated quality ratings and text to describe them.
*/
static enum AssemblyRating {
UNRATED("This processor has not been tested with the assembler." +
" The assembler will probably work on this language."),
POOR("This processor received a rating of POOR during testing." +
" We DO NOT recommend trying to assemble."),
BRONZE("This processor received a rating of BRONZE during testing." +
" A fair number of instructions may assemble, but we DO NOT recommend trying to" +
" assemble."),
SILVER("This processor received a rating of SILVER during testing." +
" Most instructions should work, but you will likely encounter a few errors."),
GOLD("This processor received a rating of GOLD during testing." +
" You should rarely encounter an error."),
PLATINUM("This processor received a rating of PLATINUM during testing.");
final String message;
private AssemblyRating(String message) {
this.message = message;
}
}
// To build the assembler in the background if it takes a while
private static class AssemblerConstructorWorker extends CachingSwingWorker<Assembler> {
private Language language;
public AssemblerConstructorWorker(Language language) {
super("Assemble", false);
this.language = language;
}
@Override
protected Assembler runInBackground(TaskMonitor monitor) {
monitor.setMessage("Constructing assembler for " + language);
return Assemblers.getAssembler(language);
}
}
private static final String ASSEMBLY_RATING = "assemblyRating";
private static final String ASSEMBLY_MESSAGE = "assemblyMessage";
private static final KeyStroke KEYBIND_PATCH_INSTRUCTION =
KeyStroke.getKeyStroke("ctrl shift G");
/**
* A listener for activation of a completion item
*
* <p>
* The {@link AbstractPatchAction#accept()} method does not suffice for this action, since one
* of the suggested byte sequences must be selected, as presented by the completer. Thus, we'll
* nop that method, and instead call our own acceptance logic from here.
*/
private class ListenerForAccept implements AutocompletionListener<AssemblyCompletion> {
@Override
public void completionActivated(AutocompletionEvent<AssemblyCompletion> ev) {
if (ev.getSelection() instanceof AssemblyInstruction) {
AssemblyInstruction ins = (AssemblyInstruction) ev.getSelection();
accept(ins);
}
}
}
private final Map<Language, CachingSwingWorker<Assembler>> cache =
LazyMap.lazyMap(new HashMap<>(), language -> new AssemblerConstructorWorker(language));
/*test*/ final Map<Language, Boolean> shownWarning =
DefaultedMap.defaultedMap(new HashMap<>(), false);
/*test*/ final AssemblyDualTextField input = new AssemblyDualTextField();
private final ListenerForAccept listenerForAccept = new ListenerForAccept();
private Language language;
private Assembler assembler;
// Callback to keep the autocompleter positioned under the fields
private FieldPanelOverLayoutListener listenerToMoveAutocompleter = ev -> {
TextFieldAutocompleter<AssemblyCompletion> autocompleter = input.getAutocompleter();
if (autocompleter.isCompletionListVisible()) {
autocompleter.updateDisplayLocation();
}
};
public PatchInstructionAction(Plugin owner, String name) {
super(owner, name);
setPopupMenuData(new MenuData(new String[] { "Patch Instruction" }, MENU_GROUP));
setKeyBindingData(new KeyBindingData(KEYBIND_PATCH_INSTRUCTION));
setHelpLocation(new HelpLocation(owner.getName(), "patch_instruction"));
input.getMnemonicField().setBorder(BorderFactory.createLineBorder(Color.RED, 2));
input.getOperandsField().setBorder(BorderFactory.createLineBorder(Color.RED, 2));
input.getAssemblyField().setBorder(BorderFactory.createLineBorder(Color.RED, 2));
input.getAutocompleter().addAutocompletionListener(listenerForAccept);
init();
}
@Override
public void dispose() {
super.dispose();
input.dispose();
}
@Override
protected void addInputFocusListener(FocusListener listener) {
input.addFocusListener(listener);
}
@Override
protected void addInputKeyListener(KeyListener listener) {
input.addKeyListener(listener);
}
@Override
protected void addLayoutListeners(FieldPanelOverLayoutManager fieldLayoutManager) {
fieldLayoutManager.addLayoutListener(listenerToMoveAutocompleter);
}
@Override
protected void removeLayoutListeners(FieldPanelOverLayoutManager fieldLayoutManager) {
fieldLayoutManager.removeLayoutListener(listenerToMoveAutocompleter);
}
@Override
protected boolean isApplicableToUnit(CodeUnit cu) {
return true;
}
@Override
protected void prepare(CodeUnit cu) {
language = cu.getProgram().getLanguage();
AssemblyRating rating = AssemblyRating.valueOf(
language.getProperty(ASSEMBLY_RATING + ":" + language.getLanguageID(),
AssemblyRating.UNRATED.name()));
if (AssemblyRating.PLATINUM != rating) {
String message = language.getProperty(ASSEMBLY_MESSAGE + ":" + language.getLanguageID(),
rating.message);
if (!shownWarning.get(language)) {
Msg.showWarn(this, null, "Assembler Rating",
"<html><body><p style='width: 300px;'>" + message + "</p></body></html>");
shownWarning.put(language, true);
}
}
cache.get(language).get(null);
assembler = Assemblers.getAssembler(cu.getProgram());
}
@Override
protected void setInputFont(Font font) {
input.setFont(font);
}
@Override
protected boolean showInputs(FieldPanel fieldPanel) {
input.setProgramLocation(getProgram(), getAddress());
FieldLocation locMnem = findFieldLocation(getAddress(), "Mnemonic");
if (locMnem == null) {
Msg.showError(this, fieldPanel, getName(),
"The Mnemonic field must be present to patch instruction");
return false;
}
FieldLocation locOpns = findFieldLocation(getAddress(), "Operands");
if (locOpns == null) {
// Use the single-box variant
fieldPanel.add(input.getAssemblyField(), locMnem);
input.setVisible(VisibilityMode.SINGLE_VISIBLE);
}
else {
// Use the split variant
fieldPanel.add(input.getMnemonicField(), locMnem);
fieldPanel.add(input.getOperandsField(), locOpns);
input.setVisible(VisibilityMode.DUAL_VISIBLE);
}
return true;
}
@Override
protected void fillInputs(CodeUnit unit) {
if (unit instanceof Instruction) {
Instruction ins = (Instruction) unit;
String instr = ins.toString();
if (ins.isInDelaySlot()) {
assert instr.startsWith("_");
instr = instr.substring(1).trim();
}
input.setText(instr);
input.setCaretPosition(instr.length());
if (input.getVisible() == VisibilityMode.SINGLE_VISIBLE) {
input.getAssemblyField().grabFocus();
}
else if (instr.contains(" ")) {
input.getOperandsField().grabFocus();
}
else {
input.getMnemonicField().grabFocus();
}
}
else {
input.setText("");
input.setCaretPosition(0);
if (input.getVisible() == VisibilityMode.SINGLE_VISIBLE) {
input.getAssemblyField().grabFocus();
}
else {
input.getMnemonicField().grabFocus();
}
}
}
@Override
public void accept() {
// Do nothing. User must select a completion item instead
}
/**
* Accept the given instruction selected by the user
*
* @param ins the selected instruction from the completion list
*/
public void accept(AssemblyInstruction ins) {
Program program = getProgram();
Address address = getAddress();
try (ProgramTransaction trans =
ProgramTransaction.open(program, "Assemble @" + address + ": " + input.getText())) {
assembler.patchProgram(ins.getData(), address);
trans.commit();
hide();
}
catch (MemoryAccessException e) {
Msg.showError(this, null, "Assemble", "Could not patch selected instruction", e);
}
}
}
@@ -0,0 +1,258 @@
/* ###
* 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.assembler;
import static org.junit.Assert.*;
import java.util.List;
import java.util.Objects;
import javax.swing.JTextField;
import org.junit.*;
import ghidra.app.plugin.assembler.Assembler;
import ghidra.app.plugin.assembler.Assemblers;
import ghidra.app.plugin.core.assembler.AssemblyDualTextField.AssemblyCompletion;
import ghidra.app.plugin.core.assembler.AssemblyDualTextField.AssemblyInstruction;
import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin;
import ghidra.app.plugin.core.codebrowser.CodeViewerProvider;
import ghidra.app.plugin.core.progmgr.ProgramManagerPlugin;
import ghidra.docking.settings.FormatSettingsDefinition;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.database.ProgramDB;
import ghidra.program.database.util.ProgramTransaction;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressSpace;
import ghidra.program.model.data.ShortDataType;
import ghidra.program.model.data.TerminatedStringDataType;
import ghidra.program.model.listing.*;
import ghidra.program.model.mem.Memory;
import ghidra.program.util.ProgramLocation;
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
import ghidra.test.TestEnv;
import ghidra.util.task.TaskMonitor;
public class AssemblerPluginTest extends AbstractGhidraHeadedIntegrationTest {
protected TestEnv env;
protected PluginTool tool;
private ProgramManagerPlugin programManager;
private AssemblerPlugin assemblerPlugin;
private CodeViewerProvider codeViewer;
private AssemblyDualTextField instructionInput;
private JTextField dataInput;
private ProgramDB program;
private AddressSpace space;
private Memory memory;
private Listing listing;
@Before
public void setUpAssemblerPluginTest() throws Exception {
env = new TestEnv();
tool = env.getTool();
programManager = addPlugin(tool, ProgramManagerPlugin.class);
addPlugin(tool, CodeBrowserPlugin.class);
assemblerPlugin = addPlugin(tool, AssemblerPlugin.class);
codeViewer = waitForComponentProvider(CodeViewerProvider.class);
instructionInput = assemblerPlugin.patchInstructionAction.input;
dataInput = assemblerPlugin.patchDataAction.input;
program = createDefaultProgram(getName(), "Toy:BE:64:default", this);
space = program.getAddressFactory().getDefaultAddressSpace();
memory = program.getMemory();
listing = program.getListing();
try (ProgramTransaction trans = ProgramTransaction.open(program, "Setup")) {
memory.createInitializedBlock(".text", space.getAddress(0x00400000), 0x1000, (byte) 0,
TaskMonitor.DUMMY, false);
trans.commit();
}
// Snuff the assembler's warning prompt
assemblerPlugin.patchInstructionAction.shownWarning.put(program.getLanguage(), true);
env.showTool();
programManager.openProgram(program);
}
@After
public void tearDownAssemblerPluginTest() {
if (program != null) {
program.release(this);
}
}
protected void assertDualFields() {
assertFalse(instructionInput.getAssemblyField().isVisible());
assertTrue(instructionInput.getMnemonicField().isVisible());
assertTrue(instructionInput.getOperandsField().isVisible());
}
protected List<AssemblyCompletion> inputAndGetCompletions(String text) {
return runSwing(() -> {
instructionInput.setText(text);
instructionInput.auto.startCompletion(instructionInput.getOperandsField());
instructionInput.auto.flushUpdates();
return instructionInput.auto.getSuggestions();
});
}
@Test
public void testActionPatchInstructionNoExisting() throws Exception {
Address address = space.getAddress(0x00400000);
codeViewer.goTo(program, new ProgramLocation(program, address));
waitForSwing();
performAction(assemblerPlugin.patchInstructionAction, codeViewer, true);
assertDualFields();
assertEquals("", instructionInput.getText());
assertEquals(address, assemblerPlugin.patchInstructionAction.getAddress());
List<AssemblyCompletion> completions = inputAndGetCompletions("imm r0, #1234");
AssemblyCompletion first = completions.get(0);
assertTrue(first instanceof AssemblyInstruction);
AssemblyInstruction ai = (AssemblyInstruction) first;
runSwing(() -> assemblerPlugin.patchInstructionAction.accept(ai));
waitForProgram(program);
Instruction ins = Objects.requireNonNull(listing.getInstructionAt(address));
assertEquals("imm r0,#0x4d2", ins.toString());
}
@Test
public void testActionPatchInstructionExisting() throws Exception {
Address address = space.getAddress(0x00400000);
Assembler asm = Assemblers.getAssembler(program);
try (ProgramTransaction trans = ProgramTransaction.open(program, "Assemble pre-existing")) {
asm.assemble(address, "imm r0,#0x4d2");
trans.commit();
}
codeViewer.goTo(program, new ProgramLocation(program, address));
waitForSwing();
performAction(assemblerPlugin.patchInstructionAction, codeViewer, true);
assertDualFields();
assertEquals("imm r0,#0x4d2", instructionInput.getText());
assertEquals(address, assemblerPlugin.patchInstructionAction.getAddress());
List<AssemblyCompletion> completions = inputAndGetCompletions("imm r0, #123");
AssemblyCompletion first = completions.get(0);
assertTrue(first instanceof AssemblyInstruction);
AssemblyInstruction ai = (AssemblyInstruction) first;
runSwing(() -> assemblerPlugin.patchInstructionAction.accept(ai));
waitForProgram(program);
Instruction ins = Objects.requireNonNull(listing.getInstructionAt(address));
assertEquals("imm r0,#0x7b", ins.toString());
}
// TODO: Test disabled on uninitialized memory
// TODO: Test disabled on read-only listings
protected Data doPatchAt(Address address, String expText, String newText) {
codeViewer.goTo(program, new ProgramLocation(program, address));
waitForSwing();
performAction(assemblerPlugin.patchDataAction, codeViewer, true);
assertTrue(dataInput.isVisible());
assertEquals(expText, dataInput.getText());
assertEquals(address, assemblerPlugin.patchDataAction.getAddress());
runSwing(() -> {
dataInput.setText(newText);
assemblerPlugin.patchDataAction.accept();
});
waitForProgram(program);
return Objects.requireNonNull(listing.getDataAt(address));
}
@Test
public void testActionPatchDataShortHexValid() throws Exception {
Address address = space.getAddress(0x00400000);
try (ProgramTransaction trans = ProgramTransaction.open(program, "Place short")) {
listing.createData(address, ShortDataType.dataType);
trans.commit();
}
Data data = doPatchAt(address, "0h", "1234h");
assertEquals("1234h", data.getDefaultValueRepresentation());
}
@Test
public void testActionPatchDataShortDecValid() throws Exception {
Address address = space.getAddress(0x00400000);
try (ProgramTransaction trans = ProgramTransaction.open(program, "Place short")) {
Data data = listing.createData(address, ShortDataType.dataType);
FormatSettingsDefinition.DEF.setChoice(data, FormatSettingsDefinition.DECIMAL);
trans.commit();
}
Data data = doPatchAt(address, "0", "1234");
assertEquals("1234", data.getDefaultValueRepresentation());
}
@Test
public void testActionPatchDataUTF8StringSameLength() throws Exception {
Address address = space.getAddress(0x00400000);
try (ProgramTransaction trans = ProgramTransaction.open(program, "Place string")) {
memory.setBytes(address, "Hello, World!\0".getBytes("utf-8"));
listing.createData(address, TerminatedStringDataType.dataType);
trans.commit();
}
Data data = doPatchAt(address, "\"Hello, World!\"", "\"Hello, Patch!\"");
assertEquals("\"Hello, Patch!\"", data.getDefaultValueRepresentation());
}
@Test
public void testActionPatchDataUTF8StringShorter() throws Exception {
Address address = space.getAddress(0x00400000);
try (ProgramTransaction trans = ProgramTransaction.open(program, "Place string")) {
memory.setBytes(address, "Hello, World!\0".getBytes("utf-8"));
listing.createData(address, TerminatedStringDataType.dataType);
trans.commit();
}
Data data = doPatchAt(address, "\"Hello, World!\"", "\"Hello!\"");
assertEquals("\"Hello!\"", data.getDefaultValueRepresentation());
assertEquals(7, data.getLength());
}
@Test
public void testActionPatchDataUTF8StringLonger() throws Exception {
Address address = space.getAddress(0x00400000);
try (ProgramTransaction trans = ProgramTransaction.open(program, "Place string")) {
memory.setBytes(address, "Hello, World!\0".getBytes("utf-8"));
listing.createData(address, TerminatedStringDataType.dataType);
trans.commit();
}
Data data = doPatchAt(address, "\"Hello, World!\"", "\"Hello to you, too!\"");
assertEquals("\"Hello to you, too!\"", data.getDefaultValueRepresentation());
assertEquals(19, data.getLength());
}
}
@@ -19,6 +19,8 @@ import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javax.swing.*;
import javax.swing.border.BevelBorder;
@@ -35,55 +37,59 @@ import ghidra.util.task.SwingUpdateManager;
/**
* An autocompleter that may be attached to one or more {@link JTextField}.
*
* <p>
* Each autocompleter instance has one associated window (displaying the list of suggestions) and
* one associated model (generating the list of suggestions). Thus, the list can only be active on
* one of the attached text fields at a time. This is usually the desired behavior, and it allows
* for one autocompleter to be reused on many fields. Behavior is undefined when multiple
* autocompleters are attached to the same text field. More likely, you should implement a
* composite model if you wish to present completions from multiple models on a single text field.
* autocompleters are attached to the same text field. More likely, you should implement a composite
* model if you wish to present completions from multiple models on a single text field.
*
* <p>
* By default, the autocompleter is activated when the user presses CTRL-SPACE, at which point, the
* model is queried for possible suggestions. The completer gives the model all the text preceding
* the current field's caret. This behavior can be changed by overriding the
* {@link #getPrefix(JTextField)} method. This may be useful, e.g., to obtain a prefix for
* the current word, rather than the full field contents, preceding the caret. The list is
* displayed such that its top-left corner is placed directly under the current field's caret. As
* the user continues typing, the suggestions are re-computed, and the list tracks with the caret.
* This positioning behavior can be modified by overriding the
* {@link #getCompletionWindowPosition()} method. As a convenience, the
* {@link #getCaretPositionOnScreen(JTextField)} method is available to compute the default
* position.
* {@link #getPrefix(JTextField)} method. This may be useful, e.g., to obtain a prefix for the
* current word, rather than the full field contents, preceding the caret. The list is displayed
* such that its top-left corner is placed directly under the current field's caret. As the user
* continues typing, the suggestions are re-computed, and the list tracks with the caret. This
* positioning behavior can be modified by overriding the {@link #getCompletionWindowPosition()}
* method. As a convenience, the {@link #getCaretPositionOnScreen(JTextField)} method is available
* to compute the default position.
*
* <p>
* Whether or not the list is currently displayed, when the user presses CTRL-SPACE, if only one
* completion is possible, it is automatically activated. This logic is applied again and again,
* until either no suggestions are given, or more than one suggestion is given (or until the
* autocompleter detects an infinite loop). This behavior can by modified on an item-by-item basis
* by overriding the {@link #getCompletionCanDefault(Object) getCompletionCanDefault(T)} method. This same behavior can be
* activated by calling the {@link #startCompletion(JTextField)} method, which may be useful, e.g.,
* to bind a different key sequence to start autocompletion.
* by overriding the {@link #getCompletionCanDefault(Object) getCompletionCanDefault(T)} method.
* This same behavior can be activated by calling the {@link #startCompletion(JTextField)} method,
* which may be useful, e.g., to bind a different key sequence to start autocompletion.
*
* <p>
* The appearance of each item in the suggestion list can be modified by overriding the various
* {@code getCompletion...} methods. Note that it's possible for an item to be displayed one way,
* but cause the insertion of different text. In any case, it is best to ensure any modification
* produces an intuitive behavior.
*
* The simplest use case is to create a text field, create an autocompleter with a custom model,
* and then attach and show.
* <p>
* The simplest use case is to create a text field, create an autocompleter with a custom model, and
* then attach and show.
*
*
* <pre>
* JTextField field = new JTextField();
*
* {@code AutocompletionModel<String> model = new AutocompletionModel<String>() }{
* AutocompletionModel<String> model = new AutocompletionModel<String>() {
* &#64;Override
* {@code public Collection<String> computeCompletions(String text)} {
* ... // Populate the completion list based on the given prefix.
* public Collection<String> computeCompletions(String text) {
* // ... Populate the completion list based on the given prefix.
* }
* }
* {@code TextFieldAutocompleter<String> completer = new TextFieldAutocompleter<String>(model);
* TextFieldAutocompleter<String> completer = new TextFieldAutocompleter<String>(model);
* completer.attachTo(field);
* ... // Add the field to, e.g., a dialog, and show.
* }</pre>
* // ... Add the field to, e.g., a dialog, and show.
*
* </pre>
*
* @param <T> the type of suggestions presented by this autocompleter.
*/
@@ -261,6 +267,7 @@ public class TextFieldAutocompleter<T> {
/**
* Create a new autocompleter associated with the given model.
*
* @param model the model giving the suggestions.
*/
public TextFieldAutocompleter(AutocompletionModel<T> model) {
@@ -270,6 +277,7 @@ public class TextFieldAutocompleter<T> {
/**
* Recompute the display location and move with list window.
*
* <p>
* This is useful, e.g., when the window containing the associated text field(s) moves.
*/
public void updateDisplayLocation() {
@@ -281,6 +289,7 @@ public class TextFieldAutocompleter<T> {
/**
* Update the contents of the suggestion list.
*
* <p>
* This entails taking the prefix, querying the model, and rendering the list.
*/
protected void updateDisplayContents() {
@@ -288,7 +297,7 @@ public class TextFieldAutocompleter<T> {
updateManager.updateLater();
}
/*
/**
* The actual implementation of updateDisplayContents, which gets scheduled asynchronously.
*/
private void doUpdateDisplayContents() {
@@ -343,6 +352,7 @@ public class TextFieldAutocompleter<T> {
/**
* Show or hide the completion list window
*
* @param visible true to show, false to hide
*/
public void setCompletionListVisible(boolean visible) {
@@ -364,7 +374,9 @@ public class TextFieldAutocompleter<T> {
/**
* Check if the completion list window is visible.
*
* <p>
* If it is visible, this implies that the user is actively using the autocompleter.
*
* @return true if shown, false if hidden.
*/
public boolean isCompletionListVisible() {
@@ -397,11 +409,12 @@ public class TextFieldAutocompleter<T> {
/**
* Get the preferred location (on screen) of the completion list window.
*
* Typically, this is a location near the focused field. Ideally, it is positioned such that
* the displayed suggestions coincide with the applicable text in the focused field. For
* example, if the suggestions display some portion of the prefix, the window could be
* positioned such that the portion in the suggestion appears directly below the same portion
* in the field.
* <p>
* Typically, this is a location near the focused field. Ideally, it is positioned such that the
* displayed suggestions coincide with the applicable text in the focused field. For example, if
* the suggestions display some portion of the prefix, the window could be positioned such that
* the portion in the suggestion appears directly below the same portion in the field.
*
* @return the point giving the top-left corner of the completion window
*/
protected Point getCompletionWindowPosition() {
@@ -411,17 +424,18 @@ public class TextFieldAutocompleter<T> {
/**
* Get the preferred dimensions of the completion list window.
*
* <p>
* Typically, this is the width of the focused field.
* @return the dimension giving the preferred height and width. A value can be -1 to indicate
* no preference.
*
* @return the dimension giving the preferred height and width. A value can be -1 to indicate no
* preference.
*/
protected Dimension getDefaultCompletionWindowDimension() {
return new Dimension(focus.getWidth(), -1);
}
/**
* A convenience function that returns the bottom on-screen position of the given field's
* caret.
* A convenience function that returns the bottom on-screen position of the given field's caret.
*
* @param field the field, typically the one having focus
* @return the on-screen position of the caret's bottom.
@@ -444,10 +458,13 @@ public class TextFieldAutocompleter<T> {
/**
* Builds the list cell renderer for the autocompletion list.
*
* <p>
* A programmer may override this if the various {@code getCompletion...} methods prove
* insufficient for customizing the display of the suggestions. Please remember that
* {@link JLabel}s can render HTML, so {@link #getCompletionDisplay(Object) getCompletionDisplay(T)} is quite powerful
* with the default {@link AutocompletionCellRenderer}.
* {@link JLabel}s can render HTML, so {@link #getCompletionDisplay(Object)
* getCompletionDisplay(T)} is quite powerful with the default
* {@link AutocompletionCellRenderer}.
*
* @return a list cell renderer for the completion list.
*/
protected ListCellRenderer<? super T> buildListCellRenderer() {
@@ -457,7 +474,9 @@ public class TextFieldAutocompleter<T> {
/**
* Attach the autocompleter to the given text field.
*
* <p>
* If this method is never called, then the autocompleter can never appear.
*
* @param field the field that will gain this autocompletion feature
* @return true, if this field is not already attached
*/
@@ -502,6 +521,7 @@ public class TextFieldAutocompleter<T> {
/**
* Cause the currently-selected suggestion to be activated.
*
* <p>
* By default, this is called when the user presses ENTER or clicks a suggestion.
*/
protected void activateCurrentCompletion() {
@@ -516,6 +536,7 @@ public class TextFieldAutocompleter<T> {
/**
* Fire the registered autocompletion listeners on the given event.
*
* <p>
* Each registered listener is invoked in order of registration. If any listener consumes the
* event, then later-registered listeners will not be notified of the event. If any listener
* cancels the event, then the suggested text will not be inserted.
@@ -539,8 +560,9 @@ public class TextFieldAutocompleter<T> {
return;
}
try {
focus.getDocument().insertString(focus.getCaretPosition(), getCompletionText(sel),
null);
focus.getDocument()
.insertString(focus.getCaretPosition(), getCompletionText(sel),
null);
}
catch (BadLocationException e) {
throw new AssertionError("INTERNAL: Should not be here", e);
@@ -549,6 +571,7 @@ public class TextFieldAutocompleter<T> {
/**
* Register the given auto-completion listener
*
* @param l the listener to register
*/
public void addAutocompletionListener(AutocompletionListener<T> l) {
@@ -557,6 +580,7 @@ public class TextFieldAutocompleter<T> {
/**
* Unregister the given auto-completion listener
*
* @param l the listener to unregister
*/
public void removeAutocompletionListener(AutocompletionListener<T> l) {
@@ -565,6 +589,7 @@ public class TextFieldAutocompleter<T> {
/**
* Get all the registered auto-completion listeners
*
* @return an array of registered listeners
*/
@SuppressWarnings("unchecked")
@@ -576,7 +601,7 @@ public class TextFieldAutocompleter<T> {
* Get all registered listeners of the given type
*
* @param listenerType the type of listeners to get
* @return an array of registered listeners
* @return an array of registered listeners
*/
@SuppressWarnings({ "unchecked", "hiding" })
public <T> T[] getListeners(Class<T> listenerType) {
@@ -660,12 +685,13 @@ public class TextFieldAutocompleter<T> {
/**
* Decide whether the given suggestion can be automatically activated.
*
* <p>
* When autocompletion is started (via {@link #startCompletion(JTextField)}) or when the user
* presses CTRL-SPACE, if there is only a single suggestion, it is taken automatically, and the
* process repeats until there is not a sole suggestion. Before the suggestion is taken,
* though, it calls this method. If it returns false, the single suggestion is displayed in a
* 1-long list instead. This is useful to prevent consequential actions from being
* automatically activated by the autocompleter.
* process repeats until there is not a sole suggestion. Before the suggestion is taken, though,
* it calls this method. If it returns false, the single suggestion is displayed in a 1-long
* list instead. This is useful to prevent consequential actions from being automatically
* activated by the autocompleter.
*
* @param sel the potentially auto-activated suggestion.
* @return true to permit auto-activation, false to prevent it.
@@ -677,13 +703,16 @@ public class TextFieldAutocompleter<T> {
/**
* Starts the autocompleter on the given text field.
*
* <p>
* First, this repeatedly attempts auto-activation. When there are many suggestions, or when
* auto-activation is prevented (see {@link #getCompletionCanDefault(Object) getCompletionCanDefault(T)}), a list is displayed
* (usually below the caret) containing the suggestions given the fields current contents. The
* list remains open until either the user cancels it (usually via ESC) or the user activates
* a suggestion.
* auto-activation is prevented (see {@link #getCompletionCanDefault(Object)
* getCompletionCanDefault(T)}), a list is displayed (usually below the caret) containing the
* suggestions given the fields current contents. The list remains open until either the user
* cancels it (usually via ESC) or the user activates a suggestion.
*
* <p>
* <b>NOTE:</b> The text field must already be attached.
*
* NOTE: The text field must already be attached.
* @param field the field on which to start autocompletion.
*/
public void startCompletion(JTextField field) {
@@ -719,11 +748,19 @@ public class TextFieldAutocompleter<T> {
}
}
/**
* If a completion list update is pending, run it immediately
*/
public void flushUpdates() {
updateManager.flush();
}
/**
* Cause the suggestion at the given index to be selected
*
* @param index the index of the selection
*/
protected void select(int index) {
public void select(int index) {
list.setSelectedIndex(index);
list.ensureIndexIsVisible(index);
}
@@ -731,7 +768,7 @@ public class TextFieldAutocompleter<T> {
/**
* Cause the next suggestion to be selected, wrapping if applicable
*/
protected void selectNext() {
public void selectNext() {
int index = list.getSelectedIndex();
int size = listModel.getSize();
@@ -745,7 +782,7 @@ public class TextFieldAutocompleter<T> {
/**
* Cause the previous suggestion to be selected, wrapping if applicable
*/
protected void selectPrev() {
public void selectPrev() {
int index = list.getSelectedIndex();
int size = listModel.getSize();
if (index >= 0) {
@@ -794,17 +831,28 @@ public class TextFieldAutocompleter<T> {
/**
* Select the first suggestion
*/
protected void selectFirst() {
public void selectFirst() {
select(0);
}
/**
* Select the last suggestion
*/
protected void selectLast() {
public void selectLast() {
select(listModel.getSize() - 1);
}
/**
* Get the list of suggestions as ordered on screen
*
* @return an immutable copy of the list
*/
public List<T> getSuggestions() {
return IntStream.range(0, listModel.getSize())
.mapToObj(listModel::get)
.collect(Collectors.toUnmodifiableList());
}
/**
* A listener to handle all the callbacks
*/
@@ -864,7 +912,7 @@ public class TextFieldAutocompleter<T> {
}
}
else if (e.getKeyCode() == KeyEvent.VK_SPACE &&
(e.getModifiers() & InputEvent.CTRL_MASK) != 0) {
(e.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0) {
startCompletion((JTextField) e.getComponent());
e.consume();
}
@@ -953,8 +1001,9 @@ public class TextFieldAutocompleter<T> {
/**
* A demonstration of the autocompleter on a single text field.
*
* The autocompleter offers the tails from a list of strings that start with the text before
* the caret.
* <p>
* The autocompleter offers the tails from a list of strings that start with the text before the
* caret.
*/
public static class TextFieldAutocompleterDemo {
public static void main(String[] args) {
@@ -996,6 +1045,7 @@ public class TextFieldAutocompleter<T> {
/**
* A demonstration of the autocompleter on two linked text fields.
*
* <p>
* This demo was designed to test whether the autocompleter and the {@link TextFieldLinker}
* could be composed correctly.
*/
@@ -855,6 +855,15 @@ public class TextFieldLinker {
}
}
/**
* Check if all component fields are visible
*
* @return false if any component is not visible, true otherwise
*/
public boolean isVisible() {
return linkedFields.stream().allMatch(lf -> lf.field.isVisible());
}
/**
* Add a focus listener
*
@@ -32,12 +32,15 @@ public class FormatSettingsDefinition implements EnumSettingsDefinition {
protected static final String FORMAT = "format";
public static final FormatSettingsDefinition DEF_CHAR = new FormatSettingsDefinition(CHAR); // Format with CHAR default
public static final FormatSettingsDefinition DEF_HEX = new FormatSettingsDefinition(HEX); // Format with HEX default
// Definitions with each settings as a default
public static final FormatSettingsDefinition DEF_HEX = new FormatSettingsDefinition(HEX);
public static final FormatSettingsDefinition DEF_DECIMAL =
new FormatSettingsDefinition(DECIMAL); // Format with DECIMAL default
new FormatSettingsDefinition(DECIMAL);
public static final FormatSettingsDefinition DEF_BINARY = new FormatSettingsDefinition(BINARY);
public static final FormatSettingsDefinition DEF_OCTAL = new FormatSettingsDefinition(OCTAL);
public static final FormatSettingsDefinition DEF_CHAR = new FormatSettingsDefinition(CHAR);
public static final FormatSettingsDefinition DEF = DEF_HEX; // Format with HEX default
public static final FormatSettingsDefinition DEF = DEF_HEX; // Default is HEX
private final int defaultFormat;
@@ -47,6 +50,7 @@ public class FormatSettingsDefinition implements EnumSettingsDefinition {
/**
* Returns the format based on the specified settings
*
* @param settings the instance settings or null for default value.
* @return the format value (HEX, DECIMAL, BINARY, OCTAL, CHAR)
*/
@@ -66,8 +70,8 @@ public class FormatSettingsDefinition implements EnumSettingsDefinition {
}
/**
* Returns the numeric radix associated with the
* format identified by the specified settings.
* Returns the numeric radix associated with the format identified by the specified settings.
*
* @param settings the instance settings.
* @return the format radix
*/
@@ -142,6 +146,7 @@ public class FormatSettingsDefinition implements EnumSettingsDefinition {
/**
* Sets the settings object to the enum value indicating the specified choice as a string.
*
* @param settings the settings to store the value.
* @param choice enum string representing a choice in the enum.
*/
@@ -188,16 +188,52 @@ public abstract class AbstractIntegerDataType extends BuiltIn implements ArraySt
return new Scalar(size * 8, val, isSigned());
}
/**
* Get the number of bits in the integral type
*
* @param type the type
* @return the number of bits
*/
protected static int getBitCount(Class<? extends Number> type) {
if (type == Byte.class) {
return Byte.SIZE;
}
if (type == Short.class) {
return Short.SIZE;
}
if (type == Integer.class) {
return Integer.SIZE;
}
if (type == Long.class) {
return Long.SIZE;
}
throw new AssertionError();
}
protected BigInteger castValueToEncode(Object value) throws DataTypeEncodeException {
if (value instanceof BigInteger) {
return (BigInteger) value;
}
if (value instanceof Scalar) {
// I'll take the scalar's signedness and neglect this type's....
return ((Scalar) value).getBigInteger();
}
if (value instanceof Byte || value instanceof Short || value instanceof Character ||
value instanceof Integer || value instanceof Long) {
return BigInteger.valueOf(((Number) value).longValue());
if (value instanceof Character) {
int numeric = Character.getNumericValue((Character) value);
if (numeric < 0) {
throw new DataTypeEncodeException("Character cannot be converted to number", value,
this);
}
return BigInteger.valueOf(numeric);
}
if (value instanceof Byte || value instanceof Short || value instanceof Integer ||
value instanceof Long) {
Number number = (Number) value;
BigInteger signedVal = BigInteger.valueOf(number.longValue());
if (isSigned() || signedVal.signum() >= 0) {
return signedVal;
}
return signedVal.add(BigInteger.ONE.shiftLeft(getBitCount(number.getClass())));
}
throw new DataTypeEncodeException("Unsupported value type", value, this);
}
@@ -217,11 +253,21 @@ public abstract class AbstractIntegerDataType extends BuiltIn implements ArraySt
throw new DataTypeEncodeException("Length mismatch", value, this);
}
BigInteger bigValue = castValueToEncode(value);
byte[] encoding = Utils.bigIntegerToBytes(bigValue, length, isSigned());
if (!ENDIAN.isBigEndian(settings, buf)) {
ArrayUtilities.reverse(encoding);
if (bigValue.signum() == -1 && !isSigned()) {
throw new DataTypeEncodeException("Unsigned type cannot have negative value", value,
this);
}
return encoding;
BigInteger maxValueExclusive = BigInteger.ONE.shiftLeft(length * 8 - (isSigned() ? 1 : 0));
BigInteger minValueInclusive = isSigned()
? BigInteger.ONE.shiftLeft(length * 8 - 1).negate()
: BigInteger.ZERO;
if (bigValue.compareTo(maxValueExclusive) >= 0) {
throw new DataTypeEncodeException("Value is too large", bigValue, this);
}
if (minValueInclusive.compareTo(bigValue) > 0) {
throw new DataTypeEncodeException("Value is too small", bigValue, this);
}
return Utils.bigIntegerToBytes(bigValue, length, ENDIAN.isBigEndian(settings, buf));
}
@Override
@@ -337,12 +383,15 @@ public abstract class AbstractIntegerDataType extends BuiltIn implements ArraySt
case FormatSettingsDefinition.DECIMAL:
radix = 10;
suffix = "";
break;
case FormatSettingsDefinition.BINARY:
radix = 2;
suffix = "b";
break;
case FormatSettingsDefinition.OCTAL:
radix = 8;
suffix = "o";
break;
default:
throw new AssertionError();
}
@@ -356,6 +405,21 @@ public abstract class AbstractIntegerDataType extends BuiltIn implements ArraySt
catch (Exception e) {
throw new DataTypeEncodeException(repr, this, e);
}
/**
* Ghidra doesn't actually heed signedness unless the format is DECIMAL. Thus, for user
* input, and to make this an inverse of getRepresentation, we'll adjust values between SMAX
* and UMAX to ensure they get encoded as expected, rather than rejected. We'll still accept
* signed values, though, since the user would rightly expect those to work, even though
* it'll get echoed back in unsigned form.
*/
if (format != FormatSettingsDefinition.DECIMAL && isSigned()) {
BigInteger umax = BigInteger.ONE.shiftLeft(8 * length);
BigInteger smax = umax.shiftRight(1);
if (smax.compareTo(value) <= 0 && value.compareTo(umax) < 0) {
value = value.subtract(umax);
}
}
return encodeValue(value, buf, settings, length);
}
@@ -25,6 +25,28 @@ public class DataTypeEncodeException extends UsrException {
private final Object value;
private final DataType dt;
private static String computeMessage(String message, Object value, DataType dt,
Throwable cause) {
if (cause != null) {
String encodeError = "while encoding '" + value + "' for " + dt.getDisplayName();
if (message != null) {
return cause.getMessage() + " (" + encodeError + ": " + message + ")";
}
else {
return cause.getMessage() + "(" + encodeError + ")";
}
}
else {
String encodeError = "Cannot encode '" + value + "' for " + dt.getDisplayName();
if (message != null) {
return encodeError + ": " + message;
}
else {
return encodeError;
}
}
}
/**
* Constructor
*
@@ -45,8 +67,7 @@ public class DataTypeEncodeException extends UsrException {
* @param cause the exception cause
*/
public DataTypeEncodeException(String message, Object value, DataType dt, Throwable cause) {
super("Cannot encode '" + value + "' for " + dt.getDisplayName() +
(message == null ? "" : ": " + message), cause);
super(computeMessage(message, value, dt, cause), cause);
this.value = value;
this.dt = dt;
}
@@ -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.program.model.data;
import static org.junit.Assert.*;
import org.junit.Test;
import generic.test.AbstractGenericTest;
import ghidra.docking.settings.*;
import ghidra.program.model.address.Address;
import ghidra.program.model.mem.ByteMemBufferImpl;
import ghidra.program.model.mem.MemBuffer;
public class IntegerDataTypeTest extends AbstractGenericTest {
private static byte[] arr(int... vals) {
byte[] result = new byte[vals.length];
for (int i = 0; i < vals.length; i++) {
result[i] = (byte) vals[i];
}
return result;
}
private static MemBuffer buf(boolean bigEndian, int... vals) {
return new ByteMemBufferImpl(Address.NO_ADDRESS, arr(vals), bigEndian);
}
private static Settings format(FormatSettingsDefinition setDef) {
Settings settings = new SettingsImpl();
setDef.setChoice(settings, setDef.getChoice(null));
return settings;
}
// NB. Need at least one byte to appear "initialized"
private static final MemBuffer BE = buf(true, 0);
private static final MemBuffer LE = buf(false, 0);
private static final Settings HEX = format(FormatSettingsDefinition.DEF_HEX);
private static final Settings DEC = format(FormatSettingsDefinition.DEF_DECIMAL);
private static final Settings BIN = format(FormatSettingsDefinition.DEF_BINARY);
private static final Settings OCT = format(FormatSettingsDefinition.DEF_OCTAL);
private static final Settings CHR = format(FormatSettingsDefinition.DEF_CHAR);
private interface EncodeRunnable {
void run() throws Exception;
}
private static void assertFails(EncodeRunnable r) throws Exception {
try {
r.run();
}
catch (DataTypeEncodeException e) {
return; // pass
}
fail();
}
@Test
public void testEncodeValueUnsignedByteBE() throws Exception {
DataType type = AbstractIntegerDataType.getUnsignedDataType(1, null);
// Technically, these two are exactly the same test, just different Java syntax
assertArrayEquals(arr(0xff), type.encodeValue((byte) 0xff, BE, HEX, 1));
assertArrayEquals(arr(0xff), type.encodeValue((byte) -1, BE, HEX, 1));
assertFails(() -> type.encodeValue((short) 0x100, BE, HEX, 1));
assertFails(() -> type.encodeValue((short) -1, BE, HEX, 1));
assertArrayEquals(arr(0xff), type.encodeValue(0xff, BE, HEX, 1));
// This fails, because (int)-1 is 4294967295 when treated unsigned
assertFails(() -> type.encodeValue(-1, BE, HEX, 1));
}
@Test
public void testEncodeRepresentationUnsignedByteHexBE() throws Exception {
DataType type = AbstractIntegerDataType.getUnsignedDataType(1, null);
// Sanity check: Renders unsigned
assertEquals("80h", type.getRepresentation(buf(true, 0x80), HEX, 1));
assertArrayEquals(arr(0x00), type.encodeRepresentation("0h", BE, HEX, 1));
assertArrayEquals(arr(0x7f), type.encodeRepresentation("7fh", BE, HEX, 1));
assertArrayEquals(arr(0x80), type.encodeRepresentation("80h", BE, HEX, 1));
assertArrayEquals(arr(0xff), type.encodeRepresentation("ffh", BE, HEX, 1));
assertFails(() -> type.encodeRepresentation("100h", BE, HEX, 1));
assertFails(() -> type.encodeRepresentation("-1h", BE, HEX, 1));
}
@Test
public void testEncodeRepresentationSignedShortHexBE() throws Exception {
DataType type = AbstractIntegerDataType.getSignedDataType(2, null);
// Sanity check: Negative hex values render unsigned
assertEquals("8000h", type.getRepresentation(buf(true, 0x80, 0x00), HEX, 2));
assertArrayEquals(arr(0x00, 0x00), type.encodeRepresentation("0h", BE, HEX, 2));
assertArrayEquals(arr(0x7f, 0xff), type.encodeRepresentation("7fffh", BE, HEX, 2));
assertArrayEquals(arr(0x80, 0x00), type.encodeRepresentation("8000h", BE, HEX, 2));
assertArrayEquals(arr(0xff, 0xff), type.encodeRepresentation("ffffh", BE, HEX, 2));
assertArrayEquals(arr(0xff, 0xff), type.encodeRepresentation("-1h", BE, HEX, 2));
assertArrayEquals(arr(0x80, 0x00), type.encodeRepresentation("-8000h", BE, HEX, 2));
assertFails(() -> type.encodeRepresentation("10000h", BE, HEX, 2));
assertFails(() -> type.encodeRepresentation("-8001h", BE, HEX, 2));
}
@Test
public void testEncodeRepresentationSignedShortHexLE() throws Exception {
DataType type = AbstractIntegerDataType.getSignedDataType(2, null);
// Sanity check: Negative hex values render unsigned
assertEquals("8000h", type.getRepresentation(buf(false, 0x00, 0x80), HEX, 2));
assertArrayEquals(arr(0x00, 0x00), type.encodeRepresentation("0h", LE, HEX, 2));
assertArrayEquals(arr(0xff, 0x7f), type.encodeRepresentation("7fffh", LE, HEX, 2));
assertArrayEquals(arr(0x00, 0x80), type.encodeRepresentation("8000h", LE, HEX, 2));
assertArrayEquals(arr(0xff, 0xff), type.encodeRepresentation("ffffh", LE, HEX, 2));
assertArrayEquals(arr(0xff, 0xff), type.encodeRepresentation("-1h", LE, HEX, 2));
assertArrayEquals(arr(0x00, 0x80), type.encodeRepresentation("-8000h", LE, HEX, 2));
assertFails(() -> type.encodeRepresentation("10000h", LE, HEX, 2));
assertFails(() -> type.encodeRepresentation("-8001h", LE, HEX, 2));
}
@Test
public void testEncodeRepresentationUnsignedShortHexBE() throws Exception {
DataType type = AbstractIntegerDataType.getUnsignedDataType(2, null);
// Sanity check: Renders unsigned
assertEquals("8000h", type.getRepresentation(buf(true, 0x80, 0x00), HEX, 2));
assertArrayEquals(arr(0x00, 0x00), type.encodeRepresentation("0h", BE, HEX, 2));
assertArrayEquals(arr(0x7f, 0xff), type.encodeRepresentation("7fffh", BE, HEX, 2));
assertArrayEquals(arr(0x80, 0x00), type.encodeRepresentation("8000h", BE, HEX, 2));
assertArrayEquals(arr(0xff, 0xff), type.encodeRepresentation("ffffh", BE, HEX, 2));
assertFails(() -> type.encodeRepresentation("-1h", BE, HEX, 2));
assertFails(() -> type.encodeRepresentation("-8000h", BE, HEX, 2));
assertFails(() -> type.encodeRepresentation("10000h", BE, HEX, 2));
assertFails(() -> type.encodeRepresentation("-8001h", BE, HEX, 2));
}
@Test
public void testEncodeRepresentationSignedShortDecBE() throws Exception {
DataType type = AbstractIntegerDataType.getSignedDataType(2, null);
// Sanity check: Negative hex values render signed
assertEquals("-32768", type.getRepresentation(buf(true, 0x80, 0x00), DEC, 2));
assertArrayEquals(arr(0x00, 0x00), type.encodeRepresentation("0", BE, DEC, 2));
assertArrayEquals(arr(0x7f, 0xff), type.encodeRepresentation("32767", BE, DEC, 2));
assertArrayEquals(arr(0x80, 0x00), type.encodeRepresentation("-32768", BE, DEC, 2));
assertArrayEquals(arr(0xff, 0xff), type.encodeRepresentation("-1", BE, DEC, 2));
assertFails(() -> type.encodeRepresentation("32768", BE, DEC, 2));
assertFails(() -> type.encodeRepresentation("-32769", BE, DEC, 2));
}
@Test
public void testEncodeRepresentationUnsignedShortDecBE() throws Exception {
DataType type = AbstractIntegerDataType.getUnsignedDataType(2, null);
// Sanity check: Renders unsigned
assertEquals("32768", type.getRepresentation(buf(true, 0x80, 0x00), DEC, 2));
assertArrayEquals(arr(0x00, 0x00), type.encodeRepresentation("0", BE, DEC, 2));
assertArrayEquals(arr(0x7f, 0xff), type.encodeRepresentation("32767", BE, DEC, 2));
assertArrayEquals(arr(0x80, 0x00), type.encodeRepresentation("32768", BE, DEC, 2));
assertArrayEquals(arr(0xff, 0xff), type.encodeRepresentation("65535", BE, DEC, 2));
assertFails(() -> type.encodeRepresentation("-1", BE, DEC, 2));
assertFails(() -> type.encodeRepresentation("65536", BE, DEC, 2));
}
@Test
public void testEncodeRepresentationSignedShortBinBE() throws Exception {
DataType type = AbstractIntegerDataType.getUnsignedDataType(2, null);
// Sanity check
assertEquals("100000011b", type.getRepresentation(buf(true, 0x01, 0x03), BIN, 2));
assertArrayEquals(arr(0x01, 0x03), type.encodeRepresentation("100000011b", BE, BIN, 2));
}
@Test
public void testEncodeRepresentationSignedShortOctBE() throws Exception {
DataType type = AbstractIntegerDataType.getUnsignedDataType(2, null);
// Sanity check
assertEquals("403o", type.getRepresentation(buf(true, 0x01, 0x03), OCT, 2));
assertArrayEquals(arr(0x01, 0x03), type.encodeRepresentation("403o", BE, OCT, 2));
}
@Test
public void testEncodeRepresentationChar() throws Exception {
DataType stype = AbstractIntegerDataType.getSignedDataType(1, null);
DataType utype = AbstractIntegerDataType.getUnsignedDataType(1, null);
// Sanity check
assertEquals("'A'", stype.getRepresentation(buf(true, 0x41), CHR, 1));
assertEquals("'A'", utype.getRepresentation(buf(true, 0x41), CHR, 1));
assertArrayEquals(arr(0x41), stype.encodeRepresentation("'A'", BE, CHR, 1));
assertArrayEquals(arr(0x41), utype.encodeRepresentation("'A'", BE, CHR, 1));
}
}
@@ -0,0 +1,44 @@
/* ###
* 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.assembler;
import org.junit.Test;
import ghidra.app.plugin.core.codebrowser.CodeViewerProvider;
import help.screenshot.GhidraScreenShotGenerator;
public class AssemblerPluginScreenShots extends GhidraScreenShotGenerator {
@Test
public void testCaptureAssembler() throws Exception {
setToolSize(1000, 800);
positionListingTop(0x00405120);
positionCursor(0x0040512e);
AssemblerPlugin assemblerPlugin = addPlugin(tool, AssemblerPlugin.class);
CodeViewerProvider codeViewer = waitForComponentProvider(CodeViewerProvider.class);
performAction(assemblerPlugin.patchInstructionAction, codeViewer, true);
AssemblyDualTextField input = assemblerPlugin.patchInstructionAction.input;
runSwing(() -> {
input.auto.startCompletion(input.getOperandsField());
input.auto.flushUpdates();
input.auto.select(0);
});
captureProviderWithScreenShot(codeViewer);
}
}
@@ -1,66 +0,0 @@
/* ###
* 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 help.screenshot;
import java.awt.AWTException;
import java.awt.Robot;
import java.awt.event.KeyEvent;
import org.junit.Test;
import docking.action.DockingActionIf;
import ghidra.app.plugin.core.assembler.AssemblerPlugin;
import ghidra.app.plugin.core.codebrowser.CodeViewerProvider;
import ghidra.framework.plugintool.util.PluginException;
public class AssemblerPluginScreenShots extends GhidraScreenShotGenerator {
@Test
public void testCaptureAssembler() throws PluginException, AWTException, InterruptedException {
setToolSize(1000, 800);
positionListingTop(0x00405120);
positionCursor(0x0040512e);
tool.addPlugin(AssemblerPlugin.class.getName());
DockingActionIf action = getAction(tool, "AssemblerPlugin", "Assemble");
performAction(action, true);
Robot rob = new Robot();
rob.keyPress(KeyEvent.VK_RIGHT);
rob.keyRelease(KeyEvent.VK_RIGHT);
// TODO: Will this work on Mac? control vs command
rob.keyPress(KeyEvent.VK_CONTROL);
rob.keyPress(KeyEvent.VK_SPACE);
rob.keyRelease(KeyEvent.VK_SPACE);
rob.keyRelease(KeyEvent.VK_CONTROL);
Thread.sleep(100);
rob.keyPress(KeyEvent.VK_ESCAPE);
rob.keyRelease(KeyEvent.VK_ESCAPE);
Thread.sleep(100);
rob.keyPress(KeyEvent.VK_CONTROL);
rob.keyPress(KeyEvent.VK_SPACE);
rob.keyRelease(KeyEvent.VK_SPACE);
rob.keyRelease(KeyEvent.VK_CONTROL);
captureProvider(CodeViewerProvider.class);
}
}