diff --git a/Ghidra/Features/Base/certification.manifest b/Ghidra/Features/Base/certification.manifest index 916c438c64..ec24544237 100644 --- a/Ghidra/Features/Base/certification.manifest +++ b/Ghidra/Features/Base/certification.manifest @@ -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| diff --git a/Ghidra/Features/Base/src/main/help/help/topics/AssemblerPlugin/Assembler.htm b/Ghidra/Features/Base/src/main/help/help/topics/AssemblerPlugin/Assembler.htm index 48fd5e2aff..17f0f8e132 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/AssemblerPlugin/Assembler.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/AssemblerPlugin/Assembler.htm @@ -9,109 +9,99 @@ Assembler - - -

Assembler

+

Assembler

-

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

- -

+

Assembly Editor

-
-

To edit assembly, select Patch Instruction from the - Listing View 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.

-
+

The Assembler plugin provides actions for modifying program bytes by inputing mnemonic + assembly and data values.

+ +

Actions

+ +

There are two actions: One for instructions and one for data.

+ +

Patch Instruction

+ +

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.

+ +

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.

Ghidra's assembler is based on the same SLEIGH modeling that powers the disassembler. This offers some nice benefits:

- + - -

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:

- + +

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:

+
  1. Platinum: Our automated tests did not find any errors. This offers the best possible user experience.
  2. -
  3. Gold: You will rarely encounter an error. You will find it very useful.
  4. -
  5. Silver: You may occasionally encounter an error, but the assembler is still - usable. You will likely find it useful with occasional frustration.
  6. -
  7. Bronze: 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.
  8. -
  9. Poor: 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.
  10. -
  11. Unrated: 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.
  12. + +
  13. Gold: Our automated tests found a couple of small errors. You should rarely + encounter issues.
  14. + +
  15. Silver: 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.
  16. + +
  17. Bronze: 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.
  18. + +
  19. Poor: 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.
  20. + +
  21. Unrated: 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.
- -

As of this release, our tested processors fall under Platinum, Gold, or Poor.

- - + +

If the current processor scored anything less than Platinum you will receive a + friendly warning reminding you what to expect.

+ +

Patch Data

+ +

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. NOTE: 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.

Provided by: Assembler plugin

diff --git a/Ghidra/Features/Base/src/main/help/help/topics/AssemblerPlugin/images/Assembler.png b/Ghidra/Features/Base/src/main/help/help/topics/AssemblerPlugin/images/Assembler.png index c7720e756e..7e05dfe03a 100644 Binary files a/Ghidra/Features/Base/src/main/help/help/topics/AssemblerPlugin/images/Assembler.png and b/Ghidra/Features/Base/src/main/help/help/topics/AssemblerPlugin/images/Assembler.png differ diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AbstractPatchAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AbstractPatchAction.java new file mode 100644 index 0000000000..f0bf95f198 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AbstractPatchAction.java @@ -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 + * + *

+ * 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) + * + *

+ * 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) + * + *

+ * 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 + * + *

+ * 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 + * + *

+ * 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 + * + *

+ * 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) + * + *

+ * 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 + * + *

+ * This hides the input field(s) without completing the action. + */ + public void cancel() { + hide(); + } + + /** + * Locate a listing field by name and address + * + *

+ * 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 + * + *

+ * 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); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AssembleDockingAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AssembleDockingAction.java deleted file mode 100644 index 13821413dd..0000000000 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AssembleDockingAction.java +++ /dev/null @@ -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> cache = - LazyMap.lazyMap(new HashMap<>(), language -> new AssemblerConstructorWorker(language)); - - private Map 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 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 { - 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, KeyListener { - @Override - public void completionActivated(AutocompletionEvent 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", - "

" + message + "

"); - 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; - } -} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AssemblerPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AssemblerPlugin.java index 30b8450eee..468514d339 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AssemblerPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AssemblerPlugin.java @@ -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. + *

+ * 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}. + *

+ * 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(); } - } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AssemblyDualTextField.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AssemblyDualTextField.java index 239b196d1d..64a4147d2f 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AssemblyDualTextField.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AssemblyDualTextField.java @@ -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) + * + *

+ * NOTE: 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 computeCompletions(String text) { - final AssemblyPatternBlock ctx = assembler.getContextAt(addr); + final AssemblyPatternBlock ctx = assembler.getContextAt(address); Set result = new TreeSet<>(); Collection 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)); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/PatchDataAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/PatchDataAction.java new file mode 100644 index 0000000000..5ce17b4cfb --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/PatchDataAction.java @@ -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 + } + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/PatchInstructionAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/PatchInstructionAction.java new file mode 100644 index 0000000000..838a860012 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/PatchInstructionAction.java @@ -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 { + 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 + * + *

+ * 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 { + @Override + public void completionActivated(AutocompletionEvent ev) { + if (ev.getSelection() instanceof AssemblyInstruction) { + AssemblyInstruction ins = (AssemblyInstruction) ev.getSelection(); + accept(ins); + } + } + } + + private final Map> cache = + LazyMap.lazyMap(new HashMap<>(), language -> new AssemblerConstructorWorker(language)); + + /*test*/ final Map 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 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", + "

" + message + "

"); + 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); + } + } +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/app/plugin/core/assembler/AssemblerPluginTest.java b/Ghidra/Features/Base/src/test/java/ghidra/app/plugin/core/assembler/AssemblerPluginTest.java new file mode 100644 index 0000000000..8d485e7561 --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/app/plugin/core/assembler/AssemblerPluginTest.java @@ -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 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 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 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()); + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/autocomplete/TextFieldAutocompleter.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/autocomplete/TextFieldAutocompleter.java index ae6a08f322..d1a153a307 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/autocomplete/TextFieldAutocompleter.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/autocomplete/TextFieldAutocompleter.java @@ -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}. * + *

* 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. * + *

* 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. * + *

* 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. * + *

* 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. + *

+ * The simplest use case is to create a text field, create an autocompleter with a custom model, and + * then attach and show. * - * *

  * JTextField field = new JTextField();
  * 
- * {@code AutocompletionModel model = new AutocompletionModel() }{
+ * AutocompletionModel model = new AutocompletionModel() {
  *     @Override
- *     {@code public Collection computeCompletions(String text)} {
- *         ... // Populate the completion list based on the given prefix.
+ *     public Collection computeCompletions(String text) {
+ *         // ... Populate the completion list based on the given prefix.
  *     }
  * }
- * {@code TextFieldAutocompleter completer = new TextFieldAutocompleter(model);
+ * TextFieldAutocompleter completer = new TextFieldAutocompleter(model);
  * completer.attachTo(field);
- * ... // Add the field to, e.g., a dialog, and show.
- * }
+ * // ... Add the field to, e.g., a dialog, and show. + * + * * * @param the type of suggestions presented by this autocompleter. */ @@ -261,6 +267,7 @@ public class TextFieldAutocompleter { /** * Create a new autocompleter associated with the given model. + * * @param model the model giving the suggestions. */ public TextFieldAutocompleter(AutocompletionModel model) { @@ -270,6 +277,7 @@ public class TextFieldAutocompleter { /** * Recompute the display location and move with list window. * + *

* 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 { /** * Update the contents of the suggestion list. * + *

* This entails taking the prefix, querying the model, and rendering the list. */ protected void updateDisplayContents() { @@ -288,7 +297,7 @@ public class TextFieldAutocompleter { updateManager.updateLater(); } - /* + /** * The actual implementation of updateDisplayContents, which gets scheduled asynchronously. */ private void doUpdateDisplayContents() { @@ -343,6 +352,7 @@ public class TextFieldAutocompleter { /** * 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 { /** * Check if the completion list window is visible. * + *

* 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 { /** * 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. + *

+ * 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 { /** * Get the preferred dimensions of the completion list window. * + *

* 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 { /** * Builds the list cell renderer for the autocompletion list. * + *

* 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 buildListCellRenderer() { @@ -457,7 +474,9 @@ public class TextFieldAutocompleter { /** * Attach the autocompleter to the given text field. * + *

* 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 { /** * Cause the currently-selected suggestion to be activated. * + *

* By default, this is called when the user presses ENTER or clicks a suggestion. */ protected void activateCurrentCompletion() { @@ -516,6 +536,7 @@ public class TextFieldAutocompleter { /** * Fire the registered autocompletion listeners on the given event. * + *

* 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 { 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 { /** * Register the given auto-completion listener + * * @param l the listener to register */ public void addAutocompletionListener(AutocompletionListener l) { @@ -557,6 +580,7 @@ public class TextFieldAutocompleter { /** * Unregister the given auto-completion listener + * * @param l the listener to unregister */ public void removeAutocompletionListener(AutocompletionListener l) { @@ -565,6 +589,7 @@ public class TextFieldAutocompleter { /** * Get all the registered auto-completion listeners + * * @return an array of registered listeners */ @SuppressWarnings("unchecked") @@ -576,7 +601,7 @@ public class TextFieldAutocompleter { * 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[] getListeners(Class listenerType) { @@ -660,12 +685,13 @@ public class TextFieldAutocompleter { /** * Decide whether the given suggestion can be automatically activated. * + *

* 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 { /** * Starts the autocompleter on the given text field. * + *

* 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. + * + *

+ * NOTE: 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 { } } + /** + * 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 { /** * 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 { /** * 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 { /** * 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 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 { } } 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 { /** * 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. + *

+ * 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 { /** * A demonstration of the autocompleter on two linked text fields. * + *

* This demo was designed to test whether the autocompleter and the {@link TextFieldLinker} * could be composed correctly. */ diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/TextFieldLinker.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/TextFieldLinker.java index 6bc9da7149..5b07f4e8b2 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/TextFieldLinker.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/TextFieldLinker.java @@ -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 * diff --git a/Ghidra/Framework/Docking/src/main/java/ghidra/docking/settings/FormatSettingsDefinition.java b/Ghidra/Framework/Docking/src/main/java/ghidra/docking/settings/FormatSettingsDefinition.java index fcc914ebf6..073a8fd20e 100644 --- a/Ghidra/Framework/Docking/src/main/java/ghidra/docking/settings/FormatSettingsDefinition.java +++ b/Ghidra/Framework/Docking/src/main/java/ghidra/docking/settings/FormatSettingsDefinition.java @@ -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. */ diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/AbstractIntegerDataType.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/AbstractIntegerDataType.java index a1fcbe3cfc..1d2c24624f 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/AbstractIntegerDataType.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/AbstractIntegerDataType.java @@ -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 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); } diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/DataTypeEncodeException.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/DataTypeEncodeException.java index 97c363825c..9799194b4a 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/DataTypeEncodeException.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/DataTypeEncodeException.java @@ -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; } diff --git a/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/model/data/IntegerDataTypeTest.java b/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/model/data/IntegerDataTypeTest.java new file mode 100644 index 0000000000..966437d687 --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/model/data/IntegerDataTypeTest.java @@ -0,0 +1,224 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.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)); + } +} diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/ghidra/app/plugin/core/assembler/AssemblerPluginScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/ghidra/app/plugin/core/assembler/AssemblerPluginScreenShots.java new file mode 100644 index 0000000000..36f1ed7c8d --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/screen/java/ghidra/app/plugin/core/assembler/AssemblerPluginScreenShots.java @@ -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); + } +} diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/AssemblerPluginScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/AssemblerPluginScreenShots.java deleted file mode 100644 index 703c8263e4..0000000000 --- a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/AssemblerPluginScreenShots.java +++ /dev/null @@ -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); - } -}