diff --git a/jadx-core/src/main/java/jadx/core/clsp/ClspGraph.java b/jadx-core/src/main/java/jadx/core/clsp/ClspGraph.java index 3579f762cd9..566030a96bd 100644 --- a/jadx-core/src/main/java/jadx/core/clsp/ClspGraph.java +++ b/jadx-core/src/main/java/jadx/core/clsp/ClspGraph.java @@ -13,7 +13,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jadx.core.Consts; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.ClassNode; @@ -24,15 +23,37 @@ /** * Classes hierarchy graph with methods additional info + * + * nameMap is constructed through loadClsSetFile, addClasspath, or addApp. + * + * initCache must be called after the nameMap has been propagated to fill the caches before using + * other class features. + * + * isImplements, getCommonAncestor, getImplementations and others can then be used to query the + * class hierarchy. + * + * printMissingClasses is used to display any classes encountered when enumerating parents that + * were missing from the nameMap. */ public class ClspGraph { private static final Logger LOG = LoggerFactory.getLogger(ClspGraph.class); private final RootNode root; + /** Maps class names to class details */ private Map nameMap; + /** Maps class names to all super classes and implemented interfaces */ private Map> superTypesCache; + /** Maps class names to all sub classes and implementations */ private Map> implementsCache; + /** + * Maps class names to immediate super classes - this is equivalent to the .parents attribute of the + * ClspClasses in the nameMap + */ + private Map> immediateSuperTypesCache; + /** Maps class names to immeditae sub classes and implementations */ + private Map> immediateImplementsCache; + /** Classes encountered when building the caches that have not had details provided */ private final Set missingClasses = new HashSet<>(); public ClspGraph(RootNode rootNode) { @@ -54,6 +75,11 @@ public void addClasspath(ClsSet set) { } } + /** + * Add a list of classes to the class map + * + * @param classes the classes to add + */ public void addApp(List classes) { if (nameMap == null) { nameMap = new HashMap<>(classes.size()); @@ -63,17 +89,44 @@ public void addApp(List classes) { } } + /** + * Construct the cached mappings from the added classes + */ public void initCache() { + fillImmediateSuperTypesCache(); + fillImmediateImplementsCache(); fillSuperTypesCache(); fillImplementsCache(); } + /** + * Check if the class name has been added to the graph + * + * @param fullName the raw name of the class to check + * @return + */ public boolean isClsKnown(String fullName) { return nameMap.containsKey(fullName); } + /** + * Get the ClspClass for an object + * + * @param type the ArgType of the object to fetch + * @return + */ public ClspClass getClsDetails(ArgType type) { - return nameMap.get(type.getObject()); + return getClsDetails(type.getObject()); + } + + /** + * Get the ClspClass for a class string + * + * @param cls the name of the class to fetch + * @return + */ + public ClspClass getClsDetails(String cls) { + return nameMap.get(cls); } @Nullable @@ -104,6 +157,13 @@ private ClspMethod getMethodFromClass(ClspClass cls, MethodInfo methodInfo) { return cls.getMethodsMap().get(methodInfo.getShortId()); } + /** + * Add a class to the class path graph. + * + * Extracts the name, access flags, and parents information to store in a ClspClass entry + * + * @param cls + */ private void addClass(ClassNode cls) { ArgType clsType = cls.getClassInfo().getType(); String rawName = clsType.getObject(); @@ -120,11 +180,46 @@ public boolean isImplements(String clsName, String implClsName) { return anc.contains(implClsName); } + /** + * Get all implementations of a class + * + * @param clsName + * @return + */ public List getImplementations(String clsName) { List list = implementsCache.get(clsName); return list == null ? Collections.emptyList() : list; } + /** + * Get direct implementations of a class + * + * @param clsName + * @return + */ + public List getChildren(String clsName) { + List list = immediateImplementsCache.get(clsName); + return list == null ? Collections.emptyList() : list; + } + + /** + * Propogate the immediate implements cache by reversing the immediateSuperTypesCache + */ + private void fillImmediateImplementsCache() { + Map> map = new HashMap<>(nameMap.size()); + List classes = new ArrayList<>(nameMap.keySet()); + Collections.sort(classes); + for (String cls : classes) { + for (String st : getParents(cls)) { + map.computeIfAbsent(st, v -> new ArrayList<>()).add(cls); + } + } + immediateImplementsCache = map; + } + + /** + * Propogate the implements cache by reversing the superTypesCache + */ private void fillImplementsCache() { Map> map = new HashMap<>(nameMap.size()); List classes = new ArrayList<>(nameMap.keySet()); @@ -170,57 +265,62 @@ private String searchCommonParent(Set anc, ClspClass cls) { return null; } + /** + * Get all super types for a class + * + * @param clsName + * @return + */ public Set getSuperTypes(String clsName) { Set result = superTypesCache.get(clsName); return result == null ? Collections.emptySet() : result; } - private static final Set OBJECT_SINGLE_SET = Collections.singleton(Consts.CLASS_OBJECT); + /** + * Get the immediate parents for a class + * + * @param clsName + * @return + */ + public Set getParents(String clsName) { + if (!immediateSuperTypesCache.containsKey(clsName)) { + return null; + } + return immediateSuperTypesCache.get(clsName); + } - private void fillSuperTypesCache() { - Map> map = new HashMap<>(nameMap.size()); - Set tmpSet = new HashSet<>(); + /** + * Propogate the immediateSuperTypesCache by traversing the nameMap and inspecting the parents of + * the ClspClasses + */ + private void fillImmediateSuperTypesCache() { + Map> nametoSupertypesMap = new HashMap<>(nameMap.size()); for (Map.Entry entry : nameMap.entrySet()) { + Set supertypesSet = new HashSet<>(); ClspClass cls = entry.getValue(); - tmpSet.clear(); - addSuperTypes(cls, tmpSet); - Set result; - int size = tmpSet.size(); - switch (size) { - case 0: { - result = Collections.emptySet(); - break; - } - case 1: { - String supCls = tmpSet.iterator().next(); - if (supCls.equals(Consts.CLASS_OBJECT)) { - result = OBJECT_SINGLE_SET; - } else { - result = Collections.singleton(supCls); - } - break; - } - default: { - result = new HashSet<>(tmpSet); - break; - } - } - map.put(cls.getName(), result); + addImmediateSuperTypes(cls, supertypesSet); + nametoSupertypesMap.put(cls.getName(), supertypesSet); } - superTypesCache = map; + immediateSuperTypesCache = nametoSupertypesMap; } - private void addSuperTypes(ClspClass cls, Set result) { + /** + * Add only the names of immediate super types of cls to result + * + * @param cls + * @param result + */ + private void addImmediateSuperTypes(ClspClass cls, Set result) { for (ArgType parentType : cls.getParents()) { if (parentType == null) { continue; } ClspClass parentCls = getClspClass(parentType); + + // add just the parent if (parentCls != null) { - boolean isNew = result.add(parentCls.getName()); - if (isNew) { - addSuperTypes(parentCls, result); - } + // this should be equivalent to parentType.getObject() + result.add(parentCls.getName()); } else { // parent type is unknown result.add(parentType.getObject()); @@ -228,6 +328,51 @@ private void addSuperTypes(ClspClass cls, Set result) { } } + /** + * Propogate the superTypesCache by traversing the immediateSuperTypesCache + */ + private void fillSuperTypesCache() { + Map> nametoSupertypesMap = new HashMap<>(nameMap.size()); + for (String name : immediateSuperTypesCache.keySet()) { + Set supertypesSet = new HashSet<>(); + addSuperTypes(name, supertypesSet); + nametoSupertypesMap.put(name, supertypesSet); + } + superTypesCache = nametoSupertypesMap; + } + + /** + * Add the names of super types of cls to result using immediateSuperTypesCache + * + * @param clsName + * @param result + */ + private void addSuperTypes(String clsName, Set result) { + Set parents = getParents(clsName); + if (parents == null) { + return; + } + + for (String parent : parents) { + // add the parent + boolean isNew = result.add(parent); + if (isNew) { + // add super types of the parent + addSuperTypes(parent, result); + } + } + } + + /** + * Get the ClspClass for an object when propogating the super types cache. + * Adds objects to the missingClasses list if they are encountered but haven't been added to the + * nameMap. + * + * An internal equivalent to getClsDetails that handles constructing missing classes + * + * @param clsType + * @return + */ @Nullable private ClspClass getClspClass(ArgType clsType) { ClspClass clspClass = nameMap.get(clsType.getObject()); @@ -237,6 +382,9 @@ private ClspClass getClspClass(ArgType clsType) { return clspClass; } + /** + * Display missing classes + */ public void printMissingClasses() { int count = missingClasses.size(); if (count == 0) { diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java index 42f20d2de56..47397376e5d 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java @@ -66,8 +66,17 @@ public class ClassNode extends NotificationAttrNode private final ClassInfo clsInfo; private PackageNode packageNode; private AccessInfo accessFlags; + + /** + * This class' super class + */ private ArgType superClass; + + /** + * Interfaces this class implements + */ private List interfaces; + private List generics = Collections.emptyList(); private String inputFileName; @@ -493,11 +502,17 @@ private void buildCache() { } } + /* + * Get the superclass of this class + */ @Nullable public ArgType getSuperClass() { return superClass; } + /* + * Get the interfaces this class implements + */ public List getInterfaces() { return interfaces; } diff --git a/jadx-core/src/main/java/jadx/core/utils/DotGraphUtils.java b/jadx-core/src/main/java/jadx/core/utils/DotGraphUtils.java index a7a8b2a823e..f087d9dbc9d 100644 --- a/jadx-core/src/main/java/jadx/core/utils/DotGraphUtils.java +++ b/jadx-core/src/main/java/jadx/core/utils/DotGraphUtils.java @@ -30,6 +30,7 @@ import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; +import jadx.core.dex.nodes.RootNode; import jadx.core.dex.regions.SwitchRegion; import jadx.core.dex.regions.SynchronizedRegion; import jadx.core.dex.regions.TryCatchRegion; @@ -467,12 +468,13 @@ public static String methodFormatName(JavaMethod javaMethod, boolean longName) { public static String methodFormatName(MethodNode methodNode, boolean longName) { if (longName) { + RootNode root = methodNode.root(); ClassNode parentClass = methodNode.getParentClass(); List argTypes = methodNode.getArgTypes(); ArgType retType = methodNode.getReturnType(); return classFormatName(parentClass, true) + "." + methodFormatName(methodNode, false) - + '(' + Utils.listToString(argTypes, ", ", e -> argTypeFormatName(e, parentClass, true)) + "):" - + argTypeFormatName(retType, parentClass, true); + + '(' + Utils.listToString(argTypes, ", ", e -> argTypeFormatName(e, root, true)) + "):" + + argTypeFormatName(retType, root, true); } return methodNode.getAlias(); } @@ -488,14 +490,19 @@ public static String unresolvedMethodFormatName(MethodInfo mthInfo, boolean long return name; } - public static String interfaceFormatName(ArgType iface, ClassNode cls, boolean longName) { - ClassInfo ifaceInfo = ClassInfo.fromType(cls.root(), iface); + public static String interfaceFormatName(ArgType iface, RootNode root, boolean longName) { + ClassInfo ifaceInfo = ClassInfo.fromType(root, iface); return longName ? ifaceInfo.getAliasFullName() : ifaceInfo.getAliasShortName(); } - public static String argTypeFormatName(ArgType arg, ClassNode cls, boolean longName) { + public static String rawNameFormatName(String rawName, RootNode root, boolean longName) { + ClassInfo ifaceInfo = ClassInfo.fromName(root, rawName); + return longName ? ifaceInfo.getAliasFullName() : ifaceInfo.getAliasShortName(); + } + + public static String argTypeFormatName(ArgType arg, RootNode root, boolean longName) { if (arg.isObject() && !arg.isGenericType()) { - ClassNode superCls = cls.root().resolveClass(arg); + ClassNode superCls = root.resolveClass(arg); if (superCls != null) { return DotGraphUtils.classFormatName(superCls, longName); } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/graphs/ClassInheritanceGraphDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/graphs/ClassInheritanceGraphDialog.java index 2456bfc7bfc..859bce02322 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/graphs/ClassInheritanceGraphDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/graphs/ClassInheritanceGraphDialog.java @@ -2,28 +2,39 @@ import java.awt.BorderLayout; import java.awt.Color; +import java.awt.Dimension; import java.awt.FlowLayout; import java.util.ArrayList; import java.util.Formatter; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; +import javax.swing.Box; +import javax.swing.BoxLayout; import javax.swing.JCheckBox; +import javax.swing.JLabel; import javax.swing.JMenuBar; import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.SpinnerNumberModel; +import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.UIManager; -import com.android.apksig.internal.util.Pair; - +import jadx.core.Consts; +import jadx.core.clsp.ClspClass; +import jadx.core.clsp.ClspGraph; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.MethodOverrideAttr; -import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.IMethodDetails; import jadx.core.dex.nodes.MethodNode; +import jadx.core.dex.nodes.RootNode; import jadx.core.utils.DotGraphUtils; +import jadx.core.utils.Pair; import jadx.core.utils.StringUtils; import jadx.gui.treemodel.JClass; import jadx.gui.ui.MainWindow; @@ -32,6 +43,7 @@ import static jadx.core.utils.DotGraphUtils.classFormatName; import static jadx.core.utils.DotGraphUtils.formatColor; +import static jadx.core.utils.DotGraphUtils.rawNameFormatName; import static jadx.core.utils.DotGraphUtils.toDotNodeName; public class ClassInheritanceGraphDialog extends GraphDialog { @@ -42,9 +54,14 @@ public class ClassInheritanceGraphDialog extends GraphDialog { private final ClassNode cls; private boolean longNames = false; private boolean overrides = false; + private boolean siblings = false; + private int distanceLimit = 3; + + private Set nodesToAdd; + private Set> edgesToAdd; - private Map objectToNodeID = new HashMap<>(); - private int nextNodeID = 0; + private Map nameToNodeID; + private int nextNodeID; public ClassInheritanceGraphDialog(MainWindow mainWindow, ClassNode cls) { super(mainWindow, String.format("%s: %s", @@ -73,12 +90,43 @@ public JMenuBar addMenuBar() { reload(); }); + // Siblings checkbox + JCheckBox showSiblings = new JCheckBox(NLS.str("graph_viewer.inheritance_graph.siblings")); + showSiblings.setSelected(false); + showSiblings.addItemListener(e -> { + siblings = showSiblings.isSelected(); + reload(); + }); + + // Distance spinner + SpinnerNumberModel distanceSpinnerModel = new SpinnerNumberModel(3, 0, 100, 1); + JSpinner distanceSpinner = new JSpinner(distanceSpinnerModel); + distanceSpinner.addChangeListener(e -> { + distanceLimit = (int) distanceSpinner.getValue(); + reload(); + }); + + // Distance label + JLabel distanceLbl = new JLabel(NLS.str("graph_viewer.inheritance_graph.distance")); + distanceLbl.setLabelFor(distanceSpinner); + distanceLbl.setHorizontalAlignment(SwingConstants.LEFT); + + // Assemble distance panel + JPanel distancePanel = new JPanel(); + distancePanel.setOpaque(false); + distancePanel.setLayout(new BoxLayout(distancePanel, BoxLayout.LINE_AXIS)); + distancePanel.add(distanceSpinner); + distancePanel.add(Box.createRigidArea(new Dimension(3, 0))); + distancePanel.add(distanceLbl); + // Assemble menubar panel JPanel menuBarPanel = new JPanel(); menuBarPanel.setOpaque(false); menuBarPanel.setLayout(new WrapLayout(FlowLayout.LEFT)); menuBarPanel.add(showLongNames, BorderLayout.PAGE_START); menuBarPanel.add(showOverrides, BorderLayout.PAGE_START); + menuBarPanel.add(showSiblings, BorderLayout.PAGE_START); + menuBarPanel.add(distancePanel, BorderLayout.PAGE_START); // Add menubar panel to menuBar menuBar.add(menuBarPanel); @@ -101,146 +149,360 @@ public void reload() { } private String generateGraph(ClassNode rootClass) { - objectToNodeID = new HashMap<>(); + // Reset state + this.nameToNodeID = new HashMap<>(); + this.nextNodeID = 0; + this.nodesToAdd = new HashSet<>(); + this.edgesToAdd = new HashSet<>(); + + // Build the graph + StringBuilder sb = new StringBuilder(); + try (Formatter f = new Formatter(sb)) { + // Graph header + addGraphHeader(f); + + RootNode root = rootClass.root(); + String rootClassName = rootClass.getClassInfo().getType().getObject(); + ClspGraph classGraph = root.getClsp(); + + // Collect nodes and edges within distanceLimit into nodesToAdd and edgesToAdd + visitClass(classGraph, rootClassName, distanceLimit); + + // Add nodes to the graph + addNodes(f, root, classGraph, rootClassName); + + // Add edges to the graph + addEdges(f); + + // Close graph + f.format("}"); + return f.toString(); + } + } + + /** + * Walk graph from rawName to find all nodes and edges within distanceLimit steps. + * If siblings is false, only the direct hierarchy will be shown e.g. parents of parents and + * children of children. + * If siblings is true, the whole graph will be shown e.g. other parents of children and other + * children of parents. + * + * @param graph a ClspGraph storing parent and children relationships + * @param classNode the current class + * @param distanceLimit the distance to display from the current class + */ + private void visitClass(ClspGraph graph, String rawName, int distanceLimit) { + visitClass(graph, rawName, distanceLimit, true, true); + } + + /** + * Walk graph from rawName to find all nodes and edges within distanceLimit steps + * + * @param graph a ClspGraph storing parent and children relationships + * @param classNode the current class + * @param distanceLimit the distance to display from the current class + * @param visitChildren whether to visit children of this class + * @param visitParents whether to visit parents of this class + */ + private void visitClass(ClspGraph graph, String rawName, int distanceLimit, boolean visitChildren, boolean visitParents) { + // Don't process this class again if it has already been visited + if (nodesToAdd.contains(rawName)) { + return; + } + + // Add a graph node for the current class + nodesToAdd.add(rawName); + + // Stop searching if the distance limit has been reached + if (distanceLimit <= 0) { + return; + } + + // Get the names of children and parents in the ClspGraph + List children = graph.getChildren(rawName); + Set parents = graph.getParents(rawName); + + // Visit parents + if (visitParents) { + for (String parent : parents) { + // Don't display the java.lang.Object class + if (parent == Consts.CLASS_OBJECT) { + continue; + } + Pair edge = new Pair<>(parent, rawName); + + if (edgesToAdd.contains(edge)) { + // If the egde has already been added, both sides have already been visited and don't need to be + // visited again + continue; + } + + // Add an edge from the parent to the current class + edgesToAdd.add(edge); + + // Process the parent - adds a node representing the parent + visitClass(graph, parent, distanceLimit - 1, siblings, true); + } + } + + // Visit children + if (visitChildren) { + for (String child : children) { + Pair edge = new Pair<>(rawName, child); + + if (edgesToAdd.contains(edge)) { + // If the egde has already been added, both sides have already been visited and don't need to be + // visited again + continue; + } + + // Add an edge from the current class to the child + edgesToAdd.add(edge); + + // Process the child - adds a node representing the child + visitClass(graph, child, distanceLimit - 1, true, siblings); + } + } + } + + /** + * Add the header specifying the format of nodes and edges to the graph + * + * @param f the string formatter to contain the graph + */ + private void addGraphHeader(Formatter f) { + // Construct colour strings Color themeBackground = UIManager.getColor("Panel.background"); Color themeForeground = UIManager.getColor("Label.foreground"); - Color themeHighlight = UIManager.getColor("Component.focusedBorderColor"); Color themeShade = UIManager.getColor("TextArea.background"); String bgColor = "bgcolor=" + formatColor(themeBackground); String lineColor = "color=" + formatColor(themeForeground); String fontColor = "fontcolor=" + formatColor(themeForeground); - String highlightColor = "color=" + formatColor(themeHighlight); String shadeColor = "fillcolor=" + formatColor(themeShade); - StringBuilder sb = new StringBuilder(); - try (Formatter f = new Formatter(sb)) { - // graph header - f.format("digraph G {\n"); - f.format("%s\n", bgColor); - f.format("node[shape=\"record\" style=\"filled\" %s %s %s %s]\n", FONT, fontColor, lineColor, shadeColor); - f.format("edge[arrowtail=\"onormal\" arrowhead=\"onormal\" %s %s %s]\n", FONT, fontColor, lineColor); - - // add nodes - processClass(f, rootClass, highlightColor); - // close graph - f.format("}"); - return f.toString(); - } + f.format("digraph G {\n"); + f.format("%s\n", bgColor); + f.format("node[shape=\"record\" style=\"filled\" %s %s %s %s]\n", FONT, fontColor, lineColor, shadeColor); + f.format("edge[arrowtail=\"onormal\" arrowhead=\"onormal\" %s %s %s]\n", FONT, fontColor, lineColor); } - private int processClass(Formatter f, ClassNode cls) { - return processClass(f, cls, ""); - } + /** + * Add all edges from edgesToAdd to the graph + * + * @param f the string formatter to contain the graph + */ + private void addEdges(Formatter f) { + for (Pair edge : edgesToAdd) { - private int processClass(Formatter f, ClassNode cls, String extra) { - if (objectToNodeID.containsKey(cls)) { - // Don't process a class that has been processed before - return objectToNodeID.get(cls); - } - int classID = addNode(f, cls, extra); - - // add interface relationships - for (ArgType iface : cls.getInterfaces()) { - int ifaceID; - ClassNode ifaceNode = cls.root().resolveClass(iface); - if (ifaceNode != null) { - ifaceID = processClass(f, ifaceNode); - objectToNodeID.put(iface, ifaceID); + int firstID; + int secondID; + + // Get the node ID for the source + if (nameToNodeID.containsKey(edge.getFirst())) { + firstID = nameToNodeID.get(edge.getFirst()); } else { - ifaceID = addNode(f, iface); + // If the source can't be resolved, ignore the edge + continue; } - // Classes implement interfaces, interfaces extend interfaces - String edgeLabel = cls.getAccessFlags().isInterface() ? "extends" : "implements"; - f.format("Node_%d -> Node_%d [label=\"%s\" style=\"dashed\" ]\n", classID, ifaceID, edgeLabel); - } - // add superclass relationship - ArgType superClass = cls.getSuperClass(); - if (superClass != ArgType.OBJECT && superClass != null) { - int superClsID; - cls = cls.root().resolveClass(superClass); - if (cls != null) { - superClsID = processClass(f, cls); - objectToNodeID.put(superClass, superClsID); + + // Get the node ID for the destination + if (nameToNodeID.containsKey(edge.getSecond())) { + secondID = nameToNodeID.get(edge.getSecond()); } else { - superClsID = addNode(f, superClass); + // If the destination can't be resolved, ignore the edge + continue; } - f.format("Node_%d -> Node_%d [label=\"extends\" ]\n", classID, superClsID); + + f.format("Node_%d -> Node_%d\n", firstID, secondID); } - return classID; } - // Add a node for a class - private int addNode(Formatter f, ClassNode cls) { - return addNode(f, cls, ""); + /** + * Add all nodes from nodesToAdd to the graph + * + * @param f the string formatter to contain the graph + * @param root the RootNode used to resolve node names to ClassNodes + * @param classGraph the ClspGraph used to resolve node names to ClspClasses + * @param rootClassName the name of the class to highlight in the graph + */ + private void addNodes(Formatter f, RootNode root, ClspGraph classGraph, String rootClassName) { + // Construct colour details + Color themeHighlight = UIManager.getColor("Component.focusedBorderColor"); + Color themeOutOfFocus = UIManager.getColor("Component.disabledBorderColor"); + + String highlightColor = "color=" + formatColor(themeHighlight); + String outOfFocus = "color=" + formatColor(themeOutOfFocus); + + for (String node : nodesToAdd) { + + // Attempt to resolve full ClassNode information + ClassNode classNode = root.resolveClass(node); + + if (classNode == null) { + // If resolving the ClassNode failes, attempt to resolve partial ClspClass information + ClspClass clspClass = classGraph.getClsDetails(node); + + if (clspClass == null) { + // Display an out of focus node with no additional information + addNode(f, node, outOfFocus); + } else { + // Display an out of focus node with interface/class information + addNode(f, clspClass, outOfFocus); + } + } else { + // Highlight the root class + String extra; + if (node == rootClassName) { + extra = highlightColor; + } else { + extra = ""; + } + + // Display a node with full information + addNode(f, classNode, extra); + } + } } + /** + * Add a node to the graph representing a ClassNode + * + * @param f a Formatter to build the graph into + * @param cls the class to add a node for + * @param extra extra formatting to append to the end of the current class's + * node in the graph e.g. a highlight colour + * @return the node id of the node created + */ private int addNode(Formatter f, ClassNode cls, String extra) { - int nodeID; - if (objectToNodeID.containsKey(cls)) { - nodeID = objectToNodeID.get(cls); - } else { - nodeID = nextNodeID; - nextNodeID++; - objectToNodeID.put(cls, nodeID); + String rawName = cls.getClassInfo().getType().getObject(); + if (nameToNodeID.containsKey(rawName)) { + return nameToNodeID.get(rawName); } + + int nodeID = nextNodeID++; + nameToNodeID.put(rawName, nodeID); + + // Make dashed if the class is an interface if (cls.getAccessFlags().isInterface()) { extra += " style=\"dashed, filled\""; } + + // Get the name to display the class as String name = classFormatName(cls, longNames); + + // Add the start of the graph node with the class name f.format("Node_%d [ label=\"{%s\\ ", nodeID, toDotNodeName(name)); + + // Add any override information if (overrides) { + // Start a new section f.format("|"); - List> table = new ArrayList<>(); + + // Construct a table of method name to method details + List> table = new ArrayList<>(); for (MethodNode method : cls.getMethods()) { + // Parse each overridden method MethodOverrideAttr ovrdAttr = method.get(AType.METHOD_OVERRIDE); if (ovrdAttr != null) { if (!ovrdAttr.getOverrideList().isEmpty()) { + // Get the method name String methodName = DotGraphUtils.methodFormatName(method, longNames); + // Start the details string Formatter details = new Formatter(); details.format(" overrides "); + // Mark each base class that this method overrides from for (IMethodDetails baseMthDetails : ovrdAttr.getOverrideList()) { - String baseClassName = classFormatName(baseMthDetails.getMethodInfo().getDeclClass(), longNames); + String baseClassName = classFormatName(baseMthDetails.getMethodInfo().getDeclClass(), + longNames); details.format("%s, ", baseClassName); } + // Remove any trailing commas String detailsString = StringUtils.removeSuffix(details.toString(), ", "); - table.add(Pair.of(methodName, detailsString)); + // Add the method name and details to the table + table.add(new Pair<>(methodName, detailsString)); details.close(); } } } + + // Format the table if (!table.isEmpty()) { int longestLength = table.stream().map(Pair::getFirst).map(String::length).max((a, b) -> a - b).get(); - for (Pair entry : table) { + for (Pair entry : table) { f.format("%-" + longestLength + "s %s\\l", entry.getFirst(), entry.getSecond()); } } else { f.format("No overrides."); } } + + // Close the graph node f.format("}\" %s]\n", extra); return nodeID; } - // Add a node for an unresolved arg type - private int addNode(Formatter f, ArgType argType) { - return addNode(f, argType, ""); + /** + * Add a node to the graph representing a raw name that could be resolved to a ClspClass but not a + * ClassNode + * + * @param f a Formatter to build the graph into + * @param cls the class to add a node for + * @param extra extra formatting to append to the end of the current class's + * node in the graph e.g. a highlight colour + * @return the node id of the node created + */ + private int addNode(Formatter f, ClspClass cls, String extra) { + String rawName = cls.getName(); + if (nameToNodeID.containsKey(rawName)) { + return nameToNodeID.get(rawName); + } + + int nodeID = nextNodeID++; + nameToNodeID.put(rawName, nodeID); + + // Make dashed if the class is an interface + if (cls.isInterface()) { + extra += " style=\"dashed, filled\""; + } + + // Get the name to display the class as + String name = rawNameFormatName(rawName, this.cls.root(), longNames); + + // Add the start of the graph node with the class name + f.format("Node_%d [ label=\"{%s\\ ", nodeID, toDotNodeName(name)); + + // TODO: can we retrieve overrides for classes that resolve to a ClspClass but not a ClassNode? + + // Close the graph node + f.format("}\" %s]\n", extra); + return nodeID; } - private int addNode(Formatter f, ArgType argType, String extra) { - int nodeID; - if (objectToNodeID.containsKey(argType)) { - nodeID = objectToNodeID.get(argType); - } else { - nodeID = nextNodeID; - nextNodeID++; - objectToNodeID.put(argType, nodeID); + /** + * Add a node to the graph representing a raw name that couldn't be resolved to + * a ClassNode + * + * @param f a Formatter to build the graph into + * @param rawName the class to add a node for + * @param extra extra formatting to append to the end of the current class's + * node in the graph e.g. a highlight colour + * @return the node id of the node created + */ + private int addNode(Formatter f, String rawName, String extra) { + if (nameToNodeID.containsKey(rawName)) { + return nameToNodeID.get(rawName); } - Color themeOutOfFocus = UIManager.getColor("Component.disabledBorderColor"); - String outOfFocus = "color=" + formatColor(themeOutOfFocus); - String name = DotGraphUtils.interfaceFormatName(argType, cls, longNames); - f.format("Node_%d [ label=\"{%s}\" %s %s]\n", nodeID, toDotNodeName(name), outOfFocus, extra); + + int nodeID = nextNodeID++; + nameToNodeID.put(rawName, nodeID); + + // Construct name details + String name = DotGraphUtils.rawNameFormatName(rawName, cls.root(), longNames); + + // Add the node with name and colour details + f.format("Node_%d [ label=\"{%s}\" %s]\n", nodeID, toDotNodeName(name), extra); return nodeID; } } diff --git a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties index 3de943da065..31996e57b54 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties @@ -574,6 +574,8 @@ action_category.plugin_script=Plugin-Skript #graph_viewer.method_graph.title=Methods Graph #graph_viewer.call_graph.title=Call Graph #graph_viewer.inheritance_graph.title=Inheritance Graph +#graph_viewer.inheritance_graph.distance=Distance +#graph_viewer.inheritance_graph.siblings=Show siblings #graph_viewer.cfg.title=Control Flow Graph #graph_viewer.cfg.preset_selector_label=CFG preset #graph_viewer.cfg.preset_names=Raw|Normal|Region diff --git a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties index 12384ae8d38..0eccec25d64 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -574,6 +574,8 @@ graph_viewer.default_title=Graph Viewer graph_viewer.method_graph.title=Methods Graph graph_viewer.call_graph.title=Call Graph graph_viewer.inheritance_graph.title=Inheritance Graph +graph_viewer.inheritance_graph.distance=Distance +graph_viewer.inheritance_graph.siblings=Show siblings graph_viewer.cfg.title=Control Flow Graph graph_viewer.cfg.preset_selector_label=CFG preset graph_viewer.cfg.preset_names=Raw|Normal|Region diff --git a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties index 5a1cca87bf0..a3cf05fdb3e 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties @@ -574,6 +574,8 @@ certificate.serialPubKeyY=Y #graph_viewer.method_graph.title=Methods Graph #graph_viewer.call_graph.title=Call Graph #graph_viewer.inheritance_graph.title=Inheritance Graph +#graph_viewer.inheritance_graph.distance=Distance +#graph_viewer.inheritance_graph.siblings=Show siblings #graph_viewer.cfg.title=Control Flow Graph #graph_viewer.cfg.preset_selector_label=CFG preset #graph_viewer.cfg.preset_names=Raw|Normal|Region diff --git a/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties b/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties index 26278394fe1..8ed710a0f8b 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties @@ -574,6 +574,8 @@ action_category.plugin_script=Plugin Script #graph_viewer.method_graph.title=Methods Graph #graph_viewer.call_graph.title=Call Graph #graph_viewer.inheritance_graph.title=Inheritance Graph +#graph_viewer.inheritance_graph.distance=Distance +#graph_viewer.inheritance_graph.siblings=Show siblings #graph_viewer.cfg.title=Control Flow Graph #graph_viewer.cfg.preset_selector_label=CFG preset #graph_viewer.cfg.preset_names=Raw|Normal|Region diff --git a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties index d289cbb340d..72e70d045cd 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties @@ -574,6 +574,8 @@ adb_dialog.starting_debugger=디버거 시작 중 ... #graph_viewer.method_graph.title=Methods Graph #graph_viewer.call_graph.title=Call Graph #graph_viewer.inheritance_graph.title=Inheritance Graph +#graph_viewer.inheritance_graph.distance=Distance +#graph_viewer.inheritance_graph.siblings=Show siblings #graph_viewer.cfg.title=Control Flow Graph #graph_viewer.cfg.preset_selector_label=CFG preset #graph_viewer.cfg.preset_names=Raw|Normal|Region diff --git a/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties b/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties index feeae7290e9..4d280908b88 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties @@ -574,6 +574,8 @@ adb_dialog.starting_debugger=Iniciando depurador... #graph_viewer.method_graph.title=Methods Graph #graph_viewer.call_graph.title=Call Graph #graph_viewer.inheritance_graph.title=Inheritance Graph +#graph_viewer.inheritance_graph.distance=Distance +#graph_viewer.inheritance_graph.siblings=Show siblings #graph_viewer.cfg.title=Control Flow Graph #graph_viewer.cfg.preset_selector_label=CFG preset #graph_viewer.cfg.preset_names=Raw|Normal|Region diff --git a/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties b/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties index 3827ff350a8..8d0e4924287 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties @@ -574,6 +574,8 @@ action_category.plugin_script=Скрипты и плагины #graph_viewer.method_graph.title=Methods Graph #graph_viewer.call_graph.title=Call Graph #graph_viewer.inheritance_graph.title=Inheritance Graph +#graph_viewer.inheritance_graph.distance=Distance +#graph_viewer.inheritance_graph.siblings=Show siblings #graph_viewer.cfg.title=Control Flow Graph #graph_viewer.cfg.preset_selector_label=CFG preset #graph_viewer.cfg.preset_names=Raw|Normal|Region diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties index ad739f7491a..7bea4df6110 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties @@ -574,6 +574,8 @@ graph_viewer.default_title=图形查看器 graph_viewer.method_graph.title=方法图 graph_viewer.call_graph.title=调用图 graph_viewer.inheritance_graph.title=继承图 +#graph_viewer.inheritance_graph.distance=Distance +#graph_viewer.inheritance_graph.siblings=Show siblings graph_viewer.cfg.title=控制图 #graph_viewer.cfg.preset_selector_label=CFG preset #graph_viewer.cfg.preset_names=Raw|Normal|Region diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties index e260f80511d..028a9804399 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties @@ -574,6 +574,8 @@ action_category.plugin_script=外掛程式腳本 #graph_viewer.method_graph.title=Methods Graph #graph_viewer.call_graph.title=Call Graph #graph_viewer.inheritance_graph.title=Inheritance Graph +#graph_viewer.inheritance_graph.distance=Distance +#graph_viewer.inheritance_graph.siblings=Show siblings #graph_viewer.cfg.title=Control Flow Graph #graph_viewer.cfg.preset_selector_label=CFG preset #graph_viewer.cfg.preset_names=Raw|Normal|Region