From 410af5a2720c1377586df7e1eff758db818d5030 Mon Sep 17 00:00:00 2001 From: ghidravore Date: Fri, 6 Dec 2019 11:32:03 -0500 Subject: [PATCH] GT-3317 moved generic graph interfaces to features graph module created graph service broker first commit of program graph module adapted to new graph api GT-3317 connected listeners, documented and prettied up code changed GhidraGraph to preserve order of created graph. Removed edge filtering from initial program graph display GT-3317 added exporters for supported formats GT-3317 fixed GhidraGraph bug where it lost edges updates changed to new action builder removed icons, improved AttributeFilters removed DialogComponentProviderBuilder fixed generic alphabet soup added vertex name updating. GT-3317 added threading to sugiyama adapted to take advantage of multi-threaded edge crossing reduction in circle layout eliminated parallel edges, improved sizing, updated jungrapht version GT-3317 fixing AST graph and moving modules and packages started help GT-3317 updated min-cross and color selections uses min-cross that optimizes for graph size GT-3317 help, javadocs changes from review comments and cleaning up warnings and simplifying exporter code fixing warnings, simplifying unnecessarily complicated code more changes from review more changes from review, simplifications. removed unnecessary threading, renamed vertex, edge, etc GT-3317 squashed many commits to make rebase easier. Mostly changes from first code review. --- .../Base/data/ExtensionPoint.manifest | 1 + .../GenerateBrandesKopfGraphScript.java | 131 +++ .../GenerateTestGraphScript.java | 60 ++ .../ExampleGraphServiceScript.java | 59 ++ .../AddressBasedGraphDisplayListener.java | 190 +++++ .../graph/GraphDisplayBrokerListener.java | 22 + .../core/graph/GraphDisplayBrokerPlugin.java | 209 +++++ .../app/services/GraphDisplayBroker.java | 77 ++ .../ghidra/app/services/GraphService.java | 82 -- .../viewer/field/XRefHeaderFieldFactory.java | 2 +- Ghidra/Features/Decompiler/build.gradle | 1 - .../Decompiler/ghidra_scripts/GraphAST.java | 456 +++++------ .../ghidra_scripts/GraphASTAndFlow.java | 34 +- .../core/decompile/DecompilerProvider.java | 12 +- .../actions/ASTGraphDisplayListener.java | 96 +++ .../actions/ASTGraphSelectionHandler.java | 132 --- .../core/decompile/actions/ASTGraphTask.java | 165 ++-- .../actions/GraphASTControlFlowAction.java | 15 +- ...b5ActiveDirectoryAuthenticationModule.java | 8 +- Ghidra/Features/GraphServices/Module.manifest | 7 + Ghidra/Features/GraphServices/build.gradle | 25 + .../GraphServices/certification.manifest | 17 + .../src/main/help/help/TOC_Source.xml | 60 ++ .../src/main/help/help/shared/Frontpage.css | 58 ++ .../src/main/help/help/shared/arrow.gif | Bin 0 -> 69 bytes .../src/main/help/help/shared/note.png | Bin 0 -> 4193 bytes .../src/main/help/help/shared/tip.png | Bin 0 -> 1641 bytes .../topics/GraphServices/GraphDisplay.htm | 67 ++ .../help/topics/GraphServices/GraphExport.htm | 53 ++ .../images/DefaultGraphDisplay.png | Bin 0 -> 10078 bytes .../GraphServices/images/ExportDialog.png | Bin 0 -> 3221 bytes .../java/ghidra/graph/AttributeFilters.java | 242 ++++++ .../export/AbstractGraphExporterFactory.java | 152 ++++ .../AttributedGraphExporterFactory.java | 60 ++ .../export/ExportAttributedGraphDisplay.java | 118 +++ .../ExportAttributedGraphDisplayProvider.java | 79 ++ .../graph/export/GraphExportFormat.java | 38 + .../graph/export/GraphExporterDialog.java | 333 ++++++++ .../graph/visualization/CenterAnimation.java | 83 ++ .../ghidra/graph/visualization/Colors.java | 283 +++++++ .../DefaultDisplayGraphIcons.java | 35 + .../visualization/DefaultGraphDisplay.java | 750 ++++++++++++++++++ .../DefaultGraphDisplayComponentProvider.java | 51 ++ .../DefaultGraphDisplayProvider.java | 96 +++ .../graph/visualization/EdgeComparator.java | 47 ++ .../graph/visualization/FilterDialog.java | 96 +++ .../graph/visualization/GhidraIconCache.java | 173 ++++ .../ghidra/graph/visualization/IconShape.java | 104 +++ .../graph/visualization/LayoutFunction.java | 77 ++ .../LayoutTransitionManager.java | 210 +++++ .../visualization/ProgramGraphFunctions.java | 137 ++++ .../src/main/resources/images/magnifier.png | Bin 0 -> 615 bytes .../main/resources/images/redspheregraph.png | Bin 0 -> 1733 bytes .../src/main/resources/images/sat2.png | Bin 0 -> 3884 bytes .../src/main/resources/images/tree.png | Bin 0 -> 943 bytes .../src/main/resources/jungrapht.properties | 53 ++ .../ghidra/graph/export/GraphExportTest.java | 360 +++++++++ .../graph/visualization/ColorsTest.java | 75 ++ .../graph/visualization/IconShapeTest.java | 70 ++ Ghidra/Features/ProgramGraph/Module.manifest | 2 + Ghidra/Features/ProgramGraph/build.gradle | 18 + .../ProgramGraph/certification.manifest | 13 + .../src/main/help/help/TOC_Source.xml | 57 ++ .../src/main/help/help/shared/arrow.gif | Bin 0 -> 69 bytes .../src/main/help/help/shared/note.png | Bin 0 -> 4193 bytes .../src/main/help/help/shared/tip.png | Bin 0 -> 1641 bytes .../ProgramGraphPlugin/ProgramGraph.htm | 397 +++++++++ .../images/BasicBlockExampleCode.png | Bin 0 -> 48514 bytes .../images/BasicBlockGraph.png | Bin 0 -> 20519 bytes .../images/CodeBlockGraph.png | Bin 0 -> 17920 bytes .../images/FocusGraphNode.png | Bin 0 -> 13711 bytes .../images/SelectGraphNode.png | Bin 0 -> 13288 bytes .../ghidra/graph/program/BlockGraphTask.java | 522 ++++++++++++ .../BlockModelGraphDisplayListener.java | 140 ++++ .../graph/program/ProgramGraphPlugin.java | 334 ++++++++ .../graph/program/AbstractBlockGraphTest.java | 131 +++ .../graph/program/BlockGraphEventTest.java | 118 +++ .../graph/program/BlockGraphTaskTest.java | 234 ++++++ .../graph/program/TestGraphDisplay.java | 112 +++ .../graph/program/TestGraphService.java | 62 ++ .../main/java/docking/ComponentProvider.java | 1 + .../java/docking/action/DockingAction.java | 1 - .../action/builder/AbstractActionBuilder.java | 1 - .../action/builder/MultiActionBuilder.java | 20 +- .../builder/MultiStateActionBuilder.java | 50 +- .../action/builder/ToggleActionBuilder.java | 2 +- .../main/java/docking/help/HelpManager.java | 2 +- Ghidra/Framework/Generic/build.gradle | 2 + .../ghidra/util/exception/GraphException.java | 29 +- .../src/main/java/resources/Icons.java | 111 +-- Ghidra/Framework/Graph/build.gradle | 2 + Ghidra/Framework/Graph/certification.manifest | 1 + .../Graph/data/ExtensionPoint.manifest | 2 + .../java/ghidra/service/graph/Attributed.java | 138 ++++ .../ghidra/service/graph/AttributedEdge.java | 88 ++ .../ghidra/service/graph/AttributedGraph.java | 241 ++++++ .../service/graph/AttributedVertex.java | 115 +++ .../service/graph/AttributedGraphTest.java | 218 +++++ .../framework/plugintool/PluginTool.java | 13 +- .../ghidra/program/model/graph/GraphData.java | 81 -- .../program/model/graph/GraphDisplay.java | 111 --- .../ghidra/program/model/graph/GraphEdge.java | 46 -- .../model/graph/GraphSelectionHandler.java | 97 --- .../program/model/graph/GraphVertex.java | 59 -- .../ghidra/program/model/graph/package.html | 8 - .../graph/DummyGraphDisplayListener.java | 37 + .../ghidra/service/graph/GraphDisplay.java | 116 +++ .../service/graph/GraphDisplayListener.java | 41 + .../service/graph/GraphDisplayProvider.java | 72 ++ .../screenshot/GraphServicesScreenShots.java | 101 +++ .../ProgramGraphPluginScreenShots.java | 118 +++ build.gradle | 5 + 112 files changed, 8736 insertions(+), 1094 deletions(-) create mode 100644 Ghidra/Features/Base/developer_scripts/GenerateBrandesKopfGraphScript.java create mode 100644 Ghidra/Features/Base/developer_scripts/GenerateTestGraphScript.java create mode 100644 Ghidra/Features/Base/ghidra_scripts/ExampleGraphServiceScript.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/graph/AddressBasedGraphDisplayListener.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/graph/GraphDisplayBrokerListener.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/graph/GraphDisplayBrokerPlugin.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/services/GraphDisplayBroker.java delete mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/services/GraphService.java create mode 100644 Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/ASTGraphDisplayListener.java delete mode 100644 Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/ASTGraphSelectionHandler.java create mode 100644 Ghidra/Features/GraphServices/Module.manifest create mode 100644 Ghidra/Features/GraphServices/build.gradle create mode 100644 Ghidra/Features/GraphServices/certification.manifest create mode 100644 Ghidra/Features/GraphServices/src/main/help/help/TOC_Source.xml create mode 100644 Ghidra/Features/GraphServices/src/main/help/help/shared/Frontpage.css create mode 100644 Ghidra/Features/GraphServices/src/main/help/help/shared/arrow.gif create mode 100644 Ghidra/Features/GraphServices/src/main/help/help/shared/note.png create mode 100644 Ghidra/Features/GraphServices/src/main/help/help/shared/tip.png create mode 100644 Ghidra/Features/GraphServices/src/main/help/help/topics/GraphServices/GraphDisplay.htm create mode 100644 Ghidra/Features/GraphServices/src/main/help/help/topics/GraphServices/GraphExport.htm create mode 100644 Ghidra/Features/GraphServices/src/main/help/help/topics/GraphServices/images/DefaultGraphDisplay.png create mode 100644 Ghidra/Features/GraphServices/src/main/help/help/topics/GraphServices/images/ExportDialog.png create mode 100644 Ghidra/Features/GraphServices/src/main/java/ghidra/graph/AttributeFilters.java create mode 100644 Ghidra/Features/GraphServices/src/main/java/ghidra/graph/export/AbstractGraphExporterFactory.java create mode 100644 Ghidra/Features/GraphServices/src/main/java/ghidra/graph/export/AttributedGraphExporterFactory.java create mode 100644 Ghidra/Features/GraphServices/src/main/java/ghidra/graph/export/ExportAttributedGraphDisplay.java create mode 100644 Ghidra/Features/GraphServices/src/main/java/ghidra/graph/export/ExportAttributedGraphDisplayProvider.java create mode 100644 Ghidra/Features/GraphServices/src/main/java/ghidra/graph/export/GraphExportFormat.java create mode 100644 Ghidra/Features/GraphServices/src/main/java/ghidra/graph/export/GraphExporterDialog.java create mode 100644 Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/CenterAnimation.java create mode 100644 Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/Colors.java create mode 100644 Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/DefaultDisplayGraphIcons.java create mode 100644 Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/DefaultGraphDisplay.java create mode 100644 Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/DefaultGraphDisplayComponentProvider.java create mode 100644 Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/DefaultGraphDisplayProvider.java create mode 100644 Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/EdgeComparator.java create mode 100644 Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/FilterDialog.java create mode 100644 Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/GhidraIconCache.java create mode 100644 Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/IconShape.java create mode 100644 Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/LayoutFunction.java create mode 100644 Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/LayoutTransitionManager.java create mode 100644 Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/ProgramGraphFunctions.java create mode 100644 Ghidra/Features/GraphServices/src/main/resources/images/magnifier.png create mode 100644 Ghidra/Features/GraphServices/src/main/resources/images/redspheregraph.png create mode 100644 Ghidra/Features/GraphServices/src/main/resources/images/sat2.png create mode 100644 Ghidra/Features/GraphServices/src/main/resources/images/tree.png create mode 100644 Ghidra/Features/GraphServices/src/main/resources/jungrapht.properties create mode 100644 Ghidra/Features/GraphServices/src/test/java/ghidra/graph/export/GraphExportTest.java create mode 100644 Ghidra/Features/GraphServices/src/test/java/ghidra/graph/visualization/ColorsTest.java create mode 100644 Ghidra/Features/GraphServices/src/test/java/ghidra/graph/visualization/IconShapeTest.java create mode 100644 Ghidra/Features/ProgramGraph/Module.manifest create mode 100644 Ghidra/Features/ProgramGraph/build.gradle create mode 100644 Ghidra/Features/ProgramGraph/certification.manifest create mode 100644 Ghidra/Features/ProgramGraph/src/main/help/help/TOC_Source.xml create mode 100644 Ghidra/Features/ProgramGraph/src/main/help/help/shared/arrow.gif create mode 100644 Ghidra/Features/ProgramGraph/src/main/help/help/shared/note.png create mode 100644 Ghidra/Features/ProgramGraph/src/main/help/help/shared/tip.png create mode 100644 Ghidra/Features/ProgramGraph/src/main/help/help/topics/ProgramGraphPlugin/ProgramGraph.htm create mode 100644 Ghidra/Features/ProgramGraph/src/main/help/help/topics/ProgramGraphPlugin/images/BasicBlockExampleCode.png create mode 100644 Ghidra/Features/ProgramGraph/src/main/help/help/topics/ProgramGraphPlugin/images/BasicBlockGraph.png create mode 100644 Ghidra/Features/ProgramGraph/src/main/help/help/topics/ProgramGraphPlugin/images/CodeBlockGraph.png create mode 100644 Ghidra/Features/ProgramGraph/src/main/help/help/topics/ProgramGraphPlugin/images/FocusGraphNode.png create mode 100644 Ghidra/Features/ProgramGraph/src/main/help/help/topics/ProgramGraphPlugin/images/SelectGraphNode.png create mode 100644 Ghidra/Features/ProgramGraph/src/main/java/ghidra/graph/program/BlockGraphTask.java create mode 100644 Ghidra/Features/ProgramGraph/src/main/java/ghidra/graph/program/BlockModelGraphDisplayListener.java create mode 100644 Ghidra/Features/ProgramGraph/src/main/java/ghidra/graph/program/ProgramGraphPlugin.java create mode 100644 Ghidra/Features/ProgramGraph/src/test/java/ghidra/graph/program/AbstractBlockGraphTest.java create mode 100644 Ghidra/Features/ProgramGraph/src/test/java/ghidra/graph/program/BlockGraphEventTest.java create mode 100644 Ghidra/Features/ProgramGraph/src/test/java/ghidra/graph/program/BlockGraphTaskTest.java create mode 100644 Ghidra/Features/ProgramGraph/src/test/java/ghidra/graph/program/TestGraphDisplay.java create mode 100644 Ghidra/Features/ProgramGraph/src/test/java/ghidra/graph/program/TestGraphService.java create mode 100644 Ghidra/Framework/Graph/data/ExtensionPoint.manifest create mode 100644 Ghidra/Framework/Graph/src/main/java/ghidra/service/graph/Attributed.java create mode 100644 Ghidra/Framework/Graph/src/main/java/ghidra/service/graph/AttributedEdge.java create mode 100644 Ghidra/Framework/Graph/src/main/java/ghidra/service/graph/AttributedGraph.java create mode 100644 Ghidra/Framework/Graph/src/main/java/ghidra/service/graph/AttributedVertex.java create mode 100644 Ghidra/Framework/Graph/src/test/java/ghidra/service/graph/AttributedGraphTest.java delete mode 100644 Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/graph/GraphData.java delete mode 100644 Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/graph/GraphDisplay.java delete mode 100644 Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/graph/GraphEdge.java delete mode 100644 Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/graph/GraphSelectionHandler.java delete mode 100644 Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/graph/GraphVertex.java delete mode 100644 Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/graph/package.html create mode 100644 Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/service/graph/DummyGraphDisplayListener.java create mode 100644 Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/service/graph/GraphDisplay.java create mode 100644 Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/service/graph/GraphDisplayListener.java create mode 100644 Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/service/graph/GraphDisplayProvider.java create mode 100644 Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/GraphServicesScreenShots.java create mode 100644 Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/ProgramGraphPluginScreenShots.java diff --git a/Ghidra/Features/Base/data/ExtensionPoint.manifest b/Ghidra/Features/Base/data/ExtensionPoint.manifest index 45344d6ecc..a13556031b 100644 --- a/Ghidra/Features/Base/data/ExtensionPoint.manifest +++ b/Ghidra/Features/Base/data/ExtensionPoint.manifest @@ -19,3 +19,4 @@ InstructionSkipper DataTypeReferenceFinder ChecksumAlgorithm OverviewColorService + diff --git a/Ghidra/Features/Base/developer_scripts/GenerateBrandesKopfGraphScript.java b/Ghidra/Features/Base/developer_scripts/GenerateBrandesKopfGraphScript.java new file mode 100644 index 0000000000..d2b3a1edeb --- /dev/null +++ b/Ghidra/Features/Base/developer_scripts/GenerateBrandesKopfGraphScript.java @@ -0,0 +1,131 @@ +/* ### + * 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. + */ +import ghidra.app.script.GhidraScript; +import ghidra.app.services.GraphDisplayBroker; +import ghidra.framework.plugintool.PluginTool; +import ghidra.service.graph.*; + +/** + * Script to generate graph to test BrandesKopf algorithm + */ +public class GenerateBrandesKopfGraphScript extends GhidraScript { + private AttributedGraph graph = new AttributedGraph(); + private int nextEdgeID = 1; + + @Override + protected void run() throws Exception { + PluginTool tool = getState().getTool(); + GraphDisplayBroker service = tool.getService(GraphDisplayBroker.class); + GraphDisplay display = service.getDefaultGraphDisplay(false, monitor); + generateGraph(); + display.setGraph(graph, "Test2", false, monitor); + } + + + private void generateGraph() { + + AttributedVertex[] list = new AttributedVertex[24]; + int i=1; + list[i++] = vertex("1"); + list[i++] = vertex("2"); + list[i++] = vertex("3"); + list[i++] = vertex("4"); + list[i++] = vertex("5"); + list[i++] = vertex("6"); + list[i++] = vertex("7"); + list[i++] = vertex("8"); + list[i++] = vertex("9"); + list[i++] = vertex("10"); + list[i++] = vertex("11"); + list[i++] = vertex("12"); + list[i++] = vertex("13"); + list[i++] = vertex("14"); + list[i++] = vertex("15"); + list[i++] = vertex("16"); + list[i++] = vertex("17"); + list[i++] = vertex("18"); + list[i++] = vertex("19"); + list[i++] = vertex("20"); + list[i++] = vertex("21"); + list[i++] = vertex("22"); + list[i++] = vertex("23"); + + edge(list[1], list[3]); + edge(list[1], list[4]); + edge(list[1], list[13]); + edge(list[1], list[21]); + + edge(list[2], list[3]); + edge(list[2], list[20]); + + edge(list[3], list[4]); + edge(list[3], list[5]); + edge(list[3], list[23]); + + edge(list[4], list[6]); + + edge(list[5], list[7]); + + edge(list[6], list[8]); + edge(list[6], list[16]); + edge(list[6], list[23]); + + edge(list[7], list[9]); + + edge(list[8], list[10]); + edge(list[8], list[11]); + + edge(list[9], list[12]); + + edge(list[10], list[13]); + edge(list[10], list[14]); + edge(list[10], list[15]); + + edge(list[11], list[15]); + edge(list[11], list[16]); + + edge(list[12], list[20]); + + edge(list[13], list[17]); + + edge(list[14], list[17]); + edge(list[14], list[18]); + // no 15 targets + + edge(list[16], list[18]); + edge(list[16], list[19]); + edge(list[16], list[20]); + + edge(list[18], list[21]); + + edge(list[19], list[22]); + + edge(list[21], list[23]); + + edge(list[22], list[23]); + + } + + private AttributedVertex vertex(String name) { + return graph.addVertex(name, name); + } + + + private AttributedEdge edge(AttributedVertex v1, AttributedVertex v2) { + return graph.addEdge(v1, v2); + } + +} diff --git a/Ghidra/Features/Base/developer_scripts/GenerateTestGraphScript.java b/Ghidra/Features/Base/developer_scripts/GenerateTestGraphScript.java new file mode 100644 index 0000000000..2de9fb2dad --- /dev/null +++ b/Ghidra/Features/Base/developer_scripts/GenerateTestGraphScript.java @@ -0,0 +1,60 @@ +/* ### + * 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. + */ +import ghidra.app.script.GhidraScript; +import ghidra.app.services.GraphDisplayBroker; +import ghidra.framework.plugintool.PluginTool; +import ghidra.service.graph.*; + +/** + * Sample script to test graph service + */ +public class GenerateTestGraphScript extends GhidraScript { + private AttributedGraph graph = new AttributedGraph(); + private int nextEdgeID = 1; + + @Override + protected void run() throws Exception { + PluginTool tool = getState().getTool(); + GraphDisplayBroker service = tool.getService(GraphDisplayBroker.class); + GraphDisplay display = service.getDefaultGraphDisplay(false, monitor); + generateGraph(); + display.setGraph(graph, "Test", false, monitor); + } + + private void generateGraph() { + + AttributedVertex A = vertex("A"); + AttributedVertex B = vertex("B"); + AttributedVertex C = vertex("C"); + AttributedVertex D = vertex("D"); + + edge(A, B); + edge(A, C); + edge(B, D); + edge(C, D); + edge(D, A); + } + + private AttributedVertex vertex(String name) { + return graph.addVertex(name, name); + } + + private AttributedEdge edge(AttributedVertex v1, AttributedVertex v2) { + return graph.addEdge(v1, v2); + } + + +} diff --git a/Ghidra/Features/Base/ghidra_scripts/ExampleGraphServiceScript.java b/Ghidra/Features/Base/ghidra_scripts/ExampleGraphServiceScript.java new file mode 100644 index 0000000000..40909ad766 --- /dev/null +++ b/Ghidra/Features/Base/ghidra_scripts/ExampleGraphServiceScript.java @@ -0,0 +1,59 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import ghidra.app.script.GhidraScript; +import ghidra.app.services.GraphDisplayBroker; +import ghidra.framework.plugintool.PluginTool; +import ghidra.service.graph.*; + +/** + * Example script for creating and displaying a graph in ghidra + */ +public class ExampleGraphServiceScript extends GhidraScript { + private AttributedGraph graph = new AttributedGraph(); + private int nextEdgeID = 1; + + @Override + protected void run() throws Exception { + PluginTool tool = getState().getTool(); + GraphDisplayBroker service = tool.getService(GraphDisplayBroker.class); + GraphDisplay display = service.getDefaultGraphDisplay(false, monitor); + generateGraph(); + display.setGraph(graph, "Test", false, monitor); + } + + private void generateGraph() { + + AttributedVertex A = vertex("A"); + AttributedVertex B = vertex("B"); + AttributedVertex C = vertex("C"); + AttributedVertex D = vertex("D"); + + edge(A, B); + edge(A, C); + edge(B, D); + edge(C, D); + edge(D, A); + } + + private AttributedVertex vertex(String name) { + return graph.addVertex(name, name); + } + + private AttributedEdge edge(AttributedVertex v1, AttributedVertex v2) { + return graph.addEdge(v1, v2); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/graph/AddressBasedGraphDisplayListener.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/graph/AddressBasedGraphDisplayListener.java new file mode 100644 index 0000000000..38a162be22 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/graph/AddressBasedGraphDisplayListener.java @@ -0,0 +1,190 @@ +/* ### + * 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.graph; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +import ghidra.app.events.*; +import ghidra.framework.model.*; +import ghidra.framework.plugintool.PluginEvent; +import ghidra.framework.plugintool.PluginTool; +import ghidra.framework.plugintool.util.PluginEventListener; +import ghidra.program.model.address.*; +import ghidra.program.model.listing.Program; +import ghidra.program.model.symbol.*; +import ghidra.program.util.*; +import ghidra.service.graph.GraphDisplay; +import ghidra.service.graph.GraphDisplayListener; +import ghidra.util.Swing; + +/** + * Base class for GraphDisplay listeners whose nodes represent addresses. + */ +public abstract class AddressBasedGraphDisplayListener + implements GraphDisplayListener, PluginEventListener, DomainObjectListener { + + private PluginTool tool; + private GraphDisplay graphDisplay; + protected Program program; + private SymbolTable symbolTable; + private String name; + private static AtomicInteger instanceCount = new AtomicInteger(1); + + public AddressBasedGraphDisplayListener(PluginTool tool, Program program, + GraphDisplay display) { + this.tool = tool; + this.program = program; + this.symbolTable = program.getSymbolTable(); + this.graphDisplay = display; + name = getClass().getSimpleName() + instanceCount.getAndAdd(1); + tool.addListenerForAllPluginEvents(this); + program.addListener(this); + } + + @Override + public void graphClosed() { + dispose(); + } + + @Override + public void locationChanged(String vertexId) { + Address address = getAddressForVertexId(vertexId); + if (address != null) { + ProgramLocation location = new ProgramLocation(program, address); + tool.firePluginEvent(new ProgramLocationPluginEvent(name, location, program)); + } + } + + @Override + public void selectionChanged(List vertexIds) { + AddressSet addressSet = getAddressSetForVertices(vertexIds); + if (addressSet != null) { + ProgramSelection selection = new ProgramSelection(addressSet); + ProgramSelectionPluginEvent event = + new ProgramSelectionPluginEvent(name, selection, program); + tool.firePluginEvent(event); + } + } + + @Override + public void eventSent(PluginEvent event) { + if (Objects.equals(event.getSourceName(), name)) { + return; + } + + if (event instanceof ProgramClosedPluginEvent) { + ProgramClosedPluginEvent ev = (ProgramClosedPluginEvent) event; + if (isMyProgram(ev.getProgram())) { + graphDisplay.close(); + dispose(); + } + } + else if (event instanceof ProgramLocationPluginEvent) { + ProgramLocationPluginEvent ev = (ProgramLocationPluginEvent) event; + if (isMyProgram(ev.getProgram())) { + ProgramLocation location = ev.getLocation(); + graphDisplay.setLocation(getVertexIdForAddress(location.getAddress())); + } + } + else if (event instanceof ProgramSelectionPluginEvent) { + ProgramSelectionPluginEvent ev = (ProgramSelectionPluginEvent) event; + if (isMyProgram(ev.getProgram())) { + ProgramSelection selection = ev.getSelection(); + List selectedVertices = getVertices(selection); + if (selectedVertices != null) { + graphDisplay.selectVertices(selectedVertices); + } + } + } + } + + protected String getVertexIdForAddress(Address address) { + // vertex ids for external locations use symbol names since they don't have meaningful addresses. + if (address.isExternalAddress()) { + Symbol s = symbolTable.getPrimarySymbol(address); + return s.getName(true); + } + return address.toString(); + } + + protected Address getAddress(String vertexIdString) { + Address address = program.getAddressFactory().getAddress(vertexIdString); + if (address != null) { + return address; + } + + // the vertex id was not an address, see if it is an external symbol name + int index = vertexIdString.indexOf(Namespace.DELIMITER); + if (index <= 0) { + return null; + } + String namespaceName = vertexIdString.substring(0, index); + String symbolName = vertexIdString.substring(index + 2); + Namespace namespace = symbolTable.getNamespace(namespaceName, null); + if (namespace == null) { + return null; + } + + List symbols = symbolTable.getSymbols(symbolName, namespace); + if (symbols.isEmpty()) { + return null; + } + // there should only be one external symbol with the same name, so just assume the first one is good + return symbols.get(0).getAddress(); + + } + + protected Address getAddressForVertexId(String vertexId) { + return getAddress(vertexId); + } + + protected abstract List getVertices(AddressSetView selection); + + protected abstract AddressSet getAddressSetForVertices(List vertexIds); + + private boolean isMyProgram(Program p) { + return p == program; + } + + @Override + public void domainObjectChanged(DomainObjectChangedEvent ev) { + if (!ev.containsEvent(ChangeManager.DOCR_SYMBOL_RENAMED)) { + return; + } + for (DomainObjectChangeRecord record : ev) { + if (record.getEventType() == ChangeManager.DOCR_SYMBOL_RENAMED) { + ProgramChangeRecord programRecord = (ProgramChangeRecord) record; + handleSymbolRenamed(programRecord); + } + } + } + + private void handleSymbolRenamed(ProgramChangeRecord programRecord) { + Symbol symbol = (Symbol) programRecord.getObject(); + String newName = symbol.getName(); + Address address = symbol.getAddress(); + String id = getVertexIdForAddress(address); + graphDisplay.updateVertexName(id, newName); + } + + private void dispose() { + Swing.runLater(() -> tool.removeListenerForAllPluginEvents(this)); + program.removeListener(this); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/graph/GraphDisplayBrokerListener.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/graph/GraphDisplayBrokerListener.java new file mode 100644 index 0000000000..8b341154ec --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/graph/GraphDisplayBrokerListener.java @@ -0,0 +1,22 @@ +/* ### + * 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.graph; + +public interface GraphDisplayBrokerListener { + + void providersChanged(); + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/graph/GraphDisplayBrokerPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/graph/GraphDisplayBrokerPlugin.java new file mode 100644 index 0000000000..c64b0b909d --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/graph/GraphDisplayBrokerPlugin.java @@ -0,0 +1,209 @@ +/* ### + * 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.graph; + +import java.util.*; + +import docking.ActionContext; +import docking.action.MenuData; +import docking.action.ToggleDockingAction; +import ghidra.app.CorePluginPackage; +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.app.services.GraphDisplayBroker; +import ghidra.framework.options.*; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.util.PluginStatus; +import ghidra.service.graph.GraphDisplay; +import ghidra.service.graph.GraphDisplayProvider; +import ghidra.util.classfinder.ClassSearcher; +import ghidra.util.exception.GraphException; +import ghidra.util.task.TaskMonitor; + +//@formatter:off +@PluginInfo( + status = PluginStatus.RELEASED, + packageName = CorePluginPackage.NAME, + category = PluginCategoryNames.GRAPH, + shortDescription = "Manages the active Graph Display Service", + description = "This plugin searches for available graph display providers and if it finds more" + + "than one, it provides menu options for the user to choose the active provider.", + servicesProvided = { GraphDisplayBroker.class } +) +//@formatter:on +public class GraphDisplayBrokerPlugin extends Plugin + implements GraphDisplayBroker, OptionsChangeListener { + private static final String ACTIVE_GRAPH_PROVIDER = "ACTIVE_GRAPH_PROVIDER"; + private List graphDisplayProviders = new ArrayList<>(); + private GraphDisplayProvider defaultGraphDisplayProvider; + private List listeners = new ArrayList<>(); + private List actions = new ArrayList<>(); + + public GraphDisplayBrokerPlugin(PluginTool tool) { + super(tool); + loadServices(); + buildActions(); + } + + @Override + public void writeConfigState(SaveState saveState) { + if (defaultGraphDisplayProvider != null) { + saveState.putString(ACTIVE_GRAPH_PROVIDER, defaultGraphDisplayProvider.getName()); + } + } + + @Override + public void readConfigState(SaveState saveState) { + String active = saveState.getString(ACTIVE_GRAPH_PROVIDER, null); + if (active != null) { + for (GraphDisplayProvider provider : graphDisplayProviders) { + if (provider.getName().equals(active)) { + setDefaultGraphDisplayProvider(provider); + return; + } + } + } + } + + private void loadServices() { + Set instances = + new HashSet<>(ClassSearcher.getInstances(GraphDisplayProvider.class)); + graphDisplayProviders = new ArrayList<>(instances); + Collections.sort(graphDisplayProviders, (s1, s2) -> s1.getName().compareTo(s2.getName())); + initializeServices(); + if (!graphDisplayProviders.isEmpty()) { + defaultGraphDisplayProvider = graphDisplayProviders.get(0); + } + } + + private void initializeServices() { + for (GraphDisplayProvider service : graphDisplayProviders) { + ToolOptions options = tool.getOptions("Graph"); + options.addOptionsChangeListener(this); + service.initialize(tool, options); + } + } + + private void buildActions() { + if (graphDisplayProviders.size() <= 1) { + return; + } + for (GraphDisplayProvider graphDisplayProvider : graphDisplayProviders) { + createAction(graphDisplayProvider); + } + updateActions(); + } + + private void createAction(GraphDisplayProvider provider) { + GraphSelectionAction action = new GraphSelectionAction(getName(), provider); + actions.add(action); + tool.addAction(action); + } + + private void updateActions() { + for (GraphSelectionAction action : actions) { + action.setSelected(defaultGraphDisplayProvider == action.provider); + } + } + + protected void notifyListeners() { + for (GraphDisplayBrokerListener listener : listeners) { + listener.providersChanged(); + } + } + + @Override + public GraphDisplayProvider getDefaultGraphDisplayProvider() { + return defaultGraphDisplayProvider; + } + + @Override + public void addGraphDisplayBrokerListener(GraphDisplayBrokerListener listener) { + listeners.add(listener); + } + + @Override + public void removeGraphDisplayBrokerLisetener(GraphDisplayBrokerListener listener) { + listeners.remove(listener); + } + + @Override + public void dispose() { + for (GraphDisplayProvider graphService : graphDisplayProviders) { + graphService.dispose(); + } + } + + @Override + public GraphDisplay getDefaultGraphDisplay(boolean reuseGraph, + TaskMonitor monitor) throws GraphException { + if (defaultGraphDisplayProvider != null) { + return defaultGraphDisplayProvider.getGraphDisplay(reuseGraph, monitor); + } + return null; + } + + public void setDefaultGraphDisplayProvider(GraphDisplayProvider provider) { + defaultGraphDisplayProvider = provider; + notifyListeners(); + updateActions(); + } + + @Override + public boolean hasDefaultGraphDisplayProvider() { + return !graphDisplayProviders.isEmpty(); + } + + /** + * Action for selecting a {@link GraphDisplayProvider} to be the currently active provider + */ + private class GraphSelectionAction extends ToggleDockingAction { + + private GraphDisplayProvider provider; + + public GraphSelectionAction(String owner, GraphDisplayProvider provider) { + super(provider.getName(), owner); + this.provider = provider; + setMenuBarData( + new MenuData(new String[] { "Graph", "Graph Output", provider.getName() }, "z")); + setHelpLocation(provider.getHelpLocation()); + } + + @Override + public void actionPerformed(ActionContext context) { + setDefaultGraphDisplayProvider(provider); + } + } + + @Override + public void optionsChanged(ToolOptions options, String optionName, Object oldValue, + Object newValue) { + + for (GraphDisplayProvider graphService : graphDisplayProviders) { + graphService.optionsChanged(options); + } + } + + @Override + public GraphDisplayProvider getGraphDisplayProvider(String providerName) { + for (GraphDisplayProvider provider : graphDisplayProviders) { + if (provider.getName().equals(providerName)) { + return provider; + } + } + return null; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/services/GraphDisplayBroker.java b/Ghidra/Features/Base/src/main/java/ghidra/app/services/GraphDisplayBroker.java new file mode 100644 index 0000000000..57112667a0 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/services/GraphDisplayBroker.java @@ -0,0 +1,77 @@ +/* ### + * 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.services; + +import ghidra.app.plugin.core.graph.GraphDisplayBrokerListener; +import ghidra.app.plugin.core.graph.GraphDisplayBrokerPlugin; +import ghidra.framework.plugintool.ServiceInfo; +import ghidra.service.graph.GraphDisplay; +import ghidra.service.graph.GraphDisplayProvider; +import ghidra.util.exception.GraphException; +import ghidra.util.task.TaskMonitor; + +/** + * Ghidra service interface for managing and directing graph output. It purpose is to discover + * available graphing display providers and (if more than one) allow the user to select the currently + * active graph consumer. Clients that generate graphs don't have to worry about how to display them + * or export graphs. They simply send their graphs to the broker and register for graph events if + * they want interactive support. + */ +@ServiceInfo(defaultProvider = GraphDisplayBrokerPlugin.class, description = "Get a Graph Display") +public interface GraphDisplayBroker { + + /** + * Gets the currently active GraphDisplayProvider that will be used to display/export graphs + * @return the currently active GraphDisplayProvider + */ + public GraphDisplayProvider getDefaultGraphDisplayProvider(); + + /** + * Adds a listener for notification when the set of graph display providers change or the currently + * active graph display provider changes + * @param listener the listener to be notified + */ + public void addGraphDisplayBrokerListener(GraphDisplayBrokerListener listener); + + /** + * Removes the given listener + * @param listener the listener to no longer be notified of changes + */ + public void removeGraphDisplayBrokerLisetener(GraphDisplayBrokerListener listener); + + /** + * A convenience method for getting a {@link GraphDisplay} from the currently active provider + * @param reuseGraph if true, the provider will attempt to re-use a current graph display + * @param monitor the {@link TaskMonitor} that can be used to cancel the operation + * @return a {@link GraphDisplay} object to sends graphs to be displayed or exported. + * @throws GraphException thrown if an error occurs trying to get a graph display + */ + public GraphDisplay getDefaultGraphDisplay(boolean reuseGraph, TaskMonitor monitor) + throws GraphException; + + /** + * Checks if there is at least one {@link GraphDisplayProvider} in the system. + * @return true if there is at least one {@link GraphDisplayProvider} + */ + public boolean hasDefaultGraphDisplayProvider(); + + /** + * Gets the {@link GraphDisplayProvider} with the given name + * @param name the name of the GraphDisplayProvider to get + * @return the GraphDisplayProvider with the given name or null if none with that name exists. + */ + public GraphDisplayProvider getGraphDisplayProvider(String name); +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/services/GraphService.java b/Ghidra/Features/Base/src/main/java/ghidra/app/services/GraphService.java deleted file mode 100644 index 31f3d6e836..0000000000 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/services/GraphService.java +++ /dev/null @@ -1,82 +0,0 @@ -/* ### - * 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.services; - -import ghidra.framework.plugintool.ServiceInfo; -import ghidra.program.model.graph.*; -import ghidra.util.exception.GraphException; - - -/** - * Service for getting a Graph display. - * - */ -@ServiceInfo(/* defaultProvider = NONE, */ description = "Get a Graph Display") -public interface GraphService { - - /** - * Create Graph Data compatible with this graph service - */ - GraphData createGraphContent(); - - /** - * Get a graph display. - * @param newDisplay a new graph window will be used if true. - * @throws GraphException if unable to obtain a graph window. - */ - GraphDisplay getGraphDisplay(boolean newDisplay) throws GraphException; - - /** - * Get a graph display. - * @throws GraphException if unable to obtain a graph window. - */ - GraphDisplay getGraphDisplay() throws GraphException; - - /** - * Send specified selection object to all connected graphs - * that understand the concept of "selection." - * @param selection selection object to interpret - */ - void setSelection(Object selection); - - /** - * Send specified location object to all connected graphs that understand - * the concept of "location." - * @param location location object to interpret - */ - void setLocation(Object location); - - /** - * Set the selection for all connected graphs and fire a selection event - * for Ghidra. - * @param selection selection object to interpret - */ - void fireSelectionEvent(Object selection); - /** - * Set the location for all connected graphs and fire a location event - * for Ghidra. - * @param location location object to interpret - */ - void fireLocationEvent(Object location); - /** - * Handle notification from graph. - * @param notificationType command generated from graph - * @param handler associated graph handler - * @return true if notification was handled and there is no need for any other - * graph service provider to notified. - */ - boolean fireNotificationEvent(String notificationType, GraphSelectionHandler handler); -} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/XRefHeaderFieldFactory.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/XRefHeaderFieldFactory.java index ff63d5c44c..4d587296b8 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/XRefHeaderFieldFactory.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/XRefHeaderFieldFactory.java @@ -34,7 +34,7 @@ import ghidra.program.util.XRefHeaderFieldLocation; * Field for display XRef headers. */ public class XRefHeaderFieldFactory extends XRefFieldFactory { - private static final String XREF_FIELD_NAME = "XRef Header"; + public static final String XREF_FIELD_NAME = "XRef Header"; public XRefHeaderFieldFactory() { super(XREF_FIELD_NAME); diff --git a/Ghidra/Features/Decompiler/build.gradle b/Ghidra/Features/Decompiler/build.gradle index 55b09c0f2d..73cc0e965e 100644 --- a/Ghidra/Features/Decompiler/build.gradle +++ b/Ghidra/Features/Decompiler/build.gradle @@ -13,7 +13,6 @@ eclipse.project.name = 'Features Decompiler' dependencies { compile project(':Base') compile project(':SoftwareModeling') - // include Base src/test/resources when running decompiler integration tests (uses defaultTools) integrationTestRuntime project(path: ':Base', configuration: 'testArtifacts') testCompile "org.jmockit:jmockit:1.44" diff --git a/Ghidra/Features/Decompiler/ghidra_scripts/GraphAST.java b/Ghidra/Features/Decompiler/ghidra_scripts/GraphAST.java index 6c4c3f0757..f1d5d06ff9 100644 --- a/Ghidra/Features/Decompiler/ghidra_scripts/GraphAST.java +++ b/Ghidra/Features/Decompiler/ghidra_scripts/GraphAST.java @@ -19,309 +19,245 @@ import java.util.*; import ghidra.app.decompiler.*; +import ghidra.app.plugin.core.graph.AddressBasedGraphDisplayListener; import ghidra.app.script.GhidraScript; -import ghidra.app.services.GraphService; +import ghidra.app.services.GraphDisplayBroker; import ghidra.framework.plugintool.PluginTool; import ghidra.program.model.address.*; -import ghidra.program.model.graph.*; import ghidra.program.model.lang.Register; import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.Program; import ghidra.program.model.pcode.*; +import ghidra.service.graph.*; import ghidra.util.Msg; public class GraphAST extends GhidraScript { protected static final String COLOR_ATTRIBUTE = "Color"; protected static final String ICON_ATTRIBUTE = "Icon"; - - Function func; - HighFunction high; - GraphData graph; - int edgecount; - - @Override - public void run() throws Exception { + + private Function func; + private AttributedGraph graph; + protected HighFunction high; + + @Override + public void run() throws Exception { PluginTool tool = state.getTool(); if (tool == null) { println("Script is not running in GUI"); } - GraphService graphSvc = tool.getService(GraphService.class); - if (graphSvc == null) { - Msg.showError(this, - tool.getToolFrame(), - "GraphAST Error", - "GraphService not found: Please add a graph service provider to your tool"); + GraphDisplayBroker graphDisplayBroker = tool.getService(GraphDisplayBroker.class); + if (graphDisplayBroker == null) { + Msg.showError(this, tool.getToolFrame(), "GraphAST Error", + "No graph display providers found: Please add a graph display provider to your tool"); return; } - - func = this.getFunctionContaining(this.currentAddress); + + func = this.getFunctionContaining(this.currentAddress); if (func == null) { - Msg.showWarn(this, - state.getTool().getToolFrame(), - "GraphAST Error", - "No Function at current location"); + Msg.showWarn(this, state.getTool().getToolFrame(), "GraphAST Error", + "No Function at current location"); return; } - + buildAST(); - - graph = graphSvc.createGraphContent(); + + graph = new AttributedGraph(); buildGraph(); - - GraphDisplay graphDisplay = graphSvc.getGraphDisplay(true); + + GraphDisplay graphDisplay = + graphDisplayBroker.getDefaultGraphDisplay(false, monitor); // graphDisplay.defineVertexAttribute(CODE_ATTRIBUTE); // // graphDisplay.defineVertexAttribute(SYMBOLS_ATTRIBUTE); // graphDisplay.defineEdgeAttribute(EDGE_TYPE_ATTRIBUTE); - graphDisplay.setGraphData(graph); - - // Install a handler so the selection/location will map - graphDisplay.setSelectionHandler(new GraphASTSelectionHandler(graphSvc, high,func.getProgram().getAddressFactory())); - } + graphDisplay.setGraph(graph, "Data-flow AST", false, monitor); - private void buildAST() throws DecompileException { - DecompileOptions options = new DecompileOptions(); + // Install a handler so the selection/location will map + graphDisplay.setGraphDisplayListener( + new ASTGraphDisplayListener(tool, graphDisplay, high, func.getProgram())); + } + + private void buildAST() throws DecompileException { + DecompileOptions options = new DecompileOptions(); DecompInterface ifc = new DecompInterface(); ifc.setOptions(options); - - if ( !ifc.openProgram(this.currentProgram) ) { - throw new DecompileException("Decompiler", "Unable to initialize: "+ifc.getLastMessage()); + + if (!ifc.openProgram(this.currentProgram)) { + throw new DecompileException("Decompiler", + "Unable to initialize: " + ifc.getLastMessage()); } ifc.setSimplificationStyle("normalize"); DecompileResults res = ifc.decompileFunction(func, 30, null); - high = res.getHighFunction(); - - } - - private String getVarnodeKey(VarnodeAST vn) { - PcodeOp op = vn.getDef(); - String id; - if (op != null) - id = op.getSeqnum().getTarget().toString(true) + " v " + Integer.toString(vn.getUniqueId()); - else - id = "i v " + Integer.toString(vn.getUniqueId()); - return id; - } - - private String getOpKey(PcodeOpAST op) { - SequenceNumber sq = op.getSeqnum(); - String id = sq.getTarget().toString(true) + " o " +Integer.toString(op.getSeqnum().getTime()); - return id; - } - - protected GraphVertex createVarnodeVertex(VarnodeAST vn) { - String name = vn.getAddress().toString(true); - String id = getVarnodeKey(vn); - String colorattrib = "Red"; - if (vn.isConstant()) - colorattrib = "DarkGreen"; - else if (vn.isRegister()) { - colorattrib = "Blue"; - Register reg = func.getProgram().getRegister(vn.getAddress(),vn.getSize()); - if (reg != null) - name = reg.getName(); - } - else if (vn.isUnique()) - colorattrib = "Black"; - else if (vn.isPersistant()) - colorattrib = "DarkOrange"; - else if (vn.isAddrTied()) - colorattrib = "Orange"; - GraphVertex vert = graph.createVertex(name, id); - if (vn.isInput()) - vert.setAttribute(ICON_ATTRIBUTE, "TriangleDown"); - else - vert.setAttribute(ICON_ATTRIBUTE, "Circle"); - vert.setAttribute(COLOR_ATTRIBUTE,colorattrib); - return vert; - } - - protected GraphVertex createOpVertex(PcodeOpAST op) { - String name = op.getMnemonic(); - String id = getOpKey(op); - int opcode = op.getOpcode(); - if ((opcode==PcodeOp.LOAD)||(opcode==PcodeOp.STORE)) { - Varnode vn = op.getInput(0); - AddressSpace addrspace = func.getProgram().getAddressFactory().getAddressSpace((int)vn.getOffset()); - name += ' ' + addrspace.getName(); - } - else if (opcode == PcodeOp.INDIRECT) { - Varnode vn = op.getInput(1); - if (vn != null) { - PcodeOp indOp = high.getOpRef((int)vn.getOffset()); - if (indOp != null) { - name += " (" + indOp.getMnemonic() +')'; - } - } - } - GraphVertex vert = graph.createVertex(name, id); - vert.setAttribute(ICON_ATTRIBUTE, "Square"); - return vert; - } - - protected GraphVertex getVarnodeVertex(HashMap vertices,VarnodeAST vn) { - GraphVertex res; - res = vertices.get(vn.getUniqueId()); - if (res == null) { - res = createVarnodeVertex(vn); - vertices.put(vn.getUniqueId(), res); - } - return res; - } - - protected GraphEdge createEdge(GraphVertex in,GraphVertex out) { - String id = Integer.toString(edgecount); - edgecount += 1; - return graph.createEdge(id, in, out); - } - - protected void buildGraph() { + high = res.getHighFunction(); - HashMap vertices = new HashMap(); - - edgecount = 0; - Iterator opiter = getPcodeOpIterator(); - while(opiter.hasNext()) { - PcodeOpAST op = opiter.next(); - GraphVertex o = createOpVertex(op); - for(int i=0;i vertices, VarnodeAST vn) { + AttributedVertex res; + res = vertices.get(vn.getUniqueId()); + if (res == null) { + res = createVarnodeVertex(vn); + vertices.put(vn.getUniqueId(), res); + } + return res; + } + + protected AttributedEdge createEdge(AttributedVertex in, AttributedVertex out) { + return graph.addEdge(in, out); + } + + protected void buildGraph() { + + HashMap vertices = new HashMap<>(); + + Iterator opiter = getPcodeOpIterator(); + while (opiter.hasNext()) { + PcodeOpAST op = opiter.next(); + AttributedVertex o = createOpVertex(op); + for (int i = 0; i < op.getNumInputs(); ++i) { + int opcode = op.getOpcode(); + if ((i == 0) && ((opcode == PcodeOp.LOAD) || (opcode == PcodeOp.STORE))) { + continue; + } + if ((i == 1) && (opcode == PcodeOp.INDIRECT)) { + continue; + } + VarnodeAST vn = (VarnodeAST) op.getInput(i); + if (vn != null) { + AttributedVertex v = getVarnodeVertex(vertices, vn); + createEdge(v, o); + } + } + VarnodeAST outvn = (VarnodeAST) op.getOutput(); + if (outvn != null) { + AttributedVertex outv = getVarnodeVertex(vertices, outvn); + if (outv != null) { + createEdge(o, outv); + } + } + } + } protected Iterator getPcodeOpIterator() { Iterator opiter = high.getPcodeOps(); return opiter; } - - class GraphASTSelectionHandler implements GraphSelectionHandler { - private boolean active; // true if the window is active - private boolean enabled; - HighFunction highfunc; - private GraphService graphService; - private AddressFactory addrFactory; - public GraphASTSelectionHandler(GraphService graphService,HighFunction highfunc,AddressFactory addrFactory) { - active = false; - enabled = true; - this.graphService = graphService; - this.highfunc = highfunc; - this.addrFactory = addrFactory; - } + class ASTGraphDisplayListener extends AddressBasedGraphDisplayListener { - private Address keyToAddress(String key) { - int firstcolon = key.indexOf(':'); - if (firstcolon == -1) return null; - int firstspace = key.indexOf(' '); - String addrspacestring = key.substring(0,firstcolon); - String addrstring = key.substring(firstcolon+1,firstspace); - AddressSpace spc = addrFactory.getAddressSpace(addrspacestring); - if (spc == null) return null; - try { - return spc.getAddress(addrstring); - } catch (AddressFormatException e) { + HighFunction highfunc; + + public ASTGraphDisplayListener(PluginTool tool, GraphDisplay display, HighFunction high, + Program program) { + super(tool, program, display); + highfunc = high; + } + + @Override + protected List getVertices(AddressSetView selection) { + List ids = new ArrayList(); + return ids; + } + + @Override + protected AddressSet getAddressSetForVertices(List vertexIds) { + AddressSet set = new AddressSet(); + for (String id : vertexIds) { + Address address = getAddressForVertexId(id); + if (address != null) { + set.add(address); + } + } + return set; + } + + @Override + protected Address getAddressForVertexId(String vertexId) { + int firstcolon = vertexId.indexOf(':'); + if (firstcolon == -1) { return null; } - } - - public String getGraphType() { - return "Data-flow AST"; - } - public boolean isActive() { - return active; + int firstSpace = vertexId.indexOf(' '); + String addrString = vertexId.substring(0, firstSpace); + return getAddress(addrString); } - - public boolean isEnabled() { - return enabled; - } - - public void locate(String renoirLocation) { - Address addr = keyToAddress(renoirLocation); - if (addr==null) return; - graphService.fireLocationEvent(addr); - } - - public String locate(Object ghidraLocation) { - if (!(ghidraLocation instanceof Address)) - return null; - - Address addr = (Address)ghidraLocation; - Iterator iter = highfunc.getPcodeOps(addr); - if (iter.hasNext()) { - PcodeOpAST op = iter.next(); - return getOpKey(op); - } - return null; - } - - public boolean notify(String notificationType) { - return false; - } - - public void select(String[] renoirSelections) { - if (!enabled) - return; - - AddressSet set = new AddressSet(); - for (int i = 0; i < renoirSelections.length; i++) { - Address addr = keyToAddress(renoirSelections[i]); - if (addr == null) { - continue; - } - set.addRange(addr,addr); - } - - graphService.fireSelectionEvent(set); - } - - public String[] select(Object ghidraSelection) { - String [] keys; - if (ghidraSelection == null) { - keys = new String[0]; - return keys; - } - if (!(ghidraSelection instanceof AddressSetView)) { - return null; // selection not understood - } - AddressSetView set = (AddressSetView) ghidraSelection; - ArrayList ops = new ArrayList(); - Iterator iter = highfunc.getPcodeOps(); - while(iter.hasNext()) { - PcodeOpAST op = iter.next(); - Address addr = op.getSeqnum().getTarget(); - if (set.contains(addr)) { - ops.add(getOpKey(op)); - VarnodeAST vn = (VarnodeAST)op.getOutput(); - if (vn != null) - ops.add(getVarnodeKey(vn)); - } - } - keys = new String[ ops.size() ]; - return ops.toArray(keys); - } - - public void setActive(boolean active) { - this.active = active; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - } + } } diff --git a/Ghidra/Features/Decompiler/ghidra_scripts/GraphASTAndFlow.java b/Ghidra/Features/Decompiler/ghidra_scripts/GraphASTAndFlow.java index 4e553c1b66..64b45e3f57 100644 --- a/Ghidra/Features/Decompiler/ghidra_scripts/GraphASTAndFlow.java +++ b/Ghidra/Features/Decompiler/ghidra_scripts/GraphASTAndFlow.java @@ -18,40 +18,40 @@ import java.util.*; -import ghidra.program.model.graph.GraphEdge; -import ghidra.program.model.graph.GraphVertex; import ghidra.program.model.pcode.*; +import ghidra.service.graph.AttributedEdge; +import ghidra.service.graph.AttributedVertex; public class GraphASTAndFlow extends GraphAST { @Override protected void buildGraph() { - HashMap vertices = new HashMap(); + HashMap vertices = new HashMap<>(); - edgecount = 0; Iterator opiter = getPcodeOpIterator(); - HashMap map = new HashMap(); + HashMap map = new HashMap(); while (opiter.hasNext()) { PcodeOpAST op = opiter.next(); - GraphVertex o = createOpVertex(op); + AttributedVertex o = createOpVertex(op); map.put(op, o); for (int i = 0; i < op.getNumInputs(); ++i) { if ((i == 0) && ((op.getOpcode() == PcodeOp.LOAD) || (op.getOpcode() == PcodeOp.STORE))) { continue; } - if ((i == 1)&&(op.getOpcode() == PcodeOp.INDIRECT)) + if ((i == 1) && (op.getOpcode() == PcodeOp.INDIRECT)) { continue; + } VarnodeAST vn = (VarnodeAST) op.getInput(i); if (vn != null) { - GraphVertex v = getVarnodeVertex(vertices, vn); + AttributedVertex v = getVarnodeVertex(vertices, vn); createEdge(v, o); } } VarnodeAST outvn = (VarnodeAST) op.getOutput(); if (outvn != null) { - GraphVertex outv = getVarnodeVertex(vertices, outvn); + AttributedVertex outv = getVarnodeVertex(vertices, outvn); if (outv != null) { createEdge(o, outv); } @@ -59,8 +59,8 @@ public class GraphASTAndFlow extends GraphAST { } opiter = getPcodeOpIterator(); HashSet seenParents = new HashSet(); - HashMap first = new HashMap(); - HashMap last = new HashMap(); + HashMap first = new HashMap<>(); + HashMap last = new HashMap<>(); while (opiter.hasNext()) { PcodeOpAST op = opiter.next(); PcodeBlockBasic parent = op.getParent(); @@ -76,7 +76,7 @@ public class GraphASTAndFlow extends GraphAST { first.put(parent, map.get(next)); } if (prev != null && map.containsKey(prev) && map.containsKey(next)) { - GraphEdge edge = createEdge(map.get(prev), map.get(next)); + AttributedEdge edge = createEdge(map.get(prev), map.get(next)); edge.setAttribute(COLOR_ATTRIBUTE, "Black"); } prev = next; @@ -91,18 +91,10 @@ public class GraphASTAndFlow extends GraphAST { for (int i = 0; i < block.getInSize(); i++) { PcodeBlock in = block.getIn(i); if (last.containsKey(in)) { - GraphEdge edge = createEdge(last.get(in), first.get(block)); + AttributedEdge edge = createEdge(last.get(in), first.get(block)); edge.setAttribute(COLOR_ATTRIBUTE, "Red"); } } -// All outs were already handled by the ins! Don't make two links! -// for (int i = 0; i < block.getOutSize(); i++) { -// PcodeBlock out = block.getOut(i); -// if (first.containsKey(out)) { -// GraphEdge edge = createEdge(last.get(block), first.get(out)); -// edge.setAttribute(COLOR_ATTRIBUTE, "Red"); -// } -// } } } diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/DecompilerProvider.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/DecompilerProvider.java index fd02731241..839925bdad 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/DecompilerProvider.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/DecompilerProvider.java @@ -125,14 +125,14 @@ public class DecompilerProvider extends NavigatableComponentProviderAdapter @Override public void serviceRemoved(Class interfaceClass, Object service) { - if (interfaceClass.equals(GraphService.class)) { + if (interfaceClass.equals(GraphDisplayBroker.class)) { graphServiceRemoved(); } } @Override public void serviceAdded(Class interfaceClass, Object service) { - if (interfaceClass.equals(GraphService.class)) { + if (interfaceClass.equals(GraphDisplayBroker.class)) { graphServiceAdded(); } } @@ -967,7 +967,10 @@ public class DecompilerProvider extends NavigatableComponentProviderAdapter } private void graphServiceRemoved() { - if (graphASTControlFlowAction != null && tool.getService(GraphService.class) == null) { + if (graphASTControlFlowAction == null) { + return; + } + if (tool.getService(GraphDisplayBroker.class) == null) { tool.removeAction(graphASTControlFlowAction); graphASTControlFlowAction.dispose(); graphASTControlFlowAction = null; @@ -975,7 +978,8 @@ public class DecompilerProvider extends NavigatableComponentProviderAdapter } private void graphServiceAdded() { - if (graphASTControlFlowAction == null && tool.getService(GraphService.class) != null) { + GraphDisplayBroker service = tool.getService(GraphDisplayBroker.class); + if (service != null && service.getDefaultGraphDisplayProvider() != null) { graphASTControlFlowAction = new GraphASTControlFlowAction(); addLocalAction(graphASTControlFlowAction); } diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/ASTGraphDisplayListener.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/ASTGraphDisplayListener.java new file mode 100644 index 0000000000..6493033efc --- /dev/null +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/ASTGraphDisplayListener.java @@ -0,0 +1,96 @@ +/* ### + * 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.decompile.actions; + +import static ghidra.app.plugin.core.decompile.actions.ASTGraphTask.GraphType.*; + +import java.util.List; + +import ghidra.app.plugin.core.decompile.actions.ASTGraphTask.GraphType; +import ghidra.app.plugin.core.graph.AddressBasedGraphDisplayListener; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.*; +import ghidra.program.model.pcode.HighFunction; +import ghidra.program.model.pcode.PcodeBlockBasic; +import ghidra.service.graph.GraphDisplay; + +/** + * Listener for when an AST graph's nodes are selected. + */ +public class ASTGraphDisplayListener extends AddressBasedGraphDisplayListener { + private HighFunction hfunction; + private GraphType graphType; + + ASTGraphDisplayListener(PluginTool tool, GraphDisplay display, HighFunction hfunction, + GraphType graphType) { + super(tool, hfunction.getFunction().getProgram(), display); + this.hfunction = hfunction; + this.graphType = graphType; + } + + @Override + protected List getVertices(AddressSetView selection) { + return null; + } + + @Override + protected AddressSet getAddressSetForVertices(List vertexIds) { + if (graphType != CONTROL_FLOW_GRAPH) { + return null; + } + + AddressSet set = new AddressSet(); + Address location = null; + List blocks = hfunction.getBasicBlocks(); + for (String vertixId : vertexIds) { + try { + int index = Integer.parseInt(vertixId); + PcodeBlockBasic block = blocks.get(index); + Address start = block.getStart(); + set.addRange(start, block.getStop()); + if (location == null || start.compareTo(location) < 0) { + location = start; + } + } + catch (NumberFormatException e) { + // continue + } + } + return set; + } + + @Override + protected String getVertexIdForAddress(Address address) { + if (graphType != CONTROL_FLOW_GRAPH) { + return null; + } + List blocks = hfunction.getBasicBlocks(); + for (PcodeBlockBasic block : blocks) { + Address start = block.getStart(); + Address stop = block.getStop(); + if (address.compareTo(start) >= 0 && address.compareTo(stop) <= 0) { + return Integer.toString(block.getIndex()); + } + } + return super.getVertexIdForAddress(address); + } + + @Override + protected Address getAddressForVertexId(String vertexId) { + return null; + } + +} diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/ASTGraphSelectionHandler.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/ASTGraphSelectionHandler.java deleted file mode 100644 index e08fd7f3a2..0000000000 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/ASTGraphSelectionHandler.java +++ /dev/null @@ -1,132 +0,0 @@ -/* ### - * 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.decompile.actions; - -import java.util.List; - -import ghidra.app.services.GraphService; -import ghidra.program.model.address.Address; -import ghidra.program.model.address.AddressSet; -import ghidra.program.model.graph.GraphSelectionHandler; -import ghidra.program.model.pcode.HighFunction; -import ghidra.program.model.pcode.PcodeBlockBasic; -import ghidra.program.util.ProgramSelection; - -class ASTGraphSelectionHandler implements GraphSelectionHandler { - - private GraphService graphService; - private HighFunction hfunction; - private int graphType; - - private boolean active = false; // true if the window is active - private boolean enabled = true; - - ASTGraphSelectionHandler(GraphService graphService, HighFunction hfunction, int graphType) { - this.graphService = graphService; - this.hfunction = hfunction; - this.graphType = graphType; - } - - public String getGraphType() { - return graphType == ASTGraphTask.DATA_FLOW_GRAPH ? - "AST Data Flow" : "AST Control Flow"; - } - - public boolean isActive() { - return active; - } - - public boolean isEnabled() { - return enabled; - } - - public void setActive(boolean active) { - this.active = active; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public void locate(String location) { - //Msg.debug(this, "locate1: " + location); - } - - public String locate(Object locationObject) { - - if (graphType != ASTGraphTask.CONTROL_FLOW_GRAPH) { - return null; - } - - if (!(locationObject instanceof Address)) - return null; - - Address addr = (Address) locationObject; - - List blocks = hfunction.getBasicBlocks(); - for (PcodeBlockBasic block : blocks) { - Address start = block.getStart(); - Address stop = block.getStop(); - if (addr.compareTo(start) >= 0 && addr.compareTo(stop) <= 0) { - //Msg.debug(this, "index=" + block.getIndex()); - return Integer.toString(block.getIndex()); - } - } - return addr.toString(); - } - - public boolean notify(String notificationType) { - //Msg.debug(this, "notify: " + notificationType); - return false; - } - - public void select(String[] selectedIndexes) { - - if (graphType != ASTGraphTask.CONTROL_FLOW_GRAPH) { - return; - } - - AddressSet set = new AddressSet(); - Address location = null; - List blocks = hfunction.getBasicBlocks(); - for (String indexStr : selectedIndexes) { - try { - int index = Integer.parseInt(indexStr); - PcodeBlockBasic block = blocks.get(index); - Address start = block.getStart(); - set.addRange(start, block.getStop()); - if (location == null || start.compareTo(location) < 0) { - location = start; - } - } - catch (NumberFormatException e) { - // continue - } - } - if (location != null) { - graphService.fireLocationEvent(location); - } - graphService.fireSelectionEvent(new ProgramSelection(set)); - } - - public String[] select(Object ghidraSelection) { - // TODO Auto-generated method stub - return null; - } - - - -} diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/ASTGraphTask.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/ASTGraphTask.java index 32310f8acf..5f586f9084 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/ASTGraphTask.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/ASTGraphTask.java @@ -15,27 +15,34 @@ */ package ghidra.app.plugin.core.decompile.actions; -import ghidra.app.services.GraphService; +import java.util.Iterator; + +import ghidra.app.services.GraphDisplayBroker; +import ghidra.framework.plugintool.PluginTool; import ghidra.program.model.address.Address; -import ghidra.program.model.graph.*; import ghidra.program.model.lang.Register; import ghidra.program.model.listing.Program; import ghidra.program.model.pcode.*; +import ghidra.service.graph.*; import ghidra.util.Msg; import ghidra.util.NumericUtilities; import ghidra.util.exception.CancelledException; import ghidra.util.exception.GraphException; -import ghidra.util.task.*; - -import java.util.Iterator; +import ghidra.util.task.Task; +import ghidra.util.task.TaskMonitor; public class ASTGraphTask extends Task { + enum GraphType { + CONTROL_FLOW_GRAPH("AST Control Flow"), DATA_FLOW_GRAPH("AST Data Flow"); + private String name; + GraphType(String name) { + this.name = name; + } - static final int CONTROL_FLOW_GRAPH = 0; - static final int DATA_FLOW_GRAPH = 1; - - private static final String[] GRAPH_TYPES = - new String[] { "AST Control Flow", "AST Data Flow" }; + public String getName() { + return name; + } + } private static final String CODE_ATTRIBUTE = "Code"; private static final String SYMBOLS_ATTRIBUTE = "Symbols"; @@ -55,19 +62,19 @@ public class ASTGraphTask extends Task { private final static String DATA_NODE = "Data"; // "6"; // Data Node, used for indirection - private GraphService graphService; + private GraphDisplayBroker graphService; private boolean newGraph; private int codeLimitPerBlock; private Address location; private HighFunction hfunction; - private int graphType; + private GraphType graphType; private int uniqueNum = 0; - private TaskListener listener; + private PluginTool tool; - public ASTGraphTask(GraphService graphService, boolean newGraph, int codeLimitPerBlock, - Address location, HighFunction hfunction, int graphType) { - super("Graph " + GRAPH_TYPES[graphType], true, false, true); + public ASTGraphTask(GraphDisplayBroker graphService, boolean newGraph, int codeLimitPerBlock, + Address location, HighFunction hfunction, GraphType graphType, PluginTool tool) { + super("Graph " + graphType.getName(), true, false, true); this.graphService = graphService; this.newGraph = newGraph; @@ -75,87 +82,60 @@ public class ASTGraphTask extends Task { this.location = location; this.hfunction = hfunction; this.graphType = graphType; - - this.listener = new TaskListener() { - @Override - public void taskCancelled(Task task) { - // don't care - } - - @Override - public void taskCompleted(Task task) { - try { - GraphDisplay graphDisplay = - ASTGraphTask.this.graphService.getGraphDisplay(false); - if (graphDisplay != null) { - graphDisplay.popup(); - } - } - catch (GraphException e) { - // the programmer was too lazy to handle this - } - } - }; - addTaskListener(listener); + this.tool = tool; } @Override public void run(TaskMonitor monitor) { // get a new graph - GraphData graph = graphService.createGraphContent(); - if (graph == null) - return; + AttributedGraph graph = new AttributedGraph(); - ASTGraphSelectionHandler handler = null; try { monitor.setMessage("Computing Graph..."); - if (graphType == DATA_FLOW_GRAPH) { + if (graphType == GraphType.DATA_FLOW_GRAPH) { createDataFlowGraph(graph, monitor); } else { createControlFlowGraph(graph, monitor); } - handler = new ASTGraphSelectionHandler(graphService, hfunction, graphType); - } - catch (CancelledException e1) { - return; - } + GraphDisplay display = graphService.getDefaultGraphDisplay(!newGraph, monitor); + ASTGraphDisplayListener displayListener = + new ASTGraphDisplayListener(tool, display, hfunction, graphType); + display.setGraphDisplayListener(displayListener); - GraphDisplay display; - try { monitor.setMessage("Obtaining handle to graph provider..."); - display = graphService.getGraphDisplay(newGraph); - if (monitor.isCancelled()) + if (monitor.isCancelled()) { return; - monitor.setCancelEnabled(false); - - if (!newGraph) { - display.clear(); } - display.setSelectionHandler(handler); + monitor.setCancelEnabled(false); monitor.setMessage("Rendering Graph..."); display.defineVertexAttribute(CODE_ATTRIBUTE); display.defineVertexAttribute(SYMBOLS_ATTRIBUTE); - display.setGraphData(graph); - display.setVertexLabel(CODE_ATTRIBUTE, GraphDisplay.ALIGN_LEFT, 12, true, - graphType == CONTROL_FLOW_GRAPH ? (codeLimitPerBlock + 1) : 1); + graphType == GraphType.CONTROL_FLOW_GRAPH ? (codeLimitPerBlock + 1) : 1); + String description = + graphType == GraphType.DATA_FLOW_GRAPH ? "AST Data Flow" : "AST Control Flow"; + display.setGraph(graph, description, false, monitor); // set the graph location if (location != null) { - display.locate(location, false); + display.setLocation(displayListener.getVertexIdForAddress(location)); } } catch (GraphException e) { Msg.showError(this, null, "Graph Error", e.getMessage()); } + catch (CancelledException e1) { + return; + } + } - protected void createDataFlowGraph(GraphData graph, TaskMonitor monitor) + protected void createDataFlowGraph(AttributedGraph graph, TaskMonitor monitor) throws CancelledException { Iterator opIter = hfunction.getPcodeOps(); while (opIter.hasNext()) { @@ -164,7 +144,7 @@ public class ASTGraphTask extends Task { } } - private void graphOpData(GraphData graph, PcodeOpAST op, TaskMonitor monitor) + private void graphOpData(AttributedGraph graph, PcodeOpAST op, TaskMonitor monitor) throws CancelledException { // TODO: Dropped INDIRECT pcode ops ?? @@ -173,13 +153,13 @@ public class ASTGraphTask extends Task { return; } - GraphVertex opVertex = getOpVertex(graph, op, monitor); + AttributedVertex opVertex = getOpVertex(graph, op, monitor); Varnode output = op.getOutput(); if (output != null) { opVertex = getOpVertex(graph, op, monitor); - GraphVertex outVertex = getDataVertex(graph, output, monitor); - graph.createEdge(Integer.toString(++uniqueNum), opVertex, outVertex); + AttributedVertex outVertex = getDataVertex(graph, output, monitor); + graph.addEdge(opVertex, outVertex); // TODO: set edge attributes ?? } @@ -204,26 +184,26 @@ public class ASTGraphTask extends Task { if (opVertex == null) { opVertex = getOpVertex(graph, op, monitor); } - GraphVertex inVertex = getDataVertex(graph, input, monitor); - graph.createEdge(Integer.toString(++uniqueNum), inVertex, opVertex); + AttributedVertex inVertex = getDataVertex(graph, input, monitor); + graph.addEdge(inVertex, opVertex); // TODO: set edge attributes ?? } } } - private GraphVertex getOpVertex(GraphData graph, PcodeOpAST op, TaskMonitor monitor) { + private AttributedVertex getOpVertex(AttributedGraph graph, PcodeOpAST op, TaskMonitor monitor) { String key = "O_" + Integer.toString(op.getSeqnum().getTime()); - GraphVertex vertex = graph.getVertex(key); + AttributedVertex vertex = graph.getVertex(key); if (vertex == null) { - vertex = graph.createVertex(key, key); + vertex = graph.addVertex(key, key); setOpVertexAttributes(vertex, op); } return vertex; } - private void setOpVertexAttributes(GraphVertex vertex, PcodeOpAST op) { + private void setOpVertexAttributes(AttributedVertex vertex, PcodeOpAST op) { vertex.setAttribute(CODE_ATTRIBUTE, formatOpMnemonic(op)); @@ -243,11 +223,11 @@ public class ASTGraphTask extends Task { vertex.setAttribute(VERTEX_TYPE_ATTRIBUTE, vertexType); } - private GraphVertex getDataVertex(GraphData graph, Varnode node, TaskMonitor monitor) { + private AttributedVertex getDataVertex(AttributedGraph graph, Varnode node, TaskMonitor monitor) { // TODO: Missing Varnode unique ID ?? - GraphVertex vertex = null; + AttributedVertex vertex = null; HighVariable var = node.getHigh(); String key; if (var != null) { @@ -259,13 +239,13 @@ public class ASTGraphTask extends Task { } if (vertex == null) { - vertex = graph.createVertex(key, key); + vertex = graph.addVertex(key, key); setVarnodeVertexAttributes(vertex, node); } return vertex; } - private void setVarnodeVertexAttributes(GraphVertex vertex, Varnode node) { + private void setVarnodeVertexAttributes(AttributedVertex vertex, Varnode node) { String label = ""; HighVariable var = node.getHigh(); @@ -277,7 +257,7 @@ public class ASTGraphTask extends Task { vertex.setAttribute(VERTEX_TYPE_ATTRIBUTE, DATA_NODE); } - protected void createControlFlowGraph(GraphData graph, TaskMonitor monitor) + protected void createControlFlowGraph(AttributedGraph graph, TaskMonitor monitor) throws CancelledException { Iterator pblockIter = hfunction.getBasicBlocks().iterator(); while (pblockIter.hasNext()) { @@ -286,32 +266,32 @@ public class ASTGraphTask extends Task { } } - private void graphPcodeBlock(GraphData graph, PcodeBlock pblock, TaskMonitor monitor) + private void graphPcodeBlock(AttributedGraph graph, PcodeBlock pblock, TaskMonitor monitor) throws CancelledException { if (pblock == null) { return; } - GraphVertex fromVertex = getBlockVertex(graph, pblock, monitor); + AttributedVertex fromVertex = getBlockVertex(graph, pblock, monitor); int outCnt = pblock.getOutSize(); for (int i = 0; i < outCnt; i++) { monitor.checkCanceled(); PcodeBlock outPBlock = pblock.getOut(i); - GraphVertex toVertex = getBlockVertex(graph, outPBlock, monitor); - graph.createEdge(Integer.toString(++uniqueNum), fromVertex, toVertex); + AttributedVertex toVertex = getBlockVertex(graph, outPBlock, monitor); + graph.addEdge(fromVertex, toVertex); // TODO: set edge attributes ?? } } - private GraphVertex getBlockVertex(GraphData graph, PcodeBlock pblock, TaskMonitor monitor) { + private AttributedVertex getBlockVertex(AttributedGraph graph, PcodeBlock pblock, TaskMonitor monitor) { String key = Integer.toString(pblock.getIndex()); - GraphVertex vertex = graph.getVertex(key); + AttributedVertex vertex = graph.getVertex(key); if (vertex == null) { - vertex = graph.createVertex(key, key); + vertex = graph.addVertex(key, key); if (pblock instanceof PcodeBlockBasic) { setBlockVertexAttributes(vertex, (PcodeBlockBasic) pblock); } @@ -323,7 +303,7 @@ public class ASTGraphTask extends Task { return vertex; } - private void setBlockVertexAttributes(GraphVertex vertex, PcodeBlockBasic basicBlk) { + private void setBlockVertexAttributes(AttributedVertex vertex, PcodeBlockBasic basicBlk) { // Build Pcode representation StringBuffer buf = new StringBuffer(); @@ -440,19 +420,4 @@ public class ASTGraphTask extends Task { } return node.toString(); } - -// private void assignVertexSymbols(GraphVertex vertex, Address addr) { -// Symbol[] symbols = function.getProgram().getSymbolTable().getSymbols(addr); -// if (symbols.length != 0) { -// StringBuffer buf = new StringBuffer(); -// for (int i = 0; i < symbols.length; i++) { -// if (i != 0) { -// buf.append('\n'); -// } -// buf.append(symbols[i].getName()); -// } -// vertex.setAttribute(SYMBOLS_ATTRIBUTE, buf.toString()); -// } -// } - } diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/GraphASTControlFlowAction.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/GraphASTControlFlowAction.java index 990b00b2e9..a9a2e4ca56 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/GraphASTControlFlowAction.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/GraphASTControlFlowAction.java @@ -15,16 +15,17 @@ */ package ghidra.app.plugin.core.decompile.actions; +import static ghidra.app.plugin.core.decompile.actions.ASTGraphTask.GraphType.*; + import docking.action.MenuData; import ghidra.app.plugin.core.decompile.DecompilerActionContext; -import ghidra.app.services.GraphService; +import ghidra.app.services.GraphDisplayBroker; import ghidra.framework.options.Options; import ghidra.framework.plugintool.PluginTool; import ghidra.program.model.address.Address; import ghidra.program.model.pcode.HighFunction; import ghidra.util.Msg; import ghidra.util.task.TaskLauncher; - public class GraphASTControlFlowAction extends AbstractDecompilerAction { public GraphASTControlFlowAction() { @@ -40,10 +41,10 @@ public class GraphASTControlFlowAction extends AbstractDecompilerAction { @Override protected void decompilerActionPerformed(DecompilerActionContext context) { PluginTool tool = context.getTool(); - GraphService graphService = tool.getService(GraphService.class); - if (graphService == null) { + GraphDisplayBroker service = tool.getService(GraphDisplayBroker.class); + if (service == null) { Msg.showError(this, tool.getToolFrame(), "AST Graph Failed", - "GraphService not found: Please add a graph service provider to your tool"); + "Graph consumer not found: Please add a graph consumer provider to your tool"); return; } @@ -53,8 +54,8 @@ public class GraphASTControlFlowAction extends AbstractDecompilerAction { int codeLimitPerBlock = options.getInt("Max Code Lines Displayed", 10); HighFunction highFunction = context.getHighFunction(); Address locationAddr = context.getLocation().getAddress(); - ASTGraphTask task = new ASTGraphTask(graphService, !reuseGraph, codeLimitPerBlock, - locationAddr, highFunction, ASTGraphTask.CONTROL_FLOW_GRAPH); + ASTGraphTask task = new ASTGraphTask(service, !reuseGraph, codeLimitPerBlock, locationAddr, + highFunction, CONTROL_FLOW_GRAPH, tool); new TaskLauncher(task, tool.getToolFrame()); } diff --git a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/Krb5ActiveDirectoryAuthenticationModule.java b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/Krb5ActiveDirectoryAuthenticationModule.java index 9f128657a3..369a2b8a9c 100644 --- a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/Krb5ActiveDirectoryAuthenticationModule.java +++ b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/Krb5ActiveDirectoryAuthenticationModule.java @@ -115,10 +115,10 @@ public class Krb5ActiveDirectoryAuthenticationModule implements AuthenticationMo throw new IOException("Missing username or password values"); } - NameCallback destNcb = AuthenticationModule.getFirstCallbackOfType( - NameCallback.class, loginmodule_callbacks); - PasswordCallback destPcb = AuthenticationModule.getFirstCallbackOfType( - PasswordCallback.class, loginmodule_callbacks); + NameCallback destNcb = AuthenticationModule + .getFirstCallbackOfType(NameCallback.class, loginmodule_callbacks); + PasswordCallback destPcb = AuthenticationModule + .getFirstCallbackOfType(PasswordCallback.class, loginmodule_callbacks); if (destNcb != null) { destNcb.setName(tmpName); diff --git a/Ghidra/Features/GraphServices/Module.manifest b/Ghidra/Features/GraphServices/Module.manifest new file mode 100644 index 0000000000..2a754f3992 --- /dev/null +++ b/Ghidra/Features/GraphServices/Module.manifest @@ -0,0 +1,7 @@ +EXCLUDE FROM GHIDRA JAR: true + +MODULE FILE LICENSE: lib/jungrapht-visualization-2.11.20 BSD +MODULE FILE LICENSE: lib/jgrapht-core-1.3.1.jar LGPL 2.1 +MODULE FILE LICENSE: lib/jgrapht-io-1.3.1.jar LGPL 2.1 +MODULE FILE LICENSE: lib/jheaps-0.10.jar Apache License 2.0 +MODULE FILE LICENSE: lib/slf4j-api-1.7.25.jar MIT diff --git a/Ghidra/Features/GraphServices/build.gradle b/Ghidra/Features/GraphServices/build.gradle new file mode 100644 index 0000000000..abc1114f66 --- /dev/null +++ b/Ghidra/Features/GraphServices/build.gradle @@ -0,0 +1,25 @@ +apply from: "$rootProject.projectDir/gradle/distributableGhidraModule.gradle" +apply from: "$rootProject.projectDir/gradle/javaProject.gradle" +apply from: "$rootProject.projectDir/gradle/helpProject.gradle" +apply from: "$rootProject.projectDir/gradle/jacocoProject.gradle" +apply from: "$rootProject.projectDir/gradle/javaTestProject.gradle" +apply plugin: 'eclipse' + +eclipse.project.name = 'Features Graph Services' + + +dependencies { + compile project(":Base") + + compile "com.github.tomnelson:jungrapht-visualization:1.0-RC7" + compile "org.jgrapht:jgrapht-core:1.4.0" + + // not using jgrapht-io code that depends on antlr, so exclude antlr + compile ("org.jgrapht:jgrapht-io:1.4.0") { exclude group: "org.antlr", module: "antlr4-runtime" } + runtime "org.slf4j:slf4j-api:1.7.25" + runtime "org.jheaps:jheaps:0.11" + + helpPath project(path: ":Base", configuration: 'helpPath') + +} + diff --git a/Ghidra/Features/GraphServices/certification.manifest b/Ghidra/Features/GraphServices/certification.manifest new file mode 100644 index 0000000000..111ded149c --- /dev/null +++ b/Ghidra/Features/GraphServices/certification.manifest @@ -0,0 +1,17 @@ +##VERSION: 2.0 +##MODULE IP: BSD +Module.manifest||GHIDRA||||END| +build.gradle||GHIDRA||||END| +src/main/help/help/TOC_Source.xml||GHIDRA||||END| +src/main/help/help/shared/arrow.gif||GHIDRA||||END| +src/main/help/help/shared/note.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END| +src/main/help/help/shared/tip.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END| +src/main/help/help/topics/GraphServices/GraphDisplay.htm||GHIDRA||||END| +src/main/help/help/topics/GraphServices/GraphExport.htm||GHIDRA||||END| +src/main/help/help/topics/GraphServices/images/DefaultGraphDisplay.png||GHIDRA||||END| +src/main/help/help/topics/GraphServices/images/ExportDialog.png||GHIDRA||||END| +src/main/resources/images/magnifier.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END| +src/main/resources/images/redspheregraph.png||GHIDRA||||END| +src/main/resources/images/sat2.png||GHIDRA||||END| +src/main/resources/images/tree.png||GHIDRA||||END| +src/main/resources/jungrapht.properties||GHIDRA||||END| diff --git a/Ghidra/Features/GraphServices/src/main/help/help/TOC_Source.xml b/Ghidra/Features/GraphServices/src/main/help/help/TOC_Source.xml new file mode 100644 index 0000000000..372d07792a --- /dev/null +++ b/Ghidra/Features/GraphServices/src/main/help/help/TOC_Source.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + diff --git a/Ghidra/Features/GraphServices/src/main/help/help/shared/Frontpage.css b/Ghidra/Features/GraphServices/src/main/help/help/shared/Frontpage.css new file mode 100644 index 0000000000..452bf6e6b5 --- /dev/null +++ b/Ghidra/Features/GraphServices/src/main/help/help/shared/Frontpage.css @@ -0,0 +1,58 @@ +/* ### + * 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. + */ +/* + WARNING! + This file is copied to all help directories. If you change this file, you must copy it + to each src/main/help/help/shared directory. + + + Java Help Note: JavaHelp does not accept sizes (like in 'margin-top') in anything but + px (pixel) or with no type marking. + +*/ + +body { margin-bottom: 50px; margin-left: 10px; margin-right: 10px; margin-top: 10px; } /* some padding to improve readability */ +li { font-family:times new roman; font-size:14pt; } +h1 { color:#000080; font-family:times new roman; font-size:36pt; font-style:italic; font-weight:bold; text-align:center; } +h2 { margin: 10px; margin-top: 20px; color:#984c4c; font-family:times new roman; font-size:18pt; font-weight:bold; } +h3 { margin-left: 10px; margin-top: 20px; color:#0000ff; font-family:times new roman; font-size:14pt; font-weight:bold; } +h4 { margin-left: 10px; margin-top: 20px; font-family:times new roman; font-size:14pt; font-style:italic; } + +/* + P tag code. Most of the help files nest P tags inside of blockquote tags (the was the + way it had been done in the beginning). The net effect is that the text is indented. In + modern HTML we would use CSS to do this. We need to support the Ghidra P tags, nested in + blockquote tags, as well as naked P tags. The following two lines accomplish this. Note + that the 'blockquote p' definition will inherit from the first 'p' definition. +*/ +p { margin-left: 40px; font-family:times new roman; font-size:14pt; } +blockquote p { margin-left: 10px; } + +p.providedbyplugin { color:#7f7f7f; margin-left: 10px; font-size:14pt; margin-top:100px } +p.ProvidedByPlugin { color:#7f7f7f; margin-left: 10px; font-size:14pt; margin-top:100px } +p.relatedtopic { color:#800080; margin-left: 10px; font-size:14pt; } +p.RelatedTopic { color:#800080; margin-left: 10px; font-size:14pt; } + +/* + We wish for a tables to have space between it and the preceding element, so that text + is not too close to the top of the table. Also, nest the table a bit so that it is clear + the table relates to the preceding text. +*/ +table { margin-left: 20px; margin-top: 10px; width: 80%;} +td { font-family:times new roman; font-size:14pt; vertical-align: top; } +th { font-family:times new roman; font-size:14pt; font-weight:bold; background-color: #EDF3FE; } + +code { color: black; font-family: courier new; font-size: 14pt; } diff --git a/Ghidra/Features/GraphServices/src/main/help/help/shared/arrow.gif b/Ghidra/Features/GraphServices/src/main/help/help/shared/arrow.gif new file mode 100644 index 0000000000000000000000000000000000000000..bcb3db70578a43a6aef37fbb13aeb0b3b0ca81be GIT binary patch literal 69 zcmZ?wbhEHb6k_0GXkY+=|Ns9h{$ycfU|?j>0r5dH3`{aT{VPwu<>#Lfcu7j_z|Sb& THV==isgesP{Mz8g$Y2csRcRCu literal 0 HcmV?d00001 diff --git a/Ghidra/Features/GraphServices/src/main/help/help/shared/note.png b/Ghidra/Features/GraphServices/src/main/help/help/shared/note.png new file mode 100644 index 0000000000000000000000000000000000000000..51e1c8f8c1fa1688a64436ed366e3dba15a6b750 GIT binary patch literal 4193 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 literal 0 HcmV?d00001 diff --git a/Ghidra/Features/GraphServices/src/main/help/help/shared/tip.png b/Ghidra/Features/GraphServices/src/main/help/help/shared/tip.png new file mode 100644 index 0000000000000000000000000000000000000000..209f1d28c8ce13db65dd975598913501cb1bbd97 GIT binary patch literal 1641 zcmV-v2A27WP)Eyh)6Gs@ zkuH#=l-3Vj=tBr?aOgrIebHNKUeXuSHqbT@2!=vwFqBgK(n6XND2+?n*xB65R%I!c zEJasWSJK>V*7JR3lvk7V-devo-{C9+^W$SU3yBE&Vs>1~WBzdl`_>`vO$=Yn(FeT= zTsd0J!|*geF-0XPEei}QHlu!5D9h>f=D5v z(44pM#t%qrb6Gzad>#WUj1NHJL*Tz1JS*Mkk!$0hJIInfzkBttS zdJ%2*E3JI;%NrEMC-N)#Pw*jVd;s$1kuli`pUJAjYvNilnRO;600g4jyf3>o*mFAh zz_8I_bL6Si&y&+;EkR0#wn#V@pUoT^|qBR5R|W=2m+GZC_qdsEK8x9cvQ*C ztAnVo{RY4TeF-20K!n6}44Xzq#cc3^ zi>Z53di$9p`1hobkG>oA^bg1zN5{x;nFc>)LpHmFj4ki5Me-G>qO_+PjU5^us5ONg6~|;bbhArT)*wwNO)$d0-raKsNLlkBkZHZr zAUp1Y1X|{sjmA)ExY~(a2n?$yCv-fqN z;#{FDbC4{gVzzT42q}PIgpe~4bESfGtHXLAh>~Ki-PN|ETPX;DbaRR4nrK}ISv1$P z0NuN|NLtmcxa$)$2m%6Ru+oZ_*Tg3haXsR4+0Bb~HxlL8#sGq%k8C63)t{|X_^*sC z{GqRvkV^iq)z;RS%2OMF6h#pWGj_XFJZg7b-rLCCeU&5uiJ-uwO6}B!;M;%3$(6J} zfvofk%9EU0VHKP!K%|HRqa4pKMqbe-Frt)|w!j1yD#XNqI;}vp!U_fcZ2x<~=Wb+F z9hg;H^@EheE`)a-JXd(Vx!=FB>X)vrx}|IHmdcZdR3su0VMr1?s$M$?-+uuoS8D(F zZYVDgy~IIqfIo8As&bqhMAGVNT`6-;AT&a44^^`ag+OK$)oY_v-zd>cc8|5G|He%AQutjm?QM0*^v^9rME;sf>;PvH^i{onJTUg$_*OL|*TO%Ms znSsunfh65L_9ven1+XUqKYnqdwswo1r=I*OMu$AiUw#$L2FFj#;ERtwj>pdaP=Dd! zGnb#(D*-lUrPYb~?S&mRIdvHM{)a#yln$N5%9g@*GeHzZx6lI$y+Fw0Bte>{@IAjU zf~7U?zyAbU%_i#gI@;~FgT2ZCfl`wjilP`vnqYZ(8QS#$0!l#uOQ|9Q_MT;+I0AW| zmz9DN3LgLV6R7SR0yAT2X$g(3h7eJ6&+di=r$2q_*iP7d>DaMj<3TBa6k=KdfiySY ns8$E)H%9 literal 0 HcmV?d00001 diff --git a/Ghidra/Features/GraphServices/src/main/help/help/topics/GraphServices/GraphDisplay.htm b/Ghidra/Features/GraphServices/src/main/help/help/topics/GraphServices/GraphDisplay.htm new file mode 100644 index 0000000000..fc902b4cbe --- /dev/null +++ b/Ghidra/Features/GraphServices/src/main/help/help/topics/GraphServices/GraphDisplay.htm @@ -0,0 +1,67 @@ + + + + + + + Graphing + + + + + + +

Default Graph Display

+

Visualization of a Graph

+ +
+

The visualization display will show the graph in a new window or in a new tab of a previously created graph window.

+
+
+

+
+
+
+

Manipulating the Graph:

+
    +
  • MouseButton1+drag will translate the display in the x and y axis
  • +
  • Mouse Wheel will zoom in and out
  • +
  • Ctrl+MouseButton1 will select a vertex or edge
  • +
      +
    • Shift+Ctrl+MouseButton1 over an unselected vertex will add that vertex to the selection
    • +
    • Shift+Ctrl+MouseButton1 over a previously selected vertex will remove that vertex from the selection
    • +
    +
  • Ctrl+MouseButton1+drag on an empty area will create a rectangular area and select enclosed vertices
  • +
  • Ctrl+MouseButton1+drag over a vertex will reposition all selected vertices
  • +
+

Upper-right Icon Buttons:

+
+

Provided by: Program Graph Plugin

+ +

Related Topics

+
+ + diff --git a/Ghidra/Features/ProgramGraph/src/main/help/help/topics/ProgramGraphPlugin/images/BasicBlockExampleCode.png b/Ghidra/Features/ProgramGraph/src/main/help/help/topics/ProgramGraphPlugin/images/BasicBlockExampleCode.png new file mode 100644 index 0000000000000000000000000000000000000000..1e1af62e66f9ddf169653d054816e9580d183576 GIT binary patch literal 48514 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

y(uF3#ch*^A7no>YcB4cz16lyS_desGEp-jb5wY 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

1lVrChTvd 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 WyRa^&zuE@iziSFAm$T%IAN*f26mZD^ literal 0 HcmV?d00001 diff --git a/Ghidra/Features/ProgramGraph/src/main/help/help/topics/ProgramGraphPlugin/images/CodeBlockGraph.png b/Ghidra/Features/ProgramGraph/src/main/help/help/topics/ProgramGraphPlugin/images/CodeBlockGraph.png new file mode 100644 index 0000000000000000000000000000000000000000..91aa6af2d6c094cdfe53a3cd19c8db48509cae47 GIT binary patch literal 17920 zcmeIacTkgG*EV_!C`ADkQ2_xF1O)*VX@O8hdXXYZ6;Y7hLvMoc6QwulNDV={lu%Vf zK&p_?LXloWhe-c+(C0bx%=gZmIqx~&KWAQMoIsNMzV}{x?X}msu50;5LrtEVijfL} zAZkU0`I^|AQYlV>cZR!V5fCKTtax8q+sz0+LizpzwvOO%@g&)cOM>Ycez&d} zIhi>QNboI0;jtw^%@P~x_+&(4dbo!K;BpRAO;H*{P&XW-Knrb6?>|oio#NA~9 z^E2FOwiCNy9(4=N`TiOQ$YAuBOKEVG%u>db)Xf zcG$j=dc2BhZs?N*T@vS%`}vlpo@`~T@R%1}f-VIbEP<2>y;(jjbmJed6zlmpS{Cug zv}xYkgO=^@@0iRsWM$o_HTJ}pZ);)v$yqK_QgkES9Pm%WJINKVzI9_?$7pY8h?M^fzzo2q(`$Y>{_@>6v>|h%hj5&K-3;o>GOY^8` zbqUdZW@}D^G{lg_0#?^Jv|ZWKn0dXx3iA7Uv;7e(q;Yl!HaJq_N-MUvQom=*(OtLI zr|3@vFy_ez=eBKgaUmYl!AY|ToCEq3a zlSNO~8?h&gL*@C0@IdcC+b^$9j@yF^x8{?Xgsi(K4Xd3g!?zQJ`0F+`MTSv2JO?lN z^=Hujojwg5oA^4ysK?`+1a{|T$28x=ogS`uMJneUgV_|e(|UW6w+CNXvkY^sNR@cmeHFfx>Ugv9Jksa)I62&F@J0OVUio*I&(SpOueU}1 zy58P(uPtue%YXS}StnKS^qrgC*TEPa1DAW%B}e*@?`ki}47CV`@`)}Cq)P`3bU8KW zE7ubpTI7#IXVj`8dUI5kq|K4zZgt-ZI6-&$QPYAD1h zXwW;}Tl+m3-rsoFP$n^=SL8)%0$*-?^%v!w!L~Se&S8e_z3VHn24kGh^>j#5l3|wb z3G<0og_OA?F8zk%o+j_v!#FwGEYcs;r!}z0Ru-Oss4{QsdqYyv94@8V0d}w*W8Z^v zpHD@CWu%`o!zEBfZBce=FT!isrYT2ohz3Gx_;V`Fycy!YfqCjX5EQi{KZ#2 zcMaZQAZBj#_F(u)nb|!1D|Pr*Olp6HjsC&zl=Jtu{Pm9ujOz5%1WlLIWqSt%W;&_cWujLv`Y!8@5x^s3jdoGojtkUTkIJ~B@yw>kX zE!K@*iF8kcA(oKA)2>dq*1e}e*Smilg~P`_@dy4#Y$-^w7FFVsU$u<#ykB;AnB5us z0aa9z(NkS`!q1MX3`-7OxUVNR=%^f!$SKcv{Z^|*SnPHGX#pytv*V|mvu>K1z&OeX9%n!cA&%*3(Se+tNQio@m|&} z<79JqmskULd#QXl3%(O|cL$;Tq+*z-%R4-aQ!N>Zs{GOv>p3~dGpG-6ObUME|)%R>U_pK z;Es=6F-^rLjn7Wx=@uDz49(}Jv8Y9_fpEoprG7pq>8pE&>&j^4v`U%;{L10TiqEAz zhT|yhs7#VT08^V=tM}d;>UgG7l}A%L9C8XqX{;{37APhf>3Jl&BPu*>FDqh8l5*fl z&3Ny|m5Q-FSz7qL*%dom`{hJ&_tJ&Yx*C?wEdv~rUF|swF8>s8j5g)%7r>y;Oo}N> z65hKUhzZqny1TxszrKwBUY~B;`sC%`S(lc`JQqa6%$UH{%a5LlYPm;A{h$VccTVy- z*nPsAK_j+w+dEKwC?U->$h`giIU4oEdpDE$zi5Y963@mSqpw)o8hlPJd<&z6GHDDN zFCCFGPj&iyf*g537&BJ)ocj>`OtQ9%RPaSTEhAE_LbQm z^mCT|h0OJp>joP~JlEx)>VJB9j?W21&xNgitKsqwcZ9%oFH-kvLU zl`L@wpPz0Gt+uzI)ukClXsI>>VzGcKSa;cUcP1hz8X=fD`=o}eynV%^sDLo^G=*hd*#x% zT0vc)zcZm?+sV8mF#x

POYH@(<~O#-i@Z3Vgdy^`Hw7{Lg*hYYC6hV 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 QL?bH7sogJ>dHU*q0aRqf3jhEB literal 0 HcmV?d00001 diff --git a/Ghidra/Features/ProgramGraph/src/main/help/help/topics/ProgramGraphPlugin/images/FocusGraphNode.png b/Ghidra/Features/ProgramGraph/src/main/help/help/topics/ProgramGraphPlugin/images/FocusGraphNode.png new file mode 100644 index 0000000000000000000000000000000000000000..0573d5fca8a4e25ab4df494234542feffd385470 GIT binary patch literal 13711 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 Zf3GpJ^m?QU@Szn*MNw0sLjKW<{{;!x(~bZD literal 0 HcmV?d00001 diff --git a/Ghidra/Features/ProgramGraph/src/main/help/help/topics/ProgramGraphPlugin/images/SelectGraphNode.png b/Ghidra/Features/ProgramGraph/src/main/help/help/topics/ProgramGraphPlugin/images/SelectGraphNode.png new file mode 100644 index 0000000000000000000000000000000000000000..570ed9d64355ff33e447403e7621ec9945a4ab03 GIT binary patch literal 13288 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 is a threaded task creating either a block or call graph. + */ +public class BlockGraphTask extends Task { + + private static final String CODE_ATTRIBUTE = "Code"; + private static final String SYMBOLS_ATTRIBUTE = "Symbols"; + + protected static final String PROGRESS_DIALOG_TITLE = "Graphing Program"; + protected static final String INIT_PROGRESS_MSG = "Graphing Program..."; + + private boolean graphEntryPointNexus = false; + private boolean showCode = false; + private int codeLimitPerBlock = 10; + + private ColorizingService colorizingService; + + /** + * Edge flow tags + */ + protected final static int FALLTHROUGH = 0; + protected final static int CONDITIONAL_RETURN = 1; + protected final static int UNCONDITIONAL_JUMP = 2; + protected final static int CONDITIONAL_JUMP = 3; + protected final static int UNCONDITIONAL_CALL = 4; + protected final static int CONDITIONAL_CALL = 5; + protected final static int TERMINATOR = 6; + protected final static int COMPUTED = 7; + protected final static int INDIRECTION = 8; + protected final static int ENTRY = 9; // from Entry Nexus + + protected final static String[] edgeNames = + { "1", "2", "3", "4", "5", "6", "7", "13", "14", "15" }; + + // @formatter:off + protected final static String[] edgeTypes = { + "Fall-Through", + "Conditional-Return", + "Unconditional-Jump", + "Conditional-Jump", + "Unconditional-Call", + "Conditional-Call", + "Terminator", + "Computed", + "Indirection", + "Entry" + }; + // @formatter:on + + private final static String ENTRY_NODE = "Entry"; + // "1"; // beginning of a block, someone calls it + private final static String BODY_NODE = "Body"; + // "2"; // Body block, no flow + private final static String EXIT_NODE = "Exit"; + // "3"; // Terminator + private final static String SWITCH_NODE = "Switch"; + // "4"; // Switch/computed jump + private final static String BAD_NODE = "Bad"; + // "5"; // Bad destination + private final static String DATA_NODE = "Data"; + // "6"; // Data Node, used for indirection + private final static String ENTRY_NEXUS = "Entry-Nexus"; + // "7"; // + private final static String EXTERNAL_NODE = "External"; + // "8"; // node is external to program + + private final static String ENTRY_NEXUS_NAME = "Entry Points"; + private CodeBlockModel blockModel; + private AddressSetView selection; + private GraphDisplayProvider graphService; + private boolean reuseGraph; + private boolean appendGraph; + private PluginTool tool; + private String actionName; + private Program program; + + + public BlockGraphTask(String actionName, boolean graphEntryPointNexus, boolean showCode, + boolean reuseGraph, + boolean appendGraph, PluginTool tool, ProgramSelection selection, + CodeBlockModel blockModel, GraphDisplayProvider graphService) { + super("Graph Program", true, false, true); + this.actionName = actionName; + + this.graphEntryPointNexus = graphEntryPointNexus; + this.showCode = showCode; + this.reuseGraph = reuseGraph; + this.appendGraph = appendGraph; + this.tool = tool; + this.blockModel = blockModel; + this.graphService = graphService; + this.colorizingService = tool.getService(ColorizingService.class); + this.selection = selection; + this.program = blockModel.getProgram(); + } + + /** + * Runs the move memory operation. + */ + @Override + public void run(TaskMonitor monitor) throws CancelledException { + AttributedGraph graph = createGraph(); + monitor.setMessage("Generating Graph..."); + try { + GraphDisplay display = graphService.getGraphDisplay(reuseGraph, monitor); + display.setGraphDisplayListener( + new BlockModelGraphDisplayListener(tool, blockModel, display)); + if (showCode) { + display.defineVertexAttribute(CODE_ATTRIBUTE); + display.defineVertexAttribute(SYMBOLS_ATTRIBUTE); + display.setVertexLabel(CODE_ATTRIBUTE, GraphDisplay.ALIGN_LEFT, 12, true, + codeLimitPerBlock + 1); + } + display.setGraph(graph, actionName, appendGraph, monitor); + } + catch (GraphException e) { + if (!monitor.isCancelled()) { + Msg.showError(this, null, "Graphing Error", e.getMessage()); + } + } + } + + /** + * Set the maximum number of code lines which will be used per block when + * showCode is enabled. + * @param maxLines maximum number of code lines + */ + public void setCodeLimitPerBlock(int maxLines) { + codeLimitPerBlock = maxLines; + } + + protected AttributedGraph createGraph() throws CancelledException { + int blockCount = 0; + AttributedGraph graph = new AttributedGraph(); + + CodeBlockIterator it = getBlockIterator(); + List entryPoints = new ArrayList<>(); + + while (it.hasNext()) { + CodeBlock curBB = it.next(); + Address start = graphBlock(graph, curBB, entryPoints); + + if (start != null && (++blockCount % 50) == 0) { + taskMonitor.setMessage("Process Block: " + start.toString()); + } + } + + // if option is set and there is more than one entry point vertex, create fake entry node + // and connect to each entry point vertex + if (graphEntryPointNexus && entryPoints.size() > 1) { + addEntryEdges(graph, entryPoints); + } + + return graph; + } + + + private CodeBlockIterator getBlockIterator() throws CancelledException { + if (selection == null || selection.isEmpty()) { + return blockModel.getCodeBlocks(taskMonitor); + } + return blockModel.getCodeBlocksContaining(selection, taskMonitor); + } + + private Address graphBlock(AttributedGraph graph, CodeBlock curBB, List entries) + throws CancelledException { + + Address[] startAddrs = curBB.getStartAddresses(); + + if (startAddrs == null || startAddrs.length == 0) { + Msg.error(this, "Block not graphed, missing start address: " + curBB.getMinAddress()); + return null; + } + + AttributedVertex vertex = graphBasicBlock(graph, curBB); + + if (graphEntryPointNexus && hasExternalEntryPoint(startAddrs)) { + entries.add(vertex); + } + return startAddrs[0]; + } + + private boolean hasExternalEntryPoint(Address[] startAddrs) { + SymbolTable symbolTable = program.getSymbolTable(); + for (Address address : startAddrs) { + if (symbolTable.isExternalEntryPoint(address)) { + return true; + } + } + return false; + } + + private void addEntryEdges(AttributedGraph graph, List entries) { + AttributedVertex entryNexusVertex = getEntryNexusVertex(graph); + for (AttributedVertex vertex : entries) { + AttributedEdge edge = graph.addEdge(entryNexusVertex, vertex); + edge.setAttribute("Name", edgeNames[ENTRY]); + edge.setAttribute("EdgeType", edgeTypes[ENTRY]); + } + } + + + protected AttributedVertex graphBasicBlock(AttributedGraph graph, CodeBlock curBB) + throws CancelledException { + + AttributedVertex fromVertex = getBasicBlockVertex(graph, curBB); + + // for each destination block + // create a vertex if it doesn't exit and add an edge to the destination vertex + CodeBlockReferenceIterator refIter = curBB.getDestinations(taskMonitor); + while (refIter.hasNext()) { + CodeBlockReference cbRef = refIter.next(); + + CodeBlock db = cbRef.getDestinationBlock(); + + // must be a reference to a data block + if (db == null) { + continue; + } + + // don't include destination if it does not overlap selection + // always include if selection is empty + if (selection != null && !selection.isEmpty() && !selection.intersects(db)) { + continue; + } + + AttributedVertex toVertex = getBasicBlockVertex(graph, db); + if (toVertex == null) { + continue; + } + + // put the edge in the graph + String edgeAddr = cbRef.getReferent().toString(); + AttributedEdge newEdge = graph.addEdge(fromVertex, toVertex); + + // set it's attributes (really its name) + setEdgeAttributes(newEdge, cbRef); + setEdgeColor(newEdge, fromVertex, toVertex); + + } + return fromVertex; + } + + private void setEdgeColor(AttributedEdge edge, AttributedVertex fromVertex, AttributedVertex toVertex) { + // color the edge: first on the 'from' vertex, then try to 'to' vertex + String fromColor = fromVertex.getAttribute("Color"); + String toColor = toVertex.getAttribute("Color"); + if (fromColor != null || toColor != null) { + if (fromColor != null) { + edge.setAttribute("Color", fromColor); + } + else if (toColor != null) { + edge.setAttribute("Color", toColor); + } + } + + } + + private String getVertexId(CodeBlock bb) { + // vertex has attributes of Name = Label + // Address = address of blocks start + // VertexType = flow type of vertex + Address addr = bb.getFirstStartAddress(); + if (addr.isExternalAddress()) { + Symbol s = bb.getModel().getProgram().getSymbolTable().getPrimarySymbol(addr); + return s.getName(true); + } + return addr.toString(); + } + + protected AttributedVertex getBasicBlockVertex(AttributedGraph graph, CodeBlock bb) + throws CancelledException { + + String vertexId = getVertexId(bb); + AttributedVertex vertex = graph.getVertex(vertexId); + + if (vertex != null) { + return vertex; + } + + String vertexName = bb.getName(); + vertex = graph.addVertex(vertexId, vertexName); + + // add attributes for this vertex - + setVertexAttributes(vertex, bb, vertexName.equals(vertexId) ? false : isEntryNode(bb)); + + if (showCode) { + addSymbolAttribute(vertex, bb); + addCodeAttribute(vertex, bb); + } + + return vertex; + } + + private void addCodeAttribute(AttributedVertex vertex, CodeBlock bb) { + if (!bb.getMinAddress().isMemoryAddress()) { + vertex.setAttribute(CODE_ATTRIBUTE, vertex.getAttribute(SYMBOLS_ATTRIBUTE)); + } + + Listing listing = program.getListing(); + CodeUnitIterator cuIter = listing.getCodeUnits(bb, true); + int cnt = 0; + int maxMnemonicFieldLen = 0; + StringBuffer buf = new StringBuffer(); + while (cuIter.hasNext()) { + CodeUnit cu = cuIter.next(); + if (cnt != 0) { + buf.append('\n'); + } + String line = cu.toString(); + int ix = line.indexOf(' '); + if (ix > maxMnemonicFieldLen) { + maxMnemonicFieldLen = ix; + } + buf.append(line); + if (++cnt == codeLimitPerBlock) { + buf.append("\n..."); + break; + } + } + vertex.setAttribute(CODE_ATTRIBUTE, adjustCode(buf, maxMnemonicFieldLen + 1)); + } + + private void addSymbolAttribute(AttributedVertex vertex, CodeBlock bb) { + Symbol[] symbols = program.getSymbolTable().getSymbols(bb.getMinAddress()); + if (symbols.length != 0) { + StringBuffer buf = new StringBuffer(); + for (int i = 0; i < symbols.length; i++) { + if (i != 0) { + buf.append('\n'); + } + buf.append(symbols[i].getName()); + } + vertex.setAttribute(SYMBOLS_ATTRIBUTE, buf.toString()); + } + + } + + private String adjustCode(StringBuffer buf, int mnemonicFieldLen) { + if (mnemonicFieldLen <= 1) { + return buf.toString(); + } + int ix = 0; + char[] pad = new char[mnemonicFieldLen]; + Arrays.fill(pad, ' '); + while (ix < buf.length()) { + int eolIx = buf.indexOf("\n", ix); + if (eolIx < 0) { + eolIx = buf.length(); + } + int padIx = buf.indexOf(" ", ix); + if (padIx > 0 && padIx < eolIx) { + int padSize = mnemonicFieldLen - padIx + ix; + if (padSize > 0) { + buf.insert(padIx, pad, 0, padSize); + eolIx += padSize; + } + } + ix = eolIx + 1; + } + return buf.toString(); + } + + /** + * Determine if the specified block is an entry node. + * @param block the basic block to test + * @return true if the specified block is an entry node. + * @throws CancelledException if the operation is cancelled + */ + protected boolean isEntryNode(CodeBlock block) throws CancelledException { + CodeBlockReferenceIterator iter = block.getSources(taskMonitor); + boolean isSource = true; + while (iter.hasNext()) { + isSource = false; + if (iter.next().getFlowType().isCall()) { + return true; + } + } + return isSource; + } + + protected void setEdgeAttributes(AttributedEdge edge, CodeBlockReference ref) { + + int edgeType; + FlowType flowType = ref.getFlowType(); + if (flowType == RefType.FALL_THROUGH) { + edgeType = FALLTHROUGH; + } + else if (flowType == RefType.UNCONDITIONAL_JUMP) { + edgeType = UNCONDITIONAL_JUMP; + } + else if (flowType == RefType.CONDITIONAL_JUMP) { + edgeType = CONDITIONAL_JUMP; + } + else if (flowType == RefType.UNCONDITIONAL_CALL) { + edgeType = UNCONDITIONAL_CALL; + } + else if (flowType == RefType.CONDITIONAL_CALL) { + edgeType = CONDITIONAL_CALL; + } + else if (flowType.isComputed()) { + edgeType = COMPUTED; + } + else if (flowType.isIndirect()) { + edgeType = INDIRECTION; + } + else if (flowType == RefType.TERMINATOR) { + edgeType = TERMINATOR; + } + else { // only FlowType.CONDITIONAL_TERMINATOR remains unchecked + edgeType = CONDITIONAL_RETURN; + } + // set attributes on this edge + edge.setAttribute("Name", edgeNames[edgeType]); + edge.setAttribute("EdgeType", edgeTypes[edgeType]); + } + + protected void setVertexAttributes(AttributedVertex vertex, CodeBlock bb, boolean isEntry) { + + String vertexType = BODY_NODE; + + Address firstStartAddress = bb.getFirstStartAddress(); + if (firstStartAddress.isExternalAddress()) { + vertexType = EXTERNAL_NODE; + } + else if (isEntry) { + vertexType = ENTRY_NODE; + } + else { + FlowType flowType = bb.getFlowType(); + if (flowType.isTerminal()) { + vertexType = EXIT_NODE; + } + else if (flowType.isComputed()) { + vertexType = SWITCH_NODE; + } + else if (flowType == RefType.INDIRECTION) { + vertexType = DATA_NODE; + } + else if (flowType == RefType.INVALID) { + vertexType = BAD_NODE; + } + } + + vertex.setAttribute("VertexType", vertexType); + + setVertexColor(vertex, vertexType, firstStartAddress); + } + + private void setVertexColor(AttributedVertex vertex, String vertexType, Address address) { + + if (colorizingService == null) { + return; + } + + Color color = colorizingService.getBackgroundColor(address); + if (color == null) { + return; + } + + // color format: RGBrrrgggbbb + // -where rrr/ggg/bbb is a three digit int value for each respective color range + String rgb = "RGB" + HTMLUtilities.toRGBString(color); + vertex.setAttribute("Color", rgb); // sets the vertex color + + // This value triggers the vertex to be painted with its color and not a + // while background. + if (showCode) { + // our own custom override of Labels/Icons + vertex.setAttribute("VertexType", "ColorFilled"); + } + else { + // the default preferences for VertexType + vertex.setAttribute("VertexType", vertexType + ".Filled"); + } + } + + private AttributedVertex getEntryNexusVertex(AttributedGraph graph) { + AttributedVertex vertex = graph.getVertex(ENTRY_NEXUS_NAME); + if (vertex == null) { + vertex = graph.addVertex(ENTRY_NEXUS_NAME, ENTRY_NEXUS_NAME); + vertex.setAttribute("VertexType", ENTRY_NEXUS); + } + return vertex; + } +} diff --git a/Ghidra/Features/ProgramGraph/src/main/java/ghidra/graph/program/BlockModelGraphDisplayListener.java b/Ghidra/Features/ProgramGraph/src/main/java/ghidra/graph/program/BlockModelGraphDisplayListener.java new file mode 100644 index 0000000000..b5781a76f2 --- /dev/null +++ b/Ghidra/Features/ProgramGraph/src/main/java/ghidra/graph/program/BlockModelGraphDisplayListener.java @@ -0,0 +1,140 @@ +/* ### + * 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.program; + +import java.util.*; + +import ghidra.app.plugin.core.graph.AddressBasedGraphDisplayListener; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.*; +import ghidra.program.model.block.*; +import ghidra.program.model.symbol.Symbol; +import ghidra.program.model.symbol.SymbolTable; +import ghidra.service.graph.GraphDisplay; +import ghidra.service.graph.GraphDisplayListener; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * {@link GraphDisplayListener} that handle events back and from from program graphs. + */ +public class BlockModelGraphDisplayListener extends AddressBasedGraphDisplayListener { + + private CodeBlockModel blockModel; + + public BlockModelGraphDisplayListener(PluginTool tool, CodeBlockModel blockModel, + GraphDisplay display) { + super(tool, blockModel.getProgram(), display); + this.blockModel = blockModel; + } + + @Override + protected String getVertexIdForAddress(Address address) { + try { + CodeBlock[] blocks = blockModel.getCodeBlocksContaining(address, TaskMonitor.DUMMY); + if (blocks != null && blocks.length > 0) { + return super.getVertexIdForAddress(blocks[0].getFirstStartAddress()); + } + } + catch (CancelledException e) { + // Will not happen with dummyMonitor + // Model has already done the work when the graph was created + } + return super.getVertexIdForAddress(address); + } + + @Override + protected List getVertices(AddressSetView addrSet) { + if (addrSet.isEmpty()) { + return Collections.emptyList(); + } + + // Identify all blocks which have an entry point within the selection address set + ArrayList blockList = new ArrayList(); + try { + SymbolTable symTable = program.getSymbolTable(); + CodeBlockIterator cbIter = + blockModel.getCodeBlocksContaining(addrSet, TaskMonitor.DUMMY); + while (cbIter.hasNext()) { + CodeBlock block = cbIter.next(); + String addrString; + Address addr = block.getFirstStartAddress(); + if (addr.isExternalAddress()) { + Symbol s = symTable.getPrimarySymbol(addr); + addrString = s.getName(true); + } + else { + addrString = addr.toString(); + } + blockList.add(addrString); + } + } + catch (CancelledException e) { + // Will not happen with dummyMonitor + // Model has already done the work when the graph was created + } + + return blockList; + } + + @Override + protected AddressSet getAddressSetForVertices(List vertexIds) { + AddressSet addrSet = new AddressSet(); + + try { + // for each address string, translate it into a block + // and add it to the address set. + for (String vertexId : vertexIds) { + Address blockAddr = getAddressForVertexId(vertexId); + if (!isValidAddress(blockAddr)) { + continue; + } + CodeBlock blocks[] = null; + if (blockModel != null) { + CodeBlock block = blockModel.getCodeBlockAt(blockAddr, TaskMonitor.DUMMY); + if (block != null) { + blocks = new CodeBlock[1]; + blocks[0] = block; + } + else { + blocks = blockModel.getCodeBlocksContaining(blockAddr, TaskMonitor.DUMMY); + } + } + if (blocks != null && blocks.length > 0) { + for (CodeBlock block : blocks) { + addrSet.add(block); + } + } + else { + addrSet.addRange(blockAddr, blockAddr); + } + } + } + catch (CancelledException e) { + // Will not happen with dummyMonitor + // Model has already done the work when the graph was created + } + return addrSet; + } + + protected boolean isValidAddress(Address addr) { + if (addr == null || program == null) { + return false; + } + return program.getMemory().contains(addr) || addr.isExternalAddress(); + } + +} diff --git a/Ghidra/Features/ProgramGraph/src/main/java/ghidra/graph/program/ProgramGraphPlugin.java b/Ghidra/Features/ProgramGraph/src/main/java/ghidra/graph/program/ProgramGraphPlugin.java new file mode 100644 index 0000000000..d23b2f14e3 --- /dev/null +++ b/Ghidra/Features/ProgramGraph/src/main/java/ghidra/graph/program/ProgramGraphPlugin.java @@ -0,0 +1,334 @@ +/* ### + * 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.program; + +import java.util.ArrayList; +import java.util.List; + +import docking.ActionContext; +import docking.action.DockingAction; +import docking.action.ToggleDockingAction; +import docking.action.builder.ActionBuilder; +import docking.action.builder.ToggleActionBuilder; +import ghidra.app.CorePluginPackage; +import ghidra.app.events.ProgramLocationPluginEvent; +import ghidra.app.events.ProgramSelectionPluginEvent; +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.app.plugin.ProgramPlugin; +import ghidra.app.plugin.core.graph.GraphDisplayBrokerListener; +import ghidra.app.services.*; +import ghidra.framework.options.*; +import ghidra.framework.plugintool.PluginInfo; +import ghidra.framework.plugintool.PluginTool; +import ghidra.framework.plugintool.util.PluginStatus; +import ghidra.program.model.block.CodeBlockModel; +import ghidra.service.graph.GraphDisplayProvider; +import ghidra.util.HelpLocation; +import ghidra.util.Msg; +import ghidra.util.exception.NotFoundException; +import ghidra.util.task.TaskLauncher; + +/** + * Plugin for generating program graphs. It uses the GraphServiceBroker to consume/display + * the graphs that it generates. This plugin generates several different types of program graphs. + * Both the "Block flow" and "code flow" actions generate graph of basic block flows. The only + * difference is that the "code flow" action generates a graph that + * displays the assembly for for each basic block, whereas the "block flow" action generates a graph + * that displays the symbol or address at the start of the basic block. This plugin also + * generates call graphs, using either the default subroutine model or one that the user chooses. + */ + +//@formatter:off +@PluginInfo( + status = PluginStatus.RELEASED, + packageName = CorePluginPackage.NAME, + category = PluginCategoryNames.GRAPH, + shortDescription = "Program graph generator", + description = "This plugin provides actions for creating and managing program graphs" + + " (block graphs and call graphs)." + + "Once a graph is created, it uses the currenly selected graph output to display " + + "or export the graph. The plugin " + + "also provides event handling to facilitate interaction between " + + "the graph and the tool.", + servicesRequired = { GoToService.class, BlockModelService.class, GraphDisplayBroker.class }, + eventsProduced = { ProgramLocationPluginEvent.class, ProgramSelectionPluginEvent.class } +) +//@formatter:on +public class ProgramGraphPlugin extends ProgramPlugin + implements OptionsChangeListener, BlockModelServiceListener, GraphDisplayBrokerListener { + private static final String MAX_CODE_LINES_DISPLAYED = "Max Code Lines Displayed"; + private static final String REUSE_GRAPH = "Reuse Graph"; + private static final String GRAPH_ENTRY_POINT_NEXUS = "Graph Entry Point Nexus"; + private static final String FORCE_LOCATION_DISPLAY_OPTION = "Force Location Visible on Graph"; + public static final String MENU_GRAPH = "&Graph"; + + private BlockModelService blockModelService; + + private List subUsingGraphActions = new ArrayList<>(); + private ToggleDockingAction reuseGraphAction; + private ToggleDockingAction appendGraphAction; + + private boolean reuseGraph = false; + private boolean appendToGraph = false; + + private boolean graphEntryPointNexus = false; + private int codeLimitPerBlock = 10; + + private ToggleDockingAction forceLocationVisibleAction; + + private GraphDisplayBroker broker; + + private GraphDisplayProvider defaultGraphService; + + public ProgramGraphPlugin(PluginTool tool) { + super(tool, true, true); + intializeOptions(); + } + + private void intializeOptions() { + HelpLocation help = new HelpLocation(getName(), "Graph_Option"); + ToolOptions options = tool.getOptions("Graph"); + + options.registerOption(MAX_CODE_LINES_DISPLAYED, codeLimitPerBlock, help, + "Specifies the maximum number of instructions to display in each graph " + + "node in a Code Flow Graph."); + + options.registerOption(REUSE_GRAPH, false, help, + "Determines whether the graph will reuse the active graph window when displaying graphs."); + + options.registerOption(GRAPH_ENTRY_POINT_NEXUS, false, help, + "Add a dummy node at the root of the graph and adds dummy edges to each node that has " + + "no incoming edges."); + + options.registerOption(FORCE_LOCATION_DISPLAY_OPTION, false, help, + "Specifies whether or not " + + "graph displays should force the visible graph to pan and/or scale to ensure that focused " + + "locations are visible."); + + setOptions(options); + options.addOptionsChangeListener(this); + options.setOptionsHelpLocation(new HelpLocation(getName(), "Graph_Option")); + + } + + @Override + protected void init() { + broker = tool.getService(GraphDisplayBroker.class); + broker.addGraphDisplayBrokerListener(this); + defaultGraphService = broker.getDefaultGraphDisplayProvider(); + + blockModelService = tool.getService(BlockModelService.class); + blockModelService.addListener(this); + + createActions(); + } + + @Override + public void dispose() { + super.dispose(); + if (blockModelService != null) { + blockModelService.removeListener(this); + blockModelService = null; + } + } + + /** + * Notification that an option changed. + * + * @param options + * options object containing the property that changed + * @param optionName + * name of option that changed + * @param oldValue + * old value of the option + * @param newValue + * new value of the option + */ + @Override + public void optionsChanged(ToolOptions options, String optionName, Object oldValue, + Object newValue) { + setOptions(options); + } + + private void setOptions(Options options) { + codeLimitPerBlock = options.getInt(MAX_CODE_LINES_DISPLAYED, codeLimitPerBlock); + graphEntryPointNexus = options.getBoolean(GRAPH_ENTRY_POINT_NEXUS, false); + reuseGraph = options.getBoolean(REUSE_GRAPH, false); + if (reuseGraphAction != null) { + reuseGraphAction.setSelected(reuseGraph); + } + // Note: we don't care about the FORCE_LOCATION_DISPLAY_OPTION. We register it, but its + // the actually the various GraphDisplays the make use of it. + } + + private void createActions() { + + new ActionBuilder("Graph Block Flow", getName()) + .menuPath(MENU_GRAPH, "&Block Flow") + .menuGroup("Graph", "A") + .onAction(c -> graphBlockFlow()) + .enabledWhen(this::canGraph) + .buildAndInstall(tool); + + new ActionBuilder("Graph Code Flow", getName()) + .menuPath(MENU_GRAPH, "C&ode Flow") + .menuGroup("Graph", "B") + .onAction(c -> graphCodeFlow()) + .enabledWhen(this::canGraph) + .buildAndInstall(tool); + + new ActionBuilder("Graph Calls Using Default Model", getName()) + .menuPath(MENU_GRAPH, "&Calls") + .menuGroup("Graph", "C") + .onAction(c -> graphSubroutines()) + .enabledWhen(this::canGraph) + .buildAndInstall(tool); + + reuseGraphAction = new ToggleActionBuilder("Reuse Graph", getName()) + .menuPath(MENU_GRAPH, "Reuse Graph") + .menuGroup("Graph Options") + .selected(reuseGraph) + .onAction(c -> reuseGraph = reuseGraphAction.isSelected()) + .enabledWhen(this::canGraph) + .buildAndInstall(tool); + + appendGraphAction = new ToggleActionBuilder("Append Graph", getName()) + .menuPath(MENU_GRAPH, "Append Graph") + .menuGroup("Graph Options") + .selected(false) + .onAction(c -> updateAppendAndReuseGraph()) + .enabledWhen(this::canGraph) + .buildAndInstall(tool); + + forceLocationVisibleAction = new ToggleActionBuilder("Show Location in Graph", getName()) + .menuPath(MENU_GRAPH, "Show Location") + .description("Tell the graph to pan/scale as need to keep location changes visible") + .menuGroup("Graph Options") + .onAction(c -> toggleForceLocationVisible()) + .enabledWhen(this::canGraph) + .buildAndInstall(tool); + + updateSubroutineActions(); + } + + private boolean canGraph(ActionContext context) { + return currentProgram != null && defaultGraphService != null; + } + + private void toggleForceLocationVisible() { + ToolOptions options = tool.getOptions("Graph"); + options.setBoolean(FORCE_LOCATION_DISPLAY_OPTION, forceLocationVisibleAction.isSelected()); + } + + private void updateAppendAndReuseGraph() { + appendToGraph = appendGraphAction.isSelected(); + if (appendToGraph && !reuseGraph) { + reuseGraph = true; + reuseGraphAction.setSelected(true); + } + } + + private void updateSubroutineActions() { + + // Remove old actions + for (DockingAction action : subUsingGraphActions) { + tool.removeAction(action); + } + + // Create subroutine graph actions for each subroutine provided by BlockModelService + + String[] subModels = + blockModelService.getAvailableModelNames(BlockModelService.SUBROUTINE_MODEL); + + if (subModels.length <= 1) { // Not needed if only one subroutine model + return; + } + + HelpLocation helpLoc = new HelpLocation(getName(), "Graph_Calls_Using_Model"); + for (String blockModelName : subModels) { + DockingAction action = buildGraphActionWithModel(blockModelName, helpLoc); + subUsingGraphActions.add(action); + } + + tool.setMenuGroup(new String[] { "Graph", "Calls Using Model" }, "Graph"); + } + + private DockingAction buildGraphActionWithModel(String blockModelName, HelpLocation helpLoc) { + return new ActionBuilder("Graph Calls using " + blockModelName, getName()) + .menuPath("Graph", "Calls Using Model", blockModelName) + .menuGroup("Graph") + .helpLocation(helpLoc) + .onAction(c -> graphSubroutinesUsing(blockModelName)) + .enabledWhen(this::canGraph) + .buildAndInstall(tool); + } + + private void graphBlockFlow() { + graph("Flow Graph", blockModelService.getActiveBlockModelName(), false); + } + + private void graphCodeFlow() { + graph("Code Graph", blockModelService.getActiveBlockModelName(), true); + } + + private void graphSubroutines() { + graph("Call Graph", blockModelService.getActiveSubroutineModelName(), false); + } + + private void graphSubroutinesUsing(String modelName) { + graph("Call Graph", modelName, false); + } + + private void graph(String actionName, String modelName, boolean showCode) { + try { + CodeBlockModel model = + blockModelService.getNewModelByName(modelName, currentProgram, true); + BlockGraphTask task = + new BlockGraphTask(actionName, graphEntryPointNexus, showCode, reuseGraph, + appendToGraph, tool, currentSelection, model, defaultGraphService); + task.setCodeLimitPerBlock(codeLimitPerBlock); + new TaskLauncher(task, tool.getToolFrame()); + } + catch (NotFoundException e) { + Msg.showError(this, null, "Error That Can't Happen", + "Can't find a block model from a name that we got from the existing block models!"); + } + } + + String getProgramName() { + return currentProgram != null ? currentProgram.getName() : null; + } + + @Override + public void modelAdded(String modeName, int modelType) { + if (modelType == BlockModelService.SUBROUTINE_MODEL) { + updateSubroutineActions(); + } + } + + @Override + public void modelRemoved(String modeName, int modelType) { + if (modelType == BlockModelService.SUBROUTINE_MODEL) { + updateSubroutineActions(); + } + } + + @Override + public void providersChanged() { + defaultGraphService = broker.getDefaultGraphDisplayProvider(); + } + +} diff --git a/Ghidra/Features/ProgramGraph/src/test/java/ghidra/graph/program/AbstractBlockGraphTest.java b/Ghidra/Features/ProgramGraph/src/test/java/ghidra/graph/program/AbstractBlockGraphTest.java new file mode 100644 index 0000000000..e54a6467bc --- /dev/null +++ b/Ghidra/Features/ProgramGraph/src/test/java/ghidra/graph/program/AbstractBlockGraphTest.java @@ -0,0 +1,131 @@ +/* ### + * 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.program; + +import org.junit.After; +import org.junit.Before; + +import ghidra.app.plugin.core.blockmodel.BlockModelServicePlugin; +import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin; +import ghidra.app.services.BlockModelService; +import ghidra.app.services.ProgramManager; +import ghidra.framework.plugintool.PluginTool; +import ghidra.framework.plugintool.util.PluginException; +import ghidra.program.database.ProgramDB; +import ghidra.program.model.address.Address; +import ghidra.program.model.mem.MemoryAccessException; +import ghidra.test.*; + +public class AbstractBlockGraphTest extends AbstractGhidraHeadedIntegrationTest { + protected PluginTool tool; + protected ProgramDB program; + protected TestEnv env; + protected BlockModelService blockModelService; + private ToyProgramBuilder builder; + protected CodeBrowserPlugin codeBrowser; + + protected Address addr(long addr) { + return builder.getAddress(addr); + } + + @Before + public void setUp() throws Exception { + + setErrorGUIEnabled(false); + + env = new TestEnv(); + tool = env.getTool(); + + initializeTool(); + + } + + @After + public void tearDown() { + env.dispose(); + } + + protected void initializeTool() throws Exception { + installPlugins(); + + openProgram(); + ProgramManager pm = tool.getService(ProgramManager.class); + pm.openProgram(program.getDomainFile()); + + showTool(tool); + blockModelService = tool.getService(BlockModelService.class); + } + + protected void installPlugins() throws PluginException { + tool.addPlugin(CodeBrowserPlugin.class.getName()); + tool.addPlugin(BlockModelServicePlugin.class.getName()); + codeBrowser = env.getPlugin(CodeBrowserPlugin.class); + } + + protected void openProgram() throws Exception { + + builder = new ToyProgramBuilder("sample", true); + builder.createMemory("caller", "0x01002200", 8); + builder.createMemory("simple", "0x01002239", 8); + + buildCallerFunction(builder); + buildSimpleFunction(builder); + + program = builder.getProgram(); + } + + private void buildCallerFunction(ToyProgramBuilder builder) throws MemoryAccessException { + // just a function that calls another + builder.addBytesNOP("0x01002200", 1); + builder.addBytesCall("0x01002201", "0x01002239");// jump to C + builder.addBytesReturn("0x01002203"); + + builder.disassemble("0x01002200", 4, true); + builder.createFunction("0x01002200"); + builder.createLabel("0x01002200", "entry");// function label + } + + private void buildSimpleFunction(ToyProgramBuilder builder) throws MemoryAccessException { + // just a function to render in the graph so that we can clear out settings/cache + // 01002239 + + /* + + A + |->B + C + + + */ + + // A + builder.addBytesNOP("0x01002239", 1); + builder.addBytesBranchConditional("0x0100223a", "0x0100223e");// jump to C + + // B + builder.addBytesNOP("0x0100223c", 1); + builder.addBytesNOP("0x0100223d", 1);// fallthrough to C + + // C + builder.addBytesNOP("0x0100223e", 1); + builder.addBytesReturn("0x0100223f"); + + builder.disassemble("0x01002239", 8, true); + builder.createFunction("0x01002239"); + builder.createLabel("0x01002239", "simple");// function label + } + +} diff --git a/Ghidra/Features/ProgramGraph/src/test/java/ghidra/graph/program/BlockGraphEventTest.java b/Ghidra/Features/ProgramGraph/src/test/java/ghidra/graph/program/BlockGraphEventTest.java new file mode 100644 index 0000000000..c078f68ec5 --- /dev/null +++ b/Ghidra/Features/ProgramGraph/src/test/java/ghidra/graph/program/BlockGraphEventTest.java @@ -0,0 +1,118 @@ +/* ### + * 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.program; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; + +import ghidra.app.events.ProgramSelectionPluginEvent; +import ghidra.program.model.address.AddressSet; +import ghidra.program.model.block.CodeBlockModel; +import ghidra.program.util.ProgramLocation; +import ghidra.program.util.ProgramSelection; +import ghidra.service.graph.AttributedGraph; +import ghidra.util.task.TaskMonitor; + +public class BlockGraphEventTest extends AbstractBlockGraphTest { + + private TestGraphDisplay display; + private AttributedGraph graph; + + @Override + public void setUp() throws Exception { + super.setUp(); + String modelName = blockModelService.getActiveBlockModelName(); + CodeBlockModel model = + blockModelService.getNewModelByName(modelName, program, true); + TestGraphService graphService = new TestGraphService(); + BlockGraphTask task = + new BlockGraphTask("test", false, false, false, false, + tool, null, model, graphService); + + task.monitoredRun(TaskMonitor.DUMMY); + + display = (TestGraphDisplay) graphService.getGraphDisplay(true, TaskMonitor.DUMMY); + graph = display.getGraph(); + } + + @Test + public void testGhidraLocationChanged() { + codeBrowser.goTo(new ProgramLocation(program, addr(0x1002239))); + assertEquals("01002239", display.getFocusedVertex()); + codeBrowser.goTo(new ProgramLocation(program, addr(0x1002200))); + assertEquals("01002200", display.getFocusedVertex()); + + // also try a location that is not the start of a block + codeBrowser.goTo(new ProgramLocation(program, addr(0x100223a))); + assertEquals("01002239", display.getFocusedVertex()); + } + + + private AddressSet addrSet(long start, long end) { + return new AddressSet(addr(start), addr(end)); + } + + @Test + public void testGhidraSelectionChanged() { + setSelection(addrSet(0x1002239, 0x1002241)); + Set selected = new HashSet<>(display.getSelectedVertices()); + assertEquals(3, selected.size()); + assertTrue(selected.contains("01002239")); + assertTrue(selected.contains("0100223c")); + assertTrue(selected.contains("0100223e")); + + setSelection(new AddressSet(addr(0x1002200), addr(0x1002210))); + selected = new HashSet<>(display.getSelectedVertices()); + assertEquals(2, selected.size()); + assertTrue(selected.contains("01002200")); + assertTrue(selected.contains("01002203")); + + } + + @Test + public void testGraphNodeFocused() { + display.focusChanged("01002203"); + assertEquals(addr(0x01002203), codeBrowser.getCurrentLocation().getAddress()); + + display.focusChanged("0100223c"); + assertEquals(addr(0x0100223c), codeBrowser.getCurrentLocation().getAddress()); + + } + + @Test + public void testGraphNodesSelected() { + display.selectionChanged(Arrays.asList("01002239", "0100223c")); + ProgramSelection selection = codeBrowser.getCurrentSelection(); + assertEquals(addr(0x01002239), selection.getMinAddress()); + assertEquals(addr(0x0100223d), selection.getMaxAddress()); + + display.selectionChanged(Arrays.asList("01002200", "01002203")); + selection = codeBrowser.getCurrentSelection(); + assertEquals(addr(0x01002200), selection.getMinAddress()); + assertEquals(addr(0x01002204), selection.getMaxAddress()); + + } + + private void setSelection(final AddressSet addrSet) { + runSwing( + () -> tool.firePluginEvent( + new ProgramSelectionPluginEvent("test", new ProgramSelection(addrSet), program)), + true); + } +} diff --git a/Ghidra/Features/ProgramGraph/src/test/java/ghidra/graph/program/BlockGraphTaskTest.java b/Ghidra/Features/ProgramGraph/src/test/java/ghidra/graph/program/BlockGraphTaskTest.java new file mode 100644 index 0000000000..0c92cf0029 --- /dev/null +++ b/Ghidra/Features/ProgramGraph/src/test/java/ghidra/graph/program/BlockGraphTaskTest.java @@ -0,0 +1,234 @@ +/* ### + * 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.program; + +import static org.junit.Assert.*; + +import java.util.Map; + +import org.junit.Test; + +import ghidra.program.model.block.CodeBlockModel; +import ghidra.program.util.ProgramSelection; +import ghidra.service.graph.*; +import ghidra.util.task.TaskMonitor; + +public class BlockGraphTaskTest extends AbstractBlockGraphTest { + private static final boolean SHOW_CODE = true; + private static final boolean DONT_SHOW_CODE = false; + + @Test + public void testBlockGraph() throws Exception { + String modelName = blockModelService.getActiveBlockModelName(); + CodeBlockModel model = + blockModelService.getNewModelByName(modelName, program, true); + TestGraphService graphService = new TestGraphService(); + BlockGraphTask task = + new BlockGraphTask("test", false, DONT_SHOW_CODE, false, false, + tool, null, model, graphService); + + task.monitoredRun(TaskMonitor.DUMMY); + + TestGraphDisplay display = + (TestGraphDisplay) graphService.getGraphDisplay(true, TaskMonitor.DUMMY); + + AttributedGraph graph = display.getGraph(); + + assertEquals(5, graph.getVertexCount()); + AttributedVertex v1 = graph.getVertex("01002200"); + AttributedVertex v2 = graph.getVertex("01002203"); + AttributedVertex v3 = graph.getVertex("01002239"); + AttributedVertex v4 = graph.getVertex("0100223c"); + AttributedVertex v5 = graph.getVertex("0100223e"); + + assertNotNull(v1); + assertNotNull(v2); + assertNotNull(v3); + assertNotNull(v4); + assertNotNull(v5); + + assertEquals(5, graph.getEdgeCount()); + AttributedEdge e1 = graph.getEdge(v1, v2); + AttributedEdge e2 = graph.getEdge(v1, v3); + AttributedEdge e3 = graph.getEdge(v3, v4); + AttributedEdge e4 = graph.getEdge(v4, v5); + AttributedEdge e5 = graph.getEdge(v3, v5); + assertNotNull(e1); + assertNotNull(e2); + assertNotNull(e3); + assertNotNull(e4); + assertNotNull(e5); + + Map map = v1.getAttributeMap(); + assertEquals(2, map.size()); + assertTrue(map.containsKey("Name")); + assertTrue(map.containsKey("VertexType")); + + assertEquals("Entry", v3.getAttribute("VertexType")); + assertEquals("Body", v4.getAttribute("VertexType")); + assertEquals("Exit", v5.getAttribute("VertexType")); + + map = e1.getAttributeMap(); + assertEquals(2, map.size()); + assertTrue(map.containsKey("Name")); + assertTrue(map.containsKey("EdgeType")); + + assertEquals("Fall-Through", e3.getAttribute("EdgeType")); + assertEquals("Fall-Through", e4.getAttribute("EdgeType")); + assertEquals("Conditional-Jump", e5.getAttribute("EdgeType")); + } + + @Test + public void testCodeBlockGraph() throws Exception { + String modelName = blockModelService.getActiveBlockModelName(); + CodeBlockModel model = + blockModelService.getNewModelByName(modelName, program, true); + TestGraphService graphService = new TestGraphService(); + BlockGraphTask task = + new BlockGraphTask("test", false, SHOW_CODE, false, false, + tool, null, model, graphService); + + task.monitoredRun(TaskMonitor.DUMMY); + + TestGraphDisplay display = + (TestGraphDisplay) graphService.getGraphDisplay(true, TaskMonitor.DUMMY); + + AttributedGraph graph = display.getGraph(); + + assertEquals(5, graph.getVertexCount()); + AttributedVertex v1 = graph.getVertex("01002200"); + AttributedVertex v2 = graph.getVertex("01002203"); + AttributedVertex v3 = graph.getVertex("01002239"); + AttributedVertex v4 = graph.getVertex("0100223c"); + AttributedVertex v5 = graph.getVertex("0100223e"); + + assertNotNull(v1); + assertNotNull(v2); + assertNotNull(v3); + assertNotNull(v4); + assertNotNull(v5); + + assertEquals(5, graph.getEdgeCount()); + AttributedEdge e1 = graph.getEdge(v1, v2); + AttributedEdge e2 = graph.getEdge(v1, v3); + AttributedEdge e3 = graph.getEdge(v3, v4); + AttributedEdge e4 = graph.getEdge(v4, v5); + AttributedEdge e5 = graph.getEdge(v3, v5); + assertNotNull(e1); + assertNotNull(e2); + assertNotNull(e3); + assertNotNull(e4); + assertNotNull(e5); + + Map map = v3.getAttributeMap(); + assertEquals(4, map.size()); + assertTrue(map.containsKey("Name")); + assertTrue(map.containsKey("VertexType")); + assertTrue(map.containsKey("Code")); + assertTrue(map.containsKey("Symbols")); + + assertEquals("simple", v3.getAttribute("Symbols")); + assertEquals("nop #0x1\nbreq 0x0100223e", v3.getAttribute("Code")); + } + + @Test + public void testCallGraph() throws Exception { + String modelName = blockModelService.getActiveSubroutineModelName(); + CodeBlockModel model = + blockModelService.getNewModelByName(modelName, program, true); + TestGraphService graphService = new TestGraphService(); + BlockGraphTask task = + new BlockGraphTask("test", false, false, false, false, + tool, null, model, graphService); + + task.monitoredRun(TaskMonitor.DUMMY); + + TestGraphDisplay display = + (TestGraphDisplay) graphService.getGraphDisplay(true, TaskMonitor.DUMMY); + + AttributedGraph graph = display.getGraph(); + + assertEquals(2, graph.getVertexCount()); + AttributedVertex v1 = graph.getVertex("01002200"); + AttributedVertex v2 = graph.getVertex("01002239"); + + assertNotNull(v1); + assertNotNull(v2); + + assertEquals(1, graph.getEdgeCount()); + AttributedEdge e1 = graph.getEdge(v1, v2); + assertNotNull(e1); + + Map map = v1.getAttributeMap(); + assertEquals(2, map.size()); + assertTrue(map.containsKey("Name")); + assertTrue(map.containsKey("VertexType")); + + assertEquals("Entry", v1.getAttribute("VertexType")); + assertEquals("Entry", v2.getAttribute("VertexType")); + + map = e1.getAttributeMap(); + + assertEquals(2, map.size()); + assertTrue(map.containsKey("Name")); + assertTrue(map.containsKey("EdgeType")); + + assertEquals("Unconditional-Call", e1.getAttribute("EdgeType")); + + } + + @Test + public void testBlockGraphWithSelection() throws Exception { + String modelName = blockModelService.getActiveBlockModelName(); + CodeBlockModel model = + blockModelService.getNewModelByName(modelName, program, true); + TestGraphService graphService = new TestGraphService(); + ProgramSelection sel = new ProgramSelection(addr(0x1002239), addr(0x1002247)); + BlockGraphTask task = + new BlockGraphTask("test", false, DONT_SHOW_CODE, false, false, + tool, sel, model, graphService); + + task.monitoredRun(TaskMonitor.DUMMY); + + TestGraphDisplay display = + (TestGraphDisplay) graphService.getGraphDisplay(true, TaskMonitor.DUMMY); + + AttributedGraph graph = display.getGraph(); + + assertEquals(3, graph.getVertexCount()); + AttributedVertex v1 = graph.getVertex("01002200"); + AttributedVertex v2 = graph.getVertex("01002203"); + AttributedVertex v3 = graph.getVertex("01002239"); + AttributedVertex v4 = graph.getVertex("0100223c"); + AttributedVertex v5 = graph.getVertex("0100223e"); + + assertNull(v1); + assertNull(v2); + assertNotNull(v3); + assertNotNull(v4); + assertNotNull(v5); + + assertEquals(3, graph.getEdgeCount()); + AttributedEdge e3 = graph.getEdge(v3, v4); + AttributedEdge e4 = graph.getEdge(v4, v5); + AttributedEdge e5 = graph.getEdge(v3, v5); + assertNotNull(e3); + assertNotNull(e4); + assertNotNull(e5); + + } + +} diff --git a/Ghidra/Features/ProgramGraph/src/test/java/ghidra/graph/program/TestGraphDisplay.java b/Ghidra/Features/ProgramGraph/src/test/java/ghidra/graph/program/TestGraphDisplay.java new file mode 100644 index 0000000000..8b9159f068 --- /dev/null +++ b/Ghidra/Features/ProgramGraph/src/test/java/ghidra/graph/program/TestGraphDisplay.java @@ -0,0 +1,112 @@ +/* ### + * 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.program; + +import java.util.*; + +import ghidra.service.graph.*; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +public class TestGraphDisplay implements GraphDisplay { + private Set definedVertexAttributes = new HashSet<>(); + private Set definedEdgeAttributes = new HashSet<>(); + private String vertexAttributeName; + private AttributedGraph graph; + private String graphDescription; + private GraphDisplayListener listener; + private String currentFocusedVertex; + private List currentSelection; + + @Override + public void setGraphDisplayListener(GraphDisplayListener listener) { + this.listener = listener; + } + + @Override + public void setLocation(String vertexID) { + currentFocusedVertex = vertexID; + } + + public String getFocusedVertex() { + return currentFocusedVertex; + } + + @Override + public void selectVertices(List vertexList) { + currentSelection = vertexList; + } + + public List getSelectedVertices() { + return currentSelection; + } + + @Override + public void close() { + // nothing + } + + @Override + public void defineVertexAttribute(String name) { + definedVertexAttributes.add(name); + } + + @Override + public void defineEdgeAttribute(String name) { + definedEdgeAttributes.add(name); + } + + @Override + public void setVertexLabel(String attributeName, int alignment, int size, boolean monospace, + int maxLines) { + vertexAttributeName = attributeName; + } + + @Override + public void setGraph(AttributedGraph graph, String description, boolean append, + TaskMonitor monitor) + throws CancelledException { + this.graph = graph; + this.graphDescription = description; + } + + @Override + public void clear() { + // nothing + } + + @Override + public void updateVertexName(String id, String newName) { + // nothing + } + + @Override + public String getGraphDescription() { + return graphDescription; + } + + public AttributedGraph getGraph() { + return graph; + } + + public void focusChanged(String vertexId) { + listener.locationChanged(vertexId); + } + + public void selectionChanged(List vertexIds) { + listener.selectionChanged(vertexIds); + } +} diff --git a/Ghidra/Features/ProgramGraph/src/test/java/ghidra/graph/program/TestGraphService.java b/Ghidra/Features/ProgramGraph/src/test/java/ghidra/graph/program/TestGraphService.java new file mode 100644 index 0000000000..4e463b5c26 --- /dev/null +++ b/Ghidra/Features/ProgramGraph/src/test/java/ghidra/graph/program/TestGraphService.java @@ -0,0 +1,62 @@ +/* ### + * 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.program; + +import ghidra.framework.options.Options; +import ghidra.framework.plugintool.PluginTool; +import ghidra.service.graph.GraphDisplay; +import ghidra.service.graph.GraphDisplayProvider; +import ghidra.util.HelpLocation; +import ghidra.util.exception.GraphException; +import ghidra.util.task.TaskMonitor; + +public class TestGraphService implements GraphDisplayProvider { + private TestGraphDisplay testDisplay = new TestGraphDisplay(); + + @Override + public String getName() { + return "Test Graph Service"; + } + + @Override + public GraphDisplay getGraphDisplay(boolean reuseGraph, + TaskMonitor monitor) throws GraphException { + return testDisplay; + } + + @Override + public void initialize(PluginTool tool, Options options) { + // nothing + + } + + @Override + public void optionsChanged(Options options) { + // nothing + + } + + @Override + public void dispose() { + // nothing + } + + @Override + public HelpLocation getHelpLocation() { + return null; + } + +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/ComponentProvider.java b/Ghidra/Framework/Docking/src/main/java/docking/ComponentProvider.java index a0677035e5..2e546433b2 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/ComponentProvider.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/ComponentProvider.java @@ -248,6 +248,7 @@ public abstract class ComponentProvider implements HelpDescriptor, ActionContext * Removes this provider from the tool. */ public void removeFromTool() { + dockingTool.removeAction(showProviderAction); dockingTool.removeComponentProvider(this); } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/action/DockingAction.java b/Ghidra/Framework/Docking/src/main/java/docking/action/DockingAction.java index 2034c88eda..545dd33c15 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/action/DockingAction.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/action/DockingAction.java @@ -594,7 +594,6 @@ public abstract class DockingAction implements DockingActionIf { inceptionInformation = ""; return; } - inceptionInformation = getInceptionFromTheFirstClassThatIsNotUsOrABuilder(); } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/action/builder/AbstractActionBuilder.java b/Ghidra/Framework/Docking/src/main/java/docking/action/builder/AbstractActionBuilder.java index e961eee4d8..aae81e68f7 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/action/builder/AbstractActionBuilder.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/action/builder/AbstractActionBuilder.java @@ -117,7 +117,6 @@ public abstract class AbstractActionBuilder extends AbstractActionBuilder, ActionContext, MultiStateActionBuilder> { private BiConsumer, EventTrigger> actionStateChangedCallback; - private boolean performActionOnButtonClick; + private boolean performActionOnButtonClick = false; + + private List> states = new ArrayList<>(); /** * Builder constructor @@ -73,6 +79,41 @@ public class MultiStateActionBuilder extends return self(); } + /** + * Add an action state + * + * @param displayName the name to appear in the action menu + * @param icon the icon to appear in the action menu + * @param userData the data associated with this state + * @return this MultiActionDockingActionBuilder (for chaining) + */ + public MultiStateActionBuilder addState(String displayName, Icon icon, T userData) { + states.add(new ActionState(displayName, icon, userData)); + return self(); + } + + /** + * Add an action state + * + * @param actionState the action state to add + * @return this MultiActionDockingActionBuilder (for chaining) + */ + public MultiStateActionBuilder addState(ActionState actionState) { + states.add(actionState); + return self(); + } + + /** + * Add a list of action states + * + * @param list a list of ActionStates; + * @return this MultiActionDockingActionBuilder (for chaining) + */ + public MultiStateActionBuilder addStates(List> list) { + states.addAll(list); + return self(); + } + @Override public MultiStateDockingAction build() { validate(); @@ -84,7 +125,7 @@ public class MultiStateActionBuilder extends EventTrigger trigger) { actionStateChangedCallback.accept(newActionState, trigger); } - + @Override protected void doActionPerformed(ActionContext context) { if (actionCallback != null) { @@ -93,11 +134,14 @@ public class MultiStateActionBuilder extends } }; + for (ActionState actionState : states) { + action.addActionState(actionState); + } + decorateAction(action); action.setPerformActionOnPrimaryButtonClick(performActionOnButtonClick); return action; } - @Override protected void validate() { diff --git a/Ghidra/Framework/Docking/src/main/java/docking/action/builder/ToggleActionBuilder.java b/Ghidra/Framework/Docking/src/main/java/docking/action/builder/ToggleActionBuilder.java index 3123c35aa5..ce16207f9a 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/action/builder/ToggleActionBuilder.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/action/builder/ToggleActionBuilder.java @@ -14,6 +14,7 @@ * limitations under the License. */ package docking.action.builder; + import docking.ActionContext; import docking.action.ToggleDockingAction; @@ -69,4 +70,3 @@ public class ToggleActionBuilder extends } } - diff --git a/Ghidra/Framework/Docking/src/main/java/docking/help/HelpManager.java b/Ghidra/Framework/Docking/src/main/java/docking/help/HelpManager.java index 47046a22f7..d20ca7ec32 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/help/HelpManager.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/help/HelpManager.java @@ -74,7 +74,7 @@ public class HelpManager implements HelpService { private boolean isValidHelp; private boolean hasBeenDisplayed; - private Set excludedFromHelp = new HashSet<>(); + private Set excludedFromHelp = Collections.newSetFromMap(new WeakHashMap<>()); /** * Constructor. diff --git a/Ghidra/Framework/Generic/build.gradle b/Ghidra/Framework/Generic/build.gradle index 814347f76c..3b1dbfd3c7 100644 --- a/Ghidra/Framework/Generic/build.gradle +++ b/Ghidra/Framework/Generic/build.gradle @@ -17,6 +17,8 @@ dependencies { compile "org.apache.logging.log4j:log4j-core:2.12.1" compile "org.apache.commons:commons-collections4:4.1" compile "org.apache.commons:commons-lang3:3.9" + compile "org.apache.commons:commons-text:1.6" + compile "commons-io:commons-io:2.6" compileOnly "junit:junit:4.12" diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/exception/GraphException.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/exception/GraphException.java index f35f64ec55..d54ef7927d 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/exception/GraphException.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/exception/GraphException.java @@ -1,6 +1,5 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,17 +21,21 @@ package ghidra.util.exception; public class GraphException extends UsrException { /** - * Default constructor - */ - public GraphException() { - super("Graph Error."); - } + * Default constructor + */ + public GraphException() { + super("Graph Error."); + } - /** - * Constructor - * @param message detailed message - */ - public GraphException(String message) { - super(message); - } + /** + * Constructor + * @param message detailed message + */ + public GraphException(String message) { + super(message); + } + + public GraphException(String title, Throwable cause) { + super(title, cause); + } } diff --git a/Ghidra/Framework/Generic/src/main/java/resources/Icons.java b/Ghidra/Framework/Generic/src/main/java/resources/Icons.java index c1582a42be..401b0e0080 100644 --- a/Ghidra/Framework/Generic/src/main/java/resources/Icons.java +++ b/Ghidra/Framework/Generic/src/main/java/resources/Icons.java @@ -33,72 +33,58 @@ import resources.icons.TranslateIcon; */ public class Icons { - public static final ImageIcon EMPTY_ICON = ResourceManager.loadImage("images/EmptyIcon16.gif"); + public static final ImageIcon EMPTY_ICON = get("images/EmptyIcon16.gif"); - public static final ImageIcon HELP_ICON = - ResourceManager.loadImage("images/help-browser.png"); + public static final ImageIcon HELP_ICON = get("images/help-browser.png"); - public static final ImageIcon ADD_ICON = ResourceManager.loadImage("images/Plus2.png"); + public static final ImageIcon ADD_ICON = get("images/Plus2.png"); - public static final ImageIcon COLLAPSE_ALL_ICON = - ResourceManager.loadImage("images/collapse_all.png"); - public static final ImageIcon EXPAND_ALL_ICON = - ResourceManager.loadImage("images/expand_all.png"); + public static final ImageIcon COLLAPSE_ALL_ICON = get("images/collapse_all.png"); + public static final ImageIcon EXPAND_ALL_ICON = get("images/expand_all.png"); - public static final ImageIcon CONFIGURE_FILTER_ICON = - ResourceManager.loadImage("images/exec.png"); - public static final ImageIcon DELETE_ICON = ResourceManager.loadImage("images/error.png"); - public static final ImageIcon ERROR_ICON = - ResourceManager.loadImage("images/emblem-important.png"); + public static final ImageIcon CONFIGURE_FILTER_ICON = get("images/exec.png"); + public static final ImageIcon DELETE_ICON = get("images/error.png"); + public static final ImageIcon ERROR_ICON = get("images/emblem-important.png"); - public static final ImageIcon NAVIGATE_ON_INCOMING_EVENT_ICON = - ResourceManager.loadImage("images/locationIn.gif"); - public static final ImageIcon NAVIGATE_ON_OUTGOING_EVENT_ICON = - ResourceManager.loadImage("images/locationOut.gif"); + public static final ImageIcon NAVIGATE_ON_INCOMING_EVENT_ICON = get("images/locationIn.gif"); + public static final ImageIcon NAVIGATE_ON_OUTGOING_EVENT_ICON = get("images/locationOut.gif"); - public static final ImageIcon NOT_ALLOWED_ICON = ResourceManager.loadImage("images/no.png"); - public static final ImageIcon OPEN_FOLDER_ICON = - ResourceManager.loadImage("images/openSmallFolder.png"); - public static final ImageIcon REFRESH_ICON = ResourceManager.loadImage("images/reload3.png"); + public static final ImageIcon NOT_ALLOWED_ICON = get("images/no.png"); + public static final ImageIcon OPEN_FOLDER_ICON = get("images/openSmallFolder.png"); + public static final ImageIcon REFRESH_ICON = get("images/reload3.png"); - public static final ImageIcon SORT_ASCENDING_ICON = - ResourceManager.loadImage("images/sortascending.png"); - public static final ImageIcon SORT_DESCENDING_ICON = - ResourceManager.loadImage("images/sortdescending.png"); + public static final ImageIcon SORT_ASCENDING_ICON = get("images/sortascending.png"); + public static final ImageIcon SORT_DESCENDING_ICON = get("images/sortdescending.png"); - public static final ImageIcon STOP_ICON = ResourceManager.loadImage("images/process-stop.png"); - public static final ImageIcon STRONG_WARNING_ICON = - ResourceManager.loadImage("images/software-update-urgent.png"); + public static final ImageIcon STOP_ICON = get("images/process-stop.png"); + public static final ImageIcon STRONG_WARNING_ICON = get("images/software-update-urgent.png"); - public static final ImageIcon LEFT_ICON = ResourceManager.loadImage("images/left.png"); - public static final ImageIcon RIGHT_ICON = ResourceManager.loadImage("images/right.png"); + public static final ImageIcon LEFT_ICON = get("images/left.png"); + public static final ImageIcon RIGHT_ICON = get("images/right.png"); /** An version of the LEFT_ICON with a different color */ - public static final ImageIcon LEFT_ALTERNATE_ICON = - ResourceManager.loadImage("images/left.alternate.png"); + public static final ImageIcon LEFT_ALTERNATE_ICON = get("images/left.alternate.png"); /** An version of the RIGHT_ICON with a different color */ - public static final ImageIcon RIGHT_ALTERNATE_ICON = - ResourceManager.loadImage("images/right.alternate.png"); + public static final ImageIcon RIGHT_ALTERNATE_ICON = get("images/right.alternate.png"); - public static final ImageIcon SAVE_AS = ResourceManager.getImageIcon( - new DotDotDotIcon(ResourceManager.loadImage("images/Disk.png"))); + public static final ImageIcon SAVE_AS = + ResourceManager.getImageIcon(new DotDotDotIcon(get("images/Disk.png"))); - public static final ImageIcon MAKE_SELECTION_ICON = - ResourceManager.loadImage("images/text_align_justify.png"); + public static final ImageIcon MAKE_SELECTION_ICON = get("images/text_align_justify.png"); // Not necessarily re-usable, but this is needed for the help system; these should // probably be moved to the client that uses them, while updating the // help system to use them there. - public static final ImageIcon ARROW_DOWN_RIGHT_ICON = ResourceManager.getImageIcon( - new RotateIcon(ResourceManager.loadImage("images/viewmagfit.png"), 90)); - public static final ImageIcon ARROW_UP_LEFT_ICON = ResourceManager.getImageIcon( - new RotateIcon(ResourceManager.loadImage("images/viewmagfit.png"), 275)); - public static final ImageIcon FILTER_NOT_ACCEPTED_ICON = ResourceManager.getImageIcon( - new MultiIcon(ResourceManager.loadImage("images/flag.png"), new TranslateIcon( + public static final ImageIcon ARROW_DOWN_RIGHT_ICON = + ResourceManager.getImageIcon(new RotateIcon(get("images/viewmagfit.png"), 90)); + public static final ImageIcon ARROW_UP_LEFT_ICON = + ResourceManager.getImageIcon(new RotateIcon(get("images/viewmagfit.png"), 275)); + public static final ImageIcon FILTER_NOT_ACCEPTED_ICON = + ResourceManager.getImageIcon(new MultiIcon(get("images/flag.png"), new TranslateIcon( ResourceManager.loadImage("images/dialog-cancel.png", 10, 10), 6, 6))); - public static final ImageIcon APPLY_BLOCKED_MATCH_ICON = ResourceManager.getImageIcon( - new MultiIcon(ResourceManager.loadImage("images/kgpg.png"), new TranslateIcon( + public static final ImageIcon APPLY_BLOCKED_MATCH_ICON = + ResourceManager.getImageIcon(new MultiIcon(get("images/kgpg.png"), new TranslateIcon( ResourceManager.loadImage("images/checkmark_green.gif", 12, 12), 4, 0))); /** @@ -134,6 +120,39 @@ public class Icons { return new IconProvider(icon, url); } + /** + * Gets the icon for the given icon path. The given path should be relative to the classpath. + * If an icon by that name can't be found, the default "bomb" icon is returned instead. + *

+ * For example, an icon named foo.png would typically be stored in the module at + * "{modulePath}/src/main/resources/image/foo.png". To reference that icon, use the path + * "images/foo.png", since "{modulePath}/src/main/resources" is in the classpath. + * + * @param iconPath the icon path (relative to the classpath) + * @return The icon referenced by that path. + */ + public static ImageIcon get(String iconPath) { + return ResourceManager.loadImage(iconPath); + } + + /** + * Gets the icon for the given icon path and scale it to the specifed width and height. + * The given path should be relative to the classpath. + * If an icon by that name can't be found, the default "bomb" icon is returned instead. + *

+ * For example, an icon named foo.png would typically be stored in the module at + * "{modulePath}/src/main/resources/image/foo.png". To reference that icon, use the path + * "images/foo.png", since "{modulePath}/src/main/resources" is in the classpath. + * + * @param iconPath the icon path (relative to the classpath) + * @param width the desired width after scaling + * @param height the desired height after scaling + * @return The icon referenced by that path. + */ + public static ImageIcon get(String iconPath, int width, int height) { + return ResourceManager.loadImage(iconPath, width, height); + } + private static String getIconName(String snippet) { if (!isIconsReference(snippet)) { return null; diff --git a/Ghidra/Framework/Graph/build.gradle b/Ghidra/Framework/Graph/build.gradle index dbad32538c..52f54ce712 100644 --- a/Ghidra/Framework/Graph/build.gradle +++ b/Ghidra/Framework/Graph/build.gradle @@ -17,6 +17,8 @@ dependencies { compile "net.sf.jung:jung-graph-impl:2.1.1" compile "net.sf.jung:jung-visualization:2.1.1" + compile "org.jgrapht:jgrapht-core:1.4.0" + // These have abstract test classes and stubs needed by this module testCompile project(path: ':Docking', configuration: 'testArtifacts') } diff --git a/Ghidra/Framework/Graph/certification.manifest b/Ghidra/Framework/Graph/certification.manifest index 48f4f88706..f73ee48a62 100644 --- a/Ghidra/Framework/Graph/certification.manifest +++ b/Ghidra/Framework/Graph/certification.manifest @@ -4,6 +4,7 @@ ##MODULE IP: Oxygen Icons - LGPL 3.0 Module.manifest||GHIDRA||||END| build.gradle||GHIDRA||||END| +data/ExtensionPoint.manifest||GHIDRA||||END| src/main/docs/README.txt||GHIDRA||||END| src/main/docs/VerticesAndEdges.png||GHIDRA||||END| src/main/docs/VerticesAndEdges.xml||GHIDRA||||END| diff --git a/Ghidra/Framework/Graph/data/ExtensionPoint.manifest b/Ghidra/Framework/Graph/data/ExtensionPoint.manifest new file mode 100644 index 0000000000..6aafdd13cc --- /dev/null +++ b/Ghidra/Framework/Graph/data/ExtensionPoint.manifest @@ -0,0 +1,2 @@ +GraphDisplayProvider + diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/service/graph/Attributed.java b/Ghidra/Framework/Graph/src/main/java/ghidra/service/graph/Attributed.java new file mode 100644 index 0000000000..9b408496d2 --- /dev/null +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/service/graph/Attributed.java @@ -0,0 +1,138 @@ +/* ### + * 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.service.graph; + +import java.util.*; + +public class Attributed { + + /** + * the {@link HashMap} to contain attribute mappings + */ + private Map attributes = new HashMap<>(); + + /** + * Returns an unmodifiable view of the attribute map + * @return an unmodifiable view of the attribute map + */ + + public Map getAttributeMap() { + return Collections.unmodifiableMap(attributes); + } + + /** + * Sets the attribute with the given key and value + * + * @param key attribute key + * @param value attribute value + * @return the previous value of the attribute + */ + public String setAttribute(String key, String value) { + return attributes.put(key, value); + } + + /** + * gets the value of the given attribute name + * + * @param key attribute name + * @return the mapped value for the supplied key + */ + public String getAttribute(String key) { + return attributes.get(key); + } + + /** + * Removes the attribute with the given key + * + * @param key attribute key + * @return the value of the removed attribute + */ + public String removeAttribute(String key) { + return attributes.remove(key); + } + + /** + * Returns true if there is an attribute with that name + * + * @param key attribute key + * @return true if there is an attribute with that name + */ + public boolean hasAttribute(String key) { + return attributes.containsKey(key); + } + + /** + * Returns the number of attributes defined + * + * @return the number of attributes defined + */ + public int size() { + return attributes.size(); + } + + /** + * Return true if there are no attributes + * + * @return true if there are no mapped attributes + */ + public boolean isEmpty() { + return attributes.isEmpty(); + } + + /** + * Adds all the key/value pairs from the given map as attributes + * + * @param map a map of key/values to add as attributes + */ + public void putAttributes(Map map) { + attributes.putAll(map); + } + + /** + * removes all key/value mappings + */ + public void clear() { + attributes.clear(); + } + + /** + * Returns the keys for the attributes + * + * @return the keys for the attributes + */ + public Set keys() { + return attributes.keySet(); + } + + /** + * Returns the attribute values + * + * @return the attribute values + */ + public Collection values() { + return attributes.values(); + } + + /** + * Returns a {@link Set} containing the key/value entry associations + * + * @return a {@link Set} containing the key/value entry associations + */ + public Set> entrySet() { + return attributes.entrySet(); + } + +} diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/service/graph/AttributedEdge.java b/Ghidra/Framework/Graph/src/main/java/ghidra/service/graph/AttributedEdge.java new file mode 100644 index 0000000000..cd4f65b312 --- /dev/null +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/service/graph/AttributedEdge.java @@ -0,0 +1,88 @@ +/* ### + * 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.service.graph; + +import java.util.Map; + +/** + * Generic directed graph edge implementation + */ +public class AttributedEdge extends Attributed { + private final String id; + /** + * cache of the edge label parsed as html + */ + private String htmlString; + + /** + * Constructs a new GhidraEdge + * @param id the unique id for the edge + */ + public AttributedEdge(String id) { + this.id = id; + } + + @Override + public String toString() { + return id; + } + + /** + * create (once) the html representation of the key/values for this edge + * @return html formatted label for the edge + */ + public String getHtmlString() { + if (htmlString == null) { + StringBuilder buf = new StringBuilder(""); + for (Map.Entry entry : entrySet()) { + buf.append(entry.getKey()); + buf.append(":"); + buf.append(entry.getValue()); + buf.append("
"); + } + htmlString = buf.toString(); + } + return htmlString; + } + + /** + * Returns the id for this edge + * @return the id for this edge + */ + public String getId() { + return id; + } + + @Override + public int hashCode() { + return id.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + AttributedEdge other = (AttributedEdge) obj; + return id.equals(other.id); + } +} diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/service/graph/AttributedGraph.java b/Ghidra/Framework/Graph/src/main/java/ghidra/service/graph/AttributedGraph.java new file mode 100644 index 0000000000..977c205ae1 --- /dev/null +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/service/graph/AttributedGraph.java @@ -0,0 +1,241 @@ +/* ### + * 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.service.graph; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +import org.jgrapht.graph.AbstractBaseGraph; +import org.jgrapht.graph.DefaultGraphType; + +/** + * Basic graph implementation for a directed graph whose vertices and edges support attributes. + *

+ * The graph can be configured as to how to handle multiple edges with the same source and destination + * vertices. One option is to simply allow multiple edges. The second option is to collapse + * duplicate edges such that there is only ever one edge with the same + * source and destination. In this case, each additional duplicate edge added will cause the + * edge to have a "Weight" attribute that will be the total number of edges that were added + * to the same source/destination vertex pair. + */ +public class AttributedGraph extends AbstractBaseGraph { + private static final String WEIGHT = "Weight"; + + private Map vertexMap = new HashMap<>(); + private final boolean collapseDuplicateEdges; + + /** + * Create a new empty AttributedGraph that automatically collapses duplicate edges + */ + public AttributedGraph() { + this(true); + } + + /** + * Create a new empty AttributedGraph. + * + * @param collapseDuplicateEdges if true, duplicate edges will be collapsed into a single + * edge with a "Weight" attribute whose value is the number of edges between those vertices. + */ + public AttributedGraph(boolean collapseDuplicateEdges) { + super(new VertexSupplier(), new EdgeSupplier(), DefaultGraphType.directedPseudograph()); + this.collapseDuplicateEdges = collapseDuplicateEdges; + } + + /** + * Adds a new vertex with the given id. The vertex's name will be the same as the id. + * If a vertex already exists with that id, + * then that vertex will be returned. + * + * @param id the unique vertex id that the graph should have a vertex for. + * @return either an existing vertex with that id, or a newly added vertex with that id + */ + public AttributedVertex addVertex(String id) { + return addVertex(id, id); + } + + /** + * Adds a new vertex with the given id and name. If a vertex already exists with that id, + * then that vertex will be returned, but with its name changed to the given name. + * + * @param id the unique vertex id that the graph should have a vertex for. + * @param name the name to associate with this vertex + * @return either an existing vertex with that id, or a newly added vertex with that id + */ + public AttributedVertex addVertex(String id, String name) { + if (vertexMap.containsKey(id)) { + AttributedVertex vertex = vertexMap.get(id); + vertex.setName(name); + } + AttributedVertex newVertex = new AttributedVertex(id, name); + addVertex(newVertex); + return newVertex; + } + + @Override + public AttributedVertex addVertex() { + AttributedVertex vertex = super.addVertex(); + vertexMap.put(vertex.getId(), vertex); + return vertex; + } + + @Override + public boolean addVertex(AttributedVertex vertex) { + if (super.addVertex(vertex)) { + vertexMap.put(vertex.getId(), vertex); + return true; + } + return false; + } + + /** + * Creates and adds a new directed edge with the given id between the given source and + * target vertices. If the graph is set to collapse duplicate edges and an edge for that + * source and target exists, then the existing edge will be return with its "Weight" attribute + * set to the total number of edges that have been added between the source and target vertices. + * + * @param source the source vertex of the directed edge to be created. + * @param target the target vertex of the directed edge to be created. + * @param edgeId the id to use for the new edge. Note: if this is a duplicate and edges + * are being collapsed, then this edgeId will not be used. + * @return a new edge between the source and target if it is the first one or the graph is + * not collapsing edges. Otherwise, an existing edge with its "Weight" attribute set accordingly. + */ + public AttributedEdge addEdge(AttributedVertex source, AttributedVertex target, String edgeId) { + AttributedEdge basicEdge = new AttributedEdge(edgeId); + addEdge(source, target, basicEdge); + return basicEdge; + } + + /** + * Creates and adds a new directed edge with the given edge object. If the graph is set to + * collapse duplicate edges and an edge for that + * source and target exists, then the existing edge will be return with its "Weight" attribute + * set to the total number of edges that have been added between the source and target vertices. + * + * @param source the source vertex of the directed edge to be created. + * @param target the target vertex of the directed edge to be created. + * @param edge the BasicEdge object to use for the new edge. Note: if this is a duplicate and + * edges are being collapsed, then this edge object will not be used. + * @return true if the edge was added. Note that if this graph is collapsing duplicate edges, then + * it will always return true. + */ + @Override + public boolean addEdge(AttributedVertex source, AttributedVertex target, AttributedEdge edge) { + ensureInGraph(source); + ensureInGraph(target); + if (collapseDuplicateEdges) { + AttributedEdge existingEdge = getEdge(source, target); + if (existingEdge != null) { + incrementWeightProperty(existingEdge); + return true; + } + } + return super.addEdge(source, target, edge); + } + + /** + * Creates and adds a new directed edge between the given source and + * target vertices. If the graph is set to collapse duplicate edges and an edge for that + * source and target exists, then the existing edge will be return with its "Weight" attribute + * set to the total number of edges that have been added between the source and target vertices. + * + * @param source the source vertex of the directed edge to be created. + * @param target the target vertex of the directed edge to be created. + * @return a new edge between the source and target if it is the first one or the graph is + * not collapsing edges. Otherwise, an existing edge with its "Weight" attribute set accordingly. + */ + @Override + public AttributedEdge addEdge(AttributedVertex source, AttributedVertex target) { + ensureInGraph(source); + ensureInGraph(target); + + if (collapseDuplicateEdges) { + AttributedEdge edge = getEdge(source, target); + if (edge != null) { + incrementWeightProperty(edge); + return edge; + } + } + return super.addEdge(source, target); + } + + /** + * Returns the total number of edges in the graph + * @return the total number of edges in the graph + */ + public int getEdgeCount() { + return edgeSet().size(); + } + + /** + * Returns the total number of vertices in the graph + * @return the total number of vertices in the graph + */ + public int getVertexCount() { + return vertexSet().size(); + } + + /** + * Returns the vertex with the given vertex id + * @param vertexId the id of the vertex to retrieve + * @return the vertex with the given vertex id or null if none found + */ + public AttributedVertex getVertex(String vertexId) { + return vertexMap.get(vertexId); + } + + private void ensureInGraph(AttributedVertex vertex) { + if (!containsVertex(vertex)) { + addVertex(vertex); + } + } + + private static void incrementWeightProperty(AttributedEdge edge) { + if (edge.hasAttribute(WEIGHT)) { + String weightString = edge.getAttribute(WEIGHT); + edge.setAttribute(WEIGHT, incrementWeightStringValue(weightString)); + } + else { + edge.setAttribute(WEIGHT, "2"); + } + } + + private static String incrementWeightStringValue(String value) { + int weight = Integer.parseInt(value); + weight++; + return Integer.toString(weight); + } + + private static class VertexSupplier implements Supplier { + long id = 0; + + @Override + public AttributedVertex get() { + return new AttributedVertex(Long.toString(id++)); + } + } + + private static class EdgeSupplier implements Supplier { + long id = 0; + + @Override + public AttributedEdge get() { + return new AttributedEdge(Long.toString(id++)); + } + } +} diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/service/graph/AttributedVertex.java b/Ghidra/Framework/Graph/src/main/java/ghidra/service/graph/AttributedVertex.java new file mode 100644 index 0000000000..3309ca6322 --- /dev/null +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/service/graph/AttributedVertex.java @@ -0,0 +1,115 @@ +/* ### + * 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.service.graph; + +import java.util.Map; + +/** + * Graph vertex with attributes + */ +public class AttributedVertex extends Attributed { + + private final String id; + /** + * cache of the html rendering of the vertex attributes + */ + private String htmlString; + + /** + * Constructs a new GhidraVertex with the given id and name + * + * @param id the unique id for the vertex + * @param name the name for the vertex + */ + public AttributedVertex(String id, String name) { + this.id = id; + setName(name); + } + + public AttributedVertex(String id) { + this(id, id); + } + + /** + * Sets the name on the vertex + * + * @param name the new name for the vertex + */ + public void setName(String name) { + setAttribute("Name", name); + } + + /** + * Returns the id for this vertex + * @return the id for this vertex + */ + public String getId() { + return id; + } + + /** + * returns the name of the vertex + * + * @return the name of the vertex + */ + public String getName() { + return getAttribute("Name"); + } + + @Override + public String toString() { + return getName() + " (" + id + ")"; + } + + /** + * parse (one time) then cache the attributes to html + * @return the html string + */ + public String getHtmlString() { + if (htmlString == null) { + StringBuilder buf = new StringBuilder(""); + for (Map.Entry entry : entrySet()) { + buf.append(entry.getKey()); + buf.append(":"); + buf.append(entry.getValue()); + buf.append("
"); + } + htmlString = buf.toString(); + } + return htmlString; + } + + @Override + public int hashCode() { + return id.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + AttributedVertex other = (AttributedVertex) obj; + return id.equals(other.id); + } + +} diff --git a/Ghidra/Framework/Graph/src/test/java/ghidra/service/graph/AttributedGraphTest.java b/Ghidra/Framework/Graph/src/test/java/ghidra/service/graph/AttributedGraphTest.java new file mode 100644 index 0000000000..56719b7fe9 --- /dev/null +++ b/Ghidra/Framework/Graph/src/test/java/ghidra/service/graph/AttributedGraphTest.java @@ -0,0 +1,218 @@ +/* ### + * 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.service.graph; + +import static org.junit.Assert.*; + +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; + +public class AttributedGraphTest { + + private AttributedGraph graph; + + @Before + public void setup() { + graph = new AttributedGraph(); + } + + @Test + public void testAddVertex() { + AttributedVertex v = graph.addVertex(); + assertTrue(graph.containsVertex(v)); + assertEquals(1, graph.getVertexCount()); + } + + @Test + public void testAddVertexTwice() { + AttributedVertex v = graph.addVertex(); + assertFalse(graph.addVertex(v)); + assertEquals(1, graph.getVertexCount()); + } + + @Test + public void testAddVertexWithId() { + AttributedVertex v = graph.addVertex("A"); + assertTrue(graph.containsVertex(v)); + assertEquals(1, graph.getVertexCount()); + assertEquals("A", v.getId()); + assertEquals("A", v.getName()); + } + + @Test + public void testAddVertexWithIdAndName() { + AttributedVertex v = graph.addVertex("A", "Bob"); + assertTrue(graph.containsVertex(v)); + assertEquals(1, graph.getVertexCount()); + assertEquals("A", v.getId()); + assertEquals("Bob", v.getName()); + } + + @Test + public void testAddVertexWithExistingVertex() { + AttributedVertex v = new AttributedVertex("A"); + graph.addVertex(v); + assertTrue(graph.containsVertex(v)); + assertEquals(1, graph.getVertexCount()); + assertEquals("A", v.getId()); + } + + @Test + public void testAddDuplicateVertex() { + AttributedVertex v1 = graph.addVertex("A"); + AttributedVertex v2 = graph.addVertex("A"); + + assertEquals(1, graph.getVertexCount()); + assertTrue(v1 == v2); + } + + @Test + public void testAddDuplicateVertexWithDifferentName() { + AttributedVertex v1 = graph.addVertex("A", "Bob"); + AttributedVertex v2 = graph.addVertex("A", "Joe"); + + assertEquals(1, graph.getVertexCount()); + assertTrue(v1 == v2); + assertEquals("Bob", v2.getName()); + } + + @Test + public void testAddEdge() { + AttributedVertex v1 = graph.addVertex("A", "Bob"); + AttributedVertex v2 = graph.addVertex("B", "Joe"); + AttributedEdge e = graph.addEdge(v1, v2); + assertEquals(1, graph.getEdgeCount()); + assertEquals(v1, graph.getEdgeSource(e)); + assertEquals(v2, graph.getEdgeTarget(e)); + } + + @Test + public void testAddExistingEdge() { + AttributedVertex v1 = graph.addVertex("A", "Bob"); + AttributedVertex v2 = graph.addVertex("B", "Joe"); + AttributedEdge e = new AttributedEdge("E1"); + assertTrue(graph.addEdge(v1, v2, e)); + + assertEquals(1, graph.getEdgeCount()); + assertEquals(v1, graph.getEdgeSource(e)); + assertEquals(v2, graph.getEdgeTarget(e)); + } + + @Test + public void testAddEdgeWithId() { + AttributedVertex v1 = graph.addVertex("A", "Bob"); + AttributedVertex v2 = graph.addVertex("B", "Joe"); + AttributedEdge e = graph.addEdge(v1, v2, "X"); + + assertEquals(1, graph.getEdgeCount()); + assertEquals(v1, graph.getEdgeSource(e)); + assertEquals(v2, graph.getEdgeTarget(e)); + assertEquals("X", e.getId()); + } + + @Test + public void testCanAddEdgeWithVerticesNotInGraph() { + AttributedVertex v1 = new AttributedVertex("A", "Bob"); + AttributedVertex v2 = new AttributedVertex("B", "Joe"); + + AttributedEdge e = graph.addEdge(v1, v2); + + assertEquals(2, graph.getVertexCount()); + assertEquals(1, graph.getEdgeCount()); + + assertTrue(graph.containsVertex(v1)); + assertTrue(graph.containsVertex(v2)); + } + + @Test + public void testGetVertexById() { + + // create a vertex with all the possible ways + AttributedVertex v1 = graph.addVertex("A"); + AttributedVertex v2 = graph.addVertex("B", "NAME"); + AttributedVertex v3 = graph.addVertex(); + AttributedVertex v4 = new AttributedVertex("C"); + graph.addVertex(v4); + + Set vertexSet = graph.vertexSet(); + assertEquals(4, vertexSet.size()); + + // make sure all vertices were added to the id to vertex map + assertEquals(v1, graph.getVertex("A")); + assertEquals(v2, graph.getVertex("B")); + assertEquals(v3, graph.getVertex(v3.getId())); + assertEquals(v4, graph.getVertex("C")); + + } + + @Test + public void testCollapseDuplicateEdges() { + AttributedVertex v1 = graph.addVertex("A"); + AttributedVertex v2 = graph.addVertex("B"); + + graph.addEdge(v1, v2); + graph.addEdge(v1, v2); + graph.addEdge(v1, v2); + + assertEquals(1, graph.getEdgeCount()); + + assertEquals("3", graph.getEdge(v1, v2).getAttribute("Weight")); + } + + @Test + public void testCollapseDuplicateEdgesWithSuppliedEdges() { + AttributedVertex v1 = graph.addVertex("A"); + AttributedVertex v2 = graph.addVertex("B"); + + graph.addEdge(v1, v2, new AttributedEdge("1")); + graph.addEdge(v1, v2, new AttributedEdge("2")); + graph.addEdge(v1, v2, new AttributedEdge("3")); + + assertEquals(1, graph.getEdgeCount()); + + AttributedEdge edge = graph.getEdge(v1, v2); + assertEquals("3", edge.getAttribute("Weight")); + assertEquals("1", edge.getId()); + } + + @Test + public void testNonCollapsingEdges() { + graph = new AttributedGraph(false); + + AttributedVertex v1 = graph.addVertex("A"); + AttributedVertex v2 = graph.addVertex("B"); + + graph.addEdge(v1, v2); + graph.addEdge(v1, v2); + graph.addEdge(v1, v2, new AttributedEdge("x")); + + assertEquals(3, graph.getEdgeCount()); + } + + @Test + public void testReverseEdgesDontCollapse() { + AttributedVertex v1 = graph.addVertex("A"); + AttributedVertex v2 = graph.addVertex("B"); + + graph.addEdge(v1, v2); + graph.addEdge(v2, v1); + + assertEquals(2, graph.getEdgeCount()); + } + +} diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginTool.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginTool.java index 6ac65b5e65..0449b6dd93 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginTool.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginTool.java @@ -968,8 +968,8 @@ public abstract class PluginTool extends AbstractDockingTool { saveAsAction.setMenuBarData(menuData); saveAsAction.setEnabled(true); - saveAsAction.setHelpLocation( - new HelpLocation(ToolConstants.TOOL_HELP_TOPIC, "Tool_Changes")); + saveAsAction + .setHelpLocation(new HelpLocation(ToolConstants.TOOL_HELP_TOPIC, "Tool_Changes")); addAction(saveAction); addAction(saveAsAction); @@ -994,8 +994,8 @@ public abstract class PluginTool extends AbstractDockingTool { new String[] { ToolConstants.MENU_FILE, exportPullright, "Export Tool..." }); menuData.setMenuSubGroup(Integer.toString(subGroup++)); exportToolAction.setMenuBarData(menuData); - exportToolAction.setHelpLocation( - new HelpLocation(ToolConstants.TOOL_HELP_TOPIC, "Export_Tool")); + exportToolAction + .setHelpLocation(new HelpLocation(ToolConstants.TOOL_HELP_TOPIC, "Export_Tool")); addAction(exportToolAction); DockingAction exportDefautToolAction = @@ -1340,7 +1340,8 @@ public abstract class PluginTool extends AbstractDockingTool { eventMgr.addEventProducer(eventClass); } - void addEventListener(Class eventClass, PluginEventListener listener) { + public void addEventListener(Class eventClass, + PluginEventListener listener) { eventMgr.addEventListener(eventClass, listener); } @@ -1356,7 +1357,7 @@ public abstract class PluginTool extends AbstractDockingTool { eventMgr.removeAllEventListener(listener); } - void removeEventListener(Class eventClass, + public void removeEventListener(Class eventClass, PluginEventListener listener) { eventMgr.removeEventListener(eventClass, listener); } diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/graph/GraphData.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/graph/GraphData.java deleted file mode 100644 index 5eee3028dc..0000000000 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/graph/GraphData.java +++ /dev/null @@ -1,81 +0,0 @@ -/* ### - * IP: GHIDRA - * REVIEWED: YES - * - * 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.graph; - -import java.util.Iterator; - - -/** - * Collection of edges and vertices that make up a graph. - * GraphData is intended to be displayed on a GraphDisplay. - */ -public interface GraphData { - /** - * Create a Vertex with a given name and vertex ID. - * The vertexID string is used to uniquely identify a vertex. It is - * used for selection and location mapping from/to Ghidra and the graph - * display. It should be mappable back to an location/selection that represents - * the vertex in ghidra terms. - * - * @param name name of the vertex, its label - * @param vertexID identifier to uniquely identify this vertex. - * - * @return a graph vertex - */ - public GraphVertex createVertex(String name, String vertexID); - - /** - * Get a vertex with a given address string. - * - * @param vertexID identifier to uniquely identify this vertex. The key is - * useful for mapping location/selection from/to Ghidra and Renoir - * - * @return a vertex tagged with the given address. - */ - public GraphVertex getVertex(String vertexID); - - /** - * Create an edge on the graph connecting two vertices. - * NOTE: These MUST be two vertices created from the above createVertex function. - * - * The address string is used to uniquely identify a vertex. It is - * used for selection and location mapping from/to Ghidra and the graph - * display. It should be mappable back to an actual address in ghidra - * terms. - * - * @param vertexID identifier to uniquely identify this vertex - * @param start start vertex - * @param end end vertex - * - * @return a graph edge - */ - public GraphEdge createEdge(String vertexID, GraphVertex start, GraphVertex end); - - /** - * Get an iterator over all defined vertices. Every object in the iterator - * will be a GraphVertex. - * - * @return a vertex iterator - */ - public Iterator getVertices(); - - /** - * Get an iterator over all defined edges. Every object in the iterator - * will be a GraphEdge. - */ - public Iterator getEdges(); -} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/graph/GraphDisplay.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/graph/GraphDisplay.java deleted file mode 100644 index 2d6a728f96..0000000000 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/graph/GraphDisplay.java +++ /dev/null @@ -1,111 +0,0 @@ -/* ### - * 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. - */ -/* - * GraphDisplay.java - * - * Created on March 4, 2002, 3:42 PM - */ - -package ghidra.program.model.graph; - -import ghidra.util.exception.GraphException; - -/** - * Handle object to a graph display. - */ -public interface GraphDisplay { - /**Aligns the graph text to the left*/ - public static final int ALIGN_LEFT = 0; - /**Aligns the graph text to the center*/ - public static final int ALIGN_CENTER = 1; - /**Aligns the graph text to the right*/ - public static final int ALIGN_RIGHT = 2; - /** - * Pop the graph display to the front. - * @throws GraphException thrown if an error occurs while communicating with the graph service. - */ - void popup() throws GraphException; - /** - * Clear the graph data in the graph display - * @throws GraphException thrown if an error occurs while communicating with the graph service. - */ - void clear() throws GraphException; - /** - * Close the graph. This destroys the graph display. - */ - void close(); - /** - * Check if the graph display is still valid. - */ - boolean isValid(); - /** - * Set the graph data. This will append the data to the graph. - * Call the clear method if this data is to replace the exising - * data on this display. - * - * @param graph the graph data to apply to the graph display. - * @throws GraphException thrown if an error occurs while communicating with the graph service. - */ - void setGraphData(GraphData graph) throws GraphException; - /** - * Define the name of an attribute on edges in the graph displayed. - * @param attributeName the name of the attribute to define on an edge. - * @throws GraphException thrown if an error occurs while communicating with the graph service. - */ - void defineEdgeAttribute(String attributeName) throws GraphException; - /** - * Define the name of an attribute on vertices in the graph displayed. - * @param attributeName the name of the attribute to define on a vertex. - * @throws GraphException thrown if an error occurs while communicating with the graph service. - */ - void defineVertexAttribute(String attributeName) throws GraphException; - /** - * Indicate that the specified vertex attribute should be displayed as the vertex label. - * @param attributeName the name of the vertex attribute - * @param alignment ALIGN_LEFT, ALIGN_CENTER or ALIGN_RIGHT - * @param size font size (8, 10, 12, etc.) - * @param monospace if true a monospace font will be used. - * @param maxLines indicate the maximum number of lines to be displayed for the label. A value <= 1 will - * result in a single line display and the preferred geometric shapes. A value >1 will force the use of - * rectangualr nodes. - * @throws GraphException thrown if an error occurs while communicating with the graph service. - */ - void setVertexLabel(String attributeName, int alignment, int size, boolean monospace, int maxLines) throws GraphException; - /** - * Set the handler that will map addresses strings on vertices and edges - * to/from objects that make sense to the generator of the graph. - * @param handler the GraphSelectionHandler to set on this GraphDisplay. - */ - void setSelectionHandler(GraphSelectionHandler handler); - /** - * Tell the display to set the selection set on the graph based on some - * object that will be passed to the GraphSelectionHandler to map into - * address strings. - * - * @param selectionObject opaque object to be passed to the selection handler. - * @param global true if the selection is to be set on all known graph windows. - */ - void select(Object selectionObject, boolean global); - /** - * Tell the display to set the location cursor on the graph based on some - * object that will be passed to the GraphSelectionHandler to map into - * and address string. - * - * @param locationObject opaque object to be passed to the selection handler. - * @param global true if the selection is to be set on all known graph windows. - */ - void locate(Object locationObject, boolean global); -} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/graph/GraphEdge.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/graph/GraphEdge.java deleted file mode 100644 index 056e7482d2..0000000000 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/graph/GraphEdge.java +++ /dev/null @@ -1,46 +0,0 @@ -/* ### - * IP: GHIDRA - * REVIEWED: YES - * - * 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.graph; - -/** - * Simple interface for a graph edge. - */ -public interface GraphEdge { - /** - * Get the unique ID string tagged to this edge - * @return edge ID - */ - public String getID(); - /** - * Set an attribute on this edge. - * - * NOTE: you must also define the attribute name on the graph - * display that this graph edge will be displayed on. - * - * @param attributeName the name of the attribute - * @param value the value of the attribute - */ - public void setAttribute(String attributeName, String value); - /** - * Get the value of an attribute. - * - * @param attributeName the name of the attribute - * - * @return the string value of the attribute - */ - public String getAttribute(String attributeName); -} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/graph/GraphSelectionHandler.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/graph/GraphSelectionHandler.java deleted file mode 100644 index 51c7b53cd3..0000000000 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/graph/GraphSelectionHandler.java +++ /dev/null @@ -1,97 +0,0 @@ -/* ### - * IP: GHIDRA - * REVIEWED: YES - * - * 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.graph; - -/** - * Handler for selection/location event mappings from/to Ghidra and Renoir. - */ -public interface GraphSelectionHandler { - /** - * Check if the graph is the active graph window. - * - * @return true if this handler is active because the window it is handling - * is active - */ - public boolean isActive(); - /** - * Set the handler to active/inactive based on whether the window it is - * handling is active or inactive. - * - * @param active true to activate the graph (may pop the graph window to the top) - */ - public void setActive(boolean active); - /** - * Check if the graph is enabled to receive/send events. - * - * @return true if this handler is enabled. - */ - public boolean isEnabled(); - /** - * Set the handler to enabled/disabled. This sets an enabled flag on - * this instance and has no affect on the other methods. - * - * @param enabled true to enable mapping selection/location events - */ - public void setEnabled(boolean enabled); - /** - * Translate a Renoir Selection into a Ghidra selection. - * - * @param renoirSelections selection identifiers for selection within Renoir graph - * The Strings are the keys used for the graph vertex - * when generating the graph. - */ - public void select(String [] renoirSelections); - /** - * Translate a Renoir Location into a Ghidra location. - * - * @param renoirLocation string representing the location in renoir - */ - public void locate(String renoirLocation); - /** - * Translate a Ghidra selection into a renoir selection. - * - * @param ghidraSelection ghidra selection object - * @return set of strings that correspond to a Renoir selection - * The strings should be the key strings used when generating the graph. - */ - public String[] select(Object ghidraSelection); - /** - * Translate a Ghidra location into a renoir location. - * - * @param ghidraLocation the location object to translate into a graph key string - * - * @return string representation of the location for Renoir. This should be the - * key of the graph vertex that represents the ghidraLocation object - * on the graph. - */ - public String locate(Object ghidraLocation); - - /** - * Handle Renoir notification. - * @param notificationType command from Renoir - * @return true if notification was handled and there is no need for any other - * handler to be notified. - */ - public boolean notify(String notificationType); - - /** - * Get brief text describing the type of graph. - * - * @return String describing the graph. - */ - public String getGraphType(); -} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/graph/GraphVertex.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/graph/GraphVertex.java deleted file mode 100644 index 77c0ae3d64..0000000000 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/graph/GraphVertex.java +++ /dev/null @@ -1,59 +0,0 @@ -/* ### - * IP: GHIDRA - * REVIEWED: YES - * - * 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.graph; - -/** - * Simple interface for a graph vertex. - */ -public interface GraphVertex { - /** - * Set the name of the vertex. Usually it's label on the graph display. - * @param name the new vertex name. - */ - public void setName(String name); - /** - * Get the name of the vertex. - * - * @return vertex name - */ - public String getName(); - /** - * Get the unique ID string tagged to the vertex. This was set when - * the vertex was created. - * - * @return vertex ID - */ - public String getID(); - /** - * Set an attribute on this edge. - * - * NOTE: you must also define the attribute name on the graph - * display that this graph edge will be displayed on. - * - * @param attributeName the name of the attribute - * @param value the value of the attribute - */ - public void setAttribute(String attributeName, String value); - /** - * Get the value of an attribute. - * - * @param attributeName the name of the attribute - * - * @return the string value of the attribute - */ - public String getAttribute(String attributeName); -} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/graph/package.html b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/graph/package.html deleted file mode 100644 index bb0a686a67..0000000000 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/graph/package.html +++ /dev/null @@ -1,8 +0,0 @@ - - -ghidra.program.model.graph - - -Provides interfaces for creating and managing Graphs. - - diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/service/graph/DummyGraphDisplayListener.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/service/graph/DummyGraphDisplayListener.java new file mode 100644 index 0000000000..5b388dd52d --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/service/graph/DummyGraphDisplayListener.java @@ -0,0 +1,37 @@ +/* ### + * 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.service.graph; + +import java.util.List; + +public class DummyGraphDisplayListener implements GraphDisplayListener { + + @Override + public void graphClosed() { + // I'm a dummy + } + + @Override + public void selectionChanged(List vertexIds) { + // I'm a dummy + } + + @Override + public void locationChanged(String vertexId) { + // I'm a dummy + } + +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/service/graph/GraphDisplay.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/service/graph/GraphDisplay.java new file mode 100644 index 0000000000..89fdab32e6 --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/service/graph/GraphDisplay.java @@ -0,0 +1,116 @@ +/* ### + * 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.service.graph; + +import java.util.List; + +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * Interface for objects that display (or consume) graphs. Normally, a graph display represents + * a visual component for displaying and interacting with a graph. Some implementation may not + * be a visual component, but instead consumes/processes the graph (i.e. graph exporter). In this + * case, there is no interactive element and once the graph has been set on the display, it is + * closed. + */ +public interface GraphDisplay { + public static final int ALIGN_LEFT = 0; // aligns graph text to the left + public static final int ALIGN_CENTER = 1; // aligns graph text to the center + public static final int ALIGN_RIGHT = 2; // aligns graph text to the right + + /** + * Sets a {@link GraphDisplayListener} to be notified when the user changes the vertex focus + * or selects one or more nodes in a graph window + * + * @param listener the listener to be notified + */ + public void setGraphDisplayListener(GraphDisplayListener listener); + + /** + * Tells the graph display window to focus + * + * @param vertexID the id of the vertex to focus + */ + public void setLocation(String vertexID); + + /** + * Tells the graph display window to select the vertices with the given ids + * + * @param vertexList the list of vertex ids to select + */ + public void selectVertices(List vertexList); + + /** + * Closes this graph display window. + */ + public void close(); + + /** + * Defines a vertex attribute type for this graph window + * + * @param name the name of the attribute which may be attached to vertices. + */ + public void defineVertexAttribute(String name); + + /** + * Defines an edge attribute type for this graph window + * + * @param name the name of the attribute which may be attached to edges. + */ + public void defineEdgeAttribute(String name); + + /** + * Sets the name of the attribute which should be used as the primary vertex label in the display. + * @param attributeName the name of the attribute to use as the display label for vertices. + * @param alignment (ALIGN_LEFT, ALIGN_RIGHT, or ALIGN_CENTER) + * @param size the font size to use for the display label + * @param monospace true if the font should be monospaced + * @param maxLines the maximum number lines to display in the vertex labels + */ + public void setVertexLabel(String attributeName, int alignment, int size, boolean monospace, + int maxLines); + + /** + * Sets the graph to be displayed or consumed by this graph display + * @param graph the graph to display or consume + * @param description a description of the graph + * @param monitor a {@link TaskMonitor} which can be used to cancel the graphing operation + * @param append if true, append the new graph to any existing graph. + * @throws CancelledException thrown if the graphing operation was cancelled + */ + public void setGraph(AttributedGraph graph, String description, boolean append, + TaskMonitor monitor) + throws CancelledException; + + /** + * Clears all graph vertices and edges from this graph display + */ + public void clear(); + + /** + * Updates a vertex to a new name + * @param id the vertix id + * @param newName the new name of the vertex + */ + public void updateVertexName(String id, String newName); + + /** + * Returns the description of the current graph + * @return the description of the current graph + */ + public String getGraphDescription(); +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/service/graph/GraphDisplayListener.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/service/graph/GraphDisplayListener.java new file mode 100644 index 0000000000..73886c6fc7 --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/service/graph/GraphDisplayListener.java @@ -0,0 +1,41 @@ +/* ### + * 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.service.graph; + +import java.util.List; + +/** + * Interface for being notified when the user interacts with a visual graph display. + */ +public interface GraphDisplayListener { + /** + * Notification that the graph window has been closed + */ + public void graphClosed(); + + /** + * Notification that the list of selected vertices has changed + * + * @param vertexIds the list of vertex ids for the currently selected vertices. + */ + public void selectionChanged(List vertexIds); + + /** + * Notification that the "focused" (active) vertex has changed. + * @param vertexId the vertex id of the currently "focused" vertex + */ + public void locationChanged(String vertexId); +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/service/graph/GraphDisplayProvider.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/service/graph/GraphDisplayProvider.java new file mode 100644 index 0000000000..fedf585ab5 --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/service/graph/GraphDisplayProvider.java @@ -0,0 +1,72 @@ +/* ### + * 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.service.graph; + +import ghidra.framework.options.Options; +import ghidra.framework.plugintool.PluginTool; +import ghidra.util.HelpLocation; +import ghidra.util.classfinder.ExtensionPoint; +import ghidra.util.exception.GraphException; +import ghidra.util.task.TaskMonitor; + +/** + * Basic interface for objects that can display or otherwise consume a generic graph + */ +public interface GraphDisplayProvider extends ExtensionPoint { + + /** + * The name of this provider (for displaying as menu option when graphing) + * @return the name of this provider. + */ + public String getName(); + + /** + * Returns a GraphDisplay that can be used to "display" a graph + * + * @param reuseGraph if true, this provider will attempt to re-use an existing GraphDisplay + * @param monitor the {@link TaskMonitor} that can be used to monitor and cancel the operation + * @return A GraphDisplay that can be used to display (or otherwise consume - e.g. export) the graph + * @throws GraphException thrown if there is a problem creating a GraphDisplay + */ + public GraphDisplay getGraphDisplay(boolean reuseGraph, + TaskMonitor monitor) throws GraphException; + + /** + * Provides an opportunity for this provider to register and read tool options + * + * @param tool the tool hosting this display + * @param options the tool options for graphing + */ + public void initialize(PluginTool tool, Options options); + + /** + * Called if the graph options change + * + * @param options the current tool options + */ + public void optionsChanged(Options options); + + /** + * Disposes this GraphDisplayProvider + */ + public void dispose(); + + /** + * Gets the help location for this GraphDisplayProvider + * @return help location for this GraphDisplayProvider + */ + public HelpLocation getHelpLocation(); +} diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/GraphServicesScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/GraphServicesScreenShots.java new file mode 100644 index 0000000000..c3ca2e97eb --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/GraphServicesScreenShots.java @@ -0,0 +1,101 @@ +/* ### + * 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 help.screenshot; + +import java.awt.Dimension; +import java.awt.Window; + +import org.junit.Test; + +import docking.ComponentProvider; +import ghidra.app.services.GraphDisplayBroker; +import ghidra.graph.export.GraphExporterDialog; +import ghidra.graph.visualization.DefaultGraphDisplay; +import ghidra.graph.visualization.DefaultGraphDisplayComponentProvider; +import ghidra.service.graph.*; +import ghidra.util.exception.AssertException; +import ghidra.util.task.TaskMonitor; + +public class GraphServicesScreenShots extends GhidraScreenShotGenerator { + + public GraphServicesScreenShots() { + super(); + } + + @Override + public void setUp() throws Exception { + super.setUp(); + setUser("User"); + } + + @Test + public void testExportDialog() throws Exception { + GraphDisplayBroker broker = tool.getService(GraphDisplayBroker.class); + GraphDisplayProvider export = broker.getGraphDisplayProvider("Graph Export"); + GraphDisplay display = export.getGraphDisplay(false, TaskMonitor.DUMMY); + AttributedGraph graph = new AttributedGraph(); + display.setGraph(graph, "test", false, TaskMonitor.DUMMY); + GraphExporterDialog dialog = (GraphExporterDialog) getDialog(); + dialog.setFilePath("/users/user1/graph"); + captureDialog(); + } + + @Test + public void testDefaultGraphDisplay() throws Exception { + + GraphDisplayBroker broker = tool.getService(GraphDisplayBroker.class); + GraphDisplayProvider export = broker.getGraphDisplayProvider("Default Graph Display"); + GraphDisplay display = export.getGraphDisplay(false, TaskMonitor.DUMMY); + AttributedGraph graph = new AttributedGraph(); + AttributedVertex v1 = graph.addVertex("0000", "main"); + v1.setAttribute("VertexType", "Entry"); + AttributedVertex v2 = graph.addVertex("0100", "Fun_One"); + v2.setAttribute("VertexType", "Entry"); + AttributedVertex v3 = graph.addVertex("0200", "Fun_Two"); + v3.setAttribute("VertexType", "Entry"); + + AttributedEdge e1 = graph.addEdge(v1, v2); + e1.setAttribute("EdgeType", "Unconditional-Call"); + AttributedEdge e2 = graph.addEdge(v1, v3); + e2.setAttribute("EdgeType", "Unconditional-Call"); + + display.setGraph(graph, "test", false, TaskMonitor.DUMMY); + waitForSwing(); + setGraphWindowSize(700, 500); + ((DefaultGraphDisplay) display).centerAndScale(); + + captureProvider(DefaultGraphDisplayComponentProvider.class); + } + + private void setGraphWindowSize(int width, int height) { + ComponentProvider provider = tool.getWindowManager() + .getComponentProvider(DefaultGraphDisplayComponentProvider.class); + runSwing(() -> { + Window window = tool.getWindowManager().getProviderWindow(provider); + if (window == null) { + throw new AssertException("Could not find window for " + + "provider--is it showing?: " + provider.getName()); + } + + window.setSize(new Dimension(width, height)); + window.toFront(); + provider.getComponent().requestFocus(); + paintFix(window); + }); + + } + +} diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/ProgramGraphPluginScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/ProgramGraphPluginScreenShots.java new file mode 100644 index 0000000000..f05807334b --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/ProgramGraphPluginScreenShots.java @@ -0,0 +1,118 @@ +/* ### + * 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 help.screenshot; + +import java.awt.*; + +import org.junit.Test; + +import docking.action.DockingActionIf; +import ghidra.app.util.viewer.field.*; +import ghidra.graph.visualization.DefaultGraphDisplayComponentProvider; + +public class ProgramGraphPluginScreenShots extends GhidraScreenShotGenerator { + + private static final int STARTX = 450; + private static final int NUM_LINES = 31; + private static final int WIDTH = 50; + private int lineHeight; + + @Test + public void testBasicBlockExampleCode() { + env.showTool(); + removeField(BytesFieldFactory.FIELD_NAME); + removeField(EolCommentFieldFactory.FIELD_NAME); + removeField(XRefFieldFactory.FIELD_NAME); + removeField(XRefHeaderFieldFactory.XREF_FIELD_NAME); + removeFlowArrows(); + setToolSize(1000, 1200); + captureListingRange(0x004010e0, 0x00401126, 650); + int imageHeight = image.getHeight(null); + lineHeight = imageHeight / NUM_LINES; + drawBlockLines(0, 5, "Block 1"); + drawBlockLines(5, 7, "Block 2"); + drawBlockLines(7, 16, "Block 3"); + drawBlockLines(16, 18, "Block 4"); + drawBlockLines(18, 24, "Block 5"); + drawBlockLines(24, 28, "Block 6"); + drawBlockLines(28, 31, "Block 7"); + crop(new Rectangle(20, 0, 580, imageHeight)); + } + + @Test + public void testBasicBlockGraph() { + goToListing(0x004010e0); + addSelection(0x004010e0, 0x00401126); + DockingActionIf action = getAction(tool, "ProgramGraphPlugin", "Graph Block Flow"); + performAction(action); + captureIsolatedProvider(DefaultGraphDisplayComponentProvider.class, 500, 950); + int height = image.getHeight(null); + int width = image.getWidth(null); + crop(new Rectangle(50, 50, width - 100, height - 100)); + } + + @Test + public void testCodeBlockGraph() { + goToListing(0x00401a74); + addSelection(0x00401a74, 0x00401a94); + DockingActionIf action = getAction(tool, "ProgramGraphPlugin", "Graph Code Flow"); + performAction(action); + + captureIsolatedProvider(DefaultGraphDisplayComponentProvider.class, 1000, 1000); + int height = image.getHeight(null); + int width = image.getWidth(null); + crop(new Rectangle(50, 250, width - 100, height - 500)); + } + + @Test + public void testSelectGraphNode() { + goToListing(0x40812d); + addSelection(0x0040812d, 0x040813b); + DockingActionIf action = getAction(tool, "ProgramGraphPlugin", "Graph Block Flow"); + performAction(action); + makeSelection(0x00408133, 0x0408139); + captureIsolatedProvider(DefaultGraphDisplayComponentProvider.class, 500, 750); + int height = image.getHeight(null); + int width = image.getWidth(null); + crop(new Rectangle(50, 250, width - 200, height - 260)); + } + + @Test + public void testFocusGraphNode() { + goToListing(0x40812d); + addSelection(0x0040812d, 0x040813b); + DockingActionIf action = getAction(tool, "ProgramGraphPlugin", "Graph Block Flow"); + performAction(action); + goToListing(0x408133); + captureIsolatedProvider(DefaultGraphDisplayComponentProvider.class, 500, 750); + int height = image.getHeight(null); + int width = image.getWidth(null); + crop(new Rectangle(50, 250, width - 200, height - 260)); + } + + private void drawBlockLines(int startLine, int endLine, String string) { + int startY = startLine * lineHeight; + int endY = endLine * lineHeight; + Point p1 = new Point(STARTX, startY); + Point p2 = new Point(STARTX + WIDTH, startY); + Point p3 = new Point(STARTX, endY); + Point p4 = new Point(STARTX + WIDTH, endY); + drawLine(Color.BLACK, 3, p1, p2); + drawLine(Color.BLACK, 3, p2, p4); + drawLine(Color.BLACK, 3, p3, p4); + drawText(string, Color.BLACK, new Point(STARTX + WIDTH + 10, (startY + endY) / 2), 12); + } +} diff --git a/build.gradle b/build.gradle index b48e92eead..09551a7e8a 100644 --- a/build.gradle +++ b/build.gradle @@ -55,6 +55,11 @@ allprojects { if (file("flatRepo").isDirectory()) { allprojects { repositories { + mavenLocal() + + maven { // include the standard maven snapshot location for now + url "https://oss.sonatype.org/content/repositories/snapshots" + } mavenCentral() jcenter() flatDir name: "flat", dirs:["$rootProject.projectDir/flatRepo"]