Compare commits
103 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 63a571306c | |||
| bc4db61e25 | |||
| c7e6e28830 | |||
| 1d7b6fdb2c | |||
| ce5d8eeff8 | |||
| 894e0e6132 | |||
| 127f0ecf3f | |||
| cf7767e702 | |||
| e0aedc7949 | |||
| bad78de74c | |||
| 12df8a169f | |||
| 15c9d33339 | |||
| 7e0fafbaf1 | |||
| 57b9c1dd7a | |||
| 8ba0c17259 | |||
| cd32151083 | |||
| 75b52d672e | |||
| 11d04508f7 | |||
| e641b773b5 | |||
| 6e5899c654 | |||
| c66ffaa7f9 | |||
| 5193c6a5d8 | |||
| e7212af547 | |||
| 3ca1357af4 | |||
| 90e95213e4 | |||
| ae2d4da585 | |||
| 691d5cd1e6 | |||
| 58a46c6417 | |||
| d3f6160e62 | |||
| 03e4afb12f | |||
| 6802f6028e | |||
| 5ca61cfe18 | |||
| 32d55b48f2 | |||
| ab4b6f9e54 | |||
| 9100ad1220 | |||
| 8b4f8fb572 | |||
| 87e0e5bf16 | |||
| e4c2d6cf6e | |||
| fb0bdb5112 | |||
| f4b3645435 | |||
| c27f2badf7 | |||
| 1a877d6535 | |||
| 5ada9331b6 | |||
| a0f4ccb7a4 | |||
| 5b5524a7dd | |||
| 3cc464c9c9 | |||
| 51555667cf | |||
| e01ea7010f | |||
| 77732c83c9 | |||
| a67fc83949 | |||
| 3d920725aa | |||
| 2f2fbea558 | |||
| e7a86a2960 | |||
| b282d97ffe | |||
| e4ca52a95f | |||
| d972d9ec74 | |||
| 0721a6b050 | |||
| 762ee6550e | |||
| 18070eb7a6 | |||
| 8486891728 | |||
| 4679172d4f | |||
| 92a6c333d8 | |||
| 358adbdd65 | |||
| 65f7c80222 | |||
| d2e6bb236e | |||
| eaeb114258 | |||
| 1533b7fe6e | |||
| a2cd8e1ead | |||
| 4edb512121 | |||
| 702b88228c | |||
| 14fd88b2f8 | |||
| 20657e8bb5 | |||
| 93d3194e3b | |||
| 39331d9120 | |||
| b4fa6644bc | |||
| 0b2e2ed034 | |||
| 81231206f3 | |||
| 49d0e76272 | |||
| 0809993b37 | |||
| 0c3afcc24c | |||
| d6c851eed4 | |||
| dcf4a7c4e3 | |||
| 9ba07b986b | |||
| e6b6b93cbb | |||
| fcd58ae76f | |||
| df380dea27 | |||
| 9d88592391 | |||
| c906c11b0f | |||
| 4fbc56cdb0 | |||
| 98c0416b20 | |||
| fa41874e30 | |||
| 2aa6c99c90 | |||
| 5f60c0f1bb | |||
| cb741db623 | |||
| 1df217c4a0 | |||
| 81f209ba9e | |||
| 34a31aa7df | |||
| 5099e02c9b | |||
| f364b39b29 | |||
| 4cd4746f9a | |||
| 6448f0e32b | |||
| e07332d49a | |||
| bd8a44c4c9 |
@@ -0,0 +1,7 @@
|
||||
version: 2
|
||||
updates:
|
||||
# Set update schedule for GitHub Actions
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
queries: +security-extended
|
||||
languages: ${{ matrix.language }}
|
||||
@@ -38,4 +38,4 @@ jobs:
|
||||
./gradlew clean build -x checkstyleTest -x checkstyleMain -x test -x ':jadx-core:testClasses'
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
uses: github/codeql-action/analyze@v2
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
## JADX
|
||||
|
||||
[](https://github.com/skylot/jadx/actions?query=workflow%3ABuild)
|
||||
[](https://lgtm.com/projects/g/skylot/jadx/alerts/)
|
||||
[](https://github.com/semantic-release/semantic-release)
|
||||

|
||||

|
||||

|
||||

|
||||
[](https://search.maven.org/search?q=g:io.github.skylot%20AND%20jadx)
|
||||
[](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"/>
|
||||
|
||||
### Download
|
||||
- release from [github: ](https://github.com/skylot/jadx/releases/latest)
|
||||
- latest [unstable build](https://nightly.link/skylot/jadx/workflows/build/master)
|
||||
- release
|
||||
from [github: ](https://github.com/skylot/jadx/releases/latest)
|
||||
- latest [unstable build ](https://nightly.link/skylot/jadx/workflows/build-artifacts/master)
|
||||
|
||||
After download unpack zip file go to `bin` directory and run:
|
||||
- `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).
|
||||
|
||||
### Install
|
||||
1. Arch linux
|
||||
1. Arch linux 
|
||||
```bash
|
||||
sudo pacman -S jadx
|
||||
sudo pacman -S jadx
|
||||
```
|
||||
2. macOS
|
||||
2. macOS 
|
||||
```bash
|
||||
brew install jadx
|
||||
brew install jadx
|
||||
```
|
||||
3. [Flathub ](https://flathub.org/apps/details/com.github.skylot.jadx)
|
||||
```bash
|
||||
flatpak install flathub com.github.skylot.jadx
|
||||
```
|
||||
|
||||
### Use jadx as a library
|
||||
You can use jadx in your java projects, check details on [wiki page](https://github.com/skylot/jadx/wiki/Use-jadx-as-a-library)
|
||||
@@ -95,6 +102,7 @@ options:
|
||||
--add-debug-lines - add comments with debug line numbers if available
|
||||
--no-inline-anonymous - disable anonymous classes inline
|
||||
--no-inline-methods - disable methods inline
|
||||
--no-finally - don't extract finally block
|
||||
--no-replace-consts - don't replace constant value with matching constant field
|
||||
--escape-unicode - escape non latin characters in strings (with \u)
|
||||
--respect-bytecode-access-modifiers - don't change original access modifiers
|
||||
@@ -107,9 +115,12 @@ options:
|
||||
'read-or-save' - read if found, save otherwise (don't overwrite)
|
||||
'overwrite' - don't read, always save
|
||||
'ignore' - don't read and don't save
|
||||
--deobf-rewrite-cfg - set '--deobf-cfg-file-mode' to 'overwrite' (deprecated)
|
||||
--deobf-use-sourcename - use source file name as class name alias
|
||||
--deobf-parse-kotlin-metadata - parse kotlin metadata to class and package names
|
||||
--deobf-res-name-source - better name source for resources:
|
||||
'auto' - automatically select best name (default)
|
||||
'resources' - use resources names
|
||||
'code' - use R class fields names
|
||||
--use-kotlin-methods-for-var-names - use kotlin intrinsic methods to rename variables, values: disable, apply, apply-and-hide, default: apply
|
||||
--rename-flags - fix options (comma-separated list of):
|
||||
'case' - fix case sensitivity issues (according to --fs-case-sensitive option),
|
||||
@@ -130,11 +141,11 @@ options:
|
||||
-h, --help - print this help
|
||||
|
||||
Plugin options (-P<name>=<value>):
|
||||
1) dex-input (Load .dex and .apk files)
|
||||
-Pdex-input.verify-checksum - Verify dex file checksum before load, values: [yes, no], default: yes
|
||||
2) java-convert (Convert .jar and .class files to dex)
|
||||
-Pjava-convert.mode - Convert mode, values: [dx, d8, both], default: both
|
||||
-Pjava-convert.d8-desugar - Use desugar in d8, values: [yes, no], default: no
|
||||
1) dex-input: Load .dex and .apk files
|
||||
- dex-input.verify-checksum - verify dex file checksum before load, values: [yes, no], default: yes
|
||||
2) java-convert: Convert .class, .jar and .aar files to dex
|
||||
- java-convert.mode - convert mode, values: [dx, d8, both], default: both
|
||||
- java-convert.d8-desugar - use desugar in d8, values: [yes, no], default: no
|
||||
|
||||
Examples:
|
||||
jadx -d out classes.dex
|
||||
|
||||
-33
@@ -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>
|
||||
+11
-13
@@ -1,6 +1,6 @@
|
||||
plugins {
|
||||
id 'com.github.ben-manes.versions' version '0.42.0'
|
||||
id 'com.diffplug.spotless' version '6.5.0'
|
||||
id 'com.diffplug.spotless' version '6.9.1'
|
||||
}
|
||||
|
||||
ext.jadxVersion = System.getenv('JADX_VERSION') ?: "dev"
|
||||
@@ -14,7 +14,6 @@ allprojects {
|
||||
version = jadxVersion
|
||||
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
|
||||
compileJava {
|
||||
options.encoding = "UTF-8"
|
||||
@@ -32,11 +31,11 @@ allprojects {
|
||||
|
||||
testImplementation 'ch.qos.logback:logback-classic:1.2.11'
|
||||
testImplementation 'org.hamcrest:hamcrest-library:2.2'
|
||||
testImplementation 'org.mockito:mockito-core:4.5.1'
|
||||
testImplementation 'org.assertj:assertj-core:3.22.0'
|
||||
testImplementation 'org.mockito:mockito-core:4.7.0'
|
||||
testImplementation 'org.assertj:assertj-core:3.23.1'
|
||||
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
|
||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0'
|
||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0'
|
||||
|
||||
testCompileOnly 'org.jetbrains:annotations:23.0.0'
|
||||
}
|
||||
@@ -50,6 +49,11 @@ allprojects {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
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'
|
||||
eclipse().configFile 'config/code-formatter/eclipse.xml'
|
||||
if (JavaVersion.current() < JavaVersion.VERSION_16) {
|
||||
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)')
|
||||
}
|
||||
removeUnusedImports()
|
||||
|
||||
lineEndings(com.diffplug.spotless.LineEnding.UNIX)
|
||||
encoding("UTF-8")
|
||||
|
||||
@@ -1,2 +1,11 @@
|
||||
org.gradle.warning.mode=all
|
||||
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'
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+2
-2
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=29e49b10984e585d8118b7d0bc452f944e386458df27371b49b4ac1dec4b7fda
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
|
||||
distributionSha256Sum=f6b8596b10cce501591e92f229816aa4046424f3b24d771751b06779d58c8ec4
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -205,6 +205,12 @@ set -- \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
echo "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
|
||||
Vendored
+8
-6
@@ -14,7 +14,7 @@
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@@ -25,7 +25,7 @@
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
@@ -75,13 +75,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
@@ -8,6 +8,7 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -22,6 +23,7 @@ import jadx.api.plugins.JadxPluginInfo;
|
||||
import jadx.api.plugins.JadxPluginManager;
|
||||
import jadx.api.plugins.options.JadxPluginOptions;
|
||||
import jadx.api.plugins.options.OptionDescription;
|
||||
import jadx.core.utils.Utils;
|
||||
|
||||
public class JCommanderWrapper<T> {
|
||||
private final JCommander jc;
|
||||
@@ -50,12 +52,24 @@ public class JCommanderWrapper<T> {
|
||||
if (parameter.isAssigned()) {
|
||||
// copy assigned field value to obj
|
||||
Parameterized parameterized = parameter.getParameterized();
|
||||
Object val = parameterized.get(parameter.getObject());
|
||||
parameterized.set(obj, val);
|
||||
Object providedValue = parameterized.get(parameter.getObject());
|
||||
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() {
|
||||
// print usage in not sorted fields order (by default its sorted by description)
|
||||
PrintStream out = System.out;
|
||||
@@ -179,11 +193,11 @@ public class JCommanderWrapper<T> {
|
||||
return false;
|
||||
}
|
||||
JadxPluginInfo pluginInfo = plugin.getPluginInfo();
|
||||
out.append("\n ").append(k).append(") ");
|
||||
out.append(pluginInfo.getPluginId()).append(" (").append(pluginInfo.getDescription()).append(") ");
|
||||
out.append("\n ").append(k).append(") ");
|
||||
out.append(pluginInfo.getPluginId()).append(": ").append(pluginInfo.getDescription());
|
||||
for (OptionDescription desc : descs) {
|
||||
StringBuilder opt = new StringBuilder();
|
||||
opt.append(" -P").append(desc.name());
|
||||
opt.append(" - ").append(desc.name());
|
||||
addSpaces(opt, maxNamesLen - opt.length());
|
||||
opt.append("- ").append(desc.description());
|
||||
if (!desc.values().isEmpty()) {
|
||||
|
||||
@@ -66,8 +66,14 @@ public class JadxCLI {
|
||||
|
||||
private static boolean checkForErrors(JadxDecompiler jadx) {
|
||||
if (jadx.getRoot().getClasses().isEmpty()) {
|
||||
LOG.error("Load failed! No classes for decompile!");
|
||||
return true;
|
||||
if (jadx.getArgs().isSkipResources()) {
|
||||
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) {
|
||||
LOG.error("Load with errors! Check log for details");
|
||||
|
||||
@@ -21,6 +21,7 @@ import jadx.api.JadxArgs.RenameEnum;
|
||||
import jadx.api.JadxArgs.UseKotlinMethodsForVarNames;
|
||||
import jadx.api.JadxDecompiler;
|
||||
import jadx.api.args.DeobfuscationMapFileMode;
|
||||
import jadx.api.args.ResourceNameSource;
|
||||
import jadx.core.utils.exceptions.JadxException;
|
||||
import jadx.core.utils.files.FileUtils;
|
||||
|
||||
@@ -88,6 +89,9 @@ public class JadxCLIArgs {
|
||||
@Parameter(names = { "--no-inline-methods" }, description = "disable methods inline")
|
||||
protected boolean inlineMethods = true;
|
||||
|
||||
@Parameter(names = "--no-finally", description = "don't extract finally block")
|
||||
protected boolean extractFinally = true;
|
||||
|
||||
@Parameter(names = "--no-replace-consts", description = "don't replace constant value with matching constant field")
|
||||
protected boolean replaceConsts = true;
|
||||
|
||||
@@ -123,15 +127,22 @@ public class JadxCLIArgs {
|
||||
)
|
||||
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")
|
||||
protected boolean deobfuscationUseSourceNameAsAlias = false;
|
||||
|
||||
@Parameter(names = { "--deobf-parse-kotlin-metadata" }, description = "parse kotlin metadata to class and package names")
|
||||
protected boolean deobfuscationParseKotlinMetadata = false;
|
||||
|
||||
@Parameter(
|
||||
names = { "--deobf-res-name-source" },
|
||||
description = "better name source for resources:"
|
||||
+ "\n 'auto' - automatically select best name (default)"
|
||||
+ "\n 'resources' - use resources names"
|
||||
+ "\n 'code' - use R class fields names",
|
||||
converter = ResourceNameSourceConverter.class
|
||||
)
|
||||
protected ResourceNameSource resourceNameSource = ResourceNameSource.AUTO;
|
||||
|
||||
@Parameter(
|
||||
names = { "--use-kotlin-methods-for-var-names" },
|
||||
description = "use kotlin intrinsic methods to rename variables, values: disable, apply, apply-and-hide",
|
||||
@@ -259,16 +270,13 @@ public class JadxCLIArgs {
|
||||
args.setReplaceConsts(replaceConsts);
|
||||
args.setDeobfuscationOn(deobfuscationOn);
|
||||
args.setDeobfuscationMapFile(FileUtils.toFile(deobfuscationMapFile));
|
||||
if (deobfuscationForceSave) {
|
||||
args.setDeobfuscationMapFileMode(DeobfuscationMapFileMode.OVERWRITE);
|
||||
} else {
|
||||
args.setDeobfuscationMapFileMode(deobfuscationMapFileMode);
|
||||
}
|
||||
args.setDeobfuscationMapFileMode(deobfuscationMapFileMode);
|
||||
args.setDeobfuscationMinLength(deobfuscationMinLength);
|
||||
args.setDeobfuscationMaxLength(deobfuscationMaxLength);
|
||||
args.setUseSourceNameAsClassAlias(deobfuscationUseSourceNameAsAlias);
|
||||
args.setParseKotlinMetadata(deobfuscationParseKotlinMetadata);
|
||||
args.setUseKotlinMethodsForVarNames(useKotlinMethodsForVarNames);
|
||||
args.setResourceNameSource(resourceNameSource);
|
||||
args.setEscapeUnicode(escapeUnicode);
|
||||
args.setRespectBytecodeAccModifiers(respectBytecodeAccessModifiers);
|
||||
args.setExportAsGradleProject(exportAsGradleProject);
|
||||
@@ -277,6 +285,7 @@ public class JadxCLIArgs {
|
||||
args.setInsertDebugLines(addDebugLines);
|
||||
args.setInlineAnonymousClasses(inlineAnonymousClasses);
|
||||
args.setInlineMethods(inlineMethods);
|
||||
args.setExtractFinally(extractFinally);
|
||||
args.setRenameFlags(renameFlags);
|
||||
args.setFsCaseSensitive(fsCaseSensitive);
|
||||
args.setCommentsLevel(commentsLevel);
|
||||
@@ -357,6 +366,10 @@ public class JadxCLIArgs {
|
||||
return inlineMethods;
|
||||
}
|
||||
|
||||
public boolean isExtractFinally() {
|
||||
return extractFinally;
|
||||
}
|
||||
|
||||
public boolean isDeobfuscationOn() {
|
||||
return deobfuscationOn;
|
||||
}
|
||||
@@ -377,10 +390,6 @@ public class JadxCLIArgs {
|
||||
return deobfuscationMapFileMode;
|
||||
}
|
||||
|
||||
public boolean isDeobfuscationForceSave() {
|
||||
return deobfuscationForceSave;
|
||||
}
|
||||
|
||||
public boolean isDeobfuscationUseSourceNameAsAlias() {
|
||||
return deobfuscationUseSourceNameAsAlias;
|
||||
}
|
||||
@@ -389,6 +398,10 @@ public class JadxCLIArgs {
|
||||
return deobfuscationParseKotlinMetadata;
|
||||
}
|
||||
|
||||
public ResourceNameSource getResourceNameSource() {
|
||||
return resourceNameSource;
|
||||
}
|
||||
|
||||
public UseKotlinMethodsForVarNames getUseKotlinMethodsForVarNames() {
|
||||
return useKotlinMethodsForVarNames;
|
||||
}
|
||||
@@ -513,6 +526,19 @@ public class JadxCLIArgs {
|
||||
}
|
||||
}
|
||||
|
||||
public static class ResourceNameSourceConverter implements IStringConverter<ResourceNameSource> {
|
||||
@Override
|
||||
public ResourceNameSource convert(String value) {
|
||||
try {
|
||||
return ResourceNameSource.valueOf(value.toUpperCase());
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException(
|
||||
'\'' + value + "' is unknown, possible values are: "
|
||||
+ JadxCLIArgs.enumValuesString(ResourceNameSource.values()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class DecompilationModeConverter implements IStringConverter<DecompilationMode> {
|
||||
@Override
|
||||
public DecompilationMode convert(String value) {
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
package jadx.cli;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import static jadx.core.utils.Utils.newConstStringMap;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
||||
@@ -47,6 +52,40 @@ public class JadxCLIArgsTest {
|
||||
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) {
|
||||
return parse(new JadxCLIArgs(), args);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,33 @@ public class TestInput {
|
||||
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 {
|
||||
List<String> args = new ArrayList<>();
|
||||
Path tempDir = FileUtils.createTempDir(tmpDirName);
|
||||
|
||||
Binary file not shown.
@@ -5,12 +5,11 @@ plugins {
|
||||
dependencies {
|
||||
api(project(':jadx-plugins:jadx-plugins-api'))
|
||||
|
||||
implementation 'com.google.code.gson:gson:2.9.0'
|
||||
implementation 'com.android.tools.build:aapt2-proto:4.2.1-7147631'
|
||||
constraints {
|
||||
// Force protobuf version to prevent Java-7 issue
|
||||
implementation 'com.google.protobuf:protobuf-java:3.11.4'
|
||||
}
|
||||
implementation 'com.google.code.gson:gson:2.9.1'
|
||||
|
||||
// TODO: move resources decoding to separate plugin module
|
||||
implementation 'com.android.tools.build:aapt2-proto:7.2.2-7984345'
|
||||
implementation 'com.google.protobuf:protobuf-java:3.21.5' // forcing latest version
|
||||
|
||||
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-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'
|
||||
}
|
||||
|
||||
|
||||
@@ -12,13 +12,18 @@ import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.args.DeobfuscationMapFileMode;
|
||||
import jadx.api.args.ResourceNameSource;
|
||||
import jadx.api.data.ICodeData;
|
||||
import jadx.api.impl.AnnotatedCodeWriter;
|
||||
import jadx.api.impl.InMemoryCodeCache;
|
||||
import jadx.core.utils.files.FileUtils;
|
||||
|
||||
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);
|
||||
|
||||
@@ -68,6 +73,7 @@ public class JadxArgs {
|
||||
private File deobfuscationMapFile = null;
|
||||
|
||||
private DeobfuscationMapFileMode deobfuscationMapFileMode = DeobfuscationMapFileMode.READ;
|
||||
private ResourceNameSource resourceNameSource = ResourceNameSource.AUTO;
|
||||
|
||||
private int deobfuscationMinLength = 0;
|
||||
private int deobfuscationMaxLength = Integer.MAX_VALUE;
|
||||
@@ -122,6 +128,19 @@ public class JadxArgs {
|
||||
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() {
|
||||
return inputFiles;
|
||||
}
|
||||
@@ -352,6 +371,14 @@ public class JadxArgs {
|
||||
this.deobfuscationMapFile = deobfuscationMapFile;
|
||||
}
|
||||
|
||||
public ResourceNameSource getResourceNameSource() {
|
||||
return resourceNameSource;
|
||||
}
|
||||
|
||||
public void setResourceNameSource(ResourceNameSource resourceNameSource) {
|
||||
this.resourceNameSource = resourceNameSource;
|
||||
}
|
||||
|
||||
public boolean isEscapeUnicode() {
|
||||
return escapeUnicode;
|
||||
}
|
||||
@@ -523,6 +550,7 @@ public class JadxArgs {
|
||||
String argStr = "args:" + decompilationMode + useImports + showInconsistentCode
|
||||
+ inlineAnonymousClasses + inlineMethods
|
||||
+ deobfuscationOn + deobfuscationMinLength + deobfuscationMaxLength
|
||||
+ resourceNameSource
|
||||
+ parseKotlinMetadata + useKotlinMethodsForVarNames
|
||||
+ insertDebugLines + extractFinally
|
||||
+ debugInfo + useSourceNameAsClassAlias + escapeUnicode + replaceConsts
|
||||
@@ -547,6 +575,7 @@ public class JadxArgs {
|
||||
+ ", deobfuscationOn=" + deobfuscationOn
|
||||
+ ", deobfuscationMapFile=" + deobfuscationMapFile
|
||||
+ ", deobfuscationMapFileMode=" + deobfuscationMapFileMode
|
||||
+ ", resourceNameSource=" + resourceNameSource
|
||||
+ ", useSourceNameAsClassAlias=" + useSourceNameAsClassAlias
|
||||
+ ", parseKotlinMetadata=" + parseKotlinMetadata
|
||||
+ ", useKotlinMethodsForVarNames=" + useKotlinMethodsForVarNames
|
||||
|
||||
@@ -37,8 +37,6 @@ import jadx.api.plugins.input.data.ILoadResult;
|
||||
import jadx.api.plugins.options.JadxPluginOptions;
|
||||
import jadx.core.Jadx;
|
||||
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.FieldNode;
|
||||
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<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<>();
|
||||
|
||||
@@ -161,8 +159,13 @@ public final class JadxDecompiler implements Closeable {
|
||||
classesMap.clear();
|
||||
methodsMap.clear();
|
||||
fieldsMap.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
reset();
|
||||
closeInputs();
|
||||
args.close();
|
||||
}
|
||||
|
||||
private void closeInputs() {
|
||||
@@ -176,11 +179,6 @@ public final class JadxDecompiler implements Closeable {
|
||||
loadedInputs.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
reset();
|
||||
}
|
||||
|
||||
private void loadPlugins(JadxArgs args) {
|
||||
pluginManager.providesSuggestion("java-input", args.isUseDxInput() ? "java-convert" : "java-input");
|
||||
pluginManager.load();
|
||||
@@ -202,6 +200,7 @@ public final class JadxDecompiler implements Closeable {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public void registerPlugin(JadxPlugin plugin) {
|
||||
pluginManager.register(plugin);
|
||||
}
|
||||
@@ -357,8 +356,9 @@ public final class JadxDecompiler implements Closeable {
|
||||
tasks.add(() -> {
|
||||
for (JavaClass cls : decompileBatch) {
|
||||
try {
|
||||
ICodeInfo code = cls.getCodeInfo();
|
||||
SaveCode.save(outDir, cls.getClassNode(), code);
|
||||
ClassNode clsNode = cls.getClassNode();
|
||||
ICodeInfo code = clsNode.getCode();
|
||||
SaveCode.save(outDir, clsNode, code);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Error saving class: {}", cls, e);
|
||||
}
|
||||
@@ -467,23 +467,10 @@ public final class JadxDecompiler implements Closeable {
|
||||
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
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
JavaClass convertClassNode(ClassNode cls) {
|
||||
return classesMap.compute(cls, (node, prevJavaCls) -> {
|
||||
if (prevJavaCls != null && prevJavaCls.getClassNode() == cls) {
|
||||
@@ -497,100 +484,20 @@ public final class JadxDecompiler implements Closeable {
|
||||
});
|
||||
}
|
||||
|
||||
@Nullable("For not generated classes")
|
||||
@ApiStatus.Internal
|
||||
public JavaClass getJavaClassByNode(ClassNode cls) {
|
||||
JavaClass javaClass = classesMap.get(cls);
|
||||
if (javaClass != null && javaClass.getClassNode() == cls) {
|
||||
return javaClass;
|
||||
}
|
||||
// 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);
|
||||
JavaField convertFieldNode(FieldNode field) {
|
||||
return fieldsMap.computeIfAbsent(field, fldNode -> {
|
||||
JavaClass parentCls = convertClassNode(fldNode.getParentClass());
|
||||
return new JavaField(parentCls, fldNode);
|
||||
});
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
@Nullable
|
||||
public JavaMethod getJavaMethodByNode(MethodNode mth) {
|
||||
JavaMethod javaMethod = methodsMap.get(mth);
|
||||
if (javaMethod != null && javaMethod.getMethodNode() == mth) {
|
||||
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);
|
||||
JavaMethod convertMethodNode(MethodNode method) {
|
||||
return methodsMap.computeIfAbsent(method, mthNode -> {
|
||||
ClassNode parentCls = mthNode.getParentClass();
|
||||
return new JavaMethod(convertClassNode(parentCls), mthNode);
|
||||
});
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -598,7 +505,7 @@ public final class JadxDecompiler implements Closeable {
|
||||
return getRoot().getClasses().stream()
|
||||
.filter(cls -> cls.getClassInfo().getFullName().equals(fullName))
|
||||
.findFirst()
|
||||
.map(this::getJavaClassByNode)
|
||||
.map(this::convertClassNode)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@@ -619,9 +526,9 @@ public final class JadxDecompiler implements Closeable {
|
||||
.orElse(null);
|
||||
if (node != null) {
|
||||
if (node.contains(AFlag.DONT_GENERATE)) {
|
||||
return getJavaClassByNode(node.getTopParentClass());
|
||||
return convertClassNode(node.getTopParentClass());
|
||||
} else {
|
||||
return getJavaClassByNode(node);
|
||||
return convertClassNode(node);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -632,7 +539,7 @@ public final class JadxDecompiler implements Closeable {
|
||||
return getRoot().getClasses().stream()
|
||||
.filter(cls -> cls.getClassInfo().getAliasFullName().equals(fullName))
|
||||
.findFirst()
|
||||
.map(this::getJavaClassByNode)
|
||||
.map(this::convertClassNode)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@@ -650,9 +557,9 @@ public final class JadxDecompiler implements Closeable {
|
||||
case CLASS:
|
||||
return convertClassNode((ClassNode) ann);
|
||||
case METHOD:
|
||||
return getJavaMethodByNode((MethodNode) ann);
|
||||
return convertMethodNode((MethodNode) ann);
|
||||
case FIELD:
|
||||
return getJavaFieldByNode((FieldNode) ann);
|
||||
return convertFieldNode((FieldNode) ann);
|
||||
case DECLARATION:
|
||||
return getJavaNodeByCodeAnnotation(codeInfo, ((NodeDeclareRef) ann).getNode());
|
||||
case VAR:
|
||||
@@ -670,7 +577,7 @@ public final class JadxDecompiler implements Closeable {
|
||||
@Nullable
|
||||
private JavaVariable resolveVarNode(VarNode varNode) {
|
||||
MethodNode mthNode = varNode.getMth();
|
||||
JavaMethod mth = getJavaMethodByNode(mthNode);
|
||||
JavaMethod mth = convertMethodNode(mthNode);
|
||||
if (mth == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -683,10 +590,13 @@ public final class JadxDecompiler implements Closeable {
|
||||
throw new JadxRuntimeException("Missing code info for resolve VarRef: " + varRef);
|
||||
}
|
||||
ICodeAnnotation varNodeAnn = codeInfo.getCodeMetadata().getAt(varRef.getRefPos());
|
||||
if (varNodeAnn == null) {
|
||||
return null;
|
||||
if (varNodeAnn != null && varNodeAnn.getAnnType() == ICodeAnnotation.AnnType.DECLARATION) {
|
||||
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) {
|
||||
|
||||
@@ -10,18 +10,23 @@ import java.util.Map;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.metadata.ICodeAnnotation;
|
||||
import jadx.api.metadata.ICodeNodeRef;
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.attributes.AType;
|
||||
import jadx.core.dex.attributes.nodes.AnonymousClassAttr;
|
||||
import jadx.core.dex.attributes.nodes.InlinedAttr;
|
||||
import jadx.core.dex.info.AccessInfo;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.core.dex.nodes.FieldNode;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
import jadx.core.utils.ListUtils;
|
||||
|
||||
public final class JavaClass implements JavaNode {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(JavaClass.class);
|
||||
|
||||
private final JadxDecompiler decompiler;
|
||||
private final ClassNode cls;
|
||||
@@ -53,7 +58,10 @@ public final class JavaClass implements JavaNode {
|
||||
}
|
||||
|
||||
public @NotNull ICodeInfo getCodeInfo() {
|
||||
load();
|
||||
ICodeInfo code = load();
|
||||
if (code != null) {
|
||||
return code;
|
||||
}
|
||||
return cls.decompile();
|
||||
}
|
||||
|
||||
@@ -83,6 +91,14 @@ public final class JavaClass implements JavaNode {
|
||||
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!
|
||||
*/
|
||||
@@ -94,20 +110,24 @@ public final class JavaClass implements JavaNode {
|
||||
/**
|
||||
* Decompile class and loads internal lists of fields, methods, etc.
|
||||
* 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 void load() {
|
||||
private synchronized @Nullable ICodeInfo load() {
|
||||
if (listsLoaded) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
listsLoaded = true;
|
||||
JadxDecompiler rootDecompiler = getRootDecompiler();
|
||||
ICodeCache codeCache = rootDecompiler.getArgs().getCodeCache();
|
||||
if (!codeCache.contains(cls.getRawName())) {
|
||||
cls.decompile();
|
||||
|
||||
ICodeInfo code;
|
||||
if (cls.getState().isProcessComplete()) {
|
||||
// already decompiled -> class internals loaded
|
||||
code = null;
|
||||
} else {
|
||||
code = cls.decompile();
|
||||
}
|
||||
|
||||
JadxDecompiler rootDecompiler = getRootDecompiler();
|
||||
int inClsCount = cls.getInnerClasses().size();
|
||||
if (inClsCount != 0) {
|
||||
List<JavaClass> list = new ArrayList<>(inClsCount);
|
||||
@@ -135,10 +155,9 @@ public final class JavaClass implements JavaNode {
|
||||
if (fieldsCount != 0) {
|
||||
List<JavaField> flds = new ArrayList<>(fieldsCount);
|
||||
for (FieldNode f : cls.getFields()) {
|
||||
// if (!f.contains(AFlag.DONT_GENERATE)) {
|
||||
JavaField javaField = new JavaField(this, f);
|
||||
flds.add(javaField);
|
||||
// }
|
||||
if (!f.contains(AFlag.DONT_GENERATE)) {
|
||||
flds.add(rootDecompiler.convertFieldNode(f));
|
||||
}
|
||||
}
|
||||
this.fields = Collections.unmodifiableList(flds);
|
||||
}
|
||||
@@ -148,16 +167,16 @@ public final class JavaClass implements JavaNode {
|
||||
List<JavaMethod> mths = new ArrayList<>(methodsCount);
|
||||
for (MethodNode m : cls.getMethods()) {
|
||||
if (!m.contains(AFlag.DONT_GENERATE)) {
|
||||
JavaMethod javaMethod = new JavaMethod(this, m);
|
||||
mths.add(javaMethod);
|
||||
mths.add(rootDecompiler.convertMethodNode(m));
|
||||
}
|
||||
}
|
||||
mths.sort(Comparator.comparing(JavaMethod::getName));
|
||||
this.methods = Collections.unmodifiableList(mths);
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
protected JadxDecompiler getRootDecompiler() {
|
||||
JadxDecompiler getRootDecompiler() {
|
||||
if (parent != null) {
|
||||
return parent.getRootDecompiler();
|
||||
}
|
||||
@@ -188,23 +207,16 @@ public final class JavaClass implements JavaNode {
|
||||
}
|
||||
|
||||
public List<Integer> getUsePlacesFor(ICodeInfo codeInfo, JavaNode javaNode) {
|
||||
Map<Integer, ICodeAnnotation> map = codeInfo.getCodeMetadata().getAsMap();
|
||||
if (map.isEmpty() || decompiler == null) {
|
||||
if (!codeInfo.hasMetadata()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
JadxDecompiler rootDec = getRootDecompiler();
|
||||
List<Integer> result = new ArrayList<>();
|
||||
for (Map.Entry<Integer, ICodeAnnotation> entry : map.entrySet()) {
|
||||
ICodeAnnotation ann = entry.getValue();
|
||||
if (ann.getAnnType() == ICodeAnnotation.AnnType.DECLARATION) {
|
||||
// ignore declarations
|
||||
continue;
|
||||
codeInfo.getCodeMetadata().searchDown(0, (pos, ann) -> {
|
||||
if (javaNode.isOwnCodeAnnotation(ann)) {
|
||||
result.add(pos);
|
||||
}
|
||||
JavaNode annNode = rootDec.getJavaNodeByCodeAnnotation(codeInfo, ann);
|
||||
if (javaNode.equals(annNode)) {
|
||||
result.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -240,19 +252,37 @@ public final class JavaClass implements JavaNode {
|
||||
return parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaClass getTopParentClass() {
|
||||
if (cls.contains(AType.ANONYMOUS_CLASS)) {
|
||||
// moved to usage class
|
||||
return getParentForAnonymousClass();
|
||||
}
|
||||
return parent == null ? this : parent.getTopParentClass();
|
||||
public JavaClass getOriginalTopParentClass() {
|
||||
return parent == null ? this : parent.getOriginalTopParentClass();
|
||||
}
|
||||
|
||||
private JavaClass getParentForAnonymousClass() {
|
||||
AnonymousClassAttr attr = cls.get(AType.ANONYMOUS_CLASS);
|
||||
ClassNode topParentClass = attr.getOuterCls().getTopParentClass();
|
||||
return getRootDecompiler().convertClassNode(topParentClass);
|
||||
/**
|
||||
* Return top parent class which contains code of this class.
|
||||
* Code parent can be different from original parent after move or inline
|
||||
*
|
||||
* @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() {
|
||||
@@ -285,7 +315,16 @@ public final class JavaClass implements JavaNode {
|
||||
if (methodNode == 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
|
||||
|
||||
@@ -4,6 +4,7 @@ import java.util.List;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
import jadx.api.metadata.ICodeAnnotation;
|
||||
import jadx.core.dex.info.AccessInfo;
|
||||
import jadx.core.dex.instructions.args.ArgType;
|
||||
import jadx.core.dex.nodes.FieldNode;
|
||||
@@ -65,6 +66,14 @@ public final class JavaField implements JavaNode {
|
||||
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!
|
||||
*/
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.jetbrains.annotations.ApiStatus;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.metadata.ICodeAnnotation;
|
||||
import jadx.core.dex.attributes.AType;
|
||||
import jadx.core.dex.attributes.nodes.MethodOverrideAttr;
|
||||
import jadx.core.dex.info.AccessInfo;
|
||||
@@ -18,6 +19,7 @@ import jadx.core.utils.Utils;
|
||||
|
||||
public final class JavaMethod implements JavaNode {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(JavaMethod.class);
|
||||
|
||||
private final MethodNode mth;
|
||||
private final JavaClass parent;
|
||||
|
||||
@@ -78,7 +80,7 @@ public final class JavaMethod implements JavaNode {
|
||||
JadxDecompiler decompiler = getDeclaringClass().getRootDecompiler();
|
||||
return ovrdAttr.getRelatedMthNodes().stream()
|
||||
.map(m -> {
|
||||
JavaMethod javaMth = decompiler.getJavaMethodByNode(m);
|
||||
JavaMethod javaMth = decompiler.convertMethodNode(m);
|
||||
if (javaMth == null) {
|
||||
LOG.warn("Failed convert to java method: {}", m);
|
||||
}
|
||||
@@ -106,6 +108,14 @@ public final class JavaMethod implements JavaNode {
|
||||
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!
|
||||
*/
|
||||
|
||||
@@ -2,6 +2,8 @@ package jadx.api;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import jadx.api.metadata.ICodeAnnotation;
|
||||
|
||||
public interface JavaNode {
|
||||
|
||||
String getName();
|
||||
@@ -18,4 +20,6 @@ public interface JavaNode {
|
||||
|
||||
default void removeAlias() {
|
||||
}
|
||||
|
||||
boolean isOwnCodeAnnotation(ICodeAnnotation ann);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import java.util.List;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import jadx.api.metadata.ICodeAnnotation;
|
||||
|
||||
public final class JavaPackage implements JavaNode, Comparable<JavaPackage> {
|
||||
private final String name;
|
||||
private final List<JavaClass> classes;
|
||||
@@ -49,6 +51,11 @@ public final class JavaPackage implements JavaNode, Comparable<JavaPackage> {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOwnCodeAnnotation(ICodeAnnotation ann) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NotNull JavaPackage o) {
|
||||
return name.compareTo(o.name);
|
||||
|
||||
@@ -4,8 +4,12 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
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.VarRef;
|
||||
import jadx.core.dex.instructions.args.ArgType;
|
||||
|
||||
public class JavaVariable implements JavaNode {
|
||||
private final JavaMethod mth;
|
||||
@@ -29,7 +33,7 @@ public class JavaVariable implements JavaNode {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
public @Nullable String getName() {
|
||||
return varNode.getName();
|
||||
}
|
||||
|
||||
@@ -43,6 +47,10 @@ public class JavaVariable implements JavaNode {
|
||||
return varNode.getType() + " " + varNode.getName() + " (r" + varNode.getReg() + "v" + varNode.getSsa() + ")";
|
||||
}
|
||||
|
||||
public ArgType getType() {
|
||||
return ArgType.tryToResolveClassAlias(mth.getMethodNode().root(), varNode.getType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaClass getDeclaringClass() {
|
||||
return mth.getDeclaringClass();
|
||||
@@ -63,6 +71,15 @@ public class JavaVariable implements JavaNode {
|
||||
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
|
||||
public int hashCode() {
|
||||
return varNode.hashCode();
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package jadx.api.args;
|
||||
|
||||
/**
|
||||
* Resources original name source (for deobfuscation)
|
||||
*/
|
||||
public enum ResourceNameSource {
|
||||
|
||||
/**
|
||||
* Automatically select best name (default)
|
||||
*/
|
||||
AUTO,
|
||||
|
||||
/**
|
||||
* Force use resources provided names
|
||||
*/
|
||||
RESOURCES,
|
||||
|
||||
/**
|
||||
* Force use resources names from R class
|
||||
*/
|
||||
CODE,
|
||||
}
|
||||
@@ -9,7 +9,8 @@ public interface ICodeAnnotation {
|
||||
VAR,
|
||||
VAR_REF,
|
||||
DECLARATION,
|
||||
OFFSET
|
||||
OFFSET,
|
||||
END // class or method body end
|
||||
}
|
||||
|
||||
AnnType getAnnType();
|
||||
|
||||
@@ -32,6 +32,22 @@ public class NodeDeclareRef implements ICodeAnnotation {
|
||||
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
|
||||
public String toString() {
|
||||
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.TreeMap;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.api.metadata.ICodeAnnotation;
|
||||
import jadx.api.metadata.ICodeAnnotation.AnnType;
|
||||
import jadx.api.metadata.ICodeMetadata;
|
||||
import jadx.api.metadata.ICodeNodeRef;
|
||||
import jadx.api.metadata.annotations.NodeDeclareRef;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
import jadx.core.utils.Utils;
|
||||
|
||||
public class CodeMetadataStorage implements ICodeMetadata {
|
||||
@@ -55,7 +53,7 @@ public class CodeMetadataStorage implements ICodeMetadata {
|
||||
}
|
||||
|
||||
@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()) {
|
||||
if (v.getAnnType() == annType) {
|
||||
return v;
|
||||
@@ -65,7 +63,7 @@ public class CodeMetadataStorage implements ICodeMetadata {
|
||||
}
|
||||
|
||||
@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()) {
|
||||
if (v.getAnnType() == annType) {
|
||||
return v;
|
||||
@@ -99,28 +97,40 @@ public class CodeMetadataStorage implements ICodeMetadata {
|
||||
|
||||
@Override
|
||||
public ICodeNodeRef getNodeAt(int position) {
|
||||
return navMap.tailMap(position, true)
|
||||
.values().stream()
|
||||
.flatMap(CodeMetadataStorage::mapEnclosingNode)
|
||||
.findFirst().orElse(null);
|
||||
int nesting = 0;
|
||||
for (ICodeAnnotation ann : navMap.tailMap(position, true).values()) {
|
||||
switch (ann.getAnnType()) {
|
||||
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
|
||||
public ICodeNodeRef getNodeBelow(int position) {
|
||||
return navMap.headMap(position, true).descendingMap()
|
||||
.values().stream()
|
||||
.flatMap(CodeMetadataStorage::mapEnclosingNode)
|
||||
.findFirst().orElse(null);
|
||||
}
|
||||
|
||||
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);
|
||||
for (ICodeAnnotation ann : navMap.headMap(position, true).descendingMap().values()) {
|
||||
if (ann.getAnnType() == AnnType.DECLARATION) {
|
||||
ICodeNodeRef node = ((NodeDeclareRef) ann).getNode();
|
||||
AnnType nodeType = node.getAnnType();
|
||||
if (nodeType == AnnType.CLASS || nodeType == AnnType.METHOD) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Stream.empty();
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -135,7 +145,7 @@ public class CodeMetadataStorage implements ICodeMetadata {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CodeMetadata{lines=" + lines
|
||||
+ ", annotations=\n" + Utils.listToString(navMap.entrySet(), "\n") + "\n}";
|
||||
return "CodeMetadata{\nlines=" + lines
|
||||
+ "\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_OVERLOADED_CASTS = 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_STRING = "java.lang.String";
|
||||
|
||||
@@ -191,7 +191,6 @@ public class Jadx {
|
||||
passes.add(new ProcessInstructionsVisitor());
|
||||
|
||||
passes.add(new BlockSplitter());
|
||||
passes.add(new MethodVisitor(mth -> mth.add(AFlag.DISABLE_BLOCKS_LOCK)));
|
||||
if (args.isRawCFGOutput()) {
|
||||
passes.add(DotGraphVisitor.dumpRaw());
|
||||
}
|
||||
@@ -215,9 +214,6 @@ public class Jadx {
|
||||
passes.add(new CodeShrinkVisitor());
|
||||
passes.add(new SimplifyVisitor());
|
||||
passes.add(new MethodVisitor(mth -> mth.remove(AFlag.DONT_GENERATE)));
|
||||
if (args.isRawCFGOutput()) {
|
||||
passes.add(DotGraphVisitor.dumpRaw());
|
||||
}
|
||||
if (args.isCfgOutput()) {
|
||||
passes.add(DotGraphVisitor.dump());
|
||||
}
|
||||
@@ -238,9 +234,17 @@ public class Jadx {
|
||||
private static String version;
|
||||
|
||||
public static String getVersion() {
|
||||
if (version != null) {
|
||||
return version;
|
||||
if (version == null) {
|
||||
version = searchJadxVersion();
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
public static boolean isDevVersion() {
|
||||
return getVersion().equals(VERSION_DEV);
|
||||
}
|
||||
|
||||
private static String searchJadxVersion() {
|
||||
try {
|
||||
ClassLoader classLoader = Jadx.class.getClassLoader();
|
||||
if (classLoader != null) {
|
||||
@@ -250,7 +254,6 @@ public class Jadx {
|
||||
Manifest manifest = new Manifest(is);
|
||||
String ver = manifest.getMainAttributes().getValue("jadx-version");
|
||||
if (ver != null) {
|
||||
version = ver;
|
||||
return ver;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,6 +203,9 @@ public class ClspGraph {
|
||||
if (isNew) {
|
||||
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.ICodeWriter;
|
||||
import jadx.api.JadxArgs;
|
||||
import jadx.api.metadata.annotations.NodeEnd;
|
||||
import jadx.api.plugins.input.data.AccessFlags;
|
||||
import jadx.api.plugins.input.data.annotations.EncodedType;
|
||||
import jadx.api.plugins.input.data.annotations.EncodedValue;
|
||||
@@ -256,6 +257,7 @@ public class ClassGen {
|
||||
addInnerClsAndMethods(clsCode);
|
||||
clsCode.decIndent();
|
||||
clsCode.startLine('}');
|
||||
clsCode.attachAnnotation(NodeEnd.VALUE);
|
||||
}
|
||||
|
||||
private void addInnerClsAndMethods(ICodeWriter clsCode) {
|
||||
@@ -369,6 +371,7 @@ public class ClassGen {
|
||||
mthGen.addInstructions(code);
|
||||
code.decIndent();
|
||||
code.startLine('}');
|
||||
code.attachAnnotation(NodeEnd.VALUE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -614,21 +617,23 @@ public class ClassGen {
|
||||
if (useCls.equals(extClsInfo)) {
|
||||
return shortName;
|
||||
}
|
||||
if (extClsInfo.getPackage().equals("java.lang") && extClsInfo.getParentClass() == null) {
|
||||
return shortName;
|
||||
}
|
||||
if (isClassInnerFor(useCls, extClsInfo)) {
|
||||
return shortName;
|
||||
}
|
||||
if (extClsInfo.isInner()) {
|
||||
return expandInnerClassName(useCls, extClsInfo);
|
||||
}
|
||||
if (searchCollision(cls.root(), useCls, extClsInfo)) {
|
||||
if (checkInnerCollision(cls.root(), useCls, extClsInfo)
|
||||
|| checkInPackageCollision(cls.root(), useCls, extClsInfo)) {
|
||||
return fullName;
|
||||
}
|
||||
if (isBothClassesInOneTopClass(useCls, extClsInfo)) {
|
||||
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
|
||||
if (extClsInfo.getPackage().equals(useCls.getPackage()) && !extClsInfo.isInner()) {
|
||||
return shortName;
|
||||
@@ -709,7 +714,7 @@ public class ClassGen {
|
||||
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) {
|
||||
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) {
|
||||
|
||||
@@ -168,10 +168,11 @@ public class InsnGen {
|
||||
* Variable definition without type, only var name
|
||||
*/
|
||||
private void defVar(ICodeWriter code, CodeVar codeVar) {
|
||||
String varName = mgen.getNameGen().assignArg(codeVar);
|
||||
if (code.isMetadataSupported()) {
|
||||
code.attachDefinition(VarNode.get(mth, codeVar));
|
||||
}
|
||||
code.add(mgen.getNameGen().assignArg(codeVar));
|
||||
code.add(varName);
|
||||
}
|
||||
|
||||
private String lit(LiteralArg arg) {
|
||||
@@ -760,6 +761,7 @@ public class InsnGen {
|
||||
ctor.add(AFlag.DONT_GENERATE);
|
||||
}
|
||||
}
|
||||
code.attachDefinition(cls);
|
||||
code.add("new ");
|
||||
useClass(code, parent);
|
||||
MethodNode callMth = mth.root().resolveMethod(insn.getCallMth());
|
||||
@@ -806,14 +808,9 @@ public class InsnGen {
|
||||
break;
|
||||
|
||||
case SUPER:
|
||||
ClassInfo superCallCls = getClassForSuperCall(code, callMth);
|
||||
if (superCallCls != null) {
|
||||
useClass(code, superCallCls);
|
||||
code.add('.');
|
||||
}
|
||||
// use 'super' instead 'this' in 0 arg
|
||||
code.add("super").add('.');
|
||||
k++;
|
||||
callSuper(code, callMth);
|
||||
k++; // use 'super' instead 'this' in 0 arg
|
||||
code.add('.');
|
||||
break;
|
||||
|
||||
case STATIC:
|
||||
@@ -827,9 +824,15 @@ public class InsnGen {
|
||||
}
|
||||
if (callMthNode != null) {
|
||||
code.attachAnnotation(callMthNode);
|
||||
code.add(callMthNode.getAlias());
|
||||
}
|
||||
if (insn.contains(AFlag.FORCE_RAW_NAME)) {
|
||||
code.add(callMth.getName());
|
||||
} else {
|
||||
code.add(callMth.getAlias());
|
||||
if (callMthNode != null) {
|
||||
code.add(callMthNode.getAlias());
|
||||
} else {
|
||||
code.add(callMth.getAlias());
|
||||
}
|
||||
}
|
||||
generateMethodArguments(code, insn, k, callMthNode);
|
||||
}
|
||||
@@ -964,34 +967,43 @@ public class InsnGen {
|
||||
code.startLine('}');
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private ClassInfo getClassForSuperCall(ICodeWriter code, MethodInfo callMth) {
|
||||
ClassNode useCls = mth.getParentClass();
|
||||
ClassInfo insnCls = useCls.getClassInfo();
|
||||
ClassInfo declClass = callMth.getDeclClass();
|
||||
if (insnCls.equals(declClass)) {
|
||||
return null;
|
||||
private void callSuper(ICodeWriter code, MethodInfo callMth) {
|
||||
ClassInfo superCallCls = getClassForSuperCall(callMth);
|
||||
if (superCallCls == null) {
|
||||
// unknown class, add comment to keep that info
|
||||
code.add("super/*").add(callMth.getDeclClass().getFullName()).add("*/");
|
||||
return;
|
||||
}
|
||||
ClassNode topClass = useCls.getTopParentClass();
|
||||
if (topClass.getClassInfo().equals(declClass)) {
|
||||
return declClass;
|
||||
ClassInfo curClass = mth.getParentClass().getClassInfo();
|
||||
if (superCallCls.equals(curClass)) {
|
||||
code.add("super");
|
||||
return;
|
||||
}
|
||||
// search call class
|
||||
ClassNode nextParent = useCls;
|
||||
do {
|
||||
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);
|
||||
// use custom class
|
||||
useClass(code, superCallCls);
|
||||
code.add(".super");
|
||||
}
|
||||
|
||||
// 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,
|
||||
|
||||
@@ -26,6 +26,7 @@ import jadx.core.dex.attributes.AType;
|
||||
import jadx.core.dex.attributes.nodes.JadxError;
|
||||
import jadx.core.dex.attributes.nodes.JumpInfo;
|
||||
import jadx.core.dex.attributes.nodes.MethodOverrideAttr;
|
||||
import jadx.core.dex.attributes.nodes.MethodReplaceAttr;
|
||||
import jadx.core.dex.info.AccessInfo;
|
||||
import jadx.core.dex.instructions.ConstStringNode;
|
||||
import jadx.core.dex.instructions.IfNode;
|
||||
@@ -144,8 +145,9 @@ public class MethodGen {
|
||||
} else {
|
||||
classGen.useType(code, mth.getReturnType());
|
||||
code.add(' ');
|
||||
code.attachDefinition(mth);
|
||||
code.add(mth.getAlias());
|
||||
MethodNode defMth = getMethodForDefinition();
|
||||
code.attachDefinition(defMth);
|
||||
code.add(defMth.getAlias());
|
||||
}
|
||||
code.add('(');
|
||||
|
||||
@@ -178,6 +180,14 @@ public class MethodGen {
|
||||
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) {
|
||||
MethodOverrideAttr overrideAttr = mth.get(AType.METHOD_OVERRIDE);
|
||||
if (overrideAttr == null) {
|
||||
|
||||
@@ -53,7 +53,9 @@ public class SimpleModeHelper {
|
||||
startLabel.set(block.getId());
|
||||
} else if (predsCount == 1 && prev != null) {
|
||||
if (!prev.equals(preds.get(0))) {
|
||||
startLabel.set(block.getId());
|
||||
if (!block.contains(AFlag.EXC_BOTTOM_SPLITTER)) {
|
||||
startLabel.set(block.getId());
|
||||
}
|
||||
if (prev.getSuccessors().size() == 1 && !mth.isPreExitBlocks(prev)) {
|
||||
endGoto.set(prev.getId());
|
||||
}
|
||||
|
||||
@@ -428,7 +428,9 @@ public class Deobfuscator {
|
||||
return "Enum";
|
||||
}
|
||||
String result = "";
|
||||
if (cls.getAccessFlags().isAbstract()) {
|
||||
if (cls.getAccessFlags().isInterface()) {
|
||||
result += "Interface";
|
||||
} else if (cls.getAccessFlags().isAbstract()) {
|
||||
result += "Abstract";
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ public enum AFlag {
|
||||
HIDDEN, // instruction used inside other instruction but not listed in args
|
||||
|
||||
DONT_RENAME, // do not rename during deobfuscation
|
||||
FORCE_RAW_NAME, // force use of raw name instead alias
|
||||
|
||||
ADDED_TO_REGION,
|
||||
|
||||
EXC_TOP_SPLITTER,
|
||||
|
||||
@@ -91,6 +91,19 @@ public class LoopInfo {
|
||||
this.parentLoop = parentLoop;
|
||||
}
|
||||
|
||||
public boolean hasParent(LoopInfo searchLoop) {
|
||||
LoopInfo parent = parentLoop;
|
||||
while (true) {
|
||||
if (parent == null) {
|
||||
return false;
|
||||
}
|
||||
if (parent == searchLoop) {
|
||||
return true;
|
||||
}
|
||||
parent = parent.getParentLoop();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LOOP:" + id + ": " + start + "->" + end;
|
||||
|
||||
@@ -180,12 +180,12 @@ public final class ClassInfo implements Comparable<ClassInfo> {
|
||||
return makeFullClsName(pkg, name, parentClass, false, true);
|
||||
}
|
||||
|
||||
private String makeAliasFullName() {
|
||||
public String makeAliasFullName() {
|
||||
return makeFullClsName(getAliasPkg(), getAliasShortName(), parentClass, true, false);
|
||||
}
|
||||
|
||||
private String makeAliasRawFullName() {
|
||||
return makeFullClsName(pkg, name, parentClass, true, true);
|
||||
public String makeAliasRawFullName() {
|
||||
return makeFullClsName(getAliasPkg(), getAliasShortName(), parentClass, true, true);
|
||||
}
|
||||
|
||||
public String getAliasFullPath() {
|
||||
|
||||
@@ -19,29 +19,59 @@ import static jadx.core.utils.Utils.lockList;
|
||||
|
||||
public final class BlockNode extends AttrNode implements IBlock, Comparable<BlockNode> {
|
||||
|
||||
/**
|
||||
* Const ID
|
||||
*/
|
||||
private final int cid;
|
||||
|
||||
/**
|
||||
* ID linked to position in blocks list (easier to use BitSet)
|
||||
* TODO: rename to avoid confusion
|
||||
*/
|
||||
private int id;
|
||||
|
||||
/**
|
||||
* Offset in methods bytecode
|
||||
*/
|
||||
private final int startOffset;
|
||||
|
||||
private final List<InsnNode> instructions = new ArrayList<>(2);
|
||||
|
||||
private List<BlockNode> predecessors = new ArrayList<>(1);
|
||||
private List<BlockNode> successors = new ArrayList<>(1);
|
||||
private List<BlockNode> cleanSuccessors;
|
||||
|
||||
// all dominators
|
||||
/**
|
||||
* All dominators, excluding self
|
||||
*/
|
||||
private BitSet doms = EmptyBitSet.EMPTY;
|
||||
// dominance frontier
|
||||
|
||||
/**
|
||||
* Dominance frontier
|
||||
*/
|
||||
private BitSet domFrontier;
|
||||
// immediate dominator
|
||||
|
||||
/**
|
||||
* Immediate dominator
|
||||
*/
|
||||
private BlockNode idom;
|
||||
// blocks on which dominates this block
|
||||
|
||||
/**
|
||||
* Blocks on which dominates this block
|
||||
*/
|
||||
private List<BlockNode> dominatesOn = new ArrayList<>(3);
|
||||
|
||||
public BlockNode(int id, int offset) {
|
||||
public BlockNode(int cid, int id, int offset) {
|
||||
this.cid = cid;
|
||||
this.id = id;
|
||||
this.startOffset = offset;
|
||||
}
|
||||
|
||||
public void setId(int id) {
|
||||
public int getCId() {
|
||||
return cid;
|
||||
}
|
||||
|
||||
void setId(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@@ -170,6 +200,10 @@ public final class BlockNode extends AttrNode implements IBlock, Comparable<Bloc
|
||||
return contains(AFlag.RETURN);
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return instructions.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return startOffset;
|
||||
@@ -184,12 +218,12 @@ public final class BlockNode extends AttrNode implements IBlock, Comparable<Bloc
|
||||
return false;
|
||||
}
|
||||
BlockNode other = (BlockNode) obj;
|
||||
return id == other.id && startOffset == other.startOffset;
|
||||
return cid == other.cid && startOffset == other.startOffset;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NotNull BlockNode o) {
|
||||
return Integer.compare(id, o.id);
|
||||
return Integer.compare(cid, o.cid);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -199,6 +233,6 @@ public final class BlockNode extends AttrNode implements IBlock, Comparable<Bloc
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "B:" + id + ':' + InsnUtils.formatOffset(startOffset);
|
||||
return "B:" + cid + ':' + InsnUtils.formatOffset(startOffset);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import jadx.api.ICodeCache;
|
||||
import jadx.api.ICodeInfo;
|
||||
import jadx.api.ICodeWriter;
|
||||
import jadx.api.JadxArgs;
|
||||
import jadx.api.impl.SimpleCodeInfo;
|
||||
import jadx.api.plugins.input.data.IClassData;
|
||||
import jadx.api.plugins.input.data.IFieldData;
|
||||
import jadx.api.plugins.input.data.IMethodData;
|
||||
@@ -378,8 +379,16 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
|
||||
return code;
|
||||
}
|
||||
}
|
||||
ICodeInfo codeInfo = root.getProcessClasses().generateCode(this);
|
||||
codeCache.add(clsRawName, codeInfo);
|
||||
ICodeInfo 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;
|
||||
}
|
||||
|
||||
@@ -460,6 +469,9 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
|
||||
}
|
||||
|
||||
public void addField(FieldNode fld) {
|
||||
if (fields == null || fields.isEmpty()) {
|
||||
fields = new ArrayList<>(1);
|
||||
}
|
||||
fields.add(fld);
|
||||
}
|
||||
|
||||
@@ -648,6 +660,10 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
|
||||
return contains(AType.ANONYMOUS_CLASS);
|
||||
}
|
||||
|
||||
public boolean isSynthetic() {
|
||||
return contains(AFlag.SYNTHETIC);
|
||||
}
|
||||
|
||||
public boolean isInner() {
|
||||
return parentClass != this;
|
||||
}
|
||||
|
||||
@@ -57,6 +57,10 @@ public class FieldNode extends NotificationAttrNode implements ICodeNode {
|
||||
return accFlags.isStatic();
|
||||
}
|
||||
|
||||
public boolean isInstance() {
|
||||
return !accFlags.isStatic();
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return fieldInfo.getName();
|
||||
}
|
||||
@@ -65,6 +69,10 @@ public class FieldNode extends NotificationAttrNode implements ICodeNode {
|
||||
return fieldInfo.getAlias();
|
||||
}
|
||||
|
||||
public void rename(String alias) {
|
||||
fieldInfo.setAlias(alias);
|
||||
}
|
||||
|
||||
public ArgType getType() {
|
||||
return type;
|
||||
}
|
||||
@@ -73,6 +81,10 @@ public class FieldNode extends NotificationAttrNode implements ICodeNode {
|
||||
return parentClass;
|
||||
}
|
||||
|
||||
public ClassNode getTopParentClass() {
|
||||
return parentClass.getTopParentClass();
|
||||
}
|
||||
|
||||
public List<MethodNode> getUseIn() {
|
||||
return useIn;
|
||||
}
|
||||
|
||||
@@ -264,21 +264,6 @@ public class InsnNode extends LineAttrNode {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean canReorderRecursive() {
|
||||
if (!canReorder()) {
|
||||
return false;
|
||||
}
|
||||
for (InsnArg arg : this.getArguments()) {
|
||||
if (arg.isInsnWrap()) {
|
||||
InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn();
|
||||
if (!wrapInsn.canReorderRecursive()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean containsWrappedInsn() {
|
||||
for (InsnArg arg : this.getArguments()) {
|
||||
if (arg.isInsnWrap()) {
|
||||
|
||||
@@ -61,6 +61,7 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
|
||||
private List<RegisterArg> argsList;
|
||||
private InsnNode[] instructions;
|
||||
private List<BlockNode> blocks;
|
||||
private int blocksMaxCId;
|
||||
private BlockNode enterBlock;
|
||||
private BlockNode exitBlock;
|
||||
private List<SSAVar> sVars;
|
||||
@@ -316,6 +317,19 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
|
||||
return blocks;
|
||||
}
|
||||
|
||||
public void setBasicBlocks(List<BlockNode> blocks) {
|
||||
this.blocks = blocks;
|
||||
int i = 0;
|
||||
for (BlockNode block : blocks) {
|
||||
block.setId(i);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
public int getNextBlockCId() {
|
||||
return blocksMaxCId++;
|
||||
}
|
||||
|
||||
public BlockNode getEnterBlock() {
|
||||
return enterBlock;
|
||||
}
|
||||
@@ -461,6 +475,10 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
|
||||
return regsCount;
|
||||
}
|
||||
|
||||
public int getArgsStartReg() {
|
||||
return argsStartReg;
|
||||
}
|
||||
|
||||
public SSAVar makeNewSVar(@NotNull RegisterArg assignArg) {
|
||||
int regNum = assignArg.getRegNum();
|
||||
return makeNewSVar(regNum, getNextSVarVersion(regNum), assignArg);
|
||||
|
||||
@@ -42,7 +42,8 @@ import jadx.core.utils.StringUtils;
|
||||
import jadx.core.utils.Utils;
|
||||
import jadx.core.utils.android.AndroidResourcesUtils;
|
||||
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.entry.ResourceEntry;
|
||||
import jadx.core.xmlgen.entry.ValuesParser;
|
||||
@@ -115,21 +116,25 @@ public class RootNode {
|
||||
}
|
||||
|
||||
private void addDummyClass(IClassData classData, Exception exc) {
|
||||
String typeStr = classData.getType();
|
||||
String name = null;
|
||||
try {
|
||||
ClassInfo clsInfo = ClassInfo.fromName(this, typeStr);
|
||||
if (clsInfo != null) {
|
||||
name = clsInfo.getShortName();
|
||||
String typeStr = classData.getType();
|
||||
String name = null;
|
||||
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) {
|
||||
LOG.error("Failed to get name for class with type {}", typeStr, e);
|
||||
if (name == null || name.isEmpty()) {
|
||||
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) {
|
||||
@@ -159,23 +164,13 @@ public class RootNode {
|
||||
}
|
||||
|
||||
public void loadResources(List<ResourceFile> resources) {
|
||||
ResourceFile arsc = null;
|
||||
for (ResourceFile rf : resources) {
|
||||
if (rf.getType() == ResourceType.ARSC) {
|
||||
arsc = rf;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ResourceFile arsc = getResourceFile(resources);
|
||||
if (arsc == null) {
|
||||
LOG.debug("'.arsc' file not found");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ResTableParser parser = ResourcesLoader.decodeStream(arsc, (size, is) -> {
|
||||
ResTableParser tableParser = new ResTableParser(this);
|
||||
tableParser.decode(is);
|
||||
return tableParser;
|
||||
});
|
||||
IResParser parser = ResourcesLoader.decodeStream(arsc, (size, is) -> ResDecoder.decode(this, arsc, is));
|
||||
if (parser != null) {
|
||||
processResources(parser.getResStorage());
|
||||
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) {
|
||||
constValues.setResourcesNames(resStorage.getResourcesNames());
|
||||
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()) {
|
||||
return;
|
||||
}
|
||||
@@ -265,6 +269,7 @@ public class RootNode {
|
||||
public void runPreDecompileStage() {
|
||||
boolean debugEnabled = LOG.isDebugEnabled();
|
||||
for (IDexTreeVisitor pass : preDecompilePasses) {
|
||||
Utils.checkThreadInterrupt();
|
||||
long start = debugEnabled ? System.currentTimeMillis() : 0;
|
||||
try {
|
||||
pass.init(this);
|
||||
|
||||
@@ -49,6 +49,10 @@ public final class LoopRegion extends ConditionRegion {
|
||||
return header;
|
||||
}
|
||||
|
||||
public boolean isEndless() {
|
||||
return header == null;
|
||||
}
|
||||
|
||||
public IRegion getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
@@ -14,9 +14,9 @@ import jadx.core.dex.instructions.args.ArgType;
|
||||
import jadx.core.dex.instructions.args.InsnArg;
|
||||
import jadx.core.dex.nodes.BlockNode;
|
||||
import jadx.core.dex.nodes.IContainer;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
import jadx.core.utils.InsnUtils;
|
||||
import jadx.core.utils.Utils;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
|
||||
public class ExceptionHandler {
|
||||
|
||||
@@ -33,9 +33,14 @@ public class ExceptionHandler {
|
||||
|
||||
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;
|
||||
addCatchType(type);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,7 +48,7 @@ public class ExceptionHandler {
|
||||
*
|
||||
* @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 (catchTypes.contains(type)) {
|
||||
return false;
|
||||
@@ -51,14 +56,16 @@ public class ExceptionHandler {
|
||||
return catchTypes.add(type);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
public void addCatchTypes(Collection<ClassInfo> types) {
|
||||
public void addCatchTypes(MethodNode mth, Collection<ClassInfo> types) {
|
||||
for (ClassInfo type : types) {
|
||||
addCatchType(type);
|
||||
addCatchType(mth, type);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,10 @@ public class TryCatchBlockAttr implements IJadxAttribute {
|
||||
return throwFound;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public List<ExceptionHandler> getHandlers() {
|
||||
return handlers;
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ public class AttachTryCatchVisitor extends AbstractVisitor {
|
||||
ExcHandlerAttr excHandlerAttr = insn.get(AType.EXC_HANDLER);
|
||||
if (excHandlerAttr != null) {
|
||||
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
|
||||
return null;
|
||||
}
|
||||
@@ -143,7 +143,7 @@ public class AttachTryCatchVisitor extends AbstractVisitor {
|
||||
} else {
|
||||
insn = insertNOP(insnByOffset, handlerOffset);
|
||||
}
|
||||
ExceptionHandler handler = new ExceptionHandler(handlerOffset, type);
|
||||
ExceptionHandler handler = ExceptionHandler.build(mth, handlerOffset, type);
|
||||
mth.addExceptionHandler(handler);
|
||||
insn.addAttr(new ExcHandlerAttr(handler));
|
||||
return handler;
|
||||
|
||||
@@ -10,6 +10,7 @@ import java.util.Objects;
|
||||
import jadx.api.plugins.input.data.AccessFlags;
|
||||
import jadx.core.Consts;
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.attributes.AType;
|
||||
import jadx.core.dex.attributes.nodes.FieldReplaceAttr;
|
||||
import jadx.core.dex.attributes.nodes.MethodReplaceAttr;
|
||||
import jadx.core.dex.attributes.nodes.SkipMethodArgsAttr;
|
||||
@@ -225,7 +226,7 @@ public class ClassModifier extends AbstractVisitor {
|
||||
}
|
||||
|
||||
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());
|
||||
if (allInsns.size() == 1) {
|
||||
InsnNode wrappedInsn = allInsns.get(0);
|
||||
@@ -235,12 +236,10 @@ public class ClassModifier extends AbstractVisitor {
|
||||
wrappedInsn = ((InsnWrapArg) arg).getWrapInsn();
|
||||
}
|
||||
}
|
||||
if (checkSyntheticWrapper(mth, wrappedInsn)) {
|
||||
return true;
|
||||
}
|
||||
return checkSyntheticWrapper(mth, wrappedInsn);
|
||||
}
|
||||
}
|
||||
return !isMethodUnique(cls, mth);
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean checkSyntheticWrapper(MethodNode mth, InsnNode insn) {
|
||||
@@ -283,6 +282,9 @@ public class ClassModifier extends AbstractVisitor {
|
||||
if (!Objects.equals(wrappedMth.getAlias(), 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;
|
||||
}
|
||||
|
||||
@@ -299,20 +301,6 @@ public class ClassModifier extends AbstractVisitor {
|
||||
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)
|
||||
*/
|
||||
|
||||
@@ -100,7 +100,7 @@ public class DotGraphVisitor extends AbstractVisitor {
|
||||
if (insnArr == null) {
|
||||
return;
|
||||
}
|
||||
BlockNode block = new BlockNode(0, 0);
|
||||
BlockNode block = new BlockNode(0, 0, 0);
|
||||
List<InsnNode> insnList = block.getInstructions();
|
||||
for (InsnNode insn : insnArr) {
|
||||
if (insn != null) {
|
||||
@@ -199,7 +199,7 @@ public class DotGraphVisitor extends AbstractVisitor {
|
||||
dot.add("color=red,");
|
||||
}
|
||||
dot.add("label=\"{");
|
||||
dot.add(String.valueOf(block.getId())).add("\\:\\ ");
|
||||
dot.add(String.valueOf(block.getCId())).add("\\:\\ ");
|
||||
dot.add(InsnUtils.formatOffset(block.getStartOffset()));
|
||||
if (!attrs.isEmpty()) {
|
||||
dot.add('|').add(attrs);
|
||||
@@ -230,10 +230,10 @@ public class DotGraphVisitor extends AbstractVisitor {
|
||||
|
||||
if (PRINT_DOMINATORS) {
|
||||
for (BlockNode c : block.getDominatesOn()) {
|
||||
conn.startLine(block.getId() + " -> " + c.getId() + "[color=green];");
|
||||
conn.startLine(block.getCId() + " -> " + c.getCId() + "[color=green];");
|
||||
}
|
||||
for (BlockNode dom : BlockUtils.bitSetToBlocks(mth, block.getDomFrontier())) {
|
||||
conn.startLine("f_" + block.getId() + " -> f_" + dom.getId() + "[color=blue];");
|
||||
conn.startLine("f_" + block.getCId() + " -> f_" + dom.getCId() + "[color=blue];");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -273,7 +273,7 @@ public class DotGraphVisitor extends AbstractVisitor {
|
||||
private String makeName(IContainer c) {
|
||||
String name;
|
||||
if (c instanceof BlockNode) {
|
||||
name = "Node_" + ((BlockNode) c).getId();
|
||||
name = "Node_" + ((BlockNode) c).getCId();
|
||||
} else if (c instanceof IBlock) {
|
||||
name = "Node_" + c.getClass().getSimpleName() + '_' + c.hashCode();
|
||||
} else {
|
||||
|
||||
@@ -18,6 +18,7 @@ import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.attributes.AType;
|
||||
import jadx.core.dex.attributes.nodes.EnumClassAttr;
|
||||
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.info.AccessInfo;
|
||||
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.InsnType;
|
||||
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.InsnArg;
|
||||
import jadx.core.dex.instructions.args.InsnWrapArg;
|
||||
@@ -59,6 +61,7 @@ import static jadx.core.utils.InsnUtils.getWrappedInsn;
|
||||
public class EnumVisitor extends AbstractVisitor {
|
||||
|
||||
private MethodInfo enumValueOfMth;
|
||||
private MethodInfo cloneMth;
|
||||
|
||||
@Override
|
||||
public void init(RootNode root) {
|
||||
@@ -68,6 +71,12 @@ public class EnumVisitor extends AbstractVisitor {
|
||||
"valueOf",
|
||||
Arrays.asList(ArgType.CLASS, ArgType.STRING),
|
||||
ArgType.ENUM);
|
||||
|
||||
cloneMth = MethodInfo.fromDetails(root,
|
||||
ClassInfo.fromType(root, ArgType.OBJECT),
|
||||
"clone",
|
||||
Collections.emptyList(),
|
||||
ArgType.OBJECT);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -377,6 +386,7 @@ public class EnumVisitor extends AbstractVisitor {
|
||||
return enumFieldNode;
|
||||
}
|
||||
|
||||
@SuppressWarnings("StatementWithEmptyBody")
|
||||
private EnumField createEnumFieldByConstructor(ClassNode cls, FieldNode enumFieldNode, ConstructorInsn co) {
|
||||
// usually constructor signature is '<init>(Ljava/lang/String;I)V'.
|
||||
// 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) {
|
||||
String valuesMethod = "values()" + TypeGen.signature(ArgType.array(clsType));
|
||||
FieldInfo valuesFieldInfo = valuesField.getFieldInfo();
|
||||
|
||||
String valuesMethodShortId = "values()" + TypeGen.signature(ArgType.array(clsType));
|
||||
MethodNode valuesMethod = null;
|
||||
// remove compiler generated methods
|
||||
for (MethodNode mth : cls.getMethods()) {
|
||||
MethodInfo mi = mth.getMethodInfo();
|
||||
if (mi.isClassInit()) {
|
||||
if (mi.isClassInit() || mth.isNoCode()) {
|
||||
continue;
|
||||
}
|
||||
String shortId = mi.getShortId();
|
||||
@@ -432,12 +441,33 @@ public class EnumVisitor extends AbstractVisitor {
|
||||
mth.add(AFlag.DONT_GENERATE);
|
||||
}
|
||||
markArgsForSkip(mth);
|
||||
} else if (shortId.equals(valuesMethod)
|
||||
|| usesValuesField(mth, valuesFieldInfo)
|
||||
|| simpleValueOfMth(mth, clsType)) {
|
||||
} else if (mi.getShortId().equals(valuesMethodShortId)) {
|
||||
if (isValuesMethod(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);
|
||||
}
|
||||
}
|
||||
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) {
|
||||
@@ -458,6 +488,25 @@ public class EnumVisitor extends AbstractVisitor {
|
||||
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) {
|
||||
InsnNode returnInsn = InsnUtils.searchSingleReturnInsn(mth, insn -> insn.getArgsCount() == 1);
|
||||
if (returnInsn == null) {
|
||||
@@ -472,9 +521,41 @@ public class EnumVisitor extends AbstractVisitor {
|
||||
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);
|
||||
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) {
|
||||
|
||||
@@ -29,6 +29,7 @@ import jadx.core.dex.nodes.MethodNode;
|
||||
import jadx.core.dex.visitors.shrink.CodeShrinkVisitor;
|
||||
import jadx.core.utils.BlockUtils;
|
||||
import jadx.core.utils.InsnRemover;
|
||||
import jadx.core.utils.ListUtils;
|
||||
import jadx.core.utils.Utils;
|
||||
import jadx.core.utils.exceptions.JadxException;
|
||||
|
||||
@@ -45,20 +46,22 @@ public class ExtractFieldInit extends AbstractVisitor {
|
||||
for (ClassNode inner : cls.getInnerClasses()) {
|
||||
visit(inner);
|
||||
}
|
||||
moveStaticFieldsInit(cls);
|
||||
moveCommonFieldsInit(cls);
|
||||
if (!cls.getFields().isEmpty()) {
|
||||
moveStaticFieldsInit(cls);
|
||||
moveCommonFieldsInit(cls);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static final class FieldInitInfo {
|
||||
final FieldNode fieldNode;
|
||||
final IndexInsnNode putInsn;
|
||||
final boolean singlePath;
|
||||
final boolean canMove;
|
||||
|
||||
public FieldInitInfo(FieldNode fieldNode, IndexInsnNode putInsn, boolean singlePath) {
|
||||
public FieldInitInfo(FieldNode fieldNode, IndexInsnNode putInsn, boolean canMove) {
|
||||
this.fieldNode = fieldNode;
|
||||
this.putInsn = putInsn;
|
||||
this.singlePath = singlePath;
|
||||
this.canMove = canMove;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +83,9 @@ public class ExtractFieldInit extends AbstractVisitor {
|
||||
|| classInitMth.getBasicBlocks() == null) {
|
||||
return;
|
||||
}
|
||||
if (ListUtils.noneMatch(cls.getFields(), FieldNode::isStatic)) {
|
||||
return;
|
||||
}
|
||||
while (processStaticFields(cls, classInitMth)) {
|
||||
// sometimes instructions moved to field init prevent from vars inline -> inline and try again
|
||||
CodeShrinkVisitor.shrinkMethod(classInitMth);
|
||||
@@ -116,15 +122,15 @@ public class ExtractFieldInit extends AbstractVisitor {
|
||||
}
|
||||
|
||||
private static void moveCommonFieldsInit(ClassNode cls) {
|
||||
if (ListUtils.noneMatch(cls.getFields(), FieldNode::isInstance)) {
|
||||
return;
|
||||
}
|
||||
List<MethodNode> constructors = getConstructorsList(cls);
|
||||
if (constructors.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<ConstructorInitInfo> infoList = new ArrayList<>(constructors.size());
|
||||
for (MethodNode constructorMth : constructors) {
|
||||
if (constructorMth.isNoCode()) {
|
||||
return;
|
||||
}
|
||||
List<FieldInitInfo> inits = collectFieldsInit(cls, constructorMth, InsnType.IPUT);
|
||||
filterFieldsInit(inits);
|
||||
if (inits.isEmpty()) {
|
||||
@@ -168,19 +174,25 @@ public class ExtractFieldInit extends AbstractVisitor {
|
||||
Set<BlockNode> singlePathBlocks = new HashSet<>();
|
||||
BlockUtils.visitSinglePath(mth.getEnterBlock(), singlePathBlocks::add);
|
||||
|
||||
boolean canReorder = true;
|
||||
for (BlockNode block : mth.getBasicBlocks()) {
|
||||
for (InsnNode insn : block.getInstructions()) {
|
||||
boolean fieldInsn = false;
|
||||
if (insn.getType() == putType) {
|
||||
IndexInsnNode putInsn = (IndexInsnNode) insn;
|
||||
FieldInfo field = (FieldInfo) putInsn.getIndex();
|
||||
if (field.getDeclClass().equals(cls.getClassInfo())) {
|
||||
FieldNode fn = cls.searchField(field);
|
||||
if (fn != null) {
|
||||
boolean singlePath = singlePathBlocks.contains(block);
|
||||
fieldsInit.add(new FieldInitInfo(fn, putInsn, singlePath));
|
||||
boolean canMove = canReorder && singlePathBlocks.contains(block);
|
||||
fieldsInit.add(new FieldInitInfo(fn, putInsn, canMove));
|
||||
fieldInsn = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!fieldInsn && canReorder && !insn.canReorder()) {
|
||||
canReorder = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return fieldsInit;
|
||||
@@ -226,14 +238,14 @@ public class ExtractFieldInit extends AbstractVisitor {
|
||||
}
|
||||
|
||||
private static boolean checkInsn(FieldInitInfo initInfo) {
|
||||
if (!initInfo.singlePath) {
|
||||
if (!initInfo.canMove) {
|
||||
return false;
|
||||
}
|
||||
IndexInsnNode insn = initInfo.putInsn;
|
||||
InsnArg arg = insn.getArg(0);
|
||||
if (arg.isInsnWrap()) {
|
||||
InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn();
|
||||
if (!wrapInsn.canReorderRecursive() && insn.contains(AType.EXC_CATCH)) {
|
||||
if (!wrapInsn.canReorder() && insn.contains(AType.EXC_CATCH)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
@@ -364,7 +376,7 @@ public class ExtractFieldInit extends AbstractVisitor {
|
||||
AccessInfo accFlags = mth.getAccessFlags();
|
||||
if (!accFlags.isStatic() && accFlags.isConstructor()) {
|
||||
list.add(mth);
|
||||
if (BlockUtils.isAllBlocksEmpty(mth.getBasicBlocks())) {
|
||||
if (mth.isNoCode() || BlockUtils.isAllBlocksEmpty(mth.getBasicBlocks())) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,8 +362,7 @@ public class ModVisitor extends AbstractVisitor {
|
||||
private static void removeCheckCast(MethodNode mth, BlockNode block, int i, IndexInsnNode insn) {
|
||||
InsnArg castArg = insn.getArg(0);
|
||||
ArgType castType = (ArgType) insn.getIndex();
|
||||
if (!ArgType.isCastNeeded(mth.root(), castArg.getType(), castType)
|
||||
|| isCastDuplicate(insn)) {
|
||||
if (!ArgType.isCastNeeded(mth.root(), castArg.getType(), castType)) {
|
||||
RegisterArg result = insn.getResult();
|
||||
result.setType(castArg.getType());
|
||||
|
||||
@@ -371,10 +370,19 @@ public class ModVisitor extends AbstractVisitor {
|
||||
move.setResult(result);
|
||||
move.addArg(castArg);
|
||||
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);
|
||||
if (arg.isRegister()) {
|
||||
SSAVar sVar = ((RegisterArg) arg).getSVar();
|
||||
@@ -382,11 +390,13 @@ public class ModVisitor extends AbstractVisitor {
|
||||
InsnNode assignInsn = sVar.getAssign().getParentInsn();
|
||||
if (assignInsn != null && assignInsn.getType() == InsnType.CHECK_CAST) {
|
||||
ArgType assignCastType = (ArgType) ((IndexInsnNode) assignInsn).getIndex();
|
||||
return assignCastType.equals(castInsn.getIndex());
|
||||
if (assignCastType.equals(castInsn.getIndex())) {
|
||||
return assignInsn;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -222,6 +222,10 @@ public class ProcessAnonymous extends AbstractVisitor {
|
||||
// exclude self usage
|
||||
return null;
|
||||
}
|
||||
if (ctrUseCls.getTopParentClass().equals(cls)) {
|
||||
// exclude usage inside inner classes
|
||||
return null;
|
||||
}
|
||||
for (MethodNode mth : cls.getMethods()) {
|
||||
if (mth == ctr) {
|
||||
continue;
|
||||
|
||||
@@ -50,7 +50,7 @@ public class BlockExceptionHandler {
|
||||
return false;
|
||||
}
|
||||
BlockProcessor.updateCleanSuccessors(mth);
|
||||
BlockProcessor.computeDominanceFrontier(mth);
|
||||
DominatorTree.computeDominanceFrontier(mth);
|
||||
|
||||
processCatchAttr(mth);
|
||||
initExcHandlers(mth);
|
||||
@@ -171,10 +171,6 @@ public class BlockExceptionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
protected static void removeTmpConnections(MethodNode mth) {
|
||||
mth.getBasicBlocks().forEach(BlockExceptionHandler::removeTmpConnection);
|
||||
}
|
||||
|
||||
private static void removeTmpConnection(BlockNode block) {
|
||||
TmpEdgeAttr tmpEdgeAttr = block.get(AType.TMP_EDGE);
|
||||
if (tmpEdgeAttr != null) {
|
||||
@@ -402,6 +398,13 @@ public class BlockExceptionHandler {
|
||||
}
|
||||
BlockNode topDom = BlockUtils.getCommonDominator(mth, blocks);
|
||||
if (topDom != null) {
|
||||
// dominator always return one up block if blocks already contains dominator, use successor instead
|
||||
if (topDom.getSuccessors().size() == 1) {
|
||||
BlockNode upBlock = topDom.getSuccessors().get(0);
|
||||
if (blocks.contains(upBlock)) {
|
||||
return upBlock;
|
||||
}
|
||||
}
|
||||
return adjustTopBlock(topDom);
|
||||
}
|
||||
throw new JadxRuntimeException("Failed to find top block for try-catch from: " + blocks);
|
||||
@@ -549,7 +552,7 @@ public class BlockExceptionHandler {
|
||||
if (handler == resultHandler) {
|
||||
return false;
|
||||
}
|
||||
resultHandler.addCatchTypes(handler.getCatchTypes());
|
||||
resultHandler.addCatchTypes(mth, handler.getCatchTypes());
|
||||
handler.markForRemove();
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package jadx.core.dex.visitors.blocks;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.BitSet;
|
||||
import java.util.Collections;
|
||||
import java.util.Deque;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
@@ -31,7 +28,6 @@ import jadx.core.utils.Utils;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
|
||||
import static jadx.core.dex.visitors.blocks.BlockSplitter.connect;
|
||||
import static jadx.core.utils.EmptyBitSet.EMPTY;
|
||||
|
||||
public class BlockProcessor extends AbstractVisitor {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(BlockProcessor.class);
|
||||
@@ -50,29 +46,23 @@ public class BlockProcessor extends AbstractVisitor {
|
||||
computeDominators(mth);
|
||||
if (independentBlockTreeMod(mth)) {
|
||||
checkForUnreachableBlocks(mth);
|
||||
clearBlocksState(mth);
|
||||
computeDominators(mth);
|
||||
}
|
||||
if (FixMultiEntryLoops.process(mth)) {
|
||||
clearBlocksState(mth);
|
||||
computeDominators(mth);
|
||||
}
|
||||
updateCleanSuccessors(mth);
|
||||
|
||||
int i = 0;
|
||||
while (modifyBlocksTree(mth)) {
|
||||
// revert calculations
|
||||
clearBlocksState(mth);
|
||||
// recalculate dominators tree
|
||||
computeDominators(mth);
|
||||
|
||||
if (i++ > 100) {
|
||||
throw new JadxRuntimeException("CFG modification limit reached, blocks count: " + mth.getBasicBlocks().size());
|
||||
}
|
||||
}
|
||||
checkForUnreachableBlocks(mth);
|
||||
|
||||
computeDominanceFrontier(mth);
|
||||
DominatorTree.computeDominanceFrontier(mth);
|
||||
registerLoops(mth);
|
||||
processNestedLoops(mth);
|
||||
|
||||
@@ -209,139 +199,9 @@ public class BlockProcessor extends AbstractVisitor {
|
||||
}
|
||||
|
||||
private static void computeDominators(MethodNode mth) {
|
||||
List<BlockNode> basicBlocks = mth.getBasicBlocks();
|
||||
int nBlocks = basicBlocks.size();
|
||||
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);
|
||||
clearBlocksState(mth);
|
||||
DominatorTree.compute(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) {
|
||||
@@ -349,7 +209,7 @@ public class BlockProcessor extends AbstractVisitor {
|
||||
// Every successor that dominates its predecessor is a header of a loop,
|
||||
// block -> successor is a back edge.
|
||||
block.getSuccessors().forEach(successor -> {
|
||||
if (block.getDoms().get(successor.getId())) {
|
||||
if (block.getDoms().get(successor.getId()) || block == successor) {
|
||||
successor.add(AFlag.LOOP_START);
|
||||
block.add(AFlag.LOOP_END);
|
||||
|
||||
|
||||
@@ -137,8 +137,9 @@ public class BlockSplitter extends AbstractVisitor {
|
||||
}
|
||||
|
||||
static BlockNode startNewBlock(MethodNode mth, int offset) {
|
||||
BlockNode block = new BlockNode(mth.getBasicBlocks().size(), offset);
|
||||
mth.getBasicBlocks().add(block);
|
||||
List<BlockNode> blocks = mth.getBasicBlocks();
|
||||
BlockNode block = new BlockNode(mth.getNextBlockCId(), blocks.size(), offset);
|
||||
blocks.add(block);
|
||||
return block;
|
||||
}
|
||||
|
||||
@@ -391,7 +392,8 @@ public class BlockSplitter extends AbstractVisitor {
|
||||
&& block.getSuccessors().size() <= 1
|
||||
&& !block.getPredecessors().isEmpty()
|
||||
&& !block.contains(AFlag.MTH_ENTER_BLOCK)
|
||||
&& !block.contains(AFlag.MTH_EXIT_BLOCK);
|
||||
&& !block.contains(AFlag.MTH_EXIT_BLOCK)
|
||||
&& !block.getSuccessors().contains(block); // no self loop
|
||||
}
|
||||
|
||||
static void collectSuccessors(BlockNode startBlock, BlockNode methodEnterBlock, Set<BlockNode> toRemove) {
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
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);
|
||||
}
|
||||
|
||||
private static List<BlockNode> sortBlocks(MethodNode mth) {
|
||||
int blocksCount = mth.getBasicBlocks().size();
|
||||
List<BlockNode> sorted = new ArrayList<>(blocksCount);
|
||||
BlockUtils.dfsVisit(mth, sorted::add);
|
||||
if (sorted.size() != blocksCount) {
|
||||
throw new JadxRuntimeException("Found unreachable blocks");
|
||||
}
|
||||
mth.setBasicBlocks(sorted);
|
||||
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,13 @@ import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import jadx.core.dex.nodes.BlockNode;
|
||||
import jadx.core.dex.nodes.InsnNode;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
import jadx.core.dex.trycatch.ExceptionHandler;
|
||||
import jadx.core.utils.Utils;
|
||||
|
||||
public class FinallyExtractInfo {
|
||||
private final MethodNode mth;
|
||||
private final ExceptionHandler finallyHandler;
|
||||
private final List<BlockNode> allHandlerBlocks;
|
||||
private final List<InsnsSlice> duplicateSlices = new ArrayList<>();
|
||||
@@ -16,12 +20,21 @@ public class FinallyExtractInfo {
|
||||
private final InsnsSlice finallyInsnsSlice = new InsnsSlice();
|
||||
private final BlockNode startBlock;
|
||||
|
||||
public FinallyExtractInfo(ExceptionHandler finallyHandler, BlockNode startBlock, List<BlockNode> allHandlerBlocks) {
|
||||
private InsnsSlice curDupSlice;
|
||||
private List<InsnNode> curDupInsns;
|
||||
private int curDupInsnsOffset;
|
||||
|
||||
public FinallyExtractInfo(MethodNode mth, ExceptionHandler finallyHandler, BlockNode startBlock, List<BlockNode> allHandlerBlocks) {
|
||||
this.mth = mth;
|
||||
this.finallyHandler = finallyHandler;
|
||||
this.startBlock = startBlock;
|
||||
this.allHandlerBlocks = allHandlerBlocks;
|
||||
}
|
||||
|
||||
public MethodNode getMth() {
|
||||
return mth;
|
||||
}
|
||||
|
||||
public ExceptionHandler getFinallyHandler() {
|
||||
return finallyHandler;
|
||||
}
|
||||
@@ -45,4 +58,33 @@ public class FinallyExtractInfo {
|
||||
public BlockNode getStartBlock() {
|
||||
return startBlock;
|
||||
}
|
||||
|
||||
public InsnsSlice getCurDupSlice() {
|
||||
return curDupSlice;
|
||||
}
|
||||
|
||||
public void setCurDupSlice(InsnsSlice curDupSlice) {
|
||||
this.curDupSlice = curDupSlice;
|
||||
}
|
||||
|
||||
public List<InsnNode> getCurDupInsns() {
|
||||
return curDupInsns;
|
||||
}
|
||||
|
||||
public int getCurDupInsnsOffset() {
|
||||
return curDupInsnsOffset;
|
||||
}
|
||||
|
||||
public void setCurDupInsns(List<InsnNode> insns, int offset) {
|
||||
this.curDupInsns = insns;
|
||||
this.curDupInsnsOffset = offset;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "FinallyExtractInfo{"
|
||||
+ "\n finally:\n " + finallyInsnsSlice
|
||||
+ "\n dups:\n " + Utils.listToString(duplicateSlices, "\n ")
|
||||
+ "\n}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,10 @@ import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.core.Consts;
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.attributes.AType;
|
||||
import jadx.core.dex.attributes.nodes.RegDebugInfoAttr;
|
||||
import jadx.core.dex.instructions.InsnType;
|
||||
import jadx.core.dex.instructions.args.InsnArg;
|
||||
import jadx.core.dex.instructions.args.RegisterArg;
|
||||
@@ -27,6 +29,7 @@ import jadx.core.dex.visitors.IDexTreeVisitor;
|
||||
import jadx.core.dex.visitors.JadxVisitor;
|
||||
import jadx.core.dex.visitors.ssa.SSATransform;
|
||||
import jadx.core.utils.BlockUtils;
|
||||
import jadx.core.utils.InsnList;
|
||||
import jadx.core.utils.ListUtils;
|
||||
import jadx.core.utils.Utils;
|
||||
|
||||
@@ -38,6 +41,7 @@ import jadx.core.utils.Utils;
|
||||
)
|
||||
public class MarkFinallyVisitor extends AbstractVisitor {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MarkFinallyVisitor.class);
|
||||
// private static final Logger LOG = LoggerFactory.getLogger(MarkFinallyVisitor.class);
|
||||
|
||||
@Override
|
||||
public void visit(MethodNode mth) {
|
||||
@@ -60,7 +64,7 @@ public class MarkFinallyVisitor extends AbstractVisitor {
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Undo finally extract visitor, mth: {}", mth, e);
|
||||
mth.addWarnComment("Undo finally extract visitor", e);
|
||||
undoFinallyVisitor(mth);
|
||||
}
|
||||
}
|
||||
@@ -100,20 +104,23 @@ public class MarkFinallyVisitor extends AbstractVisitor {
|
||||
List<BlockNode> handlerBlocks =
|
||||
new ArrayList<>(BlockUtils.collectBlocksDominatedByWithExcHandlers(mth, handlerBlock, handlerBlock));
|
||||
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)) {
|
||||
// remove empty catch
|
||||
allHandler.getTryBlock().removeHandler(allHandler);
|
||||
return true;
|
||||
}
|
||||
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();
|
||||
List<ExceptionHandler> handlers;
|
||||
if (hasInnerBlocks) {
|
||||
// collect handlers from this and all inner blocks (intentionally not using recursive collect for
|
||||
// now)
|
||||
// collect handlers from this and all inner blocks
|
||||
// (intentionally not using recursive collect for now)
|
||||
handlers = new ArrayList<>(tryBlock.getHandlers());
|
||||
for (TryCatchBlockAttr innerTryBlock : tryBlock.getInnerTryBlocks()) {
|
||||
handlers.addAll(innerTryBlock.getHandlers());
|
||||
@@ -137,10 +144,12 @@ public class MarkFinallyVisitor extends AbstractVisitor {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Consts.DEBUG_FINALLY) {
|
||||
LOG.debug("Handlers slices:\n{}", extractInfo);
|
||||
}
|
||||
boolean mergeInnerTryBlocks;
|
||||
int duplicatesCount = extractInfo.getDuplicateSlices().size();
|
||||
boolean fullTryBlock = duplicatesCount == (handlers.size() - 1);
|
||||
if (fullTryBlock) {
|
||||
if (duplicatesCount == (handlers.size() - 1)) {
|
||||
// all collected handlers have duplicate block
|
||||
mergeInnerTryBlocks = hasInnerBlocks;
|
||||
} else {
|
||||
@@ -170,15 +179,24 @@ public class MarkFinallyVisitor extends AbstractVisitor {
|
||||
if (upPath.size() < handlerBlocks.size()) {
|
||||
continue;
|
||||
}
|
||||
if (Consts.DEBUG_FINALLY) {
|
||||
LOG.debug("Checking dup path starts: {} from {}", upPath, pred);
|
||||
}
|
||||
for (BlockNode block : upPath) {
|
||||
if (searchDuplicateInsns(block, extractInfo)) {
|
||||
found = true;
|
||||
if (Consts.DEBUG_FINALLY) {
|
||||
LOG.debug("Found dup in: {} from {}", block, pred);
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
extractInfo.getFinallyInsnsSlice().resetIncomplete();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Consts.DEBUG_FINALLY) {
|
||||
LOG.debug("Result slices:\n{}", extractInfo);
|
||||
}
|
||||
if (!found) {
|
||||
return false;
|
||||
}
|
||||
@@ -204,6 +222,28 @@ public class MarkFinallyVisitor extends AbstractVisitor {
|
||||
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) {
|
||||
Stream<BlockNode> preds = bottom.getPredecessors().stream().filter(b -> b != bottomFinallyBlock);
|
||||
if (bottom == mth.getExitBlock()) {
|
||||
@@ -219,9 +259,8 @@ public class MarkFinallyVisitor extends AbstractVisitor {
|
||||
for (InsnsSlice dupSlice : extractInfo.getDuplicateSlices()) {
|
||||
List<InsnNode> dupInsnsList = dupSlice.getInsnsList();
|
||||
if (dupInsnsList.size() != finallyInsnsList.size()) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Incorrect finally slice size: {}, expected: {}", dupSlice, finallySlice);
|
||||
}
|
||||
extractInfo.getMth().addDebugComment(
|
||||
"Incorrect finally slice size: " + dupSlice + ", expected: " + finallySlice);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -231,9 +270,8 @@ public class MarkFinallyVisitor extends AbstractVisitor {
|
||||
List<InsnNode> insnsList = dupSlice.getInsnsList();
|
||||
InsnNode dupInsn = insnsList.get(i);
|
||||
if (finallyInsn.getType() != dupInsn.getType()) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Incorrect finally slice insn: {}, expected: {}", dupInsn, finallyInsn);
|
||||
}
|
||||
extractInfo.getMth().addDebugComment(
|
||||
"Incorrect finally slice insn: " + dupInsn + ", expected: " + finallyInsn);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -340,26 +378,32 @@ public class MarkFinallyVisitor extends AbstractVisitor {
|
||||
* 'Finally' instructions can start in the middle of the first block.
|
||||
*/
|
||||
private static InsnsSlice isStartBlock(BlockNode dupBlock, BlockNode finallyBlock, FinallyExtractInfo extractInfo) {
|
||||
extractInfo.setCurDupSlice(null);
|
||||
List<InsnNode> dupInsns = dupBlock.getInstructions();
|
||||
List<InsnNode> finallyInsns = finallyBlock.getInstructions();
|
||||
if (dupInsns.size() < finallyInsns.size()) {
|
||||
int dupSize = dupInsns.size();
|
||||
int finSize = finallyInsns.size();
|
||||
if (dupSize < finSize) {
|
||||
return null;
|
||||
}
|
||||
int startPos = dupInsns.size() - finallyInsns.size();
|
||||
int startPos;
|
||||
int endPos = 0;
|
||||
// fast check from end of block
|
||||
if (!checkInsns(dupInsns, finallyInsns, startPos)) {
|
||||
// check from block start
|
||||
if (checkInsns(dupInsns, finallyInsns, 0)) {
|
||||
startPos = 0;
|
||||
endPos = finallyInsns.size();
|
||||
} else {
|
||||
if (dupSize == finSize) {
|
||||
if (!checkInsns(extractInfo, dupInsns, finallyInsns, 0)) {
|
||||
return null;
|
||||
}
|
||||
startPos = 0;
|
||||
} else {
|
||||
// dupSize > finSize
|
||||
startPos = dupSize - finSize;
|
||||
// fast check from end of block
|
||||
if (!checkInsns(extractInfo, dupInsns, finallyInsns, startPos)) {
|
||||
// search start insn
|
||||
boolean found = false;
|
||||
for (int i = 1; i < startPos; i++) {
|
||||
if (checkInsns(dupInsns, finallyInsns, i)) {
|
||||
if (checkInsns(extractInfo, dupInsns, finallyInsns, i)) {
|
||||
startPos = i;
|
||||
endPos = finallyInsns.size() + i;
|
||||
endPos = finSize + i;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
@@ -373,13 +417,14 @@ public class MarkFinallyVisitor extends AbstractVisitor {
|
||||
// put instructions into slices
|
||||
boolean complete;
|
||||
InsnsSlice slice = new InsnsSlice();
|
||||
extractInfo.setCurDupSlice(slice);
|
||||
int endIndex;
|
||||
if (endPos != 0) {
|
||||
endIndex = endPos + 1;
|
||||
// both slices completed
|
||||
complete = true;
|
||||
} else {
|
||||
endIndex = dupInsns.size();
|
||||
endIndex = dupSize;
|
||||
complete = false;
|
||||
}
|
||||
|
||||
@@ -393,9 +438,8 @@ public class MarkFinallyVisitor extends AbstractVisitor {
|
||||
if (finallySlice.isComplete()) {
|
||||
// compare slices
|
||||
if (finallySlice.getInsnsList().size() != slice.getInsnsList().size()) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Another duplicated slice has different insns count: {}, finally: {}", slice, finallySlice);
|
||||
}
|
||||
extractInfo.getMth().addDebugComment(
|
||||
"Another duplicated slice has different insns count: " + slice + ", finally: " + finallySlice);
|
||||
return null;
|
||||
}
|
||||
// TODO: add additional slices checks
|
||||
@@ -413,11 +457,12 @@ public class MarkFinallyVisitor extends AbstractVisitor {
|
||||
return slice;
|
||||
}
|
||||
|
||||
private static boolean checkInsns(List<InsnNode> remInsns, List<InsnNode> finallyInsns, int delta) {
|
||||
private static boolean checkInsns(FinallyExtractInfo extractInfo, List<InsnNode> dupInsns, List<InsnNode> finallyInsns, int delta) {
|
||||
extractInfo.setCurDupInsns(dupInsns, delta);
|
||||
for (int i = finallyInsns.size() - 1; i >= 0; i--) {
|
||||
InsnNode startInsn = finallyInsns.get(i);
|
||||
InsnNode remInsn = remInsns.get(delta + i);
|
||||
if (!sameInsns(remInsn, startInsn)) {
|
||||
InsnNode dupInsn = dupInsns.get(delta + i);
|
||||
if (!sameInsns(extractInfo, dupInsn, startInsn)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -469,8 +514,9 @@ public class MarkFinallyVisitor extends AbstractVisitor {
|
||||
if (dupInsnCount < finallyInsnCount) {
|
||||
return false;
|
||||
}
|
||||
extractInfo.setCurDupInsns(dupInsns, 0);
|
||||
for (int i = 0; i < finallyInsnCount; i++) {
|
||||
if (!sameInsns(dupInsns.get(i), finallyInsns.get(i))) {
|
||||
if (!sameInsns(extractInfo, dupInsns.get(i), finallyInsns.get(i))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -484,26 +530,85 @@ public class MarkFinallyVisitor extends AbstractVisitor {
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean sameInsns(InsnNode remInsn, InsnNode fInsn) {
|
||||
if (!remInsn.isSame(fInsn)) {
|
||||
private static boolean sameInsns(FinallyExtractInfo extractInfo, InsnNode dupInsn, InsnNode fInsn) {
|
||||
if (!dupInsn.isSame(fInsn)) {
|
||||
return false;
|
||||
}
|
||||
// TODO: check instance arg in ConstructorInsn
|
||||
// TODO: compare literals
|
||||
for (int i = 0; i < remInsn.getArgsCount(); i++) {
|
||||
InsnArg remArg = remInsn.getArg(i);
|
||||
for (int i = 0; i < dupInsn.getArgsCount(); i++) {
|
||||
InsnArg dupArg = dupInsn.getArg(i);
|
||||
InsnArg fArg = fInsn.getArg(i);
|
||||
if (remArg.isRegister() != fArg.isRegister()) {
|
||||
if (!isSameArgs(extractInfo, dupArg, fArg)) {
|
||||
return false;
|
||||
}
|
||||
boolean remConst = remArg.isConst();
|
||||
if (remConst != fArg.isConst()) {
|
||||
return false;
|
||||
}
|
||||
if (remConst && !remArg.isSameConst(fArg)) {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@SuppressWarnings("RedundantIfStatement")
|
||||
private static boolean isSameArgs(FinallyExtractInfo extractInfo, InsnArg dupArg, InsnArg fArg) {
|
||||
boolean isReg = dupArg.isRegister();
|
||||
if (isReg != fArg.isRegister()) {
|
||||
return false;
|
||||
}
|
||||
if (isReg) {
|
||||
RegisterArg dupReg = (RegisterArg) dupArg;
|
||||
RegisterArg fReg = (RegisterArg) fArg;
|
||||
if (!dupReg.sameCodeVar(fReg)
|
||||
&& !sameDebugInfo(dupReg, fReg)
|
||||
&& assignedOutsideHandler(extractInfo, dupReg, fReg)
|
||||
&& assignInsnDifferent(dupReg, fReg)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
boolean remConst = dupArg.isConst();
|
||||
if (remConst != fArg.isConst()) {
|
||||
return false;
|
||||
}
|
||||
if (remConst && !dupArg.isSameConst(fArg)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean sameDebugInfo(RegisterArg dupReg, RegisterArg fReg) {
|
||||
RegDebugInfoAttr fDbgInfo = fReg.get(AType.REG_DEBUG_INFO);
|
||||
RegDebugInfoAttr dupDbgInfo = dupReg.get(AType.REG_DEBUG_INFO);
|
||||
if (fDbgInfo == null || dupDbgInfo == null) {
|
||||
return false;
|
||||
}
|
||||
return dupDbgInfo.equals(fDbgInfo);
|
||||
}
|
||||
|
||||
private static boolean assignInsnDifferent(RegisterArg dupReg, RegisterArg fReg) {
|
||||
InsnNode assignInsn = fReg.getAssignInsn();
|
||||
InsnNode dupAssign = dupReg.getAssignInsn();
|
||||
if (assignInsn == null || dupAssign == null) {
|
||||
return true;
|
||||
}
|
||||
if (!assignInsn.isSame(dupAssign)) {
|
||||
return true;
|
||||
}
|
||||
if (assignInsn.isConstInsn() && dupAssign.isConstInsn()) {
|
||||
return !assignInsn.isDeepEquals(dupAssign);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressWarnings("RedundantIfStatement")
|
||||
private static boolean assignedOutsideHandler(FinallyExtractInfo extractInfo, RegisterArg dupReg, RegisterArg fReg) {
|
||||
if (InsnList.contains(extractInfo.getFinallyInsnsSlice().getInsnsList(), fReg.getAssignInsn())) {
|
||||
return false;
|
||||
}
|
||||
InsnNode dupAssign = dupReg.getAssignInsn();
|
||||
InsnsSlice curDupSlice = extractInfo.getCurDupSlice();
|
||||
if (curDupSlice != null && InsnList.contains(curDupSlice.getInsnsList(), dupAssign)) {
|
||||
return false;
|
||||
}
|
||||
List<InsnNode> curDupInsns = extractInfo.getCurDupInsns();
|
||||
if (Utils.notEmpty(curDupInsns) && InsnList.contains(curDupInsns, dupAssign, extractInfo.getCurDupInsnsOffset())) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -522,7 +627,7 @@ public class MarkFinallyVisitor extends AbstractVisitor {
|
||||
DepthTraversal.visit(visitor, mth);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Undo finally extract failed, mth: {}", mth, e);
|
||||
mth.addError("Undo finally extract failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import jadx.core.dex.nodes.IContainer;
|
||||
import jadx.core.dex.nodes.IRegion;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
import jadx.core.dex.regions.Region;
|
||||
import jadx.core.dex.regions.loops.LoopRegion;
|
||||
import jadx.core.dex.visitors.AbstractVisitor;
|
||||
|
||||
public class CleanRegions extends AbstractVisitor {
|
||||
@@ -42,6 +43,13 @@ public class CleanRegions extends AbstractVisitor {
|
||||
BlockNode block = (BlockNode) container;
|
||||
return block.getInstructions().isEmpty();
|
||||
}
|
||||
if (container instanceof LoopRegion) {
|
||||
LoopRegion loopRegion = (LoopRegion) container;
|
||||
if (loopRegion.isEndless()) {
|
||||
// keep empty endless loops
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (container instanceof IRegion) {
|
||||
List<IContainer> subBlocks = ((IRegion) container).getSubBlocks();
|
||||
for (IContainer subBlock : subBlocks) {
|
||||
|
||||
@@ -464,7 +464,7 @@ public class RegionMaker {
|
||||
BlockNode exitEnd = BlockUtils.followEmptyPath(exit);
|
||||
List<LoopInfo> loops = exitEnd.getAll(AType.LOOP);
|
||||
for (LoopInfo loopAtEnd : loops) {
|
||||
if (loopAtEnd != loop) {
|
||||
if (loopAtEnd != loop && loop.hasParent(loopAtEnd)) {
|
||||
insertEdge = exitEdge;
|
||||
confirm = true;
|
||||
break;
|
||||
|
||||
@@ -14,9 +14,7 @@ import jadx.core.Consts;
|
||||
import jadx.core.codegen.json.JsonMappingGen;
|
||||
import jadx.core.deobf.Deobfuscator;
|
||||
import jadx.core.deobf.NameMapper;
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.attributes.nodes.RenameReasonAttr;
|
||||
import jadx.core.dex.info.AccessInfo;
|
||||
import jadx.core.dex.info.ClassInfo;
|
||||
import jadx.core.dex.info.FieldInfo;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
@@ -45,8 +43,8 @@ public class RenameVisitor extends AbstractVisitor {
|
||||
deobfuscator.execute();
|
||||
}
|
||||
|
||||
checkClasses(deobfuscator, root, args);
|
||||
UserRenames.applyForNodes(root);
|
||||
checkClasses(deobfuscator, root, args);
|
||||
|
||||
if (args.isDeobfuscationOn() || !args.isJsonOutput()) {
|
||||
deobfuscator.savePresets();
|
||||
@@ -196,11 +194,6 @@ public class RenameVisitor extends AbstractVisitor {
|
||||
if (args.isRenameValid()) {
|
||||
Set<String> names = new HashSet<>(methods.size());
|
||||
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);
|
||||
if (!names.add(signature)) {
|
||||
deobfuscator.forceRenameMethod(mth);
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package jadx.core.utils;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.core.deobf.NameMapper;
|
||||
import jadx.core.deobf.TldHelper;
|
||||
|
||||
public class BetterName {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(BetterName.class);
|
||||
|
||||
private static final boolean DEBUG = true;
|
||||
|
||||
public static String compareAndGet(String first, String second) {
|
||||
if (Objects.equals(first, second)) {
|
||||
return first;
|
||||
}
|
||||
int firstRating = calcRating(first);
|
||||
int secondRating = calcRating(second);
|
||||
boolean firstBetter = firstRating >= secondRating;
|
||||
if (DEBUG) {
|
||||
if (firstBetter) {
|
||||
LOG.info("Better name: '{}' > '{}' ({} > {})", first, second, firstRating, secondRating);
|
||||
} else {
|
||||
LOG.info("Better name: '{}' > '{}' ({} > {})", second, first, secondRating, firstRating);
|
||||
}
|
||||
}
|
||||
return firstBetter ? first : second;
|
||||
}
|
||||
|
||||
public static int calcRating(String str) {
|
||||
int rating = str.length() * 3;
|
||||
rating += differentCharsCount(str) * 20;
|
||||
|
||||
if (NameMapper.isAllCharsPrintable(str)) {
|
||||
rating += 100;
|
||||
}
|
||||
if (NameMapper.isValidIdentifier(str)) {
|
||||
rating += 50;
|
||||
}
|
||||
if (TldHelper.contains(str)) {
|
||||
rating += 20;
|
||||
}
|
||||
if (str.contains("_")) {
|
||||
// rare in obfuscated names
|
||||
rating += 100;
|
||||
}
|
||||
return rating;
|
||||
}
|
||||
|
||||
private static int differentCharsCount(String str) {
|
||||
String lower = str.toLowerCase(Locale.ROOT);
|
||||
Set<Integer> chars = new HashSet<>();
|
||||
StringUtils.visitCodePoints(lower, chars::add);
|
||||
return chars.size();
|
||||
}
|
||||
}
|
||||
@@ -697,7 +697,7 @@ public class BlockUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* Search lowest common ancestor in dominator tree for input set.
|
||||
* Search the lowest common ancestor in dominator tree for input set.
|
||||
*/
|
||||
@Nullable
|
||||
public static BlockNode getCommonDominator(MethodNode mth, List<BlockNode> blocks) {
|
||||
@@ -1013,6 +1013,9 @@ public class BlockUtils {
|
||||
*/
|
||||
@Nullable
|
||||
public static InsnNode getOnlyOneInsnFromMth(MethodNode mth) {
|
||||
if (mth.isNoCode()) {
|
||||
return null;
|
||||
}
|
||||
InsnNode insn = null;
|
||||
for (BlockNode block : mth.getBasicBlocks()) {
|
||||
List<InsnNode> blockInsns = block.getInstructions();
|
||||
|
||||
@@ -246,4 +246,29 @@ public class DebugUtils {
|
||||
Set<Object> seen = ConcurrentHashMap.newKeySet();
|
||||
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 jadx.api.IDecompileScheduler;
|
||||
import jadx.api.JadxDecompiler;
|
||||
import jadx.api.JavaClass;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
|
||||
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 boolean DEBUG_BATCHES = false;
|
||||
|
||||
private final JadxDecompiler decompiler;
|
||||
|
||||
public DecompilerScheduler(JadxDecompiler decompiler) {
|
||||
this.decompiler = decompiler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<List<JavaClass>> buildBatches(List<JavaClass> classes) {
|
||||
try {
|
||||
long start = System.currentTimeMillis();
|
||||
List<List<ClassNode>> batches = internalBatches(Utils.collectionMap(classes, JavaClass::getClassNode));
|
||||
List<List<JavaClass>> result = Utils.collectionMap(batches, l -> Utils.collectionMapNoNull(l, decompiler::getJavaClassByNode));
|
||||
List<List<JavaClass>> result = internalBatches(classes);
|
||||
if (LOG.isDebugEnabled()) {
|
||||
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.
|
||||
* 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);
|
||||
Set<ClassNode> added = new HashSet<>(classes.size());
|
||||
Comparator<ClassNode> cmpDepSize = Comparator.comparingInt(ClassNode::getTotalDepsCount);
|
||||
List<List<ClassNode>> result = new ArrayList<>();
|
||||
List<ClassNode> mergedBatch = new ArrayList<>(MERGED_BATCH_SIZE);
|
||||
Set<JavaClass> added = new HashSet<>(classes.size());
|
||||
Comparator<JavaClass> cmpDepSize = Comparator.comparingInt(JavaClass::getTotalDepsCount);
|
||||
List<List<JavaClass>> result = new ArrayList<>();
|
||||
List<JavaClass> mergedBatch = new ArrayList<>(MERGED_BATCH_SIZE);
|
||||
for (DepInfo depInfo : deps) {
|
||||
ClassNode cls = depInfo.getCls();
|
||||
JavaClass cls = depInfo.getCls();
|
||||
if (!added.add(cls)) {
|
||||
continue;
|
||||
}
|
||||
@@ -73,9 +64,9 @@ public class DecompilerScheduler implements IDecompileScheduler {
|
||||
mergedBatch = new ArrayList<>(MERGED_BATCH_SIZE);
|
||||
}
|
||||
} else {
|
||||
List<ClassNode> batch = new ArrayList<>(depsSize + 1);
|
||||
for (ClassNode dep : cls.getDependencies()) {
|
||||
ClassNode topDep = dep.getTopParentClass();
|
||||
List<JavaClass> batch = new ArrayList<>(depsSize + 1);
|
||||
for (JavaClass dep : cls.getDependencies()) {
|
||||
JavaClass topDep = dep.getTopParentClass();
|
||||
if (!added.contains(topDep)) {
|
||||
batch.add(topDep);
|
||||
added.add(topDep);
|
||||
@@ -95,11 +86,11 @@ public class DecompilerScheduler implements IDecompileScheduler {
|
||||
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());
|
||||
for (ClassNode cls : classes) {
|
||||
for (JavaClass cls : classes) {
|
||||
int count = 0;
|
||||
for (ClassNode dep : cls.getDependencies()) {
|
||||
for (JavaClass dep : cls.getDependencies()) {
|
||||
count += 1 + dep.getTotalDepsCount();
|
||||
}
|
||||
deps.add(new DepInfo(cls, count));
|
||||
@@ -109,15 +100,15 @@ public class DecompilerScheduler implements IDecompileScheduler {
|
||||
}
|
||||
|
||||
private static final class DepInfo implements Comparable<DepInfo> {
|
||||
private final ClassNode cls;
|
||||
private final JavaClass cls;
|
||||
private final int depsCount;
|
||||
|
||||
private DepInfo(ClassNode cls, int depsCount) {
|
||||
private DepInfo(JavaClass cls, int depsCount) {
|
||||
this.cls = cls;
|
||||
this.depsCount = depsCount;
|
||||
}
|
||||
|
||||
public ClassNode getCls() {
|
||||
public JavaClass getCls() {
|
||||
return cls;
|
||||
}
|
||||
|
||||
@@ -129,7 +120,7 @@ public class DecompilerScheduler implements IDecompileScheduler {
|
||||
public int compareTo(@NotNull DecompilerScheduler.DepInfo o) {
|
||||
int deps = Integer.compare(depsCount, o.depsCount);
|
||||
if (deps == 0) {
|
||||
return cls.compareTo(o.cls);
|
||||
return cls.getClassNode().compareTo(o.cls.getClassNode());
|
||||
}
|
||||
return deps;
|
||||
}
|
||||
@@ -147,9 +138,9 @@ public class DecompilerScheduler implements IDecompileScheduler {
|
||||
.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);
|
||||
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);
|
||||
LOG.info("Batches stats:"
|
||||
+ "\n input classes: " + classes.size()
|
||||
|
||||
@@ -29,8 +29,12 @@ public final class InsnList implements Iterable<InsnNode> {
|
||||
}
|
||||
|
||||
public static int getIndex(List<InsnNode> list, InsnNode insn) {
|
||||
return getIndex(list, insn, 0);
|
||||
}
|
||||
|
||||
public static int getIndex(List<InsnNode> list, InsnNode insn, int startOffset) {
|
||||
int size = list.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
for (int i = startOffset; i < size; i++) {
|
||||
if (list.get(i) == insn) {
|
||||
return i;
|
||||
}
|
||||
@@ -38,6 +42,14 @@ public final class InsnList implements Iterable<InsnNode> {
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static boolean contains(List<InsnNode> list, InsnNode insn) {
|
||||
return getIndex(list, insn, 0) != -1;
|
||||
}
|
||||
|
||||
public static boolean contains(List<InsnNode> list, InsnNode insn, int startOffset) {
|
||||
return getIndex(list, insn, startOffset) != -1;
|
||||
}
|
||||
|
||||
public int getIndex(InsnNode insn) {
|
||||
return getIndex(list, insn);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package jadx.core.utils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -140,6 +141,37 @@ public class InsnUtils {
|
||||
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
|
||||
public static RegisterArg getRegFromInsn(List<RegisterArg> regs, InsnType insnType) {
|
||||
for (RegisterArg reg : regs) {
|
||||
|
||||
@@ -160,6 +160,10 @@ public class ListUtils {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static <T> boolean noneMatch(Collection<T> list, Predicate<T> test) {
|
||||
return !anyMatch(list, test);
|
||||
}
|
||||
|
||||
public static <T> boolean anyMatch(Collection<T> list, Predicate<T> test) {
|
||||
if (list == null || list.isEmpty()) {
|
||||
return false;
|
||||
|
||||
@@ -22,6 +22,7 @@ import jadx.core.dex.nodes.IRegion;
|
||||
import jadx.core.dex.nodes.InsnNode;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
import jadx.core.dex.regions.Region;
|
||||
import jadx.core.dex.regions.loops.LoopRegion;
|
||||
import jadx.core.dex.trycatch.CatchAttr;
|
||||
import jadx.core.dex.trycatch.ExceptionHandler;
|
||||
import jadx.core.dex.trycatch.TryCatchBlockAttr;
|
||||
@@ -289,7 +290,11 @@ public class RegionUtils {
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else if (container instanceof IRegion) {
|
||||
}
|
||||
if (container instanceof LoopRegion) {
|
||||
return true;
|
||||
}
|
||||
if (container instanceof IRegion) {
|
||||
IRegion region = (IRegion) container;
|
||||
for (IContainer block : region.getSubBlocks()) {
|
||||
if (notEmpty(block)) {
|
||||
@@ -297,9 +302,8 @@ public class RegionUtils {
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
throw new JadxRuntimeException(unknownContainerType(container));
|
||||
}
|
||||
throw new JadxRuntimeException(unknownContainerType(container));
|
||||
}
|
||||
|
||||
public static void getAllRegionBlocks(IContainer container, Set<IBlock> blocks) {
|
||||
|
||||
@@ -428,7 +428,7 @@ public class Utils {
|
||||
}
|
||||
|
||||
public static void checkThreadInterrupt() {
|
||||
if (Thread.interrupted()) {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
throw new JadxRuntimeException("Thread interrupted");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ public class AndroidResourcesUtils {
|
||||
FieldNode newResField = new FieldNode(typeCls, rFieldInfo,
|
||||
AccessFlags.PUBLIC | AccessFlags.STATIC | AccessFlags.FINAL);
|
||||
newResField.addAttr(new EncodedValue(EncodedType.ENCODED_INT, resource.getId()));
|
||||
typeCls.getFields().add(newResField);
|
||||
typeCls.addField(newResField);
|
||||
if (rClsExists) {
|
||||
newResField.addInfoComment("Added by JADX");
|
||||
}
|
||||
|
||||
@@ -10,12 +10,16 @@ import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.FileVisitOption;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.jar.JarEntry;
|
||||
@@ -103,31 +107,42 @@ public class FileUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static void deleteFileIfExists(Path filePath) throws IOException {
|
||||
Files.deleteIfExists(filePath);
|
||||
}
|
||||
|
||||
public static boolean deleteDir(File dir) {
|
||||
File[] content = dir.listFiles();
|
||||
if (content != null) {
|
||||
for (File file : content) {
|
||||
deleteDir(file);
|
||||
}
|
||||
}
|
||||
return dir.delete();
|
||||
deleteDir(dir.toPath());
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void deleteDirIfExists(Path 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) {
|
||||
try (Stream<Path> pathStream = Files.walk(dir)) {
|
||||
pathStream.sorted(Comparator.reverseOrder())
|
||||
.map(Path::toFile)
|
||||
.forEach(file -> {
|
||||
if (!file.delete()) {
|
||||
LOG.warn("Failed to remove file: {}", file.getAbsolutePath());
|
||||
}
|
||||
});
|
||||
private static final SimpleFileVisitor<Path> FILE_DELETE_VISITOR = new SimpleFileVisitor<Path>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||
Files.delete(file);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
|
||||
Files.delete(dir);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
};
|
||||
|
||||
private static void deleteDir(Path dir) {
|
||||
try {
|
||||
Files.walkFileTree(dir, Collections.emptySet(), Integer.MAX_VALUE, FILE_DELETE_VISITOR);
|
||||
} catch (Exception e) {
|
||||
throw new JadxRuntimeException("Failed to delete directory " + dir, e);
|
||||
}
|
||||
@@ -152,11 +167,11 @@ public class FileUtils {
|
||||
}
|
||||
|
||||
public static void deleteTempRootDir() {
|
||||
deleteDir(TEMP_ROOT_DIR);
|
||||
deleteDirIfExists(TEMP_ROOT_DIR);
|
||||
}
|
||||
|
||||
public static void clearTempRootDir() {
|
||||
deleteDir(TEMP_ROOT_DIR);
|
||||
deleteDirIfExists(TEMP_ROOT_DIR);
|
||||
makeDirs(TEMP_ROOT_DIR);
|
||||
}
|
||||
|
||||
@@ -217,6 +232,16 @@ public class FileUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static void writeFile(Path file, String data) throws IOException {
|
||||
FileUtils.makeDirsForFile(file);
|
||||
Files.write(file, data.getBytes(StandardCharsets.UTF_8),
|
||||
StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
}
|
||||
|
||||
public static String readFile(Path textFile) throws IOException {
|
||||
return new String(Files.readAllBytes(textFile), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static File prepareFile(File file) {
|
||||
File saveFile = cutFileName(file);
|
||||
@@ -254,6 +279,28 @@ public class FileUtils {
|
||||
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) {
|
||||
try (InputStream is = new FileInputStream(file)) {
|
||||
byte[] headers = new byte[4];
|
||||
|
||||
@@ -89,7 +89,8 @@ public class BinaryXMLParser extends CommonBinaryParser {
|
||||
is.mark(4);
|
||||
int v = is.readInt16(); // version
|
||||
int h = is.readInt16(); // header size
|
||||
if (v == 0x0003 && h == 0x0008) {
|
||||
// Some APK Manifest.xml the version is 0
|
||||
if (h == 0x0008) {
|
||||
return true;
|
||||
}
|
||||
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.Type;
|
||||
import com.android.aapt.Resources.Value;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import jadx.api.ICodeInfo;
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
import jadx.core.utils.files.FileUtils;
|
||||
import jadx.core.xmlgen.entry.EntryConfig;
|
||||
import jadx.core.xmlgen.entry.ProtoValue;
|
||||
import jadx.core.xmlgen.entry.ResourceEntry;
|
||||
import jadx.core.xmlgen.entry.ValuesParser;
|
||||
|
||||
public class ResProtoParser {
|
||||
public class ResProtoParser implements IResParser {
|
||||
private final RootNode root;
|
||||
private final ResourceStorage resStorage = new ResourceStorage();
|
||||
|
||||
@@ -38,11 +38,7 @@ public class ResProtoParser {
|
||||
}
|
||||
|
||||
public ResContainer decodeFiles(InputStream inputStream) throws IOException {
|
||||
ResourceTable table = decodeProto(inputStream);
|
||||
for (Package p : table.getPackageList()) {
|
||||
parse(p);
|
||||
}
|
||||
resStorage.finish();
|
||||
decode(inputStream);
|
||||
ValuesParser vp = new ValuesParser(new String[0], resStorage.getResourcesNames());
|
||||
ResXmlGen resGen = new ResXmlGen(resStorage, vp);
|
||||
ICodeInfo content = XmlGenUtils.makeXmlDump(root.makeCodeWriter(), resStorage);
|
||||
@@ -50,6 +46,15 @@ public class ResProtoParser {
|
||||
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) {
|
||||
String name = p.getPackageName();
|
||||
resStorage.setAppPackage(name);
|
||||
@@ -241,8 +246,13 @@ public class ResProtoParser {
|
||||
return "";
|
||||
}
|
||||
|
||||
private ResourceTable decodeProto(InputStream inputStream)
|
||||
throws InvalidProtocolBufferException, IOException {
|
||||
return ResourceTable.parseFrom(XmlGenUtils.readData(inputStream));
|
||||
@Override
|
||||
public ResourceStorage getResStorage() {
|
||||
return resStorage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getStrings() {
|
||||
return new String[0];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,21 +10,25 @@ import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.ICodeInfo;
|
||||
import jadx.api.args.ResourceNameSource;
|
||||
import jadx.core.deobf.NameMapper;
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.nodes.FieldNode;
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
import jadx.core.utils.BetterName;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.core.xmlgen.entry.EntryConfig;
|
||||
import jadx.core.xmlgen.entry.RawNamedValue;
|
||||
import jadx.core.xmlgen.entry.RawValue;
|
||||
import jadx.core.xmlgen.entry.ResourceEntry;
|
||||
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 Pattern VALID_RES_KEY_PATTERN = Pattern.compile("[\\w\\d_]+");
|
||||
@@ -76,6 +80,7 @@ public class ResTableParser extends CommonBinaryParser {
|
||||
this.useRawResName = useRawResNames;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(InputStream inputStream) throws IOException {
|
||||
is = new ParserStream(inputStream);
|
||||
decodeTableChunk();
|
||||
@@ -93,14 +98,6 @@ public class ResTableParser extends CommonBinaryParser {
|
||||
return ResContainer.resourceTable("res", xmlFiles, content);
|
||||
}
|
||||
|
||||
public ResourceStorage getResStorage() {
|
||||
return resStorage;
|
||||
}
|
||||
|
||||
public String[] getStrings() {
|
||||
return strings;
|
||||
}
|
||||
|
||||
void decodeTableChunk() throws IOException {
|
||||
is.checkInt16(RES_TABLE_TYPE, "Not a table chunk");
|
||||
is.checkInt16(0x000c, "Unexpected table header size");
|
||||
@@ -294,35 +291,66 @@ public class ResTableParser extends CommonBinaryParser {
|
||||
if (renamedKey != null) {
|
||||
return renamedKey;
|
||||
}
|
||||
FieldNode constField = root.getConstValues().getGlobalConstFields().get(resRef);
|
||||
if (constField != null) {
|
||||
constField.add(AFlag.DONT_RENAME);
|
||||
return constField.getName();
|
||||
}
|
||||
// styles might contain dots in name, use VALID_RES_KEY_PATTERN only for resource file name
|
||||
// styles might contain dots in name, search for alias only for resources names
|
||||
if (typeName.equals("style")) {
|
||||
return origKeyName;
|
||||
} else if (VALID_RES_KEY_PATTERN.matcher(origKeyName).matches()) {
|
||||
return origKeyName;
|
||||
}
|
||||
FieldNode constField = root.getConstValues().getGlobalConstFields().get(resRef);
|
||||
String resAlias = getResAlias(resRef, origKeyName, constField);
|
||||
resStorage.addRename(resRef, resAlias);
|
||||
if (constField != null) {
|
||||
constField.rename(resAlias);
|
||||
constField.add(AFlag.DONT_RENAME);
|
||||
}
|
||||
return resAlias;
|
||||
}
|
||||
|
||||
private String getResAlias(int resRef, String origKeyName, @Nullable FieldNode constField) {
|
||||
String name;
|
||||
if (constField == null || constField.getTopParentClass().isSynthetic()) {
|
||||
name = origKeyName;
|
||||
} else {
|
||||
name = getBetterName(root.getArgs().getResourceNameSource(), origKeyName, constField.getName());
|
||||
}
|
||||
Matcher matcher = VALID_RES_KEY_PATTERN.matcher(name);
|
||||
if (matcher.matches()) {
|
||||
return name;
|
||||
}
|
||||
// Making sure origKeyName compliant with resource file name rules
|
||||
Matcher m = VALID_RES_KEY_PATTERN.matcher(origKeyName);
|
||||
String cleanedResName = cleanName(matcher);
|
||||
String newResName = String.format("res_0x%08x", resRef);
|
||||
if (cleanedResName.isEmpty()) {
|
||||
return newResName;
|
||||
}
|
||||
// autogenerate key name, appended with cleaned origKeyName to be human-friendly
|
||||
return newResName + "_" + cleanedResName.toLowerCase();
|
||||
}
|
||||
|
||||
public static String getBetterName(ResourceNameSource nameSource, String resName, String codeName) {
|
||||
switch (nameSource) {
|
||||
case AUTO:
|
||||
return BetterName.compareAndGet(resName, codeName);
|
||||
case RESOURCES:
|
||||
return resName;
|
||||
case CODE:
|
||||
return codeName;
|
||||
|
||||
default:
|
||||
throw new JadxRuntimeException("Unexpected ResourceNameSource value: " + nameSource);
|
||||
}
|
||||
}
|
||||
|
||||
private String cleanName(Matcher matcher) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
boolean first = true;
|
||||
while (m.find()) {
|
||||
while (matcher.find()) {
|
||||
if (!first) {
|
||||
sb.append("_");
|
||||
}
|
||||
sb.append(m.group());
|
||||
sb.append(matcher.group());
|
||||
first = false;
|
||||
}
|
||||
// autogenerate key name, appended with cleaned origKeyName to be human-friendly
|
||||
String newResName = String.format("res_0x%08x", resRef);
|
||||
String cleanedResName = sb.toString();
|
||||
if (!cleanedResName.isEmpty()) {
|
||||
newResName += "_" + cleanedResName.toLowerCase();
|
||||
}
|
||||
return newResName;
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private RawNamedValue parseValueMap() throws IOException {
|
||||
@@ -434,4 +462,14 @@ public class ResTableParser extends CommonBinaryParser {
|
||||
is.skipToPos(start + length, "readScriptOrVariantChar");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceStorage getResStorage() {
|
||||
return resStorage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getStrings() {
|
||||
return strings;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,11 +168,13 @@ public class ResXmlGen {
|
||||
private void addItem(ICodeWriter cw, String itemTag, String typeName, RawNamedValue value) {
|
||||
String nameStr = vp.decodeNameRef(value.getNameRef());
|
||||
String valueStr = vp.decodeValue(value.getRawValue());
|
||||
int dataType = value.getRawValue().getDataType();
|
||||
|
||||
if (!typeName.equals("attr")) {
|
||||
if (valueStr == null || valueStr.equals("0")) {
|
||||
if (dataType == ParserConstants.TYPE_REFERENCE && (valueStr == null || valueStr.equals("0"))) {
|
||||
valueStr = "@null";
|
||||
}
|
||||
if (nameStr != null) {
|
||||
if (dataType == ParserConstants.TYPE_INT_DEC && nameStr != null) {
|
||||
try {
|
||||
int intVal = Integer.parseInt(valueStr);
|
||||
String newVal = ManifestAttributes.getInstance().decode(nameStr.replace("android:attr.", ""), intVal);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
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;
|
||||
|
||||
public class JadxInternalAccess {
|
||||
@@ -7,4 +10,16 @@ public class JadxInternalAccess {
|
||||
public static RootNode getRoot(JadxDecompiler d) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package jadx.core.utils;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static jadx.core.utils.BetterName.calcRating;
|
||||
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
|
||||
|
||||
public class TestBetterName {
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
expectFirst("color_main", "t0");
|
||||
expectFirst("done", "oOo0oO0o");
|
||||
}
|
||||
|
||||
private void expectFirst(String first, String second) {
|
||||
String best = BetterName.compareAndGet(first, second);
|
||||
assertThat(best)
|
||||
.as(() -> String.format("'%s'=%d, '%s'=%d", first, calcRating(first), second, calcRating(second)))
|
||||
.isEqualTo(first);
|
||||
}
|
||||
}
|
||||
@@ -43,9 +43,6 @@ import jadx.api.args.DeobfuscationMapFileMode;
|
||||
import jadx.api.metadata.ICodeMetadata;
|
||||
import jadx.api.metadata.annotations.InsnCodeOffset;
|
||||
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.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.rightPad;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.empty;
|
||||
import static org.hamcrest.Matchers.emptyArray;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
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.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_DIRECTORY2 = "jadx-core/" + TEST_DIRECTORY;
|
||||
|
||||
private static final String OUT_DIR = "test-out-tmp";
|
||||
|
||||
private static final String DEFAULT_INPUT_PLUGIN = "dx";
|
||||
/**
|
||||
* 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;
|
||||
|
||||
args = new JadxArgs();
|
||||
args.setOutDir(new File(OUT_DIR));
|
||||
args.setOutDir(new File("test-out-tmp"));
|
||||
args.setShowInconsistentCode(true);
|
||||
args.setThreadsCount(1);
|
||||
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() {
|
||||
return this.getClass().getSimpleName();
|
||||
}
|
||||
@@ -287,7 +284,7 @@ public abstract class IntegrationTest extends TestUtils {
|
||||
}
|
||||
|
||||
protected void runChecks(List<ClassNode> clsList) {
|
||||
clsList.forEach(this::checkCode);
|
||||
clsList.forEach(cls -> checkCode(cls, allowWarnInCode));
|
||||
compileClassNode(clsList);
|
||||
clsList.forEach(this::runAutoCheck);
|
||||
}
|
||||
@@ -345,33 +342,6 @@ public abstract class IntegrationTest extends TestUtils {
|
||||
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) {
|
||||
String clsName = cls.getClassInfo().getRawName().replace('/', '.');
|
||||
try {
|
||||
@@ -585,7 +555,7 @@ public abstract class IntegrationTest extends TestUtils {
|
||||
|
||||
protected void enableDeobfuscation() {
|
||||
args.setDeobfuscationOn(true);
|
||||
args.setDeobfuscationMapFileMode(DeobfuscationMapFileMode.OVERWRITE);
|
||||
args.setDeobfuscationMapFileMode(DeobfuscationMapFileMode.IGNORE);
|
||||
args.setDeobfuscationMinLength(2);
|
||||
args.setDeobfuscationMaxLength(64);
|
||||
}
|
||||
|
||||
@@ -19,10 +19,15 @@ import static org.hamcrest.Matchers.notNullValue;
|
||||
|
||||
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_EXT = ".smali";
|
||||
|
||||
private String currentProject = "jadx-core";
|
||||
|
||||
public void setCurrentProject(String currentProject) {
|
||||
this.currentProject = currentProject;
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void init() {
|
||||
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());
|
||||
}
|
||||
|
||||
private static File getSmaliFile(String baseName) {
|
||||
private File getSmaliFile(String baseName) {
|
||||
File smaliFile = new File(SMALI_TESTS_DIR, baseName + SMALI_TESTS_EXT);
|
||||
if (smaliFile.exists()) {
|
||||
return smaliFile;
|
||||
}
|
||||
File pathFromRoot = new File(SMALI_TESTS_PROJECT, smaliFile.getPath());
|
||||
File pathFromRoot = new File(currentProject, smaliFile.getPath());
|
||||
if (pathFromRoot.exists()) {
|
||||
return pathFromRoot;
|
||||
}
|
||||
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);
|
||||
if (smaliDir.exists()) {
|
||||
return smaliDir;
|
||||
}
|
||||
File pathFromRoot = new File(SMALI_TESTS_PROJECT, smaliDir.getPath());
|
||||
File pathFromRoot = new File(currentProject, smaliDir.getPath());
|
||||
if (pathFromRoot.exists()) {
|
||||
return pathFromRoot;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.tools.DiagnosticListener;
|
||||
import javax.tools.JavaCompiler;
|
||||
@@ -82,7 +81,7 @@ public class TestCompiler implements Closeable {
|
||||
arguments.addAll(options.getArguments());
|
||||
|
||||
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);
|
||||
CompilationTask compilerTask = compiler.getTask(out, fileManager, diagnostic, arguments, null, jfObjects);
|
||||
if (Boolean.FALSE.equals(compilerTask.call())) {
|
||||
@@ -114,7 +113,7 @@ public class TestCompiler implements Closeable {
|
||||
assertNotNull(mth, "Failed to get method " + methodName + '(' + Arrays.toString(types) + ')');
|
||||
return mth.invoke(inst, args);
|
||||
} catch (Throwable e) {
|
||||
IntegrationTest.rethrow("Invoke error", e);
|
||||
IntegrationTest.rethrow("Invoke error for method: " + methodName, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,10 @@ public enum TestProfile implements Consumer<IntegrationTest> {
|
||||
test.useTargetJavaVersion(11);
|
||||
test.useJavaInput();
|
||||
}),
|
||||
JAVA17("java-17", test -> {
|
||||
test.useTargetJavaVersion(17);
|
||||
test.useJavaInput();
|
||||
}),
|
||||
ECJ_DX_J8("ecj-dx-j8", test -> {
|
||||
test.useEclipseCompiler();
|
||||
test.useTargetJavaVersion(8);
|
||||
@@ -52,8 +56,9 @@ public enum TestProfile implements Consumer<IntegrationTest> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(IntegrationTest integrationTest) {
|
||||
this.setup.accept(integrationTest);
|
||||
public void accept(IntegrationTest test) {
|
||||
this.setup.accept(test);
|
||||
test.setOutDirSuffix(description);
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
|
||||
@@ -3,7 +3,21 @@ package jadx.tests.api.utils;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import jadx.NotYetImplementedExtension;
|
||||
import jadx.api.CommentsLevel;
|
||||
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)
|
||||
public class TestUtils {
|
||||
@@ -35,4 +49,31 @@ public class TestUtils {
|
||||
}
|
||||
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.metadata.ICodeAnnotation;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.core.dex.nodes.ICodeNode;
|
||||
import jadx.tests.api.IntegrationTest;
|
||||
|
||||
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
|
||||
@@ -58,11 +57,12 @@ public class JadxClassNodeAssertions extends AbstractObjectAssert<JadxClassNodeA
|
||||
return this;
|
||||
}
|
||||
|
||||
public void checkCodeAnnotationFor(String refStr, ICodeNode node) {
|
||||
public JadxClassNodeAssertions checkCodeAnnotationFor(String refStr, ICodeAnnotation 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();
|
||||
int codePos = code.getCodeStr().indexOf(refStr);
|
||||
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()) {
|
||||
if (entry.getKey() == refPos) {
|
||||
Assertions.assertThat(entry.getValue()).isEqualTo(node);
|
||||
return;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
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.JadxDecompiler;
|
||||
import jadx.api.JadxInternalAccess;
|
||||
import jadx.api.metadata.ICodeAnnotation;
|
||||
import jadx.api.metadata.ICodeNodeRef;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
import jadx.core.utils.DebugChecks;
|
||||
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.Matchers.greaterThan;
|
||||
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);
|
||||
|
||||
protected JadxDecompiler decompiler;
|
||||
|
||||
protected abstract String getSamplesDir();
|
||||
|
||||
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) {
|
||||
JadxDecompiler jadx = new JadxDecompiler(jadxArgs);
|
||||
jadx.load();
|
||||
decompiler = new JadxDecompiler(jadxArgs);
|
||||
decompiler.load();
|
||||
|
||||
if (clsPatternStr == null) {
|
||||
jadx.save();
|
||||
decompiler.save();
|
||||
} else {
|
||||
processByPatterns(jadx, clsPatternStr, mthPatternStr);
|
||||
processByPatterns(decompiler, clsPatternStr, mthPatternStr);
|
||||
}
|
||||
printErrorReport(jadx);
|
||||
return jadx;
|
||||
printErrorReport(decompiler);
|
||||
return decompiler;
|
||||
}
|
||||
|
||||
private void processByPatterns(JadxDecompiler jadx, String clsPattern, @Nullable String mthPattern) {
|
||||
@@ -109,7 +112,7 @@ public abstract class BaseExternalTest extends IntegrationTest {
|
||||
} else {
|
||||
LOG.info("Code: \n{}", classNode.getCode());
|
||||
}
|
||||
checkCode(classNode);
|
||||
checkCode(classNode, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -134,7 +137,7 @@ public abstract class BaseExternalTest extends IntegrationTest {
|
||||
String dashLine = "======================================================================================";
|
||||
for (MethodNode mth : classNode.getMethods()) {
|
||||
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(),
|
||||
dashLine,
|
||||
mthCode,
|
||||
@@ -143,36 +146,33 @@ public abstract class BaseExternalTest extends IntegrationTest {
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String cutMethodCode(ICodeInfo codeInfo, String code, MethodNode mth) {
|
||||
int defPos = mth.getDefPosition();
|
||||
int startPos = getCommentStartPos(code, defPos);
|
||||
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();
|
||||
private String cutMethodCode(ICodeInfo codeInfo, MethodNode mth) {
|
||||
int startPos = getCommentStartPos(codeInfo, mth.getDefPosition());
|
||||
int stopPos = getNextNodePos(mth, codeInfo);
|
||||
return codeInfo.getCodeStr().substring(startPos, stopPos);
|
||||
}
|
||||
|
||||
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;
|
||||
int emptyLinePos = code.lastIndexOf(emptyLine, pos);
|
||||
int emptyLinePos = codeInfo.getCodeStr().lastIndexOf(emptyLine, pos);
|
||||
return emptyLinePos == -1 ? pos : emptyLinePos + emptyLine.length();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package jadx.tests.integration.deobf;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import jadx.tests.api.IntegrationTest;
|
||||
|
||||
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
|
||||
|
||||
public class TestInheritedMethodRename extends IntegrationTest {
|
||||
|
||||
public static class TestCls {
|
||||
|
||||
public static class A extends B {
|
||||
}
|
||||
|
||||
public static class B {
|
||||
public void call() {
|
||||
System.out.println("call");
|
||||
}
|
||||
}
|
||||
|
||||
public void test(A a) {
|
||||
// reference to A.call() not renamed,
|
||||
// should be resolved to B.call() and use alias
|
||||
a.call();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
noDebugInfo();
|
||||
enableDeobfuscation();
|
||||
getArgs().setDeobfuscationMinLength(99);
|
||||
|
||||
assertThat(getClassNode(TestCls.class))
|
||||
.code()
|
||||
.containsOne("public void m0call() {")
|
||||
.doesNotContain(".call();")
|
||||
.containsOne(".m0call();");
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
package jadx.tests.integration.enums;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.api.CommentsLevel;
|
||||
import jadx.tests.api.SmaliTest;
|
||||
|
||||
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
|
||||
@@ -32,14 +34,31 @@ public class TestEnumObfuscated extends SmaliTest {
|
||||
public synthetic int getNum() {
|
||||
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
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
ClassNode cls = getClassNodeFromSmali();
|
||||
assertThat(cls)
|
||||
getArgs().setCommentsLevel(CommentsLevel.WARN);
|
||||
getArgs().setRenameFlags(Collections.emptySet());
|
||||
assertThat(getClassNodeFromSmali())
|
||||
.code()
|
||||
.doesNotContain("$VLS")
|
||||
.doesNotContain("vo(")
|
||||
|
||||
+1
-3
@@ -56,8 +56,6 @@ public class TestGenericsMthOverride extends IntegrationTest {
|
||||
assertThat(code, containsOne("public Y method(Exception x) {"));
|
||||
assertThat(code, containsOne("public Object method(Object x) {"));
|
||||
|
||||
assertThat(code, countString(3, "@Override"));
|
||||
// TODO: @Override missing for class C
|
||||
// assertThat(code, countString(4, "@Override"));
|
||||
assertThat(code, countString(4, "@Override"));
|
||||
}
|
||||
}
|
||||
|
||||
+47
@@ -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)");
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user