Compare commits

...

68 Commits

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-13 18:41:27 +00:00
dependabot[bot] 189e4181de build(deps): bump softprops/action-gh-release from 2 to 3 (#2848)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2 to 3.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v2...v3)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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

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

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

* Apply suggestions from code review

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

* Apply suggestions from code review

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

---------

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-23 20:32:02 +00:00
Skylot 462e582bb8 chore: update gradle and dependencies 2026-03-22 21:15:36 +00:00
Skylot 14a7b63707 chore: apply common code style 2026-03-22 20:48:27 +00:00
Skylot 4051a50146 fix: stop type inference on exception (prevent endless loop) 2026-03-22 19:41:26 +00:00
Skylot b3db337abd feat: use queue instead of recursion for type updates 2026-03-22 19:10:42 +00:00
eason 15ea9a56b9 fix: handle null bounds in WindowLocation to prevent NPE on dialog dispose (PR #2826)
* fix: handle null bounds in WindowLocation to prevent NPE on dialog dispose

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

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

Fixes #2571

* additional null checks and null annotations

---------

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

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

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

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

---------

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-04 20:07:59 +00:00
dependabot[bot] 22ee9a216c build(deps): bump actions/download-artifact from 7 to 8 (PR #2815)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v7...v8)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-04 20:07:43 +00:00
Hd 06b4467fdc fix(gui): update zh_CN translation (PR #2812) 2026-03-04 20:07:19 +00:00
Jan S. ff778ab372 fix(gui): use UI thread for adding logcat messages (#2811)
* fix(gui): use UI thread for adding logcat messages
other minor/logging improvements for debugger and adb connection

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

* call `resetAllDebuggingInfo` also from UI thread

---------

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

* revert non-working string searcher

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

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

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

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

* resolve issues with script code area after merge

---------

Co-authored-by: Jan S. <jpstotz@users.noreply.github.com>
Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
2026-02-13 19:02:36 +00:00
455 changed files with 17265 additions and 5990 deletions
+5 -5
View File
@@ -25,7 +25,7 @@ jobs:
echo "JADX_VERSION=$JADX_VERSION" >> $GITHUB_ENV echo "JADX_VERSION=$JADX_VERSION" >> $GITHUB_ENV
- name: Setup Gradle - name: Setup Gradle
uses: gradle/actions/setup-gradle@v5 uses: gradle/actions/setup-gradle@v6
- name: Build - name: Build
run: ./gradlew dist distWin run: ./gradlew dist distWin
@@ -33,7 +33,7 @@ jobs:
JADX_BUILD_JAVA_VERSION: 11 JADX_BUILD_JAVA_VERSION: 11
- name: Save bundle artifact - name: Save bundle artifact
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v7
with: with:
name: ${{ format('jadx-{0}', env.JADX_VERSION) }} name: ${{ format('jadx-{0}', env.JADX_VERSION) }}
# Waiting fix for https://github.com/actions/upload-artifact/issues/39 to upload zip file # Waiting fix for https://github.com/actions/upload-artifact/issues/39 to upload zip file
@@ -43,7 +43,7 @@ jobs:
retention-days: 14 retention-days: 14
- name: Save Windows bundle artifact - name: Save Windows bundle artifact
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v7
with: with:
name: ${{ format('jadx-gui-{0}-no-jre-win', env.JADX_VERSION) }} name: ${{ format('jadx-gui-{0}-no-jre-win', env.JADX_VERSION) }}
# Upload unpacked files for now # Upload unpacked files for now
@@ -75,13 +75,13 @@ jobs:
echo "JADX_VERSION=$JADX_VERSION" >> $GITHUB_ENV echo "JADX_VERSION=$JADX_VERSION" >> $GITHUB_ENV
- name: Setup Gradle - name: Setup Gradle
uses: gradle/actions/setup-gradle@v5 uses: gradle/actions/setup-gradle@v6
- name: Build - name: Build
run: ./gradlew dist -PbundleJRE=true run: ./gradlew dist -PbundleJRE=true
- name: Save Windows with JRE bundle artifact - name: Save Windows with JRE bundle artifact
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v7
with: with:
name: ${{ format('jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }} name: ${{ format('jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }}
# Upload unpacked files for now # Upload unpacked files for now
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
java-version: 25 java-version: 25
- name: Setup Gradle - name: Setup Gradle
uses: gradle/actions/setup-gradle@v5 uses: gradle/actions/setup-gradle@v6
- name: Build - name: Build
run: ./gradlew build dist distWin run: ./gradlew build dist distWin
+7 -7
View File
@@ -21,20 +21,20 @@ jobs:
release: 25 release: 25
- name: Set jadx version - name: Set jadx version
uses: actions/github-script@v8 uses: actions/github-script@v9
with: with:
script: | script: |
const jadxVersion = context.ref.split('/').pop().substring(1) const jadxVersion = context.ref.split('/').pop().substring(1)
core.exportVariable('JADX_VERSION', jadxVersion); core.exportVariable('JADX_VERSION', jadxVersion);
- name: Setup Gradle - name: Setup Gradle
uses: gradle/actions/setup-gradle@v5 uses: gradle/actions/setup-gradle@v6
- name: Build - name: Build
run: ./gradlew dist -PbundleJRE=true run: ./gradlew dist -PbundleJRE=true
- name: Save JRE bundle artifact - name: Save JRE bundle artifact
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v7
with: with:
name: ${{ format('jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }} name: ${{ format('jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }}
path: ${{ format('build/distWinWithJre/jadx-gui-{0}-with-jre-win.zip', env.JADX_VERSION) }} path: ${{ format('build/distWinWithJre/jadx-gui-{0}-with-jre-win.zip', env.JADX_VERSION) }}
@@ -54,14 +54,14 @@ jobs:
java-version: 25 java-version: 25
- name: Set jadx version and release name - name: Set jadx version and release name
uses: actions/github-script@v8 uses: actions/github-script@v9
with: with:
script: | script: |
const jadxVersion = context.ref.split('/').pop().substring(1) const jadxVersion = context.ref.split('/').pop().substring(1)
core.exportVariable('JADX_VERSION', jadxVersion); core.exportVariable('JADX_VERSION', jadxVersion);
- name: Setup Gradle - name: Setup Gradle
uses: gradle/actions/setup-gradle@v5 uses: gradle/actions/setup-gradle@v6
- name: Build - name: Build
run: ./gradlew dist distWin run: ./gradlew dist distWin
@@ -69,7 +69,7 @@ jobs:
JADX_BUILD_JAVA_VERSION: 11 JADX_BUILD_JAVA_VERSION: 11
- name: Download Windows JRE bundle - name: Download Windows JRE bundle
uses: actions/download-artifact@v7 uses: actions/download-artifact@v8
with: with:
name: ${{ format('jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }} name: ${{ format('jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }}
path: ${{ format('build/jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }} path: ${{ format('build/jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }}
@@ -84,7 +84,7 @@ jobs:
ls -l *.zip ls -l *.zip
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v3
with: with:
name: ${{ env.JADX_VERSION }} name: ${{ env.JADX_VERSION }}
draft: true draft: true
+4 -1
View File
@@ -37,7 +37,10 @@ jadx-output/
*.log *.log
*.cfg *.cfg
*.orig *.orig
quark.json *.json
*.dot
.env
cliff.toml cliff.toml
jadx-gui/src/main/resources/logback.xml jadx-gui/src/main/resources/logback.xml
+2 -1
View File
@@ -72,7 +72,7 @@ For Windows, you can download it from [oracle.com](https://www.oracle.com/java/t
You can use jadx in your java projects, check details on [wiki page](https://github.com/skylot/jadx/wiki/Use-jadx-as-a-library) You can use jadx in your java projects, check details on [wiki page](https://github.com/skylot/jadx/wiki/Use-jadx-as-a-library)
### Build from source ### Build from source
JDK 11 or higher must be installed: JDK 17 or higher must be installed:
``` ```
git clone https://github.com/skylot/jadx.git git clone https://github.com/skylot/jadx.git
cd jadx cd jadx
@@ -166,6 +166,7 @@ options:
--fs-case-sensitive - treat filesystem as case sensitive, false by default --fs-case-sensitive - treat filesystem as case sensitive, false by default
--cfg - save methods control flow graph to dot file --cfg - save methods control flow graph to dot file
--raw-cfg - save methods control flow graph (use raw instructions) --raw-cfg - save methods control flow graph (use raw instructions)
--call-graph - save app call graph in format: 'dot' or 'json', default: none
-f, --fallback - set '--decompilation-mode' to 'fallback' (deprecated) -f, --fallback - set '--decompilation-mode' to 'fallback' (deprecated)
--use-dx - use dx/d8 to convert java bytecode --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 --comments-level - set code comments level, values: error, warn, info, debug, user-only, none, default: info
+58 -5
View File
@@ -6,12 +6,14 @@ import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
import java.util.Locale import java.util.Locale
plugins { plugins {
id("com.github.ben-manes.versions") version "0.53.0" id("com.github.ben-manes.versions") version "0.54.0"
id("se.patrikerdes.use-latest-versions") version "0.2.19" id("se.patrikerdes.use-latest-versions") version "0.2.19"
id("com.diffplug.spotless") version "6.25.0" id("com.diffplug.spotless") version "8.7.0"
} }
val jadxVersion by extra { System.getenv("JADX_VERSION") ?: "dev" } val jadxEnv = loadEnv(file("$rootDir/.env"))
val jadxVersion by extra { jadxEnv["JADX_VERSION"] ?: "dev" }
println("jadx version: $jadxVersion") println("jadx version: $jadxVersion")
version = jadxVersion version = jadxVersion
@@ -19,7 +21,7 @@ val jadxBuildJavaVersion by extra { getBuildJavaVersion() }
fun getBuildJavaVersion(): Int? { fun getBuildJavaVersion(): Int? {
val envVarName = "JADX_BUILD_JAVA_VERSION" val envVarName = "JADX_BUILD_JAVA_VERSION"
val buildJavaVer = System.getenv(envVarName)?.toInt() ?: return null val buildJavaVer = jadxEnv[envVarName]?.toInt() ?: return null
if (buildJavaVer < 11) { if (buildJavaVer < 11) {
throw GradleException("'$envVarName' can't be set to lower than 11") throw GradleException("'$envVarName' can't be set to lower than 11")
} }
@@ -27,6 +29,24 @@ fun getBuildJavaVersion(): Int? {
return buildJavaVer return buildJavaVer
} }
// control ErrorProne checks level, can be: off, warn, error
val jadxBuildChecksMode: String by extra { getBuildChecksMode() }
fun getBuildChecksMode(): String {
val buildChecksMode = jadxEnv["JADX_BUILD_CHECKS_MODE"]?.lowercase() ?: "off"
val expectedValues = listOf("off", "warn", "error")
if (!expectedValues.contains(buildChecksMode)) {
throw GradleException("Unknown check mode: '$buildChecksMode', should be one of $expectedValues")
}
if (buildChecksMode != "off") {
val javaVersion = jadxBuildJavaVersion?.let { JavaVersion.toVersion(it) } ?: JavaVersion.current()
if (!javaVersion.isCompatibleWith(JavaVersion.VERSION_21)) {
throw GradleException("Error Prone requires Java 21")
}
}
return buildChecksMode
}
allprojects { allprojects {
apply(plugin = "java") apply(plugin = "java")
apply(plugin = "checkstyle") apply(plugin = "checkstyle")
@@ -82,6 +102,30 @@ fun isNonStable(version: String): Boolean {
return isStable.not() return isStable.not()
} }
fun loadEnv(file: File): Map<String, String> {
val envMap = HashMap<String, String>()
System
.getenv()
.filter { it.key.startsWith("JADX_") }
.forEach { envMap[it.key] = it.value }
if (file.exists()) {
file
.readLines()
.map { it.trim() }
.filter { it.isNotEmpty() && !it.startsWith("#") }
.forEach {
val (k, v) = it.split("=", limit = 2)
envMap[k.trim()] = v.trim()
}
}
println(
"Loaded env vars (${envMap.size}):\n${
envMap.toList().sortedBy { it.first }.joinToString(separator = "\n") { "${it.first}=${it.second}" }
}\n",
)
return envMap
}
val distWinConfiguration: Configuration by configurations.creating { val distWinConfiguration: Configuration by configurations.creating {
isCanBeConsumed = false isCanBeConsumed = false
} }
@@ -98,7 +142,9 @@ val copyArtifacts by tasks.registering(Copy::class) {
from(tasks.getByPath(":jadx-cli:installShadowDist")) { from(tasks.getByPath(":jadx-cli:installShadowDist")) {
exclude("**/*.jar") exclude("**/*.jar")
filter { line -> filter { line ->
jarCliPattern.matcher(line).replaceAll("jadx-$1-all.jar") jarCliPattern
.matcher(line)
.replaceAll("jadx-$1-all.jar")
.replace("-jar \"\\\"\$CLASSPATH\\\"\"", "-cp \"\\\"\$CLASSPATH\\\"\" jadx.cli.JadxCLI") .replace("-jar \"\\\"\$CLASSPATH\\\"\"", "-cp \"\\\"\$CLASSPATH\\\"\" jadx.cli.JadxCLI")
.replace("-jar \"%CLASSPATH%\"", "-cp \"%CLASSPATH%\" jadx.cli.JadxCLI") .replace("-jar \"%CLASSPATH%\"", "-cp \"%CLASSPATH%\" jadx.cli.JadxCLI")
} }
@@ -124,6 +170,13 @@ val pack by tasks.registering(Zip::class) {
from(copyArtifacts) from(copyArtifacts)
archiveFileName.set("jadx-$jadxVersion.zip") archiveFileName.set("jadx-$jadxVersion.zip")
destinationDirectory.set(layout.buildDirectory) destinationDirectory.set(layout.buildDirectory)
eachFile {
if (path == "bin/jadx" || path == "bin/jadx-gui") {
permissions {
unix("rwxr-xr-x")
}
}
}
} }
val distWin by tasks.registering(Zip::class) { val distWin by tasks.registering(Zip::class) {
+2
View File
@@ -6,6 +6,8 @@ dependencies {
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.10") implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.10")
implementation("org.openrewrite:plugin:6.19.1") implementation("org.openrewrite:plugin:6.19.1")
implementation("net.ltgt.errorprone:net.ltgt.errorprone.gradle.plugin:4.2.0")
implementation("net.ltgt.nullaway:net.ltgt.nullaway.gradle.plugin:2.3.0")
} }
repositories { repositories {
+38 -5
View File
@@ -1,29 +1,38 @@
import org.gradle.api.tasks.testing.logging.TestExceptionFormat import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import net.ltgt.gradle.errorprone.CheckSeverity
import net.ltgt.gradle.errorprone.errorprone
import net.ltgt.gradle.nullaway.nullaway
plugins { plugins {
java java
checkstyle checkstyle
id("jadx-rewrite") id("jadx-rewrite")
id("net.ltgt.errorprone")
id("net.ltgt.nullaway")
} }
val jadxVersion: String by rootProject.extra val jadxVersion: String by rootProject.extra
val jadxBuildJavaVersion: Int? by rootProject.extra val jadxBuildJavaVersion: Int? by rootProject.extra
val jadxBuildChecksMode: String by rootProject.extra
group = "io.github.skylot" group = "io.github.skylot"
version = jadxVersion version = jadxVersion
dependencies { dependencies {
implementation("org.slf4j:slf4j-api:2.0.17") implementation("org.slf4j:slf4j-api:2.0.18")
compileOnly("org.jetbrains:annotations:26.0.2") compileOnly("org.jetbrains:annotations:26.1.0")
testImplementation("ch.qos.logback:logback-classic:1.5.22") testImplementation("ch.qos.logback:logback-classic:1.5.34")
testImplementation("org.assertj:assertj-core:3.27.6") testImplementation("org.assertj:assertj-core:3.27.7")
testImplementation("org.junit.jupiter:junit-jupiter:5.13.3") testImplementation("org.junit.jupiter:junit-jupiter:5.13.3")
testRuntimeOnly("org.junit.platform:junit-platform-launcher") testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testCompileOnly("org.jetbrains:annotations:26.0.2") testCompileOnly("org.jetbrains:annotations:26.1.0")
errorprone("com.google.errorprone:error_prone_core:2.50.0")
errorprone("com.uber.nullaway:nullaway:0.13.7")
} }
repositories { repositories {
@@ -62,3 +71,27 @@ tasks {
} }
} }
} }
tasks.withType<JavaCompile>().configureEach {
val checkEnabled = jadxBuildChecksMode != "off"
if (checkEnabled) {
options.compilerArgs.add("-XDaddTypeAnnotationsToSymbol=true")
}
options.errorprone {
isEnabled = checkEnabled
allErrorsAsWarnings = jadxBuildChecksMode == "warn"
excludedPaths = ".*/test/.*"
nullaway {
if (jadxBuildChecksMode == "error") {
error()
}
annotatedPackages.add("jadx")
}
// TODO: fix and enable all checks
disable("MixedMutabilityReturnType")
disable("EqualsGetClass")
disable("OperatorPrecedence")
disable("UnusedVariable")
disable("ImmutableEnumChecker")
}
}
@@ -7,10 +7,10 @@ repositories {
} }
dependencies { dependencies {
rewrite("org.openrewrite.recipe:rewrite-testing-frameworks:3.24.0") rewrite("org.openrewrite.recipe:rewrite-testing-frameworks:3.38.0")
rewrite("org.openrewrite.recipe:rewrite-logging-frameworks:3.20.0") rewrite("org.openrewrite.recipe:rewrite-logging-frameworks:3.29.1")
rewrite("org.openrewrite.recipe:rewrite-migrate-java:3.24.0") rewrite("org.openrewrite.recipe:rewrite-migrate-java:3.37.0")
rewrite("org.openrewrite.recipe:rewrite-static-analysis:2.24.0") rewrite("org.openrewrite.recipe:rewrite-static-analysis:2.37.0")
} }
tasks { tasks {
Binary file not shown.
+2 -2
View File
@@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionSha256Sum=72f44c9f8ebcb1af43838f45ee5c4aa9c5444898b3468ab3f4af7b6076c5bc3f distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
Vendored
+1 -1
View File
@@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
+3 -2
View File
@@ -11,6 +11,7 @@ dependencies {
implementation(project(":jadx-core")) implementation(project(":jadx-core"))
implementation(project(":jadx-plugins-tools")) implementation(project(":jadx-plugins-tools"))
implementation(project(":jadx-commons:jadx-app-commons")) implementation(project(":jadx-commons:jadx-app-commons"))
implementation(project(":jadx-commons:jadx-analysis"))
runtimeOnly(project(":jadx-plugins:jadx-dex-input")) runtimeOnly(project(":jadx-plugins:jadx-dex-input"))
runtimeOnly(project(":jadx-plugins:jadx-java-input")) runtimeOnly(project(":jadx-plugins:jadx-java-input"))
@@ -25,8 +26,8 @@ dependencies {
runtimeOnly(project(":jadx-plugins:jadx-apks-input")) runtimeOnly(project(":jadx-plugins:jadx-apks-input"))
implementation("org.jcommander:jcommander:2.0") implementation("org.jcommander:jcommander:2.0")
implementation("ch.qos.logback:logback-classic:1.5.22") implementation("ch.qos.logback:logback-classic:1.5.34")
implementation("com.google.code.gson:gson:2.13.2") implementation("com.google.code.gson:gson:2.14.0")
} }
application { application {
@@ -1,11 +1,14 @@
package jadx.cli; package jadx.cli;
import java.nio.file.Path;
import java.util.function.Consumer; import java.util.function.Consumer;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import jadx.analysis.callgraph.JadxCallGraph;
import jadx.analysis.callgraph.api.ICallGraph;
import jadx.api.JadxArgs; import jadx.api.JadxArgs;
import jadx.api.JadxDecompiler; import jadx.api.JadxDecompiler;
import jadx.api.impl.AnnotatedCodeWriter; import jadx.api.impl.AnnotatedCodeWriter;
@@ -16,6 +19,7 @@ import jadx.cli.LogHelper.LogLevelEnum;
import jadx.cli.config.JadxConfigAdapter; import jadx.cli.config.JadxConfigAdapter;
import jadx.cli.plugins.JadxFilesGetter; import jadx.cli.plugins.JadxFilesGetter;
import jadx.core.utils.exceptions.JadxArgsValidateException; import jadx.core.utils.exceptions.JadxArgsValidateException;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.plugins.tools.JadxExternalPluginsLoader; import jadx.plugins.tools.JadxExternalPluginsLoader;
public class JadxCLI { public class JadxCLI {
@@ -73,6 +77,7 @@ public class JadxCLI {
if (checkForErrors(jadx)) { if (checkForErrors(jadx)) {
return 2; return 2;
} }
writeCallGraph(jadx, cliArgs);
if (!SingleClassMode.process(jadx, cliArgs)) { if (!SingleClassMode.process(jadx, cliArgs)) {
save(jadx); save(jadx);
} }
@@ -131,4 +136,29 @@ public class JadxCLI {
System.out.print(" \r"); System.out.print(" \r");
} }
} }
private static void writeCallGraph(JadxDecompiler jadx, JadxCLIArgs cliArgs) {
JadxCLIArgs.CallGraphSaveMode mode = cliArgs.callGraphSaveMode;
if (mode == null || mode == JadxCLIArgs.CallGraphSaveMode.NONE) {
return;
}
Path outPath = jadx.getArgs().getOutDir().toPath();
ICallGraph callGraph = JadxCallGraph.builder(jadx)
.resolvedOnly(true)
.build();
Path cgPath;
switch (mode) {
case JSON:
cgPath = outPath.resolve("callgraph.json");
callGraph.writeJson(cgPath);
break;
case DOT:
cgPath = outPath.resolve("callgraph.dot");
callGraph.writeDot(cgPath);
break;
default:
throw new JadxRuntimeException("Unexpected call graph save mode: " + mode);
}
LOG.info("Call graph saved: {}", cgPath.toAbsolutePath());
}
} }
@@ -283,6 +283,13 @@ public class JadxCLIArgs implements IJadxConfig {
@Parameter(names = { "--raw-cfg" }, description = "save methods control flow graph (use raw instructions)") @Parameter(names = { "--raw-cfg" }, description = "save methods control flow graph (use raw instructions)")
protected boolean rawCfgOutput = false; protected boolean rawCfgOutput = false;
@Parameter(
names = { "--call-graph" },
description = "save app call graph in format: 'dot' or 'json'",
converter = CallGraphSaveModeConverter.class
)
protected CallGraphSaveMode callGraphSaveMode = CallGraphSaveMode.NONE;
@Parameter(names = { "-f", "--fallback" }, description = "set '--decompilation-mode' to 'fallback' (deprecated)") @Parameter(names = { "-f", "--fallback" }, description = "set '--decompilation-mode' to 'fallback' (deprecated)")
protected boolean fallbackMode = false; protected boolean fallbackMode = false;
@@ -827,6 +834,14 @@ public class JadxCLIArgs implements IJadxConfig {
this.rawCfgOutput = rawCfgOutput; this.rawCfgOutput = rawCfgOutput;
} }
public CallGraphSaveMode getCallGraphSaveMode() {
return callGraphSaveMode;
}
public void setCallGraphSaveMode(CallGraphSaveMode callGraphSaveMode) {
this.callGraphSaveMode = callGraphSaveMode;
}
public boolean isReplaceConsts() { public boolean isReplaceConsts() {
return replaceConsts; return replaceConsts;
} }
@@ -1022,6 +1037,18 @@ public class JadxCLIArgs implements IJadxConfig {
} }
} }
public enum CallGraphSaveMode {
NONE,
DOT,
JSON,
}
public static class CallGraphSaveModeConverter extends BaseEnumConverter<CallGraphSaveMode> {
public CallGraphSaveModeConverter() {
super(CallGraphSaveMode::valueOf, CallGraphSaveMode::values);
}
}
public abstract static class BaseEnumConverter<E extends Enum<E>> implements IStringConverter<E> { public abstract static class BaseEnumConverter<E extends Enum<E>> implements IStringConverter<E> {
private final Function<String, E> parse; private final Function<String, E> parse;
private final Supplier<E[]> values; private final Supplier<E[]> values;
@@ -72,6 +72,9 @@ public class LogHelper {
setLevelForClass(JadxCLI.class, Level.INFO); setLevelForClass(JadxCLI.class, Level.INFO);
setLevelForClass(JadxDecompiler.class, Level.INFO); setLevelForClass(JadxDecompiler.class, Level.INFO);
setLevelForClass(SingleClassMode.class, Level.INFO); setLevelForClass(SingleClassMode.class, Level.INFO);
// show warnings and errors from input plugins
setLevelForPackage("jadx.plugins.input", Level.WARN);
} }
private static void applyLogLevel(@NotNull LogLevelEnum logLevel) { private static void applyLogLevel(@NotNull LogLevelEnum logLevel) {
+1
View File
@@ -9,6 +9,7 @@
<!-- jadx-gui --> <!-- jadx-gui -->
<logger name="com.pinterest.ktlint" level="INFO"/> <logger name="com.pinterest.ktlint" level="INFO"/>
<logger name="guru.nidi.graphviz" level="WARN"/>
<root level="INFO"> <root level="INFO">
<appender-ref ref="STDOUT"/> <appender-ref ref="STDOUT"/>
@@ -73,4 +73,12 @@ public class TestInput extends BaseCliIntegrationTest {
path -> path.getFileName().toString().equalsIgnoreCase("AndroidManifest.xml")); path -> path.getFileName().toString().equalsIgnoreCase("AndroidManifest.xml"));
assertThat(files).isNotEmpty(); assertThat(files).isNotEmpty();
} }
@Test
public void testNoRenameForDefPkg() throws Exception {
int result = execJadxCli(buildArgs(List.of("--rename-flags", "none"), "samples/defpkg.smali"));
assertThat(result).isEqualTo(0);
List<Path> files = collectJavaFilesInDir(outputDir);
assertThat(files).hasSize(1);
}
} }
@@ -0,0 +1,2 @@
.class public LA;
.super Ljava/lang/Object;
+29
View File
@@ -0,0 +1,29 @@
## jadx analysis
Various utilities for analyze and process code and related information.
### Call graph
Full app code usage/call graph.
Usage:
```java
JadxArgs args = new JadxArgs();
args.addInputFile(new File("input.apk"));
try (JadxDecompiler jadx = new JadxDecompiler(args)) {
jadx.load();
ICallGraph callGraph = JadxCallGraph.builder(jadx)
.includePackages("com.example") // filter nodes by package
.resolvedOnly(true) // add nodes only from app (exclude framework/lib calls)
.build();
for (ICallGraphEdge edge : callGraph.edges()) {
if (edge.isResolved()) {
System.out.printf("Edge from '%s' to '%s'%n", edge.from(), edge.to());
}
}
callGraph.writeDot(Path.of("test.dot")); // export to '.dot'
callGraph.writeJson(Path.of("test.json")); // export to JSON
}
```
@@ -0,0 +1,12 @@
plugins {
id("jadx-library")
}
dependencies {
implementation(project(":jadx-core"))
implementation("com.google.code.gson:gson:2.14.0")
testRuntimeOnly(project(":jadx-plugins:jadx-dex-input"))
testRuntimeOnly(project(":jadx-plugins:jadx-smali-input"))
}
@@ -0,0 +1,34 @@
package jadx.analysis.callgraph;
import java.nio.file.Path;
import java.util.List;
import jadx.analysis.callgraph.api.ICallGraph;
import jadx.analysis.callgraph.api.ICallGraphEdge;
import jadx.api.JadxArgs;
class CallGraph implements ICallGraph {
private final JadxArgs args;
private final List<ICallGraphEdge> edges;
public CallGraph(JadxArgs args, List<ICallGraphEdge> edges) {
this.args = args;
this.edges = edges;
}
@Override
public List<ICallGraphEdge> edges() {
return edges;
}
@Override
public void writeDot(Path path) {
new CallGraphExportDot(args, this).writeTo(path);
}
@Override
public void writeJson(Path path) {
new CallGraphExportJson(this).writeTo(path);
}
}
@@ -0,0 +1,6 @@
package jadx.analysis.callgraph;
import jadx.core.dex.attributes.AttrNode;
class CallGraphAttrNode extends AttrNode {
}
@@ -0,0 +1,92 @@
package jadx.analysis.callgraph;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import org.jetbrains.annotations.Nullable;
import jadx.analysis.callgraph.api.ICallGraph;
import jadx.analysis.callgraph.api.ICallGraphBuilder;
import jadx.analysis.callgraph.api.ICallGraphEdge;
import jadx.api.JadxDecompiler;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.MethodNode;
class CallGraphBuilder implements ICallGraphBuilder {
private final JadxDecompiler decompiler;
private boolean resolvedOnly = false;
private @Nullable String pkgFilter;
public CallGraphBuilder(JadxDecompiler decompiler) {
this.decompiler = decompiler;
}
@Override
public ICallGraphBuilder resolvedOnly(boolean resolved) {
this.resolvedOnly = resolved;
return this;
}
@Override
public ICallGraphBuilder includePackages(String pkgFilter) {
this.pkgFilter = pkgFilter.endsWith(".") ? pkgFilter : pkgFilter + '.';
return this;
}
@Override
public ICallGraph build() {
return new CallGraph(decompiler.getArgs(), collectEdges());
}
private List<ICallGraphEdge> collectEdges() {
AtomicInteger nodeId = new AtomicInteger();
Map<MethodInfo, CallGraphNode> nodes = new HashMap<>();
List<ICallGraphEdge> edges = new ArrayList<>();
for (ClassNode cls : decompiler.getRoot().getClasses(true)) {
if (ignorePkg(cls.getClassInfo())) {
continue;
}
for (MethodNode mth : cls.getMethods()) {
CallGraphNode thisNode = getCallGraphNode(mth, nodes, nodeId);
for (MethodNode use : mth.getUseIn()) {
if (ignorePkg(use.getDeclaringClass().getClassInfo())) {
continue;
}
CallGraphNode useInNode = getCallGraphNode(use, nodes, nodeId);
edges.add(new CallGraphEdge(useInNode, thisNode));
}
if (!resolvedOnly) {
for (MethodInfo used : mth.getUnresolvedUsed()) {
if (ignorePkg(used.getDeclClass())) {
continue;
}
CallGraphNode usedNode = getCallGraphNode(used, nodes, nodeId);
edges.add(new CallGraphEdge(thisNode, usedNode));
}
}
}
}
return edges;
}
private boolean ignorePkg(ClassInfo cls) {
if (pkgFilter == null) {
return false;
}
return !cls.getFullName().startsWith(pkgFilter);
}
private static CallGraphNode getCallGraphNode(MethodNode mth, Map<MethodInfo, CallGraphNode> nodes, AtomicInteger nodeId) {
return nodes.computeIfAbsent(mth.getMethodInfo(), i -> new CallGraphNode(nodeId.incrementAndGet(), mth));
}
private static CallGraphNode getCallGraphNode(MethodInfo mth, Map<MethodInfo, CallGraphNode> nodes, AtomicInteger nodeId) {
return nodes.computeIfAbsent(mth, i -> new CallGraphNode(nodeId.incrementAndGet(), mth));
}
}
@@ -0,0 +1,41 @@
package jadx.analysis.callgraph;
import jadx.analysis.callgraph.api.ICallGraphEdge;
import jadx.analysis.callgraph.api.ICallGraphNode;
import jadx.core.dex.attributes.IAttributeNode;
class CallGraphEdge implements ICallGraphEdge {
private final ICallGraphNode from;
private final ICallGraphNode to;
private final CallGraphAttrNode attrNode = new CallGraphAttrNode();
public CallGraphEdge(ICallGraphNode from, ICallGraphNode to) {
this.from = from;
this.to = to;
}
@Override
public ICallGraphNode from() {
return from;
}
@Override
public ICallGraphNode to() {
return to;
}
@Override
public boolean isResolved() {
return to.isResolved();
}
@Override
public IAttributeNode attributes() {
return attrNode;
}
@Override
public String toString() {
return "CallGraphEdge{from=" + from + ", to=" + to + '}';
}
}
@@ -0,0 +1,92 @@
package jadx.analysis.callgraph;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import jadx.analysis.callgraph.api.ICallGraph;
import jadx.analysis.callgraph.api.ICallGraphEdge;
import jadx.analysis.callgraph.api.ICallGraphNode;
import jadx.api.ICodeWriter;
import jadx.api.JadxArgs;
import jadx.api.impl.SimpleCodeWriter;
import jadx.core.utils.DotGraphUtils;
import jadx.core.utils.files.FileUtils;
import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
import static java.nio.file.StandardOpenOption.WRITE;
public class CallGraphExportDot {
private final JadxArgs args;
private final ICallGraph callGraph;
public CallGraphExportDot(JadxArgs args, ICallGraph callGraph) {
this.args = args;
this.callGraph = callGraph;
}
public void writeTo(Path path) {
try {
FileUtils.makeDirsForFile(path);
Files.writeString(path, writeToString(), StandardCharsets.UTF_8,
WRITE, TRUNCATE_EXISTING, CREATE);
} catch (IOException e) {
throw new RuntimeException("Failed to save JSON file: " + path, e);
}
}
public String writeToString() {
// collect nodes
Map<Integer, Node> nodeMap = new HashMap<>();
for (ICallGraphEdge edge : callGraph.edges()) {
addNode(edge.from(), nodeMap);
addNode(edge.to(), nodeMap);
}
List<Node> nodes = new ArrayList<>(nodeMap.values());
nodes.sort(Comparator.comparingInt(o -> o.id));
SimpleCodeWriter cw = new SimpleCodeWriter(args);
cw.add("digraph CallGraph {");
for (Node node : nodes) {
cw.startLine();
addNodeName(cw, node.id);
cw.add("[shape=record,label=\"{");
cw.add(DotGraphUtils.escape(node.method));
cw.add("}\"];");
}
for (ICallGraphEdge edge : callGraph.edges()) {
cw.startLine();
addNodeName(cw, edge.from().getId());
cw.add(" -> ");
addNodeName(cw, edge.to().getId());
cw.add(';');
}
cw.startLine('}');
return cw.getCodeStr();
}
private void addNodeName(ICodeWriter cw, int id) {
cw.add('N').add(Integer.toString(id));
}
private void addNode(ICallGraphNode cgNode, Map<Integer, Node> nodeMap) {
nodeMap.computeIfAbsent(cgNode.getId(), id -> {
Node node = new Node();
node.id = id;
node.method = cgNode.getMethodInfo().getRawFullId();
return node;
});
}
static final class Node {
int id;
String method;
}
}
@@ -0,0 +1,97 @@
package jadx.analysis.callgraph;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.Strictness;
import jadx.analysis.callgraph.api.ICallGraph;
import jadx.analysis.callgraph.api.ICallGraphEdge;
import jadx.analysis.callgraph.api.ICallGraphNode;
import jadx.core.utils.files.FileUtils;
import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
import static java.nio.file.StandardOpenOption.WRITE;
public class CallGraphExportJson {
private final ICallGraph callGraph;
private final Gson gson;
public CallGraphExportJson(ICallGraph callGraph) {
this.callGraph = callGraph;
this.gson = new GsonBuilder()
.disableJdkUnsafe()
.disableInnerClassSerialization()
.setStrictness(Strictness.STRICT)
// .setPrettyPrinting() // TODO: add option for pretty print?
.create();
}
public void writeTo(Path path) {
try {
FileUtils.makeDirsForFile(path);
Files.writeString(path, writeToString(), StandardCharsets.UTF_8,
WRITE, TRUNCATE_EXISTING, CREATE);
} catch (IOException e) {
throw new RuntimeException("Failed to save JSON file: " + path, e);
}
}
public String writeToString() {
List<Edge> edges = new ArrayList<>();
Map<Integer, Node> nodeMap = new HashMap<>();
for (ICallGraphEdge edge : callGraph.edges()) {
ICallGraphNode from = edge.from();
ICallGraphNode to = edge.to();
addNode(from, nodeMap);
addNode(to, nodeMap);
Edge jsonEdge = new Edge();
jsonEdge.from = from.getId();
jsonEdge.to = to.getId();
jsonEdge.resolved = edge.isResolved();
edges.add(jsonEdge);
}
List<Node> nodes = new ArrayList<>(nodeMap.values());
nodes.sort(Comparator.comparingInt(o -> o.id));
RootNode rootNode = new RootNode();
rootNode.nodes = nodes;
rootNode.edges = edges;
return gson.toJson(rootNode);
}
private void addNode(ICallGraphNode cgNode, Map<Integer, Node> nodeMap) {
nodeMap.computeIfAbsent(cgNode.getId(), id -> {
Node node = new Node();
node.id = id;
node.method = cgNode.getMethodInfo().getRawFullId();
return node;
});
}
static final class RootNode {
List<Node> nodes;
List<Edge> edges;
}
static final class Node {
int id;
String method;
}
static final class Edge {
int from;
int to;
boolean resolved;
}
}
@@ -0,0 +1,60 @@
package jadx.analysis.callgraph;
import org.jetbrains.annotations.Nullable;
import jadx.analysis.callgraph.api.ICallGraphNode;
import jadx.core.dex.attributes.IAttributeNode;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.nodes.MethodNode;
class CallGraphNode implements ICallGraphNode {
private final int id;
private final MethodInfo mthInfo;
private final @Nullable MethodNode mthNode;
private final CallGraphAttrNode attrNode;
public CallGraphNode(int id, MethodInfo mthInfo) {
this(id, mthInfo, null);
}
public CallGraphNode(int id, MethodNode mthNode) {
this(id, mthNode.getMethodInfo(), mthNode);
}
public CallGraphNode(int id, MethodInfo mthInfo, @Nullable MethodNode mthNode) {
this.id = id;
this.mthInfo = mthInfo;
this.mthNode = mthNode;
this.attrNode = new CallGraphAttrNode();
}
@Override
public int getId() {
return id;
}
@Override
public MethodInfo getMethodInfo() {
return mthInfo;
}
@Override
public @Nullable MethodNode getMethodNode() {
return mthNode;
}
@Override
public boolean isResolved() {
return mthNode != null;
}
@Override
public IAttributeNode attributes() {
return attrNode;
}
@Override
public String toString() {
return mthInfo.getFullId();
}
}
@@ -0,0 +1,11 @@
package jadx.analysis.callgraph;
import jadx.analysis.callgraph.api.ICallGraphBuilder;
import jadx.api.JadxDecompiler;
public class JadxCallGraph {
public static ICallGraphBuilder builder(JadxDecompiler decompiler) {
return new CallGraphBuilder(decompiler);
}
}
@@ -0,0 +1,13 @@
package jadx.analysis.callgraph.api;
import java.nio.file.Path;
import java.util.List;
public interface ICallGraph {
List<ICallGraphEdge> edges();
void writeDot(Path path);
void writeJson(Path path);
}
@@ -0,0 +1,10 @@
package jadx.analysis.callgraph.api;
public interface ICallGraphBuilder {
ICallGraphBuilder includePackages(String pkgFilter);
ICallGraphBuilder resolvedOnly(boolean resolved);
ICallGraph build();
}
@@ -0,0 +1,14 @@
package jadx.analysis.callgraph.api;
import jadx.core.dex.attributes.IAttributeNode;
public interface ICallGraphEdge {
ICallGraphNode from();
ICallGraphNode to();
boolean isResolved();
IAttributeNode attributes();
}
@@ -0,0 +1,21 @@
package jadx.analysis.callgraph.api;
import org.jetbrains.annotations.Nullable;
import jadx.core.dex.attributes.IAttributeNode;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.nodes.MethodNode;
public interface ICallGraphNode {
int getId();
MethodInfo getMethodInfo();
@Nullable
MethodNode getMethodNode();
boolean isResolved();
IAttributeNode attributes();
}
@@ -0,0 +1,86 @@
package jadx.analysis.callgraph.test;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import jadx.analysis.callgraph.CallGraphExportDot;
import jadx.analysis.callgraph.CallGraphExportJson;
import jadx.analysis.callgraph.JadxCallGraph;
import jadx.analysis.callgraph.api.ICallGraph;
import jadx.analysis.callgraph.api.ICallGraphEdge;
import jadx.api.JadxArgs;
import jadx.api.JadxDecompiler;
import static org.assertj.core.api.Assertions.assertThat;
class JadxCallGraphTest {
@TempDir
Path tempDir;
@SuppressWarnings("unused")
void usageExample() {
JadxArgs args = new JadxArgs();
args.addInputFile(new File("input.apk"));
try (JadxDecompiler jadx = new JadxDecompiler(args)) {
jadx.load();
ICallGraph callGraph = JadxCallGraph.builder(jadx)
.includePackages("com.example")
.resolvedOnly(false)
.build();
for (ICallGraphEdge edge : callGraph.edges()) {
if (edge.isResolved()) {
System.out.printf("Edge from '%s' to '%s'%n", edge.from(), edge.to());
}
}
callGraph.writeDot(Path.of("test.dot"));
callGraph.writeJson(Path.of("test.json"));
}
}
@Test
void simpleTest() {
JadxArgs args = new JadxArgs();
args.addInputFile(getSampleFile("simple.smali"));
try (JadxDecompiler jadx = new JadxDecompiler(args)) {
jadx.load();
ICallGraph callGraph = JadxCallGraph.builder(jadx)
.includePackages("test.pkg")
.resolvedOnly(false)
.build();
assertThat(callGraph.edges()).hasSize(1);
for (ICallGraphEdge edge : callGraph.edges()) {
System.out.println("Edge from " + edge.from() + " to " + edge.to());
}
String dotStr = new CallGraphExportDot(jadx.getArgs(), callGraph).writeToString();
System.out.println("dot: " + dotStr);
String jsonStr = new CallGraphExportJson(callGraph).writeToString();
System.out.println("json: " + jsonStr);
callGraph.writeDot(tempDir.resolve("test.dot"));
callGraph.writeJson(tempDir.resolve("test.json"));
}
}
private File getSampleFile(String sampleName) {
try {
URL resource = getClass().getResource("/samples/" + sampleName);
assertThat(resource).describedAs("Sample not found: %s", sampleName).isNotNull();
return new File(resource.toURI().toURL().getFile());
} catch (MalformedURLException | URISyntaxException e) {
throw new RuntimeException("Failed to load sample file: " + sampleName, e);
}
}
}
@@ -0,0 +1,19 @@
.class Ltest/pkg/HelloWorld;
.super Ljava/lang/Object;
.source "HelloWorld.java"
.method public static main([Ljava/lang/String;)V
.registers 2
const-string v0, "Hello, World"
invoke-static {p0, v0}, Ltest/pkg/HelloWorld;->hello(Ljava/lang/String;)V
return-void
.end method
.method public static hello(Ljava/lang/String;)V
.registers 2
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
invoke-virtual {v0, p0}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
return-void
.end method
@@ -1,8 +1,10 @@
package jadx.commons.app; package jadx.commons.app;
import org.jetbrains.annotations.Nullable;
public class JadxCommonEnv { public class JadxCommonEnv {
public static String get(String varName, String defValue) { public static @Nullable String get(String varName, @Nullable String defValue) {
String strValue = System.getenv(varName); String strValue = System.getenv(varName);
return isNullOrEmpty(strValue) ? defValue : strValue; return isNullOrEmpty(strValue) ? defValue : strValue;
} }
@@ -23,7 +25,7 @@ public class JadxCommonEnv {
return Integer.parseInt(strValue); return Integer.parseInt(strValue);
} }
private static boolean isNullOrEmpty(String value) { private static boolean isNullOrEmpty(@Nullable String value) {
return value == null || value.isEmpty(); return value == null || value.isEmpty();
} }
} }
@@ -3,7 +3,8 @@ package jadx.commons.app;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.function.Function; import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -30,40 +31,39 @@ public class JadxCommonFiles {
static { static {
DirsLoader loader = new DirsLoader(); DirsLoader loader = new DirsLoader();
loader.init();
CONFIG_DIR = loader.getConfigDir(); CONFIG_DIR = loader.getConfigDir();
CACHE_DIR = loader.getCacheDir(); CACHE_DIR = loader.getCacheDir();
} }
private static final class DirsLoader { private static final class DirsLoader {
private @Nullable ProjectDirectories dirs; private final Path configDir;
private Path configDir; private final Path cacheDir;
private Path cacheDir;
public void init() { DirsLoader() {
try { try {
configDir = loadEnvDir("JADX_CONFIG_DIR", pd -> pd.configDir); AtomicReference<@Nullable ProjectDirectories> pdRef = new AtomicReference<>();
cacheDir = loadEnvDir("JADX_CACHE_DIR", pd -> pd.cacheDir); configDir = loadEnvDir("JADX_CONFIG_DIR", () -> loadDirs(pdRef).configDir);
cacheDir = loadEnvDir("JADX_CACHE_DIR", () -> loadDirs(pdRef).cacheDir);
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("Failed to init common directories", e); throw new RuntimeException("Failed to init common directories", e);
} }
} }
private Path loadEnvDir(String envVar, Function<ProjectDirectories, String> dirFunc) throws IOException { private static Path loadEnvDir(String envVar, Supplier<String> dirFunc) throws IOException {
String envDir = JadxCommonEnv.get(envVar, null); String envDir = JadxCommonEnv.get(envVar, null);
String dirStr; String dirStr;
if (envDir != null) { if (envDir != null) {
dirStr = envDir; dirStr = envDir;
} else { } else {
dirStr = dirFunc.apply(loadDirs()); dirStr = dirFunc.get();
} }
Path path = Path.of(dirStr).toAbsolutePath(); Path path = Path.of(dirStr).toAbsolutePath();
Files.createDirectories(path); Files.createDirectories(path);
return path; return path;
} }
private synchronized ProjectDirectories loadDirs() { private static ProjectDirectories loadDirs(AtomicReference<@Nullable ProjectDirectories> pdRef) {
ProjectDirectories currentDirs = dirs; ProjectDirectories currentDirs = pdRef.get();
if (currentDirs != null) { if (currentDirs != null) {
return currentDirs; return currentDirs;
} }
@@ -76,7 +76,7 @@ public class JadxCommonFiles {
LOG.debug("Loaded system dirs ({}ms): config: {}, cache: {}", LOG.debug("Loaded system dirs ({}ms): config: {}, cache: {}",
System.currentTimeMillis() - start, loadedDirs.configDir, loadedDirs.cacheDir); System.currentTimeMillis() - start, loadedDirs.configDir, loadedDirs.cacheDir);
} }
dirs = loadedDirs; pdRef.set(loadedDirs);
return loadedDirs; return loadedDirs;
} }
@@ -84,21 +84,22 @@ public class JadxCommonFiles {
* Return JNI, Foreign or PowerShell implementation * Return JNI, Foreign or PowerShell implementation
*/ */
private static Windows getWinDirs() { private static Windows getWinDirs() {
Windows defSup = Windows.getDefaultSupplier().get(); Windows impl = Windows.getDefaultSupplier().get();
if (defSup instanceof WindowsPowerShell) { if (impl instanceof WindowsPowerShell) {
if (JadxSystemInfo.IS_AMD64) { if (JadxSystemInfo.IS_AMD64) {
// JNI library compiled for x86-64 // JNI library compiled only for x86-64
return new WindowsJni(); impl = new WindowsJni();
} }
} }
return defSup; LOG.debug("Using win dirs implementation: {}", impl.getClass().getSimpleName());
return impl;
} }
public Path getCacheDir() { Path getCacheDir() {
return cacheDir; return cacheDir;
} }
public Path getConfigDir() { Path getConfigDir() {
return configDir; return configDir;
} }
} }
@@ -2,6 +2,7 @@ package jadx.commons.app;
import java.util.Locale; import java.util.Locale;
@SuppressWarnings("unused")
public class JadxSystemInfo { public class JadxSystemInfo {
public static final String JAVA_VM = System.getProperty("java.vm.name", "?"); public static final String JAVA_VM = System.getProperty("java.vm.name", "?");
public static final String JAVA_VER = System.getProperty("java.version", "?"); public static final String JAVA_VER = System.getProperty("java.version", "?");
@@ -16,7 +17,7 @@ public class JadxSystemInfo {
public static final boolean IS_LINUX = !IS_WINDOWS && !IS_MAC; public static final boolean IS_LINUX = !IS_WINDOWS && !IS_MAC;
public static final boolean IS_UNIX = !IS_WINDOWS; public static final boolean IS_UNIX = !IS_WINDOWS;
private static final String OS_ARCH_LOWER = OS_NAME.toLowerCase(Locale.ENGLISH); private static final String OS_ARCH_LOWER = OS_ARCH.toLowerCase(Locale.ENGLISH);
public static final boolean IS_AMD64 = OS_ARCH_LOWER.equals("amd64"); public static final boolean IS_AMD64 = OS_ARCH_LOWER.equals("amd64");
public static final boolean IS_ARM64 = OS_ARCH_LOWER.equals("aarch64"); public static final boolean IS_ARM64 = OS_ARCH_LOWER.equals("aarch64");
@@ -1,6 +1,7 @@
package jadx.zip; package jadx.zip;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Set; import java.util.Set;
@@ -9,6 +10,7 @@ import java.util.function.Function;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import jadx.zip.fallback.FallbackException;
import jadx.zip.fallback.FallbackZipParser; import jadx.zip.fallback.FallbackZipParser;
import jadx.zip.parser.JadxZipParser; import jadx.zip.parser.JadxZipParser;
import jadx.zip.security.IJadxZipSecurity; import jadx.zip.security.IJadxZipSecurity;
@@ -39,13 +41,15 @@ public class ZipReader {
@SuppressWarnings("resource") @SuppressWarnings("resource")
public ZipContent open(File zipFile) throws IOException { public ZipContent open(File zipFile) throws IOException {
if (!zipFile.exists()) {
throw new FileNotFoundException(zipFile.getAbsolutePath());
}
try { try {
JadxZipParser jadxParser = new JadxZipParser(zipFile, options); JadxZipParser jadxParser = new JadxZipParser(zipFile, options);
IZipParser detectedParser = detectParser(zipFile, jadxParser); IZipParser detectedParser = detectParser(zipFile, jadxParser);
if (detectedParser != jadxParser) {
jadxParser.close();
}
return detectedParser.open(); return detectedParser.open();
} catch (FallbackException e) {
throw e;
} catch (Exception e) { } catch (Exception e) {
if (options.getFlags().contains(ZipReaderFlags.DONT_USE_FALLBACK)) { if (options.getFlags().contains(ZipReaderFlags.DONT_USE_FALLBACK)) {
throw new IOException("Failed to open zip: " + zipFile, e); throw new IOException("Failed to open zip: " + zipFile, e);
@@ -90,7 +94,7 @@ public class ZipReader {
return options; return options;
} }
private IZipParser detectParser(File zipFile, JadxZipParser jadxParser) { private IZipParser detectParser(File zipFile, JadxZipParser jadxParser) throws IOException {
if (zipFile.getName().endsWith(".apk") if (zipFile.getName().endsWith(".apk")
|| options.getFlags().contains(ZipReaderFlags.DONT_USE_FALLBACK)) { || options.getFlags().contains(ZipReaderFlags.DONT_USE_FALLBACK)) {
return jadxParser; return jadxParser;
@@ -105,7 +109,7 @@ public class ZipReader {
return jadxParser; return jadxParser;
} }
private FallbackZipParser buildFallbackParser(File zipFile) { private FallbackZipParser buildFallbackParser(File zipFile) throws IOException {
return new FallbackZipParser(zipFile, options); return new FallbackZipParser(zipFile, options);
} }
} }
@@ -0,0 +1,9 @@
package jadx.zip.fallback;
import java.io.IOException;
public class FallbackException extends IOException {
public FallbackException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -22,39 +22,45 @@ import jadx.zip.security.IJadxZipSecurity;
public class FallbackZipParser implements IZipParser { public class FallbackZipParser implements IZipParser {
private static final Logger LOG = LoggerFactory.getLogger(FallbackZipParser.class); private static final Logger LOG = LoggerFactory.getLogger(FallbackZipParser.class);
private final File file; private final File file;
private final ZipFile zipFile;
private final IJadxZipSecurity zipSecurity; private final IJadxZipSecurity zipSecurity;
private final boolean useLimitedDataStream; private final boolean useLimitedDataStream;
private ZipFile zipFile; public FallbackZipParser(File file, ZipReaderOptions options) throws FallbackException {
try {
public FallbackZipParser(File file, ZipReaderOptions options) { this.file = file;
this.file = file; this.zipFile = new ZipFile(file);
this.zipSecurity = options.getZipSecurity(); this.zipSecurity = options.getZipSecurity();
this.useLimitedDataStream = zipSecurity.useLimitedDataStream(); this.useLimitedDataStream = zipSecurity.useLimitedDataStream();
} catch (Exception e) {
throw new FallbackException("Error opening zip file: " + file.getAbsolutePath(), e);
}
} }
@Override @Override
public ZipContent open() throws IOException { public ZipContent open() throws IOException {
zipFile = new ZipFile(file); try {
int maxEntriesCount = zipSecurity.getMaxEntriesCount();
int maxEntriesCount = zipSecurity.getMaxEntriesCount(); if (maxEntriesCount == -1) {
if (maxEntriesCount == -1) { maxEntriesCount = Integer.MAX_VALUE;
maxEntriesCount = Integer.MAX_VALUE; }
} List<IZipEntry> list = new ArrayList<>();
Enumeration<? extends ZipEntry> entries = zipFile.entries();
List<IZipEntry> list = new ArrayList<>(); while (entries.hasMoreElements()) {
Enumeration<? extends ZipEntry> entries = zipFile.entries(); FallbackZipEntry zipEntry = new FallbackZipEntry(this, entries.nextElement());
while (entries.hasMoreElements()) { if (isValidEntry(zipEntry)) {
FallbackZipEntry zipEntry = new FallbackZipEntry(this, entries.nextElement()); list.add(zipEntry);
if (isValidEntry(zipEntry)) { if (list.size() > maxEntriesCount) {
list.add(zipEntry); throw new IllegalStateException("Max entries count limit exceeded: " + list.size());
if (list.size() > maxEntriesCount) { }
throw new IllegalStateException("Max entries count limit exceeded: " + list.size());
} }
} }
return new ZipContent(this, list);
} catch (Exception e) {
throw new FallbackException("Error opening zip file: " + file.getAbsolutePath(), e);
} }
return new ZipContent(this, list);
} }
private boolean isValidEntry(IZipEntry zipEntry) { private boolean isValidEntry(IZipEntry zipEntry) {
@@ -98,12 +104,8 @@ public class FallbackZipParser implements IZipParser {
@Override @Override
public void close() throws IOException { public void close() throws IOException {
try { if (zipFile != null) {
if (zipFile != null) { zipFile.close();
zipFile.close();
}
} finally {
zipFile = null;
} }
} }
} }
@@ -12,6 +12,7 @@ public class ByteBufferBackedInputStream extends InputStream {
this.buf = buf; this.buf = buf;
} }
@Override
public int read() throws IOException { public int read() throws IOException {
if (!buf.hasRemaining()) { if (!buf.hasRemaining()) {
return -1; return -1;
@@ -19,6 +20,7 @@ public class ByteBufferBackedInputStream extends InputStream {
return buf.get() & 0xFF; return buf.get() & 0xFF;
} }
@Override
@SuppressWarnings("NullableProblems") @SuppressWarnings("NullableProblems")
public int read(byte[] bytes, int off, int len) throws IOException { public int read(byte[] bytes, int off, int len) throws IOException {
if (!buf.hasRemaining()) { if (!buf.hasRemaining()) {
@@ -35,6 +35,7 @@ public final class JadxZipEntry implements IZipEntry {
return compressedSize <= uncompressedSize; return compressedSize <= uncompressedSize;
} }
@Override
public String getName() { public String getName() {
return fileName; return fileName;
} }
@@ -49,9 +49,9 @@ public final class JadxZipParser implements IZipParser {
private final boolean verify; private final boolean verify;
private final boolean useLimitedDataStream; private final boolean useLimitedDataStream;
private RandomAccessFile file; private @Nullable RandomAccessFile file;
private FileChannel fileChannel; private @Nullable FileChannel fileChannel;
private ByteBuffer byteBuffer; private @Nullable ByteBuffer byteBuffer;
private int endOfCDStart = -2; private int endOfCDStart = -2;
@@ -90,23 +90,25 @@ public final class JadxZipParser implements IZipParser {
} }
} }
@SuppressWarnings("RedundantIfStatement")
public boolean canOpen() { public boolean canOpen() {
try { try {
load(); load();
int eocdStart = searchEndOfCDStart(); int eocdStart = searchEndOfCDStart();
ByteBuffer buf = byteBuffer; ByteBuffer buf = getBuffer();
buf.position(eocdStart + 4); buf.position(eocdStart + 4);
int diskNum = readU2(buf); int diskNum = readU2(buf);
if (diskNum == 0xFFFF) { if (diskNum != 0xFFFF) { // Zip64 not supported
// Zip64 return true;
return false;
} }
return true;
} catch (Exception e) { } catch (Exception e) {
LOG.warn("Jadx parser can't open zip file: {}", zipFile, e); LOG.warn("Jadx parser can't open zip file: {}", zipFile, e);
return false;
} }
try {
close();
} catch (Exception e) {
LOG.warn("Failed to close jadx parser, zip file: {}", zipFile, e);
}
return false;
} }
private boolean isValidEntry(JadxZipEntry zipEntry) { private boolean isValidEntry(JadxZipEntry zipEntry) {
@@ -117,13 +119,21 @@ public final class JadxZipParser implements IZipParser {
return validEntry; return validEntry;
} }
private ByteBuffer getBuffer() {
ByteBuffer buf = byteBuffer;
if (buf == null) {
throw new RuntimeException("File not opened: " + zipFile);
}
return buf;
}
private void load() throws IOException { private void load() throws IOException {
if (byteBuffer != null) { if (byteBuffer != null) {
// already loaded // already loaded
return; return;
} }
file = new RandomAccessFile(zipFile, "r"); RandomAccessFile raFile = new RandomAccessFile(zipFile, "r");
long size = file.length(); long size = raFile.length();
if (size >= Integer.MAX_VALUE) { if (size >= Integer.MAX_VALUE) {
throw new IOException("Zip file is too big"); throw new IOException("Zip file is too big");
} }
@@ -131,16 +141,16 @@ public final class JadxZipParser implements IZipParser {
if (fileLen < 100 * 1024 * 1024) { if (fileLen < 100 * 1024 * 1024) {
// load files smaller than 100MB directly into memory // load files smaller than 100MB directly into memory
byte[] bytes = new byte[fileLen]; byte[] bytes = new byte[fileLen];
file.readFully(bytes); raFile.readFully(bytes);
byteBuffer = ByteBuffer.wrap(bytes).asReadOnlyBuffer(); byteBuffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
file.close(); raFile.close();
file = null;
} else { } else {
// for big files - use a memory mapped file // for big files - use a memory mapped file
fileChannel = file.getChannel(); file = raFile;
fileChannel = raFile.getChannel();
byteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size()); byteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
} }
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
} }
private List<IZipEntry> searchLocalFileHeaders(int maxEntriesCount) { private List<IZipEntry> searchLocalFileHeaders(int maxEntriesCount) {
@@ -165,7 +175,7 @@ public final class JadxZipParser implements IZipParser {
if (eocdStart < 0) { if (eocdStart < 0) {
throw new RuntimeException("End of central directory not found"); throw new RuntimeException("End of central directory not found");
} }
ByteBuffer buf = byteBuffer; ByteBuffer buf = getBuffer();
buf.position(eocdStart + 10); buf.position(eocdStart + 10);
int entriesCount = readU2(buf); int entriesCount = readU2(buf);
buf.position(eocdStart + 16); buf.position(eocdStart + 16);
@@ -186,7 +196,7 @@ public final class JadxZipParser implements IZipParser {
} }
private JadxZipEntry loadCDEntry() { private JadxZipEntry loadCDEntry() {
ByteBuffer buf = byteBuffer; ByteBuffer buf = getBuffer();
int start = buf.position(); int start = buf.position();
buf.position(start + 28); buf.position(start + 28);
int fileNameLen = readU2(buf); int fileNameLen = readU2(buf);
@@ -207,7 +217,7 @@ public final class JadxZipParser implements IZipParser {
} }
private JadxZipEntry fixEntryFromCD(JadxZipEntry entry, int start) { private JadxZipEntry fixEntryFromCD(JadxZipEntry entry, int start) {
ByteBuffer buf = byteBuffer; ByteBuffer buf = getBuffer();
buf.position(start + 10); buf.position(start + 10);
int comprMethod = readU2(buf); int comprMethod = readU2(buf);
buf.position(start + 20); buf.position(start + 20);
@@ -237,7 +247,7 @@ public final class JadxZipParser implements IZipParser {
} }
private JadxZipEntry loadFileEntry(int start) { private JadxZipEntry loadFileEntry(int start) {
ByteBuffer buf = byteBuffer; ByteBuffer buf = getBuffer();
buf.position(start + 8); buf.position(start + 8);
int comprMethod = readU2(buf); int comprMethod = readU2(buf);
buf.position(start + 18); buf.position(start + 18);
@@ -255,7 +265,7 @@ public final class JadxZipParser implements IZipParser {
if (endOfCDStart != -2) { if (endOfCDStart != -2) {
return endOfCDStart; return endOfCDStart;
} }
ByteBuffer buf = byteBuffer; ByteBuffer buf = getBuffer();
int pos = buf.limit() - 22; int pos = buf.limit() - 22;
int minPos = Math.max(0, pos - 0xffff); int minPos = Math.max(0, pos - 0xffff);
while (true) { while (true) {
@@ -273,7 +283,7 @@ public final class JadxZipParser implements IZipParser {
} }
private int searchEntryStart() { private int searchEntryStart() {
ByteBuffer buf = byteBuffer; ByteBuffer buf = getBuffer();
while (true) { while (true) {
int start = buf.position(); int start = buf.position();
if (start + 4 > buf.limit()) { if (start + 4 > buf.limit()) {
@@ -297,14 +307,14 @@ public final class JadxZipParser implements IZipParser {
InputStream stream; InputStream stream;
if (entry.getCompressMethod() == 8) { if (entry.getCompressMethod() == 8) {
try { try {
stream = ZipDeflate.decompressEntryToStream(byteBuffer, entry); stream = ZipDeflate.decompressEntryToStream(getBuffer(), entry);
} catch (Exception e) { } catch (Exception e) {
entryParseFailed(entry, e); entryParseFailed(entry, e);
return useFallbackParser(entry).getInputStream(); return useFallbackParser(entry).getInputStream();
} }
} else { } else {
// treat any other compression methods values as UNCOMPRESSED // treat any other compression methods values as UNCOMPRESSED
stream = bufferToStream(byteBuffer, entry.getDataStart(), (int) entry.getUncompressedSize()); stream = bufferToStream(getBuffer(), entry.getDataStart(), (int) entry.getUncompressedSize());
} }
if (useLimitedDataStream) { if (useLimitedDataStream) {
return new LimitedInputStream(stream, entry.getUncompressedSize()); return new LimitedInputStream(stream, entry.getUncompressedSize());
@@ -318,14 +328,14 @@ public final class JadxZipParser implements IZipParser {
} }
if (entry.getCompressMethod() == 8) { if (entry.getCompressMethod() == 8) {
try { try {
return ZipDeflate.decompressEntryToBytes(byteBuffer, entry); return ZipDeflate.decompressEntryToBytes(getBuffer(), entry);
} catch (Exception e) { } catch (Exception e) {
entryParseFailed(entry, e); entryParseFailed(entry, e);
return useFallbackParser(entry).getBytes(); return useFallbackParser(entry).getBytes();
} }
} }
// treat any other compression methods values as UNCOMPRESSED // treat any other compression methods values as UNCOMPRESSED
return bufferToBytes(byteBuffer, entry.getDataStart(), (int) entry.getUncompressedSize()); return bufferToBytes(getBuffer(), entry.getDataStart(), (int) entry.getUncompressedSize());
} }
private static void verifyEntry(JadxZipEntry entry) { private static void verifyEntry(JadxZipEntry entry) {
@@ -361,7 +371,7 @@ public final class JadxZipParser implements IZipParser {
} }
@SuppressWarnings("resource") @SuppressWarnings("resource")
private ZipContent initFallbackParser() { private synchronized ZipContent initFallbackParser() {
if (fallbackZipContent == null) { if (fallbackZipContent == null) {
try { try {
fallbackZipContent = new FallbackZipParser(zipFile, options).open(); fallbackZipContent = new FallbackZipParser(zipFile, options).open();
@@ -378,7 +388,7 @@ public final class JadxZipParser implements IZipParser {
} }
private int readFlags(JadxZipEntry entry) { private int readFlags(JadxZipEntry entry) {
ByteBuffer buf = byteBuffer; ByteBuffer buf = getBuffer();
buf.position(entry.getEntryStart() + 6); buf.position(entry.getEntryStart() + 6);
return readU2(buf); return readU2(buf);
} }
@@ -407,6 +417,7 @@ public final class JadxZipParser implements IZipParser {
return new String(bytes, StandardCharsets.UTF_8); return new String(bytes, StandardCharsets.UTF_8);
} }
@SuppressWarnings("DataFlowIssue")
@Override @Override
public void close() throws IOException { public void close() throws IOException {
try { try {
+14 -7
View File
@@ -6,15 +6,17 @@ dependencies {
api(project(":jadx-plugins:jadx-input-api")) api(project(":jadx-plugins:jadx-input-api"))
api(project(":jadx-commons:jadx-zip")) api(project(":jadx-commons:jadx-zip"))
implementation("com.google.code.gson:gson:2.13.2") implementation("com.google.code.gson:gson:2.14.0")
testImplementation("org.apache.commons:commons-lang3:3.20.0") testImplementation("org.apache.commons:commons-lang3:3.20.0")
testImplementation(project(":jadx-plugins:jadx-dex-input")) testImplementation(project(":jadx-plugins:jadx-dex-input"))
testRuntimeOnly(project(":jadx-plugins:jadx-smali-input")) // 'ClassNotFound' error is raised if set as 'testRuntime'
testRuntimeOnly(project(":jadx-plugins:jadx-java-convert")) // for the plugins below when running the tests from vscode.
testRuntimeOnly(project(":jadx-plugins:jadx-java-input")) testImplementation(project(":jadx-plugins:jadx-smali-input"))
testRuntimeOnly(project(":jadx-plugins:jadx-raung-input")) testImplementation(project(":jadx-plugins:jadx-java-convert"))
testImplementation(project(":jadx-plugins:jadx-java-input"))
testImplementation(project(":jadx-plugins:jadx-raung-input"))
testImplementation("org.eclipse.jdt:ecj") { testImplementation("org.eclipse.jdt:ecj") {
version { version {
@@ -22,7 +24,7 @@ dependencies {
strictly("[3.33, 3.34[") // from 3.34 compiled with Java 17 strictly("[3.33, 3.34[") // from 3.34 compiled with Java 17
} }
} }
testImplementation("tools.profiler:async-profiler:4.2") testImplementation("tools.profiler:async-profiler:4.4")
} }
val jadxTestJavaVersion = getTestJavaVersion() val jadxTestJavaVersion = getTestJavaVersion()
@@ -30,7 +32,10 @@ val jadxTestJavaVersion = getTestJavaVersion()
fun getTestJavaVersion(): Int? { fun getTestJavaVersion(): Int? {
val envVarName = "JADX_TEST_JAVA_VERSION" val envVarName = "JADX_TEST_JAVA_VERSION"
val testJavaVer = System.getenv(envVarName)?.toInt() ?: return null val testJavaVer = System.getenv(envVarName)?.toInt() ?: return null
val currentJavaVer = java.toolchain.languageVersion.get().asInt() val currentJavaVer =
java.toolchain.languageVersion
.get()
.asInt()
if (testJavaVer < currentJavaVer) { if (testJavaVer < currentJavaVer) {
throw GradleException("'$envVarName' can't be set to lower version than $currentJavaVer") throw GradleException("'$envVarName' can't be set to lower version than $currentJavaVer")
} }
@@ -52,4 +57,6 @@ tasks.named<Test>("test") {
// exclude temp tests // exclude temp tests
exclude("**/tmp/*") exclude("**/tmp/*")
// maxHeapSize = "4g"
} }
@@ -95,7 +95,7 @@ public class JadxArgs implements Closeable {
/** /**
* Predicate that allows to filter the classes to be process based on their full name * Predicate that allows to filter the classes to be process based on their full name
*/ */
private Predicate<String> classFilter = null; private @Nullable Predicate<String> classFilter = null;
/** /**
* Save dependencies for classes accepted by {@code classFilter} * Save dependencies for classes accepted by {@code classFilter}
@@ -227,7 +227,6 @@ public class JadxArgs implements Closeable {
@Override @Override
public void close() { public void close() {
try { try {
inputFiles = null;
if (codeCache != null) { if (codeCache != null) {
codeCache.close(); codeCache.close();
} }
@@ -239,9 +238,6 @@ public class JadxArgs implements Closeable {
} }
} catch (Exception e) { } catch (Exception e) {
LOG.error("Failed to close JadxArgs", e); LOG.error("Failed to close JadxArgs", e);
} finally {
codeCache = null;
usageInfoCache = null;
} }
} }
@@ -498,7 +494,7 @@ public class JadxArgs implements Closeable {
*/ */
@Deprecated @Deprecated
public void setUseSourceNameAsClassAlias(boolean useSourceNameAsClassAlias) { public void setUseSourceNameAsClassAlias(boolean useSourceNameAsClassAlias) {
final var useSourceNameAsClassNameAlias = UseSourceNameAsClassNameAlias.create(useSourceNameAsClassAlias); var useSourceNameAsClassNameAlias = UseSourceNameAsClassNameAlias.create(useSourceNameAsClassAlias);
setUseSourceNameAsClassNameAlias(useSourceNameAsClassNameAlias); setUseSourceNameAsClassNameAlias(useSourceNameAsClassNameAlias);
} }
+23 -11
View File
@@ -6,12 +6,11 @@ import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.ICodeAnnotation;
import jadx.api.metadata.ICodeNodeRef; import jadx.api.metadata.ICodeNodeRef;
@@ -26,11 +25,9 @@ import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.ListUtils; import jadx.core.utils.ListUtils;
public final class JavaClass implements JavaNode { public final class JavaClass implements JavaNode {
private static final Logger LOG = LoggerFactory.getLogger(JavaClass.class); private final @Nullable JadxDecompiler decompiler;
private final JadxDecompiler decompiler;
private final ClassNode cls; private final ClassNode cls;
private final JavaClass parent; private final @Nullable JavaClass parent;
private List<JavaClass> innerClasses = Collections.emptyList(); private List<JavaClass> innerClasses = Collections.emptyList();
private List<JavaClass> inlinedClasses = Collections.emptyList(); private List<JavaClass> inlinedClasses = Collections.emptyList();
@@ -38,7 +35,7 @@ public final class JavaClass implements JavaNode {
private List<JavaMethod> methods = Collections.emptyList(); private List<JavaMethod> methods = Collections.emptyList();
private boolean listsLoaded; private boolean listsLoaded;
JavaClass(ClassNode classNode, JadxDecompiler decompiler) { JavaClass(ClassNode classNode, @NotNull JadxDecompiler decompiler) {
this.decompiler = decompiler; this.decompiler = decompiler;
this.cls = classNode; this.cls = classNode;
this.parent = null; this.parent = null;
@@ -47,7 +44,7 @@ public final class JavaClass implements JavaNode {
/** /**
* Inner classes constructor * Inner classes constructor
*/ */
JavaClass(ClassNode classNode, JavaClass parent) { JavaClass(ClassNode classNode, @NotNull JavaClass parent) {
this.decompiler = null; this.decompiler = null;
this.cls = classNode; this.cls = classNode;
this.parent = parent; this.parent = parent;
@@ -69,6 +66,21 @@ public final class JavaClass implements JavaNode {
load(); load();
} }
/**
* Detect if calling load() would trigger a potentially expensive decompilation operation.
*/
public boolean loadingWouldRequireDecompilation() {
if (listsLoaded) {
// lists are already populated, so it's safe regardless of the state of the class itself
return false;
}
if (cls.getState().isProcessComplete()) {
// decompilation has already finished
return false;
}
return true;
}
public synchronized ICodeInfo reload() { public synchronized ICodeInfo reload() {
listsLoaded = false; listsLoaded = false;
return cls.reloadCode(); return cls.reloadCode();
@@ -187,10 +199,10 @@ public final class JavaClass implements JavaNode {
if (parent != null) { if (parent != null) {
return parent.getRootDecompiler(); return parent.getRootDecompiler();
} }
return decompiler; return Objects.requireNonNull(decompiler);
} }
public ICodeAnnotation getAnnotationAt(int pos) { public @Nullable ICodeAnnotation getAnnotationAt(int pos) {
return getCodeInfo().getCodeMetadata().getAt(pos); return getCodeInfo().getCodeMetadata().getAt(pos);
} }
@@ -259,7 +271,7 @@ public final class JavaClass implements JavaNode {
} }
@Override @Override
public JavaClass getDeclaringClass() { public @Nullable JavaClass getDeclaringClass() {
return parent; return parent;
} }
@@ -5,21 +5,18 @@ import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.ICodeAnnotation;
import jadx.api.metadata.ICodeNodeRef; import jadx.api.metadata.ICodeNodeRef;
import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.MethodOverrideAttr; import jadx.core.dex.attributes.nodes.MethodOverrideAttr;
import jadx.core.dex.info.AccessInfo; import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.Utils; import jadx.core.utils.Utils;
public final class JavaMethod implements JavaNode { public final class JavaMethod implements JavaNode {
private static final Logger LOG = LoggerFactory.getLogger(JavaMethod.class);
private final MethodNode mth; private final MethodNode mth;
private final JavaClass parent; private final JavaClass parent;
@@ -72,6 +69,18 @@ public final class JavaMethod implements JavaNode {
return getDeclaringClass().getRootDecompiler().convertNodes(mth.getUseIn()); return getDeclaringClass().getRootDecompiler().convertNodes(mth.getUseIn());
} }
public List<JavaNode> getUsed() {
return getDeclaringClass().getRootDecompiler().convertNodes(mth.getUsed());
}
public List<MethodInfo> getUnresolvedUsed() {
return mth.getUnresolvedUsed();
}
public boolean callsSelf() {
return mth.callsSelf();
}
public List<JavaMethod> getOverrideRelatedMethods() { public List<JavaMethod> getOverrideRelatedMethods() {
MethodOverrideAttr ovrdAttr = mth.get(AType.METHOD_OVERRIDE); MethodOverrideAttr ovrdAttr = mth.get(AType.METHOD_OVERRIDE);
if (ovrdAttr == null) { if (ovrdAttr == null) {
@@ -32,8 +32,11 @@ public class CodeMetadataStorage implements ICodeMetadata {
return new CodeMetadataStorage(Collections.emptyMap(), Collections.emptyNavigableMap()); return new CodeMetadataStorage(Collections.emptyMap(), Collections.emptyNavigableMap());
} }
// <decomp file line number> -> <dex debug line number>
private final Map<Integer, Integer> lines; private final Map<Integer, Integer> lines;
// <character index into the file> -> <code annotation>
// the key is what is returned by AbstractCodeArea#getCaretPos() when clicking in a code panel.
private final NavigableMap<Integer, ICodeAnnotation> navMap; private final NavigableMap<Integer, ICodeAnnotation> navMap;
private CodeMetadataStorage(Map<Integer, Integer> lines, NavigableMap<Integer, ICodeAnnotation> navMap) { private CodeMetadataStorage(Map<Integer, Integer> lines, NavigableMap<Integer, ICodeAnnotation> navMap) {
@@ -2,6 +2,7 @@ package jadx.api.usage;
import java.util.List; import java.util.List;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.MethodNode;
@@ -18,5 +19,11 @@ public interface IUsageInfoVisitor {
void visitMethodsUsage(MethodNode mth, List<MethodNode> methods); void visitMethodsUsage(MethodNode mth, List<MethodNode> methods);
void visitMethodsUses(MethodNode mth, List<MethodNode> methods);
void visitUnresolvedMethodsUsage(MethodNode mth, List<MethodInfo> methods);
void visitIsSelfCall(MethodNode mth, boolean isSelfCall);
void visitComplete(); void visitComplete();
} }
@@ -15,6 +15,7 @@ import jadx.api.JadxArgs;
import jadx.core.deobf.DeobfuscatorVisitor; import jadx.core.deobf.DeobfuscatorVisitor;
import jadx.core.deobf.SaveDeobfMapping; import jadx.core.deobf.SaveDeobfMapping;
import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.visitors.AdjustForIfMergeVisitor;
import jadx.core.dex.visitors.AnonymousClassVisitor; import jadx.core.dex.visitors.AnonymousClassVisitor;
import jadx.core.dex.visitors.ApplyVariableNames; import jadx.core.dex.visitors.ApplyVariableNames;
import jadx.core.dex.visitors.AttachCommentsVisitor; import jadx.core.dex.visitors.AttachCommentsVisitor;
@@ -156,6 +157,8 @@ public class Jadx {
passes.add(new FixTypesVisitor()); passes.add(new FixTypesVisitor());
passes.add(new FinishTypeInference()); passes.add(new FinishTypeInference());
passes.add(new AdjustForIfMergeVisitor());
if (args.getUseKotlinMethodsForVarNames() != JadxArgs.UseKotlinMethodsForVarNames.DISABLE) { if (args.getUseKotlinMethodsForVarNames() != JadxArgs.UseKotlinMethodsForVarNames.DISABLE) {
passes.add(new ProcessKotlinInternals()); passes.add(new ProcessKotlinInternals());
} }
@@ -19,6 +19,7 @@ import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.DecompileModeOverrideAttr; import jadx.core.dex.attributes.nodes.DecompileModeOverrideAttr;
import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.LoadStage; import jadx.core.dex.nodes.LoadStage;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode; import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.visitors.DepthTraversal; import jadx.core.dex.visitors.DepthTraversal;
import jadx.core.dex.visitors.IDexTreeVisitor; import jadx.core.dex.visitors.IDexTreeVisitor;
@@ -207,6 +208,44 @@ public class ProcessClass {
} }
} }
public boolean processMethodUntilVisitor(MethodNode mth, String visitorName, boolean includeVisitor) {
IDexTreeVisitor foundPass = null;
IDexTreeVisitor prevPass = null;
for (IDexTreeVisitor pass : passes) {
if (pass.getName().equals(visitorName)) {
if (includeVisitor) {
foundPass = pass;
} else {
foundPass = prevPass;
}
break;
}
prevPass = pass;
}
if (foundPass == null) {
return false;
}
return processMethodToVisitor(mth, foundPass);
}
public boolean processMethodToVisitor(MethodNode mth, IDexTreeVisitor lastPassToProcess) {
synchronized (mth.getTopParentClass().getClassInfo()) {
try {
mth.unload();
mth.load();
for (IDexTreeVisitor pass : passes) {
DepthTraversal.visit(pass, mth);
if (pass == lastPassToProcess) {
return true;
}
}
} catch (Exception e) {
throw new JadxRuntimeException("Failed to process method to visitor: " + lastPassToProcess, e);
}
return false;
}
}
// TODO: make passes list private and not visible // TODO: make passes list private and not visible
public List<IDexTreeVisitor> getPasses() { public List<IDexTreeVisitor> getPasses() {
return passes; return passes;
@@ -34,6 +34,7 @@ import jadx.core.dex.attributes.nodes.EnumClassAttr;
import jadx.core.dex.attributes.nodes.EnumClassAttr.EnumField; import jadx.core.dex.attributes.nodes.EnumClassAttr.EnumField;
import jadx.core.dex.attributes.nodes.LineAttrNode; import jadx.core.dex.attributes.nodes.LineAttrNode;
import jadx.core.dex.attributes.nodes.MethodInlineAttr; import jadx.core.dex.attributes.nodes.MethodInlineAttr;
import jadx.core.dex.attributes.nodes.NotificationAttrNode;
import jadx.core.dex.attributes.nodes.SkipMethodArgsAttr; import jadx.core.dex.attributes.nodes.SkipMethodArgsAttr;
import jadx.core.dex.info.AccessInfo; import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.ClassInfo;
@@ -164,12 +165,7 @@ public class ClassGen {
if (af.isInterface()) { if (af.isInterface()) {
af = af.remove(AccessFlags.ABSTRACT) af = af.remove(AccessFlags.ABSTRACT)
.remove(AccessFlags.STATIC); .remove(AccessFlags.STATIC);
} else if (af.isEnum()) {
af = af.remove(AccessFlags.FINAL)
.remove(AccessFlags.ABSTRACT)
.remove(AccessFlags.STATIC);
} }
// 'static' and 'private' modifier not allowed for top classes (not inner) // 'static' and 'private' modifier not allowed for top classes (not inner)
if (!cls.getClassInfo().isInner()) { if (!cls.getClassInfo().isInner()) {
af = af.remove(AccessFlags.STATIC).remove(AccessFlags.PRIVATE); af = af.remove(AccessFlags.STATIC).remove(AccessFlags.PRIVATE);
@@ -294,7 +290,7 @@ public class ClassGen {
private void addInnerClsAndMethods(ICodeWriter clsCode) { private void addInnerClsAndMethods(ICodeWriter clsCode) {
Stream.of(cls.getInnerClasses(), cls.getMethods()) Stream.of(cls.getInnerClasses(), cls.getMethods())
.flatMap(Collection::stream) .flatMap(Collection::stream)
.filter(node -> !node.contains(AFlag.DONT_GENERATE) || fallback) .filter(node -> !skipNode(node))
.sorted(Comparator.comparingInt(LineAttrNode::getSourceLine)) .sorted(Comparator.comparingInt(LineAttrNode::getSourceLine))
.forEach(node -> { .forEach(node -> {
if (node instanceof ClassNode) { if (node instanceof ClassNode) {
@@ -305,6 +301,18 @@ public class ClassGen {
}); });
} }
private boolean skipNode(NotificationAttrNode node) {
if (fallback) {
return false;
}
if (Consts.DEBUG_ATTRIBUTES) {
if (node.contains(AType.JADX_COMMENTS)) {
return false;
}
}
return node.contains(AFlag.DONT_GENERATE);
}
private void addInnerClass(ICodeWriter code, ClassNode innerCls) { private void addInnerClass(ICodeWriter code, ClassNode innerCls) {
try { try {
ClassGen inClGen = new ClassGen(innerCls, getParentGen()); ClassGen inClGen = new ClassGen(innerCls, getParentGen());
@@ -1,7 +1,5 @@
package jadx.core.codegen; package jadx.core.codegen;
import java.util.Collections;
import java.util.Iterator;
import java.util.List; import java.util.List;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -29,6 +27,7 @@ import jadx.core.dex.attributes.nodes.JadxError;
import jadx.core.dex.attributes.nodes.JumpInfo; import jadx.core.dex.attributes.nodes.JumpInfo;
import jadx.core.dex.attributes.nodes.MethodOverrideAttr; import jadx.core.dex.attributes.nodes.MethodOverrideAttr;
import jadx.core.dex.attributes.nodes.MethodReplaceAttr; import jadx.core.dex.attributes.nodes.MethodReplaceAttr;
import jadx.core.dex.attributes.nodes.SkipMethodArgsAttr;
import jadx.core.dex.info.AccessInfo; import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.instructions.ConstStringNode; import jadx.core.dex.instructions.ConstStringNode;
import jadx.core.dex.instructions.IfNode; import jadx.core.dex.instructions.IfNode;
@@ -109,10 +108,6 @@ public class MethodGen {
if (clsAccFlags.isAnnotation()) { if (clsAccFlags.isAnnotation()) {
ai = ai.remove(AccessFlags.PUBLIC); ai = ai.remove(AccessFlags.PUBLIC);
} }
if (mth.getMethodInfo().isConstructor() && mth.getParentClass().isEnum()) {
ai = ai.remove(AccessInfo.VISIBILITY_FLAGS);
}
if (mth.getMethodInfo().hasAlias() && !ai.isConstructor()) { if (mth.getMethodInfo().hasAlias() && !ai.isConstructor()) {
CodeGenUtils.addRenamedComment(code, mth, mth.getName()); CodeGenUtils.addRenamedComment(code, mth, mth.getName());
} }
@@ -152,21 +147,7 @@ public class MethodGen {
code.add(defMth.getAlias()); code.add(defMth.getAlias());
} }
code.add('('); code.add('(');
addMethodArguments(code);
List<RegisterArg> args = mth.getArgRegs();
if (mth.getMethodInfo().isConstructor()
&& mth.getParentClass().contains(AType.ENUM_CLASS)) {
if (args.size() == 2) {
args = Collections.emptyList();
} else if (args.size() > 2) {
args = args.subList(2, args.size());
} else {
mth.addWarnComment("Incorrect number of args for enum constructor: " + args.size() + " (expected >= 2)");
}
} else if (mth.contains(AFlag.SKIP_FIRST_ARG)) {
args = args.subList(1, args.size());
}
addMethodArguments(code, args);
code.add(')'); code.add(')');
annotationGen.addThrows(mth, code); annotationGen.addThrows(mth, code);
@@ -209,12 +190,22 @@ public class MethodGen {
} }
} }
private void addMethodArguments(ICodeWriter code, List<RegisterArg> args) { private void addMethodArguments(ICodeWriter code) {
List<RegisterArg> args = mth.getArgRegs();
AnnotationMethodParamsAttr paramsAnnotation = mth.get(JadxAttrType.ANNOTATION_MTH_PARAMETERS); AnnotationMethodParamsAttr paramsAnnotation = mth.get(JadxAttrType.ANNOTATION_MTH_PARAMETERS);
int i = 0; int argNum = -1;
Iterator<RegisterArg> it = args.iterator(); int lastArgNum = args.size() - 1;
while (it.hasNext()) { boolean first = true;
RegisterArg mthArg = it.next(); for (RegisterArg mthArg : args) {
argNum++;
if (SkipMethodArgsAttr.isSkip(mth, argNum)) {
continue;
}
if (first) {
first = false;
} else {
code.add(", ");
}
SSAVar ssaVar = mthArg.getSVar(); SSAVar ssaVar = mthArg.getSVar();
CodeVar var; CodeVar var;
if (ssaVar == null) { if (ssaVar == null) {
@@ -226,7 +217,7 @@ public class MethodGen {
// add argument annotation // add argument annotation
if (paramsAnnotation != null) { if (paramsAnnotation != null) {
annotationGen.addForParameter(code, paramsAnnotation, i); annotationGen.addForParameter(code, paramsAnnotation, argNum);
} }
if (var.isFinal()) { if (var.isFinal()) {
code.add("final "); code.add("final ");
@@ -239,7 +230,7 @@ public class MethodGen {
} else { } else {
argType = varType; argType = varType;
} }
if (!it.hasNext() && mth.getAccessFlags().isVarArgs()) { if (argNum == lastArgNum && mth.getAccessFlags().isVarArgs()) {
// change last array argument to varargs // change last array argument to varargs
if (argType.isArray()) { if (argType.isArray()) {
ArgType elType = argType.getArrayElement(); ArgType elType = argType.getArrayElement();
@@ -258,11 +249,6 @@ public class MethodGen {
code.attachDefinition(VarNode.get(mth, var)); code.attachDefinition(VarNode.get(mth, var));
} }
code.add(varName); code.add(varName);
i++;
if (it.hasNext()) {
code.add(", ");
}
} }
} }
@@ -411,13 +397,12 @@ public class MethodGen {
for (IDexTreeVisitor visitor : Jadx.getFallbackPassesList()) { for (IDexTreeVisitor visitor : Jadx.getFallbackPassesList()) {
DepthTraversal.visit(visitor, mth); DepthTraversal.visit(visitor, mth);
} }
errors.forEach(err -> mth.addAttr(AType.JADX_ERROR, err));
} catch (Exception e) { } catch (Exception e) {
LOG.error("Error reload instructions in fallback mode:", e); LOG.error("Error reload instructions in fallback mode:", e);
code.startLine("// Can't load method instructions: " + e.getMessage()); code.startLine("// Can't load method instructions: " + e.getMessage());
return; return;
} finally { } finally {
errors.forEach(err -> mth.addAttr(AType.JADX_ERROR, err)); mth.addAttr(AType.JADX_ERROR, errors);
} }
} }
InsnNode[] insnArr = mth.getInstructions(); InsnNode[] insnArr = mth.getInstructions();
@@ -3,6 +3,8 @@ package jadx.core.codegen;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jadx.core.deobf.NameMapper; import jadx.core.deobf.NameMapper;
import jadx.core.dex.attributes.nodes.LoopLabelAttr; import jadx.core.dex.attributes.nodes.LoopLabelAttr;
@@ -84,15 +86,32 @@ public class NameGen {
return name; return name;
} }
private static final Pattern ENDS_WITH_NUMBER = Pattern.compile(".*(\\d+)$");
private String getUniqueVarName(String name) { private String getUniqueVarName(String name) {
String r = name; if (!varNames.contains(name)) {
int i = 2; varNames.add(name);
while (varNames.contains(r)) { return name;
r = name + i; }
i++; // code duplication reuse same variable in different places
// parse variable name and increment index
String base;
int i;
Matcher matcher = ENDS_WITH_NUMBER.matcher(name);
if (matcher.matches()) {
base = name.substring(0, matcher.start(1));
i = 1 + Integer.parseInt(matcher.group(1));
} else {
base = name;
i = 2;
}
while (true) {
String newName = base + i++;
if (!varNames.contains(newName)) {
varNames.add(newName);
return newName;
}
} }
varNames.add(r);
return r;
} }
private String makeArgName(CodeVar var) { private String makeArgName(CodeVar var) {
@@ -1,5 +1,7 @@
package jadx.core.codegen; package jadx.core.codegen;
import java.util.List;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -30,6 +32,10 @@ public class TypeGen {
return stype.getShortName(); return stype.getShortName();
} }
public static List<String> signatures(List<ArgType> types) {
return Utils.collectionMap(types, TypeGen::signature);
}
/** /**
* Convert literal arg to string (preferred method) * Convert literal arg to string (preferred method)
*/ */
@@ -83,9 +83,12 @@ public class FileTypeDetector {
try { try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true); factory.setNamespaceAware(true);
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); 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-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setXIncludeAware(false);
factory.setExpandEntityReferences(false);
DocumentBuilder builder = factory.newDocumentBuilder(); DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new java.io.ByteArrayInputStream(data)); Document doc = builder.parse(new java.io.ByteArrayInputStream(data));
@@ -18,17 +18,23 @@ public enum AFlag {
DONT_WRAP, DONT_WRAP,
DONT_INLINE, DONT_INLINE,
DONT_INLINE_CONST, DONT_INLINE_CONST,
DONT_INVERT, // don't invert this if statement
DONT_GENERATE, // process as usual, but don't output to generated code DONT_GENERATE, // process as usual, but don't output to generated code
COMMENT_OUT, // process as usual, but comment insn in generated code COMMENT_OUT, // process as usual, but comment insn in generated code
REMOVE, // can be completely removed REMOVE, // can be completely removed
REMOVE_SUPER_CLASS, // don't add super class REMOVE_SUPER_CLASS, // don't add super class
HIDDEN, // instruction used inside other instruction but not listed in args HIDDEN, // instruction used inside other instruction but not listed in args
CONVERTED_ENUM, // enum class successfully restored to original form
DONT_RENAME, // do not rename during deobfuscation DONT_RENAME, // do not rename during deobfuscation
FORCE_RAW_NAME, // force use of raw name instead alias FORCE_RAW_NAME, // force use of raw name instead alias
ADDED_TO_REGION, ADDED_TO_REGION,
DUPLICATED,
// this loop condition has been merged or otherwise shouldn't be subject to the 1 instruction limit
ALLOW_MULTIPLE_INSNS_LOOP_COND,
EXC_TOP_SPLITTER, EXC_TOP_SPLITTER,
EXC_BOTTOM_SPLITTER, EXC_BOTTOM_SPLITTER,
@@ -11,6 +11,7 @@ import jadx.core.dex.attributes.nodes.DecompileModeOverrideAttr;
import jadx.core.dex.attributes.nodes.EdgeInsnAttr; import jadx.core.dex.attributes.nodes.EdgeInsnAttr;
import jadx.core.dex.attributes.nodes.EnumClassAttr; import jadx.core.dex.attributes.nodes.EnumClassAttr;
import jadx.core.dex.attributes.nodes.EnumMapAttr; import jadx.core.dex.attributes.nodes.EnumMapAttr;
import jadx.core.dex.attributes.nodes.ExcSplitCrossAttr;
import jadx.core.dex.attributes.nodes.FieldReplaceAttr; import jadx.core.dex.attributes.nodes.FieldReplaceAttr;
import jadx.core.dex.attributes.nodes.ForceReturnAttr; import jadx.core.dex.attributes.nodes.ForceReturnAttr;
import jadx.core.dex.attributes.nodes.GenericInfoAttr; import jadx.core.dex.attributes.nodes.GenericInfoAttr;
@@ -92,6 +93,7 @@ public final class AType<T extends IJadxAttribute> implements IJadxAttrType<T> {
public static final AType<AttrList<SpecialEdgeAttr>> SPECIAL_EDGE = new AType<>(); public static final AType<AttrList<SpecialEdgeAttr>> SPECIAL_EDGE = new AType<>();
public static final AType<TmpEdgeAttr> TMP_EDGE = new AType<>(); public static final AType<TmpEdgeAttr> TMP_EDGE = new AType<>();
public static final AType<TryCatchBlockAttr> TRY_BLOCK = new AType<>(); public static final AType<TryCatchBlockAttr> TRY_BLOCK = new AType<>();
public static final AType<ExcSplitCrossAttr> EXC_SPLIT_CROSS = new AType<>();
// block or insn // block or insn
public static final AType<ExcHandlerAttr> EXC_HANDLER = new AType<>(); public static final AType<ExcHandlerAttr> EXC_HANDLER = new AType<>();
@@ -9,11 +9,19 @@ import jadx.core.utils.Utils;
public class AttrList<T> implements IJadxAttribute { public class AttrList<T> implements IJadxAttribute {
private static final int MAX_ATTRLIST_LENGTH = 300;
private final IJadxAttrType<AttrList<T>> type; private final IJadxAttrType<AttrList<T>> type;
private final List<T> list = new ArrayList<>(); private final List<T> list;
public AttrList(IJadxAttrType<AttrList<T>> type, List<T> attrList) {
this.type = type;
this.list = attrList;
}
public AttrList(IJadxAttrType<AttrList<T>> type) { public AttrList(IJadxAttrType<AttrList<T>> type) {
this.type = type; this.type = type;
this.list = new ArrayList<>();
} }
public List<T> getList() { public List<T> getList() {
@@ -27,6 +35,11 @@ public class AttrList<T> implements IJadxAttribute {
@Override @Override
public String toString() { public String toString() {
return Utils.listToString(list, ", "); String commaDelimited = Utils.listToString(list, ", ");
// if the comma delimited list is too long, use newlines instead to maintain readability
if (commaDelimited.length() > MAX_ATTRLIST_LENGTH) {
return Utils.listToString(list, "\n ");
}
return commaDelimited;
} }
} }
@@ -50,8 +50,7 @@ public abstract class AttrNode implements IAttributeNode {
} }
public <T> void addAttr(IJadxAttrType<AttrList<T>> type, List<T> list) { public <T> void addAttr(IJadxAttrType<AttrList<T>> type, List<T> list) {
AttributeStorage strg = initStorage(); initStorage().addAttrList(type, list);
list.forEach(attr -> strg.add(type, attr));
} }
@Override @Override
@@ -14,6 +14,7 @@ import jadx.api.plugins.input.data.attributes.IJadxAttrType;
import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.api.plugins.input.data.attributes.IJadxAttribute;
import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.api.plugins.input.data.attributes.JadxAttrType;
import jadx.api.plugins.input.data.attributes.types.AnnotationsAttr; import jadx.api.plugins.input.data.attributes.types.AnnotationsAttr;
import jadx.core.utils.ListUtils;
import jadx.core.utils.Utils; import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.exceptions.JadxRuntimeException;
@@ -62,11 +63,20 @@ public class AttributeStorage {
public <T> void add(IJadxAttrType<AttrList<T>> type, T obj) { public <T> void add(IJadxAttrType<AttrList<T>> type, T obj) {
AttrList<T> list = get(type); AttrList<T> list = get(type);
if (list == null) { if (list != null) {
list = new AttrList<>(type); list.getList().add(obj);
add(list); } else {
add(new AttrList<>(type, ListUtils.mutableListOf(obj)));
}
}
public <T> void addAttrList(IJadxAttrType<AttrList<T>> type, List<T> attrList) {
AttrList<T> list = get(type);
if (list != null) {
list.getList().addAll(attrList);
} else {
add(new AttrList<>(type, attrList));
} }
list.getList().add(obj);
} }
public void addAll(AttributeStorage otherList) { public void addAll(AttributeStorage otherList) {
@@ -53,4 +53,9 @@ public class CodeFeaturesAttr implements IJadxAttribute {
public String toAttrString() { public String toAttrString() {
return "CodeFeatures{" + codeFeatures + '}'; return "CodeFeatures{" + codeFeatures + '}';
} }
@Override
public String toString() {
return toAttrString();
}
} }
@@ -2,6 +2,8 @@ package jadx.core.dex.attributes.nodes;
import java.util.List; import java.util.List;
import org.jetbrains.annotations.Nullable;
import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.api.plugins.input.data.attributes.IJadxAttribute;
import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.AType;
import jadx.core.dex.instructions.mods.ConstructorInsn; import jadx.core.dex.instructions.mods.ConstructorInsn;
@@ -14,11 +16,13 @@ public class EnumClassAttr implements IJadxAttribute {
public static class EnumField { public static class EnumField {
private final FieldNode field; private final FieldNode field;
private final ConstructorInsn constrInsn; private final ConstructorInsn constrInsn;
private final @Nullable String nameStr;
private ClassNode cls; private ClassNode cls;
public EnumField(FieldNode field, ConstructorInsn co) { public EnumField(FieldNode field, ConstructorInsn co, @Nullable String nameStr) {
this.field = field; this.field = field;
this.constrInsn = co; this.constrInsn = co;
this.nameStr = nameStr;
} }
public FieldNode getField() { public FieldNode getField() {
@@ -37,6 +41,10 @@ public class EnumClassAttr implements IJadxAttribute {
this.cls = cls; this.cls = cls;
} }
public @Nullable String getNameStr() {
return nameStr;
}
@Override @Override
public String toString() { public String toString() {
return field + "(" + constrInsn + ") " + cls; return field + "(" + constrInsn + ") " + cls;
@@ -0,0 +1,35 @@
package jadx.core.dex.attributes.nodes;
import jadx.api.plugins.input.data.attributes.IJadxAttrType;
import jadx.api.plugins.input.data.attributes.IJadxAttribute;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.nodes.BlockNode;
/**
* This attribute is set on the new synthetic node that BlockExceptionHandler creates at the bottom
* of certain try regions. It stores a reference to the original path cross of the bottom of the try
* region, so that blocks can be restructured to not pass through it when that would create an
* erroneous loop.
*/
public class ExcSplitCrossAttr implements IJadxAttribute {
private final BlockNode originalPathCross;
public ExcSplitCrossAttr(BlockNode originalPathCross) {
this.originalPathCross = originalPathCross;
}
public BlockNode getOriginalPathCross() {
return this.originalPathCross;
}
@Override
public IJadxAttrType<? extends IJadxAttribute> getAttrType() {
return AType.EXC_SPLIT_CROSS;
}
@Override
public String toString() {
return "ExcSplitCross -> " + originalPathCross.toString();
}
}
@@ -5,6 +5,7 @@ import java.util.BitSet;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import jadx.api.plugins.input.data.attributes.PinnedAttribute; import jadx.api.plugins.input.data.attributes.PinnedAttribute;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.AType;
import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.MethodNode;
@@ -34,6 +35,9 @@ public class SkipMethodArgsAttr extends PinnedAttribute {
if (mth == null) { if (mth == null) {
return false; return false;
} }
if (argNum == 0 && mth.contains(AFlag.SKIP_FIRST_ARG)) {
return true;
}
SkipMethodArgsAttr attr = mth.get(AType.SKIP_MTH_ARGS); SkipMethodArgsAttr attr = mth.get(AType.SKIP_MTH_ARGS);
if (attr == null) { if (attr == null) {
return false; return false;
@@ -214,9 +214,12 @@ public final class ClassInfo implements Comparable<ClassInfo> {
} }
public String getAliasFullPath() { public String getAliasFullPath() {
return getAliasPkg().replace('.', File.separatorChar) String fileName = getAliasNameWithoutPackage().replace('.', '_');
+ File.separatorChar String aliasPkg = getAliasPkg();
+ getAliasNameWithoutPackage().replace('.', '_'); if (aliasPkg.isEmpty()) {
return fileName;
}
return aliasPkg.replace('.', File.separatorChar) + File.separatorChar + fileName;
} }
public String getFullName() { public String getFullName() {
@@ -15,7 +15,6 @@ import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.IFieldInfoRef; import jadx.core.dex.nodes.IFieldInfoRef;
import jadx.core.dex.nodes.RootNode; import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.visitors.prepare.CollectConstValues;
public class ConstStorage { public class ConstStorage {
@@ -23,18 +22,18 @@ public class ConstStorage {
private final Map<Object, IFieldInfoRef> values = new ConcurrentHashMap<>(); private final Map<Object, IFieldInfoRef> values = new ConcurrentHashMap<>();
private final Set<Object> duplicates = new HashSet<>(); private final Set<Object> duplicates = new HashSet<>();
public Map<Object, IFieldInfoRef> getValues() { Map<Object, IFieldInfoRef> getValues() {
return values; return values;
} }
public IFieldInfoRef get(Object key) { IFieldInfoRef get(Object key) {
return values.get(key); return values.get(key);
} }
/** /**
* @return true if this value is duplicated * @return true if this value is duplicated
*/ */
public boolean put(Object value, IFieldInfoRef fld) { boolean put(Object value, IFieldInfoRef fld) {
if (duplicates.contains(value)) { if (duplicates.contains(value)) {
values.remove(value); values.remove(value);
return true; return true;
@@ -85,14 +84,6 @@ public class ConstStorage {
globalValues.put(value, fld); globalValues.put(value, fld);
} }
/**
* Use method from CollectConstValues class
*/
@Deprecated
public static @Nullable Object getFieldConstValue(FieldNode fld) {
return CollectConstValues.getFieldConstValue(fld);
}
public void removeForClass(ClassNode cls) { public void removeForClass(ClassNode cls) {
classes.remove(cls); classes.remove(cls);
globalValues.removeForCls(cls); globalValues.removeForCls(cls);
@@ -35,6 +35,6 @@ public final class ConstStringNode extends InsnNode {
@Override @Override
public String toString() { public String toString() {
return super.toString() + ' ' + StringUtils.getInstance().unescapeString(str); return super.baseString() + StringUtils.getInstance().unescapeString(str) + super.attributesString();
} }
} }
@@ -348,6 +348,7 @@ public class InsnDecoder {
return insn(InsnType.THROW, null, InsnArg.reg(insn, 0, ArgType.THROWABLE)); return insn(InsnType.THROW, null, InsnArg.reg(insn, 0, ArgType.THROWABLE));
case MOVE_EXCEPTION: case MOVE_EXCEPTION:
method.add(AFlag.COMPUTE_POST_DOM); // Post dominators required for try/catch block processing
return insn(InsnType.MOVE_EXCEPTION, InsnArg.reg(insn, 0, ArgType.UNKNOWN_OBJECT_NO_ARRAY)); return insn(InsnType.MOVE_EXCEPTION, InsnArg.reg(insn, 0, ArgType.UNKNOWN_OBJECT_NO_ARRAY));
case RETURN_VOID: case RETURN_VOID:
@@ -527,18 +528,10 @@ public class InsnDecoder {
private InsnNode makeNewArray(InsnData insn) { private InsnNode makeNewArray(InsnData insn) {
ArgType indexType = ArgType.parse(insn.getIndexAsType()); ArgType indexType = ArgType.parse(insn.getIndexAsType());
// NEW_ARRAY literal = dimensions to wrap the operand by: 0 if it is already the full array type
// (dalvik new-array, java multianewarray), 1 for newarray/anewarray (operand is the element type)
int dim = (int) insn.getLiteral(); int dim = (int) insn.getLiteral();
ArgType arrType; ArgType arrType = dim == 0 ? indexType : ArgType.array(indexType, dim);
if (dim == 0) {
arrType = indexType;
} else {
if (indexType.isArray()) {
// java bytecode can pass array as a base type
arrType = indexType;
} else {
arrType = ArgType.array(indexType, dim);
}
}
int regsCount = insn.getRegsCount(); int regsCount = insn.getRegsCount();
NewArrayNode newArr = new NewArrayNode(arrType, regsCount - 1); NewArrayNode newArr = new NewArrayNode(arrType, regsCount - 1);
newArr.setResult(InsnArg.reg(insn, 0, arrType)); newArr.setResult(InsnArg.reg(insn, 0, arrType));
@@ -12,6 +12,7 @@ import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.instructions.args.SSAVar;
import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.IBlock;
import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.InsnNode;
import jadx.core.utils.InsnRemover; import jadx.core.utils.InsnRemover;
import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.exceptions.JadxRuntimeException;
@@ -110,6 +111,18 @@ public final class PhiInsn extends InsnNode {
return null; return null;
} }
@Nullable
public RegisterArg getArgByBlock(IBlock block) {
if (getArgsCount() == 0) {
return null;
}
int index = blockBinds.indexOf(block);
if (index == -1) {
return null;
}
return getArg(index);
}
@Override @Override
public boolean replaceArg(InsnArg from, InsnArg to) { public boolean replaceArg(InsnArg from, InsnArg to) {
if (!(from instanceof RegisterArg) || !(to instanceof RegisterArg)) { if (!(from instanceof RegisterArg) || !(to instanceof RegisterArg)) {
@@ -10,6 +10,8 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly; import org.jetbrains.annotations.TestOnly;
import com.google.errorprone.annotations.Immutable;
import jadx.core.Consts; import jadx.core.Consts;
import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.nodes.RootNode; import jadx.core.dex.nodes.RootNode;
@@ -18,6 +20,7 @@ import jadx.core.utils.ListUtils;
import jadx.core.utils.Utils; import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.exceptions.JadxRuntimeException;
@Immutable
public abstract class ArgType { public abstract class ArgType {
public static final ArgType INT = primitive(PrimitiveType.INT); public static final ArgType INT = primitive(PrimitiveType.INT);
public static final ArgType BOOLEAN = primitive(PrimitiveType.BOOLEAN); public static final ArgType BOOLEAN = primitive(PrimitiveType.BOOLEAN);
@@ -200,7 +203,7 @@ public abstract class ArgType {
private static final class PrimitiveArg extends KnownType { private static final class PrimitiveArg extends KnownType {
private final PrimitiveType type; private final PrimitiveType type;
public PrimitiveArg(PrimitiveType type) { PrimitiveArg(PrimitiveType type) {
this.type = type; this.type = type;
this.hash = type.hashCode(); this.hash = type.hashCode();
} }
@@ -229,7 +232,7 @@ public abstract class ArgType {
private static class ObjectType extends KnownType { private static class ObjectType extends KnownType {
protected final String objName; protected final String objName;
public ObjectType(String obj) { ObjectType(String obj) {
this.objName = obj; this.objName = obj;
this.hash = objName.hashCode(); this.hash = objName.hashCode();
} }
@@ -263,15 +266,15 @@ public abstract class ArgType {
private static final class GenericType extends ObjectType { private static final class GenericType extends ObjectType {
private List<ArgType> extendTypes; private List<ArgType> extendTypes;
public GenericType(String obj) { GenericType(String obj) {
this(obj, Collections.emptyList()); this(obj, Collections.emptyList());
} }
public GenericType(String obj, ArgType extendType) { GenericType(String obj, ArgType extendType) {
this(obj, Collections.singletonList(extendType)); this(obj, Collections.singletonList(extendType));
} }
public GenericType(String obj, List<ArgType> extendTypes) { GenericType(String obj, List<ArgType> extendTypes) {
super(obj); super(obj);
this.extendTypes = extendTypes; this.extendTypes = extendTypes;
} }
@@ -337,7 +340,7 @@ public abstract class ArgType {
private final ArgType type; private final ArgType type;
private final WildcardBound bound; private final WildcardBound bound;
public WildcardType(ArgType obj, WildcardBound bound) { WildcardType(ArgType obj, WildcardBound bound) {
super(OBJECT.getObject()); super(OBJECT.getObject());
this.type = Objects.requireNonNull(obj); this.type = Objects.requireNonNull(obj);
this.bound = Objects.requireNonNull(bound); this.bound = Objects.requireNonNull(bound);
@@ -382,7 +385,7 @@ public abstract class ArgType {
private static class GenericObject extends ObjectType { private static class GenericObject extends ObjectType {
private final List<ArgType> generics; private final List<ArgType> generics;
public GenericObject(String obj, List<ArgType> generics) { GenericObject(String obj, List<ArgType> generics) {
super(obj); super(obj);
this.generics = Objects.requireNonNull(generics); this.generics = Objects.requireNonNull(generics);
this.hash = calcHash(); this.hash = calcHash();
@@ -418,7 +421,7 @@ public abstract class ArgType {
private final ObjectType outerType; private final ObjectType outerType;
private final ObjectType innerType; private final ObjectType innerType;
public OuterGenericObject(ObjectType outerType, ObjectType innerType) { OuterGenericObject(ObjectType outerType, ObjectType innerType) {
super(outerType.getObject() + '$' + innerType.getObject()); super(outerType.getObject() + '$' + innerType.getObject());
this.outerType = outerType; this.outerType = outerType;
this.innerType = innerType; this.innerType = innerType;
@@ -466,7 +469,7 @@ public abstract class ArgType {
private static final PrimitiveType[] ARRAY_POSSIBLES = new PrimitiveType[] { PrimitiveType.ARRAY }; private static final PrimitiveType[] ARRAY_POSSIBLES = new PrimitiveType[] { PrimitiveType.ARRAY };
private final ArgType arrayElement; private final ArgType arrayElement;
public ArrayArg(ArgType arrayElement) { ArrayArg(ArgType arrayElement) {
this.arrayElement = arrayElement; this.arrayElement = arrayElement;
this.hash = arrayElement.hashCode(); this.hash = arrayElement.hashCode();
} }
@@ -526,7 +529,7 @@ public abstract class ArgType {
private static final class UnknownArg extends ArgType { private static final class UnknownArg extends ArgType {
private final PrimitiveType[] possibleTypes; private final PrimitiveType[] possibleTypes;
public UnknownArg(PrimitiveType[] types) { UnknownArg(PrimitiveType[] types) {
this.possibleTypes = types; this.possibleTypes = types;
this.hash = Arrays.hashCode(possibleTypes); this.hash = Arrays.hashCode(possibleTypes);
} }
@@ -131,6 +131,7 @@ public abstract class InsnArg extends Typed {
} }
} }
} }
RegisterArg resArg = insn.getResult();
InsnArg arg = wrapInsnIntoArg(insn); InsnArg arg = wrapInsnIntoArg(insn);
InsnArg oldArg = parent.getArg(i); InsnArg oldArg = parent.getArg(i);
if (arg.getType() == ArgType.UNKNOWN) { if (arg.getType() == ArgType.UNKNOWN) {
@@ -141,6 +142,8 @@ public abstract class InsnArg extends Typed {
InsnRemover.unbindArgUsage(mth, oldArg); InsnRemover.unbindArgUsage(mth, oldArg);
if (unbind) { if (unbind) {
InsnRemover.unbindArgUsage(mth, this); InsnRemover.unbindArgUsage(mth, this);
}
if (resArg != null && !insn.contains(AFlag.FORCE_ASSIGN_INLINE)) {
// result not needed in wrapped insn // result not needed in wrapped insn
InsnRemover.unbindResult(mth, insn); InsnRemover.unbindResult(mth, insn);
insn.setResult(null); insn.setResult(null);
@@ -292,6 +295,17 @@ public abstract class InsnArg extends Typed {
return false; return false;
} }
public boolean isSameVar(SSAVar ssaVar) {
if (ssaVar == null) {
return false;
}
if (isRegister()) {
SSAVar thisSsaVar = ((RegisterArg) this).getSVar();
return Objects.equals(thisSsaVar, ssaVar);
}
return false;
}
public boolean isSameCodeVar(RegisterArg arg) { public boolean isSameCodeVar(RegisterArg arg) {
if (arg == null) { if (arg == null) {
return false; return false;
@@ -312,9 +326,7 @@ public abstract class InsnArg extends Typed {
return copy; return copy;
} }
public InsnArg duplicate() { public abstract InsnArg duplicate();
return this;
}
public String toShortString() { public String toShortString() {
return this.toString(); return this.toString();
@@ -41,7 +41,14 @@ public final class InsnWrapArg extends InsnArg {
@Override @Override
public InsnArg duplicate() { public InsnArg duplicate() {
InsnWrapArg copy = new InsnWrapArg(wrappedInsn.copyWithoutResult()); InsnNode wrapInsn = wrappedInsn;
InsnNode wrapInsnCopy = wrapInsn.copyWithoutResult();
if (wrapInsn.getResult() != null && wrapInsn.contains(AFlag.FORCE_ASSIGN_INLINE)) {
// keep same SSA var in result arg, this will break previous version, mark it for removal
wrapInsnCopy.setResult(wrapInsn.getResult().duplicate());
wrapInsn.add(AFlag.DONT_GENERATE);
}
InsnWrapArg copy = new InsnWrapArg(wrapInsnCopy);
copy.setType(type); copy.setType(type);
return copyCommonParams(copy); return copyCommonParams(copy);
} }
@@ -84,7 +91,7 @@ public final class InsnWrapArg extends InsnArg {
if (wrappedInsn.getType() == InsnType.CONST_STR) { if (wrappedInsn.getType() == InsnType.CONST_STR) {
return "(\"" + ((ConstStringNode) wrappedInsn).getString() + "\")"; return "(\"" + ((ConstStringNode) wrappedInsn).getString() + "\")";
} }
return "(wrap:" + type + ":" + wrappedInsn.getType() + ')'; return "(wrap " + type + ":" + wrappedInsn.getType() + ')';
} }
@Override @Override
@@ -92,6 +99,6 @@ public final class InsnWrapArg extends InsnArg {
if (wrappedInsn.getType() == InsnType.CONST_STR) { if (wrappedInsn.getType() == InsnType.CONST_STR) {
return "(\"" + ((ConstStringNode) wrappedInsn).getString() + "\")"; return "(\"" + ((ConstStringNode) wrappedInsn).getString() + "\")";
} }
return "(wrap:" + type + ":" + wrappedInsn + ')'; return "(wrap " + type + ":" + wrappedInsn + ')';
} }
} }
@@ -746,6 +746,14 @@ public class ClassNode extends NotificationAttrNode
} }
} }
public void getInnerClassesRecursive(Set<ClassNode> resultClassesSet) {
for (ClassNode innerCls : innerClasses) {
if (resultClassesSet.add(innerCls)) {
innerCls.getInnerAndInlinedClassesRecursive(resultClassesSet);
}
}
}
public void addInnerClass(ClassNode cls) { public void addInnerClass(ClassNode cls) {
if (innerClasses.isEmpty()) { if (innerClasses.isEmpty()) {
innerClasses = new ArrayList<>(5); innerClasses = new ArrayList<>(5);
@@ -1,12 +1,24 @@
package jadx.core.dex.nodes; package jadx.core.dex.nodes;
public class Edge { import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AttrNode;
public class Edge extends AttrNode {
private final BlockNode source; private final BlockNode source;
private final BlockNode target; private final BlockNode target;
public Edge(BlockNode source, BlockNode target) { public Edge(BlockNode source, BlockNode target) {
this(source, target, false);
}
public Edge(BlockNode source, BlockNode target, boolean isSynthetic) {
if (isSynthetic) {
this.add(AFlag.SYNTHETIC);
}
this.source = source; this.source = source;
this.target = target; this.target = target;
} }
public BlockNode getSource() { public BlockNode getSource() {
@@ -17,6 +29,10 @@ public class Edge {
return target; return target;
} }
public boolean isSynthetic() {
return this.contains(AFlag.SYNTHETIC);
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) { if (this == o) {
@@ -406,6 +406,14 @@ public class InsnNode extends LineAttrNode {
&& Objects.equals(arguments, other.arguments); && Objects.equals(arguments, other.arguments);
} }
@SuppressWarnings("unchecked")
public static <T extends InsnArg> @Nullable T duplicateArg(@Nullable T arg) {
if (arg == null) {
return null;
}
return (T) arg.duplicate();
}
protected final <T extends InsnNode> T copyCommonParams(T copy) { protected final <T extends InsnNode> T copyCommonParams(T copy) {
if (copy.getArgsCount() == 0) { if (copy.getArgsCount() == 0) {
for (InsnArg arg : this.getArguments()) { for (InsnArg arg : this.getArguments()) {
@@ -2,8 +2,10 @@ package jadx.core.dex.nodes;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -71,7 +73,7 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
// decompilation data, reset on unload // decompilation data, reset on unload
private RegisterArg thisArg; private RegisterArg thisArg;
private List<RegisterArg> argsList; private List<RegisterArg> argsList;
private InsnNode[] instructions; private @Nullable InsnNode[] instructions;
private List<BlockNode> blocks; private List<BlockNode> blocks;
private int blocksMaxCId; private int blocksMaxCId;
private BlockNode enterBlock; private BlockNode enterBlock;
@@ -81,7 +83,14 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
private List<LoopInfo> loops; private List<LoopInfo> loops;
private Region region; private Region region;
// Methods that use this method
private List<MethodNode> useIn = Collections.emptyList(); private List<MethodNode> useIn = Collections.emptyList();
// Unresolved methods that use this method
private List<MethodInfo> unresolvedUsed = Collections.emptyList();
// Methods that this method uses
private Set<MethodNode> methodsUsed = new HashSet<>();
// True if this method contains a self call
private boolean callsSelf = false;
private JavaMethod javaNode; private JavaMethod javaNode;
@@ -96,11 +105,12 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
this.parentClass = classNode; this.parentClass = classNode;
this.accFlags = new AccessInfo(mthData.getAccessFlags(), AFType.METHOD); this.accFlags = new AccessInfo(mthData.getAccessFlags(), AFType.METHOD);
ICodeReader codeReader = mthData.getCodeReader(); ICodeReader codeReader = mthData.getCodeReader();
this.noCode = codeReader == null; if (codeReader == null) {
if (noCode) { this.noCode = true;
this.codeReader = null; this.codeReader = null;
this.insnsCount = 0; this.insnsCount = 0;
} else { } else {
this.noCode = false;
this.codeReader = codeReader.copy(); this.codeReader = codeReader.copy();
this.insnsCount = codeReader.getUnitsCount(); this.insnsCount = codeReader.getUnitsCount();
} }
@@ -120,6 +130,7 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
sVars = Collections.emptyList(); sVars = Collections.emptyList();
instructions = null; instructions = null;
blocks = null; blocks = null;
blocksMaxCId = 0;
enterBlock = null; enterBlock = null;
exitBlock = null; exitBlock = null;
region = null; region = null;
@@ -702,12 +713,56 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
return codeReader; return codeReader;
} }
@Override
public List<MethodNode> getUseIn() { public List<MethodNode> getUseIn() {
return useIn; return useIn;
} }
// Do not modify passed list after setting
public void setUseIn(List<MethodNode> useIn) { public void setUseIn(List<MethodNode> useIn) {
this.useIn = useIn; this.useIn = useIn;
// Notify all methods (callers) this method (callee) is used in
for (MethodNode methodUsedIn : useIn) {
methodUsedIn.addUsed(this);
}
}
public void addUsed(MethodNode used) {
if (used != null) {
this.methodsUsed.add(used);
}
}
public void setUsed(List<MethodNode> methodsUsed) {
this.methodsUsed = new HashSet<>(methodsUsed);
}
public Set<MethodNode> getUsed() {
this.removeInvalidMethodsUsed();
return methodsUsed;
}
public List<MethodInfo> getUnresolvedUsed() {
return unresolvedUsed;
}
public void setUnresolvedUsed(List<MethodInfo> unresolvedUsed) {
this.unresolvedUsed = unresolvedUsed;
}
public void setCallsSelf(boolean callsSelf) {
this.callsSelf = callsSelf;
}
public boolean callsSelf() {
return this.callsSelf;
}
// Remove any methods from the list of used methods (calees) if this method (caller) has been
// removed from the calee's list of callers
private void removeInvalidMethodsUsed() {
methodsUsed.removeIf(methodUsed -> !methodUsed.getUseIn().contains(this));
} }
public JavaMethod getJavaNode() { public JavaMethod getJavaNode() {
@@ -74,7 +74,9 @@ public final class SwitchRegion extends AbstractRegion implements IBranchRegion
public List<IContainer> getSubBlocks() { public List<IContainer> getSubBlocks() {
List<IContainer> all = new ArrayList<>(cases.size() + 1); List<IContainer> all = new ArrayList<>(cases.size() + 1);
all.add(header); all.add(header);
all.addAll(getCaseContainers()); for (CaseInfo caseInfo : cases) {
all.add(caseInfo.container);
}
return Collections.unmodifiableList(all); return Collections.unmodifiableList(all);
} }
@@ -5,6 +5,8 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import org.jetbrains.annotations.Nullable;
import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.MethodNode;
@@ -18,7 +20,7 @@ public final class IfInfo {
private final BlockNode elseBlock; private final BlockNode elseBlock;
private final Set<BlockNode> skipBlocks; private final Set<BlockNode> skipBlocks;
private final List<InsnNode> forceInlineInsns; private final List<InsnNode> forceInlineInsns;
private BlockNode outBlock; private @Nullable BlockNode outBlock;
public IfInfo(MethodNode mth, IfCondition condition, BlockNode thenBlock, BlockNode elseBlock) { public IfInfo(MethodNode mth, IfCondition condition, BlockNode thenBlock, BlockNode elseBlock) {
this(mth, condition, thenBlock, elseBlock, BlockSet.empty(mth), new HashSet<>(), new ArrayList<>()); this(mth, condition, thenBlock, elseBlock, BlockSet.empty(mth), new HashSet<>(), new ArrayList<>());
@@ -84,11 +86,11 @@ public final class IfInfo {
return elseBlock; return elseBlock;
} }
public BlockNode getOutBlock() { public @Nullable BlockNode getOutBlock() {
return outBlock; return outBlock;
} }
public void setOutBlock(BlockNode outBlock) { public void setOutBlock(@Nullable BlockNode outBlock) {
this.outBlock = outBlock; this.outBlock = outBlock;
} }
@@ -7,6 +7,7 @@ import org.jetbrains.annotations.Nullable;
import jadx.api.ICodeWriter; import jadx.api.ICodeWriter;
import jadx.core.codegen.RegionGen; import jadx.core.codegen.RegionGen;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.nodes.LoopInfo; import jadx.core.dex.attributes.nodes.LoopInfo;
import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.BlockNode;
@@ -126,6 +127,7 @@ public final class LoopRegion extends ConditionRegion {
preCondInsns.addAll(condInsns); preCondInsns.addAll(condInsns);
condInsns.clear(); condInsns.clear();
condInsns.addAll(preCondInsns); condInsns.addAll(preCondInsns);
header.add(AFlag.ALLOW_MULTIPLE_INSNS_LOOP_COND);
preCondInsns.clear(); preCondInsns.clear();
preCondition = null; preCondition = null;
} }
@@ -6,26 +6,31 @@ import java.util.List;
import java.util.Objects; import java.util.Objects;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.Consts; import jadx.core.Consts;
import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.IContainer; import jadx.core.dex.nodes.IRegion;
import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.InsnUtils; import jadx.core.utils.InsnUtils;
import jadx.core.utils.Utils; import jadx.core.utils.Utils;
public class ExceptionHandler { public class ExceptionHandler {
private static final Logger LOG = LoggerFactory.getLogger(ExceptionHandler.class);
private final List<ClassInfo> catchTypes = new ArrayList<>(1); private final List<ClassInfo> catchTypes = new ArrayList<>(1);
private final int handlerOffset; private final int handlerOffset;
private BlockNode handlerBlock; private BlockNode handlerBlock;
private final List<BlockNode> blocks = new ArrayList<>(); private final List<BlockNode> blocks = new ArrayList<>();
private IContainer handlerRegion; private IRegion handlerRegion;
private InsnArg arg; private InsnArg arg;
private TryCatchBlockAttr tryBlock; private TryCatchBlockAttr tryBlock;
@@ -117,11 +122,11 @@ public class ExceptionHandler {
blocks.add(node); blocks.add(node);
} }
public IContainer getHandlerRegion() { public IRegion getHandlerRegion() {
return handlerRegion; return handlerRegion;
} }
public void setHandlerRegion(IContainer handlerRegion) { public void setHandlerRegion(IRegion handlerRegion) {
this.handlerRegion = handlerRegion; this.handlerRegion = handlerRegion;
} }
@@ -153,6 +158,42 @@ public class ExceptionHandler {
return removed; return removed;
} }
@Nullable
public BlockNode getBottomSplitter() {
TryCatchBlockAttr handlerTryBlock = getTryBlock();
// TODO: Implement support for finding bottom splitter of catch with inner tries
if (handlerTryBlock.getInnerTryBlocks().size() > 1) {
LOG.warn("No support yet for finding bottom block of try body with multipe inner trys");
return null;
}
TryCatchBlockAttr searchForTryBody;
if (handlerTryBlock.getInnerTryBlocks().isEmpty()) {
searchForTryBody = handlerTryBlock;
} else {
searchForTryBody = Utils.getOne(handlerTryBlock.getInnerTryBlocks());
}
BlockNode splitter = null;
for (BlockNode handlerPredecessor : getHandlerBlock().getPredecessors()) {
if (!handlerPredecessor.contains(AFlag.EXC_BOTTOM_SPLITTER)) {
continue;
}
for (BlockNode splitterPredecessor : handlerPredecessor.getPredecessors()) {
TryCatchBlockAttr tryBody = splitterPredecessor.get(AType.TRY_BLOCK);
if (tryBody == searchForTryBody) {
splitter = handlerPredecessor;
break;
}
}
if (splitter != null) {
break;
}
}
return splitter;
}
public void markForRemove() { public void markForRemove() {
this.removed = true; this.removed = true;
this.blocks.forEach(b -> b.add(AFlag.REMOVE)); this.blocks.forEach(b -> b.add(AFlag.REMOVE));
@@ -2,17 +2,34 @@ package jadx.core.dex.trycatch;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
import jadx.api.plugins.input.data.attributes.IJadxAttrType; import jadx.api.plugins.input.data.attributes.IJadxAttrType;
import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.api.plugins.input.data.attributes.IJadxAttribute;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.LoopInfo;
import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.Edge;
import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.BlockUtils;
import jadx.core.utils.ListUtils;
import jadx.core.utils.Utils; import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException;
public class TryCatchBlockAttr implements IJadxAttribute { public class TryCatchBlockAttr implements IJadxAttribute {
public static boolean isImplicitOrMerged(TryCatchBlockAttr tryBlock) {
return tryBlock.isMerged() || tryBlock.getHandlers().isEmpty();
}
private final int id; private final int id;
private final List<ExceptionHandler> handlers; private final List<ExceptionHandler> handlers;
private List<BlockNode> blocks; private List<BlockNode> blocks;
@@ -134,6 +151,242 @@ public class TryCatchBlockAttr implements IJadxAttribute {
return id; return id;
} }
public List<TryEdge> getHandlerTryEdges() {
List<ExceptionHandler> mergedHandlers = getMergedHandlers();
List<TryEdge> edges = new ArrayList<>(mergedHandlers.size());
for (ExceptionHandler handler : mergedHandlers) {
BlockNode handlerBlock = handler.getHandlerBlock();
BlockNode handlerSplitter = handler.getBottomSplitter();
if (handlerSplitter == null) {
// If we cannot find a bottom splitter, there might be none. In this case, assume that the top
// splitter of this try catch is the source of the exit.
List<BlockNode> allChildren = ListUtils.filter(handlerBlock.getPredecessors(), blk -> getBlocks().contains(blk));
handlerSplitter = BlockUtils.getBottomBlock(allChildren);
if (handlerSplitter == null) {
handlerSplitter = getTopSplitter();
}
}
TryEdge edge = new TryEdge(handlerSplitter, handlerBlock, handler);
edges.add(edge);
}
return edges;
}
public List<TryEdge> getFallthroughTryEdges() {
List<TryEdge> edges = new LinkedList<>();
List<BlockNode> exploredBlocks = new ArrayList<>();
List<TryCatchBlockAttr> exploredTrys = new LinkedList<>();
getFallthroughTryEdges(edges, exploredBlocks, exploredTrys);
return edges;
}
public void getFallthroughTryEdges(List<TryEdge> edges, List<BlockNode> exploredBlocks, List<TryCatchBlockAttr> exploredTrys) {
List<ExceptionHandler> mergedHandlers = getMergedHandlers();
Set<BlockNode> searchBlocks = new HashSet<>(getBlocks());
for (ExceptionHandler handler : mergedHandlers) {
handler.getBlocks().forEach(searchBlocks::remove);
}
BlockNode sourceBlock = BlockUtils.getTopBlock(new ArrayList<>(searchBlocks));
if (sourceBlock != null) {
exploredTrys.add(this);
exploreTryPath(edges, sourceBlock, searchBlocks, exploredBlocks, exploredTrys);
}
}
public List<TryEdge> getTryEdges() {
List<TryEdge> handlerEdges = getHandlerTryEdges();
List<TryEdge> fallthroughEdges = getFallthroughTryEdges();
List<TryEdge> edges = new ArrayList<>(handlerEdges.size() + fallthroughEdges.size());
edges.addAll(handlerEdges);
edges.addAll(fallthroughEdges);
return Collections.unmodifiableList(edges);
}
private void exploreTryPath(List<TryEdge> edges, BlockNode blk, Set<BlockNode> searchBlocks, List<BlockNode> exploredBlocks,
List<TryCatchBlockAttr> exploredTrys) {
for (BlockNode successor : blk.getSuccessors()) {
// If a separate branch has already explored this block, we don't need to recalculate its exits.
if (exploredBlocks.contains(successor)) {
continue;
}
// If this is a bottom splitter, ignore - we only care about non-handler edges.
if (successor.contains(AFlag.EXC_BOTTOM_SPLITTER)) {
continue;
}
exploredBlocks.add(successor);
if (successor.contains(AFlag.LOOP_END)) {
var loopsAttrList = successor.get(AType.LOOP);
List<LoopInfo> loops = loopsAttrList.getList();
List<BlockNode> loopStartBlocks = new LinkedList<>();
for (LoopInfo loop : loops) {
loopStartBlocks.add(loop.getStart());
List<Edge> loopEdges = loop.getExitEdges();
for (Edge loopEdge : loopEdges) {
if (loopEdge.getTarget() == successor) {
loopStartBlocks.add(loopEdge.getSource());
}
}
}
boolean includesAllLoopStart = ListUtils.allMatch(loopStartBlocks, exploredBlocks::contains);
if (!includesAllLoopStart) {
edges.add(new TryEdge(blk, successor, TryEdgeType.LOOP_EXIT));
continue;
}
}
boolean isPathToAnySearchBlock = false;
for (BlockNode searchBlock : searchBlocks) {
if (BlockUtils.isPathExists(successor, searchBlock)) {
isPathToAnySearchBlock = true;
break;
}
}
if (!searchBlocks.contains(successor) && !isPathToAnySearchBlock) {
// This block is not contained within this try's block list. This can either be since it is an exit
// to the try or it is a block which leads to an exit (for example, an exception handler).
// If this block (successor) leads to an exit, then the "bottom block" of all try blocks and this
// block will be
// equal to the bottom block of all try blocks. If this block is an exit, then either:
// - a path does not exist from all try blocks to this block, thus making the bottom block null.
// - a path does exist from all try blocks to this block but no more try blocks follow, thus making
// the bottom block this block.
List<BlockNode> allBlocksWithCurrent = new ArrayList<>(getBlocks().size() + 1);
allBlocksWithCurrent.addAll(getBlocks());
allBlocksWithCurrent.add(successor);
BlockNode bottomBlock = BlockUtils.getBottomBlock(allBlocksWithCurrent);
if (!(bottomBlock == null || bottomBlock == successor)) {
// This block leads to an exit.
exploreTryPath(edges, successor, searchBlocks, exploredBlocks, exploredTrys);
continue;
}
BlockNode emptyPathEndOfSuccessor = BlockUtils.followEmptyPath(successor, false, false);
if (emptyPathEndOfSuccessor.contains(AFlag.EXC_TOP_SPLITTER)) {
// This block is an exit which enters another try catch. In this case, the next try catch is within
// the same scope. Thus, we will take all of the edges out of that try and add them to the list of
// edges of this try.
Set<TryCatchBlockAttr> nestedTrys = new HashSet<>();
List<BlockNode> allSuccessorsOnTryBody = ListUtils.filter(emptyPathEndOfSuccessor.getSuccessors(),
potentialTryBlock -> potentialTryBlock.contains(AFlag.TRY_ENTER));
for (BlockNode tryBodyEnter : allSuccessorsOnTryBody) {
TryCatchBlockAttr nestedTry = tryBodyEnter.get(AType.TRY_BLOCK);
if (nestedTry == null) {
continue;
}
// If we have already added a try's edges, skip over it to avoid infinite recursion.
if (exploredTrys.contains(nestedTry)) {
continue;
}
// Unsure of why these top splitters have to be the same for them to be "nested" trys, but this
// seems to work (?)
if (nestedTry.getTopSplitter() != getTopSplitter()) {
continue;
}
nestedTrys.add(nestedTry);
}
// Only will we attempt to add nested inners if there exists any. If none exist, perform normal
// handling of the edge.
if (!nestedTrys.isEmpty()) {
for (TryCatchBlockAttr nestedTry : nestedTrys) {
nestedTry.getFallthroughTryEdges(edges, exploredBlocks, exploredTrys);
}
continue;
}
}
if (bottomBlock == null) {
// This block is an exit which occurs before all try blocks are logically executed.
edges.add(new TryEdge(blk, successor, TryEdgeType.PREMATURE_EXIT));
} else if (bottomBlock == successor) {
// This block is an exit which occurs after all try blocks are logically executed.
edges.add(new TryEdge(blk, successor, TryEdgeType.TRUE_FALLTHROUGH));
} else {
// All possible cases should have been caught by the above if / else and the preceeding if.
// If this is hit, any changes made to this algorithm must aptly handle all possible code paths
// before executing this.
throw new JadxRuntimeException(
"Unexpected code execution branch taken during try edge resolution: blk="
+ blk + ",successor=" + successor);
}
} else {
exploreTryPath(edges, successor, searchBlocks, exploredBlocks, exploredTrys);
}
}
}
public List<ExceptionHandler> getMergedHandlers() {
boolean hasInnerBlocks = !getInnerTryBlocks().isEmpty();
List<ExceptionHandler> mergedHandlers;
if (hasInnerBlocks) {
// collect handlers from this and all inner blocks
// (intentionally not using recursive collect for now)
mergedHandlers = new ArrayList<>(getHandlers());
for (TryCatchBlockAttr innerTryBlock : getInnerTryBlocks()) {
mergedHandlers.addAll(innerTryBlock.getHandlers());
}
} else {
mergedHandlers = getHandlers();
}
return Collections.unmodifiableList(mergedHandlers);
}
public Map<TryEdge, BlockNode> getEdgeBlockMap() {
List<TryEdge> edges = getTryEdges();
Map<TryEdge, BlockNode> blockMap = new HashMap<>();
for (TryEdge edge : edges) {
blockMap.put(edge, edge.getTarget());
}
return blockMap;
}
public TryEdgeScopeGroupMap getExecutionScopeGroups(MethodNode mth) {
Map<TryEdge, BlockNode> handlerBlocks = getEdgeBlockMap();
TryEdgeScopeGroupMap scopeGroups = new TryEdgeScopeGroupMap(mth, this, handlerBlocks.size());
scopeGroups.populateFromEdges(handlerBlocks);
return scopeGroups;
}
public Map<BlockNode, List<TryEdge>> getHandlerFallthroughGroups(MethodNode mth, TryEdgeScopeGroupMap scopeGroups) {
return scopeGroups.getScopeEnds(mth);
}
public List<BlockNode> getSearchBlocksFromFallthroughGroups(MethodNode mth, ExceptionHandler finallyHandler,
Map<BlockNode, List<TryEdge>> fallthroughGroups) {
List<BlockNode> searchBlocks = new LinkedList<>();
for (Map.Entry<BlockNode, List<TryEdge>> entry : fallthroughGroups.entrySet()) {
BlockNode scopeEndBlock = entry.getKey();
List<TryEdge> sourceHandlers = entry.getValue();
for (BlockNode scopeEndPredecessor : scopeEndBlock.getPredecessors()) {
// Add all predecessors to the scope end which are connected to some handler's scope start
try (Stream<TryEdge> stream = sourceHandlers.stream()) {
Object[] matchedHandlerPaths =
stream.filter(handler -> !(handler.isHandlerExit() && handler.getExceptionHandler() == finallyHandler))
.map(handler -> handler.getTarget())
.filter(scopeStart -> BlockUtils.isPathExists(scopeStart, scopeEndPredecessor))
.toArray();
if (matchedHandlerPaths.length != 0) {
searchBlocks.add(scopeEndPredecessor);
}
}
}
}
return searchBlocks;
}
@Override @Override
public IJadxAttrType<? extends IJadxAttribute> getAttrType() { public IJadxAttrType<? extends IJadxAttribute> getAttrType() {
return AType.TRY_BLOCK; return AType.TRY_BLOCK;
@@ -0,0 +1,110 @@
package jadx.core.dex.trycatch;
import java.util.Objects;
import java.util.Optional;
import org.jetbrains.annotations.NotNull;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.utils.exceptions.JadxRuntimeException;
/**
* Represents an edge between two blocks representing an exit out of a try body.
* The source block will be within the try body.
*/
public final class TryEdge {
private final BlockNode source;
private final BlockNode target;
private final Optional<ExceptionHandler> handler;
private final TryEdgeType type;
public TryEdge(BlockNode source, BlockNode target, TryEdgeType type) {
this(source, target, type, Optional.empty());
}
public TryEdge(BlockNode source, BlockNode target, @NotNull ExceptionHandler handler) {
this(source, target, TryEdgeType.HANDLER, Optional.of(handler));
}
public TryEdge(BlockNode source, BlockNode target, TryEdgeType type, Optional<ExceptionHandler> handler) {
this.source = source;
this.target = target;
this.handler = handler;
this.type = type;
if (isHandlerExit() && handler.isEmpty()) {
throw new JadxRuntimeException("Attempted to add a null exception handler as an edge of \"" + type.toString() + "\" type");
} else if (isNotHandlerExit() && handler.isPresent()) {
throw new JadxRuntimeException("Attempted to add an exception handler as an edge of \"" + type.toString() + "\" type");
}
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("TryEdge: [");
sb.append(type);
sb.append(' ');
sb.append(source.toString());
sb.append(" -> ");
sb.append(target.toString());
sb.append("] - Handler: ");
if (handler.isEmpty()) {
sb.append("None");
} else {
sb.append(handler.get().toString());
}
return sb.toString();
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof TryEdge)) {
return false;
}
TryEdge other = (TryEdge) obj;
return source.equals(other.source)
&& target.equals(other.target)
&& handler.equals(other.handler)
&& type.equals(other.type);
}
@Override
public int hashCode() {
return Objects.hash(source, target, type, handler);
}
public BlockNode getSource() {
return source;
}
public BlockNode getTarget() {
return target;
}
public TryEdgeType getType() {
return type;
}
public boolean isHandlerExit() {
return type == TryEdgeType.HANDLER;
}
public boolean isNotHandlerExit() {
return !isHandlerExit();
}
public ExceptionHandler getExceptionHandler() {
if (!isHandlerExit()) {
throw new JadxRuntimeException("Attempted to get the exception handler of a non-handler edge type");
}
if (handler.isEmpty()) {
throw new JadxRuntimeException("Attempted to get the exception handler of a handler edge type, however none was present");
}
return handler.get();
}
}
@@ -0,0 +1,374 @@
package jadx.core.dex.trycatch;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jetbrains.annotations.Nullable;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.BlockUtils;
import jadx.core.utils.Pair;
import jadx.core.utils.exceptions.JadxRuntimeException;
/**
* A map which stores the information of how try edges correlate with each other.
* K is a try edge and V contains all other try edges whose who share the same logical scope.
*/
public final class TryEdgeScopeGroupMap implements Map<TryEdge, Map<TryEdge, BlockNode>> {
private static final class TryEdgeScope {
private final TryEdge edge;
private final BlockNode block;
public TryEdgeScope(TryEdge edge, BlockNode block) {
this.edge = edge;
this.block = block;
}
}
private final List<Pair<TryEdge>> mergedEdges = new ArrayList<>();
private final TryCatchBlockAttr tryCatch;
private final Map<TryEdge, Map<TryEdge, BlockNode>> underlyingMap;
public TryEdgeScopeGroupMap(MethodNode mth, TryCatchBlockAttr tryCatch, int initialCapacity) {
this.tryCatch = tryCatch;
underlyingMap = new HashMap<>(initialCapacity);
}
@Override
public void clear() {
underlyingMap.clear();
}
@Override
public boolean containsKey(Object key) {
return underlyingMap.containsKey(key);
}
@Override
public boolean containsValue(Object value) {
if (!(value instanceof TryEdge)) {
return false;
}
TryEdge edge = (TryEdge) value;
return underlyingMap.containsKey(edge);
}
@Override
public Set<Entry<TryEdge, Map<TryEdge, BlockNode>>> entrySet() {
return underlyingMap.entrySet();
}
@Override
public Map<TryEdge, BlockNode> get(Object key) {
return underlyingMap.get(key);
}
@Override
public boolean isEmpty() {
return underlyingMap.isEmpty();
}
@Override
public Set<TryEdge> keySet() {
return underlyingMap.keySet();
}
@Override
public Map<TryEdge, BlockNode> put(TryEdge key, Map<TryEdge, BlockNode> value) {
return underlyingMap.put(key, value);
}
@Override
public void putAll(Map<? extends TryEdge, ? extends Map<TryEdge, BlockNode>> otherMap) {
underlyingMap.putAll(otherMap);
}
@Override
public Map<TryEdge, BlockNode> remove(Object key) {
return underlyingMap.remove(key);
}
@Override
public int size() {
return underlyingMap.size();
}
@Override
public Collection<Map<TryEdge, BlockNode>> values() {
return underlyingMap.values();
}
public boolean hasMergedEdges() {
return !mergedEdges.isEmpty();
}
public List<Pair<TryEdge>> getMergedScopes() {
return mergedEdges;
}
public void populateFromEdges(Map<TryEdge, BlockNode> edges) {
mergeSameScopes(edges);
for (TryEdge edge : edges.keySet()) {
BlockNode edgeBlock = edges.get(edge);
Map<TryEdge, BlockNode> handlerFallthroughMap = createEdgeTerminusMap(edges, edge, edgeBlock);
put(edge, handlerFallthroughMap);
}
}
/**
* Returns a map of all points where edges meet with each other, dictating the end of that
* edge's scope.
*/
public Map<BlockNode, List<TryEdge>> getScopeEnds(MethodNode mth) {
Map<BlockNode, List<TryEdge>> groups = new HashMap<>();
// A list containing pairs of edges where there are no shared common clean successors between the
// two handlers. This usually indicates that these edge pairs must be processed differently.
List<TryEdge> isolatedEdgePairs = new LinkedList<>();
for (TryEdge mergeEdgeA : keySet()) {
Pair<TryEdge> edgeMergedPair = getMergedNodeFromEdge(mergeEdgeA);
if (edgeMergedPair != null) {
continue;
}
Map<TryEdge, BlockNode> handlerRelations = get(mergeEdgeA);
List<BlockNode> scopeEnds = new ArrayList<>(handlerRelations.size());
for (TryEdge mergeEdgeB : handlerRelations.keySet()) {
Pair<TryEdge> mergedPairFromRelation = getMergedNodeFromEdge(mergeEdgeB);
if (mergedPairFromRelation != null && mergedPairFromRelation.getFirst() == mergeEdgeA) {
continue;
}
BlockNode sharedTerminator = handlerRelations.get(mergeEdgeB);
if (sharedTerminator == null) {
// There are no common clean succesors between the two handlers.
isolatedEdgePairs.add(mergeEdgeB);
} else {
scopeEnds.add(sharedTerminator);
}
}
if (scopeEnds.isEmpty()) {
// Isolated edge pairs found - we will deal with them later
continue;
}
BlockNode topGrouping = BlockUtils.getTopBlock(scopeEnds);
if (groups.containsKey(topGrouping)) {
groups.get(topGrouping).add(mergeEdgeA);
} else {
List<TryEdge> groupingHandlers = new LinkedList<>();
groupingHandlers.add(mergeEdgeA);
groups.put(topGrouping, groupingHandlers);
}
}
for (TryEdge isolatedEdge : isolatedEdgePairs) {
boolean isInList = false;
for (List<TryEdge> foundEdges : groups.values()) {
if (foundEdges.contains(isolatedEdge)) {
isInList = true;
break;
}
}
if (isInList) {
// The isolated edge is not isolated with another handler - we can ignore this edge.
break;
}
// If an isolated edge has not been added to the groupings, we will add it now.
// This will be added by locating the point where the search for a common successor stops.
// Since a common successor of all blocks which do have some clean path can be found in the method
// exit node, the mentioned point will be the farthest successor of the edge target which has no
// clean successors.
BlockNode target = isolatedEdge.getTarget();
List<BlockNode> successorBlocks = BlockUtils.collectAllSuccessors(mth, target, true);
BlockNode cleanSuccessorEnd = BlockUtils.getBottomBlock(successorBlocks);
if (cleanSuccessorEnd == null) {
throw new JadxRuntimeException("Could not find bottom clean successor for isolated try edge");
}
List<TryEdge> scopeTerminusList;
if (groups.containsKey(cleanSuccessorEnd)) {
scopeTerminusList = groups.get(cleanSuccessorEnd);
} else {
scopeTerminusList = new LinkedList<>();
groups.put(cleanSuccessorEnd, scopeTerminusList);
}
scopeTerminusList.add(isolatedEdge);
}
if (groups.size() == 1) {
for (Pair<TryEdge> pair : mergedEdges) {
TryEdge keptEdge = pair.getFirst();
TryEdge removedEdge = pair.getSecond();
if (keptEdge.isHandlerExit() && !tryCatch.getHandlers().contains(keptEdge.getExceptionHandler())) {
continue;
}
if (removedEdge.isHandlerExit() && !tryCatch.getHandlers().contains(removedEdge.getExceptionHandler())) {
continue;
}
// If both handlers are not handler exits, we can assume that the code paths merge at some Phi node
// which begins the finally duplicated code.
if (keptEdge.isNotHandlerExit() && removedEdge.isNotHandlerExit()) {
continue;
}
for (List<TryEdge> edgesWithTerminus : groups.values()) {
if (edgesWithTerminus.contains(keptEdge)) {
edgesWithTerminus.remove(keptEdge);
}
}
BlockNode terminus = get(keptEdge).get(removedEdge);
List<TryEdge> terminusEdges;
if (!groups.containsKey(terminus)) {
terminusEdges = new LinkedList<>();
terminusEdges.add(keptEdge);
groups.put(terminus, terminusEdges);
} else {
terminusEdges = groups.get(terminus);
}
terminusEdges.add(removedEdge);
}
}
return groups;
}
@Nullable
private Pair<TryEdge> getMergedNodeFromEdge(TryEdge edge) {
for (Pair<TryEdge> pair : mergedEdges) {
if (pair.getSecond() == edge) {
return pair;
}
}
return null;
}
private Map<TryEdge, BlockNode> createEdgeTerminusMap(Map<TryEdge, BlockNode> edgeStartMap, TryEdge edge,
BlockNode edgeStart) {
Map<TryEdge, BlockNode> scopeRelations = new HashMap<>(edgeStartMap.size() - 1);
for (TryEdge otherEdge : edgeStartMap.keySet()) {
if (edge == otherEdge) {
continue;
}
BlockNode otherEdgeStart = edgeStartMap.get(otherEdge);
boolean eitherEdgeIsHandler = edge.isHandlerExit() || otherEdge.isHandlerExit();
if (otherEdgeStart == edgeStart && eitherEdgeIsHandler) {
continue;
}
if (otherEdgeStart.isMthExitBlock()) {
scopeRelations.put(otherEdge, otherEdgeStart);
// Everything leads to the exit node so merged edges are no longer needed
mergedEdges.clear();
continue;
}
if (edgeStart.isMthExitBlock()) {
scopeRelations.put(otherEdge, edgeStart);
// Everything leads to the exit node so merged edges are no longer needed
mergedEdges.clear();
continue;
}
BitSet sharedPostDominators = (BitSet) edgeStart.getPostDoms().clone();
BitSet otherPostDoms = otherEdgeStart.getPostDoms();
if (sharedPostDominators.isEmpty() || otherPostDoms.isEmpty()) {
continue;
}
sharedPostDominators.and(otherPostDoms);
List<BlockNode> postDomHandler = new LinkedList<>();
BlockNode currentBlock = edgeStart;
while (currentBlock != null) {
postDomHandler.add(currentBlock);
currentBlock = currentBlock.getIPostDom();
}
BlockNode commonPostDom = null;
currentBlock = otherEdgeStart;
while (currentBlock != null) {
if (postDomHandler.contains(currentBlock)) {
commonPostDom = currentBlock;
break;
}
currentBlock = currentBlock.getIPostDom();
}
BlockNode scopeEnd = commonPostDom;
scopeRelations.put(otherEdge, scopeEnd);
}
return scopeRelations;
}
/**
* If two scopes ever merge, as in if one edge leads to the same execution point as the target of
* another edge, this function will record it.
*
* @param handlers
* @return
*/
private Map<TryEdge, BlockNode> mergeSameScopes(Map<TryEdge, BlockNode> handlers) {
List<Entry<TryEdge, BlockNode>> exceptionHandlers = new ArrayList<>(handlers.entrySet());
List<Pair<TryEdgeScope>> handlerPairs = new LinkedList<>();
for (int i = 0; i < exceptionHandlers.size(); i++) {
for (int j = i + 1; j < exceptionHandlers.size(); j++) {
TryEdgeScope a = new TryEdgeScope(exceptionHandlers.get(i).getKey(), exceptionHandlers.get(i).getValue());
TryEdgeScope b = new TryEdgeScope(exceptionHandlers.get(j).getKey(), exceptionHandlers.get(j).getValue());
handlerPairs.add(new Pair<>(a, b));
}
}
Map<TryEdge, BlockNode> simplifiedScopes = new HashMap<>(handlers);
int i = 0;
while (i < handlerPairs.size()) {
Pair<TryEdgeScope> handlerPair = handlerPairs.get(i);
TryEdgeScope edgeScopeA = handlerPair.getFirst();
TryEdgeScope edgeScopeB = handlerPair.getSecond();
BlockNode edgeBlockA = edgeScopeA.block;
BlockNode edgeBlockB = edgeScopeB.block;
boolean pathExists = BlockUtils.isPathExists(edgeBlockA, edgeBlockB) || BlockUtils.isPathExists(edgeBlockB, edgeBlockA);
if (pathExists) {
BlockNode bottomBlock = BlockUtils.getBottomBlock(List.of(edgeBlockA, edgeBlockB));
// The two blocks are within the same scope - remove these from the matrix
TryEdge removeHandler = edgeBlockA != bottomBlock ? edgeScopeA.edge : edgeScopeB.edge;
TryEdge keepHandler = edgeBlockA == bottomBlock ? edgeScopeA.edge : edgeScopeB.edge;
simplifiedScopes.remove(removeHandler);
handlerPairs.remove(i);
mergedEdges.add(new Pair<>(keepHandler, removeHandler));
} else {
i++;
}
}
return simplifiedScopes;
}
}
@@ -0,0 +1,8 @@
package jadx.core.dex.trycatch;
public enum TryEdgeType {
TRUE_FALLTHROUGH,
PREMATURE_EXIT,
LOOP_EXIT,
HANDLER
}
@@ -0,0 +1,136 @@
package jadx.core.dex.visitors;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.SpecialEdgeAttr;
import jadx.core.dex.attributes.nodes.SpecialEdgeAttr.SpecialEdgeType;
import jadx.core.dex.instructions.InsnType;
import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.visitors.regions.RegionMakerVisitor;
import jadx.core.dex.visitors.typeinference.FinishTypeInference;
import jadx.core.utils.BlockUtils;
@JadxVisitor(
name = "AdjustForIfMergeVisitor",
desc = "Move instructions between if blocks that can't be inlined but are safe to push through the if to allow the ifs to merge",
runBefore = { RegionMakerVisitor.class },
runAfter = { FinishTypeInference.class }
)
public class AdjustForIfMergeVisitor extends AbstractVisitor {
@Override
public void visit(MethodNode mth) {
if (mth.isNoCode() || mth.getBasicBlocks() == null) {
return;
}
// Find candidates for adjustment by selecting blocks between two if statements
List<BlockNode> blocks = mth.getBasicBlocks();
for (BlockNode blk : blocks) {
if (areSurroundingsCorrectShape(blk)) {
BlockNode pred = blk.getPredecessors().get(0);
BlockNode succ = blk.getCleanSuccessors().get(0);
if (isSimpleIf(pred) && isSimpleIf(succ)) {
List<InsnNode> movableInstructions = getMovableInstructions(blk, succ);
if (!movableInstructions.isEmpty() && couldMerge(mth, pred, blk, succ)) {
doMove(mth, blk, succ, movableInstructions);
}
}
}
}
}
private boolean areSurroundingsCorrectShape(BlockNode blk) {
return (blk.getPredecessors().size() == 1 && blk.getCleanSuccessors().size() == 1);
}
private boolean isSimpleIf(BlockNode blk) {
return blk.getInstructions().size() == 1 && blk.getInstructions().get(0).getType() == InsnType.IF;
}
private boolean couldMerge(MethodNode mth, BlockNode pred, BlockNode blk, BlockNode succ) {
// we cannot merge if the edge from blk to succ is a back edge
// there's a function in BlockUtils that purports to check if something is a back edge but it
// doesn't so do it by hand here
List<SpecialEdgeAttr> specialEdges = mth.getAll(AType.SPECIAL_EDGE);
for (SpecialEdgeAttr edge : specialEdges) {
if (edge.getStart() == blk && edge.getEnd() == succ && edge.getType() == SpecialEdgeType.BACK_EDGE) {
mth.addDebugComment("Refusing to push insns through at block " + blk.toString() + " : edge to successor is a back edge.");
return false;
}
}
return true;
}
private List<InsnNode> getMovableInstructions(BlockNode blk, BlockNode succ) {
// A 'movable instruction' is one that does not impact either codegen or the semantics of the
// following block, so it can be pushed through into the new synthetics.
// For now, we just look for nop moves along the same register such that the target variable is not
// used in the succ block.
List<InsnNode> movableInstructions = new ArrayList<>();
for (InsnNode insn : blk.getInstructions()) {
if (insn.getType() == InsnType.MOVE) {
if (!(insn.getArg(0) instanceof RegisterArg)) {
// could be a LiteralArg
continue;
}
RegisterArg source = (RegisterArg) insn.getArg(0);
RegisterArg target = insn.getResult();
List<RegisterArg> uses = target.getSVar().getUseList();
for (RegisterArg use : uses) {
if (BlockUtils.blockContains(succ, use.getParentInsn())) {
// the target is used inside the successor, so we can't cleanly do the assignment afterwards
continue;
}
}
// we don't want to just push everything through, e.g.
// if (condition) { return; }
// x = 123456
// if (condition) { return; }
// would be a less clean result if the assignment was pushed into the block of the 2nd if.
if (source.getRegNum() == target.getRegNum()) {
movableInstructions.add(insn);
}
}
}
return movableInstructions;
}
private void doMove(MethodNode mth, BlockNode target, BlockNode bottomIf, List<InsnNode> movableInstructions) {
// Move instructions from the list out of blk and into new synthetics on each edge out of succ
// preserving instruction ordering, although it's unlikely that it would ever matter here
Collections.reverse(movableInstructions);
for (InsnNode insn : movableInstructions) {
target.getInstructions().remove(insn);
for (BlockNode succ : bottomIf.getCleanSuccessors()) {
succ.getInstructions().add(0, insn); // add at start
if (succ.contains(AFlag.LOOP_START)) {
// if we're merging into a loop condition, silence the warning when there's more than one
// instruction in the loop header
succ.add(AFlag.ALLOW_MULTIPLE_INSNS_LOOP_COND);
}
}
}
}
}
@@ -44,7 +44,8 @@ import jadx.core.utils.exceptions.JadxException;
runAfter = { runAfter = {
ModVisitor.class, ModVisitor.class,
FixAccessModifiers.class, FixAccessModifiers.class,
ProcessAnonymous.class ProcessAnonymous.class,
ExtractFieldInit.class
} }
) )
public class ClassModifier extends AbstractVisitor { public class ClassModifier extends AbstractVisitor {
@@ -326,8 +327,9 @@ public class ClassModifier extends AbstractVisitor {
} }
AccessInfo af = mth.getAccessFlags(); AccessInfo af = mth.getAccessFlags();
boolean publicConstructor = mth.isConstructor() && af.isPublic(); boolean publicConstructor = mth.isConstructor() && af.isPublic();
boolean enumDefConstructor = mth.isConstructor() && mth.getParentClass().contains(AFlag.CONVERTED_ENUM);
boolean clsInit = mth.getMethodInfo().isClassInit() && af.isStatic(); boolean clsInit = mth.getMethodInfo().isClassInit() && af.isStatic();
if (publicConstructor || clsInit) { if (publicConstructor || enumDefConstructor || clsInit) {
if (!BlockUtils.isAllBlocksEmpty(mth.getBasicBlocks())) { if (!BlockUtils.isAllBlocksEmpty(mth.getBasicBlocks())) {
return; return;
} }
@@ -164,7 +164,7 @@ public class ConstructorVisitor extends AbstractVisitor {
private static boolean canRemoveConstructor(MethodNode mth, ConstructorInsn co) { private static boolean canRemoveConstructor(MethodNode mth, ConstructorInsn co) {
ClassNode parentClass = mth.getParentClass(); ClassNode parentClass = mth.getParentClass();
if (co.isSuper() && (co.getArgsCount() == 0 || parentClass.isEnum())) { if (co.isSuper() && co.getArgsCount() == 0) {
return true; return true;
} }
if (co.isThis() && co.getArgsCount() == 0) { if (co.isThis() && co.getArgsCount() == 0) {
@@ -1,32 +1,12 @@
package jadx.core.dex.visitors; package jadx.core.dex.visitors;
import java.io.File; import java.io.File;
import java.util.Collections; import java.util.Optional;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import jadx.api.ICodeWriter;
import jadx.api.impl.SimpleCodeWriter;
import jadx.core.codegen.MethodGen;
import jadx.core.dex.attributes.IAttributeNode;
import jadx.core.dex.instructions.IfNode;
import jadx.core.dex.instructions.InsnType;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.IBlock;
import jadx.core.dex.nodes.IContainer;
import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.IRegion;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.trycatch.ExceptionHandler; import jadx.core.utils.DotGraphUtils;
import jadx.core.utils.BlockUtils;
import jadx.core.utils.InsnUtils;
import jadx.core.utils.RegionUtils;
import jadx.core.utils.StringUtils;
import jadx.core.utils.Utils;
import static jadx.core.codegen.MethodGen.FallbackOption.BLOCK_DUMP;
public class DotGraphVisitor extends AbstractVisitor { public class DotGraphVisitor extends AbstractVisitor {
@@ -38,6 +18,9 @@ public class DotGraphVisitor extends AbstractVisitor {
private final boolean useRegions; private final boolean useRegions;
private final boolean rawInsn; private final boolean rawInsn;
// if present, this region and it's children will still be drawn when not in regions mode.
private Optional<IRegion> highlightRegion;
public static DotGraphVisitor dump() { public static DotGraphVisitor dump() {
return new DotGraphVisitor(false, false); return new DotGraphVisitor(false, false);
} }
@@ -54,9 +37,26 @@ public class DotGraphVisitor extends AbstractVisitor {
return new DotGraphVisitor(true, true); return new DotGraphVisitor(true, true);
} }
/**
* Helper function to generate a cfg at a given point showing only one of the regions in the graph.
* Intended to be called during a debugging session to produce a CFG with only a region of interest,
* with DotGraphVisitor.debugDumpWithRegionHiglight(region).visit(mth);
*
* @param region the region to show
* @return the visitor, to be invoked with `.visit(mth);`
*/
public static DotGraphVisitor debugDumpWithRegionHighlight(IRegion region) {
return new DotGraphVisitor(false, false, Optional.of(region));
}
private DotGraphVisitor(boolean useRegions, boolean rawInsn) { private DotGraphVisitor(boolean useRegions, boolean rawInsn) {
this(useRegions, rawInsn, Optional.empty());
}
private DotGraphVisitor(boolean useRegions, boolean rawInsn, Optional<IRegion> highlightRegion) {
this.useRegions = useRegions; this.useRegions = useRegions;
this.rawInsn = rawInsn; this.rawInsn = rawInsn;
this.highlightRegion = highlightRegion;
} }
@Override @Override
@@ -69,264 +69,13 @@ public class DotGraphVisitor extends AbstractVisitor {
if (mth.isNoCode()) { if (mth.isNoCode()) {
return; return;
} }
File outRootDir = mth.root().getArgs().getOutDir(); new DotGraphUtils(useRegions, rawInsn, highlightRegion).dumpToFile(mth);
new DumpDotGraph(outRootDir).process(mth);
} }
public void save(File dir, MethodNode mth) { public void save(File dir, MethodNode mth) {
if (mth.isNoCode()) { if (mth.isNoCode()) {
return; return;
} }
new DumpDotGraph(dir).process(mth); new DotGraphUtils(useRegions, rawInsn, highlightRegion).dumpToFile(mth, dir);
}
private class DumpDotGraph {
private final ICodeWriter dot = new SimpleCodeWriter();
private final ICodeWriter conn = new SimpleCodeWriter();
private final File dir;
public DumpDotGraph(File dir) {
this.dir = dir;
}
public void process(MethodNode mth) {
dot.startLine("digraph \"CFG for");
dot.add(escape(mth.getMethodInfo().getFullId()));
dot.add("\" {");
BlockNode enterBlock = mth.getEnterBlock();
if (useRegions) {
if (mth.getRegion() == null) {
return;
}
processMethodRegion(mth);
} else {
List<BlockNode> blocks = mth.getBasicBlocks();
if (blocks == null) {
InsnNode[] insnArr = mth.getInstructions();
if (insnArr == null) {
return;
}
BlockNode block = new BlockNode(0, 0, 0);
List<InsnNode> insnList = block.getInstructions();
for (InsnNode insn : insnArr) {
if (insn != null) {
insnList.add(insn);
}
}
enterBlock = block;
blocks = Collections.singletonList(block);
}
for (BlockNode block : blocks) {
processBlock(mth, block, false);
}
}
dot.startLine("MethodNode[shape=record,label=\"{");
dot.add(escape(mth.getAccessFlags().makeString(true)));
dot.add(escape(mth.getReturnType() + " "
+ mth.getParentClass() + '.' + mth.getName()
+ '(' + Utils.listToString(mth.getAllArgRegs()) + ") "));
String attrs = attributesString(mth);
if (!attrs.isEmpty()) {
dot.add(" | ").add(attrs);
}
dot.add("}\"];");
dot.startLine("MethodNode -> ").add(makeName(enterBlock)).add(';');
dot.add(conn.toString());
dot.startLine('}');
dot.startLine();
String fileName = StringUtils.escape(mth.getMethodInfo().getShortId())
+ (useRegions ? ".regions" : "")
+ (rawInsn ? ".raw" : "")
+ ".dot";
File file = dir.toPath()
.resolve(mth.getParentClass().getClassInfo().getAliasFullPath() + "_graphs")
.resolve(fileName)
.toFile();
SaveCode.save(dot.finish(), file);
}
private void processMethodRegion(MethodNode mth) {
processRegion(mth, mth.getRegion());
for (ExceptionHandler h : mth.getExceptionHandlers()) {
if (h.getHandlerRegion() != null) {
processRegion(mth, h.getHandlerRegion());
}
}
Set<IBlock> regionsBlocks = new HashSet<>(mth.getBasicBlocks().size());
RegionUtils.getAllRegionBlocks(mth.getRegion(), regionsBlocks);
for (ExceptionHandler handler : mth.getExceptionHandlers()) {
IContainer handlerRegion = handler.getHandlerRegion();
if (handlerRegion != null) {
RegionUtils.getAllRegionBlocks(handlerRegion, regionsBlocks);
}
}
for (BlockNode block : mth.getBasicBlocks()) {
if (!regionsBlocks.contains(block)) {
processBlock(mth, block, true);
}
}
}
private void processRegion(MethodNode mth, IContainer region) {
if (region instanceof IRegion) {
IRegion r = (IRegion) region;
dot.startLine("subgraph " + makeName(region) + " {");
dot.startLine("label = \"").add(r.toString());
String attrs = attributesString(r);
if (!attrs.isEmpty()) {
dot.add(" | ").add(attrs);
}
dot.add("\";");
dot.startLine("node [shape=record,color=blue];");
for (IContainer c : r.getSubBlocks()) {
processRegion(mth, c);
}
dot.startLine('}');
} else if (region instanceof BlockNode) {
processBlock(mth, (BlockNode) region, false);
} else if (region instanceof IBlock) {
processIBlock(mth, (IBlock) region, false);
}
}
private void processBlock(MethodNode mth, BlockNode block, boolean error) {
String attrs = attributesString(block);
dot.startLine(makeName(block));
dot.add(" [shape=record,");
if (error) {
dot.add("color=red,");
}
dot.add("label=\"{");
dot.add(String.valueOf(block.getCId())).add("\\:\\ ");
dot.add(InsnUtils.formatOffset(block.getStartOffset()));
if (!attrs.isEmpty()) {
dot.add('|').add(attrs);
}
if (PRINT_DOMINATORS_INFO) {
dot.add('|');
dot.startLine("doms: ").add(escape(block.getDoms()));
dot.startLine("\\lidom: ").add(escape(block.getIDom()));
dot.startLine("\\lpost-doms: ").add(escape(block.getPostDoms()));
dot.startLine("\\lpost-idom: ").add(escape(block.getIPostDom()));
dot.startLine("\\ldom-f: ").add(escape(block.getDomFrontier()));
dot.startLine("\\ldoms-on: ").add(escape(Utils.listToString(block.getDominatesOn())));
dot.startLine("\\l");
}
String insns = insertInsns(mth, block);
if (!insns.isEmpty()) {
dot.add('|').add(insns);
}
dot.add("}\"];");
BlockNode falsePath = null;
InsnNode lastInsn = BlockUtils.getLastInsn(block);
if (lastInsn != null && lastInsn.getType() == InsnType.IF) {
falsePath = ((IfNode) lastInsn).getElseBlock();
}
for (BlockNode next : block.getSuccessors()) {
String style = next == falsePath ? "[style=dashed]" : "";
addEdge(block, next, style);
}
if (PRINT_DOMINATORS) {
for (BlockNode c : block.getDominatesOn()) {
conn.startLine(block.getCId() + " -> " + c.getCId() + "[color=green];");
}
for (BlockNode dom : BlockUtils.bitSetToBlocks(mth, block.getDomFrontier())) {
conn.startLine("f_" + block.getCId() + " -> f_" + dom.getCId() + "[color=blue];");
}
}
}
private void processIBlock(MethodNode mth, IBlock block, boolean error) {
String attrs = attributesString(block);
dot.startLine(makeName(block));
dot.add(" [shape=record,");
if (error) {
dot.add("color=red,");
}
dot.add("label=\"{");
if (!attrs.isEmpty()) {
dot.add(attrs);
}
String insns = insertInsns(mth, block);
if (!insns.isEmpty()) {
dot.add('|').add(insns);
}
dot.add("}\"];");
}
private void addEdge(BlockNode from, BlockNode to, String style) {
conn.startLine(makeName(from)).add(" -> ").add(makeName(to));
conn.add(style);
conn.add(';');
}
private String attributesString(IAttributeNode block) {
StringBuilder attrs = new StringBuilder();
for (String attr : block.getAttributesStringsList()) {
attrs.append(escape(attr)).append(NL);
}
return attrs.toString();
}
private String makeName(IContainer c) {
String name;
if (c instanceof BlockNode) {
name = "Node_" + ((BlockNode) c).getCId();
} else if (c instanceof IBlock) {
name = "Node_" + c.getClass().getSimpleName() + '_' + c.hashCode();
} else {
name = "cluster_" + c.getClass().getSimpleName() + '_' + c.hashCode();
}
return name;
}
private String insertInsns(MethodNode mth, IBlock block) {
if (rawInsn) {
StringBuilder sb = new StringBuilder();
for (InsnNode insn : block.getInstructions()) {
sb.append(escape(insn)).append(NL);
}
return sb.toString();
} else {
ICodeWriter code = new SimpleCodeWriter();
List<InsnNode> instructions = block.getInstructions();
MethodGen.addFallbackInsns(code, mth, instructions.toArray(new InsnNode[0]), BLOCK_DUMP);
String str = escape(code.newLine().toString());
if (str.startsWith(NL)) {
str = str.substring(NL.length());
}
return str;
}
}
private String escape(Object obj) {
if (obj == null) {
return "null";
}
return escape(obj.toString());
}
private String escape(String string) {
return string
.replace("\\", "") // TODO replace \"
.replace("/", "\\/")
.replace(">", "\\>").replace("<", "\\<")
.replace("{", "\\{").replace("}", "\\}")
.replace("\"", "\\\"")
.replace("-", "\\-")
.replace("|", "\\|")
.replaceAll("\\R", NLQR);
}
} }
} }
@@ -49,6 +49,7 @@ import jadx.core.utils.BlockInsnPair;
import jadx.core.utils.BlockUtils; import jadx.core.utils.BlockUtils;
import jadx.core.utils.InsnRemover; import jadx.core.utils.InsnRemover;
import jadx.core.utils.InsnUtils; import jadx.core.utils.InsnUtils;
import jadx.core.utils.ListUtils;
import jadx.core.utils.Utils; import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxException; import jadx.core.utils.exceptions.JadxException;
import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.exceptions.JadxRuntimeException;
@@ -72,6 +73,7 @@ import static jadx.core.utils.InsnUtils.getWrappedInsn;
} }
) )
public class EnumVisitor extends AbstractVisitor { public class EnumVisitor extends AbstractVisitor {
private static final String ENUM_SUPER_CONSTRUCTOR_ID = "java.lang.Enum.<init>(Ljava/lang/String;I)V";
private MethodInfo enumValueOfMth; private MethodInfo enumValueOfMth;
private MethodInfo cloneMth; private MethodInfo cloneMth;
@@ -149,6 +151,14 @@ public class EnumVisitor extends AbstractVisitor {
if (arrArg.isInsnWrap()) { if (arrArg.isInsnWrap()) {
InsnNode wrappedInsn = ((InsnWrapArg) arrArg).getWrapInsn(); InsnNode wrappedInsn = ((InsnWrapArg) arrArg).getWrapInsn();
enumFields = extractEnumFieldsFromInsn(data, wrappedInsn); enumFields = extractEnumFieldsFromInsn(data, wrappedInsn);
} else if (arrArg.isRegister()) {
// Kotlin 1.9+ $ENTRIES pattern: array register has multiple uses,
// preventing CodeShrinkVisitor from inlining into the SPUT
RegisterArg regArg = (RegisterArg) arrArg;
InsnNode assignInsn = regArg.getAssignInsn();
if (assignInsn != null) {
enumFields = extractEnumFieldsFromInsn(data, assignInsn);
}
} }
if (enumFields == null) { if (enumFields == null) {
cls.addWarnComment("Unknown enum class pattern. Please report as an issue!"); cls.addWarnComment("Unknown enum class pattern. Please report as an issue!");
@@ -162,11 +172,8 @@ public class EnumVisitor extends AbstractVisitor {
cls.addAttr(attr); cls.addAttr(attr);
for (EnumField enumField : attr.getFields()) { for (EnumField enumField : attr.getFields()) {
ConstructorInsn co = enumField.getConstrInsn();
FieldNode fieldNode = enumField.getField(); FieldNode fieldNode = enumField.getField();
String name = enumField.getNameStr();
// use string arg from the constructor as enum field name
String name = getConstString(cls.root(), co.getArg(0));
if (name != null if (name != null
&& !fieldNode.getAlias().equals(name) && !fieldNode.getAlias().equals(name)
&& NameMapper.isValidAndPrintable(name) && NameMapper.isValidAndPrintable(name)
@@ -184,9 +191,24 @@ public class EnumVisitor extends AbstractVisitor {
CodeShrinkVisitor.shrinkMethod(classInitMth); CodeShrinkVisitor.shrinkMethod(classInitMth);
} }
removeEnumMethods(cls, data.valuesField); removeEnumMethods(cls, data.valuesField);
fixAccessFlags(cls);
cls.add(AFlag.CONVERTED_ENUM);
return true; return true;
} }
private static void fixAccessFlags(ClassNode cls) {
// remove invalid access flags
cls.setAccessFlags(cls.getAccessFlags()
.remove(AccessFlags.FINAL)
.remove(AccessFlags.ABSTRACT)
.remove(AccessFlags.STATIC));
for (MethodNode mth : cls.getMethods()) {
if (mth.getMethodInfo().isConstructor()) {
mth.setAccessFlags(mth.getAccessFlags().remove(AccessInfo.VISIBILITY_FLAGS));
}
}
}
/** /**
* Search "$VALUES" field (holds all enum values) * Search "$VALUES" field (holds all enum values)
*/ */
@@ -291,8 +313,13 @@ public class EnumVisitor extends AbstractVisitor {
return null; return null;
} }
List<EnumField> enumFields = extractEnumFieldsFromInsn(enumData, wrappedInsn); List<EnumField> enumFields = extractEnumFieldsFromInsn(enumData, wrappedInsn);
if (enumFields != null) { if (enumFields != null && ListUtils.isSingleElement(valuesMth.getUseIn(), enumData.classInitMth)) {
valuesMth.add(AFlag.DONT_GENERATE); valuesMth.add(AFlag.DONT_GENERATE);
if (valuesMth.getName().equals("$values")) {
// Kotlin synthetic method used for init values
// rename to actual values method to use in $ENTRIES init code
valuesMth.getMethodInfo().setAlias("values");
}
} }
return enumFields; return enumFields;
} }
@@ -419,13 +446,9 @@ public class EnumVisitor extends AbstractVisitor {
return enumFieldNode; return enumFieldNode;
} }
@SuppressWarnings("StatementWithEmptyBody")
private EnumField createEnumFieldByConstructor(EnumData data, FieldNode enumFieldNode, ConstructorInsn co) { private EnumField createEnumFieldByConstructor(EnumData data, FieldNode enumFieldNode, ConstructorInsn co) {
// usually constructor signature is '<init>(Ljava/lang/String;I)V'. // usually constructor signature is '<init>(Ljava/lang/String;I)V', sometimes one or both args can
// sometimes for one field enum second arg can be omitted // be omitted
if (co.getArgsCount() < 1) {
return null;
}
ClassNode cls = data.cls; ClassNode cls = data.cls;
ClassInfo clsInfo = co.getClassType(); ClassInfo clsInfo = co.getClassType();
ClassNode constrCls = cls.root().resolveClass(clsInfo); ClassNode constrCls = cls.root().resolveClass(clsInfo);
@@ -443,17 +466,45 @@ public class EnumVisitor extends AbstractVisitor {
if (ctrMth == null) { if (ctrMth == null) {
return null; return null;
} }
List<RegisterArg> regs = new ArrayList<>(); // usually constructor signature is '<init>(Ljava/lang/String;I)V'
co.getRegisterArgs(regs); // sometimes one or both args can be inlined or omitted
if (!regs.isEmpty()) { String nameStr = null;
ConstructorInsn replacedCo = inlineExternalRegs(data, co); if (co.getArgsCount() == 0) {
if (replacedCo == null) { ConstructorInsn ctrInsn = searchEnumSuperCtrInsn(ctrMth);
throw new JadxRuntimeException("Init of enum field '" + enumFieldNode.getName() + "' uses external variables"); if (ctrInsn != null && ctrInsn.getArgsCount() != 0) {
nameStr = getConstString(ctrMth.root(), ctrInsn.getArg(0));
}
} else {
nameStr = getConstString(cls.root(), co.getArg(0));
// verify and try to inline additional constructor args
List<RegisterArg> regs = new ArrayList<>();
co.getRegisterArgs(regs);
if (!regs.isEmpty()) {
ConstructorInsn replacedCo = inlineExternalRegs(data, co);
if (replacedCo == null) {
throw new JadxRuntimeException("Init of enum field '" + enumFieldNode.getName() + "' uses external variables");
}
data.toRemove.add(co);
co = replacedCo;
} }
data.toRemove.add(co);
co = replacedCo;
} }
return new EnumField(enumFieldNode, co); return new EnumField(enumFieldNode, co, nameStr);
}
private @Nullable ConstructorInsn searchEnumSuperCtrInsn(MethodNode ctrMth) {
for (BlockNode block : ctrMth.getBasicBlocks()) {
for (InsnNode insn : block.getInstructions()) {
if (insn.getType() == InsnType.CONSTRUCTOR) {
ConstructorInsn ctrCall = (ConstructorInsn) insn;
if (ctrCall.isSuper()
&& ctrCall.getArgsCount() != 0
&& ctrCall.getCallMth().getRawFullId().equals(ENUM_SUPER_CONSTRUCTOR_ID)) {
return ctrCall;
}
}
}
}
return null;
} }
private ConstructorInsn inlineExternalRegs(EnumData data, ConstructorInsn co) { private ConstructorInsn inlineExternalRegs(EnumData data, ConstructorInsn co) {
@@ -506,7 +557,18 @@ public class EnumVisitor extends AbstractVisitor {
} }
case FILLED_NEW_ARRAY: { case FILLED_NEW_ARRAY: {
// allow usage in values init instruction // allow usage in values init instruction
if (!data.valuesInitInsn.getArg(0).unwrap().equals(useInsn)) { InsnArg valuesArg = data.valuesInitInsn.getArg(0);
InsnNode unwrapped = valuesArg.unwrap();
if (unwrapped != null) {
if (unwrapped != useInsn) {
return null;
}
} else if (valuesArg.isRegister()) {
InsnNode valuesAssign = ((RegisterArg) valuesArg).getAssignInsn();
if (valuesAssign != useInsn) {
return null;
}
} else {
return null; return null;
} }
break; break;
@@ -549,10 +611,16 @@ public class EnumVisitor extends AbstractVisitor {
} }
String shortId = mi.getShortId(); String shortId = mi.getShortId();
if (mi.isConstructor()) { if (mi.isConstructor()) {
markArgsForSkip(mth);
// remove super constructor call
ConstructorInsn superCtrInsn = searchEnumSuperCtrInsn(mth);
if (superCtrInsn != null) {
superCtrInsn.add(AFlag.DONT_GENERATE);
InsnRemover.remove(mth, superCtrInsn);
}
if (isDefaultConstructor(mth, shortId)) { if (isDefaultConstructor(mth, shortId)) {
mth.add(AFlag.DONT_GENERATE); mth.add(AFlag.DONT_GENERATE);
} }
markArgsForSkip(mth);
} else if (mi.getShortId().equals(valuesMethodShortId)) { } else if (mi.getShortId().equals(valuesMethodShortId)) {
if (isValuesMethod(mth, clsType)) { if (isValuesMethod(mth, clsType)) {
valuesMethod = mth; valuesMethod = mth;
@@ -732,4 +800,9 @@ public class EnumVisitor extends AbstractVisitor {
this.staticBlocks = staticBlocks; this.staticBlocks = staticBlocks;
} }
} }
@Override
public String getName() {
return "EnumVisitor";
}
} }
@@ -158,7 +158,9 @@ public class InlineMethods extends AbstractVisitor {
} }
private void updateUsageInfo(MethodNode mth, MethodNode inlinedMth, InsnNode insn) { private void updateUsageInfo(MethodNode mth, MethodNode inlinedMth, InsnNode insn) {
inlinedMth.getUseIn().remove(mth); List<MethodNode> newUseIn = new ArrayList<>(inlinedMth.getUseIn());
newUseIn.remove(mth);
inlinedMth.setUseIn(newUseIn);
insn.visitInsns(innerInsn -> { insn.visitInsns(innerInsn -> {
// TODO: share code with UsageInfoVisitor // TODO: share code with UsageInfoVisitor
switch (innerInsn.getType()) { switch (innerInsn.getType()) {
@@ -167,7 +169,7 @@ public class InlineMethods extends AbstractVisitor {
MethodInfo callMth = ((BaseInvokeNode) innerInsn).getCallMth(); MethodInfo callMth = ((BaseInvokeNode) innerInsn).getCallMth();
MethodNode callMthNode = mth.root().resolveMethod(callMth); MethodNode callMthNode = mth.root().resolveMethod(callMth);
if (callMthNode != null) { if (callMthNode != null) {
callMthNode.setUseIn(ListUtils.safeReplace(callMthNode.getUseIn(), inlinedMth, mth)); callMthNode.setUseIn(ListUtils.safeReplace(new ArrayList<>(callMthNode.getUseIn()), inlinedMth, mth));
replaceClsUsage(mth, inlinedMth, callMthNode.getParentClass()); replaceClsUsage(mth, inlinedMth, callMthNode.getParentClass());
} }
break; break;
@@ -179,7 +181,7 @@ public class InlineMethods extends AbstractVisitor {
FieldInfo fieldInfo = (FieldInfo) ((IndexInsnNode) innerInsn).getIndex(); FieldInfo fieldInfo = (FieldInfo) ((IndexInsnNode) innerInsn).getIndex();
FieldNode fieldNode = mth.root().resolveField(fieldInfo); FieldNode fieldNode = mth.root().resolveField(fieldInfo);
if (fieldNode != null) { if (fieldNode != null) {
fieldNode.setUseIn(ListUtils.safeReplace(fieldNode.getUseIn(), inlinedMth, mth)); fieldNode.setUseIn(ListUtils.safeReplace(new ArrayList<>(fieldNode.getUseIn()), inlinedMth, mth));
replaceClsUsage(mth, inlinedMth, fieldNode.getParentClass()); replaceClsUsage(mth, inlinedMth, fieldNode.getParentClass());
} }
break; break;
@@ -114,7 +114,7 @@ public class MethodThrowsVisitor extends AbstractVisitor {
return; return;
} }
try { try {
blocks: for (final BlockNode block : mth.getBasicBlocks()) { blocks: for (BlockNode block : mth.getBasicBlocks()) {
// Skip e.g. throw instructions of synchronized regions // Skip e.g. throw instructions of synchronized regions
boolean skipExceptions = block.contains(AFlag.REMOVE) || block.contains(AFlag.DONT_GENERATE); boolean skipExceptions = block.contains(AFlag.REMOVE) || block.contains(AFlag.DONT_GENERATE);
Set<String> excludedExceptions = new HashSet<>(); Set<String> excludedExceptions = new HashSet<>();
@@ -127,7 +127,7 @@ public class MethodThrowsVisitor extends AbstractVisitor {
excludedExceptions.add(handler.getArgType().toString()); excludedExceptions.add(handler.getArgType().toString());
} }
} }
for (final InsnNode insn : block.getInstructions()) { for (InsnNode insn : block.getInstructions()) {
checkInsn(mth, insn, excludedExceptions, skipExceptions); checkInsn(mth, insn, excludedExceptions, skipExceptions);
} }
} }
@@ -85,7 +85,6 @@ public class SimplifyVisitor extends AbstractVisitor {
int insnCount = list.size(); int insnCount = list.size();
InsnNode modInsn = simplifyInsn(mth, insn, null); InsnNode modInsn = simplifyInsn(mth, insn, null);
if (modInsn != null) { if (modInsn != null) {
modInsn.rebindArgs();
if (i < list.size() && list.get(i) == insn) { if (i < list.size() && list.get(i) == insn) {
list.set(i, modInsn); list.set(i, modInsn);
} else { } else {
@@ -95,6 +94,8 @@ public class SimplifyVisitor extends AbstractVisitor {
} }
list.set(idx, modInsn); list.set(idx, modInsn);
} }
InsnRemover.unbindInsn(mth, insn);
modInsn.rebindArgs();
if (list.size() < insnCount) { if (list.size() < insnCount) {
// some insns removed => restart block processing // some insns removed => restart block processing
simplifyBlock(mth, block); simplifyBlock(mth, block);
@@ -113,8 +114,7 @@ public class SimplifyVisitor extends AbstractVisitor {
InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn(); InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn();
InsnNode replaceInsn = simplifyInsn(mth, wrapInsn, insn); InsnNode replaceInsn = simplifyInsn(mth, wrapInsn, insn);
if (replaceInsn != null) { if (replaceInsn != null) {
arg.wrapInstruction(mth, replaceInsn, false); arg.wrapInstruction(mth, replaceInsn);
InsnRemover.unbindInsn(mth, wrapInsn);
changed = true; changed = true;
} }
} }
@@ -239,8 +239,8 @@ public class SimplifyVisitor extends AbstractVisitor {
|| shadowedByOuterCast(mth.root(), castToType, parentInsn)) { || shadowedByOuterCast(mth.root(), castToType, parentInsn)) {
InsnNode insnNode = new InsnNode(InsnType.MOVE, 1); InsnNode insnNode = new InsnNode(InsnType.MOVE, 1);
insnNode.setOffset(castInsn.getOffset()); insnNode.setOffset(castInsn.getOffset());
insnNode.setResult(castInsn.getResult()); insnNode.setResult(InsnNode.duplicateArg(castInsn.getResult()));
insnNode.addArg(castArg); insnNode.addArg(castArg.duplicate());
return insnNode; return insnNode;
} }
return null; return null;
@@ -576,7 +576,11 @@ public class SimplifyVisitor extends AbstractVisitor {
if (litArg.isNegative()) { if (litArg.isNegative()) {
LiteralArg negLitArg = litArg.negate(); LiteralArg negLitArg = litArg.negate();
if (negLitArg != null) { if (negLitArg != null) {
return new ArithNode(ArithOp.SUB, arith.getResult(), arith.getArg(0), negLitArg); RegisterArg resArg = InsnNode.duplicateArg(arith.getResult());
ArithNode newInsn = new ArithNode(ArithOp.SUB, resArg, arith.getArg(0).duplicate(), negLitArg);
newInsn.copyAttributesFrom(arith);
newInsn.setOffset(arith.getOffset());
return newInsn;
} }
} }
break; break;
@@ -586,10 +590,12 @@ public class SimplifyVisitor extends AbstractVisitor {
InsnArg firstArg = arith.getArg(0); InsnArg firstArg = arith.getArg(0);
long lit = litArg.getLiteral(); long lit = litArg.getLiteral();
if (firstArg.getType() == ArgType.BOOLEAN && (lit == 0 || lit == 1)) { if (firstArg.getType() == ArgType.BOOLEAN && (lit == 0 || lit == 1)) {
InsnNode node = new InsnNode(lit == 0 ? InsnType.MOVE : InsnType.NOT, 1); InsnNode newInsn = new InsnNode(lit == 0 ? InsnType.MOVE : InsnType.NOT, 1);
node.setResult(arith.getResult()); newInsn.setResult(InsnNode.duplicateArg(arith.getResult()));
node.addArg(firstArg); newInsn.addArg(firstArg.duplicate());
return node; newInsn.copyAttributesFrom(arith);
newInsn.setOffset(arith.getOffset());
return newInsn;
} }
break; break;
} }
@@ -637,16 +643,22 @@ public class SimplifyVisitor extends AbstractVisitor {
} }
if (wrapType == InsnType.ARITH) { if (wrapType == InsnType.ARITH) {
ArithNode ar = (ArithNode) wrap; ArithNode ar = (ArithNode) wrap;
return ArithNode.oneArgOp(ar.getOp(), fArg, ar.getArg(1)); ArithNode newInsn = ArithNode.oneArgOp(ar.getOp(), fArg, ar.getArg(1).duplicate());
newInsn.copyAttributesFrom(insn);
newInsn.setOffset(insn.getOffset());
return newInsn;
} }
int argsCount = wrap.getArgsCount(); int argsCount = wrap.getArgsCount();
InsnNode concat = new InsnNode(InsnType.STR_CONCAT, argsCount - 1); InsnNode concat = new InsnNode(InsnType.STR_CONCAT, argsCount - 1);
for (int i = 1; i < argsCount; i++) { for (int i = 1; i < argsCount; i++) {
concat.addArg(wrap.getArg(i)); concat.addArg(wrap.getArg(i).duplicate());
} }
InsnArg concatArg = InsnArg.wrapArg(concat); InsnArg concatArg = InsnArg.wrapArg(concat);
concatArg.setType(ArgType.STRING); concatArg.setType(ArgType.STRING);
return ArithNode.oneArgOp(ArithOp.ADD, fArg, concatArg); ArithNode newInsn = ArithNode.oneArgOp(ArithOp.ADD, fArg, concatArg);
newInsn.copyAttributesFrom(wrap);
newInsn.setOffset(wrap.getOffset());
return newInsn;
} catch (Exception e) { } catch (Exception e) {
LOG.debug("Can't convert field arith insn: {}, mth: {}", insn, mth, e); LOG.debug("Can't convert field arith insn: {}, mth: {}", insn, mth, e);
} }
@@ -23,6 +23,7 @@ import jadx.api.plugins.utils.Utils;
import jadx.core.Consts; import jadx.core.Consts;
import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.ExcSplitCrossAttr;
import jadx.core.dex.attributes.nodes.TmpEdgeAttr; import jadx.core.dex.attributes.nodes.TmpEdgeAttr;
import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.InsnType;
@@ -380,6 +381,23 @@ public class BlockExceptionHandler {
} }
connectSplittersAndHandlers(tryCatchBlock, topSplitterBlock, bottomSplitterBlock); connectSplittersAndHandlers(tryCatchBlock, topSplitterBlock, bottomSplitterBlock);
// At this point, it's possible that a cross edge to the original bottom has been turned into a back
// edge by the insertion of the new bottom. This causes problems because back edges usually signifiy
// loops, but this is not a loop. To fix this, predecessors of the bottom that also have a path from
// the bottom are rewritten to point to the original path crossing point (before synthetic blocks).
if (bottom != null && bottom.contains(AType.EXC_SPLIT_CROSS)) {
List<BlockNode> convertBlocks = new ArrayList<>();
for (BlockNode b : bottom.getPredecessors()) {
if (BlockUtils.isAnyPathExists(bottom, b)) {
convertBlocks.add(b);
}
}
for (BlockNode b : convertBlocks) {
// The connection can't be replaced during the first loop because it would modify the preds list.
BlockSplitter.replaceConnection(b, bottom, bottom.get(AType.EXC_SPLIT_CROSS).getOriginalPathCross());
}
}
for (BlockNode block : blocks) { for (BlockNode block : blocks) {
TryCatchBlockAttr currentTCBAttr = block.get(AType.TRY_BLOCK); TryCatchBlockAttr currentTCBAttr = block.get(AType.TRY_BLOCK);
if (currentTCBAttr == null || currentTCBAttr.getInnerTryBlocks().contains(tryCatchBlock)) { if (currentTCBAttr == null || currentTCBAttr.getInnerTryBlocks().contains(tryCatchBlock)) {
@@ -460,12 +478,15 @@ public class BlockExceptionHandler {
List<BlockNode> outsidePredecessors = preds.stream() List<BlockNode> outsidePredecessors = preds.stream()
.filter(p -> !BlockUtils.atLeastOnePathExists(blocks, p)) .filter(p -> !BlockUtils.atLeastOnePathExists(blocks, p))
.collect(Collectors.toList()); .collect(Collectors.toList());
if (outsidePredecessors.isEmpty()) { // if we have no predecessors or every predecessor is outside (which would mean that inserting the
// new synthetic block does nothing), just return the existing path cross instead.
if (outsidePredecessors.isEmpty() || outsidePredecessors.size() == pathCross.getPredecessors().size()) {
return pathCross; return pathCross;
} }
// some predecessors outside of input set paths -> split block only for input set // some predecessors outside of input set paths -> split block only for input set
BlockNode splitCross = BlockSplitter.blockSplitTop(mth, pathCross); BlockNode splitCross = BlockSplitter.blockSplitTop(mth, pathCross);
splitCross.add(AFlag.SYNTHETIC); splitCross.add(AFlag.SYNTHETIC);
splitCross.addAttr(new ExcSplitCrossAttr(pathCross));
for (BlockNode outsidePredecessor : outsidePredecessors) { for (BlockNode outsidePredecessor : outsidePredecessors) {
// return predecessors to split bottom block (original) // return predecessors to split bottom block (original)
BlockSplitter.replaceConnection(outsidePredecessor, splitCross, pathCross); BlockSplitter.replaceConnection(outsidePredecessor, splitCross, pathCross);
@@ -127,13 +127,55 @@ public class BlockProcessor extends AbstractVisitor {
} }
private static void checkForUnreachableBlocks(MethodNode mth) { private static void checkForUnreachableBlocks(MethodNode mth) {
for (BlockNode block : mth.getBasicBlocks()) { while (true) {
if (block.getPredecessors().isEmpty() && block != mth.getEnterBlock()) { boolean fixed = false;
throw new JadxRuntimeException("Unreachable block: " + block); for (BlockNode block : mth.getBasicBlocks()) {
if (block.getPredecessors().isEmpty() && block != mth.getEnterBlock()) {
// Sometimes a split cross block will have all it's predecessors moved elsewhere after it's been
// created. This is usually detected at the time of it's creation, but in certain edge cases it
// is difficult to do so. In those cases it will be cleanly removed here, along with the associated
// bottom splitter.
if (block.contains(AType.EXC_SPLIT_CROSS) && fixUnreachableSplitCross(mth, block)) {
mth.addInfoComment("Removed unreachable split cross block " + block);
fixed = true;
break;
}
throw new JadxRuntimeException("Unreachable block: " + block);
}
}
if (!fixed) {
break;
} }
} }
} }
/**
* Attempts to remove an unreachable synthetic split cross block that has been added previously,
* along with the associated bottom splitter.
*
* @param mth the method containing the unreachable block
* @param splitCross the unreachable block
* @return true if the operation was successful, false if a precondition was not satisfied and no
* changes were made.
*/
private static boolean fixUnreachableSplitCross(MethodNode mth, BlockNode splitCross) {
BlockNode bottomSplitter = null;
for (BlockNode succ : splitCross.getSuccessors()) {
if (succ.contains(AFlag.EXC_BOTTOM_SPLITTER)) {
bottomSplitter = succ;
break;
}
}
if (bottomSplitter == null || bottomSplitter.getPredecessors().size() != 1) {
return false;
}
Set<BlockNode> removeSet = new HashSet<>();
removeSet.add(bottomSplitter);
removeSet.add(splitCross);
removeFromMethod(removeSet, mth);
return true;
}
private static boolean deduplicateBlockInsns(MethodNode mth, BlockNode block) { private static boolean deduplicateBlockInsns(MethodNode mth, BlockNode block) {
if (block.contains(AFlag.LOOP_START) || block.contains(AFlag.LOOP_END)) { if (block.contains(AFlag.LOOP_START) || block.contains(AFlag.LOOP_END)) {
// search for same instruction at end of all predecessors blocks // search for same instruction at end of all predecessors blocks
@@ -179,7 +179,7 @@ public class BlockSplitter extends AbstractVisitor {
replaceTarget(source, oldDest, newDest); replaceTarget(source, oldDest, newDest);
} }
static BlockNode insertBlockBetween(MethodNode mth, BlockNode source, BlockNode target) { public static BlockNode insertBlockBetween(MethodNode mth, BlockNode source, BlockNode target) {
BlockNode newBlock = startNewBlock(mth, target.getStartOffset()); BlockNode newBlock = startNewBlock(mth, target.getStartOffset());
newBlock.add(AFlag.SYNTHETIC); newBlock.add(AFlag.SYNTHETIC);
removeConnection(source, target); removeConnection(source, target);
@@ -0,0 +1,153 @@
package jadx.core.dex.visitors.finaly;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.nodes.InsnNode;
/**
* A centrality state is an object which helps track how instructions can be skipped.
* When looking for a finally, one of the things we have to do is make sure that instructions
* are not part of the return and are actually part of the "finally" block.
* This object helps keep track of registers, instructions etc to see if instructions can be
* skipped.
*/
public final class CentralityState {
private final Set<RegisterArg> allowableOutputArguments = new HashSet<>();
private final SameInstructionsStrategy sameInstructionsStrategy;
private boolean allowsCentral = true;
private boolean allowsNonStartingNode;
public CentralityState(SameInstructionsStrategy sameInstructionsStrategy, boolean allowsNonStartingNode) {
this.sameInstructionsStrategy = sameInstructionsStrategy;
this.allowsNonStartingNode = allowsNonStartingNode;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("CentralityState - ");
if (allowsCentral) {
sb.append("allows central");
} else {
sb.append("disallows central");
}
sb.append(" | ");
for (RegisterArg registerArg : allowableOutputArguments) {
sb.append(registerArg.getName());
sb.append(" ");
}
return sb.toString();
}
public SameInstructionsStrategy getSameInstructionsStrategy() {
return sameInstructionsStrategy;
}
public boolean getAllowsCentral() {
return allowsCentral;
}
public void setAllowsCentral(boolean allowsCentral) {
this.allowsCentral = allowsCentral;
}
public boolean getAllowsNonStartingNode() {
return allowsNonStartingNode;
}
public void setAllowsNonStartingNode(boolean allowsNonStartingNode) {
this.allowsNonStartingNode = allowsNonStartingNode;
}
public void addAllowableOutput(RegisterArg allowableOutput) {
allowableOutputArguments.add(allowableOutput);
}
public void addAllowableOutputs(Collection<RegisterArg> allowableOutputs) {
allowableOutputArguments.addAll(allowableOutputs);
}
/**
* Adds all inputs register arguments from an instruction as allowable output arguments.
*
* @param allowableOutputInsn The instruction to retrieve the list of inputs from.
*/
public void addAllowableOutputs(InsnNode allowableOutputInsn) {
List<RegisterArg> registerArgs = new LinkedList<>();
for (InsnArg arg : allowableOutputInsn.getArgList()) {
if (!(arg instanceof RegisterArg)) {
continue;
}
registerArgs.add((RegisterArg) arg);
}
registerArgs.forEach(this::addAllowableOutput);
}
public boolean hasAllowableOutput(InsnNode insn) {
if (allowableOutputArguments.isEmpty()) {
return false;
}
RegisterArg registerArg;
if (insn.getResult() != null) {
registerArg = insn.getResult();
} else {
registerArg = null;
}
if (registerArg == null) {
return false;
}
for (RegisterArg allowableOutput : allowableOutputArguments) {
if (allowableOutput.equals(registerArg)) {
return true;
}
}
return false;
}
@SuppressWarnings("unused")
public boolean hasAllowableInputs(InsnNode insn) {
if (allowableOutputArguments.isEmpty()) {
return false;
}
List<RegisterArg> registerArgs = new ArrayList<>();
for (InsnArg arg : insn.getArgList()) {
if (arg instanceof RegisterArg) {
registerArgs.add((RegisterArg) arg);
}
}
if (registerArgs.isEmpty() || allowableOutputArguments.isEmpty()) {
return false;
}
for (RegisterArg regArg : registerArgs) {
boolean foundMatch = false;
for (RegisterArg allowableOutput : allowableOutputArguments) {
if (regArg.equals(allowableOutput)) {
foundMatch = true;
break;
}
}
if (!foundMatch) {
return false;
}
}
return true;
}
public CentralityState duplicate() {
CentralityState state = new CentralityState(sameInstructionsStrategy, allowsNonStartingNode);
state.allowsCentral = allowsCentral;
state.allowableOutputArguments.addAll(allowableOutputArguments);
return state;
}
public Set<RegisterArg> getAllowableOutputArguments() {
return allowableOutputArguments;
}
}
@@ -1,90 +0,0 @@
package jadx.core.dex.visitors.finaly;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
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.dex.trycatch.ExceptionHandler;
import jadx.core.utils.Utils;
public class FinallyExtractInfo {
private final MethodNode mth;
private final ExceptionHandler finallyHandler;
private final List<BlockNode> allHandlerBlocks;
private final List<InsnsSlice> duplicateSlices = new ArrayList<>();
private final Set<BlockNode> checkedBlocks = new HashSet<>();
private final InsnsSlice finallyInsnsSlice = new InsnsSlice();
private final BlockNode startBlock;
private InsnsSlice curDupSlice;
private List<InsnNode> curDupInsns;
private int curDupInsnsOffset;
public FinallyExtractInfo(MethodNode mth, ExceptionHandler finallyHandler, BlockNode startBlock, List<BlockNode> allHandlerBlocks) {
this.mth = mth;
this.finallyHandler = finallyHandler;
this.startBlock = startBlock;
this.allHandlerBlocks = allHandlerBlocks;
}
public MethodNode getMth() {
return mth;
}
public ExceptionHandler getFinallyHandler() {
return finallyHandler;
}
public List<BlockNode> getAllHandlerBlocks() {
return allHandlerBlocks;
}
public InsnsSlice getFinallyInsnsSlice() {
return finallyInsnsSlice;
}
public List<InsnsSlice> getDuplicateSlices() {
return duplicateSlices;
}
public Set<BlockNode> getCheckedBlocks() {
return checkedBlocks;
}
public BlockNode getStartBlock() {
return startBlock;
}
public InsnsSlice getCurDupSlice() {
return curDupSlice;
}
public void setCurDupSlice(InsnsSlice curDupSlice) {
this.curDupSlice = curDupSlice;
}
public List<InsnNode> getCurDupInsns() {
return curDupInsns;
}
public int getCurDupInsnsOffset() {
return curDupInsnsOffset;
}
public void setCurDupInsns(List<InsnNode> insns, int offset) {
this.curDupInsns = insns;
this.curDupInsnsOffset = offset;
}
@Override
public String toString() {
return "FinallyExtractInfo{"
+ "\n finally:\n " + finallyInsnsSlice
+ "\n dups:\n " + Utils.listToString(duplicateSlices, "\n ")
+ "\n}";
}
}
@@ -1,79 +0,0 @@
package jadx.core.dex.visitors.finaly;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.jetbrains.annotations.Nullable;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.InsnNode;
public class InsnsSlice {
private final List<InsnNode> insnsList = new ArrayList<>();
private final Map<InsnNode, BlockNode> insnMap = new IdentityHashMap<>();
private boolean complete;
public void addInsn(InsnNode insn, BlockNode block) {
insnsList.add(insn);
insnMap.put(insn, block);
}
public void addBlock(BlockNode block) {
for (InsnNode insn : block.getInstructions()) {
addInsn(insn, block);
}
}
public void addInsns(BlockNode block, int startIndex, int endIndex) {
List<InsnNode> insns = block.getInstructions();
for (int i = startIndex; i < endIndex; i++) {
addInsn(insns.get(i), block);
}
}
@Nullable
public BlockNode getBlock(InsnNode insn) {
return insnMap.get(insn);
}
public List<InsnNode> getInsnsList() {
return insnsList;
}
public Set<BlockNode> getBlocks() {
Set<BlockNode> set = new LinkedHashSet<>();
for (InsnNode insn : insnsList) {
set.add(insnMap.get(insn));
}
return set;
}
public void resetIncomplete() {
if (!complete) {
insnsList.clear();
insnMap.clear();
}
}
public boolean isComplete() {
return complete;
}
public void setComplete(boolean complete) {
this.complete = complete;
}
@Override
public String toString() {
return "{["
+ insnsList.stream().map(insn -> insn.getType().toString()).collect(Collectors.joining(", "))
+ ']'
+ (complete ? " complete" : "")
+ '}';
}
}

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