Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 158fc2fca3 | |||
| 24284a6f3a | |||
| 85c2c63aa3 | |||
| f354f7de63 | |||
| 540c0a8100 | |||
| 4d00fede56 | |||
| b1bc5c08ff | |||
| 305d4f4fe5 | |||
| 2d149e9a5d | |||
| 87b9ff3c35 | |||
| 1c36b3c74c | |||
| 068e4b8e3d | |||
| df38a6424f | |||
| 5d186e56a5 | |||
| 0fafcfa006 | |||
| e3fdbafd86 | |||
| 07c2b14479 | |||
| cdc844aaf3 | |||
| e1b7d361b9 | |||
| 12ef29bebc | |||
| 22ed241d50 | |||
| 28e5a3c5be | |||
| bb4d88cc68 | |||
| 4aaea2b93f | |||
| bc8d7c4fc3 | |||
| 5ea6c46778 | |||
| b28f8ba85b | |||
| 4db50fb749 | |||
| 1dd0c90a04 | |||
| 2bace2bde2 | |||
| 1a9cb832ab | |||
| 6844a46c93 | |||
| e9e45707da | |||
| b9d02ff4c4 | |||
| 29b64300bc | |||
| 777355e86e | |||
| 620a177ce8 | |||
| 683c2dfbeb | |||
| 266cbcc6f4 | |||
| 8a45602ae6 | |||
| 711419a797 | |||
| 603f3057eb | |||
| fa6fc1f871 | |||
| 49fa320989 | |||
| 2f301bf150 | |||
| b4892ce17f | |||
| 151c171616 | |||
| 79477a2de3 | |||
| 78aadda931 | |||
| b50706505f | |||
| 9114821fb1 | |||
| 1195582da8 | |||
| 258987b0ff | |||
| a6a734c70d | |||
| d6c23a2a9b | |||
| db028904d7 |
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
+9
-23
@@ -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;
|
||||
}
|
||||
}
|
||||
+25
@@ -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();
|
||||
|
||||
+4
-9
@@ -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
@@ -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
Reference in New Issue
Block a user