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 0000000000..bcb3db7057 Binary files /dev/null and b/Ghidra/Features/GraphServices/src/main/help/help/shared/arrow.gif differ 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 0000000000..51e1c8f8c1 Binary files /dev/null and b/Ghidra/Features/GraphServices/src/main/help/help/shared/note.png differ 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 0000000000..209f1d28c8 Binary files /dev/null and b/Ghidra/Features/GraphServices/src/main/help/help/shared/tip.png differ 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 0000000000..1e1af62e66 Binary files /dev/null and b/Ghidra/Features/ProgramGraph/src/main/help/help/topics/ProgramGraphPlugin/images/BasicBlockExampleCode.png differ diff --git a/Ghidra/Features/ProgramGraph/src/main/help/help/topics/ProgramGraphPlugin/images/BasicBlockGraph.png b/Ghidra/Features/ProgramGraph/src/main/help/help/topics/ProgramGraphPlugin/images/BasicBlockGraph.png new file mode 100644 index 0000000000..de6ca006e9 Binary files /dev/null and b/Ghidra/Features/ProgramGraph/src/main/help/help/topics/ProgramGraphPlugin/images/BasicBlockGraph.png differ 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 0000000000..91aa6af2d6 Binary files /dev/null and b/Ghidra/Features/ProgramGraph/src/main/help/help/topics/ProgramGraphPlugin/images/CodeBlockGraph.png differ 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 0000000000..0573d5fca8 Binary files /dev/null and b/Ghidra/Features/ProgramGraph/src/main/help/help/topics/ProgramGraphPlugin/images/FocusGraphNode.png differ 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 0000000000..570ed9d643 Binary files /dev/null and b/Ghidra/Features/ProgramGraph/src/main/help/help/topics/ProgramGraphPlugin/images/SelectGraphNode.png differ diff --git a/Ghidra/Features/ProgramGraph/src/main/java/ghidra/graph/program/BlockGraphTask.java b/Ghidra/Features/ProgramGraph/src/main/java/ghidra/graph/program/BlockGraphTask.java new file mode 100644 index 0000000000..188bda0f42 --- /dev/null +++ b/Ghidra/Features/ProgramGraph/src/main/java/ghidra/graph/program/BlockGraphTask.java @@ -0,0 +1,522 @@ +/* ### + * 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.awt.Color; +import java.util.*; + +import ghidra.app.plugin.core.colorizer.ColorizingService; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressSetView; +import ghidra.program.model.block.*; +import ghidra.program.model.listing.*; +import ghidra.program.model.symbol.*; +import ghidra.program.util.ProgramSelection; +import ghidra.service.graph.*; +import ghidra.util.HTMLUtilities; +import ghidra.util.Msg; +import ghidra.util.exception.CancelledException; +import ghidra.util.exception.GraphException; +import ghidra.util.task.Task; +import ghidra.util.task.TaskMonitor; + +/** + * GraphTask 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"]