Compare commits

..

139 Commits

Author SHA1 Message Date
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
477 changed files with 17595 additions and 4496 deletions
+1 -1
View File
@@ -61,7 +61,7 @@ jobs:
- name: Set up JDK
uses: oracle-actions/setup-java@v1
with:
release: 21
release: 24
- name: Print Java version
shell: bash
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
- name: Set up JDK
uses: oracle-actions/setup-java@v1
with:
release: 23
release: 24
- name: Set jadx version
uses: actions/github-script@v7
+2
View File
@@ -21,6 +21,7 @@ build/
classes/
idea/
.gradle/
.kotlin/
node_modules/
.vscode/
@@ -31,6 +32,7 @@ jadx-output/
*.jadx
*.class
*.jar
*.dump
*.log
*.cfg
+2 -12
View File
@@ -8,17 +8,7 @@ before_script:
stages:
- test
java-11:
stage: test
image: eclipse-temurin:11
script: ./gradlew clean build dist distWin
java-17:
stage: test
image: eclipse-temurin:17
script: ./gradlew clean build dist distWin
java-21:
build-test:
stage: test
image: eclipse-temurin:21
script: ./gradlew clean build dist distWin
script: JADX_BUILD_JAVA_VERSION=11 JADX_TEST_JAVA_VERSION=11 ./gradlew clean build dist distWin
+1 -1
View File
@@ -4,7 +4,7 @@
<module name="jadx.jadx-gui.main"/>
<option name="PROGRAM_PARAMETERS" value="-v"/>
<option name="VM_PARAMETERS"
value="-Xms128M -XX:MaxRAMPercentage=70.0 -Dawt.useSystemAAFontSettings=lcd -Dswing.aatext=true -Djava.util.Arrays.useLegacyMergeSort=true -Djdk.util.zip.disableZip64ExtraFieldValidation=true -XX:+IgnoreUnrecognizedVMOptions --add-opens=java.base/java.lang=ALL-UNNAMED -Dsun.java2d.noddraw=true -Dsun.java2d.d3d=false -Dsun.java2d.ddforcevram=true -Dsun.java2d.ddblit=false -Dswing.useflipBufferStrategy=True"/>
value="-Xms128M -XX:MaxRAMPercentage=70.0 -Dawt.useSystemAAFontSettings=lcd -Dswing.aatext=true -Djava.util.Arrays.useLegacyMergeSort=true -Djdk.util.zip.disableZip64ExtraFieldValidation=true -XX:+IgnoreUnrecognizedVMOptions --add-opens=java.base/java.lang=ALL-UNNAMED --enable-native-access=ALL-UNNAMED -Dsun.java2d.noddraw=true -Dsun.java2d.d3d=false -Dsun.java2d.ddforcevram=true -Dsun.java2d.ddblit=false -Dswing.useflipBufferStrategy=true"/>
<method v="2">
<option name="Make" enabled="true"/>
</method>
+116 -89
View File
@@ -86,106 +86,115 @@ and also packed to `build/jadx-<version>.zip`
### Usage
```
jadx[-gui] [command] [options] <input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk, .jadx.kts)
jadx[-gui] [command] [options] <input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk, .apkm, .jadx.kts)
commands (use '<command> --help' for command options):
plugins - manage jadx plugins
options:
-d, --output-dir - output directory
-ds, --output-dir-src - output directory for sources
-dr, --output-dir-res - output directory for resources
-r, --no-res - do not decode resources
-s, --no-src - do not decompile source code
--single-class - decompile a single class, full name, raw or alias
--single-class-output - file or dir for write if decompile a single class
--output-format - can be 'java' or 'json', default: java
-e, --export-gradle - save as android gradle project
-j, --threads-count - processing threads count, default: 4
-m, --decompilation-mode - code output mode:
'auto' - trying best options (default)
'restructure' - restore code structure (normal java code)
'simple' - simplified instructions (linear, with goto's)
'fallback' - raw instructions without modifications
--show-bad-code - show inconsistent code (incorrectly decompiled)
--no-xml-pretty-print - do not prettify XML
--no-imports - disable use of imports, always write entire package name
--no-debug-info - disable debug info parsing and processing
--add-debug-lines - add comments with debug line numbers if available
--no-inline-anonymous - disable anonymous classes inline
--no-inline-methods - disable methods inline
--no-move-inner-classes - disable move inner classes into parent
--no-inline-kotlin-lambda - disable inline for Kotlin lambdas
--no-finally - don't extract finally block
--no-restore-switch-over-string - don't restore switch over string
--no-replace-consts - don't replace constant value with matching constant field
--escape-unicode - escape non latin characters in strings (with \u)
--respect-bytecode-access-modifiers - don't change original access modifiers
--mappings-path - deobfuscation mappings file or directory. Allowed formats: Tiny and Tiny v2 (both '.tiny'), Enigma (.mapping) or Enigma directory
--mappings-mode - set mode for handling the deobfuscation mapping file:
'read' - just read, user can always save manually (default)
'read-and-autosave-every-change' - read and autosave after every change
'read-and-autosave-before-closing' - read and autosave before exiting the app or closing the project
'ignore' - don't read or save (can be used to skip loading mapping files referenced in the project file)
--deobf - activate deobfuscation
--deobf-min - min length of name, renamed if shorter, default: 3
--deobf-max - max length of name, renamed if longer, default: 64
--deobf-whitelist - space separated list of classes (full name) and packages (ends with '.*') to exclude from deobfuscation, default: android.support.v4.* android.support.v7.* android.support.v4.os.* android.support.annotation.Px androidx.core.os.* androidx.annotation.Px
--deobf-cfg-file - deobfuscation mappings file used for JADX auto-generated names (in the JOBF file format), default: same dir and name as input file with '.jobf' extension
--deobf-cfg-file-mode - set mode for handling the JADX auto-generated names' deobfuscation map file:
'read' - read if found, don't save (default)
'read-or-save' - read if found, save otherwise (don't overwrite)
'overwrite' - don't read, always save
'ignore' - don't read and don't save
--deobf-res-name-source - better name source for resources:
'auto' - automatically select best name (default)
'resources' - use resources names
'code' - use R class fields names
--use-source-name-as-class-name-alias - use source name as class name alias:
'always' - always use source name if it's available
'if-better' - use source name if it seems better than the current one
'never' - never use source name, even if it's available
--use-kotlin-methods-for-var-names - use kotlin intrinsic methods to rename variables, values: disable, apply, apply-and-hide, default: apply
--rename-flags - fix options (comma-separated list of):
'case' - fix case sensitivity issues (according to --fs-case-sensitive option),
'valid' - rename java identifiers to make them valid,
'printable' - remove non-printable chars from identifiers,
or single 'none' - to disable all renames
or single 'all' - to enable all (default)
--integer-format - how integers are displayed:
'auto' - automatically select (default)
'decimal' - use decimal
'hexadecimal' - use hexadecimal
--fs-case-sensitive - treat filesystem as case sensitive, false by default
--cfg - save methods control flow graph to dot file
--raw-cfg - save methods control flow graph (use raw instructions)
-f, --fallback - set '--decompilation-mode' to 'fallback' (deprecated)
--use-dx - use dx/d8 to convert java bytecode
--comments-level - set code comments level, values: error, warn, info, debug, user-only, none, default: info
--log-level - set log level, values: quiet, progress, error, warn, info, debug, default: progress
-v, --verbose - verbose output (set --log-level to DEBUG)
-q, --quiet - turn off output (set --log-level to QUIET)
--version - print jadx version
-h, --help - print this help
-d, --output-dir - output directory
-ds, --output-dir-src - output directory for sources
-dr, --output-dir-res - output directory for resources
-r, --no-res - do not decode resources
-s, --no-src - do not decompile source code
-j, --threads-count - processing threads count, default: 4
--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
--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)
--disable-plugins - comma separated list of plugin ids to disable, default:
--version - print jadx version
-h, --help - print this help
Plugin options (-P<name>=<value>):
dex-input: Load .dex and .apk files
- dex-input.verify-checksum - verify dex file checksum before load, values: [yes, no], default: yes
- dex-input.verify-checksum - verify dex file checksum before load, values: [yes, no], default: yes
java-convert: Convert .class, .jar and .aar files to dex
- java-convert.mode - convert mode, values: [dx, d8, both], default: both
- java-convert.d8-desugar - use desugar in d8, values: [yes, no], default: no
- java-convert.mode - convert mode, values: [dx, d8, both], default: both
- java-convert.d8-desugar - use desugar in d8, values: [yes, no], default: no
kotlin-metadata: Use kotlin.Metadata annotation for code generation
- kotlin-metadata.class-alias - rename class alias, values: [yes, no], default: yes
- kotlin-metadata.method-args - rename function arguments, values: [yes, no], default: yes
- kotlin-metadata.fields - rename fields, values: [yes, no], default: yes
- kotlin-metadata.companion - rename companion object, values: [yes, no], default: yes
- kotlin-metadata.data-class - add data class modifier, values: [yes, no], default: yes
- kotlin-metadata.to-string - rename fields using toString, values: [yes, no], default: yes
- kotlin-metadata.getters - rename simple getters to field names, values: [yes, no], default: yes
- kotlin-metadata.class-alias - rename class alias, values: [yes, no], default: yes
- kotlin-metadata.method-args - rename function arguments, values: [yes, no], default: yes
- kotlin-metadata.fields - rename fields, values: [yes, no], default: yes
- kotlin-metadata.companion - rename companion object, values: [yes, no], default: yes
- kotlin-metadata.data-class - add data class modifier, values: [yes, no], default: yes
- kotlin-metadata.to-string - rename fields using toString, values: [yes, no], default: yes
- kotlin-metadata.getters - rename simple getters to field names, values: [yes, no], default: yes
kotlin-smap: Use kotlin.SourceDebugExtension annotation for rename class alias
- kotlin-smap.class-alias-source-dbg - rename class alias from SourceDebugExtension, values: [yes, no], default: no
rename-mappings: various mappings support
- rename-mappings.format - mapping format, values: [AUTO, TINY_FILE, TINY_2_FILE, ENIGMA_FILE, ENIGMA_DIR, SRG_FILE, XSRG_FILE, JAM_FILE, CSRG_FILE, TSRG_FILE, TSRG_2_FILE, PROGUARD_FILE, RECAF_SIMPLE_FILE, JOBF_FILE], default: AUTO
- rename-mappings.invert - invert mapping on load, values: [yes, no], default: no
- rename-mappings.format - mapping format, values: [AUTO, TINY_FILE, TINY_2_FILE, ENIGMA_FILE, ENIGMA_DIR, SRG_FILE, XSRG_FILE, JAM_FILE, CSRG_FILE, TSRG_FILE, TSRG_2_FILE, PROGUARD_FILE, INTELLIJ_MIGRATION_MAP_FILE, RECAF_SIMPLE_FILE, JOBF_FILE], default: AUTO
- rename-mappings.invert - invert mapping on load, values: [yes, no], default: no
smali-input: Load .smali files
- smali-input.api-level - Android API level, default: 27
- smali-input.api-level - Android API level, default: 27
Environment variables:
JADX_DISABLE_XML_SECURITY - set to 'true' to disable all security checks for XML files
@@ -204,6 +213,24 @@ Examples:
```
These options also work in jadx-gui running from command line and override options from preferences' dialog
Usage for `plugins` command
```
usage: plugins [options]
options:
-i, --install <locationId> - install plugin with locationId
-j, --install-jar <path-to.jar> - install plugin from jar file
-l, --list - list installed plugins
-a, --available - list available plugins from jadx-plugins-list (aka marketplace)
-u, --update - update installed plugins
--uninstall <pluginId> - uninstall plugin with pluginId
--disable <pluginId> - disable plugin with pluginId
--enable <pluginId> - enable plugin with pluginId
--list-all - list all plugins including bundled and dropins
--list-versions <locationId> - fetch latest versions of plugin from locationId (will download all artefacts, limited to 10)
-h, --help - print this help
```
### Troubleshooting
Please check wiki page [Troubleshooting Q&A](https://github.com/skylot/jadx/wiki/Troubleshooting-Q&A)
+1 -1
View File
@@ -6,7 +6,7 @@ import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
import java.util.Locale
plugins {
id("com.github.ben-manes.versions") version "0.51.0"
id("com.github.ben-manes.versions") version "0.52.0"
id("se.patrikerdes.use-latest-versions") version "0.2.18"
id("com.diffplug.spotless") version "6.25.0"
}
+1 -1
View File
@@ -3,7 +3,7 @@ plugins {
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.21")
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.21")
implementation("org.openrewrite:plugin:6.19.1")
}
@@ -14,16 +14,16 @@ group = "io.github.skylot"
version = jadxVersion
dependencies {
implementation("org.slf4j:slf4j-api:2.0.16")
compileOnly("org.jetbrains:annotations:26.0.1")
implementation("org.slf4j:slf4j-api:2.0.17")
compileOnly("org.jetbrains:annotations:26.0.2")
testImplementation("ch.qos.logback:logback-classic:1.5.12")
testImplementation("org.assertj:assertj-core:3.26.3")
testImplementation("ch.qos.logback:logback-classic:1.5.18")
testImplementation("org.assertj:assertj-core:3.27.3")
testImplementation("org.junit.jupiter:junit-jupiter:5.11.3")
testImplementation("org.junit.jupiter:junit-jupiter:5.12.2")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testCompileOnly("org.jetbrains:annotations:26.0.1")
testCompileOnly("org.jetbrains:annotations:26.0.2")
}
repositories {
@@ -45,6 +45,7 @@ java {
tasks {
compileJava {
options.encoding = "UTF-8"
// options.compilerArgs = listOf("-Xlint:deprecation")
}
jar {
manifest {
@@ -1,5 +1,3 @@
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
plugins {
id("org.openrewrite.rewrite")
}
@@ -9,10 +7,10 @@ repositories {
}
dependencies {
rewrite("org.openrewrite.recipe:rewrite-testing-frameworks:2.21.0")
rewrite("org.openrewrite.recipe:rewrite-logging-frameworks:2.15.1")
rewrite("org.openrewrite.recipe:rewrite-migrate-java:2.28.0")
rewrite("org.openrewrite.recipe:rewrite-static-analysis:1.19.0")
rewrite("org.openrewrite.recipe:rewrite-testing-frameworks:3.8.0")
rewrite("org.openrewrite.recipe:rewrite-logging-frameworks:3.8.0")
rewrite("org.openrewrite.recipe:rewrite-migrate-java:3.9.0")
rewrite("org.openrewrite.recipe:rewrite-static-analysis:2.9.0")
}
tasks {
+3
View File
@@ -126,6 +126,9 @@
</module>
<module name="IllegalImport">
<property name="illegalClasses" value="jadx.core.utils.DebugUtils"/>
<!-- don't use nullable annotations from RxJava -->
<property name="illegalClasses" value="io.reactivex.rxjava3.annotations.NonNull"/>
<property name="illegalClasses" value="io.reactivex.rxjava3.annotations.Nullable"/>
</module>
<module name="RegexpSinglelineJava">
<property name="id" value="printstacktrace"/>
+10
View File
@@ -0,0 +1,10 @@
[Desktop Entry]
Name=JADX GUI
Comment=Dex to Java decompiler
Icon=jadx
Exec=jadx-gui %f
Terminal=false
Type=Application
Categories=Development;Java;
Keywords=Java;Decompiler;
StartupWMClass=jadx-gui-JadxGUI
+4
View File
@@ -2,6 +2,10 @@ org.gradle.warning.mode=all
org.gradle.parallel=true
org.gradle.caching=true
### Disable configuration cache for now: causing issues with spotless and version plugins
# org.gradle.configuration-cache=true
# org.gradle.configuration-cache.problems=warn
# Flags for google-java-format (optimize imports by spotless) for Java >= 16.
# Java < 9 will ignore unsupported flags (thanks to -XX:+IgnoreUnrecognizedVMOptions)
org.gradle.jvmargs=-XX:+IgnoreUnrecognizedVMOptions \
Binary file not shown.
+2 -2
View File
@@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=31c55713e40233a8303827ceb42ca48a47267a0ad4bab9177123121e71524c26
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
distributionSha256Sum=61ad310d3c7d3e5da131b76bbf22b5a4c0786e9d892dae8c1658d4b484de3caa
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Vendored
+2 -3
View File
@@ -86,8 +86,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -206,7 +205,7 @@ 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.
+6 -2
View File
@@ -4,7 +4,7 @@ plugins {
id("application")
// use shadow only for application scripts, jar will be copied from jadx-gui
id("com.gradleup.shadow") version "8.3.5"
id("com.gradleup.shadow") version "8.3.6"
}
dependencies {
@@ -18,12 +18,14 @@ 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-kotlin-source-debug-extension"))
runtimeOnly(project(":jadx-plugins:jadx-script:jadx-script-plugin"))
runtimeOnly(project(":jadx-plugins:jadx-xapk-input"))
runtimeOnly(project(":jadx-plugins:jadx-aab-input"))
runtimeOnly(project(":jadx-plugins:jadx-apkm-input"))
implementation("org.jcommander:jcommander:2.0")
implementation("ch.qos.logback:logback-classic:1.5.12")
implementation("ch.qos.logback:logback-classic:1.5.18")
}
application {
@@ -35,6 +37,8 @@ application {
"-XX:MaxRAMPercentage=70.0",
// 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")
@@ -27,7 +27,7 @@ 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;
@@ -42,6 +42,7 @@ public class JCommanderWrapper<T> {
public boolean parse(String[] args) {
try {
jc.parse(args);
applyFiles(argsObj);
return true;
} catch (ParameterException e) {
System.err.println("Arguments parse error: " + e.getMessage());
@@ -50,6 +51,15 @@ public class JCommanderWrapper<T> {
}
}
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 +68,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,10 +96,6 @@ public class JCommanderWrapper<T> {
return value;
}
public List<String> getUnknownOptions() {
return jc.getUnknownOptions();
}
public void printUsage() {
LogHelper.setLogLevel(LogHelper.LogLevelEnum.ERROR); // mute logger while printing help
@@ -237,13 +244,17 @@ public class JCommanderWrapper<T> {
JadxPluginManager pluginManager = decompiler.getPluginManager();
pluginManager.load(new JadxExternalPluginsLoader());
pluginManager.initAll();
for (PluginContext context : pluginManager.getAllPluginContexts()) {
JadxPluginOptions options = context.getOptions();
if (options != null) {
if (appendPlugin(context.getPluginInfo(), context.getOptions(), sb, maxNamesLen)) {
k++;
try {
for (PluginContext context : pluginManager.getAllPluginContexts()) {
JadxPluginOptions options = context.getOptions();
if (options != null) {
if (appendPlugin(context.getPluginInfo(), context.getOptions(), sb, maxNamesLen)) {
k++;
}
}
}
} finally {
pluginManager.unloadAll();
}
}
if (sb.length() == 0) {
@@ -0,0 +1,48 @@
package jadx.cli;
import java.util.Set;
import jadx.api.JadxArgs;
import jadx.api.security.JadxSecurityFlag;
import jadx.api.security.impl.JadxSecurity;
import jadx.commons.app.JadxCommonEnv;
import jadx.zip.security.DisabledZipSecurity;
import jadx.zip.security.IJadxZipSecurity;
import jadx.zip.security.JadxZipSecurity;
public class JadxAppCommon {
public static void applyEnvVars(JadxArgs jadxArgs) {
Set<JadxSecurityFlag> flags = JadxSecurityFlag.all();
IJadxZipSecurity zipSecurity;
boolean disableXmlSecurity = JadxCommonEnv.getBool("JADX_DISABLE_XML_SECURITY", false);
if (disableXmlSecurity) {
flags.remove(JadxSecurityFlag.SECURE_XML_PARSER);
// TODO: not related to 'xml security', but kept for compatibility
flags.remove(JadxSecurityFlag.VERIFY_APP_PACKAGE);
}
boolean disableZipSecurity = JadxCommonEnv.getBool("JADX_DISABLE_ZIP_SECURITY", false);
if (disableZipSecurity) {
flags.remove(JadxSecurityFlag.SECURE_ZIP_READER);
zipSecurity = DisabledZipSecurity.INSTANCE;
} else {
JadxZipSecurity jadxZipSecurity = new JadxZipSecurity();
int maxZipEntriesCount = JadxCommonEnv.getInt("JADX_ZIP_MAX_ENTRIES_COUNT", -2);
if (maxZipEntriesCount != -2) {
jadxZipSecurity.setMaxEntriesCount(maxZipEntriesCount);
}
int zipBombMinUncompressedSize = JadxCommonEnv.getInt("JADX_ZIP_BOMB_MIN_UNCOMPRESSED_SIZE", -2);
if (zipBombMinUncompressedSize != -2) {
jadxZipSecurity.setZipBombMinUncompressedSize(zipBombMinUncompressedSize);
}
int setZipBombDetectionFactor = JadxCommonEnv.getInt("JADX_ZIP_BOMB_DETECTION_FACTOR", -2);
if (setZipBombDetectionFactor != -2) {
jadxZipSecurity.setZipBombDetectionFactor(setZipBombDetectionFactor);
}
zipSecurity = jadxZipSecurity;
}
jadxArgs.setSecurity(new JadxSecurity(flags, zipSecurity));
}
}
+19 -25
View File
@@ -1,7 +1,8 @@
package jadx.cli;
import java.util.Set;
import java.util.function.Consumer;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -10,11 +11,8 @@ import jadx.api.JadxDecompiler;
import jadx.api.impl.AnnotatedCodeWriter;
import jadx.api.impl.NoOpCodeCache;
import jadx.api.impl.SimpleCodeWriter;
import jadx.api.security.JadxSecurityFlag;
import jadx.api.security.impl.JadxSecurity;
import jadx.cli.LogHelper.LogLevelEnum;
import jadx.cli.plugins.JadxFilesGetter;
import jadx.commons.app.JadxCommonEnv;
import jadx.core.utils.exceptions.JadxArgsValidateException;
import jadx.plugins.tools.JadxExternalPluginsLoader;
@@ -31,10 +29,18 @@ public class JadxCLI {
}
public static int execute(String[] args) {
return execute(args, null);
}
public static int execute(String[] args, @Nullable Consumer<JadxArgs> argsMod) {
try {
JadxCLIArgs jadxArgs = new JadxCLIArgs();
if (jadxArgs.processArgs(args)) {
return processAndSave(jadxArgs);
JadxCLIArgs cliArgs = new JadxCLIArgs();
if (cliArgs.processArgs(args)) {
JadxArgs jadxArgs = buildArgs(cliArgs);
if (argsMod != null) {
argsMod.accept(jadxArgs);
}
return runSave(jadxArgs, cliArgs);
}
return 0;
} catch (JadxArgsValidateException e) {
@@ -46,7 +52,7 @@ public class JadxCLI {
}
}
private static int processAndSave(JadxCLIArgs cliArgs) {
private static JadxArgs buildArgs(JadxCLIArgs cliArgs) {
LogHelper.initLogLevel(cliArgs);
LogHelper.setLogLevelsForLoadingStage();
JadxArgs jadxArgs = cliArgs.toJadxArgs();
@@ -54,7 +60,11 @@ public class JadxCLI {
jadxArgs.setPluginLoader(new JadxExternalPluginsLoader());
jadxArgs.setFilesGetter(JadxFilesGetter.INSTANCE);
initCodeWriterProvider(jadxArgs);
applyEnvVars(jadxArgs);
JadxAppCommon.applyEnvVars(jadxArgs);
return jadxArgs;
}
private static int runSave(JadxArgs jadxArgs, JadxCLIArgs cliArgs) {
try (JadxDecompiler jadx = new JadxDecompiler(jadxArgs)) {
jadx.load();
if (checkForErrors(jadx)) {
@@ -87,22 +97,6 @@ public class JadxCLI {
}
}
private static void applyEnvVars(JadxArgs jadxArgs) {
Set<JadxSecurityFlag> flags = JadxSecurityFlag.all();
boolean modified = false;
boolean disableXmlSecurity = JadxCommonEnv.getBool("JADX_DISABLE_XML_SECURITY", false);
if (disableXmlSecurity) {
flags.remove(JadxSecurityFlag.SECURE_XML_PARSER);
// TODO: not related to 'xml security', but kept for compatibility
flags.remove(JadxSecurityFlag.VERIFY_APP_PACKAGE);
modified = true;
}
// TODO: migrate 'ZipSecurity'
if (modified) {
jadxArgs.setSecurity(new JadxSecurity(flags));
}
}
private static boolean checkForErrors(JadxDecompiler jadx) {
if (jadx.getRoot().getClasses().isEmpty()) {
if (jadx.getArgs().isSkipResources()) {
@@ -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,8 @@ import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jetbrains.annotations.Nullable;
import com.beust.jcommander.DynamicParameter;
import com.beust.jcommander.IStringConverter;
import com.beust.jcommander.Parameter;
@@ -30,13 +32,14 @@ import jadx.api.args.ResourceNameSource;
import jadx.api.args.UseSourceNameAsClassNameAlias;
import jadx.api.args.UserRenamesMappingsMode;
import jadx.core.deobf.conditions.DeobfWhitelist;
import jadx.core.export.ExportGradleType;
import jadx.core.utils.exceptions.JadxArgsValidateException;
import jadx.core.utils.files.FileUtils;
public class JadxCLIArgs {
@Parameter(description = "<input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk, .jadx.kts)")
protected List<String> files = new ArrayList<>(1);
@Parameter(description = "<input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk, .apkm, .jadx.kts)")
protected List<String> files = Collections.emptyList();
@Parameter(names = { "-d", "--output-dir" }, description = "output directory")
protected String outDir;
@@ -53,6 +56,9 @@ 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;
@Parameter(names = { "--single-class" }, description = "decompile a single class, full name, raw or alias")
protected String singleClass = null;
@@ -62,11 +68,19 @@ public class JadxCLIArgs {
@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" },
@@ -200,6 +214,12 @@ public class JadxCLIArgs {
)
protected UseSourceNameAsClassNameAlias useSourceNameAsClassNameAlias = null;
@Parameter(
names = { "--source-name-repeat-limit" },
description = "allow using source name if it appears less than a limit number"
)
protected int sourceNameRepeatLimit = 10;
@Parameter(
names = { "--use-kotlin-methods-for-var-names" },
description = "use kotlin intrinsic methods to rename variables, values: disable, apply, apply-and-hide",
@@ -207,6 +227,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):"
@@ -277,29 +303,11 @@ public class JadxCLIArgs {
protected Map<String, String> pluginOptions = new HashMap<>();
public boolean processArgs(String[] args) {
JCommanderWrapper<JadxCLIArgs> jcw = new JCommanderWrapper<>(this);
JCommanderWrapper 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.
*/
public boolean overrideProvided(String[] args) {
JCommanderWrapper<JadxCLIArgs> jcw = new JCommanderWrapper<>(newInstance());
if (!jcw.parse(args)) {
return false;
}
jcw.overrideProvided(this);
return process(jcw);
}
protected JadxCLIArgs newInstance() {
return new JadxCLIArgs();
}
private boolean process(JCommanderWrapper<JadxCLIArgs> jcw) {
files.addAll(jcw.getUnknownOptions());
public boolean process(JCommanderWrapper jcw) {
if (jcw.processCommands()) {
return false;
}
@@ -352,11 +360,16 @@ public class JadxCLIArgs {
args.setDeobfuscationMaxLength(deobfuscationMaxLength);
args.setDeobfuscationWhitelist(Arrays.asList(deobfuscationWhitelistStr.split(" ")));
args.setUseSourceNameAsClassNameAlias(getUseSourceNameAsClassNameAlias());
args.setUseHeadersForDetectResourceExtensions(useHeadersForDetectResourceExtensions);
args.setSourceNameRepeatLimit(sourceNameRepeatLimit);
args.setUseKotlinMethodsForVarNames(useKotlinMethodsForVarNames);
args.setResourceNameSource(resourceNameSource);
args.setEscapeUnicode(escapeUnicode);
args.setRespectBytecodeAccModifiers(respectBytecodeAccessModifiers);
args.setExportAsGradleProject(exportAsGradleProject);
args.setExportGradleType(exportGradleType);
if (exportAsGradleProject && exportGradleType == null) {
args.setExportGradleType(ExportGradleType.AUTO);
}
args.setSkipXmlPrettyPrint(skipXmlPrettyPrint);
args.setUseImports(useImports);
args.setDebugInfo(debugInfo);
@@ -381,6 +394,10 @@ public class JadxCLIArgs {
return files;
}
public void setFiles(List<String> files) {
this.files = files;
}
public String getOutDir() {
return outDir;
}
@@ -508,6 +525,10 @@ public class JadxCLIArgs {
}
}
public int getSourceNameRepeatLimit() {
return sourceNameRepeatLimit;
}
/**
* @deprecated Use {@link #getUseSourceNameAsClassNameAlias()} instead.
*/
@@ -572,6 +593,10 @@ public class JadxCLIArgs {
return fsCaseSensitive;
}
public boolean isUseHeadersForDetectResourceExtensions() {
return useHeadersForDetectResourceExtensions;
}
public CommentsLevel getCommentsLevel() {
return commentsLevel;
}
@@ -653,6 +678,12 @@ public class JadxCLIArgs {
}
}
public static class ExportGradleTypeConverter extends BaseEnumConverter<ExportGradleType> {
public ExportGradleTypeConverter() {
super(ExportGradleType::valueOf, ExportGradleType::values);
}
}
public static class LogLevelConverter extends BaseEnumConverter<LogHelper.LogLevelEnum> {
public LogLevelConverter() {
super(LogHelper.LogLevelEnum::valueOf, LogHelper.LogLevelEnum::values);
@@ -24,7 +24,7 @@ public class JadxCLICommands {
COMMANDS_MAP.forEach(builder::addCommand);
}
public static boolean process(JCommanderWrapper<?> jcw, JCommander jc, String parsedCommand) {
public static boolean process(JCommanderWrapper jcw, JCommander jc, String parsedCommand) {
ICommand command = COMMANDS_MAP.get(parsedCommand);
if (command == null) {
throw new JadxArgsValidateException("Unknown command: " + parsedCommand
@@ -65,7 +65,7 @@ public class CommandPlugins implements ICommand {
@SuppressWarnings("UnnecessaryReturnStatement")
@Override
public void process(JCommanderWrapper<?> jcw, JCommander subCommander) {
public void process(JCommanderWrapper jcw, JCommander subCommander) {
if (printHelp) {
jcw.printUsage(subCommander);
return;
@@ -156,7 +156,7 @@ public class CommandPlugins implements ICommand {
sb.append(" (disabled)");
}
sb.append(" - ").append(plugin.getName());
sb.append(": ").append(plugin.getDescription());
sb.append(": ").append(formatDescription(plugin.getDescription()));
System.out.println(sb);
}
}
@@ -192,11 +192,24 @@ public class CommandPlugins implements ICommand {
if (!installedSet.contains(plugin.getPluginId())) {
System.out.println(" - " + plugin.getPluginId()
+ " - " + plugin.getName()
+ ": " + plugin.getDescription());
+ ": " + formatDescription(plugin.getDescription()));
}
}
}
private static String formatDescription(String desc) {
if (desc.contains("\n")) {
// remove new lines
desc = desc.replaceAll("\\R+", " ");
}
int maxLen = 512;
if (desc.length() > maxLen) {
// truncate very long descriptions
desc = desc.substring(0, maxLen) + " ...";
}
return desc;
}
private void installPlugin(String locationId) {
JadxPluginMetadata plugin = JadxPluginsTools.getInstance().install(locationId);
System.out.println("Plugin installed: " + plugin.getPluginId() + ":" + plugin.getVersion());
@@ -7,5 +7,5 @@ import jadx.cli.JCommanderWrapper;
public interface ICommand {
String name();
void process(JCommanderWrapper<?> jcw, JCommander subCommander);
void process(JCommanderWrapper jcw, JCommander subCommander);
}
@@ -1,6 +1,5 @@
package jadx.cli.tools;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
@@ -11,7 +10,6 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -19,8 +17,10 @@ import org.slf4j.LoggerFactory;
import jadx.api.JadxArgs;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.android.TextResMapFile;
import jadx.core.utils.files.ZipFile;
import jadx.core.xmlgen.ResTableBinaryParser;
import jadx.zip.IZipEntry;
import jadx.zip.ZipContent;
import jadx.zip.ZipReader;
import static jadx.core.utils.files.FileUtils.expandDirs;
@@ -54,24 +54,25 @@ public class ConvertArscFile {
LOG.info("Input entries count: {}", resMap.size());
RootNode root = new RootNode(new JadxArgs()); // not really needed
ZipReader zipReader = new ZipReader();
rewritesCount = 0;
for (Path resFile : inputResFiles) {
ResTableBinaryParser resTableParser = new ResTableBinaryParser(root, true);
if (resFile.getFileName().toString().endsWith(".jar")) {
// Load resources.arsc from android.jar
try (ZipFile zip = new ZipFile(resFile.toFile())) {
ZipEntry entry = zip.getEntry("resources.arsc");
try (ZipContent zip = zipReader.open(resFile.toFile())) {
IZipEntry entry = zip.searchEntry("resources.arsc");
if (entry == null) {
LOG.error("Failed to load \"resources.arsc\" from {}", resFile);
continue;
}
try (InputStream inputStream = zip.getInputStream(entry)) {
try (InputStream inputStream = entry.getInputStream()) {
resTableParser.decode(inputStream);
}
}
} else {
// Load resources.arsc from extracted file
try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(resFile))) {
try (InputStream inputStream = Files.newInputStream(resFile)) {
resTableParser.decode(inputStream);
}
}
@@ -0,0 +1,139 @@
package jadx.cli;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.io.TempDir;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.plugins.loader.JadxBasePluginLoader;
import jadx.core.plugins.files.SingleDirFilesGetter;
import jadx.core.utils.Utils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
public class BaseCliIntegrationTest {
private static final Logger LOG = LoggerFactory.getLogger(BaseCliIntegrationTest.class);
static final PathMatcher LOG_ALL_FILES = path -> {
LOG.debug("File in result dir: {}", path);
return true;
};
@TempDir
Path testDir;
Path outputDir;
@BeforeEach
public void setUp() {
outputDir = testDir.resolve("output");
}
int execJadxCli(String sampleName, String... options) {
return execJadxCli(buildArgs(List.of(options), sampleName));
}
int execJadxCli(String[] args) {
return JadxCLI.execute(args, jadxArgs -> {
// don't use global config and plugins
jadxArgs.setFilesGetter(new SingleDirFilesGetter(testDir));
jadxArgs.setPluginLoader(new JadxBasePluginLoader());
});
}
String[] buildArgs(List<String> options, String... inputSamples) {
List<String> args = new ArrayList<>(options);
args.add("-v");
args.add("-d");
args.add(outputDir.toAbsolutePath().toString());
for (String inputSample : inputSamples) {
try {
URL resource = getClass().getClassLoader().getResource(inputSample);
assertThat(resource).isNotNull();
String sampleFile = resource.toURI().getRawPath();
args.add(sampleFile);
} catch (URISyntaxException e) {
fail("Failed to load sample: " + inputSample, e);
}
}
return args.toArray(new String[0]);
}
void decompile(String... inputSamples) throws IOException {
int result = execJadxCli(buildArgs(List.of(), inputSamples));
assertThat(result).isEqualTo(0);
List<Path> resultJavaFiles = collectJavaFilesInDir(outputDir);
assertThat(resultJavaFiles).isNotEmpty();
// do not copy input files as resources
for (Path path : collectFilesInDir(outputDir, LOG_ALL_FILES)) {
for (String inputSample : inputSamples) {
assertThat(path.toAbsolutePath().toString()).doesNotContain(inputSample);
}
}
}
static void printFiles(List<Path> files) {
LOG.info("Output files (count: {}):", files.size());
for (Path file : files) {
LOG.info(" {}", file);
}
LOG.info("");
}
String pathToUniformString(Path path) {
return path.toString().replace('\\', '/');
}
Path printFileContent(Path file) {
try {
String content = Files.readString(outputDir.resolve(file));
String spacer = Utils.strRepeat("=", 70);
LOG.info("File content: {}\n{}\n{}\n{}", file, spacer, content, spacer);
return file;
} catch (IOException e) {
throw new RuntimeException("Failed to load file: " + file, e);
}
}
static List<Path> collectJavaFilesInDir(Path dir) throws IOException {
PathMatcher javaMatcher = dir.getFileSystem().getPathMatcher("glob:**.java");
return collectFilesInDir(dir, javaMatcher);
}
static List<Path> collectAllFilesInDir(Path dir) throws IOException {
try (Stream<Path> pathStream = Files.walk(dir)) {
List<Path> files = pathStream
.filter(Files::isRegularFile)
.map(dir::relativize)
.collect(Collectors.toList());
printFiles(files);
return files;
}
}
static List<Path> collectFilesInDir(Path dir, PathMatcher matcher) throws IOException {
try (Stream<Path> pathStream = Files.walk(dir)) {
List<Path> files = pathStream
.filter(p -> Files.isRegularFile(p, LinkOption.NOFOLLOW_LINKS))
.filter(matcher::matches)
.collect(Collectors.toList());
printFiles(files);
return files;
}
}
}
@@ -93,7 +93,16 @@ public class JadxCLIArgsTest {
}
private JadxCLIArgs override(JadxCLIArgs jadxArgs, String... args) {
return check(jadxArgs, jadxArgs.overrideProvided(args));
return check(jadxArgs, overrideProvided(jadxArgs, args));
}
private static boolean overrideProvided(JadxCLIArgs jadxArgs, String[] args) {
JCommanderWrapper jcw = new JCommanderWrapper(new JadxCLIArgs());
if (!jcw.parse(args)) {
return false;
}
jcw.overrideProvided(jadxArgs);
return jadxArgs.process(jcw);
}
private static JadxCLIArgs check(JadxCLIArgs jadxArgs, boolean res) {
@@ -0,0 +1,77 @@
package jadx.cli;
import org.assertj.core.api.Condition;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class TestExport extends BaseCliIntegrationTest {
@Test
public void testBasicExport() throws Exception {
int result = execJadxCli("samples/small.apk");
assertThat(result).isEqualTo(0);
assertThat(collectAllFilesInDir(outputDir))
.map(this::pathToUniformString)
.haveExactly(2, new Condition<>(f -> f.startsWith("sources/") && f.endsWith(".java"), "sources"))
.haveExactly(10, new Condition<>(f -> f.startsWith("resources/"), "resources"))
.haveExactly(1, new Condition<>(f -> f.equals("resources/AndroidManifest.xml"), "manifest"))
.hasSize(12);
}
@Test
public void testGradleExportApk() throws Exception {
int result = execJadxCli("samples/small.apk", "--export-gradle");
assertThat(result).isEqualTo(0);
assertThat(collectAllFilesInDir(outputDir))
.describedAs("check output files")
.map(this::pathToUniformString)
.haveExactly(2, new Condition<>(f -> f.endsWith(".java"), "java classes"))
.haveExactly(0, new Condition<>(f -> f.endsWith("classes.dex"), "dex files"))
.hasSize(15);
}
@Test
public void testGradleExportAAR() throws Exception {
int result = execJadxCli("samples/test-lib.aar", "--export-gradle");
assertThat(result).isEqualTo(0);
assertThat(collectAllFilesInDir(outputDir))
.describedAs("check output files")
.map(this::printFileContent)
.map(this::pathToUniformString)
.haveExactly(1, new Condition<>(f -> f.startsWith("lib/src/main/java/") && f.endsWith(".java"), "java"))
.haveExactly(0, new Condition<>(f -> f.endsWith(".jar"), "jar files"))
.hasSize(8);
}
@Test
public void testGradleExportSimpleJava() throws Exception {
int result = execJadxCli("samples/HelloWorld.class", "--export-gradle");
assertThat(result).isEqualTo(0);
assertThat(collectAllFilesInDir(outputDir))
.describedAs("check output files")
.map(this::printFileContent)
.map(this::pathToUniformString)
.haveExactly(1, new Condition<>(f -> f.endsWith(".java") && f.startsWith("app/src/main/java/"), "java"))
.haveExactly(0, new Condition<>(f -> f.endsWith(".class"), "class files"))
.haveExactly(1, new Condition<>(f -> f.equals("settings.gradle.kts"), "settings"))
.haveExactly(1, new Condition<>(f -> f.equals("app/build.gradle.kts"), "build"))
.hasSize(3);
}
@Test
public void testGradleExportInvalidType() throws Exception {
int result = execJadxCli("samples/HelloWorld.class", "--export-gradle-type", "android-app");
assertThat(result).isEqualTo(0);
// expect output in 'android-app' template, but most fields will be set to UNKNOWN.
assertThat(collectAllFilesInDir(outputDir))
.describedAs("check output files")
.map(this::printFileContent)
.map(this::pathToUniformString)
.haveExactly(1, new Condition<>(f -> f.endsWith(".java") && f.startsWith("app/src/main/java/"), "java"))
.haveExactly(1, new Condition<>(f -> f.equals("settings.gradle"), "settings"))
.haveExactly(1, new Condition<>(f -> f.equals("build.gradle"), "build"))
.haveExactly(1, new Condition<>(f -> f.equals("app/build.gradle"), "app build"))
.hasSize(4);
}
}
+11 -93
View File
@@ -1,56 +1,32 @@
package jadx.cli;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.assertj.core.api.Condition;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.assertj.core.api.Assertions.assertThat;
public class TestInput {
private static final Logger LOG = LoggerFactory.getLogger(TestInput.class);
private static final PathMatcher LOG_ALL_FILES = path -> {
LOG.debug("File in result dir: {}", path);
return true;
};
@TempDir
Path testDir;
public class TestInput extends BaseCliIntegrationTest {
@Test
public void testHelp() {
int result = JadxCLI.execute(new String[] { "--help" });
int result = execJadxCli(new String[] { "--help" });
assertThat(result).isEqualTo(0);
}
@Test
public void testApkInput() throws Exception {
int result = JadxCLI.execute(buildArgs(List.of(), "samples/small.apk"));
int result = execJadxCli(buildArgs(List.of(), "samples/small.apk"));
assertThat(result).isEqualTo(0);
List<Path> resultFiles = collectAllFilesInDir(testDir);
printFiles(resultFiles);
assertThat(resultFiles)
assertThat(collectAllFilesInDir(outputDir))
.describedAs("check output files")
.map(p -> p.getFileName().toString())
.haveExactly(2, new Condition<>(f -> f.endsWith(".java"), "java classes"))
.haveExactly(9, new Condition<>(f -> f.endsWith(".xml"), "xml resources"))
.haveExactly(1, new Condition<>(f -> f.equals("classes.dex"), "dex"))
.haveExactly(1, new Condition<>(f -> f.equals("AndroidManifest.xml"), "manifest"))
.hasSize(13);
.hasSize(12);
}
@Test
@@ -75,84 +51,26 @@ public class TestInput {
@Test
public void testFallbackMode() throws Exception {
int result = JadxCLI.execute(buildArgs(List.of("-f"), "samples/hello.dex"));
int result = execJadxCli(buildArgs(List.of("-f"), "samples/hello.dex"));
assertThat(result).isEqualTo(0);
List<Path> files = collectJavaFilesInDir(testDir);
List<Path> files = collectJavaFilesInDir(outputDir);
assertThat(files).hasSize(1);
}
@Test
public void testSimpleMode() throws Exception {
int result = JadxCLI.execute(buildArgs(List.of("--decompilation-mode", "simple"), "samples/hello.dex"));
int result = execJadxCli(buildArgs(List.of("--decompilation-mode", "simple"), "samples/hello.dex"));
assertThat(result).isEqualTo(0);
List<Path> files = collectJavaFilesInDir(testDir);
List<Path> files = collectJavaFilesInDir(outputDir);
assertThat(files).hasSize(1);
}
@Test
public void testResourceOnly() throws Exception {
int result = JadxCLI.execute(buildArgs(List.of(), "samples/resources-only.apk"));
int result = execJadxCli(buildArgs(List.of(), "samples/resources-only.apk"));
assertThat(result).isEqualTo(0);
List<Path> files = collectFilesInDir(testDir,
List<Path> files = collectFilesInDir(outputDir,
path -> path.getFileName().toString().equalsIgnoreCase("AndroidManifest.xml"));
assertThat(files).isNotEmpty();
}
private void decompile(String... inputSamples) throws URISyntaxException, IOException {
int result = JadxCLI.execute(buildArgs(List.of(), inputSamples));
assertThat(result).isEqualTo(0);
List<Path> resultJavaFiles = collectJavaFilesInDir(testDir);
assertThat(resultJavaFiles).isNotEmpty();
// do not copy input files as resources
for (Path path : collectFilesInDir(testDir, LOG_ALL_FILES)) {
for (String inputSample : inputSamples) {
assertThat(path.toAbsolutePath().toString()).doesNotContain(inputSample);
}
}
}
private String[] buildArgs(List<String> options, String... inputSamples) throws URISyntaxException {
List<String> args = new ArrayList<>(options);
args.add("-v");
args.add("-d");
args.add(testDir.toAbsolutePath().toString());
for (String inputSample : inputSamples) {
URL resource = getClass().getClassLoader().getResource(inputSample);
assertThat(resource).isNotNull();
String sampleFile = resource.toURI().getRawPath();
args.add(sampleFile);
}
return args.toArray(new String[0]);
}
private void printFiles(List<Path> files) {
LOG.info("Output files (count: {}):", files.size());
for (Path file : files) {
LOG.info(" {}", testDir.relativize(file));
}
}
private static List<Path> collectJavaFilesInDir(Path dir) throws IOException {
PathMatcher javaMatcher = dir.getFileSystem().getPathMatcher("glob:**.java");
return collectFilesInDir(dir, javaMatcher);
}
private static List<Path> collectAllFilesInDir(Path dir) throws IOException {
try (Stream<Path> pathStream = Files.walk(dir)) {
return pathStream
.filter(Files::isRegularFile)
.collect(Collectors.toList());
}
}
private static List<Path> collectFilesInDir(Path dir, PathMatcher matcher) throws IOException {
try (Stream<Path> pathStream = Files.walk(dir)) {
return pathStream
.filter(p -> Files.isRegularFile(p, LinkOption.NOFOLLOW_LINKS))
.filter(matcher::matches)
.collect(Collectors.toList());
}
}
}
@@ -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");
}
}
Binary file not shown.
@@ -3,5 +3,5 @@ plugins {
}
dependencies {
implementation("dev.dirs:directories:26")
implementation("io.get-coursier.util:directories-jni:0.1.3")
}
@@ -6,10 +6,16 @@ import java.nio.file.Path;
import java.util.function.Function;
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;
@@ -57,10 +63,32 @@ public class JadxCommonFiles {
}
private synchronized ProjectDirectories loadDirs() {
if (dirs == null) {
dirs = ProjectDirectories.from("io.github", "skylot", "jadx");
ProjectDirectories currentDirs = dirs;
if (currentDirs != null) {
return currentDirs;
}
return dirs;
LOG.debug("Loading system dirs ...");
long start = System.currentTimeMillis();
ProjectDirectories loadedDirs = ProjectDirectories.from("io.github", "skylot", "jadx", DirsLoader::getWinDirs);
if (LOG.isDebugEnabled()) {
LOG.debug("Loaded system dirs ({}ms): config: {}, cache: {}",
System.currentTimeMillis() - start, loadedDirs.configDir, loadedDirs.cacheDir);
}
dirs = loadedDirs;
return loadedDirs;
}
/**
* Return JNI or Foreign implementation
*/
private static Windows getWinDirs() {
Windows defSup = Windows.getDefaultSupplier().get();
if (defSup instanceof WindowsPowerShell) {
return new WindowsJni();
}
return defSup;
}
public Path getCacheDir() {
+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,35 @@
package jadx.zip;
import java.io.Closeable;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.jetbrains.annotations.Nullable;
public class ZipContent implements Closeable {
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 = entries.stream().collect(Collectors.toMap(IZipEntry::getName, Function.identity()));
}
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,111 @@
package jadx.zip;
import java.io.File;
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.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 {
try {
JadxZipParser jadxParser = new JadxZipParser(zipFile, options);
IZipParser detectedParser = detectParser(zipFile, jadxParser);
if (detectedParser != jadxParser) {
jadxParser.close();
}
return detectedParser.open();
} 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) {
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) {
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,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,109 @@
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 IJadxZipSecurity zipSecurity;
private final boolean useLimitedDataStream;
private ZipFile zipFile;
public FallbackZipParser(File file, ZipReaderOptions options) {
this.file = file;
this.zipSecurity = options.getZipSecurity();
this.useLimitedDataStream = zipSecurity.useLimitedDataStream();
}
@Override
public ZipContent open() throws IOException {
zipFile = new ZipFile(file);
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);
}
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 {
try {
if (zipFile != null) {
zipFile.close();
}
} finally {
zipFile = null;
}
}
}
@@ -0,0 +1,46 @@
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;
}
public int read() throws IOException {
if (!buf.hasRemaining()) {
return -1;
}
return buf.get() & 0xFF;
}
@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,93 @@
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;
}
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,439 @@
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 RandomAccessFile file;
private FileChannel fileChannel;
private 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();
}
}
@SuppressWarnings("RedundantIfStatement")
public boolean canOpen() {
try {
load();
int eocdStart = searchEndOfCDStart();
ByteBuffer buf = byteBuffer;
buf.position(eocdStart + 4);
int diskNum = readU2(buf);
if (diskNum == 0xFFFF) {
// Zip64
return false;
}
return true;
} catch (Exception e) {
LOG.warn("Jadx parser can't open 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 void load() throws IOException {
if (byteBuffer != null) {
// already loaded
return;
}
file = new RandomAccessFile(zipFile, "r");
long size = file.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];
file.readFully(bytes);
byteBuffer = ByteBuffer.wrap(bytes).asReadOnlyBuffer();
file.close();
file = null;
} else {
// for big files - use a memory mapped file
fileChannel = file.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 = byteBuffer;
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 = byteBuffer;
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 = byteBuffer;
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 = byteBuffer;
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 = byteBuffer;
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 = byteBuffer;
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(byteBuffer, entry);
} catch (Exception e) {
entryParseFailed(entry, e);
return useFallbackParser(entry).getInputStream();
}
} else {
// treat any other compression methods values as UNCOMPRESSED
stream = bufferToStream(byteBuffer, 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(byteBuffer, entry);
} catch (Exception e) {
entryParseFailed(entry, e);
return useFallbackParser(entry).getBytes();
}
}
// treat any other compression methods values as UNCOMPRESSED
return bufferToBytes(byteBuffer, 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 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 = byteBuffer;
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);
}
@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,35 @@
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());
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,132 @@
package jadx.zip.security;
import java.io.File;
import java.io.IOException;
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 File CWD = getCWD();
/**
* 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;
}
}
try {
File currentPath = CWD;
File canonical = new File(currentPath, entryName).getCanonicalFile();
if (isInSubDirectoryInternal(currentPath, canonical)) {
return true;
}
} catch (Exception e) {
// check failed
}
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;
}
private static File getCWD() {
try {
return new File(".").getCanonicalFile();
} catch (IOException e) {
throw new RuntimeException("Failed to init current working dir constant", e);
}
}
}
+3 -2
View File
@@ -4,8 +4,9 @@ plugins {
dependencies {
api(project(":jadx-plugins:jadx-input-api"))
api(project(":jadx-commons:jadx-zip"))
implementation("com.google.code.gson:gson:2.11.0")
implementation("com.google.code.gson:gson:2.13.1")
testImplementation("org.apache.commons:commons-lang3:3.17.0")
@@ -21,7 +22,7 @@ dependencies {
strictly("[3.33, 3.34[") // from 3.34 compiled with Java 17
}
}
testImplementation("tools.profiler:async-profiler:3.0")
testImplementation("tools.profiler:async-profiler:4.0")
}
val jadxTestJavaVersion = getTestJavaVersion()
@@ -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);
}
}
}
+41 -6
View File
@@ -39,6 +39,7 @@ import jadx.api.usage.impl.InMemoryUsageInfoCache;
import jadx.core.deobf.DeobfAliasProvider;
import jadx.core.deobf.conditions.DeobfWhitelist;
import jadx.core.deobf.conditions.JadxRenameConditions;
import jadx.core.export.ExportGradleType;
import jadx.core.plugins.PluginContext;
import jadx.core.plugins.files.IJadxFilesGetter;
import jadx.core.plugins.files.TempFilesGetter;
@@ -90,6 +91,7 @@ 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
@@ -106,6 +108,7 @@ public class JadxArgs implements Closeable {
private boolean deobfuscationOn = false;
private UseSourceNameAsClassNameAlias useSourceNameAsClassNameAlias = UseSourceNameAsClassNameAlias.getDefault();
private int sourceNameRepeatLimit = 10;
private File generatedRenamesMappingFile = null;
private GeneratedRenamesMappingFileMode generatedRenamesMappingFileMode = GeneratedRenamesMappingFileMode.getDefault();
@@ -132,7 +135,7 @@ public class JadxArgs implements Closeable {
private boolean escapeUnicode = false;
private boolean replaceConsts = true;
private boolean respectBytecodeAccModifiers = false;
private boolean exportAsGradleProject = false;
private @Nullable ExportGradleType exportGradleType = null;
private boolean restoreSwitchOverString = true;
@@ -460,6 +463,14 @@ public class JadxArgs implements Closeable {
this.useSourceNameAsClassNameAlias = useSourceNameAsClassNameAlias;
}
public int getSourceNameRepeatLimit() {
return sourceNameRepeatLimit;
}
public void setSourceNameRepeatLimit(int sourceNameRepeatLimit) {
this.sourceNameRepeatLimit = sourceNameRepeatLimit;
}
/**
* @deprecated Use {@link #getUseSourceNameAsClassNameAlias()} instead.
*/
@@ -558,11 +569,25 @@ public class JadxArgs implements Closeable {
}
public boolean isExportAsGradleProject() {
return exportAsGradleProject;
return exportGradleType != null;
}
public void setExportAsGradleProject(boolean exportAsGradleProject) {
this.exportAsGradleProject = exportAsGradleProject;
if (exportAsGradleProject) {
if (exportGradleType == null) {
exportGradleType = ExportGradleType.AUTO;
}
} else {
exportGradleType = null;
}
}
public @Nullable ExportGradleType getExportGradleType() {
return exportGradleType;
}
public void setExportGradleType(@Nullable ExportGradleType exportGradleType) {
this.exportGradleType = exportGradleType;
}
public boolean isRestoreSwitchOverString() {
@@ -793,6 +818,14 @@ public class JadxArgs implements Closeable {
this.loadJadxClsSetFile = loadJadxClsSetFile;
}
public void setUseHeadersForDetectResourceExtensions(boolean useHeadersForDetectResourceExtensions) {
this.useHeadersForDetectResourceExtensions = useHeadersForDetectResourceExtensions;
}
public boolean isUseHeadersForDetectResourceExtensions() {
return useHeadersForDetectResourceExtensions;
}
/**
* Hash of all options that can change result code
*/
@@ -800,8 +833,8 @@ public class JadxArgs implements Closeable {
String argStr = "args:" + decompilationMode + useImports + showInconsistentCode
+ inlineAnonymousClasses + inlineMethods + moveInnerClasses + allowInlineKotlinLambda
+ deobfuscationOn + deobfuscationMinLength + deobfuscationMaxLength + deobfuscationWhitelist
+ useSourceNameAsClassNameAlias
+ resourceNameSource
+ useSourceNameAsClassNameAlias + sourceNameRepeatLimit
+ resourceNameSource + useHeadersForDetectResourceExtensions
+ useKotlinMethodsForVarNames
+ insertDebugLines + extractFinally
+ debugInfo + escapeUnicode + replaceConsts + restoreSwitchOverString
@@ -841,6 +874,7 @@ public class JadxArgs implements Closeable {
+ ", generatedRenamesMappingFileMode=" + generatedRenamesMappingFileMode
+ ", resourceNameSource=" + resourceNameSource
+ ", useSourceNameAsClassNameAlias=" + useSourceNameAsClassNameAlias
+ ", sourceNameRepeatLimit=" + sourceNameRepeatLimit
+ ", useKotlinMethodsForVarNames=" + useKotlinMethodsForVarNames
+ ", insertDebugLines=" + insertDebugLines
+ ", extractFinally=" + extractFinally
@@ -851,7 +885,7 @@ public class JadxArgs implements Closeable {
+ ", replaceConsts=" + replaceConsts
+ ", restoreSwitchOverString=" + restoreSwitchOverString
+ ", respectBytecodeAccModifiers=" + respectBytecodeAccModifiers
+ ", exportAsGradleProject=" + exportAsGradleProject
+ ", exportGradleType=" + exportGradleType
+ ", skipXmlPrettyPrint=" + skipXmlPrettyPrint
+ ", fsCaseSensitive=" + fsCaseSensitive
+ ", renameFlags=" + renameFlags
@@ -863,6 +897,7 @@ public class JadxArgs implements Closeable {
+ ", pluginOptions=" + pluginOptions
+ ", cfgOutput=" + cfgOutput
+ ", rawCFGOutput=" + rawCFGOutput
+ ", useHeadersForDetectResourceExtensions=" + useHeadersForDetectResourceExtensions
+ '}';
}
}
@@ -7,6 +7,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -42,7 +43,8 @@ import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.PackageNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.visitors.SaveCode;
import jadx.core.export.ExportGradleTask;
import jadx.core.export.ExportGradle;
import jadx.core.export.OutDirs;
import jadx.core.plugins.JadxPluginManager;
import jadx.core.plugins.PluginContext;
import jadx.core.plugins.events.JadxEventsImpl;
@@ -50,9 +52,9 @@ import jadx.core.utils.DecompilerScheduler;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
import jadx.core.utils.files.ZipPatch;
import jadx.core.utils.tasks.TaskExecutor;
import jadx.core.xmlgen.ResourcesSaver;
import jadx.zip.ZipReader;
/**
* Jadx API usage example:
@@ -87,6 +89,7 @@ public final class JadxDecompiler implements Closeable {
private final JadxArgs args;
private final JadxPluginManager pluginManager;
private final List<ICodeLoader> loadedInputs = new ArrayList<>();
private final ZipReader zipReader;
private RootNode root;
private List<JavaClass> classes;
@@ -98,6 +101,7 @@ public final class JadxDecompiler implements Closeable {
private final List<ICodeLoader> customCodeLoaders = new ArrayList<>();
private final List<CustomResourcesLoader> customResourcesLoaders = new ArrayList<>();
private final Map<JadxPassType, List<JadxPass>> customPasses = new HashMap<>();
private final List<Closeable> closeableList = new ArrayList<>();
private IJadxEvents events = new JadxEventsImpl();
@@ -109,6 +113,7 @@ public final class JadxDecompiler implements Closeable {
this.args = Objects.requireNonNull(args);
this.pluginManager = new JadxPluginManager(this);
this.resourcesLoader = new ResourcesLoader(this);
this.zipReader = new ZipReader(args.getSecurity());
}
public void load() {
@@ -119,13 +124,15 @@ public final class JadxDecompiler implements Closeable {
loadPlugins();
loadInputFiles();
root = new RootNode(args);
root = new RootNode(this);
root.init();
root.setDecompilerRef(this);
root.mergePasses(customPasses);
// load classes and resources
root.loadClasses(loadedInputs);
root.initClassPath();
root.loadResources(resourcesLoader, getResources());
root.finishClassLoad();
root.initClassPath();
// init passes
root.mergePasses(customPasses);
root.runPreDecompileStage();
root.initPasses();
loadFinished();
@@ -145,9 +152,7 @@ public final class JadxDecompiler implements Closeable {
private void loadInputFiles() {
loadedInputs.clear();
List<File> inputs = ZipPatch.patchZipFiles(args.getInputFiles());
args.setInputFiles(inputs);
List<Path> inputPaths = Utils.collectionMap(inputs, File::toPath);
List<Path> inputPaths = Utils.collectionMap(args.getInputFiles(), File::toPath);
List<Path> inputFiles = FileUtils.expandDirs(inputPaths);
long start = System.currentTimeMillis();
for (PluginContext plugin : pluginManager.getResolvedPluginContexts()) {
@@ -158,7 +163,7 @@ public final class JadxDecompiler implements Closeable {
loadedInputs.add(loader);
}
} catch (Exception e) {
throw new JadxRuntimeException("Failed to load code for plugin: " + plugin, e);
LOG.warn("Failed to load code for plugin: {}", plugin, e);
}
}
}
@@ -169,6 +174,7 @@ public final class JadxDecompiler implements Closeable {
}
private void reset() {
unloadPlugins();
root = null;
classes = null;
resources = null;
@@ -178,32 +184,27 @@ public final class JadxDecompiler implements Closeable {
@Override
public void close() {
reset();
closeInputs();
closeLoaders();
closeAll(loadedInputs);
closeAll(customCodeLoaders);
closeAll(customResourcesLoaders);
closeAll(closeableList);
FileUtils.deleteDirIfExists(args.getFilesGetter().getTempDir());
args.close();
FileUtils.clearTempRootDir();
}
private void closeInputs() {
loadedInputs.forEach(load -> {
try {
load.close();
} catch (Exception e) {
LOG.error("Failed to close input", e);
}
});
loadedInputs.clear();
}
private void closeLoaders() {
for (CustomResourcesLoader resourcesLoader : customResourcesLoaders) {
try {
resourcesLoader.close();
} catch (Exception e) {
LOG.error("Failed to close resource loader: {}", resourcesLoader, e);
private void closeAll(List<? extends Closeable> list) {
try {
for (Closeable closeable : list) {
try {
closeable.close();
} catch (Exception e) {
LOG.warn("Fail to close '{}'", closeable, e);
}
}
} finally {
list.clear();
}
customResourcesLoaders.clear();
}
private void loadPlugins() {
@@ -220,6 +221,10 @@ public final class JadxDecompiler implements Closeable {
}
}
private void unloadPlugins() {
pluginManager.unloadResolved();
}
private void loadFinished() {
LOG.debug("Load finished");
List<JadxPass> list = customPasses.get(JadxAfterLoadPass.TYPE);
@@ -297,31 +302,28 @@ public final class JadxDecompiler implements Closeable {
if (root == null) {
throw new JadxRuntimeException("No loaded files");
}
File sourcesOutDir;
File resOutDir;
ExportGradleTask gradleExportTask;
if (args.isExportAsGradleProject()) {
gradleExportTask = new ExportGradleTask(resources, root, args.getOutDir());
gradleExportTask.init();
sourcesOutDir = gradleExportTask.getSrcOutDir();
resOutDir = gradleExportTask.getResOutDir();
OutDirs outDirs;
ExportGradle gradleExport;
if (args.getExportGradleType() != null) {
gradleExport = new ExportGradle(root, args.getOutDir(), getResources());
outDirs = gradleExport.init();
} else {
sourcesOutDir = args.getOutDirSrc();
resOutDir = args.getOutDirRes();
gradleExportTask = null;
gradleExport = null;
outDirs = new OutDirs(args.getOutDirSrc(), args.getOutDirRes());
outDirs.makeDirs();
}
TaskExecutor executor = new TaskExecutor();
executor.setThreadsCount(args.getThreadsCount());
if (saveResources) {
// save resources first because decompilation can stop or fail
appendResourcesSaveTasks(executor, resOutDir);
appendResourcesSaveTasks(executor, outDirs.getResOutDir());
}
if (saveSources) {
appendSourcesSave(executor, sourcesOutDir);
appendSourcesSave(executor, outDirs.getSrcOutDir());
}
if (gradleExportTask != null) {
executor.addSequentialTask(gradleExportTask);
if (gradleExport != null) {
executor.addSequentialTask(gradleExport::generateGradleFiles);
}
return executor;
}
@@ -333,13 +335,15 @@ public final class JadxDecompiler implements Closeable {
// process AndroidManifest.xml first to load complete resource ids table
for (ResourceFile resourceFile : getResources()) {
if (resourceFile.getType() == ResourceType.MANIFEST) {
new ResourcesSaver(outDir, resourceFile).run();
new ResourcesSaver(this, outDir, resourceFile).run();
break;
}
}
Set<String> inputFileNames = args.getInputFiles().stream()
.map(File::getAbsolutePath)
.collect(Collectors.toSet());
Set<String> codeSources = collectCodeSources();
List<Runnable> tasks = new ArrayList<>();
for (ResourceFile resourceFile : getResources()) {
ResourceType resType = resourceFile.getType();
@@ -347,16 +351,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);
@@ -587,6 +619,8 @@ public final class JadxDecompiler implements Closeable {
return convertMethodNode((MethodNode) ann);
case FIELD:
return convertFieldNode((FieldNode) ann);
case PKG:
return convertPackageNode((PackageNode) ann);
case DECLARATION:
return getJavaNodeByCodeAnnotation(codeInfo, ((NodeDeclareRef) ann).getNode());
case VAR:
@@ -700,6 +734,14 @@ public final class JadxDecompiler implements Closeable {
return resourcesLoader;
}
public ZipReader getZipReader() {
return zipReader;
}
public void addCloseable(Closeable closeable) {
closeableList.add(closeable);
}
@Override
public String toString() {
return "jadx decompiler " + getVersion();
@@ -122,8 +122,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 +129,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 +142,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 +153,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,7 +181,6 @@ public final class JavaClass implements JavaNode {
mths.sort(Comparator.comparing(JavaMethod::getName));
this.methods = Collections.unmodifiableList(mths);
}
return code;
}
JadxDecompiler getRootDecompiler() {
@@ -2,39 +2,21 @@ package jadx.api;
import java.io.File;
import jadx.api.plugins.utils.ZipSecurity;
import org.jetbrains.annotations.Nullable;
import jadx.core.deobf.FileTypeDetector;
import jadx.core.utils.StringUtils;
import jadx.core.utils.exceptions.JadxException;
import jadx.core.xmlgen.ResContainer;
import jadx.core.xmlgen.entry.ResourceEntry;
import jadx.zip.IZipEntry;
public class ResourceFile {
public static final class ZipRef {
private final File zipFile;
private final String entryName;
public ZipRef(File zipFile, String entryName) {
this.zipFile = zipFile;
this.entryName = entryName;
}
public File getZipFile() {
return zipFile;
}
public String getEntryName() {
return entryName;
}
@Override
public String toString() {
return "ZipRef{" + zipFile + ", '" + entryName + "'}";
}
}
private final JadxDecompiler decompiler;
private final String name;
private final ResourceType type;
private ZipRef zipRef;
private ResourceType type;
private @Nullable IZipEntry zipEntry;
private String deobfName;
public static ResourceFile createResourceFile(JadxDecompiler decompiler, File file, ResourceType type) {
@@ -42,7 +24,7 @@ public class ResourceFile {
}
public static ResourceFile createResourceFile(JadxDecompiler decompiler, String name, ResourceType type) {
if (!ZipSecurity.isValidZipEntryName(name)) {
if (!decompiler.getArgs().getSecurity().isValidEntryName(name)) {
return null;
}
return new ResourceFile(decompiler, name, type);
@@ -74,28 +56,73 @@ public class ResourceFile {
return ResourcesLoader.loadContent(decompiler, this);
}
void setZipRef(ZipRef zipRef) {
this.zipRef = zipRef;
}
public boolean setAlias(ResourceEntry ri) {
public boolean setAlias(ResourceEntry entry, boolean useHeaders) {
StringBuilder sb = new StringBuilder();
sb.append("res/").append(ri.getTypeName()).append(ri.getConfig());
sb.append("/").append(ri.getKeyName());
int lastDot = name.lastIndexOf('.');
if (lastDot != -1) {
sb.append(name.substring(lastDot));
sb.append("res/").append(entry.getTypeName()).append(entry.getConfig());
sb.append("/").append(entry.getKeyName());
if (useHeaders) {
try {
int maxBytesToReadLimit = 4096;
byte[] bytes = ResourcesLoader.decodeStream(this, (size, is) -> {
int bytesToRead;
if (size > 0) {
bytesToRead = (int) Math.min(size, maxBytesToReadLimit);
} else if (size == 0) {
bytesToRead = 0;
} else {
bytesToRead = maxBytesToReadLimit;
}
if (bytesToRead == 0) {
return new byte[0];
}
return is.readNBytes(bytesToRead);
});
String fileExtension = FileTypeDetector.detectFileExtension(bytes);
if (!StringUtils.isEmpty(fileExtension)) {
sb.append(fileExtension);
} else {
sb.append(getExtFromName(name));
}
} catch (JadxException ignored) {
}
} else {
sb.append(getExtFromName(name));
}
String alias = sb.toString();
if (!alias.equals(name)) {
setDeobfName(alias);
type = ResourceType.getFileType(alias);
return true;
}
return false;
}
public ZipRef getZipRef() {
return zipRef;
private String getExtFromName(String name) {
// the image .9.png extension always saved, when resource shrinking by aapt2
if (name.contains(".9.png")) {
return ".9.png";
}
int lastDot = name.lastIndexOf('.');
if (lastDot != -1) {
return name.substring(lastDot);
}
return "";
}
public @Nullable IZipEntry getZipEntry() {
return zipEntry;
}
void setZipEntry(@Nullable IZipEntry zipEntry) {
this.zipEntry = zipEntry;
}
public JadxDecompiler getDecompiler() {
return decompiler;
}
@Override
@@ -0,0 +1,17 @@
package jadx.api;
import jadx.core.xmlgen.ResContainer;
public class ResourceFileContainer extends ResourceFile {
private final ResContainer container;
public ResourceFileContainer(String name, ResourceType type, ResContainer container) {
super(null, name, type);
this.container = container;
}
@Override
public ResContainer loadContent() {
return container;
}
}
@@ -10,9 +10,15 @@ public enum ResourceType {
CODE(".dex", ".jar", ".class"),
XML(".xml"),
ARSC(".arsc"),
FONT(".ttf", ".otf"),
IMG(".png", ".gif", ".jpg"),
MEDIA(".mp3", ".wav"),
APK(".apk", ".apkm", ".apks"),
FONT(".ttf", ".ttc", ".otf"),
IMG(".png", ".gif", ".jpg", ".webp", ".bmp", ".tiff"),
ARCHIVE(".zip", ".rar", ".7zip", ".7z", ".arj", ".tar", ".gzip", ".bzip", ".bzip2", ".cab", ".cpio", ".ar", ".gz", ".tgz", ".bz2"),
VIDEOS(".mp4", ".mkv", ".webm", ".avi", ".flv", ".3gp"),
SOUNDS(".aac", ".ogg", ".opus", ".mp3", ".wav", ".wma", ".mid", ".midi"),
JSON(".json"),
TEXT(".txt", ".ini", ".conf", ".yaml", ".properties", ".js"),
HTML(".html"),
LIB(".so"),
MANIFEST,
UNKNOWN;
@@ -6,31 +6,30 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.ResourceFile.ZipRef;
import jadx.api.impl.SimpleCodeInfo;
import jadx.api.plugins.CustomResourcesLoader;
import jadx.api.plugins.resources.IResContainerFactory;
import jadx.api.plugins.resources.IResTableParserProvider;
import jadx.api.plugins.resources.IResourcesLoader;
import jadx.api.plugins.utils.ZipSecurity;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.Utils;
import jadx.core.utils.android.Res9patchStreamDecoder;
import jadx.core.utils.exceptions.JadxException;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
import jadx.core.utils.files.ZipFile;
import jadx.core.xmlgen.BinaryXMLParser;
import jadx.core.xmlgen.IResTableParser;
import jadx.core.xmlgen.ResContainer;
import jadx.core.xmlgen.ResTableBinaryParserProvider;
import jadx.zip.IZipEntry;
import jadx.zip.ZipContent;
import static jadx.core.utils.files.FileUtils.READ_BUFFER_SIZE;
import static jadx.core.utils.files.FileUtils.copyStream;
@@ -39,21 +38,21 @@ import static jadx.core.utils.files.FileUtils.copyStream;
public final class ResourcesLoader implements IResourcesLoader {
private static final Logger LOG = LoggerFactory.getLogger(ResourcesLoader.class);
private final JadxDecompiler jadxRef;
private final JadxDecompiler decompiler;
private final List<IResTableParserProvider> resTableParserProviders = new ArrayList<>();
private final List<IResContainerFactory> resContainerFactories = new ArrayList<>();
private BinaryXMLParser binaryXmlParser;
ResourcesLoader(JadxDecompiler jadxRef) {
this.jadxRef = jadxRef;
ResourcesLoader(JadxDecompiler decompiler) {
this.decompiler = decompiler;
this.resTableParserProviders.add(new ResTableBinaryParserProvider());
}
List<ResourceFile> load(RootNode root) {
init(root);
List<File> inputFiles = jadxRef.getArgs().getInputFiles();
List<File> inputFiles = decompiler.getArgs().getInputFiles();
List<ResourceFile> list = new ArrayList<>(inputFiles.size());
for (File file : inputFiles) {
loadFile(list, file);
@@ -94,28 +93,19 @@ public final class ResourcesLoader implements IResourcesLoader {
public static <T> T decodeStream(ResourceFile rf, ResourceDecoder<T> decoder) throws JadxException {
try {
ZipRef zipRef = rf.getZipRef();
if (zipRef == null) {
IZipEntry zipEntry = rf.getZipEntry();
if (zipEntry != null) {
try (InputStream inputStream = zipEntry.getInputStream()) {
return decoder.decode(zipEntry.getUncompressedSize(), inputStream);
}
} else {
File file = new File(rf.getOriginalName());
try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) {
return decoder.decode(file.length(), inputStream);
}
} else {
try (ZipFile zipFile = new ZipFile(zipRef.getZipFile())) {
ZipEntry entry = zipFile.getEntry(zipRef.getEntryName());
if (entry == null) {
throw new IOException("Zip entry not found: " + zipRef);
}
if (!ZipSecurity.isValidZipEntry(entry)) {
return null;
}
try (InputStream inputStream = ZipSecurity.getInputStreamForEntry(zipFile, entry)) {
return decoder.decode(entry.getSize(), inputStream);
}
}
}
} catch (Exception e) {
throw new JadxException("Error decode: " + rf.getDeobfName(), e);
throw new JadxException("Error decode: " + rf.getOriginalName(), e);
}
}
@@ -170,12 +160,13 @@ public final class ResourcesLoader implements IResourcesLoader {
if (parser == null) {
throw new JadxRuntimeException("Unknown type of resource file: " + resFile.getOriginalName());
}
parser.setBaseFileName(resFile.getDeobfName());
parser.decode(is);
return parser;
}
private static ResContainer decodeImage(ResourceFile rf, InputStream inputStream) {
String name = rf.getOriginalName();
String name = rf.getDeobfName();
if (name.endsWith(".9.png")) {
try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
Res9patchStreamDecoder decoder = new Res9patchStreamDecoder();
@@ -195,7 +186,7 @@ public final class ResourcesLoader implements IResourcesLoader {
}
// Try to load the resources with a custom loader first
for (CustomResourcesLoader loader : jadxRef.getCustomResourcesLoaders()) {
for (CustomResourcesLoader loader : decompiler.getCustomResourcesLoaders()) {
if (loader.load(this, list, file)) {
LOG.debug("Custom loader used for {}", file.getAbsolutePath());
return;
@@ -208,25 +199,31 @@ public final class ResourcesLoader implements IResourcesLoader {
public void defaultLoadFile(List<ResourceFile> list, File file, String subDir) {
if (FileUtils.isZipFile(file)) {
ZipSecurity.visitZipEntries(file, (zipFile, entry) -> {
addEntry(list, file, entry, subDir);
return null;
});
try {
ZipContent zipContent = decompiler.getZipReader().open(file);
// do not close a zip now, entry content will be read later
decompiler.addCloseable(zipContent);
for (IZipEntry entry : zipContent.getEntries()) {
addEntry(list, file, entry, subDir);
}
} catch (Exception e) {
throw new RuntimeException("Failed to open zip file: " + file.getAbsolutePath(), e);
}
} else {
ResourceType type = ResourceType.getFileType(file.getAbsolutePath());
list.add(ResourceFile.createResourceFile(jadxRef, file, type));
list.add(ResourceFile.createResourceFile(decompiler, file, type));
}
}
public void addEntry(List<ResourceFile> list, File zipFile, ZipEntry entry, String subDir) {
public void addEntry(List<ResourceFile> list, File zipFile, IZipEntry entry, String subDir) {
if (entry.isDirectory()) {
return;
}
String name = entry.getName();
ResourceType type = ResourceType.getFileType(name);
ResourceFile rf = ResourceFile.createResourceFile(jadxRef, subDir + name, type);
ResourceFile rf = ResourceFile.createResourceFile(decompiler, subDir + name, type);
if (rf != null) {
rf.setZipRef(new ZipRef(zipFile, name));
rf.setZipEntry(entry);
list.add(rf);
}
}
@@ -234,12 +231,12 @@ public final class ResourcesLoader implements IResourcesLoader {
public static ICodeInfo loadToCodeWriter(InputStream is) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream(READ_BUFFER_SIZE);
copyStream(is, baos);
return new SimpleCodeInfo(baos.toString("UTF-8"));
return new SimpleCodeInfo(baos.toString(StandardCharsets.UTF_8));
}
private synchronized BinaryXMLParser loadBinaryXmlParser() {
if (binaryXmlParser == null) {
binaryXmlParser = new BinaryXMLParser(jadxRef.getRoot());
binaryXmlParser = new BinaryXMLParser(decompiler.getRoot());
}
return binaryXmlParser;
}
@@ -0,0 +1,32 @@
package jadx.api.gui.tree;
import javax.swing.Icon;
import javax.swing.tree.TreeNode;
import org.jetbrains.annotations.Nullable;
import jadx.api.metadata.ICodeNodeRef;
public interface ITreeNode extends TreeNode {
/**
* Locale independent node identifier
*/
String getID();
/**
* Node title
*/
String getName();
/**
* Node icon
*/
Icon getIcon();
/**
* Related code node reference.
*/
@Nullable
ICodeNodeRef getCodeNodeRef();
}
@@ -11,7 +11,6 @@ import jadx.api.JadxArgs;
import jadx.api.metadata.ICodeAnnotation;
import jadx.api.metadata.ICodeNodeRef;
import jadx.api.metadata.annotations.NodeDeclareRef;
import jadx.api.metadata.annotations.VarRef;
import jadx.core.utils.StringUtils;
public class AnnotatedCodeWriter extends SimpleCodeWriter implements ICodeWriter {
@@ -151,7 +150,6 @@ public class AnnotatedCodeWriter extends SimpleCodeWriter implements ICodeWriter
@Override
public ICodeInfo finish() {
validateAnnotations();
String code = buf.toString();
buf = null;
return new AnnotatedCodeInfo(code, lineMap, annotations);
@@ -161,17 +159,4 @@ public class AnnotatedCodeWriter extends SimpleCodeWriter implements ICodeWriter
public Map<Integer, ICodeAnnotation> getRawAnnotations() {
return annotations;
}
private void validateAnnotations() {
if (annotations.isEmpty()) {
return;
}
annotations.values().removeIf(v -> {
if (v.getAnnType() == ICodeAnnotation.AnnType.VAR_REF) {
VarRef varRef = (VarRef) v;
return varRef.getRefPos() == 0;
}
return false;
});
}
}
@@ -23,4 +23,12 @@ public interface JadxPlugin {
* For long operation, prefer {@link JadxPreparePass} or {@link JadxAfterLoadPass} instead.
*/
void init(JadxPluginContext context);
/**
* Plugin unload handler.
* Can be used to clean up resources on plugin unloading.
*/
default void unload() {
// optional method
}
}
@@ -14,6 +14,7 @@ import jadx.api.plugins.input.JadxCodeInput;
import jadx.api.plugins.options.JadxPluginOptions;
import jadx.api.plugins.pass.JadxPass;
import jadx.api.plugins.resources.IResourcesLoader;
import jadx.zip.ZipReader;
public interface JadxPluginContext {
@@ -59,4 +60,9 @@ public interface JadxPluginContext {
* Access to plugin specific files and directories
*/
IJadxFiles files();
/**
* Custom jadx zip reader to fight tampering and provide additional security checks
*/
ZipReader getZipReader();
}
@@ -26,4 +26,13 @@ public interface ISettingsGroup {
default List<ISettingsGroup> getSubGroups() {
return Collections.emptyList();
}
/**
* Settings close handler.
* Apply settings if 'save' param set to true.
* It can be used to clean up resources.
*/
default void close(boolean save) {
// optional method
}
}
@@ -2,12 +2,16 @@ package jadx.api.plugins.gui;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.KeyStroke;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import jadx.api.gui.tree.ITreeNode;
import jadx.api.metadata.ICodeNodeRef;
public interface JadxGuiContext {
@@ -34,6 +38,15 @@ public interface JadxGuiContext {
@Nullable String keyBinding,
Consumer<ICodeNodeRef> action);
/**
* Add popup menu entry for tree node
*
* @param name entry title
* @param addPredicate check if entry should be added for provided node, called on popup creation
*/
@ApiStatus.Experimental
void addTreePopupMenuEntry(String name, Predicate<ITreeNode> addPredicate, Consumer<ITreeNode> action);
/**
* Attach new key binding to main window
*
@@ -57,6 +70,16 @@ public interface JadxGuiContext {
*/
JFrame getMainFrame();
/**
* Load SVG icon from jadx resources.
* All available icons can be found in "jadx-gui/src/main/resources/icons".
* Method is thread-safe.
*
* @param name short name in form: "category/iconName", example: "nodes/publicClass"
* @return loaded and cached icon, if icon not found returns default icon: "ui/error"
*/
ImageIcon getSVGIcon(String name);
ICodeNodeRef getNodeUnderCaret();
ICodeNodeRef getNodeUnderMouse();
@@ -72,6 +95,11 @@ public interface JadxGuiContext {
*/
boolean open(ICodeNodeRef ref);
/**
* Open usage dialog for a node
*/
void openUsageDialog(ICodeNodeRef ref);
/**
* Reload code in active tab
*/
@@ -4,63 +4,52 @@ import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.JadxDecompiler;
import jadx.api.plugins.JadxPluginContext;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.ZipFile;
import jadx.zip.IZipEntry;
import jadx.zip.ZipReader;
import jadx.zip.io.LimitedInputStream;
import jadx.zip.security.DisabledZipSecurity;
import jadx.zip.security.IJadxZipSecurity;
import jadx.zip.security.JadxZipSecurity;
/**
* Deprecated, migrate to {@link ZipReader}. <br>
* Prefer already configured instance from {@link JadxDecompiler#getZipReader()} or
* {@link JadxPluginContext#getZipReader()}.
*/
@Deprecated
public class ZipSecurity {
private static final Logger LOG = LoggerFactory.getLogger(ZipSecurity.class);
private static final boolean DISABLE_CHECKS = Utils.getEnvVarBool("JADX_DISABLE_ZIP_SECURITY", false);
/**
* size of uncompressed zip entry shouldn't be bigger of compressed in
* {@link #ZIP_BOMB_DETECTION_FACTOR} times
*/
private static final int ZIP_BOMB_DETECTION_FACTOR = 100;
/**
* Zip entries that have an uncompressed size of less than {@link #ZIP_BOMB_MIN_UNCOMPRESSED_SIZE}
* are considered safe
*/
private static final int ZIP_BOMB_MIN_UNCOMPRESSED_SIZE = 25 * 1024 * 1024;
private static final int MAX_ENTRIES_COUNT = Utils.getEnvVarInt("JADX_ZIP_MAX_ENTRIES_COUNT", 100_000);
private static final IJadxZipSecurity ZIP_SECURITY = buildZipSecurity();
private static final ZipReader ZIP_READER = new ZipReader(ZIP_SECURITY);
private static IJadxZipSecurity buildZipSecurity() {
if (DISABLE_CHECKS) {
return DisabledZipSecurity.INSTANCE;
}
JadxZipSecurity jadxZipSecurity = new JadxZipSecurity();
jadxZipSecurity.setMaxEntriesCount(MAX_ENTRIES_COUNT);
return jadxZipSecurity;
}
private ZipSecurity() {
}
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 static boolean isInSubDirectory(File baseDir, File file) {
if (DISABLE_CHECKS) {
return true;
}
try {
return isInSubDirectoryInternal(baseDir.getCanonicalFile(), file.getCanonicalFile());
} catch (IOException e) {
return false;
}
return ZIP_SECURITY.isInSubDirectory(baseDir, file);
}
/**
@@ -68,48 +57,15 @@ public class ZipSecurity {
* to limit output only to the specified directory
*/
public static boolean isValidZipEntryName(String entryName) {
if (DISABLE_CHECKS) {
return true;
}
if (entryName.contains("..")) { // quick pre-check
if (entryName.contains("../") || entryName.contains("..\\")) {
LOG.error("Path traversal attack detected in entry: '{}'", entryName);
return false;
}
}
try {
File currentPath = CommonFileUtils.CWD;
File canonical = new File(currentPath, entryName).getCanonicalFile();
if (isInSubDirectoryInternal(currentPath, canonical)) {
return true;
}
} catch (Exception e) {
// check failed
}
LOG.error("Invalid file name or path traversal attack detected: {}", entryName);
return false;
return ZIP_SECURITY.isValidEntryName(entryName);
}
public static boolean isZipBomb(ZipEntry entry) {
if (DISABLE_CHECKS) {
return false;
}
long compressedSize = entry.getCompressedSize();
long uncompressedSize = entry.getSize();
boolean invalidSize = (compressedSize < 0) || (uncompressedSize < 0);
boolean possibleZipBomb = (uncompressedSize >= ZIP_BOMB_MIN_UNCOMPRESSED_SIZE)
&& (compressedSize * ZIP_BOMB_DETECTION_FACTOR < uncompressedSize);
if (invalidSize || possibleZipBomb) {
LOG.error("Potential zip bomb attack detected, invalid sizes: compressed {}, uncompressed {}, name {}",
compressedSize, uncompressedSize, entry.getName());
return true;
}
return false;
public static boolean isZipBomb(IZipEntry entry) {
return !ZIP_SECURITY.isValidEntry(entry);
}
public static boolean isValidZipEntry(ZipEntry entry) {
return isValidZipEntryName(entry.getName())
&& !isZipBomb(entry);
public static boolean isValidZipEntry(IZipEntry entry) {
return ZIP_SECURITY.isValidEntry(entry);
}
public static InputStream getInputStreamForEntry(ZipFile zipFile, ZipEntry entry) throws IOException {
@@ -122,44 +78,15 @@ public class ZipSecurity {
}
/**
* Visit valid entries in zip file.
* Visit valid entries in a zip file.
* Return not null value from visitor to stop iteration.
*/
@Nullable
public static <R> R visitZipEntries(File file, BiFunction<ZipFile, ZipEntry, R> visitor) {
try (ZipFile zip = new ZipFile(file)) {
Enumeration<? extends ZipEntry> entries = zip.entries();
int entriesProcessed = 0;
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
if (isValidZipEntry(entry)) {
R result = visitor.apply(zip, entry);
if (result != null) {
return result;
}
entriesProcessed++;
if (!DISABLE_CHECKS && entriesProcessed > MAX_ENTRIES_COUNT) {
throw new JadxRuntimeException("Zip entries count limit exceeded: " + MAX_ENTRIES_COUNT
+ ", last entry: " + entry.getName());
}
}
}
} catch (Exception e) {
throw new JadxRuntimeException("Failed to process zip file: " + file.getAbsolutePath(), e);
}
return null;
public static <R> R visitZipEntries(File file, Function<IZipEntry, R> visitor) {
return ZIP_READER.visitEntries(file, visitor);
}
public static void readZipEntries(File file, BiConsumer<ZipEntry, InputStream> visitor) {
visitZipEntries(file, (zip, entry) -> {
if (!entry.isDirectory()) {
try (InputStream in = getInputStreamForEntry(zip, entry)) {
visitor.accept(entry, in);
} catch (Exception e) {
throw new JadxRuntimeException("Failed to process zip entry: " + entry.getName());
}
}
return null;
});
public static void readZipEntries(File file, BiConsumer<IZipEntry, InputStream> visitor) {
ZIP_READER.readEntries(file, visitor);
}
}
@@ -4,7 +4,9 @@ import java.io.InputStream;
import org.w3c.dom.Document;
public interface IJadxSecurity {
import jadx.zip.security.IJadxZipSecurity;
public interface IJadxSecurity extends IJadxZipSecurity {
/**
* Check if application package is safe
@@ -6,7 +6,8 @@ import java.util.Set;
public enum JadxSecurityFlag {
VERIFY_APP_PACKAGE,
SECURE_XML_PARSER;
SECURE_XML_PARSER,
SECURE_ZIP_READER;
public static Set<JadxSecurityFlag> all() {
return EnumSet.allOf(JadxSecurityFlag.class);
@@ -1,5 +1,6 @@
package jadx.api.security.impl;
import java.io.File;
import java.io.InputStream;
import java.util.Set;
@@ -12,14 +13,52 @@ import org.w3c.dom.Document;
import jadx.api.security.IJadxSecurity;
import jadx.api.security.JadxSecurityFlag;
import jadx.core.deobf.NameMapper;
import jadx.zip.IZipEntry;
import jadx.zip.security.DisabledZipSecurity;
import jadx.zip.security.IJadxZipSecurity;
import jadx.zip.security.JadxZipSecurity;
import static jadx.api.security.JadxSecurityFlag.SECURE_ZIP_READER;
public class JadxSecurity implements IJadxSecurity {
private static final Logger LOG = LoggerFactory.getLogger(JadxSecurity.class);
private final Set<JadxSecurityFlag> flags;
private final IJadxZipSecurity zipSecurity;
public JadxSecurity(Set<JadxSecurityFlag> flags) {
this.flags = flags;
this.zipSecurity = flags.contains(SECURE_ZIP_READER) ? new JadxZipSecurity() : DisabledZipSecurity.INSTANCE;
}
public JadxSecurity(Set<JadxSecurityFlag> flags, IJadxZipSecurity zipSecurity) {
this.flags = flags;
this.zipSecurity = zipSecurity;
}
@Override
public boolean isValidEntry(IZipEntry entry) {
return zipSecurity.isValidEntry(entry);
}
@Override
public boolean isValidEntryName(String entryName) {
return zipSecurity.isValidEntryName(entryName);
}
@Override
public boolean isInSubDirectory(File baseDir, File file) {
return zipSecurity.isInSubDirectory(baseDir, file);
}
@Override
public boolean useLimitedDataStream() {
return zipSecurity.useLimitedDataStream();
}
@Override
public int getMaxEntriesCount() {
return zipSecurity.getMaxEntriesCount();
}
@Override
@@ -16,7 +16,9 @@ public class Consts {
public static final String CLASS_STRING = "java.lang.String";
public static final String CLASS_CLASS = "java.lang.Class";
public static final String CLASS_THROWABLE = "java.lang.Throwable";
public static final String CLASS_ERROR = "java.lang.Error";
public static final String CLASS_EXCEPTION = "java.lang.Exception";
public static final String CLASS_RUNTIME_EXCEPTION = "java.lang.RuntimeException";
public static final String CLASS_ENUM = "java.lang.Enum";
public static final String CLASS_STRING_BUILDER = "java.lang.StringBuilder";
+8 -1
View File
@@ -16,6 +16,7 @@ import jadx.core.deobf.DeobfuscatorVisitor;
import jadx.core.deobf.SaveDeobfMapping;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.visitors.AnonymousClassVisitor;
import jadx.core.dex.visitors.ApplyVariableNames;
import jadx.core.dex.visitors.AttachCommentsVisitor;
import jadx.core.dex.visitors.AttachMethodDetails;
import jadx.core.dex.visitors.AttachTryCatchVisitor;
@@ -35,6 +36,7 @@ import jadx.core.dex.visitors.InitCodeVariables;
import jadx.core.dex.visitors.InlineMethods;
import jadx.core.dex.visitors.MarkMethodsForInline;
import jadx.core.dex.visitors.MethodInvokeVisitor;
import jadx.core.dex.visitors.MethodThrowsVisitor;
import jadx.core.dex.visitors.MethodVisitor;
import jadx.core.dex.visitors.ModVisitor;
import jadx.core.dex.visitors.MoveInlineVisitor;
@@ -54,6 +56,7 @@ import jadx.core.dex.visitors.debuginfo.DebugInfoApplyVisitor;
import jadx.core.dex.visitors.debuginfo.DebugInfoAttachVisitor;
import jadx.core.dex.visitors.finaly.MarkFinallyVisitor;
import jadx.core.dex.visitors.fixaccessmodifiers.FixAccessModifiers;
import jadx.core.dex.visitors.gradle.NonFinalResIdsVisitor;
import jadx.core.dex.visitors.kotlin.ProcessKotlinInternals;
import jadx.core.dex.visitors.prepare.AddAndroidConstants;
import jadx.core.dex.visitors.prepare.CollectConstValues;
@@ -101,7 +104,6 @@ public class Jadx {
passes.add(new SignatureProcessor());
passes.add(new OverrideMethodVisitor());
passes.add(new AddAndroidConstants());
passes.add(new CollectConstValues());
// rename and deobfuscation
passes.add(new DeobfuscatorVisitor());
@@ -110,6 +112,7 @@ public class Jadx {
passes.add(new SaveDeobfMapping());
passes.add(new UsageInfoVisitor());
passes.add(new CollectConstValues());
passes.add(new ProcessAnonymous());
passes.add(new ProcessMethodsForInline());
return passes;
@@ -179,6 +182,8 @@ public class Jadx {
passes.add(new ReturnVisitor());
passes.add(new CleanRegions());
passes.add(new MethodThrowsVisitor());
passes.add(new CodeShrinkVisitor());
passes.add(new MethodInvokeVisitor());
passes.add(new SimplifyVisitor());
@@ -186,6 +191,7 @@ public class Jadx {
passes.add(new EnumVisitor());
passes.add(new FixSwitchOverEnum());
passes.add(new NonFinalResIdsVisitor());
passes.add(new ExtractFieldInit());
passes.add(new FixAccessModifiers());
passes.add(new ClassModifier());
@@ -195,6 +201,7 @@ public class Jadx {
passes.add(new MarkMethodsForInline());
}
passes.add(new ProcessVariables());
passes.add(new ApplyVariableNames());
passes.add(new PrepareForCodeGen());
if (args.isCfgOutput()) {
passes.add(DotGraphVisitor.dumpRegions());
@@ -347,6 +347,10 @@ public class ClassGen {
* Additional checks for inlined methods
*/
private boolean skipMethod(MethodNode mth) {
if (cls.root().getArgs().getDecompilationMode().isSpecial()) {
// show all methods for special decompilation modes
return false;
}
MethodInlineAttr inlineAttr = mth.get(AType.METHOD_INLINE);
if (inlineAttr == null || inlineAttr.notNeeded()) {
return false;
@@ -81,8 +81,9 @@ public class MethodGen {
public boolean addDefinition(ICodeWriter code) {
if (mth.getMethodInfo().isClassInit()) {
code.startLine();
code.attachDefinition(mth);
code.startLine("static");
code.add("static");
return true;
}
if (mth.contains(AFlag.ANONYMOUS_CONSTRUCTOR)) {
@@ -2,57 +2,22 @@ package jadx.core.codegen;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import jadx.core.Consts;
import jadx.core.deobf.NameMapper;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.nodes.LoopLabelAttr;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.instructions.InvokeNode;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.instructions.args.CodeVar;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.InsnWrapArg;
import jadx.core.dex.instructions.args.NamedArg;
import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.instructions.args.SSAVar;
import jadx.core.dex.instructions.mods.ConstructorInsn;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.StringUtils;
import jadx.core.utils.Utils;
public class NameGen {
private static final Map<String, String> OBJ_ALIAS;
private final Set<String> varNames = new HashSet<>();
private final MethodNode mth;
private final boolean fallback;
static {
OBJ_ALIAS = Utils.newConstStringMap(
Consts.CLASS_STRING, "str",
Consts.CLASS_CLASS, "cls",
Consts.CLASS_THROWABLE, "th",
Consts.CLASS_OBJECT, "obj",
"java.util.Iterator", "it",
"java.lang.Boolean", "bool",
"java.lang.Short", "sh",
"java.lang.Integer", "num",
"java.lang.Character", "ch",
"java.lang.Byte", "b",
"java.lang.Float", "f",
"java.lang.Long", "l",
"java.lang.Double", "d",
"java.lang.StringBuilder", "sb",
"java.lang.Exception", "exc");
}
private final Set<String> varNames = new HashSet<>();
public NameGen(MethodNode mth, ClassGen classGen) {
this.mth = mth;
@@ -99,9 +64,9 @@ public class NameGen {
if (fallback) {
return name;
}
name = getUniqueVarName(name);
arg.setName(name);
return name;
String uniqName = getUniqueVarName(name);
arg.setName(uniqName);
return uniqName;
}
public String useArg(RegisterArg arg) {
@@ -132,13 +97,10 @@ public class NameGen {
private String makeArgName(CodeVar var) {
String name = var.getName();
if (name == null) {
name = guessName(var);
if (NameMapper.isValidAndPrintable(name)) {
return name;
}
if (!NameMapper.isValidAndPrintable(name)) {
name = getFallbackName(var);
}
return name;
return getFallbackName(var);
}
private String getFallbackName(CodeVar var) {
@@ -152,153 +114,4 @@ public class NameGen {
private String getFallbackName(RegisterArg arg) {
return "r" + arg.getRegNum();
}
private String guessName(CodeVar var) {
List<SSAVar> ssaVars = var.getSsaVars();
if (ssaVars != null && !ssaVars.isEmpty()) {
// TODO: use all vars for better name generation
SSAVar ssaVar = ssaVars.get(0);
if (ssaVar != null && ssaVar.getName() == null) {
RegisterArg assignArg = ssaVar.getAssign();
InsnNode assignInsn = assignArg.getParentInsn();
if (assignInsn != null) {
String name = makeNameFromInsn(assignInsn);
if (name != null && NameMapper.isValidAndPrintable(name)) {
return name;
}
}
}
}
return makeNameForType(var.getType());
}
private String makeNameForType(ArgType type) {
if (type.isPrimitive()) {
return type.getPrimitiveType().getShortName().toLowerCase();
}
if (type.isArray()) {
return makeNameForType(type.getArrayRootElement()) + "Arr";
}
return makeNameForObject(type);
}
private String makeNameForObject(ArgType type) {
if (type.isGenericType()) {
return StringUtils.escape(type.getObject().toLowerCase());
}
if (type.isObject()) {
String alias = getAliasForObject(type.getObject());
if (alias != null) {
return alias;
}
return makeNameForCheckedClass(ClassInfo.fromType(mth.root(), type));
}
return StringUtils.escape(type.toString());
}
private String makeNameForCheckedClass(ClassInfo classInfo) {
String shortName = classInfo.getAliasShortName();
String vName = fromName(shortName);
if (vName != null) {
return vName;
}
String lower = StringUtils.escape(shortName.toLowerCase());
if (shortName.equals(lower)) {
return lower + "Var";
}
return lower;
}
private String makeNameForClass(ClassInfo classInfo) {
String alias = getAliasForObject(classInfo.getFullName());
if (alias != null) {
return alias;
}
return makeNameForCheckedClass(classInfo);
}
private static String fromName(String name) {
if (name == null || name.isEmpty()) {
return null;
}
if (name.toUpperCase().equals(name)) {
// all characters are upper case
return name.toLowerCase();
}
String v1 = Character.toLowerCase(name.charAt(0)) + name.substring(1);
if (!v1.equals(name)) {
return v1;
}
if (name.length() < 3) {
return name + "Var";
}
return null;
}
private static String getAliasForObject(String name) {
return OBJ_ALIAS.get(name);
}
private String makeNameFromInsn(InsnNode insn) {
switch (insn.getType()) {
case INVOKE:
InvokeNode inv = (InvokeNode) insn;
return makeNameFromInvoke(inv.getCallMth());
case CONSTRUCTOR:
ConstructorInsn co = (ConstructorInsn) insn;
MethodNode callMth = mth.root().getMethodUtils().resolveMethod(co);
if (callMth != null && callMth.contains(AFlag.ANONYMOUS_CONSTRUCTOR)) {
// don't use name of anonymous class
return null;
}
return makeNameForClass(co.getClassType());
case ARRAY_LENGTH:
return "length";
case ARITH:
case TERNARY:
case CAST:
for (InsnArg arg : insn.getArguments()) {
if (arg.isInsnWrap()) {
InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn();
String wName = makeNameFromInsn(wrapInsn);
if (wName != null) {
return wName;
}
}
}
break;
default:
break;
}
return null;
}
private String makeNameFromInvoke(MethodInfo callMth) {
String name = callMth.getAlias();
ClassInfo declClass = callMth.getDeclClass();
if ("getInstance".equals(name)) {
// e.g. Cipher.getInstance
return makeNameForClass(declClass);
}
if (name.startsWith("get") || name.startsWith("set")) {
return fromName(name.substring(3));
}
if ("iterator".equals(name)) {
return "it";
}
if ("toString".equals(name)) {
return makeNameForClass(declClass);
}
if ("forName".equals(name) && declClass.getType().equals(ArgType.CLASS)) {
return OBJ_ALIAS.get(Consts.CLASS_CLASS);
}
if (name.startsWith("to")) {
return fromName(name.substring(2));
}
return name;
}
}
@@ -9,7 +9,6 @@ import org.jetbrains.annotations.Nullable;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import jadx.api.ICodeInfo;
import jadx.api.ICodeWriter;
@@ -32,13 +31,13 @@ import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.GsonUtils;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException;
public class JsonCodeGen {
private static final Gson GSON = new GsonBuilder()
.setPrettyPrinting()
private static final Gson GSON = GsonUtils.defaultGsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_DASHES)
.disableHtmlEscaping()
.create();
@@ -11,7 +11,6 @@ import org.slf4j.LoggerFactory;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import jadx.api.JadxArgs;
import jadx.core.codegen.json.mapping.JsonClsMapping;
@@ -24,14 +23,14 @@ import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.GsonUtils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
public class JsonMappingGen {
private static final Logger LOG = LoggerFactory.getLogger(JsonMappingGen.class);
private static final Gson GSON = new GsonBuilder()
.setPrettyPrinting()
private static final Gson GSON = GsonUtils.defaultGsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_DASHES)
.disableHtmlEscaping()
.create();
@@ -0,0 +1,129 @@
package jadx.core.deobf;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import jadx.core.utils.FileSignature;
import jadx.core.utils.StringUtils;
public class FileTypeDetector {
private static final Pattern DOCTYPE_PATTERN = Pattern.compile("\\s*<!doctype *(\\w+)[ >]", Pattern.CASE_INSENSITIVE);
private static final List<FileSignature> FILE_SIGNATURES = new ArrayList<>();
static {
register("png", "89 50 4E 47");
register("jpg", "FF D8 FF");
register("gif", "47 49 46 38");
register("webp", "52 49 46 46 ?? ?? ?? ?? 57 45 42 50 56 50 38");
register("bmp", "42 4D");
register("bmp", "42 41");
register("bmp", "43 49");
register("bmp", "43 50");
register("bmp", "49 43");
register("bmp", "50 54");
register("mp4", "00 00 00 ?? 66 74 79 70 69 73 6F 36");
register("mp4", "00 00 00 ?? 66 74 79 70 6D 70 34 32");
register("m4a", "00 00 00 ?? 66 74 79 70 4D 34 41 20");
register("mp3", "49 44 33");
register("ogg", "4F 67 67 53");
register("wav", "52 49 46 46 ?? ?? ?? ?? 57 41 56 45");
register("ttf", "00 01 00 00");
register("ttc", "74 74 63 66");
register("otf", "4F 54 54 4F");
register("xml", "03 00 08 00");
}
public static void register(String fileType, String signature) {
FILE_SIGNATURES.add(new FileSignature(fileType, signature));
}
private static String detectByHeaders(byte[] data) {
for (FileSignature sig : FILE_SIGNATURES) {
if (FileSignature.matches(sig, data)) {
if (sig.getFileType().equals("png") && isNinePatch(data)) {
return ".9.png";
}
return "." + sig.getFileType();
}
}
return null;
}
public static String detectFileExtension(byte[] data) {
// detect ext by headers
String extByHeaders = detectByHeaders(data);
if (!StringUtils.isEmpty(extByHeaders)) {
return extByHeaders;
}
// detect ext by readable text
String text = new String(data, StandardCharsets.UTF_8);
if (text.startsWith("-----BEGIN CERTIFICATE-----")) {
return ".cer";
}
if (text.startsWith("-----BEGIN PRIVATE KEY-----")) {
return ".key";
}
if (text.contains("<html>")) {
return ".html";
}
Matcher m = DOCTYPE_PATTERN.matcher(text);
if (m.lookingAt()) {
return "." + m.group(1).toLowerCase();
}
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new java.io.ByteArrayInputStream(data));
String rootTag = doc.getDocumentElement().getNodeName();
if ("svg".equalsIgnoreCase(rootTag)) {
return ".svg";
}
if ("plist".equalsIgnoreCase(rootTag)) {
return ".plist";
}
if ("kml".equalsIgnoreCase(rootTag)) {
return ".kml";
}
return ".xml";
} catch (Exception ignored) {
}
return null;
}
private static int readInt(byte[] data, int offset) {
return (data[offset] & 0xFF) << 24
| (data[offset + 1] & 0xFF) << 16
| (data[offset + 2] & 0xFF) << 8
| (data[offset + 3] & 0xFF);
}
private static boolean isNinePatch(byte[] data) {
int offset = 8;
while (offset + 8 < data.length) {
int chunkLength = readInt(data, offset);
int chunkType = readInt(data, offset + 4);
if (chunkType == 0x6e705463) { // 'npTc'
return true;
}
offset += 8 + chunkLength + 4; // chunk + data + CRC
}
return false;
}
}
@@ -24,6 +24,7 @@ import jadx.core.dex.attributes.nodes.MethodBridgeAttr;
import jadx.core.dex.attributes.nodes.MethodInlineAttr;
import jadx.core.dex.attributes.nodes.MethodOverrideAttr;
import jadx.core.dex.attributes.nodes.MethodReplaceAttr;
import jadx.core.dex.attributes.nodes.MethodThrowsAttr;
import jadx.core.dex.attributes.nodes.MethodTypeVarsAttr;
import jadx.core.dex.attributes.nodes.PhiListAttr;
import jadx.core.dex.attributes.nodes.RegDebugInfoAttr;
@@ -76,6 +77,7 @@ public final class AType<T extends IJadxAttribute> implements IJadxAttrType<T> {
public static final AType<MethodTypeVarsAttr> METHOD_TYPE_VARS = new AType<>();
public static final AType<AttrList<TryCatchBlockAttr>> TRY_BLOCKS_LIST = new AType<>();
public static final AType<CodeFeaturesAttr> METHOD_CODE_FEATURES = new AType<>();
public static final AType<MethodThrowsAttr> METHOD_THROWS = new AType<>();
// region
public static final AType<DeclareVariablesAttr> DECLARE_VARIABLES = new AType<>();
@@ -12,7 +12,7 @@ import jadx.core.utils.Utils;
public abstract class AttrNode implements IAttributeNode {
private static final AttributeStorage EMPTY_ATTR_STORAGE = new EmptyAttrStorage();
private static final AttributeStorage EMPTY_ATTR_STORAGE = EmptyAttrStorage.INSTANCE;
private AttributeStorage storage = EMPTY_ATTR_STORAGE;
@@ -35,6 +35,9 @@ public abstract class AttrNode implements IAttributeNode {
@Override
public void addAttrs(List<IJadxAttribute> list) {
if (list.isEmpty()) {
return;
}
initStorage().add(list);
}
@@ -18,12 +18,19 @@ import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException;
/**
* Storage for different attribute types:
* 1. flags - boolean attribute (set or not)
* 2. attribute - class instance associated with attribute type.
* Storage for different attribute types:<br>
* 1. Flags - boolean attribute (set or not)<br>
* 2. Attributes - class instance ({@link IJadxAttribute}) associated with an attribute type
* ({@link IJadxAttrType})<br>
*/
public class AttributeStorage {
public static AttributeStorage fromList(List<IJadxAttribute> list) {
AttributeStorage storage = new AttributeStorage();
storage.add(list);
return storage;
}
static {
int flagsCount = AFlag.values().length;
if (flagsCount >= 64) {
@@ -31,17 +38,14 @@ public class AttributeStorage {
}
}
private static final Map<IJadxAttrType<?>, IJadxAttribute> EMPTY_ATTRIBUTES = Collections.emptyMap();
private final Set<AFlag> flags;
private Map<IJadxAttrType<?>, IJadxAttribute> attributes;
public AttributeStorage() {
flags = EnumSet.noneOf(AFlag.class);
attributes = Collections.emptyMap();
}
public AttributeStorage(List<IJadxAttribute> attributesList) {
this();
add(attributesList);
attributes = EMPTY_ATTRIBUTES;
}
public void add(AFlag flag) {
@@ -125,11 +129,14 @@ public class AttributeStorage {
}
private void writeAttributes(Consumer<Map<IJadxAttrType<?>, IJadxAttribute>> mapConsumer) {
if (attributes.isEmpty()) {
attributes = new IdentityHashMap<>(5);
}
synchronized (this) {
if (attributes == EMPTY_ATTRIBUTES) {
attributes = new IdentityHashMap<>(2); // only 1 or 2 attributes added in most cases
}
mapConsumer.accept(attributes);
if (attributes.isEmpty()) {
attributes = EMPTY_ATTRIBUTES;
}
}
}
@@ -137,9 +144,7 @@ public class AttributeStorage {
if (attributes.isEmpty()) {
return;
}
synchronized (this) {
attributes.entrySet().removeIf(entry -> !entry.getValue().keepLoaded());
}
writeAttributes(map -> map.entrySet().removeIf(entry -> !entry.getValue().keepLoaded()));
}
public List<String> getAttributeStrings() {
@@ -9,6 +9,12 @@ import jadx.api.plugins.input.data.attributes.IJadxAttribute;
public final class EmptyAttrStorage extends AttributeStorage {
public static final AttributeStorage INSTANCE = new EmptyAttrStorage();
private EmptyAttrStorage() {
// singleton
}
@Override
public boolean contains(AFlag flag) {
return false;
@@ -0,0 +1,40 @@
package jadx.core.dex.attributes.nodes;
import java.util.Set;
import jadx.api.plugins.input.data.attributes.IJadxAttrType;
import jadx.api.plugins.input.data.attributes.PinnedAttribute;
import jadx.core.dex.attributes.AType;
public class MethodThrowsAttr extends PinnedAttribute {
private final Set<String> list;
private boolean visited;
public MethodThrowsAttr(Set<String> list) {
this.list = list;
}
public boolean isVisited() {
return visited;
}
public void setVisited(boolean visited) {
this.visited = visited;
}
public Set<String> getList() {
return list;
}
@Override
public IJadxAttrType<MethodThrowsAttr> getAttrType() {
return AType.METHOD_THROWS;
}
@Override
public String toString() {
return "THROWS:" + list;
}
}
@@ -34,7 +34,9 @@ public abstract class ArgType {
public static final ArgType STRING = objectNoCache(Consts.CLASS_STRING);
public static final ArgType ENUM = objectNoCache(Consts.CLASS_ENUM);
public static final ArgType THROWABLE = objectNoCache(Consts.CLASS_THROWABLE);
public static final ArgType ERROR = objectNoCache(Consts.CLASS_ERROR);
public static final ArgType EXCEPTION = objectNoCache(Consts.CLASS_EXCEPTION);
public static final ArgType RUNTIME_EXCEPTION = objectNoCache(Consts.CLASS_RUNTIME_EXCEPTION);
public static final ArgType OBJECT_ARRAY = array(OBJECT);
public static final ArgType WILDCARD = wildcard();
@@ -2,6 +2,7 @@ package jadx.core.dex.instructions.args;
import org.jetbrains.annotations.NotNull;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.instructions.ConstStringNode;
import jadx.core.dex.instructions.InsnType;
import jadx.core.dex.nodes.InsnNode;
@@ -24,6 +25,12 @@ public final class InsnWrapArg extends InsnArg {
return wrappedInsn;
}
public InsnNode unWrapWithCopy() {
InsnNode copy = wrappedInsn.copyWithoutResult();
copy.remove(AFlag.WRAPPED);
return copy;
}
@Override
public void setParentInsn(InsnNode parentInsn) {
if (parentInsn == wrappedInsn) {
@@ -59,13 +59,11 @@ public class SSAVar implements Comparable<SSAVar> {
return version;
}
@NotNull
public RegisterArg getAssign() {
public @NotNull RegisterArg getAssign() {
return assign;
}
@Nullable
public InsnNode getAssignInsn() {
public @Nullable InsnNode getAssignInsn() {
return assign.getParentInsn();
}
@@ -59,6 +59,14 @@ public final class TernaryInsn extends InsnNode {
list.addAll(condition.getRegisterArgs());
}
@Override
public boolean replaceArg(InsnArg from, InsnArg to) {
if (super.replaceArg(from, to)) {
return true;
}
return condition.replaceArg(from, to);
}
public void visitInsns(Consumer<InsnNode> visitor) {
super.visitInsns(visitor);
condition.visitInsns(visitor);
@@ -25,10 +25,9 @@ public final class BlockNode extends AttrNode implements IBlock, Comparable<Bloc
private final int cid;
/**
* ID linked to position in blocks list (easier to use BitSet)
* TODO: rename to avoid confusion
* Position in blocks list (easier to use BitSet)
*/
private int id;
private int pos;
/**
* Offset in methods bytecode
@@ -71,9 +70,9 @@ public final class BlockNode extends AttrNode implements IBlock, Comparable<Bloc
*/
private List<BlockNode> dominatesOn = new ArrayList<>(3);
public BlockNode(int cid, int id, int offset) {
public BlockNode(int cid, int pos, int offset) {
this.cid = cid;
this.id = id;
this.pos = pos;
this.startOffset = offset;
}
@@ -81,12 +80,20 @@ public final class BlockNode extends AttrNode implements IBlock, Comparable<Bloc
return cid;
}
void setId(int id) {
this.id = id;
void setPos(int id) {
this.pos = id;
}
/**
* Deprecated. Use {@link #getPos()}.
*/
@Deprecated
public int getId() {
return id;
return pos;
}
public int getPos() {
return pos;
}
public List<BlockNode> getPredecessors() {
@@ -105,6 +112,13 @@ public final class BlockNode extends AttrNode implements IBlock, Comparable<Bloc
cleanSuccessors = cleanSuccessors(this);
}
public static void updateBlockPositions(List<BlockNode> blocks) {
int count = blocks.size();
for (int i = 0; i < count; i++) {
blocks.get(i).setPos(i);
}
}
public void lock() {
try {
List<BlockNode> successorsList = successors;
@@ -161,7 +175,7 @@ public final class BlockNode extends AttrNode implements IBlock, Comparable<Bloc
* Check if 'block' dominated on this node
*/
public boolean isDominator(BlockNode block) {
return doms.get(block.getId());
return doms.get(block.getPos());
}
/**
@@ -236,7 +250,7 @@ public final class BlockNode extends AttrNode implements IBlock, Comparable<Bloc
@Override
public int hashCode() {
return startOffset;
return cid;
}
@Override
@@ -248,7 +262,7 @@ public final class BlockNode extends AttrNode implements IBlock, Comparable<Bloc
return false;
}
BlockNode other = (BlockNode) obj;
return cid == other.cid && startOffset == other.startOffset;
return cid == other.cid;
}
@Override
@@ -258,11 +272,11 @@ public final class BlockNode extends AttrNode implements IBlock, Comparable<Bloc
@Override
public String baseString() {
return Integer.toString(id);
return Integer.toString(cid);
}
@Override
public String toString() {
return "B:" + id + ':' + InsnUtils.formatOffset(startOffset);
return "B:" + cid + ':' + InsnUtils.formatOffset(startOffset);
}
}
@@ -24,6 +24,7 @@ import jadx.api.impl.SimpleCodeInfo;
import jadx.api.impl.SimpleCodeWriter;
import jadx.api.metadata.ICodeAnnotation;
import jadx.api.metadata.annotations.NodeDeclareRef;
import jadx.api.metadata.annotations.VarRef;
import jadx.api.plugins.input.data.IClassData;
import jadx.api.plugins.input.data.IFieldData;
import jadx.api.plugins.input.data.IMethodData;
@@ -175,9 +176,7 @@ public class ClassNode extends NotificationAttrNode
}
private static void processSpecialClasses(ClassNode cls) {
AccessInfo flags = cls.getAccessFlags();
if (flags.isSynthetic() && flags.isInterface() && flags.isAbstract()
&& cls.getName().equals("package-info")) {
if (cls.getName().equals("package-info") && cls.getFields().isEmpty() && cls.getMethods().isEmpty()) {
cls.add(AFlag.PACKAGE_INFO);
cls.add(AFlag.DONT_RENAME);
}
@@ -425,6 +424,20 @@ public class ClassNode extends NotificationAttrNode
declareRef.getNode().setDefPosition(pos);
}
}
// validate var refs
annotations.values().removeIf(v -> {
if (v.getAnnType() == ICodeAnnotation.AnnType.VAR_REF) {
VarRef varRef = (VarRef) v;
if (varRef.getRefPos() == 0) {
if (LOG.isDebugEnabled()) {
LOG.debug("Var reference '{}' incorrect (ref pos is zero) and was removed from metadata", varRef);
}
return true;
}
return false;
}
return false;
});
}
@Nullable
@@ -458,13 +471,15 @@ public class ClassNode extends NotificationAttrNode
if (state == NOT_LOADED) {
return;
}
methods.forEach(MethodNode::unload);
innerClasses.forEach(ClassNode::unload);
fields.forEach(FieldNode::unload);
unloadAttributes();
setState(NOT_LOADED);
this.loadStage = LoadStage.NONE;
this.smali = null;
synchronized (clsInfo) { // decompilation sync
methods.forEach(MethodNode::unload);
innerClasses.forEach(ClassNode::unload);
fields.forEach(FieldNode::unload);
unloadAttributes();
setState(NOT_LOADED);
this.loadStage = LoadStage.NONE;
this.smali = null;
}
}
private void buildCache() {
@@ -823,6 +838,9 @@ public class ClassNode extends NotificationAttrNode
return clsInfo.getAliasShortName();
}
/**
* Deprecated. Use {@link #getAlias()}
*/
@Deprecated
public String getShortName() {
return clsInfo.getAliasShortName();
@@ -26,6 +26,7 @@ import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.LoopInfo;
import jadx.core.dex.attributes.nodes.MethodOverrideAttr;
import jadx.core.dex.attributes.nodes.MethodThrowsAttr;
import jadx.core.dex.attributes.nodes.NotificationAttrNode;
import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.info.AccessInfo.AFType;
@@ -366,14 +367,11 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
public void setBasicBlocks(List<BlockNode> blocks) {
this.blocks = blocks;
updateBlockIds(blocks);
updateBlockPositions();
}
public void updateBlockIds(List<BlockNode> blocks) {
int count = blocks.size();
for (int i = 0; i < count; i++) {
blocks.get(i).setId(i);
}
public void updateBlockPositions() {
BlockNode.updateBlockPositions(blocks);
}
public int getNextBlockCId() {
@@ -480,11 +478,15 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
@Override
public List<ArgType> getThrows() {
ExceptionsAttr exceptionsAttr = get(JadxAttrType.EXCEPTIONS);
if (exceptionsAttr == null) {
return Collections.emptyList();
MethodThrowsAttr throwsAttr = get(AType.METHOD_THROWS);
if (throwsAttr != null) {
return Utils.collectionMap(throwsAttr.getList(), ArgType::object);
}
return Utils.collectionMap(exceptionsAttr.getList(), ArgType::object);
ExceptionsAttr exceptionsAttr = get(JadxAttrType.EXCEPTIONS);
if (exceptionsAttr != null) {
return Utils.collectionMap(exceptionsAttr.getList(), ArgType::object);
}
return Collections.emptyList();
}
/**
@@ -100,7 +100,20 @@ public class RootNode {
private @Nullable ManifestAttributes manifestAttributes;
public RootNode(JadxDecompiler decompiler) {
this(decompiler, decompiler.getArgs());
}
/**
* Deprecated. Prefer {@link #RootNode(JadxDecompiler)}
*/
@Deprecated
public RootNode(JadxArgs args) {
this(null, args);
}
private RootNode(@Nullable JadxDecompiler decompiler, JadxArgs args) {
this.decompiler = decompiler;
this.args = args;
this.preDecompilePasses = Jadx.getPreDecompilePassesList();
this.processClasses = new ProcessClass(Jadx.getPassesList(args));
@@ -131,6 +144,9 @@ public class RootNode {
Utils.checkThreadInterrupt();
});
}
}
public void finishClassLoad() {
if (classes.size() != clsMap.size()) {
// class name duplication detected
markDuplicatedClasses(classes);
@@ -255,6 +271,7 @@ public class RootNode {
if (args.isSkipResources()) {
return;
}
boolean useHeaders = args.isUseHeadersForDetectResourceExtensions();
long start = System.currentTimeMillis();
int renamedCount = 0;
ResourceStorage resStorage = parser.getResStorage();
@@ -269,7 +286,7 @@ public class RootNode {
for (ResourceFile resource : resources) {
ResourceEntry resEntry = entryNames.get(resource.getOriginalName());
if (resEntry != null) {
if (resource.setAlias(resEntry)) {
if (resource.setAlias(resEntry, useHeaders)) {
renamedCount++;
}
}
@@ -290,7 +307,7 @@ public class RootNode {
List<ClassNode> updated = new ArrayList<>();
for (ClassNode cls : inner) {
ClassInfo clsInfo = cls.getClassInfo();
ClassNode parent = resolveClass(clsInfo.getParentClass());
ClassNode parent = resolveParentClass(clsInfo);
if (parent == null) {
clsMap.remove(clsInfo);
clsInfo.notInner(this);
@@ -482,6 +499,33 @@ public class RootNode {
return rawClsMap.get(rawFullName);
}
/**
* Find and correct the parent of an inner class.
* <br>
* Sometimes inner ClassInfo generated wrong parent info.
* e.g. inner is `Cls$mth$1`, current parent = `Cls$mth`, real parent = `Cls`
*/
@Nullable
public ClassNode resolveParentClass(ClassInfo clsInfo) {
ClassInfo parentInfo = clsInfo.getParentClass();
ClassNode parentNode = resolveClass(parentInfo);
if (parentNode == null && parentInfo != null) {
String parClsName = parentInfo.getFullName();
// strip last part as method name
int sep = parClsName.lastIndexOf('.');
if (sep > 0 && sep != parClsName.length() - 1) {
String mthName = parClsName.substring(sep + 1);
String upperParClsName = parClsName.substring(0, sep);
ClassNode tmpParent = resolveClass(upperParClsName);
if (tmpParent != null && tmpParent.searchMethodByShortName(mthName) != null) {
parentNode = tmpParent;
clsInfo.convertToInner(parentNode);
}
}
}
return parentNode;
}
/**
* Searches for ClassNode by its full name (original or alias name)
* <br>
@@ -688,10 +732,6 @@ public class RootNode {
return args;
}
public void setDecompilerRef(JadxDecompiler jadxDecompiler) {
this.decompiler = jadxDecompiler;
}
public @Nullable JadxDecompiler getDecompiler() {
return decompiler;
}
@@ -226,6 +226,10 @@ public class TypeUtils {
for (int i = 0; i < genericParamsCount; i++) {
ArgType actualType = actualTypes.get(i);
ArgType typeVar = typeParameters.get(i);
if (typeVar.getExtendTypes() != null) {
// force short form (only type var name)
typeVar = ArgType.genericType(typeVar.getObject());
}
replaceMap.put(typeVar, actualType);
}
return replaceMap;
@@ -68,11 +68,11 @@ public abstract class ConditionRegion extends AbstractRegion implements IConditi
}
/**
* Prefer way for update condition info
* Preferred way to update condition info
*/
public void updateCondition(IfInfo info) {
this.condition = info.getCondition();
this.conditionBlocks = info.getMergedBlocks();
this.conditionBlocks = info.getMergedBlocks().toList();
}
public void updateCondition(IfCondition condition, List<BlockNode> conditionBlocks) {
@@ -16,6 +16,7 @@ import jadx.core.dex.instructions.ArithOp;
import jadx.core.dex.instructions.IfNode;
import jadx.core.dex.instructions.IfOp;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.InsnWrapArg;
import jadx.core.dex.instructions.args.LiteralArg;
import jadx.core.dex.instructions.args.RegisterArg;
@@ -264,6 +265,18 @@ public final class IfCondition extends AttrNode {
return list;
}
public boolean replaceArg(InsnArg from, InsnArg to) {
if (mode == Mode.COMPARE) {
return compare.getInsn().replaceArg(from, to);
}
for (IfCondition arg : args) {
if (arg.replaceArg(from, to)) {
return true;
}
}
return false;
}
public void visitInsns(Consumer<InsnNode> visitor) {
if (mode == Mode.COMPARE) {
compare.getInsn().visitInsns(visitor);
@@ -8,11 +8,12 @@ import java.util.Set;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.blocks.BlockSet;
public final class IfInfo {
private final MethodNode mth;
private final IfCondition condition;
private final List<BlockNode> mergedBlocks;
private final BlockSet mergedBlocks;
private final BlockNode thenBlock;
private final BlockNode elseBlock;
private final Set<BlockNode> skipBlocks;
@@ -20,7 +21,7 @@ public final class IfInfo {
private BlockNode outBlock;
public IfInfo(MethodNode mth, IfCondition condition, BlockNode thenBlock, BlockNode elseBlock) {
this(mth, condition, thenBlock, elseBlock, new ArrayList<>(), new HashSet<>(), new ArrayList<>());
this(mth, condition, thenBlock, elseBlock, BlockSet.empty(mth), new HashSet<>(), new ArrayList<>());
}
public IfInfo(IfInfo info, BlockNode thenBlock, BlockNode elseBlock) {
@@ -29,7 +30,7 @@ public final class IfInfo {
}
private IfInfo(MethodNode mth, IfCondition condition, BlockNode thenBlock, BlockNode elseBlock,
List<BlockNode> mergedBlocks, Set<BlockNode> skipBlocks, List<InsnNode> forceInlineInsns) {
BlockSet mergedBlocks, Set<BlockNode> skipBlocks, List<InsnNode> forceInlineInsns) {
this.mth = mth;
this.condition = condition;
this.thenBlock = thenBlock;
@@ -56,7 +57,11 @@ public final class IfInfo {
@Deprecated
public BlockNode getFirstIfBlock() {
return mergedBlocks.get(0);
return mergedBlocks.getFirst();
}
public BlockSet getMergedBlocks() {
return mergedBlocks;
}
public MethodNode getMth() {
@@ -67,10 +72,6 @@ public final class IfInfo {
return condition;
}
public List<BlockNode> getMergedBlocks() {
return mergedBlocks;
}
public Set<BlockNode> getSkipBlocks() {
return skipBlocks;
}
@@ -0,0 +1,272 @@
package jadx.core.dex.visitors;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jetbrains.annotations.Nullable;
import jadx.core.Consts;
import jadx.core.deobf.NameMapper;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.instructions.InvokeNode;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.instructions.args.CodeVar;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.InsnWrapArg;
import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.instructions.args.SSAVar;
import jadx.core.dex.instructions.mods.ConstructorInsn;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.visitors.regions.variables.ProcessVariables;
import jadx.core.utils.StringUtils;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxException;
@JadxVisitor(
name = "ApplyVariableNames",
desc = "Try to guess variable name from usage",
runAfter = {
ProcessVariables.class
}
)
public class ApplyVariableNames extends AbstractVisitor {
private static final Map<String, String> OBJ_ALIAS = Utils.newConstStringMap(
Consts.CLASS_STRING, "str",
Consts.CLASS_CLASS, "cls",
Consts.CLASS_THROWABLE, "th",
Consts.CLASS_OBJECT, "obj",
"java.util.Iterator", "it",
"java.util.HashMap", "map",
"java.lang.Boolean", "bool",
"java.lang.Short", "sh",
"java.lang.Integer", "num",
"java.lang.Character", "ch",
"java.lang.Byte", "b",
"java.lang.Float", "f",
"java.lang.Long", "l",
"java.lang.Double", "d",
"java.lang.StringBuilder", "sb",
"java.lang.Exception", "exc");
private static final Set<String> GOOD_VAR_NAMES = Set.of(
"size", "length", "list", "map", "next");
private static final List<String> INVOKE_PREFIXES = List.of(
"get", "set", "to", "parse", "read", "format");
private RootNode root;
@Override
public void init(RootNode root) throws JadxException {
this.root = root;
}
@Override
public void visit(MethodNode mth) throws JadxException {
for (SSAVar ssaVar : mth.getSVars()) {
CodeVar codeVar = ssaVar.getCodeVar();
String newName = guessName(codeVar);
if (newName != null) {
codeVar.setName(newName);
}
}
}
private @Nullable String guessName(CodeVar var) {
if (var.isThis()) {
return RegisterArg.THIS_ARG_NAME;
}
if (!var.isDeclared()) {
// name is not used in code
return null;
}
if (NameMapper.isValidAndPrintable(var.getName())) {
// the current name is valid, keep it
return null;
}
List<SSAVar> ssaVars = var.getSsaVars();
if (Utils.notEmpty(ssaVars)) {
boolean mthArg = ssaVars.stream().anyMatch(ssaVar -> ssaVar.getAssign().contains(AFlag.METHOD_ARGUMENT));
if (mthArg) {
// for method args use defined type and ignore usage
return makeNameForType(var.getType());
}
for (SSAVar ssaVar : ssaVars) {
String name = makeNameForSSAVar(ssaVar);
if (name != null) {
return name;
}
}
}
return makeNameForType(var.getType());
}
private @Nullable String makeNameForSSAVar(SSAVar ssaVar) {
String ssaVarName = ssaVar.getName();
if (ssaVarName != null) {
return ssaVarName;
}
InsnNode assignInsn = ssaVar.getAssignInsn();
if (assignInsn != null) {
String name = makeNameFromInsn(ssaVar, assignInsn);
if (NameMapper.isValidAndPrintable(name)) {
return name;
}
}
return null;
}
private String makeNameFromInsn(SSAVar ssaVar, InsnNode insn) {
switch (insn.getType()) {
case INVOKE:
return makeNameFromInvoke(ssaVar, (InvokeNode) insn);
case CONSTRUCTOR:
ConstructorInsn co = (ConstructorInsn) insn;
MethodNode callMth = root.getMethodUtils().resolveMethod(co);
if (callMth != null && callMth.contains(AFlag.ANONYMOUS_CONSTRUCTOR)) {
// don't use name of anonymous class
return null;
}
return makeNameForClass(co.getClassType());
case ARRAY_LENGTH:
return "length";
case ARITH:
case TERNARY:
case CAST:
for (InsnArg arg : insn.getArguments()) {
if (arg.isInsnWrap()) {
InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn();
String wName = makeNameFromInsn(ssaVar, wrapInsn);
if (wName != null) {
return wName;
}
}
}
break;
default:
break;
}
return null;
}
private String makeNameForType(ArgType type) {
if (type.isPrimitive()) {
return type.getPrimitiveType().getShortName().toLowerCase();
}
if (type.isArray()) {
return makeNameForType(type.getArrayRootElement()) + "Arr";
}
return makeNameForObject(type);
}
private String makeNameForObject(ArgType type) {
if (type.isGenericType()) {
return StringUtils.escape(type.getObject().toLowerCase());
}
if (type.isObject()) {
String alias = getAliasForObject(type.getObject());
if (alias != null) {
return alias;
}
return makeNameForCheckedClass(ClassInfo.fromType(root, type));
}
return StringUtils.escape(type.toString());
}
private String makeNameForCheckedClass(ClassInfo classInfo) {
String shortName = classInfo.getAliasShortName();
String vName = fromName(shortName);
if (vName != null) {
return vName;
}
String lower = StringUtils.escape(shortName.toLowerCase());
if (shortName.equals(lower)) {
return lower + "Var";
}
return lower;
}
private String makeNameForClass(ClassInfo classInfo) {
String alias = getAliasForObject(classInfo.getFullName());
if (alias != null) {
return alias;
}
return makeNameForCheckedClass(classInfo);
}
private static String fromName(String name) {
if (name == null || name.isEmpty()) {
return null;
}
if (name.toUpperCase().equals(name)) {
// all characters are upper case
return name.toLowerCase();
}
String v1 = Character.toLowerCase(name.charAt(0)) + name.substring(1);
if (!v1.equals(name)) {
return v1;
}
if (name.length() < 3) {
return name + "Var";
}
return null;
}
private static String getAliasForObject(String name) {
return OBJ_ALIAS.get(name);
}
private String makeNameFromInvoke(SSAVar ssaVar, InvokeNode inv) {
MethodInfo callMth = inv.getCallMth();
String name = callMth.getAlias();
ClassInfo declClass = callMth.getDeclClass();
if ("getInstance".equals(name)) {
// e.g. Cipher.getInstance
return makeNameForClass(declClass);
}
String shortName = cutPrefix(name);
if (shortName != null) {
return fromName(shortName);
}
if ("iterator".equals(name)) {
return "it";
}
if ("toString".equals(name)) {
return makeNameForClass(declClass);
}
if ("forName".equals(name) && declClass.getType().equals(ArgType.CLASS)) {
return OBJ_ALIAS.get(Consts.CLASS_CLASS);
}
// use method name as a variable name not the best idea in most cases
if (!GOOD_VAR_NAMES.contains(name)) {
String typeName = makeNameForType(ssaVar.getCodeVar().getType());
if (!typeName.equalsIgnoreCase(name)) {
return typeName + StringUtils.capitalizeFirstChar(name);
}
}
return name;
}
private @Nullable String cutPrefix(String name) {
for (String prefix : INVOKE_PREFIXES) {
if (name.startsWith(prefix)) {
return name.substring(prefix.length());
}
}
return null;
}
@Override
public String getName() {
return "ApplyVariableNames";
}
}
@@ -86,8 +86,11 @@ public class ClassModifier extends AbstractVisitor {
ClassInfo clsInfo = ClassInfo.fromType(cls.root(), fldType);
ClassNode fieldsCls = cls.root().resolveClass(clsInfo);
ClassInfo parentClass = cls.getClassInfo().getParentClass();
if (fieldsCls != null
&& (inline || Objects.equals(parentClass, fieldsCls.getClassInfo()))) {
if (fieldsCls == null) {
continue;
}
boolean isParentInst = Objects.equals(parentClass, fieldsCls.getClassInfo());
if (inline || isParentInst) {
int found = 0;
for (MethodNode mth : cls.getMethods()) {
if (removeFieldUsageFromConstructor(mth, field, fieldsCls)) {
@@ -95,7 +98,9 @@ public class ClassModifier extends AbstractVisitor {
}
}
if (found != 0) {
field.addAttr(new FieldReplaceAttr(fieldsCls.getClassInfo()));
if (isParentInst) {
field.addAttr(new FieldReplaceAttr(fieldsCls.getClassInfo()));
}
field.add(AFlag.DONT_GENERATE);
}
}
@@ -89,7 +89,7 @@ public class DotGraphVisitor extends AbstractVisitor {
public void process(MethodNode mth) {
dot.startLine("digraph \"CFG for");
dot.add(escape(mth.getParentClass() + "." + mth.getMethodInfo().getShortId()));
dot.add(escape(mth.getMethodInfo().getFullId()));
dot.add("\" {");
BlockNode enterBlock = mth.getEnterBlock();
@@ -204,7 +204,7 @@ public class DotGraphVisitor extends AbstractVisitor {
dot.add("color=red,");
}
dot.add("label=\"{");
dot.add(String.valueOf(block.getId())).add("\\:\\ ");
dot.add(String.valueOf(block.getCId())).add("\\:\\ ");
dot.add(InsnUtils.formatOffset(block.getStartOffset()));
if (!attrs.isEmpty()) {
dot.add('|').add(attrs);
@@ -237,10 +237,10 @@ public class DotGraphVisitor extends AbstractVisitor {
if (PRINT_DOMINATORS) {
for (BlockNode c : block.getDominatesOn()) {
conn.startLine(block.getId() + " -> " + c.getId() + "[color=green];");
conn.startLine(block.getCId() + " -> " + c.getCId() + "[color=green];");
}
for (BlockNode dom : BlockUtils.bitSetToBlocks(mth, block.getDomFrontier())) {
conn.startLine("f_" + block.getId() + " -> f_" + dom.getId() + "[color=blue];");
conn.startLine("f_" + block.getCId() + " -> f_" + dom.getCId() + "[color=blue];");
}
}
}
@@ -280,7 +280,7 @@ public class DotGraphVisitor extends AbstractVisitor {
private String makeName(IContainer c) {
String name;
if (c instanceof BlockNode) {
name = "Node_" + ((BlockNode) c).getId();
name = "Node_" + ((BlockNode) c).getCId();
} else if (c instanceof IBlock) {
name = "Node_" + c.getClass().getSimpleName() + '_' + c.hashCode();
} else {
@@ -25,6 +25,7 @@ import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.visitors.typeinference.TypeInferenceVisitor;
import jadx.core.utils.BlockUtils;
import jadx.core.utils.InsnRemover;
import jadx.core.utils.ListUtils;
import jadx.core.utils.exceptions.JadxException;
import jadx.core.utils.exceptions.JadxRuntimeException;
@@ -46,7 +47,7 @@ public class InlineMethods extends AbstractVisitor {
for (BlockNode block : mth.getBasicBlocks()) {
for (InsnNode insn : block.getInstructions()) {
if (insn.getType() == InsnType.INVOKE) {
processInvokeInsn(mth, block, ((InvokeNode) insn));
processInvokeInsn(mth, block, (InvokeNode) insn);
}
}
}
@@ -82,47 +83,65 @@ public class InlineMethods extends AbstractVisitor {
private void inlineMethod(MethodNode mth, MethodNode callMth, MethodInlineAttr mia, BlockNode block, InvokeNode insn) {
InsnNode inlCopy = mia.getInsn().copyWithoutResult();
RegisterArg resultArg = insn.getResult();
if (resultArg != null) {
inlCopy.setResult(resultArg.duplicate());
} else if (isAssignNeeded(mia.getInsn(), insn, callMth)) {
// add fake result to make correct java expression (see test TestGetterInlineNegative)
inlCopy.setResult(mth.makeSyntheticRegArg(callMth.getReturnType(), "unused"));
}
if (!callMth.getMethodInfo().getArgumentsTypes().isEmpty()) {
// remap args
InsnArg[] regs = new InsnArg[callMth.getRegsCount()];
int[] regNums = mia.getArgsRegNums();
for (int i = 0; i < regNums.length; i++) {
InsnArg arg = insn.getArg(i);
regs[regNums[i]] = arg;
if (replaceRegs(mth, callMth, mia, insn, inlCopy)) {
IMethodDetails methodDetailsAttr = inlCopy.get(AType.METHOD_DETAILS);
// replaceInsn replaces the attributes as well, make sure to preserve METHOD_DETAILS
if (BlockUtils.replaceInsn(mth, block, insn, inlCopy)) {
if (methodDetailsAttr != null) {
inlCopy.addAttr(methodDetailsAttr);
}
updateUsageInfo(mth, callMth, mia.getInsn());
return;
}
// replace args
List<RegisterArg> inlArgs = new ArrayList<>();
inlCopy.getRegisterArgs(inlArgs);
for (RegisterArg r : inlArgs) {
int regNum = r.getRegNum();
if (regNum >= regs.length) {
LOG.warn("Unknown register number {} in method call: {} from {}", r, callMth, mth);
} else {
}
mth.addWarnComment("Failed to inline method: " + callMth);
// undo changes to insn
InsnRemover.unbindInsn(mth, inlCopy);
insn.rebindArgs();
}
private boolean replaceRegs(MethodNode mth, MethodNode callMth, MethodInlineAttr mia, InvokeNode insn, InsnNode inlCopy) {
try {
if (!callMth.getMethodInfo().getArgumentsTypes().isEmpty()) {
// remap args
InsnArg[] regs = new InsnArg[callMth.getRegsCount()];
int[] regNums = mia.getArgsRegNums();
for (int i = 0; i < regNums.length; i++) {
InsnArg arg = insn.getArg(i);
regs[regNums[i]] = arg;
}
// replace args
List<RegisterArg> inlArgs = new ArrayList<>();
inlCopy.getRegisterArgs(inlArgs);
for (RegisterArg r : inlArgs) {
int regNum = r.getRegNum();
if (regNum >= regs.length) {
mth.addWarnComment("Unknown register number '" + r + "' in method call: " + callMth);
return false;
}
InsnArg repl = regs[regNum];
if (repl == null) {
LOG.warn("Not passed register {} in method call: {} from {}", r, callMth, mth);
} else {
inlCopy.replaceArg(r, repl);
mth.addWarnComment("Not passed register '" + r + "' in method call: " + callMth);
return false;
}
if (!inlCopy.replaceArg(r, repl.duplicate())) {
mth.addWarnComment("Failed to replace arg " + r + " for method inline: " + callMth);
return false;
}
}
}
RegisterArg resultArg = insn.getResult();
if (resultArg != null) {
inlCopy.setResult(resultArg.duplicate());
} else if (isAssignNeeded(mia.getInsn(), insn, callMth)) {
// add a fake result to make correct java expression (see test TestGetterInlineNegative)
inlCopy.setResult(mth.makeSyntheticRegArg(callMth.getReturnType(), "unused"));
}
return true;
} catch (Exception e) {
mth.addWarnComment("Method inline failed with exception", e);
return false;
}
IMethodDetails methodDetailsAttr = inlCopy.get(AType.METHOD_DETAILS);
if (!BlockUtils.replaceInsn(mth, block, insn, inlCopy)) {
mth.addWarnComment("Failed to inline method: " + callMth);
}
// replaceInsn replaces the attributes as well, make sure to preserve METHOD_DETAILS
if (methodDetailsAttr != null) {
inlCopy.addAttr(methodDetailsAttr);
}
updateUsageInfo(mth, callMth, mia.getInsn());
}
private boolean isAssignNeeded(InsnNode inlineInsn, InvokeNode parentInsn, MethodNode callMthNode) {
@@ -1,12 +1,10 @@
package jadx.core.dex.visitors;
import java.util.ArrayList;
import java.util.List;
import org.jetbrains.annotations.Nullable;
import jadx.api.plugins.input.data.AccessFlags;
import jadx.core.Consts;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.MethodInlineAttr;
@@ -17,6 +15,7 @@ import jadx.core.dex.instructions.InvokeNode;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.InsnWrapArg;
import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.instructions.args.SSAVar;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.visitors.fixaccessmodifiers.FixAccessModifiers;
@@ -80,17 +79,17 @@ public class MarkMethodsForInline extends AbstractVisitor {
if (!arg.isInsnWrap()) {
return null;
}
return addInlineAttr(mth, ((InsnWrapArg) arg).getWrapInsn());
return addInlineAttr(mth, ((InsnWrapArg) arg).unWrapWithCopy(), true);
}
// method invoke
return addInlineAttr(mth, insn);
return addInlineAttr(mth, insn, false);
}
if (insnsCount == 2 && insns.get(1).getType() == InsnType.RETURN) {
InsnNode firstInsn = insns.get(0);
InsnNode retInsn = insns.get(1);
if (retInsn.getArgsCount() == 0
|| isSyntheticAccessPattern(mth, firstInsn, retInsn)) {
return addInlineAttr(mth, firstInsn);
return addInlineAttr(mth, firstInsn, false);
}
}
// TODO: inline field arithmetics. Disabled tests: TestAnonymousClass3a and TestAnonymousClass5
@@ -130,18 +129,29 @@ public class MarkMethodsForInline extends AbstractVisitor {
}
}
private static MethodInlineAttr addInlineAttr(MethodNode mth, InsnNode insn) {
private static @Nullable MethodInlineAttr addInlineAttr(MethodNode mth, InsnNode insn, boolean isCopy) {
if (!fixVisibilityOfInlineCode(mth, insn)) {
if (isCopy) {
unbindSsaVars(insn);
}
return null;
}
InsnNode copy = insn.copyWithoutResult();
// unbind SSA variables from copy instruction
List<RegisterArg> regArgs = new ArrayList<>();
copy.getRegisterArgs(regArgs);
for (RegisterArg regArg : regArgs) {
copy.replaceArg(regArg, regArg.duplicate(regArg.getRegNum(), null));
}
return MethodInlineAttr.markForInline(mth, copy);
InsnNode inlInsn = isCopy ? insn : insn.copyWithoutResult();
unbindSsaVars(inlInsn);
return MethodInlineAttr.markForInline(mth, inlInsn);
}
private static void unbindSsaVars(InsnNode insn) {
insn.visitArgs(arg -> {
if (arg.isRegister()) {
RegisterArg reg = (RegisterArg) arg;
SSAVar ssaVar = reg.getSVar();
if (ssaVar != null) {
ssaVar.removeUse(reg);
reg.resetSSAVar();
}
}
});
}
private static boolean fixVisibilityOfInlineCode(MethodNode mth, InsnNode insn) {
@@ -150,7 +160,7 @@ public class MarkMethodsForInline extends AbstractVisitor {
if (insnType == InsnType.INVOKE) {
InvokeNode invoke = (InvokeNode) insn;
MethodNode callMthNode = mth.root().resolveMethod(invoke.getCallMth());
if (callMthNode != null) {
if (callMthNode != null && !callMthNode.root().getArgs().isRespectBytecodeAccModifiers()) {
FixAccessModifiers.changeVisibility(callMthNode, newVisFlag);
}
return true;
@@ -169,9 +179,7 @@ public class MarkMethodsForInline extends AbstractVisitor {
return true;
}
}
if (Consts.DEBUG) {
mth.addDebugComment("can't inline method, not implemented redirect type: " + insn);
}
mth.addDebugComment("Can't inline method, not implemented redirect type for insn: " + insn);
return false;
}
}

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