Compare commits
31 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 |
@@ -59,7 +59,7 @@ jobs:
|
||||
- name: Set up JDK
|
||||
uses: oracle-actions/setup-java@v1
|
||||
with:
|
||||
release: 18
|
||||
release: 17
|
||||
|
||||
- name: Print Java version
|
||||
shell: bash
|
||||
|
||||
+10
-10
@@ -1,6 +1,6 @@
|
||||
plugins {
|
||||
id 'com.github.ben-manes.versions' version '0.43.0'
|
||||
id 'com.diffplug.spotless' version '6.11.0'
|
||||
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:2.0.3'
|
||||
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.3.4'
|
||||
testImplementation 'ch.qos.logback:logback-classic:1.3.5'
|
||||
testImplementation 'org.hamcrest:hamcrest-library:2.2'
|
||||
testImplementation 'org.mockito:mockito-core:4.8.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.1'
|
||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.1'
|
||||
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.3.4'
|
||||
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.3.1-8691043'
|
||||
implementation 'com.google.protobuf:protobuf-java:3.21.8' // forcing latest version
|
||||
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.31.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
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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()));
|
||||
@@ -835,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;
|
||||
|
||||
@@ -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,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 + '>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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
|
||||
@@ -1,32 +1,32 @@
|
||||
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.3.4'
|
||||
implementation 'ch.qos.logback:logback-classic:1.3.5'
|
||||
|
||||
implementation 'com.fifesoft:rsyntaxtextarea:3.3.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.6'
|
||||
implementation 'com.formdev:flatlaf-intellij-themes:2.6'
|
||||
implementation 'com.formdev:flatlaf-extras:2.6'
|
||||
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.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.3.1'
|
||||
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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,6 +73,7 @@ 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;
|
||||
@@ -268,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;
|
||||
}
|
||||
|
||||
@@ -643,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));
|
||||
@@ -665,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);
|
||||
|
||||
@@ -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;
|
||||
@@ -293,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());
|
||||
@@ -314,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);
|
||||
@@ -346,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));
|
||||
@@ -377,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();
|
||||
@@ -530,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);
|
||||
});
|
||||
@@ -659,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;
|
||||
@@ -1422,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);
|
||||
@@ -1500,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;
|
||||
}
|
||||
@@ -1515,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());
|
||||
|
||||
@@ -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 + "'";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,10 +454,10 @@ 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(node.getTooltip());
|
||||
label.setIcon(node.getIcon());
|
||||
label.disableHtml(node.disableHtml());
|
||||
return label;
|
||||
}
|
||||
if (!node.hasDescString()) {
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
package jadx.gui.ui.dialog;
|
||||
|
||||
import java.awt.Component;
|
||||
import java.awt.HeadlessException;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.swing.JDialog;
|
||||
import javax.swing.JFileChooser;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.filechooser.FileNameExtensionFilter;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.api.plugins.utils.CommonFileUtils;
|
||||
import jadx.core.utils.Utils;
|
||||
import jadx.core.utils.files.FileUtils;
|
||||
import jadx.gui.settings.JadxProject;
|
||||
import jadx.gui.ui.MainWindow;
|
||||
import jadx.gui.utils.NLS;
|
||||
|
||||
public class FileDialog {
|
||||
|
||||
public enum OpenMode {
|
||||
OPEN,
|
||||
OPEN_PROJECT,
|
||||
ADD,
|
||||
SAVE_PROJECT,
|
||||
EXPORT,
|
||||
CUSTOM_SAVE,
|
||||
CUSTOM_OPEN
|
||||
}
|
||||
|
||||
private final MainWindow mainWindow;
|
||||
|
||||
private boolean isOpen;
|
||||
private String title;
|
||||
private List<String> fileExtList;
|
||||
private int selectionMode = JFileChooser.FILES_AND_DIRECTORIES;
|
||||
private @Nullable Path currentDir;
|
||||
private @Nullable Path selectedFile;
|
||||
|
||||
public FileDialog(MainWindow mainWindow, OpenMode mode) {
|
||||
this.mainWindow = mainWindow;
|
||||
initForMode(mode);
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public void setFileExtList(List<String> fileExtList) {
|
||||
this.fileExtList = fileExtList;
|
||||
}
|
||||
|
||||
public void setSelectionMode(int selectionMode) {
|
||||
this.selectionMode = selectionMode;
|
||||
}
|
||||
|
||||
public void setSelectedFile(Path path) {
|
||||
this.selectedFile = path;
|
||||
}
|
||||
|
||||
public void setCurrentDir(Path currentDir) {
|
||||
this.currentDir = currentDir;
|
||||
}
|
||||
|
||||
public List<Path> show() {
|
||||
FileChooser fileChooser = buildFileChooser();
|
||||
int ret = isOpen ? fileChooser.showOpenDialog(mainWindow) : fileChooser.showSaveDialog(mainWindow);
|
||||
if (ret != JFileChooser.APPROVE_OPTION) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
currentDir = fileChooser.getCurrentDirectory().toPath();
|
||||
File[] selectedFiles = fileChooser.getSelectedFiles();
|
||||
if (selectedFiles.length != 0) {
|
||||
return FileUtils.toPaths(selectedFiles);
|
||||
}
|
||||
File chosenFile = fileChooser.getSelectedFile();
|
||||
if (chosenFile != null) {
|
||||
return Collections.singletonList(chosenFile.toPath());
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public Path getCurrentDir() {
|
||||
return currentDir;
|
||||
}
|
||||
|
||||
private void initForMode(OpenMode mode) {
|
||||
switch (mode) {
|
||||
case OPEN:
|
||||
case OPEN_PROJECT:
|
||||
case ADD:
|
||||
if (mode == OpenMode.OPEN_PROJECT) {
|
||||
fileExtList = Collections.singletonList(JadxProject.PROJECT_EXTENSION);
|
||||
title = NLS.str("file.open_title");
|
||||
} else {
|
||||
fileExtList = new ArrayList<>(Arrays.asList("apk", "dex", "jar", "class", "smali", "zip", "xapk", "aar", "arsc"));
|
||||
if (mode == OpenMode.OPEN) {
|
||||
fileExtList.addAll(Arrays.asList(JadxProject.PROJECT_EXTENSION, "aab"));
|
||||
title = NLS.str("file.open_title");
|
||||
} else {
|
||||
title = NLS.str("file.add_files_action");
|
||||
}
|
||||
}
|
||||
selectionMode = JFileChooser.FILES_AND_DIRECTORIES;
|
||||
currentDir = mainWindow.getSettings().getLastOpenFilePath();
|
||||
isOpen = true;
|
||||
break;
|
||||
|
||||
case SAVE_PROJECT:
|
||||
title = NLS.str("file.save_project");
|
||||
fileExtList = Collections.singletonList(JadxProject.PROJECT_EXTENSION);
|
||||
selectionMode = JFileChooser.FILES_ONLY;
|
||||
currentDir = mainWindow.getSettings().getLastSaveFilePath();
|
||||
isOpen = false;
|
||||
break;
|
||||
|
||||
case EXPORT:
|
||||
title = NLS.str("file.save_all_msg");
|
||||
fileExtList = Collections.emptyList();
|
||||
selectionMode = JFileChooser.DIRECTORIES_ONLY;
|
||||
currentDir = mainWindow.getSettings().getLastSaveFilePath();
|
||||
isOpen = false;
|
||||
break;
|
||||
|
||||
case CUSTOM_SAVE:
|
||||
isOpen = false;
|
||||
break;
|
||||
|
||||
case CUSTOM_OPEN:
|
||||
isOpen = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private FileChooser buildFileChooser() {
|
||||
FileChooser fileChooser = new FileChooser(currentDir);
|
||||
fileChooser.setToolTipText(title);
|
||||
fileChooser.setFileSelectionMode(selectionMode);
|
||||
fileChooser.setMultiSelectionEnabled(isOpen);
|
||||
fileChooser.setAcceptAllFileFilterUsed(true);
|
||||
if (Utils.notEmpty(fileExtList)) {
|
||||
String description = NLS.str("file_dialog.supported_files") + ": (" + Utils.listToString(fileExtList) + ')';
|
||||
fileChooser.setFileFilter(new FileNameExtensionFilter(description, fileExtList.toArray(new String[0])));
|
||||
}
|
||||
if (selectedFile != null) {
|
||||
fileChooser.setSelectedFile(selectedFile.toFile());
|
||||
}
|
||||
return fileChooser;
|
||||
}
|
||||
|
||||
private class FileChooser extends JFileChooser {
|
||||
|
||||
public FileChooser(@Nullable Path currentDirectory) {
|
||||
super(currentDirectory == null ? CommonFileUtils.CWD : currentDirectory.toFile());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JDialog createDialog(Component parent) throws HeadlessException {
|
||||
JDialog dialog = super.createDialog(parent);
|
||||
dialog.setTitle(title);
|
||||
dialog.setLocationRelativeTo(null);
|
||||
mainWindow.getSettings().loadWindowPos(dialog);
|
||||
dialog.addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosed(WindowEvent e) {
|
||||
mainWindow.getSettings().saveWindowPos(dialog);
|
||||
super.windowClosed(e);
|
||||
}
|
||||
});
|
||||
return dialog;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void approveSelection() {
|
||||
if (selectionMode == FILES_AND_DIRECTORIES) {
|
||||
File currentFile = getSelectedFile();
|
||||
if (currentFile.isDirectory()) {
|
||||
int option = JOptionPane.showConfirmDialog(
|
||||
mainWindow,
|
||||
NLS.str("file_dialog.load_dir_confirm") + "\n " + currentFile,
|
||||
NLS.str("file_dialog.load_dir_title"),
|
||||
JOptionPane.YES_NO_OPTION);
|
||||
if (option != JOptionPane.YES_OPTION) {
|
||||
this.setCurrentDirectory(currentFile);
|
||||
this.updateUI();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
super.approveSelection();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package jadx.gui.ui.filedialog;
|
||||
|
||||
import java.awt.Component;
|
||||
import java.awt.HeadlessException;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.swing.JDialog;
|
||||
import javax.swing.JFileChooser;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.UIManager;
|
||||
import javax.swing.filechooser.FileNameExtensionFilter;
|
||||
|
||||
import jadx.api.plugins.utils.CommonFileUtils;
|
||||
import jadx.core.utils.Utils;
|
||||
import jadx.core.utils.files.FileUtils;
|
||||
import jadx.gui.ui.MainWindow;
|
||||
import jadx.gui.utils.NLS;
|
||||
|
||||
class CustomFileChooser extends JFileChooser {
|
||||
|
||||
static {
|
||||
// disable left shortcut panel, can crush in "Win32ShellFolderManager2.getNetwork()" or similar call
|
||||
UIManager.put("FileChooser.noPlacesBar", Boolean.TRUE);
|
||||
}
|
||||
|
||||
private final FileDialogWrapper data;
|
||||
|
||||
public CustomFileChooser(FileDialogWrapper data) {
|
||||
super(data.getCurrentDir() == null ? CommonFileUtils.CWD : data.getCurrentDir().toFile());
|
||||
putClientProperty("FileChooser.useShellFolder", Boolean.FALSE);
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public List<Path> showDialog() {
|
||||
setToolTipText(data.getTitle());
|
||||
setFileSelectionMode(data.getSelectionMode());
|
||||
setMultiSelectionEnabled(data.isOpen());
|
||||
setAcceptAllFileFilterUsed(true);
|
||||
List<String> fileExtList = data.getFileExtList();
|
||||
if (Utils.notEmpty(fileExtList)) {
|
||||
String description = NLS.str("file_dialog.supported_files") + ": (" + Utils.listToString(fileExtList) + ')';
|
||||
setFileFilter(new FileNameExtensionFilter(description, fileExtList.toArray(new String[0])));
|
||||
}
|
||||
if (data.getSelectedFile() != null) {
|
||||
setSelectedFile(data.getSelectedFile().toFile());
|
||||
}
|
||||
MainWindow mainWindow = data.getMainWindow();
|
||||
int ret = data.isOpen() ? showOpenDialog(mainWindow) : showSaveDialog(mainWindow);
|
||||
if (ret != JFileChooser.APPROVE_OPTION) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
data.setCurrentDir(getCurrentDirectory().toPath());
|
||||
File[] selectedFiles = getSelectedFiles();
|
||||
if (selectedFiles.length != 0) {
|
||||
return FileUtils.toPaths(selectedFiles);
|
||||
}
|
||||
File chosenFile = getSelectedFile();
|
||||
if (chosenFile != null) {
|
||||
return Collections.singletonList(chosenFile.toPath());
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JDialog createDialog(Component parent) throws HeadlessException {
|
||||
JDialog dialog = super.createDialog(parent);
|
||||
dialog.setTitle(data.getTitle());
|
||||
dialog.setLocationRelativeTo(null);
|
||||
data.getMainWindow().getSettings().loadWindowPos(dialog);
|
||||
dialog.addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosed(WindowEvent e) {
|
||||
data.getMainWindow().getSettings().saveWindowPos(dialog);
|
||||
super.windowClosed(e);
|
||||
}
|
||||
});
|
||||
return dialog;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void approveSelection() {
|
||||
if (data.getSelectionMode() == FILES_AND_DIRECTORIES) {
|
||||
File currentFile = getSelectedFile();
|
||||
if (currentFile.isDirectory()) {
|
||||
int option = JOptionPane.showConfirmDialog(
|
||||
data.getMainWindow(),
|
||||
NLS.str("file_dialog.load_dir_confirm") + "\n " + currentFile,
|
||||
NLS.str("file_dialog.load_dir_title"),
|
||||
JOptionPane.YES_NO_OPTION);
|
||||
if (option != JOptionPane.YES_OPTION) {
|
||||
this.setCurrentDirectory(currentFile);
|
||||
this.updateUI();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
super.approveSelection();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package jadx.gui.ui.filedialog;
|
||||
|
||||
import java.awt.FileDialog;
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import jadx.core.utils.ListUtils;
|
||||
import jadx.core.utils.Utils;
|
||||
import jadx.core.utils.files.FileUtils;
|
||||
|
||||
class CustomFileDialog {
|
||||
|
||||
private final FileDialogWrapper data;
|
||||
|
||||
public CustomFileDialog(FileDialogWrapper data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public List<Path> showDialog() {
|
||||
FileDialog fileDialog = new FileDialog(data.getMainWindow(), data.getTitle());
|
||||
fileDialog.setMode(data.isOpen() ? FileDialog.LOAD : FileDialog.SAVE);
|
||||
fileDialog.setMultipleMode(true);
|
||||
List<String> fileExtList = data.getFileExtList();
|
||||
if (Utils.notEmpty(fileExtList)) {
|
||||
fileDialog.setFilenameFilter((dir, name) -> ListUtils.anyMatch(fileExtList, name::endsWith));
|
||||
}
|
||||
if (data.getSelectedFile() != null) {
|
||||
fileDialog.setFile(data.getSelectedFile().toAbsolutePath().toString());
|
||||
}
|
||||
if (data.getCurrentDir() != null) {
|
||||
fileDialog.setDirectory(data.getCurrentDir().toAbsolutePath().toString());
|
||||
}
|
||||
fileDialog.setVisible(true);
|
||||
File[] selectedFiles = fileDialog.getFiles();
|
||||
if (!Utils.isEmpty(selectedFiles)) {
|
||||
data.setCurrentDir(Paths.get(fileDialog.getDirectory()));
|
||||
return FileUtils.toPaths(selectedFiles);
|
||||
}
|
||||
if (fileDialog.getFile() != null) {
|
||||
return Collections.singletonList(Paths.get(fileDialog.getFile()));
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package jadx.gui.ui.filedialog;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.swing.JFileChooser;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.gui.settings.JadxProject;
|
||||
import jadx.gui.ui.MainWindow;
|
||||
import jadx.gui.utils.NLS;
|
||||
|
||||
public class FileDialogWrapper {
|
||||
|
||||
private final MainWindow mainWindow;
|
||||
|
||||
private boolean isOpen;
|
||||
private String title;
|
||||
private List<String> fileExtList;
|
||||
private int selectionMode = JFileChooser.FILES_AND_DIRECTORIES;
|
||||
private @Nullable Path currentDir;
|
||||
private @Nullable Path selectedFile;
|
||||
|
||||
public FileDialogWrapper(MainWindow mainWindow, FileOpenMode mode) {
|
||||
this.mainWindow = mainWindow;
|
||||
initForMode(mode);
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public void setFileExtList(List<String> fileExtList) {
|
||||
this.fileExtList = fileExtList;
|
||||
}
|
||||
|
||||
public void setSelectionMode(int selectionMode) {
|
||||
this.selectionMode = selectionMode;
|
||||
}
|
||||
|
||||
public void setSelectedFile(Path path) {
|
||||
this.selectedFile = path;
|
||||
}
|
||||
|
||||
public void setCurrentDir(Path currentDir) {
|
||||
this.currentDir = currentDir;
|
||||
}
|
||||
|
||||
public List<Path> show() {
|
||||
if (mainWindow.getSettings().isUseAlternativeFileDialog()) {
|
||||
return new CustomFileDialog(this).showDialog();
|
||||
} else {
|
||||
return new CustomFileChooser(this).showDialog();
|
||||
}
|
||||
}
|
||||
|
||||
private void initForMode(FileOpenMode mode) {
|
||||
switch (mode) {
|
||||
case OPEN:
|
||||
case OPEN_PROJECT:
|
||||
case ADD:
|
||||
if (mode == FileOpenMode.OPEN_PROJECT) {
|
||||
fileExtList = Collections.singletonList(JadxProject.PROJECT_EXTENSION);
|
||||
title = NLS.str("file.open_title");
|
||||
} else {
|
||||
fileExtList = new ArrayList<>(Arrays.asList("apk", "dex", "jar", "class", "smali", "zip", "xapk", "aar", "arsc"));
|
||||
if (mode == FileOpenMode.OPEN) {
|
||||
fileExtList.addAll(Arrays.asList(JadxProject.PROJECT_EXTENSION, "aab"));
|
||||
title = NLS.str("file.open_title");
|
||||
} else {
|
||||
title = NLS.str("file.add_files_action");
|
||||
}
|
||||
}
|
||||
selectionMode = JFileChooser.FILES_AND_DIRECTORIES;
|
||||
currentDir = mainWindow.getSettings().getLastOpenFilePath();
|
||||
isOpen = true;
|
||||
break;
|
||||
|
||||
case SAVE_PROJECT:
|
||||
title = NLS.str("file.save_project");
|
||||
fileExtList = Collections.singletonList(JadxProject.PROJECT_EXTENSION);
|
||||
selectionMode = JFileChooser.FILES_ONLY;
|
||||
currentDir = mainWindow.getSettings().getLastSaveFilePath();
|
||||
isOpen = false;
|
||||
break;
|
||||
|
||||
case EXPORT:
|
||||
title = NLS.str("file.save_all_msg");
|
||||
fileExtList = Collections.emptyList();
|
||||
selectionMode = JFileChooser.DIRECTORIES_ONLY;
|
||||
currentDir = mainWindow.getSettings().getLastSaveFilePath();
|
||||
isOpen = false;
|
||||
break;
|
||||
|
||||
case CUSTOM_SAVE:
|
||||
isOpen = false;
|
||||
currentDir = mainWindow.getSettings().getLastSaveFilePath();
|
||||
break;
|
||||
|
||||
case CUSTOM_OPEN:
|
||||
isOpen = true;
|
||||
currentDir = mainWindow.getSettings().getLastOpenFilePath();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public Path getCurrentDir() {
|
||||
return currentDir;
|
||||
}
|
||||
|
||||
public MainWindow getMainWindow() {
|
||||
return mainWindow;
|
||||
}
|
||||
|
||||
public boolean isOpen() {
|
||||
return isOpen;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public List<String> getFileExtList() {
|
||||
return fileExtList;
|
||||
}
|
||||
|
||||
public int getSelectionMode() {
|
||||
return selectionMode;
|
||||
}
|
||||
|
||||
public Path getSelectedFile() {
|
||||
return selectedFile;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package jadx.gui.ui.filedialog;
|
||||
|
||||
public enum FileOpenMode {
|
||||
OPEN,
|
||||
OPEN_PROJECT,
|
||||
ADD,
|
||||
SAVE_PROJECT,
|
||||
EXPORT,
|
||||
CUSTOM_SAVE,
|
||||
CUSTOM_OPEN
|
||||
}
|
||||
@@ -94,6 +94,8 @@ public class LogcatPanel extends JPanel {
|
||||
};
|
||||
|
||||
public boolean showLogcat() {
|
||||
this.removeAll();
|
||||
|
||||
ArrayList<String> pkgs = new ArrayList<>();
|
||||
pids = new ArrayList<>();
|
||||
JPanel procBox;
|
||||
|
||||
@@ -2,17 +2,22 @@ package jadx.gui.ui.treenodes;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.ImageIcon;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.text.StringEscapeUtils;
|
||||
|
||||
import jadx.api.ICodeInfo;
|
||||
import jadx.api.ResourceFile;
|
||||
import jadx.api.impl.SimpleCodeInfo;
|
||||
import jadx.core.dex.attributes.IAttributeNode;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
@@ -75,7 +80,7 @@ public class SummaryNode extends JNode {
|
||||
List<String> codeSources = classes.stream()
|
||||
.map(ClassNode::getInputFileName)
|
||||
.distinct()
|
||||
.sorted()
|
||||
.sorted(Comparator.naturalOrder())
|
||||
.collect(Collectors.toList());
|
||||
codeSources.remove("synthetic");
|
||||
int codeSourcesCount = codeSources.size();
|
||||
@@ -84,17 +89,15 @@ public class SummaryNode extends JNode {
|
||||
if (codeSourcesCount != 1) {
|
||||
builder.append("<li>Count: " + codeSourcesCount + "</li>");
|
||||
}
|
||||
// dex files list
|
||||
codeSources.removeIf(f -> !f.endsWith(".dex"));
|
||||
if (!codeSources.isEmpty()) {
|
||||
for (String input : codeSources) {
|
||||
builder.append("<li>");
|
||||
builder.escape(input);
|
||||
builder.append("</li>");
|
||||
}
|
||||
for (String input : codeSources) {
|
||||
builder.append("<li>");
|
||||
builder.escape(input);
|
||||
builder.append("</li>");
|
||||
}
|
||||
builder.append("</ul>");
|
||||
|
||||
addNativeLibsInfo(builder);
|
||||
|
||||
int methodsCount = classes.stream().mapToInt(cls -> cls.getMethods().size()).sum();
|
||||
int fieldsCount = classes.stream().mapToInt(cls -> cls.getFields().size()).sum();
|
||||
int insnCount = classes.stream().flatMap(cls -> cls.getMethods().stream()).mapToInt(MethodNode::getInsnsCount).sum();
|
||||
@@ -107,6 +110,50 @@ public class SummaryNode extends JNode {
|
||||
builder.append("</ul>");
|
||||
}
|
||||
|
||||
private void addNativeLibsInfo(StringEscapeUtils.Builder builder) {
|
||||
List<String> nativeLibs = wrapper.getResources().stream()
|
||||
.map(ResourceFile::getOriginalName)
|
||||
.filter(f -> f.endsWith(".so"))
|
||||
.sorted(Comparator.naturalOrder())
|
||||
.collect(Collectors.toList());
|
||||
builder.append("<h3>Native libs</h3>");
|
||||
builder.append("<ul>");
|
||||
if (nativeLibs.isEmpty()) {
|
||||
builder.append("<li>Total count: 0</li>");
|
||||
} else {
|
||||
Map<String, Set<String>> libsByArch = new HashMap<>();
|
||||
for (String libFile : nativeLibs) {
|
||||
String[] parts = StringUtils.split(libFile, '/');
|
||||
int count = parts.length;
|
||||
if (count >= 2) {
|
||||
String arch = parts[count - 2];
|
||||
String name = parts[count - 1];
|
||||
libsByArch.computeIfAbsent(arch, (a) -> new HashSet<>())
|
||||
.add(name);
|
||||
}
|
||||
}
|
||||
String arches = libsByArch.keySet().stream()
|
||||
.sorted(Comparator.naturalOrder())
|
||||
.collect(Collectors.joining(", "));
|
||||
builder.append("<li>Arch list: " + arches + "</li>");
|
||||
|
||||
String perArchCount = libsByArch.entrySet().stream()
|
||||
.map(entry -> entry.getKey() + ":" + entry.getValue().size())
|
||||
.sorted(Comparator.naturalOrder())
|
||||
.collect(Collectors.joining(", "));
|
||||
builder.append("<li>Per arch count: " + perArchCount + "</li>");
|
||||
|
||||
builder.append("<br>");
|
||||
builder.append("<li>Total count: " + nativeLibs.size() + "</li>");
|
||||
for (String lib : nativeLibs) {
|
||||
builder.append("<li>");
|
||||
builder.escape(lib);
|
||||
builder.append("</li>");
|
||||
}
|
||||
}
|
||||
builder.append("</ul>");
|
||||
}
|
||||
|
||||
private void writeDecompilationSummary(StringEscapeUtils.Builder builder) {
|
||||
builder.append("<h2>Decompilation</h2>");
|
||||
List<ClassNode> classes = wrapper.getRootNode().getClassesWithoutInner();
|
||||
|
||||
@@ -15,6 +15,8 @@ import com.formdev.flatlaf.FlatLaf;
|
||||
import com.formdev.flatlaf.FlatLightLaf;
|
||||
import com.formdev.flatlaf.extras.FlatAnimatedLafChange;
|
||||
import com.formdev.flatlaf.intellijthemes.FlatAllIJThemes;
|
||||
import com.formdev.flatlaf.themes.FlatMacDarkLaf;
|
||||
import com.formdev.flatlaf.themes.FlatMacLightLaf;
|
||||
|
||||
import jadx.gui.settings.JadxSettings;
|
||||
|
||||
@@ -68,6 +70,8 @@ public class LafManager {
|
||||
// default flatlaf themes
|
||||
map.put(FlatLightLaf.NAME, FlatLightLaf.class.getName());
|
||||
map.put(FlatDarkLaf.NAME, FlatDarkLaf.class.getName());
|
||||
map.put(FlatMacLightLaf.NAME, FlatMacLightLaf.class.getName());
|
||||
map.put(FlatMacDarkLaf.NAME, FlatMacDarkLaf.class.getName());
|
||||
map.put(FlatIntelliJLaf.NAME, FlatIntelliJLaf.class.getName());
|
||||
map.put(FlatDarculaLaf.NAME, FlatDarculaLaf.class.getName());
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ public class NLS {
|
||||
LANG_LOCALES.add(new LangLocale("de", "DE"));
|
||||
LANG_LOCALES.add(new LangLocale("ko", "KR"));
|
||||
LANG_LOCALES.add(new LangLocale("pt", "BR"));
|
||||
LANG_LOCALES.add(new LangLocale("ru", "RU"));
|
||||
|
||||
LANG_LOCALES.forEach(NLS::load);
|
||||
|
||||
|
||||
@@ -144,6 +144,7 @@ preferences.other=Andere
|
||||
preferences.language=Sprache
|
||||
preferences.lineNumbersMode=Editor Zeilennummern-Modus
|
||||
preferences.jumpOnDoubleClick=Sprung bei Doppelklick aktivieren
|
||||
#preferences.useAlternativeFileDialog=Use alternative file dialog
|
||||
preferences.check_for_updates=Nach Updates beim Start suchen
|
||||
preferences.useDx=dx/d8 zur Konvertierung von Java Bytecode verwenden
|
||||
preferences.decompilationMode=Dekompilierungsmodus
|
||||
|
||||
@@ -144,6 +144,7 @@ preferences.other=Other
|
||||
preferences.language=Language
|
||||
preferences.lineNumbersMode=Editor line numbers mode
|
||||
preferences.jumpOnDoubleClick=Enable jump on double click
|
||||
preferences.useAlternativeFileDialog=Use alternative file dialog
|
||||
preferences.check_for_updates=Check for updates on startup
|
||||
preferences.useDx=Use dx/d8 to convert java bytecode
|
||||
preferences.decompilationMode=Decompilation mode
|
||||
|
||||
@@ -144,6 +144,7 @@ preferences.other=Otros
|
||||
preferences.language=Idioma
|
||||
#preferences.lineNumbersMode=Editor line numbers mode
|
||||
#preferences.jumpOnDoubleClick=Enable jump on double click
|
||||
#preferences.useAlternativeFileDialog=Use alternative file dialog
|
||||
preferences.check_for_updates=Buscar actualizaciones al iniciar
|
||||
#preferences.useDx=Use dx/d8 to convert java bytecode
|
||||
#preferences.decompilationMode=Decompilation mode
|
||||
|
||||
@@ -144,6 +144,7 @@ preferences.other=기타
|
||||
preferences.language=언어
|
||||
preferences.lineNumbersMode=편집기 줄 번호 모드
|
||||
preferences.jumpOnDoubleClick=더블 클릭 시 점프 활성화
|
||||
#preferences.useAlternativeFileDialog=Use alternative file dialog
|
||||
preferences.check_for_updates=시작시 업데이트 확인
|
||||
preferences.useDx=dx/d8을 사용하여 Java 바이트 코드 변환
|
||||
preferences.decompilationMode=디컴파일 모드
|
||||
|
||||
@@ -144,6 +144,7 @@ preferences.other=Outro
|
||||
preferences.language=Idioma
|
||||
preferences.lineNumbersMode=Modo do contador de linhas do editor
|
||||
preferences.jumpOnDoubleClick=Ativar salto no duplo clique
|
||||
#preferences.useAlternativeFileDialog=Use alternative file dialog
|
||||
preferences.check_for_updates=Verificar por atualizações ao inicializar
|
||||
preferences.useDx=Usar dx/d8 para converter bytecode Java
|
||||
preferences.decompilationMode=Modo de descompilação
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
language.name=Русский
|
||||
|
||||
menu.file=Файл
|
||||
menu.view=Вид
|
||||
menu.recent_projects=Недавние проекты
|
||||
menu.no_recent_projects=Нет недавно открытых проектов
|
||||
menu.preferences=Параметры
|
||||
menu.sync=Синхр. файлы с редактором
|
||||
menu.flatten=Плоская структура пакетов
|
||||
menu.heapUsageBar=Использование ОЗУ
|
||||
menu.alwaysSelectOpened=Выбирать открытый файл/класс
|
||||
menu.navigation=Навигация
|
||||
menu.text_search=Поиск строк
|
||||
menu.class_search=Поиск классов
|
||||
menu.comment_search=Поиск комментариев
|
||||
menu.tools=Инструменты
|
||||
menu.deobfuscation=Деобфускация
|
||||
menu.log=Просмотр логов
|
||||
menu.help=Помощь
|
||||
menu.about=О программе
|
||||
menu.update_label=Версия %s уже доступна!
|
||||
|
||||
file.open_action=Открыть файлы...
|
||||
file.add_files_action=Добавить файл
|
||||
file.open_title=Открыть файл
|
||||
file.open_project=Открыть проект
|
||||
file.new_project=Новый проект
|
||||
file.save_project=Сохранить проект
|
||||
file.save_project_as=Сохранить проект как...
|
||||
file.reload=Пересканировать файлы
|
||||
file.live_reload=Автосканирование
|
||||
file.live_reload_desc=Автоматически перезагружать файлы при внешних изменениях
|
||||
file.export_mappings_as=Экспортировать маппинги...
|
||||
file.save_all=Сохранить все
|
||||
file.export_gradle=Сохранить как Gradle проект
|
||||
file.save_all_msg=Выбрать папку для декомпилированных проектов
|
||||
file.exit=Выход
|
||||
|
||||
start_page.title=Начальная страница
|
||||
start_page.start=Начать
|
||||
start_page.recent=Недавние проекты
|
||||
|
||||
tree.sources_title=Код
|
||||
tree.resources_title=Ресурсы
|
||||
tree.loading=Загрузка...
|
||||
|
||||
progress.load=Загрузка
|
||||
progress.export_mappings=Экспорт таблицы маппингов
|
||||
progress.decompile=Декомпиляция
|
||||
progress.canceling=Прерывание
|
||||
|
||||
error_dialog.title=Ошибка
|
||||
|
||||
search.previous=Назад
|
||||
search.next=Вперед
|
||||
search.mark_all=Выбрать все
|
||||
search.regex=Регулярные выражения
|
||||
search.match_case=Учитывать регистр
|
||||
search.whole_word=Поиск по словам
|
||||
search.find=Найти
|
||||
|
||||
tabs.copy_class_name=Копировать имя
|
||||
tabs.close=Закрыть
|
||||
tabs.closeOthers=Закрыть другие
|
||||
tabs.closeAll=Закрыть все
|
||||
tabs.code=Код
|
||||
tabs.smali=Smali
|
||||
|
||||
nav.back=Назад
|
||||
nav.forward=Вперед
|
||||
|
||||
message.taskTimeout=Задача отменена по таймауту в %d мс.
|
||||
message.userCancelTask=Задача прервана пользователем.
|
||||
message.memoryLow=Jadx запущен с малым количеством доступной оперативной памяти. Перезапустите увеличив Heap Size
|
||||
message.taskError=Выполнение задачи закончено с ошибкой (смотрите лог для подробностей).
|
||||
message.errorTitle=Ошибка
|
||||
message.load_errors=Ошибка прогрузки.\nКоличество ошибок: %d\nНажмите OK, чтобы открыть лог.
|
||||
message.no_classes=Классы не найдены, нечего декомпилировать!
|
||||
|
||||
message.saveIncomplete=<html>Сохранение не завершено.<br> %s<br> %d классов или ресурсов не сохранено!</html>
|
||||
message.indexIncomplete=<html>Индексирование некоторых классов пропущено.<br> %s<br> %d классов не индексировано, и не будет отображаться в результатах поиска!</html>
|
||||
message.indexingClassesSkipped=<html>JaDX запущен с малым количеством ОЗУ. %d классов не индексировано.<br>Если вы хотите их индексировать, перезапустите JaDX с большим Heap Size.</html>
|
||||
|
||||
heapUsage.text=JADX использует: %.2f ГБ из %.2f ГБ
|
||||
|
||||
common_dialog.ok=Ok
|
||||
common_dialog.cancel=Отмена
|
||||
common_dialog.add=Добавить
|
||||
common_dialog.update=Обновить
|
||||
common_dialog.remove=Убрать
|
||||
|
||||
file_dialog.supported_files=Поддерж. файлы
|
||||
file_dialog.load_dir_title=Импортировать папку
|
||||
file_dialog.load_dir_confirm=Импортировать все файлы из директории?
|
||||
|
||||
search_dialog.open=Открыть
|
||||
search_dialog.cancel=Отмена
|
||||
search_dialog.open_by_name=Поиск по тексту:
|
||||
search_dialog.search_button=Поиск
|
||||
search_dialog.search_history=История поиска
|
||||
search_dialog.auto_search=Автопоиск
|
||||
search_dialog.search_in=Поиск вхождений:
|
||||
search_dialog.class=Класс
|
||||
search_dialog.method=Метод
|
||||
search_dialog.field=Поле
|
||||
search_dialog.code=Код
|
||||
search_dialog.options=Опции поиска:
|
||||
search_dialog.ignorecase=Игнорировать регистр
|
||||
search_dialog.load_more=Загрузить еще
|
||||
search_dialog.load_all=Загрузить все
|
||||
search_dialog.stop=Стоп
|
||||
search_dialog.results_incomplete=Найдено %d+
|
||||
search_dialog.results_complete=Найдено %d (поиск завершен)
|
||||
search_dialog.col_node=Вхождения
|
||||
search_dialog.col_code=Код
|
||||
search_dialog.sort_results=Сортировка результатов
|
||||
search_dialog.regex=Регулярные выражения
|
||||
search_dialog.active_tab=Только активные вкладки
|
||||
search_dialog.comments=Комментарии
|
||||
search_dialog.resource=Ресурсы
|
||||
search_dialog.keep_open=Оставлять поиск открытым
|
||||
search_dialog.tip_searching=Поиск...
|
||||
|
||||
usage_dialog.title=Поиск использований
|
||||
usage_dialog.label=Использования:
|
||||
|
||||
comment_dialog.title.add=Добавить комментарий
|
||||
comment_dialog.title.update=Обновить
|
||||
comment_dialog.label=Комментарий:
|
||||
comment_dialog.usage=Используйте Shift + Enter для переноса строки
|
||||
|
||||
log_viewer.title=Просмотр логов
|
||||
log_viewer.log_level=Уровень лога:
|
||||
|
||||
about_dialog.title=О программе JADX
|
||||
|
||||
preferences.title=Параметры
|
||||
preferences.deobfuscation=Деобфускация
|
||||
preferences.appearance=Внешний вид
|
||||
preferences.decompile=Декомпиляция
|
||||
preferences.plugins=Плагины
|
||||
preferences.project=Проект
|
||||
preferences.other=Прочее
|
||||
preferences.language=Язык
|
||||
preferences.lineNumbersMode=Тип переноса строк
|
||||
preferences.jumpOnDoubleClick=Переход по двойному клику
|
||||
preferences.useAlternativeFileDialog=Использовать альтернативный файлпикер
|
||||
preferences.check_for_updates=Проверять наличие новых версий
|
||||
preferences.useDx=DX/D8 для конвертации java байткода
|
||||
preferences.decompilationMode=Режим декомпиляции
|
||||
preferences.codeCacheMode=Кеширование кода
|
||||
preferences.showInconsistentCode=Показывать некорректный код
|
||||
preferences.escapeUnicode=Кодирование unicode
|
||||
preferences.replaceConsts=Замена констант
|
||||
preferences.respectBytecodeAccessModifiers=Исходные модификаторы доступа
|
||||
preferences.useImports=Использовать импорты
|
||||
preferences.useDebugInfo=Отладочная информация
|
||||
preferences.inlineAnonymous=Объединять анонимные классы
|
||||
preferences.inlineMethods=Объединять методы
|
||||
preferences.extractFinally=Вычленять finally блоки
|
||||
preferences.fsCaseSensitive=Учитывать регистр в файловой системе
|
||||
preferences.skipResourcesDecode=Не декодировать ресурсы
|
||||
preferences.useKotlinMethodsForVarNames=Kotlin методы как имена полей
|
||||
preferences.commentsLevel=Уровень лога операций
|
||||
preferences.autoSave=Автосохранение
|
||||
preferences.threads=Количество используемых потоков
|
||||
preferences.excludedPackages=Исключенные пакеты
|
||||
preferences.excludedPackages.tooltip=Список пакетов, которые не будут декомпилироваться и индексироваться (экономит ОЗУ)
|
||||
preferences.excludedPackages.button=Изменить
|
||||
preferences.excludedPackages.editDialog=<html>Список пакетов, которые не будут декомпилироваться и индексироваться (экономит ОЗУ)<br>например: <code>android.support</code><br>Разделитель - одинарный пробел</html>
|
||||
preferences.cfg=Методы генерации графиков CFG (в "dot" формате)
|
||||
preferences.raw_cfg=Генерировать необработанные графики CFG
|
||||
preferences.font=Шрифт редактора Java
|
||||
preferences.smali_font=Шрифт редактора smali
|
||||
preferences.laf_theme=Тема приложения
|
||||
preferences.theme=Тема редактора
|
||||
preferences.start_jobs=Автоматическая декомпиляция
|
||||
preferences.select_font=Изменить
|
||||
preferences.select_smali_font=Изменить
|
||||
preferences.deobfuscation_on=Включить деобфускацию
|
||||
preferences.deobfuscation_map_file_mode=Режим обработки маппингов
|
||||
preferences.deobfuscation_min_len=Минимальная длина имени
|
||||
preferences.deobfuscation_max_len=Максимальная длина имени
|
||||
preferences.deobfuscation_source_alias=Иcпользовать атрибут SOURCE
|
||||
preferences.deobfuscation_kotlin_metadata=Использовать метаданные Kotlin
|
||||
preferences.deobfuscation_res_name_source=Расшифровка имен ресурсов
|
||||
preferences.save=Сохранить
|
||||
preferences.cancel=Отмена
|
||||
preferences.reset=Сброс
|
||||
preferences.reset_message=Сбросить настройки на значения по умолчанию?
|
||||
preferences.reset_title=Сбросить настройки
|
||||
preferences.copy=Скопировать в буфер обмена
|
||||
preferences.copy_message=Все настройки скопированы в буфер обмена
|
||||
preferences.rename=Переименовать идентификаторы
|
||||
preferences.rename_case=И исправить проблемы именования
|
||||
preferences.rename_valid=И сделать их верными
|
||||
preferences.rename_printable=И сделать их доступными для печати
|
||||
preferences.search_group_title=Поиск
|
||||
preferences.search_results_per_page=Результатов на страницу (0 - без лимита)
|
||||
preferences.res_file_ext=Расширения файлов ресурсов ('xml|html', * для всех)
|
||||
preferences.res_skip_file=Пропускать ресурсы больше чем (в МБ)
|
||||
|
||||
msg.open_file=Пожалуйста, откройте файл
|
||||
msg.saving_sources=Сохранение ресурсов
|
||||
msg.language_changed_title=Язык изменен
|
||||
msg.language_changed=Новый язык применится при следующем запуске программы
|
||||
msg.project_error_title=Ошибка
|
||||
msg.project_error=Проект не может быть загружен
|
||||
msg.cmd_select_class_error=Ошибка выбора класса\n%s\nЭтот класс не существует.
|
||||
msg.cant_add_comment=Невозможно добавить комментарий сюда
|
||||
|
||||
popup.bytecode_col=Показать Dalvik байткод
|
||||
popup.line_wrap=Перенос строк
|
||||
popup.undo=Отменить
|
||||
popup.redo=Вернуть
|
||||
popup.cut=Вырезать
|
||||
popup.copy=Копировать
|
||||
popup.paste=Вставить
|
||||
popup.delete=Удалить
|
||||
popup.select_all=Выбрать все
|
||||
popup.frida=Копировать как хук frida
|
||||
popup.xposed=Копировать как хук Xposed
|
||||
popup.find_usage=Найти использования
|
||||
popup.go_to_declaration=Перейти к объявлению
|
||||
popup.exclude=Исключить
|
||||
popup.exclude_packages=Исключить пакеты
|
||||
popup.add_comment=Комментарий
|
||||
popup.search_comment=Поиск комментариев
|
||||
popup.rename=Переименовать
|
||||
popup.search=Найти "%s"
|
||||
popup.search_global=Глобальный поиск "%s"
|
||||
|
||||
exclude_dialog.title=Выбор пакетов
|
||||
exclude_dialog.ok=OK
|
||||
exclude_dialog.select_all=Выбрать все
|
||||
exclude_dialog.deselect=Убрать
|
||||
exclude_dialog.invert=Инвертировать
|
||||
|
||||
confirm.save_as_title=Подтверджение сохранения
|
||||
confirm.save_as_message=%s уже существует.\nВы хотите его перезаписать?
|
||||
confirm.not_saved_title=Сохранить проект
|
||||
confirm.not_saved_message=Сохранить текущий проект перед выходом?
|
||||
|
||||
certificate.cert_type=Type
|
||||
certificate.serialSigVer=Version
|
||||
certificate.serialNumber=Serial number
|
||||
certificate.cert_subject=Subject
|
||||
certificate.serialValidFrom=Valid from
|
||||
certificate.serialValidUntil=Valid until
|
||||
certificate.serialPubKeyType=Public key type
|
||||
certificate.serialPubKeyExponent=Exponent
|
||||
certificate.serialPubKeyModulus=Modulus
|
||||
certificate.serialPubKeyModulusSize=Modulus size (bits)
|
||||
certificate.serialSigType=Signature type
|
||||
certificate.serialSigOID=Signature OID
|
||||
certificate.serialMD5=MD5 Fingerprint
|
||||
certificate.serialSHA1=SHA-1 Fingerprint
|
||||
certificate.serialSHA256=SHA-256 Fingerprint
|
||||
certificate.serialPubKeyY=Y
|
||||
|
||||
apkSignature.signer=Signer
|
||||
apkSignature.verificationSuccess=Signature verification succeeded
|
||||
apkSignature.verificationFailed=Signature verification failed
|
||||
apkSignature.signatureSuccess=Valid APK signature v%d found
|
||||
apkSignature.signatureFailed=Invalid APK signature v%d found
|
||||
apkSignature.errors=Errors
|
||||
apkSignature.warnings=Warnings
|
||||
apkSignature.exception=APK verification failed
|
||||
apkSignature.unprotectedEntry=Files that are not protected by APK signature v1. Unauthorized modifications to these entries can only be detected by APK signature v2 and higher.
|
||||
|
||||
issues_panel.label=Проблемы:
|
||||
issues_panel.errors=%d ошибок
|
||||
issues_panel.warnings=%d предупреждений
|
||||
issues_panel.tooltip=Открыть просмотр логов
|
||||
|
||||
debugger.process_selector=Выбрать процесс для отладки
|
||||
debugger.step_into=Перейти (F7)
|
||||
debugger.step_over=Прыжок (F8)
|
||||
debugger.step_out=Выйти (Shift + F8)
|
||||
debugger.run=Запустить (F9)
|
||||
debugger.stop=Остановить
|
||||
debugger.pause=Пауза
|
||||
debugger.rerun=Повторить
|
||||
debugger.cfm_dialog_title=Выйти во время отладки
|
||||
debugger.cfm_dialog_msg=Вы действительно хотите остановить дебаггер?
|
||||
|
||||
debugger.popup_set_value=Задать значение
|
||||
debugger.popup_change_to_zero=Изменить на 0
|
||||
debugger.popup_change_to_one=Изменить на 1
|
||||
debugger.popup_copy_value=Копировать значение
|
||||
|
||||
logcat.pause=Остановить
|
||||
logcat.start=Возобновить
|
||||
logcat.clear=Очистить
|
||||
|
||||
logcat.error_fail_start=Ошибка запуска логгера
|
||||
logcat.process=Процесс
|
||||
logcat.level=Уровень лога
|
||||
logcat.default=По умолчанию
|
||||
logcat.verbose=Verbose
|
||||
logcat.debug=Debug
|
||||
logcat.info=Info
|
||||
logcat.warn=Warn
|
||||
logcat.error=Error
|
||||
logcat.fatal=Fatal
|
||||
logcat.silent=Silent
|
||||
logcat.logcat=Лог
|
||||
logcat.select_attached=Прикрепленный процесс
|
||||
logcat.select_all=Выделить все
|
||||
logcat.unselect_all=Отменить выделение
|
||||
|
||||
set_value_dialog.label_value=Значение
|
||||
set_value_dialog.btn_set=Присвоить
|
||||
set_value_dialog.title=Присвоить значение
|
||||
set_value_dialog.neg_msg=Невозможно присвоить значение
|
||||
set_value_dialog.sel_type=Выберите тип значения
|
||||
|
||||
adb_dialog.addr=Адрес
|
||||
adb_dialog.port=Порт
|
||||
adb_dialog.path=Путь к ADB
|
||||
adb_dialog.launch_app=Запустить приложение
|
||||
adb_dialog.start_server=Запустить ADB сервер
|
||||
adb_dialog.refresh=Обновить
|
||||
adb_dialog.tip_devices=%d устройств
|
||||
adb_dialog.device_node=Устройство
|
||||
adb_dialog.missing_path=Вы должны указать путь к ADB, для того чтобы запустить сервер
|
||||
adb_dialog.waiting=Ожидание подключения к ADB...
|
||||
adb_dialog.connecting=Подключение к ADB серверу %s на порту %s...
|
||||
adb_dialog.connect_okay=Подключено к серверу, адрес: %s:%s
|
||||
adb_dialog.connect_fail=Невозможно подключиться к ADB
|
||||
adb_dialog.disconnected=ADB сервер отключен
|
||||
adb_dialog.start_okay=ADB запущен на порту: %s.
|
||||
adb_dialog.start_fail=Ошибка запуска ADB сервера на порту: %s!
|
||||
adb_dialog.forward_fail=Не удалось перейти из-за неизвестной ошибки
|
||||
adb_dialog.being_debugged_msg=Похоже что к этому процессу уже подключен отладчик. Вы хотите продолжить?
|
||||
adb_dialog.unknown_android_ver=Ошибка парсинга версии Android. Использовать Android 8 по умолчанию?
|
||||
adb_dialog.being_debugged_title=Этот процесс уже занят
|
||||
adb_dialog.init_dbg_fail=Ошибка инициализации отладчика
|
||||
adb_dialog.msg_read_mani_fail=Ошибка парсинга AndroidManifest.xml
|
||||
adb_dialog.no_devices=Нет устройств для запуска приложения
|
||||
adb_dialog.restart_while_debugging_title=Перезапустить отладчик
|
||||
adb_dialog.restart_while_debugging_msg=Запущен процесс отдадки приложения. Вы действительно хотите перезапустить сессию?
|
||||
adb_dialog.starting_debugger=Запуск отладки...
|
||||
@@ -144,6 +144,7 @@ preferences.other=其他
|
||||
preferences.language=语言
|
||||
preferences.lineNumbersMode=编辑器行号模式
|
||||
preferences.jumpOnDoubleClick=启用双击跳转
|
||||
#preferences.useAlternativeFileDialog=Use alternative file dialog
|
||||
preferences.check_for_updates=启动时检查更新
|
||||
preferences.useDx=使用 dx/d8 来转换java字节码
|
||||
preferences.decompilationMode=反编译模式
|
||||
|
||||
@@ -144,6 +144,7 @@ preferences.other=其他
|
||||
preferences.language=語言
|
||||
preferences.lineNumbersMode=編輯器行號模式
|
||||
preferences.jumpOnDoubleClick=啟用點擊兩下時跳躍
|
||||
#preferences.useAlternativeFileDialog=Use alternative file dialog
|
||||
preferences.check_for_updates=啟動時檢查更新
|
||||
preferences.useDx=使用 dx/d8 來轉換 Java 位元組碼
|
||||
preferences.decompilationMode=反編譯模式
|
||||
|
||||
@@ -8,6 +8,8 @@ import org.slf4j.LoggerFactory;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.tests.api.SmaliTest;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class DbgSmaliTest extends SmaliTest {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DbgSmaliTest.class);
|
||||
|
||||
@@ -17,10 +19,23 @@ class DbgSmaliTest extends SmaliTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void test() {
|
||||
void testSwitch() {
|
||||
disableCompilation();
|
||||
ClassNode cls = getClassNodeFromSmali("switch", "SwitchTest");
|
||||
Smali disasm = Smali.disassemble(cls);
|
||||
LOG.debug("{}", disasm.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParams() {
|
||||
disableCompilation();
|
||||
ClassNode cls = getClassNodeFromSmali("params", "ParamsTest");
|
||||
Smali disasm = Smali.disassemble(cls);
|
||||
String code = disasm.getCode();
|
||||
LOG.debug("{}", code);
|
||||
assertThat(code)
|
||||
.doesNotContain("Failed to write method")
|
||||
.doesNotContain(".param p1")
|
||||
.contains(".local p1, \"arg0\":Landroid/widget/AdapterView;, \"Landroid/widget/AdapterView<*>;\"");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
.class LParamsTest;
|
||||
.super Ljava/lang/Object;
|
||||
|
||||
.method public test(Landroid/widget/AdapterView;Landroid/view/View;IJ)V
|
||||
.registers 10
|
||||
.param p2, "arg1" # Landroid/view/View;
|
||||
.param p3, "arg2" # I
|
||||
.param p4, "arg3" # J
|
||||
.annotation system Ldalvik/annotation/Signature;
|
||||
value = {
|
||||
"(",
|
||||
"Landroid/widget/AdapterView",
|
||||
"<*>;",
|
||||
"Landroid/view/View;",
|
||||
"IJ)V"
|
||||
}
|
||||
.end annotation
|
||||
|
||||
.prologue
|
||||
.line 69
|
||||
.local p1, "arg0":Landroid/widget/AdapterView;, "Landroid/widget/AdapterView<*>;"
|
||||
iget-object v2, p0, LParamsTest;->this$0:Ltest/ColorListActivity;
|
||||
|
||||
.line 72
|
||||
iget-object v2, v2, Ltest/ColorListActivity;->mSortedColorList:[Ljava/lang/String;
|
||||
|
||||
.line 75
|
||||
aget-object v0, v2, p3
|
||||
|
||||
.line 80
|
||||
.local v0, "colorString":Ljava/lang/String;
|
||||
new-instance v1, Landroid/content/Intent;
|
||||
|
||||
.line 83
|
||||
iget-object v2, p0, LParamsTest;->this$0:Ltest/ColorListActivity;
|
||||
|
||||
.line 86
|
||||
const-class v3, Ltest/ColorItemActivity;
|
||||
|
||||
.line 89
|
||||
invoke-direct {v1, v2, v3}, Landroid/content/Intent;-><init>(Landroid/content/Context;Ljava/lang/Class;)V
|
||||
|
||||
.line 94
|
||||
.local v1, "intent":Landroid/content/Intent;
|
||||
const-string v2, "colorString"
|
||||
|
||||
.line 97
|
||||
invoke-virtual {v1, v2, v0}, Landroid/content/Intent;->putExtra(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;
|
||||
|
||||
.line 101
|
||||
iget-object v2, p0, LParamsTest;->this$0:Ltest/ColorListActivity;
|
||||
|
||||
.line 104
|
||||
invoke-virtual {v2, v1}, Ltest/ColorListActivity;->startActivity(Landroid/content/Intent;)V
|
||||
|
||||
.line 108
|
||||
return-void
|
||||
.end method
|
||||
+3
-2
@@ -279,8 +279,9 @@ public class DexInsnInfo {
|
||||
register(arr, DexOpcodes.SHR_INT_LIT8, Opcode.SHR_INT_LIT, DexInsnFormat.FORMAT_22B);
|
||||
register(arr, DexOpcodes.USHR_INT_LIT8, Opcode.USHR_INT_LIT, DexInsnFormat.FORMAT_22B);
|
||||
|
||||
register(arr, DexOpcodes.INVOKE_POLYMORPHIC, Opcode.INVOKE_POLYMORPHIC, DexInsnFormat.FORMAT_45CC);
|
||||
register(arr, DexOpcodes.INVOKE_POLYMORPHIC_RANGE, Opcode.INVOKE_POLYMORPHIC_RANGE, DexInsnFormat.FORMAT_4RCC);
|
||||
register(arr, DexOpcodes.INVOKE_POLYMORPHIC, Opcode.INVOKE_POLYMORPHIC, DexInsnFormat.FORMAT_45CC, InsnIndexType.METHOD_REF);
|
||||
register(arr, DexOpcodes.INVOKE_POLYMORPHIC_RANGE, Opcode.INVOKE_POLYMORPHIC_RANGE, DexInsnFormat.FORMAT_4RCC,
|
||||
InsnIndexType.METHOD_REF);
|
||||
|
||||
register(arr, DexOpcodes.INVOKE_CUSTOM, Opcode.INVOKE_CUSTOM, DexInsnFormat.FORMAT_35C, InsnIndexType.CALL_SITE);
|
||||
register(arr, DexOpcodes.INVOKE_CUSTOM_RANGE, Opcode.INVOKE_CUSTOM_RANGE, DexInsnFormat.FORMAT_3RC, InsnIndexType.CALL_SITE);
|
||||
|
||||
+18
@@ -24,6 +24,24 @@ public class DexMethodProto implements IMethodProto {
|
||||
return returnType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (!(other instanceof IMethodProto)) {
|
||||
return false;
|
||||
}
|
||||
IMethodProto that = (IMethodProto) other;
|
||||
return argTypes.equals(that.getArgTypes())
|
||||
&& returnType.equals(that.getReturnType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return 31 * argTypes.hashCode() + returnType.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "(" + Utils.listToStr(argTypes) + ")" + returnType;
|
||||
|
||||
+3
-2
@@ -96,8 +96,9 @@ public class DebugInfoParser {
|
||||
int nameId = in.readUleb128p1();
|
||||
String name = ext.getString(nameId);
|
||||
if (name != null && i < argsCount) {
|
||||
int regNum = argRegs[i];
|
||||
startVar(new DexLocalVar(regNum, name, argTypes.get(i)), -1);
|
||||
DexLocalVar paramVar = new DexLocalVar(argRegs[i], name, argTypes.get(i));
|
||||
startVar(paramVar, addr);
|
||||
paramVar.markAsParameter();
|
||||
varsInfoFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
+11
@@ -7,6 +7,8 @@ import jadx.api.plugins.utils.Utils;
|
||||
import jadx.plugins.input.dex.sections.SectionReader;
|
||||
|
||||
public class DexLocalVar implements ILocalVar {
|
||||
private static final int PARAM_START_OFFSET = -1;
|
||||
|
||||
private final int regNum;
|
||||
private final String name;
|
||||
private final String type;
|
||||
@@ -78,6 +80,15 @@ public class DexLocalVar implements ILocalVar {
|
||||
return startOffset;
|
||||
}
|
||||
|
||||
public void markAsParameter() {
|
||||
startOffset = PARAM_START_OFFSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMarkedAsParameter() {
|
||||
return startOffset == PARAM_START_OFFSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEndOffset() {
|
||||
return endOffset;
|
||||
|
||||
@@ -7,7 +7,7 @@ dependencies {
|
||||
|
||||
implementation(project(":jadx-plugins:jadx-dex-input"))
|
||||
implementation('com.jakewharton.android.repackaged:dalvik-dx:11.0.0_r3')
|
||||
implementation('com.android.tools:r8:3.3.75')
|
||||
implementation('com.android.tools:r8:4.0.48')
|
||||
|
||||
implementation 'org.ow2.asm:asm:9.4'
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@ dependencies {
|
||||
api(project(":jadx-plugins:jadx-plugins-api"))
|
||||
|
||||
// show bytecode disassemble
|
||||
implementation 'io.github.skylot:raung-disasm:0.0.2'
|
||||
implementation 'io.github.skylot:raung-disasm:0.0.3'
|
||||
}
|
||||
|
||||
+5
@@ -61,6 +61,11 @@ public class JavaLocalVar implements ILocalVar {
|
||||
return endOffset;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMarkedAsParameter() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = regNum;
|
||||
|
||||
@@ -15,4 +15,10 @@ public interface ILocalVar {
|
||||
int getStartOffset();
|
||||
|
||||
int getEndOffset();
|
||||
|
||||
/**
|
||||
* Hint if variable is a method parameter.
|
||||
* Can be incorrect and shouldn't be trusted.
|
||||
*/
|
||||
boolean isMarkedAsParameter();
|
||||
}
|
||||
|
||||
+3
-3
@@ -53,12 +53,12 @@ public class EncodedValue extends PinnedAttribute {
|
||||
switch (type) {
|
||||
case ENCODED_NULL:
|
||||
return "null";
|
||||
case ENCODED_STRING:
|
||||
return (String) value;
|
||||
case ENCODED_ARRAY:
|
||||
return "[" + value + "]";
|
||||
case ENCODED_STRING:
|
||||
return "{STRING: \"" + value + "\"}";
|
||||
default:
|
||||
return "{" + type + ": " + value + '}';
|
||||
return "{" + type.toString().substring(8) + ": " + value + '}';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@ dependencies {
|
||||
|
||||
implementation(project(":jadx-plugins:jadx-java-input"))
|
||||
|
||||
implementation('io.github.skylot:raung-asm:0.0.2')
|
||||
implementation('io.github.skylot:raung-asm:0.0.3')
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ public class SmaliConvert implements Closeable {
|
||||
SmaliOptions options = new SmaliOptions();
|
||||
options.outputDexFile = output.toAbsolutePath().toString();
|
||||
options.verboseErrors = true;
|
||||
options.apiLevel = 27; // TODO: add as plugin option
|
||||
|
||||
List<String> inputFileNames = inputFiles.stream()
|
||||
.map(p -> p.toAbsolutePath().toString())
|
||||
|
||||
Reference in New Issue
Block a user