GP-5806 added namespace chooser to add/edit label/function dialog

This commit is contained in:
ghidragon
2025-10-06 15:10:13 -04:00
parent 5ea1b04604
commit 298aa9827f
19 changed files with 887 additions and 159 deletions
@@ -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|
Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 17 KiB

@@ -137,8 +137,6 @@
<P><I><B>Enter Label</B></I></P>
<BLOCKQUOTE>
<UL>
<LI>
Text field for entering the name of the label. A combo box is included which allows
selecting recently used label names.
@@ -150,35 +148,36 @@
</PRE>
For example, the following string denotes a full namespace path that starts at the
<B>Global</B> namespace and ends at the label name <TT>myLabel</TT>:<BR>
<PRE>
Global::foo::bar::myLabel
</PRE>
<PRE>
Global::foo::bar::myLabel
</PRE>
The namespace in the <I>Namespace</I> combo box will be used as the parent namespace
for the label name and any included namespaces. However, if the you provide a namespace
path that starts with <B>Global</B>, then the value of the <I>Namespace</I> combo box
will be ignored.
</LI>
</UL>
</BLOCKQUOTE>
<P><I><B>Namespace</B></I></P>
<BLOCKQUOTE>
<UL>
<LI>The defining scope of the label. The available namespaces are based upon the current
address. When editing a label, the available namespaces are not necessarily those in the
namespace hierarchy in which the label is located, but rather are those based upon the
address of that label. The <B>Global</B> namespace is always included by default, as well
as the parent namespace of the current label, if one is being edited.</LI>
</UL>
<P>The containing namespace for the label or function. </P>
<P>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.</P>
<P>Next to the combobox, there is a browse button which can be pressed to bring up the
<A href="#NamespaceChooserDialog">Namespace Chooser Dialog</A>. 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.</P>
<BLOCKQUOTE>
<P><IMG src="help/shared/note.png" alt="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.<BR>
<P><IMG src="help/shared/note.png" alt="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. <BR>
</P>
</BLOCKQUOTE>
</BLOCKQUOTE>
@@ -186,38 +185,66 @@
<P><I><B>Entry Point</B></I></P>
<BLOCKQUOTE>
<UL>
<LI>Sets the entry point property for address associated with this label. Setting this
Sets the entry point property for address associated with this label. Setting this
property on one symbol, changes it for all symbols at the same address.</LI>
</UL>
</BLOCKQUOTE>
<P><I><B>Primary</B></I></P>
<BLOCKQUOTE>
<UL>
<LI>Sets the primary property for this symbol. If there is only one symbol at this
Sets the primary property for this symbol. If there is only one symbol at this
address, the checkbox will be selected and disabled, since it must be primary. Whenever
the checkbox is selected, it will be disabled because the only way to make a symbol
become non-primary is to select another symbol at the same address and make it primary.
This ensures that there will always be one label that is primary. If there is a function
at this address and you add a new label, the checkbox will be enabled such that if you
select the checkbox, the function is renamed to the new label that you entered.&nbsp; The
function symbol must always be the primary symbol.</LI>
</UL>
function symbol must always be the primary symbol.
</BLOCKQUOTE>
<P><I><B>Pinned</B></I></P>
<BLOCKQUOTE>
<UL>
<LI>Sets the label to pinned. A pinned label will not move if the image base is changed
Sets the label to pinned. A pinned label will not move if the image base is changed
or a memory block is moved. A label that is not pinned, will move with the code
or data if a memory block is moved or the image base is changed. Also, a pinned
label will not be removed if the memory block that contains it is removed. Only code,
data, or function labels may be pinned.</LI>
</UL>
data, or function labels may be pinned.
</BLOCKQUOTE>
</BLOCKQUOTE>
<H2><A name="NamespaceChooserDialog"></A>Namespace Chooser Dialog</H2>
<BLOCKQUOTE>
<P>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.</P>
<P align="center"><IMG border="0" src="images/ChooseNamespace.png" alt=""><BR>
<I>Namespace Chooser Dialog</I></P>
<P>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 <B>Ctrl Down</B> and
<B>Ctrl Up</B> to change the mode forward and backward, respectively. Regardless of the mode,
the searches are not case sensitive.</P>
<P>These modes are:</P>
<UL>
<LI><B>Starts With ^</B> - In this mode, the typed text will match all namespaces that
have a base name (not path) that starts with the typed text.</LI>
<LI><B>Contains <I>()</I></B> - In this mode, the typed text will match all namespaces that
have the typed text anywhere in its full namespace path. </LI>
<LI><B>Wildcard <I>*?</I></B> - In this mode, the typed text may contain wildcard characters
which will be used to create a pattern for matching against the full namespace path. The
wildcard char '*' will match 0 or more characters, while the wildcard char '?' will match any
single character. </LI>
</UL>
</BLOCKQUOTE>
<H2><A name="OperandLabelDialog"></A>Set Label Dialog</H2>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

@@ -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<String> 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<Namespace> getSelectableNamespaces() {
if (!namespaceChoices.isEnabled()) {
namespaceChoices.addItem(new NamespaceWrapper(symbol.getParentNamespace()));
selectNamespace();
return List.of(symbol.getParentNamespace());
}
SequencedSet<Namespace> namespaces = new LinkedHashSet<>();
addGlobalNamespace(namespaces);
addCurrentNamespace(namespaces);
addSuggestedNamespace(namespaces);
addRecentNamespaces(namespaces);
return namespaces;
}
private void addRecentNamespaces(SequencedSet<Namespace> namespaces) {
List<Namespace> recentNamespaces = NamespaceCache.get(program);
if (recentNamespaces == null) {
return;
}
for (Namespace namespace : recentNamespaces) {
if (!namespaces.contains(namespace)) {
namespaces.add(namespace);
}
}
}
Collection<NamespaceWrapper> collection = new HashSet<>();
private void addSuggestedNamespace(SequencedSet<Namespace> 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<Namespace> 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<Namespace> 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();
}
}
}
@@ -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<Program, LRUSet<Namespace>> 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<Namespace> get(Program program) {
LRUSet<Namespace> 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<Namespace> recents = recentNamespaces.get(program);
if (recents == null) {
recents = new LRUSet<Namespace>(MAX_RECENTS);
recentNamespaces.put(program, recents);
program.addCloseListener(p -> recentNamespaces.remove(p));
}
recents.add(namespace);
}
}
@@ -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<Namespace> 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<Namespace> 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<Namespace> 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<Namespace> namespaces;
private Program program;
GatherNamespacesTask(Program program) {
super("Gather Namespaces");
this.program = program;
}
@Override
public void run(TaskMonitor monitor) throws CancelledException {
List<Namespace> 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<Namespace> getNamespaces() {
return namespaces;
}
}
}
@@ -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<Namespace> {
private static final char END_CHAR = '\uffff';
private List<Namespace> namespaces;
private ListCellRenderer<Namespace> renderer;
private Comparator<Namespace> comparator =
(n1, n2) -> n1.getName().compareToIgnoreCase(n2.getName());
private Comparator<String> stringComparator = new CaseInsensitiveDuplicateStringComparator();
NamespaceDropDownModel() {
renderer = GListCellRenderer.createDefaultTextRenderer(this::getListDisplay);
setNamespaces(Collections.emptyList());
}
@Override
public List<Namespace> getMatchingData(String searchText) {
throw new UnsupportedOperationException(
"Method no longer supported. Instead, call getMatchingData(String, SearchMode)");
}
public void setNamespaces(List<Namespace> 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<Namespace> 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<Namespace> getMatchingDataRegex(Pattern p) {
List<Namespace> 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<Namespace> getMatchingDataStartsWith(String searchText) {
MappedList<Namespace, String> 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<SearchMode> getSupportedSearchModes() {
return List.of(SearchMode.STARTS_WITH, SearchMode.CONTAINS, SearchMode.WILDCARD);
}
@Override
public int getIndexOfFirstMatchingEntry(List<Namespace> 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<Namespace> 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 <S> The type of elements in the source list
* @param <T> The type of elements in the mapped list
*/
private static class MappedList<S, T> extends AbstractList<T> {
private final List<S> sourceList;
private final Function<S, T> transformer;
public MappedList(List<S> sourceList, Function<S, T> 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();
}
}
}
@@ -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<Namespace> 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<Namespace> getComboNamespaces() {
return runSwing(() -> {
List<Namespace> 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));
}
}
}
@@ -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<AddEditDialog> 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<Namespace> 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<Namespace> 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<Namespace> ref = new AtomicReference<>();
runSwing(() -> {
private List<Namespace> getComboNamespaces() {
return runSwing(() -> {
List<Namespace> 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) {
@@ -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<Namespace> 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<Namespace> 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<Namespace> 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<Namespace> recents = NamespaceCache.get(program);
assertEquals(3, recents.size());
program.release(this);
recents = NamespaceCache.get(program);
assertTrue(recents.isEmpty());
}
}
@@ -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<T> extends LRUMap<T, T> implements Iterable<T> {
@Override
public String toString() {
return map.keySet().toString();
return keySet().toString();
}
/**
* {@return a List of elements in this set}
*/
public List<T> toList() {
return new ArrayList<>(keySet());
}
}
@@ -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<Component> dataTypeWindows = new ArrayList<>(Arrays.asList(popUpWindows));
dataTypeWindows.add(dataTypeDialog);
captureComponents(dataTypeWindows);
closeAllWindows();
}
private DropDownSelectionTextField<?> showTypeChooserDialog() throws Exception {
@@ -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() {