Compare commits

...

333 Commits

Author SHA1 Message Date
Skylot 03009b5159 chore: update dependencies 2026-06-17 22:32:00 +01:00
Skylot d25eda4839 feat: new module/library for work with call graphs (#2890) 2026-06-17 22:32:00 +01:00
Skylot 59003bdb1f refactor: use MethodInfo for unresolved methods usage, improve related code 2026-06-17 22:31:57 +01:00
Ananya 9d4babcdce fix: keep all dimensions for anewarray of an array element type (PR #2887) 2026-06-14 20:50:06 +00:00
Skylot 8c28a8530e fix(gui): improve dot graph viewer, CFG generation simplified 2026-06-02 22:00:16 +01:00
Skylot 6775cc5a93 fix(gui): improve code tabs, sync and related code 2026-05-30 21:20:14 +01:00
Muhammad Mufti Ismail e648e60af9 fix(quark): change from 'virtualenv' to 'venv' (PR #2879)
Refactor virtual environment creation command

moving from virtualenv to venv
2026-05-23 20:44:12 +00:00
mbv06 e6b7db35c1 feat(gui): handle pasted file lists in file chooser (PR #2876) 2026-05-15 19:28:46 +00:00
Skylot c3f7027bdd fix: resolve duplicated error attr in fallback codegen, allow to set whole attr list at once (#2842) 2026-05-11 17:09:23 +01:00
Skylot 2fe95da570 fix: workaround for duplicated variables names index increment (#2868) 2026-05-09 20:51:02 +01:00
Ruffalo Lavoisier 62fa2735dc feat(gui): collapse single-child directory chains in resource tree (PR #2866) 2026-05-09 16:39:22 +00:00
xxr0ss bce6611aaf feat(gui): add "copy reference" to context menu (PR #2863)
Co-authored-by: xxr0ss <xxr0ss@users.noreply.github.com>
2026-05-01 21:30:27 +01:00
ewt45 21aa90c5d1 fix: cover more situations for SwitchOverString (PR #2860) 2026-04-27 18:36:06 +00:00
Skylot 55e79fb70f fix(res): use safe number parsing for android manifest, refactor params object (#2857) 2026-04-25 20:06:02 +01:00
Skylot 044c75ab9f fix(gui): ensure all fonts are 'composite' (#2856) 2026-04-25 19:26:27 +01:00
Skylot 97fa8ff210 fix: build correct file path for class from default package (#2854) 2026-04-20 19:32:57 +01:00
Skylot 27c283fb11 fix: limit 'if' region out block to current scope (#2791) 2026-04-19 21:32:58 +01:00
Skylot bd1c3fffde fix: improve handler path check for regions 2026-04-19 20:32:14 +01:00
Skylot 169ad2901f feat: add partial region visitor with result return 2026-04-14 20:59:02 +01:00
Skylot 869422b424 refactor: replace recursion with loop for region traversal 2026-04-14 20:57:41 +01:00
Ruffalo Lavoisier ccc4164d54 fix: harden XML parser in FileTypeDetector against XML bomb DoS (PR #2851) 2026-04-14 18:15:34 +00:00
dependabot[bot] b61642a646 build(deps): bump actions/github-script from 8 to 9 (#2849)
Bumps [actions/github-script](https://github.com/actions/github-script) from 8 to 9.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v8...v9)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-version: '9'
  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>
2026-04-13 18:41:27 +00:00
dependabot[bot] 189e4181de build(deps): bump softprops/action-gh-release from 2 to 3 (#2848)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2 to 3.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v2...v3)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: '3'
  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>
2026-04-13 18:41:16 +00:00
Skylot 5b960db77e feat(gui): open single loaded class after load 2026-04-10 22:17:15 +01:00
Skylot 3119e8c893 fix: disable ternary mod for duplicated blocks (#2844) 2026-04-10 21:56:11 +01:00
Skylot f95c5e8f39 fix: limit ternary wrap to one level (#2844) 2026-04-09 20:50:40 +01:00
Skylot 1e908f7af3 refactor: add ErrorProne and NullAway checks, fix some issues 2026-04-06 21:13:08 +01:00
Jan S. 7ce40baacb fix(gui): NullPointerException when changing line wrap mode (PR #2843) 2026-04-05 18:48:49 +01:00
Skylot 7d689a85ea fix: remove result in wrapped insntructions (#2835) 2026-04-02 22:29:30 +01:00
Skylot c7a162d827 fix: resolve minor decompilation issues (#2835) 2026-04-02 21:22:35 +01:00
Skylot 325b3ac991 fix: use correct args copy/replace in wrapped insns (#2835) 2026-04-01 20:19:56 +01:00
Skylot 9a8a11619b chore: fix build warnings in java doc comments 2026-04-01 20:18:06 +01:00
Jan S. 7ae6bd737c fix(build): set *nix executable permissions on start scripts (PR #2838) 2026-03-30 19:03:04 +01:00
ewt45 57fd9b5bdb fix(gui): add file end line feed for exported code (PR #2836)
chore: add file end line feed for export code
2026-03-29 20:07:04 +01:00
Skylot cdd5bf536d fix(xapk): support files in sub-dirs in xapk (#2834) 2026-03-27 21:50:37 +00:00
Skylot dcce3aaa39 fix: use correct property for OS arch checks (#2830) 2026-03-24 20:17:35 +00:00
John b3d86ae908 fix: resolve class/package name conflicts on case-insensitive filesystems (PR #2828)
* Fix: resolve class/package name conflicts on case-insensitive filesystems

**RenameVisitor**
- Added package-level conflict detection for case-insensitive filesystems:
  when two packages differ only by case (e.g. com.Example vs com.example),
  rename the conflicting one and loop until the new name is also unique
- Added class-level conflict detection (same pattern): when two classes in
  the same package differ only by case (e.g. Sink vs sink), rename the
  conflicting one to prevent file overwrite on Windows export

**Tests**
- Added TestCaseSensitivePkgChecks: verifies package rename when packages
  differ only by case; fixed smali data (2.smali changed Bar→Foo to create
  a genuine path conflict under case-insensitive FS)
- Added TestCaseSensitiveClassInPkgChecks + smali fixtures: verifies class
  rename when two classes in a named package differ only by case
  (com.example.User vs com.example.user)

* Apply suggestions from code review

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

* Apply suggestions from code review

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

---------

Co-authored-by: john <johnwsa@qq.com>
Co-authored-by: skylot <118523+skylot@users.noreply.github.com>
2026-03-23 20:42:25 +00:00
dependabot[bot] 8b7d3f497e build(deps): bump gradle/actions from 5 to 6 (PR #2829)
Bumps [gradle/actions](https://github.com/gradle/actions) from 5 to 6.
- [Release notes](https://github.com/gradle/actions/releases)
- [Commits](https://github.com/gradle/actions/compare/v5...v6)

---
updated-dependencies:
- dependency-name: gradle/actions
  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>
2026-03-23 20:32:02 +00:00
Skylot 462e582bb8 chore: update gradle and dependencies 2026-03-22 21:15:36 +00:00
Skylot 14a7b63707 chore: apply common code style 2026-03-22 20:48:27 +00:00
Skylot 4051a50146 fix: stop type inference on exception (prevent endless loop) 2026-03-22 19:41:26 +00:00
Skylot b3db337abd feat: use queue instead of recursion for type updates 2026-03-22 19:10:42 +00:00
eason 15ea9a56b9 fix: handle null bounds in WindowLocation to prevent NPE on dialog dispose (PR #2826)
* fix: handle null bounds in WindowLocation to prevent NPE on dialog dispose

The equals(), hashCode(), and toString() methods in WindowLocation
could throw NullPointerException when bounds is null. This happens
when a dialog window is disposed before its bounds are fully
initialized (e.g., on macOS when closing the search dialog).

Use Objects.equals()/Objects.hashCode() for null-safe comparisons
and add a null guard in toString().

Fixes #2571

* additional null checks and null annotations

---------

Co-authored-by: easonysliu <easonysliu@tencent.com>
Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
2026-03-18 19:05:49 +00:00
Skylot 165ae24722 fix: support enum restore for constructor without args (#2821) 2026-03-16 22:25:26 +00:00
Skylot 11b38ffed2 fix(gui): use localized label for code cache preferences 2026-03-16 19:49:02 +00:00
Hd 81dfac6d83 fix(gui): localize code cache mode labels (PR #2825)
* fix(gui): localize code cache mode labels

* chore: fix spotless apply
2026-03-16 19:42:24 +00:00
peasoft a3bd3b09fd feat(gui): add mimetype for desktop file 2026-03-14 18:31:04 +00:00
peasoft 13d306024a fix(gui): resolve "launchScriptPath" is null 2026-03-14 18:31:04 +00:00
Skylot ff64da705c fix: remove useless PHI for duplicate moves (#2813) 2026-03-13 19:11:16 +00:00
Skylot 00196e412b fix(gui): move scripts leftover code, save tree state on script run 2026-03-07 19:54:32 +00:00
Skylot 06c4fea4d2 fix(api): allow to create input category node even if no matching files found (#2806) 2026-03-07 19:54:32 +00:00
小明 69fd88d883 fix(gui): update zh_tw translation (PR #2818) 2026-03-07 19:54:14 +00:00
Ananya Sharma f2f145019d fix: handle Kotlin 1.9+ $ENTRIES pattern in enum restoration (PR #2814)
* fix: handle Kotlin 1.9+ $ENTRIES pattern in enum restoration

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* keep $ENTRIES field, it still used in getEntries() method

---------

Co-authored-by: clawdbot-silly-waddle <clawdbot-silly-waddle@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
2026-03-07 19:37:12 +00:00
dependabot[bot] 7b3563fb62 build(deps): bump actions/upload-artifact from 6 to 7 (PR #2816)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-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>
2026-03-04 20:07:59 +00:00
dependabot[bot] 22ee9a216c build(deps): bump actions/download-artifact from 7 to 8 (PR #2815)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  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>
2026-03-04 20:07:43 +00:00
Hd 06b4467fdc fix(gui): update zh_CN translation (PR #2812) 2026-03-04 20:07:19 +00:00
Jan S. ff778ab372 fix(gui): use UI thread for adding logcat messages (#2811)
* fix(gui): use UI thread for adding logcat messages
other minor/logging improvements for debugger and adb connection

* checkstyle
2026-03-04 20:06:30 +00:00
Jan S. ff4dde62ae fix(gui): use UI thread for refreshStackFrameList in debugger view (PR #2807)
* fix(gui): use UI thread for refreshStackFrameList in debugger view

* call `resetAllDebuggingInfo` also from UI thread

---------

Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
2026-02-27 16:49:04 +00:00
Skylot 468b52342d fix(cli): in 'progress' log mode show issues from input plugins (#2803) 2026-02-25 19:44:50 +00:00
Jan S. 09d39de604 fix(gui): make the Split view checkbox correctly toggle between both modes (PR #2801) 2026-02-24 19:58:40 +00:00
Skylot eb079bd435 fix(gui): use UI thread for scroll in code (#2798) 2026-02-23 21:04:41 +00:00
Skylot d1b1fc0ab6 fix(gui): stop token process on NULL token in JadxTokenMaker (#2798) 2026-02-23 20:05:56 +00:00
Skylot 859d479569 chore: remove 'jadx-script-kotlin' 2026-02-22 15:53:02 +00:00
Skylot b0954e9620 fix(gui): resolve possible NPE during project data loading (#2794) 2026-02-18 22:28:36 +00:00
Skylot dfe1d0477d fix(script): update imports in example scripts (#2795) 2026-02-18 22:28:04 +00:00
Skylot a1aa6d7ecd fix: insert generic casts for variable assigned from fields with known types (#2776) 2026-02-17 20:55:23 +00:00
Skylot 6f01e9f76b fix(gui): update usage file data version (was changed in PR #2784) 2026-02-15 18:02:26 +00:00
gordon-f0 be8b96280e feat: graph views, code pane sync, and more (PR #2784)
* snapshot 219

* revert non-working string searcher

* fix(gui): fix illegal ':' character in path when exporting resources.arsc/res

* fix(gui): use resource short name when exporting a folder via context menu

* fix(gui): use new resource class for files in arsc (#2771)

* fix(gui): limit tabs title length, fix tooltips (#2771)

* resolve issues with script code area after merge

---------

Co-authored-by: Jan S. <jpstotz@users.noreply.github.com>
Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
2026-02-13 19:02:36 +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
Skylot 13e934ce4d build: update java to 24 for release workflow 2025-06-02 19:54:06 +01:00
Skylot 7b54c3ae70 fix(gui): prevent UI thread freeze on plugin install/uninstall 2025-06-01 20:25:18 +01:00
Skylot b82791706a fix(gui): redirect plugins menu action to show plugins settings page 2025-06-01 18:55:11 +01:00
Skylot e51a7fe417 fix(gui): lazy load hex viewer content 2025-06-01 17:01:10 +01:00
Skylot cf96fdec59 fix: add arsc file name prefix only in jadx-gui (#2373) 2025-05-31 20:12:54 +01:00
Skylot 3374f9b64a fix: use resource table file name as prefix in sub files (#2373) 2025-05-29 23:18:32 +01:00
Skylot 6b54cde89c fix: use wrap layout for options in search dialog, add size limit option 2025-05-29 22:44:21 +01:00
Skylot 59b560b553 fix: lazily create dirs in default file getter implementation 2025-05-29 17:59:26 +01:00
Skylot 8b08ac3806 fix: wrap MethodThrowsVisitor insns processing in try/catch (#2441) 2025-05-27 20:01:45 +01:00
Skylot d9d4180581 feat: use parallel jobs to delete files in directory (#2436) 2025-05-27 20:01:45 +01:00
Skylot 33f93ddc8a feat: move variable name apply from codegen to new pass (#2422) 2025-05-26 23:20:45 +01:00
Yaroslav bcd0c949dc feat(gui): use list component for recent projects on start page (PR #2513)
* feat(gui): use list component witch recent projects on start page

* fix: spotless check

* move classes into one package and some minor changes

---------

Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
2025-05-25 20:35:35 +01:00
Yaroslav fc0f1f9a1c fix(gui): validate extensions list in file dialog (PR #2515)
* fix(gui): fixed export file without extensions (close #2514)

* don't add null extensions

---------

Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
2025-05-25 20:23:36 +01:00
Yaroslav abd64007e2 fix(gui): validate hex address in GotoAddressDialog (PR #2512)
chore: use check if entered valid hex address from GotoAddressDialog & make error style search field if hex search value not valid in hex viewer
2025-05-24 23:00:18 +01:00
Skylot fb02e32a6a chore: don't use nullable annotations from RxJava 2025-05-24 22:16:10 +01:00
Skylot f33a2e4768 fix: for static synchronized methods remove top synchronized block (#2493) 2025-05-24 22:04:32 +01:00
Yaroslav 3d8e5e5851 chore: migrate from old unsupported rxjava2 to rxjava3 (PR #2511) 2025-05-24 21:56:23 +01:00
Yaroslav 646ee2d963 fix(gui): minor improved search dialog & small refactor for search fields (PR #2510)
* fix(gui): minor improved search dialog & small refactor for search fields

* fix: removed unused key string

* fix(gui): added icon for active tab in SearchDialog
2025-05-24 21:04:59 +01:00
Jan S. b6f27d8a1a feat(zip): provide direct InputStream of ZIP entries (PR #2509)
* chore: provide direct InputStream of ZIP entries

* use limited stream, return bytes without using stream

---------

Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
2025-05-24 20:36:10 +01:00
Skylot 97d04edb01 fix(xapk): new implementation to reduce zip unpack and fix NPE (#2501) 2025-05-24 00:09:29 +01:00
Yaroslav 2486c981a8 fix(gui): improve colors in usage tree, some UI fixes (PR #2506)
* fix(gui): refactor code in UsageDialogPlus

* fix(gui): added padding between buttons in package exclusion dialog & fixed crash

* fix(gui): added padding between buttons in settings dialog

* fix: fix javadoc

* fix: fix javadoc

* fix: fix javadoc
2025-05-23 23:15:16 +01:00
MewtR b7a8a2879e feat(gui): add option to disable the tooltip that pops up on hover (PR #2505)
feat(gui): add option to disable the tooltip that pops up on hover
2025-05-23 22:08:06 +01:00
Yaroslav 092e897104 fix(gui): fix rename package behaviors (PR #2500)
* fix(gui): fix rename package behaviors

* disable rename for default and synthetic packages

---------

Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
2025-05-22 23:26:39 +01:00
Yaroslav a0a9f7fd41 feat(gui): notify user if code has non-displayable character with current font (PR #2490)
* feat(gui): notify user, if code has non-displayable character in current font (fix #621)

* fix(gui): improve check showing undisplayed chars on current font

* fix code style

---------

Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
2025-05-22 23:05:05 +01:00
Little Hour Y 00f0f5547b fix(gui): using actual tree in usage tree dialog (PR #2503)
* A tree - shaped usage rendering scheme has been added

* A tree - shaped usage rendering scheme has been added - i18n update

* A tree - shaped usage rendering scheme has been added - Enhance the interactive experience

* A tree - shaped usage rendering scheme has been added - Jtree Render

---------

Co-authored-by: y <y@a.com>
2025-05-22 22:02:31 +01:00
Yaroslav 00608f8e51 feat(res): use file headers to detect extension for obfuscated resources (PR #2495)
* feat(res): add feature to use headers for detect resource extensions if resource obfuscated

* fix(res): read first 4kb data, for detect headers & use utf8 charset for decode bytes to string
2025-05-20 21:07:41 +01:00
Little Hour Y d0351a88ba feat(gui): a tree structure usage search (PR #2498)
* A tree - shaped usage rendering scheme has been added

* A tree - shaped usage rendering scheme has been added - i18n update

---------

Co-authored-by: y <y@a.com>
2025-05-20 20:46:07 +01:00
Yaroslav bee476895c feat(tools): improve tool for sync and update I18N lines (PR #2494)
* feat(tools): add to NLSAddNewLines tool remove lines if it not found on default reference & update commented untranslated line from reference

* fix: sync i18n translation from english reference file

* rename class and task, adjust code style

---------

Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
2025-05-19 19:17:07 +01:00
Yaroslav 580f25faae fix(res): fix decode xml attributes value (PR #2492)
* fix(res): now when decoding attributes used namespace (fix #1675)

* fix: code formatted

* fix: some code improved on ManifestAttributes class
2025-05-19 18:31:31 +01:00
Skylot 1d1ca7d0c0 fix: process synthetic resource inner classes using common method (#2482) 2025-05-18 22:58:37 +01:00
Skylot dbf4527ce6 chore: update dependencies 2025-05-17 23:29:38 +01:00
Yaroslav ec726d6ca1 fix(gui): update resource.arsc nodes after opened tab with resource ids (PR #2489)
* fix(gui): update resource.arsc nodes after opened tab with resource ids list by double click (fix #2488)

* Update jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java

---------

Co-authored-by: skylot <118523+skylot@users.noreply.github.com>
2025-05-17 22:51:55 +01:00
Yaroslav be1c02455f feat(gui): added dialog for select methods for generate frida class snippet (PR #2487)
feat(gui): add dialog for select methods for generate frida snippet
2025-05-16 18:14:33 +01:00
Yaroslav aee1e86398 fix(gui): restore tree state after renaming (PR #2486)
fix(gui): restore tree state after renaming
2025-05-15 20:54:50 +01:00
Yaroslav c0d721bea1 feat(gui): added feature to close all left tabs (#1390) (PR #2484)
feat(gui): added feature to close all left tabs (fix #1390)
2025-05-15 20:32:05 +01:00
Yaroslav e19a456642 feat(gui): added support view and open more resource files (PR #2483)
* feat(gui): added support view and open more resource files

* fix(gui): remove svg extension from resource types

* fix(gui): use error dialog from common utils

* fix: reformat code
2025-05-15 18:50:19 +01:00
Yaroslav acd57930cc fix(gui): improve apk signature loading speed (PR #2481)
fix(gui): improve spk signature loading speed (fix #1827)
2025-05-14 19:35:24 +01:00
Yaroslav 73348e5183 feat(gui): new HexViewer (PR #2469)
* feat(gui): rewrite hex viewer (#2415)

* fix: resolved merge conflict

* fix: resolved spotless checks

* fix: try fix CodeQL checks

* fix: fixed checkstyle checks

* fix: always reset selection on hex viewer by left mouse button

* fix: always reset selection on hex viewer by left mouse button

* chore(gui): changed hex viewer to bined lib

* fix: fixed checkstyle checks

* fix: fixed goto address dialog

* chore(gui): added search on hex viewer & updated bined library

* fix: remove commented code

* fix: removed useless code

* fix: removed useless code

* fix: try fix CodeQL scanning

* fix: try fix CodeQL scanning

* fix: try fix CodeQL scanning

* fix(gui): fixed search bar on hex viewer

* fix(gui): fixed is hex string checker

* fix(gui): fixed typo
2025-05-14 19:34:33 +01:00
Little Hour Y 32c92dd9a8 feat(gui): add "Copy All" button to Search dialog (PR #2480)
* add copy all button in CommonSearchDialog

* fix i18n test error

---------

Co-authored-by: ymoon <ymoon@ymoon.com>
2025-05-10 19:05:03 +01:00
nitram84 8bbdbfc563 feat: generate missing throws declarations, validate exceptions (#2441) (PR #2475)
* fix: generate missing throws declarations, validate exceptions (#2441)

* use ClspGraph.isImplements to check base classes, some code improvements

---------

Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
2025-05-09 21:08:41 +01:00
Yaroslav fd6cb2451b fix(gui): improved preview tab behaviors (PR #2477)
* fix(gui): improved preview tab behaviors

* Update jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java

---------

Co-authored-by: skylot <118523+skylot@users.noreply.github.com>
2025-05-08 21:53:15 +01:00
Yaroslav 47647bbb9a feat(gui): add Preview Tab Feature (PR #2474)
* feat(gui): created simple preview mode (#756)

* feat(gui): fixed opening code preview tab, if open from available tabs

* fix(gui): rollback mouse events for tree
2025-05-04 20:17:00 +01:00
Skylot e31d697cd9 fix: check and report invalid magic in '.dex' files (#2473) 2025-05-03 21:55:53 +01:00
Skylot a796d15289 fix(gui): add missing annotations color for dynamic theme (#2471) 2025-05-03 21:15:06 +01:00
Skylot f56eb271a1 feat(gui): allow to set code area theme with custom code (#2471) 2025-05-03 20:24:49 +01:00
Yaroslav fbebcb9845 feat(gui): add dynamic code editor theme from UI theme (PR #2471)
fix(gui): created dynamic theme for code editor (#1763)
2025-05-02 17:10:27 +01:00
Skylot 79c91634ad build: set Java 24 for windows bundle 2025-05-01 20:13:51 +01:00
Skylot f6e12d71a0 chore: update gradle and dependencies 2025-05-01 19:22:16 +01:00
JustFor 62835fbade fix(gui): update Messages_zh_CN.properties (PR #2470)
sync new text
2025-04-30 18:59:20 +01:00
Skylot 07c66b5c3c fix(tests): fix paths check on Windows 2025-04-29 22:31:29 +01:00
Skylot e3aa49aaa9 feat: add gradle export templates, support android app/lib and simple java 2025-04-29 21:54:40 +01:00
Skylot 9981949a2b feat(api): add 'unload' method to JadxPlugin (#2463) 2025-04-20 21:44:47 +01:00
Skylot ea6492e5ba fix(gui): properly handle excluded classes in code search (#2432) 2025-04-18 22:30:00 +01:00
Skylot 03d4cb134f build: downgrade java to version 21 for windows bundle 2025-04-15 22:42:39 +01:00
Skylot 37b0b09f25 chore: update dependencies 2025-04-15 21:50:27 +01:00
Skylot 1e75544636 refactor: deprecate temp methods which uses global temp dir 2025-04-15 21:47:52 +01:00
Skylot d4ce969fb7 fix(test): don't use global configs for cli integration tests 2025-04-15 21:47:51 +01:00
Skylot 9a692d6be4 fix: collect usage info in generic types 2025-04-14 21:43:26 +01:00
Adrian d3a8a43c74 fix: ignore external and invalid class names in signature (#2459)(PR #2460)
* fix: erroneous SignatureProcessor resolution of standard non-generic types with Annotation Signatures

* ignore external and invalid class names in signature

---------

Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
2025-04-14 21:36:54 +01:00
Jan S. 518da3d8b5 fix(gui): Xposed snippet argument types corrected (PR #2454) (#2458) 2025-04-13 18:50:43 +01:00
Josh Ryan 61f5386fe5 feat(gui): support APK Signature Scheme v3.1 signers (PR #2452) 2025-04-02 22:22:21 +01:00
Jan S. 20cb9c6a3b fix(gui): flatten packages view: non-leaf packages with classes were not visible in the sources tree (PR #2450)
fix(gui): Flatten packages view: non-leaf packages with classes were not visible in the sources tree
2025-03-28 14:21:56 +03:00
5idereal 4a6784dc68 fix(gui): update zh_TW translation (PR #2448) 2025-03-27 13:48:23 +03:00
Skylot bb6db25c9d fix(gui): apply log level as early as possible 2025-03-25 22:00:21 +00:00
Skylot d0c496858e build: improve launch4j parameters 2025-03-25 20:48:10 +00:00
Skylot b0ab702f9e build: add accessibility module for win bundle (#2446) 2025-03-25 20:46:32 +00:00
Skylot 5846e6d22e fix: scan annotations in usage collector visitor (#2435) 2025-03-22 22:04:15 +00:00
Skylot 4daad2fc79 chore: update gradle and dependencies 2025-03-19 22:25:33 +00:00
Skylot cca6ca25d1 fix: deduplicate blocks to help 'complex if' restructure (#2445) 2025-03-19 22:04:45 +00:00
Skylot dfa6a83f7c fix: improve BlockSet class, use it in more places 2025-03-19 22:01:47 +00:00
Skylot d1a3935c9e fix(res): improve resource table and config decoding (#2420) 2025-03-17 22:14:26 +00:00
Skylot b4f1021885 fix(gui): improve accessibility of preferences dialog (#2445) 2025-03-17 21:25:59 +00:00
Sencaid a163e5a1de fix(gui): update Messages_de_DE.properties (PR #2410)
* Deutsch aktualisiert

* Add files via upload

* Update jadx-gui/src/main/resources/i18n/Messages_de_DE.properties

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

* Add files via upload

* Apply suggestions from code review

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

---------

Co-authored-by: skylot <118523+skylot@users.noreply.github.com>
2025-03-14 19:13:32 +03:00
Jan S. 6eeb303d73 fix(res): chunk parsing was aborted too early for unordered entries (PR #2444) 2025-03-14 18:48:37 +03:00
Skylot 3209dbb7a4 fix: do not reopen zip file on every resource decode 2025-03-13 20:22:26 +00:00
Skylot d84f0389ec feat: custom zip reader implementation to fight tampering
fix(zip): use size info from CD if LFH entry is incorrect

refactor: move custom zip implementation into new module

feat: move ZipSecurity into jadx-zip module
2025-03-13 20:22:26 +00:00
Skylot 5d720dd29c fix(gui): allow file and directory have same name in tree (#2420) 2025-03-13 17:29:33 +00:00
Skylot c9d650d186 fix: unload attributes map if empty (#2433) 2025-03-06 19:36:46 +00:00
Skylot ce60aa8635 fix(gui): minor action names and icons adjustments (#2419) 2025-02-22 20:47:11 +00:00
yyyair 4a9276e904 feat(gui): tabs UI improvements (PR #2419)
* Adds "Reveal In Explorer" tab, to focus on on the current class's tree node

* Adds separators between tab categories, similar to how tabs look in VSCode/IntelliJ

* Rename from reveal_in_explorer to reveal_in_tree

* Minor fixes

* Handle mouse presses on tabs better

* use exist action name instead new one

---------

Co-authored-by: glu0n <glu0n@gmail.com>
Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
2025-02-22 18:46:00 +00:00
Skylot b78d3aa2f7 fix: do not replace constant fields which still used in code (#2414) 2025-02-19 23:05:35 +00:00
Skylot 4644d1d8ac fix(gui): correct class init method actions and highlight (#2412) 2025-02-17 20:38:21 +00:00
Skylot 7b8fc01319 feat(plugins): new API method to add popup menu entry for tree nodes (#2412) 2025-02-17 19:52:49 +00:00
Skylot ff66f95a8a fix(smali-input): improve error report for smali assemble (#2411) 2025-02-15 21:36:00 +00:00
Skylot 4ef1f3b12b feat(dex-input): initial support for DEX v41 (#2128) 2025-02-12 22:25:34 +00:00
Skylot 8873038b57 fix(gui): save current caret position and add it to tab code jump event (#2409) 2025-02-12 19:56:48 +00:00
Skylot bf58f03405 fix(gui): don't ask to save blank project 2025-02-09 19:23:50 +00:00
Skylot acf1c8187e refactor(gui): extract common dialog code into new parent class 2025-02-06 19:47:53 +00:00
Skylot 7bd1b14728 fix(gui): skip window pos saving if not changed 2025-02-06 18:46:56 +00:00
Skylot 61f04d6b07 fix(gui): set minimum size for comment dialog (#2405) 2025-02-06 18:46:55 +00:00
Skylot 5d5bf325fe fix: use fork of directories library with JNI implemetation for Windows (#2401) 2025-02-04 17:53:45 +00:00
Hidoni afdd2d8b39 feat(gui): add button to install desktop file directly from jadx-gui (PR #2404)
* fix: missing/incorrect .desktop file values

- correct comment
- Add StartupWMClass

* feat: add button to install desktop file from jadx-gui (#1392)

* fix: use POSIX compliant realpath instead of readlink

* fix: get XDG executable from PATH
2025-02-01 23:03:42 +00:00
Skylot b18604071a fix(gui): new implementation for tree state save/load (#2399) 2025-02-01 19:33:39 +00:00
Skylot 801890f0a8 fix(gui): improve JNode caching (#2400) 2025-02-01 17:36:53 +00:00
Jan S. 3d36c93beb fix(res): ignore reserved value in type chunk (PR #2403)
fix: ignore reserved value in type chunk (fixes #2402)
2025-01-29 17:12:28 +00:00
Skylot 54fbbd9524 fix(gui): workaround to force class decompilation in loading task (#2400) 2025-01-25 20:38:56 +00:00
Skylot 306547d499 fix(gui): improve expand tree (speed up and duplicate entries removal) (#2399) 2025-01-24 18:49:36 +00:00
Skylot a43b475be7 fix: improve rename using source class name (#2397) 2025-01-20 21:55:46 +00:00
Skylot bc4bb0dc41 fix(cli): concat new lines in plugin description 2025-01-20 19:35:54 +00:00
Skylot ea5916452d doc: add plugins usage section 2025-01-20 19:21:49 +00:00
Skylot 19f3cdf501 feat(plugins): add method to open usage dialog 2025-01-20 19:05:06 +00:00
Skylot 45d320a596 feat(plugins): add method to get jadx-gui icons by name 2025-01-20 17:13:23 +00:00
Skylot 6860a8be7e fix(res): handle null values in android manifest parsing (#2392) 2025-01-13 19:55:06 +00:00
Skylot 94915db739 chore: update gradle and dependencies 2025-01-13 19:55:06 +00:00
Yaroslav 29d114402d feat: parse and use Kotlin SourceDebugExtension with SMAP for rename classes and packages (PR #2389)
* feat: parse and use Kotlin SourceDebugExtension for rename classes and package

* fix: fixed typo

* fix: fixed spotless checks

* fix: fixed spotless checks
2025-01-06 20:16:26 +00:00
Bnyro 6889670b11 chore: add .desktop file for jadx-gui (PR #2388) 2025-01-01 20:47:29 +03:00
JustFor fe7d527fcc fix(gui): update Messages_zh_CN.properties (PR #2387)
sync new text
2024-12-31 18:53:01 +03:00
ewt45 84e211b809 fix: IfRegionMaker find the wrong outBlock (PR #2385)
* IfRegionMaker find wrong out block

* fix: test codes fail because of the previous commit
2024-12-30 16:42:03 +03:00
Skylot f4849d67cf build: reduce gitlab test stages due to new compute limit 2024-12-23 21:31:14 +00:00
Skylot 60a8d8b9fd fix: show all methods for 'simple' and 'fallback' decompilation modes 2024-12-23 21:18:04 +00:00
Skylot 1449354c54 fix: allow to inline methods with ternary instructions (#2380) 2024-12-23 20:59:42 +00:00
Skylot 0df474f35a fix: resolve incorrect var ref validation and remove (#2381) 2024-12-23 19:57:08 +00:00
Skylot de629544a3 fix: force type var short form as a key in generic resolve mapping (#2370) 2024-12-22 21:11:34 +00:00
Iscle 7a2dad8ef2 feat: add APKM support (PR #2379)
* feat: Add APKM support

* fix: Removed unused imports

With spotlessApply :P
2024-12-22 16:58:44 +00:00
Yaroslav a46e35c15c feat(gui): add export dialog with options (PR #2378)
fix(gui):add export dialog with options (#1983)
2024-12-21 20:34:54 +00:00
Skylot 73966fda89 fix(gui): add missing sync in clean task at search dialog close (#2363) 2024-12-21 19:44:15 +00:00
Skylot fe0ab5ebf8 fix(gui): improve detection of multi line method comments for update 2024-12-20 20:51:21 +00:00
Yaroslav 8345edf76b fix(gui): text on popup action change if need update comment & fixes default size on comment diallog (PR #2376)
* fix(gui):text on popup action change if need update comment & fixed default comment dialog size (#2155)

* don't add new action, just rename labels

---------

Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
2024-12-20 20:21:57 +00:00
Yaroslav ff0fbba720 feat(gui): added confirm dialog when removing script (PR #2375)
feat(gui): added confirm dialog when removing script (#2353)
2024-12-19 19:11:18 +00:00
Yaroslav fe41d6ed3d feat(gui): add script rename feature (PR #2374)
* feat(gui): add script rename feature (#2354)

* fix: resolve spotless check
2024-12-19 18:13:08 +00:00
nitram84 ff95b9e999 fix: better package-info support (PR #2369)
* fix: better package-info support

* fix: adjust checks for package-info
2024-12-18 22:33:48 +00:00
Skylot 87844d2193 fix: remove wrapped flag for inlined instruction (#2363) 2024-12-18 21:41:26 +00:00
Skylot 2ac0cc62e6 chore: update dependencies 2024-12-18 21:41:26 +00:00
Yaroslav 2924bb259c feat(gui): add webp image preview support (PR #2372)
* feat(gui): add webp image preview support (#2190)

* fix: resolve spotless check

* remove explicit webp provider register

* Update jadx-gui/src/main/java/jadx/gui/ui/panel/ImagePanel.java

---------

Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
2024-12-18 19:28:44 +00:00
ewt45 17695babf4 fix: inline anonymous class wrongly handles field as Classname.this (PR #2367)
* fix: inline anonymous class wrongly handle Classname.this

* add test case

---------

Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
2024-12-12 18:54:05 +00:00
ewt45 0d105a5095 fix: inner class parent is wrong when name formatted as Class$method$xxx (PR #2364) 2024-12-11 19:25:23 +00:00
nitram84 7eab3c8534 feat(gradle): handle non-final res ids for AGP >8.0.0 (PR #2362) 2024-12-11 17:56:28 +00:00
Skylot 47f2e516e5 fix(gui): resolve ignored mouse actions (#2360) 2024-12-09 17:44:32 +00:00
Skylot fdad829c49 chore: update dependencies 2024-12-09 17:41:26 +00:00
ewt45 2fa7f84251 fix: check respectBytecodeAccModifiers in MarkMethodsForInline (PR #2361)
Update MarkMethodsForInline.java
2024-12-09 16:17:22 +00:00
nitram84 828cad3287 feat: replace constants in annotations of method arguments (PR #2358) 2024-12-04 17:06:13 +00:00
Skylot cc0cb6a3d3 chore: update gradle and dependencies 2024-11-24 19:00:09 +00:00
Skylot a67dfcb7e1 fix(plugins): use release from asset if differ from release name with GitHub resolver 2024-11-24 18:52:32 +00:00
Skylot baa93bad63 fix: additional checks for default switch case exit (#2351) 2024-11-24 18:19:33 +00:00
Skylot 45df80f036 build: enable gradle configuration caching 2024-11-17 00:07:10 +03:00
Skylot 792e0d6f3a chore: update gradle and dependencies 2024-11-17 00:07:10 +03:00
Jan S. fe9d3bcab7 fix(res): allow jumping backwards to an already processed position (PR #2344)
Use buffered stream to allow going backwards in stream in case the type chunk entries are not ordered properly (see #2343).
2024-11-15 15:12:47 +00:00
Skylot 1e1036c049 fix: disable usage of JDK Unsafe class in GSON (#2341) 2024-11-13 18:28:04 +00:00
Skylot 60dcdc7096 build: add missing modules for runtime JRE 2024-11-12 18:54:01 +00:00
951 changed files with 54003 additions and 23454 deletions
+10 -10
View File
@@ -8,15 +8,15 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up JDK
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
java-version: 25
- name: Set jadx version
run: |
@@ -25,7 +25,7 @@ jobs:
echo "JADX_VERSION=$JADX_VERSION" >> $GITHUB_ENV
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
uses: gradle/actions/setup-gradle@v6
- name: Build
run: ./gradlew dist distWin
@@ -33,7 +33,7 @@ jobs:
JADX_BUILD_JAVA_VERSION: 11
- name: Save bundle artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: ${{ format('jadx-{0}', env.JADX_VERSION) }}
# Waiting fix for https://github.com/actions/upload-artifact/issues/39 to upload zip file
@@ -43,7 +43,7 @@ jobs:
retention-days: 14
- name: Save Windows bundle artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: ${{ format('jadx-gui-{0}-no-jre-win', env.JADX_VERSION) }}
# Upload unpacked files for now
@@ -54,14 +54,14 @@ jobs:
build-win-bundle:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up JDK
uses: oracle-actions/setup-java@v1
with:
release: 21
release: 25
- name: Print Java version
shell: bash
@@ -75,13 +75,13 @@ jobs:
echo "JADX_VERSION=$JADX_VERSION" >> $GITHUB_ENV
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
uses: gradle/actions/setup-gradle@v6
- name: Build
run: ./gradlew dist -PbundleJRE=true
- name: Save Windows with JRE bundle artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: ${{ format('jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }}
# Upload unpacked files for now
+4 -5
View File
@@ -14,19 +14,18 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Set up JDK
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
java-version: 25
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
uses: gradle/actions/setup-gradle@v6
- name: Build
run: ./gradlew build dist distWin
env:
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
+12 -12
View File
@@ -13,28 +13,28 @@ jobs:
build-release-win-bundle:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Set up JDK
uses: oracle-actions/setup-java@v1
with:
release: 23
release: 25
- name: Set jadx version
uses: actions/github-script@v7
uses: actions/github-script@v9
with:
script: |
const jadxVersion = context.ref.split('/').pop().substring(1)
core.exportVariable('JADX_VERSION', jadxVersion);
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
uses: gradle/actions/setup-gradle@v6
- name: Build
run: ./gradlew dist -PbundleJRE=true
- name: Save JRE bundle artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
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) }}
@@ -45,23 +45,23 @@ jobs:
needs: build-release-win-bundle
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Set up JDK
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
java-version: 25
- name: Set jadx version and release name
uses: actions/github-script@v7
uses: actions/github-script@v9
with:
script: |
const jadxVersion = context.ref.split('/').pop().substring(1)
core.exportVariable('JADX_VERSION', jadxVersion);
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
uses: gradle/actions/setup-gradle@v6
- name: Build
run: ./gradlew dist distWin
@@ -69,7 +69,7 @@ jobs:
JADX_BUILD_JAVA_VERSION: 11
- name: Download Windows JRE bundle
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: ${{ format('jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }}
path: ${{ format('build/jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }}
@@ -84,7 +84,7 @@ jobs:
ls -l *.zip
- name: Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
with:
name: ${{ env.JADX_VERSION }}
draft: true
+6 -1
View File
@@ -21,6 +21,7 @@ build/
classes/
idea/
.gradle/
.kotlin/
node_modules/
.vscode/
@@ -31,11 +32,15 @@ jadx-output/
*.jadx
*.class
*.jar
*.dump
*.log
*.cfg
*.orig
quark.json
*.json
*.dot
.env
cliff.toml
jadx-gui/src/main/resources/logback.xml
+2 -12
View File
@@ -8,17 +8,7 @@ before_script:
stages:
- test
java-11:
stage: test
image: eclipse-temurin:11
script: ./gradlew clean build dist distWin
java-17:
stage: test
image: eclipse-temurin:17
script: ./gradlew clean build dist distWin
java-21:
build-test:
stage: test
image: eclipse-temurin:21
script: ./gradlew clean build dist distWin
script: JADX_BUILD_JAVA_VERSION=11 JADX_TEST_JAVA_VERSION=11 ./gradlew clean build dist distWin
+1 -1
View File
@@ -4,7 +4,7 @@
<module name="jadx.jadx-gui.main"/>
<option name="PROGRAM_PARAMETERS" value="-v"/>
<option name="VM_PARAMETERS"
value="-Xms128M -XX:MaxRAMPercentage=70.0 -Dawt.useSystemAAFontSettings=lcd -Dswing.aatext=true -Djava.util.Arrays.useLegacyMergeSort=true -Djdk.util.zip.disableZip64ExtraFieldValidation=true -XX:+IgnoreUnrecognizedVMOptions --add-opens=java.base/java.lang=ALL-UNNAMED -Dsun.java2d.noddraw=true -Dsun.java2d.d3d=false -Dsun.java2d.ddforcevram=true -Dsun.java2d.ddblit=false -Dswing.useflipBufferStrategy=True"/>
value="-Xms128M -XX:MaxRAMPercentage=70.0 -Dawt.useSystemAAFontSettings=lcd -Dswing.aatext=true -Djava.util.Arrays.useLegacyMergeSort=true -Djdk.util.zip.disableZip64ExtraFieldValidation=true -XX:+IgnoreUnrecognizedVMOptions --add-opens=java.base/java.lang=ALL-UNNAMED --enable-native-access=ALL-UNNAMED -Dsun.java2d.noddraw=true -Dsun.java2d.d3d=false -Dsun.java2d.ddforcevram=true -Dsun.java2d.ddblit=false -Dswing.useflipBufferStrategy=true"/>
<method v="2">
<option name="Make" enabled="true"/>
</method>
+129 -90
View File
@@ -72,7 +72,7 @@ For Windows, you can download it from [oracle.com](https://www.oracle.com/java/t
You can use jadx in your java projects, check details on [wiki page](https://github.com/skylot/jadx/wiki/Use-jadx-as-a-library)
### Build from source
JDK 11 or higher must be installed:
JDK 17 or higher must be installed:
```
git clone https://github.com/skylot/jadx.git
cd jadx
@@ -86,106 +86,127 @@ and also packed to `build/jadx-<version>.zip`
### Usage
```
jadx[-gui] [command] [options] <input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk, .jadx.kts)
jadx[-gui] [command] [options] <input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk, .apkm, .jadx.kts)
commands (use '<command> --help' for command options):
plugins - manage jadx plugins
options:
-d, --output-dir - output directory
-ds, --output-dir-src - output directory for sources
-dr, --output-dir-res - output directory for resources
-r, --no-res - do not decode resources
-s, --no-src - do not decompile source code
--single-class - decompile a single class, full name, raw or alias
--single-class-output - file or dir for write if decompile a single class
--output-format - can be 'java' or 'json', default: java
-e, --export-gradle - save as android gradle project
-j, --threads-count - processing threads count, default: 4
-m, --decompilation-mode - code output mode:
'auto' - trying best options (default)
'restructure' - restore code structure (normal java code)
'simple' - simplified instructions (linear, with goto's)
'fallback' - raw instructions without modifications
--show-bad-code - show inconsistent code (incorrectly decompiled)
--no-xml-pretty-print - do not prettify XML
--no-imports - disable use of imports, always write entire package name
--no-debug-info - disable debug info parsing and processing
--add-debug-lines - add comments with debug line numbers if available
--no-inline-anonymous - disable anonymous classes inline
--no-inline-methods - disable methods inline
--no-move-inner-classes - disable move inner classes into parent
--no-inline-kotlin-lambda - disable inline for Kotlin lambdas
--no-finally - don't extract finally block
--no-restore-switch-over-string - don't restore switch over string
--no-replace-consts - don't replace constant value with matching constant field
--escape-unicode - escape non latin characters in strings (with \u)
--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-mode - set mode for handling the deobfuscation mapping file:
'read' - just read, user can always save manually (default)
'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
'ignore' - don't read or save (can be used to skip loading mapping files referenced in the project file)
--deobf - activate deobfuscation
--deobf-min - min length of name, renamed if shorter, default: 3
--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-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:
'read' - read if found, don't save (default)
'read-or-save' - read if found, save otherwise (don't overwrite)
'overwrite' - don't read, always save
'ignore' - don't read and don't save
--deobf-res-name-source - better name source for resources:
'auto' - automatically select best name (default)
'resources' - use resources names
'code' - use R class fields names
--use-source-name-as-class-name-alias - use source name as class name alias:
'always' - always use source name if it's available
'if-better' - use source name if it seems better than the current one
'never' - never use source name, even if it's available
--use-kotlin-methods-for-var-names - use kotlin intrinsic methods to rename variables, values: disable, apply, apply-and-hide, default: apply
--rename-flags - fix options (comma-separated list of):
'case' - fix case sensitivity issues (according to --fs-case-sensitive option),
'valid' - rename java identifiers to make them valid,
'printable' - remove non-printable chars from identifiers,
or single 'none' - to disable all renames
or single 'all' - to enable all (default)
--integer-format - how integers are displayed:
'auto' - automatically select (default)
'decimal' - use decimal
'hexadecimal' - use hexadecimal
--fs-case-sensitive - treat filesystem as case sensitive, false by default
--cfg - save methods control flow graph to dot file
--raw-cfg - save methods control flow graph (use raw instructions)
-f, --fallback - set '--decompilation-mode' to 'fallback' (deprecated)
--use-dx - use dx/d8 to convert java bytecode
--comments-level - set code comments level, values: error, warn, info, debug, user-only, none, default: info
--log-level - set log level, values: quiet, progress, error, warn, info, debug, default: progress
-v, --verbose - verbose output (set --log-level to DEBUG)
-q, --quiet - turn off output (set --log-level to QUIET)
--version - print jadx version
-h, --help - print this help
-d, --output-dir - output directory
-ds, --output-dir-src - output directory for sources
-dr, --output-dir-res - output directory for resources
-r, --no-res - do not decode resources
-s, --no-src - do not decompile source code
-j, --threads-count - processing threads count, default: 16
--single-class - decompile a single class, full name, raw or alias
--single-class-output - file or dir for write if decompile a single class
--output-format - can be 'java' or 'json', default: java
-e, --export-gradle - save as gradle project (set '--export-gradle-type' to 'auto')
--export-gradle-type - Gradle project template for export:
'auto' - detect automatically
'android-app' - Android Application (apk)
'android-library' - Android Library (aar)
'simple-java' - simple Java
-m, --decompilation-mode - code output mode:
'auto' - trying best options (default)
'restructure' - restore code structure (normal java code)
'simple' - simplified instructions (linear, with goto's)
'fallback' - raw instructions without modifications
--show-bad-code - show inconsistent code (incorrectly decompiled)
--no-xml-pretty-print - do not prettify XML
--no-imports - disable use of imports, always write entire package name
--no-debug-info - disable debug info parsing and processing
--add-debug-lines - add comments with debug line numbers if available
--no-inline-anonymous - disable anonymous classes inline
--no-inline-methods - disable methods inline
--no-move-inner-classes - disable move inner classes into parent
--no-inline-kotlin-lambda - disable inline for Kotlin lambdas
--no-finally - don't extract finally block
--no-restore-switch-over-string - don't restore switch over string
--no-replace-consts - don't replace constant value with matching constant field
--escape-unicode - escape non latin characters in strings (with \u)
--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-mode - set mode for handling the deobfuscation mapping file:
'read' - just read, user can always save manually (default)
'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
'ignore' - don't read or save (can be used to skip loading mapping files referenced in the project file)
--deobf - activate deobfuscation
--deobf-min - min length of name, renamed if shorter, default: 3
--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-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:
'read' - read if found, don't save (default)
'read-or-save' - read if found, save otherwise (don't overwrite)
'overwrite' - don't read, always save
'ignore' - don't read and don't save
--deobf-res-name-source - better name source for resources:
'auto' - automatically select best name (default)
'resources' - use resources names
'code' - use R class fields names
--use-source-name-as-class-name-alias - use source name as class name alias:
'always' - always use source name if it's available
'if-better' - use source name if it seems better than the current one
'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
--use-kotlin-methods-for-var-names - use kotlin intrinsic methods to rename variables, values: disable, apply, apply-and-hide, default: apply
--use-headers-for-detect-resource-extensions - Use headers for detect resource extensions if resource obfuscated
--rename-flags - fix options (comma-separated list of):
'case' - fix case sensitivity issues (according to --fs-case-sensitive option),
'valid' - rename java identifiers to make them valid,
'printable' - remove non-printable chars from identifiers,
or single 'none' - to disable all renames
or single 'all' - to enable all (default)
--integer-format - how integers are displayed:
'auto' - automatically select (default)
'decimal' - use decimal
'hexadecimal' - use hexadecimal
--type-update-limit - type update limit count (per one instruction), default: 10
--fs-case-sensitive - treat filesystem as case sensitive, false by default
--cfg - save methods control flow graph to dot file
--raw-cfg - save methods control flow graph (use raw instructions)
--call-graph - save app call graph in format: 'dot' or 'json', default: none
-f, --fallback - set '--decompilation-mode' to 'fallback' (deprecated)
--use-dx - use dx/d8 to convert java bytecode
--comments-level - set code comments level, values: error, warn, info, debug, user-only, none, default: info
--log-level - set log level, values: quiet, progress, error, warn, info, debug, default: progress
-v, --verbose - verbose output (set --log-level to DEBUG)
-q, --quiet - turn off output (set --log-level to QUIET)
--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>):
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.mode - convert mode, values: [dx, d8, both], default: both
- java-convert.d8-desugar - use desugar in d8, values: [yes, no], default: no
- java-convert.mode - convert mode, values: [dx, d8, both], default: both
- java-convert.d8-desugar - use desugar in d8, values: [yes, no], default: no
kotlin-metadata: Use kotlin.Metadata annotation for code generation
- 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.fields - rename fields, 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.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.class-alias - rename class alias, 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.companion - rename companion object, 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.getters - rename simple getters to field names, values: [yes, no], default: yes
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
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, RECAF_SIMPLE_FILE, JOBF_FILE], default: AUTO
- rename-mappings.invert - invert mapping on load, values: [yes, no], default: no
- 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
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:
JADX_DISABLE_XML_SECURITY - set to 'true' to disable all security checks for XML files
@@ -204,6 +225,24 @@ Examples:
```
These options also work in jadx-gui running from command line and override options from preferences' dialog
Usage for `plugins` command
```
usage: plugins [options]
options:
-i, --install <locationId> - install plugin with locationId
-j, --install-jar <path-to.jar> - install plugin from jar file
-l, --list - list installed plugins
-a, --available - list available plugins from jadx-plugins-list (aka marketplace)
-u, --update - update installed plugins
--uninstall <pluginId> - uninstall plugin with pluginId
--disable <pluginId> - disable plugin with pluginId
--enable <pluginId> - enable plugin with pluginId
--list-all - list all plugins including bundled and dropins
--list-versions <locationId> - fetch latest versions of plugin from locationId (will download all artefacts, limited to 10)
-h, --help - print this help
```
### Troubleshooting
Please check wiki page [Troubleshooting Q&A](https://github.com/skylot/jadx/wiki/Troubleshooting-Q&A)
+72 -12
View File
@@ -6,12 +6,14 @@ import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
import java.util.Locale
plugins {
id("com.github.ben-manes.versions") version "0.51.0"
id("se.patrikerdes.use-latest-versions") version "0.2.18"
id("com.diffplug.spotless") version "6.25.0"
id("com.github.ben-manes.versions") version "0.54.0"
id("se.patrikerdes.use-latest-versions") version "0.2.19"
id("com.diffplug.spotless") version "8.7.0"
}
val jadxVersion by extra { System.getenv("JADX_VERSION") ?: "dev" }
val jadxEnv = loadEnv(file("$rootDir/.env"))
val jadxVersion by extra { jadxEnv["JADX_VERSION"] ?: "dev" }
println("jadx version: $jadxVersion")
version = jadxVersion
@@ -19,7 +21,7 @@ val jadxBuildJavaVersion by extra { getBuildJavaVersion() }
fun getBuildJavaVersion(): Int? {
val envVarName = "JADX_BUILD_JAVA_VERSION"
val buildJavaVer = System.getenv(envVarName)?.toInt() ?: return null
val buildJavaVer = jadxEnv[envVarName]?.toInt() ?: return null
if (buildJavaVer < 11) {
throw GradleException("'$envVarName' can't be set to lower than 11")
}
@@ -27,6 +29,24 @@ fun getBuildJavaVersion(): Int? {
return buildJavaVer
}
// control ErrorProne checks level, can be: off, warn, error
val jadxBuildChecksMode: String by extra { getBuildChecksMode() }
fun getBuildChecksMode(): String {
val buildChecksMode = jadxEnv["JADX_BUILD_CHECKS_MODE"]?.lowercase() ?: "off"
val expectedValues = listOf("off", "warn", "error")
if (!expectedValues.contains(buildChecksMode)) {
throw GradleException("Unknown check mode: '$buildChecksMode', should be one of $expectedValues")
}
if (buildChecksMode != "off") {
val javaVersion = jadxBuildJavaVersion?.let { JavaVersion.toVersion(it) } ?: JavaVersion.current()
if (!javaVersion.isCompatibleWith(JavaVersion.VERSION_21)) {
throw GradleException("Error Prone requires Java 21")
}
}
return buildChecksMode
}
allprojects {
apply(plugin = "java")
apply(plugin = "checkstyle")
@@ -82,12 +102,49 @@ fun isNonStable(version: String): Boolean {
return isStable.not()
}
fun loadEnv(file: File): Map<String, String> {
val envMap = HashMap<String, String>()
System
.getenv()
.filter { it.key.startsWith("JADX_") }
.forEach { envMap[it.key] = it.value }
if (file.exists()) {
file
.readLines()
.map { it.trim() }
.filter { it.isNotEmpty() && !it.startsWith("#") }
.forEach {
val (k, v) = it.split("=", limit = 2)
envMap[k.trim()] = v.trim()
}
}
println(
"Loaded env vars (${envMap.size}):\n${
envMap.toList().sortedBy { it.first }.joinToString(separator = "\n") { "${it.first}=${it.second}" }
}\n",
)
return envMap
}
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 jarCliPattern = "jadx-cli-(.*)-all.jar".toPattern()
from(tasks.getByPath(":jadx-cli:installShadowDist")) {
exclude("**/*.jar")
filter { line ->
jarCliPattern.matcher(line).replaceAll("jadx-$1-all.jar")
jarCliPattern
.matcher(line)
.replaceAll("jadx-$1-all.jar")
.replace("-jar \"\\\"\$CLASSPATH\\\"\"", "-cp \"\\\"\$CLASSPATH\\\"\" jadx.cli.JadxCLI")
.replace("-jar \"%CLASSPATH%\"", "-cp \"%CLASSPATH%\" jadx.cli.JadxCLI")
}
@@ -113,15 +170,20 @@ val pack by tasks.registering(Zip::class) {
from(copyArtifacts)
archiveFileName.set("jadx-$jadxVersion.zip")
destinationDirectory.set(layout.buildDirectory)
eachFile {
if (path == "bin/jadx" || path == "bin/jadx-gui") {
permissions {
unix("rwxr-xr-x")
}
}
}
}
val distWin by tasks.registering(Zip::class) {
group = "jadx"
description = "Build Windows bundle"
val guiTask = tasks.getByPath("jadx-gui:copyDistWin")
dependsOn(guiTask)
from(guiTask.outputs)
from(distWinConfiguration)
destinationDirectory.set(layout.buildDirectory.dir("distWin"))
archiveFileName.set("jadx-gui-$jadxVersion-win.zip")
@@ -131,9 +193,7 @@ val distWin by tasks.registering(Zip::class) {
val distWinWithJre by tasks.registering(Zip::class) {
description = "Build Windows with JRE bundle"
val guiTask = tasks.getByPath(":jadx-gui:copyDistWinWithJre")
dependsOn(guiTask)
from(guiTask.outputs)
from(distWinWithJreConfiguration)
destinationDirectory.set(layout.buildDirectory.dir("distWinWithJre"))
archiveFileName.set("jadx-gui-$jadxVersion-with-jre-win.zip")
+3 -1
View File
@@ -3,9 +3,11 @@ plugins {
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.21")
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.10")
implementation("org.openrewrite:plugin:6.19.1")
implementation("net.ltgt.errorprone:net.ltgt.errorprone.gradle.plugin:4.2.0")
implementation("net.ltgt.nullaway:net.ltgt.nullaway.gradle.plugin:2.3.0")
}
repositories {
+40 -6
View File
@@ -1,29 +1,38 @@
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import net.ltgt.gradle.errorprone.CheckSeverity
import net.ltgt.gradle.errorprone.errorprone
import net.ltgt.gradle.nullaway.nullaway
plugins {
java
checkstyle
id("jadx-rewrite")
id("net.ltgt.errorprone")
id("net.ltgt.nullaway")
}
val jadxVersion: String by rootProject.extra
val jadxBuildJavaVersion: Int? by rootProject.extra
val jadxBuildChecksMode: String by rootProject.extra
group = "io.github.skylot"
version = jadxVersion
dependencies {
implementation("org.slf4j:slf4j-api:2.0.16")
compileOnly("org.jetbrains:annotations:26.0.1")
implementation("org.slf4j:slf4j-api:2.0.18")
compileOnly("org.jetbrains:annotations:26.1.0")
testImplementation("ch.qos.logback:logback-classic:1.5.12")
testImplementation("org.assertj:assertj-core:3.26.3")
testImplementation("ch.qos.logback:logback-classic:1.5.34")
testImplementation("org.assertj:assertj-core:3.27.7")
testImplementation("org.junit.jupiter:junit-jupiter:5.11.3")
testImplementation("org.junit.jupiter:junit-jupiter:5.13.3")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testCompileOnly("org.jetbrains:annotations:26.0.1")
testCompileOnly("org.jetbrains:annotations:26.1.0")
errorprone("com.google.errorprone:error_prone_core:2.50.0")
errorprone("com.uber.nullaway:nullaway:0.13.7")
}
repositories {
@@ -45,6 +54,7 @@ java {
tasks {
compileJava {
options.encoding = "UTF-8"
// options.compilerArgs = listOf("-Xlint:deprecation")
}
jar {
manifest {
@@ -61,3 +71,27 @@ tasks {
}
}
}
tasks.withType<JavaCompile>().configureEach {
val checkEnabled = jadxBuildChecksMode != "off"
if (checkEnabled) {
options.compilerArgs.add("-XDaddTypeAnnotationsToSymbol=true")
}
options.errorprone {
isEnabled = checkEnabled
allErrorsAsWarnings = jadxBuildChecksMode == "warn"
excludedPaths = ".*/test/.*"
nullaway {
if (jadxBuildChecksMode == "error") {
error()
}
annotatedPackages.add("jadx")
}
// TODO: fix and enable all checks
disable("MixedMutabilityReturnType")
disable("EqualsGetClass")
disable("OperatorPrecedence")
disable("UnusedVariable")
disable("ImmutableEnumChecker")
}
}
@@ -5,6 +5,11 @@ plugins {
id("org.jetbrains.kotlin.jvm")
}
dependencies {
implementation(kotlin("stdlib"))
implementation(kotlin("reflect")) // don't work from plugin classloader
}
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
@@ -1,5 +1,3 @@
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
plugins {
id("org.openrewrite.rewrite")
}
@@ -9,10 +7,10 @@ repositories {
}
dependencies {
rewrite("org.openrewrite.recipe:rewrite-testing-frameworks:2.21.0")
rewrite("org.openrewrite.recipe:rewrite-logging-frameworks:2.15.1")
rewrite("org.openrewrite.recipe:rewrite-migrate-java:2.28.0")
rewrite("org.openrewrite.recipe:rewrite-static-analysis:1.19.0")
rewrite("org.openrewrite.recipe:rewrite-testing-frameworks:3.38.0")
rewrite("org.openrewrite.recipe:rewrite-logging-frameworks:3.29.1")
rewrite("org.openrewrite.recipe:rewrite-migrate-java:3.37.0")
rewrite("org.openrewrite.recipe:rewrite-static-analysis:2.37.0")
}
tasks {
+3
View File
@@ -126,6 +126,9 @@
</module>
<module name="IllegalImport">
<property name="illegalClasses" value="jadx.core.utils.DebugUtils"/>
<!-- don't use nullable annotations from RxJava -->
<property name="illegalClasses" value="io.reactivex.rxjava3.annotations.NonNull"/>
<property name="illegalClasses" value="io.reactivex.rxjava3.annotations.Nullable"/>
</module>
<module name="RegexpSinglelineJava">
<property name="id" value="printstacktrace"/>
+10
View File
@@ -0,0 +1,10 @@
[Desktop Entry]
Name=JADX GUI
Comment=Dex to Java decompiler
Icon=jadx
Exec=jadx-gui %f
Terminal=false
Type=Application
Categories=Development;Java;
Keywords=Java;Decompiler;
StartupWMClass=jadx-gui-JadxGUI
+4
View File
@@ -2,6 +2,10 @@ org.gradle.warning.mode=all
org.gradle.parallel=true
org.gradle.caching=true
### Disable configuration cache for now: causing issues with spotless and version plugins
# org.gradle.configuration-cache=true
# org.gradle.configuration-cache.problems=warn
# Flags for google-java-format (optimize imports by spotless) for Java >= 16.
# Java < 9 will ignore unsupported flags (thanks to -XX:+IgnoreUnrecognizedVMOptions)
org.gradle.jvmargs=-XX:+IgnoreUnrecognizedVMOptions \
Binary file not shown.
+2 -2
View File
@@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=31c55713e40233a8303827ceb42ca48a47267a0ad4bab9177123121e71524c26
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Vendored
+5 -9
View File
@@ -1,7 +1,7 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -86,8 +86,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -115,7 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
@@ -173,7 +171,6 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -206,15 +203,14 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
Vendored
+1 -2
View File
@@ -70,11 +70,10 @@ goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@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
@rem End local scope for the variables with windows NT shell
+11 -3
View File
@@ -4,13 +4,14 @@ plugins {
id("application")
// use shadow only for application scripts, jar will be copied from jadx-gui
id("com.gradleup.shadow") version "8.3.5"
id("com.gradleup.shadow") version "8.3.8"
}
dependencies {
implementation(project(":jadx-core"))
implementation(project(":jadx-plugins-tools"))
implementation(project(":jadx-commons:jadx-app-commons"))
implementation(project(":jadx-commons:jadx-analysis"))
runtimeOnly(project(":jadx-plugins:jadx-dex-input"))
runtimeOnly(project(":jadx-plugins:jadx-java-input"))
@@ -18,12 +19,15 @@ dependencies {
runtimeOnly(project(":jadx-plugins:jadx-smali-input"))
runtimeOnly(project(":jadx-plugins:jadx-rename-mappings"))
runtimeOnly(project(":jadx-plugins:jadx-kotlin-metadata"))
runtimeOnly(project(":jadx-plugins:jadx-script:jadx-script-plugin"))
runtimeOnly(project(":jadx-plugins:jadx-kotlin-source-debug-extension"))
runtimeOnly(project(":jadx-plugins:jadx-xapk-input"))
runtimeOnly(project(":jadx-plugins:jadx-aab-input"))
runtimeOnly(project(":jadx-plugins:jadx-apkm-input"))
runtimeOnly(project(":jadx-plugins:jadx-apks-input"))
implementation("org.jcommander:jcommander:2.0")
implementation("ch.qos.logback:logback-classic:1.5.12")
implementation("ch.qos.logback:logback-classic:1.5.34")
implementation("com.google.code.gson:gson:2.14.0")
}
application {
@@ -31,10 +35,14 @@ application {
mainClass.set("jadx.cli.JadxCLI")
applicationDefaultJvmArgs =
listOf(
"-XX:+IgnoreUnrecognizedVMOptions",
"-Xms256M",
"-XX:MaxRAMPercentage=70.0",
"-XX:ParallelGCThreads=3",
// disable zip checks (#1962)
"-Djdk.util.zip.disableZip64ExtraFieldValidation=true",
// Foreign API access for 'directories' library (Windows only)
"--enable-native-access=ALL-UNNAMED",
)
applicationDistribution.from("$rootDir") {
include("README.md")
@@ -25,9 +25,8 @@ import jadx.api.plugins.options.OptionDescription;
import jadx.core.plugins.JadxPluginManager;
import jadx.core.plugins.PluginContext;
import jadx.core.utils.Utils;
import jadx.plugins.tools.JadxExternalPluginsLoader;
public class JCommanderWrapper<T> {
public class JCommanderWrapper {
private final JCommander jc;
private final JadxCLIArgs argsObj;
@@ -41,15 +40,25 @@ public class JCommanderWrapper<T> {
public boolean parse(String[] args) {
try {
jc.parse(args);
String[] fixedArgs = fixArgsForEmptySaveConfig(args);
jc.parse(fixedArgs);
applyFiles(argsObj);
return true;
} catch (ParameterException e) {
System.err.println("Arguments parse error: " + e.getMessage());
printUsage();
return false;
}
}
public void overrideProvided(JadxCLIArgs obj) {
applyFiles(obj);
for (ParameterDescription parameter : jc.getParameters()) {
if (parameter.isAssigned()) {
overrideProperty(obj, parameter);
}
}
}
public boolean processCommands() {
String parsedCommand = jc.getParsedCommand();
if (parsedCommand == null) {
@@ -58,20 +67,21 @@ public class JCommanderWrapper<T> {
return JadxCLICommands.process(this, jc, parsedCommand);
}
public void overrideProvided(JadxCLIArgs obj) {
List<ParameterDescription> fieldsParams = jc.getParameters();
List<ParameterDescription> parameters = new ArrayList<>(1 + fieldsParams.size());
parameters.add(jc.getMainParameterValue());
parameters.addAll(fieldsParams);
for (ParameterDescription parameter : parameters) {
if (parameter.isAssigned()) {
// copy assigned field value to obj
Parameterized parameterized = parameter.getParameterized();
Object providedValue = parameterized.get(parameter.getObject());
Object newValue = mergeValues(parameterized.getType(), providedValue, () -> parameterized.get(obj));
parameterized.set(obj, newValue);
}
}
/**
* The main parameter parsing doesn't work if accepting unknown options
*/
private void applyFiles(JadxCLIArgs argsObj) {
argsObj.setFiles(jc.getUnknownOptions());
}
/**
* Override assigned field value to obj
*/
private static void overrideProperty(JadxCLIArgs obj, ParameterDescription parameter) {
Parameterized parameterized = parameter.getParameterized();
Object providedValue = parameterized.get(parameter.getObject());
Object newValue = mergeValues(parameterized.getType(), providedValue, () -> parameterized.get(obj));
parameterized.set(obj, newValue);
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@@ -85,8 +95,39 @@ public class JCommanderWrapper<T> {
return value;
}
public List<String> getUnknownOptions() {
return jc.getUnknownOptions();
/**
* 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() {
@@ -175,7 +216,9 @@ public class JCommanderWrapper<T> {
}
if (addDefaults) {
String defaultValue = getDefaultValue(args, f);
if (defaultValue != null && !description.contains("(default)")) {
if (defaultValue != null
&& !defaultValue.isEmpty()
&& !description.contains("(default)")) {
opt.append(", default: ").append(defaultValue);
}
}
@@ -231,19 +274,20 @@ public class JCommanderWrapper<T> {
private String appendPluginOptions(int maxNamesLen) {
StringBuilder sb = new StringBuilder();
int k = 1;
// load and init all options plugins to print all options
try (JadxDecompiler decompiler = new JadxDecompiler(argsObj.toJadxArgs())) {
JadxPluginManager pluginManager = decompiler.getPluginManager();
pluginManager.load(new JadxExternalPluginsLoader());
pluginManager.load(decompiler.getArgs().getPluginLoader());
pluginManager.initAll();
for (PluginContext context : pluginManager.getAllPluginContexts()) {
JadxPluginOptions options = context.getOptions();
if (options != null) {
if (appendPlugin(context.getPluginInfo(), context.getOptions(), sb, maxNamesLen)) {
k++;
try {
for (PluginContext context : pluginManager.getAllPluginContexts()) {
JadxPluginOptions options = context.getOptions();
if (options != null) {
appendPlugin(context.getPluginInfo(), context.getOptions(), sb, maxNamesLen);
}
}
} finally {
pluginManager.unloadAll();
}
}
if (sb.length() == 0) {
@@ -0,0 +1,48 @@
package jadx.cli;
import java.util.Set;
import jadx.api.JadxArgs;
import jadx.api.security.JadxSecurityFlag;
import jadx.api.security.impl.JadxSecurity;
import jadx.commons.app.JadxCommonEnv;
import jadx.zip.security.DisabledZipSecurity;
import jadx.zip.security.IJadxZipSecurity;
import jadx.zip.security.JadxZipSecurity;
public class JadxAppCommon {
public static void applyEnvVars(JadxArgs jadxArgs) {
Set<JadxSecurityFlag> flags = JadxSecurityFlag.all();
IJadxZipSecurity zipSecurity;
boolean disableXmlSecurity = JadxCommonEnv.getBool("JADX_DISABLE_XML_SECURITY", false);
if (disableXmlSecurity) {
flags.remove(JadxSecurityFlag.SECURE_XML_PARSER);
// TODO: not related to 'xml security', but kept for compatibility
flags.remove(JadxSecurityFlag.VERIFY_APP_PACKAGE);
}
boolean disableZipSecurity = JadxCommonEnv.getBool("JADX_DISABLE_ZIP_SECURITY", false);
if (disableZipSecurity) {
flags.remove(JadxSecurityFlag.SECURE_ZIP_READER);
zipSecurity = DisabledZipSecurity.INSTANCE;
} else {
JadxZipSecurity jadxZipSecurity = new JadxZipSecurity();
int maxZipEntriesCount = JadxCommonEnv.getInt("JADX_ZIP_MAX_ENTRIES_COUNT", -2);
if (maxZipEntriesCount != -2) {
jadxZipSecurity.setMaxEntriesCount(maxZipEntriesCount);
}
int zipBombMinUncompressedSize = JadxCommonEnv.getInt("JADX_ZIP_BOMB_MIN_UNCOMPRESSED_SIZE", -2);
if (zipBombMinUncompressedSize != -2) {
jadxZipSecurity.setZipBombMinUncompressedSize(zipBombMinUncompressedSize);
}
int setZipBombDetectionFactor = JadxCommonEnv.getInt("JADX_ZIP_BOMB_DETECTION_FACTOR", -2);
if (setZipBombDetectionFactor != -2) {
jadxZipSecurity.setZipBombDetectionFactor(setZipBombDetectionFactor);
}
zipSecurity = jadxZipSecurity;
}
jadxArgs.setSecurity(new JadxSecurity(flags, zipSecurity));
}
}
+60 -34
View File
@@ -1,21 +1,25 @@
package jadx.cli;
import java.util.Set;
import java.nio.file.Path;
import java.util.function.Consumer;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.analysis.callgraph.JadxCallGraph;
import jadx.analysis.callgraph.api.ICallGraph;
import jadx.api.JadxArgs;
import jadx.api.JadxDecompiler;
import jadx.api.impl.AnnotatedCodeWriter;
import jadx.api.impl.NoOpCodeCache;
import jadx.api.impl.SimpleCodeWriter;
import jadx.api.security.JadxSecurityFlag;
import jadx.api.security.impl.JadxSecurity;
import jadx.api.usage.impl.EmptyUsageInfoCache;
import jadx.cli.LogHelper.LogLevelEnum;
import jadx.cli.config.JadxConfigAdapter;
import jadx.cli.plugins.JadxFilesGetter;
import jadx.commons.app.JadxCommonEnv;
import jadx.core.utils.exceptions.JadxArgsValidateException;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.plugins.tools.JadxExternalPluginsLoader;
public class JadxCLI {
@@ -31,12 +35,22 @@ public class JadxCLI {
}
public static int execute(String[] args) {
return execute(args, null);
}
public static int execute(String[] args, @Nullable Consumer<JadxArgs> argsMod) {
try {
JadxCLIArgs jadxArgs = new JadxCLIArgs();
if (jadxArgs.processArgs(args)) {
return processAndSave(jadxArgs);
JadxCLIArgs cliArgs = JadxCLIArgs.processArgs(args,
new JadxCLIArgs(),
new JadxConfigAdapter<>(JadxCLIArgs.class, "cli"));
if (cliArgs == null) {
return 0;
}
return 0;
JadxArgs jadxArgs = buildArgs(cliArgs);
if (argsMod != null) {
argsMod.accept(jadxArgs);
}
return runSave(jadxArgs, cliArgs);
} catch (JadxArgsValidateException e) {
LOG.error("Incorrect arguments: {}", e.getMessage());
return 1;
@@ -46,21 +60,24 @@ public class JadxCLI {
}
}
private static int processAndSave(JadxCLIArgs cliArgs) {
LogHelper.initLogLevel(cliArgs);
LogHelper.setLogLevelsForLoadingStage();
private static JadxArgs buildArgs(JadxCLIArgs cliArgs) {
JadxArgs jadxArgs = cliArgs.toJadxArgs();
jadxArgs.setCodeCache(new NoOpCodeCache());
jadxArgs.setUsageInfoCache(new EmptyUsageInfoCache());
jadxArgs.setPluginLoader(new JadxExternalPluginsLoader());
jadxArgs.setFilesGetter(JadxFilesGetter.INSTANCE);
initCodeWriterProvider(jadxArgs);
applyEnvVars(jadxArgs);
JadxAppCommon.applyEnvVars(jadxArgs);
return jadxArgs;
}
private static int runSave(JadxArgs jadxArgs, JadxCLIArgs cliArgs) {
try (JadxDecompiler jadx = new JadxDecompiler(jadxArgs)) {
jadx.load();
if (checkForErrors(jadx)) {
return 1;
return 2;
}
LogHelper.setLogLevelsForDecompileStage();
writeCallGraph(jadx, cliArgs);
if (!SingleClassMode.process(jadx, cliArgs)) {
save(jadx);
}
@@ -68,7 +85,7 @@ public class JadxCLI {
if (errorsCount != 0) {
jadx.printErrorsReport();
LOG.error("finished with errors, count: {}", errorsCount);
return 1;
return 3;
}
LOG.info("done");
return 0;
@@ -87,22 +104,6 @@ public class JadxCLI {
}
}
private static void applyEnvVars(JadxArgs jadxArgs) {
Set<JadxSecurityFlag> flags = JadxSecurityFlag.all();
boolean modified = false;
boolean disableXmlSecurity = JadxCommonEnv.getBool("JADX_DISABLE_XML_SECURITY", false);
if (disableXmlSecurity) {
flags.remove(JadxSecurityFlag.SECURE_XML_PARSER);
// TODO: not related to 'xml security', but kept for compatibility
flags.remove(JadxSecurityFlag.VERIFY_APP_PACKAGE);
modified = true;
}
// TODO: migrate 'ZipSecurity'
if (modified) {
jadxArgs.setSecurity(new JadxSecurity(flags));
}
}
private static boolean checkForErrors(JadxDecompiler jadx) {
if (jadx.getRoot().getClasses().isEmpty()) {
if (jadx.getArgs().isSkipResources()) {
@@ -114,10 +115,10 @@ public class JadxCLI {
jadx.getArgs().setSkipSources(true);
}
}
if (jadx.getErrorsCount() > 0) {
LOG.error("Load with errors! Check log for details");
int errorsCount = jadx.getErrorsCount();
if (errorsCount > 0) {
LOG.error("Loading finished with errors! Count: {}", errorsCount);
// continue processing
return false;
}
return false;
}
@@ -135,4 +136,29 @@ public class JadxCLI {
System.out.print(" \r");
}
}
private static void writeCallGraph(JadxDecompiler jadx, JadxCLIArgs cliArgs) {
JadxCLIArgs.CallGraphSaveMode mode = cliArgs.callGraphSaveMode;
if (mode == null || mode == JadxCLIArgs.CallGraphSaveMode.NONE) {
return;
}
Path outPath = jadx.getArgs().getOutDir().toPath();
ICallGraph callGraph = JadxCallGraph.builder(jadx)
.resolvedOnly(true)
.build();
Path cgPath;
switch (mode) {
case JSON:
cgPath = outPath.resolve("callgraph.json");
callGraph.writeJson(cgPath);
break;
case DOT:
cgPath = outPath.resolve("callgraph.dot");
callGraph.writeDot(cgPath);
break;
default:
throw new JadxRuntimeException("Unexpected call graph save mode: " + mode);
}
LOG.info("Call graph saved: {}", cgPath.toAbsolutePath());
}
}
+413 -29
View File
@@ -1,8 +1,8 @@
package jadx.cli;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
@@ -14,6 +14,10 @@ import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.beust.jcommander.DynamicParameter;
import com.beust.jcommander.IStringConverter;
import com.beust.jcommander.Parameter;
@@ -29,21 +33,33 @@ import jadx.api.args.IntegerFormat;
import jadx.api.args.ResourceNameSource;
import jadx.api.args.UseSourceNameAsClassNameAlias;
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.export.ExportGradleType;
import jadx.core.utils.exceptions.JadxArgsValidateException;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
public class JadxCLIArgs {
public class JadxCLIArgs implements IJadxConfig {
private static final Logger LOG = LoggerFactory.getLogger(JadxCLIArgs.class);
@Parameter(description = "<input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk, .jadx.kts)")
protected List<String> files = new ArrayList<>(1);
@JadxConfigExclude
@Parameter(description = "<input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk, .apkm, .jadx.kts)")
protected List<String> files = Collections.emptyList();
@JadxConfigExclude
@Parameter(names = { "-d", "--output-dir" }, description = "output directory")
protected String outDir;
@JadxConfigExclude
@Parameter(names = { "-ds", "--output-dir-src" }, description = "output directory for sources")
protected String outDirSrc;
@JadxConfigExclude
@Parameter(names = { "-dr", "--output-dir-res" }, description = "output directory for resources")
protected String outDirRes;
@@ -53,20 +69,33 @@ public class JadxCLIArgs {
@Parameter(names = { "-s", "--no-src" }, description = "do not decompile source code")
protected boolean skipSources = false;
@Parameter(names = { "-j", "--threads-count" }, description = "processing threads count")
protected int threadsCount = JadxArgs.DEFAULT_THREADS_COUNT;
@JadxConfigExclude
@Parameter(names = { "--single-class" }, description = "decompile a single class, full name, raw or alias")
protected String singleClass = null;
@JadxConfigExclude
@Parameter(names = { "--single-class-output" }, description = "file or dir for write if decompile a single class")
protected String singleClassOutput = null;
@Parameter(names = { "--output-format" }, description = "can be 'java' or 'json'")
protected String outputFormat = "java";
@Parameter(names = { "-e", "--export-gradle" }, description = "save as android gradle project")
@Parameter(names = { "-e", "--export-gradle" }, description = "save as gradle project (set '--export-gradle-type' to 'auto')")
protected boolean exportAsGradleProject = false;
@Parameter(names = { "-j", "--threads-count" }, description = "processing threads count")
protected int threadsCount = JadxArgs.DEFAULT_THREADS_COUNT;
@Parameter(
names = { "--export-gradle-type" },
description = "Gradle project template for export:"
+ "\n 'auto' - detect automatically"
+ "\n 'android-app' - Android Application (apk)"
+ "\n 'android-library' - Android Library (aar)"
+ "\n 'simple-java' - simple Java",
converter = ExportGradleTypeConverter.class
)
protected @Nullable ExportGradleType exportGradleType = null;
@Parameter(
names = { "-m", "--decompilation-mode" },
@@ -152,6 +181,7 @@ public class JadxCLIArgs {
)
protected String deobfuscationWhitelistStr = DeobfWhitelist.DEFAULT_STR;
@JadxConfigExclude
@Parameter(
names = { "--deobf-cfg-file" },
description = "deobfuscation mappings file used for JADX auto-generated names (in the JOBF file format),"
@@ -200,6 +230,12 @@ public class JadxCLIArgs {
)
protected UseSourceNameAsClassNameAlias useSourceNameAsClassNameAlias = null;
@Parameter(
names = { "--source-name-repeat-limit" },
description = "allow using source name if it appears less than a limit number"
)
protected int sourceNameRepeatLimit = 10;
@Parameter(
names = { "--use-kotlin-methods-for-var-names" },
description = "use kotlin intrinsic methods to rename variables, values: disable, apply, apply-and-hide",
@@ -207,6 +243,12 @@ public class JadxCLIArgs {
)
protected UseKotlinMethodsForVarNames useKotlinMethodsForVarNames = UseKotlinMethodsForVarNames.APPLY;
@Parameter(
names = { "--use-headers-for-detect-resource-extensions" },
description = "Use headers for detect resource extensions if resource obfuscated"
)
protected boolean useHeadersForDetectResourceExtensions = false;
@Parameter(
names = { "--rename-flags" },
description = "fix options (comma-separated list of):"
@@ -215,7 +257,7 @@ public class JadxCLIArgs {
+ "\n 'printable' - remove non-printable chars from identifiers,"
+ "\nor single 'none' - to disable all renames"
+ "\nor single 'all' - to enable all (default)",
converter = RenameConverter.class
listConverter = RenameConverter.class
)
protected Set<RenameEnum> renameFlags = EnumSet.allOf(RenameEnum.class);
@@ -229,6 +271,9 @@ public class JadxCLIArgs {
)
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")
protected boolean fsCaseSensitive = false;
@@ -238,6 +283,13 @@ public class JadxCLIArgs {
@Parameter(names = { "--raw-cfg" }, description = "save methods control flow graph (use raw instructions)")
protected boolean rawCfgOutput = false;
@Parameter(
names = { "--call-graph" },
description = "save app call graph in format: 'dot' or 'json'",
converter = CallGraphSaveModeConverter.class
)
protected CallGraphSaveMode callGraphSaveMode = CallGraphSaveMode.NONE;
@Parameter(names = { "-f", "--fallback" }, description = "set '--decompilation-mode' to 'fallback' (deprecated)")
protected boolean fallbackMode = false;
@@ -258,48 +310,113 @@ public class JadxCLIArgs {
)
protected LogHelper.LogLevelEnum logLevel = LogHelper.LogLevelEnum.PROGRESS;
@JadxConfigExclude
@Parameter(names = { "-v", "--verbose" }, description = "verbose output (set --log-level to DEBUG)")
protected boolean verbose = false;
@JadxConfigExclude
@Parameter(names = { "-q", "--quiet" }, description = "turn off output (set --log-level to QUIET)")
protected boolean quiet = false;
@JadxConfigExclude
@Parameter(names = { "--disable-plugins" }, description = "comma separated list of plugin ids to disable")
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")
protected boolean printVersion = false;
@JadxConfigExclude
@Parameter(names = { "-h", "--help" }, description = "print this help", help = true)
protected boolean printHelp = false;
@DynamicParameter(names = "-P", description = "Plugin options", hidden = true)
protected Map<String, String> pluginOptions = new HashMap<>();
public boolean processArgs(String[] args) {
JCommanderWrapper<JadxCLIArgs> jcw = new JCommanderWrapper<>(this);
return jcw.parse(args) && process(jcw);
}
/**
* Set values only for options provided in cmd.
* Used to merge saved options and options passed in command line.
* Obsolete method without config support,
* prefer {@link #processArgs(String[], JadxCLIArgs, JadxConfigAdapter)}
*/
public boolean overrideProvided(String[] args) {
JCommanderWrapper<JadxCLIArgs> jcw = new JCommanderWrapper<>(newInstance());
public boolean processArgs(String[] args) {
return processArgs(args, this, null) != null;
}
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 false;
return null;
}
jcw.overrideProvided(this);
return process(jcw);
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;
}
protected JadxCLIArgs newInstance() {
return new JadxCLIArgs();
private static <T extends JadxCLIArgs> void applyArgs(T argsObj) {
// apply log levels
LogHelper.initLogLevel(argsObj);
LogHelper.applyLogLevels();
}
private boolean process(JCommanderWrapper<JadxCLIArgs> jcw) {
files.addAll(jcw.getUnknownOptions());
public boolean process(JCommanderWrapper jcw) {
if (jcw.processCommands()) {
return false;
}
@@ -311,9 +428,7 @@ public class JadxCLIArgs {
System.out.println(JadxDecompiler.getVersion());
return false;
}
if (threadsCount <= 0) {
throw new JadxArgsValidateException("Threads count must be positive, got: " + threadsCount);
}
// unknown options added to 'files', run checks
for (String fileName : files) {
if (fileName.startsWith("-")) {
throw new JadxArgsValidateException("Unknown option: " + fileName);
@@ -322,6 +437,29 @@ public class JadxCLIArgs {
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() {
JadxArgs args = new JadxArgs();
args.setInputFiles(files.stream().map(FileUtils::toFile).collect(Collectors.toList()));
@@ -352,11 +490,16 @@ public class JadxCLIArgs {
args.setDeobfuscationMaxLength(deobfuscationMaxLength);
args.setDeobfuscationWhitelist(Arrays.asList(deobfuscationWhitelistStr.split(" ")));
args.setUseSourceNameAsClassNameAlias(getUseSourceNameAsClassNameAlias());
args.setUseHeadersForDetectResourceExtensions(useHeadersForDetectResourceExtensions);
args.setSourceNameRepeatLimit(sourceNameRepeatLimit);
args.setUseKotlinMethodsForVarNames(useKotlinMethodsForVarNames);
args.setResourceNameSource(resourceNameSource);
args.setEscapeUnicode(escapeUnicode);
args.setRespectBytecodeAccModifiers(respectBytecodeAccessModifiers);
args.setExportAsGradleProject(exportAsGradleProject);
args.setExportGradleType(exportGradleType);
if (exportAsGradleProject && exportGradleType == null) {
args.setExportGradleType(ExportGradleType.AUTO);
}
args.setSkipXmlPrettyPrint(skipXmlPrettyPrint);
args.setUseImports(useImports);
args.setDebugInfo(debugInfo);
@@ -367,20 +510,31 @@ public class JadxCLIArgs {
args.setAllowInlineKotlinLambda(allowInlineKotlinLambda);
args.setExtractFinally(extractFinally);
args.setRestoreSwitchOverString(restoreSwitchOverString);
args.setRenameFlags(renameFlags);
args.setRenameFlags(buildEnumSetForRenameFlags());
args.setFsCaseSensitive(fsCaseSensitive);
args.setCommentsLevel(commentsLevel);
args.setIntegerFormat(integerFormat);
args.setTypeUpdatesLimitCount(typeUpdatesLimitCount);
args.setUseDxInput(useDx);
args.setPluginOptions(pluginOptions);
args.setDisabledPlugins(Arrays.stream(disablePlugins.split(",")).map(String::trim).collect(Collectors.toSet()));
return args;
}
private EnumSet<RenameEnum> buildEnumSetForRenameFlags() {
EnumSet<RenameEnum> set = EnumSet.noneOf(RenameEnum.class);
set.addAll(renameFlags);
return set;
}
public List<String> getFiles() {
return files;
}
public void setFiles(List<String> files) {
this.files = files;
}
public String getOutDir() {
return outDir;
}
@@ -405,14 +559,26 @@ public class JadxCLIArgs {
return skipResources;
}
public void setSkipResources(boolean skipResources) {
this.skipResources = skipResources;
}
public boolean isSkipSources() {
return skipSources;
}
public void setSkipSources(boolean skipSources) {
this.skipSources = skipSources;
}
public int getThreadsCount() {
return threadsCount;
}
public void setThreadsCount(int threadsCount) {
this.threadsCount = threadsCount;
}
public boolean isFallbackMode() {
return fallbackMode;
}
@@ -421,82 +587,170 @@ public class JadxCLIArgs {
return useDx;
}
public void setUseDx(boolean useDx) {
this.useDx = useDx;
}
public DecompilationMode getDecompilationMode() {
return decompilationMode;
}
public void setDecompilationMode(DecompilationMode decompilationMode) {
this.decompilationMode = decompilationMode;
}
public boolean isShowInconsistentCode() {
return showInconsistentCode;
}
public void setShowInconsistentCode(boolean showInconsistentCode) {
this.showInconsistentCode = showInconsistentCode;
}
public boolean isUseImports() {
return useImports;
}
public void setUseImports(boolean useImports) {
this.useImports = useImports;
}
public boolean isDebugInfo() {
return debugInfo;
}
public void setDebugInfo(boolean debugInfo) {
this.debugInfo = debugInfo;
}
public boolean isAddDebugLines() {
return addDebugLines;
}
public void setAddDebugLines(boolean addDebugLines) {
this.addDebugLines = addDebugLines;
}
public boolean isInlineAnonymousClasses() {
return inlineAnonymousClasses;
}
public void setInlineAnonymousClasses(boolean inlineAnonymousClasses) {
this.inlineAnonymousClasses = inlineAnonymousClasses;
}
public boolean isInlineMethods() {
return inlineMethods;
}
public void setInlineMethods(boolean inlineMethods) {
this.inlineMethods = inlineMethods;
}
public boolean isMoveInnerClasses() {
return moveInnerClasses;
}
public void setMoveInnerClasses(boolean moveInnerClasses) {
this.moveInnerClasses = moveInnerClasses;
}
public boolean isAllowInlineKotlinLambda() {
return allowInlineKotlinLambda;
}
public void setAllowInlineKotlinLambda(boolean allowInlineKotlinLambda) {
this.allowInlineKotlinLambda = allowInlineKotlinLambda;
}
public boolean isExtractFinally() {
return extractFinally;
}
public void setExtractFinally(boolean extractFinally) {
this.extractFinally = extractFinally;
}
public boolean isRestoreSwitchOverString() {
return restoreSwitchOverString;
}
public void setRestoreSwitchOverString(boolean restoreSwitchOverString) {
this.restoreSwitchOverString = restoreSwitchOverString;
}
public Path getUserRenamesMappingsPath() {
return userRenamesMappingsPath;
}
public void setUserRenamesMappingsPath(Path userRenamesMappingsPath) {
this.userRenamesMappingsPath = userRenamesMappingsPath;
}
public UserRenamesMappingsMode getUserRenamesMappingsMode() {
return userRenamesMappingsMode;
}
public void setUserRenamesMappingsMode(UserRenamesMappingsMode userRenamesMappingsMode) {
this.userRenamesMappingsMode = userRenamesMappingsMode;
}
public boolean isDeobfuscationOn() {
return deobfuscationOn;
}
public void setDeobfuscationOn(boolean deobfuscationOn) {
this.deobfuscationOn = deobfuscationOn;
}
public int getDeobfuscationMinLength() {
return deobfuscationMinLength;
}
public void setDeobfuscationMinLength(int deobfuscationMinLength) {
this.deobfuscationMinLength = deobfuscationMinLength;
}
public int getDeobfuscationMaxLength() {
return deobfuscationMaxLength;
}
public void setDeobfuscationMaxLength(int deobfuscationMaxLength) {
this.deobfuscationMaxLength = deobfuscationMaxLength;
}
public String getDeobfuscationWhitelistStr() {
return deobfuscationWhitelistStr;
}
public void setDeobfuscationWhitelistStr(String deobfuscationWhitelistStr) {
this.deobfuscationWhitelistStr = deobfuscationWhitelistStr;
}
public String getGeneratedRenamesMappingFile() {
return generatedRenamesMappingFile;
}
public void setGeneratedRenamesMappingFile(String generatedRenamesMappingFile) {
this.generatedRenamesMappingFile = generatedRenamesMappingFile;
}
public GeneratedRenamesMappingFileMode getGeneratedRenamesMappingFileMode() {
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() {
if (useSourceNameAsClassNameAlias != null) {
return useSourceNameAsClassNameAlias;
@@ -508,6 +762,10 @@ public class JadxCLIArgs {
}
}
public void setUseSourceNameAsClassNameAlias(UseSourceNameAsClassNameAlias useSourceNameAsClassNameAlias) {
this.useSourceNameAsClassNameAlias = useSourceNameAsClassNameAlias;
}
/**
* @deprecated Use {@link #getUseSourceNameAsClassNameAlias()} instead.
*/
@@ -516,46 +774,106 @@ public class JadxCLIArgs {
return getUseSourceNameAsClassNameAlias().toBoolean();
}
public void setDeobfuscationUseSourceNameAsAlias(Boolean deobfuscationUseSourceNameAsAlias) {
this.deobfuscationUseSourceNameAsAlias = deobfuscationUseSourceNameAsAlias;
}
public ResourceNameSource getResourceNameSource() {
return resourceNameSource;
}
public void setResourceNameSource(ResourceNameSource resourceNameSource) {
this.resourceNameSource = resourceNameSource;
}
public UseKotlinMethodsForVarNames getUseKotlinMethodsForVarNames() {
return useKotlinMethodsForVarNames;
}
public void setUseKotlinMethodsForVarNames(UseKotlinMethodsForVarNames useKotlinMethodsForVarNames) {
this.useKotlinMethodsForVarNames = useKotlinMethodsForVarNames;
}
public IntegerFormat getIntegerFormat() {
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() {
return escapeUnicode;
}
public void setEscapeUnicode(boolean escapeUnicode) {
this.escapeUnicode = escapeUnicode;
}
public boolean isCfgOutput() {
return cfgOutput;
}
public void setCfgOutput(boolean cfgOutput) {
this.cfgOutput = cfgOutput;
}
public boolean isRawCfgOutput() {
return rawCfgOutput;
}
public void setRawCfgOutput(boolean rawCfgOutput) {
this.rawCfgOutput = rawCfgOutput;
}
public CallGraphSaveMode getCallGraphSaveMode() {
return callGraphSaveMode;
}
public void setCallGraphSaveMode(CallGraphSaveMode callGraphSaveMode) {
this.callGraphSaveMode = callGraphSaveMode;
}
public boolean isReplaceConsts() {
return replaceConsts;
}
public void setReplaceConsts(boolean replaceConsts) {
this.replaceConsts = replaceConsts;
}
public boolean isRespectBytecodeAccessModifiers() {
return respectBytecodeAccessModifiers;
}
public void setRespectBytecodeAccessModifiers(boolean respectBytecodeAccessModifiers) {
this.respectBytecodeAccessModifiers = respectBytecodeAccessModifiers;
}
public boolean isExportAsGradleProject() {
return exportAsGradleProject;
}
public void setExportAsGradleProject(boolean exportAsGradleProject) {
this.exportAsGradleProject = exportAsGradleProject;
}
public boolean isSkipXmlPrettyPrint() {
return skipXmlPrettyPrint;
}
public void setSkipXmlPrettyPrint(boolean skipXmlPrettyPrint) {
this.skipXmlPrettyPrint = skipXmlPrettyPrint;
}
public boolean isRenameCaseSensitive() {
return renameFlags.contains(RenameEnum.CASE);
}
@@ -572,22 +890,70 @@ public class JadxCLIArgs {
return fsCaseSensitive;
}
public void setFsCaseSensitive(boolean fsCaseSensitive) {
this.fsCaseSensitive = fsCaseSensitive;
}
public boolean isUseHeadersForDetectResourceExtensions() {
return useHeadersForDetectResourceExtensions;
}
public void setUseHeadersForDetectResourceExtensions(boolean useHeadersForDetectResourceExtensions) {
this.useHeadersForDetectResourceExtensions = useHeadersForDetectResourceExtensions;
}
public CommentsLevel getCommentsLevel() {
return commentsLevel;
}
public void setCommentsLevel(CommentsLevel commentsLevel) {
this.commentsLevel = commentsLevel;
}
public LogHelper.LogLevelEnum getLogLevel() {
return logLevel;
}
public void setLogLevel(LogHelper.LogLevelEnum logLevel) {
this.logLevel = logLevel;
}
public Map<String, String> getPluginOptions() {
return pluginOptions;
}
public void setPluginOptions(Map<String, String> pluginOptions) {
this.pluginOptions = pluginOptions;
}
public String getDisablePlugins() {
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>> {
private final String paramName;
@@ -653,6 +1019,12 @@ public class JadxCLIArgs {
}
}
public static class ExportGradleTypeConverter extends BaseEnumConverter<ExportGradleType> {
public ExportGradleTypeConverter() {
super(ExportGradleType::valueOf, ExportGradleType::values);
}
}
public static class LogLevelConverter extends BaseEnumConverter<LogHelper.LogLevelEnum> {
public LogLevelConverter() {
super(LogHelper.LogLevelEnum::valueOf, LogHelper.LogLevelEnum::values);
@@ -665,6 +1037,18 @@ public class JadxCLIArgs {
}
}
public enum CallGraphSaveMode {
NONE,
DOT,
JSON,
}
public static class CallGraphSaveModeConverter extends BaseEnumConverter<CallGraphSaveMode> {
public CallGraphSaveModeConverter() {
super(CallGraphSaveMode::valueOf, CallGraphSaveMode::values);
}
}
public abstract static class BaseEnumConverter<E extends Enum<E>> implements IStringConverter<E> {
private final Function<String, E> parse;
private final Supplier<E[]> values;
@@ -24,7 +24,7 @@ public class JadxCLICommands {
COMMANDS_MAP.forEach(builder::addCommand);
}
public static boolean process(JCommanderWrapper<?> jcw, JCommander jc, String parsedCommand) {
public static boolean process(JCommanderWrapper jcw, JCommander jc, String parsedCommand) {
ICommand command = COMMANDS_MAP.get(parsedCommand);
if (command == null) {
throw new JadxArgsValidateException("Unknown command: " + parsedCommand
+7 -18
View File
@@ -43,10 +43,9 @@ public class LogHelper {
return null;
}
if (args.quiet) {
return LogLevelEnum.QUIET;
}
if (args.verbose) {
return LogLevelEnum.DEBUG;
args.logLevel = LogLevelEnum.QUIET;
} else if (args.verbose) {
args.logLevel = LogLevelEnum.DEBUG;
}
return args.logLevel;
}
@@ -56,20 +55,7 @@ public class LogHelper {
applyLogLevel(logLevelValue);
}
public static void setLogLevelsForLoadingStage() {
if (logLevelValue == null) {
return;
}
if (logLevelValue == LogLevelEnum.PROGRESS) {
// show load errors
LogHelper.applyLogLevel(LogLevelEnum.ERROR);
fixForShowProgress();
return;
}
applyLogLevel(logLevelValue);
}
public static void setLogLevelsForDecompileStage() {
public static void applyLogLevels() {
if (logLevelValue == null) {
return;
}
@@ -86,6 +72,9 @@ public class LogHelper {
setLevelForClass(JadxCLI.class, Level.INFO);
setLevelForClass(JadxDecompiler.class, Level.INFO);
setLevelForClass(SingleClassMode.class, Level.INFO);
// show warnings and errors from input plugins
setLevelForPackage("jadx.plugins.input", Level.WARN);
}
private static void applyLogLevel(@NotNull LogLevelEnum logLevel) {
@@ -15,6 +15,7 @@ import jadx.cli.LogHelper;
import jadx.core.utils.StringUtils;
import jadx.plugins.tools.JadxPluginsList;
import jadx.plugins.tools.JadxPluginsTools;
import jadx.plugins.tools.data.JadxPluginListEntry;
import jadx.plugins.tools.data.JadxPluginMetadata;
import jadx.plugins.tools.data.JadxPluginUpdate;
@@ -65,7 +66,7 @@ public class CommandPlugins implements ICommand {
@SuppressWarnings("UnnecessaryReturnStatement")
@Override
public void process(JCommanderWrapper<?> jcw, JCommander subCommander) {
public void process(JCommanderWrapper jcw, JCommander subCommander) {
if (printHelp) {
jcw.printUsage(subCommander);
return;
@@ -116,9 +117,9 @@ public class CommandPlugins implements ICommand {
return;
}
if (available) {
List<JadxPluginMetadata> availableList = JadxPluginsList.getInstance().get();
List<JadxPluginListEntry> availableList = JadxPluginsList.getInstance().get();
System.out.println("Available plugins: " + availableList.size());
for (JadxPluginMetadata plugin : availableList) {
for (JadxPluginListEntry plugin : availableList) {
System.out.println(" - " + plugin.getName() + ": " + plugin.getDescription()
+ " (" + plugin.getLocationId() + ")");
}
@@ -156,7 +157,7 @@ public class CommandPlugins implements ICommand {
sb.append(" (disabled)");
}
sb.append(" - ").append(plugin.getName());
sb.append(": ").append(plugin.getDescription());
sb.append(": ").append(formatDescription(plugin.getDescription()));
System.out.println(sb);
}
}
@@ -192,11 +193,24 @@ public class CommandPlugins implements ICommand {
if (!installedSet.contains(plugin.getPluginId())) {
System.out.println(" - " + plugin.getPluginId()
+ " - " + plugin.getName()
+ ": " + plugin.getDescription());
+ ": " + formatDescription(plugin.getDescription()));
}
}
}
private static String formatDescription(String desc) {
if (desc.contains("\n")) {
// remove new lines
desc = desc.replaceAll("\\R+", " ");
}
int maxLen = 512;
if (desc.length() > maxLen) {
// truncate very long descriptions
desc = desc.substring(0, maxLen) + " ...";
}
return desc;
}
private void installPlugin(String locationId) {
JadxPluginMetadata plugin = JadxPluginsTools.getInstance().install(locationId);
System.out.println("Plugin installed: " + plugin.getPluginId() + ":" + plugin.getVersion());
@@ -7,5 +7,5 @@ import jadx.cli.JCommanderWrapper;
public interface ICommand {
String name();
void process(JCommanderWrapper<?> jcw, JCommander subCommander);
void process(JCommanderWrapper jcw, JCommander subCommander);
}
@@ -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 {
}
@@ -1,6 +1,5 @@
package jadx.cli.tools;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
@@ -11,7 +10,6 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -19,8 +17,10 @@ import org.slf4j.LoggerFactory;
import jadx.api.JadxArgs;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.android.TextResMapFile;
import jadx.core.utils.files.ZipFile;
import jadx.core.xmlgen.ResTableBinaryParser;
import jadx.zip.IZipEntry;
import jadx.zip.ZipContent;
import jadx.zip.ZipReader;
import static jadx.core.utils.files.FileUtils.expandDirs;
@@ -54,24 +54,25 @@ public class ConvertArscFile {
LOG.info("Input entries count: {}", resMap.size());
RootNode root = new RootNode(new JadxArgs()); // not really needed
ZipReader zipReader = new ZipReader();
rewritesCount = 0;
for (Path resFile : inputResFiles) {
ResTableBinaryParser resTableParser = new ResTableBinaryParser(root, true);
if (resFile.getFileName().toString().endsWith(".jar")) {
// Load resources.arsc from android.jar
try (ZipFile zip = new ZipFile(resFile.toFile())) {
ZipEntry entry = zip.getEntry("resources.arsc");
try (ZipContent zip = zipReader.open(resFile.toFile())) {
IZipEntry entry = zip.searchEntry("resources.arsc");
if (entry == null) {
LOG.error("Failed to load \"resources.arsc\" from {}", resFile);
continue;
}
try (InputStream inputStream = zip.getInputStream(entry)) {
try (InputStream inputStream = entry.getInputStream()) {
resTableParser.decode(inputStream);
}
}
} else {
// Load resources.arsc from extracted file
try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(resFile))) {
try (InputStream inputStream = Files.newInputStream(resFile)) {
resTableParser.decode(inputStream);
}
}
+1
View File
@@ -9,6 +9,7 @@
<!-- jadx-gui -->
<logger name="com.pinterest.ktlint" level="INFO"/>
<logger name="guru.nidi.graphviz" level="WARN"/>
<root level="INFO">
<appender-ref ref="STDOUT"/>
@@ -0,0 +1,139 @@
package jadx.cli;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.io.TempDir;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.plugins.loader.JadxBasePluginLoader;
import jadx.core.plugins.files.SingleDirFilesGetter;
import jadx.core.utils.Utils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
public class BaseCliIntegrationTest {
private static final Logger LOG = LoggerFactory.getLogger(BaseCliIntegrationTest.class);
static final PathMatcher LOG_ALL_FILES = path -> {
LOG.debug("File in result dir: {}", path);
return true;
};
@TempDir
Path testDir;
Path outputDir;
@BeforeEach
public void setUp() {
outputDir = testDir.resolve("output");
}
int execJadxCli(String sampleName, String... options) {
return execJadxCli(buildArgs(List.of(options), sampleName));
}
int execJadxCli(String[] args) {
return JadxCLI.execute(args, jadxArgs -> {
// don't use global config and plugins
jadxArgs.setFilesGetter(new SingleDirFilesGetter(testDir));
jadxArgs.setPluginLoader(new JadxBasePluginLoader());
});
}
String[] buildArgs(List<String> options, String... inputSamples) {
List<String> args = new ArrayList<>(options);
args.add("-v");
args.add("-d");
args.add(outputDir.toAbsolutePath().toString());
for (String inputSample : inputSamples) {
try {
URL resource = getClass().getClassLoader().getResource(inputSample);
assertThat(resource).isNotNull();
String sampleFile = resource.toURI().getRawPath();
args.add(sampleFile);
} catch (URISyntaxException e) {
fail("Failed to load sample: " + inputSample, e);
}
}
return args.toArray(new String[0]);
}
void decompile(String... inputSamples) throws IOException {
int result = execJadxCli(buildArgs(List.of(), inputSamples));
assertThat(result).isEqualTo(0);
List<Path> resultJavaFiles = collectJavaFilesInDir(outputDir);
assertThat(resultJavaFiles).isNotEmpty();
// do not copy input files as resources
for (Path path : collectFilesInDir(outputDir, LOG_ALL_FILES)) {
for (String inputSample : inputSamples) {
assertThat(path.toAbsolutePath().toString()).doesNotContain(inputSample);
}
}
}
static void printFiles(List<Path> files) {
LOG.info("Output files (count: {}):", files.size());
for (Path file : files) {
LOG.info(" {}", file);
}
LOG.info("");
}
String pathToUniformString(Path path) {
return path.toString().replace('\\', '/');
}
Path printFileContent(Path file) {
try {
String content = Files.readString(outputDir.resolve(file));
String spacer = Utils.strRepeat("=", 70);
LOG.info("File content: {}\n{}\n{}\n{}", file, spacer, content, spacer);
return file;
} catch (IOException e) {
throw new RuntimeException("Failed to load file: " + file, e);
}
}
static List<Path> collectJavaFilesInDir(Path dir) throws IOException {
PathMatcher javaMatcher = dir.getFileSystem().getPathMatcher("glob:**.java");
return collectFilesInDir(dir, javaMatcher);
}
static List<Path> collectAllFilesInDir(Path dir) throws IOException {
try (Stream<Path> pathStream = Files.walk(dir)) {
List<Path> files = pathStream
.filter(Files::isRegularFile)
.map(dir::relativize)
.collect(Collectors.toList());
printFiles(files);
return files;
}
}
static List<Path> collectFilesInDir(Path dir, PathMatcher matcher) throws IOException {
try (Stream<Path> pathStream = Files.walk(dir)) {
List<Path> files = pathStream
.filter(p -> Files.isRegularFile(p, LinkOption.NOFOLLOW_LINKS))
.filter(matcher::matches)
.collect(Collectors.toList());
printFiles(files);
return files;
}
}
}
@@ -93,7 +93,16 @@ public class JadxCLIArgsTest {
}
private JadxCLIArgs override(JadxCLIArgs jadxArgs, String... args) {
return check(jadxArgs, jadxArgs.overrideProvided(args));
return check(jadxArgs, overrideProvided(jadxArgs, args));
}
private static boolean overrideProvided(JadxCLIArgs jadxArgs, String[] args) {
JCommanderWrapper jcw = new JCommanderWrapper(new JadxCLIArgs());
if (!jcw.parse(args)) {
return false;
}
jcw.overrideProvided(jadxArgs);
return jadxArgs.process(jcw);
}
private static JadxCLIArgs check(JadxCLIArgs jadxArgs, boolean res) {
@@ -0,0 +1,77 @@
package jadx.cli;
import org.assertj.core.api.Condition;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class TestExport extends BaseCliIntegrationTest {
@Test
public void testBasicExport() throws Exception {
int result = execJadxCli("samples/small.apk");
assertThat(result).isEqualTo(0);
assertThat(collectAllFilesInDir(outputDir))
.map(this::pathToUniformString)
.haveExactly(2, new Condition<>(f -> f.startsWith("sources/") && f.endsWith(".java"), "sources"))
.haveExactly(10, new Condition<>(f -> f.startsWith("resources/"), "resources"))
.haveExactly(1, new Condition<>(f -> f.equals("resources/AndroidManifest.xml"), "manifest"))
.hasSize(12);
}
@Test
public void testGradleExportApk() throws Exception {
int result = execJadxCli("samples/small.apk", "--export-gradle");
assertThat(result).isEqualTo(0);
assertThat(collectAllFilesInDir(outputDir))
.describedAs("check output files")
.map(this::pathToUniformString)
.haveExactly(2, new Condition<>(f -> f.endsWith(".java"), "java classes"))
.haveExactly(0, new Condition<>(f -> f.endsWith("classes.dex"), "dex files"))
.hasSize(15);
}
@Test
public void testGradleExportAAR() throws Exception {
int result = execJadxCli("samples/test-lib.aar", "--export-gradle");
assertThat(result).isEqualTo(0);
assertThat(collectAllFilesInDir(outputDir))
.describedAs("check output files")
.map(this::printFileContent)
.map(this::pathToUniformString)
.haveExactly(1, new Condition<>(f -> f.startsWith("lib/src/main/java/") && f.endsWith(".java"), "java"))
.haveExactly(0, new Condition<>(f -> f.endsWith(".jar"), "jar files"))
.hasSize(8);
}
@Test
public void testGradleExportSimpleJava() throws Exception {
int result = execJadxCli("samples/HelloWorld.class", "--export-gradle");
assertThat(result).isEqualTo(0);
assertThat(collectAllFilesInDir(outputDir))
.describedAs("check output files")
.map(this::printFileContent)
.map(this::pathToUniformString)
.haveExactly(1, new Condition<>(f -> f.endsWith(".java") && f.startsWith("app/src/main/java/"), "java"))
.haveExactly(0, new Condition<>(f -> f.endsWith(".class"), "class files"))
.haveExactly(1, new Condition<>(f -> f.equals("settings.gradle.kts"), "settings"))
.haveExactly(1, new Condition<>(f -> f.equals("app/build.gradle.kts"), "build"))
.hasSize(3);
}
@Test
public void testGradleExportInvalidType() throws Exception {
int result = execJadxCli("samples/HelloWorld.class", "--export-gradle-type", "android-app");
assertThat(result).isEqualTo(0);
// expect output in 'android-app' template, but most fields will be set to UNKNOWN.
assertThat(collectAllFilesInDir(outputDir))
.describedAs("check output files")
.map(this::printFileContent)
.map(this::pathToUniformString)
.haveExactly(1, new Condition<>(f -> f.endsWith(".java") && f.startsWith("app/src/main/java/"), "java"))
.haveExactly(1, new Condition<>(f -> f.equals("settings.gradle"), "settings"))
.haveExactly(1, new Condition<>(f -> f.equals("build.gradle"), "build"))
.haveExactly(1, new Condition<>(f -> f.equals("app/build.gradle"), "app build"))
.hasSize(4);
}
}
+16 -90
View File
@@ -1,56 +1,32 @@
package jadx.cli;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.assertj.core.api.Condition;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.assertj.core.api.Assertions.assertThat;
public class TestInput {
private static final Logger LOG = LoggerFactory.getLogger(TestInput.class);
private static final PathMatcher LOG_ALL_FILES = path -> {
LOG.debug("File in result dir: {}", path);
return true;
};
@TempDir
Path testDir;
public class TestInput extends BaseCliIntegrationTest {
@Test
public void testHelp() {
int result = JadxCLI.execute(new String[] { "--help" });
int result = execJadxCli(new String[] { "--help" });
assertThat(result).isEqualTo(0);
}
@Test
public void testApkInput() throws Exception {
int result = JadxCLI.execute(buildArgs(List.of(), "samples/small.apk"));
int result = execJadxCli(buildArgs(List.of(), "samples/small.apk"));
assertThat(result).isEqualTo(0);
List<Path> resultFiles = collectAllFilesInDir(testDir);
printFiles(resultFiles);
assertThat(resultFiles)
assertThat(collectAllFilesInDir(outputDir))
.describedAs("check output files")
.map(p -> p.getFileName().toString())
.haveExactly(2, new Condition<>(f -> f.endsWith(".java"), "java classes"))
.haveExactly(9, new Condition<>(f -> f.endsWith(".xml"), "xml resources"))
.haveExactly(1, new Condition<>(f -> f.equals("classes.dex"), "dex"))
.haveExactly(1, new Condition<>(f -> f.equals("AndroidManifest.xml"), "manifest"))
.hasSize(13);
.hasSize(12);
}
@Test
@@ -75,84 +51,34 @@ public class TestInput {
@Test
public void testFallbackMode() throws Exception {
int result = JadxCLI.execute(buildArgs(List.of("-f"), "samples/hello.dex"));
int result = execJadxCli(buildArgs(List.of("-f"), "samples/hello.dex"));
assertThat(result).isEqualTo(0);
List<Path> files = collectJavaFilesInDir(testDir);
List<Path> files = collectJavaFilesInDir(outputDir);
assertThat(files).hasSize(1);
}
@Test
public void testSimpleMode() throws Exception {
int result = JadxCLI.execute(buildArgs(List.of("--decompilation-mode", "simple"), "samples/hello.dex"));
int result = execJadxCli(buildArgs(List.of("--decompilation-mode", "simple"), "samples/hello.dex"));
assertThat(result).isEqualTo(0);
List<Path> files = collectJavaFilesInDir(testDir);
List<Path> files = collectJavaFilesInDir(outputDir);
assertThat(files).hasSize(1);
}
@Test
public void testResourceOnly() throws Exception {
int result = JadxCLI.execute(buildArgs(List.of(), "samples/resources-only.apk"));
int result = execJadxCli(buildArgs(List.of(), "samples/resources-only.apk"));
assertThat(result).isEqualTo(0);
List<Path> files = collectFilesInDir(testDir,
List<Path> files = collectFilesInDir(outputDir,
path -> path.getFileName().toString().equalsIgnoreCase("AndroidManifest.xml"));
assertThat(files).isNotEmpty();
}
private void decompile(String... inputSamples) throws URISyntaxException, IOException {
int result = JadxCLI.execute(buildArgs(List.of(), inputSamples));
@Test
public void testNoRenameForDefPkg() throws Exception {
int result = execJadxCli(buildArgs(List.of("--rename-flags", "none"), "samples/defpkg.smali"));
assertThat(result).isEqualTo(0);
List<Path> resultJavaFiles = collectJavaFilesInDir(testDir);
assertThat(resultJavaFiles).isNotEmpty();
// do not copy input files as resources
for (Path path : collectFilesInDir(testDir, LOG_ALL_FILES)) {
for (String inputSample : inputSamples) {
assertThat(path.toAbsolutePath().toString()).doesNotContain(inputSample);
}
}
}
private String[] buildArgs(List<String> options, String... inputSamples) throws URISyntaxException {
List<String> args = new ArrayList<>(options);
args.add("-v");
args.add("-d");
args.add(testDir.toAbsolutePath().toString());
for (String inputSample : inputSamples) {
URL resource = getClass().getClassLoader().getResource(inputSample);
assertThat(resource).isNotNull();
String sampleFile = resource.toURI().getRawPath();
args.add(sampleFile);
}
return args.toArray(new String[0]);
}
private void printFiles(List<Path> files) {
LOG.info("Output files (count: {}):", files.size());
for (Path file : files) {
LOG.info(" {}", testDir.relativize(file));
}
}
private static List<Path> collectJavaFilesInDir(Path dir) throws IOException {
PathMatcher javaMatcher = dir.getFileSystem().getPathMatcher("glob:**.java");
return collectFilesInDir(dir, javaMatcher);
}
private static List<Path> collectAllFilesInDir(Path dir) throws IOException {
try (Stream<Path> pathStream = Files.walk(dir)) {
return pathStream
.filter(Files::isRegularFile)
.collect(Collectors.toList());
}
}
private static List<Path> collectFilesInDir(Path dir, PathMatcher matcher) throws IOException {
try (Stream<Path> pathStream = Files.walk(dir)) {
return pathStream
.filter(p -> Files.isRegularFile(p, LinkOption.NOFOLLOW_LINKS))
.filter(matcher::matches)
.collect(Collectors.toList());
}
List<Path> files = collectJavaFilesInDir(outputDir);
assertThat(files).hasSize(1);
}
}
@@ -0,0 +1,17 @@
package jadx.plugins.tools.utils;
import org.junit.jupiter.api.Test;
import static jadx.plugins.tools.utils.PluginUtils.extractVersion;
import static org.assertj.core.api.Assertions.assertThat;
class PluginUtilsTest {
@Test
public void testExtractVersion() {
assertThat(extractVersion("plugin-name-v1.2.3.jar")).isEqualTo("1.2.3");
assertThat(extractVersion("plugin-name-v1.2.jar")).isEqualTo("1.2");
assertThat(extractVersion("1.2.3.jar")).isEqualTo("1.2.3");
}
}
@@ -0,0 +1,2 @@
.class public LA;
.super Ljava/lang/Object;
Binary file not shown.
+29
View File
@@ -0,0 +1,29 @@
## jadx analysis
Various utilities for analyze and process code and related information.
### Call graph
Full app code usage/call graph.
Usage:
```java
JadxArgs args = new JadxArgs();
args.addInputFile(new File("input.apk"));
try (JadxDecompiler jadx = new JadxDecompiler(args)) {
jadx.load();
ICallGraph callGraph = JadxCallGraph.builder(jadx)
.includePackages("com.example") // filter nodes by package
.resolvedOnly(true) // add nodes only from app (exclude framework/lib calls)
.build();
for (ICallGraphEdge edge : callGraph.edges()) {
if (edge.isResolved()) {
System.out.printf("Edge from '%s' to '%s'%n", edge.from(), edge.to());
}
}
callGraph.writeDot(Path.of("test.dot")); // export to '.dot'
callGraph.writeJson(Path.of("test.json")); // export to JSON
}
```
@@ -0,0 +1,12 @@
plugins {
id("jadx-library")
}
dependencies {
implementation(project(":jadx-core"))
implementation("com.google.code.gson:gson:2.14.0")
testRuntimeOnly(project(":jadx-plugins:jadx-dex-input"))
testRuntimeOnly(project(":jadx-plugins:jadx-smali-input"))
}
@@ -0,0 +1,34 @@
package jadx.analysis.callgraph;
import java.nio.file.Path;
import java.util.List;
import jadx.analysis.callgraph.api.ICallGraph;
import jadx.analysis.callgraph.api.ICallGraphEdge;
import jadx.api.JadxArgs;
class CallGraph implements ICallGraph {
private final JadxArgs args;
private final List<ICallGraphEdge> edges;
public CallGraph(JadxArgs args, List<ICallGraphEdge> edges) {
this.args = args;
this.edges = edges;
}
@Override
public List<ICallGraphEdge> edges() {
return edges;
}
@Override
public void writeDot(Path path) {
new CallGraphExportDot(args, this).writeTo(path);
}
@Override
public void writeJson(Path path) {
new CallGraphExportJson(this).writeTo(path);
}
}
@@ -0,0 +1,6 @@
package jadx.analysis.callgraph;
import jadx.core.dex.attributes.AttrNode;
class CallGraphAttrNode extends AttrNode {
}
@@ -0,0 +1,92 @@
package jadx.analysis.callgraph;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import org.jetbrains.annotations.Nullable;
import jadx.analysis.callgraph.api.ICallGraph;
import jadx.analysis.callgraph.api.ICallGraphBuilder;
import jadx.analysis.callgraph.api.ICallGraphEdge;
import jadx.api.JadxDecompiler;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.MethodNode;
class CallGraphBuilder implements ICallGraphBuilder {
private final JadxDecompiler decompiler;
private boolean resolvedOnly = false;
private @Nullable String pkgFilter;
public CallGraphBuilder(JadxDecompiler decompiler) {
this.decompiler = decompiler;
}
@Override
public ICallGraphBuilder resolvedOnly(boolean resolved) {
this.resolvedOnly = resolved;
return this;
}
@Override
public ICallGraphBuilder includePackages(String pkgFilter) {
this.pkgFilter = pkgFilter.endsWith(".") ? pkgFilter : pkgFilter + '.';
return this;
}
@Override
public ICallGraph build() {
return new CallGraph(decompiler.getArgs(), collectEdges());
}
private List<ICallGraphEdge> collectEdges() {
AtomicInteger nodeId = new AtomicInteger();
Map<MethodInfo, CallGraphNode> nodes = new HashMap<>();
List<ICallGraphEdge> edges = new ArrayList<>();
for (ClassNode cls : decompiler.getRoot().getClasses(true)) {
if (ignorePkg(cls.getClassInfo())) {
continue;
}
for (MethodNode mth : cls.getMethods()) {
CallGraphNode thisNode = getCallGraphNode(mth, nodes, nodeId);
for (MethodNode use : mth.getUseIn()) {
if (ignorePkg(use.getDeclaringClass().getClassInfo())) {
continue;
}
CallGraphNode useInNode = getCallGraphNode(use, nodes, nodeId);
edges.add(new CallGraphEdge(useInNode, thisNode));
}
if (!resolvedOnly) {
for (MethodInfo used : mth.getUnresolvedUsed()) {
if (ignorePkg(used.getDeclClass())) {
continue;
}
CallGraphNode usedNode = getCallGraphNode(used, nodes, nodeId);
edges.add(new CallGraphEdge(thisNode, usedNode));
}
}
}
}
return edges;
}
private boolean ignorePkg(ClassInfo cls) {
if (pkgFilter == null) {
return false;
}
return !cls.getFullName().startsWith(pkgFilter);
}
private static CallGraphNode getCallGraphNode(MethodNode mth, Map<MethodInfo, CallGraphNode> nodes, AtomicInteger nodeId) {
return nodes.computeIfAbsent(mth.getMethodInfo(), i -> new CallGraphNode(nodeId.incrementAndGet(), mth));
}
private static CallGraphNode getCallGraphNode(MethodInfo mth, Map<MethodInfo, CallGraphNode> nodes, AtomicInteger nodeId) {
return nodes.computeIfAbsent(mth, i -> new CallGraphNode(nodeId.incrementAndGet(), mth));
}
}
@@ -0,0 +1,41 @@
package jadx.analysis.callgraph;
import jadx.analysis.callgraph.api.ICallGraphEdge;
import jadx.analysis.callgraph.api.ICallGraphNode;
import jadx.core.dex.attributes.IAttributeNode;
class CallGraphEdge implements ICallGraphEdge {
private final ICallGraphNode from;
private final ICallGraphNode to;
private final CallGraphAttrNode attrNode = new CallGraphAttrNode();
public CallGraphEdge(ICallGraphNode from, ICallGraphNode to) {
this.from = from;
this.to = to;
}
@Override
public ICallGraphNode from() {
return from;
}
@Override
public ICallGraphNode to() {
return to;
}
@Override
public boolean isResolved() {
return to.isResolved();
}
@Override
public IAttributeNode attributes() {
return attrNode;
}
@Override
public String toString() {
return "CallGraphEdge{from=" + from + ", to=" + to + '}';
}
}
@@ -0,0 +1,92 @@
package jadx.analysis.callgraph;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import jadx.analysis.callgraph.api.ICallGraph;
import jadx.analysis.callgraph.api.ICallGraphEdge;
import jadx.analysis.callgraph.api.ICallGraphNode;
import jadx.api.ICodeWriter;
import jadx.api.JadxArgs;
import jadx.api.impl.SimpleCodeWriter;
import jadx.core.utils.DotGraphUtils;
import jadx.core.utils.files.FileUtils;
import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
import static java.nio.file.StandardOpenOption.WRITE;
public class CallGraphExportDot {
private final JadxArgs args;
private final ICallGraph callGraph;
public CallGraphExportDot(JadxArgs args, ICallGraph callGraph) {
this.args = args;
this.callGraph = callGraph;
}
public void writeTo(Path path) {
try {
FileUtils.makeDirsForFile(path);
Files.writeString(path, writeToString(), StandardCharsets.UTF_8,
WRITE, TRUNCATE_EXISTING, CREATE);
} catch (IOException e) {
throw new RuntimeException("Failed to save JSON file: " + path, e);
}
}
public String writeToString() {
// collect nodes
Map<Integer, Node> nodeMap = new HashMap<>();
for (ICallGraphEdge edge : callGraph.edges()) {
addNode(edge.from(), nodeMap);
addNode(edge.to(), nodeMap);
}
List<Node> nodes = new ArrayList<>(nodeMap.values());
nodes.sort(Comparator.comparingInt(o -> o.id));
SimpleCodeWriter cw = new SimpleCodeWriter(args);
cw.add("digraph CallGraph {");
for (Node node : nodes) {
cw.startLine();
addNodeName(cw, node.id);
cw.add("[shape=record,label=\"{");
cw.add(DotGraphUtils.escape(node.method));
cw.add("}\"];");
}
for (ICallGraphEdge edge : callGraph.edges()) {
cw.startLine();
addNodeName(cw, edge.from().getId());
cw.add(" -> ");
addNodeName(cw, edge.to().getId());
cw.add(';');
}
cw.startLine('}');
return cw.getCodeStr();
}
private void addNodeName(ICodeWriter cw, int id) {
cw.add('N').add(Integer.toString(id));
}
private void addNode(ICallGraphNode cgNode, Map<Integer, Node> nodeMap) {
nodeMap.computeIfAbsent(cgNode.getId(), id -> {
Node node = new Node();
node.id = id;
node.method = cgNode.getMethodInfo().getRawFullId();
return node;
});
}
static final class Node {
int id;
String method;
}
}
@@ -0,0 +1,97 @@
package jadx.analysis.callgraph;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.Strictness;
import jadx.analysis.callgraph.api.ICallGraph;
import jadx.analysis.callgraph.api.ICallGraphEdge;
import jadx.analysis.callgraph.api.ICallGraphNode;
import jadx.core.utils.files.FileUtils;
import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
import static java.nio.file.StandardOpenOption.WRITE;
public class CallGraphExportJson {
private final ICallGraph callGraph;
private final Gson gson;
public CallGraphExportJson(ICallGraph callGraph) {
this.callGraph = callGraph;
this.gson = new GsonBuilder()
.disableJdkUnsafe()
.disableInnerClassSerialization()
.setStrictness(Strictness.STRICT)
// .setPrettyPrinting() // TODO: add option for pretty print?
.create();
}
public void writeTo(Path path) {
try {
FileUtils.makeDirsForFile(path);
Files.writeString(path, writeToString(), StandardCharsets.UTF_8,
WRITE, TRUNCATE_EXISTING, CREATE);
} catch (IOException e) {
throw new RuntimeException("Failed to save JSON file: " + path, e);
}
}
public String writeToString() {
List<Edge> edges = new ArrayList<>();
Map<Integer, Node> nodeMap = new HashMap<>();
for (ICallGraphEdge edge : callGraph.edges()) {
ICallGraphNode from = edge.from();
ICallGraphNode to = edge.to();
addNode(from, nodeMap);
addNode(to, nodeMap);
Edge jsonEdge = new Edge();
jsonEdge.from = from.getId();
jsonEdge.to = to.getId();
jsonEdge.resolved = edge.isResolved();
edges.add(jsonEdge);
}
List<Node> nodes = new ArrayList<>(nodeMap.values());
nodes.sort(Comparator.comparingInt(o -> o.id));
RootNode rootNode = new RootNode();
rootNode.nodes = nodes;
rootNode.edges = edges;
return gson.toJson(rootNode);
}
private void addNode(ICallGraphNode cgNode, Map<Integer, Node> nodeMap) {
nodeMap.computeIfAbsent(cgNode.getId(), id -> {
Node node = new Node();
node.id = id;
node.method = cgNode.getMethodInfo().getRawFullId();
return node;
});
}
static final class RootNode {
List<Node> nodes;
List<Edge> edges;
}
static final class Node {
int id;
String method;
}
static final class Edge {
int from;
int to;
boolean resolved;
}
}
@@ -0,0 +1,60 @@
package jadx.analysis.callgraph;
import org.jetbrains.annotations.Nullable;
import jadx.analysis.callgraph.api.ICallGraphNode;
import jadx.core.dex.attributes.IAttributeNode;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.nodes.MethodNode;
class CallGraphNode implements ICallGraphNode {
private final int id;
private final MethodInfo mthInfo;
private final @Nullable MethodNode mthNode;
private final CallGraphAttrNode attrNode;
public CallGraphNode(int id, MethodInfo mthInfo) {
this(id, mthInfo, null);
}
public CallGraphNode(int id, MethodNode mthNode) {
this(id, mthNode.getMethodInfo(), mthNode);
}
public CallGraphNode(int id, MethodInfo mthInfo, @Nullable MethodNode mthNode) {
this.id = id;
this.mthInfo = mthInfo;
this.mthNode = mthNode;
this.attrNode = new CallGraphAttrNode();
}
@Override
public int getId() {
return id;
}
@Override
public MethodInfo getMethodInfo() {
return mthInfo;
}
@Override
public @Nullable MethodNode getMethodNode() {
return mthNode;
}
@Override
public boolean isResolved() {
return mthNode != null;
}
@Override
public IAttributeNode attributes() {
return attrNode;
}
@Override
public String toString() {
return mthInfo.getFullId();
}
}
@@ -0,0 +1,11 @@
package jadx.analysis.callgraph;
import jadx.analysis.callgraph.api.ICallGraphBuilder;
import jadx.api.JadxDecompiler;
public class JadxCallGraph {
public static ICallGraphBuilder builder(JadxDecompiler decompiler) {
return new CallGraphBuilder(decompiler);
}
}
@@ -0,0 +1,13 @@
package jadx.analysis.callgraph.api;
import java.nio.file.Path;
import java.util.List;
public interface ICallGraph {
List<ICallGraphEdge> edges();
void writeDot(Path path);
void writeJson(Path path);
}
@@ -0,0 +1,10 @@
package jadx.analysis.callgraph.api;
public interface ICallGraphBuilder {
ICallGraphBuilder includePackages(String pkgFilter);
ICallGraphBuilder resolvedOnly(boolean resolved);
ICallGraph build();
}
@@ -0,0 +1,14 @@
package jadx.analysis.callgraph.api;
import jadx.core.dex.attributes.IAttributeNode;
public interface ICallGraphEdge {
ICallGraphNode from();
ICallGraphNode to();
boolean isResolved();
IAttributeNode attributes();
}
@@ -0,0 +1,21 @@
package jadx.analysis.callgraph.api;
import org.jetbrains.annotations.Nullable;
import jadx.core.dex.attributes.IAttributeNode;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.nodes.MethodNode;
public interface ICallGraphNode {
int getId();
MethodInfo getMethodInfo();
@Nullable
MethodNode getMethodNode();
boolean isResolved();
IAttributeNode attributes();
}
@@ -0,0 +1,86 @@
package jadx.analysis.callgraph.test;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import jadx.analysis.callgraph.CallGraphExportDot;
import jadx.analysis.callgraph.CallGraphExportJson;
import jadx.analysis.callgraph.JadxCallGraph;
import jadx.analysis.callgraph.api.ICallGraph;
import jadx.analysis.callgraph.api.ICallGraphEdge;
import jadx.api.JadxArgs;
import jadx.api.JadxDecompiler;
import static org.assertj.core.api.Assertions.assertThat;
class JadxCallGraphTest {
@TempDir
Path tempDir;
@SuppressWarnings("unused")
void usageExample() {
JadxArgs args = new JadxArgs();
args.addInputFile(new File("input.apk"));
try (JadxDecompiler jadx = new JadxDecompiler(args)) {
jadx.load();
ICallGraph callGraph = JadxCallGraph.builder(jadx)
.includePackages("com.example")
.resolvedOnly(false)
.build();
for (ICallGraphEdge edge : callGraph.edges()) {
if (edge.isResolved()) {
System.out.printf("Edge from '%s' to '%s'%n", edge.from(), edge.to());
}
}
callGraph.writeDot(Path.of("test.dot"));
callGraph.writeJson(Path.of("test.json"));
}
}
@Test
void simpleTest() {
JadxArgs args = new JadxArgs();
args.addInputFile(getSampleFile("simple.smali"));
try (JadxDecompiler jadx = new JadxDecompiler(args)) {
jadx.load();
ICallGraph callGraph = JadxCallGraph.builder(jadx)
.includePackages("test.pkg")
.resolvedOnly(false)
.build();
assertThat(callGraph.edges()).hasSize(1);
for (ICallGraphEdge edge : callGraph.edges()) {
System.out.println("Edge from " + edge.from() + " to " + edge.to());
}
String dotStr = new CallGraphExportDot(jadx.getArgs(), callGraph).writeToString();
System.out.println("dot: " + dotStr);
String jsonStr = new CallGraphExportJson(callGraph).writeToString();
System.out.println("json: " + jsonStr);
callGraph.writeDot(tempDir.resolve("test.dot"));
callGraph.writeJson(tempDir.resolve("test.json"));
}
}
private File getSampleFile(String sampleName) {
try {
URL resource = getClass().getResource("/samples/" + sampleName);
assertThat(resource).describedAs("Sample not found: %s", sampleName).isNotNull();
return new File(resource.toURI().toURL().getFile());
} catch (MalformedURLException | URISyntaxException e) {
throw new RuntimeException("Failed to load sample file: " + sampleName, e);
}
}
}
@@ -0,0 +1,19 @@
.class Ltest/pkg/HelloWorld;
.super Ljava/lang/Object;
.source "HelloWorld.java"
.method public static main([Ljava/lang/String;)V
.registers 2
const-string v0, "Hello, World"
invoke-static {p0, v0}, Ltest/pkg/HelloWorld;->hello(Ljava/lang/String;)V
return-void
.end method
.method public static hello(Ljava/lang/String;)V
.registers 2
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
invoke-virtual {v0, p0}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
return-void
.end method
@@ -3,5 +3,5 @@ plugins {
}
dependencies {
implementation("dev.dirs:directories:26")
implementation("io.get-coursier.util:directories-jni:0.1.4")
}
@@ -1,8 +1,10 @@
package jadx.commons.app;
import org.jetbrains.annotations.Nullable;
public class JadxCommonEnv {
public static String get(String varName, String defValue) {
public static @Nullable String get(String varName, @Nullable String defValue) {
String strValue = System.getenv(varName);
return isNullOrEmpty(strValue) ? defValue : strValue;
}
@@ -23,7 +25,7 @@ public class JadxCommonEnv {
return Integer.parseInt(strValue);
}
private static boolean isNullOrEmpty(String value) {
private static boolean isNullOrEmpty(@Nullable String value) {
return value == null || value.isEmpty();
}
}
@@ -3,13 +3,20 @@ package jadx.commons.app;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.function.Function;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import dev.dirs.ProjectDirectories;
import dev.dirs.impl.Windows;
import dev.dirs.impl.WindowsPowerShell;
import dev.dirs.jni.WindowsJni;
public class JadxCommonFiles {
private static final Logger LOG = LoggerFactory.getLogger(JadxCommonFiles.class);
private static final Path CONFIG_DIR;
private static final Path CACHE_DIR;
@@ -24,50 +31,75 @@ public class JadxCommonFiles {
static {
DirsLoader loader = new DirsLoader();
loader.init();
CONFIG_DIR = loader.getConfigDir();
CACHE_DIR = loader.getCacheDir();
}
private static final class DirsLoader {
private @Nullable ProjectDirectories dirs;
private Path configDir;
private Path cacheDir;
private final Path configDir;
private final Path cacheDir;
public void init() {
DirsLoader() {
try {
configDir = loadEnvDir("JADX_CONFIG_DIR", pd -> pd.configDir);
cacheDir = loadEnvDir("JADX_CACHE_DIR", pd -> pd.cacheDir);
AtomicReference<@Nullable ProjectDirectories> pdRef = new AtomicReference<>();
configDir = loadEnvDir("JADX_CONFIG_DIR", () -> loadDirs(pdRef).configDir);
cacheDir = loadEnvDir("JADX_CACHE_DIR", () -> loadDirs(pdRef).cacheDir);
} catch (Exception e) {
throw new RuntimeException("Failed to init common directories", e);
}
}
private Path loadEnvDir(String envVar, Function<ProjectDirectories, String> dirFunc) throws IOException {
private static Path loadEnvDir(String envVar, Supplier<String> dirFunc) throws IOException {
String envDir = JadxCommonEnv.get(envVar, null);
String dirStr;
if (envDir != null) {
dirStr = envDir;
} else {
dirStr = dirFunc.apply(loadDirs());
dirStr = dirFunc.get();
}
Path path = Path.of(dirStr).toAbsolutePath();
Files.createDirectories(path);
return path;
}
private synchronized ProjectDirectories loadDirs() {
if (dirs == null) {
dirs = ProjectDirectories.from("io.github", "skylot", "jadx");
private static ProjectDirectories loadDirs(AtomicReference<@Nullable ProjectDirectories> pdRef) {
ProjectDirectories currentDirs = pdRef.get();
if (currentDirs != null) {
return currentDirs;
}
return dirs;
LOG.debug("Loading system dirs ...");
long start = System.currentTimeMillis();
ProjectDirectories loadedDirs = ProjectDirectories.from("io.github", "skylot", "jadx", DirsLoader::getWinDirs);
if (LOG.isDebugEnabled()) {
LOG.debug("Loaded system dirs ({}ms): config: {}, cache: {}",
System.currentTimeMillis() - start, loadedDirs.configDir, loadedDirs.cacheDir);
}
pdRef.set(loadedDirs);
return loadedDirs;
}
public Path getCacheDir() {
/**
* Return JNI, Foreign or PowerShell implementation
*/
private static Windows getWinDirs() {
Windows impl = Windows.getDefaultSupplier().get();
if (impl instanceof WindowsPowerShell) {
if (JadxSystemInfo.IS_AMD64) {
// JNI library compiled only for x86-64
impl = new WindowsJni();
}
}
LOG.debug("Using win dirs implementation: {}", impl.getClass().getSimpleName());
return impl;
}
Path getCacheDir() {
return cacheDir;
}
public Path getConfigDir() {
Path getConfigDir() {
return configDir;
}
}
@@ -0,0 +1,26 @@
package jadx.commons.app;
import java.util.Locale;
@SuppressWarnings("unused")
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_ARCH.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");
Path dir;
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 {
dir = Files.createTempDirectory(JADX_TMP_INSTANCE_PREFIX);
}
+3
View File
@@ -0,0 +1,3 @@
## jadx zip
Custom zip reader implementation to fight tampering and provide additional security checks
+3
View File
@@ -0,0 +1,3 @@
plugins {
id("jadx-library")
}
@@ -0,0 +1,36 @@
package jadx.zip;
import java.io.File;
import java.io.InputStream;
public interface IZipEntry {
/**
* Zip entry name
*/
String getName();
/**
* Uncompressed bytes
*/
byte[] getBytes();
/**
* Stream of uncompressed bytes.
*/
InputStream getInputStream();
long getCompressedSize();
long getUncompressedSize();
boolean isDirectory();
File getZipFile();
/**
* Return true if {@link #getBytes()} method is more optimal to use other than
* {@link #getInputStream()}
*/
boolean preferBytes();
}
@@ -0,0 +1,9 @@
package jadx.zip;
import java.io.Closeable;
import java.io.IOException;
public interface IZipParser extends Closeable {
ZipContent open() throws IOException;
}
@@ -0,0 +1,50 @@
package jadx.zip;
import java.io.Closeable;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ZipContent implements Closeable {
private static final Logger LOG = LoggerFactory.getLogger(ZipContent.class);
private final IZipParser zipParser;
private final List<IZipEntry> entries;
private final Map<String, IZipEntry> entriesMap;
public ZipContent(IZipParser zipParser, List<IZipEntry> entries) {
this.zipParser = zipParser;
this.entries = entries;
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() {
return entries;
}
public @Nullable IZipEntry searchEntry(String fileName) {
return entriesMap.get(fileName);
}
@Override
public void close() throws IOException {
zipParser.close();
}
}
@@ -0,0 +1,115 @@
package jadx.zip;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;
import org.jetbrains.annotations.Nullable;
import jadx.zip.fallback.FallbackException;
import jadx.zip.fallback.FallbackZipParser;
import jadx.zip.parser.JadxZipParser;
import jadx.zip.security.IJadxZipSecurity;
import jadx.zip.security.JadxZipSecurity;
/**
* Jadx wrapper to provide custom zip parser ({@link JadxZipParser})
* with fallback to default Java implementation.
*/
public class ZipReader {
private final ZipReaderOptions options;
public ZipReader() {
this(ZipReaderOptions.getDefault());
}
public ZipReader(Set<ZipReaderFlags> flags) {
this(new ZipReaderOptions(new JadxZipSecurity(), flags));
}
public ZipReader(IJadxZipSecurity security) {
this(new ZipReaderOptions(security, ZipReaderFlags.none()));
}
public ZipReader(ZipReaderOptions options) {
this.options = options;
}
@SuppressWarnings("resource")
public ZipContent open(File zipFile) throws IOException {
if (!zipFile.exists()) {
throw new FileNotFoundException(zipFile.getAbsolutePath());
}
try {
JadxZipParser jadxParser = new JadxZipParser(zipFile, options);
IZipParser detectedParser = detectParser(zipFile, jadxParser);
return detectedParser.open();
} catch (FallbackException e) {
throw e;
} catch (Exception e) {
if (options.getFlags().contains(ZipReaderFlags.DONT_USE_FALLBACK)) {
throw new IOException("Failed to open zip: " + zipFile, e);
}
// switch to fallback parser
return buildFallbackParser(zipFile).open();
}
}
/**
* Visit valid entries in a zip file.
* Return not null value from visitor to stop iteration.
*/
public <R> @Nullable R visitEntries(File file, Function<IZipEntry, R> visitor) {
try (ZipContent content = open(file)) {
for (IZipEntry entry : content.getEntries()) {
R result = visitor.apply(entry);
if (result != null) {
return result;
}
}
} catch (Exception e) {
throw new RuntimeException("Failed to process zip file: " + file.getAbsolutePath(), e);
}
return null;
}
public void readEntries(File file, BiConsumer<IZipEntry, InputStream> visitor) {
visitEntries(file, entry -> {
if (!entry.isDirectory()) {
try (InputStream in = entry.getInputStream()) {
visitor.accept(entry, in);
} catch (Exception e) {
throw new RuntimeException("Failed to process zip entry: " + entry, e);
}
}
return null;
});
}
public ZipReaderOptions getOptions() {
return options;
}
private IZipParser detectParser(File zipFile, JadxZipParser jadxParser) throws IOException {
if (zipFile.getName().endsWith(".apk")
|| options.getFlags().contains(ZipReaderFlags.DONT_USE_FALLBACK)) {
return jadxParser;
}
if (!jadxParser.canOpen()) {
return buildFallbackParser(zipFile);
}
// default
if (options.getFlags().contains(ZipReaderFlags.FALLBACK_AS_DEFAULT)) {
return buildFallbackParser(zipFile);
}
return jadxParser;
}
private FallbackZipParser buildFallbackParser(File zipFile) throws IOException {
return new FallbackZipParser(zipFile, options);
}
}
@@ -0,0 +1,32 @@
package jadx.zip;
import java.util.EnumSet;
import java.util.Set;
public enum ZipReaderFlags {
/**
* Search all local file headers by signature without reading
* 'central directory' and 'end of central directory' entries
*/
IGNORE_CENTRAL_DIR_ENTRIES,
/**
* Enable additional checks to verify zip data and report possible tampering
*/
REPORT_TAMPERING,
/**
* Use fallback (java built-in implementation) parser as default.
* Custom implementation will be used for '*.apk' files only.
*/
FALLBACK_AS_DEFAULT,
/**
* Use only jadx custom parser and do not switch to fallback on errors.
*/
DONT_USE_FALLBACK;
public static Set<ZipReaderFlags> none() {
return EnumSet.noneOf(ZipReaderFlags.class);
}
}
@@ -0,0 +1,29 @@
package jadx.zip;
import java.util.Set;
import jadx.zip.security.IJadxZipSecurity;
import jadx.zip.security.JadxZipSecurity;
public class ZipReaderOptions {
public static ZipReaderOptions getDefault() {
return new ZipReaderOptions(new JadxZipSecurity(), ZipReaderFlags.none());
}
private final IJadxZipSecurity zipSecurity;
private final Set<ZipReaderFlags> flags;
public ZipReaderOptions(IJadxZipSecurity zipSecurity, Set<ZipReaderFlags> flags) {
this.zipSecurity = zipSecurity;
this.flags = flags;
}
public IJadxZipSecurity getZipSecurity() {
return zipSecurity;
}
public Set<ZipReaderFlags> getFlags() {
return flags;
}
}
@@ -0,0 +1,9 @@
package jadx.zip.fallback;
import java.io.IOException;
public class FallbackException extends IOException {
public FallbackException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,61 @@
package jadx.zip.fallback;
import java.io.File;
import java.io.InputStream;
import java.util.zip.ZipEntry;
import jadx.zip.IZipEntry;
public class FallbackZipEntry implements IZipEntry {
private final FallbackZipParser parser;
private final ZipEntry zipEntry;
public FallbackZipEntry(FallbackZipParser parser, ZipEntry zipEntry) {
this.parser = parser;
this.zipEntry = zipEntry;
}
public ZipEntry getZipEntry() {
return zipEntry;
}
@Override
public String getName() {
return zipEntry.getName();
}
@Override
public boolean preferBytes() {
return false;
}
@Override
public byte[] getBytes() {
return parser.getBytes(this);
}
@Override
public InputStream getInputStream() {
return parser.getInputStream(this);
}
@Override
public long getCompressedSize() {
return zipEntry.getCompressedSize();
}
@Override
public long getUncompressedSize() {
return zipEntry.getSize();
}
@Override
public boolean isDirectory() {
return zipEntry.isDirectory();
}
@Override
public File getZipFile() {
return parser.getZipFile();
}
}
@@ -0,0 +1,111 @@
package jadx.zip.fallback;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.zip.IZipEntry;
import jadx.zip.IZipParser;
import jadx.zip.ZipContent;
import jadx.zip.ZipReaderOptions;
import jadx.zip.io.LimitedInputStream;
import jadx.zip.security.IJadxZipSecurity;
public class FallbackZipParser implements IZipParser {
private static final Logger LOG = LoggerFactory.getLogger(FallbackZipParser.class);
private final File file;
private final ZipFile zipFile;
private final IJadxZipSecurity zipSecurity;
private final boolean useLimitedDataStream;
public FallbackZipParser(File file, ZipReaderOptions options) throws FallbackException {
try {
this.file = file;
this.zipFile = new ZipFile(file);
this.zipSecurity = options.getZipSecurity();
this.useLimitedDataStream = zipSecurity.useLimitedDataStream();
} catch (Exception e) {
throw new FallbackException("Error opening zip file: " + file.getAbsolutePath(), e);
}
}
@Override
public ZipContent open() throws IOException {
try {
int maxEntriesCount = zipSecurity.getMaxEntriesCount();
if (maxEntriesCount == -1) {
maxEntriesCount = Integer.MAX_VALUE;
}
List<IZipEntry> list = new ArrayList<>();
Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
FallbackZipEntry zipEntry = new FallbackZipEntry(this, entries.nextElement());
if (isValidEntry(zipEntry)) {
list.add(zipEntry);
if (list.size() > maxEntriesCount) {
throw new IllegalStateException("Max entries count limit exceeded: " + list.size());
}
}
}
return new ZipContent(this, list);
} catch (Exception e) {
throw new FallbackException("Error opening zip file: " + file.getAbsolutePath(), e);
}
}
private boolean isValidEntry(IZipEntry zipEntry) {
boolean validEntry = zipSecurity.isValidEntry(zipEntry);
if (!validEntry) {
LOG.warn("Zip entry '{}' is invalid and excluded from processing", zipEntry);
}
return validEntry;
}
public byte[] getBytes(FallbackZipEntry entry) {
try (InputStream is = getEntryStream(entry)) {
return is.readAllBytes();
} catch (Exception e) {
throw new RuntimeException("Failed to read bytes for entry: " + entry.getName(), e);
}
}
public InputStream getInputStream(FallbackZipEntry entry) {
try {
return getEntryStream(entry);
} catch (Exception e) {
throw new RuntimeException("Failed to open input stream for entry: " + entry.getName(), e);
}
}
private InputStream getEntryStream(FallbackZipEntry entry) throws IOException {
InputStream entryStream = zipFile.getInputStream(entry.getZipEntry());
InputStream stream;
if (useLimitedDataStream) {
stream = new LimitedInputStream(entryStream, entry.getUncompressedSize());
} else {
stream = entryStream;
}
return new BufferedInputStream(stream);
}
public File getZipFile() {
return file;
}
@Override
public void close() throws IOException {
if (zipFile != null) {
zipFile.close();
}
}
}
@@ -0,0 +1,48 @@
package jadx.zip.io;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
public class ByteBufferBackedInputStream extends InputStream {
private final ByteBuffer buf;
private int markedPosition = 0;
public ByteBufferBackedInputStream(ByteBuffer buf) {
this.buf = buf;
}
@Override
public int read() throws IOException {
if (!buf.hasRemaining()) {
return -1;
}
return buf.get() & 0xFF;
}
@Override
@SuppressWarnings("NullableProblems")
public int read(byte[] bytes, int off, int len) throws IOException {
if (!buf.hasRemaining()) {
return -1;
}
int readLen = Math.min(len, buf.remaining());
buf.get(bytes, off, readLen);
return readLen;
}
@Override
public boolean markSupported() {
return true;
}
@Override
public synchronized void mark(int unused) {
markedPosition = buf.position();
}
@Override
public synchronized void reset() {
buf.position(markedPosition);
}
}
@@ -1,21 +1,22 @@
package jadx.api.plugins.utils;
package jadx.zip.io;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
public class LimitedInputStream extends FilterInputStream {
private final long maxSize;
private long currentPos;
private long markPos;
protected LimitedInputStream(InputStream in, long maxSize) {
public LimitedInputStream(InputStream in, long maxSize) {
super(in);
this.maxSize = maxSize;
}
private void checkPos() {
private void addAndCheckPos(long count) {
currentPos += count;
if (currentPos > maxSize) {
throw new IllegalStateException("Read limit exceeded");
}
@@ -25,18 +26,17 @@ public class LimitedInputStream extends FilterInputStream {
public int read() throws IOException {
int data = super.read();
if (data != -1) {
currentPos++;
checkPos();
addAndCheckPos(1);
}
return data;
}
@SuppressWarnings("NullableProblems")
@Override
public int read(byte[] b, int off, int len) throws IOException {
int count = super.read(b, off, len);
if (count > 0) {
currentPos += count;
checkPos();
addAndCheckPos(count);
}
return count;
}
@@ -44,10 +44,21 @@ public class LimitedInputStream extends FilterInputStream {
@Override
public long skip(long n) throws IOException {
long skipped = super.skip(n);
if (skipped != 0) {
currentPos += skipped;
checkPos();
if (skipped > 0) {
addAndCheckPos(skipped);
}
return skipped;
}
@Override
public void mark(int readLimit) {
super.mark(readLimit);
markPos = currentPos;
}
@Override
public void reset() throws IOException {
super.reset();
currentPos = markPos;
}
}
@@ -0,0 +1,94 @@
package jadx.zip.parser;
import java.io.File;
import java.io.InputStream;
import jadx.zip.IZipEntry;
public final class JadxZipEntry implements IZipEntry {
private final JadxZipParser parser;
private final String fileName;
private final int compressMethod;
private final int entryStart;
private final int dataStart;
private final long compressedSize;
private final long uncompressedSize;
JadxZipEntry(JadxZipParser parser, String fileName, int entryStart, int dataStart,
int compressMethod, long compressedSize, long uncompressedSize) {
this.parser = parser;
this.fileName = fileName;
this.entryStart = entryStart;
this.dataStart = dataStart;
this.compressMethod = compressMethod;
this.compressedSize = compressedSize;
this.uncompressedSize = uncompressedSize;
}
public boolean isSizesValid() {
if (compressedSize <= 0) {
return false;
}
if (uncompressedSize <= 0) {
return false;
}
return compressedSize <= uncompressedSize;
}
@Override
public String getName() {
return fileName;
}
@Override
public long getCompressedSize() {
return compressedSize;
}
@Override
public long getUncompressedSize() {
return uncompressedSize;
}
@Override
public boolean isDirectory() {
return fileName.endsWith("/");
}
@Override
public boolean preferBytes() {
return true;
}
@Override
public byte[] getBytes() {
return parser.getBytes(this);
}
@Override
public InputStream getInputStream() {
return parser.getInputStream(this);
}
public int getEntryStart() {
return entryStart;
}
public int getDataStart() {
return dataStart;
}
public int getCompressMethod() {
return compressMethod;
}
@Override
public File getZipFile() {
return parser.getZipFile();
}
@Override
public String toString() {
return parser.getZipFile().getName() + ':' + fileName;
}
}
@@ -0,0 +1,450 @@
package jadx.zip.parser;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.zip.IZipEntry;
import jadx.zip.IZipParser;
import jadx.zip.ZipContent;
import jadx.zip.ZipReaderFlags;
import jadx.zip.ZipReaderOptions;
import jadx.zip.fallback.FallbackZipParser;
import jadx.zip.io.ByteBufferBackedInputStream;
import jadx.zip.io.LimitedInputStream;
import jadx.zip.security.IJadxZipSecurity;
/**
* Custom and simple zip parser to fight tampering.
* Many zip features aren't supported:
* - Compression methods other than STORE or DEFLATE
* - Zip64
* - Checksum verification
* - Multi file archives
*/
public final class JadxZipParser implements IZipParser {
private static final Logger LOG = LoggerFactory.getLogger(JadxZipParser.class);
private static final byte LOCAL_FILE_HEADER_START = 0x50;
private static final int LOCAL_FILE_HEADER_SIGN = 0x04034b50;
private static final int CD_SIGN = 0x02014b50;
private static final int END_OF_CD_SIGN = 0x06054b50;
private final File zipFile;
private final ZipReaderOptions options;
private final IJadxZipSecurity zipSecurity;
private final Set<ZipReaderFlags> flags;
private final boolean verify;
private final boolean useLimitedDataStream;
private @Nullable RandomAccessFile file;
private @Nullable FileChannel fileChannel;
private @Nullable ByteBuffer byteBuffer;
private int endOfCDStart = -2;
private @Nullable ZipContent fallbackZipContent;
public JadxZipParser(File zipFile, ZipReaderOptions options) {
this.zipFile = zipFile;
this.options = options;
this.zipSecurity = options.getZipSecurity();
this.flags = options.getFlags();
this.verify = options.getFlags().contains(ZipReaderFlags.REPORT_TAMPERING);
this.useLimitedDataStream = zipSecurity.useLimitedDataStream();
}
@Override
public ZipContent open() throws IOException {
load();
try {
int maxEntriesCount = zipSecurity.getMaxEntriesCount();
if (maxEntriesCount == -1) {
maxEntriesCount = Integer.MAX_VALUE;
}
List<IZipEntry> entries;
if (flags.contains(ZipReaderFlags.IGNORE_CENTRAL_DIR_ENTRIES)) {
entries = searchLocalFileHeaders(maxEntriesCount);
} else {
entries = loadFromCentralDirs(maxEntriesCount);
}
return new ZipContent(this, entries);
} catch (Exception e) {
if (flags.contains(ZipReaderFlags.DONT_USE_FALLBACK)) {
throw new IOException("Failed to open zip: " + zipFile + ", error: " + e.getMessage(), e);
}
LOG.warn("Zip open failed, switching to fallback parser, zip: {}", zipFile, e);
return initFallbackParser();
}
}
public boolean canOpen() {
try {
load();
int eocdStart = searchEndOfCDStart();
ByteBuffer buf = getBuffer();
buf.position(eocdStart + 4);
int diskNum = readU2(buf);
if (diskNum != 0xFFFF) { // Zip64 not supported
return true;
}
} catch (Exception e) {
LOG.warn("Jadx parser can't open zip file: {}", zipFile, e);
}
try {
close();
} catch (Exception e) {
LOG.warn("Failed to close jadx parser, zip file: {}", zipFile, e);
}
return false;
}
private boolean isValidEntry(JadxZipEntry zipEntry) {
boolean validEntry = zipSecurity.isValidEntry(zipEntry);
if (!validEntry) {
LOG.warn("Zip entry '{}' is invalid and excluded from processing", zipEntry);
}
return validEntry;
}
private ByteBuffer getBuffer() {
ByteBuffer buf = byteBuffer;
if (buf == null) {
throw new RuntimeException("File not opened: " + zipFile);
}
return buf;
}
private void load() throws IOException {
if (byteBuffer != null) {
// already loaded
return;
}
RandomAccessFile raFile = new RandomAccessFile(zipFile, "r");
long size = raFile.length();
if (size >= Integer.MAX_VALUE) {
throw new IOException("Zip file is too big");
}
int fileLen = (int) size;
if (fileLen < 100 * 1024 * 1024) {
// load files smaller than 100MB directly into memory
byte[] bytes = new byte[fileLen];
raFile.readFully(bytes);
byteBuffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
raFile.close();
} else {
// for big files - use a memory mapped file
file = raFile;
fileChannel = raFile.getChannel();
byteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
}
}
private List<IZipEntry> searchLocalFileHeaders(int maxEntriesCount) {
List<IZipEntry> entries = new ArrayList<>();
while (true) {
int start = searchEntryStart();
if (start == -1) {
return entries;
}
JadxZipEntry zipEntry = loadFileEntry(start);
if (isValidEntry(zipEntry)) {
entries.add(zipEntry);
if (entries.size() > maxEntriesCount) {
throw new IllegalStateException("Max entries count limit exceeded: " + entries.size());
}
}
}
}
private List<IZipEntry> loadFromCentralDirs(int maxEntriesCount) throws IOException {
int eocdStart = searchEndOfCDStart();
if (eocdStart < 0) {
throw new RuntimeException("End of central directory not found");
}
ByteBuffer buf = getBuffer();
buf.position(eocdStart + 10);
int entriesCount = readU2(buf);
buf.position(eocdStart + 16);
int cdOffset = buf.getInt();
if (entriesCount > maxEntriesCount) {
throw new IllegalStateException("Max entries count limit exceeded: " + entriesCount);
}
List<IZipEntry> entries = new ArrayList<>(entriesCount);
buf.position(cdOffset);
for (int i = 0; i < entriesCount; i++) {
JadxZipEntry zipEntry = loadCDEntry();
if (isValidEntry(zipEntry)) {
entries.add(zipEntry);
}
}
return entries;
}
private JadxZipEntry loadCDEntry() {
ByteBuffer buf = getBuffer();
int start = buf.position();
buf.position(start + 28);
int fileNameLen = readU2(buf);
int extraFieldLen = readU2(buf);
int commentLen = readU2(buf);
buf.position(start + 42);
int fileEntryStart = buf.getInt();
int entryEnd = start + 46 + fileNameLen + extraFieldLen + commentLen;
JadxZipEntry entry = loadFileEntry(fileEntryStart);
if (verify) {
compareCDAndLFH(buf, start, entry);
}
if (!entry.isSizesValid()) {
entry = fixEntryFromCD(entry, start);
}
buf.position(entryEnd);
return entry;
}
private JadxZipEntry fixEntryFromCD(JadxZipEntry entry, int start) {
ByteBuffer buf = getBuffer();
buf.position(start + 10);
int comprMethod = readU2(buf);
buf.position(start + 20);
int comprSize = buf.getInt();
int unComprSize = buf.getInt();
return new JadxZipEntry(this, entry.getName(), start, entry.getDataStart(), comprMethod, comprSize, unComprSize);
}
private static void compareCDAndLFH(ByteBuffer buf, int start, JadxZipEntry entry) {
buf.position(start + 10);
int comprMethod = readU2(buf);
if (comprMethod != entry.getCompressMethod()) {
LOG.warn("Compression method differ in CD {} and LFH {} for {}",
comprMethod, entry.getCompressMethod(), entry);
}
buf.position(start + 20);
int comprSize = buf.getInt();
int unComprSize = buf.getInt();
if (comprSize != entry.getCompressedSize()) {
LOG.warn("Compressed size differ in CD {} and LFH {} for {}",
comprSize, entry.getCompressedSize(), entry);
}
if (unComprSize != entry.getUncompressedSize()) {
LOG.warn("Uncompressed size differ in CD {} and LFH {} for {}",
unComprSize, entry.getUncompressedSize(), entry);
}
}
private JadxZipEntry loadFileEntry(int start) {
ByteBuffer buf = getBuffer();
buf.position(start + 8);
int comprMethod = readU2(buf);
buf.position(start + 18);
int comprSize = buf.getInt();
int unComprSize = buf.getInt();
int fileNameLen = readU2(buf);
int extraFieldLen = readU2(buf);
String fileName = readString(buf, fileNameLen);
int dataStart = start + 30 + fileNameLen + extraFieldLen;
buf.position(dataStart + comprSize);
return new JadxZipEntry(this, fileName, start, dataStart, comprMethod, comprSize, unComprSize);
}
private int searchEndOfCDStart() throws IOException {
if (endOfCDStart != -2) {
return endOfCDStart;
}
ByteBuffer buf = getBuffer();
int pos = buf.limit() - 22;
int minPos = Math.max(0, pos - 0xffff);
while (true) {
buf.position(pos);
int sign = buf.getInt();
if (sign == END_OF_CD_SIGN) {
endOfCDStart = pos;
return pos;
}
pos--;
if (pos < minPos) {
throw new IOException("End of central directory record not found");
}
}
}
private int searchEntryStart() {
ByteBuffer buf = getBuffer();
while (true) {
int start = buf.position();
if (start + 4 > buf.limit()) {
return -1;
}
byte b = buf.get();
if (b == LOCAL_FILE_HEADER_START) {
buf.position(start);
int sign = buf.getInt();
if (sign == LOCAL_FILE_HEADER_SIGN) {
return start;
}
}
}
}
synchronized InputStream getInputStream(JadxZipEntry entry) {
if (verify) {
verifyEntry(entry);
}
InputStream stream;
if (entry.getCompressMethod() == 8) {
try {
stream = ZipDeflate.decompressEntryToStream(getBuffer(), entry);
} catch (Exception e) {
entryParseFailed(entry, e);
return useFallbackParser(entry).getInputStream();
}
} else {
// treat any other compression methods values as UNCOMPRESSED
stream = bufferToStream(getBuffer(), entry.getDataStart(), (int) entry.getUncompressedSize());
}
if (useLimitedDataStream) {
return new LimitedInputStream(stream, entry.getUncompressedSize());
}
return stream;
}
synchronized byte[] getBytes(JadxZipEntry entry) {
if (verify) {
verifyEntry(entry);
}
if (entry.getCompressMethod() == 8) {
try {
return ZipDeflate.decompressEntryToBytes(getBuffer(), entry);
} catch (Exception e) {
entryParseFailed(entry, e);
return useFallbackParser(entry).getBytes();
}
}
// treat any other compression methods values as UNCOMPRESSED
return bufferToBytes(getBuffer(), entry.getDataStart(), (int) entry.getUncompressedSize());
}
private static void verifyEntry(JadxZipEntry entry) {
int compressMethod = entry.getCompressMethod();
if (compressMethod == 0) {
if (entry.getCompressedSize() != entry.getUncompressedSize()) {
LOG.warn("Not equal sizes for STORE method: compressed: {}, uncompressed: {}, entry: {}",
entry.getCompressedSize(), entry.getUncompressedSize(), entry);
}
} else if (compressMethod != 8) {
LOG.warn("Unknown compress method: {} in entry: {}", compressMethod, entry);
}
}
private void entryParseFailed(JadxZipEntry entry, Exception e) {
if (isEncrypted(entry)) {
throw new RuntimeException("Entry is encrypted, failed to decompress: " + entry, e);
}
if (flags.contains(ZipReaderFlags.DONT_USE_FALLBACK)) {
throw new RuntimeException("Failed to decompress zip entry: " + entry + ", error: " + e.getMessage(), e);
}
LOG.warn("Entry '{}' parse failed, switching to fallback parser", entry, e);
}
@SuppressWarnings("resource")
private IZipEntry useFallbackParser(JadxZipEntry entry) {
LOG.debug("useFallbackParser used for {}", entry);
IZipEntry zipEntry = initFallbackParser().searchEntry(entry.getName());
if (zipEntry == null) {
throw new RuntimeException("Fallback parser can't find entry: " + entry);
}
return zipEntry;
}
@SuppressWarnings("resource")
private synchronized ZipContent initFallbackParser() {
if (fallbackZipContent == null) {
try {
fallbackZipContent = new FallbackZipParser(zipFile, options).open();
} catch (Exception e) {
throw new RuntimeException("Fallback parser failed to open file: " + zipFile, e);
}
}
return fallbackZipContent;
}
private boolean isEncrypted(JadxZipEntry entry) {
int flags = readFlags(entry);
return (flags & 1) != 0;
}
private int readFlags(JadxZipEntry entry) {
ByteBuffer buf = getBuffer();
buf.position(entry.getEntryStart() + 6);
return readU2(buf);
}
static byte[] bufferToBytes(ByteBuffer buf, int start, int size) {
byte[] data = new byte[size];
buf.position(start);
buf.get(data);
return data;
}
static InputStream bufferToStream(ByteBuffer buf, int start, int size) {
buf.position(start);
ByteBuffer streamBuf = buf.slice();
streamBuf.limit(size);
return new ByteBufferBackedInputStream(streamBuf);
}
private static int readU2(ByteBuffer buf) {
return buf.getShort() & 0xFFFF;
}
private static String readString(ByteBuffer buf, int fileNameLen) {
byte[] bytes = new byte[fileNameLen];
buf.get(bytes);
return new String(bytes, StandardCharsets.UTF_8);
}
@SuppressWarnings("DataFlowIssue")
@Override
public void close() throws IOException {
try {
if (fileChannel != null) {
fileChannel.close();
}
if (file != null) {
file.close();
}
if (fallbackZipContent != null) {
fallbackZipContent.close();
}
} finally {
fileChannel = null;
file = null;
byteBuffer = null;
endOfCDStart = -2;
fallbackZipContent = null;
}
}
public File getZipFile() {
return zipFile;
}
@Override
public String toString() {
return "JadxZipParser{" + zipFile + '}';
}
}
@@ -0,0 +1,38 @@
package jadx.zip.parser;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
import static jadx.zip.parser.JadxZipParser.bufferToStream;
final class ZipDeflate {
private static final int BUFFER_SIZE = 4096;
static byte[] decompressEntryToBytes(ByteBuffer buf, JadxZipEntry entry) throws DataFormatException {
buf.position(entry.getDataStart());
ByteBuffer entryBuf = buf.slice();
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()];
Inflater inflater = new Inflater(true);
inflater.setInput(entryBuf);
int written = inflater.inflate(out);
inflater.end();
if (written != out.length) {
throw new DataFormatException("Unexpected size of decompressed entry: " + entry
+ ", got: " + written + ", expected: " + out.length);
}
return out;
}
static InputStream decompressEntryToStream(ByteBuffer buf, JadxZipEntry entry) {
InputStream stream = bufferToStream(buf, entry.getDataStart(), (int) entry.getCompressedSize());
Inflater inflater = new Inflater(true);
return new InflaterInputStream(stream, inflater, BUFFER_SIZE);
}
}
@@ -0,0 +1,35 @@
package jadx.zip.security;
import java.io.File;
import jadx.zip.IZipEntry;
public class DisabledZipSecurity implements IJadxZipSecurity {
public static final DisabledZipSecurity INSTANCE = new DisabledZipSecurity();
@Override
public boolean isValidEntry(IZipEntry entry) {
return true;
}
@Override
public boolean isValidEntryName(String entryName) {
return true;
}
@Override
public boolean isInSubDirectory(File baseDir, File file) {
return true;
}
@Override
public boolean useLimitedDataStream() {
return false;
}
@Override
public int getMaxEntriesCount() {
return -1;
}
}
@@ -0,0 +1,35 @@
package jadx.zip.security;
import java.io.File;
import jadx.zip.IZipEntry;
public interface IJadxZipSecurity {
/**
* Check if zip entry is valid and safe to process
*/
boolean isValidEntry(IZipEntry entry);
/**
* Check if the zip entry name is valid.
* This check should be part of {@link #isValidEntry(IZipEntry)} method.
*/
boolean isValidEntryName(String entryName);
/**
* Use limited InputStream for entry uncompressed data
*/
boolean useLimitedDataStream();
/**
* Max entries count expected in a zip file, fail zip open if the limit exceeds.
* Return -1 to disable entries count check.
*/
int getMaxEntriesCount();
/**
* Check if a file will be inside baseDir after a system resolves its path
*/
boolean isInSubDirectory(File baseDir, File file);
}
@@ -0,0 +1,129 @@
package jadx.zip.security;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.zip.IZipEntry;
public class JadxZipSecurity implements IJadxZipSecurity {
private static final Logger LOG = LoggerFactory.getLogger(JadxZipSecurity.class);
private static final Path CWD = Paths.get(".").toAbsolutePath().normalize();
/**
* The size of uncompressed zip entry shouldn't be bigger of compressed in zipBombDetectionFactor
* times
*/
private int zipBombDetectionFactor = 100;
/**
* Zip entries that have an uncompressed size of less than zipBombMinUncompressedSize are considered
* safe
*/
private int zipBombMinUncompressedSize = 25 * 1024 * 1024;
private int maxEntriesCount = 100_000;
private boolean useLimitedDataStream = true;
@Override
public boolean isValidEntry(IZipEntry entry) {
return isValidEntryName(entry.getName()) && !isZipBomb(entry);
}
@Override
public boolean useLimitedDataStream() {
return useLimitedDataStream;
}
@Override
public int getMaxEntriesCount() {
return maxEntriesCount;
}
/**
* Checks that entry name contains no any traversals and prevents cases like "../classes.dex",
* to limit output only to the specified directory
*/
@Override
public boolean isValidEntryName(String entryName) {
if (entryName.contains("..")) { // quick pre-check
if (entryName.contains("../") || entryName.contains("..\\")) {
LOG.error("Path traversal attack detected in entry: '{}'", entryName);
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 {
Path entryPath = CWD.resolve(entryName).normalize();
if (entryPath.startsWith(CWD)) {
return true;
}
} catch (Exception e) {
// 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);
return false;
}
@Override
public boolean isInSubDirectory(File baseDir, File file) {
try {
return isInSubDirectoryInternal(baseDir.getCanonicalFile(), file.getCanonicalFile());
} catch (IOException e) {
return false;
}
}
public boolean isZipBomb(IZipEntry entry) {
long compressedSize = entry.getCompressedSize();
long uncompressedSize = entry.getUncompressedSize();
boolean invalidSize = compressedSize < 0 || uncompressedSize < 0;
boolean possibleZipBomb = uncompressedSize >= zipBombMinUncompressedSize
&& compressedSize * zipBombDetectionFactor < uncompressedSize;
if (invalidSize || possibleZipBomb) {
LOG.error("Potential zip bomb attack detected, invalid sizes: compressed {}, uncompressed {}, name {}",
compressedSize, uncompressedSize, entry.getName());
return true;
}
return false;
}
private static boolean isInSubDirectoryInternal(File baseDir, File file) {
File current = file;
while (true) {
if (current == null) {
return false;
}
if (current.equals(baseDir)) {
return true;
}
current = current.getParentFile();
}
}
public void setMaxEntriesCount(int maxEntriesCount) {
this.maxEntriesCount = maxEntriesCount;
}
public void setZipBombDetectionFactor(int zipBombDetectionFactor) {
this.zipBombDetectionFactor = zipBombDetectionFactor;
}
public void setZipBombMinUncompressedSize(int zipBombMinUncompressedSize) {
this.zipBombMinUncompressedSize = zipBombMinUncompressedSize;
}
public void setUseLimitedDataStream(boolean useLimitedDataStream) {
this.useLimitedDataStream = useLimitedDataStream;
}
}
+16 -8
View File
@@ -4,16 +4,19 @@ plugins {
dependencies {
api(project(":jadx-plugins:jadx-input-api"))
api(project(":jadx-commons:jadx-zip"))
implementation("com.google.code.gson:gson:2.11.0")
implementation("com.google.code.gson:gson:2.14.0")
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"))
testRuntimeOnly(project(":jadx-plugins:jadx-smali-input"))
testRuntimeOnly(project(":jadx-plugins:jadx-java-convert"))
testRuntimeOnly(project(":jadx-plugins:jadx-java-input"))
testRuntimeOnly(project(":jadx-plugins:jadx-raung-input"))
// 'ClassNotFound' error is raised if set as 'testRuntime'
// for the plugins below when running the tests from vscode.
testImplementation(project(":jadx-plugins:jadx-smali-input"))
testImplementation(project(":jadx-plugins:jadx-java-convert"))
testImplementation(project(":jadx-plugins:jadx-java-input"))
testImplementation(project(":jadx-plugins:jadx-raung-input"))
testImplementation("org.eclipse.jdt:ecj") {
version {
@@ -21,7 +24,7 @@ dependencies {
strictly("[3.33, 3.34[") // from 3.34 compiled with Java 17
}
}
testImplementation("tools.profiler:async-profiler:3.0")
testImplementation("tools.profiler:async-profiler:4.4")
}
val jadxTestJavaVersion = getTestJavaVersion()
@@ -29,7 +32,10 @@ val jadxTestJavaVersion = getTestJavaVersion()
fun getTestJavaVersion(): Int? {
val envVarName = "JADX_TEST_JAVA_VERSION"
val testJavaVer = System.getenv(envVarName)?.toInt() ?: return null
val currentJavaVer = java.toolchain.languageVersion.get().asInt()
val currentJavaVer =
java.toolchain.languageVersion
.get()
.asInt()
if (testJavaVer < currentJavaVer) {
throw GradleException("'$envVarName' can't be set to lower version than $currentJavaVer")
}
@@ -51,4 +57,6 @@ tasks.named<Test>("test") {
// exclude temp tests
exclude("**/tmp/*")
// maxHeapSize = "4g"
}
@@ -19,5 +19,18 @@ public enum DecompilationMode {
/**
* Raw instructions without modifications
*/
FALLBACK
FALLBACK;
public boolean isSpecial() {
switch (this) {
case AUTO:
case RESTRUCTURE:
return false;
case SIMPLE:
case FALLBACK:
return true;
default:
throw new RuntimeException("Unexpected decompilation mode: " + this);
}
}
}
+73 -15
View File
@@ -4,7 +4,6 @@ import java.io.Closeable;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
@@ -39,6 +38,7 @@ import jadx.api.usage.impl.InMemoryUsageInfoCache;
import jadx.core.deobf.DeobfAliasProvider;
import jadx.core.deobf.conditions.DeobfWhitelist;
import jadx.core.deobf.conditions.JadxRenameConditions;
import jadx.core.export.ExportGradleType;
import jadx.core.plugins.PluginContext;
import jadx.core.plugins.files.IJadxFilesGetter;
import jadx.core.plugins.files.TempFilesGetter;
@@ -90,11 +90,12 @@ public class JadxArgs implements Closeable {
private boolean skipResources = false;
private boolean skipSources = false;
private boolean useHeadersForDetectResourceExtensions;
/**
* Predicate that allows to filter the classes to be process based on their full name
*/
private Predicate<String> classFilter = null;
private @Nullable Predicate<String> classFilter = null;
/**
* Save dependencies for classes accepted by {@code classFilter}
@@ -106,6 +107,7 @@ public class JadxArgs implements Closeable {
private boolean deobfuscationOn = false;
private UseSourceNameAsClassNameAlias useSourceNameAsClassNameAlias = UseSourceNameAsClassNameAlias.getDefault();
private int sourceNameRepeatLimit = 10;
private File generatedRenamesMappingFile = null;
private GeneratedRenamesMappingFileMode generatedRenamesMappingFileMode = GeneratedRenamesMappingFileMode.getDefault();
@@ -132,7 +134,7 @@ public class JadxArgs implements Closeable {
private boolean escapeUnicode = false;
private boolean replaceConsts = true;
private boolean respectBytecodeAccModifiers = false;
private boolean exportAsGradleProject = false;
private @Nullable ExportGradleType exportGradleType = null;
private boolean restoreSwitchOverString = true;
@@ -164,6 +166,12 @@ public class JadxArgs implements Closeable {
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;
public enum UseKotlinMethodsForVarNames {
@@ -193,6 +201,11 @@ public class JadxArgs implements Closeable {
*/
private boolean runDebugChecks = false;
/**
* Passes to exclude from processing.
*/
private final List<String> disabledPasses = new ArrayList<>();
private Map<String, String> pluginOptions = new HashMap<>();
private Set<String> disabledPlugins = new HashSet<>();
@@ -214,7 +227,6 @@ public class JadxArgs implements Closeable {
@Override
public void close() {
try {
inputFiles = null;
if (codeCache != null) {
codeCache.close();
}
@@ -226,9 +238,6 @@ public class JadxArgs implements Closeable {
}
} catch (Exception e) {
LOG.error("Failed to close JadxArgs", e);
} finally {
codeCache = null;
usageInfoCache = null;
}
}
@@ -236,8 +245,12 @@ public class JadxArgs implements Closeable {
return inputFiles;
}
public void addInputFile(File inputFile) {
this.inputFiles.add(inputFile);
}
public void setInputFile(File inputFile) {
this.inputFiles = Collections.singletonList(inputFile);
addInputFile(inputFile);
}
public void setInputFiles(List<File> inputFiles) {
@@ -460,6 +473,14 @@ public class JadxArgs implements Closeable {
this.useSourceNameAsClassNameAlias = useSourceNameAsClassNameAlias;
}
public int getSourceNameRepeatLimit() {
return sourceNameRepeatLimit;
}
public void setSourceNameRepeatLimit(int sourceNameRepeatLimit) {
this.sourceNameRepeatLimit = sourceNameRepeatLimit;
}
/**
* @deprecated Use {@link #getUseSourceNameAsClassNameAlias()} instead.
*/
@@ -473,7 +494,7 @@ public class JadxArgs implements Closeable {
*/
@Deprecated
public void setUseSourceNameAsClassAlias(boolean useSourceNameAsClassAlias) {
final var useSourceNameAsClassNameAlias = UseSourceNameAsClassNameAlias.create(useSourceNameAsClassAlias);
var useSourceNameAsClassNameAlias = UseSourceNameAsClassNameAlias.create(useSourceNameAsClassAlias);
setUseSourceNameAsClassNameAlias(useSourceNameAsClassNameAlias);
}
@@ -558,11 +579,25 @@ public class JadxArgs implements Closeable {
}
public boolean isExportAsGradleProject() {
return exportAsGradleProject;
return exportGradleType != null;
}
public void setExportAsGradleProject(boolean exportAsGradleProject) {
this.exportAsGradleProject = exportAsGradleProject;
if (exportAsGradleProject) {
if (exportGradleType == null) {
exportGradleType = ExportGradleType.AUTO;
}
} else {
exportGradleType = null;
}
}
public @Nullable ExportGradleType getExportGradleType() {
return exportGradleType;
}
public void setExportGradleType(@Nullable ExportGradleType exportGradleType) {
this.exportGradleType = exportGradleType;
}
public boolean isRestoreSwitchOverString() {
@@ -713,6 +748,14 @@ public class JadxArgs implements Closeable {
this.integerFormat = format;
}
public int getTypeUpdatesLimitCount() {
return typeUpdatesLimitCount;
}
public void setTypeUpdatesLimitCount(int typeUpdatesLimitCount) {
this.typeUpdatesLimitCount = Math.max(1, typeUpdatesLimitCount);
}
public boolean isUseDxInput() {
return useDxInput;
}
@@ -761,6 +804,10 @@ public class JadxArgs implements Closeable {
this.runDebugChecks = runDebugChecks;
}
public List<String> getDisabledPasses() {
return disabledPasses;
}
public Map<String, String> getPluginOptions() {
return pluginOptions;
}
@@ -793,6 +840,14 @@ public class JadxArgs implements Closeable {
this.loadJadxClsSetFile = loadJadxClsSetFile;
}
public void setUseHeadersForDetectResourceExtensions(boolean useHeadersForDetectResourceExtensions) {
this.useHeadersForDetectResourceExtensions = useHeadersForDetectResourceExtensions;
}
public boolean isUseHeadersForDetectResourceExtensions() {
return useHeadersForDetectResourceExtensions;
}
/**
* Hash of all options that can change result code
*/
@@ -800,13 +855,13 @@ public class JadxArgs implements Closeable {
String argStr = "args:" + decompilationMode + useImports + showInconsistentCode
+ inlineAnonymousClasses + inlineMethods + moveInnerClasses + allowInlineKotlinLambda
+ deobfuscationOn + deobfuscationMinLength + deobfuscationMaxLength + deobfuscationWhitelist
+ useSourceNameAsClassNameAlias
+ resourceNameSource
+ useSourceNameAsClassNameAlias + sourceNameRepeatLimit
+ resourceNameSource + useHeadersForDetectResourceExtensions
+ useKotlinMethodsForVarNames
+ insertDebugLines + extractFinally
+ debugInfo + escapeUnicode + replaceConsts + restoreSwitchOverString
+ respectBytecodeAccModifiers + fsCaseSensitive + renameFlags
+ commentsLevel + useDxInput + integerFormat
+ commentsLevel + useDxInput + integerFormat + typeUpdatesLimitCount
+ "|" + buildPluginsHash(decompiler);
return FileUtils.md5Sum(argStr);
}
@@ -841,6 +896,7 @@ public class JadxArgs implements Closeable {
+ ", generatedRenamesMappingFileMode=" + generatedRenamesMappingFileMode
+ ", resourceNameSource=" + resourceNameSource
+ ", useSourceNameAsClassNameAlias=" + useSourceNameAsClassNameAlias
+ ", sourceNameRepeatLimit=" + sourceNameRepeatLimit
+ ", useKotlinMethodsForVarNames=" + useKotlinMethodsForVarNames
+ ", insertDebugLines=" + insertDebugLines
+ ", extractFinally=" + extractFinally
@@ -851,7 +907,7 @@ public class JadxArgs implements Closeable {
+ ", replaceConsts=" + replaceConsts
+ ", restoreSwitchOverString=" + restoreSwitchOverString
+ ", respectBytecodeAccModifiers=" + respectBytecodeAccModifiers
+ ", exportAsGradleProject=" + exportAsGradleProject
+ ", exportGradleType=" + exportGradleType
+ ", skipXmlPrettyPrint=" + skipXmlPrettyPrint
+ ", fsCaseSensitive=" + fsCaseSensitive
+ ", renameFlags=" + renameFlags
@@ -863,6 +919,8 @@ public class JadxArgs implements Closeable {
+ ", pluginOptions=" + pluginOptions
+ ", cfgOutput=" + cfgOutput
+ ", rawCFGOutput=" + rawCFGOutput
+ ", useHeadersForDetectResourceExtensions=" + useHeadersForDetectResourceExtensions
+ ", typeUpdatesLimitCount=" + typeUpdatesLimitCount
+ '}';
}
}
@@ -7,6 +7,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -42,7 +43,8 @@ import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.PackageNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.visitors.SaveCode;
import jadx.core.export.ExportGradleTask;
import jadx.core.export.ExportGradle;
import jadx.core.export.OutDirs;
import jadx.core.plugins.JadxPluginManager;
import jadx.core.plugins.PluginContext;
import jadx.core.plugins.events.JadxEventsImpl;
@@ -50,9 +52,9 @@ import jadx.core.utils.DecompilerScheduler;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
import jadx.core.utils.files.ZipPatch;
import jadx.core.utils.tasks.TaskExecutor;
import jadx.core.xmlgen.ResourcesSaver;
import jadx.zip.ZipReader;
/**
* Jadx API usage example:
@@ -87,6 +89,7 @@ public final class JadxDecompiler implements Closeable {
private final JadxArgs args;
private final JadxPluginManager pluginManager;
private final List<ICodeLoader> loadedInputs = new ArrayList<>();
private final ZipReader zipReader;
private RootNode root;
private List<JavaClass> classes;
@@ -98,6 +101,7 @@ public final class JadxDecompiler implements Closeable {
private final List<ICodeLoader> customCodeLoaders = new ArrayList<>();
private final List<CustomResourcesLoader> customResourcesLoaders = new ArrayList<>();
private final Map<JadxPassType, List<JadxPass>> customPasses = new HashMap<>();
private final List<Closeable> closeableList = new ArrayList<>();
private IJadxEvents events = new JadxEventsImpl();
@@ -109,6 +113,7 @@ public final class JadxDecompiler implements Closeable {
this.args = Objects.requireNonNull(args);
this.pluginManager = new JadxPluginManager(this);
this.resourcesLoader = new ResourcesLoader(this);
this.zipReader = new ZipReader(args.getSecurity());
}
public void load() {
@@ -119,23 +124,30 @@ public final class JadxDecompiler implements Closeable {
loadPlugins();
loadInputFiles();
root = new RootNode(args);
root = new RootNode(this);
root.init();
root.setDecompilerRef(this);
root.mergePasses(customPasses);
// load classes and resources
root.loadClasses(loadedInputs);
root.initClassPath();
root.loadResources(resourcesLoader, getResources());
root.finishClassLoad();
root.initClassPath();
// init passes
root.mergePasses(customPasses);
root.runPreDecompileStage();
root.initPasses();
loadFinished();
}
/**
* Reload passes and plugins without processing classes and inputs
*/
public void reloadPasses() {
LOG.info("reloading (passes only) ...");
customPasses.clear();
root.resetPasses();
events.reset();
unloadPlugins();
loadPlugins();
root.mergePasses(customPasses);
root.restartVisitors();
@@ -145,9 +157,7 @@ public final class JadxDecompiler implements Closeable {
private void loadInputFiles() {
loadedInputs.clear();
List<File> inputs = ZipPatch.patchZipFiles(args.getInputFiles());
args.setInputFiles(inputs);
List<Path> inputPaths = Utils.collectionMap(inputs, File::toPath);
List<Path> inputPaths = Utils.collectionMap(args.getInputFiles(), File::toPath);
List<Path> inputFiles = FileUtils.expandDirs(inputPaths);
long start = System.currentTimeMillis();
for (PluginContext plugin : pluginManager.getResolvedPluginContexts()) {
@@ -158,7 +168,7 @@ public final class JadxDecompiler implements Closeable {
loadedInputs.add(loader);
}
} catch (Exception e) {
throw new JadxRuntimeException("Failed to load code for plugin: " + plugin, e);
LOG.warn("Failed to load code for plugin: {}", plugin, e);
}
}
}
@@ -169,6 +179,7 @@ public final class JadxDecompiler implements Closeable {
}
private void reset() {
unloadPlugins();
root = null;
classes = null;
resources = null;
@@ -178,32 +189,27 @@ public final class JadxDecompiler implements Closeable {
@Override
public void close() {
reset();
closeInputs();
closeLoaders();
closeAll(loadedInputs);
closeAll(customCodeLoaders);
closeAll(customResourcesLoaders);
closeAll(closeableList);
FileUtils.deleteDirIfExists(args.getFilesGetter().getTempDir());
args.close();
FileUtils.clearTempRootDir();
}
private void closeInputs() {
loadedInputs.forEach(load -> {
try {
load.close();
} catch (Exception e) {
LOG.error("Failed to close input", e);
}
});
loadedInputs.clear();
}
private void closeLoaders() {
for (CustomResourcesLoader resourcesLoader : customResourcesLoaders) {
try {
resourcesLoader.close();
} catch (Exception e) {
LOG.error("Failed to close resource loader: {}", resourcesLoader, e);
private void closeAll(List<? extends Closeable> list) {
try {
for (Closeable closeable : list) {
try {
closeable.close();
} catch (Exception e) {
LOG.warn("Fail to close '{}'", closeable, e);
}
}
} finally {
list.clear();
}
customResourcesLoaders.clear();
}
private void loadPlugins() {
@@ -220,6 +226,10 @@ public final class JadxDecompiler implements Closeable {
}
}
private void unloadPlugins() {
pluginManager.unloadResolved();
}
private void loadFinished() {
LOG.debug("Load finished");
List<JadxPass> list = customPasses.get(JadxAfterLoadPass.TYPE);
@@ -297,31 +307,28 @@ public final class JadxDecompiler implements Closeable {
if (root == null) {
throw new JadxRuntimeException("No loaded files");
}
File sourcesOutDir;
File resOutDir;
ExportGradleTask gradleExportTask;
if (args.isExportAsGradleProject()) {
gradleExportTask = new ExportGradleTask(resources, root, args.getOutDir());
gradleExportTask.init();
sourcesOutDir = gradleExportTask.getSrcOutDir();
resOutDir = gradleExportTask.getResOutDir();
OutDirs outDirs;
ExportGradle gradleExport;
if (args.getExportGradleType() != null) {
gradleExport = new ExportGradle(root, args.getOutDir(), getResources());
outDirs = gradleExport.init();
} else {
sourcesOutDir = args.getOutDirSrc();
resOutDir = args.getOutDirRes();
gradleExportTask = null;
gradleExport = null;
outDirs = new OutDirs(args.getOutDirSrc(), args.getOutDirRes());
outDirs.makeDirs();
}
TaskExecutor executor = new TaskExecutor();
executor.setThreadsCount(args.getThreadsCount());
if (saveResources) {
// save resources first because decompilation can stop or fail
appendResourcesSaveTasks(executor, resOutDir);
appendResourcesSaveTasks(executor, outDirs.getResOutDir());
}
if (saveSources) {
appendSourcesSave(executor, sourcesOutDir);
appendSourcesSave(executor, outDirs.getSrcOutDir());
}
if (gradleExportTask != null) {
executor.addSequentialTask(gradleExportTask);
if (gradleExport != null) {
executor.addSequentialTask(gradleExport::generateGradleFiles);
}
return executor;
}
@@ -333,13 +340,15 @@ public final class JadxDecompiler implements Closeable {
// process AndroidManifest.xml first to load complete resource ids table
for (ResourceFile resourceFile : getResources()) {
if (resourceFile.getType() == ResourceType.MANIFEST) {
new ResourcesSaver(outDir, resourceFile).run();
new ResourcesSaver(this, outDir, resourceFile).run();
break;
}
}
Set<String> inputFileNames = args.getInputFiles().stream()
.map(File::getAbsolutePath)
.collect(Collectors.toSet());
Set<String> codeSources = collectCodeSources();
List<Runnable> tasks = new ArrayList<>();
for (ResourceFile resourceFile : getResources()) {
ResourceType resType = resourceFile.getType();
@@ -347,16 +356,44 @@ public final class JadxDecompiler implements Closeable {
// already processed
continue;
}
if (resType != ResourceType.ARSC
&& inputFileNames.contains(resourceFile.getOriginalName())) {
// ignore resource made from input file
String resOriginalName = resourceFile.getOriginalName();
if (resType != ResourceType.ARSC && inputFileNames.contains(resOriginalName)) {
// ignore resource made from an input file
continue;
}
tasks.add(new ResourcesSaver(outDir, resourceFile));
if (codeSources.contains(resOriginalName)) {
// don't output code source resources (.dex, .class, etc)
// do not trust file extensions, use only sources set as class inputs
continue;
}
tasks.add(new ResourcesSaver(this, outDir, resourceFile));
}
executor.addParallelTasks(tasks);
}
private Set<String> collectCodeSources() {
Set<String> set = new HashSet<>();
for (ClassNode cls : root.getClasses(true)) {
if (cls.getClsData() == null) {
// exclude synthetic classes
continue;
}
String inputFileName = cls.getInputFileName();
if (inputFileName.endsWith(".class")) {
// cut .class name to get source .jar file
// current template: "<optional input files>:<.jar>:<full class name>"
// TODO: add property to set file name or reference to resource name
int endIdx = inputFileName.lastIndexOf(':');
if (endIdx != -1) {
int startIdx = inputFileName.lastIndexOf(':', endIdx - 1) + 1;
inputFileName = inputFileName.substring(startIdx, endIdx);
}
}
set.add(inputFileName);
}
return set;
}
private void appendSourcesSave(ITaskExecutor executor, File outDir) {
List<JavaClass> classes = getClasses();
List<JavaClass> processQueue = filterClasses(classes);
@@ -403,7 +440,7 @@ public final class JadxDecompiler implements Closeable {
return list;
}
public List<JavaClass> getClasses() {
public synchronized List<JavaClass> getClasses() {
if (root == null) {
return Collections.emptyList();
}
@@ -411,10 +448,7 @@ public final class JadxDecompiler implements Closeable {
List<ClassNode> classNodeList = root.getClasses();
List<JavaClass> clsList = new ArrayList<>(classNodeList.size());
for (ClassNode classNode : classNodeList) {
if (classNode.contains(AFlag.DONT_GENERATE)) {
continue;
}
if (!classNode.getClassInfo().isInner()) {
if (!classNode.contains(AFlag.DONT_GENERATE) && !classNode.isInner()) {
clsList.add(convertClassNode(classNode));
}
}
@@ -514,9 +548,10 @@ public final class JadxDecompiler implements Closeable {
return foundPkg;
}
List<JavaClass> clsList = Utils.collectionMap(pkg.getClasses(), this::convertClassNode);
List<JavaClass> clsListNoDup = Utils.collectionMap(pkg.getClassesNoDup(), this::convertClassNode);
int subPkgsCount = pkg.getSubPackages().size();
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) {
// add subpackages after parent to avoid endless recursion
for (PackageNode subPackage : pkg.getSubPackages()) {
@@ -587,6 +622,8 @@ public final class JadxDecompiler implements Closeable {
return convertMethodNode((MethodNode) ann);
case FIELD:
return convertFieldNode((FieldNode) ann);
case PKG:
return convertPackageNode((PackageNode) ann);
case DECLARATION:
return getJavaNodeByCodeAnnotation(codeInfo, ((NodeDeclareRef) ann).getNode());
case VAR:
@@ -700,6 +737,14 @@ public final class JadxDecompiler implements Closeable {
return resourcesLoader;
}
public ZipReader getZipReader() {
return zipReader;
}
public void addCloseable(Closeable closeable) {
closeableList.add(closeable);
}
@Override
public String toString() {
return "jadx decompiler " + getVersion();
+30 -16
View File
@@ -6,12 +6,11 @@ import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.metadata.ICodeAnnotation;
import jadx.api.metadata.ICodeNodeRef;
@@ -26,11 +25,9 @@ import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.ListUtils;
public final class JavaClass implements JavaNode {
private static final Logger LOG = LoggerFactory.getLogger(JavaClass.class);
private final JadxDecompiler decompiler;
private final @Nullable JadxDecompiler decompiler;
private final ClassNode cls;
private final JavaClass parent;
private final @Nullable JavaClass parent;
private List<JavaClass> innerClasses = Collections.emptyList();
private List<JavaClass> inlinedClasses = Collections.emptyList();
@@ -38,7 +35,7 @@ public final class JavaClass implements JavaNode {
private List<JavaMethod> methods = Collections.emptyList();
private boolean listsLoaded;
JavaClass(ClassNode classNode, JadxDecompiler decompiler) {
JavaClass(ClassNode classNode, @NotNull JadxDecompiler decompiler) {
this.decompiler = decompiler;
this.cls = classNode;
this.parent = null;
@@ -47,7 +44,7 @@ public final class JavaClass implements JavaNode {
/**
* Inner classes constructor
*/
JavaClass(ClassNode classNode, JavaClass parent) {
JavaClass(ClassNode classNode, @NotNull JavaClass parent) {
this.decompiler = null;
this.cls = classNode;
this.parent = parent;
@@ -69,6 +66,21 @@ public final class JavaClass implements JavaNode {
load();
}
/**
* Detect if calling load() would trigger a potentially expensive decompilation operation.
*/
public boolean loadingWouldRequireDecompilation() {
if (listsLoaded) {
// lists are already populated, so it's safe regardless of the state of the class itself
return false;
}
if (cls.getState().isProcessComplete()) {
// decompilation has already finished
return false;
}
return true;
}
public synchronized ICodeInfo reload() {
listsLoaded = false;
return cls.reloadCode();
@@ -122,8 +134,6 @@ public final class JavaClass implements JavaNode {
if (listsLoaded) {
return null;
}
listsLoaded = true;
ICodeInfo code;
if (cls.getState().isProcessComplete()) {
// already decompiled -> class internals loaded
@@ -131,7 +141,12 @@ public final class JavaClass implements JavaNode {
} else {
code = cls.decompile();
}
loadLists();
return code;
}
private void loadLists() {
listsLoaded = true;
JadxDecompiler rootDecompiler = getRootDecompiler();
int inClsCount = cls.getInnerClasses().size();
if (inClsCount != 0) {
@@ -139,7 +154,7 @@ public final class JavaClass implements JavaNode {
for (ClassNode inner : cls.getInnerClasses()) {
if (!inner.contains(AFlag.DONT_GENERATE)) {
JavaClass javaClass = rootDecompiler.convertClassNode(inner);
javaClass.load();
javaClass.loadLists();
list.add(javaClass);
}
}
@@ -150,7 +165,7 @@ public final class JavaClass implements JavaNode {
List<JavaClass> list = new ArrayList<>(inlinedClsCount);
for (ClassNode inner : cls.getInlinedClasses()) {
JavaClass javaClass = rootDecompiler.convertClassNode(inner);
javaClass.load();
javaClass.loadLists();
list.add(javaClass);
}
this.inlinedClasses = Collections.unmodifiableList(list);
@@ -178,17 +193,16 @@ public final class JavaClass implements JavaNode {
mths.sort(Comparator.comparing(JavaMethod::getName));
this.methods = Collections.unmodifiableList(mths);
}
return code;
}
JadxDecompiler getRootDecompiler() {
if (parent != null) {
return parent.getRootDecompiler();
}
return decompiler;
return Objects.requireNonNull(decompiler);
}
public ICodeAnnotation getAnnotationAt(int pos) {
public @Nullable ICodeAnnotation getAnnotationAt(int pos) {
return getCodeInfo().getCodeMetadata().getAt(pos);
}
@@ -257,7 +271,7 @@ public final class JavaClass implements JavaNode {
}
@Override
public JavaClass getDeclaringClass() {
public @Nullable JavaClass getDeclaringClass() {
return parent;
}
@@ -5,21 +5,18 @@ import java.util.List;
import java.util.stream.Collectors;
import org.jetbrains.annotations.ApiStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.metadata.ICodeAnnotation;
import jadx.api.metadata.ICodeNodeRef;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.MethodOverrideAttr;
import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.Utils;
public final class JavaMethod implements JavaNode {
private static final Logger LOG = LoggerFactory.getLogger(JavaMethod.class);
private final MethodNode mth;
private final JavaClass parent;
@@ -72,6 +69,18 @@ public final class JavaMethod implements JavaNode {
return getDeclaringClass().getRootDecompiler().convertNodes(mth.getUseIn());
}
public List<JavaNode> getUsed() {
return getDeclaringClass().getRootDecompiler().convertNodes(mth.getUsed());
}
public List<MethodInfo> getUnresolvedUsed() {
return mth.getUnresolvedUsed();
}
public boolean callsSelf() {
return mth.callsSelf();
}
public List<JavaMethod> getOverrideRelatedMethods() {
MethodOverrideAttr ovrdAttr = mth.get(AType.METHOD_OVERRIDE);
if (ovrdAttr == null) {
@@ -15,11 +15,17 @@ import jadx.core.dex.nodes.PackageNode;
public final class JavaPackage implements JavaNode, Comparable<JavaPackage> {
private final PackageNode pkgNode;
private final List<JavaClass> classes;
private final List<JavaClass> clsListNoDup;
private final 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.classes = classes;
this.clsListNoDup = clsListNoDup;
this.subPkgs = subPkgs;
}
@@ -49,6 +55,10 @@ public final class JavaPackage implements JavaNode, Comparable<JavaPackage> {
return classes;
}
public List<JavaClass> getClassesNoDup() {
return clsListNoDup;
}
public boolean isRoot() {
return pkgNode.isRoot();
}
@@ -2,39 +2,21 @@ package jadx.api;
import java.io.File;
import jadx.api.plugins.utils.ZipSecurity;
import org.jetbrains.annotations.Nullable;
import jadx.core.deobf.FileTypeDetector;
import jadx.core.utils.StringUtils;
import jadx.core.utils.exceptions.JadxException;
import jadx.core.xmlgen.ResContainer;
import jadx.core.xmlgen.entry.ResourceEntry;
import jadx.zip.IZipEntry;
public class ResourceFile {
public static final class ZipRef {
private final File zipFile;
private final String entryName;
public ZipRef(File zipFile, String entryName) {
this.zipFile = zipFile;
this.entryName = entryName;
}
public File getZipFile() {
return zipFile;
}
public String getEntryName() {
return entryName;
}
@Override
public String toString() {
return "ZipRef{" + zipFile + ", '" + entryName + "'}";
}
}
private final JadxDecompiler decompiler;
private final String name;
private final ResourceType type;
private ZipRef zipRef;
private ResourceType type;
private @Nullable IZipEntry zipEntry;
private String deobfName;
public static ResourceFile createResourceFile(JadxDecompiler decompiler, File file, ResourceType type) {
@@ -42,7 +24,7 @@ public class ResourceFile {
}
public static ResourceFile createResourceFile(JadxDecompiler decompiler, String name, ResourceType type) {
if (!ZipSecurity.isValidZipEntryName(name)) {
if (!decompiler.getArgs().getSecurity().isValidEntryName(name)) {
return null;
}
return new ResourceFile(decompiler, name, type);
@@ -74,28 +56,73 @@ public class ResourceFile {
return ResourcesLoader.loadContent(decompiler, this);
}
void setZipRef(ZipRef zipRef) {
this.zipRef = zipRef;
}
public boolean setAlias(ResourceEntry ri) {
public boolean setAlias(ResourceEntry entry, boolean useHeaders) {
StringBuilder sb = new StringBuilder();
sb.append("res/").append(ri.getTypeName()).append(ri.getConfig());
sb.append("/").append(ri.getKeyName());
int lastDot = name.lastIndexOf('.');
if (lastDot != -1) {
sb.append(name.substring(lastDot));
sb.append("res/").append(entry.getTypeName()).append(entry.getConfig());
sb.append("/").append(entry.getKeyName());
if (useHeaders) {
try {
int maxBytesToReadLimit = 4096;
byte[] bytes = ResourcesLoader.decodeStream(this, (size, is) -> {
int bytesToRead;
if (size > 0) {
bytesToRead = (int) Math.min(size, maxBytesToReadLimit);
} else if (size == 0) {
bytesToRead = 0;
} else {
bytesToRead = maxBytesToReadLimit;
}
if (bytesToRead == 0) {
return new byte[0];
}
return is.readNBytes(bytesToRead);
});
String fileExtension = FileTypeDetector.detectFileExtension(bytes);
if (!StringUtils.isEmpty(fileExtension)) {
sb.append(fileExtension);
} else {
sb.append(getExtFromName(name));
}
} catch (JadxException ignored) {
}
} else {
sb.append(getExtFromName(name));
}
String alias = sb.toString();
if (!alias.equals(name)) {
setDeobfName(alias);
type = ResourceType.getFileType(alias);
return true;
}
return false;
}
public ZipRef getZipRef() {
return zipRef;
private String getExtFromName(String name) {
// the image .9.png extension always saved, when resource shrinking by aapt2
if (name.contains(".9.png")) {
return ".9.png";
}
int lastDot = name.lastIndexOf('.');
if (lastDot != -1) {
return name.substring(lastDot);
}
return "";
}
public @Nullable IZipEntry getZipEntry() {
return zipEntry;
}
void setZipEntry(@Nullable IZipEntry zipEntry) {
this.zipEntry = zipEntry;
}
public JadxDecompiler getDecompiler() {
return decompiler;
}
@Override
@@ -0,0 +1,17 @@
package jadx.api;
import jadx.core.xmlgen.ResContainer;
public class ResourceFileContainer extends ResourceFile {
private final ResContainer container;
public ResourceFileContainer(String name, ResourceType type, ResContainer container) {
super(null, name, type);
this.container = container;
}
@Override
public ResContainer loadContent() {
return container;
}
}
@@ -4,25 +4,44 @@ import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import jadx.api.resources.ResourceContentType;
import jadx.core.utils.exceptions.JadxRuntimeException;
public enum ResourceType {
CODE(".dex", ".jar", ".class"),
XML(".xml"),
ARSC(".arsc"),
FONT(".ttf", ".otf"),
IMG(".png", ".gif", ".jpg"),
MEDIA(".mp3", ".wav"),
LIB(".so"),
MANIFEST,
UNKNOWN;
import static jadx.api.resources.ResourceContentType.CONTENT_BINARY;
import static jadx.api.resources.ResourceContentType.CONTENT_TEXT;
import static jadx.api.resources.ResourceContentType.CONTENT_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;
ResourceType(String... exts) {
ResourceType(ResourceContentType contentType, String... exts) {
this.contentType = contentType;
this.exts = exts;
}
public ResourceContentType getContentType() {
return contentType;
}
public String[] getExts() {
return exts;
}
@@ -6,31 +6,31 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.ResourceFile.ZipRef;
import jadx.api.impl.SimpleCodeInfo;
import jadx.api.plugins.CustomResourcesLoader;
import jadx.api.plugins.resources.IResContainerFactory;
import jadx.api.plugins.resources.IResTableParserProvider;
import jadx.api.plugins.resources.IResourcesLoader;
import jadx.api.plugins.utils.ZipSecurity;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.Utils;
import jadx.core.utils.android.Res9patchStreamDecoder;
import jadx.core.utils.exceptions.JadxException;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
import jadx.core.utils.files.ZipFile;
import jadx.core.xmlgen.BinaryXMLParser;
import jadx.core.xmlgen.IResTableParser;
import jadx.core.xmlgen.ResContainer;
import jadx.core.xmlgen.ResTableBinaryParserProvider;
import jadx.zip.IZipEntry;
import jadx.zip.ZipContent;
import static jadx.core.utils.files.FileUtils.READ_BUFFER_SIZE;
import static jadx.core.utils.files.FileUtils.copyStream;
@@ -39,21 +39,21 @@ import static jadx.core.utils.files.FileUtils.copyStream;
public final class ResourcesLoader implements IResourcesLoader {
private static final Logger LOG = LoggerFactory.getLogger(ResourcesLoader.class);
private final JadxDecompiler jadxRef;
private final JadxDecompiler decompiler;
private final List<IResTableParserProvider> resTableParserProviders = new ArrayList<>();
private final List<IResContainerFactory> resContainerFactories = new ArrayList<>();
private BinaryXMLParser binaryXmlParser;
ResourcesLoader(JadxDecompiler jadxRef) {
this.jadxRef = jadxRef;
ResourcesLoader(JadxDecompiler decompiler) {
this.decompiler = decompiler;
this.resTableParserProviders.add(new ResTableBinaryParserProvider());
}
List<ResourceFile> load(RootNode root) {
init(root);
List<File> inputFiles = jadxRef.getArgs().getInputFiles();
List<File> inputFiles = decompiler.getArgs().getInputFiles();
List<ResourceFile> list = new ArrayList<>(inputFiles.size());
for (File file : inputFiles) {
loadFile(list, file);
@@ -94,28 +94,19 @@ public final class ResourcesLoader implements IResourcesLoader {
public static <T> T decodeStream(ResourceFile rf, ResourceDecoder<T> decoder) throws JadxException {
try {
ZipRef zipRef = rf.getZipRef();
if (zipRef == null) {
IZipEntry zipEntry = rf.getZipEntry();
if (zipEntry != null) {
try (InputStream inputStream = zipEntry.getInputStream()) {
return decoder.decode(zipEntry.getUncompressedSize(), inputStream);
}
} else {
File file = new File(rf.getOriginalName());
try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) {
return decoder.decode(file.length(), inputStream);
}
} else {
try (ZipFile zipFile = new ZipFile(zipRef.getZipFile())) {
ZipEntry entry = zipFile.getEntry(zipRef.getEntryName());
if (entry == null) {
throw new IOException("Zip entry not found: " + zipRef);
}
if (!ZipSecurity.isValidZipEntry(entry)) {
return null;
}
try (InputStream inputStream = ZipSecurity.getInputStreamForEntry(zipFile, entry)) {
return decoder.decode(entry.getSize(), inputStream);
}
}
}
} catch (Exception e) {
throw new JadxException("Error decode: " + rf.getDeobfName(), e);
throw new JadxException("Error decode: " + rf.getOriginalName(), e);
}
}
@@ -170,12 +161,13 @@ public final class ResourcesLoader implements IResourcesLoader {
if (parser == null) {
throw new JadxRuntimeException("Unknown type of resource file: " + resFile.getOriginalName());
}
parser.setBaseFileName(resFile.getDeobfName());
parser.decode(is);
return parser;
}
private static ResContainer decodeImage(ResourceFile rf, InputStream inputStream) {
String name = rf.getOriginalName();
String name = rf.getDeobfName();
if (name.endsWith(".9.png")) {
try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
Res9patchStreamDecoder decoder = new Res9patchStreamDecoder();
@@ -195,7 +187,7 @@ public final class ResourcesLoader implements IResourcesLoader {
}
// Try to load the resources with a custom loader first
for (CustomResourcesLoader loader : jadxRef.getCustomResourcesLoaders()) {
for (CustomResourcesLoader loader : decompiler.getCustomResourcesLoaders()) {
if (loader.load(this, list, file)) {
LOG.debug("Custom loader used for {}", file.getAbsolutePath());
return;
@@ -208,38 +200,48 @@ public final class ResourcesLoader implements IResourcesLoader {
public void defaultLoadFile(List<ResourceFile> list, File file, String subDir) {
if (FileUtils.isZipFile(file)) {
ZipSecurity.visitZipEntries(file, (zipFile, entry) -> {
addEntry(list, file, entry, subDir);
return null;
});
try {
ZipContent zipContent = decompiler.getZipReader().open(file);
// do not close a zip now, entry content will be read later
decompiler.addCloseable(zipContent);
for (IZipEntry entry : zipContent.getEntries()) {
addEntry(list, file, entry, subDir);
}
} catch (Exception e) {
throw new RuntimeException("Failed to open zip file: " + file.getAbsolutePath(), e);
}
} else {
ResourceType type = ResourceType.getFileType(file.getAbsolutePath());
list.add(ResourceFile.createResourceFile(jadxRef, file, type));
list.add(ResourceFile.createResourceFile(decompiler, file, type));
}
}
public void addEntry(List<ResourceFile> list, File zipFile, ZipEntry entry, String subDir) {
public void addEntry(List<ResourceFile> list, File zipFile, IZipEntry entry, String subDir) {
if (entry.isDirectory()) {
return;
}
String name = entry.getName();
ResourceType type = ResourceType.getFileType(name);
ResourceFile rf = ResourceFile.createResourceFile(jadxRef, subDir + name, type);
ResourceFile rf = ResourceFile.createResourceFile(decompiler, subDir + name, type);
if (rf != null) {
rf.setZipRef(new ZipRef(zipFile, name));
rf.setZipEntry(entry);
list.add(rf);
}
}
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);
copyStream(is, baos);
return new SimpleCodeInfo(baos.toString("UTF-8"));
return new SimpleCodeInfo(baos.toString(charset));
}
private synchronized BinaryXMLParser loadBinaryXmlParser() {
if (binaryXmlParser == null) {
binaryXmlParser = new BinaryXMLParser(jadxRef.getRoot());
binaryXmlParser = new BinaryXMLParser(decompiler.getRoot());
}
return binaryXmlParser;
}
@@ -0,0 +1,32 @@
package jadx.api.gui.tree;
import javax.swing.Icon;
import javax.swing.tree.TreeNode;
import org.jetbrains.annotations.Nullable;
import jadx.api.metadata.ICodeNodeRef;
public interface ITreeNode extends TreeNode {
/**
* Locale independent node identifier
*/
String getID();
/**
* Node title
*/
String getName();
/**
* Node icon
*/
Icon getIcon();
/**
* Related code node reference.
*/
@Nullable
ICodeNodeRef getCodeNodeRef();
}
@@ -11,7 +11,6 @@ import jadx.api.JadxArgs;
import jadx.api.metadata.ICodeAnnotation;
import jadx.api.metadata.ICodeNodeRef;
import jadx.api.metadata.annotations.NodeDeclareRef;
import jadx.api.metadata.annotations.VarRef;
import jadx.core.utils.StringUtils;
public class AnnotatedCodeWriter extends SimpleCodeWriter implements ICodeWriter {
@@ -151,7 +150,6 @@ public class AnnotatedCodeWriter extends SimpleCodeWriter implements ICodeWriter
@Override
public ICodeInfo finish() {
validateAnnotations();
String code = buf.toString();
buf = null;
return new AnnotatedCodeInfo(code, lineMap, annotations);
@@ -161,17 +159,4 @@ public class AnnotatedCodeWriter extends SimpleCodeWriter implements ICodeWriter
public Map<Integer, ICodeAnnotation> getRawAnnotations() {
return annotations;
}
private void validateAnnotations() {
if (annotations.isEmpty()) {
return;
}
annotations.values().removeIf(v -> {
if (v.getAnnType() == ICodeAnnotation.AnnType.VAR_REF) {
VarRef varRef = (VarRef) v;
return varRef.getRefPos() == 0;
}
return false;
});
}
}
@@ -29,7 +29,7 @@ public class DecompilePassWrapper extends AbstractVisitor implements IPassWrappe
public void init(RootNode root) throws JadxException {
try {
decompilePass.init(root);
} catch (Throwable e) {
} catch (StackOverflowError | Exception 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 {
try {
return decompilePass.visit(cls);
} catch (Throwable e) {
LOG.error("Error in decompile pass: {}, class: {}", this, cls, e);
} catch (StackOverflowError | Exception e) {
cls.addError("Error in decompile pass: " + this, e);
return false;
}
}
@@ -48,8 +48,8 @@ public class DecompilePassWrapper extends AbstractVisitor implements IPassWrappe
public void visit(MethodNode mth) throws JadxException {
try {
decompilePass.visit(mth);
} catch (Throwable e) {
LOG.error("Error in decompile pass: {}, method: {}", this, mth, e);
} catch (StackOverflowError | Exception e) {
mth.addError("Error in decompile pass: " + this, e);
}
}
@@ -32,8 +32,11 @@ public class CodeMetadataStorage implements ICodeMetadata {
return new CodeMetadataStorage(Collections.emptyMap(), Collections.emptyNavigableMap());
}
// <decomp file line number> -> <dex debug line number>
private final Map<Integer, Integer> lines;
// <character index into the file> -> <code annotation>
// the key is what is returned by AbstractCodeArea#getCaretPos() when clicking in a code panel.
private final NavigableMap<Integer, ICodeAnnotation> navMap;
private CodeMetadataStorage(Map<Integer, Integer> lines, NavigableMap<Integer, ICodeAnnotation> navMap) {
@@ -23,4 +23,12 @@ public interface JadxPlugin {
* For long operation, prefer {@link JadxPreparePass} or {@link JadxAfterLoadPass} instead.
*/
void init(JadxPluginContext context);
/**
* Plugin unload handler.
* Can be used to clean up resources on plugin unloading.
*/
default void unload() {
// optional method
}
}
@@ -14,6 +14,7 @@ import jadx.api.plugins.input.JadxCodeInput;
import jadx.api.plugins.options.JadxPluginOptions;
import jadx.api.plugins.pass.JadxPass;
import jadx.api.plugins.resources.IResourcesLoader;
import jadx.zip.ZipReader;
public interface JadxPluginContext {
@@ -59,4 +60,9 @@ public interface JadxPluginContext {
* Access to plugin specific files and directories
*/
IJadxFiles files();
/**
* Custom jadx zip reader to fight tampering and provide additional security checks
*/
ZipReader getZipReader();
}
@@ -26,4 +26,13 @@ public interface ISettingsGroup {
default List<ISettingsGroup> getSubGroups() {
return Collections.emptyList();
}
/**
* Settings close handler.
* Apply settings if 'save' param set to true.
* It can be used to clean up resources.
*/
default void close(boolean save) {
// optional method
}
}
@@ -2,12 +2,16 @@ package jadx.api.plugins.gui;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.KeyStroke;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import jadx.api.gui.tree.ITreeNode;
import jadx.api.metadata.ICodeNodeRef;
public interface JadxGuiContext {
@@ -34,6 +38,15 @@ public interface JadxGuiContext {
@Nullable String keyBinding,
Consumer<ICodeNodeRef> action);
/**
* Add popup menu entry for tree node
*
* @param name entry title
* @param addPredicate check if entry should be added for provided node, called on popup creation
*/
@ApiStatus.Experimental
void addTreePopupMenuEntry(String name, Predicate<ITreeNode> addPredicate, Consumer<ITreeNode> action);
/**
* Attach new key binding to main window
*
@@ -57,6 +70,16 @@ public interface JadxGuiContext {
*/
JFrame getMainFrame();
/**
* Load SVG icon from jadx resources.
* All available icons can be found in "jadx-gui/src/main/resources/icons".
* Method is thread-safe.
*
* @param name short name in form: "category/iconName", example: "nodes/publicClass"
* @return loaded and cached icon, if icon not found returns default icon: "ui/error"
*/
ImageIcon getSVGIcon(String name);
ICodeNodeRef getNodeUnderCaret();
ICodeNodeRef getNodeUnderMouse();
@@ -72,6 +95,11 @@ public interface JadxGuiContext {
*/
boolean open(ICodeNodeRef ref);
/**
* Open usage dialog for a node
*/
void openUsageDialog(ICodeNodeRef ref);
/**
* Reload code in active tab
*/

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