Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5640f3b931 | |||
| 02bc27e887 | |||
| 794e5adb7f | |||
| a81cec7701 |
@@ -16,7 +16,7 @@ jobs:
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
java-version: 11
|
||||
|
||||
- name: Set jadx version
|
||||
run: |
|
||||
@@ -24,13 +24,10 @@ jobs:
|
||||
JADX_VERSION="r${JADX_REV}.${GITHUB_SHA:0:7}"
|
||||
echo "JADX_VERSION=$JADX_VERSION" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
|
||||
- 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@v4
|
||||
@@ -42,12 +39,11 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
- name: Save Windows bundle artifact
|
||||
- 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
|
||||
|
||||
@@ -61,7 +57,7 @@ jobs:
|
||||
- name: Set up JDK
|
||||
uses: oracle-actions/setup-java@v1
|
||||
with:
|
||||
release: 24
|
||||
release: 21
|
||||
|
||||
- name: Print Java version
|
||||
shell: bash
|
||||
@@ -74,17 +70,15 @@ jobs:
|
||||
JADX_VERSION="r${JADX_REV}.${GITHUB_SHA:0:7}"
|
||||
echo "JADX_VERSION=$JADX_VERSION" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
- 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
|
||||
- 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
|
||||
|
||||
@@ -20,13 +20,9 @@ jobs:
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
|
||||
- name: Build
|
||||
run: ./gradlew build dist distWin
|
||||
env:
|
||||
JADX_BUILD_JAVA_VERSION: 11
|
||||
java-version: 11
|
||||
|
||||
- name: Build with Gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
with:
|
||||
arguments: build dist copyExe
|
||||
|
||||
@@ -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/actions/wrapper-validation@v3
|
||||
@@ -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@v4
|
||||
|
||||
- name: Set up JDK
|
||||
uses: oracle-actions/setup-java@v1
|
||||
with:
|
||||
release: 24
|
||||
|
||||
- name: Set jadx version
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const jadxVersion = context.ref.split('/').pop().substring(1)
|
||||
core.exportVariable('JADX_VERSION', jadxVersion);
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
|
||||
- name: Build
|
||||
run: ./gradlew dist -PbundleJRE=true
|
||||
|
||||
- name: Save JRE bundle artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
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@v4
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
|
||||
- name: Set jadx version and release name
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const jadxVersion = context.ref.split('/').pop().substring(1)
|
||||
core.exportVariable('JADX_VERSION', jadxVersion);
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
|
||||
- name: Build
|
||||
run: ./gradlew dist distWin
|
||||
env:
|
||||
JADX_BUILD_JAVA_VERSION: 11
|
||||
|
||||
- name: Download Windows JRE bundle
|
||||
uses: actions/download-artifact@v4
|
||||
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
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -86,115 +86,100 @@ 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, .jadx.kts)
|
||||
commands (use '<command> --help' for command options):
|
||||
plugins - manage jadx plugins
|
||||
|
||||
options:
|
||||
-d, --output-dir - output directory
|
||||
-ds, --output-dir-src - output directory for sources
|
||||
-dr, --output-dir-res - output directory for resources
|
||||
-r, --no-res - do not decode resources
|
||||
-s, --no-src - do not decompile source code
|
||||
-j, --threads-count - processing threads count, default: 4
|
||||
--single-class - decompile a single class, full name, raw or alias
|
||||
--single-class-output - file or dir for write if decompile a single class
|
||||
--output-format - can be 'java' or 'json', default: java
|
||||
-e, --export-gradle - save as gradle project (set '--export-gradle-type' to 'auto')
|
||||
--export-gradle-type - Gradle project template for export:
|
||||
'auto' - detect automatically
|
||||
'android-app' - Android Application (apk)
|
||||
'android-library' - Android Library (aar)
|
||||
'simple-java' - simple Java
|
||||
-m, --decompilation-mode - code output mode:
|
||||
'auto' - trying best options (default)
|
||||
'restructure' - restore code structure (normal java code)
|
||||
'simple' - simplified instructions (linear, with goto's)
|
||||
'fallback' - raw instructions without modifications
|
||||
--show-bad-code - show inconsistent code (incorrectly decompiled)
|
||||
--no-xml-pretty-print - do not prettify XML
|
||||
--no-imports - disable use of imports, always write entire package name
|
||||
--no-debug-info - disable debug info parsing and processing
|
||||
--add-debug-lines - add comments with debug line numbers if available
|
||||
--no-inline-anonymous - disable anonymous classes inline
|
||||
--no-inline-methods - disable methods inline
|
||||
--no-move-inner-classes - disable move inner classes into parent
|
||||
--no-inline-kotlin-lambda - disable inline for Kotlin lambdas
|
||||
--no-finally - don't extract finally block
|
||||
--no-restore-switch-over-string - don't restore switch over string
|
||||
--no-replace-consts - don't replace constant value with matching constant field
|
||||
--escape-unicode - escape non latin characters in strings (with \u)
|
||||
--respect-bytecode-access-modifiers - don't change original access modifiers
|
||||
--mappings-path - deobfuscation mappings file or directory. Allowed formats: Tiny and Tiny v2 (both '.tiny'), Enigma (.mapping) or Enigma directory
|
||||
--mappings-mode - set mode for handling the deobfuscation mapping file:
|
||||
'read' - just read, user can always save manually (default)
|
||||
'read-and-autosave-every-change' - read and autosave after every change
|
||||
'read-and-autosave-before-closing' - read and autosave before exiting the app or closing the project
|
||||
'ignore' - don't read or save (can be used to skip loading mapping files referenced in the project file)
|
||||
--deobf - activate deobfuscation
|
||||
--deobf-min - min length of name, renamed if shorter, default: 3
|
||||
--deobf-max - max length of name, renamed if longer, default: 64
|
||||
--deobf-whitelist - space separated list of classes (full name) and packages (ends with '.*') to exclude from deobfuscation, default: android.support.v4.* android.support.v7.* android.support.v4.os.* android.support.annotation.Px androidx.core.os.* androidx.annotation.Px
|
||||
--deobf-cfg-file - deobfuscation mappings file used for JADX auto-generated names (in the JOBF file format), default: same dir and name as input file with '.jobf' extension
|
||||
--deobf-cfg-file-mode - set mode for handling the JADX auto-generated names' deobfuscation map file:
|
||||
'read' - read if found, don't save (default)
|
||||
'read-or-save' - read if found, save otherwise (don't overwrite)
|
||||
'overwrite' - don't read, always save
|
||||
'ignore' - don't read and don't save
|
||||
--deobf-res-name-source - better name source for resources:
|
||||
'auto' - automatically select best name (default)
|
||||
'resources' - use resources names
|
||||
'code' - use R class fields names
|
||||
--use-source-name-as-class-name-alias - use source name as class name alias:
|
||||
'always' - always use source name if it's available
|
||||
'if-better' - use source name if it seems better than the current one
|
||||
'never' - never use source name, even if it's available
|
||||
--source-name-repeat-limit - allow using source name if it appears less than a limit number, default: 10
|
||||
--use-kotlin-methods-for-var-names - use kotlin intrinsic methods to rename variables, values: disable, apply, apply-and-hide, default: apply
|
||||
--rename-flags - fix options (comma-separated list of):
|
||||
'case' - fix case sensitivity issues (according to --fs-case-sensitive option),
|
||||
'valid' - rename java identifiers to make them valid,
|
||||
'printable' - remove non-printable chars from identifiers,
|
||||
or single 'none' - to disable all renames
|
||||
or single 'all' - to enable all (default)
|
||||
--integer-format - how integers are displayed:
|
||||
'auto' - automatically select (default)
|
||||
'decimal' - use decimal
|
||||
'hexadecimal' - use hexadecimal
|
||||
--fs-case-sensitive - treat filesystem as case sensitive, false by default
|
||||
--cfg - save methods control flow graph to dot file
|
||||
--raw-cfg - save methods control flow graph (use raw instructions)
|
||||
-f, --fallback - set '--decompilation-mode' to 'fallback' (deprecated)
|
||||
--use-dx - use dx/d8 to convert java bytecode
|
||||
--comments-level - set code comments level, values: error, warn, info, debug, user-only, none, default: info
|
||||
--log-level - set log level, values: quiet, progress, error, warn, info, debug, default: progress
|
||||
-v, --verbose - verbose output (set --log-level to DEBUG)
|
||||
-q, --quiet - turn off output (set --log-level to QUIET)
|
||||
--disable-plugins - comma separated list of plugin ids to disable, default:
|
||||
--version - print jadx version
|
||||
-h, --help - print this help
|
||||
-d, --output-dir - output directory
|
||||
-ds, --output-dir-src - output directory for sources
|
||||
-dr, --output-dir-res - output directory for resources
|
||||
-r, --no-res - do not decode resources
|
||||
-s, --no-src - do not decompile source code
|
||||
--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.* android.os.* androidx.core.os.* androidx.annotation.*
|
||||
--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, SRG_FILE, XSRG_FILE, JAM_FILE, CSRG_FILE, TSRG_FILE, TSRG_2_FILE, PROGUARD_FILE, INTELLIJ_MIGRATION_MAP_FILE, RECAF_SIMPLE_FILE, JOBF_FILE], default: AUTO
|
||||
- rename-mappings.invert - invert mapping on load, values: [yes, no], default: no
|
||||
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_FILE, TINY_2_FILE, ENIGMA_FILE, ENIGMA_DIR, SRG_FILE, XSRG_FILE, CSRG_FILE, TSRG_FILE, TSRG_2_FILE, PROGUARD_FILE], default: AUTO
|
||||
- rename-mappings.invert - invert mapping on load, values: [yes, no], default: no
|
||||
|
||||
Environment variables:
|
||||
JADX_DISABLE_XML_SECURITY - set to 'true' to disable all security checks for XML files
|
||||
@@ -213,24 +198,6 @@ Examples:
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
|
||||
### Troubleshooting
|
||||
Please check wiki page [Troubleshooting Q&A](https://github.com/skylot/jadx/wiki/Troubleshooting-Q&A)
|
||||
|
||||
|
||||
+3
-4
@@ -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.
|
||||
|
||||
+25
-30
@@ -6,7 +6,7 @@ import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
|
||||
import java.util.Locale
|
||||
|
||||
plugins {
|
||||
id("com.github.ben-manes.versions") version "0.52.0"
|
||||
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")
|
||||
@@ -115,34 +103,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"
|
||||
|
||||
val guiTask = tasks.getByPath("jadx-gui:copyDistWin")
|
||||
dependsOn(guiTask)
|
||||
from(guiTask.outputs)
|
||||
// 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"
|
||||
|
||||
val guiTask = tasks.getByPath(":jadx-gui:copyDistWinWithJre")
|
||||
dependsOn(guiTask)
|
||||
from(guiTask.outputs)
|
||||
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)
|
||||
|
||||
@@ -150,14 +144,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)
|
||||
|
||||
@@ -3,9 +3,7 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.21")
|
||||
|
||||
implementation("org.openrewrite:plugin:6.19.1")
|
||||
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.23")
|
||||
}
|
||||
|
||||
repositories {
|
||||
|
||||
@@ -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.13")
|
||||
compileOnly("org.jetbrains:annotations:24.1.0")
|
||||
|
||||
testImplementation("ch.qos.logback:logback-classic:1.5.18")
|
||||
testImplementation("org.assertj:assertj-core:3.27.3")
|
||||
testImplementation("ch.qos.logback:logback-classic:1.5.6")
|
||||
testImplementation("org.hamcrest:hamcrest-library:2.2")
|
||||
testImplementation("org.mockito:mockito-core:5.11.0")
|
||||
testImplementation("org.assertj:assertj-core:3.25.3")
|
||||
|
||||
testImplementation("org.junit.jupiter:junit-jupiter:5.12.2")
|
||||
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
|
||||
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 {
|
||||
|
||||
@@ -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.8.0")
|
||||
rewrite("org.openrewrite.recipe:rewrite-logging-frameworks:3.8.0")
|
||||
rewrite("org.openrewrite.recipe:rewrite-migrate-java:3.9.0")
|
||||
rewrite("org.openrewrite.recipe:rewrite-static-analysis:2.9.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")
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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 \
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+2
-2
@@ -1,7 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=61ad310d3c7d3e5da131b76bbf22b5a4c0786e9d892dae8c1658d4b484de3caa
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
|
||||
distributionSha256Sum=544c35d6bd849ae8a5ed0bcea39ba677dc40f49df7d1835561582da2009b961d
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -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
|
||||
@@ -205,7 +203,7 @@ 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.
|
||||
|
||||
Vendored
-2
@@ -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 ##########################################################################
|
||||
|
||||
@@ -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.6"
|
||||
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,14 +16,12 @@ 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"))
|
||||
|
||||
implementation("org.jcommander:jcommander:2.0")
|
||||
implementation("ch.qos.logback:logback-classic:1.5.18")
|
||||
implementation("org.jcommander:jcommander:1.83")
|
||||
implementation("ch.qos.logback:logback-classic:1.5.6")
|
||||
}
|
||||
|
||||
application {
|
||||
@@ -37,8 +33,6 @@ application {
|
||||
"-XX:MaxRAMPercentage=70.0",
|
||||
// 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,7 +13,6 @@ 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;
|
||||
@@ -27,7 +26,7 @@ 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;
|
||||
|
||||
@@ -42,7 +41,6 @@ public class JCommanderWrapper {
|
||||
public boolean parse(String[] args) {
|
||||
try {
|
||||
jc.parse(args);
|
||||
applyFiles(argsObj);
|
||||
return true;
|
||||
} catch (ParameterException e) {
|
||||
System.err.println("Arguments parse error: " + e.getMessage());
|
||||
@@ -51,15 +49,6 @@ public class JCommanderWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -68,21 +57,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" })
|
||||
@@ -96,6 +84,10 @@ public class JCommanderWrapper {
|
||||
return value;
|
||||
}
|
||||
|
||||
public List<String> getUnknownOptions() {
|
||||
return jc.getUnknownOptions();
|
||||
}
|
||||
|
||||
public void printUsage() {
|
||||
LogHelper.setLogLevel(LogHelper.LogLevelEnum.ERROR); // mute logger while printing help
|
||||
|
||||
@@ -141,16 +133,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;
|
||||
|
||||
@@ -163,12 +153,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]);
|
||||
@@ -191,11 +177,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
|
||||
*/
|
||||
@@ -244,17 +225,13 @@ public class JCommanderWrapper {
|
||||
JadxPluginManager pluginManager = decompiler.getPluginManager();
|
||||
pluginManager.load(new JadxExternalPluginsLoader());
|
||||
pluginManager.initAll();
|
||||
try {
|
||||
for (PluginContext context : pluginManager.getAllPluginContexts()) {
|
||||
JadxPluginOptions options = context.getOptions();
|
||||
if (options != null) {
|
||||
if (appendPlugin(context.getPluginInfo(), context.getOptions(), sb, maxNamesLen)) {
|
||||
k++;
|
||||
}
|
||||
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) {
|
||||
@@ -263,12 +240,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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -12,59 +9,44 @@ import jadx.api.impl.AnnotatedCodeWriter;
|
||||
import jadx.api.impl.NoOpCodeCache;
|
||||
import jadx.api.impl.SimpleCodeWriter;
|
||||
import jadx.cli.LogHelper.LogLevelEnum;
|
||||
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 = new JadxCLIArgs();
|
||||
if (cliArgs.processArgs(args)) {
|
||||
JadxArgs jadxArgs = buildArgs(cliArgs);
|
||||
if (argsMod != null) {
|
||||
argsMod.accept(jadxArgs);
|
||||
}
|
||||
return runSave(jadxArgs, cliArgs);
|
||||
}
|
||||
return 0;
|
||||
} 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.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)) {
|
||||
@@ -78,11 +60,11 @@ public class JadxCLI {
|
||||
if (errorsCount != 0) {
|
||||
jadx.printErrorsReport();
|
||||
LOG.error("finished with errors, count: {}", errorsCount);
|
||||
return 1;
|
||||
} else {
|
||||
LOG.info("done");
|
||||
}
|
||||
LOG.info("done");
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static void initCodeWriterProvider(JadxArgs jadxArgs) {
|
||||
|
||||
@@ -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,8 +14,6 @@ import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import com.beust.jcommander.DynamicParameter;
|
||||
import com.beust.jcommander.IStringConverter;
|
||||
import com.beust.jcommander.Parameter;
|
||||
@@ -29,17 +27,15 @@ 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.core.deobf.conditions.DeobfWhitelist;
|
||||
import jadx.core.export.ExportGradleType;
|
||||
import jadx.core.utils.exceptions.JadxArgsValidateException;
|
||||
import jadx.core.utils.files.FileUtils;
|
||||
|
||||
public class JadxCLIArgs {
|
||||
|
||||
@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, .jadx.kts)")
|
||||
protected List<String> files = new ArrayList<>(1);
|
||||
|
||||
@Parameter(names = { "-d", "--output-dir" }, description = "output directory")
|
||||
protected String outDir;
|
||||
@@ -56,9 +52,6 @@ public class JadxCLIArgs {
|
||||
@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;
|
||||
|
||||
@Parameter(names = { "--single-class" }, description = "decompile a single class, full name, raw or alias")
|
||||
protected String singleClass = null;
|
||||
|
||||
@@ -68,19 +61,11 @@ public class JadxCLIArgs {
|
||||
@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" },
|
||||
@@ -123,9 +108,6 @@ public class JadxCLIArgs {
|
||||
@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;
|
||||
|
||||
@@ -184,15 +166,8 @@ public class JadxCLIArgs {
|
||||
)
|
||||
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" },
|
||||
@@ -204,22 +179,6 @@ public class JadxCLIArgs {
|
||||
)
|
||||
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",
|
||||
@@ -227,12 +186,6 @@ public class JadxCLIArgs {
|
||||
)
|
||||
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):"
|
||||
@@ -290,9 +243,6 @@ public class JadxCLIArgs {
|
||||
@Parameter(names = { "-q", "--quiet" }, description = "turn off output (set --log-level to QUIET)")
|
||||
protected boolean quiet = false;
|
||||
|
||||
@Parameter(names = { "--disable-plugins" }, description = "comma separated list of plugin ids to disable")
|
||||
protected String disablePlugins = "";
|
||||
|
||||
@Parameter(names = { "--version" }, description = "print jadx version")
|
||||
protected boolean printVersion = false;
|
||||
|
||||
@@ -303,11 +253,29 @@ public class JadxCLIArgs {
|
||||
protected Map<String, String> pluginOptions = new HashMap<>();
|
||||
|
||||
public boolean processArgs(String[] args) {
|
||||
JCommanderWrapper jcw = new JCommanderWrapper(this);
|
||||
JCommanderWrapper<JadxCLIArgs> jcw = new JCommanderWrapper<>(this);
|
||||
return jcw.parse(args) && process(jcw);
|
||||
}
|
||||
|
||||
public boolean process(JCommanderWrapper jcw) {
|
||||
/**
|
||||
* 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 false;
|
||||
}
|
||||
jcw.overrideProvided(this);
|
||||
return process(jcw);
|
||||
}
|
||||
|
||||
protected JadxCLIArgs newInstance() {
|
||||
return new JadxCLIArgs();
|
||||
}
|
||||
|
||||
private boolean process(JCommanderWrapper<JadxCLIArgs> jcw) {
|
||||
files.addAll(jcw.getUnknownOptions());
|
||||
if (jcw.processCommands()) {
|
||||
return false;
|
||||
}
|
||||
@@ -359,17 +327,12 @@ public class JadxCLIArgs {
|
||||
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);
|
||||
@@ -379,14 +342,12 @@ public class JadxCLIArgs {
|
||||
args.setMoveInnerClasses(moveInnerClasses);
|
||||
args.setAllowInlineKotlinLambda(allowInlineKotlinLambda);
|
||||
args.setExtractFinally(extractFinally);
|
||||
args.setRestoreSwitchOverString(restoreSwitchOverString);
|
||||
args.setRenameFlags(renameFlags);
|
||||
args.setFsCaseSensitive(fsCaseSensitive);
|
||||
args.setCommentsLevel(commentsLevel);
|
||||
args.setIntegerFormat(integerFormat);
|
||||
args.setUseDxInput(useDx);
|
||||
args.setPluginOptions(pluginOptions);
|
||||
args.setDisabledPlugins(Arrays.stream(disablePlugins.split(",")).map(String::trim).collect(Collectors.toSet()));
|
||||
return args;
|
||||
}
|
||||
|
||||
@@ -394,10 +355,6 @@ public class JadxCLIArgs {
|
||||
return files;
|
||||
}
|
||||
|
||||
public void setFiles(List<String> files) {
|
||||
this.files = files;
|
||||
}
|
||||
|
||||
public String getOutDir() {
|
||||
return outDir;
|
||||
}
|
||||
@@ -478,10 +435,6 @@ public class JadxCLIArgs {
|
||||
return extractFinally;
|
||||
}
|
||||
|
||||
public boolean isRestoreSwitchOverString() {
|
||||
return restoreSwitchOverString;
|
||||
}
|
||||
|
||||
public Path getUserRenamesMappingsPath() {
|
||||
return userRenamesMappingsPath;
|
||||
}
|
||||
@@ -514,27 +467,8 @@ public class JadxCLIArgs {
|
||||
return generatedRenamesMappingFileMode;
|
||||
}
|
||||
|
||||
public UseSourceNameAsClassNameAlias getUseSourceNameAsClassNameAlias() {
|
||||
if (useSourceNameAsClassNameAlias != null) {
|
||||
return useSourceNameAsClassNameAlias;
|
||||
} else if (deobfuscationUseSourceNameAsAlias != null) {
|
||||
// noinspection deprecation
|
||||
return UseSourceNameAsClassNameAlias.create(deobfuscationUseSourceNameAsAlias);
|
||||
} else {
|
||||
return UseSourceNameAsClassNameAlias.getDefault();
|
||||
}
|
||||
}
|
||||
|
||||
public int getSourceNameRepeatLimit() {
|
||||
return sourceNameRepeatLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #getUseSourceNameAsClassNameAlias()} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public boolean isDeobfuscationUseSourceNameAsAlias() {
|
||||
return getUseSourceNameAsClassNameAlias().toBoolean();
|
||||
return deobfuscationUseSourceNameAsAlias;
|
||||
}
|
||||
|
||||
public ResourceNameSource getResourceNameSource() {
|
||||
@@ -593,10 +527,6 @@ public class JadxCLIArgs {
|
||||
return fsCaseSensitive;
|
||||
}
|
||||
|
||||
public boolean isUseHeadersForDetectResourceExtensions() {
|
||||
return useHeadersForDetectResourceExtensions;
|
||||
}
|
||||
|
||||
public CommentsLevel getCommentsLevel() {
|
||||
return commentsLevel;
|
||||
}
|
||||
@@ -609,10 +539,6 @@ public class JadxCLIArgs {
|
||||
return pluginOptions;
|
||||
}
|
||||
|
||||
public String getDisablePlugins() {
|
||||
return disablePlugins;
|
||||
}
|
||||
|
||||
static class RenameConverter implements IStringConverter<Set<RenameEnum>> {
|
||||
private final String paramName;
|
||||
|
||||
@@ -666,24 +592,12 @@ public class JadxCLIArgs {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package jadx.cli;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import com.beust.jcommander.JCommander;
|
||||
|
||||
@@ -10,7 +10,7 @@ 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,7 +24,7 @@ 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
|
||||
|
||||
@@ -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;
|
||||
@@ -48,7 +47,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);
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
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.JadxPluginMetadata;
|
||||
@@ -21,40 +15,24 @@ 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;
|
||||
|
||||
@@ -63,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();
|
||||
@@ -101,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<JadxPluginMetadata> availableList = JadxPluginsList.getInstance().get();
|
||||
System.out.println("Available plugins: " + availableList.size());
|
||||
int i = 1;
|
||||
for (JadxPluginMetadata plugin : availableList) {
|
||||
System.out.println(" - " + plugin.getName() + ": " + plugin.getDescription()
|
||||
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,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;
|
||||
@@ -18,11 +21,6 @@ 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;
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
for (Path resFile : inputPaths) {
|
||||
LOG.info("Processing {}", resFile);
|
||||
ResTableBinaryParser resTableParser = new ResTableBinaryParser(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;
|
||||
}
|
||||
|
||||
@@ -9,8 +9,9 @@ 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,16 +25,16 @@ 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
|
||||
@@ -42,6 +43,7 @@ public class RenameConverterTest {
|
||||
() -> 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);
|
||||
}
|
||||
}
|
||||
@@ -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.
@@ -3,5 +3,5 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("io.get-coursier.util:directories-jni:0.1.3")
|
||||
implementation("dev.dirs:directories:26")
|
||||
}
|
||||
|
||||
@@ -3,19 +3,12 @@ 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;
|
||||
@@ -42,20 +35,20 @@ public class JadxCommonFiles {
|
||||
|
||||
public void init() {
|
||||
try {
|
||||
configDir = loadEnvDir("JADX_CONFIG_DIR", pd -> pd.configDir);
|
||||
cacheDir = loadEnvDir("JADX_CACHE_DIR", pd -> pd.cacheDir);
|
||||
configDir = loadEnvDir("JADX_CONFIG_DIR");
|
||||
cacheDir = loadEnvDir("JADX_CACHE_DIR");
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to init common directories", e);
|
||||
}
|
||||
}
|
||||
|
||||
private Path loadEnvDir(String envVar, Function<ProjectDirectories, String> dirFunc) throws IOException {
|
||||
private Path loadEnvDir(String envVar) throws IOException {
|
||||
String envDir = JadxCommonEnv.get(envVar, null);
|
||||
String dirStr;
|
||||
if (envDir != null) {
|
||||
dirStr = envDir;
|
||||
} else {
|
||||
dirStr = dirFunc.apply(loadDirs());
|
||||
dirStr = loadDirs().configDir;
|
||||
}
|
||||
Path path = Path.of(dirStr).toAbsolutePath();
|
||||
Files.createDirectories(path);
|
||||
@@ -63,32 +56,10 @@ public class JadxCommonFiles {
|
||||
}
|
||||
|
||||
private synchronized ProjectDirectories loadDirs() {
|
||||
ProjectDirectories currentDirs = dirs;
|
||||
if (currentDirs != null) {
|
||||
return currentDirs;
|
||||
if (dirs == null) {
|
||||
dirs = ProjectDirectories.from("io.github", "skylot", "jadx");
|
||||
}
|
||||
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 or Foreign implementation
|
||||
*/
|
||||
private static Windows getWinDirs() {
|
||||
Windows defSup = Windows.getDefaultSupplier().get();
|
||||
if (defSup instanceof WindowsPowerShell) {
|
||||
return new WindowsJni();
|
||||
}
|
||||
return defSup;
|
||||
return dirs;
|
||||
}
|
||||
|
||||
public Path getCacheDir() {
|
||||
|
||||
@@ -1,31 +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) {
|
||||
dir = Files.createTempDirectory(Paths.get(jadxTmpDir), 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
## jadx zip
|
||||
|
||||
Custom zip reader implementation to fight tampering and provide additional security checks
|
||||
@@ -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,35 +0,0 @@
|
||||
package jadx.zip;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class ZipContent implements Closeable {
|
||||
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 = entries.stream().collect(Collectors.toMap(IZipEntry::getName, Function.identity()));
|
||||
}
|
||||
|
||||
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,35 +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());
|
||||
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,132 +0,0 @@
|
||||
package jadx.zip.security;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
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 File CWD = getCWD();
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
try {
|
||||
File currentPath = CWD;
|
||||
File canonical = new File(currentPath, entryName).getCanonicalFile();
|
||||
if (isInSubDirectoryInternal(currentPath, canonical)) {
|
||||
return true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// check failed
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
private static File getCWD() {
|
||||
try {
|
||||
return new File(".").getCanonicalFile();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to init current working dir constant", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,11 +4,10 @@ plugins {
|
||||
|
||||
dependencies {
|
||||
api(project(":jadx-plugins:jadx-input-api"))
|
||||
api(project(":jadx-commons:jadx-zip"))
|
||||
|
||||
implementation("com.google.code.gson:gson:2.13.1")
|
||||
implementation("com.google.code.gson:gson:2.10.1")
|
||||
|
||||
testImplementation("org.apache.commons:commons-lang3:3.17.0")
|
||||
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 +21,9 @@ dependencies {
|
||||
strictly("[3.33, 3.34[") // from 3.34 compiled with Java 17
|
||||
}
|
||||
}
|
||||
testImplementation("tools.profiler:async-profiler:4.0")
|
||||
testImplementation("tools.profiler:async-profiler: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
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ 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;
|
||||
@@ -22,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;
|
||||
@@ -31,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 {
|
||||
@@ -91,7 +83,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
|
||||
@@ -107,8 +98,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();
|
||||
@@ -120,7 +110,7 @@ public class JadxArgs implements Closeable {
|
||||
/**
|
||||
* List of classes and packages (ends with '.*') to exclude from deobfuscation
|
||||
*/
|
||||
private List<String> deobfuscationWhitelist = DeobfWhitelist.DEFAULT_LIST;
|
||||
private List<String> deobfuscationWhitelist = new ArrayList<>(DeobfWhitelist.DEFAULT_LIST);
|
||||
|
||||
/**
|
||||
* Nodes alias provider for deobfuscator and rename visitor
|
||||
@@ -135,9 +125,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;
|
||||
|
||||
@@ -175,31 +163,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;
|
||||
|
||||
private Map<String, String> pluginOptions = new HashMap<>();
|
||||
|
||||
private Set<String> disabledPlugins = new HashSet<>();
|
||||
|
||||
private JadxPluginLoader pluginLoader = new JadxBasePluginLoader();
|
||||
|
||||
private boolean loadJadxClsSetFile = true;
|
||||
@@ -455,37 +425,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() {
|
||||
@@ -569,33 +514,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() {
|
||||
@@ -754,22 +677,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;
|
||||
}
|
||||
@@ -778,14 +685,6 @@ public class JadxArgs implements Closeable {
|
||||
this.skipFilesSave = skipFilesSave;
|
||||
}
|
||||
|
||||
public boolean isRunDebugChecks() {
|
||||
return runDebugChecks;
|
||||
}
|
||||
|
||||
public void setRunDebugChecks(boolean runDebugChecks) {
|
||||
this.runDebugChecks = runDebugChecks;
|
||||
}
|
||||
|
||||
public Map<String, String> getPluginOptions() {
|
||||
return pluginOptions;
|
||||
}
|
||||
@@ -794,14 +693,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;
|
||||
}
|
||||
@@ -818,14 +709,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
|
||||
*/
|
||||
@@ -833,11 +716,10 @@ 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
|
||||
+ "|" + buildPluginsHash(decompiler);
|
||||
@@ -873,8 +755,7 @@ public class JadxArgs implements Closeable {
|
||||
+ ", generatedRenamesMappingFile=" + generatedRenamesMappingFile
|
||||
+ ", generatedRenamesMappingFileMode=" + generatedRenamesMappingFileMode
|
||||
+ ", resourceNameSource=" + resourceNameSource
|
||||
+ ", useSourceNameAsClassNameAlias=" + useSourceNameAsClassNameAlias
|
||||
+ ", sourceNameRepeatLimit=" + sourceNameRepeatLimit
|
||||
+ ", useSourceNameAsClassAlias=" + useSourceNameAsClassAlias
|
||||
+ ", useKotlinMethodsForVarNames=" + useKotlinMethodsForVarNames
|
||||
+ ", insertDebugLines=" + insertDebugLines
|
||||
+ ", extractFinally=" + extractFinally
|
||||
@@ -883,9 +764,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
|
||||
@@ -897,7 +777,6 @@ public class JadxArgs implements Closeable {
|
||||
+ ", pluginOptions=" + pluginOptions
|
||||
+ ", cfgOutput=" + cfgOutput
|
||||
+ ", rawCFGOutput=" + rawCFGOutput
|
||||
+ ", useHeadersForDetectResourceExtensions=" + useHeadersForDetectResourceExtensions
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,8 @@ 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.ResourcesSaver;
|
||||
import jadx.zip.ZipReader;
|
||||
|
||||
/**
|
||||
* Jadx API usage example:
|
||||
@@ -87,52 +85,44 @@ 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 final IDecompileScheduler decompileScheduler = new DecompilerScheduler();
|
||||
private final ResourcesLoader resourcesLoader;
|
||||
private final JadxEventsImpl events = new JadxEventsImpl();
|
||||
private final ResourcesLoader resourcesLoader = new ResourcesLoader(this);
|
||||
|
||||
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.init();
|
||||
// load classes and resources
|
||||
root.loadClasses(loadedInputs);
|
||||
root.loadResources(resourcesLoader, getResources());
|
||||
root.finishClassLoad();
|
||||
root.initClassPath();
|
||||
// init passes
|
||||
root = new RootNode(args);
|
||||
root.setDecompilerRef(this);
|
||||
root.mergePasses(customPasses);
|
||||
root.loadClasses(loadedInputs);
|
||||
root.initClassPath();
|
||||
root.loadResources(resourcesLoader, getResources());
|
||||
root.runPreDecompileStage();
|
||||
root.initPasses();
|
||||
loadFinished();
|
||||
@@ -163,7 +153,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -174,37 +164,41 @@ public final class JadxDecompiler implements Closeable {
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
unloadPlugins();
|
||||
root = null;
|
||||
classes = null;
|
||||
resources = null;
|
||||
binaryXmlParser = 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() {
|
||||
@@ -221,10 +215,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);
|
||||
@@ -302,28 +292,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;
|
||||
}
|
||||
@@ -335,15 +328,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();
|
||||
@@ -351,44 +342,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);
|
||||
@@ -503,6 +466,13 @@ public final class JadxDecompiler implements Closeable {
|
||||
return root;
|
||||
}
|
||||
|
||||
synchronized BinaryXMLParser getBinaryXmlParser() {
|
||||
if (binaryXmlParser == null) {
|
||||
binaryXmlParser = new BinaryXMLParser(root);
|
||||
}
|
||||
return binaryXmlParser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get JavaClass by ClassNode without loading and decompilation
|
||||
*/
|
||||
@@ -619,8 +589,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:
|
||||
@@ -703,10 +671,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);
|
||||
}
|
||||
@@ -734,14 +698,6 @@ public final class JadxDecompiler implements Closeable {
|
||||
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();
|
||||
|
||||
@@ -76,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);
|
||||
@@ -56,73 +74,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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -10,15 +10,9 @@ public enum ResourceType {
|
||||
CODE(".dex", ".jar", ".class"),
|
||||
XML(".xml"),
|
||||
ARSC(".arsc"),
|
||||
APK(".apk", ".apkm", ".apks"),
|
||||
FONT(".ttf", ".ttc", ".otf"),
|
||||
IMG(".png", ".gif", ".jpg", ".webp", ".bmp", ".tiff"),
|
||||
ARCHIVE(".zip", ".rar", ".7zip", ".7z", ".arj", ".tar", ".gzip", ".bzip", ".bzip2", ".cab", ".cpio", ".ar", ".gz", ".tgz", ".bz2"),
|
||||
VIDEOS(".mp4", ".mkv", ".webm", ".avi", ".flv", ".3gp"),
|
||||
SOUNDS(".aac", ".ogg", ".opus", ".mp3", ".wav", ".wma", ".mid", ".midi"),
|
||||
JSON(".json"),
|
||||
TEXT(".txt", ".ini", ".conf", ".yaml", ".properties", ".js"),
|
||||
HTML(".html"),
|
||||
FONT(".ttf", ".otf"),
|
||||
IMG(".png", ".gif", ".jpg"),
|
||||
MEDIA(".mp3", ".wav"),
|
||||
LIB(".so"),
|
||||
MANIFEST,
|
||||
UNKNOWN;
|
||||
|
||||
@@ -6,18 +6,21 @@ import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
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;
|
||||
@@ -28,8 +31,6 @@ 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 static jadx.core.utils.files.FileUtils.READ_BUFFER_SIZE;
|
||||
import static jadx.core.utils.files.FileUtils.copyStream;
|
||||
@@ -38,21 +39,21 @@ import static jadx.core.utils.files.FileUtils.copyStream;
|
||||
public final class ResourcesLoader implements IResourcesLoader {
|
||||
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;
|
||||
ResourcesLoader(JadxDecompiler jadxRef) {
|
||||
this.jadxRef = jadxRef;
|
||||
this.resTableParserProviders.add(new ResTableBinaryParserProvider());
|
||||
}
|
||||
|
||||
List<ResourceFile> load(RootNode root) {
|
||||
init(root);
|
||||
List<File> inputFiles = decompiler.getArgs().getInputFiles();
|
||||
List<File> inputFiles = jadxRef.getArgs().getInputFiles();
|
||||
List<ResourceFile> list = new ArrayList<>(inputFiles.size());
|
||||
for (File file : inputFiles) {
|
||||
loadFile(list, file);
|
||||
@@ -93,19 +94,28 @@ public final class ResourcesLoader implements IResourcesLoader {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,13 +170,12 @@ public final class ResourcesLoader implements IResourcesLoader {
|
||||
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();
|
||||
@@ -186,7 +195,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;
|
||||
@@ -199,31 +208,25 @@ 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);
|
||||
}
|
||||
}
|
||||
@@ -231,12 +234,12 @@ public final class ResourcesLoader implements IResourcesLoader {
|
||||
public static ICodeInfo loadToCodeWriter(InputStream is) throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream(READ_BUFFER_SIZE);
|
||||
copyStream(is, baos);
|
||||
return new SimpleCodeInfo(baos.toString(StandardCharsets.UTF_8));
|
||||
return new SimpleCodeInfo(baos.toString("UTF-8"));
|
||||
}
|
||||
|
||||
private synchronized BinaryXMLParser loadBinaryXmlParser() {
|
||||
if (binaryXmlParser == null) {
|
||||
binaryXmlParser = new BinaryXMLParser(decompiler.getRoot());
|
||||
binaryXmlParser = new BinaryXMLParser(jadxRef.getRoot());
|
||||
}
|
||||
return binaryXmlParser;
|
||||
}
|
||||
|
||||
@@ -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,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 {
|
||||
@@ -61,7 +62,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()) {
|
||||
@@ -150,6 +151,7 @@ public class AnnotatedCodeWriter extends SimpleCodeWriter implements ICodeWriter
|
||||
|
||||
@Override
|
||||
public ICodeInfo finish() {
|
||||
validateAnnotations();
|
||||
String code = buf.toString();
|
||||
buf = null;
|
||||
return new AnnotatedCodeInfo(code, lineMap, annotations);
|
||||
@@ -159,4 +161,17 @@ public class AnnotatedCodeWriter extends SimpleCodeWriter implements ICodeWriter
|
||||
public Map<Integer, ICodeAnnotation> getRawAnnotations() {
|
||||
return annotations;
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,7 +6,6 @@ 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;
|
||||
@@ -14,7 +13,6 @@ 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 {
|
||||
|
||||
@@ -55,14 +53,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();
|
||||
}
|
||||
|
||||
@@ -6,13 +6,4 @@ public abstract class JadxEventType<T extends IJadxEvent> {
|
||||
return new JadxEventType<>() {
|
||||
};
|
||||
}
|
||||
|
||||
public static <E extends IJadxEvent> JadxEventType<E> create(String name) {
|
||||
return new JadxEventType<>() {
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,17 +16,17 @@ public class JadxEvents {
|
||||
/**
|
||||
* Notify about renaming done by user (GUI only).
|
||||
*/
|
||||
public static final JadxEventType<NodeRenamedByUser> NODE_RENAMED_BY_USER = create("NODE_RENAMED_BY_USER");
|
||||
public static final JadxEventType<NodeRenamedByUser> NODE_RENAMED_BY_USER = create();
|
||||
|
||||
/**
|
||||
* Request reload of a current project (GUI only).
|
||||
*/
|
||||
public static final JadxEventType<ReloadProject> RELOAD_PROJECT = create("RELOAD_PROJECT");
|
||||
public static final JadxEventType<ReloadProject> RELOAD_PROJECT = create();
|
||||
|
||||
/**
|
||||
* Request reload of a settings window (GUI only).
|
||||
* Useful for a reload custom settings group which was set with
|
||||
* {@link JadxGuiSettings#setCustomSettingsGroup(ISettingsGroup)}.
|
||||
*/
|
||||
public static final JadxEventType<ReloadSettingsWindow> RELOAD_SETTINGS_WINDOW = create("RELOAD_SETTINGS_WINDOW");
|
||||
public static final JadxEventType<ReloadSettingsWindow> RELOAD_SETTINGS_WINDOW = create();
|
||||
}
|
||||
|
||||
@@ -26,13 +26,4 @@ public interface ISettingsGroup {
|
||||
default List<ISettingsGroup> getSubGroups() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings close handler.
|
||||
* Apply settings if 'save' param set to true.
|
||||
* It can be used to clean up resources.
|
||||
*/
|
||||
default void close(boolean save) {
|
||||
// optional method
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,11 @@ package jadx.api.plugins.gui;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import javax.swing.ImageIcon;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.api.gui.tree.ITreeNode;
|
||||
import jadx.api.metadata.ICodeNodeRef;
|
||||
|
||||
public interface JadxGuiContext {
|
||||
@@ -38,15 +33,6 @@ public interface JadxGuiContext {
|
||||
@Nullable String keyBinding,
|
||||
Consumer<ICodeNodeRef> action);
|
||||
|
||||
/**
|
||||
* Add popup menu entry for tree node
|
||||
*
|
||||
* @param name entry title
|
||||
* @param addPredicate check if entry should be added for provided node, called on popup creation
|
||||
*/
|
||||
@ApiStatus.Experimental
|
||||
void addTreePopupMenuEntry(String name, Predicate<ITreeNode> addPredicate, Consumer<ITreeNode> action);
|
||||
|
||||
/**
|
||||
* Attach new key binding to main window
|
||||
*
|
||||
@@ -64,22 +50,6 @@ public interface JadxGuiContext {
|
||||
*/
|
||||
JadxGuiSettings settings();
|
||||
|
||||
/**
|
||||
* Main window component.
|
||||
* Can be used as a parent for creating new windows or dialogs.
|
||||
*/
|
||||
JFrame getMainFrame();
|
||||
|
||||
/**
|
||||
* Load SVG icon from jadx resources.
|
||||
* All available icons can be found in "jadx-gui/src/main/resources/icons".
|
||||
* Method is thread-safe.
|
||||
*
|
||||
* @param name short name in form: "category/iconName", example: "nodes/publicClass"
|
||||
* @return loaded and cached icon, if icon not found returns default icon: "ui/error"
|
||||
*/
|
||||
ImageIcon getSVGIcon(String name);
|
||||
|
||||
ICodeNodeRef getNodeUnderCaret();
|
||||
|
||||
ICodeNodeRef getNodeUnderMouse();
|
||||
@@ -95,11 +65,6 @@ public interface JadxGuiContext {
|
||||
*/
|
||||
boolean open(ICodeNodeRef ref);
|
||||
|
||||
/**
|
||||
* Open usage dialog for a node
|
||||
*/
|
||||
void openUsageDialog(ICodeNodeRef ref);
|
||||
|
||||
/**
|
||||
* Reload code in active tab
|
||||
*/
|
||||
|
||||
+11
-22
@@ -1,22 +1,21 @@
|
||||
package jadx.zip.io;
|
||||
package jadx.api.plugins.utils;
|
||||
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class LimitedInputStream extends FilterInputStream {
|
||||
|
||||
private final long maxSize;
|
||||
|
||||
private long currentPos;
|
||||
private long markPos;
|
||||
|
||||
public LimitedInputStream(InputStream in, long maxSize) {
|
||||
protected LimitedInputStream(InputStream in, long maxSize) {
|
||||
super(in);
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
private void addAndCheckPos(long count) {
|
||||
currentPos += count;
|
||||
private void checkPos() {
|
||||
if (currentPos > maxSize) {
|
||||
throw new IllegalStateException("Read limit exceeded");
|
||||
}
|
||||
@@ -26,17 +25,18 @@ public class LimitedInputStream extends FilterInputStream {
|
||||
public int read() throws IOException {
|
||||
int data = super.read();
|
||||
if (data != -1) {
|
||||
addAndCheckPos(1);
|
||||
currentPos++;
|
||||
checkPos();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@SuppressWarnings("NullableProblems")
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
int count = super.read(b, off, len);
|
||||
if (count > 0) {
|
||||
addAndCheckPos(count);
|
||||
currentPos += count;
|
||||
checkPos();
|
||||
}
|
||||
return count;
|
||||
}
|
||||
@@ -44,21 +44,10 @@ public class LimitedInputStream extends FilterInputStream {
|
||||
@Override
|
||||
public long skip(long n) throws IOException {
|
||||
long skipped = super.skip(n);
|
||||
if (skipped > 0) {
|
||||
addAndCheckPos(skipped);
|
||||
if (skipped != 0) {
|
||||
currentPos += skipped;
|
||||
checkPos();
|
||||
}
|
||||
return skipped;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mark(int readLimit) {
|
||||
super.mark(readLimit);
|
||||
markPos = currentPos;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() throws IOException {
|
||||
super.reset();
|
||||
currentPos = markPos;
|
||||
}
|
||||
}
|
||||
@@ -4,52 +4,63 @@ import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Enumeration;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.JadxDecompiler;
|
||||
import jadx.api.plugins.JadxPluginContext;
|
||||
import jadx.core.utils.Utils;
|
||||
import jadx.zip.IZipEntry;
|
||||
import jadx.zip.ZipReader;
|
||||
import jadx.zip.io.LimitedInputStream;
|
||||
import jadx.zip.security.DisabledZipSecurity;
|
||||
import jadx.zip.security.IJadxZipSecurity;
|
||||
import jadx.zip.security.JadxZipSecurity;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
|
||||
/**
|
||||
* Deprecated, migrate to {@link ZipReader}. <br>
|
||||
* Prefer already configured instance from {@link JadxDecompiler#getZipReader()} or
|
||||
* {@link JadxPluginContext#getZipReader()}.
|
||||
*/
|
||||
@Deprecated
|
||||
public class ZipSecurity {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ZipSecurity.class);
|
||||
|
||||
private static final boolean DISABLE_CHECKS = Utils.getEnvVarBool("JADX_DISABLE_ZIP_SECURITY", false);
|
||||
|
||||
/**
|
||||
* size of uncompressed zip entry shouldn't be bigger of compressed in
|
||||
* {@link #ZIP_BOMB_DETECTION_FACTOR} times
|
||||
*/
|
||||
private static final int ZIP_BOMB_DETECTION_FACTOR = 100;
|
||||
|
||||
/**
|
||||
* Zip entries that have an uncompressed size of less than {@link #ZIP_BOMB_MIN_UNCOMPRESSED_SIZE}
|
||||
* are considered safe
|
||||
*/
|
||||
private static final int ZIP_BOMB_MIN_UNCOMPRESSED_SIZE = 25 * 1024 * 1024;
|
||||
|
||||
private static final int MAX_ENTRIES_COUNT = Utils.getEnvVarInt("JADX_ZIP_MAX_ENTRIES_COUNT", 100_000);
|
||||
|
||||
private static final IJadxZipSecurity ZIP_SECURITY = buildZipSecurity();
|
||||
|
||||
private static final ZipReader ZIP_READER = new ZipReader(ZIP_SECURITY);
|
||||
|
||||
private static IJadxZipSecurity buildZipSecurity() {
|
||||
if (DISABLE_CHECKS) {
|
||||
return DisabledZipSecurity.INSTANCE;
|
||||
}
|
||||
JadxZipSecurity jadxZipSecurity = new JadxZipSecurity();
|
||||
jadxZipSecurity.setMaxEntriesCount(MAX_ENTRIES_COUNT);
|
||||
return jadxZipSecurity;
|
||||
}
|
||||
|
||||
private ZipSecurity() {
|
||||
}
|
||||
|
||||
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 static boolean isInSubDirectory(File baseDir, File file) {
|
||||
return ZIP_SECURITY.isInSubDirectory(baseDir, file);
|
||||
if (DISABLE_CHECKS) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
return isInSubDirectoryInternal(baseDir.getCanonicalFile(), file.getCanonicalFile());
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,15 +68,48 @@ public class ZipSecurity {
|
||||
* to limit output only to the specified directory
|
||||
*/
|
||||
public static boolean isValidZipEntryName(String entryName) {
|
||||
return ZIP_SECURITY.isValidEntryName(entryName);
|
||||
if (DISABLE_CHECKS) {
|
||||
return true;
|
||||
}
|
||||
if (entryName.contains("..")) { // quick pre-check
|
||||
if (entryName.contains("../") || entryName.contains("..\\")) {
|
||||
LOG.error("Path traversal attack detected in entry: '{}'", entryName);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
try {
|
||||
File currentPath = CommonFileUtils.CWD;
|
||||
File canonical = new File(currentPath, entryName).getCanonicalFile();
|
||||
if (isInSubDirectoryInternal(currentPath, canonical)) {
|
||||
return true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// check failed
|
||||
}
|
||||
LOG.error("Invalid file name or path traversal attack detected: {}", entryName);
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean isZipBomb(IZipEntry entry) {
|
||||
return !ZIP_SECURITY.isValidEntry(entry);
|
||||
public static boolean isZipBomb(ZipEntry entry) {
|
||||
if (DISABLE_CHECKS) {
|
||||
return false;
|
||||
}
|
||||
long compressedSize = entry.getCompressedSize();
|
||||
long uncompressedSize = entry.getSize();
|
||||
boolean invalidSize = (compressedSize < 0) || (uncompressedSize < 0);
|
||||
boolean possibleZipBomb = (uncompressedSize >= ZIP_BOMB_MIN_UNCOMPRESSED_SIZE)
|
||||
&& (compressedSize * ZIP_BOMB_DETECTION_FACTOR < uncompressedSize);
|
||||
if (invalidSize || possibleZipBomb) {
|
||||
LOG.error("Potential zip bomb attack detected, invalid sizes: compressed {}, uncompressed {}, name {}",
|
||||
compressedSize, uncompressedSize, entry.getName());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean isValidZipEntry(IZipEntry entry) {
|
||||
return ZIP_SECURITY.isValidEntry(entry);
|
||||
public static boolean isValidZipEntry(ZipEntry entry) {
|
||||
return isValidZipEntryName(entry.getName())
|
||||
&& !isZipBomb(entry);
|
||||
}
|
||||
|
||||
public static InputStream getInputStreamForEntry(ZipFile zipFile, ZipEntry entry) throws IOException {
|
||||
@@ -78,15 +122,44 @@ public class ZipSecurity {
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit valid entries in a zip file.
|
||||
* Visit valid entries in zip file.
|
||||
* Return not null value from visitor to stop iteration.
|
||||
*/
|
||||
@Nullable
|
||||
public static <R> R visitZipEntries(File file, Function<IZipEntry, R> visitor) {
|
||||
return ZIP_READER.visitEntries(file, visitor);
|
||||
public static <R> R visitZipEntries(File file, BiFunction<ZipFile, ZipEntry, R> visitor) {
|
||||
try (ZipFile zip = new ZipFile(file)) {
|
||||
Enumeration<? extends ZipEntry> entries = zip.entries();
|
||||
int entriesProcessed = 0;
|
||||
while (entries.hasMoreElements()) {
|
||||
ZipEntry entry = entries.nextElement();
|
||||
if (isValidZipEntry(entry)) {
|
||||
R result = visitor.apply(zip, entry);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
entriesProcessed++;
|
||||
if (!DISABLE_CHECKS && entriesProcessed > MAX_ENTRIES_COUNT) {
|
||||
throw new JadxRuntimeException("Zip entries count limit exceeded: " + MAX_ENTRIES_COUNT
|
||||
+ ", last entry: " + entry.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new JadxRuntimeException("Failed to process zip file: " + file.getAbsolutePath(), e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void readZipEntries(File file, BiConsumer<IZipEntry, InputStream> visitor) {
|
||||
ZIP_READER.readEntries(file, visitor);
|
||||
public static void readZipEntries(File file, BiConsumer<ZipEntry, InputStream> visitor) {
|
||||
visitZipEntries(file, (zip, entry) -> {
|
||||
if (!entry.isDirectory()) {
|
||||
try (InputStream in = getInputStreamForEntry(zip, entry)) {
|
||||
visitor.accept(entry, in);
|
||||
} catch (Exception e) {
|
||||
throw new JadxRuntimeException("Failed to process zip entry: " + entry.getName());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package jadx.api.security;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
import org.w3c.dom.Document;
|
||||
|
||||
import jadx.zip.security.IJadxZipSecurity;
|
||||
|
||||
public interface IJadxSecurity extends IJadxZipSecurity {
|
||||
|
||||
/**
|
||||
* Check if application package is safe
|
||||
*
|
||||
* @return normalized/sanitized string or same string if safe
|
||||
*/
|
||||
String verifyAppPackage(String appPackage);
|
||||
|
||||
/**
|
||||
* XML document parser
|
||||
*/
|
||||
Document parseXml(InputStream in);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package jadx.api.security;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.Set;
|
||||
|
||||
public enum JadxSecurityFlag {
|
||||
|
||||
VERIFY_APP_PACKAGE,
|
||||
SECURE_XML_PARSER,
|
||||
SECURE_ZIP_READER;
|
||||
|
||||
public static Set<JadxSecurityFlag> all() {
|
||||
return EnumSet.allOf(JadxSecurityFlag.class);
|
||||
}
|
||||
|
||||
public static Set<JadxSecurityFlag> none() {
|
||||
return EnumSet.noneOf(JadxSecurityFlag.class);
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
package jadx.api.security.impl;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.w3c.dom.Document;
|
||||
|
||||
import jadx.api.security.IJadxSecurity;
|
||||
import jadx.api.security.JadxSecurityFlag;
|
||||
import jadx.core.deobf.NameMapper;
|
||||
import jadx.zip.IZipEntry;
|
||||
import jadx.zip.security.DisabledZipSecurity;
|
||||
import jadx.zip.security.IJadxZipSecurity;
|
||||
import jadx.zip.security.JadxZipSecurity;
|
||||
|
||||
import static jadx.api.security.JadxSecurityFlag.SECURE_ZIP_READER;
|
||||
|
||||
public class JadxSecurity implements IJadxSecurity {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(JadxSecurity.class);
|
||||
|
||||
private final Set<JadxSecurityFlag> flags;
|
||||
private final IJadxZipSecurity zipSecurity;
|
||||
|
||||
public JadxSecurity(Set<JadxSecurityFlag> flags) {
|
||||
this.flags = flags;
|
||||
this.zipSecurity = flags.contains(SECURE_ZIP_READER) ? new JadxZipSecurity() : DisabledZipSecurity.INSTANCE;
|
||||
}
|
||||
|
||||
public JadxSecurity(Set<JadxSecurityFlag> flags, IJadxZipSecurity zipSecurity) {
|
||||
this.flags = flags;
|
||||
this.zipSecurity = zipSecurity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidEntry(IZipEntry entry) {
|
||||
return zipSecurity.isValidEntry(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidEntryName(String entryName) {
|
||||
return zipSecurity.isValidEntryName(entryName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInSubDirectory(File baseDir, File file) {
|
||||
return zipSecurity.isInSubDirectory(baseDir, file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useLimitedDataStream() {
|
||||
return zipSecurity.useLimitedDataStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxEntriesCount() {
|
||||
return zipSecurity.getMaxEntriesCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String verifyAppPackage(String appPackage) {
|
||||
if (flags.contains(JadxSecurityFlag.VERIFY_APP_PACKAGE)
|
||||
&& !NameMapper.isValidFullIdentifier(appPackage)) {
|
||||
LOG.warn("App package '{}' has invalid format and will be ignored", appPackage);
|
||||
return "INVALID_PACKAGE";
|
||||
}
|
||||
return appPackage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Document parseXml(InputStream in) {
|
||||
DocumentBuilderFactory dbf;
|
||||
if (flags.contains(JadxSecurityFlag.SECURE_XML_PARSER)) {
|
||||
dbf = SecureDBFHolder.INSTANCE;
|
||||
} else {
|
||||
dbf = SimpleDBFHolder.INSTANCE;
|
||||
}
|
||||
try {
|
||||
return dbf.newDocumentBuilder().parse(in);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to parse xml", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SimpleDBFHolder {
|
||||
private static final DocumentBuilderFactory INSTANCE = DocumentBuilderFactory.newInstance();
|
||||
}
|
||||
|
||||
private static final class SecureDBFHolder {
|
||||
private static final DocumentBuilderFactory INSTANCE = buildSecureDBF();
|
||||
|
||||
private static DocumentBuilderFactory buildSecureDBF() {
|
||||
try {
|
||||
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
||||
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
|
||||
dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
|
||||
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
|
||||
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
|
||||
dbf.setFeature("http://apache.org/xml/features/dom/create-entity-ref-nodes", false);
|
||||
dbf.setXIncludeAware(false);
|
||||
dbf.setExpandEntityReferences(false);
|
||||
return dbf;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Fail to build secure XML DocumentBuilderFactory", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,5 @@
|
||||
package jadx.api.utils;
|
||||
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
import jadx.api.ICodeInfo;
|
||||
import jadx.api.metadata.ICodeAnnotation;
|
||||
import jadx.api.metadata.ICodeNodeRef;
|
||||
import jadx.api.metadata.annotations.NodeDeclareRef;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
|
||||
public class CodeUtils {
|
||||
|
||||
public static String getLineForPos(String code, int pos) {
|
||||
@@ -55,71 +47,4 @@ public class CodeUtils {
|
||||
line++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cut method code (including comments and annotations) from class code.
|
||||
*
|
||||
* @return method code or empty string if metadata is not available
|
||||
*/
|
||||
public static String extractMethodCode(MethodNode mth, ICodeInfo codeInfo) {
|
||||
int end = getMethodEnd(mth, codeInfo);
|
||||
if (end == -1) {
|
||||
return "";
|
||||
}
|
||||
int start = getMethodStart(mth, codeInfo);
|
||||
if (end < start) {
|
||||
return "";
|
||||
}
|
||||
return codeInfo.getCodeStr().substring(start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search first empty line before method definition to include comments and annotations
|
||||
*/
|
||||
private static int getMethodStart(MethodNode mth, ICodeInfo codeInfo) {
|
||||
int pos = mth.getDefPosition();
|
||||
String newLineStr = mth.root().getArgs().getCodeNewLineStr();
|
||||
String emptyLine = newLineStr + newLineStr;
|
||||
int emptyLinePos = codeInfo.getCodeStr().lastIndexOf(emptyLine, pos);
|
||||
return emptyLinePos == -1 ? pos : emptyLinePos + emptyLine.length();
|
||||
}
|
||||
|
||||
/**
|
||||
* Search method end position in provided class code info.
|
||||
*
|
||||
* @return end pos or -1 if metadata not available
|
||||
*/
|
||||
public static int getMethodEnd(MethodNode mth, ICodeInfo codeInfo) {
|
||||
if (!codeInfo.hasMetadata()) {
|
||||
return -1;
|
||||
}
|
||||
// skip nested nodes DEF/END until first unpaired END annotation (end of this method)
|
||||
Integer end = codeInfo.getCodeMetadata().searchDown(mth.getDefPosition() + 1, new BiFunction<>() {
|
||||
int nested = 0;
|
||||
|
||||
@Override
|
||||
public Integer apply(Integer pos, ICodeAnnotation ann) {
|
||||
switch (ann.getAnnType()) {
|
||||
case DECLARATION:
|
||||
ICodeNodeRef node = ((NodeDeclareRef) ann).getNode();
|
||||
switch (node.getAnnType()) {
|
||||
case CLASS:
|
||||
case METHOD:
|
||||
nested++;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case END:
|
||||
if (nested == 0) {
|
||||
return pos;
|
||||
}
|
||||
nested--;
|
||||
break;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
return end == null ? -1 : end;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,15 +10,13 @@ public class Consts {
|
||||
public static final boolean DEBUG_FINALLY = false;
|
||||
public static final boolean DEBUG_ATTRIBUTES = false;
|
||||
public static final boolean DEBUG_RESTRUCTURE = false;
|
||||
public static final boolean DEBUG_EVENTS = Jadx.isDevVersion();
|
||||
public static final boolean DEBUG_EVENTS = true;
|
||||
|
||||
public static final String CLASS_OBJECT = "java.lang.Object";
|
||||
public static final String CLASS_STRING = "java.lang.String";
|
||||
public static final String CLASS_CLASS = "java.lang.Class";
|
||||
public static final String CLASS_THROWABLE = "java.lang.Throwable";
|
||||
public static final String CLASS_ERROR = "java.lang.Error";
|
||||
public static final String CLASS_EXCEPTION = "java.lang.Exception";
|
||||
public static final String CLASS_RUNTIME_EXCEPTION = "java.lang.RuntimeException";
|
||||
public static final String CLASS_ENUM = "java.lang.Enum";
|
||||
|
||||
public static final String CLASS_STRING_BUILDER = "java.lang.StringBuilder";
|
||||
|
||||
@@ -13,10 +13,10 @@ import org.slf4j.LoggerFactory;
|
||||
import jadx.api.CommentsLevel;
|
||||
import jadx.api.JadxArgs;
|
||||
import jadx.core.deobf.DeobfuscatorVisitor;
|
||||
import jadx.core.deobf.InitRenameProviders;
|
||||
import jadx.core.deobf.SaveDeobfMapping;
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.visitors.AnonymousClassVisitor;
|
||||
import jadx.core.dex.visitors.ApplyVariableNames;
|
||||
import jadx.core.dex.visitors.AttachCommentsVisitor;
|
||||
import jadx.core.dex.visitors.AttachMethodDetails;
|
||||
import jadx.core.dex.visitors.AttachTryCatchVisitor;
|
||||
@@ -29,6 +29,7 @@ import jadx.core.dex.visitors.DotGraphVisitor;
|
||||
import jadx.core.dex.visitors.EnumVisitor;
|
||||
import jadx.core.dex.visitors.ExtractFieldInit;
|
||||
import jadx.core.dex.visitors.FallbackModeVisitor;
|
||||
import jadx.core.dex.visitors.FixAccessModifiers;
|
||||
import jadx.core.dex.visitors.FixSwitchOverEnum;
|
||||
import jadx.core.dex.visitors.GenericTypesVisitor;
|
||||
import jadx.core.dex.visitors.IDexTreeVisitor;
|
||||
@@ -36,7 +37,6 @@ import jadx.core.dex.visitors.InitCodeVariables;
|
||||
import jadx.core.dex.visitors.InlineMethods;
|
||||
import jadx.core.dex.visitors.MarkMethodsForInline;
|
||||
import jadx.core.dex.visitors.MethodInvokeVisitor;
|
||||
import jadx.core.dex.visitors.MethodThrowsVisitor;
|
||||
import jadx.core.dex.visitors.MethodVisitor;
|
||||
import jadx.core.dex.visitors.ModVisitor;
|
||||
import jadx.core.dex.visitors.MoveInlineVisitor;
|
||||
@@ -49,14 +49,11 @@ import jadx.core.dex.visitors.ReplaceNewArray;
|
||||
import jadx.core.dex.visitors.ShadowFieldVisitor;
|
||||
import jadx.core.dex.visitors.SignatureProcessor;
|
||||
import jadx.core.dex.visitors.SimplifyVisitor;
|
||||
import jadx.core.dex.visitors.blocks.BlockFinisher;
|
||||
import jadx.core.dex.visitors.blocks.BlockProcessor;
|
||||
import jadx.core.dex.visitors.blocks.BlockSplitter;
|
||||
import jadx.core.dex.visitors.debuginfo.DebugInfoApplyVisitor;
|
||||
import jadx.core.dex.visitors.debuginfo.DebugInfoAttachVisitor;
|
||||
import jadx.core.dex.visitors.finaly.MarkFinallyVisitor;
|
||||
import jadx.core.dex.visitors.fixaccessmodifiers.FixAccessModifiers;
|
||||
import jadx.core.dex.visitors.gradle.NonFinalResIdsVisitor;
|
||||
import jadx.core.dex.visitors.kotlin.ProcessKotlinInternals;
|
||||
import jadx.core.dex.visitors.prepare.AddAndroidConstants;
|
||||
import jadx.core.dex.visitors.prepare.CollectConstValues;
|
||||
@@ -66,7 +63,6 @@ import jadx.core.dex.visitors.regions.IfRegionVisitor;
|
||||
import jadx.core.dex.visitors.regions.LoopRegionVisitor;
|
||||
import jadx.core.dex.visitors.regions.RegionMakerVisitor;
|
||||
import jadx.core.dex.visitors.regions.ReturnVisitor;
|
||||
import jadx.core.dex.visitors.regions.SwitchOverStringVisitor;
|
||||
import jadx.core.dex.visitors.regions.variables.ProcessVariables;
|
||||
import jadx.core.dex.visitors.rename.CodeRenameVisitor;
|
||||
import jadx.core.dex.visitors.rename.RenameVisitor;
|
||||
@@ -104,15 +100,16 @@ public class Jadx {
|
||||
passes.add(new SignatureProcessor());
|
||||
passes.add(new OverrideMethodVisitor());
|
||||
passes.add(new AddAndroidConstants());
|
||||
passes.add(new CollectConstValues());
|
||||
|
||||
// rename and deobfuscation
|
||||
passes.add(new InitRenameProviders());
|
||||
passes.add(new DeobfuscatorVisitor());
|
||||
passes.add(new SourceFileRename());
|
||||
passes.add(new RenameVisitor());
|
||||
passes.add(new SaveDeobfMapping());
|
||||
|
||||
passes.add(new UsageInfoVisitor());
|
||||
passes.add(new CollectConstValues());
|
||||
passes.add(new ProcessAnonymous());
|
||||
passes.add(new ProcessMethodsForInline());
|
||||
return passes;
|
||||
@@ -135,7 +132,6 @@ public class Jadx {
|
||||
// blocks IR
|
||||
passes.add(new BlockSplitter());
|
||||
passes.add(new BlockProcessor());
|
||||
passes.add(new BlockFinisher());
|
||||
if (args.isRawCFGOutput()) {
|
||||
passes.add(DotGraphVisitor.dumpRaw());
|
||||
}
|
||||
@@ -176,14 +172,9 @@ public class Jadx {
|
||||
// regions IR
|
||||
passes.add(new RegionMakerVisitor());
|
||||
passes.add(new IfRegionVisitor());
|
||||
if (args.isRestoreSwitchOverString()) {
|
||||
passes.add(new SwitchOverStringVisitor());
|
||||
}
|
||||
passes.add(new ReturnVisitor());
|
||||
passes.add(new CleanRegions());
|
||||
|
||||
passes.add(new MethodThrowsVisitor());
|
||||
|
||||
passes.add(new CodeShrinkVisitor());
|
||||
passes.add(new MethodInvokeVisitor());
|
||||
passes.add(new SimplifyVisitor());
|
||||
@@ -191,7 +182,6 @@ public class Jadx {
|
||||
|
||||
passes.add(new EnumVisitor());
|
||||
passes.add(new FixSwitchOverEnum());
|
||||
passes.add(new NonFinalResIdsVisitor());
|
||||
passes.add(new ExtractFieldInit());
|
||||
passes.add(new FixAccessModifiers());
|
||||
passes.add(new ClassModifier());
|
||||
@@ -201,7 +191,6 @@ public class Jadx {
|
||||
passes.add(new MarkMethodsForInline());
|
||||
}
|
||||
passes.add(new ProcessVariables());
|
||||
passes.add(new ApplyVariableNames());
|
||||
passes.add(new PrepareForCodeGen());
|
||||
if (args.isCfgOutput()) {
|
||||
passes.add(DotGraphVisitor.dumpRegions());
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.ICodeInfo;
|
||||
import jadx.api.JadxArgs;
|
||||
import jadx.api.impl.SimpleCodeInfo;
|
||||
import jadx.core.codegen.CodeGen;
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
@@ -31,8 +32,8 @@ public class ProcessClass {
|
||||
|
||||
private final List<IDexTreeVisitor> passes;
|
||||
|
||||
public ProcessClass(List<IDexTreeVisitor> passesList) {
|
||||
this.passes = passesList;
|
||||
public ProcessClass(JadxArgs args) {
|
||||
this.passes = Jadx.getPassesList(args);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -124,33 +125,6 @@ public class ProcessClass {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and process class without its deps
|
||||
*/
|
||||
public void forceProcess(ClassNode cls) {
|
||||
ClassNode topParentClass = cls.getTopParentClass();
|
||||
if (topParentClass != cls) {
|
||||
forceProcess(topParentClass);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
process(cls, false);
|
||||
} catch (Throwable e) {
|
||||
throw new JadxRuntimeException("Failed to process class: " + cls.getFullName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate code for class without processing its deps
|
||||
*/
|
||||
public @Nullable ICodeInfo forceGenerateCode(ClassNode cls) {
|
||||
try {
|
||||
return process(cls, true);
|
||||
} catch (Throwable e) {
|
||||
throw new JadxRuntimeException("Failed to generate code for class: " + cls.getFullName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public void initPasses(RootNode root) {
|
||||
for (IDexTreeVisitor pass : passes) {
|
||||
try {
|
||||
|
||||
@@ -24,7 +24,7 @@ public class ClspClass {
|
||||
private Map<String, ClspMethod> methodsMap = Collections.emptyMap();
|
||||
private List<ArgType> typeParameters = Collections.emptyList();
|
||||
|
||||
private final ClspClassSource source;
|
||||
private ClspClassSource source;
|
||||
|
||||
public ClspClass(ArgType clsType, int id, int accFlags, ClspClassSource source) {
|
||||
this.clsType = clsType;
|
||||
|
||||
@@ -347,10 +347,6 @@ public class ClassGen {
|
||||
* Additional checks for inlined methods
|
||||
*/
|
||||
private boolean skipMethod(MethodNode mth) {
|
||||
if (cls.root().getArgs().getDecompilationMode().isSpecial()) {
|
||||
// show all methods for special decompilation modes
|
||||
return false;
|
||||
}
|
||||
MethodInlineAttr inlineAttr = mth.get(AType.METHOD_INLINE);
|
||||
if (inlineAttr == null || inlineAttr.notNeeded()) {
|
||||
return false;
|
||||
@@ -656,13 +652,6 @@ public class ClassGen {
|
||||
code.add(clsName);
|
||||
}
|
||||
|
||||
public void addClsShortNameForced(ICodeWriter code, ClassInfo classInfo) {
|
||||
code.add(classInfo.getAliasShortName());
|
||||
if (!isBothClassesInOneTopClass(cls.getClassInfo(), classInfo)) {
|
||||
addImport(classInfo);
|
||||
}
|
||||
}
|
||||
|
||||
private String useClassInternal(ClassInfo useCls, ClassInfo extClsInfo) {
|
||||
String fullName = extClsInfo.getAliasFullName();
|
||||
if (fallback || !useImports) {
|
||||
|
||||
@@ -434,7 +434,7 @@ public class InsnGen {
|
||||
code.add(']');
|
||||
}
|
||||
int dim = arrayType.getArrayDimension();
|
||||
for (; k < dim; k++) {
|
||||
for (; k < dim - 1; k++) {
|
||||
code.add("[]");
|
||||
}
|
||||
break;
|
||||
@@ -749,7 +749,6 @@ public class InsnGen {
|
||||
code.attachAnnotation(refMth);
|
||||
code.add("this");
|
||||
} else {
|
||||
boolean forceShortName = addOuterClassInstance(insn, code, callMth);
|
||||
code.add("new ");
|
||||
if (refMth == null || refMth.contains(AFlag.DONT_GENERATE)) {
|
||||
// use class reference if constructor method is missing (default constructor)
|
||||
@@ -757,11 +756,7 @@ public class InsnGen {
|
||||
} else {
|
||||
code.attachAnnotation(refMth);
|
||||
}
|
||||
if (forceShortName) {
|
||||
mgen.getClassGen().addClsShortNameForced(code, insn.getClassType());
|
||||
} else {
|
||||
mgen.getClassGen().addClsName(code, insn.getClassType());
|
||||
}
|
||||
mgen.getClassGen().addClsName(code, insn.getClassType());
|
||||
GenericInfoAttr genericInfoAttr = insn.get(AType.GENERIC_INFO);
|
||||
if (genericInfoAttr != null) {
|
||||
code.add('<');
|
||||
@@ -782,27 +777,6 @@ public class InsnGen {
|
||||
generateMethodArguments(code, insn, 0, callMth);
|
||||
}
|
||||
|
||||
private boolean addOuterClassInstance(ConstructorInsn insn, ICodeWriter code, MethodNode callMth) throws CodegenException {
|
||||
if (callMth == null || !callMth.contains(AFlag.SKIP_FIRST_ARG)) {
|
||||
return false;
|
||||
}
|
||||
ClassNode ctrCls = callMth.getDeclaringClass();
|
||||
if (!ctrCls.isInner() || insn.getArgsCount() == 0) {
|
||||
return false;
|
||||
}
|
||||
InsnArg instArg = insn.getArg(0);
|
||||
if (instArg.isThis()) {
|
||||
return false;
|
||||
}
|
||||
// instance arg should be of an outer class type
|
||||
if (!instArg.getType().equals(ctrCls.getDeclaringClass().getType())) {
|
||||
return false;
|
||||
}
|
||||
addArgDot(code, instArg);
|
||||
// can't use another dot, force short name of class
|
||||
return true;
|
||||
}
|
||||
|
||||
private void inlineAnonymousConstructor(ICodeWriter code, ClassNode cls, ConstructorInsn insn) throws CodegenException {
|
||||
cls.ensureProcessed();
|
||||
if (this.mth.getParentClass() == cls) {
|
||||
|
||||
@@ -81,9 +81,8 @@ public class MethodGen {
|
||||
|
||||
public boolean addDefinition(ICodeWriter code) {
|
||||
if (mth.getMethodInfo().isClassInit()) {
|
||||
code.startLine();
|
||||
code.attachDefinition(mth);
|
||||
code.add("static");
|
||||
code.startLine("static");
|
||||
return true;
|
||||
}
|
||||
if (mth.contains(AFlag.ANONYMOUS_CONSTRUCTOR)) {
|
||||
@@ -270,7 +269,7 @@ public class MethodGen {
|
||||
JadxArgs args = mth.root().getArgs();
|
||||
switch (args.getDecompilationMode()) {
|
||||
case AUTO:
|
||||
if (classGen.isFallbackMode() || mth.getRegion() == null) {
|
||||
if (classGen.isFallbackMode()) {
|
||||
// TODO: try simple mode first
|
||||
dumpInstructions(code);
|
||||
} else {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user