GP-5481 Created prototype data graph feature

This commit is contained in:
ghidragon
2025-07-02 13:20:47 -04:00
parent 8d95e97521
commit f54bd20d40
102 changed files with 9267 additions and 366 deletions
+1
View File
@@ -31,6 +31,7 @@ dependencies {
api project(':DecompilerDependent')
api project(':FunctionGraph')
api project(':ProposedUtils')
api project(':DataGraph')
testImplementation project(path: ':Generic', configuration: 'testArtifacts')
testImplementation project(path: ':Base', configuration: 'testArtifacts')
@@ -0,0 +1,76 @@
/* ###
* 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.debug.gui.graph.data;
import datagraph.AbstractDataGraphPlugin;
import ghidra.app.context.ListingActionContext;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.app.plugin.core.debug.event.TraceLocationPluginEvent;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.program.util.ProgramLocation;
/**
* Plugin for showing a graph of data from the listing.
*/
//@formatter:off
@PluginInfo(
status = PluginStatus.RELEASED,
packageName = DebuggerPluginPackage.NAME,
category = PluginCategoryNames.DEBUGGER,
shortDescription = "Debugger Data Graph",
description = """
Plugin for displaying graphs of data objects in memory. From any data object in the
listing, the user can display a graph of that data object. Initially, a graph will be shown
with one vertex that has a scrollable view of the values in memory associated with that data.
Also, any pointers or references from or to that data can be explored by following the
references and creating additional vertices for the referenced code or data.
""",
eventsConsumed = {
TraceLocationPluginEvent.class,
},
eventsProduced = {
TraceLocationPluginEvent.class,
}
)
//@formatter:on
public class DebuggerDataGraphPlugin extends AbstractDataGraphPlugin {
public DebuggerDataGraphPlugin(PluginTool plugintool) {
super(plugintool);
}
@Override
public void processEvent(PluginEvent event) {
if (event instanceof TraceLocationPluginEvent ev) {
ProgramLocation location = ev.getLocation();
goTo(location);
}
}
@Override
public void fireLocationEvent(ProgramLocation location) {
firePluginEvent(new TraceLocationPluginEvent(getName(), location));
}
@Override
protected boolean isGraphActionEnabled(ListingActionContext context) {
if (!context.getNavigatable().isDynamic()) {
return false;
}
return super.isGraphActionEnabled(context);
}
}
@@ -73,6 +73,7 @@ public class CircleWithLabelVertexShapeProvider implements VertexShapeProvider {
protected boolean useDebugBorders = false;
private String fullLabelText;
private int circleCenterYOffset;
public CircleWithLabelVertexShapeProvider(String label) {
this.fullLabelText = label;
@@ -85,6 +86,10 @@ public class CircleWithLabelVertexShapeProvider implements VertexShapeProvider {
buildUi();
}
public int getCircleCenterYOffset() {
return circleCenterYOffset;
}
protected void buildUi() {
String name = generateLabelText();
@@ -201,6 +206,7 @@ public class CircleWithLabelVertexShapeProvider implements VertexShapeProvider {
vertexImageLabel.setBounds(x, y, size.width, size.height);
Dimension shapeSize = vertexShape.getBounds().getSize();
circleCenterYOffset = shapeSize.height / 2 - parentSize.height / 2;
// setFrame() will make sure the shape's x,y values are where they need to be
// for the later 'full shape' creation
@@ -332,7 +338,7 @@ public class CircleWithLabelVertexShapeProvider implements VertexShapeProvider {
return false;
}
protected void setTogglesVisible(boolean visible) {
public void setTogglesVisible(boolean visible) {
toggleInsButton.setVisible(visible);
toggleOutsButton.setVisible(visible);
}
@@ -397,6 +397,50 @@ public class ProgramBuilder {
}
}
public void setString(String address, String string) throws Exception {
byte[] bytes = string.getBytes();
setBytes(address, bytes);
}
public void setShort(String address, short value) throws Exception {
DataConverter converter = getDataConverter();
byte[] bytes = converter.getBytes(value);
setBytes(address, bytes);
}
public void setInt(String address, int value) throws Exception {
DataConverter converter = getDataConverter();
byte[] bytes = converter.getBytes(value);
setBytes(address, bytes);
}
public void setLong(String address, long value) throws Exception {
DataConverter converter = getDataConverter();
byte[] bytes = converter.getBytes(value);
setBytes(address, bytes);
}
public void putAddress(String address, String pointerAddress) throws Exception {
Address pointer = addr(pointerAddress);
long offset = pointer.getOffset();
int pointerSize = pointer.getAddressSpace().getPointerSize();
switch (pointerSize) {
case 2:
setShort(address, (short) offset);
break;
case 4:
setInt(address, (int) offset);
break;
default:
setLong(address, offset);
}
}
private DataConverter getDataConverter() {
boolean bigEndian = program.getMemory().isBigEndian();
return bigEndian ? BigEndianDataConverter.INSTANCE : LittleEndianDataConverter.INSTANCE;
}
public void setRead(MemoryBlock block, boolean r) {
tx(() -> block.setRead(r));
}
@@ -0,0 +1 @@
EXCLUDE FROM GHIDRA JAR: true
+34
View File
@@ -0,0 +1,34 @@
/* ###
* 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.
*/
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 Data Graph'
// Note: this module's name is 'Data Graph'
dependencies {
api project(":Base")
// These have abstract test classes and stubs needed by this module
testImplementation project(path: ':Project', configuration: 'testArtifacts')
testImplementation project(path: ':SoftwareModeling', configuration: 'testArtifacts')
}
@@ -0,0 +1,8 @@
##VERSION: 2.0
Module.manifest||GHIDRA||||END|
data/datagraph.theme.properties||GHIDRA||||END|
src/main/help/help/TOC_Source.xml||GHIDRA||||END|
src/main/help/help/topics/DataGraphPlugin/Data_Graph.html||GHIDRA||||END|
src/main/help/help/topics/DataGraphPlugin/images/CodeVertex.png||GHIDRA||||END|
src/main/help/help/topics/DataGraphPlugin/images/DataGraph.png||GHIDRA||||END|
src/main/resources/images/view_detailed_16.png||Nuvola Icons - LGPL 2.1|||Nuvola icon set|END|
@@ -0,0 +1,4 @@
[Defaults]
color.fg.datagraph.value = color.palette.blue
icon.plugin.datagraph.action.viewer.vertex.format = view_detailed_16.png
icon.plugin.datagraph.action.viewer.reset = icon.refresh
@@ -0,0 +1,64 @@
<?xml version='1.0' encoding='ISO-8859-1' ?>
<!--
This is an XML file intended to be parsed by the Ghidra help system. It is loosely based
upon the JavaHelp table of contents document format. The Ghidra help system uses a
TOC_Source.xml file to allow a module with help to define how its contents appear in the
Ghidra help viewer's table of contents. The main document (in the Base module)
defines a basic structure for the
Ghidra table of contents system. Other TOC_Source.xml files may use this structure to insert
their files directly into this structure (and optionally define a substructure).
In this document, a tag can be either a <tocdef> or a <tocref>. The former is a definition
of an XML item that may have a link and may contain other <tocdef> and <tocref> children.
<tocdef> items may be referred to in other documents by using a <tocref> tag with the
appropriate id attribute value. Using these two tags allows any module to define a place
in the table of contents system (<tocdef>), which also provides a place for
other TOC_Source.xml files to insert content (<tocref>).
During the help build time, all TOC_Source.xml files will be parsed and validated to ensure
that all <tocref> tags point to valid <tocdef> tags. From these files will be generated
<module name>_TOC.xml files, which are table of contents files written in the format
desired by the JavaHelp system. Additionally, the genated files will be merged together
as they are loaded by the JavaHelp system. In the end, when displaying help in the Ghidra
help GUI, there will be on table of contents that has been created from the definitions in
all of the modules' TOC_Source.xml files.
Tags and Attributes
<tocdef>
-id - the name of the definition (this must be unique across all TOC_Source.xml files)
-text - the display text of the node, as seen in the help GUI
-target** - the file to display when the node is clicked in the GUI
-sortgroup - this is a string that defines where a given node should appear under a given
parent. The string values will be sorted by the JavaHelp system using
a javax.text.RulesBasedCollator. If this attribute is not specified, then
the text of attribute will be used.
<tocref>
-id - The id of the <tocdef> that this reference points to
**The URL for the target is relative and should start with 'help/topics'. This text is
used by the Ghidra help system to provide a universal starting point for all links so that
they can be resolved at runtime, across modules.
-->
<tocroot>
<tocref id="Graphing">
<tocdef id="Data Graph" text="Data Graph" target="help/topics/DataGraphPlugin/Data_Graph.html" >
<tocdef id="Vertices" sortgroup="a" text="Vertices" target="help/topics/DataGraphPlugin/Data_Graph.html#Vertices"/>
<tocdef id="Graph Actions" sortgroup="b" text="Graph Actions" target="help/topics/DataGraphPlugin/Data_Graph.html#Data_Graph_Actions" />
<tocdef id="Popups" sortgroup="c" text="Popups" target="help/topics/DataGraphPlugin/Data_Graph.html#Popups" />
<tocdef id="Layout" sortgroup="d" text="Layout" target="help/topics/DataGraphPlugin/Data_Graph.html#Layout" />
</tocdef>
</tocref>
</tocroot>
@@ -0,0 +1,250 @@
<!DOCTYPE doctype PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN">
<HTML>
<HEAD>
<META name="generator" content=
"HTML Tidy for Java (vers. 2009-12-01), see jtidy.sourceforge.net">
<TITLE>Function Graph Plugin</TITLE>
<META http-equiv="Content-Type" content="text/html; charset=windows-1252">
<LINK rel="stylesheet" type="text/css" href="help/shared/DefaultStyle.css">
</HEAD>
<BODY lang="EN-US">
<H1><A name="Data_Graph"></A> <A name="DataGraphPlugin"></A>Data Graph</H1>
<TABLE x-use-null-cells="" width="100%">
<TBODY>
<TR>
<TD align="center" width="100%"><IMG alt="" border="1" src="images/DataGraph.png"></TD>
</TR>
</TBODY>
</TABLE>
<BLOCKQUOTE>
<P>The data graph is a graph display that shows data objects in memory. Each data vertex
displays a scrollable view of a data object and its contents.</P>
<P>Initially, a data graph is generated from a specific data program location. The graph
will be populated with one data vertex showing the data for that location. From this original
source vertex, the references to and from this vertex can be explored to add additional
vertices to the graph. In the data vertex display, any data elements that have an outgoing
reference will have an <IMG src="Icons.RIGHT_ICON" alt="" border="0"> icon that can be clicked
to quickly explore that reference.</P>
<P>If a reference leads from/to code, a simple code vertex is generated
that simply shows the function and offset of the reference from the function's entry point.
For the purposes of the data graph, code vertices are end points and cannot be explored
further.</P>
<P>The display consists of the <A href="#Primary_View">Primary View</A> and an optional <A
href="help/topics/VisualGraph/Visual_Graph.html#Satellite_View">Satellite View</A>.</P>
</BLOCKQUOTE>
<H2><A name="Primary_View"></A>Primary View</H2>
<BLOCKQUOTE>
<P>The primary view shows an initial data object that was used to create the graph. This
data vertex can be used to explore references and pointers. As you explore, new vertices
will be added to the graph. All vertices in the graph can trace a reference relationship
back to the original source data object.</P>
<BLOCKQUOTE>
<P><IMG src="help/shared/tip.png" alt="" border="0">The original source data vertex
has a <IMG src="Icons.HOME_ICON" alt="" border="0"> icon in its header to indicate it is the original source vertex.</P>
</BLOCKQUOTE>
</BLOCKQUOTE>
<H2><A name="Vertices"></A>Vertices</H2>
<BLOCKQUOTE>
<H3>Data Vertices</H3>
<BLOCKQUOTE>
<P>Data vertices show information about the data and values they contain. If the vertex
datatype is a primitive, such as an int, the vertex contains only one row that shows its name
and value. If the data is more complex such as a structure or array, the vertex will display
multiple rows showing the name and value of each field in the structure. If the structure
contains other structures or arrays of data, those data items can be expanded in a tree/table
structure showing the names and values of that internal data.</P>
<P>Any elements that are pointers (or have attached outgoing references) will display a
<IMG src="Icons.RIGHT_ICON" alt="" border="0"> icon that can be clicked to explore those
references to add new vertices.</P>
<P>Data vertices can be resized by dragging the bottom right corner. Also, the interior column
sizes can be adjusted by hovering the mouse on a column boundary and dragging left or
right.</P>
<P>The header of a vertex contains the name of the label and/or address of the data object.
The header also contains buttons that allow you to perform some common operations on the
vertex.</P>
<P>As long as you are within the <A href="help/topics/VisualGraph/Visual_Graph.html#Interaction_Threshold">interaction threshold</A>,
you may interact with the vertex to expand/collapse sub-data elements and to click on any
pointer references to add vertices to the graph.</P>
</BLOCKQUOTE>
<H3>Code Vertices</H3>
<BLOCKQUOTE>
<P>Generally vertices in the graph show data objects, but a vertex can also represent a
reference from or to code. In the data graph, code vertices are terminal vertices, simply
showing the function name and offset into or out of that function.</P>
<TABLE x-use-null-cells="" width="100%">
<TBODY>
<TR>
<TD align="center" width="100%"><IMG alt="" border="1" src=
"images/CodeVertex.png"></TD>
</TR>
</TBODY>
</TABLE>
<P>In the image above, the graph is showing two references to the same string.</P>
</BLOCKQUOTE>
<H3><A name="Layout">Vertex Layout</A></H3>
<BLOCKQUOTE>
<P>The data graph uses a special layout to attempt to maintain a logical structure as
more vertices are added to the graph. This layout uses the exploration order
to layout the vertices.</P>
<P>Starting with the original data vertex. All vertices that were
added to the graph by following outgoing references are displayed in a column to the right of
the vertex. Within this column, the vertices are ordered by the order they are first referenced
from the source vertex.</P>
<P>Similarly, vertices added to the graph by following incoming references are shown in a
column to the left of the source vertex. Within this column, the vertices are ordered by
address.</P>
<P>Additional layers of vertices can be added by further exploring the child vertices and their
descendants. These additional vertices are ordered in the same way relative to their immediate
source vertex.</P>
<P>Whenever a vertex is removed from the graph, all vertices that were discovered by exploring
from that vertex are also removed.</P>
</BLOCKQUOTE>
<H3>Vertex Actions</H3>
<BLOCKQUOTE>
<P>The following toolbar actions are available on a data vertex.</P>
<UL>
<LI><A name="Expand_All"><IMG alt="" src="Icons.EXPAND_ALL_ICON">&nbsp<B> Expand All</B> -
Expands all expandable sub-data elements recursively contained in the data object. Note
that this action is not available if the data has no sub-data elements.</A></LI>
<LI><A name="Collapse_All"><IMG alt="" src="Icons.COLLAPSE_ALL_ICON">&nbsp<B>Collapse All</B>
- Collapses all expanded sub-data elements. Note that this action is not available if the
data has no sub-data elements.</A></LI>
<LI><A name="Delete_Vertex"><IMG alt="" src="Icons.CLOSE_ICON">&nbsp<B>Delete Vertex</B> - Removes this vertex and all vertices
that descend from this vertex.</A></LI>
</UL>
<P><A name="Popups">The following popup actions are are available depending on where the
mouse is positioned when the popup is triggered.</A></P>
<UL>
<LI><A name="Add_Outgoing"><B>Add All Outgoing References</B> - All outgoing references
from this data element or its sub-elements will generate a new vertex in the graph, if
not already present.</A></LI>
<LI><A name="Add_Incoming"><B>Add All Incoming References</B> - The program will be
searched for any references to this data element or its sub-elents and a new vertex be
created each discovered reference, if not already present.</A></LI>
<LI><A name="Expand_Fully"><B>Expand Fully</B> - If the mouse is over an
expandable row in data vertex, the vertex and all it's child elements will be fully
expanded.</A></LI>
<LI><A name="Original_Source"><B>Set Original Source</B> - Makes this the original source
root vertex. All other vertices are reorganized and laid out as if they were discovered
by following references from this vertex.See</A> <A href="#Layout">Vertex
Layout.</A></LI>
<LI><A name="Delete_Selected"><B>Delete Selected Vertices</B> - Deletes the selected
vertices and any descendants vertices (vertices that were discovered via exploring from
that vertex.)</A></LI>
</UL>
</BLOCKQUOTE>
<H3>Selecting Vertices</H3>
<BLOCKQUOTE>
<P>Left-clicking a vertex will select that vertex. To select multiple vertices, hold down
the <B><TT>Ctrl</TT></B> key while clicking. To deselect
a block, hold the <TT>Ctrl</TT> key while clicking the block. To clear all selected
blocks, click in an empty area of the primary view. When selected, a block is adorned
with a halo.</P>
<P>You may also select multiple vertices in one action by holding the <B><TT>Ctrl</TT></B> key
while performing a drag operation. Press the <B><TT>Ctrl</TT></B> key and start the drag in an
empty area of the primary view (not over a vertex). This will create a bounding rectangle
on the screen that will select any vertices contained therein when the action is
finished.</P>
</BLOCKQUOTE>
<H3>Navigating Vertices</H3>
<BLOCKQUOTE>
<P>By default the data graph can be used to navigate the main views of the tool. Clicking
on a vertex or a row within a vertex will generate a goto event for that address, causing
the main tool to navigate to that location. See the actions below to turn off this
behavior.</P>
<P>Also, the data graph can listen for tool location changes and select the appropriate
vertex in the graph if any vertices contain the tool location address. This behavior is
off by default, but can be turn on via a popup action.</P>
</BLOCKQUOTE>
</BLOCKQUOTE>
<H2><A name="Data_Graph_Actions"></A>Data Graph Actions</H2>
<BLOCKQUOTE>
<H3>Toolbar Actions</H3>
<UL>
<LI><A name="Relayout_Graph"><IMG alt="" src=
"icon.plugin.datagraph.action.viewer.reset">&nbsp<B>Refresh Layout</B> - All manually positioned
vertices will be reset and the graph will relayout to its automated locations.</A></LI>
<LI><A name="Select_Home_Vertex"><IMG alt="" src=
"Icons.HOME_ICON">&nbsp<B>Go To Source Vertex</B> - The original source vertex will be selected
and centered in the graph.</A></LI>
<LI><A name="Navigate_In"><IMG alt="" src=
"Icons.NAVIGATE_ON_INCOMING_EVENT_ICON">&nbsp<B>Navigate In</B> - If selected, the graph will
listen for tool location events and select the vertex that contains the location address,
is one exists.</A></LI>
<LI><A name="Navigate_Out"><IMG alt="" src=
"Icons.NAVIGATE_ON_OUTGOING_EVENT_ICON">&nbsp<B>Navigate Out</B> - If selected, the graph will
generate tool location events when vertices are selected or rows within a vertex are
selected.</A></LI>
<LI><A name="Expanded_Format"><IMG alt="" src=
"icon.plugin.datagraph.action.viewer.vertex.format">&nbsp<B>Expanded Format</B> - If selected,
vertices will show more information for each row in the display. In compact mode, a data
row will generally show the field name and its value. In expanded mode, a data row will
generally show the datatype, field name, and its value.</A></LI>
</UL>
</BLOCKQUOTE
<H2>Standard Graph Features and Actions</H2>
<BLOCKQUOTE>
<P>The data graph is a type of Ghidra Visual Graph and has some standard concepts, features
and actions.
<BLOCKQUOTE>
<UL>
<LI><A href="help/topics/VisualGraph/Visual_Graph.html#Satellite_View">Satellite View</A></LI>
<LI><A href="help/topics/VisualGraph/Visual_Graph.html#Pan">Panning</A></LI>
<LI><A href="help/topics/VisualGraph/Visual_Graph.html#Zoom">Zooming</A></LI>
<LI><A href="help/topics/VisualGraph/Visual_Graph.html#Interaction_Threshold">Interaction Threshold</A></LI>
</UL>
</BLOCKQUOTE>
</BLOCKQUOTE>
<P class="providedbyplugin">Provided by: <I>Data Graph Plugin</I></P>
<BR>
<BR>
</BODY>
</HTML>
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

@@ -0,0 +1,84 @@
/* ###
* 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 datagraph;
import java.util.HashSet;
import java.util.Set;
import docking.action.builder.ActionBuilder;
import ghidra.app.context.ListingActionContext;
import ghidra.app.plugin.ProgramPlugin;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.listing.Data;
import ghidra.program.util.ProgramLocation;
import ghidra.util.HelpLocation;
/**
* Base class for plugins that show a graph of data from program.
*/
public abstract class AbstractDataGraphPlugin extends ProgramPlugin {
private Set<DataGraphProvider> activeProviders = new HashSet<>();
public AbstractDataGraphPlugin(PluginTool plugintool) {
super(plugintool);
createActions();
}
public void goTo(ProgramLocation location) {
activeProviders.forEach(p -> p.goTo(location));
}
private void createActions() {
new ActionBuilder("Display Data Graph", getName())
.popupMenuPath("Data", "Display Data Graph")
.keyBinding("ctrl G")
.helpLocation(new HelpLocation("DataGraphPlugin", "Data_Graph"))
.withContext(ListingActionContext.class)
.enabledWhen(this::isGraphActionEnabled)
.onAction(this::showDataGraph)
.buildAndInstall(tool);
}
protected boolean isGraphActionEnabled(ListingActionContext context) {
return context.getCodeUnit() instanceof Data;
}
private void showDataGraph(ListingActionContext context) {
Data data = (Data) context.getCodeUnit();
// the data from the context may be an internal sub-data, we want the outermost data.
data = getTopLevelData(data);
DataGraphProvider provider = new DataGraphProvider(this, data);
activeProviders.add(provider);
tool.showComponentProvider(provider, true);
}
private Data getTopLevelData(Data data) {
Data parent = data.getParent();
while (parent != null) {
data = parent;
parent = data.getParent();
}
return data;
}
void removeProvider(DataGraphProvider provider) {
activeProviders.remove(provider);
}
public abstract void fireLocationEvent(ProgramLocation location);
}
@@ -0,0 +1,75 @@
/* ###
* 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 datagraph;
import ghidra.app.CorePluginPackage;
import ghidra.app.context.ListingActionContext;
import ghidra.app.events.ProgramLocationPluginEvent;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.program.util.ProgramLocation;
/**
* Plugin for showing a graph of data from the listing.
*/
//@formatter:off
@PluginInfo(
status = PluginStatus.RELEASED,
packageName = CorePluginPackage.NAME,
category = PluginCategoryNames.GRAPH,
shortDescription = "Data Graph",
description = """
Plugin for displaying graphs of data objects in memory. From any data object in the
listing, the user can display a graph of that data object. Initially, a graph will be shown
with one vertex that has a scrollable view of the values in memory associated with that data.
Also, any pointers or references from or to that data can be explored by following the
references and creating additional vertices for the referenced code or data.
""",
eventsConsumed = {
ProgramLocationPluginEvent.class,
},
eventsProduced = {
ProgramLocationPluginEvent.class,
}
)
//@formatter:on
public class DataGraphPlugin extends AbstractDataGraphPlugin {
public DataGraphPlugin(PluginTool plugintool) {
super(plugintool);
}
@Override
public void processEvent(PluginEvent event) {
if (event instanceof ProgramLocationPluginEvent ev) {
ProgramLocation location = ev.getLocation();
goTo(location);
}
}
@Override
public void fireLocationEvent(ProgramLocation location) {
firePluginEvent(new ProgramLocationPluginEvent(getName(), location, location.getProgram()));
}
@Override
protected boolean isGraphActionEnabled(ListingActionContext context) {
if (context.getNavigatable().isDynamic()) {
return false;
}
return super.isGraphActionEnabled(context);
}
}
@@ -0,0 +1,296 @@
/* ###
* 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 datagraph;
import java.awt.BorderLayout;
import java.awt.event.MouseEvent;
import java.util.Set;
import javax.swing.JComponent;
import javax.swing.JPanel;
import datagraph.data.graph.*;
import docking.ActionContext;
import docking.ComponentProvider;
import docking.action.ToggleDockingAction;
import docking.action.builder.ActionBuilder;
import docking.action.builder.ToggleActionBuilder;
import generic.theme.GIcon;
import ghidra.graph.VisualGraphComponentProvider;
import ghidra.graph.viewer.*;
import ghidra.graph.viewer.GraphComponent.SatellitePosition;
import ghidra.graph.viewer.event.mouse.VertexMouseInfo;
import ghidra.program.model.listing.Data;
import ghidra.program.model.listing.Program;
import ghidra.program.util.ProgramLocation;
import ghidra.util.HelpLocation;
import ghidra.util.exception.AssertException;
import resources.Icons;
/**
* A {@link ComponentProvider} that is the UI component of the {@link DataGraphPlugin}. This
* shows a graph of a Data object in memory and its referenced objects.
*/
public class DataGraphProvider
extends VisualGraphComponentProvider<DegVertex, DegEdge, DataExplorationGraph> {
private static final GIcon DETAILS_ICON =
new GIcon("icon.plugin.datagraph.action.viewer.vertex.format");
private static final GIcon RESET_ICON = new GIcon("icon.plugin.datagraph.action.viewer.reset");
private static final String NAME = "Data Graph";
private AbstractDataGraphPlugin plugin;
private JPanel mainPanel;
private DegController controller;
private ToggleDockingAction navagateInAction;
private ToggleDockingAction navagateOutAction;
private ToggleDockingAction expandedFormatAction;
/**
* Constructor
* @param plugin the DataGraphPlugin
* @param data the initial data object to display in the graph.
*/
public DataGraphProvider(AbstractDataGraphPlugin plugin, Data data) {
super(plugin.getTool(), NAME, plugin.getName());
this.plugin = plugin;
controller = new DegController(this, data);
createActions();
setTransient();
buildComponent();
addToTool();
addSatelliteFeature(false, SatellitePosition.LOWER_LEFT);
setHelpLocation(new HelpLocation("DataGraphPlugin", "DataGraphPlugin"));
}
private void buildComponent() {
mainPanel = new JPanel(new BorderLayout());
mainPanel.add(controller.getComponent());
}
@Override
public VisualGraphView<DegVertex, DegEdge, DataExplorationGraph> getView() {
return controller.getView();
}
@Override
public ActionContext getActionContext(MouseEvent event) {
Set<DegVertex> selectedVertices = getSelectedVertices();
if (event == null) {
return new DegContext(this, controller.getFocusedVertex(),
selectedVertices);
}
Object source = event.getSource();
if (source instanceof SatelliteGraphViewer) {
return new DegSatelliteContext(this);
}
if (source instanceof GraphViewer) {
@SuppressWarnings("unchecked")
GraphViewer<DegVertex, DegEdge> viewer = (GraphViewer<DegVertex, DegEdge>) source;
VertexMouseInfo<DegVertex, DegEdge> vertexMouseInfo =
GraphViewerUtils.convertMouseEventToVertexMouseEvent(viewer, event);
DegVertex target = vertexMouseInfo != null ? vertexMouseInfo.getVertex() : null;
return new DegContext(this, target, selectedVertices, vertexMouseInfo);
}
throw new AssertException(
"Received mouse event from unexpected source in getActionContext(): " + source);
}
@Override
public void dispose() {
plugin.removeProvider(this);
controller.dispose();
super.dispose();
removeFromTool();
}
@Override
public JComponent getComponent() {
return mainPanel;
}
@Override
public void closeComponent() {
super.closeComponent();
dispose();
}
public Program getProgram() {
return controller.getProgram();
}
public DegController getController() {
return controller;
}
private void createActions() {
new ActionBuilder("Select Home Vertex", plugin.getName())
.toolBarIcon(Icons.HOME_ICON)
.toolBarGroup("A")
.description("Selects and Centers Original Source Vertx")
.onAction(c -> controller.selectAndCenterHomeVertex())
.buildAndInstallLocal(this);
new ActionBuilder("Relayout Graph", plugin.getName())
.toolBarIcon(RESET_ICON)
.toolBarGroup("A")
.description("Erases all manual vertex positioning information")
.onAction(c -> controller.resetAndRelayoutGraph())
.buildAndInstallLocal(this);
expandedFormatAction = new ToggleActionBuilder("Show Expanded Format", plugin.getName())
.toolBarIcon(DETAILS_ICON)
.toolBarGroup("A")
.description("Show Expanded information in data vertices.")
.helpLocation(new HelpLocation("DataGraphPlugin", "Expanded_Format"))
.onAction(c -> controller.setCompactFormat(!expandedFormatAction.isSelected()))
.buildAndInstallLocal(this);
navagateInAction =
new ToggleActionBuilder("Navigate on Incoming Location Changes", plugin.getName())
.sharedKeyBinding()
.toolBarIcon(Icons.NAVIGATE_ON_INCOMING_EVENT_ICON)
.toolBarGroup("B")
.description("Attemps to select vertex corresponding to tool location changes.")
.helpLocation(new HelpLocation("DataGraphPlugin", "Navigate_In"))
.onAction(c -> controller.setNavigateIn(navagateInAction.isSelected()))
.buildAndInstallLocal(this);
// this name is same as SelectionNavigationAction which allows sharing of keybinding
navagateOutAction = new ToggleActionBuilder("Selection Navigation Action", plugin.getName())
.toolBarIcon(Icons.NAVIGATE_ON_OUTGOING_EVENT_ICON)
.toolBarGroup("B")
.sharedKeyBinding()
.description(
"Selecting vetices or locations inside a vertex sends navigates the tool.")
.helpLocation(new HelpLocation("DataGraphPlugin", "Navigate_Out"))
.onAction(c -> controller.setNavigateOut(navagateOutAction.isSelected()))
.selected(true)
.buildAndInstallLocal(this);
new ActionBuilder("Incoming References", plugin.getName())
.popupMenuPath("Add All Incoming References")
.popupMenuGroup("A", "2")
.description("Show Vertices for known references to this vertex.")
.helpLocation(new HelpLocation("DataGraphPlugin", "Add Incoming"))
.withContext(DegContext.class)
.enabledWhen(c -> canShowReferences(c.getVertex()))
.onAction(c -> controller.showAllIncommingReferences((DataDegVertex) c.getVertex()))
.buildAndInstallLocal(this);
new ActionBuilder("Outgoing References", plugin.getName())
.popupMenuPath("Add All Outgoing References")
.popupMenuGroup("A", "1")
.description("Show Vertices for known references to this vertex.")
.helpLocation(new HelpLocation("DataGraphPlugin", "Add Outgoing"))
.withContext(DegContext.class)
.enabledWhen(c -> canShowReferences(c.getVertex()))
.onAction(c -> controller.showAllOutgoingReferences((DataDegVertex) c.getVertex()))
.buildAndInstallLocal(this);
new ActionBuilder("Delete Vertices", plugin.getName())
.popupMenuPath("Delete Selected Vertices")
.popupMenuGroup("B", "1")
.description("Removes the selected vertices and their descendents from the graph")
.helpLocation(new HelpLocation("DataGraphPlugin", "Delete_Selected"))
.withContext(DegContext.class)
.enabledWhen(c -> canClose(c.getSelectedVertices()))
.onAction(c -> controller.deleteVertices(c.getSelectedVertices()))
.buildAndInstallLocal(this);
new ActionBuilder("Set Original Vertex", plugin.getName())
.popupMenuPath("Set Vertex as Original Source")
.popupMenuGroup("B", "2")
.description("Reorient graph as though this was the first vertex shown")
.helpLocation(new HelpLocation("DataGraphPlugin", "Original_Source"))
.withContext(DegContext.class)
.enabledWhen(c -> canOrientGraphAround(c.getVertex()))
.onAction(c -> controller.orientAround(c.getVertex()))
.buildAndInstallLocal(this);
new ActionBuilder("Reset Vertex Location", plugin.getName())
.popupMenuPath("Restore Location")
.popupMenuGroup("B", "3")
.popupMenuIcon(Icons.REFRESH_ICON)
.description("Resets the vertex to the automated layout location.")
.helpLocation(new HelpLocation("DataGraphPlugin", "Reset_Location"))
.withContext(DegContext.class)
.enabledWhen(c -> c.getVertex() != null && c.getVertex().hasUserChangedLocation())
.onAction(c -> c.getVertex().clearUserChangedLocation())
.buildAndInstallLocal(this);
new ActionBuilder("Expand Fully", plugin.getName())
.popupMenuPath("Expand Fully")
.popupMenuGroup("C", "1")
.description("Expand all levels under selected row")
.helpLocation(new HelpLocation("DataGraphPlugin", "Expand_Fully"))
.withContext(DegContext.class)
.enabledWhen(this::canExpandRecursively)
.onAction(this::expandRecursively)
.buildAndInstallLocal(this);
}
private boolean canOrientGraphAround(DegVertex vertex) {
if (vertex instanceof DataDegVertex) {
return !vertex.isRoot();
}
return false;
}
private boolean canShowReferences(DegVertex vertex) {
return vertex instanceof DataDegVertex;
}
private boolean canClose(Set<DegVertex> selectedVertices) {
if (selectedVertices.isEmpty()) {
return false;
}
if (selectedVertices.size() > 1) {
return true;
}
// Special case for just one vertex selected. Can't delete the root vertex.
DegVertex v = selectedVertices.iterator().next();
return !v.isRoot();
}
void goTo(ProgramLocation location) {
controller.locationChanged(location);
}
private boolean canExpandRecursively(DegContext context) {
DegVertex vertex = context.getVertex();
if (vertex instanceof DataDegVertex dataVertex) {
return dataVertex.isOnExpandableRow();
}
return false;
}
private void expandRecursively(DegContext context) {
DataDegVertex vertex = (DataDegVertex) context.getVertex();
vertex.expandSelectedRowRecursively();
}
public void navigateOut(ProgramLocation location) {
plugin.fireLocationEvent(location);
}
}
@@ -0,0 +1,54 @@
/* ###
* 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 datagraph;
import java.util.Set;
import datagraph.data.graph.DegEdge;
import datagraph.data.graph.DegVertex;
import docking.ActionContext;
import ghidra.graph.viewer.actions.VgVertexContext;
import ghidra.graph.viewer.event.mouse.VertexMouseInfo;
/**
* {@link ActionContext} for the data exploration graph.
*/
public class DegContext extends VgVertexContext<DegVertex> {
private Set<DegVertex> selectedVertices;
public DegContext(DataGraphProvider dataGraphProvider, DegVertex targetVertex,
Set<DegVertex> selectedVertices) {
this(dataGraphProvider, targetVertex, selectedVertices, null);
}
public DegContext(DataGraphProvider dataGraphProvider,
DegVertex targetVertex, Set<DegVertex> selectedVertices,
VertexMouseInfo<DegVertex, DegEdge> vertexMouseInfo) {
super(dataGraphProvider, targetVertex);
this.selectedVertices = selectedVertices;
}
public Set<DegVertex> getSelectedVertices() {
return selectedVertices;
}
@Override
public boolean shouldShowSatelliteActions() {
return getVertex() == null;
}
}
@@ -0,0 +1,32 @@
/* ###
* 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 datagraph;
import docking.ActionContext;
import ghidra.app.context.ProgramActionContext;
import ghidra.graph.viewer.actions.VisualGraphActionContext;
/**
* {@link ActionContext} for the data exploration satellite graph.
*/
public class DegSatelliteContext extends ProgramActionContext
implements VisualGraphActionContext {
public DegSatelliteContext(DataGraphProvider dataGraphProvider) {
super(dataGraphProvider, dataGraphProvider.getProgram());
}
}
@@ -0,0 +1,181 @@
/* ###
* 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 datagraph.data.graph;
import java.awt.Component;
import java.awt.Shape;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;
import javax.swing.JComponent;
import datagraph.graph.explore.EgVertex;
import docking.action.DockingAction;
import ghidra.base.graph.CircleWithLabelVertexShapeProvider;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.*;
/**
* A vertex that represents code in a data exploration graph. Currently, code vertices are
* "dead end" vertices in the graph and cannot be explored further.
*/
public class CodeDegVertex extends DegVertex {
private Instruction instruction;
private CircleWithLabelVertexShapeProvider shapeProvider;
/**
* Constructor
* @param controller the graph controller
* @param instruction the instruction that is reference from/to a data object in the graph.
* @param parent the source vertex (from what vertex did you explore to get here)
*/
public CodeDegVertex(DegController controller, Instruction instruction, DegVertex parent) {
super(controller, parent);
this.instruction = instruction;
String label = getVertexLabel();
this.shapeProvider = new CircleWithLabelVertexShapeProvider(label);
shapeProvider.setTogglesVisible(false);
}
@Override
public String getTitle() {
return null;
}
@Override
public int hashCode() {
return instruction.hashCode();
}
@Override
public DegVertexStatus refreshGraph(boolean checkDataTypes) {
return DegVertexStatus.VALID;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
CodeDegVertex other = (CodeDegVertex) obj;
return instruction.equals(other.instruction);
}
private String getVertexLabel() {
Address address = instruction.getAddress();
Program program = instruction.getProgram();
FunctionManager functionManager = program.getFunctionManager();
Function f = functionManager.getFunctionContaining(address);
if (f != null) {
String name = f.getName();
if (!f.getEntryPoint().equals(address)) {
name += " + " + address.subtract(f.getEntryPoint());
}
return name;
}
return address.toString();
}
@Override
public void clearUserChangedLocation() {
super.clearUserChangedLocation();
controller.relayoutGraph();
}
@Override
public String toString() {
return "Instruction @ " + instruction.getAddress().toString();
}
@Override
public Shape getCompactShape() {
return shapeProvider.getCompactShape();
}
@Override
public Shape getFullShape() {
return shapeProvider.getFullShape();
}
@Override
public JComponent getComponent() {
return shapeProvider.getComponent();
}
@Override
protected void addAction(DockingAction action) {
//codeVertexPanel.addAction(action);
}
@Override
public Address getAddress() {
return instruction.getMinAddress();
}
@Override
public void setSelected(boolean selected) {
super.setSelected(selected);
controller.navigateOut(instruction.getAddress(), null);
}
@Override
public CodeUnit getCodeUnit() {
return instruction;
}
@Override
public boolean isGrabbable(Component component) {
return true;
}
@Override
protected Point2D getStartingEdgePoint(EgVertex end) {
Point2D location = getLocation();
return new Point2D.Double(location.getX(),
location.getY() + shapeProvider.getCircleCenterYOffset());
}
@Override
protected Point2D getEndingEdgePoint(EgVertex start) {
Point2D location = getLocation();
return new Point2D.Double(location.getX(),
location.getY() + shapeProvider.getCircleCenterYOffset());
}
@Override
protected boolean containsAddress(Address address) {
return instruction.contains(address);
}
@Override
public String getTooltip(MouseEvent e) {
return null;
}
@Override
public int compare(DegVertex o1, DegVertex o2) {
return o1.getAddress().compareTo(o2.getAddress());
}
}
@@ -0,0 +1,390 @@
/* ###
* 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 datagraph.data.graph;
import static datagraph.data.graph.DegVertex.DegVertexStatus.*;
import java.awt.Dimension;
import java.awt.Shape;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;
import java.util.List;
import java.util.Set;
import javax.swing.JComponent;
import datagraph.DataGraphPlugin;
import datagraph.data.graph.panel.DataVertexPanel;
import datagraph.data.graph.panel.model.row.DataRowObject;
import datagraph.graph.explore.EgVertex;
import docking.action.DockingAction;
import docking.action.DockingActionIf;
import docking.action.builder.ActionBuilder;
import ghidra.program.model.address.Address;
import ghidra.program.model.data.*;
import ghidra.program.model.listing.CodeUnit;
import ghidra.program.model.listing.Data;
import ghidra.util.HelpLocation;
import resources.Icons;
/**
* A vertex in the data exploration graph for displaying the contents of a single data object.
*/
public class DataDegVertex extends DegVertex {
private Data data;
private DataVertexPanel dataVertexPanel;
private DockingAction deleteAction;
private long dataTypeHash;
/**
* Constructor
* @param controller the controller
* @param data the Data object to be displayed by this vertex
* @param source the source vertex (from what vertex did you explore to get here)
* @param compactFormat determines if the row displays are in a compact format or an expanded
* format
*/
public DataDegVertex(DegController controller, Data data, DegVertex source,
boolean compactFormat) {
super(controller, source);
this.data = data;
dataVertexPanel = new DataVertexPanel(controller, this, compactFormat);
createActions();
dataVertexPanel.updateHeader();
dataVertexPanel.updateShape();
if (source == null) {
dataVertexPanel.setIsRoot(true);
}
dataTypeHash = hash(data.getDataType());
}
@Override
public String getTitle() {
return dataVertexPanel.getTitle();
}
@Override
public DegVertexStatus refreshGraph(boolean checkDataType) {
Address address = data.getAddress();
Data newData = data.getProgram().getListing().getDataAt(address);
if (newData == null) {
return MISSING;
}
if (data != newData) {
this.data = newData;
dataVertexPanel.setData(newData);
return CHANGED;
}
if (checkDataType) {
long newHash = hash(data.getDataType());
if (newHash != dataTypeHash) {
dataTypeHash = newHash;
dataVertexPanel.setData(data); // force the data model to reset
return CHANGED;
}
}
return VALID;
}
/**
* {@return a list of vertex row objects (currently only used for testing).}
*/
public List<DataRowObject> getRowObjects() {
return dataVertexPanel.getRowObjects();
}
@Override
public void clearUserChangedLocation() {
super.clearUserChangedLocation();
controller.relayoutGraph();
}
@Override
public int hashCode() {
return data.hashCode();
}
@Override
public void setSource(EgVertex source) {
super.setSource(source);
dataVertexPanel.setIsRoot(source == null);
deleteAction.setEnabled(source != null);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
DataDegVertex other = (DataDegVertex) obj;
return data.equals(other.data);
}
@Override
public JComponent getComponent() {
return dataVertexPanel;
}
@Override
public String toString() {
return "Data @ " + data.getAddress().toString();
}
@Override
public Address getAddress() {
return data.getAddress();
}
@Override
public CodeUnit getCodeUnit() {
return data;
}
@Override
protected void addAction(DockingAction action) {
dataVertexPanel.addAction(action);
}
@Override
public DockingActionIf getAction(String name) {
return dataVertexPanel.getAction(name);
}
@Override
public int getOutgoingEdgeOffsetFromCenter(EgVertex v) {
return dataVertexPanel.getOutgoingEdgeOffsetFromCenter(v);
}
@Override
public int getIncomingEdgeOffsetFromCenter(EgVertex vertex) {
return dataVertexPanel.getIncommingEdgeOffsetFromCenter(vertex);
}
@Override
public void setSelected(boolean selected) {
super.setSelected(selected);
dataVertexPanel.setSelected(selected);
}
@Override
public void setFocused(boolean focused) {
super.setFocused(focused);
dataVertexPanel.setFocused(focused);
}
@Override
public void dispose() {
dataVertexPanel.dispose();
dataVertexPanel = null;
}
@Override
public Shape getCompactShape() {
Shape shape = dataVertexPanel.getShape();
return shape;
}
public Data getData() {
return data;
}
public Dimension getSize() {
return dataVertexPanel.getSize();
}
/**
* Sets the size of this vertex by the user.
* @param dimension the new size for this vertex;
*/
public void setSizeByUser(Dimension dimension) {
dataVertexPanel.setSizeByUser(dimension);
}
/**
* Records the componentPath of the sub-data in this vertex that the edge going to
* the end vertex is associated with. Used to compute the y coordinate of the edge so that
* aligns with that data as the data is scrolled within the component.
* @param end the vertex that our outgoing edge to attached
* @param componentPath the component path of the sub data in this vertex associated with
* the edge going to the end vertex
*/
public void addOutgoingEdgeAnchor(DegVertex end, int[] componentPath) {
dataVertexPanel.addOutgoingEdge(end, componentPath);
}
/**
* Records the Address of the sub-data in this vertex that the edge coming in
* from the start vertex is associated with. Used to compute the y coordinate of the edge so
* that it aligns with the sub-data as the data is scrolled within the component. For incoming
* edges, we only record edges that are offset from the data start. If the incoming edge points
* to the overall data object, the edge will always be attached to the top of the vertex
* regardless of the scroll position.
*
* @param start the vertex that associated with an incoming edge
* @param address the address of the sub data in this vertex associated with
* the edge coming from the start vertex.
*/
public void addIncomingEdgeAnchor(DegVertex start, Address address) {
dataVertexPanel.addIncommingEdge(start, address);
}
@Override
public int compare(DegVertex v1, DegVertex v2) {
// outgoing child vertices are ordered based on the paths of the data that references
// them so that they are in the same order they appear in the referring structure datatype.
return dataVertexPanel.comparePaths(v1, v2);
}
private void createActions() {
String owner = DataGraphPlugin.class.getSimpleName();
if (dataVertexPanel.isExpandable()) {
DockingAction openAllAction = new ActionBuilder("Expand All", owner)
.toolBarIcon(Icons.EXPAND_ALL_ICON)
.description("Recursively open all data in this vertex.")
.helpLocation(new HelpLocation("DataGraphPlugin", "Expand_All"))
.onAction(c -> dataVertexPanel.expandAll())
.build();
addAction(openAllAction);
DockingAction closeAllAction = new ActionBuilder("Collapse All", owner)
.toolBarIcon(Icons.COLLAPSE_ALL_ICON)
.description("Close all data in this vertex.")
.helpLocation(new HelpLocation("DataGraphPlugin", "Collapse_All"))
.onAction(c -> dataVertexPanel.collapseAll())
.build();
addAction(closeAllAction);
}
deleteAction = new ActionBuilder("Close Vertex", owner)
.toolBarIcon(Icons.CLOSE_ICON)
.description("Removes this vertex and any of its descendents from the graph.")
.helpLocation(new HelpLocation("DataGraphPlugin", "Delete_Vertex"))
.enabled(source != null)
.onAction(c -> controller.deleteVertices(Set.of(DataDegVertex.this)))
.build();
addAction(deleteAction);
}
@Override
public String getTooltip(MouseEvent e) {
return dataVertexPanel.getToolTipText(e);
}
@Override
protected Point2D getStartingEdgePoint(EgVertex end) {
Point2D startLocation = getLocation();
// For the edge leaving this vertex going to the given end vertex, we need the
// starting point of the edge.
//
// We do this by starting with this vertex's location which is at the center point in
// the vertex. We want the x coordinate to be on the right edge of the vertex, so we add in
// half the width. We want the y coordinate to be wherever the corresponding data element
// is being displayed, so we need to know how much above or below the center point
// to draw the edge point to make it line up in the scrolled display.
int yOffset = getOutgoingEdgeOffsetFromCenter(end);
double x = startLocation.getX() + getSize().width / 2;
double y = startLocation.getY() + yOffset;
return new Point2D.Double(x, y);
}
@Override
protected Point2D getEndingEdgePoint(EgVertex start) {
Point2D endLocation = getLocation();
// For the edge entering this vertex from the given start vertex, we need the
// ending point of the edge.
//
// We do this by starting with this vertex's location which is at the center point in
// the vertex. We want the x coordinate to be on the left edge of the vertex, so we subtract
// half the width. We want the y coordinate to be wherever the corresponding address of
// the reference is being displayed, so we need to know how much above or below the center
// point to draw the edge point to make it line up in the scrolled display.
int yOffset = getIncomingEdgeOffsetFromCenter(start);
double x = endLocation.getX() - getSize().width / 2;
double y = endLocation.getY() + yOffset;
return new Point2D.Double(x, y);
}
/**
* Sets whether the column model should be a compact format or an expanded format. The basic
* difference is that expanded format includes a datatype for each row and the compact only
* shows a datatype if there is no value.
* @param b true to show a compact row, false to show more information.
*/
public void setCompactFormat(boolean b) {
dataVertexPanel.setCompactFormat(b);
}
/**
* Opens the given data row to show its sub-data components.
* @param row the row to expand
*/
public void expand(int row) {
dataVertexPanel.expand(row);
}
/**
* Adds a new vertex following the reference(s) coming out of the data component on the given
* row.
* @param row the row to open new vertices from
*/
public void openPointerReference(int row) {
dataVertexPanel.openPointerReference(row);
}
/**
* {@return true if the selected row in the vertex is expandable.}
*/
public boolean isOnExpandableRow() {
return dataVertexPanel.isSelectedRowExpandable();
}
/**
* Expands the selected row in the vertex recursively.
*/
public void expandSelectedRowRecursively() {
dataVertexPanel.expandSelectedRowRecursively();
}
@Override
protected boolean containsAddress(Address address) {
return data.contains(address);
}
private long hash(DataType dataType) {
long hash = dataType.getLength() * 31 + dataType.getName().hashCode();
if (dataType instanceof Composite composite) {
for (DataTypeComponent dataTypeComponent : composite.getDefinedComponents()) {
hash = 31 * hash + hash(dataTypeComponent.getDataType());
}
}
return hash;
}
}
@@ -0,0 +1,61 @@
/* ###
* 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 datagraph.data.graph;
import datagraph.graph.explore.AbstractExplorationGraph;
import ghidra.graph.viewer.layout.VisualGraphLayout;
/**
* A graph for exploring data and its incoming and outgoing references.
*/
public class DataExplorationGraph extends AbstractExplorationGraph<DegVertex, DegEdge> {
private VisualGraphLayout<DegVertex, DegEdge> layout;
/**
* The initial vertex for the graph. All other vertices in this graph can trace back its source
* to this vertex.
* @param root the initial source vertex for this explore graph
*/
public DataExplorationGraph(DegVertex root) {
super(root);
}
@Override
public VisualGraphLayout<DegVertex, DegEdge> getLayout() {
return layout;
}
@Override
public DataExplorationGraph copy() {
DataExplorationGraph newGraph = new DataExplorationGraph(getRoot());
for (DegVertex v : vertices.keySet()) {
newGraph.addVertex(v);
}
for (DegEdge e : edges.keySet()) {
newGraph.addEdge(e);
}
return newGraph;
}
void setLayout(VisualGraphLayout<DegVertex, DegEdge> layout) {
this.layout = layout;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,35 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package datagraph.data.graph;
import datagraph.graph.explore.EgEdge;
/**
* An edge for the {@link DataExplorationGraph}
*/
public class DegEdge extends EgEdge<DegVertex> {
public DegEdge(DegVertex start, DegVertex end) {
super(start, end);
}
@SuppressWarnings("unchecked")
// Suppressing warning on the return type; we know our class is the right type
@Override
public DegEdge cloneEdge(DegVertex start, DegVertex end) {
return new DegEdge(start, end);
}
}
@@ -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.
*/
package datagraph.data.graph;
import java.awt.event.MouseEvent;
import javax.swing.JComponent;
import ghidra.graph.viewer.GraphViewer;
import ghidra.graph.viewer.VisualGraphView;
import ghidra.graph.viewer.event.mouse.VertexTooltipProvider;
/**
* Extends the VisualGraphView mainly to provide appropriate tool tips.
*/
public class DegGraphView extends VisualGraphView<DegVertex, DegEdge, DataExplorationGraph> {
DegGraphView() {
super();
setSatelliteVisible(false);
}
@Override
protected void installGraphViewer() {
super.installGraphViewer();
GraphViewer<DegVertex, DegEdge> viewer = graphComponent.getPrimaryViewer();
viewer.setVertexTooltipProvider(new DataGraphVertexTipProvider());
}
private class DataGraphVertexTipProvider implements VertexTooltipProvider<DegVertex, DegEdge> {
@Override
public JComponent getTooltip(DegVertex v) {
return null;
}
@Override
public JComponent getTooltip(DegVertex v, DegEdge e) {
return null;
}
@Override
public String getTooltipText(DegVertex v, MouseEvent e) {
return v.getTooltip(e);
}
}
}
@@ -0,0 +1,71 @@
/* ###
* 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 datagraph.data.graph;
import java.util.Comparator;
import datagraph.graph.explore.EgEdgeTransformer;
import datagraph.graph.explore.EgGraphLayout;
import ghidra.graph.VisualGraph;
import ghidra.graph.viewer.layout.AbstractVisualGraphLayout;
/**
* The layout for the DataExplorationGraph. It extends the {@link EgGraphLayout} the implements
* the basic incoming and outgoing tree structures. This basically just adds the ordering logic
* for vertices.
*/
public class DegLayout extends EgGraphLayout<DegVertex, DegEdge> {
protected DegLayout(DataExplorationGraph g, int verticalGap, int horizontalGap) {
super(g, "Data Graph Layout", verticalGap, horizontalGap);
}
@Override
public DataExplorationGraph getVisualGraph() {
return (DataExplorationGraph) getGraph();
}
@Override
public AbstractVisualGraphLayout<DegVertex, DegEdge> createClonedLayout(
VisualGraph<DegVertex, DegEdge> newGraph) {
if (!(newGraph instanceof DataExplorationGraph dataGraph)) {
throw new IllegalArgumentException(
"Must pass a " + DataExplorationGraph.class.getSimpleName() +
"to clone the " + getClass().getSimpleName());
}
DegLayout newLayout = new DegLayout(dataGraph, verticalGap, horizontalGap);
return newLayout;
}
@Override
protected Comparator<DegVertex> getIncommingVertexComparator() {
return (v1, v2) -> v1.getAddress().compareTo(v2.getAddress());
}
@Override
protected Comparator<DegVertex> getOutgoingVertexComparator() {
return (v1, v2) -> {
DegVertex parent = (DegVertex) v1.getSourceVertex();
return parent.compare(v1, v2);
};
}
@Override
protected EgEdgeTransformer<DegVertex, DegEdge> createEdgeTransformer() {
return new EgEdgeTransformer<>();
}
}
@@ -0,0 +1,46 @@
/* ###
* 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 datagraph.data.graph;
import ghidra.graph.viewer.layout.AbstractLayoutProvider;
import ghidra.graph.viewer.layout.VisualGraphLayout;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
/**
* Provider for the DegLayout
*/
public class DegLayoutProvider
extends AbstractLayoutProvider<DegVertex, DegEdge, DataExplorationGraph> {
private static final String NAME = "Data Graph Layout";
private static final int VERTICAL_GAP = 50;
private static final int HORIZONTAL_GAP = 100;
@Override
public VisualGraphLayout<DegVertex, DegEdge> getLayout(DataExplorationGraph graph,
TaskMonitor monitor)
throws CancelledException {
DegLayout layout = new DegLayout(graph, VERTICAL_GAP, HORIZONTAL_GAP);
initVertexLocations(graph, layout);
return layout;
}
@Override
public String getLayoutName() {
return NAME;
}
}
@@ -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 datagraph.data.graph;
import java.awt.Component;
import java.awt.event.MouseEvent;
import java.util.Comparator;
import datagraph.graph.explore.EgVertex;
import docking.GenericHeader;
import docking.action.DockingAction;
import docking.action.DockingActionIf;
import ghidra.graph.viewer.vertex.VertexShapeProvider;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.CodeUnit;
/**
* A vertex for the {@DataExorationGraph}
*/
public abstract class DegVertex extends EgVertex implements VertexShapeProvider,
Comparator<DegVertex> {
// enum for returning that status of a vertex when refreshing after a program change
public enum DegVertexStatus {
VALID,
MISSING,
CHANGED
}
protected DegController controller;
/**
* Constructor
* @param controller the controller for the data exploration graph
* @param source the vertex that spawned this vertex. The original source vertex has no
* parent source, but all the others must have a source which can be used to trace back
* to the original source vertex.
*/
public DegVertex(DegController controller, DegVertex source) {
super(source);
this.controller = controller;
}
@Override
public boolean isGrabbable(Component component) {
Component c = component;
while (c != null) {
if (c instanceof GenericHeader) {
return true;
}
c = c.getParent();
}
return false;
}
/**
*{@return the program's address associated with this node.}
*/
public abstract Address getAddress();
/**
*{@return the CodeUnit associated with this node.}
*/
public abstract CodeUnit getCodeUnit();
/**
* {@return the tooltip for this vertex}
* @param e the the mouse even triggering this call
*/
public abstract String getTooltip(MouseEvent e);
/**
* Checks if the given vertex is still valid after a program change.
* @param checkDataTypes if true, the underlying datatype should also be checked
* @return the status of the vertex. The vertex can be valid, changed, or missing.
*/
public abstract DegVertexStatus refreshGraph(boolean checkDataTypes);
/**
* {@return the title of this vertex}
*/
public abstract String getTitle();
/**
* {@return true if code unit associated with this vertex contains the given address.}
* @param address the address to check if it is at or in this vertex
*/
protected abstract boolean containsAddress(Address address);
/**
* {@return the docking action with the given name from this vertex.}
* @param name the name of the action to retrieve
*/
public DockingActionIf getAction(String name) {
return null;
}
/**
* Adds the given action to this vertex.
* @param action the action to add to this vertex
*/
protected abstract void addAction(DockingAction action);
}
@@ -0,0 +1,43 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package datagraph.data.graph.panel;
import java.util.Comparator;
/**
* Comparator for comparing two data component paths
*/
public class DtComponentPathComparator implements Comparator<int[]> {
@Override
public int compare(int[] o1, int[] o2) {
int level = 0;
int length1 = o1.length;
int length2 = o2.length;
while (level < length1 && level < length2) {
int index1 = o1[level];
int index2 = o2[level];
if (index1 != index2) {
return index1 - index2;
}
level++;
}
if (length1 == length2) {
return 0;
}
return length1 - length2;
}
}
@@ -0,0 +1,132 @@
/* ###
* 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 datagraph.data.graph.panel.model.column;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import datagraph.data.graph.panel.model.row.DataRowObject;
import docking.widgets.trable.*;
/**
* A GTrable column model for showing information about data and sub-data items. This model
* shows the information in a compact format generally showing the field name and the current
* value for that field. For the top element that has no field name, the datatype name is shown
* instead. Also for the value field, if it doesn't have a value because it has sub pieces that
* have values (structure), then the datatype name is shown in the value value field.
*/
public class CompactDataColumnModel extends GTrableColumnModel<DataRowObject> {
@Override
protected void populateColumns(List<GTrableColumn<DataRowObject, ?>> columnList) {
columnList.add(new NameColumn());
columnList.add(new ValueColumn());
columnList.add(new PointerButtonColumn());
}
/**
* {@return true if the given column represents the pointer icon column where the user can
* click to add new vertices to the graph.}
* @param column the column to check
*/
public boolean isPointerButtonColumn(int column) {
return column == 2;
}
private class NameColumn extends GTrableColumn<DataRowObject, String> {
@Override
public String getValue(DataRowObject row) {
// if in compact format, show type in name field if it doesn't have a name
if (row.getIndentLevel() == 0) {
return row.getDataType();
}
return row.getName();
}
@Override
protected int getPreferredWidth() {
return 150;
}
}
private static class TypeColumn extends GTrableColumn<DataRowObject, String> {
@Override
public String getValue(DataRowObject row) {
return row.getDataType();
}
@Override
protected int getPreferredWidth() {
return 120;
}
}
private class ValueColumn extends GTrableColumn<DataRowObject, String> {
private GTrableCellRenderer<String> renderer = new ValueColumnRenderer();
@Override
public String getValue(DataRowObject row) {
String value = row.getValue();
if (!StringUtils.isBlank(value)) {
value = " = " + value;
}
else if (row.getIndentLevel() > 0) {
// special case if in compact for to show data types in the value column
// on entries that can be opened to show inner values.
value = " " + row.getDataType();
}
return value;
}
@Override
protected int getPreferredWidth() {
return 100;
}
@Override
public GTrableCellRenderer<String> getRenderer() {
return renderer;
}
}
private static class PointerButtonColumn extends GTrableColumn<DataRowObject, Boolean> {
private GTrableCellRenderer<Boolean> renderer = new PointerColumnRenderer();
@Override
public Boolean getValue(DataRowObject row) {
return row.hasOutgoingReferences();
}
@Override
protected int getPreferredWidth() {
return 24;
}
@Override
public boolean isResizable() {
return false;
}
@Override
public GTrableCellRenderer<Boolean> getRenderer() {
return renderer;
}
}
}
@@ -0,0 +1,122 @@
/* ###
* 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 datagraph.data.graph.panel.model.column;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import datagraph.data.graph.panel.model.row.DataRowObject;
import docking.widgets.trable.*;
/**
* A GTrable column model for showing information about data and sub-data items. This model
* shows the information in a expanded format that displays the datatype, the field name and
* the current value for that field.
*/
public class ExpandedDataColumnModel extends GTrableColumnModel<DataRowObject> {
@Override
protected void populateColumns(List<GTrableColumn<DataRowObject, ?>> columnList) {
columnList.add(new TypeColumn());
columnList.add(new NameColumn());
columnList.add(new ValueColumn());
columnList.add(new PointerButtonColumn());
}
/**
* {@return true if the given column represents the pointer icon column where the user can
* click to add new vertices to the graph.}
* @param column the column to check
*/
public boolean isPointerButtonColumn(int column) {
return column == 3;
}
private static class NameColumn extends GTrableColumn<DataRowObject, String> {
@Override
public String getValue(DataRowObject row) {
return row.getName();
}
@Override
protected int getPreferredWidth() {
return 150;
}
}
private static class TypeColumn extends GTrableColumn<DataRowObject, String> {
@Override
public String getValue(DataRowObject row) {
return row.getDataType();
}
@Override
protected int getPreferredWidth() {
return 120;
}
}
private static class ValueColumn extends GTrableColumn<DataRowObject, String> {
private GTrableCellRenderer<String> renderer = new ValueColumnRenderer();
@Override
public String getValue(DataRowObject row) {
String value = row.getValue();
if (!StringUtils.isBlank(value)) {
value = " = " + value;
}
return value;
}
@Override
protected int getPreferredWidth() {
return 100;
}
@Override
public GTrableCellRenderer<String> getRenderer() {
return renderer;
}
}
private static class PointerButtonColumn extends GTrableColumn<DataRowObject, Boolean> {
private GTrableCellRenderer<Boolean> renderer = new PointerColumnRenderer();
@Override
public Boolean getValue(DataRowObject row) {
return row.hasOutgoingReferences();
}
@Override
protected int getPreferredWidth() {
return 24;
}
@Override
public boolean isResizable() {
return false;
}
@Override
public GTrableCellRenderer<Boolean> getRenderer() {
return renderer;
}
}
}
@@ -0,0 +1,44 @@
/* ###
* 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 datagraph.data.graph.panel.model.column;
import java.awt.Component;
import javax.swing.Icon;
import docking.widgets.trable.DefaultGTrableCellRenderer;
import docking.widgets.trable.GTrable;
import resources.Icons;
/**
* Renderer for the pointer icon column where the use can click to add vertices to the graph.
*/
public class PointerColumnRenderer extends DefaultGTrableCellRenderer<Boolean> {
private static final Icon ICON = Icons.RIGHT_ICON;
@Override
public Component getCellRenderer(GTrable<?> trable, Boolean value, boolean isSelected,
boolean hasFocus, int row, int column) {
super.getCellRenderer(trable, null, isSelected, hasFocus, row, column);
boolean isPointer = value;
Icon icon = isPointer ? ICON : null;
setIcon(icon);
return this;
}
}
@@ -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 datagraph.data.graph.panel.model.column;
import java.awt.Color;
import java.awt.Component;
import docking.widgets.trable.DefaultGTrableCellRenderer;
import docking.widgets.trable.GTrable;
import generic.theme.GColor;
/**
* Column renderer for the values column. Used to change the foreground color for values.
*/
public class ValueColumnRenderer extends DefaultGTrableCellRenderer<String> {
private Color valueColor = new GColor("color.fg.datagraph.value");
@Override
public Component getCellRenderer(GTrable<?> trable, String value, boolean isSelected,
boolean hasFocus, int row, int column) {
super.getCellRenderer(trable, value, isSelected, hasFocus, row, column);
if (value.startsWith(" =") && !isSelected) {
setForeground(valueColor);
}
return this;
}
}
@@ -0,0 +1,61 @@
/* ###
* 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 datagraph.data.graph.panel.model.row;
import ghidra.program.model.listing.Data;
/**
* DataRowObject for groups of array elements. Because arrays can be large they are recursively
* grouped.
*/
public class ArrayGroupDataRowObject extends DataRowObject {
private String name;
private Data data;
ArrayGroupDataRowObject(Data data, int startIndex, int length, int indentLevel,
boolean isOpen) {
super(indentLevel, isOpen);
this.data = data;
this.name = "[" + startIndex + " - " + (startIndex + length - 1) + "]";
}
@Override
public String getName() {
return name;
}
@Override
public String getValue() {
return "";
}
@Override
public String getDataType() {
return "";
}
@Override
public boolean isExpandable() {
return true;
}
@Override
public Data getData() {
return data;
}
}
@@ -0,0 +1,65 @@
/* ###
* 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 datagraph.data.graph.panel.model.row;
import ghidra.program.model.listing.Data;
/**
* DataRowObject for actual DataComponents. This directly corresponds to a
* Data or sub Data object in a program.
*/
public class ComponentDataRowObject extends DataRowObject {
static final int ARRAY_GROUP_SIZE = 100;
protected Data data;
public ComponentDataRowObject(int indentLevel, Data data, boolean isOpen) {
super(indentLevel, isOpen);
this.data = data;
}
@Override
public boolean isExpandable() {
return data.getNumComponents() > 0;
}
@Override
public Data getData() {
return data;
}
@Override
public String getName() {
return data.getFieldName();
}
@Override
public String getValue() {
return data.getDefaultValueRepresentation();
}
@Override
public String getDataType() {
return data.getDataType().getDisplayName();
}
@Override
public boolean hasOutgoingReferences() {
if (data.isPointer()) {
return true;
}
return data.getProgram().getReferenceManager().hasReferencesFrom(data.getAddress());
}
}
@@ -0,0 +1,85 @@
/* ###
* 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 datagraph.data.graph.panel.model.row;
import docking.widgets.table.GTable;
import docking.widgets.table.RowObject;
import ghidra.program.model.listing.Data;
/**
* Abstract class for displaying rows in a Data GTrable model. Similar to a {@link RowObject} in
* a {@link GTable}. The big difference is that each row maintains its indent level and whether
* or not is is expanded. GTrables are like tables, but with a tree like structure. Each row that is
* a child of another row has its indent level set to one more than its parent. The expanded flag is
* used to indicate if a given row has visible child rows showing or not.
*/
public abstract class DataRowObject {
private int indentLevel;
private boolean isExpanded;
/**
* Constructor
* @param indentLevel the indent level for this row object
* @param isExpanded true if this object has child rows that are being displayed
*/
protected DataRowObject(int indentLevel, boolean isExpanded) {
this.indentLevel = indentLevel;
this.isExpanded = isExpanded;
}
public int getIndentLevel() {
return indentLevel;
}
public boolean isExpanded() {
return isExpanded;
}
/**
* {@return the name for this row. Typically this will be the field name, but it could be
* a descriptive title for a group such as array range.}
*/
public abstract String getName();
/**
* {@return the interpreted value for the data at this location.}
*/
public abstract String getValue();
/**
* {@return the name of the datatype at this location.}
*/
public abstract String getDataType();
/**
* {@return true if the row can produce child rows.}
*/
public abstract boolean isExpandable();
/**
* {@return true if this location represents a pointer or has outgoing references}
*/
public boolean hasOutgoingReferences() {
return false;
}
/**
* @{return the Data object associated with this row.}
*/
public abstract Data getData();
}
@@ -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.
*/
package datagraph.data.graph.panel.model.row;
import java.util.ArrayList;
import java.util.List;
/**
* Cache for {@link DataRowObject}s. DataRowObjects are created as needed to conserve space. The
* visible rows are kept in this cache to avoid having to recreate them on each paint call. It uses
* a simple premise that paint calls will paint rows in order. So anytime a put occurs that is not
* one more than the previous call, the assumption is that the view was scrolled, so the cache
* is cleared and a new cache sequence is started.
*/
public class DataRowObjectCache {
private static final int MAX_CACHE_SIZE = 300;
List<DataRowObject> cachedRows = new ArrayList<>();
int startIndex = 0;
public boolean contains(int rowIndex) {
int cacheIndex = rowIndex - startIndex;
return cacheIndex >= 0 && cacheIndex < cachedRows.size();
}
public DataRowObject getDataRow(int rowIndex) {
return cachedRows.get(rowIndex - startIndex);
}
public void putData(int rowIndex, DataRowObject row) {
// This cache expects data to be put in sequentially from some start row. The idea is
// to cache the rows that are currently in the scrolled view. So anytime we are putting
// in a row that is not the next expected row in sequence, throw away the cache and
// start over.
if (rowIndex != startIndex + cachedRows.size() || cachedRows.size() > MAX_CACHE_SIZE) {
clear();
startIndex = rowIndex;
}
cachedRows.add(row);
}
public void clear() {
startIndex = 0;
cachedRows.clear();
}
}
@@ -0,0 +1,141 @@
/* ###
* 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 datagraph.data.graph.panel.model.row;
import docking.widgets.trable.AbstractGTrableRowModel;
import docking.widgets.trable.GTrable;
import ghidra.program.model.listing.Data;
/**
* Row model for Data objects in {@link GTrable}. Most of the complexity is handled by
* the {@link OpenDataChildren} object. If only the top most row is displaying (i.e. it is not
* expanded), then the openChildren object is null. When row 0 is expanded, an OpenChildren is
* created to manage the rows for the sub data child rows. This pattern is repeated inside the
* OpenChilren node (open rows have corresponding OpenChildren objects to manage its child
* rows.)
*/
public class DataTrableRowModel extends AbstractGTrableRowModel<DataRowObject> {
private Data data;
private OpenDataChildren openChildren;
private DataRowObjectCache cache = new DataRowObjectCache();
public DataTrableRowModel(Data data) {
this.data = data;
}
public Data getData() {
return data;
}
public void setData(Data data) {
this.data = data;
cache.clear();
openChildren = null;
}
@Override
public int getRowCount() {
if (openChildren == null) {
return 1;
}
return openChildren.getRowCount() + 1;
}
@Override
public DataRowObject getRow(int rowIndex) {
if (cache.contains(rowIndex)) {
return cache.getDataRow(rowIndex);
}
DataRowObject row = generateRow(rowIndex);
cache.putData(rowIndex, row);
return row;
}
private DataRowObject generateRow(int rowIndex) {
if (rowIndex == 0) {
return new ComponentDataRowObject(0, data, openChildren != null);
}
return openChildren.getRow(rowIndex - 1);
}
@Override
public boolean isExpandable(int rowIndex) {
DataRowObject dataRow = getRow(rowIndex);
return dataRow != null && dataRow.isExpandable();
}
@Override
public boolean isExpanded(int rowIndex) {
DataRowObject row = getRow(rowIndex);
return row != null && row.isExpanded();
}
@Override
public int collapseRow(int rowIndex) {
cache.clear();
if (rowIndex < 0 || rowIndex >= getRowCount()) {
throw new IndexOutOfBoundsException();
}
if (rowIndex == 0) {
if (openChildren == null) {
return 0;
}
int diff = openChildren.getRowCount();
openChildren = null;
fireModelChanged();
return diff;
}
int rowCountDiff = openChildren.collapseChild(rowIndex - 1);
fireModelChanged();
return rowCountDiff;
}
@Override
public int expandRow(int rowIndex) {
cache.clear();
if (rowIndex < 0 || rowIndex >= getRowCount()) {
throw new IndexOutOfBoundsException();
}
if (rowIndex == 0) {
// are we already open?
if (openChildren != null) {
return 0;
}
openChildren = OpenDataChildren.createOpenDataNode(data, 0, 0, 1);
fireModelChanged();
return openChildren.getRowCount();
}
int diff = openChildren.expandChild(rowIndex - 1);
fireModelChanged();
return diff;
}
@Override
public int getIndentLevel(int rowIndex) {
DataRowObject row = getRow(rowIndex);
return row.getIndentLevel();
}
public void refresh() {
cache.clear();
if (openChildren != null) {
if (!openChildren.refresh(data)) {
openChildren = null;
}
}
fireModelChanged();
}
}
@@ -0,0 +1,469 @@
/* ###
* 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 datagraph.data.graph.panel.model.row;
import java.util.*;
import ghidra.program.model.data.Array;
import ghidra.program.model.listing.Data;
/**
* Manages open rows in a DataTrableModel. Since this manages the open rows for its parent data
* object, it has one row for each sub data component in its parent data. Of course, each of these
* rows can also potentially be expandable. If any of the top level rows managed by this object are
* expanded, it creates a OpenDataChildren object to manage its sub rows. The OpenChildrenObjects
* are store in a list that is ordered by its row index.
* <P>
* To find out if a row is expanded or not, a binary search is used to find out if there is an
* OpenDataChildren object for that row. If it is expanded, that object is used to recursively go
* down to get the leaf row object.
*/
public abstract class OpenDataChildren implements Comparable<OpenDataChildren> {
// when arrays are bigger than 100, we group them into chunks of 100 each. The size
// should be a power of 10 and 100 seems like a good choice.
private static final int ARRAY_GROUP_SIZE = 100;
private int rowIndex; // row index relative to parent
private int rowCount; // number of rows represented by this node, including any open children
private int componentIndex;
private int componentCount;
protected int indentLevel;
protected List<OpenDataChildren> openChildren;
protected Data data;
/**
* Constructor
* @param data the data object that this is managing rows for its child data components
* @param rowIndex the row index is the overall row index of the first child element row
* @param componentIndex the index of the the managed data component within its parent
* @param componentCount the number of direct rows this managed component has. (It may have
* indirect rows if its rows have child rows)
* @param indentLevel the indent level for all direct rows managed by this object
*/
protected OpenDataChildren(Data data, int rowIndex, int componentIndex,
int componentCount, int indentLevel) {
this.data = data;
this.rowIndex = rowIndex;
this.componentCount = componentCount;
this.rowCount = componentCount;
this.componentIndex = componentIndex;
this.indentLevel = indentLevel;
openChildren = new ArrayList<>();
}
/**
* Convenience static factory method for create the correct subtype of an OpenDataChildren
* object. (Arrays require special handling.)
* @param data the data object that is being expanded and this object will manage its child
* data components.
* @param rowIndex the overall row index of the first child row that is expanded
* @param componentIndex the component index of the data within its parent that this object
* is managing
* @param indentLevel the indent level for all rows directly managed by this object
* @return a new OpenDataChildren object
*/
public static OpenDataChildren createOpenDataNode(Data data, int rowIndex, int componentIndex,
int indentLevel) {
int numComponents = data.getNumComponents();
if (data.getDataType() instanceof Array) {
if (numComponents <= ARRAY_GROUP_SIZE) {
return new ArrayElementsComponentNode(data, rowIndex, componentIndex, numComponents,
0, indentLevel);
}
return new ArrayGroupComponentNode(data, rowIndex, componentIndex, 0, numComponents,
indentLevel);
}
return new DataComponentNode(data, rowIndex, componentIndex, numComponents, indentLevel);
}
/**
* Private constructor used to create a object that can be used in a binary search
* @param rowIndex the row to search for
*/
private OpenDataChildren(int rowIndex) {
this.rowIndex = rowIndex;
}
/**
* {@return the total number of rows currently managed by this object (includes rows
* recursively managed by its expanded children)}
*/
public int getRowCount() {
return rowCount;
}
/**
* {@return the row object for the given index.}
* @param childRowIndex the index to get a row for. This index is relative to this
* OpenDataChildren object and not the overall row index for the model.
*/
public DataRowObject getRow(int childRowIndex) {
OpenDataChildren node = findChildNodeAtOrBefore(childRowIndex);
if (node == null) {
return generateRow(childRowIndex, false);
}
if (node.getRowIndex() == childRowIndex) {
return generateRow(node.getComponentIndex(), true);
}
int childIndex = childRowIndex - node.getRowIndex() - 1;
if (childIndex < node.getRowCount()) {
return node.getRow(childIndex);
}
int childComponentIndex = node.getComponentIndex() + childRowIndex - node.getRowIndex() -
node.getRowCount();
return generateRow(childComponentIndex, false);
}
protected int getComponentIndex() {
return componentIndex;
}
protected abstract DataRowObject generateRow(int childComponentIndex, boolean isOpen);
/**
* Expands the sub child at the given relative row index
* @param childRowIndex relative index to this OpenDataChildren object and not the overall
* model row index.
* @return the number of additional rows this caused to be added to the overall model
*/
public int expandChild(int childRowIndex) {
OpenDataChildren node = findChildNodeAtOrBefore(childRowIndex);
if (node == null) {
return insertNode(childRowIndex, childRowIndex);
}
int indexPastNode = childRowIndex - node.getRowIndex();
if (indexPastNode == 0) {
return 0; // we are already open
}
if (indexPastNode <= node.getRowCount()) {
int diff = node.expandChild(indexPastNode - 1);
rebuildNodeIndex();
return diff;
}
int childComponentIndex = node.getComponentIndex() + indexPastNode - node.rowCount;
return insertNode(childRowIndex, childComponentIndex);
}
/**
* Collapse the child at the relative index.
* @param childRowIndex the relative child index to collapse
* @return the number of rows removed from the overall model
*/
public int collapseChild(int childRowIndex) {
OpenDataChildren node = findChildNodeAtOrBefore(childRowIndex);
// the given index is not open, so do nothing
if (node == null) {
return 0;
}
// compute the number of indexes past the open node we found
int offsetIndex = childRowIndex - node.rowIndex;
// if we found a node at that index, just delete it since we only retain open nodes
if (offsetIndex == 0) {
openChildren.remove(node);
rebuildNodeIndex();
return node.getRowCount();
}
// if the index is contained in the node, recurse down to close
if (offsetIndex <= node.getRowCount()) {
int diff = node.collapseChild(offsetIndex - 1);
rebuildNodeIndex();
return diff;
}
// again, the given index is not open, so do nothing
return 0;
}
protected void rebuildNodeIndex() {
rowCount = componentCount;
OpenDataChildren lastNode = null;
for (OpenDataChildren node : openChildren) {
if (lastNode == null) {
node.rowIndex = node.componentIndex;
}
else {
node.rowIndex = lastNode.rowIndex + node.componentIndex - lastNode.componentIndex +
lastNode.rowCount;
}
rowCount += node.rowCount;
lastNode = node;
}
}
protected int insertNode(int childRowIndex, int childComponentIndex) {
OpenDataChildren newNode = generatedNode(childRowIndex, childComponentIndex);
if (newNode == null) {
return 0; // tried to open a node that can't be opened
}
int index = Collections.binarySearch(openChildren, newNode);
// It should never be positive since we searched and didn't find one at this index
if (index >= 0) {
return 0;
}
int insertionIndex = -index - 1;
openChildren.add(insertionIndex, newNode);
rebuildNodeIndex();
return newNode.getRowCount();
}
protected abstract OpenDataChildren generatedNode(int childRowIndex, int childComponentIndex);
/**
* Returns the rowIndex of of this node relative to its parent.
* @return the rowIndex of of this node relative to its parent
*/
int getRowIndex() {
return rowIndex;
}
void setRowIndex(int rowIndex) {
this.rowIndex = rowIndex;
}
@Override
public int compareTo(OpenDataChildren o) {
return getRowIndex() - o.getRowIndex();
}
public boolean refresh(Data newData) {
int newComponentCount = data.getNumComponents();
// if the data is different or it went to something not expandable (count == 0)
// return false to indicate to parent this node needs to be removed
if (data != newData || newComponentCount == 0) {
openChildren.clear();
return false;
}
// if the number of components changes, this node can remain, but need to close any
// children it has because it gets too complicated to correct
if (newComponentCount != componentCount) {
openChildren.clear();
componentCount = newComponentCount;
rowCount = componentCount;
return true;
}
// recursively refresh open children, removing any that can't be refreshed
Iterator<OpenDataChildren> it = openChildren.iterator();
while (it.hasNext()) {
OpenDataChildren child = it.next();
if (!child.refresh(data.getComponent(child.componentIndex))) {
it.remove();
}
}
rebuildNodeIndex();
return true;
}
protected OpenDataChildren findChildNodeAtOrBefore(int childRowIndex) {
if (openChildren.isEmpty()) {
return null;
}
int index = Collections.binarySearch(openChildren, new SearchKeyNode(childRowIndex));
if (index < 0) {
index = -index - 2;
}
return index < 0 ? null : openChildren.get(index);
}
private static class SearchKeyNode extends OpenDataChildren {
SearchKeyNode(int rowIndex) {
super(rowIndex);
}
@Override
protected DataRowObject generateRow(int childComponentIndex, boolean isOpen) {
return null;
}
@Override
protected OpenDataChildren generatedNode(int childRowIndex, int childComponentIndex) {
return null;
}
}
private static class DataComponentNode extends OpenDataChildren {
public DataComponentNode(Data data, int rowIndex, int componentIndex,
int numComponents, int indentLevel) {
super(data, rowIndex, componentIndex, numComponents, indentLevel);
}
@Override
protected DataRowObject generateRow(int childComponentIndex, boolean isOpen) {
Data component = data.getComponent(childComponentIndex);
return new ComponentDataRowObject(indentLevel, component, isOpen);
}
@Override
protected OpenDataChildren generatedNode(int childRowIndex, int childComponentIndex) {
Data component = data.getComponent(childComponentIndex);
return createOpenDataNode(component, childRowIndex, childComponentIndex,
indentLevel + 1);
}
}
private static class ArrayElementsComponentNode extends OpenDataChildren {
private int arrayStartIndex;
private int totalArraySize;
protected ArrayElementsComponentNode(Data data, int rowIndex, int componentIndex,
int componentCount, int arrayStartIndex, int indentLevel) {
super(data, rowIndex, componentIndex, componentCount, indentLevel);
this.arrayStartIndex = arrayStartIndex;
this.totalArraySize = data.getNumComponents();
}
@Override
protected DataRowObject generateRow(int childComponentIndex, boolean isOpen) {
Data component = data.getComponent(arrayStartIndex + childComponentIndex);
return new ComponentDataRowObject(indentLevel + 1, component, isOpen);
}
@Override
protected OpenDataChildren generatedNode(int childRowIndex, int childComponentIndex) {
Data component = data.getComponent(arrayStartIndex + childComponentIndex);
return createOpenDataNode(component, childRowIndex, childComponentIndex,
indentLevel + 1);
}
@Override
public boolean refresh(Data newData) {
// NOTE: if this is a child of a array group node, this these check that can return
// false can't happen as they have already been checked by the parent node. These
// exist in case this is directly parented from a normal data node
if (!(newData.getDataType() instanceof Array)) {
return false;
}
int newTotalArraySize = data.getNumComponents();
// if the data is different or the array changed size, it needs to be removed
if (data != newData || newTotalArraySize != totalArraySize) {
return false;
}
// recursively refresh open children. Since the elements are all the same type, if
// one can't refresh, then none can refresh
for (OpenDataChildren child : openChildren) {
Data component = data.getComponent(arrayStartIndex + child.getComponentIndex());
if (!child.refresh(component)) {
openChildren.clear();
break;
}
}
rebuildNodeIndex();
return true;
}
}
private static class ArrayGroupComponentNode extends OpenDataChildren {
private int arrayStartIndex;
private int groupSize;
private int arrayCount;
private int totalArraySize;
public ArrayGroupComponentNode(Data data, int rowIndex, int componentIndex,
int arrayStartIndex, int arrayCount, int indentLevel) {
super(data, rowIndex, componentIndex, getGroupCount(arrayCount), indentLevel);
this.arrayStartIndex = arrayStartIndex;
this.arrayCount = arrayCount;
this.groupSize = getGroupSize(arrayCount);
this.totalArraySize = data.getNumComponents();
}
private static int getGroupCount(int length) {
int groupSize = ARRAY_GROUP_SIZE;
int numGroups = (length + groupSize - 1) / groupSize;
while (numGroups > ARRAY_GROUP_SIZE) {
groupSize = groupSize * ARRAY_GROUP_SIZE;
numGroups = (length + groupSize - 1) / groupSize;
}
return numGroups;
}
private static int getGroupSize(int length) {
int groupSize = ARRAY_GROUP_SIZE;
int numGroups = (length + groupSize - 1) / groupSize;
while (numGroups > ARRAY_GROUP_SIZE) {
groupSize = groupSize * ARRAY_GROUP_SIZE;
numGroups = (length + groupSize - 1) / groupSize;
}
return groupSize;
}
@Override
protected DataRowObject generateRow(int childComponentIndex, boolean isOpen) {
int subArrayStartIndex = arrayStartIndex + childComponentIndex * groupSize;
int length = Math.min(groupSize, arrayCount - (childComponentIndex * groupSize));
return new ArrayGroupDataRowObject(data, subArrayStartIndex, length, indentLevel + 1,
isOpen);
}
@Override
protected OpenDataChildren generatedNode(int childRowIndex, int childComponentIndex) {
int arrayOffsetFromStart = childComponentIndex * groupSize;
int subArrayStartIndex = arrayStartIndex + arrayOffsetFromStart;
int length = Math.min(groupSize, arrayCount - arrayOffsetFromStart);
if (groupSize == ARRAY_GROUP_SIZE) {
return new ArrayElementsComponentNode(data, childRowIndex, childComponentIndex,
length, subArrayStartIndex, indentLevel + 1);
}
return new ArrayGroupComponentNode(data, childRowIndex, childComponentIndex,
subArrayStartIndex, length, indentLevel + 1);
}
@Override
public boolean refresh(Data newData) {
if (!(newData.getDataType() instanceof Array)) {
return false;
}
int newTotalArraySize = data.getNumComponents();
// if the data is different or the array changed size,
// it needs to be removed
if (data != newData || newTotalArraySize != totalArraySize) {
return false;
}
// recursively refresh open children. Since the elements are all the same type, if
// one can't refresh, then none can refresh so clear all open children
for (OpenDataChildren child : openChildren) {
if (!child.refresh(data)) {
openChildren.clear();
break;
}
}
rebuildNodeIndex();
return true;
}
}
}
@@ -0,0 +1,143 @@
/* ###
* 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 datagraph.graph.explore;
import java.util.*;
import ghidra.graph.graphs.DefaultVisualGraph;
import ghidra.graph.viewer.layout.VisualGraphLayout;
/**
* Base graph for exploration graphs. An exploration graph is a graph that typically starts with
* just one vertex, but then is interactively expanded with additional vertices by exploring
* incoming and outgoing links.
*
* @param <V> the vertex type
* @param <E> the edge type
*/
public abstract class AbstractExplorationGraph<V extends EgVertex, E extends EgEdge<V>>
extends DefaultVisualGraph<V, E> {
private V root;
private EgGraphLayout<V, E> layout;
/**
* Constructor
* @param root the initial vertex in the graph. All nodes in the graph must be connected directly
* or indirectly to this vertex.
*/
public AbstractExplorationGraph(V root) {
this.root = root;
addVertex(root);
}
/**
* {@return the original source vertex for this graph.}
*/
public V getRoot() {
return root;
}
public void setLayout(EgGraphLayout<V, E> layout) {
this.layout = layout;
}
@Override
public VisualGraphLayout<V, E> getLayout() {
return layout;
}
/**
* {@return a set of all vertices that can trace a source path back to the given vertex. In
* other words either getSource() or getSource().getSource(), and so on, on the given vertex.}
* @param source the vertex to see if a vertex is a descendant from.
*/
public Set<V> getDescendants(V source) {
Set<V> descendents = new HashSet<>();
getDescendants(source, descendents);
return descendents;
}
/**
* Sets the root of this graph to the new root.
* @param newRoot the new source root for the graph
*/
public void setRoot(V newRoot) {
// First clear out the source from each vertex to so they can be reassigned as we
// explore from the new root. We will use a null source to indicate the vertex hasn't been
// processed.
getVertices().forEach(v -> v.setSource(null));
// temporarily set the root source to mark it as processed
newRoot.setSource(newRoot);
// create a queue of vertices to process and prime with the new root
Queue<V> vertexQueue = new LinkedList<>();
vertexQueue.add(newRoot);
// follow edges assigning new source vertices
assignSource(vertexQueue);
// set the root source to null to indicate it is the root source vertex.
newRoot.setSource(null);
this.root = newRoot;
}
private void getDescendants(V source, Set<V> descendents) {
for (E e : getOutEdges(source)) {
V end = e.getEnd();
if (source.equals(end.source)) {
descendents.add(end);
getDescendants(end, descendents);
}
}
for (E e : getInEdges(source)) {
V start = e.getStart();
if (source.equals(start.source)) {
descendents.add(start);
getDescendants(start, descendents);
}
}
}
private void assignSource(Queue<V> vertexQueue) {
while (!vertexQueue.isEmpty()) {
V remove = vertexQueue.remove();
processEdges(remove, vertexQueue);
}
}
private void processEdges(V v, Queue<V> vertexQueue) {
Collection<E> outEdges = getOutEdges(v);
for (EgEdge<V> edge : outEdges) {
V next = edge.getEnd();
if (next.getSourceVertex() == null) {
next.setSource(v);
vertexQueue.add(next);
}
}
Collection<E> inEdges = getInEdges(v);
for (E e : inEdges) {
V previous = e.getStart();
if (previous.getSourceVertex() == null) {
previous.setSource(v);
vertexQueue.add(previous);
}
}
}
}
@@ -0,0 +1,30 @@
/* ###
* 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 datagraph.graph.explore;
import ghidra.graph.viewer.edge.AbstractVisualEdge;
/**
* An edge for the {@link AbstractExplorationGraph}
* @param <V> The vertex type
*/
public abstract class EgEdge<V extends EgVertex> extends AbstractVisualEdge<V> {
public EgEdge(V start, V end) {
super(start, end);
}
}
@@ -0,0 +1,49 @@
/* ###
* 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 datagraph.graph.explore;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import com.google.common.base.Function;
import edu.uci.ics.jung.graph.Graph;
import edu.uci.ics.jung.visualization.RenderContext;
import ghidra.graph.viewer.VisualEdge;
import ghidra.graph.viewer.edge.VisualEdgeRenderer;
/**
* Edge renderer for {@link AbstractExplorationGraph}s. Using information from the vertices to
* vertically align incoming and outgoing edges with the corresponding inner pieces in the
* vertex's display component.
*
* @param <V> the vertex type
* @param <E> the edge type
*/
public class EgEdgeRenderer<V extends EgVertex, E extends VisualEdge<V>>
extends VisualEdgeRenderer<V, E> {
@Override
public Shape getEdgeShape(RenderContext<V, E> rc, Graph<V, E> graph, E e, float x1, float y1,
float x2, float y2, boolean isLoop, Shape vertexShape) {
Function<? super E, Shape> edgeXform = rc.getEdgeShapeTransformer();
Shape shape = edgeXform.apply(e);
AffineTransform xform = AffineTransform.getTranslateInstance(x1, y1);
// apply the transformations; converting the given shape from model space into graph space
return xform.createTransformedShape(shape);
}
}
@@ -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 datagraph.graph.explore;
import java.awt.Dimension;
import java.awt.Shape;
import java.awt.geom.*;
import com.google.common.base.Function;
import ghidra.graph.viewer.GraphViewerUtils;
import ghidra.graph.viewer.VisualEdge;
/**
* An edge shape that draws edges from left side of source vertex to the right side of the
* destination vertex. The vertical position on edges of the source vertex and destination is
* determined by the calls to the vertex so that edges can be aligned with a vertex's internals.
*
* @param <V> the vertex type
* @param <E> the edge type
*/
public class EgEdgeTransformer<V extends EgVertex, E extends VisualEdge<V>>
implements Function<E, Shape> {
private static final double OVERLAP_GAP = 20;
private static int LOOP_SIZE = 12;
/**
* Get the shape for this edge
*
* @param e the edge
* @return the edge shape
*/
@Override
public Shape apply(E e) {
V start = e.getStart();
V end = e.getEnd();
Dimension startSize = start.getComponent().getSize();
Point2D location = start.getLocation();
double originX = location.getX();
double originY = location.getY();
Point2D startPoint = start.getStartingEdgePoint(end);
Point2D endPoint = end.getEndingEdgePoint(start);
boolean isLoop = start.equals(end);
if (isLoop) {
Shape hollowEgdeLoop = GraphViewerUtils.createHollowEgdeLoop();
AffineTransform xform =
AffineTransform.getTranslateInstance(startSize.width / 2 + LOOP_SIZE / 2,
start.getOutgoingEdgeOffsetFromCenter(end));
xform.scale(LOOP_SIZE, LOOP_SIZE);
return xform.createTransformedShape(hollowEgdeLoop);
}
GeneralPath path = new GeneralPath();
path.moveTo(startPoint.getX() - originX, startPoint.getY() - originY);
if (startPoint.getX() > endPoint.getX() - OVERLAP_GAP) {
if (start.getLocation().getX() != startPoint.getX()) {
path.lineTo(startPoint.getX() - originX + OVERLAP_GAP, startPoint.getY() - originY);
}
if (end.getLocation().getX() != endPoint.getX()) {
path.lineTo(endPoint.getX() - originX - OVERLAP_GAP, endPoint.getY() - originY);
}
}
path.lineTo(endPoint.getX() - originX, endPoint.getY() - originY);
return path;
}
}
@@ -0,0 +1,268 @@
/* ###
* 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 datagraph.graph.explore;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.Point2D;
import java.util.*;
import java.util.function.Function;
import javax.help.UnsupportedOperationException;
import edu.uci.ics.jung.visualization.RenderContext;
import edu.uci.ics.jung.visualization.renderers.BasicEdgeRenderer;
import ghidra.graph.VisualGraph;
import ghidra.graph.viewer.layout.*;
import ghidra.graph.viewer.layout.LayoutListener.ChangeType;
import ghidra.graph.viewer.vertex.VisualGraphVertexShapeTransformer;
import ghidra.util.task.TaskMonitor;
/**
* A custom layout to arrange the vertices of an {@link AbstractExplorationGraph} using a tree
* structure that reflects the exploration order of the vertices. The basic algorithm is that
* an original vertex is the root vertex and all other vertices are displayed in a double tree
* structure that show a tree of outgoing edges to the right and a tree of incoming edges
* to the left. The immediate vertices at each level are simply shown in a vertical column.
* <P>
* However, the tricky concept is that each vertex can then have edges that go back. For example,
* if the original vertex has three outgoing vertices, but then those
* vertices can spawn other incoming vertices. In this case, the vertex is pushed further out to
* make room for its spawned incoming vertices. This is done recursively, where each child
* vertex's sub tree is computed and then all the child subtrees are stacked in a column.
*
* @param <V> the vertex type
* @param <E> the edge type
*/
public abstract class EgGraphLayout<V extends EgVertex, E extends EgEdge<V>>
extends AbstractVisualGraphLayout<V, E> {
private EgEdgeRenderer<V, E> edgeRenderer = new EgEdgeRenderer<>();
private EgEdgeTransformer<V, E> edgeTransformer;
private Function<V, Shape> vertexShapeTransformer = new VisualGraphVertexShapeTransformer<V>();
protected int verticalGap;
protected int horizontalGap;
protected EgGraphLayout(AbstractExplorationGraph<V, E> graph, String name, int verticalGap,
int horizontalGap) {
super(graph, name);
this.verticalGap = verticalGap;
this.horizontalGap = horizontalGap;
this.edgeTransformer = createEdgeTransformer();
}
protected abstract EgEdgeTransformer<V, E> createEdgeTransformer();
protected abstract Comparator<V> getIncommingVertexComparator();
protected abstract Comparator<V> getOutgoingVertexComparator();
@Override
public Point2D apply(V v) {
if (v.hasUserChangedLocation()) {
return v.getLocation();
}
return super.apply(v);
}
@SuppressWarnings("unchecked")
@Override
public VisualGraph<V, E> getVisualGraph() {
return (VisualGraph<V, E>) getGraph();
}
@Override
public BasicEdgeRenderer<V, E> getEdgeRenderer() {
return edgeRenderer;
}
@Override
public com.google.common.base.Function<E, Shape> getEdgeShapeTransformer(
RenderContext<V, E> context) {
return edgeTransformer;
}
@Override
protected LayoutPositions<V, E> doCalculateLocations(VisualGraph<V, E> g,
TaskMonitor taskMonitor) {
if (!(g instanceof AbstractExplorationGraph<V, E> layeredGraph)) {
throw new IllegalArgumentException("This layout only supports Layered graphs!");
}
try {
monitor = taskMonitor;
return computePositions(layeredGraph);
}
finally {
monitor = TaskMonitor.DUMMY;
}
}
private LayoutPositions<V, E> computePositions(AbstractExplorationGraph<V, E> g) {
GraphLocationMap<V> locationMap = getLocationMap(g, g.getRoot());
Map<V, Point2D> vertexLocations = locationMap.getVertexLocations();
return LayoutPositions.createNewPositions(vertexLocations,
Collections.emptyMap());
}
@Override
protected GridLocationMap<V, E> performInitialGridLayout(VisualGraph<V, E> g) {
// we override the method that calls this abstract method, so it isn't used.
throw new UnsupportedOperationException();
}
private GraphLocationMap<V> getLocationMap(AbstractExplorationGraph<V, E> g, V v) {
List<GraphLocationMap<V>> leftMaps = getMapsForIncommingEdges(g, v);
List<GraphLocationMap<V>> rightMaps = getMapsForOutgoingEdges(g, v);
Shape shape = vertexShapeTransformer.apply(v);
Rectangle bounds = shape.getBounds();
GraphLocationMap<V> baseMap = new GraphLocationMap<>(v, bounds.width, bounds.height);
if (leftMaps != null) {
mergeLeftMaps(baseMap, leftMaps);
}
if (rightMaps != null) {
mergeRightMaps(baseMap, rightMaps);
}
return baseMap;
}
/**
* Merges all the incoming vertex sub-tree maps in a column to the left of the base map. Since
* these maps will all be to the left of their parent vertex base map, we align them in the
* column such that their right map edge boundaries align.
* @param baseMap the map for the parent vertex
* @param leftMaps the list of maps to be organized in a column to the left of the base map.
*/
private void mergeLeftMaps(GraphLocationMap<V> baseMap, List<GraphLocationMap<V>> leftMaps) {
int shiftY = getTopGroupShift(leftMaps, verticalGap);
int baseShiftX = baseMap.getMinX() - horizontalGap;
for (GraphLocationMap<V> map : leftMaps) {
shiftY += map.getHeight() / 2;
int shiftX = baseShiftX - map.getMaxX();
baseMap.merge(map, shiftX, shiftY);
shiftY += map.getHeight() / 2 + verticalGap;
}
}
/**
* Merges all the outgoing vertex sub-tree maps in a column to the right of the base map. Since
* these maps will all be to the right of their parent vertex base map, we align them in the
* column such that their left map edge boundaries align.
* @param baseMap the map for the parent vertex
* @param rightMaps the list of maps to be organized in a column to the right of the base map.
*/
private void mergeRightMaps(GraphLocationMap<V> baseMap, List<GraphLocationMap<V>> rightMaps) {
int shiftY = getTopGroupShift(rightMaps, verticalGap);
int baseShiftX = baseMap.getMaxX() + horizontalGap;
for (GraphLocationMap<V> map : rightMaps) {
shiftY += map.getHeight() / 2;
int shiftX = baseShiftX - map.getMinX();
baseMap.merge(map, shiftX, shiftY);
shiftY += map.getHeight() / 2 + verticalGap;
}
}
private int getTopGroupShift(List<GraphLocationMap<V>> maps, int gap) {
int totalHeight = 0;
for (GraphLocationMap<V> map : maps) {
totalHeight += map.getHeight();
}
totalHeight += (maps.size() - 1) * gap;
return -totalHeight / 2;
}
private List<GraphLocationMap<V>> getMapsForOutgoingEdges(AbstractExplorationGraph<V, E> g,
V v) {
List<E> edges = getOutgoingNextLayerEdges(g, v);
if (edges == null || edges.isEmpty()) {
return null;
}
return getOutgoingGraphMaps(g, edges);
}
private List<GraphLocationMap<V>> getMapsForIncommingEdges(AbstractExplorationGraph<V, E> g,
V v) {
List<E> edges = getIncommingNextLayerEdges(g, v);
if (edges == null || edges.isEmpty()) {
return null;
}
return getIncomingGraphMaps(g, edges);
}
private List<GraphLocationMap<V>> getOutgoingGraphMaps(AbstractExplorationGraph<V, E> g,
List<E> edges) {
List<GraphLocationMap<V>> maps = new ArrayList<>(edges.size());
for (E e : edges) {
maps.add(getLocationMap(g, e.getEnd()));
}
return maps;
}
private List<GraphLocationMap<V>> getIncomingGraphMaps(AbstractExplorationGraph<V, E> g,
List<E> edges) {
List<GraphLocationMap<V>> maps = new ArrayList<>(edges.size());
for (E e : edges) {
maps.add(getLocationMap(g, e.getStart()));
}
return maps;
}
private List<E> getOutgoingNextLayerEdges(AbstractExplorationGraph<V, E> g, V v) {
Collection<E> outEdges = g.getOutEdges(v);
if (outEdges == null || outEdges.isEmpty()) {
return null;
}
List<E> nextLayerEdges = new ArrayList<>();
for (E e : outEdges) {
if (v.equals(e.getEnd().getSourceVertex())) {
nextLayerEdges.add(e);
}
}
Comparator<V> c = getOutgoingVertexComparator();
nextLayerEdges.sort((e1, e2) -> c.compare(e1.getEnd(), e2.getEnd()));
return nextLayerEdges;
}
private List<E> getIncommingNextLayerEdges(AbstractExplorationGraph<V, E> g, V v) {
Collection<E> inEdges = g.getInEdges(v);
if (inEdges == null || inEdges.isEmpty()) {
return null;
}
List<E> nextLayerEdges = new ArrayList<>();
for (E e : inEdges) {
if (v.equals(e.getStart().getSourceVertex())) {
nextLayerEdges.add(e);
}
}
Comparator<V> c = getIncommingVertexComparator();
nextLayerEdges.sort((e1, e2) -> c.compare(e1.getStart(), e2.getStart()));
return nextLayerEdges;
}
@Override
protected void fireVertexLocationChanged(V v, Point2D p, ChangeType type) {
if (type == ChangeType.USER) {
v.setUserChangedLocation(new Point2D.Double(p.getX(), p.getY()));
}
super.fireVertexLocationChanged(v, p, type);
}
}
@@ -0,0 +1,129 @@
/* ###
* 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 datagraph.graph.explore;
import java.awt.geom.Point2D;
import ghidra.graph.viewer.vertex.AbstractVisualVertex;
/**
* A type of VisualVertex that is part of a exploration graph where each vertex (except the root
* vertex) has a concept of a source vertex that indicates what vertex's edge was first used to add
* this vertex to the graph. Any vertex in the graph can follow the source vertex and it's source
* vertex all the way back to the root source vertex. This means that any graph consisting of
* {@link EgVertex}s, regardless of its full set of edges, has a natural representation as a tree
* which is useful for creating a layout that reflects how the tree was generated from a single
* source seed vertex.
*/
public abstract class EgVertex extends AbstractVisualVertex {
protected EgVertex source;
private boolean userChangedLocation = false;
/**
* Constructor
* @param source the vertex from which this vertex was added by following an edge
*/
public EgVertex(EgVertex source) {
this.source = source;
}
/**
* {@return the vertex that was used to discover this vertex.}
*/
public EgVertex getSourceVertex() {
return source;
}
@Override
public void dispose() {
// subclasses can override this if they need to perform cleanup
}
@Override
public void setLocation(Point2D location) {
if (userChangedLocation) {
return;
}
super.setLocation(location);
}
/**
* Manually sets the location for this vertex.
* @param location the location for the vertex
*/
public void setUserChangedLocation(Point2D location) {
userChangedLocation = true;
super.setLocation(location);
}
/**
* Clears the user changed location lock, allowing the vertex to get a new location when the
* graph is laid out.
*/
public void clearUserChangedLocation() {
userChangedLocation = false;
}
/**
* {@return true if the user manually set the location. A manually set location ignores
* locations generated by the layout.}
*/
public boolean hasUserChangedLocation() {
return userChangedLocation;
}
/**
* {@return true if this is the root node for the exploration graph.}
*/
public boolean isRoot() {
return source == null;
}
protected void setSource(EgVertex source) {
this.source = source;
}
/**
* {@return the point to use for the first point in an edge going to the given end vertex.}
* @param end the destination vertex for the edge
*/
protected abstract Point2D getStartingEdgePoint(EgVertex end);
/**
* {@return the point to use for the last point in an edge coming from the given start vertex.}
* @param start the starting vertex for the edge
*/
protected abstract Point2D getEndingEdgePoint(EgVertex start);
/**
* {@return the outgoing offset of the edge from the center of the vertex for the edge ending
* at the given end vertex.}
* @param end the destination vertex of the edge to get an outgoing edge offset for
*/
protected int getOutgoingEdgeOffsetFromCenter(EgVertex end) {
return 0;
}
/**
* {@return the incoming offset of the edge from the center of the vertex from the edge starting
* at the given start vertex.}
* @param start the starting vertex of the edge to get an incoming edge offset for
*/
protected int getIncomingEdgeOffsetFromCenter(EgVertex start) {
return 0;
}
}
@@ -0,0 +1,130 @@
/* ###
* 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 datagraph.graph.explore;
import java.awt.Point;
import java.awt.geom.Point2D;
import java.util.*;
/**
* Map of vertex locations in layout space and the boundaries for the space this sub graph
* occupies.
*
* @param <V> the vertex type
*/
public class GraphLocationMap<V> {
private Map<V, Point> vertexPoints = new HashMap<>();
private int minX;
private int maxX;
private int minY;
private int maxY;
/**
* Constructs a map with exactly one vertex which is located at P(0,0). The size of the vertex
* determines the map's bounds.
* @param vertex the initial vertex
* @param width the width of the the verte.
* @param height the height of the vertex
*/
public GraphLocationMap(V vertex, int width, int height) {
vertexPoints.put(vertex, new Point(0, 0));
this.maxX = (width + 1) / 2; // add 1 so that odd sizes have the extra size to the right
this.minX = -width / 2;
this.maxY = (height + 1) / 2; // add 1 so that odd sizes have the extra size to the bottom
this.minY = -height / 2;
}
/**
* Merges another {@link GraphLocationMap} into this one shifting its bounds and bounds by the
* given shift values. When this completes, the other map is disposed.
* @param other the other GridLocationMap to merge into this one
* @param xShift the amount to shift the other map horizontally before merging.
* @param yShift the amount to shift the other map vertically before merging.
*/
public void merge(GraphLocationMap<V> other, int xShift, int yShift) {
other.shift(xShift, yShift);
vertexPoints.putAll(other.vertexPoints);
this.minX = Math.min(minX, other.minX);
this.maxX = Math.max(maxX, other.maxX);
this.minY = Math.min(minY, other.minY);
this.maxY = Math.max(maxY, other.maxY);
other.dispose();
}
private void dispose() {
vertexPoints.clear();
}
private void shift(int xShift, int yShift) {
Collection<Point> values = vertexPoints.values();
for (Point point : values) {
point.x += xShift;
point.y += yShift;
}
maxX += xShift;
minX += xShift;
maxY += yShift;
minY += yShift;
}
/**
* {@return the width of this map. This includes space for the size of vertices and not just
* their center points.}
*/
public int getWidth() {
return maxX - minX;
}
/**
* {@return the height of this map. This includes space for the size of vertices and not just
* their center points.}
*/
public int getHeight() {
return maxY - minY;
}
/**
* {@return the location for the given vertex.}
* @param v the vertex to get a location for
*/
public Point get(V v) {
return vertexPoints.get(v);
}
/**
* {@return a map of the vertices and their locations.}
*/
public Map<V, Point2D> getVertexLocations() {
Map<V, Point2D> points = new HashMap<>();
points.putAll(vertexPoints);
return points;
}
/**
* {@return the minimum x coordinate of the graph.}
*/
public int getMinX() {
return minX;
}
/**
* {@return the maximum x coordinate of the graph.}
*/
public int getMaxX() {
return maxX;
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 961 B

@@ -0,0 +1,304 @@
/* ###
* 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 datagraph.graph;
import static org.junit.Assert.*;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.geom.Point2D;
import java.util.Collection;
import java.util.Comparator;
import javax.swing.*;
import org.junit.Before;
import org.junit.Test;
import datagraph.graph.explore.*;
import docking.test.AbstractDockingTest;
import docking.widgets.label.GDLabel;
import generic.theme.GThemeDefaults.Colors.Palette;
import ghidra.graph.VisualGraph;
import ghidra.graph.graphs.DefaultVisualGraph;
import ghidra.graph.viewer.GraphComponent;
import ghidra.graph.viewer.layout.AbstractVisualGraphLayout;
import ghidra.util.Swing;
public class EgGraphLayoutTest extends AbstractDockingTest {
private static int VERTEX_SIZE = 50;
private static int VERTEX_GAP = 100;
private TestExplorationGraph g;
private TestVertex root = new TestVertex(null, "R", VERTEX_SIZE, VERTEX_SIZE);
private GraphComponent<TestVertex, TestEdge, TestExplorationGraph> graphComponent;
@Before
public void setUp() {
g = new TestExplorationGraph(root);
}
@Test
public void testGraphOneOutgoingChild() {
TestVertex A = v(root, "A");
edge(root, A);
showGraph();
assertEquals(p(0, 0), root.getLocation());
int expectedX = VERTEX_GAP + root.width / 2 + A.width / 2;
assertEquals(p(expectedX, 0), A.getLocation());
}
@Test
public void testGraphTwoOutgoingChildren() {
TestVertex A = v(root, "A");
TestVertex B = v(root, "B");
edge(root, A);
edge(root, B);
showGraph();
assertEquals(p(0, 0), root.getLocation());
int expectedX = VERTEX_GAP + root.width / 2 + A.width / 2;
int totalHeight = VERTEX_GAP + A.height + B.height;
int expectedYa = -totalHeight / 2 + A.height / 2;
int expectedYb = totalHeight / 2 - B.height / 2;
assertEquals(p(expectedX, expectedYa), A.getLocation());
assertEquals(p(expectedX, expectedYb), B.getLocation());
}
@Test
public void testGraphOneIncomingChild() {
TestVertex A = v(root, "A", VERTEX_SIZE, VERTEX_SIZE);
edge(A, root);
showGraph();
assertEquals(p(0, 0), root.getLocation());
int expectedX = -VERTEX_GAP - root.width / 2 - A.width / 2;
assertEquals(p(expectedX, 0), A.getLocation());
}
@Test
public void testGraphTwoIncomingChildren() {
TestVertex A = v(root, "A");
TestVertex B = v(root, "B");
edge(A, root);
edge(B, root);
showGraph();
assertEquals(p(0, 0), root.getLocation());
int expectedX = -VERTEX_GAP - root.width / 2 - A.width / 2;
int expectedYa = -VERTEX_GAP / 2 - A.height / 2;
int expectedYb = VERTEX_GAP / 2 + B.height / 2;
assertEquals(p(expectedX, expectedYa), A.getLocation());
assertEquals(p(expectedX, expectedYb), B.getLocation());
}
@Test
public void testGraphTwoOutgoingChildrenDifferentSize() {
TestVertex A = v(root, "A");
TestVertex B = bigV(root, "B");
edge(root, A);
edge(root, B);
showGraph();
assertEquals(p(0, 0), root.getLocation());
int expectedXa = VERTEX_GAP + root.width / 2 + A.width / 2;
int expectedXb = VERTEX_GAP + root.width / 2 + B.width / 2;
int totalHeight = VERTEX_GAP + A.height + B.height;
int expectedYa = -totalHeight / 2 + A.height / 2;
int expectedYb = totalHeight / 2 - B.height / 2;
assertEquals(p(expectedXa, expectedYa), A.getLocation());
assertEquals(p(expectedXb, expectedYb), B.getLocation());
}
@Test
public void testGraphTwoIncomingChildrenDifferentSize() {
TestVertex A = v(root, "A");
TestVertex B = bigV(root, "B");
edge(A, root);
edge(B, root);
showGraph();
assertEquals(p(0, 0), root.getLocation());
int expectedXa = -VERTEX_GAP - root.width / 2 - A.width / 2;
int expectedXb = -VERTEX_GAP - root.width / 2 - B.width / 2;
int totalHeight = VERTEX_GAP + A.height + B.height;
int expectedYa = -totalHeight / 2 + A.height / 2;
int expectedYb = totalHeight / 2 - B.height / 2;
assertEquals(p(expectedXa, expectedYa), A.getLocation());
assertEquals(p(expectedXb, expectedYb), B.getLocation());
}
private Point p(int x, int y) {
return new Point(x, y);
}
protected void showGraph() {
Swing.runNow(() -> {
JFrame frame = new JFrame("Graph Viewer Test");
TestEgGraphLayout layout = new TestEgGraphLayout(g, root);
g.setLayout(layout);
graphComponent = new GraphComponent<>(g);
graphComponent.setSatelliteVisible(false);
frame.setSize(new Dimension(800, 800));
frame.setLocation(2400, 100);
frame.getContentPane().add(graphComponent.getComponent());
frame.setVisible(true);
frame.validate();
});
}
protected TestVertex bigV(TestVertex source, String name) {
TestVertex v = new TestVertex(source, name, VERTEX_SIZE * 2, VERTEX_SIZE * 2);
g.addVertex(v);
return v;
}
protected TestVertex v(TestVertex source, String name) {
TestVertex v = new TestVertex(source, name, VERTEX_SIZE, VERTEX_SIZE);
g.addVertex(v);
return v;
}
private TestVertex v(TestVertex source, String name, int width, int height) {
TestVertex v = new TestVertex(source, name, width, height);
g.addVertex(v);
return v;
}
protected TestEdge edge(TestVertex v1, TestVertex v2) {
TestEdge testEdge = new TestEdge(v1, v2);
g.addEdge(testEdge);
return testEdge;
}
private class TestVertex extends EgVertex {
private JLabel label;
private String name;
private int width;
private int height;
TestVertex(TestVertex source, String name, int width, int height) {
super(source);
this.name = name;
this.width = width;
this.height = height;
}
@Override
public String toString() {
return name;
}
@Override
public JComponent getComponent() {
if (label == null) {
label = new GDLabel();
label.setText(name);
label.setPreferredSize(new Dimension(width, height));
label.setBackground(Palette.GOLD);
label.setOpaque(true);
label.setBorder(BorderFactory.createRaisedBevelBorder());
label.setHorizontalAlignment(SwingConstants.CENTER);
}
return label;
}
@Override
protected Point2D getStartingEdgePoint(EgVertex end) {
return new Point(0, 0);
}
@Override
protected Point2D getEndingEdgePoint(EgVertex start) {
return new Point(0, 0);
}
}
private class TestEdge extends EgEdge<TestVertex> {
public TestEdge(TestVertex start, TestVertex end) {
super(start, end);
}
@SuppressWarnings("unchecked")
// Suppressing warning on the return type; we know our class is the right type
@Override
public TestEdge cloneEdge(TestVertex start, TestVertex end) {
return new TestEdge(start, end);
}
}
private class TestExplorationGraph extends AbstractExplorationGraph<TestVertex, TestEdge> {
TestExplorationGraph(TestVertex root) {
super(root);
}
@Override
public DefaultVisualGraph<TestVertex, TestEdge> copy() {
Collection<TestVertex> v = getVertices();
Collection<TestEdge> e = getEdges();
TestExplorationGraph newGraph = new TestExplorationGraph(getRoot());
v.forEach(newGraph::addVertex);
e.forEach(newGraph::addEdge);
return newGraph;
}
}
private static class TestEgGraphLayout
extends EgGraphLayout<TestVertex, TestEdge> {
protected TestEgGraphLayout(TestExplorationGraph graph, TestVertex root) {
super(graph, "Test", VERTEX_GAP, VERTEX_GAP);
}
@Override
public AbstractVisualGraphLayout<TestVertex, TestEdge> createClonedLayout(
VisualGraph<TestVertex, TestEdge> newGraph) {
throw new UnsupportedOperationException();
}
@Override
protected EgEdgeTransformer<TestVertex, TestEdge> createEdgeTransformer() {
return new EgEdgeTransformer<EgGraphLayoutTest.TestVertex, EgGraphLayoutTest.TestEdge>();
}
@Override
protected Comparator<TestVertex> getIncommingVertexComparator() {
return (v1, v2) -> v1.name.compareTo(v2.name);
}
@Override
protected Comparator<TestVertex> getOutgoingVertexComparator() {
return (v1, v2) -> v1.name.compareTo(v2.name);
}
}
}
@@ -0,0 +1,91 @@
/* ###
* 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 datagraph.graph;
import static org.junit.Assert.*;
import java.awt.Point;
import org.junit.Test;
import datagraph.graph.explore.GraphLocationMap;
public class GraphLocationMapTest {
private GraphLocationMap<TestVertex> map1;
private GraphLocationMap<TestVertex> map2;
@Test
public void testInitialState() {
TestVertex v = new TestVertex("A");
assertEquals("A", v.getName());
map1 = new GraphLocationMap<>(v, 10, 20);
assertEquals(10, map1.getWidth());
assertEquals(20, map1.getHeight());
assertEquals(p(0, 0), map1.get(v));
}
@Test
public void testMergeRight() {
TestVertex a = new TestVertex("A");
TestVertex b = new TestVertex("B");
map1 = new GraphLocationMap<>(a, 10, 20);
map2 = new GraphLocationMap<>(b, 100, 50);
// merge maps left to right, shifting map2 by 1000
map1.merge(map2, 1000, 0);
assertEquals(1055, map1.getWidth()); // width of both maps + the gap
assertEquals(50, map1.getHeight()); // height of the tallest map
assertEquals(p(0, 0), map1.get(a)); // point in first map doesn't move
assertEquals(p(1000, 0), map1.get(b));// point in second map moved by shift
}
@Test
public void testMergeBottom() {
TestVertex a = new TestVertex("A");
TestVertex b = new TestVertex("B");
map1 = new GraphLocationMap<>(a, 10, 20);
map2 = new GraphLocationMap<>(b, 100, 50);
// merge maps left to right, shifting map2 by 1000
map1.merge(map2, 0, 1000);
assertEquals(100, map1.getWidth()); // width of both maps + the gap
assertEquals(1035, map1.getHeight()); // height of the tallest map
assertEquals(p(0, 0), map1.get(a)); // point in first map doesn't move
assertEquals(p(0, 1000), map1.get(b));// point in second map moved by shift
}
private Point p(int x, int y) {
return new Point(x, y);
}
private class TestVertex {
private String name;
TestVertex(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
}
@@ -54,7 +54,7 @@
<tocdef id="Function Graph" text="Function Graph" target="help/topics/FunctionGraphPlugin/Function_Graph.html" >
<tocdef id="Primary View" sortgroup="a" text="Primary View" target="help/topics/FunctionGraphPlugin/Function_Graph.html#Primary_View" />
<tocdef id="Satellite View" sortgroup="b" text="Satellite View" target="help/topics/FunctionGraphPlugin/Function_Graph.html#Satellite_View" />
<tocdef id="Satellite View" sortgroup="b" text="Satellite View" target="help/topics/VisualGraph/Visual_Graph.html#Satellite_View" />
<tocdef id="Vertices" sortgroup="c" text="Vertices" target="help/topics/FunctionGraphPlugin/Function_Graph.html#Vertices">
<tocdef id="Vertex Grouping" sortgroup="a" text="Vertex Grouping" target="help/topics/FunctionGraphPlugin/Function_Graph.html#Vertex_Grouping" />

Some files were not shown because too many files have changed in this diff Show More