Compare commits

...

54 Commits

Author SHA1 Message Date
Skylot fdf170529f fix: use strict patterns for synthetic methods inline (#1829) 2023-04-19 17:52:37 +01:00
Skylot 50283ab543 fix: additional checks to forbid inline of null consts (#1828) 2023-04-19 15:49:06 +01:00
Skylot 3fa3e5acec fix: correct args shift for instance invoke-custom (#1816) 2023-04-16 20:10:57 +01:00
Skylot 4230cd5b5a feat(plugins): allow to load classes using input stream or byte array in jadx-input plugin (#1457) 2023-04-10 21:28:53 +01:00
nitram84 1ad6527de5 fix(xml): use parent attibute only for styles (PR #1815) 2023-04-10 18:34:17 +01:00
nitram84 0421ad80c1 fix: filter invalid chars in app name for gradle export (PR #1813) 2023-04-08 18:16:14 +01:00
nitram84 35e0201f06 fix(gradle): fix gradle build with version 7.4.2 - 7.6 2023-04-08 19:49:15 +03:00
nitram84 118eea5e77 fix(res): set empty parent for styles without a parent, remove duplicated code 2023-04-08 19:49:15 +03:00
nitram84 7f317be325 fix(res): resolve declare-styleable atrributes 2023-04-08 19:49:15 +03:00
nitram84 e1aa9f6de4 fix(res): resolve custom attributes 2023-04-08 19:49:15 +03:00
nitram84 058a5e3bb2 fix(res): resolve int hex attributes 2023-04-08 19:49:15 +03:00
JustFor 92b49ec2b5 fix(gui): update Messages_zh_CN.properties (PR #1811)
sync new text, and Some symbols are translated
2023-04-07 19:20:15 +01:00
Skylot 583a04b092 fix(gui): show skipped resources count during search (#1808) 2023-03-24 22:00:07 +00:00
Skylot 444a04e2f7 fix(gui): redirect jump from search for inlined classes 2023-03-24 15:34:46 +00:00
Skylot 157e702ffd feat: inline lambdas by instance field (#1800) 2023-03-24 15:34:45 +00:00
Jan S 77892f41ec fix(res): parsing of sparse RES_TABLE_TYPE_TYPE and RES_TABLE_TYPE_STAGED_ALIAS chunks (#1806 #1803)(PR #1807) 2023-03-23 17:30:29 +00:00
Jan S 6ba0e1dbf6 fix(res): handle RES_TABLE_TYPE_OVERLAY (#1748) (PR #1804) 2023-03-18 14:23:49 +00:00
Skylot 950fbbaa83 fix: restore missing type parameter declarations (#1800) 2023-03-17 20:28:42 +00:00
Skylot 912c431511 fix(debugger): process UI updates in correct thread (#1796) 2023-03-17 14:23:36 +00:00
Skylot 5d6b82724a fix(gui): search constant fields usage in all classes (#1801) 2023-03-16 17:41:23 +00:00
Jan S 78c976ad4f fix(res): resolve manifest decoding error Expected strings start (#1797)(PR #1798) 2023-03-10 18:46:31 +03:00
Skylot fbdfd135da fix(cli): use common enum args parser (#1787) 2023-02-27 19:35:49 +00:00
Jacob Davis-Hansson dd51783d9e fix(cli): make enum CLI arguments match documented format (PR #1787)
Currently if you do `jadx --help`, it says the `--deobf-cfg-file-mode` option accepts the value `read-or-save`. 

However, if you give it that option, it instead prints the following error message:

```
java.lang.IllegalArgumentException: 'read-or-save' is unknown, possible values are: read, read-or-save, overwrite, ignore
	at jadx.cli.JadxCLIArgs$DeobfuscationMapFileModeConverter.convert(JadxCLIArgs.java:524)
	at jadx.cli.JadxCLIArgs$DeobfuscationMapFileModeConverter.convert(JadxCLIArgs.java:516)
	at com.beust.jcommander.JCommander.convertValue(JCommander.java:1340)
	at com.beust.jcommander.ParameterDescription.addValue(ParameterDescription.java:249)
	at com.beust.jcommander.JCommander.processFixedArity(JCommander.java:920)
	at com.beust.jcommander.JCommander.processFixedArity(JCommander.java:901)
	at com.beust.jcommander.JCommander.parseValues(JCommander.java:731)
	at com.beust.jcommander.JCommander.parse(JCommander.java:363)
	at com.beust.jcommander.JCommander.parse(JCommander.java:342)
	at jadx.cli.JCommanderWrapper.parse(JCommanderWrapper.java:37)
	at jadx.cli.JadxCLIArgs.processArgs(JadxCLIArgs.java:211)
	at jadx.cli.JadxCLI.execute(JadxCLI.java:35)
	at jadx.cli.JadxCLI.main(JadxCLI.java:20)
```

This commit changes all the enum parsers to do the inverse string of `enumValuesString`, so the documented behavior works.
2023-02-27 19:08:07 +00:00
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
134 changed files with 3593 additions and 795 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 {
@@ -7,6 +7,8 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -89,6 +91,9 @@ public class JadxCLIArgs {
@Parameter(names = { "--no-inline-methods" }, description = "disable methods inline")
protected boolean inlineMethods = true;
@Parameter(names = { "--no-inline-kotlin-lambda" }, description = "disable inline for Kotlin lambdas")
protected boolean allowInlineKotlinLambda = true;
@Parameter(names = "--no-finally", description = "don't extract finally block")
protected boolean extractFinally = true;
@@ -187,7 +192,7 @@ public class JadxCLIArgs {
@Parameter(
names = { "--log-level" },
description = "set log level, values: quiet, progress, error, warn, info, debug",
converter = LogHelper.LogLevelConverter.class
converter = LogLevelConverter.class
)
protected LogHelper.LogLevelEnum logLevel = LogHelper.LogLevelEnum.PROGRESS;
@@ -285,6 +290,7 @@ public class JadxCLIArgs {
args.setInsertDebugLines(addDebugLines);
args.setInlineAnonymousClasses(inlineAnonymousClasses);
args.setInlineMethods(inlineMethods);
args.setAllowInlineKotlinLambda(allowInlineKotlinLambda);
args.setExtractFinally(extractFinally);
args.setRenameFlags(renameFlags);
args.setFsCaseSensitive(fsCaseSensitive);
@@ -366,6 +372,10 @@ public class JadxCLIArgs {
return inlineMethods;
}
public boolean isAllowInlineKotlinLambda() {
return allowInlineKotlinLambda;
}
public boolean isExtractFinally() {
return extractFinally;
}
@@ -487,67 +497,58 @@ public class JadxCLIArgs {
}
}
public static class CommentsLevelConverter implements IStringConverter<CommentsLevel> {
@Override
public CommentsLevel convert(String value) {
try {
return CommentsLevel.valueOf(value.toUpperCase());
} catch (Exception e) {
throw new IllegalArgumentException(
'\'' + value + "' is unknown comments level, possible values are: "
+ JadxCLIArgs.enumValuesString(CommentsLevel.values()));
}
public static class CommentsLevelConverter extends BaseEnumConverter<CommentsLevel> {
public CommentsLevelConverter() {
super(CommentsLevel::valueOf, CommentsLevel::values);
}
}
public static class UseKotlinMethodsForVarNamesConverter implements IStringConverter<UseKotlinMethodsForVarNames> {
@Override
public UseKotlinMethodsForVarNames convert(String value) {
try {
return UseKotlinMethodsForVarNames.valueOf(value.replace('-', '_').toUpperCase());
} catch (Exception e) {
throw new IllegalArgumentException(
'\'' + value + "' is unknown, possible values are: "
+ JadxCLIArgs.enumValuesString(CommentsLevel.values()));
}
public static class UseKotlinMethodsForVarNamesConverter extends BaseEnumConverter<UseKotlinMethodsForVarNames> {
public UseKotlinMethodsForVarNamesConverter() {
super(UseKotlinMethodsForVarNames::valueOf, UseKotlinMethodsForVarNames::values);
}
}
public static class DeobfuscationMapFileModeConverter implements IStringConverter<DeobfuscationMapFileMode> {
@Override
public DeobfuscationMapFileMode convert(String value) {
try {
return DeobfuscationMapFileMode.valueOf(value.toUpperCase());
} catch (Exception e) {
throw new IllegalArgumentException(
'\'' + value + "' is unknown, possible values are: "
+ JadxCLIArgs.enumValuesString(DeobfuscationMapFileMode.values()));
}
public static class DeobfuscationMapFileModeConverter extends BaseEnumConverter<DeobfuscationMapFileMode> {
public DeobfuscationMapFileModeConverter() {
super(DeobfuscationMapFileMode::valueOf, DeobfuscationMapFileMode::values);
}
}
public static class ResourceNameSourceConverter implements IStringConverter<ResourceNameSource> {
@Override
public ResourceNameSource convert(String value) {
try {
return ResourceNameSource.valueOf(value.toUpperCase());
} catch (Exception e) {
throw new IllegalArgumentException(
'\'' + value + "' is unknown, possible values are: "
+ JadxCLIArgs.enumValuesString(ResourceNameSource.values()));
}
public static class ResourceNameSourceConverter extends BaseEnumConverter<ResourceNameSource> {
public ResourceNameSourceConverter() {
super(ResourceNameSource::valueOf, ResourceNameSource::values);
}
}
public static class DecompilationModeConverter implements IStringConverter<DecompilationMode> {
public static class DecompilationModeConverter extends BaseEnumConverter<DecompilationMode> {
public DecompilationModeConverter() {
super(DecompilationMode::valueOf, DecompilationMode::values);
}
}
public static class LogLevelConverter extends BaseEnumConverter<LogHelper.LogLevelEnum> {
public LogLevelConverter() {
super(LogHelper.LogLevelEnum::valueOf, LogHelper.LogLevelEnum::values);
}
}
public abstract static class BaseEnumConverter<E extends Enum<E>> implements IStringConverter<E> {
private final Function<String, E> parse;
private final Supplier<E[]> values;
public BaseEnumConverter(Function<String, E> parse, Supplier<E[]> values) {
this.parse = parse;
this.values = values;
}
@Override
public DecompilationMode convert(String value) {
public E convert(String value) {
try {
return DecompilationMode.valueOf(value.toUpperCase());
return parse.apply(stringAsEnumName(value));
} catch (Exception e) {
throw new IllegalArgumentException(
'\'' + value + "' is unknown, possible values are: "
+ JadxCLIArgs.enumValuesString(DecompilationMode.values()));
'\'' + value + "' is unknown, possible values are: " + enumValuesString(values.get()));
}
}
}
@@ -557,4 +558,9 @@ public class JadxCLIArgs {
.map(v -> v.name().replace('_', '-').toLowerCase(Locale.ROOT))
.collect(Collectors.joining(", "));
}
private static String stringAsEnumName(String value) {
// inverse of enumValuesString conversion
return value.replace('-', '_').toUpperCase(Locale.ROOT);
}
}
@@ -4,8 +4,6 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.LoggerFactory;
import com.beust.jcommander.IStringConverter;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
@@ -119,18 +117,4 @@ public class LogHelper {
}
return false;
}
public static class LogLevelConverter implements IStringConverter<LogLevelEnum> {
@Override
public LogLevelEnum convert(String value) {
try {
return LogLevelEnum.valueOf(value.toUpperCase());
} catch (Exception e) {
throw new IllegalArgumentException(
'\'' + value + "' is unknown log level, possible values are "
+ JadxCLIArgs.enumValuesString(LogLevelEnum.values()));
}
}
}
}
+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 {
@@ -53,6 +53,7 @@ public class JadxArgs {
private boolean extractFinally = true;
private boolean inlineAnonymousClasses = true;
private boolean inlineMethods = true;
private boolean allowInlineKotlinLambda = true;
private boolean skipResources = false;
private boolean skipSources = false;
@@ -263,6 +264,14 @@ public class JadxArgs {
this.inlineMethods = inlineMethods;
}
public boolean isAllowInlineKotlinLambda() {
return allowInlineKotlinLambda;
}
public void setAllowInlineKotlinLambda(boolean allowInlineKotlinLambda) {
this.allowInlineKotlinLambda = allowInlineKotlinLambda;
}
public boolean isExtractFinally() {
return extractFinally;
}
@@ -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,8 +14,10 @@ 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.FieldInitInsnAttr;
import jadx.core.dex.attributes.nodes.FieldReplaceAttr;
import jadx.core.dex.attributes.nodes.GenericInfoAttr;
import jadx.core.dex.attributes.nodes.LoopLabelAttr;
@@ -36,6 +38,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;
@@ -208,7 +211,31 @@ public class InsnGen {
}
}
protected void staticField(ICodeWriter code, FieldInfo field) throws CodegenException {
FieldNode fieldNode = root.resolveField(field);
if (fieldNode != null
&& fieldNode.contains(AFlag.INLINE_INSTANCE_FIELD)
&& fieldNode.getParentClass().contains(AType.ANONYMOUS_CLASS)) {
FieldInitInsnAttr initInsnAttr = fieldNode.get(AType.FIELD_INIT_INSN);
if (initInsnAttr != null) {
InsnNode insn = initInsnAttr.getInsn();
if (insn instanceof ConstructorInsn) {
fieldNode.add(AFlag.DONT_GENERATE);
inlineAnonymousConstructor(code, fieldNode.getParentClass(), (ConstructorInsn) insn);
return;
}
}
}
makeStaticFieldAccess(code, field, fieldNode, mgen.getClassGen());
}
public static void makeStaticFieldAccess(ICodeWriter code, FieldInfo field, ClassGen clsGen) {
FieldNode fieldNode = clsGen.getClassNode().root().resolveField(field);
makeStaticFieldAccess(code, field, fieldNode, clsGen);
}
private static void makeStaticFieldAccess(ICodeWriter code,
FieldInfo field, @Nullable FieldNode fieldNode, ClassGen clsGen) {
ClassInfo declClass = field.getDeclClass();
// TODO
boolean fieldFromThisClass = clsGen.getClassNode().getClassInfo().equals(declClass);
@@ -219,7 +246,6 @@ public class InsnGen {
}
code.add('.');
}
FieldNode fieldNode = clsGen.getClassNode().root().resolveField(field);
if (fieldNode != null) {
code.attachAnnotation(fieldNode);
}
@@ -230,10 +256,6 @@ public class InsnGen {
}
}
protected void staticField(ICodeWriter code, FieldInfo field) {
makeStaticFieldAccess(code, field, mgen.getClassGen());
}
public void useClass(ICodeWriter code, ArgType type) {
mgen.getClassGen().useClass(code, type);
}
@@ -693,9 +715,7 @@ public class InsnGen {
private void makeConstructor(ConstructorInsn insn, ICodeWriter code) throws CodegenException {
ClassNode cls = mth.root().resolveClass(insn.getClassType());
if (cls != null && cls.isAnonymous() && !fallback) {
cls.ensureProcessed();
inlineAnonymousConstructor(code, cls, insn);
mth.getParentClass().addInlinedClass(cls);
return;
}
if (insn.isSelf()) {
@@ -746,6 +766,7 @@ public class InsnGen {
}
private void inlineAnonymousConstructor(ICodeWriter code, ClassNode cls, ConstructorInsn insn) throws CodegenException {
cls.ensureProcessed();
if (this.mth.getParentClass() == cls) {
cls.remove(AType.ANONYMOUS_CLASS);
cls.remove(AFlag.DONT_GENERATE);
@@ -784,6 +805,8 @@ public class InsnGen {
ClassGen classGen = new ClassGen(cls, mgen.getClassGen().getParentGen());
classGen.setOuterNameGen(mgen.getNameGen());
classGen.addClassBody(code, true);
mth.getParentClass().addInlinedClass(cls);
}
private void makeInvoke(InvokeNode insn, ICodeWriter code) throws CodegenException {
@@ -795,11 +818,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 +872,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()) {
@@ -954,9 +1014,10 @@ public class InsnGen {
// force set external arg names into call method args
int extArgsCount = customNode.getArgsCount();
int startArg = customNode.getHandleType() == MethodHandleType.INVOKE_STATIC ? 0 : 1; // skip 'this' arg
int callArg = 0;
for (int i = startArg; i < extArgsCount; i++) {
RegisterArg extArg = (RegisterArg) customNode.getArg(i);
RegisterArg callRegArg = callArgs.get(i);
RegisterArg callRegArg = callArgs.get(callArg++);
callRegArg.getSVar().setCodeVar(extArg.getSVar().getCodeVar());
}
code.add(" -> {");
@@ -270,7 +270,7 @@ public class RegionGen extends InsnGen {
code.startLine('}');
}
private void addCaseKey(ICodeWriter code, InsnArg arg, Object k) {
private void addCaseKey(ICodeWriter code, InsnArg arg, Object k) throws CodegenException {
if (k instanceof FieldNode) {
FieldNode fn = (FieldNode) k;
if (fn.getParentClass().isEnum()) {
@@ -37,7 +37,9 @@ public enum AFlag {
SKIP_FIRST_ARG,
SKIP_ARG, // skip argument in invoke call
NO_SKIP_ARGS,
ANONYMOUS_CONSTRUCTOR,
INLINE_INSTANCE_FIELD,
THIS,
SUPER,
@@ -7,12 +7,19 @@ import jadx.core.dex.nodes.ClassNode;
public class AnonymousClassAttr extends PinnedAttribute {
public enum InlineType {
CONSTRUCTOR,
INSTANCE_FIELD,
}
private final ClassNode outerCls;
private final ArgType baseType;
private final InlineType inlineType;
public AnonymousClassAttr(ClassNode outerCls, ArgType baseType) {
public AnonymousClassAttr(ClassNode outerCls, ArgType baseType, InlineType inlineType) {
this.outerCls = outerCls;
this.baseType = baseType;
this.inlineType = inlineType;
}
public ClassNode getOuterCls() {
@@ -23,6 +30,10 @@ public class AnonymousClassAttr extends PinnedAttribute {
return baseType;
}
public InlineType getInlineType() {
return inlineType;
}
@Override
public AType<AnonymousClassAttr> getAttrType() {
return AType.ANONYMOUS_CLASS;
@@ -30,6 +41,6 @@ public class AnonymousClassAttr extends PinnedAttribute {
@Override
public String toString() {
return "AnonymousClass{" + outerCls + ", base: " + baseType + '}';
return "AnonymousClass{" + outerCls + ", base: " + baseType + ", inline type: " + inlineType + '}';
}
}
@@ -1,5 +1,7 @@
package jadx.core.dex.info;
import org.intellij.lang.annotations.MagicConstant;
import jadx.api.plugins.input.data.AccessFlags;
import jadx.core.Consts;
import jadx.core.utils.exceptions.JadxRuntimeException;
@@ -20,10 +22,21 @@ public class AccessInfo {
this.type = type;
}
@MagicConstant(valuesFromClass = AccessFlags.class)
public boolean containsFlag(int flag) {
return (accFlags & flag) != 0;
}
@MagicConstant(valuesFromClass = AccessFlags.class)
public boolean containsFlags(int... flags) {
for (int flag : flags) {
if ((accFlags & flag) == 0) {
return false;
}
}
return true;
}
public AccessInfo remove(int flag) {
if (containsFlag(flag)) {
return new AccessInfo(accFlags & ~flag, type);
@@ -82,16 +82,24 @@ public class ConstStorage {
return;
}
for (FieldNode f : staticFields) {
AccessInfo accFlags = f.getAccessFlags();
if (accFlags.isStatic() && accFlags.isFinal()) {
EncodedValue constVal = f.get(JadxAttrType.CONSTANT_VALUE);
if (constVal != null && constVal.getValue() != null) {
addConstField(cls, f, constVal.getValue(), accFlags.isPublic());
}
Object value = getFieldConstValue(f);
if (value != null) {
addConstField(cls, f, value, f.getAccessFlags().isPublic());
}
}
}
public static @Nullable Object getFieldConstValue(FieldNode fld) {
AccessInfo accFlags = fld.getAccessFlags();
if (accFlags.isStatic() && accFlags.isFinal()) {
EncodedValue constVal = fld.get(JadxAttrType.CONSTANT_VALUE);
if (constVal != null) {
return constVal.getValue();
}
}
return null;
}
public void removeForClass(ClassNode cls) {
classes.remove(cls);
globalValues.removeForCls(cls);
@@ -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,
}
@@ -264,6 +264,13 @@ public abstract class InsnArg extends Typed {
return false;
}
public boolean isSameVar(RegisterArg arg) {
if (isRegister()) {
return ((RegisterArg) this).sameRegAndSVar(arg);
}
return false;
}
protected final <T extends InsnArg> T copyCommonParams(T copy) {
copy.copyAttributesFrom(this);
copy.setParentInsn(parentInsn);
@@ -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);
}
}
}
@@ -13,14 +13,13 @@ import java.util.stream.Collectors;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.DecompilationMode;
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;
@@ -55,8 +54,6 @@ import static jadx.core.dex.nodes.ProcessState.LOADED;
import static jadx.core.dex.nodes.ProcessState.NOT_LOADED;
public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeNode, Comparable<ClassNode> {
private static final Logger LOG = LoggerFactory.getLogger(ClassNode.class);
private final RootNode root;
private final IClassData clsData;
@@ -100,6 +97,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()));
@@ -171,10 +170,10 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
return ArgType.object(superType);
}
public void updateGenericClsData(ArgType superClass, List<ArgType> interfaces, List<ArgType> generics) {
public void updateGenericClsData(List<ArgType> generics, ArgType superClass, List<ArgType> interfaces) {
this.generics = generics;
this.superClass = superClass;
this.interfaces = interfaces;
this.generics = generics;
}
private static void processAttributes(ClassNode cls) {
@@ -835,6 +834,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;
@@ -43,6 +43,7 @@ import jadx.core.utils.Utils;
import jadx.core.utils.android.AndroidResourcesUtils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.xmlgen.IResParser;
import jadx.core.xmlgen.ManifestAttributes;
import jadx.core.xmlgen.ResDecoder;
import jadx.core.xmlgen.ResourceStorage;
import jadx.core.xmlgen.entry.ResourceEntry;
@@ -174,12 +175,18 @@ public class RootNode {
if (parser != null) {
processResources(parser.getResStorage());
updateObfuscatedFiles(parser, resources);
updateManifestAttribMap(parser);
}
} catch (Exception e) {
LOG.error("Failed to parse '.arsc' file", e);
}
}
private void updateManifestAttribMap(IResParser parser) {
ManifestAttributes manifestAttributes = ManifestAttributes.getInstance();
manifestAttributes.updateAttributes(parser);
}
private @Nullable ResourceFile getResourceFile(List<ResourceFile> resources) {
for (ResourceFile rf : resources) {
if (rf.getType() == ResourceType.ARSC) {
@@ -125,17 +125,30 @@ public class ConstInlineVisitor extends AbstractVisitor {
int k = 0;
for (RegisterArg useArg : useList) {
InsnNode insn = useArg.getParentInsn();
if (insn == null) {
continue;
}
if (!canUseNull(insn, useArg)) {
useArg.add(AFlag.DONT_INLINE_CONST);
if (insn != null && forbidNullArgInline(insn, useArg)) {
k++;
}
}
return k == useList.size();
}
private static boolean forbidNullArgInline(InsnNode insn, RegisterArg useArg) {
switch (insn.getType()) {
case MOVE:
case CAST:
case CHECK_CAST:
// result is null, chain checks
return forbidNullInlines(insn.getResult().getSVar());
default:
if (!canUseNull(insn, useArg)) {
useArg.add(AFlag.DONT_INLINE_CONST);
return true;
}
return false;
}
}
private static boolean canUseNull(InsnNode insn, RegisterArg useArg) {
switch (insn.getType()) {
case INVOKE:
@@ -384,8 +384,14 @@ public class ExtractFieldInit extends AbstractVisitor {
return list;
}
private static void addFieldInitAttr(MethodNode mth, FieldNode field, InsnNode insn) {
InsnNode assignInsn = InsnNode.wrapArg(insn.getArg(0));
private static void addFieldInitAttr(MethodNode mth, FieldNode field, IndexInsnNode putInsn) {
InsnNode assignInsn;
InsnArg fldArg = putInsn.getArg(0);
if (fldArg.isInsnWrap()) {
assignInsn = ((InsnWrapArg) fldArg).getWrapInsn();
} else {
assignInsn = InsnNode.wrapArg(fldArg);
}
field.addAttr(new FieldInitInsnAttr(mth, assignInsn));
}
}
@@ -80,13 +80,47 @@ public class MarkMethodsForInline extends AbstractVisitor {
return addInlineAttr(mth, insn);
}
if (insnsCount == 2 && insns.get(1).getType() == InsnType.RETURN) {
// synthetic field setter
return addInlineAttr(mth, insns.get(0));
InsnNode firstInsn = insns.get(0);
InsnNode retInsn = insns.get(1);
if (retInsn.getArgsCount() == 0
|| isSyntheticAccessPattern(mth, firstInsn, retInsn)) {
return addInlineAttr(mth, firstInsn);
}
}
// TODO: inline field arithmetics. Disabled tests: TestAnonymousClass3a and TestAnonymousClass5
return null;
}
private static boolean isSyntheticAccessPattern(MethodNode mth, InsnNode firstInsn, InsnNode retInsn) {
List<RegisterArg> mthRegs = mth.getArgRegs();
switch (firstInsn.getType()) {
case IGET:
return mthRegs.size() == 1
&& retInsn.getArg(0).isSameVar(firstInsn.getResult())
&& firstInsn.getArg(0).isSameVar(mthRegs.get(0));
case SGET:
return mthRegs.size() == 0
&& retInsn.getArg(0).isSameVar(firstInsn.getResult());
case IPUT:
return mthRegs.size() == 2
&& retInsn.getArg(0).isSameVar(mthRegs.get(1))
&& firstInsn.getArg(0).isSameVar(mthRegs.get(1))
&& firstInsn.getArg(1).isSameVar(mthRegs.get(0));
case SPUT:
return mthRegs.size() == 1
&& retInsn.getArg(0).isSameVar(mthRegs.get(0))
&& firstInsn.getArg(0).isSameVar(mthRegs.get(0));
case INVOKE:
return mthRegs.size() >= 1
&& firstInsn.getArg(0).isSameVar(mthRegs.get(0))
&& retInsn.getArg(0).isSameVar(firstInsn.getResult());
default:
return false;
}
}
private static MethodInlineAttr addInlineAttr(MethodNode mth, InsnNode insn) {
if (!fixVisibilityOfInlineCode(mth, insn)) {
return null;
@@ -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;
}
@@ -10,9 +10,11 @@ import java.util.Set;
import org.jetbrains.annotations.Nullable;
import jadx.api.plugins.input.data.AccessFlags;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.AnonymousClassAttr;
import jadx.core.dex.attributes.nodes.AnonymousClassAttr.InlineType;
import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.nodes.ClassNode;
@@ -30,6 +32,7 @@ import jadx.core.utils.exceptions.JadxException;
UsageInfoVisitor.class
}
)
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public class ProcessAnonymous extends AbstractVisitor {
private boolean inlineAnonymousClasses;
@@ -64,17 +67,26 @@ public class ProcessAnonymous extends AbstractVisitor {
if (!canBeAnonymous(cls)) {
return;
}
MethodNode anonymousConstructor = checkUsage(cls);
MethodNode anonymousConstructor = ListUtils.filterOnlyOne(cls.getMethods(), MethodNode::isConstructor);
if (anonymousConstructor == null) {
return;
}
InlineType inlineType = checkUsage(cls, anonymousConstructor);
if (inlineType == null) {
return;
}
ArgType baseType = getBaseType(cls);
if (baseType == null) {
return;
}
ClassNode outerCls = anonymousConstructor.getUseIn().get(0).getParentClass();
ClassNode outerCls;
if (inlineType == InlineType.INSTANCE_FIELD) {
outerCls = cls.getUseInMth().get(0).getParentClass();
} else {
outerCls = anonymousConstructor.getUseIn().get(0).getParentClass();
}
outerCls.addInlinedClass(cls);
cls.addAttr(new AnonymousClassAttr(outerCls, baseType));
cls.addAttr(new AnonymousClassAttr(outerCls, baseType, inlineType));
cls.add(AFlag.DONT_GENERATE);
anonymousConstructor.add(AFlag.ANONYMOUS_CONSTRUCTOR);
@@ -202,14 +214,11 @@ public class ProcessAnonymous extends AbstractVisitor {
* Checks:
* - class have only one constructor which used only once (allow common code for field init)
* - methods or fields not used outside (allow only nested inner classes with synthetic usage)
* - if constructor used only in class init check if possible inline by instance field
*
* @return anonymous constructor method
* @return decided inline type
*/
private static MethodNode checkUsage(ClassNode cls) {
MethodNode ctr = ListUtils.filterOnlyOne(cls.getMethods(), MethodNode::isConstructor);
if (ctr == null) {
return null;
}
private static InlineType checkUsage(ClassNode cls, MethodNode ctr) {
if (ctr.getUseIn().size() != 1) {
// check if used in common field init in all constructors
if (!checkForCommonFieldInit(ctr)) {
@@ -219,6 +228,9 @@ public class ProcessAnonymous extends AbstractVisitor {
MethodNode ctrUseMth = ctr.getUseIn().get(0);
ClassNode ctrUseCls = ctrUseMth.getParentClass();
if (ctrUseCls.equals(cls)) {
if (checkForInstanceFieldUsage(cls, ctr)) {
return InlineType.INSTANCE_FIELD;
}
// exclude self usage
return null;
}
@@ -226,6 +238,20 @@ public class ProcessAnonymous extends AbstractVisitor {
// exclude usage inside inner classes
return null;
}
if (!checkMethodsUsage(cls, ctr, ctrUseMth)) {
return null;
}
for (FieldNode field : cls.getFields()) {
for (MethodNode useMth : field.getUseIn()) {
if (badMethodUsage(cls, useMth, field.getAccessFlags())) {
return null;
}
}
}
return InlineType.CONSTRUCTOR;
}
private static boolean checkMethodsUsage(ClassNode cls, MethodNode ctr, MethodNode ctrUseMth) {
for (MethodNode mth : cls.getMethods()) {
if (mth == ctr) {
continue;
@@ -235,18 +261,46 @@ public class ProcessAnonymous extends AbstractVisitor {
continue;
}
if (badMethodUsage(cls, useMth, mth.getAccessFlags())) {
return null;
return false;
}
}
}
return true;
}
private static boolean checkForInstanceFieldUsage(ClassNode cls, MethodNode ctr) {
MethodNode ctrUseMth = ctr.getUseIn().get(0);
if (!ctrUseMth.getMethodInfo().isClassInit()) {
return false;
}
FieldNode instFld = ListUtils.filterOnlyOne(cls.getFields(),
f -> f.getAccessFlags().containsFlags(AccessFlags.PUBLIC, AccessFlags.STATIC, AccessFlags.FINAL)
&& f.getFieldInfo().getType().equals(cls.getClassInfo().getType()));
if (instFld == null) {
return false;
}
List<MethodNode> instFldUseIn = instFld.getUseIn();
if (instFldUseIn.size() != 2
|| !instFldUseIn.contains(ctrUseMth) // initialized in class init
|| !instFldUseIn.containsAll(cls.getUseInMth()) // class used only with this field
) {
return false;
}
if (!checkMethodsUsage(cls, ctr, ctrUseMth)) {
return false;
}
for (FieldNode field : cls.getFields()) {
if (field == instFld) {
continue;
}
for (MethodNode useMth : field.getUseIn()) {
if (badMethodUsage(cls, useMth, field.getAccessFlags())) {
return null;
return false;
}
}
}
return ctr;
instFld.add(AFlag.INLINE_INSTANCE_FIELD);
return true;
}
private static boolean badMethodUsage(ClassNode cls, MethodNode useMth, AccessInfo accessFlags) {
@@ -297,6 +351,13 @@ public class ProcessAnonymous extends AbstractVisitor {
if (cls.root().getClsp().isImplements(superCls.getObject(), interfaceType.getObject())) {
return superCls;
}
if (cls.root().getArgs().isAllowInlineKotlinLambda()) {
if (superCls.getObject().equals("kotlin.jvm.internal.Lambda")) {
// Inline such class with have different semantic: missing 'arity' property.
// For now, it is unclear how it may affect code execution.
return interfaceType;
}
}
return null;
}
}
@@ -2,8 +2,12 @@ package jadx.core.dex.visitors;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.jetbrains.annotations.Nullable;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.instructions.args.ArgType;
@@ -18,7 +22,6 @@ import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxException;
public class SignatureProcessor extends AbstractVisitor {
private RootNode root;
@Override
@@ -55,12 +58,54 @@ public class SignatureProcessor extends AbstractVisitor {
break;
}
}
cls.updateGenericClsData(superClass, interfaces, generics);
generics = fixTypeParamDeclarations(cls, generics, superClass, interfaces);
cls.updateGenericClsData(generics, superClass, interfaces);
} catch (Exception e) {
cls.addWarnComment("Failed to parse class signature: " + sp.getSignature(), e);
}
}
/**
* Add missing type parameters from super type and interfaces to make code compilable
*/
private static List<ArgType> fixTypeParamDeclarations(ClassNode cls,
List<ArgType> generics, ArgType superClass, List<ArgType> interfaces) {
if (interfaces.isEmpty() && superClass.equals(ArgType.OBJECT)) {
return generics;
}
Set<String> typeParams = new HashSet<>();
superClass.visitTypes(t -> addGenericType(typeParams, t));
interfaces.forEach(i -> i.visitTypes(t -> addGenericType(typeParams, t)));
if (typeParams.isEmpty()) {
return generics;
}
List<ArgType> knownTypeParams;
if (cls.isInner()) {
knownTypeParams = new ArrayList<>(generics);
cls.visitParentClasses(p -> knownTypeParams.addAll(p.getGenericTypeParameters()));
} else {
knownTypeParams = generics;
}
for (ArgType declTypeParam : knownTypeParams) {
typeParams.remove(declTypeParam.getObject());
}
if (typeParams.isEmpty()) {
return generics;
}
cls.addInfoComment("Add missing generic type declarations: " + typeParams);
List<ArgType> fixedGenerics = new ArrayList<>(generics.size() + typeParams.size());
fixedGenerics.addAll(generics);
typeParams.stream().sorted().map(ArgType::genericType).forEach(fixedGenerics::add);
return fixedGenerics;
}
private static @Nullable Object addGenericType(Set<String> usedTypeParameters, ArgType t) {
if (t.isGenericType()) {
usedTypeParameters.add(t.getObject());
}
return null;
}
private ArgType validateClsType(ClassNode cls, ArgType candidateType, ArgType currentType) {
if (!candidateType.isObject()) {
cls.addWarnComment("Incorrect class signature, class is not object: " + SignatureParser.getSignature(cls));
@@ -9,6 +9,8 @@ import java.util.Set;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.instructions.InsnType;
import jadx.core.dex.instructions.InvokeCustomNode;
import jadx.core.dex.instructions.InvokeNode;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.InsnWrapArg;
import jadx.core.dex.instructions.args.Named;
@@ -123,6 +125,9 @@ public class CodeShrinkVisitor extends AbstractVisitor {
return;
}
}
if (!checkLambdaInline(arg, assignInsn)) {
return;
}
int assignPos = insnList.getIndex(assignInsn);
if (assignPos != -1) {
@@ -145,6 +150,26 @@ public class CodeShrinkVisitor extends AbstractVisitor {
}
}
/**
* Forbid inline lambda into invoke as an instance arg, i.e. this will not compile:
* {@code () -> { ... }.apply(); }
*/
private static boolean checkLambdaInline(RegisterArg arg, InsnNode assignInsn) {
if (assignInsn.getType() == InsnType.INVOKE && assignInsn instanceof InvokeCustomNode) {
for (RegisterArg useArg : arg.getSVar().getUseList()) {
InsnNode parentInsn = useArg.getParentInsn();
if (parentInsn != null && parentInsn.getType() == InsnType.INVOKE) {
InvokeNode invokeNode = (InvokeNode) parentInsn;
InsnArg instArg = invokeNode.getInstanceArg();
if (instArg != null && instArg == useArg) {
return false;
}
}
}
}
return true;
}
private static boolean varWithSameNameExists(MethodNode mth, SSAVar inlineVar) {
for (SSAVar ssaVar : mth.getSVars()) {
if (ssaVar == inlineVar || ssaVar.getCodeVar() == inlineVar.getCodeVar()) {
@@ -325,7 +325,7 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
ClassNode ctrCls = root.resolveClass(ctr.getClassType());
if (ctrCls != null && ctrCls.contains(AFlag.DONT_GENERATE)) {
AnonymousClassAttr baseTypeAttr = ctrCls.get(AType.ANONYMOUS_CLASS);
if (baseTypeAttr != null) {
if (baseTypeAttr != null && baseTypeAttr.getInlineType() == AnonymousClassAttr.InlineType.CONSTRUCTOR) {
return baseTypeAttr.getBaseType();
}
}
@@ -6,6 +6,7 @@ import java.io.StringReader;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Pattern;
import javax.xml.parsers.DocumentBuilder;
@@ -29,6 +30,8 @@ public class ExportGradleProject {
private static final Logger LOG = LoggerFactory.getLogger(ExportGradleProject.class);
private static final Pattern ILLEGAL_GRADLE_CHARS = Pattern.compile("[/\\\\:>\"?*|]");
private static final Set<String> IGNORE_CLS_NAMES = new HashSet<>(Arrays.asList(
"R",
"BuildConfig"));
@@ -72,7 +75,7 @@ public class ExportGradleProject {
private void saveSettingsGradle() throws IOException {
TemplateFile tmpl = TemplateFile.fromResources("/export/settings.gradle.tmpl");
tmpl.add("applicationName", applicationParams.getApplicationName());
tmpl.add("applicationName", ILLEGAL_GRADLE_CHARS.matcher(applicationParams.getApplicationName()).replaceAll(""));
tmpl.save(new File(projectDir, "settings.gradle"));
}
@@ -139,11 +139,13 @@ public class DecompilerScheduler implements IDecompileScheduler {
}
private void dumpBatchesStats(List<JavaClass> classes, List<List<JavaClass>> result, List<DepInfo> deps) {
int clsInBatches = result.stream().mapToInt(List::size).sum();
double avg = result.stream().mapToInt(List::size).average().orElse(-1);
int maxSingleDeps = classes.stream().mapToInt(JavaClass::getTotalDepsCount).max().orElse(-1);
int maxSubDeps = deps.stream().mapToInt(DepInfo::getDepsCount).max().orElse(-1);
LOG.info("Batches stats:"
+ "\n input classes: " + classes.size()
+ ",\n classes in batches: " + clsInBatches
+ ",\n batches: " + result.size()
+ ",\n average batch size: " + String.format("%.2f", avg)
+ ",\n max single deps count: " + maxSingleDeps
@@ -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) {
@@ -26,7 +26,7 @@ public class CommonBinaryParser extends ParserConstants {
int[] stringsOffset = is.readInt32Array(stringCount);
int[] stylesOffset = is.readInt32Array(styleCount);
is.checkPos(start + stringsStart, "Expected strings start");
is.skipToPos(start + stringsStart, "Expected strings start");
String[] strings = new String[stringCount];
byte[] strData = is.readInt8Array((int) (chunkEnd - is.getPos()));
if ((flags & UTF8_FLAG) != 0) {
@@ -17,6 +17,9 @@ import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.xmlgen.entry.RawNamedValue;
import jadx.core.xmlgen.entry.ResourceEntry;
import jadx.core.xmlgen.entry.ValuesParser;
public class ManifestAttributes {
private static final Logger LOG = LoggerFactory.getLogger(ManifestAttributes.class);
@@ -52,6 +55,8 @@ public class ManifestAttributes {
private final Map<String, MAttr> attrMap = new HashMap<>();
private final Map<String, MAttr> appAttrMap = new HashMap<>();
private static ManifestAttributes instance;
public static ManifestAttributes getInstance() {
@@ -168,7 +173,10 @@ public class ManifestAttributes {
public String decode(String attrName, long value) {
MAttr attr = attrMap.get(attrName);
if (attr == null) {
return null;
attr = appAttrMap.get(attrName);
if (attr == null) {
return null;
}
}
if (attr.getType() == MAttrType.ENUM) {
return attr.getValues().get(value);
@@ -190,4 +198,32 @@ public class ManifestAttributes {
}
return null;
}
public void updateAttributes(IResParser parser) {
appAttrMap.clear();
ResourceStorage resStorage = parser.getResStorage();
ValuesParser vp = new ValuesParser(parser.getStrings(), resStorage.getResourcesNames());
for (ResourceEntry ri : resStorage.getResources()) {
if (ri.getTypeName().equals("attr") && ri.getNamedValues().size() > 1) {
RawNamedValue first = ri.getNamedValues().get(0);
MAttrType attrTyp;
if (first.getRawValue().getData() == ValuesParser.ATTR_TYPE_FLAGS) {
attrTyp = MAttrType.FLAG;
} else if (first.getRawValue().getData() == ValuesParser.ATTR_TYPE_ENUM || first.getRawValue().getData() == 65600) {
attrTyp = MAttrType.ENUM;
} else {
continue;
}
MAttr attr = new MAttr(attrTyp);
for (int i = 1; i < ri.getNamedValues().size(); i++) {
RawNamedValue rv = ri.getNamedValues().get(i);
String value = vp.decodeNameRef(rv.getNameRef());
attr.getValues().put((long) rv.getRawValue().getData(), value.startsWith("id.") ? value.substring(3) : value);
}
appAttrMap.put(ri.getKeyName(), attr);
}
}
}
}
@@ -33,7 +33,8 @@ public class ParserConstants {
protected static final int RES_TABLE_TYPE_SPEC_TYPE = 0x0202; // 514
protected static final int RES_TABLE_TYPE_LIBRARY = 0x0203; // 515
protected static final int RES_TABLE_TYPE_OVERLAY = 0x0204; // 516
protected static final int RES_TABLE_TYPE_STAGED_ALIAS = 0x0206; // 517
protected static final int RES_TABLE_TYPE_OVERLAY_POLICY = 0x0205; // 517
protected static final int RES_TABLE_TYPE_STAGED_ALIAS = 0x0206; // 518
/**
* Type constants
@@ -127,6 +127,10 @@ public class ParserStream {
public void skipToPos(long expectedOffset, String error) throws IOException {
long pos = getPos();
if (pos > expectedOffset) {
throw new IOException(error + ", expected offset not reachable: 0x" + Long.toHexString(expectedOffset)
+ ", actual: 0x" + Long.toHexString(getPos()));
}
if (pos < expectedOffset) {
skip(expectedOffset - pos);
}
@@ -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);
}
@@ -4,7 +4,9 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
@@ -168,11 +170,14 @@ public class ResTableParser extends CommonBinaryParser implements IResParser {
parseLibraryTypeChunk(chunkStart);
break;
case RES_TABLE_TYPE_OVERLAY: // 0x0204
parseOverlayTypeChunk(chunkStart);
break;
case RES_TABLE_TYPE_OVERLAY_POLICY: // 0x0205
throw new IOException(
String.format("Encountered unsupported chunk type TYPE_OVERLAY at offset 0x%x ", chunkStart));
String.format("Encountered unsupported chunk type RES_TABLE_TYPE_OVERLAY_POLICY at offset 0x%x ", chunkStart));
case RES_TABLE_TYPE_STAGED_ALIAS: // 0x0206
throw new IOException(
String.format("Encountered unsupported chunk type TYPE_STAGED_ALIAS at offset 0x%x ", chunkStart));
parseStagedAliasChunk(chunkStart);
break;
default:
LOG.warn("Unknown chunk type {} encountered at offset {}", type, chunkStart);
}
@@ -242,6 +247,13 @@ public class ResTableParser extends CommonBinaryParser implements IResParser {
}
}
/**
* Parse an <code>ResTable_type</code> (except for the 2 bytes <code>uint16_t</code>
* from <code>ResChunk_header</code>).
*
* @see <a href=
* "https://github.com/aosp-mirror/platform_frameworks_base/blob/master/libs/androidfw/include/androidfw/ResourceTypes.h"></a>ResourceTypes.h</a>
*/
private void parseTypeChunk(long start, PackageChunk pkg) throws IOException {
/* int headerSize = */
is.readInt16();
@@ -249,9 +261,13 @@ public class ResTableParser extends CommonBinaryParser implements IResParser {
long chunkSize = is.readUInt32();
long chunkEnd = start + chunkSize;
// The type identifier this chunk is holding. Type IDs start at 1 (corresponding
// to the value of the type bits in a resource identifier). 0 is invalid.
int id = is.readInt8();
is.checkInt8(0, "type chunk, res0");
is.checkInt16(0, "type chunk, res1");
int flags = is.readInt8(); // 0 or 1
boolean flagSparse = (flags == 1);
is.checkInt16(0, "type chunk, reserved");
int entryCount = is.readInt32();
long entriesStart = start + is.readInt32();
@@ -262,21 +278,30 @@ public class ResTableParser extends CommonBinaryParser implements IResParser {
LOG.warn("Invalid config flags detected: {}{}", typeName, config.getQualifiers());
}
int[] entryIndexes = new int[entryCount];
for (int i = 0; i < entryCount; i++) {
entryIndexes[i] = is.readInt32();
Map<Integer, Integer> entryOffsetMap = new LinkedHashMap<>(entryCount);
if (flagSparse) {
for (int i = 0; i < entryCount; i++) {
entryOffsetMap.put(is.readInt16(), is.readInt16());
}
} else {
for (int i = 0; i < entryCount; i++) {
entryOffsetMap.put(i, is.readInt32());
}
}
is.checkPos(entriesStart, "Expected entry start");
for (int i = 0; i < entryCount; i++) {
if (entryIndexes[i] != NO_ENTRY) {
int processed = 0;
for (int index : entryOffsetMap.keySet()) {
int offset = entryOffsetMap.get(index);
if (offset != NO_ENTRY) {
if (is.getPos() >= chunkEnd) {
// Certain resource obfuscated apps like com.facebook.orca have more entries defined
// than actually fit into the chunk size -> ignore the remaining entries
LOG.warn("End of chunk reached - ignoring remaining {} entries", entryCount - i);
LOG.warn("End of chunk reached - ignoring remaining {} entries", entryCount - processed);
break;
}
parseEntry(pkg, id, i, config.getQualifiers());
parseEntry(pkg, id, index, config.getQualifiers());
}
processed++;
}
if (chunkEnd > is.getPos()) {
// Skip remaining unknown data in this chunk (e.g. type 8 entries")
@@ -287,6 +312,36 @@ public class ResTableParser extends CommonBinaryParser implements IResParser {
}
}
private void parseOverlayTypeChunk(long chunkStart) throws IOException {
LOG.trace("parsing overlay type chunk starting at offset {}", chunkStart);
// read ResTable_overlayable_header
/* headerSize = */ is.readInt16(); // usually 1032 bytes
int chunkSize = is.readInt32(); // e.g. 1056 bytes
long expectedEndPos = chunkStart + chunkSize;
String name = is.readString16Fixed(256); // 512 bytes
String actor = is.readString16Fixed(256); // 512 bytes
LOG.trace("Overlay header data: name={} actor={}", name, actor);
// skip: ResTable_overlayable_policy_header + ResTable_ref * x
is.skipToPos(expectedEndPos, "overlay chunk end");
}
private void parseStagedAliasChunk(long chunkStart) throws IOException {
// read ResTable_staged_alias_header
LOG.trace("parsing staged alias chunk starting at offset {}", chunkStart);
/* headerSize = */ is.readInt16();
int chunkSize = is.readInt32();
long expectedEndPos = chunkStart + chunkSize;
int count = is.readInt32();
for (int i = 0; i < count; i++) {
// read ResTable_staged_alias_entry
int stagedResId = is.readInt32();
int finalizedResId = is.readInt32();
LOG.debug("Staged alias: stagedResId {} finalizedResId {}", stagedResId, finalizedResId);
}
is.skipToPos(expectedEndPos, "staged alias chunk end");
}
private void parseEntry(PackageChunk pkg, int typeId, int entryId, String config) throws IOException {
int size = is.readInt16();
int flags = is.readInt16();
@@ -109,10 +109,10 @@ public class ResXmlGen {
addSimpleValue(cw, ri.getTypeName(), ri.getTypeName(), "name", ri.getKeyName(), valueStr);
} else {
cw.startLine();
cw.add('<').add(ri.getTypeName()).add(' ');
cw.add('<').add(ri.getTypeName()).add(" name=\"");
String itemTag = "item";
if (ri.getTypeName().equals("attr") && !ri.getNamedValues().isEmpty()) {
cw.add("name=\"").add(ri.getKeyName());
cw.add(ri.getKeyName());
int type = ri.getNamedValues().get(0).getRawValue().getData();
if ((type & ValuesParser.ATTR_TYPE_ENUM) != 0) {
itemTag = "enum";
@@ -123,15 +123,17 @@ public class ResXmlGen {
if (formatValue != null) {
cw.add("\" format=\"").add(formatValue);
}
cw.add("\"");
} else {
cw.add("name=\"").add(ri.getKeyName()).add('\"');
cw.add(ri.getKeyName());
}
if (ri.getParentRef() != 0) {
String parent = vp.decodeValue(TYPE_REFERENCE, ri.getParentRef());
cw.add(" parent=\"").add(parent).add('\"');
if (ri.getTypeName().equals("style") || ri.getParentRef() != 0) {
cw.add("\" parent=\"");
if (ri.getParentRef() != 0) {
String parent = vp.decodeValue(TYPE_REFERENCE, ri.getParentRef());
cw.add(parent);
}
}
cw.add(">");
cw.add("\">");
cw.incIndent();
for (RawNamedValue value : ri.getNamedValues()) {
@@ -177,7 +179,18 @@ public class ResXmlGen {
if (dataType == ParserConstants.TYPE_INT_DEC && nameStr != null) {
try {
int intVal = Integer.parseInt(valueStr);
String newVal = ManifestAttributes.getInstance().decode(nameStr.replace("android:attr.", ""), intVal);
String newVal = ManifestAttributes.getInstance().decode(nameStr.replace("android:", "").replace("attr.", ""), intVal);
if (newVal != null) {
valueStr = newVal;
}
} catch (NumberFormatException e) {
// ignore
}
}
if (dataType == ParserConstants.TYPE_INT_HEX && nameStr != null) {
try {
int intVal = Integer.decode(valueStr);
String newVal = ManifestAttributes.getInstance().decode(nameStr.replace("android:", "").replace("attr.", ""), intVal);
if (newVal != null) {
valueStr = newVal;
}
@@ -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) {
@@ -4,7 +4,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.0'
classpath 'com.android.tools.build:gradle:4.2.2'
}
}
@@ -67,4 +67,10 @@ public abstract class ExportGradleTest {
assertThat(appBuildGradle.exists());
return loadFileContent(appBuildGradle);
}
protected String getSettingsGradle() {
File settingsGradle = new File(exportDir, "settings.gradle");
assertThat(settingsGradle.exists());
return loadFileContent(settingsGradle);
}
}
@@ -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;
@@ -217,6 +224,11 @@ public abstract class IntegrationTest extends TestUtils {
return sortedClsNodes;
}
@NotNull
public ClassNode searchTestCls(List<ClassNode> list, String shortClsName) {
return searchCls(list, getTestPkg() + '.' + shortClsName);
}
@NotNull
public ClassNode searchCls(List<ClassNode> list, String clsName) {
for (ClassNode cls : list) {
@@ -346,11 +358,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 +372,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 +557,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,17 @@
package jadx.tests.export;
import org.junit.jupiter.api.Test;
import jadx.tests.api.ExportGradleTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
class IllegalCharsForGradleWrapper extends ExportGradleTest {
@Test
void test() {
exportGradle("IllegalCharsForGradleWrapper.xml", "strings.xml");
assertThat(getSettingsGradle()).contains("'JadxTestApp'");
}
}
@@ -0,0 +1,77 @@
package jadx.tests.integration.inline;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import org.junit.jupiter.api.Test;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.visitors.ProcessAnonymous;
import jadx.core.utils.ListUtils;
import jadx.tests.api.SmaliTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestInstanceLambda extends SmaliTest {
@SuppressWarnings({ "unchecked", "rawtypes", "SameParameterValue" })
public static class TestCls {
public <T> Map<T, T> test(List<? extends T> list) {
return toMap(list, Lambda$1.INSTANCE);
}
/**
* Smali test missing 'T' definition in 'Lambda<T>'
* Note: use '$1' so class looks like generated by compiler and pass check in
* {@link ProcessAnonymous#canBeAnonymous(ClassNode)}
*/
@SuppressWarnings({ "CheckStyle", "checkstyle:TypeName" })
private static class Lambda$1<T> implements Function<T, T> {
public static final Lambda$1 INSTANCE = new Lambda$1();
@Override
public T apply(T t) {
return t;
}
}
private static <T> Map<T, T> toMap(List<? extends T> list, Function<T, T> valueMap) {
return null;
}
}
@Test
public void test() {
useJavaInput();
noDebugInfo();
assertThat(getClassNode(TestCls.class))
.code();
}
@Test
public void testSmaliDisableInline() {
args.setInlineAnonymousClasses(false);
List<ClassNode> classNodes = loadFromSmaliFiles();
assertThat(searchTestCls(classNodes, "Lambda$1"))
.code()
.containsOne("class Lambda$1<T> implements Function<T, T> {");
assertThat(searchTestCls(classNodes, "TestCls"))
.code()
.containsOne("Lambda$1.INSTANCE");
}
@Test
public void testSmali() {
List<ClassNode> classNodes = loadFromSmaliFiles();
assertThat(ListUtils.filter(classNodes, c -> !c.contains(AFlag.DONT_GENERATE)))
.describedAs("Expect lambda to be inlined")
.hasSize(1);
assertThat(searchTestCls(classNodes, "TestCls"))
.code()
.doesNotContain("Lambda$1.INSTANCE")
.containsOne("toMap(list, new Function<T, T>() {");
}
}
@@ -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,36 @@
package jadx.tests.integration.java8;
import org.junit.jupiter.api.Test;
import jadx.tests.api.IntegrationTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestLambdaInstance2 extends IntegrationTest {
public static class TestCls {
private String field;
public Runnable test(String str, int i) {
return () -> call(str, i);
}
public void call(String str, int i) {
field = str + '=' + i;
}
public void check() throws Exception {
field = "";
test("num", 7).run();
assertThat(field).isEqualTo("num=7");
}
}
@Test
public void test() {
assertThat(getClassNode(TestCls.class))
.code()
.doesNotContain("lambda$")
.containsOne("call(str, i)");
}
}
@@ -0,0 +1,61 @@
package jadx.tests.integration.java8;
import org.junit.jupiter.api.Test;
import jadx.tests.api.IntegrationTest;
import jadx.tests.api.extensions.profiles.TestProfile;
import jadx.tests.api.extensions.profiles.TestWithProfiles;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestLambdaReturn extends IntegrationTest {
@SuppressWarnings("unused")
public static class TestCls {
interface Function0<R> {
R apply();
}
public static class T2 {
public long l;
public T2(long l) {
this.l = l;
}
public void w() {
}
}
public Byte test(Byte b1) {
Function0<Void> f1 = () -> {
new T2(94L).w();
return null;
};
f1.apply();
return null;
}
}
@TestWithProfiles(TestProfile.DX_J8)
public void test() {
assertThat(getClassNode(TestCls.class))
.code()
.containsLines(2,
"Function0<Void> f1 = () -> {",
indent() + "new T2(94L).w();",
indent() + "return null;",
"};");
}
@TestWithProfiles(TestProfile.D8_J11_DESUGAR)
public void testLambda() {
getClassNode(TestCls.class);
}
@Test
public void testNoDebug() {
noDebugInfo();
getClassNode(TestCls.class);
}
}
@@ -0,0 +1,43 @@
package jadx.tests.integration.others;
import org.junit.jupiter.api.Test;
import jadx.tests.api.IntegrationTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestNullInline extends IntegrationTest {
@SuppressWarnings({ "RedundantCast", "DataFlowIssue", "unused" })
public static class TestCls {
public static Long test(Double d1) {
T1<T2, Byte> t1 = (T1<T2, Byte>) null;
return t1.t2.l;
}
static class T2 {
public long l;
}
static class T1<H, P extends Byte> {
public T2 t2;
public T1(T2 t2) {
this.t2 = t2;
}
}
}
@Test
public void test() {
assertThat(getClassNode(TestCls.class))
.code()
.containsOne("Long.valueOf(t1.t2.l);");
}
@Test
public void testNoDebug() {
noDebugInfo();
getClassNode(TestCls.class);
}
}
@@ -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,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" android:compileSdkVersion="33" android:compileSdkVersionCodename="13" package="jadx.test.app" platformBuildVersionCode="33" platformBuildVersionName="13">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="32"/>
<application android:label="JadxTestApp/\:?*|">
</application>
</manifest>
@@ -0,0 +1,40 @@
.class public Linline/Lambda$1;
.super Ljava/lang/Object;
.implements Ljava/util/function/Function;
.annotation system Ldalvik/annotation/Signature;
value = {
"Ljava/lang/Object;",
"Ljava/util/function/Function",
"<TT;TT;>;"
}
.end annotation
.field public static final INSTANCE:Linline/Lambda$1;
.method static constructor <clinit>()V
.registers 1
new-instance v0, Linline/Lambda$1;
invoke-direct {v0}, Linline/Lambda$1;-><init>()V
sput-object v0, Linline/Lambda$1;->INSTANCE:Linline/Lambda$1;
return-void
.end method
.method private constructor <init>()V
.registers 1
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
.method public final apply(Ljava/lang/Object;)Ljava/lang/Object;
.registers 2
.annotation system Ldalvik/annotation/Signature;
value = {
"(TT;)TT;"
}
.end annotation
return-object p1
.end method
@@ -0,0 +1,42 @@
.class public Linline/TestCls;
.super Ljava/lang/Object;
.method public test(Ljava/util/List;)Ljava/util/Map;
.registers 3
.annotation system Ldalvik/annotation/Signature;
value = {
"<T:",
"Ljava/lang/Object;",
">(",
"Ljava/util/List",
"<+TT;>;)",
"Ljava/util/Map",
"<TT;TT;>;"
}
.end annotation
sget-object v0, Linline/Lambda$1;->INSTANCE:Linline/Lambda$1;
invoke-static {p1, v0}, Linline/TestCls;->toMap(Ljava/util/List;Ljava/util/function/Function;)Ljava/util/Map;
move-result-object v0
return-object v0
.end method
.method private static toMap(Ljava/util/List;Ljava/util/function/Function;)Ljava/util/Map;
.registers 4
.annotation system Ldalvik/annotation/Signature;
value = {
"<T:",
"Ljava/lang/Object;",
">(",
"Ljava/util/List",
"<+TT;>;",
"Ljava/util/function/Function",
"<TT;TT;>;)",
"Ljava/util/Map",
"<TT;TT;>;"
}
.end annotation
const/4 v0, 0x0
return-object v0
.end method
@@ -0,0 +1,77 @@
.class public Linvoke/TestPolymorphicInvoke;
.super Ljava/lang/Object;
.method public func(II)Ljava/lang/String;
.registers 4
.param p1, "a" # I
.param p2, "c" # I
.line 23
add-int v0, p1, p2
invoke-static {v0}, Ljava/lang/String;->valueOf(I)Ljava/lang/String;
move-result-object v0
return-object v0
.end method
.method public test()V
.registers 7
.line 32
:try_start_0
invoke-static {}, Ljava/lang/invoke/MethodHandles;->lookup()Ljava/lang/invoke/MethodHandles$Lookup;
move-result-object v0
.line 33
.local v0, "lookup":Ljava/lang/invoke/MethodHandles$Lookup;
const-class v1, Ljava/lang/String;
sget-object v2, Ljava/lang/Integer;->TYPE:Ljava/lang/Class;
const/4 v3, 0x1
new-array v3, v3, [Ljava/lang/Class;
const/4 v4, 0x0
sget-object v5, Ljava/lang/Integer;->TYPE:Ljava/lang/Class;
aput-object v5, v3, v4
invoke-static {v1, v2, v3}, Ljava/lang/invoke/MethodType;->methodType(Ljava/lang/Class;Ljava/lang/Class;[Ljava/lang/Class;)Ljava/lang/invoke/MethodType;
move-result-object v1
.line 34
.local v1, "methodType":Ljava/lang/invoke/MethodType;
const-class v2, Linvoke/TestPolymorphicInvoke;
const-string v3, "func"
invoke-virtual {v0, v2, v3, v1}, Ljava/lang/invoke/MethodHandles$Lookup;->findVirtual(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle;
move-result-object v2
.line 35
.local v2, "methodHandle":Ljava/lang/invoke/MethodHandle;
const/16 v3, 0xa
const/16 v4, 0x14
invoke-polymorphic {v2, p0, v3, v4}, Ljava/lang/invoke/MethodHandle;->invoke([Ljava/lang/Object;)Ljava/lang/Object;, (Linvoke/TestPolymorphicInvoke;II)Ljava/lang/String;
move-result-object v3
.line 36
.local v3, "ret":Ljava/lang/String;
sget-object v4, Ljava/lang/System;->out:Ljava/io/PrintStream;
invoke-virtual {v4, v3}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
:try_end_2a
.catchall {:try_start_0 .. :try_end_2a} :catchall_2b
.line 39
.end local v0 # "lookup":Ljava/lang/invoke/MethodHandles$Lookup;
.end local v1 # "methodType":Ljava/lang/invoke/MethodType;
.end local v2 # "methodHandle":Ljava/lang/invoke/MethodHandle;
.end local v3 # "ret":Ljava/lang/String;
goto :goto_2f
.line 37
:catchall_2b
move-exception v0
.line 38
.local v0, "e":Ljava/lang/Throwable;
invoke-virtual {v0}, Ljava/lang/Throwable;->printStackTrace()V
.line 40
.end local v0 # "e":Ljava/lang/Throwable;
:goto_2f
return-void
.end method
@@ -0,0 +1,62 @@
.class public Linvoke/TestRawCustomInvoke;
.super Ljava/lang/Object;
.method public static func(ID)Ljava/lang/String;
.registers 5
int-to-double v0, p0
add-double/2addr v0, p1
invoke-static {v0, v1}, Ljava/lang/String;->valueOf(D)Ljava/lang/String;
move-result-object p0
return-object p0
.end method
.method private static staticBootstrap(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
.registers 5
:try_start_0
new-instance v0, Ljava/lang/invoke/ConstantCallSite;
invoke-virtual {p0}, Ljava/lang/invoke/MethodHandles$Lookup;->lookupClass()Ljava/lang/Class;
move-result-object v1
invoke-virtual {p0, v1, p1, p2}, Ljava/lang/invoke/MethodHandles$Lookup;->findStatic(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle;
move-result-object p0
invoke-direct {v0, p0}, Ljava/lang/invoke/ConstantCallSite;-><init>(Ljava/lang/invoke/MethodHandle;)V
:try_end_d
.catch Ljava/lang/NoSuchMethodException; {:try_start_0 .. :try_end_d} :catch_e
.catch Ljava/lang/IllegalAccessException; {:try_start_0 .. :try_end_d} :catch_e
return-object v0
:catch_e
move-exception p0
new-instance p1, Ljava/lang/RuntimeException;
invoke-direct {p1, p0}, Ljava/lang/RuntimeException;-><init>(Ljava/lang/Throwable;)V
throw p1
.end method
.method public test()Ljava/lang/String;
.registers 3
:try_start_0
const/4 v0, 0x1
const-wide/high16 v1, 0x4000000000000000L # 2.0
invoke-custom {v0, v1}, call_site_0("func", (ID)Ljava/lang/String;)@Linvoke/TestRawCustomInvoke;->staticBootstrap(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
move-result-object v0
:try_end_25
.catchall {:try_start_0 .. :try_end_25} :catchall_26
return-object v0
:catchall_26
move-exception v0
invoke-static {v0}, Lorg/junit/jupiter/api/Assertions;->fail(Ljava/lang/Throwable;)Ljava/lang/Object;
const/4 v0, 0x0
return-object v0
.end method
.method public check()V
.registers 3
invoke-virtual {p0}, Linvoke/TestRawCustomInvoke;->test()Ljava/lang/String;
move-result-object v0
invoke-static {v0}, Ljadx/tests/api/utils/assertj/JadxAssertions;->assertThat(Ljava/lang/String;)Ljadx/tests/api/utils/assertj/JadxCodeAssertions;
move-result-object v0
const-string v1, "3.0"
invoke-virtual {v0, v1}, Ljadx/tests/api/utils/assertj/JadxCodeAssertions;->isEqualTo(Ljava/lang/String;)Lorg/assertj/core/api/AbstractStringAssert;
return-void
.end method
@@ -0,0 +1,8 @@
.class interface abstract Ltest/A;
.super Ljava/lang/Object;
.method public abstract a()Ltest/B;
.end method
.method public abstract a()Ltest/C;
.end method
@@ -0,0 +1,10 @@
.class abstract Ltest/B;
.super Ljava/lang/Object;
.implements Ltest/A;
.method public a()Ltest/C;
.registers 2
const/4 v0, 0x0
return-object v0
.end method
@@ -0,0 +1,8 @@
.class public Ltest/C;
.super Ltest/B;
.method public a()Ltest/B;
.registers 2
const/4 v0, 0x0
return-object v0
.end method
+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:
@@ -14,12 +14,10 @@ import java.util.concurrent.Executors;
import javax.swing.JOptionPane;
import javax.swing.tree.DefaultMutableTreeNode;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.reactivex.annotations.NonNull;
import io.reactivex.annotations.Nullable;
import jadx.core.dex.info.FieldInfo;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.nodes.ClassNode;
@@ -42,6 +40,7 @@ import jadx.gui.ui.panel.JDebuggerPanel;
import jadx.gui.ui.panel.JDebuggerPanel.IListElement;
import jadx.gui.ui.panel.JDebuggerPanel.ValueTreeNode;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
public final class DebugController implements SmaliDebugger.SuspendListener, IDebugController {
@@ -78,7 +77,7 @@ public final class DebugController implements SmaliDebugger.SuspendListener, IDe
initTypeMap();
}
this.debuggerPanel = debuggerPanel;
debuggerPanel.resetUI();
UiUtils.uiRunAndWait(debuggerPanel::resetUI);
try {
debugger = SmaliDebugger.attach(adbHost, adbPort, this);
} catch (SmaliDebuggerException e) {
@@ -251,7 +250,6 @@ public final class DebugController implements SmaliDebugger.SuspendListener, IDe
throw new JadxRuntimeException("Unexpected type: " + type);
}
@NonNull
protected static RuntimeType castType(String type) {
RuntimeType rt = null;
if (!StringUtils.isEmpty(type)) {
@@ -663,22 +661,22 @@ public final class DebugController implements SmaliDebugger.SuspendListener, IDe
}
private void updateAllRegisters(FrameNode frame) {
if (buildRegTreeNodes(frame).size() > 0) {
fetchAllRegisters(frame);
}
UiUtils.uiRun(() -> {
if (!buildRegTreeNodes(frame).isEmpty()) {
fetchAllRegisters(frame);
}
});
}
private void fetchAllRegisters(FrameNode frame) {
List<SmaliRegister> regs = cur.regAdapter.getInitializedList(frame.getCodeOffset());
for (SmaliRegister reg : regs) {
lazyQueue.execute(() -> {
Entry<String, String> info = cur.regAdapter.getInfo(reg.getRuntimeRegNum(), frame.getCodeOffset());
RegTreeNode regNode = frame.getRegNodes().get(reg.getRegNum());
if (info != null) {
applyDbgInfo(regNode, info);
}
updateRegister(regNode, null, true);
});
Entry<String, String> info = cur.regAdapter.getInfo(reg.getRuntimeRegNum(), frame.getCodeOffset());
RegTreeNode regNode = frame.getRegNodes().get(reg.getRegNum());
if (info != null) {
applyDbgInfo(regNode, info);
}
updateRegister(regNode, null, true);
}
}
@@ -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;
}
}
@@ -20,7 +20,6 @@ import javax.swing.SwingWorker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.settings.JadxSettings;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.panel.ProgressPanel;
@@ -60,14 +59,6 @@ public class BackgroundExecutor {
return taskWorker;
}
public TaskStatus executeAndWait(IBackgroundTask task) {
try {
return execute(task).get();
} catch (Exception e) {
throw new JadxRuntimeException("Task execution error", e);
}
}
public synchronized void cancelAll() {
try {
taskRunning.values().forEach(Cancelable::cancel);
@@ -5,12 +5,15 @@ import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import javax.swing.JOptionPane;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.ICodeCache;
import jadx.api.JavaClass;
import jadx.gui.JadxWrapper;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
@@ -23,14 +26,16 @@ public class DecompileTask extends CancelableBackgroundTask {
return classCount * CLS_LIMIT + 5000;
}
private final MainWindow mainWindow;
private final JadxWrapper wrapper;
private final AtomicInteger complete = new AtomicInteger(0);
private int expectedCompleteCount;
private ProcessResult result;
public DecompileTask(JadxWrapper wrapper) {
this.wrapper = wrapper;
public DecompileTask(MainWindow mainWindow) {
this.mainWindow = mainWindow;
this.wrapper = mainWindow.getWrapper();
}
@Override
@@ -40,6 +45,10 @@ public class DecompileTask extends CancelableBackgroundTask {
@Override
public List<Runnable> scheduleJobs() {
if (mainWindow.getCacheObject().isFullDecompilationFinished()) {
return Collections.emptyList();
}
List<JavaClass> classes = wrapper.getIncludedClasses();
expectedCompleteCount = classes.size();
complete.set(0);
@@ -87,7 +96,41 @@ public class DecompileTask extends CancelableBackgroundTask {
+ ", time limit:{ total: " + timeLimit + "ms, per cls: " + CLS_LIMIT + "ms }"
+ ", status: " + taskInfo.getStatus());
}
this.result = new ProcessResult(skippedCls, taskInfo.getStatus(), timeLimit);
result = new ProcessResult(skippedCls, taskInfo.getStatus(), timeLimit);
wrapper.unloadClasses();
processDecompilationResults();
System.gc();
mainWindow.getCacheObject().setFullDecompilationFinished(skippedCls == 0);
}
private void processDecompilationResults() {
int skippedCls = result.getSkipped();
if (skippedCls == 0) {
return;
}
TaskStatus status = result.getStatus();
LOG.warn("Decompile and indexing of some classes skipped: {}, status: {}", skippedCls, status);
switch (status) {
case CANCEL_BY_USER: {
String reason = NLS.str("message.userCancelTask");
String message = NLS.str("message.indexIncomplete", reason, skippedCls);
JOptionPane.showMessageDialog(mainWindow, message);
break;
}
case CANCEL_BY_TIMEOUT: {
String reason = NLS.str("message.taskTimeout", result.getTimeLimit());
String message = NLS.str("message.indexIncomplete", reason, skippedCls);
JOptionPane.showMessageDialog(mainWindow, message);
break;
}
case CANCEL_BY_MEMORY: {
mainWindow.showHeapUsageBar();
JOptionPane.showMessageDialog(mainWindow, NLS.str("message.indexingClassesSkipped", skippedCls));
break;
}
}
}
@Override
@@ -5,6 +5,7 @@ import java.util.regex.Pattern;
import org.jetbrains.annotations.Nullable;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JResource;
public class SearchSettings {
@@ -13,6 +14,7 @@ public class SearchSettings {
private final boolean ignoreCase;
private JClass activeCls;
private JResource activeResource;
private Pattern regexPattern;
private ISearchMethod searchMethod;
@@ -64,6 +66,14 @@ public class SearchSettings {
this.activeCls = activeCls;
}
public JResource getActiveResource() {
return activeResource;
}
public void setActiveResource(JResource activeResource) {
this.activeResource = activeResource;
}
public ISearchMethod getSearchMethod() {
return searchMethod;
}
@@ -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();
@@ -1,6 +1,7 @@
package jadx.gui.search.providers;
import java.util.ArrayDeque;
import java.util.Collections;
import java.util.Deque;
import java.util.Enumeration;
import java.util.HashSet;
@@ -24,12 +25,15 @@ import jadx.gui.treemodel.JResSearchNode;
import jadx.gui.treemodel.JResource;
import jadx.gui.treemodel.JRoot;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.dialog.SearchDialog;
import jadx.gui.utils.NLS;
public class ResourceSearchProvider implements ISearchProvider {
private static final Logger LOG = LoggerFactory.getLogger(ResourceSearchProvider.class);
private final SearchSettings searchSettings;
private final Set<String> extSet;
private final SearchDialog searchDialog;
private final int sizeLimit;
private boolean anyExt;
@@ -39,11 +43,20 @@ public class ResourceSearchProvider implements ISearchProvider {
private final Deque<JResource> resQueue;
private int pos;
public ResourceSearchProvider(MainWindow mw, SearchSettings searchSettings) {
private int loadErrors = 0;
private int skipBySize = 0;
public ResourceSearchProvider(MainWindow mw, SearchSettings searchSettings, SearchDialog searchDialog) {
this.searchSettings = searchSettings;
this.sizeLimit = mw.getSettings().getSrhResourceSkipSize() * 1048576;
this.extSet = buildAllowedFilesExtensions(mw.getSettings().getSrhResourceFileExt());
this.resQueue = initResQueue(mw);
this.searchDialog = searchDialog;
JResource activeResource = searchSettings.getActiveResource();
if (activeResource != null) {
this.resQueue = new ArrayDeque<>(Collections.singleton(activeResource));
} else {
this.resQueue = initResQueue(mw);
}
}
@Override
@@ -93,32 +106,49 @@ public class ResourceSearchProvider implements ISearchProvider {
private @Nullable JResource getNextResFile(Cancelable cancelable) {
while (true) {
JResource node = resQueue.peekLast();
if (node == null) {
return null;
}
try {
node.loadNode();
} catch (Exception e) {
LOG.error("Error load resource node: {}", node, e);
resQueue.removeLast();
continue;
}
if (cancelable.isCanceled()) {
if (node == null || cancelable.isCanceled()) {
return null;
}
if (node.getType() == JResource.JResType.FILE) {
if (shouldProcess(node)) {
if (shouldProcess(node) && loadResNode(node)) {
return node;
}
resQueue.removeLast();
} else {
// dir
resQueue.removeLast();
loadResNode(node);
addChildren(node);
}
}
}
private void updateProgressInfo() {
StringBuilder sb = new StringBuilder();
if (loadErrors != 0) {
sb.append(" ").append(NLS.str("search_dialog.resources_load_errors", loadErrors));
}
if (skipBySize != 0) {
sb.append(" ").append(NLS.str("search_dialog.resources_skip_by_size", skipBySize));
}
if (sb.length() != 0) {
sb.append(" ").append(NLS.str("search_dialog.resources_check_logs"));
}
searchDialog.updateProgressLabel(sb.toString());
}
private boolean loadResNode(JResource node) {
try {
node.loadNode();
return true;
} catch (Exception e) {
LOG.error("Error load resource node: {}", node, e);
loadErrors++;
updateProgressInfo();
return false;
}
}
private void addChildren(JResource resNode) {
resQueue.addAll(resNode.getSubNodes());
}
@@ -167,19 +197,24 @@ public class ResourceSearchProvider implements ISearchProvider {
return false;
}
}
if (sizeLimit == 0) {
if (sizeLimit <= 0) {
return true;
}
try {
int charsCount = resNode.getCodeInfo().getCodeStr().length();
long size = charsCount * 8L;
if (size > sizeLimit) {
LOG.debug("Resource search skipped because of size limit: {} res size {} bytes", resNode, size);
LOG.info("Resource search skipped because of size limit. Resource '{}' size {} bytes, limit: {}",
resNode.getName(), size, sizeLimit);
skipBySize++;
updateProgressInfo();
return false;
}
return true;
} catch (Exception e) {
LOG.warn("Resource load error: {}", resNode, e);
loadErrors++;
updateProgressInfo();
return false;
}
}
@@ -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;
}
@@ -392,6 +401,10 @@ public class JadxSettings extends JadxCLIArgs {
this.inlineMethods = inlineMethods;
}
public void setAllowInlineKotlinLambda(boolean allowInlineKotlinLambda) {
this.allowInlineKotlinLambda = allowInlineKotlinLambda;
}
public void setExtractFinally(boolean extractFinally) {
this.extractFinally = extractFinally;
}
@@ -538,6 +538,13 @@ public class JadxSettingsWindow extends JDialog {
needReload();
});
JCheckBox inlineKotlinLambdas = new JCheckBox();
inlineKotlinLambdas.setSelected(settings.isAllowInlineKotlinLambda());
inlineKotlinLambdas.addItemListener(e -> {
settings.setAllowInlineKotlinLambda(e.getStateChange() == ItemEvent.SELECTED);
needReload();
});
JCheckBox extractFinally = new JCheckBox();
extractFinally.setSelected(settings.isExtractFinally());
extractFinally.addItemListener(e -> {
@@ -581,6 +588,7 @@ public class JadxSettingsWindow extends JDialog {
other.addRow(NLS.str("preferences.useDebugInfo"), useDebugInfo);
other.addRow(NLS.str("preferences.inlineAnonymous"), inlineAnonymous);
other.addRow(NLS.str("preferences.inlineMethods"), inlineMethods);
other.addRow(NLS.str("preferences.inlineKotlinLambdas"), inlineKotlinLambdas);
other.addRow(NLS.str("preferences.extractFinally"), extractFinally);
other.addRow(NLS.str("preferences.fsCaseSensitive"), fsCaseSensitive);
other.addRow(NLS.str("preferences.useDx"), useDx);
@@ -643,6 +651,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 +677,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;
@@ -94,7 +95,6 @@ import jadx.gui.device.debugger.BreakpointManager;
import jadx.gui.jobs.BackgroundExecutor;
import jadx.gui.jobs.DecompileTask;
import jadx.gui.jobs.ExportTask;
import jadx.gui.jobs.ProcessResult;
import jadx.gui.jobs.TaskStatus;
import jadx.gui.plugins.mappings.MappingExporter;
import jadx.gui.plugins.quark.QuarkDialog;
@@ -116,10 +116,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;
@@ -171,6 +172,7 @@ public class MainWindow extends JFrame {
private static final ImageIcon ICON_QUARK = UiUtils.openSvgIcon("ui/quark");
private static final ImageIcon ICON_PREF = UiUtils.openSvgIcon("ui/settings");
private static final ImageIcon ICON_DEOBF = UiUtils.openSvgIcon("ui/helmChartLock");
private static final ImageIcon ICON_DECOMPILE_ALL = UiUtils.openSvgIcon("ui/runAll");
private static final ImageIcon ICON_LOG = UiUtils.openSvgIcon("ui/logVerbose");
private static final ImageIcon ICON_INFO = UiUtils.openSvgIcon("ui/showInfos");
private static final ImageIcon ICON_DEBUGGER = UiUtils.openSvgIcon("ui/startDebugger");
@@ -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);
});
@@ -604,54 +608,17 @@ public class MainWindow extends JFrame {
new Timer().schedule(new TimerTask() {
@Override
public void run() {
waitDecompileTask();
requestFullDecompilation();
}
}, 1000);
}
}
private static final Object DECOMPILER_TASK_SYNC = new Object();
public void waitDecompileTask() {
synchronized (DECOMPILER_TASK_SYNC) {
try {
DecompileTask decompileTask = new DecompileTask(wrapper);
backgroundExecutor.executeAndWait(decompileTask);
backgroundExecutor.execute(decompileTask.getTitle(), wrapper::unloadClasses).get();
processDecompilationResults(decompileTask.getResult());
System.gc();
} catch (Exception e) {
LOG.error("Decompile task execution failed", e);
}
}
}
private void processDecompilationResults(ProcessResult decompile) {
int skippedCls = decompile.getSkipped();
if (skippedCls == 0) {
public void requestFullDecompilation() {
if (cacheObject.isFullDecompilationFinished()) {
return;
}
TaskStatus status = decompile.getStatus();
LOG.warn("Decompile and indexing of some classes skipped: {}, status: {}", skippedCls, status);
switch (status) {
case CANCEL_BY_USER: {
String reason = NLS.str("message.userCancelTask");
String message = NLS.str("message.indexIncomplete", reason, skippedCls);
JOptionPane.showMessageDialog(this, message);
break;
}
case CANCEL_BY_TIMEOUT: {
String reason = NLS.str("message.taskTimeout", decompile.getTimeLimit());
String message = NLS.str("message.indexIncomplete", reason, skippedCls);
JOptionPane.showMessageDialog(this, message);
break;
}
case CANCEL_BY_MEMORY: {
showHeapUsageBar();
JOptionPane.showMessageDialog(this, NLS.str("message.indexingClassesSkipped", skippedCls));
break;
}
}
backgroundExecutor.execute(new DecompileTask(this));
}
public void cancelBackgroundJobs() {
@@ -659,7 +626,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;
@@ -1037,6 +1004,10 @@ public class MainWindow extends JFrame {
commentSearchAction.putValue(Action.ACCELERATOR_KEY, getKeyStroke(KeyEvent.VK_SEMICOLON,
UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK));
ActionHandler decompileAllAction = new ActionHandler(ev -> requestFullDecompilation());
decompileAllAction.setNameAndDesc(NLS.str("menu.decompile_all"));
decompileAllAction.setIcon(ICON_DECOMPILE_ALL);
Action deobfAction = new AbstractAction(NLS.str("menu.deobfuscation"), ICON_DEOBF) {
@Override
public void actionPerformed(ActionEvent e) {
@@ -1148,6 +1119,7 @@ public class MainWindow extends JFrame {
JMenu tools = new JMenu(NLS.str("menu.tools"));
tools.setMnemonic(KeyEvent.VK_T);
tools.add(decompileAllAction);
tools.add(deobfMenuItem);
tools.add(quarkAction);
tools.add(openDeviceAction);
@@ -1227,6 +1199,7 @@ public class MainWindow extends JFrame {
exportAction.setEnabled(loaded);
saveProjectAsAction.setEnabled(loaded);
reload.setEnabled(loaded);
decompileAllAction.setEnabled(loaded);
deobfAction.setEnabled(loaded);
quarkAction.setEnabled(loaded);
return false;
@@ -1422,8 +1395,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 +1474,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 +1489,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 + "'";
}
}
@@ -475,28 +475,28 @@ public class ADBDialog extends JDialog implements ADB.DeviceStateListener, ADB.J
LOG.error("Failed to find device", e);
return;
}
node.tNode.removeAllChildren();
DefaultMutableTreeNode tempNode = null;
for (String s : procList) {
DefaultMutableTreeNode pnode = new DefaultMutableTreeNode(s);
node.tNode.add(pnode);
if (!debugSetter.expectPkg.isEmpty() && s.endsWith(debugSetter.expectPkg)) {
if (debugSetter.autoAttachPkg && debugSetter.device.equals(node.device)) {
debugSetter.set(node.device, debugSetter.ver, getPid(s), s);
if (attachProcess(mainWindow)) {
dispose();
return;
}
}
tempNode = pnode;
}
}
DefaultMutableTreeNode theNode = tempNode;
SwingUtilities.invokeLater(() -> {
node.tNode.removeAllChildren();
DefaultMutableTreeNode foundNode = null;
for (String procStr : procList) {
DefaultMutableTreeNode pnode = new DefaultMutableTreeNode(procStr);
node.tNode.add(pnode);
if (!debugSetter.expectPkg.isEmpty() && procStr.endsWith(debugSetter.expectPkg)) {
if (debugSetter.autoAttachPkg && debugSetter.device.equals(node.device)) {
debugSetter.set(node.device, debugSetter.ver, getPid(procStr), procStr);
if (attachProcess(mainWindow)) {
dispose();
return;
}
}
foundNode = pnode;
}
}
procTreeModel.reload(node.tNode);
procTree.expandPath(new TreePath(node.tNode.getPath()));
if (theNode != null) {
TreePath thePath = new TreePath(theNode.getPath());
if (foundNode != null) {
TreePath thePath = new TreePath(foundNode.getPath());
procTree.scrollPathToVisible(thePath);
procTree.setSelectionPath(thePath);
}
@@ -17,6 +17,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Objects;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
@@ -44,6 +45,12 @@ import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.Level;
import jadx.api.JavaClass;
import jadx.api.metadata.ICodeAnnotation;
import jadx.api.metadata.annotations.NodeDeclareRef;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode;
import jadx.gui.treemodel.JResSearchNode;
import jadx.gui.ui.MainWindow;
@@ -73,6 +80,7 @@ public abstract class CommonSearchDialog extends JFrame {
protected ResultsModel resultsModel;
protected ResultsTable resultsTable;
protected JLabel resultsInfoLabel;
protected JLabel progressInfoLabel;
protected JLabel warnLabel;
protected ProgressPanel progressPane;
@@ -142,13 +150,45 @@ public abstract class CommonSearchDialog extends JFrame {
JumpPosition jmpPos = new JumpPosition(((JResSearchNode) node).getResNode(), node.getPos());
tabbedPane.codeJump(jmpPos);
} else {
tabbedPane.codeJump(node);
if (!checkForRedirects(node)) {
tabbedPane.codeJump(node);
}
}
if (!mainWindow.getSettings().getKeepCommonDialogOpen()) {
dispose();
}
}
// TODO: temp solution, move implementation into corresponding nodes
private boolean checkForRedirects(JNode node) {
if (node instanceof JClass) {
JavaClass cls = ((JClass) node).getCls();
JavaClass origTopCls = cls.getOriginalTopParentClass();
JavaClass codeParent = cls.getTopParentClass();
if (Objects.equals(codeParent, origTopCls)) {
return false;
}
JClass jumpCls = mainWindow.getCacheObject().getNodeCache().makeFrom(codeParent);
mainWindow.getBackgroundExecutor().execute(
NLS.str("progress.load"),
jumpCls::loadNode, // load code in background
status -> {
// search original node in jump class
codeParent.getCodeInfo().getCodeMetadata().searchDown(0, (pos, ann) -> {
if (ann.getAnnType() == ICodeAnnotation.AnnType.DECLARATION) {
if (((NodeDeclareRef) ann).getNode().equals(cls.getClassNode())) {
tabbedPane.codeJump(new JumpPosition(jumpCls, pos));
return true;
}
}
return null;
});
});
return true;
}
return false;
}
@Nullable
private JNode getSelectedNode() {
try {
@@ -260,6 +300,15 @@ public abstract class CommonSearchDialog extends JFrame {
resultsInfoLabel = new JLabel("");
resultsInfoLabel.setFont(mainWindow.getSettings().getFont());
progressInfoLabel = new JLabel("");
progressInfoLabel.setFont(mainWindow.getSettings().getFont());
progressInfoLabel.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
LogViewerDialog.openWithLevel(mainWindow, Level.INFO);
}
});
JPanel resultsActionsPanel = new JPanel();
resultsActionsPanel.setLayout(new BoxLayout(resultsActionsPanel, BoxLayout.LINE_AXIS));
resultsActionsPanel.setBorder(BorderFactory.createEmptyBorder(10, 0, 10, 0));
@@ -276,6 +325,8 @@ public abstract class CommonSearchDialog extends JFrame {
protected void addResultsActions(JPanel resultsActionsPanel) {
resultsActionsPanel.add(Box.createRigidArea(new Dimension(20, 0)));
resultsActionsPanel.add(resultsInfoLabel);
resultsActionsPanel.add(Box.createRigidArea(new Dimension(20, 0)));
resultsActionsPanel.add(progressInfoLabel);
resultsActionsPanel.add(Box.createHorizontalGlue());
}
@@ -454,10 +505,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();
}
}
}
@@ -55,6 +55,7 @@ import jadx.gui.search.providers.MethodSearchProvider;
import jadx.gui.search.providers.ResourceSearchProvider;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode;
import jadx.gui.treemodel.JResource;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.JumpPosition;
import jadx.gui.utils.NLS;
@@ -452,9 +453,18 @@ public class SearchDialog extends CommonSearchDialog {
resultsInfoLabel.setText("Can't search in current tab");
return false;
}
JClass activeCls = currentPos.getNode().getRootClass();
searchSettings.setActiveCls(activeCls);
allClasses = Collections.singletonList(activeCls.getCls());
JNode currentNode = currentPos.getNode();
if (currentNode instanceof JClass) {
JClass activeCls = currentNode.getRootClass();
searchSettings.setActiveCls(activeCls);
allClasses = Collections.singletonList(activeCls.getCls());
} else if (currentNode instanceof JResource) {
searchSettings.setActiveResource((JResource) currentNode);
allClasses = Collections.emptyList();
} else {
resultsInfoLabel.setText("Can't search in current tab");
return false;
}
} else {
allClasses = mainWindow.getWrapper().getIncludedClassesWithInners();
}
@@ -475,9 +485,10 @@ public class SearchDialog extends CommonSearchDialog {
merged.add(new FieldSearchProvider(mainWindow, searchSettings, allClasses));
}
if (options.contains(CODE)) {
if (allClasses.size() == 1) {
int clsCount = allClasses.size();
if (clsCount == 1) {
newSearchTask.addProviderJob(new CodeSearchProvider(mainWindow, searchSettings, allClasses));
} else {
} else if (clsCount > 1) {
List<List<JavaClass>> batches = mainWindow.getCacheObject().getDecompileBatches();
if (batches == null) {
List<JavaClass> topClasses = ListUtils.filter(allClasses, c -> !c.isInner());
@@ -490,7 +501,7 @@ public class SearchDialog extends CommonSearchDialog {
}
}
if (options.contains(RESOURCE)) {
newSearchTask.addProviderJob(new ResourceSearchProvider(mainWindow, searchSettings));
newSearchTask.addProviderJob(new ResourceSearchProvider(mainWindow, searchSettings, this));
}
if (options.contains(COMMENT)) {
newSearchTask.addProviderJob(new CommentSearchProvider(mainWindow, searchSettings));
@@ -549,6 +560,7 @@ public class SearchDialog extends CommonSearchDialog {
synchronized (pendingResults) {
pendingResults.clear();
}
updateProgressLabel("");
progressPane.setVisible(false);
warnLabel.setVisible(false);
loadAllButton.setEnabled(false);
@@ -598,6 +610,10 @@ public class SearchDialog extends CommonSearchDialog {
});
}
public void updateProgressLabel(String text) {
UiUtils.uiRun(() -> progressInfoLabel.setText(text));
}
private void searchFinished(ITaskInfo status, Boolean complete) {
UiUtils.uiThreadGuard();
LOG.debug("Search complete: {}, complete: {}", status, complete);
@@ -19,11 +19,14 @@ import jadx.api.JavaClass;
import jadx.api.JavaMethod;
import jadx.api.JavaNode;
import jadx.api.utils.CodeUtils;
import jadx.core.dex.info.ConstStorage;
import jadx.core.dex.nodes.FieldNode;
import jadx.gui.JadxWrapper;
import jadx.gui.jobs.TaskStatus;
import jadx.gui.settings.JadxSettings;
import jadx.gui.treemodel.CodeNode;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JField;
import jadx.gui.treemodel.JMethod;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.MainWindow;
@@ -51,6 +54,7 @@ public class UsageDialog extends CommonSearchDialog {
@Override
protected void openInit() {
progressStartCommon();
prepareUsageData();
mainWindow.getBackgroundExecutor().execute(NLS.str("progress.load"),
this::collectUsageData,
(status) -> {
@@ -63,26 +67,39 @@ public class UsageDialog extends CommonSearchDialog {
});
}
private void prepareUsageData() {
if (mainWindow.getSettings().isReplaceConsts() && node instanceof JField) {
FieldNode fld = ((JField) node).getJavaField().getFieldNode();
boolean constField = ConstStorage.getFieldConstValue(fld) != null;
if (constField && !fld.getAccessFlags().isPrivate()) {
// run full decompilation to prepare for full code scan
mainWindow.requestFullDecompilation();
}
}
}
private void collectUsageData() {
usageList = new ArrayList<>();
Map<JavaNode, List<JavaNode>> usageQuery = buildUsageQuery();
usageQuery.forEach((searchNode, useNodes) -> useNodes.stream()
.map(JavaNode::getTopParentClass)
.distinct()
.forEach(u -> processUsage(searchNode, u)));
buildUsageQuery().forEach(
(searchNode, useNodes) -> useNodes.stream()
.map(JavaNode::getTopParentClass)
.distinct()
.forEach(u -> processUsage(searchNode, u)));
}
/**
* Return mapping of 'node to search' to 'use places'
*/
private Map<JavaNode, List<JavaNode>> buildUsageQuery() {
Map<JavaNode, List<JavaNode>> map = new HashMap<>();
private Map<JavaNode, List<? extends JavaNode>> buildUsageQuery() {
Map<JavaNode, List<? extends JavaNode>> map = new HashMap<>();
if (node instanceof JMethod) {
JavaMethod javaMethod = ((JMethod) node).getJavaMethod();
for (JavaMethod mth : getMethodWithOverrides(javaMethod)) {
map.put(mth, mth.getUseIn());
}
} else if (node instanceof JClass) {
return map;
}
if (node instanceof JClass) {
JavaClass javaCls = ((JClass) node).getCls();
map.put(javaCls, javaCls.getUseIn());
// add constructors usage into class usage
@@ -91,10 +108,19 @@ public class UsageDialog extends CommonSearchDialog {
map.put(javaMth, javaMth.getUseIn());
}
}
} else {
JavaNode javaNode = node.getJavaNode();
map.put(javaNode, javaNode.getUseIn());
return map;
}
if (node instanceof JField && mainWindow.getSettings().isReplaceConsts()) {
FieldNode fld = ((JField) node).getJavaField().getFieldNode();
boolean constField = ConstStorage.getFieldConstValue(fld) != null;
if (constField && !fld.getAccessFlags().isPrivate()) {
// search all classes to collect usage of replaced constants
map.put(fld.getJavaNode(), mainWindow.getWrapper().getIncludedClasses());
return map;
}
}
JavaNode javaNode = node.getJavaNode();
map.put(javaNode, javaNode.getUseIn());
return map;
}
@@ -108,9 +134,12 @@ public class UsageDialog extends CommonSearchDialog {
private void processUsage(JavaNode searchNode, JavaClass topUseClass) {
ICodeInfo codeInfo = topUseClass.getCodeInfo();
List<Integer> usePositions = topUseClass.getUsePlacesFor(codeInfo, searchNode);
if (usePositions.isEmpty()) {
return;
}
String code = codeInfo.getCodeStr();
JadxWrapper wrapper = mainWindow.getWrapper();
List<Integer> usePositions = topUseClass.getUsePlacesFor(codeInfo, searchNode);
for (int pos : usePositions) {
String line = CodeUtils.getLineForPos(code, pos);
if (line.startsWith("import ")) {
@@ -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
}

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