Compare commits
162 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 03009b5159 | |||
| d25eda4839 | |||
| 59003bdb1f | |||
| 9d4babcdce | |||
| 8c28a8530e | |||
| 6775cc5a93 | |||
| e648e60af9 | |||
| e6b7db35c1 | |||
| c3f7027bdd | |||
| 2fe95da570 | |||
| 62fa2735dc | |||
| bce6611aaf | |||
| 21aa90c5d1 | |||
| 55e79fb70f | |||
| 044c75ab9f | |||
| 97fa8ff210 | |||
| 27c283fb11 | |||
| bd1c3fffde | |||
| 169ad2901f | |||
| 869422b424 | |||
| ccc4164d54 | |||
| b61642a646 | |||
| 189e4181de | |||
| 5b960db77e | |||
| 3119e8c893 | |||
| f95c5e8f39 | |||
| 1e908f7af3 | |||
| 7ce40baacb | |||
| 7d689a85ea | |||
| c7a162d827 | |||
| 325b3ac991 | |||
| 9a8a11619b | |||
| 7ae6bd737c | |||
| 57fd9b5bdb | |||
| cdd5bf536d | |||
| dcce3aaa39 | |||
| b3d86ae908 | |||
| 8b7d3f497e | |||
| 462e582bb8 | |||
| 14a7b63707 | |||
| 4051a50146 | |||
| b3db337abd | |||
| 15ea9a56b9 | |||
| 165ae24722 | |||
| 11b38ffed2 | |||
| 81dfac6d83 | |||
| a3bd3b09fd | |||
| 13d306024a | |||
| ff64da705c | |||
| 00196e412b | |||
| 06c4fea4d2 | |||
| 69fd88d883 | |||
| f2f145019d | |||
| 7b3563fb62 | |||
| 22ee9a216c | |||
| 06b4467fdc | |||
| ff778ab372 | |||
| ff4dde62ae | |||
| 468b52342d | |||
| 09d39de604 | |||
| eb079bd435 | |||
| d1b1fc0ab6 | |||
| 859d479569 | |||
| b0954e9620 | |||
| dfe1d0477d | |||
| a1aa6d7ecd | |||
| 6f01e9f76b | |||
| be8b96280e | |||
| 2a2806ebd7 | |||
| c7a0f7a092 | |||
| 9710ebe09a | |||
| f0da486703 | |||
| 925503ba04 | |||
| 039900a278 | |||
| d73455c689 | |||
| 07b3e5a7c0 | |||
| c1fc73a524 | |||
| 0d982e9709 | |||
| e6b38e172a | |||
| 331c4aaa5e | |||
| 4e82233cbc | |||
| ad267e1618 | |||
| 54265e34e5 | |||
| c677901aa5 | |||
| 220a2198a1 | |||
| b725dd18b6 | |||
| a0466d4494 | |||
| ae1a5e9277 | |||
| 018c20187d | |||
| add11dff1d | |||
| 005a59668c | |||
| 8f727325d6 | |||
| 7bbb58863b | |||
| 8629e4cf22 | |||
| bd5c6b6516 | |||
| 85a7aaa9fb | |||
| a3df83e60f | |||
| b3be2794a0 | |||
| 02ea3be8f2 | |||
| b78745a87b | |||
| 01af6481f6 | |||
| 0c3e6e77cd | |||
| 8b48219dc3 | |||
| 80187c7e29 | |||
| 22f7b40151 | |||
| 3e709d6693 | |||
| 1aa16c4664 | |||
| 9fba709687 | |||
| 6b61599114 | |||
| 2829e284f3 | |||
| 27b15aeb1c | |||
| 74c52a8884 | |||
| 39e7b82353 | |||
| 365e258180 | |||
| ea68024851 | |||
| 56ae4a6048 | |||
| 1d831d82d4 | |||
| 4c760f1029 | |||
| cf0101f13d | |||
| 748f45b386 | |||
| 9e278efc5c | |||
| cc1beab0b5 | |||
| 2fba204b33 | |||
| 9bf079aad4 | |||
| 6aeaf6aca9 | |||
| 7ea478e18a | |||
| ef99412de1 | |||
| cda1b1ad2c | |||
| 05863881f8 | |||
| 3e208509e0 | |||
| bd75baef56 | |||
| d191f62b8d | |||
| f17c46224d | |||
| e008e818b0 | |||
| 560b1a9ca7 | |||
| 06f113f0a6 | |||
| 48a0073002 | |||
| 88e90722ae | |||
| 1becfa9977 | |||
| 20ed13936a | |||
| 421de675a2 | |||
| 06a118c104 | |||
| 2e93656007 | |||
| c8c4adb60c | |||
| f2e78fe800 | |||
| 7172f8df2d | |||
| d7c6be5664 | |||
| 654cf5e4fb | |||
| bf2d5b5e2e | |||
| 0f495afc99 | |||
| 5f1985f281 | |||
| 2a574d45d5 | |||
| 3ee476c6b6 | |||
| fd3d488016 | |||
| 9898cbb4a1 | |||
| 104a0f0636 | |||
| e58b77267e | |||
| 73913651b4 | |||
| f01e6aa505 | |||
| d9da6a7f89 | |||
| 5726a52ab6 | |||
| f61d90ec2f |
@@ -8,7 +8,7 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
java-version: 25
|
||||
|
||||
- name: Set jadx version
|
||||
run: |
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
echo "JADX_VERSION=$JADX_VERSION" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
|
||||
- name: Build
|
||||
run: ./gradlew dist distWin
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
JADX_BUILD_JAVA_VERSION: 11
|
||||
|
||||
- name: Save bundle artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ format('jadx-{0}', env.JADX_VERSION) }}
|
||||
# Waiting fix for https://github.com/actions/upload-artifact/issues/39 to upload zip file
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
retention-days: 14
|
||||
|
||||
- name: Save Windows bundle artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ format('jadx-gui-{0}-no-jre-win', env.JADX_VERSION) }}
|
||||
# Upload unpacked files for now
|
||||
@@ -54,14 +54,14 @@ jobs:
|
||||
build-win-bundle:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK
|
||||
uses: oracle-actions/setup-java@v1
|
||||
with:
|
||||
release: 24
|
||||
release: 25
|
||||
|
||||
- name: Print Java version
|
||||
shell: bash
|
||||
@@ -75,13 +75,13 @@ jobs:
|
||||
echo "JADX_VERSION=$JADX_VERSION" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
|
||||
- name: Build
|
||||
run: ./gradlew dist -PbundleJRE=true
|
||||
|
||||
- name: Save Windows with JRE bundle artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ format('jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }}
|
||||
# Upload unpacked files for now
|
||||
|
||||
@@ -14,19 +14,18 @@ jobs:
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
java-version: 25
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
|
||||
- name: Build
|
||||
run: ./gradlew build dist distWin
|
||||
env:
|
||||
JADX_BUILD_JAVA_VERSION: 11
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [master]
|
||||
schedule:
|
||||
- cron: '0 9 * * 5'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: ['java']
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
queries: +security-extended
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
# Don't build tests in jadx-core also skip tests execution and checkstyle tasks
|
||||
- run: |
|
||||
./gradlew clean build -x checkstyleTest -x checkstyleMain -x test -x ':jadx-core:testClasses'
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
@@ -13,28 +13,28 @@ jobs:
|
||||
build-release-win-bundle:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up JDK
|
||||
uses: oracle-actions/setup-java@v1
|
||||
with:
|
||||
release: 24
|
||||
release: 25
|
||||
|
||||
- name: Set jadx version
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const jadxVersion = context.ref.split('/').pop().substring(1)
|
||||
core.exportVariable('JADX_VERSION', jadxVersion);
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
|
||||
- name: Build
|
||||
run: ./gradlew dist -PbundleJRE=true
|
||||
|
||||
- name: Save JRE bundle artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ format('jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }}
|
||||
path: ${{ format('build/distWinWithJre/jadx-gui-{0}-with-jre-win.zip', env.JADX_VERSION) }}
|
||||
@@ -45,23 +45,23 @@ jobs:
|
||||
needs: build-release-win-bundle
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
java-version: 25
|
||||
|
||||
- name: Set jadx version and release name
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const jadxVersion = context.ref.split('/').pop().substring(1)
|
||||
core.exportVariable('JADX_VERSION', jadxVersion);
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
|
||||
- name: Build
|
||||
run: ./gradlew dist distWin
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
JADX_BUILD_JAVA_VERSION: 11
|
||||
|
||||
- name: Download Windows JRE bundle
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: ${{ format('jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }}
|
||||
path: ${{ format('build/jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }}
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
ls -l *.zip
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
name: ${{ env.JADX_VERSION }}
|
||||
draft: true
|
||||
|
||||
+4
-1
@@ -37,7 +37,10 @@ jadx-output/
|
||||
*.log
|
||||
*.cfg
|
||||
*.orig
|
||||
quark.json
|
||||
*.json
|
||||
*.dot
|
||||
|
||||
.env
|
||||
|
||||
cliff.toml
|
||||
jadx-gui/src/main/resources/logback.xml
|
||||
|
||||
@@ -72,7 +72,7 @@ For Windows, you can download it from [oracle.com](https://www.oracle.com/java/t
|
||||
You can use jadx in your java projects, check details on [wiki page](https://github.com/skylot/jadx/wiki/Use-jadx-as-a-library)
|
||||
|
||||
### Build from source
|
||||
JDK 11 or higher must be installed:
|
||||
JDK 17 or higher must be installed:
|
||||
```
|
||||
git clone https://github.com/skylot/jadx.git
|
||||
cd jadx
|
||||
@@ -91,110 +91,122 @@ commands (use '<command> --help' for command options):
|
||||
plugins - manage jadx plugins
|
||||
|
||||
options:
|
||||
-d, --output-dir - output directory
|
||||
-ds, --output-dir-src - output directory for sources
|
||||
-dr, --output-dir-res - output directory for resources
|
||||
-r, --no-res - do not decode resources
|
||||
-s, --no-src - do not decompile source code
|
||||
-j, --threads-count - processing threads count, default: 4
|
||||
--single-class - decompile a single class, full name, raw or alias
|
||||
--single-class-output - file or dir for write if decompile a single class
|
||||
--output-format - can be 'java' or 'json', default: java
|
||||
-e, --export-gradle - save as gradle project (set '--export-gradle-type' to 'auto')
|
||||
--export-gradle-type - Gradle project template for export:
|
||||
'auto' - detect automatically
|
||||
'android-app' - Android Application (apk)
|
||||
'android-library' - Android Library (aar)
|
||||
'simple-java' - simple Java
|
||||
-m, --decompilation-mode - code output mode:
|
||||
'auto' - trying best options (default)
|
||||
'restructure' - restore code structure (normal java code)
|
||||
'simple' - simplified instructions (linear, with goto's)
|
||||
'fallback' - raw instructions without modifications
|
||||
--show-bad-code - show inconsistent code (incorrectly decompiled)
|
||||
--no-xml-pretty-print - do not prettify XML
|
||||
--no-imports - disable use of imports, always write entire package name
|
||||
--no-debug-info - disable debug info parsing and processing
|
||||
--add-debug-lines - add comments with debug line numbers if available
|
||||
--no-inline-anonymous - disable anonymous classes inline
|
||||
--no-inline-methods - disable methods inline
|
||||
--no-move-inner-classes - disable move inner classes into parent
|
||||
--no-inline-kotlin-lambda - disable inline for Kotlin lambdas
|
||||
--no-finally - don't extract finally block
|
||||
--no-restore-switch-over-string - don't restore switch over string
|
||||
--no-replace-consts - don't replace constant value with matching constant field
|
||||
--escape-unicode - escape non latin characters in strings (with \u)
|
||||
--respect-bytecode-access-modifiers - don't change original access modifiers
|
||||
--mappings-path - deobfuscation mappings file or directory. Allowed formats: Tiny and Tiny v2 (both '.tiny'), Enigma (.mapping) or Enigma directory
|
||||
--mappings-mode - set mode for handling the deobfuscation mapping file:
|
||||
'read' - just read, user can always save manually (default)
|
||||
'read-and-autosave-every-change' - read and autosave after every change
|
||||
'read-and-autosave-before-closing' - read and autosave before exiting the app or closing the project
|
||||
'ignore' - don't read or save (can be used to skip loading mapping files referenced in the project file)
|
||||
--deobf - activate deobfuscation
|
||||
--deobf-min - min length of name, renamed if shorter, default: 3
|
||||
--deobf-max - max length of name, renamed if longer, default: 64
|
||||
--deobf-whitelist - space separated list of classes (full name) and packages (ends with '.*') to exclude from deobfuscation, default: android.support.v4.* android.support.v7.* android.support.v4.os.* android.support.annotation.Px androidx.core.os.* androidx.annotation.Px
|
||||
--deobf-cfg-file - deobfuscation mappings file used for JADX auto-generated names (in the JOBF file format), default: same dir and name as input file with '.jobf' extension
|
||||
--deobf-cfg-file-mode - set mode for handling the JADX auto-generated names' deobfuscation map file:
|
||||
'read' - read if found, don't save (default)
|
||||
'read-or-save' - read if found, save otherwise (don't overwrite)
|
||||
'overwrite' - don't read, always save
|
||||
'ignore' - don't read and don't save
|
||||
--deobf-res-name-source - better name source for resources:
|
||||
'auto' - automatically select best name (default)
|
||||
'resources' - use resources names
|
||||
'code' - use R class fields names
|
||||
--use-source-name-as-class-name-alias - use source name as class name alias:
|
||||
'always' - always use source name if it's available
|
||||
'if-better' - use source name if it seems better than the current one
|
||||
'never' - never use source name, even if it's available
|
||||
--source-name-repeat-limit - allow using source name if it appears less than a limit number, default: 10
|
||||
--use-kotlin-methods-for-var-names - use kotlin intrinsic methods to rename variables, values: disable, apply, apply-and-hide, default: apply
|
||||
--rename-flags - fix options (comma-separated list of):
|
||||
'case' - fix case sensitivity issues (according to --fs-case-sensitive option),
|
||||
'valid' - rename java identifiers to make them valid,
|
||||
'printable' - remove non-printable chars from identifiers,
|
||||
or single 'none' - to disable all renames
|
||||
or single 'all' - to enable all (default)
|
||||
--integer-format - how integers are displayed:
|
||||
'auto' - automatically select (default)
|
||||
'decimal' - use decimal
|
||||
'hexadecimal' - use hexadecimal
|
||||
--fs-case-sensitive - treat filesystem as case sensitive, false by default
|
||||
--cfg - save methods control flow graph to dot file
|
||||
--raw-cfg - save methods control flow graph (use raw instructions)
|
||||
-f, --fallback - set '--decompilation-mode' to 'fallback' (deprecated)
|
||||
--use-dx - use dx/d8 to convert java bytecode
|
||||
--comments-level - set code comments level, values: error, warn, info, debug, user-only, none, default: info
|
||||
--log-level - set log level, values: quiet, progress, error, warn, info, debug, default: progress
|
||||
-v, --verbose - verbose output (set --log-level to DEBUG)
|
||||
-q, --quiet - turn off output (set --log-level to QUIET)
|
||||
--disable-plugins - comma separated list of plugin ids to disable, default:
|
||||
--version - print jadx version
|
||||
-h, --help - print this help
|
||||
-d, --output-dir - output directory
|
||||
-ds, --output-dir-src - output directory for sources
|
||||
-dr, --output-dir-res - output directory for resources
|
||||
-r, --no-res - do not decode resources
|
||||
-s, --no-src - do not decompile source code
|
||||
-j, --threads-count - processing threads count, default: 16
|
||||
--single-class - decompile a single class, full name, raw or alias
|
||||
--single-class-output - file or dir for write if decompile a single class
|
||||
--output-format - can be 'java' or 'json', default: java
|
||||
-e, --export-gradle - save as gradle project (set '--export-gradle-type' to 'auto')
|
||||
--export-gradle-type - Gradle project template for export:
|
||||
'auto' - detect automatically
|
||||
'android-app' - Android Application (apk)
|
||||
'android-library' - Android Library (aar)
|
||||
'simple-java' - simple Java
|
||||
-m, --decompilation-mode - code output mode:
|
||||
'auto' - trying best options (default)
|
||||
'restructure' - restore code structure (normal java code)
|
||||
'simple' - simplified instructions (linear, with goto's)
|
||||
'fallback' - raw instructions without modifications
|
||||
--show-bad-code - show inconsistent code (incorrectly decompiled)
|
||||
--no-xml-pretty-print - do not prettify XML
|
||||
--no-imports - disable use of imports, always write entire package name
|
||||
--no-debug-info - disable debug info parsing and processing
|
||||
--add-debug-lines - add comments with debug line numbers if available
|
||||
--no-inline-anonymous - disable anonymous classes inline
|
||||
--no-inline-methods - disable methods inline
|
||||
--no-move-inner-classes - disable move inner classes into parent
|
||||
--no-inline-kotlin-lambda - disable inline for Kotlin lambdas
|
||||
--no-finally - don't extract finally block
|
||||
--no-restore-switch-over-string - don't restore switch over string
|
||||
--no-replace-consts - don't replace constant value with matching constant field
|
||||
--escape-unicode - escape non latin characters in strings (with \u)
|
||||
--respect-bytecode-access-modifiers - don't change original access modifiers
|
||||
--mappings-path - deobfuscation mappings file or directory. Allowed formats: Tiny and Tiny v2 (both '.tiny'), Enigma (.mapping) or Enigma directory
|
||||
--mappings-mode - set mode for handling the deobfuscation mapping file:
|
||||
'read' - just read, user can always save manually (default)
|
||||
'read-and-autosave-every-change' - read and autosave after every change
|
||||
'read-and-autosave-before-closing' - read and autosave before exiting the app or closing the project
|
||||
'ignore' - don't read or save (can be used to skip loading mapping files referenced in the project file)
|
||||
--deobf - activate deobfuscation
|
||||
--deobf-min - min length of name, renamed if shorter, default: 3
|
||||
--deobf-max - max length of name, renamed if longer, default: 64
|
||||
--deobf-whitelist - space separated list of classes (full name) and packages (ends with '.*') to exclude from deobfuscation, default: android.support.v4.* android.support.v7.* android.support.v4.os.* android.support.annotation.Px androidx.core.os.* androidx.annotation.Px
|
||||
--deobf-cfg-file - deobfuscation mappings file used for JADX auto-generated names (in the JOBF file format), default: same dir and name as input file with '.jobf' extension
|
||||
--deobf-cfg-file-mode - set mode for handling the JADX auto-generated names' deobfuscation map file:
|
||||
'read' - read if found, don't save (default)
|
||||
'read-or-save' - read if found, save otherwise (don't overwrite)
|
||||
'overwrite' - don't read, always save
|
||||
'ignore' - don't read and don't save
|
||||
--deobf-res-name-source - better name source for resources:
|
||||
'auto' - automatically select best name (default)
|
||||
'resources' - use resources names
|
||||
'code' - use R class fields names
|
||||
--use-source-name-as-class-name-alias - use source name as class name alias:
|
||||
'always' - always use source name if it's available
|
||||
'if-better' - use source name if it seems better than the current one
|
||||
'never' - never use source name, even if it's available
|
||||
--source-name-repeat-limit - allow using source name if it appears less than a limit number, default: 10
|
||||
--use-kotlin-methods-for-var-names - use kotlin intrinsic methods to rename variables, values: disable, apply, apply-and-hide, default: apply
|
||||
--use-headers-for-detect-resource-extensions - Use headers for detect resource extensions if resource obfuscated
|
||||
--rename-flags - fix options (comma-separated list of):
|
||||
'case' - fix case sensitivity issues (according to --fs-case-sensitive option),
|
||||
'valid' - rename java identifiers to make them valid,
|
||||
'printable' - remove non-printable chars from identifiers,
|
||||
or single 'none' - to disable all renames
|
||||
or single 'all' - to enable all (default)
|
||||
--integer-format - how integers are displayed:
|
||||
'auto' - automatically select (default)
|
||||
'decimal' - use decimal
|
||||
'hexadecimal' - use hexadecimal
|
||||
--type-update-limit - type update limit count (per one instruction), default: 10
|
||||
--fs-case-sensitive - treat filesystem as case sensitive, false by default
|
||||
--cfg - save methods control flow graph to dot file
|
||||
--raw-cfg - save methods control flow graph (use raw instructions)
|
||||
--call-graph - save app call graph in format: 'dot' or 'json', default: none
|
||||
-f, --fallback - set '--decompilation-mode' to 'fallback' (deprecated)
|
||||
--use-dx - use dx/d8 to convert java bytecode
|
||||
--comments-level - set code comments level, values: error, warn, info, debug, user-only, none, default: info
|
||||
--log-level - set log level, values: quiet, progress, error, warn, info, debug, default: progress
|
||||
-v, --verbose - verbose output (set --log-level to DEBUG)
|
||||
-q, --quiet - turn off output (set --log-level to QUIET)
|
||||
--disable-plugins - comma separated list of plugin ids to disable
|
||||
--config <config-ref> - load configuration from file, <config-ref> can be:
|
||||
path to '.json' file
|
||||
short name - uses file with this name from config directory
|
||||
'none' - to disable config loading
|
||||
--save-config <config-ref> - save current options into configuration file and exit, <config-ref> can be:
|
||||
empty - for default config
|
||||
path to '.json' file
|
||||
short name - file will be saved in config directory
|
||||
--print-files - print files and directories used by jadx (config, cache, temp)
|
||||
--version - print jadx version
|
||||
-h, --help - print this help
|
||||
|
||||
Plugin options (-P<name>=<value>):
|
||||
dex-input: Load .dex and .apk files
|
||||
- dex-input.verify-checksum - verify dex file checksum before load, values: [yes, no], default: yes
|
||||
- dex-input.verify-checksum - verify dex file checksum before load, values: [yes, no], default: yes
|
||||
java-convert: Convert .class, .jar and .aar files to dex
|
||||
- java-convert.mode - convert mode, values: [dx, d8, both], default: both
|
||||
- java-convert.d8-desugar - use desugar in d8, values: [yes, no], default: no
|
||||
- java-convert.mode - convert mode, values: [dx, d8, both], default: both
|
||||
- java-convert.d8-desugar - use desugar in d8, values: [yes, no], default: no
|
||||
kotlin-metadata: Use kotlin.Metadata annotation for code generation
|
||||
- kotlin-metadata.class-alias - rename class alias, values: [yes, no], default: yes
|
||||
- kotlin-metadata.method-args - rename function arguments, values: [yes, no], default: yes
|
||||
- kotlin-metadata.fields - rename fields, values: [yes, no], default: yes
|
||||
- kotlin-metadata.companion - rename companion object, values: [yes, no], default: yes
|
||||
- kotlin-metadata.data-class - add data class modifier, values: [yes, no], default: yes
|
||||
- kotlin-metadata.to-string - rename fields using toString, values: [yes, no], default: yes
|
||||
- kotlin-metadata.getters - rename simple getters to field names, values: [yes, no], default: yes
|
||||
- kotlin-metadata.class-alias - rename class alias, values: [yes, no], default: yes
|
||||
- kotlin-metadata.method-args - rename function arguments, values: [yes, no], default: yes
|
||||
- kotlin-metadata.fields - rename fields, values: [yes, no], default: yes
|
||||
- kotlin-metadata.companion - rename companion object, values: [yes, no], default: yes
|
||||
- kotlin-metadata.data-class - add data class modifier, values: [yes, no], default: yes
|
||||
- kotlin-metadata.to-string - rename fields using toString, values: [yes, no], default: yes
|
||||
- kotlin-metadata.getters - rename simple getters to field names, values: [yes, no], default: yes
|
||||
kotlin-smap: Use kotlin.SourceDebugExtension annotation for rename class alias
|
||||
- kotlin-smap.class-alias-source-dbg - rename class alias from SourceDebugExtension, values: [yes, no], default: no
|
||||
- kotlin-smap.class-alias-source-dbg - rename class alias from SourceDebugExtension, values: [yes, no], default: no
|
||||
rename-mappings: various mappings support
|
||||
- rename-mappings.format - mapping format, values: [AUTO, TINY_FILE, TINY_2_FILE, ENIGMA_FILE, ENIGMA_DIR, SRG_FILE, XSRG_FILE, JAM_FILE, CSRG_FILE, TSRG_FILE, TSRG_2_FILE, PROGUARD_FILE, INTELLIJ_MIGRATION_MAP_FILE, RECAF_SIMPLE_FILE, JOBF_FILE], default: AUTO
|
||||
- rename-mappings.invert - invert mapping on load, values: [yes, no], default: no
|
||||
- rename-mappings.format - mapping format, values: [AUTO, TINY_FILE, TINY_2_FILE, ENIGMA_FILE, ENIGMA_DIR, PROGUARD_FILE, SRG_FILE, XSRG_FILE, JAM_FILE, CSRG_FILE, TSRG_FILE, TSRG_2_FILE, INTELLIJ_MIGRATION_MAP_FILE, RECAF_SIMPLE_FILE, JOBF_FILE], default: AUTO
|
||||
- rename-mappings.invert - invert mapping on load, values: [yes, no], default: no
|
||||
smali-input: Load .smali files
|
||||
- smali-input.api-level - Android API level, default: 27
|
||||
- smali-input.api-level - Android API level, default: 27
|
||||
|
||||
Environment variables:
|
||||
JADX_DISABLE_XML_SECURITY - set to 'true' to disable all security checks for XML files
|
||||
|
||||
+72
-12
@@ -6,12 +6,14 @@ import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
|
||||
import java.util.Locale
|
||||
|
||||
plugins {
|
||||
id("com.github.ben-manes.versions") version "0.52.0"
|
||||
id("se.patrikerdes.use-latest-versions") version "0.2.18"
|
||||
id("com.diffplug.spotless") version "6.25.0"
|
||||
id("com.github.ben-manes.versions") version "0.54.0"
|
||||
id("se.patrikerdes.use-latest-versions") version "0.2.19"
|
||||
id("com.diffplug.spotless") version "8.7.0"
|
||||
}
|
||||
|
||||
val jadxVersion by extra { System.getenv("JADX_VERSION") ?: "dev" }
|
||||
val jadxEnv = loadEnv(file("$rootDir/.env"))
|
||||
|
||||
val jadxVersion by extra { jadxEnv["JADX_VERSION"] ?: "dev" }
|
||||
println("jadx version: $jadxVersion")
|
||||
version = jadxVersion
|
||||
|
||||
@@ -19,7 +21,7 @@ val jadxBuildJavaVersion by extra { getBuildJavaVersion() }
|
||||
|
||||
fun getBuildJavaVersion(): Int? {
|
||||
val envVarName = "JADX_BUILD_JAVA_VERSION"
|
||||
val buildJavaVer = System.getenv(envVarName)?.toInt() ?: return null
|
||||
val buildJavaVer = jadxEnv[envVarName]?.toInt() ?: return null
|
||||
if (buildJavaVer < 11) {
|
||||
throw GradleException("'$envVarName' can't be set to lower than 11")
|
||||
}
|
||||
@@ -27,6 +29,24 @@ fun getBuildJavaVersion(): Int? {
|
||||
return buildJavaVer
|
||||
}
|
||||
|
||||
// control ErrorProne checks level, can be: off, warn, error
|
||||
val jadxBuildChecksMode: String by extra { getBuildChecksMode() }
|
||||
|
||||
fun getBuildChecksMode(): String {
|
||||
val buildChecksMode = jadxEnv["JADX_BUILD_CHECKS_MODE"]?.lowercase() ?: "off"
|
||||
val expectedValues = listOf("off", "warn", "error")
|
||||
if (!expectedValues.contains(buildChecksMode)) {
|
||||
throw GradleException("Unknown check mode: '$buildChecksMode', should be one of $expectedValues")
|
||||
}
|
||||
if (buildChecksMode != "off") {
|
||||
val javaVersion = jadxBuildJavaVersion?.let { JavaVersion.toVersion(it) } ?: JavaVersion.current()
|
||||
if (!javaVersion.isCompatibleWith(JavaVersion.VERSION_21)) {
|
||||
throw GradleException("Error Prone requires Java 21")
|
||||
}
|
||||
}
|
||||
return buildChecksMode
|
||||
}
|
||||
|
||||
allprojects {
|
||||
apply(plugin = "java")
|
||||
apply(plugin = "checkstyle")
|
||||
@@ -82,12 +102,49 @@ fun isNonStable(version: String): Boolean {
|
||||
return isStable.not()
|
||||
}
|
||||
|
||||
fun loadEnv(file: File): Map<String, String> {
|
||||
val envMap = HashMap<String, String>()
|
||||
System
|
||||
.getenv()
|
||||
.filter { it.key.startsWith("JADX_") }
|
||||
.forEach { envMap[it.key] = it.value }
|
||||
if (file.exists()) {
|
||||
file
|
||||
.readLines()
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotEmpty() && !it.startsWith("#") }
|
||||
.forEach {
|
||||
val (k, v) = it.split("=", limit = 2)
|
||||
envMap[k.trim()] = v.trim()
|
||||
}
|
||||
}
|
||||
println(
|
||||
"Loaded env vars (${envMap.size}):\n${
|
||||
envMap.toList().sortedBy { it.first }.joinToString(separator = "\n") { "${it.first}=${it.second}" }
|
||||
}\n",
|
||||
)
|
||||
return envMap
|
||||
}
|
||||
|
||||
val distWinConfiguration: Configuration by configurations.creating {
|
||||
isCanBeConsumed = false
|
||||
}
|
||||
val distWinWithJreConfiguration: Configuration by configurations.creating {
|
||||
isCanBeConsumed = false
|
||||
}
|
||||
dependencies {
|
||||
distWinConfiguration(project(":jadx-gui", "distWinConfiguration"))
|
||||
distWinWithJreConfiguration(project(":jadx-gui", "distWinWithJreConfiguration"))
|
||||
}
|
||||
|
||||
val copyArtifacts by tasks.registering(Copy::class) {
|
||||
val jarCliPattern = "jadx-cli-(.*)-all.jar".toPattern()
|
||||
from(tasks.getByPath(":jadx-cli:installShadowDist")) {
|
||||
exclude("**/*.jar")
|
||||
filter { line ->
|
||||
jarCliPattern.matcher(line).replaceAll("jadx-$1-all.jar")
|
||||
jarCliPattern
|
||||
.matcher(line)
|
||||
.replaceAll("jadx-$1-all.jar")
|
||||
.replace("-jar \"\\\"\$CLASSPATH\\\"\"", "-cp \"\\\"\$CLASSPATH\\\"\" jadx.cli.JadxCLI")
|
||||
.replace("-jar \"%CLASSPATH%\"", "-cp \"%CLASSPATH%\" jadx.cli.JadxCLI")
|
||||
}
|
||||
@@ -113,15 +170,20 @@ val pack by tasks.registering(Zip::class) {
|
||||
from(copyArtifacts)
|
||||
archiveFileName.set("jadx-$jadxVersion.zip")
|
||||
destinationDirectory.set(layout.buildDirectory)
|
||||
eachFile {
|
||||
if (path == "bin/jadx" || path == "bin/jadx-gui") {
|
||||
permissions {
|
||||
unix("rwxr-xr-x")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val distWin by tasks.registering(Zip::class) {
|
||||
group = "jadx"
|
||||
description = "Build Windows bundle"
|
||||
|
||||
val guiTask = tasks.getByPath("jadx-gui:copyDistWin")
|
||||
dependsOn(guiTask)
|
||||
from(guiTask.outputs)
|
||||
from(distWinConfiguration)
|
||||
|
||||
destinationDirectory.set(layout.buildDirectory.dir("distWin"))
|
||||
archiveFileName.set("jadx-gui-$jadxVersion-win.zip")
|
||||
@@ -131,9 +193,7 @@ val distWin by tasks.registering(Zip::class) {
|
||||
val distWinWithJre by tasks.registering(Zip::class) {
|
||||
description = "Build Windows with JRE bundle"
|
||||
|
||||
val guiTask = tasks.getByPath(":jadx-gui:copyDistWinWithJre")
|
||||
dependsOn(guiTask)
|
||||
from(guiTask.outputs)
|
||||
from(distWinWithJreConfiguration)
|
||||
|
||||
destinationDirectory.set(layout.buildDirectory.dir("distWinWithJre"))
|
||||
archiveFileName.set("jadx-gui-$jadxVersion-with-jre-win.zip")
|
||||
|
||||
@@ -3,9 +3,11 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.0")
|
||||
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.10")
|
||||
|
||||
implementation("org.openrewrite:plugin:6.19.1")
|
||||
implementation("net.ltgt.errorprone:net.ltgt.errorprone.gradle.plugin:4.2.0")
|
||||
implementation("net.ltgt.nullaway:net.ltgt.nullaway.gradle.plugin:2.3.0")
|
||||
}
|
||||
|
||||
repositories {
|
||||
|
||||
@@ -1,29 +1,38 @@
|
||||
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
|
||||
import net.ltgt.gradle.errorprone.CheckSeverity
|
||||
import net.ltgt.gradle.errorprone.errorprone
|
||||
import net.ltgt.gradle.nullaway.nullaway
|
||||
|
||||
plugins {
|
||||
java
|
||||
checkstyle
|
||||
|
||||
id("jadx-rewrite")
|
||||
id("net.ltgt.errorprone")
|
||||
id("net.ltgt.nullaway")
|
||||
}
|
||||
|
||||
val jadxVersion: String by rootProject.extra
|
||||
val jadxBuildJavaVersion: Int? by rootProject.extra
|
||||
val jadxBuildChecksMode: String by rootProject.extra
|
||||
|
||||
group = "io.github.skylot"
|
||||
version = jadxVersion
|
||||
|
||||
dependencies {
|
||||
implementation("org.slf4j:slf4j-api:2.0.17")
|
||||
compileOnly("org.jetbrains:annotations:26.0.2")
|
||||
implementation("org.slf4j:slf4j-api:2.0.18")
|
||||
compileOnly("org.jetbrains:annotations:26.1.0")
|
||||
|
||||
testImplementation("ch.qos.logback:logback-classic:1.5.18")
|
||||
testImplementation("org.assertj:assertj-core:3.27.3")
|
||||
testImplementation("ch.qos.logback:logback-classic:1.5.34")
|
||||
testImplementation("org.assertj:assertj-core:3.27.7")
|
||||
|
||||
testImplementation("org.junit.jupiter:junit-jupiter:5.13.3")
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
|
||||
testCompileOnly("org.jetbrains:annotations:26.0.2")
|
||||
testCompileOnly("org.jetbrains:annotations:26.1.0")
|
||||
|
||||
errorprone("com.google.errorprone:error_prone_core:2.50.0")
|
||||
errorprone("com.uber.nullaway:nullaway:0.13.7")
|
||||
}
|
||||
|
||||
repositories {
|
||||
@@ -62,3 +71,27 @@ tasks {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<JavaCompile>().configureEach {
|
||||
val checkEnabled = jadxBuildChecksMode != "off"
|
||||
if (checkEnabled) {
|
||||
options.compilerArgs.add("-XDaddTypeAnnotationsToSymbol=true")
|
||||
}
|
||||
options.errorprone {
|
||||
isEnabled = checkEnabled
|
||||
allErrorsAsWarnings = jadxBuildChecksMode == "warn"
|
||||
excludedPaths = ".*/test/.*"
|
||||
nullaway {
|
||||
if (jadxBuildChecksMode == "error") {
|
||||
error()
|
||||
}
|
||||
annotatedPackages.add("jadx")
|
||||
}
|
||||
// TODO: fix and enable all checks
|
||||
disable("MixedMutabilityReturnType")
|
||||
disable("EqualsGetClass")
|
||||
disable("OperatorPrecedence")
|
||||
disable("UnusedVariable")
|
||||
disable("ImmutableEnumChecker")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,11 @@ plugins {
|
||||
id("org.jetbrains.kotlin.jvm")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(kotlin("stdlib"))
|
||||
implementation(kotlin("reflect")) // don't work from plugin classloader
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_11)
|
||||
|
||||
@@ -7,10 +7,10 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
rewrite("org.openrewrite.recipe:rewrite-testing-frameworks:3.13.0")
|
||||
rewrite("org.openrewrite.recipe:rewrite-logging-frameworks:3.11.0")
|
||||
rewrite("org.openrewrite.recipe:rewrite-migrate-java:3.13.0")
|
||||
rewrite("org.openrewrite.recipe:rewrite-static-analysis:2.12.0")
|
||||
rewrite("org.openrewrite.recipe:rewrite-testing-frameworks:3.38.0")
|
||||
rewrite("org.openrewrite.recipe:rewrite-logging-frameworks:3.29.1")
|
||||
rewrite("org.openrewrite.recipe:rewrite-migrate-java:3.37.0")
|
||||
rewrite("org.openrewrite.recipe:rewrite-static-analysis:2.37.0")
|
||||
}
|
||||
|
||||
tasks {
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+2
-2
@@ -1,7 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||
distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -57,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@@ -114,7 +114,6 @@ case "$( uname )" in #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH="\\\"\\\""
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
@@ -172,7 +171,6 @@ fi
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
@@ -212,7 +210,6 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
|
||||
Vendored
+1
-2
@@ -70,11 +70,10 @@ goto fail
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
|
||||
@@ -11,6 +11,7 @@ dependencies {
|
||||
implementation(project(":jadx-core"))
|
||||
implementation(project(":jadx-plugins-tools"))
|
||||
implementation(project(":jadx-commons:jadx-app-commons"))
|
||||
implementation(project(":jadx-commons:jadx-analysis"))
|
||||
|
||||
runtimeOnly(project(":jadx-plugins:jadx-dex-input"))
|
||||
runtimeOnly(project(":jadx-plugins:jadx-java-input"))
|
||||
@@ -19,13 +20,14 @@ dependencies {
|
||||
runtimeOnly(project(":jadx-plugins:jadx-rename-mappings"))
|
||||
runtimeOnly(project(":jadx-plugins:jadx-kotlin-metadata"))
|
||||
runtimeOnly(project(":jadx-plugins:jadx-kotlin-source-debug-extension"))
|
||||
runtimeOnly(project(":jadx-plugins:jadx-script:jadx-script-plugin"))
|
||||
runtimeOnly(project(":jadx-plugins:jadx-xapk-input"))
|
||||
runtimeOnly(project(":jadx-plugins:jadx-aab-input"))
|
||||
runtimeOnly(project(":jadx-plugins:jadx-apkm-input"))
|
||||
runtimeOnly(project(":jadx-plugins:jadx-apks-input"))
|
||||
|
||||
implementation("org.jcommander:jcommander:2.0")
|
||||
implementation("ch.qos.logback:logback-classic:1.5.18")
|
||||
implementation("ch.qos.logback:logback-classic:1.5.34")
|
||||
implementation("com.google.code.gson:gson:2.14.0")
|
||||
}
|
||||
|
||||
application {
|
||||
@@ -36,6 +38,7 @@ application {
|
||||
"-XX:+IgnoreUnrecognizedVMOptions",
|
||||
"-Xms256M",
|
||||
"-XX:MaxRAMPercentage=70.0",
|
||||
"-XX:ParallelGCThreads=3",
|
||||
// disable zip checks (#1962)
|
||||
"-Djdk.util.zip.disableZip64ExtraFieldValidation=true",
|
||||
// Foreign API access for 'directories' library (Windows only)
|
||||
|
||||
@@ -25,7 +25,6 @@ import jadx.api.plugins.options.OptionDescription;
|
||||
import jadx.core.plugins.JadxPluginManager;
|
||||
import jadx.core.plugins.PluginContext;
|
||||
import jadx.core.utils.Utils;
|
||||
import jadx.plugins.tools.JadxExternalPluginsLoader;
|
||||
|
||||
public class JCommanderWrapper {
|
||||
private final JCommander jc;
|
||||
@@ -41,12 +40,12 @@ public class JCommanderWrapper {
|
||||
|
||||
public boolean parse(String[] args) {
|
||||
try {
|
||||
jc.parse(args);
|
||||
String[] fixedArgs = fixArgsForEmptySaveConfig(args);
|
||||
jc.parse(fixedArgs);
|
||||
applyFiles(argsObj);
|
||||
return true;
|
||||
} catch (ParameterException e) {
|
||||
System.err.println("Arguments parse error: " + e.getMessage());
|
||||
printUsage();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -96,6 +95,41 @@ public class JCommanderWrapper {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Workaround to allow empty value (i.e. zero arity) for '--save-config' option
|
||||
* Insert empty string arg if another option start right after this one, or it is a last one.
|
||||
*/
|
||||
private String[] fixArgsForEmptySaveConfig(String[] args) {
|
||||
int len = args.length;
|
||||
for (int i = 0; i < len; i++) {
|
||||
String arg = args[i];
|
||||
if (arg.equals("--save-config")) {
|
||||
int next = i + 1;
|
||||
if (next == len) {
|
||||
return insertEmptyArg(args, next, true);
|
||||
}
|
||||
if (next < len) {
|
||||
String nextArg = args[next];
|
||||
if (nextArg.startsWith("-")) {
|
||||
return insertEmptyArg(args, next, false);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
private static String[] insertEmptyArg(String[] args, int i, boolean add) {
|
||||
List<String> strings = new ArrayList<>(Arrays.asList(args));
|
||||
if (add) {
|
||||
strings.add("");
|
||||
} else {
|
||||
strings.add(i, "");
|
||||
}
|
||||
return strings.toArray(new String[0]);
|
||||
}
|
||||
|
||||
public void printUsage() {
|
||||
LogHelper.setLogLevel(LogHelper.LogLevelEnum.ERROR); // mute logger while printing help
|
||||
|
||||
@@ -182,7 +216,9 @@ public class JCommanderWrapper {
|
||||
}
|
||||
if (addDefaults) {
|
||||
String defaultValue = getDefaultValue(args, f);
|
||||
if (defaultValue != null && !description.contains("(default)")) {
|
||||
if (defaultValue != null
|
||||
&& !defaultValue.isEmpty()
|
||||
&& !description.contains("(default)")) {
|
||||
opt.append(", default: ").append(defaultValue);
|
||||
}
|
||||
}
|
||||
@@ -238,19 +274,16 @@ public class JCommanderWrapper {
|
||||
|
||||
private String appendPluginOptions(int maxNamesLen) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
int k = 1;
|
||||
// load and init all options plugins to print all options
|
||||
try (JadxDecompiler decompiler = new JadxDecompiler(argsObj.toJadxArgs())) {
|
||||
JadxPluginManager pluginManager = decompiler.getPluginManager();
|
||||
pluginManager.load(new JadxExternalPluginsLoader());
|
||||
pluginManager.load(decompiler.getArgs().getPluginLoader());
|
||||
pluginManager.initAll();
|
||||
try {
|
||||
for (PluginContext context : pluginManager.getAllPluginContexts()) {
|
||||
JadxPluginOptions options = context.getOptions();
|
||||
if (options != null) {
|
||||
if (appendPlugin(context.getPluginInfo(), context.getOptions(), sb, maxNamesLen)) {
|
||||
k++;
|
||||
}
|
||||
appendPlugin(context.getPluginInfo(), context.getOptions(), sb, maxNamesLen);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package jadx.cli;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.analysis.callgraph.JadxCallGraph;
|
||||
import jadx.analysis.callgraph.api.ICallGraph;
|
||||
import jadx.api.JadxArgs;
|
||||
import jadx.api.JadxDecompiler;
|
||||
import jadx.api.impl.AnnotatedCodeWriter;
|
||||
@@ -13,8 +16,10 @@ import jadx.api.impl.NoOpCodeCache;
|
||||
import jadx.api.impl.SimpleCodeWriter;
|
||||
import jadx.api.usage.impl.EmptyUsageInfoCache;
|
||||
import jadx.cli.LogHelper.LogLevelEnum;
|
||||
import jadx.cli.config.JadxConfigAdapter;
|
||||
import jadx.cli.plugins.JadxFilesGetter;
|
||||
import jadx.core.utils.exceptions.JadxArgsValidateException;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.plugins.tools.JadxExternalPluginsLoader;
|
||||
|
||||
public class JadxCLI {
|
||||
@@ -35,15 +40,17 @@ public class JadxCLI {
|
||||
|
||||
public static int execute(String[] args, @Nullable Consumer<JadxArgs> argsMod) {
|
||||
try {
|
||||
JadxCLIArgs cliArgs = new JadxCLIArgs();
|
||||
if (cliArgs.processArgs(args)) {
|
||||
JadxArgs jadxArgs = buildArgs(cliArgs);
|
||||
if (argsMod != null) {
|
||||
argsMod.accept(jadxArgs);
|
||||
}
|
||||
return runSave(jadxArgs, cliArgs);
|
||||
JadxCLIArgs cliArgs = JadxCLIArgs.processArgs(args,
|
||||
new JadxCLIArgs(),
|
||||
new JadxConfigAdapter<>(JadxCLIArgs.class, "cli"));
|
||||
if (cliArgs == null) {
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
JadxArgs jadxArgs = buildArgs(cliArgs);
|
||||
if (argsMod != null) {
|
||||
argsMod.accept(jadxArgs);
|
||||
}
|
||||
return runSave(jadxArgs, cliArgs);
|
||||
} catch (JadxArgsValidateException e) {
|
||||
LOG.error("Incorrect arguments: {}", e.getMessage());
|
||||
return 1;
|
||||
@@ -54,8 +61,6 @@ public class JadxCLI {
|
||||
}
|
||||
|
||||
private static JadxArgs buildArgs(JadxCLIArgs cliArgs) {
|
||||
LogHelper.initLogLevel(cliArgs);
|
||||
LogHelper.setLogLevelsForLoadingStage();
|
||||
JadxArgs jadxArgs = cliArgs.toJadxArgs();
|
||||
jadxArgs.setCodeCache(new NoOpCodeCache());
|
||||
jadxArgs.setUsageInfoCache(new EmptyUsageInfoCache());
|
||||
@@ -70,9 +75,9 @@ public class JadxCLI {
|
||||
try (JadxDecompiler jadx = new JadxDecompiler(jadxArgs)) {
|
||||
jadx.load();
|
||||
if (checkForErrors(jadx)) {
|
||||
return 1;
|
||||
return 2;
|
||||
}
|
||||
LogHelper.setLogLevelsForDecompileStage();
|
||||
writeCallGraph(jadx, cliArgs);
|
||||
if (!SingleClassMode.process(jadx, cliArgs)) {
|
||||
save(jadx);
|
||||
}
|
||||
@@ -80,7 +85,7 @@ public class JadxCLI {
|
||||
if (errorsCount != 0) {
|
||||
jadx.printErrorsReport();
|
||||
LOG.error("finished with errors, count: {}", errorsCount);
|
||||
return 1;
|
||||
return 3;
|
||||
}
|
||||
LOG.info("done");
|
||||
return 0;
|
||||
@@ -110,10 +115,10 @@ public class JadxCLI {
|
||||
jadx.getArgs().setSkipSources(true);
|
||||
}
|
||||
}
|
||||
if (jadx.getErrorsCount() > 0) {
|
||||
LOG.error("Load with errors! Check log for details");
|
||||
int errorsCount = jadx.getErrorsCount();
|
||||
if (errorsCount > 0) {
|
||||
LOG.error("Loading finished with errors! Count: {}", errorsCount);
|
||||
// continue processing
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -131,4 +136,29 @@ public class JadxCLI {
|
||||
System.out.print(" \r");
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeCallGraph(JadxDecompiler jadx, JadxCLIArgs cliArgs) {
|
||||
JadxCLIArgs.CallGraphSaveMode mode = cliArgs.callGraphSaveMode;
|
||||
if (mode == null || mode == JadxCLIArgs.CallGraphSaveMode.NONE) {
|
||||
return;
|
||||
}
|
||||
Path outPath = jadx.getArgs().getOutDir().toPath();
|
||||
ICallGraph callGraph = JadxCallGraph.builder(jadx)
|
||||
.resolvedOnly(true)
|
||||
.build();
|
||||
Path cgPath;
|
||||
switch (mode) {
|
||||
case JSON:
|
||||
cgPath = outPath.resolve("callgraph.json");
|
||||
callGraph.writeJson(cgPath);
|
||||
break;
|
||||
case DOT:
|
||||
cgPath = outPath.resolve("callgraph.dot");
|
||||
callGraph.writeDot(cgPath);
|
||||
break;
|
||||
default:
|
||||
throw new JadxRuntimeException("Unexpected call graph save mode: " + mode);
|
||||
}
|
||||
LOG.info("Call graph saved: {}", cgPath.toAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.beust.jcommander.DynamicParameter;
|
||||
import com.beust.jcommander.IStringConverter;
|
||||
@@ -31,22 +33,33 @@ import jadx.api.args.IntegerFormat;
|
||||
import jadx.api.args.ResourceNameSource;
|
||||
import jadx.api.args.UseSourceNameAsClassNameAlias;
|
||||
import jadx.api.args.UserRenamesMappingsMode;
|
||||
import jadx.cli.config.IJadxConfig;
|
||||
import jadx.cli.config.JadxConfigAdapter;
|
||||
import jadx.cli.config.JadxConfigExclude;
|
||||
import jadx.commons.app.JadxCommonFiles;
|
||||
import jadx.commons.app.JadxTempFiles;
|
||||
import jadx.core.deobf.conditions.DeobfWhitelist;
|
||||
import jadx.core.export.ExportGradleType;
|
||||
import jadx.core.utils.exceptions.JadxArgsValidateException;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.core.utils.files.FileUtils;
|
||||
|
||||
public class JadxCLIArgs {
|
||||
public class JadxCLIArgs implements IJadxConfig {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(JadxCLIArgs.class);
|
||||
|
||||
@JadxConfigExclude
|
||||
@Parameter(description = "<input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk, .apkm, .jadx.kts)")
|
||||
protected List<String> files = Collections.emptyList();
|
||||
|
||||
@JadxConfigExclude
|
||||
@Parameter(names = { "-d", "--output-dir" }, description = "output directory")
|
||||
protected String outDir;
|
||||
|
||||
@JadxConfigExclude
|
||||
@Parameter(names = { "-ds", "--output-dir-src" }, description = "output directory for sources")
|
||||
protected String outDirSrc;
|
||||
|
||||
@JadxConfigExclude
|
||||
@Parameter(names = { "-dr", "--output-dir-res" }, description = "output directory for resources")
|
||||
protected String outDirRes;
|
||||
|
||||
@@ -59,9 +72,11 @@ public class JadxCLIArgs {
|
||||
@Parameter(names = { "-j", "--threads-count" }, description = "processing threads count")
|
||||
protected int threadsCount = JadxArgs.DEFAULT_THREADS_COUNT;
|
||||
|
||||
@JadxConfigExclude
|
||||
@Parameter(names = { "--single-class" }, description = "decompile a single class, full name, raw or alias")
|
||||
protected String singleClass = null;
|
||||
|
||||
@JadxConfigExclude
|
||||
@Parameter(names = { "--single-class-output" }, description = "file or dir for write if decompile a single class")
|
||||
protected String singleClassOutput = null;
|
||||
|
||||
@@ -166,6 +181,7 @@ public class JadxCLIArgs {
|
||||
)
|
||||
protected String deobfuscationWhitelistStr = DeobfWhitelist.DEFAULT_STR;
|
||||
|
||||
@JadxConfigExclude
|
||||
@Parameter(
|
||||
names = { "--deobf-cfg-file" },
|
||||
description = "deobfuscation mappings file used for JADX auto-generated names (in the JOBF file format),"
|
||||
@@ -241,7 +257,7 @@ public class JadxCLIArgs {
|
||||
+ "\n 'printable' - remove non-printable chars from identifiers,"
|
||||
+ "\nor single 'none' - to disable all renames"
|
||||
+ "\nor single 'all' - to enable all (default)",
|
||||
converter = RenameConverter.class
|
||||
listConverter = RenameConverter.class
|
||||
)
|
||||
protected Set<RenameEnum> renameFlags = EnumSet.allOf(RenameEnum.class);
|
||||
|
||||
@@ -255,6 +271,9 @@ public class JadxCLIArgs {
|
||||
)
|
||||
protected IntegerFormat integerFormat = IntegerFormat.AUTO;
|
||||
|
||||
@Parameter(names = { "--type-update-limit" }, description = "type update limit count (per one instruction)")
|
||||
protected int typeUpdatesLimitCount = 10;
|
||||
|
||||
@Parameter(names = { "--fs-case-sensitive" }, description = "treat filesystem as case sensitive, false by default")
|
||||
protected boolean fsCaseSensitive = false;
|
||||
|
||||
@@ -264,6 +283,13 @@ public class JadxCLIArgs {
|
||||
@Parameter(names = { "--raw-cfg" }, description = "save methods control flow graph (use raw instructions)")
|
||||
protected boolean rawCfgOutput = false;
|
||||
|
||||
@Parameter(
|
||||
names = { "--call-graph" },
|
||||
description = "save app call graph in format: 'dot' or 'json'",
|
||||
converter = CallGraphSaveModeConverter.class
|
||||
)
|
||||
protected CallGraphSaveMode callGraphSaveMode = CallGraphSaveMode.NONE;
|
||||
|
||||
@Parameter(names = { "-f", "--fallback" }, description = "set '--decompilation-mode' to 'fallback' (deprecated)")
|
||||
protected boolean fallbackMode = false;
|
||||
|
||||
@@ -284,27 +310,110 @@ public class JadxCLIArgs {
|
||||
)
|
||||
protected LogHelper.LogLevelEnum logLevel = LogHelper.LogLevelEnum.PROGRESS;
|
||||
|
||||
@JadxConfigExclude
|
||||
@Parameter(names = { "-v", "--verbose" }, description = "verbose output (set --log-level to DEBUG)")
|
||||
protected boolean verbose = false;
|
||||
|
||||
@JadxConfigExclude
|
||||
@Parameter(names = { "-q", "--quiet" }, description = "turn off output (set --log-level to QUIET)")
|
||||
protected boolean quiet = false;
|
||||
|
||||
@JadxConfigExclude
|
||||
@Parameter(names = { "--disable-plugins" }, description = "comma separated list of plugin ids to disable")
|
||||
protected String disablePlugins = "";
|
||||
|
||||
@JadxConfigExclude
|
||||
@Parameter(
|
||||
names = { "--config" },
|
||||
defaultValueDescription = "<config-ref>",
|
||||
description = "load configuration from file, <config-ref> can be:"
|
||||
+ "\n path to '.json' file"
|
||||
+ "\n short name - uses file with this name from config directory"
|
||||
+ "\n 'none' - to disable config loading"
|
||||
)
|
||||
protected String config = "";
|
||||
|
||||
@JadxConfigExclude
|
||||
@Parameter(
|
||||
names = { "--save-config" },
|
||||
defaultValueDescription = "<config-ref>",
|
||||
description = "save current options into configuration file and exit, <config-ref> can be:"
|
||||
+ "\n empty - for default config"
|
||||
+ "\n path to '.json' file"
|
||||
+ "\n short name - file will be saved in config directory"
|
||||
)
|
||||
protected String saveConfig = null;
|
||||
|
||||
@JadxConfigExclude
|
||||
@Parameter(names = { "--print-files" }, description = "print files and directories used by jadx (config, cache, temp)")
|
||||
protected boolean printFiles = false;
|
||||
|
||||
@JadxConfigExclude
|
||||
@Parameter(names = { "--version" }, description = "print jadx version")
|
||||
protected boolean printVersion = false;
|
||||
|
||||
@JadxConfigExclude
|
||||
@Parameter(names = { "-h", "--help" }, description = "print this help", help = true)
|
||||
protected boolean printHelp = false;
|
||||
|
||||
@DynamicParameter(names = "-P", description = "Plugin options", hidden = true)
|
||||
protected Map<String, String> pluginOptions = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Obsolete method without config support,
|
||||
* prefer {@link #processArgs(String[], JadxCLIArgs, JadxConfigAdapter)}
|
||||
*/
|
||||
public boolean processArgs(String[] args) {
|
||||
JCommanderWrapper jcw = new JCommanderWrapper(this);
|
||||
return jcw.parse(args) && process(jcw);
|
||||
return processArgs(args, this, null) != null;
|
||||
}
|
||||
|
||||
public static <T extends JadxCLIArgs> @Nullable T processArgs(
|
||||
String[] args, T argsObj, @Nullable JadxConfigAdapter<T> configAdapter) {
|
||||
JCommanderWrapper jcw = new JCommanderWrapper(argsObj);
|
||||
if (!jcw.parse(args)) {
|
||||
return null;
|
||||
}
|
||||
applyArgs(argsObj);
|
||||
|
||||
// process commands and early exit flags
|
||||
if (!argsObj.process(jcw)) {
|
||||
return null;
|
||||
}
|
||||
if (configAdapter != null) {
|
||||
if (argsObj.printFiles) {
|
||||
printFilesAndDirs(configAdapter.getDefaultConfigFileName());
|
||||
return null;
|
||||
}
|
||||
if (!argsObj.config.equalsIgnoreCase("none")) {
|
||||
// load config file and merge with command line args
|
||||
try {
|
||||
configAdapter.useConfigRef(argsObj.config);
|
||||
T configObj = configAdapter.load();
|
||||
if (configObj != null) {
|
||||
jcw.overrideProvided(configObj);
|
||||
argsObj = configObj;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Config load failed, continue with default values", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
// verify result object
|
||||
argsObj.verify();
|
||||
applyArgs(argsObj);
|
||||
|
||||
// save config if requested
|
||||
if (argsObj.saveConfig != null) {
|
||||
saveConfig(argsObj, configAdapter);
|
||||
return null;
|
||||
}
|
||||
return argsObj;
|
||||
}
|
||||
|
||||
private static <T extends JadxCLIArgs> void applyArgs(T argsObj) {
|
||||
// apply log levels
|
||||
LogHelper.initLogLevel(argsObj);
|
||||
LogHelper.applyLogLevels();
|
||||
}
|
||||
|
||||
public boolean process(JCommanderWrapper jcw) {
|
||||
@@ -319,9 +428,7 @@ public class JadxCLIArgs {
|
||||
System.out.println(JadxDecompiler.getVersion());
|
||||
return false;
|
||||
}
|
||||
if (threadsCount <= 0) {
|
||||
throw new JadxArgsValidateException("Threads count must be positive, got: " + threadsCount);
|
||||
}
|
||||
// unknown options added to 'files', run checks
|
||||
for (String fileName : files) {
|
||||
if (fileName.startsWith("-")) {
|
||||
throw new JadxArgsValidateException("Unknown option: " + fileName);
|
||||
@@ -330,6 +437,29 @@ public class JadxCLIArgs {
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void printFilesAndDirs(String defaultConfigFileName) {
|
||||
System.out.println("Files and directories used by jadx:");
|
||||
System.out.println(" - default config file: " + JadxCommonFiles.getConfigDir().resolve(defaultConfigFileName).toAbsolutePath());
|
||||
System.out.println(" - config directory: " + JadxCommonFiles.getConfigDir().toAbsolutePath());
|
||||
System.out.println(" - cache directory: " + JadxCommonFiles.getCacheDir().toAbsolutePath());
|
||||
System.out.println(" - temp directory: " + JadxTempFiles.getTempRootDir().getParent().toAbsolutePath());
|
||||
}
|
||||
|
||||
public void verify() {
|
||||
if (threadsCount <= 0) {
|
||||
throw new JadxArgsValidateException("Threads count must be positive, got: " + threadsCount);
|
||||
}
|
||||
}
|
||||
|
||||
private static <T extends JadxCLIArgs> void saveConfig(T argsObj, @Nullable JadxConfigAdapter<T> configAdapter) {
|
||||
if (configAdapter == null) {
|
||||
throw new JadxRuntimeException("Config adapter set to null, can't save config");
|
||||
}
|
||||
configAdapter.useConfigRef(argsObj.saveConfig);
|
||||
configAdapter.save(argsObj);
|
||||
System.out.println("Config saved to " + configAdapter.getConfigPath().toAbsolutePath());
|
||||
}
|
||||
|
||||
public JadxArgs toJadxArgs() {
|
||||
JadxArgs args = new JadxArgs();
|
||||
args.setInputFiles(files.stream().map(FileUtils::toFile).collect(Collectors.toList()));
|
||||
@@ -380,16 +510,23 @@ public class JadxCLIArgs {
|
||||
args.setAllowInlineKotlinLambda(allowInlineKotlinLambda);
|
||||
args.setExtractFinally(extractFinally);
|
||||
args.setRestoreSwitchOverString(restoreSwitchOverString);
|
||||
args.setRenameFlags(renameFlags);
|
||||
args.setRenameFlags(buildEnumSetForRenameFlags());
|
||||
args.setFsCaseSensitive(fsCaseSensitive);
|
||||
args.setCommentsLevel(commentsLevel);
|
||||
args.setIntegerFormat(integerFormat);
|
||||
args.setTypeUpdatesLimitCount(typeUpdatesLimitCount);
|
||||
args.setUseDxInput(useDx);
|
||||
args.setPluginOptions(pluginOptions);
|
||||
args.setDisabledPlugins(Arrays.stream(disablePlugins.split(",")).map(String::trim).collect(Collectors.toSet()));
|
||||
return args;
|
||||
}
|
||||
|
||||
private EnumSet<RenameEnum> buildEnumSetForRenameFlags() {
|
||||
EnumSet<RenameEnum> set = EnumSet.noneOf(RenameEnum.class);
|
||||
set.addAll(renameFlags);
|
||||
return set;
|
||||
}
|
||||
|
||||
public List<String> getFiles() {
|
||||
return files;
|
||||
}
|
||||
@@ -422,14 +559,26 @@ public class JadxCLIArgs {
|
||||
return skipResources;
|
||||
}
|
||||
|
||||
public void setSkipResources(boolean skipResources) {
|
||||
this.skipResources = skipResources;
|
||||
}
|
||||
|
||||
public boolean isSkipSources() {
|
||||
return skipSources;
|
||||
}
|
||||
|
||||
public void setSkipSources(boolean skipSources) {
|
||||
this.skipSources = skipSources;
|
||||
}
|
||||
|
||||
public int getThreadsCount() {
|
||||
return threadsCount;
|
||||
}
|
||||
|
||||
public void setThreadsCount(int threadsCount) {
|
||||
this.threadsCount = threadsCount;
|
||||
}
|
||||
|
||||
public boolean isFallbackMode() {
|
||||
return fallbackMode;
|
||||
}
|
||||
@@ -438,82 +587,170 @@ public class JadxCLIArgs {
|
||||
return useDx;
|
||||
}
|
||||
|
||||
public void setUseDx(boolean useDx) {
|
||||
this.useDx = useDx;
|
||||
}
|
||||
|
||||
public DecompilationMode getDecompilationMode() {
|
||||
return decompilationMode;
|
||||
}
|
||||
|
||||
public void setDecompilationMode(DecompilationMode decompilationMode) {
|
||||
this.decompilationMode = decompilationMode;
|
||||
}
|
||||
|
||||
public boolean isShowInconsistentCode() {
|
||||
return showInconsistentCode;
|
||||
}
|
||||
|
||||
public void setShowInconsistentCode(boolean showInconsistentCode) {
|
||||
this.showInconsistentCode = showInconsistentCode;
|
||||
}
|
||||
|
||||
public boolean isUseImports() {
|
||||
return useImports;
|
||||
}
|
||||
|
||||
public void setUseImports(boolean useImports) {
|
||||
this.useImports = useImports;
|
||||
}
|
||||
|
||||
public boolean isDebugInfo() {
|
||||
return debugInfo;
|
||||
}
|
||||
|
||||
public void setDebugInfo(boolean debugInfo) {
|
||||
this.debugInfo = debugInfo;
|
||||
}
|
||||
|
||||
public boolean isAddDebugLines() {
|
||||
return addDebugLines;
|
||||
}
|
||||
|
||||
public void setAddDebugLines(boolean addDebugLines) {
|
||||
this.addDebugLines = addDebugLines;
|
||||
}
|
||||
|
||||
public boolean isInlineAnonymousClasses() {
|
||||
return inlineAnonymousClasses;
|
||||
}
|
||||
|
||||
public void setInlineAnonymousClasses(boolean inlineAnonymousClasses) {
|
||||
this.inlineAnonymousClasses = inlineAnonymousClasses;
|
||||
}
|
||||
|
||||
public boolean isInlineMethods() {
|
||||
return inlineMethods;
|
||||
}
|
||||
|
||||
public void setInlineMethods(boolean inlineMethods) {
|
||||
this.inlineMethods = inlineMethods;
|
||||
}
|
||||
|
||||
public boolean isMoveInnerClasses() {
|
||||
return moveInnerClasses;
|
||||
}
|
||||
|
||||
public void setMoveInnerClasses(boolean moveInnerClasses) {
|
||||
this.moveInnerClasses = moveInnerClasses;
|
||||
}
|
||||
|
||||
public boolean isAllowInlineKotlinLambda() {
|
||||
return allowInlineKotlinLambda;
|
||||
}
|
||||
|
||||
public void setAllowInlineKotlinLambda(boolean allowInlineKotlinLambda) {
|
||||
this.allowInlineKotlinLambda = allowInlineKotlinLambda;
|
||||
}
|
||||
|
||||
public boolean isExtractFinally() {
|
||||
return extractFinally;
|
||||
}
|
||||
|
||||
public void setExtractFinally(boolean extractFinally) {
|
||||
this.extractFinally = extractFinally;
|
||||
}
|
||||
|
||||
public boolean isRestoreSwitchOverString() {
|
||||
return restoreSwitchOverString;
|
||||
}
|
||||
|
||||
public void setRestoreSwitchOverString(boolean restoreSwitchOverString) {
|
||||
this.restoreSwitchOverString = restoreSwitchOverString;
|
||||
}
|
||||
|
||||
public Path getUserRenamesMappingsPath() {
|
||||
return userRenamesMappingsPath;
|
||||
}
|
||||
|
||||
public void setUserRenamesMappingsPath(Path userRenamesMappingsPath) {
|
||||
this.userRenamesMappingsPath = userRenamesMappingsPath;
|
||||
}
|
||||
|
||||
public UserRenamesMappingsMode getUserRenamesMappingsMode() {
|
||||
return userRenamesMappingsMode;
|
||||
}
|
||||
|
||||
public void setUserRenamesMappingsMode(UserRenamesMappingsMode userRenamesMappingsMode) {
|
||||
this.userRenamesMappingsMode = userRenamesMappingsMode;
|
||||
}
|
||||
|
||||
public boolean isDeobfuscationOn() {
|
||||
return deobfuscationOn;
|
||||
}
|
||||
|
||||
public void setDeobfuscationOn(boolean deobfuscationOn) {
|
||||
this.deobfuscationOn = deobfuscationOn;
|
||||
}
|
||||
|
||||
public int getDeobfuscationMinLength() {
|
||||
return deobfuscationMinLength;
|
||||
}
|
||||
|
||||
public void setDeobfuscationMinLength(int deobfuscationMinLength) {
|
||||
this.deobfuscationMinLength = deobfuscationMinLength;
|
||||
}
|
||||
|
||||
public int getDeobfuscationMaxLength() {
|
||||
return deobfuscationMaxLength;
|
||||
}
|
||||
|
||||
public void setDeobfuscationMaxLength(int deobfuscationMaxLength) {
|
||||
this.deobfuscationMaxLength = deobfuscationMaxLength;
|
||||
}
|
||||
|
||||
public String getDeobfuscationWhitelistStr() {
|
||||
return deobfuscationWhitelistStr;
|
||||
}
|
||||
|
||||
public void setDeobfuscationWhitelistStr(String deobfuscationWhitelistStr) {
|
||||
this.deobfuscationWhitelistStr = deobfuscationWhitelistStr;
|
||||
}
|
||||
|
||||
public String getGeneratedRenamesMappingFile() {
|
||||
return generatedRenamesMappingFile;
|
||||
}
|
||||
|
||||
public void setGeneratedRenamesMappingFile(String generatedRenamesMappingFile) {
|
||||
this.generatedRenamesMappingFile = generatedRenamesMappingFile;
|
||||
}
|
||||
|
||||
public GeneratedRenamesMappingFileMode getGeneratedRenamesMappingFileMode() {
|
||||
return generatedRenamesMappingFileMode;
|
||||
}
|
||||
|
||||
public void setGeneratedRenamesMappingFileMode(GeneratedRenamesMappingFileMode generatedRenamesMappingFileMode) {
|
||||
this.generatedRenamesMappingFileMode = generatedRenamesMappingFileMode;
|
||||
}
|
||||
|
||||
public int getSourceNameRepeatLimit() {
|
||||
return sourceNameRepeatLimit;
|
||||
}
|
||||
|
||||
public void setSourceNameRepeatLimit(int sourceNameRepeatLimit) {
|
||||
this.sourceNameRepeatLimit = sourceNameRepeatLimit;
|
||||
}
|
||||
|
||||
public UseSourceNameAsClassNameAlias getUseSourceNameAsClassNameAlias() {
|
||||
if (useSourceNameAsClassNameAlias != null) {
|
||||
return useSourceNameAsClassNameAlias;
|
||||
@@ -525,8 +762,8 @@ public class JadxCLIArgs {
|
||||
}
|
||||
}
|
||||
|
||||
public int getSourceNameRepeatLimit() {
|
||||
return sourceNameRepeatLimit;
|
||||
public void setUseSourceNameAsClassNameAlias(UseSourceNameAsClassNameAlias useSourceNameAsClassNameAlias) {
|
||||
this.useSourceNameAsClassNameAlias = useSourceNameAsClassNameAlias;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -537,46 +774,106 @@ public class JadxCLIArgs {
|
||||
return getUseSourceNameAsClassNameAlias().toBoolean();
|
||||
}
|
||||
|
||||
public void setDeobfuscationUseSourceNameAsAlias(Boolean deobfuscationUseSourceNameAsAlias) {
|
||||
this.deobfuscationUseSourceNameAsAlias = deobfuscationUseSourceNameAsAlias;
|
||||
}
|
||||
|
||||
public ResourceNameSource getResourceNameSource() {
|
||||
return resourceNameSource;
|
||||
}
|
||||
|
||||
public void setResourceNameSource(ResourceNameSource resourceNameSource) {
|
||||
this.resourceNameSource = resourceNameSource;
|
||||
}
|
||||
|
||||
public UseKotlinMethodsForVarNames getUseKotlinMethodsForVarNames() {
|
||||
return useKotlinMethodsForVarNames;
|
||||
}
|
||||
|
||||
public void setUseKotlinMethodsForVarNames(UseKotlinMethodsForVarNames useKotlinMethodsForVarNames) {
|
||||
this.useKotlinMethodsForVarNames = useKotlinMethodsForVarNames;
|
||||
}
|
||||
|
||||
public IntegerFormat getIntegerFormat() {
|
||||
return integerFormat;
|
||||
}
|
||||
|
||||
public void setIntegerFormat(IntegerFormat integerFormat) {
|
||||
this.integerFormat = integerFormat;
|
||||
}
|
||||
|
||||
public int getTypeUpdatesLimitCount() {
|
||||
return typeUpdatesLimitCount;
|
||||
}
|
||||
|
||||
public void setTypeUpdatesLimitCount(int typeUpdatesLimitCount) {
|
||||
this.typeUpdatesLimitCount = typeUpdatesLimitCount;
|
||||
}
|
||||
|
||||
public boolean isEscapeUnicode() {
|
||||
return escapeUnicode;
|
||||
}
|
||||
|
||||
public void setEscapeUnicode(boolean escapeUnicode) {
|
||||
this.escapeUnicode = escapeUnicode;
|
||||
}
|
||||
|
||||
public boolean isCfgOutput() {
|
||||
return cfgOutput;
|
||||
}
|
||||
|
||||
public void setCfgOutput(boolean cfgOutput) {
|
||||
this.cfgOutput = cfgOutput;
|
||||
}
|
||||
|
||||
public boolean isRawCfgOutput() {
|
||||
return rawCfgOutput;
|
||||
}
|
||||
|
||||
public void setRawCfgOutput(boolean rawCfgOutput) {
|
||||
this.rawCfgOutput = rawCfgOutput;
|
||||
}
|
||||
|
||||
public CallGraphSaveMode getCallGraphSaveMode() {
|
||||
return callGraphSaveMode;
|
||||
}
|
||||
|
||||
public void setCallGraphSaveMode(CallGraphSaveMode callGraphSaveMode) {
|
||||
this.callGraphSaveMode = callGraphSaveMode;
|
||||
}
|
||||
|
||||
public boolean isReplaceConsts() {
|
||||
return replaceConsts;
|
||||
}
|
||||
|
||||
public void setReplaceConsts(boolean replaceConsts) {
|
||||
this.replaceConsts = replaceConsts;
|
||||
}
|
||||
|
||||
public boolean isRespectBytecodeAccessModifiers() {
|
||||
return respectBytecodeAccessModifiers;
|
||||
}
|
||||
|
||||
public void setRespectBytecodeAccessModifiers(boolean respectBytecodeAccessModifiers) {
|
||||
this.respectBytecodeAccessModifiers = respectBytecodeAccessModifiers;
|
||||
}
|
||||
|
||||
public boolean isExportAsGradleProject() {
|
||||
return exportAsGradleProject;
|
||||
}
|
||||
|
||||
public void setExportAsGradleProject(boolean exportAsGradleProject) {
|
||||
this.exportAsGradleProject = exportAsGradleProject;
|
||||
}
|
||||
|
||||
public boolean isSkipXmlPrettyPrint() {
|
||||
return skipXmlPrettyPrint;
|
||||
}
|
||||
|
||||
public void setSkipXmlPrettyPrint(boolean skipXmlPrettyPrint) {
|
||||
this.skipXmlPrettyPrint = skipXmlPrettyPrint;
|
||||
}
|
||||
|
||||
public boolean isRenameCaseSensitive() {
|
||||
return renameFlags.contains(RenameEnum.CASE);
|
||||
}
|
||||
@@ -593,26 +890,70 @@ public class JadxCLIArgs {
|
||||
return fsCaseSensitive;
|
||||
}
|
||||
|
||||
public void setFsCaseSensitive(boolean fsCaseSensitive) {
|
||||
this.fsCaseSensitive = fsCaseSensitive;
|
||||
}
|
||||
|
||||
public boolean isUseHeadersForDetectResourceExtensions() {
|
||||
return useHeadersForDetectResourceExtensions;
|
||||
}
|
||||
|
||||
public void setUseHeadersForDetectResourceExtensions(boolean useHeadersForDetectResourceExtensions) {
|
||||
this.useHeadersForDetectResourceExtensions = useHeadersForDetectResourceExtensions;
|
||||
}
|
||||
|
||||
public CommentsLevel getCommentsLevel() {
|
||||
return commentsLevel;
|
||||
}
|
||||
|
||||
public void setCommentsLevel(CommentsLevel commentsLevel) {
|
||||
this.commentsLevel = commentsLevel;
|
||||
}
|
||||
|
||||
public LogHelper.LogLevelEnum getLogLevel() {
|
||||
return logLevel;
|
||||
}
|
||||
|
||||
public void setLogLevel(LogHelper.LogLevelEnum logLevel) {
|
||||
this.logLevel = logLevel;
|
||||
}
|
||||
|
||||
public Map<String, String> getPluginOptions() {
|
||||
return pluginOptions;
|
||||
}
|
||||
|
||||
public void setPluginOptions(Map<String, String> pluginOptions) {
|
||||
this.pluginOptions = pluginOptions;
|
||||
}
|
||||
|
||||
public String getDisablePlugins() {
|
||||
return disablePlugins;
|
||||
}
|
||||
|
||||
public void setDisablePlugins(String disablePlugins) {
|
||||
this.disablePlugins = disablePlugins;
|
||||
}
|
||||
|
||||
public void setExportGradleType(@Nullable ExportGradleType exportGradleType) {
|
||||
this.exportGradleType = exportGradleType;
|
||||
}
|
||||
|
||||
public void setOutputFormat(String outputFormat) {
|
||||
this.outputFormat = outputFormat;
|
||||
}
|
||||
|
||||
public Set<RenameEnum> getRenameFlags() {
|
||||
return renameFlags;
|
||||
}
|
||||
|
||||
public void setRenameFlags(Set<RenameEnum> renameFlags) {
|
||||
this.renameFlags = renameFlags;
|
||||
}
|
||||
|
||||
public String getConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
static class RenameConverter implements IStringConverter<Set<RenameEnum>> {
|
||||
private final String paramName;
|
||||
|
||||
@@ -696,6 +1037,18 @@ public class JadxCLIArgs {
|
||||
}
|
||||
}
|
||||
|
||||
public enum CallGraphSaveMode {
|
||||
NONE,
|
||||
DOT,
|
||||
JSON,
|
||||
}
|
||||
|
||||
public static class CallGraphSaveModeConverter extends BaseEnumConverter<CallGraphSaveMode> {
|
||||
public CallGraphSaveModeConverter() {
|
||||
super(CallGraphSaveMode::valueOf, CallGraphSaveMode::values);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract static class BaseEnumConverter<E extends Enum<E>> implements IStringConverter<E> {
|
||||
private final Function<String, E> parse;
|
||||
private final Supplier<E[]> values;
|
||||
|
||||
@@ -43,10 +43,9 @@ public class LogHelper {
|
||||
return null;
|
||||
}
|
||||
if (args.quiet) {
|
||||
return LogLevelEnum.QUIET;
|
||||
}
|
||||
if (args.verbose) {
|
||||
return LogLevelEnum.DEBUG;
|
||||
args.logLevel = LogLevelEnum.QUIET;
|
||||
} else if (args.verbose) {
|
||||
args.logLevel = LogLevelEnum.DEBUG;
|
||||
}
|
||||
return args.logLevel;
|
||||
}
|
||||
@@ -56,20 +55,7 @@ public class LogHelper {
|
||||
applyLogLevel(logLevelValue);
|
||||
}
|
||||
|
||||
public static void setLogLevelsForLoadingStage() {
|
||||
if (logLevelValue == null) {
|
||||
return;
|
||||
}
|
||||
if (logLevelValue == LogLevelEnum.PROGRESS) {
|
||||
// show load errors
|
||||
LogHelper.applyLogLevel(LogLevelEnum.ERROR);
|
||||
fixForShowProgress();
|
||||
return;
|
||||
}
|
||||
applyLogLevel(logLevelValue);
|
||||
}
|
||||
|
||||
public static void setLogLevelsForDecompileStage() {
|
||||
public static void applyLogLevels() {
|
||||
if (logLevelValue == null) {
|
||||
return;
|
||||
}
|
||||
@@ -86,6 +72,9 @@ public class LogHelper {
|
||||
setLevelForClass(JadxCLI.class, Level.INFO);
|
||||
setLevelForClass(JadxDecompiler.class, Level.INFO);
|
||||
setLevelForClass(SingleClassMode.class, Level.INFO);
|
||||
|
||||
// show warnings and errors from input plugins
|
||||
setLevelForPackage("jadx.plugins.input", Level.WARN);
|
||||
}
|
||||
|
||||
private static void applyLogLevel(@NotNull LogLevelEnum logLevel) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import jadx.cli.LogHelper;
|
||||
import jadx.core.utils.StringUtils;
|
||||
import jadx.plugins.tools.JadxPluginsList;
|
||||
import jadx.plugins.tools.JadxPluginsTools;
|
||||
import jadx.plugins.tools.data.JadxPluginListEntry;
|
||||
import jadx.plugins.tools.data.JadxPluginMetadata;
|
||||
import jadx.plugins.tools.data.JadxPluginUpdate;
|
||||
|
||||
@@ -116,9 +117,9 @@ public class CommandPlugins implements ICommand {
|
||||
return;
|
||||
}
|
||||
if (available) {
|
||||
List<JadxPluginMetadata> availableList = JadxPluginsList.getInstance().get();
|
||||
List<JadxPluginListEntry> availableList = JadxPluginsList.getInstance().get();
|
||||
System.out.println("Available plugins: " + availableList.size());
|
||||
for (JadxPluginMetadata plugin : availableList) {
|
||||
for (JadxPluginListEntry plugin : availableList) {
|
||||
System.out.println(" - " + plugin.getName() + ": " + plugin.getDescription()
|
||||
+ " (" + plugin.getLocationId() + ")");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package jadx.cli.config;
|
||||
|
||||
/**
|
||||
* Marker interface for jadx config objects
|
||||
*/
|
||||
public interface IJadxConfig {
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package jadx.cli.config;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import com.google.gson.ExclusionStrategy;
|
||||
import com.google.gson.FieldAttributes;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
|
||||
import jadx.commons.app.JadxCommonFiles;
|
||||
import jadx.core.utils.GsonUtils;
|
||||
import jadx.core.utils.exceptions.JadxArgsValidateException;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
|
||||
public class JadxConfigAdapter<T extends IJadxConfig> {
|
||||
private static final ExclusionStrategy GSON_EXCLUSION_STRATEGY = new ExclusionStrategy() {
|
||||
@Override
|
||||
public boolean shouldSkipField(FieldAttributes f) {
|
||||
return f.getAnnotation(JadxConfigExclude.class) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldSkipClass(Class<?> clazz) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
private final Class<T> configCls;
|
||||
private final String defaultConfigFileName;
|
||||
private final Gson gson;
|
||||
|
||||
private Path configPath;
|
||||
|
||||
public JadxConfigAdapter(Class<T> configCls, String defaultConfigName) {
|
||||
this(configCls, defaultConfigName, gsonBuilder -> {
|
||||
});
|
||||
}
|
||||
|
||||
public JadxConfigAdapter(Class<T> configCls, String defaultConfigName, Consumer<GsonBuilder> applyGsonOptions) {
|
||||
this.configCls = configCls;
|
||||
this.defaultConfigFileName = defaultConfigName + ".json";
|
||||
GsonBuilder gsonBuilder = GsonUtils.defaultGsonBuilder();
|
||||
gsonBuilder.setExclusionStrategies(GSON_EXCLUSION_STRATEGY);
|
||||
applyGsonOptions.accept(gsonBuilder);
|
||||
this.gson = gsonBuilder.create();
|
||||
}
|
||||
|
||||
public void useConfigRef(String configRef) {
|
||||
this.configPath = resolveConfigRef(configRef);
|
||||
}
|
||||
|
||||
public Path getConfigPath() {
|
||||
return configPath;
|
||||
}
|
||||
|
||||
public String getDefaultConfigFileName() {
|
||||
return defaultConfigFileName;
|
||||
}
|
||||
|
||||
public @Nullable T load() {
|
||||
if (!Files.isRegularFile(configPath)) {
|
||||
// file not found
|
||||
return null;
|
||||
}
|
||||
try (JsonReader reader = gson.newJsonReader(Files.newBufferedReader(configPath))) {
|
||||
return gson.fromJson(reader, configCls);
|
||||
} catch (Exception e) {
|
||||
throw new JadxRuntimeException("Failed to load config file: " + configPath, e);
|
||||
}
|
||||
}
|
||||
|
||||
public void save(T configObject) {
|
||||
try {
|
||||
String jsonStr = gson.toJson(configObject, configCls);
|
||||
// don't use stream writer here because serialization errors will corrupt config
|
||||
Files.writeString(configPath, jsonStr);
|
||||
} catch (Exception e) {
|
||||
throw new JadxRuntimeException("Failed to save config file: " + configPath, e);
|
||||
}
|
||||
}
|
||||
|
||||
public String objectToJsonString(T configObject) {
|
||||
return gson.toJson(configObject, configCls);
|
||||
}
|
||||
|
||||
public T jsonStringToObject(String jsonStr) {
|
||||
return gson.fromJson(jsonStr, configCls);
|
||||
}
|
||||
|
||||
private Path resolveConfigRef(String configRef) {
|
||||
if (configRef == null || configRef.isEmpty()) {
|
||||
// use default config file
|
||||
return JadxCommonFiles.getConfigDir().resolve(defaultConfigFileName);
|
||||
}
|
||||
if (configRef.contains("/") || configRef.contains("\\")) {
|
||||
if (!configRef.toLowerCase().endsWith(".json")) {
|
||||
throw new JadxArgsValidateException("Config file extension should be '.json'");
|
||||
}
|
||||
return Path.of(configRef);
|
||||
}
|
||||
// treat as a short name
|
||||
return JadxCommonFiles.getConfigDir().resolve(configRef + ".json");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package jadx.cli.config;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.FIELD)
|
||||
public @interface JadxConfigExclude {
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
<!-- jadx-gui -->
|
||||
<logger name="com.pinterest.ktlint" level="INFO"/>
|
||||
<logger name="guru.nidi.graphviz" level="WARN"/>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
|
||||
@@ -73,4 +73,12 @@ public class TestInput extends BaseCliIntegrationTest {
|
||||
path -> path.getFileName().toString().equalsIgnoreCase("AndroidManifest.xml"));
|
||||
assertThat(files).isNotEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoRenameForDefPkg() throws Exception {
|
||||
int result = execJadxCli(buildArgs(List.of("--rename-flags", "none"), "samples/defpkg.smali"));
|
||||
assertThat(result).isEqualTo(0);
|
||||
List<Path> files = collectJavaFilesInDir(outputDir);
|
||||
assertThat(files).hasSize(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
.class public LA;
|
||||
.super Ljava/lang/Object;
|
||||
@@ -0,0 +1,29 @@
|
||||
## jadx analysis
|
||||
|
||||
Various utilities for analyze and process code and related information.
|
||||
|
||||
|
||||
### Call graph
|
||||
|
||||
Full app code usage/call graph.
|
||||
Usage:
|
||||
```java
|
||||
JadxArgs args = new JadxArgs();
|
||||
args.addInputFile(new File("input.apk"));
|
||||
try (JadxDecompiler jadx = new JadxDecompiler(args)) {
|
||||
jadx.load();
|
||||
|
||||
ICallGraph callGraph = JadxCallGraph.builder(jadx)
|
||||
.includePackages("com.example") // filter nodes by package
|
||||
.resolvedOnly(true) // add nodes only from app (exclude framework/lib calls)
|
||||
.build();
|
||||
|
||||
for (ICallGraphEdge edge : callGraph.edges()) {
|
||||
if (edge.isResolved()) {
|
||||
System.out.printf("Edge from '%s' to '%s'%n", edge.from(), edge.to());
|
||||
}
|
||||
}
|
||||
callGraph.writeDot(Path.of("test.dot")); // export to '.dot'
|
||||
callGraph.writeJson(Path.of("test.json")); // export to JSON
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,12 @@
|
||||
plugins {
|
||||
id("jadx-library")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":jadx-core"))
|
||||
|
||||
implementation("com.google.code.gson:gson:2.14.0")
|
||||
|
||||
testRuntimeOnly(project(":jadx-plugins:jadx-dex-input"))
|
||||
testRuntimeOnly(project(":jadx-plugins:jadx-smali-input"))
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package jadx.analysis.callgraph;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import jadx.analysis.callgraph.api.ICallGraph;
|
||||
import jadx.analysis.callgraph.api.ICallGraphEdge;
|
||||
import jadx.api.JadxArgs;
|
||||
|
||||
class CallGraph implements ICallGraph {
|
||||
|
||||
private final JadxArgs args;
|
||||
private final List<ICallGraphEdge> edges;
|
||||
|
||||
public CallGraph(JadxArgs args, List<ICallGraphEdge> edges) {
|
||||
this.args = args;
|
||||
this.edges = edges;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ICallGraphEdge> edges() {
|
||||
return edges;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeDot(Path path) {
|
||||
new CallGraphExportDot(args, this).writeTo(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeJson(Path path) {
|
||||
new CallGraphExportJson(this).writeTo(path);
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package jadx.analysis.callgraph;
|
||||
|
||||
import jadx.core.dex.attributes.AttrNode;
|
||||
|
||||
class CallGraphAttrNode extends AttrNode {
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package jadx.analysis.callgraph;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.analysis.callgraph.api.ICallGraph;
|
||||
import jadx.analysis.callgraph.api.ICallGraphBuilder;
|
||||
import jadx.analysis.callgraph.api.ICallGraphEdge;
|
||||
import jadx.api.JadxDecompiler;
|
||||
import jadx.core.dex.info.ClassInfo;
|
||||
import jadx.core.dex.info.MethodInfo;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
|
||||
class CallGraphBuilder implements ICallGraphBuilder {
|
||||
private final JadxDecompiler decompiler;
|
||||
private boolean resolvedOnly = false;
|
||||
private @Nullable String pkgFilter;
|
||||
|
||||
public CallGraphBuilder(JadxDecompiler decompiler) {
|
||||
this.decompiler = decompiler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ICallGraphBuilder resolvedOnly(boolean resolved) {
|
||||
this.resolvedOnly = resolved;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ICallGraphBuilder includePackages(String pkgFilter) {
|
||||
this.pkgFilter = pkgFilter.endsWith(".") ? pkgFilter : pkgFilter + '.';
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ICallGraph build() {
|
||||
return new CallGraph(decompiler.getArgs(), collectEdges());
|
||||
}
|
||||
|
||||
private List<ICallGraphEdge> collectEdges() {
|
||||
AtomicInteger nodeId = new AtomicInteger();
|
||||
Map<MethodInfo, CallGraphNode> nodes = new HashMap<>();
|
||||
List<ICallGraphEdge> edges = new ArrayList<>();
|
||||
|
||||
for (ClassNode cls : decompiler.getRoot().getClasses(true)) {
|
||||
if (ignorePkg(cls.getClassInfo())) {
|
||||
continue;
|
||||
}
|
||||
for (MethodNode mth : cls.getMethods()) {
|
||||
CallGraphNode thisNode = getCallGraphNode(mth, nodes, nodeId);
|
||||
for (MethodNode use : mth.getUseIn()) {
|
||||
if (ignorePkg(use.getDeclaringClass().getClassInfo())) {
|
||||
continue;
|
||||
}
|
||||
CallGraphNode useInNode = getCallGraphNode(use, nodes, nodeId);
|
||||
edges.add(new CallGraphEdge(useInNode, thisNode));
|
||||
}
|
||||
if (!resolvedOnly) {
|
||||
for (MethodInfo used : mth.getUnresolvedUsed()) {
|
||||
if (ignorePkg(used.getDeclClass())) {
|
||||
continue;
|
||||
}
|
||||
CallGraphNode usedNode = getCallGraphNode(used, nodes, nodeId);
|
||||
edges.add(new CallGraphEdge(thisNode, usedNode));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return edges;
|
||||
}
|
||||
|
||||
private boolean ignorePkg(ClassInfo cls) {
|
||||
if (pkgFilter == null) {
|
||||
return false;
|
||||
}
|
||||
return !cls.getFullName().startsWith(pkgFilter);
|
||||
}
|
||||
|
||||
private static CallGraphNode getCallGraphNode(MethodNode mth, Map<MethodInfo, CallGraphNode> nodes, AtomicInteger nodeId) {
|
||||
return nodes.computeIfAbsent(mth.getMethodInfo(), i -> new CallGraphNode(nodeId.incrementAndGet(), mth));
|
||||
}
|
||||
|
||||
private static CallGraphNode getCallGraphNode(MethodInfo mth, Map<MethodInfo, CallGraphNode> nodes, AtomicInteger nodeId) {
|
||||
return nodes.computeIfAbsent(mth, i -> new CallGraphNode(nodeId.incrementAndGet(), mth));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package jadx.analysis.callgraph;
|
||||
|
||||
import jadx.analysis.callgraph.api.ICallGraphEdge;
|
||||
import jadx.analysis.callgraph.api.ICallGraphNode;
|
||||
import jadx.core.dex.attributes.IAttributeNode;
|
||||
|
||||
class CallGraphEdge implements ICallGraphEdge {
|
||||
private final ICallGraphNode from;
|
||||
private final ICallGraphNode to;
|
||||
private final CallGraphAttrNode attrNode = new CallGraphAttrNode();
|
||||
|
||||
public CallGraphEdge(ICallGraphNode from, ICallGraphNode to) {
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ICallGraphNode from() {
|
||||
return from;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ICallGraphNode to() {
|
||||
return to;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isResolved() {
|
||||
return to.isResolved();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IAttributeNode attributes() {
|
||||
return attrNode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CallGraphEdge{from=" + from + ", to=" + to + '}';
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package jadx.analysis.callgraph;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import jadx.analysis.callgraph.api.ICallGraph;
|
||||
import jadx.analysis.callgraph.api.ICallGraphEdge;
|
||||
import jadx.analysis.callgraph.api.ICallGraphNode;
|
||||
import jadx.api.ICodeWriter;
|
||||
import jadx.api.JadxArgs;
|
||||
import jadx.api.impl.SimpleCodeWriter;
|
||||
import jadx.core.utils.DotGraphUtils;
|
||||
import jadx.core.utils.files.FileUtils;
|
||||
|
||||
import static java.nio.file.StandardOpenOption.CREATE;
|
||||
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
|
||||
import static java.nio.file.StandardOpenOption.WRITE;
|
||||
|
||||
public class CallGraphExportDot {
|
||||
private final JadxArgs args;
|
||||
private final ICallGraph callGraph;
|
||||
|
||||
public CallGraphExportDot(JadxArgs args, ICallGraph callGraph) {
|
||||
this.args = args;
|
||||
this.callGraph = callGraph;
|
||||
}
|
||||
|
||||
public void writeTo(Path path) {
|
||||
try {
|
||||
FileUtils.makeDirsForFile(path);
|
||||
Files.writeString(path, writeToString(), StandardCharsets.UTF_8,
|
||||
WRITE, TRUNCATE_EXISTING, CREATE);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to save JSON file: " + path, e);
|
||||
}
|
||||
}
|
||||
|
||||
public String writeToString() {
|
||||
// collect nodes
|
||||
Map<Integer, Node> nodeMap = new HashMap<>();
|
||||
for (ICallGraphEdge edge : callGraph.edges()) {
|
||||
addNode(edge.from(), nodeMap);
|
||||
addNode(edge.to(), nodeMap);
|
||||
}
|
||||
List<Node> nodes = new ArrayList<>(nodeMap.values());
|
||||
nodes.sort(Comparator.comparingInt(o -> o.id));
|
||||
|
||||
SimpleCodeWriter cw = new SimpleCodeWriter(args);
|
||||
cw.add("digraph CallGraph {");
|
||||
for (Node node : nodes) {
|
||||
cw.startLine();
|
||||
addNodeName(cw, node.id);
|
||||
cw.add("[shape=record,label=\"{");
|
||||
cw.add(DotGraphUtils.escape(node.method));
|
||||
cw.add("}\"];");
|
||||
}
|
||||
for (ICallGraphEdge edge : callGraph.edges()) {
|
||||
cw.startLine();
|
||||
addNodeName(cw, edge.from().getId());
|
||||
cw.add(" -> ");
|
||||
addNodeName(cw, edge.to().getId());
|
||||
cw.add(';');
|
||||
}
|
||||
cw.startLine('}');
|
||||
return cw.getCodeStr();
|
||||
}
|
||||
|
||||
private void addNodeName(ICodeWriter cw, int id) {
|
||||
cw.add('N').add(Integer.toString(id));
|
||||
}
|
||||
|
||||
private void addNode(ICallGraphNode cgNode, Map<Integer, Node> nodeMap) {
|
||||
nodeMap.computeIfAbsent(cgNode.getId(), id -> {
|
||||
Node node = new Node();
|
||||
node.id = id;
|
||||
node.method = cgNode.getMethodInfo().getRawFullId();
|
||||
return node;
|
||||
});
|
||||
}
|
||||
|
||||
static final class Node {
|
||||
int id;
|
||||
String method;
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
package jadx.analysis.callgraph;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.Strictness;
|
||||
|
||||
import jadx.analysis.callgraph.api.ICallGraph;
|
||||
import jadx.analysis.callgraph.api.ICallGraphEdge;
|
||||
import jadx.analysis.callgraph.api.ICallGraphNode;
|
||||
import jadx.core.utils.files.FileUtils;
|
||||
|
||||
import static java.nio.file.StandardOpenOption.CREATE;
|
||||
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
|
||||
import static java.nio.file.StandardOpenOption.WRITE;
|
||||
|
||||
public class CallGraphExportJson {
|
||||
private final ICallGraph callGraph;
|
||||
private final Gson gson;
|
||||
|
||||
public CallGraphExportJson(ICallGraph callGraph) {
|
||||
this.callGraph = callGraph;
|
||||
this.gson = new GsonBuilder()
|
||||
.disableJdkUnsafe()
|
||||
.disableInnerClassSerialization()
|
||||
.setStrictness(Strictness.STRICT)
|
||||
// .setPrettyPrinting() // TODO: add option for pretty print?
|
||||
.create();
|
||||
}
|
||||
|
||||
public void writeTo(Path path) {
|
||||
try {
|
||||
FileUtils.makeDirsForFile(path);
|
||||
Files.writeString(path, writeToString(), StandardCharsets.UTF_8,
|
||||
WRITE, TRUNCATE_EXISTING, CREATE);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to save JSON file: " + path, e);
|
||||
}
|
||||
}
|
||||
|
||||
public String writeToString() {
|
||||
List<Edge> edges = new ArrayList<>();
|
||||
Map<Integer, Node> nodeMap = new HashMap<>();
|
||||
for (ICallGraphEdge edge : callGraph.edges()) {
|
||||
ICallGraphNode from = edge.from();
|
||||
ICallGraphNode to = edge.to();
|
||||
addNode(from, nodeMap);
|
||||
addNode(to, nodeMap);
|
||||
Edge jsonEdge = new Edge();
|
||||
jsonEdge.from = from.getId();
|
||||
jsonEdge.to = to.getId();
|
||||
jsonEdge.resolved = edge.isResolved();
|
||||
edges.add(jsonEdge);
|
||||
}
|
||||
List<Node> nodes = new ArrayList<>(nodeMap.values());
|
||||
nodes.sort(Comparator.comparingInt(o -> o.id));
|
||||
|
||||
RootNode rootNode = new RootNode();
|
||||
rootNode.nodes = nodes;
|
||||
rootNode.edges = edges;
|
||||
return gson.toJson(rootNode);
|
||||
}
|
||||
|
||||
private void addNode(ICallGraphNode cgNode, Map<Integer, Node> nodeMap) {
|
||||
nodeMap.computeIfAbsent(cgNode.getId(), id -> {
|
||||
Node node = new Node();
|
||||
node.id = id;
|
||||
node.method = cgNode.getMethodInfo().getRawFullId();
|
||||
return node;
|
||||
});
|
||||
}
|
||||
|
||||
static final class RootNode {
|
||||
List<Node> nodes;
|
||||
List<Edge> edges;
|
||||
}
|
||||
|
||||
static final class Node {
|
||||
int id;
|
||||
String method;
|
||||
}
|
||||
|
||||
static final class Edge {
|
||||
int from;
|
||||
int to;
|
||||
boolean resolved;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package jadx.analysis.callgraph;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.analysis.callgraph.api.ICallGraphNode;
|
||||
import jadx.core.dex.attributes.IAttributeNode;
|
||||
import jadx.core.dex.info.MethodInfo;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
|
||||
class CallGraphNode implements ICallGraphNode {
|
||||
private final int id;
|
||||
private final MethodInfo mthInfo;
|
||||
private final @Nullable MethodNode mthNode;
|
||||
private final CallGraphAttrNode attrNode;
|
||||
|
||||
public CallGraphNode(int id, MethodInfo mthInfo) {
|
||||
this(id, mthInfo, null);
|
||||
}
|
||||
|
||||
public CallGraphNode(int id, MethodNode mthNode) {
|
||||
this(id, mthNode.getMethodInfo(), mthNode);
|
||||
}
|
||||
|
||||
public CallGraphNode(int id, MethodInfo mthInfo, @Nullable MethodNode mthNode) {
|
||||
this.id = id;
|
||||
this.mthInfo = mthInfo;
|
||||
this.mthNode = mthNode;
|
||||
this.attrNode = new CallGraphAttrNode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MethodInfo getMethodInfo() {
|
||||
return mthInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable MethodNode getMethodNode() {
|
||||
return mthNode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isResolved() {
|
||||
return mthNode != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IAttributeNode attributes() {
|
||||
return attrNode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return mthInfo.getFullId();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package jadx.analysis.callgraph;
|
||||
|
||||
import jadx.analysis.callgraph.api.ICallGraphBuilder;
|
||||
import jadx.api.JadxDecompiler;
|
||||
|
||||
public class JadxCallGraph {
|
||||
|
||||
public static ICallGraphBuilder builder(JadxDecompiler decompiler) {
|
||||
return new CallGraphBuilder(decompiler);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package jadx.analysis.callgraph.api;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
public interface ICallGraph {
|
||||
|
||||
List<ICallGraphEdge> edges();
|
||||
|
||||
void writeDot(Path path);
|
||||
|
||||
void writeJson(Path path);
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package jadx.analysis.callgraph.api;
|
||||
|
||||
public interface ICallGraphBuilder {
|
||||
|
||||
ICallGraphBuilder includePackages(String pkgFilter);
|
||||
|
||||
ICallGraphBuilder resolvedOnly(boolean resolved);
|
||||
|
||||
ICallGraph build();
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package jadx.analysis.callgraph.api;
|
||||
|
||||
import jadx.core.dex.attributes.IAttributeNode;
|
||||
|
||||
public interface ICallGraphEdge {
|
||||
|
||||
ICallGraphNode from();
|
||||
|
||||
ICallGraphNode to();
|
||||
|
||||
boolean isResolved();
|
||||
|
||||
IAttributeNode attributes();
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package jadx.analysis.callgraph.api;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.core.dex.attributes.IAttributeNode;
|
||||
import jadx.core.dex.info.MethodInfo;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
|
||||
public interface ICallGraphNode {
|
||||
|
||||
int getId();
|
||||
|
||||
MethodInfo getMethodInfo();
|
||||
|
||||
@Nullable
|
||||
MethodNode getMethodNode();
|
||||
|
||||
boolean isResolved();
|
||||
|
||||
IAttributeNode attributes();
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package jadx.analysis.callgraph.test;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import jadx.analysis.callgraph.CallGraphExportDot;
|
||||
import jadx.analysis.callgraph.CallGraphExportJson;
|
||||
import jadx.analysis.callgraph.JadxCallGraph;
|
||||
import jadx.analysis.callgraph.api.ICallGraph;
|
||||
import jadx.analysis.callgraph.api.ICallGraphEdge;
|
||||
import jadx.api.JadxArgs;
|
||||
import jadx.api.JadxDecompiler;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class JadxCallGraphTest {
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
void usageExample() {
|
||||
JadxArgs args = new JadxArgs();
|
||||
args.addInputFile(new File("input.apk"));
|
||||
try (JadxDecompiler jadx = new JadxDecompiler(args)) {
|
||||
jadx.load();
|
||||
|
||||
ICallGraph callGraph = JadxCallGraph.builder(jadx)
|
||||
.includePackages("com.example")
|
||||
.resolvedOnly(false)
|
||||
.build();
|
||||
|
||||
for (ICallGraphEdge edge : callGraph.edges()) {
|
||||
if (edge.isResolved()) {
|
||||
System.out.printf("Edge from '%s' to '%s'%n", edge.from(), edge.to());
|
||||
}
|
||||
}
|
||||
callGraph.writeDot(Path.of("test.dot"));
|
||||
callGraph.writeJson(Path.of("test.json"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void simpleTest() {
|
||||
JadxArgs args = new JadxArgs();
|
||||
args.addInputFile(getSampleFile("simple.smali"));
|
||||
try (JadxDecompiler jadx = new JadxDecompiler(args)) {
|
||||
jadx.load();
|
||||
|
||||
ICallGraph callGraph = JadxCallGraph.builder(jadx)
|
||||
.includePackages("test.pkg")
|
||||
.resolvedOnly(false)
|
||||
.build();
|
||||
|
||||
assertThat(callGraph.edges()).hasSize(1);
|
||||
|
||||
for (ICallGraphEdge edge : callGraph.edges()) {
|
||||
System.out.println("Edge from " + edge.from() + " to " + edge.to());
|
||||
}
|
||||
|
||||
String dotStr = new CallGraphExportDot(jadx.getArgs(), callGraph).writeToString();
|
||||
System.out.println("dot: " + dotStr);
|
||||
|
||||
String jsonStr = new CallGraphExportJson(callGraph).writeToString();
|
||||
System.out.println("json: " + jsonStr);
|
||||
|
||||
callGraph.writeDot(tempDir.resolve("test.dot"));
|
||||
callGraph.writeJson(tempDir.resolve("test.json"));
|
||||
}
|
||||
}
|
||||
|
||||
private File getSampleFile(String sampleName) {
|
||||
try {
|
||||
URL resource = getClass().getResource("/samples/" + sampleName);
|
||||
assertThat(resource).describedAs("Sample not found: %s", sampleName).isNotNull();
|
||||
return new File(resource.toURI().toURL().getFile());
|
||||
} catch (MalformedURLException | URISyntaxException e) {
|
||||
throw new RuntimeException("Failed to load sample file: " + sampleName, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
.class Ltest/pkg/HelloWorld;
|
||||
.super Ljava/lang/Object;
|
||||
.source "HelloWorld.java"
|
||||
|
||||
.method public static main([Ljava/lang/String;)V
|
||||
.registers 2
|
||||
|
||||
const-string v0, "Hello, World"
|
||||
invoke-static {p0, v0}, Ltest/pkg/HelloWorld;->hello(Ljava/lang/String;)V
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public static hello(Ljava/lang/String;)V
|
||||
.registers 2
|
||||
|
||||
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
|
||||
invoke-virtual {v0, p0}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
|
||||
return-void
|
||||
.end method
|
||||
@@ -1,8 +1,10 @@
|
||||
package jadx.commons.app;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class JadxCommonEnv {
|
||||
|
||||
public static String get(String varName, String defValue) {
|
||||
public static @Nullable String get(String varName, @Nullable String defValue) {
|
||||
String strValue = System.getenv(varName);
|
||||
return isNullOrEmpty(strValue) ? defValue : strValue;
|
||||
}
|
||||
@@ -23,7 +25,7 @@ public class JadxCommonEnv {
|
||||
return Integer.parseInt(strValue);
|
||||
}
|
||||
|
||||
private static boolean isNullOrEmpty(String value) {
|
||||
private static boolean isNullOrEmpty(@Nullable String value) {
|
||||
return value == null || value.isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ package jadx.commons.app;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.function.Function;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
@@ -30,40 +31,39 @@ public class JadxCommonFiles {
|
||||
|
||||
static {
|
||||
DirsLoader loader = new DirsLoader();
|
||||
loader.init();
|
||||
CONFIG_DIR = loader.getConfigDir();
|
||||
CACHE_DIR = loader.getCacheDir();
|
||||
}
|
||||
|
||||
private static final class DirsLoader {
|
||||
private @Nullable ProjectDirectories dirs;
|
||||
private Path configDir;
|
||||
private Path cacheDir;
|
||||
private final Path configDir;
|
||||
private final Path cacheDir;
|
||||
|
||||
public void init() {
|
||||
DirsLoader() {
|
||||
try {
|
||||
configDir = loadEnvDir("JADX_CONFIG_DIR", pd -> pd.configDir);
|
||||
cacheDir = loadEnvDir("JADX_CACHE_DIR", pd -> pd.cacheDir);
|
||||
AtomicReference<@Nullable ProjectDirectories> pdRef = new AtomicReference<>();
|
||||
configDir = loadEnvDir("JADX_CONFIG_DIR", () -> loadDirs(pdRef).configDir);
|
||||
cacheDir = loadEnvDir("JADX_CACHE_DIR", () -> loadDirs(pdRef).cacheDir);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to init common directories", e);
|
||||
}
|
||||
}
|
||||
|
||||
private Path loadEnvDir(String envVar, Function<ProjectDirectories, String> dirFunc) throws IOException {
|
||||
private static Path loadEnvDir(String envVar, Supplier<String> dirFunc) throws IOException {
|
||||
String envDir = JadxCommonEnv.get(envVar, null);
|
||||
String dirStr;
|
||||
if (envDir != null) {
|
||||
dirStr = envDir;
|
||||
} else {
|
||||
dirStr = dirFunc.apply(loadDirs());
|
||||
dirStr = dirFunc.get();
|
||||
}
|
||||
Path path = Path.of(dirStr).toAbsolutePath();
|
||||
Files.createDirectories(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
private synchronized ProjectDirectories loadDirs() {
|
||||
ProjectDirectories currentDirs = dirs;
|
||||
private static ProjectDirectories loadDirs(AtomicReference<@Nullable ProjectDirectories> pdRef) {
|
||||
ProjectDirectories currentDirs = pdRef.get();
|
||||
if (currentDirs != null) {
|
||||
return currentDirs;
|
||||
}
|
||||
@@ -76,7 +76,7 @@ public class JadxCommonFiles {
|
||||
LOG.debug("Loaded system dirs ({}ms): config: {}, cache: {}",
|
||||
System.currentTimeMillis() - start, loadedDirs.configDir, loadedDirs.cacheDir);
|
||||
}
|
||||
dirs = loadedDirs;
|
||||
pdRef.set(loadedDirs);
|
||||
return loadedDirs;
|
||||
}
|
||||
|
||||
@@ -84,21 +84,22 @@ public class JadxCommonFiles {
|
||||
* Return JNI, Foreign or PowerShell implementation
|
||||
*/
|
||||
private static Windows getWinDirs() {
|
||||
Windows defSup = Windows.getDefaultSupplier().get();
|
||||
if (defSup instanceof WindowsPowerShell) {
|
||||
Windows impl = Windows.getDefaultSupplier().get();
|
||||
if (impl instanceof WindowsPowerShell) {
|
||||
if (JadxSystemInfo.IS_AMD64) {
|
||||
// JNI library compiled for x86-64
|
||||
return new WindowsJni();
|
||||
// JNI library compiled only for x86-64
|
||||
impl = new WindowsJni();
|
||||
}
|
||||
}
|
||||
return defSup;
|
||||
LOG.debug("Using win dirs implementation: {}", impl.getClass().getSimpleName());
|
||||
return impl;
|
||||
}
|
||||
|
||||
public Path getCacheDir() {
|
||||
Path getCacheDir() {
|
||||
return cacheDir;
|
||||
}
|
||||
|
||||
public Path getConfigDir() {
|
||||
Path getConfigDir() {
|
||||
return configDir;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package jadx.commons.app;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class JadxSystemInfo {
|
||||
public static final String JAVA_VM = System.getProperty("java.vm.name", "?");
|
||||
public static final String JAVA_VER = System.getProperty("java.version", "?");
|
||||
@@ -16,7 +17,7 @@ public class JadxSystemInfo {
|
||||
public static final boolean IS_LINUX = !IS_WINDOWS && !IS_MAC;
|
||||
public static final boolean IS_UNIX = !IS_WINDOWS;
|
||||
|
||||
private static final String OS_ARCH_LOWER = OS_NAME.toLowerCase(Locale.ENGLISH);
|
||||
private static final String OS_ARCH_LOWER = OS_ARCH.toLowerCase(Locale.ENGLISH);
|
||||
public static final boolean IS_AMD64 = OS_ARCH_LOWER.equals("amd64");
|
||||
public static final boolean IS_ARM64 = OS_ARCH_LOWER.equals("aarch64");
|
||||
|
||||
|
||||
@@ -18,7 +18,9 @@ public class JadxTempFiles {
|
||||
String jadxTmpDir = System.getenv("JADX_TMP_DIR");
|
||||
Path dir;
|
||||
if (jadxTmpDir != null) {
|
||||
dir = Files.createTempDirectory(Paths.get(jadxTmpDir), JADX_TMP_INSTANCE_PREFIX);
|
||||
Path customTmpRootDir = Paths.get(jadxTmpDir);
|
||||
Files.createDirectories(customTmpRootDir);
|
||||
dir = Files.createTempDirectory(customTmpRootDir, JADX_TMP_INSTANCE_PREFIX);
|
||||
} else {
|
||||
dir = Files.createTempDirectory(JADX_TMP_INSTANCE_PREFIX);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package jadx.zip;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Set;
|
||||
@@ -9,6 +10,7 @@ import java.util.function.Function;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.zip.fallback.FallbackException;
|
||||
import jadx.zip.fallback.FallbackZipParser;
|
||||
import jadx.zip.parser.JadxZipParser;
|
||||
import jadx.zip.security.IJadxZipSecurity;
|
||||
@@ -39,13 +41,15 @@ public class ZipReader {
|
||||
|
||||
@SuppressWarnings("resource")
|
||||
public ZipContent open(File zipFile) throws IOException {
|
||||
if (!zipFile.exists()) {
|
||||
throw new FileNotFoundException(zipFile.getAbsolutePath());
|
||||
}
|
||||
try {
|
||||
JadxZipParser jadxParser = new JadxZipParser(zipFile, options);
|
||||
IZipParser detectedParser = detectParser(zipFile, jadxParser);
|
||||
if (detectedParser != jadxParser) {
|
||||
jadxParser.close();
|
||||
}
|
||||
return detectedParser.open();
|
||||
} catch (FallbackException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
if (options.getFlags().contains(ZipReaderFlags.DONT_USE_FALLBACK)) {
|
||||
throw new IOException("Failed to open zip: " + zipFile, e);
|
||||
@@ -90,7 +94,7 @@ public class ZipReader {
|
||||
return options;
|
||||
}
|
||||
|
||||
private IZipParser detectParser(File zipFile, JadxZipParser jadxParser) {
|
||||
private IZipParser detectParser(File zipFile, JadxZipParser jadxParser) throws IOException {
|
||||
if (zipFile.getName().endsWith(".apk")
|
||||
|| options.getFlags().contains(ZipReaderFlags.DONT_USE_FALLBACK)) {
|
||||
return jadxParser;
|
||||
@@ -105,7 +109,7 @@ public class ZipReader {
|
||||
return jadxParser;
|
||||
}
|
||||
|
||||
private FallbackZipParser buildFallbackParser(File zipFile) {
|
||||
private FallbackZipParser buildFallbackParser(File zipFile) throws IOException {
|
||||
return new FallbackZipParser(zipFile, options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package jadx.zip.fallback;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class FallbackException extends IOException {
|
||||
public FallbackException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -22,39 +22,45 @@ import jadx.zip.security.IJadxZipSecurity;
|
||||
|
||||
public class FallbackZipParser implements IZipParser {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FallbackZipParser.class);
|
||||
|
||||
private final File file;
|
||||
private final ZipFile zipFile;
|
||||
private final IJadxZipSecurity zipSecurity;
|
||||
private final boolean useLimitedDataStream;
|
||||
|
||||
private ZipFile zipFile;
|
||||
|
||||
public FallbackZipParser(File file, ZipReaderOptions options) {
|
||||
this.file = file;
|
||||
this.zipSecurity = options.getZipSecurity();
|
||||
this.useLimitedDataStream = zipSecurity.useLimitedDataStream();
|
||||
public FallbackZipParser(File file, ZipReaderOptions options) throws FallbackException {
|
||||
try {
|
||||
this.file = file;
|
||||
this.zipFile = new ZipFile(file);
|
||||
this.zipSecurity = options.getZipSecurity();
|
||||
this.useLimitedDataStream = zipSecurity.useLimitedDataStream();
|
||||
} catch (Exception e) {
|
||||
throw new FallbackException("Error opening zip file: " + file.getAbsolutePath(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ZipContent open() throws IOException {
|
||||
zipFile = new ZipFile(file);
|
||||
|
||||
int maxEntriesCount = zipSecurity.getMaxEntriesCount();
|
||||
if (maxEntriesCount == -1) {
|
||||
maxEntriesCount = Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
List<IZipEntry> list = new ArrayList<>();
|
||||
Enumeration<? extends ZipEntry> entries = zipFile.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
FallbackZipEntry zipEntry = new FallbackZipEntry(this, entries.nextElement());
|
||||
if (isValidEntry(zipEntry)) {
|
||||
list.add(zipEntry);
|
||||
if (list.size() > maxEntriesCount) {
|
||||
throw new IllegalStateException("Max entries count limit exceeded: " + list.size());
|
||||
try {
|
||||
int maxEntriesCount = zipSecurity.getMaxEntriesCount();
|
||||
if (maxEntriesCount == -1) {
|
||||
maxEntriesCount = Integer.MAX_VALUE;
|
||||
}
|
||||
List<IZipEntry> list = new ArrayList<>();
|
||||
Enumeration<? extends ZipEntry> entries = zipFile.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
FallbackZipEntry zipEntry = new FallbackZipEntry(this, entries.nextElement());
|
||||
if (isValidEntry(zipEntry)) {
|
||||
list.add(zipEntry);
|
||||
if (list.size() > maxEntriesCount) {
|
||||
throw new IllegalStateException("Max entries count limit exceeded: " + list.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
return new ZipContent(this, list);
|
||||
} catch (Exception e) {
|
||||
throw new FallbackException("Error opening zip file: " + file.getAbsolutePath(), e);
|
||||
}
|
||||
return new ZipContent(this, list);
|
||||
}
|
||||
|
||||
private boolean isValidEntry(IZipEntry zipEntry) {
|
||||
@@ -98,12 +104,8 @@ public class FallbackZipParser implements IZipParser {
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
try {
|
||||
if (zipFile != null) {
|
||||
zipFile.close();
|
||||
}
|
||||
} finally {
|
||||
zipFile = null;
|
||||
if (zipFile != null) {
|
||||
zipFile.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ public class ByteBufferBackedInputStream extends InputStream {
|
||||
this.buf = buf;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
if (!buf.hasRemaining()) {
|
||||
return -1;
|
||||
@@ -19,6 +20,7 @@ public class ByteBufferBackedInputStream extends InputStream {
|
||||
return buf.get() & 0xFF;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("NullableProblems")
|
||||
public int read(byte[] bytes, int off, int len) throws IOException {
|
||||
if (!buf.hasRemaining()) {
|
||||
|
||||
@@ -35,6 +35,7 @@ public final class JadxZipEntry implements IZipEntry {
|
||||
return compressedSize <= uncompressedSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
@@ -49,9 +49,9 @@ public final class JadxZipParser implements IZipParser {
|
||||
private final boolean verify;
|
||||
private final boolean useLimitedDataStream;
|
||||
|
||||
private RandomAccessFile file;
|
||||
private FileChannel fileChannel;
|
||||
private ByteBuffer byteBuffer;
|
||||
private @Nullable RandomAccessFile file;
|
||||
private @Nullable FileChannel fileChannel;
|
||||
private @Nullable ByteBuffer byteBuffer;
|
||||
|
||||
private int endOfCDStart = -2;
|
||||
|
||||
@@ -90,23 +90,25 @@ public final class JadxZipParser implements IZipParser {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("RedundantIfStatement")
|
||||
public boolean canOpen() {
|
||||
try {
|
||||
load();
|
||||
int eocdStart = searchEndOfCDStart();
|
||||
ByteBuffer buf = byteBuffer;
|
||||
ByteBuffer buf = getBuffer();
|
||||
buf.position(eocdStart + 4);
|
||||
int diskNum = readU2(buf);
|
||||
if (diskNum == 0xFFFF) {
|
||||
// Zip64
|
||||
return false;
|
||||
if (diskNum != 0xFFFF) { // Zip64 not supported
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Jadx parser can't open zip file: {}", zipFile, e);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
close();
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Failed to close jadx parser, zip file: {}", zipFile, e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isValidEntry(JadxZipEntry zipEntry) {
|
||||
@@ -117,13 +119,21 @@ public final class JadxZipParser implements IZipParser {
|
||||
return validEntry;
|
||||
}
|
||||
|
||||
private ByteBuffer getBuffer() {
|
||||
ByteBuffer buf = byteBuffer;
|
||||
if (buf == null) {
|
||||
throw new RuntimeException("File not opened: " + zipFile);
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
private void load() throws IOException {
|
||||
if (byteBuffer != null) {
|
||||
// already loaded
|
||||
return;
|
||||
}
|
||||
file = new RandomAccessFile(zipFile, "r");
|
||||
long size = file.length();
|
||||
RandomAccessFile raFile = new RandomAccessFile(zipFile, "r");
|
||||
long size = raFile.length();
|
||||
if (size >= Integer.MAX_VALUE) {
|
||||
throw new IOException("Zip file is too big");
|
||||
}
|
||||
@@ -131,16 +141,16 @@ public final class JadxZipParser implements IZipParser {
|
||||
if (fileLen < 100 * 1024 * 1024) {
|
||||
// load files smaller than 100MB directly into memory
|
||||
byte[] bytes = new byte[fileLen];
|
||||
file.readFully(bytes);
|
||||
byteBuffer = ByteBuffer.wrap(bytes).asReadOnlyBuffer();
|
||||
file.close();
|
||||
file = null;
|
||||
raFile.readFully(bytes);
|
||||
byteBuffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
|
||||
raFile.close();
|
||||
} else {
|
||||
// for big files - use a memory mapped file
|
||||
fileChannel = file.getChannel();
|
||||
file = raFile;
|
||||
fileChannel = raFile.getChannel();
|
||||
byteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
|
||||
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
}
|
||||
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
}
|
||||
|
||||
private List<IZipEntry> searchLocalFileHeaders(int maxEntriesCount) {
|
||||
@@ -165,7 +175,7 @@ public final class JadxZipParser implements IZipParser {
|
||||
if (eocdStart < 0) {
|
||||
throw new RuntimeException("End of central directory not found");
|
||||
}
|
||||
ByteBuffer buf = byteBuffer;
|
||||
ByteBuffer buf = getBuffer();
|
||||
buf.position(eocdStart + 10);
|
||||
int entriesCount = readU2(buf);
|
||||
buf.position(eocdStart + 16);
|
||||
@@ -186,7 +196,7 @@ public final class JadxZipParser implements IZipParser {
|
||||
}
|
||||
|
||||
private JadxZipEntry loadCDEntry() {
|
||||
ByteBuffer buf = byteBuffer;
|
||||
ByteBuffer buf = getBuffer();
|
||||
int start = buf.position();
|
||||
buf.position(start + 28);
|
||||
int fileNameLen = readU2(buf);
|
||||
@@ -207,7 +217,7 @@ public final class JadxZipParser implements IZipParser {
|
||||
}
|
||||
|
||||
private JadxZipEntry fixEntryFromCD(JadxZipEntry entry, int start) {
|
||||
ByteBuffer buf = byteBuffer;
|
||||
ByteBuffer buf = getBuffer();
|
||||
buf.position(start + 10);
|
||||
int comprMethod = readU2(buf);
|
||||
buf.position(start + 20);
|
||||
@@ -237,7 +247,7 @@ public final class JadxZipParser implements IZipParser {
|
||||
}
|
||||
|
||||
private JadxZipEntry loadFileEntry(int start) {
|
||||
ByteBuffer buf = byteBuffer;
|
||||
ByteBuffer buf = getBuffer();
|
||||
buf.position(start + 8);
|
||||
int comprMethod = readU2(buf);
|
||||
buf.position(start + 18);
|
||||
@@ -255,7 +265,7 @@ public final class JadxZipParser implements IZipParser {
|
||||
if (endOfCDStart != -2) {
|
||||
return endOfCDStart;
|
||||
}
|
||||
ByteBuffer buf = byteBuffer;
|
||||
ByteBuffer buf = getBuffer();
|
||||
int pos = buf.limit() - 22;
|
||||
int minPos = Math.max(0, pos - 0xffff);
|
||||
while (true) {
|
||||
@@ -273,7 +283,7 @@ public final class JadxZipParser implements IZipParser {
|
||||
}
|
||||
|
||||
private int searchEntryStart() {
|
||||
ByteBuffer buf = byteBuffer;
|
||||
ByteBuffer buf = getBuffer();
|
||||
while (true) {
|
||||
int start = buf.position();
|
||||
if (start + 4 > buf.limit()) {
|
||||
@@ -297,14 +307,14 @@ public final class JadxZipParser implements IZipParser {
|
||||
InputStream stream;
|
||||
if (entry.getCompressMethod() == 8) {
|
||||
try {
|
||||
stream = ZipDeflate.decompressEntryToStream(byteBuffer, entry);
|
||||
stream = ZipDeflate.decompressEntryToStream(getBuffer(), entry);
|
||||
} catch (Exception e) {
|
||||
entryParseFailed(entry, e);
|
||||
return useFallbackParser(entry).getInputStream();
|
||||
}
|
||||
} else {
|
||||
// treat any other compression methods values as UNCOMPRESSED
|
||||
stream = bufferToStream(byteBuffer, entry.getDataStart(), (int) entry.getUncompressedSize());
|
||||
stream = bufferToStream(getBuffer(), entry.getDataStart(), (int) entry.getUncompressedSize());
|
||||
}
|
||||
if (useLimitedDataStream) {
|
||||
return new LimitedInputStream(stream, entry.getUncompressedSize());
|
||||
@@ -318,14 +328,14 @@ public final class JadxZipParser implements IZipParser {
|
||||
}
|
||||
if (entry.getCompressMethod() == 8) {
|
||||
try {
|
||||
return ZipDeflate.decompressEntryToBytes(byteBuffer, entry);
|
||||
return ZipDeflate.decompressEntryToBytes(getBuffer(), entry);
|
||||
} catch (Exception e) {
|
||||
entryParseFailed(entry, e);
|
||||
return useFallbackParser(entry).getBytes();
|
||||
}
|
||||
}
|
||||
// treat any other compression methods values as UNCOMPRESSED
|
||||
return bufferToBytes(byteBuffer, entry.getDataStart(), (int) entry.getUncompressedSize());
|
||||
return bufferToBytes(getBuffer(), entry.getDataStart(), (int) entry.getUncompressedSize());
|
||||
}
|
||||
|
||||
private static void verifyEntry(JadxZipEntry entry) {
|
||||
@@ -361,7 +371,7 @@ public final class JadxZipParser implements IZipParser {
|
||||
}
|
||||
|
||||
@SuppressWarnings("resource")
|
||||
private ZipContent initFallbackParser() {
|
||||
private synchronized ZipContent initFallbackParser() {
|
||||
if (fallbackZipContent == null) {
|
||||
try {
|
||||
fallbackZipContent = new FallbackZipParser(zipFile, options).open();
|
||||
@@ -378,7 +388,7 @@ public final class JadxZipParser implements IZipParser {
|
||||
}
|
||||
|
||||
private int readFlags(JadxZipEntry entry) {
|
||||
ByteBuffer buf = byteBuffer;
|
||||
ByteBuffer buf = getBuffer();
|
||||
buf.position(entry.getEntryStart() + 6);
|
||||
return readU2(buf);
|
||||
}
|
||||
@@ -407,6 +417,7 @@ public final class JadxZipParser implements IZipParser {
|
||||
return new String(bytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@SuppressWarnings("DataFlowIssue")
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
try {
|
||||
|
||||
@@ -15,6 +15,9 @@ final class ZipDeflate {
|
||||
buf.position(entry.getDataStart());
|
||||
ByteBuffer entryBuf = buf.slice();
|
||||
entryBuf.limit((int) entry.getCompressedSize());
|
||||
if (entry.getUncompressedSize() > Integer.MAX_VALUE) {
|
||||
throw new DataFormatException("Entry too large: " + entry.getUncompressedSize());
|
||||
}
|
||||
byte[] out = new byte[(int) entry.getUncompressedSize()];
|
||||
Inflater inflater = new Inflater(true);
|
||||
inflater.setInput(entryBuf);
|
||||
|
||||
@@ -2,6 +2,8 @@ package jadx.zip.security;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -11,7 +13,7 @@ import jadx.zip.IZipEntry;
|
||||
public class JadxZipSecurity implements IJadxZipSecurity {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(JadxZipSecurity.class);
|
||||
|
||||
private static final File CWD = getCWD();
|
||||
private static final Path CWD = Paths.get(".").toAbsolutePath().normalize();
|
||||
|
||||
/**
|
||||
* The size of uncompressed zip entry shouldn't be bigger of compressed in zipBombDetectionFactor
|
||||
@@ -56,14 +58,17 @@ public class JadxZipSecurity implements IJadxZipSecurity {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Path traversal check as presented on
|
||||
// https://www.heise.de/en/background/Secure-Coding-Best-practices-for-using-Java-NIO-against-path-traversal-9996787.html
|
||||
try {
|
||||
File currentPath = CWD;
|
||||
File canonical = new File(currentPath, entryName).getCanonicalFile();
|
||||
if (isInSubDirectoryInternal(currentPath, canonical)) {
|
||||
Path entryPath = CWD.resolve(entryName).normalize();
|
||||
if (entryPath.startsWith(CWD)) {
|
||||
return true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// check failed
|
||||
LOG.error("Invalid file name or path traversal attack detected: {} - error: {}", entryName, e.getMessage());
|
||||
return false;
|
||||
}
|
||||
LOG.error("Invalid file name or path traversal attack detected: {}", entryName);
|
||||
return false;
|
||||
@@ -121,12 +126,4 @@ public class JadxZipSecurity implements IJadxZipSecurity {
|
||||
this.useLimitedDataStream = useLimitedDataStream;
|
||||
}
|
||||
|
||||
private static File getCWD() {
|
||||
try {
|
||||
return new File(".").getCanonicalFile();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to init current working dir constant", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,15 +6,17 @@ dependencies {
|
||||
api(project(":jadx-plugins:jadx-input-api"))
|
||||
api(project(":jadx-commons:jadx-zip"))
|
||||
|
||||
implementation("com.google.code.gson:gson:2.13.1")
|
||||
implementation("com.google.code.gson:gson:2.14.0")
|
||||
|
||||
testImplementation("org.apache.commons:commons-lang3:3.18.0")
|
||||
testImplementation("org.apache.commons:commons-lang3:3.20.0")
|
||||
|
||||
testImplementation(project(":jadx-plugins:jadx-dex-input"))
|
||||
testRuntimeOnly(project(":jadx-plugins:jadx-smali-input"))
|
||||
testRuntimeOnly(project(":jadx-plugins:jadx-java-convert"))
|
||||
testRuntimeOnly(project(":jadx-plugins:jadx-java-input"))
|
||||
testRuntimeOnly(project(":jadx-plugins:jadx-raung-input"))
|
||||
// 'ClassNotFound' error is raised if set as 'testRuntime'
|
||||
// for the plugins below when running the tests from vscode.
|
||||
testImplementation(project(":jadx-plugins:jadx-smali-input"))
|
||||
testImplementation(project(":jadx-plugins:jadx-java-convert"))
|
||||
testImplementation(project(":jadx-plugins:jadx-java-input"))
|
||||
testImplementation(project(":jadx-plugins:jadx-raung-input"))
|
||||
|
||||
testImplementation("org.eclipse.jdt:ecj") {
|
||||
version {
|
||||
@@ -22,7 +24,7 @@ dependencies {
|
||||
strictly("[3.33, 3.34[") // from 3.34 compiled with Java 17
|
||||
}
|
||||
}
|
||||
testImplementation("tools.profiler:async-profiler:4.0")
|
||||
testImplementation("tools.profiler:async-profiler:4.4")
|
||||
}
|
||||
|
||||
val jadxTestJavaVersion = getTestJavaVersion()
|
||||
@@ -30,7 +32,10 @@ val jadxTestJavaVersion = getTestJavaVersion()
|
||||
fun getTestJavaVersion(): Int? {
|
||||
val envVarName = "JADX_TEST_JAVA_VERSION"
|
||||
val testJavaVer = System.getenv(envVarName)?.toInt() ?: return null
|
||||
val currentJavaVer = java.toolchain.languageVersion.get().asInt()
|
||||
val currentJavaVer =
|
||||
java.toolchain.languageVersion
|
||||
.get()
|
||||
.asInt()
|
||||
if (testJavaVer < currentJavaVer) {
|
||||
throw GradleException("'$envVarName' can't be set to lower version than $currentJavaVer")
|
||||
}
|
||||
@@ -52,4 +57,6 @@ tasks.named<Test>("test") {
|
||||
|
||||
// exclude temp tests
|
||||
exclude("**/tmp/*")
|
||||
|
||||
// maxHeapSize = "4g"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
@@ -96,7 +95,7 @@ public class JadxArgs implements Closeable {
|
||||
/**
|
||||
* Predicate that allows to filter the classes to be process based on their full name
|
||||
*/
|
||||
private Predicate<String> classFilter = null;
|
||||
private @Nullable Predicate<String> classFilter = null;
|
||||
|
||||
/**
|
||||
* Save dependencies for classes accepted by {@code classFilter}
|
||||
@@ -167,6 +166,12 @@ public class JadxArgs implements Closeable {
|
||||
|
||||
private IntegerFormat integerFormat = IntegerFormat.AUTO;
|
||||
|
||||
/**
|
||||
* Maximum updates allowed total in method per one instruction.
|
||||
* Should be more or equal 1, default value is 10.
|
||||
*/
|
||||
private int typeUpdatesLimitCount = 10;
|
||||
|
||||
private boolean useDxInput = false;
|
||||
|
||||
public enum UseKotlinMethodsForVarNames {
|
||||
@@ -196,6 +201,11 @@ public class JadxArgs implements Closeable {
|
||||
*/
|
||||
private boolean runDebugChecks = false;
|
||||
|
||||
/**
|
||||
* Passes to exclude from processing.
|
||||
*/
|
||||
private final List<String> disabledPasses = new ArrayList<>();
|
||||
|
||||
private Map<String, String> pluginOptions = new HashMap<>();
|
||||
|
||||
private Set<String> disabledPlugins = new HashSet<>();
|
||||
@@ -217,7 +227,6 @@ public class JadxArgs implements Closeable {
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
inputFiles = null;
|
||||
if (codeCache != null) {
|
||||
codeCache.close();
|
||||
}
|
||||
@@ -229,9 +238,6 @@ public class JadxArgs implements Closeable {
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to close JadxArgs", e);
|
||||
} finally {
|
||||
codeCache = null;
|
||||
usageInfoCache = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,8 +245,12 @@ public class JadxArgs implements Closeable {
|
||||
return inputFiles;
|
||||
}
|
||||
|
||||
public void addInputFile(File inputFile) {
|
||||
this.inputFiles.add(inputFile);
|
||||
}
|
||||
|
||||
public void setInputFile(File inputFile) {
|
||||
this.inputFiles = Collections.singletonList(inputFile);
|
||||
addInputFile(inputFile);
|
||||
}
|
||||
|
||||
public void setInputFiles(List<File> inputFiles) {
|
||||
@@ -484,7 +494,7 @@ public class JadxArgs implements Closeable {
|
||||
*/
|
||||
@Deprecated
|
||||
public void setUseSourceNameAsClassAlias(boolean useSourceNameAsClassAlias) {
|
||||
final var useSourceNameAsClassNameAlias = UseSourceNameAsClassNameAlias.create(useSourceNameAsClassAlias);
|
||||
var useSourceNameAsClassNameAlias = UseSourceNameAsClassNameAlias.create(useSourceNameAsClassAlias);
|
||||
setUseSourceNameAsClassNameAlias(useSourceNameAsClassNameAlias);
|
||||
}
|
||||
|
||||
@@ -738,6 +748,14 @@ public class JadxArgs implements Closeable {
|
||||
this.integerFormat = format;
|
||||
}
|
||||
|
||||
public int getTypeUpdatesLimitCount() {
|
||||
return typeUpdatesLimitCount;
|
||||
}
|
||||
|
||||
public void setTypeUpdatesLimitCount(int typeUpdatesLimitCount) {
|
||||
this.typeUpdatesLimitCount = Math.max(1, typeUpdatesLimitCount);
|
||||
}
|
||||
|
||||
public boolean isUseDxInput() {
|
||||
return useDxInput;
|
||||
}
|
||||
@@ -786,6 +804,10 @@ public class JadxArgs implements Closeable {
|
||||
this.runDebugChecks = runDebugChecks;
|
||||
}
|
||||
|
||||
public List<String> getDisabledPasses() {
|
||||
return disabledPasses;
|
||||
}
|
||||
|
||||
public Map<String, String> getPluginOptions() {
|
||||
return pluginOptions;
|
||||
}
|
||||
@@ -839,7 +861,7 @@ public class JadxArgs implements Closeable {
|
||||
+ insertDebugLines + extractFinally
|
||||
+ debugInfo + escapeUnicode + replaceConsts + restoreSwitchOverString
|
||||
+ respectBytecodeAccModifiers + fsCaseSensitive + renameFlags
|
||||
+ commentsLevel + useDxInput + integerFormat
|
||||
+ commentsLevel + useDxInput + integerFormat + typeUpdatesLimitCount
|
||||
+ "|" + buildPluginsHash(decompiler);
|
||||
return FileUtils.md5Sum(argStr);
|
||||
}
|
||||
@@ -898,6 +920,7 @@ public class JadxArgs implements Closeable {
|
||||
+ ", cfgOutput=" + cfgOutput
|
||||
+ ", rawCFGOutput=" + rawCFGOutput
|
||||
+ ", useHeadersForDetectResourceExtensions=" + useHeadersForDetectResourceExtensions
|
||||
+ ", typeUpdatesLimitCount=" + typeUpdatesLimitCount
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,11 +138,16 @@ public final class JadxDecompiler implements Closeable {
|
||||
loadFinished();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload passes and plugins without processing classes and inputs
|
||||
*/
|
||||
public void reloadPasses() {
|
||||
LOG.info("reloading (passes only) ...");
|
||||
customPasses.clear();
|
||||
root.resetPasses();
|
||||
events.reset();
|
||||
unloadPlugins();
|
||||
|
||||
loadPlugins();
|
||||
root.mergePasses(customPasses);
|
||||
root.restartVisitors();
|
||||
@@ -435,7 +440,7 @@ public final class JadxDecompiler implements Closeable {
|
||||
return list;
|
||||
}
|
||||
|
||||
public List<JavaClass> getClasses() {
|
||||
public synchronized List<JavaClass> getClasses() {
|
||||
if (root == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
@@ -443,10 +448,7 @@ public final class JadxDecompiler implements Closeable {
|
||||
List<ClassNode> classNodeList = root.getClasses();
|
||||
List<JavaClass> clsList = new ArrayList<>(classNodeList.size());
|
||||
for (ClassNode classNode : classNodeList) {
|
||||
if (classNode.contains(AFlag.DONT_GENERATE)) {
|
||||
continue;
|
||||
}
|
||||
if (!classNode.getClassInfo().isInner()) {
|
||||
if (!classNode.contains(AFlag.DONT_GENERATE) && !classNode.isInner()) {
|
||||
clsList.add(convertClassNode(classNode));
|
||||
}
|
||||
}
|
||||
@@ -546,9 +548,10 @@ public final class JadxDecompiler implements Closeable {
|
||||
return foundPkg;
|
||||
}
|
||||
List<JavaClass> clsList = Utils.collectionMap(pkg.getClasses(), this::convertClassNode);
|
||||
List<JavaClass> clsListNoDup = Utils.collectionMap(pkg.getClassesNoDup(), this::convertClassNode);
|
||||
int subPkgsCount = pkg.getSubPackages().size();
|
||||
List<JavaPackage> subPkgs = subPkgsCount == 0 ? Collections.emptyList() : new ArrayList<>(subPkgsCount);
|
||||
JavaPackage javaPkg = new JavaPackage(pkg, clsList, subPkgs);
|
||||
JavaPackage javaPkg = new JavaPackage(pkg, clsList, clsListNoDup, subPkgs);
|
||||
if (subPkgsCount != 0) {
|
||||
// add subpackages after parent to avoid endless recursion
|
||||
for (PackageNode subPackage : pkg.getSubPackages()) {
|
||||
|
||||
@@ -6,12 +6,11 @@ import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.metadata.ICodeAnnotation;
|
||||
import jadx.api.metadata.ICodeNodeRef;
|
||||
@@ -26,11 +25,9 @@ import jadx.core.dex.nodes.MethodNode;
|
||||
import jadx.core.utils.ListUtils;
|
||||
|
||||
public final class JavaClass implements JavaNode {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(JavaClass.class);
|
||||
|
||||
private final JadxDecompiler decompiler;
|
||||
private final @Nullable JadxDecompiler decompiler;
|
||||
private final ClassNode cls;
|
||||
private final JavaClass parent;
|
||||
private final @Nullable JavaClass parent;
|
||||
|
||||
private List<JavaClass> innerClasses = Collections.emptyList();
|
||||
private List<JavaClass> inlinedClasses = Collections.emptyList();
|
||||
@@ -38,7 +35,7 @@ public final class JavaClass implements JavaNode {
|
||||
private List<JavaMethod> methods = Collections.emptyList();
|
||||
private boolean listsLoaded;
|
||||
|
||||
JavaClass(ClassNode classNode, JadxDecompiler decompiler) {
|
||||
JavaClass(ClassNode classNode, @NotNull JadxDecompiler decompiler) {
|
||||
this.decompiler = decompiler;
|
||||
this.cls = classNode;
|
||||
this.parent = null;
|
||||
@@ -47,7 +44,7 @@ public final class JavaClass implements JavaNode {
|
||||
/**
|
||||
* Inner classes constructor
|
||||
*/
|
||||
JavaClass(ClassNode classNode, JavaClass parent) {
|
||||
JavaClass(ClassNode classNode, @NotNull JavaClass parent) {
|
||||
this.decompiler = null;
|
||||
this.cls = classNode;
|
||||
this.parent = parent;
|
||||
@@ -69,6 +66,21 @@ public final class JavaClass implements JavaNode {
|
||||
load();
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if calling load() would trigger a potentially expensive decompilation operation.
|
||||
*/
|
||||
public boolean loadingWouldRequireDecompilation() {
|
||||
if (listsLoaded) {
|
||||
// lists are already populated, so it's safe regardless of the state of the class itself
|
||||
return false;
|
||||
}
|
||||
if (cls.getState().isProcessComplete()) {
|
||||
// decompilation has already finished
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public synchronized ICodeInfo reload() {
|
||||
listsLoaded = false;
|
||||
return cls.reloadCode();
|
||||
@@ -187,10 +199,10 @@ public final class JavaClass implements JavaNode {
|
||||
if (parent != null) {
|
||||
return parent.getRootDecompiler();
|
||||
}
|
||||
return decompiler;
|
||||
return Objects.requireNonNull(decompiler);
|
||||
}
|
||||
|
||||
public ICodeAnnotation getAnnotationAt(int pos) {
|
||||
public @Nullable ICodeAnnotation getAnnotationAt(int pos) {
|
||||
return getCodeInfo().getCodeMetadata().getAt(pos);
|
||||
}
|
||||
|
||||
@@ -259,7 +271,7 @@ public final class JavaClass implements JavaNode {
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaClass getDeclaringClass() {
|
||||
public @Nullable JavaClass getDeclaringClass() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,21 +5,18 @@ import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.metadata.ICodeAnnotation;
|
||||
import jadx.api.metadata.ICodeNodeRef;
|
||||
import jadx.core.dex.attributes.AType;
|
||||
import jadx.core.dex.attributes.nodes.MethodOverrideAttr;
|
||||
import jadx.core.dex.info.AccessInfo;
|
||||
import jadx.core.dex.info.MethodInfo;
|
||||
import jadx.core.dex.instructions.args.ArgType;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
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;
|
||||
|
||||
@@ -72,6 +69,18 @@ public final class JavaMethod implements JavaNode {
|
||||
return getDeclaringClass().getRootDecompiler().convertNodes(mth.getUseIn());
|
||||
}
|
||||
|
||||
public List<JavaNode> getUsed() {
|
||||
return getDeclaringClass().getRootDecompiler().convertNodes(mth.getUsed());
|
||||
}
|
||||
|
||||
public List<MethodInfo> getUnresolvedUsed() {
|
||||
return mth.getUnresolvedUsed();
|
||||
}
|
||||
|
||||
public boolean callsSelf() {
|
||||
return mth.callsSelf();
|
||||
}
|
||||
|
||||
public List<JavaMethod> getOverrideRelatedMethods() {
|
||||
MethodOverrideAttr ovrdAttr = mth.get(AType.METHOD_OVERRIDE);
|
||||
if (ovrdAttr == null) {
|
||||
|
||||
@@ -15,11 +15,17 @@ import jadx.core.dex.nodes.PackageNode;
|
||||
public final class JavaPackage implements JavaNode, Comparable<JavaPackage> {
|
||||
private final PackageNode pkgNode;
|
||||
private final List<JavaClass> classes;
|
||||
private final List<JavaClass> clsListNoDup;
|
||||
private final List<JavaPackage> subPkgs;
|
||||
|
||||
JavaPackage(PackageNode pkgNode, List<JavaClass> classes, List<JavaPackage> subPkgs) {
|
||||
this(pkgNode, classes, classes, subPkgs);
|
||||
}
|
||||
|
||||
JavaPackage(PackageNode pkgNode, List<JavaClass> classes, List<JavaClass> clsListNoDup, List<JavaPackage> subPkgs) {
|
||||
this.pkgNode = pkgNode;
|
||||
this.classes = classes;
|
||||
this.clsListNoDup = clsListNoDup;
|
||||
this.subPkgs = subPkgs;
|
||||
}
|
||||
|
||||
@@ -49,6 +55,10 @@ public final class JavaPackage implements JavaNode, Comparable<JavaPackage> {
|
||||
return classes;
|
||||
}
|
||||
|
||||
public List<JavaClass> getClassesNoDup() {
|
||||
return clsListNoDup;
|
||||
}
|
||||
|
||||
public boolean isRoot() {
|
||||
return pkgNode.isRoot();
|
||||
}
|
||||
|
||||
@@ -17,14 +17,14 @@ public enum ResourceType {
|
||||
ARSC(CONTENT_TEXT, ".arsc"),
|
||||
APK(CONTENT_BINARY, ".apk", ".apkm", ".apks"),
|
||||
FONT(CONTENT_BINARY, ".ttf", ".ttc", ".otf"),
|
||||
IMG(CONTENT_BINARY, ".png", ".gif", ".jpg", ".webp", ".bmp", ".tiff"),
|
||||
IMG(CONTENT_BINARY, ".png", ".gif", ".jpg", ".jpeg", ".webp", ".bmp", ".tiff"),
|
||||
ARCHIVE(CONTENT_BINARY, ".zip", ".rar", ".7zip", ".7z", ".arj", ".tar", ".gzip", ".bzip", ".bzip2", ".cab", ".cpio", ".ar", ".gz",
|
||||
".tgz", ".bz2"),
|
||||
VIDEOS(CONTENT_BINARY, ".mp4", ".mkv", ".webm", ".avi", ".flv", ".3gp"),
|
||||
SOUNDS(CONTENT_BINARY, ".aac", ".ogg", ".opus", ".mp3", ".wav", ".wma", ".mid", ".midi"),
|
||||
JSON(CONTENT_TEXT, ".json"),
|
||||
TEXT(CONTENT_TEXT, ".txt", ".ini", ".conf", ".yaml", ".properties", ".js"),
|
||||
HTML(CONTENT_TEXT, ".html"),
|
||||
TEXT(CONTENT_TEXT, ".txt", ".ini", ".conf", ".yaml", ".properties", ".js", ".java", ".kt", ".md"),
|
||||
HTML(CONTENT_TEXT, ".html", ".htm"),
|
||||
LIB(CONTENT_BINARY, ".so"),
|
||||
MANIFEST(CONTENT_TEXT),
|
||||
UNKNOWN_BIN(CONTENT_BINARY, ".bin"),
|
||||
|
||||
@@ -29,7 +29,7 @@ public class DecompilePassWrapper extends AbstractVisitor implements IPassWrappe
|
||||
public void init(RootNode root) throws JadxException {
|
||||
try {
|
||||
decompilePass.init(root);
|
||||
} catch (Throwable e) {
|
||||
} catch (StackOverflowError | Exception e) {
|
||||
LOG.error("Error in decompile pass init: {}", this, e);
|
||||
}
|
||||
}
|
||||
@@ -38,8 +38,8 @@ public class DecompilePassWrapper extends AbstractVisitor implements IPassWrappe
|
||||
public boolean visit(ClassNode cls) throws JadxException {
|
||||
try {
|
||||
return decompilePass.visit(cls);
|
||||
} catch (Throwable e) {
|
||||
LOG.error("Error in decompile pass: {}, class: {}", this, cls, e);
|
||||
} catch (StackOverflowError | Exception e) {
|
||||
cls.addError("Error in decompile pass: " + this, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -48,8 +48,8 @@ public class DecompilePassWrapper extends AbstractVisitor implements IPassWrappe
|
||||
public void visit(MethodNode mth) throws JadxException {
|
||||
try {
|
||||
decompilePass.visit(mth);
|
||||
} catch (Throwable e) {
|
||||
LOG.error("Error in decompile pass: {}, method: {}", this, mth, e);
|
||||
} catch (StackOverflowError | Exception e) {
|
||||
mth.addError("Error in decompile pass: " + this, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,8 +32,11 @@ public class CodeMetadataStorage implements ICodeMetadata {
|
||||
return new CodeMetadataStorage(Collections.emptyMap(), Collections.emptyNavigableMap());
|
||||
}
|
||||
|
||||
// <decomp file line number> -> <dex debug line number>
|
||||
private final Map<Integer, Integer> lines;
|
||||
|
||||
// <character index into the file> -> <code annotation>
|
||||
// the key is what is returned by AbstractCodeArea#getCaretPos() when clicking in a code panel.
|
||||
private final NavigableMap<Integer, ICodeAnnotation> navMap;
|
||||
|
||||
private CodeMetadataStorage(Map<Integer, Integer> lines, NavigableMap<Integer, ICodeAnnotation> navMap) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package jadx.api.usage;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import jadx.core.dex.info.MethodInfo;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.core.dex.nodes.FieldNode;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
@@ -18,5 +19,11 @@ public interface IUsageInfoVisitor {
|
||||
|
||||
void visitMethodsUsage(MethodNode mth, List<MethodNode> methods);
|
||||
|
||||
void visitMethodsUses(MethodNode mth, List<MethodNode> methods);
|
||||
|
||||
void visitUnresolvedMethodsUsage(MethodNode mth, List<MethodInfo> methods);
|
||||
|
||||
void visitIsSelfCall(MethodNode mth, boolean isSelfCall);
|
||||
|
||||
void visitComplete();
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import jadx.api.JadxArgs;
|
||||
import jadx.core.deobf.DeobfuscatorVisitor;
|
||||
import jadx.core.deobf.SaveDeobfMapping;
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.visitors.AdjustForIfMergeVisitor;
|
||||
import jadx.core.dex.visitors.AnonymousClassVisitor;
|
||||
import jadx.core.dex.visitors.ApplyVariableNames;
|
||||
import jadx.core.dex.visitors.AttachCommentsVisitor;
|
||||
@@ -66,6 +67,7 @@ import jadx.core.dex.visitors.regions.IfRegionVisitor;
|
||||
import jadx.core.dex.visitors.regions.LoopRegionVisitor;
|
||||
import jadx.core.dex.visitors.regions.RegionMakerVisitor;
|
||||
import jadx.core.dex.visitors.regions.ReturnVisitor;
|
||||
import jadx.core.dex.visitors.regions.SwitchBreakVisitor;
|
||||
import jadx.core.dex.visitors.regions.SwitchOverStringVisitor;
|
||||
import jadx.core.dex.visitors.regions.variables.ProcessVariables;
|
||||
import jadx.core.dex.visitors.rename.CodeRenameVisitor;
|
||||
@@ -155,6 +157,8 @@ public class Jadx {
|
||||
passes.add(new FixTypesVisitor());
|
||||
passes.add(new FinishTypeInference());
|
||||
|
||||
passes.add(new AdjustForIfMergeVisitor());
|
||||
|
||||
if (args.getUseKotlinMethodsForVarNames() != JadxArgs.UseKotlinMethodsForVarNames.DISABLE) {
|
||||
passes.add(new ProcessKotlinInternals());
|
||||
}
|
||||
@@ -196,12 +200,14 @@ public class Jadx {
|
||||
passes.add(new FixAccessModifiers());
|
||||
passes.add(new ClassModifier());
|
||||
passes.add(new LoopRegionVisitor());
|
||||
passes.add(new SwitchBreakVisitor());
|
||||
|
||||
if (args.isInlineMethods()) {
|
||||
passes.add(new MarkMethodsForInline());
|
||||
}
|
||||
passes.add(new ProcessVariables());
|
||||
passes.add(new ApplyVariableNames());
|
||||
|
||||
passes.add(new PrepareForCodeGen());
|
||||
if (args.isCfgOutput()) {
|
||||
passes.add(DotGraphVisitor.dumpRegions());
|
||||
|
||||
@@ -1,21 +1,29 @@
|
||||
package jadx.core;
|
||||
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.DecompilationMode;
|
||||
import jadx.api.ICodeInfo;
|
||||
import jadx.api.JadxArgs;
|
||||
import jadx.api.impl.SimpleCodeInfo;
|
||||
import jadx.core.codegen.CodeGen;
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.attributes.AType;
|
||||
import jadx.core.dex.attributes.nodes.DecompileModeOverrideAttr;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.core.dex.nodes.LoadStage;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
import jadx.core.dex.visitors.DepthTraversal;
|
||||
import jadx.core.dex.visitors.IDexTreeVisitor;
|
||||
import jadx.core.utils.Utils;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
|
||||
import static jadx.core.dex.nodes.ProcessState.GENERATED_AND_UNLOADED;
|
||||
@@ -41,6 +49,7 @@ public class ProcessClass {
|
||||
// nothing to do
|
||||
return null;
|
||||
}
|
||||
Utils.checkThreadInterrupt();
|
||||
synchronized (cls.getClassInfo()) {
|
||||
try {
|
||||
if (cls.contains(AFlag.CLASS_DEEP_RELOAD)) {
|
||||
@@ -76,6 +85,7 @@ public class ProcessClass {
|
||||
cls.setState(PROCESS_COMPLETE);
|
||||
}
|
||||
if (codegen) {
|
||||
Utils.checkThreadInterrupt();
|
||||
ICodeInfo code = CodeGen.generate(cls);
|
||||
if (!cls.contains(AFlag.DONT_UNLOAD_CLASS)) {
|
||||
cls.unload();
|
||||
@@ -84,7 +94,7 @@ public class ProcessClass {
|
||||
return code;
|
||||
}
|
||||
return null;
|
||||
} catch (Throwable e) {
|
||||
} catch (StackOverflowError | Exception e) {
|
||||
if (codegen) {
|
||||
throw e;
|
||||
}
|
||||
@@ -119,7 +129,7 @@ public class ProcessClass {
|
||||
throw new JadxRuntimeException("Codegen failed");
|
||||
}
|
||||
return code;
|
||||
} catch (Throwable e) {
|
||||
} catch (StackOverflowError | Exception e) {
|
||||
throw new JadxRuntimeException("Failed to generate code for class: " + cls.getFullName(), e);
|
||||
}
|
||||
}
|
||||
@@ -135,7 +145,7 @@ public class ProcessClass {
|
||||
}
|
||||
try {
|
||||
process(cls, false);
|
||||
} catch (Throwable e) {
|
||||
} catch (StackOverflowError | Exception e) {
|
||||
throw new JadxRuntimeException("Failed to process class: " + cls.getFullName(), e);
|
||||
}
|
||||
}
|
||||
@@ -146,11 +156,48 @@ public class ProcessClass {
|
||||
public @Nullable ICodeInfo forceGenerateCode(ClassNode cls) {
|
||||
try {
|
||||
return process(cls, true);
|
||||
} catch (Throwable e) {
|
||||
} catch (StackOverflowError | Exception e) {
|
||||
throw new JadxRuntimeException("Failed to generate code for class: " + cls.getFullName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private final Map<DecompilationMode, ProcessClass> modesMap = new EnumMap<>(DecompilationMode.class);
|
||||
|
||||
public @Nullable ICodeInfo forceGenerateCodeForMode(ClassNode cls, DecompilationMode mode) {
|
||||
synchronized (modesMap) {
|
||||
ProcessClass prCls = modesMap.computeIfAbsent(mode, m -> {
|
||||
RootNode root = cls.root();
|
||||
ProcessClass newPrCls = new ProcessClass(getPassesForMode(root.getArgs(), m));
|
||||
newPrCls.initPasses(root);
|
||||
return newPrCls;
|
||||
});
|
||||
try {
|
||||
cls.addAttr(new DecompileModeOverrideAttr(mode));
|
||||
return prCls.forceGenerateCode(cls);
|
||||
} finally {
|
||||
cls.remove(AType.DECOMPILE_MODE_OVERRIDE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<IDexTreeVisitor> getPassesForMode(JadxArgs baseArgs, DecompilationMode mode) {
|
||||
switch (mode) {
|
||||
case FALLBACK:
|
||||
return Jadx.getFallbackPassesList();
|
||||
|
||||
case SIMPLE:
|
||||
// copy properties into new args
|
||||
// keep in sync with properties usage in Jadx.getSimpleModePasses method
|
||||
JadxArgs args = new JadxArgs();
|
||||
args.setDebugInfo(baseArgs.isDebugInfo());
|
||||
args.setCommentsLevel(baseArgs.getCommentsLevel());
|
||||
return Jadx.getSimpleModePasses(args);
|
||||
|
||||
default:
|
||||
throw new JadxRuntimeException("Unexpected decompilation mode: " + mode);
|
||||
}
|
||||
}
|
||||
|
||||
public void initPasses(RootNode root) {
|
||||
for (IDexTreeVisitor pass : passes) {
|
||||
try {
|
||||
@@ -161,6 +208,44 @@ public class ProcessClass {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean processMethodUntilVisitor(MethodNode mth, String visitorName, boolean includeVisitor) {
|
||||
IDexTreeVisitor foundPass = null;
|
||||
IDexTreeVisitor prevPass = null;
|
||||
for (IDexTreeVisitor pass : passes) {
|
||||
if (pass.getName().equals(visitorName)) {
|
||||
if (includeVisitor) {
|
||||
foundPass = pass;
|
||||
} else {
|
||||
foundPass = prevPass;
|
||||
}
|
||||
break;
|
||||
}
|
||||
prevPass = pass;
|
||||
}
|
||||
if (foundPass == null) {
|
||||
return false;
|
||||
}
|
||||
return processMethodToVisitor(mth, foundPass);
|
||||
}
|
||||
|
||||
public boolean processMethodToVisitor(MethodNode mth, IDexTreeVisitor lastPassToProcess) {
|
||||
synchronized (mth.getTopParentClass().getClassInfo()) {
|
||||
try {
|
||||
mth.unload();
|
||||
mth.load();
|
||||
for (IDexTreeVisitor pass : passes) {
|
||||
DepthTraversal.visit(pass, mth);
|
||||
if (pass == lastPassToProcess) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new JadxRuntimeException("Failed to process method to visitor: " + lastPassToProcess, e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: make passes list private and not visible
|
||||
public List<IDexTreeVisitor> getPasses() {
|
||||
return passes;
|
||||
|
||||
@@ -34,6 +34,7 @@ import jadx.core.dex.attributes.nodes.EnumClassAttr;
|
||||
import jadx.core.dex.attributes.nodes.EnumClassAttr.EnumField;
|
||||
import jadx.core.dex.attributes.nodes.LineAttrNode;
|
||||
import jadx.core.dex.attributes.nodes.MethodInlineAttr;
|
||||
import jadx.core.dex.attributes.nodes.NotificationAttrNode;
|
||||
import jadx.core.dex.attributes.nodes.SkipMethodArgsAttr;
|
||||
import jadx.core.dex.info.AccessInfo;
|
||||
import jadx.core.dex.info.ClassInfo;
|
||||
@@ -155,8 +156,6 @@ public class ClassGen {
|
||||
if (Consts.DEBUG_USAGE) {
|
||||
addClassUsageInfo(code, cls);
|
||||
}
|
||||
CodeGenUtils.addErrorsAndComments(code, cls);
|
||||
CodeGenUtils.addSourceFileInfo(code, cls);
|
||||
addClassDeclaration(code);
|
||||
addClassBody(code);
|
||||
}
|
||||
@@ -166,20 +165,19 @@ public class ClassGen {
|
||||
if (af.isInterface()) {
|
||||
af = af.remove(AccessFlags.ABSTRACT)
|
||||
.remove(AccessFlags.STATIC);
|
||||
} else if (af.isEnum()) {
|
||||
af = af.remove(AccessFlags.FINAL)
|
||||
.remove(AccessFlags.ABSTRACT)
|
||||
.remove(AccessFlags.STATIC);
|
||||
}
|
||||
|
||||
// 'static' and 'private' modifier not allowed for top classes (not inner)
|
||||
if (!cls.getClassInfo().isInner()) {
|
||||
af = af.remove(AccessFlags.STATIC).remove(AccessFlags.PRIVATE);
|
||||
}
|
||||
|
||||
annotationGen.addForClass(clsCode);
|
||||
insertRenameInfo(clsCode, cls);
|
||||
CodeGenUtils.addComments(clsCode, cls);
|
||||
CodeGenUtils.addClassRenamedComment(clsCode, cls);
|
||||
CodeGenUtils.addErrors(clsCode, cls);
|
||||
CodeGenUtils.addSourceFileInfo(clsCode, cls);
|
||||
CodeGenUtils.addInputFileInfo(clsCode, cls);
|
||||
|
||||
annotationGen.addForClass(clsCode);
|
||||
clsCode.startLineWithNum(cls.getSourceLine()).add(af.makeString(cls.checkCommentsLevel(CommentsLevel.INFO)));
|
||||
if (af.isInterface()) {
|
||||
if (af.isAnnotation()) {
|
||||
@@ -292,7 +290,7 @@ public class ClassGen {
|
||||
private void addInnerClsAndMethods(ICodeWriter clsCode) {
|
||||
Stream.of(cls.getInnerClasses(), cls.getMethods())
|
||||
.flatMap(Collection::stream)
|
||||
.filter(node -> !node.contains(AFlag.DONT_GENERATE) || fallback)
|
||||
.filter(node -> !skipNode(node))
|
||||
.sorted(Comparator.comparingInt(LineAttrNode::getSourceLine))
|
||||
.forEach(node -> {
|
||||
if (node instanceof ClassNode) {
|
||||
@@ -303,6 +301,18 @@ public class ClassGen {
|
||||
});
|
||||
}
|
||||
|
||||
private boolean skipNode(NotificationAttrNode node) {
|
||||
if (fallback) {
|
||||
return false;
|
||||
}
|
||||
if (Consts.DEBUG_ATTRIBUTES) {
|
||||
if (node.contains(AType.JADX_COMMENTS)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return node.contains(AFlag.DONT_GENERATE);
|
||||
}
|
||||
|
||||
private void addInnerClass(ICodeWriter code, ClassNode innerCls) {
|
||||
try {
|
||||
ClassGen inClGen = new ClassGen(innerCls, getParentGen());
|
||||
@@ -434,10 +444,10 @@ public class ClassGen {
|
||||
if (Consts.DEBUG_USAGE) {
|
||||
addFieldUsageInfo(code, f);
|
||||
}
|
||||
CodeGenUtils.addComments(code, f);
|
||||
if (f.getFieldInfo().hasAlias()) {
|
||||
CodeGenUtils.addRenamedComment(code, f, f.getName());
|
||||
}
|
||||
CodeGenUtils.addComments(code, f);
|
||||
annotationGen.addForField(code, f);
|
||||
|
||||
code.startLine(f.getAccessFlags().makeString(f.checkCommentsLevel(CommentsLevel.INFO)));
|
||||
@@ -802,13 +812,6 @@ public class ClassGen {
|
||||
return root.getClsp().isClsKnown(currentPkg + '.' + shortName);
|
||||
}
|
||||
|
||||
private void insertRenameInfo(ICodeWriter code, ClassNode cls) {
|
||||
ClassInfo classInfo = cls.getClassInfo();
|
||||
if (classInfo.hasAlias() && cls.checkCommentsLevel(CommentsLevel.INFO)) {
|
||||
CodeGenUtils.addRenamedComment(code, cls, classInfo.getType().getObject());
|
||||
}
|
||||
}
|
||||
|
||||
private static void addClassUsageInfo(ICodeWriter code, ClassNode cls) {
|
||||
List<ClassNode> deps = cls.getDependencies();
|
||||
code.startLine("// deps - ").add(Integer.toString(deps.size()));
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
package jadx.core.codegen;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.CommentsLevel;
|
||||
import jadx.api.DecompilationMode;
|
||||
import jadx.api.ICodeWriter;
|
||||
import jadx.api.JadxArgs;
|
||||
import jadx.api.args.IntegerFormat;
|
||||
@@ -25,10 +22,12 @@ import jadx.core.Jadx;
|
||||
import jadx.core.codegen.utils.CodeGenUtils;
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.attributes.AType;
|
||||
import jadx.core.dex.attributes.nodes.DecompileModeOverrideAttr;
|
||||
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.attributes.nodes.SkipMethodArgsAttr;
|
||||
import jadx.core.dex.info.AccessInfo;
|
||||
import jadx.core.dex.instructions.ConstStringNode;
|
||||
import jadx.core.dex.instructions.IfNode;
|
||||
@@ -109,10 +108,6 @@ public class MethodGen {
|
||||
if (clsAccFlags.isAnnotation()) {
|
||||
ai = ai.remove(AccessFlags.PUBLIC);
|
||||
}
|
||||
if (mth.getMethodInfo().isConstructor() && mth.getParentClass().isEnum()) {
|
||||
ai = ai.remove(AccessInfo.VISIBILITY_FLAGS);
|
||||
}
|
||||
|
||||
if (mth.getMethodInfo().hasAlias() && !ai.isConstructor()) {
|
||||
CodeGenUtils.addRenamedComment(code, mth, mth.getName());
|
||||
}
|
||||
@@ -152,21 +147,7 @@ public class MethodGen {
|
||||
code.add(defMth.getAlias());
|
||||
}
|
||||
code.add('(');
|
||||
|
||||
List<RegisterArg> args = mth.getArgRegs();
|
||||
if (mth.getMethodInfo().isConstructor()
|
||||
&& mth.getParentClass().contains(AType.ENUM_CLASS)) {
|
||||
if (args.size() == 2) {
|
||||
args = Collections.emptyList();
|
||||
} else if (args.size() > 2) {
|
||||
args = args.subList(2, args.size());
|
||||
} else {
|
||||
mth.addWarnComment("Incorrect number of args for enum constructor: " + args.size() + " (expected >= 2)");
|
||||
}
|
||||
} else if (mth.contains(AFlag.SKIP_FIRST_ARG)) {
|
||||
args = args.subList(1, args.size());
|
||||
}
|
||||
addMethodArguments(code, args);
|
||||
addMethodArguments(code);
|
||||
code.add(')');
|
||||
|
||||
annotationGen.addThrows(mth, code);
|
||||
@@ -209,12 +190,22 @@ public class MethodGen {
|
||||
}
|
||||
}
|
||||
|
||||
private void addMethodArguments(ICodeWriter code, List<RegisterArg> args) {
|
||||
private void addMethodArguments(ICodeWriter code) {
|
||||
List<RegisterArg> args = mth.getArgRegs();
|
||||
AnnotationMethodParamsAttr paramsAnnotation = mth.get(JadxAttrType.ANNOTATION_MTH_PARAMETERS);
|
||||
int i = 0;
|
||||
Iterator<RegisterArg> it = args.iterator();
|
||||
while (it.hasNext()) {
|
||||
RegisterArg mthArg = it.next();
|
||||
int argNum = -1;
|
||||
int lastArgNum = args.size() - 1;
|
||||
boolean first = true;
|
||||
for (RegisterArg mthArg : args) {
|
||||
argNum++;
|
||||
if (SkipMethodArgsAttr.isSkip(mth, argNum)) {
|
||||
continue;
|
||||
}
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
code.add(", ");
|
||||
}
|
||||
SSAVar ssaVar = mthArg.getSVar();
|
||||
CodeVar var;
|
||||
if (ssaVar == null) {
|
||||
@@ -226,7 +217,7 @@ public class MethodGen {
|
||||
|
||||
// add argument annotation
|
||||
if (paramsAnnotation != null) {
|
||||
annotationGen.addForParameter(code, paramsAnnotation, i);
|
||||
annotationGen.addForParameter(code, paramsAnnotation, argNum);
|
||||
}
|
||||
if (var.isFinal()) {
|
||||
code.add("final ");
|
||||
@@ -239,7 +230,7 @@ public class MethodGen {
|
||||
} else {
|
||||
argType = varType;
|
||||
}
|
||||
if (!it.hasNext() && mth.getAccessFlags().isVarArgs()) {
|
||||
if (argNum == lastArgNum && mth.getAccessFlags().isVarArgs()) {
|
||||
// change last array argument to varargs
|
||||
if (argType.isArray()) {
|
||||
ArgType elType = argType.getArrayElement();
|
||||
@@ -258,17 +249,19 @@ public class MethodGen {
|
||||
code.attachDefinition(VarNode.get(mth, var));
|
||||
}
|
||||
code.add(varName);
|
||||
|
||||
i++;
|
||||
if (it.hasNext()) {
|
||||
code.add(", ");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void addInstructions(ICodeWriter code) throws CodegenException {
|
||||
JadxArgs args = mth.root().getArgs();
|
||||
switch (args.getDecompilationMode()) {
|
||||
DecompileModeOverrideAttr modeOverrideAttr = mth.getTopParentClass().get(AType.DECOMPILE_MODE_OVERRIDE);
|
||||
DecompilationMode mode;
|
||||
if (modeOverrideAttr != null) {
|
||||
mode = modeOverrideAttr.getMode();
|
||||
} else {
|
||||
mode = args.getDecompilationMode();
|
||||
}
|
||||
switch (mode) {
|
||||
case AUTO:
|
||||
if (classGen.isFallbackMode() || mth.getRegion() == null) {
|
||||
// TODO: try simple mode first
|
||||
@@ -381,6 +374,20 @@ public class MethodGen {
|
||||
}
|
||||
|
||||
public void addFallbackMethodCode(ICodeWriter code, FallbackOption fallbackOption) {
|
||||
if (fallbackOption == COMMENTED_DUMP && mth.getCommentsLevel() != CommentsLevel.DEBUG) {
|
||||
long insnCountEstimate = mth.getInsnsCount();
|
||||
if (insnCountEstimate > 200) {
|
||||
code.incIndent();
|
||||
code.startLine("Method dump skipped, instruction units count: " + insnCountEstimate);
|
||||
if (code.isMetadataSupported()) {
|
||||
code.startLine("To view this dump change 'Code comments level' option to 'DEBUG'");
|
||||
} else {
|
||||
code.startLine("To view this dump add '--comments-level debug' option");
|
||||
}
|
||||
code.decIndent();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (fallbackOption != FALLBACK_MODE) {
|
||||
List<JadxError> errors = mth.getAll(AType.JADX_ERROR); // preserve error before unload
|
||||
try {
|
||||
@@ -390,13 +397,12 @@ public class MethodGen {
|
||||
for (IDexTreeVisitor visitor : Jadx.getFallbackPassesList()) {
|
||||
DepthTraversal.visit(visitor, mth);
|
||||
}
|
||||
errors.forEach(err -> mth.addAttr(AType.JADX_ERROR, err));
|
||||
} catch (Exception e) {
|
||||
LOG.error("Error reload instructions in fallback mode:", e);
|
||||
code.startLine("// Can't load method instructions: " + e.getMessage());
|
||||
return;
|
||||
} finally {
|
||||
errors.forEach(err -> mth.addAttr(AType.JADX_ERROR, err));
|
||||
mth.addAttr(AType.JADX_ERROR, errors);
|
||||
}
|
||||
}
|
||||
InsnNode[] insnArr = mth.getInstructions();
|
||||
@@ -404,23 +410,6 @@ public class MethodGen {
|
||||
code.startLine("// Can't load method instructions.");
|
||||
return;
|
||||
}
|
||||
if (fallbackOption == COMMENTED_DUMP && mth.getCommentsLevel() != CommentsLevel.DEBUG) {
|
||||
long insnCountEstimate = Stream.of(insnArr)
|
||||
.filter(Objects::nonNull)
|
||||
.filter(insn -> insn.getType() != InsnType.NOP)
|
||||
.count();
|
||||
if (insnCountEstimate > 100) {
|
||||
code.incIndent();
|
||||
code.startLine("Method dump skipped, instructions count: " + insnArr.length);
|
||||
if (code.isMetadataSupported()) {
|
||||
code.startLine("To view this dump change 'Code comments level' option to 'DEBUG'");
|
||||
} else {
|
||||
code.startLine("To view this dump add '--comments-level debug' option");
|
||||
}
|
||||
code.decIndent();
|
||||
return;
|
||||
}
|
||||
}
|
||||
code.incIndent();
|
||||
if (mth.getThisArg() != null) {
|
||||
code.startLine(nameGen.useArg(mth.getThisArg())).add(" = this;");
|
||||
|
||||
@@ -3,6 +3,8 @@ package jadx.core.codegen;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import jadx.core.deobf.NameMapper;
|
||||
import jadx.core.dex.attributes.nodes.LoopLabelAttr;
|
||||
@@ -84,15 +86,32 @@ public class NameGen {
|
||||
return name;
|
||||
}
|
||||
|
||||
private static final Pattern ENDS_WITH_NUMBER = Pattern.compile(".*(\\d+)$");
|
||||
|
||||
private String getUniqueVarName(String name) {
|
||||
String r = name;
|
||||
int i = 2;
|
||||
while (varNames.contains(r)) {
|
||||
r = name + i;
|
||||
i++;
|
||||
if (!varNames.contains(name)) {
|
||||
varNames.add(name);
|
||||
return name;
|
||||
}
|
||||
// code duplication reuse same variable in different places
|
||||
// parse variable name and increment index
|
||||
String base;
|
||||
int i;
|
||||
Matcher matcher = ENDS_WITH_NUMBER.matcher(name);
|
||||
if (matcher.matches()) {
|
||||
base = name.substring(0, matcher.start(1));
|
||||
i = 1 + Integer.parseInt(matcher.group(1));
|
||||
} else {
|
||||
base = name;
|
||||
i = 2;
|
||||
}
|
||||
while (true) {
|
||||
String newName = base + i++;
|
||||
if (!varNames.contains(newName)) {
|
||||
varNames.add(newName);
|
||||
return newName;
|
||||
}
|
||||
}
|
||||
varNames.add(r);
|
||||
return r;
|
||||
}
|
||||
|
||||
private String makeArgName(CodeVar var) {
|
||||
|
||||
@@ -294,6 +294,9 @@ public class RegionGen extends InsnGen {
|
||||
isEnum = clsDetails != null && clsDetails.hasAccFlag(AccessFlags.ENUM);
|
||||
}
|
||||
if (isEnum) {
|
||||
if (fld != null) {
|
||||
code.attachAnnotation(fld);
|
||||
}
|
||||
code.add(fldInfo.getAlias());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package jadx.core.codegen;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -30,6 +32,10 @@ public class TypeGen {
|
||||
return stype.getShortName();
|
||||
}
|
||||
|
||||
public static List<String> signatures(List<ArgType> types) {
|
||||
return Utils.collectionMap(types, TypeGen::signature);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert literal arg to string (preferred method)
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package jadx.core.codegen.utils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -16,6 +17,7 @@ import jadx.core.dex.attributes.nodes.JadxCommentsAttr;
|
||||
import jadx.core.dex.attributes.nodes.JadxError;
|
||||
import jadx.core.dex.attributes.nodes.NotificationAttrNode;
|
||||
import jadx.core.dex.attributes.nodes.RenameReasonAttr;
|
||||
import jadx.core.dex.info.ClassInfo;
|
||||
import jadx.core.dex.instructions.args.CodeVar;
|
||||
import jadx.core.dex.instructions.args.RegisterArg;
|
||||
import jadx.core.dex.instructions.args.SSAVar;
|
||||
@@ -26,8 +28,8 @@ import jadx.core.utils.Utils;
|
||||
public class CodeGenUtils {
|
||||
|
||||
public static void addErrorsAndComments(ICodeWriter code, NotificationAttrNode node) {
|
||||
addErrors(code, node);
|
||||
addComments(code, node);
|
||||
addErrors(code, node);
|
||||
}
|
||||
|
||||
public static void addErrors(ICodeWriter code, NotificationAttrNode node) {
|
||||
@@ -86,9 +88,37 @@ public class CodeGenUtils {
|
||||
} else {
|
||||
code.add(' ');
|
||||
}
|
||||
CommentStyle style = comment.getStyle();
|
||||
addCommentWithStyle(code, comment.getStyle(), comment.getComment());
|
||||
}
|
||||
|
||||
public static void addJadxNodeComment(ICodeWriter code, NotificationAttrNode node,
|
||||
CommentsLevel level, BiConsumer<ICodeWriter, String> commentFunc) {
|
||||
if (node.checkCommentsLevel(level)) {
|
||||
code.startLine();
|
||||
addCommentWithStyle(code, CommentStyle.BLOCK_CONDENSED, (commentCode, newLinePrefix) -> {
|
||||
commentCode.add("JADX ").add(level.name()).add(": ");
|
||||
commentFunc.accept(commentCode, newLinePrefix);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static void addJadxComment(ICodeWriter code, CommentsLevel level, String commentStr) {
|
||||
code.startLine();
|
||||
addCommentWithStyle(code, CommentStyle.BLOCK_CONDENSED, "JADX " + level.name() + ": " + commentStr);
|
||||
}
|
||||
|
||||
private static void addCommentWithStyle(ICodeWriter code, CommentStyle style, String commentStr) {
|
||||
appendMultiLineString(code, "", style.getStart());
|
||||
appendMultiLineString(code, style.getOnNewLine(), comment.getComment());
|
||||
appendMultiLineString(code, style.getOnNewLine(), commentStr);
|
||||
appendMultiLineString(code, "", style.getEnd());
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert comment with function, use second arg as new line prefix
|
||||
*/
|
||||
private static void addCommentWithStyle(ICodeWriter code, CommentStyle style, BiConsumer<ICodeWriter, String> commentFunc) {
|
||||
appendMultiLineString(code, "", style.getStart());
|
||||
commentFunc.accept(code, style.getOnNewLine());
|
||||
appendMultiLineString(code, "", style.getEnd());
|
||||
}
|
||||
|
||||
@@ -107,16 +137,21 @@ public class CodeGenUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static void addClassRenamedComment(ICodeWriter code, ClassNode cls) {
|
||||
ClassInfo classInfo = cls.getClassInfo();
|
||||
if (classInfo.hasAlias()) {
|
||||
addRenamedComment(code, cls, classInfo.getType().getObject());
|
||||
}
|
||||
}
|
||||
|
||||
public static void addRenamedComment(ICodeWriter code, NotificationAttrNode node, String origName) {
|
||||
if (!node.checkCommentsLevel(CommentsLevel.INFO)) {
|
||||
return;
|
||||
}
|
||||
code.startLine("/* renamed from: ").add(origName);
|
||||
RenameReasonAttr renameReasonAttr = node.get(AType.RENAME_REASON);
|
||||
if (renameReasonAttr != null) {
|
||||
code.add(", reason: ").add(renameReasonAttr.getDescription());
|
||||
}
|
||||
code.add(" */");
|
||||
addJadxNodeComment(code, node, CommentsLevel.INFO, (commentCode, newLinePrefix) -> {
|
||||
commentCode.add("renamed from: ").add(origName);
|
||||
RenameReasonAttr renameReasonAttr = node.get(AType.RENAME_REASON);
|
||||
if (renameReasonAttr != null) {
|
||||
commentCode.add(", reason: ").add(renameReasonAttr.getDescription());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void addSourceFileInfo(ICodeWriter code, ClassNode node) {
|
||||
@@ -131,7 +166,7 @@ public class CodeGenUtils {
|
||||
// ignore similar name
|
||||
return;
|
||||
}
|
||||
code.startLine("/* compiled from: ").add(fileName).add(" */");
|
||||
addJadxComment(code, CommentsLevel.INFO, "compiled from: " + fileName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +181,7 @@ public class CodeGenUtils {
|
||||
// don't add same comment for inner classes
|
||||
return;
|
||||
}
|
||||
code.startLine("/* loaded from: ").add(inputFileName).add(" */");
|
||||
addJadxComment(code, CommentsLevel.INFO, "loaded from: " + inputFileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,9 +83,12 @@ public class FileTypeDetector {
|
||||
try {
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
factory.setNamespaceAware(true);
|
||||
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
|
||||
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
|
||||
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
|
||||
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
|
||||
factory.setXIncludeAware(false);
|
||||
factory.setExpandEntityReferences(false);
|
||||
|
||||
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||
Document doc = builder.parse(new java.io.ByteArrayInputStream(data));
|
||||
|
||||
@@ -18,17 +18,23 @@ public enum AFlag {
|
||||
DONT_WRAP,
|
||||
DONT_INLINE,
|
||||
DONT_INLINE_CONST,
|
||||
DONT_INVERT, // don't invert this if statement
|
||||
DONT_GENERATE, // process as usual, but don't output to generated code
|
||||
COMMENT_OUT, // process as usual, but comment insn in generated code
|
||||
REMOVE, // can be completely removed
|
||||
REMOVE_SUPER_CLASS, // don't add super class
|
||||
|
||||
HIDDEN, // instruction used inside other instruction but not listed in args
|
||||
CONVERTED_ENUM, // enum class successfully restored to original form
|
||||
|
||||
DONT_RENAME, // do not rename during deobfuscation
|
||||
FORCE_RAW_NAME, // force use of raw name instead alias
|
||||
|
||||
ADDED_TO_REGION,
|
||||
DUPLICATED,
|
||||
|
||||
// this loop condition has been merged or otherwise shouldn't be subject to the 1 instruction limit
|
||||
ALLOW_MULTIPLE_INSNS_LOOP_COND,
|
||||
|
||||
EXC_TOP_SPLITTER,
|
||||
EXC_BOTTOM_SPLITTER,
|
||||
|
||||
@@ -7,9 +7,11 @@ import jadx.core.dex.attributes.nodes.AnonymousClassAttr;
|
||||
import jadx.core.dex.attributes.nodes.ClassTypeVarsAttr;
|
||||
import jadx.core.dex.attributes.nodes.CodeFeaturesAttr;
|
||||
import jadx.core.dex.attributes.nodes.DeclareVariablesAttr;
|
||||
import jadx.core.dex.attributes.nodes.DecompileModeOverrideAttr;
|
||||
import jadx.core.dex.attributes.nodes.EdgeInsnAttr;
|
||||
import jadx.core.dex.attributes.nodes.EnumClassAttr;
|
||||
import jadx.core.dex.attributes.nodes.EnumMapAttr;
|
||||
import jadx.core.dex.attributes.nodes.ExcSplitCrossAttr;
|
||||
import jadx.core.dex.attributes.nodes.FieldReplaceAttr;
|
||||
import jadx.core.dex.attributes.nodes.ForceReturnAttr;
|
||||
import jadx.core.dex.attributes.nodes.GenericInfoAttr;
|
||||
@@ -62,6 +64,7 @@ public final class AType<T extends IJadxAttribute> implements IJadxAttrType<T> {
|
||||
public static final AType<ClassTypeVarsAttr> CLASS_TYPE_VARS = new AType<>();
|
||||
public static final AType<AnonymousClassAttr> ANONYMOUS_CLASS = new AType<>();
|
||||
public static final AType<InlinedAttr> INLINED = new AType<>();
|
||||
public static final AType<DecompileModeOverrideAttr> DECOMPILE_MODE_OVERRIDE = new AType<>();
|
||||
|
||||
// field
|
||||
public static final AType<FieldInitInsnAttr> FIELD_INIT_INSN = new AType<>();
|
||||
@@ -90,6 +93,7 @@ public final class AType<T extends IJadxAttribute> implements IJadxAttrType<T> {
|
||||
public static final AType<AttrList<SpecialEdgeAttr>> SPECIAL_EDGE = new AType<>();
|
||||
public static final AType<TmpEdgeAttr> TMP_EDGE = new AType<>();
|
||||
public static final AType<TryCatchBlockAttr> TRY_BLOCK = new AType<>();
|
||||
public static final AType<ExcSplitCrossAttr> EXC_SPLIT_CROSS = new AType<>();
|
||||
|
||||
// block or insn
|
||||
public static final AType<ExcHandlerAttr> EXC_HANDLER = new AType<>();
|
||||
|
||||
@@ -9,11 +9,19 @@ import jadx.core.utils.Utils;
|
||||
|
||||
public class AttrList<T> implements IJadxAttribute {
|
||||
|
||||
private static final int MAX_ATTRLIST_LENGTH = 300;
|
||||
|
||||
private final IJadxAttrType<AttrList<T>> type;
|
||||
private final List<T> list = new ArrayList<>();
|
||||
private final List<T> list;
|
||||
|
||||
public AttrList(IJadxAttrType<AttrList<T>> type, List<T> attrList) {
|
||||
this.type = type;
|
||||
this.list = attrList;
|
||||
}
|
||||
|
||||
public AttrList(IJadxAttrType<AttrList<T>> type) {
|
||||
this.type = type;
|
||||
this.list = new ArrayList<>();
|
||||
}
|
||||
|
||||
public List<T> getList() {
|
||||
@@ -27,6 +35,11 @@ public class AttrList<T> implements IJadxAttribute {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return Utils.listToString(list, ", ");
|
||||
String commaDelimited = Utils.listToString(list, ", ");
|
||||
// if the comma delimited list is too long, use newlines instead to maintain readability
|
||||
if (commaDelimited.length() > MAX_ATTRLIST_LENGTH) {
|
||||
return Utils.listToString(list, "\n ");
|
||||
}
|
||||
return commaDelimited;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,8 +50,7 @@ public abstract class AttrNode implements IAttributeNode {
|
||||
}
|
||||
|
||||
public <T> void addAttr(IJadxAttrType<AttrList<T>> type, List<T> list) {
|
||||
AttributeStorage strg = initStorage();
|
||||
list.forEach(attr -> strg.add(type, attr));
|
||||
initStorage().addAttrList(type, list);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -14,6 +14,7 @@ import jadx.api.plugins.input.data.attributes.IJadxAttrType;
|
||||
import jadx.api.plugins.input.data.attributes.IJadxAttribute;
|
||||
import jadx.api.plugins.input.data.attributes.JadxAttrType;
|
||||
import jadx.api.plugins.input.data.attributes.types.AnnotationsAttr;
|
||||
import jadx.core.utils.ListUtils;
|
||||
import jadx.core.utils.Utils;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
|
||||
@@ -62,11 +63,20 @@ public class AttributeStorage {
|
||||
|
||||
public <T> void add(IJadxAttrType<AttrList<T>> type, T obj) {
|
||||
AttrList<T> list = get(type);
|
||||
if (list == null) {
|
||||
list = new AttrList<>(type);
|
||||
add(list);
|
||||
if (list != null) {
|
||||
list.getList().add(obj);
|
||||
} else {
|
||||
add(new AttrList<>(type, ListUtils.mutableListOf(obj)));
|
||||
}
|
||||
}
|
||||
|
||||
public <T> void addAttrList(IJadxAttrType<AttrList<T>> type, List<T> attrList) {
|
||||
AttrList<T> list = get(type);
|
||||
if (list != null) {
|
||||
list.getList().addAll(attrList);
|
||||
} else {
|
||||
add(new AttrList<>(type, attrList));
|
||||
}
|
||||
list.getList().add(obj);
|
||||
}
|
||||
|
||||
public void addAll(AttributeStorage otherList) {
|
||||
|
||||
@@ -53,4 +53,9 @@ public class CodeFeaturesAttr implements IJadxAttribute {
|
||||
public String toAttrString() {
|
||||
return "CodeFeatures{" + codeFeatures + '}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toAttrString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package jadx.core.dex.attributes.nodes;
|
||||
|
||||
import jadx.api.DecompilationMode;
|
||||
import jadx.api.plugins.input.data.attributes.IJadxAttrType;
|
||||
import jadx.api.plugins.input.data.attributes.IJadxAttribute;
|
||||
import jadx.core.dex.attributes.AType;
|
||||
|
||||
public class DecompileModeOverrideAttr implements IJadxAttribute {
|
||||
|
||||
private final DecompilationMode mode;
|
||||
|
||||
public DecompileModeOverrideAttr(DecompilationMode mode) {
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
public DecompilationMode getMode() {
|
||||
return mode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IJadxAttrType<DecompileModeOverrideAttr> getAttrType() {
|
||||
return AType.DECOMPILE_MODE_OVERRIDE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DECOMPILE_MODE_OVERRIDE: " + mode;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package jadx.core.dex.attributes.nodes;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.api.plugins.input.data.attributes.IJadxAttribute;
|
||||
import jadx.core.dex.attributes.AType;
|
||||
import jadx.core.dex.instructions.mods.ConstructorInsn;
|
||||
@@ -14,11 +16,13 @@ public class EnumClassAttr implements IJadxAttribute {
|
||||
public static class EnumField {
|
||||
private final FieldNode field;
|
||||
private final ConstructorInsn constrInsn;
|
||||
private final @Nullable String nameStr;
|
||||
private ClassNode cls;
|
||||
|
||||
public EnumField(FieldNode field, ConstructorInsn co) {
|
||||
public EnumField(FieldNode field, ConstructorInsn co, @Nullable String nameStr) {
|
||||
this.field = field;
|
||||
this.constrInsn = co;
|
||||
this.nameStr = nameStr;
|
||||
}
|
||||
|
||||
public FieldNode getField() {
|
||||
@@ -37,6 +41,10 @@ public class EnumClassAttr implements IJadxAttribute {
|
||||
this.cls = cls;
|
||||
}
|
||||
|
||||
public @Nullable String getNameStr() {
|
||||
return nameStr;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return field + "(" + constrInsn + ") " + cls;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package jadx.core.dex.attributes.nodes;
|
||||
|
||||
import jadx.api.plugins.input.data.attributes.IJadxAttrType;
|
||||
import jadx.api.plugins.input.data.attributes.IJadxAttribute;
|
||||
import jadx.core.dex.attributes.AType;
|
||||
import jadx.core.dex.nodes.BlockNode;
|
||||
|
||||
/**
|
||||
* This attribute is set on the new synthetic node that BlockExceptionHandler creates at the bottom
|
||||
* of certain try regions. It stores a reference to the original path cross of the bottom of the try
|
||||
* region, so that blocks can be restructured to not pass through it when that would create an
|
||||
* erroneous loop.
|
||||
*/
|
||||
public class ExcSplitCrossAttr implements IJadxAttribute {
|
||||
|
||||
private final BlockNode originalPathCross;
|
||||
|
||||
public ExcSplitCrossAttr(BlockNode originalPathCross) {
|
||||
this.originalPathCross = originalPathCross;
|
||||
}
|
||||
|
||||
public BlockNode getOriginalPathCross() {
|
||||
return this.originalPathCross;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IJadxAttrType<? extends IJadxAttribute> getAttrType() {
|
||||
return AType.EXC_SPLIT_CROSS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ExcSplitCross -> " + originalPathCross.toString();
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,6 @@ public class RegionRefAttr implements IJadxAttribute {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "RegionRef:" + region;
|
||||
return "RegionRef:" + region.baseString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import java.util.BitSet;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.api.plugins.input.data.attributes.PinnedAttribute;
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.attributes.AType;
|
||||
import jadx.core.dex.instructions.args.RegisterArg;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
@@ -34,6 +35,9 @@ public class SkipMethodArgsAttr extends PinnedAttribute {
|
||||
if (mth == null) {
|
||||
return false;
|
||||
}
|
||||
if (argNum == 0 && mth.contains(AFlag.SKIP_FIRST_ARG)) {
|
||||
return true;
|
||||
}
|
||||
SkipMethodArgsAttr attr = mth.get(AType.SKIP_MTH_ARGS);
|
||||
if (attr == null) {
|
||||
return false;
|
||||
|
||||
@@ -23,9 +23,9 @@ public final class ClassInfo implements Comparable<ClassInfo> {
|
||||
@Nullable
|
||||
private ClassAliasInfo alias;
|
||||
|
||||
private ClassInfo(RootNode root, ArgType type) {
|
||||
private ClassInfo(RootNode root, ArgType type, boolean canBeInner) {
|
||||
this.type = type;
|
||||
splitAndApplyNames(root, type, root.getArgs().isMoveInnerClasses());
|
||||
splitAndApplyNames(root, type, canBeInner);
|
||||
}
|
||||
|
||||
public static ClassInfo fromType(RootNode root, ArgType type) {
|
||||
@@ -34,7 +34,8 @@ public final class ClassInfo implements Comparable<ClassInfo> {
|
||||
if (cls != null) {
|
||||
return cls;
|
||||
}
|
||||
ClassInfo newClsInfo = new ClassInfo(root, clsType);
|
||||
boolean canBeInner = root.getArgs().isMoveInnerClasses();
|
||||
ClassInfo newClsInfo = new ClassInfo(root, clsType, canBeInner);
|
||||
return root.getInfoStorage().putCls(newClsInfo);
|
||||
}
|
||||
|
||||
@@ -42,6 +43,10 @@ public final class ClassInfo implements Comparable<ClassInfo> {
|
||||
return fromType(root, ArgType.object(clsName));
|
||||
}
|
||||
|
||||
public static ClassInfo fromNameWithoutCache(RootNode root, String fullClsName, boolean canBeInner) {
|
||||
return new ClassInfo(root, ArgType.object(fullClsName), canBeInner);
|
||||
}
|
||||
|
||||
private static ArgType checkClassType(ArgType type) {
|
||||
if (type == null) {
|
||||
throw new JadxRuntimeException("Null class type");
|
||||
@@ -209,9 +214,12 @@ public final class ClassInfo implements Comparable<ClassInfo> {
|
||||
}
|
||||
|
||||
public String getAliasFullPath() {
|
||||
return getAliasPkg().replace('.', File.separatorChar)
|
||||
+ File.separatorChar
|
||||
+ getAliasNameWithoutPackage().replace('.', '_');
|
||||
String fileName = getAliasNameWithoutPackage().replace('.', '_');
|
||||
String aliasPkg = getAliasPkg();
|
||||
if (aliasPkg.isEmpty()) {
|
||||
return fileName;
|
||||
}
|
||||
return aliasPkg.replace('.', File.separatorChar) + File.separatorChar + fileName;
|
||||
}
|
||||
|
||||
public String getFullName() {
|
||||
|
||||
@@ -15,7 +15,6 @@ import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.core.dex.nodes.FieldNode;
|
||||
import jadx.core.dex.nodes.IFieldInfoRef;
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
import jadx.core.dex.visitors.prepare.CollectConstValues;
|
||||
|
||||
public class ConstStorage {
|
||||
|
||||
@@ -23,18 +22,18 @@ public class ConstStorage {
|
||||
private final Map<Object, IFieldInfoRef> values = new ConcurrentHashMap<>();
|
||||
private final Set<Object> duplicates = new HashSet<>();
|
||||
|
||||
public Map<Object, IFieldInfoRef> getValues() {
|
||||
Map<Object, IFieldInfoRef> getValues() {
|
||||
return values;
|
||||
}
|
||||
|
||||
public IFieldInfoRef get(Object key) {
|
||||
IFieldInfoRef get(Object key) {
|
||||
return values.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if this value is duplicated
|
||||
*/
|
||||
public boolean put(Object value, IFieldInfoRef fld) {
|
||||
boolean put(Object value, IFieldInfoRef fld) {
|
||||
if (duplicates.contains(value)) {
|
||||
values.remove(value);
|
||||
return true;
|
||||
@@ -85,14 +84,6 @@ public class ConstStorage {
|
||||
globalValues.put(value, fld);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use method from CollectConstValues class
|
||||
*/
|
||||
@Deprecated
|
||||
public static @Nullable Object getFieldConstValue(FieldNode fld) {
|
||||
return CollectConstValues.getFieldConstValue(fld);
|
||||
}
|
||||
|
||||
public void removeForClass(ClassNode cls) {
|
||||
classes.remove(cls);
|
||||
globalValues.removeForCls(cls);
|
||||
|
||||
@@ -35,6 +35,6 @@ public final class ConstStringNode extends InsnNode {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString() + ' ' + StringUtils.getInstance().unescapeString(str);
|
||||
return super.baseString() + StringUtils.getInstance().unescapeString(str) + super.attributesString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,12 @@ public class InsnDecoder {
|
||||
rawInsn.decode();
|
||||
insn = decode(rawInsn);
|
||||
} catch (Exception e) {
|
||||
boolean mthWithErrors = method.contains(AType.JADX_ERROR);
|
||||
method.addError("Failed to decode insn: " + rawInsn, e);
|
||||
if (mthWithErrors) {
|
||||
// second error in this method => abort processing
|
||||
throw new JadxRuntimeException("Failed to decode insn: " + rawInsn, e);
|
||||
}
|
||||
insn = new InsnNode(InsnType.NOP, 0);
|
||||
insn.addAttr(AType.JADX_ERROR, new JadxError("decode failed: " + e.getMessage(), e));
|
||||
}
|
||||
@@ -343,6 +348,7 @@ public class InsnDecoder {
|
||||
return insn(InsnType.THROW, null, InsnArg.reg(insn, 0, ArgType.THROWABLE));
|
||||
|
||||
case MOVE_EXCEPTION:
|
||||
method.add(AFlag.COMPUTE_POST_DOM); // Post dominators required for try/catch block processing
|
||||
return insn(InsnType.MOVE_EXCEPTION, InsnArg.reg(insn, 0, ArgType.UNKNOWN_OBJECT_NO_ARRAY));
|
||||
|
||||
case RETURN_VOID:
|
||||
@@ -478,7 +484,7 @@ public class InsnDecoder {
|
||||
case FILL_ARRAY_DATA:
|
||||
return new FillArrayInsn(InsnArg.reg(insn, 0, ArgType.UNKNOWN_ARRAY), insn.getTarget());
|
||||
case FILL_ARRAY_DATA_PAYLOAD:
|
||||
return new FillArrayData(((IArrayPayload) Objects.requireNonNull(insn.getPayload())));
|
||||
return new FillArrayData((IArrayPayload) Objects.requireNonNull(insn.getPayload()));
|
||||
|
||||
case FILLED_NEW_ARRAY:
|
||||
return filledNewArray(insn, false);
|
||||
@@ -492,7 +498,7 @@ public class InsnDecoder {
|
||||
|
||||
case PACKED_SWITCH_PAYLOAD:
|
||||
case SPARSE_SWITCH_PAYLOAD:
|
||||
return new SwitchData(((ISwitchPayload) insn.getPayload()));
|
||||
return new SwitchData((ISwitchPayload) insn.getPayload());
|
||||
|
||||
case MONITOR_ENTER:
|
||||
return insn(InsnType.MONITOR_ENTER,
|
||||
@@ -510,7 +516,7 @@ public class InsnDecoder {
|
||||
}
|
||||
|
||||
private SwitchInsn makeSwitch(InsnData insn, boolean packed) {
|
||||
SwitchInsn swInsn = new SwitchInsn(InsnArg.reg(insn, 0, ArgType.UNKNOWN), insn.getTarget(), packed);
|
||||
SwitchInsn swInsn = new SwitchInsn(InsnArg.reg(insn, 0, ArgType.NARROW_INTEGRAL), insn.getTarget(), packed);
|
||||
ICustomPayload payload = insn.getPayload();
|
||||
if (payload != null) {
|
||||
swInsn.attachSwitchData(new SwitchData((ISwitchPayload) payload), insn.getTarget());
|
||||
@@ -522,18 +528,10 @@ public class InsnDecoder {
|
||||
|
||||
private InsnNode makeNewArray(InsnData insn) {
|
||||
ArgType indexType = ArgType.parse(insn.getIndexAsType());
|
||||
// NEW_ARRAY literal = dimensions to wrap the operand by: 0 if it is already the full array type
|
||||
// (dalvik new-array, java multianewarray), 1 for newarray/anewarray (operand is the element type)
|
||||
int dim = (int) insn.getLiteral();
|
||||
ArgType arrType;
|
||||
if (dim == 0) {
|
||||
arrType = indexType;
|
||||
} else {
|
||||
if (indexType.isArray()) {
|
||||
// java bytecode can pass array as a base type
|
||||
arrType = indexType;
|
||||
} else {
|
||||
arrType = ArgType.array(indexType, dim);
|
||||
}
|
||||
}
|
||||
ArgType arrType = dim == 0 ? indexType : ArgType.array(indexType, dim);
|
||||
int regsCount = insn.getRegsCount();
|
||||
NewArrayNode newArr = new NewArrayNode(arrType, regsCount - 1);
|
||||
newArr.setResult(InsnArg.reg(insn, 0, arrType));
|
||||
|
||||
@@ -12,6 +12,7 @@ import jadx.core.dex.instructions.args.InsnArg;
|
||||
import jadx.core.dex.instructions.args.RegisterArg;
|
||||
import jadx.core.dex.instructions.args.SSAVar;
|
||||
import jadx.core.dex.nodes.BlockNode;
|
||||
import jadx.core.dex.nodes.IBlock;
|
||||
import jadx.core.dex.nodes.InsnNode;
|
||||
import jadx.core.utils.InsnRemover;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
@@ -69,6 +70,15 @@ public final class PhiInsn extends InsnNode {
|
||||
return (RegisterArg) super.getArg(n);
|
||||
}
|
||||
|
||||
public @Nullable RegisterArg getArgByBlock(BlockNode block) {
|
||||
for (int i = 0; i < blockBinds.size(); i++) {
|
||||
if (blockBinds.get(i) == block) {
|
||||
return getArg(i);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeArg(InsnArg arg) {
|
||||
int index = getArgIndex(arg);
|
||||
@@ -101,6 +111,18 @@ public final class PhiInsn extends InsnNode {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public RegisterArg getArgByBlock(IBlock block) {
|
||||
if (getArgsCount() == 0) {
|
||||
return null;
|
||||
}
|
||||
int index = blockBinds.indexOf(block);
|
||||
if (index == -1) {
|
||||
return null;
|
||||
}
|
||||
return getArg(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean replaceArg(InsnArg from, InsnArg to) {
|
||||
if (!(from instanceof RegisterArg) || !(to instanceof RegisterArg)) {
|
||||
|
||||
@@ -10,6 +10,8 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.TestOnly;
|
||||
|
||||
import com.google.errorprone.annotations.Immutable;
|
||||
|
||||
import jadx.core.Consts;
|
||||
import jadx.core.dex.info.ClassInfo;
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
@@ -18,6 +20,7 @@ import jadx.core.utils.ListUtils;
|
||||
import jadx.core.utils.Utils;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
|
||||
@Immutable
|
||||
public abstract class ArgType {
|
||||
public static final ArgType INT = primitive(PrimitiveType.INT);
|
||||
public static final ArgType BOOLEAN = primitive(PrimitiveType.BOOLEAN);
|
||||
@@ -61,6 +64,9 @@ public abstract class ArgType {
|
||||
PrimitiveType.INT, PrimitiveType.FLOAT,
|
||||
PrimitiveType.SHORT, PrimitiveType.BYTE, PrimitiveType.CHAR);
|
||||
|
||||
public static final ArgType NARROW_NEG_NUMBERS = unknown(
|
||||
PrimitiveType.INT, PrimitiveType.SHORT, PrimitiveType.BYTE, PrimitiveType.FLOAT);
|
||||
|
||||
public static final ArgType NARROW_NUMBERS_NO_FLOAT = unknown(
|
||||
PrimitiveType.INT, PrimitiveType.BOOLEAN,
|
||||
PrimitiveType.SHORT, PrimitiveType.BYTE, PrimitiveType.CHAR);
|
||||
@@ -197,7 +203,7 @@ public abstract class ArgType {
|
||||
private static final class PrimitiveArg extends KnownType {
|
||||
private final PrimitiveType type;
|
||||
|
||||
public PrimitiveArg(PrimitiveType type) {
|
||||
PrimitiveArg(PrimitiveType type) {
|
||||
this.type = type;
|
||||
this.hash = type.hashCode();
|
||||
}
|
||||
@@ -226,7 +232,7 @@ public abstract class ArgType {
|
||||
private static class ObjectType extends KnownType {
|
||||
protected final String objName;
|
||||
|
||||
public ObjectType(String obj) {
|
||||
ObjectType(String obj) {
|
||||
this.objName = obj;
|
||||
this.hash = objName.hashCode();
|
||||
}
|
||||
@@ -260,15 +266,15 @@ public abstract class ArgType {
|
||||
private static final class GenericType extends ObjectType {
|
||||
private List<ArgType> extendTypes;
|
||||
|
||||
public GenericType(String obj) {
|
||||
GenericType(String obj) {
|
||||
this(obj, Collections.emptyList());
|
||||
}
|
||||
|
||||
public GenericType(String obj, ArgType extendType) {
|
||||
GenericType(String obj, ArgType extendType) {
|
||||
this(obj, Collections.singletonList(extendType));
|
||||
}
|
||||
|
||||
public GenericType(String obj, List<ArgType> extendTypes) {
|
||||
GenericType(String obj, List<ArgType> extendTypes) {
|
||||
super(obj);
|
||||
this.extendTypes = extendTypes;
|
||||
}
|
||||
@@ -334,7 +340,7 @@ public abstract class ArgType {
|
||||
private final ArgType type;
|
||||
private final WildcardBound bound;
|
||||
|
||||
public WildcardType(ArgType obj, WildcardBound bound) {
|
||||
WildcardType(ArgType obj, WildcardBound bound) {
|
||||
super(OBJECT.getObject());
|
||||
this.type = Objects.requireNonNull(obj);
|
||||
this.bound = Objects.requireNonNull(bound);
|
||||
@@ -379,7 +385,7 @@ public abstract class ArgType {
|
||||
private static class GenericObject extends ObjectType {
|
||||
private final List<ArgType> generics;
|
||||
|
||||
public GenericObject(String obj, List<ArgType> generics) {
|
||||
GenericObject(String obj, List<ArgType> generics) {
|
||||
super(obj);
|
||||
this.generics = Objects.requireNonNull(generics);
|
||||
this.hash = calcHash();
|
||||
@@ -415,7 +421,7 @@ public abstract class ArgType {
|
||||
private final ObjectType outerType;
|
||||
private final ObjectType innerType;
|
||||
|
||||
public OuterGenericObject(ObjectType outerType, ObjectType innerType) {
|
||||
OuterGenericObject(ObjectType outerType, ObjectType innerType) {
|
||||
super(outerType.getObject() + '$' + innerType.getObject());
|
||||
this.outerType = outerType;
|
||||
this.innerType = innerType;
|
||||
@@ -463,7 +469,7 @@ public abstract class ArgType {
|
||||
private static final PrimitiveType[] ARRAY_POSSIBLES = new PrimitiveType[] { PrimitiveType.ARRAY };
|
||||
private final ArgType arrayElement;
|
||||
|
||||
public ArrayArg(ArgType arrayElement) {
|
||||
ArrayArg(ArgType arrayElement) {
|
||||
this.arrayElement = arrayElement;
|
||||
this.hash = arrayElement.hashCode();
|
||||
}
|
||||
@@ -523,7 +529,7 @@ public abstract class ArgType {
|
||||
private static final class UnknownArg extends ArgType {
|
||||
private final PrimitiveType[] possibleTypes;
|
||||
|
||||
public UnknownArg(PrimitiveType[] types) {
|
||||
UnknownArg(PrimitiveType[] types) {
|
||||
this.possibleTypes = types;
|
||||
this.hash = Arrays.hashCode(possibleTypes);
|
||||
}
|
||||
@@ -746,6 +752,9 @@ public abstract class ArgType {
|
||||
case '[':
|
||||
return array(parse(type.substring(1)));
|
||||
default:
|
||||
if (type.length() != 1) {
|
||||
throw new JadxRuntimeException("Unknown type string: \"" + type + '"');
|
||||
}
|
||||
return parse(f);
|
||||
}
|
||||
}
|
||||
@@ -772,7 +781,7 @@ public abstract class ArgType {
|
||||
return VOID;
|
||||
|
||||
default:
|
||||
return null;
|
||||
throw new JadxRuntimeException("Unknown type char: '" + f + "' (0x" + Integer.toHexString(f) + ')');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -131,6 +131,7 @@ public abstract class InsnArg extends Typed {
|
||||
}
|
||||
}
|
||||
}
|
||||
RegisterArg resArg = insn.getResult();
|
||||
InsnArg arg = wrapInsnIntoArg(insn);
|
||||
InsnArg oldArg = parent.getArg(i);
|
||||
if (arg.getType() == ArgType.UNKNOWN) {
|
||||
@@ -141,6 +142,8 @@ public abstract class InsnArg extends Typed {
|
||||
InsnRemover.unbindArgUsage(mth, oldArg);
|
||||
if (unbind) {
|
||||
InsnRemover.unbindArgUsage(mth, this);
|
||||
}
|
||||
if (resArg != null && !insn.contains(AFlag.FORCE_ASSIGN_INLINE)) {
|
||||
// result not needed in wrapped insn
|
||||
InsnRemover.unbindResult(mth, insn);
|
||||
insn.setResult(null);
|
||||
@@ -292,6 +295,17 @@ public abstract class InsnArg extends Typed {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isSameVar(SSAVar ssaVar) {
|
||||
if (ssaVar == null) {
|
||||
return false;
|
||||
}
|
||||
if (isRegister()) {
|
||||
SSAVar thisSsaVar = ((RegisterArg) this).getSVar();
|
||||
return Objects.equals(thisSsaVar, ssaVar);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isSameCodeVar(RegisterArg arg) {
|
||||
if (arg == null) {
|
||||
return false;
|
||||
@@ -312,9 +326,7 @@ public abstract class InsnArg extends Typed {
|
||||
return copy;
|
||||
}
|
||||
|
||||
public InsnArg duplicate() {
|
||||
return this;
|
||||
}
|
||||
public abstract InsnArg duplicate();
|
||||
|
||||
public String toShortString() {
|
||||
return this.toString();
|
||||
|
||||
@@ -41,7 +41,14 @@ public final class InsnWrapArg extends InsnArg {
|
||||
|
||||
@Override
|
||||
public InsnArg duplicate() {
|
||||
InsnWrapArg copy = new InsnWrapArg(wrappedInsn.copyWithoutResult());
|
||||
InsnNode wrapInsn = wrappedInsn;
|
||||
InsnNode wrapInsnCopy = wrapInsn.copyWithoutResult();
|
||||
if (wrapInsn.getResult() != null && wrapInsn.contains(AFlag.FORCE_ASSIGN_INLINE)) {
|
||||
// keep same SSA var in result arg, this will break previous version, mark it for removal
|
||||
wrapInsnCopy.setResult(wrapInsn.getResult().duplicate());
|
||||
wrapInsn.add(AFlag.DONT_GENERATE);
|
||||
}
|
||||
InsnWrapArg copy = new InsnWrapArg(wrapInsnCopy);
|
||||
copy.setType(type);
|
||||
return copyCommonParams(copy);
|
||||
}
|
||||
@@ -84,7 +91,7 @@ public final class InsnWrapArg extends InsnArg {
|
||||
if (wrappedInsn.getType() == InsnType.CONST_STR) {
|
||||
return "(\"" + ((ConstStringNode) wrappedInsn).getString() + "\")";
|
||||
}
|
||||
return "(wrap:" + type + ":" + wrappedInsn.getType() + ')';
|
||||
return "(wrap " + type + ":" + wrappedInsn.getType() + ')';
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -92,6 +99,6 @@ public final class InsnWrapArg extends InsnArg {
|
||||
if (wrappedInsn.getType() == InsnType.CONST_STR) {
|
||||
return "(\"" + ((ConstStringNode) wrappedInsn).getString() + "\")";
|
||||
}
|
||||
return "(wrap:" + type + ":" + wrappedInsn + ')';
|
||||
return "(wrap " + type + ":" + wrappedInsn + ')';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,9 @@ public final class LiteralArg extends InsnArg {
|
||||
if (value == 1) {
|
||||
return ArgType.NARROW_NUMBERS;
|
||||
}
|
||||
if (value < 0) {
|
||||
return ArgType.NARROW_NEG_NUMBERS;
|
||||
}
|
||||
return ArgType.NARROW_NUMBERS_NO_BOOL;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ import org.slf4j.LoggerFactory;
|
||||
import jadx.api.DecompilationMode;
|
||||
import jadx.api.ICodeCache;
|
||||
import jadx.api.ICodeInfo;
|
||||
import jadx.api.JadxArgs;
|
||||
import jadx.api.JavaClass;
|
||||
import jadx.api.impl.SimpleCodeInfo;
|
||||
import jadx.api.impl.SimpleCodeWriter;
|
||||
@@ -38,8 +37,6 @@ import jadx.api.plugins.input.data.attributes.types.SourceFileAttr;
|
||||
import jadx.api.plugins.input.data.impl.ListConsumer;
|
||||
import jadx.api.usage.IUsageInfoData;
|
||||
import jadx.core.Consts;
|
||||
import jadx.core.Jadx;
|
||||
import jadx.core.ProcessClass;
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.attributes.AType;
|
||||
import jadx.core.dex.attributes.nodes.InlinedAttr;
|
||||
@@ -72,6 +69,7 @@ public class ClassNode extends NotificationAttrNode
|
||||
private ArgType superClass;
|
||||
private List<ArgType> interfaces;
|
||||
private List<ArgType> generics = Collections.emptyList();
|
||||
private String inputFileName;
|
||||
|
||||
private List<MethodNode> methods;
|
||||
private List<FieldNode> fields;
|
||||
@@ -123,6 +121,7 @@ public class ClassNode extends NotificationAttrNode
|
||||
this.accessFlags = new AccessInfo(getAccessFlags(cls), AFType.CLASS);
|
||||
this.superClass = checkSuperType(cls);
|
||||
this.interfaces = Utils.collectionMap(cls.getInterfacesTypes(), ArgType::object);
|
||||
setInputFileName(cls.getInputFileName());
|
||||
|
||||
ListConsumer<IFieldData, FieldNode> fieldsConsumer = new ListConsumer<>(fld -> FieldNode.build(this, fld));
|
||||
ListConsumer<IMethodData, MethodNode> methodsConsumer = new ListConsumer<>(mth -> MethodNode.build(this, mth));
|
||||
@@ -228,6 +227,7 @@ public class ClassNode extends NotificationAttrNode
|
||||
public static ClassNode addSyntheticClass(RootNode root, ClassInfo clsInfo, int accessFlags) {
|
||||
ClassNode cls = new ClassNode(root, clsInfo, accessFlags);
|
||||
cls.add(AFlag.SYNTHETIC);
|
||||
cls.setInputFileName("synthetic");
|
||||
cls.setState(ProcessState.PROCESS_COMPLETE);
|
||||
root.addClassNode(cls);
|
||||
return cls;
|
||||
@@ -317,23 +317,25 @@ public class ClassNode extends NotificationAttrNode
|
||||
* WARNING: Slow operation! Use with caution!
|
||||
*/
|
||||
public ICodeInfo decompileWithMode(DecompilationMode mode) {
|
||||
DecompilationMode baseMode = root.getArgs().getDecompilationMode();
|
||||
if (mode == baseMode) {
|
||||
return decompile(true);
|
||||
}
|
||||
synchronized (DECOMPILE_WITH_MODE_SYNC) {
|
||||
JadxArgs args = root.getArgs();
|
||||
try {
|
||||
unload();
|
||||
args.setDecompilationMode(mode);
|
||||
ProcessClass process = new ProcessClass(Jadx.getPassesList(args));
|
||||
process.initPasses(root);
|
||||
ICodeInfo code = process.forceGenerateCode(this);
|
||||
return Utils.getOrElse(code, ICodeInfo.EMPTY);
|
||||
} finally {
|
||||
args.setDecompilationMode(baseMode);
|
||||
unload();
|
||||
}
|
||||
switch (mode) {
|
||||
case AUTO:
|
||||
case RESTRUCTURE:
|
||||
return decompile(true);
|
||||
|
||||
case SIMPLE:
|
||||
case FALLBACK:
|
||||
synchronized (DECOMPILE_WITH_MODE_SYNC) {
|
||||
try {
|
||||
unload();
|
||||
ICodeInfo code = root.getProcessClasses().forceGenerateCodeForMode(this, mode);
|
||||
return Utils.getOrElse(code, ICodeInfo.EMPTY);
|
||||
} finally {
|
||||
unload();
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
throw new JadxRuntimeException("Unknown mode: " + mode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -403,7 +405,7 @@ public class ClassNode extends NotificationAttrNode
|
||||
ICodeInfo codeInfo = root.getProcessClasses().generateCode(this);
|
||||
processDefinitionAnnotations(codeInfo);
|
||||
return codeInfo;
|
||||
} catch (Throwable e) {
|
||||
} catch (StackOverflowError | Exception e) {
|
||||
addError("Code generation failed", e);
|
||||
return new SimpleCodeInfo(Utils.getStackTrace(e));
|
||||
}
|
||||
@@ -612,17 +614,9 @@ public class ClassNode extends NotificationAttrNode
|
||||
return parentClass;
|
||||
}
|
||||
|
||||
public void updateParentClass() {
|
||||
if (clsInfo.isInner()) {
|
||||
ClassNode parent = root.resolveClass(clsInfo.getParentClass());
|
||||
if (parent != null) {
|
||||
parentClass = parent;
|
||||
return;
|
||||
}
|
||||
// undo inner mark in class info
|
||||
clsInfo.notInner(root);
|
||||
}
|
||||
parentClass = this;
|
||||
public void notInner() {
|
||||
this.clsInfo.notInner(root);
|
||||
this.parentClass = this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -632,32 +626,36 @@ public class ClassNode extends NotificationAttrNode
|
||||
*/
|
||||
@Override
|
||||
public void rename(String newName) {
|
||||
int lastDot = newName.lastIndexOf('.');
|
||||
if (lastDot == -1) {
|
||||
if (newName.indexOf('.') == -1) {
|
||||
clsInfo.changeShortName(newName);
|
||||
return;
|
||||
}
|
||||
if (clsInfo.isInner()) {
|
||||
addWarn("Can't change package for inner class: " + this + " to " + newName);
|
||||
return;
|
||||
}
|
||||
// full name provided
|
||||
ClassInfo newClsInfo = ClassInfo.fromNameWithoutCache(root, newName, clsInfo.isInner());
|
||||
// change class package
|
||||
String newPkg = newName.substring(0, lastDot);
|
||||
String newShortName = newName.substring(lastDot + 1);
|
||||
if (changeClassNodePackage(newPkg)) {
|
||||
clsInfo.changePkgAndName(newPkg, newShortName);
|
||||
} else {
|
||||
String newPkg = newClsInfo.getPackage();
|
||||
String newShortName = newClsInfo.getShortName();
|
||||
if (clsInfo.isInner()) {
|
||||
if (!newPkg.equals(clsInfo.getPackage())) {
|
||||
addWarn("Can't change package for inner class: " + this + " to " + newName);
|
||||
}
|
||||
clsInfo.changeShortName(newShortName);
|
||||
} else {
|
||||
if (changeClassNodePackage(newPkg)) {
|
||||
clsInfo.changePkgAndName(newPkg, newShortName);
|
||||
} else {
|
||||
clsInfo.changeShortName(newShortName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean changeClassNodePackage(String fullPkg) {
|
||||
if (clsInfo.isInner()) {
|
||||
throw new JadxRuntimeException("Can't change package for inner class: " + clsInfo);
|
||||
}
|
||||
if (fullPkg.equals(clsInfo.getAliasPkg())) {
|
||||
return false;
|
||||
}
|
||||
if (clsInfo.isInner()) {
|
||||
throw new JadxRuntimeException("Can't change package for inner class: " + clsInfo);
|
||||
}
|
||||
root.removeClsFromPackage(packageNode, this);
|
||||
packageNode = PackageNode.getForClass(root, fullPkg, this);
|
||||
root.sortPackages();
|
||||
@@ -748,6 +746,14 @@ public class ClassNode extends NotificationAttrNode
|
||||
}
|
||||
}
|
||||
|
||||
public void getInnerClassesRecursive(Set<ClassNode> resultClassesSet) {
|
||||
for (ClassNode innerCls : innerClasses) {
|
||||
if (resultClassesSet.add(innerCls)) {
|
||||
innerCls.getInnerAndInlinedClassesRecursive(resultClassesSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void addInnerClass(ClassNode cls) {
|
||||
if (innerClasses.isEmpty()) {
|
||||
innerClasses = new ArrayList<>(5);
|
||||
@@ -878,7 +884,7 @@ public class ClassNode extends NotificationAttrNode
|
||||
code.startLine(String.format("###### Class %s (%s)", getFullName(), getRawName()));
|
||||
try {
|
||||
code.startLine(clsData.getDisassembledCode());
|
||||
} catch (Throwable e) {
|
||||
} catch (Exception e) {
|
||||
code.startLine("Failed to disassemble class:");
|
||||
code.startLine(Utils.getStackTrace(e));
|
||||
}
|
||||
@@ -965,7 +971,11 @@ public class ClassNode extends NotificationAttrNode
|
||||
|
||||
@Override
|
||||
public String getInputFileName() {
|
||||
return clsData == null ? "synthetic" : clsData.getInputFileName();
|
||||
return inputFileName;
|
||||
}
|
||||
|
||||
public void setInputFileName(String inputFileName) {
|
||||
this.inputFileName = inputFileName;
|
||||
}
|
||||
|
||||
public JavaClass getJavaNode() {
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
package jadx.core.dex.nodes;
|
||||
|
||||
public class Edge {
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.attributes.AttrNode;
|
||||
|
||||
public class Edge extends AttrNode {
|
||||
private final BlockNode source;
|
||||
private final BlockNode target;
|
||||
|
||||
public Edge(BlockNode source, BlockNode target) {
|
||||
this(source, target, false);
|
||||
}
|
||||
|
||||
public Edge(BlockNode source, BlockNode target, boolean isSynthetic) {
|
||||
if (isSynthetic) {
|
||||
this.add(AFlag.SYNTHETIC);
|
||||
}
|
||||
|
||||
this.source = source;
|
||||
this.target = target;
|
||||
|
||||
}
|
||||
|
||||
public BlockNode getSource() {
|
||||
@@ -17,6 +29,10 @@ public class Edge {
|
||||
return target;
|
||||
}
|
||||
|
||||
public boolean isSynthetic() {
|
||||
return this.contains(AFlag.SYNTHETIC);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package jadx.core.dex.nodes;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import jadx.core.dex.attributes.AttrNode;
|
||||
@@ -14,7 +14,9 @@ public final class InsnContainer extends AttrNode implements IBlock {
|
||||
private final List<InsnNode> insns;
|
||||
|
||||
public InsnContainer(InsnNode insn) {
|
||||
this.insns = Collections.singletonList(insn);
|
||||
List<InsnNode> list = new ArrayList<>(1);
|
||||
list.add(insn);
|
||||
this.insns = list;
|
||||
}
|
||||
|
||||
public InsnContainer(List<InsnNode> insns) {
|
||||
@@ -28,11 +30,11 @@ public final class InsnContainer extends AttrNode implements IBlock {
|
||||
|
||||
@Override
|
||||
public String baseString() {
|
||||
return Integer.toString(insns.size());
|
||||
return "IC";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "InsnContainer:" + insns.size();
|
||||
return "InsnContainer";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,6 +217,19 @@ public class InsnNode extends LineAttrNode {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isExitEdgeInsn() {
|
||||
switch (getType()) {
|
||||
case RETURN:
|
||||
case THROW:
|
||||
case CONTINUE:
|
||||
case BREAK:
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean canRemoveResult() {
|
||||
switch (getType()) {
|
||||
case INVOKE:
|
||||
@@ -393,6 +406,14 @@ public class InsnNode extends LineAttrNode {
|
||||
&& Objects.equals(arguments, other.arguments);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T extends InsnArg> @Nullable T duplicateArg(@Nullable T arg) {
|
||||
if (arg == null) {
|
||||
return null;
|
||||
}
|
||||
return (T) arg.duplicate();
|
||||
}
|
||||
|
||||
protected final <T extends InsnNode> T copyCommonParams(T copy) {
|
||||
if (copy.getArgsCount() == 0) {
|
||||
for (InsnArg arg : this.getArguments()) {
|
||||
|
||||
@@ -2,8 +2,10 @@ package jadx.core.dex.nodes;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -48,6 +50,7 @@ import static jadx.core.utils.Utils.lockList;
|
||||
|
||||
public class MethodNode extends NotificationAttrNode implements IMethodDetails, ILoadable, ICodeNode, Comparable<MethodNode> {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MethodNode.class);
|
||||
private static final InsnNode[] EMPTY_INSN_ARRAY = new InsnNode[0];
|
||||
|
||||
private final MethodInfo mthInfo;
|
||||
private final ClassNode parentClass;
|
||||
@@ -70,7 +73,7 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
|
||||
// decompilation data, reset on unload
|
||||
private RegisterArg thisArg;
|
||||
private List<RegisterArg> argsList;
|
||||
private InsnNode[] instructions;
|
||||
private @Nullable InsnNode[] instructions;
|
||||
private List<BlockNode> blocks;
|
||||
private int blocksMaxCId;
|
||||
private BlockNode enterBlock;
|
||||
@@ -80,7 +83,14 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
|
||||
private List<LoopInfo> loops;
|
||||
private Region region;
|
||||
|
||||
// Methods that use this method
|
||||
private List<MethodNode> useIn = Collections.emptyList();
|
||||
// Unresolved methods that use this method
|
||||
private List<MethodInfo> unresolvedUsed = Collections.emptyList();
|
||||
// Methods that this method uses
|
||||
private Set<MethodNode> methodsUsed = new HashSet<>();
|
||||
// True if this method contains a self call
|
||||
private boolean callsSelf = false;
|
||||
|
||||
private JavaMethod javaNode;
|
||||
|
||||
@@ -95,11 +105,12 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
|
||||
this.parentClass = classNode;
|
||||
this.accFlags = new AccessInfo(mthData.getAccessFlags(), AFType.METHOD);
|
||||
ICodeReader codeReader = mthData.getCodeReader();
|
||||
this.noCode = codeReader == null;
|
||||
if (noCode) {
|
||||
if (codeReader == null) {
|
||||
this.noCode = true;
|
||||
this.codeReader = null;
|
||||
this.insnsCount = 0;
|
||||
} else {
|
||||
this.noCode = false;
|
||||
this.codeReader = codeReader.copy();
|
||||
this.insnsCount = codeReader.getUnitsCount();
|
||||
}
|
||||
@@ -119,6 +130,7 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
|
||||
sVars = Collections.emptyList();
|
||||
instructions = null;
|
||||
blocks = null;
|
||||
blocksMaxCId = 0;
|
||||
enterBlock = null;
|
||||
exitBlock = null;
|
||||
region = null;
|
||||
@@ -154,8 +166,14 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
|
||||
this.regsCount = codeReader.getRegistersCount();
|
||||
this.argsStartReg = codeReader.getArgsStartReg();
|
||||
initArguments(this.argTypes);
|
||||
InsnDecoder decoder = new InsnDecoder(this);
|
||||
this.instructions = decoder.process(codeReader);
|
||||
|
||||
if (contains(AType.JADX_ERROR)) {
|
||||
// don't load instructions for method with errors
|
||||
this.instructions = EMPTY_INSN_ARRAY;
|
||||
} else {
|
||||
InsnDecoder decoder = new InsnDecoder(this);
|
||||
this.instructions = decoder.process(codeReader);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (!noCode) {
|
||||
unload();
|
||||
@@ -695,12 +713,56 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
|
||||
return codeReader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MethodNode> getUseIn() {
|
||||
return useIn;
|
||||
}
|
||||
|
||||
// Do not modify passed list after setting
|
||||
public void setUseIn(List<MethodNode> useIn) {
|
||||
this.useIn = useIn;
|
||||
|
||||
// Notify all methods (callers) this method (callee) is used in
|
||||
for (MethodNode methodUsedIn : useIn) {
|
||||
methodUsedIn.addUsed(this);
|
||||
}
|
||||
}
|
||||
|
||||
public void addUsed(MethodNode used) {
|
||||
if (used != null) {
|
||||
this.methodsUsed.add(used);
|
||||
}
|
||||
}
|
||||
|
||||
public void setUsed(List<MethodNode> methodsUsed) {
|
||||
this.methodsUsed = new HashSet<>(methodsUsed);
|
||||
}
|
||||
|
||||
public Set<MethodNode> getUsed() {
|
||||
this.removeInvalidMethodsUsed();
|
||||
return methodsUsed;
|
||||
}
|
||||
|
||||
public List<MethodInfo> getUnresolvedUsed() {
|
||||
return unresolvedUsed;
|
||||
}
|
||||
|
||||
public void setUnresolvedUsed(List<MethodInfo> unresolvedUsed) {
|
||||
this.unresolvedUsed = unresolvedUsed;
|
||||
}
|
||||
|
||||
public void setCallsSelf(boolean callsSelf) {
|
||||
this.callsSelf = callsSelf;
|
||||
}
|
||||
|
||||
public boolean callsSelf() {
|
||||
return this.callsSelf;
|
||||
}
|
||||
|
||||
// Remove any methods from the list of used methods (calees) if this method (caller) has been
|
||||
// removed from the calee's list of callers
|
||||
private void removeInvalidMethodsUsed() {
|
||||
methodsUsed.removeIf(methodUsed -> !methodUsed.getUseIn().contains(this));
|
||||
}
|
||||
|
||||
public JavaMethod getJavaNode() {
|
||||
|
||||
@@ -3,6 +3,7 @@ package jadx.core.dex.nodes;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -188,6 +189,14 @@ public class PackageNode extends LineAttrNode
|
||||
return classes;
|
||||
}
|
||||
|
||||
public List<ClassNode> getClassesNoDup() {
|
||||
return classes.stream()
|
||||
.map(ClassNode::getClassInfo)
|
||||
.collect(Collectors.toSet())
|
||||
.stream()
|
||||
.map(e -> root.resolveClass(e)).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public JavaPackage getJavaNode() {
|
||||
return javaNode;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user