Compare commits

...

48 Commits

Author SHA1 Message Date
Skylot 09335395f5 doc: update option description 2022-02-20 16:51:36 +00:00
Skylot 57e3dd8f15 feat(cli): improve single file mode (#1344)(#1384) 2022-02-20 15:04:59 +00:00
Skylot a9bbadd602 feat: add option for deobfuscation map file handle mode (#1351) 2022-02-19 21:20:11 +03:00
skylot 2c570681f7 doc: add link to jadx-gui key bindings in readme 2022-02-18 20:26:39 +00:00
Skylot 25166970cc feat(gui): ctrl+c copy node string in search window (#293) 2022-02-18 19:10:56 +00:00
Skylot d3a0a56b8b feat(gui): ctrl+c copy highlighted word in code view (#1292) 2022-02-18 19:10:34 +00:00
YenKoc 3c2c198a0e feat(gui): add Xposed snippet copy action (PR #1383)
* add xposedscript
* fix code style and minor issues
* some code style changes for Xposed snippets
* some code style changes for Frida snippets + a fix for multidimensional arrays in overload params
* hide frida and xposed when right-clicking on a null node
* small style fix
* fixed formatting violations
* fix minor issues

Co-authored-by: Skylot <skylot@gmail.com>
Co-authored-by: Orip <oriori1703@gmail.com>
2022-02-18 12:54:41 +00:00
Skylot 4d4d67f0b4 fix: remove shadowed catch handlers (#1377) 2022-02-16 19:31:19 +00:00
Skylot 97e8a34906 fix: prevent some NPE in try/catch/finally processing (#1379) 2022-02-15 12:29:30 +00:00
Skylot 82f3b57e83 perf: improve ternary mod on big methods (#1379) 2022-02-15 12:03:06 +00:00
Skylot af2f14f807 fix: prevent endless loop in anonymous class analysis (#1382) 2022-02-14 23:23:02 +00:00
Skylot fe248d7098 fix: check values in inner class annotation (#1382) 2022-02-14 18:25:54 +00:00
Skylot 1a2e702b25 fix: inline nested anonymous classes (#1379) 2022-02-14 17:30:22 +00:00
Skylot 1da20b8e7d doc: update readme 2022-02-14 16:41:31 +00:00
Skylot 01f74ff706 chore: update gradle and dependencies 2022-02-13 19:08:49 +00:00
Skylot 89e95eb9ee fix: correct code reload after rename (#1378) 2022-02-12 19:15:18 +00:00
Skylot a61ebaaa00 fix: sum only sub dependencies in batches build (#1376) 2022-02-11 19:53:12 +00:00
xxjy 7a5a2fcd84 fix: nested try catches with overlap try blocks (#1374)(PR #1375)
* fix: nested try catch decompilation failed (#1374)
* add tests and sort handlers

Co-authored-by: Skylot <skylot@gmail.com>
2022-02-09 20:55:15 +00:00
Jan S 8d5554f1b5 fix(gui): frida context menu entry does nothing (#1365)(PR #1372) 2022-02-08 12:47:49 +00:00
Ori Perry 873aabb471 fix: use raw class names in Frida action (#1365)(PR #1366)
* Use raw_name instead of full_name for the names of class in generated frida snippet.
Also cleaned the code a bit

* Fixed getting method parameters from inlined methods

* fixed generating code for constructor overloads, more cleaning

* Fixed getting method parameters from inlined methods for real this time

* made the option for a frida snippet only appear if clicked on a relevant node

* added support for generating a frida snippet for fields

* apply spotless

* Update jadx-gui/src/main/java/jadx/gui/ui/codearea/FridaAction.java

Co-authored-by: skylot <118523+skylot@users.noreply.github.com>

* moved the overload check from NodeMethod to FridaAction

* added semicolons in the end of lines of the generated frida snippet

* fix code formatting
2022-02-07 21:50:01 +00:00
cyqw 4bed9dc358 fix(gui): results in usage search should be sorted by name (PR #1363) 2022-02-07 15:39:57 +00:00
nitram84 e229874195 fix: check if targetSdkVersion is missing in gradle export (#1367)(PR #1370) 2022-02-07 10:39:09 +00:00
Skylot 473b6e31e9 fix: support multi-entry loops (simple case) (#1320) 2022-02-06 18:36:33 +00:00
Jan S b5ce460618 feat(deobf): do not deobfuscate known top level domains with 2 or 3 characters (PR #1369) 2022-02-06 12:56:59 +00:00
Skylot 3c05b05196 fix: check names from Kotlin metadata before use (#1364) 2022-02-05 21:49:36 +00:00
Skylot bdb2efdb6b fix(res): remove static caching map for xml renames (#1364) 2022-02-05 20:23:44 +00:00
Skylot a27ba3ff4b fix(res): skip '.9.png' decode if patch data not found (#1112) 2022-02-05 17:45:08 +00:00
Skylot 4684207b54 fix: remove duplicate classes from decompilation batches (#1361) 2022-02-05 17:45:07 +00:00
Skylot dd1be3039b fix(gui): split decompile and index tasks for correct time counting (#1361) 2022-02-05 17:45:07 +00:00
Skylot 8b30b770cd fix(gui): missing icons and html decorations in usage dialog 2022-02-05 13:36:26 +00:00
Yotam 47caa91e85 fix(cli): fix and add debug log messages in initialization phase (PR #1362)
* Fix log level settings in the CLI
* Add log messages in initialization phase
2022-02-02 19:04:19 +00:00
Skylot d71f3e09df fix: prevent endless loop in path cross search (#1360) 2022-02-01 14:32:44 +00:00
Jan S 06c7415827 fix(res): improved decoding of flag attributes in binary XML files (#1156)(PR #1359) 2022-01-31 18:00:50 +00:00
Skylot bd3e62617e fix: correct inline for enums in j$.time.temporal 2022-01-31 11:49:59 +00:00
Skylot 00b48473a0 test: add internal option to disable file save 2022-01-31 10:27:20 +00:00
Skylot 84facb13d0 fix: don't inline named variables (#1338) 2022-01-28 18:33:38 +00:00
Skylot 96f90e18e8 fix: improve exception handlers attach 2022-01-26 15:43:40 +00:00
Skylot 8ff18e63ee chore: update dependencies 2022-01-25 18:51:43 +00:00
Skylot 381405ea99 fix: always use deep resolve for fields and methods (#1357) 2022-01-25 11:37:36 +00:00
Ahmet Bilal Can ae5c00397a feat(gui): add frida action to copy methods/classes as frida snippets (#1355)(PR #1356)
* add frida action to copy methods/classes as frida snippets
* bug: call toString before comparing
2022-01-24 21:37:12 +00:00
Skylot bd4509f1a7 fix: update field usage on const replace (#1348) 2022-01-24 18:22:43 +00:00
Skylot b8c84886a8 fix: correct use of class names for inner types (#1340) 2022-01-24 14:11:40 +00:00
Skylot 45021389bc fix: correct method arg name if unused 2022-01-24 13:38:49 +00:00
Yotam f674a29a64 fix(deobf): rename classes as anonymous only if they are a number (PR #1354) 2022-01-23 21:16:05 +00:00
Yotam 0c9e3227d0 fix(deobf): collect missing renames for .jobf file (#1350)(PR #1353) 2022-01-23 16:08:54 +00:00
cyqw be7e1479a1 fix(gui): find usage for overridden methods (#1349)(PR #1352) 2022-01-23 16:06:13 +00:00
Skylot 19827fca20 fix: support full class name in inner generic types (#1340) 2022-01-22 18:49:31 +00:00
Skylot 5eb7cc40ed feat: check dex checksum before parsing (#1343) 2022-01-20 19:24:49 +00:00
155 changed files with 3707 additions and 665 deletions
+14 -5
View File
@@ -5,13 +5,14 @@
[![Build status](https://github.com/skylot/jadx/workflows/Build/badge.svg)](https://github.com/skylot/jadx/actions?query=workflow%3ABuild)
[![Alerts from lgtm.com](https://img.shields.io/lgtm/alerts/g/skylot/jadx.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/skylot/jadx/alerts/)
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
[![Maven Central](https://img.shields.io/maven-central/v/io.github.skylot/jadx-core)](https://search.maven.org/search?q=g:io.github.skylot%20AND%20jadx)
[![License](http://img.shields.io/:license-apache-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0.html)
**jadx** - Dex to Java decompiler
Command line and GUI tools for producing Java source code from Android Dex and Apk files
:exclamation: :exclamation: :exclamation: Please note that in most cases Jadx can't decompile all 100% of the code, so errors will occur. Check [Troubleshooting guide](https://github.com/skylot/jadx/wiki/Troubleshooting-Q&A#decompilation-issues) for workarounds
:exclamation::exclamation::exclamation: Please note that in most cases **jadx** can't decompile all 100% of the code, so errors will occur. Check [Troubleshooting guide](https://github.com/skylot/jadx/wiki/Troubleshooting-Q&A#decompilation-issues) for workarounds
**Main features:**
- decompile Dalvik bytecode to java classes from APK, dex, aar, aab and zip files
@@ -23,7 +24,9 @@ Command line and GUI tools for producing Java source code from Android Dex and A
- jump to declaration
- find usage
- full text search
- smali debugger (thanks to [@LBJ-the-GOAT](https://github.com/LBJ-the-GOAT)), check [wiki page](https://github.com/skylot/jadx/wiki/Smali-debugger) for setup and usage
- smali debugger, check [wiki page](https://github.com/skylot/jadx/wiki/Smali-debugger) for setup and usage
Jadx-gui key bindings can be found [here](https://github.com/skylot/jadx/wiki/JADX-GUI-Key-bindings)
See these features in action here: [jadx-gui features overview](https://github.com/skylot/jadx/wiki/jadx-gui-features-overview)
@@ -39,7 +42,7 @@ After download unpack zip file go to `bin` directory and run:
On Windows run `.bat` files with double-click\
**Note:** ensure you have installed Java 11 or later 64-bit version.
For windows you can download it from [oracle.com](https://www.oracle.com/java/technologies/downloads/#jdk17-windows) (select x64 Installer).
For Windows, you can download it from [oracle.com](https://www.oracle.com/java/technologies/downloads/#jdk17-windows) (select x64 Installer).
### Install
1. Arch linux
@@ -76,7 +79,8 @@ options:
-dr, --output-dir-res - output directory for resources
-r, --no-res - do not decode resources
-s, --no-src - do not decompile source code
--single-class - decompile a single class
--single-class - decompile a single class, full name, raw or alias
--single-class-output - file or dir for write if decompile a single class
--output-format - can be 'java' or 'json', default: java
-e, --export-gradle - save as android gradle project
-j, --threads-count - processing threads count, default: 4
@@ -93,7 +97,12 @@ options:
--deobf-min - min length of name, renamed if shorter, default: 3
--deobf-max - max length of name, renamed if longer, default: 64
--deobf-cfg-file - deobfuscation map file, default: same dir and name as input file with '.jobf' extension
--deobf-rewrite-cfg - force to ignore and overwrite deobfuscation map file
--deobf-cfg-file-mode - set mode for handle deobfuscation map file:
'read' - read if found, don't save (default)
'read-or-save' - read if found, save otherwise (don't overwrite)
'overwrite' - don't read, always save
'ignore' - don't read and don't save
--deobf-rewrite-cfg - set '--deobf-cfg-file-mode' to 'overwrite' (deprecated)
--deobf-use-sourcename - use source file name as class name alias
--deobf-parse-kotlin-metadata - parse kotlin metadata to class and package names
--use-kotlin-methods-for-var-names - use kotlin intrinsic methods to rename variables, values: disable, apply, apply-and-hide, default: apply
+7 -6
View File
@@ -1,6 +1,6 @@
plugins {
id 'com.github.ben-manes.versions' version '0.41.0'
id 'com.diffplug.spotless' version '6.2.0'
id 'com.github.ben-manes.versions' version '0.42.0'
id 'com.diffplug.spotless' version '6.2.2'
}
ext.jadxVersion = System.getenv('JADX_VERSION') ?: "dev"
@@ -27,12 +27,12 @@ allprojects {
}
dependencies {
implementation 'org.slf4j:slf4j-api:1.7.33'
implementation 'org.slf4j:slf4j-api:1.7.36'
compileOnly 'org.jetbrains:annotations:23.0.0'
testImplementation 'ch.qos.logback:logback-classic:1.2.10'
testImplementation 'org.hamcrest:hamcrest-library:2.2'
testImplementation 'org.mockito:mockito-core:4.2.0'
testImplementation 'org.mockito:mockito-core:4.3.1'
testImplementation 'org.assertj:assertj-core:3.22.0'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
@@ -67,8 +67,9 @@ spotless {
if (JavaVersion.current() < JavaVersion.VERSION_16) {
removeUnusedImports()
} else {
// google-format broken on java 16 (https://github.com/diffplug/spotless/issues/834)
println('Warning! Unused imports remove is disabled for Java 16')
// google-format on Java 16+ issue: https://github.com/diffplug/spotless/issues/834
println('Warning! Unused imports remove is disabled for Java 16+'
+ ' (use workaround from https://github.com/diffplug/spotless/tree/main/plugin-gradle#google-java-format)')
}
lineEndings(com.diffplug.spotless.LineEnding.UNIX)
Binary file not shown.
+2 -2
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=b586e04868a22fd817c8971330fec37e298f3242eb85c374181b12d637f80302
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
distributionSha256Sum=8cc27038d5dbd815759851ba53e70cf62e481b87494cc97cfd97982ada5ba634
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
@@ -94,7 +94,7 @@ public class JCommanderWrapper<T> {
opt.append("- ").append(description);
}
String defaultValue = getDefaultValue(args, f, opt);
if (defaultValue != null) {
if (defaultValue != null && !description.contains("(default)")) {
opt.append(", default: ").append(defaultValue);
}
out.println(opt);
+16 -9
View File
@@ -32,23 +32,19 @@ public class JadxCLI {
public static int execute(String[] args) {
JadxCLIArgs jadxArgs = new JadxCLIArgs();
if (jadxArgs.processArgs(args)) {
return processAndSave(jadxArgs.toJadxArgs());
return processAndSave(jadxArgs);
}
return 0;
}
private static int processAndSave(JadxArgs jadxArgs) {
private static int processAndSave(JadxCLIArgs cliArgs) {
JadxArgs jadxArgs = cliArgs.toJadxArgs();
jadxArgs.setCodeCache(new NoOpCodeCache());
jadxArgs.setCodeWriterProvider(SimpleCodeWriter::new);
try (JadxDecompiler jadx = new JadxDecompiler(jadxArgs)) {
jadx.load();
if (LogHelper.getLogLevel() == LogHelper.LogLevelEnum.QUIET) {
jadx.save();
} else {
jadx.save(500, (done, total) -> {
int progress = (int) (done * 100.0 / total);
System.out.printf("INFO - progress: %d of %d (%d%%)\r", done, total, progress);
});
if (!SingleClassMode.process(jadx, cliArgs)) {
save(jadx);
}
int errorsCount = jadx.getErrorsCount();
if (errorsCount != 0) {
@@ -60,4 +56,15 @@ public class JadxCLI {
}
return 0;
}
private static void save(JadxDecompiler jadx) {
if (LogHelper.getLogLevel() == LogHelper.LogLevelEnum.QUIET) {
jadx.save();
} else {
jadx.save(500, (done, total) -> {
int progress = (int) (done * 100.0 / total);
System.out.printf("INFO - progress: %d of %d (%d%%)\r", done, total, progress);
});
}
}
}
@@ -16,6 +16,7 @@ import jadx.api.JadxArgs;
import jadx.api.JadxArgs.RenameEnum;
import jadx.api.JadxArgs.UseKotlinMethodsForVarNames;
import jadx.api.JadxDecompiler;
import jadx.api.args.DeobfuscationMapFileMode;
import jadx.core.utils.exceptions.JadxException;
import jadx.core.utils.files.FileUtils;
@@ -39,9 +40,12 @@ public class JadxCLIArgs {
@Parameter(names = { "-s", "--no-src" }, description = "do not decompile source code")
protected boolean skipSources = false;
@Parameter(names = { "--single-class" }, description = "decompile a single class")
@Parameter(names = { "--single-class" }, description = "decompile a single class, full name, raw or alias")
protected String singleClass = null;
@Parameter(names = { "--single-class-output" }, description = "file or dir for write if decompile a single class")
protected String singleClassOutput = null;
@Parameter(names = { "--output-format" }, description = "can be 'java' or 'json'")
protected String outputFormat = "java";
@@ -93,7 +97,18 @@ public class JadxCLIArgs {
)
protected String deobfuscationMapFile;
@Parameter(names = { "--deobf-rewrite-cfg" }, description = "force to ignore and overwrite deobfuscation map file")
@Parameter(
names = { "--deobf-cfg-file-mode" },
description = "set mode for handle deobfuscation map file:"
+ "\n 'read' - read if found, don't save (default)"
+ "\n 'read-or-save' - read if found, save otherwise (don't overwrite)"
+ "\n 'overwrite' - don't read, always save"
+ "\n 'ignore' - don't read and don't save",
converter = DeobfuscationMapFileModeConverter.class
)
protected DeobfuscationMapFileMode deobfuscationMapFileMode = DeobfuscationMapFileMode.READ;
@Parameter(names = { "--deobf-rewrite-cfg" }, description = "set '--deobf-cfg-file-mode' to 'overwrite' (deprecated)")
protected boolean deobfuscationForceSave = false;
@Parameter(names = { "--deobf-use-sourcename" }, description = "use source file name as class name alias")
@@ -215,9 +230,6 @@ public class JadxCLIArgs {
args.setOutputFormat(JadxArgs.OutputFormatEnum.valueOf(outputFormat.toUpperCase()));
args.setThreadsCount(threadsCount);
args.setSkipSources(skipSources);
if (singleClass != null) {
args.setClassFilter(className -> singleClass.equals(className));
}
args.setSkipResources(skipResources);
args.setFallbackMode(fallbackMode);
args.setShowInconsistentCode(showInconsistentCode);
@@ -226,7 +238,11 @@ public class JadxCLIArgs {
args.setReplaceConsts(replaceConsts);
args.setDeobfuscationOn(deobfuscationOn);
args.setDeobfuscationMapFile(FileUtils.toFile(deobfuscationMapFile));
args.setDeobfuscationForceSave(deobfuscationForceSave);
if (deobfuscationForceSave) {
args.setDeobfuscationMapFileMode(DeobfuscationMapFileMode.OVERWRITE);
} else {
args.setDeobfuscationMapFileMode(deobfuscationMapFileMode);
}
args.setDeobfuscationMinLength(deobfuscationMinLength);
args.setDeobfuscationMaxLength(deobfuscationMaxLength);
args.setUseSourceNameAsClassAlias(deobfuscationUseSourceNameAsAlias);
@@ -263,6 +279,14 @@ public class JadxCLIArgs {
return outDirRes;
}
public String getSingleClass() {
return singleClass;
}
public String getSingleClassOutput() {
return singleClassOutput;
}
public boolean isSkipResources() {
return skipResources;
}
@@ -323,6 +347,10 @@ public class JadxCLIArgs {
return deobfuscationMapFile;
}
public DeobfuscationMapFileMode getDeobfuscationMapFileMode() {
return deobfuscationMapFileMode;
}
public boolean isDeobfuscationForceSave() {
return deobfuscationForceSave;
}
@@ -438,6 +466,19 @@ public class JadxCLIArgs {
}
}
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 String enumValuesString(Enum<?>[] values) {
return Stream.of(values)
.map(v -> v.name().replace('_', '-').toLowerCase(Locale.ROOT))
@@ -54,10 +54,11 @@ public class LogHelper {
Logger rootLogger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
rootLogger.setLevel(logLevel.getLevel());
if (logLevel != LogLevelEnum.QUIET) {
if (logLevel == LogLevelEnum.PROGRESS) {
// show progress for all levels except quiet
setLevelForClass(JadxCLI.class, Level.INFO);
setLevelForClass(JadxDecompiler.class, Level.INFO);
setLevelForClass(SingleClassMode.class, Level.INFO);
}
}
@@ -0,0 +1,87 @@
package jadx.cli;
import java.io.File;
import java.util.List;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.ICodeInfo;
import jadx.api.JadxDecompiler;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.visitors.SaveCode;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
public class SingleClassMode {
private static final Logger LOG = LoggerFactory.getLogger(SingleClassMode.class);
public static boolean process(JadxDecompiler jadx, JadxCLIArgs cliArgs) {
String singleClass = cliArgs.getSingleClass();
String singleClassOutput = cliArgs.getSingleClassOutput();
if (singleClass == null && singleClassOutput == null) {
return false;
}
ClassNode clsForProcess;
if (singleClass != null) {
clsForProcess = jadx.getRoot().resolveClass(singleClass);
if (clsForProcess == null) {
clsForProcess = jadx.getRoot().getClasses().stream()
.filter(cls -> cls.getClassInfo().getAliasFullName().equals(singleClass))
.findFirst().orElse(null);
}
if (clsForProcess == null) {
throw new JadxRuntimeException("Input class not found: " + singleClass);
}
if (clsForProcess.contains(AFlag.DONT_GENERATE)) {
throw new JadxRuntimeException("Input class can't be saved by currect jadx settings (marked as DONT_GENERATE)");
}
if (clsForProcess.isInner()) {
clsForProcess = clsForProcess.getTopParentClass();
LOG.warn("Input class is inner, parent class will be saved: {}", clsForProcess.getFullName());
}
} else {
// singleClassOutput is set
// expect only one class to be loaded
List<ClassNode> classes = jadx.getRoot().getClasses().stream()
.filter(c -> !c.isInner() && !c.contains(AFlag.DONT_GENERATE))
.collect(Collectors.toList());
int size = classes.size();
if (size == 1) {
clsForProcess = classes.get(0);
} else {
throw new JadxRuntimeException("Found " + size + " classes, single class output can't be used");
}
}
ICodeInfo codeInfo;
try {
codeInfo = clsForProcess.decompile();
} catch (Exception e) {
throw new JadxRuntimeException("Class decompilation failed", e);
}
String fileExt = SaveCode.getFileExtension(jadx.getRoot());
File out;
if (singleClassOutput == null) {
out = new File(jadx.getArgs().getOutDirSrc(), clsForProcess.getClassInfo().getAliasFullPath() + fileExt);
} else {
if (singleClassOutput.endsWith(fileExt)) {
// treat as file name
out = new File(singleClassOutput);
} else {
// treat as directory
out = new File(singleClassOutput, clsForProcess.getShortName() + fileExt);
}
}
File resultOut = FileUtils.prepareFile(out);
if (clsForProcess.getClassInfo().hasAlias()) {
LOG.info("Saving class '{}' (alias: '{}') to file '{}'",
clsForProcess.getClassInfo().getFullName(), clsForProcess.getFullName(), resultOut.getAbsolutePath());
} else {
LOG.info("Saving class '{}' to file '{}'", clsForProcess.getFullName(), resultOut.getAbsolutePath());
}
SaveCode.save(codeInfo.getCodeStr(), resultOut);
return true;
}
}
+1 -1
View File
@@ -5,7 +5,7 @@ plugins {
dependencies {
api(project(':jadx-plugins:jadx-plugins-api'))
implementation 'com.google.code.gson:gson:2.8.9'
implementation 'com.google.code.gson:gson:2.9.0'
implementation 'com.android.tools.build:aapt2-proto:4.2.1-7147631'
constraints {
// Force protobuf version to prevent Java-7 issue
+31 -4
View File
@@ -9,6 +9,7 @@ import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import jadx.api.args.DeobfuscationMapFileMode;
import jadx.api.data.ICodeData;
import jadx.api.impl.AnnotatedCodeWriter;
import jadx.api.impl.InMemoryCodeCache;
@@ -54,11 +55,12 @@ public class JadxArgs {
private Predicate<String> classFilter = null;
private boolean deobfuscationOn = false;
private boolean deobfuscationForceSave = false;
private boolean useSourceNameAsClassAlias = false;
private boolean parseKotlinMetadata = false;
private File deobfuscationMapFile = null;
private DeobfuscationMapFileMode deobfuscationMapFileMode = DeobfuscationMapFileMode.READ;
private int deobfuscationMinLength = 0;
private int deobfuscationMaxLength = Integer.MAX_VALUE;
@@ -93,6 +95,11 @@ public class JadxArgs {
private UseKotlinMethodsForVarNames useKotlinMethodsForVarNames = UseKotlinMethodsForVarNames.APPLY;
/**
* Don't save files (can be using for performance testing)
*/
private boolean skipFilesSave = false;
public JadxArgs() {
// use default options
}
@@ -259,12 +266,24 @@ public class JadxArgs {
this.deobfuscationOn = deobfuscationOn;
}
@Deprecated
public boolean isDeobfuscationForceSave() {
return deobfuscationForceSave;
return deobfuscationMapFileMode == DeobfuscationMapFileMode.OVERWRITE;
}
@Deprecated
public void setDeobfuscationForceSave(boolean deobfuscationForceSave) {
this.deobfuscationForceSave = deobfuscationForceSave;
if (deobfuscationForceSave) {
this.deobfuscationMapFileMode = DeobfuscationMapFileMode.OVERWRITE;
}
}
public DeobfuscationMapFileMode getDeobfuscationMapFileMode() {
return deobfuscationMapFileMode;
}
public void setDeobfuscationMapFileMode(DeobfuscationMapFileMode deobfuscationMapFileMode) {
this.deobfuscationMapFileMode = deobfuscationMapFileMode;
}
public boolean isUseSourceNameAsClassAlias() {
@@ -447,6 +466,14 @@ public class JadxArgs {
this.useKotlinMethodsForVarNames = useKotlinMethodsForVarNames;
}
public boolean isSkipFilesSave() {
return skipFilesSave;
}
public void setSkipFilesSave(boolean skipFilesSave) {
this.skipFilesSave = skipFilesSave;
}
@Override
public String toString() {
return "JadxArgs{" + "inputFiles=" + inputFiles
@@ -463,7 +490,7 @@ public class JadxArgs {
+ ", skipSources=" + skipSources
+ ", deobfuscationOn=" + deobfuscationOn
+ ", deobfuscationMapFile=" + deobfuscationMapFile
+ ", deobfuscationForceSave=" + deobfuscationForceSave
+ ", deobfuscationMapFileMode=" + deobfuscationMapFileMode
+ ", useSourceNameAsClassAlias=" + useSourceNameAsClassAlias
+ ", parseKotlinMetadata=" + parseKotlinMetadata
+ ", useKotlinMethodsForVarNames=" + useKotlinMethodsForVarNames
@@ -128,6 +128,7 @@ public final class JadxDecompiler implements Closeable {
loadedInputs.add(loadResult);
}
}
LOG.debug("Loaded using {} inputs plugin", loadedInputs.size());
}
private void reset() {
@@ -282,6 +283,9 @@ public final class JadxDecompiler implements Closeable {
}
private void appendResourcesSaveTasks(List<Runnable> tasks, File outDir) {
if (args.isSkipFilesSave()) {
return;
}
Set<String> inputFileNames = args.getInputFiles().stream().map(File::getAbsolutePath).collect(Collectors.toSet());
for (ResourceFile resourceFile : getResources()) {
if (resourceFile.getType() != ResourceType.ARSC
@@ -306,7 +310,13 @@ public final class JadxDecompiler implements Closeable {
}
processQueue.add(cls);
}
for (List<JavaClass> decompileBatch : decompileScheduler.buildBatches(processQueue)) {
List<List<JavaClass>> batches;
try {
batches = decompileScheduler.buildBatches(processQueue);
} catch (Exception e) {
throw new JadxRuntimeException("Decompilation batches build failed", e);
}
for (List<JavaClass> decompileBatch : batches) {
tasks.add(() -> {
for (JavaClass cls : decompileBatch) {
try {
+10 -10
View File
@@ -11,6 +11,8 @@ import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.AnonymousClassAttr;
import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
@@ -69,6 +71,10 @@ public final class JavaClass implements JavaNode {
cls.unloadCode();
}
public boolean isNoCode() {
return cls.contains(AFlag.DONT_GENERATE);
}
public synchronized String getSmali() {
return cls.getDisassembledCode();
}
@@ -237,7 +243,7 @@ public final class JavaClass implements JavaNode {
@Override
public JavaClass getTopParentClass() {
if (cls.contains(AFlag.ANONYMOUS_CLASS)) {
if (cls.contains(AType.ANONYMOUS_CLASS)) {
// moved to usage class
return getParentForAnonymousClass();
}
@@ -245,15 +251,9 @@ public final class JavaClass implements JavaNode {
}
private JavaClass getParentForAnonymousClass() {
List<JavaNode> useIn = getUseIn();
if (useIn.isEmpty()) {
return this;
}
JavaNode useNode = useIn.get(0);
if (useNode.equals(this)) {
return this;
}
return useNode.getTopParentClass();
AnonymousClassAttr attr = cls.get(AType.ANONYMOUS_CLASS);
ClassNode topParentClass = attr.getOuterCls().getTopParentClass();
return getRootDecompiler().convertClassNode(topParentClass);
}
public AccessInfo getAccessInfo() {
@@ -28,6 +28,10 @@ public final class JavaField implements JavaNode {
return parent.getFullName() + '.' + getName();
}
public String getRawName() {
return field.getName();
}
@Override
public JavaClass getDeclaringClass() {
return parent;
@@ -126,8 +126,9 @@ public final class ResourcesLoader {
if (name.endsWith(".9.png")) {
try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
Res9patchStreamDecoder decoder = new Res9patchStreamDecoder();
decoder.decode(inputStream, os);
return ResContainer.decodedData(rf.getDeobfName(), os.toByteArray());
if (decoder.decode(inputStream, os)) {
return ResContainer.decodedData(rf.getDeobfName(), os.toByteArray());
}
} catch (Exception e) {
LOG.error("Failed to decode 9-patch png image, path: {}", name, e);
}
@@ -0,0 +1,32 @@
package jadx.api.args;
public enum DeobfuscationMapFileMode {
/**
* Load if found, don't save (default)
*/
READ,
/**
* Load if found, save only if new (don't overwrite)
*/
READ_OR_SAVE,
/**
* Don't load, always save
*/
OVERWRITE,
/**
* Don't load and don't save
*/
IGNORE;
public boolean shouldRead() {
return this == READ || this == READ_OR_SAVE;
}
public boolean shouldWrite() {
return this == READ_OR_SAVE || this == OVERWRITE;
}
}
@@ -34,17 +34,17 @@ public final class ProcessClass {
if (cls.contains(AFlag.CLASS_DEEP_RELOAD)) {
cls.remove(AFlag.CLASS_DEEP_RELOAD);
cls.deepUnload();
cls.root().runPreDecompileStageForClass(cls);
cls.add(AFlag.CLASS_UNLOADED);
}
if (cls.contains(AFlag.CLASS_UNLOADED)) {
cls.remove(AFlag.CLASS_UNLOADED);
cls.root().runPreDecompileStageForClass(cls);
cls.remove(AFlag.CLASS_UNLOADED);
}
if (cls.getState() == GENERATED_AND_UNLOADED) {
// force loading code again
cls.setState(NOT_LOADED);
}
if (codegen) {
if (cls.getState() == GENERATED_AND_UNLOADED) {
// allow to run code generation again
cls.setState(NOT_LOADED);
}
cls.setLoadStage(LoadStage.CODEGEN_STAGE);
if (cls.contains(AFlag.RELOAD_AT_CODEGEN_STAGE)) {
cls.remove(AFlag.RELOAD_AT_CODEGEN_STAGE);
@@ -285,7 +285,7 @@ public class ClassGen {
private boolean isInnerClassesPresents() {
for (ClassNode innerCls : cls.getInnerClasses()) {
if (!innerCls.contains(AFlag.ANONYMOUS_CLASS)) {
if (!innerCls.contains(AType.ANONYMOUS_CLASS)) {
return true;
}
}
@@ -459,7 +459,7 @@ public class ClassGen {
}
if (f.getCls() != null) {
code.add(' ');
new ClassGen(f.getCls(), this).addClassBody(code);
new ClassGen(f.getCls(), this).addClassBody(code, true);
}
if (it.hasNext()) {
code.add(',');
@@ -526,12 +526,42 @@ public class ClassGen {
if (outerType != null) {
useClass(code, outerType);
code.add('.');
// import not needed, force use short name
useClassShortName(code, type.getObject());
addInnerType(code, type);
return;
}
useClass(code, ClassInfo.fromType(cls.root(), type));
addGenerics(code, type);
}
private void addInnerType(ICodeWriter code, ArgType baseType) {
ArgType innerType = baseType.getInnerType();
ArgType outerType = innerType.getOuterType();
if (outerType != null) {
useClassWithShortName(code, baseType, outerType);
code.add('.');
addInnerType(code, innerType);
return;
}
useClassWithShortName(code, baseType, innerType);
}
private void useClassWithShortName(ICodeWriter code, ArgType baseType, ArgType type) {
String fullNameObj;
if (type.getObject().contains(".")) {
fullNameObj = type.getObject();
} else {
fullNameObj = baseType.getObject();
}
ClassInfo classInfo = ClassInfo.fromName(cls.root(), fullNameObj);
ClassNode classNode = cls.root().resolveClass(classInfo);
if (classNode != null) {
code.attachAnnotation(classNode);
}
code.add(classInfo.getAliasShortName());
addGenerics(code, type);
}
private void addGenerics(ICodeWriter code, ArgType type) {
List<ArgType> generics = type.getGenericTypes();
if (generics != null) {
code.add('<');
@@ -556,15 +586,6 @@ public class ClassGen {
}
}
private void useClassShortName(ICodeWriter code, String object) {
ClassInfo classInfo = ClassInfo.fromName(cls.root(), object);
ClassNode classNode = cls.root().resolveClass(classInfo);
if (classNode != null) {
code.attachAnnotation(classNode);
}
code.add(classInfo.getAliasShortName());
}
public void useClass(ICodeWriter code, ClassInfo classInfo) {
ClassNode classNode = cls.root().resolveClass(classInfo);
if (classNode != null) {
@@ -172,7 +172,7 @@ public class InsnGen {
private void instanceField(ICodeWriter code, FieldInfo field, InsnArg arg) throws CodegenException {
ClassNode pCls = mth.getParentClass();
FieldNode fieldNode = pCls.root().deepResolveField(field);
FieldNode fieldNode = pCls.root().resolveField(field);
if (fieldNode != null) {
FieldReplaceAttr replace = fieldNode.get(AType.FIELD_REPLACE);
if (replace != null) {
@@ -210,7 +210,7 @@ public class InsnGen {
}
code.add('.');
}
FieldNode fieldNode = clsGen.getClassNode().root().deepResolveField(field);
FieldNode fieldNode = clsGen.getClassNode().root().resolveField(field);
if (fieldNode != null) {
code.attachAnnotation(fieldNode);
}
@@ -719,13 +719,13 @@ public class InsnGen {
private void inlineAnonymousConstructor(ICodeWriter code, ClassNode cls, ConstructorInsn insn) throws CodegenException {
if (this.mth.getParentClass() == cls) {
cls.remove(AFlag.ANONYMOUS_CLASS);
cls.remove(AType.ANONYMOUS_CLASS);
cls.remove(AFlag.DONT_GENERATE);
mth.getParentClass().getTopParentClass().add(AFlag.RESTART_CODEGEN);
throw new CodegenException("Anonymous inner class unlimited recursion detected."
+ " Convert class to inner: " + cls.getClassInfo().getFullName());
}
ArgType parent = cls.get(AType.ANONYMOUS_CLASS_BASE).getBaseType();
ArgType parent = cls.get(AType.ANONYMOUS_CLASS).getBaseType();
// hide empty anonymous constructors
for (MethodNode ctor : cls.getMethods()) {
if (ctor.contains(AFlag.ANONYMOUS_CONSTRUCTOR)
@@ -764,7 +764,7 @@ public class InsnGen {
return;
}
MethodInfo callMth = insn.getCallMth();
MethodNode callMthNode = mth.root().deepResolveMethod(callMth);
MethodNode callMthNode = mth.root().resolveMethod(callMth);
int k = 0;
switch (type) {
@@ -238,10 +238,11 @@ public class MethodGen {
classGen.useType(code, argType);
}
code.add(' ');
if (code.isMetadataSupported() && ssaVar != null) {
String varName = nameGen.assignArg(var);
if (code.isMetadataSupported() && ssaVar != null /* for fallback mode */) {
code.attachDefinition(VarDeclareRef.get(mth, var));
}
code.add(nameGen.assignArg(var));
code.add(varName);
i++;
if (it.hasNext()) {
@@ -0,0 +1,24 @@
package jadx.core.deobf;
public class ClsAliasPair {
private final String pkg;
private final String name;
public ClsAliasPair(String pkg, String name) {
this.pkg = pkg;
this.name = name;
}
public String getPkg() {
return pkg;
}
public String getName() {
return name;
}
@Override
public String toString() {
return pkg + '.' + name;
}
}
@@ -17,6 +17,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.JadxArgs;
import jadx.api.args.DeobfuscationMapFileMode;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.info.FieldInfo;
import jadx.core.dex.info.MethodInfo;
@@ -50,6 +51,9 @@ public class DeobfPresets {
@Nullable
private static Path getPathDeobfMapPath(RootNode root) {
JadxArgs jadxArgs = root.getArgs();
if (jadxArgs.getDeobfuscationMapFileMode() == DeobfuscationMapFileMode.IGNORE) {
return null;
}
File deobfMapFile = jadxArgs.getDeobfuscationMapFile();
if (deobfMapFile != null) {
return deobfMapFile.toPath();
@@ -70,9 +74,9 @@ public class DeobfPresets {
/**
* Loads deobfuscator presets
*/
public void load() {
public boolean load() {
if (!Files.exists(deobfMapFile)) {
return;
return false;
}
LOG.info("Loading obfuscation map from: {}", deobfMapFile.toAbsolutePath());
try {
@@ -106,8 +110,10 @@ public class DeobfPresets {
break;
}
}
return true;
} catch (Exception e) {
LOG.error("Failed to load deobfuscation map file '{}'", deobfMapFile.toAbsolutePath(), e);
return false;
}
}
@@ -142,9 +148,7 @@ public class DeobfPresets {
}
Files.write(deobfMapFile, list, MAP_FILE_CHARSET,
StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
if (LOG.isDebugEnabled()) {
LOG.debug("Deobfuscation map file saved as: {}", deobfMapFile);
}
LOG.info("Deobfuscation map file saved as: {}", deobfMapFile);
}
public String getForCls(ClassInfo cls) {
@@ -16,6 +16,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.JadxArgs;
import jadx.api.args.DeobfuscationMapFileMode;
import jadx.api.plugins.input.data.attributes.JadxAttrType;
import jadx.api.plugins.input.data.attributes.types.SourceFileAttr;
import jadx.core.dex.attributes.AFlag;
@@ -76,22 +77,25 @@ public class Deobfuscator {
}
public void execute() {
if (!args.isDeobfuscationForceSave()) {
deobfPresets.load();
for (Map.Entry<String, String> pkgEntry : deobfPresets.getPkgPresetMap().entrySet()) {
addPackagePreset(pkgEntry.getKey(), pkgEntry.getValue());
if (args.getDeobfuscationMapFileMode().shouldRead()) {
if (deobfPresets.load()) {
for (Map.Entry<String, String> pkgEntry : deobfPresets.getPkgPresetMap().entrySet()) {
addPackagePreset(pkgEntry.getKey(), pkgEntry.getValue());
}
deobfPresets.getPkgPresetMap().clear(); // not needed anymore
initIndexes();
}
deobfPresets.getPkgPresetMap().clear(); // not needed anymore
initIndexes();
}
process();
}
public void savePresets() {
DeobfuscationMapFileMode mode = args.getDeobfuscationMapFileMode();
if (!mode.shouldWrite()) {
return;
}
Path deobfMapFile = deobfPresets.getDeobfMapFile();
if (Files.exists(deobfMapFile) && !args.isDeobfuscationForceSave()) {
LOG.info("Deobfuscation map file '{}' exists. Use command line option '--deobf-rewrite-cfg' to rewrite it",
deobfMapFile.toAbsolutePath());
if (mode == DeobfuscationMapFileMode.READ_OR_SAVE && Files.exists(deobfMapFile)) {
return;
}
try {
@@ -112,16 +116,25 @@ public class Deobfuscator {
deobfPresets.getPkgPresetMap().put(p.getName(), p.getAlias());
}
}
for (DeobfClsInfo deobfClsInfo : clsMap.values()) {
if (deobfClsInfo.getAlias() != null) {
deobfPresets.getClsPresetMap().put(deobfClsInfo.getCls().getClassInfo().makeRawFullName(), deobfClsInfo.getAlias());
for (ClassNode cls : root.getClasses()) {
ClassInfo classInfo = cls.getClassInfo();
if (classInfo.hasAlias()) {
deobfPresets.getClsPresetMap().put(classInfo.makeRawFullName(), classInfo.getAliasShortName());
}
for (FieldNode fld : cls.getFields()) {
FieldInfo fieldInfo = fld.getFieldInfo();
if (fieldInfo.hasAlias()) {
deobfPresets.getFldPresetMap().put(fieldInfo.getRawFullId(), fld.getAlias());
}
}
for (MethodNode mth : cls.getMethods()) {
MethodInfo methodInfo = mth.getMethodInfo();
if (methodInfo.hasAlias()) {
deobfPresets.getFldPresetMap().put(methodInfo.getRawFullId(), methodInfo.getAlias());
}
}
}
for (FieldInfo fld : fldMap.keySet()) {
deobfPresets.getFldPresetMap().put(fld.getRawFullId(), fld.getAlias());
}
for (MethodInfo mth : mthMap.keySet()) {
deobfPresets.getMthPresetMap().put(mth.getRawFullId(), mth.getAlias());
}
}
@@ -377,10 +390,10 @@ public class Deobfuscator {
String alias = null;
String pkgName = null;
if (this.parseKotlinMetadata) {
ClassInfo kotlinCls = KotlinMetadataUtils.getClassName(cls);
ClsAliasPair kotlinCls = KotlinMetadataUtils.getClassAlias(cls);
if (kotlinCls != null) {
alias = prepareNameFull(kotlinCls.getShortName(), "C");
pkgName = kotlinCls.getPackage();
alias = kotlinCls.getName();
pkgName = kotlinCls.getPkg();
}
}
if (alias == null && this.useSourceNameAsAlias) {
@@ -572,6 +585,7 @@ public class Deobfuscator {
if (!pkg.hasAlias()) {
String pkgName = pkg.getName();
if ((args.isDeobfuscationOn() && shouldRename(pkgName))
&& (pkg.getParentPackage() != rootPackage || !TldHelper.contains(pkgName)) // check if first level is a valid tld
|| (args.isRenameValid() && !NameMapper.isValidIdentifier(pkgName))
|| (args.isRenamePrintable() && !NameMapper.isAllCharsPrintable(pkgName))) {
String pkgAlias = String.format("p%03d%s", pkgIndex++, prepareNamePart(pkg.getName()));
@@ -592,20 +606,6 @@ public class Deobfuscator {
return NameMapper.removeInvalidCharsMiddle(name);
}
private String prepareNameFull(String name, String prefix) {
if (name.length() > maxLength) {
return makeHashName(name, prefix);
}
String result = NameMapper.removeInvalidChars(name, prefix);
if (result.isEmpty()) {
return makeHashName(name, prefix);
}
if (NameMapper.isReserved(result)) {
return prefix + result;
}
return result;
}
private static String makeHashName(String name, String invalidPrefix) {
return invalidPrefix + 'x' + Integer.toHexString(name.hashCode());
}
@@ -0,0 +1,38 @@
package jadx.core.deobf;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.HashSet;
import java.util.Set;
import jadx.core.utils.exceptions.JadxRuntimeException;
/**
* Provides a list of all top level domains with 3 characters and less,
* so we can exclude them from deobfuscation.
*/
public class TldHelper {
private static final Set<String> TLD_SET = loadTldFile();
private static Set<String> loadTldFile() {
Set<String> tldNames = new HashSet<>();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(Deobfuscator.class.getResourceAsStream("tld_3.txt")))) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (!line.startsWith("#") && !line.isEmpty()) {
tldNames.add(line);
}
}
return tldNames;
} catch (Exception e) {
throw new JadxRuntimeException("Failed to load top level domain list tld_3.txt", e);
}
}
public static boolean contains(String name) {
return TLD_SET.contains(name);
}
}
@@ -35,7 +35,6 @@ public enum AFlag {
SKIP_ARG, // skip argument in invoke call
NO_SKIP_ARGS,
ANONYMOUS_CONSTRUCTOR,
ANONYMOUS_CLASS,
THIS,
SUPER,
@@ -77,6 +76,7 @@ public enum AFlag {
INCONSISTENT_CODE, // warning about incorrect decompilation
REQUEST_IF_REGION_OPTIMIZE, // run if region visitor again
REQUEST_CODE_SHRINK,
RERUN_SSA_TRANSFORM,
METHOD_CANDIDATE_FOR_INLINE,
@@ -2,7 +2,7 @@ package jadx.core.dex.attributes;
import jadx.api.plugins.input.data.attributes.IJadxAttrType;
import jadx.api.plugins.input.data.attributes.IJadxAttribute;
import jadx.core.dex.attributes.nodes.AnonymousClassBaseAttr;
import jadx.core.dex.attributes.nodes.AnonymousClassAttr;
import jadx.core.dex.attributes.nodes.ClassTypeVarsAttr;
import jadx.core.dex.attributes.nodes.DeclareVariablesAttr;
import jadx.core.dex.attributes.nodes.EdgeInsnAttr;
@@ -25,6 +25,7 @@ import jadx.core.dex.attributes.nodes.PhiListAttr;
import jadx.core.dex.attributes.nodes.RegDebugInfoAttr;
import jadx.core.dex.attributes.nodes.RenameReasonAttr;
import jadx.core.dex.attributes.nodes.SkipMethodArgsAttr;
import jadx.core.dex.attributes.nodes.SpecialEdgeAttr;
import jadx.core.dex.attributes.nodes.TmpEdgeAttr;
import jadx.core.dex.nodes.IMethodDetails;
import jadx.core.dex.trycatch.CatchAttr;
@@ -53,7 +54,7 @@ public final class AType<T extends IJadxAttribute> implements IJadxAttrType<T> {
public static final AType<EnumClassAttr> ENUM_CLASS = new AType<>();
public static final AType<EnumMapAttr> ENUM_MAP = new AType<>();
public static final AType<ClassTypeVarsAttr> CLASS_TYPE_VARS = new AType<>();
public static final AType<AnonymousClassBaseAttr> ANONYMOUS_CLASS_BASE = new AType<>();
public static final AType<AnonymousClassAttr> ANONYMOUS_CLASS = new AType<>();
// field
public static final AType<FieldInitInsnAttr> FIELD_INIT_INSN = new AType<>();
@@ -76,6 +77,7 @@ public final class AType<T extends IJadxAttribute> implements IJadxAttrType<T> {
public static final AType<ForceReturnAttr> FORCE_RETURN = new AType<>();
public static final AType<AttrList<LoopInfo>> LOOP = new AType<>();
public static final AType<AttrList<EdgeInsnAttr>> EDGE_INSN = new AType<>();
public static final AType<AttrList<SpecialEdgeAttr>> SPECIAL_EDGE = new AType<>();
public static final AType<TmpEdgeAttr> TMP_EDGE = new AType<>();
public static final AType<TryCatchBlockAttr> TRY_BLOCK = new AType<>();
@@ -0,0 +1,35 @@
package jadx.core.dex.attributes.nodes;
import jadx.api.plugins.input.data.attributes.PinnedAttribute;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.nodes.ClassNode;
public class AnonymousClassAttr extends PinnedAttribute {
private final ClassNode outerCls;
private final ArgType baseType;
public AnonymousClassAttr(ClassNode outerCls, ArgType baseType) {
this.outerCls = outerCls;
this.baseType = baseType;
}
public ClassNode getOuterCls() {
return outerCls;
}
public ArgType getBaseType() {
return baseType;
}
@Override
public AType<AnonymousClassAttr> getAttrType() {
return AType.ANONYMOUS_CLASS;
}
@Override
public String toString() {
return "AnonymousClass{" + outerCls + ", base: " + baseType + '}';
}
}
@@ -1,28 +0,0 @@
package jadx.core.dex.attributes.nodes;
import jadx.api.plugins.input.data.attributes.PinnedAttribute;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.instructions.args.ArgType;
public class AnonymousClassBaseAttr extends PinnedAttribute {
private final ArgType baseType;
public AnonymousClassBaseAttr(ArgType baseType) {
this.baseType = baseType;
}
public ArgType getBaseType() {
return baseType;
}
@Override
public AType<AnonymousClassBaseAttr> getAttrType() {
return AType.ANONYMOUS_CLASS_BASE;
}
@Override
public String toString() {
return "AnonymousClassBaseAttr{" + baseType + '}';
}
}
@@ -1,7 +1,6 @@
package jadx.core.dex.attributes.nodes;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -20,10 +19,10 @@ public class LoopInfo {
private int id;
private LoopInfo parentLoop;
public LoopInfo(BlockNode start, BlockNode end) {
public LoopInfo(BlockNode start, BlockNode end, Set<BlockNode> loopBlocks) {
this.start = start;
this.end = end;
this.loopBlocks = Collections.unmodifiableSet(BlockUtils.getAllPathsBlocks(start, end));
this.loopBlocks = loopBlocks;
}
public BlockNode getStart() {
@@ -6,6 +6,16 @@ import jadx.core.dex.attributes.AttrNode;
public class RenameReasonAttr implements IJadxAttribute {
public static RenameReasonAttr forNode(AttrNode node) {
RenameReasonAttr renameReasonAttr = node.get(AType.RENAME_REASON);
if (renameReasonAttr != null) {
return renameReasonAttr;
}
RenameReasonAttr newAttr = new RenameReasonAttr();
node.addAttr(newAttr);
return newAttr;
}
private String description;
public RenameReasonAttr() {
@@ -0,0 +1,46 @@
package jadx.core.dex.attributes.nodes;
import jadx.api.plugins.input.data.attributes.IJadxAttribute;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.AttrList;
import jadx.core.dex.nodes.BlockNode;
public class SpecialEdgeAttr implements IJadxAttribute {
public enum SpecialEdgeType {
BACK_EDGE,
CROSS_EDGE
}
private final SpecialEdgeType type;
private final BlockNode start;
private final BlockNode end;
public SpecialEdgeAttr(SpecialEdgeType type, BlockNode start, BlockNode end) {
this.type = type;
this.start = start;
this.end = end;
}
public SpecialEdgeType getType() {
return type;
}
public BlockNode getStart() {
return start;
}
public BlockNode getEnd() {
return end;
}
@Override
public AType<AttrList<SpecialEdgeAttr>> getAttrType() {
return AType.SPECIAL_EDGE;
}
@Override
public String toString() {
return type + ": " + start + " -> " + end;
}
}
@@ -246,13 +246,13 @@ public final class ClassInfo implements Comparable<ClassInfo> {
}
public void notInner(RootNode root) {
this.parentClass = null;
splitAndApplyNames(root, type, false);
this.parentClass = null;
}
public void convertToInner(ClassNode parent) {
this.parentClass = parent.getClassInfo();
splitAndApplyNames(parent.root(), type, true);
this.parentClass = parent.getClassInfo();
}
public void updateNames(RootNode root) {
@@ -176,6 +176,9 @@ public class ConstStorage {
@Nullable
public FieldNode getConstFieldByLiteralArg(ClassNode cls, LiteralArg arg) {
if (!replaceEnabled) {
return null;
}
PrimitiveType type = arg.getType().getPrimitiveType();
if (type == null) {
return null;
@@ -196,17 +196,6 @@ public class SSAVar {
return usedInPhi != null && !usedInPhi.isEmpty();
}
public int getVariableUseCount() {
int count = useList.size();
if (usedInPhi == null) {
return count;
}
for (PhiInsn phiInsn : usedInPhi) {
count += phiInsn.getResult().getSVar().getUseCount();
}
return count;
}
public void setName(String name) {
if (name != null) {
if (codeVar == null) {
@@ -33,6 +33,7 @@ import jadx.api.plugins.input.data.impl.ListConsumer;
import jadx.core.Consts;
import jadx.core.ProcessClass;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.NotificationAttrNode;
import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.info.AccessInfo.AFType;
@@ -42,6 +43,7 @@ import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.instructions.args.LiteralArg;
import jadx.core.dex.nodes.utils.TypeUtils;
import jadx.core.utils.ListUtils;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException;
@@ -358,6 +360,17 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
return codeInfo;
}
@Nullable
public ICodeInfo getCodeFromCache() {
ICodeCache codeCache = root().getCodeCache();
String clsRawName = getRawName();
ICodeInfo codeInfo = codeCache.get(clsRawName);
if (codeInfo == ICodeInfo.EMPTY) {
return null;
}
return codeInfo;
}
@Override
public void load() {
for (MethodNode mth : getMethods()) {
@@ -608,7 +621,7 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
}
public boolean isAnonymous() {
return contains(AFlag.ANONYMOUS_CLASS);
return contains(AType.ANONYMOUS_CLASS);
}
public boolean isInner() {
@@ -739,6 +752,10 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
this.dependencies = dependencies;
}
public void removeDependency(ClassNode dep) {
this.dependencies = ListUtils.safeRemoveAndTrim(this.dependencies, dep);
}
public List<ClassNode> getCodegenDeps() {
return codegenDeps;
}
@@ -747,6 +764,14 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
this.codegenDeps = codegenDeps;
}
public void addCodegenDep(ClassNode dep) {
this.codegenDeps = ListUtils.safeAdd(this.codegenDeps, dep);
}
public int getTotalDepsCount() {
return dependencies.size() + codegenDeps.size();
}
public List<ClassNode> getUseIn() {
return useIn;
}
@@ -9,6 +9,7 @@ import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.info.AccessInfo.AFType;
import jadx.core.dex.info.FieldInfo;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.utils.ListUtils;
public class FieldNode extends NotificationAttrNode implements ICodeNode {
@@ -80,6 +81,10 @@ public class FieldNode extends NotificationAttrNode implements ICodeNode {
this.useIn = useIn;
}
public synchronized void addUseIn(MethodNode mth) {
useIn = ListUtils.safeAdd(useIn, mth);
}
@Override
public String typeName() {
return "field";
@@ -300,20 +300,6 @@ public class InsnNode extends LineAttrNode {
}
}
/**
* Visit all args recursively (including inner instructions),
* but excluding wrapped args
*/
public void visitArgs(Consumer<InsnArg> visitor) {
for (InsnArg arg : getArguments()) {
if (arg.isInsnWrap()) {
((InsnWrapArg) arg).getWrapInsn().visitArgs(visitor);
} else {
visitor.accept(arg);
}
}
}
/**
* Visit this instruction and all inner (wrapped) instructions
* To terminate visiting return non-null value
@@ -336,6 +322,40 @@ public class InsnNode extends LineAttrNode {
return null;
}
/**
* Visit all args recursively (including inner instructions), but excluding wrapped args
*/
public void visitArgs(Consumer<InsnArg> visitor) {
for (InsnArg arg : getArguments()) {
if (arg.isInsnWrap()) {
((InsnWrapArg) arg).getWrapInsn().visitArgs(visitor);
} else {
visitor.accept(arg);
}
}
}
/**
* Visit all args recursively (including inner instructions), but excluding wrapped args.
* To terminate visiting return non-null value
*/
@Nullable
public <R> R visitArgs(Function<InsnArg, R> visitor) {
for (InsnArg arg : getArguments()) {
R result;
if (arg.isInsnWrap()) {
InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn();
result = wrapInsn.visitArgs(visitor);
} else {
result = visitor.apply(arg);
}
if (result != null) {
return result;
}
}
return null;
}
/**
* 'Soft' equals, don't compare arguments, only instruction specific parameters.
*/
@@ -99,9 +99,6 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
@Override
public void unload() {
loaded = false;
if (noCode) {
return;
}
// don't unload retType, argTypes, typeParameters
thisArg = null;
argsList = null;
@@ -378,15 +378,6 @@ public class RootNode {
@Nullable
public MethodNode resolveMethod(@NotNull MethodInfo mth) {
ClassNode cls = resolveClass(mth.getDeclClass());
if (cls != null) {
return cls.searchMethod(mth);
}
return null;
}
@Nullable
public MethodNode deepResolveMethod(@NotNull MethodInfo mth) {
ClassNode cls = resolveClass(mth.getDeclClass());
if (cls == null) {
return null;
@@ -430,19 +421,14 @@ public class RootNode {
@Nullable
public FieldNode resolveField(FieldInfo field) {
ClassNode cls = resolveClass(field.getDeclClass());
if (cls != null) {
return cls.searchField(field);
}
return null;
}
@Nullable
public FieldNode deepResolveField(@NotNull FieldInfo field) {
ClassNode cls = resolveClass(field.getDeclClass());
if (cls == null) {
return null;
}
FieldNode fieldNode = cls.searchField(field);
if (fieldNode != null) {
return fieldNode;
}
return deepResolveField(cls, field);
}
@@ -2,7 +2,6 @@ package jadx.core.dex.nodes.parser;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import org.jetbrains.annotations.Nullable;
@@ -197,6 +196,8 @@ public class SignatureParser {
String obj = slice();
if (!innerType) {
obj += ';';
} else {
obj = obj.replace('/', '.');
}
List<ArgType> typeVars = consumeGenericArgs();
consume('>');
@@ -227,7 +228,7 @@ public class SignatureParser {
}
private List<ArgType> consumeGenericArgs() {
List<ArgType> list = new LinkedList<>();
List<ArgType> list = new ArrayList<>();
ArgType type;
do {
if (lookAhead('*')) {
@@ -38,7 +38,7 @@ public class MethodUtils {
@Nullable
public IMethodDetails getMethodDetails(MethodInfo callMth) {
MethodNode mthNode = root.deepResolveMethod(callMth);
MethodNode mthNode = root.resolveMethod(callMth);
if (mthNode != null) {
return mthNode;
}
@@ -63,7 +63,7 @@ public class TypeUtils {
public ArgType expandTypeVariables(ClassNode cls, ArgType type) {
if (type.containsTypeVariable()) {
expandTypeVar(cls, type, cls.getGenericTypeParameters());
expandTypeVar(cls, type, getKnownTypeVarsAtClass(cls));
}
return type;
}
@@ -115,11 +115,18 @@ public class TypeUtils {
return varsAttr.getTypeVars();
}
private static Set<ArgType> collectKnownTypeVarsAtMethod(MethodNode mth) {
ClassNode declCls = mth.getParentClass();
Set<ArgType> typeVars = new HashSet<>(declCls.getGenericTypeParameters());
declCls.visitParentClasses(parent -> typeVars.addAll(parent.getGenericTypeParameters()));
private static Collection<ArgType> getKnownTypeVarsAtClass(ClassNode cls) {
if (cls.isInner()) {
Set<ArgType> typeVars = new HashSet<>(cls.getGenericTypeParameters());
cls.visitParentClasses(parent -> typeVars.addAll(parent.getGenericTypeParameters()));
return typeVars;
}
return cls.getGenericTypeParameters();
}
private static Set<ArgType> collectKnownTypeVarsAtMethod(MethodNode mth) {
Set<ArgType> typeVars = new HashSet<>();
typeVars.addAll(getKnownTypeVarsAtClass(mth.getParentClass()));
typeVars.addAll(mth.getTypeParameters());
return typeVars.isEmpty() ? Collections.emptySet() : typeVars;
}
@@ -1,5 +1,6 @@
package jadx.core.dex.trycatch;
import java.util.Comparator;
import java.util.List;
import jadx.api.plugins.input.data.attributes.IJadxAttribute;
@@ -8,9 +9,14 @@ import jadx.core.utils.Utils;
public class CatchAttr implements IJadxAttribute {
public static CatchAttr build(List<ExceptionHandler> handlers) {
handlers.sort(Comparator.comparingInt(ExceptionHandler::getHandlerOffset));
return new CatchAttr(handlers);
}
private final List<ExceptionHandler> handlers;
public CatchAttr(List<ExceptionHandler> handlers) {
private CatchAttr(List<ExceptionHandler> handlers) {
this.handlers = handlers;
}
@@ -7,6 +7,7 @@ import java.util.List;
import java.util.Map;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.FieldReplaceAttr;
import jadx.core.dex.attributes.nodes.SkipMethodArgsAttr;
import jadx.core.dex.info.FieldInfo;
@@ -36,7 +37,7 @@ public class AnonymousClassVisitor extends AbstractVisitor {
@Override
public boolean visit(ClassNode cls) throws JadxException {
if (cls.contains(AFlag.ANONYMOUS_CLASS)) {
if (cls.contains(AType.ANONYMOUS_CLASS)) {
for (MethodNode mth : cls.getMethods()) {
if (mth.contains(AFlag.ANONYMOUS_CONSTRUCTOR)) {
processAnonymousConstructor(mth);
@@ -1,6 +1,7 @@
package jadx.core.dex.visitors;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.jetbrains.annotations.Nullable;
@@ -15,12 +16,16 @@ import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.instructions.InsnType;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.trycatch.CatchAttr;
import jadx.core.dex.trycatch.ExcHandlerAttr;
import jadx.core.dex.trycatch.ExceptionHandler;
import jadx.core.dex.visitors.typeinference.TypeCompare;
import jadx.core.dex.visitors.typeinference.TypeCompareEnum;
import jadx.core.utils.exceptions.JadxException;
import jadx.core.utils.exceptions.JadxRuntimeException;
import static jadx.core.dex.visitors.ProcessInstructionsVisitor.getNextInsnOffset;
@@ -51,11 +56,11 @@ public class AttachTryCatchVisitor extends AbstractVisitor {
tries.forEach(tryData -> LOG.debug(" - {}", tryData));
}
for (ITry tryData : tries) {
List<ExceptionHandler> handlers = attachHandlers(mth, tryData.getCatch(), insnByOffset);
List<ExceptionHandler> handlers = convertToHandlers(mth, tryData.getCatch(), insnByOffset);
if (handlers.isEmpty()) {
continue;
}
markTryBounds(insnByOffset, tryData, new CatchAttr(handlers));
markTryBounds(insnByOffset, tryData, CatchAttr.build(handlers));
}
}
@@ -96,13 +101,13 @@ public class AttachTryCatchVisitor extends AbstractVisitor {
if (existAttr != null) {
// merge handlers
List<ExceptionHandler> handlers = Utils.concat(existAttr.getHandlers(), catchAttr.getHandlers());
insn.addAttr(new CatchAttr(handlers));
insn.addAttr(CatchAttr.build(handlers));
} else {
insn.addAttr(catchAttr);
}
}
private static List<ExceptionHandler> attachHandlers(MethodNode mth, ICatch catchBlock, InsnNode[] insnByOffset) {
private static List<ExceptionHandler> convertToHandlers(MethodNode mth, ICatch catchBlock, InsnNode[] insnByOffset) {
int[] handlerOffsetArr = catchBlock.getHandlers();
String[] handlerTypes = catchBlock.getTypes();
@@ -117,6 +122,7 @@ public class AttachTryCatchVisitor extends AbstractVisitor {
if (allHandlerOffset >= 0) {
Utils.addToList(list, createHandler(mth, insnByOffset, allHandlerOffset, null));
}
checkAndFilterHandlers(mth, list);
return list;
}
@@ -143,6 +149,45 @@ public class AttachTryCatchVisitor extends AbstractVisitor {
return handler;
}
private static void checkAndFilterHandlers(MethodNode mth, List<ExceptionHandler> list) {
if (list.size() <= 1) {
return;
}
// Remove shadowed handlers (with same or narrow type compared to previous)
TypeCompare typeCompare = mth.root().getTypeCompare();
Iterator<ExceptionHandler> it = list.iterator();
ArgType maxType = null;
while (it.hasNext()) {
ExceptionHandler handler = it.next();
ArgType maxCatch = maxCatchFromHandler(handler, typeCompare);
if (maxType == null) {
maxType = maxCatch;
} else {
TypeCompareEnum result = typeCompare.compareObjects(maxType, maxCatch);
if (result.isWiderOrEqual()) {
if (Consts.DEBUG_EXC_HANDLERS) {
LOG.debug("Removed shadowed catch handler: {}, from list: {}", handler, list);
}
it.remove();
}
}
}
}
private static ArgType maxCatchFromHandler(ExceptionHandler handler, TypeCompare typeCompare) {
List<ClassInfo> catchTypes = handler.getCatchTypes();
if (catchTypes.isEmpty()) {
return ArgType.THROWABLE;
}
if (catchTypes.size() == 1) {
return catchTypes.get(0).getType();
}
return catchTypes.stream()
.map(ClassInfo::getType)
.max(typeCompare.getComparator())
.orElseThrow(() -> new JadxRuntimeException("Failed to get max type from catch list: " + catchTypes));
}
private static InsnNode insertNOP(InsnNode[] insnByOffset, int offset) {
InsnNode nop = new InsnNode(InsnType.NOP, 0);
nop.setOffset(offset);
@@ -244,7 +244,7 @@ public class ClassModifier extends AbstractVisitor {
return false;
}
MethodInfo callMth = invokeInsn.getCallMth();
MethodNode wrappedMth = mth.root().deepResolveMethod(callMth);
MethodNode wrappedMth = mth.root().resolveMethod(callMth);
if (wrappedMth == null) {
return false;
}
@@ -65,6 +65,7 @@ public class ConstInlineVisitor extends AbstractVisitor {
SSAVar sVar = insn.getResult().getSVar();
InsnArg constArg;
Runnable onSuccess = null;
InsnType insnType = insn.getType();
if (insnType == InsnType.CONST || insnType == InsnType.MOVE) {
@@ -90,6 +91,7 @@ public class ConstInlineVisitor extends AbstractVisitor {
InsnNode constGet = new IndexInsnNode(InsnType.SGET, f.getFieldInfo(), 0);
constArg = InsnArg.wrapArg(constGet);
constArg.setType(ArgType.STRING);
onSuccess = () -> f.addUseIn(mth);
}
} else if (insnType == InsnType.CONST_CLASS) {
if (sVar.isUsedInPhi()) {
@@ -104,6 +106,9 @@ public class ConstInlineVisitor extends AbstractVisitor {
// all check passed, run replace
if (replaceConst(mth, insn, constArg)) {
toRemove.add(insn);
if (onSuccess != null) {
onSuccess.run();
}
}
}
@@ -235,7 +240,10 @@ public class ConstInlineVisitor extends AbstractVisitor {
fieldNode = mth.getParentClass().getConstField((int) literal, false);
}
if (fieldNode != null) {
litArg.wrapInstruction(mth, new IndexInsnNode(InsnType.SGET, fieldNode.getFieldInfo(), 0));
IndexInsnNode sgetInsn = new IndexInsnNode(InsnType.SGET, fieldNode.getFieldInfo(), 0);
if (litArg.wrapInstruction(mth, sgetInsn) != null) {
fieldNode.addUseIn(mth);
}
} else {
if (needExplicitCast(useInsn, litArg)) {
litArg.add(AFlag.EXPLICIT_PRIMITIVE_TYPE);
@@ -15,6 +15,7 @@ import jadx.api.plugins.input.data.AccessFlags;
import jadx.core.codegen.TypeGen;
import jadx.core.deobf.NameMapper;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.EnumClassAttr;
import jadx.core.dex.attributes.nodes.EnumClassAttr.EnumField;
import jadx.core.dex.attributes.nodes.SkipMethodArgsAttr;
@@ -71,7 +72,14 @@ public class EnumVisitor extends AbstractVisitor {
@Override
public boolean visit(ClassNode cls) throws JadxException {
if (!convertToEnum(cls)) {
boolean converted;
try {
converted = convertToEnum(cls);
} catch (Exception e) {
cls.addWarnComment("Enum visitor error", e);
converted = false;
}
if (!converted) {
AccessInfo accessFlags = cls.getAccessFlags();
if (accessFlags.isEnum()) {
cls.setAccessFlags(accessFlags.remove(AccessFlags.ENUM));
@@ -179,8 +187,7 @@ public class EnumVisitor extends AbstractVisitor {
if (!enumClsInfo.equals(cls.getClassInfo())) {
ClassNode enumCls = cls.root().resolveClass(enumClsInfo);
if (enumCls != null) {
processEnumCls(enumField, enumCls);
cls.addInlinedClass(enumCls);
processEnumCls(cls, enumField, enumCls);
}
}
List<RegisterArg> regs = new ArrayList<>();
@@ -381,7 +388,11 @@ public class EnumVisitor extends AbstractVisitor {
if (constrCls == null) {
return null;
}
if (!clsInfo.equals(cls.getClassInfo()) && !constrCls.getAccessFlags().isEnum()) {
if (constrCls.equals(cls)) {
// allow same class
} else if (constrCls.contains(AType.ANONYMOUS_CLASS)) {
// allow external class already marked as anonymous
} else {
return null;
}
MethodNode ctrMth = cls.root().resolveMethod(co.getCallMth());
@@ -466,7 +477,7 @@ public class EnumVisitor extends AbstractVisitor {
return InsnUtils.searchInsn(mth, InsnType.SGET, insnTest) != null;
}
private static void processEnumCls(EnumField field, ClassNode innerCls) {
private static void processEnumCls(ClassNode cls, EnumField field, ClassNode innerCls) {
// remove constructor, because it is anonymous class
for (MethodNode innerMth : innerCls.getMethods()) {
if (innerMth.getAccessFlags().isConstructor()) {
@@ -474,7 +485,11 @@ public class EnumVisitor extends AbstractVisitor {
}
}
field.setCls(innerCls);
innerCls.add(AFlag.DONT_GENERATE);
if (!innerCls.getParentClass().equals(cls)) {
// not inner
cls.addInlinedClass(innerCls);
innerCls.add(AFlag.DONT_GENERATE);
}
}
private ConstructorInsn getConstructorInsn(InsnNode insn) {
@@ -26,9 +26,6 @@ public class InitCodeVariables extends AbstractVisitor {
@Override
public void visit(MethodNode mth) throws JadxException {
if (mth.isNoCode()) {
return;
}
initCodeVars(mth);
}
@@ -42,16 +39,24 @@ public class InitCodeVariables extends AbstractVisitor {
private static void initCodeVars(MethodNode mth) {
RegisterArg thisArg = mth.getThisArg();
if (thisArg != null) {
initCodeVar(thisArg.getSVar());
initCodeVar(mth, thisArg);
}
for (RegisterArg mthArg : mth.getArgRegs()) {
initCodeVar(mthArg.getSVar());
initCodeVar(mth, mthArg);
}
for (SSAVar ssaVar : mth.getSVars()) {
initCodeVar(ssaVar);
}
}
public static void initCodeVar(MethodNode mth, RegisterArg regArg) {
SSAVar ssaVar = regArg.getSVar();
if (ssaVar == null) {
ssaVar = mth.makeNewSVar(regArg);
}
initCodeVar(ssaVar);
}
public static void initCodeVar(SSAVar ssaVar) {
if (ssaVar.isCodeVarSet()) {
return;
@@ -106,7 +106,7 @@ public class MarkMethodsForInline extends AbstractVisitor {
InsnType insnType = insn.getType();
if (insnType == InsnType.INVOKE) {
InvokeNode invoke = (InvokeNode) insn;
MethodNode callMthNode = mth.root().deepResolveMethod(invoke.getCallMth());
MethodNode callMthNode = mth.root().resolveMethod(invoke.getCallMth());
if (callMthNode != null) {
FixAccessModifiers.changeVisibility(callMthNode, newVisFlag);
}
@@ -114,7 +114,7 @@ public class ModVisitor extends AbstractVisitor {
break;
case SWITCH:
replaceConstKeys(parentClass, (SwitchInsn) insn);
replaceConstKeys(mth, parentClass, (SwitchInsn) insn);
break;
case NEW_ARRAY:
@@ -228,13 +228,14 @@ public class ModVisitor extends AbstractVisitor {
return result == TypeCompareEnum.NARROW; // true if use class is subclass of field class
}
private static void replaceConstKeys(ClassNode parentClass, SwitchInsn insn) {
private static void replaceConstKeys(MethodNode mth, ClassNode parentClass, SwitchInsn insn) {
int[] keys = insn.getKeys();
int len = keys.length;
for (int k = 0; k < len; k++) {
FieldNode f = parentClass.getConstField(keys[k]);
if (f != null) {
insn.modifyKey(k, f);
f.addUseIn(mth);
}
}
}
@@ -291,6 +292,13 @@ public class ModVisitor extends AbstractVisitor {
@SuppressWarnings("unchecked")
private EncodedValue replaceConstValue(ClassNode parentCls, EncodedValue encodedValue) {
if (encodedValue.getType() == EncodedType.ENCODED_ANNOTATION) {
IAnnotation annotation = (IAnnotation) encodedValue.getValue();
for (Map.Entry<String, EncodedValue> entry : annotation.getValues().entrySet()) {
entry.setValue(replaceConstValue(parentCls, entry.getValue()));
}
return encodedValue;
}
if (encodedValue.getType() == EncodedType.ENCODED_ARRAY) {
List<EncodedValue> listVal = (List<EncodedValue>) encodedValue.getValue();
if (!listVal.isEmpty()) {
@@ -320,6 +328,7 @@ public class ModVisitor extends AbstractVisitor {
InsnNode inode = new IndexInsnNode(InsnType.SGET, f.getFieldInfo(), 0);
inode.setResult(insn.getResult());
replaceInsn(mth, block, i, inode);
f.addUseIn(mth);
}
}
@@ -332,7 +341,9 @@ public class ModVisitor extends AbstractVisitor {
FieldNode f = parentClass.getConstFieldByLiteralArg((LiteralArg) litArg);
if (f != null) {
InsnNode fGet = new IndexInsnNode(InsnType.SGET, f.getFieldInfo(), 0);
arithNode.replaceArg(litArg, InsnArg.wrapArg(fGet));
if (arithNode.replaceArg(litArg, InsnArg.wrapArg(fGet))) {
f.addUseIn(mth);
}
}
}
}
@@ -446,7 +457,7 @@ public class ModVisitor extends AbstractVisitor {
}
SkipMethodArgsAttr attr = callMth.get(AType.SKIP_MTH_ARGS);
if (attr != null) {
int argsCount = Math.min(callMth.getArgRegs().size(), co.getArgsCount());
int argsCount = Math.min(callMth.getMethodInfo().getArgsCount(), co.getArgsCount());
for (int i = 0; i < argsCount; i++) {
if (attr.isSkip(i)) {
anonymousCallArgMod(co.getArg(i));
@@ -516,6 +527,7 @@ public class ModVisitor extends AbstractVisitor {
if (f != null) {
InsnNode fGet = new IndexInsnNode(InsnType.SGET, f.getFieldInfo(), 0);
filledArr.addArg(InsnArg.wrapArg(fGet));
f.addUseIn(mth);
} else {
filledArr.addArg(arg);
}
@@ -5,15 +5,24 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
import org.jetbrains.annotations.Nullable;
import jadx.api.plugins.input.data.IFieldRef;
import jadx.api.plugins.input.data.annotations.AnnotationVisibility;
import jadx.api.plugins.input.data.annotations.EncodedValue;
import jadx.api.plugins.input.data.annotations.IAnnotation;
import jadx.api.plugins.input.data.attributes.JadxAttrType;
import jadx.api.plugins.input.data.attributes.types.AnnotationsAttr;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.AttrNode;
import jadx.core.dex.attributes.nodes.DeclareVariablesAttr;
import jadx.core.dex.attributes.nodes.LineAttrNode;
import jadx.core.dex.info.FieldInfo;
import jadx.core.dex.instructions.ArithNode;
import jadx.core.dex.instructions.ArithOp;
import jadx.core.dex.instructions.InsnType;
@@ -25,6 +34,7 @@ import jadx.core.dex.instructions.mods.ConstructorInsn;
import jadx.core.dex.instructions.mods.TernaryInsn;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.InsnContainer;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
@@ -54,6 +64,7 @@ public class PrepareForCodeGen extends AbstractVisitor {
if (cls.root().getArgs().isDebugInfo()) {
setClassSourceLine(cls);
}
collectFieldsUsageInAnnotations(cls);
return true;
}
@@ -73,6 +84,7 @@ public class PrepareForCodeGen extends AbstractVisitor {
checkConstUsage(block);
}
moveConstructorInConstructor(mth);
collectFieldsUsageInAnnotations(mth, mth);
}
private static void removeInstructions(BlockNode block) {
@@ -310,4 +322,61 @@ public class PrepareForCodeGen extends AbstractVisitor {
cls.setSourceLine(minLine - 1);
}
}
private void collectFieldsUsageInAnnotations(ClassNode cls) {
MethodNode useMth = cls.getDefaultConstructor();
if (useMth == null && !cls.getMethods().isEmpty()) {
useMth = cls.getMethods().get(0);
}
if (useMth == null) {
return;
}
collectFieldsUsageInAnnotations(useMth, cls);
MethodNode finalUseMth = useMth;
cls.getFields().forEach(f -> collectFieldsUsageInAnnotations(finalUseMth, f));
}
private void collectFieldsUsageInAnnotations(MethodNode mth, AttrNode attrNode) {
AnnotationsAttr annotationsList = attrNode.get(JadxAttrType.ANNOTATION_LIST);
if (annotationsList == null) {
return;
}
for (IAnnotation annotation : annotationsList.getAll()) {
if (annotation.getVisibility() == AnnotationVisibility.SYSTEM) {
continue;
}
for (Map.Entry<String, EncodedValue> entry : annotation.getValues().entrySet()) {
checkEncodedValue(mth, entry.getValue());
}
}
}
@SuppressWarnings("unchecked")
private void checkEncodedValue(MethodNode mth, EncodedValue encodedValue) {
switch (encodedValue.getType()) {
case ENCODED_FIELD:
Object fieldData = encodedValue.getValue();
FieldInfo fieldInfo;
if (fieldData instanceof IFieldRef) {
fieldInfo = FieldInfo.fromRef(mth.root(), (IFieldRef) fieldData);
} else {
fieldInfo = (FieldInfo) fieldData;
}
FieldNode fieldNode = mth.root().resolveField(fieldInfo);
if (fieldNode != null) {
fieldNode.addUseIn(mth);
}
break;
case ENCODED_ANNOTATION:
IAnnotation annotation = (IAnnotation) encodedValue.getValue();
annotation.getValues().forEach((k, v) -> checkEncodedValue(mth, v));
break;
case ENCODED_ARRAY:
List<EncodedValue> valueList = (List<EncodedValue>) encodedValue.getValue();
valueList.forEach(v -> checkEncodedValue(mth, v));
break;
}
}
}
@@ -1,11 +1,18 @@
package jadx.core.dex.visitors;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jetbrains.annotations.Nullable;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.nodes.AnonymousClassBaseAttr;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.AnonymousClassAttr;
import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.nodes.ClassNode;
@@ -25,27 +32,36 @@ import jadx.core.utils.exceptions.JadxException;
)
public class ProcessAnonymous extends AbstractVisitor {
private boolean inlineAnonymous;
private boolean inlineAnonymousClasses;
@Override
public void init(RootNode root) {
inlineAnonymous = root.getArgs().isInlineAnonymousClasses();
inlineAnonymousClasses = root.getArgs().isInlineAnonymousClasses();
if (!inlineAnonymousClasses) {
return;
}
for (ClassNode cls : root.getClasses()) {
markAnonymousClass(cls);
}
mergeAnonymousDeps(root);
}
@Override
public boolean visit(ClassNode cls) throws JadxException {
if (!inlineAnonymous) {
return false;
if (inlineAnonymousClasses && cls.contains(AFlag.CLASS_UNLOADED)) {
// enter only on class reload
visitClassAndInners(cls);
}
return false;
}
private void visitClassAndInners(ClassNode cls) {
markAnonymousClass(cls);
return true;
cls.getInnerClasses().forEach(this::visitClassAndInners);
}
private static void markAnonymousClass(ClassNode cls) {
boolean synthetic = cls.getAccessFlags().isSynthetic()
|| cls.getClassInfo().getShortName().contains("$")
|| Character.isDigit(cls.getClassInfo().getShortName().charAt(0));
if (!synthetic) {
if (!canBeAnonymous(cls)) {
return;
}
MethodNode anonymousConstructor = checkUsage(cls);
@@ -56,27 +72,131 @@ public class ProcessAnonymous extends AbstractVisitor {
if (baseType == null) {
return;
}
cls.add(AFlag.ANONYMOUS_CLASS);
cls.addAttr(new AnonymousClassBaseAttr(baseType));
ClassNode outerCls = anonymousConstructor.getUseIn().get(0).getParentClass();
cls.addAttr(new AnonymousClassAttr(outerCls, baseType));
cls.add(AFlag.DONT_GENERATE);
anonymousConstructor.add(AFlag.ANONYMOUS_CONSTRUCTOR);
// force anonymous class to be processed before outer class,
// actual usage of outer class will be removed at anonymous class process,
// see ModVisitor.processAnonymousConstructor method
ClassNode outerCls = anonymousConstructor.getUseIn().get(0).getParentClass();
ClassNode topOuterCls = outerCls.getTopParentClass();
ListUtils.safeRemove(cls.getDependencies(), topOuterCls);
cls.removeDependency(topOuterCls);
ListUtils.safeRemove(outerCls.getUseIn(), cls);
// move dependency to codegen stage
if (cls.isTopClass()) {
topOuterCls.setDependencies(ListUtils.safeRemoveAndTrim(topOuterCls.getDependencies(), cls));
topOuterCls.setCodegenDeps(ListUtils.safeAdd(topOuterCls.getCodegenDeps(), cls));
topOuterCls.removeDependency(cls);
topOuterCls.addCodegenDep(cls);
}
}
private static void undoAnonymousMark(ClassNode cls) {
AnonymousClassAttr attr = cls.get(AType.ANONYMOUS_CLASS);
ClassNode outerCls = attr.getOuterCls();
cls.setDependencies(ListUtils.safeAdd(cls.getDependencies(), outerCls.getTopParentClass()));
outerCls.setUseIn(ListUtils.safeAdd(outerCls.getUseIn(), cls));
cls.remove(AType.ANONYMOUS_CLASS);
cls.remove(AFlag.DONT_GENERATE);
for (MethodNode mth : cls.getMethods()) {
if (mth.isConstructor()) {
mth.remove(AFlag.ANONYMOUS_CONSTRUCTOR);
}
}
cls.addDebugComment("Anonymous mark cleared");
}
private void mergeAnonymousDeps(RootNode root) {
// Collect edges to build bidirectional tree:
// inline edge: anonymous -> outer (one-to-one)
// use edges: outer -> *anonymous (one-to-many)
Map<ClassNode, ClassNode> inlineMap = new HashMap<>();
Map<ClassNode, List<ClassNode>> useMap = new HashMap<>();
for (ClassNode anonymousCls : root.getClasses()) {
AnonymousClassAttr attr = anonymousCls.get(AType.ANONYMOUS_CLASS);
if (attr != null) {
ClassNode outerCls = attr.getOuterCls();
List<ClassNode> list = useMap.get(outerCls);
if (list == null || list.isEmpty()) {
list = new ArrayList<>(2);
useMap.put(outerCls, list);
}
list.add(anonymousCls);
useMap.putIfAbsent(anonymousCls, Collections.emptyList()); // put leaf explicitly
inlineMap.put(anonymousCls, outerCls);
}
}
if (inlineMap.isEmpty()) {
return;
}
// starting from leaf process deps in nodes up to root
Set<ClassNode> added = new HashSet<>();
useMap.forEach((key, list) -> {
if (list.isEmpty()) {
added.clear();
updateDeps(key, inlineMap, added);
}
});
for (ClassNode cls : root.getClasses()) {
List<ClassNode> deps = cls.getCodegenDeps();
if (deps.size() > 1) {
// distinct sorted dep, reusing collections to reduce memory allocations :)
added.clear();
added.addAll(deps);
deps.clear();
deps.addAll(added);
Collections.sort(deps);
}
}
}
private void updateDeps(ClassNode leafCls, Map<ClassNode, ClassNode> inlineMap, Set<ClassNode> added) {
ClassNode topNode;
ClassNode current = leafCls;
while (true) {
if (!added.add(current)) {
current.addWarnComment("Loop in anonymous inline: " + current + ", path: " + added);
added.forEach(ProcessAnonymous::undoAnonymousMark);
return;
}
ClassNode next = inlineMap.get(current);
if (next == null) {
topNode = current.getTopParentClass();
break;
}
current = next;
}
if (added.size() <= 2) {
// first level deps already processed
return;
}
List<ClassNode> deps = topNode.getCodegenDeps();
if (deps.isEmpty()) {
deps = new ArrayList<>(added.size());
topNode.setCodegenDeps(deps);
}
for (ClassNode add : added) {
deps.add(add.getTopParentClass());
}
}
private static boolean canBeAnonymous(ClassNode cls) {
if (cls.getAccessFlags().isSynthetic()) {
return true;
}
String shortName = cls.getClassInfo().getShortName();
if (shortName.contains("$") || Character.isDigit(shortName.charAt(0))) {
return true;
}
if (cls.getUseIn().size() == 1 && cls.getUseInMth().size() == 1) {
MethodNode useMth = cls.getUseInMth().get(0);
// allow use in enum class init
return useMth.getMethodInfo().isClassInit() && useMth.getParentClass().isEnum();
}
return false;
}
/**
* Checks:
* - class have only one constructor which used only once (allow common code for field init)
@@ -164,7 +164,9 @@ public class ReSugarCode extends AbstractVisitor {
FieldNode f = mth.getParentClass().getConstFieldByLiteralArg((LiteralArg) valueArg);
if (f != null) {
InsnNode fGet = new IndexInsnNode(InsnType.SGET, f.getFieldInfo(), 0);
return InsnArg.wrapArg(fGet);
InsnArg arg = InsnArg.wrapArg(fGet);
f.addUseIn(mth);
return arg;
}
}
return valueArg.duplicate();
@@ -11,6 +11,7 @@ import jadx.api.JadxArgs;
import jadx.api.plugins.utils.ZipSecurity;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
@@ -34,7 +35,10 @@ public class SaveCode {
if (codeStr.isEmpty()) {
return;
}
String fileName = cls.getClassInfo().getAliasFullPath() + getFileExtension(cls);
if (cls.root().getArgs().isSkipFilesSave()) {
return;
}
String fileName = cls.getClassInfo().getAliasFullPath() + getFileExtension(cls.root());
save(codeStr, dir, fileName);
}
@@ -58,8 +62,8 @@ public class SaveCode {
}
}
private static String getFileExtension(ClassNode cls) {
JadxArgs.OutputFormatEnum outputFormat = cls.root().getArgs().getOutputFormat();
public static String getFileExtension(RootNode root) {
JadxArgs.OutputFormatEnum outputFormat = root.getArgs().getOutputFormat();
switch (outputFormat) {
case JAVA:
return ".java";
@@ -80,11 +80,15 @@ public class SignatureProcessor extends AbstractVisitor {
}
ClassNode cls = field.getParentClass();
try {
ArgType gType = sp.consumeType();
if (gType == null) {
ArgType signatureType = sp.consumeType();
if (signatureType == null) {
return;
}
ArgType type = root.getTypeUtils().expandTypeVariables(cls, gType);
if (!validateInnerType(signatureType)) {
field.addWarnComment("Incorrect inner types in field signature: " + sp.getSignature());
return;
}
ArgType type = root.getTypeUtils().expandTypeVariables(cls, signatureType);
if (!validateParsedType(type, field.getType())) {
cls.addWarnComment("Incorrect field signature: " + sp.getSignature());
return;
@@ -105,6 +109,11 @@ public class SignatureProcessor extends AbstractVisitor {
List<ArgType> parsedArgTypes = sp.consumeMethodArgs(mth.getMethodInfo().getArgsCount());
ArgType parsedRetType = sp.consumeType();
if (!validateInnerType(parsedRetType) || !validateInnerType(parsedArgTypes)) {
mth.addWarnComment("Incorrect inner types in method signature: " + sp.getSignature());
return;
}
mth.updateTypeParameters(typeParameters); // apply before expand args
TypeUtils typeUtils = root.getTypeUtils();
ArgType retType = typeUtils.expandTypeVariables(mth, parsedRetType);
@@ -172,4 +181,54 @@ public class SignatureProcessor extends AbstractVisitor {
TypeCompareEnum result = root.getTypeCompare().compareTypes(parsedType, currentType);
return result != TypeCompareEnum.CONFLICT;
}
private boolean validateInnerType(List<ArgType> types) {
for (ArgType type : types) {
if (!validateInnerType(type)) {
return false;
}
}
return true;
}
private boolean validateInnerType(ArgType type) {
ArgType innerType = type.getInnerType();
if (innerType == null) {
return true;
}
// check in outer type has inner type as inner class
ArgType outerType = type.getOuterType();
ClassNode outerCls = root.resolveClass(outerType);
if (outerCls == null) {
// can't check class not found
return true;
}
String innerObj;
if (innerType.getOuterType() != null) {
innerObj = innerType.getOuterType().getObject();
// "next" inner type will be processed at end of method
} else {
innerObj = innerType.getObject();
}
if (!innerObj.contains(".")) {
// short reference
for (ClassNode innerClass : outerCls.getInnerClasses()) {
if (innerClass.getShortName().equals(innerObj)) {
return true;
}
}
return false;
}
// full name
ClassNode innerCls = root.resolveClass(innerObj);
if (innerCls == null) {
return false;
}
if (!innerCls.getParentClass().equals(outerCls)) {
// not inner => fixing
outerCls.addInnerClass(innerCls);
innerCls.getClassInfo().convertToInner(outerCls);
}
return validateInnerType(innerType);
}
}
@@ -126,7 +126,7 @@ public class BlockExceptionHandler {
commonCatchAttr = catchAttr;
continue;
}
if (commonCatchAttr != catchAttr) {
if (!commonCatchAttr.equals(catchAttr)) {
return null;
}
}
@@ -390,15 +390,23 @@ public class BlockExceptionHandler {
private static BlockNode searchTopBlock(MethodNode mth, List<BlockNode> blocks) {
BlockNode top = BlockUtils.getTopBlock(blocks);
if (top != null) {
return top;
return adjustTopBlock(top);
}
BlockNode topDom = BlockUtils.getCommonDominator(mth, blocks);
if (topDom != null) {
return topDom;
return adjustTopBlock(topDom);
}
throw new JadxRuntimeException("Failed to find top block for try-catch from: " + blocks);
}
private static BlockNode adjustTopBlock(BlockNode topBlock) {
if (topBlock.getSuccessors().size() == 1 && !topBlock.contains(AType.EXC_CATCH)) {
// top block can be lifted by other exception handlers included in blocks list, trying to undo that
return topBlock.getSuccessors().get(0);
}
return topBlock;
}
@Nullable
private static BlockNode searchBottomBlock(MethodNode mth, List<BlockNode> blocks) {
// search common post-dominator block inside input set
@@ -53,6 +53,10 @@ public class BlockProcessor extends AbstractVisitor {
clearBlocksState(mth);
computeDominators(mth);
}
if (FixMultiEntryLoops.process(mth)) {
clearBlocksState(mth);
computeDominators(mth);
}
updateCleanSuccessors(mth);
int i = 0;
@@ -347,7 +351,8 @@ public class BlockProcessor extends AbstractVisitor {
successor.add(AFlag.LOOP_START);
block.add(AFlag.LOOP_END);
LoopInfo loop = new LoopInfo(successor, block);
Set<BlockNode> loopBlocks = BlockUtils.getAllPathsBlocks(successor, block);
LoopInfo loop = new LoopInfo(successor, block, loopBlocks);
successor.addAttr(AType.LOOP, loop);
block.addAttr(AType.LOOP, loop);
}
@@ -192,6 +192,14 @@ public class BlockSplitter extends AbstractVisitor {
return newBlock;
}
static void copyBlockData(BlockNode from, BlockNode to) {
List<InsnNode> toInsns = to.getInstructions();
for (InsnNode insn : from.getInstructions()) {
toInsns.add(insn.copyWithoutSsa());
}
to.copyAttributesFrom(from);
}
static void replaceTarget(BlockNode source, BlockNode oldTarget, BlockNode newTarget) {
InsnNode lastInsn = BlockUtils.getLastInsn(source);
if (lastInsn instanceof TargetInsnNode) {
@@ -0,0 +1,104 @@
package jadx.core.dex.visitors.blocks;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.SpecialEdgeAttr;
import jadx.core.dex.attributes.nodes.SpecialEdgeAttr.SpecialEdgeType;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.ListUtils;
public class FixMultiEntryLoops {
public static boolean process(MethodNode mth) {
try {
detectSpecialEdges(mth);
} catch (Exception e) {
mth.addWarnComment("Failed to detect multi-entry loops", e);
return false;
}
List<SpecialEdgeAttr> specialEdges = mth.getAll(AType.SPECIAL_EDGE);
List<SpecialEdgeAttr> multiEntryLoops = specialEdges.stream()
.filter(e -> e.getType() == SpecialEdgeType.BACK_EDGE)
.filter(e -> !isSingleEntryLoop(e))
.collect(Collectors.toList());
if (multiEntryLoops.isEmpty()) {
return false;
}
try {
List<SpecialEdgeAttr> crossEdges = ListUtils.filter(specialEdges, e -> e.getType() == SpecialEdgeType.CROSS_EDGE);
boolean changed = false;
for (SpecialEdgeAttr backEdge : multiEntryLoops) {
changed |= fixLoop(mth, backEdge, crossEdges);
}
return changed;
} catch (Exception e) {
mth.addWarnComment("Failed to fix multi-entry loops", e);
return false;
}
}
private static boolean fixLoop(MethodNode mth, SpecialEdgeAttr backEdge, List<SpecialEdgeAttr> crossEdges) {
BlockNode header = backEdge.getEnd();
BlockNode headerIDom = header.getIDom();
SpecialEdgeAttr subEntry = ListUtils.filterOnlyOne(crossEdges, e -> e.getStart() == headerIDom);
if (subEntry == null || !isSupportedPattern(header, subEntry)) {
// TODO: for now only sub entry in header successor is supported
mth.addWarnComment("Unsupported multi-entry loop pattern (" + backEdge + "). Please submit an issue!!!");
return false;
}
BlockNode loopEnd = backEdge.getStart();
BlockNode subEntryBlock = subEntry.getEnd();
BlockNode copyHeader = BlockSplitter.insertBlockBetween(mth, loopEnd, header);
BlockSplitter.copyBlockData(header, copyHeader);
BlockSplitter.replaceConnection(copyHeader, header, subEntryBlock);
mth.addDebugComment("Duplicate block to fix multi-entry loop: " + backEdge);
return true;
}
private static boolean isSupportedPattern(BlockNode header, SpecialEdgeAttr subEntry) {
return ListUtils.isSingleElement(header.getSuccessors(), subEntry.getEnd());
}
private static boolean isSingleEntryLoop(SpecialEdgeAttr e) {
BlockNode header = e.getEnd();
BlockNode loopEnd = e.getStart();
return header == loopEnd
|| loopEnd.getDoms().get(header.getId()); // header dominates loop end
}
private enum BlockColor {
WHITE, GRAY, BLACK
}
private static void detectSpecialEdges(MethodNode mth) {
List<BlockNode> blocks = mth.getBasicBlocks();
BlockColor[] colors = new BlockColor[blocks.size()];
Arrays.fill(colors, BlockColor.WHITE);
colorDFS(mth, blocks, colors, mth.getEnterBlock().getId());
}
// TODO: transform to non-recursive form
private static void colorDFS(MethodNode mth, List<BlockNode> blocks, BlockColor[] colors, int cur) {
colors[cur] = BlockColor.GRAY;
BlockNode block = blocks.get(cur);
for (BlockNode v : block.getSuccessors()) {
int vId = v.getId();
switch (colors[vId]) {
case WHITE:
colorDFS(mth, blocks, colors, vId);
break;
case GRAY:
mth.addAttr(AType.SPECIAL_EDGE, new SpecialEdgeAttr(SpecialEdgeType.BACK_EDGE, block, v));
break;
case BLACK:
mth.addAttr(AType.SPECIAL_EDGE, new SpecialEdgeAttr(SpecialEdgeType.CROSS_EDGE, block, v));
break;
}
}
colors[cur] = BlockColor.BLACK;
}
}
@@ -3,7 +3,7 @@ package jadx.core.dex.visitors.debuginfo;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
import org.slf4j.Logger;
@@ -73,31 +73,22 @@ public class DebugInfoApplyVisitor extends AbstractVisitor {
if (Consts.DEBUG_TYPE_INFERENCE) {
LOG.info("Apply debug info for method: {}", mth);
}
mth.getSVars().forEach(ssaVar -> collectVarDebugInfo(mth, ssaVar));
mth.getSVars().forEach(ssaVar -> searchAndApplyVarDebugInfo(mth, ssaVar));
fixLinesForReturn(mth);
fixNamesForPhiInsns(mth);
}
private static void collectVarDebugInfo(MethodNode mth, SSAVar ssaVar) {
Set<RegDebugInfoAttr> debugInfoSet = new HashSet<>(ssaVar.getUseCount() + 1);
addRegDbdInfo(debugInfoSet, ssaVar.getAssign());
ssaVar.getUseList().forEach(registerArg -> addRegDbdInfo(debugInfoSet, registerArg));
int dbgCount = debugInfoSet.size();
if (dbgCount == 0) {
searchDebugInfoByOffset(mth, ssaVar);
private static void searchAndApplyVarDebugInfo(MethodNode mth, SSAVar ssaVar) {
if (applyDebugInfo(mth, ssaVar, ssaVar.getAssign())) {
return;
}
if (dbgCount == 1) {
RegDebugInfoAttr debugInfo = debugInfoSet.iterator().next();
applyDebugInfo(mth, ssaVar, debugInfo.getRegType(), debugInfo.getName());
} else {
mth.addInfoComment("Multiple debug info for " + ssaVar + ": " + debugInfoSet);
for (RegDebugInfoAttr debugInfo : debugInfoSet) {
applyDebugInfo(mth, ssaVar, debugInfo.getRegType(), debugInfo.getName());
for (RegisterArg useArg : ssaVar.getUseList()) {
if (applyDebugInfo(mth, ssaVar, useArg)) {
return;
}
}
searchDebugInfoByOffset(mth, ssaVar);
}
private static void searchDebugInfoByOffset(MethodNode mth, SSAVar ssaVar) {
@@ -105,14 +96,12 @@ public class DebugInfoApplyVisitor extends AbstractVisitor {
if (debugInfoAttr == null) {
return;
}
Optional<Integer> max = ssaVar.getUseList().stream()
.map(DebugInfoApplyVisitor::getInsnOffsetByArg)
.max(Integer::compareTo);
OptionalInt max = ssaVar.getUseList().stream().mapToInt(DebugInfoApplyVisitor::getInsnOffsetByArg).max();
if (!max.isPresent()) {
return;
}
int startOffset = getInsnOffsetByArg(ssaVar.getAssign());
int endOffset = max.get();
int endOffset = max.getAsInt();
int regNum = ssaVar.getRegNum();
for (ILocalVar localVar : debugInfoAttr.getLocalVars()) {
if (localVar.getRegNum() == regNum) {
@@ -144,24 +133,26 @@ public class DebugInfoApplyVisitor extends AbstractVisitor {
return -1;
}
public static void applyDebugInfo(MethodNode mth, SSAVar ssaVar, ArgType type, String varName) {
TypeUpdateResult result = mth.root().getTypeUpdate().applyWithWiderAllow(mth, ssaVar, type);
public static boolean applyDebugInfo(MethodNode mth, SSAVar ssaVar, RegisterArg arg) {
RegDebugInfoAttr debugInfoAttr = arg.get(AType.REG_DEBUG_INFO);
if (debugInfoAttr == null) {
return false;
}
return applyDebugInfo(mth, ssaVar, debugInfoAttr.getRegType(), debugInfoAttr.getName());
}
public static boolean applyDebugInfo(MethodNode mth, SSAVar ssaVar, ArgType type, String varName) {
TypeUpdateResult result = mth.root().getTypeUpdate().applyWithWiderIgnoreUnknown(mth, ssaVar, type);
if (result == TypeUpdateResult.REJECT) {
if (Consts.DEBUG_TYPE_INFERENCE) {
LOG.debug("Reject debug info of type: {} and name: '{}' for {}, mth: {}", type, varName, ssaVar, mth);
}
} else {
if (NameMapper.isValidAndPrintable(varName)) {
ssaVar.setName(varName);
}
return false;
}
}
private static void addRegDbdInfo(Set<RegDebugInfoAttr> debugInfo, RegisterArg reg) {
RegDebugInfoAttr debugInfoAttr = reg.get(AType.REG_DEBUG_INFO);
if (debugInfoAttr != null) {
debugInfo.add(debugInfoAttr);
if (NameMapper.isValidAndPrintable(varName)) {
ssaVar.setName(varName);
}
return true;
}
/**
@@ -89,25 +89,33 @@ public class DebugInfoAttachVisitor extends AbstractVisitor {
}
for (int i = start; i <= end; i++) {
InsnNode insn = insnArr[i];
if (insn != null) {
attachDebugInfo(insn.getResult(), debugInfoAttr, regNum);
for (InsnArg arg : insn.getArguments()) {
attachDebugInfo(arg, debugInfoAttr, regNum);
}
if (insn == null) {
continue;
}
int count = 0;
for (InsnArg arg : insn.getArguments()) {
count += attachDebugInfo(arg, debugInfoAttr, regNum);
}
if (count != 0) {
// don't apply same info for result if applied to args
continue;
}
attachDebugInfo(insn.getResult(), debugInfoAttr, regNum);
}
}
mth.addAttr(new LocalVarsDebugInfoAttr(localVars));
}
private void attachDebugInfo(InsnArg arg, RegDebugInfoAttr debugInfoAttr, int regNum) {
private int attachDebugInfo(InsnArg arg, RegDebugInfoAttr debugInfoAttr, int regNum) {
if (arg instanceof RegisterArg) {
RegisterArg reg = (RegisterArg) arg;
if (regNum == reg.getRegNum()) {
reg.addAttr(debugInfoAttr);
return 1;
}
}
return 0;
}
public static ArgType getVarType(MethodNode mth, ILocalVar var) {
@@ -154,7 +154,11 @@ public class MarkFinallyVisitor extends AbstractVisitor {
// remove 'finally' from 'try' blocks, check all up paths on each exit (connected with finally exit)
List<BlockNode> tryBlocks = allHandler.getTryBlock().getBlocks();
BlockNode bottomFinallyBlock = BlockUtils.followEmptyPath(BlockUtils.getBottomBlock(allHandler.getBlocks()));
BlockNode bottomBlock = BlockUtils.getBottomBlock(allHandler.getBlocks());
if (bottomBlock == null) {
return false;
}
BlockNode bottomFinallyBlock = BlockUtils.followEmptyPath(bottomBlock);
BlockNode bottom = BlockUtils.getNextBlock(bottomFinallyBlock);
if (bottom == null) {
return false;
@@ -382,6 +382,9 @@ public class IfMakerHelper {
}
if (useCount > 1) {
forceInlineInsns.add(insn);
} else {
// allow only forced assign inline
pass = false;
}
}
}
@@ -16,8 +16,6 @@ import jadx.core.utils.RegionUtils;
import static jadx.core.utils.RegionUtils.insnsCount;
public class IfRegionVisitor extends AbstractVisitor {
private static final TernaryMod TERNARY_VISITOR = new TernaryMod();
private static final ProcessIfRegionVisitor PROCESS_IF_REGION_VISITOR = new ProcessIfRegionVisitor();
private static final RemoveRedundantElseVisitor REMOVE_REDUNDANT_ELSE_VISITOR = new RemoveRedundantElseVisitor();
@@ -30,7 +28,7 @@ public class IfRegionVisitor extends AbstractVisitor {
}
public static void process(MethodNode mth) {
DepthRegionTraversal.traverseIterative(mth, TERNARY_VISITOR);
TernaryMod.process(mth);
DepthRegionTraversal.traverse(mth, PROCESS_IF_REGION_VISITOR);
DepthRegionTraversal.traverseIterative(mth, REMOVE_REDUNDANT_ELSE_VISITOR);
}
@@ -25,10 +25,38 @@ import jadx.core.utils.InsnRemover;
/**
* Convert 'if' to ternary operation
*/
public class TernaryMod implements IRegionIterativeVisitor {
public class TernaryMod extends AbstractRegionVisitor implements IRegionIterativeVisitor {
private static final TernaryMod INSTANCE = new TernaryMod();
public static void process(MethodNode mth) {
// first: convert all found ternary nodes in one iteration
DepthRegionTraversal.traverse(mth, INSTANCE);
if (mth.contains(AFlag.REQUEST_CODE_SHRINK)) {
CodeShrinkVisitor.shrinkMethod(mth);
}
// second: iterative runs with shrink after each change
DepthRegionTraversal.traverseIterative(mth, INSTANCE);
}
@Override
public boolean enterRegion(MethodNode mth, IRegion region) {
if (processRegion(mth, region)) {
mth.add(AFlag.REQUEST_CODE_SHRINK);
}
return true;
}
@Override
public boolean visitRegion(MethodNode mth, IRegion region) {
if (processRegion(mth, region)) {
CodeShrinkVisitor.shrinkMethod(mth);
return true;
}
return false;
}
private static boolean processRegion(MethodNode mth, IRegion region) {
if (region instanceof IfRegion) {
return makeTernaryInsn(mth, (IfRegion) region);
}
@@ -115,9 +143,6 @@ public class TernaryMod implements IRegionIterativeVisitor {
header.getInstructions().add(ternInsn);
clearConditionBlocks(conditionBlocks, header);
// shrink method again
CodeShrinkVisitor.shrinkMethod(mth);
return true;
}
@@ -151,8 +176,6 @@ public class TernaryMod implements IRegionIterativeVisitor {
header.add(AFlag.RETURN);
clearConditionBlocks(conditionBlocks, header);
CodeShrinkVisitor.shrinkMethod(mth);
return true;
}
return false;
@@ -291,4 +314,7 @@ public class TernaryMod implements IRegionIterativeVisitor {
// shrink method again
CodeShrinkVisitor.shrinkMethod(mth);
}
private TernaryMod() {
}
}
@@ -5,6 +5,7 @@ import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import org.jetbrains.annotations.Nullable;
@@ -25,6 +26,7 @@ import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.visitors.AbstractVisitor;
public class RenameVisitor extends AbstractVisitor {
private static final Pattern ANONYMOUS_CLASS_PATTERN = Pattern.compile("^\\d+$");
@Override
public void init(RootNode root) {
@@ -130,11 +132,12 @@ public class RenameVisitor extends AbstractVisitor {
private static String fixClsShortName(JadxArgs args, String clsName) {
boolean renameValid = args.isRenameValid();
if (renameValid) {
char firstChar = clsName.charAt(0);
if (Character.isDigit(firstChar)) {
if (ANONYMOUS_CLASS_PATTERN.matcher(clsName).matches()) {
return Consts.ANONYMOUS_CLASS_PREFIX + NameMapper.removeInvalidCharsMiddle(clsName);
}
if (firstChar == '$') {
char firstChar = clsName.charAt(0);
if (firstChar == '$' || Character.isDigit(firstChar)) {
return 'C' + NameMapper.removeInvalidCharsMiddle(clsName);
}
}
@@ -4,12 +4,14 @@ import java.util.ArrayList;
import java.util.BitSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Objects;
import java.util.Set;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.instructions.InsnType;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.InsnWrapArg;
import jadx.core.dex.instructions.args.Named;
import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.instructions.args.SSAVar;
import jadx.core.dex.nodes.BlockNode;
@@ -40,6 +42,7 @@ public class CodeShrinkVisitor extends AbstractVisitor {
if (mth.isNoCode()) {
return;
}
mth.remove(AFlag.REQUEST_CODE_SHRINK);
for (BlockNode block : mth.getBasicBlocks()) {
shrinkBlock(mth, block);
simplifyMoveInsns(mth, block);
@@ -76,7 +79,9 @@ public class CodeShrinkVisitor extends AbstractVisitor {
private static void checkInline(MethodNode mth, BlockNode block, InsnList insnList,
List<WrapInfo> wrapList, ArgsInfo argsInfo, RegisterArg arg) {
if (arg.contains(AFlag.DONT_INLINE)) {
if (arg.contains(AFlag.DONT_INLINE)
|| arg.getParentInsn() == null
|| arg.getParentInsn().contains(AFlag.DONT_GENERATE)) {
return;
}
SSAVar sVar = arg.getSVar();
@@ -89,21 +94,34 @@ public class CodeShrinkVisitor extends AbstractVisitor {
|| assignInsn.contains(AFlag.WRAPPED)) {
return;
}
// allow inline only one use arg
boolean assignInline = assignInsn.contains(AFlag.FORCE_ASSIGN_INLINE);
if (!assignInline && sVar.getVariableUseCount() != 1) {
if (!assignInline && sVar.isUsedInPhi()) {
return;
}
List<RegisterArg> useList = sVar.getUseList();
if (!useList.isEmpty()) {
RegisterArg useArg = useList.get(0);
// allow inline only one use arg
int useCount = 0;
for (RegisterArg useArg : sVar.getUseList()) {
InsnNode parentInsn = useArg.getParentInsn();
if (parentInsn != null && parentInsn.contains(AFlag.DONT_GENERATE)) {
return;
continue;
}
if (!assignInline && useArg.contains(AFlag.DONT_INLINE_CONST)) {
return;
}
useCount++;
}
if (!assignInline && useCount != 1) {
return;
}
if (!assignInline && sVar.getName() != null) {
if (searchArgWithName(assignInsn, sVar.getName())) {
// allow inline if name is reused in result
} else if (varWithSameNameExists(mth, sVar)) {
// allow inline if var name is duplicated
} else {
// reject inline of named variable
return;
}
}
int assignPos = insnList.getIndex(assignInsn);
@@ -127,6 +145,31 @@ public class CodeShrinkVisitor extends AbstractVisitor {
}
}
private static boolean varWithSameNameExists(MethodNode mth, SSAVar inlineVar) {
for (SSAVar ssaVar : mth.getSVars()) {
if (ssaVar == inlineVar || ssaVar.getCodeVar() == inlineVar.getCodeVar()) {
continue;
}
if (Objects.equals(ssaVar.getName(), inlineVar.getName())) {
return ssaVar.getUseCount() > inlineVar.getUseCount();
}
}
return false;
}
private static boolean searchArgWithName(InsnNode assignInsn, String varName) {
InsnArg result = assignInsn.visitArgs(insnArg -> {
if (insnArg instanceof Named) {
String argName = ((Named) insnArg).getName();
if (Objects.equals(argName, varName)) {
return insnArg;
}
}
return null;
});
return result != null;
}
private static boolean assignInline(MethodNode mth, RegisterArg arg, InsnNode assignInsn, BlockNode assignBlock) {
RegisterArg useArg = arg.getSVar().getUseList().get(0);
InsnNode useInsn = useArg.getParentInsn();
@@ -140,7 +140,7 @@ public class SSATransform extends AbstractVisitor {
stack.push(initState);
while (!stack.isEmpty()) {
RenameState state = stack.pop();
renameVarsInBlock(state);
renameVarsInBlock(mth, state);
for (BlockNode dominated : state.getBlock().getDominatesOn()) {
stack.push(RenameState.copyFrom(state, dominated));
}
@@ -156,7 +156,7 @@ public class SSATransform extends AbstractVisitor {
}
}
private static void renameVarsInBlock(RenameState state) {
private static void renameVarsInBlock(MethodNode mth, RenameState state) {
BlockNode block = state.getBlock();
for (InsnNode insn : block.getInstructions()) {
if (insn.getType() != InsnType.PHI) {
@@ -168,8 +168,9 @@ public class SSATransform extends AbstractVisitor {
int regNum = reg.getRegNum();
SSAVar var = state.getVar(regNum);
if (var == null) {
throw new JadxRuntimeException("Not initialized variable reg: " + regNum
+ ", insn: " + insn + ", block:" + block);
// TODO: in most cases issue in incorrectly attached exception handlers
mth.addWarnComment("Not initialized variable reg: " + regNum + ", insn: " + insn + ", block:" + block);
var = state.startVar(reg);
}
var.use(reg);
}
@@ -9,6 +9,7 @@ import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.instructions.args.ArgType.WildcardBound;
import jadx.core.dex.instructions.args.PrimitiveType;
@@ -42,6 +43,17 @@ public class TypeCompare {
return compareObjects(first.getType(), second.getType());
}
public TypeCompareEnum compareTypes(ClassInfo first, ClassInfo second) {
return compareObjects(first.getType(), second.getType());
}
public TypeCompareEnum compareObjects(ArgType first, ArgType second) {
if (first == second || Objects.equals(first, second)) {
return TypeCompareEnum.EQUAL;
}
return compareObjectsNoPreCheck(first, second);
}
/**
* Compare two type and return result for first argument (narrow, wider or conflict)
*/
@@ -81,7 +93,7 @@ public class TypeCompare {
boolean firstObj = first.isObject();
boolean secondObj = second.isObject();
if (firstObj && secondObj) {
return compareObjects(first, second);
return compareObjectsNoPreCheck(first, second);
} else {
// primitive types conflicts with objects
if (firstObj && secondPrimitive) {
@@ -159,7 +171,7 @@ public class TypeCompare {
return CONFLICT;
}
private TypeCompareEnum compareObjects(ArgType first, ArgType second) {
private TypeCompareEnum compareObjectsNoPreCheck(ArgType first, ArgType second) {
boolean objectsEquals = first.getObject().equals(second.getObject());
boolean firstGenericType = first.isGenericType();
boolean secondGenericType = second.isGenericType();
@@ -262,7 +274,7 @@ public class TypeCompare {
return NARROW;
}
for (ArgType extendType : extendTypes) {
TypeCompareEnum res = compareObjects(extendType, objType);
TypeCompareEnum res = compareObjectsNoPreCheck(extendType, objType);
if (!res.isNarrow()) {
return res;
}
@@ -39,6 +39,10 @@ public enum TypeCompareEnum {
return this == WIDER || this == WIDER_BY_GENERIC;
}
public boolean isWiderOrEqual() {
return isEqual() || isWider();
}
public boolean isNarrow() {
return this == NARROW || this == NARROW_BY_GENERIC;
}
@@ -19,7 +19,7 @@ import jadx.core.Consts;
import jadx.core.clsp.ClspGraph;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.AnonymousClassBaseAttr;
import jadx.core.dex.attributes.nodes.AnonymousClassAttr;
import jadx.core.dex.attributes.nodes.PhiListAttr;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.instructions.ArithNode;
@@ -321,7 +321,7 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
if (ctr.isNewInstance()) {
ClassNode ctrCls = root.resolveClass(ctr.getClassType());
if (ctrCls != null && ctrCls.contains(AFlag.DONT_GENERATE)) {
AnonymousClassBaseAttr baseTypeAttr = ctrCls.get(AType.ANONYMOUS_CLASS_BASE);
AnonymousClassAttr baseTypeAttr = ctrCls.get(AType.ANONYMOUS_CLASS);
if (baseTypeAttr != null) {
return baseTypeAttr.getBaseType();
}
@@ -66,7 +66,11 @@ public final class TypeUpdate {
* Force type setting
*/
public TypeUpdateResult applyWithWiderIgnSame(MethodNode mth, SSAVar ssaVar, ArgType candidateType) {
return apply(mth, ssaVar, candidateType, TypeUpdateFlags.FLAGS_WIDER_IGNSAME);
return apply(mth, ssaVar, candidateType, TypeUpdateFlags.FLAGS_WIDER_IGNORE_SAME);
}
public TypeUpdateResult applyWithWiderIgnoreUnknown(MethodNode mth, SSAVar ssaVar, ArgType candidateType) {
return apply(mth, ssaVar, candidateType, TypeUpdateFlags.FLAGS_WIDER_IGNORE_UNKNOWN);
}
private TypeUpdateResult apply(MethodNode mth, SSAVar ssaVar, ArgType candidateType, TypeUpdateFlags flags) {
@@ -110,6 +114,9 @@ public final class TypeUpdate {
}
TypeCompareEnum compareResult = comparator.compareTypes(candidateType, currentType);
if (compareResult == TypeCompareEnum.UNKNOWN && updateInfo.getFlags().isIgnoreUnknown()) {
return REJECT;
}
if (arg.isTypeImmutable() && currentType != ArgType.UNKNOWN) {
// don't changed type
if (compareResult == TypeCompareEnum.EQUAL) {
@@ -1,39 +1,58 @@
package jadx.core.dex.visitors.typeinference;
import org.jetbrains.annotations.NotNull;
public class TypeUpdateFlags {
private static final int ALLOW_WIDER = 1;
private static final int IGNORE_SAME = 2;
private static final int IGNORE_UNKNOWN = 4;
public static final TypeUpdateFlags FLAGS_EMPTY = new TypeUpdateFlags(false, false);
public static final TypeUpdateFlags FLAGS_WIDER = new TypeUpdateFlags(true, false);
public static final TypeUpdateFlags FLAGS_WIDER_IGNSAME = new TypeUpdateFlags(true, true);
public static final TypeUpdateFlags FLAGS_EMPTY = build(0);
public static final TypeUpdateFlags FLAGS_WIDER = build(ALLOW_WIDER);
public static final TypeUpdateFlags FLAGS_WIDER_IGNORE_SAME = build(ALLOW_WIDER | IGNORE_SAME);
public static final TypeUpdateFlags FLAGS_WIDER_IGNORE_UNKNOWN = build(ALLOW_WIDER | IGNORE_UNKNOWN);
private final boolean allowWider;
private final boolean ignoreSame;
private final int flags;
private TypeUpdateFlags(boolean allowWider, boolean ignoreSame) {
this.allowWider = allowWider;
this.ignoreSame = ignoreSame;
@NotNull
private static TypeUpdateFlags build(int flags) {
return new TypeUpdateFlags(flags);
}
private TypeUpdateFlags(int flags) {
this.flags = flags;
}
public boolean isAllowWider() {
return allowWider;
return (flags & ALLOW_WIDER) != 0;
}
public boolean isIgnoreSame() {
return ignoreSame;
return (flags & IGNORE_SAME) != 0;
}
public boolean isIgnoreUnknown() {
return (flags & IGNORE_UNKNOWN) != 0;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
if (allowWider) {
if (isAllowWider()) {
sb.append("ALLOW_WIDER");
}
if (ignoreSame) {
if (isIgnoreSame()) {
if (sb.length() != 0) {
sb.append('|');
}
sb.append("IGNORE_SAME");
}
if (isIgnoreUnknown()) {
if (sb.length() != 0) {
sb.append('|');
}
sb.append("IGNORE_UNKNOWN");
}
return sb.toString();
}
}
@@ -110,7 +110,8 @@ public class ExportGradleProject {
Integer versionCode = Integer.valueOf(manifest.getAttribute("android:versionCode"));
String versionName = manifest.getAttribute("android:versionName");
Integer minSdk = Integer.valueOf(usesSdk.getAttribute("android:minSdkVersion"));
Integer targetSdk = Integer.valueOf(usesSdk.getAttribute("android:targetSdkVersion"));
String stringTargetSdk = usesSdk.getAttribute("android:targetSdkVersion");
Integer targetSdk = stringTargetSdk.isEmpty() ? minSdk : Integer.valueOf(stringTargetSdk);
String appName = "UNKNOWN";
if (application.hasAttribute("android:label")) {
@@ -35,6 +35,7 @@ import jadx.core.dex.nodes.IBlock;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.regions.conditions.IfCondition;
import jadx.core.dex.trycatch.ExceptionHandler;
import jadx.core.utils.exceptions.JadxRuntimeException;
public class BlockUtils {
@@ -382,6 +383,7 @@ public class BlockUtils {
/**
* Return first successor which not exception handler and not follow loop back edge
*/
@Nullable
public static BlockNode getNextBlock(BlockNode block) {
List<BlockNode> s = block.getCleanSuccessors();
return s.isEmpty() ? null : s.get(0);
@@ -594,6 +596,7 @@ public class BlockUtils {
/**
* Search last block in control flow graph from input set.
*/
@Nullable
public static BlockNode getBottomBlock(List<BlockNode> blocks) {
if (blocks.size() == 1) {
return blocks.get(0);
@@ -701,7 +704,7 @@ public class BlockUtils {
mth.getLoops().forEach(l -> excluded.set(l.getStart().getId()));
if (!mth.isNoExceptionHandlers()) {
// exclude exception handlers paths
mth.getExceptionHandlers().forEach(h -> excluded.or(h.getHandlerBlock().getDomFrontier()));
mth.getExceptionHandlers().forEach(h -> mergeExcHandlerDomFrontier(mth, h, excluded));
}
domFrontBS.andNot(excluded);
oneBlock = bitSetToOneBlock(mth, domFrontBS);
@@ -709,6 +712,7 @@ public class BlockUtils {
return oneBlock;
}
BitSet combinedDF = newBlocksBitSet(mth);
int k = mth.getBasicBlocks().size();
while (true) {
// collect dom frontier blocks from current set until only one block left
forEachBlockFromBitSet(mth, domFrontBS, block -> {
@@ -726,6 +730,10 @@ public class BlockUtils {
if (cardinality == 0) {
return null;
}
if (k-- < 0) {
mth.addWarnComment("Path cross not found for " + blocks + ", limit reached: " + mth.getBasicBlocks().size());
return null;
}
// replace domFrontBS with combinedDF
domFrontBS.clear();
domFrontBS.or(combinedDF);
@@ -733,6 +741,20 @@ public class BlockUtils {
}
}
private static void mergeExcHandlerDomFrontier(MethodNode mth, ExceptionHandler handler, BitSet set) {
BlockNode handlerBlock = handler.getHandlerBlock();
if (handlerBlock == null) {
mth.addDebugComment("Null handler block in: " + handler);
return;
}
BitSet domFrontier = handlerBlock.getDomFrontier();
if (domFrontier == null) {
mth.addDebugComment("Null dom frontier in handler: " + handler);
return;
}
set.or(domFrontier);
}
public static BlockNode getPathCross(MethodNode mth, BlockNode b1, BlockNode b2) {
if (b1 == b2) {
return b1;
@@ -3,11 +3,10 @@ package jadx.core.utils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
@@ -17,12 +16,13 @@ import jadx.api.IDecompileScheduler;
import jadx.api.JadxDecompiler;
import jadx.api.JavaClass;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.utils.exceptions.JadxRuntimeException;
public class DecompilerScheduler implements IDecompileScheduler {
private static final Logger LOG = LoggerFactory.getLogger(DecompilerScheduler.class);
private static final int MERGED_BATCH_SIZE = 16;
private static final boolean DUMP_STATS = false;
private static final boolean DEBUG_BATCHES = false;
private final JadxDecompiler decompiler;
@@ -32,13 +32,21 @@ public class DecompilerScheduler implements IDecompileScheduler {
@Override
public List<List<JavaClass>> buildBatches(List<JavaClass> classes) {
long start = System.currentTimeMillis();
List<List<ClassNode>> batches = internalBatches(Utils.collectionMap(classes, JavaClass::getClassNode));
List<List<JavaClass>> result = Utils.collectionMap(batches, l -> Utils.collectionMapNoNull(l, decompiler::getJavaClassByNode));
if (LOG.isDebugEnabled()) {
LOG.debug("Build decompilation batches in {}ms", System.currentTimeMillis() - start);
try {
long start = System.currentTimeMillis();
List<List<ClassNode>> batches = internalBatches(Utils.collectionMap(classes, JavaClass::getClassNode));
List<List<JavaClass>> result = Utils.collectionMap(batches, l -> Utils.collectionMapNoNull(l, decompiler::getJavaClassByNode));
if (LOG.isDebugEnabled()) {
LOG.debug("Build decompilation batches in {}ms", System.currentTimeMillis() - start);
}
if (DEBUG_BATCHES) {
check(result, classes);
}
return result;
} catch (Throwable e) {
LOG.warn("Build batches failed (continue with fallback)", e);
return buildFallback(classes);
}
return result;
}
/**
@@ -46,26 +54,20 @@ public class DecompilerScheduler implements IDecompileScheduler {
* Build batches for dependencies of single class to avoid locking from another thread.
*/
public List<List<ClassNode>> internalBatches(List<ClassNode> classes) {
Map<ClassNode, DepInfo> depsMap = new HashMap<>(classes.size());
Set<ClassNode> visited = new HashSet<>();
for (ClassNode classNode : classes) {
visited.clear();
sumDeps(classNode, depsMap, visited);
}
List<DepInfo> deps = new ArrayList<>(depsMap.values());
Collections.sort(deps);
List<DepInfo> deps = sumDependencies(classes);
Set<ClassNode> added = new HashSet<>(classes.size());
Comparator<ClassNode> cmpDepSize = Comparator.comparingInt(c -> c.getDependencies().size());
Comparator<ClassNode> cmpDepSize = Comparator.comparingInt(ClassNode::getTotalDepsCount);
List<List<ClassNode>> result = new ArrayList<>();
List<ClassNode> mergedBatch = new ArrayList<>(MERGED_BATCH_SIZE);
for (DepInfo depInfo : deps) {
ClassNode cls = depInfo.getCls();
int depsSize = cls.getDependencies().size();
if (!added.add(cls)) {
continue;
}
int depsSize = cls.getTotalDepsCount();
if (depsSize == 0) {
// add classes without dependencies in merged batch
mergedBatch.add(cls);
added.add(cls);
if (mergedBatch.size() >= MERGED_BATCH_SIZE) {
result.add(mergedBatch);
mergedBatch = new ArrayList<>(MERGED_BATCH_SIZE);
@@ -76,38 +78,34 @@ public class DecompilerScheduler implements IDecompileScheduler {
ClassNode topDep = dep.getTopParentClass();
if (!added.contains(topDep)) {
batch.add(topDep);
added.add(topDep);
}
}
batch.sort(cmpDepSize);
batch.add(cls);
added.addAll(batch);
result.add(batch);
}
}
if (mergedBatch.size() > 0) {
result.add(mergedBatch);
}
if (DUMP_STATS) {
if (DEBUG_BATCHES) {
dumpBatchesStats(classes, result, deps);
}
return result;
}
public int sumDeps(ClassNode cls, Map<ClassNode, DepInfo> depsMap, Set<ClassNode> visited) {
visited.add(cls);
DepInfo depInfo = depsMap.get(cls);
if (depInfo != null) {
return depInfo.getDepsCount();
}
List<ClassNode> deps = cls.getDependencies();
int count = deps.size();
for (ClassNode dep : deps) {
if (!visited.contains(dep)) {
count += sumDeps(dep, depsMap, visited);
private static List<DepInfo> sumDependencies(List<ClassNode> classes) {
List<DepInfo> deps = new ArrayList<>(classes.size());
for (ClassNode cls : classes) {
int count = 0;
for (ClassNode dep : cls.getDependencies()) {
count += 1 + dep.getTotalDepsCount();
}
deps.add(new DepInfo(cls, count));
}
depsMap.put(cls, new DepInfo(cls, count));
return count;
Collections.sort(deps);
return deps;
}
private static final class DepInfo implements Comparable<DepInfo> {
@@ -129,19 +127,43 @@ public class DecompilerScheduler implements IDecompileScheduler {
@Override
public int compareTo(@NotNull DecompilerScheduler.DepInfo o) {
return Integer.compare(depsCount, o.depsCount);
int deps = Integer.compare(depsCount, o.depsCount);
if (deps == 0) {
return cls.compareTo(o.cls);
}
return deps;
}
@Override
public String toString() {
return cls + ":" + depsCount;
}
}
private static List<List<JavaClass>> buildFallback(List<JavaClass> classes) {
return classes.stream()
.sorted(Comparator.comparingInt(c -> c.getClassNode().getTotalDepsCount()))
.map(Collections::singletonList)
.collect(Collectors.toList());
}
private void dumpBatchesStats(List<ClassNode> classes, List<List<ClassNode>> result, List<DepInfo> deps) {
double avg = result.stream().mapToInt(List::size).average().orElse(-1);
int maxSingleDeps = classes.stream().mapToInt(c -> c.getDependencies().size()).max().orElse(-1);
int maxRecursiveDeps = deps.stream().mapToInt(DepInfo::getDepsCount).max().orElse(-1);
int maxSingleDeps = classes.stream().mapToInt(ClassNode::getTotalDepsCount).max().orElse(-1);
int maxSubDeps = deps.stream().mapToInt(DepInfo::getDepsCount).max().orElse(-1);
LOG.info("Batches stats:"
+ "\n input classes: " + classes.size()
+ ",\n batches: " + result.size()
+ ",\n average batch size: " + avg
+ ",\n average batch size: " + String.format("%.2f", avg)
+ ",\n max single deps count: " + maxSingleDeps
+ ",\n max recursive deps count: " + maxRecursiveDeps);
+ ",\n max sub deps count: " + maxSubDeps);
}
private static void check(List<List<JavaClass>> result, List<JavaClass> classes) {
int classInBatches = result.stream().mapToInt(List::size).sum();
if (classes.size() != classInBatches) {
throw new JadxRuntimeException(
"Incorrect number of classes in result batch: " + classInBatches + ", expected: " + classes.size());
}
}
}
@@ -96,7 +96,7 @@ public class InsnUtils {
return ((ConstClassNode) insn).getClsType();
case SGET:
FieldInfo f = (FieldInfo) ((IndexInsnNode) insn).getIndex();
FieldNode fieldNode = root.deepResolveField(f);
FieldNode fieldNode = root.resolveField(f);
if (fieldNode == null) {
LOG.warn("Field {} not found", f);
return null;
@@ -6,13 +6,13 @@ import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Function;
import java.util.function.Predicate;
import org.jetbrains.annotations.Nullable;
import jadx.core.dex.nodes.BlockNode;
public class ListUtils {
public static <T> boolean isSingleElement(@Nullable List<T> list, T obj) {
@@ -48,7 +48,19 @@ public class ListUtils {
return list.get(list.size() - 1);
}
public static List<BlockNode> distinctList(List<BlockNode> list) {
public static <T extends Comparable<T>> List<T> distinctMergeSortedLists(List<T> first, List<T> second) {
if (first.isEmpty()) {
return second;
}
if (second.isEmpty()) {
return first;
}
Set<T> set = new TreeSet<>(first);
set.addAll(second);
return new ArrayList<>(set);
}
public static <T> List<T> distinctList(List<T> list) {
return new ArrayList<>(new LinkedHashSet<>(list));
}
@@ -100,6 +112,19 @@ public class ListUtils {
return list;
}
public static <T> List<T> filter(List<T> list, Predicate<T> filter) {
if (list == null || list.isEmpty()) {
return Collections.emptyList();
}
List<T> result = new ArrayList<>();
for (T element : list) {
if (filter.test(element)) {
result.add(element);
}
}
return result;
}
/**
* Search exactly one element in list by filter
*
@@ -134,4 +159,16 @@ public class ListUtils {
}
return true;
}
public static <T> boolean anyMatch(List<T> list, Predicate<T> test) {
if (list == null || list.isEmpty()) {
return false;
}
for (T element : list) {
if (test.test(element)) {
return true;
}
}
return false;
}
}
@@ -24,6 +24,8 @@ import java.io.OutputStream;
import javax.imageio.ImageIO;
import org.jetbrains.annotations.Nullable;
import jadx.core.utils.exceptions.JadxRuntimeException;
/**
@@ -31,16 +33,19 @@ import jadx.core.utils.exceptions.JadxRuntimeException;
*/
public class Res9patchStreamDecoder {
public void decode(InputStream in, OutputStream out) {
public boolean decode(InputStream in, OutputStream out) {
try {
BufferedImage im = ImageIO.read(in);
NinePatch np = getNinePatch(in);
if (np == null) {
return false;
}
int w = im.getWidth();
int h = im.getHeight();
BufferedImage im2 = new BufferedImage(w + 2, h + 2, BufferedImage.TYPE_INT_ARGB);
im2.createGraphics().drawImage(im, 1, 1, w, h, null);
NinePatch np = getNinePatch(in);
drawHLine(im2, h + 1, np.padLeft + 1, w - np.padRight);
drawVLine(im2, w + 1, np.padTop + 1, h - np.padBottom);
@@ -55,28 +60,32 @@ public class Res9patchStreamDecoder {
}
ImageIO.write(im2, "png", out);
return true;
} catch (Exception e) {
throw new JadxRuntimeException("9patch image decode error", e);
}
}
@Nullable
private NinePatch getNinePatch(InputStream in) throws IOException {
ExtDataInput di = new ExtDataInput(in);
find9patchChunk(di);
if (!find9patchChunk(di)) {
return null;
}
return NinePatch.decode(di);
}
private void find9patchChunk(DataInput di) throws IOException {
private boolean find9patchChunk(DataInput di) throws IOException {
di.skipBytes(8);
while (true) {
int size;
try {
size = di.readInt();
} catch (IOException ex) {
throw new JadxRuntimeException("Cant find nine patch chunk", ex);
return false;
}
if (di.readInt() == NP_CHUNK_TYPE) {
return;
return true;
}
di.skipBytes(size + 4);
}
@@ -9,8 +9,12 @@ import org.slf4j.LoggerFactory;
import jadx.api.plugins.input.data.annotations.EncodedType;
import jadx.api.plugins.input.data.annotations.EncodedValue;
import jadx.api.plugins.input.data.annotations.IAnnotation;
import jadx.core.deobf.ClsAliasPair;
import jadx.core.deobf.NameMapper;
import jadx.core.dex.attributes.nodes.RenameReasonAttr;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.utils.Utils;
// TODO: parse data from d1 (protobuf encoded) to get original method names and other useful info
public class KotlinMetadataUtils {
@@ -18,13 +22,12 @@ public class KotlinMetadataUtils {
private static final String KOTLIN_METADATA_ANNOTATION = "Lkotlin/Metadata;";
private static final String KOTLIN_METADATA_D2_PARAMETER = "d2";
private static final String KOTLIN_METADATA_CLASSNAME_REGEX = "(L.*;)";
/**
* Try to get class info from Kotlin Metadata annotation
*/
@Nullable
public static ClassInfo getClassName(ClassNode cls) {
public static ClsAliasPair getClassAlias(ClassNode cls) {
IAnnotation metadataAnnotation = cls.getAnnotation(KOTLIN_METADATA_ANNOTATION);
List<EncodedValue> d2Param = getParamAsList(metadataAnnotation, KOTLIN_METADATA_D2_PARAMETER);
if (d2Param == null || d2Param.isEmpty()) {
@@ -36,8 +39,14 @@ public class KotlinMetadataUtils {
}
try {
String rawClassName = ((String) firstValue.getValue()).trim();
if (rawClassName.matches(KOTLIN_METADATA_CLASSNAME_REGEX)) {
return ClassInfo.fromName(cls.root(), rawClassName);
if (rawClassName.isEmpty()) {
return null;
}
String clsName = Utils.cleanObjectName(rawClassName);
ClsAliasPair alias = splitAndCheckClsName(cls, clsName);
if (alias != null) {
RenameReasonAttr.forNode(cls).append("from Kotlin metadata");
return alias;
}
} catch (Exception e) {
LOG.error("Failed to parse kotlin metadata", e);
@@ -45,6 +54,54 @@ public class KotlinMetadataUtils {
return null;
}
// Don't use ClassInfo facility to not pollute class into cache
private static ClsAliasPair splitAndCheckClsName(ClassNode originCls, String fullClsName) {
if (!NameMapper.isValidFullIdentifier(fullClsName)) {
return null;
}
String pkg;
String name;
int dot = fullClsName.lastIndexOf('.');
if (dot == -1) {
pkg = "";
name = fullClsName;
} else {
pkg = fullClsName.substring(0, dot);
name = fullClsName.substring(dot + 1);
}
ClassInfo originClsInfo = originCls.getClassInfo();
String originName = originClsInfo.getShortName();
if (originName.equals(name)
|| name.contains("$")
|| !NameMapper.isValidIdentifier(name)
|| countPkgParts(originClsInfo.getPackage()) != countPkgParts(pkg)
|| pkg.startsWith("java.")) {
return null;
}
ClassNode newClsNode = originCls.root().resolveClass(fullClsName);
if (newClsNode != null) {
// class with alias name already exist
return null;
}
return new ClsAliasPair(pkg, name);
}
private static int countPkgParts(String pkg) {
if (pkg.isEmpty()) {
return 0;
}
int count = 1;
int pos = 0;
while (true) {
pos = pkg.indexOf('.', pos);
if (pos == -1) {
return count;
}
pos++;
count++;
}
}
@SuppressWarnings("unchecked")
private static List<EncodedValue> getParamAsList(IAnnotation annotation, String paramName) {
if (annotation == null) {
@@ -1,9 +1,13 @@
package jadx.core.xmlgen;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.xml.parsers.DocumentBuilder;
@@ -171,19 +175,20 @@ public class ManifestAttributes {
if (attr.getType() == MAttrType.ENUM) {
return attr.getValues().get(value);
} else if (attr.getType() == MAttrType.FLAG) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<Long, String> entry : attr.getValues().entrySet()) {
long key = entry.getKey();
List<String> flagList = new LinkedList<>();
List<Long> attrKeys = new ArrayList<>(attr.getValues().keySet());
attrKeys.sort((a, b) -> Long.compare(b, a)); // sort descending
for (Long key : attrKeys) {
String attrValue = attr.getValues().get(key);
if (value == key) {
sb = new StringBuilder(entry.getValue() + '|');
flagList.add(attrValue);
break;
} else if ((key != 0) && ((value & key) == key)) {
sb.append(entry.getValue()).append('|');
flagList.add(attrValue);
value ^= key;
}
}
if (sb.length() != 0) {
return sb.deleteCharAt(sb.length() - 1).toString();
}
return flagList.stream().collect(Collectors.joining("|"));
}
return null;
}
@@ -1,46 +1,33 @@
package jadx.core.xmlgen;
import java.util.HashMap;
import java.util.Map;
import org.jetbrains.annotations.Nullable;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.nodes.RootNode;
/*
* modifies android:name attributes and xml tags which are old class names
* but were changed during deobfuscation
* Modifies android:name attributes and xml tags which were changed during deobfuscation
*/
public class XmlDeobf {
private static final Map<String, String> DEOBF_MAP = new HashMap<>();
private XmlDeobf() {
}
@Nullable
public static String deobfClassName(RootNode rootNode, String potencialClassName, String packageName) {
potencialClassName = potencialClassName.replace('$', '.');
if (packageName != null && potencialClassName.startsWith(".")) {
potencialClassName = packageName + potencialClassName;
public static String deobfClassName(RootNode root, String potentialClassName, String packageName) {
if (potentialClassName.indexOf('.') == -1) {
return null;
}
return getNewClassName(rootNode, potencialClassName);
}
private static String getNewClassName(RootNode rootNode, String old) {
if (DEOBF_MAP.isEmpty()) {
for (ClassNode classNode : rootNode.getClasses(true)) {
ClassInfo classInfo = classNode.getClassInfo();
if (classInfo.hasAlias()) {
String oldName = classInfo.getFullName();
String newName = classInfo.getAliasFullName();
if (!oldName.equals(newName)) {
DEOBF_MAP.put(oldName, newName);
}
}
}
if (packageName != null && potentialClassName.startsWith(".")) {
potentialClassName = packageName + potentialClassName;
}
return DEOBF_MAP.get(old);
ArgType clsType = ArgType.object(potentialClassName);
ClassInfo classInfo = root.getInfoStorage().getCls(clsType);
if (classInfo == null) {
// unknown class reference
return null;
}
return classInfo.getAliasFullName();
}
}
@@ -0,0 +1,469 @@
# All tld domains with 3 or less characters
# Created from https://data.iana.org/TLD/tlds-alpha-by-domain.txt version 2022020500
aaa
abb
abc
ac
aco
ad
ads
ae
aeg
af
afl
ag
ai
aig
al
am
anz
ao
aol
app
aq
ar
art
as
at
au
aw
aws
ax
axa
az
ba
bar
bb
bbc
bbt
bcg
bcn
bd
be
bet
bf
bg
bh
bi
bid
bio
biz
bj
bm
bms
bmw
bn
bo
bom
boo
bot
box
br
bs
bt
buy
bv
bw
by
bz
bzh
ca
cab
cal
cam
car
cat
cba
cbn
cbs
cc
cd
ceo
cf
cfa
cfd
cg
ch
ci
ck
cl
cm
cn
co
com
cpa
cr
crs
cu
cv
cw
cx
cy
cz
dad
day
dds
de
dev
dhl
diy
dj
dk
dm
dnp
do
dog
dot
dtv
dvr
dz
eat
ec
eco
edu
ee
eg
er
es
esq
et
eu
eus
fan
fi
fit
fj
fk
fly
fm
fo
foo
fox
fr
frl
ftr
fun
fyi
ga
gal
gap
gay
gb
gd
gdn
ge
gea
gf
gg
gh
gi
gl
gle
gm
gmo
gmx
gn
goo
gop
got
gov
gp
gq
gr
gs
gt
gu
gw
gy
hbo
hiv
hk
hkt
hm
hn
hot
how
hr
ht
hu
ibm
ice
icu
id
ie
ifm
il
im
in
inc
ing
ink
int
io
iq
ir
is
ist
it
itv
jcb
je
jio
jll
jm
jmp
jnj
jo
jot
joy
jp
ke
kfh
kg
kh
ki
kia
kim
km
kn
kp
kpn
kr
krd
kw
ky
kz
la
lat
law
lb
lc
lds
li
lk
llc
llp
lol
lpl
lr
ls
lt
ltd
lu
lv
ly
ma
man
map
mba
mc
md
me
med
men
mg
mh
mil
mit
mk
ml
mlb
mls
mm
mma
mn
mo
moe
moi
mom
mov
mp
mq
mr
ms
msd
mt
mtn
mtr
mu
mv
mw
mx
my
mz
na
nab
nba
nc
ne
nec
net
new
nf
nfl
ng
ngo
nhk
ni
nl
no
now
np
nr
nra
nrw
ntt
nu
nyc
nz
obi
om
one
ong
onl
ooo
org
ott
ovh
pa
pay
pe
pet
pf
pg
ph
phd
pid
pin
pk
pl
pm
pn
pnc
pr
pro
pru
ps
pt
pub
pw
pwc
py
qa
re
red
ren
ril
rio
rip
ro
rs
ru
run
rw
rwe
sa
sap
sas
sb
sbi
sbs
sc
sca
scb
sd
se
ses
sew
sex
sfr
sg
sh
si
sj
sk
ski
sky
sl
sm
sn
so
soy
spa
sr
srl
ss
st
stc
su
sv
sx
sy
sz
tab
tax
tc
tci
td
tdk
tel
tf
tg
th
thd
tj
tjx
tk
tl
tm
tn
to
top
tr
trv
tt
tui
tv
tvs
tw
tz
ua
ubs
ug
uk
uno
uol
ups
us
uy
uz
va
vc
ve
vet
vg
vi
vig
vin
vip
vn
vu
wed
wf
win
wme
wow
ws
wtc
wtf
xin
xxx
xyz
ye
you
yt
yun
za
zip
zm
zw
@@ -0,0 +1,17 @@
package jadx.api;
import jadx.core.dex.nodes.RootNode;
import jadx.core.xmlgen.BinaryXMLParser;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class JadxDecompilerTestUtils {
public static JadxDecompiler getMockDecompiler() {
JadxDecompiler decompiler = mock(JadxDecompiler.class);
RootNode rootNode = new RootNode(new JadxArgs());
when(decompiler.getRoot()).thenReturn(rootNode);
when(decompiler.getBinaryXmlParser()).thenReturn(new BinaryXMLParser(rootNode));
return decompiler;
}
}
@@ -0,0 +1,70 @@
package jadx.tests.api;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.stream.Stream;
import org.junit.jupiter.api.io.TempDir;
import jadx.api.ICodeInfo;
import jadx.api.JadxDecompiler;
import jadx.api.JadxDecompilerTestUtils;
import jadx.api.ResourceFile;
import jadx.core.dex.nodes.RootNode;
import jadx.core.export.ExportGradleProject;
import jadx.core.xmlgen.ResContainer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public abstract class ExportGradleTest {
private static final String MANIFEST_TESTS_DIR = "src/test/manifest";
@TempDir
private File exportDir;
protected ResContainer createResourceContainer(String filename) {
final ResContainer container = mock(ResContainer.class);
ICodeInfo codeInfo = mock(ICodeInfo.class);
when(codeInfo.getCodeStr()).thenReturn(loadFileContent(new File(MANIFEST_TESTS_DIR, filename)));
when(container.getText()).thenReturn(codeInfo);
return container;
}
private static String loadFileContent(File filePath) {
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(filePath.toPath(), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
fail("Loading file failed: %s", e.getMessage());
}
return contentBuilder.toString();
}
protected void exportGradle(String manifestFilename, String stringsFileName) {
final JadxDecompiler decompiler = JadxDecompilerTestUtils.getMockDecompiler();
ResourceFile androidManifest = mock(ResourceFile.class);
final ResContainer androidManifestContainer = createResourceContainer(manifestFilename);
when(androidManifest.loadContent()).thenReturn(androidManifestContainer);
final ResContainer strings = createResourceContainer(stringsFileName);
final RootNode root = decompiler.getRoot();
final ExportGradleProject export =
new ExportGradleProject(root, exportDir, androidManifest, strings);
export.init();
assertThat(export.getSrcOutDir().exists());
assertThat(export.getResOutDir().exists());
}
protected String getAppGradleBuild() {
File appBuildGradle = new File(exportDir, "app/build.gradle");
assertThat(appBuildGradle.exists());
return loadFileContent(appBuildGradle);
}
}
@@ -5,6 +5,7 @@ import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
@@ -16,8 +17,9 @@ import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assumptions;
@@ -95,6 +97,8 @@ public abstract class IntegrationTest extends TestUtils {
protected boolean useEclipseCompiler;
private int targetJavaVersion = 8;
private boolean saveTestJar = false;
protected Map<Integer, String> resMap = Collections.emptyMap();
private boolean allowWarnInCode;
@@ -457,17 +461,28 @@ public abstract class IntegrationTest extends TestUtils {
Path outTmp = FileUtils.createTempDir("jadx-tmp-classes");
List<File> files = StaticCompiler.compile(compileFileList, outTmp.toFile(), withDebugInfo, useEclipseCompiler, targetJavaVersion);
files.forEach(File::deleteOnExit);
if (saveTestJar) {
saveToJar(files, outTmp);
}
// remove classes which are parents for test class
String clsName = clsFullName.substring(clsFullName.lastIndexOf('.') + 1);
files.removeIf(next -> !next.getName().contains(clsName));
return files;
}
@NotNull
protected static String removeLineComments(ClassNode cls) {
String code = cls.getCode().getCodeStr().replaceAll("\\W*//.*", "");
System.out.println(code);
return code;
private void saveToJar(List<File> files, Path baseDir) throws IOException {
Path jarFile = Files.createTempFile("tests-" + getTestName() + '-', ".jar");
try (JarOutputStream jar = new JarOutputStream(Files.newOutputStream(jarFile))) {
for (File file : files) {
Path fullPath = file.toPath();
Path relativePath = baseDir.relativize(fullPath);
JarEntry entry = new JarEntry(relativePath.toString());
jar.putNextEntry(entry);
jar.write(Files.readAllBytes(fullPath));
jar.closeEntry();
}
}
LOG.info("Test jar saved to: {}", jarFile.toAbsolutePath());
}
public JadxArgs getArgs() {
@@ -537,21 +552,12 @@ public abstract class IntegrationTest extends TestUtils {
}
// Use only for debug purpose
@Deprecated
protected void outputCFG() {
this.args.setCfgOutput(true);
this.args.setRawCFGOutput(true);
}
// Use only for debug purpose
@Deprecated
protected void printDisassemble() {
this.printDisassemble = true;
}
// Use only for debug purpose
@Deprecated
protected void outputRawCFG() {
this.args.setRawCFGOutput(true);
protected void saveTestJar() {
this.saveTestJar = true;
}
}
@@ -52,19 +52,18 @@ public class JadxCodeAssertions extends AbstractStringAssert<JadxCodeAssertions>
}
String indent = TestUtils.indent(commonIndent);
StringBuilder sb = new StringBuilder();
boolean first = true;
for (String line : lines) {
if (!line.isEmpty()) {
if (first) {
first = false;
} else {
sb.append(ICodeWriter.NL);
}
sb.append(indent);
sb.append(line);
sb.append(ICodeWriter.NL);
if (line.isEmpty()) {
// don't add common indent to empty lines
continue;
}
String searchLine = indent + line;
sb.append(searchLine);
// check every line for easier debugging
contains(searchLine);
}
return containsOnlyOnce(sb.toString());
return containsOnlyOnce(sb.substring(ICodeWriter.NL.length()));
}
public JadxCodeAssertions removeBlockComments() {
@@ -0,0 +1,18 @@
package jadx.tests.export;
import org.junit.jupiter.api.Test;
import jadx.tests.api.ExportGradleTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class OptionalTargetSdkVersion extends ExportGradleTest {
@Test
void test() {
exportGradle("OptionalTargetSdkVersion.xml", "strings.xml");
assertThat(getAppGradleBuild()).contains("targetSdkVersion 14");
}
}
@@ -10,14 +10,15 @@ import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.CommentsLevel;
import jadx.api.ICodeWriter;
import jadx.api.JadxArgs;
import jadx.api.JadxDecompiler;
import jadx.api.JadxInternalAccess;
import jadx.api.JavaClass;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.DebugChecks;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.tests.api.IntegrationTest;
@@ -35,9 +36,14 @@ public abstract class BaseExternalTest extends IntegrationTest {
}
protected JadxArgs prepare(File input) {
DebugChecks.checksEnabled = false;
JadxArgs args = new JadxArgs();
args.getInputFiles().add(input);
args.setOutDir(new File("../jadx-external-tests-tmp"));
args.setSkipFilesSave(true);
args.setSkipResources(true);
args.setShowInconsistentCode(true);
args.setCommentsLevel(CommentsLevel.DEBUG);
return args;
}
@@ -54,8 +60,7 @@ public abstract class BaseExternalTest extends IntegrationTest {
jadx.load();
if (clsPatternStr == null) {
processAll(jadx);
// jadx.saveSources();
jadx.save();
} else {
processByPatterns(jadx, clsPatternStr, mthPatternStr);
}
@@ -63,16 +68,6 @@ public abstract class BaseExternalTest extends IntegrationTest {
return jadx;
}
private void processAll(JadxDecompiler jadx) {
for (JavaClass javaClass : jadx.getClasses()) {
try {
javaClass.decompile();
} catch (Exception e) {
LOG.error("Failed to decompile class: {}", javaClass, e);
}
}
}
private void processByPatterns(JadxDecompiler jadx, String clsPattern, @Nullable String mthPattern) {
RootNode root = JadxInternalAccess.getRoot(jadx);
int processed = 0;
@@ -70,6 +70,19 @@ class SignatureParserTest {
assertThat(result, equalTo(outerGeneric(outerGeneric(obj, object("I")), object("X"))));
}
@Test
public void testNestedInnerGeneric2() {
// full name in inner class
String signature = "Lsome/long/pkg/ba<Lsome/pkg/s;>.some/long/pkg/bb<Lsome/pkg/p;Lsome/pkg/n;>;";
ArgType result = new SignatureParser(signature).consumeType();
System.out.println(result);
assertThat(result.getObject(), is("some.long.pkg.ba$some.long.pkg.bb"));
ArgType baseObj = generic("Lsome/long/pkg/ba;", object("Lsome/pkg/s;"));
ArgType innerObj = generic("Lsome/long/pkg/bb;", object("Lsome/pkg/p;"), object("Lsome/pkg/n;"));
ArgType obj = outerGeneric(baseObj, innerObj);
assertThat(result, equalTo(obj));
}
@Test
public void testWildcards() {
checkWildcards("*", wildcard());
@@ -28,7 +28,8 @@ public class TestKotlinMetadata extends SmaliTest {
prepareArgs(true);
assertThat(getClassNodeFromSmali())
.code()
.containsOne("class TestMetaData {");
.containsOne("class TestMetaData {")
.containsOne("reason: from Kotlin metadata");
}
@Test
@@ -42,6 +43,7 @@ public class TestKotlinMetadata extends SmaliTest {
private void prepareArgs(boolean parseKotlinMetadata) {
enableDeobfuscation();
args.setDeobfuscationMinLength(100); // rename everything
args.setDeobfuscationForceSave(true);
getArgs().setParseKotlinMetadata(parseKotlinMetadata);
disableCompilation();
}
@@ -2,11 +2,10 @@ package jadx.tests.integration.enums;
import org.junit.jupiter.api.Test;
import jadx.core.dex.nodes.ClassNode;
import jadx.api.CommentsLevel;
import jadx.tests.api.IntegrationTest;
import jadx.tests.api.utils.JadxMatchers;
import static org.hamcrest.MatcherAssert.assertThat;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestEnums2 extends IntegrationTest {
@@ -32,25 +31,25 @@ public class TestEnums2 extends IntegrationTest {
@Test
public void test() {
ClassNode cls = getClassNode(TestCls.class);
String code = removeLineComments(cls);
assertThat(code, JadxMatchers.containsLines(1,
"public enum Operation {",
indent(1) + "PLUS {",
indent(2) + "@Override",
indent(2) + "public int apply(int x, int y) {",
indent(3) + "return x + y;",
indent(2) + '}',
indent(1) + "},",
indent(1) + "MINUS {",
indent(2) + "@Override",
indent(2) + "public int apply(int x, int y) {",
indent(3) + "return x - y;",
indent(2) + '}',
indent(1) + "};",
"",
indent(1) + "public abstract int apply(int i, int i2);",
"}"));
getArgs().setCommentsLevel(CommentsLevel.WARN);
assertThat(getClassNode(TestCls.class))
.code()
.containsLines(1,
"public enum Operation {",
indent(1) + "PLUS {",
indent(2) + "@Override",
indent(2) + "public int apply(int x, int y) {",
indent(3) + "return x + y;",
indent(2) + '}',
indent(1) + "},",
indent(1) + "MINUS {",
indent(2) + "@Override",
indent(2) + "public int apply(int x, int y) {",
indent(3) + "return x - y;",
indent(2) + '}',
indent(1) + "};",
"",
indent(1) + "public abstract int apply(int i, int i2);",
"}");
}
}
@@ -2,11 +2,10 @@ package jadx.tests.integration.enums;
import org.junit.jupiter.api.Test;
import jadx.core.dex.nodes.ClassNode;
import jadx.api.CommentsLevel;
import jadx.tests.api.IntegrationTest;
import jadx.tests.api.utils.JadxMatchers;
import static org.hamcrest.MatcherAssert.assertThat;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestEnumsInterface extends IntegrationTest {
@@ -34,23 +33,23 @@ public class TestEnumsInterface extends IntegrationTest {
@Test
public void test() {
ClassNode cls = getClassNode(TestCls.class);
String code = removeLineComments(cls);
assertThat(code, JadxMatchers.containsLines(1,
"public enum Operation implements IOperation {",
indent(1) + "PLUS {",
indent(2) + "@Override",
indent(2) + "public int apply(int x, int y) {",
indent(3) + "return x + y;",
indent(2) + '}',
indent(1) + "},",
indent(1) + "MINUS {",
indent(2) + "@Override",
indent(2) + "public int apply(int x, int y) {",
indent(3) + "return x - y;",
indent(2) + '}',
indent(1) + '}',
"}"));
getArgs().setCommentsLevel(CommentsLevel.WARN);
assertThat(getClassNode(TestCls.class))
.code()
.containsLines(1,
"public enum Operation implements IOperation {",
indent(1) + "PLUS {",
indent(2) + "@Override",
indent(2) + "public int apply(int x, int y) {",
indent(3) + "return x + y;",
indent(2) + '}',
indent(1) + "},",
indent(1) + "MINUS {",
indent(2) + "@Override",
indent(2) + "public int apply(int x, int y) {",
indent(3) + "return x - y;",
indent(2) + '}',
indent(1) + '}',
"}");
}
}
@@ -0,0 +1,36 @@
package jadx.tests.integration.generics;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
import jadx.tests.api.IntegrationTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestConstructorGenerics extends IntegrationTest {
@SuppressWarnings({ "MismatchedQueryAndUpdateOfCollection", "RedundantOperationOnEmptyContainer" })
public static class TestCls {
public String test() {
Map<String, String> map = new HashMap<>();
return map.get("test");
}
}
@Test
public void test() {
assertThat(getClassNode(TestCls.class))
.code()
.containsOne("Map<String, String> map = new HashMap<>();");
}
@Test
public void testNoDebug() {
noDebugInfo();
assertThat(getClassNode(TestCls.class))
.code()
.containsOne("return (String) new HashMap().get(\"test\");");
}
}
@@ -19,8 +19,7 @@ public class TestAnonymousClass10 extends IntegrationTest {
public A test() {
Random random = new Random();
int a2 = random.nextInt();
int a3 = a2 + 3;
return new A(this, a2, a3, 4, 5, random.nextDouble()) {
return new A(this, a2, a2 + 3, 4, 5, random.nextDouble()) {
@Override
public void m() {
System.out.println(1);
@@ -0,0 +1,57 @@
package jadx.tests.integration.inner;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
import org.junit.jupiter.api.Test;
import jadx.core.dex.nodes.ClassNode;
import jadx.tests.api.SmaliTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestNestedAnonymousClass extends SmaliTest {
@SuppressWarnings("Convert2Lambda")
public static class TestCls {
public void test() {
use(new Callable<Runnable>() {
@Override
public Runnable call() {
return new Runnable() {
@Override
public void run() {
System.out.println("run");
}
};
}
});
}
public void testLambda() {
use(() -> () -> System.out.println("lambda"));
}
public void use(Callable<Runnable> r) {
}
}
@Test
public void test() {
assertThat(getClassNode(TestCls.class))
.code()
.containsOne("use(new Callable<Runnable>() {")
.containsOne("return new Runnable() {");
}
@Test
public void testSmali() {
getArgs().setRenameFlags(Collections.emptySet());
List<ClassNode> classes = loadFromSmaliFiles();
assertThat(searchCls(classes, "A"))
.code()
.containsOne("use(new Callable<Runnable>() {")
.containsOne("return new Runnable() {");
}
}
@@ -0,0 +1,17 @@
package jadx.tests.integration.loops;
import org.junit.jupiter.api.Test;
import jadx.tests.api.SmaliTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestMultiEntryLoop extends SmaliTest {
@Test
public void test() {
assertThat(getClassNodeFromSmali())
.code()
.containsOne("while (true) {");
}
}
@@ -32,6 +32,6 @@ public class TestNestedLoops2 extends IntegrationTest {
String code = cls.getCode().toString();
assertThat(code, containsOne("for (int i = 0; i < list.size(); i++) {"));
assertThat(code, containsOne("while (j < list.get(i).length()) {"));
assertThat(code, containsOne("while (j < s.length()) {"));
}
}
@@ -12,6 +12,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
public class TestSequentialLoops2 extends IntegrationTest {
@SuppressWarnings({ "unused", "FieldMayBeFinal" })
public static class TestCls {
private static char[] lowercases = new char[] { 'a' };

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