Compare commits

..

105 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
Skylot d22db30166 fix: use secure xml parser for process manifest 2022-01-20 11:17:12 +00:00
Skylot 6db61e7a59 chore: update dependencies 2022-01-20 10:23:49 +00:00
Skylot 86582de521 feat: use kotlin intrinsic methods for variables rename (#1207) 2022-01-19 17:30:04 +00:00
Skylot a7c63c2eb3 fix: handle method override with several bases (#1234) 2022-01-18 18:27:09 +00:00
Skylot 081a0e21ee fix: precalculate class deps for inline methods (#1339) 2022-01-17 14:38:38 +00:00
Skylot 9ac9c05265 fix: simplify cascading casts (#1336) 2022-01-15 16:31:18 +00:00
Skylot b7daf79b26 fix: add explicit type for non-int constants (#1336) 2022-01-15 14:11:44 +00:00
Skylot b67a3561a4 build: add CodeQL analysis 2022-01-13 22:37:36 +03:00
Skylot 52ac6dbbaf docs: add security.md 2022-01-13 16:45:32 +00:00
Skylot 72381ad8f3 fix: correct literal negate for double and float (#1334) 2022-01-13 14:00:53 +00:00
Skylot 6a065c46f4 chore: update dependencies 2022-01-13 12:12:15 +00:00
Skylot 092d0d7e67 fix(gui): reduce tree focus switching 2022-01-12 19:57:38 +03:00
Skylot 5ca7285558 fix(gui): correct handling for tree row click (#1324) 2022-01-12 16:57:25 +00:00
Skylot 7576f9cd5e fix: wrap negative literals before cast (#1327) 2022-01-12 17:31:40 +03:00
Skylot 46b5725d98 refactor(test): replace inputs with test profiles 2022-01-12 17:31:37 +03:00
Jan S 72542fa6f9 fix(gui): processing threads spinner initialization (#1331)(PR #1332)
* fix: processing threads spinner initialization (#1331)
* fix: processing threads spinner initialization (#1331)
2022-01-12 14:23:07 +00:00
demonlol a250d0461b fix(dbg): support multiple main <action> and <activity-alias> tags (#1322)(PR #1323)
* fix(dgb): support multiple main <action> and <activity-alias> tags in manifest
* Update jadx-gui/src/main/java/jadx/gui/device/debugger/DbgUtils.java
2022-01-02 20:09:24 +03:00
Skylot c7795bfc48 fix: improve anonymous class inline (#523) 2021-12-26 13:06:49 +00:00
Skylot 5de46b7e40 chore: update gradle and dependencies 2021-12-24 12:53:30 +00:00
Skylot 99c70872c1 fix: use debug line numbers only at fixed offsets (#1315) 2021-12-22 22:55:14 +03:00
Skylot 3566669303 chore: update lgtm config 2021-12-22 12:24:01 +00:00
Skylot 4557d05256 fix: use correct type for anonymous class instance (#597) 2021-12-21 17:47:52 +00:00
Skylot fa421d165e build: disable missing warnings from javadoc 2021-12-21 12:52:52 +00:00
Skylot ecf20020d7 chore: cache current working dir in static field, other minor changes 2021-12-20 19:25:07 +00:00
Skylot ae85af61c7 fix: skip input file name checks by zip name validator (#1310) 2021-12-20 18:55:28 +00:00
Skylot 659bbbf4fb fix: correct usage of Path.getParent() 2021-12-20 16:48:50 +00:00
Jan S 427e2dddc4 fix: use relative file paths in .jadx project file (#1312) (PR #1313)
* chore: use relative file paths in .jadx project file (#1312)
* code beautified
* requested changes
2021-12-20 13:52:51 +00:00
skylot d47483f957 docs: use jadx as a library 2021-12-19 20:36:58 +00:00
276 changed files with 7977 additions and 1876 deletions
+41
View File
@@ -0,0 +1,41 @@
name: "CodeQL"
on:
push:
branches: [master]
pull_request:
# The branches below must be a subset of the branches above
branches: [master]
schedule:
- cron: '0 9 * * 5'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: ['java']
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
queries: +security-extended
languages: ${{ matrix.language }}
# Don't build tests in jadx-core also skip tests execution and checkstyle tasks
- run: |
./gradlew clean build -x checkstyleTest -x checkstyleMain -x test -x ':jadx-core:testClasses'
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
+2
View File
@@ -34,3 +34,5 @@ jadx-output/
*.cfg
*.orig
quark.json
cliff.toml
+28 -6
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
@@ -51,6 +54,9 @@ For windows you can download it from [oracle.com](https://www.oracle.com/java/te
brew install jadx
```
### Use jadx as a library
You can use jadx in your java projects, check details on [wiki page](https://github.com/skylot/jadx/wiki/Use-jadx-as-a-library)
### Build from source
JDK 8 or higher must be installed:
```
@@ -73,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
@@ -90,9 +97,15 @@ 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
--rename-flags - fix options (comma-separated list of):
'case' - fix case sensitivity issues (according to --fs-case-sensitive option),
'valid' - rename java identifiers to make them valid,
@@ -104,17 +117,26 @@ options:
--raw-cfg - save methods control flow graph (use raw instructions)
-f, --fallback - make simple dump (using goto instead of 'if', 'for', etc)
--use-dx - use dx/d8 to convert java bytecode
--comments-level - set code comments level, values: none, user_only, error, warn, info, debug, default: info
--comments-level - set code comments level, values: error, warn, info, debug, user-only, none, default: info
--log-level - set log level, values: quiet, progress, error, warn, info, debug, default: progress
-v, --verbose - verbose output (set --log-level to DEBUG)
-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
+7
View File
@@ -0,0 +1,7 @@
# Security Policy
## Reporting a Vulnerability
To report a security issue, please email `skylot@gmail.com` with a description of the issue, the steps you took to create the issue, affected versions, and, if known, mitigations for the issue.
We will check and respond within 3 working days. If the issue is confirmed as a vulnerability, we will apply required mitigations at the next release.
This project follows a 90 day disclosure timeline.
+9 -9
View File
@@ -1,6 +1,6 @@
plugins {
id 'com.github.ben-manes.versions' version '0.39.0'
id 'com.diffplug.spotless' version '6.0.2'
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.32'
implementation 'org.slf4j:slf4j-api:1.7.36'
compileOnly 'org.jetbrains:annotations:23.0.0'
testImplementation 'ch.qos.logback:logback-classic:1.2.8'
testImplementation 'ch.qos.logback:logback-classic:1.2.11'
testImplementation 'org.hamcrest:hamcrest-library:2.2'
testImplementation 'org.mockito:mockito-core:4.1.0'
testImplementation 'org.assertj:assertj-core:3.21.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)
@@ -73,4 +73,6 @@ javadoc {
if (JavaVersion.current().isJava9Compatible()) {
options.addBooleanOption('html5', true)
}
// disable 'missing' warnings
options.addStringOption('Xdoclint:all,-missing', '-quiet')
}
Binary file not shown.
+2 -2
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=9afb3ca688fc12c761a0e9e4321e4d24e977a4a8916c8a768b1fe05ddb4d6b66
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.1-bin.zip
distributionSha256Sum=e5444a57cda4a95f90b0c9446a9e1b47d3d7f69057765bfb54bd4f482542d548
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+2 -2
View File
@@ -10,8 +10,8 @@ dependencies {
runtimeOnly(project(':jadx-plugins:jadx-java-convert'))
runtimeOnly(project(':jadx-plugins:jadx-smali-input'))
implementation 'com.beust:jcommander:1.81'
implementation 'ch.qos.logback:logback-classic:1.2.8'
implementation 'com.beust:jcommander:1.82'
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,19 +2,24 @@ 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;
import jadx.api.CommentsLevel;
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;
@@ -38,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";
@@ -92,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")
@@ -101,6 +120,13 @@ public class JadxCLIArgs {
@Parameter(names = { "--deobf-parse-kotlin-metadata" }, description = "parse kotlin metadata to class and package names")
protected boolean deobfuscationParseKotlinMetadata = false;
@Parameter(
names = { "--use-kotlin-methods-for-var-names" },
description = "use kotlin intrinsic methods to rename variables, values: disable, apply, apply-and-hide",
converter = UseKotlinMethodsForVarNamesConverter.class
)
protected UseKotlinMethodsForVarNames useKotlinMethodsForVarNames = UseKotlinMethodsForVarNames.APPLY;
@Parameter(
names = { "--rename-flags" },
description = "fix options (comma-separated list of):"
@@ -130,7 +156,7 @@ public class JadxCLIArgs {
@Parameter(
names = { "--comments-level" },
description = "set code comments level, values: error, warn, info, debug, user_only, none",
description = "set code comments level, values: error, warn, info, debug, user-only, none",
converter = CommentsLevelConverter.class
)
protected CommentsLevel commentsLevel = CommentsLevel.INFO;
@@ -154,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);
@@ -189,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();
@@ -207,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);
@@ -218,11 +243,16 @@ 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);
args.setParseKotlinMetadata(deobfuscationParseKotlinMetadata);
args.setUseKotlinMethodsForVarNames(useKotlinMethodsForVarNames);
args.setEscapeUnicode(escapeUnicode);
args.setRespectBytecodeAccModifiers(respectBytecodeAccessModifiers);
args.setExportAsGradleProject(exportAsGradleProject);
@@ -235,6 +265,7 @@ public class JadxCLIArgs {
args.setFsCaseSensitive(fsCaseSensitive);
args.setCommentsLevel(commentsLevel);
args.setUseDxInput(useDx);
args.setPluginOptions(pluginOptions);
return args;
}
@@ -254,6 +285,14 @@ public class JadxCLIArgs {
return outDirRes;
}
public String getSingleClass() {
return singleClass;
}
public String getSingleClassOutput() {
return singleClassOutput;
}
public boolean isSkipResources() {
return skipResources;
}
@@ -314,6 +353,10 @@ public class JadxCLIArgs {
return deobfuscationMapFile;
}
public DeobfuscationMapFileMode getDeobfuscationMapFileMode() {
return deobfuscationMapFileMode;
}
public boolean isDeobfuscationForceSave() {
return deobfuscationForceSave;
}
@@ -326,6 +369,10 @@ public class JadxCLIArgs {
return deobfuscationParseKotlinMetadata;
}
public UseKotlinMethodsForVarNames getUseKotlinMethodsForVarNames() {
return useKotlinMethodsForVarNames;
}
public boolean isEscapeUnicode() {
return escapeUnicode;
}
@@ -370,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;
@@ -412,9 +467,35 @@ public class JadxCLIArgs {
}
}
public static class UseKotlinMethodsForVarNamesConverter implements IStringConverter<UseKotlinMethodsForVarNames> {
@Override
public UseKotlinMethodsForVarNames convert(String value) {
try {
return UseKotlinMethodsForVarNames.valueOf(value.replace('-', '_').toUpperCase());
} catch (Exception e) {
throw new IllegalArgumentException(
'\'' + value + "' is unknown, possible values are: "
+ JadxCLIArgs.enumValuesString(CommentsLevel.values()));
}
}
}
public static class 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().toLowerCase(Locale.ROOT))
.map(v -> v.name().replace('_', '-').toLowerCase(Locale.ROOT))
.collect(Collectors.joining(", "));
}
}
+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 {
+60 -5
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;
@@ -87,6 +91,19 @@ public class JadxArgs {
private boolean useDxInput = false;
public enum UseKotlinMethodsForVarNames {
DISABLE, APPLY, APPLY_AND_HIDE
}
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
}
@@ -138,7 +155,7 @@ public class JadxArgs {
}
public void setThreadsCount(int threadsCount) {
this.threadsCount = threadsCount;
this.threadsCount = Math.max(1, threadsCount); // make sure threadsCount >= 1
}
public boolean isCfgOutput() {
@@ -253,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() {
@@ -433,6 +462,30 @@ public class JadxArgs {
this.useDxInput = useDxInput;
}
public UseKotlinMethodsForVarNames getUseKotlinMethodsForVarNames() {
return useKotlinMethodsForVarNames;
}
public void setUseKotlinMethodsForVarNames(UseKotlinMethodsForVarNames useKotlinMethodsForVarNames) {
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
@@ -449,9 +502,10 @@ public class JadxArgs {
+ ", skipSources=" + skipSources
+ ", deobfuscationOn=" + deobfuscationOn
+ ", deobfuscationMapFile=" + deobfuscationMapFile
+ ", deobfuscationForceSave=" + deobfuscationForceSave
+ ", deobfuscationMapFileMode=" + deobfuscationMapFileMode
+ ", useSourceNameAsClassAlias=" + useSourceNameAsClassAlias
+ ", parseKotlinMetadata=" + parseKotlinMetadata
+ ", useKotlinMethodsForVarNames=" + useKotlinMethodsForVarNames
+ ", deobfuscationMinLength=" + deobfuscationMinLength
+ ", deobfuscationMaxLength=" + deobfuscationMaxLength
+ ", escapeUnicode=" + escapeUnicode
@@ -465,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;
@@ -37,6 +37,10 @@ public class ResourceFile {
private ZipRef zipRef;
private String deobfName;
public static ResourceFile createResourceFile(JadxDecompiler decompiler, File file, ResourceType type) {
return new ResourceFile(decompiler, file.getAbsolutePath(), type);
}
public static ResourceFile createResourceFile(JadxDecompiler decompiler, String name, ResourceType type) {
if (!ZipSecurity.isValidZipEntryName(name)) {
return null;
@@ -41,7 +41,7 @@ public enum ResourceType {
}
public static ResourceType getFileType(String fileName) {
if (fileName.matches("[^/]+/resources.pb")) {
if (fileName.endsWith("/resources.pb")) {
return ARSC;
}
int dot = fileName.lastIndexOf('.');
@@ -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);
}
@@ -145,16 +146,8 @@ public final class ResourcesLoader {
return null;
});
} else {
addResourceFile(list, file);
}
}
private void addResourceFile(List<ResourceFile> list, File file) {
String name = file.getAbsolutePath();
ResourceType type = ResourceType.getFileType(name);
ResourceFile rf = ResourceFile.createResourceFile(jadxRef, name, type);
if (rf != null) {
list.add(rf);
ResourceType type = ResourceType.getFileType(file.getAbsolutePath());
list.add(ResourceFile.createResourceFile(jadxRef, file, type));
}
}
@@ -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;
}
}
+17 -1
View File
@@ -12,6 +12,7 @@ import org.slf4j.LoggerFactory;
import jadx.api.CommentsLevel;
import jadx.api.JadxArgs;
import jadx.core.dex.visitors.AnonymousClassVisitor;
import jadx.core.dex.visitors.AttachCommentsVisitor;
import jadx.core.dex.visitors.AttachMethodDetails;
import jadx.core.dex.visitors.AttachTryCatchVisitor;
@@ -37,6 +38,7 @@ import jadx.core.dex.visitors.OverrideMethodVisitor;
import jadx.core.dex.visitors.PrepareForCodeGen;
import jadx.core.dex.visitors.ProcessAnonymous;
import jadx.core.dex.visitors.ProcessInstructionsVisitor;
import jadx.core.dex.visitors.ProcessMethodsForInline;
import jadx.core.dex.visitors.ReSugarCode;
import jadx.core.dex.visitors.ShadowFieldVisitor;
import jadx.core.dex.visitors.SignatureProcessor;
@@ -46,6 +48,7 @@ import jadx.core.dex.visitors.blocks.BlockSplitter;
import jadx.core.dex.visitors.debuginfo.DebugInfoApplyVisitor;
import jadx.core.dex.visitors.debuginfo.DebugInfoAttachVisitor;
import jadx.core.dex.visitors.finaly.MarkFinallyVisitor;
import jadx.core.dex.visitors.kotlin.ProcessKotlinInternals;
import jadx.core.dex.visitors.regions.CheckRegions;
import jadx.core.dex.visitors.regions.CleanRegions;
import jadx.core.dex.visitors.regions.IfRegionVisitor;
@@ -88,6 +91,7 @@ public class Jadx {
passes.add(new RenameVisitor());
passes.add(new UsageInfoVisitor());
passes.add(new ProcessAnonymous());
passes.add(new ProcessMethodsForInline());
return passes;
}
@@ -128,6 +132,9 @@ public class Jadx {
if (args.isDebugInfo()) {
passes.add(new DebugInfoApplyVisitor());
}
if (args.getUseKotlinMethodsForVarNames() != JadxArgs.UseKotlinMethodsForVarNames.DISABLE) {
passes.add(new ProcessKotlinInternals());
}
passes.add(new CodeRenameVisitor());
if (args.isInlineMethods()) {
passes.add(new InlineMethods());
@@ -135,6 +142,7 @@ public class Jadx {
passes.add(new GenericTypesVisitor());
passes.add(new ShadowFieldVisitor());
passes.add(new DeboxingVisitor());
passes.add(new AnonymousClassVisitor());
passes.add(new ModVisitor());
passes.add(new CodeShrinkVisitor());
passes.add(new ReSugarCode());
@@ -170,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) {
@@ -180,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;
}
}
@@ -188,6 +204,6 @@ public class Jadx {
} catch (Exception e) {
LOG.error("Can't get manifest file", e);
}
return "dev";
return VERSION_DEV;
}
}
@@ -1,10 +1,5 @@
package jadx.core;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -39,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);
@@ -94,21 +89,13 @@ public final class ProcessClass {
return generateCode(topParentClass);
}
try {
Set<ClassNode> useIn = new HashSet<>(cls.getUseIn());
List<ClassNode> usedInDeps = new ArrayList<>();
for (ClassNode depCls : cls.getDependencies()) {
if (useIn.contains(depCls)) {
// postpone to resolve cross dependencies
usedInDeps.add(depCls);
} else {
process(depCls, false);
}
process(depCls, false);
}
if (!usedInDeps.isEmpty()) {
// process current class before its usage
if (!cls.getCodegenDeps().isEmpty()) {
process(cls, false);
for (ClassNode depCls : usedInDeps) {
process(depCls, false);
for (ClassNode codegenDep : cls.getCodegenDeps()) {
process(codegenDep, false);
}
}
ICodeInfo code = process(cls, true);
@@ -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;
@@ -15,7 +15,6 @@ import jadx.api.data.annotations.InsnCodeOffset;
import jadx.api.data.annotations.VarDeclareRef;
import jadx.api.data.annotations.VarRef;
import jadx.api.plugins.input.data.MethodHandleType;
import jadx.core.deobf.NameMapper;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.FieldReplaceAttr;
@@ -112,7 +111,7 @@ public class InsnGen {
}
code.add(mgen.getNameGen().useArg(reg));
} else if (arg.isLiteral()) {
code.add(lit((LiteralArg) arg));
addLiteralArg(code, (LiteralArg) arg, flags);
} else if (arg.isInsnWrap()) {
addWrappedArg(code, (InsnWrapArg) arg, flags);
} else if (arg.isNamed()) {
@@ -122,6 +121,15 @@ public class InsnGen {
}
}
private void addLiteralArg(ICodeWriter code, LiteralArg litArg, Set<Flags> flags) {
String literalStr = lit(litArg);
if (!flags.contains(Flags.BODY_ONLY_NOWRAP) && literalStr.startsWith("-")) {
code.add('(').add(literalStr).add(')');
} else {
code.add(literalStr);
}
}
private void addWrappedArg(ICodeWriter code, InsnWrapArg arg, Set<Flags> flags) throws CodegenException {
InsnNode wrapInsn = arg.getWrapInsn();
if (wrapInsn.contains(AFlag.FORCE_ASSIGN_INLINE)) {
@@ -164,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) {
@@ -202,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);
}
@@ -711,20 +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());
}
cls.add(AFlag.DONT_GENERATE);
ArgType parent;
if (cls.getInterfaces().size() == 1) {
parent = cls.getInterfaces().get(0);
} else {
parent = cls.getSuperClass();
}
ArgType parent = cls.get(AType.ANONYMOUS_CLASS).getBaseType();
// hide empty anonymous constructors
for (MethodNode ctor : cls.getMethods()) {
if (ctor.contains(AFlag.ANONYMOUS_CONSTRUCTOR)
@@ -732,14 +733,22 @@ public class InsnGen {
ctor.add(AFlag.DONT_GENERATE);
}
}
code.add("new ");
if (parent == null) {
code.add("Object");
} else {
useClass(code, parent);
}
useClass(code, parent);
MethodNode callMth = mth.root().resolveMethod(insn.getCallMth());
if (callMth != null) {
// copy var names
List<RegisterArg> mthArgs = callMth.getArgRegs();
int argsCount = Math.min(insn.getArgsCount(), mthArgs.size());
for (int i = 0; i < argsCount; i++) {
InsnArg arg = insn.getArg(i);
if (arg.isRegister()) {
RegisterArg mthArg = mthArgs.get(i);
RegisterArg insnArg = (RegisterArg) arg;
mthArg.getSVar().setCodeVar(insnArg.getSVar().getCodeVar());
}
}
}
generateMethodArguments(code, insn, 0, callMth);
code.add(' ');
@@ -755,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) {
@@ -1029,7 +1038,6 @@ public class InsnGen {
} else {
condGen.wrap(code, insn.getCondition());
code.add(" ? ");
addCastIfNeeded(code, first, second);
addArg(code, first, false);
code.add(" : ");
addArg(code, second, false);
@@ -1039,33 +1047,6 @@ public class InsnGen {
}
}
private void addCastIfNeeded(ICodeWriter code, InsnArg first, InsnArg second) {
if (first.isLiteral() && second.isLiteral()) {
if (first.getType() == ArgType.BYTE) {
long lit1 = ((LiteralArg) first).getLiteral();
long lit2 = ((LiteralArg) second).getLiteral();
if (lit1 != Byte.MAX_VALUE && lit1 != Byte.MIN_VALUE
&& lit2 != Byte.MAX_VALUE && lit2 != Byte.MIN_VALUE) {
code.add("(byte) ");
}
} else if (first.getType() == ArgType.SHORT) {
long lit1 = ((LiteralArg) first).getLiteral();
long lit2 = ((LiteralArg) second).getLiteral();
if (lit1 != Short.MAX_VALUE && lit1 != Short.MIN_VALUE
&& lit2 != Short.MAX_VALUE && lit2 != Short.MIN_VALUE) {
code.add("(short) ");
}
} else if (first.getType() == ArgType.CHAR) {
long lit1 = ((LiteralArg) first).getLiteral();
long lit2 = ((LiteralArg) second).getLiteral();
if (!NameMapper.isPrintableChar((char) (lit1))
&& !NameMapper.isPrintableChar((char) (lit2))) {
code.add("(char) ");
}
}
}
}
private void makeArith(ArithNode insn, ICodeWriter code, Set<Flags> state) throws CodegenException {
if (insn.contains(AFlag.ARITH_ONEARG)) {
makeArithOneArg(insn, code);
@@ -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;
@@ -180,7 +180,7 @@ public class MethodGen {
if (overrideAttr == null) {
return;
}
if (!overrideAttr.isAtBaseMth()) {
if (!overrideAttr.getBaseMethods().contains(mth)) {
code.startLine("@Override");
if (mth.checkCommentsLevel(CommentsLevel.INFO)) {
code.add(" // ");
@@ -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()) {
@@ -162,8 +162,7 @@ public class NameGen {
InsnNode assignInsn = assignArg.getParentInsn();
if (assignInsn != null) {
String name = makeNameFromInsn(assignInsn);
if (name != null && !NameMapper.isReserved(name)) {
assignArg.setName(name);
if (name != null && NameMapper.isValidAndPrintable(name)) {
return name;
}
}
@@ -202,7 +201,11 @@ public class NameGen {
return vName;
}
if (shortName != null) {
return StringUtils.escape(shortName.toLowerCase());
String lower = StringUtils.escape(shortName.toLowerCase());
if (shortName.equals(lower)) {
return lower + "Var";
}
return lower;
}
}
return StringUtils.escape(type.toString());
@@ -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).getAbsoluteFile().toPath();
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());
}
@@ -143,19 +143,15 @@ public class NameMapper {
/**
* Return modified string with removed:
* <p>
* <ul>
* <li>not printable chars (including unicode)
* <li>chars not valid for java identifier part
* </ul>
* <p>
* Note: this 'middle' method must be used with prefixed string:
* <p>
* <ul>
* <li>can leave invalid chars for java identifier start (i.e numbers)
* <li>result not checked for reserved words
* </ul>
* <p>
*/
public static String removeInvalidCharsMiddle(String name) {
if (isValidIdentifier(name) && isAllCharsPrintable(name)) {
@@ -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);
}
}
@@ -33,8 +33,8 @@ public enum AFlag {
SKIP_FIRST_ARG,
SKIP_ARG, // skip argument in invoke call
NO_SKIP_ARGS,
ANONYMOUS_CONSTRUCTOR,
ANONYMOUS_CLASS,
THIS,
SUPER,
@@ -76,8 +76,11 @@ 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,
// Class processing flags
RESTART_CODEGEN, // codegen must be executed again
RELOAD_AT_CODEGEN_STAGE, // class can't be analyzed at 'process' stage => unload before 'codegen' stage
@@ -2,6 +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.AnonymousClassAttr;
import jadx.core.dex.attributes.nodes.ClassTypeVarsAttr;
import jadx.core.dex.attributes.nodes.DeclareVariablesAttr;
import jadx.core.dex.attributes.nodes.EdgeInsnAttr;
@@ -16,6 +17,7 @@ import jadx.core.dex.attributes.nodes.JumpInfo;
import jadx.core.dex.attributes.nodes.LocalVarsDebugInfoAttr;
import jadx.core.dex.attributes.nodes.LoopInfo;
import jadx.core.dex.attributes.nodes.LoopLabelAttr;
import jadx.core.dex.attributes.nodes.MethodBridgeAttr;
import jadx.core.dex.attributes.nodes.MethodInlineAttr;
import jadx.core.dex.attributes.nodes.MethodOverrideAttr;
import jadx.core.dex.attributes.nodes.MethodTypeVarsAttr;
@@ -23,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;
@@ -51,6 +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<AnonymousClassAttr> ANONYMOUS_CLASS = new AType<>();
// field
public static final AType<FieldInitInsnAttr> FIELD_INIT_INSN = new AType<>();
@@ -63,6 +67,7 @@ public final class AType<T extends IJadxAttribute> implements IJadxAttrType<T> {
public static final AType<MethodOverrideAttr> METHOD_OVERRIDE = new AType<>();
public static final AType<MethodTypeVarsAttr> METHOD_TYPE_VARS = new AType<>();
public static final AType<AttrList<TryCatchBlockAttr>> TRY_BLOCKS_LIST = new AType<>();
public static final AType<MethodBridgeAttr> BRIDGED_BY = new AType<>();
// region
public static final AType<DeclareVariablesAttr> DECLARE_VARIABLES = new AType<>();
@@ -72,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,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() {
@@ -0,0 +1,28 @@
package jadx.core.dex.attributes.nodes;
import jadx.api.plugins.input.data.attributes.PinnedAttribute;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.nodes.MethodNode;
public class MethodBridgeAttr extends PinnedAttribute {
private final MethodNode bridgeMth;
public MethodBridgeAttr(MethodNode bridgeMth) {
this.bridgeMth = bridgeMth;
}
public MethodNode getBridgeMth() {
return bridgeMth;
}
@Override
public AType<MethodBridgeAttr> getAttrType() {
return AType.BRIDGED_BY;
}
@Override
public String toString() {
return "BRIDGED_BY: " + bridgeMth;
}
}
@@ -1,6 +1,7 @@
package jadx.core.dex.attributes.nodes;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import jadx.api.plugins.input.data.attributes.PinnedAttribute;
@@ -20,27 +21,26 @@ public class MethodOverrideAttr extends PinnedAttribute {
*/
private SortedSet<MethodNode> relatedMthNodes;
public MethodOverrideAttr(List<IMethodDetails> overrideList, SortedSet<MethodNode> relatedMthNodes) {
private Set<IMethodDetails> baseMethods;
public MethodOverrideAttr(List<IMethodDetails> overrideList, SortedSet<MethodNode> relatedMthNodes, Set<IMethodDetails> baseMethods) {
this.overrideList = overrideList;
this.relatedMthNodes = relatedMthNodes;
}
public boolean isAtBaseMth() {
return overrideList.isEmpty();
this.baseMethods = baseMethods;
}
public List<IMethodDetails> getOverrideList() {
return overrideList;
}
public void setOverrideList(List<IMethodDetails> overrideList) {
this.overrideList = overrideList;
}
public SortedSet<MethodNode> getRelatedMthNodes() {
return relatedMthNodes;
}
public Set<IMethodDetails> getBaseMethods() {
return baseMethods;
}
public void setRelatedMthNodes(SortedSet<MethodNode> relatedMthNodes) {
this.relatedMthNodes = relatedMthNodes;
}
@@ -52,6 +52,6 @@ public class MethodOverrideAttr extends PinnedAttribute {
@Override
public String toString() {
return "METHOD_OVERRIDE: " + overrideList;
return "METHOD_OVERRIDE: " + getBaseMethods();
}
}
@@ -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:
@@ -508,7 +508,17 @@ public class InsnDecoder {
private InsnNode makeNewArray(InsnData insn) {
ArgType indexType = ArgType.parse(insn.getIndexAsType());
int dim = (int) insn.getLiteral();
ArgType arrType = dim == 0 ? indexType : ArgType.array(indexType, dim);
ArgType arrType;
if (dim == 0) {
arrType = indexType;
} else {
if (indexType.isArray()) {
// java bytecode can pass array as a base type
arrType = indexType;
} else {
arrType = ArgType.array(indexType, dim);
}
}
int regsCount = insn.getRegsCount();
NewArrayNode newArr = new NewArrayNode(arrType, regsCount - 1);
newArr.setResult(InsnArg.reg(insn, 0, arrType));
@@ -597,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;
@@ -655,7 +655,7 @@ public abstract class ArgType {
if (from.equals(to)) {
return false;
}
TypeCompareEnum result = root.getTypeUpdate().getTypeCompare().compareTypes(from, to);
TypeCompareEnum result = root.getTypeCompare().compareTypes(from, to);
return !result.isNarrow();
}
@@ -1,5 +1,7 @@
package jadx.core.dex.instructions.args;
import org.jetbrains.annotations.Nullable;
import jadx.core.codegen.TypeGen;
import jadx.core.utils.StringUtils;
import jadx.core.utils.exceptions.JadxRuntimeException;
@@ -57,12 +59,48 @@ public final class LiteralArg extends InsnArg {
}
public boolean isInteger() {
PrimitiveType type = this.type.getPrimitiveType();
return type == PrimitiveType.INT
|| type == PrimitiveType.BYTE
|| type == PrimitiveType.CHAR
|| type == PrimitiveType.SHORT
|| type == PrimitiveType.LONG;
switch (type.getPrimitiveType()) {
case INT:
case BYTE:
case CHAR:
case SHORT:
case LONG:
return true;
default:
return false;
}
}
public boolean isNegative() {
if (isInteger()) {
return literal < 0;
}
if (type == ArgType.FLOAT) {
float val = Float.intBitsToFloat(((int) literal));
return val < 0 && Float.isFinite(val);
}
if (type == ArgType.DOUBLE) {
double val = Double.longBitsToDouble(literal);
return val < 0 && Double.isFinite(val);
}
return false;
}
@Nullable
public LiteralArg negate() {
long neg;
if (isInteger()) {
neg = -literal;
} else if (type == ArgType.FLOAT) {
float val = Float.intBitsToFloat(((int) literal));
neg = Float.floatToIntBits(-val);
} else if (type == ArgType.DOUBLE) {
double val = Double.longBitsToDouble(literal);
neg = Double.doubleToLongBits(-val);
} else {
return null;
}
return new LiteralArg(neg, type);
}
@Override
@@ -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,12 +43,12 @@ 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;
import static jadx.core.dex.nodes.ProcessState.LOADED;
import static jadx.core.dex.nodes.ProcessState.NOT_LOADED;
import static jadx.core.dex.nodes.ProcessState.PROCESS_COMPLETE;
public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeNode, Comparable<ClassNode> {
private static final Logger LOG = LoggerFactory.getLogger(ClassNode.class);
@@ -79,6 +80,10 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
* Top level classes used in this class (only for top level classes, empty for inners)
*/
private List<ClassNode> dependencies = Collections.emptyList();
/**
* Top level classes needed for code generation stage
*/
private List<ClassNode> codegenDeps = Collections.emptyList();
/**
* Classes which uses this class
*/
@@ -283,12 +288,15 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
return true;
}
public boolean checkProcessed() {
return getTopParentClass().getState().isProcessComplete();
}
public void ensureProcessed() {
ClassNode topClass = getTopParentClass();
ProcessState state = topClass.getState();
if (state != PROCESS_COMPLETE) {
if (!checkProcessed()) {
ClassNode topParentClass = getTopParentClass();
throw new JadxRuntimeException("Expected class to be processed at this point,"
+ " class: " + topClass + ", state: " + state);
+ " class: " + topParentClass + ", state: " + topParentClass.getState());
}
}
@@ -352,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()) {
@@ -602,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() {
@@ -733,6 +752,26 @@ 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;
}
public void setCodegenDeps(List<ClassNode> codegenDeps) {
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";
@@ -322,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.
*/
@@ -385,17 +419,16 @@ public class InsnNode extends LineAttrNode {
/**
* Make copy of InsnNode object.
* <p>
* <br>
* NOTE: can't copy instruction with result argument
* (SSA variable can't be used in two different assigns).
* <p>
* <br>
* Prefer use next methods:
* <ul>
* <li>{@link #copyWithoutResult()} to explicitly state that result not needed
* <li>{@link #copy(RegisterArg)} to provide new result arg
* <li>{@link #copyWithNewSsaVar(MethodNode)} to make new SSA variable for result arg
* </ul>
* <p>
*/
public InsnNode copy() {
if (this.getClass() != InsnNode.class) {
@@ -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;
@@ -336,12 +336,9 @@ public class RootNode {
/**
* Searches for ClassNode by its full name (original or alias name)
*
* <br>
* Warning: This method has a runtime of O(n) (n = number of classes).
* If you need to call it more than once consider {@link #buildFullAliasClassCache()} instead
*
* @param fullName
* @return
*/
@Nullable
public ClassNode searchClassByFullAlias(String fullName) {
@@ -355,10 +352,6 @@ public class RootNode {
return null;
}
/**
*
* @return
*/
public Map<String, ClassNode> buildFullAliasClassCache() {
Map<String, ClassNode> classNameCache = new HashMap<>(classes.size());
for (ClassNode cls : classes) {
@@ -385,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;
@@ -437,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('*')) {
@@ -8,6 +8,9 @@ import org.jetbrains.annotations.Nullable;
import jadx.core.clsp.ClspClass;
import jadx.core.clsp.ClspMethod;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.MethodBridgeAttr;
import jadx.core.dex.attributes.nodes.MethodOverrideAttr;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.instructions.BaseInvokeNode;
import jadx.core.dex.instructions.args.ArgType;
@@ -15,6 +18,7 @@ import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.IMethodDetails;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.Utils;
public class MethodUtils {
private final RootNode root;
@@ -34,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;
}
@@ -67,7 +71,7 @@ public class MethodUtils {
return null;
}
public boolean processMethodArgsOverloaded(ArgType startCls, MethodInfo mthInfo, @Nullable List<IMethodDetails> collectedMths) {
private boolean processMethodArgsOverloaded(ArgType startCls, MethodInfo mthInfo, @Nullable List<IMethodDetails> collectedMths) {
if (startCls == null || !startCls.isObject()) {
return false;
}
@@ -122,4 +126,25 @@ public class MethodUtils {
}
return false;
}
@Nullable
public IMethodDetails getOverrideBaseMth(MethodNode mth) {
MethodOverrideAttr overrideAttr = mth.get(AType.METHOD_OVERRIDE);
if (overrideAttr == null) {
return null;
}
return Utils.getOne(overrideAttr.getBaseMethods());
}
public ClassInfo getMethodOriginDeclClass(MethodNode mth) {
IMethodDetails baseMth = getOverrideBaseMth(mth);
if (baseMth != null) {
return baseMth.getMethodInfo().getDeclClass();
}
MethodBridgeAttr bridgeAttr = mth.get(AType.BRIDGED_BY);
if (bridgeAttr != null) {
return getMethodOriginDeclClass(bridgeAttr.getBridgeMth());
}
return mth.getMethodInfo().getDeclClass();
}
}
@@ -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;
}
@@ -0,0 +1,133 @@
package jadx.core.dex.visitors;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
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;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.instructions.IndexInsnNode;
import jadx.core.dex.instructions.InsnType;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.instructions.args.SSAVar;
import jadx.core.dex.instructions.mods.ConstructorInsn;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.visitors.shrink.CodeShrinkVisitor;
import jadx.core.utils.exceptions.JadxException;
@JadxVisitor(
name = "AnonymousClassVisitor",
desc = "Prepare anonymous class for inline",
runBefore = {
ModVisitor.class,
CodeShrinkVisitor.class
}
)
public class AnonymousClassVisitor extends AbstractVisitor {
@Override
public boolean visit(ClassNode cls) throws JadxException {
if (cls.contains(AType.ANONYMOUS_CLASS)) {
for (MethodNode mth : cls.getMethods()) {
if (mth.contains(AFlag.ANONYMOUS_CONSTRUCTOR)) {
processAnonymousConstructor(mth);
break;
}
}
}
return true;
}
private static void processAnonymousConstructor(MethodNode mth) {
List<InsnNode> usedInsns = new ArrayList<>();
Map<InsnArg, FieldNode> argsMap = getArgsToFieldsMapping(mth, usedInsns);
if (argsMap.isEmpty()) {
mth.add(AFlag.NO_SKIP_ARGS);
} else {
for (Map.Entry<InsnArg, FieldNode> entry : argsMap.entrySet()) {
FieldNode field = entry.getValue();
if (field == null) {
continue;
}
InsnArg arg = entry.getKey();
field.addAttr(new FieldReplaceAttr(arg));
field.add(AFlag.DONT_GENERATE);
if (arg.isRegister()) {
arg.add(AFlag.SKIP_ARG);
SkipMethodArgsAttr.skipArg(mth, ((RegisterArg) arg));
}
}
}
for (InsnNode usedInsn : usedInsns) {
usedInsn.add(AFlag.DONT_GENERATE);
}
}
private static Map<InsnArg, FieldNode> getArgsToFieldsMapping(MethodNode mth, List<InsnNode> usedInsns) {
MethodInfo callMth = mth.getMethodInfo();
ClassNode cls = mth.getParentClass();
List<RegisterArg> argList = mth.getArgRegs();
ClassNode outerCls = mth.getUseIn().get(0).getParentClass();
int startArg = 0;
if (callMth.getArgsCount() != 0 && callMth.getArgumentsTypes().get(0).equals(outerCls.getClassInfo().getType())) {
startArg = 1;
}
Map<InsnArg, FieldNode> map = new LinkedHashMap<>();
int argsCount = argList.size();
for (int i = startArg; i < argsCount; i++) {
RegisterArg arg = argList.get(i);
InsnNode useInsn = getParentInsnSkipMove(arg);
if (useInsn == null) {
return Collections.emptyMap();
}
switch (useInsn.getType()) {
case IPUT:
FieldNode fieldNode = cls.searchField((FieldInfo) ((IndexInsnNode) useInsn).getIndex());
if (fieldNode == null || !fieldNode.getAccessFlags().isSynthetic()) {
return Collections.emptyMap();
}
map.put(arg, fieldNode);
usedInsns.add(useInsn);
break;
case CONSTRUCTOR:
ConstructorInsn superConstr = (ConstructorInsn) useInsn;
if (!superConstr.isSuper()) {
return Collections.emptyMap();
}
usedInsns.add(useInsn);
break;
default:
return Collections.emptyMap();
}
}
return map;
}
private static InsnNode getParentInsnSkipMove(RegisterArg arg) {
SSAVar sVar = arg.getSVar();
if (sVar.getUseCount() != 1) {
return null;
}
RegisterArg useArg = sVar.getUseList().get(0);
InsnNode parentInsn = useArg.getParentInsn();
if (parentInsn == null) {
return null;
}
if (parentInsn.getType() == InsnType.MOVE) {
return getParentInsnSkipMove(parentInsn.getResult());
}
return parentInsn;
}
}
@@ -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);
@@ -1,7 +1,10 @@
package jadx.core.dex.visitors;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import jadx.api.plugins.input.data.AccessFlags;
@@ -55,7 +58,7 @@ public class ClassModifier extends AbstractVisitor {
removeSyntheticFields(cls);
cls.getMethods().forEach(ClassModifier::removeSyntheticMethods);
cls.getMethods().forEach(ClassModifier::removeEmptyMethods);
cls.getMethods().forEach(ClassModifier::cleanInsnsInAnonymousConstructor);
cls.getMethods().forEach(ClassModifier::processAnonymousConstructor);
return false;
}
@@ -241,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;
}
@@ -326,27 +329,86 @@ public class ClassModifier extends AbstractVisitor {
/**
* Remove super call and put into removed fields from anonymous constructor
*/
private static void cleanInsnsInAnonymousConstructor(MethodNode mth) {
private static void processAnonymousConstructor(MethodNode mth) {
if (!mth.contains(AFlag.ANONYMOUS_CONSTRUCTOR)) {
return;
}
for (BlockNode block : mth.getBasicBlocks()) {
for (InsnNode insn : block.getInstructions()) {
InsnType type = insn.getType();
if (type == InsnType.CONSTRUCTOR) {
ConstructorInsn ctorInsn = (ConstructorInsn) insn;
if (ctorInsn.isSuper()) {
ctorInsn.add(AFlag.DONT_GENERATE);
}
} else if (type == InsnType.IPUT) {
FieldInfo fldInfo = (FieldInfo) ((IndexInsnNode) insn).getIndex();
FieldNode fieldNode = mth.root().resolveField(fldInfo);
if (fieldNode != null && fieldNode.contains(AFlag.DONT_GENERATE)) {
insn.add(AFlag.DONT_GENERATE);
}
}
List<InsnNode> usedInsns = new ArrayList<>();
Map<InsnArg, FieldNode> argsMap = getArgsToFieldsMapping(mth, usedInsns);
for (Map.Entry<InsnArg, FieldNode> entry : argsMap.entrySet()) {
FieldNode field = entry.getValue();
if (field == null) {
continue;
}
InsnArg arg = entry.getKey();
field.addAttr(new FieldReplaceAttr(arg));
field.add(AFlag.DONT_GENERATE);
if (arg.isRegister()) {
arg.add(AFlag.SKIP_ARG);
SkipMethodArgsAttr.skipArg(mth, ((RegisterArg) arg));
}
}
for (InsnNode usedInsn : usedInsns) {
usedInsn.add(AFlag.DONT_GENERATE);
}
}
private static Map<InsnArg, FieldNode> getArgsToFieldsMapping(MethodNode mth, List<InsnNode> usedInsns) {
MethodInfo callMth = mth.getMethodInfo();
ClassNode cls = mth.getParentClass();
List<RegisterArg> argList = mth.getArgRegs();
ClassNode outerCls = mth.getUseIn().get(0).getParentClass();
int startArg = 0;
if (callMth.getArgsCount() != 0 && callMth.getArgumentsTypes().get(0).equals(outerCls.getClassInfo().getType())) {
startArg = 1;
}
Map<InsnArg, FieldNode> map = new LinkedHashMap<>();
int argsCount = argList.size();
for (int i = startArg; i < argsCount; i++) {
RegisterArg arg = argList.get(i);
InsnNode useInsn = getParentInsnSkipMove(arg);
if (useInsn == null) {
return Collections.emptyMap();
}
switch (useInsn.getType()) {
case IPUT:
FieldNode fieldNode = cls.searchField((FieldInfo) ((IndexInsnNode) useInsn).getIndex());
if (fieldNode == null || !fieldNode.getAccessFlags().isSynthetic()) {
return Collections.emptyMap();
}
map.put(arg, fieldNode);
usedInsns.add(useInsn);
break;
case CONSTRUCTOR:
ConstructorInsn superConstr = (ConstructorInsn) useInsn;
if (!superConstr.isSuper()) {
return Collections.emptyMap();
}
usedInsns.add(useInsn);
break;
default:
return Collections.emptyMap();
}
}
return map;
}
private static InsnNode getParentInsnSkipMove(RegisterArg arg) {
SSAVar sVar = arg.getSVar();
if (sVar.getUseCount() != 1) {
return null;
}
RegisterArg useArg = sVar.getUseList().get(0);
InsnNode parentInsn = useArg.getParentInsn();
if (parentInsn == null) {
return null;
}
if (parentInsn.getType() == InsnType.MOVE) {
return getParentInsnSkipMove(parentInsn.getResult());
}
return parentInsn;
}
private static boolean isNonDefaultConstructorExists(MethodNode defCtor) {
@@ -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) {
@@ -86,7 +86,7 @@ public class FixAccessModifiers extends AbstractVisitor {
if (!accessFlags.isPublic()) {
// if class is used in inlinable method => make it public
for (MethodNode useMth : cls.getUseInMth()) {
boolean canInline = MarkMethodsForInline.canInline(useMth) || useMth.contains(AType.METHOD_INLINE);
boolean canInline = useMth.contains(AFlag.METHOD_CANDIDATE_FOR_INLINE) || useMth.contains(AType.METHOD_INLINE);
if (canInline && !useMth.getUseIn().isEmpty()) {
return AccessFlags.PUBLIC;
}
@@ -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;
@@ -10,7 +10,6 @@ import jadx.core.Consts;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.MethodInlineAttr;
import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.info.FieldInfo;
import jadx.core.dex.instructions.IndexInsnNode;
import jadx.core.dex.instructions.InsnType;
@@ -47,7 +46,7 @@ public class MarkMethodsForInline extends AbstractVisitor {
if (mia != null) {
return mia;
}
if (canInline(mth)) {
if (mth.contains(AFlag.METHOD_CANDIDATE_FOR_INLINE)) {
if (mth.getBasicBlocks() == null) {
return null;
}
@@ -59,14 +58,6 @@ public class MarkMethodsForInline extends AbstractVisitor {
return MethodInlineAttr.inlineNotNeeded(mth);
}
public static boolean canInline(MethodNode mth) {
if (mth.isNoCode() || mth.contains(AFlag.DONT_GENERATE)) {
return false;
}
AccessInfo accessFlags = mth.getAccessFlags();
return accessFlags.isSynthetic() && accessFlags.isStatic();
}
@Nullable
private static MethodInlineAttr inlineMth(MethodNode mth) {
List<InsnNode> insns = BlockUtils.collectInsnsWithLimit(mth.getBasicBlocks(), 2);
@@ -79,7 +70,11 @@ public class MarkMethodsForInline extends AbstractVisitor {
if (insn.getType() == InsnType.RETURN && insn.getArgsCount() == 1) {
// synthetic field getter
// set arg from 'return' instruction
return addInlineAttr(mth, InsnNode.wrapArg(insn.getArg(0)));
InsnArg arg = insn.getArg(0);
if (!arg.isInsnWrap()) {
return null;
}
return addInlineAttr(mth, ((InsnWrapArg) arg).getWrapInsn());
}
// method invoke
return addInlineAttr(mth, insn);
@@ -111,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);
}
@@ -1,7 +1,5 @@
package jadx.core.dex.visitors;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -19,10 +17,9 @@ 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.FieldReplaceAttr;
import jadx.core.dex.attributes.nodes.SkipMethodArgsAttr;
import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.info.FieldInfo;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.instructions.ArithNode;
import jadx.core.dex.instructions.ConstClassNode;
import jadx.core.dex.instructions.ConstStringNode;
@@ -46,6 +43,7 @@ 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.IMethodDetails;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.regions.conditions.IfCondition;
@@ -116,7 +114,7 @@ public class ModVisitor extends AbstractVisitor {
break;
case SWITCH:
replaceConstKeys(parentClass, (SwitchInsn) insn);
replaceConstKeys(mth, parentClass, (SwitchInsn) insn);
break;
case NEW_ARRAY:
@@ -230,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);
}
}
}
@@ -293,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()) {
@@ -322,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);
}
}
@@ -334,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);
}
}
}
}
@@ -432,96 +441,39 @@ public class ModVisitor extends AbstractVisitor {
return false;
}
/**
* For args in anonymous constructor invoke apply:
* - forbid inline into constructor call
* - make variables final (compiler require this implicitly)
*/
private static void processAnonymousConstructor(MethodNode mth, ConstructorInsn co) {
MethodInfo callMth = co.getCallMth();
MethodNode callMthNode = mth.root().resolveMethod(callMth);
if (callMthNode == null) {
IMethodDetails callMthDetails = mth.root().getMethodUtils().getMethodDetails(co);
if (!(callMthDetails instanceof MethodNode)) {
return;
}
ClassNode classNode = callMthNode.getParentClass();
if (!classNode.isAnonymous()) {
MethodNode callMth = (MethodNode) callMthDetails;
if (!callMth.contains(AFlag.ANONYMOUS_CONSTRUCTOR) || callMth.contains(AFlag.NO_SKIP_ARGS)) {
return;
}
if (!mth.getParentClass().getInnerClasses().contains(classNode)) {
return;
}
Map<InsnArg, FieldNode> argsMap = getArgsToFieldsMapping(callMthNode, co);
if (argsMap.isEmpty() && !callMthNode.getArgRegs().isEmpty()) {
return;
}
for (Map.Entry<InsnArg, FieldNode> entry : argsMap.entrySet()) {
FieldNode field = entry.getValue();
if (field == null) {
continue;
}
InsnArg arg = entry.getKey();
field.addAttr(new FieldReplaceAttr(arg));
field.add(AFlag.DONT_GENERATE);
if (arg.isRegister()) {
RegisterArg reg = (RegisterArg) arg;
SSAVar sVar = reg.getSVar();
if (sVar != null) {
sVar.getCodeVar().setFinal(true);
SkipMethodArgsAttr attr = callMth.get(AType.SKIP_MTH_ARGS);
if (attr != null) {
int argsCount = Math.min(callMth.getMethodInfo().getArgsCount(), co.getArgsCount());
for (int i = 0; i < argsCount; i++) {
if (attr.isSkip(i)) {
anonymousCallArgMod(co.getArg(i));
}
reg.add(AFlag.DONT_INLINE);
reg.add(AFlag.SKIP_ARG);
}
} else {
// additional info not available apply mods to all args (the safest solution)
co.getArguments().forEach(ModVisitor::anonymousCallArgMod);
}
}
private static Map<InsnArg, FieldNode> getArgsToFieldsMapping(MethodNode callMthNode, ConstructorInsn co) {
Map<InsnArg, FieldNode> map = new LinkedHashMap<>();
MethodInfo callMth = callMthNode.getMethodInfo();
ClassNode cls = callMthNode.getParentClass();
ClassNode parentClass = cls.getParentClass();
List<RegisterArg> argList = callMthNode.getArgRegs();
int startArg = 0;
if (callMth.getArgsCount() != 0 && callMth.getArgumentsTypes().get(0).equals(parentClass.getClassInfo().getType())) {
startArg = 1;
private static void anonymousCallArgMod(InsnArg arg) {
arg.add(AFlag.DONT_INLINE);
if (arg.isRegister()) {
((RegisterArg) arg).getSVar().getCodeVar().setFinal(true);
}
int argsCount = argList.size();
for (int i = startArg; i < argsCount; i++) {
RegisterArg arg = argList.get(i);
InsnNode useInsn = getParentInsnSkipMove(arg);
if (useInsn == null) {
return Collections.emptyMap();
}
FieldNode fieldNode = null;
if (useInsn.getType() == InsnType.IPUT) {
FieldInfo field = (FieldInfo) ((IndexInsnNode) useInsn).getIndex();
fieldNode = cls.searchField(field);
if (fieldNode == null || !fieldNode.getAccessFlags().isSynthetic()) {
return Collections.emptyMap();
}
} else if (useInsn.getType() == InsnType.CONSTRUCTOR) {
ConstructorInsn superConstr = (ConstructorInsn) useInsn;
if (!superConstr.isSuper()) {
return Collections.emptyMap();
}
} else {
return Collections.emptyMap();
}
map.put(co.getArg(i), fieldNode);
}
return map;
}
private static InsnNode getParentInsnSkipMove(RegisterArg arg) {
SSAVar sVar = arg.getSVar();
if (sVar.getUseCount() != 1) {
return null;
}
RegisterArg useArg = sVar.getUseList().get(0);
InsnNode parentInsn = useArg.getParentInsn();
if (parentInsn == null) {
return null;
}
if (parentInsn.getType() == InsnType.MOVE) {
return getParentInsnSkipMove(parentInsn.getResult());
}
return parentInsn;
}
/**
@@ -575,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);
}
@@ -1,11 +1,11 @@
package jadx.core.dex.visitors;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.stream.Collectors;
@@ -17,6 +17,7 @@ import jadx.core.clsp.ClspClass;
import jadx.core.clsp.ClspMethod;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.MethodBridgeAttr;
import jadx.core.dex.attributes.nodes.MethodOverrideAttr;
import jadx.core.dex.attributes.nodes.RenameReasonAttr;
import jadx.core.dex.info.AccessInfo;
@@ -32,6 +33,7 @@ import jadx.core.dex.visitors.typeinference.TypeCompareEnum;
import jadx.core.dex.visitors.typeinference.TypeInferenceVisitor;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxException;
import jadx.core.utils.exceptions.JadxRuntimeException;
@JadxVisitor(
name = "OverrideMethodVisitor",
@@ -45,70 +47,86 @@ public class OverrideMethodVisitor extends AbstractVisitor {
@Override
public boolean visit(ClassNode cls) throws JadxException {
processCls(cls);
SuperTypesData superData = collectSuperTypes(cls);
if (superData != null) {
for (MethodNode mth : cls.getMethods()) {
processMth(mth, superData);
}
}
return true;
}
private void processCls(ClassNode cls) {
List<ArgType> superTypes = collectSuperTypes(cls);
if (!superTypes.isEmpty()) {
for (MethodNode mth : cls.getMethods()) {
processMth(cls, superTypes, mth);
}
}
}
private void processMth(ClassNode cls, List<ArgType> superTypes, MethodNode mth) {
private void processMth(MethodNode mth, SuperTypesData superData) {
if (mth.isConstructor() || mth.getAccessFlags().isStatic() || mth.getAccessFlags().isPrivate()) {
return;
}
MethodOverrideAttr attr = processOverrideMethods(cls, mth, superTypes);
MethodOverrideAttr attr = processOverrideMethods(mth, superData);
if (attr != null) {
if (attr.getBaseMethods().isEmpty()) {
throw new JadxRuntimeException("No base methods for override attribute: " + attr.getOverrideList());
}
mth.addAttr(attr);
IMethodDetails baseMth = Utils.last(attr.getOverrideList());
IMethodDetails baseMth = Utils.getOne(attr.getBaseMethods());
if (baseMth != null) {
boolean updated = fixMethodReturnType(mth, baseMth, superTypes);
updated |= fixMethodArgTypes(mth, baseMth, superTypes);
if (updated && cls.root().getArgs().isRenameValid()) {
boolean updated = fixMethodReturnType(mth, baseMth, superData);
updated |= fixMethodArgTypes(mth, baseMth, superData);
if (updated) {
// check if new signature cause method collisions
fixMethodSignatureCollisions(mth);
checkMethodSignatureCollisions(mth, mth.root().getArgs().isRenameValid());
}
}
}
}
private MethodOverrideAttr processOverrideMethods(ClassNode cls, MethodNode mth, List<ArgType> superTypes) {
private MethodOverrideAttr processOverrideMethods(MethodNode mth, SuperTypesData superData) {
MethodOverrideAttr result = mth.get(AType.METHOD_OVERRIDE);
if (result != null) {
return result;
}
ClassNode cls = mth.getParentClass();
String signature = mth.getMethodInfo().makeSignature(false);
List<IMethodDetails> overrideList = new ArrayList<>();
for (ArgType superType : superTypes) {
ClassNode classNode = cls.root().resolveClass(superType);
Set<IMethodDetails> baseMethods = new HashSet<>();
for (ArgType superType : superData.getSuperTypes()) {
ClassNode classNode = mth.root().resolveClass(superType);
if (classNode != null) {
MethodNode ovrdMth = searchOverriddenMethod(classNode, signature);
if (ovrdMth != null && isMethodVisibleInCls(ovrdMth, cls)) {
overrideList.add(ovrdMth);
MethodOverrideAttr attr = ovrdMth.get(AType.METHOD_OVERRIDE);
if (attr != null) {
return buildOverrideAttr(mth, overrideList, attr);
if (ovrdMth != null) {
if (isMethodVisibleInCls(ovrdMth, cls)) {
overrideList.add(ovrdMth);
MethodOverrideAttr attr = ovrdMth.get(AType.METHOD_OVERRIDE);
if (attr != null) {
addBaseMethod(superData, overrideList, baseMethods, superType);
return buildOverrideAttr(mth, overrideList, baseMethods, attr);
}
}
}
} else {
ClspClass clsDetails = cls.root().getClsp().getClsDetails(superType);
ClspClass clsDetails = mth.root().getClsp().getClsDetails(superType);
if (clsDetails != null) {
Map<String, ClspMethod> methodsMap = clsDetails.getMethodsMap();
for (Map.Entry<String, ClspMethod> entry : methodsMap.entrySet()) {
String mthShortId = entry.getKey();
if (mthShortId.startsWith(signature)) {
overrideList.add(entry.getValue());
break;
}
}
}
}
addBaseMethod(superData, overrideList, baseMethods, superType);
}
return buildOverrideAttr(mth, overrideList, baseMethods, null);
}
private void addBaseMethod(SuperTypesData superData, List<IMethodDetails> overrideList, Set<IMethodDetails> baseMethods,
ArgType superType) {
if (superData.getEndTypes().contains(superType.getObject())) {
IMethodDetails last = Utils.last(overrideList);
if (last != null) {
baseMethods.add(last);
}
}
return buildOverrideAttr(mth, overrideList, null);
}
@Nullable
@@ -123,22 +141,24 @@ public class OverrideMethodVisitor extends AbstractVisitor {
@Nullable
private MethodOverrideAttr buildOverrideAttr(MethodNode mth, List<IMethodDetails> overrideList,
@Nullable MethodOverrideAttr attr) {
Set<IMethodDetails> baseMethods, @Nullable MethodOverrideAttr attr) {
if (overrideList.isEmpty() && attr == null) {
return null;
}
if (attr == null) {
// traced to base method
List<IMethodDetails> cleanOverrideList = overrideList.stream().distinct().collect(Collectors.toList());
return applyOverrideAttr(mth, cleanOverrideList, false);
return applyOverrideAttr(mth, cleanOverrideList, baseMethods, false);
}
// trace stopped at already processed method -> start merging
List<IMethodDetails> mergedOverrideList = Utils.mergeLists(overrideList, attr.getOverrideList());
List<IMethodDetails> cleanOverrideList = mergedOverrideList.stream().distinct().collect(Collectors.toList());
return applyOverrideAttr(mth, cleanOverrideList, true);
Set<IMethodDetails> mergedBaseMethods = Utils.mergeSets(baseMethods, attr.getBaseMethods());
return applyOverrideAttr(mth, cleanOverrideList, mergedBaseMethods, true);
}
private MethodOverrideAttr applyOverrideAttr(MethodNode mth, List<IMethodDetails> overrideList, boolean update) {
private MethodOverrideAttr applyOverrideAttr(MethodNode mth, List<IMethodDetails> overrideList,
Set<IMethodDetails> baseMethods, boolean update) {
// don't rename method if override list contains not resolved method
boolean dontRename = overrideList.stream().anyMatch(m -> !(m instanceof MethodNode));
SortedSet<MethodNode> relatedMethods = null;
@@ -188,10 +208,10 @@ public class OverrideMethodVisitor extends AbstractVisitor {
continue;
}
}
mthNode.addAttr(new MethodOverrideAttr(Utils.listTail(overrideList, depth), relatedMethods));
mthNode.addAttr(new MethodOverrideAttr(Utils.listTail(overrideList, depth), relatedMethods, baseMethods));
depth++;
}
return new MethodOverrideAttr(overrideList, relatedMethods);
return new MethodOverrideAttr(overrideList, relatedMethods, baseMethods);
}
@NotNull
@@ -221,52 +241,92 @@ public class OverrideMethodVisitor extends AbstractVisitor {
return Objects.equals(superMth.getParentClass().getPackage(), cls.getPackage());
}
private List<ArgType> collectSuperTypes(ClassNode cls) {
Map<String, ArgType> superTypes = new LinkedHashMap<>();
collectSuperTypes(cls, superTypes);
if (superTypes.isEmpty()) {
return Collections.emptyList();
private static final class SuperTypesData {
private final List<ArgType> superTypes;
private final Set<String> endTypes;
private SuperTypesData(List<ArgType> superTypes, Set<String> endTypes) {
this.superTypes = superTypes;
this.endTypes = endTypes;
}
public List<ArgType> getSuperTypes() {
return superTypes;
}
public Set<String> getEndTypes() {
return endTypes;
}
return new ArrayList<>(superTypes.values());
}
private void collectSuperTypes(ClassNode cls, Map<String, ArgType> superTypes) {
@Nullable
private SuperTypesData collectSuperTypes(ClassNode cls) {
List<ArgType> superTypes = new ArrayList<>();
Set<String> endTypes = new HashSet<>();
collectSuperTypes(cls, superTypes, endTypes);
if (superTypes.isEmpty()) {
return null;
}
if (endTypes.isEmpty()) {
throw new JadxRuntimeException("No end types in class hierarchy: " + cls);
}
return new SuperTypesData(superTypes, endTypes);
}
private void collectSuperTypes(ClassNode cls, List<ArgType> superTypes, Set<String> endTypes) {
RootNode root = cls.root();
int k = 0;
ArgType superClass = cls.getSuperClass();
if (superClass != null && !Objects.equals(superClass, ArgType.OBJECT)) {
addSuperType(root, superTypes, superClass);
if (superClass != null) {
k += addSuperType(root, superTypes, endTypes, superClass);
}
for (ArgType iface : cls.getInterfaces()) {
addSuperType(root, superTypes, iface);
k += addSuperType(root, superTypes, endTypes, iface);
}
if (k == 0) {
endTypes.add(cls.getType().getObject());
}
}
private void addSuperType(RootNode root, Map<String, ArgType> superTypesMap, ArgType superType) {
superTypesMap.put(superType.getObject(), superType);
private int addSuperType(RootNode root, List<ArgType> superTypesMap, Set<String> endTypes, ArgType superType) {
if (Objects.equals(superType, ArgType.OBJECT)) {
return 0;
}
superTypesMap.add(superType);
ClassNode classNode = root.resolveClass(superType);
if (classNode == null) {
for (String superCls : root.getClsp().getSuperTypes(superType.getObject())) {
ArgType type = ArgType.object(superCls);
superTypesMap.put(type.getObject(), type);
}
} else {
collectSuperTypes(classNode, superTypesMap);
if (classNode != null) {
collectSuperTypes(classNode, superTypesMap, endTypes);
return 1;
}
ClspClass clsDetails = root.getClsp().getClsDetails(superType);
if (clsDetails != null) {
int k = 0;
for (ArgType parentType : clsDetails.getParents()) {
k += addSuperType(root, superTypesMap, endTypes, parentType);
}
if (k == 0) {
endTypes.add(superType.getObject());
}
return 1;
}
// no info found => treat as hierarchy end
endTypes.add(superType.getObject());
return 1;
}
private boolean fixMethodReturnType(MethodNode mth, IMethodDetails baseMth, List<ArgType> superTypes) {
private boolean fixMethodReturnType(MethodNode mth, IMethodDetails baseMth, SuperTypesData superData) {
ArgType returnType = mth.getReturnType();
if (returnType == ArgType.VOID) {
return false;
}
boolean updated = updateReturnType(mth, baseMth, superTypes);
boolean updated = updateReturnType(mth, baseMth, superData);
if (updated) {
mth.addDebugComment("Return type fixed from '" + returnType + "' to match base method");
}
return updated;
}
private boolean updateReturnType(MethodNode mth, IMethodDetails baseMth, List<ArgType> superTypes) {
private boolean updateReturnType(MethodNode mth, IMethodDetails baseMth, SuperTypesData superData) {
ArgType baseReturnType = baseMth.getReturnType();
if (mth.getReturnType().equals(baseReturnType)) {
return false;
@@ -276,7 +336,7 @@ public class OverrideMethodVisitor extends AbstractVisitor {
}
TypeCompare typeCompare = mth.root().getTypeUpdate().getTypeCompare();
ArgType baseCls = baseMth.getMethodInfo().getDeclClass().getType();
for (ArgType superType : superTypes) {
for (ArgType superType : superData.getSuperTypes()) {
TypeCompareEnum compareResult = typeCompare.compareTypes(superType, baseCls);
if (compareResult == TypeCompareEnum.NARROW_BY_GENERIC) {
ArgType targetRetType = mth.root().getTypeUtils().replaceClassGenerics(superType, baseReturnType);
@@ -291,7 +351,7 @@ public class OverrideMethodVisitor extends AbstractVisitor {
return false;
}
private boolean fixMethodArgTypes(MethodNode mth, IMethodDetails baseMth, List<ArgType> superTypes) {
private boolean fixMethodArgTypes(MethodNode mth, IMethodDetails baseMth, SuperTypesData superData) {
List<ArgType> mthArgTypes = mth.getArgTypes();
List<ArgType> baseArgTypes = baseMth.getArgTypes();
if (mthArgTypes.equals(baseArgTypes)) {
@@ -304,7 +364,7 @@ public class OverrideMethodVisitor extends AbstractVisitor {
boolean changed = false;
List<ArgType> newArgTypes = new ArrayList<>(argCount);
for (int argNum = 0; argNum < argCount; argNum++) {
ArgType newType = updateArgType(mth, baseMth, superTypes, argNum);
ArgType newType = updateArgType(mth, baseMth, superData, argNum);
if (newType != null) {
changed = true;
newArgTypes.add(newType);
@@ -318,7 +378,7 @@ public class OverrideMethodVisitor extends AbstractVisitor {
return changed;
}
private ArgType updateArgType(MethodNode mth, IMethodDetails baseMth, List<ArgType> superTypes, int argNum) {
private ArgType updateArgType(MethodNode mth, IMethodDetails baseMth, SuperTypesData superData, int argNum) {
ArgType arg = mth.getArgTypes().get(argNum);
ArgType baseArg = baseMth.getArgTypes().get(argNum);
if (arg.equals(baseArg)) {
@@ -329,7 +389,7 @@ public class OverrideMethodVisitor extends AbstractVisitor {
}
TypeCompare typeCompare = mth.root().getTypeUpdate().getTypeCompare();
ArgType baseCls = baseMth.getMethodInfo().getDeclClass().getType();
for (ArgType superType : superTypes) {
for (ArgType superType : superData.getSuperTypes()) {
TypeCompareEnum compareResult = typeCompare.compareTypes(superType, baseCls);
if (compareResult == TypeCompareEnum.NARROW_BY_GENERIC) {
ArgType targetArgType = mth.root().getTypeUtils().replaceClassGenerics(superType, baseArg);
@@ -343,7 +403,7 @@ public class OverrideMethodVisitor extends AbstractVisitor {
return null;
}
private void fixMethodSignatureCollisions(MethodNode mth) {
private void checkMethodSignatureCollisions(MethodNode mth, boolean rename) {
String mthName = mth.getMethodInfo().getAlias();
String newSignature = MethodInfo.makeShortId(mthName, mth.getArgTypes(), null);
for (MethodNode otherMth : mth.getParentClass().getMethods()) {
@@ -351,12 +411,16 @@ public class OverrideMethodVisitor extends AbstractVisitor {
if (otherMthName.equals(mthName) && otherMth != mth) {
String otherSignature = otherMth.getMethodInfo().makeSignature(true, false);
if (otherSignature.equals(newSignature)) {
if (otherMth.contains(AFlag.DONT_RENAME) || otherMth.contains(AType.METHOD_OVERRIDE)) {
otherMth.addWarnComment("Can't rename method to resolve collision");
} else {
otherMth.getMethodInfo().setAlias(makeNewAlias(otherMth));
otherMth.addAttr(new RenameReasonAttr("avoid collision after fix types in other method"));
if (rename) {
if (otherMth.contains(AFlag.DONT_RENAME) || otherMth.contains(AType.METHOD_OVERRIDE)) {
otherMth.addWarnComment("Can't rename method to resolve collision");
} else {
otherMth.getMethodInfo().setAlias(makeNewAlias(otherMth));
otherMth.addAttr(new RenameReasonAttr("avoid collision after fix types in other method"));
}
}
otherMth.addAttr(new MethodBridgeAttr(mth));
return;
}
}
}
@@ -5,18 +5,28 @@ 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;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.InsnWrapArg;
import jadx.core.dex.instructions.args.RegisterArg;
@@ -24,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;
@@ -53,6 +64,7 @@ public class PrepareForCodeGen extends AbstractVisitor {
if (cls.root().getArgs().isDebugInfo()) {
setClassSourceLine(cls);
}
collectFieldsUsageInAnnotations(cls);
return true;
}
@@ -69,8 +81,10 @@ public class PrepareForCodeGen extends AbstractVisitor {
checkInline(block);
removeParenthesis(block);
modifyArith(block);
checkConstUsage(block);
}
moveConstructorInConstructor(mth);
collectFieldsUsageInAnnotations(mth, mth);
}
private static void removeInstructions(BlockNode block) {
@@ -122,6 +136,38 @@ public class PrepareForCodeGen extends AbstractVisitor {
}
}
/**
* Add explicit type for non int constants
*/
private static void checkConstUsage(BlockNode block) {
for (InsnNode blockInsn : block.getInstructions()) {
blockInsn.visitInsns(insn -> {
if (forbidExplicitType(insn.getType())) {
return;
}
for (InsnArg arg : insn.getArguments()) {
if (arg.isLiteral() && arg.getType() != ArgType.INT) {
arg.add(AFlag.EXPLICIT_PRIMITIVE_TYPE);
}
}
});
}
}
private static boolean forbidExplicitType(InsnType type) {
switch (type) {
case CONST:
case CAST:
case IF:
case FILLED_NEW_ARRAY:
case APUT:
case ARITH:
return true;
default:
return false;
}
}
private static void removeParenthesis(BlockNode block) {
for (InsnNode insn : block.getInstructions()) {
removeParenthesis(insn);
@@ -276,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,26 @@
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.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;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.visitors.usage.UsageInfoVisitor;
import jadx.core.utils.ListUtils;
import jadx.core.utils.exceptions.JadxException;
@JadxVisitor(
@@ -17,95 +32,266 @@ 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) {
if (usedOnlyOnce(cls) || isAnonymous(cls) || isLambdaCls(cls)) {
if (isStaticFieldUsedOutside(cls)) {
if (!canBeAnonymous(cls)) {
return;
}
MethodNode anonymousConstructor = checkUsage(cls);
if (anonymousConstructor == null) {
return;
}
ArgType baseType = getBaseType(cls);
if (baseType == null) {
return;
}
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 topOuterCls = outerCls.getTopParentClass();
cls.removeDependency(topOuterCls);
ListUtils.safeRemove(outerCls.getUseIn(), cls);
// move dependency to codegen stage
if (cls.isTopClass()) {
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;
}
cls.add(AFlag.ANONYMOUS_CLASS);
cls.add(AFlag.DONT_GENERATE);
for (MethodNode mth : cls.getMethods()) {
if (mth.isConstructor()) {
mth.add(AFlag.ANONYMOUS_CONSTRUCTOR);
}
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 isStaticFieldUsedOutside(ClassNode cls) {
ClassNode topCls = cls.getTopParentClass();
for (FieldNode field : cls.getFields()) {
if (field.isStatic()) {
for (MethodNode useMth : field.getUseIn()) {
ClassNode useCls = useMth.getParentClass().getTopParentClass();
if (!useCls.equals(topCls)) {
return true;
}
}
}
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;
}
return false;
}
private static boolean usedOnlyOnce(ClassNode cls) {
if (cls.getUseIn().size() == 1 && cls.getUseInMth().size() == 1) {
// used only once
boolean synthetic = cls.getAccessFlags().isSynthetic() || cls.getClassInfo().getShortName().contains("$");
if (synthetic) {
// must have only one constructor which used only once
MethodNode ctr = null;
for (MethodNode mth : cls.getMethods()) {
if (mth.isConstructor()) {
if (ctr != null) {
ctr = null;
break;
}
ctr = mth;
}
}
return ctr != null && ctr.getUseIn().size() == 1;
}
MethodNode useMth = cls.getUseInMth().get(0);
// allow use in enum class init
return useMth.getMethodInfo().isClassInit() && useMth.getParentClass().isEnum();
}
return false;
}
private static boolean isAnonymous(ClassNode cls) {
return cls.getClassInfo().isInner()
&& Character.isDigit(cls.getClassInfo().getShortName().charAt(0))
&& cls.getMethods().stream().filter(MethodNode::isConstructor).count() == 1;
}
private static boolean isLambdaCls(ClassNode cls) {
return cls.getAccessFlags().isSynthetic()
&& cls.getAccessFlags().isFinal()
&& cls.getClassInfo().getRawName().contains(".-$$Lambda$")
&& countStaticFields(cls) == 0;
}
private static int countStaticFields(ClassNode cls) {
int c = 0;
for (FieldNode field : cls.getFields()) {
if (field.getAccessFlags().isStatic()) {
c++;
/**
* Checks:
* - class have only one constructor which used only once (allow common code for field init)
* - methods or fields not used outside (allow only nested inner classes with synthetic usage)
*
* @return anonymous constructor method
*/
private static MethodNode checkUsage(ClassNode cls) {
MethodNode ctr = ListUtils.filterOnlyOne(cls.getMethods(), MethodNode::isConstructor);
if (ctr == null) {
return null;
}
if (ctr.getUseIn().size() != 1) {
// check if used in common field init in all constructors
if (!checkForCommonFieldInit(ctr)) {
return null;
}
}
return c;
MethodNode ctrUseMth = ctr.getUseIn().get(0);
ClassNode ctrUseCls = ctrUseMth.getParentClass();
if (ctrUseCls.equals(cls)) {
// exclude self usage
return null;
}
for (MethodNode mth : cls.getMethods()) {
if (mth == ctr) {
continue;
}
for (MethodNode useMth : mth.getUseIn()) {
if (useMth.equals(ctrUseMth)) {
continue;
}
if (badMethodUsage(cls, useMth, mth.getAccessFlags())) {
return null;
}
}
}
for (FieldNode field : cls.getFields()) {
for (MethodNode useMth : field.getUseIn()) {
if (badMethodUsage(cls, useMth, field.getAccessFlags())) {
return null;
}
}
}
return ctr;
}
private static boolean badMethodUsage(ClassNode cls, MethodNode useMth, AccessInfo accessFlags) {
ClassNode useCls = useMth.getParentClass();
if (useCls.equals(cls)) {
return false;
}
if (accessFlags.isSynthetic()) {
// allow synthetic usage in inner class
return !useCls.getParentClass().equals(cls);
}
return true;
}
/**
* Checks:
* + all in constructors
* + all usage in one class
* - same field put (ignored: methods not loaded yet)
*/
private static boolean checkForCommonFieldInit(MethodNode ctrMth) {
List<MethodNode> ctrUse = ctrMth.getUseIn();
if (ctrUse.isEmpty()) {
return false;
}
ClassNode firstUseCls = ctrUse.get(0).getParentClass();
return ListUtils.allMatch(ctrUse, m -> m.isConstructor() && m.getParentClass().equals(firstUseCls));
}
@Nullable
private static ArgType getBaseType(ClassNode cls) {
int interfacesCount = cls.getInterfaces().size();
if (interfacesCount > 1) {
return null;
}
ArgType superCls = cls.getSuperClass();
if (superCls == null || superCls.equals(ArgType.OBJECT)) {
if (interfacesCount == 1) {
return cls.getInterfaces().get(0);
}
return ArgType.OBJECT;
}
if (interfacesCount == 0) {
return superCls;
}
// check if super class already implement that interface (weird case)
ArgType interfaceType = cls.getInterfaces().get(0);
if (cls.root().getClsp().isImplements(superCls.getObject(), interfaceType.getObject())) {
return superCls;
}
return null;
}
}
@@ -0,0 +1,60 @@
package jadx.core.dex.visitors;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.visitors.usage.UsageInfoVisitor;
import jadx.core.utils.ListUtils;
import jadx.core.utils.exceptions.JadxException;
@JadxVisitor(
name = "ProcessMethodsForInline",
desc = "Mark methods for future inline",
runAfter = {
UsageInfoVisitor.class
}
)
public class ProcessMethodsForInline extends AbstractVisitor {
private boolean inlineMethods;
@Override
public void init(RootNode root) {
inlineMethods = root.getArgs().isInlineMethods();
}
@Override
public boolean visit(ClassNode cls) throws JadxException {
if (!inlineMethods) {
return false;
}
for (MethodNode mth : cls.getMethods()) {
if (canInline(mth)) {
mth.add(AFlag.METHOD_CANDIDATE_FOR_INLINE);
fixClassDependencies(mth);
}
}
return true;
}
private static boolean canInline(MethodNode mth) {
if (mth.isNoCode() || mth.contains(AFlag.DONT_GENERATE)) {
return false;
}
AccessInfo accessFlags = mth.getAccessFlags();
boolean isSynthetic = accessFlags.isSynthetic() || mth.getName().contains("$");
return isSynthetic && accessFlags.isStatic();
}
private static void fixClassDependencies(MethodNode mth) {
ClassNode parentClass = mth.getTopParentClass();
for (MethodNode useInMth : mth.getUseIn()) {
// remove possible cross dependency to force class with inline method to be processed before its
// usage
ClassNode useTopCls = useInMth.getTopParentClass();
parentClass.setDependencies(ListUtils.safeRemoveAndTrim(parentClass.getDependencies(), useTopCls));
}
}
}
@@ -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);
@@ -154,7 +163,7 @@ public class SignatureProcessor extends AbstractVisitor {
return newArgTypes;
}
}
mth.addWarnComment("Incorrect args count in method signature: " + sp.getSignature());
mth.addDebugComment("Incorrect args count in method signature: " + sp.getSignature());
return null;
}
for (int i = 0; i < len; i++) {
@@ -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);
}
}
@@ -38,6 +38,7 @@ import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.regions.conditions.IfCondition;
import jadx.core.dex.visitors.shrink.CodeShrinkVisitor;
import jadx.core.dex.visitors.typeinference.TypeCompareEnum;
import jadx.core.utils.BlockUtils;
import jadx.core.utils.InsnList;
import jadx.core.utils.InsnRemover;
@@ -82,7 +83,7 @@ public class SimplifyVisitor extends AbstractVisitor {
for (int i = 0; i < list.size(); i++) {
InsnNode insn = list.get(i);
int insnCount = list.size();
InsnNode modInsn = simplifyInsn(mth, insn);
InsnNode modInsn = simplifyInsn(mth, insn, null);
if (modInsn != null) {
modInsn.rebindArgs();
if (i < list.size() && list.get(i) == insn) {
@@ -110,7 +111,7 @@ public class SimplifyVisitor extends AbstractVisitor {
for (InsnArg arg : insn.getArguments()) {
if (arg.isInsnWrap()) {
InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn();
InsnNode replaceInsn = simplifyInsn(mth, wrapInsn);
InsnNode replaceInsn = simplifyInsn(mth, wrapInsn, insn);
if (replaceInsn != null) {
arg.wrapInstruction(mth, replaceInsn);
InsnRemover.unbindInsn(mth, wrapInsn);
@@ -123,7 +124,7 @@ public class SimplifyVisitor extends AbstractVisitor {
}
}
private InsnNode simplifyInsn(MethodNode mth, InsnNode insn) {
private InsnNode simplifyInsn(MethodNode mth, InsnNode insn, @Nullable InsnNode parentInsn) {
if (insn.contains(AFlag.DONT_GENERATE)) {
return null;
}
@@ -146,8 +147,9 @@ public class SimplifyVisitor extends AbstractVisitor {
case SPUT:
return convertFieldArith(mth, insn);
case CAST:
case CHECK_CAST:
return processCast(mth, (IndexInsnNode) insn);
return processCast(mth, (IndexInsnNode) insn, parentInsn);
case MOVE:
InsnArg firstArg = insn.getArg(0);
@@ -212,7 +214,7 @@ public class SimplifyVisitor extends AbstractVisitor {
return null;
}
private static InsnNode processCast(MethodNode mth, IndexInsnNode castInsn) {
private static InsnNode processCast(MethodNode mth, IndexInsnNode castInsn, @Nullable InsnNode parentInsn) {
if (castInsn.contains(AFlag.EXPLICIT_CAST)) {
return null;
}
@@ -229,7 +231,8 @@ public class SimplifyVisitor extends AbstractVisitor {
ArgType castToType = (ArgType) castInsn.getIndex();
if (!ArgType.isCastNeeded(mth.root(), argType, castToType)
|| isCastDuplicate(castInsn)) {
|| isCastDuplicate(castInsn)
|| shadowedByOuterCast(mth.root(), castToType, parentInsn)) {
InsnNode insnNode = new InsnNode(InsnType.MOVE, 1);
insnNode.setOffset(castInsn.getOffset());
insnNode.setResult(castInsn.getResult());
@@ -254,6 +257,15 @@ public class SimplifyVisitor extends AbstractVisitor {
return false;
}
private static boolean shadowedByOuterCast(RootNode root, ArgType castType, @Nullable InsnNode parentInsn) {
if (parentInsn != null && parentInsn.getType() == InsnType.CAST) {
ArgType parentCastType = (ArgType) ((IndexInsnNode) parentInsn).getIndex();
TypeCompareEnum result = root.getTypeCompare().compareTypes(parentCastType, castType);
return result.isNarrow();
}
return false;
}
/**
* Simplify 'cmp' instruction in if condition
*/
@@ -532,32 +544,44 @@ public class SimplifyVisitor extends AbstractVisitor {
if (arith.getArgsCount() != 2) {
return null;
}
InsnArg litArg = null;
LiteralArg litArg = null;
InsnArg secondArg = arith.getArg(1);
if (secondArg.isInsnWrap()) {
InsnNode wr = ((InsnWrapArg) secondArg).getWrapInsn();
if (wr.getType() == InsnType.CONST) {
litArg = wr.getArg(0);
InsnArg arg = wr.getArg(0);
if (arg.isLiteral()) {
litArg = (LiteralArg) arg;
}
}
} else if (secondArg.isLiteral()) {
litArg = secondArg;
litArg = (LiteralArg) secondArg;
}
if (litArg != null) {
long lit = ((LiteralArg) litArg).getLiteral();
// fix 'c + (-1)' => 'c - (1)'
if (arith.getOp() == ArithOp.ADD && lit < 0) {
return new ArithNode(ArithOp.SUB,
arith.getResult(), arith.getArg(0),
InsnArg.lit(-lit, litArg.getType()));
}
InsnArg firstArg = arith.getArg(0);
if (arith.getOp() == ArithOp.XOR && firstArg.getType() == ArgType.BOOLEAN
&& (lit == 0 || lit == 1)) {
InsnNode node = new InsnNode(lit == 0 ? InsnType.MOVE : InsnType.NOT, 1);
node.setResult(arith.getResult());
node.addArg(firstArg);
return node;
}
if (litArg == null) {
return null;
}
switch (arith.getOp()) {
case ADD:
// fix 'c + (-1)' to 'c - (1)'
if (litArg.isNegative()) {
LiteralArg negLitArg = litArg.negate();
if (negLitArg != null) {
return new ArithNode(ArithOp.SUB, arith.getResult(), arith.getArg(0), negLitArg);
}
}
break;
case XOR:
// simplify xor on boolean
InsnArg firstArg = arith.getArg(0);
long lit = litArg.getLiteral();
if (firstArg.getType() == ArgType.BOOLEAN && (lit == 0 || lit == 1)) {
InsnNode node = new InsnNode(lit == 0 ? InsnType.MOVE : InsnType.NOT, 1);
node.setResult(arith.getResult());
node.addArg(firstArg);
return node;
}
break;
}
return null;
}
@@ -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;
@@ -0,0 +1,226 @@
package jadx.core.dex.visitors.kotlin;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.JadxArgs.UseKotlinMethodsForVarNames;
import jadx.api.plugins.input.data.attributes.JadxAttrType;
import jadx.core.deobf.NameMapper;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.info.FieldInfo;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.instructions.ConstStringNode;
import jadx.core.dex.instructions.IndexInsnNode;
import jadx.core.dex.instructions.InsnType;
import jadx.core.dex.instructions.InvokeNode;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.InsnWrapArg;
import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.visitors.AbstractVisitor;
import jadx.core.dex.visitors.InitCodeVariables;
import jadx.core.dex.visitors.JadxVisitor;
import jadx.core.dex.visitors.debuginfo.DebugInfoApplyVisitor;
import jadx.core.dex.visitors.rename.CodeRenameVisitor;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxException;
@JadxVisitor(
name = "ProcessKotlinInternals",
desc = "Use variable names from Kotlin intrinsic1 methods",
runAfter = {
InitCodeVariables.class,
DebugInfoApplyVisitor.class
},
runBefore = {
CodeRenameVisitor.class
}
)
public class ProcessKotlinInternals extends AbstractVisitor {
private static final Logger LOG = LoggerFactory.getLogger(ProcessKotlinInternals.class);
private static final String KOTLIN_INTERNAL_PKG = "kotlin.jvm.internal.";
private static final String KOTLIN_INTRINSICS_CLS = KOTLIN_INTERNAL_PKG + "Intrinsics";
private static final String KOTLIN_VARNAME_SOURCE_MTH1 = "(Ljava/lang/Object;Ljava/lang/String;)V";
private static final String KOTLIN_VARNAME_SOURCE_MTH2 = "(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)V";
private @Nullable ClassInfo kotlinIntrinsicsCls;
private Set<MethodInfo> kotlinVarNameSourceMethods;
private boolean hideInsns;
@Override
public void init(RootNode root) throws JadxException {
ClassNode kotlinCls = searchKotlinIntrinsicsClass(root);
if (kotlinCls != null) {
kotlinIntrinsicsCls = kotlinCls.getClassInfo();
kotlinVarNameSourceMethods = collectMethods(kotlinCls);
LOG.debug("Kotlin Intrinsics class: {}, methods: {}", kotlinCls, kotlinVarNameSourceMethods.size());
} else {
kotlinIntrinsicsCls = null;
LOG.debug("Kotlin Intrinsics class not found");
}
hideInsns = root.getArgs().getUseKotlinMethodsForVarNames() == UseKotlinMethodsForVarNames.APPLY_AND_HIDE;
}
@Override
public boolean visit(ClassNode cls) {
if (kotlinIntrinsicsCls == null) {
return false;
}
for (MethodNode mth : cls.getMethods()) {
processMth(mth);
}
return true;
}
private void processMth(MethodNode mth) {
if (mth.isNoCode()) {
return;
}
for (BlockNode block : mth.getBasicBlocks()) {
for (InsnNode insn : block.getInstructions()) {
if (insn.getType() == InsnType.INVOKE) {
try {
processInvoke(mth, insn);
} catch (Exception e) {
mth.addWarnComment("Failed to extract var names", e);
}
}
}
}
}
private void processInvoke(MethodNode mth, InsnNode insn) {
int argsCount = insn.getArgsCount();
if (argsCount < 2) {
return;
}
MethodInfo invokeMth = ((InvokeNode) insn).getCallMth();
if (!kotlinVarNameSourceMethods.contains(invokeMth)) {
return;
}
InsnArg firstArg = insn.getArg(0);
if (!firstArg.isRegister()) {
return;
}
RegisterArg varArg = (RegisterArg) firstArg;
boolean renamed = false;
if (argsCount == 2) {
String str = getConstString(mth, insn, 1);
if (str != null) {
renamed = checkAndRename(varArg, str);
}
} else if (argsCount == 3) {
// TODO: use second arg for rename class
String str = getConstString(mth, insn, 2);
if (str != null) {
renamed = checkAndRename(varArg, str);
}
}
if (renamed && hideInsns) {
insn.add(AFlag.DONT_GENERATE);
}
}
private boolean checkAndRename(RegisterArg arg, String str) {
String name = trimName(str);
if (NameMapper.isValidAndPrintable(name)) {
arg.getSVar().getCodeVar().setName(name);
return true;
}
return false;
}
@Nullable
private String getConstString(MethodNode mth, InsnNode insn, int arg) {
InsnArg strArg = insn.getArg(arg);
if (!strArg.isInsnWrap()) {
return null;
}
InsnNode constInsn = ((InsnWrapArg) strArg).getWrapInsn();
InsnType insnType = constInsn.getType();
if (insnType == InsnType.CONST_STR) {
return ((ConstStringNode) constInsn).getString();
}
if (insnType == InsnType.SGET) {
// revert const field inline :(
FieldInfo fieldInfo = (FieldInfo) ((IndexInsnNode) constInsn).getIndex();
FieldNode fieldNode = mth.root().resolveField(fieldInfo);
if (fieldNode != null) {
String str = (String) fieldNode.get(JadxAttrType.CONSTANT_VALUE).getValue();
InsnArg newArg = InsnArg.wrapArg(new ConstStringNode(str));
insn.replaceArg(strArg, newArg);
return str;
}
}
return null;
}
private String trimName(String str) {
if (str.startsWith("$this$")) {
return str.substring(6);
}
if (str.startsWith("$")) {
return str.substring(1);
}
return str;
}
@Nullable
private static ClassNode searchKotlinIntrinsicsClass(RootNode root) {
ClassNode kotlinCls = root.resolveClass(KOTLIN_INTRINSICS_CLS);
if (kotlinCls != null) {
return kotlinCls;
}
List<ClassNode> candidates = new ArrayList<>();
for (ClassNode cls : root.getClasses()) {
if (isKotlinIntrinsicsClass(cls)) {
candidates.add(cls);
}
}
return Utils.getOne(candidates);
}
private static boolean isKotlinIntrinsicsClass(ClassNode cls) {
if (!cls.getClassInfo().getFullName().startsWith(KOTLIN_INTERNAL_PKG)) {
return false;
}
if (cls.getMethods().size() < 5) {
return false;
}
int mthCount = 0;
for (MethodNode mth : cls.getMethods()) {
if (mth.getAccessFlags().isStatic()
&& mth.getMethodInfo().getShortId().endsWith(KOTLIN_VARNAME_SOURCE_MTH1)) {
mthCount++;
}
}
return mthCount > 2;
}
private Set<MethodInfo> collectMethods(ClassNode kotlinCls) {
Set<MethodInfo> set = new HashSet<>();
for (MethodNode mth : kotlinCls.getMethods()) {
if (!mth.getAccessFlags().isStatic()) {
continue;
}
String shortId = mth.getMethodInfo().getShortId();
if (shortId.endsWith(KOTLIN_VARNAME_SOURCE_MTH1) || shortId.endsWith(KOTLIN_VARNAME_SOURCE_MTH2)) {
set.add(mth.getMethodInfo());
}
}
return set;
}
}
@@ -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) {
@@ -98,6 +110,10 @@ public class TypeCompare {
|| secondPrimitiveType == PrimitiveType.BOOLEAN) {
return CONFLICT;
}
if (swapEquals(firstPrimitiveType, secondPrimitiveType, PrimitiveType.CHAR, PrimitiveType.BYTE)
|| swapEquals(firstPrimitiveType, secondPrimitiveType, PrimitiveType.CHAR, PrimitiveType.SHORT)) {
return CONFLICT;
}
return firstPrimitiveType.compareTo(secondPrimitiveType) > 0 ? WIDER : NARROW;
}
@@ -105,6 +121,10 @@ public class TypeCompare {
return TypeCompareEnum.CONFLICT;
}
private boolean swapEquals(PrimitiveType first, PrimitiveType second, PrimitiveType a, PrimitiveType b) {
return (first == a && second == b) || (first == b && second == a);
}
private TypeCompareEnum compareArrayWithOtherType(ArgType array, ArgType other) {
if (!other.isTypeKnown()) {
if (other.contains(PrimitiveType.ARRAY)) {
@@ -151,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();
@@ -254,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,6 +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.AnonymousClassAttr;
import jadx.core.dex.attributes.nodes.PhiListAttr;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.instructions.ArithNode;
@@ -35,12 +36,15 @@ import jadx.core.dex.instructions.args.LiteralArg;
import jadx.core.dex.instructions.args.PrimitiveType;
import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.instructions.args.SSAVar;
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.IMethodDetails;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.nodes.utils.MethodUtils;
import jadx.core.dex.trycatch.ExcHandlerAttr;
import jadx.core.dex.visitors.AbstractVisitor;
import jadx.core.dex.visitors.AttachMethodDetails;
@@ -83,6 +87,7 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
this::tryDeduceTypes,
this::trySplitConstInsns,
this::tryToFixIncompatiblePrimitives,
this::tryToForceImmutableTypes,
this::tryInsertAdditionalMove,
this::runMultiVariableSearch,
this::tryRemoveGenerics);
@@ -273,6 +278,11 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
addBound(typeInfo, new TypeBoundConst(BoundEnum.ASSIGN, clsType));
break;
case CONSTRUCTOR:
ArgType ctrClsType = replaceAnonymousType((ConstructorInsn) insn);
addBound(typeInfo, new TypeBoundConst(BoundEnum.ASSIGN, ctrClsType));
break;
case CONST:
LiteralArg constLit = (LiteralArg) insn.getArg(0);
addBound(typeInfo, new TypeBoundConst(BoundEnum.ASSIGN, constLit.getType()));
@@ -308,6 +318,19 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
}
}
private ArgType replaceAnonymousType(ConstructorInsn ctr) {
if (ctr.isNewInstance()) {
ClassNode ctrCls = root.resolveClass(ctr.getClassType());
if (ctrCls != null && ctrCls.contains(AFlag.DONT_GENERATE)) {
AnonymousClassAttr baseTypeAttr = ctrCls.get(AType.ANONYMOUS_CLASS);
if (baseTypeAttr != null) {
return baseTypeAttr.getBaseType();
}
}
}
return ctr.getClassType().getType();
}
private ITypeBound makeAssignFieldGetBound(IndexInsnNode insn) {
ArgType initType = insn.getResult().getInitType();
if (initType.containsTypeVariable()) {
@@ -340,7 +363,7 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
return null;
}
if (insn instanceof BaseInvokeNode) {
TypeBoundInvokeUse invokeUseBound = makeInvokeUseBound(regArg, (BaseInvokeNode) insn);
ITypeBound invokeUseBound = makeInvokeUseBound(regArg, (BaseInvokeNode) insn);
if (invokeUseBound != null) {
return invokeUseBound;
}
@@ -352,21 +375,32 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
return new TypeBoundConst(BoundEnum.USE, regArg.getInitType(), regArg);
}
private TypeBoundInvokeUse makeInvokeUseBound(RegisterArg regArg, BaseInvokeNode invoke) {
private ITypeBound makeInvokeUseBound(RegisterArg regArg, BaseInvokeNode invoke) {
InsnArg instanceArg = invoke.getInstanceArg();
if (instanceArg == null || instanceArg == regArg) {
if (instanceArg == null) {
return null;
}
IMethodDetails methodDetails = root.getMethodUtils().getMethodDetails(invoke);
MethodUtils methodUtils = root.getMethodUtils();
IMethodDetails methodDetails = methodUtils.getMethodDetails(invoke);
if (methodDetails == null) {
return null;
}
int argIndex = invoke.getArgIndex(regArg) - invoke.getFirstArgOffset();
ArgType argType = methodDetails.getArgTypes().get(argIndex);
if (!argType.containsTypeVariable()) {
return null;
if (instanceArg != regArg) {
int argIndex = invoke.getArgIndex(regArg) - invoke.getFirstArgOffset();
ArgType argType = methodDetails.getArgTypes().get(argIndex);
if (!argType.containsTypeVariable()) {
return null;
}
return new TypeBoundInvokeUse(root, invoke, regArg, argType);
}
return new TypeBoundInvokeUse(root, invoke, regArg, argType);
// for override methods use origin declared class as type
if (methodDetails instanceof MethodNode) {
MethodNode callMth = (MethodNode) methodDetails;
ClassInfo declCls = methodUtils.getMethodOriginDeclClass(callMth);
return new TypeBoundConst(BoundEnum.USE, declCls.getType(), regArg);
}
return null;
}
private boolean tryPossibleTypes(MethodNode mth, SSAVar var, ArgType type) {
@@ -802,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()) {
@@ -809,6 +844,7 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
if (!boundType.contains(PrimitiveType.BOOLEAN)) {
return false;
}
assigned = true;
break;
case USE:
if (!boundType.canBeAnyNumber()) {
@@ -817,6 +853,9 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
break;
}
}
if (!assigned) {
return false;
}
boolean fixed = false;
for (ITypeBound bound : typeInfo.getBounds()) {
@@ -899,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();
}
}
@@ -8,7 +8,6 @@ import java.util.HashSet;
import java.util.Set;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -24,6 +23,7 @@ import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
import jadx.core.xmlgen.ResContainer;
import jadx.core.xmlgen.XmlSecurity;
public class ExportGradleProject {
@@ -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")) {
@@ -139,7 +140,7 @@ public class ExportGradleProject {
private Document parseXml(String xmlContent) {
try {
DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
DocumentBuilder builder = XmlSecurity.getSecureDbf().newDocumentBuilder();
Document document = builder.parse(new InputSource(new StringReader(xmlContent)));
document.getDocumentElement().normalize();
@@ -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,12 +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) {
@@ -47,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));
}
@@ -62,8 +75,100 @@ public class ListUtils {
newList.add(newObj);
return newList;
}
list.remove(oldObj);
list.add(newObj);
int idx = list.indexOf(oldObj);
if (idx != -1) {
list.set(idx, newObj);
} else {
list.add(newObj);
}
return list;
}
public static <T> void safeRemove(List<T> list, T obj) {
if (list != null && !list.isEmpty()) {
list.remove(obj);
}
}
public static <T> List<T> safeRemoveAndTrim(List<T> list, T obj) {
if (list == null || list.isEmpty()) {
return list;
}
if (list.remove(obj)) {
if (list.isEmpty()) {
return Collections.emptyList();
}
}
return list;
}
public static <T> List<T> safeAdd(List<T> list, T obj) {
if (list == null || list.isEmpty()) {
List<T> newList = new ArrayList<>(1);
newList.add(obj);
return newList;
}
list.add(obj);
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
*
* @return null if found not exactly one element (zero or more than one)
*/
@Nullable
public static <T> T filterOnlyOne(List<T> list, Predicate<T> filter) {
if (list == null || list.isEmpty()) {
return null;
}
T found = null;
for (T element : list) {
if (filter.test(element)) {
if (found != null) {
// found second
return null;
}
found = element;
}
}
return found;
}
public static <T> boolean allMatch(List<T> list, Predicate<T> test) {
if (list == null || list.isEmpty()) {
return false;
}
for (T element : list) {
if (!test.test(element)) {
return false;
}
}
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;
}
}
@@ -8,10 +8,12 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import org.jetbrains.annotations.Nullable;
@@ -284,6 +286,19 @@ public class Utils {
return result;
}
public static <T> Set<T> mergeSets(Set<T> first, Set<T> second) {
if (isEmpty(first)) {
return second;
}
if (isEmpty(second)) {
return first;
}
Set<T> result = new HashSet<>(first.size() + second.size());
result.addAll(first);
result.addAll(second);
return result;
}
public static Map<String, String> newConstStringMap(String... parameters) {
int len = parameters.length;
if (len == 0) {
@@ -338,6 +353,14 @@ public class Utils {
return list.get(0);
}
@Nullable
public static <T> T getOne(@Nullable Collection<T> collection) {
if (collection == null || collection.size() != 1) {
return null;
}
return collection.iterator().next();
}
@Nullable
public static <T> T first(List<T> list) {
if (list.isEmpty()) {

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