Compare commits

...

132 Commits

Author SHA1 Message Date
Jan S. 1d5d6b0100 fix(gui): make the Split view checkbox correctly toggle between both modes (PR #2801)
Build Test / tests (ubuntu-latest) (pull_request) Has been cancelled
Build Test / tests (windows-latest) (pull_request) Has been cancelled
2026-02-24 20:39:35 +00:00
Skylot 344270a0ba build: run test actions for 'stable' branch on push 2026-02-24 20:39:35 +00:00
Skylot 9957f694b9 fix(gui): use UI thread for scroll in code (#2798) 2026-02-23 21:24:05 +00:00
Skylot 4b9d69a169 fix(gui): stop token process on NULL token in JadxTokenMaker (#2798) 2026-02-23 21:24:00 +00:00
Skylot 1730dd83bf fix(gui): resolve possible NPE during project data loading (#2794) 2026-02-23 21:23:46 +00:00
Skylot a3a3b492dd fix: insert generic casts for variable assigned from fields with known types (#2776) 2026-02-23 21:20:41 +00:00
Skylot 2a2806ebd7 feat(plugins): allow to use .zip as plugin artifact (with jars inside) 2026-02-12 19:38:34 +00:00
Skylot c7a0f7a092 feat: make jadx-script-kotlin plugin external 2026-02-12 19:38:33 +00:00
Jan S. 9710ebe09a fix(res): back to the old correct resource ID calculation (PR #2788)
fix: back to the old correct resourceID calculation
2026-02-12 17:27:53 +00:00
Jan S. f0da486703 fix(gui): go to main activity and application class when class name was renamed by deobfuscator (PR #2779)
fix(gui): go to main activity and application class when class name was renamed by deobfuscator
2026-02-08 18:21:50 +00:00
Jan S. 925503ba04 fix(res): prevent problems arising by parsing duplicate resource entries from resources.arsc (#2775)(PR #2777)
* fix(core): prevent resource name collisions by reading the same resource multiple times

* fix(core): prevent resource names getting longer by every rename operation

* initialize HashSet size properly

* formatting
2026-02-07 17:54:50 +00:00
Ruffalo Lavoisier 039900a278 fix(zip): check uncompressed size exceeds the maximum value of an integer (PR #2773)
* check whether it exceeds the maximum value of an integer
---------

Co-authored-by: skylot <118523+skylot@users.noreply.github.com>
2026-02-04 20:29:16 +00:00
Skylot d73455c689 build(github): remove CodeQL action 2026-02-02 21:57:46 +00:00
Skylot 07b3e5a7c0 fix(gui): limit tabs title length, fix tooltips (#2771) 2026-02-02 21:09:15 +00:00
Skylot c1fc73a524 fix(gui): use new resource class for files in arsc (#2771) 2026-02-01 20:01:17 +00:00
Jan S. 0d982e9709 fix(gui): use resource short name when exporting a folder via context menu 2026-02-01 18:21:19 +00:00
Jan S. e6b38e172a fix(gui): fix illegal ':' character in path when exporting resources.arsc/res 2026-02-01 18:21:19 +00:00
Skylot 331c4aaa5e fix(gui): load class code before resolving jump position to get corrent value 2026-01-26 20:13:38 +00:00
Skylot 4e82233cbc fix(java-input): fix dup2_x1 stack to regs conversion for wide types (#2755) 2026-01-26 20:13:38 +00:00
Skylot ad267e1618 fix: improve switch over string restore (#2359) 2026-01-21 20:07:58 +00:00
ewt45 54265e34e5 fix: avoid more false positive throws (PR #2752)
Update MethodThrowsVisitor.java
2026-01-21 18:43:00 +00:00
ewt45 c677901aa5 fix(gui): use FontUtils.getCompositeFont() that supports CJK (PR #2751)
fix: use FontUtils.getCompositeFont() that supports CJK
2026-01-20 17:58:08 +00:00
ewt45 220a2198a1 fix: add more checks to better find handler's end (PR #2749) 2026-01-19 17:39:03 +00:00
Skylot b725dd18b6 fix: terminate type inference on first stack overflow (#2744) 2026-01-18 17:53:07 +00:00
Jan S. a0466d4494 fix: speed up file path security checks (PR #2745)
chore: Speed up path traversal check by using java.nio.file
2026-01-16 18:36:48 +00:00
Skylot ae1a5e9277 fix: improve codegen methods for custom decompilation mode 2026-01-12 19:47:52 +00:00
Skylot 018c20187d fix: keep wide upcast for arith instructions (#2730) 2026-01-12 18:40:01 +00:00
Skylot add11dff1d fix: add missing level info for jadx code comments (#2737) 2026-01-11 18:44:05 +00:00
Jan S. 005a59668c fix(res): as workaround use INVALID_STRING_PLACEHOLDER when string offset is is negative (#2729)(PR #2739)
core: as workaround use INVALID_STRING_PLACEHOLDER when string offset is negative
2026-01-07 16:22:18 +00:00
Deleted user 8f727325d6 fix(cli): distinguish JadxCLI error code (PR #2734) 2026-01-04 16:28:31 +00:00
Skylot 7bbb58863b fix(gui): don't init already loaded plugins while collecting options (#2727) 2025-12-24 20:28:21 +00:00
Skylot 8629e4cf22 tests(cli): use provided plugin loader to not use global plugins in tests 2025-12-23 21:40:35 +00:00
Skylot bd5c6b6516 fix(plugins): use class with correct fields for plugins list entry 2025-12-23 21:27:40 +00:00
Skylot 85a7aaa9fb fix(gui): do not enforce new nodes method for backward compatibility 2025-12-23 20:48:40 +00:00
Skylot a3df83e60f feat(gui): report plugin exceptions to plugin github project 2025-12-23 20:41:44 +00:00
Skylot b3be2794a0 fix(plugins): reduce plugins-list update checks 2025-12-22 21:53:54 +00:00
Skylot 02ea3be8f2 fix(plugins): improve errors handling and reporting (#2597) 2025-12-22 21:53:54 +00:00
Skylot b78745a87b fix(gui): reset selected tab on last tabs closed 2025-12-22 21:53:37 +00:00
Skylot 01af6481f6 fix(gui): handle config save error, don't write partial config 2025-12-21 19:33:21 +00:00
Skylot 0c3e6e77cd feat(gui): change fonts size with UI zoom, use FlatLaf fonts bundle 2025-12-20 20:18:31 +00:00
Skylot 8b48219dc3 chore: update dependencies 2025-12-19 18:57:41 +00:00
dependabot[bot] 80187c7e29 build(deps): bump actions/upload-artifact from 5 to 6 (#2724)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-15 21:59:33 +00:00
dependabot[bot] 22f7b40151 build(deps): bump actions/download-artifact from 6 to 7 (#2725)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-15 21:59:16 +00:00
Skylot 3e709d6693 feat(cli): implement config file load and save (#1731) 2025-12-15 21:07:10 +00:00
Skylot 1aa16c4664 feat(gui): add option for UI zoom factor (#2720) 2025-12-10 18:43:54 +00:00
Skylot 9fba709687 chore: update some dependencies 2025-12-08 20:11:16 +00:00
RedArms 6b61599114 fix(xapk): use stream copy to prevent OOM on large files (#2619) (PR #2719) 2025-12-08 19:00:57 +00:00
Skylot 2829e284f3 fix: prevent endless loop in region maker on dead code with loops (#2715) 2025-12-04 20:45:02 +00:00
RedArms 27b15aeb1c fix(gui): resolve NPE with smali breakpoint (#2717) (PR #2718)
* fix(gui): resolve exception in search dialog on project reload (#2714)

* Revert "fix(gui): resolve exception in search dialog on project reload (#2714)"

This reverts commit 74c52a8884.

---------

Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
2025-12-04 19:25:45 +00:00
Skylot 74c52a8884 fix(gui): resolve exception in search dialog on project reload (#2714) 2025-12-03 19:38:27 +00:00
Skylot 39e7b82353 fix(gui): in comment dialog insert new line at current caret position (#2706) 2025-12-03 19:32:52 +00:00
Skylot 365e258180 fix: resolve edge cases for select best classes from duplicates (#2701) 2025-12-03 19:21:19 +00:00
Midori Kochiya ea68024851 fix(dex-input): use length in header for checksum, fix error in Dex v41 (PR #2711)
* Use length in header for checksum, fix error in Dex v41

* check if length can be read, use utils method to read int values

---------

Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
2025-12-02 19:14:51 +00:00
Skylot 56ae4a6048 fix: refactor and improve class duplicates removal (#2701) 2025-11-29 22:51:27 +03:00
Histausse 1d831d82d4 feat: select better class from duplicates (#2701)(PR #2702)
* select the right class in in case of duplicate

* select correct class in gui in case of duplication

* fix code format

---------

Co-authored-by: Jean-Marie 'Histausse' Mineau <histausse@protonmail.com>
2025-11-29 19:50:49 +00:00
dependabot[bot] 4c760f1029 build(deps): bump actions/checkout from 5 to 6 (#2703)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-29 16:26:03 +00:00
Skylot cf0101f13d fix: support 'break' extract for nested 'if' (#2697) 2025-11-22 22:20:32 +00:00
Skylot 748f45b386 chore: update dependencies 2025-11-21 20:29:20 +00:00
Skylot 9e278efc5c chore: update gradle 2025-11-21 20:23:54 +00:00
Skylot cc1beab0b5 fix(gui): use correct threads for code from close and reload actions (#2684) 2025-11-21 20:16:16 +00:00
Skylot 2fba204b33 fix: correctly handle variable in ternary transform 2025-11-20 20:30:10 +00:00
Skylot 9bf079aad4 fix: improve common 'break' extract checks (#2697) 2025-11-20 19:21:58 +00:00
Skylot 6aeaf6aca9 fix: extract common switch break, remove unreachable (#2697) 2025-11-13 20:48:51 +00:00
nitram84 7ea478e18a fix: respect arg offset for type var mapping on invoke (PR #2698)
fix: respect arg offset for type var apping on invoke
2025-11-11 22:01:27 +00:00
Skylot ef99412de1 fix: keep generics while applying debug info (#2687) 2025-11-07 20:27:29 +00:00
Jan S. cda1b1ad2c fix(gui): prevent unlimited recursion in text search (#2685) (PR #2694) 2025-11-06 20:34:42 +00:00
nitram84 05863881f8 fix: detect more for-each loops (also prevent issues with missing generics types) (PR #2689) 2025-11-05 21:04:22 +00:00
Skylot 3e208509e0 fix(gui): rework background executor to reduce delay for short tasks 2025-11-05 19:19:38 +00:00
Skylot bd75baef56 fix(gui): split loading and UI update for code area (#2682) 2025-11-04 20:53:02 +00:00
Skylot d191f62b8d fix: handle OutOfMemoryError to correctly halt processing (#2676) 2025-10-29 21:03:58 +00:00
Skylot f17c46224d fix: improve decoding errors handling (#2676) 2025-10-29 19:08:18 +00:00
Jan S. e008e818b0 fix: enhancements for abnormal situations (PR #2679)
* Allow to disable log panel (OFF)

* Limit the maximum number of garbage collector threads
2025-10-29 19:06:32 +00:00
JustFor 560b1a9ca7 fix(gui): update Chinese localization for various menu items (PR #2673)
translate new texts and update some old texts.
2025-10-27 18:23:55 +00:00
dependabot[bot] 06f113f0a6 build(deps): bump actions/download-artifact from 5 to 6 (#2675)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 18:16:57 +00:00
dependabot[bot] 48a0073002 build(deps): bump actions/upload-artifact from 4 to 5 (#2674)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 18:15:23 +00:00
Jan S. 88e90722ae fix: more file extensions added for file type detection (PR #2670) 2025-10-24 19:49:47 +01:00
Skylot 1becfa9977 fix(gui): use simple token maker as default to avoid parsing error (#2669) 2025-10-23 22:21:30 +01:00
Skylot 20ed13936a build(github): use latest LTS Java 25 2025-10-21 22:46:47 +01:00
Skylot 421de675a2 chore: update dependencies 2025-10-21 22:04:07 +01:00
Skylot 06a118c104 fix(gui): handle syntax parsing errors during search (#2669) 2025-10-21 22:02:41 +01:00
Skylot 2e93656007 fix: catch all exceptions in type update (#2668) 2025-10-21 20:37:43 +01:00
dependabot[bot] c8c4adb60c build(deps): bump github/codeql-action from 3 to 4 (PR #2666)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-20 22:34:53 +01:00
Skylot f2e78fe800 fix(tests): use correct file separator to load resources 2025-10-14 23:31:34 +01:00
Skylot 7172f8df2d chore: update dependencies 2025-10-14 22:56:59 +01:00
Skylot d7c6be5664 fix: resolve rename of inner class using full name (#1997) 2025-10-14 22:56:59 +01:00
Skylot 654cf5e4fb fix(smali-input): use synced list for threaded processing, improve inner classes handling 2025-10-14 22:56:59 +01:00
wech71 bf2d5b5e2e fix(kotlin-metadata): upgrade to kotlin 2.2 metadata library (PR #2655)
* fix Metadata Bytes String reading and upgrade to kotlin 2.2 metadata library which is now finalized and has therefore renamed from kotlinx.metadata to kotlin.metadata

* add test with sample java class

---------

Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
2025-10-12 00:16:57 +01:00
wech71 0f495afc99 fix(java-input): support Java modified UTF-8 strings (PR #2654)
* [core] fix jadx.plugins.input.java.data.ConstPoolReader.parseString() error with Kotlin Annotation of byte array as a String to follow jvms-4.4.7 rules for encoding annotation strings in class files

* move decode method into utility class, add test, fix code style

---------

Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
2025-10-11 23:57:00 +01:00
dependabot[bot] 5f1985f281 build(deps): bump gradle/actions from 4 to 5 (PR #2649)
Bumps [gradle/actions](https://github.com/gradle/actions) from 4 to 5.
- [Release notes](https://github.com/gradle/actions/releases)
- [Commits](https://github.com/gradle/actions/compare/v4...v5)

---
updated-dependencies:
- dependency-name: gradle/actions
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-06 22:29:23 +01:00
o0kam1 2a574d45d5 feat: support '.apks' files (PR #2647) 2025-09-29 18:26:17 +01:00
Skylot 3ee476c6b6 build: update 'beryx.runtime' gradle plugin to fix 'win with jre' build 2025-09-28 17:00:52 +01:00
huqiuser fd3d488016 fix(aab-input): correct check for '.aab' file (PR #2646)
fix: Error decode xml (manifest) when parsing an APK file containing '.aab' in the file path

Co-authored-by: licheng <licheng@cmcm.com>
2025-09-28 16:18:09 +01:00
Skylot 9898cbb4a1 fix: add missing field code annotation for enum fields in switch-case 2025-09-22 22:12:29 +01:00
Skylot 104a0f0636 fix: resolve race condition in task executor 2025-09-22 22:12:29 +01:00
Skylot e58b77267e build: fix distribution artifacts sharing between projects 2025-09-22 22:12:29 +01:00
Skylot 73913651b4 chore: update gradle and dependencies 2025-09-21 19:55:11 +01:00
pubiqq f01e6aa505 chore: update android attrs (platform-36_r02) (PR #2638) 2025-09-17 19:19:15 +01:00
nitram84 d9da6a7f89 fix: avoid false positive throws (PR #2636)(#2475) 2025-09-15 00:08:06 +01:00
Skylot 5726a52ab6 fix: minor fixes for type update limit option (#2629) 2025-09-09 21:16:54 +01:00
Away f61d90ec2f feat: allow to change limit for type inference updates (PR #2629)
* Fix Type inference error: updates count limit reached

* Fix for the "Type inference error: updates count limit reached"
* Users will be able to choose the limit and possibly avoid this error
* Adding options on decompilation settings

* Updating README.md with new type update limit parameter

Updating README.md with new type update limit parameter
2025-09-09 20:50:04 +01:00
dependabot[bot] 2cd112cd3d build(deps): bump actions/github-script from 7 to 8 (#2628)
Bumps [actions/github-script](https://github.com/actions/github-script) from 7 to 8.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-08 19:20:27 +01:00
dependabot[bot] 43358643be build(deps): bump actions/setup-java from 4 to 5 (PR #2614)
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 4 to 5.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](https://github.com/actions/setup-java/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-29 20:41:47 +01:00
dependabot[bot] 1f0d3dac0f build(deps): bump actions/download-artifact from 4 to 5 (#2602)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 09:52:54 +01:00
dependabot[bot] da95a8ae17 build(deps): bump actions/checkout from 4 to 5 (#2603)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 09:51:04 +01:00
Andrei Kudryavtsev da3ac6bff0 fix(gui): various tabs related fixes (PR #2595)
* 1. Fix tab rendering when preview state changed

* 2. Fix selected tab after closing currently active tab

* 3. Fix tab selection when pinning currently active tab

* 4. Make current preview tab permanent on double click

* 5. Fix preview tab font not reloading on settings change
2025-08-04 18:09:22 +01:00
beaverxsheet bdbeaff8f0 fix: use proper newlines when generating CFG (PR #2592)
fixing new lines in the cfg
2025-08-04 18:03:11 +01:00
Jan S. b1f48f1db1 fix(gui): fix NullPointerException in copyAllSearchResults (#2580) (PR #2581) 2025-07-24 18:37:47 +01:00
Skylot 5b09378614 fix: use 'dev.dirs' JNI implementation only on x86-64 (#2578) 2025-07-23 19:26:14 +01:00
Skylot b64c93160b refactor: move system info class to common app lib 2025-07-23 19:24:26 +01:00
Skylot 58c4f56a71 feat(gui): allow view and edit input smali files 2025-07-19 22:16:19 +01:00
Skylot 3d11d1fa87 fix(gui): resolve NPE on code area close 2025-07-19 22:14:30 +01:00
Skylot 0d158592e4 chore: update gradle 2025-07-19 19:14:24 +01:00
Skylot 0c253f9a1f chore: update dependencies 2025-07-19 19:08:18 +01:00
Loyie King f565178c8c feat(aab): enhance AAB parsing (PR #2557)
* fix(aab): resources.pb always NPE

* fix(aab): resources.pb shows wrong resources id

* feat(aab): add parser for native.pb, assets.pb, dependencies.pb

* apply code formating

---------

Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
2025-07-07 21:08:12 +01:00
Skylot f6d13f1860 fix(cli): add missing '-XX:+IgnoreUnrecognizedVMOptions' JVM flag (#2554) 2025-07-06 17:30:50 +01:00
TinyServal d58c9ac926 fix(gui): minor UI fixes (PR #2549)
* fix(gui): use system menu bar on macOS

* fix(gui): font consistency in AboutDialog

* fix(gui): fix horizontal scrolling speed in CommonSearchDialog

* fix(gui): slightly increase debounce timer to reduce UI flickering

* fix(gui): disable tab scroll wrap-around behavior
2025-07-02 20:37:16 +01:00
Skylot 7f9d51b9b1 fix(zip): allow to load zip with duplicate names in entries 2025-06-29 20:44:17 +01:00
Skylot 432e49df03 fix(gui): disable debugger action if project not loaded (#2547) 2025-06-29 19:48:10 +01:00
Skylot b530c234f3 chore: update gradle 2025-06-29 19:48:10 +01:00
Skylot 181dcf7b4f chore: update dependencies 2025-06-29 19:48:10 +01:00
xiaojye dc4dcb2bd0 refactor(gui): improve menu icon, such as 'Select in Tree' and 'Go to AndroidManifest.xml' buttons (PR #2543) 2025-06-26 20:48:23 +01:00
Jan S. 74c396448e fix(gui): prevent IndexOutOfBoundsException in MethodSearchProvider (PR #2542) 2025-06-20 22:37:27 +01:00
Skylot cb9693a9d1 fix: correct hex format for negative numbers (#2531) 2025-06-19 22:49:59 +01:00
Mart Lintz 5d13acc6f3 chore(export): add missing ExpiringTargetSdkVersion in #2533 (PR #2538) 2025-06-17 22:38:02 +01:00
Skylot c04dddfa81 fix(cli): improve memory usage in jadx-cli (#2524) 2025-06-16 21:00:01 +01:00
Jan S. 1bb645d676 fix: correct loading for plain text XML files (PR #2537) 2025-06-16 19:26:35 +01:00
Jan S. ecb597a461 chore(export): disable targetSDKVersion checks in AndroidStudio (PR #2533)
chore: disable targetSDKVersion warnings when opening exported project in AndroidStudio
2025-06-11 23:13:02 +03:00
Akexorcist 46cd3b5597 feat(export): use compileSdkVersion from manifest, update AGP to 8.10.1, other improvements (PR #2528)
* chore(core): export to android project with AGP 8.10.1

* fix code format

* fix export template test

---------

Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
2025-06-07 20:09:29 +01:00
Jeroen Beckers d523f1b15e fix(gui): use var instead let for class variable in Frida script (PR #2527)
Co-authored-by: Jeroen Beckers <dauntless@dauntless.be>
2025-06-07 19:53:19 +01:00
skylot 8030c2f84e feat(gui): new search options to search in text or binary resources (PR #2526)
* search in text or binary resources

* load matching tab for scroll to pos in binary panel, treat unknown files as binary in search
2025-06-07 19:23:08 +01:00
Ruffalo Lavoisier 47224dc599 fix(gui): remove duplicate class of Frida snippet (PR #2525)
fix duplicate class of frida snippet
2025-06-03 17:52:06 +01:00
Yaroslav d492628bfe fix(gui): workaround for wrap layout repaint issue in search dialog (PR #2521)
* fix(gui): fixed incorrectly handles full window repaint in search dialog

* fix: fix spotless check
2025-06-02 21:45:46 +01:00
420 changed files with 21729 additions and 15400 deletions
+10 -10
View File
@@ -8,15 +8,15 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up JDK - name: Set up JDK
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
distribution: temurin distribution: temurin
java-version: 21 java-version: 25
- name: Set jadx version - name: Set jadx version
run: | run: |
@@ -25,7 +25,7 @@ jobs:
echo "JADX_VERSION=$JADX_VERSION" >> $GITHUB_ENV echo "JADX_VERSION=$JADX_VERSION" >> $GITHUB_ENV
- name: Setup Gradle - name: Setup Gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v5
- name: Build - name: Build
run: ./gradlew dist distWin run: ./gradlew dist distWin
@@ -33,7 +33,7 @@ jobs:
JADX_BUILD_JAVA_VERSION: 11 JADX_BUILD_JAVA_VERSION: 11
- name: Save bundle artifact - name: Save bundle artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
with: with:
name: ${{ format('jadx-{0}', env.JADX_VERSION) }} name: ${{ format('jadx-{0}', env.JADX_VERSION) }}
# Waiting fix for https://github.com/actions/upload-artifact/issues/39 to upload zip file # Waiting fix for https://github.com/actions/upload-artifact/issues/39 to upload zip file
@@ -43,7 +43,7 @@ jobs:
retention-days: 14 retention-days: 14
- name: Save Windows bundle artifact - name: Save Windows bundle artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
with: with:
name: ${{ format('jadx-gui-{0}-no-jre-win', env.JADX_VERSION) }} name: ${{ format('jadx-gui-{0}-no-jre-win', env.JADX_VERSION) }}
# Upload unpacked files for now # Upload unpacked files for now
@@ -54,14 +54,14 @@ jobs:
build-win-bundle: build-win-bundle:
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up JDK - name: Set up JDK
uses: oracle-actions/setup-java@v1 uses: oracle-actions/setup-java@v1
with: with:
release: 24 release: 25
- name: Print Java version - name: Print Java version
shell: bash shell: bash
@@ -75,13 +75,13 @@ jobs:
echo "JADX_VERSION=$JADX_VERSION" >> $GITHUB_ENV echo "JADX_VERSION=$JADX_VERSION" >> $GITHUB_ENV
- name: Setup Gradle - name: Setup Gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v5
- name: Build - name: Build
run: ./gradlew dist -PbundleJRE=true run: ./gradlew dist -PbundleJRE=true
- name: Save Windows with JRE bundle artifact - name: Save Windows with JRE bundle artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
with: with:
name: ${{ format('jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }} name: ${{ format('jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }}
# Upload unpacked files for now # Upload unpacked files for now
+5 -6
View File
@@ -2,7 +2,7 @@ name: Build Test
on: on:
push: push:
branches: [ master, build-test ] branches: [ master, stable, build-test ]
pull_request: pull_request:
branches: [ master ] branches: [ master ]
@@ -14,19 +14,18 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- name: Set up JDK - name: Set up JDK
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
distribution: temurin distribution: temurin
java-version: 21 java-version: 25
- name: Setup Gradle - name: Setup Gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v5
- name: Build - name: Build
run: ./gradlew build dist distWin run: ./gradlew build dist distWin
env: env:
JADX_BUILD_JAVA_VERSION: 11 JADX_BUILD_JAVA_VERSION: 11
-41
View File
@@ -1,41 +0,0 @@
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@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
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@v3
+11 -11
View File
@@ -13,28 +13,28 @@ jobs:
build-release-win-bundle: build-release-win-bundle:
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- name: Set up JDK - name: Set up JDK
uses: oracle-actions/setup-java@v1 uses: oracle-actions/setup-java@v1
with: with:
release: 24 release: 25
- name: Set jadx version - name: Set jadx version
uses: actions/github-script@v7 uses: actions/github-script@v8
with: with:
script: | script: |
const jadxVersion = context.ref.split('/').pop().substring(1) const jadxVersion = context.ref.split('/').pop().substring(1)
core.exportVariable('JADX_VERSION', jadxVersion); core.exportVariable('JADX_VERSION', jadxVersion);
- name: Setup Gradle - name: Setup Gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v5
- name: Build - name: Build
run: ./gradlew dist -PbundleJRE=true run: ./gradlew dist -PbundleJRE=true
- name: Save JRE bundle artifact - name: Save JRE bundle artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
with: with:
name: ${{ format('jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }} name: ${{ format('jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }}
path: ${{ format('build/distWinWithJre/jadx-gui-{0}-with-jre-win.zip', env.JADX_VERSION) }} path: ${{ format('build/distWinWithJre/jadx-gui-{0}-with-jre-win.zip', env.JADX_VERSION) }}
@@ -45,23 +45,23 @@ jobs:
needs: build-release-win-bundle needs: build-release-win-bundle
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- name: Set up JDK - name: Set up JDK
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
distribution: temurin distribution: temurin
java-version: 21 java-version: 25
- name: Set jadx version and release name - name: Set jadx version and release name
uses: actions/github-script@v7 uses: actions/github-script@v8
with: with:
script: | script: |
const jadxVersion = context.ref.split('/').pop().substring(1) const jadxVersion = context.ref.split('/').pop().substring(1)
core.exportVariable('JADX_VERSION', jadxVersion); core.exportVariable('JADX_VERSION', jadxVersion);
- name: Setup Gradle - name: Setup Gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v5
- name: Build - name: Build
run: ./gradlew dist distWin run: ./gradlew dist distWin
@@ -69,7 +69,7 @@ jobs:
JADX_BUILD_JAVA_VERSION: 11 JADX_BUILD_JAVA_VERSION: 11
- name: Download Windows JRE bundle - name: Download Windows JRE bundle
uses: actions/download-artifact@v4 uses: actions/download-artifact@v7
with: with:
name: ${{ format('jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }} name: ${{ format('jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }}
path: ${{ format('build/jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }} path: ${{ format('build/jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }}
+107 -96
View File
@@ -91,110 +91,121 @@ commands (use '<command> --help' for command options):
plugins - manage jadx plugins plugins - manage jadx plugins
options: options:
-d, --output-dir - output directory -d, --output-dir - output directory
-ds, --output-dir-src - output directory for sources -ds, --output-dir-src - output directory for sources
-dr, --output-dir-res - output directory for resources -dr, --output-dir-res - output directory for resources
-r, --no-res - do not decode resources -r, --no-res - do not decode resources
-s, --no-src - do not decompile source code -s, --no-src - do not decompile source code
-j, --threads-count - processing threads count, default: 4 -j, --threads-count - processing threads count, default: 16
--single-class - decompile a single class, full name, raw or alias --single-class - decompile a single class, full name, raw or alias
--single-class-output - file or dir for write if decompile a single class --single-class-output - file or dir for write if decompile a single class
--output-format - can be 'java' or 'json', default: java --output-format - can be 'java' or 'json', default: java
-e, --export-gradle - save as gradle project (set '--export-gradle-type' to 'auto') -e, --export-gradle - save as gradle project (set '--export-gradle-type' to 'auto')
--export-gradle-type - Gradle project template for export: --export-gradle-type - Gradle project template for export:
'auto' - detect automatically 'auto' - detect automatically
'android-app' - Android Application (apk) 'android-app' - Android Application (apk)
'android-library' - Android Library (aar) 'android-library' - Android Library (aar)
'simple-java' - simple Java 'simple-java' - simple Java
-m, --decompilation-mode - code output mode: -m, --decompilation-mode - code output mode:
'auto' - trying best options (default) 'auto' - trying best options (default)
'restructure' - restore code structure (normal java code) 'restructure' - restore code structure (normal java code)
'simple' - simplified instructions (linear, with goto's) 'simple' - simplified instructions (linear, with goto's)
'fallback' - raw instructions without modifications 'fallback' - raw instructions without modifications
--show-bad-code - show inconsistent code (incorrectly decompiled) --show-bad-code - show inconsistent code (incorrectly decompiled)
--no-xml-pretty-print - do not prettify XML --no-xml-pretty-print - do not prettify XML
--no-imports - disable use of imports, always write entire package name --no-imports - disable use of imports, always write entire package name
--no-debug-info - disable debug info parsing and processing --no-debug-info - disable debug info parsing and processing
--add-debug-lines - add comments with debug line numbers if available --add-debug-lines - add comments with debug line numbers if available
--no-inline-anonymous - disable anonymous classes inline --no-inline-anonymous - disable anonymous classes inline
--no-inline-methods - disable methods inline --no-inline-methods - disable methods inline
--no-move-inner-classes - disable move inner classes into parent --no-move-inner-classes - disable move inner classes into parent
--no-inline-kotlin-lambda - disable inline for Kotlin lambdas --no-inline-kotlin-lambda - disable inline for Kotlin lambdas
--no-finally - don't extract finally block --no-finally - don't extract finally block
--no-restore-switch-over-string - don't restore switch over string --no-restore-switch-over-string - don't restore switch over string
--no-replace-consts - don't replace constant value with matching constant field --no-replace-consts - don't replace constant value with matching constant field
--escape-unicode - escape non latin characters in strings (with \u) --escape-unicode - escape non latin characters in strings (with \u)
--respect-bytecode-access-modifiers - don't change original access modifiers --respect-bytecode-access-modifiers - don't change original access modifiers
--mappings-path - deobfuscation mappings file or directory. Allowed formats: Tiny and Tiny v2 (both '.tiny'), Enigma (.mapping) or Enigma directory --mappings-path - deobfuscation mappings file or directory. Allowed formats: Tiny and Tiny v2 (both '.tiny'), Enigma (.mapping) or Enigma directory
--mappings-mode - set mode for handling the deobfuscation mapping file: --mappings-mode - set mode for handling the deobfuscation mapping file:
'read' - just read, user can always save manually (default) 'read' - just read, user can always save manually (default)
'read-and-autosave-every-change' - read and autosave after every change 'read-and-autosave-every-change' - read and autosave after every change
'read-and-autosave-before-closing' - read and autosave before exiting the app or closing the project 'read-and-autosave-before-closing' - read and autosave before exiting the app or closing the project
'ignore' - don't read or save (can be used to skip loading mapping files referenced in the project file) 'ignore' - don't read or save (can be used to skip loading mapping files referenced in the project file)
--deobf - activate deobfuscation --deobf - activate deobfuscation
--deobf-min - min length of name, renamed if shorter, default: 3 --deobf-min - min length of name, renamed if shorter, default: 3
--deobf-max - max length of name, renamed if longer, default: 64 --deobf-max - max length of name, renamed if longer, default: 64
--deobf-whitelist - space separated list of classes (full name) and packages (ends with '.*') to exclude from deobfuscation, default: android.support.v4.* android.support.v7.* android.support.v4.os.* android.support.annotation.Px androidx.core.os.* androidx.annotation.Px --deobf-whitelist - space separated list of classes (full name) and packages (ends with '.*') to exclude from deobfuscation, default: android.support.v4.* android.support.v7.* android.support.v4.os.* android.support.annotation.Px androidx.core.os.* androidx.annotation.Px
--deobf-cfg-file - deobfuscation mappings file used for JADX auto-generated names (in the JOBF file format), default: same dir and name as input file with '.jobf' extension --deobf-cfg-file - deobfuscation mappings file used for JADX auto-generated names (in the JOBF file format), default: same dir and name as input file with '.jobf' extension
--deobf-cfg-file-mode - set mode for handling the JADX auto-generated names' deobfuscation map file: --deobf-cfg-file-mode - set mode for handling the JADX auto-generated names' deobfuscation map file:
'read' - read if found, don't save (default) 'read' - read if found, don't save (default)
'read-or-save' - read if found, save otherwise (don't overwrite) 'read-or-save' - read if found, save otherwise (don't overwrite)
'overwrite' - don't read, always save 'overwrite' - don't read, always save
'ignore' - don't read and don't save 'ignore' - don't read and don't save
--deobf-res-name-source - better name source for resources: --deobf-res-name-source - better name source for resources:
'auto' - automatically select best name (default) 'auto' - automatically select best name (default)
'resources' - use resources names 'resources' - use resources names
'code' - use R class fields names 'code' - use R class fields names
--use-source-name-as-class-name-alias - use source name as class name alias: --use-source-name-as-class-name-alias - use source name as class name alias:
'always' - always use source name if it's available 'always' - always use source name if it's available
'if-better' - use source name if it seems better than the current one 'if-better' - use source name if it seems better than the current one
'never' - never use source name, even if it's available 'never' - never use source name, even if it's available
--source-name-repeat-limit - allow using source name if it appears less than a limit number, default: 10 --source-name-repeat-limit - allow using source name if it appears less than a limit number, default: 10
--use-kotlin-methods-for-var-names - use kotlin intrinsic methods to rename variables, values: disable, apply, apply-and-hide, default: apply --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): --use-headers-for-detect-resource-extensions - Use headers for detect resource extensions if resource obfuscated
'case' - fix case sensitivity issues (according to --fs-case-sensitive option), --rename-flags - fix options (comma-separated list of):
'valid' - rename java identifiers to make them valid, 'case' - fix case sensitivity issues (according to --fs-case-sensitive option),
'printable' - remove non-printable chars from identifiers, 'valid' - rename java identifiers to make them valid,
or single 'none' - to disable all renames 'printable' - remove non-printable chars from identifiers,
or single 'all' - to enable all (default) or single 'none' - to disable all renames
--integer-format - how integers are displayed: or single 'all' - to enable all (default)
'auto' - automatically select (default) --integer-format - how integers are displayed:
'decimal' - use decimal 'auto' - automatically select (default)
'hexadecimal' - use hexadecimal 'decimal' - use decimal
--fs-case-sensitive - treat filesystem as case sensitive, false by default 'hexadecimal' - use hexadecimal
--cfg - save methods control flow graph to dot file --type-update-limit - type update limit count (per one instruction), default: 10
--raw-cfg - save methods control flow graph (use raw instructions) --fs-case-sensitive - treat filesystem as case sensitive, false by default
-f, --fallback - set '--decompilation-mode' to 'fallback' (deprecated) --cfg - save methods control flow graph to dot file
--use-dx - use dx/d8 to convert java bytecode --raw-cfg - save methods control flow graph (use raw instructions)
--comments-level - set code comments level, values: error, warn, info, debug, user-only, none, default: info -f, --fallback - set '--decompilation-mode' to 'fallback' (deprecated)
--log-level - set log level, values: quiet, progress, error, warn, info, debug, default: progress --use-dx - use dx/d8 to convert java bytecode
-v, --verbose - verbose output (set --log-level to DEBUG) --comments-level - set code comments level, values: error, warn, info, debug, user-only, none, default: info
-q, --quiet - turn off output (set --log-level to QUIET) --log-level - set log level, values: quiet, progress, error, warn, info, debug, default: progress
--disable-plugins - comma separated list of plugin ids to disable, default: -v, --verbose - verbose output (set --log-level to DEBUG)
--version - print jadx version -q, --quiet - turn off output (set --log-level to QUIET)
-h, --help - print this help --disable-plugins - comma separated list of plugin ids to disable
--config <config-ref> - load configuration from file, <config-ref> can be:
path to '.json' file
short name - uses file with this name from config directory
'none' - to disable config loading
--save-config <config-ref> - save current options into configuration file and exit, <config-ref> can be:
empty - for default config
path to '.json' file
short name - file will be saved in config directory
--print-files - print files and directories used by jadx (config, cache, temp)
--version - print jadx version
-h, --help - print this help
Plugin options (-P<name>=<value>): Plugin options (-P<name>=<value>):
dex-input: Load .dex and .apk files dex-input: Load .dex and .apk files
- dex-input.verify-checksum - verify dex file checksum before load, values: [yes, no], default: yes - dex-input.verify-checksum - verify dex file checksum before load, values: [yes, no], default: yes
java-convert: Convert .class, .jar and .aar files to dex java-convert: Convert .class, .jar and .aar files to dex
- java-convert.mode - convert mode, values: [dx, d8, both], default: both - java-convert.mode - convert mode, values: [dx, d8, both], default: both
- java-convert.d8-desugar - use desugar in d8, values: [yes, no], default: no - java-convert.d8-desugar - use desugar in d8, values: [yes, no], default: no
kotlin-metadata: Use kotlin.Metadata annotation for code generation kotlin-metadata: Use kotlin.Metadata annotation for code generation
- kotlin-metadata.class-alias - rename class alias, values: [yes, no], default: yes - kotlin-metadata.class-alias - rename class alias, values: [yes, no], default: yes
- kotlin-metadata.method-args - rename function arguments, values: [yes, no], default: yes - kotlin-metadata.method-args - rename function arguments, values: [yes, no], default: yes
- kotlin-metadata.fields - rename fields, values: [yes, no], default: yes - kotlin-metadata.fields - rename fields, values: [yes, no], default: yes
- kotlin-metadata.companion - rename companion object, values: [yes, no], default: yes - kotlin-metadata.companion - rename companion object, values: [yes, no], default: yes
- kotlin-metadata.data-class - add data class modifier, values: [yes, no], default: yes - kotlin-metadata.data-class - add data class modifier, values: [yes, no], default: yes
- kotlin-metadata.to-string - rename fields using toString, values: [yes, no], default: yes - kotlin-metadata.to-string - rename fields using toString, values: [yes, no], default: yes
- kotlin-metadata.getters - rename simple getters to field names, values: [yes, no], default: yes - kotlin-metadata.getters - rename simple getters to field names, values: [yes, no], default: yes
kotlin-smap: Use kotlin.SourceDebugExtension annotation for rename class alias kotlin-smap: Use kotlin.SourceDebugExtension annotation for rename class alias
- kotlin-smap.class-alias-source-dbg - rename class alias from SourceDebugExtension, values: [yes, no], default: no - kotlin-smap.class-alias-source-dbg - rename class alias from SourceDebugExtension, values: [yes, no], default: no
rename-mappings: various mappings support rename-mappings: various mappings support
- rename-mappings.format - mapping format, values: [AUTO, TINY_FILE, TINY_2_FILE, ENIGMA_FILE, ENIGMA_DIR, SRG_FILE, XSRG_FILE, JAM_FILE, CSRG_FILE, TSRG_FILE, TSRG_2_FILE, PROGUARD_FILE, INTELLIJ_MIGRATION_MAP_FILE, RECAF_SIMPLE_FILE, JOBF_FILE], default: AUTO - rename-mappings.format - mapping format, values: [AUTO, TINY_FILE, TINY_2_FILE, ENIGMA_FILE, ENIGMA_DIR, PROGUARD_FILE, SRG_FILE, XSRG_FILE, JAM_FILE, CSRG_FILE, TSRG_FILE, TSRG_2_FILE, INTELLIJ_MIGRATION_MAP_FILE, RECAF_SIMPLE_FILE, JOBF_FILE], default: AUTO
- rename-mappings.invert - invert mapping on load, values: [yes, no], default: no - rename-mappings.invert - invert mapping on load, values: [yes, no], default: no
smali-input: Load .smali files smali-input: Load .smali files
- smali-input.api-level - Android API level, default: 27 - smali-input.api-level - Android API level, default: 27
Environment variables: Environment variables:
JADX_DISABLE_XML_SECURITY - set to 'true' to disable all security checks for XML files JADX_DISABLE_XML_SECURITY - set to 'true' to disable all security checks for XML files
+15 -8
View File
@@ -6,8 +6,8 @@ import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
import java.util.Locale import java.util.Locale
plugins { plugins {
id("com.github.ben-manes.versions") version "0.52.0" id("com.github.ben-manes.versions") version "0.53.0"
id("se.patrikerdes.use-latest-versions") version "0.2.18" id("se.patrikerdes.use-latest-versions") version "0.2.19"
id("com.diffplug.spotless") version "6.25.0" id("com.diffplug.spotless") version "6.25.0"
} }
@@ -82,6 +82,17 @@ fun isNonStable(version: String): Boolean {
return isStable.not() return isStable.not()
} }
val distWinConfiguration: Configuration by configurations.creating {
isCanBeConsumed = false
}
val distWinWithJreConfiguration: Configuration by configurations.creating {
isCanBeConsumed = false
}
dependencies {
distWinConfiguration(project(":jadx-gui", "distWinConfiguration"))
distWinWithJreConfiguration(project(":jadx-gui", "distWinWithJreConfiguration"))
}
val copyArtifacts by tasks.registering(Copy::class) { val copyArtifacts by tasks.registering(Copy::class) {
val jarCliPattern = "jadx-cli-(.*)-all.jar".toPattern() val jarCliPattern = "jadx-cli-(.*)-all.jar".toPattern()
from(tasks.getByPath(":jadx-cli:installShadowDist")) { from(tasks.getByPath(":jadx-cli:installShadowDist")) {
@@ -119,9 +130,7 @@ val distWin by tasks.registering(Zip::class) {
group = "jadx" group = "jadx"
description = "Build Windows bundle" description = "Build Windows bundle"
val guiTask = tasks.getByPath("jadx-gui:copyDistWin") from(distWinConfiguration)
dependsOn(guiTask)
from(guiTask.outputs)
destinationDirectory.set(layout.buildDirectory.dir("distWin")) destinationDirectory.set(layout.buildDirectory.dir("distWin"))
archiveFileName.set("jadx-gui-$jadxVersion-win.zip") archiveFileName.set("jadx-gui-$jadxVersion-win.zip")
@@ -131,9 +140,7 @@ val distWin by tasks.registering(Zip::class) {
val distWinWithJre by tasks.registering(Zip::class) { val distWinWithJre by tasks.registering(Zip::class) {
description = "Build Windows with JRE bundle" description = "Build Windows with JRE bundle"
val guiTask = tasks.getByPath(":jadx-gui:copyDistWinWithJre") from(distWinWithJreConfiguration)
dependsOn(guiTask)
from(guiTask.outputs)
destinationDirectory.set(layout.buildDirectory.dir("distWinWithJre")) destinationDirectory.set(layout.buildDirectory.dir("distWinWithJre"))
archiveFileName.set("jadx-gui-$jadxVersion-with-jre-win.zip") archiveFileName.set("jadx-gui-$jadxVersion-with-jre-win.zip")
+1 -1
View File
@@ -3,7 +3,7 @@ plugins {
} }
dependencies { dependencies {
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.21") implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.10")
implementation("org.openrewrite:plugin:6.19.1") implementation("org.openrewrite:plugin:6.19.1")
} }
@@ -17,10 +17,10 @@ dependencies {
implementation("org.slf4j:slf4j-api:2.0.17") implementation("org.slf4j:slf4j-api:2.0.17")
compileOnly("org.jetbrains:annotations:26.0.2") compileOnly("org.jetbrains:annotations:26.0.2")
testImplementation("ch.qos.logback:logback-classic:1.5.18") testImplementation("ch.qos.logback:logback-classic:1.5.22")
testImplementation("org.assertj:assertj-core:3.27.3") testImplementation("org.assertj:assertj-core:3.27.6")
testImplementation("org.junit.jupiter:junit-jupiter:5.12.2") testImplementation("org.junit.jupiter:junit-jupiter:5.13.3")
testRuntimeOnly("org.junit.platform:junit-platform-launcher") testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testCompileOnly("org.jetbrains:annotations:26.0.2") testCompileOnly("org.jetbrains:annotations:26.0.2")
@@ -5,6 +5,11 @@ plugins {
id("org.jetbrains.kotlin.jvm") id("org.jetbrains.kotlin.jvm")
} }
dependencies {
implementation(kotlin("stdlib"))
implementation(kotlin("reflect")) // don't work from plugin classloader
}
kotlin { kotlin {
compilerOptions { compilerOptions {
jvmTarget.set(JvmTarget.JVM_11) jvmTarget.set(JvmTarget.JVM_11)
@@ -7,10 +7,10 @@ repositories {
} }
dependencies { dependencies {
rewrite("org.openrewrite.recipe:rewrite-testing-frameworks:3.8.0") rewrite("org.openrewrite.recipe:rewrite-testing-frameworks:3.24.0")
rewrite("org.openrewrite.recipe:rewrite-logging-frameworks:3.8.0") rewrite("org.openrewrite.recipe:rewrite-logging-frameworks:3.20.0")
rewrite("org.openrewrite.recipe:rewrite-migrate-java:3.9.0") rewrite("org.openrewrite.recipe:rewrite-migrate-java:3.24.0")
rewrite("org.openrewrite.recipe:rewrite-static-analysis:2.9.0") rewrite("org.openrewrite.recipe:rewrite-static-analysis:2.24.0")
} }
tasks { tasks {
Binary file not shown.
+2 -2
View File
@@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionSha256Sum=61ad310d3c7d3e5da131b76bbf22b5a4c0786e9d892dae8c1658d4b484de3caa distributionSha256Sum=72f44c9f8ebcb1af43838f45ee5c4aa9c5444898b3468ab3f4af7b6076c5bc3f
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
Vendored
+2 -5
View File
@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
# #
# Copyright © 2015-2021 the original authors. # Copyright © 2015 the original authors.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -114,7 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;; NONSTOP* ) nonstop=true ;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
@@ -172,7 +171,6 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java # For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" ) JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -212,8 +210,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
org.gradle.wrapper.GradleWrapperMain \
"$@" "$@"
# Stop when "xargs" is not available. # Stop when "xargs" is not available.
Vendored
+1 -2
View File
@@ -70,11 +70,10 @@ goto fail
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle @rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell
+6 -3
View File
@@ -4,7 +4,7 @@ plugins {
id("application") id("application")
// use shadow only for application scripts, jar will be copied from jadx-gui // use shadow only for application scripts, jar will be copied from jadx-gui
id("com.gradleup.shadow") version "8.3.6" id("com.gradleup.shadow") version "8.3.8"
} }
dependencies { dependencies {
@@ -19,13 +19,14 @@ dependencies {
runtimeOnly(project(":jadx-plugins:jadx-rename-mappings")) runtimeOnly(project(":jadx-plugins:jadx-rename-mappings"))
runtimeOnly(project(":jadx-plugins:jadx-kotlin-metadata")) runtimeOnly(project(":jadx-plugins:jadx-kotlin-metadata"))
runtimeOnly(project(":jadx-plugins:jadx-kotlin-source-debug-extension")) runtimeOnly(project(":jadx-plugins:jadx-kotlin-source-debug-extension"))
runtimeOnly(project(":jadx-plugins:jadx-script:jadx-script-plugin"))
runtimeOnly(project(":jadx-plugins:jadx-xapk-input")) runtimeOnly(project(":jadx-plugins:jadx-xapk-input"))
runtimeOnly(project(":jadx-plugins:jadx-aab-input")) runtimeOnly(project(":jadx-plugins:jadx-aab-input"))
runtimeOnly(project(":jadx-plugins:jadx-apkm-input")) runtimeOnly(project(":jadx-plugins:jadx-apkm-input"))
runtimeOnly(project(":jadx-plugins:jadx-apks-input"))
implementation("org.jcommander:jcommander:2.0") implementation("org.jcommander:jcommander:2.0")
implementation("ch.qos.logback:logback-classic:1.5.18") implementation("ch.qos.logback:logback-classic:1.5.22")
implementation("com.google.code.gson:gson:2.13.2")
} }
application { application {
@@ -33,8 +34,10 @@ application {
mainClass.set("jadx.cli.JadxCLI") mainClass.set("jadx.cli.JadxCLI")
applicationDefaultJvmArgs = applicationDefaultJvmArgs =
listOf( listOf(
"-XX:+IgnoreUnrecognizedVMOptions",
"-Xms256M", "-Xms256M",
"-XX:MaxRAMPercentage=70.0", "-XX:MaxRAMPercentage=70.0",
"-XX:ParallelGCThreads=3",
// disable zip checks (#1962) // disable zip checks (#1962)
"-Djdk.util.zip.disableZip64ExtraFieldValidation=true", "-Djdk.util.zip.disableZip64ExtraFieldValidation=true",
// Foreign API access for 'directories' library (Windows only) // Foreign API access for 'directories' library (Windows only)
@@ -25,7 +25,6 @@ import jadx.api.plugins.options.OptionDescription;
import jadx.core.plugins.JadxPluginManager; import jadx.core.plugins.JadxPluginManager;
import jadx.core.plugins.PluginContext; import jadx.core.plugins.PluginContext;
import jadx.core.utils.Utils; import jadx.core.utils.Utils;
import jadx.plugins.tools.JadxExternalPluginsLoader;
public class JCommanderWrapper { public class JCommanderWrapper {
private final JCommander jc; private final JCommander jc;
@@ -41,12 +40,12 @@ public class JCommanderWrapper {
public boolean parse(String[] args) { public boolean parse(String[] args) {
try { try {
jc.parse(args); String[] fixedArgs = fixArgsForEmptySaveConfig(args);
jc.parse(fixedArgs);
applyFiles(argsObj); applyFiles(argsObj);
return true; return true;
} catch (ParameterException e) { } catch (ParameterException e) {
System.err.println("Arguments parse error: " + e.getMessage()); System.err.println("Arguments parse error: " + e.getMessage());
printUsage();
return false; return false;
} }
} }
@@ -96,6 +95,41 @@ public class JCommanderWrapper {
return value; return value;
} }
/**
* Workaround to allow empty value (i.e. zero arity) for '--save-config' option
* Insert empty string arg if another option start right after this one, or it is a last one.
*/
private String[] fixArgsForEmptySaveConfig(String[] args) {
int len = args.length;
for (int i = 0; i < len; i++) {
String arg = args[i];
if (arg.equals("--save-config")) {
int next = i + 1;
if (next == len) {
return insertEmptyArg(args, next, true);
}
if (next < len) {
String nextArg = args[next];
if (nextArg.startsWith("-")) {
return insertEmptyArg(args, next, false);
}
}
break;
}
}
return args;
}
private static String[] insertEmptyArg(String[] args, int i, boolean add) {
List<String> strings = new ArrayList<>(Arrays.asList(args));
if (add) {
strings.add("");
} else {
strings.add(i, "");
}
return strings.toArray(new String[0]);
}
public void printUsage() { public void printUsage() {
LogHelper.setLogLevel(LogHelper.LogLevelEnum.ERROR); // mute logger while printing help LogHelper.setLogLevel(LogHelper.LogLevelEnum.ERROR); // mute logger while printing help
@@ -182,7 +216,9 @@ public class JCommanderWrapper {
} }
if (addDefaults) { if (addDefaults) {
String defaultValue = getDefaultValue(args, f); String defaultValue = getDefaultValue(args, f);
if (defaultValue != null && !description.contains("(default)")) { if (defaultValue != null
&& !defaultValue.isEmpty()
&& !description.contains("(default)")) {
opt.append(", default: ").append(defaultValue); opt.append(", default: ").append(defaultValue);
} }
} }
@@ -238,19 +274,16 @@ public class JCommanderWrapper {
private String appendPluginOptions(int maxNamesLen) { private String appendPluginOptions(int maxNamesLen) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
int k = 1;
// load and init all options plugins to print all options // load and init all options plugins to print all options
try (JadxDecompiler decompiler = new JadxDecompiler(argsObj.toJadxArgs())) { try (JadxDecompiler decompiler = new JadxDecompiler(argsObj.toJadxArgs())) {
JadxPluginManager pluginManager = decompiler.getPluginManager(); JadxPluginManager pluginManager = decompiler.getPluginManager();
pluginManager.load(new JadxExternalPluginsLoader()); pluginManager.load(decompiler.getArgs().getPluginLoader());
pluginManager.initAll(); pluginManager.initAll();
try { try {
for (PluginContext context : pluginManager.getAllPluginContexts()) { for (PluginContext context : pluginManager.getAllPluginContexts()) {
JadxPluginOptions options = context.getOptions(); JadxPluginOptions options = context.getOptions();
if (options != null) { if (options != null) {
if (appendPlugin(context.getPluginInfo(), context.getOptions(), sb, maxNamesLen)) { appendPlugin(context.getPluginInfo(), context.getOptions(), sb, maxNamesLen);
k++;
}
} }
} }
} finally { } finally {
+18 -16
View File
@@ -11,7 +11,9 @@ import jadx.api.JadxDecompiler;
import jadx.api.impl.AnnotatedCodeWriter; import jadx.api.impl.AnnotatedCodeWriter;
import jadx.api.impl.NoOpCodeCache; import jadx.api.impl.NoOpCodeCache;
import jadx.api.impl.SimpleCodeWriter; import jadx.api.impl.SimpleCodeWriter;
import jadx.api.usage.impl.EmptyUsageInfoCache;
import jadx.cli.LogHelper.LogLevelEnum; import jadx.cli.LogHelper.LogLevelEnum;
import jadx.cli.config.JadxConfigAdapter;
import jadx.cli.plugins.JadxFilesGetter; import jadx.cli.plugins.JadxFilesGetter;
import jadx.core.utils.exceptions.JadxArgsValidateException; import jadx.core.utils.exceptions.JadxArgsValidateException;
import jadx.plugins.tools.JadxExternalPluginsLoader; import jadx.plugins.tools.JadxExternalPluginsLoader;
@@ -34,15 +36,17 @@ public class JadxCLI {
public static int execute(String[] args, @Nullable Consumer<JadxArgs> argsMod) { public static int execute(String[] args, @Nullable Consumer<JadxArgs> argsMod) {
try { try {
JadxCLIArgs cliArgs = new JadxCLIArgs(); JadxCLIArgs cliArgs = JadxCLIArgs.processArgs(args,
if (cliArgs.processArgs(args)) { new JadxCLIArgs(),
JadxArgs jadxArgs = buildArgs(cliArgs); new JadxConfigAdapter<>(JadxCLIArgs.class, "cli"));
if (argsMod != null) { if (cliArgs == null) {
argsMod.accept(jadxArgs); return 0;
}
return runSave(jadxArgs, cliArgs);
} }
return 0; JadxArgs jadxArgs = buildArgs(cliArgs);
if (argsMod != null) {
argsMod.accept(jadxArgs);
}
return runSave(jadxArgs, cliArgs);
} catch (JadxArgsValidateException e) { } catch (JadxArgsValidateException e) {
LOG.error("Incorrect arguments: {}", e.getMessage()); LOG.error("Incorrect arguments: {}", e.getMessage());
return 1; return 1;
@@ -53,10 +57,9 @@ public class JadxCLI {
} }
private static JadxArgs buildArgs(JadxCLIArgs cliArgs) { private static JadxArgs buildArgs(JadxCLIArgs cliArgs) {
LogHelper.initLogLevel(cliArgs);
LogHelper.setLogLevelsForLoadingStage();
JadxArgs jadxArgs = cliArgs.toJadxArgs(); JadxArgs jadxArgs = cliArgs.toJadxArgs();
jadxArgs.setCodeCache(new NoOpCodeCache()); jadxArgs.setCodeCache(new NoOpCodeCache());
jadxArgs.setUsageInfoCache(new EmptyUsageInfoCache());
jadxArgs.setPluginLoader(new JadxExternalPluginsLoader()); jadxArgs.setPluginLoader(new JadxExternalPluginsLoader());
jadxArgs.setFilesGetter(JadxFilesGetter.INSTANCE); jadxArgs.setFilesGetter(JadxFilesGetter.INSTANCE);
initCodeWriterProvider(jadxArgs); initCodeWriterProvider(jadxArgs);
@@ -68,9 +71,8 @@ public class JadxCLI {
try (JadxDecompiler jadx = new JadxDecompiler(jadxArgs)) { try (JadxDecompiler jadx = new JadxDecompiler(jadxArgs)) {
jadx.load(); jadx.load();
if (checkForErrors(jadx)) { if (checkForErrors(jadx)) {
return 1; return 2;
} }
LogHelper.setLogLevelsForDecompileStage();
if (!SingleClassMode.process(jadx, cliArgs)) { if (!SingleClassMode.process(jadx, cliArgs)) {
save(jadx); save(jadx);
} }
@@ -78,7 +80,7 @@ public class JadxCLI {
if (errorsCount != 0) { if (errorsCount != 0) {
jadx.printErrorsReport(); jadx.printErrorsReport();
LOG.error("finished with errors, count: {}", errorsCount); LOG.error("finished with errors, count: {}", errorsCount);
return 1; return 3;
} }
LOG.info("done"); LOG.info("done");
return 0; return 0;
@@ -108,10 +110,10 @@ public class JadxCLI {
jadx.getArgs().setSkipSources(true); jadx.getArgs().setSkipSources(true);
} }
} }
if (jadx.getErrorsCount() > 0) { int errorsCount = jadx.getErrorsCount();
LOG.error("Load with errors! Check log for details"); if (errorsCount > 0) {
LOG.error("Loading finished with errors! Count: {}", errorsCount);
// continue processing // continue processing
return false;
} }
return false; return false;
} }
+336 -10
View File
@@ -15,6 +15,8 @@ import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.beust.jcommander.DynamicParameter; import com.beust.jcommander.DynamicParameter;
import com.beust.jcommander.IStringConverter; import com.beust.jcommander.IStringConverter;
@@ -31,22 +33,33 @@ import jadx.api.args.IntegerFormat;
import jadx.api.args.ResourceNameSource; import jadx.api.args.ResourceNameSource;
import jadx.api.args.UseSourceNameAsClassNameAlias; import jadx.api.args.UseSourceNameAsClassNameAlias;
import jadx.api.args.UserRenamesMappingsMode; import jadx.api.args.UserRenamesMappingsMode;
import jadx.cli.config.IJadxConfig;
import jadx.cli.config.JadxConfigAdapter;
import jadx.cli.config.JadxConfigExclude;
import jadx.commons.app.JadxCommonFiles;
import jadx.commons.app.JadxTempFiles;
import jadx.core.deobf.conditions.DeobfWhitelist; import jadx.core.deobf.conditions.DeobfWhitelist;
import jadx.core.export.ExportGradleType; import jadx.core.export.ExportGradleType;
import jadx.core.utils.exceptions.JadxArgsValidateException; import jadx.core.utils.exceptions.JadxArgsValidateException;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils; import jadx.core.utils.files.FileUtils;
public class JadxCLIArgs { public class JadxCLIArgs implements IJadxConfig {
private static final Logger LOG = LoggerFactory.getLogger(JadxCLIArgs.class);
@JadxConfigExclude
@Parameter(description = "<input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk, .apkm, .jadx.kts)") @Parameter(description = "<input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk, .apkm, .jadx.kts)")
protected List<String> files = Collections.emptyList(); protected List<String> files = Collections.emptyList();
@JadxConfigExclude
@Parameter(names = { "-d", "--output-dir" }, description = "output directory") @Parameter(names = { "-d", "--output-dir" }, description = "output directory")
protected String outDir; protected String outDir;
@JadxConfigExclude
@Parameter(names = { "-ds", "--output-dir-src" }, description = "output directory for sources") @Parameter(names = { "-ds", "--output-dir-src" }, description = "output directory for sources")
protected String outDirSrc; protected String outDirSrc;
@JadxConfigExclude
@Parameter(names = { "-dr", "--output-dir-res" }, description = "output directory for resources") @Parameter(names = { "-dr", "--output-dir-res" }, description = "output directory for resources")
protected String outDirRes; protected String outDirRes;
@@ -59,9 +72,11 @@ public class JadxCLIArgs {
@Parameter(names = { "-j", "--threads-count" }, description = "processing threads count") @Parameter(names = { "-j", "--threads-count" }, description = "processing threads count")
protected int threadsCount = JadxArgs.DEFAULT_THREADS_COUNT; protected int threadsCount = JadxArgs.DEFAULT_THREADS_COUNT;
@JadxConfigExclude
@Parameter(names = { "--single-class" }, description = "decompile a single class, full name, raw or alias") @Parameter(names = { "--single-class" }, description = "decompile a single class, full name, raw or alias")
protected String singleClass = null; protected String singleClass = null;
@JadxConfigExclude
@Parameter(names = { "--single-class-output" }, description = "file or dir for write if decompile a single class") @Parameter(names = { "--single-class-output" }, description = "file or dir for write if decompile a single class")
protected String singleClassOutput = null; protected String singleClassOutput = null;
@@ -166,6 +181,7 @@ public class JadxCLIArgs {
) )
protected String deobfuscationWhitelistStr = DeobfWhitelist.DEFAULT_STR; protected String deobfuscationWhitelistStr = DeobfWhitelist.DEFAULT_STR;
@JadxConfigExclude
@Parameter( @Parameter(
names = { "--deobf-cfg-file" }, names = { "--deobf-cfg-file" },
description = "deobfuscation mappings file used for JADX auto-generated names (in the JOBF file format)," description = "deobfuscation mappings file used for JADX auto-generated names (in the JOBF file format),"
@@ -241,7 +257,7 @@ public class JadxCLIArgs {
+ "\n 'printable' - remove non-printable chars from identifiers," + "\n 'printable' - remove non-printable chars from identifiers,"
+ "\nor single 'none' - to disable all renames" + "\nor single 'none' - to disable all renames"
+ "\nor single 'all' - to enable all (default)", + "\nor single 'all' - to enable all (default)",
converter = RenameConverter.class listConverter = RenameConverter.class
) )
protected Set<RenameEnum> renameFlags = EnumSet.allOf(RenameEnum.class); protected Set<RenameEnum> renameFlags = EnumSet.allOf(RenameEnum.class);
@@ -255,6 +271,9 @@ public class JadxCLIArgs {
) )
protected IntegerFormat integerFormat = IntegerFormat.AUTO; protected IntegerFormat integerFormat = IntegerFormat.AUTO;
@Parameter(names = { "--type-update-limit" }, description = "type update limit count (per one instruction)")
protected int typeUpdatesLimitCount = 10;
@Parameter(names = { "--fs-case-sensitive" }, description = "treat filesystem as case sensitive, false by default") @Parameter(names = { "--fs-case-sensitive" }, description = "treat filesystem as case sensitive, false by default")
protected boolean fsCaseSensitive = false; protected boolean fsCaseSensitive = false;
@@ -284,27 +303,110 @@ public class JadxCLIArgs {
) )
protected LogHelper.LogLevelEnum logLevel = LogHelper.LogLevelEnum.PROGRESS; protected LogHelper.LogLevelEnum logLevel = LogHelper.LogLevelEnum.PROGRESS;
@JadxConfigExclude
@Parameter(names = { "-v", "--verbose" }, description = "verbose output (set --log-level to DEBUG)") @Parameter(names = { "-v", "--verbose" }, description = "verbose output (set --log-level to DEBUG)")
protected boolean verbose = false; protected boolean verbose = false;
@JadxConfigExclude
@Parameter(names = { "-q", "--quiet" }, description = "turn off output (set --log-level to QUIET)") @Parameter(names = { "-q", "--quiet" }, description = "turn off output (set --log-level to QUIET)")
protected boolean quiet = false; protected boolean quiet = false;
@JadxConfigExclude
@Parameter(names = { "--disable-plugins" }, description = "comma separated list of plugin ids to disable") @Parameter(names = { "--disable-plugins" }, description = "comma separated list of plugin ids to disable")
protected String disablePlugins = ""; protected String disablePlugins = "";
@JadxConfigExclude
@Parameter(
names = { "--config" },
defaultValueDescription = "<config-ref>",
description = "load configuration from file, <config-ref> can be:"
+ "\n path to '.json' file"
+ "\n short name - uses file with this name from config directory"
+ "\n 'none' - to disable config loading"
)
protected String config = "";
@JadxConfigExclude
@Parameter(
names = { "--save-config" },
defaultValueDescription = "<config-ref>",
description = "save current options into configuration file and exit, <config-ref> can be:"
+ "\n empty - for default config"
+ "\n path to '.json' file"
+ "\n short name - file will be saved in config directory"
)
protected String saveConfig = null;
@JadxConfigExclude
@Parameter(names = { "--print-files" }, description = "print files and directories used by jadx (config, cache, temp)")
protected boolean printFiles = false;
@JadxConfigExclude
@Parameter(names = { "--version" }, description = "print jadx version") @Parameter(names = { "--version" }, description = "print jadx version")
protected boolean printVersion = false; protected boolean printVersion = false;
@JadxConfigExclude
@Parameter(names = { "-h", "--help" }, description = "print this help", help = true) @Parameter(names = { "-h", "--help" }, description = "print this help", help = true)
protected boolean printHelp = false; protected boolean printHelp = false;
@DynamicParameter(names = "-P", description = "Plugin options", hidden = true) @DynamicParameter(names = "-P", description = "Plugin options", hidden = true)
protected Map<String, String> pluginOptions = new HashMap<>(); protected Map<String, String> pluginOptions = new HashMap<>();
/**
* Obsolete method without config support,
* prefer {@link #processArgs(String[], JadxCLIArgs, JadxConfigAdapter)}
*/
public boolean processArgs(String[] args) { public boolean processArgs(String[] args) {
JCommanderWrapper jcw = new JCommanderWrapper(this); return processArgs(args, this, null) != null;
return jcw.parse(args) && process(jcw); }
public static <T extends JadxCLIArgs> @Nullable T processArgs(
String[] args, T argsObj, @Nullable JadxConfigAdapter<T> configAdapter) {
JCommanderWrapper jcw = new JCommanderWrapper(argsObj);
if (!jcw.parse(args)) {
return null;
}
applyArgs(argsObj);
// process commands and early exit flags
if (!argsObj.process(jcw)) {
return null;
}
if (configAdapter != null) {
if (argsObj.printFiles) {
printFilesAndDirs(configAdapter.getDefaultConfigFileName());
return null;
}
if (!argsObj.config.equalsIgnoreCase("none")) {
// load config file and merge with command line args
try {
configAdapter.useConfigRef(argsObj.config);
T configObj = configAdapter.load();
if (configObj != null) {
jcw.overrideProvided(configObj);
argsObj = configObj;
}
} catch (Exception e) {
LOG.error("Config load failed, continue with default values", e);
}
}
}
// verify result object
argsObj.verify();
applyArgs(argsObj);
// save config if requested
if (argsObj.saveConfig != null) {
saveConfig(argsObj, configAdapter);
return null;
}
return argsObj;
}
private static <T extends JadxCLIArgs> void applyArgs(T argsObj) {
// apply log levels
LogHelper.initLogLevel(argsObj);
LogHelper.applyLogLevels();
} }
public boolean process(JCommanderWrapper jcw) { public boolean process(JCommanderWrapper jcw) {
@@ -319,9 +421,7 @@ public class JadxCLIArgs {
System.out.println(JadxDecompiler.getVersion()); System.out.println(JadxDecompiler.getVersion());
return false; return false;
} }
if (threadsCount <= 0) { // unknown options added to 'files', run checks
throw new JadxArgsValidateException("Threads count must be positive, got: " + threadsCount);
}
for (String fileName : files) { for (String fileName : files) {
if (fileName.startsWith("-")) { if (fileName.startsWith("-")) {
throw new JadxArgsValidateException("Unknown option: " + fileName); throw new JadxArgsValidateException("Unknown option: " + fileName);
@@ -330,6 +430,29 @@ public class JadxCLIArgs {
return true; return true;
} }
private static void printFilesAndDirs(String defaultConfigFileName) {
System.out.println("Files and directories used by jadx:");
System.out.println(" - default config file: " + JadxCommonFiles.getConfigDir().resolve(defaultConfigFileName).toAbsolutePath());
System.out.println(" - config directory: " + JadxCommonFiles.getConfigDir().toAbsolutePath());
System.out.println(" - cache directory: " + JadxCommonFiles.getCacheDir().toAbsolutePath());
System.out.println(" - temp directory: " + JadxTempFiles.getTempRootDir().getParent().toAbsolutePath());
}
public void verify() {
if (threadsCount <= 0) {
throw new JadxArgsValidateException("Threads count must be positive, got: " + threadsCount);
}
}
private static <T extends JadxCLIArgs> void saveConfig(T argsObj, @Nullable JadxConfigAdapter<T> configAdapter) {
if (configAdapter == null) {
throw new JadxRuntimeException("Config adapter set to null, can't save config");
}
configAdapter.useConfigRef(argsObj.saveConfig);
configAdapter.save(argsObj);
System.out.println("Config saved to " + configAdapter.getConfigPath().toAbsolutePath());
}
public JadxArgs toJadxArgs() { public JadxArgs toJadxArgs() {
JadxArgs args = new JadxArgs(); JadxArgs args = new JadxArgs();
args.setInputFiles(files.stream().map(FileUtils::toFile).collect(Collectors.toList())); args.setInputFiles(files.stream().map(FileUtils::toFile).collect(Collectors.toList()));
@@ -380,16 +503,23 @@ public class JadxCLIArgs {
args.setAllowInlineKotlinLambda(allowInlineKotlinLambda); args.setAllowInlineKotlinLambda(allowInlineKotlinLambda);
args.setExtractFinally(extractFinally); args.setExtractFinally(extractFinally);
args.setRestoreSwitchOverString(restoreSwitchOverString); args.setRestoreSwitchOverString(restoreSwitchOverString);
args.setRenameFlags(renameFlags); args.setRenameFlags(buildEnumSetForRenameFlags());
args.setFsCaseSensitive(fsCaseSensitive); args.setFsCaseSensitive(fsCaseSensitive);
args.setCommentsLevel(commentsLevel); args.setCommentsLevel(commentsLevel);
args.setIntegerFormat(integerFormat); args.setIntegerFormat(integerFormat);
args.setTypeUpdatesLimitCount(typeUpdatesLimitCount);
args.setUseDxInput(useDx); args.setUseDxInput(useDx);
args.setPluginOptions(pluginOptions); args.setPluginOptions(pluginOptions);
args.setDisabledPlugins(Arrays.stream(disablePlugins.split(",")).map(String::trim).collect(Collectors.toSet())); args.setDisabledPlugins(Arrays.stream(disablePlugins.split(",")).map(String::trim).collect(Collectors.toSet()));
return args; return args;
} }
private EnumSet<RenameEnum> buildEnumSetForRenameFlags() {
EnumSet<RenameEnum> set = EnumSet.noneOf(RenameEnum.class);
set.addAll(renameFlags);
return set;
}
public List<String> getFiles() { public List<String> getFiles() {
return files; return files;
} }
@@ -422,14 +552,26 @@ public class JadxCLIArgs {
return skipResources; return skipResources;
} }
public void setSkipResources(boolean skipResources) {
this.skipResources = skipResources;
}
public boolean isSkipSources() { public boolean isSkipSources() {
return skipSources; return skipSources;
} }
public void setSkipSources(boolean skipSources) {
this.skipSources = skipSources;
}
public int getThreadsCount() { public int getThreadsCount() {
return threadsCount; return threadsCount;
} }
public void setThreadsCount(int threadsCount) {
this.threadsCount = threadsCount;
}
public boolean isFallbackMode() { public boolean isFallbackMode() {
return fallbackMode; return fallbackMode;
} }
@@ -438,82 +580,170 @@ public class JadxCLIArgs {
return useDx; return useDx;
} }
public void setUseDx(boolean useDx) {
this.useDx = useDx;
}
public DecompilationMode getDecompilationMode() { public DecompilationMode getDecompilationMode() {
return decompilationMode; return decompilationMode;
} }
public void setDecompilationMode(DecompilationMode decompilationMode) {
this.decompilationMode = decompilationMode;
}
public boolean isShowInconsistentCode() { public boolean isShowInconsistentCode() {
return showInconsistentCode; return showInconsistentCode;
} }
public void setShowInconsistentCode(boolean showInconsistentCode) {
this.showInconsistentCode = showInconsistentCode;
}
public boolean isUseImports() { public boolean isUseImports() {
return useImports; return useImports;
} }
public void setUseImports(boolean useImports) {
this.useImports = useImports;
}
public boolean isDebugInfo() { public boolean isDebugInfo() {
return debugInfo; return debugInfo;
} }
public void setDebugInfo(boolean debugInfo) {
this.debugInfo = debugInfo;
}
public boolean isAddDebugLines() { public boolean isAddDebugLines() {
return addDebugLines; return addDebugLines;
} }
public void setAddDebugLines(boolean addDebugLines) {
this.addDebugLines = addDebugLines;
}
public boolean isInlineAnonymousClasses() { public boolean isInlineAnonymousClasses() {
return inlineAnonymousClasses; return inlineAnonymousClasses;
} }
public void setInlineAnonymousClasses(boolean inlineAnonymousClasses) {
this.inlineAnonymousClasses = inlineAnonymousClasses;
}
public boolean isInlineMethods() { public boolean isInlineMethods() {
return inlineMethods; return inlineMethods;
} }
public void setInlineMethods(boolean inlineMethods) {
this.inlineMethods = inlineMethods;
}
public boolean isMoveInnerClasses() { public boolean isMoveInnerClasses() {
return moveInnerClasses; return moveInnerClasses;
} }
public void setMoveInnerClasses(boolean moveInnerClasses) {
this.moveInnerClasses = moveInnerClasses;
}
public boolean isAllowInlineKotlinLambda() { public boolean isAllowInlineKotlinLambda() {
return allowInlineKotlinLambda; return allowInlineKotlinLambda;
} }
public void setAllowInlineKotlinLambda(boolean allowInlineKotlinLambda) {
this.allowInlineKotlinLambda = allowInlineKotlinLambda;
}
public boolean isExtractFinally() { public boolean isExtractFinally() {
return extractFinally; return extractFinally;
} }
public void setExtractFinally(boolean extractFinally) {
this.extractFinally = extractFinally;
}
public boolean isRestoreSwitchOverString() { public boolean isRestoreSwitchOverString() {
return restoreSwitchOverString; return restoreSwitchOverString;
} }
public void setRestoreSwitchOverString(boolean restoreSwitchOverString) {
this.restoreSwitchOverString = restoreSwitchOverString;
}
public Path getUserRenamesMappingsPath() { public Path getUserRenamesMappingsPath() {
return userRenamesMappingsPath; return userRenamesMappingsPath;
} }
public void setUserRenamesMappingsPath(Path userRenamesMappingsPath) {
this.userRenamesMappingsPath = userRenamesMappingsPath;
}
public UserRenamesMappingsMode getUserRenamesMappingsMode() { public UserRenamesMappingsMode getUserRenamesMappingsMode() {
return userRenamesMappingsMode; return userRenamesMappingsMode;
} }
public void setUserRenamesMappingsMode(UserRenamesMappingsMode userRenamesMappingsMode) {
this.userRenamesMappingsMode = userRenamesMappingsMode;
}
public boolean isDeobfuscationOn() { public boolean isDeobfuscationOn() {
return deobfuscationOn; return deobfuscationOn;
} }
public void setDeobfuscationOn(boolean deobfuscationOn) {
this.deobfuscationOn = deobfuscationOn;
}
public int getDeobfuscationMinLength() { public int getDeobfuscationMinLength() {
return deobfuscationMinLength; return deobfuscationMinLength;
} }
public void setDeobfuscationMinLength(int deobfuscationMinLength) {
this.deobfuscationMinLength = deobfuscationMinLength;
}
public int getDeobfuscationMaxLength() { public int getDeobfuscationMaxLength() {
return deobfuscationMaxLength; return deobfuscationMaxLength;
} }
public void setDeobfuscationMaxLength(int deobfuscationMaxLength) {
this.deobfuscationMaxLength = deobfuscationMaxLength;
}
public String getDeobfuscationWhitelistStr() { public String getDeobfuscationWhitelistStr() {
return deobfuscationWhitelistStr; return deobfuscationWhitelistStr;
} }
public void setDeobfuscationWhitelistStr(String deobfuscationWhitelistStr) {
this.deobfuscationWhitelistStr = deobfuscationWhitelistStr;
}
public String getGeneratedRenamesMappingFile() { public String getGeneratedRenamesMappingFile() {
return generatedRenamesMappingFile; return generatedRenamesMappingFile;
} }
public void setGeneratedRenamesMappingFile(String generatedRenamesMappingFile) {
this.generatedRenamesMappingFile = generatedRenamesMappingFile;
}
public GeneratedRenamesMappingFileMode getGeneratedRenamesMappingFileMode() { public GeneratedRenamesMappingFileMode getGeneratedRenamesMappingFileMode() {
return generatedRenamesMappingFileMode; return generatedRenamesMappingFileMode;
} }
public void setGeneratedRenamesMappingFileMode(GeneratedRenamesMappingFileMode generatedRenamesMappingFileMode) {
this.generatedRenamesMappingFileMode = generatedRenamesMappingFileMode;
}
public int getSourceNameRepeatLimit() {
return sourceNameRepeatLimit;
}
public void setSourceNameRepeatLimit(int sourceNameRepeatLimit) {
this.sourceNameRepeatLimit = sourceNameRepeatLimit;
}
public UseSourceNameAsClassNameAlias getUseSourceNameAsClassNameAlias() { public UseSourceNameAsClassNameAlias getUseSourceNameAsClassNameAlias() {
if (useSourceNameAsClassNameAlias != null) { if (useSourceNameAsClassNameAlias != null) {
return useSourceNameAsClassNameAlias; return useSourceNameAsClassNameAlias;
@@ -525,8 +755,8 @@ public class JadxCLIArgs {
} }
} }
public int getSourceNameRepeatLimit() { public void setUseSourceNameAsClassNameAlias(UseSourceNameAsClassNameAlias useSourceNameAsClassNameAlias) {
return sourceNameRepeatLimit; this.useSourceNameAsClassNameAlias = useSourceNameAsClassNameAlias;
} }
/** /**
@@ -537,46 +767,98 @@ public class JadxCLIArgs {
return getUseSourceNameAsClassNameAlias().toBoolean(); return getUseSourceNameAsClassNameAlias().toBoolean();
} }
public void setDeobfuscationUseSourceNameAsAlias(Boolean deobfuscationUseSourceNameAsAlias) {
this.deobfuscationUseSourceNameAsAlias = deobfuscationUseSourceNameAsAlias;
}
public ResourceNameSource getResourceNameSource() { public ResourceNameSource getResourceNameSource() {
return resourceNameSource; return resourceNameSource;
} }
public void setResourceNameSource(ResourceNameSource resourceNameSource) {
this.resourceNameSource = resourceNameSource;
}
public UseKotlinMethodsForVarNames getUseKotlinMethodsForVarNames() { public UseKotlinMethodsForVarNames getUseKotlinMethodsForVarNames() {
return useKotlinMethodsForVarNames; return useKotlinMethodsForVarNames;
} }
public void setUseKotlinMethodsForVarNames(UseKotlinMethodsForVarNames useKotlinMethodsForVarNames) {
this.useKotlinMethodsForVarNames = useKotlinMethodsForVarNames;
}
public IntegerFormat getIntegerFormat() { public IntegerFormat getIntegerFormat() {
return integerFormat; return integerFormat;
} }
public void setIntegerFormat(IntegerFormat integerFormat) {
this.integerFormat = integerFormat;
}
public int getTypeUpdatesLimitCount() {
return typeUpdatesLimitCount;
}
public void setTypeUpdatesLimitCount(int typeUpdatesLimitCount) {
this.typeUpdatesLimitCount = typeUpdatesLimitCount;
}
public boolean isEscapeUnicode() { public boolean isEscapeUnicode() {
return escapeUnicode; return escapeUnicode;
} }
public void setEscapeUnicode(boolean escapeUnicode) {
this.escapeUnicode = escapeUnicode;
}
public boolean isCfgOutput() { public boolean isCfgOutput() {
return cfgOutput; return cfgOutput;
} }
public void setCfgOutput(boolean cfgOutput) {
this.cfgOutput = cfgOutput;
}
public boolean isRawCfgOutput() { public boolean isRawCfgOutput() {
return rawCfgOutput; return rawCfgOutput;
} }
public void setRawCfgOutput(boolean rawCfgOutput) {
this.rawCfgOutput = rawCfgOutput;
}
public boolean isReplaceConsts() { public boolean isReplaceConsts() {
return replaceConsts; return replaceConsts;
} }
public void setReplaceConsts(boolean replaceConsts) {
this.replaceConsts = replaceConsts;
}
public boolean isRespectBytecodeAccessModifiers() { public boolean isRespectBytecodeAccessModifiers() {
return respectBytecodeAccessModifiers; return respectBytecodeAccessModifiers;
} }
public void setRespectBytecodeAccessModifiers(boolean respectBytecodeAccessModifiers) {
this.respectBytecodeAccessModifiers = respectBytecodeAccessModifiers;
}
public boolean isExportAsGradleProject() { public boolean isExportAsGradleProject() {
return exportAsGradleProject; return exportAsGradleProject;
} }
public void setExportAsGradleProject(boolean exportAsGradleProject) {
this.exportAsGradleProject = exportAsGradleProject;
}
public boolean isSkipXmlPrettyPrint() { public boolean isSkipXmlPrettyPrint() {
return skipXmlPrettyPrint; return skipXmlPrettyPrint;
} }
public void setSkipXmlPrettyPrint(boolean skipXmlPrettyPrint) {
this.skipXmlPrettyPrint = skipXmlPrettyPrint;
}
public boolean isRenameCaseSensitive() { public boolean isRenameCaseSensitive() {
return renameFlags.contains(RenameEnum.CASE); return renameFlags.contains(RenameEnum.CASE);
} }
@@ -593,26 +875,70 @@ public class JadxCLIArgs {
return fsCaseSensitive; return fsCaseSensitive;
} }
public void setFsCaseSensitive(boolean fsCaseSensitive) {
this.fsCaseSensitive = fsCaseSensitive;
}
public boolean isUseHeadersForDetectResourceExtensions() { public boolean isUseHeadersForDetectResourceExtensions() {
return useHeadersForDetectResourceExtensions; return useHeadersForDetectResourceExtensions;
} }
public void setUseHeadersForDetectResourceExtensions(boolean useHeadersForDetectResourceExtensions) {
this.useHeadersForDetectResourceExtensions = useHeadersForDetectResourceExtensions;
}
public CommentsLevel getCommentsLevel() { public CommentsLevel getCommentsLevel() {
return commentsLevel; return commentsLevel;
} }
public void setCommentsLevel(CommentsLevel commentsLevel) {
this.commentsLevel = commentsLevel;
}
public LogHelper.LogLevelEnum getLogLevel() { public LogHelper.LogLevelEnum getLogLevel() {
return logLevel; return logLevel;
} }
public void setLogLevel(LogHelper.LogLevelEnum logLevel) {
this.logLevel = logLevel;
}
public Map<String, String> getPluginOptions() { public Map<String, String> getPluginOptions() {
return pluginOptions; return pluginOptions;
} }
public void setPluginOptions(Map<String, String> pluginOptions) {
this.pluginOptions = pluginOptions;
}
public String getDisablePlugins() { public String getDisablePlugins() {
return disablePlugins; return disablePlugins;
} }
public void setDisablePlugins(String disablePlugins) {
this.disablePlugins = disablePlugins;
}
public void setExportGradleType(@Nullable ExportGradleType exportGradleType) {
this.exportGradleType = exportGradleType;
}
public void setOutputFormat(String outputFormat) {
this.outputFormat = outputFormat;
}
public Set<RenameEnum> getRenameFlags() {
return renameFlags;
}
public void setRenameFlags(Set<RenameEnum> renameFlags) {
this.renameFlags = renameFlags;
}
public String getConfig() {
return config;
}
static class RenameConverter implements IStringConverter<Set<RenameEnum>> { static class RenameConverter implements IStringConverter<Set<RenameEnum>> {
private final String paramName; private final String paramName;
+4 -18
View File
@@ -43,10 +43,9 @@ public class LogHelper {
return null; return null;
} }
if (args.quiet) { if (args.quiet) {
return LogLevelEnum.QUIET; args.logLevel = LogLevelEnum.QUIET;
} } else if (args.verbose) {
if (args.verbose) { args.logLevel = LogLevelEnum.DEBUG;
return LogLevelEnum.DEBUG;
} }
return args.logLevel; return args.logLevel;
} }
@@ -56,20 +55,7 @@ public class LogHelper {
applyLogLevel(logLevelValue); applyLogLevel(logLevelValue);
} }
public static void setLogLevelsForLoadingStage() { public static void applyLogLevels() {
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) { if (logLevelValue == null) {
return; return;
} }
@@ -15,6 +15,7 @@ import jadx.cli.LogHelper;
import jadx.core.utils.StringUtils; import jadx.core.utils.StringUtils;
import jadx.plugins.tools.JadxPluginsList; import jadx.plugins.tools.JadxPluginsList;
import jadx.plugins.tools.JadxPluginsTools; import jadx.plugins.tools.JadxPluginsTools;
import jadx.plugins.tools.data.JadxPluginListEntry;
import jadx.plugins.tools.data.JadxPluginMetadata; import jadx.plugins.tools.data.JadxPluginMetadata;
import jadx.plugins.tools.data.JadxPluginUpdate; import jadx.plugins.tools.data.JadxPluginUpdate;
@@ -116,9 +117,9 @@ public class CommandPlugins implements ICommand {
return; return;
} }
if (available) { if (available) {
List<JadxPluginMetadata> availableList = JadxPluginsList.getInstance().get(); List<JadxPluginListEntry> availableList = JadxPluginsList.getInstance().get();
System.out.println("Available plugins: " + availableList.size()); System.out.println("Available plugins: " + availableList.size());
for (JadxPluginMetadata plugin : availableList) { for (JadxPluginListEntry plugin : availableList) {
System.out.println(" - " + plugin.getName() + ": " + plugin.getDescription() System.out.println(" - " + plugin.getName() + ": " + plugin.getDescription()
+ " (" + plugin.getLocationId() + ")"); + " (" + plugin.getLocationId() + ")");
} }
@@ -0,0 +1,7 @@
package jadx.cli.config;
/**
* Marker interface for jadx config objects
*/
public interface IJadxConfig {
}
@@ -0,0 +1,109 @@
package jadx.cli.config;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.function.Consumer;
import org.jetbrains.annotations.Nullable;
import com.google.gson.ExclusionStrategy;
import com.google.gson.FieldAttributes;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.stream.JsonReader;
import jadx.commons.app.JadxCommonFiles;
import jadx.core.utils.GsonUtils;
import jadx.core.utils.exceptions.JadxArgsValidateException;
import jadx.core.utils.exceptions.JadxRuntimeException;
public class JadxConfigAdapter<T extends IJadxConfig> {
private static final ExclusionStrategy GSON_EXCLUSION_STRATEGY = new ExclusionStrategy() {
@Override
public boolean shouldSkipField(FieldAttributes f) {
return f.getAnnotation(JadxConfigExclude.class) != null;
}
@Override
public boolean shouldSkipClass(Class<?> clazz) {
return false;
}
};
private final Class<T> configCls;
private final String defaultConfigFileName;
private final Gson gson;
private Path configPath;
public JadxConfigAdapter(Class<T> configCls, String defaultConfigName) {
this(configCls, defaultConfigName, gsonBuilder -> {
});
}
public JadxConfigAdapter(Class<T> configCls, String defaultConfigName, Consumer<GsonBuilder> applyGsonOptions) {
this.configCls = configCls;
this.defaultConfigFileName = defaultConfigName + ".json";
GsonBuilder gsonBuilder = GsonUtils.defaultGsonBuilder();
gsonBuilder.setExclusionStrategies(GSON_EXCLUSION_STRATEGY);
applyGsonOptions.accept(gsonBuilder);
this.gson = gsonBuilder.create();
}
public void useConfigRef(String configRef) {
this.configPath = resolveConfigRef(configRef);
}
public Path getConfigPath() {
return configPath;
}
public String getDefaultConfigFileName() {
return defaultConfigFileName;
}
public @Nullable T load() {
if (!Files.isRegularFile(configPath)) {
// file not found
return null;
}
try (JsonReader reader = gson.newJsonReader(Files.newBufferedReader(configPath))) {
return gson.fromJson(reader, configCls);
} catch (Exception e) {
throw new JadxRuntimeException("Failed to load config file: " + configPath, e);
}
}
public void save(T configObject) {
try {
String jsonStr = gson.toJson(configObject, configCls);
// don't use stream writer here because serialization errors will corrupt config
Files.writeString(configPath, jsonStr);
} catch (Exception e) {
throw new JadxRuntimeException("Failed to save config file: " + configPath, e);
}
}
public String objectToJsonString(T configObject) {
return gson.toJson(configObject, configCls);
}
public T jsonStringToObject(String jsonStr) {
return gson.fromJson(jsonStr, configCls);
}
private Path resolveConfigRef(String configRef) {
if (configRef == null || configRef.isEmpty()) {
// use default config file
return JadxCommonFiles.getConfigDir().resolve(defaultConfigFileName);
}
if (configRef.contains("/") || configRef.contains("\\")) {
if (!configRef.toLowerCase().endsWith(".json")) {
throw new JadxArgsValidateException("Config file extension should be '.json'");
}
return Path.of(configRef);
}
// treat as a short name
return JadxCommonFiles.getConfigDir().resolve(configRef + ".json");
}
}
@@ -0,0 +1,11 @@
package jadx.cli.config;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface JadxConfigExclude {
}
@@ -3,5 +3,5 @@ plugins {
} }
dependencies { dependencies {
implementation("io.get-coursier.util:directories-jni:0.1.3") implementation("io.get-coursier.util:directories-jni:0.1.4")
} }
@@ -81,12 +81,15 @@ public class JadxCommonFiles {
} }
/** /**
* Return JNI or Foreign implementation * Return JNI, Foreign or PowerShell implementation
*/ */
private static Windows getWinDirs() { private static Windows getWinDirs() {
Windows defSup = Windows.getDefaultSupplier().get(); Windows defSup = Windows.getDefaultSupplier().get();
if (defSup instanceof WindowsPowerShell) { if (defSup instanceof WindowsPowerShell) {
return new WindowsJni(); if (JadxSystemInfo.IS_AMD64) {
// JNI library compiled for x86-64
return new WindowsJni();
}
} }
return defSup; return defSup;
} }
@@ -0,0 +1,25 @@
package jadx.commons.app;
import java.util.Locale;
public class JadxSystemInfo {
public static final String JAVA_VM = System.getProperty("java.vm.name", "?");
public static final String JAVA_VER = System.getProperty("java.version", "?");
public static final String OS_NAME = System.getProperty("os.name", "?");
public static final String OS_ARCH = System.getProperty("os.arch", "?");
public static final String OS_VERSION = System.getProperty("os.version", "?");
private static final String OS_NAME_LOWER = OS_NAME.toLowerCase(Locale.ENGLISH);
public static final boolean IS_WINDOWS = OS_NAME_LOWER.startsWith("windows");
public static final boolean IS_MAC = OS_NAME_LOWER.startsWith("mac");
public static final boolean IS_LINUX = !IS_WINDOWS && !IS_MAC;
public static final boolean IS_UNIX = !IS_WINDOWS;
private static final String OS_ARCH_LOWER = OS_NAME.toLowerCase(Locale.ENGLISH);
public static final boolean IS_AMD64 = OS_ARCH_LOWER.equals("amd64");
public static final boolean IS_ARM64 = OS_ARCH_LOWER.equals("aarch64");
private JadxSystemInfo() {
}
}
@@ -18,7 +18,9 @@ public class JadxTempFiles {
String jadxTmpDir = System.getenv("JADX_TMP_DIR"); String jadxTmpDir = System.getenv("JADX_TMP_DIR");
Path dir; Path dir;
if (jadxTmpDir != null) { if (jadxTmpDir != null) {
dir = Files.createTempDirectory(Paths.get(jadxTmpDir), JADX_TMP_INSTANCE_PREFIX); Path customTmpRootDir = Paths.get(jadxTmpDir);
Files.createDirectories(customTmpRootDir);
dir = Files.createTempDirectory(customTmpRootDir, JADX_TMP_INSTANCE_PREFIX);
} else { } else {
dir = Files.createTempDirectory(JADX_TMP_INSTANCE_PREFIX); dir = Files.createTempDirectory(JADX_TMP_INSTANCE_PREFIX);
} }
@@ -2,14 +2,17 @@ package jadx.zip;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ZipContent implements Closeable { public class ZipContent implements Closeable {
private static final Logger LOG = LoggerFactory.getLogger(ZipContent.class);
private final IZipParser zipParser; private final IZipParser zipParser;
private final List<IZipEntry> entries; private final List<IZipEntry> entries;
private final Map<String, IZipEntry> entriesMap; private final Map<String, IZipEntry> entriesMap;
@@ -17,7 +20,19 @@ public class ZipContent implements Closeable {
public ZipContent(IZipParser zipParser, List<IZipEntry> entries) { public ZipContent(IZipParser zipParser, List<IZipEntry> entries) {
this.zipParser = zipParser; this.zipParser = zipParser;
this.entries = entries; this.entries = entries;
this.entriesMap = entries.stream().collect(Collectors.toMap(IZipEntry::getName, Function.identity())); this.entriesMap = buildNameMap(zipParser, entries);
}
private static Map<String, IZipEntry> buildNameMap(IZipParser zipParser, List<IZipEntry> entries) {
Map<String, IZipEntry> map = new HashMap<>(entries.size());
for (IZipEntry entry : entries) {
String name = entry.getName();
IZipEntry prevEntry = map.put(name, entry);
if (prevEntry != null) {
LOG.warn("Found duplicate entry: {} in {}", name, zipParser);
}
}
return map;
} }
public List<IZipEntry> getEntries() { public List<IZipEntry> getEntries() {
@@ -15,6 +15,9 @@ final class ZipDeflate {
buf.position(entry.getDataStart()); buf.position(entry.getDataStart());
ByteBuffer entryBuf = buf.slice(); ByteBuffer entryBuf = buf.slice();
entryBuf.limit((int) entry.getCompressedSize()); entryBuf.limit((int) entry.getCompressedSize());
if (entry.getUncompressedSize() > Integer.MAX_VALUE) {
throw new DataFormatException("Entry too large: " + entry.getUncompressedSize());
}
byte[] out = new byte[(int) entry.getUncompressedSize()]; byte[] out = new byte[(int) entry.getUncompressedSize()];
Inflater inflater = new Inflater(true); Inflater inflater = new Inflater(true);
inflater.setInput(entryBuf); inflater.setInput(entryBuf);
@@ -2,6 +2,8 @@ package jadx.zip.security;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -11,7 +13,7 @@ import jadx.zip.IZipEntry;
public class JadxZipSecurity implements IJadxZipSecurity { public class JadxZipSecurity implements IJadxZipSecurity {
private static final Logger LOG = LoggerFactory.getLogger(JadxZipSecurity.class); private static final Logger LOG = LoggerFactory.getLogger(JadxZipSecurity.class);
private static final File CWD = getCWD(); private static final Path CWD = Paths.get(".").toAbsolutePath().normalize();
/** /**
* The size of uncompressed zip entry shouldn't be bigger of compressed in zipBombDetectionFactor * The size of uncompressed zip entry shouldn't be bigger of compressed in zipBombDetectionFactor
@@ -56,14 +58,17 @@ public class JadxZipSecurity implements IJadxZipSecurity {
return false; return false;
} }
} }
// Path traversal check as presented on
// https://www.heise.de/en/background/Secure-Coding-Best-practices-for-using-Java-NIO-against-path-traversal-9996787.html
try { try {
File currentPath = CWD; Path entryPath = CWD.resolve(entryName).normalize();
File canonical = new File(currentPath, entryName).getCanonicalFile(); if (entryPath.startsWith(CWD)) {
if (isInSubDirectoryInternal(currentPath, canonical)) {
return true; return true;
} }
} catch (Exception e) { } catch (Exception e) {
// check failed // check failed
LOG.error("Invalid file name or path traversal attack detected: {} - error: {}", entryName, e.getMessage());
return false;
} }
LOG.error("Invalid file name or path traversal attack detected: {}", entryName); LOG.error("Invalid file name or path traversal attack detected: {}", entryName);
return false; return false;
@@ -121,12 +126,4 @@ public class JadxZipSecurity implements IJadxZipSecurity {
this.useLimitedDataStream = useLimitedDataStream; this.useLimitedDataStream = useLimitedDataStream;
} }
private static File getCWD() {
try {
return new File(".").getCanonicalFile();
} catch (IOException e) {
throw new RuntimeException("Failed to init current working dir constant", e);
}
}
} }
+3 -3
View File
@@ -6,9 +6,9 @@ dependencies {
api(project(":jadx-plugins:jadx-input-api")) api(project(":jadx-plugins:jadx-input-api"))
api(project(":jadx-commons:jadx-zip")) api(project(":jadx-commons:jadx-zip"))
implementation("com.google.code.gson:gson:2.13.1") implementation("com.google.code.gson:gson:2.13.2")
testImplementation("org.apache.commons:commons-lang3:3.17.0") testImplementation("org.apache.commons:commons-lang3:3.20.0")
testImplementation(project(":jadx-plugins:jadx-dex-input")) testImplementation(project(":jadx-plugins:jadx-dex-input"))
testRuntimeOnly(project(":jadx-plugins:jadx-smali-input")) testRuntimeOnly(project(":jadx-plugins:jadx-smali-input"))
@@ -22,7 +22,7 @@ dependencies {
strictly("[3.33, 3.34[") // from 3.34 compiled with Java 17 strictly("[3.33, 3.34[") // from 3.34 compiled with Java 17
} }
} }
testImplementation("tools.profiler:async-profiler:4.0") testImplementation("tools.profiler:async-profiler:4.2")
} }
val jadxTestJavaVersion = getTestJavaVersion() val jadxTestJavaVersion = getTestJavaVersion()
+30 -3
View File
@@ -4,7 +4,6 @@ import java.io.Closeable;
import java.io.File; import java.io.File;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
@@ -167,6 +166,12 @@ public class JadxArgs implements Closeable {
private IntegerFormat integerFormat = IntegerFormat.AUTO; private IntegerFormat integerFormat = IntegerFormat.AUTO;
/**
* Maximum updates allowed total in method per one instruction.
* Should be more or equal 1, default value is 10.
*/
private int typeUpdatesLimitCount = 10;
private boolean useDxInput = false; private boolean useDxInput = false;
public enum UseKotlinMethodsForVarNames { public enum UseKotlinMethodsForVarNames {
@@ -196,6 +201,11 @@ public class JadxArgs implements Closeable {
*/ */
private boolean runDebugChecks = false; private boolean runDebugChecks = false;
/**
* Passes to exclude from processing.
*/
private final List<String> disabledPasses = new ArrayList<>();
private Map<String, String> pluginOptions = new HashMap<>(); private Map<String, String> pluginOptions = new HashMap<>();
private Set<String> disabledPlugins = new HashSet<>(); private Set<String> disabledPlugins = new HashSet<>();
@@ -239,8 +249,12 @@ public class JadxArgs implements Closeable {
return inputFiles; return inputFiles;
} }
public void addInputFile(File inputFile) {
this.inputFiles.add(inputFile);
}
public void setInputFile(File inputFile) { public void setInputFile(File inputFile) {
this.inputFiles = Collections.singletonList(inputFile); addInputFile(inputFile);
} }
public void setInputFiles(List<File> inputFiles) { public void setInputFiles(List<File> inputFiles) {
@@ -738,6 +752,14 @@ public class JadxArgs implements Closeable {
this.integerFormat = format; this.integerFormat = format;
} }
public int getTypeUpdatesLimitCount() {
return typeUpdatesLimitCount;
}
public void setTypeUpdatesLimitCount(int typeUpdatesLimitCount) {
this.typeUpdatesLimitCount = Math.max(1, typeUpdatesLimitCount);
}
public boolean isUseDxInput() { public boolean isUseDxInput() {
return useDxInput; return useDxInput;
} }
@@ -786,6 +808,10 @@ public class JadxArgs implements Closeable {
this.runDebugChecks = runDebugChecks; this.runDebugChecks = runDebugChecks;
} }
public List<String> getDisabledPasses() {
return disabledPasses;
}
public Map<String, String> getPluginOptions() { public Map<String, String> getPluginOptions() {
return pluginOptions; return pluginOptions;
} }
@@ -839,7 +865,7 @@ public class JadxArgs implements Closeable {
+ insertDebugLines + extractFinally + insertDebugLines + extractFinally
+ debugInfo + escapeUnicode + replaceConsts + restoreSwitchOverString + debugInfo + escapeUnicode + replaceConsts + restoreSwitchOverString
+ respectBytecodeAccModifiers + fsCaseSensitive + renameFlags + respectBytecodeAccModifiers + fsCaseSensitive + renameFlags
+ commentsLevel + useDxInput + integerFormat + commentsLevel + useDxInput + integerFormat + typeUpdatesLimitCount
+ "|" + buildPluginsHash(decompiler); + "|" + buildPluginsHash(decompiler);
return FileUtils.md5Sum(argStr); return FileUtils.md5Sum(argStr);
} }
@@ -898,6 +924,7 @@ public class JadxArgs implements Closeable {
+ ", cfgOutput=" + cfgOutput + ", cfgOutput=" + cfgOutput
+ ", rawCFGOutput=" + rawCFGOutput + ", rawCFGOutput=" + rawCFGOutput
+ ", useHeadersForDetectResourceExtensions=" + useHeadersForDetectResourceExtensions + ", useHeadersForDetectResourceExtensions=" + useHeadersForDetectResourceExtensions
+ ", typeUpdatesLimitCount=" + typeUpdatesLimitCount
+ '}'; + '}';
} }
} }
@@ -138,11 +138,16 @@ public final class JadxDecompiler implements Closeable {
loadFinished(); loadFinished();
} }
/**
* Reload passes and plugins without processing classes and inputs
*/
public void reloadPasses() { public void reloadPasses() {
LOG.info("reloading (passes only) ..."); LOG.info("reloading (passes only) ...");
customPasses.clear(); customPasses.clear();
root.resetPasses(); root.resetPasses();
events.reset(); events.reset();
unloadPlugins();
loadPlugins(); loadPlugins();
root.mergePasses(customPasses); root.mergePasses(customPasses);
root.restartVisitors(); root.restartVisitors();
@@ -435,7 +440,7 @@ public final class JadxDecompiler implements Closeable {
return list; return list;
} }
public List<JavaClass> getClasses() { public synchronized List<JavaClass> getClasses() {
if (root == null) { if (root == null) {
return Collections.emptyList(); return Collections.emptyList();
} }
@@ -443,10 +448,7 @@ public final class JadxDecompiler implements Closeable {
List<ClassNode> classNodeList = root.getClasses(); List<ClassNode> classNodeList = root.getClasses();
List<JavaClass> clsList = new ArrayList<>(classNodeList.size()); List<JavaClass> clsList = new ArrayList<>(classNodeList.size());
for (ClassNode classNode : classNodeList) { for (ClassNode classNode : classNodeList) {
if (classNode.contains(AFlag.DONT_GENERATE)) { if (!classNode.contains(AFlag.DONT_GENERATE) && !classNode.isInner()) {
continue;
}
if (!classNode.getClassInfo().isInner()) {
clsList.add(convertClassNode(classNode)); clsList.add(convertClassNode(classNode));
} }
} }
@@ -546,9 +548,10 @@ public final class JadxDecompiler implements Closeable {
return foundPkg; return foundPkg;
} }
List<JavaClass> clsList = Utils.collectionMap(pkg.getClasses(), this::convertClassNode); List<JavaClass> clsList = Utils.collectionMap(pkg.getClasses(), this::convertClassNode);
List<JavaClass> clsListNoDup = Utils.collectionMap(pkg.getClassesNoDup(), this::convertClassNode);
int subPkgsCount = pkg.getSubPackages().size(); int subPkgsCount = pkg.getSubPackages().size();
List<JavaPackage> subPkgs = subPkgsCount == 0 ? Collections.emptyList() : new ArrayList<>(subPkgsCount); List<JavaPackage> subPkgs = subPkgsCount == 0 ? Collections.emptyList() : new ArrayList<>(subPkgsCount);
JavaPackage javaPkg = new JavaPackage(pkg, clsList, subPkgs); JavaPackage javaPkg = new JavaPackage(pkg, clsList, clsListNoDup, subPkgs);
if (subPkgsCount != 0) { if (subPkgsCount != 0) {
// add subpackages after parent to avoid endless recursion // add subpackages after parent to avoid endless recursion
for (PackageNode subPackage : pkg.getSubPackages()) { for (PackageNode subPackage : pkg.getSubPackages()) {
@@ -15,11 +15,17 @@ import jadx.core.dex.nodes.PackageNode;
public final class JavaPackage implements JavaNode, Comparable<JavaPackage> { public final class JavaPackage implements JavaNode, Comparable<JavaPackage> {
private final PackageNode pkgNode; private final PackageNode pkgNode;
private final List<JavaClass> classes; private final List<JavaClass> classes;
private final List<JavaClass> clsListNoDup;
private final List<JavaPackage> subPkgs; private final List<JavaPackage> subPkgs;
JavaPackage(PackageNode pkgNode, List<JavaClass> classes, List<JavaPackage> subPkgs) { JavaPackage(PackageNode pkgNode, List<JavaClass> classes, List<JavaPackage> subPkgs) {
this(pkgNode, classes, classes, subPkgs);
}
JavaPackage(PackageNode pkgNode, List<JavaClass> classes, List<JavaClass> clsListNoDup, List<JavaPackage> subPkgs) {
this.pkgNode = pkgNode; this.pkgNode = pkgNode;
this.classes = classes; this.classes = classes;
this.clsListNoDup = clsListNoDup;
this.subPkgs = subPkgs; this.subPkgs = subPkgs;
} }
@@ -49,6 +55,10 @@ public final class JavaPackage implements JavaNode, Comparable<JavaPackage> {
return classes; return classes;
} }
public List<JavaClass> getClassesNoDup() {
return clsListNoDup;
}
public boolean isRoot() { public boolean isRoot() {
return pkgNode.isRoot(); return pkgNode.isRoot();
} }
@@ -4,31 +4,44 @@ import java.util.HashMap;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import jadx.api.resources.ResourceContentType;
import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.exceptions.JadxRuntimeException;
public enum ResourceType { import static jadx.api.resources.ResourceContentType.CONTENT_BINARY;
CODE(".dex", ".jar", ".class"), import static jadx.api.resources.ResourceContentType.CONTENT_TEXT;
XML(".xml"), import static jadx.api.resources.ResourceContentType.CONTENT_UNKNOWN;
ARSC(".arsc"),
APK(".apk", ".apkm", ".apks"),
FONT(".ttf", ".ttc", ".otf"),
IMG(".png", ".gif", ".jpg", ".webp", ".bmp", ".tiff"),
ARCHIVE(".zip", ".rar", ".7zip", ".7z", ".arj", ".tar", ".gzip", ".bzip", ".bzip2", ".cab", ".cpio", ".ar", ".gz", ".tgz", ".bz2"),
VIDEOS(".mp4", ".mkv", ".webm", ".avi", ".flv", ".3gp"),
SOUNDS(".aac", ".ogg", ".opus", ".mp3", ".wav", ".wma", ".mid", ".midi"),
JSON(".json"),
TEXT(".txt", ".ini", ".conf", ".yaml", ".properties", ".js"),
HTML(".html"),
LIB(".so"),
MANIFEST,
UNKNOWN;
public enum ResourceType {
CODE(CONTENT_BINARY, ".dex", ".jar", ".class"),
XML(CONTENT_TEXT, ".xml"),
ARSC(CONTENT_TEXT, ".arsc"),
APK(CONTENT_BINARY, ".apk", ".apkm", ".apks"),
FONT(CONTENT_BINARY, ".ttf", ".ttc", ".otf"),
IMG(CONTENT_BINARY, ".png", ".gif", ".jpg", ".jpeg", ".webp", ".bmp", ".tiff"),
ARCHIVE(CONTENT_BINARY, ".zip", ".rar", ".7zip", ".7z", ".arj", ".tar", ".gzip", ".bzip", ".bzip2", ".cab", ".cpio", ".ar", ".gz",
".tgz", ".bz2"),
VIDEOS(CONTENT_BINARY, ".mp4", ".mkv", ".webm", ".avi", ".flv", ".3gp"),
SOUNDS(CONTENT_BINARY, ".aac", ".ogg", ".opus", ".mp3", ".wav", ".wma", ".mid", ".midi"),
JSON(CONTENT_TEXT, ".json"),
TEXT(CONTENT_TEXT, ".txt", ".ini", ".conf", ".yaml", ".properties", ".js", ".java", ".kt", ".md"),
HTML(CONTENT_TEXT, ".html", ".htm"),
LIB(CONTENT_BINARY, ".so"),
MANIFEST(CONTENT_TEXT),
UNKNOWN_BIN(CONTENT_BINARY, ".bin"),
UNKNOWN(CONTENT_UNKNOWN);
private final ResourceContentType contentType;
private final String[] exts; private final String[] exts;
ResourceType(String... exts) { ResourceType(ResourceContentType contentType, String... exts) {
this.contentType = contentType;
this.exts = exts; this.exts = exts;
} }
public ResourceContentType getContentType() {
return contentType;
}
public String[] getExts() { public String[] getExts() {
return exts; return exts;
} }
@@ -6,6 +6,7 @@ import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -229,9 +230,13 @@ public final class ResourcesLoader implements IResourcesLoader {
} }
public static ICodeInfo loadToCodeWriter(InputStream is) throws IOException { public static ICodeInfo loadToCodeWriter(InputStream is) throws IOException {
return loadToCodeWriter(is, StandardCharsets.UTF_8);
}
public static ICodeInfo loadToCodeWriter(InputStream is, Charset charset) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream(READ_BUFFER_SIZE); ByteArrayOutputStream baos = new ByteArrayOutputStream(READ_BUFFER_SIZE);
copyStream(is, baos); copyStream(is, baos);
return new SimpleCodeInfo(baos.toString(StandardCharsets.UTF_8)); return new SimpleCodeInfo(baos.toString(charset));
} }
private synchronized BinaryXMLParser loadBinaryXmlParser() { private synchronized BinaryXMLParser loadBinaryXmlParser() {
@@ -29,7 +29,7 @@ public class DecompilePassWrapper extends AbstractVisitor implements IPassWrappe
public void init(RootNode root) throws JadxException { public void init(RootNode root) throws JadxException {
try { try {
decompilePass.init(root); decompilePass.init(root);
} catch (Throwable e) { } catch (StackOverflowError | Exception e) {
LOG.error("Error in decompile pass init: {}", this, e); LOG.error("Error in decompile pass init: {}", this, e);
} }
} }
@@ -38,8 +38,8 @@ public class DecompilePassWrapper extends AbstractVisitor implements IPassWrappe
public boolean visit(ClassNode cls) throws JadxException { public boolean visit(ClassNode cls) throws JadxException {
try { try {
return decompilePass.visit(cls); return decompilePass.visit(cls);
} catch (Throwable e) { } catch (StackOverflowError | Exception e) {
LOG.error("Error in decompile pass: {}, class: {}", this, cls, e); cls.addError("Error in decompile pass: " + this, e);
return false; return false;
} }
} }
@@ -48,8 +48,8 @@ public class DecompilePassWrapper extends AbstractVisitor implements IPassWrappe
public void visit(MethodNode mth) throws JadxException { public void visit(MethodNode mth) throws JadxException {
try { try {
decompilePass.visit(mth); decompilePass.visit(mth);
} catch (Throwable e) { } catch (StackOverflowError | Exception e) {
LOG.error("Error in decompile pass: {}, method: {}", this, mth, e); mth.addError("Error in decompile pass: " + this, e);
} }
} }
@@ -0,0 +1,8 @@
package jadx.api.resources;
public enum ResourceContentType {
CONTENT_TEXT,
CONTENT_BINARY,
CONTENT_NONE,
CONTENT_UNKNOWN,
}
@@ -66,6 +66,7 @@ import jadx.core.dex.visitors.regions.IfRegionVisitor;
import jadx.core.dex.visitors.regions.LoopRegionVisitor; import jadx.core.dex.visitors.regions.LoopRegionVisitor;
import jadx.core.dex.visitors.regions.RegionMakerVisitor; import jadx.core.dex.visitors.regions.RegionMakerVisitor;
import jadx.core.dex.visitors.regions.ReturnVisitor; import jadx.core.dex.visitors.regions.ReturnVisitor;
import jadx.core.dex.visitors.regions.SwitchBreakVisitor;
import jadx.core.dex.visitors.regions.SwitchOverStringVisitor; import jadx.core.dex.visitors.regions.SwitchOverStringVisitor;
import jadx.core.dex.visitors.regions.variables.ProcessVariables; import jadx.core.dex.visitors.regions.variables.ProcessVariables;
import jadx.core.dex.visitors.rename.CodeRenameVisitor; import jadx.core.dex.visitors.rename.CodeRenameVisitor;
@@ -196,12 +197,14 @@ public class Jadx {
passes.add(new FixAccessModifiers()); passes.add(new FixAccessModifiers());
passes.add(new ClassModifier()); passes.add(new ClassModifier());
passes.add(new LoopRegionVisitor()); passes.add(new LoopRegionVisitor());
passes.add(new SwitchBreakVisitor());
if (args.isInlineMethods()) { if (args.isInlineMethods()) {
passes.add(new MarkMethodsForInline()); passes.add(new MarkMethodsForInline());
} }
passes.add(new ProcessVariables()); passes.add(new ProcessVariables());
passes.add(new ApplyVariableNames()); passes.add(new ApplyVariableNames());
passes.add(new PrepareForCodeGen()); passes.add(new PrepareForCodeGen());
if (args.isCfgOutput()) { if (args.isCfgOutput()) {
passes.add(DotGraphVisitor.dumpRegions()); passes.add(DotGraphVisitor.dumpRegions());
@@ -1,21 +1,28 @@
package jadx.core; package jadx.core;
import java.util.EnumMap;
import java.util.List; import java.util.List;
import java.util.Map;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import jadx.api.DecompilationMode;
import jadx.api.ICodeInfo; import jadx.api.ICodeInfo;
import jadx.api.JadxArgs;
import jadx.api.impl.SimpleCodeInfo; import jadx.api.impl.SimpleCodeInfo;
import jadx.core.codegen.CodeGen; import jadx.core.codegen.CodeGen;
import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.DecompileModeOverrideAttr;
import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.LoadStage; import jadx.core.dex.nodes.LoadStage;
import jadx.core.dex.nodes.RootNode; import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.visitors.DepthTraversal; import jadx.core.dex.visitors.DepthTraversal;
import jadx.core.dex.visitors.IDexTreeVisitor; import jadx.core.dex.visitors.IDexTreeVisitor;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.exceptions.JadxRuntimeException;
import static jadx.core.dex.nodes.ProcessState.GENERATED_AND_UNLOADED; import static jadx.core.dex.nodes.ProcessState.GENERATED_AND_UNLOADED;
@@ -41,6 +48,7 @@ public class ProcessClass {
// nothing to do // nothing to do
return null; return null;
} }
Utils.checkThreadInterrupt();
synchronized (cls.getClassInfo()) { synchronized (cls.getClassInfo()) {
try { try {
if (cls.contains(AFlag.CLASS_DEEP_RELOAD)) { if (cls.contains(AFlag.CLASS_DEEP_RELOAD)) {
@@ -76,6 +84,7 @@ public class ProcessClass {
cls.setState(PROCESS_COMPLETE); cls.setState(PROCESS_COMPLETE);
} }
if (codegen) { if (codegen) {
Utils.checkThreadInterrupt();
ICodeInfo code = CodeGen.generate(cls); ICodeInfo code = CodeGen.generate(cls);
if (!cls.contains(AFlag.DONT_UNLOAD_CLASS)) { if (!cls.contains(AFlag.DONT_UNLOAD_CLASS)) {
cls.unload(); cls.unload();
@@ -84,7 +93,7 @@ public class ProcessClass {
return code; return code;
} }
return null; return null;
} catch (Throwable e) { } catch (StackOverflowError | Exception e) {
if (codegen) { if (codegen) {
throw e; throw e;
} }
@@ -119,7 +128,7 @@ public class ProcessClass {
throw new JadxRuntimeException("Codegen failed"); throw new JadxRuntimeException("Codegen failed");
} }
return code; return code;
} catch (Throwable e) { } catch (StackOverflowError | Exception e) {
throw new JadxRuntimeException("Failed to generate code for class: " + cls.getFullName(), e); throw new JadxRuntimeException("Failed to generate code for class: " + cls.getFullName(), e);
} }
} }
@@ -135,7 +144,7 @@ public class ProcessClass {
} }
try { try {
process(cls, false); process(cls, false);
} catch (Throwable e) { } catch (StackOverflowError | Exception e) {
throw new JadxRuntimeException("Failed to process class: " + cls.getFullName(), e); throw new JadxRuntimeException("Failed to process class: " + cls.getFullName(), e);
} }
} }
@@ -146,11 +155,48 @@ public class ProcessClass {
public @Nullable ICodeInfo forceGenerateCode(ClassNode cls) { public @Nullable ICodeInfo forceGenerateCode(ClassNode cls) {
try { try {
return process(cls, true); return process(cls, true);
} catch (Throwable e) { } catch (StackOverflowError | Exception e) {
throw new JadxRuntimeException("Failed to generate code for class: " + cls.getFullName(), e); throw new JadxRuntimeException("Failed to generate code for class: " + cls.getFullName(), e);
} }
} }
private final Map<DecompilationMode, ProcessClass> modesMap = new EnumMap<>(DecompilationMode.class);
public @Nullable ICodeInfo forceGenerateCodeForMode(ClassNode cls, DecompilationMode mode) {
synchronized (modesMap) {
ProcessClass prCls = modesMap.computeIfAbsent(mode, m -> {
RootNode root = cls.root();
ProcessClass newPrCls = new ProcessClass(getPassesForMode(root.getArgs(), m));
newPrCls.initPasses(root);
return newPrCls;
});
try {
cls.addAttr(new DecompileModeOverrideAttr(mode));
return prCls.forceGenerateCode(cls);
} finally {
cls.remove(AType.DECOMPILE_MODE_OVERRIDE);
}
}
}
private static List<IDexTreeVisitor> getPassesForMode(JadxArgs baseArgs, DecompilationMode mode) {
switch (mode) {
case FALLBACK:
return Jadx.getFallbackPassesList();
case SIMPLE:
// copy properties into new args
// keep in sync with properties usage in Jadx.getSimpleModePasses method
JadxArgs args = new JadxArgs();
args.setDebugInfo(baseArgs.isDebugInfo());
args.setCommentsLevel(baseArgs.getCommentsLevel());
return Jadx.getSimpleModePasses(args);
default:
throw new JadxRuntimeException("Unexpected decompilation mode: " + mode);
}
}
public void initPasses(RootNode root) { public void initPasses(RootNode root) {
for (IDexTreeVisitor pass : passes) { for (IDexTreeVisitor pass : passes) {
try { try {
@@ -155,8 +155,6 @@ public class ClassGen {
if (Consts.DEBUG_USAGE) { if (Consts.DEBUG_USAGE) {
addClassUsageInfo(code, cls); addClassUsageInfo(code, cls);
} }
CodeGenUtils.addErrorsAndComments(code, cls);
CodeGenUtils.addSourceFileInfo(code, cls);
addClassDeclaration(code); addClassDeclaration(code);
addClassBody(code); addClassBody(code);
} }
@@ -177,9 +175,13 @@ public class ClassGen {
af = af.remove(AccessFlags.STATIC).remove(AccessFlags.PRIVATE); af = af.remove(AccessFlags.STATIC).remove(AccessFlags.PRIVATE);
} }
annotationGen.addForClass(clsCode); CodeGenUtils.addComments(clsCode, cls);
insertRenameInfo(clsCode, cls); CodeGenUtils.addClassRenamedComment(clsCode, cls);
CodeGenUtils.addErrors(clsCode, cls);
CodeGenUtils.addSourceFileInfo(clsCode, cls);
CodeGenUtils.addInputFileInfo(clsCode, cls); CodeGenUtils.addInputFileInfo(clsCode, cls);
annotationGen.addForClass(clsCode);
clsCode.startLineWithNum(cls.getSourceLine()).add(af.makeString(cls.checkCommentsLevel(CommentsLevel.INFO))); clsCode.startLineWithNum(cls.getSourceLine()).add(af.makeString(cls.checkCommentsLevel(CommentsLevel.INFO)));
if (af.isInterface()) { if (af.isInterface()) {
if (af.isAnnotation()) { if (af.isAnnotation()) {
@@ -434,10 +436,10 @@ public class ClassGen {
if (Consts.DEBUG_USAGE) { if (Consts.DEBUG_USAGE) {
addFieldUsageInfo(code, f); addFieldUsageInfo(code, f);
} }
CodeGenUtils.addComments(code, f);
if (f.getFieldInfo().hasAlias()) { if (f.getFieldInfo().hasAlias()) {
CodeGenUtils.addRenamedComment(code, f, f.getName()); CodeGenUtils.addRenamedComment(code, f, f.getName());
} }
CodeGenUtils.addComments(code, f);
annotationGen.addForField(code, f); annotationGen.addForField(code, f);
code.startLine(f.getAccessFlags().makeString(f.checkCommentsLevel(CommentsLevel.INFO))); code.startLine(f.getAccessFlags().makeString(f.checkCommentsLevel(CommentsLevel.INFO)));
@@ -802,13 +804,6 @@ public class ClassGen {
return root.getClsp().isClsKnown(currentPkg + '.' + shortName); return root.getClsp().isClsKnown(currentPkg + '.' + shortName);
} }
private void insertRenameInfo(ICodeWriter code, ClassNode cls) {
ClassInfo classInfo = cls.getClassInfo();
if (classInfo.hasAlias() && cls.checkCommentsLevel(CommentsLevel.INFO)) {
CodeGenUtils.addRenamedComment(code, cls, classInfo.getType().getObject());
}
}
private static void addClassUsageInfo(ICodeWriter code, ClassNode cls) { private static void addClassUsageInfo(ICodeWriter code, ClassNode cls) {
List<ClassNode> deps = cls.getDependencies(); List<ClassNode> deps = cls.getDependencies();
code.startLine("// deps - ").add(Integer.toString(deps.size())); code.startLine("// deps - ").add(Integer.toString(deps.size()));
@@ -3,14 +3,13 @@ package jadx.core.codegen;
import java.util.Collections; import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import jadx.api.CommentsLevel; import jadx.api.CommentsLevel;
import jadx.api.DecompilationMode;
import jadx.api.ICodeWriter; import jadx.api.ICodeWriter;
import jadx.api.JadxArgs; import jadx.api.JadxArgs;
import jadx.api.args.IntegerFormat; import jadx.api.args.IntegerFormat;
@@ -25,6 +24,7 @@ import jadx.core.Jadx;
import jadx.core.codegen.utils.CodeGenUtils; import jadx.core.codegen.utils.CodeGenUtils;
import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.DecompileModeOverrideAttr;
import jadx.core.dex.attributes.nodes.JadxError; import jadx.core.dex.attributes.nodes.JadxError;
import jadx.core.dex.attributes.nodes.JumpInfo; import jadx.core.dex.attributes.nodes.JumpInfo;
import jadx.core.dex.attributes.nodes.MethodOverrideAttr; import jadx.core.dex.attributes.nodes.MethodOverrideAttr;
@@ -268,7 +268,14 @@ public class MethodGen {
public void addInstructions(ICodeWriter code) throws CodegenException { public void addInstructions(ICodeWriter code) throws CodegenException {
JadxArgs args = mth.root().getArgs(); JadxArgs args = mth.root().getArgs();
switch (args.getDecompilationMode()) { DecompileModeOverrideAttr modeOverrideAttr = mth.getTopParentClass().get(AType.DECOMPILE_MODE_OVERRIDE);
DecompilationMode mode;
if (modeOverrideAttr != null) {
mode = modeOverrideAttr.getMode();
} else {
mode = args.getDecompilationMode();
}
switch (mode) {
case AUTO: case AUTO:
if (classGen.isFallbackMode() || mth.getRegion() == null) { if (classGen.isFallbackMode() || mth.getRegion() == null) {
// TODO: try simple mode first // TODO: try simple mode first
@@ -381,6 +388,20 @@ public class MethodGen {
} }
public void addFallbackMethodCode(ICodeWriter code, FallbackOption fallbackOption) { public void addFallbackMethodCode(ICodeWriter code, FallbackOption fallbackOption) {
if (fallbackOption == COMMENTED_DUMP && mth.getCommentsLevel() != CommentsLevel.DEBUG) {
long insnCountEstimate = mth.getInsnsCount();
if (insnCountEstimate > 200) {
code.incIndent();
code.startLine("Method dump skipped, instruction units count: " + insnCountEstimate);
if (code.isMetadataSupported()) {
code.startLine("To view this dump change 'Code comments level' option to 'DEBUG'");
} else {
code.startLine("To view this dump add '--comments-level debug' option");
}
code.decIndent();
return;
}
}
if (fallbackOption != FALLBACK_MODE) { if (fallbackOption != FALLBACK_MODE) {
List<JadxError> errors = mth.getAll(AType.JADX_ERROR); // preserve error before unload List<JadxError> errors = mth.getAll(AType.JADX_ERROR); // preserve error before unload
try { try {
@@ -404,23 +425,6 @@ public class MethodGen {
code.startLine("// Can't load method instructions."); code.startLine("// Can't load method instructions.");
return; return;
} }
if (fallbackOption == COMMENTED_DUMP && mth.getCommentsLevel() != CommentsLevel.DEBUG) {
long insnCountEstimate = Stream.of(insnArr)
.filter(Objects::nonNull)
.filter(insn -> insn.getType() != InsnType.NOP)
.count();
if (insnCountEstimate > 100) {
code.incIndent();
code.startLine("Method dump skipped, instructions count: " + insnArr.length);
if (code.isMetadataSupported()) {
code.startLine("To view this dump change 'Code comments level' option to 'DEBUG'");
} else {
code.startLine("To view this dump add '--comments-level debug' option");
}
code.decIndent();
return;
}
}
code.incIndent(); code.incIndent();
if (mth.getThisArg() != null) { if (mth.getThisArg() != null) {
code.startLine(nameGen.useArg(mth.getThisArg())).add(" = this;"); code.startLine(nameGen.useArg(mth.getThisArg())).add(" = this;");
@@ -294,6 +294,9 @@ public class RegionGen extends InsnGen {
isEnum = clsDetails != null && clsDetails.hasAccFlag(AccessFlags.ENUM); isEnum = clsDetails != null && clsDetails.hasAccFlag(AccessFlags.ENUM);
} }
if (isEnum) { if (isEnum) {
if (fld != null) {
code.attachAnnotation(fld);
}
code.add(fldInfo.getAlias()); code.add(fldInfo.getAlias());
return; return;
} }
@@ -1,6 +1,7 @@
package jadx.core.codegen.utils; package jadx.core.codegen.utils;
import java.util.List; import java.util.List;
import java.util.function.BiConsumer;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -16,6 +17,7 @@ import jadx.core.dex.attributes.nodes.JadxCommentsAttr;
import jadx.core.dex.attributes.nodes.JadxError; import jadx.core.dex.attributes.nodes.JadxError;
import jadx.core.dex.attributes.nodes.NotificationAttrNode; import jadx.core.dex.attributes.nodes.NotificationAttrNode;
import jadx.core.dex.attributes.nodes.RenameReasonAttr; import jadx.core.dex.attributes.nodes.RenameReasonAttr;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.instructions.args.CodeVar; import jadx.core.dex.instructions.args.CodeVar;
import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.instructions.args.SSAVar;
@@ -26,8 +28,8 @@ import jadx.core.utils.Utils;
public class CodeGenUtils { public class CodeGenUtils {
public static void addErrorsAndComments(ICodeWriter code, NotificationAttrNode node) { public static void addErrorsAndComments(ICodeWriter code, NotificationAttrNode node) {
addErrors(code, node);
addComments(code, node); addComments(code, node);
addErrors(code, node);
} }
public static void addErrors(ICodeWriter code, NotificationAttrNode node) { public static void addErrors(ICodeWriter code, NotificationAttrNode node) {
@@ -86,9 +88,37 @@ public class CodeGenUtils {
} else { } else {
code.add(' '); code.add(' ');
} }
CommentStyle style = comment.getStyle(); addCommentWithStyle(code, comment.getStyle(), comment.getComment());
}
public static void addJadxNodeComment(ICodeWriter code, NotificationAttrNode node,
CommentsLevel level, BiConsumer<ICodeWriter, String> commentFunc) {
if (node.checkCommentsLevel(level)) {
code.startLine();
addCommentWithStyle(code, CommentStyle.BLOCK_CONDENSED, (commentCode, newLinePrefix) -> {
commentCode.add("JADX ").add(level.name()).add(": ");
commentFunc.accept(commentCode, newLinePrefix);
});
}
}
public static void addJadxComment(ICodeWriter code, CommentsLevel level, String commentStr) {
code.startLine();
addCommentWithStyle(code, CommentStyle.BLOCK_CONDENSED, "JADX " + level.name() + ": " + commentStr);
}
private static void addCommentWithStyle(ICodeWriter code, CommentStyle style, String commentStr) {
appendMultiLineString(code, "", style.getStart()); appendMultiLineString(code, "", style.getStart());
appendMultiLineString(code, style.getOnNewLine(), comment.getComment()); appendMultiLineString(code, style.getOnNewLine(), commentStr);
appendMultiLineString(code, "", style.getEnd());
}
/**
* Insert comment with function, use second arg as new line prefix
*/
private static void addCommentWithStyle(ICodeWriter code, CommentStyle style, BiConsumer<ICodeWriter, String> commentFunc) {
appendMultiLineString(code, "", style.getStart());
commentFunc.accept(code, style.getOnNewLine());
appendMultiLineString(code, "", style.getEnd()); appendMultiLineString(code, "", style.getEnd());
} }
@@ -107,16 +137,21 @@ public class CodeGenUtils {
} }
} }
public static void addClassRenamedComment(ICodeWriter code, ClassNode cls) {
ClassInfo classInfo = cls.getClassInfo();
if (classInfo.hasAlias()) {
addRenamedComment(code, cls, classInfo.getType().getObject());
}
}
public static void addRenamedComment(ICodeWriter code, NotificationAttrNode node, String origName) { public static void addRenamedComment(ICodeWriter code, NotificationAttrNode node, String origName) {
if (!node.checkCommentsLevel(CommentsLevel.INFO)) { addJadxNodeComment(code, node, CommentsLevel.INFO, (commentCode, newLinePrefix) -> {
return; commentCode.add("renamed from: ").add(origName);
} RenameReasonAttr renameReasonAttr = node.get(AType.RENAME_REASON);
code.startLine("/* renamed from: ").add(origName); if (renameReasonAttr != null) {
RenameReasonAttr renameReasonAttr = node.get(AType.RENAME_REASON); commentCode.add(", reason: ").add(renameReasonAttr.getDescription());
if (renameReasonAttr != null) { }
code.add(", reason: ").add(renameReasonAttr.getDescription()); });
}
code.add(" */");
} }
public static void addSourceFileInfo(ICodeWriter code, ClassNode node) { public static void addSourceFileInfo(ICodeWriter code, ClassNode node) {
@@ -131,7 +166,7 @@ public class CodeGenUtils {
// ignore similar name // ignore similar name
return; return;
} }
code.startLine("/* compiled from: ").add(fileName).add(" */"); addJadxComment(code, CommentsLevel.INFO, "compiled from: " + fileName);
} }
} }
@@ -146,7 +181,7 @@ public class CodeGenUtils {
// don't add same comment for inner classes // don't add same comment for inner classes
return; return;
} }
code.startLine("/* loaded from: ").add(inputFileName).add(" */"); addJadxComment(code, CommentsLevel.INFO, "loaded from: " + inputFileName);
} }
} }
} }
@@ -7,6 +7,7 @@ import jadx.core.dex.attributes.nodes.AnonymousClassAttr;
import jadx.core.dex.attributes.nodes.ClassTypeVarsAttr; import jadx.core.dex.attributes.nodes.ClassTypeVarsAttr;
import jadx.core.dex.attributes.nodes.CodeFeaturesAttr; import jadx.core.dex.attributes.nodes.CodeFeaturesAttr;
import jadx.core.dex.attributes.nodes.DeclareVariablesAttr; import jadx.core.dex.attributes.nodes.DeclareVariablesAttr;
import jadx.core.dex.attributes.nodes.DecompileModeOverrideAttr;
import jadx.core.dex.attributes.nodes.EdgeInsnAttr; import jadx.core.dex.attributes.nodes.EdgeInsnAttr;
import jadx.core.dex.attributes.nodes.EnumClassAttr; import jadx.core.dex.attributes.nodes.EnumClassAttr;
import jadx.core.dex.attributes.nodes.EnumMapAttr; import jadx.core.dex.attributes.nodes.EnumMapAttr;
@@ -62,6 +63,7 @@ public final class AType<T extends IJadxAttribute> implements IJadxAttrType<T> {
public static final AType<ClassTypeVarsAttr> CLASS_TYPE_VARS = new AType<>(); public static final AType<ClassTypeVarsAttr> CLASS_TYPE_VARS = new AType<>();
public static final AType<AnonymousClassAttr> ANONYMOUS_CLASS = new AType<>(); public static final AType<AnonymousClassAttr> ANONYMOUS_CLASS = new AType<>();
public static final AType<InlinedAttr> INLINED = new AType<>(); public static final AType<InlinedAttr> INLINED = new AType<>();
public static final AType<DecompileModeOverrideAttr> DECOMPILE_MODE_OVERRIDE = new AType<>();
// field // field
public static final AType<FieldInitInsnAttr> FIELD_INIT_INSN = new AType<>(); public static final AType<FieldInitInsnAttr> FIELD_INIT_INSN = new AType<>();
@@ -0,0 +1,29 @@
package jadx.core.dex.attributes.nodes;
import jadx.api.DecompilationMode;
import jadx.api.plugins.input.data.attributes.IJadxAttrType;
import jadx.api.plugins.input.data.attributes.IJadxAttribute;
import jadx.core.dex.attributes.AType;
public class DecompileModeOverrideAttr implements IJadxAttribute {
private final DecompilationMode mode;
public DecompileModeOverrideAttr(DecompilationMode mode) {
this.mode = mode;
}
public DecompilationMode getMode() {
return mode;
}
@Override
public IJadxAttrType<DecompileModeOverrideAttr> getAttrType() {
return AType.DECOMPILE_MODE_OVERRIDE;
}
@Override
public String toString() {
return "DECOMPILE_MODE_OVERRIDE: " + mode;
}
}
@@ -1,10 +1,11 @@
package jadx.core.dex.attributes.nodes; package jadx.core.dex.attributes.nodes;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import jadx.api.CommentsLevel; import jadx.api.CommentsLevel;
@@ -30,10 +31,10 @@ public class JadxCommentsAttr implements IJadxAttribute {
return newAttr; return newAttr;
} }
private final Map<CommentsLevel, List<String>> comments = new EnumMap<>(CommentsLevel.class); private final Map<CommentsLevel, Set<String>> comments = new EnumMap<>(CommentsLevel.class);
public void add(CommentsLevel level, String comment) { public void add(CommentsLevel level, String comment) {
comments.computeIfAbsent(level, l -> new ArrayList<>()).add(comment); comments.computeIfAbsent(level, l -> new HashSet<>()).add(comment);
} }
public List<String> formatAndFilter(CommentsLevel level) { public List<String> formatAndFilter(CommentsLevel level) {
@@ -47,12 +48,11 @@ public class JadxCommentsAttr implements IJadxAttribute {
return e.getValue().stream() return e.getValue().stream()
.map(v -> "JADX " + levelName + ": " + v); .map(v -> "JADX " + levelName + ": " + v);
}) })
.distinct()
.sorted() .sorted()
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
public Map<CommentsLevel, List<String>> getComments() { public Map<CommentsLevel, Set<String>> getComments() {
return comments; return comments;
} }
@@ -25,6 +25,6 @@ public class RegionRefAttr implements IJadxAttribute {
@Override @Override
public String toString() { public String toString() {
return "RegionRef:" + region; return "RegionRef:" + region.baseString();
} }
} }
@@ -23,9 +23,9 @@ public final class ClassInfo implements Comparable<ClassInfo> {
@Nullable @Nullable
private ClassAliasInfo alias; private ClassAliasInfo alias;
private ClassInfo(RootNode root, ArgType type) { private ClassInfo(RootNode root, ArgType type, boolean canBeInner) {
this.type = type; this.type = type;
splitAndApplyNames(root, type, root.getArgs().isMoveInnerClasses()); splitAndApplyNames(root, type, canBeInner);
} }
public static ClassInfo fromType(RootNode root, ArgType type) { public static ClassInfo fromType(RootNode root, ArgType type) {
@@ -34,7 +34,8 @@ public final class ClassInfo implements Comparable<ClassInfo> {
if (cls != null) { if (cls != null) {
return cls; return cls;
} }
ClassInfo newClsInfo = new ClassInfo(root, clsType); boolean canBeInner = root.getArgs().isMoveInnerClasses();
ClassInfo newClsInfo = new ClassInfo(root, clsType, canBeInner);
return root.getInfoStorage().putCls(newClsInfo); return root.getInfoStorage().putCls(newClsInfo);
} }
@@ -42,6 +43,10 @@ public final class ClassInfo implements Comparable<ClassInfo> {
return fromType(root, ArgType.object(clsName)); return fromType(root, ArgType.object(clsName));
} }
public static ClassInfo fromNameWithoutCache(RootNode root, String fullClsName, boolean canBeInner) {
return new ClassInfo(root, ArgType.object(fullClsName), canBeInner);
}
private static ArgType checkClassType(ArgType type) { private static ArgType checkClassType(ArgType type) {
if (type == null) { if (type == null) {
throw new JadxRuntimeException("Null class type"); throw new JadxRuntimeException("Null class type");
@@ -50,7 +50,12 @@ public class InsnDecoder {
rawInsn.decode(); rawInsn.decode();
insn = decode(rawInsn); insn = decode(rawInsn);
} catch (Exception e) { } catch (Exception e) {
boolean mthWithErrors = method.contains(AType.JADX_ERROR);
method.addError("Failed to decode insn: " + rawInsn, e); method.addError("Failed to decode insn: " + rawInsn, e);
if (mthWithErrors) {
// second error in this method => abort processing
throw new JadxRuntimeException("Failed to decode insn: " + rawInsn, e);
}
insn = new InsnNode(InsnType.NOP, 0); insn = new InsnNode(InsnType.NOP, 0);
insn.addAttr(AType.JADX_ERROR, new JadxError("decode failed: " + e.getMessage(), e)); insn.addAttr(AType.JADX_ERROR, new JadxError("decode failed: " + e.getMessage(), e));
} }
@@ -478,7 +483,7 @@ public class InsnDecoder {
case FILL_ARRAY_DATA: case FILL_ARRAY_DATA:
return new FillArrayInsn(InsnArg.reg(insn, 0, ArgType.UNKNOWN_ARRAY), insn.getTarget()); return new FillArrayInsn(InsnArg.reg(insn, 0, ArgType.UNKNOWN_ARRAY), insn.getTarget());
case FILL_ARRAY_DATA_PAYLOAD: case FILL_ARRAY_DATA_PAYLOAD:
return new FillArrayData(((IArrayPayload) Objects.requireNonNull(insn.getPayload()))); return new FillArrayData((IArrayPayload) Objects.requireNonNull(insn.getPayload()));
case FILLED_NEW_ARRAY: case FILLED_NEW_ARRAY:
return filledNewArray(insn, false); return filledNewArray(insn, false);
@@ -492,7 +497,7 @@ public class InsnDecoder {
case PACKED_SWITCH_PAYLOAD: case PACKED_SWITCH_PAYLOAD:
case SPARSE_SWITCH_PAYLOAD: case SPARSE_SWITCH_PAYLOAD:
return new SwitchData(((ISwitchPayload) insn.getPayload())); return new SwitchData((ISwitchPayload) insn.getPayload());
case MONITOR_ENTER: case MONITOR_ENTER:
return insn(InsnType.MONITOR_ENTER, return insn(InsnType.MONITOR_ENTER,
@@ -510,7 +515,7 @@ public class InsnDecoder {
} }
private SwitchInsn makeSwitch(InsnData insn, boolean packed) { private SwitchInsn makeSwitch(InsnData insn, boolean packed) {
SwitchInsn swInsn = new SwitchInsn(InsnArg.reg(insn, 0, ArgType.UNKNOWN), insn.getTarget(), packed); SwitchInsn swInsn = new SwitchInsn(InsnArg.reg(insn, 0, ArgType.NARROW_INTEGRAL), insn.getTarget(), packed);
ICustomPayload payload = insn.getPayload(); ICustomPayload payload = insn.getPayload();
if (payload != null) { if (payload != null) {
swInsn.attachSwitchData(new SwitchData((ISwitchPayload) payload), insn.getTarget()); swInsn.attachSwitchData(new SwitchData((ISwitchPayload) payload), insn.getTarget());
@@ -69,6 +69,15 @@ public final class PhiInsn extends InsnNode {
return (RegisterArg) super.getArg(n); return (RegisterArg) super.getArg(n);
} }
public @Nullable RegisterArg getArgByBlock(BlockNode block) {
for (int i = 0; i < blockBinds.size(); i++) {
if (blockBinds.get(i) == block) {
return getArg(i);
}
}
return null;
}
@Override @Override
public boolean removeArg(InsnArg arg) { public boolean removeArg(InsnArg arg) {
int index = getArgIndex(arg); int index = getArgIndex(arg);
@@ -61,6 +61,9 @@ public abstract class ArgType {
PrimitiveType.INT, PrimitiveType.FLOAT, PrimitiveType.INT, PrimitiveType.FLOAT,
PrimitiveType.SHORT, PrimitiveType.BYTE, PrimitiveType.CHAR); PrimitiveType.SHORT, PrimitiveType.BYTE, PrimitiveType.CHAR);
public static final ArgType NARROW_NEG_NUMBERS = unknown(
PrimitiveType.INT, PrimitiveType.SHORT, PrimitiveType.BYTE, PrimitiveType.FLOAT);
public static final ArgType NARROW_NUMBERS_NO_FLOAT = unknown( public static final ArgType NARROW_NUMBERS_NO_FLOAT = unknown(
PrimitiveType.INT, PrimitiveType.BOOLEAN, PrimitiveType.INT, PrimitiveType.BOOLEAN,
PrimitiveType.SHORT, PrimitiveType.BYTE, PrimitiveType.CHAR); PrimitiveType.SHORT, PrimitiveType.BYTE, PrimitiveType.CHAR);
@@ -746,6 +749,9 @@ public abstract class ArgType {
case '[': case '[':
return array(parse(type.substring(1))); return array(parse(type.substring(1)));
default: default:
if (type.length() != 1) {
throw new JadxRuntimeException("Unknown type string: \"" + type + '"');
}
return parse(f); return parse(f);
} }
} }
@@ -772,7 +778,7 @@ public abstract class ArgType {
return VOID; return VOID;
default: default:
return null; throw new JadxRuntimeException("Unknown type char: '" + f + "' (0x" + Integer.toHexString(f) + ')');
} }
} }
@@ -292,6 +292,17 @@ public abstract class InsnArg extends Typed {
return false; return false;
} }
public boolean isSameVar(SSAVar ssaVar) {
if (ssaVar == null) {
return false;
}
if (isRegister()) {
SSAVar thisSsaVar = ((RegisterArg) this).getSVar();
return Objects.equals(thisSsaVar, ssaVar);
}
return false;
}
public boolean isSameCodeVar(RegisterArg arg) { public boolean isSameCodeVar(RegisterArg arg) {
if (arg == null) { if (arg == null) {
return false; return false;
@@ -23,6 +23,9 @@ public final class LiteralArg extends InsnArg {
if (value == 1) { if (value == 1) {
return ArgType.NARROW_NUMBERS; return ArgType.NARROW_NUMBERS;
} }
if (value < 0) {
return ArgType.NARROW_NEG_NUMBERS;
}
return ArgType.NARROW_NUMBERS_NO_BOOL; return ArgType.NARROW_NUMBERS_NO_BOOL;
} }
@@ -18,7 +18,6 @@ import org.slf4j.LoggerFactory;
import jadx.api.DecompilationMode; import jadx.api.DecompilationMode;
import jadx.api.ICodeCache; import jadx.api.ICodeCache;
import jadx.api.ICodeInfo; import jadx.api.ICodeInfo;
import jadx.api.JadxArgs;
import jadx.api.JavaClass; import jadx.api.JavaClass;
import jadx.api.impl.SimpleCodeInfo; import jadx.api.impl.SimpleCodeInfo;
import jadx.api.impl.SimpleCodeWriter; import jadx.api.impl.SimpleCodeWriter;
@@ -38,8 +37,6 @@ import jadx.api.plugins.input.data.attributes.types.SourceFileAttr;
import jadx.api.plugins.input.data.impl.ListConsumer; import jadx.api.plugins.input.data.impl.ListConsumer;
import jadx.api.usage.IUsageInfoData; import jadx.api.usage.IUsageInfoData;
import jadx.core.Consts; import jadx.core.Consts;
import jadx.core.Jadx;
import jadx.core.ProcessClass;
import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.InlinedAttr; import jadx.core.dex.attributes.nodes.InlinedAttr;
@@ -72,6 +69,7 @@ public class ClassNode extends NotificationAttrNode
private ArgType superClass; private ArgType superClass;
private List<ArgType> interfaces; private List<ArgType> interfaces;
private List<ArgType> generics = Collections.emptyList(); private List<ArgType> generics = Collections.emptyList();
private String inputFileName;
private List<MethodNode> methods; private List<MethodNode> methods;
private List<FieldNode> fields; private List<FieldNode> fields;
@@ -123,6 +121,7 @@ public class ClassNode extends NotificationAttrNode
this.accessFlags = new AccessInfo(getAccessFlags(cls), AFType.CLASS); this.accessFlags = new AccessInfo(getAccessFlags(cls), AFType.CLASS);
this.superClass = checkSuperType(cls); this.superClass = checkSuperType(cls);
this.interfaces = Utils.collectionMap(cls.getInterfacesTypes(), ArgType::object); this.interfaces = Utils.collectionMap(cls.getInterfacesTypes(), ArgType::object);
setInputFileName(cls.getInputFileName());
ListConsumer<IFieldData, FieldNode> fieldsConsumer = new ListConsumer<>(fld -> FieldNode.build(this, fld)); ListConsumer<IFieldData, FieldNode> fieldsConsumer = new ListConsumer<>(fld -> FieldNode.build(this, fld));
ListConsumer<IMethodData, MethodNode> methodsConsumer = new ListConsumer<>(mth -> MethodNode.build(this, mth)); ListConsumer<IMethodData, MethodNode> methodsConsumer = new ListConsumer<>(mth -> MethodNode.build(this, mth));
@@ -150,6 +149,8 @@ public class ClassNode extends NotificationAttrNode
IUsageInfoData usageInfoData = root.getArgs().getUsageInfoCache().get(root); IUsageInfoData usageInfoData = root.getArgs().getUsageInfoCache().get(root);
if (usageInfoData != null) { if (usageInfoData != null) {
usageInfoData.applyForClass(this); usageInfoData.applyForClass(this);
} else {
LOG.warn("Can't restore usage data for class: {}", this);
} }
} }
@@ -226,6 +227,7 @@ public class ClassNode extends NotificationAttrNode
public static ClassNode addSyntheticClass(RootNode root, ClassInfo clsInfo, int accessFlags) { public static ClassNode addSyntheticClass(RootNode root, ClassInfo clsInfo, int accessFlags) {
ClassNode cls = new ClassNode(root, clsInfo, accessFlags); ClassNode cls = new ClassNode(root, clsInfo, accessFlags);
cls.add(AFlag.SYNTHETIC); cls.add(AFlag.SYNTHETIC);
cls.setInputFileName("synthetic");
cls.setState(ProcessState.PROCESS_COMPLETE); cls.setState(ProcessState.PROCESS_COMPLETE);
root.addClassNode(cls); root.addClassNode(cls);
return cls; return cls;
@@ -315,23 +317,25 @@ public class ClassNode extends NotificationAttrNode
* WARNING: Slow operation! Use with caution! * WARNING: Slow operation! Use with caution!
*/ */
public ICodeInfo decompileWithMode(DecompilationMode mode) { public ICodeInfo decompileWithMode(DecompilationMode mode) {
DecompilationMode baseMode = root.getArgs().getDecompilationMode(); switch (mode) {
if (mode == baseMode) { case AUTO:
return decompile(true); case RESTRUCTURE:
} return decompile(true);
synchronized (DECOMPILE_WITH_MODE_SYNC) {
JadxArgs args = root.getArgs(); case SIMPLE:
try { case FALLBACK:
unload(); synchronized (DECOMPILE_WITH_MODE_SYNC) {
args.setDecompilationMode(mode); try {
ProcessClass process = new ProcessClass(Jadx.getPassesList(args)); unload();
process.initPasses(root); ICodeInfo code = root.getProcessClasses().forceGenerateCodeForMode(this, mode);
ICodeInfo code = process.forceGenerateCode(this); return Utils.getOrElse(code, ICodeInfo.EMPTY);
return Utils.getOrElse(code, ICodeInfo.EMPTY); } finally {
} finally { unload();
args.setDecompilationMode(baseMode); }
unload(); }
}
default:
throw new JadxRuntimeException("Unknown mode: " + mode);
} }
} }
@@ -401,7 +405,7 @@ public class ClassNode extends NotificationAttrNode
ICodeInfo codeInfo = root.getProcessClasses().generateCode(this); ICodeInfo codeInfo = root.getProcessClasses().generateCode(this);
processDefinitionAnnotations(codeInfo); processDefinitionAnnotations(codeInfo);
return codeInfo; return codeInfo;
} catch (Throwable e) { } catch (StackOverflowError | Exception e) {
addError("Code generation failed", e); addError("Code generation failed", e);
return new SimpleCodeInfo(Utils.getStackTrace(e)); return new SimpleCodeInfo(Utils.getStackTrace(e));
} }
@@ -610,17 +614,9 @@ public class ClassNode extends NotificationAttrNode
return parentClass; return parentClass;
} }
public void updateParentClass() { public void notInner() {
if (clsInfo.isInner()) { this.clsInfo.notInner(root);
ClassNode parent = root.resolveClass(clsInfo.getParentClass()); this.parentClass = this;
if (parent != null) {
parentClass = parent;
return;
}
// undo inner mark in class info
clsInfo.notInner(root);
}
parentClass = this;
} }
/** /**
@@ -630,32 +626,36 @@ public class ClassNode extends NotificationAttrNode
*/ */
@Override @Override
public void rename(String newName) { public void rename(String newName) {
int lastDot = newName.lastIndexOf('.'); if (newName.indexOf('.') == -1) {
if (lastDot == -1) {
clsInfo.changeShortName(newName); clsInfo.changeShortName(newName);
return; return;
} }
if (clsInfo.isInner()) { // full name provided
addWarn("Can't change package for inner class: " + this + " to " + newName); ClassInfo newClsInfo = ClassInfo.fromNameWithoutCache(root, newName, clsInfo.isInner());
return;
}
// change class package // change class package
String newPkg = newName.substring(0, lastDot); String newPkg = newClsInfo.getPackage();
String newShortName = newName.substring(lastDot + 1); String newShortName = newClsInfo.getShortName();
if (changeClassNodePackage(newPkg)) { if (clsInfo.isInner()) {
clsInfo.changePkgAndName(newPkg, newShortName); if (!newPkg.equals(clsInfo.getPackage())) {
} else { addWarn("Can't change package for inner class: " + this + " to " + newName);
}
clsInfo.changeShortName(newShortName); clsInfo.changeShortName(newShortName);
} else {
if (changeClassNodePackage(newPkg)) {
clsInfo.changePkgAndName(newPkg, newShortName);
} else {
clsInfo.changeShortName(newShortName);
}
} }
} }
private boolean changeClassNodePackage(String fullPkg) { private boolean changeClassNodePackage(String fullPkg) {
if (clsInfo.isInner()) {
throw new JadxRuntimeException("Can't change package for inner class: " + clsInfo);
}
if (fullPkg.equals(clsInfo.getAliasPkg())) { if (fullPkg.equals(clsInfo.getAliasPkg())) {
return false; return false;
} }
if (clsInfo.isInner()) {
throw new JadxRuntimeException("Can't change package for inner class: " + clsInfo);
}
root.removeClsFromPackage(packageNode, this); root.removeClsFromPackage(packageNode, this);
packageNode = PackageNode.getForClass(root, fullPkg, this); packageNode = PackageNode.getForClass(root, fullPkg, this);
root.sortPackages(); root.sortPackages();
@@ -876,7 +876,7 @@ public class ClassNode extends NotificationAttrNode
code.startLine(String.format("###### Class %s (%s)", getFullName(), getRawName())); code.startLine(String.format("###### Class %s (%s)", getFullName(), getRawName()));
try { try {
code.startLine(clsData.getDisassembledCode()); code.startLine(clsData.getDisassembledCode());
} catch (Throwable e) { } catch (Exception e) {
code.startLine("Failed to disassemble class:"); code.startLine("Failed to disassemble class:");
code.startLine(Utils.getStackTrace(e)); code.startLine(Utils.getStackTrace(e));
} }
@@ -963,7 +963,11 @@ public class ClassNode extends NotificationAttrNode
@Override @Override
public String getInputFileName() { public String getInputFileName() {
return clsData == null ? "synthetic" : clsData.getInputFileName(); return inputFileName;
}
public void setInputFileName(String inputFileName) {
this.inputFileName = inputFileName;
} }
public JavaClass getJavaNode() { public JavaClass getJavaNode() {
@@ -1,6 +1,6 @@
package jadx.core.dex.nodes; package jadx.core.dex.nodes;
import java.util.Collections; import java.util.ArrayList;
import java.util.List; import java.util.List;
import jadx.core.dex.attributes.AttrNode; import jadx.core.dex.attributes.AttrNode;
@@ -14,7 +14,9 @@ public final class InsnContainer extends AttrNode implements IBlock {
private final List<InsnNode> insns; private final List<InsnNode> insns;
public InsnContainer(InsnNode insn) { public InsnContainer(InsnNode insn) {
this.insns = Collections.singletonList(insn); List<InsnNode> list = new ArrayList<>(1);
list.add(insn);
this.insns = list;
} }
public InsnContainer(List<InsnNode> insns) { public InsnContainer(List<InsnNode> insns) {
@@ -28,11 +30,11 @@ public final class InsnContainer extends AttrNode implements IBlock {
@Override @Override
public String baseString() { public String baseString() {
return Integer.toString(insns.size()); return "IC";
} }
@Override @Override
public String toString() { public String toString() {
return "InsnContainer:" + insns.size(); return "InsnContainer";
} }
} }
@@ -217,6 +217,19 @@ public class InsnNode extends LineAttrNode {
} }
} }
public boolean isExitEdgeInsn() {
switch (getType()) {
case RETURN:
case THROW:
case CONTINUE:
case BREAK:
return true;
default:
return false;
}
}
public boolean canRemoveResult() { public boolean canRemoveResult() {
switch (getType()) { switch (getType()) {
case INVOKE: case INVOKE:
@@ -48,6 +48,7 @@ import static jadx.core.utils.Utils.lockList;
public class MethodNode extends NotificationAttrNode implements IMethodDetails, ILoadable, ICodeNode, Comparable<MethodNode> { public class MethodNode extends NotificationAttrNode implements IMethodDetails, ILoadable, ICodeNode, Comparable<MethodNode> {
private static final Logger LOG = LoggerFactory.getLogger(MethodNode.class); private static final Logger LOG = LoggerFactory.getLogger(MethodNode.class);
private static final InsnNode[] EMPTY_INSN_ARRAY = new InsnNode[0];
private final MethodInfo mthInfo; private final MethodInfo mthInfo;
private final ClassNode parentClass; private final ClassNode parentClass;
@@ -154,8 +155,14 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
this.regsCount = codeReader.getRegistersCount(); this.regsCount = codeReader.getRegistersCount();
this.argsStartReg = codeReader.getArgsStartReg(); this.argsStartReg = codeReader.getArgsStartReg();
initArguments(this.argTypes); initArguments(this.argTypes);
InsnDecoder decoder = new InsnDecoder(this);
this.instructions = decoder.process(codeReader); if (contains(AType.JADX_ERROR)) {
// don't load instructions for method with errors
this.instructions = EMPTY_INSN_ARRAY;
} else {
InsnDecoder decoder = new InsnDecoder(this);
this.instructions = decoder.process(codeReader);
}
} catch (Exception e) { } catch (Exception e) {
if (!noCode) { if (!noCode) {
unload(); unload();
@@ -3,6 +3,7 @@ package jadx.core.dex.nodes;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.stream.Collectors;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -188,6 +189,14 @@ public class PackageNode extends LineAttrNode
return classes; return classes;
} }
public List<ClassNode> getClassesNoDup() {
return classes.stream()
.map(ClassNode::getClassInfo)
.collect(Collectors.toSet())
.stream()
.map(e -> root.resolveClass(e)).collect(Collectors.toList());
}
public JavaPackage getJavaNode() { public JavaPackage getJavaNode() {
return javaNode; return javaNode;
} }
@@ -4,8 +4,11 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -42,6 +45,7 @@ import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.info.PackageInfo; import jadx.core.dex.info.PackageInfo;
import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.nodes.utils.MethodUtils; import jadx.core.dex.nodes.utils.MethodUtils;
import jadx.core.dex.nodes.utils.SelectFromDuplicates;
import jadx.core.dex.nodes.utils.TypeUtils; import jadx.core.dex.nodes.utils.TypeUtils;
import jadx.core.dex.visitors.DepthTraversal; import jadx.core.dex.visitors.DepthTraversal;
import jadx.core.dex.visitors.IDexTreeVisitor; import jadx.core.dex.visitors.IDexTreeVisitor;
@@ -149,7 +153,7 @@ public class RootNode {
public void finishClassLoad() { public void finishClassLoad() {
if (classes.size() != clsMap.size()) { if (classes.size() != clsMap.size()) {
// class name duplication detected // class name duplication detected
markDuplicatedClasses(classes); fixDuplicatedClasses();
} }
classes = new ArrayList<>(clsMap.values()); classes = new ArrayList<>(clsMap.values());
@@ -159,7 +163,7 @@ public class RootNode {
LOG.info("Loaded classes: {}, methods: {}, instructions: {}", classes.size(), mthCount, insnsCount); LOG.info("Loaded classes: {}, methods: {}, instructions: {}", classes.size(), mthCount, insnsCount);
// sort classes by name, expect top classes before inner // sort classes by name, expect top classes before inner
classes.sort(Comparator.comparing(ClassNode::getFullName)); classes.sort(Comparator.comparing(ClassNode::getRawName));
if (args.isMoveInnerClasses()) { if (args.isMoveInnerClasses()) {
// detect and move inner classes // detect and move inner classes
@@ -191,24 +195,30 @@ public class RootNode {
} }
} }
private static void markDuplicatedClasses(List<ClassNode> classes) { private void fixDuplicatedClasses() {
classes.stream() classes.stream()
.collect(Collectors.groupingBy(ClassNode::getClassInfo)) .collect(Collectors.groupingBy(ClassNode::getClassInfo))
.entrySet() .entrySet()
.stream() .stream()
.filter(entry -> entry.getValue().size() > 1) .filter(entry -> entry.getValue().size() > 1)
.forEach(entry -> { .forEach(entry -> {
List<String> sources = Utils.collectionMap(entry.getValue(), ClassNode::getInputFileName); ClassInfo clsInfo = entry.getKey();
LOG.warn("Found duplicated class: {}, count: {}. Only one will be loaded!\n {}", List<ClassNode> dupClsList = entry.getValue();
entry.getKey(), entry.getValue().size(), String.join("\n ", sources)); ClassNode selectedCls = SelectFromDuplicates.process(dupClsList);
entry.getValue().forEach(cls -> {
String thisSource = cls.getInputFileName(); // keep only selected class in classes maps
String otherSourceStr = sources.stream() clsMap.put(clsInfo, selectedCls);
.filter(s -> !s.equals(thisSource)) rawClsMap.put(selectedCls.getRawName(), selectedCls);
.sorted()
.collect(Collectors.joining("\n ")); String selectedSource = selectedCls.getInputFileName();
cls.addWarnComment("Classes with same name are omitted:\n " + otherSourceStr + '\n'); String sources = dupClsList.stream()
}); .map(ClassNode::getInputFileName)
.sorted()
.collect(Collectors.joining("\n "));
LOG.warn("Found duplicated class: {}, count: {}, sources:"
+ "\n {}\n Keep class with source: {}, others will be removed.",
clsInfo, dupClsList.size(), sources, selectedSource);
selectedCls.addWarnComment("Classes with same name are omitted, all sources:\n " + sources + '\n');
}); });
} }
@@ -309,10 +319,8 @@ public class RootNode {
ClassInfo clsInfo = cls.getClassInfo(); ClassInfo clsInfo = cls.getClassInfo();
ClassNode parent = resolveParentClass(clsInfo); ClassNode parent = resolveParentClass(clsInfo);
if (parent == null) { if (parent == null) {
clsMap.remove(clsInfo);
clsInfo.notInner(this);
clsMap.put(clsInfo, cls);
updated.add(cls); updated.add(cls);
cls.notInner();
} else { } else {
parent.addInnerClass(cls); parent.addInnerClass(cls);
} }
@@ -323,7 +331,6 @@ public class RootNode {
innerCls.getClassInfo().updateNames(this); innerCls.getClassInfo().updateNames(this);
} }
} }
classes.forEach(ClassNode::updateParentClass);
for (PackageNode pkg : packages) { for (PackageNode pkg : packages) {
pkg.getClasses().removeIf(cls -> cls.getClassInfo().isInner()); pkg.getClasses().removeIf(cls -> cls.getClassInfo().isInner());
} }
@@ -345,6 +352,19 @@ public class RootNode {
preDecompilePasses = DebugChecks.insertPasses(preDecompilePasses); preDecompilePasses = DebugChecks.insertPasses(preDecompilePasses);
processClasses = new ProcessClass(DebugChecks.insertPasses(processClasses.getPasses())); processClasses = new ProcessClass(DebugChecks.insertPasses(processClasses.getPasses()));
} }
List<String> disabledPasses = args.getDisabledPasses();
if (!disabledPasses.isEmpty()) {
Set<String> disabledSet = new HashSet<>(disabledPasses);
Predicate<IDexTreeVisitor> filter = p -> {
if (disabledSet.contains(p.getName())) {
LOG.debug("Disable pass: {}", p.getName());
return true;
}
return false;
};
preDecompilePasses.removeIf(filter);
processClasses.getPasses().removeIf(filter);
}
} }
public void runPreDecompileStage() { public void runPreDecompileStage() {
@@ -0,0 +1,74 @@
package jadx.core.dex.nodes.utils;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.dex.nodes.ClassNode;
/**
* Select best class from list of classes with same full name
* Current implementation: use class with source file as 'classesN.dex' where N is minimal
*/
public class SelectFromDuplicates {
private static final Logger LOG = LoggerFactory.getLogger(SelectFromDuplicates.class);
private static final Pattern CLASSES_DEX_PATTERN = Pattern.compile("classes([1-9]\\d*)\\.dex");
public static ClassNode process(List<ClassNode> dupClsList) {
ClassNode bestCls = null;
int bestClsIndex = -1;
for (ClassNode clsNode : dupClsList) {
boolean selectCurrent = false;
if (bestCls == null) {
selectCurrent = true;
} else {
int clsIndex = getClassesIndex(clsNode.getInputFileName());
if (clsIndex != -1) {
if (bestClsIndex != -1) {
// if both are valid, the lower index has precedence
if (clsIndex < bestClsIndex) {
selectCurrent = true;
}
} else {
// valid dex names have precedence
selectCurrent = true;
}
}
}
if (selectCurrent) {
bestCls = clsNode;
bestClsIndex = getClassesIndex(clsNode.getInputFileName());
}
}
return bestCls;
}
/**
* Get N from classesN.dex
*
* @return -1 if source is not valid dex name
*/
private static int getClassesIndex(String source) {
if ("classes.dex".equals(source)) {
return 1;
}
try {
Matcher matcher = CLASSES_DEX_PATTERN.matcher(source);
if (!matcher.matches()) {
return -1;
}
String num = matcher.group(1);
if (num.equals("1")) {
return -1;
}
return Integer.parseInt(num);
} catch (Exception e) {
LOG.debug("Failed to parse source classes index", e);
return -1;
}
}
}
@@ -242,9 +242,9 @@ public class TypeUtils {
} }
Map<ArgType, ArgType> map = new HashMap<>(1 + invokeInsn.getArgsCount()); Map<ArgType, ArgType> map = new HashMap<>(1 + invokeInsn.getArgsCount());
addTypeVarMapping(map, mthDetails.getReturnType(), invokeInsn.getResult()); addTypeVarMapping(map, mthDetails.getReturnType(), invokeInsn.getResult());
int argCount = Math.min(mthDetails.getArgTypes().size(), invokeInsn.getArgsCount()); int argCount = Math.min(mthDetails.getArgTypes().size(), invokeInsn.getArgsCount() - invokeInsn.getFirstArgOffset());
for (int i = 0; i < argCount; i++) { for (int i = 0; i < argCount; i++) {
addTypeVarMapping(map, mthDetails.getArgTypes().get(i), invokeInsn.getArg(i)); addTypeVarMapping(map, mthDetails.getArgTypes().get(i), invokeInsn.getArg(i + invokeInsn.getFirstArgOffset()));
} }
return map; return map;
} }
@@ -41,6 +41,10 @@ public final class SwitchRegion extends AbstractRegion implements IBranchRegion
this.container = container; this.container = container;
} }
public boolean isDefaultCase() {
return keys.size() == 1 && keys.get(0) == DEFAULT_CASE_KEY;
}
public List<Object> getKeys() { public List<Object> getKeys() {
return keys; return keys;
} }
@@ -86,13 +90,13 @@ public final class SwitchRegion extends AbstractRegion implements IBranchRegion
@Override @Override
public String baseString() { public String baseString() {
return header.baseString(); return "SW:" + header.baseString();
} }
@Override @Override
public String toString() { public String toString() {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("Switch: ").append(cases.size()); sb.append("Switch: ").append(header.baseString());
for (CaseInfo caseInfo : cases) { for (CaseInfo caseInfo : cases) {
List<String> keyStrings = Utils.collectionMap(caseInfo.getKeys(), List<String> keyStrings = Utils.collectionMap(caseInfo.getKeys(),
k -> k == DEFAULT_CASE_KEY ? "default" : k.toString()); k -> k == DEFAULT_CASE_KEY ? "default" : k.toString());
@@ -12,7 +12,7 @@ public class DepthTraversal {
cls.getInnerClasses().forEach(inCls -> visit(visitor, inCls)); cls.getInnerClasses().forEach(inCls -> visit(visitor, inCls));
cls.getMethods().forEach(mth -> visit(visitor, mth)); cls.getMethods().forEach(mth -> visit(visitor, mth));
} }
} catch (StackOverflowError | Exception e) { } catch (StackOverflowError | BootstrapMethodError | Exception e) {
cls.addError(e.getClass().getSimpleName() + " in pass: " + visitor.getClass().getSimpleName(), e); cls.addError(e.getClass().getSimpleName() + " in pass: " + visitor.getClass().getSimpleName(), e);
} }
} }
@@ -23,7 +23,7 @@ public class DepthTraversal {
return; return;
} }
visitor.visit(mth); visitor.visit(mth);
} catch (StackOverflowError | Exception e) { } catch (StackOverflowError | BootstrapMethodError | Exception e) {
mth.addError(e.getClass().getSimpleName() + " in pass: " + visitor.getClass().getSimpleName(), e); mth.addError(e.getClass().getSimpleName() + " in pass: " + visitor.getClass().getSimpleName(), e);
} }
} }
@@ -5,6 +5,7 @@ import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.regex.Matcher;
import jadx.api.ICodeWriter; import jadx.api.ICodeWriter;
import jadx.api.impl.SimpleCodeWriter; import jadx.api.impl.SimpleCodeWriter;
@@ -30,6 +31,7 @@ import static jadx.core.codegen.MethodGen.FallbackOption.BLOCK_DUMP;
public class DotGraphVisitor extends AbstractVisitor { public class DotGraphVisitor extends AbstractVisitor {
private static final String NL = "\\l"; private static final String NL = "\\l";
private static final String NLQR = Matcher.quoteReplacement(NL);
private static final boolean PRINT_DOMINATORS = false; private static final boolean PRINT_DOMINATORS = false;
private static final boolean PRINT_DOMINATORS_INFO = false; private static final boolean PRINT_DOMINATORS_INFO = false;
@@ -324,7 +326,7 @@ public class DotGraphVisitor extends AbstractVisitor {
.replace("\"", "\\\"") .replace("\"", "\\\"")
.replace("-", "\\-") .replace("-", "\\-")
.replace("|", "\\|") .replace("|", "\\|")
.replaceAll("\\R", NL); .replaceAll("\\R", NLQR);
} }
} }
} }
@@ -1,7 +1,6 @@
package jadx.core.dex.visitors; package jadx.core.dex.visitors;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -153,12 +152,12 @@ public class MethodThrowsVisitor extends AbstractVisitor {
return; return;
} }
} }
visitThrows(mth, exceptionType); visitThrows(mth, exceptionType, excludedExceptions);
} else { } else {
if (throwArg instanceof InsnWrapArg) { if (throwArg instanceof InsnWrapArg) {
InsnWrapArg insnWrapArg = (InsnWrapArg) throwArg; InsnWrapArg insnWrapArg = (InsnWrapArg) throwArg;
ArgType exceptionType = insnWrapArg.getType(); ArgType exceptionType = insnWrapArg.getType();
visitThrows(mth, exceptionType); visitThrows(mth, exceptionType, excludedExceptions);
} }
} }
return; return;
@@ -180,7 +179,9 @@ public class MethodThrowsVisitor extends AbstractVisitor {
MethodThrowsAttr cAttr = cMth.get(AType.METHOD_THROWS); MethodThrowsAttr cAttr = cMth.get(AType.METHOD_THROWS);
MethodThrowsAttr attr = mth.get(AType.METHOD_THROWS); MethodThrowsAttr attr = mth.get(AType.METHOD_THROWS);
if (attr != null && cAttr != null && !cAttr.getList().isEmpty()) { if (attr != null && cAttr != null && !cAttr.getList().isEmpty()) {
attr.getList().addAll(filterExceptions(cAttr.getList(), excludedExceptions)); for (String argTypeStr : cAttr.getList()) {
visitThrows(mth, ArgType.object(argTypeStr), excludedExceptions);
}
} }
} else { } else {
ClspClass clsDetails = root.getClsp().getClsDetails(classInfo.getType()); ClspClass clsDetails = root.getClsp().getClsDetails(classInfo.getType());
@@ -189,8 +190,8 @@ public class MethodThrowsVisitor extends AbstractVisitor {
if (cMth != null && cMth.getThrows() != null && !cMth.getThrows().isEmpty()) { if (cMth != null && cMth.getThrows() != null && !cMth.getThrows().isEmpty()) {
MethodThrowsAttr attr = mth.get(AType.METHOD_THROWS); MethodThrowsAttr attr = mth.get(AType.METHOD_THROWS);
if (attr != null) { if (attr != null) {
for (ArgType ex : cMth.getThrows()) { for (ArgType argType : cMth.getThrows()) {
attr.getList().add(ex.getObject()); visitThrows(mth, argType, excludedExceptions);
} }
} }
} }
@@ -199,8 +200,14 @@ public class MethodThrowsVisitor extends AbstractVisitor {
} }
} }
private void visitThrows(MethodNode mth, ArgType excType) { private void visitThrows(MethodNode mth, ArgType excType, Set<String> excludedExceptions) {
if (excType.isTypeKnown() && isThrowsRequired(mth, excType)) { if (excType.isTypeKnown() && isThrowsRequired(mth, excType)) {
for (String excludedException : excludedExceptions) {
if (isBaseException(excType.getObject(), excludedException)) {
return;
}
}
mth.get(AType.METHOD_THROWS).getList().add(excType.getObject()); mth.get(AType.METHOD_THROWS).getList().add(excType.getObject());
} }
} }
@@ -231,7 +238,7 @@ public class MethodThrowsVisitor extends AbstractVisitor {
} }
/** /**
* @return is 'possibleParent' a exception class of 'exception' * @return is 'possibleParent' an exception class of 'exception'
*/ */
private boolean isBaseException(String exception, String possibleParent) { private boolean isBaseException(String exception, String possibleParent) {
if (exception.equals(possibleParent)) { if (exception.equals(possibleParent)) {
@@ -247,23 +254,6 @@ public class MethodThrowsVisitor extends AbstractVisitor {
return root.getClsp().isImplements(type.getObject(), baseType.getObject()); return root.getClsp().isImplements(type.getObject(), baseType.getObject());
} }
private Collection<String> filterExceptions(Set<String> exceptions, Set<String> excludedExceptions) {
Set<String> filteredExceptions = new HashSet<>();
for (String exception : exceptions) {
boolean filtered = false;
for (String excluded : excludedExceptions) {
filtered = exception.equals(excluded) || isBaseException(exception, excluded);
if (filtered) {
break;
}
}
if (!filtered) {
filteredExceptions.add(exception);
}
}
return filteredExceptions;
}
private @Nullable MethodNode searchOverriddenMethod(ClassNode cls, MethodInfo mth, String signature) { private @Nullable MethodNode searchOverriddenMethod(ClassNode cls, MethodInfo mth, String signature) {
// search by exact full signature (with return value) to fight obfuscation (see test // search by exact full signature (with return value) to fight obfuscation (see test
// 'TestOverrideWithSameName') // 'TestOverrideWithSameName')
@@ -64,7 +64,7 @@ public class ProcessAnonymous extends AbstractVisitor {
private static void processClass(ClassNode cls) { private static void processClass(ClassNode cls) {
try { try {
markAnonymousClass(cls); markAnonymousClass(cls);
} catch (Throwable e) { } catch (StackOverflowError | Exception e) {
cls.addError("Anonymous visitor error", e); cls.addError("Anonymous visitor error", e);
} }
} }
@@ -231,6 +231,9 @@ public class SimplifyVisitor extends AbstractVisitor {
} }
ArgType castToType = (ArgType) castInsn.getIndex(); ArgType castToType = (ArgType) castInsn.getIndex();
if (isArithWideUpCast(parentInsn, argType, castToType)) {
return null;
}
if (!ArgType.isCastNeeded(mth.root(), argType, castToType) if (!ArgType.isCastNeeded(mth.root(), argType, castToType)
|| isCastDuplicate(castInsn) || isCastDuplicate(castInsn)
|| shadowedByOuterCast(mth.root(), castToType, parentInsn)) { || shadowedByOuterCast(mth.root(), castToType, parentInsn)) {
@@ -243,6 +246,21 @@ public class SimplifyVisitor extends AbstractVisitor {
return null; return null;
} }
/**
* Keep cast to wide types in arith instructions,
* because arguments type determine instruction used in result bytecode.
* Example: (long) i << 32 - without 'long' cast will be used 'int shift' instruction and result
* will be incorrect
*/
private static boolean isArithWideUpCast(@Nullable InsnNode parentInsn, ArgType argType, ArgType castToType) {
if (parentInsn != null
&& parentInsn.getType() == InsnType.ARITH
&& argType.isPrimitive() && castToType.isPrimitive()) {
return castToType.getRegCount() > argType.getRegCount();
}
return false;
}
private static boolean isCastDuplicate(IndexInsnNode castInsn) { private static boolean isCastDuplicate(IndexInsnNode castInsn) {
InsnArg arg = castInsn.getArg(0); InsnArg arg = castInsn.getArg(0);
if (arg.isRegister()) { if (arg.isRegister()) {
@@ -18,6 +18,7 @@ import jadx.api.plugins.input.data.attributes.IJadxAttrType;
import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.api.plugins.input.data.attributes.IJadxAttribute;
import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.CodeFeaturesAttr;
import jadx.core.dex.attributes.nodes.LoopInfo; import jadx.core.dex.attributes.nodes.LoopInfo;
import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.InsnType;
import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnArg;
@@ -315,6 +316,13 @@ public class BlockProcessor extends AbstractVisitor {
if (mergeConstReturn(mth)) { if (mergeConstReturn(mth)) {
return true; return true;
} }
if (CodeFeaturesAttr.contains(mth, CodeFeaturesAttr.CodeFeature.SWITCH)) {
for (BlockNode basicBlock : mth.getBasicBlocks()) {
if (duplicateSimpleMoveBlock(mth, basicBlock)) {
return true;
}
}
}
return splitExitBlocks(mth); return splitExitBlocks(mth);
} }
@@ -383,6 +391,65 @@ public class BlockProcessor extends AbstractVisitor {
return changed; return changed;
} }
/**
* Duplicate block if it contains only one 'move' insn and all predecessors are 'switch' and 'if'.
* This will help to resolve switch cases order and fallthrough detection
* because such move blocks can be deduplicated by compiler.
*/
private static boolean duplicateSimpleMoveBlock(MethodNode mth, BlockNode block) {
List<InsnNode> insns = block.getInstructions();
if (insns.size() == 1 && block.getSuccessors().size() == 1) {
InsnNode insn = insns.get(0);
if (insn.getType() == InsnType.MOVE) {
List<BlockNode> preds = block.getPredecessors();
int predSize = preds.size();
if (predSize >= 3 && onlySwitchAndIfInLastInsns(preds)) {
// confirmed, duplicate block
BlockNode successor = block.getSuccessors().get(0);
List<BlockNode> predsCopy = new ArrayList<>(preds);
for (int i = 1; i < predSize; i++) {
BlockNode pred = predsCopy.get(i);
BlockNode newBlock = BlockSplitter.startNewBlock(mth, -1);
newBlock.add(AFlag.SYNTHETIC);
for (InsnNode oldInsn : block.getInstructions()) {
InsnNode copyInsn = oldInsn.copyWithoutSsa();
copyInsn.add(AFlag.SYNTHETIC);
newBlock.getInstructions().add(copyInsn);
}
newBlock.copyAttributesFrom(block);
BlockSplitter.replaceConnection(pred, block, newBlock);
BlockSplitter.connect(newBlock, successor);
}
return true;
}
}
}
return false;
}
private static boolean onlySwitchAndIfInLastInsns(List<BlockNode> preds) {
boolean hasSwitch = false;
boolean hasIf = false;
for (BlockNode pred : preds) {
InsnNode lastInsn = BlockUtils.getLastInsn(pred);
if (lastInsn == null) {
return false;
}
InsnType insnType = lastInsn.getType();
switch (insnType) {
case SWITCH:
hasSwitch = true;
break;
case IF:
hasIf = true;
break;
default:
return false;
}
}
return hasSwitch && hasIf;
}
private static boolean simplifyLoopEnd(MethodNode mth, LoopInfo loop) { private static boolean simplifyLoopEnd(MethodNode mth, LoopInfo loop) {
BlockNode loopEnd = loop.getEnd(); BlockNode loopEnd = loop.getEnd();
if (loopEnd.getSuccessors().size() <= 1) { if (loopEnd.getSuccessors().size() <= 1) {
@@ -61,7 +61,7 @@ public class PostDominatorTree {
} }
mth.addInfoComment("Infinite loop detected, blocks: " + blocksDelta + ", insns: " + insnsCount); mth.addInfoComment("Infinite loop detected, blocks: " + blocksDelta + ", insns: " + insnsCount);
} }
} catch (Throwable e) { } catch (StackOverflowError | Exception e) {
// show error as a warning because this info not always used // show error as a warning because this info not always used
mth.addWarnComment("Failed to build post-dominance tree", e); mth.addWarnComment("Failed to build post-dominance tree", e);
} finally { } finally {
@@ -134,7 +134,7 @@ public class DebugInfoApplyVisitor extends AbstractVisitor {
} }
public static boolean applyDebugInfo(MethodNode mth, SSAVar ssaVar, ArgType type, String varName) { public static boolean applyDebugInfo(MethodNode mth, SSAVar ssaVar, ArgType type, String varName) {
TypeUpdateResult result = mth.root().getTypeUpdate().applyWithWiderIgnoreUnknown(mth, ssaVar, type); TypeUpdateResult result = mth.root().getTypeUpdate().applyDebugInfo(mth, ssaVar, type);
if (result == TypeUpdateResult.REJECT) { if (result == TypeUpdateResult.REJECT) {
if (Consts.DEBUG_TYPE_INFERENCE) { if (Consts.DEBUG_TYPE_INFERENCE) {
LOG.debug("Reject debug info of type: {} and name: '{}' for {}, mth: {}", type, varName, ssaVar, mth); LOG.debug("Reject debug info of type: {} and name: '{}' for {}, mth: {}", type, varName, ssaVar, mth);
@@ -13,6 +13,7 @@ import jadx.api.JadxArgs.UseKotlinMethodsForVarNames;
import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.api.plugins.input.data.attributes.JadxAttrType;
import jadx.core.deobf.NameMapper; import jadx.core.deobf.NameMapper;
import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.info.FieldInfo; import jadx.core.dex.info.FieldInfo;
import jadx.core.dex.info.MethodInfo; import jadx.core.dex.info.MethodInfo;
@@ -87,7 +88,7 @@ public class ProcessKotlinInternals extends AbstractVisitor {
} }
private void processMth(MethodNode mth) { private void processMth(MethodNode mth) {
if (mth.isNoCode()) { if (mth.isNoCode() || mth.contains(AType.JADX_ERROR)) {
return; return;
} }
for (BlockNode block : mth.getBasicBlocks()) { for (BlockNode block : mth.getBasicBlocks()) {
@@ -12,7 +12,7 @@ public abstract class AbstractRegionVisitor implements IRegionVisitor {
} }
@Override @Override
public void processBlock(MethodNode mth, IBlock container) { public void processBlock(MethodNode mth, IBlock block) {
} }
@Override @Override
@@ -30,7 +30,17 @@ public class IfRegionVisitor extends AbstractVisitor {
process(mth); process(mth);
} }
public static void process(MethodNode mth) { public static void processIfRequested(MethodNode mth) {
if (mth.contains(AFlag.REQUEST_IF_REGION_OPTIMIZE)) {
try {
process(mth);
} finally {
mth.remove(AFlag.REQUEST_IF_REGION_OPTIMIZE);
}
}
}
private static void process(MethodNode mth) {
TernaryMod.process(mth); TernaryMod.process(mth);
DepthRegionTraversal.traverse(mth, PROCESS_IF_REGION_VISITOR); DepthRegionTraversal.traverse(mth, PROCESS_IF_REGION_VISITOR);
DepthRegionTraversal.traverseIterative(mth, REMOVE_REDUNDANT_ELSE_VISITOR); DepthRegionTraversal.traverseIterative(mth, REMOVE_REDUNDANT_ELSE_VISITOR);
@@ -48,7 +58,7 @@ public class IfRegionVisitor extends AbstractVisitor {
} }
} }
@SuppressWarnings({ "UnnecessaryReturnStatement", "StatementWithEmptyBody" }) @SuppressWarnings({ "UnnecessaryReturnStatement" })
private static void orderBranches(MethodNode mth, IfRegion ifRegion) { private static void orderBranches(MethodNode mth, IfRegion ifRegion) {
if (RegionUtils.isEmpty(ifRegion.getElseRegion())) { if (RegionUtils.isEmpty(ifRegion.getElseRegion())) {
return; return;
@@ -158,7 +168,7 @@ public class IfRegionVisitor extends AbstractVisitor {
} }
} }
@SuppressWarnings("StatementWithEmptyBody") @SuppressWarnings("UnnecessaryParentheses")
private static boolean removeRedundantElseBlock(MethodNode mth, IfRegion ifRegion) { private static boolean removeRedundantElseBlock(MethodNode mth, IfRegion ifRegion) {
if (ifRegion.getElseRegion() == null) { if (ifRegion.getElseRegion() == null) {
return false; return false;
@@ -53,10 +53,7 @@ public class LoopRegionVisitor extends AbstractVisitor implements IRegionVisitor
@Override @Override
public void visit(MethodNode mth) { public void visit(MethodNode mth) {
DepthRegionTraversal.traverse(mth, this); DepthRegionTraversal.traverse(mth, this);
if (mth.contains(AFlag.REQUEST_IF_REGION_OPTIMIZE)) { IfRegionVisitor.processIfRequested(mth);
IfRegionVisitor.process(mth);
mth.remove(AFlag.REQUEST_IF_REGION_OPTIMIZE);
}
} }
@Override @Override
@@ -396,7 +393,7 @@ public class LoopRegionVisitor extends AbstractVisitor implements IRegionVisitor
if (insn.getType() == InsnType.INVOKE) { if (insn.getType() == InsnType.INVOKE) {
InvokeNode inv = (InvokeNode) insn; InvokeNode inv = (InvokeNode) insn;
MethodInfo callMth = inv.getCallMth(); MethodInfo callMth = inv.getCallMth();
if (inv.getInvokeType() == InvokeType.INTERFACE if ((inv.getInvokeType() == InvokeType.INTERFACE || inv.getInvokeType() == InvokeType.VIRTUAL)
&& callMth.getShortId().equals(mthId)) { && callMth.getShortId().equals(mthId)) {
if (declClsFullName == null) { if (declClsFullName == null) {
return true; return true;
@@ -1,20 +1,11 @@
package jadx.core.dex.visitors.regions; package jadx.core.dex.visitors.regions;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.EdgeInsnAttr; import jadx.core.dex.attributes.nodes.EdgeInsnAttr;
import jadx.core.dex.instructions.InsnType;
import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.IBlock;
import jadx.core.dex.nodes.IContainer; import jadx.core.dex.nodes.IContainer;
import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.IRegion;
import jadx.core.dex.nodes.InsnContainer; import jadx.core.dex.nodes.InsnContainer;
@@ -23,11 +14,9 @@ import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.regions.Region; import jadx.core.dex.regions.Region;
import jadx.core.dex.regions.SwitchRegion; import jadx.core.dex.regions.SwitchRegion;
import jadx.core.dex.regions.loops.LoopRegion; import jadx.core.dex.regions.loops.LoopRegion;
import jadx.core.utils.RegionUtils; import jadx.core.dex.visitors.regions.maker.SwitchRegionMaker;
final class PostProcessRegions extends AbstractRegionVisitor {
private static final Logger LOG = LoggerFactory.getLogger(PostProcessRegions.class);
public final class PostProcessRegions extends AbstractRegionVisitor {
private static final IRegionVisitor INSTANCE = new PostProcessRegions(); private static final IRegionVisitor INSTANCE = new PostProcessRegions();
static void process(MethodNode mth) { static void process(MethodNode mth) {
@@ -41,8 +30,7 @@ final class PostProcessRegions extends AbstractRegionVisitor {
LoopRegion loop = (LoopRegion) region; LoopRegion loop = (LoopRegion) region;
loop.mergePreCondition(); loop.mergePreCondition();
} else if (region instanceof SwitchRegion) { } else if (region instanceof SwitchRegion) {
// insert 'break' in switch cases (run after try/catch insertion) SwitchRegionMaker.insertBreaks(mth, (SwitchRegion) region);
processSwitch(mth, (SwitchRegion) region);
} else if (region instanceof Region) { } else if (region instanceof Region) {
insertEdgeInsn((Region) region); insertEdgeInsn((Region) region);
} }
@@ -76,55 +64,6 @@ final class PostProcessRegions extends AbstractRegionVisitor {
region.add(new InsnContainer(insns)); region.add(new InsnContainer(insns));
} }
private static void processSwitch(MethodNode mth, SwitchRegion sw) {
for (IContainer c : sw.getBranches()) {
if (c instanceof Region) {
Set<IBlock> blocks = new HashSet<>();
RegionUtils.getAllRegionBlocks(c, blocks);
if (blocks.isEmpty()) {
addBreakToContainer((Region) c);
} else {
for (IBlock block : blocks) {
if (block instanceof BlockNode) {
addBreakForBlock(mth, c, blocks, (BlockNode) block);
}
}
}
}
}
}
private static void addBreakToContainer(Region c) {
if (RegionUtils.hasExitEdge(c)) {
return;
}
List<InsnNode> insns = new ArrayList<>(1);
insns.add(new InsnNode(InsnType.BREAK, 0));
c.add(new InsnContainer(insns));
}
private static void addBreakForBlock(MethodNode mth, IContainer c, Set<IBlock> blocks, BlockNode bn) {
for (BlockNode s : bn.getCleanSuccessors()) {
if (!blocks.contains(s)
&& !bn.contains(AFlag.ADDED_TO_REGION)
&& !s.contains(AFlag.FALL_THROUGH)) {
addBreak(mth, c, bn);
return;
}
}
}
private static void addBreak(MethodNode mth, IContainer c, BlockNode bn) {
IContainer blockContainer = RegionUtils.getBlockContainer(c, bn);
if (blockContainer instanceof Region) {
addBreakToContainer((Region) blockContainer);
} else if (c instanceof Region) {
addBreakToContainer((Region) c);
} else {
LOG.warn("Can't insert break, container: {}, block: {}, mth: {}", blockContainer, bn, mth);
}
}
private PostProcessRegions() { private PostProcessRegions() {
// singleton // singleton
} }
@@ -0,0 +1,267 @@
package jadx.core.dex.visitors.regions;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
import org.jetbrains.annotations.Nullable;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.CodeFeaturesAttr;
import jadx.core.dex.attributes.nodes.RegionRefAttr;
import jadx.core.dex.instructions.InsnType;
import jadx.core.dex.nodes.IBlock;
import jadx.core.dex.nodes.IBranchRegion;
import jadx.core.dex.nodes.IContainer;
import jadx.core.dex.nodes.IRegion;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.regions.SwitchRegion;
import jadx.core.dex.visitors.AbstractVisitor;
import jadx.core.dex.visitors.JadxVisitor;
import jadx.core.dex.visitors.regions.maker.SwitchRegionMaker;
import jadx.core.utils.BlockInsnPair;
import jadx.core.utils.BlockParentContainer;
import jadx.core.utils.BlockUtils;
import jadx.core.utils.ListUtils;
import jadx.core.utils.RegionUtils;
import jadx.core.utils.exceptions.JadxException;
import static jadx.core.dex.attributes.nodes.CodeFeaturesAttr.CodeFeature.SWITCH;
@JadxVisitor(
name = "SwitchBreakVisitor",
desc = "Optimize 'break' instruction: common code extract, remove unreachable",
runAfter = LoopRegionVisitor.class // can add 'continue' at case end
)
public class SwitchBreakVisitor extends AbstractVisitor {
@Override
public void visit(MethodNode mth) throws JadxException {
if (CodeFeaturesAttr.contains(mth, SWITCH)) {
runSwitchTraverse(mth, ExtractCommonBreak::new);
runSwitchTraverse(mth, RemoveUnreachableBreak::new);
IfRegionVisitor.processIfRequested(mth);
}
}
private static void runSwitchTraverse(MethodNode mth, Supplier<BaseSwitchRegionVisitor> builder) {
DepthRegionTraversal.traverse(mth, new IterativeSwitchRegionVisitor(builder));
}
/**
* Add common 'break' if 'break' or exit insn ('return', 'throw', 'continue') found in all branches.
* Remove exist common break if all branches contain exit insn.
*/
private static final class ExtractCommonBreak extends BaseSwitchRegionVisitor {
@Override
public void processRegion(MethodNode mth, IRegion region) {
if (region instanceof IBranchRegion && !(region instanceof SwitchRegion)) {
// if break in all branches extract to parent region
processBranchRegion(mth, region);
}
}
private void processBranchRegion(MethodNode mth, IRegion region) {
IRegion parentRegion = region.getParent();
if (parentRegion.contains(AFlag.FALL_THROUGH)) {
// fallthrough case, can't extract break
return;
}
boolean dontAddCommonBreak = false;
IBlock lastParentBlock = RegionUtils.getLastBlock(parentRegion);
if (BlockUtils.containsExitInsn(lastParentBlock)) {
if (isBreakBlock(lastParentBlock)) {
// parent block already contains 'break'
dontAddCommonBreak = true;
} else {
// can't add 'break' after 'return', 'throw' or 'continue'
return;
}
}
List<IContainer> branches = ((IBranchRegion) region).getBranches();
boolean removeCommonBreak = true; // all branches contain exit insns, common break is unreachable
List<BlockParentContainer> forBreakRemove = new ArrayList<>();
for (IContainer branch : branches) {
if (branch == null) {
removeCommonBreak = false;
continue;
}
BlockInsnPair last = RegionUtils.getLastInsnWithBlock(branch);
if (last == null) {
return;
}
InsnNode lastInsn = last.getInsn();
if (lastInsn.getType() == InsnType.BREAK) {
IBlock block = last.getBlock();
IContainer parent = RegionUtils.getBlockContainer(branch, block);
forBreakRemove.add(new BlockParentContainer(parent, block));
removeCommonBreak = false;
} else if (!lastInsn.isExitEdgeInsn()) {
removeCommonBreak = false;
}
}
if (!forBreakRemove.isEmpty()) {
// common 'break' confirmed
for (BlockParentContainer breakData : forBreakRemove) {
removeBreak(breakData.getBlock(), breakData.getParent());
}
if (!dontAddCommonBreak) {
addBreakRegion.add(parentRegion);
// new 'break' might become 'common' for upper branch region, request to run checks again
requestReRun();
}
// removed 'break' may allow to use 'else-if' chain
mth.add(AFlag.REQUEST_IF_REGION_OPTIMIZE);
}
if (removeCommonBreak && lastParentBlock != null) {
removeBreak(lastParentBlock, parentRegion);
}
}
}
private static final class RemoveUnreachableBreak extends BaseSwitchRegionVisitor {
@Override
public void processRegion(MethodNode mth, IRegion region) {
List<IContainer> subBlocks = region.getSubBlocks();
IContainer lastContainer = ListUtils.last(subBlocks);
if (lastContainer instanceof IBlock) {
IBlock block = (IBlock) lastContainer;
if (isBreakBlock(block) && isPrevInsnIsExit(block, subBlocks)) {
removeBreak(block, region);
}
}
}
private boolean isPrevInsnIsExit(IBlock breakBlock, List<IContainer> subBlocks) {
InsnNode prevInsn = null;
if (breakBlock.getInstructions().size() > 1) {
// check prev insn in same block
List<InsnNode> insns = breakBlock.getInstructions();
prevInsn = insns.get(insns.size() - 2);
} else if (subBlocks.size() > 1) {
IContainer prev = subBlocks.get(subBlocks.size() - 2);
if (prev instanceof IBlock) {
List<InsnNode> insns = ((IBlock) prev).getInstructions();
prevInsn = ListUtils.last(insns);
}
}
return prevInsn != null && prevInsn.isExitEdgeInsn();
}
}
/**
* For every 'switch' region run new instance of provided 'switch' visitor.
* If rerun requested, run traverse for that visitor again.
*/
private static final class IterativeSwitchRegionVisitor extends AbstractRegionVisitor {
private final Supplier<BaseSwitchRegionVisitor> builder;
public IterativeSwitchRegionVisitor(Supplier<BaseSwitchRegionVisitor> builder) {
this.builder = builder;
}
@Override
public void leaveRegion(MethodNode mth, IRegion region) {
if (region instanceof SwitchRegion) {
SwitchRegion switchRegion = (SwitchRegion) region;
BaseSwitchRegionVisitor switchVisitor = builder.get();
switchVisitor.setCurrentSwitch(switchRegion);
boolean runAgain;
int k = 0;
do {
runAgain = false;
DepthRegionTraversal.traverse(mth, switchRegion, switchVisitor);
if (switchVisitor.isReRunRequested()) {
switchVisitor.reset();
runAgain = true;
}
if (k++ > 20) {
// 20 nested 'if' are not expected
mth.addWarnComment("Unexpected iteration count in SwitchBreakVisitor. Please report as an issue");
break;
}
} while (runAgain);
}
}
}
private abstract static class BaseSwitchRegionVisitor extends AbstractRegionVisitor {
protected final Set<IRegion> addBreakRegion = new HashSet<>();
protected final Set<IContainer> cleanupSet = new HashSet<>();
protected SwitchRegion currentSwitch;
private boolean reRunRequested = false;
public abstract void processRegion(MethodNode mth, IRegion region);
@Override
public boolean enterRegion(MethodNode mth, IRegion region) {
processRegion(mth, region);
return true;
}
@Override
public void leaveRegion(MethodNode mth, IRegion region) {
if (addBreakRegion.contains(region)) {
addBreakRegion.remove(region);
region.getSubBlocks().add(SwitchRegionMaker.buildBreakContainer(currentSwitch));
}
if (cleanupSet.contains(region)) {
cleanupSet.remove(region);
region.getSubBlocks().removeIf(r -> r.contains(AFlag.REMOVE));
}
}
/**
* Method called before visitor rerun
*/
public void reset() {
reRunRequested = false;
addBreakRegion.clear();
cleanupSet.clear();
}
public void requestReRun() {
reRunRequested = true;
}
public boolean isReRunRequested() {
return reRunRequested;
}
public void setCurrentSwitch(SwitchRegion currentSwitch) {
this.currentSwitch = currentSwitch;
}
protected boolean isBreakBlock(@Nullable IBlock block) {
if (block != null) {
InsnNode lastInsn = ListUtils.last(block.getInstructions());
if (lastInsn != null && lastInsn.getType() == InsnType.BREAK) {
RegionRefAttr regionRefAttr = lastInsn.get(AType.REGION_REF);
return regionRefAttr != null && regionRefAttr.getRegion() == currentSwitch;
}
}
return false;
}
protected void removeBreak(IBlock breakBlock, IContainer parentContainer) {
List<InsnNode> instructions = breakBlock.getInstructions();
InsnNode last = ListUtils.last(instructions);
if (last != null && last.getType() == InsnType.BREAK) {
ListUtils.removeLast(instructions);
if (instructions.isEmpty()) {
breakBlock.add(AFlag.REMOVE);
cleanupSet.add(parentContainer);
}
}
}
}
@Override
public String getName() {
return "SwitchBreakVisitor";
}
}
@@ -13,6 +13,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import jadx.api.plugins.input.data.annotations.EncodedType; import jadx.api.plugins.input.data.annotations.EncodedType;
import jadx.api.plugins.input.data.annotations.EncodedValue;
import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.api.plugins.input.data.attributes.JadxAttrType;
import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.IAttributeNode; import jadx.core.dex.attributes.IAttributeNode;
@@ -23,17 +24,20 @@ import jadx.core.dex.instructions.IfNode;
import jadx.core.dex.instructions.IfOp; import jadx.core.dex.instructions.IfOp;
import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.InsnType;
import jadx.core.dex.instructions.InvokeNode; import jadx.core.dex.instructions.InvokeNode;
import jadx.core.dex.instructions.PhiInsn;
import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.InsnWrapArg; import jadx.core.dex.instructions.args.InsnWrapArg;
import jadx.core.dex.instructions.args.LiteralArg; import jadx.core.dex.instructions.args.LiteralArg;
import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.instructions.args.SSAVar;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.IContainer; import jadx.core.dex.nodes.IContainer;
import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.IRegion;
import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.regions.SwitchRegion; import jadx.core.dex.regions.SwitchRegion;
import jadx.core.dex.regions.conditions.Compare;
import jadx.core.dex.regions.conditions.IfCondition; import jadx.core.dex.regions.conditions.IfCondition;
import jadx.core.dex.regions.conditions.IfRegion; import jadx.core.dex.regions.conditions.IfRegion;
import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.dex.visitors.AbstractVisitor;
@@ -42,6 +46,7 @@ import jadx.core.utils.BlockUtils;
import jadx.core.utils.InsnRemover; import jadx.core.utils.InsnRemover;
import jadx.core.utils.InsnUtils; import jadx.core.utils.InsnUtils;
import jadx.core.utils.RegionUtils; import jadx.core.utils.RegionUtils;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxException; import jadx.core.utils.exceptions.JadxException;
@JadxVisitor( @JadxVisitor(
@@ -79,19 +84,21 @@ public class SwitchOverStringVisitor extends AbstractVisitor implements IRegionI
return false; return false;
} }
int casesCount = switchRegion.getCases().size(); int casesCount = switchRegion.getCases().size();
boolean defaultCaseAdded = switchRegion.getCases().stream().anyMatch(SwitchRegion.CaseInfo::isDefaultCase);
int casesWithString = defaultCaseAdded ? casesCount - 1 : casesCount;
SSAVar strVar = strArg.getSVar(); SSAVar strVar = strArg.getSVar();
if (strVar.getUseCount() - 1 < casesCount) { if (strVar.getUseCount() - 1 < casesWithString) {
// one 'hashCode' invoke and at least one 'equals' per case // one 'hashCode' invoke and at least one 'equals' per case
return false; return false;
} }
// quick checks done, start collecting data to create a new switch region // quick checks done, start collecting data to create a new switch region
Map<InsnNode, String> strEqInsns = collectEqualsInsns(mth, strVar); Map<InsnNode, String> strEqInsns = collectEqualsInsns(mth, strVar);
if (strEqInsns.size() < casesCount) { if (strEqInsns.size() < casesWithString) {
return false; return false;
} }
SwitchData switchData = new SwitchData(mth, switchRegion); SwitchData switchData = new SwitchData(mth, switchRegion);
switchData.setStrEqInsns(strEqInsns); switchData.setStrEqInsns(strEqInsns);
switchData.setCases(new ArrayList<>(strEqInsns.size())); switchData.setCases(new ArrayList<>(casesCount));
for (SwitchRegion.CaseInfo swCaseInfo : switchRegion.getCases()) { for (SwitchRegion.CaseInfo swCaseInfo : switchRegion.getCases()) {
if (!processCase(switchData, swCaseInfo)) { if (!processCase(switchData, swCaseInfo)) {
mth.addWarnComment("Failed to restore switch over string. Please report as a decompilation issue"); mth.addWarnComment("Failed to restore switch over string. Please report as a decompilation issue");
@@ -118,7 +125,7 @@ public class SwitchOverStringVisitor extends AbstractVisitor implements IRegionI
// use string arg directly in switch // use string arg directly in switch
swInsn.replaceArg(swInsn.getArg(0), strArg.duplicate()); swInsn.replaceArg(swInsn.getArg(0), strArg.duplicate());
return true; return true;
} catch (Throwable e) { } catch (StackOverflowError | Exception e) {
mth.addWarnComment("Failed to restore switch over string. Please report as a decompilation issue", e); mth.addWarnComment("Failed to restore switch over string. Please report as a decompilation issue", e);
return false; return false;
} }
@@ -151,38 +158,33 @@ public class SwitchOverStringVisitor extends AbstractVisitor implements IRegionI
} }
} }
InsnRemover.removeAllMarked(mth); InsnRemover.removeAllMarked(mth);
} catch (Throwable e) { } catch (StackOverflowError | Exception e) {
mth.addWarnComment("Failed to clean up code after switch over string restore", e); mth.addWarnComment("Failed to clean up code after switch over string restore", e);
} }
} }
private boolean mergeWithCode(SwitchData switchData) { private boolean mergeWithCode(SwitchData switchData) {
// check for second switch
IContainer nextContainer = RegionUtils.getNextContainer(switchData.getMth(), switchData.getSwitchRegion());
if (!(nextContainer instanceof SwitchRegion)) {
return false;
}
SwitchRegion codeSwitch = (SwitchRegion) nextContainer;
InsnNode swInsn = BlockUtils.getLastInsnWithType(codeSwitch.getHeader(), InsnType.SWITCH);
if (swInsn == null || !swInsn.getArg(0).isRegister()) {
return false;
}
RegisterArg numArg = (RegisterArg) swInsn.getArg(0);
List<CaseData> cases = switchData.getCases(); List<CaseData> cases = switchData.getCases();
// search index assign in cases code // search index assign in cases code
RegisterArg numArg = null;
int extracted = 0; int extracted = 0;
for (CaseData caseData : cases) { for (CaseData caseData : cases) {
IContainer container = caseData.getCode(); InsnNode numInsn = searchConstInsn(switchData, caseData, swInsn);
List<InsnNode> insns = RegionUtils.collectInsns(switchData.getMth(), container); Integer num = extractConstNumber(switchData, numInsn, numArg);
insns.removeIf(i -> i.getType() == InsnType.BREAK); if (num != null) {
if (insns.size() != 1) { caseData.setCodeNum(num);
continue; extracted++;
}
InsnNode numInsn = insns.get(0);
if (numInsn.getArgsCount() == 1) {
Object constVal = InsnUtils.getConstValueByArg(switchData.getMth().root(), numInsn.getArg(0));
if (constVal instanceof LiteralArg) {
if (numArg == null) {
numArg = numInsn.getResult();
} else {
if (!numArg.sameCodeVar(numInsn.getResult())) {
return false;
}
}
int num = (int) ((LiteralArg) constVal).getLiteral();
caseData.setCodeNum(num);
extracted++;
}
} }
} }
if (extracted == 0) { if (extracted == 0) {
@@ -195,16 +197,7 @@ public class SwitchOverStringVisitor extends AbstractVisitor implements IRegionI
// TODO: additional checks for found index numbers // TODO: additional checks for found index numbers
cases.sort(Comparator.comparingInt(CaseData::getCodeNum)); cases.sort(Comparator.comparingInt(CaseData::getCodeNum));
// extract complete, second switch on 'numArg' should be the next region // extract complete
IContainer nextContainer = RegionUtils.getNextContainer(switchData.getMth(), switchData.getSwitchRegion());
if (!(nextContainer instanceof SwitchRegion)) {
return false;
}
SwitchRegion codeSwitch = (SwitchRegion) nextContainer;
InsnNode swInsn = BlockUtils.getLastInsnWithType(codeSwitch.getHeader(), InsnType.SWITCH);
if (swInsn == null || !swInsn.getArg(0).isSameCodeVar(numArg)) {
return false;
}
Map<Integer, CaseData> casesMap = new HashMap<>(cases.size()); Map<Integer, CaseData> casesMap = new HashMap<>(cases.size());
for (CaseData caseData : cases) { for (CaseData caseData : cases) {
CaseData prev = casesMap.put(caseData.getCodeNum(), caseData); CaseData prev = casesMap.put(caseData.getCodeNum(), caseData);
@@ -215,42 +208,39 @@ public class SwitchOverStringVisitor extends AbstractVisitor implements IRegionI
block -> switchData.getToRemove().add(block)); block -> switchData.getToRemove().add(block));
} }
final var newCases = new ArrayList<SwitchRegion.CaseInfo>(); List<SwitchRegion.CaseInfo> newCases = new ArrayList<>();
for (SwitchRegion.CaseInfo caseInfo : codeSwitch.getCases()) { for (SwitchRegion.CaseInfo caseInfo : codeSwitch.getCases()) {
SwitchRegion.CaseInfo newCase = null; SwitchRegion.CaseInfo newCase = null;
for (Object key : caseInfo.getKeys()) { for (Object key : caseInfo.getKeys()) {
final Integer intKey = unwrapIntKey(key); Integer intKey = unwrapIntKey(key);
if (intKey != null) { if (intKey != null) {
final var caseData = casesMap.remove(intKey); CaseData caseData = casesMap.remove(intKey);
if (caseData == null) { if (caseData == null) {
return false; return false;
} }
if (newCase == null) { if (newCase == null) {
final List<Object> keys = new ArrayList<>(caseData.getStrValues()); List<Object> keys = new ArrayList<>(caseData.getStrValues());
newCase = new SwitchRegion.CaseInfo(keys, caseInfo.getContainer()); newCase = new SwitchRegion.CaseInfo(keys, caseInfo.getContainer());
} else { } else {
// merge cases // merge cases
newCase.getKeys().addAll(caseData.getStrValues()); newCase.getKeys().addAll(caseData.getStrValues());
} }
} else if (key == SwitchRegion.DEFAULT_CASE_KEY) { } else if (key == SwitchRegion.DEFAULT_CASE_KEY) {
final var iterator = casesMap.entrySet().iterator(); var iterator = casesMap.entrySet().iterator();
while (iterator.hasNext()) { while (iterator.hasNext()) {
final var caseData = iterator.next().getValue(); CaseData caseData = iterator.next().getValue();
if (newCase == null) { if (newCase == null) {
final List<Object> keys = new ArrayList<>(caseData.getStrValues()); List<Object> keys = new ArrayList<>(caseData.getStrValues());
newCase = new SwitchRegion.CaseInfo(keys, caseInfo.getContainer()); newCase = new SwitchRegion.CaseInfo(keys, caseInfo.getContainer());
} else { } else {
// merge cases // merge cases
newCase.getKeys().addAll(caseData.getStrValues()); newCase.getKeys().addAll(caseData.getStrValues());
} }
iterator.remove(); iterator.remove();
} }
if (newCase == null) { if (newCase == null) {
newCase = new SwitchRegion.CaseInfo(new ArrayList<>(), caseInfo.getContainer()); newCase = new SwitchRegion.CaseInfo(new ArrayList<>(), caseInfo.getContainer());
} }
newCase.getKeys().add(SwitchRegion.DEFAULT_CASE_KEY); newCase.getKeys().add(SwitchRegion.DEFAULT_CASE_KEY);
} else { } else {
return false; return false;
@@ -258,25 +248,61 @@ public class SwitchOverStringVisitor extends AbstractVisitor implements IRegionI
} }
newCases.add(newCase); newCases.add(newCase);
} }
switchData.setCodeSwitch(codeSwitch); switchData.setCodeSwitch(codeSwitch);
switchData.setNumArg(numArg); switchData.setNumArg(numArg);
switchData.setNewCases(newCases); switchData.setNewCases(newCases);
return true; return true;
} }
private @Nullable Integer extractConstNumber(SwitchData switchData, @Nullable InsnNode numInsn, RegisterArg numArg) {
if (numInsn == null || numInsn.getArgsCount() != 1) {
return null;
}
Object constVal = InsnUtils.getConstValueByArg(switchData.getMth().root(), numInsn.getArg(0));
if (constVal instanceof LiteralArg) {
if (numArg.sameCodeVar(numInsn.getResult())) {
return (int) ((LiteralArg) constVal).getLiteral();
}
}
return null;
}
private static @Nullable InsnNode searchConstInsn(SwitchData switchData, CaseData caseData, InsnNode swInsn) {
IContainer container = caseData.getCode();
if (container != null) {
List<InsnNode> insns = RegionUtils.collectInsns(switchData.getMth(), container);
insns.removeIf(i -> i.getType() == InsnType.BREAK);
if (insns.size() == 1) {
return insns.get(0);
}
} else if (caseData.getBlockRef() != null) {
// variable used unchanged on path from block ref
BlockNode blockRef = caseData.getBlockRef();
InsnArg swArg = swInsn.getArg(0);
if (swArg.isRegister()) {
InsnNode assignInsn = ((RegisterArg) swArg).getSVar().getAssignInsn();
if (assignInsn != null && assignInsn.getType() == InsnType.PHI) {
RegisterArg arg = ((PhiInsn) assignInsn).getArgByBlock(blockRef);
if (arg != null) {
return arg.getAssignInsn();
}
}
}
}
return null;
}
private Integer unwrapIntKey(Object key) { private Integer unwrapIntKey(Object key) {
if (key instanceof Integer) { if (key instanceof Integer) {
return (Integer) key; return (Integer) key;
} else if (key instanceof FieldNode) { }
final var encodedValue = ((FieldNode) key).get(JadxAttrType.CONSTANT_VALUE); if (key instanceof FieldNode) {
EncodedValue encodedValue = ((FieldNode) key).get(JadxAttrType.CONSTANT_VALUE);
if (encodedValue != null && encodedValue.getType() == EncodedType.ENCODED_INT) { if (encodedValue != null && encodedValue.getType() == EncodedType.ENCODED_INT) {
return (Integer) encodedValue.getValue(); return (Integer) encodedValue.getValue();
} else {
return null;
} }
return null;
} }
return null; return null;
} }
@@ -299,6 +325,11 @@ public class SwitchOverStringVisitor extends AbstractVisitor implements IRegionI
} }
private boolean processCase(SwitchData switchData, SwitchRegion.CaseInfo caseInfo) { private boolean processCase(SwitchData switchData, SwitchRegion.CaseInfo caseInfo) {
if (caseInfo.isDefaultCase()) {
CaseData caseData = new CaseData();
caseData.setCode(caseInfo.getContainer());
return true;
}
AtomicBoolean fail = new AtomicBoolean(false); AtomicBoolean fail = new AtomicBoolean(false);
RegionUtils.visitRegions(switchData.getMth(), caseInfo.getContainer(), region -> { RegionUtils.visitRegions(switchData.getMth(), caseInfo.getContainer(), region -> {
if (fail.get()) { if (fail.get()) {
@@ -324,30 +355,39 @@ public class SwitchOverStringVisitor extends AbstractVisitor implements IRegionI
condition = condition.getArgs().get(0); condition = condition.getArgs().get(0);
neg = true; neg = true;
} }
Compare compare = condition.getCompare();
if (compare == null) {
return null;
}
IfNode ifInsn = compare.getInsn();
InsnArg firstArg = ifInsn.getArg(0);
String str = null; String str = null;
if (condition.isCompare()) { if (firstArg.isInsnWrap()) {
IfNode ifInsn = condition.getCompare().getInsn(); str = switchData.getStrEqInsns().get(((InsnWrapArg) firstArg).getWrapInsn());
InsnArg firstArg = ifInsn.getArg(0);
if (firstArg.isInsnWrap()) {
str = switchData.getStrEqInsns().get(((InsnWrapArg) firstArg).getWrapInsn());
}
if (ifInsn.getOp() == IfOp.NE && ifInsn.getArg(1).isTrue()) {
neg = true;
}
if (ifInsn.getOp() == IfOp.EQ && ifInsn.getArg(1).isFalse()) {
neg = true;
}
if (str != null) {
switchData.getToRemove().add(ifInsn);
switchData.getToRemove().addAll(ifRegion.getConditionBlocks());
}
} }
if (str == null) { if (str == null) {
return null; return null;
} }
if (ifInsn.getOp() == IfOp.NE && ifInsn.getArg(1).isTrue()) {
neg = true;
}
if (ifInsn.getOp() == IfOp.EQ && ifInsn.getArg(1).isFalse()) {
neg = true;
}
switchData.getToRemove().add(ifInsn);
switchData.getToRemove().addAll(ifRegion.getConditionBlocks());
CaseData caseData = new CaseData(); CaseData caseData = new CaseData();
caseData.getStrValues().add(str); caseData.getStrValues().add(str);
caseData.setCode(neg ? ifRegion.getElseRegion() : ifRegion.getThenRegion());
IContainer codeContainer = neg ? ifRegion.getElseRegion() : ifRegion.getThenRegion();
if (codeContainer == null) {
// no code
// use last condition block for later data tracing
caseData.setBlockRef(Utils.last(ifRegion.getConditionBlocks()));
} else {
caseData.setCode(codeContainer);
}
return caseData; return caseData;
} }
@@ -447,21 +487,30 @@ public class SwitchOverStringVisitor extends AbstractVisitor implements IRegionI
private static final class CaseData { private static final class CaseData {
private final List<String> strValues = new ArrayList<>(); private final List<String> strValues = new ArrayList<>();
private IContainer code = null; private @Nullable IContainer code = null;
private @Nullable BlockNode blockRef = null;
private int codeNum = -1; private int codeNum = -1;
public List<String> getStrValues() { public List<String> getStrValues() {
return strValues; return strValues;
} }
public IContainer getCode() { public @Nullable IContainer getCode() {
return code; return code;
} }
public void setCode(IContainer code) { public void setCode(@Nullable IContainer code) {
this.code = code; this.code = code;
} }
public @Nullable BlockNode getBlockRef() {
return blockRef;
}
public void setBlockRef(@Nullable BlockNode blockRef) {
this.blockRef = blockRef;
}
public int getCodeNum() { public int getCodeNum() {
return codeNum; return codeNum;
} }
@@ -122,6 +122,7 @@ public class TernaryMod extends AbstractRegionVisitor implements IRegionIterativ
int branchLine = Math.max(thenInsn.getSourceLine(), elseInsn.getSourceLine()); int branchLine = Math.max(thenInsn.getSourceLine(), elseInsn.getSourceLine());
ternInsn.setSourceLine(Math.max(ifRegion.getSourceLine(), branchLine)); ternInsn.setSourceLine(Math.max(ifRegion.getSourceLine(), branchLine));
thenInsn.setResult(null); // unset without unbind, SSA var still in use
InsnRemover.unbindResult(mth, elseInsn); InsnRemover.unbindResult(mth, elseInsn);
// remove 'if' instruction // remove 'if' instruction
@@ -18,12 +18,12 @@ public abstract class TracedRegionVisitor implements IRegionVisitor {
} }
@Override @Override
public void processBlock(MethodNode mth, IBlock container) { public void processBlock(MethodNode mth, IBlock block) {
IRegion curRegion = regionStack.peek(); IRegion curRegion = regionStack.peek();
processBlockTraced(mth, container, curRegion); processBlockTraced(mth, block, curRegion);
} }
public abstract void processBlockTraced(MethodNode mth, IBlock container, IRegion currentRegion); public abstract void processBlockTraced(MethodNode mth, IBlock block, IRegion parentRegion);
@Override @Override
public void leaveRegion(MethodNode mth, IRegion region) { public void leaveRegion(MethodNode mth, IRegion region) {
@@ -132,8 +132,18 @@ public class ExcHandlersRegionMaker {
if (dom.contains(AFlag.REMOVE)) { if (dom.contains(AFlag.REMOVE)) {
return; return;
} }
BitSet domFrontier = dom.getDomFrontier(); List<BlockNode> handlerExits = new ArrayList<>();
List<BlockNode> handlerExits = BlockUtils.bitSetToBlocks(mth, domFrontier);
BlockNode handlerOutBlock = BlockUtils.getTryAndHandlerCrossBlock(mth, handler);
if (handlerOutBlock != null) {
// ensure frontier's other predecessors comes from try end
handlerExits.add(handlerOutBlock);
} else {
// fallback to simple frontier
BitSet domFrontier = dom.getDomFrontier();
handlerExits.addAll(BlockUtils.bitSetToBlocks(mth, domFrontier));
}
boolean inLoop = mth.getLoopForBlock(start) != null; boolean inLoop = mth.getLoopForBlock(start) != null;
for (BlockNode exit : handlerExits) { for (BlockNode exit : handlerExits) {
if ((!inLoop || BlockUtils.isPathExists(start, exit)) if ((!inLoop || BlockUtils.isPathExists(start, exit))
@@ -28,6 +28,7 @@ import jadx.core.dex.regions.conditions.IfCondition;
import jadx.core.dex.regions.conditions.IfInfo; import jadx.core.dex.regions.conditions.IfInfo;
import jadx.core.dex.regions.conditions.IfRegion; import jadx.core.dex.regions.conditions.IfRegion;
import jadx.core.dex.regions.loops.LoopRegion; import jadx.core.dex.regions.loops.LoopRegion;
import jadx.core.dex.trycatch.ExcHandlerAttr;
import jadx.core.utils.BlockUtils; import jadx.core.utils.BlockUtils;
import jadx.core.utils.blocks.BlockSet; import jadx.core.utils.blocks.BlockSet;
import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.exceptions.JadxRuntimeException;
@@ -197,6 +198,21 @@ final class IfRegionMaker {
info = new IfInfo(info, elseBlock, null); info = new IfInfo(info, elseBlock, null);
info.setOutBlock(thenBlock); info.setOutBlock(thenBlock);
} }
// getPathCross may not find outBlock (e.g. one branch has return, outBlock definitely is
// null), so should check further
if (info.getOutBlock() == null) {
BlockNode scopeOutBlockThen = findScopeOutBlock(mth, info.getThenBlock());
BlockNode scopeOutBlockElse = findScopeOutBlock(mth, info.getElseBlock());
if (scopeOutBlockThen == null && scopeOutBlockElse != null) {
info.setOutBlock(scopeOutBlockElse);
} else if (scopeOutBlockThen != null && scopeOutBlockElse == null) {
info.setOutBlock(scopeOutBlockThen);
} else if (scopeOutBlockThen != null && scopeOutBlockThen == scopeOutBlockElse) {
info.setOutBlock(scopeOutBlockThen);
}
}
if (BlockUtils.isBackEdge(block, info.getOutBlock())) { if (BlockUtils.isBackEdge(block, info.getOutBlock())) {
info.setOutBlock(null); info.setOutBlock(null);
} }
@@ -243,6 +259,33 @@ final class IfRegionMaker {
return true; return true;
} }
/**
* if startBlock is in a (try) scope, find the scope end as outBlock
*/
private @Nullable static BlockNode findScopeOutBlock(MethodNode mth, BlockNode startBlock) {
if (startBlock == null) {
return null;
}
List<BlockNode> domFrontiers = BlockUtils.bitSetToBlocks(mth, startBlock.getDomFrontier());
BlockNode scopeOutBlock = null;
// find handler from domFrontier(could be scope end), if domFrontier is handler
// and its topSplitter dominates branch block, then branch should end
for (BlockNode domFrontier : domFrontiers) {
ExcHandlerAttr handler = domFrontier.get(AType.EXC_HANDLER);
if (handler == null) {
continue;
}
BlockNode topSplitter = handler.getTryBlock().getTopSplitter();
if (startBlock.isDominator(topSplitter)) {
scopeOutBlock = BlockUtils.getTryAndHandlerCrossBlock(mth, handler.getHandler());
break;
}
}
return scopeOutBlock;
}
static IfInfo mergeNestedIfNodes(IfInfo currentIf) { static IfInfo mergeNestedIfNodes(IfInfo currentIf) {
BlockNode curThen = currentIf.getThenBlock(); BlockNode curThen = currentIf.getThenBlock();
BlockNode curElse = currentIf.getElseBlock(); BlockNode curElse = currentIf.getElseBlock();
@@ -24,6 +24,7 @@ import jadx.core.dex.trycatch.ExceptionHandler;
import jadx.core.utils.BlockUtils; import jadx.core.utils.BlockUtils;
import jadx.core.utils.ListUtils; import jadx.core.utils.ListUtils;
import jadx.core.utils.RegionUtils; import jadx.core.utils.RegionUtils;
import jadx.core.utils.blocks.BlockSet;
import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.exceptions.JadxRuntimeException;
import static jadx.core.utils.BlockUtils.getNextBlock; import static jadx.core.utils.BlockUtils.getNextBlock;
@@ -349,7 +350,12 @@ final class LoopRegionMaker {
if (!confirm) { if (!confirm) {
BlockNode insertBlock = null; BlockNode insertBlock = null;
while (exit != null) { BlockSet visited = new BlockSet(mth);
while (true) {
if (exit == null || visited.contains(exit)) {
break;
}
visited.add(exit);
if (insertBlock != null && isPathExists(loopExit, exit)) { if (insertBlock != null && isPathExists(loopExit, exit)) {
// found cross // found cross
if (canInsertBreak(insertBlock)) { if (canInsertBreak(insertBlock)) {
@@ -6,6 +6,7 @@ import java.util.Deque;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -83,7 +84,7 @@ final class RegionStack {
* *
* @param exit boundary node, null will be ignored * @param exit boundary node, null will be ignored
*/ */
public void addExit(BlockNode exit) { public void addExit(@Nullable BlockNode exit) {
if (exit != null) { if (exit != null) {
curState.exits.add(exit); curState.exits.add(exit);
} }
@@ -95,7 +96,7 @@ final class RegionStack {
} }
} }
public void removeExit(BlockNode exit) { public void removeExit(@Nullable BlockNode exit) {
if (exit != null) { if (exit != null) {
curState.exits.remove(exit); curState.exits.remove(exit);
} }
@@ -19,17 +19,24 @@ import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.IContainer;
import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.IRegion;
import jadx.core.dex.nodes.InsnContainer;
import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.regions.Region; import jadx.core.dex.regions.Region;
import jadx.core.dex.regions.SwitchRegion; import jadx.core.dex.regions.SwitchRegion;
import jadx.core.dex.visitors.regions.AbstractRegionVisitor;
import jadx.core.dex.visitors.regions.DepthRegionTraversal;
import jadx.core.dex.visitors.regions.SwitchBreakVisitor;
import jadx.core.utils.BlockUtils; import jadx.core.utils.BlockUtils;
import jadx.core.utils.ListUtils;
import jadx.core.utils.RegionUtils; import jadx.core.utils.RegionUtils;
import jadx.core.utils.Utils; import jadx.core.utils.Utils;
import jadx.core.utils.blocks.BlockSet;
import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.exceptions.JadxRuntimeException;
final class SwitchRegionMaker { public final class SwitchRegionMaker {
private final MethodNode mth; private final MethodNode mth;
private final RegionMaker regionMaker; private final RegionMaker regionMaker;
@@ -61,21 +68,32 @@ final class SwitchRegionMaker {
BlockNode out = calcSwitchOut(block, insn, stack); BlockNode out = calcSwitchOut(block, insn, stack);
stack.addExit(out); stack.addExit(out);
processFallThroughCases(sw, out, stack, blocksMap); addCases(sw, out, stack, blocksMap);
removeEmptyCases(insn, sw, defCase); removeEmptyCases(insn, sw, defCase);
stack.pop(); stack.pop();
return out; return out;
} }
private void processFallThroughCases(SwitchRegion sw, @Nullable BlockNode out, /**
* Insert 'break' for all cases in switch region
* Executed in {@link jadx.core.dex.visitors.regions.PostProcessRegions} after try/catch wrap to
* handle all blocks
*/
public static void insertBreaks(MethodNode mth, SwitchRegion sw) {
for (SwitchRegion.CaseInfo caseInfo : sw.getCases()) {
insertBreaksForCase(mth, sw, caseInfo.getContainer());
}
}
private void addCases(SwitchRegion sw, @Nullable BlockNode out,
RegionStack stack, Map<BlockNode, List<Object>> blocksMap) { RegionStack stack, Map<BlockNode, List<Object>> blocksMap) {
Map<BlockNode, BlockNode> fallThroughCases = new LinkedHashMap<>(); Map<BlockNode, BlockNode> fallThroughCases = new LinkedHashMap<>();
if (out != null) { if (out != null) {
// detect fallthrough cases // detect fallthrough cases
BitSet caseBlocks = BlockUtils.blocksToBitSet(mth, blocksMap.keySet()); BitSet caseBlocks = BlockUtils.blocksToBitSet(mth, blocksMap.keySet());
caseBlocks.clear(out.getId()); caseBlocks.clear(out.getPos());
for (BlockNode successor : sw.getHeader().getCleanSuccessors()) { for (BlockNode successor : sw.getHeader().getSuccessors()) {
BitSet df = successor.getDomFrontier(); BitSet df = successor.getDomFrontier();
if (df.intersects(caseBlocks)) { if (df.intersects(caseBlocks)) {
BlockNode fallThroughBlock = getOneIntersectionBlock(out, caseBlocks, df); BlockNode fallThroughBlock = getOneIntersectionBlock(out, caseBlocks, df);
@@ -93,31 +111,30 @@ final class SwitchRegionMaker {
} }
} }
} }
for (Map.Entry<BlockNode, List<Object>> entry : blocksMap.entrySet()) { for (Map.Entry<BlockNode, List<Object>> entry : blocksMap.entrySet()) {
List<Object> keysList = entry.getValue(); List<Object> keysList = entry.getValue();
BlockNode caseBlock = entry.getKey(); BlockNode caseBlock = entry.getKey();
Region caseRegion;
if (stack.containsExit(caseBlock)) { if (stack.containsExit(caseBlock)) {
sw.addCase(keysList, new Region(stack.peekRegion())); caseRegion = new Region(stack.peekRegion());
} else { } else {
BlockNode next = fallThroughCases.get(caseBlock); BlockNode next = fallThroughCases.get(caseBlock);
stack.addExit(next); stack.addExit(next);
Region caseRegion = regionMaker.makeRegion(caseBlock); caseRegion = regionMaker.makeRegion(caseBlock);
stack.removeExit(next); stack.removeExit(next);
if (next != null) { if (next != null) {
next.add(AFlag.FALL_THROUGH); next.add(AFlag.FALL_THROUGH);
caseRegion.add(AFlag.FALL_THROUGH); caseRegion.add(AFlag.FALL_THROUGH);
} }
sw.addCase(keysList, caseRegion);
// 'break' instruction will be inserted in RegionMakerVisitor.PostRegionVisitor
} }
sw.addCase(keysList, caseRegion);
} }
} }
@Nullable @Nullable
private BlockNode getOneIntersectionBlock(BlockNode out, BitSet caseBlocks, BitSet fallThroughSet) { private BlockNode getOneIntersectionBlock(BlockNode out, BitSet caseBlocks, BitSet fallThroughSet) {
BitSet caseExits = BlockUtils.copyBlocksBitSet(mth, fallThroughSet); BitSet caseExits = BlockUtils.copyBlocksBitSet(mth, fallThroughSet);
caseExits.clear(out.getId()); caseExits.clear(out.getPos());
caseExits.and(caseBlocks); caseExits.and(caseBlocks);
return BlockUtils.bitSetToOneBlock(mth, caseExits); return BlockUtils.bitSetToOneBlock(mth, caseExits);
} }
@@ -341,4 +358,49 @@ final class SwitchRegionMaker {
} }
return inserted; return inserted;
} }
/**
* Add break to every exit edge from 'case' region.
* 'Break' optimizations (code duplication, unreachable, etc.) will be done at
* {@link SwitchBreakVisitor}
*/
private static void insertBreaksForCase(MethodNode mth, SwitchRegion switchRegion, IContainer caseContainer) {
BlockSet caseBlocks = new BlockSet(mth);
RegionUtils.visitBlockNodes(mth, caseContainer, caseBlocks::add);
DepthRegionTraversal.traverse(mth, caseContainer, new AbstractRegionVisitor() {
@Override
public void leaveRegion(MethodNode mth, IRegion region) {
boolean insertBreak = false;
if (region == caseContainer) {
// top region
insertBreak = true;
} else {
IContainer lastContainer = ListUtils.last(region.getSubBlocks());
if (lastContainer instanceof BlockNode) {
BlockNode lastBlock = (BlockNode) lastContainer;
for (BlockNode successor : lastBlock.getSuccessors()) {
if (!caseBlocks.contains(successor)) {
insertBreak = true;
break;
}
}
}
}
if (insertBreak && canAppendBreak(region)) {
region.getSubBlocks().add(buildBreakContainer(switchRegion));
}
}
});
}
public static boolean canAppendBreak(IRegion region) {
return !region.contains(AFlag.FALL_THROUGH) && !RegionUtils.hasExitBlock(region);
}
public static InsnContainer buildBreakContainer(SwitchRegion switchRegion) {
InsnNode breakInsn = new InsnNode(InsnType.BREAK, 0);
breakInsn.add(AFlag.SYNTHETIC);
breakInsn.addAttr(new RegionRefAttr(switchRegion));
return new InsnContainer(breakInsn);
}
} }
@@ -23,6 +23,7 @@ import jadx.core.dex.instructions.ArithNode;
import jadx.core.dex.instructions.ArithOp; import jadx.core.dex.instructions.ArithOp;
import jadx.core.dex.instructions.IndexInsnNode; import jadx.core.dex.instructions.IndexInsnNode;
import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.InsnType;
import jadx.core.dex.instructions.InvokeNode;
import jadx.core.dex.instructions.PhiInsn; import jadx.core.dex.instructions.PhiInsn;
import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnArg;
@@ -32,6 +33,7 @@ import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.instructions.args.SSAVar;
import jadx.core.dex.instructions.mods.TernaryInsn; import jadx.core.dex.instructions.mods.TernaryInsn;
import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.IMethodDetails;
import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode; import jadx.core.dex.nodes.RootNode;
@@ -70,6 +72,7 @@ public final class FixTypesVisitor extends AbstractVisitor {
this.typeUpdate = root.getTypeUpdate(); this.typeUpdate = root.getTypeUpdate();
this.typeInference.init(root); this.typeInference.init(root);
this.resolvers = Arrays.asList( this.resolvers = Arrays.asList(
this::applyFieldType,
this::tryRestoreTypeVarCasts, this::tryRestoreTypeVarCasts,
this::tryInsertCasts, this::tryInsertCasts,
this::tryDeduceTypes, this::tryDeduceTypes,
@@ -292,6 +295,117 @@ public final class FixTypesVisitor extends AbstractVisitor {
return false; return false;
} }
/**
* Use type for var assigned from field (IGET or SGET).
* Insert additional casts at var use places.
*/
private Boolean applyFieldType(MethodNode mth) {
try {
boolean changed = false;
// will add new SSA vars, can't use for-each loop
List<SSAVar> sVars = mth.getSVars();
for (int i = 0, varsCount = sVars.size(); i < varsCount; i++) {
SSAVar ssaVar = sVars.get(i);
if (tryFieldTypeWithNewCasts(mth, ssaVar, true)) {
changed = true;
}
}
if (!changed) {
return false;
}
// rerun full type inference
InitCodeVariables.rerun(mth);
typeInference.initTypeBounds(mth);
typeInference.runTypePropagation(mth);
// check if changed var types are fixed
boolean success = true;
for (SSAVar ssaVar : mth.getSVars()) {
if (tryFieldTypeWithNewCasts(mth, ssaVar, false)) {
success = false;
}
}
if (!success) {
typeInference.initTypeBounds(mth);
typeInference.runTypePropagation(mth);
mth.addWarnComment("Type inference incomplete: some casts might be missing");
}
return success;
} catch (Exception e) {
mth.addWarnComment("Type inference fix 'apply assigned field type' failed", e);
return false;
}
}
private boolean tryFieldTypeWithNewCasts(MethodNode mth, SSAVar ssaVar, boolean insertCasts) {
ArgType type = ssaVar.getTypeInfo().getType();
if (type.isTypeKnown() || ssaVar.isTypeImmutable()) {
return false;
}
InsnNode assignInsn = ssaVar.getAssignInsn();
if (assignInsn == null) {
return false;
}
InsnType insnType = assignInsn.getType();
if (insnType != InsnType.IGET && insnType != InsnType.SGET) {
return false;
}
ArgType fieldType = assignInsn.getResult().getInitType();
// field type should be used
if (insertCasts) {
// try to find a use place and insert cast
boolean inserted = false;
for (RegisterArg useArg : ssaVar.getUseList()) {
if (insertExplicitUseCast(mth, ssaVar, useArg, fieldType)) {
inserted = true;
}
}
return inserted;
}
// force field type, will make type inference incomplete,
// but it is better that completely unknown type
ssaVar.setType(fieldType);
return true;
}
private boolean insertExplicitUseCast(MethodNode mth, SSAVar ssaVar, RegisterArg useArg, ArgType fieldType) {
InsnNode parentInsn = useArg.getParentInsn();
if (!InsnUtils.isInsnType(parentInsn, InsnType.INVOKE)) {
return false;
}
InvokeNode invoke = (InvokeNode) parentInsn;
InsnArg instanceArg = invoke.getInstanceArg();
if (instanceArg == null || !instanceArg.isSameVar(ssaVar)) {
return false;
}
IMethodDetails details = mth.root().getMethodUtils().getMethodDetails(invoke);
if (details == null) {
return false;
}
int newCasts = 0;
int k = -1;
for (InsnArg invArg : invoke.getArgList()) {
if (invArg == instanceArg) {
continue;
}
k++;
if (!invArg.isRegister()) {
continue;
}
ArgType detailsArg = details.getArgTypes().get(k);
ArgType invArgType = invArg.getType();
ArgType resolvedType = mth.root().getTypeUtils().replaceClassGenerics(fieldType, invArgType, detailsArg);
if (resolvedType != null && !resolvedType.equals(invArgType)) {
IndexInsnNode castInsn = insertUseCast(mth, (RegisterArg) invArg, resolvedType);
if (castInsn != null) {
castInsn.add(AFlag.EXPLICIT_CAST);
newCasts++;
}
}
}
return newCasts > 0;
}
/** /**
* Fix check casts to type var extend type: * Fix check casts to type var extend type:
* <br> * <br>
@@ -372,7 +486,9 @@ public final class FixTypesVisitor extends AbstractVisitor {
&& !boundType.equals(var.getTypeInfo().getType()) && !boundType.equals(var.getTypeInfo().getType())
&& boundType.containsTypeVariable() && boundType.containsTypeVariable()
&& !mth.root().getTypeUtils().containsUnknownTypeVar(mth, boundType)) { && !mth.root().getTypeUtils().containsUnknownTypeVar(mth, boundType)) {
if (insertAssignCast(mth, var, boundType)) { IndexInsnNode castInsn = insertAssignCast(mth, var, boundType);
if (castInsn != null) {
castInsn.add(AFlag.SOFT_CAST);
return 1; return 1;
} }
return insertUseCasts(mth, var); return insertUseCasts(mth, var);
@@ -388,58 +504,65 @@ public final class FixTypesVisitor extends AbstractVisitor {
} }
int useCasts = 0; int useCasts = 0;
for (RegisterArg useReg : new ArrayList<>(useList)) { for (RegisterArg useReg : new ArrayList<>(useList)) {
if (insertSoftUseCast(mth, useReg)) { IndexInsnNode castInsn = insertUseCast(mth, useReg, useReg.getInitType());
if (castInsn != null) {
castInsn.add(AFlag.SOFT_CAST);
useCasts++; useCasts++;
} }
} }
return useCasts; return useCasts;
} }
private boolean insertAssignCast(MethodNode mth, SSAVar var, ArgType castType) { private @Nullable IndexInsnNode insertAssignCast(MethodNode mth, SSAVar var, ArgType castType) {
RegisterArg assignArg = var.getAssign(); RegisterArg assignArg = var.getAssign();
InsnNode assignInsn = assignArg.getParentInsn(); InsnNode assignInsn = assignArg.getParentInsn();
if (assignInsn == null || assignInsn.getType() == InsnType.PHI) { if (assignInsn == null || assignInsn.getType() == InsnType.PHI) {
return false; return null;
} }
BlockNode assignBlock = BlockUtils.getBlockByInsn(mth, assignInsn); BlockNode assignBlock = BlockUtils.getBlockByInsn(mth, assignInsn);
if (assignBlock == null) { if (assignBlock == null) {
return false; return null;
} }
assignInsn.setResult(assignArg.duplicateWithNewSSAVar(mth)); assignInsn.setResult(assignArg.duplicateWithNewSSAVar(mth));
IndexInsnNode castInsn = makeSoftCastInsn(assignArg.duplicate(), assignInsn.getResult().duplicate(), castType); IndexInsnNode castInsn = makeCastInsn(assignArg.duplicate(), assignInsn.getResult().duplicate(), castType);
return BlockUtils.insertAfterInsn(assignBlock, assignInsn, castInsn); if (!BlockUtils.insertAfterInsn(assignBlock, assignInsn, castInsn)) {
return null;
}
return castInsn;
} }
private boolean insertSoftUseCast(MethodNode mth, RegisterArg useArg) { private @Nullable IndexInsnNode insertUseCast(MethodNode mth, RegisterArg useArg, ArgType castType) {
InsnNode useInsn = useArg.getParentInsn(); InsnNode useInsn = useArg.getParentInsn();
if (useInsn == null || useInsn.getType() == InsnType.PHI) { if (useInsn == null || useInsn.getType() == InsnType.PHI) {
return false; return null;
} }
if (useInsn.getType() == InsnType.IF && useInsn.getArg(1).isZeroConst()) { if (useInsn.getType() == InsnType.IF && useInsn.getArg(1).isZeroConst()) {
// cast isn't needed if compare with null // cast isn't needed if compare with null
return false; return null;
} }
BlockNode useBlock = BlockUtils.getBlockByInsn(mth, useInsn); BlockNode useBlock = BlockUtils.getBlockByInsn(mth, useInsn);
if (useBlock == null) { if (useBlock == null) {
return false; return null;
} }
IndexInsnNode castInsn = makeSoftCastInsn( IndexInsnNode castInsn = makeCastInsn(
useArg.duplicateWithNewSSAVar(mth), useArg.duplicateWithNewSSAVar(mth),
useArg.duplicate(), useArg.duplicate(),
useArg.getInitType()); castType);
useInsn.replaceArg(useArg, castInsn.getResult().duplicate()); useInsn.replaceArg(useArg, castInsn.getResult().duplicate());
boolean success = BlockUtils.insertBeforeInsn(useBlock, useInsn, castInsn); boolean inserted = BlockUtils.insertBeforeInsn(useBlock, useInsn, castInsn);
if (Consts.DEBUG_TYPE_INFERENCE && success) { if (!inserted) {
LOG.info("Insert soft cast for {} before {} in {}", useArg, useInsn, useBlock); return null;
} }
return success; if (Consts.DEBUG_TYPE_INFERENCE) {
LOG.info("Insert cast for {} before {} in {}", useArg, useInsn, useBlock);
}
return castInsn;
} }
private IndexInsnNode makeSoftCastInsn(RegisterArg result, RegisterArg arg, ArgType castType) { private IndexInsnNode makeCastInsn(RegisterArg result, RegisterArg arg, ArgType castType) {
IndexInsnNode castInsn = new IndexInsnNode(InsnType.CHECK_CAST, castType, 1); IndexInsnNode castInsn = new IndexInsnNode(InsnType.CHECK_CAST, castType, 1);
castInsn.setResult(result); castInsn.setResult(result);
castInsn.addArg(arg); castInsn.addArg(arg);
castInsn.add(AFlag.SOFT_CAST);
castInsn.add(AFlag.SYNTHETIC); castInsn.add(AFlag.SYNTHETIC);
return castInsn; return castInsn;
} }
@@ -73,6 +73,8 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
assignImmutableTypes(mth); assignImmutableTypes(mth);
initTypeBounds(mth); initTypeBounds(mth);
runTypePropagation(mth); runTypePropagation(mth);
} catch (StackOverflowError | BootstrapMethodError e) {
mth.addError("Type inference failed with stack overflow", new JadxOverflowException(e.getMessage()));
} catch (Exception e) { } catch (Exception e) {
mth.addError("Type inference failed", e); mth.addError("Type inference failed", e);
} }
@@ -12,6 +12,7 @@ import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import jadx.api.JadxArgs;
import jadx.core.Consts; import jadx.core.Consts;
import jadx.core.clsp.ClspClass; import jadx.core.clsp.ClspClass;
import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AFlag;
@@ -29,7 +30,6 @@ import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode; import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.nodes.utils.TypeUtils; import jadx.core.dex.nodes.utils.TypeUtils;
import jadx.core.utils.exceptions.JadxOverflowException;
import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.exceptions.JadxRuntimeException;
import static jadx.core.dex.visitors.typeinference.TypeUpdateResult.CHANGED; import static jadx.core.dex.visitors.typeinference.TypeUpdateResult.CHANGED;
@@ -42,9 +42,11 @@ public final class TypeUpdate {
private final RootNode root; private final RootNode root;
private final Map<InsnType, ITypeListener> listenerRegistry; private final Map<InsnType, ITypeListener> listenerRegistry;
private final TypeCompare comparator; private final TypeCompare comparator;
private final JadxArgs args;
public TypeUpdate(RootNode root) { public TypeUpdate(RootNode root) {
this.root = root; this.root = root;
this.args = root.getArgs();
this.listenerRegistry = initListenerRegistry(); this.listenerRegistry = initListenerRegistry();
this.comparator = new TypeCompare(root); this.comparator = new TypeCompare(root);
} }
@@ -70,30 +72,35 @@ public final class TypeUpdate {
return apply(mth, ssaVar, candidateType, TypeUpdateFlags.FLAGS_WIDER_IGNORE_SAME); return apply(mth, ssaVar, candidateType, TypeUpdateFlags.FLAGS_WIDER_IGNORE_SAME);
} }
public TypeUpdateResult applyWithWiderIgnoreUnknown(MethodNode mth, SSAVar ssaVar, ArgType candidateType) { public TypeUpdateResult applyDebugInfo(MethodNode mth, SSAVar ssaVar, ArgType candidateType) {
return apply(mth, ssaVar, candidateType, TypeUpdateFlags.FLAGS_WIDER_IGNORE_UNKNOWN); return apply(mth, ssaVar, candidateType, TypeUpdateFlags.FLAGS_APPLY_DEBUG);
} }
private TypeUpdateResult apply(MethodNode mth, SSAVar ssaVar, ArgType candidateType, TypeUpdateFlags flags) { private TypeUpdateResult apply(MethodNode mth, SSAVar ssaVar, ArgType candidateType, TypeUpdateFlags flags) {
if (candidateType == null || !candidateType.isTypeKnown()) { try {
if (candidateType == null || !candidateType.isTypeKnown()) {
return REJECT;
}
TypeUpdateInfo updateInfo = new TypeUpdateInfo(mth, flags, args);
TypeUpdateResult result = updateTypeChecked(updateInfo, ssaVar.getAssign(), candidateType);
if (result == REJECT) {
return result;
}
if (updateInfo.isEmpty()) {
return SAME;
}
if (Consts.DEBUG_TYPE_INFERENCE) {
LOG.debug("Applying type {} to {}:", candidateType, ssaVar.toShortString());
updateInfo.getSortedUpdates().forEach(upd -> LOG.debug(" {} -> {} in {}",
upd.getType(), upd.getArg().toShortString(), upd.getArg().getParentInsn()));
}
updateInfo.applyUpdates();
return CHANGED;
} catch (Exception e) {
mth.addWarnComment("Type update failed for variable: " + ssaVar + ", new type: " + candidateType, e);
return REJECT; return REJECT;
} }
TypeUpdateInfo updateInfo = new TypeUpdateInfo(mth, flags);
TypeUpdateResult result = updateTypeChecked(updateInfo, ssaVar.getAssign(), candidateType);
if (result == REJECT) {
return result;
}
if (updateInfo.isEmpty()) {
return SAME;
}
if (Consts.DEBUG_TYPE_INFERENCE) {
LOG.debug("Applying type {} to {}:", candidateType, ssaVar.toShortString());
updateInfo.getSortedUpdates().forEach(upd -> LOG.debug(" {} -> {} in {}",
upd.getType(), upd.getArg().toShortString(), upd.getArg().getParentInsn()));
}
updateInfo.applyUpdates();
return CHANGED;
} }
private TypeUpdateResult updateTypeChecked(TypeUpdateInfo updateInfo, InsnArg arg, ArgType candidateType) { private TypeUpdateResult updateTypeChecked(TypeUpdateInfo updateInfo, InsnArg arg, ArgType candidateType) {
@@ -116,8 +123,9 @@ public final class TypeUpdate {
private @Nullable TypeUpdateResult verifyType(TypeUpdateInfo updateInfo, InsnArg arg, ArgType candidateType) { private @Nullable TypeUpdateResult verifyType(TypeUpdateInfo updateInfo, InsnArg arg, ArgType candidateType) {
ArgType currentType = arg.getType(); ArgType currentType = arg.getType();
TypeUpdateFlags typeUpdateFlags = updateInfo.getFlags();
if (Objects.equals(currentType, candidateType)) { if (Objects.equals(currentType, candidateType)) {
if (!updateInfo.getFlags().isIgnoreSame()) { if (!typeUpdateFlags.isIgnoreSame()) {
return SAME; return SAME;
} }
} else { } else {
@@ -135,7 +143,7 @@ public final class TypeUpdate {
} }
return REJECT; return REJECT;
} }
if (compareResult == TypeCompareEnum.UNKNOWN && updateInfo.getFlags().isIgnoreUnknown()) { if (compareResult == TypeCompareEnum.UNKNOWN && typeUpdateFlags.isIgnoreUnknown()) {
return REJECT; return REJECT;
} }
if (arg.isTypeImmutable() && currentType != ArgType.UNKNOWN) { if (arg.isTypeImmutable() && currentType != ArgType.UNKNOWN) {
@@ -148,7 +156,13 @@ public final class TypeUpdate {
} }
return REJECT; return REJECT;
} }
if (compareResult.isWider() && !updateInfo.getFlags().isAllowWider()) { if (compareResult == TypeCompareEnum.WIDER_BY_GENERIC && typeUpdateFlags.isKeepGenerics()) {
if (Consts.DEBUG_TYPE_INFERENCE) {
LOG.debug("Type rejected for {}: candidate={} is removing generic from current={}", arg, candidateType, currentType);
}
return REJECT;
}
if (compareResult.isWider() && !typeUpdateFlags.isAllowWider()) {
if (Consts.DEBUG_TYPE_INFERENCE) { if (Consts.DEBUG_TYPE_INFERENCE) {
LOG.debug("Type rejected for {}: candidate={} is wider than current={}", arg, candidateType, currentType); LOG.debug("Type rejected for {}: candidate={} is wider than current={}", arg, candidateType, currentType);
} }
@@ -208,16 +222,11 @@ public final class TypeUpdate {
return CHANGED; return CHANGED;
} }
updateInfo.requestUpdate(arg, candidateType); updateInfo.requestUpdate(arg, candidateType);
try { TypeUpdateResult result = runListeners(updateInfo, arg, candidateType);
TypeUpdateResult result = runListeners(updateInfo, arg, candidateType); if (result == REJECT) {
if (result == REJECT) { updateInfo.rollbackUpdate(arg);
updateInfo.rollbackUpdate(arg);
}
return result;
} catch (StackOverflowError | BootstrapMethodError error) {
throw new JadxOverflowException("Type update terminated with stack overflow, arg: " + arg
+ ", method size: " + updateInfo.getMth().getInsnsCount());
} }
return result;
} }
private TypeUpdateResult runListeners(TypeUpdateInfo updateInfo, InsnArg arg, ArgType candidateType) { private TypeUpdateResult runListeners(TypeUpdateInfo updateInfo, InsnArg arg, ArgType candidateType) {
@@ -432,17 +441,15 @@ public final class TypeUpdate {
} }
private TypeUpdateResult moveListener(TypeUpdateInfo updateInfo, InsnNode insn, InsnArg arg, ArgType candidateType) { private TypeUpdateResult moveListener(TypeUpdateInfo updateInfo, InsnNode insn, InsnArg arg, ArgType candidateType) {
if (insn.getResult() == null) {
return CHANGED;
}
boolean assignChanged = isAssign(insn, arg); boolean assignChanged = isAssign(insn, arg);
InsnArg changeArg = assignChanged ? insn.getArg(0) : insn.getResult(); InsnArg changeArg = assignChanged ? insn.getArg(0) : insn.getResult();
boolean correctType; // allow result to be wider
if (changeArg.getType().isTypeKnown()) { TypeCompareEnum cmp = comparator.compareTypes(candidateType, changeArg.getType());
// allow result to be wider boolean correctType = cmp.isEqual() || (assignChanged ? cmp.isWider() : cmp.isNarrow());
TypeCompareEnum cmp = comparator.compareTypes(candidateType, changeArg.getType());
correctType = cmp.isEqual() || (assignChanged ? cmp.isWider() : cmp.isNarrow());
} else {
correctType = true;
}
TypeUpdateResult result = updateTypeChecked(updateInfo, changeArg, candidateType); TypeUpdateResult result = updateTypeChecked(updateInfo, changeArg, candidateType);
if (result == SAME && !correctType) { if (result == SAME && !correctType) {
@@ -1,55 +1,61 @@
package jadx.core.dex.visitors.typeinference; package jadx.core.dex.visitors.typeinference;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import static jadx.core.dex.visitors.typeinference.TypeUpdateFlags.FlagsEnum.ALLOW_WIDER;
import static jadx.core.dex.visitors.typeinference.TypeUpdateFlags.FlagsEnum.IGNORE_SAME;
import static jadx.core.dex.visitors.typeinference.TypeUpdateFlags.FlagsEnum.IGNORE_UNKNOWN;
import static jadx.core.dex.visitors.typeinference.TypeUpdateFlags.FlagsEnum.KEEP_GENERICS;
public class TypeUpdateFlags { public class TypeUpdateFlags {
private static final int ALLOW_WIDER = 1; enum FlagsEnum {
private static final int IGNORE_SAME = 2; ALLOW_WIDER,
private static final int IGNORE_UNKNOWN = 4; IGNORE_SAME,
IGNORE_UNKNOWN,
public static final TypeUpdateFlags FLAGS_EMPTY = build(0); KEEP_GENERICS,
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 int flags;
private static TypeUpdateFlags build(int flags) {
return new TypeUpdateFlags(flags);
} }
private TypeUpdateFlags(int flags) { static final TypeUpdateFlags FLAGS_EMPTY = build();
static final TypeUpdateFlags FLAGS_WIDER = build(ALLOW_WIDER);
static final TypeUpdateFlags FLAGS_WIDER_IGNORE_SAME = build(ALLOW_WIDER, IGNORE_SAME);
static final TypeUpdateFlags FLAGS_APPLY_DEBUG = build(ALLOW_WIDER, KEEP_GENERICS, IGNORE_UNKNOWN);
private final Set<FlagsEnum> flags;
private static TypeUpdateFlags build(FlagsEnum... flags) {
EnumSet<FlagsEnum> set;
if (flags.length == 0) {
set = EnumSet.noneOf(FlagsEnum.class);
} else {
set = EnumSet.copyOf(List.of(flags));
}
return new TypeUpdateFlags(set);
}
private TypeUpdateFlags(Set<FlagsEnum> flags) {
this.flags = flags; this.flags = flags;
} }
public boolean isAllowWider() { public boolean isAllowWider() {
return (flags & ALLOW_WIDER) != 0; return flags.contains(ALLOW_WIDER);
} }
public boolean isIgnoreSame() { public boolean isIgnoreSame() {
return (flags & IGNORE_SAME) != 0; return flags.contains(IGNORE_SAME);
} }
public boolean isIgnoreUnknown() { public boolean isIgnoreUnknown() {
return (flags & IGNORE_UNKNOWN) != 0; return flags.contains(IGNORE_UNKNOWN);
}
public boolean isKeepGenerics() {
return flags.contains(KEEP_GENERICS);
} }
@Override @Override
public String toString() { public String toString() {
StringBuilder sb = new StringBuilder(); return flags.toString();
if (isAllowWider()) {
sb.append("ALLOW_WIDER");
}
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();
} }
} }
@@ -5,9 +5,11 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import jadx.api.JadxArgs;
import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxOverflowException; import jadx.core.utils.exceptions.JadxOverflowException;
import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.exceptions.JadxRuntimeException;
@@ -18,10 +20,10 @@ public class TypeUpdateInfo {
private final int updatesLimitCount; private final int updatesLimitCount;
private int updateSeq = 0; private int updateSeq = 0;
public TypeUpdateInfo(MethodNode mth, TypeUpdateFlags flags) { public TypeUpdateInfo(MethodNode mth, TypeUpdateFlags flags, JadxArgs args) {
this.mth = mth; this.mth = mth;
this.flags = flags; this.flags = flags;
this.updatesLimitCount = mth.getInsnsCount() * 10; this.updatesLimitCount = mth.getInsnsCount() * args.getTypeUpdatesLimitCount();
} }
public void requestUpdate(InsnArg arg, ArgType changeType) { public void requestUpdate(InsnArg arg, ArgType changeType) {
@@ -32,7 +34,12 @@ public class TypeUpdateInfo {
+ ", insn: " + arg.getParentInsn()); + ", insn: " + arg.getParentInsn());
} }
if (updateSeq > updatesLimitCount) { if (updateSeq > updatesLimitCount) {
throw new JadxOverflowException("Type inference error: updates count limit reached"); throw new JadxOverflowException("Type inference error: updates count limit reached"
+ " with updateSeq = " + updateSeq + ". Try increasing type updates limit count.");
}
if (updateSeq % 100 == 0) {
// check for interruption sometimes (every update is too often)
Utils.checkThreadInterrupt();
} }
} }
@@ -129,7 +129,8 @@ public class UsageInfoVisitor extends AbstractVisitor {
try { try {
processInsn(root, mth, insnData, usageInfo); processInsn(root, mth, insnData, usageInfo);
} catch (Exception e) { } catch (Exception e) {
mth.addError("Dependency scan failed at insn: " + insnData, e); throw new JadxRuntimeException(
"Usage info collection failed with error: " + e.getMessage() + " at insn: " + insnData, e);
} }
}); });
} }
@@ -31,7 +31,8 @@ public class AndroidGradleGenerator implements IExportGradleGenerator {
private static final Logger LOG = LoggerFactory.getLogger(AndroidGradleGenerator.class); private static final Logger LOG = LoggerFactory.getLogger(AndroidGradleGenerator.class);
private static final Pattern ILLEGAL_GRADLE_CHARS = Pattern.compile("[/\\\\:>\"?*|]"); private static final Pattern ILLEGAL_GRADLE_CHARS = Pattern.compile("[/\\\\:>\"?*|]");
private static final ApplicationParams UNKNOWN_APP_PARAMS = new ApplicationParams("UNKNOWN", 0, 0, 0, "UNKNOWN", "UNKNOWN", "UNKNOWN"); private static final ApplicationParams UNKNOWN_APP_PARAMS =
new ApplicationParams("UNKNOWN", 0, 0, 0, 0, "UNKNOWN", "UNKNOWN", "UNKNOWN");
private final RootNode root; private final RootNode root;
private final File projectDir; private final File projectDir;
@@ -107,6 +108,7 @@ public class AndroidGradleGenerator implements IExportGradleGenerator {
if (exportApp) { if (exportApp) {
attrs.add(AppAttribute.APPLICATION_LABEL); attrs.add(AppAttribute.APPLICATION_LABEL);
attrs.add(AppAttribute.TARGET_SDK_VERSION); attrs.add(AppAttribute.TARGET_SDK_VERSION);
attrs.add(AppAttribute.COMPILE_SDK_VERSION);
attrs.add(AppAttribute.VERSION_NAME); attrs.add(AppAttribute.VERSION_NAME);
attrs.add(AppAttribute.VERSION_CODE); attrs.add(AppAttribute.VERSION_CODE);
} }
@@ -114,7 +116,7 @@ public class AndroidGradleGenerator implements IExportGradleGenerator {
IJadxSecurity security = root.getArgs().getSecurity(); IJadxSecurity security = root.getArgs().getSecurity();
AndroidManifestParser parser = new AndroidManifestParser(androidManifest, strings, attrs, security); AndroidManifestParser parser = new AndroidManifestParser(androidManifest, strings, attrs, security);
return parser.parse(); return parser.parse();
} catch (Throwable t) { } catch (Exception t) {
LOG.warn("Failed to parse AndroidManifest.xml", t); LOG.warn("Failed to parse AndroidManifest.xml", t);
return UNKNOWN_APP_PARAMS; return UNKNOWN_APP_PARAMS;
} }
@@ -160,6 +162,7 @@ public class AndroidGradleGenerator implements IExportGradleGenerator {
TemplateFile tmpl = TemplateFile.fromResources("/export/android/app.build.gradle.tmpl"); TemplateFile tmpl = TemplateFile.fromResources("/export/android/app.build.gradle.tmpl");
tmpl.add("applicationId", appPackage); tmpl.add("applicationId", appPackage);
tmpl.add("minSdkVersion", minSdkVersion); tmpl.add("minSdkVersion", minSdkVersion);
tmpl.add("compileSdkVersion", applicationParams.getCompileSdkVersion());
tmpl.add("targetSdkVersion", applicationParams.getTargetSdkVersion()); tmpl.add("targetSdkVersion", applicationParams.getTargetSdkVersion());
tmpl.add("versionCode", applicationParams.getVersionCode()); tmpl.add("versionCode", applicationParams.getVersionCode());
tmpl.add("versionName", applicationParams.getVersionName()); tmpl.add("versionName", applicationParams.getVersionName());
@@ -174,6 +177,7 @@ public class AndroidGradleGenerator implements IExportGradleGenerator {
TemplateFile tmpl = TemplateFile.fromResources("/export/android/lib.build.gradle.tmpl"); TemplateFile tmpl = TemplateFile.fromResources("/export/android/lib.build.gradle.tmpl");
tmpl.add("packageId", pkg); tmpl.add("packageId", pkg);
tmpl.add("minSdkVersion", minSdkVersion); tmpl.add("minSdkVersion", minSdkVersion);
tmpl.add("compileSdkVersion", applicationParams.getCompileSdkVersion());
tmpl.add("additionalOptions", genAdditionalAndroidPluginOptions(minSdkVersion)); tmpl.add("additionalOptions", genAdditionalAndroidPluginOptions(minSdkVersion));
tmpl.save(new File(baseDir, "build.gradle")); tmpl.save(new File(baseDir, "build.gradle"));
@@ -148,7 +148,7 @@ public class JadxPluginManager {
} }
context.init(); context.init();
} catch (Exception e) { } catch (Exception e) {
LOG.warn("Failed to init plugin: {}", context.getPluginId(), e); LOG.error("Failed to init plugin: {}", context.getPluginId(), e);
} }
} }
for (PluginContext context : pluginContexts) { for (PluginContext context : pluginContexts) {
@@ -39,6 +39,7 @@ public class PluginContext implements JadxPluginContext, JadxPluginRuntimeData,
private final JadxPluginsData pluginsData; private final JadxPluginsData pluginsData;
private final JadxPlugin plugin; private final JadxPlugin plugin;
private final JadxPluginInfo pluginInfo; private final JadxPluginInfo pluginInfo;
private final ClassLoader pluginClassLoader;
private AppContext appContext; private AppContext appContext;
@@ -53,16 +54,30 @@ public class PluginContext implements JadxPluginContext, JadxPluginRuntimeData,
this.pluginsData = pluginsData; this.pluginsData = pluginsData;
this.plugin = plugin; this.plugin = plugin;
this.pluginInfo = plugin.getPluginInfo(); this.pluginInfo = plugin.getPluginInfo();
this.pluginClassLoader = plugin.getClass().getClassLoader();
} }
public void init() { public void init() {
plugin.init(this); classLoaderWrap(() -> {
initialized = true; plugin.init(this);
initialized = true;
});
} }
public void unload() { public void unload() {
if (initialized) { if (initialized) {
plugin.unload(); classLoaderWrap(plugin::unload);
}
}
public void classLoaderWrap(Runnable task) {
Thread thread = Thread.currentThread();
ClassLoader prevClassLoader = thread.getContextClassLoader();
thread.setContextClassLoader(pluginClassLoader);
try {
task.run();
} finally {
thread.setContextClassLoader(prevClassLoader);
} }
} }
@@ -2,19 +2,19 @@ package jadx.core.utils;
import java.util.Objects; import java.util.Objects;
import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.IBlock;
import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.InsnNode;
public class BlockInsnPair { public class BlockInsnPair {
private final BlockNode block; private final IBlock block;
private final InsnNode insn; private final InsnNode insn;
public BlockInsnPair(BlockNode block, InsnNode insn) { public BlockInsnPair(IBlock block, InsnNode insn) {
this.block = block; this.block = block;
this.insn = insn; this.insn = insn;
} }
public BlockNode getBlock() { public IBlock getBlock() {
return block; return block;
} }
@@ -0,0 +1,30 @@
package jadx.core.utils;
import java.util.Objects;
import jadx.core.dex.nodes.IBlock;
import jadx.core.dex.nodes.IContainer;
public class BlockParentContainer {
private final IContainer parent;
private final IBlock block;
public BlockParentContainer(IContainer parent, IBlock block) {
this.parent = Objects.requireNonNull(parent);
this.block = Objects.requireNonNull(block);
}
public IBlock getBlock() {
return block;
}
public IContainer getParent() {
return parent;
}
@Override
public String toString() {
return "BlockParentContainer{" + block + ", parent=" + parent + '}';
}
}
@@ -599,7 +599,11 @@ public class BlockUtils {
if (s == until) { if (s == until) {
return true; return true;
} }
int id = s.getId(); if (s == from) {
// ignore possible block self loop
continue;
}
int id = s.getPos();
if (!visited.get(id)) { if (!visited.get(id)) {
visited.set(id); visited.set(id);
if (until.isDominator(s)) { if (until.isDominator(s)) {
@@ -1180,6 +1184,47 @@ public class BlockUtils {
return block; return block;
} }
/**
* Return out block of try catch, by finding where try branch meets catch branch.
* It traverse domFrontier start from handler block, find the first frontier
* whose predecessor is try end.
* <br>
* It could return null if they never meets, but this doesn't mean that catch
* ends at the method exit.
* (see TestSwitchWithTryCatch and ExcHandlersRegionMaker#processExcHandler).
*/
@Nullable
public static BlockNode getTryAndHandlerCrossBlock(MethodNode mth, ExceptionHandler handler) {
BlockNode start = handler.getHandlerBlock();
BlockNode topSplitter = BlockUtils.getTopSplitterForHandler(start);
List<ExceptionHandler> allHandlers = handler.getTryBlock().getHandlers();
List<BlockNode> handlerExitsCandidate = new ArrayList<>(BlockUtils.bitSetToBlocks(mth, start.getDomFrontier()));
BitSet visited = newBlocksBitSet(mth);
while (!handlerExitsCandidate.isEmpty()) {
BlockNode frontier = handlerExitsCandidate.remove(0);
if (visited.get(frontier.getPos())) {
continue;
}
visited.set(frontier.getPos());
// In some cases, handler's domFrontier is in the half of catch block
// instead of the end, so we need to make sure frontier's predecessor
// comes from try branch end:
// 1. not from handler branch, doesn't exist path from handler to pred
// 2. from try branch, exists path from topSplitter to pred
// 3. skip method exit
for (BlockNode pred : frontier.getPredecessors()) {
boolean predFromHandler = allHandlers.stream().anyMatch(h -> isPathExists(h.getHandlerBlock(), pred));
if (!predFromHandler && BlockUtils.isPathExists(topSplitter, pred)
&& frontier != mth.getExitBlock()) {
return frontier;
}
}
// if not found, add this frontier's frontier to candidate list
handlerExitsCandidate.addAll(BlockUtils.bitSetToBlocks(mth, frontier.getDomFrontier()));
}
return null;
}
@Nullable @Nullable
public static BlockNode getBlockWithFlag(List<BlockNode> blocks, AFlag flag) { public static BlockNode getBlockWithFlag(List<BlockNode> blocks, AFlag flag) {
for (BlockNode block : blocks) { for (BlockNode block : blocks) {
@@ -52,7 +52,7 @@ public class DebugChecks {
try { try {
checkMethod(mth); checkMethod(mth);
} catch (Exception e) { } catch (Exception e) {
throw new JadxRuntimeException("Debug check failed after visitor: " + visitor, e); mth.addError("Debug check failed after visitor: " + visitor, e);
} }
} }
@@ -23,7 +23,7 @@ public class DebugChecksPass extends AbstractVisitor {
if (!mth.contains(AType.JADX_ERROR)) { if (!mth.contains(AType.JADX_ERROR)) {
try { try {
DebugChecks.runChecksAfterVisitor(mth, visitorName); DebugChecks.runChecksAfterVisitor(mth, visitorName);
} catch (Throwable e) { } catch (Exception e) {
mth.addError("Check error", e); mth.addError("Check error", e);
} }
} }
@@ -49,6 +49,8 @@ import jadx.core.utils.exceptions.JadxException;
public class DebugUtils { public class DebugUtils {
private static final Logger LOG = LoggerFactory.getLogger(DebugUtils.class); private static final Logger LOG = LoggerFactory.getLogger(DebugUtils.class);
public static final Predicate<MethodNode> TEST_MTH_FILTER = mth -> mth.getName().equals("test");
private DebugUtils() { private DebugUtils() {
} }
@@ -63,7 +65,7 @@ public class DebugUtils {
} }
public static void dumpRawTest(MethodNode mth, String desc) { public static void dumpRawTest(MethodNode mth, String desc) {
dumpRaw(mth, desc, method -> method.getName().equals("test")); dumpRaw(mth, desc, TEST_MTH_FILTER);
} }
public static void dumpRaw(MethodNode mth, String desc) { public static void dumpRaw(MethodNode mth, String desc) {
@@ -91,6 +93,10 @@ public class DebugUtils {
}; };
} }
public static IDexTreeVisitor dumpRawTestVisitor(String desc) {
return dumpRawVisitor(desc, TEST_MTH_FILTER);
}
public static void dump(MethodNode mth, String desc) { public static void dump(MethodNode mth, String desc) {
File out = new File("test-graph-" + desc + "-tmp"); File out = new File("test-graph-" + desc + "-tmp");
DotGraphVisitor.dump().save(out, mth); DotGraphVisitor.dump().save(out, mth);

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