Candidate release of source code.

This commit is contained in:
Dan 2019-03-26 13:45:32 -04:00
parent db81e6b3b0
commit 79d8f164f8
12449 changed files with 2800756 additions and 16 deletions

View file

@ -0,0 +1,60 @@
<?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="FunctionCallGraph"
text="Function Call Graph"
target="help/topics/FunctionCallGraphPlugin/Function_Call_Graph.html" />
</tocref>
</tocroot>

View file

@ -0,0 +1,58 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
WARNING!
This file is copied to all help directories. If you change this file, you must copy it
to each src/main/help/help/shared directory.
Java Help Note: JavaHelp does not accept sizes (like in 'margin-top') in anything but
px (pixel) or with no type marking.
*/
body { margin-bottom: 50px; margin-left: 10px; margin-right: 10px; margin-top: 10px; } /* some padding to improve readability */
li { font-family:times new roman; font-size:14pt; }
h1 { color:#000080; font-family:times new roman; font-size:36pt; font-style:italic; font-weight:bold; text-align:center; }
h2 { margin: 10px; margin-top: 20px; color:#984c4c; font-family:times new roman; font-size:18pt; font-weight:bold; }
h3 { margin-left: 10px; margin-top: 20px; color:#0000ff; font-family:times new roman; font-size:14pt; font-weight:bold; }
h4 { margin-left: 10px; margin-top: 20px; font-family:times new roman; font-size:14pt; font-style:italic; }
/*
P tag code. Most of the help files nest P tags inside of blockquote tags (the was the
way it had been done in the beginning). The net effect is that the text is indented. In
modern HTML we would use CSS to do this. We need to support the Ghidra P tags, nested in
blockquote tags, as well as naked P tags. The following two lines accomplish this. Note
that the 'blockquote p' definition will inherit from the first 'p' definition.
*/
p { margin-left: 40px; font-family:times new roman; font-size:14pt; }
blockquote p { margin-left: 10px; }
p.providedbyplugin { color:#7f7f7f; margin-left: 10px; font-size:14pt; margin-top:100px }
p.ProvidedByPlugin { color:#7f7f7f; margin-left: 10px; font-size:14pt; margin-top:100px }
p.relatedtopic { color:#800080; margin-left: 10px; font-size:14pt; }
p.RelatedTopic { color:#800080; margin-left: 10px; font-size:14pt; }
/*
We wish for a tables to have space between it and the preceding element, so that text
is not too close to the top of the table. Also, nest the table a bit so that it is clear
the table relates to the preceding text.
*/
table { margin-left: 20px; margin-top: 10px; width: 80%;}
td { font-family:times new roman; font-size:14pt; vertical-align: top; }
th { font-family:times new roman; font-size:14pt; font-weight:bold; background-color: #EDF3FE; }
code { color: black; font-family: courier new; font-size: 14pt; }

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,248 @@
<!DOCTYPE doctype PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN">
<HTML>
<HEAD>
<TITLE>Function Call Graph Plugin</TITLE>
<META http-equiv="Content-Type" content="text/html; charset=windows-1252">
<LINK rel="stylesheet" type="text/css" href="../../shared/Frontpage.css">
</HEAD>
<BODY>
<H1><A name="FunctionCallGraphPlugin"></A> <A name="Display_Function_Call_Graph">
</A>Function Call Graph Plugin</H1>
<TABLE border="0" width="100%">
<TBODY>
<TR>
<TD align="center">
<IMG src="images/FunctionCallGraphProvider.png" alt="" border="1"></TD>
</TR>
</TBODY>
</TABLE>
<BLOCKQUOTE>
<P>The Function Call Graph Plugin is a simple graph display that shows incoming and
outgoing calls (as edges) for the function containing the current address, also known as
the <I>Source Function</I>, in the
Listing. This display provides some context for how a function is used within the
program. The functions are organized by <A HREF="#Level">Level</A>.
</P>
<P>
To show the Function Call Graph provider window,
select the <b>Window</b><img border="0" src="../../shared/arrow.gif">
<b>Function Call Graph</b> option on the tool menu.
</P>
<P>
The graph of function calls related to the source function being displayed can be
explored by <A HREF="#Expand_Collapse">adding existing function calls</A>
to the initial graph display.
</P>
<BLOCKQUOTE>
<P>
<IMG src="images/tip.png" border="0">The graph updates itself as you navigate within
the tool. To prevent losing graph state (e.g., expanded functions, node locations,
etc), a small number of graphs will be cached. For example, if you navigate
away from a function and then immediately return, the graph will be restored to
its previous state.
</P>
</BLOCKQUOTE>
<H2>Terms</H2>
<BLOCKQUOTE>
<UL>
<LI><B>Source Function</B>: the function that contains the current address in
the Listing. This function is considered the center of the graph, with all
other callers/callees added to the graph at a new level.
<BR>
<BR>
</LI>
<LI><A NAME="Level"></A><B>Level</B>: Each function node in the graph belongs to a
level. The source function is at level 1; the source function's incoming
calls are at level 2; the source function's outgoing calls are also at level 2.
Organizing functions by level allows the user to quickly see how many hops, or
calls, a given function is from the source function.
<BR>
<BR>
New levels of calls can be added to the graph by the user.
<BR>
<BR>
</LI>
<LI><B>Direction</B>: Each function node, other than the source function, is
considered to be in one of two directions: In or Out. All function nodes in
a given level share the same direction. So, all nodes that directly call the
source function node are considered to be the <I>In</I> direction; all nodes
directly called by the source function are considered to be the <I>Out</I>
direction.
<BR>
<BR>
When a given node's level is expanded in the graph, the nodes added are based
upon the selected node's direction: for <I>In</I> nodes, the newly added nodes
will be those nodes that <B>call</B> the selected node; for <I>Out</I> nodes, the
newly added nodes will be those nodes <B>called by</B> the selected node.
<BR>
<BR>
</LI>
<LI><B>Direct Edges</B>: An edge (a call) between two adjacent levels.
<BR>
<BR>
</LI>
<LI><A NAME="Indirect_Edges"></A><B>Indirect Edges</B>: An edge (a call)
between two non-adjacent levels or an edge within the same level. These
edges are rendered with less emphasis than <I>direct edges</I>.
<BR>
<BR>
</LI>
</UL>
</BLOCKQUOTE>
<H2>Actions</H2>
<BLOCKQUOTE>
<H3><A NAME="Expand_Collapse"></A>Show/Hide Edges Action</H3>
<P>
Within the <I>Function Call Graph</I> you can show and hide function calls
as desired.
Showing additional function calls can be accomplished multiple ways. From
any function node, you can select the <I>Expand</I> icon (
<IMG SRC="Icons.EXPAND_ALL_ICON" />), which appears
on a node when hovered. When clicked, this button will toggle related
function calls: showing them if not already in the graph; hiding them
if they are in the graph.
</P>
<P>
Additionally, these same functionality is provided from the popup menu
actions (i.e., <B>Show/Hide Incoming Edges</B> and </B>Show/Hide Outgoing
Edges</B>).
</P>
<BLOCKQUOTE>
<P>
<IMG src="images/note.png" border="0">As new vertices are added to the
graph, any <A HREF="#Indirect_Edges">indirect edges</A> will be added to the
graph.
</P>
</BLOCKQUOTE>
<P>Note here how new vertices may appear in odd places when expanding (such as
when they are already in the graph at a previous level).
<BLOCKQUOTE>
<P>
<IMG src="../../shared/warning.png" border="0">It is important to understand
that the graph is only a subset of the entire program graph. This
graph does not represent all functions and function calls in the
program.
</P>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>
<IMG src="../../shared/warning.png" border="0">Sometimes a function
may have too many references to display in the graph. When this
happens, the function node will be a gray color, with the expand icon
replaced with a warning icon, as so:<BR>
<CENTER>
<IMG src="images/TooManyReferences.png" border="1">
</CENTER>
</P>
</BLOCKQUOTE>
</BLOCKQUOTE>
<BLOCKQUOTE>
<H3>Show/Hide Level Edges Action</H3>
<P>
All functions that relate to the <A HREF="#Level">Level</A> of
the selected function will be shown, <b>not just calls to the selected
function.</b>
</P>
</BLOCKQUOTE>
<BLOCKQUOTE>
<H3><A NAME="Navigation_Incoming"></A>Navigate on Incoming Location Changes</H3>
<P>
This action (<IMG SRC="Icons.NAVIGATE_ON_INCOMING_EVENT_ICON" />), when
toggled on, upon receiving Program Location changes from the tool, will graph
the function containing that location. When toggled off, location changes will
not affect the graph.
</P>
<P>
Having this action on is useful if you wish to quickly see the graph of
different functions as you navigate the program. Alternatively, having this
action off is useful when you wish to explore the program by navigating from
within the graph, say by double-clicking function nodes in the graph.
</P>
</BLOCKQUOTE>
<BLOCKQUOTE>
<H3><A NAME="Relayout_Graph"></A>Layout Action</H3>
<P>This action (<IMG SRC="Icons.REFRESH_ICON" />) will relayout the current graph and
reset the graph to show only the initial nodes.
</P>
</BLOCKQUOTE>
<BLOCKQUOTE>
<H3><A NAME="Graph_Node_Function_Calls"></A>Graph '<I>Function Name</I>'</H3>
<P>This action is available from the popup menu of any node that is not the
currently graphed node. When pressed, this action will graph the clicked
function.
</P>
</BLOCKQUOTE>
<H2><A name="Satellite_View"></A>Satellite View</H2>
<BLOCKQUOTE>
<P>The Satellite View works exactly as the
<A href="help/topics/FunctionGraphPlugin/Function_Graph.html#Satellite_View">
Function Graph's Satellite View</A>.
</P>
</BLOCKQUOTE>
</BLOCKQUOTE>
<P class="providedbyplugin">Provided by: <I>Function Call Graph Plugin</I></P>
<P class="relatedtopic">Related Topics:</P>
<UL>
<!-- TODO update this when we introduce the generic graphing help -->
<LI>Graphs</LI>
</UL><BR>
<BR>
</BODY>
</HTML>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -0,0 +1,38 @@
/* ###
* 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 functioncalls.graph;
/**
* Represents whether a vertex is an incoming vertex (the start or from) on an edge,
* an outgoing vertex (the end or to) on an edge, or if it is both.
*/
public enum FcgDirection {
// this order is from top to bottom: In -> In/Out -> Out
IN, IN_AND_OUT, OUT;
public boolean isSource() {
return this == IN_AND_OUT;
}
public boolean isIn() {
return this == IN;
}
public boolean isOut() {
return this == OUT;
}
}

View file

@ -0,0 +1,58 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package functioncalls.graph;
import ghidra.graph.viewer.edge.AbstractVisualEdge;
/**
* A {@link FunctionCallGraph} edge
*/
public class FcgEdge extends AbstractVisualEdge<FcgVertex> {
public FcgEdge(FcgVertex start, FcgVertex end) {
super(start, end);
}
@SuppressWarnings("unchecked")
// Suppressing warning on the return type; we know our class is the right type
@Override
public FcgEdge cloneEdge(FcgVertex start, FcgVertex end) {
return new FcgEdge(start, end);
}
/**
* Returns true if this edge is a direct edge from a lower level. Any other edges are
* considered indirect and are less important in the graph.
*
* @return true if this edge is a direct edge from a lower level
*/
public boolean isDirectEdge() {
FcgLevel startLevel = getStart().getLevel();
FcgLevel endLevel = getEnd().getLevel();
if (startLevel.isSource() || endLevel.isSource()) {
// all info leaving the source is important/'direct'
return true;
}
FcgLevel parent = startLevel.parent();
if (parent.equals(endLevel)) {
return true;
}
FcgLevel child = startLevel.child();
return child.equals(endLevel);
}
}

View file

@ -0,0 +1,219 @@
/* ###
* 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 functioncalls.graph;
import static functioncalls.graph.FcgDirection.IN_AND_OUT;
import static functioncalls.graph.FcgDirection.OUT;
/**
* A container class that represents a {@link FunctionCallGraph} level, or row. A level
* is both the row of the vertex (the number of hops from the source vertex) and the
* direction.
*/
public class FcgLevel implements Comparable<FcgLevel> {
/** A 1-based */
private int row;
private FcgDirection direction;
public static FcgLevel sourceLevel() {
return new FcgLevel(0, IN_AND_OUT);
}
public FcgLevel(int distance, FcgDirection direction) {
this.row = toRow(distance);
this.direction = direction;
if (row == 0) {
throw new IllegalArgumentException("The FcgLevel uses a 1-based row system");
}
if (row == 1 && direction != IN_AND_OUT) {
throw new IllegalArgumentException("Row 1 must be FcgDirection.IN_AND_OUT");
}
}
private int toRow(int distance) {
int oneBased = distance + 1;
return (direction == OUT) ? -oneBased : oneBased;
}
public int getRow() {
return row;
}
public int getDistance() {
return Math.abs(row) - 1;
}
public FcgDirection getDirection() {
return direction;
}
/**
* Returns true if this level represents the source level from which all other levels
* emanate, which is row 1.
*
* @return true if this level represents the source level
*/
public boolean isSource() {
return direction.isSource();
}
/**
* Returns the parent level of this level. The parent of a level has the same direction
* as this level, with a distance of one less than this level.
*
* @return returns the parent level of this level
* @throws IllegalArgumentException if this is the source level, which is row 1
*/
public FcgLevel parent() {
if (direction == IN_AND_OUT) {
// undefined--we are the parent of all
throw new IllegalArgumentException(
"To get the parent of the source level you must use the constructor directly");
}
int newDistance = getDistance() - 1;
FcgDirection newDirection = direction;
if (newDistance == 0) {
newDirection = IN_AND_OUT;
}
return new FcgLevel(newDistance, newDirection);
}
/**
* Returns the child level of this level. The child of a level has the same direction
* as this level, with a distance of one more than this level.
*
* @return returns the child level of this level
* @throws IllegalArgumentException if this is the source level, which is row 1
*/
public FcgLevel child() {
if (direction == IN_AND_OUT) {
// undefined--this node goes in both directions
throw new IllegalArgumentException(
"To get the child of the source level you " + "must use the constructor directly");
}
return child(direction);
}
/**
* Returns true if this level is the immediate predecessor of the given other level.
*
* <P>The <tt>source</tt> level is the parent of the first level in either direction.
*
* @param other the other level that is a potential child level
* @return true if this level is the immediate predecessor of the given other level
*/
public boolean isParentOf(FcgLevel other) {
if (isSource()) {
return other.getDistance() == 1;
}
if (direction != other.direction) {
return false;
}
// e.g., row 2 - row 1 = 1
return other.getDistance() - getDistance() == 1;
}
/**
* Returns true if this level is the immediate successor of the given other level
*
* @param other the other level that is a potential child level
* @return true if this level is the immediate successor of the given other level
*/
public boolean isChildOf(FcgLevel other) {
return other.isParentOf(this);
}
/**
* Returns the child level of this level. The child of a level has the same direction
* as this level, with a distance of one more than this level.
*
* @param the direction of the child
* @return returns the child level of this level
* @throws IllegalArgumentException if this is the source level, which is row 1
*/
public FcgLevel child(FcgDirection newDirection) {
if (newDirection == IN_AND_OUT) {
// undefined--IN_AND_OUT goes in both directions
throw new IllegalArgumentException("Direction cannot be IN_AND_OUT");
}
int newDistance = getDistance() + 1;
return new FcgLevel(newDistance, newDirection);
}
@Override
public String toString() {
return direction + " - row " + Integer.toString(getRelativeRow());
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((direction == null) ? 0 : direction.hashCode());
result = prime * result + row;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
FcgLevel other = (FcgLevel) obj;
if (direction != other.direction) {
return false;
}
if (row != other.row) {
return false;
}
return true;
}
/**
* Returns the row of this vertex
* @return the row of this vertex
*/
private int getRelativeRow() {
return direction == OUT ? -row : row;
}
@Override
public int compareTo(FcgLevel l2) {
int result = getDirection().compareTo(l2.getDirection());
if (result != 0) {
return result; // compare by direction first In on top; Out on bottom
}
// same direction, use row
return -(getRelativeRow() - l2.getRelativeRow());
}
}

View file

@ -0,0 +1,621 @@
/* ###
* 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 functioncalls.graph;
import java.awt.*;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Ellipse2D.Double;
import java.awt.image.BufferedImage;
import java.util.Objects;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.LineBorder;
import docking.widgets.EmptyBorderButton;
import ghidra.graph.viewer.vertex.AbstractVisualVertex;
import ghidra.graph.viewer.vertex.VertexShapeProvider;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Function;
import ghidra.util.StringUtilities;
import resources.Icons;
import resources.ResourceManager;
/**
* A {@link FunctionCallGraph} vertex
*/
public class FcgVertex extends AbstractVisualVertex implements VertexShapeProvider {
// TODO to be made an option in an upcoming ticket
public static final Color DEFAULT_VERTEX_SHAPE_COLOR = new Color(110, 197, 174);
private static final Color TOO_BIG_VERTEX_SHAPE_COLOR = Color.LIGHT_GRAY;
public static final Icon NOT_ALLOWED_ICON = Icons.ERROR_ICON;
private static final Icon EXPAND_ICON =
ResourceManager.getScaledIcon(Icons.EXPAND_ALL_ICON, 10, 10);
private static final Icon COLLAPSE_ICON =
ResourceManager.getScaledIcon(Icons.COLLAPSE_ALL_ICON, 10, 10);
// higher numbered layers go on top
private static final Integer VERTEX_SHAPE_LAYER = new Integer(100);
private static final Integer TOGGLE_BUTTON_LAYER = new Integer(200);
private static final Integer LABEL_LAYER = new Integer(300);
private static final int GAP = 2;
private static final int VERTEX_SHAPE_SIZE = 50;
// TODO to be made an option in an upcoming ticket
// based upon the default function name, plus some extra
private static final int MAX_NAME_LENGTH = 30;
private Function function;
private JLayeredPane layeredPane;
private JButton toggleInsButton = new EmptyBorderButton(EXPAND_ICON);
private JButton toggleOutsButton = new EmptyBorderButton(EXPAND_ICON);
private JLabel nameLabel = new JLabel();
private JLabel vertexImageLabel = new JLabel();
private Double vertexShape;
private Double compactShape;
private Shape fullShape;
// these values are set after construction from external sources
private boolean hasIncomingReferences;
private boolean hasOutgoingReferences;
private boolean tooManyIncomingReferences;
private boolean tooManyOutgoingReferences;
private boolean incomingExpanded;
private boolean outgoingExpanded;
// set this to true to see borders around the components of this vertex
private boolean useDebugBorders = false;
private Paint inPaint;
private Paint outPaint;
private FcgLevel level;
/**
* Constructor
*
* @param function the function represented by this vertex
* @param level the level of this vertex
* @param expansionListener the listener for expanding connections to this vertex
*/
public FcgVertex(Function function, FcgLevel level,
FcgVertexExpansionListener expansionListener) {
this.function = function;
this.level = level;
Objects.requireNonNull(expansionListener);
toggleInsButton.addActionListener(e -> {
if (tooManyIncomingReferences) {
return;
}
expansionListener.toggleIncomingVertices(FcgVertex.this);
});
toggleOutsButton.addActionListener(e -> {
if (tooManyOutgoingReferences) {
return;
}
expansionListener.toggleOutgoingVertices(FcgVertex.this);
});
buildUi();
setTogglesVisible(false);
}
private void createPaints() {
Color vertexShapeColor = getVertexShapeColor();
Color lightColor = vertexShapeColor;
Color darkColor = vertexShapeColor.darker();
Color darkestColor = darkColor.darker();
int offset = 5 * level.getDistance();
int half = VERTEX_SHAPE_SIZE / 2;
int start = 0;
int end = half + offset;
// paint top-down: dark to light for incoming; light to dark for outgoing
inPaint = new LinearGradientPaint(new Point(0, start), new Point(0, end),
new float[] { .0f, .2f, 1f }, new Color[] { darkestColor, darkColor, lightColor });
start = half - offset; // (offset + 10);
end = VERTEX_SHAPE_SIZE;
outPaint = new LinearGradientPaint(new Point(0, start), new Point(0, end),
new float[] { .0f, .8f, 1f }, new Color[] { lightColor, darkColor, darkestColor });
}
private void buildUi() {
createPaints();
// init the components
String truncated = StringUtilities.trimMiddle(getName(), MAX_NAME_LENGTH);
nameLabel.setText(truncated);
buildVertexShape();
// calculate the needed size
layeredPane = new JLayeredPane();
Border border = createDebugBorder(new LineBorder(Color.YELLOW.darker(), 1));
layeredPane.setBorder(border);
updateLayeredPaneSize();
// layout the components
addVertexShape();
addToggleButtons();
addNameLabel();
buildFullShape();
}
private Border createDebugBorder(Border border) {
if (useDebugBorders) {
return border;
}
return BorderFactory.createEmptyBorder();
}
private void buildFullShape() {
// Note: this method assumes all bounds have been set
Area parent = new Area();
Area v = new Area(vertexShape);
Area name = new Area(nameLabel.getBounds());
parent.add(v);
parent.add(name);
// for now, the buttons only appear on hover, but if we want to avoid clipping when
// painting, we need to account for them in the shape's overall bounds
Area in = new Area(toggleInsButton.getBounds());
Area out = new Area(toggleOutsButton.getBounds());
parent.add(in);
parent.add(out);
fullShape = parent;
}
private void updateLayeredPaneSize() {
//
// The overall component size is the total width and height of all components, with any
// spacing between them.
//
Dimension shapeSize = vertexImageLabel.getPreferredSize();
Dimension nameLabelSize = nameLabel.getPreferredSize();
int height = shapeSize.height + GAP + nameLabelSize.height;
Dimension insSize = toggleInsButton.getPreferredSize();
Dimension outsSize = toggleOutsButton.getPreferredSize();
int buttonWidth = Math.max(insSize.width, outsSize.width);
int offset = buttonWidth / 3; // overlap the vertex shape
int width = offset + shapeSize.width;
width = Math.max(width, nameLabelSize.width);
layeredPane.setPreferredSize(new Dimension(width, height));
}
private void buildVertexShape() {
int w = VERTEX_SHAPE_SIZE;
int h = VERTEX_SHAPE_SIZE;
Double circle = new Ellipse2D.Double(0, 0, w, h);
BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = (Graphics2D) image.getGraphics();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
FcgDirection direction = level.getDirection();
if (direction.isSource()) {
g2.setColor(getVertexShapeColor());
}
else if (direction.isIn()) {
g2.setPaint(inPaint);
}
else {
g2.setPaint(outPaint);
}
g2.fill(circle);
g2.dispose();
vertexShape = circle;
compactShape = (Double) vertexShape.clone();
vertexImageLabel.setIcon(new ImageIcon(image));
Border border = createDebugBorder(new LineBorder(Color.PINK, 1));
vertexImageLabel.setBorder(border);
}
private Color getVertexShapeColor() {
if (isInDirection() && tooManyIncomingReferences) {
return TOO_BIG_VERTEX_SHAPE_COLOR;
}
if (isOutDirection() && tooManyOutgoingReferences) {
return TOO_BIG_VERTEX_SHAPE_COLOR;
}
return DEFAULT_VERTEX_SHAPE_COLOR;
}
private boolean isInDirection() {
FcgDirection direction = level.getDirection();
boolean isIn = direction.isIn() || direction.isSource();
return isIn;
}
private boolean isOutDirection() {
FcgDirection direction = level.getDirection();
boolean isOut = direction.isOut() || direction.isSource();
return isOut;
}
private void addVertexShape() {
Dimension parentSize = layeredPane.getPreferredSize();
Dimension size = vertexImageLabel.getPreferredSize();
// centered
int x = (parentSize.width / 2) - (size.width / 2);
int y = 0;
vertexImageLabel.setBounds(x, y, size.width, size.height);
Dimension shapeSize = vertexShape.getBounds().getSize();
// setFrame() will make sure the shape's x,y values are where they need to be
// for the later 'full shape' creation
vertexShape.setFrame(x, y, shapeSize.width, shapeSize.height);
layeredPane.add(vertexImageLabel, VERTEX_SHAPE_LAYER);
}
private void addNameLabel() {
Border border = createDebugBorder(new LineBorder(Color.GREEN, 1));
nameLabel.setBorder(border);
// assume the vertex label has been bounded
Rectangle parentBounds = vertexImageLabel.getBounds();
Dimension size = nameLabel.getPreferredSize();
// bottom, centered under the shape
int x = (parentBounds.x + (parentBounds.width / 2)) - (size.width / 2);
int y = parentBounds.y + parentBounds.height + GAP;
nameLabel.setBounds(x, y, size.width, size.height);
layeredPane.add(nameLabel, LABEL_LAYER);
}
private void addToggleButtons() {
// hide the button background
toggleInsButton.setBackground(new Color(255, 255, 255, 0));
toggleOutsButton.setBackground(new Color(255, 255, 255, 0));
Rectangle parentBounds = vertexImageLabel.getBounds();
Dimension size = toggleInsButton.getPreferredSize();
// upper toggle; upper-left
int x = parentBounds.x - (size.width / 3);
int y = 0;
toggleInsButton.setBounds(x, y, size.width, size.height);
layeredPane.add(toggleInsButton, TOGGLE_BUTTON_LAYER);
// lower toggle; lower-left, lined-up with the vertex shape
size = toggleOutsButton.getPreferredSize();
Dimension vertexSize = parentBounds.getSize();
y = vertexSize.height - size.height;
toggleOutsButton.setBounds(x, y, size.width, size.height);
layeredPane.add(toggleOutsButton, TOGGLE_BUTTON_LAYER);
}
public String getName() {
return function.getName();
}
public Function getFunction() {
return function;
}
public Address getAddress() {
return function.getEntryPoint();
}
public FcgLevel getLevel() {
return level;
}
public int getDegree() {
return level.getRow();
}
public FcgDirection getDirection() {
return level.getDirection();
}
public JButton getIncomingToggleButton() {
return toggleInsButton;
}
public JButton getOutgoingToggleButton() {
return toggleOutsButton;
}
/**
* Sets to true if this vertex is showing all edges in the incoming direction
*
* @param setExpanded true if this vertex is showing all edges in the incoming direction
*/
public void setIncomingExpanded(boolean setExpanded) {
validateIncomingExpandedState(setExpanded);
this.incomingExpanded = setExpanded;
toggleInsButton.setIcon(setExpanded ? COLLAPSE_ICON : EXPAND_ICON);
String hideShow = setExpanded ? "hide" : "show";
toggleInsButton.setToolTipText("Click to " + hideShow + " incoming edges");
}
private void validateOutgoingExpandedState(boolean isExpanding) {
if (isExpanding) {
if (!canExpandOutgoingReferences()) {
throw new IllegalStateException("Vertex cannot be expanded: " + this);
}
return;
}
// collapsing
if (!isOutgoingExpanded()) {
throw new IllegalStateException("Vertex cannot be collapsed: " + this);
}
}
private void validateIncomingExpandedState(boolean expanding) {
if (expanding) {
if (!canExpandIncomingReferences()) {
throw new IllegalStateException("Vertex cannot be expanded: " + this);
}
return;
}
// collapsing
if (!isIncomingExpanded()) {
throw new IllegalStateException("Vertex cannot be collapsed: " + this);
}
}
/**
* Returns true if this vertex is showing all edges in the incoming direction
*
* @return true if this vertex is showing all edges in the incoming direction
*/
public boolean isIncomingExpanded() {
return incomingExpanded;
}
/**
* Sets to true if this vertex is showing all edges in the outgoing direction
*
* @param setExpanded true if this vertex is showing all edges in the outgoing direction
*/
public void setOutgoingExpanded(boolean setExpanded) {
validateOutgoingExpandedState(setExpanded);
this.outgoingExpanded = setExpanded;
toggleOutsButton.setIcon(setExpanded ? COLLAPSE_ICON : EXPAND_ICON);
String hideShow = setExpanded ? "hide" : "show";
toggleInsButton.setToolTipText("Click to " + hideShow + " outgoing edges");
}
/**
* Returns true if this vertex is showing all edges in the outgoing direction
*
* @return true if this vertex is showing all edges in the outgoing direction
*/
public boolean isOutgoingExpanded() {
return outgoingExpanded;
}
/**
* Returns whether this vertex is fully expanded in its current direction
*
* @return whether this vertex is fully expanded in its current direction
*/
public boolean isExpanded() {
FcgDirection direction = level.getDirection();
if (direction.isSource()) {
return isIncomingExpanded() && isOutgoingExpanded();
}
if (direction.isIn()) {
return isIncomingExpanded();
}
return isOutgoingExpanded();
}
/**
* Sets whether this vertex has too many incoming references, where too many is subjectively
* defined by this class. Too many nodes in the display would ruin rendering and general
* usability.
*
* @param tooMany if there are too many references
*/
public void setTooManyIncomingReferences(boolean tooMany) {
this.tooManyIncomingReferences = tooMany;
toggleInsButton.setIcon(NOT_ALLOWED_ICON);
toggleInsButton.setToolTipText("Too many incoming references to show");
buildUi();
}
/**
* Sets whether this vertex has too many outgoing references, where too many is subjectively
* defined by this class. Too many nodes in the display would ruin rendering and general
* usability.
*
* @param tooMany if there are too many references
*/
public void setTooManyOutgoingReferences(boolean tooMany) {
this.tooManyOutgoingReferences = tooMany;
toggleOutsButton.setIcon(NOT_ALLOWED_ICON);
toggleOutsButton.setToolTipText("Too many outgoing references to show");
buildUi();
}
/**
* Returns whether this vertex has too many incoming references, where too many is subjectively
* defined by this class. Too many nodes in the display would ruin rendering and general
* usability.
*
* @return true if there are too many references
*/
public boolean hasTooManyIncomingReferences() {
return tooManyIncomingReferences;
}
/**
* Returns whether this vertex has too many outgoing references, where too many is subjectively
* defined by this class. Too many nodes in the display would ruin rendering and general
* usability.
*
* @return true if there are too many references
*/
public boolean hasTooManyOutgoingReferences() {
return tooManyOutgoingReferences;
}
/**
* Returns true if this vertex can expand itself in its current direction, or in either
* direction if this is a source vertex
*
* @return true if this vertex can be expanded
*/
public boolean canExpand() {
FcgDirection direction = level.getDirection();
if (direction.isSource()) {
return canExpandIncomingReferences() || canExpandOutgoingReferences();
}
if (direction.isIn()) {
return canExpandIncomingReferences();
}
return canExpandOutgoingReferences();
}
public boolean canExpandIncomingReferences() {
return hasIncomingReferences && !tooManyIncomingReferences && !incomingExpanded;
}
public boolean canExpandOutgoingReferences() {
return hasOutgoingReferences && !tooManyOutgoingReferences && !outgoingExpanded;
}
/**
* Sets whether this vertex has any incoming references
*
* @param hasIncoming true if this vertex has any incoming references
*/
public void setHasIncomingReferences(boolean hasIncoming) {
this.hasIncomingReferences = hasIncoming;
}
/**
* Sets whether this vertex has any outgoing references
*
* @param hasIncoming true if this vertex has any incoming references
*/
public void setHasOutgoingReferences(boolean hasOutgoing) {
this.hasOutgoingReferences = hasOutgoing;
}
@Override
public void setHovered(boolean hovered) {
super.setHovered(hovered);
setTogglesVisible(hovered);
}
private void setTogglesVisible(boolean visible) {
boolean isIn = isInDirection();
boolean turnOn = isIn && hasIncomingReferences && visible;
toggleInsButton.setVisible(turnOn);
boolean isOut = isOutDirection();
turnOn = isOut && hasOutgoingReferences && visible;
toggleOutsButton.setVisible(turnOn);
}
@Override
public JComponent getComponent() {
return layeredPane;
}
@Override
public Shape getCompactShape() {
return compactShape;
}
@Override
public Shape getFullShape() {
return fullShape;
}
@Override
public String toString() {
return getName();// + " @ " + level; // + " (" + System.identityHashCode(this) + ')';
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((function == null) ? 0 : function.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
FcgVertex other = (FcgVertex) obj;
return Objects.equals(function, other.function);
}
@Override
public void dispose() {
// nothing to do
}
}

View file

@ -0,0 +1,36 @@
/* ###
* 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 functioncalls.graph;
/**
* A listener to know when a vertex has been told to expand
*/
public interface FcgVertexExpansionListener {
/**
* Show or hide those vertices that are on incoming edges to v
*
* @param v the vertex
*/
public void toggleIncomingVertices(FcgVertex v);
/**
* Show or hide those vertices that are on outgoing edges to v
*
* @param v the vertex
*/
public void toggleOutgoingVertices(FcgVertex v);
}

View file

@ -0,0 +1,161 @@
/* ###
* 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 functioncalls.graph;
import java.util.*;
import org.apache.commons.collections4.IterableUtils;
import org.apache.commons.collections4.map.LazyMap;
import functioncalls.plugin.FunctionCallGraphPlugin;
import ghidra.graph.graphs.FilteringVisualGraph;
import ghidra.graph.viewer.layout.VisualGraphLayout;
import ghidra.program.model.listing.Function;
/**
* A graph for the {@link FunctionCallGraphPlugin}
*/
public class FunctionCallGraph extends FilteringVisualGraph<FcgVertex, FcgEdge> {
private VisualGraphLayout<FcgVertex, FcgEdge> layout;
private FcgVertex source;
private Map<Function, FcgVertex> verticesByFunction = new HashMap<>();
private Comparator<FcgVertex> vertexComparator =
(v1, v2) -> v1.getAddress().compareTo(v2.getAddress());
private Map<FcgLevel, Set<FcgVertex>> verticesByLevel =
LazyMap.lazyMap(new HashMap<>(), () -> new TreeSet<>(vertexComparator));
/**
* Sets the source vertex from which the graph is created
* @param source the source vertex from which the graph is created
*/
public void setSource(FcgVertex source) {
if (this.source != null) {
throw new IllegalStateException("Cannot change graph source once it has been created");
}
this.source = source;
addVertex(source);
}
/**
* Returns the vertex from which the graph is created
* @return the vertex from which the graph is created
*/
public FcgVertex getSource() {
return source;
}
/**
* Returns the vertex mapped to the given function; null if there is no matching vertex
*
* @param f the function
* @return the vertex
*/
public FcgVertex getVertex(Function f) {
return verticesByFunction.get(f);
}
/**
* Returns true if this graph contains a vertex for the given function
*
* @param f the function
* @return true if this graph contains a vertex for the given function
*/
public boolean containsFunction(Function f) {
return verticesByFunction.containsKey(f);
}
/**
* Returns all vertices in the given level. The result will be non-null.
*
* @param level the level of the vertices to retrieve
* @return all vertices in the given level
*/
public Iterable<FcgVertex> getVerticesByLevel(FcgLevel level) {
return IterableUtils.unmodifiableIterable(verticesByLevel.get(level));
}
/**
* Returns the largest level (the furthest level from the source node) in the given
* direction
*
* @param direction the direction to search
* @return the largest level
*/
public FcgLevel getLargestLevel(FcgDirection direction) {
FcgLevel greatest = new FcgLevel(1, direction);
Set<FcgLevel> keys = verticesByLevel.keySet();
for (FcgLevel level : keys) {
if (level.getDirection() != direction) {
continue;
}
if (level.getRow() > greatest.getRow()) {
greatest = level;
}
}
return greatest;
}
@Override
public VisualGraphLayout<FcgVertex, FcgEdge> getLayout() {
return layout;
}
@Override
public FunctionCallGraph copy() {
FunctionCallGraph newGraph = new FunctionCallGraph();
for (FcgVertex v : vertices.keySet()) {
newGraph.addVertex(v);
}
for (FcgEdge e : edges.keySet()) {
newGraph.addEdge(e);
}
return newGraph;
}
public void setLayout(VisualGraphLayout<FcgVertex, FcgEdge> layout) {
this.layout = layout;
}
@Override
protected void verticesAdded(Collection<FcgVertex> added) {
for (FcgVertex v : added) {
Function f = v.getFunction();
verticesByFunction.put(f, v);
verticesByLevel.get(v.getLevel()).add(v);
}
super.verticesAdded(added);
}
@Override
protected void verticesRemoved(Collection<FcgVertex> removed) {
for (FcgVertex v : removed) {
Function f = v.getFunction();
verticesByFunction.remove(f);
verticesByLevel.get(v.getLevel()).remove(v);
}
super.verticesRemoved(removed);
}
}

View file

@ -0,0 +1,365 @@
/* ###
* 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 functioncalls.graph.job;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.*;
import com.google.common.base.Function;
import edu.uci.ics.jung.algorithms.layout.Layout;
import edu.uci.ics.jung.visualization.RenderContext;
import functioncalls.graph.*;
import functioncalls.graph.layout.BowTieLayout;
import ghidra.graph.job.AbstractGraphTransitionJob;
import ghidra.graph.viewer.GraphViewer;
import ghidra.graph.viewer.GraphViewerUtils;
import ghidra.util.Msg;
/**
* A graph job to layout a given set of graph vertices. Most of the work for this class is
* done in {@link #arrangeNewVertices()}.
*
* <P>This class is handed a group of edges to processes. In this group there are vertices that
* do not need to be arranged, referred to as the <tt>existing</tt> vertices. This
* classes uses {@link VertexCollection} to find and store the new vertices that need
* to be arranged.
*/
public class BowTieExpandVerticesJob extends AbstractGraphTransitionJob<FcgVertex, FcgEdge> {
private boolean incoming;
private FcgLevel expandingLevel;
private FcgExpandingVertexCollection newVertexCollection;
/**
* Constructor
*
* @param viewer the graph viewer
* @param newVertexCollection the collection of new vertices and edges being addeds
* @param useAnimation true to use animation
*/
public BowTieExpandVerticesJob(GraphViewer<FcgVertex, FcgEdge> viewer,
FcgExpandingVertexCollection newVertexCollection, boolean useAnimation) {
super(viewer, useAnimation);
this.newVertexCollection = newVertexCollection;
this.incoming = newVertexCollection.isIncoming();
this.expandingLevel = newVertexCollection.getExpandingLevel();
if (!(graphLayout instanceof BowTieLayout)) {
throw new IllegalArgumentException("The current graph layout must be the " +
BowTieLayout.class.getSimpleName() + " to use this job");
}
Msg.trace(this,
"\nBow Tie Expand Job - new vertices: " + newVertexCollection.getNewVertices());
// for debug
// duration = 5000;
}
@Override
protected boolean isTooBigToAnimate() {
return graph.getVertexCount() > 1000; // not sure about the best number here
}
@Override
protected void updateOpacity(double percentComplete) {
/*
Aesthetic Note: due to the colors used in the graph in conjunction with the
alpha used here, when using the same opacity for the vertices and the edges, the
edges can be seen through the vertices, which looks bad. To fix this, have the
edges be less visible until the vertices are more opaque.
*/
double x = percentComplete;
double x2 = x * x; // change slower than x
double remaining = 1 - percentComplete; // start opacity towards the end
double y = x2 - remaining;
//Msg.debug(this, String.format("%%: %.3f - x^2: %.3f - remaining: %.3f - edge alpha: %.3f",
// percentComplete, (x * x), remaining, y));
Set<FcgVertex> newVertices = newVertexCollection.getNewVertices();
double vertexAlpha = x;
double edgeAlpha = Math.max(y, 0);
for (FcgVertex v : newVertices) {
v.setAlpha(vertexAlpha);
}
Iterable<FcgEdge> newEdges = newVertexCollection.getNewEdges();
for (FcgEdge edge : newEdges) {
edge.setAlpha(edgeAlpha);
}
}
@Override
public boolean canShortcut() {
return true;
}
@Override
public void shortcut() {
isShortcut = true;
if (vertexLocations.isEmpty()) {
// have not yet initialized; do so now before the final locations are applied
initializeVertexLocations();
}
stop();
}
@Override
protected void initializeVertexLocations() {
Map<FcgVertex, TransitionPoints> destinationLocations = createDestinationLocation();
vertexLocations.putAll(destinationLocations);
}
private Map<FcgVertex, TransitionPoints> createDestinationLocation() {
// note: both collections of vertices are sorted
Map<FcgVertex, Point2D> finalDestinations = arrangeNewVertices();
Map<FcgVertex, TransitionPoints> transitions = new HashMap<>();
FcgLevel parentLevel = expandingLevel.parent();
Iterable<FcgEdge> newEdges = newVertexCollection.getNewEdges();
Set<FcgVertex> newVertices = newVertexCollection.getNewVertices();
for (FcgEdge e : newEdges) {
FcgVertex newVertex = incoming ? e.getStart() : e.getEnd();
if (!finalDestinations.containsKey(newVertex)) {
continue; // this implies the edges is between 2 existing vertices
}
if (!newVertices.contains(newVertex)) {
continue; // a new edge to an existing vertex
}
FcgVertex existingVertex = incoming ? e.getEnd() : e.getStart();
FcgLevel existingLevel = existingVertex.getLevel();
if (!existingLevel.equals(parentLevel)) {
// Only the parent level can be the source of the transition. This ensures
// that the animation starts at the level that was expanded and not at some
// other level in the graph that happened to have an edge to the new node.
continue;
}
Point2D start = (Point2D) toLocation(existingVertex).clone();
Point2D end = finalDestinations.get(newVertex);
TransitionPoints trans = new TransitionPoints(start, end);
transitions.put(newVertex, trans);
}
return transitions;
}
private Map<FcgVertex, Point2D> arrangeNewVertices() {
/*
Add the new row above (or below) the existing row that is being expanded. So,
the new graph will appear as so:
v3.1 v3.2 v3.4 v3.5 v3.6
v2.1 v2.2
v1.1
Where the '3.x' nodes are those being added. They will be centered in relation to
the existing row.
*/
BowTieLayout bowTie = (BowTieLayout) graphLayout;
boolean isCondensed = bowTie.isCondensedLayout();
int widthPadding = isCondensed ? GraphViewerUtils.EXTRA_LAYOUT_COLUMN_SPACING_CONDENSED
: GraphViewerUtils.EXTRA_LAYOUT_COLUMN_SPACING;
// More space the further away from center, for aesthetics/readability (as the graph
// gets larger, more edges are added, cluttering up the display).
widthPadding *= expandingLevel.getDistance();
int heightPadding = calculateHeightPadding(isCondensed);
FcgLevel parentLevel = expandingLevel.parent();
List<FcgVertex> parentLevelVertices = newVertexCollection.getVerticesByLevel(parentLevel);
if (parentLevelVertices.isEmpty()) {
// this can happen if all the new edges being added already exist in the graph
// at a different level than the parent
return Collections.emptyMap();
}
Rectangle existingRowBounds = getBounds(parentLevelVertices);
Msg.trace(this, "existing row bounds " + existingRowBounds);
double existingY = existingRowBounds.y;
double existingCenterX = existingRowBounds.x + (existingRowBounds.width / 2);
// Layout all vertices at the new level, even the hidden ones, so the new ones fit
// within the overall level. This allows future expansions to put the new nodes in
// the correct spot.
List<FcgVertex> allLevelVertices = newVertexCollection.getAllVerticesAtNewLevel();
double newRowWidth = getWidth(allLevelVertices, widthPadding);
double newRowHeight = getHeight(allLevelVertices);
double newRowX = existingCenterX - (newRowWidth / 2);
double newRowY = 0;
if (newVertexCollection.isIncoming()) {
newRowY = existingY - newRowHeight - heightPadding;
}
else {
newRowY = existingY + existingRowBounds.height + heightPadding;
}
Msg.trace(this, "new row bounds " +
new Rectangle2D.Double(newRowX, newRowY, newRowWidth, newRowHeight));
Map<FcgVertex, Point2D> locations = getExistingLocations(allLevelVertices);
if (!locations.isEmpty()) {
// use the existing locations so that the nodes appear where the user expects
return locations;
}
RenderContext<FcgVertex, FcgEdge> renderContext = viewer.getRenderContext();
Function<? super FcgVertex, Shape> shaper = renderContext.getVertexShapeTransformer();
double x = newRowX;
double y = newRowY;
int n = allLevelVertices.size();
for (int i = 0; i < n; i++) {
FcgVertex v = allLevelVertices.get(i);
Rectangle myBounds = shaper.apply(v).getBounds();
double myHalf = myBounds.width / 2;
double nextHalf = 0;
boolean isLast = i == n - 1;
if (!isLast) {
FcgVertex nextV = allLevelVertices.get(i + 1);
Rectangle nextBounds = shaper.apply(nextV).getBounds();
nextHalf = nextBounds.width / 2;
}
Point2D p = new Point2D.Double(x, y);
locations.put(v, p);
double vWidth = myHalf + widthPadding + nextHalf;
Msg.trace(this, v + " at x,width: " + x + "," + vWidth);
x += vWidth;
}
return locations;
}
private int calculateHeightPadding(boolean isCondensed) {
// give each successive level
int basePadding = isCondensed ? GraphViewerUtils.EXTRA_LAYOUT_ROW_SPACING_CONDENSED
: GraphViewerUtils.EXTRA_LAYOUT_ROW_SPACING;
double separationFactor = expandingLevel.getDistance();
/*
Let's scale the distance between 2 rows. As the level increases so too should the
distance, based upon how busyness (how many edges) of the new level being added.
-If the new level is not busy, then keep the standard distance;
-If the new level is busy, increase the y-distance, up to the max, based upon
the busyness
*/
List<FcgVertex> allLevelVertices = newVertexCollection.getAllVerticesAtNewLevel();
int count = allLevelVertices.size();
// grow each layer more than linear to add space for the edges
double to = 1.25; // 1.5;
double power = Math.pow(separationFactor, to);
int maxPadding = (int) (basePadding * power);
// range from 0-1 (%) based on edge count, with 20 being the high-side
int delta = maxPadding - basePadding;
double percent = Math.min(count / 20f, 1);
int padding = basePadding + (int) (delta * percent);
return padding;
}
private Map<FcgVertex, Point2D> getExistingLocations(List<FcgVertex> vertices) {
Map<FcgVertex, Point2D> locations = new HashMap<>();
for (FcgVertex v : vertices) {
Point2D p = toLocation(v);
if (p.getX() == 0 && p.getY() == 0) {
// no location for this vertex--we have to build them
return new HashMap<>();
}
locations.put(v, (Point2D) p.clone());
}
return locations;
}
private Rectangle getBounds(List<FcgVertex> vertices) {
RenderContext<FcgVertex, FcgEdge> renderContext = viewer.getRenderContext();
Function<? super FcgVertex, Shape> shaper = renderContext.getVertexShapeTransformer();
Layout<FcgVertex, FcgEdge> layout = viewer.getGraphLayout();
Rectangle area = null;
for (FcgVertex v : vertices) {
Rectangle bounds = shaper.apply(v).getBounds();
Point2D loc = layout.apply(v);
int x = (int) loc.getX();
int y = (int) loc.getY();
// do we need to compensate for vertex centering (like is done in the default layout)?
// x -= (bounds.width / 2);
// y -= (bounds.height / 2);
bounds.setLocation(x, y);
if (area == null) {
area = bounds; // initialize
}
area.add(bounds);
}
return area;
}
private int getWidth(List<FcgVertex> vertices, int widthPadding) {
RenderContext<FcgVertex, FcgEdge> renderContext = viewer.getRenderContext();
Function<? super FcgVertex, Shape> shaper = renderContext.getVertexShapeTransformer();
int width = 0;
for (FcgVertex v : vertices) {
width += shaper.apply(v).getBounds().width + widthPadding;
}
return width;
}
private int getHeight(List<FcgVertex> vertices) {
RenderContext<FcgVertex, FcgEdge> renderContext = viewer.getRenderContext();
Function<? super FcgVertex, Shape> shaper = renderContext.getVertexShapeTransformer();
int height = 0;
for (FcgVertex v : vertices) {
height = Math.max(height, shaper.apply(v).getBounds().height);
}
return height;
}
}

View file

@ -0,0 +1,53 @@
/* ###
* 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 functioncalls.graph.job;
import java.util.Set;
import functioncalls.graph.FcgEdge;
import functioncalls.graph.FcgVertex;
import ghidra.graph.job.AbstractGraphVisibilityTransitionJob;
import ghidra.graph.viewer.GraphViewer;
/**
* A job to emphasize a given set of edges. This will make them bigger and then restore them
* to a non-emphasized state.
*/
public class FcgEmphasizeEdgesJob extends AbstractGraphVisibilityTransitionJob<FcgVertex, FcgEdge> {
private Set<FcgEdge> edges;
public FcgEmphasizeEdgesJob(GraphViewer<FcgVertex, FcgEdge> viewer, Set<FcgEdge> edges) {
super(viewer, true);
this.edges = edges;
}
@Override
protected void updateOpacity(double percentComplete) {
double remaining = percentComplete;
if (percentComplete > .5) {
remaining = 1 - percentComplete;
}
double modified = remaining * 10;
//double remaining = 1 - percentComplete; // start opacity towards the end
for (FcgEdge e : edges) {
e.setEmphasis(modified);
}
}
}

View file

@ -0,0 +1,256 @@
/* ###
* 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 functioncalls.graph.job;
import java.awt.geom.Point2D;
import java.util.*;
import java.util.stream.Collectors;
import org.apache.commons.collections4.IterableUtils;
import org.apache.commons.collections4.map.LazyMap;
import edu.uci.ics.jung.algorithms.layout.Layout;
import functioncalls.graph.*;
import ghidra.graph.viewer.GraphViewer;
import util.CollectionUtils;
/**
* A container to house all newly added vertices (those being arranged) and the sources, or
* 'from' vertices, of the new vertices.
*
* <P>This offers exiting vertices and new vertices pre-sorted by position in the graph in
* order to minimize edge crossings. Specifically, the new vertices will be sorted
* by the level of the parent and then the x-value of the parent so that the
* immediate parent level will be preferred, with the x-value dictating where to place
* the child so that we can minimize edge crossings.
*/
public class FcgExpandingVertexCollection {
private Comparator<FcgVertex> sourceVertexComparator = this::compareVerticesByLayoutPosition;
private Comparator<FcgVertex> addressComparator =
(v1, v2) -> v1.getAddress().compareTo(v2.getAddress());
private Map<FcgVertex, TreeSet<FcgVertex>> newVerticesBySource = LazyMap.lazyMap(
new TreeMap<>(sourceVertexComparator), () -> new TreeSet<>(addressComparator));
private GraphViewer<FcgVertex, FcgEdge> viewer;
private FcgLevel parentLevel;
private FcgLevel expandingLevel;
private Set<FcgVertex> newVertices;
private Set<FcgEdge> newEdges;
private Set<FcgEdge> indirectEdges = Collections.emptySet();
private boolean isIncoming;
private Iterable<FcgVertex> sources;
/**
* Constructor
*
* @param sources all vertices that are the source of the expansion. This will be either a
* single vertex, clicked by the user, or all vertices of a source level being expanded
* @param parentLevel a.k.a, the source vertex level for the vertex that is the source of the
* new vertices
* @param expandingLevel the level of the new vertices
* @param newVertices the expanding vertices; those that are emanating from the parent
* @param newEdges the new edges being added as a result of adding the new vertices
* @param allParentLevelEdges all edges from all siblings of the parent vertex
* @param isIncoming true if the newly added vertices are callers of the source vertex;
* false if the newly added vertices are called by the source vertex
* @param viewer the viewer that is painting the graph. This is needed to sort by layout
* position
*/
public FcgExpandingVertexCollection(Iterable<FcgVertex> sources, FcgLevel parentLevel,
FcgLevel expandingLevel, Set<FcgVertex> newVertices, Set<FcgEdge> newEdges,
Set<FcgEdge> allParentLevelEdges, boolean isIncoming,
GraphViewer<FcgVertex, FcgEdge> viewer) {
this.sources = sources;
this.parentLevel = parentLevel;
this.newVertices = newVertices;
this.newEdges = newEdges;
this.isIncoming = isIncoming;
this.viewer = viewer;
this.expandingLevel = expandingLevel;
// we need to use the parent edges to generate the siblings of the newly
// added vertices, as well as to sort the new vertices amongst their siblings
for (FcgEdge e : allParentLevelEdges) {
FcgVertex start = e.getStart();
FcgVertex end = e.getEnd();
FcgLevel startLevel = start.getLevel();
FcgLevel endLevel = end.getLevel();
if (expandingLevel.equals(startLevel)) {
if (expandingLevel.equals(endLevel)) {
// self-loop
newVerticesBySource.get(start).add(end);
newVerticesBySource.get(end).add(start);
}
else {
newVerticesBySource.get(end).add(start);
}
}
else {
newVerticesBySource.get(start).add(end);
}
}
}
private int compareVerticesByLayoutPosition(FcgVertex v1, FcgVertex v2) {
Layout<FcgVertex, FcgEdge> layout = viewer.getGraphLayout();
FcgLevel l1 = v1.getLevel();
FcgLevel l2 = v2.getLevel();
int result = l1.compareTo(l2);
if (result != 0) {
// prefer the parent level over all
if (l1.equals(parentLevel)) {
return -1;
}
else if (l2.equals(parentLevel)) {
return 1;
}
return result;
}
Point2D p1 = layout.apply(v1);
Point2D p2 = layout.apply(v2);
return (int) (p1.getX() - p2.getX());
}
/**
* Returns all vertices at the given level
*
* @param level the level to filter on
* @return the vertices
*/
public List<FcgVertex> getVerticesByLevel(FcgLevel level) {
// note: these are sorted, since they are housed in a TreeMap
Set<FcgVertex> existingVertices = newVerticesBySource.keySet();
//@formatter:off
List<FcgVertex> verticesAtLevel = existingVertices
.stream()
.filter(v -> v.getLevel().equals(level))
.collect(Collectors.toList())
;
//@formatter:on
return verticesAtLevel;
}
/**
* Returns all vertices that have just been added to the graph; those now being arranged
*
* @return all vertices that have just been added to the graph; those now being arranged
*/
public List<FcgVertex> getAllVerticesAtNewLevel() {
// note: these are sorted, since they are housed in a TreeMap
Set<FcgVertex> existingVertices = newVerticesBySource.keySet();
//@formatter:off
LinkedHashSet<FcgVertex> sortedVertices = existingVertices
.stream()
.map(v -> newVerticesBySource.get(v))
.flatMap(set -> set.stream())
.filter(v -> v.getLevel().equals(expandingLevel)) // only include vertices not in graph
.collect(Collectors.toCollection(LinkedHashSet::new)) // unique, sorted by traversal from above
;
//@formatter:on
return new ArrayList<>(sortedVertices);
}
/**
* Returns all vertices being added to the graph
* @return the vertices
*/
public Set<FcgVertex> getNewVertices() {
return newVertices;
}
/**
* Returns all new edges being added to the graph
* @return the edges
*/
public Iterable<FcgEdge> getNewEdges() {
return IterableUtils.chainedIterable(newEdges, indirectEdges);
}
/**
* Returns all edges that are being added to existing nodes
* @return the edges
*/
public Set<FcgEdge> getIndirectEdges() {
return indirectEdges;
}
/**
* Returns the number of newly added edges
* @return the number of newly added edges
*/
public int getNewEdgeCount() {
return newEdges.size() + indirectEdges.size();
}
/**
* Sets indirect edges--those edges that are not a direct link between the source
* vertices and the newly added vertices
* @param indirectEdges the edges
*/
public void setIndirectEdges(Set<FcgEdge> indirectEdges) {
this.indirectEdges = CollectionUtils.asSet(indirectEdges);
}
/**
* Returns the level of the newly added vertices, which is the level that is being expanded
* @return the level
*/
public FcgLevel getExpandingLevel() {
return expandingLevel;
}
/**
* Returns the direction of the expansion
* @return the direction of the expansion
*/
public FcgDirection getExpandingDirection() {
return expandingLevel.getDirection();
}
/**
* All vertices that are the source of the expansion
* @return the source vertices
*/
public Iterable<FcgVertex> getSources() {
return sources;
}
/**
* Returns true if the newly added vertices are callers of the source vertex; false if
* the newly added vertices are called by the source vertex
*
* @return true if the new vertices are incoming calls
*/
public boolean isIncoming() {
return isIncoming;
}
}

View file

@ -0,0 +1,160 @@
/* ###
* 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 functioncalls.graph.layout;
import java.awt.Rectangle;
import java.awt.geom.Point2D;
import java.util.*;
import java.util.stream.Collectors;
import functioncalls.graph.*;
import ghidra.graph.VisualGraph;
import ghidra.graph.viewer.layout.*;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
/**
* A layout that will arrange vertices around a single vertex, with the incoming and outgoing
* vertices above and below the source vertex, respectively.
*
* <p>The result will look loosely like this:
* <pre>
* In1 In2 In3
* \ | /
* \ | /
* V
* / | \
* / | \
* Out1 Out2 Out3
*
* </pre>
*/
public class BowTieLayout extends AbstractVisualGraphLayout<FcgVertex, FcgEdge> {
protected BowTieLayout(FunctionCallGraph graph) {
super(graph);
}
@Override
public AbstractVisualGraphLayout<FcgVertex, FcgEdge> createClonedLayout(
VisualGraph<FcgVertex, FcgEdge> newGraph) {
if (!(newGraph instanceof FunctionCallGraph)) {
throw new IllegalArgumentException(
"Must pass a " + FunctionCallGraph.class.getSimpleName() + "to clone the " +
getClass().getSimpleName());
}
BowTieLayout newLayout = new BowTieLayout((FunctionCallGraph) newGraph);
return newLayout;
}
@Override
protected Point2D getVertexLocation(FcgVertex v, Column col, Row<FcgVertex> row,
Rectangle bounds) {
return getCenteredVertexLocation(v, col, row, bounds);
}
@Override
public boolean isCondensedLayout() {
// TODO revisit; condensing looks odd with only one column, as it is not centered;
// try some real data and maybe set condensed based upon how many columns are
// present? When not condensed, the width of each cell will be that of the largest
// column, which means a really long name could cause normally-sized names to
// consume way more space than is needed
// return false;
return true; // not sure about this
}
@Override
public FunctionCallGraph getVisualGraph() {
return (FunctionCallGraph) getGraph();
}
@Override
protected GridLocationMap<FcgVertex, FcgEdge> performInitialGridLayout(
VisualGraph<FcgVertex, FcgEdge> g) throws CancelledException {
if (!(g instanceof FunctionCallGraph)) {
throw new IllegalArgumentException(
"This layout can only be used with the " + FunctionCallGraph.class);
}
return layoutFunctionCallGraph((FunctionCallGraph) g);
}
@Override
public LayoutPositions<FcgVertex, FcgEdge> calculateLocations(VisualGraph<FcgVertex, FcgEdge> g,
TaskMonitor taskMonitor) {
LayoutPositions<FcgVertex, FcgEdge> locs = super.calculateLocations(g, taskMonitor);
// TODO put x offset manipulation here...
// -if the number of vertices in each row is not the same odd/even as the
// largest row, then slide the x values for each vertex in the row left or
// right as needed
return locs;
}
private GridLocationMap<FcgVertex, FcgEdge> layoutFunctionCallGraph(FunctionCallGraph g) {
GridLocationMap<FcgVertex, FcgEdge> grid = new GridLocationMap<>();
FcgVertex source = Objects.requireNonNull(g.getSource());
//
// Incoming nodes on top
// -sorted by address
//
List<FcgEdge> inEdges = new ArrayList<>(g.getInEdges(source));
List<FcgVertex> inVertices =
inEdges.stream().map(e -> e.getStart()).collect(Collectors.toList());
inVertices.sort((v1, v2) -> v1.getAddress().compareTo(v2.getAddress()));
int row = 0; // first row
for (int col = 0; col < inVertices.size(); col++) {
FcgVertex v = inVertices.get(col);
grid.set(v, row, col);
}
//
// Source node
//
row = 1; // middle row
grid.set(source, row, 0);
// Outgoing nodes on the bottom
// -sorted by address
//
List<FcgEdge> outEdges = new ArrayList<>(g.getOutEdges(source));
List<FcgVertex> outVertices =
outEdges.stream().map(e -> e.getEnd()).collect(Collectors.toList());
// leave already processed vertices in the top row; this can happen if the in vertex is
// also called by the source function, creating a cycle
outVertices.removeAll(inVertices);
outVertices.sort((v1, v2) -> v1.getAddress().compareTo(v2.getAddress()));
row = 2; // last
for (int col = 0; col < outVertices.size(); col++) {
FcgVertex v = outVertices.get(col);
grid.set(v, row, col);
}
grid.centerRows();
return grid;
}
}

View file

@ -0,0 +1,55 @@
/* ###
* 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 functioncalls.graph.layout;
import javax.swing.Icon;
import functioncalls.graph.*;
import ghidra.graph.viewer.layout.AbstractLayoutProvider;
import ghidra.graph.viewer.layout.VisualGraphLayout;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
import resources.ResourceManager;
/**
* A layout provider for the {@link BowTieLayout}
*/
public class BowTieLayoutProvider
extends AbstractLayoutProvider<FcgVertex, FcgEdge, FunctionCallGraph> {
public static final String NAME = "Bow Tie Layout";
private static final Icon DEFAULT_ICON = ResourceManager.loadImage("images/color_swatch.png");
@Override
public VisualGraphLayout<FcgVertex, FcgEdge> getLayout(FunctionCallGraph graph,
TaskMonitor monitor) throws CancelledException {
BowTieLayout layout = new BowTieLayout(graph);
initVertexLocations(graph, layout);
return layout;
}
@Override
public String getLayoutName() {
return NAME;
}
@Override
public Icon getActionIcon() {
return DEFAULT_ICON;
}
}

View file

@ -0,0 +1,78 @@
/* ###
* 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 functioncalls.graph.renderer;
import java.awt.Color;
import java.awt.Paint;
import com.google.common.base.Function;
import functioncalls.graph.FcgEdge;
/**
* Generates colors for a given {@link FcgEdge}
*/
public class FcgEdgePaintTransformer implements Function<FcgEdge, Paint> {
// private static final Paint LESS_IMPORTANT_COLOR = new Color(125, 125, 125, 75);
private Color directColor;
private Color indirectColor;
private Color[] directColorWithAlpha = new Color[10];
// only one color for now; may have more; these should be changeable via graph options
public FcgEdgePaintTransformer(Color directColor, Color indirectColor) {
this.directColor = directColor;
this.indirectColor = indirectColor;
directColorWithAlpha = alphatize(directColor);
}
private Color[] alphatize(Color c) {
Color[] alphad = new Color[10];
alphad[0] = c;
for (int i = 1; i < 10; i++) {
double newAlpha = 255 - (i * 25.5);
alphad[i] = new Color(c.getRed(), c.getGreen(), c.getBlue(), (int) newAlpha);
}
return alphad;
}
@Override
public Paint apply(FcgEdge e) {
if (e.isDirectEdge()) {
return getDirectEdgeColor(e);
}
return indirectColor;
}
private Color getDirectEdgeColor(FcgEdge e) {
return directColor;
/*// this allows us to make the edges fainter as the outward levels increase
FcgVertex start = e.getStart();
FcgVertex end = e.getEnd();
FcgLevel sl = start.getLevel();
FcgLevel el = end.getLevel();
int level = Math.min(sl.getDistance(), el.getDistance());
level = Math.min(level, 9);
Color c = directColorWithAlpha[level];
return c;
*/
}
}

View file

@ -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 functioncalls.graph.renderer;
import java.awt.Component;
import java.awt.event.MouseEvent;
import javax.swing.*;
import functioncalls.graph.FcgEdge;
import functioncalls.graph.FcgVertex;
import ghidra.graph.viewer.event.mouse.VertexTooltipProvider;
/**
* A class that provides tooltips for a given vertex
*/
public class FcgTooltipProvider implements VertexTooltipProvider<FcgVertex, FcgEdge> {
@Override
public JComponent getTooltip(FcgVertex v) {
JToolTip tip = new JToolTip();
tip.setTipText(v.getName());
return tip;
}
@Override
public JComponent getTooltip(FcgVertex v, FcgEdge e) {
return null;
}
@Override
public String getTooltipText(FcgVertex v, MouseEvent e) {
// TODO we could have the name label return just the function name; the vertex shape
// return a full function signature and the +/- toggle buttons return a tip
Component child = e.getComponent();
// the buttons may have extra information
if (child instanceof JButton) {
return ((JButton) child).getToolTipText();
}
return v.getName();
}
}

View file

@ -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 functioncalls.graph.renderer;
import java.awt.Color;
import java.awt.Paint;
import com.google.common.base.Function;
import functioncalls.graph.FcgVertex;
import functioncalls.graph.FunctionCallGraph;
/**
* A class that takes a {@link FunctionCallGraph} vertex and determines which fill color
* should be used to paint
*/
public class FcgVertexPaintTransformer implements Function<FcgVertex, Paint> {
private Color color;
// only one color for now; may have more; these should be changeable via graph options
public FcgVertexPaintTransformer(Color color) {
this.color = color;
}
@Override
public Paint apply(FcgVertex v) {
return color;
}
}

View file

@ -0,0 +1,105 @@
/* ###
* 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 functioncalls.graph.view;
import java.awt.Color;
import edu.uci.ics.jung.visualization.RenderContext;
import functioncalls.graph.*;
import functioncalls.graph.renderer.FcgEdgePaintTransformer;
import functioncalls.graph.renderer.FcgVertexPaintTransformer;
import functioncalls.plugin.FunctionCallGraphPlugin;
import ghidra.graph.viewer.*;
import ghidra.graph.viewer.layout.VisualGraphLayout;
/**
* A graph component for the {@link FunctionCallGraphPlugin}
*/
public class FcgComponent extends GraphComponent<FcgVertex, FcgEdge, FunctionCallGraph> {
// our parent stores a reference to this graph, but we do it here to maintain its type
private FunctionCallGraph fcGraph;
// TODO use options for color
private FcgVertexPaintTransformer vertexPaintTransformer =
new FcgVertexPaintTransformer(FcgVertex.DEFAULT_VERTEX_SHAPE_COLOR);
private Color lightGreen = new Color(143, 197, 143);
private Color lightGray = new Color(233, 233, 233);
// the satellite gets too cluttered, so wash out the edges
private Color washedOutBlack = new Color(0, 0, 0, 25);
private FcgEdgePaintTransformer edgePaintTransformer =
new FcgEdgePaintTransformer(lightGreen, lightGray);
private FcgEdgePaintTransformer satelliteEdgePaintTransformer =
new FcgEdgePaintTransformer(washedOutBlack, new Color(125, 125, 125, 25));
FcgComponent(FunctionCallGraph g) {
setGraph(g);
build();
}
@Override
protected FcgVertex getInitialVertex() {
return fcGraph.getSource();
}
@Override
protected void decoratePrimaryViewer(GraphViewer<FcgVertex, FcgEdge> viewer,
VisualGraphLayout<FcgVertex, FcgEdge> layout) {
super.decoratePrimaryViewer(viewer, layout);
RenderContext<FcgVertex, FcgEdge> renderContext = viewer.getRenderContext();
renderContext.setVertexFillPaintTransformer(vertexPaintTransformer);
// Note: setting the fill for the edges has the effect of drawing a filled-in circle
// instead of just the outer edge.
// renderContext.setEdgeFillPaintTransformer(edgePaintTransformer);
renderContext.setEdgeDrawPaintTransformer(edgePaintTransformer);
renderContext.setArrowFillPaintTransformer(edgePaintTransformer);
renderContext.setArrowDrawPaintTransformer(edgePaintTransformer);
}
@Override
protected void decorateSatelliteViewer(SatelliteGraphViewer<FcgVertex, FcgEdge> viewer,
VisualGraphLayout<FcgVertex, FcgEdge> layout) {
super.decorateSatelliteViewer(viewer, layout);
RenderContext<FcgVertex, FcgEdge> renderContext = viewer.getRenderContext();
renderContext.setVertexFillPaintTransformer(vertexPaintTransformer);
//renderContext.setEdgeFillPaintTransformer(satelliteEdgePaintTransformer);
renderContext.setEdgeDrawPaintTransformer(satelliteEdgePaintTransformer);
renderContext.setArrowFillPaintTransformer(satelliteEdgePaintTransformer);
renderContext.setArrowDrawPaintTransformer(satelliteEdgePaintTransformer);
}
@Override
public void dispose() {
fcGraph = null;
super.dispose();
}
@Override // open access for testing
public VisualGraphViewUpdater<FcgVertex, FcgEdge> getViewUpdater() {
return super.getViewUpdater();
}
}

View file

@ -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 functioncalls.graph.view;
import functioncalls.graph.*;
import functioncalls.plugin.FunctionCallGraphPlugin;
import ghidra.graph.viewer.VisualGraphView;
import ghidra.graph.viewer.options.VisualGraphOptions;
/**
* A graph view for the {@link FunctionCallGraphPlugin}
*/
public class FcgView extends VisualGraphView<FcgVertex, FcgEdge, FunctionCallGraph> {
@Override
protected void installGraphViewer() {
FcgComponent component = createGraphComponent();
component.setGraphOptions(new VisualGraphOptions());
setGraphComponent(component);
}
private FcgComponent createGraphComponent() {
FcgComponent component = new FcgComponent(getVisualGraph());
return component;
}
@Override
public FcgComponent getGraphComponent() {
return (FcgComponent) super.getGraphComponent();
}
}

View file

@ -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 functioncalls.plugin;
import functioncalls.graph.*;
import ghidra.graph.viewer.GraphPerspectiveInfo;
import ghidra.program.model.listing.Function;
/**
* An empty data that is used to avoid null checks
*/
public class EmptyFcgData implements FcgData {
@Override
public Function getFunction() {
throw new UnsupportedOperationException("Empty data has no function");
}
@Override
public boolean isFunction(Function f) {
return false;
}
@Override
public FunctionCallGraph getGraph() {
throw new UnsupportedOperationException("Empty data has no graph");
}
@Override
public FunctionEdgeCache getFunctionEdgeCache() {
throw new UnsupportedOperationException("Empty data has no function edge cache");
}
@Override
public boolean hasResults() {
return false;
}
@Override
public void dispose() {
// we are empty; nothing to do
}
@Override
public boolean isInitialized() {
return false;
}
@Override
public GraphPerspectiveInfo<FcgVertex, FcgEdge> getGraphPerspective() {
throw new UnsupportedOperationException("Empty data does not need view information");
}
@Override
public void setGraphPerspective(GraphPerspectiveInfo<FcgVertex, FcgEdge> info) {
throw new UnsupportedOperationException("Empty data does not need view information");
}
}

View file

@ -0,0 +1,89 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package functioncalls.plugin;
import functioncalls.graph.*;
import ghidra.graph.viewer.GraphPerspectiveInfo;
import ghidra.program.model.listing.Function;
/**
* This class allows clients to retrieve and work on the graph and its related data. Also,
* this class makes caching the data herein simple.
*/
interface FcgData {
/**
* The function of this data
* @return the function
*/
Function getFunction();
/**
* The graph of this data
*
* @return the graph
*/
FunctionCallGraph getGraph();
/**
* Returns the cache of {@link Function} edges. These edges are not in the graph, but
* rather are simple edges that represent a link between two functions. This is used to
* track existing edges that are not yet in the graph, which may be added later as the
* relevant nodes are inserted into the graph.
*
* @return the cache
*/
FunctionEdgeCache getFunctionEdgeCache();
/**
* True if this data has a valid function
* @return true if this data has a valid function
*/
boolean hasResults();
/**
* False if the graph in this data has not yet been loaded
* @return false if the graph in this data has not yet been loaded
*/
boolean isInitialized();
/**
* Dispose the contents of this data
*/
void dispose();
/**
* Returns the view's graph perspective. This is used by the view to restore itself.
* @return the view's graph perspective
*/
GraphPerspectiveInfo<FcgVertex, FcgEdge> getGraphPerspective();
/**
* Sets the view information for this graph data. This will be later used by the view
* to restore itself.
*
* @param info the perspective
*/
void setGraphPerspective(GraphPerspectiveInfo<FcgVertex, FcgEdge> info);
/**
* Returns true if this data's function is equal to the given function
*
* @param f the function to test
* @return true if this data's function is equal to the given function
*/
boolean isFunction(Function f);
}

View file

@ -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 functioncalls.plugin;
import com.google.common.cache.*;
import functioncalls.graph.FunctionCallGraph;
import ghidra.program.model.listing.Function;
/**
* A factory that will create {@link FunctionCallGraph} data objects for a given function.
* Internally, this factory uses an MRU cache.
*/
public class FcgDataFactory {
private LoadingCache<Function, FcgData> cache;
FcgDataFactory(RemovalListener<Function, FcgData> listener) {
//@formatter:off
cache = CacheBuilder
.newBuilder()
.maximumSize(5)
.removalListener(listener)
// Note: using soft values means that sometimes our data is reclaimed by the
// Garbage Collector. We don't want that, we wish to call dispose() on the data
//.softValues()
.build(new CacheLoader<Function, FcgData>() {
@Override
public FcgData load(Function f) throws Exception {
return new ValidFcgData(f, new FunctionCallGraph());
}
});
//@formatter:on
}
FcgData create(Function f) {
if (f == null) {
return new EmptyFcgData();
}
FcgData data = cache.getUnchecked(f);
return data;
}
void remove(Function f) {
cache.invalidate(f);
}
void dispose() {
cache.invalidateAll();
}
}

View file

@ -0,0 +1,145 @@
/* ###
* 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 functioncalls.plugin;
import docking.ActionContext;
import docking.action.DockingAction;
import ghidra.app.CorePluginPackage;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.plugin.ProgramPlugin;
import ghidra.app.services.GoToService;
import ghidra.framework.options.SaveState;
import ghidra.framework.plugintool.PluginInfo;
import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.program.model.address.Address;
import ghidra.program.util.ProgramLocation;
import ghidra.util.HelpLocation;
import ghidra.util.SystemUtilities;
import ghidra.util.task.SwingUpdateManager;
/**
* Plugin to show a graph of function calls for a given function
*/
//@formatter:off
@PluginInfo(
status = PluginStatus.RELEASED,
packageName = CorePluginPackage.NAME,
category = PluginCategoryNames.GRAPH,
shortDescription = "Function Call Graph Plugin",
description = "Displays a graph of incoming and outgoing calls for a given function."
)
//@formatter:on
public class FunctionCallGraphPlugin extends ProgramPlugin {
/*package*/ static final String NAME = "Function Call Graph";
/*package*/ static final String SHOW_PROVIDER_ACTION_NAME = "Display Function Call Graph";
/*package*/ static final HelpLocation DEFAULT_HELP =
new HelpLocation(FunctionCallGraphPlugin.class.getSimpleName(),
FunctionCallGraphPlugin.class.getSimpleName());
private FcgProvider provider;
// enough time for users to click around without the graph starting its work
private static final int MIN_UPDATE_DELAY = 750;
private SwingUpdateManager locationUpdater = new SwingUpdateManager(MIN_UPDATE_DELAY, () -> {
doLocationChanged();
});
public FunctionCallGraphPlugin(PluginTool tool) {
super(tool, true, false);
}
@Override
protected void init() {
provider = new FcgProvider(tool, this);
createActions();
}
@Override
public void writeConfigState(SaveState state) {
provider.writeConfigState(state);
}
@Override
public void readConfigState(SaveState state) {
provider.readConfigState(state);
}
@Override
protected void locationChanged(ProgramLocation loc) {
locationUpdater.update();
}
private void doLocationChanged() {
provider.locationChanged(getCurrentLocation());
}
void handleProviderLocationChanged(ProgramLocation location) {
// For snapshots
// if (provider != connectedProvider) {
// return;
// }
GoToService goTo = tool.getService(GoToService.class);
if (goTo == null) {
return;
}
// do later so the current event processing can finish
SystemUtilities.runSwingLater(() -> {
goTo.goTo(location);
});
}
@Override
protected void dispose() {
provider.dispose();
}
private void createActions() {
DockingAction showProviderAction = new DockingAction(SHOW_PROVIDER_ACTION_NAME, getName()) {
@Override
public void actionPerformed(ActionContext context) {
provider.setVisible(true);
}
};
// TODO create icon from scratch: bow-tie
// ImageIcon icon = ResourceManager.loadImage("images/applications-development.png");
// showProviderAction.setToolBarData(new ToolBarData(icon, "View"));
tool.addAction(showProviderAction);
}
void showProvider() {
provider.setVisible(true);
}
FcgProvider getProvider() {
return provider;
}
Address getCurrentAddress() {
if (currentLocation == null) {
return null;
}
return currentLocation.getAddress();
}
ProgramLocation getCurrentLocation() {
return currentLocation;
}
}

View file

@ -0,0 +1,81 @@
/* ###
* 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 functioncalls.plugin;
import java.util.Objects;
import ghidra.program.model.listing.Function;
/**
* An edge between two functions. This edge is never added to the graph, but exists for us
* to maintain relationships between functions outside of the visual graph.
*/
class FunctionEdge {
private Function start;
private Function end;
FunctionEdge(Function start, Function end) {
this.start = start;
this.end = end;
}
Function getStart() {
return start;
}
Function getEnd() {
return end;
}
@Override
public String toString() {
return "[" + start + ", " + end + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((end == null) ? 0 : end.hashCode());
result = prime * result + ((start == null) ? 0 : start.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
FunctionEdge other = (FunctionEdge) obj;
if (!Objects.equals(end, other.end)) {
return false;
}
if (!Objects.equals(start, other.start)) {
return false;
}
return true;
}
}

View file

@ -0,0 +1,50 @@
/* ###
* 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 functioncalls.plugin;
import java.util.*;
import org.apache.commons.collections4.map.LazyMap;
import ghidra.program.model.listing.Function;
/**
* A class to cache known function edges
*/
public class FunctionEdgeCache {
/** Contains all known edges, even those not showing in the graph */
private Map<Function, Set<FunctionEdge>> allEdgesByFunction =
LazyMap.lazyMap(new HashMap<>(), () -> new HashSet<>());
// note: having a function as a key in the above map is not enough to know if it has been
// processed already (as the function can be added by processing edges of other
// nodes). Being in this structure means that it has been processed for its
// incoming and outgoing connections
private Set<Function> tracked = new HashSet<>();
public Set<FunctionEdge> get(Function f) {
return allEdgesByFunction.get(f);
}
public boolean isTracked(Function f) {
return tracked.contains(f);
}
public void setTracked(Function f) {
tracked.add(f);
}
}

View file

@ -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 functioncalls.plugin;
import java.util.Objects;
import functioncalls.graph.*;
import ghidra.graph.viewer.GraphPerspectiveInfo;
import ghidra.program.model.listing.Function;
/**
* A simple class that is used to cache graph data for a given function
*/
public class ValidFcgData implements FcgData {
private Function function;
private FunctionCallGraph graph;
private GraphPerspectiveInfo<FcgVertex, FcgEdge> perspectiveInfo;
/** Contains all known edges, even those not showing in the graph */
private FunctionEdgeCache allEdgesByFunction = new FunctionEdgeCache();
ValidFcgData(Function function, FunctionCallGraph graph) {
this.function = Objects.requireNonNull(function);
this.graph = Objects.requireNonNull(graph);
}
@Override
public Function getFunction() {
return function;
}
@Override
public boolean isFunction(Function f) {
return function.equals(f);
}
@Override
public FunctionCallGraph getGraph() {
return graph;
}
@Override
public FunctionEdgeCache getFunctionEdgeCache() {
return allEdgesByFunction;
}
@Override
public boolean hasResults() {
return true; // this object is always considered valid; use EmptyFcgData for bad Functions
}
@Override
public boolean isInitialized() {
return !graph.isEmpty();
}
@Override
public void dispose() {
graph.dispose();
}
@Override
public GraphPerspectiveInfo<FcgVertex, FcgEdge> getGraphPerspective() {
return perspectiveInfo;
}
@Override
public void setGraphPerspective(GraphPerspectiveInfo<FcgVertex, FcgEdge> info) {
this.perspectiveInfo = info;
}
}

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.
*/
package functioncalls.plugin.context;
import docking.ActionContext;
import functioncalls.graph.FunctionCallGraph;
import functioncalls.plugin.FcgProvider;
/**
* Context for the {@link FunctionCallGraph}
*/
public class FcgActionContext extends ActionContext {
public FcgActionContext(FcgProvider provider) {
this(provider, null);
}
public FcgActionContext(FcgProvider provider, Object contextObject) {
super(provider, contextObject);
}
}

View file

@ -0,0 +1,38 @@
/* ###
* 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 functioncalls.plugin.context;
import java.util.Objects;
import functioncalls.graph.FcgVertex;
import functioncalls.plugin.FcgProvider;
/**
* Context that contains a single vertex
*/
public class FcgVertexActionContext extends FcgActionContext {
private FcgVertex vertex;
public FcgVertexActionContext(FcgProvider provider, FcgVertex vertex) {
super(provider);
this.vertex = Objects.requireNonNull(vertex);
}
public FcgVertex getVertex() {
return vertex;
}
}

View file

@ -0,0 +1,191 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package help.screenshot;
import java.awt.*;
import javax.swing.SwingUtilities;
import org.junit.*;
import docking.DockableComponent;
import edu.uci.ics.jung.visualization.picking.PickedState;
import functioncalls.graph.*;
import functioncalls.graph.view.FcgView;
import functioncalls.plugin.*;
import generic.test.TestUtils;
import ghidra.graph.viewer.*;
import ghidra.program.model.address.TestAddress;
import ghidra.program.model.listing.Function;
public class FunctionCallGraphPluginScreenShots extends GhidraScreenShotGenerator {
private FcgProvider provider;
@Override
@Before
public void setUp() throws Exception {
super.setUp();
FunctionCallGraphPlugin plugin = env.addPlugin(FunctionCallGraphPlugin.class);
provider = new FcgProvider(tool, plugin);
provider.setVisible(true);
setTestFunctionInProvider();
}
@Override
public void dockingSetUp() {
// We get an error dialog about default tools, since this test is not in the
// Integration Test project. Disable the error dialogs.
setErrorGUIEnabled(false);
}
@Override
@After
public void tearDown() throws Exception {
tool.showComponentProvider(provider, false);
super.tearDown();
}
@Test
public void testFunctionCallGraphProvider() {
Window w = moveProviderToItsOwnWindow(provider, 700, 500);
centerGraph();
captureWindow(w);
}
@Test
public void testTooManyReferences() {
FcgFunction source = new FcgFunction("FUN_1234", new TestAddress(1));
createManyOutgoingReferences(source, FcgProvider.MAX_REFERENCES + 1);
setFunction(source);
FcgVertex v = graph().getVertex(source);
clearSelection();
runSwing(() -> v.setHovered(true));
captureProvider(provider);
captureNode(v);
}
private void clearSelection() {
GraphViewer<FcgVertex, FcgEdge> viewer = viewer();
PickedState<FcgVertex> picker = viewer.getPickedVertexState();
runSwing(() -> {
picker.clear();
});
}
private GraphViewer<FcgVertex, FcgEdge> viewer() {
FcgView view = provider.getView();
return view.getPrimaryGraphViewer();
}
private void captureNode(FcgVertex v) {
FcgView view = provider.getView();
GraphViewer<FcgVertex, FcgEdge> viewer = view.getPrimaryGraphViewer();
Rectangle bounds = GraphViewerUtils.getVertexBoundsInViewSpace(viewer, v);
DockableComponent dockableComponent = getDockableComponent(viewer);
Point loc = SwingUtilities.convertPoint(viewer, bounds.getLocation(), dockableComponent);
bounds.setLocation(loc);
Rectangle area = new Rectangle(bounds);
int offset = 10;
area.x -= offset;
area.y -= offset;
area.width += (2 * offset);
area.height += (2 * offset);
// drawRectangle(Color.ORANGE, area, 5);
crop(area);
}
private FunctionCallGraph graph() {
return (FunctionCallGraph) invokeInstanceMethod("getGraph", provider);
}
private void createManyOutgoingReferences(FcgFunction f, int n) {
int counter = 10;
for (int i = 0; i < n; i++) {
FcgFunction newF =
new FcgFunction("Many_Outgoing_Function_" + (i + 1), new TestAddress(counter++));
f.addCalledFunction(newF);
}
}
private void setTestFunctionInProvider() {
int counter = 0;
FcgFunction f = new FcgFunction("Source_Function", new TestAddress(counter++));
FcgFunction in = new FcgFunction("FUN_IN_1", new TestAddress(counter++));
in.addCalledFunction(f);
f.addCallerFunction(in);
in = new FcgFunction("FUN_IN_2", new TestAddress(counter++));
in.addCalledFunction(f);
f.addCallerFunction(in);
for (int i = 1; i < 5; i++) {
String name = "FUN_OUT_" + i;
FcgFunction out = new FcgFunction(name, new TestAddress(counter++));
out.addCallerFunction(f);
f.addCalledFunction(out);
}
setFunction(f);
}
private void setFunction(FcgFunction f) {
runSwing(() -> {
TestUtils.invokeInstanceMethod("setFunction", provider, Function.class, f);
});
}
private void centerGraph() {
waitForGraph();
VisualGraphViewUpdater<FcgVertex, FcgEdge> updater = getGraphUpdater();
updater.fitGraphToViewerNow();
waitForGraph();
}
private VisualGraphViewUpdater<FcgVertex, FcgEdge> getGraphUpdater() {
@SuppressWarnings("unchecked")
VisualGraphView<FcgVertex, FcgEdge, FunctionCallGraph> view =
(VisualGraphView<FcgVertex, FcgEdge, FunctionCallGraph>) getInstanceField("view",
provider);
GraphViewer<FcgVertex, FcgEdge> primaryViewer = view.getPrimaryGraphViewer();
VisualGraphViewUpdater<FcgVertex, FcgEdge> updater = primaryViewer.getViewUpdater();
return updater;
}
private void waitForGraph() {
VisualGraphViewUpdater<FcgVertex, FcgEdge> updater = getGraphUpdater();
waitForCondition(() -> !updater.isBusy());
waitForSwing();
}
}

View file

@ -0,0 +1,66 @@
/* ###
* 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 functioncalls.plugin;
import java.util.*;
import ghidra.program.model.FunctionTestDouble;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Function;
import ghidra.util.task.TaskMonitor;
/**
* A fake function double for use in testing the {@link FunctionCallGraphPlugin}
*/
public class FcgFunction extends FunctionTestDouble {
private Set<Function> calledFunctions = new HashSet<>();
private Set<Function> callingFunctions = new HashSet<>();
private Address entry;
public FcgFunction(String name, Address entry) {
super(name);
this.entry = entry;
}
public void addCalledFunction(Function f) {
calledFunctions.add(f);
}
public void addCallerFunction(Function f) {
callingFunctions.add(f);
}
@Override
public Address getEntryPoint() {
return entry;
}
@Override
public Set<Function> getCalledFunctions(TaskMonitor monitor) {
return Collections.unmodifiableSet(calledFunctions);
}
@Override
public Set<Function> getCallingFunctions(TaskMonitor monitor) {
return Collections.unmodifiableSet(callingFunctions);
}
@Override
public String toString() {
return super.toString() + " @ " + getEntryPoint().getOffset();
}
}