feat: new module/library for work with call graphs (#2890)
This commit is contained in:
+2
-1
@@ -37,7 +37,8 @@ jadx-output/
|
||||
*.log
|
||||
*.cfg
|
||||
*.orig
|
||||
quark.json
|
||||
*.json
|
||||
*.dot
|
||||
|
||||
.env
|
||||
|
||||
|
||||
@@ -166,6 +166,7 @@ options:
|
||||
--fs-case-sensitive - treat filesystem as case sensitive, false by default
|
||||
--cfg - save methods control flow graph to dot file
|
||||
--raw-cfg - save methods control flow graph (use raw instructions)
|
||||
--call-graph - save app call graph in format: 'dot' or 'json', default: none
|
||||
-f, --fallback - set '--decompilation-mode' to 'fallback' (deprecated)
|
||||
--use-dx - use dx/d8 to convert java bytecode
|
||||
--comments-level - set code comments level, values: error, warn, info, debug, user-only, none, default: info
|
||||
|
||||
@@ -11,6 +11,7 @@ dependencies {
|
||||
implementation(project(":jadx-core"))
|
||||
implementation(project(":jadx-plugins-tools"))
|
||||
implementation(project(":jadx-commons:jadx-app-commons"))
|
||||
implementation(project(":jadx-commons:jadx-analysis"))
|
||||
|
||||
runtimeOnly(project(":jadx-plugins:jadx-dex-input"))
|
||||
runtimeOnly(project(":jadx-plugins:jadx-java-input"))
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package jadx.cli;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.analysis.callgraph.JadxCallGraph;
|
||||
import jadx.analysis.callgraph.api.ICallGraph;
|
||||
import jadx.api.JadxArgs;
|
||||
import jadx.api.JadxDecompiler;
|
||||
import jadx.api.impl.AnnotatedCodeWriter;
|
||||
@@ -16,6 +19,7 @@ import jadx.cli.LogHelper.LogLevelEnum;
|
||||
import jadx.cli.config.JadxConfigAdapter;
|
||||
import jadx.cli.plugins.JadxFilesGetter;
|
||||
import jadx.core.utils.exceptions.JadxArgsValidateException;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.plugins.tools.JadxExternalPluginsLoader;
|
||||
|
||||
public class JadxCLI {
|
||||
@@ -73,6 +77,7 @@ public class JadxCLI {
|
||||
if (checkForErrors(jadx)) {
|
||||
return 2;
|
||||
}
|
||||
writeCallGraph(jadx, cliArgs);
|
||||
if (!SingleClassMode.process(jadx, cliArgs)) {
|
||||
save(jadx);
|
||||
}
|
||||
@@ -131,4 +136,29 @@ public class JadxCLI {
|
||||
System.out.print(" \r");
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeCallGraph(JadxDecompiler jadx, JadxCLIArgs cliArgs) {
|
||||
JadxCLIArgs.CallGraphSaveMode mode = cliArgs.callGraphSaveMode;
|
||||
if (mode == null || mode == JadxCLIArgs.CallGraphSaveMode.NONE) {
|
||||
return;
|
||||
}
|
||||
Path outPath = jadx.getArgs().getOutDir().toPath();
|
||||
ICallGraph callGraph = JadxCallGraph.builder(jadx)
|
||||
.resolvedOnly(true)
|
||||
.build();
|
||||
Path cgPath;
|
||||
switch (mode) {
|
||||
case JSON:
|
||||
cgPath = outPath.resolve("callgraph.json");
|
||||
callGraph.writeJson(cgPath);
|
||||
break;
|
||||
case DOT:
|
||||
cgPath = outPath.resolve("callgraph.dot");
|
||||
callGraph.writeDot(cgPath);
|
||||
break;
|
||||
default:
|
||||
throw new JadxRuntimeException("Unexpected call graph save mode: " + mode);
|
||||
}
|
||||
LOG.info("Call graph saved: {}", cgPath.toAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,6 +283,13 @@ public class JadxCLIArgs implements IJadxConfig {
|
||||
@Parameter(names = { "--raw-cfg" }, description = "save methods control flow graph (use raw instructions)")
|
||||
protected boolean rawCfgOutput = false;
|
||||
|
||||
@Parameter(
|
||||
names = { "--call-graph" },
|
||||
description = "save app call graph in format: 'dot' or 'json'",
|
||||
converter = CallGraphSaveModeConverter.class
|
||||
)
|
||||
protected CallGraphSaveMode callGraphSaveMode = CallGraphSaveMode.NONE;
|
||||
|
||||
@Parameter(names = { "-f", "--fallback" }, description = "set '--decompilation-mode' to 'fallback' (deprecated)")
|
||||
protected boolean fallbackMode = false;
|
||||
|
||||
@@ -827,6 +834,14 @@ public class JadxCLIArgs implements IJadxConfig {
|
||||
this.rawCfgOutput = rawCfgOutput;
|
||||
}
|
||||
|
||||
public CallGraphSaveMode getCallGraphSaveMode() {
|
||||
return callGraphSaveMode;
|
||||
}
|
||||
|
||||
public void setCallGraphSaveMode(CallGraphSaveMode callGraphSaveMode) {
|
||||
this.callGraphSaveMode = callGraphSaveMode;
|
||||
}
|
||||
|
||||
public boolean isReplaceConsts() {
|
||||
return replaceConsts;
|
||||
}
|
||||
@@ -1022,6 +1037,18 @@ public class JadxCLIArgs implements IJadxConfig {
|
||||
}
|
||||
}
|
||||
|
||||
public enum CallGraphSaveMode {
|
||||
NONE,
|
||||
DOT,
|
||||
JSON,
|
||||
}
|
||||
|
||||
public static class CallGraphSaveModeConverter extends BaseEnumConverter<CallGraphSaveMode> {
|
||||
public CallGraphSaveModeConverter() {
|
||||
super(CallGraphSaveMode::valueOf, CallGraphSaveMode::values);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract static class BaseEnumConverter<E extends Enum<E>> implements IStringConverter<E> {
|
||||
private final Function<String, E> parse;
|
||||
private final Supplier<E[]> values;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
## jadx analysis
|
||||
|
||||
Various utilities for analyze and process code and related information.
|
||||
|
||||
|
||||
### Call graph
|
||||
|
||||
Full app code usage/call graph.
|
||||
Usage:
|
||||
```java
|
||||
JadxArgs args = new JadxArgs();
|
||||
args.addInputFile(new File("input.apk"));
|
||||
try (JadxDecompiler jadx = new JadxDecompiler(args)) {
|
||||
jadx.load();
|
||||
|
||||
ICallGraph callGraph = JadxCallGraph.builder(jadx)
|
||||
.includePackages("com.example") // filter nodes by package
|
||||
.resolvedOnly(true) // add nodes only from app (exclude framework/lib calls)
|
||||
.build();
|
||||
|
||||
for (ICallGraphEdge edge : callGraph.edges()) {
|
||||
if (edge.isResolved()) {
|
||||
System.out.printf("Edge from '%s' to '%s'%n", edge.from(), edge.to());
|
||||
}
|
||||
}
|
||||
callGraph.writeDot(Path.of("test.dot")); // export to '.dot'
|
||||
callGraph.writeJson(Path.of("test.json")); // export to JSON
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,12 @@
|
||||
plugins {
|
||||
id("jadx-library")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":jadx-core"))
|
||||
|
||||
implementation("com.google.code.gson:gson:2.13.2")
|
||||
|
||||
testRuntimeOnly(project(":jadx-plugins:jadx-dex-input"))
|
||||
testRuntimeOnly(project(":jadx-plugins:jadx-smali-input"))
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package jadx.analysis.callgraph;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import jadx.analysis.callgraph.api.ICallGraph;
|
||||
import jadx.analysis.callgraph.api.ICallGraphEdge;
|
||||
import jadx.api.JadxArgs;
|
||||
|
||||
class CallGraph implements ICallGraph {
|
||||
|
||||
private final JadxArgs args;
|
||||
private final List<ICallGraphEdge> edges;
|
||||
|
||||
public CallGraph(JadxArgs args, List<ICallGraphEdge> edges) {
|
||||
this.args = args;
|
||||
this.edges = edges;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ICallGraphEdge> edges() {
|
||||
return edges;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeDot(Path path) {
|
||||
new CallGraphExportDot(args, this).writeTo(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeJson(Path path) {
|
||||
new CallGraphExportJson(this).writeTo(path);
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package jadx.analysis.callgraph;
|
||||
|
||||
import jadx.core.dex.attributes.AttrNode;
|
||||
|
||||
class CallGraphAttrNode extends AttrNode {
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package jadx.analysis.callgraph;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.analysis.callgraph.api.ICallGraph;
|
||||
import jadx.analysis.callgraph.api.ICallGraphBuilder;
|
||||
import jadx.analysis.callgraph.api.ICallGraphEdge;
|
||||
import jadx.api.JadxDecompiler;
|
||||
import jadx.core.dex.info.ClassInfo;
|
||||
import jadx.core.dex.info.MethodInfo;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
|
||||
class CallGraphBuilder implements ICallGraphBuilder {
|
||||
private final JadxDecompiler decompiler;
|
||||
private boolean resolvedOnly = false;
|
||||
private @Nullable String pkgFilter;
|
||||
|
||||
public CallGraphBuilder(JadxDecompiler decompiler) {
|
||||
this.decompiler = decompiler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ICallGraphBuilder resolvedOnly(boolean resolved) {
|
||||
this.resolvedOnly = resolved;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ICallGraphBuilder includePackages(String pkgFilter) {
|
||||
this.pkgFilter = pkgFilter.endsWith(".") ? pkgFilter : pkgFilter + '.';
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ICallGraph build() {
|
||||
return new CallGraph(decompiler.getArgs(), collectEdges());
|
||||
}
|
||||
|
||||
private List<ICallGraphEdge> collectEdges() {
|
||||
AtomicInteger nodeId = new AtomicInteger();
|
||||
Map<MethodInfo, CallGraphNode> nodes = new HashMap<>();
|
||||
List<ICallGraphEdge> edges = new ArrayList<>();
|
||||
|
||||
for (ClassNode cls : decompiler.getRoot().getClasses(true)) {
|
||||
if (ignorePkg(cls.getClassInfo())) {
|
||||
continue;
|
||||
}
|
||||
for (MethodNode mth : cls.getMethods()) {
|
||||
CallGraphNode thisNode = getCallGraphNode(mth, nodes, nodeId);
|
||||
for (MethodNode use : mth.getUseIn()) {
|
||||
if (ignorePkg(use.getDeclaringClass().getClassInfo())) {
|
||||
continue;
|
||||
}
|
||||
CallGraphNode useInNode = getCallGraphNode(use, nodes, nodeId);
|
||||
edges.add(new CallGraphEdge(useInNode, thisNode));
|
||||
}
|
||||
if (!resolvedOnly) {
|
||||
for (MethodInfo used : mth.getUnresolvedUsed()) {
|
||||
if (ignorePkg(used.getDeclClass())) {
|
||||
continue;
|
||||
}
|
||||
CallGraphNode usedNode = getCallGraphNode(used, nodes, nodeId);
|
||||
edges.add(new CallGraphEdge(thisNode, usedNode));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return edges;
|
||||
}
|
||||
|
||||
private boolean ignorePkg(ClassInfo cls) {
|
||||
if (pkgFilter == null) {
|
||||
return false;
|
||||
}
|
||||
return !cls.getFullName().startsWith(pkgFilter);
|
||||
}
|
||||
|
||||
private static CallGraphNode getCallGraphNode(MethodNode mth, Map<MethodInfo, CallGraphNode> nodes, AtomicInteger nodeId) {
|
||||
return nodes.computeIfAbsent(mth.getMethodInfo(), i -> new CallGraphNode(nodeId.incrementAndGet(), mth));
|
||||
}
|
||||
|
||||
private static CallGraphNode getCallGraphNode(MethodInfo mth, Map<MethodInfo, CallGraphNode> nodes, AtomicInteger nodeId) {
|
||||
return nodes.computeIfAbsent(mth, i -> new CallGraphNode(nodeId.incrementAndGet(), mth));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package jadx.analysis.callgraph;
|
||||
|
||||
import jadx.analysis.callgraph.api.ICallGraphEdge;
|
||||
import jadx.analysis.callgraph.api.ICallGraphNode;
|
||||
import jadx.core.dex.attributes.IAttributeNode;
|
||||
|
||||
class CallGraphEdge implements ICallGraphEdge {
|
||||
private final ICallGraphNode from;
|
||||
private final ICallGraphNode to;
|
||||
private final CallGraphAttrNode attrNode = new CallGraphAttrNode();
|
||||
|
||||
public CallGraphEdge(ICallGraphNode from, ICallGraphNode to) {
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ICallGraphNode from() {
|
||||
return from;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ICallGraphNode to() {
|
||||
return to;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isResolved() {
|
||||
return to.isResolved();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IAttributeNode attributes() {
|
||||
return attrNode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CallGraphEdge{from=" + from + ", to=" + to + '}';
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package jadx.analysis.callgraph;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import jadx.analysis.callgraph.api.ICallGraph;
|
||||
import jadx.analysis.callgraph.api.ICallGraphEdge;
|
||||
import jadx.analysis.callgraph.api.ICallGraphNode;
|
||||
import jadx.api.ICodeWriter;
|
||||
import jadx.api.JadxArgs;
|
||||
import jadx.api.impl.SimpleCodeWriter;
|
||||
import jadx.core.utils.DotGraphUtils;
|
||||
import jadx.core.utils.files.FileUtils;
|
||||
|
||||
import static java.nio.file.StandardOpenOption.CREATE;
|
||||
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
|
||||
import static java.nio.file.StandardOpenOption.WRITE;
|
||||
|
||||
public class CallGraphExportDot {
|
||||
private final JadxArgs args;
|
||||
private final ICallGraph callGraph;
|
||||
|
||||
public CallGraphExportDot(JadxArgs args, ICallGraph callGraph) {
|
||||
this.args = args;
|
||||
this.callGraph = callGraph;
|
||||
}
|
||||
|
||||
public void writeTo(Path path) {
|
||||
try {
|
||||
FileUtils.makeDirsForFile(path);
|
||||
Files.writeString(path, writeToString(), StandardCharsets.UTF_8,
|
||||
WRITE, TRUNCATE_EXISTING, CREATE);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to save JSON file: " + path, e);
|
||||
}
|
||||
}
|
||||
|
||||
public String writeToString() {
|
||||
// collect nodes
|
||||
Map<Integer, Node> nodeMap = new HashMap<>();
|
||||
for (ICallGraphEdge edge : callGraph.edges()) {
|
||||
addNode(edge.from(), nodeMap);
|
||||
addNode(edge.to(), nodeMap);
|
||||
}
|
||||
List<Node> nodes = new ArrayList<>(nodeMap.values());
|
||||
nodes.sort(Comparator.comparingInt(o -> o.id));
|
||||
|
||||
SimpleCodeWriter cw = new SimpleCodeWriter(args);
|
||||
cw.add("digraph CallGraph {");
|
||||
for (Node node : nodes) {
|
||||
cw.startLine();
|
||||
addNodeName(cw, node.id);
|
||||
cw.add("[shape=record,label=\"{");
|
||||
cw.add(DotGraphUtils.escape(node.method));
|
||||
cw.add("}\"];");
|
||||
}
|
||||
for (ICallGraphEdge edge : callGraph.edges()) {
|
||||
cw.startLine();
|
||||
addNodeName(cw, edge.from().getId());
|
||||
cw.add(" -> ");
|
||||
addNodeName(cw, edge.to().getId());
|
||||
cw.add(';');
|
||||
}
|
||||
cw.startLine('}');
|
||||
return cw.getCodeStr();
|
||||
}
|
||||
|
||||
private void addNodeName(ICodeWriter cw, int id) {
|
||||
cw.add('N').add(Integer.toString(id));
|
||||
}
|
||||
|
||||
private void addNode(ICallGraphNode cgNode, Map<Integer, Node> nodeMap) {
|
||||
nodeMap.computeIfAbsent(cgNode.getId(), id -> {
|
||||
Node node = new Node();
|
||||
node.id = id;
|
||||
node.method = cgNode.getMethodInfo().getRawFullId();
|
||||
return node;
|
||||
});
|
||||
}
|
||||
|
||||
static final class Node {
|
||||
int id;
|
||||
String method;
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
package jadx.analysis.callgraph;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.Strictness;
|
||||
|
||||
import jadx.analysis.callgraph.api.ICallGraph;
|
||||
import jadx.analysis.callgraph.api.ICallGraphEdge;
|
||||
import jadx.analysis.callgraph.api.ICallGraphNode;
|
||||
import jadx.core.utils.files.FileUtils;
|
||||
|
||||
import static java.nio.file.StandardOpenOption.CREATE;
|
||||
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
|
||||
import static java.nio.file.StandardOpenOption.WRITE;
|
||||
|
||||
public class CallGraphExportJson {
|
||||
private final ICallGraph callGraph;
|
||||
private final Gson gson;
|
||||
|
||||
public CallGraphExportJson(ICallGraph callGraph) {
|
||||
this.callGraph = callGraph;
|
||||
this.gson = new GsonBuilder()
|
||||
.disableJdkUnsafe()
|
||||
.disableInnerClassSerialization()
|
||||
.setStrictness(Strictness.STRICT)
|
||||
// .setPrettyPrinting() // TODO: add option for pretty print?
|
||||
.create();
|
||||
}
|
||||
|
||||
public void writeTo(Path path) {
|
||||
try {
|
||||
FileUtils.makeDirsForFile(path);
|
||||
Files.writeString(path, writeToString(), StandardCharsets.UTF_8,
|
||||
WRITE, TRUNCATE_EXISTING, CREATE);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to save JSON file: " + path, e);
|
||||
}
|
||||
}
|
||||
|
||||
public String writeToString() {
|
||||
List<Edge> edges = new ArrayList<>();
|
||||
Map<Integer, Node> nodeMap = new HashMap<>();
|
||||
for (ICallGraphEdge edge : callGraph.edges()) {
|
||||
ICallGraphNode from = edge.from();
|
||||
ICallGraphNode to = edge.to();
|
||||
addNode(from, nodeMap);
|
||||
addNode(to, nodeMap);
|
||||
Edge jsonEdge = new Edge();
|
||||
jsonEdge.from = from.getId();
|
||||
jsonEdge.to = to.getId();
|
||||
jsonEdge.resolved = edge.isResolved();
|
||||
edges.add(jsonEdge);
|
||||
}
|
||||
List<Node> nodes = new ArrayList<>(nodeMap.values());
|
||||
nodes.sort(Comparator.comparingInt(o -> o.id));
|
||||
|
||||
RootNode rootNode = new RootNode();
|
||||
rootNode.nodes = nodes;
|
||||
rootNode.edges = edges;
|
||||
return gson.toJson(rootNode);
|
||||
}
|
||||
|
||||
private void addNode(ICallGraphNode cgNode, Map<Integer, Node> nodeMap) {
|
||||
nodeMap.computeIfAbsent(cgNode.getId(), id -> {
|
||||
Node node = new Node();
|
||||
node.id = id;
|
||||
node.method = cgNode.getMethodInfo().getRawFullId();
|
||||
return node;
|
||||
});
|
||||
}
|
||||
|
||||
static final class RootNode {
|
||||
List<Node> nodes;
|
||||
List<Edge> edges;
|
||||
}
|
||||
|
||||
static final class Node {
|
||||
int id;
|
||||
String method;
|
||||
}
|
||||
|
||||
static final class Edge {
|
||||
int from;
|
||||
int to;
|
||||
boolean resolved;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package jadx.analysis.callgraph;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.analysis.callgraph.api.ICallGraphNode;
|
||||
import jadx.core.dex.attributes.IAttributeNode;
|
||||
import jadx.core.dex.info.MethodInfo;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
|
||||
class CallGraphNode implements ICallGraphNode {
|
||||
private final int id;
|
||||
private final MethodInfo mthInfo;
|
||||
private final @Nullable MethodNode mthNode;
|
||||
private final CallGraphAttrNode attrNode;
|
||||
|
||||
public CallGraphNode(int id, MethodInfo mthInfo) {
|
||||
this(id, mthInfo, null);
|
||||
}
|
||||
|
||||
public CallGraphNode(int id, MethodNode mthNode) {
|
||||
this(id, mthNode.getMethodInfo(), mthNode);
|
||||
}
|
||||
|
||||
public CallGraphNode(int id, MethodInfo mthInfo, @Nullable MethodNode mthNode) {
|
||||
this.id = id;
|
||||
this.mthInfo = mthInfo;
|
||||
this.mthNode = mthNode;
|
||||
this.attrNode = new CallGraphAttrNode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MethodInfo getMethodInfo() {
|
||||
return mthInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable MethodNode getMethodNode() {
|
||||
return mthNode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isResolved() {
|
||||
return mthNode != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IAttributeNode attributes() {
|
||||
return attrNode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return mthInfo.getFullId();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package jadx.analysis.callgraph;
|
||||
|
||||
import jadx.analysis.callgraph.api.ICallGraphBuilder;
|
||||
import jadx.api.JadxDecompiler;
|
||||
|
||||
public class JadxCallGraph {
|
||||
|
||||
public static ICallGraphBuilder builder(JadxDecompiler decompiler) {
|
||||
return new CallGraphBuilder(decompiler);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package jadx.analysis.callgraph.api;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
public interface ICallGraph {
|
||||
|
||||
List<ICallGraphEdge> edges();
|
||||
|
||||
void writeDot(Path path);
|
||||
|
||||
void writeJson(Path path);
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package jadx.analysis.callgraph.api;
|
||||
|
||||
public interface ICallGraphBuilder {
|
||||
|
||||
ICallGraphBuilder includePackages(String pkgFilter);
|
||||
|
||||
ICallGraphBuilder resolvedOnly(boolean resolved);
|
||||
|
||||
ICallGraph build();
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package jadx.analysis.callgraph.api;
|
||||
|
||||
import jadx.core.dex.attributes.IAttributeNode;
|
||||
|
||||
public interface ICallGraphEdge {
|
||||
|
||||
ICallGraphNode from();
|
||||
|
||||
ICallGraphNode to();
|
||||
|
||||
boolean isResolved();
|
||||
|
||||
IAttributeNode attributes();
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package jadx.analysis.callgraph.api;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.core.dex.attributes.IAttributeNode;
|
||||
import jadx.core.dex.info.MethodInfo;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
|
||||
public interface ICallGraphNode {
|
||||
|
||||
int getId();
|
||||
|
||||
MethodInfo getMethodInfo();
|
||||
|
||||
@Nullable
|
||||
MethodNode getMethodNode();
|
||||
|
||||
boolean isResolved();
|
||||
|
||||
IAttributeNode attributes();
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package jadx.analysis.callgraph.test;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import jadx.analysis.callgraph.CallGraphExportDot;
|
||||
import jadx.analysis.callgraph.CallGraphExportJson;
|
||||
import jadx.analysis.callgraph.JadxCallGraph;
|
||||
import jadx.analysis.callgraph.api.ICallGraph;
|
||||
import jadx.analysis.callgraph.api.ICallGraphEdge;
|
||||
import jadx.api.JadxArgs;
|
||||
import jadx.api.JadxDecompiler;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class JadxCallGraphTest {
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
void usageExample() {
|
||||
JadxArgs args = new JadxArgs();
|
||||
args.addInputFile(new File("input.apk"));
|
||||
try (JadxDecompiler jadx = new JadxDecompiler(args)) {
|
||||
jadx.load();
|
||||
|
||||
ICallGraph callGraph = JadxCallGraph.builder(jadx)
|
||||
.includePackages("com.example")
|
||||
.resolvedOnly(false)
|
||||
.build();
|
||||
|
||||
for (ICallGraphEdge edge : callGraph.edges()) {
|
||||
if (edge.isResolved()) {
|
||||
System.out.printf("Edge from '%s' to '%s'%n", edge.from(), edge.to());
|
||||
}
|
||||
}
|
||||
callGraph.writeDot(Path.of("test.dot"));
|
||||
callGraph.writeJson(Path.of("test.json"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void simpleTest() {
|
||||
JadxArgs args = new JadxArgs();
|
||||
args.addInputFile(getSampleFile("simple.smali"));
|
||||
try (JadxDecompiler jadx = new JadxDecompiler(args)) {
|
||||
jadx.load();
|
||||
|
||||
ICallGraph callGraph = JadxCallGraph.builder(jadx)
|
||||
.includePackages("test.pkg")
|
||||
.resolvedOnly(false)
|
||||
.build();
|
||||
|
||||
assertThat(callGraph.edges()).hasSize(1);
|
||||
|
||||
for (ICallGraphEdge edge : callGraph.edges()) {
|
||||
System.out.println("Edge from " + edge.from() + " to " + edge.to());
|
||||
}
|
||||
|
||||
String dotStr = new CallGraphExportDot(jadx.getArgs(), callGraph).writeToString();
|
||||
System.out.println("dot: " + dotStr);
|
||||
|
||||
String jsonStr = new CallGraphExportJson(callGraph).writeToString();
|
||||
System.out.println("json: " + jsonStr);
|
||||
|
||||
callGraph.writeDot(tempDir.resolve("test.dot"));
|
||||
callGraph.writeJson(tempDir.resolve("test.json"));
|
||||
}
|
||||
}
|
||||
|
||||
private File getSampleFile(String sampleName) {
|
||||
try {
|
||||
URL resource = getClass().getResource("/samples/" + sampleName);
|
||||
assertThat(resource).describedAs("Sample not found: %s", sampleName).isNotNull();
|
||||
return new File(resource.toURI().toURL().getFile());
|
||||
} catch (MalformedURLException | URISyntaxException e) {
|
||||
throw new RuntimeException("Failed to load sample file: " + sampleName, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
.class Ltest/pkg/HelloWorld;
|
||||
.super Ljava/lang/Object;
|
||||
.source "HelloWorld.java"
|
||||
|
||||
.method public static main([Ljava/lang/String;)V
|
||||
.registers 2
|
||||
|
||||
const-string v0, "Hello, World"
|
||||
invoke-static {p0, v0}, Ltest/pkg/HelloWorld;->hello(Ljava/lang/String;)V
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public static hello(Ljava/lang/String;)V
|
||||
.registers 2
|
||||
|
||||
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
|
||||
invoke-virtual {v0, p0}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
|
||||
return-void
|
||||
.end method
|
||||
@@ -156,6 +156,10 @@ public class UsageInfo implements IUsageInfoData {
|
||||
* Add method usage: {@code useMth} occurrence found in {@code mth} code
|
||||
*/
|
||||
public void unresolvedMethodUse(MethodNode mth, MethodInfo useMth) {
|
||||
if (useMth.getRawFullId().equals("java.lang.Object.<init>()V")) {
|
||||
// ignore default object constructor (called in every constructor)
|
||||
return;
|
||||
}
|
||||
unresolvedMthUsage.add(mth, useMth);
|
||||
}
|
||||
|
||||
|
||||
@@ -428,18 +428,18 @@ public class DotGraphUtils {
|
||||
}
|
||||
}
|
||||
|
||||
private String escape(Object obj) {
|
||||
public static String escape(Object obj) {
|
||||
if (obj == null) {
|
||||
return "null";
|
||||
}
|
||||
return escape(obj.toString());
|
||||
}
|
||||
|
||||
private String escape(String string) {
|
||||
public static String escape(String string) {
|
||||
return escape(string, NLQR);
|
||||
}
|
||||
|
||||
private String escape(String string, String newline) {
|
||||
public static String escape(String string, String newline) {
|
||||
return string
|
||||
.replace("\\", "") // TODO replace \"
|
||||
.replace("/", "\\/")
|
||||
|
||||
@@ -16,6 +16,7 @@ include("jadx-plugins-tools")
|
||||
|
||||
include("jadx-commons:jadx-app-commons")
|
||||
include("jadx-commons:jadx-zip")
|
||||
include("jadx-commons:jadx-analysis")
|
||||
|
||||
include("jadx-plugins:jadx-input-api")
|
||||
include("jadx-plugins:jadx-dex-input")
|
||||
|
||||
Reference in New Issue
Block a user