Compare commits

...

528 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
Skylot c4c3d42d16 build: fix release workflow 2024-11-11 22:46:31 +00:00
Skylot 58c36de8c2 build: add release github workflow 2024-11-10 20:15:34 +00:00
Skylot b4ca566a19 build: exclude shadow jar from publishing to maven 2024-11-10 20:15:34 +00:00
Skylot e644bad758 chore: update dependencies 2024-11-10 20:15:34 +00:00
Skylot dd86abcc5e chore: remove LGTM config 2024-11-10 17:26:24 +00:00
Skylot f0513a1a55 fix: insert new filled array insn before first usage (#2340) 2024-11-07 20:02:41 +00:00
Skylot be6cb573b1 feat(plugins): allow to set minimum required jadx version in plugin info (#2314) 2024-11-06 16:30:22 +00:00
pubiqq 5d064d3e50 feat(res): improve resource names (PR #2316) 2024-11-06 18:08:35 +03:00
pubiqq 15b6309e2c fix: support "fall-through to default" case in switch-over-string (PR #2338) 2024-11-05 21:41:03 +00:00
Skylot 417bb7a7e9 feat: add method to update all blocks related info in method (#2335) 2024-11-02 22:08:26 +00:00
Ahmet Bilal Can 4c74e8e300 feat: give ability to plugins to edit blocks before locking (PR #2336)
Plugins can use `.before('BlockFinisher')` to edit blocks before they are locked.
2024-11-02 21:38:57 +00:00
Skylot 57238de6ff feat(cli): add option to disable plugins (#2277) 2024-11-01 20:13:34 +00:00
Skylot 313c4a121a fix: improve negation condition checks for switch over string (#2333) 2024-11-01 16:33:28 +00:00
pubiqq 39912398fc fix: unwrap consts in switch-over-string (PR #2332) 2024-10-31 21:24:19 +00:00
Skylot 7544d1a113 fix: prevent endless loop in pre header insertion mod (#2300) 2024-10-31 20:00:06 +00:00
pubiqq cfbe5ab672 fix: fix default branch position in switch-over-string (PR #2331) 2024-10-31 19:58:35 +00:00
Ruffalo Lavoisier 2661b91a6f feat(gui): create Frida hooking snippet for all methods in the class (PR #2328) 2024-10-30 17:59:29 +00:00
Skylot 61578e8793 fix(gui): correct tabs filter in "Close others" tab action (#2330) 2024-10-30 17:20:16 +00:00
Skylot cc6a893402 feat: allow to disable installed plugins (#2277) 2024-10-28 23:35:28 +03:00
pubiqq 4d8a5d6671 fix(core): fix primitive-to-primitive conversions (PR #2326) 2024-10-28 20:33:10 +00:00
Skylot 982307b1ac fix(gui): use correct section filter in plugins list 2024-10-25 19:01:43 +01:00
Skylot 37054dc84e fix(gui): load plugins settings in temp context without UI (#2206) 2024-10-25 18:51:38 +01:00
Skylot 343e2c531a chore: expand input dirs in ConvertArscFile 2024-10-25 18:12:48 +01:00
pubiqq 6fa5d247f0 fix(res): update Android attrs to API 35 (PR #2318) 2024-10-25 18:08:36 +01:00
pubiqq 688dea0c50 fix(deobf): update TLDs (PR#2320) 2024-10-25 16:44:07 +01:00
Skylot c0815b12bc chore(gui): add missing ref text in untranslated messages (#2319)
fix
2024-10-24 20:33:52 +03:00
JustFor 233f8692b2 fix(gui): update Messages_zh_CN.properties (PR #2319)
1. New text is translated simultaneously, two untranslated texts remain (no English text)
2. Adjusted and unified part of the symbol, into the full Angle symbol.
2024-10-24 18:31:14 +01:00
Skylot e5be41b9cc fix(gui): use another implementation for font dialog (#2310) 2024-10-22 21:32:14 +01:00
Skylot 3788e4ef3a fix(gui): in settings row reduce space between description and value 2024-10-22 18:52:42 +01:00
Skylot 32855f4511 fix(gui): improve plugins preferences group 2024-10-22 18:51:19 +01:00
Skylot 3d5e225274 fix: clear temp root dir instead delete (#2312) 2024-10-22 18:01:52 +01:00
nitram84 8a34d973ff build: use jadx-gui as a library in plugins (PR #2310)
* fix(gui): fix javadoc issues

* feat(gui): export jadx-gui as a library

* fix(gui): fix javadoc issues

* fix(gui): remove invalid characters for javadoc

* add jadx-library also for jadx-cli and jadx-script-ide modules

---------

Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
2024-10-22 17:24:57 +01:00
Skylot a43b3282ef fix(gui): update check fixed to match current artifact naming 2024-10-17 21:57:20 +01:00
Skylot 249801880c feat(api): allow to get method code (#2305) 2024-10-17 19:20:07 +01:00
Skylot 742d30d770 chore: resolve warnings in SmaliArea class 2024-10-16 17:29:36 +01:00
Skylot 267413332a chore: resolve KtLint deprecation warning 2024-10-16 17:23:09 +01:00
Skylot f9c94f27f1 chore: update dependencies 2024-10-16 17:01:03 +01:00
Skylot d2131d9640 build: gradle wrapper validation not needed, check done in setup-java action 2024-10-16 17:00:43 +01:00
Skylot 548821c4df build: allow to change java toolchain for build and tests 2024-10-16 16:54:28 +01:00
pubiqq 958ab245ae fix(res): don't rename resource entries when useRawResName = true (PR #2306) 2024-10-14 20:32:12 +01:00
Skylot 8f3cc3e8c1 fix: add missing null check in codegen for classes generated by jadx 2024-10-14 19:19:31 +01:00
Skylot b872ffd1b9 fix: replace patched zip early in input files (#2302) 2024-10-12 20:53:09 +01:00
Skylot c5263f92fe fix(gui): send select tab event before code jump (#2292) 2024-10-12 21:23:53 +03:00
pubiqq 1475e887c8 fix: update reserved keywords (PR #2301) 2024-10-11 20:47:34 +01:00
Skylot c21cabcba7 fix: use temp dir env var only in apps 2024-10-10 22:57:40 +03:00
Skylot 063af8cd62 feat(api): add JadxArgs property to adjust xml security checks (#2291) 2024-10-10 22:57:40 +03:00
Skylot 2d10537050 chore: update dependencies 2024-10-10 22:57:40 +03:00
pubiqq 3814951408 feat: improve better name algorithm (PR #2299) 2024-10-10 20:01:57 +01:00
qfalconer 964bd62d35 feat: adding automatic patching for semi-corrupted APKs (PR #2298)
* feat: patching semi-corrupted APKs

* fix: using secure temp file creation

* fix: using TWR when handling the files
2024-10-10 17:53:26 +01:00
xiaojye 3ed2df828f feat(gui): add button to go to Android Manifest (PR #2296)
* feat: Add button to go to Android Manifest

* fix: Change the text from 'Go to Manifest' to 'Go to AndroidManifest.xml' and replace icon source
2024-10-10 17:11:08 +01:00
Skylot b26abdc851 feat(plugins): get config and cache dirs for plugins 2024-09-29 21:55:05 +01:00
Skylot 90185fd947 feat(plugins): get a main window reference as JFrame 2024-09-29 21:54:24 +01:00
Skylot 681f8a98b5 fix: improve checks for restore new filled array (#2289) 2024-09-28 16:52:08 +01:00
Skylot 0b225238fb feat: support restore of switch over string (basic case)(#2288) 2024-09-27 21:11:38 +01:00
Skylot a7649dda7a chore: update gradle and dependencies 2024-09-27 21:08:38 +01:00
pubiqq b5e3dcf70f feat: add the option to always use source file name as class name alias (PR #2287) 2024-09-23 22:47:08 +01:00
Skylot 7abbc81886 fix: improve switch out block search if all method exits are inside (#2264) 2024-09-22 21:35:40 +01:00
Skylot 9c30aeacdb refactor: split region maker 2024-09-22 20:23:03 +01:00
Skylot 8f27de4d0e chore: update dependencies 2024-09-21 22:00:03 +01:00
Skylot 02b69d2d29 fix(gui): prevent old refs leak in shortcuts controller 2024-09-21 21:53:31 +01:00
Skylot e6fde48b69 fix: don't add same 'loaded from:' comment for inner classes 2024-09-21 20:28:18 +01:00
Skylot 109dea0857 feat: support inner class contruction with outer instance (#2253) 2024-09-21 20:27:16 +01:00
Skylot 1d34328dd3 fix(build): disable cache for core tests to allow reruns (#2283) 2024-09-20 21:44:55 +01:00
Skylot ef4f1d3ed4 fix: ignore debug lines hints if numbers was adjusted for method 2024-09-20 21:36:13 +01:00
Skylot 23696d3971 fix: use type from new-instance if differ from constructor call (#2285) 2024-09-20 21:34:21 +01:00
Andy Smith efa2f5d172 feat(gui): limit search to a package (PR #2284)
* Add isDescendantOf and getJavaPackage helper functions

* Add i18n strings for search package

* Added search package to options in SearchSettings

* Add package limiting to each search provider

* Add package search to dialog and logic to get package by string.

* Added search option to package context menu

* Fix spotlessJavaCheck complaints

* Revert changes to individual search providers and add filter to base provider
2024-09-20 12:31:20 +03:00
Skylot 699ceb197e fix: improve condition branch checks in loops (#2274) 2024-09-16 20:37:34 +01:00
Skylot 5c83c22501 feat(java-input): support StackMapTable to get stack info for unvisited jumps (#2271) 2024-09-14 22:45:31 +01:00
Skylot 7bb5c0a859 fix: protect class deps from loading in different decompilation mode 2024-09-13 21:25:11 +01:00
Skylot 603863403f fix: do not add custom passes for fallback and simple modes (#2276) 2024-09-13 21:01:19 +01:00
Skylot 889a945cf1 fix: additional checks for class signature (#2272) 2024-09-12 20:29:05 +01:00
Skylot fd80e03809 fix: check if debug info offset is invalid (#1653) 2024-09-11 19:54:49 +01:00
Skylot b5807082d9 chore: update gradle and dependencies 2024-09-11 19:54:49 +01:00
Skylot 5d1f0b8cae feat(res): support grammar inflection flag in res config (#2270) 2024-09-08 21:49:13 +01:00
Skylot 3f9aa34057 fix(gui): resolve old objects reference leak in TabsController 2024-09-07 01:21:46 +03:00
Skylot e2c860f260 refactor(res): use list instead map to store entries offsets 2024-09-07 01:21:46 +03:00
Skylot 0938351d97 feat(res): support compact resource entries (#2268) 2024-09-07 01:21:42 +03:00
pubiqq 937dd20794 feat(res): support 16-bit entry offsets (PR #2269) 2024-09-06 23:21:21 +01:00
pubiqq ea5e87560a feat(res): improve error message for unsupported ResTable flags (PR #2266) 2024-09-05 19:13:26 +01:00
Skylot 5fbbf2150e fix(res): prevent duplication of ARSC entries (#2263) 2024-09-04 21:31:37 +01:00
Skylot 0e11bffe82 chore: update dependencies 2024-09-04 20:02:56 +01:00
Skylot ba9af5c288 fix(gui): minor fixes for code jumps 2024-09-04 19:59:57 +01:00
Skylot cca706c94f fix: improve 'continue' insertion for switch in loop (#2249) 2024-09-01 23:02:22 +01:00
Skylot 2df69bbfb4 fix(gui): prevent UI stuck on class load (#2259) 2024-08-31 22:30:18 +01:00
Skylot f5307636ef fix(gui): merge full class name tokens for constructors (#2261) 2024-08-30 22:39:43 +03:00
Mino 9a39b70a46 fix(gui): Quick Tabs Optimization (PR #2242)
* optimize tabs reorder

* restructure based on quick tabs architecture

* code formatting

* log all exceptions from background executor

* various improvements

---------

Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
2024-08-30 20:33:05 +01:00
pubiqq e63808bc4b fix: improve checking of access modifiers (PR #2255) 2024-08-20 16:45:28 +03:00
Skylot 847225a6a9 fix: improve try/catch temp edges injection (#2247) 2024-08-17 21:09:31 +01:00
Skylot eee354a3ab chore: update gradle and dependencies 2024-08-17 21:01:27 +01:00
Skylot 1cc00a54f3 fix(gui): use correct translation in rename dialog (#2254) 2024-08-17 21:58:39 +03:00
pubiqq ffdad1b652 fix: improve checking of access modifiers for methods (PR #2252) 2024-08-15 23:20:07 +01:00
pubiqq 9a8ec76989 fix: improve checking of access modifiers for classes (PR #2251) 2024-08-15 19:59:44 +01:00
Skylot 0be5b2cea9 refactor(tests): add debug checks switch to jadx args 2024-08-13 22:39:48 +01:00
Skylot c94201be4a fix: improve switch out search in loop (#2246) 2024-08-12 22:06:03 +01:00
Skylot 1051dacb1e refactor(tests): migrate from Hamcrest to AssertJ 2024-08-11 21:55:56 +01:00
Skylot a2bfe9bbe8 chore: add openrewrite gradle plugin to improve code quality 2024-08-11 21:10:01 +01:00
Skylot 8c6ec3bccc fix(gui): trim also leading spaces in paths from file dialog (#2244) 2024-08-10 19:24:05 +01:00
Skylot 015876b790 fix(gui): trim trailing spaces in input files (#2244) 2024-08-10 00:31:25 +01:00
Skylot 280f3870a9 fix: handle quick return on branched constructor (#2240) 2024-08-09 22:54:13 +01:00
Skylot e9d770ae9e fix: use correct approach to get prev block on path (#2239) 2024-08-09 22:51:37 +01:00
Skylot 5f1bd1d9ba chore: migrate gradle shadow plugin 2024-08-08 20:44:34 +01:00
pubiqq 60fb458024 fix: improve inlining synthetic accessors (PR #2243)
* fix: fix inlining synthetic accessors

* add test, undo changes in InsnNode

---------

Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
2024-08-08 20:27:08 +01:00
Skylot 1b08779536 chore: update dependencies 2024-08-07 19:06:58 +01:00
Skylot c37e39a819 chore: code improvements by cleanthat 2024-08-07 00:34:22 +01:00
Skylot 2d58fbd4b1 chore: forbid use ArrayList as a variable type 2024-08-07 00:28:39 +01:00
Mino ffbf800404 feat(gui): Quick Tabs Overhaul (PR #2241)
* restructure quick tabs code

* code formatting

* display open tabs

* added bookmark tabs feature

* fix tabs pin and bookmark not saved

* fix NPE treeModel not initialized

* Fix hardcoded strings

* remove unused statement

* fix NPE again

* added bookmark overlay

* preserve tabs order

* fix context menu actions

* remove unnecessary public modifier

* save tabs in tabbedpane order

* remove unreferenced tabs

* Update jadx-gui/src/main/java/jadx/gui/ui/tab/TabComponent.java

---------

Co-authored-by: skylot <118523+skylot@users.noreply.github.com>
2024-08-06 23:47:25 +01:00
dependabot[bot] 500aa8a68d build(deps): bump gradle/actions from 3 to 4 (PR #2238)
Bumps [gradle/actions](https://github.com/gradle/actions) from 3 to 4.
- [Release notes](https://github.com/gradle/actions/releases)
- [Commits](https://github.com/gradle/actions/compare/v3...v4)

---
updated-dependencies:
- dependency-name: gradle/actions
  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>
2024-08-05 18:35:39 +01:00
Skylot 58e8268126 fix: workaround to make method inline deterministic (#1089) 2024-08-04 22:46:46 +01:00
Skylot f9da6e00ed fix(gui): add VM flags to fix UI ghosting (#2225) 2024-08-04 19:40:14 +01:00
Skylot 821cc668c7 fix: don't rerun SSA transform in ConstructorVisitor (#2236) 2024-08-02 21:26:25 +01:00
Skylot 287ba49008 fix(gui): show folding actions in code popup menu (#2234) 2024-08-02 17:13:16 +01:00
pubiqq 115e563a2b fix: improve checking if methods are inline for FixAccessModifiers (PR #2235) 2024-08-01 21:45:52 +01:00
Mino 1669200e62 feat(gui): smali code folding (PR #2233)
* added smali code folding

* remove unnecessary public modifiers

* improve code

* undo method extract

---------

Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
2024-08-01 19:33:37 +01:00
Mino 6ab224ea0d feat(gui): pin tabs (PR #2230)
* add ability to pin tabs

* save pinned tabs with Save Project action

* further prevent closing pinned tabs

* add translation entries

* prevent pinning start page

* add pinned tabs tree view

* properly dispose of quickTabsTree

* restructure code

* more unpin context menu items
2024-08-01 18:06:45 +01:00
pubiqq 61855a7ea1 fix: make detailed var info deterministic (PR #2231) 2024-07-31 22:16:07 +01:00
Mino bda3119e86 fix(gui): horizontal scrolling in Linux (PR #2229)
* fix horizontal scrolling in linux

* improve code

---------

Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
2024-07-31 17:02:00 +01:00
Mino b26abed686 feat(gui): export resource/class/package (PR #2228)
* feat: export resource

* feat: export class

* restructure code: introduce enum for exporting classes

* feat: export package

* feat: export resource folder

* check directory exists before creation

* apply code formatting

* fix code formatting

* Apply suggestions from code review

---------

Co-authored-by: skylot <118523+skylot@users.noreply.github.com>
2024-07-31 17:00:54 +01:00
Skylot c179beee95 chore: update dependencies 2024-07-28 20:57:37 +01:00
Skylot 04a454094b fix: improve exception handlers checks (#2086) 2024-07-27 22:09:14 +01:00
Skylot 33bcbfec41 chore(docs): minor fixes 2024-07-27 22:09:13 +01:00
Skylot ec645d80b1 fix(mappings): try to prevent mapping file reset on export exception, refactor and fix code to avoid NPE (#2220)(#2226) 2024-07-25 17:44:23 +01:00
Jan S. a8d889de3d fix(launch4j): do not overwrite Java heap configuration from applicationDefaultJvmArgs (PR #2218) 2024-07-20 20:46:45 +01:00
Skylot ec0bf701c8 fix(build): fix hiding console in Windows (#2196)
Regression after migration to shadow jar in #1868,
instead CreateStartScripts task from 'application' plugin
'startShadowScripts' task should be modified.
2024-07-19 00:19:47 +03:00
Skylot ad4dd116be chore: update gradle and dependencies 2024-07-19 00:19:47 +03:00
Skylot ef79f266c8 fix(build): fix gitlab build 2024-07-19 00:19:43 +03:00
Jan S 366225f9be fix(quark): fix automated installation and check exit code of executed external commands (#2119)(PR #2216) 2024-07-12 18:39:17 +01:00
Skylot 730db0d24f fix(build): do not wrap jar in launch4j (#2186) 2024-07-08 22:17:06 +01:00
Iscle 05fb77e9bd feat(gui): add button to go to Application class (#2208)(PR #2213)
* feat: Add button to go to Application class

Icons from: https://intellij-icons.jetbrains.design/

* fix: Rename "goto" to "go_to" to keep things consistent
2024-07-08 18:36:45 +01:00
Iscle bbabfa0354 fix(gui): fix Xposed args code generation (PR #2212)
* Rename .java to .kt

* fix: Fix wrong function arguments when generating kotlin xposed code
2024-07-08 18:34:28 +01:00
Jan S 96bd9f0f17 fix(xml): AXML/Manifest parsing improvements (PR #2211)
* log and ignore decodeValue errors

* skip extra data in package header

* ResourceTypes.h
2024-07-06 19:27:47 +03:00
qfalconer fd5b397b40 fix(xml): allow for non-standard attributes sizes and avoid index exceptions when decoding some strings (PR #2210)
More lenient AXML parsing: allow for non-standard attributes sizes and avoid index exceptions when decoding some strings

* The attributes size of an XML element is now accounted for. This size must be at least 20 (0x14) bytes but can be greater. Extra bytes are just skipped. When decoding a string, if such decoding is impossible a placeholder string is returned instead of throwing an exception. This is necessary because some malware purposely add android:tag attributes with invalid string index to throw parsers off. They also employ non-standard attribute sizes.

* Minor code restyling

---------

Co-authored-by: qfalconer <knm241@gmail.com>
2024-07-03 18:50:15 +01:00
Artem Zhiganov f5e3a261b4 fix(gui): update russian translation (PR #2209)
Co-authored-by: SVolf <dev@thunderdog.ru>
2024-06-28 18:39:23 +01:00
JustFor 52a884608a fix(gui): update Messages_zh_CN.properties (PR #2203)
sync new texts.
2024-06-14 18:52:31 +01:00
Iscle 74ddfde950 feat(gui): allow to check for unstable releases (PR #2200)
* feat: gui: convert JadxUpdate to Kotlin

* feat: gui: allow updater to check for latest unstable artifacts

* fix: remove nullable operator from onUpdate() interface
2024-06-11 22:49:44 +01:00
Skylot 9aacb4f312 fix: config dir was used instead cache dir 2024-06-11 20:55:49 +01:00
Jiaxin Peng 82e2104f3c fix(gui): support filtering files with multiple extensions in file dialog (PR #2185)
* fix(gui): support filtering files with multiple extensions in file dialog

* lint
2024-05-19 17:47:35 +01:00
Skylot 09fa35f144 feat: allow to change config and cache dirs with env vars (#2159) 2024-04-27 21:48:42 +01:00
Skylot f2a6a1e942 build: update JDK to 21 for windows artifacts 2024-04-27 17:40:06 +01:00
Andrei Kudryavtsev b85900aa3d feat: move AAB support to separate plugin (PR #2165)
* wip: finished with factories

* wip: bundleconfig.pb

* wip: jadx-aab-input, separate BundleConfig parser

* wip: removed test apks

* wip: proto xml pretty print

* wip: fixed getNamedValues NPE

* minor fixes

* spotless

* enabled zip64 for gui shadow jar

* spotless

* spotless

* reverted manifest identification since signature parsing not working at the moment

* replace static methods with new API methods

---------

Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
2024-04-26 21:54:54 +01:00
Jan S 37a42d1418 fix(gui): show correct content of binary resources in hex view (#2160) (PR #2166) 2024-04-24 19:29:52 +01:00
Jan S 07dde05337 fix(build): configure launch4j to not change current directory (#2162) (PR #2163) 2024-04-24 18:52:08 +01:00
Nick 8618214c7f docs: improve installation section (PR #2161)
* Update README.md

* Update README.md
2024-04-24 18:43:05 +01:00
Skylot b80f32a36f fix(smali-input): compile one smali file at a time to avoid 64k limit (#2158) 2024-04-23 22:14:59 +01:00
Skylot ce527ed753 fix(build): add missing files in bundle 2024-04-20 21:04:17 +01:00
Skylot f2ea6415c9 fix(cli): don't print stacktrace for incorrect options (#2140) 2024-04-20 18:06:30 +01:00
Skylot bc70f8eabb fix: use correct new line string for simple code writer 2024-04-20 17:37:45 +01:00
Skylot be25cbf8c2 fix: use common parser for manifest, verify app package 2024-04-20 17:37:45 +01:00
Skylot f9c0cad146 chore: update dependencies 2024-04-19 20:14:25 +01:00
Skylot b356ff76e1 fix: improve StringBuilder elimination (#2148) 2024-04-19 20:14:25 +01:00
Skylot ec9244a635 fix(gui): use common code for manifest parsing in debugger 2024-04-19 20:14:25 +01:00
omerfarukkykc a5bd64461d fix(gui): remember selected device in debugger (PR #2153)
* ADBDialog->launchApp() if multiple devices presented should let user select the one they desire.

* compare objects directly instead parsing

---------

Co-authored-by: Ömer Faruk KAYIKCI <omer.kayikci@tubitak.gov.tr>
Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
2024-04-19 19:02:12 +01:00
dependabot[bot] 54bf79ccc5 build(deps): bump gradle/wrapper-validation-action from 2 to 3 (#2149) 2024-04-15 18:17:48 +00:00
Skylot 6182332eef fix: avoid self-loop for exception handlers (#2147) 2024-04-11 23:07:45 +03:00
Skylot 37b57096ec fix: allow use FieldInfo as switch key (#2147) 2024-04-11 23:07:44 +03:00
Skylot 6aab8fabc9 chore: update dependencies 2024-04-11 23:07:41 +03:00
JustFor 665c1e57d2 fix(gui): update Messages_zh_CN.properties (PR #2146)
Sync new Jadx text.
2024-04-09 22:11:24 +01:00
Skylot 6e8affcbdc feat: add options to JadxArgs to change code new line and indent (#1945, #1948) 2024-04-08 21:51:24 +01:00
Skylot 41d6b0018e fix: add missing " * " on new line for block comments, flip addCodeComment args (#2145) 2024-04-08 21:34:34 +01:00
Skylot dbadbb01fc refactor: rename method collectArgsWithoutLoading into collectArgNodes in MethodNode (#2142) 2024-04-07 23:09:02 +01:00
Skylot 0f52077c5c feat: allow to set style for code comments (#2145) 2024-04-07 23:06:32 +01:00
Skylot ea861829c7 fix: support end block entry for mutli-entry loops (#889) 2024-04-06 22:49:32 +01:00
Skylot c1de235289 fix: in anonymous class checks ignore instance fields not used outside 2024-04-06 22:45:30 +01:00
Skylot 8f969d4e89 chore: update gradle and dependencies 2024-04-03 21:03:48 +01:00
Skylot 0c1f830f94 fix: lambda decoding and code generation (#2139) 2024-04-03 21:03:48 +01:00
Skylot 43c082e4da feat: replace Android resource ids with android.R fields (#2119) 2024-03-31 20:37:33 +01:00
Skylot ecdc4e6757 refactor: move constant collection into separate pass (#2119) 2024-03-30 21:51:02 +00:00
Skylot b865c9c687 refactor: allow store unresolved fields in ConstStorage (#2119) 2024-03-30 20:52:31 +00:00
xnumad 6b4976c593 fix(gui): handle paths where file name is null (#2136)(PR #2137)
* fix: Ignore invalid files

Avoid NullPointerException when using "Open files" or drag-n-drop

* refactor: Replace Stream API chain with loop

IntelliJ

* fix: Ignore invalid files

Avoid NullPointerException when using "Add files"

* fix: Fall back to complete path string

Instead of empty project name

* fix: Render tree

Project tree (sidebar) didn’t load
Toggling "View > Show flatten packages" threw a NPE here

* fix code formatting

---------

Co-authored-by: Skylot <skylot@gmail.com>
2024-03-29 22:30:01 +00:00
Skylot 2807dc5090 fix(script): add example script for resources rename (#2126) 2024-03-20 18:46:44 +00:00
Skylot 463d2b90fa fix: don't apply node positions and prevent eager loading for custom decompile modes (#2116) 2024-03-19 20:23:04 +00:00
Skylot bff00d101f fix(script): add option flags, fix missing script options in help 2024-03-19 20:22:50 +00:00
Skylot 1290ef63a2 fix(build): enable publish to maven for rename-mappings plugin 2024-03-16 21:58:03 +03:00
Skylot 49d2b34d84 chore: update dependencies 2024-03-16 21:58:00 +03:00
CKCat eecdfae73f fix(res): resolve some manifest decode errors (PR #2122)
* The elementSize may be larger than the actual size of the element chunk.

* end namespace chunk size can be any value.

* keep at least a warning.
2024-03-16 18:57:10 +00:00
JustFor 8760b4ddde fix(gui): copy strings without quotes (PR #2121)
* Update AbstractCodeArea.java

In general, we need data, not text in code. But now every time you copy the highlighted text, you copy the highlighted quotes as well. This often results in an extra need to delete the quotation marks around the sides, which is confusing.
Now when copying selected highlighted text, quotes are not copied in.

* Update AbstractCodeArea.java

fix code format

* additional checks, move to common method

---------

Co-authored-by: Skylot <skylot@gmail.com>
2024-03-16 18:55:57 +00:00
Andrei Kudryavtsev 3599b248a4 feat(gui): dragging tab appearance settings (#2120)(PR #2118) 2024-03-08 23:11:58 +03:00
bagipro 2fdd496518 fix(res): add indents for namespace declarations (PR #2114)
Co-authored-by: bagipro <bugi@bugi>
2024-03-01 16:32:47 +00:00
bagipro 278e3c2d47 fix(res): avoid duplicated XML attributes (PR #2112)
Co-authored-by: bagipro <bugi@bugi>
2024-02-27 18:49:09 +00:00
bagipro 881a716b8e fix(res): fixed XML proto parsing for removed debug data (PR #2111)
* Fixed XML proto parsing for removed debug data

* Fixed codestyle check

---------

Co-authored-by: bagipro <bugi@bugi>
2024-02-27 17:34:37 +00:00
Skylot a73c9e90fc fix(dex-input): improve error report message for invalid dex checksum 2024-02-26 19:36:28 +00:00
Skylot 56749b2afb chore: update dependencies 2024-02-25 22:38:00 +03:00
Andrei Kudryavtsev d7ec35791b feat(gui): tabs drag and drop reorder support (#1212) (PR #2109) 2024-02-25 19:36:46 +00:00
Skylot d51362ed50 fix: don't remove exception handlers (#2104) 2024-02-19 20:18:44 +00:00
Skylot 5c0c1daa71 fix(gui): use new RSTA line number formatter API to show source lines 2024-02-16 18:38:56 +00:00
Skylot 603ea3989a chore: update dependencies 2024-02-16 17:41:37 +00:00
Emiel Matthys 018ff98df7 feat(gui): remember save preference decision (PR #2103)
* First version

* Use dropdown

* Spotless

* Language strings and tests

* Comment out translated versions

* Remove more translations

---------

Co-authored-by: Emiel Matthys <emiel.matthys@guardsquare.com>
2024-02-15 18:00:37 +00:00
nitram84 5fbabdefca fix: NPE in unused EcxHandler block removal code (#2086) (PR #2104) 2024-02-15 17:57:30 +00:00
nitram84 13607fc8b6 fix(res): add missing namespace declarations (PR #2102)
* fix(res): add missing namespace declarations

* remove `writer.getIndent() == 0`

---------

Co-authored-by: skylot <118523+skylot@users.noreply.github.com>
2024-02-15 17:56:39 +00:00
DanielFi 0c33d723c8 fix: optimize switch fallthrough (PR #2054)
* cache post dom map between switch cases
* cache post dom map of whole methods
* calculate full post dom tree, fix switch out block detection

---------

Co-authored-by: Skylot <skylot@gmail.com>
2024-02-14 18:31:38 +00:00
Skylot 0143423dc9 fix: use empty line before field in correct place (#2101) 2024-02-12 15:39:41 +00:00
Skylot 21b1452485 chore: update gradle and dependencies 2024-02-12 15:39:40 +00:00
Skylot ecb8abb98e fix: correct rollback if type update failed (#2090) 2024-02-10 16:08:51 +00:00
Skylot a3a4fabd5a fix: store classes access flags in class set 2024-02-07 22:10:31 +03:00
Skylot edf6ce273c fix: clear node flags for custom decompilation mode 2024-02-06 21:54:41 +03:00
dependabot[bot] 1bb956a8b0 build(deps): bump gradle/wrapper-validation-action from 1 to 2 (PR #2099)
Bumps [gradle/wrapper-validation-action](https://github.com/gradle/wrapper-validation-action) from 1 to 2.
- [Release notes](https://github.com/gradle/wrapper-validation-action/releases)
- [Commits](https://github.com/gradle/wrapper-validation-action/compare/v1...v2)

---
updated-dependencies:
- dependency-name: gradle/wrapper-validation-action
  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>
2024-02-05 18:38:02 +00:00
1531 changed files with 77537 additions and 31263 deletions
+34 -28
View File
@@ -8,56 +8,60 @@ 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: 11
java-version: 25
- name: Set jadx version
run: |
JADX_LAST_TAG=$(git describe --abbrev=0 --tags)
JADX_VERSION="${JADX_LAST_TAG:1}.$GITHUB_RUN_NUMBER-${GITHUB_SHA:0:8}"
JADX_REV=$(git rev-list --count HEAD)
JADX_VERSION="r${JADX_REV}.${GITHUB_SHA:0:7}"
echo "JADX_VERSION=$JADX_VERSION" >> $GITHUB_ENV
- name: Build with Gradle
uses: gradle/actions/setup-gradle@v3
with:
arguments: dist copyExe
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
- name: Build
run: ./gradlew dist distWin
env:
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
# Upload unpacked files for now
path: build/jadx/**/*
if-no-files-found: error
retention-days: 30
retention-days: 14
- name: Save exe artifact
uses: actions/upload-artifact@v4
- name: Save Windows bundle artifact
uses: actions/upload-artifact@v7
with:
name: ${{ format('jadx-gui-{0}-no-jre-win.exe', env.JADX_VERSION) }}
path: build/*.exe
name: ${{ format('jadx-gui-{0}-no-jre-win', env.JADX_VERSION) }}
# Upload unpacked files for now
path: jadx-gui/build/jadx-gui-win/*
if-no-files-found: error
retention-days: 30
retention-days: 14
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: 17
release: 25
- name: Print Java version
shell: bash
@@ -66,19 +70,21 @@ jobs:
- name: Set jadx version
shell: bash
run: |
JADX_LAST_TAG=$(git describe --abbrev=0 --tags)
JADX_VERSION="${JADX_LAST_TAG:1}.$GITHUB_RUN_NUMBER-${GITHUB_SHA:0:8}"
JADX_REV=$(git rev-list --count HEAD)
JADX_VERSION="r${JADX_REV}.${GITHUB_SHA:0:7}"
echo "JADX_VERSION=$JADX_VERSION" >> $GITHUB_ENV
- name: Build with Gradle
uses: gradle/actions/setup-gradle@v3
with:
arguments: dist -PbundleJRE=true
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
- name: Save exe bundle artifact
uses: actions/upload-artifact@v4
- name: Build
run: ./gradlew dist -PbundleJRE=true
- name: Save Windows with JRE bundle artifact
uses: actions/upload-artifact@v7
with:
name: ${{ format('jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }}
path: jadx-gui/build/*-with-jre-win/*
# Upload unpacked files for now
path: jadx-gui/build/jadx-gui-with-jre-win/*
if-no-files-found: error
retention-days: 30
retention-days: 14
+10 -7
View File
@@ -14,15 +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: 11
java-version: 25
- name: Build with Gradle
uses: gradle/actions/setup-gradle@v3
with:
arguments: build dist copyExe
- name: Setup Gradle
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
@@ -1,15 +0,0 @@
name: Validate Gradle Wrapper
on:
push:
branches: [ master, build-test ]
pull_request:
branches: [ master ]
jobs:
validation:
name: Validation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: gradle/wrapper-validation-action@v1
+92
View File
@@ -0,0 +1,92 @@
name: Release
on:
push:
tags:
- "v*.*.*"
# additional permissions for provided GitHub token to create new release
permissions:
contents: write
jobs:
build-release-win-bundle:
runs-on: windows-latest
steps:
- uses: actions/checkout@v6
- name: Set up JDK
uses: oracle-actions/setup-java@v1
with:
release: 25
- name: Set jadx version
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@v6
- name: Build
run: ./gradlew dist -PbundleJRE=true
- name: Save JRE bundle artifact
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) }}
if-no-files-found: error
retention-days: 1
release:
needs: build-release-win-bundle
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Set up JDK
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 25
- name: Set jadx version and release name
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@v6
- name: Build
run: ./gradlew dist distWin
env:
JADX_BUILD_JAVA_VERSION: 11
- name: Download Windows JRE bundle
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) }}
- run: |
cd build
pwd
ls -l
ls jadx-gui-*-with-jre-win
mv jadx-gui-*-with-jre-win/jadx-gui-*-with-jre-win.zip .
mv distWin/jadx-gui-*-win.zip .
ls -l *.zip
- name: Release
uses: softprops/action-gh-release@v3
with:
name: ${{ env.JADX_VERSION }}
draft: true
fail_on_unmatched_files: true
files: build/jadx-*.zip
+7 -1
View File
@@ -21,6 +21,7 @@ build/
classes/
idea/
.gradle/
.kotlin/
node_modules/
.vscode/
@@ -28,13 +29,18 @@ jadx-output/
*-tmp/
**/tmp/
*.jobf
*.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 copyExe
java-17:
stage: test
image: eclipse-temurin:17
script: ./gradlew clean build dist copyExe
java-21:
build-test:
stage: test
image: eclipse-temurin:21
script: ./gradlew clean build dist copyExe
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"/>
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>
+10 -23
View File
@@ -3,7 +3,7 @@
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
contributors and maintainers pledge to make participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
@@ -23,13 +23,13 @@ include:
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
professional setting
## Our Responsibilities
@@ -45,25 +45,12 @@ threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at skylot@gmail.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
This Code of Conduct applies within all project spaces, and it also applies when
an individual is representing the project or its community in public spaces.
Examples of representing a project or community include using an official
project e-mail address, posting via an official social media account, or acting
as an appointed representative at an online or offline event. Representation of
a project may be further defined and clarified by project maintainers.
## Attribution
+159 -104
View File
@@ -8,16 +8,19 @@
![GitHub release (latest by SemVer)](https://img.shields.io/github/downloads/skylot/jadx/latest/total)
![Latest release](https://img.shields.io/github/release/skylot/jadx.svg)
[![Maven Central](https://img.shields.io/maven-central/v/io.github.skylot/jadx-core)](https://search.maven.org/search?q=g:io.github.skylot%20AND%20jadx)
![Java 11+](https://img.shields.io/badge/Java-11%2B-blue)
[![License](http://img.shields.io/:license-apache-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0.html)
**jadx** - Dex to Java decompiler
Command line and GUI tools for producing Java source code from Android Dex and Apk files
:exclamation::exclamation::exclamation: Please note that in most cases **jadx** can't decompile all 100% of the code, so errors will occur. Check [Troubleshooting guide](https://github.com/skylot/jadx/wiki/Troubleshooting-Q&A#decompilation-issues) for workarounds
> [!WARNING]
> Please note that in most cases **jadx** can't decompile all 100% of the code, so errors will occur.<br />
> Check [Troubleshooting guide](https://github.com/skylot/jadx/wiki/Troubleshooting-Q&A#decompilation-issues) for workarounds.
**Main features:**
- decompile Dalvik bytecode to java classes from APK, dex, aar, aab and zip files
- decompile Dalvik bytecode to Java code from APK, dex, aar, aab and zip files
- decode `AndroidManifest.xml` and other resources from `resources.arsc`
- deobfuscator included
@@ -48,24 +51,28 @@ On Windows run `.bat` files with double-click\
For Windows, you can download it from [oracle.com](https://www.oracle.com/java/technologies/downloads/#jdk17-windows) (select x64 Installer).
### Install
1. Arch linux ![Arch Linux package](https://img.shields.io/archlinux/v/extra/any/jadx?label=)
```bash
sudo pacman -S jadx
```
2. macOS ![homebrew version](https://img.shields.io/homebrew/v/jadx?label=)
```bash
brew install jadx
```
3. [Flathub ![Flathub](https://img.shields.io/flathub/v/com.github.skylot.jadx?label=)](https://flathub.org/apps/details/com.github.skylot.jadx)
```bash
flatpak install flathub com.github.skylot.jadx
```
- Arch Linux
[![Arch Linux package](https://img.shields.io/archlinux/v/extra/any/jadx)](https://archlinux.org/packages/extra/any/jadx/)
[![AUR Version](https://img.shields.io/aur/version/jadx-git)](https://aur.archlinux.org/packages/jadx-git)
```bash
sudo pacman -S jadx
```
- macOS
[![homebrew version](https://img.shields.io/homebrew/v/jadx)](https://formulae.brew.sh/formula/jadx)
```bash
brew install jadx
```
- Flathub
[![Flathub Version](https://img.shields.io/flathub/v/com.github.skylot.jadx)](https://flathub.org/apps/com.github.skylot.jadx)
```bash
flatpak install flathub com.github.skylot.jadx
```
### Use jadx as a library
You can use jadx in your java projects, check details on [wiki page](https://github.com/skylot/jadx/wiki/Use-jadx-as-a-library)
### Build from source
JDK 11 or higher must be installed:
JDK 17 or higher must be installed:
```
git clone https://github.com/skylot/jadx.git
cd jadx
@@ -79,104 +86,134 @@ 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[-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-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-use-sourcename - use source file name as class name alias
--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-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>):
1) dex-input: Load .dex and .apk files
- dex-input.verify-checksum - verify dex file checksum before load, values: [yes, no], default: yes
2) 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
3) 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
4) rename-mappings: various mappings support
- rename-mappings.format - mapping format, values: [auto, TINY, TINY_2, ENIGMA, ENIGMA_DIR, MCP, SRG, TSRG, TSRG2, PROGUARD], default: auto
- rename-mappings.invert - invert mapping, values: [yes, no], default: no
dex-input: Load .dex and .apk files
- 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
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-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, 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
Environment variables:
JADX_DISABLE_XML_SECURITY - set to 'true' to disable all security checks for XML files
JADX_DISABLE_ZIP_SECURITY - set to 'true' to disable all security checks for zip files
JADX_ZIP_MAX_ENTRIES_COUNT - maximum allowed number of entries in zip files (default: 100 000)
JADX_CONFIG_DIR - custom config directory, using system by default
JADX_CACHE_DIR - custom cache directory, using system by default
JADX_TMP_DIR - custom temp directory, using system by default
Examples:
@@ -186,7 +223,25 @@ Examples:
jadx --log-level ERROR app.apk
jadx -Pdex-input.verify-checksum=no app.apk
```
These options also worked on jadx-gui running from command line and override options from preferences dialog
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)
+4 -3
View File
@@ -2,6 +2,7 @@
## Reporting a Vulnerability
To report a security issue, please email `skylot@gmail.com` with a description of the issue, the steps you took to create the issue, affected versions, and, if known, mitigations for the issue.
We will check and respond within 3 working days. If the issue is confirmed as a vulnerability, we will apply required mitigations at the next release.
This project follows a 90 day disclosure timeline.
To report a security issue, please open a [new security advisory](https://github.com/skylot/jadx/security/advisories/new).
Please fill the steps you took to create the issue, affected versions, and, if known, mitigations for the issue.
We will check and respond within 3 working days.
If the issue is confirmed as a vulnerability, we will apply required mitigations at the next release.
+98 -29
View File
@@ -6,15 +6,47 @@ 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
val jadxBuildJavaVersion by extra { getBuildJavaVersion() }
fun getBuildJavaVersion(): Int? {
val envVarName = "JADX_BUILD_JAVA_VERSION"
val buildJavaVer = jadxEnv[envVarName]?.toInt() ?: return null
if (buildJavaVer < 11) {
throw GradleException("'$envVarName' can't be set to lower than 11")
}
println("Set Java toolchain for jadx build to version '$buildJavaVer'")
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")
@@ -70,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")
}
@@ -89,6 +158,10 @@ val copyArtifacts by tasks.registering(Copy::class) {
include("**/*.jar")
rename("jadx-gui-(.*)-all.jar", "jadx-$1-all.jar")
}
from(layout.projectDirectory) {
include("README.md")
include("LICENSE")
}
into(layout.buildDirectory.dir("jadx"))
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
@@ -97,42 +170,39 @@ 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 copyExe by tasks.registering(Copy::class) {
val distWin by tasks.registering(Zip::class) {
group = "jadx"
description = "Copy exe to build dir"
description = "Build Windows bundle"
// next task dependencies not needed, but gradle throws warning because of same output dir
mustRunAfter("jar")
mustRunAfter(pack)
from(distWinConfiguration)
from(tasks.getByPath("jadx-gui:createExe"))
include("*.exe")
into(layout.buildDirectory)
destinationDirectory.set(layout.buildDirectory.dir("distWin"))
archiveFileName.set("jadx-gui-$jadxVersion-win.zip")
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
val distWinBundle by tasks.registering(Copy::class) {
group = "jadx"
description = "Copy bundle to build dir"
val distWinWithJre by tasks.registering(Zip::class) {
description = "Build Windows with JRE bundle"
dependsOn(tasks.getByPath(":jadx-gui:distWinWithJre"))
from(distWinWithJreConfiguration)
// next task dependencies not needed, but gradle throws warning because of same output dir
mustRunAfter("jar")
mustRunAfter(pack)
from(tasks.getByPath("jadx-gui:distWinWithJre").outputs) {
include("*.zip")
}
into(layout.buildDirectory)
destinationDirectory.set(layout.buildDirectory.dir("distWinWithJre"))
archiveFileName.set("jadx-gui-$jadxVersion-with-jre-win.zip")
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
val dist by tasks.registering {
group = "jadx"
description = "Build jadx distribution zip"
description = "Build jadx distribution zip bundles"
dependsOn(pack)
@@ -140,15 +210,14 @@ val dist by tasks.registering {
if (os.isWindows) {
if (project.hasProperty("bundleJRE")) {
println("Build win bundle with JRE")
dependsOn(distWinBundle)
dependsOn(distWinWithJre)
} else {
dependsOn(copyExe)
dependsOn(distWin)
}
}
}
val cleanBuildDir by tasks.registering(Delete::class) {
group = "jadx"
delete(layout.buildDirectory)
}
tasks.getByName("clean").dependsOn(cleanBuildDir)
+5 -1
View File
@@ -3,7 +3,11 @@ plugins {
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22")
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 {
+48 -8
View File
@@ -1,28 +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.11")
compileOnly("org.jetbrains:annotations:24.1.0")
implementation("org.slf4j:slf4j-api:2.0.18")
compileOnly("org.jetbrains:annotations:26.1.0")
testImplementation("ch.qos.logback:logback-classic:1.4.14")
testImplementation("org.hamcrest:hamcrest-library:2.2")
testImplementation("org.mockito:mockito-core:5.10.0")
testImplementation("org.assertj:assertj-core:3.25.2")
testImplementation("ch.qos.logback:logback-classic:1.5.34")
testImplementation("org.assertj:assertj-core:3.27.7")
testImplementation("org.junit.jupiter:junit-jupiter:5.10.1")
testImplementation("org.junit.jupiter:junit-jupiter:5.13.3")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testCompileOnly("org.jetbrains:annotations:24.1.0")
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 {
@@ -32,6 +42,11 @@ repositories {
}
java {
jadxBuildJavaVersion?.let { buildJavaVer ->
toolchain {
languageVersion = JavaLanguageVersion.of(buildJavaVer)
}
}
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
@@ -39,6 +54,7 @@ java {
tasks {
compileJava {
options.encoding = "UTF-8"
// options.compilerArgs = listOf("-Xlint:deprecation")
}
jar {
manifest {
@@ -55,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)
@@ -30,7 +30,7 @@ publishing {
}
pom {
name.set(project.name)
description.set("Dex to Java decompiler")
description.set(project.description ?: "Dex to Java decompiler")
url.set("https://github.com/skylot/jadx")
licenses {
license {
@@ -42,14 +42,14 @@ publishing {
developer {
id.set("skylot")
name.set("Skylot")
email.set("skylot@gmail.com")
email.set(project.properties["libEmail"].toString())
url.set("https://github.com/skylot")
}
}
scm {
connection .set("scm:git:git://github.com/skylot/jadx.git")
connection.set("scm:git:git://github.com/skylot/jadx.git")
developerConnection.set("scm:git:ssh://github.com:skylot/jadx.git")
url .set("https://github.com/skylot/jadx")
url.set("https://github.com/skylot/jadx")
}
}
}
@@ -0,0 +1,35 @@
plugins {
id("org.openrewrite.rewrite")
}
repositories {
mavenCentral()
}
dependencies {
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 {
rewrite {
// exclusion("src/test/java/jadx/tests/integration")
// activeRecipe("org.openrewrite.java.migrate.Java8toJava11")
// checkstyle auto fix
// activeRecipe("org.openrewrite.staticanalysis.CodeCleanup")
// setCheckstyleConfigFile(file("$rootDir/config/checkstyle/checkstyle.xml"))
// logging
// activeRecipe("org.openrewrite.java.logging.slf4j.Slf4jBestPractices")
// activeRecipe("org.openrewrite.java.logging.slf4j.LoggersNamedForEnclosingClass")
// activeRecipe("org.openrewrite.java.logging.slf4j.ParameterizedLogging")
// activeRecipe("org.openrewrite.java.logging.PrintStackTraceToLogError")
// testing
activeRecipe("org.openrewrite.java.testing.assertj.Assertj")
}
}
+9 -2
View File
@@ -120,15 +120,22 @@
<module name="SuppressWarningsHolder"/>
<module name="IllegalType"/>
<module name="IllegalType">
<property name="illegalClassNames" value="java.util.ArrayList, java.util.HashMap, java.util.HashSet,
java.util.LinkedHashMap, java.util.LinkedHashSet, java.util.TreeMap, java.util.TreeSet"/>
</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"/>
<property name="format" value="\.printStackTrace\(\)"/>
<property name="ignoreComments" value="true"/>
<property name="message" value="Using Throwable.printStackTrace() is forbidden. Use logger to print exception"/>
<property name="message"
value="Using Throwable.printStackTrace() is forbidden. Use logger to print exception"/>
</module>
</module>
+2 -4
View File
@@ -1,7 +1,7 @@
/*
* Generated on 11/22/21, 8:58 PM
*/
package jadx.gui.ui.codeearea;
package jadx.gui.ui.codearea;
import java.io.*;
import javax.swing.text.Segment;
@@ -9,7 +9,7 @@ import javax.swing.text.Segment;
import org.fife.ui.rsyntaxtextarea.*;
/**
/*
* 用于Smali代码高亮
* MartinKay@qq.com
*/
@@ -173,7 +173,6 @@ import org.fife.ui.rsyntaxtextarea.*;
zzAtEOF = false;
}
%}
Letter = [A-Za-z]
@@ -678,4 +677,3 @@ FLAG_ARRAY = (":array_"{SimpleName})
\n { addToken(start,zzStartRead-1, Token.COMMENT_EOL); addNullToken(); return firstToken; }
<<EOF>> { addToken(start,zzStartRead-1, Token.COMMENT_EOL); addNullToken(); return firstToken; }
}
+16 -13
View File
@@ -57,12 +57,12 @@
private int yychar;
/**
* the number of characters from the last newline up to the start of the
* the number of characters from the last newline up to the start of the
* matched text
*/
private int yycolumn;
/**
/**
* zzAtBOL == true <=> the scanner is currently at the beginning of a line
*/
private boolean zzAtBOL = true;
@@ -102,6 +102,9 @@
zzLexicalState = newState;
}
public final int yystate() {
return zzLexicalState;
}
/**
* Returns the text matched by the current regular expression.
@@ -112,12 +115,12 @@
/**
* Returns the character at position <tt>pos</tt> from the
* matched text.
*
* Returns the character at position <tt>pos</tt> from the
* matched text.
*
* It is equivalent to yytext().charAt(pos), but faster
*
* @param pos the position of the character to fetch.
* @param pos the position of the character to fetch.
* A value from 0 to yylength()-1.
*
* @return the character at position pos
@@ -138,8 +141,8 @@
/**
* Reports an error that occured while scanning.
*
* In a wellformed scanner (no or only correct usage of
* yypushback(int) and a match-all fallback rule) this method
* In a wellformed scanner (no or only correct usage of
* yypushback(int) and a match-all fallback rule) this method
* will only be called with things that "Can't Possibly Happen".
* If this method is called, something is seriously wrong
* (e.g. a JFlex bug producing a faulty scanner etc.).
@@ -159,7 +162,7 @@
}
--- throws clause
}
}
/**
@@ -206,12 +209,12 @@
zzAction = -1;
zzCurrentPosL = zzCurrentPos = zzStartRead = zzMarkedPosL;
--- start admin (lexstate etc)
zzForAction: {
while (true) {
--- next input, line, col, char count, next transition, isFinal action
zzAction = zzState;
zzMarkedPosL = zzCurrentPosL;
@@ -226,11 +229,11 @@
--- char count update
--- actions
default:
default:
if (zzInput == YYEOF && zzStartRead == zzCurrentPos) {
zzAtEOF = true;
--- eofvalue
}
}
else {
--- no match
}
+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=9d926787066a081739e8200858338b4a69e837c3a821a33aca9db09dd4a41026
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-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
+7 -8
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.
@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/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/.
@@ -84,7 +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 "${APP_HOME:-./}" > /dev/null && pwd -P ) || 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
@@ -112,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.
@@ -170,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" )
@@ -203,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
+13 -12
View File
@@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
@@ -57,22 +59,21 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
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
+15 -4
View File
@@ -1,14 +1,17 @@
plugins {
id("jadx-java")
id("jadx-library")
id("application")
// use shadow only for application scripts, jar will be copied from jadx-gui
id("com.github.johnrengelman.shadow") version "8.1.1"
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"))
@@ -16,11 +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:1.83")
implementation("ch.qos.logback:logback-classic:1.4.14")
implementation("org.jcommander:jcommander:2.0")
implementation("ch.qos.logback:logback-classic:1.5.34")
implementation("com.google.code.gson:gson:2.14.0")
}
application {
@@ -28,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")
@@ -4,7 +4,7 @@ import java.io.PrintStream;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -13,11 +13,11 @@ import java.util.function.Supplier;
import org.jetbrains.annotations.Nullable;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.ParameterDescription;
import com.beust.jcommander.ParameterException;
import com.beust.jcommander.Parameterized;
import jadx.api.JadxArgs;
import jadx.api.JadxDecompiler;
import jadx.api.plugins.JadxPluginInfo;
import jadx.api.plugins.options.JadxPluginOptions;
@@ -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() {
@@ -109,8 +150,11 @@ public class JCommanderWrapper<T> {
out.println(appendPluginOptions(maxNamesLen));
out.println();
out.println("Environment variables:");
out.println(" JADX_DISABLE_XML_SECURITY - set to 'true' to disable all security checks for XML files");
out.println(" JADX_DISABLE_ZIP_SECURITY - set to 'true' to disable all security checks for zip files");
out.println(" JADX_ZIP_MAX_ENTRIES_COUNT - maximum allowed number of entries in zip files (default: 100 000)");
out.println(" JADX_CONFIG_DIR - custom config directory, using system by default");
out.println(" JADX_CACHE_DIR - custom cache directory, using system by default");
out.println(" JADX_TMP_DIR - custom temp directory, using system by default");
out.println();
out.println("Examples:");
@@ -131,14 +175,16 @@ public class JCommanderWrapper<T> {
out.println("options:");
List<ParameterDescription> params = jc.getParameters();
Map<String, ParameterDescription> paramsMap = new LinkedHashMap<>(params.size());
Map<String, ParameterDescription> paramsMap = new HashMap<>(params.size());
int maxNamesLen = 0;
for (ParameterDescription p : params) {
paramsMap.put(p.getParameterized().getName(), p);
int len = p.getNames().length();
if (len > maxNamesLen) {
maxNamesLen = len;
String valueDesc = getValueDesc(p);
if (valueDesc != null) {
len += 1 + valueDesc.length();
}
maxNamesLen = Math.max(maxNamesLen, len);
}
maxNamesLen += 3;
@@ -151,8 +197,12 @@ public class JCommanderWrapper<T> {
}
StringBuilder opt = new StringBuilder();
opt.append(" ").append(p.getNames());
String description = p.getDescription();
String valueDesc = getValueDesc(p);
if (valueDesc != null) {
opt.append(' ').append(valueDesc);
}
addSpaces(opt, maxNamesLen - opt.length());
String description = p.getDescription();
if (description.contains("\n")) {
String[] lines = description.split("\n");
opt.append("- ").append(lines[0]);
@@ -165,8 +215,10 @@ public class JCommanderWrapper<T> {
opt.append("- ").append(description);
}
if (addDefaults) {
String defaultValue = getDefaultValue(args, f, opt);
if (defaultValue != null && !description.contains("(default)")) {
String defaultValue = getDefaultValue(args, f);
if (defaultValue != null
&& !defaultValue.isEmpty()
&& !description.contains("(default)")) {
opt.append(", default: ").append(defaultValue);
}
}
@@ -175,6 +227,11 @@ public class JCommanderWrapper<T> {
return maxNamesLen;
}
private static @Nullable String getValueDesc(ParameterDescription p) {
Parameter parameterAnnotation = p.getParameterAnnotation();
return parameterAnnotation == null ? null : parameterAnnotation.defaultValueDescription();
}
/**
* Get all declared fields of the specified class and all super classes
*/
@@ -188,7 +245,7 @@ public class JCommanderWrapper<T> {
}
@Nullable
private static String getDefaultValue(Object args, Field f, StringBuilder opt) {
private static String getDefaultValue(Object args, Field f) {
try {
Class<?> fieldType = f.getType();
if (fieldType == int.class) {
@@ -217,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(new JadxArgs())) {
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)) {
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) {
@@ -238,12 +296,12 @@ public class JCommanderWrapper<T> {
return "\nPlugin options (-P<name>=<value>):" + sb;
}
private boolean appendPlugin(JadxPluginInfo pluginInfo, JadxPluginOptions options, StringBuilder out, int maxNamesLen, int k) {
private boolean appendPlugin(JadxPluginInfo pluginInfo, JadxPluginOptions options, StringBuilder out, int maxNamesLen) {
List<OptionDescription> descs = options.getOptionsDescriptions();
if (descs.isEmpty()) {
return false;
}
out.append("\n ").append(k).append(") ");
out.append("\n ");
out.append(pluginInfo.getPluginId()).append(": ").append(pluginInfo.getDescription());
for (OptionDescription desc : descs) {
StringBuilder opt = new StringBuilder();
@@ -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));
}
}
+75 -25
View File
@@ -1,58 +1,83 @@
package jadx.cli;
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.usage.impl.EmptyUsageInfoCache;
import jadx.cli.LogHelper.LogLevelEnum;
import jadx.cli.config.JadxConfigAdapter;
import jadx.cli.plugins.JadxFilesGetter;
import jadx.core.utils.exceptions.JadxArgsValidateException;
import jadx.core.utils.files.FileUtils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.plugins.tools.JadxExternalPluginsLoader;
public class JadxCLI {
private static final Logger LOG = LoggerFactory.getLogger(JadxCLI.class);
public static void main(String[] args) {
int result = 0;
int result = 1;
try {
result = execute(args);
} catch (JadxArgsValidateException e) {
LOG.error("Incorrect arguments: {}", e.getMessage());
result = 1;
} catch (Throwable e) {
LOG.error("Process error:", e);
result = 1;
} finally {
FileUtils.deleteTempRootDir();
System.exit(result);
}
}
public static int execute(String[] args) {
JadxCLIArgs jadxArgs = new JadxCLIArgs();
if (jadxArgs.processArgs(args)) {
return processAndSave(jadxArgs);
}
return 0;
return execute(args, null);
}
private static int processAndSave(JadxCLIArgs cliArgs) {
LogHelper.initLogLevel(cliArgs);
LogHelper.setLogLevelsForLoadingStage();
public static int execute(String[] args, @Nullable Consumer<JadxArgs> argsMod) {
try {
JadxCLIArgs cliArgs = JadxCLIArgs.processArgs(args,
new JadxCLIArgs(),
new JadxConfigAdapter<>(JadxCLIArgs.class, "cli"));
if (cliArgs == null) {
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;
} catch (Throwable e) {
LOG.error("Process error:", e);
return 1;
}
}
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);
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);
}
@@ -60,11 +85,11 @@ public class JadxCLI {
if (errorsCount != 0) {
jadx.printErrorsReport();
LOG.error("finished with errors, count: {}", errorsCount);
} else {
LOG.info("done");
return 3;
}
LOG.info("done");
return 0;
}
return 0;
}
private static void initCodeWriterProvider(JadxArgs jadxArgs) {
@@ -90,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;
}
@@ -111,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());
}
}
+479 -41
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;
@@ -27,22 +31,35 @@ import jadx.api.JadxDecompiler;
import jadx.api.args.GeneratedRenamesMappingFileMode;
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.utils.exceptions.JadxException;
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)")
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;
@@ -52,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" },
@@ -108,6 +138,9 @@ public class JadxCLIArgs {
@Parameter(names = "--no-finally", description = "don't extract finally block")
protected boolean extractFinally = true;
@Parameter(names = "--no-restore-switch-over-string", description = "don't restore switch over string")
protected boolean restoreSwitchOverString = true;
@Parameter(names = "--no-replace-consts", description = "don't replace constant value with matching constant field")
protected boolean replaceConsts = true;
@@ -148,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),"
@@ -166,8 +200,15 @@ public class JadxCLIArgs {
)
protected GeneratedRenamesMappingFileMode generatedRenamesMappingFileMode = GeneratedRenamesMappingFileMode.getDefault();
@Parameter(names = { "--deobf-use-sourcename" }, description = "use source file name as class name alias")
protected boolean deobfuscationUseSourceNameAsAlias = false;
@SuppressWarnings("DeprecatedIsStillUsed")
@Parameter(
names = { "--deobf-use-sourcename" },
description = "use source file name as class name alias."
+ "\nDEPRECATED, use \"--use-source-name-as-class-name-alias\" instead",
hidden = true
)
@Deprecated
protected Boolean deobfuscationUseSourceNameAsAlias = null;
@Parameter(
names = { "--deobf-res-name-source" },
@@ -179,6 +220,22 @@ public class JadxCLIArgs {
)
protected ResourceNameSource resourceNameSource = ResourceNameSource.AUTO;
@Parameter(
names = { "--use-source-name-as-class-name-alias" },
description = "use source name as class name alias:"
+ "\n 'always' - always use source name if it's available"
+ "\n 'if-better' - use source name if it seems better than the current one"
+ "\n 'never' - never use source name, even if it's available",
converter = UseSourceNameAsClassNameConverter.class
)
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",
@@ -186,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):"
@@ -194,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);
@@ -208,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;
@@ -217,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;
@@ -237,45 +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;
}
@@ -287,18 +428,38 @@ public class JadxCLIArgs {
System.out.println(JadxDecompiler.getVersion());
return false;
}
try {
if (threadsCount <= 0) {
throw new JadxException("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);
}
} catch (JadxException e) {
System.err.println("ERROR: " + e.getMessage());
jcw.printUsage();
return false;
}
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()));
@@ -328,12 +489,17 @@ public class JadxCLIArgs {
args.setDeobfuscationMinLength(deobfuscationMinLength);
args.setDeobfuscationMaxLength(deobfuscationMaxLength);
args.setDeobfuscationWhitelist(Arrays.asList(deobfuscationWhitelistStr.split(" ")));
args.setUseSourceNameAsClassAlias(deobfuscationUseSourceNameAsAlias);
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);
@@ -343,19 +509,32 @@ public class JadxCLIArgs {
args.setMoveInnerClasses(moveInnerClasses);
args.setAllowInlineKotlinLambda(allowInlineKotlinLambda);
args.setExtractFinally(extractFinally);
args.setRenameFlags(renameFlags);
args.setRestoreSwitchOverString(restoreSwitchOverString);
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;
}
@@ -380,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;
}
@@ -396,122 +587,293 @@ 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;
} else if (deobfuscationUseSourceNameAsAlias != null) {
// noinspection deprecation
return UseSourceNameAsClassNameAlias.create(deobfuscationUseSourceNameAsAlias);
} else {
return UseSourceNameAsClassNameAlias.getDefault();
}
}
public void setUseSourceNameAsClassNameAlias(UseSourceNameAsClassNameAlias useSourceNameAsClassNameAlias) {
this.useSourceNameAsClassNameAlias = useSourceNameAsClassNameAlias;
}
/**
* @deprecated Use {@link #getUseSourceNameAsClassNameAlias()} instead.
*/
@Deprecated
public boolean isDeobfuscationUseSourceNameAsAlias() {
return deobfuscationUseSourceNameAsAlias;
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);
}
@@ -528,18 +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;
@@ -559,8 +973,8 @@ public class JadxCLIArgs {
for (String s : value.split(",")) {
try {
set.add(RenameEnum.valueOf(s.trim().toUpperCase(Locale.ROOT)));
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(
} catch (Exception e) {
throw new JadxArgsValidateException(
'\'' + s + "' is unknown for parameter " + paramName
+ ", possible values are " + enumValuesString(RenameEnum.values()));
}
@@ -593,12 +1007,24 @@ public class JadxCLIArgs {
}
}
public static class UseSourceNameAsClassNameConverter extends BaseEnumConverter<UseSourceNameAsClassNameAlias> {
public UseSourceNameAsClassNameConverter() {
super(UseSourceNameAsClassNameAlias::valueOf, UseSourceNameAsClassNameAlias::values);
}
}
public static class DecompilationModeConverter extends BaseEnumConverter<DecompilationMode> {
public DecompilationModeConverter() {
super(DecompilationMode::valueOf, DecompilationMode::values);
}
}
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);
@@ -611,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;
@@ -625,7 +1063,7 @@ public class JadxCLIArgs {
try {
return parse.apply(stringAsEnumName(value));
} catch (Exception e) {
throw new IllegalArgumentException(
throw new JadxArgsValidateException(
'\'' + value + "' is unknown, possible values are: " + enumValuesString(values.get()));
}
}
@@ -1,15 +1,16 @@
package jadx.cli;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;
import com.beust.jcommander.JCommander;
import jadx.cli.commands.CommandPlugins;
import jadx.cli.commands.ICommand;
import jadx.core.utils.exceptions.JadxArgsValidateException;
public class JadxCLICommands {
private static final Map<String, ICommand> COMMANDS_MAP = new TreeMap<>();
private static final Map<String, ICommand> COMMANDS_MAP = new LinkedHashMap<>();
static {
JadxCLICommands.register(new CommandPlugins());
@@ -23,10 +24,11 @@ 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 IllegalArgumentException("Unknown command: " + parsedCommand);
throw new JadxArgsValidateException("Unknown command: " + parsedCommand
+ ". Expected one of: " + COMMANDS_MAP.keySet());
}
JCommander subCommander = jc.getCommands().get(parsedCommand);
command.process(jcw, subCommander);
+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) {
@@ -12,6 +12,7 @@ import jadx.api.JadxDecompiler;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.visitors.SaveCode;
import jadx.core.utils.exceptions.JadxArgsValidateException;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
@@ -33,10 +34,10 @@ public class SingleClassMode {
.findFirst().orElse(null);
}
if (clsForProcess == null) {
throw new JadxRuntimeException("Input class not found: " + singleClass);
throw new JadxArgsValidateException("Input class not found: " + singleClass);
}
if (clsForProcess.contains(AFlag.DONT_GENERATE)) {
throw new JadxRuntimeException("Input class can't be saved by current jadx settings (marked as DONT_GENERATE)");
throw new JadxArgsValidateException("Input class can't be saved by current jadx settings (marked as DONT_GENERATE)");
}
if (clsForProcess.isInner()) {
clsForProcess = clsForProcess.getTopParentClass();
@@ -52,7 +53,7 @@ public class SingleClassMode {
if (size == 1) {
clsForProcess = classes.get(0);
} else {
throw new JadxRuntimeException("Found " + size + " classes, single class output can't be used");
throw new JadxArgsValidateException("Found " + size + " classes, single class output can't be used");
}
}
ICodeInfo codeInfo;
@@ -12,6 +12,7 @@ import org.slf4j.LoggerFactory;
import jadx.api.JadxArgs;
import jadx.api.JadxDecompiler;
import jadx.api.args.UseSourceNameAsClassNameAlias;
import jadx.core.clsp.ClsSet;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.files.FileUtils;
@@ -23,8 +24,9 @@ public class ConvertToClsSet {
private static final Logger LOG = LoggerFactory.getLogger(ConvertToClsSet.class);
public static void usage() {
LOG.info("<output .jcst file> <several input dex or jar files> ");
LOG.info("<android API level (number)> <output .jcst file> <several input dex or jar files> ");
LOG.info("Arguments to update core.jcst: "
+ "<android API level (number)> "
+ "<jadx root>/jadx-core/src/main/resources/clst/core.jcst "
+ "<sdk_root>/platforms/android-<api level>/android.jar"
+ "<sdk_root>/platforms/android-<api level>/optional/android.car.jar "
@@ -32,11 +34,12 @@ public class ConvertToClsSet {
}
public static void main(String[] args) {
if (args.length < 2) {
if (args.length != 5) {
usage();
System.exit(1);
}
List<Path> inputPaths = Stream.of(args).map(Paths::get).collect(Collectors.toList());
int androidApiLevel = Integer.parseInt(args[0]);
List<Path> inputPaths = Stream.of(args).skip(1).map(Paths::get).collect(Collectors.toList());
Path output = inputPaths.remove(0);
JadxArgs jadxArgs = new JadxArgs();
@@ -45,7 +48,7 @@ public class ConvertToClsSet {
// disable not needed passes executed at prepare stage
jadxArgs.setDeobfuscationOn(false);
jadxArgs.setRenameFlags(EnumSet.noneOf(JadxArgs.RenameEnum.class));
jadxArgs.setUseSourceNameAsClassAlias(false);
jadxArgs.setUseSourceNameAsClassNameAlias(UseSourceNameAsClassNameAlias.NEVER);
jadxArgs.setMoveInnerClasses(false);
jadxArgs.setInlineAnonymousClasses(false);
jadxArgs.setInlineMethods(false);
@@ -57,6 +60,7 @@ public class ConvertToClsSet {
decompiler.load();
RootNode root = decompiler.getRoot();
ClsSet set = new ClsSet(root);
set.setAndroidApiLevel(androidApiLevel);
set.loadFrom(root);
set.save(output);
@@ -1,38 +1,61 @@
package jadx.cli.commands;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import jadx.api.plugins.JadxPluginInfo;
import jadx.cli.JCommanderWrapper;
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;
@Parameters(commandDescription = "manage jadx plugins")
public class CommandPlugins implements ICommand {
@Parameter(names = { "-i", "--install" }, description = "install plugin with locationId")
@Parameter(names = { "-i", "--install" }, description = "install plugin with locationId", defaultValueDescription = "<locationId>")
protected String install;
@Parameter(names = { "-j", "--install-jar" }, description = "install plugin from jar file")
@Parameter(names = { "-j", "--install-jar" }, description = "install plugin from jar file", defaultValueDescription = "<path-to.jar>")
protected String installJar;
@Parameter(names = { "-l", "--list" }, description = "list installed plugins")
protected boolean list;
@Parameter(names = { "-a", "--available" }, description = "list available plugins")
@Parameter(names = { "-a", "--available" }, description = "list available plugins from jadx-plugins-list (aka marketplace)")
protected boolean available;
@Parameter(names = { "-u", "--update" }, description = "update installed plugins")
protected boolean update;
@Parameter(names = { "--uninstall" }, description = "uninstall plugin with pluginId")
@Parameter(names = { "--uninstall" }, description = "uninstall plugin with pluginId", defaultValueDescription = "<pluginId>")
protected String uninstall;
@Parameter(names = { "--disable" }, description = "disable plugin with pluginId", defaultValueDescription = "<pluginId>")
protected String disable;
@Parameter(names = { "--enable" }, description = "enable plugin with pluginId", defaultValueDescription = "<pluginId>")
protected String enable;
@Parameter(names = { "--list-all" }, description = "list all plugins including bundled and dropins")
protected boolean listAll;
@Parameter(
names = { "--list-versions" },
description = "fetch latest versions of plugin from locationId (will download all artefacts, limited to 10)",
defaultValueDescription = "<locationId>"
)
protected String listVersions;
@Parameter(names = { "-h", "--help" }, description = "print this help", help = true)
protected boolean printHelp = false;
@@ -41,21 +64,33 @@ public class CommandPlugins implements ICommand {
return "plugins";
}
@SuppressWarnings("UnnecessaryReturnStatement")
@Override
public void process(JCommanderWrapper<?> jcw, JCommander subCommander) {
public void process(JCommanderWrapper jcw, JCommander subCommander) {
if (printHelp) {
jcw.printUsage(subCommander);
return;
}
Set<String> unknownOptions = new HashSet<>(subCommander.getUnknownOptions());
boolean verbose = unknownOptions.remove("-v") || unknownOptions.remove("--verbose");
LogHelper.setLogLevel(verbose ? LogHelper.LogLevelEnum.DEBUG : LogHelper.LogLevelEnum.INFO);
if (!unknownOptions.isEmpty()) {
System.out.println("Error: found unknown options: " + unknownOptions);
}
if (install != null) {
installPlugin(install);
return;
}
if (installJar != null) {
installPlugin("file:" + installJar);
return;
}
if (uninstall != null) {
boolean uninstalled = JadxPluginsTools.getInstance().uninstall(uninstall);
System.out.println(uninstalled ? "Uninstalled" : "Plugin not found");
return;
}
if (update) {
List<JadxPluginUpdate> updates = JadxPluginsTools.getInstance().updateAll();
@@ -67,28 +102,113 @@ public class CommandPlugins implements ICommand {
System.out.println(" " + update.getPluginId() + ": " + update.getOldVersion() + " -> " + update.getNewVersion());
}
}
return;
}
if (list) {
List<JadxPluginMetadata> installed = JadxPluginsTools.getInstance().getInstalled();
System.out.println("Installed plugins: " + installed.size());
int i = 1;
for (JadxPluginMetadata plugin : installed) {
System.out.println(" " + (i++) + ") "
+ plugin.getPluginId() + " (" + plugin.getVersion() + ") - "
+ plugin.getName() + ": " + plugin.getDescription());
}
printPlugins(JadxPluginsTools.getInstance().getInstalled());
return;
}
if (listAll) {
printAllPlugins();
return;
}
if (listVersions != null) {
printVersions(listVersions, 10);
return;
}
if (available) {
List<JadxPluginMetadata> availableList = JadxPluginsList.getInstance().get();
List<JadxPluginListEntry> availableList = JadxPluginsList.getInstance().get();
System.out.println("Available plugins: " + availableList.size());
int i = 1;
for (JadxPluginMetadata plugin : availableList) {
System.out.println(" " + (i++) + ") "
+ plugin.getName() + ": " + plugin.getDescription()
for (JadxPluginListEntry plugin : availableList) {
System.out.println(" - " + plugin.getName() + ": " + plugin.getDescription()
+ " (" + plugin.getLocationId() + ")");
}
return;
}
if (disable != null) {
if (JadxPluginsTools.getInstance().changeDisabledStatus(disable, true)) {
System.out.println("Plugin '" + disable + "' disabled.");
} else {
System.out.println("Plugin '" + disable + "' already disabled.");
}
return;
}
if (enable != null) {
if (JadxPluginsTools.getInstance().changeDisabledStatus(enable, false)) {
System.out.println("Plugin '" + enable + "' enabled.");
} else {
System.out.println("Plugin '" + enable + "' already enabled.");
}
return;
}
}
private static void printPlugins(List<JadxPluginMetadata> installed) {
System.out.println("Installed plugins: " + installed.size());
for (JadxPluginMetadata plugin : installed) {
StringBuilder sb = new StringBuilder();
sb.append(" - ").append(plugin.getPluginId());
String version = plugin.getVersion();
if (version != null) {
sb.append(" (").append(version).append(')');
}
if (plugin.isDisabled()) {
sb.append(" (disabled)");
}
sb.append(" - ").append(plugin.getName());
sb.append(": ").append(formatDescription(plugin.getDescription()));
System.out.println(sb);
}
}
private void printVersions(String locationId, int limit) {
System.out.println("Loading ...");
List<JadxPluginMetadata> versions = JadxPluginsTools.getInstance().getVersionsByLocation(locationId, 1, limit);
if (versions.isEmpty()) {
System.out.println("No versions found");
return;
}
JadxPluginMetadata plugin = versions.get(0);
System.out.println("Versions for plugin id: " + plugin.getPluginId());
for (JadxPluginMetadata version : versions) {
StringBuilder sb = new StringBuilder();
sb.append(" - ").append(version.getVersion());
String reqVer = version.getRequiredJadxVersion();
if (StringUtils.notBlank(reqVer)) {
sb.append(", require jadx: ").append(reqVer);
}
System.out.println(sb);
}
}
private static void printAllPlugins() {
List<JadxPluginMetadata> installed = JadxPluginsTools.getInstance().getInstalled();
printPlugins(installed);
Set<String> installedSet = installed.stream().map(JadxPluginMetadata::getPluginId).collect(Collectors.toSet());
List<JadxPluginInfo> plugins = JadxPluginsTools.getInstance().getAllPluginsInfo();
System.out.println("Other plugins: " + plugins.size());
for (JadxPluginInfo plugin : plugins) {
if (!installedSet.contains(plugin.getPluginId())) {
System.out.println(" - " + plugin.getPluginId()
+ " - " + plugin.getName()
+ ": " + 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) {
@@ -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 {
}
@@ -0,0 +1,31 @@
package jadx.cli.plugins;
import java.nio.file.Path;
import jadx.commons.app.JadxCommonFiles;
import jadx.commons.app.JadxTempFiles;
import jadx.core.plugins.files.IJadxFilesGetter;
public class JadxFilesGetter implements IJadxFilesGetter {
public static final JadxFilesGetter INSTANCE = new JadxFilesGetter();
@Override
public Path getConfigDir() {
return JadxCommonFiles.getConfigDir();
}
@Override
public Path getCacheDir() {
return JadxCommonFiles.getCacheDir();
}
@Override
public Path getTempDir() {
return JadxTempFiles.getTempRootDir();
}
private JadxFilesGetter() {
// singleton
}
}
@@ -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,8 +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 java.util.zip.ZipFile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -20,7 +17,12 @@ import org.slf4j.LoggerFactory;
import jadx.api.JadxArgs;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.android.TextResMapFile;
import jadx.core.xmlgen.ResTableParser;
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;
/**
* Utility class for convert '.arsc' to simple text file with mapping id to resource name
@@ -30,7 +32,7 @@ public class ConvertArscFile {
private static int rewritesCount;
public static void usage() {
LOG.info("<res-map file> <input .arsc files>");
LOG.info("<res-map file> <input .arsc/android.jar files or dir>");
LOG.info("");
LOG.info("Note: If res-map already exists - it will be merged and updated");
}
@@ -42,6 +44,7 @@ public class ConvertArscFile {
}
List<Path> inputPaths = Stream.of(args).map(Paths::get).collect(Collectors.toList());
Path resMapFile = inputPaths.remove(0);
List<Path> inputResFiles = filterAndSort(expandDirs(inputPaths));
Map<Integer, String> resMap;
if (Files.isReadable(resMapFile)) {
resMap = TextResMapFile.read(resMapFile);
@@ -51,25 +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 : inputPaths) {
LOG.info("Processing {}", resFile);
ResTableParser resTableParser = new ResTableParser(root, true);
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);
}
}
@@ -84,6 +87,16 @@ public class ConvertArscFile {
LOG.info("done");
}
private static List<Path> filterAndSort(List<Path> inputPaths) {
return inputPaths.stream()
.filter(p -> {
String fileName = p.getFileName().toString();
return fileName.endsWith(".arsc") || fileName.endsWith(".jar");
})
.sorted()
.collect(Collectors.toList());
}
private static void mergeResMaps(Map<Integer, String> mainResMap, Map<Integer, String> newResMap) {
for (Map.Entry<Integer, String> entry : newResMap.entrySet()) {
Integer id = entry.getKey();
+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;
}
}
}
@@ -3,14 +3,12 @@ package jadx.cli;
import java.util.Collections;
import java.util.Map;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static jadx.core.utils.Utils.newConstStringMap;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.assertj.core.api.Assertions.assertThat;
public class JadxCLIArgsTest {
@@ -18,38 +16,38 @@ public class JadxCLIArgsTest {
@Test
public void testInvertedBooleanOption() {
assertThat(parse("--no-replace-consts").isReplaceConsts(), is(false));
assertThat(parse("").isReplaceConsts(), is(true));
assertThat(parse("--no-replace-consts").isReplaceConsts()).isFalse();
assertThat(parse("").isReplaceConsts()).isTrue();
}
@Test
public void testEscapeUnicodeOption() {
assertThat(parse("--escape-unicode").isEscapeUnicode(), is(true));
assertThat(parse("").isEscapeUnicode(), is(false));
assertThat(parse("--escape-unicode").isEscapeUnicode()).isTrue();
assertThat(parse("").isEscapeUnicode()).isFalse();
}
@Test
public void testSrcOption() {
assertThat(parse("--no-src").isSkipSources(), is(true));
assertThat(parse("-s").isSkipSources(), is(true));
assertThat(parse("").isSkipSources(), is(false));
assertThat(parse("--no-src").isSkipSources()).isTrue();
assertThat(parse("-s").isSkipSources()).isTrue();
assertThat(parse("").isSkipSources()).isFalse();
}
@Test
public void testOptionsOverride() {
assertThat(override(new JadxCLIArgs(), "--no-imports").isUseImports(), is(false));
assertThat(override(new JadxCLIArgs(), "--no-debug-info").isDebugInfo(), is(false));
assertThat(override(new JadxCLIArgs(), "").isUseImports(), is(true));
assertThat(override(new JadxCLIArgs(), "--no-imports").isUseImports()).isFalse();
assertThat(override(new JadxCLIArgs(), "--no-debug-info").isDebugInfo()).isFalse();
assertThat(override(new JadxCLIArgs(), "").isUseImports()).isTrue();
JadxCLIArgs args = new JadxCLIArgs();
args.useImports = false;
assertThat(override(args, "--no-imports").isUseImports(), is(false));
assertThat(override(args, "--no-imports").isUseImports()).isFalse();
args.debugInfo = false;
assertThat(override(args, "--no-debug-info").isDebugInfo(), is(false));
assertThat(override(args, "--no-debug-info").isDebugInfo()).isFalse();
args = new JadxCLIArgs();
args.useImports = false;
assertThat(override(args, "").isUseImports(), is(false));
assertThat(override(args, "").isUseImports()).isFalse();
}
@Test
@@ -83,7 +81,7 @@ public class JadxCLIArgsTest {
JadxCLIArgs args = new JadxCLIArgs();
args.pluginOptions = baseMap;
Map<String, String> resultMap = override(args, providedArgs).getPluginOptions();
assertThat(resultMap, Matchers.equalTo(expectedMap));
assertThat(resultMap).isEqualTo(expectedMap);
}
private JadxCLIArgs parse(String... args) {
@@ -91,15 +89,24 @@ public class JadxCLIArgsTest {
}
private JadxCLIArgs parse(JadxCLIArgs jadxArgs, String... args) {
boolean res = jadxArgs.processArgs(args);
assertThat(res, is(true));
LOG.info("Jadx args: {}", jadxArgs.toJadxArgs());
return jadxArgs;
return check(jadxArgs, jadxArgs.processArgs(args));
}
private JadxCLIArgs override(JadxCLIArgs jadxArgs, String... args) {
boolean res = jadxArgs.overrideProvided(args);
assertThat(res, is(true));
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) {
assertThat(res).isTrue();
LOG.info("Jadx args: {}", jadxArgs.toJadxArgs());
return jadxArgs;
}
@@ -7,10 +7,10 @@ import org.junit.jupiter.api.Test;
import jadx.api.JadxArgs.RenameEnum;
import jadx.cli.JadxCLIArgs.RenameConverter;
import jadx.core.utils.exceptions.JadxArgsValidateException;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class RenameConverterTest {
@@ -24,25 +24,24 @@ public class RenameConverterTest {
@Test
public void all() {
Set<RenameEnum> set = converter.convert("all");
assertEquals(3, set.size());
assertTrue(set.contains(RenameEnum.CASE));
assertTrue(set.contains(RenameEnum.VALID));
assertTrue(set.contains(RenameEnum.PRINTABLE));
assertThat(set).hasSize(3);
assertThat(set).contains(RenameEnum.CASE);
assertThat(set).contains(RenameEnum.VALID);
assertThat(set).contains(RenameEnum.PRINTABLE);
}
@Test
public void none() {
Set<RenameEnum> set = converter.convert("none");
assertTrue(set.isEmpty());
assertThat(set).isEmpty();
}
@Test
public void wrong() {
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class,
JadxArgsValidateException thrown = assertThrows(JadxArgsValidateException.class,
() -> converter.convert("wrong"),
"Expected convert() to throw, but it didn't");
assertEquals("'wrong' is unknown for parameter someParam, possible values are case, valid, printable",
thrown.getMessage());
assertThat(thrown.getMessage()).isEqualTo("'wrong' is unknown for parameter someParam, possible values are case, valid, printable");
}
}
@@ -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);
}
}
+50 -89
View File
@@ -1,123 +1,84 @@
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.AfterAll;
import org.assertj.core.api.Condition;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.utils.files.FileUtils;
import static org.assertj.core.api.Assertions.assertThat;
public class TestInput {
private static final Logger LOG = LoggerFactory.getLogger(TestInput.class);
public class TestInput extends BaseCliIntegrationTest {
@Test
public void testHelp() {
int result = execJadxCli(new String[] { "--help" });
assertThat(result).isEqualTo(0);
}
@Test
public void testApkInput() throws Exception {
int result = execJadxCli(buildArgs(List.of(), "samples/small.apk"));
assertThat(result).isEqualTo(0);
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("AndroidManifest.xml"), "manifest"))
.hasSize(12);
}
@Test
public void testDexInput() throws Exception {
decompile("dex", "samples/hello.dex");
decompile("samples/hello.dex");
}
@Test
public void testSmaliInput() throws Exception {
decompile("smali", "samples/HelloWorld.smali");
decompile("samples/HelloWorld.smali");
}
@Test
public void testClassInput() throws Exception {
decompile("class", "samples/HelloWorld.class");
decompile("samples/HelloWorld.class");
}
@Test
public void testMultipleInput() throws Exception {
decompile("multi", "samples/hello.dex", "samples/HelloWorld.smali");
decompile("samples/hello.dex", "samples/HelloWorld.smali");
}
@Test
public void testFallbackMode() throws Exception {
int result = execJadxCli(buildArgs(List.of("-f"), "samples/hello.dex"));
assertThat(result).isEqualTo(0);
List<Path> files = collectJavaFilesInDir(outputDir);
assertThat(files).hasSize(1);
}
@Test
public void testSimpleMode() throws Exception {
int result = execJadxCli(buildArgs(List.of("--decompilation-mode", "simple"), "samples/hello.dex"));
assertThat(result).isEqualTo(0);
List<Path> files = collectJavaFilesInDir(outputDir);
assertThat(files).hasSize(1);
}
@Test
public void testResourceOnly() throws Exception {
decode("resourceOnly", "samples/resources-only.apk");
}
private void decode(String tmpDirName, String apkSample) throws URISyntaxException, IOException {
List<String> args = new ArrayList<>();
Path tempDir = FileUtils.createTempDir(tmpDirName);
args.add("-v");
args.add("-d");
args.add(tempDir.toAbsolutePath().toString());
URL resource = getClass().getClassLoader().getResource(apkSample);
assertThat(resource).isNotNull();
String sampleFile = resource.toURI().getRawPath();
args.add(sampleFile);
int result = JadxCLI.execute(args.toArray(new String[0]));
int result = execJadxCli(buildArgs(List.of(), "samples/resources-only.apk"));
assertThat(result).isEqualTo(0);
List<Path> files = Files.find(
tempDir,
3,
(file, attr) -> file.getFileName().toString().equalsIgnoreCase("AndroidManifest.xml"))
.collect(Collectors.toList());
assertThat(files.isEmpty()).isFalse();
List<Path> files = collectFilesInDir(outputDir,
path -> path.getFileName().toString().equalsIgnoreCase("AndroidManifest.xml"));
assertThat(files).isNotEmpty();
}
private void decompile(String tmpDirName, String... inputSamples) throws URISyntaxException, IOException {
List<String> args = new ArrayList<>();
Path tempDir = FileUtils.createTempDir(tmpDirName);
args.add("-v");
args.add("-d");
args.add(tempDir.toAbsolutePath().toString());
for (String inputSample : inputSamples) {
URL resource = getClass().getClassLoader().getResource(inputSample);
assertThat(resource).isNotNull();
String sampleFile = resource.toURI().getRawPath();
args.add(sampleFile);
}
int result = JadxCLI.execute(args.toArray(new String[0]));
@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(tempDir);
assertThat(resultJavaFiles).isNotEmpty();
// do not copy input files as resources
PathMatcher logAllFiles = path -> {
LOG.debug("File in result dir: {}", path);
return true;
};
for (Path path : collectFilesInDir(tempDir, logAllFiles)) {
for (String inputSample : inputSamples) {
assertThat(path.toAbsolutePath().toString()).doesNotContain(inputSample);
}
}
}
private static List<Path> collectJavaFilesInDir(Path dir) throws IOException {
PathMatcher javaMatcher = dir.getFileSystem().getPathMatcher("glob:**.java");
return collectFilesInDir(dir, javaMatcher);
}
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());
}
}
@AfterAll
public static void cleanup() {
FileUtils.clearTempRootDir();
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.
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
+6
View File
@@ -0,0 +1,6 @@
## jadx app commons
This module contains common utilities used in jadx apps (cli and gui) and not needed in jadx-code module:
- `JadxCommonFiles` - wrapper for `dev.dirs:directories` lib to get
'config' and 'cache' directories in cross-platform way
- `JadxCommonEnv` - utils for work with environment variables
@@ -0,0 +1,7 @@
plugins {
id("jadx-library")
}
dependencies {
implementation("io.get-coursier.util:directories-jni:0.1.4")
}
@@ -0,0 +1,31 @@
package jadx.commons.app;
import org.jetbrains.annotations.Nullable;
public class JadxCommonEnv {
public static @Nullable String get(String varName, @Nullable String defValue) {
String strValue = System.getenv(varName);
return isNullOrEmpty(strValue) ? defValue : strValue;
}
public static boolean getBool(String varName, boolean defValue) {
String strValue = System.getenv(varName);
if (isNullOrEmpty(strValue)) {
return defValue;
}
return strValue.equalsIgnoreCase("true");
}
public static int getInt(String varName, int defValue) {
String strValue = System.getenv(varName);
if (isNullOrEmpty(strValue)) {
return defValue;
}
return Integer.parseInt(strValue);
}
private static boolean isNullOrEmpty(@Nullable String value) {
return value == null || value.isEmpty();
}
}
@@ -0,0 +1,106 @@
package jadx.commons.app;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
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;
public static Path getConfigDir() {
return CONFIG_DIR;
}
public static Path getCacheDir() {
return CACHE_DIR;
}
static {
DirsLoader loader = new DirsLoader();
CONFIG_DIR = loader.getConfigDir();
CACHE_DIR = loader.getCacheDir();
}
private static final class DirsLoader {
private final Path configDir;
private final Path cacheDir;
DirsLoader() {
try {
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 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.get();
}
Path path = Path.of(dirStr).toAbsolutePath();
Files.createDirectories(path);
return path;
}
private static ProjectDirectories loadDirs(AtomicReference<@Nullable ProjectDirectories> pdRef) {
ProjectDirectories currentDirs = pdRef.get();
if (currentDirs != null) {
return currentDirs;
}
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;
}
/**
* 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;
}
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() {
}
}
@@ -0,0 +1,33 @@
package jadx.commons.app;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class JadxTempFiles {
private static final String JADX_TMP_INSTANCE_PREFIX = "jadx-instance-";
private static final Path TEMP_ROOT_DIR = createTempRootDir();
public static Path getTempRootDir() {
return TEMP_ROOT_DIR;
}
private static Path createTempRootDir() {
try {
String jadxTmpDir = System.getenv("JADX_TMP_DIR");
Path dir;
if (jadxTmpDir != null) {
Path customTmpRootDir = Paths.get(jadxTmpDir);
Files.createDirectories(customTmpRootDir);
dir = Files.createTempDirectory(customTmpRootDir, JADX_TMP_INSTANCE_PREFIX);
} else {
dir = Files.createTempDirectory(JADX_TMP_INSTANCE_PREFIX);
}
dir.toFile().deleteOnExit();
return dir;
} catch (Exception e) {
throw new RuntimeException("Failed to create temp root directory", e);
}
}
}
+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;
}
}
+42 -13
View File
@@ -4,20 +4,19 @@ plugins {
dependencies {
api(project(":jadx-plugins:jadx-input-api"))
api(project(":jadx-commons:jadx-zip"))
implementation("com.google.code.gson:gson:2.10.1")
implementation("com.google.code.gson:gson:2.14.0")
// TODO: move resources decoding to separate plugin module
implementation("com.android.tools.build:aapt2-proto:8.2.2-10154469")
implementation("com.google.protobuf:protobuf-java:3.25.2") // forcing latest version
testImplementation("org.apache.commons:commons-lang3:3.14.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 {
@@ -25,9 +24,39 @@ 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")
}
tasks.test {
exclude("**/tmp/*")
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()
if (testJavaVer < currentJavaVer) {
throw GradleException("'$envVarName' can't be set to lower version than $currentJavaVer")
}
println("Set Java toolchain for core tests to version '$testJavaVer'")
return testJavaVer
}
tasks.named<Test>("test") {
jadxTestJavaVersion?.let { testJavaVer ->
javaLauncher =
javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(testJavaVer)
}
}
// disable cache to allow test's rerun,
// because most tests are integration and depends on plugins and environment
outputs.cacheIf { false }
// 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);
}
}
}
@@ -8,8 +8,6 @@ import jadx.api.metadata.ICodeAnnotation;
import jadx.api.metadata.ICodeNodeRef;
public interface ICodeWriter {
String NL = System.getProperty("line.separator");
String INDENT_STR = " ";
boolean isMetadataSupported();
+186 -19
View File
@@ -4,9 +4,9 @@ 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;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -21,6 +21,7 @@ import org.slf4j.LoggerFactory;
import jadx.api.args.GeneratedRenamesMappingFileMode;
import jadx.api.args.IntegerFormat;
import jadx.api.args.ResourceNameSource;
import jadx.api.args.UseSourceNameAsClassNameAlias;
import jadx.api.args.UserRenamesMappingsMode;
import jadx.api.data.ICodeData;
import jadx.api.deobf.IAliasProvider;
@@ -29,12 +30,18 @@ import jadx.api.impl.AnnotatedCodeWriter;
import jadx.api.impl.InMemoryCodeCache;
import jadx.api.plugins.loader.JadxBasePluginLoader;
import jadx.api.plugins.loader.JadxPluginLoader;
import jadx.api.security.IJadxSecurity;
import jadx.api.security.JadxSecurityFlag;
import jadx.api.security.impl.JadxSecurity;
import jadx.api.usage.IUsageInfoCache;
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;
import jadx.core.utils.files.FileUtils;
public class JadxArgs implements Closeable {
@@ -42,6 +49,9 @@ public class JadxArgs implements Closeable {
public static final int DEFAULT_THREADS_COUNT = Math.max(1, Runtime.getRuntime().availableProcessors() / 2);
public static final String DEFAULT_NEW_LINE_STR = System.lineSeparator();
public static final String DEFAULT_INDENT_STR = " ";
public static final String DEFAULT_OUT_DIR = "jadx-output";
public static final String DEFAULT_SRC_DIR = "sources";
public static final String DEFAULT_RES_DIR = "resources";
@@ -80,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}
@@ -95,7 +106,8 @@ public class JadxArgs implements Closeable {
private UserRenamesMappingsMode userRenamesMappingsMode = UserRenamesMappingsMode.getDefault();
private boolean deobfuscationOn = false;
private boolean useSourceNameAsClassAlias = false;
private UseSourceNameAsClassNameAlias useSourceNameAsClassNameAlias = UseSourceNameAsClassNameAlias.getDefault();
private int sourceNameRepeatLimit = 10;
private File generatedRenamesMappingFile = null;
private GeneratedRenamesMappingFileMode generatedRenamesMappingFileMode = GeneratedRenamesMappingFileMode.getDefault();
@@ -122,7 +134,9 @@ 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;
private boolean skipXmlPrettyPrint = false;
@@ -144,10 +158,20 @@ public class JadxArgs implements Closeable {
private ICodeData codeData;
private String codeNewLineStr = DEFAULT_NEW_LINE_STR;
private String codeIndentStr = DEFAULT_INDENT_STR;
private CommentsLevel commentsLevel = CommentsLevel.INFO;
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 {
@@ -156,13 +180,36 @@ public class JadxArgs implements Closeable {
private UseKotlinMethodsForVarNames useKotlinMethodsForVarNames = UseKotlinMethodsForVarNames.APPLY;
/**
* Additional files structure info.
* Defaults to tmp dirs.
*/
private IJadxFilesGetter filesGetter = TempFilesGetter.INSTANCE;
/**
* Additional data validation and security checks
*/
private IJadxSecurity security = new JadxSecurity(JadxSecurityFlag.all());
/**
* Don't save files (can be using for performance testing)
*/
private boolean skipFilesSave = false;
/**
* Run additional expensive checks to verify internal invariants and info integrity
*/
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<>();
private JadxPluginLoader pluginLoader = new JadxBasePluginLoader();
private boolean loadJadxClsSetFile = true;
@@ -180,7 +227,6 @@ public class JadxArgs implements Closeable {
@Override
public void close() {
try {
inputFiles = null;
if (codeCache != null) {
codeCache.close();
}
@@ -192,9 +238,6 @@ public class JadxArgs implements Closeable {
}
} catch (Exception e) {
LOG.error("Failed to close JadxArgs", e);
} finally {
codeCache = null;
usageInfoCache = null;
}
}
@@ -202,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) {
@@ -418,12 +465,37 @@ public class JadxArgs implements Closeable {
this.generatedRenamesMappingFileMode = mode;
}
public boolean isUseSourceNameAsClassAlias() {
return useSourceNameAsClassAlias;
public UseSourceNameAsClassNameAlias getUseSourceNameAsClassNameAlias() {
return useSourceNameAsClassNameAlias;
}
public void setUseSourceNameAsClassNameAlias(UseSourceNameAsClassNameAlias useSourceNameAsClassNameAlias) {
this.useSourceNameAsClassNameAlias = useSourceNameAsClassNameAlias;
}
public int getSourceNameRepeatLimit() {
return sourceNameRepeatLimit;
}
public void setSourceNameRepeatLimit(int sourceNameRepeatLimit) {
this.sourceNameRepeatLimit = sourceNameRepeatLimit;
}
/**
* @deprecated Use {@link #getUseSourceNameAsClassNameAlias()} instead.
*/
@Deprecated
public boolean isUseSourceNameAsClassAlias() {
return getUseSourceNameAsClassNameAlias().toBoolean();
}
/**
* @deprecated Use {@link #setUseSourceNameAsClassNameAlias(UseSourceNameAsClassNameAlias)} instead.
*/
@Deprecated
public void setUseSourceNameAsClassAlias(boolean useSourceNameAsClassAlias) {
this.useSourceNameAsClassAlias = useSourceNameAsClassAlias;
var useSourceNameAsClassNameAlias = UseSourceNameAsClassNameAlias.create(useSourceNameAsClassAlias);
setUseSourceNameAsClassNameAlias(useSourceNameAsClassNameAlias);
}
public int getDeobfuscationMinLength() {
@@ -507,11 +579,33 @@ 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() {
return restoreSwitchOverString;
}
public void setRestoreSwitchOverString(boolean restoreSwitchOverString) {
this.restoreSwitchOverString = restoreSwitchOverString;
}
public boolean isSkipXmlPrettyPrint() {
@@ -622,6 +716,22 @@ public class JadxArgs implements Closeable {
this.codeData = codeData;
}
public String getCodeIndentStr() {
return codeIndentStr;
}
public void setCodeIndentStr(String codeIndentStr) {
this.codeIndentStr = codeIndentStr;
}
public String getCodeNewLineStr() {
return codeNewLineStr;
}
public void setCodeNewLineStr(String codeNewLineStr) {
this.codeNewLineStr = codeNewLineStr;
}
public CommentsLevel getCommentsLevel() {
return commentsLevel;
}
@@ -638,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;
}
@@ -654,6 +772,22 @@ public class JadxArgs implements Closeable {
this.useKotlinMethodsForVarNames = useKotlinMethodsForVarNames;
}
public IJadxFilesGetter getFilesGetter() {
return filesGetter;
}
public void setFilesGetter(IJadxFilesGetter filesGetter) {
this.filesGetter = filesGetter;
}
public IJadxSecurity getSecurity() {
return security;
}
public void setSecurity(IJadxSecurity security) {
this.security = security;
}
public boolean isSkipFilesSave() {
return skipFilesSave;
}
@@ -662,6 +796,18 @@ public class JadxArgs implements Closeable {
this.skipFilesSave = skipFilesSave;
}
public boolean isRunDebugChecks() {
return runDebugChecks;
}
public void setRunDebugChecks(boolean runDebugChecks) {
this.runDebugChecks = runDebugChecks;
}
public List<String> getDisabledPasses() {
return disabledPasses;
}
public Map<String, String> getPluginOptions() {
return pluginOptions;
}
@@ -670,6 +816,14 @@ public class JadxArgs implements Closeable {
this.pluginOptions = pluginOptions;
}
public Set<String> getDisabledPlugins() {
return disabledPlugins;
}
public void setDisabledPlugins(Set<String> disabledPlugins) {
this.disabledPlugins = disabledPlugins;
}
public JadxPluginLoader getPluginLoader() {
return pluginLoader;
}
@@ -686,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
*/
@@ -693,12 +855,13 @@ public class JadxArgs implements Closeable {
String argStr = "args:" + decompilationMode + useImports + showInconsistentCode
+ inlineAnonymousClasses + inlineMethods + moveInnerClasses + allowInlineKotlinLambda
+ deobfuscationOn + deobfuscationMinLength + deobfuscationMaxLength + deobfuscationWhitelist
+ resourceNameSource
+ useSourceNameAsClassNameAlias + sourceNameRepeatLimit
+ resourceNameSource + useHeadersForDetectResourceExtensions
+ useKotlinMethodsForVarNames
+ insertDebugLines + extractFinally
+ debugInfo + useSourceNameAsClassAlias + escapeUnicode + replaceConsts
+ debugInfo + escapeUnicode + replaceConsts + restoreSwitchOverString
+ respectBytecodeAccModifiers + fsCaseSensitive + renameFlags
+ commentsLevel + useDxInput + integerFormat
+ commentsLevel + useDxInput + integerFormat + typeUpdatesLimitCount
+ "|" + buildPluginsHash(decompiler);
return FileUtils.md5Sum(argStr);
}
@@ -732,7 +895,8 @@ public class JadxArgs implements Closeable {
+ ", generatedRenamesMappingFile=" + generatedRenamesMappingFile
+ ", generatedRenamesMappingFileMode=" + generatedRenamesMappingFileMode
+ ", resourceNameSource=" + resourceNameSource
+ ", useSourceNameAsClassAlias=" + useSourceNameAsClassAlias
+ ", useSourceNameAsClassNameAlias=" + useSourceNameAsClassNameAlias
+ ", sourceNameRepeatLimit=" + sourceNameRepeatLimit
+ ", useKotlinMethodsForVarNames=" + useKotlinMethodsForVarNames
+ ", insertDebugLines=" + insertDebugLines
+ ", extractFinally=" + extractFinally
@@ -741,8 +905,9 @@ public class JadxArgs implements Closeable {
+ ", deobfuscationWhitelist=" + deobfuscationWhitelist
+ ", escapeUnicode=" + escapeUnicode
+ ", replaceConsts=" + replaceConsts
+ ", restoreSwitchOverString=" + restoreSwitchOverString
+ ", respectBytecodeAccModifiers=" + respectBytecodeAccModifiers
+ ", exportAsGradleProject=" + exportAsGradleProject
+ ", exportGradleType=" + exportGradleType
+ ", skipXmlPrettyPrint=" + skipXmlPrettyPrint
+ ", fsCaseSensitive=" + fsCaseSensitive
+ ", renameFlags=" + renameFlags
@@ -754,6 +919,8 @@ public class JadxArgs implements Closeable {
+ ", pluginOptions=" + pluginOptions
+ ", cfgOutput=" + cfgOutput
+ ", rawCFGOutput=" + rawCFGOutput
+ ", useHeadersForDetectResourceExtensions=" + useHeadersForDetectResourceExtensions
+ ", typeUpdatesLimitCount=" + typeUpdatesLimitCount
+ '}';
}
}
@@ -28,12 +28,6 @@ public class JadxArgsValidator {
if (inputFiles.isEmpty() && jadx.getCustomCodeLoaders().isEmpty()) {
throw new JadxArgsValidateException("Please specify input file");
}
for (File inputFile : inputFiles) {
String fileName = inputFile.getName();
if (fileName.startsWith("--")) {
throw new JadxArgsValidateException("Unknown argument: " + fileName);
}
}
for (File file : inputFiles) {
checkFile(file);
}
@@ -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;
@@ -51,9 +53,8 @@ import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
import jadx.core.utils.tasks.TaskExecutor;
import jadx.core.xmlgen.BinaryXMLParser;
import jadx.core.xmlgen.ProtoXMLParser;
import jadx.core.xmlgen.ResourcesSaver;
import jadx.zip.ZipReader;
/**
* Jadx API usage example:
@@ -86,55 +87,67 @@ public final class JadxDecompiler implements Closeable {
private static final Logger LOG = LoggerFactory.getLogger(JadxDecompiler.class);
private final JadxArgs args;
private final JadxPluginManager pluginManager = new JadxPluginManager(this);
private final JadxPluginManager pluginManager;
private final List<ICodeLoader> loadedInputs = new ArrayList<>();
private final ZipReader zipReader;
private RootNode root;
private List<JavaClass> classes;
private List<ResourceFile> resources;
private BinaryXMLParser binaryXmlParser;
private ProtoXMLParser protoXmlParser;
private final IDecompileScheduler decompileScheduler = new DecompilerScheduler();
private final JadxEventsImpl events = new JadxEventsImpl();
private final ResourcesLoader resourcesLoader;
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();
public JadxDecompiler() {
this(new JadxArgs());
}
public JadxDecompiler(JadxArgs args) {
this.args = args;
this.args = Objects.requireNonNull(args);
this.pluginManager = new JadxPluginManager(this);
this.resourcesLoader = new ResourcesLoader(this);
this.zipReader = new ZipReader(args.getSecurity());
}
public void load() {
reset();
JadxArgsValidator.validate(this);
LOG.info("loading ...");
FileUtils.updateTempRootDir(args.getFilesGetter().getTempDir());
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.loadResources(resourcesLoader, getResources());
root.finishClassLoad();
root.initClassPath();
root.loadResources(getResources());
// 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();
@@ -155,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);
}
}
}
@@ -166,42 +179,37 @@ public final class JadxDecompiler implements Closeable {
}
private void reset() {
unloadPlugins();
root = null;
classes = null;
resources = null;
binaryXmlParser = null;
protoXmlParser = null;
events.reset();
}
@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() {
@@ -218,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);
@@ -295,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;
}
@@ -331,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();
@@ -345,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);
@@ -401,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();
}
@@ -409,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));
}
}
@@ -430,7 +466,7 @@ public final class JadxDecompiler implements Closeable {
if (root == null) {
return Collections.emptyList();
}
resources = new ResourcesLoader(this).load();
resources = resourcesLoader.load(root);
}
return resources;
}
@@ -469,20 +505,6 @@ public final class JadxDecompiler implements Closeable {
return root;
}
synchronized BinaryXMLParser getBinaryXmlParser() {
if (binaryXmlParser == null) {
binaryXmlParser = new BinaryXMLParser(root);
}
return binaryXmlParser;
}
synchronized ProtoXMLParser getProtoXmlParser() {
if (protoXmlParser == null) {
protoXmlParser = new ProtoXMLParser(root);
}
return protoXmlParser;
}
/**
* Get JavaClass by ClassNode without loading and decompilation
*/
@@ -526,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()) {
@@ -599,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:
@@ -681,6 +706,10 @@ public final class JadxDecompiler implements Closeable {
return events;
}
public void setEventsImpl(IJadxEvents eventsImpl) {
this.events = eventsImpl;
}
public void addCustomCodeLoader(ICodeLoader customCodeLoader) {
customCodeLoaders.add(customCodeLoader);
}
@@ -704,6 +733,18 @@ public final class JadxDecompiler implements Closeable {
customPasses.computeIfAbsent(pass.getPassType(), l -> new ArrayList<>()).add(pass);
}
public ResourcesLoader getResourcesLoader() {
return resourcesLoader;
}
public ZipReader getZipReader() {
return zipReader;
}
public void addCloseable(Closeable closeable) {
closeableList.add(closeable);
}
@Override
public String toString() {
return "jadx decompiler " + getVersion();
+34 -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);
}
@@ -252,8 +266,12 @@ public final class JavaClass implements JavaNode {
return cls.getPackage();
}
public JavaPackage getJavaPackage() {
return cls.getPackageNode().getJavaNode();
}
@Override
public JavaClass getDeclaringClass() {
public @Nullable JavaClass getDeclaringClass() {
return parent;
}

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