Compare commits

..

1 Commits

Author SHA1 Message Date
Skylot 5b8793155c test: enum used in other field init 2024-02-06 18:34:08 +00:00
1335 changed files with 27415 additions and 62536 deletions
+28 -34
View File
@@ -8,60 +8,56 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up JDK
uses: actions/setup-java@v5
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 25
java-version: 11
- name: Set jadx version
run: |
JADX_REV=$(git rev-list --count HEAD)
JADX_VERSION="r${JADX_REV}.${GITHUB_SHA:0:7}"
JADX_LAST_TAG=$(git describe --abbrev=0 --tags)
JADX_VERSION="${JADX_LAST_TAG:1}.$GITHUB_RUN_NUMBER-${GITHUB_SHA:0:8}"
echo "JADX_VERSION=$JADX_VERSION" >> $GITHUB_ENV
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
- name: Build
run: ./gradlew dist distWin
env:
JADX_BUILD_JAVA_VERSION: 11
- name: Build with Gradle
uses: gradle/actions/setup-gradle@v3
with:
arguments: dist copyExe
- name: Save bundle artifact
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
with:
name: ${{ format('jadx-{0}', env.JADX_VERSION) }}
# Waiting fix for https://github.com/actions/upload-artifact/issues/39 to upload zip file
# Upload unpacked files for now
path: build/jadx/**/*
if-no-files-found: error
retention-days: 14
retention-days: 30
- name: Save Windows bundle artifact
uses: actions/upload-artifact@v6
- name: Save exe artifact
uses: actions/upload-artifact@v4
with:
name: ${{ format('jadx-gui-{0}-no-jre-win', env.JADX_VERSION) }}
# Upload unpacked files for now
path: jadx-gui/build/jadx-gui-win/*
name: ${{ format('jadx-gui-{0}-no-jre-win.exe', env.JADX_VERSION) }}
path: build/*.exe
if-no-files-found: error
retention-days: 14
retention-days: 30
build-win-bundle:
runs-on: windows-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up JDK
uses: oracle-actions/setup-java@v1
with:
release: 25
release: 17
- name: Print Java version
shell: bash
@@ -70,21 +66,19 @@ jobs:
- name: Set jadx version
shell: bash
run: |
JADX_REV=$(git rev-list --count HEAD)
JADX_VERSION="r${JADX_REV}.${GITHUB_SHA:0:7}"
JADX_LAST_TAG=$(git describe --abbrev=0 --tags)
JADX_VERSION="${JADX_LAST_TAG:1}.$GITHUB_RUN_NUMBER-${GITHUB_SHA:0:8}"
echo "JADX_VERSION=$JADX_VERSION" >> $GITHUB_ENV
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
- name: Build with Gradle
uses: gradle/actions/setup-gradle@v3
with:
arguments: dist -PbundleJRE=true
- name: Build
run: ./gradlew dist -PbundleJRE=true
- name: Save Windows with JRE bundle artifact
uses: actions/upload-artifact@v6
- name: Save exe bundle artifact
uses: actions/upload-artifact@v4
with:
name: ${{ format('jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }}
# Upload unpacked files for now
path: jadx-gui/build/jadx-gui-with-jre-win/*
path: jadx-gui/build/*-with-jre-win/*
if-no-files-found: error
retention-days: 14
retention-days: 30
+8 -11
View File
@@ -2,7 +2,7 @@ name: Build Test
on:
push:
branches: [ master, stable, build-test ]
branches: [ master, build-test ]
pull_request:
branches: [ master ]
@@ -14,18 +14,15 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v5
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 25
java-version: 11
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
- name: Build
run: ./gradlew build dist distWin
env:
JADX_BUILD_JAVA_VERSION: 11
- name: Build with Gradle
uses: gradle/actions/setup-gradle@v3
with:
arguments: build dist copyExe
+41
View File
@@ -0,0 +1,41 @@
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@v4
- 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
@@ -0,0 +1,15 @@
name: Validate Gradle Wrapper
on:
push:
branches: [ master, build-test ]
pull_request:
branches: [ master ]
jobs:
validation:
name: Validation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: gradle/wrapper-validation-action@v1
-92
View File
@@ -1,92 +0,0 @@
name: Release
on:
push:
tags:
- "v*.*.*"
# additional permissions for provided GitHub token to create new release
permissions:
contents: write
jobs:
build-release-win-bundle:
runs-on: windows-latest
steps:
- uses: actions/checkout@v6
- name: Set up JDK
uses: oracle-actions/setup-java@v1
with:
release: 25
- name: Set jadx version
uses: actions/github-script@v8
with:
script: |
const jadxVersion = context.ref.split('/').pop().substring(1)
core.exportVariable('JADX_VERSION', jadxVersion);
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
- name: Build
run: ./gradlew dist -PbundleJRE=true
- name: Save JRE bundle artifact
uses: actions/upload-artifact@v6
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) }}
if-no-files-found: error
retention-days: 1
release:
needs: build-release-win-bundle
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Set up JDK
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 25
- name: Set jadx version and release name
uses: actions/github-script@v8
with:
script: |
const jadxVersion = context.ref.split('/').pop().substring(1)
core.exportVariable('JADX_VERSION', jadxVersion);
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
- name: Build
run: ./gradlew dist distWin
env:
JADX_BUILD_JAVA_VERSION: 11
- name: Download Windows JRE bundle
uses: actions/download-artifact@v7
with:
name: ${{ format('jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }}
path: ${{ format('build/jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }}
- run: |
cd build
pwd
ls -l
ls jadx-gui-*-with-jre-win
mv jadx-gui-*-with-jre-win/jadx-gui-*-with-jre-win.zip .
mv distWin/jadx-gui-*-win.zip .
ls -l *.zip
- name: Release
uses: softprops/action-gh-release@v2
with:
name: ${{ env.JADX_VERSION }}
draft: true
fail_on_unmatched_files: true
files: build/jadx-*.zip
-3
View File
@@ -21,7 +21,6 @@ build/
classes/
idea/
.gradle/
.kotlin/
node_modules/
.vscode/
@@ -29,10 +28,8 @@ jadx-output/
*-tmp/
**/tmp/
*.jobf
*.jadx
*.class
*.jar
*.dump
*.log
*.cfg
+12 -2
View File
@@ -8,7 +8,17 @@ before_script:
stages:
- test
build-test:
java-11:
stage: test
image: eclipse-temurin:11
script: ./gradlew clean build dist copyExe
java-17:
stage: test
image: eclipse-temurin:17
script: ./gradlew clean build dist copyExe
java-21:
stage: test
image: eclipse-temurin:21
script: JADX_BUILD_JAVA_VERSION=11 JADX_TEST_JAVA_VERSION=11 ./gradlew clean build dist distWin
script: ./gradlew clean build dist copyExe
+1 -1
View File
@@ -4,7 +4,7 @@
<module name="jadx.jadx-gui.main"/>
<option name="PROGRAM_PARAMETERS" value="-v"/>
<option name="VM_PARAMETERS"
value="-Xms128M -XX:MaxRAMPercentage=70.0 -Dawt.useSystemAAFontSettings=lcd -Dswing.aatext=true -Djava.util.Arrays.useLegacyMergeSort=true -Djdk.util.zip.disableZip64ExtraFieldValidation=true -XX:+IgnoreUnrecognizedVMOptions --add-opens=java.base/java.lang=ALL-UNNAMED --enable-native-access=ALL-UNNAMED -Dsun.java2d.noddraw=true -Dsun.java2d.d3d=false -Dsun.java2d.ddforcevram=true -Dsun.java2d.ddblit=false -Dswing.useflipBufferStrategy=true"/>
value="-Xms128M -XX:MaxRAMPercentage=70.0 -Dawt.useSystemAAFontSettings=lcd -Dswing.aatext=true -Djava.util.Arrays.useLegacyMergeSort=true -Djdk.util.zip.disableZip64ExtraFieldValidation=true -XX:+IgnoreUnrecognizedVMOptions --add-opens=java.base/java.lang=ALL-UNNAMED"/>
<method v="2">
<option name="Make" enabled="true"/>
</method>
+23 -10
View File
@@ -3,7 +3,7 @@
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to make participation in our project and
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
@@ -23,13 +23,13 @@ include:
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
professional setting
## Our Responsibilities
@@ -45,12 +45,25 @@ threatening, offensive, or harmful.
## Scope
This Code of Conduct applies within all project spaces, and it also applies when
an individual is representing the project or its community in public spaces.
Examples of representing a project or community include using an official
project e-mail address, posting via an official social media account, or acting
as an appointed representative at an online or offline event. Representation of
a project may be further defined and clarified by project maintainers.
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at skylot@gmail.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
+103 -157
View File
@@ -8,19 +8,16 @@
![GitHub release (latest by SemVer)](https://img.shields.io/github/downloads/skylot/jadx/latest/total)
![Latest release](https://img.shields.io/github/release/skylot/jadx.svg)
[![Maven Central](https://img.shields.io/maven-central/v/io.github.skylot/jadx-core)](https://search.maven.org/search?q=g:io.github.skylot%20AND%20jadx)
![Java 11+](https://img.shields.io/badge/Java-11%2B-blue)
[![License](http://img.shields.io/:license-apache-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0.html)
**jadx** - Dex to Java decompiler
Command line and GUI tools for producing Java source code from Android Dex and Apk files
> [!WARNING]
> Please note that in most cases **jadx** can't decompile all 100% of the code, so errors will occur.<br />
> Check [Troubleshooting guide](https://github.com/skylot/jadx/wiki/Troubleshooting-Q&A#decompilation-issues) for workarounds.
:exclamation::exclamation::exclamation: Please note that in most cases **jadx** can't decompile all 100% of the code, so errors will occur. Check [Troubleshooting guide](https://github.com/skylot/jadx/wiki/Troubleshooting-Q&A#decompilation-issues) for workarounds
**Main features:**
- decompile Dalvik bytecode to Java code from APK, dex, aar, aab and zip files
- decompile Dalvik bytecode to java classes from APK, dex, aar, aab and zip files
- decode `AndroidManifest.xml` and other resources from `resources.arsc`
- deobfuscator included
@@ -51,22 +48,18 @@ On Windows run `.bat` files with double-click\
For Windows, you can download it from [oracle.com](https://www.oracle.com/java/technologies/downloads/#jdk17-windows) (select x64 Installer).
### Install
- Arch Linux
[![Arch Linux package](https://img.shields.io/archlinux/v/extra/any/jadx)](https://archlinux.org/packages/extra/any/jadx/)
[![AUR Version](https://img.shields.io/aur/version/jadx-git)](https://aur.archlinux.org/packages/jadx-git)
```bash
sudo pacman -S jadx
```
- macOS
[![homebrew version](https://img.shields.io/homebrew/v/jadx)](https://formulae.brew.sh/formula/jadx)
```bash
brew install jadx
```
- Flathub
[![Flathub Version](https://img.shields.io/flathub/v/com.github.skylot.jadx)](https://flathub.org/apps/com.github.skylot.jadx)
```bash
flatpak install flathub com.github.skylot.jadx
```
1. Arch linux ![Arch Linux package](https://img.shields.io/archlinux/v/extra/any/jadx?label=)
```bash
sudo pacman -S jadx
```
2. macOS ![homebrew version](https://img.shields.io/homebrew/v/jadx?label=)
```bash
brew install jadx
```
3. [Flathub ![Flathub](https://img.shields.io/flathub/v/com.github.skylot.jadx?label=)](https://flathub.org/apps/details/com.github.skylot.jadx)
```bash
flatpak install flathub com.github.skylot.jadx
```
### Use jadx as a library
You can use jadx in your java projects, check details on [wiki page](https://github.com/skylot/jadx/wiki/Use-jadx-as-a-library)
@@ -86,133 +79,104 @@ and also packed to `build/jadx-<version>.zip`
### Usage
```
jadx[-gui] [command] [options] <input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk, .apkm, .jadx.kts)
jadx[-gui] [command] [options] <input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk)
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: 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)
-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
-d, --output-dir - output directory
-ds, --output-dir-src - output directory for sources
-dr, --output-dir-res - output directory for resources
-r, --no-res - do not decode resources
-s, --no-src - do not decompile source code
--single-class - decompile a single class, full name, raw or alias
--single-class-output - file or dir for write if decompile a single class
--output-format - can be 'java' or 'json', default: java
-e, --export-gradle - save as android gradle project
-j, --threads-count - processing threads count, default: 4
-m, --decompilation-mode - code output mode:
'auto' - trying best options (default)
'restructure' - restore code structure (normal java code)
'simple' - simplified instructions (linear, with goto's)
'fallback' - raw instructions without modifications
--show-bad-code - show inconsistent code (incorrectly decompiled)
--no-xml-pretty-print - do not prettify XML
--no-imports - disable use of imports, always write entire package name
--no-debug-info - disable debug info parsing and processing
--add-debug-lines - add comments with debug line numbers if available
--no-inline-anonymous - disable anonymous classes inline
--no-inline-methods - disable methods inline
--no-move-inner-classes - disable move inner classes into parent
--no-inline-kotlin-lambda - disable inline for Kotlin lambdas
--no-finally - don't extract finally block
--no-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-use-sourcename - use source file name as class name alias
--deobf-res-name-source - better name source for resources:
'auto' - automatically select best name (default)
'resources' - use resources names
'code' - use R class fields names
--use-kotlin-methods-for-var-names - use kotlin intrinsic methods to rename variables, values: disable, apply, apply-and-hide, default: apply
--rename-flags - fix options (comma-separated list of):
'case' - fix case sensitivity issues (according to --fs-case-sensitive option),
'valid' - rename java identifiers to make them valid,
'printable' - remove non-printable chars from identifiers,
or single 'none' - to disable all renames
or single 'all' - to enable all (default)
--integer-format - how integers are displayed:
'auto' - automatically select (default)
'decimal' - use decimal
'hexadecimal' - use hexadecimal
--fs-case-sensitive - treat filesystem as case sensitive, false by default
--cfg - save methods control flow graph to dot file
--raw-cfg - save methods control flow graph (use raw instructions)
-f, --fallback - set '--decompilation-mode' to 'fallback' (deprecated)
--use-dx - use dx/d8 to convert java bytecode
--comments-level - set code comments level, values: error, warn, info, debug, user-only, none, default: info
--log-level - set log level, values: quiet, progress, error, warn, info, debug, default: progress
-v, --verbose - verbose output (set --log-level to DEBUG)
-q, --quiet - turn off output (set --log-level to QUIET)
--version - print jadx version
-h, --help - print this help
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
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
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-smap: Use kotlin.SourceDebugExtension annotation for rename class alias
- kotlin-smap.class-alias-source-dbg - rename class alias from SourceDebugExtension, values: [yes, no], default: no
rename-mappings: various mappings support
- rename-mappings.format - mapping format, values: [AUTO, TINY_FILE, TINY_2_FILE, ENIGMA_FILE, ENIGMA_DIR, 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
1) dex-input: Load .dex and .apk files
- dex-input.verify-checksum - verify dex file checksum before load, values: [yes, no], default: yes
2) java-convert: Convert .class, .jar and .aar files to dex
- java-convert.mode - convert mode, values: [dx, d8, both], default: both
- java-convert.d8-desugar - use desugar in d8, values: [yes, no], default: no
3) 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
4) rename-mappings: various mappings support
- rename-mappings.format - mapping format, values: [auto, TINY, TINY_2, ENIGMA, ENIGMA_DIR, MCP, SRG, TSRG, TSRG2, PROGUARD], default: auto
- rename-mappings.invert - invert mapping, values: [yes, no], default: no
Environment variables:
JADX_DISABLE_XML_SECURITY - set to 'true' to disable all security checks for XML files
JADX_DISABLE_ZIP_SECURITY - set to 'true' to disable all security checks for zip files
JADX_ZIP_MAX_ENTRIES_COUNT - maximum allowed number of entries in zip files (default: 100 000)
JADX_CONFIG_DIR - custom config directory, using system by default
JADX_CACHE_DIR - custom cache directory, using system by default
JADX_TMP_DIR - custom temp directory, using system by default
Examples:
@@ -222,25 +186,7 @@ Examples:
jadx --log-level ERROR app.apk
jadx -Pdex-input.verify-checksum=no app.apk
```
These options also work in jadx-gui running from command line and override options from preferences' dialog
Usage for `plugins` command
```
usage: plugins [options]
options:
-i, --install <locationId> - install plugin with locationId
-j, --install-jar <path-to.jar> - install plugin from jar file
-l, --list - list installed plugins
-a, --available - list available plugins from jadx-plugins-list (aka marketplace)
-u, --update - update installed plugins
--uninstall <pluginId> - uninstall plugin with pluginId
--disable <pluginId> - disable plugin with pluginId
--enable <pluginId> - enable plugin with pluginId
--list-all - list all plugins including bundled and dropins
--list-versions <locationId> - fetch latest versions of plugin from locationId (will download all artefacts, limited to 10)
-h, --help - print this help
```
These options also worked on jadx-gui running from command line and override options from preferences dialog
### Troubleshooting
Please check wiki page [Troubleshooting Q&A](https://github.com/skylot/jadx/wiki/Troubleshooting-Q&A)
+3 -4
View File
@@ -2,7 +2,6 @@
## Reporting a Vulnerability
To report a security issue, please open a [new security advisory](https://github.com/skylot/jadx/security/advisories/new).
Please fill the steps you took to create the issue, affected versions, and, if known, mitigations for the issue.
We will check and respond within 3 working days.
If the issue is confirmed as a vulnerability, we will apply required mitigations at the next release.
To report a security issue, please email `skylot@gmail.com` with a description of the issue, the steps you took to create the issue, affected versions, and, if known, mitigations for the issue.
We will check and respond within 3 working days. If the issue is confirmed as a vulnerability, we will apply required mitigations at the next release.
This project follows a 90 day disclosure timeline.
+26 -42
View File
@@ -6,8 +6,8 @@ import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
import java.util.Locale
plugins {
id("com.github.ben-manes.versions") version "0.53.0"
id("se.patrikerdes.use-latest-versions") version "0.2.19"
id("com.github.ben-manes.versions") version "0.51.0"
id("se.patrikerdes.use-latest-versions") version "0.2.18"
id("com.diffplug.spotless") version "6.25.0"
}
@@ -15,18 +15,6 @@ val jadxVersion by extra { System.getenv("JADX_VERSION") ?: "dev" }
println("jadx version: $jadxVersion")
version = jadxVersion
val jadxBuildJavaVersion by extra { getBuildJavaVersion() }
fun getBuildJavaVersion(): Int? {
val envVarName = "JADX_BUILD_JAVA_VERSION"
val buildJavaVer = System.getenv(envVarName)?.toInt() ?: return null
if (buildJavaVer < 11) {
throw GradleException("'$envVarName' can't be set to lower than 11")
}
println("Set Java toolchain for jadx build to version '$buildJavaVer'")
return buildJavaVer
}
allprojects {
apply(plugin = "java")
apply(plugin = "checkstyle")
@@ -82,17 +70,6 @@ fun isNonStable(version: String): Boolean {
return isStable.not()
}
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")) {
@@ -112,10 +89,6 @@ val copyArtifacts by tasks.registering(Copy::class) {
include("**/*.jar")
rename("jadx-gui-(.*)-all.jar", "jadx-$1-all.jar")
}
from(layout.projectDirectory) {
include("README.md")
include("LICENSE")
}
into(layout.buildDirectory.dir("jadx"))
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
@@ -126,30 +99,40 @@ val pack by tasks.registering(Zip::class) {
destinationDirectory.set(layout.buildDirectory)
}
val distWin by tasks.registering(Zip::class) {
val copyExe by tasks.registering(Copy::class) {
group = "jadx"
description = "Build Windows bundle"
description = "Copy exe to build dir"
from(distWinConfiguration)
// next task dependencies not needed, but gradle throws warning because of same output dir
mustRunAfter("jar")
mustRunAfter(pack)
destinationDirectory.set(layout.buildDirectory.dir("distWin"))
archiveFileName.set("jadx-gui-$jadxVersion-win.zip")
from(tasks.getByPath("jadx-gui:createExe"))
include("*.exe")
into(layout.buildDirectory)
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
val distWinWithJre by tasks.registering(Zip::class) {
description = "Build Windows with JRE bundle"
val distWinBundle by tasks.registering(Copy::class) {
group = "jadx"
description = "Copy bundle to build dir"
from(distWinWithJreConfiguration)
dependsOn(tasks.getByPath(":jadx-gui:distWinWithJre"))
destinationDirectory.set(layout.buildDirectory.dir("distWinWithJre"))
archiveFileName.set("jadx-gui-$jadxVersion-with-jre-win.zip")
// next task dependencies not needed, but gradle throws warning because of same output dir
mustRunAfter("jar")
mustRunAfter(pack)
from(tasks.getByPath("jadx-gui:distWinWithJre").outputs) {
include("*.zip")
}
into(layout.buildDirectory)
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
val dist by tasks.registering {
group = "jadx"
description = "Build jadx distribution zip bundles"
description = "Build jadx distribution zip"
dependsOn(pack)
@@ -157,14 +140,15 @@ val dist by tasks.registering {
if (os.isWindows) {
if (project.hasProperty("bundleJRE")) {
println("Build win bundle with JRE")
dependsOn(distWinWithJre)
dependsOn(distWinBundle)
} else {
dependsOn(distWin)
dependsOn(copyExe)
}
}
}
val cleanBuildDir by tasks.registering(Delete::class) {
group = "jadx"
delete(layout.buildDirectory)
}
tasks.getByName("clean").dependsOn(cleanBuildDir)
+1 -3
View File
@@ -3,9 +3,7 @@ plugins {
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.10")
implementation("org.openrewrite:plugin:6.19.1")
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22")
}
repositories {
+8 -15
View File
@@ -3,27 +3,26 @@ import org.gradle.api.tasks.testing.logging.TestExceptionFormat
plugins {
java
checkstyle
id("jadx-rewrite")
}
val jadxVersion: String by rootProject.extra
val jadxBuildJavaVersion: Int? 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.11")
compileOnly("org.jetbrains:annotations:24.1.0")
testImplementation("ch.qos.logback:logback-classic:1.5.22")
testImplementation("org.assertj:assertj-core:3.27.6")
testImplementation("ch.qos.logback:logback-classic:1.4.14")
testImplementation("org.hamcrest:hamcrest-library:2.2")
testImplementation("org.mockito:mockito-core:5.10.0")
testImplementation("org.assertj:assertj-core:3.25.2")
testImplementation("org.junit.jupiter:junit-jupiter:5.13.3")
testImplementation("org.junit.jupiter:junit-jupiter:5.10.1")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testCompileOnly("org.jetbrains:annotations:26.0.2")
testCompileOnly("org.jetbrains:annotations:24.1.0")
}
repositories {
@@ -33,11 +32,6 @@ repositories {
}
java {
jadxBuildJavaVersion?.let { buildJavaVer ->
toolchain {
languageVersion = JavaLanguageVersion.of(buildJavaVer)
}
}
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
@@ -45,7 +39,6 @@ java {
tasks {
compileJava {
options.encoding = "UTF-8"
// options.compilerArgs = listOf("-Xlint:deprecation")
}
jar {
manifest {
@@ -5,11 +5,6 @@ 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)
@@ -30,7 +30,7 @@ publishing {
}
pom {
name.set(project.name)
description.set(project.description ?: "Dex to Java decompiler")
description.set("Dex to Java decompiler")
url.set("https://github.com/skylot/jadx")
licenses {
license {
@@ -42,14 +42,14 @@ publishing {
developer {
id.set("skylot")
name.set("Skylot")
email.set(project.properties["libEmail"].toString())
email.set("skylot@gmail.com")
url.set("https://github.com/skylot")
}
}
scm {
connection.set("scm:git:git://github.com/skylot/jadx.git")
connection .set("scm:git:git://github.com/skylot/jadx.git")
developerConnection.set("scm:git:ssh://github.com:skylot/jadx.git")
url.set("https://github.com/skylot/jadx")
url .set("https://github.com/skylot/jadx")
}
}
}
@@ -1,35 +0,0 @@
plugins {
id("org.openrewrite.rewrite")
}
repositories {
mavenCentral()
}
dependencies {
rewrite("org.openrewrite.recipe:rewrite-testing-frameworks:3.24.0")
rewrite("org.openrewrite.recipe:rewrite-logging-frameworks:3.20.0")
rewrite("org.openrewrite.recipe:rewrite-migrate-java:3.24.0")
rewrite("org.openrewrite.recipe:rewrite-static-analysis:2.24.0")
}
tasks {
rewrite {
// exclusion("src/test/java/jadx/tests/integration")
// activeRecipe("org.openrewrite.java.migrate.Java8toJava11")
// checkstyle auto fix
// activeRecipe("org.openrewrite.staticanalysis.CodeCleanup")
// setCheckstyleConfigFile(file("$rootDir/config/checkstyle/checkstyle.xml"))
// logging
// activeRecipe("org.openrewrite.java.logging.slf4j.Slf4jBestPractices")
// activeRecipe("org.openrewrite.java.logging.slf4j.LoggersNamedForEnclosingClass")
// activeRecipe("org.openrewrite.java.logging.slf4j.ParameterizedLogging")
// activeRecipe("org.openrewrite.java.logging.PrintStackTraceToLogError")
// testing
activeRecipe("org.openrewrite.java.testing.assertj.Assertj")
}
}
+2 -9
View File
@@ -120,22 +120,15 @@
<module name="SuppressWarningsHolder"/>
<module name="IllegalType">
<property name="illegalClassNames" value="java.util.ArrayList, java.util.HashMap, java.util.HashSet,
java.util.LinkedHashMap, java.util.LinkedHashSet, java.util.TreeMap, java.util.TreeSet"/>
</module>
<module name="IllegalType"/>
<module name="IllegalImport">
<property name="illegalClasses" value="jadx.core.utils.DebugUtils"/>
<!-- don't use nullable annotations from RxJava -->
<property name="illegalClasses" value="io.reactivex.rxjava3.annotations.NonNull"/>
<property name="illegalClasses" value="io.reactivex.rxjava3.annotations.Nullable"/>
</module>
<module name="RegexpSinglelineJava">
<property name="id" value="printstacktrace"/>
<property name="format" value="\.printStackTrace\(\)"/>
<property name="ignoreComments" value="true"/>
<property name="message"
value="Using Throwable.printStackTrace() is forbidden. Use logger to print exception"/>
<property name="message" value="Using Throwable.printStackTrace() is forbidden. Use logger to print exception"/>
</module>
</module>
+4 -2
View File
@@ -1,7 +1,7 @@
/*
* Generated on 11/22/21, 8:58 PM
*/
package jadx.gui.ui.codearea;
package jadx.gui.ui.codeearea;
import java.io.*;
import javax.swing.text.Segment;
@@ -9,7 +9,7 @@ import javax.swing.text.Segment;
import org.fife.ui.rsyntaxtextarea.*;
/*
/**
* 用于Smali代码高亮
* MartinKay@qq.com
*/
@@ -173,6 +173,7 @@ import org.fife.ui.rsyntaxtextarea.*;
zzAtEOF = false;
}
%}
Letter = [A-Za-z]
@@ -677,3 +678,4 @@ FLAG_ARRAY = (":array_"{SimpleName})
\n { addToken(start,zzStartRead-1, Token.COMMENT_EOL); addNullToken(); return firstToken; }
<<EOF>> { addToken(start,zzStartRead-1, Token.COMMENT_EOL); addNullToken(); return firstToken; }
}
+13 -16
View File
@@ -57,12 +57,12 @@
private int yychar;
/**
* the number of characters from the last newline up to the start of the
* the number of characters from the last newline up to the start of the
* matched text
*/
private int yycolumn;
/**
/**
* zzAtBOL == true <=> the scanner is currently at the beginning of a line
*/
private boolean zzAtBOL = true;
@@ -102,9 +102,6 @@
zzLexicalState = newState;
}
public final int yystate() {
return zzLexicalState;
}
/**
* Returns the text matched by the current regular expression.
@@ -115,12 +112,12 @@
/**
* Returns the character at position <tt>pos</tt> from the
* matched text.
*
* Returns the character at position <tt>pos</tt> from the
* matched text.
*
* It is equivalent to yytext().charAt(pos), but faster
*
* @param pos the position of the character to fetch.
* @param pos the position of the character to fetch.
* A value from 0 to yylength()-1.
*
* @return the character at position pos
@@ -141,8 +138,8 @@
/**
* Reports an error that occured while scanning.
*
* In a wellformed scanner (no or only correct usage of
* yypushback(int) and a match-all fallback rule) this method
* In a wellformed scanner (no or only correct usage of
* yypushback(int) and a match-all fallback rule) this method
* will only be called with things that "Can't Possibly Happen".
* If this method is called, something is seriously wrong
* (e.g. a JFlex bug producing a faulty scanner etc.).
@@ -162,7 +159,7 @@
}
--- throws clause
}
}
/**
@@ -209,12 +206,12 @@
zzAction = -1;
zzCurrentPosL = zzCurrentPos = zzStartRead = zzMarkedPosL;
--- start admin (lexstate etc)
zzForAction: {
while (true) {
--- next input, line, col, char count, next transition, isFinal action
zzAction = zzState;
zzMarkedPosL = zzCurrentPosL;
@@ -229,11 +226,11 @@
--- char count update
--- actions
default:
default:
if (zzInput == YYEOF && zzStartRead == zzCurrentPos) {
zzAtEOF = true;
--- eofvalue
}
}
else {
--- no match
}
-10
View File
@@ -1,10 +0,0 @@
[Desktop Entry]
Name=JADX GUI
Comment=Dex to Java decompiler
Icon=jadx
Exec=jadx-gui %f
Terminal=false
Type=Application
Categories=Development;Java;
Keywords=Java;Decompiler;
StartupWMClass=jadx-gui-JadxGUI
-4
View File
@@ -2,10 +2,6 @@ org.gradle.warning.mode=all
org.gradle.parallel=true
org.gradle.caching=true
### Disable configuration cache for now: causing issues with spotless and version plugins
# org.gradle.configuration-cache=true
# org.gradle.configuration-cache.problems=warn
# Flags for google-java-format (optimize imports by spotless) for Java >= 16.
# Java < 9 will ignore unsupported flags (thanks to -XX:+IgnoreUnrecognizedVMOptions)
org.gradle.jvmargs=-XX:+IgnoreUnrecognizedVMOptions \
Binary file not shown.
+2 -2
View File
@@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=72f44c9f8ebcb1af43838f45ee5c4aa9c5444898b3468ab3f4af7b6076c5bc3f
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
distributionSha256Sum=9d926787066a081739e8200858338b4a69e837c3a821a33aca9db09dd4a41026
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Vendored
+8 -7
View File
@@ -1,7 +1,7 @@
#!/bin/sh
#
# Copyright © 2015 the original authors.
# Copyright © 2015-2021 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.
@@ -15,8 +15,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@@ -57,7 +55,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/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -86,7 +84,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -114,6 +112,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
@@ -171,6 +170,7 @@ 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" )
@@ -203,14 +203,15 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
Vendored
+12 -13
View File
@@ -13,8 +13,6 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@@ -45,11 +43,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
@@ -59,21 +57,22 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
+4 -14
View File
@@ -1,16 +1,14 @@
plugins {
id("jadx-java")
id("jadx-library")
id("application")
// use shadow only for application scripts, jar will be copied from jadx-gui
id("com.gradleup.shadow") version "8.3.8"
id("com.github.johnrengelman.shadow") version "8.1.1"
}
dependencies {
implementation(project(":jadx-core"))
implementation(project(":jadx-plugins-tools"))
implementation(project(":jadx-commons:jadx-app-commons"))
runtimeOnly(project(":jadx-plugins:jadx-dex-input"))
runtimeOnly(project(":jadx-plugins:jadx-java-input"))
@@ -18,15 +16,11 @@ dependencies {
runtimeOnly(project(":jadx-plugins:jadx-smali-input"))
runtimeOnly(project(":jadx-plugins:jadx-rename-mappings"))
runtimeOnly(project(":jadx-plugins:jadx-kotlin-metadata"))
runtimeOnly(project(":jadx-plugins:jadx-kotlin-source-debug-extension"))
runtimeOnly(project(":jadx-plugins:jadx-script:jadx-script-plugin"))
runtimeOnly(project(":jadx-plugins:jadx-xapk-input"))
runtimeOnly(project(":jadx-plugins:jadx-aab-input"))
runtimeOnly(project(":jadx-plugins:jadx-apkm-input"))
runtimeOnly(project(":jadx-plugins:jadx-apks-input"))
implementation("org.jcommander:jcommander:2.0")
implementation("ch.qos.logback:logback-classic:1.5.22")
implementation("com.google.code.gson:gson:2.13.2")
implementation("org.jcommander:jcommander:1.83")
implementation("ch.qos.logback:logback-classic:1.4.14")
}
application {
@@ -34,14 +28,10 @@ application {
mainClass.set("jadx.cli.JadxCLI")
applicationDefaultJvmArgs =
listOf(
"-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)
"--enable-native-access=ALL-UNNAMED",
)
applicationDistribution.from("$rootDir") {
include("README.md")
@@ -4,7 +4,7 @@ import java.io.PrintStream;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -13,11 +13,11 @@ import java.util.function.Supplier;
import org.jetbrains.annotations.Nullable;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.ParameterDescription;
import com.beust.jcommander.ParameterException;
import com.beust.jcommander.Parameterized;
import jadx.api.JadxArgs;
import jadx.api.JadxDecompiler;
import jadx.api.plugins.JadxPluginInfo;
import jadx.api.plugins.options.JadxPluginOptions;
@@ -25,8 +25,9 @@ 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 {
public class JCommanderWrapper<T> {
private final JCommander jc;
private final JadxCLIArgs argsObj;
@@ -40,25 +41,15 @@ public class JCommanderWrapper {
public boolean parse(String[] args) {
try {
String[] fixedArgs = fixArgsForEmptySaveConfig(args);
jc.parse(fixedArgs);
applyFiles(argsObj);
jc.parse(args);
return true;
} catch (ParameterException e) {
System.err.println("Arguments parse error: " + e.getMessage());
printUsage();
return false;
}
}
public void overrideProvided(JadxCLIArgs obj) {
applyFiles(obj);
for (ParameterDescription parameter : jc.getParameters()) {
if (parameter.isAssigned()) {
overrideProperty(obj, parameter);
}
}
}
public boolean processCommands() {
String parsedCommand = jc.getParsedCommand();
if (parsedCommand == null) {
@@ -67,21 +58,20 @@ public class JCommanderWrapper {
return JadxCLICommands.process(this, jc, parsedCommand);
}
/**
* The main parameter parsing doesn't work if accepting unknown options
*/
private void applyFiles(JadxCLIArgs argsObj) {
argsObj.setFiles(jc.getUnknownOptions());
}
/**
* Override assigned field value to obj
*/
private static void overrideProperty(JadxCLIArgs obj, ParameterDescription parameter) {
Parameterized parameterized = parameter.getParameterized();
Object providedValue = parameterized.get(parameter.getObject());
Object newValue = mergeValues(parameterized.getType(), providedValue, () -> parameterized.get(obj));
parameterized.set(obj, newValue);
public void overrideProvided(JadxCLIArgs obj) {
List<ParameterDescription> fieldsParams = jc.getParameters();
List<ParameterDescription> parameters = new ArrayList<>(1 + fieldsParams.size());
parameters.add(jc.getMainParameterValue());
parameters.addAll(fieldsParams);
for (ParameterDescription parameter : parameters) {
if (parameter.isAssigned()) {
// copy assigned field value to obj
Parameterized parameterized = parameter.getParameterized();
Object providedValue = parameterized.get(parameter.getObject());
Object newValue = mergeValues(parameterized.getType(), providedValue, () -> parameterized.get(obj));
parameterized.set(obj, newValue);
}
}
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@@ -95,39 +85,8 @@ 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 List<String> getUnknownOptions() {
return jc.getUnknownOptions();
}
public void printUsage() {
@@ -150,11 +109,8 @@ public class JCommanderWrapper {
out.println(appendPluginOptions(maxNamesLen));
out.println();
out.println("Environment variables:");
out.println(" JADX_DISABLE_XML_SECURITY - set to 'true' to disable all security checks for XML files");
out.println(" JADX_DISABLE_ZIP_SECURITY - set to 'true' to disable all security checks for zip files");
out.println(" JADX_ZIP_MAX_ENTRIES_COUNT - maximum allowed number of entries in zip files (default: 100 000)");
out.println(" JADX_CONFIG_DIR - custom config directory, using system by default");
out.println(" JADX_CACHE_DIR - custom cache directory, using system by default");
out.println(" JADX_TMP_DIR - custom temp directory, using system by default");
out.println();
out.println("Examples:");
@@ -175,16 +131,14 @@ public class JCommanderWrapper {
out.println("options:");
List<ParameterDescription> params = jc.getParameters();
Map<String, ParameterDescription> paramsMap = new HashMap<>(params.size());
Map<String, ParameterDescription> paramsMap = new LinkedHashMap<>(params.size());
int maxNamesLen = 0;
for (ParameterDescription p : params) {
paramsMap.put(p.getParameterized().getName(), p);
int len = p.getNames().length();
String valueDesc = getValueDesc(p);
if (valueDesc != null) {
len += 1 + valueDesc.length();
if (len > maxNamesLen) {
maxNamesLen = len;
}
maxNamesLen = Math.max(maxNamesLen, len);
}
maxNamesLen += 3;
@@ -197,12 +151,8 @@ public class JCommanderWrapper {
}
StringBuilder opt = new StringBuilder();
opt.append(" ").append(p.getNames());
String valueDesc = getValueDesc(p);
if (valueDesc != null) {
opt.append(' ').append(valueDesc);
}
addSpaces(opt, maxNamesLen - opt.length());
String description = p.getDescription();
addSpaces(opt, maxNamesLen - opt.length());
if (description.contains("\n")) {
String[] lines = description.split("\n");
opt.append("- ").append(lines[0]);
@@ -215,10 +165,8 @@ public class JCommanderWrapper {
opt.append("- ").append(description);
}
if (addDefaults) {
String defaultValue = getDefaultValue(args, f);
if (defaultValue != null
&& !defaultValue.isEmpty()
&& !description.contains("(default)")) {
String defaultValue = getDefaultValue(args, f, opt);
if (defaultValue != null && !description.contains("(default)")) {
opt.append(", default: ").append(defaultValue);
}
}
@@ -227,11 +175,6 @@ public class JCommanderWrapper {
return maxNamesLen;
}
private static @Nullable String getValueDesc(ParameterDescription p) {
Parameter parameterAnnotation = p.getParameterAnnotation();
return parameterAnnotation == null ? null : parameterAnnotation.defaultValueDescription();
}
/**
* Get all declared fields of the specified class and all super classes
*/
@@ -245,7 +188,7 @@ public class JCommanderWrapper {
}
@Nullable
private static String getDefaultValue(Object args, Field f) {
private static String getDefaultValue(Object args, Field f, StringBuilder opt) {
try {
Class<?> fieldType = f.getType();
if (fieldType == int.class) {
@@ -274,20 +217,19 @@ 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())) {
try (JadxDecompiler decompiler = new JadxDecompiler(new JadxArgs())) {
JadxPluginManager pluginManager = decompiler.getPluginManager();
pluginManager.load(decompiler.getArgs().getPluginLoader());
pluginManager.load(new JadxExternalPluginsLoader());
pluginManager.initAll();
try {
for (PluginContext context : pluginManager.getAllPluginContexts()) {
JadxPluginOptions options = context.getOptions();
if (options != null) {
appendPlugin(context.getPluginInfo(), context.getOptions(), sb, maxNamesLen);
for (PluginContext context : pluginManager.getAllPluginContexts()) {
JadxPluginOptions options = context.getOptions();
if (options != null) {
if (appendPlugin(context.getPluginInfo(), context.getOptions(), sb, maxNamesLen, k)) {
k++;
}
}
} finally {
pluginManager.unloadAll();
}
}
if (sb.length() == 0) {
@@ -296,12 +238,12 @@ public class JCommanderWrapper {
return "\nPlugin options (-P<name>=<value>):" + sb;
}
private boolean appendPlugin(JadxPluginInfo pluginInfo, JadxPluginOptions options, StringBuilder out, int maxNamesLen) {
private boolean appendPlugin(JadxPluginInfo pluginInfo, JadxPluginOptions options, StringBuilder out, int maxNamesLen, int k) {
List<OptionDescription> descs = options.getOptionsDescriptions();
if (descs.isEmpty()) {
return false;
}
out.append("\n ");
out.append("\n ").append(k).append(") ");
out.append(pluginInfo.getPluginId()).append(": ").append(pluginInfo.getDescription());
for (OptionDescription desc : descs) {
StringBuilder opt = new StringBuilder();
@@ -1,48 +0,0 @@
package jadx.cli;
import java.util.Set;
import jadx.api.JadxArgs;
import jadx.api.security.JadxSecurityFlag;
import jadx.api.security.impl.JadxSecurity;
import jadx.commons.app.JadxCommonEnv;
import jadx.zip.security.DisabledZipSecurity;
import jadx.zip.security.IJadxZipSecurity;
import jadx.zip.security.JadxZipSecurity;
public class JadxAppCommon {
public static void applyEnvVars(JadxArgs jadxArgs) {
Set<JadxSecurityFlag> flags = JadxSecurityFlag.all();
IJadxZipSecurity zipSecurity;
boolean disableXmlSecurity = JadxCommonEnv.getBool("JADX_DISABLE_XML_SECURITY", false);
if (disableXmlSecurity) {
flags.remove(JadxSecurityFlag.SECURE_XML_PARSER);
// TODO: not related to 'xml security', but kept for compatibility
flags.remove(JadxSecurityFlag.VERIFY_APP_PACKAGE);
}
boolean disableZipSecurity = JadxCommonEnv.getBool("JADX_DISABLE_ZIP_SECURITY", false);
if (disableZipSecurity) {
flags.remove(JadxSecurityFlag.SECURE_ZIP_READER);
zipSecurity = DisabledZipSecurity.INSTANCE;
} else {
JadxZipSecurity jadxZipSecurity = new JadxZipSecurity();
int maxZipEntriesCount = JadxCommonEnv.getInt("JADX_ZIP_MAX_ENTRIES_COUNT", -2);
if (maxZipEntriesCount != -2) {
jadxZipSecurity.setMaxEntriesCount(maxZipEntriesCount);
}
int zipBombMinUncompressedSize = JadxCommonEnv.getInt("JADX_ZIP_BOMB_MIN_UNCOMPRESSED_SIZE", -2);
if (zipBombMinUncompressedSize != -2) {
jadxZipSecurity.setZipBombMinUncompressedSize(zipBombMinUncompressedSize);
}
int setZipBombDetectionFactor = JadxCommonEnv.getInt("JADX_ZIP_BOMB_DETECTION_FACTOR", -2);
if (setZipBombDetectionFactor != -2) {
jadxZipSecurity.setZipBombDetectionFactor(setZipBombDetectionFactor);
}
zipSecurity = jadxZipSecurity;
}
jadxArgs.setSecurity(new JadxSecurity(flags, zipSecurity));
}
}
+24 -44
View File
@@ -1,8 +1,5 @@
package jadx.cli;
import java.util.function.Consumer;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -11,68 +8,51 @@ import jadx.api.JadxDecompiler;
import jadx.api.impl.AnnotatedCodeWriter;
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.files.FileUtils;
import jadx.plugins.tools.JadxExternalPluginsLoader;
public class JadxCLI {
private static final Logger LOG = LoggerFactory.getLogger(JadxCLI.class);
public static void main(String[] args) {
int result = 1;
int result = 0;
try {
result = execute(args);
} catch (JadxArgsValidateException e) {
LOG.error("Incorrect arguments: {}", e.getMessage());
result = 1;
} catch (Throwable e) {
LOG.error("Process error:", e);
result = 1;
} finally {
FileUtils.deleteTempRootDir();
System.exit(result);
}
}
public static int execute(String[] args) {
return execute(args, null);
}
public static int execute(String[] args, @Nullable Consumer<JadxArgs> argsMod) {
try {
JadxCLIArgs cliArgs = JadxCLIArgs.processArgs(args,
new JadxCLIArgs(),
new JadxConfigAdapter<>(JadxCLIArgs.class, "cli"));
if (cliArgs == null) {
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;
} catch (Throwable e) {
LOG.error("Process error:", e);
return 1;
JadxCLIArgs jadxArgs = new JadxCLIArgs();
if (jadxArgs.processArgs(args)) {
return processAndSave(jadxArgs);
}
return 0;
}
private static JadxArgs buildArgs(JadxCLIArgs cliArgs) {
private static int processAndSave(JadxCLIArgs cliArgs) {
LogHelper.initLogLevel(cliArgs);
LogHelper.setLogLevelsForLoadingStage();
JadxArgs jadxArgs = cliArgs.toJadxArgs();
jadxArgs.setCodeCache(new NoOpCodeCache());
jadxArgs.setUsageInfoCache(new EmptyUsageInfoCache());
jadxArgs.setPluginLoader(new JadxExternalPluginsLoader());
jadxArgs.setFilesGetter(JadxFilesGetter.INSTANCE);
initCodeWriterProvider(jadxArgs);
JadxAppCommon.applyEnvVars(jadxArgs);
return jadxArgs;
}
private static int runSave(JadxArgs jadxArgs, JadxCLIArgs cliArgs) {
try (JadxDecompiler jadx = new JadxDecompiler(jadxArgs)) {
jadx.load();
if (checkForErrors(jadx)) {
return 2;
return 1;
}
LogHelper.setLogLevelsForDecompileStage();
if (!SingleClassMode.process(jadx, cliArgs)) {
save(jadx);
}
@@ -80,11 +60,11 @@ public class JadxCLI {
if (errorsCount != 0) {
jadx.printErrorsReport();
LOG.error("finished with errors, count: {}", errorsCount);
return 3;
} else {
LOG.info("done");
}
LOG.info("done");
return 0;
}
return 0;
}
private static void initCodeWriterProvider(JadxArgs jadxArgs) {
@@ -110,10 +90,10 @@ public class JadxCLI {
jadx.getArgs().setSkipSources(true);
}
}
int errorsCount = jadx.getErrorsCount();
if (errorsCount > 0) {
LOG.error("Loading finished with errors! Count: {}", errorsCount);
if (jadx.getErrorsCount() > 0) {
LOG.error("Load with errors! Check log for details");
// continue processing
return false;
}
return false;
}
+40 -451
View File
@@ -1,8 +1,8 @@
package jadx.cli;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
@@ -14,10 +14,6 @@ import java.util.function.Supplier;
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;
import com.beust.jcommander.Parameter;
@@ -31,35 +27,22 @@ import jadx.api.JadxDecompiler;
import jadx.api.args.GeneratedRenamesMappingFileMode;
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.exceptions.JadxException;
import jadx.core.utils.files.FileUtils;
public class JadxCLIArgs implements IJadxConfig {
private static final Logger LOG = LoggerFactory.getLogger(JadxCLIArgs.class);
public class JadxCLIArgs {
@JadxConfigExclude
@Parameter(description = "<input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk, .apkm, .jadx.kts)")
protected List<String> files = Collections.emptyList();
@Parameter(description = "<input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk)")
protected List<String> files = new ArrayList<>(1);
@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;
@@ -69,33 +52,20 @@ public class JadxCLIArgs implements IJadxConfig {
@Parameter(names = { "-s", "--no-src" }, description = "do not decompile source code")
protected boolean skipSources = false;
@Parameter(names = { "-j", "--threads-count" }, description = "processing threads count")
protected int threadsCount = JadxArgs.DEFAULT_THREADS_COUNT;
@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;
@Parameter(names = { "--output-format" }, description = "can be 'java' or 'json'")
protected String outputFormat = "java";
@Parameter(names = { "-e", "--export-gradle" }, description = "save as gradle project (set '--export-gradle-type' to 'auto')")
@Parameter(names = { "-e", "--export-gradle" }, description = "save as android gradle project")
protected boolean exportAsGradleProject = false;
@Parameter(
names = { "--export-gradle-type" },
description = "Gradle project template for export:"
+ "\n 'auto' - detect automatically"
+ "\n 'android-app' - Android Application (apk)"
+ "\n 'android-library' - Android Library (aar)"
+ "\n 'simple-java' - simple Java",
converter = ExportGradleTypeConverter.class
)
protected @Nullable ExportGradleType exportGradleType = null;
@Parameter(names = { "-j", "--threads-count" }, description = "processing threads count")
protected int threadsCount = JadxArgs.DEFAULT_THREADS_COUNT;
@Parameter(
names = { "-m", "--decompilation-mode" },
@@ -138,9 +108,6 @@ public class JadxCLIArgs implements IJadxConfig {
@Parameter(names = "--no-finally", description = "don't extract finally block")
protected boolean extractFinally = true;
@Parameter(names = "--no-restore-switch-over-string", description = "don't restore switch over string")
protected boolean restoreSwitchOverString = true;
@Parameter(names = "--no-replace-consts", description = "don't replace constant value with matching constant field")
protected boolean replaceConsts = true;
@@ -181,7 +148,6 @@ public class JadxCLIArgs implements IJadxConfig {
)
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),"
@@ -200,15 +166,8 @@ public class JadxCLIArgs implements IJadxConfig {
)
protected GeneratedRenamesMappingFileMode generatedRenamesMappingFileMode = GeneratedRenamesMappingFileMode.getDefault();
@SuppressWarnings("DeprecatedIsStillUsed")
@Parameter(
names = { "--deobf-use-sourcename" },
description = "use source file name as class name alias."
+ "\nDEPRECATED, use \"--use-source-name-as-class-name-alias\" instead",
hidden = true
)
@Deprecated
protected Boolean deobfuscationUseSourceNameAsAlias = null;
@Parameter(names = { "--deobf-use-sourcename" }, description = "use source file name as class name alias")
protected boolean deobfuscationUseSourceNameAsAlias = false;
@Parameter(
names = { "--deobf-res-name-source" },
@@ -220,22 +179,6 @@ public class JadxCLIArgs implements IJadxConfig {
)
protected ResourceNameSource resourceNameSource = ResourceNameSource.AUTO;
@Parameter(
names = { "--use-source-name-as-class-name-alias" },
description = "use source name as class name alias:"
+ "\n 'always' - always use source name if it's available"
+ "\n 'if-better' - use source name if it seems better than the current one"
+ "\n 'never' - never use source name, even if it's available",
converter = UseSourceNameAsClassNameConverter.class
)
protected UseSourceNameAsClassNameAlias useSourceNameAsClassNameAlias = null;
@Parameter(
names = { "--source-name-repeat-limit" },
description = "allow using source name if it appears less than a limit number"
)
protected int sourceNameRepeatLimit = 10;
@Parameter(
names = { "--use-kotlin-methods-for-var-names" },
description = "use kotlin intrinsic methods to rename variables, values: disable, apply, apply-and-hide",
@@ -243,12 +186,6 @@ public class JadxCLIArgs implements IJadxConfig {
)
protected UseKotlinMethodsForVarNames useKotlinMethodsForVarNames = UseKotlinMethodsForVarNames.APPLY;
@Parameter(
names = { "--use-headers-for-detect-resource-extensions" },
description = "Use headers for detect resource extensions if resource obfuscated"
)
protected boolean useHeadersForDetectResourceExtensions = false;
@Parameter(
names = { "--rename-flags" },
description = "fix options (comma-separated list of):"
@@ -257,7 +194,7 @@ public class JadxCLIArgs implements IJadxConfig {
+ "\n 'printable' - remove non-printable chars from identifiers,"
+ "\nor single 'none' - to disable all renames"
+ "\nor single 'all' - to enable all (default)",
listConverter = RenameConverter.class
converter = RenameConverter.class
)
protected Set<RenameEnum> renameFlags = EnumSet.allOf(RenameEnum.class);
@@ -271,9 +208,6 @@ public class JadxCLIArgs implements IJadxConfig {
)
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;
@@ -303,113 +237,45 @@ public class JadxCLIArgs implements IJadxConfig {
)
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) {
return processArgs(args, this, null) != null;
JCommanderWrapper<JadxCLIArgs> jcw = new JCommanderWrapper<>(this);
return jcw.parse(args) && process(jcw);
}
public static <T extends JadxCLIArgs> @Nullable T processArgs(
String[] args, T argsObj, @Nullable JadxConfigAdapter<T> configAdapter) {
JCommanderWrapper jcw = new JCommanderWrapper(argsObj);
/**
* Set values only for options provided in cmd.
* Used to merge saved options and options passed in command line.
*/
public boolean overrideProvided(String[] args) {
JCommanderWrapper<JadxCLIArgs> jcw = new JCommanderWrapper<>(newInstance());
if (!jcw.parse(args)) {
return null;
return false;
}
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;
jcw.overrideProvided(this);
return process(jcw);
}
private static <T extends JadxCLIArgs> void applyArgs(T argsObj) {
// apply log levels
LogHelper.initLogLevel(argsObj);
LogHelper.applyLogLevels();
protected JadxCLIArgs newInstance() {
return new JadxCLIArgs();
}
public boolean process(JCommanderWrapper jcw) {
private boolean process(JCommanderWrapper<JadxCLIArgs> jcw) {
files.addAll(jcw.getUnknownOptions());
if (jcw.processCommands()) {
return false;
}
@@ -421,38 +287,18 @@ public class JadxCLIArgs implements IJadxConfig {
System.out.println(JadxDecompiler.getVersion());
return false;
}
// unknown options added to 'files', run checks
for (String fileName : files) {
if (fileName.startsWith("-")) {
throw new JadxArgsValidateException("Unknown option: " + fileName);
try {
if (threadsCount <= 0) {
throw new JadxException("Threads count must be positive, got: " + threadsCount);
}
} catch (JadxException e) {
System.err.println("ERROR: " + e.getMessage());
jcw.printUsage();
return false;
}
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()));
@@ -482,17 +328,12 @@ public class JadxCLIArgs implements IJadxConfig {
args.setDeobfuscationMinLength(deobfuscationMinLength);
args.setDeobfuscationMaxLength(deobfuscationMaxLength);
args.setDeobfuscationWhitelist(Arrays.asList(deobfuscationWhitelistStr.split(" ")));
args.setUseSourceNameAsClassNameAlias(getUseSourceNameAsClassNameAlias());
args.setUseHeadersForDetectResourceExtensions(useHeadersForDetectResourceExtensions);
args.setSourceNameRepeatLimit(sourceNameRepeatLimit);
args.setUseSourceNameAsClassAlias(deobfuscationUseSourceNameAsAlias);
args.setUseKotlinMethodsForVarNames(useKotlinMethodsForVarNames);
args.setResourceNameSource(resourceNameSource);
args.setEscapeUnicode(escapeUnicode);
args.setRespectBytecodeAccModifiers(respectBytecodeAccessModifiers);
args.setExportGradleType(exportGradleType);
if (exportAsGradleProject && exportGradleType == null) {
args.setExportGradleType(ExportGradleType.AUTO);
}
args.setExportAsGradleProject(exportAsGradleProject);
args.setSkipXmlPrettyPrint(skipXmlPrettyPrint);
args.setUseImports(useImports);
args.setDebugInfo(debugInfo);
@@ -502,32 +343,19 @@ public class JadxCLIArgs implements IJadxConfig {
args.setMoveInnerClasses(moveInnerClasses);
args.setAllowInlineKotlinLambda(allowInlineKotlinLambda);
args.setExtractFinally(extractFinally);
args.setRestoreSwitchOverString(restoreSwitchOverString);
args.setRenameFlags(buildEnumSetForRenameFlags());
args.setRenameFlags(renameFlags);
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;
}
public void setFiles(List<String> files) {
this.files = files;
}
public String getOutDir() {
return outDir;
}
@@ -552,26 +380,14 @@ public class JadxCLIArgs implements IJadxConfig {
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;
}
@@ -580,285 +396,122 @@ public class JadxCLIArgs implements IJadxConfig {
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;
} else if (deobfuscationUseSourceNameAsAlias != null) {
// noinspection deprecation
return UseSourceNameAsClassNameAlias.create(deobfuscationUseSourceNameAsAlias);
} else {
return UseSourceNameAsClassNameAlias.getDefault();
}
}
public void setUseSourceNameAsClassNameAlias(UseSourceNameAsClassNameAlias useSourceNameAsClassNameAlias) {
this.useSourceNameAsClassNameAlias = useSourceNameAsClassNameAlias;
}
/**
* @deprecated Use {@link #getUseSourceNameAsClassNameAlias()} instead.
*/
@Deprecated
public boolean isDeobfuscationUseSourceNameAsAlias() {
return getUseSourceNameAsClassNameAlias().toBoolean();
}
public void setDeobfuscationUseSourceNameAsAlias(Boolean deobfuscationUseSourceNameAsAlias) {
this.deobfuscationUseSourceNameAsAlias = deobfuscationUseSourceNameAsAlias;
return 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 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);
}
@@ -875,70 +528,18 @@ public class JadxCLIArgs implements IJadxConfig {
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;
@@ -958,8 +559,8 @@ public class JadxCLIArgs implements IJadxConfig {
for (String s : value.split(",")) {
try {
set.add(RenameEnum.valueOf(s.trim().toUpperCase(Locale.ROOT)));
} catch (Exception e) {
throw new JadxArgsValidateException(
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(
'\'' + s + "' is unknown for parameter " + paramName
+ ", possible values are " + enumValuesString(RenameEnum.values()));
}
@@ -992,24 +593,12 @@ public class JadxCLIArgs implements IJadxConfig {
}
}
public static class UseSourceNameAsClassNameConverter extends BaseEnumConverter<UseSourceNameAsClassNameAlias> {
public UseSourceNameAsClassNameConverter() {
super(UseSourceNameAsClassNameAlias::valueOf, UseSourceNameAsClassNameAlias::values);
}
}
public static class DecompilationModeConverter extends BaseEnumConverter<DecompilationMode> {
public DecompilationModeConverter() {
super(DecompilationMode::valueOf, DecompilationMode::values);
}
}
public static class ExportGradleTypeConverter extends BaseEnumConverter<ExportGradleType> {
public ExportGradleTypeConverter() {
super(ExportGradleType::valueOf, ExportGradleType::values);
}
}
public static class LogLevelConverter extends BaseEnumConverter<LogHelper.LogLevelEnum> {
public LogLevelConverter() {
super(LogHelper.LogLevelEnum::valueOf, LogHelper.LogLevelEnum::values);
@@ -1036,7 +625,7 @@ public class JadxCLIArgs implements IJadxConfig {
try {
return parse.apply(stringAsEnumName(value));
} catch (Exception e) {
throw new JadxArgsValidateException(
throw new IllegalArgumentException(
'\'' + value + "' is unknown, possible values are: " + enumValuesString(values.get()));
}
}
@@ -1,16 +1,15 @@
package jadx.cli;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;
import com.beust.jcommander.JCommander;
import jadx.cli.commands.CommandPlugins;
import jadx.cli.commands.ICommand;
import jadx.core.utils.exceptions.JadxArgsValidateException;
public class JadxCLICommands {
private static final Map<String, ICommand> COMMANDS_MAP = new LinkedHashMap<>();
private static final Map<String, ICommand> COMMANDS_MAP = new TreeMap<>();
static {
JadxCLICommands.register(new CommandPlugins());
@@ -24,11 +23,10 @@ public class JadxCLICommands {
COMMANDS_MAP.forEach(builder::addCommand);
}
public static boolean process(JCommanderWrapper jcw, JCommander jc, String parsedCommand) {
public static boolean process(JCommanderWrapper<?> jcw, JCommander jc, String parsedCommand) {
ICommand command = COMMANDS_MAP.get(parsedCommand);
if (command == null) {
throw new JadxArgsValidateException("Unknown command: " + parsedCommand
+ ". Expected one of: " + COMMANDS_MAP.keySet());
throw new IllegalArgumentException("Unknown command: " + parsedCommand);
}
JCommander subCommander = jc.getCommands().get(parsedCommand);
command.process(jcw, subCommander);
+18 -4
View File
@@ -43,9 +43,10 @@ public class LogHelper {
return null;
}
if (args.quiet) {
args.logLevel = LogLevelEnum.QUIET;
} else if (args.verbose) {
args.logLevel = LogLevelEnum.DEBUG;
return LogLevelEnum.QUIET;
}
if (args.verbose) {
return LogLevelEnum.DEBUG;
}
return args.logLevel;
}
@@ -55,7 +56,20 @@ public class LogHelper {
applyLogLevel(logLevelValue);
}
public static void applyLogLevels() {
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() {
if (logLevelValue == null) {
return;
}
@@ -12,7 +12,6 @@ import jadx.api.JadxDecompiler;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.visitors.SaveCode;
import jadx.core.utils.exceptions.JadxArgsValidateException;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
@@ -34,10 +33,10 @@ public class SingleClassMode {
.findFirst().orElse(null);
}
if (clsForProcess == null) {
throw new JadxArgsValidateException("Input class not found: " + singleClass);
throw new JadxRuntimeException("Input class not found: " + singleClass);
}
if (clsForProcess.contains(AFlag.DONT_GENERATE)) {
throw new JadxArgsValidateException("Input class can't be saved by current jadx settings (marked as DONT_GENERATE)");
throw new JadxRuntimeException("Input class can't be saved by current jadx settings (marked as DONT_GENERATE)");
}
if (clsForProcess.isInner()) {
clsForProcess = clsForProcess.getTopParentClass();
@@ -53,7 +52,7 @@ public class SingleClassMode {
if (size == 1) {
clsForProcess = classes.get(0);
} else {
throw new JadxArgsValidateException("Found " + size + " classes, single class output can't be used");
throw new JadxRuntimeException("Found " + size + " classes, single class output can't be used");
}
}
ICodeInfo codeInfo;
@@ -12,7 +12,6 @@ import org.slf4j.LoggerFactory;
import jadx.api.JadxArgs;
import jadx.api.JadxDecompiler;
import jadx.api.args.UseSourceNameAsClassNameAlias;
import jadx.core.clsp.ClsSet;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.files.FileUtils;
@@ -24,9 +23,8 @@ public class ConvertToClsSet {
private static final Logger LOG = LoggerFactory.getLogger(ConvertToClsSet.class);
public static void usage() {
LOG.info("<android API level (number)> <output .jcst file> <several input dex or jar files> ");
LOG.info("<output .jcst file> <several input dex or jar files> ");
LOG.info("Arguments to update core.jcst: "
+ "<android API level (number)> "
+ "<jadx root>/jadx-core/src/main/resources/clst/core.jcst "
+ "<sdk_root>/platforms/android-<api level>/android.jar"
+ "<sdk_root>/platforms/android-<api level>/optional/android.car.jar "
@@ -34,12 +32,11 @@ public class ConvertToClsSet {
}
public static void main(String[] args) {
if (args.length != 5) {
if (args.length < 2) {
usage();
System.exit(1);
}
int androidApiLevel = Integer.parseInt(args[0]);
List<Path> inputPaths = Stream.of(args).skip(1).map(Paths::get).collect(Collectors.toList());
List<Path> inputPaths = Stream.of(args).map(Paths::get).collect(Collectors.toList());
Path output = inputPaths.remove(0);
JadxArgs jadxArgs = new JadxArgs();
@@ -48,7 +45,7 @@ public class ConvertToClsSet {
// disable not needed passes executed at prepare stage
jadxArgs.setDeobfuscationOn(false);
jadxArgs.setRenameFlags(EnumSet.noneOf(JadxArgs.RenameEnum.class));
jadxArgs.setUseSourceNameAsClassNameAlias(UseSourceNameAsClassNameAlias.NEVER);
jadxArgs.setUseSourceNameAsClassAlias(false);
jadxArgs.setMoveInnerClasses(false);
jadxArgs.setInlineAnonymousClasses(false);
jadxArgs.setInlineMethods(false);
@@ -60,7 +57,6 @@ public class ConvertToClsSet {
decompiler.load();
RootNode root = decompiler.getRoot();
ClsSet set = new ClsSet(root);
set.setAndroidApiLevel(androidApiLevel);
set.loadFrom(root);
set.save(output);
@@ -1,61 +1,38 @@
package jadx.cli.commands;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import jadx.api.plugins.JadxPluginInfo;
import jadx.cli.JCommanderWrapper;
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;
@Parameters(commandDescription = "manage jadx plugins")
public class CommandPlugins implements ICommand {
@Parameter(names = { "-i", "--install" }, description = "install plugin with locationId", defaultValueDescription = "<locationId>")
@Parameter(names = { "-i", "--install" }, description = "install plugin with locationId")
protected String install;
@Parameter(names = { "-j", "--install-jar" }, description = "install plugin from jar file", defaultValueDescription = "<path-to.jar>")
@Parameter(names = { "-j", "--install-jar" }, description = "install plugin from jar file")
protected String installJar;
@Parameter(names = { "-l", "--list" }, description = "list installed plugins")
protected boolean list;
@Parameter(names = { "-a", "--available" }, description = "list available plugins from jadx-plugins-list (aka marketplace)")
@Parameter(names = { "-a", "--available" }, description = "list available plugins")
protected boolean available;
@Parameter(names = { "-u", "--update" }, description = "update installed plugins")
protected boolean update;
@Parameter(names = { "--uninstall" }, description = "uninstall plugin with pluginId", defaultValueDescription = "<pluginId>")
@Parameter(names = { "--uninstall" }, description = "uninstall plugin with pluginId")
protected String uninstall;
@Parameter(names = { "--disable" }, description = "disable plugin with pluginId", defaultValueDescription = "<pluginId>")
protected String disable;
@Parameter(names = { "--enable" }, description = "enable plugin with pluginId", defaultValueDescription = "<pluginId>")
protected String enable;
@Parameter(names = { "--list-all" }, description = "list all plugins including bundled and dropins")
protected boolean listAll;
@Parameter(
names = { "--list-versions" },
description = "fetch latest versions of plugin from locationId (will download all artefacts, limited to 10)",
defaultValueDescription = "<locationId>"
)
protected String listVersions;
@Parameter(names = { "-h", "--help" }, description = "print this help", help = true)
protected boolean printHelp = false;
@@ -64,33 +41,21 @@ public class CommandPlugins implements ICommand {
return "plugins";
}
@SuppressWarnings("UnnecessaryReturnStatement")
@Override
public void process(JCommanderWrapper jcw, JCommander subCommander) {
public void process(JCommanderWrapper<?> jcw, JCommander subCommander) {
if (printHelp) {
jcw.printUsage(subCommander);
return;
}
Set<String> unknownOptions = new HashSet<>(subCommander.getUnknownOptions());
boolean verbose = unknownOptions.remove("-v") || unknownOptions.remove("--verbose");
LogHelper.setLogLevel(verbose ? LogHelper.LogLevelEnum.DEBUG : LogHelper.LogLevelEnum.INFO);
if (!unknownOptions.isEmpty()) {
System.out.println("Error: found unknown options: " + unknownOptions);
}
if (install != null) {
installPlugin(install);
return;
}
if (installJar != null) {
installPlugin("file:" + installJar);
return;
}
if (uninstall != null) {
boolean uninstalled = JadxPluginsTools.getInstance().uninstall(uninstall);
System.out.println(uninstalled ? "Uninstalled" : "Plugin not found");
return;
}
if (update) {
List<JadxPluginUpdate> updates = JadxPluginsTools.getInstance().updateAll();
@@ -102,113 +67,28 @@ public class CommandPlugins implements ICommand {
System.out.println(" " + update.getPluginId() + ": " + update.getOldVersion() + " -> " + update.getNewVersion());
}
}
return;
}
if (list) {
printPlugins(JadxPluginsTools.getInstance().getInstalled());
return;
}
if (listAll) {
printAllPlugins();
return;
}
if (listVersions != null) {
printVersions(listVersions, 10);
return;
List<JadxPluginMetadata> installed = JadxPluginsTools.getInstance().getInstalled();
System.out.println("Installed plugins: " + installed.size());
int i = 1;
for (JadxPluginMetadata plugin : installed) {
System.out.println(" " + (i++) + ") "
+ plugin.getPluginId() + " (" + plugin.getVersion() + ") - "
+ plugin.getName() + ": " + plugin.getDescription());
}
}
if (available) {
List<JadxPluginListEntry> availableList = JadxPluginsList.getInstance().get();
List<JadxPluginMetadata> availableList = JadxPluginsList.getInstance().get();
System.out.println("Available plugins: " + availableList.size());
for (JadxPluginListEntry plugin : availableList) {
System.out.println(" - " + plugin.getName() + ": " + plugin.getDescription()
int i = 1;
for (JadxPluginMetadata plugin : availableList) {
System.out.println(" " + (i++) + ") "
+ plugin.getName() + ": " + plugin.getDescription()
+ " (" + plugin.getLocationId() + ")");
}
return;
}
if (disable != null) {
if (JadxPluginsTools.getInstance().changeDisabledStatus(disable, true)) {
System.out.println("Plugin '" + disable + "' disabled.");
} else {
System.out.println("Plugin '" + disable + "' already disabled.");
}
return;
}
if (enable != null) {
if (JadxPluginsTools.getInstance().changeDisabledStatus(enable, false)) {
System.out.println("Plugin '" + enable + "' enabled.");
} else {
System.out.println("Plugin '" + enable + "' already enabled.");
}
return;
}
}
private static void printPlugins(List<JadxPluginMetadata> installed) {
System.out.println("Installed plugins: " + installed.size());
for (JadxPluginMetadata plugin : installed) {
StringBuilder sb = new StringBuilder();
sb.append(" - ").append(plugin.getPluginId());
String version = plugin.getVersion();
if (version != null) {
sb.append(" (").append(version).append(')');
}
if (plugin.isDisabled()) {
sb.append(" (disabled)");
}
sb.append(" - ").append(plugin.getName());
sb.append(": ").append(formatDescription(plugin.getDescription()));
System.out.println(sb);
}
}
private void printVersions(String locationId, int limit) {
System.out.println("Loading ...");
List<JadxPluginMetadata> versions = JadxPluginsTools.getInstance().getVersionsByLocation(locationId, 1, limit);
if (versions.isEmpty()) {
System.out.println("No versions found");
return;
}
JadxPluginMetadata plugin = versions.get(0);
System.out.println("Versions for plugin id: " + plugin.getPluginId());
for (JadxPluginMetadata version : versions) {
StringBuilder sb = new StringBuilder();
sb.append(" - ").append(version.getVersion());
String reqVer = version.getRequiredJadxVersion();
if (StringUtils.notBlank(reqVer)) {
sb.append(", require jadx: ").append(reqVer);
}
System.out.println(sb);
}
}
private static void printAllPlugins() {
List<JadxPluginMetadata> installed = JadxPluginsTools.getInstance().getInstalled();
printPlugins(installed);
Set<String> installedSet = installed.stream().map(JadxPluginMetadata::getPluginId).collect(Collectors.toSet());
List<JadxPluginInfo> plugins = JadxPluginsTools.getInstance().getAllPluginsInfo();
System.out.println("Other plugins: " + plugins.size());
for (JadxPluginInfo plugin : plugins) {
if (!installedSet.contains(plugin.getPluginId())) {
System.out.println(" - " + plugin.getPluginId()
+ " - " + plugin.getName()
+ ": " + formatDescription(plugin.getDescription()));
}
}
}
private static String formatDescription(String desc) {
if (desc.contains("\n")) {
// remove new lines
desc = desc.replaceAll("\\R+", " ");
}
int maxLen = 512;
if (desc.length() > maxLen) {
// truncate very long descriptions
desc = desc.substring(0, maxLen) + " ...";
}
return desc;
}
private void installPlugin(String locationId) {
@@ -7,5 +7,5 @@ import jadx.cli.JCommanderWrapper;
public interface ICommand {
String name();
void process(JCommanderWrapper jcw, JCommander subCommander);
void process(JCommanderWrapper<?> jcw, JCommander subCommander);
}
@@ -1,7 +0,0 @@
package jadx.cli.config;
/**
* Marker interface for jadx config objects
*/
public interface IJadxConfig {
}
@@ -1,109 +0,0 @@
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");
}
}
@@ -1,11 +0,0 @@
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 {
}
@@ -1,31 +0,0 @@
package jadx.cli.plugins;
import java.nio.file.Path;
import jadx.commons.app.JadxCommonFiles;
import jadx.commons.app.JadxTempFiles;
import jadx.core.plugins.files.IJadxFilesGetter;
public class JadxFilesGetter implements IJadxFilesGetter {
public static final JadxFilesGetter INSTANCE = new JadxFilesGetter();
@Override
public Path getConfigDir() {
return JadxCommonFiles.getConfigDir();
}
@Override
public Path getCacheDir() {
return JadxCommonFiles.getCacheDir();
}
@Override
public Path getTempDir() {
return JadxTempFiles.getTempRootDir();
}
private JadxFilesGetter() {
// singleton
}
}
@@ -1,5 +1,6 @@
package jadx.cli.tools;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
@@ -10,6 +11,8 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -17,12 +20,7 @@ import org.slf4j.LoggerFactory;
import jadx.api.JadxArgs;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.android.TextResMapFile;
import jadx.core.xmlgen.ResTableBinaryParser;
import jadx.zip.IZipEntry;
import jadx.zip.ZipContent;
import jadx.zip.ZipReader;
import static jadx.core.utils.files.FileUtils.expandDirs;
import jadx.core.xmlgen.ResTableParser;
/**
* Utility class for convert '.arsc' to simple text file with mapping id to resource name
@@ -32,7 +30,7 @@ public class ConvertArscFile {
private static int rewritesCount;
public static void usage() {
LOG.info("<res-map file> <input .arsc/android.jar files or dir>");
LOG.info("<res-map file> <input .arsc files>");
LOG.info("");
LOG.info("Note: If res-map already exists - it will be merged and updated");
}
@@ -44,7 +42,6 @@ public class ConvertArscFile {
}
List<Path> inputPaths = Stream.of(args).map(Paths::get).collect(Collectors.toList());
Path resMapFile = inputPaths.remove(0);
List<Path> inputResFiles = filterAndSort(expandDirs(inputPaths));
Map<Integer, String> resMap;
if (Files.isReadable(resMapFile)) {
resMap = TextResMapFile.read(resMapFile);
@@ -54,25 +51,25 @@ public class ConvertArscFile {
LOG.info("Input entries count: {}", resMap.size());
RootNode root = new RootNode(new JadxArgs()); // not really needed
ZipReader zipReader = new ZipReader();
rewritesCount = 0;
for (Path resFile : inputResFiles) {
ResTableBinaryParser resTableParser = new ResTableBinaryParser(root, true);
for (Path resFile : inputPaths) {
LOG.info("Processing {}", resFile);
ResTableParser resTableParser = new ResTableParser(root, true);
if (resFile.getFileName().toString().endsWith(".jar")) {
// Load resources.arsc from android.jar
try (ZipContent zip = zipReader.open(resFile.toFile())) {
IZipEntry entry = zip.searchEntry("resources.arsc");
try (ZipFile zip = new ZipFile(resFile.toFile())) {
ZipEntry entry = zip.getEntry("resources.arsc");
if (entry == null) {
LOG.error("Failed to load \"resources.arsc\" from {}", resFile);
continue;
}
try (InputStream inputStream = entry.getInputStream()) {
try (InputStream inputStream = zip.getInputStream(entry)) {
resTableParser.decode(inputStream);
}
}
} else {
// Load resources.arsc from extracted file
try (InputStream inputStream = Files.newInputStream(resFile)) {
try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(resFile))) {
resTableParser.decode(inputStream);
}
}
@@ -87,16 +84,6 @@ public class ConvertArscFile {
LOG.info("done");
}
private static List<Path> filterAndSort(List<Path> inputPaths) {
return inputPaths.stream()
.filter(p -> {
String fileName = p.getFileName().toString();
return fileName.endsWith(".arsc") || fileName.endsWith(".jar");
})
.sorted()
.collect(Collectors.toList());
}
private static void mergeResMaps(Map<Integer, String> mainResMap, Map<Integer, String> newResMap) {
for (Map.Entry<Integer, String> entry : newResMap.entrySet()) {
Integer id = entry.getKey();
@@ -1,139 +0,0 @@
package jadx.cli;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.io.TempDir;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.plugins.loader.JadxBasePluginLoader;
import jadx.core.plugins.files.SingleDirFilesGetter;
import jadx.core.utils.Utils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
public class BaseCliIntegrationTest {
private static final Logger LOG = LoggerFactory.getLogger(BaseCliIntegrationTest.class);
static final PathMatcher LOG_ALL_FILES = path -> {
LOG.debug("File in result dir: {}", path);
return true;
};
@TempDir
Path testDir;
Path outputDir;
@BeforeEach
public void setUp() {
outputDir = testDir.resolve("output");
}
int execJadxCli(String sampleName, String... options) {
return execJadxCli(buildArgs(List.of(options), sampleName));
}
int execJadxCli(String[] args) {
return JadxCLI.execute(args, jadxArgs -> {
// don't use global config and plugins
jadxArgs.setFilesGetter(new SingleDirFilesGetter(testDir));
jadxArgs.setPluginLoader(new JadxBasePluginLoader());
});
}
String[] buildArgs(List<String> options, String... inputSamples) {
List<String> args = new ArrayList<>(options);
args.add("-v");
args.add("-d");
args.add(outputDir.toAbsolutePath().toString());
for (String inputSample : inputSamples) {
try {
URL resource = getClass().getClassLoader().getResource(inputSample);
assertThat(resource).isNotNull();
String sampleFile = resource.toURI().getRawPath();
args.add(sampleFile);
} catch (URISyntaxException e) {
fail("Failed to load sample: " + inputSample, e);
}
}
return args.toArray(new String[0]);
}
void decompile(String... inputSamples) throws IOException {
int result = execJadxCli(buildArgs(List.of(), inputSamples));
assertThat(result).isEqualTo(0);
List<Path> resultJavaFiles = collectJavaFilesInDir(outputDir);
assertThat(resultJavaFiles).isNotEmpty();
// do not copy input files as resources
for (Path path : collectFilesInDir(outputDir, LOG_ALL_FILES)) {
for (String inputSample : inputSamples) {
assertThat(path.toAbsolutePath().toString()).doesNotContain(inputSample);
}
}
}
static void printFiles(List<Path> files) {
LOG.info("Output files (count: {}):", files.size());
for (Path file : files) {
LOG.info(" {}", file);
}
LOG.info("");
}
String pathToUniformString(Path path) {
return path.toString().replace('\\', '/');
}
Path printFileContent(Path file) {
try {
String content = Files.readString(outputDir.resolve(file));
String spacer = Utils.strRepeat("=", 70);
LOG.info("File content: {}\n{}\n{}\n{}", file, spacer, content, spacer);
return file;
} catch (IOException e) {
throw new RuntimeException("Failed to load file: " + file, e);
}
}
static List<Path> collectJavaFilesInDir(Path dir) throws IOException {
PathMatcher javaMatcher = dir.getFileSystem().getPathMatcher("glob:**.java");
return collectFilesInDir(dir, javaMatcher);
}
static List<Path> collectAllFilesInDir(Path dir) throws IOException {
try (Stream<Path> pathStream = Files.walk(dir)) {
List<Path> files = pathStream
.filter(Files::isRegularFile)
.map(dir::relativize)
.collect(Collectors.toList());
printFiles(files);
return files;
}
}
static List<Path> collectFilesInDir(Path dir, PathMatcher matcher) throws IOException {
try (Stream<Path> pathStream = Files.walk(dir)) {
List<Path> files = pathStream
.filter(p -> Files.isRegularFile(p, LinkOption.NOFOLLOW_LINKS))
.filter(matcher::matches)
.collect(Collectors.toList());
printFiles(files);
return files;
}
}
}
@@ -3,12 +3,14 @@ package jadx.cli;
import java.util.Collections;
import java.util.Map;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static jadx.core.utils.Utils.newConstStringMap;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
public class JadxCLIArgsTest {
@@ -16,38 +18,38 @@ public class JadxCLIArgsTest {
@Test
public void testInvertedBooleanOption() {
assertThat(parse("--no-replace-consts").isReplaceConsts()).isFalse();
assertThat(parse("").isReplaceConsts()).isTrue();
assertThat(parse("--no-replace-consts").isReplaceConsts(), is(false));
assertThat(parse("").isReplaceConsts(), is(true));
}
@Test
public void testEscapeUnicodeOption() {
assertThat(parse("--escape-unicode").isEscapeUnicode()).isTrue();
assertThat(parse("").isEscapeUnicode()).isFalse();
assertThat(parse("--escape-unicode").isEscapeUnicode(), is(true));
assertThat(parse("").isEscapeUnicode(), is(false));
}
@Test
public void testSrcOption() {
assertThat(parse("--no-src").isSkipSources()).isTrue();
assertThat(parse("-s").isSkipSources()).isTrue();
assertThat(parse("").isSkipSources()).isFalse();
assertThat(parse("--no-src").isSkipSources(), is(true));
assertThat(parse("-s").isSkipSources(), is(true));
assertThat(parse("").isSkipSources(), is(false));
}
@Test
public void testOptionsOverride() {
assertThat(override(new JadxCLIArgs(), "--no-imports").isUseImports()).isFalse();
assertThat(override(new JadxCLIArgs(), "--no-debug-info").isDebugInfo()).isFalse();
assertThat(override(new JadxCLIArgs(), "").isUseImports()).isTrue();
assertThat(override(new JadxCLIArgs(), "--no-imports").isUseImports(), is(false));
assertThat(override(new JadxCLIArgs(), "--no-debug-info").isDebugInfo(), is(false));
assertThat(override(new JadxCLIArgs(), "").isUseImports(), is(true));
JadxCLIArgs args = new JadxCLIArgs();
args.useImports = false;
assertThat(override(args, "--no-imports").isUseImports()).isFalse();
assertThat(override(args, "--no-imports").isUseImports(), is(false));
args.debugInfo = false;
assertThat(override(args, "--no-debug-info").isDebugInfo()).isFalse();
assertThat(override(args, "--no-debug-info").isDebugInfo(), is(false));
args = new JadxCLIArgs();
args.useImports = false;
assertThat(override(args, "").isUseImports()).isFalse();
assertThat(override(args, "").isUseImports(), is(false));
}
@Test
@@ -81,7 +83,7 @@ public class JadxCLIArgsTest {
JadxCLIArgs args = new JadxCLIArgs();
args.pluginOptions = baseMap;
Map<String, String> resultMap = override(args, providedArgs).getPluginOptions();
assertThat(resultMap).isEqualTo(expectedMap);
assertThat(resultMap, Matchers.equalTo(expectedMap));
}
private JadxCLIArgs parse(String... args) {
@@ -89,24 +91,15 @@ public class JadxCLIArgsTest {
}
private JadxCLIArgs parse(JadxCLIArgs jadxArgs, String... args) {
return check(jadxArgs, jadxArgs.processArgs(args));
boolean res = jadxArgs.processArgs(args);
assertThat(res, is(true));
LOG.info("Jadx args: {}", jadxArgs.toJadxArgs());
return jadxArgs;
}
private JadxCLIArgs override(JadxCLIArgs jadxArgs, String... args) {
return check(jadxArgs, overrideProvided(jadxArgs, args));
}
private static boolean overrideProvided(JadxCLIArgs jadxArgs, String[] args) {
JCommanderWrapper jcw = new JCommanderWrapper(new JadxCLIArgs());
if (!jcw.parse(args)) {
return false;
}
jcw.overrideProvided(jadxArgs);
return jadxArgs.process(jcw);
}
private static JadxCLIArgs check(JadxCLIArgs jadxArgs, boolean res) {
assertThat(res).isTrue();
boolean res = jadxArgs.overrideProvided(args);
assertThat(res, is(true));
LOG.info("Jadx args: {}", jadxArgs.toJadxArgs());
return jadxArgs;
}
@@ -7,10 +7,10 @@ import org.junit.jupiter.api.Test;
import jadx.api.JadxArgs.RenameEnum;
import jadx.cli.JadxCLIArgs.RenameConverter;
import jadx.core.utils.exceptions.JadxArgsValidateException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class RenameConverterTest {
@@ -24,24 +24,25 @@ public class RenameConverterTest {
@Test
public void all() {
Set<RenameEnum> set = converter.convert("all");
assertThat(set).hasSize(3);
assertThat(set).contains(RenameEnum.CASE);
assertThat(set).contains(RenameEnum.VALID);
assertThat(set).contains(RenameEnum.PRINTABLE);
assertEquals(3, set.size());
assertTrue(set.contains(RenameEnum.CASE));
assertTrue(set.contains(RenameEnum.VALID));
assertTrue(set.contains(RenameEnum.PRINTABLE));
}
@Test
public void none() {
Set<RenameEnum> set = converter.convert("none");
assertThat(set).isEmpty();
assertTrue(set.isEmpty());
}
@Test
public void wrong() {
JadxArgsValidateException thrown = assertThrows(JadxArgsValidateException.class,
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class,
() -> converter.convert("wrong"),
"Expected convert() to throw, but it didn't");
assertThat(thrown.getMessage()).isEqualTo("'wrong' is unknown for parameter someParam, possible values are case, valid, printable");
assertEquals("'wrong' is unknown for parameter someParam, possible values are case, valid, printable",
thrown.getMessage());
}
}
@@ -1,77 +0,0 @@
package jadx.cli;
import org.assertj.core.api.Condition;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class TestExport extends BaseCliIntegrationTest {
@Test
public void testBasicExport() throws Exception {
int result = execJadxCli("samples/small.apk");
assertThat(result).isEqualTo(0);
assertThat(collectAllFilesInDir(outputDir))
.map(this::pathToUniformString)
.haveExactly(2, new Condition<>(f -> f.startsWith("sources/") && f.endsWith(".java"), "sources"))
.haveExactly(10, new Condition<>(f -> f.startsWith("resources/"), "resources"))
.haveExactly(1, new Condition<>(f -> f.equals("resources/AndroidManifest.xml"), "manifest"))
.hasSize(12);
}
@Test
public void testGradleExportApk() throws Exception {
int result = execJadxCli("samples/small.apk", "--export-gradle");
assertThat(result).isEqualTo(0);
assertThat(collectAllFilesInDir(outputDir))
.describedAs("check output files")
.map(this::pathToUniformString)
.haveExactly(2, new Condition<>(f -> f.endsWith(".java"), "java classes"))
.haveExactly(0, new Condition<>(f -> f.endsWith("classes.dex"), "dex files"))
.hasSize(15);
}
@Test
public void testGradleExportAAR() throws Exception {
int result = execJadxCli("samples/test-lib.aar", "--export-gradle");
assertThat(result).isEqualTo(0);
assertThat(collectAllFilesInDir(outputDir))
.describedAs("check output files")
.map(this::printFileContent)
.map(this::pathToUniformString)
.haveExactly(1, new Condition<>(f -> f.startsWith("lib/src/main/java/") && f.endsWith(".java"), "java"))
.haveExactly(0, new Condition<>(f -> f.endsWith(".jar"), "jar files"))
.hasSize(8);
}
@Test
public void testGradleExportSimpleJava() throws Exception {
int result = execJadxCli("samples/HelloWorld.class", "--export-gradle");
assertThat(result).isEqualTo(0);
assertThat(collectAllFilesInDir(outputDir))
.describedAs("check output files")
.map(this::printFileContent)
.map(this::pathToUniformString)
.haveExactly(1, new Condition<>(f -> f.endsWith(".java") && f.startsWith("app/src/main/java/"), "java"))
.haveExactly(0, new Condition<>(f -> f.endsWith(".class"), "class files"))
.haveExactly(1, new Condition<>(f -> f.equals("settings.gradle.kts"), "settings"))
.haveExactly(1, new Condition<>(f -> f.equals("app/build.gradle.kts"), "build"))
.hasSize(3);
}
@Test
public void testGradleExportInvalidType() throws Exception {
int result = execJadxCli("samples/HelloWorld.class", "--export-gradle-type", "android-app");
assertThat(result).isEqualTo(0);
// expect output in 'android-app' template, but most fields will be set to UNKNOWN.
assertThat(collectAllFilesInDir(outputDir))
.describedAs("check output files")
.map(this::printFileContent)
.map(this::pathToUniformString)
.haveExactly(1, new Condition<>(f -> f.endsWith(".java") && f.startsWith("app/src/main/java/"), "java"))
.haveExactly(1, new Condition<>(f -> f.equals("settings.gradle"), "settings"))
.haveExactly(1, new Condition<>(f -> f.equals("build.gradle"), "build"))
.haveExactly(1, new Condition<>(f -> f.equals("app/build.gradle"), "app build"))
.hasSize(4);
}
}
+92 -45
View File
@@ -1,76 +1,123 @@
package jadx.cli;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.assertj.core.api.Condition;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.utils.files.FileUtils;
import static org.assertj.core.api.Assertions.assertThat;
public class TestInput extends BaseCliIntegrationTest {
@Test
public void testHelp() {
int result = execJadxCli(new String[] { "--help" });
assertThat(result).isEqualTo(0);
}
@Test
public void testApkInput() throws Exception {
int result = execJadxCli(buildArgs(List.of(), "samples/small.apk"));
assertThat(result).isEqualTo(0);
assertThat(collectAllFilesInDir(outputDir))
.describedAs("check output files")
.map(p -> p.getFileName().toString())
.haveExactly(2, new Condition<>(f -> f.endsWith(".java"), "java classes"))
.haveExactly(9, new Condition<>(f -> f.endsWith(".xml"), "xml resources"))
.haveExactly(1, new Condition<>(f -> f.equals("AndroidManifest.xml"), "manifest"))
.hasSize(12);
}
public class TestInput {
private static final Logger LOG = LoggerFactory.getLogger(TestInput.class);
@Test
public void testDexInput() throws Exception {
decompile("samples/hello.dex");
decompile("dex", "samples/hello.dex");
}
@Test
public void testSmaliInput() throws Exception {
decompile("samples/HelloWorld.smali");
decompile("smali", "samples/HelloWorld.smali");
}
@Test
public void testClassInput() throws Exception {
decompile("samples/HelloWorld.class");
decompile("class", "samples/HelloWorld.class");
}
@Test
public void testMultipleInput() throws Exception {
decompile("samples/hello.dex", "samples/HelloWorld.smali");
}
@Test
public void testFallbackMode() throws Exception {
int result = execJadxCli(buildArgs(List.of("-f"), "samples/hello.dex"));
assertThat(result).isEqualTo(0);
List<Path> files = collectJavaFilesInDir(outputDir);
assertThat(files).hasSize(1);
}
@Test
public void testSimpleMode() throws Exception {
int result = execJadxCli(buildArgs(List.of("--decompilation-mode", "simple"), "samples/hello.dex"));
assertThat(result).isEqualTo(0);
List<Path> files = collectJavaFilesInDir(outputDir);
assertThat(files).hasSize(1);
decompile("multi", "samples/hello.dex", "samples/HelloWorld.smali");
}
@Test
public void testResourceOnly() throws Exception {
int result = execJadxCli(buildArgs(List.of(), "samples/resources-only.apk"));
decode("resourceOnly", "samples/resources-only.apk");
}
private void decode(String tmpDirName, String apkSample) throws URISyntaxException, IOException {
List<String> args = new ArrayList<>();
Path tempDir = FileUtils.createTempDir(tmpDirName);
args.add("-v");
args.add("-d");
args.add(tempDir.toAbsolutePath().toString());
URL resource = getClass().getClassLoader().getResource(apkSample);
assertThat(resource).isNotNull();
String sampleFile = resource.toURI().getRawPath();
args.add(sampleFile);
int result = JadxCLI.execute(args.toArray(new String[0]));
assertThat(result).isEqualTo(0);
List<Path> files = collectFilesInDir(outputDir,
path -> path.getFileName().toString().equalsIgnoreCase("AndroidManifest.xml"));
assertThat(files).isNotEmpty();
List<Path> files = Files.find(
tempDir,
3,
(file, attr) -> file.getFileName().toString().equalsIgnoreCase("AndroidManifest.xml"))
.collect(Collectors.toList());
assertThat(files.isEmpty()).isFalse();
}
private void decompile(String tmpDirName, String... inputSamples) throws URISyntaxException, IOException {
List<String> args = new ArrayList<>();
Path tempDir = FileUtils.createTempDir(tmpDirName);
args.add("-v");
args.add("-d");
args.add(tempDir.toAbsolutePath().toString());
for (String inputSample : inputSamples) {
URL resource = getClass().getClassLoader().getResource(inputSample);
assertThat(resource).isNotNull();
String sampleFile = resource.toURI().getRawPath();
args.add(sampleFile);
}
int result = JadxCLI.execute(args.toArray(new String[0]));
assertThat(result).isEqualTo(0);
List<Path> resultJavaFiles = collectJavaFilesInDir(tempDir);
assertThat(resultJavaFiles).isNotEmpty();
// do not copy input files as resources
PathMatcher logAllFiles = path -> {
LOG.debug("File in result dir: {}", path);
return true;
};
for (Path path : collectFilesInDir(tempDir, logAllFiles)) {
for (String inputSample : inputSamples) {
assertThat(path.toAbsolutePath().toString()).doesNotContain(inputSample);
}
}
}
private static List<Path> collectJavaFilesInDir(Path dir) throws IOException {
PathMatcher javaMatcher = dir.getFileSystem().getPathMatcher("glob:**.java");
return collectFilesInDir(dir, javaMatcher);
}
private static List<Path> collectFilesInDir(Path dir, PathMatcher matcher) throws IOException {
try (Stream<Path> pathStream = Files.walk(dir)) {
return pathStream
.filter(p -> Files.isRegularFile(p, LinkOption.NOFOLLOW_LINKS))
.filter(matcher::matches)
.collect(Collectors.toList());
}
}
@AfterAll
public static void cleanup() {
FileUtils.clearTempRootDir();
}
}
@@ -1,17 +0,0 @@
package jadx.plugins.tools.utils;
import org.junit.jupiter.api.Test;
import static jadx.plugins.tools.utils.PluginUtils.extractVersion;
import static org.assertj.core.api.Assertions.assertThat;
class PluginUtilsTest {
@Test
public void testExtractVersion() {
assertThat(extractVersion("plugin-name-v1.2.3.jar")).isEqualTo("1.2.3");
assertThat(extractVersion("plugin-name-v1.2.jar")).isEqualTo("1.2");
assertThat(extractVersion("1.2.3.jar")).isEqualTo("1.2.3");
}
}
Binary file not shown.
Binary file not shown.
-6
View File
@@ -1,6 +0,0 @@
## jadx app commons
This module contains common utilities used in jadx apps (cli and gui) and not needed in jadx-code module:
- `JadxCommonFiles` - wrapper for `dev.dirs:directories` lib to get
'config' and 'cache' directories in cross-platform way
- `JadxCommonEnv` - utils for work with environment variables
@@ -1,7 +0,0 @@
plugins {
id("jadx-library")
}
dependencies {
implementation("io.get-coursier.util:directories-jni:0.1.4")
}
@@ -1,29 +0,0 @@
package jadx.commons.app;
public class JadxCommonEnv {
public static String get(String varName, String defValue) {
String strValue = System.getenv(varName);
return isNullOrEmpty(strValue) ? defValue : strValue;
}
public static boolean getBool(String varName, boolean defValue) {
String strValue = System.getenv(varName);
if (isNullOrEmpty(strValue)) {
return defValue;
}
return strValue.equalsIgnoreCase("true");
}
public static int getInt(String varName, int defValue) {
String strValue = System.getenv(varName);
if (isNullOrEmpty(strValue)) {
return defValue;
}
return Integer.parseInt(strValue);
}
private static boolean isNullOrEmpty(String value) {
return value == null || value.isEmpty();
}
}
@@ -1,105 +0,0 @@
package jadx.commons.app;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.function.Function;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import dev.dirs.ProjectDirectories;
import dev.dirs.impl.Windows;
import dev.dirs.impl.WindowsPowerShell;
import dev.dirs.jni.WindowsJni;
public class JadxCommonFiles {
private static final Logger LOG = LoggerFactory.getLogger(JadxCommonFiles.class);
private static final Path CONFIG_DIR;
private static final Path CACHE_DIR;
public static Path getConfigDir() {
return CONFIG_DIR;
}
public static Path getCacheDir() {
return CACHE_DIR;
}
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;
public void init() {
try {
configDir = loadEnvDir("JADX_CONFIG_DIR", pd -> pd.configDir);
cacheDir = loadEnvDir("JADX_CACHE_DIR", pd -> pd.cacheDir);
} catch (Exception e) {
throw new RuntimeException("Failed to init common directories", e);
}
}
private Path loadEnvDir(String envVar, Function<ProjectDirectories, String> dirFunc) throws IOException {
String envDir = JadxCommonEnv.get(envVar, null);
String dirStr;
if (envDir != null) {
dirStr = envDir;
} else {
dirStr = dirFunc.apply(loadDirs());
}
Path path = Path.of(dirStr).toAbsolutePath();
Files.createDirectories(path);
return path;
}
private synchronized ProjectDirectories loadDirs() {
ProjectDirectories currentDirs = dirs;
if (currentDirs != null) {
return currentDirs;
}
LOG.debug("Loading system dirs ...");
long start = System.currentTimeMillis();
ProjectDirectories loadedDirs = ProjectDirectories.from("io.github", "skylot", "jadx", DirsLoader::getWinDirs);
if (LOG.isDebugEnabled()) {
LOG.debug("Loaded system dirs ({}ms): config: {}, cache: {}",
System.currentTimeMillis() - start, loadedDirs.configDir, loadedDirs.cacheDir);
}
dirs = loadedDirs;
return loadedDirs;
}
/**
* Return JNI, Foreign or PowerShell implementation
*/
private static Windows getWinDirs() {
Windows defSup = Windows.getDefaultSupplier().get();
if (defSup instanceof WindowsPowerShell) {
if (JadxSystemInfo.IS_AMD64) {
// JNI library compiled for x86-64
return new WindowsJni();
}
}
return defSup;
}
public Path getCacheDir() {
return cacheDir;
}
public Path getConfigDir() {
return configDir;
}
}
}
@@ -1,25 +0,0 @@
package jadx.commons.app;
import java.util.Locale;
public class JadxSystemInfo {
public static final String JAVA_VM = System.getProperty("java.vm.name", "?");
public static final String JAVA_VER = System.getProperty("java.version", "?");
public static final String OS_NAME = System.getProperty("os.name", "?");
public static final String OS_ARCH = System.getProperty("os.arch", "?");
public static final String OS_VERSION = System.getProperty("os.version", "?");
private static final String OS_NAME_LOWER = OS_NAME.toLowerCase(Locale.ENGLISH);
public static final boolean IS_WINDOWS = OS_NAME_LOWER.startsWith("windows");
public static final boolean IS_MAC = OS_NAME_LOWER.startsWith("mac");
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);
public static final boolean IS_AMD64 = OS_ARCH_LOWER.equals("amd64");
public static final boolean IS_ARM64 = OS_ARCH_LOWER.equals("aarch64");
private JadxSystemInfo() {
}
}
@@ -1,33 +0,0 @@
package jadx.commons.app;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class JadxTempFiles {
private static final String JADX_TMP_INSTANCE_PREFIX = "jadx-instance-";
private static final Path TEMP_ROOT_DIR = createTempRootDir();
public static Path getTempRootDir() {
return TEMP_ROOT_DIR;
}
private static Path createTempRootDir() {
try {
String jadxTmpDir = System.getenv("JADX_TMP_DIR");
Path dir;
if (jadxTmpDir != null) {
Path customTmpRootDir = Paths.get(jadxTmpDir);
Files.createDirectories(customTmpRootDir);
dir = Files.createTempDirectory(customTmpRootDir, JADX_TMP_INSTANCE_PREFIX);
} else {
dir = Files.createTempDirectory(JADX_TMP_INSTANCE_PREFIX);
}
dir.toFile().deleteOnExit();
return dir;
} catch (Exception e) {
throw new RuntimeException("Failed to create temp root directory", e);
}
}
}
-3
View File
@@ -1,3 +0,0 @@
## jadx zip
Custom zip reader implementation to fight tampering and provide additional security checks
-3
View File
@@ -1,3 +0,0 @@
plugins {
id("jadx-library")
}
@@ -1,36 +0,0 @@
package jadx.zip;
import java.io.File;
import java.io.InputStream;
public interface IZipEntry {
/**
* Zip entry name
*/
String getName();
/**
* Uncompressed bytes
*/
byte[] getBytes();
/**
* Stream of uncompressed bytes.
*/
InputStream getInputStream();
long getCompressedSize();
long getUncompressedSize();
boolean isDirectory();
File getZipFile();
/**
* Return true if {@link #getBytes()} method is more optimal to use other than
* {@link #getInputStream()}
*/
boolean preferBytes();
}
@@ -1,9 +0,0 @@
package jadx.zip;
import java.io.Closeable;
import java.io.IOException;
public interface IZipParser extends Closeable {
ZipContent open() throws IOException;
}
@@ -1,50 +0,0 @@
package jadx.zip;
import java.io.Closeable;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ZipContent implements Closeable {
private static final Logger LOG = LoggerFactory.getLogger(ZipContent.class);
private final IZipParser zipParser;
private final List<IZipEntry> entries;
private final Map<String, IZipEntry> entriesMap;
public ZipContent(IZipParser zipParser, List<IZipEntry> entries) {
this.zipParser = zipParser;
this.entries = entries;
this.entriesMap = buildNameMap(zipParser, entries);
}
private static Map<String, IZipEntry> buildNameMap(IZipParser zipParser, List<IZipEntry> entries) {
Map<String, IZipEntry> map = new HashMap<>(entries.size());
for (IZipEntry entry : entries) {
String name = entry.getName();
IZipEntry prevEntry = map.put(name, entry);
if (prevEntry != null) {
LOG.warn("Found duplicate entry: {} in {}", name, zipParser);
}
}
return map;
}
public List<IZipEntry> getEntries() {
return entries;
}
public @Nullable IZipEntry searchEntry(String fileName) {
return entriesMap.get(fileName);
}
@Override
public void close() throws IOException {
zipParser.close();
}
}
@@ -1,111 +0,0 @@
package jadx.zip;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;
import org.jetbrains.annotations.Nullable;
import jadx.zip.fallback.FallbackZipParser;
import jadx.zip.parser.JadxZipParser;
import jadx.zip.security.IJadxZipSecurity;
import jadx.zip.security.JadxZipSecurity;
/**
* Jadx wrapper to provide custom zip parser ({@link JadxZipParser})
* with fallback to default Java implementation.
*/
public class ZipReader {
private final ZipReaderOptions options;
public ZipReader() {
this(ZipReaderOptions.getDefault());
}
public ZipReader(Set<ZipReaderFlags> flags) {
this(new ZipReaderOptions(new JadxZipSecurity(), flags));
}
public ZipReader(IJadxZipSecurity security) {
this(new ZipReaderOptions(security, ZipReaderFlags.none()));
}
public ZipReader(ZipReaderOptions options) {
this.options = options;
}
@SuppressWarnings("resource")
public ZipContent open(File zipFile) throws IOException {
try {
JadxZipParser jadxParser = new JadxZipParser(zipFile, options);
IZipParser detectedParser = detectParser(zipFile, jadxParser);
if (detectedParser != jadxParser) {
jadxParser.close();
}
return detectedParser.open();
} catch (Exception e) {
if (options.getFlags().contains(ZipReaderFlags.DONT_USE_FALLBACK)) {
throw new IOException("Failed to open zip: " + zipFile, e);
}
// switch to fallback parser
return buildFallbackParser(zipFile).open();
}
}
/**
* Visit valid entries in a zip file.
* Return not null value from visitor to stop iteration.
*/
public <R> @Nullable R visitEntries(File file, Function<IZipEntry, R> visitor) {
try (ZipContent content = open(file)) {
for (IZipEntry entry : content.getEntries()) {
R result = visitor.apply(entry);
if (result != null) {
return result;
}
}
} catch (Exception e) {
throw new RuntimeException("Failed to process zip file: " + file.getAbsolutePath(), e);
}
return null;
}
public void readEntries(File file, BiConsumer<IZipEntry, InputStream> visitor) {
visitEntries(file, entry -> {
if (!entry.isDirectory()) {
try (InputStream in = entry.getInputStream()) {
visitor.accept(entry, in);
} catch (Exception e) {
throw new RuntimeException("Failed to process zip entry: " + entry, e);
}
}
return null;
});
}
public ZipReaderOptions getOptions() {
return options;
}
private IZipParser detectParser(File zipFile, JadxZipParser jadxParser) {
if (zipFile.getName().endsWith(".apk")
|| options.getFlags().contains(ZipReaderFlags.DONT_USE_FALLBACK)) {
return jadxParser;
}
if (!jadxParser.canOpen()) {
return buildFallbackParser(zipFile);
}
// default
if (options.getFlags().contains(ZipReaderFlags.FALLBACK_AS_DEFAULT)) {
return buildFallbackParser(zipFile);
}
return jadxParser;
}
private FallbackZipParser buildFallbackParser(File zipFile) {
return new FallbackZipParser(zipFile, options);
}
}
@@ -1,32 +0,0 @@
package jadx.zip;
import java.util.EnumSet;
import java.util.Set;
public enum ZipReaderFlags {
/**
* Search all local file headers by signature without reading
* 'central directory' and 'end of central directory' entries
*/
IGNORE_CENTRAL_DIR_ENTRIES,
/**
* Enable additional checks to verify zip data and report possible tampering
*/
REPORT_TAMPERING,
/**
* Use fallback (java built-in implementation) parser as default.
* Custom implementation will be used for '*.apk' files only.
*/
FALLBACK_AS_DEFAULT,
/**
* Use only jadx custom parser and do not switch to fallback on errors.
*/
DONT_USE_FALLBACK;
public static Set<ZipReaderFlags> none() {
return EnumSet.noneOf(ZipReaderFlags.class);
}
}
@@ -1,29 +0,0 @@
package jadx.zip;
import java.util.Set;
import jadx.zip.security.IJadxZipSecurity;
import jadx.zip.security.JadxZipSecurity;
public class ZipReaderOptions {
public static ZipReaderOptions getDefault() {
return new ZipReaderOptions(new JadxZipSecurity(), ZipReaderFlags.none());
}
private final IJadxZipSecurity zipSecurity;
private final Set<ZipReaderFlags> flags;
public ZipReaderOptions(IJadxZipSecurity zipSecurity, Set<ZipReaderFlags> flags) {
this.zipSecurity = zipSecurity;
this.flags = flags;
}
public IJadxZipSecurity getZipSecurity() {
return zipSecurity;
}
public Set<ZipReaderFlags> getFlags() {
return flags;
}
}
@@ -1,61 +0,0 @@
package jadx.zip.fallback;
import java.io.File;
import java.io.InputStream;
import java.util.zip.ZipEntry;
import jadx.zip.IZipEntry;
public class FallbackZipEntry implements IZipEntry {
private final FallbackZipParser parser;
private final ZipEntry zipEntry;
public FallbackZipEntry(FallbackZipParser parser, ZipEntry zipEntry) {
this.parser = parser;
this.zipEntry = zipEntry;
}
public ZipEntry getZipEntry() {
return zipEntry;
}
@Override
public String getName() {
return zipEntry.getName();
}
@Override
public boolean preferBytes() {
return false;
}
@Override
public byte[] getBytes() {
return parser.getBytes(this);
}
@Override
public InputStream getInputStream() {
return parser.getInputStream(this);
}
@Override
public long getCompressedSize() {
return zipEntry.getCompressedSize();
}
@Override
public long getUncompressedSize() {
return zipEntry.getSize();
}
@Override
public boolean isDirectory() {
return zipEntry.isDirectory();
}
@Override
public File getZipFile() {
return parser.getZipFile();
}
}
@@ -1,109 +0,0 @@
package jadx.zip.fallback;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.zip.IZipEntry;
import jadx.zip.IZipParser;
import jadx.zip.ZipContent;
import jadx.zip.ZipReaderOptions;
import jadx.zip.io.LimitedInputStream;
import jadx.zip.security.IJadxZipSecurity;
public class FallbackZipParser implements IZipParser {
private static final Logger LOG = LoggerFactory.getLogger(FallbackZipParser.class);
private final File file;
private final IJadxZipSecurity zipSecurity;
private final boolean useLimitedDataStream;
private ZipFile zipFile;
public FallbackZipParser(File file, ZipReaderOptions options) {
this.file = file;
this.zipSecurity = options.getZipSecurity();
this.useLimitedDataStream = zipSecurity.useLimitedDataStream();
}
@Override
public ZipContent open() throws IOException {
zipFile = new ZipFile(file);
int maxEntriesCount = zipSecurity.getMaxEntriesCount();
if (maxEntriesCount == -1) {
maxEntriesCount = Integer.MAX_VALUE;
}
List<IZipEntry> list = new ArrayList<>();
Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
FallbackZipEntry zipEntry = new FallbackZipEntry(this, entries.nextElement());
if (isValidEntry(zipEntry)) {
list.add(zipEntry);
if (list.size() > maxEntriesCount) {
throw new IllegalStateException("Max entries count limit exceeded: " + list.size());
}
}
}
return new ZipContent(this, list);
}
private boolean isValidEntry(IZipEntry zipEntry) {
boolean validEntry = zipSecurity.isValidEntry(zipEntry);
if (!validEntry) {
LOG.warn("Zip entry '{}' is invalid and excluded from processing", zipEntry);
}
return validEntry;
}
public byte[] getBytes(FallbackZipEntry entry) {
try (InputStream is = getEntryStream(entry)) {
return is.readAllBytes();
} catch (Exception e) {
throw new RuntimeException("Failed to read bytes for entry: " + entry.getName(), e);
}
}
public InputStream getInputStream(FallbackZipEntry entry) {
try {
return getEntryStream(entry);
} catch (Exception e) {
throw new RuntimeException("Failed to open input stream for entry: " + entry.getName(), e);
}
}
private InputStream getEntryStream(FallbackZipEntry entry) throws IOException {
InputStream entryStream = zipFile.getInputStream(entry.getZipEntry());
InputStream stream;
if (useLimitedDataStream) {
stream = new LimitedInputStream(entryStream, entry.getUncompressedSize());
} else {
stream = entryStream;
}
return new BufferedInputStream(stream);
}
public File getZipFile() {
return file;
}
@Override
public void close() throws IOException {
try {
if (zipFile != null) {
zipFile.close();
}
} finally {
zipFile = null;
}
}
}
@@ -1,46 +0,0 @@
package jadx.zip.io;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
public class ByteBufferBackedInputStream extends InputStream {
private final ByteBuffer buf;
private int markedPosition = 0;
public ByteBufferBackedInputStream(ByteBuffer buf) {
this.buf = buf;
}
public int read() throws IOException {
if (!buf.hasRemaining()) {
return -1;
}
return buf.get() & 0xFF;
}
@SuppressWarnings("NullableProblems")
public int read(byte[] bytes, int off, int len) throws IOException {
if (!buf.hasRemaining()) {
return -1;
}
int readLen = Math.min(len, buf.remaining());
buf.get(bytes, off, readLen);
return readLen;
}
@Override
public boolean markSupported() {
return true;
}
@Override
public synchronized void mark(int unused) {
markedPosition = buf.position();
}
@Override
public synchronized void reset() {
buf.position(markedPosition);
}
}
@@ -1,93 +0,0 @@
package jadx.zip.parser;
import java.io.File;
import java.io.InputStream;
import jadx.zip.IZipEntry;
public final class JadxZipEntry implements IZipEntry {
private final JadxZipParser parser;
private final String fileName;
private final int compressMethod;
private final int entryStart;
private final int dataStart;
private final long compressedSize;
private final long uncompressedSize;
JadxZipEntry(JadxZipParser parser, String fileName, int entryStart, int dataStart,
int compressMethod, long compressedSize, long uncompressedSize) {
this.parser = parser;
this.fileName = fileName;
this.entryStart = entryStart;
this.dataStart = dataStart;
this.compressMethod = compressMethod;
this.compressedSize = compressedSize;
this.uncompressedSize = uncompressedSize;
}
public boolean isSizesValid() {
if (compressedSize <= 0) {
return false;
}
if (uncompressedSize <= 0) {
return false;
}
return compressedSize <= uncompressedSize;
}
public String getName() {
return fileName;
}
@Override
public long getCompressedSize() {
return compressedSize;
}
@Override
public long getUncompressedSize() {
return uncompressedSize;
}
@Override
public boolean isDirectory() {
return fileName.endsWith("/");
}
@Override
public boolean preferBytes() {
return true;
}
@Override
public byte[] getBytes() {
return parser.getBytes(this);
}
@Override
public InputStream getInputStream() {
return parser.getInputStream(this);
}
public int getEntryStart() {
return entryStart;
}
public int getDataStart() {
return dataStart;
}
public int getCompressMethod() {
return compressMethod;
}
@Override
public File getZipFile() {
return parser.getZipFile();
}
@Override
public String toString() {
return parser.getZipFile().getName() + ':' + fileName;
}
}
@@ -1,439 +0,0 @@
package jadx.zip.parser;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.zip.IZipEntry;
import jadx.zip.IZipParser;
import jadx.zip.ZipContent;
import jadx.zip.ZipReaderFlags;
import jadx.zip.ZipReaderOptions;
import jadx.zip.fallback.FallbackZipParser;
import jadx.zip.io.ByteBufferBackedInputStream;
import jadx.zip.io.LimitedInputStream;
import jadx.zip.security.IJadxZipSecurity;
/**
* Custom and simple zip parser to fight tampering.
* Many zip features aren't supported:
* - Compression methods other than STORE or DEFLATE
* - Zip64
* - Checksum verification
* - Multi file archives
*/
public final class JadxZipParser implements IZipParser {
private static final Logger LOG = LoggerFactory.getLogger(JadxZipParser.class);
private static final byte LOCAL_FILE_HEADER_START = 0x50;
private static final int LOCAL_FILE_HEADER_SIGN = 0x04034b50;
private static final int CD_SIGN = 0x02014b50;
private static final int END_OF_CD_SIGN = 0x06054b50;
private final File zipFile;
private final ZipReaderOptions options;
private final IJadxZipSecurity zipSecurity;
private final Set<ZipReaderFlags> flags;
private final boolean verify;
private final boolean useLimitedDataStream;
private RandomAccessFile file;
private FileChannel fileChannel;
private ByteBuffer byteBuffer;
private int endOfCDStart = -2;
private @Nullable ZipContent fallbackZipContent;
public JadxZipParser(File zipFile, ZipReaderOptions options) {
this.zipFile = zipFile;
this.options = options;
this.zipSecurity = options.getZipSecurity();
this.flags = options.getFlags();
this.verify = options.getFlags().contains(ZipReaderFlags.REPORT_TAMPERING);
this.useLimitedDataStream = zipSecurity.useLimitedDataStream();
}
@Override
public ZipContent open() throws IOException {
load();
try {
int maxEntriesCount = zipSecurity.getMaxEntriesCount();
if (maxEntriesCount == -1) {
maxEntriesCount = Integer.MAX_VALUE;
}
List<IZipEntry> entries;
if (flags.contains(ZipReaderFlags.IGNORE_CENTRAL_DIR_ENTRIES)) {
entries = searchLocalFileHeaders(maxEntriesCount);
} else {
entries = loadFromCentralDirs(maxEntriesCount);
}
return new ZipContent(this, entries);
} catch (Exception e) {
if (flags.contains(ZipReaderFlags.DONT_USE_FALLBACK)) {
throw new IOException("Failed to open zip: " + zipFile + ", error: " + e.getMessage(), e);
}
LOG.warn("Zip open failed, switching to fallback parser, zip: {}", zipFile, e);
return initFallbackParser();
}
}
@SuppressWarnings("RedundantIfStatement")
public boolean canOpen() {
try {
load();
int eocdStart = searchEndOfCDStart();
ByteBuffer buf = byteBuffer;
buf.position(eocdStart + 4);
int diskNum = readU2(buf);
if (diskNum == 0xFFFF) {
// Zip64
return false;
}
return true;
} catch (Exception e) {
LOG.warn("Jadx parser can't open zip file: {}", zipFile, e);
return false;
}
}
private boolean isValidEntry(JadxZipEntry zipEntry) {
boolean validEntry = zipSecurity.isValidEntry(zipEntry);
if (!validEntry) {
LOG.warn("Zip entry '{}' is invalid and excluded from processing", zipEntry);
}
return validEntry;
}
private void load() throws IOException {
if (byteBuffer != null) {
// already loaded
return;
}
file = new RandomAccessFile(zipFile, "r");
long size = file.length();
if (size >= Integer.MAX_VALUE) {
throw new IOException("Zip file is too big");
}
int fileLen = (int) size;
if (fileLen < 100 * 1024 * 1024) {
// load files smaller than 100MB directly into memory
byte[] bytes = new byte[fileLen];
file.readFully(bytes);
byteBuffer = ByteBuffer.wrap(bytes).asReadOnlyBuffer();
file.close();
file = null;
} else {
// for big files - use a memory mapped file
fileChannel = file.getChannel();
byteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
}
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
}
private List<IZipEntry> searchLocalFileHeaders(int maxEntriesCount) {
List<IZipEntry> entries = new ArrayList<>();
while (true) {
int start = searchEntryStart();
if (start == -1) {
return entries;
}
JadxZipEntry zipEntry = loadFileEntry(start);
if (isValidEntry(zipEntry)) {
entries.add(zipEntry);
if (entries.size() > maxEntriesCount) {
throw new IllegalStateException("Max entries count limit exceeded: " + entries.size());
}
}
}
}
private List<IZipEntry> loadFromCentralDirs(int maxEntriesCount) throws IOException {
int eocdStart = searchEndOfCDStart();
if (eocdStart < 0) {
throw new RuntimeException("End of central directory not found");
}
ByteBuffer buf = byteBuffer;
buf.position(eocdStart + 10);
int entriesCount = readU2(buf);
buf.position(eocdStart + 16);
int cdOffset = buf.getInt();
if (entriesCount > maxEntriesCount) {
throw new IllegalStateException("Max entries count limit exceeded: " + entriesCount);
}
List<IZipEntry> entries = new ArrayList<>(entriesCount);
buf.position(cdOffset);
for (int i = 0; i < entriesCount; i++) {
JadxZipEntry zipEntry = loadCDEntry();
if (isValidEntry(zipEntry)) {
entries.add(zipEntry);
}
}
return entries;
}
private JadxZipEntry loadCDEntry() {
ByteBuffer buf = byteBuffer;
int start = buf.position();
buf.position(start + 28);
int fileNameLen = readU2(buf);
int extraFieldLen = readU2(buf);
int commentLen = readU2(buf);
buf.position(start + 42);
int fileEntryStart = buf.getInt();
int entryEnd = start + 46 + fileNameLen + extraFieldLen + commentLen;
JadxZipEntry entry = loadFileEntry(fileEntryStart);
if (verify) {
compareCDAndLFH(buf, start, entry);
}
if (!entry.isSizesValid()) {
entry = fixEntryFromCD(entry, start);
}
buf.position(entryEnd);
return entry;
}
private JadxZipEntry fixEntryFromCD(JadxZipEntry entry, int start) {
ByteBuffer buf = byteBuffer;
buf.position(start + 10);
int comprMethod = readU2(buf);
buf.position(start + 20);
int comprSize = buf.getInt();
int unComprSize = buf.getInt();
return new JadxZipEntry(this, entry.getName(), start, entry.getDataStart(), comprMethod, comprSize, unComprSize);
}
private static void compareCDAndLFH(ByteBuffer buf, int start, JadxZipEntry entry) {
buf.position(start + 10);
int comprMethod = readU2(buf);
if (comprMethod != entry.getCompressMethod()) {
LOG.warn("Compression method differ in CD {} and LFH {} for {}",
comprMethod, entry.getCompressMethod(), entry);
}
buf.position(start + 20);
int comprSize = buf.getInt();
int unComprSize = buf.getInt();
if (comprSize != entry.getCompressedSize()) {
LOG.warn("Compressed size differ in CD {} and LFH {} for {}",
comprSize, entry.getCompressedSize(), entry);
}
if (unComprSize != entry.getUncompressedSize()) {
LOG.warn("Uncompressed size differ in CD {} and LFH {} for {}",
unComprSize, entry.getUncompressedSize(), entry);
}
}
private JadxZipEntry loadFileEntry(int start) {
ByteBuffer buf = byteBuffer;
buf.position(start + 8);
int comprMethod = readU2(buf);
buf.position(start + 18);
int comprSize = buf.getInt();
int unComprSize = buf.getInt();
int fileNameLen = readU2(buf);
int extraFieldLen = readU2(buf);
String fileName = readString(buf, fileNameLen);
int dataStart = start + 30 + fileNameLen + extraFieldLen;
buf.position(dataStart + comprSize);
return new JadxZipEntry(this, fileName, start, dataStart, comprMethod, comprSize, unComprSize);
}
private int searchEndOfCDStart() throws IOException {
if (endOfCDStart != -2) {
return endOfCDStart;
}
ByteBuffer buf = byteBuffer;
int pos = buf.limit() - 22;
int minPos = Math.max(0, pos - 0xffff);
while (true) {
buf.position(pos);
int sign = buf.getInt();
if (sign == END_OF_CD_SIGN) {
endOfCDStart = pos;
return pos;
}
pos--;
if (pos < minPos) {
throw new IOException("End of central directory record not found");
}
}
}
private int searchEntryStart() {
ByteBuffer buf = byteBuffer;
while (true) {
int start = buf.position();
if (start + 4 > buf.limit()) {
return -1;
}
byte b = buf.get();
if (b == LOCAL_FILE_HEADER_START) {
buf.position(start);
int sign = buf.getInt();
if (sign == LOCAL_FILE_HEADER_SIGN) {
return start;
}
}
}
}
synchronized InputStream getInputStream(JadxZipEntry entry) {
if (verify) {
verifyEntry(entry);
}
InputStream stream;
if (entry.getCompressMethod() == 8) {
try {
stream = ZipDeflate.decompressEntryToStream(byteBuffer, entry);
} catch (Exception e) {
entryParseFailed(entry, e);
return useFallbackParser(entry).getInputStream();
}
} else {
// treat any other compression methods values as UNCOMPRESSED
stream = bufferToStream(byteBuffer, entry.getDataStart(), (int) entry.getUncompressedSize());
}
if (useLimitedDataStream) {
return new LimitedInputStream(stream, entry.getUncompressedSize());
}
return stream;
}
synchronized byte[] getBytes(JadxZipEntry entry) {
if (verify) {
verifyEntry(entry);
}
if (entry.getCompressMethod() == 8) {
try {
return ZipDeflate.decompressEntryToBytes(byteBuffer, entry);
} catch (Exception e) {
entryParseFailed(entry, e);
return useFallbackParser(entry).getBytes();
}
}
// treat any other compression methods values as UNCOMPRESSED
return bufferToBytes(byteBuffer, entry.getDataStart(), (int) entry.getUncompressedSize());
}
private static void verifyEntry(JadxZipEntry entry) {
int compressMethod = entry.getCompressMethod();
if (compressMethod == 0) {
if (entry.getCompressedSize() != entry.getUncompressedSize()) {
LOG.warn("Not equal sizes for STORE method: compressed: {}, uncompressed: {}, entry: {}",
entry.getCompressedSize(), entry.getUncompressedSize(), entry);
}
} else if (compressMethod != 8) {
LOG.warn("Unknown compress method: {} in entry: {}", compressMethod, entry);
}
}
private void entryParseFailed(JadxZipEntry entry, Exception e) {
if (isEncrypted(entry)) {
throw new RuntimeException("Entry is encrypted, failed to decompress: " + entry, e);
}
if (flags.contains(ZipReaderFlags.DONT_USE_FALLBACK)) {
throw new RuntimeException("Failed to decompress zip entry: " + entry + ", error: " + e.getMessage(), e);
}
LOG.warn("Entry '{}' parse failed, switching to fallback parser", entry, e);
}
@SuppressWarnings("resource")
private IZipEntry useFallbackParser(JadxZipEntry entry) {
LOG.debug("useFallbackParser used for {}", entry);
IZipEntry zipEntry = initFallbackParser().searchEntry(entry.getName());
if (zipEntry == null) {
throw new RuntimeException("Fallback parser can't find entry: " + entry);
}
return zipEntry;
}
@SuppressWarnings("resource")
private ZipContent initFallbackParser() {
if (fallbackZipContent == null) {
try {
fallbackZipContent = new FallbackZipParser(zipFile, options).open();
} catch (Exception e) {
throw new RuntimeException("Fallback parser failed to open file: " + zipFile, e);
}
}
return fallbackZipContent;
}
private boolean isEncrypted(JadxZipEntry entry) {
int flags = readFlags(entry);
return (flags & 1) != 0;
}
private int readFlags(JadxZipEntry entry) {
ByteBuffer buf = byteBuffer;
buf.position(entry.getEntryStart() + 6);
return readU2(buf);
}
static byte[] bufferToBytes(ByteBuffer buf, int start, int size) {
byte[] data = new byte[size];
buf.position(start);
buf.get(data);
return data;
}
static InputStream bufferToStream(ByteBuffer buf, int start, int size) {
buf.position(start);
ByteBuffer streamBuf = buf.slice();
streamBuf.limit(size);
return new ByteBufferBackedInputStream(streamBuf);
}
private static int readU2(ByteBuffer buf) {
return buf.getShort() & 0xFFFF;
}
private static String readString(ByteBuffer buf, int fileNameLen) {
byte[] bytes = new byte[fileNameLen];
buf.get(bytes);
return new String(bytes, StandardCharsets.UTF_8);
}
@Override
public void close() throws IOException {
try {
if (fileChannel != null) {
fileChannel.close();
}
if (file != null) {
file.close();
}
if (fallbackZipContent != null) {
fallbackZipContent.close();
}
} finally {
fileChannel = null;
file = null;
byteBuffer = null;
endOfCDStart = -2;
fallbackZipContent = null;
}
}
public File getZipFile() {
return zipFile;
}
@Override
public String toString() {
return "JadxZipParser{" + zipFile + '}';
}
}
@@ -1,38 +0,0 @@
package jadx.zip.parser;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
import static jadx.zip.parser.JadxZipParser.bufferToStream;
final class ZipDeflate {
private static final int BUFFER_SIZE = 4096;
static byte[] decompressEntryToBytes(ByteBuffer buf, JadxZipEntry entry) throws DataFormatException {
buf.position(entry.getDataStart());
ByteBuffer entryBuf = buf.slice();
entryBuf.limit((int) entry.getCompressedSize());
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);
int written = inflater.inflate(out);
inflater.end();
if (written != out.length) {
throw new DataFormatException("Unexpected size of decompressed entry: " + entry
+ ", got: " + written + ", expected: " + out.length);
}
return out;
}
static InputStream decompressEntryToStream(ByteBuffer buf, JadxZipEntry entry) {
InputStream stream = bufferToStream(buf, entry.getDataStart(), (int) entry.getCompressedSize());
Inflater inflater = new Inflater(true);
return new InflaterInputStream(stream, inflater, BUFFER_SIZE);
}
}
@@ -1,35 +0,0 @@
package jadx.zip.security;
import java.io.File;
import jadx.zip.IZipEntry;
public class DisabledZipSecurity implements IJadxZipSecurity {
public static final DisabledZipSecurity INSTANCE = new DisabledZipSecurity();
@Override
public boolean isValidEntry(IZipEntry entry) {
return true;
}
@Override
public boolean isValidEntryName(String entryName) {
return true;
}
@Override
public boolean isInSubDirectory(File baseDir, File file) {
return true;
}
@Override
public boolean useLimitedDataStream() {
return false;
}
@Override
public int getMaxEntriesCount() {
return -1;
}
}
@@ -1,35 +0,0 @@
package jadx.zip.security;
import java.io.File;
import jadx.zip.IZipEntry;
public interface IJadxZipSecurity {
/**
* Check if zip entry is valid and safe to process
*/
boolean isValidEntry(IZipEntry entry);
/**
* Check if the zip entry name is valid.
* This check should be part of {@link #isValidEntry(IZipEntry)} method.
*/
boolean isValidEntryName(String entryName);
/**
* Use limited InputStream for entry uncompressed data
*/
boolean useLimitedDataStream();
/**
* Max entries count expected in a zip file, fail zip open if the limit exceeds.
* Return -1 to disable entries count check.
*/
int getMaxEntriesCount();
/**
* Check if a file will be inside baseDir after a system resolves its path
*/
boolean isInSubDirectory(File baseDir, File file);
}
@@ -1,129 +0,0 @@
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;
import jadx.zip.IZipEntry;
public class JadxZipSecurity implements IJadxZipSecurity {
private static final Logger LOG = LoggerFactory.getLogger(JadxZipSecurity.class);
private static final Path CWD = Paths.get(".").toAbsolutePath().normalize();
/**
* The size of uncompressed zip entry shouldn't be bigger of compressed in zipBombDetectionFactor
* times
*/
private int zipBombDetectionFactor = 100;
/**
* Zip entries that have an uncompressed size of less than zipBombMinUncompressedSize are considered
* safe
*/
private int zipBombMinUncompressedSize = 25 * 1024 * 1024;
private int maxEntriesCount = 100_000;
private boolean useLimitedDataStream = true;
@Override
public boolean isValidEntry(IZipEntry entry) {
return isValidEntryName(entry.getName()) && !isZipBomb(entry);
}
@Override
public boolean useLimitedDataStream() {
return useLimitedDataStream;
}
@Override
public int getMaxEntriesCount() {
return maxEntriesCount;
}
/**
* Checks that entry name contains no any traversals and prevents cases like "../classes.dex",
* to limit output only to the specified directory
*/
@Override
public boolean isValidEntryName(String entryName) {
if (entryName.contains("..")) { // quick pre-check
if (entryName.contains("../") || entryName.contains("..\\")) {
LOG.error("Path traversal attack detected in entry: '{}'", entryName);
return false;
}
}
// 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 {
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;
}
@Override
public boolean isInSubDirectory(File baseDir, File file) {
try {
return isInSubDirectoryInternal(baseDir.getCanonicalFile(), file.getCanonicalFile());
} catch (IOException e) {
return false;
}
}
public boolean isZipBomb(IZipEntry entry) {
long compressedSize = entry.getCompressedSize();
long uncompressedSize = entry.getUncompressedSize();
boolean invalidSize = compressedSize < 0 || uncompressedSize < 0;
boolean possibleZipBomb = uncompressedSize >= zipBombMinUncompressedSize
&& compressedSize * zipBombDetectionFactor < uncompressedSize;
if (invalidSize || possibleZipBomb) {
LOG.error("Potential zip bomb attack detected, invalid sizes: compressed {}, uncompressed {}, name {}",
compressedSize, uncompressedSize, entry.getName());
return true;
}
return false;
}
private static boolean isInSubDirectoryInternal(File baseDir, File file) {
File current = file;
while (true) {
if (current == null) {
return false;
}
if (current.equals(baseDir)) {
return true;
}
current = current.getParentFile();
}
}
public void setMaxEntriesCount(int maxEntriesCount) {
this.maxEntriesCount = maxEntriesCount;
}
public void setZipBombDetectionFactor(int zipBombDetectionFactor) {
this.zipBombDetectionFactor = zipBombDetectionFactor;
}
public void setZipBombMinUncompressedSize(int zipBombMinUncompressedSize) {
this.zipBombMinUncompressedSize = zipBombMinUncompressedSize;
}
public void setUseLimitedDataStream(boolean useLimitedDataStream) {
this.useLimitedDataStream = useLimitedDataStream;
}
}
+8 -30
View File
@@ -4,11 +4,14 @@ plugins {
dependencies {
api(project(":jadx-plugins:jadx-input-api"))
api(project(":jadx-commons:jadx-zip"))
implementation("com.google.code.gson:gson:2.13.2")
implementation("com.google.code.gson:gson:2.10.1")
testImplementation("org.apache.commons:commons-lang3:3.20.0")
// TODO: move resources decoding to separate plugin module
implementation("com.android.tools.build:aapt2-proto:8.2.2-10154469")
implementation("com.google.protobuf:protobuf-java:3.25.2") // forcing latest version
testImplementation("org.apache.commons:commons-lang3:3.14.0")
testImplementation(project(":jadx-plugins:jadx-dex-input"))
testRuntimeOnly(project(":jadx-plugins:jadx-smali-input"))
@@ -22,34 +25,9 @@ dependencies {
strictly("[3.33, 3.34[") // from 3.34 compiled with Java 17
}
}
testImplementation("tools.profiler:async-profiler:4.2")
testImplementation("tools.profiler:async-profiler:3.0")
}
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()
if (testJavaVer < currentJavaVer) {
throw GradleException("'$envVarName' can't be set to lower version than $currentJavaVer")
}
println("Set Java toolchain for core tests to version '$testJavaVer'")
return testJavaVer
}
tasks.named<Test>("test") {
jadxTestJavaVersion?.let { testJavaVer ->
javaLauncher =
javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(testJavaVer)
}
}
// disable cache to allow test's rerun,
// because most tests are integration and depends on plugins and environment
outputs.cacheIf { false }
// exclude temp tests
tasks.test {
exclude("**/tmp/*")
}
@@ -19,18 +19,5 @@ public enum DecompilationMode {
/**
* Raw instructions without modifications
*/
FALLBACK;
public boolean isSpecial() {
switch (this) {
case AUTO:
case RESTRUCTURE:
return false;
case SIMPLE:
case FALLBACK:
return true;
default:
throw new RuntimeException("Unexpected decompilation mode: " + this);
}
}
FALLBACK
}
@@ -8,6 +8,8 @@ import jadx.api.metadata.ICodeAnnotation;
import jadx.api.metadata.ICodeNodeRef;
public interface ICodeWriter {
String NL = System.getProperty("line.separator");
String INDENT_STR = " ";
boolean isMetadataSupported();
+13 -184
View File
@@ -4,9 +4,9 @@ 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;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -21,7 +21,6 @@ import org.slf4j.LoggerFactory;
import jadx.api.args.GeneratedRenamesMappingFileMode;
import jadx.api.args.IntegerFormat;
import jadx.api.args.ResourceNameSource;
import jadx.api.args.UseSourceNameAsClassNameAlias;
import jadx.api.args.UserRenamesMappingsMode;
import jadx.api.data.ICodeData;
import jadx.api.deobf.IAliasProvider;
@@ -30,18 +29,12 @@ import jadx.api.impl.AnnotatedCodeWriter;
import jadx.api.impl.InMemoryCodeCache;
import jadx.api.plugins.loader.JadxBasePluginLoader;
import jadx.api.plugins.loader.JadxPluginLoader;
import jadx.api.security.IJadxSecurity;
import jadx.api.security.JadxSecurityFlag;
import jadx.api.security.impl.JadxSecurity;
import jadx.api.usage.IUsageInfoCache;
import jadx.api.usage.impl.InMemoryUsageInfoCache;
import jadx.core.deobf.DeobfAliasProvider;
import jadx.core.deobf.conditions.DeobfWhitelist;
import jadx.core.deobf.conditions.JadxRenameConditions;
import jadx.core.export.ExportGradleType;
import jadx.core.plugins.PluginContext;
import jadx.core.plugins.files.IJadxFilesGetter;
import jadx.core.plugins.files.TempFilesGetter;
import jadx.core.utils.files.FileUtils;
public class JadxArgs implements Closeable {
@@ -49,9 +42,6 @@ public class JadxArgs implements Closeable {
public static final int DEFAULT_THREADS_COUNT = Math.max(1, Runtime.getRuntime().availableProcessors() / 2);
public static final String DEFAULT_NEW_LINE_STR = System.lineSeparator();
public static final String DEFAULT_INDENT_STR = " ";
public static final String DEFAULT_OUT_DIR = "jadx-output";
public static final String DEFAULT_SRC_DIR = "sources";
public static final String DEFAULT_RES_DIR = "resources";
@@ -90,7 +80,6 @@ public class JadxArgs implements Closeable {
private boolean skipResources = false;
private boolean skipSources = false;
private boolean useHeadersForDetectResourceExtensions;
/**
* Predicate that allows to filter the classes to be process based on their full name
@@ -106,8 +95,7 @@ public class JadxArgs implements Closeable {
private UserRenamesMappingsMode userRenamesMappingsMode = UserRenamesMappingsMode.getDefault();
private boolean deobfuscationOn = false;
private UseSourceNameAsClassNameAlias useSourceNameAsClassNameAlias = UseSourceNameAsClassNameAlias.getDefault();
private int sourceNameRepeatLimit = 10;
private boolean useSourceNameAsClassAlias = false;
private File generatedRenamesMappingFile = null;
private GeneratedRenamesMappingFileMode generatedRenamesMappingFileMode = GeneratedRenamesMappingFileMode.getDefault();
@@ -134,9 +122,7 @@ public class JadxArgs implements Closeable {
private boolean escapeUnicode = false;
private boolean replaceConsts = true;
private boolean respectBytecodeAccModifiers = false;
private @Nullable ExportGradleType exportGradleType = null;
private boolean restoreSwitchOverString = true;
private boolean exportAsGradleProject = false;
private boolean skipXmlPrettyPrint = false;
@@ -158,20 +144,10 @@ public class JadxArgs implements Closeable {
private ICodeData codeData;
private String codeNewLineStr = DEFAULT_NEW_LINE_STR;
private String codeIndentStr = DEFAULT_INDENT_STR;
private CommentsLevel commentsLevel = CommentsLevel.INFO;
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 {
@@ -180,36 +156,13 @@ public class JadxArgs implements Closeable {
private UseKotlinMethodsForVarNames useKotlinMethodsForVarNames = UseKotlinMethodsForVarNames.APPLY;
/**
* Additional files structure info.
* Defaults to tmp dirs.
*/
private IJadxFilesGetter filesGetter = TempFilesGetter.INSTANCE;
/**
* Additional data validation and security checks
*/
private IJadxSecurity security = new JadxSecurity(JadxSecurityFlag.all());
/**
* Don't save files (can be using for performance testing)
*/
private boolean skipFilesSave = false;
/**
* Run additional expensive checks to verify internal invariants and info integrity
*/
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<>();
private JadxPluginLoader pluginLoader = new JadxBasePluginLoader();
private boolean loadJadxClsSetFile = true;
@@ -249,12 +202,8 @@ public class JadxArgs implements Closeable {
return inputFiles;
}
public void addInputFile(File inputFile) {
this.inputFiles.add(inputFile);
}
public void setInputFile(File inputFile) {
addInputFile(inputFile);
this.inputFiles = Collections.singletonList(inputFile);
}
public void setInputFiles(List<File> inputFiles) {
@@ -469,37 +418,12 @@ public class JadxArgs implements Closeable {
this.generatedRenamesMappingFileMode = mode;
}
public UseSourceNameAsClassNameAlias getUseSourceNameAsClassNameAlias() {
return useSourceNameAsClassNameAlias;
}
public void setUseSourceNameAsClassNameAlias(UseSourceNameAsClassNameAlias useSourceNameAsClassNameAlias) {
this.useSourceNameAsClassNameAlias = useSourceNameAsClassNameAlias;
}
public int getSourceNameRepeatLimit() {
return sourceNameRepeatLimit;
}
public void setSourceNameRepeatLimit(int sourceNameRepeatLimit) {
this.sourceNameRepeatLimit = sourceNameRepeatLimit;
}
/**
* @deprecated Use {@link #getUseSourceNameAsClassNameAlias()} instead.
*/
@Deprecated
public boolean isUseSourceNameAsClassAlias() {
return getUseSourceNameAsClassNameAlias().toBoolean();
return useSourceNameAsClassAlias;
}
/**
* @deprecated Use {@link #setUseSourceNameAsClassNameAlias(UseSourceNameAsClassNameAlias)} instead.
*/
@Deprecated
public void setUseSourceNameAsClassAlias(boolean useSourceNameAsClassAlias) {
final var useSourceNameAsClassNameAlias = UseSourceNameAsClassNameAlias.create(useSourceNameAsClassAlias);
setUseSourceNameAsClassNameAlias(useSourceNameAsClassNameAlias);
this.useSourceNameAsClassAlias = useSourceNameAsClassAlias;
}
public int getDeobfuscationMinLength() {
@@ -583,33 +507,11 @@ public class JadxArgs implements Closeable {
}
public boolean isExportAsGradleProject() {
return exportGradleType != null;
return exportAsGradleProject;
}
public void setExportAsGradleProject(boolean exportAsGradleProject) {
if (exportAsGradleProject) {
if (exportGradleType == null) {
exportGradleType = ExportGradleType.AUTO;
}
} else {
exportGradleType = null;
}
}
public @Nullable ExportGradleType getExportGradleType() {
return exportGradleType;
}
public void setExportGradleType(@Nullable ExportGradleType exportGradleType) {
this.exportGradleType = exportGradleType;
}
public boolean isRestoreSwitchOverString() {
return restoreSwitchOverString;
}
public void setRestoreSwitchOverString(boolean restoreSwitchOverString) {
this.restoreSwitchOverString = restoreSwitchOverString;
this.exportAsGradleProject = exportAsGradleProject;
}
public boolean isSkipXmlPrettyPrint() {
@@ -720,22 +622,6 @@ public class JadxArgs implements Closeable {
this.codeData = codeData;
}
public String getCodeIndentStr() {
return codeIndentStr;
}
public void setCodeIndentStr(String codeIndentStr) {
this.codeIndentStr = codeIndentStr;
}
public String getCodeNewLineStr() {
return codeNewLineStr;
}
public void setCodeNewLineStr(String codeNewLineStr) {
this.codeNewLineStr = codeNewLineStr;
}
public CommentsLevel getCommentsLevel() {
return commentsLevel;
}
@@ -752,14 +638,6 @@ 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;
}
@@ -776,22 +654,6 @@ public class JadxArgs implements Closeable {
this.useKotlinMethodsForVarNames = useKotlinMethodsForVarNames;
}
public IJadxFilesGetter getFilesGetter() {
return filesGetter;
}
public void setFilesGetter(IJadxFilesGetter filesGetter) {
this.filesGetter = filesGetter;
}
public IJadxSecurity getSecurity() {
return security;
}
public void setSecurity(IJadxSecurity security) {
this.security = security;
}
public boolean isSkipFilesSave() {
return skipFilesSave;
}
@@ -800,18 +662,6 @@ public class JadxArgs implements Closeable {
this.skipFilesSave = skipFilesSave;
}
public boolean isRunDebugChecks() {
return runDebugChecks;
}
public void setRunDebugChecks(boolean runDebugChecks) {
this.runDebugChecks = runDebugChecks;
}
public List<String> getDisabledPasses() {
return disabledPasses;
}
public Map<String, String> getPluginOptions() {
return pluginOptions;
}
@@ -820,14 +670,6 @@ public class JadxArgs implements Closeable {
this.pluginOptions = pluginOptions;
}
public Set<String> getDisabledPlugins() {
return disabledPlugins;
}
public void setDisabledPlugins(Set<String> disabledPlugins) {
this.disabledPlugins = disabledPlugins;
}
public JadxPluginLoader getPluginLoader() {
return pluginLoader;
}
@@ -844,14 +686,6 @@ public class JadxArgs implements Closeable {
this.loadJadxClsSetFile = loadJadxClsSetFile;
}
public void setUseHeadersForDetectResourceExtensions(boolean useHeadersForDetectResourceExtensions) {
this.useHeadersForDetectResourceExtensions = useHeadersForDetectResourceExtensions;
}
public boolean isUseHeadersForDetectResourceExtensions() {
return useHeadersForDetectResourceExtensions;
}
/**
* Hash of all options that can change result code
*/
@@ -859,13 +693,12 @@ public class JadxArgs implements Closeable {
String argStr = "args:" + decompilationMode + useImports + showInconsistentCode
+ inlineAnonymousClasses + inlineMethods + moveInnerClasses + allowInlineKotlinLambda
+ deobfuscationOn + deobfuscationMinLength + deobfuscationMaxLength + deobfuscationWhitelist
+ useSourceNameAsClassNameAlias + sourceNameRepeatLimit
+ resourceNameSource + useHeadersForDetectResourceExtensions
+ resourceNameSource
+ useKotlinMethodsForVarNames
+ insertDebugLines + extractFinally
+ debugInfo + escapeUnicode + replaceConsts + restoreSwitchOverString
+ debugInfo + useSourceNameAsClassAlias + escapeUnicode + replaceConsts
+ respectBytecodeAccModifiers + fsCaseSensitive + renameFlags
+ commentsLevel + useDxInput + integerFormat + typeUpdatesLimitCount
+ commentsLevel + useDxInput + integerFormat
+ "|" + buildPluginsHash(decompiler);
return FileUtils.md5Sum(argStr);
}
@@ -899,8 +732,7 @@ public class JadxArgs implements Closeable {
+ ", generatedRenamesMappingFile=" + generatedRenamesMappingFile
+ ", generatedRenamesMappingFileMode=" + generatedRenamesMappingFileMode
+ ", resourceNameSource=" + resourceNameSource
+ ", useSourceNameAsClassNameAlias=" + useSourceNameAsClassNameAlias
+ ", sourceNameRepeatLimit=" + sourceNameRepeatLimit
+ ", useSourceNameAsClassAlias=" + useSourceNameAsClassAlias
+ ", useKotlinMethodsForVarNames=" + useKotlinMethodsForVarNames
+ ", insertDebugLines=" + insertDebugLines
+ ", extractFinally=" + extractFinally
@@ -909,9 +741,8 @@ public class JadxArgs implements Closeable {
+ ", deobfuscationWhitelist=" + deobfuscationWhitelist
+ ", escapeUnicode=" + escapeUnicode
+ ", replaceConsts=" + replaceConsts
+ ", restoreSwitchOverString=" + restoreSwitchOverString
+ ", respectBytecodeAccModifiers=" + respectBytecodeAccModifiers
+ ", exportGradleType=" + exportGradleType
+ ", exportAsGradleProject=" + exportAsGradleProject
+ ", skipXmlPrettyPrint=" + skipXmlPrettyPrint
+ ", fsCaseSensitive=" + fsCaseSensitive
+ ", renameFlags=" + renameFlags
@@ -923,8 +754,6 @@ public class JadxArgs implements Closeable {
+ ", pluginOptions=" + pluginOptions
+ ", cfgOutput=" + cfgOutput
+ ", rawCFGOutput=" + rawCFGOutput
+ ", useHeadersForDetectResourceExtensions=" + useHeadersForDetectResourceExtensions
+ ", typeUpdatesLimitCount=" + typeUpdatesLimitCount
+ '}';
}
}
@@ -28,6 +28,12 @@ public class JadxArgsValidator {
if (inputFiles.isEmpty() && jadx.getCustomCodeLoaders().isEmpty()) {
throw new JadxArgsValidateException("Please specify input file");
}
for (File inputFile : inputFiles) {
String fileName = inputFile.getName();
if (fileName.startsWith("--")) {
throw new JadxArgsValidateException("Unknown argument: " + fileName);
}
}
for (File file : inputFiles) {
checkFile(file);
}
@@ -7,7 +7,6 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -43,8 +42,7 @@ import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.PackageNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.visitors.SaveCode;
import jadx.core.export.ExportGradle;
import jadx.core.export.OutDirs;
import jadx.core.export.ExportGradleTask;
import jadx.core.plugins.JadxPluginManager;
import jadx.core.plugins.PluginContext;
import jadx.core.plugins.events.JadxEventsImpl;
@@ -53,8 +51,9 @@ import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
import jadx.core.utils.tasks.TaskExecutor;
import jadx.core.xmlgen.BinaryXMLParser;
import jadx.core.xmlgen.ProtoXMLParser;
import jadx.core.xmlgen.ResourcesSaver;
import jadx.zip.ZipReader;
/**
* Jadx API usage example:
@@ -87,67 +86,55 @@ public final class JadxDecompiler implements Closeable {
private static final Logger LOG = LoggerFactory.getLogger(JadxDecompiler.class);
private final JadxArgs args;
private final JadxPluginManager pluginManager;
private final JadxPluginManager pluginManager = new JadxPluginManager(this);
private final List<ICodeLoader> loadedInputs = new ArrayList<>();
private final ZipReader zipReader;
private RootNode root;
private List<JavaClass> classes;
private List<ResourceFile> resources;
private BinaryXMLParser binaryXmlParser;
private ProtoXMLParser protoXmlParser;
private final IDecompileScheduler decompileScheduler = new DecompilerScheduler();
private final ResourcesLoader resourcesLoader;
private final JadxEventsImpl events = new JadxEventsImpl();
private final List<ICodeLoader> customCodeLoaders = new ArrayList<>();
private final List<CustomResourcesLoader> customResourcesLoaders = new ArrayList<>();
private final Map<JadxPassType, List<JadxPass>> customPasses = new HashMap<>();
private final List<Closeable> closeableList = new ArrayList<>();
private IJadxEvents events = new JadxEventsImpl();
public JadxDecompiler() {
this(new JadxArgs());
}
public JadxDecompiler(JadxArgs args) {
this.args = Objects.requireNonNull(args);
this.pluginManager = new JadxPluginManager(this);
this.resourcesLoader = new ResourcesLoader(this);
this.zipReader = new ZipReader(args.getSecurity());
this.args = args;
}
public void load() {
reset();
JadxArgsValidator.validate(this);
LOG.info("loading ...");
FileUtils.updateTempRootDir(args.getFilesGetter().getTempDir());
loadPlugins();
loadInputFiles();
root = new RootNode(this);
root = new RootNode(args);
root.init();
// load classes and resources
root.loadClasses(loadedInputs);
root.loadResources(resourcesLoader, getResources());
root.finishClassLoad();
root.initClassPath();
// init passes
root.setDecompilerRef(this);
root.mergePasses(customPasses);
root.loadClasses(loadedInputs);
root.initClassPath();
root.loadResources(getResources());
root.runPreDecompileStage();
root.initPasses();
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();
@@ -168,7 +155,7 @@ public final class JadxDecompiler implements Closeable {
loadedInputs.add(loader);
}
} catch (Exception e) {
LOG.warn("Failed to load code for plugin: {}", plugin, e);
throw new JadxRuntimeException("Failed to load code for plugin: " + plugin, e);
}
}
}
@@ -179,37 +166,42 @@ public final class JadxDecompiler implements Closeable {
}
private void reset() {
unloadPlugins();
root = null;
classes = null;
resources = null;
binaryXmlParser = null;
protoXmlParser = null;
events.reset();
}
@Override
public void close() {
reset();
closeAll(loadedInputs);
closeAll(customCodeLoaders);
closeAll(customResourcesLoaders);
closeAll(closeableList);
FileUtils.deleteDirIfExists(args.getFilesGetter().getTempDir());
closeInputs();
closeLoaders();
args.close();
FileUtils.clearTempRootDir();
}
private void closeAll(List<? extends Closeable> list) {
try {
for (Closeable closeable : list) {
try {
closeable.close();
} catch (Exception e) {
LOG.warn("Fail to close '{}'", closeable, e);
}
private void closeInputs() {
loadedInputs.forEach(load -> {
try {
load.close();
} catch (Exception e) {
LOG.error("Failed to close input", e);
}
});
loadedInputs.clear();
}
private void closeLoaders() {
for (CustomResourcesLoader resourcesLoader : customResourcesLoaders) {
try {
resourcesLoader.close();
} catch (Exception e) {
LOG.error("Failed to close resource loader: " + resourcesLoader, e);
}
} finally {
list.clear();
}
customResourcesLoaders.clear();
}
private void loadPlugins() {
@@ -226,10 +218,6 @@ public final class JadxDecompiler implements Closeable {
}
}
private void unloadPlugins() {
pluginManager.unloadResolved();
}
private void loadFinished() {
LOG.debug("Load finished");
List<JadxPass> list = customPasses.get(JadxAfterLoadPass.TYPE);
@@ -307,28 +295,31 @@ public final class JadxDecompiler implements Closeable {
if (root == null) {
throw new JadxRuntimeException("No loaded files");
}
OutDirs outDirs;
ExportGradle gradleExport;
if (args.getExportGradleType() != null) {
gradleExport = new ExportGradle(root, args.getOutDir(), getResources());
outDirs = gradleExport.init();
File sourcesOutDir;
File resOutDir;
ExportGradleTask gradleExportTask;
if (args.isExportAsGradleProject()) {
gradleExportTask = new ExportGradleTask(resources, root, args.getOutDir());
gradleExportTask.init();
sourcesOutDir = gradleExportTask.getSrcOutDir();
resOutDir = gradleExportTask.getResOutDir();
} else {
gradleExport = null;
outDirs = new OutDirs(args.getOutDirSrc(), args.getOutDirRes());
outDirs.makeDirs();
sourcesOutDir = args.getOutDirSrc();
resOutDir = args.getOutDirRes();
gradleExportTask = null;
}
TaskExecutor executor = new TaskExecutor();
executor.setThreadsCount(args.getThreadsCount());
if (saveResources) {
// save resources first because decompilation can stop or fail
appendResourcesSaveTasks(executor, outDirs.getResOutDir());
appendResourcesSaveTasks(executor, resOutDir);
}
if (saveSources) {
appendSourcesSave(executor, outDirs.getSrcOutDir());
appendSourcesSave(executor, sourcesOutDir);
}
if (gradleExport != null) {
executor.addSequentialTask(gradleExport::generateGradleFiles);
if (gradleExportTask != null) {
executor.addSequentialTask(gradleExportTask);
}
return executor;
}
@@ -340,15 +331,13 @@ public final class JadxDecompiler implements Closeable {
// process AndroidManifest.xml first to load complete resource ids table
for (ResourceFile resourceFile : getResources()) {
if (resourceFile.getType() == ResourceType.MANIFEST) {
new ResourcesSaver(this, outDir, resourceFile).run();
new ResourcesSaver(outDir, resourceFile).run();
break;
}
}
Set<String> inputFileNames = args.getInputFiles().stream()
.map(File::getAbsolutePath)
.collect(Collectors.toSet());
Set<String> codeSources = collectCodeSources();
List<Runnable> tasks = new ArrayList<>();
for (ResourceFile resourceFile : getResources()) {
ResourceType resType = resourceFile.getType();
@@ -356,44 +345,16 @@ public final class JadxDecompiler implements Closeable {
// already processed
continue;
}
String resOriginalName = resourceFile.getOriginalName();
if (resType != ResourceType.ARSC && inputFileNames.contains(resOriginalName)) {
// ignore resource made from an input file
if (resType != ResourceType.ARSC
&& inputFileNames.contains(resourceFile.getOriginalName())) {
// ignore resource made from input file
continue;
}
if (codeSources.contains(resOriginalName)) {
// don't output code source resources (.dex, .class, etc)
// do not trust file extensions, use only sources set as class inputs
continue;
}
tasks.add(new ResourcesSaver(this, outDir, resourceFile));
tasks.add(new ResourcesSaver(outDir, resourceFile));
}
executor.addParallelTasks(tasks);
}
private Set<String> collectCodeSources() {
Set<String> set = new HashSet<>();
for (ClassNode cls : root.getClasses(true)) {
if (cls.getClsData() == null) {
// exclude synthetic classes
continue;
}
String inputFileName = cls.getInputFileName();
if (inputFileName.endsWith(".class")) {
// cut .class name to get source .jar file
// current template: "<optional input files>:<.jar>:<full class name>"
// TODO: add property to set file name or reference to resource name
int endIdx = inputFileName.lastIndexOf(':');
if (endIdx != -1) {
int startIdx = inputFileName.lastIndexOf(':', endIdx - 1) + 1;
inputFileName = inputFileName.substring(startIdx, endIdx);
}
}
set.add(inputFileName);
}
return set;
}
private void appendSourcesSave(ITaskExecutor executor, File outDir) {
List<JavaClass> classes = getClasses();
List<JavaClass> processQueue = filterClasses(classes);
@@ -440,7 +401,7 @@ public final class JadxDecompiler implements Closeable {
return list;
}
public synchronized List<JavaClass> getClasses() {
public List<JavaClass> getClasses() {
if (root == null) {
return Collections.emptyList();
}
@@ -448,7 +409,10 @@ 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) && !classNode.isInner()) {
if (classNode.contains(AFlag.DONT_GENERATE)) {
continue;
}
if (!classNode.getClassInfo().isInner()) {
clsList.add(convertClassNode(classNode));
}
}
@@ -466,7 +430,7 @@ public final class JadxDecompiler implements Closeable {
if (root == null) {
return Collections.emptyList();
}
resources = resourcesLoader.load(root);
resources = new ResourcesLoader(this).load();
}
return resources;
}
@@ -505,6 +469,20 @@ public final class JadxDecompiler implements Closeable {
return root;
}
synchronized BinaryXMLParser getBinaryXmlParser() {
if (binaryXmlParser == null) {
binaryXmlParser = new BinaryXMLParser(root);
}
return binaryXmlParser;
}
synchronized ProtoXMLParser getProtoXmlParser() {
if (protoXmlParser == null) {
protoXmlParser = new ProtoXMLParser(root);
}
return protoXmlParser;
}
/**
* Get JavaClass by ClassNode without loading and decompilation
*/
@@ -548,10 +526,9 @@ 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, clsListNoDup, subPkgs);
JavaPackage javaPkg = new JavaPackage(pkg, clsList, subPkgs);
if (subPkgsCount != 0) {
// add subpackages after parent to avoid endless recursion
for (PackageNode subPackage : pkg.getSubPackages()) {
@@ -622,8 +599,6 @@ public final class JadxDecompiler implements Closeable {
return convertMethodNode((MethodNode) ann);
case FIELD:
return convertFieldNode((FieldNode) ann);
case PKG:
return convertPackageNode((PackageNode) ann);
case DECLARATION:
return getJavaNodeByCodeAnnotation(codeInfo, ((NodeDeclareRef) ann).getNode());
case VAR:
@@ -706,10 +681,6 @@ public final class JadxDecompiler implements Closeable {
return events;
}
public void setEventsImpl(IJadxEvents eventsImpl) {
this.events = eventsImpl;
}
public void addCustomCodeLoader(ICodeLoader customCodeLoader) {
customCodeLoaders.add(customCodeLoader);
}
@@ -733,18 +704,6 @@ public final class JadxDecompiler implements Closeable {
customPasses.computeIfAbsent(pass.getPassType(), l -> new ArrayList<>()).add(pass);
}
public ResourcesLoader getResourcesLoader() {
return resourcesLoader;
}
public ZipReader getZipReader() {
return zipReader;
}
public void addCloseable(Closeable closeable) {
closeableList.add(closeable);
}
@Override
public String toString() {
return "jadx decompiler " + getVersion();
@@ -122,6 +122,8 @@ public final class JavaClass implements JavaNode {
if (listsLoaded) {
return null;
}
listsLoaded = true;
ICodeInfo code;
if (cls.getState().isProcessComplete()) {
// already decompiled -> class internals loaded
@@ -129,12 +131,7 @@ public final class JavaClass implements JavaNode {
} else {
code = cls.decompile();
}
loadLists();
return code;
}
private void loadLists() {
listsLoaded = true;
JadxDecompiler rootDecompiler = getRootDecompiler();
int inClsCount = cls.getInnerClasses().size();
if (inClsCount != 0) {
@@ -142,7 +139,7 @@ public final class JavaClass implements JavaNode {
for (ClassNode inner : cls.getInnerClasses()) {
if (!inner.contains(AFlag.DONT_GENERATE)) {
JavaClass javaClass = rootDecompiler.convertClassNode(inner);
javaClass.loadLists();
javaClass.load();
list.add(javaClass);
}
}
@@ -153,7 +150,7 @@ public final class JavaClass implements JavaNode {
List<JavaClass> list = new ArrayList<>(inlinedClsCount);
for (ClassNode inner : cls.getInlinedClasses()) {
JavaClass javaClass = rootDecompiler.convertClassNode(inner);
javaClass.loadLists();
javaClass.load();
list.add(javaClass);
}
this.inlinedClasses = Collections.unmodifiableList(list);
@@ -181,6 +178,7 @@ public final class JavaClass implements JavaNode {
mths.sort(Comparator.comparing(JavaMethod::getName));
this.methods = Collections.unmodifiableList(mths);
}
return code;
}
JadxDecompiler getRootDecompiler() {
@@ -254,10 +252,6 @@ public final class JavaClass implements JavaNode {
return cls.getPackage();
}
public JavaPackage getJavaPackage() {
return cls.getPackageNode().getJavaNode();
}
@Override
public JavaClass getDeclaringClass() {
return parent;
@@ -2,6 +2,7 @@ package jadx.api;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.jetbrains.annotations.ApiStatus;
@@ -78,9 +79,15 @@ public final class JavaMethod implements JavaNode {
return Collections.emptyList();
}
JadxDecompiler decompiler = getDeclaringClass().getRootDecompiler();
return ovrdAttr.getRelatedMthNodes()
.stream()
.map(decompiler::convertMethodNode)
return ovrdAttr.getRelatedMthNodes().stream()
.map(m -> {
JavaMethod javaMth = decompiler.convertMethodNode(m);
if (javaMth == null) {
LOG.warn("Failed convert to java method: {}", m);
}
return javaMth;
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
@@ -97,10 +104,6 @@ public final class JavaMethod implements JavaNode {
return mth.getDefPosition();
}
public String getCodeStr() {
return mth.getCodeStr();
}
@Override
public void removeAlias() {
this.mth.getMethodInfo().removeAlias();
@@ -15,17 +15,11 @@ 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;
}
@@ -55,10 +49,6 @@ public final class JavaPackage implements JavaNode, Comparable<JavaPackage> {
return classes;
}
public List<JavaClass> getClassesNoDup() {
return clsListNoDup;
}
public boolean isRoot() {
return pkgNode.isRoot();
}
@@ -86,22 +76,6 @@ public final class JavaPackage implements JavaNode, Comparable<JavaPackage> {
return !Objects.equals(parent, aliasParent);
}
public boolean isDescendantOf(JavaPackage ancestor) {
JavaPackage current = this;
while (current != null) {
if (ancestor.equals(current)) {
return true;
}
if (current.getPkgNode().getParentPkg() == null) {
current = null;
} else {
current = current.getPkgNode().getParentPkg().getJavaNode();
}
}
return false;
}
@Override
public ICodeNodeRef getCodeNodeRef() {
return pkgNode;
@@ -2,21 +2,39 @@ package jadx.api;
import java.io.File;
import org.jetbrains.annotations.Nullable;
import jadx.core.deobf.FileTypeDetector;
import jadx.core.utils.StringUtils;
import jadx.core.utils.exceptions.JadxException;
import jadx.api.plugins.utils.ZipSecurity;
import jadx.core.xmlgen.ResContainer;
import jadx.core.xmlgen.entry.ResourceEntry;
import jadx.zip.IZipEntry;
public class ResourceFile {
public static final class ZipRef {
private final File zipFile;
private final String entryName;
public ZipRef(File zipFile, String entryName) {
this.zipFile = zipFile;
this.entryName = entryName;
}
public File getZipFile() {
return zipFile;
}
public String getEntryName() {
return entryName;
}
@Override
public String toString() {
return "ZipRef{" + zipFile + ", '" + entryName + "'}";
}
}
private final JadxDecompiler decompiler;
private final String name;
private ResourceType type;
private @Nullable IZipEntry zipEntry;
private final ResourceType type;
private ZipRef zipRef;
private String deobfName;
public static ResourceFile createResourceFile(JadxDecompiler decompiler, File file, ResourceType type) {
@@ -24,7 +42,7 @@ public class ResourceFile {
}
public static ResourceFile createResourceFile(JadxDecompiler decompiler, String name, ResourceType type) {
if (!decompiler.getArgs().getSecurity().isValidEntryName(name)) {
if (!ZipSecurity.isValidZipEntryName(name)) {
return null;
}
return new ResourceFile(decompiler, name, type);
@@ -44,10 +62,6 @@ public class ResourceFile {
return deobfName != null ? deobfName : name;
}
public void setDeobfName(String resFullName) {
this.deobfName = resFullName;
}
public ResourceType getType() {
return type;
}
@@ -56,73 +70,28 @@ public class ResourceFile {
return ResourcesLoader.loadContent(decompiler, this);
}
public boolean setAlias(ResourceEntry entry, boolean useHeaders) {
void setZipRef(ZipRef zipRef) {
this.zipRef = zipRef;
}
public boolean setAlias(ResourceEntry ri) {
StringBuilder sb = new StringBuilder();
sb.append("res/").append(entry.getTypeName()).append(entry.getConfig());
sb.append("/").append(entry.getKeyName());
if (useHeaders) {
try {
int maxBytesToReadLimit = 4096;
byte[] bytes = ResourcesLoader.decodeStream(this, (size, is) -> {
int bytesToRead;
if (size > 0) {
bytesToRead = (int) Math.min(size, maxBytesToReadLimit);
} else if (size == 0) {
bytesToRead = 0;
} else {
bytesToRead = maxBytesToReadLimit;
}
if (bytesToRead == 0) {
return new byte[0];
}
return is.readNBytes(bytesToRead);
});
String fileExtension = FileTypeDetector.detectFileExtension(bytes);
if (!StringUtils.isEmpty(fileExtension)) {
sb.append(fileExtension);
} else {
sb.append(getExtFromName(name));
}
} catch (JadxException ignored) {
}
} else {
sb.append(getExtFromName(name));
sb.append("res/").append(ri.getTypeName()).append(ri.getConfig());
sb.append("/").append(ri.getKeyName());
int lastDot = name.lastIndexOf('.');
if (lastDot != -1) {
sb.append(name.substring(lastDot));
}
String alias = sb.toString();
if (!alias.equals(name)) {
setDeobfName(alias);
type = ResourceType.getFileType(alias);
deobfName = alias;
return true;
}
return false;
}
private String getExtFromName(String name) {
// the image .9.png extension always saved, when resource shrinking by aapt2
if (name.contains(".9.png")) {
return ".9.png";
}
int lastDot = name.lastIndexOf('.');
if (lastDot != -1) {
return name.substring(lastDot);
}
return "";
}
public @Nullable IZipEntry getZipEntry() {
return zipEntry;
}
void setZipEntry(@Nullable IZipEntry zipEntry) {
this.zipEntry = zipEntry;
}
public JadxDecompiler getDecompiler() {
return decompiler;
public ZipRef getZipRef() {
return zipRef;
}
@Override
@@ -1,17 +0,0 @@
package jadx.api;
import jadx.core.xmlgen.ResContainer;
public class ResourceFileContainer extends ResourceFile {
private final ResContainer container;
public ResourceFileContainer(String name, ResourceType type, ResContainer container) {
super(null, name, type);
this.container = container;
}
@Override
public ResContainer loadContent() {
return container;
}
}
@@ -4,44 +4,25 @@ import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import jadx.api.resources.ResourceContentType;
import jadx.core.utils.exceptions.JadxRuntimeException;
import static jadx.api.resources.ResourceContentType.CONTENT_BINARY;
import static jadx.api.resources.ResourceContentType.CONTENT_TEXT;
import static jadx.api.resources.ResourceContentType.CONTENT_UNKNOWN;
public enum ResourceType {
CODE(CONTENT_BINARY, ".dex", ".jar", ".class"),
XML(CONTENT_TEXT, ".xml"),
ARSC(CONTENT_TEXT, ".arsc"),
APK(CONTENT_BINARY, ".apk", ".apkm", ".apks"),
FONT(CONTENT_BINARY, ".ttf", ".ttc", ".otf"),
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", ".java", ".kt", ".md"),
HTML(CONTENT_TEXT, ".html", ".htm"),
LIB(CONTENT_BINARY, ".so"),
MANIFEST(CONTENT_TEXT),
UNKNOWN_BIN(CONTENT_BINARY, ".bin"),
UNKNOWN(CONTENT_UNKNOWN);
CODE(".dex", ".jar", ".class"),
XML(".xml"),
ARSC(".arsc"),
FONT(".ttf", ".otf"),
IMG(".png", ".gif", ".jpg"),
MEDIA(".mp3", ".wav"),
LIB(".so"),
MANIFEST,
UNKNOWN;
private final ResourceContentType contentType;
private final String[] exts;
ResourceType(ResourceContentType contentType, String... exts) {
this.contentType = contentType;
ResourceType(String... exts) {
this.exts = exts;
}
public ResourceContentType getContentType() {
return contentType;
}
public String[] getExts() {
return exts;
}
@@ -6,54 +6,42 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.ResourceFile.ZipRef;
import jadx.api.impl.SimpleCodeInfo;
import jadx.api.plugins.CustomResourcesLoader;
import jadx.api.plugins.resources.IResContainerFactory;
import jadx.api.plugins.resources.IResTableParserProvider;
import jadx.api.plugins.resources.IResourcesLoader;
import jadx.api.plugins.utils.ZipSecurity;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.Utils;
import jadx.core.utils.android.Res9patchStreamDecoder;
import jadx.core.utils.exceptions.JadxException;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
import jadx.core.xmlgen.BinaryXMLParser;
import jadx.core.xmlgen.IResTableParser;
import jadx.core.xmlgen.ResContainer;
import jadx.core.xmlgen.ResTableBinaryParserProvider;
import jadx.zip.IZipEntry;
import jadx.zip.ZipContent;
import jadx.core.xmlgen.ResProtoParser;
import jadx.core.xmlgen.ResTableParser;
import static jadx.core.utils.files.FileUtils.READ_BUFFER_SIZE;
import static jadx.core.utils.files.FileUtils.copyStream;
// TODO: move to core package
public final class ResourcesLoader implements IResourcesLoader {
public final class ResourcesLoader {
private static final Logger LOG = LoggerFactory.getLogger(ResourcesLoader.class);
private final JadxDecompiler decompiler;
private final JadxDecompiler jadxRef;
private final List<IResTableParserProvider> resTableParserProviders = new ArrayList<>();
private final List<IResContainerFactory> resContainerFactories = new ArrayList<>();
private BinaryXMLParser binaryXmlParser;
ResourcesLoader(JadxDecompiler decompiler) {
this.decompiler = decompiler;
this.resTableParserProviders.add(new ResTableBinaryParserProvider());
ResourcesLoader(JadxDecompiler jadxRef) {
this.jadxRef = jadxRef;
}
List<ResourceFile> load(RootNode root) {
init(root);
List<File> inputFiles = decompiler.getArgs().getInputFiles();
List<ResourceFile> load() {
List<File> inputFiles = jadxRef.getArgs().getInputFiles();
List<ResourceFile> list = new ArrayList<>(inputFiles.size());
for (File file : inputFiles) {
loadFile(list, file);
@@ -61,59 +49,40 @@ public final class ResourcesLoader implements IResourcesLoader {
return list;
}
private void init(RootNode root) {
for (IResTableParserProvider resTableParserProvider : resTableParserProviders) {
try {
resTableParserProvider.init(root);
} catch (Exception e) {
throw new JadxRuntimeException("Failed to init res table provider: " + resTableParserProvider);
}
}
for (IResContainerFactory resContainerFactory : resContainerFactories) {
try {
resContainerFactory.init(root);
} catch (Exception e) {
throw new JadxRuntimeException("Failed to init res container factory: " + resContainerFactory);
}
}
}
public interface ResourceDecoder<T> {
T decode(long size, InputStream is) throws IOException;
}
@Override
public void addResContainerFactory(IResContainerFactory resContainerFactory) {
resContainerFactories.add(resContainerFactory);
}
@Override
public void addResTableParserProvider(IResTableParserProvider resTableParserProvider) {
resTableParserProviders.add(resTableParserProvider);
}
public static <T> T decodeStream(ResourceFile rf, ResourceDecoder<T> decoder) throws JadxException {
try {
IZipEntry zipEntry = rf.getZipEntry();
if (zipEntry != null) {
try (InputStream inputStream = zipEntry.getInputStream()) {
return decoder.decode(zipEntry.getUncompressedSize(), inputStream);
}
} else {
ZipRef zipRef = rf.getZipRef();
if (zipRef == null) {
File file = new File(rf.getOriginalName());
try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) {
return decoder.decode(file.length(), inputStream);
}
} else {
try (ZipFile zipFile = new ZipFile(zipRef.getZipFile())) {
ZipEntry entry = zipFile.getEntry(zipRef.getEntryName());
if (entry == null) {
throw new IOException("Zip entry not found: " + zipRef);
}
if (!ZipSecurity.isValidZipEntry(entry)) {
return null;
}
try (InputStream inputStream = ZipSecurity.getInputStreamForEntry(zipFile, entry)) {
return decoder.decode(entry.getSize(), inputStream);
}
}
}
} catch (Exception e) {
throw new JadxException("Error decode: " + rf.getOriginalName(), e);
throw new JadxException("Error decode: " + rf.getDeobfName(), e);
}
}
static ResContainer loadContent(JadxDecompiler jadxRef, ResourceFile rf) {
try {
ResourcesLoader resLoader = jadxRef.getResourcesLoader();
return decodeStream(rf, (size, is) -> resLoader.loadContent(rf, is));
return decodeStream(rf, (size, is) -> loadContent(jadxRef, rf, is));
} catch (JadxException e) {
LOG.error("Decode error", e);
ICodeWriter cw = jadxRef.getRoot().makeCodeWriter();
@@ -123,51 +92,38 @@ public final class ResourcesLoader implements IResourcesLoader {
}
}
private ResContainer loadContent(ResourceFile resFile, InputStream inputStream) throws IOException {
for (IResContainerFactory customFactory : resContainerFactories) {
ResContainer resContainer = customFactory.create(resFile, inputStream);
if (resContainer != null) {
return resContainer;
}
}
switch (resFile.getType()) {
private static ResContainer loadContent(JadxDecompiler jadxRef, ResourceFile rf,
InputStream inputStream) throws IOException {
RootNode root = jadxRef.getRoot();
switch (rf.getType()) {
case MANIFEST:
case XML:
ICodeInfo content = loadBinaryXmlParser().parse(inputStream);
return ResContainer.textResource(resFile.getDeobfName(), content);
case XML: {
ICodeInfo content;
if (root.isProto()) {
content = jadxRef.getProtoXmlParser().parse(inputStream);
} else {
content = jadxRef.getBinaryXmlParser().parse(inputStream);
}
return ResContainer.textResource(rf.getDeobfName(), content);
}
case ARSC:
return decodeTable(resFile, inputStream).decodeFiles();
if (root.isProto()) {
return new ResProtoParser(root).decodeFiles(inputStream);
} else {
return new ResTableParser(root).decodeFiles(inputStream);
}
case IMG:
return decodeImage(resFile, inputStream);
return decodeImage(rf, inputStream);
default:
return ResContainer.resourceFileLink(resFile);
return ResContainer.resourceFileLink(rf);
}
}
public IResTableParser decodeTable(ResourceFile resFile, InputStream is) throws IOException {
if (resFile.getType() != ResourceType.ARSC) {
throw new IllegalArgumentException("Unexpected resource type for decode: " + resFile.getType() + ", expect '.pb'/'.arsc'");
}
IResTableParser parser = null;
for (IResTableParserProvider provider : resTableParserProviders) {
parser = provider.getParser(resFile);
if (parser != null) {
break;
}
}
if (parser == null) {
throw new JadxRuntimeException("Unknown type of resource file: " + resFile.getOriginalName());
}
parser.setBaseFileName(resFile.getDeobfName());
parser.decode(is);
return parser;
}
private static ResContainer decodeImage(ResourceFile rf, InputStream inputStream) {
String name = rf.getDeobfName();
String name = rf.getOriginalName();
if (name.endsWith(".9.png")) {
try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
Res9patchStreamDecoder decoder = new Res9patchStreamDecoder();
@@ -187,7 +143,7 @@ public final class ResourcesLoader implements IResourcesLoader {
}
// Try to load the resources with a custom loader first
for (CustomResourcesLoader loader : decompiler.getCustomResourcesLoaders()) {
for (CustomResourcesLoader loader : jadxRef.getCustomResourcesLoaders()) {
if (loader.load(this, list, file)) {
LOG.debug("Custom loader used for {}", file.getAbsolutePath());
return;
@@ -200,49 +156,32 @@ public final class ResourcesLoader implements IResourcesLoader {
public void defaultLoadFile(List<ResourceFile> list, File file, String subDir) {
if (FileUtils.isZipFile(file)) {
try {
ZipContent zipContent = decompiler.getZipReader().open(file);
// do not close a zip now, entry content will be read later
decompiler.addCloseable(zipContent);
for (IZipEntry entry : zipContent.getEntries()) {
addEntry(list, file, entry, subDir);
}
} catch (Exception e) {
throw new RuntimeException("Failed to open zip file: " + file.getAbsolutePath(), e);
}
ZipSecurity.visitZipEntries(file, (zipFile, entry) -> {
addEntry(list, file, entry, subDir);
return null;
});
} else {
ResourceType type = ResourceType.getFileType(file.getAbsolutePath());
list.add(ResourceFile.createResourceFile(decompiler, file, type));
list.add(ResourceFile.createResourceFile(jadxRef, file, type));
}
}
public void addEntry(List<ResourceFile> list, File zipFile, IZipEntry entry, String subDir) {
public void addEntry(List<ResourceFile> list, File zipFile, ZipEntry entry, String subDir) {
if (entry.isDirectory()) {
return;
}
String name = entry.getName();
ResourceType type = ResourceType.getFileType(name);
ResourceFile rf = ResourceFile.createResourceFile(decompiler, subDir + name, type);
ResourceFile rf = ResourceFile.createResourceFile(jadxRef, subDir + name, type);
if (rf != null) {
rf.setZipEntry(entry);
rf.setZipRef(new ZipRef(zipFile, name));
list.add(rf);
}
}
public static ICodeInfo loadToCodeWriter(InputStream is) throws IOException {
return loadToCodeWriter(is, StandardCharsets.UTF_8);
}
public static ICodeInfo loadToCodeWriter(InputStream is, Charset charset) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream(READ_BUFFER_SIZE);
copyStream(is, baos);
return new SimpleCodeInfo(baos.toString(charset));
}
private synchronized BinaryXMLParser loadBinaryXmlParser() {
if (binaryXmlParser == null) {
binaryXmlParser = new BinaryXMLParser(decompiler.getRoot());
}
return binaryXmlParser;
return new SimpleCodeInfo(baos.toString("UTF-8"));
}
}
@@ -1,38 +0,0 @@
package jadx.api.args;
import jadx.core.utils.exceptions.JadxRuntimeException;
public enum UseSourceNameAsClassNameAlias {
ALWAYS,
IF_BETTER,
NEVER;
public static UseSourceNameAsClassNameAlias getDefault() {
return NEVER;
}
/**
* @deprecated Use {@link UseSourceNameAsClassNameAlias} directly.
*/
@Deprecated
public boolean toBoolean() {
switch (this) {
case IF_BETTER:
return true;
case NEVER:
return false;
case ALWAYS:
throw new JadxRuntimeException("No match between " + this + " and boolean");
default:
throw new JadxRuntimeException("Unhandled strategy: " + this);
}
}
/**
* @deprecated Use {@link UseSourceNameAsClassNameAlias} directly.
*/
@Deprecated
public static UseSourceNameAsClassNameAlias create(boolean useSourceNameAsAlias) {
return useSourceNameAsAlias ? IF_BETTER : NEVER;
}
}
@@ -1,69 +0,0 @@
package jadx.api.data;
public enum CommentStyle {
/**
* <pre>
* // comment
* </pre>
*/
LINE("// ", "// ", ""),
// @formatter:off
/**
* <pre>
* /*
* * comment
* *&#47;
* </pre>
*/
// @formatter:on
BLOCK("/*\n * ", " * ", "\n */"),
/**
* <pre>
* /* comment *&#47;
* </pre>
*/
BLOCK_CONDENSED("/* ", " * ", " */"),
// @formatter:off
/**
* <pre>
* /**
* * comment
* *&#47;
* </pre>
*/
// @formatter:on
JAVADOC("/**\n * ", " * ", "\n */"),
/**
* <pre>
* /** comment *&#47;
* </pre>
*/
JAVADOC_CONDENSED("/** ", " * ", " */");
private final String start;
private final String onNewLine;
private final String end;
CommentStyle(String start, String onNewLine, String end) {
this.start = start;
this.onNewLine = onNewLine;
this.end = end;
}
public String getStart() {
return start;
}
public String getOnNewLine() {
return onNewLine;
}
public String getEnd() {
return end;
}
}
@@ -10,6 +10,4 @@ public interface ICodeComment extends Comparable<ICodeComment> {
IJavaCodeRef getCodeRef();
String getComment();
CommentStyle getStyle();
}
@@ -3,7 +3,6 @@ package jadx.api.data.impl;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import jadx.api.data.CommentStyle;
import jadx.api.data.ICodeComment;
import jadx.api.data.IJavaCodeRef;
import jadx.api.data.IJavaNodeRef;
@@ -14,25 +13,15 @@ public class JadxCodeComment implements ICodeComment {
@Nullable
private IJavaCodeRef codeRef;
private String comment;
private CommentStyle style = CommentStyle.LINE;
public JadxCodeComment(IJavaNodeRef nodeRef, String comment) {
this(nodeRef, null, comment);
}
public JadxCodeComment(IJavaNodeRef nodeRef, String comment, CommentStyle style) {
this(nodeRef, null, comment, style);
}
public JadxCodeComment(IJavaNodeRef nodeRef, @Nullable IJavaCodeRef codeRef, String comment) {
this(nodeRef, codeRef, comment, CommentStyle.LINE);
}
public JadxCodeComment(IJavaNodeRef nodeRef, @Nullable IJavaCodeRef codeRef, String comment, CommentStyle style) {
this.nodeRef = nodeRef;
this.codeRef = codeRef;
this.comment = comment;
this.style = style;
}
public JadxCodeComment() {
@@ -67,15 +56,6 @@ public class JadxCodeComment implements ICodeComment {
this.comment = comment;
}
@Override
public CommentStyle getStyle() {
return style;
}
public void setStyle(CommentStyle style) {
this.style = style;
}
@Override
public int compareTo(@NotNull ICodeComment other) {
int cmpNodeRef = this.getNodeRef().compareTo(other.getNodeRef());
@@ -93,7 +73,6 @@ public class JadxCodeComment implements ICodeComment {
return "JadxCodeComment{" + nodeRef
+ ", ref=" + codeRef
+ ", comment='" + comment + '\''
+ ", style=" + style
+ '}';
}
}
@@ -1,32 +0,0 @@
package jadx.api.gui.tree;
import javax.swing.Icon;
import javax.swing.tree.TreeNode;
import org.jetbrains.annotations.Nullable;
import jadx.api.metadata.ICodeNodeRef;
public interface ITreeNode extends TreeNode {
/**
* Locale independent node identifier
*/
String getID();
/**
* Node title
*/
String getName();
/**
* Node icon
*/
Icon getIcon();
/**
* Related code node reference.
*/
@Nullable
ICodeNodeRef getCodeNodeRef();
}
@@ -11,6 +11,7 @@ import jadx.api.JadxArgs;
import jadx.api.metadata.ICodeAnnotation;
import jadx.api.metadata.ICodeNodeRef;
import jadx.api.metadata.annotations.NodeDeclareRef;
import jadx.api.metadata.annotations.VarRef;
import jadx.core.utils.StringUtils;
public class AnnotatedCodeWriter extends SimpleCodeWriter implements ICodeWriter {
@@ -20,6 +21,9 @@ public class AnnotatedCodeWriter extends SimpleCodeWriter implements ICodeWriter
private Map<Integer, ICodeAnnotation> annotations = Collections.emptyMap();
private Map<Integer, Integer> lineMap = Collections.emptyMap();
public AnnotatedCodeWriter() {
}
public AnnotatedCodeWriter(JadxArgs args) {
super(args);
}
@@ -31,9 +35,9 @@ public class AnnotatedCodeWriter extends SimpleCodeWriter implements ICodeWriter
@Override
public AnnotatedCodeWriter addMultiLine(String str) {
if (str.contains(newLineStr)) {
buf.append(str.replace(newLineStr, newLineStr + indentStr));
line += StringUtils.countMatches(str, newLineStr);
if (str.contains(NL)) {
buf.append(str.replace(NL, NL + indentStr));
line += StringUtils.countMatches(str, NL);
offset = 0;
} else {
buf.append(str);
@@ -61,7 +65,7 @@ public class AnnotatedCodeWriter extends SimpleCodeWriter implements ICodeWriter
buf.append(cw.getCodeStr());
return this;
}
AnnotatedCodeWriter code = (AnnotatedCodeWriter) cw;
AnnotatedCodeWriter code = ((AnnotatedCodeWriter) cw);
line--;
int startPos = getLength();
for (Map.Entry<Integer, ICodeAnnotation> entry : code.annotations.entrySet()) {
@@ -80,7 +84,7 @@ public class AnnotatedCodeWriter extends SimpleCodeWriter implements ICodeWriter
@Override
protected void addLine() {
buf.append(newLineStr);
buf.append(NL);
line++;
offset = 0;
}
@@ -150,6 +154,8 @@ public class AnnotatedCodeWriter extends SimpleCodeWriter implements ICodeWriter
@Override
public ICodeInfo finish() {
processDefinitionAnnotations();
validateAnnotations();
String code = buf.toString();
buf = null;
return new AnnotatedCodeInfo(code, lineMap, annotations);
@@ -159,4 +165,29 @@ public class AnnotatedCodeWriter extends SimpleCodeWriter implements ICodeWriter
public Map<Integer, ICodeAnnotation> getRawAnnotations() {
return annotations;
}
private void processDefinitionAnnotations() {
if (!annotations.isEmpty()) {
annotations.forEach((k, v) -> {
if (v instanceof NodeDeclareRef) {
NodeDeclareRef declareRef = (NodeDeclareRef) v;
declareRef.setDefPos(k);
declareRef.getNode().setDefPosition(k);
}
});
}
}
private void validateAnnotations() {
if (annotations.isEmpty()) {
return;
}
annotations.values().removeIf(v -> {
if (v.getAnnType() == ICodeAnnotation.AnnType.VAR_REF) {
VarRef varRef = (VarRef) v;
return varRef.getRefPos() == 0;
}
return false;
});
}
}
@@ -14,39 +14,38 @@ import jadx.api.metadata.ICodeNodeRef;
import jadx.core.utils.Utils;
/**
* CodeWriter implementation without meta information support
* CodeWriter implementation without meta information support (only strings builder)
*/
public class SimpleCodeWriter implements ICodeWriter {
private static final Logger LOG = LoggerFactory.getLogger(SimpleCodeWriter.class);
private static final String[] INDENT_CACHE = {
"",
INDENT_STR,
INDENT_STR + INDENT_STR,
INDENT_STR + INDENT_STR + INDENT_STR,
INDENT_STR + INDENT_STR + INDENT_STR + INDENT_STR,
INDENT_STR + INDENT_STR + INDENT_STR + INDENT_STR + INDENT_STR,
};
protected StringBuilder buf = new StringBuilder();
protected String indentStr = "";
protected int indent = 0;
protected final boolean insertLineNumbers;
protected final String singleIndentStr;
protected final String newLineStr;
private final boolean insertLineNumbers;
public SimpleCodeWriter() {
this.insertLineNumbers = false;
}
public SimpleCodeWriter(JadxArgs args) {
this.insertLineNumbers = args.isInsertDebugLines();
this.singleIndentStr = args.getCodeIndentStr();
this.newLineStr = args.getCodeNewLineStr();
if (insertLineNumbers) {
incIndent(3);
add(indentStr);
}
}
/**
* Constructor with JadxArgs should be used.
*/
@Deprecated
public SimpleCodeWriter() {
this.insertLineNumbers = false;
this.singleIndentStr = JadxArgs.DEFAULT_INDENT_STR;
this.newLineStr = JadxArgs.DEFAULT_NEW_LINE_STR;
}
@Override
public boolean isMetadataSupported() {
return false;
@@ -97,8 +96,8 @@ public class SimpleCodeWriter implements ICodeWriter {
@Override
public SimpleCodeWriter addMultiLine(String str) {
if (str.contains(newLineStr)) {
buf.append(str.replace(newLineStr, newLineStr + indentStr));
if (str.contains(NL)) {
buf.append(str.replace(NL, NL + indentStr));
} else {
buf.append(str);
}
@@ -131,12 +130,12 @@ public class SimpleCodeWriter implements ICodeWriter {
@Override
public SimpleCodeWriter addIndent() {
add(singleIndentStr);
add(INDENT_STR);
return this;
}
protected void addLine() {
buf.append(newLineStr);
buf.append(NL);
}
protected SimpleCodeWriter addLineIndent() {
@@ -145,7 +144,12 @@ public class SimpleCodeWriter implements ICodeWriter {
}
private void updateIndent() {
this.indentStr = Utils.strRepeat(singleIndentStr, indent);
int curIndent = indent;
if (curIndent < INDENT_CACHE.length) {
this.indentStr = INDENT_CACHE[curIndent];
} else {
this.indentStr = Utils.strRepeat(INDENT_STR, curIndent);
}
}
@Override
@@ -215,17 +219,17 @@ public class SimpleCodeWriter implements ICodeWriter {
@Override
public ICodeInfo finish() {
String code = getStringWithoutFirstEmptyLine();
removeFirstEmptyLine();
String code = buf.toString();
buf = null;
return new SimpleCodeInfo(code);
}
private String getStringWithoutFirstEmptyLine() {
int len = newLineStr.length();
if (buf.length() > len && buf.substring(0, len).equals(newLineStr)) {
return buf.substring(len);
protected void removeFirstEmptyLine() {
int len = NL.length();
if (buf.length() > len && buf.substring(0, len).equals(NL)) {
buf.delete(0, len);
}
return buf.toString();
}
@Override
@@ -29,7 +29,7 @@ public class DecompilePassWrapper extends AbstractVisitor implements IPassWrappe
public void init(RootNode root) throws JadxException {
try {
decompilePass.init(root);
} catch (StackOverflowError | Exception e) {
} catch (Throwable 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 (StackOverflowError | Exception e) {
cls.addError("Error in decompile pass: " + this, e);
} catch (Throwable e) {
LOG.error("Error in decompile pass: {}, class: {}", this, cls, 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 (StackOverflowError | Exception e) {
mth.addError("Error in decompile pass: " + this, e);
} catch (Throwable e) {
LOG.error("Error in decompile pass: {}, method: {}", this, mth, e);
}
}
@@ -23,12 +23,4 @@ public interface JadxPlugin {
* For long operation, prefer {@link JadxPreparePass} or {@link JadxAfterLoadPass} instead.
*/
void init(JadxPluginContext context);
/**
* Plugin unload handler.
* Can be used to clean up resources on plugin unloading.
*/
default void unload() {
// optional method
}
}
@@ -6,15 +6,12 @@ import org.jetbrains.annotations.Nullable;
import jadx.api.JadxArgs;
import jadx.api.JadxDecompiler;
import jadx.api.plugins.data.IJadxFiles;
import jadx.api.plugins.data.IJadxPlugins;
import jadx.api.plugins.events.IJadxEvents;
import jadx.api.plugins.gui.JadxGuiContext;
import jadx.api.plugins.input.JadxCodeInput;
import jadx.api.plugins.options.JadxPluginOptions;
import jadx.api.plugins.pass.JadxPass;
import jadx.api.plugins.resources.IResourcesLoader;
import jadx.zip.ZipReader;
public interface JadxPluginContext {
@@ -35,11 +32,6 @@ public interface JadxPluginContext {
*/
void registerInputsHashSupplier(Supplier<String> supplier);
/**
* Customize resource loading
*/
IResourcesLoader getResourcesLoader();
/**
* Access to jadx-gui specific methods
*/
@@ -55,14 +47,4 @@ public interface JadxPluginContext {
* Access to registered plugins and runtime data
*/
IJadxPlugins plugins();
/**
* Access to plugin specific files and directories
*/
IJadxFiles files();
/**
* Custom jadx zip reader to fight tampering and provide additional security checks
*/
ZipReader getZipReader();
}
@@ -1,29 +1,15 @@
package jadx.api.plugins;
import org.jetbrains.annotations.Nullable;
public class JadxPluginInfo {
private final String pluginId;
private final String name;
private final String description;
private String homepage;
private final String homepage;
/**
* Conflicting plugins should have the same 'provides' property; only one will be loaded
*/
private String provides;
/**
* Minimum required jadx version to run this plugin.
* <br>
* Format: "<stable version>, r<revision number of unstable version>".
* Example: "1.5.1, r2305"
*
* @see <a href="https://github.com/skylot/jadx/wiki/Jadx-plugins-guide#required-jadx-version">wiki
* page</a>
* for details.
*/
private @Nullable String requiredJadxVersion;
private final String provides;
public JadxPluginInfo(String id, String name, String description) {
this(id, name, description, "", id);
@@ -57,26 +43,10 @@ public class JadxPluginInfo {
return homepage;
}
public void setHomepage(String homepage) {
this.homepage = homepage;
}
public String getProvides() {
return provides;
}
public void setProvides(String provides) {
this.provides = provides;
}
public @Nullable String getRequiredJadxVersion() {
return requiredJadxVersion;
}
public void setRequiredJadxVersion(@Nullable String requiredJadxVersion) {
this.requiredJadxVersion = requiredJadxVersion;
}
@Override
public String toString() {
return pluginId + ": " + name + " - '" + description + '\'';
@@ -4,14 +4,11 @@ import java.util.Objects;
import org.jetbrains.annotations.Nullable;
import jadx.core.plugins.versions.VerifyRequiredVersion;
public class JadxPluginInfoBuilder {
private String pluginId;
private String name;
private String description;
private String homepage = "";
private @Nullable String requiredJadxVersion;
private @Nullable String provides;
/**
@@ -46,11 +43,6 @@ public class JadxPluginInfoBuilder {
return this;
}
public JadxPluginInfoBuilder requiredJadxVersion(String versions) {
this.requiredJadxVersion = versions;
return this;
}
public JadxPluginInfo build() {
Objects.requireNonNull(pluginId, "PluginId is required");
Objects.requireNonNull(name, "Name is required");
@@ -58,11 +50,6 @@ public class JadxPluginInfoBuilder {
if (provides == null) {
provides = pluginId;
}
if (requiredJadxVersion != null) {
VerifyRequiredVersion.verify(requiredJadxVersion);
}
JadxPluginInfo pluginInfo = new JadxPluginInfo(pluginId, name, description, homepage, provides);
pluginInfo.setRequiredJadxVersion(requiredJadxVersion);
return pluginInfo;
return new JadxPluginInfo(pluginId, name, description, homepage, provides);
}
}
@@ -1,21 +0,0 @@
package jadx.api.plugins.data;
import java.nio.file.Path;
public interface IJadxFiles {
/**
* Plugin cache directory.
*/
Path getPluginCacheDir();
/**
* Plugin config directory.
*/
Path getPluginConfigDir();
/**
* Plugin temp directory.
*/
Path getPluginTempDir();
}
@@ -15,15 +15,4 @@ public interface IJadxEvents {
* For public event types check {@link JadxEvents} class.
*/
<E extends IJadxEvent> void addListener(JadxEventType<E> eventType, Consumer<E> listener);
/**
* Remove listener for specific event.
* Listener should be same or equal object.
*/
<E extends IJadxEvent> void removeListener(JadxEventType<E> eventType, Consumer<E> listener);
/**
* Clear all listeners.
*/
void reset();
}

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