Compare commits

..

56 Commits

Author SHA1 Message Date
Skylot 158fc2fca3 chore: update raung version 2023-02-18 15:46:08 +00:00
Skylot 24284a6f3a fix: process manifest before other resources (#1740) 2023-02-17 17:54:07 +00:00
Skylot 85c2c63aa3 fix: output unknown invoke-custom as polymorphic call (#1760) 2023-02-11 16:06:30 +00:00
Skylot f354f7de63 fix(gui): split tabs loading to prevent ui dead lock 2023-02-11 14:01:31 +00:00
Skylot 540c0a8100 feat: support polymorphic invoke (#384)(#1777) 2023-02-03 16:28:24 +00:00
Skylot 4d00fede56 fix: resolve JavaNode caching issues (#1775) 2023-02-02 19:39:42 +00:00
Skylot b1bc5c08ff chore: update dependencies 2023-02-02 15:23:54 +00:00
Ran Naor 305d4f4fe5 fix(gui): print the renamed function name in a frida snippet log (#1772)(PR #1773)
* frida snippet log now prints the correct method name if the method was renamed
* fixed spotless check in frida snippet
* get the renamed method name from the alias proprety and changed to format string to fit frida-trace style
* fixed import order to fix gradle spotless warning
2023-01-28 17:59:42 +00:00
Ran Naor 2d149e9a5d fix(gui): allow html in JVariable to render renaming of variables correctly (#1769)(PR #1770) 2023-01-27 17:03:43 +00:00
Ran Naor 87b9ff3c35 feat(gui): added keyboard shortcut ctrl+w to close tab (#1765)(PR #1766) 2023-01-21 17:22:36 +00:00
Zach Snell 1c36b3c74c fix(gui): quick fix for duplicate/overlapping logcat windows (#1752)(PR #1761)
* Simple fix to duplicate/overlapping logcat windows. Could be improved by not re-creating UI every time
* Apply suggestions from code review
* Another try to fix trailing spaces using GitHub suggestions

Co-authored-by: Zach Snell <zach.snell@bivalogic.com>
Co-authored-by: skylot <118523+skylot@users.noreply.github.com>
2023-01-21 17:07:27 +00:00
Skylot 068e4b8e3d fix: allow altMetafactory method in lambda call site (#1760) 2023-01-15 16:08:34 +00:00
Skylot df38a6424f fix(gui): make bytecode output closer to smali (#1739) 2022-12-25 18:53:25 +00:00
Skylot 5d186e56a5 chore: update dependencies 2022-12-25 18:53:25 +00:00
Skylot 0fafcfa006 fix(gui): improve smali disasm method param write (#1739) 2022-12-13 17:40:00 +00:00
jmlitfi e3fdbafd86 fix(gui): resolve exception in smali method writer (#1739)(PR #1745)
Co-authored-by: jmlitfi <jeffmlitfi@gmail.com>
2022-12-13 17:39:20 +00:00
bagipro 07c2b14479 fix: escape special characters in AAB resources (PR #1747)
Co-authored-by: bagipro <bugi@macbook-pro-3.local>
2022-12-13 17:34:16 +00:00
Artem Zhiganov cdc844aaf3 feat(gui): add Russian Translation (PR #1744) 2022-12-10 13:11:34 +00:00
Skylot e1b7d361b9 fix: check full signature for search method override (#1743) 2022-12-09 17:13:01 +00:00
Skylot 12ef29bebc chore: update gradle and dependencies 2022-12-09 17:13:01 +00:00
Skylot 22ed241d50 fix(gui): correct html render in comments search results 2022-11-15 13:53:48 +00:00
Shatyuka 28e5a3c5be fix(gui): hi-dpi main window initial size (#1728)(PR #1729) 2022-11-14 18:19:09 +00:00
Skylot bb4d88cc68 fix(gui): add template for constructor and void methods to Frida snippet (#1714) 2022-11-07 19:13:11 +00:00
Mathis Hesse 4aaea2b93f fix(gui): change callMethodName of constructors in Frida action (#1714)(PR #1715)
* Change callMethodName of constructors in Frida action

* Fix format violation in FridaAction

* Fix format violation in FridaAction
2022-11-07 19:11:22 +00:00
Skylot bc8d7c4fc3 feat(gui): add native libs info to summary (#1717) 2022-11-03 19:15:33 +00:00
Skylot 5ea6c46778 fix(gui): show all code sources in summary (remove dex filter) (#1716) 2022-11-03 18:25:22 +00:00
Skylot b28f8ba85b fix(gui): try to handle exception in RSTA.getPreferredSize() (#1712) 2022-10-29 21:17:17 +01:00
Skylot 4db50fb749 fix(gui): correct html disabling in search results 2022-10-27 10:29:50 +01:00
Skylot 1dd0c90a04 build: switch java version to fix jdk install issue 2022-10-26 20:34:22 +01:00
Skylot 2bace2bde2 fix(gui): disable shell folders in file open dialog (#1709) 2022-10-26 20:08:57 +01:00
Skylot 1a9cb832ab feat(gui): add alternative file open dialog (#1709) 2022-10-26 19:58:58 +01:00
Skylot 6844a46c93 fix: disable HTML rendering in labels if not needed 2022-10-20 15:58:23 +01:00
Skylot e9e45707da chore: update dependencies 2022-10-20 14:54:31 +01:00
Skylot b9d02ff4c4 refactor: remove all LinkedList usage 2022-10-12 17:05:08 +01:00
Jan S 29b64300bc fix(gui): multi-threading issue in DebugController fixed (#1701) (PR #1702) 2022-10-11 19:21:06 +01:00
zhongqingsong 777355e86e fix(gui): update Messages_zh_CN.properties (PR #1700)
Add new text about logcat.
2022-10-10 18:57:58 +01:00
Skylot 620a177ce8 fix: restore enum class with custom code in static init (#1699) 2022-10-08 21:54:06 +01:00
Skylot 683c2dfbeb fix: improve ternary inline, resolve more enum cases (#1686) 2022-10-07 15:51:11 +01:00
Skylot 266cbcc6f4 fix(gui): migrate to fixed jdwp library fork (#1471) 2022-10-06 19:47:15 +01:00
Jan S 8a45602ae6 fix: improve logging messages for zip security errors (#750)(PR #1698)
Logging error messages on invalid file-names or path traversal attacks improved
2022-10-06 19:31:42 +01:00
Skylot 711419a797 fix: correct fix for all use places of incompatible primitives (#1688) 2022-10-03 00:11:04 +03:00
Skylot 603f3057eb chore: update dependencies 2022-10-03 00:11:01 +03:00
5idereal fa6fc1f871 fix(gui): update zh-TW translations (PR #1694) 2022-10-02 15:50:11 +01:00
Skylot 49fa320989 fix: handle possible concurrent exception in method codegen (#1685) 2022-09-29 20:28:01 +01:00
Skylot 2f301bf150 fix: don't mark constructor for inline if anonymous class inline is disabled (#1680) 2022-09-25 17:47:53 +01:00
Skylot b4892ce17f build: use fixed java version to build win artifacts 2022-09-23 21:08:16 +01:00
Skylot 151c171616 fix: handle empty block at end of else-if chain (#1674) 2022-09-23 20:40:56 +01:00
Skylot 79477a2de3 fix: don't rename bridged overridden methods (#1672) 2022-09-23 19:16:34 +01:00
dependabot[bot] 78aadda931 build(deps): bump actions/checkout from 2 to 3 (PR #1669)
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-12 18:30:35 +01:00
Jan S b50706505f fix(res): implemented parsing RES_TABLE_TYPE_LIBRARY chunks (#1663)(PR #1664)
* core: Implemented parsing RES_TABLE_TYPE_LIBRARY chunks

* skip unknown data at the end of type chunk
2022-09-10 16:58:26 +01:00
The Cobra Chicken 9114821fb1 feat(debugger): add logcat output (#1411)(PR #1666)
* Adding logcatController class and writing adb / debugger panel information to the controller.

* Finished parsing logcat binary output and writing an arraylist containing all events.

* added highlighting of logcat output based on type.  Added timestamp parsing.

* Updated code to only get new log messages.

* Added additional code for select all

* Completed Check and uncheckall options.

* Changed log highlighting to log color.  Changed from JTextArea to JTextPane. Logcat pane will now autoscroll only if it is already scrolled to the bottom.  Debugger exit will now stop logcat as well.

* Moved labels into NLS rather than using hardcoded strings.

* Implemented the ability to autoselect attached process.  Changed the formatting of logcat messages.

* Moved labels into NLS rather than using hardcoded strings.

* updating to use info getter methods rather than directly accessing variable

* Added Logcat Pause Button

* Added Clear button

* Updated clear icon

* Cleaning warnings

* cleaning

* Changed behavior to only show logcat for debugged process to start with.

* cleaning

* cleaning

* cleaning

* applying spotless

* Fixing bug with switch

* fixed formatting issue

* add missing localization strings

Co-authored-by: green9317 <38409554+green9317@users.noreply.github.com>
Co-authored-by: TheCobraChicken <jeffmlitfi@gmail.com>
Co-authored-by: Skylot <skylot@gmail.com>
2022-09-08 15:18:55 +01:00
Skylot 1195582da8 feat(gui): option for search results count per page (#1652) 2022-09-05 20:07:02 +01:00
Skylot 258987b0ff chore: update dependencies 2022-09-05 20:07:01 +01:00
Guilherme a6a734c70d fix(gui): update pt-BR translation (PR #1655) 2022-09-01 15:51:12 +01:00
Choiman1559 d6c23a2a9b fix(gui): update Korean translation (PR #1650)
* update korean
* Update Messages_ko_KR.properties
* Restore missing empty line

Co-authored-by: skylot <118523+skylot@users.noreply.github.com>
2022-08-20 17:59:37 +01:00
Skylot db028904d7 fix(gui): set legacy sort flag also for launch4j (#1628) 2022-08-20 17:37:26 +01:00
137 changed files with 4192 additions and 1039 deletions
+3 -1
View File
@@ -57,7 +57,9 @@ jobs:
fetch-depth: 0
- name: Set up JDK
uses: oracle-actions/setup-java@v1 # set latest java version by default
uses: oracle-actions/setup-java@v1
with:
release: 17
- name: Print Java version
shell: bash
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
@@ -6,5 +6,5 @@ jobs:
name: "Validation"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1
+10 -10
View File
@@ -1,6 +1,6 @@
plugins {
id 'com.github.ben-manes.versions' version '0.42.0'
id 'com.diffplug.spotless' version '6.9.1'
id 'com.github.ben-manes.versions' version '0.45.0'
id 'com.diffplug.spotless' version '6.13.0'
}
ext.jadxVersion = System.getenv('JADX_VERSION') ?: "dev"
@@ -26,18 +26,18 @@ allprojects {
}
dependencies {
implementation 'org.slf4j:slf4j-api:1.7.36'
compileOnly 'org.jetbrains:annotations:23.0.0'
implementation 'org.slf4j:slf4j-api:2.0.6'
compileOnly 'org.jetbrains:annotations:24.0.0'
testImplementation 'ch.qos.logback:logback-classic:1.2.11'
testImplementation 'ch.qos.logback:logback-classic:1.3.5'
testImplementation 'org.hamcrest:hamcrest-library:2.2'
testImplementation 'org.mockito:mockito-core:4.7.0'
testImplementation 'org.assertj:assertj-core:3.23.1'
testImplementation 'org.mockito:mockito-core:4.10.0'
testImplementation 'org.assertj:assertj-core:3.24.2'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.2'
testCompileOnly 'org.jetbrains:annotations:23.0.0'
testCompileOnly 'org.jetbrains:annotations:24.0.0'
}
test {
+2 -2
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=f6b8596b10cce501591e92f229816aa4046424f3b24d771751b06779d58c8ec4
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
distributionSha256Sum=7ba68c54029790ab444b39d7e293d3236b2632631fb5f2e012bb28b4ff669e4b
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+1 -1
View File
@@ -11,7 +11,7 @@ dependencies {
runtimeOnly(project(':jadx-plugins:jadx-smali-input'))
implementation 'com.beust:jcommander:1.82'
implementation 'ch.qos.logback:logback-classic:1.2.11'
implementation 'ch.qos.logback:logback-classic:1.3.5'
}
application {
+5 -5
View File
@@ -5,11 +5,11 @@ plugins {
dependencies {
api(project(':jadx-plugins:jadx-plugins-api'))
implementation 'com.google.code.gson:gson:2.9.1'
implementation 'com.google.code.gson:gson:2.10.1'
// TODO: move resources decoding to separate plugin module
implementation 'com.android.tools.build:aapt2-proto:7.2.2-7984345'
implementation 'com.google.protobuf:protobuf-java:3.21.5' // forcing latest version
implementation 'com.android.tools.build:aapt2-proto:7.3.1-8691043'
implementation 'com.google.protobuf:protobuf-java:3.21.12' // forcing latest version
testImplementation 'org.apache.commons:commons-lang3:3.12.0'
@@ -19,8 +19,8 @@ dependencies {
testRuntimeOnly(project(':jadx-plugins:jadx-java-input'))
testRuntimeOnly(project(':jadx-plugins:jadx-raung-input'))
testImplementation 'org.eclipse.jdt:ecj:3.30.0'
testImplementation 'tools.profiler:async-profiler:1.8.3'
testImplementation 'org.eclipse.jdt:ecj:3.32.0'
testImplementation 'tools.profiler:async-profiler:2.9'
}
test {
@@ -12,7 +12,6 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
@@ -93,10 +92,6 @@ public final class JadxDecompiler implements Closeable {
private BinaryXMLParser binaryXmlParser;
private ProtoXMLParser protoXmlParser;
private final Map<ClassNode, JavaClass> classesMap = new ConcurrentHashMap<>();
private final Map<MethodNode, JavaMethod> methodsMap = new ConcurrentHashMap<>();
private final Map<FieldNode, JavaField> fieldsMap = new ConcurrentHashMap<>();
private final IDecompileScheduler decompileScheduler = new DecompilerScheduler();
private final List<ILoadResult> customLoads = new ArrayList<>();
@@ -155,10 +150,6 @@ public final class JadxDecompiler implements Closeable {
resources = null;
binaryXmlParser = null;
protoXmlParser = null;
classesMap.clear();
methodsMap.clear();
fieldsMap.clear();
}
@Override
@@ -318,9 +309,21 @@ public final class JadxDecompiler implements Closeable {
if (args.isSkipFilesSave()) {
return;
}
// process AndroidManifest.xml first to load complete resource ids table
for (ResourceFile resourceFile : getResources()) {
if (resourceFile.getType() == ResourceType.MANIFEST) {
new ResourcesSaver(outDir, resourceFile).run();
}
}
Set<String> inputFileNames = args.getInputFiles().stream().map(File::getAbsolutePath).collect(Collectors.toSet());
for (ResourceFile resourceFile : getResources()) {
if (resourceFile.getType() != ResourceType.ARSC
ResourceType resType = resourceFile.getType();
if (resType == ResourceType.MANIFEST) {
// already processed
continue;
}
if (resType != ResourceType.ARSC
&& inputFileNames.contains(resourceFile.getOriginalName())) {
// ignore resource made from input file
continue;
@@ -391,7 +394,7 @@ public final class JadxDecompiler implements Closeable {
return Utils.collectionMap(root.getClasses(), this::convertClassNode);
}
public List<ResourceFile> getResources() {
public synchronized List<ResourceFile> getResources() {
if (resources == null) {
if (root == null) {
return Collections.emptyList();
@@ -471,33 +474,36 @@ public final class JadxDecompiler implements Closeable {
* Get JavaClass by ClassNode without loading and decompilation
*/
@ApiStatus.Internal
JavaClass convertClassNode(ClassNode cls) {
return classesMap.compute(cls, (node, prevJavaCls) -> {
if (prevJavaCls != null && prevJavaCls.getClassNode() == cls) {
// keep previous variable
return prevJavaCls;
}
if (cls.isInner()) {
return new JavaClass(cls, convertClassNode(cls.getParentClass()));
}
return new JavaClass(cls, this);
});
synchronized JavaClass convertClassNode(ClassNode cls) {
JavaClass javaClass = cls.getJavaNode();
if (javaClass == null) {
javaClass = cls.isInner()
? new JavaClass(cls, convertClassNode(cls.getParentClass()))
: new JavaClass(cls, this);
cls.setJavaNode(javaClass);
}
return javaClass;
}
@ApiStatus.Internal
JavaField convertFieldNode(FieldNode field) {
return fieldsMap.computeIfAbsent(field, fldNode -> {
JavaClass parentCls = convertClassNode(fldNode.getParentClass());
return new JavaField(parentCls, fldNode);
});
synchronized JavaField convertFieldNode(FieldNode fld) {
JavaField javaField = fld.getJavaNode();
if (javaField == null) {
JavaClass parentCls = convertClassNode(fld.getParentClass());
javaField = new JavaField(parentCls, fld);
fld.setJavaNode(javaField);
}
return javaField;
}
@ApiStatus.Internal
JavaMethod convertMethodNode(MethodNode method) {
return methodsMap.computeIfAbsent(method, mthNode -> {
ClassNode parentCls = mthNode.getParentClass();
return new JavaMethod(convertClassNode(parentCls), mthNode);
});
synchronized JavaMethod convertMethodNode(MethodNode mth) {
JavaMethod javaMethod = mth.getJavaNode();
if (javaMethod == null) {
javaMethod = new JavaMethod(convertClassNode(mth.getParentClass()), mth);
mth.setJavaNode(javaMethod);
}
return javaMethod;
}
@Nullable
@@ -574,14 +580,9 @@ public final class JadxDecompiler implements Closeable {
}
}
@Nullable
private JavaVariable resolveVarNode(VarNode varNode) {
MethodNode mthNode = varNode.getMth();
JavaMethod mth = convertMethodNode(mthNode);
if (mth == null) {
return null;
}
return new JavaVariable(mth, varNode);
JavaMethod javaNode = convertMethodNode(varNode.getMth());
return new JavaVariable(javaNode, varNode);
}
@Nullable
@@ -171,7 +171,7 @@ public class ClassGen {
ArgType sup = cls.getSuperClass();
if (sup != null
&& !sup.equals(ArgType.OBJECT)
&& !cls.isEnum()) {
&& !cls.contains(AFlag.REMOVE_SUPER_CLASS)) {
clsCode.add("extends ");
useClass(clsCode, sup);
clsCode.add(' ');
@@ -322,19 +322,25 @@ public class ClassGen {
if (inlineAttr == null || inlineAttr.notNeeded()) {
return false;
}
if (mth.getUseIn().isEmpty()) {
mth.add(AFlag.DONT_GENERATE);
return true;
try {
if (mth.getUseIn().isEmpty()) {
mth.add(AFlag.DONT_GENERATE);
return true;
}
List<MethodNode> useInCompleted = mth.getUseIn().stream()
.filter(m -> m.getTopParentClass().getState().isProcessComplete())
.collect(Collectors.toList());
if (useInCompleted.isEmpty()) {
mth.add(AFlag.DONT_GENERATE);
return true;
}
mth.addDebugComment("Method not inlined, still used in: " + useInCompleted);
return false;
} catch (Exception e) {
// check failed => keep method
mth.addWarnComment("Failed to check method usage", e);
return false;
}
List<MethodNode> useInCompleted = mth.getUseIn().stream()
.filter(m -> m.getTopParentClass().getState().isProcessComplete())
.collect(Collectors.toList());
if (useInCompleted.isEmpty()) {
mth.add(AFlag.DONT_GENERATE);
return true;
}
mth.addDebugComment("Method not inlined, still used in: " + useInCompleted);
return false;
}
private boolean isMethodsPresents() {
@@ -1,7 +1,7 @@
package jadx.core.codegen;
import java.util.ArrayDeque;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Queue;
import jadx.api.ICodeWriter;
@@ -23,7 +23,7 @@ import jadx.core.utils.exceptions.JadxRuntimeException;
public class ConditionGen extends InsnGen {
private static class CondStack {
private final Queue<IfCondition> stack = new LinkedList<>();
private final Queue<IfCondition> stack = new ArrayDeque<>();
public Queue<IfCondition> getStack() {
return stack;
@@ -14,6 +14,7 @@ import jadx.api.ICodeWriter;
import jadx.api.metadata.annotations.InsnCodeOffset;
import jadx.api.metadata.annotations.VarNode;
import jadx.api.plugins.input.data.MethodHandleType;
import jadx.api.plugins.input.data.annotations.EncodedValue;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.FieldReplaceAttr;
@@ -36,6 +37,7 @@ import jadx.core.dex.instructions.IfNode;
import jadx.core.dex.instructions.IndexInsnNode;
import jadx.core.dex.instructions.InsnType;
import jadx.core.dex.instructions.InvokeCustomNode;
import jadx.core.dex.instructions.InvokeCustomRawNode;
import jadx.core.dex.instructions.InvokeNode;
import jadx.core.dex.instructions.InvokeType;
import jadx.core.dex.instructions.NewArrayNode;
@@ -795,11 +797,23 @@ public class InsnGen {
MethodInfo callMth = insn.getCallMth();
MethodNode callMthNode = mth.root().resolveMethod(callMth);
if (type == InvokeType.CUSTOM_RAW) {
makeInvokeCustomRaw((InvokeCustomRawNode) insn, callMthNode, code);
return;
}
if (insn.isPolymorphicCall()) {
// add missing cast
code.add('(');
useType(code, callMth.getReturnType());
code.add(") ");
}
int k = 0;
switch (type) {
case DIRECT:
case VIRTUAL:
case INTERFACE:
case POLYMORPHIC:
InsnArg arg = insn.getArg(0);
if (needInvokeArg(arg)) {
addArgDot(code, arg);
@@ -837,6 +851,31 @@ public class InsnGen {
generateMethodArguments(code, insn, k, callMthNode);
}
private void makeInvokeCustomRaw(InvokeCustomRawNode insn,
@Nullable MethodNode callMthNode, ICodeWriter code) throws CodegenException {
if (isFallback()) {
code.add("call_site(");
code.incIndent();
for (EncodedValue value : insn.getCallSiteValues()) {
code.startLine(value.toString());
}
code.decIndent();
code.startLine(").invoke");
generateMethodArguments(code, insn, 0, callMthNode);
} else {
ArgType returnType = insn.getCallMth().getReturnType();
if (!returnType.isVoid()) {
code.add('(');
useType(code, returnType);
code.add(") ");
}
makeInvoke(insn.getResolveInvoke(), code);
code.add(".dynamicInvoker().invoke");
generateMethodArguments(code, insn, 0, callMthNode);
code.add(" /* invoke-custom */");
}
}
// FIXME: add 'this' for equals methods in scope
private boolean needInvokeArg(InsnArg arg) {
if (arg.isAnyThis()) {
@@ -21,6 +21,7 @@ public enum AFlag {
DONT_GENERATE, // process as usual, but don't output to generated code
COMMENT_OUT, // process as usual, but comment insn in generated code
REMOVE, // can be completely removed
REMOVE_SUPER_CLASS, // don't add super class
HIDDEN, // instruction used inside other instruction but not listed in args
@@ -1,6 +1,6 @@
package jadx.core.dex.attributes.nodes;
import java.util.LinkedList;
import java.util.ArrayList;
import java.util.List;
import jadx.api.ICodeWriter;
@@ -10,7 +10,7 @@ import jadx.core.dex.instructions.PhiInsn;
public class PhiListAttr implements IJadxAttribute {
private final List<PhiInsn> list = new LinkedList<>();
private final List<PhiInsn> list = new ArrayList<>();
@Override
public AType<PhiListAttr> getAttrType() {
@@ -1,5 +1,6 @@
package jadx.core.dex.instructions;
import java.util.List;
import java.util.Objects;
import org.jetbrains.annotations.NotNull;
@@ -7,6 +8,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.plugins.input.data.ICodeReader;
import jadx.api.plugins.input.data.IMethodProto;
import jadx.api.plugins.input.data.IMethodRef;
import jadx.api.plugins.input.insns.InsnData;
import jadx.api.plugins.input.insns.custom.IArrayPayload;
@@ -25,6 +27,7 @@ import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.DecodeException;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.input.InsnDataUtils;
@@ -440,6 +443,8 @@ public class InsnDecoder {
return invokeCustom(insn, false);
case INVOKE_SPECIAL:
return invokeSpecial(insn);
case INVOKE_POLYMORPHIC:
return invokePolymorphic(insn, false);
case INVOKE_DIRECT_RANGE:
return invoke(insn, InvokeType.DIRECT, true);
@@ -451,6 +456,8 @@ public class InsnDecoder {
return invoke(insn, InvokeType.VIRTUAL, true);
case INVOKE_CUSTOM_RANGE:
return invokeCustom(insn, true);
case INVOKE_POLYMORPHIC_RANGE:
return invokePolymorphic(insn, true);
case NEW_INSTANCE:
ArgType clsType = ArgType.parse(insn.getIndexAsType());
@@ -581,6 +588,22 @@ public class InsnDecoder {
return InvokeCustomBuilder.build(method, insn, isRange);
}
private InsnNode invokePolymorphic(InsnData insn, boolean isRange) {
IMethodRef mthRef = InsnDataUtils.getMethodRef(insn);
if (mthRef == null) {
throw new JadxRuntimeException("Failed to load method reference for insn: " + insn);
}
MethodInfo callMth = MethodInfo.fromRef(root, mthRef);
IMethodProto proto = insn.getIndexAsProto(insn.getTarget());
// expand call args
List<ArgType> args = Utils.collectionMap(proto.getArgTypes(), ArgType::parse);
ArgType returnType = ArgType.parse(proto.getReturnType());
MethodInfo effectiveCallMth = MethodInfo.fromDetails(root, callMth.getDeclClass(),
callMth.getName(), args, returnType);
return new InvokePolymorphicNode(effectiveCallMth, insn, proto, callMth, isRange);
}
private InsnNode invokeSpecial(InsnData insn) {
IMethodRef mthRef = InsnDataUtils.getMethodRef(insn);
if (mthRef == null) {
@@ -5,10 +5,15 @@ import java.util.List;
import jadx.api.plugins.input.data.ICallSite;
import jadx.api.plugins.input.data.annotations.EncodedValue;
import jadx.api.plugins.input.insns.InsnData;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.JadxError;
import jadx.core.dex.instructions.invokedynamic.CustomLambdaCall;
import jadx.core.dex.instructions.invokedynamic.CustomRawCall;
import jadx.core.dex.instructions.invokedynamic.CustomStringConcat;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.input.InsnDataUtils;
@@ -28,8 +33,16 @@ public class InvokeCustomBuilder {
if (CustomStringConcat.isStringConcat(values)) {
return CustomStringConcat.buildStringConcat(insn, isRange, values);
}
// TODO: output raw dynamic call
throw new JadxRuntimeException("Failed to process invoke-custom instruction: " + callSite);
try {
return CustomRawCall.build(mth, insn, isRange, values);
} catch (Exception e) {
mth.addWarn("Failed to decode invoke-custom: \n" + Utils.listToString(values, "\n")
+ ",\n exception: " + Utils.getStackTrace(e));
InsnNode nop = new InsnNode(InsnType.NOP, 0);
nop.add(AFlag.SYNTHETIC);
nop.addAttr(AType.JADX_ERROR, new JadxError("Failed to decode invoke-custom: " + values, e));
return nop;
}
} catch (Exception e) {
throw new JadxRuntimeException("'invoke-custom' instruction processing error: " + e.getMessage(), e);
}
@@ -0,0 +1,98 @@
package jadx.core.dex.instructions;
import java.util.List;
import org.jetbrains.annotations.Nullable;
import jadx.api.plugins.input.data.annotations.EncodedValue;
import jadx.api.plugins.input.insns.InsnData;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.invokedynamic.CustomRawCall;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.utils.InsnUtils;
import jadx.core.utils.Utils;
/**
* Information for raw invoke-custom instruction.<br>
* Output will be formatted as polymorphic call with equivalent semantic
* Contains two parts:
* - resolve: treated as additional invoke insn (uses only constant args)
* - invoke: call of resolved method (base for this invoke)
* <br>
* See {@link CustomRawCall} class for build details
*/
public class InvokeCustomRawNode extends InvokeNode {
private final InvokeNode resolve;
private List<EncodedValue> callSiteValues;
public InvokeCustomRawNode(InvokeNode resolve, MethodInfo mthInfo, InsnData insn, boolean isRange) {
super(mthInfo, insn, InvokeType.CUSTOM_RAW, false, isRange);
this.resolve = resolve;
}
public InvokeCustomRawNode(InvokeNode resolve, MethodInfo mthInfo, InvokeType invokeType, int argsCount) {
super(mthInfo, invokeType, argsCount);
this.resolve = resolve;
}
public InvokeNode getResolveInvoke() {
return resolve;
}
public void setCallSiteValues(List<EncodedValue> callSiteValues) {
this.callSiteValues = callSiteValues;
}
public List<EncodedValue> getCallSiteValues() {
return callSiteValues;
}
@Override
public InsnNode copy() {
InvokeCustomRawNode copy = new InvokeCustomRawNode(resolve, getCallMth(), getInvokeType(), getArgsCount());
copyCommonParams(copy);
copy.setCallSiteValues(callSiteValues);
return copy;
}
@Override
public boolean isStaticCall() {
return true;
}
@Override
public int getFirstArgOffset() {
return 0;
}
@Override
public @Nullable InsnArg getInstanceArg() {
return null;
}
@Override
public boolean isSame(InsnNode obj) {
if (this == obj) {
return true;
}
if (obj instanceof InvokeCustomRawNode) {
return super.isSame(obj) && resolve.isSame(((InvokeCustomRawNode) obj).resolve);
}
return false;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(InsnUtils.formatOffset(offset)).append(": INVOKE_CUSTOM ");
if (getResult() != null) {
sb.append(getResult()).append(" = ");
}
if (!appendArgs(sb)) {
sb.append('\n');
}
sb.append(" call-site: \n ").append(Utils.listToString(callSiteValues, "\n ")).append('\n');
return sb.toString();
}
}
@@ -67,6 +67,19 @@ public class InvokeNode extends BaseInvokeNode {
return type == InvokeType.STATIC;
}
public boolean isPolymorphicCall() {
if (type == InvokeType.POLYMORPHIC) {
return true;
}
// java bytecode uses virtual call with modified method info
if (type == InvokeType.VIRTUAL
&& mth.getDeclClass().getFullName().equals("java.lang.invoke.MethodHandle")
&& (mth.getName().equals("invoke") || mth.getName().equals("invokeExact"))) {
return true;
}
return false;
}
public int getFirstArgOffset() {
return type == InvokeType.STATIC ? 0 : 1;
}
@@ -0,0 +1,66 @@
package jadx.core.dex.instructions;
import jadx.api.plugins.input.data.IMethodProto;
import jadx.api.plugins.input.insns.InsnData;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.utils.InsnUtils;
public class InvokePolymorphicNode extends InvokeNode {
private final IMethodProto proto;
private final MethodInfo baseCallRef;
public InvokePolymorphicNode(MethodInfo callMth, InsnData insn, IMethodProto proto, MethodInfo baseRef, boolean isRange) {
super(callMth, insn, InvokeType.POLYMORPHIC, true, isRange);
this.proto = proto;
this.baseCallRef = baseRef;
}
public InvokePolymorphicNode(MethodInfo callMth, int argsCount, IMethodProto proto, MethodInfo baseRef) {
super(callMth, InvokeType.POLYMORPHIC, argsCount);
this.proto = proto;
this.baseCallRef = baseRef;
}
public IMethodProto getProto() {
return proto;
}
public MethodInfo getBaseCallRef() {
return baseCallRef;
}
@Override
public InsnNode copy() {
InvokePolymorphicNode copy = new InvokePolymorphicNode(getCallMth(), getArgsCount(), proto, baseCallRef);
copyCommonParams(copy);
return copy;
}
@Override
public boolean isSame(InsnNode obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof InvokePolymorphicNode) || !super.isSame(obj)) {
return false;
}
InvokePolymorphicNode other = (InvokePolymorphicNode) obj;
return proto.equals(other.proto);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(InsnUtils.formatOffset(offset)).append(": INVOKE_POLYMORPHIC ");
if (getResult() != null) {
sb.append(getResult()).append(" = ");
}
if (!appendArgs(sb)) {
sb.append('\n');
}
sb.append(" base: ").append(baseCallRef).append('\n');
sb.append(" proto: ").append(proto).append('\n');
return sb.toString();
}
}
@@ -8,4 +8,5 @@ public enum InvokeType {
SUPER,
POLYMORPHIC,
CUSTOM,
CUSTOM_RAW,
}
@@ -1,24 +1,26 @@
package jadx.core.dex.instructions.args;
public enum PrimitiveType {
BOOLEAN("Z", "boolean"),
CHAR("C", "char"),
BYTE("B", "byte"),
SHORT("S", "short"),
INT("I", "int"),
FLOAT("F", "float"),
LONG("J", "long"),
DOUBLE("D", "double"),
OBJECT("L", "OBJECT"),
ARRAY("[", "ARRAY"),
VOID("V", "void");
BOOLEAN("Z", "boolean", ArgType.object("java.lang.Boolean")),
CHAR("C", "char", ArgType.object("java.lang.Character")),
BYTE("B", "byte", ArgType.object("java.lang.Byte")),
SHORT("S", "short", ArgType.object("java.lang.Short")),
INT("I", "int", ArgType.object("java.lang.Integer")),
FLOAT("F", "float", ArgType.object("java.lang.Float")),
LONG("J", "long", ArgType.object("java.lang.Long")),
DOUBLE("D", "double", ArgType.object("java.lang.Double")),
OBJECT("L", "OBJECT", ArgType.OBJECT),
ARRAY("[", "ARRAY", ArgType.OBJECT_ARRAY),
VOID("V", "void", ArgType.object("java.lang.Void"));
private final String shortName;
private final String longName;
private final ArgType boxType;
PrimitiveType(String shortName, String longName) {
PrimitiveType(String shortName, String longName, ArgType boxType) {
this.shortName = shortName;
this.longName = longName;
this.boxType = boxType;
}
public String getShortName() {
@@ -29,6 +31,10 @@ public enum PrimitiveType {
return longName;
}
public ArgType getBoxType() {
return boxType;
}
@Override
public String toString() {
return longName;
@@ -8,6 +8,7 @@ import jadx.api.plugins.input.data.IMethodHandle;
import jadx.api.plugins.input.data.IMethodProto;
import jadx.api.plugins.input.data.IMethodRef;
import jadx.api.plugins.input.data.MethodHandleType;
import jadx.api.plugins.input.data.annotations.EncodedType;
import jadx.api.plugins.input.data.annotations.EncodedValue;
import jadx.api.plugins.input.insns.InsnData;
import jadx.core.dex.attributes.AFlag;
@@ -34,18 +35,20 @@ public class CustomLambdaCall {
if (values.size() < 6) {
return false;
}
IMethodHandle methodHandle = (IMethodHandle) values.get(0).getValue();
EncodedValue mthRef = values.get(0);
if (mthRef.getType() != EncodedType.ENCODED_METHOD_HANDLE) {
return false;
}
IMethodHandle methodHandle = (IMethodHandle) mthRef.getValue();
if (methodHandle.getType() != MethodHandleType.INVOKE_STATIC) {
return false;
}
IMethodRef methodRef = methodHandle.getMethodRef();
if (!methodRef.getName().equals("metafactory")) {
return false;
}
if (!methodRef.getParentClassType().equals("Ljava/lang/invoke/LambdaMetafactory;")) {
return false;
}
return true;
String mthName = methodRef.getName();
return mthName.equals("metafactory") || mthName.equals("altMetafactory");
}
public static InvokeCustomNode buildLambdaMethodCall(MethodNode mth, InsnData insn, boolean isRange, List<EncodedValue> values) {
@@ -115,7 +118,7 @@ public class CustomLambdaCall {
@NotNull
private static InvokeNode buildInvokeNode(MethodHandleType methodHandleType, InvokeCustomNode invokeCustomNode,
MethodInfo callMthInfo) {
InvokeType invokeType = convertInvokeType(methodHandleType);
InvokeType invokeType = InvokeCustomUtils.convertInvokeType(methodHandleType);
int callArgsCount = callMthInfo.getArgsCount();
boolean instanceCall = invokeType != InvokeType.STATIC;
if (instanceCall) {
@@ -149,21 +152,4 @@ public class CustomLambdaCall {
}
return invokeNode;
}
private static InvokeType convertInvokeType(MethodHandleType type) {
switch (type) {
case INVOKE_STATIC:
return InvokeType.STATIC;
case INVOKE_INSTANCE:
return InvokeType.VIRTUAL;
case INVOKE_DIRECT:
case INVOKE_CONSTRUCTOR:
return InvokeType.DIRECT;
case INVOKE_INTERFACE:
return InvokeType.INTERFACE;
default:
throw new JadxRuntimeException("Unsupported method handle type: " + type);
}
}
}
@@ -0,0 +1,70 @@
package jadx.core.dex.instructions.invokedynamic;
import java.util.ArrayList;
import java.util.List;
import jadx.api.plugins.input.data.IMethodHandle;
import jadx.api.plugins.input.data.IMethodProto;
import jadx.api.plugins.input.data.annotations.EncodedValue;
import jadx.api.plugins.input.insns.InsnData;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.instructions.ConstStringNode;
import jadx.core.dex.instructions.InvokeCustomRawNode;
import jadx.core.dex.instructions.InvokeNode;
import jadx.core.dex.instructions.InvokeType;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.exceptions.JadxRuntimeException;
import static jadx.core.utils.EncodedValueUtils.buildLookupArg;
import static jadx.core.utils.EncodedValueUtils.convertToInsnArg;
/**
* Show `invoke-custom` similar to polymorphic call
*/
public class CustomRawCall {
public static InsnNode build(MethodNode mth, InsnData insn, boolean isRange, List<EncodedValue> values) {
IMethodHandle resolveHandle = (IMethodHandle) values.get(0).getValue();
String invokeName = (String) values.get(1).getValue();
IMethodProto invokeProto = (IMethodProto) values.get(2).getValue();
List<InsnArg> resolveArgs = buildArgs(mth, values);
if (resolveHandle.getType().isField()) {
throw new JadxRuntimeException("Field handle not yet supported");
}
RootNode root = mth.root();
MethodInfo resolveMth = MethodInfo.fromRef(root, resolveHandle.getMethodRef());
InvokeType resolveInvokeType = InvokeCustomUtils.convertInvokeType(resolveHandle.getType());
InvokeNode resolve = new InvokeNode(resolveMth, resolveInvokeType, resolveArgs.size());
resolveArgs.forEach(resolve::addArg);
ClassInfo invokeCls = ClassInfo.fromType(root, ArgType.OBJECT); // type will be known at runtime
MethodInfo invokeMth = MethodInfo.fromMethodProto(root, invokeCls, invokeName, invokeProto);
InvokeCustomRawNode customRawNode = new InvokeCustomRawNode(resolve, invokeMth, insn, isRange);
customRawNode.setCallSiteValues(values);
return customRawNode;
}
private static List<InsnArg> buildArgs(MethodNode mth, List<EncodedValue> values) {
int valuesCount = values.size();
List<InsnArg> list = new ArrayList<>(valuesCount);
RootNode root = mth.root();
list.add(buildLookupArg(root)); // use `java.lang.invoke.MethodHandles.lookup()` as first arg
for (int i = 1; i < valuesCount; i++) {
EncodedValue value = values.get(i);
try {
list.add(convertToInsnArg(root, value));
} catch (Exception e) {
mth.addWarnComment("Failed to build arg in invoke-custom insn: " + value, e);
list.add(InsnArg.wrapArg(new ConstStringNode(value.toString())));
}
}
return list;
}
}
@@ -0,0 +1,25 @@
package jadx.core.dex.instructions.invokedynamic;
import jadx.api.plugins.input.data.MethodHandleType;
import jadx.core.dex.instructions.InvokeType;
import jadx.core.utils.exceptions.JadxRuntimeException;
public class InvokeCustomUtils {
public static InvokeType convertInvokeType(MethodHandleType type) {
switch (type) {
case INVOKE_STATIC:
return InvokeType.STATIC;
case INVOKE_INSTANCE:
return InvokeType.VIRTUAL;
case INVOKE_DIRECT:
case INVOKE_CONSTRUCTOR:
return InvokeType.DIRECT;
case INVOKE_INTERFACE:
return InvokeType.INTERFACE;
default:
throw new JadxRuntimeException("Unsupported method handle type: " + type);
}
}
}
@@ -21,6 +21,7 @@ import jadx.api.ICodeCache;
import jadx.api.ICodeInfo;
import jadx.api.ICodeWriter;
import jadx.api.JadxArgs;
import jadx.api.JavaClass;
import jadx.api.impl.SimpleCodeInfo;
import jadx.api.plugins.input.data.IClassData;
import jadx.api.plugins.input.data.IFieldData;
@@ -100,6 +101,8 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
// cache maps
private Map<MethodInfo, MethodNode> mthInfoMap = Collections.emptyMap();
private JavaClass javaNode;
public ClassNode(RootNode root, IClassData cls) {
this.root = root;
this.clsInfo = ClassInfo.fromType(root, ArgType.object(cls.getType()));
@@ -805,7 +808,9 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
}
public void addCodegenDep(ClassNode dep) {
this.codegenDeps = ListUtils.safeAdd(this.codegenDeps, dep);
if (!codegenDeps.contains(dep)) {
this.codegenDeps = ListUtils.safeAdd(this.codegenDeps, dep);
}
}
public int getTotalDepsCount() {
@@ -833,6 +838,14 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
return clsData == null ? "synthetic" : clsData.getInputFileName();
}
public JavaClass getJavaNode() {
return javaNode;
}
public void setJavaNode(JavaClass javaNode) {
this.javaNode = javaNode;
}
@Override
public AnnType getAnnType() {
return AnnType.CLASS;
@@ -3,6 +3,7 @@ package jadx.core.dex.nodes;
import java.util.Collections;
import java.util.List;
import jadx.api.JavaField;
import jadx.api.plugins.input.data.IFieldData;
import jadx.core.dex.attributes.nodes.NotificationAttrNode;
import jadx.core.dex.info.AccessInfo;
@@ -21,6 +22,8 @@ public class FieldNode extends NotificationAttrNode implements ICodeNode {
private List<MethodNode> useIn = Collections.emptyList();
private JavaField javaNode;
public static FieldNode build(ClassNode cls, IFieldData fieldData) {
FieldInfo fieldInfo = FieldInfo.fromRef(cls.root(), fieldData);
FieldNode fieldNode = new FieldNode(cls, fieldInfo, fieldData.getAccessFlags());
@@ -112,6 +115,14 @@ public class FieldNode extends NotificationAttrNode implements ICodeNode {
return parentClass.root();
}
public JavaField getJavaNode() {
return javaNode;
}
public void setJavaNode(JavaField javaNode) {
this.javaNode = javaNode;
}
@Override
public AnnType getAnnType() {
return AnnType.FIELD;
@@ -539,19 +539,25 @@ public class InsnNode extends LineAttrNode {
return super.equals(obj);
}
protected void appendArgs(StringBuilder sb) {
/**
* Append arguments type, wrap line if too long
*
* @return true if args wrapped
*/
protected boolean appendArgs(StringBuilder sb) {
if (arguments.isEmpty()) {
return;
return false;
}
String argsStr = Utils.listToString(arguments);
if (argsStr.length() < 120) {
sb.append(argsStr);
} else {
// wrap args
String separator = ICodeWriter.NL + " ";
sb.append(separator).append(Utils.listToString(arguments, separator));
sb.append(ICodeWriter.NL);
return false;
}
// wrap args
String separator = ICodeWriter.NL + " ";
sb.append(separator).append(Utils.listToString(arguments, separator));
sb.append(ICodeWriter.NL);
return true;
}
@Override
@@ -10,6 +10,7 @@ import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.JavaMethod;
import jadx.api.plugins.input.data.ICodeReader;
import jadx.api.plugins.input.data.IDebugInfo;
import jadx.api.plugins.input.data.IMethodData;
@@ -71,6 +72,8 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
private List<MethodNode> useIn = Collections.emptyList();
private JavaMethod javaNode;
public static MethodNode build(ClassNode classNode, IMethodData methodData) {
MethodNode methodNode = new MethodNode(classNode, methodData);
methodNode.addAttrs(methodData.getAttributes());
@@ -610,6 +613,14 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
this.useIn = useIn;
}
public JavaMethod getJavaNode() {
return javaNode;
}
public void setJavaNode(JavaMethod javaNode) {
this.javaNode = javaNode;
}
@Override
public AnnType getAnnType() {
return AnnType.METHOD;
@@ -156,7 +156,7 @@ public final class IfCondition extends AttrNode {
return i;
}
if (c.getOp() == IfOp.EQ && c.getB().isFalse()) {
cond = not(new IfCondition(c.invert()));
cond = new IfCondition(Mode.NOT, Collections.singletonList(new IfCondition(c.invert())));
} else {
c.normalize();
}
@@ -62,45 +62,47 @@ public class ConstInlineVisitor extends AbstractVisitor {
|| insn.getResult() == null) {
return;
}
SSAVar sVar = insn.getResult().getSVar();
InsnArg constArg;
Runnable onSuccess = null;
InsnType insnType = insn.getType();
if (insnType == InsnType.CONST || insnType == InsnType.MOVE) {
constArg = insn.getArg(0);
if (!constArg.isLiteral()) {
switch (insn.getType()) {
case CONST:
case MOVE: {
constArg = insn.getArg(0);
if (!constArg.isLiteral()) {
return;
}
long lit = ((LiteralArg) constArg).getLiteral();
if (lit == 0 && forbidNullInlines(sVar)) {
// all usages forbids inlining
return;
}
break;
}
case CONST_STR: {
String s = ((ConstStringNode) insn).getString();
FieldNode f = mth.getParentClass().getConstField(s);
if (f == null) {
InsnNode copy = insn.copyWithoutResult();
constArg = InsnArg.wrapArg(copy);
} else {
InsnNode constGet = new IndexInsnNode(InsnType.SGET, f.getFieldInfo(), 0);
constArg = InsnArg.wrapArg(constGet);
constArg.setType(ArgType.STRING);
onSuccess = () -> f.addUseIn(mth);
}
break;
}
case CONST_CLASS: {
if (sVar.isUsedInPhi()) {
return;
}
constArg = InsnArg.wrapArg(insn.copyWithoutResult());
constArg.setType(ArgType.CLASS);
break;
}
default:
return;
}
long lit = ((LiteralArg) constArg).getLiteral();
if (lit == 0 && forbidNullInlines(sVar)) {
// all usages forbids inlining
return;
}
} else if (insnType == InsnType.CONST_STR) {
if (sVar.isUsedInPhi()) {
return;
}
String s = ((ConstStringNode) insn).getString();
FieldNode f = mth.getParentClass().getConstField(s);
if (f == null) {
InsnNode copy = insn.copyWithoutResult();
constArg = InsnArg.wrapArg(copy);
} else {
InsnNode constGet = new IndexInsnNode(InsnType.SGET, f.getFieldInfo(), 0);
constArg = InsnArg.wrapArg(constGet);
constArg.setType(ArgType.STRING);
onSuccess = () -> f.addUseIn(mth);
}
} else if (insnType == InsnType.CONST_CLASS) {
if (sVar.isUsedInPhi()) {
return;
}
constArg = InsnArg.wrapArg(insn.copyWithoutResult());
constArg.setType(ArgType.CLASS);
} else {
return;
}
// all check passed, run replace
@@ -37,9 +37,13 @@ import jadx.core.dex.instructions.mods.ConstructorInsn;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.IContainer;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.regions.Region;
import jadx.core.dex.visitors.regions.CheckRegions;
import jadx.core.dex.visitors.regions.IfRegionVisitor;
import jadx.core.dex.visitors.shrink.CodeShrinkVisitor;
import jadx.core.utils.BlockInsnPair;
import jadx.core.utils.BlockUtils;
@@ -47,6 +51,7 @@ import jadx.core.utils.InsnRemover;
import jadx.core.utils.InsnUtils;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxException;
import jadx.core.utils.exceptions.JadxRuntimeException;
import static jadx.core.utils.InsnUtils.checkInsnType;
import static jadx.core.utils.InsnUtils.getSingleArg;
@@ -55,8 +60,16 @@ import static jadx.core.utils.InsnUtils.getWrappedInsn;
@JadxVisitor(
name = "EnumVisitor",
desc = "Restore enum classes",
runAfter = { CodeShrinkVisitor.class, ModVisitor.class, ReSugarCode.class },
runBefore = { ExtractFieldInit.class }
runAfter = {
CodeShrinkVisitor.class, // all possible instructions already inlined
ModVisitor.class,
ReSugarCode.class,
IfRegionVisitor.class, // ternary operator inlined
CheckRegions.class // regions processing finished
},
runBefore = {
ExtractFieldInit.class
}
)
public class EnumVisitor extends AbstractVisitor {
@@ -81,82 +94,67 @@ public class EnumVisitor extends AbstractVisitor {
@Override
public boolean visit(ClassNode cls) throws JadxException {
boolean converted;
try {
converted = convertToEnum(cls);
} catch (Exception e) {
cls.addWarnComment("Enum visitor error", e);
converted = false;
}
if (!converted) {
AccessInfo accessFlags = cls.getAccessFlags();
if (accessFlags.isEnum()) {
cls.setAccessFlags(accessFlags.remove(AccessFlags.ENUM));
cls.addWarnComment("Failed to restore enum class, 'enum' modifier removed");
if (cls.isEnum()) {
boolean converted;
try {
converted = convertToEnum(cls);
} catch (Exception e) {
cls.addWarnComment("Enum visitor error", e);
converted = false;
}
if (!converted) {
AccessInfo accessFlags = cls.getAccessFlags();
if (accessFlags.isEnum()) {
cls.setAccessFlags(accessFlags.remove(AccessFlags.ENUM));
cls.addWarnComment("Failed to restore enum class, 'enum' modifier and super class removed");
}
}
}
return true;
}
private boolean convertToEnum(ClassNode cls) {
if (!cls.isEnum()) {
return false;
ArgType superType = cls.getSuperClass();
if (superType != null && superType.getObject().equals(ArgType.ENUM.getObject())) {
cls.add(AFlag.REMOVE_SUPER_CLASS);
}
MethodNode classInitMth = cls.getClassInitMth();
if (classInitMth == null) {
cls.addWarnComment("Enum class init method not found");
return false;
}
if (classInitMth.getBasicBlocks().isEmpty()) {
Region staticRegion = classInitMth.getRegion();
if (staticRegion == null || classInitMth.getBasicBlocks().isEmpty()) {
return false;
}
ArgType clsType = cls.getClassInfo().getType();
// search "$VALUES" field (holds all enum values)
List<FieldNode> valuesCandidates = cls.getFields().stream()
.filter(f -> f.getAccessFlags().isStatic())
.filter(f -> f.getType().isArray())
.filter(f -> Objects.equals(f.getType().getArrayRootElement(), clsType))
.collect(Collectors.toList());
if (valuesCandidates.isEmpty()) {
return false;
}
if (valuesCandidates.size() > 1) {
valuesCandidates.removeIf(f -> !f.getAccessFlags().isSynthetic());
}
if (valuesCandidates.size() > 1) {
Optional<FieldNode> valuesOpt = valuesCandidates.stream().filter(f -> f.getName().equals("$VALUES")).findAny();
if (valuesOpt.isPresent()) {
valuesCandidates.clear();
valuesCandidates.add(valuesOpt.get());
// collect blocks on linear part of static method (ignore branching on method end)
List<BlockNode> staticBlocks = new ArrayList<>();
for (IContainer subBlock : staticRegion.getSubBlocks()) {
if (subBlock instanceof BlockNode) {
staticBlocks.add((BlockNode) subBlock);
} else {
break;
}
}
if (valuesCandidates.size() != 1) {
cls.addWarnComment("Found several \"values\" enum fields: " + valuesCandidates);
if (staticBlocks.isEmpty()) {
cls.addWarnComment("Unexpected branching in enum static init block");
return false;
}
FieldNode valuesField = valuesCandidates.get(0);
List<InsnNode> toRemove = new ArrayList<>();
// search "$VALUES" array init and collect enum fields
BlockInsnPair valuesInitPair = getValuesInitInsn(classInitMth, valuesField);
if (valuesInitPair == null) {
EnumData data = new EnumData(cls, classInitMth, staticBlocks);
if (!searchValuesField(data)) {
return false;
}
BlockNode staticBlock = valuesInitPair.getBlock();
InsnNode valuesInitInsn = valuesInitPair.getInsn();
List<EnumField> enumFields = null;
InsnArg arrArg = valuesInitInsn.getArg(0);
InsnArg arrArg = data.valuesInitInsn.getArg(0);
if (arrArg.isInsnWrap()) {
InsnNode wrappedInsn = ((InsnWrapArg) arrArg).getWrapInsn();
enumFields = extractEnumFieldsFromInsn(cls, staticBlock, wrappedInsn, toRemove);
enumFields = extractEnumFieldsFromInsn(data, wrappedInsn);
}
if (enumFields == null) {
cls.addWarnComment("Unknown enum class pattern. Please report as an issue!");
return false;
}
toRemove.add(valuesInitInsn);
data.toRemove.add(data.valuesInitInsn);
// all checks complete, perform transform
EnumClassAttr attr = new EnumClassAttr(enumFields);
@@ -176,59 +174,92 @@ public class EnumVisitor extends AbstractVisitor {
fieldNode.getFieldInfo().setAlias(name);
}
fieldNode.add(AFlag.DONT_GENERATE);
processConstructorInsn(cls, enumField, classInitMth, staticBlock, toRemove);
processConstructorInsn(data, enumField, classInitMth);
}
valuesField.add(AFlag.DONT_GENERATE);
InsnRemover.removeAllAndUnbind(classInitMth, staticBlock, toRemove);
data.valuesField.add(AFlag.DONT_GENERATE);
InsnRemover.removeAllAndUnbind(classInitMth, data.toRemove);
if (classInitMth.countInsns() == 0) {
classInitMth.add(AFlag.DONT_GENERATE);
} else if (!toRemove.isEmpty()) {
} else if (!data.toRemove.isEmpty()) {
CodeShrinkVisitor.shrinkMethod(classInitMth);
}
removeEnumMethods(cls, clsType, valuesField);
removeEnumMethods(cls, data.valuesField);
return true;
}
private void processConstructorInsn(ClassNode cls, EnumField enumField, MethodNode classInitMth,
BlockNode staticBlock, List<InsnNode> toRemove) {
ConstructorInsn co = enumField.getConstrInsn();
ClassInfo enumClsInfo = co.getClassType();
if (!enumClsInfo.equals(cls.getClassInfo())) {
ClassNode enumCls = cls.root().resolveClass(enumClsInfo);
if (enumCls != null) {
processEnumCls(cls, enumField, enumCls);
/**
* Search "$VALUES" field (holds all enum values)
*/
private boolean searchValuesField(EnumData data) {
ArgType clsType = data.cls.getClassInfo().getType();
List<FieldNode> valuesCandidates = data.cls.getFields().stream()
.filter(f -> f.getAccessFlags().isStatic())
.filter(f -> f.getType().isArray())
.filter(f -> Objects.equals(f.getType().getArrayRootElement(), clsType))
.collect(Collectors.toList());
if (valuesCandidates.isEmpty()) {
data.cls.addWarnComment("$VALUES field not found");
return false;
}
if (valuesCandidates.size() > 1) {
valuesCandidates.removeIf(f -> !f.getAccessFlags().isSynthetic());
}
if (valuesCandidates.size() > 1) {
Optional<FieldNode> valuesOpt = valuesCandidates.stream().filter(f -> f.getName().equals("$VALUES")).findAny();
if (valuesOpt.isPresent()) {
valuesCandidates.clear();
valuesCandidates.add(valuesOpt.get());
}
}
List<RegisterArg> regs = new ArrayList<>();
co.getRegisterArgs(regs);
if (!regs.isEmpty()) {
cls.addWarnComment("Init of enum " + enumField.getField().getName() + " can be incorrect");
if (valuesCandidates.size() != 1) {
data.cls.addWarnComment("Found several \"values\" enum fields: " + valuesCandidates);
return false;
}
MethodNode ctrMth = cls.root().resolveMethod(co.getCallMth());
data.valuesField = valuesCandidates.get(0);
// search "$VALUES" array init and collect enum fields
BlockInsnPair valuesInitPair = getValuesInitInsn(data);
if (valuesInitPair == null) {
return false;
}
data.valuesInitInsn = valuesInitPair.getInsn();
return true;
}
private void processConstructorInsn(EnumData data, EnumField enumField, MethodNode classInitMth) {
ConstructorInsn co = enumField.getConstrInsn();
ClassInfo enumClsInfo = co.getClassType();
if (!enumClsInfo.equals(data.cls.getClassInfo())) {
ClassNode enumCls = data.cls.root().resolveClass(enumClsInfo);
if (enumCls != null) {
processEnumCls(data.cls, enumField, enumCls);
}
}
MethodNode ctrMth = data.cls.root().resolveMethod(co.getCallMth());
if (ctrMth != null) {
markArgsForSkip(ctrMth);
}
RegisterArg coResArg = co.getResult();
if (coResArg == null || coResArg.getSVar().getUseList().size() <= 2) {
toRemove.add(co);
data.toRemove.add(co);
} else {
// constructor result used in other places -> replace constructor with enum field get (SGET)
IndexInsnNode enumGet = new IndexInsnNode(InsnType.SGET, enumField.getField().getFieldInfo(), 0);
enumGet.setResult(coResArg.duplicate());
BlockUtils.replaceInsn(classInitMth, staticBlock, co, enumGet);
BlockUtils.replaceInsn(classInitMth, co, enumGet);
}
}
@Nullable
private List<EnumField> extractEnumFieldsFromInsn(ClassNode cls, BlockNode staticBlock,
InsnNode wrappedInsn, List<InsnNode> toRemove) {
private List<EnumField> extractEnumFieldsFromInsn(EnumData enumData, InsnNode wrappedInsn) {
switch (wrappedInsn.getType()) {
case FILLED_NEW_ARRAY:
return extractEnumFieldsFromFilledArray(cls, wrappedInsn, staticBlock, toRemove);
return extractEnumFieldsFromFilledArray(enumData, wrappedInsn);
case INVOKE:
// handle redirection of values array fill (added in java 15)
return extractEnumFieldsFromInvoke(cls, staticBlock, (InvokeNode) wrappedInsn, toRemove);
return extractEnumFieldsFromInvoke(enumData, (InvokeNode) wrappedInsn);
case NEW_ARRAY:
InsnArg arg = wrappedInsn.getArg(0);
@@ -243,10 +274,9 @@ public class EnumVisitor extends AbstractVisitor {
}
}
private List<EnumField> extractEnumFieldsFromInvoke(ClassNode cls, BlockNode staticBlock,
InvokeNode invokeNode, List<InsnNode> toRemove) {
private List<EnumField> extractEnumFieldsFromInvoke(EnumData enumData, InvokeNode invokeNode) {
MethodInfo callMth = invokeNode.getCallMth();
MethodNode valuesMth = cls.root().resolveMethod(callMth);
MethodNode valuesMth = enumData.cls.root().resolveMethod(callMth);
if (valuesMth == null || valuesMth.isVoidReturn()) {
return null;
}
@@ -256,16 +286,16 @@ public class EnumVisitor extends AbstractVisitor {
if (wrappedInsn == null) {
return null;
}
List<EnumField> enumFields = extractEnumFieldsFromInsn(cls, staticBlock, wrappedInsn, toRemove);
List<EnumField> enumFields = extractEnumFieldsFromInsn(enumData, wrappedInsn);
if (enumFields != null) {
valuesMth.add(AFlag.DONT_GENERATE);
}
return enumFields;
}
private BlockInsnPair getValuesInitInsn(MethodNode classInitMth, FieldNode valuesField) {
FieldInfo searchField = valuesField.getFieldInfo();
for (BlockNode blockNode : classInitMth.getBasicBlocks()) {
private BlockInsnPair getValuesInitInsn(EnumData data) {
FieldInfo searchField = data.valuesField.getFieldInfo();
for (BlockNode blockNode : data.staticBlocks) {
for (InsnNode insn : blockNode.getInstructions()) {
if (insn.getType() == InsnType.SPUT) {
IndexInsnNode indexInsnNode = (IndexInsnNode) insn;
@@ -279,50 +309,49 @@ public class EnumVisitor extends AbstractVisitor {
return null;
}
private List<EnumField> extractEnumFieldsFromFilledArray(ClassNode cls, InsnNode arrFillInsn, BlockNode staticBlock,
List<InsnNode> toRemove) {
private List<EnumField> extractEnumFieldsFromFilledArray(EnumData enumData, InsnNode arrFillInsn) {
List<EnumField> enumFields = new ArrayList<>();
for (InsnArg arg : arrFillInsn.getArguments()) {
EnumField field = null;
if (arg.isInsnWrap()) {
InsnNode wrappedInsn = ((InsnWrapArg) arg).getWrapInsn();
field = processEnumFieldByWrappedInsn(cls, wrappedInsn, staticBlock, toRemove);
field = processEnumFieldByWrappedInsn(enumData, wrappedInsn);
} else if (arg.isRegister()) {
field = processEnumFieldByRegister(cls, (RegisterArg) arg, staticBlock, toRemove);
field = processEnumFieldByRegister(enumData, (RegisterArg) arg);
}
if (field == null) {
return null;
}
enumFields.add(field);
}
toRemove.add(arrFillInsn);
enumData.toRemove.add(arrFillInsn);
return enumFields;
}
private EnumField processEnumFieldByWrappedInsn(ClassNode cls, InsnNode wrappedInsn, BlockNode staticBlock, List<InsnNode> toRemove) {
private EnumField processEnumFieldByWrappedInsn(EnumData data, InsnNode wrappedInsn) {
if (wrappedInsn.getType() == InsnType.SGET) {
return processEnumFieldByField(cls, wrappedInsn, staticBlock, toRemove);
return processEnumFieldByField(data, wrappedInsn);
}
ConstructorInsn constructorInsn = castConstructorInsn(wrappedInsn);
if (constructorInsn != null) {
FieldNode enumFieldNode = createFakeField(cls, "EF" + constructorInsn.getOffset());
cls.addField(enumFieldNode);
return createEnumFieldByConstructor(cls, enumFieldNode, constructorInsn);
FieldNode enumFieldNode = createFakeField(data.cls, "EF" + constructorInsn.getOffset());
data.cls.addField(enumFieldNode);
return createEnumFieldByConstructor(data.cls, enumFieldNode, constructorInsn);
}
return null;
}
@Nullable
private EnumField processEnumFieldByField(ClassNode cls, InsnNode sgetInsn, BlockNode staticBlock, List<InsnNode> toRemove) {
private EnumField processEnumFieldByField(EnumData data, InsnNode sgetInsn) {
if (sgetInsn.getType() != InsnType.SGET) {
return null;
}
FieldInfo fieldInfo = (FieldInfo) ((IndexInsnNode) sgetInsn).getIndex();
FieldNode enumFieldNode = cls.searchField(fieldInfo);
FieldNode enumFieldNode = data.cls.searchField(fieldInfo);
if (enumFieldNode == null) {
return null;
}
InsnNode sputInsn = searchFieldPutInsn(cls, staticBlock, enumFieldNode);
InsnNode sputInsn = searchFieldPutInsn(data, enumFieldNode);
if (sputInsn == null) {
return null;
}
@@ -333,17 +362,17 @@ public class EnumVisitor extends AbstractVisitor {
}
RegisterArg sgetResult = sgetInsn.getResult();
if (sgetResult == null || sgetResult.getSVar().getUseCount() == 1) {
toRemove.add(sgetInsn);
data.toRemove.add(sgetInsn);
}
toRemove.add(sputInsn);
return createEnumFieldByConstructor(cls, enumFieldNode, co);
data.toRemove.add(sputInsn);
return createEnumFieldByConstructor(data.cls, enumFieldNode, co);
}
@Nullable
private EnumField processEnumFieldByRegister(ClassNode cls, RegisterArg arg, BlockNode staticBlock, List<InsnNode> toRemove) {
private EnumField processEnumFieldByRegister(EnumData data, RegisterArg arg) {
InsnNode assignInsn = arg.getAssignInsn();
if (assignInsn != null && assignInsn.getType() == InsnType.SGET) {
return processEnumFieldByField(cls, assignInsn, staticBlock, toRemove);
return processEnumFieldByField(data, assignInsn);
}
SSAVar ssaVar = arg.getSVar();
@@ -354,12 +383,12 @@ public class EnumVisitor extends AbstractVisitor {
if (constrInsn == null || constrInsn.getType() != InsnType.CONSTRUCTOR) {
return null;
}
FieldNode enumFieldNode = searchEnumField(cls, ssaVar, toRemove);
FieldNode enumFieldNode = searchEnumField(data, ssaVar);
if (enumFieldNode == null) {
enumFieldNode = createFakeField(cls, "EF" + arg.getRegNum());
cls.addField(enumFieldNode);
enumFieldNode = createFakeField(data.cls, "EF" + arg.getRegNum());
data.cls.addField(enumFieldNode);
}
return createEnumFieldByConstructor(cls, enumFieldNode, (ConstructorInsn) constrInsn);
return createEnumFieldByConstructor(data.cls, enumFieldNode, (ConstructorInsn) constrInsn);
}
private FieldNode createFakeField(ClassNode cls, String name) {
@@ -372,17 +401,17 @@ public class EnumVisitor extends AbstractVisitor {
}
@Nullable
private FieldNode searchEnumField(ClassNode cls, SSAVar ssaVar, List<InsnNode> toRemove) {
private FieldNode searchEnumField(EnumData data, SSAVar ssaVar) {
InsnNode sputInsn = ssaVar.getUseList().get(0).getParentInsn();
if (sputInsn == null || sputInsn.getType() != InsnType.SPUT) {
return null;
}
FieldInfo fieldInfo = (FieldInfo) ((IndexInsnNode) sputInsn).getIndex();
FieldNode enumFieldNode = cls.searchField(fieldInfo);
FieldNode enumFieldNode = data.cls.searchField(fieldInfo);
if (enumFieldNode == null) {
return null;
}
toRemove.add(sputInsn);
data.toRemove.add(sputInsn);
return enumFieldNode;
}
@@ -409,24 +438,32 @@ public class EnumVisitor extends AbstractVisitor {
if (ctrMth == null) {
return null;
}
List<RegisterArg> regs = new ArrayList<>();
co.getRegisterArgs(regs);
if (!regs.isEmpty()) {
throw new JadxRuntimeException("Init of enum " + enumFieldNode.getName() + " uses external variables");
}
return new EnumField(enumFieldNode, co);
}
@Nullable
private InsnNode searchFieldPutInsn(ClassNode cls, BlockNode staticBlock, FieldNode enumFieldNode) {
for (InsnNode sputInsn : staticBlock.getInstructions()) {
if (sputInsn != null && sputInsn.getType() == InsnType.SPUT) {
FieldInfo f = (FieldInfo) ((IndexInsnNode) sputInsn).getIndex();
FieldNode fieldNode = cls.searchField(f);
if (Objects.equals(fieldNode, enumFieldNode)) {
return sputInsn;
private InsnNode searchFieldPutInsn(EnumData data, FieldNode enumFieldNode) {
for (BlockNode block : data.staticBlocks) {
for (InsnNode sputInsn : block.getInstructions()) {
if (sputInsn != null && sputInsn.getType() == InsnType.SPUT) {
FieldInfo f = (FieldInfo) ((IndexInsnNode) sputInsn).getIndex();
FieldNode fieldNode = data.cls.searchField(f);
if (Objects.equals(fieldNode, enumFieldNode)) {
return sputInsn;
}
}
}
}
return null;
}
private void removeEnumMethods(ClassNode cls, ArgType clsType, FieldNode valuesField) {
private void removeEnumMethods(ClassNode cls, FieldNode valuesField) {
ArgType clsType = cls.getClassInfo().getType();
String valuesMethodShortId = "values()" + TypeGen.signature(ArgType.array(clsType));
MethodNode valuesMethod = null;
// remove compiler generated methods
@@ -605,4 +642,19 @@ public class EnumVisitor extends AbstractVisitor {
}
return null;
}
private static class EnumData {
final ClassNode cls;
final MethodNode classInitMth;
final List<BlockNode> staticBlocks;
final List<InsnNode> toRemove = new ArrayList<>();
FieldNode valuesField;
InsnNode valuesInitInsn;
public EnumData(ClassNode cls, MethodNode classInitMth, List<BlockNode> staticBlocks) {
this.cls = cls;
this.classInitMth = classInitMth;
this.staticBlocks = staticBlocks;
}
}
}
@@ -90,7 +90,7 @@ public class OverrideMethodVisitor extends AbstractVisitor {
for (ArgType superType : superData.getSuperTypes()) {
ClassNode classNode = mth.root().resolveClass(superType);
if (classNode != null) {
MethodNode ovrdMth = searchOverriddenMethod(classNode, signature);
MethodNode ovrdMth = searchOverriddenMethod(classNode, mth, signature);
if (ovrdMth != null) {
if (isMethodVisibleInCls(ovrdMth, cls)) {
overrideList.add(ovrdMth);
@@ -107,6 +107,8 @@ public class OverrideMethodVisitor extends AbstractVisitor {
Map<String, ClspMethod> methodsMap = clsDetails.getMethodsMap();
for (Map.Entry<String, ClspMethod> entry : methodsMap.entrySet()) {
String mthShortId = entry.getKey();
// do not check full signature, classpath methods can be trusted
// i.e. doesn't contain methods with same signature in one class
if (mthShortId.startsWith(signature)) {
overrideList.add(entry.getValue());
break;
@@ -130,12 +132,30 @@ public class OverrideMethodVisitor extends AbstractVisitor {
}
@Nullable
private MethodNode searchOverriddenMethod(ClassNode cls, String signature) {
private MethodNode searchOverriddenMethod(ClassNode cls, MethodNode mth, String signature) {
// search by exact full signature (with return value) to fight obfuscation (see test
// 'TestOverrideWithSameName')
String shortId = mth.getMethodInfo().getShortId();
for (MethodNode supMth : cls.getMethods()) {
if (!supMth.getAccessFlags().isStatic() && supMth.getMethodInfo().getShortId().startsWith(signature)) {
if (supMth.getMethodInfo().getShortId().equals(shortId) && !supMth.getAccessFlags().isStatic()) {
return supMth;
}
}
// search by signature without return value and check if return value is wider type
for (MethodNode supMth : cls.getMethods()) {
if (supMth.getMethodInfo().getShortId().startsWith(signature) && !supMth.getAccessFlags().isStatic()) {
TypeCompare typeCompare = cls.root().getTypeCompare();
ArgType supRetType = supMth.getMethodInfo().getReturnType();
ArgType mthRetType = mth.getMethodInfo().getReturnType();
TypeCompareEnum res = typeCompare.compareTypes(supRetType, mthRetType);
if (res.isWider()) {
return supMth;
}
if (res == TypeCompareEnum.UNKNOWN || res == TypeCompareEnum.CONFLICT) {
mth.addDebugComment("Possible override for method " + supMth.getMethodInfo().getFullId());
}
}
}
return null;
}
@@ -1,12 +1,12 @@
package jadx.core.dex.visitors;
import jadx.core.Consts;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.visitors.usage.UsageInfoVisitor;
import jadx.core.utils.ListUtils;
import jadx.core.utils.exceptions.JadxException;
@JadxVisitor(
@@ -45,7 +45,14 @@ public class ProcessMethodsForInline extends AbstractVisitor {
}
AccessInfo accessFlags = mth.getAccessFlags();
boolean isSynthetic = accessFlags.isSynthetic() || mth.getName().contains("$");
return isSynthetic && (accessFlags.isStatic() || mth.isConstructor());
return isSynthetic && canInlineMethod(mth, accessFlags);
}
private static boolean canInlineMethod(MethodNode mth, AccessInfo accessFlags) {
if (accessFlags.isStatic()) {
return true;
}
return mth.isConstructor() && mth.root().getArgs().isInlineAnonymousClasses();
}
private static void fixClassDependencies(MethodNode mth) {
@@ -54,8 +61,14 @@ public class ProcessMethodsForInline extends AbstractVisitor {
// remove possible cross dependency
// to force class with inline method to be processed before its usage
ClassNode useTopCls = useInMth.getTopParentClass();
parentClass.setDependencies(ListUtils.safeRemoveAndTrim(parentClass.getDependencies(), useTopCls));
useTopCls.addCodegenDep(parentClass);
if (useTopCls != parentClass) {
parentClass.removeDependency(useTopCls);
useTopCls.addCodegenDep(parentClass);
if (Consts.DEBUG_USAGE) {
parentClass.addDebugComment("Remove dependency: " + useTopCls + " to inline " + mth);
useTopCls.addDebugComment("Add dependency: " + parentClass + " to inline " + mth);
}
}
}
}
}
@@ -2,7 +2,6 @@ package jadx.core.dex.visitors.blocks;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
@@ -432,7 +431,7 @@ public class BlockProcessor extends AbstractVisitor {
BlockNode loopHeader = loop.getStart();
List<BlockNode> preds = loopHeader.getPredecessors();
if (preds.size() > 2) {
List<BlockNode> blocks = new LinkedList<>(preds);
List<BlockNode> blocks = new ArrayList<>(preds);
blocks.removeIf(block -> block.contains(AFlag.LOOP_END));
BlockNode first = blocks.remove(0);
BlockNode preHeader = BlockSplitter.insertBlockBetween(mth, first, loopHeader);
@@ -19,6 +19,10 @@ public class DepthRegionTraversal {
traverseInternal(mth, visitor, mth.getRegion());
}
public static void traverse(MethodNode mth, IContainer container, IRegionVisitor visitor) {
traverseInternal(mth, visitor, container);
}
public static void traverseIterative(MethodNode mth, IRegionIterativeVisitor visitor) {
boolean repeat;
int k = 0;
@@ -3,8 +3,10 @@ package jadx.core.dex.visitors.regions;
import java.util.List;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.instructions.InsnType;
import jadx.core.dex.nodes.IContainer;
import jadx.core.dex.nodes.IRegion;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.regions.Region;
import jadx.core.dex.regions.conditions.IfCondition;
@@ -45,7 +47,7 @@ public class IfRegionVisitor extends AbstractVisitor {
}
}
@SuppressWarnings("UnnecessaryReturnStatement")
@SuppressWarnings({ "UnnecessaryReturnStatement", "StatementWithEmptyBody" })
private static void orderBranches(MethodNode mth, IfRegion ifRegion) {
if (RegionUtils.isEmpty(ifRegion.getElseRegion())) {
return;
@@ -79,9 +81,15 @@ public class IfRegionVisitor extends AbstractVisitor {
return;
}
}
boolean lastRegion = ifRegion == RegionUtils.getLastRegion(mth.getRegion());
boolean lastRegion = RegionUtils.hasExitEdge(ifRegion);
if (elseSize == 1 && lastRegion && mth.isVoidReturn()) {
// single return at method end will be removed later
InsnNode lastElseInsn = RegionUtils.getLastInsn(ifRegion.getElseRegion());
if (lastElseInsn != null && lastElseInsn.getType() == InsnType.THROW) {
// move `throw` into `then` block
invertIfRegion(ifRegion);
} else {
// single return at method end will be removed later
}
return;
}
if (!lastRegion) {
@@ -1,6 +1,6 @@
package jadx.core.dex.visitors.regions;
import java.util.LinkedList;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
@@ -125,7 +125,7 @@ public class LoopRegionVisitor extends AbstractVisitor implements IRegionVisitor
return false;
}
// can't make loop if argument from increment instruction is assign in loop
List<RegisterArg> args = new LinkedList<>();
List<RegisterArg> args = new ArrayList<>();
incrInsn.getRegisterArgs(args);
for (RegisterArg iArg : args) {
try {
@@ -268,7 +268,7 @@ public class LoopRegionVisitor extends AbstractVisitor implements IRegionVisitor
|| !checkInvoke(nextCall, "java.util.Iterator", "next()Ljava/lang/Object;")) {
return false;
}
List<InsnNode> toSkip = new LinkedList<>();
List<InsnNode> toSkip = new ArrayList<>();
RegisterArg iterVar;
if (nextCall.contains(AFlag.WRAPPED)) {
InsnArg wrapArg = BlockUtils.searchWrappedInsnParent(mth, nextCall);
@@ -10,6 +10,7 @@ import jadx.core.dex.instructions.PhiInsn;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.InsnWrapArg;
import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.instructions.args.SSAVar;
import jadx.core.dex.instructions.mods.TernaryInsn;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.IContainer;
@@ -73,11 +74,7 @@ public class TernaryMod extends AbstractRegionVisitor implements IRegionIterativ
return false;
}
if (elseRegion == null) {
if (mth.isConstructor()) {
// force ternary conversion to inline all code in 'super' or 'this' calls
return processOneBranchTernary(mth, ifRegion);
}
return false;
return processOneBranchTernary(mth, ifRegion);
}
BlockNode tb = getTernaryInsnBlock(thenRegion);
BlockNode eb = getTernaryInsnBlock(elseRegion);
@@ -93,21 +90,8 @@ public class TernaryMod extends AbstractRegionVisitor implements IRegionIterativ
InsnNode thenInsn = tb.getInstructions().get(0);
InsnNode elseInsn = eb.getInstructions().get(0);
if (mth.contains(AFlag.USE_LINES_HINTS)
&& thenInsn.getSourceLine() != elseInsn.getSourceLine()) {
if (thenInsn.getSourceLine() != 0 && elseInsn.getSourceLine() != 0) {
// sometimes source lines incorrect
if (!checkLineStats(thenInsn, elseInsn)) {
return false;
}
} else {
// no debug info
if (containsTernary(thenInsn) || containsTernary(elseInsn)) {
// don't make nested ternary by default
// TODO: add addition checks
return false;
}
}
if (!verifyLineHints(mth, thenInsn, elseInsn)) {
return false;
}
RegisterArg thenResArg = thenInsn.getResult();
@@ -184,6 +168,20 @@ public class TernaryMod extends AbstractRegionVisitor implements IRegionIterativ
return false;
}
private static boolean verifyLineHints(MethodNode mth, InsnNode thenInsn, InsnNode elseInsn) {
if (mth.contains(AFlag.USE_LINES_HINTS)
&& thenInsn.getSourceLine() != elseInsn.getSourceLine()) {
if (thenInsn.getSourceLine() != 0 && elseInsn.getSourceLine() != 0) {
// sometimes source lines incorrect
return checkLineStats(thenInsn, elseInsn);
}
// don't make nested ternary by default
// TODO: add addition checks
return !containsTernary(thenInsn) && !containsTernary(elseInsn);
}
return true;
}
private static void clearConditionBlocks(List<BlockNode> conditionBlocks, BlockNode header) {
for (BlockNode block : conditionBlocks) {
if (block != header) {
@@ -277,6 +275,7 @@ public class TernaryMod extends AbstractRegionVisitor implements IRegionIterativ
return false;
}
@SuppressWarnings("StatementWithEmptyBody")
private static void replaceWithTernary(MethodNode mth, IfRegion ifRegion, BlockNode block, InsnNode insn) {
RegisterArg resArg = insn.getResult();
if (resArg.getSVar().getUseList().size() != 1) {
@@ -296,17 +295,45 @@ public class TernaryMod extends AbstractRegionVisitor implements IRegionIterativ
if (otherArg == null) {
return;
}
InsnNode elseAssign = otherArg.getAssignInsn();
if (mth.isConstructor() || (mth.getParentClass().isEnum() && mth.getMethodInfo().isClassInit())) {
// forcing ternary inline for constructors (will help in moving super call to the top) and enums
// skip code style checks
} else {
if (elseAssign != null && elseAssign.isConstInsn()) {
if (!verifyLineHints(mth, insn, elseAssign)) {
return;
}
} else {
if (insn.getResult().sameCodeVar(otherArg)) {
// don't use same variable in else branch to prevent: l = (l == 0) ? 1 : l
return;
}
}
}
// all checks passed
BlockNode header = ifRegion.getConditionBlocks().get(0);
if (!ifRegion.getParent().replaceSubBlock(ifRegion, header)) {
return;
}
InsnArg elseArg;
if (elseAssign != null && elseAssign.isConstInsn()) {
// inline constant
SSAVar elseVar = elseAssign.getResult().getSVar();
if (elseVar.getUseCount() == 1 && elseVar.getOnlyOneUseInPhi() == phiInsn) {
InsnRemover.remove(mth, elseAssign);
}
elseArg = InsnArg.wrapInsnIntoArg(elseAssign);
} else {
elseArg = otherArg;
}
TernaryInsn ternInsn = new TernaryInsn(ifRegion.getCondition(),
phiInsn.getResult(), InsnArg.wrapInsnIntoArg(insn), otherArg);
phiInsn.getResult(), InsnArg.wrapInsnIntoArg(insn), elseArg);
ternInsn.simplifyCondition();
InsnRemover.unbindResult(mth, insn);
InsnList.remove(block, insn);
InsnRemover.unbindAllArgs(mth, phiInsn);
header.getInstructions().clear();
ternInsn.rebindArgs();
@@ -14,6 +14,9 @@ import jadx.core.Consts;
import jadx.core.codegen.json.JsonMappingGen;
import jadx.core.deobf.Deobfuscator;
import jadx.core.deobf.NameMapper;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.MethodOverrideAttr;
import jadx.core.dex.attributes.nodes.RenameReasonAttr;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.info.FieldInfo;
@@ -195,7 +198,7 @@ public class RenameVisitor extends AbstractVisitor {
Set<String> names = new HashSet<>(methods.size());
for (MethodNode mth : methods) {
String signature = mth.getMethodInfo().makeSignature(true, false);
if (!names.add(signature)) {
if (!names.add(signature) && canRename(mth)) {
deobfuscator.forceRenameMethod(mth);
mth.addAttr(new RenameReasonAttr("collision with other method in class"));
}
@@ -203,6 +206,23 @@ public class RenameVisitor extends AbstractVisitor {
}
}
private static boolean canRename(MethodNode mth) {
if (mth.contains(AFlag.DONT_RENAME)) {
return false;
}
MethodOverrideAttr overrideAttr = mth.get(AType.METHOD_OVERRIDE);
if (overrideAttr != null) {
for (MethodNode relatedMth : overrideAttr.getRelatedMthNodes()) {
if (relatedMth != mth && mth.getParentClass().equals(relatedMth.getParentClass())) {
// ignore rename if exists related method from same class (bridge method in most cases)
// such rename will also rename current method and will not help to resolve name collision
return false;
}
}
}
return true;
}
private static void processRootPackages(Deobfuscator deobfuscator, RootNode root, List<ClassNode> classes) {
Set<String> rootPkgs = collectRootPkgs(classes);
root.getCacheStorage().setRootPkgs(rootPkgs);
@@ -1,7 +1,7 @@
package jadx.core.dex.visitors.shrink;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.LinkedList;
import java.util.List;
import jadx.core.dex.instructions.InsnType;
@@ -30,7 +30,7 @@ final class ArgsInfo {
}
public static List<RegisterArg> getArgs(InsnNode insn) {
List<RegisterArg> args = new LinkedList<>();
List<RegisterArg> args = new ArrayList<>();
addArgs(insn, args);
return args;
}
@@ -1,10 +1,10 @@
package jadx.core.dex.visitors.ssa;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Deque;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import jadx.core.dex.attributes.AFlag;
@@ -81,7 +81,7 @@ public class SSATransform extends AbstractVisitor {
int blocksCount = blocks.size();
BitSet hasPhi = new BitSet(blocksCount);
BitSet processed = new BitSet(blocksCount);
Deque<BlockNode> workList = new LinkedList<>();
Deque<BlockNode> workList = new ArrayDeque<>();
BitSet assignBlocks = la.getAssignBlocks(regNum);
for (int id = assignBlocks.nextSetBit(0); id >= 0; id = assignBlocks.nextSetBit(id + 1)) {
@@ -136,7 +136,7 @@ public class SSATransform extends AbstractVisitor {
RenameState initState = RenameState.init(mth);
initPhiInEnterBlock(initState);
Deque<RenameState> stack = new LinkedList<>();
Deque<RenameState> stack = new ArrayDeque<>();
stack.push(initState);
while (!stack.isEmpty()) {
RenameState state = stack.pop();
@@ -913,25 +913,20 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
}
boolean fixed = false;
for (ITypeBound bound : typeInfo.getBounds()) {
if (bound.getBound() == BoundEnum.USE
&& fixBooleanUsage(mth, bound)) {
for (RegisterArg arg : new ArrayList<>(var.getUseList())) {
if (fixBooleanUsage(mth, arg)) {
fixed = true;
}
}
return fixed;
}
private boolean fixBooleanUsage(MethodNode mth, ITypeBound bound) {
ArgType boundType = bound.getType();
private boolean fixBooleanUsage(MethodNode mth, RegisterArg boundArg) {
ArgType boundType = boundArg.getInitType();
if (boundType == ArgType.BOOLEAN
|| (boundType.isTypeKnown() && !boundType.isPrimitive())) {
return false;
}
RegisterArg boundArg = bound.getArg();
if (boundArg == null) {
return false;
}
InsnNode insn = boundArg.getParentInsn();
if (insn == null || insn.getType() == InsnType.IF) {
return false;
@@ -1,11 +1,33 @@
package jadx.core.utils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.jetbrains.annotations.Nullable;
import jadx.api.plugins.input.data.IMethodHandle;
import jadx.api.plugins.input.data.IMethodProto;
import jadx.api.plugins.input.data.IMethodRef;
import jadx.api.plugins.input.data.MethodHandleType;
import jadx.api.plugins.input.data.annotations.EncodedValue;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.info.FieldInfo;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.instructions.ConstClassNode;
import jadx.core.dex.instructions.ConstStringNode;
import jadx.core.dex.instructions.IndexInsnNode;
import jadx.core.dex.instructions.InsnType;
import jadx.core.dex.instructions.InvokeNode;
import jadx.core.dex.instructions.InvokeType;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.LiteralArg;
import jadx.core.dex.instructions.args.PrimitiveType;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.exceptions.JadxRuntimeException;
public class EncodedValueUtils {
@@ -50,4 +72,108 @@ public class EncodedValueUtils {
return null;
}
}
public static InsnArg convertToInsnArg(RootNode root, EncodedValue value) {
Object obj = value.getValue();
switch (value.getType()) {
case ENCODED_NULL:
case ENCODED_BYTE:
case ENCODED_SHORT:
case ENCODED_CHAR:
case ENCODED_INT:
case ENCODED_LONG:
case ENCODED_FLOAT:
case ENCODED_DOUBLE:
return (InsnArg) convertToConstValue(value);
case ENCODED_BOOLEAN:
return InsnArg.lit(((Boolean) obj) ? 0 : 1, ArgType.BOOLEAN);
case ENCODED_STRING:
return InsnArg.wrapArg(new ConstStringNode((String) obj));
case ENCODED_TYPE:
return InsnArg.wrapArg(new ConstClassNode(ArgType.parse((String) obj)));
case ENCODED_METHOD_TYPE:
return InsnArg.wrapArg(buildMethodType(root, (IMethodProto) obj));
case ENCODED_METHOD_HANDLE:
return InsnArg.wrapArg(buildMethodHandle(root, (IMethodHandle) obj));
}
throw new JadxRuntimeException("Unsupported type for raw invoke-custom: " + value.getType());
}
private static InvokeNode buildMethodType(RootNode root, IMethodProto methodProto) {
ArgType retType = ArgType.parse(methodProto.getReturnType());
List<ArgType> argTypes = Utils.collectionMap(methodProto.getArgTypes(), ArgType::parse);
List<ArgType> callTypes = new ArrayList<>(1 + argTypes.size());
callTypes.add(retType);
callTypes.addAll(argTypes);
ArgType mthType = ArgType.object("java.lang.invoke.MethodType");
ClassInfo cls = ClassInfo.fromType(root, mthType);
MethodInfo mth = MethodInfo.fromDetails(root, cls, "methodType", callTypes, mthType);
InvokeNode invoke = new InvokeNode(mth, InvokeType.STATIC, callTypes.size());
for (ArgType type : callTypes) {
InsnNode argInsn;
if (type.isPrimitive()) {
argInsn = new IndexInsnNode(InsnType.SGET, getTypeField(root, type.getPrimitiveType()), 0);
} else {
argInsn = new ConstClassNode(type);
}
invoke.addArg(InsnArg.wrapArg(argInsn));
}
return invoke;
}
public static FieldInfo getTypeField(RootNode root, PrimitiveType type) {
ArgType boxType = type.getBoxType();
ClassInfo boxCls = ClassInfo.fromType(root, boxType);
return FieldInfo.from(root, boxCls, "TYPE", boxType);
}
/**
* Build `MethodHandles.lookup().find{type}(methodCls, methodName, methodType)`
*/
private static InsnNode buildMethodHandle(RootNode root, IMethodHandle methodHandle) {
if (methodHandle.getType().isField()) {
// TODO: lookup for field
return new ConstStringNode("FIELD:" + methodHandle.getFieldRef());
}
IMethodRef methodRef = methodHandle.getMethodRef();
methodRef.load();
ClassInfo lookupCls = ClassInfo.fromName(root, "java.lang.invoke.MethodHandles.Lookup");
MethodInfo findMethod = MethodInfo.fromDetails(root, lookupCls,
getFindMethodName(methodHandle.getType()),
Arrays.asList(ArgType.CLASS, ArgType.STRING, ArgType.object("java.lang.invoke.MethodType")),
ArgType.object("java.lang.invoke.MethodHandle"));
InvokeNode invoke = new InvokeNode(findMethod, InvokeType.DIRECT, 4);
invoke.addArg(buildLookupArg(root));
invoke.addArg(InsnArg.wrapArg(new ConstClassNode(ArgType.object(methodRef.getParentClassType()))));
invoke.addArg(InsnArg.wrapArg(new ConstStringNode(methodRef.getName())));
invoke.addArg(InsnArg.wrapArg(buildMethodType(root, methodRef)));
return invoke;
}
public static InsnArg buildLookupArg(RootNode root) {
ArgType lookupType = ArgType.object("java.lang.invoke.MethodHandles.Lookup");
ClassInfo cls = ClassInfo.fromName(root, "java.lang.invoke.MethodHandles");
MethodInfo mth = MethodInfo.fromDetails(root, cls, "lookup", Collections.emptyList(), lookupType);
return InsnArg.wrapArg(new InvokeNode(mth, InvokeType.STATIC, 0));
}
private static String getFindMethodName(MethodHandleType type) {
switch (type) {
case INVOKE_STATIC:
return "findStatic";
case INVOKE_CONSTRUCTOR:
return "findConstructor";
case INVOKE_INSTANCE:
case INVOKE_DIRECT:
case INVOKE_INTERFACE:
return "findVirtual";
default:
return "<" + type + '>';
}
}
}
@@ -17,6 +17,7 @@ import jadx.core.dex.instructions.args.InsnWrapArg;
import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.instructions.args.SSAVar;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.IContainer;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.exceptions.JadxRuntimeException;
@@ -228,6 +229,18 @@ public class InsnRemover {
removeAll(block.getInstructions(), insns);
}
public static void removeAllAndUnbind(MethodNode mth, IContainer container, List<InsnNode> insns) {
unbindInsns(mth, insns);
RegionUtils.visitBlocks(mth, container, b -> removeAll(b.getInstructions(), insns));
}
public static void removeAllAndUnbind(MethodNode mth, List<InsnNode> insns) {
unbindInsns(mth, insns);
for (BlockNode block : mth.getBasicBlocks()) {
removeAll(block.getInstructions(), insns);
}
}
public static void removeAllWithoutUnbind(BlockNode block, List<InsnNode> insns) {
removeAll(block.getInstructions(), insns);
}
@@ -4,6 +4,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import org.jetbrains.annotations.Nullable;
@@ -26,6 +27,8 @@ import jadx.core.dex.regions.loops.LoopRegion;
import jadx.core.dex.trycatch.CatchAttr;
import jadx.core.dex.trycatch.ExceptionHandler;
import jadx.core.dex.trycatch.TryCatchBlockAttr;
import jadx.core.dex.visitors.regions.AbstractRegionVisitor;
import jadx.core.dex.visitors.regions.DepthRegionTraversal;
import jadx.core.utils.exceptions.JadxRuntimeException;
public class RegionUtils {
@@ -146,20 +149,6 @@ public class RegionUtils {
}
}
@Nullable
public static IContainer getLastRegion(@Nullable IContainer container) {
if (container == null) {
return null;
}
if (container instanceof IBlock || container instanceof IBranchRegion) {
return container;
}
if (container instanceof IRegion) {
return getLastRegion(Utils.last(((IRegion) container).getSubBlocks()));
}
throw new JadxRuntimeException(unknownContainerType(container));
}
public static boolean isExitBlock(MethodNode mth, IContainer container) {
if (container instanceof BlockNode) {
return BlockUtils.isExitBlock(mth, (BlockNode) container);
@@ -487,4 +476,13 @@ public class RegionUtils {
}
return "Unknown container type: " + container.getClass();
}
public static void visitBlocks(MethodNode mth, IContainer container, Consumer<IBlock> visitor) {
DepthRegionTraversal.traverse(mth, container, new AbstractRegionVisitor() {
@Override
public void processBlock(MethodNode mth, IBlock block) {
visitor.accept(block);
}
});
}
}
@@ -8,6 +8,7 @@ import java.util.Map;
import java.util.Random;
import java.util.Set;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -51,7 +52,7 @@ public class BinaryXMLParser extends CommonBinaryParser {
private boolean isLastEnd = true;
private boolean isOneLine = true;
private int namespaceDepth = 0;
private int[] resourceIds;
private @Nullable int[] resourceIds;
private final RootNode rootNode;
private String appPackageName;
@@ -358,7 +359,7 @@ public class BinaryXMLParser extends CommonBinaryParser {
// As the outcome of https://github.com/skylot/jadx/issues/1208
// Android seems to favor entries from AndroidResMap and only if
// there is no entry uses the values form the XML string pool
if (0 <= id && id < resourceIds.length) {
if (resourceIds != null && 0 <= id && id < resourceIds.length) {
int resId = resourceIds[id];
String str = ValuesParser.getAndroidResMap().get(resId);
if (str != null) {
@@ -4,10 +4,8 @@ import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.xml.parsers.DocumentBuilder;
@@ -175,7 +173,7 @@ public class ManifestAttributes {
if (attr.getType() == MAttrType.ENUM) {
return attr.getValues().get(value);
} else if (attr.getType() == MAttrType.FLAG) {
List<String> flagList = new LinkedList<>();
List<String> flagList = new ArrayList<>();
List<Long> attrKeys = new ArrayList<>(attr.getValues().keySet());
attrKeys.sort((a, b) -> Long.compare(b, a)); // sort descending
for (Long key : attrKeys) {
@@ -188,7 +186,7 @@ public class ManifestAttributes {
value ^= key;
}
}
return flagList.stream().collect(Collectors.joining("|"));
return String.join("|", flagList);
}
return null;
}
@@ -28,9 +28,12 @@ public class ParserConstants {
protected static final int RES_XML_LAST_CHUNK_TYPE = 0x017f;
protected static final int RES_XML_RESOURCE_MAP_TYPE = 0x0180;
protected static final int RES_TABLE_PACKAGE_TYPE = 0x0200;
protected static final int RES_TABLE_TYPE_TYPE = 0x0201;
protected static final int RES_TABLE_TYPE_SPEC_TYPE = 0x0202;
protected static final int RES_TABLE_PACKAGE_TYPE = 0x0200; // 512
protected static final int RES_TABLE_TYPE_TYPE = 0x0201; // 513
protected static final int RES_TABLE_TYPE_SPEC_TYPE = 0x0202; // 514
protected static final int RES_TABLE_TYPE_LIBRARY = 0x0203; // 515
protected static final int RES_TABLE_TYPE_OVERLAY = 0x0204; // 516
protected static final int RES_TABLE_TYPE_STAGED_ALIAS = 0x0206; // 517
/**
* Type constants
@@ -84,7 +84,7 @@ public class ProtoXMLParser {
}
String name = a.getName();
String value = deobfClassName(a.getValue());
writer.add(name).add("=\"").add(value).add('\"');
writer.add(name).add("=\"").add(StringUtils.escapeXML(value)).add('\"');
memorizePackageName(name, value);
}
@@ -153,13 +153,28 @@ public class ResTableParser extends CommonBinaryParser implements IResParser {
while (is.getPos() < endPos) {
long chunkStart = is.getPos();
int type = is.readInt16();
if (type == RES_NULL_TYPE) {
continue;
}
if (type == RES_TABLE_TYPE_SPEC_TYPE) {
parseTypeSpecChunk();
} else if (type == RES_TABLE_TYPE_TYPE) {
parseTypeChunk(chunkStart, pkg);
LOG.trace("res package chunk start at {} type {}", chunkStart, type);
switch (type) {
case RES_NULL_TYPE:
LOG.info("Null chunk type encountered at offset {}", chunkStart);
break;
case RES_TABLE_TYPE_TYPE: // 0x0201
parseTypeChunk(chunkStart, pkg);
break;
case RES_TABLE_TYPE_SPEC_TYPE: // 0x0202
parseTypeSpecChunk(chunkStart);
break;
case RES_TABLE_TYPE_LIBRARY: // 0x0203
parseLibraryTypeChunk(chunkStart);
break;
case RES_TABLE_TYPE_OVERLAY: // 0x0204
throw new IOException(
String.format("Encountered unsupported chunk type TYPE_OVERLAY at offset 0x%x ", chunkStart));
case RES_TABLE_TYPE_STAGED_ALIAS: // 0x0206
throw new IOException(
String.format("Encountered unsupported chunk type TYPE_STAGED_ALIAS at offset 0x%x ", chunkStart));
default:
LOG.warn("Unknown chunk type {} encountered at offset {}", type, chunkStart);
}
}
return pkg;
@@ -192,10 +207,10 @@ public class ResTableParser extends CommonBinaryParser implements IResParser {
}
@SuppressWarnings("unused")
private void parseTypeSpecChunk() throws IOException {
private void parseTypeSpecChunk(long chunkStart) throws IOException {
is.checkInt16(0x0010, "Unexpected type spec header size");
/* int size = */
is.readInt32();
int chunkSize = is.readInt32();
long expectedEndPos = chunkStart + chunkSize;
int id = is.readInt8();
is.skip(3);
@@ -203,6 +218,28 @@ public class ResTableParser extends CommonBinaryParser implements IResParser {
for (int i = 0; i < entryCount; i++) {
int entryFlag = is.readInt32();
}
if (is.getPos() != expectedEndPos) {
throw new IOException(String.format("Error reading type spec chunk at offset 0x%x", chunkStart));
}
}
private void parseLibraryTypeChunk(long chunkStart) throws IOException {
LOG.trace("parsing library type chunk starting at offset {}", chunkStart);
is.checkInt16(12, "Unexpected header size");
int chunkSize = is.readInt32();
long expectedEndPos = chunkStart + chunkSize;
int count = is.readInt32();
for (int i = 0; i < count; i++) {
int packageId = is.readInt32();
String packageName = is.readString16Fixed(128);
LOG.info("Found resource shared library {}, pkgId: {}", packageName, packageId);
if (is.getPos() > expectedEndPos) {
throw new IOException("reading after chunk end");
}
}
if (is.getPos() != expectedEndPos) {
throw new IOException(String.format("Error reading library chunk at offset 0x%x", chunkStart));
}
}
private void parseTypeChunk(long start, PackageChunk pkg) throws IOException {
@@ -241,6 +278,13 @@ public class ResTableParser extends CommonBinaryParser implements IResParser {
parseEntry(pkg, id, i, config.getQualifiers());
}
}
if (chunkEnd > is.getPos()) {
// Skip remaining unknown data in this chunk (e.g. type 8 entries")
long skipSize = chunkEnd - is.getPos();
LOG.debug("Unknown data at the end of type chunk encountered, skipping {} bytes and continuing at offset {}", skipSize,
chunkEnd);
is.skip(skipSize);
}
}
private void parseEntry(PackageChunk pkg, int typeId, int entryId, String config) throws IOException {
@@ -29,7 +29,11 @@ public class ResourcesSaver implements Runnable {
@Override
public void run() {
saveResources(resourceFile.loadContent());
try {
saveResources(resourceFile.loadContent());
} catch (Throwable e) {
LOG.warn("Failed to save resource: {}", resourceFile.getOriginalName(), e);
}
}
private void saveResources(ResContainer rc) {
@@ -49,7 +53,7 @@ public class ResourcesSaver implements Runnable {
private void save(ResContainer rc, File outDir) {
File outFile = new File(outDir, rc.getFileName());
if (!ZipSecurity.isInSubDirectory(outDir, outFile)) {
LOG.error("Path traversal attack detected, invalid resource name: {}", outFile.getPath());
LOG.error("Invalid resource name or path traversal attack detected: {}", outFile.getPath());
return;
}
saveToFile(rc, outFile);
@@ -33,6 +33,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.CommentsLevel;
import jadx.api.DecompilationMode;
import jadx.api.ICodeInfo;
import jadx.api.ICodeWriter;
import jadx.api.JadxArgs;
@@ -109,6 +110,12 @@ public abstract class IntegrationTest extends TestUtils {
private @Nullable TestCompiler sourceCompiler;
private @Nullable TestCompiler decompiledCompiler;
/**
* Run check method on decompiled code even if source check method not found.
* Useful for smali test if check method added to smali code
*/
private boolean forceDecompiledCheck = false;
static {
// enable debug checks
DebugChecks.checksEnabled = true;
@@ -346,11 +353,10 @@ public abstract class IntegrationTest extends TestUtils {
String clsName = cls.getClassInfo().getRawName().replace('/', '.');
try {
// run 'check' method from original class
if (runSourceAutoCheck(clsName)) {
return;
}
boolean sourceCheckFound = runSourceAutoCheck(clsName);
// run 'check' method from decompiled class
if (compile) {
if (compile && (sourceCheckFound || forceDecompiledCheck)) {
runDecompiledAutoCheck(cls);
}
} catch (Exception e) {
@@ -361,36 +367,36 @@ public abstract class IntegrationTest extends TestUtils {
private boolean runSourceAutoCheck(String clsName) {
if (sourceCompiler == null) {
// no source code (smali case)
return true;
System.out.println("Source check: no code");
return false;
}
Class<?> origCls;
try {
origCls = sourceCompiler.getClass(clsName);
} catch (ClassNotFoundException e) {
rethrow("Missing class: " + clsName, e);
return true;
return false;
}
Method checkMth;
try {
checkMth = sourceCompiler.getMethod(origCls, CHECK_METHOD_NAME, new Class[] {});
} catch (NoSuchMethodException e) {
// ignore
return true;
return false;
}
if (!checkMth.getReturnType().equals(void.class)
|| !Modifier.isPublic(checkMth.getModifiers())
|| Modifier.isStatic(checkMth.getModifiers())) {
fail("Wrong 'check' method");
return true;
return false;
}
try {
limitExecTime(() -> checkMth.invoke(origCls.getConstructor().newInstance()));
System.out.println("Source check: PASSED");
return true;
} catch (Throwable e) {
throw new JadxRuntimeException("Source check failed", e);
}
return false;
}
public void runDecompiledAutoCheck(ClassNode cls) {
@@ -546,13 +552,17 @@ public abstract class IntegrationTest extends TestUtils {
protected void setFallback() {
disableCompilation();
this.args.setFallbackMode(true);
this.args.setDecompilationMode(DecompilationMode.FALLBACK);
}
protected void disableCompilation() {
this.compile = false;
}
protected void forceDecompiledCheck() {
this.forceDecompiledCheck = true;
}
protected void enableDeobfuscation() {
args.setDeobfuscationOn(true);
args.setDeobfuscationMapFileMode(DeobfuscationMapFileMode.IGNORE);
@@ -34,6 +34,13 @@ public class JadxClassNodeAssertions extends AbstractObjectAssert<JadxClassNodeA
return new JadxCodeAssertions(codeStr);
}
public JadxCodeAssertions disasmCode() {
isNotNull();
String disasmCode = actual.getDisassembledCode();
assertThat(disasmCode).isNotNull().isNotBlank();
return new JadxCodeAssertions(disasmCode);
}
public JadxCodeAssertions reloadCode(IntegrationTest testInstance) {
isNotNull();
ICodeInfo code = actual.reloadCode();
@@ -6,6 +6,7 @@ import jadx.tests.api.SmaliTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
@SuppressWarnings("CommentedOutCode")
public class TestBooleanToInt extends SmaliTest {
// @formatter:off
@@ -0,0 +1,40 @@
package jadx.tests.integration.conditions;
import org.junit.jupiter.api.Test;
import jadx.tests.api.SmaliTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
@SuppressWarnings("CommentedOutCode")
public class TestBooleanToInt2 extends SmaliTest {
// @formatter:off
/*
public static class TestCls {
public void test() {
boolean v = getValue();
use1(Integer.valueOf(v));
use2(v);
}
private boolean getValue() {
return false;
}
private void use1(Integer v) {
}
private void use2(int v) {
}
}
*/
// @formatter:on
@Test
public void test() {
assertThat(getClassNodeFromSmali())
.code()
.containsOne("use1(Integer.valueOf(value ? 1 : 0));")
.containsOne("use2(value ? 1 : 0);");
}
}
@@ -0,0 +1,37 @@
package jadx.tests.integration.conditions;
import org.junit.jupiter.api.Test;
import jadx.api.ICodeWriter;
import jadx.tests.api.IntegrationTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestElseIfCodeStyle extends IntegrationTest {
@SuppressWarnings("unused")
public static class TestCls {
public void test(String str) {
if ("a".equals(str)) {
call(1);
} else if ("b".equals(str)) {
call(2);
} else if ("c".equals(str)) {
call(3);
}
}
private void call(int i) {
}
}
@Test
public void test() {
noDebugInfo();
assertThat(getClassNode(TestCls.class))
.code()
.doesNotContain("!\"c\".equals(str)")
.doesNotContain("{" + ICodeWriter.NL + indent(2) + "} else {"); // no empty `then` block
}
}
@@ -1,41 +0,0 @@
package jadx.tests.integration.conditions;
import org.junit.jupiter.api.Test;
import jadx.NotYetImplemented;
import jadx.tests.api.IntegrationTest;
public class TestSwitchTryBreak extends IntegrationTest {
public static class TestCls {
public void test(int x) {
switch (x) {
case 0:
return;
case 1:
String res;
if ("android".equals(toString())) {
res = "hello";
} else {
try {
if (String.CASE_INSENSITIVE_ORDER != null) {
break;
}
res = "hi";
} catch (Exception e) {
break;
}
}
System.out.println(res);
}
System.out.println("returning");
}
}
@Test
@NotYetImplemented
public void test() {
getClassNode(TestCls.class);
}
}
@@ -1,41 +0,0 @@
package jadx.tests.integration.conditions;
import org.junit.jupiter.api.Test;
import jadx.NotYetImplemented;
import jadx.tests.api.IntegrationTest;
public class TestSwitchTryBreak2 extends IntegrationTest {
public static class TestCls {
public void test(int x) {
switch (x) {
case 0:
return;
case 1:
String res;
if ("android".equals(toString())) {
res = "hello";
} else {
try {
if (x == 5) {
break;
}
res = "hi";
} catch (Exception e) {
break;
}
}
System.out.println(res);
}
System.out.println("returning");
}
}
@Test
@NotYetImplemented
public void test() {
getClassNode(TestCls.class);
}
}
@@ -0,0 +1,45 @@
package jadx.tests.integration.enums;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
import jadx.tests.api.IntegrationTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestEnumsWithCustomInit extends IntegrationTest {
public enum TestCls {
ONE("I"),
TWO("II"),
THREE("III");
public static final Map<String, TestCls> MAP = new HashMap<>();
static {
for (TestCls value : values()) {
MAP.put(value.toString(), value);
}
}
private final String str;
TestCls(String str) {
this.str = str;
}
public String toString() {
return str;
}
}
@Test
public void test() {
assertThat(getClassNode(TestCls.class))
.code()
.containsOne("ONE(\"I\"),")
.doesNotContain("new TestEnumsWithCustomInit$TestCls(");
}
}
@@ -0,0 +1,39 @@
package jadx.tests.integration.enums;
import jadx.tests.api.IntegrationTest;
import jadx.tests.api.extensions.profiles.TestProfile;
import jadx.tests.api.extensions.profiles.TestWithProfiles;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestEnumsWithTernary extends IntegrationTest {
public enum TestCls {
FIRST(useNumber() ? "1" : "A"),
SECOND(useNumber() ? "2" : "B"),
ANY(useNumber() ? "1" : "2");
private final String str;
TestCls(String str) {
this.str = str;
}
public String getStr() {
return str;
}
public static boolean useNumber() {
return false;
}
}
@TestWithProfiles({ TestProfile.DX_J8, TestProfile.D8_J8 })
public void test() {
noDebugInfo();
assertThat(getClassNode(TestCls.class))
.code()
.containsOne("ANY(useNumber() ? \"1\" : \"2\");")
.doesNotContain("static {");
}
}
@@ -0,0 +1,47 @@
package jadx.tests.integration.inline;
import org.assertj.core.api.Condition;
import jadx.core.dex.nodes.ClassNode;
import jadx.tests.api.IntegrationTest;
import jadx.tests.api.extensions.profiles.TestProfile;
import jadx.tests.api.extensions.profiles.TestWithProfiles;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestSyntheticBridgeRename extends IntegrationTest {
@SuppressWarnings("InnerClassMayBeStatic")
public static class TestCls {
private abstract class Inner<V> {
public abstract V get(String value);
}
public class IntInner extends Inner<Integer> {
public Integer get(String value) {
return value.length();
}
}
public void test() {
IntInner inner = new IntInner();
call(inner.get("a"));
}
private static void call(Integer value) {
}
}
@TestWithProfiles({ TestProfile.DX_J8, TestProfile.JAVA8 })
public void test() {
ClassNode cls = getClassNode(TestCls.class);
assertThat(searchCls(cls.getInnerClasses(), "IntInner").getMethods())
.as("check that bridge method was generated by compiler")
.haveAtLeastOne(new Condition<>(mth -> mth.getAccessFlags().isBridge(), "bridge"));
assertThat(cls)
.code()
.doesNotContain("mo0get")
.containsOne("call(inner.get(\"a\"));");
}
}
@@ -0,0 +1,63 @@
package jadx.tests.integration.invoke;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import org.junit.jupiter.api.Test;
import jadx.core.dex.nodes.ClassNode;
import jadx.tests.api.SmaliTest;
import jadx.tests.api.extensions.profiles.TestProfile;
import jadx.tests.api.extensions.profiles.TestWithProfiles;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
import static org.junit.jupiter.api.Assertions.fail;
public class TestPolymorphicInvoke extends SmaliTest {
public static class TestCls {
public String func(int a, int c) {
return String.valueOf(a + c);
}
public String test() {
try {
MethodType methodType = MethodType.methodType(String.class, Integer.TYPE, Integer.TYPE);
MethodHandle methodHandle = MethodHandles.lookup().findVirtual(TestCls.class, "func", methodType);
return (String) methodHandle.invoke(this, 1, 2);
} catch (Throwable e) {
fail(e);
return null;
}
}
public void check() {
assertThat(test()).isEqualTo("3");
}
}
@TestWithProfiles({ TestProfile.DX_J8, TestProfile.D8_J11 })
public void test() {
ClassNode cls = getClassNode(TestCls.class);
assertThat(cls).code()
.containsOne("return (String) methodHandle.invoke(this, 1, 2);");
assertThat(cls).disasmCode()
.containsOne("invoke-polymorphic");
}
@TestWithProfiles({ TestProfile.JAVA8, TestProfile.JAVA11 })
public void testJava() {
assertThat(getClassNode(TestCls.class))
.code()
.containsOne("return (String) methodHandle.invoke(this, 1, 2);");
// java uses 'invokevirtual'
}
@Test
public void testSmali() {
assertThat(getClassNodeFromSmali())
.code()
.containsOne("String ret = (String) methodHandle.invoke(this, 10, 20);");
}
}
@@ -0,0 +1,48 @@
package jadx.tests.integration.invoke;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import jadx.core.dex.nodes.ClassNode;
import jadx.tests.api.IntegrationTest;
import jadx.tests.api.extensions.profiles.TestProfile;
import jadx.tests.api.extensions.profiles.TestWithProfiles;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
import static org.junit.jupiter.api.Assertions.fail;
public class TestPolymorphicRangeInvoke extends IntegrationTest {
public static class TestCls {
public String func2(int a, int b, int c, int d, int e, int f) {
return String.valueOf(a + b + c + d + e + f);
}
public String test() {
try {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType methodType = MethodType.methodType(String.class, Integer.TYPE, Integer.TYPE, Integer.TYPE, Integer.TYPE,
Integer.TYPE, Integer.TYPE);
MethodHandle methodHandle = lookup.findVirtual(TestCls.class, "func2", methodType);
return (String) methodHandle.invoke(this, 10, 20, 30, 40, 50, 60);
} catch (Throwable e) {
fail(e);
return null;
}
}
public void check() {
assertThat(test()).isEqualTo("210");
}
}
@TestWithProfiles({ TestProfile.DX_J8 })
public void test() {
ClassNode cls = getClassNode(TestCls.class);
assertThat(cls).code()
.containsOne("return (String) methodHandle.invoke(this, 10, 20, 30, 40, 50, 60);");
assertThat(cls).disasmCode()
.containsOne("invoke-polymorphic/range");
}
}
@@ -0,0 +1,66 @@
package jadx.tests.integration.invoke;
import java.lang.invoke.CallSite;
import java.lang.invoke.ConstantCallSite;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import org.junit.jupiter.api.Test;
import jadx.tests.api.SmaliTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
import static org.junit.jupiter.api.Assertions.fail;
public class TestRawCustomInvoke extends SmaliTest {
public static class TestCls {
public static String func(int a, double b) {
return String.valueOf(a + b);
}
private static CallSite staticBootstrap(MethodHandles.Lookup lookup, String name, MethodType type) {
try {
return new ConstantCallSite(lookup.findStatic(lookup.lookupClass(), name, type));
} catch (NoSuchMethodException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public String test() {
try {
return (String) staticBootstrap(MethodHandles.lookup(), "func",
MethodType.methodType(String.class, Integer.TYPE, Double.TYPE))
.dynamicInvoker().invoke(1, 2.0d);
} catch (Throwable e) {
fail(e);
return null;
}
}
public void check() {
assertThat(test()).isEqualTo("3.0");
}
}
@Test
public void test() {
noDebugInfo();
// this code does not contain `invoke-custom` instruction
// only check if equivalent polymorphic call is correct
assertThat(getClassNode(TestCls.class))
.code()
.containsOne(
"return (String) staticBootstrap(MethodHandles.lookup(), \"func\", MethodType.methodType(String.class, Integer.TYPE, Double.TYPE)).dynamicInvoker().invoke(1, 2.0d);");
}
@Test
public void testSmali() {
forceDecompiledCheck();
assertThat(getClassNodeFromSmali())
.code()
.containsOne(
"return (String) staticBootstrap(MethodHandles.lookup(), \"func\", MethodType.methodType(String.class, Integer.TYPE, Double.TYPE)).dynamicInvoker().invoke(1, 2.0d) /* invoke-custom */;");
}
}
@@ -33,7 +33,5 @@ public class TestLambdaExtVar extends IntegrationTest {
.code()
.doesNotContain("lambda$")
.containsOne("return s.equals(str);"); // TODO: simplify to expression
System.out.println(cls.getCode().getCodeMetadata());
}
}
@@ -1,8 +1,8 @@
package jadx.tests.integration.names;
import java.util.ArrayDeque;
import java.util.BitSet;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import org.junit.jupiter.api.Test;
@@ -26,7 +26,7 @@ public class TestNameAssign2 extends IntegrationTest {
int blocksCount = blocks.size();
BitSet hasPhi = new BitSet(blocksCount);
BitSet processed = new BitSet(blocksCount);
Deque<BlockNode> workList = new LinkedList<>();
Deque<BlockNode> workList = new ArrayDeque<>();
BitSet assignBlocks = la.getAssignBlocks(regNum);
for (int id = assignBlocks.nextSetBit(0); id >= 0; id = assignBlocks.nextSetBit(id + 1)) {
@@ -0,0 +1,67 @@
package jadx.tests.integration.others;
import java.util.List;
import org.junit.jupiter.api.Test;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.nodes.ClassNode;
import jadx.tests.api.SmaliTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
@SuppressWarnings("CommentedOutCode")
public class TestOverrideWithSameName extends SmaliTest {
//@formatter:off
/*
interface A {
B a();
C a();
}
abstract class B implements A {
@Override
public C a() {
return null;
}
}
public class C extends B {
@Override
public B a() {
return null;
}
}
*/
//@formatter:on
@Test
public void test() {
List<ClassNode> clsNodes = loadFromSmaliFiles();
assertThat(searchCls(clsNodes, "test.A"))
.code()
.containsOne("C mo0a();") // assume second method was renamed
.doesNotContain("@Override");
ClassNode bCls = searchCls(clsNodes, "test.B");
assertThat(bCls)
.code()
.containsOne("C mo0a() {")
.containsOne("@Override");
assertThat(getMethod(bCls, "a").get(AType.METHOD_OVERRIDE).getOverrideList())
.singleElement()
.satisfies(mth -> assertThat(mth.getMethodInfo().getDeclClass().getShortName()).isEqualTo("A"));
ClassNode cCls = searchCls(clsNodes, "test.C");
assertThat(cCls)
.code()
.containsOne("B a() {")
.containsOne("@Override");
assertThat(getMethod(cCls, "a").get(AType.METHOD_OVERRIDE).getOverrideList())
.singleElement()
.satisfies(mth -> assertThat(mth.getMethodInfo().getDeclClass().getShortName()).isEqualTo("A"));
}
}
@@ -0,0 +1,29 @@
.class public Lconditions/TestBooleanToInt2;
.super Ljava/lang/Object;
.method public test()V
.registers 3
invoke-direct {p0}, Lconditions/TestBooleanToInt2;->getValue()Z
move-result v0
invoke-static {v0}, Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer;
move-result-object v1
invoke-direct {p0, v1}, Lconditions/TestBooleanToInt2;->use1(Ljava/lang/Integer;)V
invoke-direct {p0, v0}, Lconditions/TestBooleanToInt2;->use2(I)V
return-void
.end method
.method private getValue()Z
.registers 2
const/4 v0, 0x0
return v0
.end method
.method private use1(Ljava/lang/Integer;)V
.registers 2
return-void
.end method
.method private use2(I)V
.registers 2
return-void
.end method
@@ -0,0 +1,77 @@
.class public Linvoke/TestPolymorphicInvoke;
.super Ljava/lang/Object;
.method public func(II)Ljava/lang/String;
.registers 4
.param p1, "a" # I
.param p2, "c" # I
.line 23
add-int v0, p1, p2
invoke-static {v0}, Ljava/lang/String;->valueOf(I)Ljava/lang/String;
move-result-object v0
return-object v0
.end method
.method public test()V
.registers 7
.line 32
:try_start_0
invoke-static {}, Ljava/lang/invoke/MethodHandles;->lookup()Ljava/lang/invoke/MethodHandles$Lookup;
move-result-object v0
.line 33
.local v0, "lookup":Ljava/lang/invoke/MethodHandles$Lookup;
const-class v1, Ljava/lang/String;
sget-object v2, Ljava/lang/Integer;->TYPE:Ljava/lang/Class;
const/4 v3, 0x1
new-array v3, v3, [Ljava/lang/Class;
const/4 v4, 0x0
sget-object v5, Ljava/lang/Integer;->TYPE:Ljava/lang/Class;
aput-object v5, v3, v4
invoke-static {v1, v2, v3}, Ljava/lang/invoke/MethodType;->methodType(Ljava/lang/Class;Ljava/lang/Class;[Ljava/lang/Class;)Ljava/lang/invoke/MethodType;
move-result-object v1
.line 34
.local v1, "methodType":Ljava/lang/invoke/MethodType;
const-class v2, Linvoke/TestPolymorphicInvoke;
const-string v3, "func"
invoke-virtual {v0, v2, v3, v1}, Ljava/lang/invoke/MethodHandles$Lookup;->findVirtual(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle;
move-result-object v2
.line 35
.local v2, "methodHandle":Ljava/lang/invoke/MethodHandle;
const/16 v3, 0xa
const/16 v4, 0x14
invoke-polymorphic {v2, p0, v3, v4}, Ljava/lang/invoke/MethodHandle;->invoke([Ljava/lang/Object;)Ljava/lang/Object;, (Linvoke/TestPolymorphicInvoke;II)Ljava/lang/String;
move-result-object v3
.line 36
.local v3, "ret":Ljava/lang/String;
sget-object v4, Ljava/lang/System;->out:Ljava/io/PrintStream;
invoke-virtual {v4, v3}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
:try_end_2a
.catchall {:try_start_0 .. :try_end_2a} :catchall_2b
.line 39
.end local v0 # "lookup":Ljava/lang/invoke/MethodHandles$Lookup;
.end local v1 # "methodType":Ljava/lang/invoke/MethodType;
.end local v2 # "methodHandle":Ljava/lang/invoke/MethodHandle;
.end local v3 # "ret":Ljava/lang/String;
goto :goto_2f
.line 37
:catchall_2b
move-exception v0
.line 38
.local v0, "e":Ljava/lang/Throwable;
invoke-virtual {v0}, Ljava/lang/Throwable;->printStackTrace()V
.line 40
.end local v0 # "e":Ljava/lang/Throwable;
:goto_2f
return-void
.end method
@@ -0,0 +1,62 @@
.class public Linvoke/TestRawCustomInvoke;
.super Ljava/lang/Object;
.method public static func(ID)Ljava/lang/String;
.registers 5
int-to-double v0, p0
add-double/2addr v0, p1
invoke-static {v0, v1}, Ljava/lang/String;->valueOf(D)Ljava/lang/String;
move-result-object p0
return-object p0
.end method
.method private static staticBootstrap(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
.registers 5
:try_start_0
new-instance v0, Ljava/lang/invoke/ConstantCallSite;
invoke-virtual {p0}, Ljava/lang/invoke/MethodHandles$Lookup;->lookupClass()Ljava/lang/Class;
move-result-object v1
invoke-virtual {p0, v1, p1, p2}, Ljava/lang/invoke/MethodHandles$Lookup;->findStatic(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle;
move-result-object p0
invoke-direct {v0, p0}, Ljava/lang/invoke/ConstantCallSite;-><init>(Ljava/lang/invoke/MethodHandle;)V
:try_end_d
.catch Ljava/lang/NoSuchMethodException; {:try_start_0 .. :try_end_d} :catch_e
.catch Ljava/lang/IllegalAccessException; {:try_start_0 .. :try_end_d} :catch_e
return-object v0
:catch_e
move-exception p0
new-instance p1, Ljava/lang/RuntimeException;
invoke-direct {p1, p0}, Ljava/lang/RuntimeException;-><init>(Ljava/lang/Throwable;)V
throw p1
.end method
.method public test()Ljava/lang/String;
.registers 3
:try_start_0
const/4 v0, 0x1
const-wide/high16 v1, 0x4000000000000000L # 2.0
invoke-custom {v0, v1}, call_site_0("func", (ID)Ljava/lang/String;)@Linvoke/TestRawCustomInvoke;->staticBootstrap(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
move-result-object v0
:try_end_25
.catchall {:try_start_0 .. :try_end_25} :catchall_26
return-object v0
:catchall_26
move-exception v0
invoke-static {v0}, Lorg/junit/jupiter/api/Assertions;->fail(Ljava/lang/Throwable;)Ljava/lang/Object;
const/4 v0, 0x0
return-object v0
.end method
.method public check()V
.registers 3
invoke-virtual {p0}, Linvoke/TestRawCustomInvoke;->test()Ljava/lang/String;
move-result-object v0
invoke-static {v0}, Ljadx/tests/api/utils/assertj/JadxAssertions;->assertThat(Ljava/lang/String;)Ljadx/tests/api/utils/assertj/JadxCodeAssertions;
move-result-object v0
const-string v1, "3.0"
invoke-virtual {v0, v1}, Ljadx/tests/api/utils/assertj/JadxCodeAssertions;->isEqualTo(Ljava/lang/String;)Lorg/assertj/core/api/AbstractStringAssert;
return-void
.end method
@@ -0,0 +1,8 @@
.class interface abstract Ltest/A;
.super Ljava/lang/Object;
.method public abstract a()Ltest/B;
.end method
.method public abstract a()Ltest/C;
.end method
@@ -0,0 +1,10 @@
.class abstract Ltest/B;
.super Ljava/lang/Object;
.implements Ltest/A;
.method public a()Ltest/C;
.registers 2
const/4 v0, 0x0
return-object v0
.end method
@@ -0,0 +1,8 @@
.class public Ltest/C;
.super Ltest/B;
.method public a()Ltest/B;
.registers 2
const/4 v0, 0x0
return-object v0
.end method
+13 -13
View File
@@ -1,33 +1,33 @@
plugins {
id 'application'
id 'edu.sc.seis.launch4j' version '2.5.3'
id 'edu.sc.seis.launch4j' version '2.5.4'
id 'com.github.johnrengelman.shadow' version '7.1.2'
id 'org.beryx.runtime' version '1.12.7'
id 'org.beryx.runtime' version '1.13.0'
}
dependencies {
implementation(project(':jadx-core'))
implementation(project(":jadx-cli"))
implementation 'com.beust:jcommander:1.82'
implementation 'ch.qos.logback:logback-classic:1.2.11'
implementation 'ch.qos.logback:logback-classic:1.3.5'
implementation 'com.fifesoft:rsyntaxtextarea:3.2.0'
implementation 'com.fifesoft:rsyntaxtextarea:3.3.2'
implementation files('libs/jfontchooser-1.0.5.jar')
implementation 'hu.kazocsaba:image-viewer:1.2.3'
implementation 'com.formdev:flatlaf:2.4'
implementation 'com.formdev:flatlaf-intellij-themes:2.4'
implementation 'com.formdev:flatlaf-extras:2.4'
implementation 'com.formdev:svgSalamander:1.1.3'
implementation 'com.formdev:flatlaf:3.0'
implementation 'com.formdev:flatlaf-intellij-themes:3.0'
implementation 'com.formdev:flatlaf-extras:3.0'
implementation 'com.formdev:svgSalamander:1.1.4'
implementation 'com.google.code.gson:gson:2.9.1'
implementation 'com.google.code.gson:gson:2.10.1'
implementation 'org.apache.commons:commons-lang3:3.12.0'
implementation 'org.apache.commons:commons-text:1.9'
implementation 'org.apache.commons:commons-text:1.10.0'
implementation 'io.reactivex.rxjava2:rxjava:2.2.21'
implementation "com.github.akarnokd:rxjava2-swing:0.3.7"
implementation 'com.android.tools.build:apksig:7.2.2'
implementation 'io.github.hqktech:jdwp:1.0'
implementation 'com.android.tools.build:apksig:7.4.1'
implementation 'io.github.skylot:jdwp:2.0.0'
// TODO: Switch back to upstream once this PR gets merged:
// https://github.com/FabricMC/mapping-io/pull/19
@@ -90,7 +90,7 @@ launch4j {
windowTitle = 'jadx'
companyName = 'jadx'
jreMinVersion = '11'
jvmOptions = ['-Dawt.useSystemAAFontSettings=lcd', '-Dswing.aatext=true', '-XX:+UseG1GC']
jvmOptions = ['-Dawt.useSystemAAFontSettings=lcd', '-Dswing.aatext=true', '-XX:+UseG1GC', '-Djava.util.Arrays.useLegacyMergeSort=true']
jreRuntimeBits = "64"
bundledJre64Bit = true
initialHeapPercent = 5
@@ -44,11 +44,12 @@ import jadx.gui.ui.panel.JDebuggerPanel.ValueTreeNode;
import jadx.gui.utils.NLS;
public final class DebugController implements SmaliDebugger.SuspendListener, IDebugController {
private static final Logger LOG = LoggerFactory.getLogger(DebugController.class);
private static final Logger LOG = LoggerFactory.getLogger(DebugController.class);
private static final String ONCREATE_SIGNATURE = "onCreate(Landroid/os/Bundle;)V";
private static final Map<String, RuntimeType> TYPE_MAP = new HashMap<>();
private static final RuntimeType[] POSSIBLE_TYPES = { RuntimeType.OBJECT, RuntimeType.INT, RuntimeType.LONG };
private static final int DEFAULT_CACHE_SIZE = 512;
private JDebuggerPanel debuggerPanel;
private SmaliDebugger debugger;
@@ -1115,7 +1116,7 @@ public final class DebugController implements SmaliDebugger.SuspendListener, IDe
private long thisID;
public FrameNode(long threadID, SmaliDebugger.Frame frame) {
cache = new StringBuilder(16);
cache = new StringBuilder(DEFAULT_CACHE_SIZE);
this.frame = frame;
this.threadID = threadID;
regNodes = Collections.emptyList();
@@ -1153,7 +1154,7 @@ public final class DebugController implements SmaliDebugger.SuspendListener, IDe
public void setSignatures(String clsSig, String mthSig) {
this.clsSig = clsSig;
this.mthSig = mthSig;
this.cache.delete(0, this.cache.length());
resetCache();
}
public String getClsSig() {
@@ -1167,7 +1168,7 @@ public final class DebugController implements SmaliDebugger.SuspendListener, IDe
public void updateCodeOffset(long codeOffset) {
this.codeOffset = codeOffset;
if (this.codeOffset > -1) {
this.cache.delete(0, this.cache.length());
resetCache();
}
}
@@ -1209,27 +1210,34 @@ public final class DebugController implements SmaliDebugger.SuspendListener, IDe
}
}
private void resetCache() {
// Do not reuse thee existing cache instance as this can result in
// multi-threading access issues in case toString() method is active
this.cache = new StringBuilder(DEFAULT_CACHE_SIZE);
}
@Override
public String toString() {
if (cache.length() == 0) {
StringBuilder sbCache = cache;
if (sbCache.length() == 0) {
long off = getCodeOffset();
if (off < 0) {
cache.append(String.format("index: %-4d ", off));
sbCache.append(String.format("index: %-4d ", off));
} else {
cache.append(String.format("index: %04x ", off));
sbCache.append(String.format("index: %04x ", off));
}
if (clsSig == null) {
cache.append("clsID: ").append(frame.getClassID());
sbCache.append("clsID: ").append(frame.getClassID());
} else {
cache.append(clsSig).append("->");
sbCache.append(clsSig).append("->");
}
if (mthSig == null) {
cache.append(" mthID: ").append(frame.getMethodID());
sbCache.append(" mthID: ").append(frame.getMethodID());
} else {
cache.append(mthSig);
sbCache.append(mthSig);
}
}
return cache.toString();
return sbCache.toString();
}
}
@@ -1,23 +1,23 @@
package jadx.gui.device.debugger;
import io.github.hqktech.JDWP.Event.Composite.BreakpointEvent;
import io.github.hqktech.JDWP.Event.Composite.ClassPrepareEvent;
import io.github.hqktech.JDWP.Event.Composite.ClassUnloadEvent;
import io.github.hqktech.JDWP.Event.Composite.ExceptionEvent;
import io.github.hqktech.JDWP.Event.Composite.FieldAccessEvent;
import io.github.hqktech.JDWP.Event.Composite.FieldModificationEvent;
import io.github.hqktech.JDWP.Event.Composite.MethodEntryEvent;
import io.github.hqktech.JDWP.Event.Composite.MethodExitEvent;
import io.github.hqktech.JDWP.Event.Composite.MethodExitWithReturnValueEvent;
import io.github.hqktech.JDWP.Event.Composite.MonitorContendedEnterEvent;
import io.github.hqktech.JDWP.Event.Composite.MonitorContendedEnteredEvent;
import io.github.hqktech.JDWP.Event.Composite.MonitorWaitEvent;
import io.github.hqktech.JDWP.Event.Composite.MonitorWaitedEvent;
import io.github.hqktech.JDWP.Event.Composite.SingleStepEvent;
import io.github.hqktech.JDWP.Event.Composite.ThreadDeathEvent;
import io.github.hqktech.JDWP.Event.Composite.ThreadStartEvent;
import io.github.hqktech.JDWP.Event.Composite.VMDeathEvent;
import io.github.hqktech.JDWP.Event.Composite.VMStartEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.BreakpointEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.ClassPrepareEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.ClassUnloadEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.ExceptionEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.FieldAccessEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.FieldModificationEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.MethodEntryEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.MethodExitEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.MethodExitWithReturnValueEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.MonitorContendedEnterEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.MonitorContendedEnteredEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.MonitorWaitEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.MonitorWaitedEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.SingleStepEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.ThreadDeathEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.ThreadStartEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.VMDeathEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.VMStartEvent;
abstract class EventListenerAdapter {
void onVMStart(VMStartEvent event) {
@@ -0,0 +1,390 @@
package jadx.gui.device.debugger;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Timer;
import java.util.TimerTask;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.gui.device.protocol.ADBDevice;
import jadx.gui.ui.panel.LogcatPanel;
public class LogcatController {
private static final Logger LOG = LoggerFactory.getLogger(LogcatController.class);
private final ADBDevice adbDevice;
private final LogcatPanel logcatPanel;
private Timer timer;
private final String timezone;
private LogcatInfo recent = null;
private ArrayList<LogcatInfo> events = new ArrayList<>();
private LogcatFilter filter = new LogcatFilter(null, null);
private String status = "null";
public LogcatController(LogcatPanel logcatPanel, ADBDevice adbDevice) throws IOException {
this.adbDevice = adbDevice;
this.logcatPanel = logcatPanel;
this.timezone = adbDevice.getTimezone();
this.startLogcat();
}
public void startLogcat() {
timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
getLog();
}
}, 0, 1000);
this.status = "running";
}
public void stopLogcat() {
timer.cancel();
this.status = "stopped";
}
public String getStatus() {
return this.status;
}
public void clearLogcat() {
try {
adbDevice.clearLogcat();
clearEvents();
} catch (IOException e) {
LOG.error("Failed to clear Logcat", e);
}
}
private void getLog() {
if (!logcatPanel.isReady()) {
return;
}
try {
byte[] buf;
if (recent == null) {
buf = adbDevice.getBinaryLogcat();
} else {
buf = adbDevice.getBinaryLogcat(recent.getAfterTimestamp());
}
if (buf == null) {
return;
}
ByteBuffer in = ByteBuffer.wrap(buf);
in.order(ByteOrder.LITTLE_ENDIAN);
while (in.remaining() > 20) {
LogcatInfo eInfo = null;
byte[] msgBuf;
short eLen = in.getShort();
short eHdrLen = in.getShort();
if (eLen + eHdrLen > in.remaining()) {
return;
}
switch (eHdrLen) {
case 20: // header length 20 == version 1
eInfo = new LogcatInfo(eLen, eHdrLen, in.getInt(), in.getInt(), in.getInt(), in.getInt(), in.get());
msgBuf = new byte[eLen];
in.get(msgBuf, 0, eLen - 1);
eInfo.setMsg(msgBuf);
break;
case 24: // header length 24 == version 2 / 3
eInfo = new LogcatInfo(eLen, eHdrLen, in.getInt(), in.getInt(), in.getInt(), in.getInt(), in.getInt(), in.get());
msgBuf = new byte[eLen];
in.get(msgBuf, 0, eLen - 1);
eInfo.setMsg(msgBuf);
break;
case 28: // header length 28 == version 4
eInfo = new LogcatInfo(eLen, eHdrLen, in.getInt(), in.getInt(), in.getInt(), in.getInt(), in.getInt(), in.getInt(),
in.get());
msgBuf = new byte[eLen];
in.get(msgBuf, 0, eLen - 1);
eInfo.setMsg(msgBuf);
break;
default:
break;
}
if (eInfo == null) {
return;
}
if (recent == null) {
recent = eInfo;
} else if (recent.getInstant().isBefore(eInfo.getInstant())) {
recent = eInfo;
}
if (filter.doFilter(eInfo)) {
logcatPanel.log(eInfo);
}
events.add(eInfo);
}
} catch (Exception e) {
LOG.error("Failed to get logcat message", e);
}
}
public boolean reload() {
stopLogcat();
boolean ok = logcatPanel.clearLogcatArea();
if (ok) {
events.forEach((eInfo) -> {
if (filter.doFilter(eInfo)) {
logcatPanel.log(eInfo);
}
});
startLogcat();
}
return true;
}
public void clearEvents() {
this.recent = null;
this.events = new ArrayList<>();
}
public void exit() {
stopLogcat();
filter = new LogcatFilter(null, null);
recent = null;
}
public LogcatFilter getFilter() {
return this.filter;
}
public class LogcatFilter {
private final ArrayList<Integer> pid;
private ArrayList<Byte> msgType = new ArrayList<Byte>() {
{
add((byte) 1);
add((byte) 2);
add((byte) 3);
add((byte) 4);
add((byte) 5);
add((byte) 6);
add((byte) 7);
add((byte) 8);
}
};
public LogcatFilter(ArrayList<Integer> pid, ArrayList<Byte> msgType) {
if (pid != null) {
this.pid = pid;
} else {
this.pid = new ArrayList<>();
}
if (msgType != null) {
this.msgType = msgType;
}
}
public void addPid(int pid) {
if (!this.pid.contains(pid)) {
this.pid.add(pid);
}
}
public void removePid(int pid) {
int pidPos = this.pid.indexOf(pid);
if (pidPos >= 0) {
this.pid.remove(pidPos);
}
}
public void togglePid(int pid, boolean state) {
if (state) {
addPid(pid);
} else {
removePid(pid);
}
}
public void addMsgType(byte msgType) {
if (!this.msgType.contains(msgType)) {
this.msgType.add(msgType);
}
}
public void removeMsgType(byte msgType) {
int typePos = this.msgType.indexOf(msgType);
if (typePos >= 0) {
this.msgType.remove(typePos);
}
}
public void toggleMsgType(byte msgType, boolean state) {
if (state) {
addMsgType(msgType);
} else {
removeMsgType(msgType);
}
}
public boolean doFilter(LogcatInfo inInfo) {
if (pid.contains(inInfo.getPid())) {
return msgType.contains(inInfo.getMsgType());
}
return false;
}
public ArrayList<LogcatInfo> getFilteredList(ArrayList<LogcatInfo> inInfoList) {
ArrayList<LogcatInfo> outInfoList = new ArrayList<LogcatInfo>();
inInfoList.forEach((inInfo) -> {
if (doFilter(inInfo)) {
outInfoList.add(inInfo);
}
});
return outInfoList;
}
}
public class LogcatInfo {
private String msg;
private final byte msgType;
private final int nsec;
private final int pid;
private final int sec;
private final int tid;
private final short hdrSize;
private final short len;
private final short version;
private int lid;
private int uid;
public LogcatInfo(short len, short hdrSize, int pid, int tid, int sec, int nsec, byte msgType) {
this.hdrSize = hdrSize;
this.len = len;
this.msgType = msgType;
this.nsec = nsec;
this.pid = pid;
this.sec = sec;
this.tid = tid;
this.version = 1;
}
// Version 2 and 3 both have the same arguments
public LogcatInfo(short len, short hdrSize, int pid, int tid, int sec, int nsec, int lid, byte msgType) {
this.hdrSize = hdrSize;
this.len = len;
this.lid = lid;
this.msgType = msgType;
this.nsec = nsec;
this.pid = pid;
this.sec = sec;
this.tid = tid;
this.version = 3;
}
public LogcatInfo(short len, short hdrSize, int pid, int tid, int sec, int nsec, int lid, int uid, byte msgType) {
this.hdrSize = hdrSize;
this.len = len;
this.lid = lid;
this.msgType = msgType;
this.nsec = nsec;
this.pid = pid;
this.sec = sec;
this.tid = tid;
this.uid = uid;
this.version = 4;
}
public void setMsg(byte[] msg) {
this.msg = new String(msg);
}
public short getVersion() {
return this.version;
}
public short getLen() {
return this.len;
}
public short getHeaderLen() {
return this.hdrSize;
}
public int getPid() {
return this.pid;
}
public int getTid() {
return this.tid;
}
public int getSec() {
return this.sec;
}
public int getNSec() {
return this.nsec;
}
public int getLid() {
return this.lid;
}
public int getUid() {
return this.uid;
}
public Instant getInstant() {
return Instant.ofEpochSecond(getSec(), getNSec());
}
public String getTimestamp() {
DateTimeFormatter dtFormat = DateTimeFormatter.ofPattern("MM-dd HH:mm:ss.SSS").withZone(ZoneId.of(timezone));
return dtFormat.format(getInstant());
}
public String getAfterTimestamp() {
DateTimeFormatter dtFormat = DateTimeFormatter.ofPattern("MM-dd HH:mm:ss.SSS").withZone(ZoneId.of(timezone));
return dtFormat.format(getInstant().plusMillis(1));
}
public byte getMsgType() {
return this.msgType;
}
public String getMsgTypeString() {
switch (getMsgType()) {
case 0:
return "Unknown";
case 1:
return "Default";
case 2:
return "Verbose";
case 3:
return "Debug";
case 4:
return "Info";
case 5:
return "Warn";
case 6:
return "Error";
case 7:
return "Fatal";
case 8:
return "Silent";
default:
return "Unknown";
}
}
public String getMsg() {
return this.msg;
}
}
}
@@ -1,6 +1,6 @@
package jadx.gui.device.debugger;
import io.github.hqktech.JDWP;
import io.github.skylot.jdwp.JDWP;
public enum RuntimeType {
ARRAY(91, "[]"),
@@ -21,56 +21,56 @@ import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.github.hqktech.JDWP;
import io.github.hqktech.JDWP.ArrayReference.Length.LengthReplyData;
import io.github.hqktech.JDWP.ByteBuffer;
import io.github.hqktech.JDWP.Event.Composite.BreakpointEvent;
import io.github.hqktech.JDWP.Event.Composite.ClassPrepareEvent;
import io.github.hqktech.JDWP.Event.Composite.ClassUnloadEvent;
import io.github.hqktech.JDWP.Event.Composite.EventData;
import io.github.hqktech.JDWP.Event.Composite.ExceptionEvent;
import io.github.hqktech.JDWP.Event.Composite.FieldAccessEvent;
import io.github.hqktech.JDWP.Event.Composite.FieldModificationEvent;
import io.github.hqktech.JDWP.Event.Composite.MethodEntryEvent;
import io.github.hqktech.JDWP.Event.Composite.MethodExitEvent;
import io.github.hqktech.JDWP.Event.Composite.MethodExitWithReturnValueEvent;
import io.github.hqktech.JDWP.Event.Composite.MonitorContendedEnterEvent;
import io.github.hqktech.JDWP.Event.Composite.MonitorContendedEnteredEvent;
import io.github.hqktech.JDWP.Event.Composite.MonitorWaitEvent;
import io.github.hqktech.JDWP.Event.Composite.MonitorWaitedEvent;
import io.github.hqktech.JDWP.Event.Composite.SingleStepEvent;
import io.github.hqktech.JDWP.Event.Composite.ThreadDeathEvent;
import io.github.hqktech.JDWP.Event.Composite.ThreadStartEvent;
import io.github.hqktech.JDWP.Event.Composite.VMDeathEvent;
import io.github.hqktech.JDWP.Event.Composite.VMStartEvent;
import io.github.hqktech.JDWP.EventRequest.Set.ClassMatchRequest;
import io.github.hqktech.JDWP.EventRequest.Set.CountRequest;
import io.github.hqktech.JDWP.EventRequest.Set.LocationOnlyRequest;
import io.github.hqktech.JDWP.EventRequest.Set.StepRequest;
import io.github.hqktech.JDWP.Method.VariableTableWithGeneric.VarTableWithGenericData;
import io.github.hqktech.JDWP.Method.VariableTableWithGeneric.VarWithGenericSlot;
import io.github.hqktech.JDWP.ObjectReference;
import io.github.hqktech.JDWP.ObjectReference.ReferenceType.ReferenceTypeReplyData;
import io.github.hqktech.JDWP.ObjectReference.SetValues.FieldValueSetter;
import io.github.hqktech.JDWP.Packet;
import io.github.hqktech.JDWP.ReferenceType.FieldsWithGeneric.FieldsWithGenericData;
import io.github.hqktech.JDWP.ReferenceType.FieldsWithGeneric.FieldsWithGenericReplyData;
import io.github.hqktech.JDWP.ReferenceType.MethodsWithGeneric.MethodsWithGenericData;
import io.github.hqktech.JDWP.ReferenceType.MethodsWithGeneric.MethodsWithGenericReplyData;
import io.github.hqktech.JDWP.ReferenceType.Signature.SignatureReplyData;
import io.github.hqktech.JDWP.StackFrame.GetValues.GetValuesReplyData;
import io.github.hqktech.JDWP.StackFrame.GetValues.GetValuesSlots;
import io.github.hqktech.JDWP.StackFrame.SetValues.SlotValueSetter;
import io.github.hqktech.JDWP.StackFrame.ThisObject.ThisObjectReplyData;
import io.github.hqktech.JDWP.StringReference.Value.ValueReplyData;
import io.github.hqktech.JDWP.ThreadReference.Frames.FramesReplyData;
import io.github.hqktech.JDWP.ThreadReference.Frames.FramesReplyDataFrames;
import io.github.hqktech.JDWP.ThreadReference.Name.NameReplyData;
import io.github.hqktech.JDWP.VirtualMachine.AllClassesWithGeneric.AllClassesWithGenericData;
import io.github.hqktech.JDWP.VirtualMachine.AllClassesWithGeneric.AllClassesWithGenericReplyData;
import io.github.hqktech.JDWP.VirtualMachine.AllThreads.AllThreadsReplyData;
import io.github.hqktech.JDWP.VirtualMachine.AllThreads.AllThreadsReplyDataThreads;
import io.github.hqktech.JDWP.VirtualMachine.CreateString.CreateStringReplyData;
import io.github.skylot.jdwp.JDWP;
import io.github.skylot.jdwp.JDWP.ArrayReference.Length.LengthReplyData;
import io.github.skylot.jdwp.JDWP.ByteBuffer;
import io.github.skylot.jdwp.JDWP.Event.Composite.BreakpointEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.ClassPrepareEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.ClassUnloadEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.EventData;
import io.github.skylot.jdwp.JDWP.Event.Composite.ExceptionEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.FieldAccessEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.FieldModificationEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.MethodEntryEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.MethodExitEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.MethodExitWithReturnValueEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.MonitorContendedEnterEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.MonitorContendedEnteredEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.MonitorWaitEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.MonitorWaitedEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.SingleStepEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.ThreadDeathEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.ThreadStartEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.VMDeathEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.VMStartEvent;
import io.github.skylot.jdwp.JDWP.EventRequest.Set.ClassMatchRequest;
import io.github.skylot.jdwp.JDWP.EventRequest.Set.CountRequest;
import io.github.skylot.jdwp.JDWP.EventRequest.Set.LocationOnlyRequest;
import io.github.skylot.jdwp.JDWP.EventRequest.Set.StepRequest;
import io.github.skylot.jdwp.JDWP.Method.VariableTableWithGeneric.VarTableWithGenericData;
import io.github.skylot.jdwp.JDWP.Method.VariableTableWithGeneric.VarWithGenericSlot;
import io.github.skylot.jdwp.JDWP.ObjectReference;
import io.github.skylot.jdwp.JDWP.ObjectReference.ReferenceType.ReferenceTypeReplyData;
import io.github.skylot.jdwp.JDWP.ObjectReference.SetValues.FieldValueSetter;
import io.github.skylot.jdwp.JDWP.Packet;
import io.github.skylot.jdwp.JDWP.ReferenceType.FieldsWithGeneric.FieldsWithGenericData;
import io.github.skylot.jdwp.JDWP.ReferenceType.FieldsWithGeneric.FieldsWithGenericReplyData;
import io.github.skylot.jdwp.JDWP.ReferenceType.MethodsWithGeneric.MethodsWithGenericData;
import io.github.skylot.jdwp.JDWP.ReferenceType.MethodsWithGeneric.MethodsWithGenericReplyData;
import io.github.skylot.jdwp.JDWP.ReferenceType.Signature.SignatureReplyData;
import io.github.skylot.jdwp.JDWP.StackFrame.GetValues.GetValuesReplyData;
import io.github.skylot.jdwp.JDWP.StackFrame.GetValues.GetValuesSlots;
import io.github.skylot.jdwp.JDWP.StackFrame.SetValues.SlotValueSetter;
import io.github.skylot.jdwp.JDWP.StackFrame.ThisObject.ThisObjectReplyData;
import io.github.skylot.jdwp.JDWP.StringReference.Value.ValueReplyData;
import io.github.skylot.jdwp.JDWP.ThreadReference.Frames.FramesReplyData;
import io.github.skylot.jdwp.JDWP.ThreadReference.Frames.FramesReplyDataFrames;
import io.github.skylot.jdwp.JDWP.ThreadReference.Name.NameReplyData;
import io.github.skylot.jdwp.JDWP.VirtualMachine.AllClassesWithGeneric.AllClassesWithGenericData;
import io.github.skylot.jdwp.JDWP.VirtualMachine.AllClassesWithGeneric.AllClassesWithGenericReplyData;
import io.github.skylot.jdwp.JDWP.VirtualMachine.AllThreads.AllThreadsReplyData;
import io.github.skylot.jdwp.JDWP.VirtualMachine.AllThreads.AllThreadsReplyDataThreads;
import io.github.skylot.jdwp.JDWP.VirtualMachine.CreateString.CreateStringReplyData;
import io.reactivex.annotations.NonNull;
import jadx.api.plugins.input.data.AccessFlags;
@@ -1286,7 +1286,10 @@ public class SmaliDebugger {
@Override
public String getType() {
String gen = getSignature();
return gen.isEmpty() ? this.slot.signature : gen;
if (gen == null || gen.isEmpty()) {
return this.slot.signature;
}
return gen;
}
@NonNull
@@ -1304,6 +1307,11 @@ public class SmaliDebugger {
public int getEndOffset() {
return (int) (slot.codeIndex + slot.length);
}
@Override
public boolean isMarkedAsParameter() {
return false;
}
}
public static class RuntimeDebugInfo {
@@ -13,6 +13,8 @@ import java.util.Map.Entry;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.ICodeInfo;
import jadx.api.plugins.input.data.AccessFlags;
@@ -46,6 +48,7 @@ import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import static jadx.api.plugins.input.data.AccessFlagsScope.FIELD;
@@ -67,6 +70,7 @@ import static jadx.api.plugins.input.insns.Opcode.SPARSE_SWITCH;
import static jadx.api.plugins.input.insns.Opcode.SPARSE_SWITCH_PAYLOAD;
public class Smali {
private static final Logger LOG = LoggerFactory.getLogger(Smali.class);
private static SmaliInsnDecoder insnDecoder = null;
@@ -219,7 +223,15 @@ public class Smali {
writeFields(smali, clsData, fields, colWidths);
fields.clear();
}
writeMethod(smali, cls.getMethods().get(mthIndex[0]++), m, line);
try {
writeMethod(smali, cls.getMethods().get(mthIndex[0]++), m, line);
} catch (Throwable e) {
IMethodRef methodRef = m.getMethodRef();
String mthFullName = methodRef.getParentClassType() + "->" + methodRef.getName();
smali.setIndent(0);
smali.startLine("Failed to write method: " + mthFullName + "\n" + Utils.getStackTrace(e));
LOG.error("Failed to write smali code for method: {}", mthFullName, e);
}
line.reset();
});
@@ -273,24 +285,25 @@ public class Smali {
writeMethodDef(smali, mth, line);
ICodeReader codeReader = mth.getCodeReader();
if (codeReader != null) {
int regsCount = codeReader.getRegistersCount();
line.smaliMthNode.setParamRegStart(getParamStartRegNum(mth));
line.smaliMthNode.setRegCount(codeReader.getRegistersCount());
line.smaliMthNode.setRegCount(regsCount);
Map<Long, InsnNode> nodes = new HashMap<>(codeReader.getUnitsCount() / 2);
line.smaliMthNode.setInsnNodes(nodes, codeReader.getUnitsCount());
line.smaliMthNode.initRegInfoList(codeReader.getRegistersCount(), codeReader.getUnitsCount());
line.smaliMthNode.initRegInfoList(regsCount, codeReader.getUnitsCount());
smali.incIndent();
smali.startLine(".registers ")
.add("" + codeReader.getRegistersCount())
.startLine();
smali.startLine(".registers ").add(Integer.toString(regsCount));
writeTries(codeReader, line);
if (formatMthParamInfo(mth, smali, codeReader, line)) {
smali.startLine();
IDebugInfo debugInfo = codeReader.getDebugInfo();
List<ILocalVar> localVars = debugInfo != null ? debugInfo.getLocalVars() : Collections.emptyList();
formatMthParamInfo(mth, smali, line, regsCount, localVars);
if (debugInfo != null) {
formatDbgInfo(debugInfo, localVars, line);
}
smali.newLine();
smali.startLine();
if (codeReader.getDebugInfo() != null) {
formatDbgInfo(codeReader.getDebugInfo(), line);
}
// first pass to fill payload offsets for switch instructions
codeReader.visitInstructions(insn -> {
Opcode opcode = insn.getOpcode();
@@ -446,53 +459,45 @@ public class Smali {
}
}
private boolean formatMthParamInfo(IMethodData mth, SmaliWriter smali, ICodeReader codeReader, LineInfo line) {
private void formatMthParamInfo(IMethodData mth, SmaliWriter smali, LineInfo line,
int regsCount, List<ILocalVar> localVars) {
List<String> types = mth.getMethodRef().getArgTypes();
if (types.isEmpty()) {
return false;
return;
}
int paramCount = 0;
int paramStart = 0;
int regNum = line.smaliMthNode.getParamRegStart();
if (!hasStaticFlag(mth.getAccessFlags())) {
// add 'this' register
line.addRegName(regNum, "p0");
line.smaliMthNode.setParamReg(regNum, "p0");
regNum += 1;
paramStart = 1;
regNum++;
paramStart++;
}
IDebugInfo dbgInfo = codeReader.getDebugInfo();
if (dbgInfo != null) {
for (ILocalVar var : dbgInfo.getLocalVars()) {
if (var.getStartOffset() == -1) {
int i = writeParamInfo(smali, line, regNum, paramStart, var.getName(), var.getType());
regNum += i;
paramStart += i;
paramCount++;
}
if (localVars.isEmpty()) {
return;
}
ILocalVar[] params = new ILocalVar[regsCount];
for (ILocalVar var : localVars) {
if (var.isMarkedAsParameter()) {
params[var.getRegNum()] = var;
}
}
for (; paramCount < types.size(); paramCount++) {
int i = writeParamInfo(smali, line, regNum, paramStart, "", types.get(paramCount));
regNum += i;
paramStart += i;
smali.newLine();
for (String paramType : types) {
ILocalVar param = params[regNum];
if (param != null) {
String name = Utils.getOrElse(param.getName(), "");
String type = Utils.getOrElse(param.getSignature(), paramType);
String varName = "p" + paramStart;
smali.startLine(String.format(".param %s, \"%s\" # %s", varName, name, type));
line.addRegName(regNum, varName);
line.smaliMthNode.setParamReg(regNum, varName);
}
int regSize = isWideType(paramType) ? 2 : 1;
regNum += regSize;
paramStart += regSize;
}
return true;
}
private static int writeParamInfo(SmaliWriter smali, LineInfo line,
int regNum, int paramNum, String dbgInfoName, String type) {
smali.startLine(String.format(".param p%d, \"%s\":%s", paramNum, dbgInfoName, type));
String pName = "p" + paramNum;
line.addRegName(regNum, pName);
line.smaliMthNode.setParamReg(regNum, pName);
if (isWideType(type)) {
regNum++;
dbgInfoName = "p" + (paramNum + 1);
line.addRegName(regNum, dbgInfoName);
line.smaliMthNode.setParamReg(regNum, dbgInfoName);
return 2;
}
return 1;
}
private static int getParamStartRegNum(IMethodData mth) {
@@ -549,32 +554,44 @@ public class Smali {
smali.startLine(".end annotation");
}
private void formatDbgInfo(IDebugInfo dbgInfo, LineInfo line) {
private void formatDbgInfo(IDebugInfo dbgInfo, List<ILocalVar> localVars, LineInfo line) {
dbgInfo.getSourceLineMapping().forEach((codeOffset, srcLine) -> {
if (codeOffset > -1) {
line.addDebugLineTip(codeOffset, String.format(".line %d", srcLine), "");
}
});
for (ILocalVar localVar : dbgInfo.getLocalVars()) {
String type = localVar.getSignature();
if (type == null || type.trim().isEmpty()) {
type = localVar.getType();
for (ILocalVar localVar : localVars) {
if (localVar.isMarkedAsParameter()) {
continue;
}
if (localVar.getStartOffset() > -1) {
line.addTip(
localVar.getStartOffset(),
String.format(".local v%d", localVar.getRegNum()),
String.format(", \"%s\":%s", localVar.getName(), type));
}
if (localVar.getEndOffset() > -1) {
line.addTip(
localVar.getEndOffset(),
String.format(".end local v%d", localVar.getRegNum()),
String.format(" # \"%s\":%s", localVar.getName(), type));
String type = localVar.getType();
String sign = localVar.getSignature();
String longTypeStr;
if (sign == null || sign.trim().isEmpty()) {
longTypeStr = String.format(", \"%s\":%s", localVar.getName(), type);
} else {
longTypeStr = String.format(", \"%s\":%s, \"%s\"", localVar.getName(), type, localVar.getSignature());
}
line.addTip(
localVar.getStartOffset(),
".local " + formatVarName(line.smaliMthNode, localVar),
longTypeStr);
line.addTip(
localVar.getEndOffset(),
".end local " + formatVarName(line.smaliMthNode, localVar),
String.format(" # \"%s\":%s", localVar.getName(), type));
}
}
private String formatVarName(SmaliMethodNode smaliMthNode, ILocalVar localVar) {
int paramRegStart = smaliMthNode.getParamRegStart();
int regNum = localVar.getRegNum();
if (regNum < paramRegStart) {
return "v" + regNum;
}
return "p" + (regNum - paramRegStart);
}
private void writeEncodedValue(SmaliWriter smali, EncodedValue value, boolean wrapArray) {
switch (value.getType()) {
case ENCODED_ARRAY:
@@ -94,7 +94,6 @@ class SmaliMethodNode {
protected void setParamReg(int regNum, String name) {
SmaliRegister r = regList.get(regNum);
r.setParam(name);
r.setStartOffset(-1);
}
protected void setParamRegStart(int paramRegStart) {
@@ -33,10 +33,6 @@ public class SmaliRegister extends RegisterInfo {
}
protected void setStartOffset(int off) {
if (startOffset == -1 && !isParam) {
startOffset = off;
return;
}
if (off < startOffset) {
startOffset = off;
}
@@ -71,4 +67,9 @@ public class SmaliRegister extends RegisterInfo {
public int getEndOffset() {
return endOffset;
}
@Override
public boolean isMarkedAsParameter() {
return isParam;
}
}
@@ -10,6 +10,8 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -25,7 +27,7 @@ public class ADBDevice {
private static final Logger LOG = LoggerFactory.getLogger(ADBDevice.class);
private static final String CMD_TRACK_JDWP = "000atrack-jdwp";
private static final Pattern TIMESTAMP_FORMAT = Pattern.compile("^[0-9]{2}\\-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\\.[0-9]{3}$");
ADBDeviceInfo info;
String androidReleaseVer;
volatile Socket jdwpListenerSock;
@@ -120,6 +122,49 @@ public class ADBDevice {
return -1;
}
/**
* @Return binary output of logcat
*/
public byte[] getBinaryLogcat() throws IOException {
Socket socket = ADB.connect(info.getAdbHost(), info.getAdbPort());
String cmd = "logcat -dB";
return ADB.execShellCommandRaw(info.getSerial(), cmd, socket.getOutputStream(), socket.getInputStream());
}
/**
* @Return binary output of logcat after provided timestamp
* Timestamp is in the format 09-08 02:18:03.131
*/
public byte[] getBinaryLogcat(String timestamp) throws IOException {
Socket socket = ADB.connect(info.getAdbHost(), info.getAdbPort());
Matcher matcher = TIMESTAMP_FORMAT.matcher(timestamp);
if (!matcher.find()) {
LOG.error("Invalid Logcat Timestamp " + timestamp);
}
String cmd = "logcat -dB -t \"" + timestamp + "\"";
return ADB.execShellCommandRaw(info.getSerial(), cmd, socket.getOutputStream(), socket.getInputStream());
}
/**
* @Return binary output of logcat -c
*/
public void clearLogcat() throws IOException {
Socket socket = ADB.connect(info.getAdbHost(), info.getAdbPort());
String cmd = "logcat -c";
ADB.execShellCommandRaw(info.getSerial(), cmd, socket.getOutputStream(), socket.getInputStream());
}
/**
* @return Timezone for the attached android device
*/
public String getTimezone() throws IOException {
Socket socket = ADB.connect(info.getAdbHost(), info.getAdbPort());
String cmd = "getprop persist.sys.timezone";
byte[] tz = ADB.execShellCommandRaw(info.getSerial(), cmd, socket.getOutputStream(), socket.getInputStream());
return new String(tz).trim();
}
public String getAndroidReleaseVersion() {
if (!StringUtils.isEmpty(androidReleaseVer)) {
return androidReleaseVer;
@@ -160,15 +205,15 @@ public class ADBDevice {
}
public List<Process> getProcessByPkg(String pkg) throws IOException {
return getProcessList("ps | grep " + pkg, 0);
return getProcessList("ps | grep " + pkg);
}
@NonNull
public List<Process> getProcessList() throws IOException {
return getProcessList("ps", 1);
return getProcessList("ps");
}
private List<Process> getProcessList(String cmd, int index) throws IOException {
private List<Process> getProcessList(String cmd) throws IOException {
try (Socket socket = ADB.connect(info.getAdbHost(), info.getAdbPort())) {
List<Process> procs = new ArrayList<>();
byte[] payload = ADB.execShellCommandRaw(info.getSerial(), cmd,
@@ -21,6 +21,7 @@ import org.slf4j.LoggerFactory;
import jadx.gui.settings.JadxSettings;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.ui.NodeLabel;
public class QuarkDialog extends JDialog {
private static final long serialVersionUID = 4855753773520368215L;
@@ -59,7 +60,7 @@ public class QuarkDialog extends JDialog {
description.setAlignmentX(0.5f);
fileSelectCombo = new JComboBox<>(files.toArray(new Path[0]));
fileSelectCombo.setRenderer((list, value, index, isSelected, cellHasFocus) -> new JLabel(value.getFileName().toString()));
fileSelectCombo.setRenderer((list, value, index, isSelected, cellHasFocus) -> new NodeLabel(value.getFileName().toString()));
JPanel textPane = new JPanel();
textPane.add(description);
@@ -44,6 +44,7 @@ import jadx.gui.ui.MainWindow;
import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.utils.JNodeCache;
import jadx.gui.utils.ui.NodeLabel;
public class QuarkReportPanel extends ContentPanel {
private static final long serialVersionUID = -242266836695889206L;
@@ -211,7 +212,7 @@ public class QuarkReportPanel extends ContentPanel {
@Override
public Component render() {
JLabel label = new JLabel(((String) getUserObject()));
JLabel label = new NodeLabel(((String) getUserObject()));
label.setFont(bold ? boldFont : font);
label.setIcon(null);
label.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
@@ -320,7 +321,7 @@ public class QuarkReportPanel extends ContentPanel {
@Override
public Component render() {
JLabel label = new JLabel(mth.toString());
JLabel label = new NodeLabel(mth.toString());
label.setFont(font);
label.setIcon(jnode.getIcon());
label.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
@@ -5,7 +5,6 @@ import java.util.List;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
@@ -35,7 +34,6 @@ public class SearchTask extends CancelableBackgroundTask {
private final TaskProgress taskProgress = new TaskProgress();
private final AtomicInteger resultsCount = new AtomicInteger(0);
private final AtomicBoolean complete = new AtomicBoolean(false);
private int resultsLimit;
private Future<TaskStatus> future;
@@ -60,7 +58,6 @@ public class SearchTask extends CancelableBackgroundTask {
throw new IllegalStateException("Previous task not yet finished");
}
resetCancel();
complete.set(false);
resultsCount.set(0);
taskProgress.updateTotal(jobs.stream().mapToInt(s -> s.getProvider().total()).sum());
future = backgroundExecutor.execute(this);
@@ -74,7 +71,6 @@ public class SearchTask extends CancelableBackgroundTask {
this.resultsListener.accept(resultNode);
if (resultsLimit != 0 && resultsCount.incrementAndGet() >= resultsLimit) {
cancel();
complete.set(false);
return true;
}
return false;
@@ -5,6 +5,7 @@ import java.util.Objects;
import javax.swing.Icon;
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
@@ -197,7 +198,7 @@ public class CommentSearchProvider implements ISearchProvider {
@Override
public String getSyntaxName() {
return node.getSyntaxName();
return SyntaxConstants.SYNTAX_STYLE_NONE; // comment is always plain text
}
@Override
@@ -220,6 +221,11 @@ public class CommentSearchProvider implements ISearchProvider {
return node.makeLongStringHtml();
}
@Override
public boolean disableHtml() {
return node.disableHtml();
}
@Override
public int getPos() {
return node.getPos();
@@ -73,12 +73,14 @@ public class JadxSettings extends JadxCLIArgs {
private boolean showHeapUsageBar = false;
private boolean alwaysSelectOpened = false;
private boolean useAlternativeFileDialog = false;
private Map<String, WindowLocation> windowPos = new HashMap<>();
private int mainWindowExtendedState = JFrame.NORMAL;
private boolean codeAreaLineWrap = false;
private int srhResourceSkipSize = 1000;
private String srhResourceFileExt = ".xml|.html|.js|.json|.txt";
private int searchResultsPerPage = 50;
private boolean useAutoSearch = true;
private boolean keepCommonDialogOpen = false;
private boolean smaliAreaShowBytecode = false;
@@ -267,6 +269,14 @@ public class JadxSettings extends JadxCLIArgs {
partialSync(settings -> settings.alwaysSelectOpened = alwaysSelectOpened);
}
public boolean isUseAlternativeFileDialog() {
return useAlternativeFileDialog;
}
public void setUseAlternativeFileDialog(boolean useAlternativeFileDialog) {
this.useAlternativeFileDialog = useAlternativeFileDialog;
}
public String getExcludedPackages() {
return excludedPackages;
}
@@ -541,6 +551,14 @@ public class JadxSettings extends JadxCLIArgs {
srhResourceFileExt = all.trim();
}
public int getSearchResultsPerPage() {
return searchResultsPerPage;
}
public void setSearchResultsPerPage(int searchResultsPerPage) {
this.searchResultsPerPage = searchResultsPerPage;
}
public boolean isUseAutoSearch() {
return useAutoSearch;
}
@@ -45,10 +45,6 @@ import javax.swing.SpinnerNumberModel;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -647,6 +643,10 @@ public class JadxSettingsWindow extends JDialog {
jumpOnDoubleClick.setSelected(settings.isJumpOnDoubleClick());
jumpOnDoubleClick.addItemListener(e -> settings.setJumpOnDoubleClick(e.getStateChange() == ItemEvent.SELECTED));
JCheckBox useAltFileDialog = new JCheckBox();
useAltFileDialog.setSelected(settings.isUseAlternativeFileDialog());
useAltFileDialog.addItemListener(e -> settings.setUseAlternativeFileDialog(e.getStateChange() == ItemEvent.SELECTED));
JCheckBox update = new JCheckBox();
update.setSelected(settings.isCheckForUpdates());
update.addItemListener(e -> settings.setCheckForUpdates(e.getStateChange() == ItemEvent.SELECTED));
@@ -669,6 +669,7 @@ public class JadxSettingsWindow extends JDialog {
group.addRow(NLS.str("preferences.language"), languageCbx);
group.addRow(NLS.str("preferences.lineNumbersMode"), lineNumbersMode);
group.addRow(NLS.str("preferences.jumpOnDoubleClick"), jumpOnDoubleClick);
group.addRow(NLS.str("preferences.useAlternativeFileDialog"), useAltFileDialog);
group.addRow(NLS.str("preferences.check_for_updates"), update);
group.addRow(NLS.str("preferences.cfg"), cfg);
group.addRow(NLS.str("preferences.raw_cfg"), rawCfg);
@@ -676,48 +677,26 @@ public class JadxSettingsWindow extends JDialog {
}
private SettingsGroup makeSearchResGroup() {
SettingsGroup group = new SettingsGroup(NLS.str("preferences.search_res_title"));
int prevSize = settings.getSrhResourceSkipSize();
String prevExts = settings.getSrhResourceFileExt();
SpinnerNumberModel sizeLimitModel = new SpinnerNumberModel(prevSize,
0, Integer.MAX_VALUE, 1);
JSpinner spinner = new JSpinner(sizeLimitModel);
JSpinner resultsPerPage = new JSpinner(
new SpinnerNumberModel(settings.getSearchResultsPerPage(), 0, Integer.MAX_VALUE, 1));
resultsPerPage.addChangeListener(ev -> settings.setSearchResultsPerPage((Integer) resultsPerPage.getValue()));
JSpinner sizeLimit = new JSpinner(
new SpinnerNumberModel(settings.getSrhResourceSkipSize(), 0, Integer.MAX_VALUE, 1));
sizeLimit.addChangeListener(ev -> settings.setSrhResourceSkipSize((Integer) sizeLimit.getValue()));
JTextField fileExtField = new JTextField();
group.addRow(NLS.str("preferences.res_skip_file"), spinner);
group.addRow(NLS.str("preferences.res_file_ext"), fileExtField);
fileExtField.getDocument().addDocumentListener(new DocumentUpdateListener((ev) -> {
String ext = fileExtField.getText();
settings.setSrhResourceFileExt(ext);
}));
fileExtField.setText(settings.getSrhResourceFileExt());
spinner.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
int size = (Integer) spinner.getValue();
settings.setSrhResourceSkipSize(size);
}
});
fileExtField.getDocument().addDocumentListener(new DocumentListener() {
private void update() {
String ext = fileExtField.getText();
settings.setSrhResourceFileExt(ext);
}
@Override
public void insertUpdate(DocumentEvent e) {
update();
}
@Override
public void removeUpdate(DocumentEvent e) {
update();
}
@Override
public void changedUpdate(DocumentEvent e) {
update();
}
});
fileExtField.setText(prevExts);
return group;
SettingsGroup searchGroup = new SettingsGroup(NLS.str("preferences.search_group_title"));
searchGroup.addRow(NLS.str("preferences.search_results_per_page"), resultsPerPage);
searchGroup.addRow(NLS.str("preferences.res_skip_file"), sizeLimit);
searchGroup.addRow(NLS.str("preferences.res_file_ext"), fileExtField);
return searchGroup;
}
private void needReload() {
@@ -71,6 +71,11 @@ public class CodeNode extends JNode {
return jNode.makeLongStringHtml();
}
@Override
public boolean disableHtml() {
return jNode.disableHtml();
}
@Override
public String getSyntaxName() {
return jNode.getSyntaxName();
@@ -97,6 +97,11 @@ public class JField extends JNode {
return UiUtils.typeStr(field.getType()) + " " + field.getName();
}
@Override
public boolean disableHtml() {
return false;
}
@Override
public boolean hasDescString() {
return false;
@@ -155,6 +155,11 @@ public class JMethod extends JNode {
return UiUtils.typeFormatHtml(name, getReturnType());
}
@Override
public boolean disableHtml() {
return false;
}
@Override
public String makeDescString() {
return UiUtils.typeStr(getReturnType()) + " " + makeBaseString();
@@ -81,6 +81,10 @@ public abstract class JNode extends DefaultMutableTreeNode implements Comparable
return makeLongString();
}
public boolean disableHtml() {
return true;
}
public int getPos() {
JavaNode javaNode = getJavaNode();
if (javaNode == null) {
@@ -61,6 +61,11 @@ public class JVariable extends JNode {
return UiUtils.typeFormatHtml(var.getName(), var.getType());
}
@Override
public boolean disableHtml() {
return false;
}
@Override
public String getTooltip() {
String name = var.getName() + " (r" + var.getReg() + "v" + var.getSsa() + ")";
@@ -23,6 +23,7 @@ import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.geom.AffineTransform;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
@@ -116,10 +117,11 @@ import jadx.gui.ui.codearea.EditorTheme;
import jadx.gui.ui.codearea.EditorViewState;
import jadx.gui.ui.dialog.ADBDialog;
import jadx.gui.ui.dialog.AboutDialog;
import jadx.gui.ui.dialog.FileDialog;
import jadx.gui.ui.dialog.LogViewerDialog;
import jadx.gui.ui.dialog.RenameDialog;
import jadx.gui.ui.dialog.SearchDialog;
import jadx.gui.ui.filedialog.FileDialogWrapper;
import jadx.gui.ui.filedialog.FileOpenMode;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.ui.panel.IssuesPanel;
import jadx.gui.ui.panel.JDebuggerPanel;
@@ -142,6 +144,7 @@ import jadx.gui.utils.UiUtils;
import jadx.gui.utils.fileswatcher.LiveReloadWorker;
import jadx.gui.utils.logs.LogCollector;
import jadx.gui.utils.ui.ActionHandler;
import jadx.gui.utils.ui.NodeLabel;
import static io.reactivex.internal.functions.Functions.EMPTY_RUNNABLE;
import static javax.swing.KeyStroke.getKeyStroke;
@@ -292,19 +295,19 @@ public class MainWindow extends JFrame {
}
public void openFileDialog() {
showOpenDialog(FileDialog.OpenMode.OPEN);
showOpenDialog(FileOpenMode.OPEN);
}
public void openProjectDialog() {
showOpenDialog(FileDialog.OpenMode.OPEN_PROJECT);
showOpenDialog(FileOpenMode.OPEN_PROJECT);
}
private void showOpenDialog(FileDialog.OpenMode mode) {
private void showOpenDialog(FileOpenMode mode) {
saveAll();
if (!ensureProjectIsSaved()) {
return;
}
FileDialog fileDialog = new FileDialog(this, mode);
FileDialogWrapper fileDialog = new FileDialogWrapper(this, mode);
List<Path> openPaths = fileDialog.show();
if (!openPaths.isEmpty()) {
settings.setLastOpenFilePath(fileDialog.getCurrentDir());
@@ -313,7 +316,7 @@ public class MainWindow extends JFrame {
}
public void addFiles() {
FileDialog fileDialog = new FileDialog(this, FileDialog.OpenMode.ADD);
FileDialogWrapper fileDialog = new FileDialogWrapper(this, FileOpenMode.ADD);
List<Path> addPaths = fileDialog.show();
if (!addPaths.isEmpty()) {
addFiles(addPaths);
@@ -345,7 +348,7 @@ public class MainWindow extends JFrame {
}
private void saveProjectAs() {
FileDialog fileDialog = new FileDialog(this, FileDialog.OpenMode.SAVE_PROJECT);
FileDialogWrapper fileDialog = new FileDialogWrapper(this, FileOpenMode.SAVE_PROJECT);
if (project.getFilePaths().size() == 1) {
// If there is only one file loaded we suggest saving the jadx project file next to the loaded file
Path projectPath = getProjectPathForFile(this.project.getFilePaths().get(0));
@@ -376,7 +379,7 @@ public class MainWindow extends JFrame {
}
private void exportMappings(MappingFormat mappingFormat) {
FileDialog fileDialog = new FileDialog(this, FileDialog.OpenMode.CUSTOM_SAVE);
FileDialogWrapper fileDialog = new FileDialogWrapper(this, FileOpenMode.CUSTOM_SAVE);
fileDialog.setTitle(NLS.str("file.export_mappings_as"));
Path workingDir = project.getWorkingDir();
Path baseDir = workingDir != null ? workingDir : settings.getLastSaveFilePath();
@@ -529,9 +532,11 @@ public class MainWindow extends JFrame {
updateLiveReload(project.isEnableLiveReload());
BreakpointManager.init(project.getFilePaths().get(0).toAbsolutePath().getParent());
List<EditorViewState> openTabs = project.getOpenTabs(this);
backgroundExecutor.execute(NLS.str("progress.load"),
this::restoreOpenTabs,
() -> preLoadOpenTabs(openTabs),
status -> {
restoreOpenTabs(openTabs);
runInitialBackgroundJobs();
notifyLoadListeners(true);
});
@@ -658,7 +663,7 @@ public class MainWindow extends JFrame {
}
private void saveAll(boolean export) {
FileDialog fileDialog = new FileDialog(this, FileDialog.OpenMode.EXPORT);
FileDialogWrapper fileDialog = new FileDialogWrapper(this, FileOpenMode.EXPORT);
List<Path> saveDirs = fileDialog.show();
if (saveDirs.isEmpty()) {
return;
@@ -1281,6 +1286,7 @@ public class MainWindow extends JFrame {
Component c = super.getTreeCellRendererComponent(tree, value, selected, expanded, isLeaf, row, focused);
if (value instanceof JNode) {
JNode jNode = (JNode) value;
NodeLabel.disableHtml(this, jNode.disableHtml());
setText(jNode.makeStringHtml());
setIcon(jNode.getIcon());
setToolTipText(jNode.getTooltip());
@@ -1420,8 +1426,9 @@ public class MainWindow extends JFrame {
}
GraphicsDevice gd = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
DisplayMode mode = gd.getDisplayMode();
int w = mode.getWidth();
int h = mode.getHeight();
AffineTransform trans = gd.getDefaultConfiguration().getDefaultTransform();
int w = (int) (mode.getWidth() / trans.getScaleX());
int h = (int) (mode.getHeight() / trans.getScaleY());
setBounds((int) (w * BORDER_RATIO), (int) (h * BORDER_RATIO),
(int) (w * WINDOW_RATIO), (int) (h * WINDOW_RATIO));
setLocationRelativeTo(null);
@@ -1498,8 +1505,8 @@ public class MainWindow extends JFrame {
project.saveOpenTabs(tabbedPane.getEditorViewStates(), tabbedPane.getSelectedIndex());
}
private void restoreOpenTabs() {
List<EditorViewState> openTabs = project.getOpenTabs(this);
private void restoreOpenTabs(List<EditorViewState> openTabs) {
UiUtils.uiThreadGuard();
if (openTabs.isEmpty()) {
return;
}
@@ -1513,6 +1520,18 @@ public class MainWindow extends JFrame {
}
}
private void preLoadOpenTabs(List<EditorViewState> openTabs) {
UiUtils.notUiThreadGuard();
for (EditorViewState tabState : openTabs) {
JNode node = tabState.getNode();
try {
node.getCodeInfo();
} catch (Exception e) {
LOG.warn("Failed to preload code for node: {}", node, e);
}
}
}
private void saveSplittersInfo() {
settings.setMainWindowVerticalSplitterLoc(verticalSplitter.getDividerLocation());
settings.setDebuggerStackFrameSplitterLoc(debuggerPanel.getLeftSplitterLocation());
@@ -23,6 +23,7 @@ import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.utils.Icons;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.ui.NodeLabel;
public class TabComponent extends JPanel {
private static final long serialVersionUID = -8147035487543610321L;
@@ -58,7 +59,7 @@ public class TabComponent extends JPanel {
} else {
tabTitle = node.makeLongStringHtml();
}
label = new JLabel(tabTitle);
label = new NodeLabel(tabTitle, node.disableHtml());
label.setFont(getLabelFont());
String toolTip = contentPanel.getTabTooltip();
if (toolTip != null) {
@@ -73,6 +73,7 @@ public class TabbedPane extends JTabbedPane {
}
});
interceptTabKey();
interceptCloseKey();
enableSwitchingTabs();
}
@@ -120,6 +121,34 @@ public class TabbedPane extends JTabbedPane {
});
}
private void interceptCloseKey() {
KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(new KeyEventDispatcher() {
private static final int closeKey = KeyEvent.VK_W;
private boolean canClose = true;
@Override
public boolean dispatchKeyEvent(KeyEvent e) {
if (!FocusManager.isActive()) {
return false; // do nothing when tab is not on focus.
}
if (e.getKeyCode() != closeKey) {
return false; // only intercept the events of the close key
}
if (e.getID() == KeyEvent.KEY_RELEASED) {
canClose = true; // after the close key is lifted we allow to use it again
return false;
}
if (e.isControlDown() && canClose) {
// close the current tab
closeCodePanel(curTab);
canClose = false; // make sure we dont close more tabs until the close key is lifted
return true;
}
return false;
}
});
}
private void enableSwitchingTabs() {
addChangeListener(e -> {
ContentPanel tab = getSelectedCodePanel();
@@ -2,6 +2,7 @@ package jadx.gui.ui.codearea;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
@@ -396,4 +397,24 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea {
LOG.debug("Error on code area dispose", e);
}
}
@Override
public Dimension getPreferredSize() {
try {
return super.getPreferredSize();
} catch (Exception e) {
LOG.warn("Failed to calculate preferred size for code area", e);
// copied from javax.swing.JTextArea.getPreferredSize (super call above)
// as a fallback for returned null size
Dimension d = new Dimension(400, 400);
Insets insets = getInsets();
if (getColumns() != 0) {
d.width = Math.max(d.width, getColumns() * getColumnWidth() + insets.left + insets.right);
}
if (getRows() != 0) {
d.height = Math.max(d.height, getRows() * getRowHeight() + insets.top + insets.bottom);
}
return d;
}
}
}
@@ -23,7 +23,6 @@ import jadx.api.utils.CodeUtils;
import jadx.core.codegen.TypeGen;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.treemodel.JClass;
@@ -76,45 +75,50 @@ public final class FridaAction extends JNodeAction {
}
private String generateMethodSnippet(JMethod jMth) {
JavaMethod javaMethod = jMth.getJavaMethod();
MethodInfo methodInfo = javaMethod.getMethodNode().getMethodInfo();
String methodName = StringEscapeUtils.escapeEcmaScript(methodInfo.getName());
String callMethodName = methodName;
MethodNode mth = jMth.getJavaMethod().getMethodNode();
MethodInfo methodInfo = mth.getMethodInfo();
String methodName;
String newMethodName;
if (methodInfo.isConstructor()) {
methodName = "$init";
callMethodName = "$new";
}
String shortClassName = javaMethod.getDeclaringClass().getName();
String functionUntilImplementation;
if (isOverloaded(javaMethod.getMethodNode())) {
List<ArgType> methodArgs = methodInfo.getArgumentsTypes();
String overloadStr = methodArgs.stream().map(this::parseArgType).collect(Collectors.joining(", "));
functionUntilImplementation = String.format("%s[\"%s\"].overload(%s).implementation", shortClassName, methodName, overloadStr);
newMethodName = methodName;
} else {
functionUntilImplementation = String.format("%s[\"%s\"].implementation", shortClassName, methodName);
methodName = StringEscapeUtils.escapeEcmaScript(methodInfo.getName());
newMethodName = StringEscapeUtils.escapeEcmaScript(methodInfo.getAlias());
}
List<String> methodArgNames = collectMethodArgNames(javaMethod);
String functionParametersString = String.join(", ", methodArgNames);
String logParametersString =
methodArgNames.stream().map(e -> String.format("'%s: ' + %s", e, e)).collect(Collectors.joining(" + ', ' + "));
if (logParametersString.length() > 0) {
logParametersString = " + ', ' + " + logParametersString;
String overload;
if (isOverloaded(mth)) {
String overloadArgs = methodInfo.getArgumentsTypes().stream()
.map(this::parseArgType).collect(Collectors.joining(", "));
overload = ".overload(" + overloadArgs + ")";
} else {
overload = "";
}
String functionParameterAndBody = String.format(
"%s = function (%s) {\n"
+ " console.log('%s is called'%s);\n"
+ " let ret = this.%s(%s);\n"
+ " console.log('%s ret value is ' + ret);\n"
+ " return ret;\n"
+ "};",
functionUntilImplementation, functionParametersString, methodName, logParametersString, callMethodName,
functionParametersString, methodName);
return generateClassSnippet(jMth.getJParent()) + "\n" + functionParameterAndBody;
List<String> argNames = collectMethodArgNames(jMth.getJavaMethod());
String args = String.join(", ", argNames);
String logArgs;
if (argNames.isEmpty()) {
logArgs = "";
} else {
logArgs = ": " + argNames.stream().map(arg -> arg + "=${" + arg + "}").collect(Collectors.joining(", "));
}
String shortClassName = mth.getParentClass().getShortName();
String classSnippet = generateClassSnippet(jMth.getJParent());
if (methodInfo.isConstructor() || methodInfo.getReturnType() == ArgType.VOID) {
// no return value
return classSnippet + "\n"
+ shortClassName + "[\"" + methodName + "\"]" + overload + ".implementation = function (" + args + ") {\n"
+ " console.log(`" + shortClassName + "." + newMethodName + " is called" + logArgs + "`);\n"
+ " this[\"" + methodName + "\"](" + args + ");\n"
+ "};";
}
return classSnippet + "\n"
+ shortClassName + "[\"" + methodName + "\"]" + overload + ".implementation = function (" + args + ") {\n"
+ " console.log(`" + shortClassName + "." + newMethodName + " is called" + logArgs + "`);\n"
+ " let result = this[\"" + methodName + "\"](" + args + ");\n"
+ " console.log(`" + shortClassName + "." + newMethodName + " result=${result}`);\n"
+ " return result;\n"
+ "};";
}
private List<String> collectMethodArgNames(JavaMethod javaMethod) {
@@ -137,6 +141,10 @@ public final class FridaAction extends JNodeAction {
}
return null;
});
int argsCount = javaMethod.getMethodNode().getMethodInfo().getArgsCount();
if (argNames.size() != argsCount) {
LOG.warn("Incorrect args count, expected: {}, got: {}", argsCount, argNames.size());
}
return argNames;
}
@@ -159,27 +167,24 @@ public final class FridaAction extends JNodeAction {
break;
}
}
JClass jc = jf.getRootClass();
String classSnippet = generateClassSnippet(jc);
return String.format("%s\n%s = %s.%s.value;", classSnippet, fieldName, jc.getName(), rawFieldName);
}
public Boolean isOverloaded(MethodNode methodNode) {
ClassNode parentClass = methodNode.getParentClass();
List<MethodNode> methods = parentClass.getMethods();
return methods.stream()
return methodNode.getParentClass().getMethods().stream()
.anyMatch(m -> m.getName().equals(methodNode.getName())
&& !Objects.equals(methodNode.getMethodInfo().getShortId(), m.getMethodInfo().getShortId()));
}
private String parseArgType(ArgType x) {
StringBuilder parsedArgType = new StringBuilder("'");
String typeStr;
if (x.isArray()) {
parsedArgType.append(TypeGen.signature(x).replace("/", "."));
typeStr = TypeGen.signature(x).replace("/", ".");
} else {
parsedArgType.append(x);
typeStr = x.toString();
}
return parsedArgType.append("'").toString();
return "'" + typeStr + "'";
}
}
@@ -361,7 +361,9 @@ public class ADBDialog extends JDialog implements ADB.DeviceStateListener, ADB.J
debugSetter.name,
debugSetter.device.getDeviceInfo().getAdbHost(),
debugSetter.forwardTcpPort,
debugSetter.ver);
debugSetter.ver,
debugSetter.device,
debugSetter.pid);
} catch (Exception e) {
LOG.error("Failed to attach to process", e);
return false;
@@ -55,6 +55,7 @@ import jadx.gui.utils.JNodeCache;
import jadx.gui.utils.JumpPosition;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.ui.NodeLabel;
import static javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED;
import static javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED;
@@ -402,9 +403,9 @@ public abstract class CommonSearchDialog extends JFrame {
}
protected final class ResultsTableCellRenderer implements TableCellRenderer {
private final JLabel label;
private final NodeLabel label;
private final RSyntaxTextArea codeArea;
private final JLabel emptyLabel;
private final NodeLabel emptyLabel;
private final Color codeSelectedColor;
private final Color codeBackground;
@@ -414,11 +415,11 @@ public abstract class CommonSearchDialog extends JFrame {
codeArea.setRows(1);
codeBackground = codeArea.getBackground();
codeSelectedColor = codeArea.getSelectionColor();
label = new JLabel();
label = new NodeLabel();
label.setOpaque(true);
label.setFont(codeArea.getFont());
label.setHorizontalAlignment(SwingConstants.LEFT);
emptyLabel = new JLabel();
emptyLabel = new NodeLabel();
emptyLabel.setOpaque(true);
}
@@ -453,8 +454,9 @@ public abstract class CommonSearchDialog extends JFrame {
private Component makeCell(JNode node, int column) {
if (column == 0) {
label.disableHtml(node.disableHtml());
label.setText(node.makeLongStringHtml());
label.setToolTipText(label.getText());
label.setToolTipText(node.getTooltip());
label.setIcon(node.getIcon());
return label;
}

Some files were not shown because too many files have changed in this diff Show More