nodes, GTreeNode parent, int max,
- TaskMonitor monitor) throws CancelledException {
- return computeChildren(nodes, max, parent, 0, monitor);
- }
-
- /**
- * Subdivide the given list of nodes such that the list or no new parent created will have
- * more than maxNodes children.
+ * Subdivide the given list of nodes recursively such that there are generally not more
+ * than maxGroupSize number of nodes at any level. Also, if there are ever many
+ * nodes of the same name, a group for them will be created and only a few will be shown with
+ * an "xx more..." node to indicate there are additional nodes that are not shown.
*
* This algorithm uses the node names to group nodes based upon common prefixes. For example,
* if a parent node contained more than maxNodes children then a possible grouping
@@ -98,103 +93,47 @@ public class OrganizationNode extends SymbolTreeNode {
* g
*
*
- * The algorithm prefers to group nodes that have the longest common prefixes.
- *
- * Note: the given data must be sorted for this method to work properly.
- *
- * @param list list of child nodes of parent to breakup into smaller groups.
- * @param maxNodes The max number of child nodes per parent node at any node level.
- * @param parent The parent of the given children
- * @param parentLevel node depth in the tree of Organization nodes.
- * @return the given list sub-grouped as outlined above.
+ * @param list list of child nodes of to breakup into smaller groups
+ * @param maxGroupSize the max number of nodes to allow before trying to organize into
+ * smaller groups
+ * @param monitor the TaskMonitor to be checked for canceling this operation
+ * @return the given list sub-grouped as outlined above
* @throws CancelledException if the operation is cancelled
*/
- private static List computeChildren(List list, int maxNodes,
- GTreeNode parent, int parentLevel, TaskMonitor monitor) throws CancelledException {
- List children;
- if (list.size() <= maxNodes) {
- children = new ArrayList<>(list);
+ public static List organize(List list, int maxGroupSize,
+ TaskMonitor monitor) throws CancelledException {
+
+ Map> prefixMap = partition(list, maxGroupSize, monitor);
+
+ // if they didn't partition, just add all given nodes as children
+ if (prefixMap == null) {
+ return new ArrayList<>(list);
}
- else {
- int characterOffset = getPrefixSizeForGrouping(list, maxNodes);
- characterOffset = Math.max(characterOffset, parentLevel + 1);
+ // otherwise, the nodes have been partitioned into groups with a common prefix
+ // loop through and create organization nodes for groups larger than one element
+ List children = new ArrayList<>();
+ for (String prefix : prefixMap.keySet()) {
+ monitor.checkCanceled();
- children = new ArrayList<>();
- String prevStr = list.get(0).getName();
- int start = 0;
- int end = list.size();
- for (int i = 1; i < end; i++) {
- monitor.checkCanceled();
- String str = list.get(i).getName();
- if (stringsDiffer(prevStr, str, characterOffset)) {
- addNode(children, list, start, i - 1, maxNodes, characterOffset, monitor);
- start = i;
- }
- prevStr = str;
+ List nodesSamePrefix = prefixMap.get(prefix);
+
+ // all the nodes that don't have a common prefix get added directly
+ if (prefix.isEmpty()) {
+ children.addAll(nodesSamePrefix);
+ }
+ // groups with one entry, just add in the element directly
+ else if (nodesSamePrefix.size() == 1) {
+ children.addAll(nodesSamePrefix);
+ }
+ else {
+ // add an organization node for each unique prefix
+ children.add(new OrganizationNode(nodesSamePrefix, maxGroupSize, monitor));
}
- addNode(children, list, start, end - 1, maxNodes, characterOffset, monitor);
}
return children;
}
- private static boolean stringsDiffer(String s1, String s2, int diffLevel) {
- if (s1.length() <= diffLevel || s2.length() <= diffLevel) {
- return true;
- }
- return s1.substring(0, diffLevel + 1)
- .compareToIgnoreCase(s2.substring(0, diffLevel + 1)) != 0;
- }
-
- private static void addNode(List children, List list, int start, int end,
- int max, int diffLevel, TaskMonitor monitor) throws CancelledException {
- if (end - start > 0) {
- children.add(
- new OrganizationNode(list.subList(start, end + 1), max, diffLevel, monitor));
- }
- else {
- GTreeNode node = list.get(start);
- children.add(node);
- }
- }
-
- /**
- * Returns the longest prefix size such that the list of nodes can be grouped by
- * those prefixes while not exceeding maxNodes number of children.
- */
- private static int getPrefixSizeForGrouping(List list, int maxNodes) {
- IntArray prefixSizeCountBins = new IntArray();
- Iterator it = list.iterator();
- String previousNodeName = it.next().getName();
- prefixSizeCountBins.put(0, 1);
- while (it.hasNext()) {
- String currentNodeName = it.next().getName();
- int prefixSize = getCommonPrefixSize(previousNodeName, currentNodeName);
- prefixSizeCountBins.put(prefixSize, prefixSizeCountBins.get(prefixSize) + 1);
- previousNodeName = currentNodeName;
- }
-
- int binContentsTotal = 0;
- for (int i = 0; i <= prefixSizeCountBins.getLastNonEmptyIndex(); i++) {
- binContentsTotal += prefixSizeCountBins.get(i);
- if (binContentsTotal > maxNodes) {
- return Math.max(0, i - 1); // we've crossed the max; take a step back
- }
- }
-
- return prefixSizeCountBins.getLastNonEmptyIndex(); // all are allowed; use max prefix size
- }
-
- private static int getCommonPrefixSize(String s1, String s2) {
- int maxCompareLength = Math.min(s1.length(), s2.length());
- for (int i = 0; i < maxCompareLength; i++) {
- if (Character.toUpperCase(s1.charAt(i)) != Character.toUpperCase(s2.charAt(i))) {
- return i;
- }
- }
- return maxCompareLength; // one string is a subset of the other (or the same)
- }
-
@Override
public boolean equals(Object o) {
if (this == o) {
@@ -226,10 +165,6 @@ public class OrganizationNode extends SymbolTreeNode {
return false;
}
- public boolean isModifiable() {
- return false;
- }
-
@Override
public void setNodeCut(boolean isCut) {
throw new UnsupportedOperationException("Cannot cut an organization node");
@@ -245,12 +180,12 @@ public class OrganizationNode extends SymbolTreeNode {
@Override
public String getName() {
- return baseName + "...";
+ return baseName;
}
@Override
public String getToolTip() {
- return getName();
+ return "Contains labels that start with \"" + getName() + "\" (" + totalCount + ")";
}
@Override
@@ -282,6 +217,11 @@ public class OrganizationNode extends SymbolTreeNode {
* @param newNode the node to insert.
*/
public void insertNode(GTreeNode newNode) {
+ if (moreNode != null) {
+ moreNode.incrementCount();
+ return;
+ }
+
int index = Collections.binarySearch(getChildren(), newNode, getChildrenComparator());
if (index >= 0) {
// found a match
@@ -309,6 +249,23 @@ public class OrganizationNode extends SymbolTreeNode {
return null;
}
+ // special case: all symbols in this group have the same name.
+ if (moreNode != null) {
+ if (!symbolName.equals(baseName)) {
+ return null;
+ }
+ // The node either belongs to this node's children or it is represented by the
+ // 'more' node
+ for (GTreeNode child : children()) {
+ SymbolTreeNode symbolTreeNode = (SymbolTreeNode) child;
+ if (symbolTreeNode.getSymbol() == key.getSymbol()) {
+ return child;
+ }
+ }
+
+ return moreNode;
+ }
+
//
// Note: The 'key' node used for searching will find us the parent node of the symbol
// that has changed if it is an org node (this is because the org node searches
@@ -339,6 +296,112 @@ public class OrganizationNode extends SymbolTreeNode {
return COMPARATOR;
}
+ // We are being tricky here. The findSymbolTreeNode above returns the 'more' node
+ // if the searched node is one of the nodes not being shown, so then the removeNode gets
+ // called with the 'more' node, which just means to decrement the count.
+ @Override
+ public void removeNode(GTreeNode node) {
+ if (node == moreNode) {
+ moreNode.decrementCount();
+ if (!moreNode.isEmpty()) {
+ return;
+ }
+ // The 'more' node is empty, just let it be removed
+ moreNode = null;
+ }
+ super.removeNode(node);
+ }
+
+ @Override
+ public List generateChildren(TaskMonitor monitor) throws CancelledException {
+ // not used, children generated in constructor
+ return null;
+ }
+
+ private String getCommonPrefix(List children) {
+ int commonPrefixSize = getCommonPrefixSize(children);
+ return children.get(0).getName().substring(0, commonPrefixSize);
+ }
+
+ /**
+ * This is the algorithm for partitioning a list of nodes into a hierarchical structure based
+ * on common prefixes
+ * @param nodeList the list of nodes to be partitioned
+ * @param maxGroupSize the maximum number of nodes in a group before an organization is attempted
+ * @param monitor {@link TaskMonitor} so the operation can be cancelled
+ * @return a map of common prefixes to lists of nodes that have that common prefix. Returns null
+ * if the size is less than maxGroupSize or the partition didn't reduce the number of nodes
+ * @throws CancelledException if the operation was cancelled
+ */
+ private static Map> partition(List nodeList,
+ int maxGroupSize, TaskMonitor monitor) throws CancelledException {
+
+ // no need to partition of the number of nodes is small enough
+ if (nodeList.size() <= maxGroupSize) {
+ return null;
+ }
+ int commonPrefixSize = getCommonPrefixSize(nodeList);
+ int uniquePrefixSize = commonPrefixSize + 1;
+ Map> map = new LinkedHashMap<>();
+ for (GTreeNode node : nodeList) {
+ monitor.checkCanceled();
+ String prefix = getPrefix(node, uniquePrefixSize);
+ List list = map.computeIfAbsent(prefix, k -> new ArrayList());
+ list.add(node);
+ }
+ if (map.size() == 1) {
+ return null;
+ }
+ if (map.size() >= nodeList.size()) {
+ return null; // no reduction
+ }
+
+ return map;
+ }
+
+ private static String getPrefix(GTreeNode gTreeNode, int uniquePrefixSize) {
+ String name = gTreeNode.getName();
+ if (name.length() <= uniquePrefixSize) {
+ return name;
+ }
+ return name.substring(0, uniquePrefixSize);
+ }
+
+ private static int getCommonPrefixSize(List list) {
+ GTreeNode node = list.get(0);
+ String first = node.getName();
+ int inCommonSize = first.length();
+ for (int i = 1; i < list.size(); i++) {
+ String next = list.get(i).getName();
+ inCommonSize = Math.min(inCommonSize, getCommonPrefixSize(first, next, inCommonSize));
+ }
+ return inCommonSize;
+ }
+
+ private static int getCommonPrefixSize(String base, String candidate, int max) {
+ int maxCompareLength = Math.min(max, candidate.length());
+ for (int i = 0; i < maxCompareLength; i++) {
+ if (base.charAt(i) != candidate.charAt(i)) {
+ return i;
+ }
+ }
+ return maxCompareLength; // one string is a subset of the other (or the same)
+ }
+
+ private static boolean hasSameName(List list) {
+ if (list.size() < 2) {
+ return false;
+ }
+ String name = list.get(0).getName();
+ for (GTreeNode node : list) {
+ if (!name.equals(node.getName())) {
+ return false;
+ }
+ }
+ return true;
+
+ }
+
static class OrganizationNodeComparator implements Comparator {
@Override
public int compare(GTreeNode g1, GTreeNode g2) {
@@ -355,9 +418,4 @@ public class OrganizationNode extends SymbolTreeNode {
}
}
- @Override
- public List generateChildren(TaskMonitor monitor) throws CancelledException {
- // not used, children generated in constructor
- return null;
- }
}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/SymbolCategoryNode.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/SymbolCategoryNode.java
index 098444f696..53e4cbef6e 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/SymbolCategoryNode.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/SymbolCategoryNode.java
@@ -28,6 +28,7 @@ import ghidra.util.task.TaskMonitor;
import ghidra.util.task.TaskMonitorAdapter;
public abstract class SymbolCategoryNode extends SymbolTreeNode {
+ private static final int MAX_NODES = 40;
protected SymbolCategory symbolCategory;
protected SymbolTable symbolTable;
protected GlobalNamespace globalNamespace;
@@ -53,10 +54,7 @@ public abstract class SymbolCategoryNode extends SymbolTreeNode {
SymbolType symbolType = symbolCategory.getSymbolType();
List list = getSymbols(symbolType, monitor);
monitor.checkCanceled();
- if (list.size() > MAX_CHILD_NODES) {
- list = OrganizationNode.organize(list, MAX_CHILD_NODES, monitor);
- }
- return list;
+ return OrganizationNode.organize(list, MAX_NODES, monitor);
}
public Program getProgram() {
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 1915b28426..d92c3b5695 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
@@ -41,8 +41,6 @@ import ghidra.util.task.TaskMonitor;
*/
public abstract class SymbolTreeNode extends GTreeSlowLoadingNode {
- public static final int MAX_CHILD_NODES = 40;
-
public static final Comparator SYMBOL_COMPARATOR = (s1, s2) -> {
// note: not really sure if we care about the cases where 'symbol' is null, as that
// implies the symbol was deleted and the node will go away. Just be consistent.
diff --git a/Ghidra/Features/Base/src/test/java/ghidra/app/plugin/core/symboltree/nodes/OrganizationNodeTest.java b/Ghidra/Features/Base/src/test/java/ghidra/app/plugin/core/symboltree/nodes/OrganizationNodeTest.java
new file mode 100644
index 0000000000..91f04cbc18
--- /dev/null
+++ b/Ghidra/Features/Base/src/test/java/ghidra/app/plugin/core/symboltree/nodes/OrganizationNodeTest.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.plugin.core.symboltree.nodes;
+
+import static org.junit.Assert.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.Test;
+
+import docking.test.AbstractDockingTest;
+import docking.widgets.tree.GTreeNode;
+import ghidra.program.model.symbol.Symbol;
+import ghidra.program.model.symbol.StubSymbol;
+import ghidra.util.Swing;
+import ghidra.util.exception.AssertException;
+import ghidra.util.exception.CancelledException;
+import ghidra.util.task.TaskMonitor;
+
+public class OrganizationNodeTest extends AbstractDockingTest {
+
+ @Test
+ public void testOrganizeDoesNothingIfBelowMaxGroupSize() {
+ List nodeList =
+ nodes("AAA", "AAB", "AAB", "AABA", "BBA", "BBB", "BBC", "CCC", "DDD");
+ List result = organize(nodeList, 10);
+ assertEquals(nodeList, result);
+
+ result = organize(nodeList, 5);
+ assertNotEquals(nodeList, result);
+ }
+
+ @Test
+ public void testBasicPartitioning() {
+ List nodeList = nodes("AAA", "AAB", "AAC", "BBA", "BBB", "BBC", "CCC", "DDD");
+ List result = organize(nodeList, 5);
+ assertEquals(4, result.size());
+ assertEquals("AA", result.get(0).getName());
+ assertEquals("BB", result.get(1).getName());
+ assertEquals("CCC", result.get(2).getName());
+ assertEquals("DDD", result.get(3).getName());
+
+ GTreeNode aaGroup = result.get(0);
+ assertEquals(3, aaGroup.getChildCount());
+ assertEquals("AAA", aaGroup.getChild(0).getName());
+ assertEquals("AAB", aaGroup.getChild(1).getName());
+ assertEquals("AAC", aaGroup.getChild(2).getName());
+
+ GTreeNode bbGroup = result.get(1);
+ assertEquals(3, bbGroup.getChildCount());
+ }
+
+ @Test
+ public void testMultiLevel() {
+ List nodeList = nodes("A", "B", "CAA", "CAB", "CAC", "CAD", "CAE", "CAF", "CBA");
+ List result = organize(nodeList, 5);
+ assertEquals(3, result.size());
+ assertEquals("A", result.get(0).getName());
+ assertEquals("B", result.get(1).getName());
+ assertEquals("C", result.get(2).getName());
+
+ GTreeNode cGroup = result.get(2);
+ assertEquals(2, cGroup.getChildCount());
+ assertEquals("CA", cGroup.getChild(0).getName());
+ assertEquals("CBA", cGroup.getChild(1).getName());
+
+ GTreeNode caGroup = cGroup.getChild(0);
+ assertEquals(6, caGroup.getChildCount());
+ assertEquals("CAA", caGroup.getChild(0).getName());
+ assertEquals("CAF", caGroup.getChild(5).getName());
+ }
+
+ @Test
+ public void testManySameLabels() {
+ List nodeList =
+ nodes("A", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP",
+ "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP");
+
+ List result = organize(nodeList, 5);
+ assertEquals(2, result.size());
+ assertEquals("A", result.get(0).getName());
+ assertEquals("DUP", result.get(1).getName());
+
+ GTreeNode dupNode = result.get(1);
+ assertEquals(OrganizationNode.MAX_SAME_NAME + 1, dupNode.getChildCount());
+ assertEquals("11 more...", dupNode.getChild(OrganizationNode.MAX_SAME_NAME).getName());
+
+ }
+
+ @Test
+ public void testRemoveNotShownNode() {
+ List nodeList =
+ nodes("A", "D1", "D2", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP",
+ "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP");
+
+ List result = organize(nodeList, 5);
+
+ SymbolTreeNode dNode = (SymbolTreeNode) result.get(1);
+ GTreeNode dupNode = dNode.getChild(2);
+
+ assertEquals(OrganizationNode.MAX_SAME_NAME + 1, dupNode.getChildCount());
+ assertEquals("11 more...", dupNode.getChild(OrganizationNode.MAX_SAME_NAME).getName());
+
+ SymbolTreeNode node = (SymbolTreeNode) nodeList.get(nodeList.size() - 1);
+ simulateSmbolDeleted(dNode, node.getSymbol());
+
+ assertEquals("10 more...", dupNode.getChild(dupNode.getChildCount() - 1).getName());
+ }
+
+ @Test
+ public void testRemoveShownNode() {
+ List nodeList =
+ nodes("A", "D1", "D2", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP",
+ "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP");
+
+ List result = organize(nodeList, 5);
+
+ SymbolTreeNode dNode = (SymbolTreeNode) result.get(1);
+ GTreeNode dupNode = dNode.getChild(2);
+
+ assertEquals(OrganizationNode.MAX_SAME_NAME + 1, dupNode.getChildCount());
+ assertEquals("11 more...", dupNode.getChild(OrganizationNode.MAX_SAME_NAME).getName());
+
+ SymbolTreeNode node = (SymbolTreeNode) nodeList.get(4);
+ simulateSmbolDeleted(dNode, node.getSymbol());
+
+ assertEquals(OrganizationNode.MAX_SAME_NAME, dupNode.getChildCount());
+ assertEquals("11 more...", dupNode.getChild(dupNode.getChildCount() - 1).getName());
+ }
+
+ @Test
+ public void testAddDupNodeJustIncrementsCount() {
+ List nodeList =
+ nodes("A", "D1", "D2", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP",
+ "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP", "DUP");
+
+ List result = organize(nodeList, 5);
+
+ SymbolTreeNode dNode = (SymbolTreeNode) result.get(1);
+ GTreeNode dupNode = dNode.getChild(2);
+
+ assertEquals(OrganizationNode.MAX_SAME_NAME + 1, dupNode.getChildCount());
+ assertEquals("11 more...", dupNode.getChild(OrganizationNode.MAX_SAME_NAME).getName());
+
+ ((OrganizationNode) dNode).insertNode(node("DUP"));
+
+ assertEquals(OrganizationNode.MAX_SAME_NAME + 1, dupNode.getChildCount());
+ assertEquals("12 more...", dupNode.getChild(OrganizationNode.MAX_SAME_NAME).getName());
+
+ }
+
+ private void simulateSmbolDeleted(SymbolTreeNode root, Symbol symbolToDelete) {
+ SymbolNode key = SymbolNode.createKeyNode(symbolToDelete, symbolToDelete.getName(), null);
+ GTreeNode found = root.findSymbolTreeNode(key, false, TaskMonitor.DUMMY);
+ Swing.runNow(() -> found.getParent().removeNode(found));
+ }
+
+ private List organize(List list, int size) {
+ try {
+ return OrganizationNode.organize(list, size, TaskMonitor.DUMMY);
+ }
+ catch (CancelledException e) {
+ throw new AssertException("Can't happen");
+ }
+ }
+
+ private List nodes(String... names) {
+ List list = new ArrayList<>();
+ for (String name : names) {
+ list.add(node(name));
+ }
+ return list;
+ }
+
+ private GTreeNode node(String name) {
+ return new CodeSymbolNode(null, new StubSymbol(name, null));
+ }
+
+}
diff --git a/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/model/symbol/StubSymbol.java b/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/model/symbol/StubSymbol.java
new file mode 100644
index 0000000000..0c8d0b253c
--- /dev/null
+++ b/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/model/symbol/StubSymbol.java
@@ -0,0 +1,228 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.program.model.symbol;
+
+import ghidra.program.model.address.Address;
+import ghidra.program.model.listing.CircularDependencyException;
+import ghidra.program.model.listing.Program;
+import ghidra.program.util.ProgramLocation;
+import ghidra.util.exception.DuplicateNameException;
+import ghidra.util.exception.InvalidInputException;
+import ghidra.util.task.TaskMonitor;
+
+// Simple symbol test implementation
+public class StubSymbol implements Symbol {
+ private static long nextId = 0;
+
+ private long id;
+ private String name;
+ private Address address;
+
+ public StubSymbol(String name, Address address) {
+ this.name = name;
+ this.address = address;
+ id = nextId++;
+ }
+
+ @Override
+ public Address getAddress() {
+ return address;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String[] getPath() {
+ return new String[] { name };
+ }
+
+ @Override
+ public Program getProgram() {
+ return null;
+ }
+
+ @Override
+ public String getName(boolean includeNamespace) {
+ return name;
+ }
+
+ @Override
+ public Namespace getParentNamespace() {
+ return null;
+ }
+
+ @Override
+ public Symbol getParentSymbol() {
+ return null;
+ }
+
+ @Override
+ public boolean isDescendant(Namespace namespace) {
+ return false;
+ }
+
+ @Override
+ public boolean isValidParent(Namespace parent) {
+ return false;
+ }
+
+ @Override
+ public SymbolType getSymbolType() {
+ return SymbolType.LABEL;
+ }
+
+ @Override
+ public int getReferenceCount() {
+ return 0;
+ }
+
+ @Override
+ public boolean hasMultipleReferences() {
+ return false;
+ }
+
+ @Override
+ public boolean hasReferences() {
+ return false;
+ }
+
+ @Override
+ public Reference[] getReferences(TaskMonitor monitor) {
+ return null;
+ }
+
+ @Override
+ public Reference[] getReferences() {
+ return null;
+ }
+
+ @Override
+ public ProgramLocation getProgramLocation() {
+ return null;
+ }
+
+ @Override
+ public void setName(String newName, SourceType source)
+ throws DuplicateNameException, InvalidInputException {
+ this.name = newName;
+ }
+
+ @Override
+ public void setNamespace(Namespace newNamespace)
+ throws DuplicateNameException, InvalidInputException, CircularDependencyException {
+ // do nothing
+ }
+
+ @Override
+ public void setNameAndNamespace(String newName, Namespace newNamespace, SourceType source)
+ throws DuplicateNameException, InvalidInputException, CircularDependencyException {
+ this.name = newName;
+ }
+
+ @Override
+ public boolean delete() {
+ return false;
+ }
+
+ @Override
+ public boolean isPinned() {
+ return false;
+ }
+
+ @Override
+ public void setPinned(boolean pinned) {
+ // nothing
+ }
+
+ @Override
+ public boolean isDynamic() {
+ return false;
+ }
+
+ @Override
+ public boolean isExternal() {
+ return false;
+ }
+
+ @Override
+ public boolean isPrimary() {
+ return true;
+ }
+
+ @Override
+ public boolean setPrimary() {
+ return false;
+ }
+
+ @Override
+ public boolean isExternalEntryPoint() {
+ return false;
+ }
+
+ @Override
+ public long getID() {
+ return name.hashCode();
+ }
+
+ @Override
+ public Object getObject() {
+ return null;
+ }
+
+ @Override
+ public boolean isGlobal() {
+ return true;
+ }
+
+ @Override
+ public void setSource(SourceType source) {
+ // nothing
+ }
+
+ @Override
+ public SourceType getSource() {
+ return SourceType.USER_DEFINED;
+ }
+
+ @Override
+ public boolean isDeleted() {
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return (int) id;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ StubSymbol other = (StubSymbol) obj;
+ return id == other.id;
+ }
+
+}
From 59adf883430b3882ec77e1075ad1b3aa049c2422 Mon Sep 17 00:00:00 2001
From: ghidravore
Date: Tue, 3 Aug 2021 13:21:56 -0400
Subject: [PATCH 5/9] GP-116 added ability to save changes in options when
cancelling
---
.../core/analysis/AnalysisOptionsDialog.java | 46 +++++++++++++++++--
.../plugin/core/analysis/AnalysisPanel.java | 31 ++++---------
.../docking/options/editor/OptionsDialog.java | 16 ++++++-
.../docking/options/editor/OptionsPanel.java | 4 +-
4 files changed, 67 insertions(+), 30 deletions(-)
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisOptionsDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisOptionsDialog.java
index ca30b72063..e5cf8b91f8 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisOptionsDialog.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisOptionsDialog.java
@@ -15,9 +15,13 @@
*/
package ghidra.app.plugin.core.analysis;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
import java.util.List;
import docking.DialogComponentProvider;
+import docking.widgets.OptionDialog;
+import ghidra.GhidraOptions;
import ghidra.framework.options.EditorStateFactory;
import ghidra.program.model.listing.Program;
import ghidra.util.HelpLocation;
@@ -27,10 +31,12 @@ import ghidra.util.Msg;
* Dialog to show the panel for the auto analysis options.
*
*/
-public class AnalysisOptionsDialog extends DialogComponentProvider {
+public class AnalysisOptionsDialog extends DialogComponentProvider
+ implements PropertyChangeListener {
private boolean doAnalysis;
private AnalysisPanel panel;
private EditorStateFactory editorStateFactory = new EditorStateFactory();
+ private boolean hasChanges;
/**
* Constructor
@@ -49,16 +55,18 @@ public class AnalysisOptionsDialog extends DialogComponentProvider {
AnalysisOptionsDialog(List programs) {
super("Analysis Options");
setHelpLocation(new HelpLocation("AutoAnalysisPlugin", "AnalysisOptions"));
- panel = new AnalysisPanel(programs, editorStateFactory);
+ panel = new AnalysisPanel(programs, editorStateFactory, this);
addWorkPanel(panel);
addOKButton();
addCancelButton();
+ addApplyButton();
setOkButtonText("Analyze");
okButton.setMnemonic('A');
setOkEnabled(true);
setPreferredSize(1000, 600);
setRememberSize(true);
+ setHasChanges(panel.hasChangedValues());
}
@Override
@@ -73,9 +81,41 @@ public class AnalysisOptionsDialog extends DialogComponentProvider {
}
}
+ @Override
+ protected void cancelCallback() {
+ if (hasChanges) {
+ int result = OptionDialog.showYesNoCancelDialog(panel, "Save Changes?",
+ "These options are different from what is in the program.\n" +
+ "Do you want to save them to the program?");
+ if (result == OptionDialog.CANCEL_OPTION) {
+ return;
+ }
+ if (result == OptionDialog.YES_OPTION) {
+ panel.applyChanges();
+ }
+ }
+ close();
+ }
+
+ @Override
+ protected void applyCallback() {
+ panel.applyChanges();
+ setHasChanges(false);
+ }
+
boolean wasAnalyzeButtonSelected() {
return doAnalysis;
}
-}
+ private void setHasChanges(boolean hasChanges) {
+ this.hasChanges = hasChanges;
+ setApplyEnabled(hasChanges);
+ }
+ @Override
+ public void propertyChange(PropertyChangeEvent evt) {
+ if (evt.getPropertyName().equals(GhidraOptions.APPLY_ENABLED)) {
+ setHasChanges((Boolean) evt.getNewValue());
+ }
+ }
+}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisPanel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisPanel.java
index c432354a03..5e04cb76ee 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisPanel.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisPanel.java
@@ -101,18 +101,6 @@ class AnalysisPanel extends JPanel implements PropertyChangeListener {
this(List.of(program), editorStateFactory, propertyChangeListener);
}
- /**
- * Constructor
- *
- * @param programs list of programs that will be analyzed
- * @param editorStateFactory the editor factory
- * @param propertyChangeListener subscriber for property change notifications
- */
- AnalysisPanel(List programs, EditorStateFactory editorStateFactory) {
- this(programs, editorStateFactory, e -> {});
- }
-
-
/**
* Constructor
*
@@ -357,8 +345,8 @@ class AnalysisPanel extends JPanel implements PropertyChangeListener {
return;
}
File saveFile = getOptionsSaveFile(saveName);
- if (saveFile.exists() && OptionDialog.CANCEL_OPTION ==
- OptionDialog.showOptionDialogWithCancelAsDefaultButton(this, "Overwrite Configuration",
+ if (saveFile.exists() && OptionDialog.CANCEL_OPTION == OptionDialog
+ .showOptionDialogWithCancelAsDefaultButton(this, "Overwrite Configuration",
"Overwrite existing configuration file: " + saveName + " ?", "Overwrite")) {
return;
}
@@ -493,13 +481,12 @@ class AnalysisPanel extends JPanel implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
- if (checkForDifferencesWithProgram()) {
- propertyChangeListener.propertyChange(
- new PropertyChangeEvent(this, GhidraOptions.APPLY_ENABLED, null, Boolean.TRUE));
- }
+ boolean isDifferent = hasChangedValues();
+ propertyChangeListener.propertyChange(
+ new PropertyChangeEvent(this, GhidraOptions.APPLY_ENABLED, null, isDifferent));
}
- private boolean checkForDifferencesWithProgram() {
+ public boolean hasChangedValues() {
List analyzerStates = model.getModelData();
boolean changes = false;
for (AnalyzerEnablementState analyzerState : analyzerStates) {
@@ -784,11 +771,13 @@ class AnalysisPanel extends JPanel implements PropertyChangeListener {
String name = fileOptions.getName();
try {
fileOptions.save(getOptionsSaveFile(name));
- } catch (IOException e) {
+ }
+ catch (IOException e) {
Msg.error(this, "Error copying analysis options from previous Ghidra install", e);
}
}
}
+
private File getMostRecentApplicationSettingsDirWithSavedOptions() {
List ghidraUserDirsByTime = GenericRunInfo.getPreviousApplicationSettingsDirsByTime();
if (ghidraUserDirsByTime.size() == 0) {
@@ -805,7 +794,6 @@ class AnalysisPanel extends JPanel implements PropertyChangeListener {
return null;
}
-
private boolean isUserConfiguration(Options options) {
if (options == STANDARD_DEFAULT_OPTIONS ||
options == currentProgramOptions) {
@@ -833,4 +821,3 @@ class AnalysisPanel extends JPanel implements PropertyChangeListener {
deleteButton.setEnabled(isUserConfiguration(selectedOptions));
}
}
-
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/options/editor/OptionsDialog.java b/Ghidra/Framework/Docking/src/main/java/docking/options/editor/OptionsDialog.java
index 6206c3b4f0..0a89d0330a 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/options/editor/OptionsDialog.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/options/editor/OptionsDialog.java
@@ -22,6 +22,7 @@ import java.beans.PropertyChangeListener;
import javax.swing.tree.TreePath;
import docking.DialogComponentProvider;
+import docking.widgets.OptionDialog;
import ghidra.framework.options.Options;
/**
@@ -85,9 +86,20 @@ public class OptionsDialog extends DialogComponentProvider {
@Override
protected void cancelCallback() {
- if (panel.cancel()) {
- close();
+ // stop any active editors
+ panel.cancel();
+
+ if (hasChanges) {
+ int result = OptionDialog.showYesNoCancelDialog(panel, "Save Changes?",
+ "These options have changed. Do you want to save them?");
+ if (result == OptionDialog.CANCEL_OPTION) {
+ return;
+ }
+ if (result == OptionDialog.YES_OPTION) {
+ applyChanges();
+ }
}
+ close();
}
@Override
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/options/editor/OptionsPanel.java b/Ghidra/Framework/Docking/src/main/java/docking/options/editor/OptionsPanel.java
index 38ed5ff74e..f35b5bc41d 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/options/editor/OptionsPanel.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/options/editor/OptionsPanel.java
@@ -183,7 +183,7 @@ public class OptionsPanel extends JPanel {
processSelection(node);
}
- public boolean cancel() {
+ public void cancel() {
Set> entrySet = editorMap.entrySet();
for (Map.Entry entry : entrySet) {
OptionsEditor editor = entry.getValue();
@@ -199,8 +199,6 @@ public class OptionsPanel extends JPanel {
Msg.showError(this, this, title, title + "\nError Message: " + msg, e);
}
}
-
- return true;
}
public boolean apply() {
From 8ec719d4498043fb2525512b520a748ce114f2da Mon Sep 17 00:00:00 2001
From: tom
Date: Fri, 30 Jul 2021 09:37:45 -0400
Subject: [PATCH 6/9] GP-1181 override to address bug with high resolution
mouse wheel (Closes #3281, Closes #3284)
---
.../graph/visualization/DefaultGraphDisplay.java | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/DefaultGraphDisplay.java b/Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/DefaultGraphDisplay.java
index 61c07b36d2..0d07f4211f 100644
--- a/Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/DefaultGraphDisplay.java
+++ b/Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/DefaultGraphDisplay.java
@@ -297,7 +297,16 @@ public class DefaultGraphDisplay implements GraphDisplay {
Lens lens = Lens.builder().lensShape(Lens.Shape.RECTANGLE).magnification(3.f).build();
lens.setMagnification(2.f);
LensMagnificationGraphMousePlugin magnificationPlugin =
- new LensMagnificationGraphMousePlugin(1.f, 60.f, .2f);
+ new LensMagnificationGraphMousePlugin(1.f, 60.f, .2f) {
+ // Override to address a bug when using a high resolution mouse wheel.
+ // May be removed when jungrapht-visualization version is updated
+ @Override
+ public void mouseWheelMoved(MouseWheelEvent e) {
+ if (e.getWheelRotation() != 0) {
+ super.mouseWheelMoved(e);
+ }
+ }
+ };
MutableTransformer transformer = viewer.getRenderContext()
.getMultiLayerTransformer()
From bfd83ee1f64ca967e76755520fe779be8028600d Mon Sep 17 00:00:00 2001
From: emteere <47253321+emteere@users.noreply.github.com>
Date: Tue, 3 Aug 2021 22:13:01 +0000
Subject: [PATCH 7/9] GP-1184_emteere Allowing decompiler to produce results if
HighGlobal does not have a symref tag
---
.../main/java/ghidra/program/model/pcode/HighGlobal.java | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/pcode/HighGlobal.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/pcode/HighGlobal.java
index 54a236ae44..2177eddb6f 100644
--- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/pcode/HighGlobal.java
+++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/pcode/HighGlobal.java
@@ -17,6 +17,7 @@ package ghidra.program.model.pcode;
import ghidra.program.model.address.Address;
import ghidra.program.model.data.DataType;
+import ghidra.util.Msg;
import ghidra.util.xml.SpecXmlUtils;
import ghidra.xml.XmlElement;
import ghidra.xml.XmlPullParser;
@@ -58,10 +59,12 @@ public class HighGlobal extends HighVariable {
offset = SpecXmlUtils.decodeInt(attrString);
}
restoreInstances(parser, el);
- if (symref == 0) {
- throw new PcodeXMLException("Missing symref attribute in tag");
+ if (symref != 0) {
+ symbol = function.getGlobalSymbolMap().getSymbol(symref);
+ }
+ else {
+ Msg.warn(this, "Missing symref attribute in tag");
}
- symbol = function.getGlobalSymbolMap().getSymbol(symref);
if (symbol == null) { // If we don't already have symbol, synthesize it
DataType symbolType;
int symbolSize;
From 86a85afd1b6734aae561c71ec92d60dc126abbef Mon Sep 17 00:00:00 2001
From: ghidorahrex
Date: Mon, 2 Aug 2021 14:06:00 -0400
Subject: [PATCH 8/9] GP-1152 Fixed issue with superh fmov/fmov.s
decrement/read ordering
---
Ghidra/Processors/SuperH4/data/languages/SuperH4.sinc | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Ghidra/Processors/SuperH4/data/languages/SuperH4.sinc b/Ghidra/Processors/SuperH4/data/languages/SuperH4.sinc
index 724c889b7f..3fa465b6a2 100644
--- a/Ghidra/Processors/SuperH4/data/languages/SuperH4.sinc
+++ b/Ghidra/Processors/SuperH4/data/languages/SuperH4.sinc
@@ -1421,12 +1421,12 @@ N_0txx: r0",@"^N_0 is r0 & N_0 { tmp:4 = N_0; export tmp; }
OP_0=0xf & N_0t_at_neg & XDRM & FRM_0 & OP_1=0xb {
if (!( $(FPSCR_SZ) == 0 )) goto ;
- *:4 N_0t_at_neg = FRM_0;
N_0t_at_neg = N_0t_at_neg - 4;
+ *:4 N_0t_at_neg = FRM_0;
goto ;
- *:8 N_0t_at_neg = XDRM;
N_0t_at_neg = N_0t_at_neg - 8;
+ *:8 N_0t_at_neg = XDRM;
}
From 17f9cf0bc7311e73d9e013a7786a215533fbc6e5 Mon Sep 17 00:00:00 2001
From: ghidra1
Date: Tue, 3 Aug 2021 19:14:39 -0400
Subject: [PATCH 9/9] Updated Change History for 10.0.2
---
.../src/global/docs/ChangeHistory.html | 44 +++++++++++++++++++
1 file changed, 44 insertions(+)
diff --git a/Ghidra/Configurations/Public_Release/src/global/docs/ChangeHistory.html b/Ghidra/Configurations/Public_Release/src/global/docs/ChangeHistory.html
index af41b69e2c..29a991442b 100644
--- a/Ghidra/Configurations/Public_Release/src/global/docs/ChangeHistory.html
+++ b/Ghidra/Configurations/Public_Release/src/global/docs/ChangeHistory.html
@@ -7,6 +7,50 @@
+Ghidra 10.0.2 Change History (August 2021)
+New Features
+
+ - Scripting. Created an example script which demonstrates how to use the FileBytes class to do a binary export of the current program. (GP-1157)
+
+
+Improvements
+
+ - Data Types. When creating a substructure from existing components, the new structure will adopt the pack setting of the parent structure from which it was created. Note that a packed structure may still move based upon component alignment rules. (GP-1111, Issue #3193)
+ - Decompiler. Added E key binding to the Decompiler's Equate action. (GP-1146, Issue #3195)
+ - GUI. Added Apply button to analysis options dialog. Also added a last chance save/cancel dialog that is shown when a user cancels an options dialog that has unsaved changes. (GP-1169, Issue #3274)
+ - Scripting. For stripped gcc binaries, improved prototype RecoverClassesFromRTTIScript identification of vtables and simple class data, constructors, and destructors. (GP-1055, Issue #3266)
+
+
+Bugs
+
+ - Basic Infrastructure. Fixed regression that prevented Ghidra from launching on Windows when its path contained spaces. (GP-1113, Issue #3201, #3205)
+ - Data Types. Fixed IllegalArgumentException error message when adding a duplicate enumerate name for EnumDataType. (GP-1173, Issue #3246)
+ - Debugger. Changed diagnostics to write GDB.log to user directory, not installation. Clarified an error message. (GP-1133, Issue #3218)
+ - Debugger. Improved error reporting when failing to start a Debugger GADP agent. (GP-1136, Issue #3175)
+ - Debugger. Added system property to toggle alternative icons/colors for breakpoints. (GP-1139, Issue #3204)
+ - Debugger. Applying a default everything memory map for GDB targets if info proc mappings fails or produces an empty list. (GP-1142, Issue #3071, #3074, #3161, #3169)
+ - Debugger. Fixed issue with Debugger ignoring JAVA_HOME when launching child JVM. (GP-1143, Issue #3231)
+ - Debugger. Fixed command-reply matching issue when using GDB via SSH. (GP-1153, Issue #3238)
+ - Debugger:Emulator. Fixed bug in Trace Emulation causing ArrayIndexOutOfBoundsExceptions. (GP-1058)
+ - Decompiler. The decompiler now shows results when a HighGlobal has no associated symbol reference in the program. (GP-1184)
+ - DWARF. Changed processing to ignore incomplete DWARF parameter lists in Rust binaries. (GP-1121, Issue #3060)
+ - Exporter. The C/C++ Exporter now emits semicolons after function prototypes when using the Create Header File option. (GP-1145, Issue #1644)
+ - Framework. Corrected address comparison for 64-bit signed address spaces (e.g., stack space, constant space) which could produce non-transitive comparison results. (GP-1178, Issue #3302)
+ - Graphing. Corrected graph magnification behavior when using a high resolution mouse wheel. (GP-1181, Issue #3281, #3284)
+ - GUI. Fixed NullPointerException when Hovering in Decompiler over a function that is not in memory. (GP-1131)
+ - GUI. Fixed bug in Find References to search results that prevented '<' characters from being rendered. (GP-1137, Issue #3217)
+ - GUI. Fixed issue where duplicate label names could cause the symbol tree to become unstable, evidenced by broken display and scrolling actions. Also, improved grouping algorithm. (GP-1159, Issue #3264)
+ - GUI. Fixed Enter key in Set Equates dialog to choose the selected table row. Updated the Function Signature Editor dialog to allow the Cancel key to close the dialog when the focus is in the top text editor. (GP-1162, Issue #3235)
+ - Headless. Fixed a regression in analyzeHeadless.bat that prevented the headless analyzer from running on Windows in some cases. (GP-1156, Issue #3261)
+ - Importer. The MzLoader now populates the relocation table when relocations are performed. (GP-1160)
+ - Importer:ELF. Corrected dynamic GOT/PLT markup problem for images which do not contain section headers. In cases where image does not define symbols within the PLT, analysis may be relied upon for its disassembly. ELF Importer's goal is to migrate symbols which may be defined within the PLT to the External symbol space. (GP-1110, Issue #3198)
+ - Importer:Mach-O. The Mach-O importer now correctly interprets indirect symbols as references to symbols within another .dylib. (GP-1120)
+ - Processors. Fixed bug in SuperH4
fmov.s
pcode. (GP-1152)
+ - Processors. The ARM instruction semantics for the mulitple-single-element forms of the
vld1
/vst1
vector instructions have been corrected. (GP-1167)
+ - Sleigh. Fixed a string formatting error in the sleigh compiler. (GP-1124, Issue #3168)
+
+
+
Ghidra 10.0.1 Change History (July 2021)
New Features