diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/reachability/FunctionReachabilityTableModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/reachability/FunctionReachabilityTableModel.java index ce4b9b178b..72a294356d 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/reachability/FunctionReachabilityTableModel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/reachability/FunctionReachabilityTableModel.java @@ -87,6 +87,10 @@ public class FunctionReachabilityTableModel Accumulator> pathAccumulator = new PassThroughAccumulator(accumulator); + if (v1.equals(v2)) { + return; + } + monitor.setMessage("Finding paths..."); GraphAlgorithms.findPaths(graph, v1, v2, pathAccumulator, monitor); } diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/GraphAlgorithms.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/GraphAlgorithms.java index 66b8093fde..e225088557 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/GraphAlgorithms.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/GraphAlgorithms.java @@ -388,6 +388,7 @@ public class GraphAlgorithms { * @param monitor the timeout task monitor * @return the circuits * @throws CancelledException if the monitor is cancelled + * @throws TimeoutException if the algorithm times-out, as defined by the monitor */ public static > List> findCircuits(GDirectedGraph g, boolean uniqueCircuits, TimeoutTaskMonitor monitor) @@ -405,10 +406,6 @@ public class GraphAlgorithms { *

Warning: for large, dense graphs (those with many interconnected * vertices) this algorithm could run indeterminately, possibly causing the JVM to * run out of memory. - * - *

Warning: This is a recursive algorithm. As such, it is limited in how - * deep it can recurse. Any path that exceeds the {@link #JAVA_STACK_DEPTH_LIMIT} will - * not be found. * *

You are encouraged to call this method with a monitor that will limit the work to * be done, such as the {@link TimeoutTaskMonitor}. @@ -423,8 +420,8 @@ public class GraphAlgorithms { public static > void findPaths(GDirectedGraph g, V start, V end, Accumulator> accumulator, TaskMonitor monitor) throws CancelledException { - // the algorithm gets run at construction; the results are sent to the accumulator - new FindPathsAlgorithm<>(g, start, end, accumulator, monitor); + IterativeFindPathsAlgorithm algo = new IterativeFindPathsAlgorithm<>(); + algo.findPaths(g, start, end, accumulator, monitor); } /** @@ -436,10 +433,6 @@ public class GraphAlgorithms { *

Warning: for large, dense graphs (those with many interconnected * vertices) this algorithm could run indeterminately, possibly causing the JVM to * run out of memory. - * - *

Warning: This is a recursive algorithm. As such, it is limited in how - * deep it can recurse. Any path that exceeds the {@link #JAVA_STACK_DEPTH_LIMIT} will - * not be found. * * @param g the graph * @param start the start vertex @@ -453,8 +446,8 @@ public class GraphAlgorithms { Accumulator> accumulator, TimeoutTaskMonitor monitor) throws CancelledException, TimeoutException { - // the algorithm gets run at construction; the results are sent to the accumulator - new FindPathsAlgorithm<>(g, start, end, accumulator, monitor); + FindPathsAlgorithm algo = new IterativeFindPathsAlgorithm<>(); + algo.findPaths(g, start, end, accumulator, monitor); } /** @@ -549,6 +542,7 @@ public class GraphAlgorithms { * A method to debug the given graph by printing it. * * @param g the graph to print + * @param ps the output stream */ public static > void printGraph(GDirectedGraph g, PrintStream ps) { Set sources = getSources(g); diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/algo/FindPathsAlgorithm.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/algo/FindPathsAlgorithm.java index 6e964bb1e8..2c0a1501c5 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/algo/FindPathsAlgorithm.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/algo/FindPathsAlgorithm.java @@ -15,7 +15,7 @@ */ package ghidra.graph.algo; -import java.util.*; +import java.util.List; import ghidra.graph.GDirectedGraph; import ghidra.graph.GEdge; @@ -23,122 +23,10 @@ import ghidra.util.datastruct.Accumulator; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; -/** - * Finds all paths between two vertices for a given graph. - * - *

Warning: This is a recursive algorithm. As such, it is limited in how deep - * it can recurse. Any path that exceeds the {@link #JAVA_STACK_DEPTH_LIMIT} will not be found. - * - *

Note: this algorithm is based entirely on the {@link JohnsonCircuitsAlgorithm}. - * - * @param the vertex type - * @param the edge type - */ -public class FindPathsAlgorithm> { +public interface FindPathsAlgorithm> { - public static final int JAVA_STACK_DEPTH_LIMIT = 2700; - private GDirectedGraph g; - private V startVertex; - private V endVertex; + public void findPaths(GDirectedGraph g, V start, V end, Accumulator> accumulator, + TaskMonitor monitor) throws CancelledException; - private Stack stack = new Stack<>(); - private Set blockedSet = new HashSet<>(); - private Map> blockedBackEdgesMap = new HashMap<>(); - - public FindPathsAlgorithm(GDirectedGraph g, V start, V end, - Accumulator> accumulator, TaskMonitor monitor) throws CancelledException { - this.g = g; - this.startVertex = start; - this.endVertex = end; - - monitor.initialize(g.getEdgeCount()); - find(accumulator, monitor); - } - - private void find(Accumulator> accumulator, TaskMonitor monitor) - throws CancelledException { - - explore(startVertex, accumulator, 0, monitor); - } - - private boolean explore(V v, Accumulator> accumulator, int depth, TaskMonitor monitor) - throws CancelledException { - - // TODO - // Sigh. We are greatly limited in the size of paths we can processes due to the - // recursive nature of this algorithm. This should be changed to be non-recursive. - if (depth > JAVA_STACK_DEPTH_LIMIT) { - return false; - } - - boolean foundPath = false; - blockedSet.add(v); - stack.push(v); - Collection outEdges = getOutEdges(v); - for (E e : outEdges) { - monitor.checkCanceled(); - - V u = e.getEnd(); - if (u.equals(endVertex)) { - outputCircuit(accumulator); - foundPath = true; - monitor.incrementProgress(1); - } - else if (!blockedSet.contains(u)) { - foundPath |= explore(u, accumulator, depth + 1, monitor); - monitor.incrementProgress(1); - } - } - - if (foundPath) { - unblock(v); - } - else { - for (E e : outEdges) { - monitor.checkCanceled(); - V u = e.getEnd(); - addBackEdge(u, v); - } - } - - stack.pop(); - return foundPath; - } - - private Collection getOutEdges(V v) { - Collection outEdges = g.getOutEdges(v); - if (outEdges == null) { - return Collections.emptyList(); - } - return outEdges; - } - - private void unblock(V v) { - blockedSet.remove(v); - Set set = blockedBackEdgesMap.get(v); - if (set == null) { - return; - } - for (V u : set) { - if (blockedSet.contains(u)) { - unblock(u); - } - } - set.clear(); - } - - private void addBackEdge(V u, V v) { - Set set = blockedBackEdgesMap.get(u); - if (set == null) { - set = new HashSet<>(); - blockedBackEdgesMap.put(u, set); - } - set.add(v); - } - - private void outputCircuit(Accumulator> accumulator) { - List path = new ArrayList<>(stack); - path.add(endVertex); - accumulator.add(path); - } + public void setStatusListener(GraphAlgorithmStatusListener listener); } diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/algo/GraphAlgorithmStatusListener.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/algo/GraphAlgorithmStatusListener.java new file mode 100644 index 0000000000..ef41151194 --- /dev/null +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/algo/GraphAlgorithmStatusListener.java @@ -0,0 +1,43 @@ +/* ### + * 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.graph.algo; + +/** + * An interface and state values used to follow the state of vertices as they are processed by + * algorithms + * + * @param the vertex type + */ +public class GraphAlgorithmStatusListener { + + public enum STATUS { + WAITING, SCHEDULED, EXPLORING, BLOCKED, IN_PATH, + } + + protected int totalStatusChanges; + + public void statusChanged(V v, STATUS s) { + // stub + } + + public void finished() { + // stub + } + + public int getTotalStatusChanges() { + return totalStatusChanges; + } +} diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/algo/IterativeFindPathsAlgorithm.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/algo/IterativeFindPathsAlgorithm.java new file mode 100644 index 0000000000..d8e54f9b8c --- /dev/null +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/algo/IterativeFindPathsAlgorithm.java @@ -0,0 +1,247 @@ +/* ### + * 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.graph.algo; + +import java.util.*; + +import org.apache.commons.collections4.map.LazyMap; +import org.apache.commons.collections4.set.ListOrderedSet; + +import ghidra.graph.GDirectedGraph; +import ghidra.graph.GEdge; +import ghidra.graph.algo.GraphAlgorithmStatusListener.STATUS; +import ghidra.util.datastruct.Accumulator; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * Finds all paths between two vertices for a given graph. + * + *

Note: this algorithm is based on the {@link JohnsonCircuitsAlgorithm}, modified to be + * iterative instead of recursive. + * + * @param the vertex type + * @param the edge type + */ +public class IterativeFindPathsAlgorithm> + implements FindPathsAlgorithm { + + private GDirectedGraph g; + private V start; + private V end; + + private Set blockedSet = new HashSet<>(); + private Map> blockedBackEdgesMap = + LazyMap.lazyMap(new HashMap<>(), k -> new HashSet<>()); + + private GraphAlgorithmStatusListener listener = new GraphAlgorithmStatusListener<>(); + private TaskMonitor monitor; + private Accumulator> accumulator; + + @Override + public void setStatusListener(GraphAlgorithmStatusListener listener) { + this.listener = listener; + } + + @SuppressWarnings("hiding") // squash warning on names of variables + @Override + public void findPaths(GDirectedGraph g, V start, V end, Accumulator> accumulator, + TaskMonitor monitor) throws CancelledException { + this.g = g; + this.start = start; + this.end = end; + this.accumulator = accumulator; + this.monitor = monitor; + + if (start.equals(end)) { + // can't find the paths between a node and itself + throw new IllegalArgumentException("Start and end vertex cannot be the same: " + start); + } + + if (!g.containsVertex(start)) { + throw new IllegalArgumentException("Start vertex is not in the graph: " + start); + } + + if (!g.containsVertex(end)) { + throw new IllegalArgumentException("End vertex is not in the graph: " + end); + } + + find(); + listener.finished(); + } + + private void find() throws CancelledException { + Stack path = new Stack<>(); + path.push(new Node(null, start)); + + monitor.initialize(g.getEdgeCount()); + + while (!path.isEmpty()) { + + monitor.checkCanceled(); + monitor.incrementProgress(1); + Node node = path.peek(); + + setStatus(node.v, STATUS.EXPLORING); + + if (node.v.equals(end)) { + outputCircuit(path); + node.setParentFound(); + path.pop(); + } + else if (node.isExplored()) { + node.setDone(); + path.pop(); + } + else { + node = node.getNext(); + path.push(node); + } + } + } + + private void unblock(V v) { + + ListOrderedSet toProcess = new ListOrderedSet<>(); + toProcess.add(v); + + while (!toProcess.isEmpty()) { + V next = toProcess.remove(0); + Set childBlocked = doUnblock(next); + if (childBlocked != null && !childBlocked.isEmpty()) { + toProcess.addAll(childBlocked); + childBlocked.clear(); + } + } + } + + private Set doUnblock(V v) { + + blockedSet.remove(v); + setStatus(v, STATUS.WAITING); + Set set = blockedBackEdgesMap.get(v); + return set; + } + + private void blockBackEdge(V u, V v) { + Set set = blockedBackEdgesMap.get(u); + set.add(v); + } + + private void outputCircuit(Stack stack) throws CancelledException { + List path = new ArrayList<>(); + for (Node vv : stack) { + path.add(vv.v); + } + setStatus(path, STATUS.IN_PATH); + accumulator.add(path); + + monitor.checkCanceled(); // pause for listener + } + + private void setStatus(List path, STATUS s) { + for (V v : path) { + listener.statusChanged(v, s); + } + } + + private void setStatus(V v, STATUS s) { + if (blockedSet.contains(v) && s == STATUS.WAITING) { + listener.statusChanged(v, STATUS.BLOCKED); + } + else { + listener.statusChanged(v, s); + } + } + + private Collection getOutEdges(V v) { + Collection outEdges = g.getOutEdges(v); + if (outEdges == null) { + return Collections.emptyList(); + } + return outEdges; + } +//================================================================================================== +// Inner Classes +//================================================================================================== + + /** + * Simple class to maintain a relationship between a given node and its children that need + * processing. It also knows if it has been found in a path from start to end. + */ + private class Node { + private Node parent; + private V v; + private Deque unexplored; + private boolean found; + + Node(Node parent, V v) { + this.parent = parent; + this.v = v; + + blockedSet.add(v); + setStatus(v, STATUS.SCHEDULED); + + Collection outEdges = getOutEdges(v); + unexplored = new ArrayDeque<>(outEdges.size()); + for (E e : getOutEdges(v)) { + V u = e.getEnd(); + if (!blockedSet.contains(u)) { + unexplored.add(u); + } + } + } + + void setDone() { + if (found) { + setParentFound(); + } + else { + // block back edges + for (E e : getOutEdges(v)) { + V u = e.getEnd(); + blockBackEdge(u, v); + } + setStatus(v, STATUS.BLOCKED); + } + } + + void setParentFound() { + if (parent != null) { + parent.found = true; + } + unblock(v); + } + + boolean isExplored() { + return unexplored.isEmpty(); + } + + Node getNext() { + if (isExplored()) { + return null; + } + + Node node = new Node(this, unexplored.pop()); + return node; + } + + @Override + public String toString() { + return v.toString(); + } + } +} diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/algo/RecursiveFindPathsAlgorithm.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/algo/RecursiveFindPathsAlgorithm.java new file mode 100644 index 0000000000..1a332cc723 --- /dev/null +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/algo/RecursiveFindPathsAlgorithm.java @@ -0,0 +1,178 @@ +/* ### + * 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.graph.algo; + +import java.util.*; + +import ghidra.graph.GDirectedGraph; +import ghidra.graph.GEdge; +import ghidra.graph.algo.GraphAlgorithmStatusListener.STATUS; +import ghidra.util.datastruct.Accumulator; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * Finds all paths between two vertices for a given graph. + * + *

Warning: This is a recursive algorithm. As such, it is limited in how deep + * it can recurse. Any path that exceeds the {@link #JAVA_STACK_DEPTH_LIMIT} will not be found. + * + *

Note: this algorithm is based entirely on the {@link JohnsonCircuitsAlgorithm}. + * + * @param the vertex type + * @param the edge type + */ +public class RecursiveFindPathsAlgorithm> + implements FindPathsAlgorithm { + + public static final int JAVA_STACK_DEPTH_LIMIT = 2700; + private GDirectedGraph g; + private V startVertex; + private V endVertex; + + private Stack stack = new Stack<>(); + private Set blockedSet = new HashSet<>(); + private Map> blockedBackEdgesMap = new HashMap<>(); + private Accumulator> accumulator; + private TaskMonitor monitor; + + private GraphAlgorithmStatusListener listener = new GraphAlgorithmStatusListener<>(); + + @Override + public void setStatusListener(GraphAlgorithmStatusListener listener) { + this.listener = listener; + } + + @SuppressWarnings("hiding") // squash warning on names of variables + @Override + public void findPaths(GDirectedGraph g, V start, V end, Accumulator> accumulator, + TaskMonitor monitor) throws CancelledException { + this.g = g; + this.startVertex = start; + this.endVertex = end; + this.accumulator = accumulator; + this.monitor = monitor; + + explore(startVertex, 0); + listener.finished(); + } + + private boolean explore(V v, int depth) throws CancelledException { + + // TODO + // Sigh. We are greatly limited in the size of paths we can processes due to the + // recursive nature of this algorithm. This should be changed to be non-recursive. + if (depth > JAVA_STACK_DEPTH_LIMIT) { + return false; + } + + boolean foundPath = false; + blockedSet.add(v); + stack.push(v); + + setStatus(v, STATUS.EXPLORING); + + Collection outEdges = getOutEdges(v); + for (E e : outEdges) { + monitor.checkCanceled(); + + V u = e.getEnd(); + if (u.equals(endVertex)) { + outputCircuit(); + foundPath = true; + monitor.incrementProgress(1); + } + else if (!blockedSet.contains(u)) { + foundPath |= explore(u, depth + 1); + monitor.incrementProgress(1); + } + } + + if (foundPath) { + unblock(v); + } + else { + for (E e : outEdges) { + monitor.checkCanceled(); + V u = e.getEnd(); + blockBackEdge(u, v); + } + } + + stack.pop(); + setStatus(v, STATUS.WAITING); + return foundPath; + } + + private Collection getOutEdges(V v) { + Collection outEdges = g.getOutEdges(v); + if (outEdges == null) { + return Collections.emptyList(); + } + return outEdges; + } + + private void unblock(V v) { + blockedSet.remove(v); + setStatus(v, STATUS.WAITING); + + Set set = blockedBackEdgesMap.get(v); + if (set == null) { + return; + } + for (V u : set) { + if (blockedSet.contains(u)) { + unblock(u); + } + } + set.clear(); + } + + private void blockBackEdge(V u, V v) { + Set set = blockedBackEdgesMap.get(u); + if (set == null) { + set = new HashSet<>(); + blockedBackEdgesMap.put(u, set); + } + set.add(v); + setStatus(v, STATUS.BLOCKED); + } + + private void outputCircuit() throws CancelledException { + List path = new LinkedList<>(stack); + path.add(endVertex); + setStatus(path, STATUS.IN_PATH); + + accumulator.add(path); + monitor.checkCanceled(); // pause for listener + setStatus(endVertex, STATUS.WAITING); + } + + private void setStatus(List path, STATUS s) { + for (V v : path) { + listener.statusChanged(v, s); + } + } + + private void setStatus(V v, STATUS s) { + if (blockedSet.contains(v) && s == STATUS.WAITING) { + listener.statusChanged(v, STATUS.BLOCKED); + } + else { + listener.statusChanged(v, s); + } + } +} diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/edge/VisualGraphPathHighlighter.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/edge/VisualGraphPathHighlighter.java index f6bf916447..c80465fb0d 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/edge/VisualGraphPathHighlighter.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/edge/VisualGraphPathHighlighter.java @@ -722,10 +722,12 @@ public class VisualGraphPathHighlighter> accumulator = new CallbackAccumulator<>(path -> { Collection edges = pathToEdgesAsync(path); diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/AbstractVisualGraphLayout.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/AbstractVisualGraphLayout.java index 6b8667cb30..9c42ca1e30 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/AbstractVisualGraphLayout.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/AbstractVisualGraphLayout.java @@ -33,6 +33,7 @@ import ghidra.graph.VisualGraph; import ghidra.graph.viewer.*; import ghidra.graph.viewer.layout.LayoutListener.ChangeType; import ghidra.graph.viewer.renderer.ArticulatedEdgeRenderer; +import ghidra.graph.viewer.renderer.VisualGraphRenderer; import ghidra.graph.viewer.shape.ArticulatedEdgeTransformer; import ghidra.graph.viewer.vertex.VisualGraphVertexShapeTransformer; import ghidra.util.datastruct.WeakDataStructureFactory; @@ -308,8 +309,8 @@ public abstract class AbstractVisualGraphLayout) visualGraph, - // layoutLocations.copy()); +// VisualGraphRenderer.DEBUG_ROW_COL_MAP.put((Graph) visualGraph, +// layoutLocations.copy()); Rectangle graphBounds = getTotalGraphSize(vertexLayoutLocations, edgeLayoutArticulationLocations, transformer); diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/renderer/VisualGraphRenderer.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/renderer/VisualGraphRenderer.java index 70c93bf3cb..3ced28d8b7 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/renderer/VisualGraphRenderer.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/renderer/VisualGraphRenderer.java @@ -118,6 +118,7 @@ public class VisualGraphRenderer private void paintLayoutGridCells(RenderContext renderContext, Layout layout) { + // to enable this debug, search java files for commented-out uses of 'DEBUG_ROW_COL_MAP' Graph graph = layout.getGraph(); LayoutLocationMap locationMap = DEBUG_ROW_COL_MAP.get(graph); if (locationMap == null) { diff --git a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/AbstractGraphAlgorithmsTest.java b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/AbstractGraphAlgorithmsTest.java index 9de8fe44de..30906d9757 100644 --- a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/AbstractGraphAlgorithmsTest.java +++ b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/AbstractGraphAlgorithmsTest.java @@ -224,6 +224,34 @@ public abstract class AbstractGraphAlgorithmsTest extends AbstractGenericTest { return null; } + protected void assertPathExists(List> paths, TestV... vertices) { + + List expectedPath = List.of(vertices); + for (List path : paths) { + if (path.equals(expectedPath)) { + return; + } + } + fail("List of paths does not contain: " + expectedPath + "\n\tactual paths: " + paths); + } + + @SafeVarargs + protected final void assertListEqualsOneOf(List actual, List... expected) { + + StringBuilder buffy = new StringBuilder(); + for (List list : expected) { + if (areListsEquals(actual, list)) { + return; + } + buffy.append(list.toString()); + } + fail("Expected : " + buffy + "\nActual: " + actual); + } + + private boolean areListsEquals(List l1, List l2) { + return l1.equals(l2); + } + //================================================================================================== // Inner Classes //================================================================================================== @@ -245,6 +273,34 @@ public abstract class AbstractGraphAlgorithmsTest extends AbstractGenericTest { return id; } +// TODO put this in +// +// @Override +// public int hashCode() { +// final int prime = 31; +// int result = 1; +// result = prime * result + ((id == null) ? 0 : id.hashCode()); +// return result; +// } +// +// @Override +// public boolean equals(Object obj) { +// if (this == obj) { +// return true; +// } +// if (obj == null) { +// return false; +// } +// if (getClass() != obj.getClass()) { +// return false; +// } +// +// TestV other = (TestV) obj; +// if (!Objects.equals(id, other.id)) { +// return false; +// } +// return true; +// } } protected static class TestE extends DefaultGEdge { diff --git a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/GraphAlgorithmsTest.java b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/GraphAlgorithmsTest.java index 5d72906c86..d32a0c99ff 100644 --- a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/GraphAlgorithmsTest.java +++ b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/GraphAlgorithmsTest.java @@ -26,7 +26,7 @@ import org.junit.Assert; import org.junit.Test; import ghidra.graph.algo.*; -import ghidra.util.datastruct.Accumulator; +import ghidra.util.Msg; import ghidra.util.datastruct.ListAccumulator; import ghidra.util.exception.CancelledException; import ghidra.util.exception.TimeoutException; @@ -1105,23 +1105,6 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { Arrays.asList(v1, v2, v4, v5, v6, v3)); } - @SafeVarargs - private final void assertListEqualsOneOf(List actual, List... expected) { - - StringBuilder buffy = new StringBuilder(); - for (List list : expected) { - if (areListsEquals(actual, list)) { - return; - } - buffy.append(list.toString()); - } - fail("Expected : " + buffy + "\nActual: " + actual); - } - - private boolean areListsEquals(List l1, List l2) { - return l1.equals(l2); - } - @Test public void testDepthFirstPostOrder_MiddleAlternatingPaths() { /* @@ -1362,19 +1345,355 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { } @Test - public void testFindPaths_LimitRecursion() throws CancelledException { + public void testFindPaths_FullyConnected() throws CancelledException { - g = GraphFactory.createDirectedGraph(); - TestV[] vertices = - generateSimplyConnectedGraph(FindPathsAlgorithm.JAVA_STACK_DEPTH_LIMIT + 100); + TestV v1 = vertex(1); + TestV v2 = vertex(2); + TestV v3 = vertex(3); - TestV v1 = vertices[0]; - TestV v2 = vertices[vertices.length - 1]; + edge(v1, v2); + edge(v1, v3); - Accumulator> accumulator = new ListAccumulator<>(); + edge(v2, v3); + edge(v2, v1); + + edge(v3, v2); + edge(v3, v1); + + ListAccumulator> accumulator = new ListAccumulator<>(); GraphAlgorithms.findPaths(g, v1, v2, accumulator, TaskMonitor.DUMMY); - assertTrue("The Find Paths algorithm should have failed, due to hitting the recursion " + - "limite, before finding a path", accumulator.isEmpty()); + + List> paths = accumulator.asList(); + assertPathExists(paths, v1, v2); + assertPathExists(paths, v1, v3, v2); + } + + @Test + public void testFindPaths_FullyConnected2() throws CancelledException { + + TestV v1 = vertex(1); + TestV v2 = vertex(2); + TestV v3 = vertex(3); + TestV v4 = vertex(4); + TestV v5 = vertex(5); + + edge(v1, v2); + edge(v1, v3); + edge(v1, v4); + edge(v1, v5); + + edge(v2, v1); + edge(v2, v3); + edge(v2, v4); + edge(v2, v5); + + edge(v3, v1); + edge(v3, v2); + edge(v3, v4); + edge(v3, v5); + + edge(v4, v1); + edge(v4, v2); + edge(v4, v3); + edge(v4, v5); + + edge(v5, v1); + edge(v5, v2); + edge(v5, v3); + edge(v5, v4); + + ListAccumulator> accumulator = new ListAccumulator<>(); + GraphAlgorithms.findPaths(g, v1, v5, accumulator, TaskMonitor.DUMMY); + + List> paths = accumulator.asList(); + assertEquals(16, paths.size()); + assertPathExists(paths, v1, v5); + + assertPathExists(paths, v1, v2, v5); + assertPathExists(paths, v1, v3, v5); + assertPathExists(paths, v1, v4, v5); + + assertPathExists(paths, v1, v2, v3, v5); + assertPathExists(paths, v1, v2, v4, v5); + assertPathExists(paths, v1, v3, v2, v5); + assertPathExists(paths, v1, v3, v4, v5); + assertPathExists(paths, v1, v4, v2, v5); + assertPathExists(paths, v1, v4, v3, v5); + + assertPathExists(paths, v1, v2, v3, v4, v5); + assertPathExists(paths, v1, v2, v4, v3, v5); + assertPathExists(paths, v1, v3, v2, v4, v5); + assertPathExists(paths, v1, v3, v4, v2, v5); + assertPathExists(paths, v1, v4, v2, v3, v5); + assertPathExists(paths, v1, v4, v3, v2, v5); + } + + @Test + public void testFindPaths_MultiPaths() throws CancelledException { + + /* + v1 + / \ + v2 v3 + | / | \ + | v4 v5 v6 + | * | | + | | v7 + | | | \ + | | v8 v9 + | | * | + \ | / + \ | / + \|/ + v10 + + + Paths: + v1, v2, v10 + v1, v3, v5, v10 + v1, v3, v6, v7, v9, v10 + */ + + TestV v1 = vertex(1); + TestV v2 = vertex(2); + TestV v3 = vertex(3); + TestV v4 = vertex(4); + TestV v5 = vertex(5); + TestV v6 = vertex(6); + TestV v7 = vertex(7); + TestV v8 = vertex(8); + TestV v9 = vertex(9); + TestV v10 = vertex(10); + + edge(v1, v2); + edge(v1, v3); + + edge(v2, v10); + + edge(v3, v4); + edge(v3, v5); + edge(v3, v6); + + edge(v5, v10); + + edge(v6, v7); + edge(v7, v8); + edge(v7, v9); + + edge(v9, v10); + + ListAccumulator> accumulator = new ListAccumulator<>(); + GraphAlgorithms.findPaths(g, v1, v10, accumulator, TaskMonitor.DUMMY); + + List> paths = accumulator.asList(); + assertEquals(3, paths.size()); + assertPathExists(paths, v1, v2, v10); + assertPathExists(paths, v1, v3, v5, v10); + assertPathExists(paths, v1, v3, v6, v7, v9, v10); + + accumulator = new ListAccumulator<>(); + GraphAlgorithms.findPaths(g, v1, v10, accumulator, TaskMonitor.DUMMY); + + } + + @Test + public void testFindPathsNew_MultiPaths_BackFlows() throws CancelledException { + + /* + --> v1 + | / \ + -v2 v3 + / | \ + v4 v5 v6 <-- + * | | | + | v7 | + | | \ | + | v8 v9 - + | * + | + v10 + + + Paths: v1, v3, v5, v10 + */ + + TestV v1 = vertex(1); + TestV v2 = vertex(2); + TestV v3 = vertex(3); + TestV v4 = vertex(4); + TestV v5 = vertex(5); + TestV v6 = vertex(6); + TestV v7 = vertex(7); + TestV v8 = vertex(8); + TestV v9 = vertex(9); + TestV v10 = vertex(10); + + edge(v1, v2); + edge(v1, v3); + + edge(v2, v1); // back edge + + edge(v3, v4); + edge(v3, v5); + edge(v3, v6); + + edge(v5, v10); + + edge(v6, v7); + + edge(v7, v8); + edge(v7, v9); + + edge(v9, v6); // back edge + + ListAccumulator> accumulator = new ListAccumulator<>(); + + GraphAlgorithms.findPaths(g, v1, v10, accumulator, TaskMonitor.DUMMY); + + List> paths = accumulator.asList(); + assertEquals(1, paths.size()); + assertPathExists(paths, v1, v3, v5, v10); + } + + @Test + public void testFindPathsNew_MultiPaths_LongDeadEnd() throws CancelledException { + + /* + v1 + / \ + v2 v3 + | / | \ + | v4 v5 v6 + | | | | + | v11 | v7 + | | | | \ + | v12 | v8 v9 + | * | * | + \ | / + \ | / + \ |/ + v10 + + + Paths: + v1, v2, v10 + v1, v3, v5, v10 + v1, v3, v6, v7, v9, v10 + */ + + TestV v1 = vertex(1); + TestV v2 = vertex(2); + TestV v3 = vertex(3); + TestV v4 = vertex(4); + TestV v5 = vertex(5); + TestV v6 = vertex(6); + TestV v7 = vertex(7); + TestV v8 = vertex(8); + TestV v9 = vertex(9); + TestV v10 = vertex(10); + TestV v11 = vertex(11); + TestV v12 = vertex(12); + + edge(v1, v2); + edge(v1, v3); + + edge(v2, v10); + + edge(v3, v4); + edge(v3, v5); + edge(v3, v6); + + edge(v4, v11); + + edge(v11, v12); + + edge(v5, v10); + + edge(v6, v7); + edge(v7, v8); + edge(v7, v9); + + edge(v9, v10); + + ListAccumulator> accumulator = new ListAccumulator<>(); + + GraphAlgorithms.findPaths(g, v1, v10, accumulator, TaskMonitor.DUMMY); + + List> paths = accumulator.asList(); + assertEquals(3, paths.size()); + assertPathExists(paths, v1, v2, v10); + assertPathExists(paths, v1, v3, v5, v10); + assertPathExists(paths, v1, v3, v6, v7, v9, v10); + } + + @Test + public void testFindPathsNew_MultiPaths() throws CancelledException { + + /* + v1 + / \ + v2 v3 + | / | \ + | v4 v5 v6 + | * | | + | | v7 + | | | \ + | | v8 v9 + | | * | + \ | / + \ | / + \|/ + v10 + + + Paths: + v1, v2, v10 + v1, v3, v5, v10 + v1, v3, v6, v7, v9, v10 + */ + + TestV v1 = vertex(1); + TestV v2 = vertex(2); + TestV v3 = vertex(3); + TestV v4 = vertex(4); + TestV v5 = vertex(5); + TestV v6 = vertex(6); + TestV v7 = vertex(7); + TestV v8 = vertex(8); + TestV v9 = vertex(9); + TestV v10 = vertex(10); + + edge(v1, v2); + edge(v1, v3); + + edge(v2, v10); + + edge(v3, v4); + edge(v3, v5); + edge(v3, v6); + + edge(v5, v10); + + edge(v6, v7); + edge(v7, v8); + edge(v7, v9); + + edge(v9, v10); + + ListAccumulator> accumulator = new ListAccumulator<>(); + + GraphAlgorithms.findPaths(g, v1, v10, accumulator, TaskMonitor.DUMMY); + + List> paths = accumulator.asList(); + assertEquals(3, paths.size()); + assertPathExists(paths, v1, v2, v10); + assertPathExists(paths, v1, v3, v5, v10); + assertPathExists(paths, v1, v3, v6, v7, v9, v10); + + accumulator = new ListAccumulator<>(); + GraphAlgorithms.findPaths(g, v4, v10, accumulator, TaskMonitor.DUMMY); + paths = accumulator.asList(); + assertTrue(paths.isEmpty()); } @Test @@ -1383,7 +1702,7 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { startMemoryMonitorThread(false); g = GraphFactory.createDirectedGraph(); - TestV[] vertices = generateCompletelyConnectedGraph(30); // this takes a while + TestV[] vertices = generateCompletelyConnectedGraph(15); int timeout = 250; TimeoutTaskMonitor monitor = TimeoutTaskMonitor.timeoutIn(timeout, TimeUnit.MILLISECONDS); @@ -1391,7 +1710,10 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { TestV start = vertices[0]; TestV end = vertices[vertices.length - 1]; try { - GraphAlgorithms.findPaths(g, start, end, new ListAccumulator<>(), monitor); + ListAccumulator> accumulator = new ListAccumulator<>(); + GraphAlgorithms.findPaths(g, start, end, accumulator, monitor); + + Msg.debug(this, "Found paths " + accumulator.size()); fail("Did not timeout in " + timeout + " ms"); } catch (TimeoutException e) { diff --git a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/GraphAlgorithmsVisualDebugger.java b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/GraphAlgorithmsVisualDebugger.java new file mode 100644 index 0000000000..ba9796834f --- /dev/null +++ b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/GraphAlgorithmsVisualDebugger.java @@ -0,0 +1,151 @@ +/* ### + * 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.graph; + +import static org.junit.Assert.assertEquals; + +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.util.List; + +import javax.swing.JFrame; + +import org.junit.Test; + +import ghidra.graph.algo.*; +import ghidra.graph.algo.viewer.*; +import ghidra.util.SystemUtilities; +import ghidra.util.datastruct.ListAccumulator; +import ghidra.util.exception.CancelledException; + +/** + * A tool, written as a junit, that allows the user to run a test and then use the UI to step + * through the given graph algorithm. + */ +public class GraphAlgorithmsVisualDebugger extends AbstractGraphAlgorithmsTest { + + @Override + protected GDirectedGraph createGraph() { + return GraphFactory.createDirectedGraph(); + } + + @Test + public void testFindPathsNew_MultiPaths_BackFlows_WithUI_IterativeFindPathsAlgorithm() + throws CancelledException { + + FindPathsAlgorithm algo = new IterativeFindPathsAlgorithm<>(); + doTestFindPathsNew_MultiPaths_BackFlows_WithUI(algo); + } + + @Test + public void testFindPathsNew_MultiPaths_BackFlows_WithUI_RecursiveFindPathsAlgorithm() + throws CancelledException { + + FindPathsAlgorithm algo = new RecursiveFindPathsAlgorithm<>(); + doTestFindPathsNew_MultiPaths_BackFlows_WithUI(algo); + } + + private void doTestFindPathsNew_MultiPaths_BackFlows_WithUI( + FindPathsAlgorithm algo) throws CancelledException { + + /* + --> v1 + | / \ + -v2 v3 + / | \ + v4 v5 v6 <-- + * | | | + | v7 | + | | \ | + | v8 v9 - + | * + | + v10 + + + Paths: v1, v3, v5, v10 + */ + + TestV v1 = vertex(1); + TestV v2 = vertex(2); + TestV v3 = vertex(3); + TestV v4 = vertex(4); + TestV v5 = vertex(5); + TestV v6 = vertex(6); + TestV v7 = vertex(7); + TestV v8 = vertex(8); + TestV v9 = vertex(9); + TestV v10 = vertex(10); + + edge(v1, v2); + edge(v1, v3); + + edge(v2, v1); // back edge + + edge(v3, v4); + edge(v3, v5); + edge(v3, v6); + + edge(v5, v10); + + edge(v6, v7); + + edge(v7, v8); + edge(v7, v9); + + edge(v9, v6); // back edge + + AlgorithmSteppingTaskMonitor steppingMonitor = new AlgorithmSteppingTaskMonitor(); + steppingMonitor = new AlgorithmSelfSteppingTaskMonitor(500); + TestGraphAlgorithmSteppingViewerPanel gp = showViewer(steppingMonitor); + + algo.setStatusListener(gp.getStatusListener()); + ListAccumulator> accumulator = new ListAccumulator<>(); + algo.findPaths(g, v1, v10, accumulator, steppingMonitor); + + //Msg.debug(this, "Total status updates: " + gp.getStatusListener().getTotalStatusChanges()); + + steppingMonitor.pause(); // pause this thread to view the final output + + List> paths = accumulator.asList(); + assertEquals(1, paths.size()); + assertPathExists(paths, v1, v3, v5, v10); + } + + private TestGraphAlgorithmSteppingViewerPanel showViewer( + AlgorithmSteppingTaskMonitor steppingMonitor) { + + String isHeadless = Boolean.toString(false); + System.setProperty(SystemUtilities.HEADLESS_PROPERTY, isHeadless); + System.setProperty("java.awt.headless", isHeadless); + + JFrame frame = new JFrame("Graph"); + TestGraphAlgorithmSteppingViewerPanel gp = + new TestGraphAlgorithmSteppingViewerPanel<>(g, steppingMonitor); + frame.getContentPane().add(gp); + frame.setSize(800, 800); + frame.setVisible(true); + + frame.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + steppingMonitor.cancel(); + } + }); + + return gp; + } +} diff --git a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/algo/viewer/AlgorithmSelfSteppingTaskMonitor.java b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/algo/viewer/AlgorithmSelfSteppingTaskMonitor.java new file mode 100644 index 0000000000..ad4fcbd256 --- /dev/null +++ b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/algo/viewer/AlgorithmSelfSteppingTaskMonitor.java @@ -0,0 +1,51 @@ +/* ### + * 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.graph.algo.viewer; + +import ghidra.util.Msg; + +/** + * Stepping task monitor that will proceed to the next step after the specified delay + */ +public class AlgorithmSelfSteppingTaskMonitor extends AlgorithmSteppingTaskMonitor { + + private int stepTime; + + public AlgorithmSelfSteppingTaskMonitor(int stepTime) { + this.stepTime = stepTime; + } + + @Override + public void pause() { + + if (isCancelled()) { + return; // no pausing after cancelled + } + + notifyStepReady(); + + synchronized (this) { + + try { + wait(stepTime); + } + catch (InterruptedException e) { + Msg.debug(this, "Interrupted waiting for next step", e); + } + + } + } +} diff --git a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/algo/viewer/AlgorithmSteppingTaskMonitor.java b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/algo/viewer/AlgorithmSteppingTaskMonitor.java new file mode 100644 index 0000000000..e660c44c1c --- /dev/null +++ b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/algo/viewer/AlgorithmSteppingTaskMonitor.java @@ -0,0 +1,89 @@ +/* ### + * 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.graph.algo.viewer; + +import java.util.HashSet; +import java.util.Set; + +import ghidra.generic.function.Callback; +import ghidra.util.Msg; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitorAdapter; + +/** + * Task monitor that will trigger a {@link #wait()} when {@link #checkCanceled()} is called. This + * allows clients to watch algorithms as they proceed. + */ +public class AlgorithmSteppingTaskMonitor extends TaskMonitorAdapter { + + private Set stepLisetners = new HashSet<>(); + + public AlgorithmSteppingTaskMonitor() { + setCancelEnabled(true); + } + + public void addStepListener(Callback c) { + stepLisetners.add(c); + } + + @Override + public void cancel() { + super.cancel(); + step(); // wake-up any waiting threads + } + + @Override + public void checkCanceled() throws CancelledException { + + super.checkCanceled(); + + pause(); + } + + /** + * Causes this monitor to perform at {@link #wait()}. Call {@link #step()} to allow the + * client to continue. + */ + public void pause() { + + if (isCancelled()) { + return; // no pausing after cancelled + } + + notifyStepReady(); + + synchronized (this) { + + try { + wait(); + } + catch (InterruptedException e) { + Msg.debug(this, "Interrupted waiting for next step", e); + } + + } + } + + public void step() { + synchronized (this) { + notify(); + } + } + + protected void notifyStepReady() { + stepLisetners.forEach(l -> l.call()); + } +} diff --git a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/algo/viewer/AlgorithmTestSteppingEdge.java b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/algo/viewer/AlgorithmTestSteppingEdge.java new file mode 100644 index 0000000000..ed080faa0b --- /dev/null +++ b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/algo/viewer/AlgorithmTestSteppingEdge.java @@ -0,0 +1,35 @@ +/* ### + * 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.graph.algo.viewer; + +import ghidra.graph.viewer.edge.AbstractVisualEdge; + +public class AlgorithmTestSteppingEdge + extends AbstractVisualEdge> { + + AlgorithmTestSteppingEdge(AlgorithmTestSteppingVertex start, + AlgorithmTestSteppingVertex end) { + super(start, end); + } + + // sigh. I could not get this to compile with 'V' type specified + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public AlgorithmTestSteppingEdge cloneEdge(AlgorithmTestSteppingVertex start, + AlgorithmTestSteppingVertex end) { + return new AlgorithmTestSteppingEdge<>(start, end); + } +} diff --git a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/algo/viewer/AlgorithmTestSteppingVertex.java b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/algo/viewer/AlgorithmTestSteppingVertex.java new file mode 100644 index 0000000000..6a5f9e3447 --- /dev/null +++ b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/algo/viewer/AlgorithmTestSteppingVertex.java @@ -0,0 +1,183 @@ +/* ### + * 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.graph.algo.viewer; + +import java.awt.*; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Ellipse2D.Double; +import java.awt.image.BufferedImage; + +import javax.swing.*; + +import ghidra.graph.algo.GraphAlgorithmStatusListener.STATUS; +import ghidra.graph.graphs.AbstractTestVertex; +import ghidra.graph.viewer.vertex.VertexShapeProvider; + +public class AlgorithmTestSteppingVertex extends AbstractTestVertex + implements VertexShapeProvider { + + private ShapeImage defaultShape; + private ShapeImage defaultWithPathShape; + private ShapeImage scheduledShape; + private ShapeImage exploringShape; + private ShapeImage blockedShape; + private ShapeImage currentShape; + + private JLabel tempLabel = new JLabel(); + private V v; + private STATUS status = STATUS.WAITING; + + private boolean wasEverInPath; + + protected AlgorithmTestSteppingVertex(V v) { + super(v.toString()); + this.v = v; + + buildShapes(); + + tempLabel.setText(v.toString()); + } + + public void setStatus(STATUS status) { + this.status = status; + + ShapeImage si; + switch (status) { + case BLOCKED: + si = blockedShape; + if (wasEverInPath) { + si = defaultWithPathShape; + } + break; + case EXPLORING: + si = exploringShape; + break; + case SCHEDULED: + si = scheduledShape; + break; + case IN_PATH: + si = exploringShape; + wasEverInPath = true; + break; + case WAITING: + default: + si = defaultShape; + if (wasEverInPath) { + si = defaultWithPathShape; + } + break; + } + + currentShape = si; + } + + private void buildShapes() { + + defaultShape = buildCircleShape(Color.LIGHT_GRAY, "default"); + defaultWithPathShape = buildCircleShape(new Color(192, 216, 65), "default; was in path"); + scheduledShape = buildCircleShape(new Color(255, 248, 169), "scheduled"); + exploringShape = buildCircleShape(new Color(0, 147, 0), "exploring"); + blockedShape = buildCircleShape(new Color(249, 190, 190), "blocked"); + + currentShape = defaultShape; + } + + private ShapeImage buildCircleShape(Color color, String name) { + int w = 50; + int h = 50; + + Double circle = new Ellipse2D.Double(0, 0, w, h); + + BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2 = (Graphics2D) image.getGraphics(); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + g2.setColor(color); + g2.fill(circle); + + g2.dispose(); + + Dimension shapeSize = circle.getBounds().getSize(); + int x = 50; + int y = 0; + circle.setFrame(x, y, shapeSize.width, shapeSize.height); + + return new ShapeImage(image, circle, name); + } + + V getTestVertex() { + return v; + } + + @Override + public JComponent getComponent() { + ShapeImage si = getShapeImage(); + ImageIcon icon = new ImageIcon(si.getImage()); + tempLabel.setIcon(icon); + return tempLabel; + } + + private ShapeImage getShapeImage() { + return currentShape; + } + + @Override + public Shape getCompactShape() { + return getShapeImage().getShape(); + } + + @Override + public String toString() { + String statusString = status.toString(); + if (wasEverInPath) { + statusString = ""; + } + else if (status == STATUS.BLOCKED) { + statusString = ""; + } + + return v.toString() + " " + statusString; + } + + private class ShapeImage { + private Image image; + private Shape shape; + private String shapeName; + + ShapeImage(Image image, Shape shape, String name) { + this.image = image; + this.shape = shape; + this.shapeName = name; + } + + Shape getShape() { + return shape; + } + + Image getImage() { + return image; + } + + String getName() { + return shapeName; + } + + @Override + public String toString() { + return getName(); + } + } +} diff --git a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/algo/viewer/TestGraphAlgorithmSteppingViewerPanel.java b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/algo/viewer/TestGraphAlgorithmSteppingViewerPanel.java new file mode 100644 index 0000000000..b012c0c880 --- /dev/null +++ b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/algo/viewer/TestGraphAlgorithmSteppingViewerPanel.java @@ -0,0 +1,347 @@ +/* ### + * 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.graph.algo.viewer; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.util.*; +import java.util.List; + +import javax.swing.*; + +import org.apache.commons.collections4.BidiMap; +import org.apache.commons.collections4.bidimap.DualHashBidiMap; + +import edu.uci.ics.jung.visualization.decorators.EdgeShape; +import edu.uci.ics.jung.visualization.renderers.Renderer; +import ghidra.graph.*; +import ghidra.graph.algo.GraphAlgorithmStatusListener; +import ghidra.graph.graphs.DefaultVisualGraph; +import ghidra.graph.viewer.GraphViewer; +import ghidra.graph.viewer.GraphViewerUtils; +import ghidra.graph.viewer.layout.AbstractVisualGraphLayout; +import ghidra.graph.viewer.layout.GridLocationMap; +import ghidra.graph.viewer.options.VisualGraphOptions; +import ghidra.graph.viewer.vertex.VisualGraphVertexShapeTransformer; +import ghidra.graph.viewer.vertex.VisualVertexRenderer; +import ghidra.util.Msg; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.SwingUpdateManager; +import resources.ResourceManager; + +public class TestGraphAlgorithmSteppingViewerPanel> extends JPanel { + + private GraphViewer, AlgorithmTestSteppingEdge> viewer; + private GDirectedGraph graph; + private BidiMap> vertexLookupMap = new DualHashBidiMap<>(); + + private List images = new ArrayList<>(); + private JPanel phasesPanel; + private float zoom = .5f; + private SwingUpdateManager zoomRebuilder = new SwingUpdateManager(() -> rebuildImages()); + + private JPanel buttonPanel; + private AlgorithmSteppingTaskMonitor steppingMonitor; + private AbstractButton nextButton; + private GraphAlgorithmStatusListener algorithmStatusListener = + new GraphAlgorithmStatusListener<>() { + + public void finished() { + nextButton.setEnabled(true); + nextButton.setText("Done"); + } + + public void statusChanged(V v, STATUS s) { + + totalStatusChanges++; + + AlgorithmTestSteppingVertex vv = vertexLookupMap.get(v); + vv.setStatus(s); + repaint(); + + addCurrentGraphToPhases(); + } + + private void addCurrentGraphToPhases() { + + Rectangle layoutShape = GraphViewerUtils.getTotalGraphSizeInLayoutSpace(viewer); + Rectangle viewShape = GraphViewerUtils.translateRectangleFromLayoutSpaceToViewSpace( + viewer, layoutShape); + + int w = viewShape.x + viewShape.width; + int h = viewShape.y + viewShape.height; + + BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = (Graphics2D) image.getGraphics(); + g.setColor(Color.WHITE); + g.fillRect(0, 0, w, h); + + try { + SwingUtilities.invokeAndWait(() -> { + viewer.paint(g); + }); + } + catch (Exception e) { + Msg.debug(this, "Unexpected exception", e); + } + + images.add(image); + zoomRebuilder.updateLater(); + } + }; + + public TestGraphAlgorithmSteppingViewerPanel(GDirectedGraph graph, + AlgorithmSteppingTaskMonitor steppingMonitor) { + this.graph = graph; + this.steppingMonitor = steppingMonitor; + + buildGraphViewer(); + buildPhasesViewer(); + buildButtons(); + + setLayout(new BorderLayout()); + add(viewer, BorderLayout.CENTER); + add(buttonPanel, BorderLayout.SOUTH); + + steppingMonitor.addStepListener(() -> { + repaint(); + nextButton.setEnabled(true); + }); + } + + public GraphAlgorithmStatusListener getStatusListener() { + return algorithmStatusListener; + } + + private void buildGraphViewer() { + TestGraph tvg = new TestGraph(); + + Collection vertices = graph.getVertices(); + for (V v : vertices) { + AlgorithmTestSteppingVertex newV = new AlgorithmTestSteppingVertex<>(v); + tvg.addVertex(newV); + vertexLookupMap.put(v, newV); + } + + Collection edges = graph.getEdges(); + for (E e : edges) { + V start = e.getStart(); + V end = e.getEnd(); + AlgorithmTestSteppingVertex newStart = vertexLookupMap.get(start); + AlgorithmTestSteppingVertex newEnd = vertexLookupMap.get(end); + AlgorithmTestSteppingEdge newEdge = + new AlgorithmTestSteppingEdge<>(newStart, newEnd); + tvg.addEdge(newEdge); + } + + TestGraphLayout layout = new TestGraphLayout(tvg); + + tvg.setLayout(layout); + viewer = new GraphViewer<>(layout, new Dimension(400, 400)); + viewer.setBackground(Color.WHITE); + viewer.setGraphOptions(new VisualGraphOptions()); + + Renderer, AlgorithmTestSteppingEdge> renderer = + viewer.getRenderer(); + + // TODO set renderer directly + renderer.setVertexRenderer(new VisualVertexRenderer<>()); + + // TODO note: this is needed to 1) use shapes and 2) center the vertices + VisualGraphVertexShapeTransformer> shaper = + new VisualGraphVertexShapeTransformer<>(); + viewer.getRenderContext().setVertexShapeTransformer(shaper); + + viewer.getRenderContext().setEdgeShapeTransformer(EdgeShape.line(tvg)); + + viewer.getRenderContext().setVertexLabelTransformer(v -> v.toString()); + } + + private void buildPhasesViewer() { + JFrame f = new JFrame("Graph Phases"); + JPanel parentPanel = new JPanel(new BorderLayout()); + phasesPanel = new JPanel(); + + JPanel zoomPanel = new JPanel(); + JButton inButton = new JButton("+"); + inButton.addActionListener(e -> { + float newZoom = zoom + .1f; + zoom = Math.min(1f, newZoom); + zoomRebuilder.update(); + }); + JButton outButton = new JButton("-"); + outButton.addActionListener(e -> { + float newZoom = zoom - .1f; + zoom = Math.max(0.1f, newZoom); + zoomRebuilder.update(); + }); + zoomPanel.add(inButton); + zoomPanel.add(outButton); + + parentPanel.add(phasesPanel, BorderLayout.CENTER); + parentPanel.add(zoomPanel, BorderLayout.SOUTH); + + f.getContentPane().add(parentPanel); + + f.setSize(400, 400); + f.setVisible(true); + } + + private void rebuildImages() { + + phasesPanel.removeAll(); + + double scale = zoom; + + images.forEach(image -> { + + int w = image.getWidth(); + int h = image.getHeight(); + double sw = w * scale; + double sh = h * scale; + Image scaledImage = ResourceManager.createScaledImage(image, (int) sw, (int) sh, + Image.SCALE_AREA_AVERAGING); + JLabel label = new JLabel(new ImageIcon(scaledImage)); + phasesPanel.add(label); + }); + + phasesPanel.invalidate(); + phasesPanel.getParent().revalidate(); + phasesPanel.repaint(); + } + + private void buildButtons() { + buttonPanel = new JPanel(); + + nextButton = new JButton("Next >>"); + nextButton.addActionListener(e -> { + nextButton.setEnabled(false); + steppingMonitor.step(); + }); + nextButton.setEnabled(false); + + buttonPanel.add(nextButton); + } + + private class TestGraph extends + DefaultVisualGraph, AlgorithmTestSteppingEdge> { + + private TestGraphLayout layout; + + @Override + public TestGraphLayout getLayout() { + return layout; + } + + public void setLayout(TestGraphLayout layout) { + this.layout = layout; + } + + @Override + public TestGraph copy() { + + TestGraph newGraph = new TestGraph(); + + Collection> myVertices = getVertices(); + for (AlgorithmTestSteppingVertex v : myVertices) { + newGraph.addVertex(v); + } + + Collection> myEdges = getEdges(); + for (AlgorithmTestSteppingEdge e : myEdges) { + newGraph.addEdge(e); + } + + return newGraph; + } + } + + private class TestGraphLayout extends + AbstractVisualGraphLayout, AlgorithmTestSteppingEdge> { + + protected TestGraphLayout(TestGraph graph) { + super(graph); + } + + @SuppressWarnings("unchecked") + @Override + public VisualGraph, AlgorithmTestSteppingEdge> getVisualGraph() { + return (VisualGraph, AlgorithmTestSteppingEdge>) getGraph(); + } + + @Override + protected boolean isCondensedLayout() { + return false; + } + + @Override + protected GridLocationMap, AlgorithmTestSteppingEdge> performInitialGridLayout( + VisualGraph, AlgorithmTestSteppingEdge> g) + throws CancelledException { + + GridLocationMap, AlgorithmTestSteppingEdge> grid = + new GridLocationMap<>(); + + // sort by name; assume name is just a number + List> sorted = new ArrayList<>(g.getVertices()); + Collections.sort(sorted, (v1, v2) -> { + Integer i1 = Integer.parseInt(v1.getName()); + Integer i2 = Integer.parseInt(v2.getName()); + return i1.compareTo(i2); + }); + + AlgorithmTestSteppingVertex first = sorted.get(0); + assignRows(first, g, grid, 1, 1); + + return grid; + } + + private void assignRows(AlgorithmTestSteppingVertex v, + VisualGraph, AlgorithmTestSteppingEdge> g, + GridLocationMap, AlgorithmTestSteppingEdge> grid, + int row, int col) { + + int existing = grid.row(v); + if (existing > 0) { + return; // already processed + } + + grid.row(v, row); + grid.col(v, col); + int nextRow = row++; + + Collection> children = g.getOutEdges(v); + int n = children.size(); + int middle = n / 2; + int start = col - middle; + int childCol = start; + + for (AlgorithmTestSteppingEdge edge : children) { + AlgorithmTestSteppingVertex child = edge.getEnd(); + assignRows(child, g, grid, nextRow + 1, childCol++); + } + } + + @Override + public AbstractVisualGraphLayout, AlgorithmTestSteppingEdge> createClonedLayout( + VisualGraph, AlgorithmTestSteppingEdge> newGraph) { + + TestGraphLayout newLayout = new TestGraphLayout((TestGraph) newGraph); + return newLayout; + } + + } +} diff --git a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/support/TestVisualGraph.java b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/support/TestVisualGraph.java index 3f96f9a57d..a64b197566 100644 --- a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/support/TestVisualGraph.java +++ b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/support/TestVisualGraph.java @@ -50,6 +50,8 @@ public class TestVisualGraph extends DefaultVisualGraph