Compare commits

...

67 Commits

Author SHA1 Message Date
Skylot 87e0e5bf16 fix: correct inline/merge with overriden bridge method (#1580) 2022-07-20 14:49:37 +01:00
Skylot e4c2d6cf6e fix(gui): use editor font for usage label 2022-07-20 14:36:51 +01:00
Skylot fb0bdb5112 fix(gui): allow to use empty name to reset rename 2022-07-20 14:35:53 +01:00
Skylot f4b3645435 fix: ignore anonymous classes in enclosing node search (#1580) 2022-07-19 19:25:17 +01:00
Skylot c27f2badf7 fix(gui): resolve payload offset for switch insns in debug smali code (#1575) 2022-07-18 18:50:48 +01:00
Skylot 1a877d6535 fix: resolve possible decompilation double execution 2022-07-16 22:29:59 +03:00
Skylot 5ada9331b6 chore: update dependencies 2022-07-14 14:33:04 +01:00
Skylot a0f4ccb7a4 fix: update deps and fix proto resource loading (AAB) (#1129) 2022-07-14 14:33:04 +01:00
Skylot 5b5524a7dd fix: handle parent of inlined/moved classes (#1578) 2022-07-14 14:40:13 +03:00
Jan S 3cc464c9c9 fix: IndexOutOfBoundsException in JumpManager (#1576) (PR #1577) 2022-07-13 17:24:20 +01:00
Skylot 51555667cf fix: add more checks before remove or rename enum methods (#1572) 2022-07-07 16:55:32 +01:00
Skylot e01ea7010f fix: save classes with code generation error into cache (#1568) 2022-07-03 19:32:41 +01:00
Skylot 77732c83c9 fix(gui): ignore/limit waiting of canceled search task (#1568) 2022-07-01 17:57:59 +01:00
Skylot a67fc83949 fix: better dominators algorithms 2022-07-01 17:26:54 +01:00
Skylot 3d920725aa fix: check synthetic methods before remove/inline (#1560) 2022-06-29 19:19:54 +01:00
Skylot 2f2fbea558 fix(gui): check user renames (#1557) 2022-06-29 16:21:49 +01:00
Skylot e7a86a2960 fix(gui): forbid rename method args in fallback mode (#1558) 2022-06-29 15:25:23 +01:00
Skylot b282d97ffe fix(gui): set current dir directly in file chooser constructor (#1553) 2022-06-28 16:57:57 +01:00
Skylot e4ca52a95f chore: update dependencies 2022-06-28 16:23:31 +01:00
Skylot d972d9ec74 fix(gui): ignore errors on code area dispose (#1545) 2022-06-28 16:20:31 +01:00
Jan S 0721a6b050 fix(gui): QuarkReport data validation added and other minor improvements (PR #1556)
* QuarkReport: data validation added and other minor improvements
* checkStyle
2022-06-25 22:24:53 +03:00
zhongqingsong 762ee6550e fix(gui):complete Chinese translation (PR #1549)
1、Complete Chinese translation
2、Previous word polish
2022-06-25 22:15:22 +03:00
Skylot 18070eb7a6 fix(gui): allow to select file on mapping export 2022-06-20 14:19:59 +01:00
Skylot 8486891728 fix(gui): resolve various minor issues 2022-06-20 13:17:50 +01:00
Skylot 4679172d4f fix(gui): correct set highlighted text in search (#1507) 2022-06-20 13:16:42 +01:00
Skylot 92a6c333d8 fix(gui): force jadx new version check by default 2022-06-20 12:55:15 +01:00
Skylot 358adbdd65 feat(gui): allow to disable jump on double click (#1540) 2022-06-19 17:19:08 +01:00
Skylot 65f7c80222 feat(gui): add reload and live reload actions (#1537) 2022-06-18 20:20:11 +01:00
Skylot d2e6bb236e fix: use wide move for long/double store/load java opcodes 2022-06-16 16:26:14 +01:00
Skylot eaeb114258 fix: check class name collisions (#1526) 2022-06-15 18:43:33 +01:00
Skylot 1533b7fe6e fix: keep types on duplicate cast remove (#1527) 2022-06-12 21:55:12 +01:00
Julian Burner a2cd8e1ead feat(gui): support export to deobfuscation mapping file formats (#1491)(PR #1505)
* Add option to export mappings as Tiny v2 file

* Comply with JADX's import order conventions

* Only use Java 8 features

* Only use Java 8 features (2)

* Export comments to mappings file

* Method args test (doesn't work)

* Make method arg mapping exports work now

* Use `getTopParentClass()` instead of `getParentClass()`

See https://github.com/skylot/jadx/pull/1505#issuecomment-1145064865

* Remove unneeded method load call

* Small code cleanup; initial (broken) support for method vars

* Fixes regarding inner classes

* Add option to export mappings as Enigma directory

* Add option to export mappings as Enigma file/directory

Temporarily move to my mapping-io fork until this PR gets merged: https://github.com/FabricMC/mapping-io/pull/19

* Fix method vars' lv-indices

* Use correct offset value for method var mappings

* Also supply lvt-index for method var mappings

* Clarify why we're using local mapping-io fork; comment out Fabric Maven for now

* Remove unnecessary `public` modifier

* Make an `if` condition less complicated

* Move mapping export code into jadx-gui (for now)

* Make mapping export async; make export menu only clickable when everything is loaded

* Fix export mappings menu field declaration position
2022-06-11 20:19:08 +01:00
Christian Jones 4edb512121 fix(cli): allow decoding resource-only APKs (#1517)(PR #1530)
* Process resource-only inputs
* Fix error, add testing
2022-06-11 15:40:39 +01:00
Skylot 702b88228c fix(gui): resolve popup menu action run (#1514, #1529) 2022-06-11 15:08:28 +01:00
Skylot 14fd88b2f8 fix(gui): add volatile and update sync for decompiler field in wrapper (#1518) 2022-06-08 21:06:57 +01:00
Skylot 20657e8bb5 doc(cli): improve plugins section formatting 2022-06-06 19:55:58 +01:00
Skylot 93d3194e3b doc: remove incorrect tokei badge 2022-06-06 19:54:15 +01:00
Skylot 39331d9120 fix: remove deprecated --deobf-rewrite-cfg (#1513) 2022-06-06 19:53:47 +01:00
Skylot b4fa6644bc doc: add more badges 2022-06-05 16:06:19 +01:00
Skylot 0b2e2ed034 fix: improve class search for super call (#1512) 2022-06-05 14:49:34 +01:00
Skylot 81231206f3 fix(gui): reset disk cache on new jadx version 2022-06-04 23:26:25 +01:00
Skylot 49d0e76272 fix: support all-catch in multi-catch (#1510) 2022-06-04 23:25:52 +01:00
CmP-lt 0809993b37 fix(gui): improve restoration of windows saved state (PR #1511)
* Fix restoration of windows saved state
* Don't skip restoration of window saved bounds when they intersect with screen bounds.
* Restore saved bounds of main window regardless of it's saved extended state (fixes divider location of main split pane being restored incorrectly when saved state of main window is maximized).
* Add handling for out-of-screen(s) window bounds
2022-06-04 17:41:00 +01:00
Skylot 0c3afcc24c fix(gui): try to prevent jadx node leaks in UI objects 2022-06-03 16:15:14 +01:00
Skylot d6c851eed4 test: fix method code extract 2022-06-02 19:33:16 +01:00
Skylot dcf4a7c4e3 fix(gui): try to resolve some causes of memory leak 2022-06-01 19:48:51 +01:00
Skylot 9ba07b986b fix(gui): reduce usage of nullable decompiler field in jadx wrapper (#1506) 2022-06-01 16:36:30 +01:00
Skylot e6b6b93cbb fix: improve blocks tree compare for finally extract (#1501) 2022-05-31 20:57:34 +01:00
Skylot fcd58ae76f fix: remap class names for store in disk cache (#1503) 2022-05-30 18:16:05 +01:00
Skylot df380dea27 chore: update dependencies 2022-05-30 20:06:23 +03:00
CKCat 9d88592391 fix(res): ignore version in AndroidManifest.xml (#1502)(PR #1504) 2022-05-30 14:10:10 +01:00
dependabot[bot] c906c11b0f build(deps): bump github/codeql-action from 1 to 2 (PR #1500)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 1 to 2.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v1...v2)

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

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-28 18:04:08 +01:00
Skylot 4fbc56cdb0 build: add unstable win-with-jre bundle 2022-05-28 17:46:48 +01:00
Skylot 98c0416b20 fix(gui): correct close and reopen for decompiler and cache 2022-05-28 16:41:37 +03:00
ZachQin fa41874e30 feat(gui): add parameters logging in Frida code snippet (#1497)(PR #1498) 2022-05-28 14:39:02 +01:00
Skylot 2aa6c99c90 fix: skip dex files with parsing errors (#1495) 2022-05-28 13:27:29 +01:00
Skylot 5f60c0f1bb build: fix google-java-format for all java versions 2022-05-27 22:51:52 +01:00
Skylot cb741db623 fix: improve usage search, refactor java nodes creation (#1489) 2022-05-27 17:56:08 +01:00
Skylot 1df217c4a0 fix: save cache dir for reuse on project save/reopen 2022-05-27 16:50:13 +01:00
Skylot 81f209ba9e fix: check if directory exists before delete (#1493) 2022-05-26 19:23:59 +03:00
zhongqingsong 34a31aa7df fix(gui): complete Chinese Translation (PR #1492)
1. Complete translation of Chinese
2. Polish up part of the translation
3. Restore all text for subsequent translation
2022-05-24 19:09:11 +01:00
Skylot 5099e02c9b fix(gui): correct merge for plugin options from command line (#1490) 2022-05-23 19:44:49 +01:00
Skylot f364b39b29 fix(gui): save full type info in metadata (#1487) 2022-05-22 16:18:38 +01:00
Skylot 4cd4746f9a fix(gui): save variable name to show in tooltip (#1487) 2022-05-22 15:34:09 +01:00
Skylot 6448f0e32b fix: use variable length encoding instead short for offsets (can overflow) (#1489) 2022-05-22 14:23:15 +01:00
Skylot e07332d49a fix(gui): resolve cast exception for variable reference (#1489) 2022-05-21 21:33:58 +01:00
Skylot bd8a44c4c9 fix(gui): correct handle of selected file in save dialog 2022-05-21 21:31:47 +01:00
169 changed files with 4110 additions and 1371 deletions
+7
View File
@@ -0,0 +1,7 @@
version: 2
updates:
# Set update schedule for GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
+86
View File
@@ -0,0 +1,86 @@
name: Build Artifacts
on:
push:
branches: [ master, build-test ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up JDK
uses: actions/setup-java@v3
with:
distribution: 'adopt'
java-version: 8
- name: Set jadx version
run: |
JADX_LAST_TAG=$(git describe --abbrev=0 --tags)
JADX_VERSION="${JADX_LAST_TAG:1}.$GITHUB_RUN_NUMBER-${GITHUB_SHA:0:8}"
echo "JADX_VERSION=$JADX_VERSION" >> $GITHUB_ENV
- uses: burrunan/gradle-cache-action@v1
name: Build with Gradle
env:
TERM: dumb
with:
arguments: clean dist copyExe
- name: Save bundle artifact
uses: actions/upload-artifact@v3
with:
name: ${{ format('jadx-{0}', env.JADX_VERSION) }}
# Waiting fix for https://github.com/actions/upload-artifact/issues/39 to upload zip file
# Upload unpacked files for now
path: build/jadx/**/*
if-no-files-found: error
retention-days: 30
- name: Save exe artifact
uses: actions/upload-artifact@v3
with:
name: ${{ format('jadx-gui-{0}-no-jre-win.exe', env.JADX_VERSION) }}
path: build/*.exe
if-no-files-found: error
retention-days: 30
build-win-bundle:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up JDK
uses: oracle-actions/setup-java@v1 # set latest java version by default
- name: Print Java version
shell: bash
run: java -version
- name: Set jadx version
shell: bash
run: |
JADX_LAST_TAG=$(git describe --abbrev=0 --tags)
JADX_VERSION="${JADX_LAST_TAG:1}.$GITHUB_RUN_NUMBER-${GITHUB_SHA:0:8}"
echo "JADX_VERSION=$JADX_VERSION" >> $GITHUB_ENV
- uses: gradle/gradle-build-action@v2
name: Build with Gradle
env:
TERM: dumb
with:
arguments: clean dist -PbundleJRE=true
- name: Save exe bundle artifact
uses: actions/upload-artifact@v3
with:
name: ${{ format('jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }}
path: jadx-gui/build/*-with-jre-win/*
if-no-files-found: error
retention-days: 30
+28
View File
@@ -0,0 +1,28 @@
name: Build Test
on:
push:
branches: [ master, build-test ]
pull_request:
branches: [ master ]
jobs:
tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up JDK
uses: actions/setup-java@v3
with:
distribution: 'adopt'
java-version: 8
- uses: burrunan/gradle-cache-action@v1
name: Build with Gradle
env:
TERM: dumb
with:
arguments: clean build dist copyExe --warning-mode=all
-52
View File
@@ -1,52 +0,0 @@
name: Build
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set up JDK
uses: actions/setup-java@v1
with:
java-version: 8
- name: Set jadx version
run: |
JADX_LAST_TAG=$(git describe --abbrev=0 --tags)
JADX_VERSION="${JADX_LAST_TAG:1}.$GITHUB_RUN_NUMBER-${GITHUB_SHA:0:8}"
echo "JADX_VERSION=$JADX_VERSION" >> $GITHUB_ENV
- uses: burrunan/gradle-cache-action@v1
name: Build with Gradle
env:
TERM: dumb
TEST_INPUT_PLUGIN: dx
with:
arguments: clean build dist copyExe --warning-mode=all
- name: Save bundle artifact
if: success() && github.event_name == 'push'
uses: actions/upload-artifact@v2
with:
name: ${{ format('jadx-{0}', env.JADX_VERSION) }}
# Waiting fix for https://github.com/actions/upload-artifact/issues/39 to upload zip file
# Upload unpacked files for now
path: build/jadx/**/*
if-no-files-found: error
- name: Save exe artifact
if: success() && github.event_name == 'push'
uses: actions/upload-artifact@v2
with:
name: ${{ format('jadx-gui-{0}-no-jre-win.exe', env.JADX_VERSION) }}
path: build/*.exe
if-no-files-found: error
+2 -2
View File
@@ -28,7 +28,7 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v1 uses: github/codeql-action/init@v2
with: with:
queries: +security-extended queries: +security-extended
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
@@ -38,4 +38,4 @@ jobs:
./gradlew clean build -x checkstyleTest -x checkstyleMain -x test -x ':jadx-core:testClasses' ./gradlew clean build -x checkstyleTest -x checkstyleMain -x test -x ':jadx-core:testClasses'
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1 uses: github/codeql-action/analyze@v2
+20 -14
View File
@@ -3,8 +3,10 @@
## JADX ## JADX
[![Build status](https://github.com/skylot/jadx/workflows/Build/badge.svg)](https://github.com/skylot/jadx/actions?query=workflow%3ABuild) [![Build status](https://github.com/skylot/jadx/workflows/Build/badge.svg)](https://github.com/skylot/jadx/actions?query=workflow%3ABuild)
[![Alerts from lgtm.com](https://img.shields.io/lgtm/alerts/g/skylot/jadx.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/skylot/jadx/alerts/) ![GitHub contributors](https://img.shields.io/github/contributors/skylot/jadx)
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) ![GitHub all releases](https://img.shields.io/github/downloads/skylot/jadx/total)
![GitHub release (latest by SemVer)](https://img.shields.io/github/downloads/skylot/jadx/latest/total)
![Latest release](https://img.shields.io/github/release/skylot/jadx.svg)
[![Maven Central](https://img.shields.io/maven-central/v/io.github.skylot/jadx-core)](https://search.maven.org/search?q=g:io.github.skylot%20AND%20jadx) [![Maven Central](https://img.shields.io/maven-central/v/io.github.skylot/jadx-core)](https://search.maven.org/search?q=g:io.github.skylot%20AND%20jadx)
[![License](http://img.shields.io/:license-apache-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0.html) [![License](http://img.shields.io/:license-apache-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0.html)
@@ -33,8 +35,9 @@ See these features in action here: [jadx-gui features overview](https://github.c
<img src="https://user-images.githubusercontent.com/118523/142730720-839f017e-38db-423e-b53f-39f5f0a0316f.png" width="700"/> <img src="https://user-images.githubusercontent.com/118523/142730720-839f017e-38db-423e-b53f-39f5f0a0316f.png" width="700"/>
### Download ### Download
- release from [github: ![Latest release](https://img.shields.io/github/release/skylot/jadx.svg)](https://github.com/skylot/jadx/releases/latest) - release
- latest [unstable build](https://nightly.link/skylot/jadx/workflows/build/master) from [github: ![Latest release](https://img.shields.io/github/release/skylot/jadx.svg)](https://github.com/skylot/jadx/releases/latest)
- latest [unstable build ![GitHub commits since tagged version (branch)](https://img.shields.io/github/commits-since/skylot/jadx/latest/master)](https://nightly.link/skylot/jadx/workflows/build-artifacts/master)
After download unpack zip file go to `bin` directory and run: After download unpack zip file go to `bin` directory and run:
- `jadx` - command line version - `jadx` - command line version
@@ -45,14 +48,18 @@ On Windows run `.bat` files with double-click\
For Windows, you can download it from [oracle.com](https://www.oracle.com/java/technologies/downloads/#jdk17-windows) (select x64 Installer). For Windows, you can download it from [oracle.com](https://www.oracle.com/java/technologies/downloads/#jdk17-windows) (select x64 Installer).
### Install ### Install
1. Arch linux 1. Arch linux ![Arch Linux package](https://img.shields.io/archlinux/v/community/any/jadx?label=)
```bash ```bash
sudo pacman -S jadx sudo pacman -S jadx
``` ```
2. macOS 2. macOS ![homebrew version](https://img.shields.io/homebrew/v/jadx?label=)
```bash ```bash
brew install jadx brew install jadx
``` ```
3. [Flathub ![Flathub](https://img.shields.io/flathub/v/com.github.skylot.jadx?label=)](https://flathub.org/apps/details/com.github.skylot.jadx)
```bash
flatpak install flathub com.github.skylot.jadx
```
### Use jadx as a library ### 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) You can use jadx in your java projects, check details on [wiki page](https://github.com/skylot/jadx/wiki/Use-jadx-as-a-library)
@@ -107,7 +114,6 @@ options:
'read-or-save' - read if found, save otherwise (don't overwrite) 'read-or-save' - read if found, save otherwise (don't overwrite)
'overwrite' - don't read, always save 'overwrite' - don't read, always save
'ignore' - don't read and don't save 'ignore' - don't read and don't save
--deobf-rewrite-cfg - set '--deobf-cfg-file-mode' to 'overwrite' (deprecated)
--deobf-use-sourcename - use source file name as class name alias --deobf-use-sourcename - use source file name as class name alias
--deobf-parse-kotlin-metadata - parse kotlin metadata to class and package names --deobf-parse-kotlin-metadata - parse kotlin metadata to class and package names
--use-kotlin-methods-for-var-names - use kotlin intrinsic methods to rename variables, values: disable, apply, apply-and-hide, default: apply --use-kotlin-methods-for-var-names - use kotlin intrinsic methods to rename variables, values: disable, apply, apply-and-hide, default: apply
@@ -130,11 +136,11 @@ options:
-h, --help - print this help -h, --help - print this help
Plugin options (-P<name>=<value>): Plugin options (-P<name>=<value>):
1) dex-input (Load .dex and .apk files) 1) dex-input: Load .dex and .apk files
-Pdex-input.verify-checksum - Verify dex file checksum before load, values: [yes, no], default: yes - dex-input.verify-checksum - verify dex file checksum before load, values: [yes, no], default: yes
2) java-convert (Convert .jar and .class files to dex) 2) java-convert: Convert .class, .jar and .aar files to dex
-Pjava-convert.mode - Convert mode, values: [dx, d8, both], default: both - java-convert.mode - convert mode, values: [dx, d8, both], default: both
-Pjava-convert.d8-desugar - Use desugar in d8, values: [yes, no], default: no - java-convert.d8-desugar - use desugar in d8, values: [yes, no], default: no
Examples: Examples:
jadx -d out classes.dex jadx -d out classes.dex
-33
View File
@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop">
<id>com.github.skylot.jadx</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>Apache-2.0</project_license>
<name>JADX</name>
<summary>Dex to Java decompiler</summary>
<description>
<p>Command line and GUI tools for producing Java source code from Android Dex and Apk files</p>
<ul>
<li>decompile Dalvik bytecode to java classes from APK, dex, aar, aab and zip files</li>
<li>decode AndroidManifest.xml and other resources from resources.arsc</li>
<li>deobfuscator included</li>
<li>view decompiled code with highlighted syntax</li>
<li>jump to declaration</li>
<li>find usage</li>
<li>full text search</li>
<li>smali debugger</li>
</ul>
</description>
<screenshots>
<screenshot type="default">
<image>https://user-images.githubusercontent.com/118523/142730720-839f017e-38db-423e-b53f-39f5f0a0316f.png</image>
</screenshot>
</screenshots>
<content_rating type="oars-1.1" />
<launchable type="desktop-id">com.github.skylot.jadx.desktop</launchable>
<url type="homepage">https://github.com/skylot/jadx</url>
<url type="bugtracker">https://github.com/skylot/jadx/issues</url>
<releases>
<release version="1.3.4" date="2022-03-20" />
</releases>
</component>
+9 -11
View File
@@ -1,6 +1,6 @@
plugins { plugins {
id 'com.github.ben-manes.versions' version '0.42.0' id 'com.github.ben-manes.versions' version '0.42.0'
id 'com.diffplug.spotless' version '6.5.0' id 'com.diffplug.spotless' version '6.8.0'
} }
ext.jadxVersion = System.getenv('JADX_VERSION') ?: "dev" ext.jadxVersion = System.getenv('JADX_VERSION') ?: "dev"
@@ -14,7 +14,6 @@ allprojects {
version = jadxVersion version = jadxVersion
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
compileJava { compileJava {
options.encoding = "UTF-8" options.encoding = "UTF-8"
@@ -32,8 +31,8 @@ allprojects {
testImplementation 'ch.qos.logback:logback-classic:1.2.11' testImplementation 'ch.qos.logback:logback-classic:1.2.11'
testImplementation 'org.hamcrest:hamcrest-library:2.2' testImplementation 'org.hamcrest:hamcrest-library:2.2'
testImplementation 'org.mockito:mockito-core:4.5.1' testImplementation 'org.mockito:mockito-core:4.6.1'
testImplementation 'org.assertj:assertj-core:3.22.0' testImplementation 'org.assertj:assertj-core:3.23.1'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2'
@@ -50,6 +49,11 @@ allprojects {
mavenLocal() mavenLocal()
mavenCentral() mavenCentral()
google() google()
// Commented out for now since we're using a local mapping-io fork atm.
// maven {
// name 'FabricMC'
// url 'https://maven.fabricmc.net/'
// }
} }
} }
@@ -64,13 +68,7 @@ spotless {
importOrderFile 'config/code-formatter/eclipse.importorder' importOrderFile 'config/code-formatter/eclipse.importorder'
eclipse().configFile 'config/code-formatter/eclipse.xml' eclipse().configFile 'config/code-formatter/eclipse.xml'
if (JavaVersion.current() < JavaVersion.VERSION_16) { removeUnusedImports()
removeUnusedImports()
} else {
// google-format on Java 16+ issue: https://github.com/diffplug/spotless/issues/834
println('Warning! Unused imports remove is disabled for Java 16+'
+ ' (use workaround from https://github.com/diffplug/spotless/tree/main/plugin-gradle#google-java-format)')
}
lineEndings(com.diffplug.spotless.LineEnding.UNIX) lineEndings(com.diffplug.spotless.LineEnding.UNIX)
encoding("UTF-8") encoding("UTF-8")
+9
View File
@@ -1,2 +1,11 @@
org.gradle.warning.mode=all org.gradle.warning.mode=all
org.gradle.parallel=true org.gradle.parallel=true
# Flags for google-java-format (optimize imports by spotless) for Java >= 16.
# Java < 9 will ignore unsupported flags (thanks to -XX:+IgnoreUnrecognizedVMOptions)
org.gradle.jvmargs=-XX:+IgnoreUnrecognizedVMOptions \
--add-exports='jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED' \
--add-exports='jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED' \
--add-exports='jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED' \
--add-exports='jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED' \
--add-exports='jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED'
@@ -8,6 +8,7 @@ import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.function.Supplier;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -22,6 +23,7 @@ import jadx.api.plugins.JadxPluginInfo;
import jadx.api.plugins.JadxPluginManager; import jadx.api.plugins.JadxPluginManager;
import jadx.api.plugins.options.JadxPluginOptions; import jadx.api.plugins.options.JadxPluginOptions;
import jadx.api.plugins.options.OptionDescription; import jadx.api.plugins.options.OptionDescription;
import jadx.core.utils.Utils;
public class JCommanderWrapper<T> { public class JCommanderWrapper<T> {
private final JCommander jc; private final JCommander jc;
@@ -50,12 +52,24 @@ public class JCommanderWrapper<T> {
if (parameter.isAssigned()) { if (parameter.isAssigned()) {
// copy assigned field value to obj // copy assigned field value to obj
Parameterized parameterized = parameter.getParameterized(); Parameterized parameterized = parameter.getParameterized();
Object val = parameterized.get(parameter.getObject()); Object providedValue = parameterized.get(parameter.getObject());
parameterized.set(obj, val); Object newValue = mergeValues(parameterized.getType(), providedValue, () -> parameterized.get(obj));
parameterized.set(obj, newValue);
} }
} }
} }
@SuppressWarnings({ "rawtypes", "unchecked" })
private static Object mergeValues(Class<?> type, Object value, Supplier<Object> prevValueProvider) {
if (type.isAssignableFrom(Map.class)) {
// merge maps instead replacing whole map
Map prevMap = (Map) prevValueProvider.get();
return Utils.mergeMaps(prevMap, (Map) value); // value map will override keys in prevMap
}
// simple override
return value;
}
public void printUsage() { public void printUsage() {
// print usage in not sorted fields order (by default its sorted by description) // print usage in not sorted fields order (by default its sorted by description)
PrintStream out = System.out; PrintStream out = System.out;
@@ -179,11 +193,11 @@ public class JCommanderWrapper<T> {
return false; return false;
} }
JadxPluginInfo pluginInfo = plugin.getPluginInfo(); JadxPluginInfo pluginInfo = plugin.getPluginInfo();
out.append("\n ").append(k).append(") "); out.append("\n ").append(k).append(") ");
out.append(pluginInfo.getPluginId()).append(" (").append(pluginInfo.getDescription()).append(") "); out.append(pluginInfo.getPluginId()).append(": ").append(pluginInfo.getDescription());
for (OptionDescription desc : descs) { for (OptionDescription desc : descs) {
StringBuilder opt = new StringBuilder(); StringBuilder opt = new StringBuilder();
opt.append(" -P").append(desc.name()); opt.append(" - ").append(desc.name());
addSpaces(opt, maxNamesLen - opt.length()); addSpaces(opt, maxNamesLen - opt.length());
opt.append("- ").append(desc.description()); opt.append("- ").append(desc.description());
if (!desc.values().isEmpty()) { if (!desc.values().isEmpty()) {
+8 -2
View File
@@ -66,8 +66,14 @@ public class JadxCLI {
private static boolean checkForErrors(JadxDecompiler jadx) { private static boolean checkForErrors(JadxDecompiler jadx) {
if (jadx.getRoot().getClasses().isEmpty()) { if (jadx.getRoot().getClasses().isEmpty()) {
LOG.error("Load failed! No classes for decompile!"); if (jadx.getArgs().isSkipResources()) {
return true; LOG.error("Load failed! No classes for decompile!");
return true;
}
if (!jadx.getArgs().isSkipSources()) {
LOG.warn("No classes to decompile; decoding resources only");
jadx.getArgs().setSkipSources(true);
}
} }
if (jadx.getErrorsCount() > 0) { if (jadx.getErrorsCount() > 0) {
LOG.error("Load with errors! Check log for details"); LOG.error("Load with errors! Check log for details");
@@ -123,9 +123,6 @@ public class JadxCLIArgs {
) )
protected DeobfuscationMapFileMode deobfuscationMapFileMode = DeobfuscationMapFileMode.READ; protected DeobfuscationMapFileMode deobfuscationMapFileMode = DeobfuscationMapFileMode.READ;
@Parameter(names = { "--deobf-rewrite-cfg" }, description = "set '--deobf-cfg-file-mode' to 'overwrite' (deprecated)")
protected boolean deobfuscationForceSave = false;
@Parameter(names = { "--deobf-use-sourcename" }, description = "use source file name as class name alias") @Parameter(names = { "--deobf-use-sourcename" }, description = "use source file name as class name alias")
protected boolean deobfuscationUseSourceNameAsAlias = false; protected boolean deobfuscationUseSourceNameAsAlias = false;
@@ -259,11 +256,7 @@ public class JadxCLIArgs {
args.setReplaceConsts(replaceConsts); args.setReplaceConsts(replaceConsts);
args.setDeobfuscationOn(deobfuscationOn); args.setDeobfuscationOn(deobfuscationOn);
args.setDeobfuscationMapFile(FileUtils.toFile(deobfuscationMapFile)); args.setDeobfuscationMapFile(FileUtils.toFile(deobfuscationMapFile));
if (deobfuscationForceSave) { args.setDeobfuscationMapFileMode(deobfuscationMapFileMode);
args.setDeobfuscationMapFileMode(DeobfuscationMapFileMode.OVERWRITE);
} else {
args.setDeobfuscationMapFileMode(deobfuscationMapFileMode);
}
args.setDeobfuscationMinLength(deobfuscationMinLength); args.setDeobfuscationMinLength(deobfuscationMinLength);
args.setDeobfuscationMaxLength(deobfuscationMaxLength); args.setDeobfuscationMaxLength(deobfuscationMaxLength);
args.setUseSourceNameAsClassAlias(deobfuscationUseSourceNameAsAlias); args.setUseSourceNameAsClassAlias(deobfuscationUseSourceNameAsAlias);
@@ -377,10 +370,6 @@ public class JadxCLIArgs {
return deobfuscationMapFileMode; return deobfuscationMapFileMode;
} }
public boolean isDeobfuscationForceSave() {
return deobfuscationForceSave;
}
public boolean isDeobfuscationUseSourceNameAsAlias() { public boolean isDeobfuscationUseSourceNameAsAlias() {
return deobfuscationUseSourceNameAsAlias; return deobfuscationUseSourceNameAsAlias;
} }
@@ -1,9 +1,14 @@
package jadx.cli; package jadx.cli;
import java.util.Collections;
import java.util.Map;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import static jadx.core.utils.Utils.newConstStringMap;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
@@ -47,6 +52,40 @@ public class JadxCLIArgsTest {
assertThat(override(args, "").isUseImports(), is(false)); assertThat(override(args, "").isUseImports(), is(false));
} }
@Test
public void testPluginOptionsOverride() {
// add key to empty base map
checkPluginOptionsMerge(
Collections.emptyMap(),
"-Poption=otherValue",
newConstStringMap("option", "otherValue"));
// override one key
checkPluginOptionsMerge(
newConstStringMap("option", "value"),
"-Poption=otherValue",
newConstStringMap("option", "otherValue"));
// merge different keys
checkPluginOptionsMerge(
Collections.singletonMap("option1", "value1"),
"-Poption2=otherValue2",
newConstStringMap("option1", "value1", "option2", "otherValue2"));
// merge and override
checkPluginOptionsMerge(
newConstStringMap("option1", "value1", "option2", "value2"),
"-Poption2=otherValue2",
newConstStringMap("option1", "value1", "option2", "otherValue2"));
}
private void checkPluginOptionsMerge(Map<String, String> baseMap, String providedArgs, Map<String, String> expectedMap) {
JadxCLIArgs args = new JadxCLIArgs();
args.pluginOptions = baseMap;
Map<String, String> resultMap = override(args, providedArgs).getPluginOptions();
assertThat(resultMap, Matchers.equalTo(expectedMap));
}
private JadxCLIArgs parse(String... args) { private JadxCLIArgs parse(String... args) {
return parse(new JadxCLIArgs(), args); return parse(new JadxCLIArgs(), args);
} }
@@ -44,6 +44,33 @@ public class TestInput {
decompile("multi", "samples/hello.dex", "samples/HelloWorld.smali"); decompile("multi", "samples/hello.dex", "samples/HelloWorld.smali");
} }
@Test
public void testResourceOnly() throws Exception {
decode("resourceOnly", "samples/resources-only.apk");
}
private void decode(String tmpDirName, String apkSample) throws URISyntaxException, IOException {
List<String> args = new ArrayList<>();
Path tempDir = FileUtils.createTempDir(tmpDirName);
args.add("-v");
args.add("-d");
args.add(tempDir.toAbsolutePath().toString());
URL resource = getClass().getClassLoader().getResource(apkSample);
assertThat(resource).isNotNull();
String sampleFile = resource.toURI().getRawPath();
args.add(sampleFile);
int result = JadxCLI.execute(args.toArray(new String[0]));
assertThat(result).isEqualTo(0);
List<Path> files = Files.find(
tempDir,
3,
(file, attr) -> file.getFileName().toString().equalsIgnoreCase("AndroidManifest.xml"))
.collect(Collectors.toList());
assertThat(files.isEmpty()).isFalse();
}
private void decompile(String tmpDirName, String... inputSamples) throws URISyntaxException, IOException { private void decompile(String tmpDirName, String... inputSamples) throws URISyntaxException, IOException {
List<String> args = new ArrayList<>(); List<String> args = new ArrayList<>();
Path tempDir = FileUtils.createTempDir(tmpDirName); Path tempDir = FileUtils.createTempDir(tmpDirName);
+5 -6
View File
@@ -6,11 +6,10 @@ dependencies {
api(project(':jadx-plugins:jadx-plugins-api')) api(project(':jadx-plugins:jadx-plugins-api'))
implementation 'com.google.code.gson:gson:2.9.0' implementation 'com.google.code.gson:gson:2.9.0'
implementation 'com.android.tools.build:aapt2-proto:4.2.1-7147631'
constraints { // TODO: move resources decoding to separate plugin module
// Force protobuf version to prevent Java-7 issue implementation 'com.android.tools.build:aapt2-proto:7.2.1-7984345'
implementation 'com.google.protobuf:protobuf-java:3.11.4' implementation 'com.google.protobuf:protobuf-java:3.21.2' // forcing latest version
}
testImplementation 'org.apache.commons:commons-lang3:3.12.0' testImplementation 'org.apache.commons:commons-lang3:3.12.0'
@@ -20,7 +19,7 @@ dependencies {
testRuntimeOnly(project(':jadx-plugins:jadx-java-input')) testRuntimeOnly(project(':jadx-plugins:jadx-java-input'))
testRuntimeOnly(project(':jadx-plugins:jadx-raung-input')) testRuntimeOnly(project(':jadx-plugins:jadx-raung-input'))
testImplementation 'org.eclipse.jdt:ecj:3.29.0' testImplementation 'org.eclipse.jdt:ecj:3.30.0'
testImplementation 'tools.profiler:async-profiler:1.8.3' testImplementation 'tools.profiler:async-profiler:1.8.3'
} }
@@ -12,6 +12,9 @@ import java.util.Set;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Predicate; import java.util.function.Predicate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.args.DeobfuscationMapFileMode; import jadx.api.args.DeobfuscationMapFileMode;
import jadx.api.data.ICodeData; import jadx.api.data.ICodeData;
import jadx.api.impl.AnnotatedCodeWriter; import jadx.api.impl.AnnotatedCodeWriter;
@@ -19,6 +22,7 @@ import jadx.api.impl.InMemoryCodeCache;
import jadx.core.utils.files.FileUtils; import jadx.core.utils.files.FileUtils;
public class JadxArgs { public class JadxArgs {
private static final Logger LOG = LoggerFactory.getLogger(JadxArgs.class);
public static final int DEFAULT_THREADS_COUNT = Math.max(1, Runtime.getRuntime().availableProcessors() / 2); public static final int DEFAULT_THREADS_COUNT = Math.max(1, Runtime.getRuntime().availableProcessors() / 2);
@@ -122,6 +126,19 @@ public class JadxArgs {
setOutDirRes(new File(rootDir, DEFAULT_RES_DIR)); setOutDirRes(new File(rootDir, DEFAULT_RES_DIR));
} }
public void close() {
try {
inputFiles = null;
if (codeCache != null) {
codeCache.close();
}
} catch (Exception e) {
LOG.error("Failed to close JadxArgs", e);
} finally {
codeCache = null;
}
}
public List<File> getInputFiles() { public List<File> getInputFiles() {
return inputFiles; return inputFiles;
} }
@@ -37,8 +37,6 @@ import jadx.api.plugins.input.data.ILoadResult;
import jadx.api.plugins.options.JadxPluginOptions; import jadx.api.plugins.options.JadxPluginOptions;
import jadx.core.Jadx; import jadx.core.Jadx;
import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.InlinedAttr;
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;
@@ -99,7 +97,7 @@ public final class JadxDecompiler implements Closeable {
private final Map<MethodNode, JavaMethod> methodsMap = new ConcurrentHashMap<>(); private final Map<MethodNode, JavaMethod> methodsMap = new ConcurrentHashMap<>();
private final Map<FieldNode, JavaField> fieldsMap = new ConcurrentHashMap<>(); private final Map<FieldNode, JavaField> fieldsMap = new ConcurrentHashMap<>();
private final IDecompileScheduler decompileScheduler = new DecompilerScheduler(this); private final IDecompileScheduler decompileScheduler = new DecompilerScheduler();
private final List<ILoadResult> customLoads = new ArrayList<>(); private final List<ILoadResult> customLoads = new ArrayList<>();
@@ -161,8 +159,13 @@ public final class JadxDecompiler implements Closeable {
classesMap.clear(); classesMap.clear();
methodsMap.clear(); methodsMap.clear();
fieldsMap.clear(); fieldsMap.clear();
}
@Override
public void close() {
reset();
closeInputs(); closeInputs();
args.close();
} }
private void closeInputs() { private void closeInputs() {
@@ -176,11 +179,6 @@ public final class JadxDecompiler implements Closeable {
loadedInputs.clear(); loadedInputs.clear();
} }
@Override
public void close() {
reset();
}
private void loadPlugins(JadxArgs args) { private void loadPlugins(JadxArgs args) {
pluginManager.providesSuggestion("java-input", args.isUseDxInput() ? "java-convert" : "java-input"); pluginManager.providesSuggestion("java-input", args.isUseDxInput() ? "java-convert" : "java-input");
pluginManager.load(); pluginManager.load();
@@ -202,6 +200,7 @@ public final class JadxDecompiler implements Closeable {
} }
} }
@SuppressWarnings("unused")
public void registerPlugin(JadxPlugin plugin) { public void registerPlugin(JadxPlugin plugin) {
pluginManager.register(plugin); pluginManager.register(plugin);
} }
@@ -357,8 +356,9 @@ public final class JadxDecompiler implements Closeable {
tasks.add(() -> { tasks.add(() -> {
for (JavaClass cls : decompileBatch) { for (JavaClass cls : decompileBatch) {
try { try {
ICodeInfo code = cls.getCodeInfo(); ClassNode clsNode = cls.getClassNode();
SaveCode.save(outDir, cls.getClassNode(), code); ICodeInfo code = clsNode.getCode();
SaveCode.save(outDir, clsNode, code);
} catch (Exception e) { } catch (Exception e) {
LOG.error("Error saving class: {}", cls, e); LOG.error("Error saving class: {}", cls, e);
} }
@@ -467,23 +467,10 @@ public final class JadxDecompiler implements Closeable {
return protoXmlParser; return protoXmlParser;
} }
private void loadJavaClass(JavaClass javaClass) {
javaClass.getMethods().forEach(mth -> methodsMap.put(mth.getMethodNode(), mth));
javaClass.getFields().forEach(fld -> fieldsMap.put(fld.getFieldNode(), fld));
for (JavaClass innerCls : javaClass.getInnerClasses()) {
classesMap.put(innerCls.getClassNode(), innerCls);
loadJavaClass(innerCls);
}
for (JavaClass inlinedCls : javaClass.getInlinedClasses()) {
classesMap.put(inlinedCls.getClassNode(), inlinedCls);
loadJavaClass(inlinedCls);
}
}
/** /**
* Get JavaClass by ClassNode without loading and decompilation * Get JavaClass by ClassNode without loading and decompilation
*/ */
@ApiStatus.Internal
JavaClass convertClassNode(ClassNode cls) { JavaClass convertClassNode(ClassNode cls) {
return classesMap.compute(cls, (node, prevJavaCls) -> { return classesMap.compute(cls, (node, prevJavaCls) -> {
if (prevJavaCls != null && prevJavaCls.getClassNode() == cls) { if (prevJavaCls != null && prevJavaCls.getClassNode() == cls) {
@@ -497,100 +484,20 @@ public final class JadxDecompiler implements Closeable {
}); });
} }
@Nullable("For not generated classes")
@ApiStatus.Internal @ApiStatus.Internal
public JavaClass getJavaClassByNode(ClassNode cls) { JavaField convertFieldNode(FieldNode field) {
JavaClass javaClass = classesMap.get(cls); return fieldsMap.computeIfAbsent(field, fldNode -> {
if (javaClass != null && javaClass.getClassNode() == cls) { JavaClass parentCls = convertClassNode(fldNode.getParentClass());
return javaClass; return new JavaField(parentCls, fldNode);
} });
// load parent class if inner
ClassNode parentClass = cls.getTopParentClass();
if (parentClass.contains(AFlag.DONT_GENERATE)) {
return null;
}
JavaClass parentJavaClass = classesMap.get(parentClass);
if (parentJavaClass == null) {
getClasses();
parentJavaClass = classesMap.get(parentClass);
}
if (parentJavaClass != null) {
loadJavaClass(parentJavaClass);
javaClass = classesMap.get(cls);
if (javaClass != null) {
return javaClass;
}
}
// class or parent classes can be excluded from generation
if (cls.hasNotGeneratedParent()) {
return null;
}
throw new JadxRuntimeException("JavaClass not found by ClassNode: " + cls);
} }
@ApiStatus.Internal @ApiStatus.Internal
@Nullable JavaMethod convertMethodNode(MethodNode method) {
public JavaMethod getJavaMethodByNode(MethodNode mth) { return methodsMap.computeIfAbsent(method, mthNode -> {
JavaMethod javaMethod = methodsMap.get(mth); ClassNode parentCls = mthNode.getParentClass();
if (javaMethod != null && javaMethod.getMethodNode() == mth) { return new JavaMethod(convertClassNode(parentCls), mthNode);
return javaMethod; });
}
if (mth.contains(AFlag.DONT_GENERATE)) {
return null;
}
// parent class not loaded yet
ClassNode parentClass = mth.getParentClass();
ClassNode codeCls = getCodeParentClass(parentClass);
JavaClass javaClass = getJavaClassByNode(codeCls);
if (javaClass == null) {
return null;
}
loadJavaClass(javaClass);
javaMethod = methodsMap.get(mth);
if (javaMethod != null) {
return javaMethod;
}
if (parentClass.hasNotGeneratedParent()) {
return null;
}
throw new JadxRuntimeException("JavaMethod not found by MethodNode: " + mth);
}
private ClassNode getCodeParentClass(ClassNode cls) {
ClassNode codeCls;
InlinedAttr inlinedAttr = cls.get(AType.INLINED);
if (inlinedAttr != null) {
codeCls = inlinedAttr.getInlineCls().getTopParentClass();
} else {
codeCls = cls.getTopParentClass();
}
if (codeCls == cls) {
return codeCls;
}
return getCodeParentClass(codeCls);
}
@ApiStatus.Internal
@Nullable
public JavaField getJavaFieldByNode(FieldNode fld) {
JavaField javaField = fieldsMap.get(fld);
if (javaField != null && javaField.getFieldNode() == fld) {
return javaField;
}
// parent class not loaded yet
JavaClass javaClass = getJavaClassByNode(fld.getParentClass().getTopParentClass());
if (javaClass == null) {
return null;
}
loadJavaClass(javaClass);
javaField = fieldsMap.get(fld);
if (javaField != null) {
return javaField;
}
if (fld.getParentClass().hasNotGeneratedParent()) {
return null;
}
throw new JadxRuntimeException("JavaField not found by FieldNode: " + fld);
} }
@Nullable @Nullable
@@ -598,7 +505,7 @@ public final class JadxDecompiler implements Closeable {
return getRoot().getClasses().stream() return getRoot().getClasses().stream()
.filter(cls -> cls.getClassInfo().getFullName().equals(fullName)) .filter(cls -> cls.getClassInfo().getFullName().equals(fullName))
.findFirst() .findFirst()
.map(this::getJavaClassByNode) .map(this::convertClassNode)
.orElse(null); .orElse(null);
} }
@@ -619,9 +526,9 @@ public final class JadxDecompiler implements Closeable {
.orElse(null); .orElse(null);
if (node != null) { if (node != null) {
if (node.contains(AFlag.DONT_GENERATE)) { if (node.contains(AFlag.DONT_GENERATE)) {
return getJavaClassByNode(node.getTopParentClass()); return convertClassNode(node.getTopParentClass());
} else { } else {
return getJavaClassByNode(node); return convertClassNode(node);
} }
} }
return null; return null;
@@ -632,7 +539,7 @@ public final class JadxDecompiler implements Closeable {
return getRoot().getClasses().stream() return getRoot().getClasses().stream()
.filter(cls -> cls.getClassInfo().getAliasFullName().equals(fullName)) .filter(cls -> cls.getClassInfo().getAliasFullName().equals(fullName))
.findFirst() .findFirst()
.map(this::getJavaClassByNode) .map(this::convertClassNode)
.orElse(null); .orElse(null);
} }
@@ -650,9 +557,9 @@ public final class JadxDecompiler implements Closeable {
case CLASS: case CLASS:
return convertClassNode((ClassNode) ann); return convertClassNode((ClassNode) ann);
case METHOD: case METHOD:
return getJavaMethodByNode((MethodNode) ann); return convertMethodNode((MethodNode) ann);
case FIELD: case FIELD:
return getJavaFieldByNode((FieldNode) ann); return convertFieldNode((FieldNode) ann);
case DECLARATION: case DECLARATION:
return getJavaNodeByCodeAnnotation(codeInfo, ((NodeDeclareRef) ann).getNode()); return getJavaNodeByCodeAnnotation(codeInfo, ((NodeDeclareRef) ann).getNode());
case VAR: case VAR:
@@ -670,7 +577,7 @@ public final class JadxDecompiler implements Closeable {
@Nullable @Nullable
private JavaVariable resolveVarNode(VarNode varNode) { private JavaVariable resolveVarNode(VarNode varNode) {
MethodNode mthNode = varNode.getMth(); MethodNode mthNode = varNode.getMth();
JavaMethod mth = getJavaMethodByNode(mthNode); JavaMethod mth = convertMethodNode(mthNode);
if (mth == null) { if (mth == null) {
return null; return null;
} }
@@ -683,10 +590,13 @@ public final class JadxDecompiler implements Closeable {
throw new JadxRuntimeException("Missing code info for resolve VarRef: " + varRef); throw new JadxRuntimeException("Missing code info for resolve VarRef: " + varRef);
} }
ICodeAnnotation varNodeAnn = codeInfo.getCodeMetadata().getAt(varRef.getRefPos()); ICodeAnnotation varNodeAnn = codeInfo.getCodeMetadata().getAt(varRef.getRefPos());
if (varNodeAnn == null) { if (varNodeAnn != null && varNodeAnn.getAnnType() == ICodeAnnotation.AnnType.DECLARATION) {
return null; ICodeNodeRef nodeRef = ((NodeDeclareRef) varNodeAnn).getNode();
if (nodeRef.getAnnType() == ICodeAnnotation.AnnType.VAR) {
return resolveVarNode((VarNode) nodeRef);
}
} }
return (JavaVariable) getJavaNodeByCodeAnnotation(codeInfo, varNodeAnn); return null;
} }
List<JavaNode> convertNodes(Collection<? extends ICodeNodeRef> nodesList) { List<JavaNode> convertNodes(Collection<? extends ICodeNodeRef> nodesList) {
+80 -41
View File
@@ -10,18 +10,23 @@ import java.util.Map;
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;
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.AnonymousClassAttr; import jadx.core.dex.attributes.nodes.AnonymousClassAttr;
import jadx.core.dex.attributes.nodes.InlinedAttr;
import jadx.core.dex.info.AccessInfo; import jadx.core.dex.info.AccessInfo;
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;
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 JadxDecompiler decompiler; private final JadxDecompiler decompiler;
private final ClassNode cls; private final ClassNode cls;
@@ -53,7 +58,10 @@ public final class JavaClass implements JavaNode {
} }
public @NotNull ICodeInfo getCodeInfo() { public @NotNull ICodeInfo getCodeInfo() {
load(); ICodeInfo code = load();
if (code != null) {
return code;
}
return cls.decompile(); return cls.decompile();
} }
@@ -83,6 +91,14 @@ public final class JavaClass implements JavaNode {
return cls.getDisassembledCode(); return cls.getDisassembledCode();
} }
@Override
public boolean isOwnCodeAnnotation(ICodeAnnotation ann) {
if (ann.getAnnType() == ICodeAnnotation.AnnType.CLASS) {
return ann.equals(cls);
}
return false;
}
/** /**
* Internal API. Not Stable! * Internal API. Not Stable!
*/ */
@@ -94,20 +110,24 @@ public final class JavaClass implements JavaNode {
/** /**
* Decompile class and loads internal lists of fields, methods, etc. * Decompile class and loads internal lists of fields, methods, etc.
* Do nothing if already loaded. * Do nothing if already loaded.
* Return not null on first call only (for actual loading) *
* @return code info if decompilation was executed, null otherwise
*/ */
@Nullable private synchronized @Nullable ICodeInfo load() {
private synchronized void load() {
if (listsLoaded) { if (listsLoaded) {
return; return null;
} }
listsLoaded = true; listsLoaded = true;
JadxDecompiler rootDecompiler = getRootDecompiler();
ICodeCache codeCache = rootDecompiler.getArgs().getCodeCache(); ICodeInfo code;
if (!codeCache.contains(cls.getRawName())) { if (cls.getState().isProcessComplete()) {
cls.decompile(); // already decompiled -> class internals loaded
code = null;
} else {
code = cls.decompile();
} }
JadxDecompiler rootDecompiler = getRootDecompiler();
int inClsCount = cls.getInnerClasses().size(); int inClsCount = cls.getInnerClasses().size();
if (inClsCount != 0) { if (inClsCount != 0) {
List<JavaClass> list = new ArrayList<>(inClsCount); List<JavaClass> list = new ArrayList<>(inClsCount);
@@ -135,10 +155,9 @@ public final class JavaClass implements JavaNode {
if (fieldsCount != 0) { if (fieldsCount != 0) {
List<JavaField> flds = new ArrayList<>(fieldsCount); List<JavaField> flds = new ArrayList<>(fieldsCount);
for (FieldNode f : cls.getFields()) { for (FieldNode f : cls.getFields()) {
// if (!f.contains(AFlag.DONT_GENERATE)) { if (!f.contains(AFlag.DONT_GENERATE)) {
JavaField javaField = new JavaField(this, f); flds.add(rootDecompiler.convertFieldNode(f));
flds.add(javaField); }
// }
} }
this.fields = Collections.unmodifiableList(flds); this.fields = Collections.unmodifiableList(flds);
} }
@@ -148,16 +167,16 @@ public final class JavaClass implements JavaNode {
List<JavaMethod> mths = new ArrayList<>(methodsCount); List<JavaMethod> mths = new ArrayList<>(methodsCount);
for (MethodNode m : cls.getMethods()) { for (MethodNode m : cls.getMethods()) {
if (!m.contains(AFlag.DONT_GENERATE)) { if (!m.contains(AFlag.DONT_GENERATE)) {
JavaMethod javaMethod = new JavaMethod(this, m); mths.add(rootDecompiler.convertMethodNode(m));
mths.add(javaMethod);
} }
} }
mths.sort(Comparator.comparing(JavaMethod::getName)); mths.sort(Comparator.comparing(JavaMethod::getName));
this.methods = Collections.unmodifiableList(mths); this.methods = Collections.unmodifiableList(mths);
} }
return code;
} }
protected JadxDecompiler getRootDecompiler() { JadxDecompiler getRootDecompiler() {
if (parent != null) { if (parent != null) {
return parent.getRootDecompiler(); return parent.getRootDecompiler();
} }
@@ -188,23 +207,16 @@ public final class JavaClass implements JavaNode {
} }
public List<Integer> getUsePlacesFor(ICodeInfo codeInfo, JavaNode javaNode) { public List<Integer> getUsePlacesFor(ICodeInfo codeInfo, JavaNode javaNode) {
Map<Integer, ICodeAnnotation> map = codeInfo.getCodeMetadata().getAsMap(); if (!codeInfo.hasMetadata()) {
if (map.isEmpty() || decompiler == null) {
return Collections.emptyList(); return Collections.emptyList();
} }
JadxDecompiler rootDec = getRootDecompiler();
List<Integer> result = new ArrayList<>(); List<Integer> result = new ArrayList<>();
for (Map.Entry<Integer, ICodeAnnotation> entry : map.entrySet()) { codeInfo.getCodeMetadata().searchDown(0, (pos, ann) -> {
ICodeAnnotation ann = entry.getValue(); if (javaNode.isOwnCodeAnnotation(ann)) {
if (ann.getAnnType() == ICodeAnnotation.AnnType.DECLARATION) { result.add(pos);
// ignore declarations
continue;
} }
JavaNode annNode = rootDec.getJavaNodeByCodeAnnotation(codeInfo, ann); return null;
if (javaNode.equals(annNode)) { });
result.add(entry.getKey());
}
}
return result; return result;
} }
@@ -240,19 +252,37 @@ public final class JavaClass implements JavaNode {
return parent; return parent;
} }
@Override public JavaClass getOriginalTopParentClass() {
public JavaClass getTopParentClass() { return parent == null ? this : parent.getOriginalTopParentClass();
if (cls.contains(AType.ANONYMOUS_CLASS)) {
// moved to usage class
return getParentForAnonymousClass();
}
return parent == null ? this : parent.getTopParentClass();
} }
private JavaClass getParentForAnonymousClass() { /**
AnonymousClassAttr attr = cls.get(AType.ANONYMOUS_CLASS); * Return top parent class which contains code of this class.
ClassNode topParentClass = attr.getOuterCls().getTopParentClass(); * Code parent can be different from original parent after move or inline
return getRootDecompiler().convertClassNode(topParentClass); *
* @return this if already a top class
*/
@Override
public JavaClass getTopParentClass() {
JavaClass codeParent = getCodeParent();
return codeParent == null ? this : codeParent.getTopParentClass();
}
/**
* Return parent class which contains code of this class.
* Code parent can be different for original parent after move or inline
*/
public @Nullable JavaClass getCodeParent() {
AnonymousClassAttr anonymousClsAttr = cls.get(AType.ANONYMOUS_CLASS);
if (anonymousClsAttr != null) {
// moved to usage class
return getRootDecompiler().convertClassNode(anonymousClsAttr.getOuterCls());
}
InlinedAttr inlinedAttr = cls.get(AType.INLINED);
if (inlinedAttr != null) {
return getRootDecompiler().convertClassNode(inlinedAttr.getInlineCls());
}
return parent;
} }
public AccessInfo getAccessInfo() { public AccessInfo getAccessInfo() {
@@ -285,7 +315,16 @@ public final class JavaClass implements JavaNode {
if (methodNode == null) { if (methodNode == null) {
return null; return null;
} }
return new JavaMethod(this, methodNode); return getRootDecompiler().convertMethodNode(methodNode);
}
public List<JavaClass> getDependencies() {
JadxDecompiler d = getRootDecompiler();
return ListUtils.map(cls.getDependencies(), d::convertClassNode);
}
public int getTotalDepsCount() {
return cls.getTotalDepsCount();
} }
@Override @Override
@@ -4,6 +4,7 @@ import java.util.List;
import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
import jadx.api.metadata.ICodeAnnotation;
import jadx.core.dex.info.AccessInfo; import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.FieldNode;
@@ -65,6 +66,14 @@ public final class JavaField implements JavaNode {
this.field.getFieldInfo().removeAlias(); this.field.getFieldInfo().removeAlias();
} }
@Override
public boolean isOwnCodeAnnotation(ICodeAnnotation ann) {
if (ann.getAnnType() == ICodeAnnotation.AnnType.FIELD) {
return ann.equals(field);
}
return false;
}
/** /**
* Internal API. Not Stable! * Internal API. Not Stable!
*/ */
@@ -9,6 +9,7 @@ import org.jetbrains.annotations.ApiStatus;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import jadx.api.metadata.ICodeAnnotation;
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;
@@ -18,6 +19,7 @@ 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 static final Logger LOG = LoggerFactory.getLogger(JavaMethod.class);
private final MethodNode mth; private final MethodNode mth;
private final JavaClass parent; private final JavaClass parent;
@@ -78,7 +80,7 @@ public final class JavaMethod implements JavaNode {
JadxDecompiler decompiler = getDeclaringClass().getRootDecompiler(); JadxDecompiler decompiler = getDeclaringClass().getRootDecompiler();
return ovrdAttr.getRelatedMthNodes().stream() return ovrdAttr.getRelatedMthNodes().stream()
.map(m -> { .map(m -> {
JavaMethod javaMth = decompiler.getJavaMethodByNode(m); JavaMethod javaMth = decompiler.convertMethodNode(m);
if (javaMth == null) { if (javaMth == null) {
LOG.warn("Failed convert to java method: {}", m); LOG.warn("Failed convert to java method: {}", m);
} }
@@ -106,6 +108,14 @@ public final class JavaMethod implements JavaNode {
this.mth.getMethodInfo().removeAlias(); this.mth.getMethodInfo().removeAlias();
} }
@Override
public boolean isOwnCodeAnnotation(ICodeAnnotation ann) {
if (ann.getAnnType() == ICodeAnnotation.AnnType.METHOD) {
return ann.equals(mth);
}
return false;
}
/** /**
* Internal API. Not Stable! * Internal API. Not Stable!
*/ */
@@ -2,6 +2,8 @@ package jadx.api;
import java.util.List; import java.util.List;
import jadx.api.metadata.ICodeAnnotation;
public interface JavaNode { public interface JavaNode {
String getName(); String getName();
@@ -18,4 +20,6 @@ public interface JavaNode {
default void removeAlias() { default void removeAlias() {
} }
boolean isOwnCodeAnnotation(ICodeAnnotation ann);
} }
@@ -5,6 +5,8 @@ import java.util.List;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import jadx.api.metadata.ICodeAnnotation;
public final class JavaPackage implements JavaNode, Comparable<JavaPackage> { public final class JavaPackage implements JavaNode, Comparable<JavaPackage> {
private final String name; private final String name;
private final List<JavaClass> classes; private final List<JavaClass> classes;
@@ -49,6 +51,11 @@ public final class JavaPackage implements JavaNode, Comparable<JavaPackage> {
return Collections.emptyList(); return Collections.emptyList();
} }
@Override
public boolean isOwnCodeAnnotation(ICodeAnnotation ann) {
return false;
}
@Override @Override
public int compareTo(@NotNull JavaPackage o) { public int compareTo(@NotNull JavaPackage o) {
return name.compareTo(o.name); return name.compareTo(o.name);
@@ -4,8 +4,12 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import jadx.api.metadata.ICodeAnnotation;
import jadx.api.metadata.annotations.VarNode; import jadx.api.metadata.annotations.VarNode;
import jadx.api.metadata.annotations.VarRef;
import jadx.core.dex.instructions.args.ArgType;
public class JavaVariable implements JavaNode { public class JavaVariable implements JavaNode {
private final JavaMethod mth; private final JavaMethod mth;
@@ -29,7 +33,7 @@ public class JavaVariable implements JavaNode {
} }
@Override @Override
public String getName() { public @Nullable String getName() {
return varNode.getName(); return varNode.getName();
} }
@@ -43,6 +47,10 @@ public class JavaVariable implements JavaNode {
return varNode.getType() + " " + varNode.getName() + " (r" + varNode.getReg() + "v" + varNode.getSsa() + ")"; return varNode.getType() + " " + varNode.getName() + " (r" + varNode.getReg() + "v" + varNode.getSsa() + ")";
} }
public ArgType getType() {
return ArgType.tryToResolveClassAlias(mth.getMethodNode().root(), varNode.getType());
}
@Override @Override
public JavaClass getDeclaringClass() { public JavaClass getDeclaringClass() {
return mth.getDeclaringClass(); return mth.getDeclaringClass();
@@ -63,6 +71,15 @@ public class JavaVariable implements JavaNode {
return Collections.singletonList(mth); return Collections.singletonList(mth);
} }
@Override
public boolean isOwnCodeAnnotation(ICodeAnnotation ann) {
if (ann.getAnnType() == ICodeAnnotation.AnnType.VAR_REF) {
VarRef varRef = (VarRef) ann;
return varRef.getRefPos() == getDefPos();
}
return false;
}
@Override @Override
public int hashCode() { public int hashCode() {
return varNode.hashCode(); return varNode.hashCode();
@@ -9,7 +9,8 @@ public interface ICodeAnnotation {
VAR, VAR,
VAR_REF, VAR_REF,
DECLARATION, DECLARATION,
OFFSET OFFSET,
END // class or method body end
} }
AnnType getAnnType(); AnnType getAnnType();
@@ -32,6 +32,22 @@ public class NodeDeclareRef implements ICodeAnnotation {
return AnnType.DECLARATION; return AnnType.DECLARATION;
} }
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof NodeDeclareRef)) {
return false;
}
return node.equals(((NodeDeclareRef) o).node);
}
@Override
public int hashCode() {
return node.hashCode();
}
@Override @Override
public String toString() { public String toString() {
return "NodeDeclareRef{" + node + '}'; return "NodeDeclareRef{" + node + '}';
@@ -0,0 +1,21 @@
package jadx.api.metadata.annotations;
import jadx.api.metadata.ICodeAnnotation;
public class NodeEnd implements ICodeAnnotation {
public static final NodeEnd VALUE = new NodeEnd();
private NodeEnd() {
}
@Override
public AnnType getAnnType() {
return AnnType.END;
}
@Override
public String toString() {
return "END";
}
}
@@ -6,16 +6,14 @@ import java.util.Map;
import java.util.NavigableMap; import java.util.NavigableMap;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.stream.Stream;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.ICodeAnnotation;
import jadx.api.metadata.ICodeAnnotation.AnnType;
import jadx.api.metadata.ICodeMetadata; import jadx.api.metadata.ICodeMetadata;
import jadx.api.metadata.ICodeNodeRef; import jadx.api.metadata.ICodeNodeRef;
import jadx.api.metadata.annotations.NodeDeclareRef; import jadx.api.metadata.annotations.NodeDeclareRef;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.Utils; import jadx.core.utils.Utils;
public class CodeMetadataStorage implements ICodeMetadata { public class CodeMetadataStorage implements ICodeMetadata {
@@ -55,7 +53,7 @@ public class CodeMetadataStorage implements ICodeMetadata {
} }
@Override @Override
public @Nullable ICodeAnnotation searchUp(int position, ICodeAnnotation.AnnType annType) { public @Nullable ICodeAnnotation searchUp(int position, AnnType annType) {
for (ICodeAnnotation v : navMap.tailMap(position, true).values()) { for (ICodeAnnotation v : navMap.tailMap(position, true).values()) {
if (v.getAnnType() == annType) { if (v.getAnnType() == annType) {
return v; return v;
@@ -65,7 +63,7 @@ public class CodeMetadataStorage implements ICodeMetadata {
} }
@Override @Override
public @Nullable ICodeAnnotation searchUp(int position, int limitPos, ICodeAnnotation.AnnType annType) { public @Nullable ICodeAnnotation searchUp(int position, int limitPos, AnnType annType) {
for (ICodeAnnotation v : navMap.subMap(position, true, limitPos, true).values()) { for (ICodeAnnotation v : navMap.subMap(position, true, limitPos, true).values()) {
if (v.getAnnType() == annType) { if (v.getAnnType() == annType) {
return v; return v;
@@ -99,28 +97,40 @@ public class CodeMetadataStorage implements ICodeMetadata {
@Override @Override
public ICodeNodeRef getNodeAt(int position) { public ICodeNodeRef getNodeAt(int position) {
return navMap.tailMap(position, true) int nesting = 0;
.values().stream() for (ICodeAnnotation ann : navMap.tailMap(position, true).values()) {
.flatMap(CodeMetadataStorage::mapEnclosingNode) switch (ann.getAnnType()) {
.findFirst().orElse(null); case END:
nesting++;
break;
case DECLARATION:
ICodeNodeRef node = ((NodeDeclareRef) ann).getNode();
AnnType nodeType = node.getAnnType();
if (nodeType == AnnType.CLASS || nodeType == AnnType.METHOD) {
if (nesting == 0) {
return node;
}
nesting--;
}
break;
}
}
return null;
} }
@Override @Override
public ICodeNodeRef getNodeBelow(int position) { public ICodeNodeRef getNodeBelow(int position) {
return navMap.headMap(position, true).descendingMap() for (ICodeAnnotation ann : navMap.headMap(position, true).descendingMap().values()) {
.values().stream() if (ann.getAnnType() == AnnType.DECLARATION) {
.flatMap(CodeMetadataStorage::mapEnclosingNode) ICodeNodeRef node = ((NodeDeclareRef) ann).getNode();
.findFirst().orElse(null); AnnType nodeType = node.getAnnType();
} if (nodeType == AnnType.CLASS || nodeType == AnnType.METHOD) {
return node;
private static Stream<ICodeNodeRef> mapEnclosingNode(ICodeAnnotation ann) { }
if (ann instanceof NodeDeclareRef) {
ICodeNodeRef node = ((NodeDeclareRef) ann).getNode();
if (node instanceof ClassNode || node instanceof MethodNode) {
return Stream.of(node);
} }
} }
return Stream.empty(); return null;
} }
@Override @Override
@@ -135,7 +145,7 @@ public class CodeMetadataStorage implements ICodeMetadata {
@Override @Override
public String toString() { public String toString() {
return "CodeMetadata{lines=" + lines return "CodeMetadata{\nlines=" + lines
+ ", annotations=\n" + Utils.listToString(navMap.entrySet(), "\n") + "\n}"; + "\nannotations=\n " + Utils.listToString(navMap.descendingMap().entrySet(), "\n ") + "\n}";
} }
} }
@@ -7,6 +7,7 @@ public class Consts {
public static final boolean DEBUG_TYPE_INFERENCE = false; public static final boolean DEBUG_TYPE_INFERENCE = false;
public static final boolean DEBUG_OVERLOADED_CASTS = false; public static final boolean DEBUG_OVERLOADED_CASTS = false;
public static final boolean DEBUG_EXC_HANDLERS = false; public static final boolean DEBUG_EXC_HANDLERS = false;
public static final boolean DEBUG_FINALLY = false;
public static final String CLASS_OBJECT = "java.lang.Object"; public static final String CLASS_OBJECT = "java.lang.Object";
public static final String CLASS_STRING = "java.lang.String"; public static final String CLASS_STRING = "java.lang.String";
+10 -3
View File
@@ -238,9 +238,17 @@ public class Jadx {
private static String version; private static String version;
public static String getVersion() { public static String getVersion() {
if (version != null) { if (version == null) {
return version; version = searchJadxVersion();
} }
return version;
}
public static boolean isDevVersion() {
return getVersion().equals(VERSION_DEV);
}
private static String searchJadxVersion() {
try { try {
ClassLoader classLoader = Jadx.class.getClassLoader(); ClassLoader classLoader = Jadx.class.getClassLoader();
if (classLoader != null) { if (classLoader != null) {
@@ -250,7 +258,6 @@ public class Jadx {
Manifest manifest = new Manifest(is); Manifest manifest = new Manifest(is);
String ver = manifest.getMainAttributes().getValue("jadx-version"); String ver = manifest.getMainAttributes().getValue("jadx-version");
if (ver != null) { if (ver != null) {
version = ver;
return ver; return ver;
} }
} }
@@ -203,6 +203,9 @@ public class ClspGraph {
if (isNew) { if (isNew) {
addSuperTypes(parentCls, result); addSuperTypes(parentCls, result);
} }
} else {
// parent type is unknown
result.add(parentType.getObject());
} }
} }
} }
@@ -19,6 +19,7 @@ import jadx.api.CommentsLevel;
import jadx.api.ICodeInfo; import jadx.api.ICodeInfo;
import jadx.api.ICodeWriter; import jadx.api.ICodeWriter;
import jadx.api.JadxArgs; import jadx.api.JadxArgs;
import jadx.api.metadata.annotations.NodeEnd;
import jadx.api.plugins.input.data.AccessFlags; import jadx.api.plugins.input.data.AccessFlags;
import jadx.api.plugins.input.data.annotations.EncodedType; import jadx.api.plugins.input.data.annotations.EncodedType;
import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.data.annotations.EncodedValue;
@@ -256,6 +257,7 @@ public class ClassGen {
addInnerClsAndMethods(clsCode); addInnerClsAndMethods(clsCode);
clsCode.decIndent(); clsCode.decIndent();
clsCode.startLine('}'); clsCode.startLine('}');
clsCode.attachAnnotation(NodeEnd.VALUE);
} }
private void addInnerClsAndMethods(ICodeWriter clsCode) { private void addInnerClsAndMethods(ICodeWriter clsCode) {
@@ -369,6 +371,7 @@ public class ClassGen {
mthGen.addInstructions(code); mthGen.addInstructions(code);
code.decIndent(); code.decIndent();
code.startLine('}'); code.startLine('}');
code.attachAnnotation(NodeEnd.VALUE);
} }
} }
@@ -614,21 +617,23 @@ public class ClassGen {
if (useCls.equals(extClsInfo)) { if (useCls.equals(extClsInfo)) {
return shortName; return shortName;
} }
if (extClsInfo.getPackage().equals("java.lang") && extClsInfo.getParentClass() == null) {
return shortName;
}
if (isClassInnerFor(useCls, extClsInfo)) { if (isClassInnerFor(useCls, extClsInfo)) {
return shortName; return shortName;
} }
if (extClsInfo.isInner()) { if (extClsInfo.isInner()) {
return expandInnerClassName(useCls, extClsInfo); return expandInnerClassName(useCls, extClsInfo);
} }
if (searchCollision(cls.root(), useCls, extClsInfo)) { if (checkInnerCollision(cls.root(), useCls, extClsInfo)
|| checkInPackageCollision(cls.root(), useCls, extClsInfo)) {
return fullName; return fullName;
} }
if (isBothClassesInOneTopClass(useCls, extClsInfo)) { if (isBothClassesInOneTopClass(useCls, extClsInfo)) {
return shortName; return shortName;
} }
// don't add import for top classes from 'java.lang' package (subpackages excluded)
if (extClsInfo.getPackage().equals("java.lang") && extClsInfo.getParentClass() == null) {
return shortName;
}
// don't add import if this class from same package // don't add import if this class from same package
if (extClsInfo.getPackage().equals(useCls.getPackage()) && !extClsInfo.isInner()) { if (extClsInfo.getPackage().equals(useCls.getPackage()) && !extClsInfo.isInner()) {
return shortName; return shortName;
@@ -709,7 +714,7 @@ public class ClassGen {
return false; return false;
} }
private static boolean searchCollision(RootNode root, ClassInfo useCls, ClassInfo searchCls) { private static boolean checkInnerCollision(RootNode root, @Nullable ClassInfo useCls, ClassInfo searchCls) {
if (useCls == null) { if (useCls == null) {
return false; return false;
} }
@@ -726,7 +731,20 @@ public class ClassGen {
} }
} }
} }
return searchCollision(root, useCls.getParentClass(), searchCls); return checkInnerCollision(root, useCls.getParentClass(), searchCls);
}
/**
* Check if class with same name exists in current package
*/
private static boolean checkInPackageCollision(RootNode root, ClassInfo useCls, ClassInfo searchCls) {
String currentPkg = useCls.getAliasPkg();
if (currentPkg.equals(searchCls.getAliasPkg())) {
// search class already from current package
return false;
}
String shortName = searchCls.getAliasShortName();
return root.getClsp().isClsKnown(currentPkg + '.' + shortName);
} }
private void insertRenameInfo(ICodeWriter code, ClassNode cls) { private void insertRenameInfo(ICodeWriter code, ClassNode cls) {
@@ -168,10 +168,11 @@ public class InsnGen {
* Variable definition without type, only var name * Variable definition without type, only var name
*/ */
private void defVar(ICodeWriter code, CodeVar codeVar) { private void defVar(ICodeWriter code, CodeVar codeVar) {
String varName = mgen.getNameGen().assignArg(codeVar);
if (code.isMetadataSupported()) { if (code.isMetadataSupported()) {
code.attachDefinition(VarNode.get(mth, codeVar)); code.attachDefinition(VarNode.get(mth, codeVar));
} }
code.add(mgen.getNameGen().assignArg(codeVar)); code.add(varName);
} }
private String lit(LiteralArg arg) { private String lit(LiteralArg arg) {
@@ -760,6 +761,7 @@ public class InsnGen {
ctor.add(AFlag.DONT_GENERATE); ctor.add(AFlag.DONT_GENERATE);
} }
} }
code.attachDefinition(cls);
code.add("new "); code.add("new ");
useClass(code, parent); useClass(code, parent);
MethodNode callMth = mth.root().resolveMethod(insn.getCallMth()); MethodNode callMth = mth.root().resolveMethod(insn.getCallMth());
@@ -806,14 +808,9 @@ public class InsnGen {
break; break;
case SUPER: case SUPER:
ClassInfo superCallCls = getClassForSuperCall(code, callMth); callSuper(code, callMth);
if (superCallCls != null) { k++; // use 'super' instead 'this' in 0 arg
useClass(code, superCallCls); code.add('.');
code.add('.');
}
// use 'super' instead 'this' in 0 arg
code.add("super").add('.');
k++;
break; break;
case STATIC: case STATIC:
@@ -827,7 +824,9 @@ public class InsnGen {
} }
if (callMthNode != null) { if (callMthNode != null) {
code.attachAnnotation(callMthNode); code.attachAnnotation(callMthNode);
code.add(callMthNode.getAlias()); }
if (insn.contains(AFlag.FORCE_RAW_NAME)) {
code.add(callMth.getName());
} else { } else {
code.add(callMth.getAlias()); code.add(callMth.getAlias());
} }
@@ -964,34 +963,43 @@ public class InsnGen {
code.startLine('}'); code.startLine('}');
} }
@Nullable private void callSuper(ICodeWriter code, MethodInfo callMth) {
private ClassInfo getClassForSuperCall(ICodeWriter code, MethodInfo callMth) { ClassInfo superCallCls = getClassForSuperCall(callMth);
ClassNode useCls = mth.getParentClass(); if (superCallCls == null) {
ClassInfo insnCls = useCls.getClassInfo(); // unknown class, add comment to keep that info
ClassInfo declClass = callMth.getDeclClass(); code.add("super/*").add(callMth.getDeclClass().getFullName()).add("*/");
if (insnCls.equals(declClass)) { return;
return null;
} }
ClassNode topClass = useCls.getTopParentClass(); ClassInfo curClass = mth.getParentClass().getClassInfo();
if (topClass.getClassInfo().equals(declClass)) { if (superCallCls.equals(curClass)) {
return declClass; code.add("super");
return;
} }
// search call class // use custom class
ClassNode nextParent = useCls; useClass(code, superCallCls);
do { code.add(".super");
ClassInfo nextClsInfo = nextParent.getClassInfo(); }
if (nextClsInfo.equals(declClass)
|| ArgType.isInstanceOf(mth.root(), nextClsInfo.getType(), declClass.getType())) {
if (nextParent == useCls) {
return null;
}
return nextClsInfo;
}
nextParent = nextParent.getParentClass();
} while (nextParent != null && nextParent != topClass);
// search failed, just return parent class /**
return useCls.getParentClass().getClassInfo(); * Search call class in super types of this
* and all parent classes (needed for inlined synthetic calls)
*/
@Nullable
private ClassInfo getClassForSuperCall(MethodInfo callMth) {
ArgType declClsType = callMth.getDeclClass().getType();
ClassNode parentNode = mth.getParentClass();
while (true) {
ClassInfo parentCls = parentNode.getClassInfo();
if (ArgType.isInstanceOf(root, parentCls.getType(), declClsType)) {
return parentCls;
}
ClassNode nextParent = parentNode.getParentClass();
if (nextParent == parentNode) {
// no parent, class not found
return null;
}
parentNode = nextParent;
}
} }
void generateMethodArguments(ICodeWriter code, BaseInvokeNode insn, int startArgNum, void generateMethodArguments(ICodeWriter code, BaseInvokeNode insn, int startArgNum,
@@ -26,6 +26,7 @@ import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.JadxError; import jadx.core.dex.attributes.nodes.JadxError;
import jadx.core.dex.attributes.nodes.JumpInfo; import jadx.core.dex.attributes.nodes.JumpInfo;
import jadx.core.dex.attributes.nodes.MethodOverrideAttr; import jadx.core.dex.attributes.nodes.MethodOverrideAttr;
import jadx.core.dex.attributes.nodes.MethodReplaceAttr;
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;
@@ -144,8 +145,9 @@ public class MethodGen {
} else { } else {
classGen.useType(code, mth.getReturnType()); classGen.useType(code, mth.getReturnType());
code.add(' '); code.add(' ');
code.attachDefinition(mth); MethodNode defMth = getMethodForDefinition();
code.add(mth.getAlias()); code.attachDefinition(defMth);
code.add(defMth.getAlias());
} }
code.add('('); code.add('(');
@@ -178,6 +180,14 @@ public class MethodGen {
return true; return true;
} }
private MethodNode getMethodForDefinition() {
MethodReplaceAttr replaceAttr = mth.get(AType.METHOD_REPLACE);
if (replaceAttr != null) {
return replaceAttr.getReplaceMth();
}
return mth;
}
private void addOverrideAnnotation(ICodeWriter code, MethodNode mth) { private void addOverrideAnnotation(ICodeWriter code, MethodNode mth) {
MethodOverrideAttr overrideAttr = mth.get(AType.METHOD_OVERRIDE); MethodOverrideAttr overrideAttr = mth.get(AType.METHOD_OVERRIDE);
if (overrideAttr == null) { if (overrideAttr == null) {
@@ -25,6 +25,8 @@ public enum AFlag {
HIDDEN, // instruction used inside other instruction but not listed in args HIDDEN, // instruction used inside other instruction but not listed in args
DONT_RENAME, // do not rename during deobfuscation DONT_RENAME, // do not rename during deobfuscation
FORCE_RAW_NAME, // force use of raw name instead alias
ADDED_TO_REGION, ADDED_TO_REGION,
EXC_TOP_SPLITTER, EXC_TOP_SPLITTER,
@@ -180,12 +180,12 @@ public final class ClassInfo implements Comparable<ClassInfo> {
return makeFullClsName(pkg, name, parentClass, false, true); return makeFullClsName(pkg, name, parentClass, false, true);
} }
private String makeAliasFullName() { public String makeAliasFullName() {
return makeFullClsName(getAliasPkg(), getAliasShortName(), parentClass, true, false); return makeFullClsName(getAliasPkg(), getAliasShortName(), parentClass, true, false);
} }
private String makeAliasRawFullName() { public String makeAliasRawFullName() {
return makeFullClsName(pkg, name, parentClass, true, true); return makeFullClsName(getAliasPkg(), getAliasShortName(), parentClass, true, true);
} }
public String getAliasFullPath() { public String getAliasFullPath() {
@@ -170,6 +170,10 @@ public final class BlockNode extends AttrNode implements IBlock, Comparable<Bloc
return contains(AFlag.RETURN); return contains(AFlag.RETURN);
} }
public boolean isEmpty() {
return instructions.isEmpty();
}
@Override @Override
public int hashCode() { public int hashCode() {
return startOffset; return startOffset;
@@ -21,6 +21,7 @@ import jadx.api.ICodeCache;
import jadx.api.ICodeInfo; import jadx.api.ICodeInfo;
import jadx.api.ICodeWriter; import jadx.api.ICodeWriter;
import jadx.api.JadxArgs; import jadx.api.JadxArgs;
import jadx.api.impl.SimpleCodeInfo;
import jadx.api.plugins.input.data.IClassData; import jadx.api.plugins.input.data.IClassData;
import jadx.api.plugins.input.data.IFieldData; import jadx.api.plugins.input.data.IFieldData;
import jadx.api.plugins.input.data.IMethodData; import jadx.api.plugins.input.data.IMethodData;
@@ -378,8 +379,16 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
return code; return code;
} }
} }
ICodeInfo codeInfo = root.getProcessClasses().generateCode(this); ICodeInfo codeInfo;
codeCache.add(clsRawName, codeInfo); try {
codeInfo = root.getProcessClasses().generateCode(this);
} catch (Throwable e) {
addError("Code generation failed", e);
codeInfo = new SimpleCodeInfo(Utils.getStackTrace(e));
}
if (codeInfo != ICodeInfo.EMPTY) {
codeCache.add(clsRawName, codeInfo);
}
return codeInfo; return codeInfo;
} }
@@ -316,6 +316,10 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
return blocks; return blocks;
} }
public void setBasicBlocks(List<BlockNode> blocks) {
this.blocks = blocks;
}
public BlockNode getEnterBlock() { public BlockNode getEnterBlock() {
return enterBlock; return enterBlock;
} }
@@ -461,6 +465,10 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
return regsCount; return regsCount;
} }
public int getArgsStartReg() {
return argsStartReg;
}
public SSAVar makeNewSVar(@NotNull RegisterArg assignArg) { public SSAVar makeNewSVar(@NotNull RegisterArg assignArg) {
int regNum = assignArg.getRegNum(); int regNum = assignArg.getRegNum();
return makeNewSVar(regNum, getNextSVarVersion(regNum), assignArg); return makeNewSVar(regNum, getNextSVarVersion(regNum), assignArg);
@@ -42,7 +42,8 @@ import jadx.core.utils.StringUtils;
import jadx.core.utils.Utils; import jadx.core.utils.Utils;
import jadx.core.utils.android.AndroidResourcesUtils; import jadx.core.utils.android.AndroidResourcesUtils;
import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.xmlgen.ResTableParser; import jadx.core.xmlgen.IResParser;
import jadx.core.xmlgen.ResDecoder;
import jadx.core.xmlgen.ResourceStorage; import jadx.core.xmlgen.ResourceStorage;
import jadx.core.xmlgen.entry.ResourceEntry; import jadx.core.xmlgen.entry.ResourceEntry;
import jadx.core.xmlgen.entry.ValuesParser; import jadx.core.xmlgen.entry.ValuesParser;
@@ -115,21 +116,25 @@ public class RootNode {
} }
private void addDummyClass(IClassData classData, Exception exc) { private void addDummyClass(IClassData classData, Exception exc) {
String typeStr = classData.getType();
String name = null;
try { try {
ClassInfo clsInfo = ClassInfo.fromName(this, typeStr); String typeStr = classData.getType();
if (clsInfo != null) { String name = null;
name = clsInfo.getShortName(); try {
ClassInfo clsInfo = ClassInfo.fromName(this, typeStr);
if (clsInfo != null) {
name = clsInfo.getShortName();
}
} catch (Exception e) {
LOG.error("Failed to get name for class with type {}", typeStr, e);
} }
} catch (Exception e) { if (name == null || name.isEmpty()) {
LOG.error("Failed to get name for class with type {}", typeStr, e); name = "CLASS_" + typeStr;
}
ClassNode clsNode = ClassNode.addSyntheticClass(this, name, classData.getAccessFlags());
ErrorsCounter.error(clsNode, "Load error", exc);
} catch (Exception innerExc) {
LOG.error("Failed to load class from file: {}", classData.getInputFileName(), exc);
} }
if (name == null || name.isEmpty()) {
name = "CLASS_" + typeStr;
}
ClassNode clsNode = ClassNode.addSyntheticClass(this, name, classData.getAccessFlags());
ErrorsCounter.error(clsNode, "Load error", exc);
} }
private static void markDuplicatedClasses(List<ClassNode> classes) { private static void markDuplicatedClasses(List<ClassNode> classes) {
@@ -159,23 +164,13 @@ public class RootNode {
} }
public void loadResources(List<ResourceFile> resources) { public void loadResources(List<ResourceFile> resources) {
ResourceFile arsc = null; ResourceFile arsc = getResourceFile(resources);
for (ResourceFile rf : resources) {
if (rf.getType() == ResourceType.ARSC) {
arsc = rf;
break;
}
}
if (arsc == null) { if (arsc == null) {
LOG.debug("'.arsc' file not found"); LOG.debug("'.arsc' file not found");
return; return;
} }
try { try {
ResTableParser parser = ResourcesLoader.decodeStream(arsc, (size, is) -> { IResParser parser = ResourcesLoader.decodeStream(arsc, (size, is) -> ResDecoder.decode(this, arsc, is));
ResTableParser tableParser = new ResTableParser(this);
tableParser.decode(is);
return tableParser;
});
if (parser != null) { if (parser != null) {
processResources(parser.getResStorage()); processResources(parser.getResStorage());
updateObfuscatedFiles(parser, resources); updateObfuscatedFiles(parser, resources);
@@ -185,6 +180,15 @@ public class RootNode {
} }
} }
private @Nullable ResourceFile getResourceFile(List<ResourceFile> resources) {
for (ResourceFile rf : resources) {
if (rf.getType() == ResourceType.ARSC) {
return rf;
}
}
return null;
}
public void processResources(ResourceStorage resStorage) { public void processResources(ResourceStorage resStorage) {
constValues.setResourcesNames(resStorage.getResourcesNames()); constValues.setResourcesNames(resStorage.getResourcesNames());
appPackage = resStorage.getAppPackage(); appPackage = resStorage.getAppPackage();
@@ -205,7 +209,7 @@ public class RootNode {
} }
} }
private void updateObfuscatedFiles(ResTableParser parser, List<ResourceFile> resources) { private void updateObfuscatedFiles(IResParser parser, List<ResourceFile> resources) {
if (args.isSkipResources()) { if (args.isSkipResources()) {
return; return;
} }
@@ -265,6 +269,7 @@ public class RootNode {
public void runPreDecompileStage() { public void runPreDecompileStage() {
boolean debugEnabled = LOG.isDebugEnabled(); boolean debugEnabled = LOG.isDebugEnabled();
for (IDexTreeVisitor pass : preDecompilePasses) { for (IDexTreeVisitor pass : preDecompilePasses) {
Utils.checkThreadInterrupt();
long start = debugEnabled ? System.currentTimeMillis() : 0; long start = debugEnabled ? System.currentTimeMillis() : 0;
try { try {
pass.init(this); pass.init(this);
@@ -14,9 +14,9 @@ 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.IContainer;
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;
import jadx.core.utils.exceptions.JadxRuntimeException;
public class ExceptionHandler { public class ExceptionHandler {
@@ -33,9 +33,14 @@ public class ExceptionHandler {
private boolean removed = false; private boolean removed = false;
public ExceptionHandler(int addr, @Nullable ClassInfo type) { public static ExceptionHandler build(MethodNode mth, int addr, @Nullable ClassInfo type) {
ExceptionHandler eh = new ExceptionHandler(addr);
eh.addCatchType(mth, type);
return eh;
}
private ExceptionHandler(int addr) {
this.handlerOffset = addr; this.handlerOffset = addr;
addCatchType(type);
} }
/** /**
@@ -43,7 +48,7 @@ public class ExceptionHandler {
* *
* @param type - null for 'all' or 'Throwable' handler * @param type - null for 'all' or 'Throwable' handler
*/ */
public boolean addCatchType(@Nullable ClassInfo type) { public boolean addCatchType(MethodNode mth, @Nullable ClassInfo type) {
if (type != null) { if (type != null) {
if (catchTypes.contains(type)) { if (catchTypes.contains(type)) {
return false; return false;
@@ -51,14 +56,16 @@ public class ExceptionHandler {
return catchTypes.add(type); return catchTypes.add(type);
} }
if (!this.catchTypes.isEmpty()) { if (!this.catchTypes.isEmpty()) {
throw new JadxRuntimeException("Null type added to not empty exception handler: " + this); mth.addDebugComment("Throwable added to exception handler: '" + catchTypeStr() + "', keep only Throwable");
catchTypes.clear();
return true;
} }
return false; return false;
} }
public void addCatchTypes(Collection<ClassInfo> types) { public void addCatchTypes(MethodNode mth, Collection<ClassInfo> types) {
for (ClassInfo type : types) { for (ClassInfo type : types) {
addCatchType(type); addCatchType(mth, type);
} }
} }
@@ -133,7 +133,7 @@ public class AttachTryCatchVisitor extends AbstractVisitor {
ExcHandlerAttr excHandlerAttr = insn.get(AType.EXC_HANDLER); ExcHandlerAttr excHandlerAttr = insn.get(AType.EXC_HANDLER);
if (excHandlerAttr != null) { if (excHandlerAttr != null) {
ExceptionHandler handler = excHandlerAttr.getHandler(); ExceptionHandler handler = excHandlerAttr.getHandler();
if (handler.addCatchType(type)) { if (handler.addCatchType(mth, type)) {
// exist handler updated (assume from same try block) - don't add again // exist handler updated (assume from same try block) - don't add again
return null; return null;
} }
@@ -143,7 +143,7 @@ public class AttachTryCatchVisitor extends AbstractVisitor {
} else { } else {
insn = insertNOP(insnByOffset, handlerOffset); insn = insertNOP(insnByOffset, handlerOffset);
} }
ExceptionHandler handler = new ExceptionHandler(handlerOffset, type); ExceptionHandler handler = ExceptionHandler.build(mth, handlerOffset, type);
mth.addExceptionHandler(handler); mth.addExceptionHandler(handler);
insn.addAttr(new ExcHandlerAttr(handler)); insn.addAttr(new ExcHandlerAttr(handler));
return handler; return handler;
@@ -10,6 +10,7 @@ import java.util.Objects;
import jadx.api.plugins.input.data.AccessFlags; import jadx.api.plugins.input.data.AccessFlags;
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.nodes.FieldReplaceAttr; import jadx.core.dex.attributes.nodes.FieldReplaceAttr;
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.attributes.nodes.SkipMethodArgsAttr;
@@ -225,7 +226,7 @@ public class ClassModifier extends AbstractVisitor {
} }
private static boolean removeBridgeMethod(ClassNode cls, MethodNode mth) { private static boolean removeBridgeMethod(ClassNode cls, MethodNode mth) {
if (cls.root().getArgs().isRenameValid()) { if (cls.root().getArgs().isInlineMethods()) { // simple wrapper remove is same as inline
List<InsnNode> allInsns = BlockUtils.collectAllInsns(mth.getBasicBlocks()); List<InsnNode> allInsns = BlockUtils.collectAllInsns(mth.getBasicBlocks());
if (allInsns.size() == 1) { if (allInsns.size() == 1) {
InsnNode wrappedInsn = allInsns.get(0); InsnNode wrappedInsn = allInsns.get(0);
@@ -235,12 +236,10 @@ public class ClassModifier extends AbstractVisitor {
wrappedInsn = ((InsnWrapArg) arg).getWrapInsn(); wrappedInsn = ((InsnWrapArg) arg).getWrapInsn();
} }
} }
if (checkSyntheticWrapper(mth, wrappedInsn)) { return checkSyntheticWrapper(mth, wrappedInsn);
return true;
}
} }
} }
return !isMethodUnique(cls, mth); return false;
} }
private static boolean checkSyntheticWrapper(MethodNode mth, InsnNode insn) { private static boolean checkSyntheticWrapper(MethodNode mth, InsnNode insn) {
@@ -283,6 +282,9 @@ public class ClassModifier extends AbstractVisitor {
if (!Objects.equals(wrappedMth.getAlias(), alias)) { if (!Objects.equals(wrappedMth.getAlias(), alias)) {
wrappedMth.getMethodInfo().setAlias(alias); wrappedMth.getMethodInfo().setAlias(alias);
} }
wrappedMth.addAttr(new MethodReplaceAttr(mth));
wrappedMth.copyAttributeFrom(mth, AType.METHOD_OVERRIDE);
wrappedMth.addDebugComment("Method merged with bridge method");
return true; return true;
} }
@@ -299,20 +301,6 @@ public class ClassModifier extends AbstractVisitor {
return false; return false;
} }
private static boolean isMethodUnique(ClassNode cls, MethodNode mth) {
MethodInfo mi = mth.getMethodInfo();
for (MethodNode otherMth : cls.getMethods()) {
if (otherMth != mth) {
MethodInfo omi = otherMth.getMethodInfo();
if (omi.getName().equals(mi.getName())
&& Objects.equals(omi.getArgumentsTypes(), mi.getArgumentsTypes())) {
return false;
}
}
}
return true;
}
/** /**
* Remove public empty constructors (static or default) * Remove public empty constructors (static or default)
*/ */
@@ -18,6 +18,7 @@ import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.EnumClassAttr; 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.RenameReasonAttr;
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;
@@ -26,6 +27,7 @@ import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.instructions.IndexInsnNode; import jadx.core.dex.instructions.IndexInsnNode;
import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.InsnType;
import jadx.core.dex.instructions.InvokeNode; import jadx.core.dex.instructions.InvokeNode;
import jadx.core.dex.instructions.InvokeType;
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.instructions.args.InsnWrapArg; import jadx.core.dex.instructions.args.InsnWrapArg;
@@ -59,6 +61,7 @@ import static jadx.core.utils.InsnUtils.getWrappedInsn;
public class EnumVisitor extends AbstractVisitor { public class EnumVisitor extends AbstractVisitor {
private MethodInfo enumValueOfMth; private MethodInfo enumValueOfMth;
private MethodInfo cloneMth;
@Override @Override
public void init(RootNode root) { public void init(RootNode root) {
@@ -68,6 +71,12 @@ public class EnumVisitor extends AbstractVisitor {
"valueOf", "valueOf",
Arrays.asList(ArgType.CLASS, ArgType.STRING), Arrays.asList(ArgType.CLASS, ArgType.STRING),
ArgType.ENUM); ArgType.ENUM);
cloneMth = MethodInfo.fromDetails(root,
ClassInfo.fromType(root, ArgType.OBJECT),
"clone",
Collections.emptyList(),
ArgType.OBJECT);
} }
@Override @Override
@@ -377,6 +386,7 @@ public class EnumVisitor extends AbstractVisitor {
return enumFieldNode; return enumFieldNode;
} }
@SuppressWarnings("StatementWithEmptyBody")
private EnumField createEnumFieldByConstructor(ClassNode cls, FieldNode enumFieldNode, ConstructorInsn co) { private EnumField createEnumFieldByConstructor(ClassNode cls, 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 for one field enum second arg can be omitted // sometimes for one field enum second arg can be omitted
@@ -417,13 +427,12 @@ public class EnumVisitor extends AbstractVisitor {
} }
private void removeEnumMethods(ClassNode cls, ArgType clsType, FieldNode valuesField) { private void removeEnumMethods(ClassNode cls, ArgType clsType, FieldNode valuesField) {
String valuesMethod = "values()" + TypeGen.signature(ArgType.array(clsType)); String valuesMethodShortId = "values()" + TypeGen.signature(ArgType.array(clsType));
FieldInfo valuesFieldInfo = valuesField.getFieldInfo(); MethodNode valuesMethod = null;
// remove compiler generated methods // remove compiler generated methods
for (MethodNode mth : cls.getMethods()) { for (MethodNode mth : cls.getMethods()) {
MethodInfo mi = mth.getMethodInfo(); MethodInfo mi = mth.getMethodInfo();
if (mi.isClassInit()) { if (mi.isClassInit() || mth.isNoCode()) {
continue; continue;
} }
String shortId = mi.getShortId(); String shortId = mi.getShortId();
@@ -432,12 +441,33 @@ public class EnumVisitor extends AbstractVisitor {
mth.add(AFlag.DONT_GENERATE); mth.add(AFlag.DONT_GENERATE);
} }
markArgsForSkip(mth); markArgsForSkip(mth);
} else if (shortId.equals(valuesMethod) } else if (mi.getShortId().equals(valuesMethodShortId)) {
|| usesValuesField(mth, valuesFieldInfo) if (isValuesMethod(mth, clsType)) {
|| simpleValueOfMth(mth, clsType)) { valuesMethod = mth;
mth.add(AFlag.DONT_GENERATE);
} else {
// custom values method => rename to resolve conflict with enum method
mth.getMethodInfo().setAlias("valuesCustom");
mth.addAttr(new RenameReasonAttr(mth).append("to resolve conflict with enum method"));
}
} else if (isValuesMethod(mth, clsType)) {
if (!mth.getMethodInfo().getAlias().equals("values") && !mth.getUseIn().isEmpty()) {
// rename to use default values method
mth.getMethodInfo().setAlias("values");
mth.addAttr(new RenameReasonAttr(mth).append("to match enum method name"));
mth.add(AFlag.DONT_RENAME);
}
valuesMethod = mth;
mth.add(AFlag.DONT_GENERATE);
} else if (simpleValueOfMth(mth, clsType)) {
mth.add(AFlag.DONT_GENERATE); mth.add(AFlag.DONT_GENERATE);
} }
} }
FieldInfo valuesFieldInfo = valuesField.getFieldInfo();
for (MethodNode mth : cls.getMethods()) {
// fix access to 'values' field and 'values()' method
fixValuesAccess(mth, valuesFieldInfo, clsType, valuesMethod);
}
} }
private void markArgsForSkip(MethodNode mth) { private void markArgsForSkip(MethodNode mth) {
@@ -458,6 +488,25 @@ public class EnumVisitor extends AbstractVisitor {
return false; return false;
} }
// TODO: support other method patterns ???
private boolean isValuesMethod(MethodNode mth, ArgType clsType) {
ArgType retType = mth.getReturnType();
if (!retType.isArray() || !retType.getArrayElement().equals(clsType)) {
return false;
}
InsnNode returnInsn = BlockUtils.getOnlyOneInsnFromMth(mth);
if (returnInsn == null || returnInsn.getType() != InsnType.RETURN || returnInsn.getArgsCount() != 1) {
return false;
}
InsnNode wrappedInsn = getWrappedInsn(getSingleArg(returnInsn));
IndexInsnNode castInsn = (IndexInsnNode) checkInsnType(wrappedInsn, InsnType.CHECK_CAST);
if (castInsn != null && Objects.equals(castInsn.getIndex(), ArgType.array(clsType))) {
InvokeNode invokeInsn = (InvokeNode) checkInsnType(getWrappedInsn(getSingleArg(castInsn)), InsnType.INVOKE);
return invokeInsn != null && invokeInsn.getCallMth().equals(cloneMth);
}
return false;
}
private boolean simpleValueOfMth(MethodNode mth, ArgType clsType) { private boolean simpleValueOfMth(MethodNode mth, ArgType clsType) {
InsnNode returnInsn = InsnUtils.searchSingleReturnInsn(mth, insn -> insn.getArgsCount() == 1); InsnNode returnInsn = InsnUtils.searchSingleReturnInsn(mth, insn -> insn.getArgsCount() == 1);
if (returnInsn == null) { if (returnInsn == null) {
@@ -472,9 +521,41 @@ public class EnumVisitor extends AbstractVisitor {
return false; return false;
} }
private boolean usesValuesField(MethodNode mth, FieldInfo valuesFieldInfo) { private void fixValuesAccess(MethodNode mth, FieldInfo valuesFieldInfo, ArgType clsType, @Nullable MethodNode valuesMethod) {
MethodInfo mi = mth.getMethodInfo();
if (mi.isConstructor() || mi.isClassInit() || mth.isNoCode() || mth == valuesMethod) {
return;
}
// search value field usage
Predicate<InsnNode> insnTest = insn -> Objects.equals(((IndexInsnNode) insn).getIndex(), valuesFieldInfo); Predicate<InsnNode> insnTest = insn -> Objects.equals(((IndexInsnNode) insn).getIndex(), valuesFieldInfo);
return InsnUtils.searchInsn(mth, InsnType.SGET, insnTest) != null; InsnNode useInsn = InsnUtils.searchInsn(mth, InsnType.SGET, insnTest);
if (useInsn == null) {
return;
}
// replace 'values' field with 'values()' method
InsnUtils.replaceInsns(mth, insn -> {
if (insn.getType() == InsnType.SGET && insnTest.test(insn)) {
MethodInfo valueMth = valuesMethod == null
? getValueMthInfo(mth.root(), clsType)
: valuesMethod.getMethodInfo();
InvokeNode invokeNode = new InvokeNode(valueMth, InvokeType.STATIC, 0);
invokeNode.setResult(insn.getResult());
if (valuesMethod == null) {
// forcing enum method (can overlap and get renamed by custom method)
invokeNode.add(AFlag.FORCE_RAW_NAME);
}
mth.addDebugComment("Replace access to removed values field (" + valuesFieldInfo.getName() + ") with 'values()' method");
return invokeNode;
}
return null;
});
}
private MethodInfo getValueMthInfo(RootNode root, ArgType clsType) {
return MethodInfo.fromDetails(root,
ClassInfo.fromType(root, clsType),
"values",
Collections.emptyList(), ArgType.array(clsType));
} }
private static void processEnumCls(ClassNode cls, EnumField field, ClassNode innerCls) { private static void processEnumCls(ClassNode cls, EnumField field, ClassNode innerCls) {
@@ -362,8 +362,7 @@ public class ModVisitor extends AbstractVisitor {
private static void removeCheckCast(MethodNode mth, BlockNode block, int i, IndexInsnNode insn) { private static void removeCheckCast(MethodNode mth, BlockNode block, int i, IndexInsnNode insn) {
InsnArg castArg = insn.getArg(0); InsnArg castArg = insn.getArg(0);
ArgType castType = (ArgType) insn.getIndex(); ArgType castType = (ArgType) insn.getIndex();
if (!ArgType.isCastNeeded(mth.root(), castArg.getType(), castType) if (!ArgType.isCastNeeded(mth.root(), castArg.getType(), castType)) {
|| isCastDuplicate(insn)) {
RegisterArg result = insn.getResult(); RegisterArg result = insn.getResult();
result.setType(castArg.getType()); result.setType(castArg.getType());
@@ -371,10 +370,19 @@ public class ModVisitor extends AbstractVisitor {
move.setResult(result); move.setResult(result);
move.addArg(castArg); move.addArg(castArg);
replaceInsn(mth, block, i, move); replaceInsn(mth, block, i, move);
return;
}
InsnNode prevCast = isCastDuplicate(insn);
if (prevCast != null) {
// replace previous cast with move
InsnNode move = new InsnNode(InsnType.MOVE, 1);
move.setResult(prevCast.getResult());
move.addArg(prevCast.getArg(0));
replaceInsn(mth, block, prevCast, move);
} }
} }
private static boolean isCastDuplicate(IndexInsnNode castInsn) { private static @Nullable InsnNode isCastDuplicate(IndexInsnNode castInsn) {
InsnArg arg = castInsn.getArg(0); InsnArg arg = castInsn.getArg(0);
if (arg.isRegister()) { if (arg.isRegister()) {
SSAVar sVar = ((RegisterArg) arg).getSVar(); SSAVar sVar = ((RegisterArg) arg).getSVar();
@@ -382,11 +390,13 @@ public class ModVisitor extends AbstractVisitor {
InsnNode assignInsn = sVar.getAssign().getParentInsn(); InsnNode assignInsn = sVar.getAssign().getParentInsn();
if (assignInsn != null && assignInsn.getType() == InsnType.CHECK_CAST) { if (assignInsn != null && assignInsn.getType() == InsnType.CHECK_CAST) {
ArgType assignCastType = (ArgType) ((IndexInsnNode) assignInsn).getIndex(); ArgType assignCastType = (ArgType) ((IndexInsnNode) assignInsn).getIndex();
return assignCastType.equals(castInsn.getIndex()); if (assignCastType.equals(castInsn.getIndex())) {
return assignInsn;
}
} }
} }
} }
return false; return null;
} }
/** /**
@@ -50,7 +50,7 @@ public class BlockExceptionHandler {
return false; return false;
} }
BlockProcessor.updateCleanSuccessors(mth); BlockProcessor.updateCleanSuccessors(mth);
BlockProcessor.computeDominanceFrontier(mth); DominatorTree.computeDominanceFrontier(mth);
processCatchAttr(mth); processCatchAttr(mth);
initExcHandlers(mth); initExcHandlers(mth);
@@ -549,7 +549,7 @@ public class BlockExceptionHandler {
if (handler == resultHandler) { if (handler == resultHandler) {
return false; return false;
} }
resultHandler.addCatchTypes(handler.getCatchTypes()); resultHandler.addCatchTypes(mth, handler.getCatchTypes());
handler.markForRemove(); handler.markForRemove();
return true; return true;
}); });
@@ -1,9 +1,6 @@
package jadx.core.dex.visitors.blocks; package jadx.core.dex.visitors.blocks;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collections;
import java.util.Deque;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@@ -31,7 +28,6 @@ import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.exceptions.JadxRuntimeException;
import static jadx.core.dex.visitors.blocks.BlockSplitter.connect; import static jadx.core.dex.visitors.blocks.BlockSplitter.connect;
import static jadx.core.utils.EmptyBitSet.EMPTY;
public class BlockProcessor extends AbstractVisitor { public class BlockProcessor extends AbstractVisitor {
private static final Logger LOG = LoggerFactory.getLogger(BlockProcessor.class); private static final Logger LOG = LoggerFactory.getLogger(BlockProcessor.class);
@@ -50,29 +46,23 @@ public class BlockProcessor extends AbstractVisitor {
computeDominators(mth); computeDominators(mth);
if (independentBlockTreeMod(mth)) { if (independentBlockTreeMod(mth)) {
checkForUnreachableBlocks(mth); checkForUnreachableBlocks(mth);
clearBlocksState(mth);
computeDominators(mth); computeDominators(mth);
} }
if (FixMultiEntryLoops.process(mth)) { if (FixMultiEntryLoops.process(mth)) {
clearBlocksState(mth);
computeDominators(mth); computeDominators(mth);
} }
updateCleanSuccessors(mth); updateCleanSuccessors(mth);
int i = 0; int i = 0;
while (modifyBlocksTree(mth)) { while (modifyBlocksTree(mth)) {
// revert calculations
clearBlocksState(mth);
// recalculate dominators tree
computeDominators(mth); computeDominators(mth);
if (i++ > 100) { if (i++ > 100) {
throw new JadxRuntimeException("CFG modification limit reached, blocks count: " + mth.getBasicBlocks().size()); throw new JadxRuntimeException("CFG modification limit reached, blocks count: " + mth.getBasicBlocks().size());
} }
} }
checkForUnreachableBlocks(mth); checkForUnreachableBlocks(mth);
computeDominanceFrontier(mth); DominatorTree.computeDominanceFrontier(mth);
registerLoops(mth); registerLoops(mth);
processNestedLoops(mth); processNestedLoops(mth);
@@ -209,139 +199,9 @@ public class BlockProcessor extends AbstractVisitor {
} }
private static void computeDominators(MethodNode mth) { private static void computeDominators(MethodNode mth) {
List<BlockNode> basicBlocks = mth.getBasicBlocks(); clearBlocksState(mth);
int nBlocks = basicBlocks.size(); DominatorTree.compute(mth);
for (int i = 0; i < nBlocks; i++) {
BlockNode block = basicBlocks.get(i);
block.setId(i);
block.setDoms(new BitSet(nBlocks));
block.getDoms().set(0, nBlocks);
}
BlockNode entryBlock = mth.getEnterBlock();
calcDominators(basicBlocks, entryBlock);
markLoops(mth); markLoops(mth);
// clear self dominance
basicBlocks.forEach(block -> {
block.getDoms().clear(block.getId());
if (block.getDoms().isEmpty()) {
block.setDoms(EMPTY);
}
});
calcImmediateDominators(mth, basicBlocks, entryBlock);
}
private static void calcDominators(List<BlockNode> basicBlocks, BlockNode entryBlock) {
entryBlock.getDoms().clear();
entryBlock.getDoms().set(entryBlock.getId());
BitSet domSet = new BitSet(basicBlocks.size());
boolean changed;
do {
changed = false;
for (BlockNode block : basicBlocks) {
if (block == entryBlock) {
continue;
}
BitSet d = block.getDoms();
if (!changed) {
domSet.clear();
domSet.or(d);
}
for (BlockNode pred : block.getPredecessors()) {
d.and(pred.getDoms());
}
d.set(block.getId());
if (!changed && !d.equals(domSet)) {
changed = true;
}
}
} while (changed);
}
private static void calcImmediateDominators(MethodNode mth, List<BlockNode> basicBlocks, BlockNode entryBlock) {
for (BlockNode block : basicBlocks) {
if (block == entryBlock) {
continue;
}
BlockNode idom;
List<BlockNode> preds = block.getPredecessors();
if (preds.size() == 1) {
idom = preds.get(0);
} else {
BitSet bs = new BitSet(block.getDoms().length());
bs.or(block.getDoms());
for (int i = bs.nextSetBit(0); i >= 0; i = bs.nextSetBit(i + 1)) {
BlockNode dom = basicBlocks.get(i);
bs.andNot(dom.getDoms());
}
if (bs.cardinality() != 1) {
throw new JadxRuntimeException("Can't find immediate dominator for block " + block
+ " in " + bs + " preds:" + preds);
}
idom = basicBlocks.get(bs.nextSetBit(0));
}
block.setIDom(idom);
idom.addDominatesOn(block);
}
}
static void computeDominanceFrontier(MethodNode mth) {
mth.getExitBlock().setDomFrontier(EMPTY);
List<BlockNode> domSortedBlocks = new ArrayList<>(mth.getBasicBlocks().size());
Deque<BlockNode> stack = new LinkedList<>();
stack.push(mth.getEnterBlock());
while (!stack.isEmpty()) {
BlockNode node = stack.pop();
for (BlockNode dominated : node.getDominatesOn()) {
stack.push(dominated);
}
domSortedBlocks.add(node);
}
Collections.reverse(domSortedBlocks);
for (BlockNode block : domSortedBlocks) {
try {
computeBlockDF(mth, block);
} catch (Exception e) {
throw new JadxRuntimeException("Failed compute block dominance frontier", e);
}
}
}
private static void computeBlockDF(MethodNode mth, BlockNode block) {
if (block.getDomFrontier() != null) {
return;
}
List<BlockNode> blocks = mth.getBasicBlocks();
BitSet domFrontier = null;
for (BlockNode s : block.getSuccessors()) {
if (s.getIDom() != block) {
if (domFrontier == null) {
domFrontier = new BitSet(blocks.size());
}
domFrontier.set(s.getId());
}
}
for (BlockNode c : block.getDominatesOn()) {
BitSet frontier = c.getDomFrontier();
if (frontier == null) {
throw new JadxRuntimeException("Dominance frontier not calculated for dominated block: " + c + ", from: " + block);
}
for (int p = frontier.nextSetBit(0); p >= 0; p = frontier.nextSetBit(p + 1)) {
if (blocks.get(p).getIDom() != block) {
if (domFrontier == null) {
domFrontier = new BitSet(blocks.size());
}
domFrontier.set(p);
}
}
}
if (domFrontier == null || domFrontier.isEmpty()) {
domFrontier = EMPTY;
}
block.setDomFrontier(domFrontier);
} }
private static void markLoops(MethodNode mth) { private static void markLoops(MethodNode mth) {
@@ -349,7 +209,7 @@ public class BlockProcessor extends AbstractVisitor {
// Every successor that dominates its predecessor is a header of a loop, // Every successor that dominates its predecessor is a header of a loop,
// block -> successor is a back edge. // block -> successor is a back edge.
block.getSuccessors().forEach(successor -> { block.getSuccessors().forEach(successor -> {
if (block.getDoms().get(successor.getId())) { if (block.getDoms().get(successor.getId()) || block == successor) {
successor.add(AFlag.LOOP_START); successor.add(AFlag.LOOP_START);
block.add(AFlag.LOOP_END); block.add(AFlag.LOOP_END);
@@ -0,0 +1,175 @@
package jadx.core.dex.visitors.blocks;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.List;
import org.jetbrains.annotations.NotNull;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.BlockUtils;
import jadx.core.utils.EmptyBitSet;
import jadx.core.utils.exceptions.JadxRuntimeException;
/**
* Build dominator tree based on the algorithm described in paper:
* Cooper, Keith D.; Harvey, Timothy J; Kennedy, Ken (2001).
* "A Simple, Fast Dominance Algorithm"
* http://www.hipersoft.rice.edu/grads/publications/dom14.pdf
*/
@SuppressWarnings("JavadocLinkAsPlainText")
public class DominatorTree {
public static void compute(MethodNode mth) {
List<BlockNode> sorted = sortBlocks(mth);
BlockNode[] doms = build(sorted);
apply(sorted, doms);
mth.setBasicBlocks(sorted);
}
private static List<BlockNode> sortBlocks(MethodNode mth) {
int blocksCount = mth.getBasicBlocks().size();
BitSet reachSet = new BitSet(blocksCount);
List<BlockNode> sorted = new ArrayList<>(blocksCount);
BlockUtils.dfsVisit(mth, b -> {
sorted.add(b);
reachSet.set(b.getId());
});
if (reachSet.cardinality() != blocksCount) {
throw new JadxRuntimeException("Found unreachable blocks");
}
for (int i = 0; i < blocksCount; i++) {
sorted.get(i).setId(i);
}
return sorted;
}
@NotNull
private static BlockNode[] build(List<BlockNode> sorted) {
int blocksCount = sorted.size();
BlockNode[] doms = new BlockNode[blocksCount];
doms[0] = sorted.get(0);
boolean changed = true;
while (changed) {
changed = false;
for (int blockId = 1; blockId < blocksCount; blockId++) {
BlockNode b = sorted.get(blockId);
List<BlockNode> preds = b.getPredecessors();
int pickedPred = -1;
BlockNode newIDom = null;
for (BlockNode pred : preds) {
int id = pred.getId();
if (doms[id] != null) {
newIDom = pred;
pickedPred = id;
break;
}
}
if (newIDom == null) {
throw new JadxRuntimeException("No predecessors for block: " + b);
}
for (BlockNode predBlock : preds) {
int predId = predBlock.getId();
if (predId == pickedPred) {
continue;
}
if (doms[predId] != null) {
newIDom = intersect(sorted, doms, predBlock, newIDom);
}
}
if (doms[blockId] != newIDom) {
doms[blockId] = newIDom;
changed = true;
}
}
}
return doms;
}
private static BlockNode intersect(List<BlockNode> sorted, BlockNode[] doms, BlockNode b1, BlockNode b2) {
int f1 = b1.getId();
int f2 = b2.getId();
while (f1 != f2) {
while (f1 > f2) {
f1 = doms[f1].getId();
}
while (f2 > f1) {
f2 = doms[f2].getId();
}
}
return sorted.get(f1);
}
private static void apply(List<BlockNode> sorted, BlockNode[] doms) {
BlockNode enterBlock = sorted.get(0);
enterBlock.setDoms(EmptyBitSet.EMPTY);
enterBlock.setIDom(null);
int blocksCount = sorted.size();
for (int i = 1; i < blocksCount; i++) {
BlockNode block = sorted.get(i);
BlockNode idom = doms[i];
block.setIDom(idom);
idom.addDominatesOn(block);
BitSet domBS = collectDoms(doms, idom);
domBS.clear(i);
block.setDoms(domBS);
}
}
private static BitSet collectDoms(BlockNode[] doms, BlockNode idom) {
BitSet domBS = new BitSet(doms.length);
BlockNode nextIDom = idom;
while (true) {
int id = nextIDom.getId();
if (domBS.get(id)) {
break;
}
domBS.set(id);
BitSet curDoms = nextIDom.getDoms();
if (curDoms != null) {
// use already collected set
domBS.or(curDoms);
break;
}
nextIDom = doms[id];
}
return domBS;
}
public static void computeDominanceFrontier(MethodNode mth) {
List<BlockNode> blocks = mth.getBasicBlocks();
for (BlockNode block : blocks) {
block.setDomFrontier(null);
}
int blocksCount = blocks.size();
for (BlockNode block : blocks) {
List<BlockNode> preds = block.getPredecessors();
if (preds.size() >= 2) {
BlockNode idom = block.getIDom();
for (BlockNode pred : preds) {
BlockNode runner = pred;
while (runner != idom) {
addToDF(runner, block, blocksCount);
runner = runner.getIDom();
}
}
}
}
for (BlockNode block : blocks) {
BitSet df = block.getDomFrontier();
if (df == null || df.isEmpty()) {
block.setDomFrontier(EmptyBitSet.EMPTY);
}
}
}
private static void addToDF(BlockNode block, BlockNode dfBlock, int blocksCount) {
BitSet df = block.getDomFrontier();
if (df == null) {
df = new BitSet(blocksCount);
block.setDomFrontier(df);
}
df.set(dfBlock.getId());
}
}
@@ -6,9 +6,12 @@ import java.util.List;
import java.util.Set; import java.util.Set;
import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.trycatch.ExceptionHandler; import jadx.core.dex.trycatch.ExceptionHandler;
import jadx.core.utils.Utils;
public class FinallyExtractInfo { public class FinallyExtractInfo {
private final MethodNode mth;
private final ExceptionHandler finallyHandler; private final ExceptionHandler finallyHandler;
private final List<BlockNode> allHandlerBlocks; private final List<BlockNode> allHandlerBlocks;
private final List<InsnsSlice> duplicateSlices = new ArrayList<>(); private final List<InsnsSlice> duplicateSlices = new ArrayList<>();
@@ -16,12 +19,17 @@ public class FinallyExtractInfo {
private final InsnsSlice finallyInsnsSlice = new InsnsSlice(); private final InsnsSlice finallyInsnsSlice = new InsnsSlice();
private final BlockNode startBlock; private final BlockNode startBlock;
public FinallyExtractInfo(ExceptionHandler finallyHandler, BlockNode startBlock, List<BlockNode> allHandlerBlocks) { public FinallyExtractInfo(MethodNode mth, ExceptionHandler finallyHandler, BlockNode startBlock, List<BlockNode> allHandlerBlocks) {
this.mth = mth;
this.finallyHandler = finallyHandler; this.finallyHandler = finallyHandler;
this.startBlock = startBlock; this.startBlock = startBlock;
this.allHandlerBlocks = allHandlerBlocks; this.allHandlerBlocks = allHandlerBlocks;
} }
public MethodNode getMth() {
return mth;
}
public ExceptionHandler getFinallyHandler() { public ExceptionHandler getFinallyHandler() {
return finallyHandler; return finallyHandler;
} }
@@ -45,4 +53,12 @@ public class FinallyExtractInfo {
public BlockNode getStartBlock() { public BlockNode getStartBlock() {
return startBlock; return startBlock;
} }
@Override
public String toString() {
return "FinallyExtractInfo{"
+ "\n finally:\n " + finallyInsnsSlice
+ "\n dups:\n " + Utils.listToString(duplicateSlices, "\n ")
+ "\n}";
}
} }
@@ -9,6 +9,7 @@ import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
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.instructions.InsnType; import jadx.core.dex.instructions.InsnType;
@@ -38,6 +39,7 @@ import jadx.core.utils.Utils;
) )
public class MarkFinallyVisitor extends AbstractVisitor { public class MarkFinallyVisitor extends AbstractVisitor {
private static final Logger LOG = LoggerFactory.getLogger(MarkFinallyVisitor.class); private static final Logger LOG = LoggerFactory.getLogger(MarkFinallyVisitor.class);
// private static final Logger LOG = LoggerFactory.getLogger(MarkFinallyVisitor.class);
@Override @Override
public void visit(MethodNode mth) { public void visit(MethodNode mth) {
@@ -60,7 +62,7 @@ public class MarkFinallyVisitor extends AbstractVisitor {
} }
} }
} catch (Exception e) { } catch (Exception e) {
LOG.warn("Undo finally extract visitor, mth: {}", mth, e); mth.addWarnComment("Undo finally extract visitor", e);
undoFinallyVisitor(mth); undoFinallyVisitor(mth);
} }
} }
@@ -100,20 +102,23 @@ public class MarkFinallyVisitor extends AbstractVisitor {
List<BlockNode> handlerBlocks = List<BlockNode> handlerBlocks =
new ArrayList<>(BlockUtils.collectBlocksDominatedByWithExcHandlers(mth, handlerBlock, handlerBlock)); new ArrayList<>(BlockUtils.collectBlocksDominatedByWithExcHandlers(mth, handlerBlock, handlerBlock));
handlerBlocks.remove(handlerBlock); // exclude block with 'move-exception' handlerBlocks.remove(handlerBlock); // exclude block with 'move-exception'
handlerBlocks.removeIf(b -> BlockUtils.checkLastInsnType(b, InsnType.THROW)); cutPathEnds(mth, handlerBlocks);
if (handlerBlocks.isEmpty() || BlockUtils.isAllBlocksEmpty(handlerBlocks)) { if (handlerBlocks.isEmpty() || BlockUtils.isAllBlocksEmpty(handlerBlocks)) {
// remove empty catch // remove empty catch
allHandler.getTryBlock().removeHandler(allHandler); allHandler.getTryBlock().removeHandler(allHandler);
return true; return true;
} }
BlockNode startBlock = Utils.getOne(handlerBlock.getCleanSuccessors()); BlockNode startBlock = Utils.getOne(handlerBlock.getCleanSuccessors());
FinallyExtractInfo extractInfo = new FinallyExtractInfo(allHandler, startBlock, handlerBlocks); FinallyExtractInfo extractInfo = new FinallyExtractInfo(mth, allHandler, startBlock, handlerBlocks);
if (Consts.DEBUG_FINALLY) {
LOG.debug("Finally info: handler=({}), start={}, blocks={}", allHandler, startBlock, handlerBlocks);
}
boolean hasInnerBlocks = !tryBlock.getInnerTryBlocks().isEmpty(); boolean hasInnerBlocks = !tryBlock.getInnerTryBlocks().isEmpty();
List<ExceptionHandler> handlers; List<ExceptionHandler> handlers;
if (hasInnerBlocks) { if (hasInnerBlocks) {
// collect handlers from this and all inner blocks (intentionally not using recursive collect for // collect handlers from this and all inner blocks
// now) // (intentionally not using recursive collect for now)
handlers = new ArrayList<>(tryBlock.getHandlers()); handlers = new ArrayList<>(tryBlock.getHandlers());
for (TryCatchBlockAttr innerTryBlock : tryBlock.getInnerTryBlocks()) { for (TryCatchBlockAttr innerTryBlock : tryBlock.getInnerTryBlocks()) {
handlers.addAll(innerTryBlock.getHandlers()); handlers.addAll(innerTryBlock.getHandlers());
@@ -137,10 +142,12 @@ public class MarkFinallyVisitor extends AbstractVisitor {
} }
} }
} }
if (Consts.DEBUG_FINALLY) {
LOG.debug("Handlers slices:\n{}", extractInfo);
}
boolean mergeInnerTryBlocks; boolean mergeInnerTryBlocks;
int duplicatesCount = extractInfo.getDuplicateSlices().size(); int duplicatesCount = extractInfo.getDuplicateSlices().size();
boolean fullTryBlock = duplicatesCount == (handlers.size() - 1); if (duplicatesCount == (handlers.size() - 1)) {
if (fullTryBlock) {
// all collected handlers have duplicate block // all collected handlers have duplicate block
mergeInnerTryBlocks = hasInnerBlocks; mergeInnerTryBlocks = hasInnerBlocks;
} else { } else {
@@ -170,15 +177,24 @@ public class MarkFinallyVisitor extends AbstractVisitor {
if (upPath.size() < handlerBlocks.size()) { if (upPath.size() < handlerBlocks.size()) {
continue; continue;
} }
if (Consts.DEBUG_FINALLY) {
LOG.debug("Checking dup path starts: {} from {}", upPath, pred);
}
for (BlockNode block : upPath) { for (BlockNode block : upPath) {
if (searchDuplicateInsns(block, extractInfo)) { if (searchDuplicateInsns(block, extractInfo)) {
found = true; found = true;
if (Consts.DEBUG_FINALLY) {
LOG.debug("Found dup in: {} from {}", block, pred);
}
break; break;
} else { } else {
extractInfo.getFinallyInsnsSlice().resetIncomplete(); extractInfo.getFinallyInsnsSlice().resetIncomplete();
} }
} }
} }
if (Consts.DEBUG_FINALLY) {
LOG.debug("Result slices:\n{}", extractInfo);
}
if (!found) { if (!found) {
return false; return false;
} }
@@ -204,6 +220,28 @@ public class MarkFinallyVisitor extends AbstractVisitor {
return true; return true;
} }
private static void cutPathEnds(MethodNode mth, List<BlockNode> handlerBlocks) {
List<BlockNode> throwBlocks = ListUtils.filter(handlerBlocks,
b -> BlockUtils.checkLastInsnType(b, InsnType.THROW));
if (throwBlocks.size() != 1) {
mth.addDebugComment("Finally have unexpected throw blocks count: " + throwBlocks.size() + ", expect 1");
return;
}
BlockNode throwBlock = throwBlocks.get(0);
handlerBlocks.remove(throwBlock);
removeEmptyUpPath(handlerBlocks, throwBlock);
}
private static void removeEmptyUpPath(List<BlockNode> handlerBlocks, BlockNode startBlock) {
for (BlockNode pred : startBlock.getPredecessors()) {
if (pred.isEmpty()) {
if (handlerBlocks.remove(pred) && !BlockUtils.isBackEdge(pred, startBlock)) {
removeEmptyUpPath(handlerBlocks, pred);
}
}
}
}
private static List<BlockNode> getPathStarts(MethodNode mth, BlockNode bottom, BlockNode bottomFinallyBlock) { private static List<BlockNode> getPathStarts(MethodNode mth, BlockNode bottom, BlockNode bottomFinallyBlock) {
Stream<BlockNode> preds = bottom.getPredecessors().stream().filter(b -> b != bottomFinallyBlock); Stream<BlockNode> preds = bottom.getPredecessors().stream().filter(b -> b != bottomFinallyBlock);
if (bottom == mth.getExitBlock()) { if (bottom == mth.getExitBlock()) {
@@ -219,9 +257,8 @@ public class MarkFinallyVisitor extends AbstractVisitor {
for (InsnsSlice dupSlice : extractInfo.getDuplicateSlices()) { for (InsnsSlice dupSlice : extractInfo.getDuplicateSlices()) {
List<InsnNode> dupInsnsList = dupSlice.getInsnsList(); List<InsnNode> dupInsnsList = dupSlice.getInsnsList();
if (dupInsnsList.size() != finallyInsnsList.size()) { if (dupInsnsList.size() != finallyInsnsList.size()) {
if (LOG.isDebugEnabled()) { extractInfo.getMth().addDebugComment(
LOG.debug("Incorrect finally slice size: {}, expected: {}", dupSlice, finallySlice); "Incorrect finally slice size: " + dupSlice + ", expected: " + finallySlice);
}
return false; return false;
} }
} }
@@ -231,9 +268,8 @@ public class MarkFinallyVisitor extends AbstractVisitor {
List<InsnNode> insnsList = dupSlice.getInsnsList(); List<InsnNode> insnsList = dupSlice.getInsnsList();
InsnNode dupInsn = insnsList.get(i); InsnNode dupInsn = insnsList.get(i);
if (finallyInsn.getType() != dupInsn.getType()) { if (finallyInsn.getType() != dupInsn.getType()) {
if (LOG.isDebugEnabled()) { extractInfo.getMth().addDebugComment(
LOG.debug("Incorrect finally slice insn: {}, expected: {}", dupInsn, finallyInsn); "Incorrect finally slice insn: " + dupInsn + ", expected: " + finallyInsn);
}
return false; return false;
} }
} }
@@ -342,24 +378,29 @@ public class MarkFinallyVisitor extends AbstractVisitor {
private static InsnsSlice isStartBlock(BlockNode dupBlock, BlockNode finallyBlock, FinallyExtractInfo extractInfo) { private static InsnsSlice isStartBlock(BlockNode dupBlock, BlockNode finallyBlock, FinallyExtractInfo extractInfo) {
List<InsnNode> dupInsns = dupBlock.getInstructions(); List<InsnNode> dupInsns = dupBlock.getInstructions();
List<InsnNode> finallyInsns = finallyBlock.getInstructions(); List<InsnNode> finallyInsns = finallyBlock.getInstructions();
if (dupInsns.size() < finallyInsns.size()) { int dupSize = dupInsns.size();
int finSize = finallyInsns.size();
if (dupSize < finSize) {
return null; return null;
} }
int startPos = dupInsns.size() - finallyInsns.size(); int startPos;
int endPos = 0; int endPos = 0;
// fast check from end of block if (dupSize == finSize) {
if (!checkInsns(dupInsns, finallyInsns, startPos)) { if (!checkInsns(dupInsns, finallyInsns, 0)) {
// check from block start return null;
if (checkInsns(dupInsns, finallyInsns, 0)) { }
startPos = 0; startPos = 0;
endPos = finallyInsns.size(); } else {
} else { // dupSize > finSize
startPos = dupSize - finSize;
// fast check from end of block
if (!checkInsns(dupInsns, finallyInsns, startPos)) {
// search start insn // search start insn
boolean found = false; boolean found = false;
for (int i = 1; i < startPos; i++) { for (int i = 1; i < startPos; i++) {
if (checkInsns(dupInsns, finallyInsns, i)) { if (checkInsns(dupInsns, finallyInsns, i)) {
startPos = i; startPos = i;
endPos = finallyInsns.size() + i; endPos = finSize + i;
found = true; found = true;
break; break;
} }
@@ -379,7 +420,7 @@ public class MarkFinallyVisitor extends AbstractVisitor {
// both slices completed // both slices completed
complete = true; complete = true;
} else { } else {
endIndex = dupInsns.size(); endIndex = dupSize;
complete = false; complete = false;
} }
@@ -393,9 +434,8 @@ public class MarkFinallyVisitor extends AbstractVisitor {
if (finallySlice.isComplete()) { if (finallySlice.isComplete()) {
// compare slices // compare slices
if (finallySlice.getInsnsList().size() != slice.getInsnsList().size()) { if (finallySlice.getInsnsList().size() != slice.getInsnsList().size()) {
if (LOG.isDebugEnabled()) { extractInfo.getMth().addDebugComment(
LOG.debug("Another duplicated slice has different insns count: {}, finally: {}", slice, finallySlice); "Another duplicated slice has different insns count: " + slice + ", finally: " + finallySlice);
}
return null; return null;
} }
// TODO: add additional slices checks // TODO: add additional slices checks
@@ -522,7 +562,7 @@ public class MarkFinallyVisitor extends AbstractVisitor {
DepthTraversal.visit(visitor, mth); DepthTraversal.visit(visitor, mth);
} }
} catch (Exception e) { } catch (Exception e) {
LOG.error("Undo finally extract failed, mth: {}", mth, e); mth.addError("Undo finally extract failed", e);
} }
} }
} }
@@ -14,9 +14,7 @@ import jadx.core.Consts;
import jadx.core.codegen.json.JsonMappingGen; import jadx.core.codegen.json.JsonMappingGen;
import jadx.core.deobf.Deobfuscator; import jadx.core.deobf.Deobfuscator;
import jadx.core.deobf.NameMapper; import jadx.core.deobf.NameMapper;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.nodes.RenameReasonAttr; import jadx.core.dex.attributes.nodes.RenameReasonAttr;
import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.info.FieldInfo; import jadx.core.dex.info.FieldInfo;
import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.ClassNode;
@@ -45,8 +43,8 @@ public class RenameVisitor extends AbstractVisitor {
deobfuscator.execute(); deobfuscator.execute();
} }
checkClasses(deobfuscator, root, args);
UserRenames.applyForNodes(root); UserRenames.applyForNodes(root);
checkClasses(deobfuscator, root, args);
if (args.isDeobfuscationOn() || !args.isJsonOutput()) { if (args.isDeobfuscationOn() || !args.isJsonOutput()) {
deobfuscator.savePresets(); deobfuscator.savePresets();
@@ -196,11 +194,6 @@ public class RenameVisitor extends AbstractVisitor {
if (args.isRenameValid()) { if (args.isRenameValid()) {
Set<String> names = new HashSet<>(methods.size()); Set<String> names = new HashSet<>(methods.size());
for (MethodNode mth : methods) { for (MethodNode mth : methods) {
AccessInfo accessFlags = mth.getAccessFlags();
if (accessFlags.isBridge() || accessFlags.isSynthetic()
|| mth.contains(AFlag.DONT_GENERATE) /* this flag not set yet */) {
continue;
}
String signature = mth.getMethodInfo().makeSignature(true, false); String signature = mth.getMethodInfo().makeSignature(true, false);
if (!names.add(signature)) { if (!names.add(signature)) {
deobfuscator.forceRenameMethod(mth); deobfuscator.forceRenameMethod(mth);
@@ -1013,6 +1013,9 @@ public class BlockUtils {
*/ */
@Nullable @Nullable
public static InsnNode getOnlyOneInsnFromMth(MethodNode mth) { public static InsnNode getOnlyOneInsnFromMth(MethodNode mth) {
if (mth.isNoCode()) {
return null;
}
InsnNode insn = null; InsnNode insn = null;
for (BlockNode block : mth.getBasicBlocks()) { for (BlockNode block : mth.getBasicBlocks()) {
List<InsnNode> blockInsns = block.getInstructions(); List<InsnNode> blockInsns = block.getInstructions();
@@ -246,4 +246,29 @@ public class DebugUtils {
Set<Object> seen = ConcurrentHashMap.newKeySet(); Set<Object> seen = ConcurrentHashMap.newKeySet();
return t -> seen.add(keyExtractor.apply(t)); return t -> seen.add(keyExtractor.apply(t));
} }
private static Map<String, Long> execTimes;
public static void initExecTimes() {
execTimes = new ConcurrentHashMap<>();
}
public static void mergeExecTimeFromStart(String tag, long startTimeMillis) {
mergeExecTime(tag, System.currentTimeMillis() - startTimeMillis);
}
public static void mergeExecTime(String tag, long execTimeMillis) {
execTimes.merge(tag, execTimeMillis, Long::sum);
}
public static void printExecTimes() {
System.out.println("Exec times:");
execTimes.forEach((tag, time) -> System.out.println(" " + tag + ": " + time + "ms"));
}
public static void printExecTimesWithTotal(long totalMillis) {
System.out.println("Exec times: total " + totalMillis + "ms");
execTimes.forEach((tag, time) -> System.out.println(" " + tag + ": " + time + "ms"
+ String.format(" (%.2f%%)", time * 100. / (double) totalMillis)));
}
} }
@@ -13,9 +13,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import jadx.api.IDecompileScheduler; import jadx.api.IDecompileScheduler;
import jadx.api.JadxDecompiler;
import jadx.api.JavaClass; import jadx.api.JavaClass;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.exceptions.JadxRuntimeException;
public class DecompilerScheduler implements IDecompileScheduler { public class DecompilerScheduler implements IDecompileScheduler {
@@ -24,18 +22,11 @@ public class DecompilerScheduler implements IDecompileScheduler {
private static final int MERGED_BATCH_SIZE = 16; private static final int MERGED_BATCH_SIZE = 16;
private static final boolean DEBUG_BATCHES = false; private static final boolean DEBUG_BATCHES = false;
private final JadxDecompiler decompiler;
public DecompilerScheduler(JadxDecompiler decompiler) {
this.decompiler = decompiler;
}
@Override @Override
public List<List<JavaClass>> buildBatches(List<JavaClass> classes) { public List<List<JavaClass>> buildBatches(List<JavaClass> classes) {
try { try {
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
List<List<ClassNode>> batches = internalBatches(Utils.collectionMap(classes, JavaClass::getClassNode)); List<List<JavaClass>> result = internalBatches(classes);
List<List<JavaClass>> result = Utils.collectionMap(batches, l -> Utils.collectionMapNoNull(l, decompiler::getJavaClassByNode));
if (LOG.isDebugEnabled()) { if (LOG.isDebugEnabled()) {
LOG.debug("Build decompilation batches in {}ms", System.currentTimeMillis() - start); LOG.debug("Build decompilation batches in {}ms", System.currentTimeMillis() - start);
} }
@@ -53,14 +44,14 @@ public class DecompilerScheduler implements IDecompileScheduler {
* Put classes with many dependencies at the end. * Put classes with many dependencies at the end.
* Build batches for dependencies of single class to avoid locking from another thread. * Build batches for dependencies of single class to avoid locking from another thread.
*/ */
public List<List<ClassNode>> internalBatches(List<ClassNode> classes) { public List<List<JavaClass>> internalBatches(List<JavaClass> classes) {
List<DepInfo> deps = sumDependencies(classes); List<DepInfo> deps = sumDependencies(classes);
Set<ClassNode> added = new HashSet<>(classes.size()); Set<JavaClass> added = new HashSet<>(classes.size());
Comparator<ClassNode> cmpDepSize = Comparator.comparingInt(ClassNode::getTotalDepsCount); Comparator<JavaClass> cmpDepSize = Comparator.comparingInt(JavaClass::getTotalDepsCount);
List<List<ClassNode>> result = new ArrayList<>(); List<List<JavaClass>> result = new ArrayList<>();
List<ClassNode> mergedBatch = new ArrayList<>(MERGED_BATCH_SIZE); List<JavaClass> mergedBatch = new ArrayList<>(MERGED_BATCH_SIZE);
for (DepInfo depInfo : deps) { for (DepInfo depInfo : deps) {
ClassNode cls = depInfo.getCls(); JavaClass cls = depInfo.getCls();
if (!added.add(cls)) { if (!added.add(cls)) {
continue; continue;
} }
@@ -73,9 +64,9 @@ public class DecompilerScheduler implements IDecompileScheduler {
mergedBatch = new ArrayList<>(MERGED_BATCH_SIZE); mergedBatch = new ArrayList<>(MERGED_BATCH_SIZE);
} }
} else { } else {
List<ClassNode> batch = new ArrayList<>(depsSize + 1); List<JavaClass> batch = new ArrayList<>(depsSize + 1);
for (ClassNode dep : cls.getDependencies()) { for (JavaClass dep : cls.getDependencies()) {
ClassNode topDep = dep.getTopParentClass(); JavaClass topDep = dep.getTopParentClass();
if (!added.contains(topDep)) { if (!added.contains(topDep)) {
batch.add(topDep); batch.add(topDep);
added.add(topDep); added.add(topDep);
@@ -95,11 +86,11 @@ public class DecompilerScheduler implements IDecompileScheduler {
return result; return result;
} }
private static List<DepInfo> sumDependencies(List<ClassNode> classes) { private static List<DepInfo> sumDependencies(List<JavaClass> classes) {
List<DepInfo> deps = new ArrayList<>(classes.size()); List<DepInfo> deps = new ArrayList<>(classes.size());
for (ClassNode cls : classes) { for (JavaClass cls : classes) {
int count = 0; int count = 0;
for (ClassNode dep : cls.getDependencies()) { for (JavaClass dep : cls.getDependencies()) {
count += 1 + dep.getTotalDepsCount(); count += 1 + dep.getTotalDepsCount();
} }
deps.add(new DepInfo(cls, count)); deps.add(new DepInfo(cls, count));
@@ -109,15 +100,15 @@ public class DecompilerScheduler implements IDecompileScheduler {
} }
private static final class DepInfo implements Comparable<DepInfo> { private static final class DepInfo implements Comparable<DepInfo> {
private final ClassNode cls; private final JavaClass cls;
private final int depsCount; private final int depsCount;
private DepInfo(ClassNode cls, int depsCount) { private DepInfo(JavaClass cls, int depsCount) {
this.cls = cls; this.cls = cls;
this.depsCount = depsCount; this.depsCount = depsCount;
} }
public ClassNode getCls() { public JavaClass getCls() {
return cls; return cls;
} }
@@ -129,7 +120,7 @@ public class DecompilerScheduler implements IDecompileScheduler {
public int compareTo(@NotNull DecompilerScheduler.DepInfo o) { public int compareTo(@NotNull DecompilerScheduler.DepInfo o) {
int deps = Integer.compare(depsCount, o.depsCount); int deps = Integer.compare(depsCount, o.depsCount);
if (deps == 0) { if (deps == 0) {
return cls.compareTo(o.cls); return cls.getClassNode().compareTo(o.cls.getClassNode());
} }
return deps; return deps;
} }
@@ -147,9 +138,9 @@ public class DecompilerScheduler implements IDecompileScheduler {
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
private void dumpBatchesStats(List<ClassNode> classes, List<List<ClassNode>> result, List<DepInfo> deps) { private void dumpBatchesStats(List<JavaClass> classes, List<List<JavaClass>> result, List<DepInfo> deps) {
double avg = result.stream().mapToInt(List::size).average().orElse(-1); double avg = result.stream().mapToInt(List::size).average().orElse(-1);
int maxSingleDeps = classes.stream().mapToInt(ClassNode::getTotalDepsCount).max().orElse(-1); int maxSingleDeps = classes.stream().mapToInt(JavaClass::getTotalDepsCount).max().orElse(-1);
int maxSubDeps = deps.stream().mapToInt(DepInfo::getDepsCount).max().orElse(-1); int maxSubDeps = deps.stream().mapToInt(DepInfo::getDepsCount).max().orElse(-1);
LOG.info("Batches stats:" LOG.info("Batches stats:"
+ "\n input classes: " + classes.size() + "\n input classes: " + classes.size()
@@ -1,6 +1,7 @@
package jadx.core.utils; package jadx.core.utils;
import java.util.List; import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate; import java.util.function.Predicate;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -140,6 +141,37 @@ public class InsnUtils {
return null; return null;
} }
public static void replaceInsns(MethodNode mth, Function<InsnNode, InsnNode> replaceFunction) {
for (BlockNode block : mth.getBasicBlocks()) {
List<InsnNode> insns = block.getInstructions();
int insnsCount = insns.size();
for (int i = 0; i < insnsCount; i++) {
InsnNode insn = insns.get(i);
replaceInsnsInInsn(mth, insn, replaceFunction);
InsnNode replace = replaceFunction.apply(insn);
if (replace != null) {
BlockUtils.replaceInsn(mth, block, i, replace);
}
}
}
}
public static void replaceInsnsInInsn(MethodNode mth, InsnNode insn, Function<InsnNode, InsnNode> replaceFunction) {
int argsCount = insn.getArgsCount();
for (int i = 0; i < argsCount; i++) {
InsnArg arg = insn.getArg(i);
if (arg.isInsnWrap()) {
InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn();
replaceInsnsInInsn(mth, wrapInsn, replaceFunction);
InsnNode replace = replaceFunction.apply(wrapInsn);
if (replace != null) {
InsnRemover.unbindArgUsage(mth, arg);
insn.setArg(i, InsnArg.wrapInsnIntoArg(replace));
}
}
}
}
@Nullable @Nullable
public static RegisterArg getRegFromInsn(List<RegisterArg> regs, InsnType insnType) { public static RegisterArg getRegFromInsn(List<RegisterArg> regs, InsnType insnType) {
for (RegisterArg reg : regs) { for (RegisterArg reg : regs) {
@@ -428,7 +428,7 @@ public class Utils {
} }
public static void checkThreadInterrupt() { public static void checkThreadInterrupt() {
if (Thread.interrupted()) { if (Thread.currentThread().isInterrupted()) {
throw new JadxRuntimeException("Thread interrupted"); throw new JadxRuntimeException("Thread interrupted");
} }
} }
@@ -103,6 +103,10 @@ public class FileUtils {
} }
} }
public static void deleteFileIfExists(Path filePath) throws IOException {
Files.deleteIfExists(filePath);
}
public static boolean deleteDir(File dir) { public static boolean deleteDir(File dir) {
File[] content = dir.listFiles(); File[] content = dir.listFiles();
if (content != null) { if (content != null) {
@@ -115,17 +119,22 @@ public class FileUtils {
public static void deleteDirIfExists(Path dir) { public static void deleteDirIfExists(Path dir) {
if (Files.exists(dir)) { if (Files.exists(dir)) {
deleteDir(dir); try {
deleteDir(dir);
} catch (Exception e) {
LOG.error("Failed to delete dir: " + dir.toAbsolutePath(), e);
}
} }
} }
public static void deleteDir(Path dir) { private static void deleteDir(Path dir) {
try (Stream<Path> pathStream = Files.walk(dir)) { try (Stream<Path> pathStream = Files.walk(dir)) {
pathStream.sorted(Comparator.reverseOrder()) pathStream.sorted(Comparator.reverseOrder())
.map(Path::toFile) .forEach(path -> {
.forEach(file -> { try {
if (!file.delete()) { Files.delete(path);
LOG.warn("Failed to remove file: {}", file.getAbsolutePath()); } catch (Exception e) {
throw new JadxRuntimeException("Failed to delete path: " + path.toAbsolutePath(), e);
} }
}); });
} catch (Exception e) { } catch (Exception e) {
@@ -152,11 +161,11 @@ public class FileUtils {
} }
public static void deleteTempRootDir() { public static void deleteTempRootDir() {
deleteDir(TEMP_ROOT_DIR); deleteDirIfExists(TEMP_ROOT_DIR);
} }
public static void clearTempRootDir() { public static void clearTempRootDir() {
deleteDir(TEMP_ROOT_DIR); deleteDirIfExists(TEMP_ROOT_DIR);
makeDirs(TEMP_ROOT_DIR); makeDirs(TEMP_ROOT_DIR);
} }
@@ -217,6 +226,15 @@ public class FileUtils {
} }
} }
public static void writeFile(Path file, String data) throws IOException {
FileUtils.makeDirsForFile(file);
Files.write(file, data.getBytes(StandardCharsets.UTF_8));
}
public static String readFile(Path textFile) throws IOException {
return new String(Files.readAllBytes(textFile), StandardCharsets.UTF_8);
}
@NotNull @NotNull
public static File prepareFile(File file) { public static File prepareFile(File file) {
File saveFile = cutFileName(file); File saveFile = cutFileName(file);
@@ -254,6 +272,28 @@ public class FileUtils {
return new String(hexChars, StandardCharsets.UTF_8); return new String(hexChars, StandardCharsets.UTF_8);
} }
/**
* Zero padded hex string for first byte
*/
public static String byteToHex(int value) {
int v = value & 0xFF;
byte[] hexChars = new byte[] { HEX_ARRAY[v >>> 4], HEX_ARRAY[v & 0x0F] };
return new String(hexChars, StandardCharsets.US_ASCII);
}
/**
* Zero padded hex string for int value
*/
public static String intToHex(int value) {
byte[] hexChars = new byte[8];
int v = value;
for (int i = 7; i >= 0; i--) {
hexChars[i] = HEX_ARRAY[v & 0x0F];
v >>>= 4;
}
return new String(hexChars, StandardCharsets.US_ASCII);
}
public static boolean isZipFile(File file) { public static boolean isZipFile(File file) {
try (InputStream is = new FileInputStream(file)) { try (InputStream is = new FileInputStream(file)) {
byte[] headers = new byte[4]; byte[] headers = new byte[4];
@@ -89,7 +89,8 @@ public class BinaryXMLParser extends CommonBinaryParser {
is.mark(4); is.mark(4);
int v = is.readInt16(); // version int v = is.readInt16(); // version
int h = is.readInt16(); // header size int h = is.readInt16(); // header size
if (v == 0x0003 && h == 0x0008) { // Some APK Manifest.xml the version is 0
if (h == 0x0008) {
return true; return true;
} }
is.reset(); is.reset();
@@ -0,0 +1,13 @@
package jadx.core.xmlgen;
import java.io.IOException;
import java.io.InputStream;
public interface IResParser {
void decode(InputStream inputStream) throws IOException;
ResourceStorage getResStorage();
String[] getStrings();
}
@@ -0,0 +1,31 @@
package jadx.core.xmlgen;
import java.io.IOException;
import java.io.InputStream;
import jadx.api.ResourceFile;
import jadx.api.ResourceType;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.exceptions.JadxRuntimeException;
public class ResDecoder {
public static IResParser decode(RootNode root, ResourceFile resFile, InputStream is) throws IOException {
if (resFile.getType() != ResourceType.ARSC) {
throw new IllegalArgumentException("Unexpected resource type for decode: " + resFile.getType() + ", expect ARSC");
}
IResParser parser = null;
String fileName = resFile.getOriginalName();
if (fileName.endsWith(".arsc")) {
parser = new ResTableParser(root);
}
if (fileName.endsWith(".pb")) {
parser = new ResProtoParser(root);
}
if (parser == null) {
throw new JadxRuntimeException("Unknown type of resource file: " + fileName);
}
parser.decode(is);
return parser;
}
}
@@ -20,16 +20,16 @@ import com.android.aapt.Resources.Style;
import com.android.aapt.Resources.Styleable; import com.android.aapt.Resources.Styleable;
import com.android.aapt.Resources.Type; import com.android.aapt.Resources.Type;
import com.android.aapt.Resources.Value; import com.android.aapt.Resources.Value;
import com.google.protobuf.InvalidProtocolBufferException;
import jadx.api.ICodeInfo; import jadx.api.ICodeInfo;
import jadx.core.dex.nodes.RootNode; import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.files.FileUtils;
import jadx.core.xmlgen.entry.EntryConfig; import jadx.core.xmlgen.entry.EntryConfig;
import jadx.core.xmlgen.entry.ProtoValue; import jadx.core.xmlgen.entry.ProtoValue;
import jadx.core.xmlgen.entry.ResourceEntry; import jadx.core.xmlgen.entry.ResourceEntry;
import jadx.core.xmlgen.entry.ValuesParser; import jadx.core.xmlgen.entry.ValuesParser;
public class ResProtoParser { public class ResProtoParser implements IResParser {
private final RootNode root; private final RootNode root;
private final ResourceStorage resStorage = new ResourceStorage(); private final ResourceStorage resStorage = new ResourceStorage();
@@ -38,11 +38,7 @@ public class ResProtoParser {
} }
public ResContainer decodeFiles(InputStream inputStream) throws IOException { public ResContainer decodeFiles(InputStream inputStream) throws IOException {
ResourceTable table = decodeProto(inputStream); decode(inputStream);
for (Package p : table.getPackageList()) {
parse(p);
}
resStorage.finish();
ValuesParser vp = new ValuesParser(new String[0], resStorage.getResourcesNames()); ValuesParser vp = new ValuesParser(new String[0], resStorage.getResourcesNames());
ResXmlGen resGen = new ResXmlGen(resStorage, vp); ResXmlGen resGen = new ResXmlGen(resStorage, vp);
ICodeInfo content = XmlGenUtils.makeXmlDump(root.makeCodeWriter(), resStorage); ICodeInfo content = XmlGenUtils.makeXmlDump(root.makeCodeWriter(), resStorage);
@@ -50,6 +46,15 @@ public class ResProtoParser {
return ResContainer.resourceTable("res", xmlFiles, content); return ResContainer.resourceTable("res", xmlFiles, content);
} }
@Override
public void decode(InputStream inputStream) throws IOException {
ResourceTable table = ResourceTable.parseFrom(FileUtils.streamToByteArray(inputStream));
for (Package p : table.getPackageList()) {
parse(p);
}
resStorage.finish();
}
private void parse(Package p) { private void parse(Package p) {
String name = p.getPackageName(); String name = p.getPackageName();
resStorage.setAppPackage(name); resStorage.setAppPackage(name);
@@ -241,8 +246,13 @@ public class ResProtoParser {
return ""; return "";
} }
private ResourceTable decodeProto(InputStream inputStream) @Override
throws InvalidProtocolBufferException, IOException { public ResourceStorage getResStorage() {
return ResourceTable.parseFrom(XmlGenUtils.readData(inputStream)); return resStorage;
}
@Override
public String[] getStrings() {
return new String[0];
} }
} }
@@ -24,7 +24,7 @@ import jadx.core.xmlgen.entry.RawValue;
import jadx.core.xmlgen.entry.ResourceEntry; import jadx.core.xmlgen.entry.ResourceEntry;
import jadx.core.xmlgen.entry.ValuesParser; import jadx.core.xmlgen.entry.ValuesParser;
public class ResTableParser extends CommonBinaryParser { public class ResTableParser extends CommonBinaryParser implements IResParser {
private static final Logger LOG = LoggerFactory.getLogger(ResTableParser.class); private static final Logger LOG = LoggerFactory.getLogger(ResTableParser.class);
private static final Pattern VALID_RES_KEY_PATTERN = Pattern.compile("[\\w\\d_]+"); private static final Pattern VALID_RES_KEY_PATTERN = Pattern.compile("[\\w\\d_]+");
@@ -76,6 +76,7 @@ public class ResTableParser extends CommonBinaryParser {
this.useRawResName = useRawResNames; this.useRawResName = useRawResNames;
} }
@Override
public void decode(InputStream inputStream) throws IOException { public void decode(InputStream inputStream) throws IOException {
is = new ParserStream(inputStream); is = new ParserStream(inputStream);
decodeTableChunk(); decodeTableChunk();
@@ -93,14 +94,6 @@ public class ResTableParser extends CommonBinaryParser {
return ResContainer.resourceTable("res", xmlFiles, content); return ResContainer.resourceTable("res", xmlFiles, content);
} }
public ResourceStorage getResStorage() {
return resStorage;
}
public String[] getStrings() {
return strings;
}
void decodeTableChunk() throws IOException { void decodeTableChunk() throws IOException {
is.checkInt16(RES_TABLE_TYPE, "Not a table chunk"); is.checkInt16(RES_TABLE_TYPE, "Not a table chunk");
is.checkInt16(0x000c, "Unexpected table header size"); is.checkInt16(0x000c, "Unexpected table header size");
@@ -434,4 +427,14 @@ public class ResTableParser extends CommonBinaryParser {
is.skipToPos(start + length, "readScriptOrVariantChar"); is.skipToPos(start + length, "readScriptOrVariantChar");
return sb.toString(); return sb.toString();
} }
@Override
public ResourceStorage getResStorage() {
return resStorage;
}
@Override
public String[] getStrings() {
return strings;
}
} }
@@ -1,5 +1,8 @@
package jadx.api; package jadx.api;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode; import jadx.core.dex.nodes.RootNode;
public class JadxInternalAccess { public class JadxInternalAccess {
@@ -7,4 +10,16 @@ public class JadxInternalAccess {
public static RootNode getRoot(JadxDecompiler d) { public static RootNode getRoot(JadxDecompiler d) {
return d.getRoot(); return d.getRoot();
} }
public static JavaClass convertClassNode(JadxDecompiler d, ClassNode clsNode) {
return d.convertClassNode(clsNode);
}
public static JavaMethod convertMethodNode(JadxDecompiler d, MethodNode mthNode) {
return d.convertMethodNode(mthNode);
}
public static JavaField convertFieldNode(JadxDecompiler d, FieldNode fldNode) {
return d.convertFieldNode(fldNode);
}
} }
@@ -43,9 +43,6 @@ import jadx.api.args.DeobfuscationMapFileMode;
import jadx.api.metadata.ICodeMetadata; import jadx.api.metadata.ICodeMetadata;
import jadx.api.metadata.annotations.InsnCodeOffset; import jadx.api.metadata.annotations.InsnCodeOffset;
import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.IAttributeNode;
import jadx.core.dex.attributes.nodes.JadxCommentsAttr;
import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode; import jadx.core.dex.nodes.RootNode;
@@ -63,13 +60,11 @@ import jadx.tests.api.utils.TestUtils;
import static org.apache.commons.lang3.StringUtils.leftPad; import static org.apache.commons.lang3.StringUtils.leftPad;
import static org.apache.commons.lang3.StringUtils.rightPad; import static org.apache.commons.lang3.StringUtils.rightPad;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.emptyArray; import static org.hamcrest.Matchers.emptyArray;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.notNullValue;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.fail;
@@ -79,8 +74,6 @@ public abstract class IntegrationTest extends TestUtils {
private static final String TEST_DIRECTORY = "src/test/java"; private static final String TEST_DIRECTORY = "src/test/java";
private static final String TEST_DIRECTORY2 = "jadx-core/" + TEST_DIRECTORY; private static final String TEST_DIRECTORY2 = "jadx-core/" + TEST_DIRECTORY;
private static final String OUT_DIR = "test-out-tmp";
private static final String DEFAULT_INPUT_PLUGIN = "dx"; private static final String DEFAULT_INPUT_PLUGIN = "dx";
/** /**
* Set 'TEST_INPUT_PLUGIN' env variable to use 'java' or 'dx' input in tests * Set 'TEST_INPUT_PLUGIN' env variable to use 'java' or 'dx' input in tests
@@ -132,7 +125,7 @@ public abstract class IntegrationTest extends TestUtils {
this.useJavaInput = null; this.useJavaInput = null;
args = new JadxArgs(); args = new JadxArgs();
args.setOutDir(new File(OUT_DIR)); args.setOutDir(new File("test-out-tmp"));
args.setShowInconsistentCode(true); args.setShowInconsistentCode(true);
args.setThreadsCount(1); args.setThreadsCount(1);
args.setSkipResources(true); args.setSkipResources(true);
@@ -156,6 +149,10 @@ public abstract class IntegrationTest extends TestUtils {
} }
} }
public void setOutDirSuffix(String suffix) {
args.setOutDir(new File("test-out-" + suffix + "-tmp"));
}
public String getTestName() { public String getTestName() {
return this.getClass().getSimpleName(); return this.getClass().getSimpleName();
} }
@@ -287,7 +284,7 @@ public abstract class IntegrationTest extends TestUtils {
} }
protected void runChecks(List<ClassNode> clsList) { protected void runChecks(List<ClassNode> clsList) {
clsList.forEach(this::checkCode); clsList.forEach(cls -> checkCode(cls, allowWarnInCode));
compileClassNode(clsList); compileClassNode(clsList);
clsList.forEach(this::runAutoCheck); clsList.forEach(this::runAutoCheck);
} }
@@ -345,33 +342,6 @@ public abstract class IntegrationTest extends TestUtils {
root.processResources(resStorage); root.processResources(resStorage);
} }
protected void checkCode(ClassNode cls) {
assertFalse(hasErrors(cls), "Inconsistent cls: " + cls);
for (MethodNode mthNode : cls.getMethods()) {
if (hasErrors(mthNode)) {
fail("Method with problems: " + mthNode
+ "\n " + Utils.listToString(mthNode.getAttributesStringsList(), "\n "));
}
}
String code = cls.getCode().getCodeStr();
assertThat(code, not(containsString("inconsistent")));
assertThat(code, not(containsString("JADX ERROR")));
}
private boolean hasErrors(IAttributeNode node) {
if (node.contains(AFlag.INCONSISTENT_CODE) || node.contains(AType.JADX_ERROR)) {
return true;
}
if (!allowWarnInCode) {
JadxCommentsAttr commentsAttr = node.get(AType.JADX_COMMENTS);
if (commentsAttr != null) {
return commentsAttr.getComments().get(CommentsLevel.WARN) != null;
}
}
return false;
}
private void runAutoCheck(ClassNode cls) { private void runAutoCheck(ClassNode cls) {
String clsName = cls.getClassInfo().getRawName().replace('/', '.'); String clsName = cls.getClassInfo().getRawName().replace('/', '.');
try { try {
@@ -19,10 +19,15 @@ import static org.hamcrest.Matchers.notNullValue;
public abstract class SmaliTest extends IntegrationTest { public abstract class SmaliTest extends IntegrationTest {
private static final String SMALI_TESTS_PROJECT = "jadx-core";
private static final String SMALI_TESTS_DIR = "src/test/smali"; private static final String SMALI_TESTS_DIR = "src/test/smali";
private static final String SMALI_TESTS_EXT = ".smali"; private static final String SMALI_TESTS_EXT = ".smali";
private String currentProject = "jadx-core";
public void setCurrentProject(String currentProject) {
this.currentProject = currentProject;
}
@BeforeEach @BeforeEach
public void init() { public void init() {
Assumptions.assumeFalse(USE_JAVA_INPUT, "skip smali test for java input tests"); Assumptions.assumeFalse(USE_JAVA_INPUT, "skip smali test for java input tests");
@@ -89,24 +94,24 @@ public abstract class SmaliTest extends IntegrationTest {
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
private static File getSmaliFile(String baseName) { private File getSmaliFile(String baseName) {
File smaliFile = new File(SMALI_TESTS_DIR, baseName + SMALI_TESTS_EXT); File smaliFile = new File(SMALI_TESTS_DIR, baseName + SMALI_TESTS_EXT);
if (smaliFile.exists()) { if (smaliFile.exists()) {
return smaliFile; return smaliFile;
} }
File pathFromRoot = new File(SMALI_TESTS_PROJECT, smaliFile.getPath()); File pathFromRoot = new File(currentProject, smaliFile.getPath());
if (pathFromRoot.exists()) { if (pathFromRoot.exists()) {
return pathFromRoot; return pathFromRoot;
} }
throw new AssertionError("Smali file not found: " + smaliFile.getPath()); throw new AssertionError("Smali file not found: " + smaliFile.getPath());
} }
private static File getSmaliDir(String baseName) { private File getSmaliDir(String baseName) {
File smaliDir = new File(SMALI_TESTS_DIR, baseName); File smaliDir = new File(SMALI_TESTS_DIR, baseName);
if (smaliDir.exists()) { if (smaliDir.exists()) {
return smaliDir; return smaliDir;
} }
File pathFromRoot = new File(SMALI_TESTS_PROJECT, smaliDir.getPath()); File pathFromRoot = new File(currentProject, smaliDir.getPath());
if (pathFromRoot.exists()) { if (pathFromRoot.exists()) {
return pathFromRoot; return pathFromRoot;
} }
@@ -11,7 +11,6 @@ import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Locale;
import javax.tools.DiagnosticListener; import javax.tools.DiagnosticListener;
import javax.tools.JavaCompiler; import javax.tools.JavaCompiler;
@@ -82,7 +81,7 @@ public class TestCompiler implements Closeable {
arguments.addAll(options.getArguments()); arguments.addAll(options.getArguments());
DiagnosticListener<? super JavaFileObject> diagnostic = DiagnosticListener<? super JavaFileObject> diagnostic =
diagObj -> System.out.println("Compiler diagnostic: " + diagObj.getMessage(Locale.ROOT)); diagObj -> System.out.println("Compiler diagnostic: " + diagObj);
Writer out = new PrintWriter(System.out); Writer out = new PrintWriter(System.out);
CompilationTask compilerTask = compiler.getTask(out, fileManager, diagnostic, arguments, null, jfObjects); CompilationTask compilerTask = compiler.getTask(out, fileManager, diagnostic, arguments, null, jfObjects);
if (Boolean.FALSE.equals(compilerTask.call())) { if (Boolean.FALSE.equals(compilerTask.call())) {
@@ -31,6 +31,10 @@ public enum TestProfile implements Consumer<IntegrationTest> {
test.useTargetJavaVersion(11); test.useTargetJavaVersion(11);
test.useJavaInput(); test.useJavaInput();
}), }),
JAVA17("java-17", test -> {
test.useTargetJavaVersion(17);
test.useJavaInput();
}),
ECJ_DX_J8("ecj-dx-j8", test -> { ECJ_DX_J8("ecj-dx-j8", test -> {
test.useEclipseCompiler(); test.useEclipseCompiler();
test.useTargetJavaVersion(8); test.useTargetJavaVersion(8);
@@ -52,8 +56,9 @@ public enum TestProfile implements Consumer<IntegrationTest> {
} }
@Override @Override
public void accept(IntegrationTest integrationTest) { public void accept(IntegrationTest test) {
this.setup.accept(integrationTest); this.setup.accept(test);
test.setOutDirSuffix(description);
} }
public String getDescription() { public String getDescription() {
@@ -3,7 +3,21 @@ package jadx.tests.api.utils;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import jadx.NotYetImplementedExtension; import jadx.NotYetImplementedExtension;
import jadx.api.CommentsLevel;
import jadx.api.ICodeWriter; import jadx.api.ICodeWriter;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.IAttributeNode;
import jadx.core.dex.attributes.nodes.JadxCommentsAttr;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.Utils;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.fail;
@ExtendWith(NotYetImplementedExtension.class) @ExtendWith(NotYetImplementedExtension.class)
public class TestUtils { public class TestUtils {
@@ -35,4 +49,31 @@ public class TestUtils {
} }
return count; return count;
} }
protected static void checkCode(ClassNode cls, boolean allowWarnInCode) {
assertFalse(hasErrors(cls, allowWarnInCode), "Inconsistent cls: " + cls);
for (MethodNode mthNode : cls.getMethods()) {
if (hasErrors(mthNode, allowWarnInCode)) {
fail("Method with problems: " + mthNode
+ "\n " + Utils.listToString(mthNode.getAttributesStringsList(), "\n "));
}
}
String code = cls.getCode().getCodeStr();
assertThat(code, not(containsString("inconsistent")));
assertThat(code, not(containsString("JADX ERROR")));
}
protected static boolean hasErrors(IAttributeNode node, boolean allowWarnInCode) {
if (node.contains(AFlag.INCONSISTENT_CODE) || node.contains(AType.JADX_ERROR)) {
return true;
}
if (!allowWarnInCode) {
JadxCommentsAttr commentsAttr = node.get(AType.JADX_COMMENTS);
if (commentsAttr != null) {
return commentsAttr.getComments().get(CommentsLevel.WARN) != null;
}
}
return false;
}
} }
@@ -8,7 +8,6 @@ import org.assertj.core.api.Assertions;
import jadx.api.ICodeInfo; import jadx.api.ICodeInfo;
import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.ICodeAnnotation;
import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.ICodeNode;
import jadx.tests.api.IntegrationTest; import jadx.tests.api.IntegrationTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
@@ -58,11 +57,12 @@ public class JadxClassNodeAssertions extends AbstractObjectAssert<JadxClassNodeA
return this; return this;
} }
public void checkCodeAnnotationFor(String refStr, ICodeNode node) { public JadxClassNodeAssertions checkCodeAnnotationFor(String refStr, ICodeAnnotation node) {
checkCodeAnnotationFor(refStr, 0, node); checkCodeAnnotationFor(refStr, 0, node);
return this;
} }
public void checkCodeAnnotationFor(String refStr, int refOffset, ICodeNode node) { public JadxClassNodeAssertions checkCodeAnnotationFor(String refStr, int refOffset, ICodeAnnotation node) {
ICodeInfo code = actual.getCode(); ICodeInfo code = actual.getCode();
int codePos = code.getCodeStr().indexOf(refStr); int codePos = code.getCodeStr().indexOf(refStr);
assertThat(codePos).describedAs("String '%s' not found", refStr).isNotEqualTo(-1); assertThat(codePos).describedAs("String '%s' not found", refStr).isNotEqualTo(-1);
@@ -70,9 +70,10 @@ public class JadxClassNodeAssertions extends AbstractObjectAssert<JadxClassNodeA
for (Map.Entry<Integer, ICodeAnnotation> entry : code.getCodeMetadata().getAsMap().entrySet()) { for (Map.Entry<Integer, ICodeAnnotation> entry : code.getCodeMetadata().getAsMap().entrySet()) {
if (entry.getKey() == refPos) { if (entry.getKey() == refPos) {
Assertions.assertThat(entry.getValue()).isEqualTo(node); Assertions.assertThat(entry.getValue()).isEqualTo(node);
return; return this;
} }
} }
fail("Annotation for reference string: '%s' at position %d not found", refStr, refPos); fail("Annotation for reference string: '%s' at position %d not found", refStr, refPos);
return this;
} }
} }
@@ -13,21 +13,24 @@ import jadx.api.ICodeWriter;
import jadx.api.JadxArgs; import jadx.api.JadxArgs;
import jadx.api.JadxDecompiler; import jadx.api.JadxDecompiler;
import jadx.api.JadxInternalAccess; import jadx.api.JadxInternalAccess;
import jadx.api.metadata.ICodeAnnotation;
import jadx.api.metadata.ICodeNodeRef; import jadx.api.metadata.ICodeNodeRef;
import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode; import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.DebugChecks; import jadx.core.utils.DebugChecks;
import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.TestUtils;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
public abstract class BaseExternalTest extends IntegrationTest { public abstract class BaseExternalTest extends TestUtils {
private static final Logger LOG = LoggerFactory.getLogger(BaseExternalTest.class); private static final Logger LOG = LoggerFactory.getLogger(BaseExternalTest.class);
protected JadxDecompiler decompiler;
protected abstract String getSamplesDir(); protected abstract String getSamplesDir();
protected JadxArgs prepare(String inputFile) { protected JadxArgs prepare(String inputFile) {
@@ -55,16 +58,16 @@ public abstract class BaseExternalTest extends IntegrationTest {
} }
protected JadxDecompiler decompile(JadxArgs jadxArgs, @Nullable String clsPatternStr, @Nullable String mthPatternStr) { protected JadxDecompiler decompile(JadxArgs jadxArgs, @Nullable String clsPatternStr, @Nullable String mthPatternStr) {
JadxDecompiler jadx = new JadxDecompiler(jadxArgs); decompiler = new JadxDecompiler(jadxArgs);
jadx.load(); decompiler.load();
if (clsPatternStr == null) { if (clsPatternStr == null) {
jadx.save(); decompiler.save();
} else { } else {
processByPatterns(jadx, clsPatternStr, mthPatternStr); processByPatterns(decompiler, clsPatternStr, mthPatternStr);
} }
printErrorReport(jadx); printErrorReport(decompiler);
return jadx; return decompiler;
} }
private void processByPatterns(JadxDecompiler jadx, String clsPattern, @Nullable String mthPattern) { private void processByPatterns(JadxDecompiler jadx, String clsPattern, @Nullable String mthPattern) {
@@ -109,7 +112,7 @@ public abstract class BaseExternalTest extends IntegrationTest {
} else { } else {
LOG.info("Code: \n{}", classNode.getCode()); LOG.info("Code: \n{}", classNode.getCode());
} }
checkCode(classNode); checkCode(classNode, false);
return true; return true;
} }
@@ -134,7 +137,7 @@ public abstract class BaseExternalTest extends IntegrationTest {
String dashLine = "======================================================================================"; String dashLine = "======================================================================================";
for (MethodNode mth : classNode.getMethods()) { for (MethodNode mth : classNode.getMethods()) {
if (isMthMatch(mth, mthPattern)) { if (isMthMatch(mth, mthPattern)) {
String mthCode = cutMethodCode(codeInfo, code, mth); String mthCode = cutMethodCode(codeInfo, mth);
LOG.info("Print method: {}\n{}\n{}\n{}", mth.getMethodInfo().getShortId(), LOG.info("Print method: {}\n{}\n{}\n{}", mth.getMethodInfo().getShortId(),
dashLine, dashLine,
mthCode, mthCode,
@@ -143,36 +146,33 @@ public abstract class BaseExternalTest extends IntegrationTest {
} }
} }
@NotNull private String cutMethodCode(ICodeInfo codeInfo, MethodNode mth) {
private String cutMethodCode(ICodeInfo codeInfo, String code, MethodNode mth) { int startPos = getCommentStartPos(codeInfo, mth.getDefPosition());
int defPos = mth.getDefPosition(); int stopPos = getNextNodePos(mth, codeInfo);
int startPos = getCommentStartPos(code, defPos); return codeInfo.getCodeStr().substring(startPos, stopPos);
ICodeNodeRef nodeBelow = codeInfo.getCodeMetadata().getNodeBelow(defPos);
int stopPos = nodeBelow == null ? code.length() : nodeBelow.getDefPosition();
int brackets = 0;
StringBuilder mthCode = new StringBuilder();
for (int i = startPos; i > 0 && i < stopPos;) {
int codePoint = code.codePointAt(i);
mthCode.appendCodePoint(codePoint);
if (i >= defPos) {
// also count brackets for detect method end
if (codePoint == '{') {
brackets++;
} else if (codePoint == '}') {
brackets--;
if (brackets <= 0) {
break;
}
}
}
i += Character.charCount(codePoint);
}
return mthCode.toString();
} }
protected int getCommentStartPos(String code, int pos) { private int getNextNodePos(MethodNode mth, ICodeInfo codeInfo) {
int pos = mth.getDefPosition() + 1;
while (true) {
ICodeNodeRef nodeBelow = codeInfo.getCodeMetadata().getNodeBelow(pos);
if (nodeBelow == null) {
return codeInfo.getCodeStr().length();
}
if (nodeBelow.getAnnType() != ICodeAnnotation.AnnType.METHOD) {
return nodeBelow.getDefPosition();
}
MethodNode nodeMth = (MethodNode) nodeBelow;
if (nodeMth.getParentClass().equals(mth.getParentClass())) { // skip methods from anonymous classes
return getCommentStartPos(codeInfo, nodeMth.getDefPosition());
}
pos = nodeMth.getDefPosition() + 1;
}
}
protected int getCommentStartPos(ICodeInfo codeInfo, int pos) {
String emptyLine = ICodeWriter.NL + ICodeWriter.NL; String emptyLine = ICodeWriter.NL + ICodeWriter.NL;
int emptyLinePos = code.lastIndexOf(emptyLine, pos); int emptyLinePos = codeInfo.getCodeStr().lastIndexOf(emptyLine, pos);
return emptyLinePos == -1 ? pos : emptyLinePos + emptyLine.length(); return emptyLinePos == -1 ? pos : emptyLinePos + emptyLine.length();
} }
@@ -1,8 +1,10 @@
package jadx.tests.integration.enums; package jadx.tests.integration.enums;
import java.util.Collections;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import jadx.core.dex.nodes.ClassNode; import jadx.api.CommentsLevel;
import jadx.tests.api.SmaliTest; import jadx.tests.api.SmaliTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
@@ -32,14 +34,31 @@ public class TestEnumObfuscated extends SmaliTest {
public synthetic int getNum() { public synthetic int getNum() {
return this.num; return this.num;
} }
// custom values method
// should be kept and renamed to avoid collision to enum 'values()' method
public static int values() {
return new TestEnumObfuscated[0];
}
// usage of renamed 'values()' method, should be renamed back to 'values'
public static int valuesCount() {
return vs().length;
}
// usage of renamed '$VALUES' field, should be replaced with 'values()' method call
public static int valuesFieldUse() {
return $VLS.length;
}
} }
*/ */
// @formatter:on // @formatter:on
@Test @Test
public void test() { public void test() {
ClassNode cls = getClassNodeFromSmali(); getArgs().setCommentsLevel(CommentsLevel.WARN);
assertThat(cls) getArgs().setRenameFlags(Collections.emptySet());
assertThat(getClassNodeFromSmali())
.code() .code()
.doesNotContain("$VLS") .doesNotContain("$VLS")
.doesNotContain("vo(") .doesNotContain("vo(")
@@ -56,8 +56,6 @@ public class TestGenericsMthOverride extends IntegrationTest {
assertThat(code, containsOne("public Y method(Exception x) {")); assertThat(code, containsOne("public Y method(Exception x) {"));
assertThat(code, containsOne("public Object method(Object x) {")); assertThat(code, containsOne("public Object method(Object x) {"));
assertThat(code, countString(3, "@Override")); assertThat(code, countString(4, "@Override"));
// TODO: @Override missing for class C
// assertThat(code, countString(4, "@Override"));
} }
} }
@@ -0,0 +1,47 @@
package jadx.tests.integration.inline;
import java.util.Collections;
import org.junit.jupiter.api.Test;
import jadx.tests.api.SmaliTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
@SuppressWarnings("CommentedOutCode")
public class TestOverlapSyntheticMethods extends SmaliTest {
// @formatter:off
/*
public String test(int i) {
return a(i) + "|" + a(i);
}
public int a(int i) {
return i;
}
public String a(int i) {
return "i:" + i;
}
*/
// @formatter:on
@Test
public void testSmali() {
assertThat(getClassNodeFromSmali())
.code()
.containsOne("int a(int i) {")
.containsOne("String m0a(int i) {");
}
@Test
public void testSmaliNoRename() {
getArgs().setRenameFlags(Collections.emptySet());
disableCompilation();
assertThat(getClassNodeFromSmali())
.code()
.containsOne("int a(int i) {")
.containsOne("String a(int i) {")
.containsOne("return a(i) + \"|\" + a(i);");
}
}
@@ -0,0 +1,45 @@
package jadx.tests.integration.inline;
import java.util.function.Function;
import org.junit.jupiter.api.Test;
import jadx.api.metadata.ICodeAnnotation;
import jadx.api.metadata.annotations.NodeDeclareRef;
import jadx.core.dex.nodes.ClassNode;
import jadx.tests.api.SmaliTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestOverrideBridgeMerge extends SmaliTest {
public static class TestCls implements Function<String, Integer> {
@Override
public /* bridge */ /* synthetic */ Integer apply(String str) {
return test(str);
}
public Integer test(String str) {
return str.length();
}
}
@Test
public void test() {
assertThat(getClassNode(TestCls.class))
.code()
.containsOne("Integer test(String str) {"); // not inlined
}
@Test
public void testSmali() {
ClassNode cls = getClassNodeFromSmali();
ICodeAnnotation mthDef = new NodeDeclareRef(getMethod(cls, "apply"));
assertThat(cls)
.checkCodeAnnotationFor("apply(String str) {", mthDef)
.code()
.containsOne("@Override")
.containsOne("public Integer apply(String str) {")
.doesNotContain("test(String str)");
}
}
@@ -0,0 +1,42 @@
package jadx.tests.integration.invoke;
import org.junit.jupiter.api.Test;
import jadx.tests.api.IntegrationTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestSuperInvokeUnknown extends IntegrationTest {
public static class TestCls {
public static class BaseClass {
public int doSomething() {
return 0;
}
}
public static class NestedClass extends BaseClass {
@Override
public int doSomething() {
return super.doSomething();
}
}
}
@Test
public void test() {
disableCompilation();
noDebugInfo();
assertThat(getClassNode(TestCls.NestedClass.class)) // BaseClass unknown
.code()
.containsOne("return super.doSomething();");
}
@Test
public void testTopCls() {
noDebugInfo();
assertThat(getClassNode(TestCls.class))
.code()
.containsOne("return super.doSomething();");
}
}
@@ -0,0 +1,59 @@
package jadx.tests.integration.names;
import java.util.List;
import org.junit.jupiter.api.Test;
import jadx.core.dex.nodes.ClassNode;
import jadx.tests.api.IntegrationTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestCollisionWithJavaLangClasses extends IntegrationTest {
public static class TestCls1 {
public static class System {
public static void main(String[] args) {
java.lang.System.out.println("Hello world");
}
}
}
@Test
public void test1() {
assertThat(getClassNode(TestCls1.class))
.code()
.containsOne("java.lang.System.out.println");
}
public static class TestCls2 {
public void doSomething() {
System.doSomething();
java.lang.System.out.println("Hello World");
}
public static class System {
public static void doSomething() {
}
}
}
@Test
public void test2() {
assertThat(getClassNode(TestCls2.class))
.code()
.containsLine(2, "System.doSomething();")
.containsOne("java.lang.System.out.println");
}
@Test
public void test3() {
List<ClassNode> classes = getClassNodes(
jadx.tests.integration.names.pkg2.System.class,
jadx.tests.integration.names.pkg2.TestCls.class);
assertThat(searchCls(classes, "TestCls"))
.code()
.containsLine(2, "System.doSomething();")
.containsOne("java.lang.System.out.println");
}
}
@@ -0,0 +1,6 @@
package jadx.tests.integration.names.pkg2;
public class System {
public static void doSomething() {
}
}
@@ -0,0 +1,8 @@
package jadx.tests.integration.names.pkg2;
public class TestCls {
public void doSomething() {
System.doSomething();
java.lang.System.out.println("Hello World");
}
}
@@ -1,13 +1,15 @@
package jadx.tests.integration.others; package jadx.tests.integration.others;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import jadx.api.JadxInternalAccess;
import jadx.api.JavaClass; import jadx.api.JavaClass;
import jadx.api.JavaMethod; import jadx.api.JavaMethod;
import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.ICodeAnnotation;
import jadx.api.metadata.ICodeAnnotation.AnnType;
import jadx.api.metadata.ICodeMetadata; import jadx.api.metadata.ICodeMetadata;
import jadx.api.metadata.ICodeNodeRef; import jadx.api.metadata.ICodeNodeRef;
import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.ClassNode;
@@ -46,21 +48,32 @@ public class TestCodeMetadata extends IntegrationTest {
int callDefPos = callMth.getDefPosition(); int callDefPos = callMth.getDefPosition();
assertThat(callDefPos).isNotZero(); assertThat(callDefPos).isNotZero();
JavaClass javaClass = Objects.requireNonNull(jadxDecompiler.getJavaClassByNode(cls)); JavaClass javaClass = JadxInternalAccess.convertClassNode(jadxDecompiler, cls);
JavaMethod callJavaMethod = Objects.requireNonNull(jadxDecompiler.getJavaMethodByNode(callMth)); JavaMethod callJavaMethod = JadxInternalAccess.convertMethodNode(jadxDecompiler, callMth);
List<Integer> callUsePlaces = javaClass.getUsePlacesFor(javaClass.getCodeInfo(), callJavaMethod); List<Integer> callUsePlaces = javaClass.getUsePlacesFor(javaClass.getCodeInfo(), callJavaMethod);
assertThat(callUsePlaces).hasSize(1); assertThat(callUsePlaces).hasSize(1);
int callUse = callUsePlaces.get(0); int callUse = callUsePlaces.get(0);
ICodeMetadata metadata = cls.getCode().getCodeMetadata(); ICodeMetadata metadata = cls.getCode().getCodeMetadata();
System.out.println(metadata);
ICodeNodeRef callDef = metadata.getNodeAt(callUse); ICodeNodeRef callDef = metadata.getNodeAt(callUse);
assertThat(callDef).isSameAs(testMth); assertThat(callDef).isSameAs(testMth);
int beforeCallDef = callDefPos - 10; AtomicInteger endPos = new AtomicInteger();
ICodeAnnotation closest = metadata.getClosestUp(beforeCallDef); ICodeAnnotation testEnd = metadata.searchUp(callDefPos, (pos, ann) -> {
if (ann.getAnnType() == AnnType.END) {
endPos.set(pos);
return ann;
}
return null;
});
assertThat(testEnd).isNotNull();
int testEndPos = endPos.get();
ICodeAnnotation closest = metadata.getClosestUp(testEndPos);
assertThat(closest).isInstanceOf(FieldNode.class); // field reference from 'return a.str;' assertThat(closest).isInstanceOf(FieldNode.class); // field reference from 'return a.str;'
ICodeNodeRef nodeBelow = metadata.getNodeBelow(beforeCallDef); ICodeNodeRef nodeBelow = metadata.getNodeBelow(testEndPos);
assertThat(nodeBelow).isSameAs(callMth); assertThat(nodeBelow).isSameAs(callMth);
} }
} }
@@ -0,0 +1,62 @@
package jadx.tests.integration.others;
import java.util.List;
import org.junit.jupiter.api.Test;
import jadx.api.JavaClass;
import jadx.api.JavaMethod;
import jadx.api.metadata.ICodeMetadata;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.tests.api.IntegrationTest;
import static jadx.api.JadxInternalAccess.convertClassNode;
import static jadx.api.JadxInternalAccess.convertMethodNode;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestCodeMetadata2 extends IntegrationTest {
public static class TestCls {
@SuppressWarnings("Convert2Lambda")
public Runnable test(boolean a) {
if (a) {
return new Runnable() {
@Override
public void run() {
System.out.println("test");
}
};
}
System.out.println("another");
return empty();
}
public static Runnable empty() {
return new Runnable() {
@Override
public void run() {
// empty
}
};
}
}
@Test
public void test() {
ClassNode cls = getClassNode(TestCls.class);
assertThat(cls).code().containsOne("return empty();");
MethodNode testMth = getMethod(cls, "test");
MethodNode emptyMth = getMethod(cls, "empty");
JavaClass javaClass = convertClassNode(jadxDecompiler, cls);
JavaMethod emptyJavaMethod = convertMethodNode(jadxDecompiler, emptyMth);
List<Integer> emptyUsePlaces = javaClass.getUsePlacesFor(javaClass.getCodeInfo(), emptyJavaMethod);
assertThat(emptyUsePlaces).hasSize(1);
int callUse = emptyUsePlaces.get(0);
ICodeMetadata metadata = cls.getCode().getCodeMetadata();
assertThat(metadata.getNodeAt(callUse)).isSameAs(testMth);
}
}
@@ -26,7 +26,6 @@ public class TestMoveInline extends SmaliTest {
@Test @Test
public void test() { public void test() {
getArgs().setRawCFGOutput(true);
assertThat(getClassNodeFromSmali()) assertThat(getClassNodeFromSmali())
.code() .code()
// check operations order // check operations order
@@ -0,0 +1,45 @@
package jadx.tests.integration.trycatch;
import jadx.tests.api.IntegrationTest;
import jadx.tests.api.extensions.profiles.TestProfile;
import jadx.tests.api.extensions.profiles.TestWithProfiles;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestTryCatchFinally14 extends IntegrationTest {
@SuppressWarnings("unused")
public static class TestCls {
private TCls t;
public void test() {
try {
if (t != null) {
t.doSomething();
}
} finally {
if (t != null) {
t.doFinally();
}
}
}
private static class TCls {
public void doSomething() {
}
public void doFinally() {
}
}
}
@TestWithProfiles({ TestProfile.DX_J8, TestProfile.D8_J11, TestProfile.JAVA8 })
public void test() {
assertThat(getClassNode(TestCls.class))
.code()
.containsOne(".doSomething();")
.containsOne("} finally {")
.containsOne(".doFinally();")
.countString(2, "!= null) {");
}
}
@@ -25,8 +25,6 @@ public class TestTryCatchMultiException extends IntegrationTest {
@Test @Test
public void test() { public void test() {
// printDisassemble();
// setFallback();
noDebugInfo(); noDebugInfo();
ClassNode cls = getClassNode(TestCls.class); ClassNode cls = getClassNode(TestCls.class);
String code = cls.getCode().toString(); String code = cls.getCode().toString();
@@ -0,0 +1,33 @@
package jadx.tests.integration.trycatch;
import org.junit.jupiter.api.Test;
import jadx.tests.api.SmaliTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
@SuppressWarnings("CommentedOutCode")
public class TestTryCatchMultiException2 extends SmaliTest {
// @formatter:off
/*
public static boolean test() {
try {
Class<?> cls = Class.forName("c");
return ((Boolean) cls.getMethod("b", new Class[0]).invoke(cls, new Object[0])).booleanValue();
} catch (ClassNotFoundException | NoSuchMethodException | Exception | Throwable unused) {
// java compiler don't allow shadow subclasses in multi-catch
// in this case leave only Throwable
return false;
}
}
*/
// @formatter:on
@Test
public void test() {
assertThat(getClassNodeFromSmali())
.code()
.containsOne("} catch (Throwable unused) {");
}
}
@@ -0,0 +1,29 @@
package jadx.tests.integration.types;
import org.junit.jupiter.api.Test;
import jadx.tests.api.SmaliTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
/**
* Issue 1527
*/
@SuppressWarnings("CommentedOutCode")
public class TestTypeResolver21 extends SmaliTest {
// @formatter:off
/*
public Number test(Object objectArray) {
Object[] arr = (Object[]) objectArray;
return (Number) arr[0];
}
*/
// @formatter:on
@Test
public void test() {
assertThat(getClassNodeFromSmali())
.code()
.containsOne("Object[] arr = (Object[]) objectArray;");
}
}
@@ -0,0 +1,29 @@
package jadx.tests.integration.types;
import java.io.IOException;
import java.io.InputStream;
import jadx.tests.api.IntegrationTest;
import jadx.tests.api.extensions.profiles.TestProfile;
import jadx.tests.api.extensions.profiles.TestWithProfiles;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestTypeResolver22 extends IntegrationTest {
public static class TestCls {
public void test(InputStream input, long count) throws IOException {
long pos = input.skip(count);
while (pos < count) {
pos += input.skip(count - pos);
}
}
}
@TestWithProfiles({ TestProfile.JAVA8, TestProfile.DX_J8 })
public void test() {
assertThat(getClassNode(TestCls.class))
.code()
.containsOne("long pos = ");
}
}
@@ -0,0 +1,30 @@
package jadx.tests.integration.types;
import jadx.tests.api.IntegrationTest;
import jadx.tests.api.extensions.profiles.TestProfile;
import jadx.tests.api.extensions.profiles.TestWithProfiles;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestTypeResolver23 extends IntegrationTest {
public static class TestCls {
public long test(int a) {
long v = 1L;
if (a == 2) {
v = 2L;
} else if (a == 3) {
v = 3L;
}
System.out.println(v);
return v;
}
}
@TestWithProfiles({ TestProfile.JAVA8, TestProfile.DX_J8 })
public void test() {
assertThat(getClassNode(TestCls.class))
.code()
.containsOne("long v");
}
}
@@ -72,6 +72,27 @@
return-object v0 return-object v0
.end method .end method
.method public static values()[Lenums/TestEnumObfuscated;
.registers 1
sget-object v0, Lenums/TestEnumObfuscated;->$VLS:[Lenums/TestEnumObfuscated;
return v0
.end method
.method public static valuesCount()I
.registers 2
invoke-static {v0, p0}, Lenums/TestEnumObfuscated;->vs()[Lenums/TestEnumObfuscated;
move-result-object v0
array-length v1, v0
return v1
.end method
.method public static valuesFieldUse()I
.registers 2
sget-object v0, Lenums/TestEnumObfuscated;->$VLS:[Lenums/TestEnumObfuscated;
array-length v1, v0
return v1
.end method
.method public synthetic getNum()I .method public synthetic getNum()I
.registers 2 .registers 2
@@ -0,0 +1,41 @@
.class public Linline/TestOverlapSyntheticMethods;
.super Ljava/lang/Object;
.method public synthetic a(I)I
.registers 2
return p1
.end method
.method public synthetic a(I)Ljava/lang/String;
.registers 4
new-instance v0, Ljava/lang/StringBuilder;
invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V
const-string v1, "i:"
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v0
invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;
move-result-object v0
invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0
return-object v0
.end method
.method public test(I)Ljava/lang/String;
.registers 4
new-instance v0, Ljava/lang/StringBuilder;
invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V
invoke-virtual {p0, p1}, Linline/TestOverlapSyntheticMethods;->a(I)I
move-result v1
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;
move-result-object v0
const-string v1, "|"
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v0
invoke-virtual {p0, p1}, Linline/TestOverlapSyntheticMethods;->a(I)Ljava/lang/String;
move-result-object v1
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v0
invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0
return-object v0
.end method
@@ -0,0 +1,39 @@
.class public Linline/TestOverrideBridgeMerge;
.super Ljava/lang/Object;
.implements Ljava/util/function/Function;
.annotation system Ldalvik/annotation/Signature;
value = {
"Ljava/lang/Object;",
"Ljava/util/function/Function",
"<",
"Ljava/lang/String;",
"Ljava/lang/Integer;",
">;"
}
.end annotation
.method public constructor <init>()V
.registers 1
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
.method public bridge synthetic apply(Ljava/lang/Object;)Ljava/lang/Object;
.registers 3
check-cast p1, Ljava/lang/String;
invoke-virtual {p0, p1}, Linline/TestOverrideBridgeMerge;->test(Ljava/lang/String;)Ljava/lang/Integer;
move-result-object v0
return-object v0
.end method
.method public test(Ljava/lang/String;)Ljava/lang/Integer;
.registers 3
.param p1, "str" # Ljava/lang/String;
invoke-virtual {p1}, Ljava/lang/String;->length()I
move-result v0
invoke-static {v0}, Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer;
move-result-object v0
return-object v0
.end method
@@ -0,0 +1,38 @@
.class public Ltrycatch/TestTryCatchMultiException2;
.super Ljava/lang/Object;
.method public static test()Z
.registers 5
:try_start_b
const-string v0, "c"
invoke-static {v0}, Ljava/lang/Class;->forName(Ljava/lang/String;)Ljava/lang/Class;
move-result-object v1
const/4 v0, 0x0
const-string v2, "b"
new-array v3, v0, [Ljava/lang/Class;
invoke-virtual {v1, v2, v3}, Ljava/lang/Class;->getMethod(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;
move-result-object v2
new-array v3, v0, [Ljava/lang/Object;
invoke-virtual {v2, v1, v3}, Ljava/lang/reflect/Method;->invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
move-result-object v1
check-cast v1, Ljava/lang/Boolean;
invoke-virtual {v1}, Ljava/lang/Boolean;->booleanValue()Z
move-result v1
:try_end_2f
.catch Ljava/lang/ClassNotFoundException; {:try_start_b .. :try_end_2f} :catch_30
.catch Ljava/lang/NoSuchMethodException; {:try_start_b .. :try_end_2f} :catch_30
.catch Ljava/lang/Exception; {:try_start_b .. :try_end_2f} :catch_30
.catchall {:try_start_b .. :try_end_2f} :catchall_30
return v1
:catch_30
:catchall_30
return v0
.end method
@@ -0,0 +1,23 @@
.class public Ltypes/TestTypeResolver21;
.super Ljava/lang/Object;
.source "TestTypeResolver21.java"
.method public test(Ljava/lang/Object;)Ljava/lang/Number;
.registers 4
.param p1, "objectArray" # Ljava/lang/Object;
.prologue
.line 16
check-cast p1, [Ljava/lang/Object;
.end local p1 # "objectArray":Ljava/lang/Object;
move-object v0, p1
check-cast v0, [Ljava/lang/Object;
.line 17
.local v0, "arr":[Ljava/lang/Object;
const/4 v1, 0x0
aget-object v1, v0, v1
check-cast v1, Ljava/lang/Number;
return-object v1
.end method
+21 -9
View File
@@ -7,7 +7,6 @@ plugins {
dependencies { dependencies {
implementation(project(':jadx-core')) implementation(project(':jadx-core'))
implementation(project(":jadx-cli")) implementation(project(":jadx-cli"))
implementation 'com.beust:jcommander:1.82' implementation 'com.beust:jcommander:1.82'
implementation 'ch.qos.logback:logback-classic:1.2.11' implementation 'ch.qos.logback:logback-classic:1.2.11'
@@ -16,9 +15,9 @@ dependencies {
implementation files('libs/jfontchooser-1.0.5.jar') implementation files('libs/jfontchooser-1.0.5.jar')
implementation 'hu.kazocsaba:image-viewer:1.2.3' implementation 'hu.kazocsaba:image-viewer:1.2.3'
implementation 'com.formdev:flatlaf:2.2' implementation 'com.formdev:flatlaf:2.4'
implementation 'com.formdev:flatlaf-intellij-themes:2.2' implementation 'com.formdev:flatlaf-intellij-themes:2.4'
implementation 'com.formdev:flatlaf-extras:2.2' implementation 'com.formdev:flatlaf-extras:2.4'
implementation 'com.formdev:svgSalamander:1.1.3' implementation 'com.formdev:svgSalamander:1.1.3'
implementation 'com.google.code.gson:gson:2.9.0' implementation 'com.google.code.gson:gson:2.9.0'
@@ -27,9 +26,14 @@ dependencies {
implementation 'io.reactivex.rxjava2:rxjava:2.2.21' implementation 'io.reactivex.rxjava2:rxjava:2.2.21'
implementation "com.github.akarnokd:rxjava2-swing:0.3.7" implementation "com.github.akarnokd:rxjava2-swing:0.3.7"
implementation 'com.android.tools.build:apksig:4.2.1' implementation 'com.android.tools.build:apksig:7.2.1'
implementation 'io.github.hqktech:jdwp:1.0' implementation 'io.github.hqktech:jdwp:1.0'
// TODO: Switch back to upstream once this PR gets merged:
// https://github.com/FabricMC/mapping-io/pull/19
// implementation 'net.fabricmc:mapping-io:0.3.0'
implementation files('libs/mapping-io-0.4.0-SNAPSHOT.jar')
testImplementation project(":jadx-core").sourceSets.test.output testImplementation project(":jadx-core").sourceSets.test.output
} }
@@ -100,7 +104,6 @@ runtime {
addModules( addModules(
'java.desktop', 'java.desktop',
'java.naming', 'java.naming',
//'java.sql', // TODO: GSON register adapter for java.sql.Time
'java.xml', 'java.xml',
) )
jpackage { jpackage {
@@ -113,10 +116,9 @@ runtime {
} }
} }
task distWinWithJre(type: Zip, dependsOn: ['runtime', 'createExe']) { task copyDistWinWithJre(type: Copy, dependsOn: ['runtime', 'createExe']) {
group 'jadx' group 'jadx'
destinationDirectory = buildDir destinationDir = new File(buildDir, "jadx-gui-${jadxVersion}-with-jre-win")
archiveFileName = "jadx-gui-${jadxVersion}-with-jre-win.zip"
from(runtime.jreDir) { from(runtime.jreDir) {
include '**/*' include '**/*'
into 'jre' into 'jre'
@@ -126,3 +128,13 @@ task distWinWithJre(type: Zip, dependsOn: ['runtime', 'createExe']) {
} }
duplicatesStrategy = DuplicatesStrategy.EXCLUDE duplicatesStrategy = DuplicatesStrategy.EXCLUDE
} }
task distWinWithJre(type: Zip, dependsOn: ['copyDistWinWithJre']) {
group 'jadx'
destinationDirectory = buildDir
archiveFileName = "jadx-gui-${jadxVersion}-with-jre-win.zip"
from(copyDistWinWithJre.outputs) {
include '**/*'
}
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
Binary file not shown.
+114 -84
View File
@@ -1,29 +1,36 @@
package jadx.gui; package jadx.gui;
import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import jadx.api.ICodeCache; import jadx.api.ICodeInfo;
import jadx.api.JadxArgs; import jadx.api.JadxArgs;
import jadx.api.JadxDecompiler; import jadx.api.JadxDecompiler;
import jadx.api.JavaClass; import jadx.api.JavaClass;
import jadx.api.JavaNode;
import jadx.api.JavaPackage; import jadx.api.JavaPackage;
import jadx.api.ResourceFile; import jadx.api.ResourceFile;
import jadx.api.impl.InMemoryCodeCache; import jadx.api.impl.InMemoryCodeCache;
import jadx.api.metadata.ICodeNodeRef;
import jadx.api.plugins.JadxPlugin;
import jadx.api.plugins.JadxPluginManager;
import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.ProcessState; import jadx.core.dex.nodes.ProcessState;
import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.visitors.rename.RenameVisitor;
import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils; import jadx.core.utils.files.FileUtils;
import jadx.gui.settings.JadxProject; import jadx.gui.settings.JadxProject;
import jadx.gui.settings.JadxSettings; import jadx.gui.settings.JadxSettings;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.codecache.CodeStringCache; import jadx.gui.utils.codecache.CodeStringCache;
import jadx.gui.utils.codecache.disk.BufferCodeCache; import jadx.gui.utils.codecache.disk.BufferCodeCache;
import jadx.gui.utils.codecache.disk.DiskCodeCache; import jadx.gui.utils.codecache.disk.DiskCodeCache;
@@ -32,41 +39,41 @@ import static jadx.core.dex.nodes.ProcessState.GENERATED_AND_UNLOADED;
import static jadx.core.dex.nodes.ProcessState.NOT_LOADED; import static jadx.core.dex.nodes.ProcessState.NOT_LOADED;
import static jadx.core.dex.nodes.ProcessState.PROCESS_COMPLETE; import static jadx.core.dex.nodes.ProcessState.PROCESS_COMPLETE;
@SuppressWarnings("ConstantConditions")
public class JadxWrapper { public class JadxWrapper {
private static final Logger LOG = LoggerFactory.getLogger(JadxWrapper.class); private static final Logger LOG = LoggerFactory.getLogger(JadxWrapper.class);
private final JadxSettings settings; private static final Object DECOMPILER_UPDATE_SYNC = new Object();
private JadxDecompiler decompiler;
private @Nullable JadxProject project;
private List<Path> openPaths = Collections.emptyList();
public JadxWrapper(JadxSettings settings) { private final MainWindow mainWindow;
this.settings = settings; private volatile @Nullable JadxDecompiler decompiler;
this.decompiler = new JadxDecompiler(settings.toJadxArgs());
public JadxWrapper(MainWindow mainWindow) {
this.mainWindow = mainWindow;
} }
public void openFile(List<Path> paths) { public void open() {
close(); close();
this.openPaths = paths;
try { try {
JadxArgs jadxArgs = settings.toJadxArgs(); synchronized (DECOMPILER_UPDATE_SYNC) {
jadxArgs.setInputFiles(FileUtils.toFiles(paths)); JadxProject project = getProject();
if (project != null) { JadxArgs jadxArgs = getSettings().toJadxArgs();
jadxArgs.setInputFiles(FileUtils.toFiles(project.getFilePaths()));
jadxArgs.setCodeData(project.getCodeData()); jadxArgs.setCodeData(project.getCodeData());
this.decompiler = new JadxDecompiler(jadxArgs);
this.decompiler.load();
initCodeCache();
} }
closeCodeCache();
this.decompiler = new JadxDecompiler(jadxArgs);
this.decompiler.load();
initCodeCache(jadxArgs);
} catch (Exception e) { } catch (Exception e) {
LOG.error("Jadx init error", e); LOG.error("Jadx decompiler wrapper init error", e);
close(); close();
} }
} }
// TODO: check and move into core package // TODO: check and move into core package
public void unloadClasses() { public void unloadClasses() {
for (ClassNode cls : decompiler.getRoot().getClasses()) { for (ClassNode cls : getDecompiler().getRoot().getClasses()) {
ProcessState clsState = cls.getState(); ProcessState clsState = cls.getState();
cls.unload(); cls.unload();
cls.setState(clsState == PROCESS_COMPLETE ? GENERATED_AND_UNLOADED : NOT_LOADED); cls.setState(clsState == PROCESS_COMPLETE ? GENERATED_AND_UNLOADED : NOT_LOADED);
@@ -75,68 +82,50 @@ public class JadxWrapper {
public void close() { public void close() {
try { try {
decompiler.close(); synchronized (DECOMPILER_UPDATE_SYNC) {
closeCodeCache(); if (decompiler != null) {
decompiler.close();
decompiler = null;
}
}
} catch (Exception e) { } catch (Exception e) {
LOG.error("jadx decompiler close error", e); LOG.error("Jadx decompiler close error", e);
} finally {
mainWindow.getCacheObject().reset();
} }
this.openPaths = Collections.emptyList();
} }
private void initCodeCache(JadxArgs jadxArgs) { private void initCodeCache() {
switch (settings.getCodeCacheMode()) { switch (getSettings().getCodeCacheMode()) {
case MEMORY: case MEMORY:
jadxArgs.setCodeCache(new InMemoryCodeCache()); getArgs().setCodeCache(new InMemoryCodeCache());
break; break;
case DISK_WITH_CACHE: case DISK_WITH_CACHE:
jadxArgs.setCodeCache(new CodeStringCache(buildBufferedDiskCache())); getArgs().setCodeCache(new CodeStringCache(buildBufferedDiskCache()));
break; break;
case DISK: case DISK:
jadxArgs.setCodeCache(buildBufferedDiskCache()); getArgs().setCodeCache(buildBufferedDiskCache());
break; break;
} }
} }
private BufferCodeCache buildBufferedDiskCache() { private BufferCodeCache buildBufferedDiskCache() {
DiskCodeCache diskCache = new DiskCodeCache(decompiler.getRoot(), getCacheDir()); DiskCodeCache diskCache = new DiskCodeCache(getDecompiler().getRoot(), getProject().getCacheDir());
return new BufferCodeCache(diskCache); return new BufferCodeCache(diskCache);
} }
private Path getCacheDir() {
if (project != null && project.getProjectPath() != null) {
Path projectPath = project.getProjectPath();
return projectPath.resolveSibling(projectPath.getFileName() + ".cache");
}
if (!openPaths.isEmpty()) {
Path path = openPaths.get(0);
return path.resolveSibling(path.getFileName() + ".cache");
}
throw new JadxRuntimeException("Can't get working dir");
}
public void closeCodeCache() {
ICodeCache codeCache = getArgs().getCodeCache();
if (codeCache != null) {
try {
codeCache.close();
} catch (Exception e) {
throw new JadxRuntimeException("Error on cache close", e);
}
}
}
/** /**
* Get the complete list of classes * Get the complete list of classes
*/ */
public List<JavaClass> getClasses() { public List<JavaClass> getClasses() {
return decompiler.getClasses(); return getDecompiler().getClasses();
} }
/** /**
* Get all classes that are not excluded by the excluded packages settings * Get all classes that are not excluded by the excluded packages settings
*/ */
public List<JavaClass> getIncludedClasses() { public List<JavaClass> getIncludedClasses() {
List<JavaClass> classList = decompiler.getClasses(); List<JavaClass> classList = getDecompiler().getClasses();
List<String> excludedPackages = getExcludedPackages(); List<String> excludedPackages = getExcludedPackages();
if (excludedPackages.isEmpty()) { if (excludedPackages.isEmpty()) {
return classList; return classList;
@@ -150,7 +139,7 @@ public class JadxWrapper {
* Get all classes that are not excluded by the excluded packages settings including inner classes * Get all classes that are not excluded by the excluded packages settings including inner classes
*/ */
public List<JavaClass> getIncludedClassesWithInners() { public List<JavaClass> getIncludedClassesWithInners() {
List<JavaClass> classes = decompiler.getClassesWithInners(); List<JavaClass> classes = getDecompiler().getClassesWithInners();
List<String> excludedPackages = getExcludedPackages(); List<String> excludedPackages = getExcludedPackages();
if (excludedPackages.isEmpty()) { if (excludedPackages.isEmpty()) {
return classes; return classes;
@@ -172,58 +161,99 @@ public class JadxWrapper {
} }
public List<List<JavaClass>> buildDecompileBatches(List<JavaClass> classes) { public List<List<JavaClass>> buildDecompileBatches(List<JavaClass> classes) {
return decompiler.getDecompileScheduler().buildBatches(classes); return getDecompiler().getDecompileScheduler().buildBatches(classes);
} }
// TODO: move to CLI and filter classes in JadxDecompiler // TODO: move to CLI and filter classes in JadxDecompiler
public List<String> getExcludedPackages() { public List<String> getExcludedPackages() {
String excludedPackages = settings.getExcludedPackages().trim(); String excludedPackages = getSettings().getExcludedPackages().trim();
if (excludedPackages.isEmpty()) { if (excludedPackages.isEmpty()) {
return Collections.emptyList(); return Collections.emptyList();
} }
return Arrays.asList(excludedPackages.split("[ ]+")); return Arrays.asList(excludedPackages.split(" +"));
} }
public void setExcludedPackages(List<String> packagesToExclude) { public void setExcludedPackages(List<String> packagesToExclude) {
settings.setExcludedPackages(String.join(" ", packagesToExclude).trim()); getSettings().setExcludedPackages(String.join(" ", packagesToExclude).trim());
settings.sync(); getSettings().sync();
} }
public void addExcludedPackage(String packageToExclude) { public void addExcludedPackage(String packageToExclude) {
String newExclusion = settings.getExcludedPackages() + ' ' + packageToExclude; String newExclusion = getSettings().getExcludedPackages() + ' ' + packageToExclude;
settings.setExcludedPackages(newExclusion.trim()); getSettings().setExcludedPackages(newExclusion.trim());
settings.sync(); getSettings().sync();
} }
public void removeExcludedPackage(String packageToRemoveFromExclusion) { public void removeExcludedPackage(String packageToRemoveFromExclusion) {
List<String> list = new ArrayList<>(getExcludedPackages()); List<String> list = new ArrayList<>(getExcludedPackages());
list.remove(packageToRemoveFromExclusion); list.remove(packageToRemoveFromExclusion);
settings.setExcludedPackages(String.join(" ", list)); getSettings().setExcludedPackages(String.join(" ", list));
settings.sync(); getSettings().sync();
} }
public List<JavaPackage> getPackages() { public List<JadxPlugin> getAllPlugins() {
return decompiler.getPackages(); if (decompiler != null) {
return decompiler.getPluginManager().getAllPlugins();
}
JadxPluginManager pluginManager = new JadxPluginManager();
pluginManager.load();
return pluginManager.getAllPlugins();
} }
public List<ResourceFile> getResources() { /**
return decompiler.getResources(); * TODO: make method private
} * Do not store JadxDecompiler in fields to not leak old instances
*/
public List<Path> getOpenPaths() { public @NotNull JadxDecompiler getDecompiler() {
return openPaths; if (decompiler == null || decompiler.getRoot() == null) {
} throw new JadxRuntimeException("Decompiler not yet loaded");
}
public JadxDecompiler getDecompiler() {
return decompiler; return decompiler;
} }
public JadxArgs getArgs() { // TODO: forbid usage of this method
return decompiler.getArgs(); public RootNode getRootNode() {
return getDecompiler().getRoot();
} }
public void setProject(JadxProject project) { public void reInitRenameVisitor() {
this.project = project; new RenameVisitor().init(getRootNode());
}
public void reloadCodeData() {
getDecompiler().reloadCodeData();
}
public JavaNode getJavaNodeByRef(ICodeNodeRef nodeRef) {
return getDecompiler().getJavaNodeByRef(nodeRef);
}
public @Nullable JavaNode getEnclosingNode(ICodeInfo codeInfo, int pos) {
return getDecompiler().getEnclosingNode(codeInfo, pos);
}
public List<Runnable> getSaveTasks() {
return getDecompiler().getSaveTasks();
}
public List<JavaPackage> getPackages() {
return getDecompiler().getPackages();
}
public List<ResourceFile> getResources() {
return getDecompiler().getResources();
}
public JadxArgs getArgs() {
return getDecompiler().getArgs();
}
public JadxProject getProject() {
return mainWindow.getProject();
}
public JadxSettings getSettings() {
return mainWindow.getSettings();
} }
/** /**
@@ -231,14 +261,14 @@ public class JadxWrapper {
* Full name of an outer class. Inner classes are not supported. * Full name of an outer class. Inner classes are not supported.
*/ */
public @Nullable JavaClass searchJavaClassByFullAlias(String fullName) { public @Nullable JavaClass searchJavaClassByFullAlias(String fullName) {
return decompiler.getClasses().stream() return getDecompiler().getClasses().stream()
.filter(cls -> cls.getFullName().equals(fullName)) .filter(cls -> cls.getFullName().equals(fullName))
.findFirst() .findFirst()
.orElse(null); .orElse(null);
} }
public @Nullable JavaClass searchJavaClassByOrigClassName(String fullName) { public @Nullable JavaClass searchJavaClassByOrigClassName(String fullName) {
return decompiler.searchJavaClassByOrigFullName(fullName); return getDecompiler().searchJavaClassByOrigFullName(fullName);
} }
/** /**
@@ -246,7 +276,7 @@ public class JadxWrapper {
* Full raw name of an outer class. Inner classes are not supported. * Full raw name of an outer class. Inner classes are not supported.
*/ */
public @Nullable JavaClass searchJavaClassByRawName(String rawName) { public @Nullable JavaClass searchJavaClassByRawName(String rawName) {
return decompiler.getClasses().stream() return getDecompiler().getClasses().stream()
.filter(cls -> cls.getRawName().equals(rawName)) .filter(cls -> cls.getRawName().equals(rawName))
.findFirst() .findFirst()
.orElse(null); .orElse(null);
@@ -160,7 +160,7 @@ public class DbgUtils {
// TODO: parse AndroidManifest.xml instead of looking for keywords // TODO: parse AndroidManifest.xml instead of looking for keywords
private static String getManifestContent(MainWindow mainWindow) { private static String getManifestContent(MainWindow mainWindow) {
try { try {
ResourceFile androidManifest = mainWindow.getWrapper().getDecompiler().getResources() ResourceFile androidManifest = mainWindow.getWrapper().getResources()
.stream() .stream()
.filter(res -> res.getType() == ResourceType.MANIFEST) .filter(res -> res.getType() == ResourceType.MANIFEST)
.findFirst() .findFirst()
@@ -46,6 +46,7 @@ import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.exceptions.JadxRuntimeException;
import static jadx.api.plugins.input.data.AccessFlagsScope.FIELD; import static jadx.api.plugins.input.data.AccessFlagsScope.FIELD;
import static jadx.api.plugins.input.data.AccessFlagsScope.METHOD; import static jadx.api.plugins.input.data.AccessFlagsScope.METHOD;
@@ -60,7 +61,9 @@ import static jadx.api.plugins.input.insns.Opcode.INVOKE_CUSTOM;
import static jadx.api.plugins.input.insns.Opcode.INVOKE_CUSTOM_RANGE; import static jadx.api.plugins.input.insns.Opcode.INVOKE_CUSTOM_RANGE;
import static jadx.api.plugins.input.insns.Opcode.INVOKE_POLYMORPHIC; import static jadx.api.plugins.input.insns.Opcode.INVOKE_POLYMORPHIC;
import static jadx.api.plugins.input.insns.Opcode.INVOKE_POLYMORPHIC_RANGE; import static jadx.api.plugins.input.insns.Opcode.INVOKE_POLYMORPHIC_RANGE;
import static jadx.api.plugins.input.insns.Opcode.PACKED_SWITCH;
import static jadx.api.plugins.input.insns.Opcode.PACKED_SWITCH_PAYLOAD; import static jadx.api.plugins.input.insns.Opcode.PACKED_SWITCH_PAYLOAD;
import static jadx.api.plugins.input.insns.Opcode.SPARSE_SWITCH;
import static jadx.api.plugins.input.insns.Opcode.SPARSE_SWITCH_PAYLOAD; import static jadx.api.plugins.input.insns.Opcode.SPARSE_SWITCH_PAYLOAD;
public class Smali { public class Smali {
@@ -288,6 +291,14 @@ public class Smali {
if (codeReader.getDebugInfo() != null) { if (codeReader.getDebugInfo() != null) {
formatDbgInfo(codeReader.getDebugInfo(), line); formatDbgInfo(codeReader.getDebugInfo(), line);
} }
// first pass to fill payload offsets for switch instructions
codeReader.visitInstructions(insn -> {
Opcode opcode = insn.getOpcode();
if (opcode == PACKED_SWITCH || opcode == SPARSE_SWITCH) {
insn.decode();
line.addPayloadOffset(insn.getOffset(), insn.getTarget());
}
});
codeReader.visitInstructions(insn -> { codeReader.visitInstructions(insn -> {
InsnNode node = decodeInsn(insn, line); InsnNode node = decodeInsn(insn, line);
nodes.put((long) insn.getOffset(), node); nodes.put((long) insn.getOffset(), node);
@@ -404,7 +415,6 @@ public class Smali {
line.addTip(target, String.format(FMT_S_SWITCH_TAG, target), ""); line.addTip(target, String.format(FMT_S_SWITCH_TAG, target), "");
line.getLineWriter().append(", ").append(String.format(FMT_S_SWITCH, target)); line.getLineWriter().append(", ").append(String.format(FMT_S_SWITCH, target));
} }
line.addPayloadOffset(insn.getOffset(), target);
return true; return true;
} }
} }
@@ -733,7 +743,7 @@ public class Smali {
ISwitchPayload payload = (ISwitchPayload) insn.getPayload(); ISwitchPayload payload = (ISwitchPayload) insn.getPayload();
if (payload != null) { if (payload != null) {
fmtSwitchPayload(insn, FMT_P_SWITCH_CASE, FMT_P_SWITCH_CASE_TAG, line, payload, insn.getOffset()); fmtSwitchPayload(insn, FMT_P_SWITCH_CASE, FMT_P_SWITCH_CASE_TAG, line, payload);
} }
return true; return true;
} }
@@ -743,7 +753,7 @@ public class Smali {
ISwitchPayload payload = (ISwitchPayload) insn.getPayload(); ISwitchPayload payload = (ISwitchPayload) insn.getPayload();
if (payload != null) { if (payload != null) {
fmtSwitchPayload(insn, FMT_S_SWITCH_CASE, FMT_S_SWITCH_CASE_TAG, line, payload, insn.getOffset()); fmtSwitchPayload(insn, FMT_S_SWITCH_CASE, FMT_S_SWITCH_CASE_TAG, line, payload);
} }
return true; return true;
} }
@@ -755,17 +765,19 @@ public class Smali {
return false; return false;
} }
private void fmtSwitchPayload(InsnData insn, String fmtTarget, String fmtTag, LineInfo line, private void fmtSwitchPayload(InsnData insn, String fmtTarget, String fmtTag, LineInfo line, ISwitchPayload payload) {
ISwitchPayload payload, int curOffset) {
int lineStart = getInsnColStart(); int lineStart = getInsnColStart();
lineStart += CODE_OFFSET_COLUMN_WIDTH + 1 + 1; // plus 1s for space and the ':' lineStart += CODE_OFFSET_COLUMN_WIDTH + 1 + 1; // plus 1s for space and the ':'
String basicIndent = new String(new byte[lineStart]).replace("\0", " "); String basicIndent = new String(new byte[lineStart]).replace("\0", " ");
String indent = SmaliWriter.INDENT_STR + basicIndent; String indent = SmaliWriter.INDENT_STR + basicIndent;
int[] keys = payload.getKeys(); int[] keys = payload.getKeys();
int[] targets = payload.getTargets(); int[] targets = payload.getTargets();
int opcodeOffset = line.payloadOffsetMap.get(curOffset); Integer switchOffset = line.payloadOffsetMap.get(insn.getOffset());
if (switchOffset == null) {
throw new JadxRuntimeException("Unknown switch insn for payload at " + insn.getOffset());
}
for (int i = 0; i < keys.length; i++) { for (int i = 0; i < keys.length; i++) {
int target = opcodeOffset + targets[i]; int target = switchOffset + targets[i];
line.addInsnLine(insn.getOffset(), line.addInsnLine(insn.getOffset(),
String.format("%scase %d: -> " + fmtTarget, indent, keys[i], target)); String.format("%scase %d: -> " + fmtTarget, indent, keys[i], target));
line.addTip(target, line.addTip(target,
@@ -3,6 +3,7 @@ package jadx.gui.jobs;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.Future; import java.util.concurrent.Future;
@@ -11,6 +12,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.swing.SwingUtilities; import javax.swing.SwingUtilities;
import javax.swing.SwingWorker; import javax.swing.SwingWorker;
@@ -19,6 +21,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.settings.JadxSettings;
import jadx.gui.ui.MainWindow; import jadx.gui.ui.MainWindow;
import jadx.gui.ui.panel.ProgressPanel; import jadx.gui.ui.panel.ProgressPanel;
import jadx.gui.utils.NLS; import jadx.gui.utils.NLS;
@@ -33,16 +36,16 @@ import static jadx.gui.utils.UiUtils.calcProgress;
public class BackgroundExecutor { public class BackgroundExecutor {
private static final Logger LOG = LoggerFactory.getLogger(BackgroundExecutor.class); private static final Logger LOG = LoggerFactory.getLogger(BackgroundExecutor.class);
private final MainWindow mainWindow; private final JadxSettings settings;
private final ProgressPanel progressPane; private final ProgressPanel progressPane;
private ThreadPoolExecutor taskQueueExecutor; private ThreadPoolExecutor taskQueueExecutor;
private final Map<Long, IBackgroundTask> taskRunning = new ConcurrentHashMap<>(); private final Map<Long, IBackgroundTask> taskRunning = new ConcurrentHashMap<>();
private final AtomicLong idSupplier = new AtomicLong(0); private final AtomicLong idSupplier = new AtomicLong(0);
public BackgroundExecutor(MainWindow mainWindow) { public BackgroundExecutor(JadxSettings settings, ProgressPanel progressPane) {
this.mainWindow = mainWindow; this.settings = Objects.requireNonNull(settings);
this.progressPane = mainWindow.getProgressPane(); this.progressPane = Objects.requireNonNull(progressPane);
reset(); reset();
} }
@@ -68,9 +71,16 @@ public class BackgroundExecutor {
public synchronized void cancelAll() { public synchronized void cancelAll() {
try { try {
taskRunning.values().forEach(Cancelable::cancel); taskRunning.values().forEach(Cancelable::cancel);
taskQueueExecutor.shutdown(); taskQueueExecutor.shutdownNow();
boolean complete = taskQueueExecutor.awaitTermination(30, TimeUnit.SECONDS); boolean complete = taskQueueExecutor.awaitTermination(3, TimeUnit.SECONDS);
LOG.debug("Background task executor terminated with status: {}", complete ? "complete" : "interrupted"); if (complete) {
LOG.debug("Background task executor canceled successfully");
} else {
String taskNames = taskRunning.values().stream()
.map(IBackgroundTask::getTitle)
.collect(Collectors.joining(", "));
LOG.debug("Background task executor cancel failed. Running tasks: {}", taskNames);
}
} catch (Exception e) { } catch (Exception e) {
LOG.error("Error terminating task executor", e); LOG.error("Error terminating task executor", e);
} finally { } finally {
@@ -131,8 +141,17 @@ public class BackgroundExecutor {
try { try {
runJobs(); runJobs();
} finally { } finally {
taskComplete(id); try {
task.onDone(this); task.onDone(this);
// treat UI task operations as part of the task to not mix with others
UiUtils.uiRunAndWait(() -> {
task.onFinish(this);
progressPane.setVisible(false);
});
} finally {
taskComplete(id);
progressPane.changeVisibility(this, false);
}
} }
return status; return status;
} }
@@ -146,7 +165,7 @@ public class BackgroundExecutor {
progressPane.changeVisibility(this, true); progressPane.changeVisibility(this, true);
} }
status = TaskStatus.STARTED; status = TaskStatus.STARTED;
int threadsCount = mainWindow.getSettings().getThreadsCount(); int threadsCount = settings.getThreadsCount();
executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(threadsCount); executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(threadsCount);
for (Runnable job : jobs) { for (Runnable job : jobs) {
executor.execute(job); executor.execute(job);
@@ -212,8 +231,15 @@ public class BackgroundExecutor {
// force termination // force termination
task.cancel(); task.cancel();
executor.shutdown(); executor.shutdown();
if (executor.awaitTermination(2, TimeUnit.SECONDS)) {
LOG.debug("Task cancel complete");
return;
}
LOG.debug("Forcing tasks cancel");
executor.shutdownNow();
boolean complete = executor.awaitTermination(5, TimeUnit.SECONDS); boolean complete = executor.awaitTermination(5, TimeUnit.SECONDS);
LOG.debug("Task cancel complete: {}", complete ? "success" : "aborted"); LOG.debug("Forced task cancel status: {}",
complete ? "success" : "fail, still active: " + executor.getActiveCount());
} }
private Supplier<TaskStatus> buildCancelCheck(long startTime) { private Supplier<TaskStatus> buildCancelCheck(long startTime) {
@@ -251,12 +277,6 @@ public class BackgroundExecutor {
}; };
} }
@Override
protected void done() {
progressPane.setVisible(false);
task.onFinish(this);
}
@Override @Override
public TaskStatus getStatus() { public TaskStatus getStatus() {
return status; return status;
@@ -6,7 +6,6 @@ import java.util.List;
import javax.swing.JOptionPane; import javax.swing.JOptionPane;
import jadx.api.ICodeCache; import jadx.api.ICodeCache;
import jadx.api.JadxDecompiler;
import jadx.gui.JadxWrapper; import jadx.gui.JadxWrapper;
import jadx.gui.ui.MainWindow; import jadx.gui.ui.MainWindow;
import jadx.gui.utils.NLS; import jadx.gui.utils.NLS;
@@ -35,9 +34,8 @@ public class ExportTask extends CancelableBackgroundTask {
@Override @Override
public List<Runnable> scheduleJobs() { public List<Runnable> scheduleJobs() {
wrapCodeCache(); wrapCodeCache();
JadxDecompiler decompiler = wrapper.getDecompiler(); wrapper.getArgs().setRootDir(saveDir);
decompiler.getArgs().setRootDir(saveDir); List<Runnable> saveTasks = wrapper.getSaveTasks();
List<Runnable> saveTasks = decompiler.getSaveTasks();
this.timeLimit = DecompileTask.calcDecompileTimeLimit(saveTasks.size()); this.timeLimit = DecompileTask.calcDecompileTimeLimit(saveTasks.size());
return saveTasks; return saveTasks;
} }
@@ -0,0 +1,285 @@
package jadx.gui.plugins.mappings;
import java.io.IOException;
import java.nio.file.Path;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.fabricmc.mappingio.MappedElementKind;
import net.fabricmc.mappingio.MappingWriter;
import net.fabricmc.mappingio.format.MappingFormat;
import net.fabricmc.mappingio.tree.MemoryMappingTree;
import jadx.api.ICodeInfo;
import jadx.api.data.ICodeComment;
import jadx.api.data.ICodeRename;
import jadx.api.data.IJavaNodeRef.RefType;
import jadx.api.data.impl.JadxCodeData;
import jadx.api.data.impl.JadxCodeRef;
import jadx.api.metadata.ICodeNodeRef;
import jadx.api.metadata.annotations.InsnCodeOffset;
import jadx.api.metadata.annotations.NodeDeclareRef;
import jadx.api.metadata.annotations.VarNode;
import jadx.api.utils.CodeUtils;
import jadx.core.Consts;
import jadx.core.codegen.TypeGen;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.info.FieldInfo;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.files.FileUtils;
public class MappingExporter {
private static final Logger LOG = LoggerFactory.getLogger(MappingExporter.class);
private final RootNode root;
public MappingExporter(RootNode rootNode) {
this.root = rootNode;
}
private List<VarNode> collectMethodArgs(MethodNode methodNode) {
ICodeInfo codeInfo = methodNode.getTopParentClass().getCode();
int mthDefPos = methodNode.getDefPosition();
int lineEndPos = CodeUtils.getLineEndForPos(codeInfo.getCodeStr(), mthDefPos);
List<VarNode> args = new ArrayList<>();
codeInfo.getCodeMetadata().searchDown(mthDefPos, (pos, ann) -> {
if (pos > lineEndPos) {
// Stop at line end
return Boolean.TRUE;
}
if (ann instanceof NodeDeclareRef) {
ICodeNodeRef declRef = ((NodeDeclareRef) ann).getNode();
if (declRef instanceof VarNode) {
VarNode varNode = (VarNode) declRef;
if (!varNode.getMth().equals(methodNode)) {
// Stop if we've gone too far and have entered a different method
return Boolean.TRUE;
}
args.add(varNode);
}
}
return null;
});
return args;
}
private List<SimpleEntry<VarNode, Integer>> collectMethodVars(MethodNode methodNode) {
ICodeInfo codeInfo = methodNode.getTopParentClass().getCode();
int mthDefPos = methodNode.getDefPosition();
int mthLineEndPos = CodeUtils.getLineEndForPos(codeInfo.getCodeStr(), mthDefPos);
List<SimpleEntry<VarNode, Integer>> vars = new ArrayList<>();
AtomicInteger lastOffset = new AtomicInteger(-1);
codeInfo.getCodeMetadata().searchDown(mthLineEndPos, (pos, ann) -> {
if (ann instanceof InsnCodeOffset) {
lastOffset.set(((InsnCodeOffset) ann).getOffset());
}
if (ann instanceof NodeDeclareRef) {
ICodeNodeRef declRef = ((NodeDeclareRef) ann).getNode();
if (declRef instanceof VarNode) {
VarNode varNode = (VarNode) declRef;
if (!varNode.getMth().equals(methodNode)) {
// Stop if we've gone too far and have entered a different method
return Boolean.TRUE;
}
if (lastOffset.get() != -1) {
vars.add(new SimpleEntry<VarNode, Integer>(varNode, lastOffset.get()));
} else {
LOG.warn("Local variable not present in bytecode, skipping: "
+ methodNode.getMethodInfo().getRawFullId() + "#" + varNode.getName());
}
lastOffset.set(-1);
}
}
return null;
});
return vars;
}
public void exportMappings(Path path, JadxCodeData codeData, MappingFormat mappingFormat) {
MemoryMappingTree mappingTree = new MemoryMappingTree();
// Map < SrcName >
Set<String> mappedClasses = new HashSet<>();
// Map < DeclClass + ShortId >
Set<String> mappedFields = new HashSet<>();
Set<String> mappedMethods = new HashSet<>();
Set<String> methodsWithMappedElements = new HashSet<>();
// Map < DeclClass + MethodShortId + CodeRef, NewName >
Map<String, String> mappedMethodArgsAndVars = new HashMap<>();
// Map < DeclClass + *ShortId + *CodeRef, Comment >
Map<String, String> comments = new HashMap<>();
// We have to do this so we know for sure which elements are *manually* renamed
for (ICodeRename codeRename : codeData.getRenames()) {
if (codeRename.getNodeRef().getType().equals(RefType.CLASS)) {
mappedClasses.add(codeRename.getNodeRef().getDeclaringClass());
} else if (codeRename.getNodeRef().getType().equals(RefType.FIELD)) {
mappedFields.add(codeRename.getNodeRef().getDeclaringClass() + codeRename.getNodeRef().getShortId());
} else if (codeRename.getNodeRef().getType().equals(RefType.METHOD)) {
if (codeRename.getCodeRef() == null) {
mappedMethods.add(codeRename.getNodeRef().getDeclaringClass() + codeRename.getNodeRef().getShortId());
} else {
methodsWithMappedElements.add(codeRename.getNodeRef().getDeclaringClass() + codeRename.getNodeRef().getShortId());
mappedMethodArgsAndVars.put(codeRename.getNodeRef().getDeclaringClass()
+ codeRename.getNodeRef().getShortId()
+ codeRename.getCodeRef(),
codeRename.getNewName());
}
}
}
for (ICodeComment codeComment : codeData.getComments()) {
comments.put(codeComment.getNodeRef().getDeclaringClass()
+ (codeComment.getNodeRef().getShortId() == null ? "" : codeComment.getNodeRef().getShortId())
+ (codeComment.getCodeRef() == null ? "" : codeComment.getCodeRef()),
codeComment.getComment());
if (codeComment.getCodeRef() != null) {
methodsWithMappedElements.add(codeComment.getNodeRef().getDeclaringClass() + codeComment.getNodeRef().getShortId());
}
}
try {
if (mappingFormat.hasSingleFile()) {
if (path.toFile().exists()) {
path.toFile().delete();
}
path.toFile().createNewFile();
} else {
FileUtils.makeDirs(path);
}
mappingTree.visitHeader();
mappingTree.visitNamespaces("official", Arrays.asList("named"));
mappingTree.visitContent();
for (ClassNode cls : root.getClasses()) {
ClassInfo classInfo = cls.getClassInfo();
String classPath = classInfo.makeRawFullName().replace('.', '/');
String rawClassName = classInfo.getRawName();
if (classInfo.hasAlias()
&& !classInfo.getAliasShortName().equals(classInfo.getShortName())
&& mappedClasses.contains(rawClassName)) {
mappingTree.visitClass(classPath);
String alias = classInfo.makeAliasRawFullName().replace('.', '/');
if (alias.startsWith(Consts.DEFAULT_PACKAGE_NAME)) {
alias = alias.substring(Consts.DEFAULT_PACKAGE_NAME.length() + 1);
}
mappingTree.visitDstName(MappedElementKind.CLASS, 0, alias);
}
if (comments.containsKey(rawClassName)) {
mappingTree.visitClass(classPath);
mappingTree.visitComment(MappedElementKind.CLASS, comments.get(rawClassName));
}
for (FieldNode fld : cls.getFields()) {
FieldInfo fieldInfo = fld.getFieldInfo();
if (fieldInfo.hasAlias() && mappedFields.contains(rawClassName + fieldInfo.getShortId())) {
visitField(mappingTree, classPath, fieldInfo.getName(), TypeGen.signature(fieldInfo.getType()));
mappingTree.visitDstName(MappedElementKind.FIELD, 0, fieldInfo.getAlias());
}
if (comments.containsKey(rawClassName + fieldInfo.getShortId())) {
visitField(mappingTree, classPath, fieldInfo.getName(), TypeGen.signature(fieldInfo.getType()));
mappingTree.visitComment(MappedElementKind.FIELD, comments.get(rawClassName + fieldInfo.getShortId()));
}
}
for (MethodNode mth : cls.getMethods()) {
MethodInfo methodInfo = mth.getMethodInfo();
String methodName = methodInfo.getName();
String methodDesc = methodInfo.getShortId().substring(methodName.length());
if (methodInfo.hasAlias() && mappedMethods.contains(rawClassName + methodInfo.getShortId())) {
visitMethod(mappingTree, classPath, methodName, methodDesc);
mappingTree.visitDstName(MappedElementKind.METHOD, 0, methodInfo.getAlias());
}
if (comments.containsKey(rawClassName + methodInfo.getShortId())) {
visitMethod(mappingTree, classPath, methodName, methodDesc);
mappingTree.visitComment(MappedElementKind.METHOD, comments.get(rawClassName + methodInfo.getShortId()));
}
if (!methodsWithMappedElements.contains(rawClassName + methodInfo.getShortId())) {
continue;
}
// Method args
int lvtIndex = mth.getAccessFlags().isStatic() ? 0 : 1;
int lastArgLvIndex = lvtIndex - 1;
List<VarNode> args = collectMethodArgs(mth);
for (VarNode arg : args) {
int lvIndex = arg.getReg() - args.get(0).getReg() + (mth.getAccessFlags().isStatic() ? 0 : 1);
String key = rawClassName + methodInfo.getShortId()
+ JadxCodeRef.forVar(arg.getReg(), arg.getSsa());
if (mappedMethodArgsAndVars.containsKey(key)) {
visitMethodArg(mappingTree, classPath, methodName, methodDesc, args.indexOf(arg), lvIndex);
mappingTree.visitDstName(MappedElementKind.METHOD_ARG, 0, mappedMethodArgsAndVars.get(key));
mappedMethodArgsAndVars.remove(key);
}
lastArgLvIndex = lvIndex;
lvtIndex++;
// Not checking for comments since method args can't have any
}
// Method vars
List<SimpleEntry<VarNode, Integer>> vars = collectMethodVars(mth);
for (SimpleEntry<VarNode, Integer> entry : vars) {
VarNode var = entry.getKey();
int offset = entry.getValue();
int lvIndex = lastArgLvIndex + var.getReg() + (mth.getAccessFlags().isStatic() ? 0 : 1);
String key = rawClassName + methodInfo.getShortId()
+ JadxCodeRef.forVar(var.getReg(), var.getSsa());
if (mappedMethodArgsAndVars.containsKey(key)) {
visitMethodVar(mappingTree, classPath, methodName, methodDesc, lvtIndex, lvIndex, offset);
mappingTree.visitDstName(MappedElementKind.METHOD_VAR, 0, mappedMethodArgsAndVars.get(key));
}
key = rawClassName + methodInfo.getShortId() + JadxCodeRef.forInsn(offset);
if (comments.containsKey(key)) {
visitMethodVar(mappingTree, classPath, methodName, methodDesc, lvtIndex, lvIndex, offset);
mappingTree.visitComment(MappedElementKind.METHOD_VAR, comments.get(key));
}
lvtIndex++;
}
}
}
MappingWriter writer = MappingWriter.create(path, mappingFormat);
mappingTree.accept(writer);
mappingTree.visitEnd();
writer.close();
} catch (IOException e) {
LOG.error("Failed to save deobfuscation map file '{}'", path.toAbsolutePath(), e);
}
}
private void visitField(MemoryMappingTree tree, String classPath, String srcName, String srcDesc) {
tree.visitClass(classPath);
tree.visitField(srcName, srcDesc);
}
private void visitMethod(MemoryMappingTree tree, String classPath, String srcName, String srcDesc) {
tree.visitClass(classPath);
tree.visitMethod(srcName, srcDesc);
}
private void visitMethodArg(MemoryMappingTree tree, String classPath, String methodSrcName, String methodSrcDesc, int argPosition,
int lvIndex) {
visitMethod(tree, classPath, methodSrcName, methodSrcDesc);
tree.visitMethodArg(argPosition, lvIndex, null);
}
private void visitMethodVar(MemoryMappingTree tree, String classPath, String methodSrcName, String methodSrcDesc, int lvtIndex,
int lvIndex, int startOpIdx) {
visitMethod(tree, classPath, methodSrcName, methodSrcDesc);
tree.visitMethodVar(lvtIndex, lvIndex, startOpIdx, null);
}
}

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