diff --git a/.gitignore b/.gitignore index 122e32c24..4a75acf24 100644 --- a/.gitignore +++ b/.gitignore @@ -37,7 +37,8 @@ jadx-output/ *.log *.cfg *.orig -quark.json +*.json +*.dot .env diff --git a/README.md b/README.md index 2b38fe037..2da1c73d2 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/jadx-cli/build.gradle.kts b/jadx-cli/build.gradle.kts index 619e49731..75e075046 100644 --- a/jadx-cli/build.gradle.kts +++ b/jadx-cli/build.gradle.kts @@ -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")) diff --git a/jadx-cli/src/main/java/jadx/cli/JadxCLI.java b/jadx-cli/src/main/java/jadx/cli/JadxCLI.java index edf9462a0..ac31c7575 100644 --- a/jadx-cli/src/main/java/jadx/cli/JadxCLI.java +++ b/jadx-cli/src/main/java/jadx/cli/JadxCLI.java @@ -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()); + } } diff --git a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java index 55e37974e..642cc5a43 100644 --- a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java +++ b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java @@ -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 { + public CallGraphSaveModeConverter() { + super(CallGraphSaveMode::valueOf, CallGraphSaveMode::values); + } + } + public abstract static class BaseEnumConverter> implements IStringConverter { private final Function parse; private final Supplier values; diff --git a/jadx-commons/jadx-analysis/README.md b/jadx-commons/jadx-analysis/README.md new file mode 100644 index 000000000..e48f5d5ea --- /dev/null +++ b/jadx-commons/jadx-analysis/README.md @@ -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 +} +``` diff --git a/jadx-commons/jadx-analysis/build.gradle.kts b/jadx-commons/jadx-analysis/build.gradle.kts new file mode 100644 index 000000000..3acd5f939 --- /dev/null +++ b/jadx-commons/jadx-analysis/build.gradle.kts @@ -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")) +} diff --git a/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/CallGraph.java b/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/CallGraph.java new file mode 100644 index 000000000..420ae180c --- /dev/null +++ b/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/CallGraph.java @@ -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 edges; + + public CallGraph(JadxArgs args, List edges) { + this.args = args; + this.edges = edges; + } + + @Override + public List 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); + } +} diff --git a/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/CallGraphAttrNode.java b/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/CallGraphAttrNode.java new file mode 100644 index 000000000..7056cc1fd --- /dev/null +++ b/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/CallGraphAttrNode.java @@ -0,0 +1,6 @@ +package jadx.analysis.callgraph; + +import jadx.core.dex.attributes.AttrNode; + +class CallGraphAttrNode extends AttrNode { +} diff --git a/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/CallGraphBuilder.java b/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/CallGraphBuilder.java new file mode 100644 index 000000000..f2b38452e --- /dev/null +++ b/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/CallGraphBuilder.java @@ -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 collectEdges() { + AtomicInteger nodeId = new AtomicInteger(); + Map nodes = new HashMap<>(); + List 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 nodes, AtomicInteger nodeId) { + return nodes.computeIfAbsent(mth.getMethodInfo(), i -> new CallGraphNode(nodeId.incrementAndGet(), mth)); + } + + private static CallGraphNode getCallGraphNode(MethodInfo mth, Map nodes, AtomicInteger nodeId) { + return nodes.computeIfAbsent(mth, i -> new CallGraphNode(nodeId.incrementAndGet(), mth)); + } +} diff --git a/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/CallGraphEdge.java b/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/CallGraphEdge.java new file mode 100644 index 000000000..636942bc3 --- /dev/null +++ b/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/CallGraphEdge.java @@ -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 + '}'; + } +} diff --git a/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/CallGraphExportDot.java b/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/CallGraphExportDot.java new file mode 100644 index 000000000..1359bf350 --- /dev/null +++ b/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/CallGraphExportDot.java @@ -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 nodeMap = new HashMap<>(); + for (ICallGraphEdge edge : callGraph.edges()) { + addNode(edge.from(), nodeMap); + addNode(edge.to(), nodeMap); + } + List 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 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; + } +} diff --git a/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/CallGraphExportJson.java b/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/CallGraphExportJson.java new file mode 100644 index 000000000..74a9fb6fe --- /dev/null +++ b/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/CallGraphExportJson.java @@ -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 edges = new ArrayList<>(); + Map 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 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 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 nodes; + List edges; + } + + static final class Node { + int id; + String method; + } + + static final class Edge { + int from; + int to; + boolean resolved; + } +} diff --git a/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/CallGraphNode.java b/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/CallGraphNode.java new file mode 100644 index 000000000..ee7536748 --- /dev/null +++ b/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/CallGraphNode.java @@ -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(); + } +} diff --git a/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/JadxCallGraph.java b/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/JadxCallGraph.java new file mode 100644 index 000000000..d70bb650c --- /dev/null +++ b/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/JadxCallGraph.java @@ -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); + } +} diff --git a/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/api/ICallGraph.java b/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/api/ICallGraph.java new file mode 100644 index 000000000..093586d7e --- /dev/null +++ b/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/api/ICallGraph.java @@ -0,0 +1,13 @@ +package jadx.analysis.callgraph.api; + +import java.nio.file.Path; +import java.util.List; + +public interface ICallGraph { + + List edges(); + + void writeDot(Path path); + + void writeJson(Path path); +} diff --git a/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/api/ICallGraphBuilder.java b/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/api/ICallGraphBuilder.java new file mode 100644 index 000000000..ff1068784 --- /dev/null +++ b/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/api/ICallGraphBuilder.java @@ -0,0 +1,10 @@ +package jadx.analysis.callgraph.api; + +public interface ICallGraphBuilder { + + ICallGraphBuilder includePackages(String pkgFilter); + + ICallGraphBuilder resolvedOnly(boolean resolved); + + ICallGraph build(); +} diff --git a/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/api/ICallGraphEdge.java b/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/api/ICallGraphEdge.java new file mode 100644 index 000000000..7d8d460b6 --- /dev/null +++ b/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/api/ICallGraphEdge.java @@ -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(); +} diff --git a/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/api/ICallGraphNode.java b/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/api/ICallGraphNode.java new file mode 100644 index 000000000..d6b603aed --- /dev/null +++ b/jadx-commons/jadx-analysis/src/main/java/jadx/analysis/callgraph/api/ICallGraphNode.java @@ -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(); +} diff --git a/jadx-commons/jadx-analysis/src/test/java/jadx/analysis/callgraph/test/JadxCallGraphTest.java b/jadx-commons/jadx-analysis/src/test/java/jadx/analysis/callgraph/test/JadxCallGraphTest.java new file mode 100644 index 000000000..01ec5095b --- /dev/null +++ b/jadx-commons/jadx-analysis/src/test/java/jadx/analysis/callgraph/test/JadxCallGraphTest.java @@ -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); + } + } +} diff --git a/jadx-commons/jadx-analysis/src/test/resources/samples/simple.smali b/jadx-commons/jadx-analysis/src/test/resources/samples/simple.smali new file mode 100644 index 000000000..33220f917 --- /dev/null +++ b/jadx-commons/jadx-analysis/src/test/resources/samples/simple.smali @@ -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 diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UsageInfo.java b/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UsageInfo.java index 1b0c24685..c87272bbf 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UsageInfo.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UsageInfo.java @@ -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.()V")) { + // ignore default object constructor (called in every constructor) + return; + } unresolvedMthUsage.add(mth, useMth); } 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 24bf95ca5..a7a8b2a82 100644 --- a/jadx-core/src/main/java/jadx/core/utils/DotGraphUtils.java +++ b/jadx-core/src/main/java/jadx/core/utils/DotGraphUtils.java @@ -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("/", "\\/") diff --git a/settings.gradle.kts b/settings.gradle.kts index 207d3c8f4..09db33b1b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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")