feat: new module/library for work with call graphs (#2890)
This commit is contained in:
+2
-1
@@ -37,7 +37,8 @@ jadx-output/
|
|||||||
*.log
|
*.log
|
||||||
*.cfg
|
*.cfg
|
||||||
*.orig
|
*.orig
|
||||||
quark.json
|
*.json
|
||||||
|
*.dot
|
||||||
|
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ options:
|
|||||||
--fs-case-sensitive - treat filesystem as case sensitive, false by default
|
--fs-case-sensitive - treat filesystem as case sensitive, false by default
|
||||||
--cfg - save methods control flow graph to dot file
|
--cfg - save methods control flow graph to dot file
|
||||||
--raw-cfg - save methods control flow graph (use raw instructions)
|
--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)
|
-f, --fallback - set '--decompilation-mode' to 'fallback' (deprecated)
|
||||||
--use-dx - use dx/d8 to convert java bytecode
|
--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
|
--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-core"))
|
||||||
implementation(project(":jadx-plugins-tools"))
|
implementation(project(":jadx-plugins-tools"))
|
||||||
implementation(project(":jadx-commons:jadx-app-commons"))
|
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-dex-input"))
|
||||||
runtimeOnly(project(":jadx-plugins:jadx-java-input"))
|
runtimeOnly(project(":jadx-plugins:jadx-java-input"))
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package jadx.cli;
|
package jadx.cli;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import jadx.analysis.callgraph.JadxCallGraph;
|
||||||
|
import jadx.analysis.callgraph.api.ICallGraph;
|
||||||
import jadx.api.JadxArgs;
|
import jadx.api.JadxArgs;
|
||||||
import jadx.api.JadxDecompiler;
|
import jadx.api.JadxDecompiler;
|
||||||
import jadx.api.impl.AnnotatedCodeWriter;
|
import jadx.api.impl.AnnotatedCodeWriter;
|
||||||
@@ -16,6 +19,7 @@ import jadx.cli.LogHelper.LogLevelEnum;
|
|||||||
import jadx.cli.config.JadxConfigAdapter;
|
import jadx.cli.config.JadxConfigAdapter;
|
||||||
import jadx.cli.plugins.JadxFilesGetter;
|
import jadx.cli.plugins.JadxFilesGetter;
|
||||||
import jadx.core.utils.exceptions.JadxArgsValidateException;
|
import jadx.core.utils.exceptions.JadxArgsValidateException;
|
||||||
|
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||||
import jadx.plugins.tools.JadxExternalPluginsLoader;
|
import jadx.plugins.tools.JadxExternalPluginsLoader;
|
||||||
|
|
||||||
public class JadxCLI {
|
public class JadxCLI {
|
||||||
@@ -73,6 +77,7 @@ public class JadxCLI {
|
|||||||
if (checkForErrors(jadx)) {
|
if (checkForErrors(jadx)) {
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
writeCallGraph(jadx, cliArgs);
|
||||||
if (!SingleClassMode.process(jadx, cliArgs)) {
|
if (!SingleClassMode.process(jadx, cliArgs)) {
|
||||||
save(jadx);
|
save(jadx);
|
||||||
}
|
}
|
||||||
@@ -131,4 +136,29 @@ public class JadxCLI {
|
|||||||
System.out.print(" \r");
|
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)")
|
@Parameter(names = { "--raw-cfg" }, description = "save methods control flow graph (use raw instructions)")
|
||||||
protected boolean rawCfgOutput = false;
|
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)")
|
@Parameter(names = { "-f", "--fallback" }, description = "set '--decompilation-mode' to 'fallback' (deprecated)")
|
||||||
protected boolean fallbackMode = false;
|
protected boolean fallbackMode = false;
|
||||||
|
|
||||||
@@ -827,6 +834,14 @@ public class JadxCLIArgs implements IJadxConfig {
|
|||||||
this.rawCfgOutput = rawCfgOutput;
|
this.rawCfgOutput = rawCfgOutput;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CallGraphSaveMode getCallGraphSaveMode() {
|
||||||
|
return callGraphSaveMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCallGraphSaveMode(CallGraphSaveMode callGraphSaveMode) {
|
||||||
|
this.callGraphSaveMode = callGraphSaveMode;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isReplaceConsts() {
|
public boolean isReplaceConsts() {
|
||||||
return replaceConsts;
|
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> {
|
public abstract static class BaseEnumConverter<E extends Enum<E>> implements IStringConverter<E> {
|
||||||
private final Function<String, E> parse;
|
private final Function<String, E> parse;
|
||||||
private final Supplier<E[]> values;
|
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
|
* Add method usage: {@code useMth} occurrence found in {@code mth} code
|
||||||
*/
|
*/
|
||||||
public void unresolvedMethodUse(MethodNode mth, MethodInfo useMth) {
|
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);
|
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) {
|
if (obj == null) {
|
||||||
return "null";
|
return "null";
|
||||||
}
|
}
|
||||||
return escape(obj.toString());
|
return escape(obj.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private String escape(String string) {
|
public static String escape(String string) {
|
||||||
return escape(string, NLQR);
|
return escape(string, NLQR);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String escape(String string, String newline) {
|
public static String escape(String string, String newline) {
|
||||||
return string
|
return string
|
||||||
.replace("\\", "") // TODO replace \"
|
.replace("\\", "") // TODO replace \"
|
||||||
.replace("/", "\\/")
|
.replace("/", "\\/")
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ include("jadx-plugins-tools")
|
|||||||
|
|
||||||
include("jadx-commons:jadx-app-commons")
|
include("jadx-commons:jadx-app-commons")
|
||||||
include("jadx-commons:jadx-zip")
|
include("jadx-commons:jadx-zip")
|
||||||
|
include("jadx-commons:jadx-analysis")
|
||||||
|
|
||||||
include("jadx-plugins:jadx-input-api")
|
include("jadx-plugins:jadx-input-api")
|
||||||
include("jadx-plugins:jadx-dex-input")
|
include("jadx-plugins:jadx-dex-input")
|
||||||
|
|||||||
Reference in New Issue
Block a user