diff --git a/Ghidra/Features/Base/certification.manifest b/Ghidra/Features/Base/certification.manifest index 74fa75a598..a9b0a2613b 100644 --- a/Ghidra/Features/Base/certification.manifest +++ b/Ghidra/Features/Base/certification.manifest @@ -516,6 +516,7 @@ src/main/help/help/topics/Intro/images/Simple_err_dialog.png||GHIDRA||||END| src/main/help/help/topics/LabelMgrPlugin/FieldNames.htm||GHIDRA||||END| src/main/help/help/topics/LabelMgrPlugin/Labels.htm||GHIDRA||||END| src/main/help/help/topics/LabelMgrPlugin/images/AddLabel.png||GHIDRA||||END| +src/main/help/help/topics/LabelMgrPlugin/images/ChooseNamespace.png||GHIDRA||||END| src/main/help/help/topics/LabelMgrPlugin/images/EditFieldNameDialog.png||GHIDRA||||END| src/main/help/help/topics/LabelMgrPlugin/images/LabelHistoryInputDialog.png||GHIDRA||||END| src/main/help/help/topics/LabelMgrPlugin/images/SetLabel.png||GHIDRA||||END| diff --git a/Ghidra/Features/Base/src/main/help/help/topics/DataTypeEditors/images/UnionEditorPacked.png b/Ghidra/Features/Base/src/main/help/help/topics/DataTypeEditors/images/UnionEditorPacked.png index 66b0cecfdb..4a9fc8d117 100644 Binary files a/Ghidra/Features/Base/src/main/help/help/topics/DataTypeEditors/images/UnionEditorPacked.png and b/Ghidra/Features/Base/src/main/help/help/topics/DataTypeEditors/images/UnionEditorPacked.png differ diff --git a/Ghidra/Features/Base/src/main/help/help/topics/LabelMgrPlugin/Labels.htm b/Ghidra/Features/Base/src/main/help/help/topics/LabelMgrPlugin/Labels.htm index a619434162..afb0bfa9bd 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/LabelMgrPlugin/Labels.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/LabelMgrPlugin/Labels.htm @@ -137,8 +137,6 @@

Enter Label

-

Namespace

- +

The containing namespace for the label or function.

+

The applicable namespaces are based upon the current address and symbol type. + The namespace combobox + will initially be populated with the most obvious choices for that location, as well + any recently chosen namespaces.

+

Next to the combobox, there is a browse button which can be pressed to bring up the + Namespace Chooser Dialog. From this chooser dialog, + any namespace in the program can be selected. However, not all namespaces are applicable + to all locations. For example, you can't put a function into another function's namespace. + If the namespace is not applicable to the current location, the selected namespace will + still appear in the namespace field, but you will get an error when the OK button is + pressed.

-

Note:This field is disabled, if there is a - function with a default name at this address. The namespace will stay set to the parent - namespace of the function and the label name you enter will become the new function - name.
+

Note:This field is disabled when the namespace + at that location can't be changed. For example, parameters and local variables can only + have the enclosing function as its namespace.

@@ -186,38 +185,66 @@

Entry Point

- +

Primary

- + function symbol must always be the primary symbol. +

Pinned

- + data, or function labels may be pinned.
+

Namespace Chooser Dialog

+
+ +

The Namespace Chooser Dialog allows you to choose namespaces that have been defined + in the current program. It presents a drop-down text field where you can begin typing + the name of a namespace and a drop-down window will appear showing a list of + all namespaces that match the typed text.

+ + +


+ Namespace Chooser Dialog

+ +

There are several modes that determine how the typed text is used to match against all + program namespaces. The mode can be changed by pressing on the symbol(s) shown on the far + right of the text field. You can also change the search mode using Ctrl Down and + Ctrl Up to change the mode forward and backward, respectively. Regardless of the mode, + the searches are not case sensitive.

+

These modes are:

+ + + +
+

Set Label Dialog

diff --git a/Ghidra/Features/Base/src/main/help/help/topics/LabelMgrPlugin/images/AddLabel.png b/Ghidra/Features/Base/src/main/help/help/topics/LabelMgrPlugin/images/AddLabel.png index b95e614926..f3cec4d538 100644 Binary files a/Ghidra/Features/Base/src/main/help/help/topics/LabelMgrPlugin/images/AddLabel.png and b/Ghidra/Features/Base/src/main/help/help/topics/LabelMgrPlugin/images/AddLabel.png differ diff --git a/Ghidra/Features/Base/src/main/help/help/topics/LabelMgrPlugin/images/ChooseNamespace.png b/Ghidra/Features/Base/src/main/help/help/topics/LabelMgrPlugin/images/ChooseNamespace.png new file mode 100644 index 0000000000..9daf3e746f Binary files /dev/null and b/Ghidra/Features/Base/src/main/help/help/topics/LabelMgrPlugin/images/ChooseNamespace.png differ diff --git a/Ghidra/Features/Base/src/main/help/help/topics/LabelMgrPlugin/images/EditFieldNameDialog.png b/Ghidra/Features/Base/src/main/help/help/topics/LabelMgrPlugin/images/EditFieldNameDialog.png index 8e51de9e6a..7723b0d3ca 100644 Binary files a/Ghidra/Features/Base/src/main/help/help/topics/LabelMgrPlugin/images/EditFieldNameDialog.png and b/Ghidra/Features/Base/src/main/help/help/topics/LabelMgrPlugin/images/EditFieldNameDialog.png differ diff --git a/Ghidra/Features/Base/src/main/help/help/topics/LabelMgrPlugin/images/LabelHistoryInputDialog.png b/Ghidra/Features/Base/src/main/help/help/topics/LabelMgrPlugin/images/LabelHistoryInputDialog.png index 01f3248f2a..c891ef782b 100644 Binary files a/Ghidra/Features/Base/src/main/help/help/topics/LabelMgrPlugin/images/LabelHistoryInputDialog.png and b/Ghidra/Features/Base/src/main/help/help/topics/LabelMgrPlugin/images/LabelHistoryInputDialog.png differ diff --git a/Ghidra/Features/Base/src/main/help/help/topics/LabelMgrPlugin/images/SetLabel.png b/Ghidra/Features/Base/src/main/help/help/topics/LabelMgrPlugin/images/SetLabel.png index 5f0a37ab7a..88c2b07ae2 100644 Binary files a/Ghidra/Features/Base/src/main/help/help/topics/LabelMgrPlugin/images/SetLabel.png and b/Ghidra/Features/Base/src/main/help/help/topics/LabelMgrPlugin/images/SetLabel.png differ diff --git a/Ghidra/Features/Base/src/main/help/help/topics/LabelMgrPlugin/images/ShowLabelHistory.png b/Ghidra/Features/Base/src/main/help/help/topics/LabelMgrPlugin/images/ShowLabelHistory.png index 8c76192c03..dd82d15cdf 100644 Binary files a/Ghidra/Features/Base/src/main/help/help/topics/LabelMgrPlugin/images/ShowLabelHistory.png and b/Ghidra/Features/Base/src/main/help/help/topics/LabelMgrPlugin/images/ShowLabelHistory.png differ diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/AddEditDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/AddEditDialog.java index 108a3b203c..96d4edf86a 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/AddEditDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/AddEditDialog.java @@ -27,6 +27,7 @@ import org.apache.commons.lang3.StringUtils; import docking.ComponentProvider; import docking.ReusableDialogComponentProvider; import docking.widgets.OptionDialog; +import docking.widgets.button.BrowseButton; import docking.widgets.checkbox.GCheckBox; import docking.widgets.combobox.GhidraComboBox; import ghidra.app.cmd.label.*; @@ -46,6 +47,7 @@ import ghidra.util.layout.VerticalLayout; */ public class AddEditDialog extends ReusableDialogComponentProvider { private static final int MAX_RETENTION = 10; + private PluginTool tool; private TitledBorder nameBorder; private GhidraComboBox labelNameChoices; @@ -337,55 +339,65 @@ public class AddEditDialog extends ReusableDialogComponentProvider { } } - // This method only gets the namespace associated with the current address - // and it's tree of namespaces. It does not walk the namespace tree of - // the symbol, which can be different than that of the address. private void initNamespaces() { namespaceChoices.removeAllItems(); + for (Namespace namespace : getSelectableNamespaces()) { + namespaceChoices.addItem(new NamespaceWrapper(namespace)); + } + } + + private Collection getSelectableNamespaces() { if (!namespaceChoices.isEnabled()) { - namespaceChoices.addItem(new NamespaceWrapper(symbol.getParentNamespace())); - selectNamespace(); + return List.of(symbol.getParentNamespace()); + } + + SequencedSet namespaces = new LinkedHashSet<>(); + addGlobalNamespace(namespaces); + addCurrentNamespace(namespaces); + addSuggestedNamespace(namespaces); + addRecentNamespaces(namespaces); + + return namespaces; + } + + private void addRecentNamespaces(SequencedSet namespaces) { + List recentNamespaces = NamespaceCache.get(program); + if (recentNamespaces == null) { return; } + for (Namespace namespace : recentNamespaces) { + if (!namespaces.contains(namespace)) { + namespaces.add(namespace); + } + } + } - Collection collection = new HashSet<>(); + private void addSuggestedNamespace(SequencedSet namespaces) { + Namespace namespace = program.getSymbolTable().getNamespace(addr); + if (namespace == null) { + return; + } + // Don't include the currently edited symbol as a possible choice + if (symbol != null && namespace.equals(symbol.getObject())) { + return; + } + if (!namespaces.contains(namespace)) { + namespaces.add(namespace); + } + } - // we always add the global namespace + private void addGlobalNamespace(SequencedSet namespaces) { Namespace globalNamespace = program.getGlobalNamespace(); - - NamespaceWrapper composite = new NamespaceWrapper(globalNamespace); - namespaceChoices.addItem(composite); - collection.add(composite); - - Namespace currentNamespace = program.getSymbolTable().getNamespace(addr); - - // no symbol or not editing a function symbol - if ((symbol == null) || (symbol != null && symbol.getSymbolType() != SymbolType.FUNCTION)) { - // walk the tree of namespaces and collect all of the items - for (; (currentNamespace != globalNamespace); currentNamespace = - currentNamespace.getParentNamespace()) { - composite = new NamespaceWrapper(currentNamespace); - - if (!collection.contains(composite)) { - collection.add(composite); - namespaceChoices.addItem(composite); - } - } + if (!namespaces.contains(globalNamespace)) { + namespaces.add(globalNamespace); } + } + private void addCurrentNamespace(SequencedSet namespaces) { if (symbol != null) { - // we are adding the current namespace of the symbol if it is not in - // the namespace tree that belongs to the address - Namespace symbolNamespace = symbol.getParentNamespace(); - composite = new NamespaceWrapper(symbolNamespace); - if (!collection.contains(composite)) { - collection.add(composite); - namespaceChoices.insertItemAt(composite, 1); - } + namespaces.add(symbol.getParentNamespace()); } - - selectNamespace(); } /** @@ -478,6 +490,7 @@ public class AddEditDialog extends ReusableDialogComponentProvider { namespaceChoices.setEnabled(true); initNamespaces(); + selectNamespace(); clearStatusText(); } @@ -538,6 +551,7 @@ public class AddEditDialog extends ReusableDialogComponentProvider { namespaceChoices.setEnabled(true); } initNamespaces(); + selectNamespace(); clearStatusText(); } @@ -550,12 +564,11 @@ public class AddEditDialog extends ReusableDialogComponentProvider { @Override public Dimension getPreferredSize() { Dimension size = super.getPreferredSize(); - // change the preferred size to use the width determined by the # of columns in - // combo box editor instead of the largest item in the combo box data model to - // prevent the dialog from growing huge when a large label gets added to its recent - // items - Dimension editorSize = getEditor().getEditorComponent().getPreferredSize(); - size.width = editorSize.width; + // Change the preferred size to use a standard starting width. Previously, it + // was sized on the first label edited, but then it just remembered that size. + // A width of 500 is a good starting width that will fit most reasonable + // namespace names. + size.width = 500; return size; } }; @@ -598,7 +611,8 @@ public class AddEditDialog extends ReusableDialogComponentProvider { mainPanel.add(bottomPanel); topPanel.add(labelNameChoices, BorderLayout.NORTH); - midPanel.add(namespaceChoices, BorderLayout.NORTH); + midPanel.add(namespaceChoices, BorderLayout.CENTER); + midPanel.add(buildBrowsePanel(), BorderLayout.EAST); bottomPanel.add(entryPointCheckBox); bottomPanel.add(primaryCheckBox); bottomPanel.add(pinnedCheckBox); @@ -610,6 +624,26 @@ public class AddEditDialog extends ReusableDialogComponentProvider { return mainPanel; } + private Component buildBrowsePanel() { + JPanel panel = new JPanel(new BorderLayout()); + JButton browseButton = new BrowseButton(); + browseButton.setToolTipText("Choose Namespace"); + browseButton.addActionListener(e -> showNamespaceChooser()); + panel.add(browseButton, BorderLayout.CENTER); + panel.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 0)); + return panel; + } + + private void showNamespaceChooser() { + NamespaceChooserDialog dialog = new NamespaceChooserDialog(); + Namespace namespace = dialog.getNameSpace(program); + if (namespace != null) { + NamespaceCache.add(program, namespace); + initNamespaces(); + namespaceChoices.setSelectedItem(new NamespaceWrapper(namespace)); + } + } + private void addListeners() { labelNameChoices.addActionListener(e -> { if (program != null) { @@ -636,6 +670,11 @@ public class AddEditDialog extends ReusableDialogComponentProvider { return text; } + // for testing + public JComboBox getNamespaceComboBox() { + return namespaceChoices; + } + public class NamespaceWrapper { private Namespace namespace; @@ -672,4 +711,5 @@ public class AddEditDialog extends ReusableDialogComponentProvider { return namespace.hashCode(); } } + } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/NamespaceCache.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/NamespaceCache.java new file mode 100644 index 0000000000..3fc485eb75 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/NamespaceCache.java @@ -0,0 +1,59 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util; + +import java.util.*; + +import ghidra.program.model.listing.Program; +import ghidra.program.model.symbol.Namespace; +import ghidra.util.datastruct.LRUSet; + +/** + * Static class for remember the last few namespaces used for a program. + */ +public class NamespaceCache { + public static final int MAX_RECENTS = 10; + private static Map> recentNamespaces = new HashMap<>(); + + /** + * Returns the list of recently used namespaces for the given program. + * @param program the program to get namespaces for + * @return the list of recently used namespaces for the given program + */ + public static List get(Program program) { + LRUSet recents = recentNamespaces.get(program); + return recents != null ? recents.toList() : Collections.emptyList(); + } + + /** + * Adds a recently used namespace for a program. + * @param program the program to add a recently namespace + * @param namespace the recently used namespace to remember + */ + public static void add(Program program, Namespace namespace) { + // no need to cache global namespace, it is always available + if (namespace.isGlobal()) { + return; + } + LRUSet recents = recentNamespaces.get(program); + if (recents == null) { + recents = new LRUSet(MAX_RECENTS); + recentNamespaces.put(program, recents); + program.addCloseListener(p -> recentNamespaces.remove(p)); + } + recents.add(namespace); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/NamespaceChooserDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/NamespaceChooserDialog.java new file mode 100644 index 0000000000..43ddafac52 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/NamespaceChooserDialog.java @@ -0,0 +1,123 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util; + +import java.util.ArrayList; +import java.util.List; + +import javax.swing.*; + +import docking.DialogComponentProvider; +import docking.DockingWindowManager; +import docking.widgets.DropDownSelectionTextField; +import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.Program; +import ghidra.program.model.symbol.Namespace; +import ghidra.program.model.symbol.Symbol; +import ghidra.util.exception.CancelledException; +import ghidra.util.layout.PairLayout; +import ghidra.util.task.*; + +/** + * Class for choosing a namespace + */ +public class NamespaceChooserDialog extends DialogComponentProvider { + + private DropDownSelectionTextField dropDownField; + private NamespaceDropDownModel namespaceModel; + private Namespace chosenNamespace; + + public NamespaceChooserDialog() { + super("Namespace Chooser"); + namespaceModel = new NamespaceDropDownModel(); + addWorkPanel(buildWorkPanel()); + addOKButton(); + addCancelButton(); + } + + public Namespace getNameSpace(Program program) { + List namespaces = gatherNamespaces(program); + if (namespaces == null) { + // user cancelled while gathering namespaces + return null; + } + namespaceModel.setNamespaces(namespaces); + DockingWindowManager.showDialog(this); + return chosenNamespace; + } + + @Override + protected void okCallback() { + chosenNamespace = dropDownField.getSelectedValue(); + close(); + } + + @Override + protected void cancelCallback() { + chosenNamespace = null; + close(); + } + + private List gatherNamespaces(Program program) { + GatherNamespacesTask task = new GatherNamespacesTask(program); + TaskLauncher.launch(task); + return task.getNamespaces(); + } + + private JComponent buildWorkPanel() { + JPanel panel = new JPanel(new PairLayout()); + panel.add(new JLabel("Namespace: ")); + + dropDownField = new DropDownSelectionTextField<>(namespaceModel); + panel.add(dropDownField); + panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + return panel; + } + + private static class GatherNamespacesTask extends Task { + private List namespaces; + private Program program; + + GatherNamespacesTask(Program program) { + super("Gather Namespaces"); + this.program = program; + } + + @Override + public void run(TaskMonitor monitor) throws CancelledException { + List list = new ArrayList<>(); + list.add(program.getGlobalNamespace()); + for (Symbol symbol : program.getSymbolTable().getDefinedSymbols()) { + monitor.checkCancelled(); + if (!symbol.getSymbolType().isNamespace()) { + continue; + } + Object object = symbol.getObject(); + if (object instanceof Function f && f.isThunk()) { + continue; + } + list.add((Namespace) object); + } + namespaces = list; + } + + List getNamespaces() { + return namespaces; + } + + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/NamespaceDropDownModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/NamespaceDropDownModel.java new file mode 100644 index 0000000000..46ac5f93b7 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/NamespaceDropDownModel.java @@ -0,0 +1,193 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util; + +import java.util.*; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.help.UnsupportedOperationException; +import javax.swing.ListCellRenderer; + +import org.apache.commons.lang3.StringUtils; + +import docking.widgets.DropDownTextFieldDataModel; +import docking.widgets.list.GListCellRenderer; +import ghidra.program.model.symbol.Namespace; +import ghidra.util.datastruct.CaseInsensitiveDuplicateStringComparator; + +/** + * This is the drop down text field model for namespaces. + */ +public class NamespaceDropDownModel implements DropDownTextFieldDataModel { + private static final char END_CHAR = '\uffff'; + private List namespaces; + private ListCellRenderer renderer; + private Comparator comparator = + (n1, n2) -> n1.getName().compareToIgnoreCase(n2.getName()); + private Comparator stringComparator = new CaseInsensitiveDuplicateStringComparator(); + + NamespaceDropDownModel() { + renderer = GListCellRenderer.createDefaultTextRenderer(this::getListDisplay); + setNamespaces(Collections.emptyList()); + } + + @Override + public List getMatchingData(String searchText) { + throw new UnsupportedOperationException( + "Method no longer supported. Instead, call getMatchingData(String, SearchMode)"); + } + + public void setNamespaces(List namespaces) { + this.namespaces = namespaces; + Collections.sort(namespaces, comparator); + } + + private String getListDisplay(Namespace namespace) { + StringBuilder buf = new StringBuilder(namespace.getName()); + Namespace parentNamespace = namespace.getParentNamespace(); + if (parentNamespace != null) { + buf.append(" ("); + buf.append(parentNamespace.getName(true)); + buf.append(")"); + } + return buf.toString(); + } + + @Override + public List getMatchingData(String searchText, SearchMode searchMode) { + if (StringUtils.isBlank(searchText)) { + return new ArrayList<>(namespaces); + } + + if (!getSupportedSearchModes().contains(searchMode)) { + throw new IllegalArgumentException("Unsupported SearchMode: " + searchMode); + } + + if (searchMode == SearchMode.STARTS_WITH) { + return getMatchingDataStartsWith(searchText); + } + + Pattern p = searchMode.createPattern(searchText); + return getMatchingDataRegex(p); + } + + private List getMatchingDataRegex(Pattern p) { + List results = new ArrayList<>(); + for (Namespace namespace : namespaces) { + String namespacePath = namespace.getName(true); + Matcher m = p.matcher(namespacePath); + if (m.matches()) { + results.add(namespace); + } + } + return results; + } + + private List getMatchingDataStartsWith(String searchText) { + MappedList list = new MappedList<>(namespaces, n -> n.getName()); + + int startIndex = Collections.binarySearch(list, searchText, stringComparator); + int endIndex = Collections.binarySearch(list, searchText + END_CHAR, stringComparator); + + // the binary search returns a negative, incremented position if there is no match in the + // list for the given search + if (startIndex < 0) { + startIndex = -startIndex - 1; + } + + if (endIndex < 0) { + endIndex = -endIndex - 1; + } + + return namespaces.subList(startIndex, endIndex); + } + + @Override + public List getSupportedSearchModes() { + return List.of(SearchMode.STARTS_WITH, SearchMode.CONTAINS, SearchMode.WILDCARD); + } + + @Override + public int getIndexOfFirstMatchingEntry(List data, String text) { + // The data are sorted such that lower-case is before upper-case and smaller length + // matches come before longer matches. If we ever find a case-sensitive exact match, + // use that. Otherwise, keep looking for a case-insensitive exact match. The + // case-insensitive match is preferred over a non-matching item. Once we get to a + // non-matching item, we can quit. + int lastPreferredMatchIndex = -1; + for (int i = 0; i < data.size(); i++) { + Namespace namespace = data.get(i); + String name = namespace.getName(); + if (name.equals(text)) { + // an exact match is the best possible match! + return i; + } + + if (name.equalsIgnoreCase(text)) { + // keep going, but remember this location, in case we don't find any more matches + lastPreferredMatchIndex = i; + } + else { + // we've encountered a non-matching entry--nothing left to search + return lastPreferredMatchIndex; + } + } + + return -1; // we only get here when the list is empty + } + + @Override + public ListCellRenderer getListRenderer() { + return renderer; + } + + @Override + public String getDescription(Namespace value) { + return value.getName(true); + } + + @Override + public String getDisplayText(Namespace value) { + return value.getName(false); + } + + /** + * Provides an read-only mapped view List of type T from a List of type S. + * @param The type of elements in the source list + * @param The type of elements in the mapped list + */ + private static class MappedList extends AbstractList { + private final List sourceList; + private final Function transformer; + + public MappedList(List sourceList, Function transformer) { + this.sourceList = sourceList; + this.transformer = transformer; + } + + @Override + public T get(int index) { + return transformer.apply(sourceList.get(index)); + } + + @Override + public int size() { + return sourceList.size(); + } + } +} diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/label/AddEditDialogWithNamespaceChooserTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/label/AddEditDialogWithNamespaceChooserTest.java new file mode 100644 index 0000000000..229900e00c --- /dev/null +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/label/AddEditDialogWithNamespaceChooserTest.java @@ -0,0 +1,192 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.label; + +import static org.junit.Assert.*; + +import java.awt.event.KeyEvent; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.*; + +import org.junit.*; + +import ghidra.app.cmd.function.CreateFunctionCmd; +import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin; +import ghidra.app.plugin.core.navigation.GoToAddressLabelPlugin; +import ghidra.app.util.AddEditDialog; +import ghidra.app.util.NamespaceChooserDialog; +import ghidra.app.util.viewer.field.MnemonicFieldFactory; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressSet; +import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.Program; +import ghidra.program.model.symbol.*; +import ghidra.test.*; + +public class AddEditDialogWithNamespaceChooserTest extends AbstractGhidraHeadedIntegrationTest { + private TestEnv env; + private Program program; + private AddEditDialog dialog; + private PluginTool tool; + private LabelMgrPlugin plugin; + private JComboBox namespacesComboBox; + private Symbol symbol1; + private Symbol symbol2; + + @Before + public void setUp() throws Exception { + env = new TestEnv(); + + ClassicSampleX86ProgramBuilder builder = new ClassicSampleX86ProgramBuilder(); + builder.createNamespace("Foo"); + builder.createNamespace("Bar"); + program = builder.getProgram(); + + tool = env.showTool(program); + tool.addPlugin(CodeBrowserPlugin.class.getName()); + tool.addPlugin(LabelMgrPlugin.class.getName()); + tool.addPlugin(GoToAddressLabelPlugin.class.getName()); + + plugin = getPlugin(tool, LabelMgrPlugin.class); + + symbol1 = builder.createLabel("0x10065a6", "symbol1"); + symbol2 = builder.createLabel("0x10065a9", "symbol2"); + createEntryFunction(); + builder.dispose(); + + dialog = getAddEditDialog(); + + namespacesComboBox = dialog.getNamespaceComboBox(); + + } + + private AddEditDialog getAddEditDialog() { + return runSwing(() -> plugin.getAddEditDialog()); + } + + @After + public void tearDown() throws Exception { + close(dialog); + env.dispose(); + } + + @Test + public void testUsingNamespaceChooser() throws Exception { + + editLabel(symbol1); + assertNamespaces("Global", "entry"); + assertSelected("Global"); + chooseNamespaceFromChooser("Foo"); + assertSelected("Foo"); + pressOk(); + + editLabel(symbol2); + assertNamespaces("Global", "entry", "Foo"); + assertSelected("Global"); + chooseNamespaceFromChooser("Bar"); + assertSelected("Bar"); + pressOk(); + + editLabel(symbol1); + assertNamespaces("Global", "Foo", "entry", "Bar"); + assertSelected("Foo"); + pressOk(); + } + + private void assertSelected(String name) { + assertEquals(name, getSelectedNamespace().getName()); + } + + private void assertNamespaces(String... names) { + List comboNamespaces = getComboNamespaces(); + assertEquals(names.length, comboNamespaces.size()); + for (int i = 0; i < names.length; i++) { + assertEquals(names[i], comboNamespaces.get(i).getName()); + } + } + + private void chooseNamespaceFromChooser(String text) { + AbstractButton button = findButtonByName(dialog.getComponent(), "BrowseButton"); + pressButton(button, false); + NamespaceChooserDialog nd = waitForDialogComponent(NamespaceChooserDialog.class); + JTextField field = findComponent(nd.getComponent(), JTextField.class); + typeText(field, text, true); + enter(field); + pressButtonByText(nd.getComponent(), "OK"); + } + + protected void enter(JTextField field) { + triggerActionKey(field, 0, KeyEvent.VK_ENTER); + waitForSwing(); + } + + protected void typeText(JTextField textField, String text, boolean expectWindow) { + waitForSwing(); + triggerText(textField, text); + } + +//================================================================================================== +// Private Methods +//================================================================================================== + + private void pressOk() { + pressButtonByText(dialog, "OK"); + } + + private Namespace getSelectedNamespace() { + return runSwing(() -> { + Object selectedItem = namespacesComboBox.getSelectedItem(); + return ((AddEditDialog.NamespaceWrapper) selectedItem).getNamespace(); + }); + } + + private List getComboNamespaces() { + return runSwing(() -> { + List namespaces = new ArrayList<>(); + int count = namespacesComboBox.getItemCount(); + for (int i = 0; i < count; i++) { + Object itemAt = namespacesComboBox.getItemAt(i); + namespaces.add(((AddEditDialog.NamespaceWrapper) itemAt).getNamespace()); + } + return namespaces; + }); + } + + private void editLabel(final Symbol symbol) { + // this makes debugging easier + CodeBrowserPlugin cb = getPlugin(tool, CodeBrowserPlugin.class); + cb.goToField(symbol.getAddress(), MnemonicFieldFactory.FIELD_NAME, 0, 0); + + runSwing(() -> dialog.editLabel(symbol, program), false); + waitForSwing(); + } + + private void createEntryFunction() throws Exception { + Symbol s = getUniqueSymbol(program, "entry", null); + Function f = program.getListing().getFunctionAt(s.getAddress()); + if (f == null) { + Address addr = s.getAddress(); + AddressSet body = new AddressSet(addr, addr.getNewAddress(0x010065cc)); + body.addRange(addr.getNewAddress(0x10065a4), addr.getNewAddress(0x010065cc)); + CreateFunctionCmd cmd = + new CreateFunctionCmd(null, addr, body, SourceType.USER_DEFINED); + assertTrue(tool.execute(cmd, program)); + } + } +} diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/label/AddEditDialoglTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/label/AddEditDialoglTest.java index b9a2565240..1f9667792d 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/label/AddEditDialoglTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/label/AddEditDialoglTest.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -19,8 +19,8 @@ import static org.junit.Assert.*; import java.awt.Component; import java.awt.Window; +import java.util.ArrayList; import java.util.List; -import java.util.concurrent.atomic.AtomicReference; import javax.swing.*; @@ -86,11 +86,7 @@ public class AddEditDialoglTest extends AbstractGhidraHeadedIntegrationTest { } private AddEditDialog getAddEditDialog() { - AtomicReference ref = new AtomicReference<>(); - runSwing(() -> { - ref.set(plugin.getAddEditDialog()); - }); - return ref.get(); + return runSwing(() -> plugin.getAddEditDialog()); } @After @@ -106,7 +102,7 @@ public class AddEditDialoglTest extends AbstractGhidraHeadedIntegrationTest { assertEquals("", getText()); assertTrue(!entryCheckBox.isSelected()); - Namespace scope = getScope(); + Namespace scope = getSelectedNamespace(); assertEquals(globalScope, scope); assertTrue(primaryCheckBox.isSelected()); assertTrue(!primaryCheckBox.isEnabled()); @@ -195,42 +191,19 @@ public class AddEditDialoglTest extends AbstractGhidraHeadedIntegrationTest { Address address = addr(0x10065a6); Symbol symbol = st.createLabel(address, "sybmol1", scope4, SourceType.USER_DEFINED); - editLabel(symbol); - - JComboBox namespaceChoices = (JComboBox) getInstanceField("namespaceChoices", dialog); - - // get a list of all namespaces in order to compare with the values - // from the combo box - // the first item should always be the global namespace... - assertEquals("The first item in the list of namespaces is not the global namespace", - program.getGlobalNamespace(), getScope(0)); - - assertEquals("The symbol's namespace was not put into the proper place in the list.", - getScope(2), symbol.getParentNamespace()); - - // ...then, each namespace should be a child of the following - // namespace - int itemCount = namespaceChoices.getItemCount(); - for (int i = 1; i < itemCount - 1; i++) { - Namespace currentNamespace = getScope(i); - Namespace nextNamespace = getScope(i + 1); - Namespace actualParent = currentNamespace.getParentNamespace(); - - assertTrue("The namespaces are not in order in the " + - "namespaceChoices combo box. Each item should be a child of " + "following item.", - nextNamespace.equals(actualParent)); - } - - // finally, the last item should be parented on the global namespace - Namespace currentNamespace = getScope(itemCount - 1); - Namespace actualParent = currentNamespace.getParentNamespace(); - - assertTrue( - "The namespaces are not in order in the " + - "namespaceChoices combo box. Each item should be a child of " + "following item.", - actualParent.equals(program.getGlobalNamespace())); - program.endTransaction(transactionID, true); + + editLabel(symbol); + List comboNamespaces = getComboNamespaces(); + assertEquals(3, comboNamespaces.size()); + + // currently, combo is populated with: global, current symbol namespace, containing function + // namespace if in a function body, and then recently used (there are no recently used in + // this test) + assertEquals(program.getGlobalNamespace(), comboNamespaces.get(0)); + assertEquals(scope4, comboNamespaces.get(1)); + assertEquals(f, comboNamespaces.get(2)); + } @Test @@ -892,33 +865,23 @@ public class AddEditDialoglTest extends AbstractGhidraHeadedIntegrationTest { runSwing(() -> checkbox.setSelected(value)); } - private Namespace getScope() { - final AtomicReference ref = new AtomicReference<>(); - runSwing(() -> { + private Namespace getSelectedNamespace() { + return runSwing(() -> { Object selectedItem = namespacesComboBox.getSelectedItem(); - Namespace ns = ((AddEditDialog.NamespaceWrapper) selectedItem).getNamespace(); - ref.set(ns); + return ((AddEditDialog.NamespaceWrapper) selectedItem).getNamespace(); }); - return ref.get(); } - private Namespace getScope(final int index) { - final AtomicReference ref = new AtomicReference<>(); - runSwing(() -> { + private List getComboNamespaces() { + return runSwing(() -> { + List namespaces = new ArrayList<>(); int count = namespacesComboBox.getItemCount(); - if (count <= index) { - System.err.println("Available namespaces: "); - for (int i = 0; i < count; i++) { - System.err.println("\t" + namespacesComboBox.getItemAt(i)); - } - throw new IllegalArgumentException("No namespace at index: " + index); + for (int i = 0; i < count; i++) { + Object itemAt = namespacesComboBox.getItemAt(i); + namespaces.add(((AddEditDialog.NamespaceWrapper) itemAt).getNamespace()); } - - Object selectedItem = namespacesComboBox.getItemAt(index); - Namespace ns = ((AddEditDialog.NamespaceWrapper) selectedItem).getNamespace(); - ref.set(ns); + return namespaces; }); - return ref.get(); } private void setScope(final Namespace scope) { diff --git a/Ghidra/Features/Base/src/test/java/ghidra/app/util/NamespaceCacheTest.java b/Ghidra/Features/Base/src/test/java/ghidra/app/util/NamespaceCacheTest.java new file mode 100644 index 0000000000..c8a1f6d9ed --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/app/util/NamespaceCacheTest.java @@ -0,0 +1,126 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import generic.test.AbstractGuiTest; +import ghidra.program.database.ProgramBuilder; +import ghidra.program.database.ProgramDB; +import ghidra.program.model.symbol.Namespace; +import ghidra.program.model.symbol.SourceType; +import ghidra.test.ToyProgramBuilder; + +public class NamespaceCacheTest extends AbstractGuiTest { + + private Namespace a; + private Namespace b; + private Namespace c; + private Namespace d; + private Namespace e; + private Namespace f; + private Namespace g; + private Namespace h; + private Namespace i; + private Namespace j; + private Namespace k; + private ProgramDB program; + + @Before + public void setup() throws Exception { + ProgramBuilder builder = new ToyProgramBuilder("Test", false, this); + a = builder.createNamespace("a"); + b = builder.createNamespace("b", "a", SourceType.USER_DEFINED); + c = builder.createNamespace("c", "a::b", SourceType.USER_DEFINED); + d = builder.createNamespace("d"); + e = builder.createNamespace("e"); + f = builder.createNamespace("f"); + g = builder.createNamespace("g"); + h = builder.createNamespace("h"); + i = builder.createNamespace("i"); + j = builder.createNamespace("j"); + k = builder.createNamespace("k"); + program = builder.getProgram(); + } + + @Test + public void testGetRecent() { + NamespaceCache.add(program, a); + NamespaceCache.add(program, b); + NamespaceCache.add(program, c); + + List recent = NamespaceCache.get(program); + assertEquals(c, recent.get(0)); + assertEquals(b, recent.get(1)); + assertEquals(a, recent.get(2)); + } + + @Test + public void testMostRecentAtTop() { + NamespaceCache.add(program, a); + NamespaceCache.add(program, b); + NamespaceCache.add(program, c); + NamespaceCache.add(program, a); + + List recents = NamespaceCache.get(program); + assertEquals(3, recents.size()); + assertEquals(a, recents.get(0)); + assertEquals(c, recents.get(1)); + assertEquals(b, recents.get(2)); + } + + @Test + public void testMaxRecents() { + NamespaceCache.add(program, a); + NamespaceCache.add(program, b); + NamespaceCache.add(program, c); + NamespaceCache.add(program, d); + NamespaceCache.add(program, e); + NamespaceCache.add(program, f); + NamespaceCache.add(program, g); + NamespaceCache.add(program, h); + NamespaceCache.add(program, i); + NamespaceCache.add(program, j); + NamespaceCache.add(program, k); + + List recents = NamespaceCache.get(program); + assertEquals(NamespaceCache.MAX_RECENTS, recents.size()); + assertEquals(k, recents.get(0)); + assertEquals(b, recents.get(9)); + } + + @Test + public void testClosingProgramClearsRecents() { + NamespaceCache.add(program, a); + NamespaceCache.add(program, b); + NamespaceCache.add(program, c); + + List recents = NamespaceCache.get(program); + assertEquals(3, recents.size()); + + program.release(this); + + recents = NamespaceCache.get(program); + assertTrue(recents.isEmpty()); + + } + +} diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/datastruct/LRUSet.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/datastruct/LRUSet.java index c1d8870d57..57445bbba1 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/datastruct/LRUSet.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/datastruct/LRUSet.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,7 +15,7 @@ */ package ghidra.util.datastruct; -import java.util.Iterator; +import java.util.*; /** * An ordered set-like data structure. @@ -51,6 +51,13 @@ public class LRUSet extends LRUMap implements Iterable { @Override public String toString() { - return map.keySet().toString(); + return keySet().toString(); + } + + /** + * {@return a List of elements in this set} + */ + public List toList() { + return new ArrayList<>(keySet()); } } diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/DataTypeEditorsScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/DataTypeEditorsScreenShots.java index f1b5457697..b25eff8824 100644 --- a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/DataTypeEditorsScreenShots.java +++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/DataTypeEditorsScreenShots.java @@ -68,18 +68,6 @@ public class DataTypeEditorsScreenShots extends GhidraScreenShotGenerator { positionListingTop(0x40D3B8); DropDownSelectionTextField textField = showTypeChooserDialog(); - triggerText(textField, "undefined"); - - DialogComponentProvider dialog = getDialog(); - JComponent component = dialog.getComponent(); - Window dataTypeDialog = windowForComponent(component); - Window[] popUpWindows = dataTypeDialog.getOwnedWindows(); - - List dataTypeWindows = new ArrayList<>(Arrays.asList(popUpWindows)); - dataTypeWindows.add(dataTypeDialog); - - captureComponents(dataTypeWindows); - closeAllWindows(); } private DropDownSelectionTextField showTypeChooserDialog() throws Exception { diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/LabelMgrPluginScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/LabelMgrPluginScreenShots.java index 5614090ee1..69f5dc025e 100644 --- a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/LabelMgrPluginScreenShots.java +++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/LabelMgrPluginScreenShots.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -23,8 +23,7 @@ import org.junit.Test; import docking.widgets.combobox.GhidraComboBox; import ghidra.app.plugin.core.label.*; -import ghidra.app.util.AddEditDialog; -import ghidra.app.util.EditFieldNameDialog; +import ghidra.app.util.*; import ghidra.program.model.address.*; import ghidra.program.model.symbol.LabelHistory; @@ -76,11 +75,21 @@ public class LabelMgrPluginScreenShots extends GhidraScreenShotGenerator { captureDialog(); } + @Test + public void testChooseNamespace() { + runSwingLater(() -> { + NamespaceChooserDialog dialog = new NamespaceChooserDialog(); + dialog.getNameSpace(program); + }); + waitForDialogComponent(NamespaceChooserDialog.class); + captureDialog(); + } + @Test public void testSetLabel() { LabelMgrPlugin plugin = getPlugin(tool, LabelMgrPlugin.class); final OperandLabelDialog dialog = new OperandLabelDialog(plugin); - final GhidraComboBox combo = (GhidraComboBox) getInstanceField("myChoice", dialog); + final GhidraComboBox combo = (GhidraComboBox) getInstanceField("myChoice", dialog); runSwing(new Runnable() { @Override public void run() {