Compare commits

..

31 Commits

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

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

* Fix format violation in FridaAction

* Fix format violation in FridaAction
2022-11-07 19:11:22 +00:00
Skylot bc8d7c4fc3 feat(gui): add native libs info to summary (#1717) 2022-11-03 19:15:33 +00:00
Skylot 5ea6c46778 fix(gui): show all code sources in summary (remove dex filter) (#1716) 2022-11-03 18:25:22 +00:00
Skylot b28f8ba85b fix(gui): try to handle exception in RSTA.getPreferredSize() (#1712) 2022-10-29 21:17:17 +01:00
Skylot 4db50fb749 fix(gui): correct html disabling in search results 2022-10-27 10:29:50 +01:00
Skylot 1dd0c90a04 build: switch java version to fix jdk install issue 2022-10-26 20:34:22 +01:00
Skylot 2bace2bde2 fix(gui): disable shell folders in file open dialog (#1709) 2022-10-26 20:08:57 +01:00
Skylot 1a9cb832ab feat(gui): add alternative file open dialog (#1709) 2022-10-26 19:58:58 +01:00
82 changed files with 2174 additions and 479 deletions
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=f6b8596b10cce501591e92f229816aa4046424f3b24d771751b06779d58c8ec4
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
distributionSha256Sum=7ba68c54029790ab444b39d7e293d3236b2632631fb5f2e012bb28b4ff669e4b
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+1 -1
View File
@@ -11,7 +11,7 @@ dependencies {
runtimeOnly(project(':jadx-plugins:jadx-smali-input'))
implementation 'com.beust:jcommander:1.82'
implementation 'ch.qos.logback:logback-classic:1.3.4'
implementation 'ch.qos.logback:logback-classic:1.3.5'
}
application {
+4 -4
View File
@@ -5,11 +5,11 @@ plugins {
dependencies {
api(project(':jadx-plugins:jadx-plugins-api'))
implementation 'com.google.code.gson:gson:2.9.1'
implementation 'com.google.code.gson:gson:2.10.1'
// TODO: move resources decoding to separate plugin module
implementation 'com.android.tools.build:aapt2-proto:7.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;
@@ -8,6 +8,7 @@ import jadx.api.plugins.input.data.IMethodHandle;
import jadx.api.plugins.input.data.IMethodProto;
import jadx.api.plugins.input.data.IMethodRef;
import jadx.api.plugins.input.data.MethodHandleType;
import jadx.api.plugins.input.data.annotations.EncodedType;
import jadx.api.plugins.input.data.annotations.EncodedValue;
import jadx.api.plugins.input.insns.InsnData;
import jadx.core.dex.attributes.AFlag;
@@ -34,18 +35,20 @@ public class CustomLambdaCall {
if (values.size() < 6) {
return false;
}
IMethodHandle methodHandle = (IMethodHandle) values.get(0).getValue();
EncodedValue mthRef = values.get(0);
if (mthRef.getType() != EncodedType.ENCODED_METHOD_HANDLE) {
return false;
}
IMethodHandle methodHandle = (IMethodHandle) mthRef.getValue();
if (methodHandle.getType() != MethodHandleType.INVOKE_STATIC) {
return false;
}
IMethodRef methodRef = methodHandle.getMethodRef();
if (!methodRef.getName().equals("metafactory")) {
return false;
}
if (!methodRef.getParentClassType().equals("Ljava/lang/invoke/LambdaMetafactory;")) {
return false;
}
return true;
String mthName = methodRef.getName();
return mthName.equals("metafactory") || mthName.equals("altMetafactory");
}
public static InvokeCustomNode buildLambdaMethodCall(MethodNode mth, InsnData insn, boolean isRange, List<EncodedValue> values) {
@@ -115,7 +118,7 @@ public class CustomLambdaCall {
@NotNull
private static InvokeNode buildInvokeNode(MethodHandleType methodHandleType, InvokeCustomNode invokeCustomNode,
MethodInfo callMthInfo) {
InvokeType invokeType = convertInvokeType(methodHandleType);
InvokeType invokeType = InvokeCustomUtils.convertInvokeType(methodHandleType);
int callArgsCount = callMthInfo.getArgsCount();
boolean instanceCall = invokeType != InvokeType.STATIC;
if (instanceCall) {
@@ -149,21 +152,4 @@ public class CustomLambdaCall {
}
return invokeNode;
}
private static InvokeType convertInvokeType(MethodHandleType type) {
switch (type) {
case INVOKE_STATIC:
return InvokeType.STATIC;
case INVOKE_INSTANCE:
return InvokeType.VIRTUAL;
case INVOKE_DIRECT:
case INVOKE_CONSTRUCTOR:
return InvokeType.DIRECT;
case INVOKE_INTERFACE:
return InvokeType.INTERFACE;
default:
throw new JadxRuntimeException("Unsupported method handle type: " + type);
}
}
}
@@ -0,0 +1,70 @@
package jadx.core.dex.instructions.invokedynamic;
import java.util.ArrayList;
import java.util.List;
import jadx.api.plugins.input.data.IMethodHandle;
import jadx.api.plugins.input.data.IMethodProto;
import jadx.api.plugins.input.data.annotations.EncodedValue;
import jadx.api.plugins.input.insns.InsnData;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.instructions.ConstStringNode;
import jadx.core.dex.instructions.InvokeCustomRawNode;
import jadx.core.dex.instructions.InvokeNode;
import jadx.core.dex.instructions.InvokeType;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.exceptions.JadxRuntimeException;
import static jadx.core.utils.EncodedValueUtils.buildLookupArg;
import static jadx.core.utils.EncodedValueUtils.convertToInsnArg;
/**
* Show `invoke-custom` similar to polymorphic call
*/
public class CustomRawCall {
public static InsnNode build(MethodNode mth, InsnData insn, boolean isRange, List<EncodedValue> values) {
IMethodHandle resolveHandle = (IMethodHandle) values.get(0).getValue();
String invokeName = (String) values.get(1).getValue();
IMethodProto invokeProto = (IMethodProto) values.get(2).getValue();
List<InsnArg> resolveArgs = buildArgs(mth, values);
if (resolveHandle.getType().isField()) {
throw new JadxRuntimeException("Field handle not yet supported");
}
RootNode root = mth.root();
MethodInfo resolveMth = MethodInfo.fromRef(root, resolveHandle.getMethodRef());
InvokeType resolveInvokeType = InvokeCustomUtils.convertInvokeType(resolveHandle.getType());
InvokeNode resolve = new InvokeNode(resolveMth, resolveInvokeType, resolveArgs.size());
resolveArgs.forEach(resolve::addArg);
ClassInfo invokeCls = ClassInfo.fromType(root, ArgType.OBJECT); // type will be known at runtime
MethodInfo invokeMth = MethodInfo.fromMethodProto(root, invokeCls, invokeName, invokeProto);
InvokeCustomRawNode customRawNode = new InvokeCustomRawNode(resolve, invokeMth, insn, isRange);
customRawNode.setCallSiteValues(values);
return customRawNode;
}
private static List<InsnArg> buildArgs(MethodNode mth, List<EncodedValue> values) {
int valuesCount = values.size();
List<InsnArg> list = new ArrayList<>(valuesCount);
RootNode root = mth.root();
list.add(buildLookupArg(root)); // use `java.lang.invoke.MethodHandles.lookup()` as first arg
for (int i = 1; i < valuesCount; i++) {
EncodedValue value = values.get(i);
try {
list.add(convertToInsnArg(root, value));
} catch (Exception e) {
mth.addWarnComment("Failed to build arg in invoke-custom insn: " + value, e);
list.add(InsnArg.wrapArg(new ConstStringNode(value.toString())));
}
}
return list;
}
}
@@ -0,0 +1,25 @@
package jadx.core.dex.instructions.invokedynamic;
import jadx.api.plugins.input.data.MethodHandleType;
import jadx.core.dex.instructions.InvokeType;
import jadx.core.utils.exceptions.JadxRuntimeException;
public class InvokeCustomUtils {
public static InvokeType convertInvokeType(MethodHandleType type) {
switch (type) {
case INVOKE_STATIC:
return InvokeType.STATIC;
case INVOKE_INSTANCE:
return InvokeType.VIRTUAL;
case INVOKE_DIRECT:
case INVOKE_CONSTRUCTOR:
return InvokeType.DIRECT;
case INVOKE_INTERFACE:
return InvokeType.INTERFACE;
default:
throw new JadxRuntimeException("Unsupported method handle type: " + type);
}
}
}
@@ -21,6 +21,7 @@ import jadx.api.ICodeCache;
import jadx.api.ICodeInfo;
import jadx.api.ICodeWriter;
import jadx.api.JadxArgs;
import jadx.api.JavaClass;
import jadx.api.impl.SimpleCodeInfo;
import jadx.api.plugins.input.data.IClassData;
import jadx.api.plugins.input.data.IFieldData;
@@ -100,6 +101,8 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
// cache maps
private Map<MethodInfo, MethodNode> mthInfoMap = Collections.emptyMap();
private JavaClass javaNode;
public ClassNode(RootNode root, IClassData cls) {
this.root = root;
this.clsInfo = ClassInfo.fromType(root, ArgType.object(cls.getType()));
@@ -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
+9 -9
View File
@@ -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<*>;\"");
}
}
+58
View File
@@ -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
@@ -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);
@@ -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;
@@ -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;
}
}
@@ -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;
+1 -1
View File
@@ -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'
}
+1 -1
View File
@@ -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'
}
@@ -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();
}
@@ -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 + '}';
}
}
}
+1 -1
View File
@@ -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())