Compare commits

..

77 Commits

Author SHA1 Message Date
Skylot d2bef108f5 chore: update dependencies 2022-03-19 18:52:44 +00:00
Skylot ba8ba504b1 fix(debugger): small improve for jdwp handshake (#1412) 2022-03-19 18:43:34 +00:00
Skylot 481b5abf85 fix(debugger): handle stream end and partial reads (#1412) 2022-03-18 14:19:08 +00:00
Skylot c4e1d9445a fix(gui): reduce threads count on low memory, other tweaks (#1410) 2022-03-17 17:50:28 +00:00
Skylot cb03532b76 fix: allow implicit type cast for array operations (#1407) 2022-03-14 18:47:55 +00:00
Skylot c93e9eea14 fix: improve class names collision detection (#1406) 2022-03-13 12:08:03 +00:00
Skylot 9a67b19973 feat(gui): add zoom in/out actions (#1403) 2022-03-11 13:59:00 +00:00
Skylot 95c75bed1e chore: update gradle and dependencies 2022-03-11 11:34:51 +00:00
Skylot b008568a5c doc: add missing options to readme 2022-03-05 17:15:08 +00:00
Skylot 94fb91cec6 feat: add options for java-convert plugin 2022-03-02 15:40:32 +00:00
Skylot c54dd77f35 fix(gui): resolve NPE and fix code style in BreakpointManager 2022-03-02 12:10:14 +00:00
Jan S 17fbc99f29 feat(gui): dialog for showing exception details and creating an GitHub issue (PR #1399)
* chore(gui): Dialog for showing exception details and creating an GitHub issue
* directly throw test exception
* checkstyle
* minor
* log exception before the dialog is shown
2022-03-01 15:00:22 +00:00
Skylot 21dd17290b fix(gui): download only latest version info for jadx update (#1397) 2022-02-28 18:51:13 +00:00
Skylot dc73fc92be fix(gui): don't use hardcoded color for link component (#1398) 2022-02-28 18:39:51 +00:00
Skylot 592215db66 fix(gui): handle package version in update check (#1397) 2022-02-28 18:39:51 +00:00
Skylot fb318e3bd9 fix(gui): revert contextual keywords to identifiers (#1394) 2022-02-27 15:22:41 +00:00
Skylot 5f3c8816a3 fix: allow zero skips for restore new filled array 2022-02-26 17:29:00 +00:00
Skylot 6016b902c7 test: fix usage of Eclipse compiler 2022-02-26 17:29:00 +00:00
Skylot 5852da1e3d feat: support MethodParameters attribute (#1260) 2022-02-26 10:28:21 +00:00
Skylot 502fd069be test: for source auto check use compiled classes instead runtime 2022-02-26 10:28:20 +00:00
Jan S fad9e7b827 fix(gui): initialize project name with loaded files (shown in Jadx title) (#1386)(PR #1393) 2022-02-26 09:20:58 +00:00
Skylot 35116d0b1a fix: load files also by extension (#1391) 2022-02-25 11:38:44 +00:00
Skylot 3b781e41ad test: allow to pass additional compiler options 2022-02-24 20:52:34 +00:00
Skylot a3e9744364 chore(cli): additional debug messages for java-convert plugin 2022-02-24 20:51:31 +00:00
Skylot 7030daeccd fix(cli): resolve regression in applying '-v' and '-q' options 2022-02-24 19:52:58 +00:00
Jan S e7151ad7b2 fix(gui): IllegalArgumentException when saving project to a different directory than the APK file (#1387)(PR #1388) 2022-02-23 09:27:04 +00:00
Skylot ed2a3c8458 fix: prevent NPE on 'ignore' deobf map file mode 2022-02-22 18:06:01 +00:00
Skylot 779f75cd52 fix(gui): prevent NPE on open preferences without loaded files (#1385) 2022-02-22 18:05:51 +00:00
Skylot 54683e3198 feat: plugin options, add verify checksum option for dex input (#1385) 2022-02-21 19:44:00 +00:00
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
219 changed files with 5957 additions and 1342 deletions
+23 -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
@@ -114,11 +123,20 @@ options:
-q, --quiet - turn off output (set --log-level to QUIET)
--version - print jadx version
-h, --help - print this help
Plugin options (-P<name>=<value>):
1) dex-input (Load .dex and .apk files)
-Pdex-input.verify-checksum - Verify dex file checksum before load, values: [yes, no], default: yes
2) java-convert (Convert .jar and .class files to dex)
-Pjava-convert.mode - Convert mode, values: [dx, d8, both], default: both
-Pjava-convert.d8-desugar - Use desugar in d8, values: [yes, no], default: no
Examples:
jadx -d out classes.dex
jadx --rename-flags "none" classes.dex
jadx --rename-flags "valid, printable" classes.dex
jadx --log-level ERROR app.apk
jadx -Pdex-input.verify-checksum=no app.apk
```
These options also worked on jadx-gui running from command line and override options from preferences dialog
+8 -8
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.3.0'
}
ext.jadxVersion = System.getenv('JADX_VERSION') ?: "dev"
@@ -27,18 +27,17 @@ 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 'ch.qos.logback:logback-classic:1.2.11'
testImplementation 'org.hamcrest:hamcrest-library:2.2'
testImplementation 'org.mockito:mockito-core:4.2.0'
testImplementation 'org.mockito:mockito-core:4.4.0'
testImplementation 'org.assertj:assertj-core:3.22.0'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2'
testImplementation 'org.eclipse.jdt.core.compiler:ecj:4.6.1'
testCompileOnly 'org.jetbrains:annotations:23.0.0'
}
@@ -67,8 +66,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=e5444a57cda4a95f90b0c9446a9e1b47d3d7f69057765bfb54bd4f482542d548
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+1 -1
View File
@@ -11,7 +11,7 @@ dependencies {
runtimeOnly(project(':jadx-plugins:jadx-smali-input'))
implementation 'com.beust:jcommander:1.82'
implementation 'ch.qos.logback:logback-classic:1.2.10'
implementation 'ch.qos.logback:logback-classic:1.2.11'
}
application {
@@ -17,6 +17,11 @@ import com.beust.jcommander.ParameterException;
import com.beust.jcommander.Parameterized;
import jadx.api.JadxDecompiler;
import jadx.api.plugins.JadxPlugin;
import jadx.api.plugins.JadxPluginInfo;
import jadx.api.plugins.JadxPluginManager;
import jadx.api.plugins.options.JadxPluginOptions;
import jadx.api.plugins.options.OptionDescription;
public class JCommanderWrapper<T> {
private final JCommander jc;
@@ -70,40 +75,44 @@ public class JCommanderWrapper<T> {
maxNamesLen = len;
}
}
maxNamesLen += 3;
JadxCLIArgs args = (JadxCLIArgs) jc.getObjects().get(0);
for (Field f : getFields(args.getClass())) {
String name = f.getName();
ParameterDescription p = paramsMap.get(name);
if (p == null) {
if (p == null || p.getParameter().hidden()) {
continue;
}
StringBuilder opt = new StringBuilder();
opt.append(" ").append(p.getNames());
String description = p.getDescription();
addSpaces(opt, maxNamesLen - opt.length() + 3);
addSpaces(opt, maxNamesLen - opt.length());
if (description.contains("\n")) {
String[] lines = description.split("\n");
opt.append("- ").append(lines[0]);
for (int i = 1; i < lines.length; i++) {
opt.append('\n');
addSpaces(opt, maxNamesLen + 5);
addSpaces(opt, maxNamesLen + 2);
opt.append(lines[i]);
}
} else {
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);
}
out.println(appendPluginOptions(maxNamesLen));
out.println();
out.println("Examples:");
out.println(" jadx -d out classes.dex");
out.println(" jadx --rename-flags \"none\" classes.dex");
out.println(" jadx --rename-flags \"valid, printable\" classes.dex");
out.println(" jadx --log-level ERROR app.apk");
out.println(" jadx -Pdex-input.verify-checksum=no app.apk");
}
/**
@@ -145,4 +154,46 @@ public class JCommanderWrapper<T> {
str.append(' ');
}
}
private String appendPluginOptions(int maxNamesLen) {
StringBuilder sb = new StringBuilder();
JadxPluginManager pluginManager = new JadxPluginManager();
pluginManager.load();
int k = 1;
for (JadxPlugin plugin : pluginManager.getAllPlugins()) {
if (plugin instanceof JadxPluginOptions) {
if (appendPlugin(((JadxPluginOptions) plugin), sb, maxNamesLen, k)) {
k++;
}
}
}
if (sb.length() == 0) {
return "";
}
return "\nPlugin options (-P<name>=<value>):" + sb;
}
private boolean appendPlugin(JadxPluginOptions plugin, StringBuilder out, int maxNamesLen, int k) {
List<OptionDescription> descs = plugin.getOptionsDescriptions();
if (descs.isEmpty()) {
return false;
}
JadxPluginInfo pluginInfo = plugin.getPluginInfo();
out.append("\n ").append(k).append(") ");
out.append(pluginInfo.getPluginId()).append(" (").append(pluginInfo.getDescription()).append(") ");
for (OptionDescription desc : descs) {
StringBuilder opt = new StringBuilder();
opt.append(" -P").append(desc.name());
addSpaces(opt, maxNamesLen - opt.length());
opt.append("- ").append(desc.description());
if (!desc.values().isEmpty()) {
opt.append(", values: ").append(desc.values());
}
if (desc.defaultValue() != null) {
opt.append(", default: ").append(desc.defaultValue());
}
out.append("\n").append(opt);
}
return true;
}
}
+39 -10
View File
@@ -7,6 +7,7 @@ import jadx.api.JadxArgs;
import jadx.api.JadxDecompiler;
import jadx.api.impl.NoOpCodeCache;
import jadx.api.impl.SimpleCodeWriter;
import jadx.cli.LogHelper.LogLevelEnum;
import jadx.core.utils.exceptions.JadxArgsValidateException;
import jadx.core.utils.files.FileUtils;
@@ -21,7 +22,7 @@ public class JadxCLI {
LOG.error("Incorrect arguments: {}", e.getMessage());
result = 1;
} catch (Exception e) {
LOG.error("jadx error: {}", e.getMessage(), e);
LOG.error("Process error:", e);
result = 1;
} finally {
FileUtils.deleteTempRootDir();
@@ -32,23 +33,25 @@ 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) {
LogHelper.initLogLevel(cliArgs);
LogHelper.setLogLevelsForLoadingStage();
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 (checkForErrors(jadx)) {
return 1;
}
LogHelper.setLogLevelsForDecompileStage();
if (!SingleClassMode.process(jadx, cliArgs)) {
save(jadx);
}
int errorsCount = jadx.getErrorsCount();
if (errorsCount != 0) {
@@ -60,4 +63,30 @@ public class JadxCLI {
}
return 0;
}
private static boolean checkForErrors(JadxDecompiler jadx) {
if (jadx.getRoot().getClasses().isEmpty()) {
LOG.error("Load failed! No classes for decompile!");
return true;
}
if (jadx.getErrorsCount() > 0) {
LOG.error("Load with errors! Check log for details");
// continue processing
return false;
}
return false;
}
private static void save(JadxDecompiler jadx) {
if (LogHelper.getLogLevel() == 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);
});
// dumb line clear :)
System.out.print(" \r");
}
}
}
@@ -2,12 +2,15 @@ package jadx.cli;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.beust.jcommander.DynamicParameter;
import com.beust.jcommander.IStringConverter;
import com.beust.jcommander.Parameter;
@@ -16,6 +19,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 +43,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 +100,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")
@@ -162,6 +180,9 @@ public class JadxCLIArgs {
@Parameter(names = { "-h", "--help" }, description = "print this help", help = true)
protected boolean printHelp = false;
@DynamicParameter(names = "-P", description = "Plugin options", hidden = true)
protected Map<String, String> pluginOptions = new HashMap<>();
public boolean processArgs(String[] args) {
JCommanderWrapper<JadxCLIArgs> jcw = new JCommanderWrapper<>(this);
return jcw.parse(args) && process(jcw);
@@ -197,7 +218,6 @@ public class JadxCLIArgs {
if (threadsCount <= 0) {
throw new JadxException("Threads count must be positive, got: " + threadsCount);
}
LogHelper.setLogLevelFromArgs(this);
} catch (JadxException e) {
System.err.println("ERROR: " + e.getMessage());
jcw.printUsage();
@@ -215,9 +235,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 +243,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);
@@ -244,6 +265,7 @@ public class JadxCLIArgs {
args.setFsCaseSensitive(fsCaseSensitive);
args.setCommentsLevel(commentsLevel);
args.setUseDxInput(useDx);
args.setPluginOptions(pluginOptions);
return args;
}
@@ -263,6 +285,14 @@ public class JadxCLIArgs {
return outDirRes;
}
public String getSingleClass() {
return singleClass;
}
public String getSingleClassOutput() {
return singleClassOutput;
}
public boolean isSkipResources() {
return skipResources;
}
@@ -323,6 +353,10 @@ public class JadxCLIArgs {
return deobfuscationMapFile;
}
public DeobfuscationMapFileMode getDeobfuscationMapFileMode() {
return deobfuscationMapFileMode;
}
public boolean isDeobfuscationForceSave() {
return deobfuscationForceSave;
}
@@ -383,6 +417,14 @@ public class JadxCLIArgs {
return commentsLevel;
}
public LogHelper.LogLevelEnum getLogLevel() {
return logLevel;
}
public Map<String, String> getPluginOptions() {
return pluginOptions;
}
static class RenameConverter implements IStringConverter<Set<RenameEnum>> {
private final String paramName;
@@ -438,6 +480,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))
+49 -20
View File
@@ -1,5 +1,6 @@
package jadx.cli;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.LoggerFactory;
@@ -32,33 +33,61 @@ public class LogHelper {
}
}
@Nullable("For disable log level control")
private static LogLevelEnum logLevelValue;
public static void setLogLevelFromArgs(JadxCLIArgs args) {
if (isCustomLogConfig()) {
return;
}
LogLevelEnum logLevel = args.logLevel;
if (args.quiet) {
logLevel = LogLevelEnum.QUIET;
} else if (args.verbose) {
logLevel = LogLevelEnum.DEBUG;
}
applyLogLevel(logLevel);
public static void initLogLevel(JadxCLIArgs args) {
logLevelValue = getLogLevelFromArgs(args);
}
public static void applyLogLevel(LogLevelEnum logLevel) {
logLevelValue = logLevel;
private static LogLevelEnum getLogLevelFromArgs(JadxCLIArgs args) {
if (isCustomLogConfig()) {
return null;
}
if (args.quiet) {
return LogLevelEnum.QUIET;
}
if (args.verbose) {
return LogLevelEnum.DEBUG;
}
return args.logLevel;
}
public static void setLogLevelsForLoadingStage() {
if (logLevelValue == null) {
return;
}
if (logLevelValue == LogLevelEnum.PROGRESS) {
// show load errors
LogHelper.applyLogLevel(LogLevelEnum.ERROR);
fixForShowProgress();
return;
}
applyLogLevel(logLevelValue);
}
public static void setLogLevelsForDecompileStage() {
if (logLevelValue == null) {
return;
}
applyLogLevel(logLevelValue);
if (logLevelValue == LogLevelEnum.PROGRESS) {
fixForShowProgress();
}
}
/**
* Show progress: change to 'INFO' for control classes
*/
private static void fixForShowProgress() {
setLevelForClass(JadxCLI.class, Level.INFO);
setLevelForClass(JadxDecompiler.class, Level.INFO);
setLevelForClass(SingleClassMode.class, Level.INFO);
}
private static void applyLogLevel(@NotNull LogLevelEnum logLevel) {
Logger rootLogger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
rootLogger.setLevel(logLevel.getLevel());
if (logLevel != LogLevelEnum.QUIET) {
// show progress for all levels except quiet
setLevelForClass(JadxCLI.class, Level.INFO);
setLevelForClass(JadxDecompiler.class, Level.INFO);
}
}
@Nullable
@@ -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;
}
}
+3 -2
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
@@ -20,7 +20,8 @@ dependencies {
testRuntimeOnly(project(':jadx-plugins:jadx-java-input'))
testRuntimeOnly(project(':jadx-plugins:jadx-raung-input'))
testImplementation('tools.profiler:async-profiler:1.8.3')
testImplementation 'org.eclipse.jdt:ecj:3.29.0'
testImplementation 'tools.profiler:async-profiler:1.8.3'
}
test {
+44 -4
View File
@@ -4,11 +4,14 @@ import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
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 +57,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 +97,13 @@ public class JadxArgs {
private UseKotlinMethodsForVarNames useKotlinMethodsForVarNames = UseKotlinMethodsForVarNames.APPLY;
/**
* Don't save files (can be using for performance testing)
*/
private boolean skipFilesSave = false;
private Map<String, String> pluginOptions = new HashMap<>();
public JadxArgs() {
// use default options
}
@@ -259,12 +270,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 +470,22 @@ public class JadxArgs {
this.useKotlinMethodsForVarNames = useKotlinMethodsForVarNames;
}
public boolean isSkipFilesSave() {
return skipFilesSave;
}
public void setSkipFilesSave(boolean skipFilesSave) {
this.skipFilesSave = skipFilesSave;
}
public Map<String, String> getPluginOptions() {
return pluginOptions;
}
public void setPluginOptions(Map<String, String> pluginOptions) {
this.pluginOptions = pluginOptions;
}
@Override
public String toString() {
return "JadxArgs{" + "inputFiles=" + inputFiles
@@ -463,7 +502,7 @@ public class JadxArgs {
+ ", skipSources=" + skipSources
+ ", deobfuscationOn=" + deobfuscationOn
+ ", deobfuscationMapFile=" + deobfuscationMapFile
+ ", deobfuscationForceSave=" + deobfuscationForceSave
+ ", deobfuscationMapFileMode=" + deobfuscationMapFileMode
+ ", useSourceNameAsClassAlias=" + useSourceNameAsClassAlias
+ ", parseKotlinMetadata=" + parseKotlinMetadata
+ ", useKotlinMethodsForVarNames=" + useKotlinMethodsForVarNames
@@ -480,6 +519,7 @@ public class JadxArgs {
+ ", codeCache=" + codeCache
+ ", codeWriter=" + codeWriterProvider.apply(this).getClass().getSimpleName()
+ ", useDxInput=" + useDxInput
+ ", pluginOptions=" + pluginOptions
+ '}';
}
}
@@ -30,6 +30,7 @@ import jadx.api.plugins.JadxPlugin;
import jadx.api.plugins.JadxPluginManager;
import jadx.api.plugins.input.JadxInputPlugin;
import jadx.api.plugins.input.data.ILoadResult;
import jadx.api.plugins.options.JadxPluginOptions;
import jadx.core.Jadx;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.nodes.LineAttrNode;
@@ -122,12 +123,16 @@ public final class JadxDecompiler implements Closeable {
loadedInputs.clear();
List<Path> inputPaths = Utils.collectionMap(args.getInputFiles(), File::toPath);
List<Path> inputFiles = FileUtils.expandDirs(inputPaths);
long start = System.currentTimeMillis();
for (JadxInputPlugin inputPlugin : pluginManager.getInputPlugins()) {
ILoadResult loadResult = inputPlugin.loadFiles(inputFiles);
if (loadResult != null && !loadResult.isEmpty()) {
loadedInputs.add(loadResult);
}
}
if (LOG.isDebugEnabled()) {
LOG.debug("Loaded using {} inputs plugin in {} ms", loadedInputs.size(), System.currentTimeMillis() - start);
}
}
private void reset() {
@@ -167,6 +172,18 @@ public final class JadxDecompiler implements Closeable {
LOG.debug("Resolved plugins: {}", Utils.collectionMap(pluginManager.getResolvedPlugins(),
p -> p.getPluginInfo().getPluginId()));
}
Map<String, String> pluginOptions = args.getPluginOptions();
if (!pluginOptions.isEmpty()) {
LOG.debug("Applying plugin options: {}", pluginOptions);
for (JadxPluginOptions plugin : pluginManager.getPluginsWithOptions()) {
try {
plugin.setOptions(pluginOptions);
} catch (Exception e) {
String pluginId = plugin.getPluginInfo().getPluginId();
throw new JadxRuntimeException("Failed to apply options for plugin: " + pluginId, e);
}
}
}
}
public void registerPlugin(JadxPlugin plugin) {
@@ -282,6 +299,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 +326,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;
}
}
+9 -1
View File
@@ -178,7 +178,14 @@ public class Jadx {
return passes;
}
public static final String VERSION_DEV = "dev";
private static String version;
public static String getVersion() {
if (version != null) {
return version;
}
try {
ClassLoader classLoader = Jadx.class.getClassLoader();
if (classLoader != null) {
@@ -188,6 +195,7 @@ public class Jadx {
Manifest manifest = new Manifest(is);
String ver = manifest.getMainAttributes().getValue("jadx-version");
if (ver != null) {
version = ver;
return ver;
}
}
@@ -196,6 +204,6 @@ public class Jadx {
} catch (Exception e) {
LOG.error("Can't get manifest file", e);
}
return "dev";
return VERSION_DEV;
}
}
@@ -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);
@@ -13,8 +13,8 @@ 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.AnnotationDefaultAttr;
import jadx.api.plugins.input.data.attributes.types.AnnotationMethodParamsAttr;
import jadx.api.plugins.input.data.attributes.types.AnnotationsAttr;
import jadx.api.plugins.input.data.attributes.types.MethodParamsAttr;
import jadx.core.Consts;
import jadx.core.dex.attributes.IAttributeNode;
import jadx.core.dex.info.FieldInfo;
@@ -48,7 +48,7 @@ public class AnnotationGen {
add(field, code);
}
public void addForParameter(ICodeWriter code, MethodParamsAttr paramsAnnotations, int n) {
public void addForParameter(ICodeWriter code, AnnotationMethodParamsAttr paramsAnnotations, int n) {
List<AnnotationsAttr> paramList = paramsAnnotations.getParamList();
if (n >= paramList.size()) {
return;
@@ -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) {
@@ -590,6 +611,9 @@ public class ClassGen {
return fullName;
}
String shortName = extClsInfo.getAliasShortName();
if (useCls.equals(extClsInfo)) {
return shortName;
}
if (extClsInfo.getPackage().equals("java.lang") && extClsInfo.getParentClass() == null) {
return shortName;
}
@@ -599,6 +623,9 @@ public class ClassGen {
if (extClsInfo.isInner()) {
return expandInnerClassName(useCls, extClsInfo);
}
if (searchCollision(cls.root(), useCls, extClsInfo)) {
return fullName;
}
if (isBothClassesInOneTopClass(useCls, extClsInfo)) {
return shortName;
}
@@ -606,9 +633,6 @@ public class ClassGen {
if (extClsInfo.getPackage().equals(useCls.getPackage()) && !extClsInfo.isInner()) {
return shortName;
}
if (searchCollision(cls.root(), useCls, extClsInfo)) {
return fullName;
}
// ignore classes from default package
if (extClsInfo.isDefaultPackage()) {
return shortName;
@@ -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) {
@@ -16,7 +16,7 @@ import jadx.api.data.annotations.VarDeclareRef;
import jadx.api.plugins.input.data.AccessFlags;
import jadx.api.plugins.input.data.annotations.EncodedValue;
import jadx.api.plugins.input.data.attributes.JadxAttrType;
import jadx.api.plugins.input.data.attributes.types.MethodParamsAttr;
import jadx.api.plugins.input.data.attributes.types.AnnotationMethodParamsAttr;
import jadx.core.Consts;
import jadx.core.Jadx;
import jadx.core.dex.attributes.AFlag;
@@ -195,7 +195,7 @@ public class MethodGen {
}
private void addMethodArguments(ICodeWriter code, List<RegisterArg> args) {
MethodParamsAttr paramsAnnotation = mth.get(JadxAttrType.ANNOTATION_MTH_PARAMETERS);
AnnotationMethodParamsAttr paramsAnnotation = mth.get(JadxAttrType.ANNOTATION_MTH_PARAMETERS);
int i = 0;
Iterator<RegisterArg> it = args.iterator();
while (it.hasNext()) {
@@ -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;
}
}
@@ -12,11 +12,11 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jetbrains.annotations.Nullable;
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;
@@ -37,28 +37,21 @@ public class DeobfPresets {
private final Map<String, String> fldPresetMap = new HashMap<>();
private final Map<String, String> mthPresetMap = new HashMap<>();
@Nullable
public static DeobfPresets build(RootNode root) {
Path deobfMapPath = getPathDeobfMapPath(root);
if (deobfMapPath == null) {
return null;
if (root.getArgs().getDeobfuscationMapFileMode() != DeobfuscationMapFileMode.IGNORE) {
LOG.debug("Deobfuscation map file set to: {}", deobfMapPath);
}
LOG.debug("Deobfuscation map file set to: {}", deobfMapPath);
return new DeobfPresets(deobfMapPath);
}
@Nullable
private static Path getPathDeobfMapPath(RootNode root) {
JadxArgs jadxArgs = root.getArgs();
File deobfMapFile = jadxArgs.getDeobfuscationMapFile();
if (deobfMapFile != null) {
return deobfMapFile.toPath();
}
List<File> inputFiles = jadxArgs.getInputFiles();
if (inputFiles.isEmpty()) {
return null;
}
Path inputFilePath = inputFiles.get(0).toPath().toAbsolutePath();
Path inputFilePath = jadxArgs.getInputFiles().get(0).toPath().toAbsolutePath();
String baseName = FileUtils.getPathBaseName(inputFilePath);
return inputFilePath.getParent().resolve(baseName + ".jobf");
}
@@ -70,9 +63,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 +99,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 +137,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;
@@ -389,11 +389,11 @@ public class InsnDecoder {
return arrLenInsn;
case AGET:
return arrayGet(insn, ArgType.INT_FLOAT);
return arrayGet(insn, ArgType.INT_FLOAT, ArgType.NARROW_NUMBERS_NO_BOOL);
case AGET_BOOLEAN:
return arrayGet(insn, ArgType.BOOLEAN);
case AGET_BYTE:
return arrayGet(insn, ArgType.BYTE);
return arrayGet(insn, ArgType.BYTE, ArgType.NARROW_INTEGRAL);
case AGET_BYTE_BOOLEAN:
return arrayGet(insn, ArgType.BYTE_BOOLEAN);
case AGET_CHAR:
@@ -406,7 +406,7 @@ public class InsnDecoder {
return arrayGet(insn, ArgType.UNKNOWN_OBJECT);
case APUT:
return arrayPut(insn, ArgType.INT_FLOAT);
return arrayPut(insn, ArgType.INT_FLOAT, ArgType.NARROW_NUMBERS_NO_BOOL);
case APUT_BOOLEAN:
return arrayPut(insn, ArgType.BOOLEAN);
case APUT_BYTE:
@@ -607,16 +607,24 @@ public class InsnDecoder {
}
private InsnNode arrayGet(InsnData insn, ArgType argType) {
return arrayGet(insn, argType, argType);
}
private InsnNode arrayGet(InsnData insn, ArgType arrElemType, ArgType resType) {
InsnNode inode = new InsnNode(InsnType.AGET, 2);
inode.setResult(InsnArg.typeImmutableIfKnownReg(insn, 0, argType));
inode.addArg(InsnArg.typeImmutableIfKnownReg(insn, 1, ArgType.array(argType)));
inode.setResult(InsnArg.typeImmutableIfKnownReg(insn, 0, resType));
inode.addArg(InsnArg.typeImmutableIfKnownReg(insn, 1, ArgType.array(arrElemType)));
inode.addArg(InsnArg.reg(insn, 2, ArgType.NARROW_INTEGRAL));
return inode;
}
private InsnNode arrayPut(InsnData insn, ArgType argType) {
return arrayPut(insn, argType, argType);
}
private InsnNode arrayPut(InsnData insn, ArgType arrElemType, ArgType argType) {
InsnNode inode = new InsnNode(InsnType.APUT, 3);
inode.addArg(InsnArg.typeImmutableIfKnownReg(insn, 1, ArgType.array(argType)));
inode.addArg(InsnArg.typeImmutableIfKnownReg(insn, 1, ArgType.array(arrElemType)));
inode.addArg(InsnArg.reg(insn, 2, ArgType.NARROW_INTEGRAL));
inode.addArg(InsnArg.typeImmutableIfKnownReg(insn, 0, argType));
return inode;
@@ -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)
@@ -1,10 +1,10 @@
package jadx.core.dex.visitors;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import org.jetbrains.annotations.Nullable;
@@ -34,7 +34,6 @@ import jadx.core.dex.visitors.shrink.CodeShrinkVisitor;
import jadx.core.utils.InsnList;
import jadx.core.utils.InsnRemover;
import jadx.core.utils.InsnUtils;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxException;
@JadxVisitor(
@@ -87,7 +86,7 @@ public class ReSugarCode extends AbstractVisitor {
}
switch (insn.getType()) {
case NEW_ARRAY:
return processNewArray(mth, (NewArrayNode) insn, instructions, i, remover);
return processNewArray(mth, (NewArrayNode) insn, instructions, remover);
case SWITCH:
return processEnumSwitch(mth, (SwitchInsn) insn);
@@ -100,8 +99,7 @@ public class ReSugarCode extends AbstractVisitor {
/**
* Replace new-array and sequence of array-put to new filled-array instruction.
*/
private static boolean processNewArray(MethodNode mth, NewArrayNode newArrayInsn,
List<InsnNode> instructions, int i, InsnRemover remover) {
private static boolean processNewArray(MethodNode mth, NewArrayNode newArrayInsn, List<InsnNode> instructions, InsnRemover remover) {
Object arrayLenConst = InsnUtils.getConstValueByArg(mth.root(), newArrayInsn.getArg(0));
if (!(arrayLenConst instanceof LiteralArg)) {
return false;
@@ -110,50 +108,81 @@ public class ReSugarCode extends AbstractVisitor {
if (len == 0) {
return false;
}
ArgType arrType = newArrayInsn.getArrayType();
ArgType elemType = arrType.getArrayElement();
boolean allowMissingKeys = arrType.getArrayDimension() == 1 && elemType.isPrimitive();
int minLen = allowMissingKeys ? len / 2 : len;
RegisterArg arrArg = newArrayInsn.getResult();
List<RegisterArg> useList = arrArg.getSVar().getUseList();
if (useList.size() < len) {
if (useList.size() < minLen) {
return false;
}
List<InsnNode> arrPuts = useList.stream()
.map(InsnArg::getParentInsn)
.filter(Objects::nonNull)
.filter(insn -> insn.getType() == InsnType.APUT)
.sorted(Comparator.comparingLong(insn -> {
Object constVal = InsnUtils.getConstValueByArg(mth.root(), insn.getArg(1));
if (constVal instanceof LiteralArg) {
return ((LiteralArg) constVal).getLiteral();
}
return -1; // bad value, put at top to fail fast next check
}))
.collect(Collectors.toList());
if (arrPuts.size() != len) {
// quick check if APUT is used
boolean foundPut = false;
for (RegisterArg registerArg : useList) {
InsnNode parentInsn = registerArg.getParentInsn();
if (parentInsn != null && parentInsn.getType() == InsnType.APUT) {
foundPut = true;
break;
}
}
if (!foundPut) {
return false;
}
// collect put instructions sorted by array index
SortedMap<Long, InsnNode> arrPuts = new TreeMap<>();
for (RegisterArg registerArg : useList) {
InsnNode parentInsn = registerArg.getParentInsn();
if (parentInsn == null || parentInsn.getType() != InsnType.APUT) {
continue;
}
if (!arrArg.sameRegAndSVar(parentInsn.getArg(0))) {
return false;
}
Object constVal = InsnUtils.getConstValueByArg(mth.root(), parentInsn.getArg(1));
if (!(constVal instanceof LiteralArg)) {
return false;
}
long index = ((LiteralArg) constVal).getLiteral();
if (index >= len) {
return false;
}
if (arrPuts.containsKey(index)) {
// stop on index rewrite
break;
}
arrPuts.put(index, parentInsn);
}
if (arrPuts.size() < minLen) {
return false;
}
// expect all puts to be in same block
if (!new HashSet<>(instructions).containsAll(arrPuts)) {
if (!new HashSet<>(instructions).containsAll(arrPuts.values())) {
return false;
}
for (int j = 0; j < len; j++) {
InsnNode insn = arrPuts.get(j);
if (!checkPutInsn(mth, insn, arrArg, j)) {
return false;
}
}
// checks complete, apply
ArgType arrType = newArrayInsn.getArrayType();
InsnNode filledArr = new FilledNewArrayNode(arrType.getArrayElement(), len);
InsnNode filledArr = new FilledNewArrayNode(elemType, len);
filledArr.setResult(arrArg.duplicate());
for (InsnNode put : arrPuts) {
long prevIndex = -1;
for (Map.Entry<Long, InsnNode> entry : arrPuts.entrySet()) {
long index = entry.getKey();
if (index != prevIndex) {
// use zero for missing keys
for (long i = prevIndex + 1; i < index; i++) {
filledArr.addArg(InsnArg.lit(0, elemType));
}
}
InsnNode put = entry.getValue();
filledArr.addArg(replaceConstInArg(mth, put.getArg(2)));
remover.addAndUnbind(put);
prevIndex = index;
}
remover.addAndUnbind(newArrayInsn);
InsnNode lastPut = Utils.last(arrPuts);
InsnNode lastPut = arrPuts.get(arrPuts.lastKey());
int replaceIndex = InsnList.getIndex(instructions, lastPut);
instructions.set(replaceIndex, filledArr);
return true;
@@ -164,28 +193,14 @@ 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();
}
private static boolean checkPutInsn(MethodNode mth, InsnNode insn, RegisterArg arrArg, int putIndex) {
if (insn == null || insn.getType() != InsnType.APUT) {
return false;
}
if (!arrArg.sameRegAndSVar(insn.getArg(0))) {
return false;
}
InsnArg indexArg = insn.getArg(1);
Object value = InsnUtils.getConstValueByArg(mth.root(), indexArg);
if (value instanceof LiteralArg) {
int index = (int) ((LiteralArg) value).getLiteral();
return index == putIndex;
}
return false;
}
private static boolean processEnumSwitch(MethodNode mth, SwitchInsn insn) {
InsnArg arg = insn.getArg(0);
if (!arg.isInsnWrap()) {
@@ -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,13 +3,16 @@ 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;
import org.slf4j.LoggerFactory;
import jadx.api.plugins.input.data.AccessFlags;
import jadx.api.plugins.input.data.ILocalVar;
import jadx.api.plugins.input.data.attributes.JadxAttrType;
import jadx.api.plugins.input.data.attributes.types.MethodParametersAttr;
import jadx.core.Consts;
import jadx.core.deobf.NameMapper;
import jadx.core.dex.attributes.AFlag;
@@ -18,6 +21,7 @@ import jadx.core.dex.attributes.nodes.LocalVarsDebugInfoAttr;
import jadx.core.dex.attributes.nodes.RegDebugInfoAttr;
import jadx.core.dex.instructions.PhiInsn;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.instructions.args.CodeVar;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.Named;
import jadx.core.dex.instructions.args.RegisterArg;
@@ -51,6 +55,7 @@ public class DebugInfoApplyVisitor extends AbstractVisitor {
applyDebugInfo(mth);
mth.remove(AType.LOCAL_VARS_DEBUG_INFO);
}
processMethodParametersAttribute(mth);
checkTypes(mth);
} catch (Exception e) {
mth.addWarnComment("Failed to apply debug info", e);
@@ -73,31 +78,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 +101,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 +138,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;
}
/**
@@ -230,4 +226,31 @@ public class DebugInfoApplyVisitor extends AbstractVisitor {
}
});
}
private void processMethodParametersAttribute(MethodNode mth) {
MethodParametersAttr parametersAttr = mth.get(JadxAttrType.METHOD_PARAMETERS);
if (parametersAttr == null) {
return;
}
try {
List<MethodParametersAttr.Info> params = parametersAttr.getList();
if (params.size() != mth.getMethodInfo().getArgsCount()) {
return;
}
int i = 0;
for (RegisterArg mthArg : mth.getArgRegs()) {
MethodParametersAttr.Info paramInfo = params.get(i++);
String name = paramInfo.getName();
if (NameMapper.isValidAndPrintable(name)) {
CodeVar codeVar = mthArg.getSVar().getCodeVar();
codeVar.setName(name);
if (AccessFlags.hasFlag(paramInfo.getAccFlags(), AccessFlags.FINAL)) {
codeVar.setFinal(true);
}
}
}
} catch (Exception e) {
mth.addWarnComment("Failed to process method parameters attribute: " + parametersAttr.getList(), e);
}
}
}
@@ -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;
@@ -87,6 +87,7 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
this::tryDeduceTypes,
this::trySplitConstInsns,
this::tryToFixIncompatiblePrimitives,
this::tryToForceImmutableTypes,
this::tryInsertAdditionalMove,
this::runMultiVariableSearch,
this::tryRemoveGenerics);
@@ -321,7 +322,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();
}
@@ -835,6 +836,7 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
if (typeInfo.getType().isTypeKnown()) {
return false;
}
boolean assigned = false;
for (ITypeBound bound : typeInfo.getBounds()) {
ArgType boundType = bound.getType();
switch (bound.getBound()) {
@@ -842,6 +844,7 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
if (!boundType.contains(PrimitiveType.BOOLEAN)) {
return false;
}
assigned = true;
break;
case USE:
if (!boundType.canBeAnyNumber()) {
@@ -850,6 +853,9 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
break;
}
}
if (!assigned) {
return false;
}
boolean fixed = false;
for (ITypeBound bound : typeInfo.getBounds()) {
@@ -932,6 +938,36 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
return convertInsn;
}
private boolean tryToForceImmutableTypes(MethodNode mth) {
boolean fixed = false;
for (SSAVar ssaVar : mth.getSVars()) {
ArgType type = ssaVar.getTypeInfo().getType();
if (!type.isTypeKnown() && ssaVar.isTypeImmutable()) {
if (forceImmutableType(ssaVar)) {
fixed = true;
}
}
}
if (!fixed) {
return false;
}
return runTypePropagation(mth);
}
private boolean forceImmutableType(SSAVar ssaVar) {
for (RegisterArg useArg : ssaVar.getUseList()) {
InsnNode parentInsn = useArg.getParentInsn();
if (parentInsn != null) {
InsnType insnType = parentInsn.getType();
if (insnType == InsnType.AGET || insnType == InsnType.APUT) {
ssaVar.setType(ssaVar.getImmutableType());
return true;
}
}
}
return false;
}
private static void assignImmutableTypes(MethodNode mth) {
for (SSAVar ssaVar : mth.getSVars()) {
ArgType immutableType = getSsaImmutableType(ssaVar);
@@ -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) {
@@ -506,7 +513,18 @@ public final class TypeUpdate {
private TypeUpdateResult arrayGetListener(TypeUpdateInfo updateInfo, InsnNode insn, InsnArg arg, ArgType candidateType) {
if (isAssign(insn, arg)) {
return updateTypeChecked(updateInfo, insn.getArg(0), ArgType.array(candidateType));
TypeUpdateResult result = updateTypeChecked(updateInfo, insn.getArg(0), ArgType.array(candidateType));
if (result == REJECT) {
ArgType arrType = insn.getArg(0).getType();
if (arrType.isTypeKnown() && arrType.isArray() && arrType.getArrayElement().isPrimitive()) {
TypeCompareEnum compResult = comparator.compareTypes(candidateType, arrType.getArrayElement());
if (compResult == TypeCompareEnum.WIDER) {
// allow implicit upcast for primitive types (int a = byteArr[n])
return CHANGED;
}
}
}
return result;
}
InsnArg arrArg = insn.getArg(0);
if (arrArg == arg) {
@@ -514,7 +532,18 @@ public final class TypeUpdate {
if (arrayElement == null) {
return REJECT;
}
return updateTypeChecked(updateInfo, insn.getResult(), arrayElement);
TypeUpdateResult result = updateTypeChecked(updateInfo, insn.getResult(), arrayElement);
if (result == REJECT) {
ArgType resType = insn.getResult().getType();
if (resType.isTypeKnown() && resType.isPrimitive()) {
TypeCompareEnum compResult = comparator.compareTypes(resType, arrayElement);
if (compResult == TypeCompareEnum.WIDER) {
// allow implicit upcast for primitive types (int a = byteArr[n])
return CHANGED;
}
}
}
return result;
}
// index argument
return SAME;
@@ -531,10 +560,10 @@ public final class TypeUpdate {
TypeUpdateResult result = updateTypeChecked(updateInfo, putArg, arrayElement);
if (result == REJECT) {
ArgType putType = putArg.getType();
if (putType.isTypeKnown() && !putType.isPrimitive()) {
if (putType.isTypeKnown()) {
TypeCompareEnum compResult = comparator.compareTypes(arrayElement, putType);
if (compResult == TypeCompareEnum.WIDER || compResult == TypeCompareEnum.WIDER_BY_GENERIC) {
// allow wider result (i.e allow put in Object[] any objects)
// allow wider result (i.e. allow put any objects in Object[] or byte in int[])
return CHANGED;
}
}
@@ -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);
}
}
@@ -1,11 +1,14 @@
package jadx.tests.api;
import java.io.Closeable;
import java.io.File;
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.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -16,8 +19,11 @@ 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 java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assumptions;
@@ -32,6 +38,8 @@ import jadx.api.ICodeWriter;
import jadx.api.JadxArgs;
import jadx.api.JadxDecompiler;
import jadx.api.JadxInternalAccess;
import jadx.api.JavaClass;
import jadx.api.args.DeobfuscationMapFileMode;
import jadx.api.data.annotations.InsnCodeOffset;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
@@ -46,9 +54,9 @@ import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
import jadx.core.xmlgen.ResourceStorage;
import jadx.core.xmlgen.entry.ResourceEntry;
import jadx.tests.api.compiler.DynamicCompiler;
import jadx.tests.api.compiler.CompilerOptions;
import jadx.tests.api.compiler.JavaUtils;
import jadx.tests.api.compiler.StaticCompiler;
import jadx.tests.api.compiler.TestCompiler;
import jadx.tests.api.utils.TestUtils;
import static org.apache.commons.lang3.StringUtils.leftPad;
@@ -56,12 +64,12 @@ import static org.apache.commons.lang3.StringUtils.rightPad;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.emptyArray;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
public abstract class IntegrationTest extends TestUtils {
@@ -91,9 +99,9 @@ public abstract class IntegrationTest extends TestUtils {
protected JadxArgs args;
protected boolean compile;
protected boolean withDebugInfo;
protected boolean useEclipseCompiler;
private int targetJavaVersion = 8;
private CompilerOptions compilerOptions;
private boolean saveTestJar = false;
protected Map<Integer, String> resMap = Collections.emptyMap();
@@ -101,9 +109,11 @@ public abstract class IntegrationTest extends TestUtils {
private boolean printLineNumbers;
private boolean printOffsets;
private boolean printDisassemble;
private Boolean useJavaInput = null;
private @Nullable Boolean useJavaInput;
private boolean removeParentClassOnInput;
private DynamicCompiler dynamicCompiler;
private @Nullable TestCompiler sourceCompiler;
private @Nullable TestCompiler decompiledCompiler;
static {
// enable debug checks
@@ -114,10 +124,11 @@ public abstract class IntegrationTest extends TestUtils {
@BeforeEach
public void init() {
this.withDebugInfo = true;
this.compile = true;
this.useEclipseCompiler = false;
this.compilerOptions = new CompilerOptions();
this.resMap = Collections.emptyMap();
this.removeParentClassOnInput = true;
this.useJavaInput = null;
args = new JadxArgs();
args.setOutDir(new File(OUT_DIR));
@@ -126,13 +137,21 @@ public abstract class IntegrationTest extends TestUtils {
args.setSkipResources(true);
args.setFsCaseSensitive(false); // use same value on all systems
args.setCommentsLevel(CommentsLevel.DEBUG);
args.setDeobfuscationOn(false);
args.setDeobfuscationMapFileMode(DeobfuscationMapFileMode.IGNORE);
}
@AfterEach
public void after() {
public void after() throws IOException {
FileUtils.clearTempRootDir();
if (jadxDecompiler != null) {
jadxDecompiler.close();
close(jadxDecompiler);
close(sourceCompiler);
close(decompiledCompiler);
}
private void close(Closeable cloaseble) throws IOException {
if (cloaseble != null) {
cloaseble.close();
}
}
@@ -152,8 +171,22 @@ public abstract class IntegrationTest extends TestUtils {
} catch (Exception e) {
LOG.error("Failed to get class node", e);
fail(e.getMessage());
return null;
}
}
public List<ClassNode> getClassNodes(Class<?>... classes) {
try {
assertThat("Class list is empty", classes, not(emptyArray()));
List<File> srcFiles = Stream.of(classes).map(this::getSourceFileForClass).collect(Collectors.toList());
List<File> clsFiles = compileSourceFiles(srcFiles);
assertThat("Class files list is empty", clsFiles, not(empty()));
return decompileFiles(clsFiles);
} catch (Exception e) {
LOG.error("Failed to get class node", e);
fail(e.getMessage());
return null;
}
return null;
}
public ClassNode getClassNodeFromFiles(List<File> files, String clsName) {
@@ -162,12 +195,30 @@ public abstract class IntegrationTest extends TestUtils {
ClassNode cls = root.resolveClass(clsName);
assertThat("Class not found: " + clsName, cls, notNullValue());
assertThat(clsName, is(cls.getClassInfo().getFullName()));
if (removeParentClassOnInput) {
assertThat(clsName, is(cls.getClassInfo().getFullName()));
} else {
LOG.info("Convert back to top level: {}", cls);
cls.getTopParentClass().decompile(); // keep correct process order
cls.getClassInfo().notInner(root);
cls.updateParentClass();
}
decompileAndCheck(cls);
return cls;
}
public List<ClassNode> decompileFiles(List<File> files) {
jadxDecompiler = loadFiles(files);
List<ClassNode> sortedClsNodes = jadxDecompiler.getDecompileScheduler()
.buildBatches(jadxDecompiler.getClasses())
.stream()
.flatMap(Collection::stream)
.map(JavaClass::getClassNode)
.collect(Collectors.toList());
decompileAndCheck(sortedClsNodes);
return sortedClsNodes;
}
@Nullable
public ClassNode searchCls(List<ClassNode> list, String clsName) {
for (ClassNode cls : list) {
@@ -236,7 +287,7 @@ public abstract class IntegrationTest extends TestUtils {
protected void runChecks(List<ClassNode> clsList) {
clsList.forEach(this::checkCode);
compile(clsList);
compileClassNode(clsList);
clsList.forEach(this::runAutoCheck);
}
@@ -321,7 +372,7 @@ public abstract class IntegrationTest extends TestUtils {
}
private void runAutoCheck(ClassNode cls) {
String clsName = cls.getClassInfo().getFullName();
String clsName = cls.getClassInfo().getRawName().replace('/', '.');
try {
// run 'check' method from original class
if (runSourceAutoCheck(clsName)) {
@@ -338,16 +389,20 @@ public abstract class IntegrationTest extends TestUtils {
}
private boolean runSourceAutoCheck(String clsName) {
if (sourceCompiler == null) {
// no source code (smali case)
return true;
}
Class<?> origCls;
try {
origCls = Class.forName(clsName);
origCls = sourceCompiler.getClass(clsName);
} catch (ClassNotFoundException e) {
// ignore
rethrow("Missing class: " + clsName, e);
return true;
}
Method checkMth;
try {
checkMth = origCls.getMethod(CHECK_METHOD_NAME);
checkMth = sourceCompiler.getMethod(origCls, CHECK_METHOD_NAME, new Class[] {});
} catch (NoSuchMethodException e) {
// ignore
return true;
@@ -369,10 +424,10 @@ public abstract class IntegrationTest extends TestUtils {
public void runDecompiledAutoCheck(ClassNode cls) {
try {
limitExecTime(() -> invoke(cls, "check"));
limitExecTime(() -> invoke(decompiledCompiler, cls.getFullName(), CHECK_METHOD_NAME));
System.out.println("Decompiled check: PASSED");
} catch (Throwable e) {
throw new JadxRuntimeException("Decompiled check failed", e);
rethrow("Decompiled check failed", e);
}
}
@@ -396,8 +451,9 @@ public abstract class IntegrationTest extends TestUtils {
if (e instanceof InvocationTargetException) {
rethrow(msg, e.getCause());
} else if (e instanceof ExecutionException) {
rethrow(e.getMessage(), e.getCause());
rethrow(msg, e.getCause());
} else if (e instanceof AssertionError) {
System.err.println(msg);
throw (AssertionError) e;
} else {
throw new RuntimeException(msg, e);
@@ -414,66 +470,87 @@ public abstract class IntegrationTest extends TestUtils {
return null;
}
void compile(List<ClassNode> clsList) {
void compileClassNode(List<ClassNode> clsList) {
if (!compile) {
return;
}
try {
dynamicCompiler = new DynamicCompiler(clsList);
boolean result = dynamicCompiler.compile();
assertTrue(result, "Compilation failed");
// TODO: eclipse uses files or compilation units providers added in Java 9
compilerOptions.setUseEclipseCompiler(false);
decompiledCompiler = new TestCompiler(compilerOptions);
decompiledCompiler.compileNodes(clsList);
System.out.println("Compilation: PASSED");
} catch (Exception e) {
fail(e);
}
}
public Object invoke(ClassNode cls, String method) throws Exception {
return invoke(cls, method, new Class<?>[0]);
}
public Object invoke(ClassNode cls, String methodName, Class<?>[] types, Object... args) throws Exception {
assertNotNull(dynamicCompiler, "dynamicCompiler not ready");
return dynamicCompiler.invoke(cls, methodName, types, args);
public Object invoke(TestCompiler compiler, String clsFullName, String method) throws Exception {
assertNotNull(compiler, "compiler not ready");
return compiler.invoke(clsFullName, method, new Class<?>[] {}, new Object[] {});
}
private List<File> compileClass(Class<?> cls) throws IOException {
String clsFullName = cls.getName();
String rootClsName;
int end = clsFullName.indexOf('$');
if (end != -1) {
rootClsName = clsFullName.substring(0, end);
} else {
rootClsName = clsFullName;
File sourceFile = getSourceFileForClass(cls);
List<File> clsFiles = compileSourceFiles(Collections.singletonList(sourceFile));
if (removeParentClassOnInput) {
// remove classes which are parents for test class
String clsFullName = cls.getName();
String clsName = clsFullName.substring(clsFullName.lastIndexOf('.') + 1);
clsFiles.removeIf(next -> !next.getName().contains(clsName));
}
return clsFiles;
}
private File getSourceFileForClass(Class<?> cls) {
String clsFullName = cls.getName();
int innerEnd = clsFullName.indexOf('$');
String rootClsName = innerEnd == -1 ? clsFullName : clsFullName.substring(0, innerEnd);
String javaFileName = rootClsName.replace('.', '/') + ".java";
File file = new File(TEST_DIRECTORY, javaFileName);
if (!file.exists()) {
file = new File(TEST_DIRECTORY2, javaFileName);
if (file.exists()) {
return file;
}
assertThat("Test source file not found: " + javaFileName, file.exists(), is(true));
List<File> compileFileList = Collections.singletonList(file);
File file2 = new File(TEST_DIRECTORY2, javaFileName);
if (file2.exists()) {
return file2;
}
throw new JadxRuntimeException("Test source not found for class: " + clsFullName);
}
private List<File> compileSourceFiles(List<File> compileFileList) throws IOException {
Path outTmp = FileUtils.createTempDir("jadx-tmp-classes");
List<File> files = StaticCompiler.compile(compileFileList, outTmp.toFile(), withDebugInfo, useEclipseCompiler, targetJavaVersion);
files.forEach(File::deleteOnExit);
// remove classes which are parents for test class
String clsName = clsFullName.substring(clsFullName.lastIndexOf('.') + 1);
files.removeIf(next -> !next.getName().contains(clsName));
sourceCompiler = new TestCompiler(compilerOptions);
List<File> files = sourceCompiler.compileFiles(compileFileList, outTmp);
if (saveTestJar) {
saveToJar(files, outTmp);
}
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() {
return args;
}
public CompilerOptions getCompilerOptions() {
return compilerOptions;
}
public void setArgs(JadxArgs args) {
this.args = args;
}
@@ -483,16 +560,17 @@ public abstract class IntegrationTest extends TestUtils {
}
protected void noDebugInfo() {
this.withDebugInfo = false;
this.compilerOptions.setIncludeDebugInfo(false);
}
protected void useEclipseCompiler() {
this.useEclipseCompiler = true;
public void useEclipseCompiler() {
Assumptions.assumeTrue(JavaUtils.checkJavaVersion(11), "eclipse compiler library using Java 11");
this.compilerOptions.setUseEclipseCompiler(true);
}
public void useTargetJavaVersion(int version) {
Assumptions.assumeTrue(JavaUtils.checkJavaVersion(version), "skip test for higher java version");
this.targetJavaVersion = version;
this.compilerOptions.setJavaVersion(version);
}
protected void setFallback() {
@@ -506,7 +584,7 @@ public abstract class IntegrationTest extends TestUtils {
protected void enableDeobfuscation() {
args.setDeobfuscationOn(true);
args.setDeobfuscationForceSave(true);
args.setDeobfuscationMapFileMode(DeobfuscationMapFileMode.OVERWRITE);
args.setDeobfuscationMinLength(2);
args.setDeobfuscationMaxLength(64);
}
@@ -532,26 +610,26 @@ public abstract class IntegrationTest extends TestUtils {
this.useJavaInput = false;
}
public void useDexInput(String mode) {
useDexInput();
this.getArgs().getPluginOptions().put("java-convert.mode", mode);
}
protected boolean isJavaInput() {
return Utils.getOrElse(useJavaInput, USE_JAVA_INPUT);
}
// Use only for debug purpose
@Deprecated
protected void outputCFG() {
this.args.setCfgOutput(true);
this.args.setRawCFGOutput(true);
public void keepParentClassOnInput() {
this.removeParentClassOnInput = false;
}
// 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;
}
}
@@ -1,8 +1,9 @@
package jadx.tests.api.compiler;
import java.security.SecureClassLoader;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.io.Closeable;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
@@ -11,19 +12,27 @@ import javax.tools.StandardJavaFileManager;
import static javax.tools.JavaFileObject.Kind;
public class ClassFileManager extends ForwardingJavaFileManager<StandardJavaFileManager> {
public class ClassFileManager extends ForwardingJavaFileManager<StandardJavaFileManager> implements Closeable {
private DynamicClassLoader classLoader;
private final DynamicClassLoader classLoader;
public ClassFileManager(StandardJavaFileManager standardManager) {
super(standardManager);
classLoader = new DynamicClassLoader();
}
public List<JavaFileObject> getJavaFileObjectsFromFiles(List<File> sourceFiles) {
List<JavaFileObject> list = new ArrayList<>();
for (JavaFileObject javaFileObject : fileManager.getJavaFileObjectsFromFiles(sourceFiles)) {
list.add(javaFileObject);
}
return list;
}
@Override
public JavaFileObject getJavaFileForOutput(Location location, String className, Kind kind, FileObject sibling) {
JavaClassObject clsObject = new JavaClassObject(className, kind);
classLoader.getClsMap().put(className, clsObject);
classLoader.add(className, clsObject);
return clsObject;
}
@@ -32,44 +41,7 @@ public class ClassFileManager extends ForwardingJavaFileManager<StandardJavaFile
return classLoader;
}
private class DynamicClassLoader extends SecureClassLoader {
private final Map<String, JavaClassObject> clsMap = new ConcurrentHashMap<>();
private final Map<String, Class<?>> clsCache = new ConcurrentHashMap<>();
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> cls = replaceClass(name);
if (cls != null) {
return cls;
}
return super.findClass(name);
}
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> cls = replaceClass(name);
if (cls != null) {
return cls;
}
return super.loadClass(name);
}
public Class<?> replaceClass(String name) throws ClassNotFoundException {
Class<?> cacheCls = clsCache.get(name);
if (cacheCls != null) {
return cacheCls;
}
JavaClassObject clsObject = clsMap.get(name);
if (clsObject == null) {
return null;
}
byte[] clsBytes = clsObject.getBytes();
Class<?> cls = super.defineClass(name, clsBytes, 0, clsBytes.length);
clsCache.put(name, cls);
return cls;
}
public Map<String, JavaClassObject> getClsMap() {
return clsMap;
}
public DynamicClassLoader getClassLoader() {
return classLoader;
}
}
@@ -0,0 +1,56 @@
package jadx.tests.api.compiler;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class CompilerOptions {
private boolean includeDebugInfo = true;
private boolean useEclipseCompiler = false;
private int javaVersion = 8;
List<String> arguments = Collections.emptyList();
public boolean isIncludeDebugInfo() {
return includeDebugInfo;
}
public void setIncludeDebugInfo(boolean includeDebugInfo) {
this.includeDebugInfo = includeDebugInfo;
}
public boolean isUseEclipseCompiler() {
return useEclipseCompiler;
}
public void setUseEclipseCompiler(boolean useEclipseCompiler) {
this.useEclipseCompiler = useEclipseCompiler;
}
public int getJavaVersion() {
return javaVersion;
}
public void setJavaVersion(int javaVersion) {
this.javaVersion = javaVersion;
}
public List<String> getArguments() {
return Collections.unmodifiableList(arguments);
}
public void addArgument(String argName) {
if (arguments.isEmpty()) {
arguments = new ArrayList<>();
}
arguments.add(argName);
}
public void addArgument(String argName, String argValue) {
if (arguments.isEmpty()) {
arguments = new ArrayList<>();
}
arguments.add(argName);
arguments.add(argValue);
}
}
@@ -0,0 +1,54 @@
package jadx.tests.api.compiler;
import java.security.SecureClassLoader;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.jetbrains.annotations.Nullable;
public class DynamicClassLoader extends SecureClassLoader {
private final Map<String, JavaClassObject> clsMap = new ConcurrentHashMap<>();
private final Map<String, Class<?>> clsCache = new ConcurrentHashMap<>();
public void add(String className, JavaClassObject clsObject) {
this.clsMap.put(className, clsObject);
}
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> cls = replaceClass(name);
if (cls != null) {
return cls;
}
return super.findClass(name);
}
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> cls = replaceClass(name);
if (cls != null) {
return cls;
}
return super.loadClass(name);
}
@Nullable
public Class<?> replaceClass(String name) {
Class<?> cacheCls = clsCache.get(name);
if (cacheCls != null) {
return cacheCls;
}
JavaClassObject clsObject = clsMap.get(name);
if (clsObject == null) {
return null;
}
byte[] clsBytes = clsObject.getBytes();
Class<?> cls = super.defineClass(name, clsBytes, 0, clsBytes.length);
clsCache.put(name, cls);
return cls;
}
public Collection<? extends JavaClassObject> getClassObjects() {
return clsMap.values();
}
}
@@ -1,93 +0,0 @@
package jadx.tests.api.compiler;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.ToolProvider;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.dex.nodes.ClassNode;
import jadx.tests.api.IntegrationTest;
import static javax.tools.JavaCompiler.CompilationTask;
import static org.junit.jupiter.api.Assertions.assertNotNull;
public class DynamicCompiler {
private static final Logger LOG = LoggerFactory.getLogger(DynamicCompiler.class);
private final List<ClassNode> clsNodeList;
private JavaFileManager fileManager;
public DynamicCompiler(List<ClassNode> clsNodeList) {
this.clsNodeList = clsNodeList;
}
public boolean compile() {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
if (compiler == null) {
LOG.error("Can not find compiler, please use JDK instead");
return false;
}
fileManager = new ClassFileManager(compiler.getStandardFileManager(null, null, null));
List<JavaFileObject> jFiles = new ArrayList<>(clsNodeList.size());
for (ClassNode clsNode : clsNodeList) {
jFiles.add(new CharSequenceJavaFileObject(clsNode.getFullName(), clsNode.getCode().toString()));
}
CompilationTask compilerTask = compiler.getTask(null, fileManager, null, null, null, jFiles);
return Boolean.TRUE.equals(compilerTask.call());
}
private ClassLoader getClassLoader() {
return fileManager.getClassLoader(null);
}
public Object makeInstance(ClassNode cls) throws Exception {
String fullName = cls.getFullName();
return getClassLoader().loadClass(fullName).getConstructor().newInstance();
}
@NotNull
public Method getMethod(Object inst, String methodName, Class<?>[] types) throws Exception {
for (Class<?> type : types) {
checkType(type);
}
return inst.getClass().getMethod(methodName, types);
}
public Object invoke(ClassNode cls, String methodName, Class<?>[] types, Object[] args) {
try {
Object inst = makeInstance(cls);
Method reflMth = getMethod(inst, methodName, types);
assertNotNull(reflMth, "Failed to get method " + methodName + '(' + Arrays.toString(types) + ')');
return reflMth.invoke(inst, args);
} catch (Throwable e) {
IntegrationTest.rethrow("Invoke error", e);
return null;
}
}
private Class<?> checkType(Class<?> type) throws ClassNotFoundException {
if (type.isPrimitive()) {
return type;
}
if (type.isArray()) {
return checkType(type.getComponentType());
}
Class<?> decompiledCls = getClassLoader().loadClass(type.getName());
if (type != decompiledCls) {
throw new IllegalArgumentException("Internal test class cannot be used in method invoke");
}
return decompiledCls;
}
}
@@ -0,0 +1,18 @@
package jadx.tests.api.compiler;
import javax.tools.JavaCompiler;
public class EclipseCompilerUtils {
public static JavaCompiler newInstance() {
if (!JavaUtils.checkJavaVersion(11)) {
throw new IllegalArgumentException("Eclipse compiler build with Java 11");
}
try {
Class<?> ecjCls = Class.forName("org.eclipse.jdt.internal.compiler.tool.EclipseCompiler");
return (JavaCompiler) ecjCls.getConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException("Failed to init Eclipse compiler", e);
}
}
}
@@ -1,7 +1,6 @@
package jadx.tests.api.compiler;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
@@ -9,10 +8,17 @@ import javax.tools.SimpleJavaFileObject;
public class JavaClassObject extends SimpleJavaFileObject {
protected final ByteArrayOutputStream bos = new ByteArrayOutputStream();
private final String name;
private final ByteArrayOutputStream bos = new ByteArrayOutputStream();
public JavaClassObject(String name, Kind kind) {
super(URI.create("string:///" + name.replace('.', '/') + kind.extension), kind);
this.name = name;
}
@Override
public String getName() {
return name;
}
public byte[] getBytes() {
@@ -20,7 +26,7 @@ public class JavaClassObject extends SimpleJavaFileObject {
}
@Override
public OutputStream openOutputStream() throws IOException {
public OutputStream openOutputStream() {
return bos;
}
}
@@ -10,6 +10,10 @@ public class JavaUtils {
public static final int JAVA_VERSION_INT = getJavaVersionInt();
public static boolean checkJavaVersion(int requiredVersion) {
return JAVA_VERSION_INT >= requiredVersion;
}
private static int getJavaVersionInt() {
String javaSpecVerStr = SystemUtils.JAVA_SPECIFICATION_VERSION;
if (javaSpecVerStr == null) {
@@ -21,8 +25,4 @@ public class JavaUtils {
}
return Integer.parseInt(javaSpecVerStr);
}
public static boolean checkJavaVersion(int requiredVersion) {
return JAVA_VERSION_INT >= requiredVersion;
}
}
@@ -1,102 +0,0 @@
package jadx.tests.api.compiler;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaCompiler;
import javax.tools.JavaCompiler.CompilationTask;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import org.eclipse.jdt.internal.compiler.tool.EclipseCompiler;
import jadx.core.utils.files.FileUtils;
public class StaticCompiler {
public static List<File> compile(List<File> files, File outDir, boolean includeDebugInfo,
boolean useEclipseCompiler, int javaVersion) throws IOException {
if (!JavaUtils.checkJavaVersion(javaVersion)) {
throw new IllegalArgumentException("Current java version not meet requirement: "
+ "current: " + JavaUtils.JAVA_VERSION_INT + ", required: " + javaVersion);
}
JavaCompiler compiler;
if (useEclipseCompiler) {
compiler = new EclipseCompiler();
} else {
compiler = ToolProvider.getSystemJavaCompiler();
if (compiler == null) {
throw new IllegalStateException("Can not find compiler, please use JDK instead");
}
}
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjectsFromFiles(files);
StaticFileManager staticFileManager = new StaticFileManager(fileManager, outDir);
List<String> options = new ArrayList<>();
options.add(includeDebugInfo ? "-g" : "-g:none");
String javaVerStr = javaVersion <= 8 ? "1." + javaVersion : Integer.toString(javaVersion);
options.add("-source");
options.add(javaVerStr);
options.add("-target");
options.add(javaVerStr);
CompilationTask task = compiler.getTask(null, staticFileManager, null, options, null, compilationUnits);
Boolean result = task.call();
fileManager.close();
if (Boolean.TRUE.equals(result)) {
return staticFileManager.outputFiles();
}
return Collections.emptyList();
}
private static class StaticFileManager extends ForwardingJavaFileManager<StandardJavaFileManager> {
private final List<File> files = new ArrayList<>();
private final File outDir;
protected StaticFileManager(StandardJavaFileManager fileManager, File outDir) {
super(fileManager);
this.outDir = outDir;
}
@Override
public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) {
if (kind == JavaFileObject.Kind.CLASS) {
File file = new File(outDir, className.replace('.', '/') + ".class");
files.add(file);
return new ClassFileObject(file, kind);
}
throw new UnsupportedOperationException("Can't save location with kind: " + kind);
}
public List<File> outputFiles() {
return files;
}
}
private static class ClassFileObject extends SimpleJavaFileObject {
private final File file;
protected ClassFileObject(File file, Kind kind) {
super(file.toURI(), kind);
this.file = file;
}
@Override
public OutputStream openOutputStream() throws IOException {
FileUtils.makeDirsForFile(file);
return new FileOutputStream(file);
}
}
}
@@ -4,11 +4,11 @@ import java.net.URI;
import javax.tools.SimpleJavaFileObject;
public class CharSequenceJavaFileObject extends SimpleJavaFileObject {
public class StringJavaFileObject extends SimpleJavaFileObject {
private CharSequence content;
private final String content;
public CharSequenceJavaFileObject(String className, CharSequence content) {
public StringJavaFileObject(String className, String content) {
super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
this.content = content;
}

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