diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolTreePlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolTreePlugin.java index 1fd1a1d2ae..10888e87a4 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolTreePlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolTreePlugin.java @@ -47,8 +47,8 @@ import ghidra.util.bean.opteditor.OptionsVetoException; //@formatter:on public class SymbolTreePlugin extends Plugin implements SymbolTreeService { - private static final String OPTIONS_CATEGORY = "Symbol Tree"; - private static final String OPTION_NAME_GROUP_THRESHOLD = "Group Threshold"; + public static final String OPTIONS_CATEGORY = "Symbol Tree"; + public static final String OPTION_NAME_GROUP_THRESHOLD = "Group Threshold"; private SymbolTreeProvider connectedProvider; private List disconnectedProviders = new ArrayList<>(); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/SymbolTreeNode.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/SymbolTreeNode.java index b4b85be819..2c4cd7c013 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/SymbolTreeNode.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/SymbolTreeNode.java @@ -263,6 +263,9 @@ public abstract class SymbolTreeNode extends GTreeSlowLoadingNode { if (symbolNode.getSymbol() == searchSymbol) { return symbolNode; } + else if (symbolNode instanceof OrganizationNode) { + return findNode(symbolNode, key, loadChildren, monitor); + } } return null; diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/symboltree/SymbolTreePlugin2Test.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/symboltree/SymbolTreePlugin2Test.java index 75745bada3..ef8f54b214 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/symboltree/SymbolTreePlugin2Test.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/symboltree/SymbolTreePlugin2Test.java @@ -36,9 +36,9 @@ import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin; import ghidra.app.plugin.core.marker.MarkerManagerPlugin; import ghidra.app.plugin.core.programtree.ProgramTreePlugin; import ghidra.app.plugin.core.symboltree.actions.NavigateOnIncomingAction; -import ghidra.app.plugin.core.symboltree.nodes.SymbolCategoryNode; -import ghidra.app.plugin.core.symboltree.nodes.SymbolNode; +import ghidra.app.plugin.core.symboltree.nodes.*; import ghidra.app.util.viewer.field.*; +import ghidra.framework.options.ToolOptions; import ghidra.framework.plugintool.PluginTool; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressFactory; @@ -666,10 +666,150 @@ public class SymbolTreePlugin2Test extends AbstractGhidraHeadedIntegrationTest { assertEquals("MyAnotherLocal", (((SymbolNode) node).getSymbol()).getName()); } + @Test + public void testOrgNode_Namespaces() throws Exception { + + // set org node threshold to a low value + ToolOptions options = tool.getOptions(SymbolTreePlugin.OPTIONS_CATEGORY); + int newThreshold = 4; + options.setInt(SymbolTreePlugin.OPTION_NAME_GROUP_THRESHOLD, newThreshold); + + /* + Create enough nodes to trigger an org node. + + Namespaces + NsGroup1 + NsGroup10 + NsGroup100 + NsGroup2 + NsGroup3 + */ + createOrgNamespaces(newThreshold); + + // create a new namespace node that would appear under an org node + NamespaceSymbolNode parentNode = openNamespaceNodes("NsGroup1::NsGroup10::NsGroup100"); + Namespace parentNs = parentNode.getNamespace(); + createNamespace(parentNs, "NewNamespace"); + + // verify node is in the tree + assertNamespaceNodes("NsGroup1::NsGroup10::NsGroup100::NewNamespace"); + } + + @Test + public void testOrgNode_Classes() throws Exception { + + // set org node threshold to a low value + ToolOptions options = tool.getOptions(SymbolTreePlugin.OPTIONS_CATEGORY); + int newThreshold = 4; + options.setInt(SymbolTreePlugin.OPTION_NAME_GROUP_THRESHOLD, newThreshold); + + /* + Create enough nodes to trigger an org node. + + Classes + ClassGroup1 + ClassGroup10 + ClassGroup100 + ClassGroup2 + ClassGroup3 + */ + createOrgClasses(newThreshold); + + // create a new namespace node that would appear under an org node + ClassSymbolNode parentNode = + openClassNodes("ClassGroup1::ClassGroup10::ClassGroup100"); + Namespace parentNs = parentNode.getNamespace(); + createClass(parentNs, "NewClass"); + + // verify node is in the tree + assertClassNodes("ClassGroup1::ClassGroup10::ClassGroup100::NewClass"); + + } + //================================================================================================= // Private Methods //================================================================================================= + private NamespaceCategoryNode getNamespacesNode() { + GTreeNode root = tree.getViewRoot(); + return (NamespaceCategoryNode) root.getChild("Namespaces"); + } + + private ClassCategoryNode getClassesNode() { + GTreeNode root = tree.getViewRoot(); + return (ClassCategoryNode) root.getChild("Classes"); + } + + private NamespaceSymbolNode openNamespaceNodes(String path) { + + GTreeNode parent = getNamespacesNode(); + String[] parts = path.split("::"); + for (String name : parts) { + GTreeNode child = parent.getChild(name); + String message = + "Child '%s' not found in parent '%s' \n\tfor path '%s'" + .formatted(name, parent, path); + assertNotNull(message, child); + tree.expandPath(child); + parent = child; + } + waitForTree(tree); + + return (NamespaceSymbolNode) parent; + } + + private ClassSymbolNode openClassNodes(String path) { + + GTreeNode parent = getClassesNode(); + String[] parts = path.split("::"); + for (String name : parts) { + GTreeNode child = parent.getChild(name); + String message = + "Child '%s' not found in parent '%s' \n\tfor path '%s'" + .formatted(name, parent, path); + assertNotNull(message, child); + tree.expandPath(child); + parent = child; + } + waitForTree(tree); + + return (ClassSymbolNode) parent; + } + + private void createOrgNamespaces(int threshold) throws Exception { + int groupCount = 3; + for (int i = 0; i < groupCount; i++) { + // groups need to share a common prefix + String groupName = "Group" + (i + 1); // 1-based for readability + int childCount = (threshold + 1); + for (int j = 0; j < childCount; j++) { + + String subGroupName = groupName + j; + for (int k = 0; k < childCount; k++) { + + createNamespace("Ns" + subGroupName + k); + } + } + } + } + + private void createOrgClasses(int threshold) throws Exception { + int groupCount = 3; + for (int i = 0; i < groupCount; i++) { + // groups need to share a common prefix + String groupName = "Group" + (i + 1); // 1-based for readability + int childCount = (threshold + 1); + for (int j = 0; j < childCount; j++) { + + String subGroupName = groupName + j; + for (int k = 0; k < childCount; k++) { + + createClass("Class" + subGroupName + k); + } + } + } + } + private void expandClasses() { GTreeNode node = rootNode.getChild("Classes"); tree.expandTree(node); @@ -740,6 +880,10 @@ public class SymbolTreePlugin2Test extends AbstractGhidraHeadedIntegrationTest { return cmd.getNamespace(); } + private void createClass(String name) throws Exception { + createClass(program.getGlobalNamespace(), name); + } + private GhidraClass createClass(Namespace parent, String name) throws Exception { GhidraClass c = tx(program, () -> { SymbolTable symbolTable = program.getSymbolTable();