From 0fcdb7c6c376a930ec00085b9c9400444d3078e0 Mon Sep 17 00:00:00 2001 From: dragonmacher <48328597+dragonmacher@users.noreply.github.com> Date: Wed, 4 Nov 2020 16:35:31 -0500 Subject: [PATCH] Function Tags - updated DB to have a tag count cache; added service interface --- .../FunctionTagPlugin/images/FullWindow.png | Bin 32378 -> 31678 bytes .../app/cmd/function/AddFunctionTagCmd.java | 15 +- .../cmd/function/ChangeFunctionTagCmd.java | 20 +- .../cmd/function/CreateFunctionTagCmd.java | 19 +- .../cmd/function/DeleteFunctionTagCmd.java | 14 +- .../cmd/function/RemoveFunctionTagCmd.java | 18 +- .../function/tags/FunctionTagButtonPanel.java | 2 +- .../core/function/tags/FunctionTagPlugin.java | 10 +- ...Provider.java => FunctionTagProvider.java} | 229 +++++++++++++----- .../function/tags/FunctionTagRowObject.java | 3 +- .../function/tags/FunctionTagTableModel.java | 60 ++--- .../function/tags/InMemoryFunctionTag.java | 2 +- .../core/function/tags/SourceTagsPanel.java | 67 +---- .../core/function/tags/TagListPanel.java | 19 +- .../core/function/tags/TargetTagsPanel.java | 5 +- ...orTest.java => FunctionTagPluginTest.java} | 209 +++++++--------- .../widgets/textfield/HintTextField.java | 32 +-- .../concurrent/ConcurrentListenerSet.java | 9 +- .../program/database/function/FunctionDB.java | 43 ++-- .../database/function/FunctionManagerDB.java | 30 ++- .../database/function/FunctionTagDB.java | 12 +- .../function/FunctionTagManagerDB.java | 169 ++++++++++--- .../function/FunctionTagMappingAdapter.java | 26 +- .../FunctionTagMappingAdapterNoTable.java | 5 + .../function/FunctionTagMappingAdapterV0.java | 8 + .../program/model/listing/Function.java | 46 ++-- .../model/listing/FunctionManager.java | 111 ++++----- .../model/listing/FunctionTagManager.java | 33 ++- .../model/FunctionManagerTestDouble.java | 4 + .../FunctionTagPluginScreenShots.java | 57 ++--- 30 files changed, 682 insertions(+), 595 deletions(-) rename Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/tags/{FunctionTagsComponentProvider.java => FunctionTagProvider.java} (66%) rename Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/function/tags/{FunctionTagEditorTest.java => FunctionTagPluginTest.java} (79%) diff --git a/Ghidra/Features/Base/src/main/help/help/topics/FunctionTagPlugin/images/FullWindow.png b/Ghidra/Features/Base/src/main/help/help/topics/FunctionTagPlugin/images/FullWindow.png index f869f9ae6f9eac149491bb05d602e9fcb94e3f89..3c8d624e3763f1f2b009fd0dcd4d032bf3139b22 100644 GIT binary patch literal 31678 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 diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/tags/FunctionTagPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/tags/FunctionTagPlugin.java index 77a372364e..77842e29c8 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/tags/FunctionTagPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/tags/FunctionTagPlugin.java @@ -26,7 +26,7 @@ import ghidra.program.util.ProgramLocation; /** * Plugin for managing function tags. This works with the associated - * {@link FunctionTagsComponentProvider} to allow users to view and + * {@link FunctionTagProvider} to allow users to view and * edit function tags both globally and for individual functions. * */ @@ -40,18 +40,18 @@ import ghidra.program.util.ProgramLocation; ) //@formatter:on public class FunctionTagPlugin extends ProgramPlugin { - + public final static String FUNCTION_TAG_MENU_SUBGROUP = "TagFunction"; // Action visible when right-clicking on a function in the listing. private EditFunctionTagsAction editFunctionTagsAction; // The display object for this plugin. - private FunctionTagsComponentProvider provider; + private FunctionTagProvider provider; public FunctionTagPlugin(PluginTool tool) { super(tool, true, false); - provider = new FunctionTagsComponentProvider(this, getCurrentProgram()); + provider = new FunctionTagProvider(this, getCurrentProgram()); createActions(); } @@ -64,7 +64,7 @@ public class FunctionTagPlugin extends ProgramPlugin { * * @return the component provider */ - public FunctionTagsComponentProvider getProvider() { + public FunctionTagProvider getProvider() { return provider; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/tags/FunctionTagsComponentProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/tags/FunctionTagProvider.java similarity index 66% rename from Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/tags/FunctionTagsComponentProvider.java rename to Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/tags/FunctionTagProvider.java index 0522aaaf4b..8d2475f751 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/tags/FunctionTagsComponentProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/tags/FunctionTagProvider.java @@ -16,24 +16,29 @@ package ghidra.app.plugin.core.function.tags; import java.awt.*; -import java.util.ArrayList; +import java.util.*; import java.util.List; -import java.util.Set; import javax.swing.*; +import javax.swing.border.BevelBorder; +import javax.swing.border.Border; + +import org.apache.commons.lang3.StringUtils; import docking.widgets.label.GLabel; import docking.widgets.textfield.HintTextField; import ghidra.app.cmd.function.CreateFunctionTagCmd; import ghidra.app.context.ProgramActionContext; import ghidra.framework.cmd.Command; -import ghidra.framework.model.DomainObjectChangedEvent; -import ghidra.framework.model.DomainObjectListener; +import ghidra.framework.model.*; import ghidra.framework.plugintool.ComponentProviderAdapter; +import ghidra.program.database.function.FunctionManagerDB; import ghidra.program.model.address.Address; import ghidra.program.model.listing.*; import ghidra.program.util.*; import ghidra.util.*; +import ghidra.util.task.SwingUpdateManager; +import resources.ResourceManager; /** * Displays all the function tags in the database and identifies which ones have @@ -50,7 +55,7 @@ import ghidra.util.*; * "Edit Tags" option, or by selecting the "Edit Function Tags" option from the * "Window" menu. */ -public class FunctionTagsComponentProvider extends ComponentProviderAdapter +public class FunctionTagProvider extends ComponentProviderAdapter implements DomainObjectListener { private Color BORDER_COLOR = Color.GRAY; @@ -69,12 +74,35 @@ public class FunctionTagsComponentProvider extends ComponentProviderAdapter private int MIN_WIDTH = 850; private int MIN_HEIGHT = 350; + private SwingUpdateManager updater = new SwingUpdateManager(this::doUpdate); + // The current program location selected in the listing. private ProgramLocation currentLocation = null; // Character used as a separator when entering multiple tags in // the create tag entry field. - private static String INPUT_DELIMITER = ","; + private static final String INPUT_DELIMITER = ","; + + /** + * Optional! If there is a file with this name which can be found by the + * {@link ResourceManager}, and it contains a valid list of tag names, + * they will be loaded. The file must be XML with the following + * structure: + * + * + * + * TAG1 + * tag comment + * + * + * + */ + private static String TAG_FILE = "functionTags.xml"; + + // Keeps a list of the original tags as loaded from file. This is necessary when switching + // between programs where we need to know the original state of the disabled tags. Without + // this we would need to reload from file on each new program activation. + private Set tagsFromFile; /** * Constructor @@ -82,7 +110,7 @@ public class FunctionTagsComponentProvider extends ComponentProviderAdapter * @param plugin the function tag plugin * @param program the current program */ - public FunctionTagsComponentProvider(FunctionTagPlugin plugin, Program program) { + public FunctionTagProvider(FunctionTagPlugin plugin, Program program) { super(plugin.getTool(), "Function Tags", plugin.getName(), ProgramActionContext.class); setHelpLocation(new HelpLocation(plugin.getName(), plugin.getName())); @@ -94,25 +122,13 @@ public class FunctionTagsComponentProvider extends ComponentProviderAdapter * PUBLIC METHODS ******************************************************************************/ - /** - * Completely clears the UI and loads the tag table from scratch. Note that - * the model will be completely reset based on whatever the current location is - * in the listing. - */ - public void reload() { - - Swing.runLater(() -> { - tagInputField.setText(""); - updateTitle(currentLocation); - updateTagLists(); - }); - } - @Override public void componentShown() { - mainPanel = createWorkPanel(); - updateTagLists(); - updateTitle(currentLocation); + if (mainPanel == null) { + mainPanel = createWorkPanel(); + } + + updateView(); } @Override @@ -120,10 +136,6 @@ public class FunctionTagsComponentProvider extends ComponentProviderAdapter return mainPanel; } - HintTextField getTagInputField() { - return tagInputField; - } - /** * Invoked when a new location has been detected in the listing. When * this happens we need to update the tag list to show what tags are assigned @@ -133,8 +145,7 @@ public class FunctionTagsComponentProvider extends ComponentProviderAdapter */ public void locationChanged(ProgramLocation loc) { currentLocation = loc; - updateTitle(loc); - updateTagLists(); + updateView(); } public void programActivated(Program activatedProgram) { @@ -143,7 +154,7 @@ public class FunctionTagsComponentProvider extends ComponentProviderAdapter // Add a listener so we pick up domain object change events (add/delete/remove, etc...) activatedProgram.addListener(this); - updateTagLists(); + updateTagViews(); } public void programDeactivated(Program deactivatedProgram) { @@ -159,25 +170,54 @@ public class FunctionTagsComponentProvider extends ComponentProviderAdapter */ @Override public void domainObjectChanged(DomainObjectChangedEvent ev) { - if (ev.containsEvent(ChangeManager.DOCR_FUNCTION_TAG_CHANGED) || + + if (!isVisible()) { + return; + } + + if (ev.containsEvent(DomainObject.DO_OBJECT_RESTORED) || ev.containsEvent(ChangeManager.DOCR_FUNCTION_TAG_CREATED) || ev.containsEvent(ChangeManager.DOCR_FUNCTION_TAG_DELETED) || ev.containsEvent(ChangeManager.DOCR_TAG_REMOVED_FROM_FUNCTION) || ev.containsEvent(ChangeManager.DOCR_TAG_ADDED_TO_FUNCTION)) { - reload(); + updater.updateLater(); + return; + } + + if (ev.containsEvent(ChangeManager.DOCR_FUNCTION_TAG_CHANGED)) { + repaint(); } } - /****************************************************************************** - * PRIVATE METHODS - ******************************************************************************/ + private void doUpdate() { + reload(); + } + + private void reload() { + allFunctionsPanel.refresh(); + + Function function = getFunction(currentLocation); + sourcePanel.refresh(function); + targetPanel.refresh(function); + } + + private void updateView() { + updateTitle(currentLocation); + updateTagViews(); + } + + private void repaint() { + sourcePanel.repaint(); + targetPanel.repaint(); + allFunctionsPanel.repaint(); + } private void updateTitle(ProgramLocation location) { if (!isVisible()) { return; } - Function function = getFunctionAtLocation(location); + Function function = getFunction(location); if (function == null) { setSubTitle("NOT A FUNCTION"); } @@ -239,7 +279,7 @@ public class FunctionTagsComponentProvider extends ComponentProviderAdapter */ public void selectionChanged(TagListPanel panel) { - Function function = getFunctionAtLocation(currentLocation); + Function function = getFunction(currentLocation); if (panel instanceof SourceTagsPanel) { buttonPanel.sourcePanelSelectionChanged(function != null); @@ -256,22 +296,70 @@ public class FunctionTagsComponentProvider extends ComponentProviderAdapter allFunctionsPanel.setSelectedTags(sourceTags); } - TargetTagsPanel getTargetPanel() { + public AllFunctionsPanel getAllFunctionsPanel() { + return allFunctionsPanel; + } + + public TargetTagsPanel getTargetPanel() { return targetPanel; } - SourceTagsPanel getSourcePanel() { + public HintTextField getTagInputField() { + return tagInputField; + } + + public SourceTagsPanel getSourcePanel() { return sourcePanel; } + public FunctionTagButtonPanel getButtonPanel() { + return buttonPanel; + } + + public JPanel getInputPanel() { + return inputPanel; + } + + Set backgroundLoadTags() { + // Add any tags from the file system that are not in the db + List dbTags = getAllTagsFromDatabase(); + Set allTags = new HashSet<>(dbTags); + allTags.addAll(getFileTags()); + return allTags; + } + /** - * Returns the {@link Function} at the given program location. If not a function, or - * if the location is not a pointer to a function returns null. + * Loads tags from the external file specified. + * + * @return the loaded tags + */ + private Set getFileTags() { + if (tagsFromFile == null) { + tagsFromFile = FunctionTagLoader.loadTags(TAG_FILE); + } + return tagsFromFile; + } + + /** + * Returns an array of all tags stored in the database. + * + * @return list of tags + */ + private List getAllTagsFromDatabase() { + if (program == null) { + return Collections.emptyList(); + } + FunctionManagerDB functionManagerDB = (FunctionManagerDB) program.getFunctionManager(); + return functionManagerDB.getFunctionTagManager().getAllFunctionTags(); + } + + /** + * Returns the {@link Function} for the given program location * * @param loc the program location * @return function containing the location, or null if not applicable */ - private Function getFunctionAtLocation(ProgramLocation loc) { + private Function getFunction(ProgramLocation loc) { Address functionAddress = getFunctionAddress(loc); if (functionAddress == null) { @@ -293,8 +381,7 @@ public class FunctionTagsComponentProvider extends ComponentProviderAdapter return null; } - // If the user clicks on an instruction within a function we want to show - // the tags. + // If the user clicks on an instruction within a function we want to show the tags if (program.getFunctionManager().isInFunction(loc.getAddress())) { return loc.getAddress(); } @@ -309,13 +396,11 @@ public class FunctionTagsComponentProvider extends ComponentProviderAdapter } /** - * Refreshes the contents of the table with the current program and location. This - * should be called any time a program is activated or the location in the listing - * has changed. + * Refreshes the contents of the tables with the current program and location */ - private void updateTagLists() { + private void updateTagViews() { - if (sourcePanel == null || targetPanel == null || allFunctionsPanel == null) { + if (mainPanel == null || !isVisible()) { return; } @@ -332,7 +417,7 @@ public class FunctionTagsComponentProvider extends ComponentProviderAdapter allFunctionsPanel.refresh(sTags); } - Function function = getFunctionAtLocation(currentLocation); + Function function = getFunction(currentLocation); sourcePanel.refresh(function); targetPanel.refresh(function); } @@ -343,42 +428,52 @@ public class FunctionTagsComponentProvider extends ComponentProviderAdapter private void processCreates() { if (program == null) { - Msg.showInfo(this, tool.getActiveWindow(), "No program!", + Msg.showInfo(this, tool.getActiveWindow(), "No Program", "You must load a program before trying to create tags"); return; } + + List dropped = new ArrayList<>(); List names = getInputNames(); for (String name : names) { - // Only execute the create command if a tag with the given name does not - // already exist. - if (sourcePanel.tagExists(name) || targetPanel.tagExists(name)) { - Msg.showInfo(this, tool.getActiveWindow(), "Duplicate Tag Name", - "There is already a tag with the name [" + name + "]. Please try again."); + // only execute the create command if a tag with the given name does not already exist + // (note: this could fail if the model is not yet loaded, but this is unlikely. The + // only fallout is that the error message would not be shown--the database will not add + // the tag twice.) + if (sourcePanel.tagExists(name)) { + dropped.add(name); } else { Command cmd = new CreateFunctionTagCmd(name); tool.execute(cmd, program); } } + + if (!dropped.isEmpty()) { + String text = StringUtils.join(dropped, ", "); + Msg.showInfo(this, tool.getActiveWindow(), "Duplicate Tag Names", + "Tags aleady exist. Ignoring the following tags: " + text); + } + + Swing.runLater(() -> tagInputField.setText("")); } /** - * Returns a list of tag names the user has entered in the input field. + * Returns a list of tag names the user has entered in the input` field. * Note: This assumes that multiple entries are comma-delimited. * * @return the list of tag names to create */ private List getInputNames() { - // First split the string on the delimiter to get all the entries. + // first split the string on the delimiter to get all the entries String[] names = tagInputField.getText().split(INPUT_DELIMITER); - // Trim each item to remove any leading/trailing whitespace and add to - // the return list. - ArrayList nameList = new ArrayList<>(); + // trim each item to remove any leading/trailing whitespace and add to the return list + List nameList = new ArrayList<>(); for (String name : names) { - if (!name.trim().isEmpty()) { + if (!StringUtils.isBlank(name)) { nameList.add(name.trim()); } } @@ -393,14 +488,20 @@ public class FunctionTagsComponentProvider extends ComponentProviderAdapter */ private JPanel createInputPanel() { - inputPanel = new JPanel(new BorderLayout()); tagInputField = new HintTextField("tag 1, tag 2, ..."); tagInputField.setName("tagInputTF"); tagInputField.addActionListener(e -> processCreates()); + inputPanel = new JPanel(); + Border outsideBorder = BorderFactory.createBevelBorder(BevelBorder.LOWERED); + Border insideBorder = BorderFactory.createEmptyBorder(5, 2, 2, 2); + inputPanel.setBorder(BorderFactory.createCompoundBorder(outsideBorder, insideBorder)); + inputPanel.setLayout(new BoxLayout(inputPanel, BoxLayout.LINE_AXIS)); inputPanel.add(new GLabel(" Create new tag(s):"), BorderLayout.WEST); + inputPanel.add(Box.createHorizontalStrut(5)); inputPanel.add(tagInputField, BorderLayout.CENTER); return inputPanel; } + } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/tags/FunctionTagRowObject.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/tags/FunctionTagRowObject.java index 14587fd5ba..777648230a 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/tags/FunctionTagRowObject.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/tags/FunctionTagRowObject.java @@ -22,8 +22,9 @@ class FunctionTagRowObject { private FunctionTag tag; private int count; - FunctionTagRowObject(FunctionTag tag) { + FunctionTagRowObject(FunctionTag tag, int count) { this.tag = tag; + this.count = count; } FunctionTag getTag() { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/tags/FunctionTagTableModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/tags/FunctionTagTableModel.java index 0e3ff066de..55c46a013c 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/tags/FunctionTagTableModel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/tags/FunctionTagTableModel.java @@ -15,10 +15,7 @@ */ package ghidra.app.plugin.core.function.tags; -import java.util.*; -import java.util.function.Supplier; - -import org.apache.commons.collections4.map.LazyMap; +import java.util.Set; import docking.widgets.table.AbstractDynamicTableColumnStub; import docking.widgets.table.TableColumnDescriptor; @@ -27,7 +24,6 @@ import ghidra.docking.settings.Settings; import ghidra.framework.plugintool.ServiceProvider; import ghidra.program.model.listing.*; import ghidra.util.datastruct.Accumulator; -import ghidra.util.datastruct.Counter; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; @@ -37,12 +33,12 @@ import ghidra.util.task.TaskMonitor; public class FunctionTagTableModel extends ThreadedTableModel { private Program program; - private Supplier> tagLoader; + private TagListPanel tagListPanel; protected FunctionTagTableModel(String modelName, ServiceProvider serviceProvider, - Supplier> tagLoader) { + TagListPanel tagLoader) { super(modelName, serviceProvider); - this.tagLoader = tagLoader; + this.tagListPanel = tagLoader; } public void setProgram(Program program) { @@ -57,37 +53,15 @@ public class FunctionTagTableModel extends ThreadedTableModel countsByName = getFunctionTagCountsByName(monitor); - for (FunctionTag tag : tagLoader.get()) { - - FunctionTagRowObject rowObject = new FunctionTagRowObject(tag); - Counter counter = countsByName.get(tag.getName()); - rowObject.setCount(counter.count); - accumulator.add(rowObject); - } - } - - private Map getFunctionTagCountsByName(TaskMonitor monitor) - throws CancelledException { - FunctionManager functionManager = program.getFunctionManager(); - monitor.initialize(functionManager.getFunctionCount()); - - Map map = LazyMap.lazyMap(new HashMap<>(), () -> new Counter()); - FunctionIterator it = functionManager.getFunctions(true); - for (Function function : it) { + FunctionTagManager tagManager = functionManager.getFunctionTagManager(); + Set tags = tagListPanel.backgroundLoadTags(); + monitor.initialize(tags.size()); + for (FunctionTag tag : tags) { monitor.checkCanceled(); - - Set functionTags = function.getTags(); - for (FunctionTag tag : functionTags) { - - String name = tag.getName(); - map.get(name).count++; - } - + accumulator.add(new FunctionTagRowObject(tag, tagManager.getUseCount(tag))); monitor.incrementProgress(1); } - return map; } @Override @@ -115,11 +89,23 @@ public class FunctionTagTableModel extends ThreadedTableModel row.getName().equals(name)); + return getRowObject(name) != null; + } + + /** + * Returns the row object that matches the given tag name + * @param name the tag name + * @return the row object + */ + public FunctionTagRowObject getRowObject(String name) { + return getAllData().stream() + .filter(row -> row.getName().equals(name)) + .findFirst() + .orElseGet(() -> null); } /** diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/tags/InMemoryFunctionTag.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/tags/InMemoryFunctionTag.java index 7f8b7c6c27..e13f5a1155 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/tags/InMemoryFunctionTag.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/tags/InMemoryFunctionTag.java @@ -23,7 +23,7 @@ import ghidra.program.model.listing.FunctionTag; * This class provides an implementation of the {@link FunctionTag} interface for * tags that are not yet ready to be inserted into the database. This was created * to allow tags to be imported from an external file and made available to the user - * through the {@link FunctionTagsComponentProvider} UI without needing to formally + * through the {@link FunctionTagProvider} UI without needing to formally * add them to the {@code FunctionTagAdapter} table. */ class InMemoryFunctionTag implements FunctionTag { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/tags/SourceTagsPanel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/tags/SourceTagsPanel.java index 204acf6140..5835def9df 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/tags/SourceTagsPanel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/tags/SourceTagsPanel.java @@ -15,44 +15,20 @@ */ package ghidra.app.plugin.core.function.tags; -import java.util.*; +import java.util.Set; import ghidra.app.cmd.function.AddFunctionTagCmd; import ghidra.app.cmd.function.CreateFunctionTagCmd; import ghidra.framework.cmd.Command; import ghidra.framework.plugintool.PluginTool; -import ghidra.program.database.function.FunctionManagerDB; import ghidra.program.model.listing.Function; import ghidra.program.model.listing.FunctionTag; -import resources.ResourceManager; /** - * List for displaying all tags in the program that have NOT yet been assigned - * to a function. + * List for displaying all tags in the programs */ public class SourceTagsPanel extends TagListPanel { - /** - * Optional! If there is a file with this name which can be found by the - * {@link ResourceManager}, and it contains a valid list of tag names, - * they will be loaded. The file must be XML with the following - * structure: - * - * - * - * TAG1 - * tag comment - * - * - * - */ - private static String TAG_FILE = "functionTags.xml"; - - // Keeps a list of the original tags as loaded from file. This is necessary when switching - // between programs where we need to know the original state of the disabled tags. Without - // this we would need to reload from file on each new program activation. - private Set tagsFromFile; - /** * Constructor * @@ -60,12 +36,9 @@ public class SourceTagsPanel extends TagListPanel { * @param tool the plugin tool * @param title the title of the panel */ - public SourceTagsPanel(FunctionTagsComponentProvider provider, PluginTool tool, String title) { + public SourceTagsPanel(FunctionTagProvider provider, PluginTool tool, String title) { super(provider, tool, title); - // Load any tags from external sources - tagsFromFile = loadTags(); - table.setDisabled(true); } @@ -106,13 +79,7 @@ public class SourceTagsPanel extends TagListPanel { @Override protected Set backgroundLoadTags() { - - List dbTags = getAllTagsFromDatabase(); - - // Add any tags from the file system that are not in the db - Set allTags = new HashSet<>(dbTags); - allTags.addAll(tagsFromFile); - return allTags; + return provider.backgroundLoadTags(); } /** @@ -129,30 +96,4 @@ public class SourceTagsPanel extends TagListPanel { return true; } - - /****************************************************************************** - * PRIVATE METHODS - ******************************************************************************/ - - /** - * Returns an array of all tags stored in the database. - * - * @return list of tags - */ - private List getAllTagsFromDatabase() { - if (program == null) { - return Collections.emptyList(); - } - FunctionManagerDB functionManagerDB = (FunctionManagerDB) program.getFunctionManager(); - return functionManagerDB.getFunctionTagManager().getAllFunctionTags(); - } - - /** - * Loads tags from the external file specified. - * - * @return the loaded tags - */ - private Set loadTags() { - return FunctionTagLoader.loadTags(TAG_FILE); - } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/tags/TagListPanel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/tags/TagListPanel.java index fe8c40938e..4662340d24 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/tags/TagListPanel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/tags/TagListPanel.java @@ -45,6 +45,7 @@ import ghidra.util.table.GhidraThreadedTablePanel; public abstract class TagListPanel extends JPanel { protected PluginTool tool; + protected FunctionTagProvider provider; protected Program program; protected Function function; @@ -61,13 +62,13 @@ public abstract class TagListPanel extends JPanel { * @param tool the plugin tool * @param title the title of the panel */ - public TagListPanel(FunctionTagsComponentProvider provider, PluginTool tool, String title) { + public TagListPanel(FunctionTagProvider provider, PluginTool tool, String title) { this.tool = tool; + this.provider = provider; setLayout(new BorderLayout()); - model = new FunctionTagTableModel("Function Tags", provider.getTool(), - this::backgroundLoadTags); + model = new FunctionTagTableModel("Function Tags", provider.getTool(), this); GhidraThreadedTablePanel tablePanel = new GhidraThreadedTablePanel<>(model) { protected GTable createTable(ThreadedTableModel tm) { @@ -121,10 +122,6 @@ public abstract class TagListPanel extends JPanel { */ public abstract void refresh(Function newFunction); - /** - * Called by a background thread to load the tags for this panel - * @return the tags - */ protected abstract Set backgroundLoadTags(); void editRow(int row) { @@ -165,11 +162,11 @@ public abstract class TagListPanel extends JPanel { }); DockingWindowManager.showDialog(tool.getActiveWindow(), dialog); - if (dialog.isCanceled()) { - return; + String[] results = dialog.getValues(); + if (results[0] == null) { + return; // cancelled/closed } - String[] results = dialog.getValues(); String newName = results[0].trim(); String newComment = results[1].trim(); @@ -192,7 +189,7 @@ public abstract class TagListPanel extends JPanel { return model; } - FunctionTagTable getTable() { + public FunctionTagTable getTable() { return table; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/tags/TargetTagsPanel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/tags/TargetTagsPanel.java index d318a58b24..462375658e 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/tags/TargetTagsPanel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/tags/TargetTagsPanel.java @@ -24,8 +24,7 @@ import ghidra.program.model.listing.Function; import ghidra.program.model.listing.FunctionTag; /** - * Displays a list of tags that have been assigned to the currently-selected - * function in the listing + * Displays a list of tags that have been assigned to the current function */ public class TargetTagsPanel extends TagListPanel { @@ -36,7 +35,7 @@ public class TargetTagsPanel extends TagListPanel { * @param tool the plugin tool * @param title the panel title */ - public TargetTagsPanel(FunctionTagsComponentProvider provider, + public TargetTagsPanel(FunctionTagProvider provider, PluginTool tool, String title) { super(provider, tool, title); diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/function/tags/FunctionTagEditorTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/function/tags/FunctionTagPluginTest.java similarity index 79% rename from Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/function/tags/FunctionTagEditorTest.java rename to Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/function/tags/FunctionTagPluginTest.java index 0dedb545dc..5405e817ea 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/function/tags/FunctionTagEditorTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/function/tags/FunctionTagPluginTest.java @@ -23,18 +23,19 @@ import java.util.*; import javax.swing.AbstractButton; +import org.apache.commons.lang3.StringUtils; import org.junit.*; import docking.ActionContext; import docking.action.DockingActionIf; import docking.widgets.dialogs.InputDialog; import docking.widgets.textfield.HintTextField; +import ghidra.app.cmd.function.DeleteFunctionCmd; import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin; import ghidra.app.plugin.core.gotoquery.GoToServicePlugin; import ghidra.app.services.ProgramManager; import ghidra.framework.plugintool.PluginTool; import ghidra.program.database.function.FunctionDB; -import ghidra.program.database.function.FunctionManagerDB; import ghidra.program.model.address.Address; import ghidra.program.model.listing.*; import ghidra.program.util.ProgramLocation; @@ -43,13 +44,13 @@ import ghidra.util.exception.UsrException; /** * Test class for the {@link FunctionTagPlugin}. This tests the ability to use the - * function tag edit GUI ({@link FunctionTagsComponentProvider} to create/edit/delete + * function tag edit GUI ({@link FunctionTagProvider} to create/edit/delete * tags. * * Test related to the merging and diffing of tags are defined in the * {@link FunctionTagMergeTest} class. */ -public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest { +public class FunctionTagPluginTest extends AbstractGhidraHeadedIntegrationTest { private TestEnv env; private PluginTool tool; @@ -57,8 +58,6 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest { private CodeBrowserPlugin cb; private FunctionTagPlugin plugin; - // Menu item available on a function entry point that launches the - // dialog. private DockingActionIf editFunctionTags; private Address NON_FUNCTION_ADDRESS; @@ -66,8 +65,7 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest { private Address FUNCTION_ENTRY_ADDRESS_2; private Address FUNCTION_ENTRY_ADDRESS_3; - // The UI we're testing. - private FunctionTagsComponentProvider provider = null; + private FunctionTagProvider provider = null; @Before public void setUp() throws Exception { @@ -104,10 +102,6 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest { env.dispose(); } - /**************************************************************************************** - * TESTS - ****************************************************************************************/ - /** * Tests that the menu option for bringing up the editor exists * on function headers only. @@ -143,13 +137,13 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest { @Test public void testDeleteImmutableTag() throws Exception { - FunctionTagTable table = getTagListInPanel("sourcePanel"); + FunctionTagTable table = getSourceTable(); // Get an immutable tag from the source panel and set it to be selected. Verify that // the delete button is disabled. InMemoryFunctionTag immutableTag = getImmutableTag(); assertTrue("Must have at least one immutable tag for this test", immutableTag != null); - selectTagInList(immutableTag.getName(), table); + selectTagInTable(immutableTag.getName(), table); waitForSwing(); assertFalse(isButtonEnabled("deleteBtn")); @@ -164,7 +158,7 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest { // Select just the non-immutable tag and verify that the delete button is now // enabled. - selectTagInList(tagName, table); + selectTagInTable(tagName, table); waitForSwing(); assertTrue(isButtonEnabled("deleteBtn")); } @@ -177,7 +171,7 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest { @Test public void testDeleteImmutableTagAfterUse() throws Exception { - FunctionTagTable table = getTagListInPanel("targetPanel"); + FunctionTagTable table = getTargetTable(); // Get an immutable tag from the source panel. InMemoryFunctionTag tag = getImmutableTag(); @@ -191,7 +185,7 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest { boolean inList = tagExists(tag.getName(), getAllTags()); assertTrue(inList); - selectTagInList(tag.getName(), table); + selectTagInTable(tag.getName(), table); waitForSwing(); assertTrue(isButtonEnabled("deleteBtn")); } @@ -292,8 +286,22 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest { String name = "TEST"; createTag(name); addTagToFunction(name, FUNCTION_ENTRY_ADDRESS); + assertTableTagCount(name, 1); removeTagFromFunction(name, FUNCTION_ENTRY_ADDRESS); assertFalse(tagExists(name, getTags(FUNCTION_ENTRY_ADDRESS))); + assertTableTagCount(name, 0); + } + + @Test + public void testFunctionRemoved() throws Exception { + String name = "TEST"; + createTag(name); + addTagToFunction(name, FUNCTION_ENTRY_ADDRESS); + assertTableTagCount(name, 1); + + removeFunction(FUNCTION_ENTRY_ADDRESS); + assertTrue(tagExists(name, getAllTags())); + assertTableTagCount(name, 0); } /** @@ -305,7 +313,7 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest { public void testViewFunctionsForTag() throws Exception { // Verify that the function panel is initially empty - AllFunctionsPanel functionsPanel = getFunctionsPanel(); + AllFunctionsPanel functionsPanel = provider.getAllFunctionsPanel(); List functions = functionsPanel.getFunctions(); assertTrue(functions.isEmpty()); @@ -315,8 +323,8 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest { addTagToFunction(tagName1, FUNCTION_ENTRY_ADDRESS); // Select the tag in the target panel - FunctionTagTable table = getTagListInPanel("targetPanel"); - selectTagInList(tagName1, table); + FunctionTagTable table = getTargetTable(); + selectTagInTable(tagName1, table); waitForTableModel(functionsPanel.getTableModel()); // Verify that the function is shown in the function panel (check that @@ -338,7 +346,7 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest { public void testMultipleFunctionsWithTag() throws Exception { // Verify that the function panel is initially empty - AllFunctionsPanel functionsPanel = getFunctionsPanel(); + AllFunctionsPanel functionsPanel = provider.getAllFunctionsPanel(); List functions = functionsPanel.getFunctions(); assertTrue(functions.isEmpty()); @@ -349,8 +357,8 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest { addTagToFunction(tagName1, FUNCTION_ENTRY_ADDRESS_2); // Select the tag in the target panel - FunctionTagTable table = getTagListInPanel("targetPanel"); - selectTagInList(tagName1, table); + FunctionTagTable table = getTargetTable(); + selectTagInTable(tagName1, table); waitForTableModel(functionsPanel.getTableModel()); // Verify that both functions are shown in the function panel (check that @@ -374,7 +382,7 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest { public void testViewMultipleFunctions() throws Exception { // Verify that the function panel is initially empty - AllFunctionsPanel functionsPanel = getFunctionsPanel(); + AllFunctionsPanel functionsPanel = provider.getAllFunctionsPanel(); List functions = functionsPanel.getFunctions(); assertTrue(functions.isEmpty()); @@ -391,8 +399,8 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest { // Select both tags and verify that 3 functions are listed in // the functions panel goTo(tool, program, addr("00000000")); - FunctionTagTable table = getTagListInPanel("sourcePanel"); - selectTagInList(tagName1, table); + FunctionTagTable table = getSourceTable(); + selectTagInTable(tagName1, table); int index = table.getSelectedRow(); table.addRowSelectionInterval(index, index + 1); clickTableRange(table, index, 2); @@ -466,9 +474,9 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest { cb.goTo(new ProgramLocation(program, address)); waitForSwing(); - FunctionTagTable list = getTagListInPanel("sourcePanel"); - selectTagInList(name, list); - clickButtonByName("addBtn"); + FunctionTagTable list = getSourceTable(); + selectTagInTable(name, list); + clickButton("addBtn"); } /** @@ -483,42 +491,21 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest { cb.goTo(new ProgramLocation(program, address)); waitForSwing(); - FunctionTagTable table = getTagListInPanel("targetPanel"); - selectTagInList(name, table); - clickButtonByName("removeBtn"); + FunctionTagTable table = getTargetTable(); + selectTagInTable(name, table); + clickButton("removeBtn"); } - /** - * Gets the instance of the tag table in the given panel. - * - * @param panelName the name of the panel to search - * @return the function tag list - * @throws UsrException if there's an error retrieving the panel or tag list instances - */ - private FunctionTagTable getTagListInPanel(String panelName) throws UsrException { - Object comp = getInstanceField(panelName, provider); - if (comp == null) { - throw new UsrException("Error getting targetPanel field in provider"); - } - - Object table = getInstanceField("table", comp); - if (table == null) { - throw new UsrException("Error getting table in tag panel"); - } - - FunctionTagTable tagTable = (FunctionTagTable) table; - return tagTable; + private void removeFunction(Address address) { + applyCmd(program, new DeleteFunctionCmd(address)); } - /** - * Returns the functions panel - * - * @return the functions panel - */ - private AllFunctionsPanel getFunctionsPanel() { - AllFunctionsPanel panel = - (AllFunctionsPanel) getInstanceField("allFunctionsPanel", provider); - return panel; + private FunctionTagTable getTargetTable() { + return provider.getTargetPanel().getTable(); + } + + private FunctionTagTable getSourceTable() { + return provider.getSourcePanel().getTable(); } /** @@ -528,7 +515,7 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest { * @param table the table to search * @throws Exception if there is a problem clicking cells in a list */ - private void selectTagInList(String name, FunctionTagTable table) throws Exception { + private void selectTagInTable(String name, FunctionTagTable table) throws Exception { assertTagExists(name, table); int row = 0; @@ -548,14 +535,9 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest { * Clicks the button with the given name in the {@link FunctionTagButtonPanel}. * * @param name the button name - * @throws UsrException if there's an error retrieving the button panel instance */ - private void clickButtonByName(String name) throws UsrException { - Object buttonPanel = getInstanceField("buttonPanel", provider); - if (buttonPanel == null) { - throw new UsrException("Error getting button panel field in provider"); - } - FunctionTagButtonPanel btnPanel = (FunctionTagButtonPanel) buttonPanel; + private void clickButton(String name) { + FunctionTagButtonPanel btnPanel = provider.getButtonPanel(); pressButtonByName(btnPanel, name); waitForSwing(); } @@ -565,14 +547,9 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest { * * @param name the button name * @return true if enabled - * @throws UsrException if there's an error retrieving the button panel instance */ - private boolean isButtonEnabled(String name) throws UsrException { - Object buttonPanel = getInstanceField("buttonPanel", provider); - if (buttonPanel == null) { - throw new UsrException("Error getting button panel field in provider"); - } - FunctionTagButtonPanel btnPanel = (FunctionTagButtonPanel) buttonPanel; + private boolean isButtonEnabled(String name) { + FunctionTagButtonPanel btnPanel = provider.getButtonPanel(); AbstractButton button = findAbstractButtonByName(btnPanel, name); return isEnabled(button); } @@ -591,11 +568,17 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest { return null; } - /** - * Loads the notepad program. - * - * @throws Exception if there's an error creating the program builder - */ + private void assertTableTagCount(String name, int expected) { + + waitForSwing(); + FunctionTagTableModel model = provider.getSourcePanel().getModel(); + waitForTableModel(model); + + FunctionTagRowObject rowObject = model.getRowObject(name); + assertNotNull(rowObject); + assertEquals("Tag count in table is incorrect", expected, rowObject.getCount()); + } + private void loadProgram() throws Exception { ClassicSampleX86ProgramBuilder builder = new ClassicSampleX86ProgramBuilder(); @@ -604,37 +587,35 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest { pm.openProgram(program.getDomainFile()); } - /** - * Displays the function tag dialog. - */ private void showDialog() { performAction(editFunctionTags, cb.getProvider(), false); - provider = waitForComponentProvider(FunctionTagsComponentProvider.class); + provider = waitForComponentProvider(FunctionTagProvider.class); tool.showComponentProvider(provider, true); } - /** - * Places the code browser cursor at the given function entry point. - * - * @param address entry point of a function - */ private void goToFunction(Address address) { cb.goTo(new ProgramLocation(program, address)); assertEquals(address, cb.getCurrentAddress()); } - /** - * Adds a tag to the database. - * - * @param name the name of the tag - */ - private void createTag(String name) { + private void createTag(String nameList) { HintTextField inputField = provider.getTagInputField(); - setText(inputField, name); + setText(inputField, nameList); triggerEnter(inputField); + waitForTasks(); waitForTables(); - waitForSwing(); + + Collection tags = getAllTags(); + String[] parts = nameList.split(","); + for (String name : parts) { + if (StringUtils.isBlank(name)) { + continue; + } + name = name.trim(); + assertTrue("Tag not created '" + name + "' from text '" + nameList + "'", + tagExists(name.trim(), tags)); + } } private void waitForTables() { @@ -644,20 +625,14 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest { FunctionTagTableModel smodel = sourcePanel.getModel(); waitForTableModel(tmodel); waitForTableModel(smodel); + waitForSwing(); } - /** - * Removes a tag from the database. - * - * @param name the name of the tag - * @throws IOException if there's an error retrieving tags from the database - */ - private void deleteTag(String name) throws IOException { + private void deleteTag(String name) { FunctionTag tag = getTagForName(name, getAllTags()); tx(program, () -> tag.delete()); waitForTables(); - waitForSwing(); } /** @@ -691,7 +666,7 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest { */ private InMemoryFunctionTag getImmutableTag() throws UsrException { - FunctionTagTable table = getTagListInPanel("sourcePanel"); + FunctionTagTable table = getSourceTable(); FunctionTagTableModel model = (FunctionTagTableModel) table.getModel(); Optional optional = model.getModelData().stream().filter(row -> row.isImmutable()).findAny(); @@ -700,35 +675,15 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest { return (InMemoryFunctionTag) foundTag; } - /** - * Returns all tags in the database. - * - * @return list of all tags in the database - */ private Collection getAllTags() { - FunctionManagerDB functionManager = (FunctionManagerDB) program.getFunctionManager(); - return functionManager.getFunctionTagManager().getAllFunctionTags(); + return provider.backgroundLoadTags(); } - /** - * Returns true if a tag is in the given list. - * - * @param name the tag name to search for - * @param tags the list to inspect - * @return true if found - */ private boolean tagExists(String name, Collection tags) { FunctionTag tag = getTagForName(name, tags); return tag != null; } - /** - * Returns the {@link FunctionTag} object with the given name. - * - * @param name the tag name - * @param tags the list to inspect - * @return function tag, or null if not found - */ private FunctionTag getTagForName(String name, Collection tags) { for (FunctionTag tag : tags) { if (tag.getName().equals(name)) { diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/HintTextField.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/HintTextField.java index 21f33dde8e..7ed92cc045 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/HintTextField.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/HintTextField.java @@ -56,7 +56,7 @@ public class HintTextField extends JTextField { * * @param hint the hint text * @param required true if the field should be marked as required - */ + */ public HintTextField(String hint, boolean required) { this(hint, required, null); } @@ -81,14 +81,14 @@ public class HintTextField extends JTextField { * Key listener allows us to check field validity on every key typed */ public void addListeners() { - - getDocument().addDocumentListener( new DocumentListener() { + + getDocument().addDocumentListener(new DocumentListener() { @Override public void removeUpdate(DocumentEvent e) { validateField(); } - + @Override public void insertUpdate(DocumentEvent e) { validateField(); @@ -119,18 +119,20 @@ public class HintTextField extends JTextField { public void paintComponent(Graphics g) { super.paintComponent(g); - if (getText().isEmpty()) { - if (g instanceof Graphics2D) { - Graphics2D g2 = (Graphics2D) g; - g2.setColor(Color.lightGray); - g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, - RenderingHints.VALUE_ANTIALIAS_ON); - - if (hint != null) { - g2.drawString(hint, 7, 19); - } - } + if (!getText().isEmpty() || hint == null) { + return; } + + Graphics2D g2 = (Graphics2D) g; + g2.setColor(Color.LIGHT_GRAY); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + + Rectangle bounds = getBounds(); + int x = 10; // offset + int y = bounds.height - bounds.y; // baseline of text; bottom of the text field + + g2.drawString(hint, x, y); } /** diff --git a/Ghidra/Framework/Generic/src/main/java/generic/concurrent/ConcurrentListenerSet.java b/Ghidra/Framework/Generic/src/main/java/generic/concurrent/ConcurrentListenerSet.java index 8ffb40b034..355e97e147 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/concurrent/ConcurrentListenerSet.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/concurrent/ConcurrentListenerSet.java @@ -1,6 +1,5 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +15,7 @@ */ package generic.concurrent; -import java.util.Iterator; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; /** @@ -26,7 +25,7 @@ import java.util.concurrent.ConcurrentHashMap; public class ConcurrentListenerSet implements Iterable { // we use a ConcurrentHashMap because Java has no ConcurrentHashSet - private ConcurrentHashMap storage = new ConcurrentHashMap(); + private ConcurrentHashMap storage = new ConcurrentHashMap<>(); public void add(T t) { storage.put(t, t); @@ -44,4 +43,8 @@ public class ConcurrentListenerSet implements Iterable { public Iterator iterator() { return storage.keySet().iterator(); } + + public List asList() { + return new ArrayList<>(storage.keySet()); + } } diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/function/FunctionDB.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/function/FunctionDB.java index 67d28beb6f..7d1131530e 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/function/FunctionDB.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/function/FunctionDB.java @@ -283,8 +283,9 @@ public class FunctionDB extends DatabaseObject implements Function { manager.lock.acquire(); try { checkIsValid(); - return manager.getCodeManager().getComment(CodeUnit.REPEATABLE_COMMENT, - getEntryPoint()); + return manager.getCodeManager() + .getComment(CodeUnit.REPEATABLE_COMMENT, + getEntryPoint()); } finally { manager.lock.release(); @@ -301,8 +302,9 @@ public class FunctionDB extends DatabaseObject implements Function { manager.lock.acquire(); try { checkDeleted(); - manager.getCodeManager().setComment(getEntryPoint(), CodeUnit.REPEATABLE_COMMENT, - comment); + manager.getCodeManager() + .setComment(getEntryPoint(), CodeUnit.REPEATABLE_COMMENT, + comment); } finally { manager.lock.release(); @@ -885,8 +887,9 @@ public class FunctionDB extends DatabaseObject implements Function { } } } - program.getBookmarkManager().setBookmark(getEntryPoint(), BookmarkType.ERROR, - "Bad Variables Removed", "Removed " + badSymbols.size() + " bad variables"); + program.getBookmarkManager() + .setBookmark(getEntryPoint(), BookmarkType.ERROR, + "Bad Variables Removed", "Removed " + badSymbols.size() + " bad variables"); for (Symbol s : badSymbols) { s.delete(); } @@ -2703,8 +2706,10 @@ public class FunctionDB extends DatabaseObject implements Function { callFixupMap.remove(entryPoint); } else { - if (program.getCompilerSpec().getPcodeInjectLibrary().getPayload( - InjectPayload.CALLFIXUP_TYPE, name, null, null) == null) { + if (program.getCompilerSpec() + .getPcodeInjectLibrary() + .getPayload( + InjectPayload.CALLFIXUP_TYPE, name, null, null) == null) { Msg.warn(this, "Undefined CallFixup set at " + entryPoint + ": " + name); } callFixupMap.add(entryPoint, name); @@ -2815,13 +2820,12 @@ public class FunctionDB extends DatabaseObject implements Function { tag = tagManager.createFunctionTag(name, ""); } - FunctionTagMappingAdapter mappingAdapter = tagManager.getFunctionTagMappingAdapter(); - if (mappingAdapter.getRecord(getID(), tag.getId()) == null) { - mappingAdapter.createFunctionTagRecord(getID(), tag.getId()); + if (!tagManager.isTagApplied(getID(), tag.getId())) { + tagManager.applyFunctionTag(getID(), tag.getId()); Address addr = getEntryPoint(); - program.setChanged(ChangeManager.DOCR_TAG_ADDED_TO_FUNCTION, addr, addr, null, - null); + program.setChanged(ChangeManager.DOCR_TAG_ADDED_TO_FUNCTION, addr, addr, tag, + tag); } // Add to local cache @@ -2829,9 +2833,6 @@ public class FunctionDB extends DatabaseObject implements Function { tags.add(tag); } } - catch (IOException e) { - manager.dbError(e); - } finally { endUpdate(); manager.lock.release(); @@ -2853,13 +2854,12 @@ public class FunctionDB extends DatabaseObject implements Function { FunctionTagManagerDB tagManager = (FunctionTagManagerDB) manager.getFunctionTagManager(); - FunctionTagMappingAdapter mappingAdapter = tagManager.getFunctionTagMappingAdapter(); - boolean removed = mappingAdapter.removeFunctionTagRecord(getID(), tag.getId()); + boolean removed = tagManager.removeFunctionTag(getID(), tag.getId()); if (removed) { Address addr = getEntryPoint(); - program.setChanged(ChangeManager.DOCR_TAG_REMOVED_FROM_FUNCTION, addr, addr, null, - null); + program.setChanged(ChangeManager.DOCR_TAG_REMOVED_FROM_FUNCTION, addr, addr, tag, + tag); // Remove from the local cache. if (tags != null) { @@ -2867,9 +2867,6 @@ public class FunctionDB extends DatabaseObject implements Function { } } } - catch (IOException e) { - manager.dbError(e); - } finally { endUpdate(); manager.lock.release(); diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/function/FunctionManagerDB.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/function/FunctionManagerDB.java index 51c9e5d443..96a6cf0018 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/function/FunctionManagerDB.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/function/FunctionManagerDB.java @@ -21,7 +21,8 @@ import java.util.function.Predicate; import db.*; import generic.FilteredIterator; -import ghidra.program.database.*; +import ghidra.program.database.DBObjectCache; +import ghidra.program.database.ProgramDB; import ghidra.program.database.code.CodeManager; import ghidra.program.database.external.ExternalLocationDB; import ghidra.program.database.map.AddressMap; @@ -48,7 +49,7 @@ import ghidra.util.task.TaskMonitor; * all function related calls are routed to this class. * */ -public class FunctionManagerDB implements ManagerDB, FunctionManager { +public class FunctionManagerDB implements FunctionManager { private final String CALLFIXUP_MAP = "CallFixup"; // string map used to store call-fixup name @@ -64,7 +65,7 @@ public class FunctionManagerDB implements ManagerDB, FunctionManager { private NamespaceManager namespaceMgr; private SymbolManager symbolMgr; private CodeManager codeMgr; - private FunctionTagManager functionTagManager; + private FunctionTagManagerDB functionTagManager; private Namespace globalNamespace; private Predicate functionFilter = f -> { @@ -352,7 +353,7 @@ public class FunctionManagerDB implements ManagerDB, FunctionManager { throw new IllegalArgumentException( "Function entryPoint may not be created on defined data"); } - + if (namespaceMgr.overlapsNamespace(body) != null) { throw new OverlappingFunctionException(entryPoint); } @@ -552,7 +553,7 @@ public class FunctionManagerDB implements ManagerDB, FunctionManager { // Remove all tag mappings associated with this function for (FunctionTag tag : function.getTags()) { - tag.delete(); + function.removeTag(tag.getName()); } long functionID = function.getID(); @@ -930,6 +931,7 @@ public class FunctionManagerDB implements ManagerDB, FunctionManager { public void invalidateCache(boolean all) { lock.acquire(); try { + functionTagManager.invalidateCache(); callFixupMap = null; lastFuncID = -1; cache.invalidate(); @@ -996,14 +998,16 @@ public class FunctionManagerDB implements ManagerDB, FunctionManager { */ FunctionIteratorDB(boolean external, boolean forward) { if (external) { - it = program.getSymbolTable().getSymbols( - new AddressSet(AddressSpace.EXTERNAL_SPACE.getMinAddress(), - AddressSpace.EXTERNAL_SPACE.getMaxAddress()), - SymbolType.FUNCTION, forward); + it = program.getSymbolTable() + .getSymbols( + new AddressSet(AddressSpace.EXTERNAL_SPACE.getMinAddress(), + AddressSpace.EXTERNAL_SPACE.getMaxAddress()), + SymbolType.FUNCTION, forward); } else { - it = program.getSymbolTable().getSymbols(program.getMemory(), SymbolType.FUNCTION, - forward); + it = program.getSymbolTable() + .getSymbols(program.getMemory(), SymbolType.FUNCTION, + forward); } } @@ -1144,8 +1148,7 @@ public class FunctionManagerDB implements ManagerDB, FunctionManager { list.add(symbol); } } - for (int i = 0; i < list.size(); i++) { - Symbol symbol = list.get(i); + for (Symbol symbol : list) { symbol.delete(); } } @@ -1477,6 +1480,7 @@ public class FunctionManagerDB implements ManagerDB, FunctionManager { } } + @Override public FunctionTagManager getFunctionTagManager() { return functionTagManager; } diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/function/FunctionTagDB.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/function/FunctionTagDB.java index f372ea3490..241566da4a 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/function/FunctionTagDB.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/function/FunctionTagDB.java @@ -53,9 +53,10 @@ public class FunctionTagDB extends DatabaseObject implements FunctionTag { comment = ""; } - if (!comment.equals(record.getString(FunctionTagAdapter.COMMENT_COL))) { + String oldValue = record.getString(FunctionTagAdapter.COMMENT_COL); + if (!comment.equals(oldValue)) { record.setString(FunctionTagAdapter.COMMENT_COL, comment); - mgr.updateFunctionTag(this); + mgr.updateFunctionTag(this, oldValue, comment); } } @@ -77,9 +78,10 @@ public class FunctionTagDB extends DatabaseObject implements FunctionTag { name = ""; } - if (!name.equals(record.getString(FunctionTagAdapter.NAME_COL))) { + String oldValue = record.getString(FunctionTagAdapter.NAME_COL); + if (!name.equals(oldValue)) { record.setString(FunctionTagAdapter.NAME_COL, name); - mgr.updateFunctionTag(this); + mgr.updateFunctionTag(this, oldValue, name); } } catch (IOException e) { @@ -137,7 +139,7 @@ public class FunctionTagDB extends DatabaseObject implements FunctionTag { // is null, use whatever is in the database. if (rec == null) { try { - rec = mgr.getFunctionTagAdapter().getRecord(key); + rec = mgr.getTagRecord(key); } catch (IOException e) { mgr.dbError(e); diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/function/FunctionTagManagerDB.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/function/FunctionTagManagerDB.java index 022140ce6a..6dc63ecbcc 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/function/FunctionTagManagerDB.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/function/FunctionTagManagerDB.java @@ -18,6 +18,8 @@ package ghidra.program.database.function; import java.io.IOException; import java.util.*; +import org.apache.commons.collections4.map.LazyMap; + import db.*; import db.util.ErrorHandler; import ghidra.program.database.DBObjectCache; @@ -25,6 +27,7 @@ import ghidra.program.database.ProgramDB; import ghidra.program.model.listing.*; import ghidra.program.util.ChangeManager; import ghidra.util.Lock; +import ghidra.util.datastruct.Counter; import ghidra.util.exception.CancelledException; import ghidra.util.exception.VersionException; import ghidra.util.task.TaskMonitor; @@ -41,6 +44,8 @@ public class FunctionTagManagerDB implements FunctionTagManager, ErrorHandler { private DBObjectCache cache; + private Map tagCountCache; + protected final Lock lock; /** @@ -53,7 +58,7 @@ public class FunctionTagManagerDB implements FunctionTagManager, ErrorHandler { * @throws VersionException if the database is incompatible with the current * schema * @throws IOException if there is a problem accessing the database. - * @throws CancelledException + * @throws CancelledException if the program loading is cancelled */ FunctionTagManagerDB(DBHandle handle, int openMode, Lock lock, TaskMonitor monitor) throws VersionException, IOException, CancelledException { @@ -62,10 +67,9 @@ public class FunctionTagManagerDB implements FunctionTagManager, ErrorHandler { functionTagAdapter = FunctionTagAdapter.getAdapter(handle, openMode, monitor); functionTagMappingAdapter = FunctionTagMappingAdapter.getAdapter(handle, openMode, monitor); - cache = new DBObjectCache(100); + cache = new DBObjectCache<>(100); } - @Override public void setProgram(Program program) { this.program = (ProgramDB) program; } @@ -172,21 +176,89 @@ public class FunctionTagManagerDB implements FunctionTagManager, ErrorHandler { return null; } - void updateFunctionTag(FunctionTagDB tag) throws IOException { + boolean isTagApplied(long functionId, long tagId) { + + lock.acquire(); + + try { + return functionTagMappingAdapter.getRecord(functionId, tagId) != null; + } + catch (IOException e) { + dbError(e); + } + finally { + lock.release(); + } + return false; + } + + void applyFunctionTag(long functionId, long tagId) { + lock.acquire(); + + try { + FunctionTag tag = getFunctionTag(tagId); + if (tag == null) { + return; // shouldn't happen + } + + functionTagMappingAdapter.createFunctionTagRecord(functionId, tagId); + incrementCountCache(tag); + } + catch (IOException e) { + dbError(e); + } + finally { + lock.release(); + } + } + + private void incrementCountCache(FunctionTag tag) { + if (tagCountCache != null) { + tagCountCache.get(tag).count++; + } + } + + private void decrementCountCache(FunctionTag tag) { + if (tagCountCache != null) { + tagCountCache.get(tag).count--; + } + } + + boolean removeFunctionTag(long functionId, long tagId) { + + lock.acquire(); + + try { + FunctionTag tag = getFunctionTag(tagId); + if (tag == null) { + return false; // shouldn't happen + } + + if (functionTagMappingAdapter.removeFunctionTagRecord(functionId, tagId)) { + decrementCountCache(tag); + return true; + } + } + catch (IOException e) { + dbError(e); + } + finally { + lock.release(); + } + return false; + } + + void updateFunctionTag(FunctionTagDB tag, String oldValue, String newValue) throws IOException { // Update the tag attributes. functionTagAdapter.updateRecord(tag.getRecord()); // Notify subscribers of the change. - fireTagChangedNotification(ChangeManager.DOCR_FUNCTION_TAG_CHANGED, tag); + fireTagChangedNotification(ChangeManager.DOCR_FUNCTION_TAG_CHANGED, tag, oldValue, + newValue); invalidateFunctions(); } - /* (non-Javadoc) - * TODO: Be a bit smarter about this - if the cache contains all the tags in - * the db we can just return the cache itself. If not, then go to the - * db to get the remainder. - */ @Override public List getAllFunctionTags() { @@ -211,27 +283,25 @@ public class FunctionTagManagerDB implements FunctionTagManager, ErrorHandler { return Collections.emptyList(); } - public FunctionTagAdapter getFunctionTagAdapter() { - return functionTagAdapter; + public Record getTagRecord(long id) throws IOException { + return functionTagAdapter.getRecord(id); } - public FunctionTagMappingAdapter getFunctionTagMappingAdapter() { - return functionTagMappingAdapter; - } - - /***************************************************************************** - * PRIVATE METHODS - *****************************************************************************/ +//================================================================================================== +// Private Methods +//================================================================================================== /** * Sends a notification when a tag has been changed (edited or deleted). * * @param type {@link ChangeManager} change type * @param tag the tag that was changed - * @throws IOException + * @param oldValue the old value + * @param newValue the new value */ - private void fireTagChangedNotification(int type, FunctionTag tag) throws IOException { - program.tagChanged(tag, type, null, null); + private void fireTagChangedNotification(int type, FunctionTag tag, String oldValue, + String newValue) { + program.tagChanged(tag, type, oldValue, newValue); } /** @@ -239,9 +309,8 @@ public class FunctionTagManagerDB implements FunctionTagManager, ErrorHandler { * * @param type {@link ChangeManager} change type * @param tag the tag that was created - * @throws IOException */ - private void fireTagCreatedNotification(int type, FunctionTag tag) throws IOException { + private void fireTagCreatedNotification(int type, FunctionTag tag) { program.tagCreated(tag, type); } @@ -250,10 +319,9 @@ public class FunctionTagManagerDB implements FunctionTagManager, ErrorHandler { * * @param type the type of change * @param tag the tag that was deleted - * @throws IOException */ - private void fireTagDeletedNotification(int type, FunctionTag tag) throws IOException { - program.tagChanged(tag, type, null, null); + private void fireTagDeletedNotification(int type, FunctionTag tag) { + program.tagChanged(tag, type, tag, null); } /** @@ -275,6 +343,7 @@ public class FunctionTagManagerDB implements FunctionTagManager, ErrorHandler { * Deletes the given function tag. * * @param tag the tag to delete + * @throws IOException if there is an issue reading from the db */ void doDeleteTag(FunctionTag tag) throws IOException { @@ -304,20 +373,56 @@ public class FunctionTagManagerDB implements FunctionTagManager, ErrorHandler { * Returns all function tags associated with the given function id. * * @param functionId the function id - * @return - * @throws IOException + * @return the tags + * @throws IOException if there is an issue reading from the db */ Set getFunctionTagsByFunctionID(long functionId) throws IOException { Set tags = new HashSet<>(); - RecordIterator functionTagMappingRecords = + RecordIterator functionRecords = functionTagMappingAdapter.getRecordsByFunctionID(functionId); - while (functionTagMappingRecords.hasNext()) { - Record mappingRecord = functionTagMappingRecords.next(); + while (functionRecords.hasNext()) { + Record mappingRecord = functionRecords.next(); Record tagRecord = functionTagAdapter.getRecord( mappingRecord.getLongValue(FunctionTagMappingAdapter.TAG_ID_COL)); tags.add(getFunctionTagFromCache(tagRecord)); } return tags; } + + void invalidateCache() { + cache.invalidate(); + tagCountCache = null; + } + + @Override + public int getUseCount(FunctionTag tag) { + lock.acquire(); + try { + if (tagCountCache == null) { + buildTagCountCache(); + } + Counter counter = tagCountCache.get(tag); + return counter.count; + } + catch (IOException e) { + dbError(e); + return 0; + } + finally { + lock.release(); + } + } + + private void buildTagCountCache() throws IOException { + Map map = LazyMap.lazyMap(new HashMap<>(), () -> new Counter()); + RecordIterator records = functionTagMappingAdapter.getRecords(); + while (records.hasNext()) { + Record mappingRecord = records.next(); + long tagId = mappingRecord.getLongValue(FunctionTagMappingAdapter.TAG_ID_COL); + FunctionTag tag = getFunctionTag(tagId); + map.get(tag).count++; + } + tagCountCache = map; + } } diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/function/FunctionTagMappingAdapter.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/function/FunctionTagMappingAdapter.java index f9c9d03f96..151014c195 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/function/FunctionTagMappingAdapter.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/function/FunctionTagMappingAdapter.java @@ -32,7 +32,7 @@ abstract class FunctionTagMappingAdapter { static final String TABLE_NAME = "Function Tag Map"; static final int CURRENT_VERSION = 0; - + static final int FUNCTION_ID_COL = 0; static final int TAG_ID_COL = 1; @@ -74,7 +74,7 @@ abstract class FunctionTagMappingAdapter { * * @param functionID index into the {@link SymbolTable} table * @return iterator of database records - * @throws IOException + * @throws IOException if database error occurs */ abstract RecordIterator getRecordsByFunctionID(long functionID) throws IOException; @@ -84,17 +84,17 @@ abstract class FunctionTagMappingAdapter { * @param functionID index into the {@link SymbolTable} table * @param tagID index into the {@link FunctionTagAdapter} table * @return null if not found - * @throws IOException + * @throws IOException if database error occurs */ abstract Record getRecord(long functionID, long tagID) throws IOException; /** * Creates a new record with the given function and tag ID's. * - * @param functionID - * @param tagID + * @param functionID the function's database id + * @param tagID the FunctionTags database id * @return newly-created database record - * @throws IOException + * @throws IOException if database error occurs */ abstract Record createFunctionTagRecord(long functionID, long tagID) throws IOException; @@ -106,7 +106,7 @@ abstract class FunctionTagMappingAdapter { * @param functionID index into the {@link SymbolTable} table * @param tagID index into the {@link FunctionTagAdapter} table * @return true if the remove was performed - * @throws IOException + * @throws IOException if database error occurs */ abstract boolean removeFunctionTagRecord(long functionID, long tagID) throws IOException; @@ -116,16 +116,22 @@ abstract class FunctionTagMappingAdapter { * whenever a tag is being deleted from the system. * * @param tagID index into the {@link FunctionTagAdapter} table - * @throws IOException + * @throws IOException if database error occurs */ abstract void removeFunctionTagRecord(long tagID) throws IOException; /** * Determine if the specified tag ID has been applied to a function - * @param tagId tag ID + * @param id tag ID * @return true if tag applied to one or more functions - * @throws IOException + * @throws IOException if database error occurs */ abstract boolean isTagAssigned(long id) throws IOException; + /** + * Returns a RecordIterator over all the records in this table + * @return a RecordIterator over all the records in this table + * @throws IOException if database error occurs + */ + protected abstract RecordIterator getRecords() throws IOException; } diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/function/FunctionTagMappingAdapterNoTable.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/function/FunctionTagMappingAdapterNoTable.java index 789432521b..71dd0a0fe3 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/function/FunctionTagMappingAdapterNoTable.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/function/FunctionTagMappingAdapterNoTable.java @@ -65,4 +65,9 @@ class FunctionTagMappingAdapterNoTable extends FunctionTagMappingAdapter { boolean isTagAssigned(long id) throws IOException { return false; } + + @Override + protected RecordIterator getRecords() throws IOException { + return new EmptyRecordIterator(); + } } diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/function/FunctionTagMappingAdapterV0.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/function/FunctionTagMappingAdapterV0.java index 6bc1de6842..0b5ece9e93 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/function/FunctionTagMappingAdapterV0.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/function/FunctionTagMappingAdapterV0.java @@ -147,6 +147,14 @@ class FunctionTagMappingAdapterV0 extends FunctionTagMappingAdapter implements D return table.indexIterator(V0_FUNCTION_ID_COL, value, value, true); } + @Override + protected RecordIterator getRecords() throws IOException { + if (table == null) { + return new EmptyRecordIterator(); + } + return table.iterator(); + } + @Override boolean isTagAssigned(long tagID) throws IOException { if (table == null) { diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/Function.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/Function.java index 3bd8278a61..d8e0f388b4 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/Function.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/Function.java @@ -29,10 +29,7 @@ import ghidra.util.exception.InvalidInputException; import ghidra.util.task.TaskMonitor; /** - * Interface to define methods available on a function. Functions - * have a single entry point. - * - * + * Interface to define methods available on a function. Functions have a single entry point. */ public interface Function extends Namespace { @@ -110,8 +107,8 @@ public interface Function extends Namespace { public void setCallFixup(String name); /** - * Returns the current call-fixup name set on this instruction or null - * if one has not been set. + * Returns the current call-fixup name set on this instruction or null if one has not been set + * @return the name */ public String getCallFixup(); @@ -131,6 +128,7 @@ public interface Function extends Namespace { /** * Returns the function (same as plate) comment as an array of strings where * each item in the array is a line of text in the comment. + * @return the comments */ public String[] getCommentAsArray(); @@ -194,11 +192,15 @@ public interface Function extends Namespace { /** * Set the return data-type and storage. - *

NOTE: The storage and source are ignored if the function does not have custom storage enabled. - * @param type - * @param storage + * + *

NOTE: The storage and source are ignored if the function does not have custom storage + * enabled. + * + * @param type the data type + * @param storage the storage * @param source source to be combined with the overall signature source. - * @throws InvalidInputException if data type is not a fixed length or storage is improperly sized + * @throws InvalidInputException if data type is not a fixed length or storage is improperly + * sized */ public void setReturn(DataType type, VariableStorage storage, SourceType source) throws InvalidInputException; @@ -216,21 +218,22 @@ public interface Function extends Namespace { /** * Get the function's signature. - *

WARNING! It is important to note that the calling convention may not be properly retained - * by the returned signature object if a non-generic calling convention is used by this function as - * defined by the program's compiler specification. + *

WARNING! It is important to note that the calling convention may not be properly + * retained by the returned signature object if a non-generic calling convention is used by + * this function as defined by the program's compiler specification. + * * @param formalSignature if true only original raw types will be retained and * auto-params discarded (e.g., this, __return_storage_ptr__, etc.) within the returned * signature. If false, the effective signature will be returned where forced indirect * and auto-params are reflected in the signature. This option has no affect if the specified * function has custom storage enabled. - * * @return the function's signature */ public FunctionSignature getSignature(boolean formalSignature); /** * Return a string representation of the function signature + * * @param formalSignature if true only original raw return/parameter types will be retained and * auto-params discarded (e.g., this, __return_storage_ptr__, etc.) within the returned * signature. If false, the effective signature will be returned where forced indirect @@ -238,6 +241,7 @@ public interface Function extends Namespace { * function has custom storage enabled. * @param includeCallingConvention if true prototype will include call convention * declaration if known. + * @return the prototype */ public String getPrototypeString(boolean formalSignature, boolean includeCallingConvention); @@ -464,6 +468,7 @@ public interface Function extends Namespace { * @param fromOrdinal from ordinal position using the current numbering * @param toOrdinal the final position of the specified parameter * @return parameter which was moved + * @throws InvalidInputException if either ordinal is invalid * @deprecated The use of this method is discouraged. The function signature should generally be * adjusted with a single call to {@link #updateFunction(String, Variable, List, FunctionUpdateType, boolean, SourceType)} */ @@ -520,7 +525,7 @@ public interface Function extends Namespace { /** * Returns an array of all local and parameter variables - * @return + * @return the variables */ public Variable[] getAllVariables(); @@ -533,6 +538,7 @@ public interface Function extends Namespace { * @return the Variable added to the program. * @throws DuplicateNameException if another local variable or parameter already * has that name. + * @throws InvalidInputException if there is an error or conflict when resolving the variable */ public Variable addLocalVariable(Variable var, SourceType source) throws DuplicateNameException, InvalidInputException; @@ -553,14 +559,16 @@ public interface Function extends Namespace { public void setBody(AddressSetView newBody) throws OverlappingFunctionException; /** - * Returns true if this function has a variable argument list (VarArgs). + * Returns true if this function has a variable argument list (VarArgs) + * @return true if this function has a variable argument list (VarArgs) */ public boolean hasVarArgs(); /** - * Set whether parameters can be passed as a VarArg (variable argument list). + * Set whether parameters can be passed as a VarArg (variable argument list) * - * @param hasVarArgs true if this function has a variable argument list (ie printf(fmt, ...)). + * @param hasVarArgs true if this function has a variable argument list + * (e.g., printf(fmt, ...)). */ public void setVarArgs(boolean hasVarArgs); @@ -595,7 +603,7 @@ public interface Function extends Namespace { /** * Set whether or not this function uses custom variable storage - * @param hasCustomVariableStorage + * @param hasCustomVariableStorage true if this function uses custom storage */ public void setCustomVariableStorage(boolean hasCustomVariableStorage); diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/FunctionManager.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/FunctionManager.java index 6414529a08..be598d8661 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/FunctionManager.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/FunctionManager.java @@ -15,22 +15,29 @@ */ package ghidra.program.model.listing; -import java.io.IOException; import java.util.Iterator; import java.util.List; -import ghidra.program.database.ProgramDB; +import ghidra.program.database.ManagerDB; import ghidra.program.database.function.OverlappingFunctionException; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressSetView; import ghidra.program.model.lang.PrototypeModel; import ghidra.program.model.symbol.Namespace; import ghidra.program.model.symbol.SourceType; -import ghidra.util.exception.*; +import ghidra.util.exception.CancelledException; +import ghidra.util.exception.InvalidInputException; import ghidra.util.task.TaskMonitor; -public interface FunctionManager { +/** + * The manager for functions + */ +public interface FunctionManager extends ManagerDB { + /** + * Returns this manager's program + * @return the program + */ public Program getProgram(); /** @@ -50,8 +57,8 @@ public interface FunctionManager { public PrototypeModel getDefaultCallingConvention(); /** - * Gets the prototype model of the calling convention with the specified name in this program. - * + * Gets the prototype model of the calling convention with the specified name in this program + * @param name the calling convention name * @return the named function calling convention prototype model or null. */ public PrototypeModel getCallingConvention(String name); @@ -113,25 +120,28 @@ public interface FunctionManager { throws OverlappingFunctionException; /** - * Returns the total number of functions in the program including external functions. + * Returns the total number of functions in the program including external functions + * @return the count */ public int getFunctionCount(); /** - * Remove a function defined at entryPoint. + * Remove a function defined at entryPoint + * @param entryPoint the entry point + * @return true if the function was removed */ public boolean removeFunction(Address entryPoint); /** - * Get the function at entryPoint. - * - * @return null if there is no function at entryPoint. + * Get the function at entryPoint + * @param entryPoint the entry point + * @return null if there is no function at entryPoint */ public Function getFunctionAt(Address entryPoint); /** - * Get the function which resides at the specified address or is referenced from the specified - * address. + * Get the function which resides at the specified address or is referenced from the specified + * address * * @param address function address or address of pointer to a function. * @return referenced function or null @@ -147,19 +157,18 @@ public interface FunctionManager { public Function getFunctionContaining(Address addr); /** - * Returns an iterator over all non-external functions in address (entry point) order. - * + * Returns an iterator over all non-external functions in address (entry point) order * @param forward true means to iterate in ascending address order + * @return the iterator */ public FunctionIterator getFunctions(boolean forward); /** * Get an iterator over non-external functions starting at an address and ordered by entry - * address. + * address * * @param start starting address * @param forward true means to iterate in ascending address order - * * @return an iterator over functions. */ public FunctionIterator getFunctions(Address start, boolean forward); @@ -176,9 +185,10 @@ public interface FunctionManager { /** * Returns an iterator over all REAL functions in address (entry point) order (real functions - * have instructions, and aren't stubs). + * have instructions, and aren't stubs) * * @param forward true means to iterate in ascending address order + * @return the iterator */ public FunctionIterator getFunctionsNoStubs(boolean forward); @@ -220,39 +230,6 @@ public interface FunctionManager { */ public boolean isInFunction(Address addr); - /** - * @see ghidra.program.database.ManagerDB#moveAddressRange(ghidra.program.model.address.Address, - * ghidra.program.model.address.Address, long, ghidra.util.task.TaskMonitor) - */ - public void moveAddressRange(Address fromAddr, Address toAddr, long length, TaskMonitor monitor) - throws CancelledException; - - /** - * @see ghidra.program.database.ManagerDB#deleteAddressRange(ghidra.program.model.address.Address, - * ghidra.program.model.address.Address, ghidra.util.task.TaskMonitor) - */ - public void deleteAddressRange(Address startAddr, Address endAddr, TaskMonitor monitor) - throws CancelledException; - - /** - * @see ghidra.program.database.ManagerDB#setProgram(ghidra.program.database.ProgramDB) - */ - public void setProgram(ProgramDB program); - - /** - * @param currentRevision TODO - * @see ghidra.program.database.ManagerDB#programReady(int, int, ghidra.util.task.TaskMonitor) - */ - public void programReady(int openMode, int currentRevision, TaskMonitor monitor) - throws IOException, CancelledException; - - /* - * (non-Javadoc) - * - * @see ghidra.program.database.ManagerDB#invalidateCache(boolean) - */ - public void invalidateCache(boolean all); - /** * Return an iterator over functions that overlap the given address set. * @@ -268,10 +245,10 @@ public interface FunctionManager { * function. While this does not account for the actual instruction flow, it is hopefully * accurate enough for most situations. * - * @param instrAddr - * @param storageAddr + * @param instrAddr the instruction address + * @param storageAddr the storage address * @param size varnode size in bytes (1 is assumed if value <= 0) - * @param isRead + * @param isRead true if the reference is a read reference * @return referenced variable or null if one not found */ public Variable getReferencedVariable(Address instrAddr, Address storageAddr, int size, @@ -284,4 +261,30 @@ public interface FunctionManager { */ public Function getFunction(long key); + /** + * Returns the function tag manager + * @return the function tag manager + */ + public FunctionTagManager getFunctionTagManager(); + + /** + * Clears all data caches + * @param all if false, some managers may not need to update their cache if they can + * tell that its not necessary. If this flag is true, then all managers should clear + * their cache no matter what. + */ + @Override + public void invalidateCache(boolean all); // note: redeclared to not throw an exception + + /** + * Move all objects within an address range to a new location + * @param fromAddr the first address of the range to be moved + * @param toAddr the address where to the range is to be moved + * @param length the number of addresses to move + * @param monitor the task monitor to use in any upgrade operations + * @throws CancelledException if the user cancelled the operation via the task monitor + */ + @Override + void moveAddressRange(Address fromAddr, Address toAddr, long length, TaskMonitor monitor) + throws CancelledException; // note: redeclared to not throw an AddressOverflowException } diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/FunctionTagManager.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/FunctionTagManager.java index c824c4a29f..6add062838 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/FunctionTagManager.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/FunctionTagManager.java @@ -18,39 +18,39 @@ package ghidra.program.model.listing; import java.util.List; /** - * Interface for managing function tags. Tags are simple objects consisting of - * a name and an optional comment, which can be applied to functions in Ghidra. + * Interface for managing function tags. Tags are simple objects consisting of a name and an + * optional comment, which can be applied to functions. * - * @see ghidra.program.database.function.FunctionTagAdapter FunctionTagAdapter - * @see ghidra.program.database.function.FunctionTagMappingAdapter FunctionTagMappingAdapter + * See ghidra.program.database.function.FunctionTagAdapter + * See ghidra.program.database.function.FunctionTagMappingAdapter */ public interface FunctionTagManager { /** - * Returns the function tag with the given name. + * Returns the function tag with the given name * * @param name the tag name * @return the function tag, or null if not found */ - FunctionTag getFunctionTag(String name); + public FunctionTag getFunctionTag(String name); /** - * Returns the function tag with the given database id. + * Returns the function tag with the given database id * * @param id the tags database id * @return the function tag, or null if not found */ - FunctionTag getFunctionTag(long id); + public FunctionTag getFunctionTag(long id); /** - * Returns all function tags in the database. + * Returns all function tags in the database * * @return list of function tags */ - List getAllFunctionTags(); + public List getAllFunctionTags(); /** - * Returns true if the given tag is assigned to a function. + * Returns true if the given tag is assigned to a function * * @param name the tag name * @return true if assigned to a function @@ -65,13 +65,12 @@ public interface FunctionTagManager { * @param comment the comment associated with the tag (optional) * @return the new function tag */ - FunctionTag createFunctionTag(String name, String comment); + public FunctionTag createFunctionTag(String name, String comment); /** - * Sets the program on this manager to allow the manager to persist changes - * and notify subscribers. - * - * @param program the program + * Returns the number of times the given tag has been applied to a function + * @param tag the tag + * @return the count */ - void setProgram(Program program); + public int getUseCount(FunctionTag tag); } diff --git a/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/model/FunctionManagerTestDouble.java b/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/model/FunctionManagerTestDouble.java index e4d918be73..e82f26d818 100644 --- a/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/model/FunctionManagerTestDouble.java +++ b/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/model/FunctionManagerTestDouble.java @@ -187,4 +187,8 @@ public class FunctionManagerTestDouble implements FunctionManager { throw new UnsupportedOperationException(); } + @Override + public FunctionTagManager getFunctionTagManager() { + throw new UnsupportedOperationException(); + } } diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/FunctionTagPluginScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/FunctionTagPluginScreenShots.java index 7bf45555ba..a1f332cf48 100644 --- a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/FunctionTagPluginScreenShots.java +++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/FunctionTagPluginScreenShots.java @@ -32,58 +32,47 @@ import ghidra.program.model.listing.Function; import ghidra.program.model.listing.FunctionIterator; import ghidra.program.util.ProgramLocation; import ghidra.util.Swing; -import ghidra.util.exception.UsrException; public class FunctionTagPluginScreenShots extends GhidraScreenShotGenerator { @Test public void testFullWindow() { - showProvider(FunctionTagsComponentProvider.class); + showProvider(FunctionTagProvider.class); waitForSwing(); addTableData(); - captureIsolatedProvider(FunctionTagsComponentProvider.class, 950, 400); + captureIsolatedProvider(FunctionTagProvider.class, 950, 400); } @Test public void testInputField() { - showProvider(FunctionTagsComponentProvider.class); + showProvider(FunctionTagProvider.class); waitForSwing(); - FunctionTagsComponentProvider provider = getProvider(FunctionTagsComponentProvider.class); - final JPanel inputPanel = (JPanel) getInstanceField("inputPanel", provider); + FunctionTagProvider provider = getProvider(FunctionTagProvider.class); + final JPanel inputPanel = provider.getInputPanel(); captureComponent(inputPanel); } - @Test - public void testFilterField() { - showProvider(FunctionTagsComponentProvider.class); - waitForSwing(); - FunctionTagsComponentProvider provider = getProvider(FunctionTagsComponentProvider.class); - final JPanel filterPanel = (JPanel) getInstanceField("filterPanel", provider); - captureComponent(filterPanel); - } - /** * For this test the item in row 1 of the available tags table must be an editable * item. If not, edit the function_tags.xml file to remove all items and re-run this. */ @Test public void testEditTag() { - showProvider(FunctionTagsComponentProvider.class); + showProvider(FunctionTagProvider.class); waitForSwing(); addTableData(); - FunctionTagsComponentProvider provider = getProvider(FunctionTagsComponentProvider.class); - SourceTagsPanel sourcePanel = (SourceTagsPanel) getInstanceField("sourcePanel", provider); - FunctionTagTable table = (FunctionTagTable) getInstanceField("table", sourcePanel); + FunctionTagProvider provider = getProvider(FunctionTagProvider.class); + SourceTagsPanel sourcePanel = provider.getSourcePanel(); + FunctionTagTable table = sourcePanel.getTable(); Rectangle bounds = table.getCellRect(7, 0, false); // Cell 7 is an editable item doubleClick(table, bounds.x, bounds.y); - InputDialog warningDialog = waitForDialogComponent(InputDialog.class); - - captureDialog(warningDialog); + InputDialog editDialog = waitForDialogComponent(InputDialog.class); + captureDialog(editDialog); } - /** + /* * Captures the warning dialog when trying to delete a tag. Note that this assumes the * tag in row 1 is NOT read-only. If that's the not the case, modify the function_tags.xml * file to remove any tags that may be interfering with this. @@ -91,12 +80,12 @@ public class FunctionTagPluginScreenShots extends GhidraScreenShotGenerator { * @throws UsrException */ @Test - public void testDeleteWarning() throws UsrException { - showProvider(FunctionTagsComponentProvider.class); + public void testDeleteWarning() { + showProvider(FunctionTagProvider.class); waitForSwing(); addTableData(); - FunctionTagsComponentProvider provider = getProvider(FunctionTagsComponentProvider.class); + FunctionTagProvider provider = getProvider(FunctionTagProvider.class); SourceTagsPanel sourcePanel = (SourceTagsPanel) getInstanceField("sourcePanel", provider); FunctionTagTable table = (FunctionTagTable) getInstanceField("table", sourcePanel); table.setRowSelectionInterval(7, 7); @@ -107,20 +96,20 @@ public class FunctionTagPluginScreenShots extends GhidraScreenShotGenerator { captureDialog(warningDialog); } - /** + /* * Captures the read-only warning when trying to edit a tag * * @throws UsrException */ @Test - public void testEditNotAllowedWarning() throws UsrException { - showProvider(FunctionTagsComponentProvider.class); + public void testEditNotAllowedWarning() { + showProvider(FunctionTagProvider.class); waitForSwing(); addTableData(); - FunctionTagsComponentProvider provider = getProvider(FunctionTagsComponentProvider.class); - SourceTagsPanel sourcePanel = (SourceTagsPanel) getInstanceField("sourcePanel", provider); - FunctionTagTable table = (FunctionTagTable) getInstanceField("table", sourcePanel); + FunctionTagProvider provider = getProvider(FunctionTagProvider.class); + SourceTagsPanel sourcePanel = provider.getSourcePanel(); + FunctionTagTable table = sourcePanel.getTable(); doubleClickItem(table, "LIBRARY"); // pick a known read-only tag OptionDialog warningDialog = waitForDialogComponent(OptionDialog.class); @@ -151,7 +140,7 @@ public class FunctionTagPluginScreenShots extends GhidraScreenShotGenerator { private void addTableData() { - FunctionTagsComponentProvider provider = getProvider(FunctionTagsComponentProvider.class); + FunctionTagProvider provider = getProvider(FunctionTagProvider.class); Swing.runNow(() -> { provider.programActivated(program); @@ -171,7 +160,7 @@ public class FunctionTagPluginScreenShots extends GhidraScreenShotGenerator { * * @param provider the component provider */ - private void navigateToFunction(FunctionTagsComponentProvider provider) { + private void navigateToFunction(FunctionTagProvider provider) { FunctionIterator iter = program.getFunctionManager().getFunctions(true); while (iter.hasNext()) { Function func = iter.next();