mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-04 10:19:23 +02:00
638 lines
18 KiB
Java
638 lines
18 KiB
Java
/* ###
|
|
* 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 docking.widgets.label.GDLabel;
|
|
import functioncalls.plugin.FcgOptions;
|
|
import generic.theme.GColor;
|
|
import generic.theme.GThemeDefaults.Colors.Palette;
|
|
import generic.theme.Gui;
|
|
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 {
|
|
|
|
//@formatter:off
|
|
public static final Color DEFAULT_VERTEX_SHAPE_COLOR = new GColor("color.bg.plugin.fcg.vertex.default");
|
|
private static final Color TOO_BIG_VERTEX_SHAPE_COLOR = new GColor("color.bg.plugin.fcg.vertex.toobig");
|
|
//@formatter:on
|
|
|
|
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 = 100;
|
|
private static final Integer TOGGLE_BUTTON_LAYER = 200;
|
|
private static final Integer LABEL_LAYER = 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 GDLabel();
|
|
private JLabel vertexImageLabel = new GDLabel();
|
|
|
|
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;
|
|
private FcgOptions options;
|
|
|
|
/**
|
|
* 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
|
|
* @param options the tool options
|
|
*/
|
|
public FcgVertex(Function function, FcgLevel level,
|
|
FcgVertexExpansionListener expansionListener, FcgOptions options) {
|
|
this.function = function;
|
|
this.level = level;
|
|
this.options = options;
|
|
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 = Gui.darker(vertexShapeColor);
|
|
Color darkestColor = Gui.darker(darkColor);
|
|
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
|
|
boolean truncate = options.useTruncatedFunctionNames();
|
|
String name = getName();
|
|
if (truncate) {
|
|
name = StringUtilities.trimMiddle(getName(), MAX_NAME_LENGTH);
|
|
}
|
|
|
|
nameLabel.setText(name);
|
|
buildVertexShape();
|
|
|
|
// calculate the needed size
|
|
layeredPane = new JLayeredPane();
|
|
Border border = createDebugBorder(new LineBorder(Palette.GOLD, 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(Palette.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(Palette.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(Palette.NO_COLOR);
|
|
toggleOutsButton.setBackground(Palette.NO_COLOR);
|
|
|
|
// This is needed for Flat Dark theme to work correctly, due to the fact that it wants to
|
|
// paint its parent background when the button is opaque. The parent background will get
|
|
// painted over any items that lie between the button and the parent.
|
|
toggleInsButton.setOpaque(false);
|
|
toggleOutsButton.setOpaque(false);
|
|
|
|
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 hasOutgoing true if this vertex has any outgoing 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() {
|
|
return Objects.hash(function);
|
|
}
|
|
|
|
@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
|
|
}
|
|
}
|