Compare commits

..

4 Commits

Author SHA1 Message Date
Skylot 5640f3b931 fix test 2024-06-02 18:21:21 +01:00
Skylot 02bc27e887 build exclude list in init method to improve performance, add default values migration 2024-06-02 18:21:04 +01:00
bagipro 794e5adb7f Change deobf-min default to 2 2024-05-09 18:22:02 +07:00
bagipro a81cec7701 Improve deobf whitelist 2024-05-08 16:16:08 +07:00
1058 changed files with 21336 additions and 47019 deletions
+15 -21
View File
@@ -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
+5 -9
View File
@@ -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
-92
View File
@@ -1,92 +0,0 @@
name: Release
on:
push:
tags:
- "v*.*.*"
# additional permissions for provided GitHub token to create new release
permissions:
contents: write
jobs:
build-release-win-bundle:
runs-on: windows-latest
steps:
- uses: actions/checkout@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
-3
View File
@@ -21,7 +21,6 @@ build/
classes/
idea/
.gradle/
.kotlin/
node_modules/
.vscode/
@@ -29,10 +28,8 @@ jadx-output/
*-tmp/
**/tmp/
*.jobf
*.jadx
*.class
*.jar
*.dump
*.log
*.cfg
+12 -2
View File
@@ -8,7 +8,17 @@ before_script:
stages:
- test
build-test:
java-11:
stage: test
image: eclipse-temurin:11
script: ./gradlew clean build dist copyExe
java-17:
stage: test
image: eclipse-temurin:17
script: ./gradlew clean build dist copyExe
java-21:
stage: test
image: eclipse-temurin:21
script: JADX_BUILD_JAVA_VERSION=11 JADX_TEST_JAVA_VERSION=11 ./gradlew clean build dist distWin
script: ./gradlew clean build dist copyExe
+1 -1
View File
@@ -4,7 +4,7 @@
<module name="jadx.jadx-gui.main"/>
<option name="PROGRAM_PARAMETERS" value="-v"/>
<option name="VM_PARAMETERS"
value="-Xms128M -XX:MaxRAMPercentage=70.0 -Dawt.useSystemAAFontSettings=lcd -Dswing.aatext=true -Djava.util.Arrays.useLegacyMergeSort=true -Djdk.util.zip.disableZip64ExtraFieldValidation=true -XX:+IgnoreUnrecognizedVMOptions --add-opens=java.base/java.lang=ALL-UNNAMED --enable-native-access=ALL-UNNAMED -Dsun.java2d.noddraw=true -Dsun.java2d.d3d=false -Dsun.java2d.ddforcevram=true -Dsun.java2d.ddblit=false -Dswing.useflipBufferStrategy=true"/>
value="-Xms128M -XX:MaxRAMPercentage=70.0 -Dawt.useSystemAAFontSettings=lcd -Dswing.aatext=true -Djava.util.Arrays.useLegacyMergeSort=true -Djdk.util.zip.disableZip64ExtraFieldValidation=true -XX:+IgnoreUnrecognizedVMOptions --add-opens=java.base/java.lang=ALL-UNNAMED"/>
<method v="2">
<option name="Make" enabled="true"/>
</method>
+23 -10
View File
@@ -3,7 +3,7 @@
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to make participation in our project and
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
@@ -23,13 +23,13 @@ include:
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
professional setting
## Our Responsibilities
@@ -45,12 +45,25 @@ threatening, offensive, or harmful.
## Scope
This Code of Conduct applies within all project spaces, and it also applies when
an individual is representing the project or its community in public spaces.
Examples of representing a project or community include using an official
project e-mail address, posting via an official social media account, or acting
as an appointed representative at an online or offline event. Representation of
a project may be further defined and clarified by project maintainers.
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at skylot@gmail.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
+88 -121
View File
@@ -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
View File
@@ -2,7 +2,6 @@
## Reporting a Vulnerability
To report a security issue, please open a [new security advisory](https://github.com/skylot/jadx/security/advisories/new).
Please fill the steps you took to create the issue, affected versions, and, if known, mitigations for the issue.
We will check and respond within 3 working days.
If the issue is confirmed as a vulnerability, we will apply required mitigations at the next release.
To report a security issue, please email `skylot@gmail.com` with a description of the issue, the steps you took to create the issue, affected versions, and, if known, mitigations for the issue.
We will check and respond within 3 working days. If the issue is confirmed as a vulnerability, we will apply required mitigations at the next release.
This project follows a 90 day disclosure timeline.
+25 -30
View File
@@ -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)
+1 -3
View File
@@ -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 {
+8 -15
View File
@@ -3,27 +3,26 @@ import org.gradle.api.tasks.testing.logging.TestExceptionFormat
plugins {
java
checkstyle
id("jadx-rewrite")
}
val jadxVersion: String by rootProject.extra
val jadxBuildJavaVersion: Int? by rootProject.extra
group = "io.github.skylot"
version = jadxVersion
dependencies {
implementation("org.slf4j:slf4j-api:2.0.17")
compileOnly("org.jetbrains:annotations:26.0.2")
implementation("org.slf4j:slf4j-api:2.0.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")
}
}
+2 -9
View File
@@ -120,22 +120,15 @@
<module name="SuppressWarningsHolder"/>
<module name="IllegalType">
<property name="illegalClassNames" value="java.util.ArrayList, java.util.HashMap, java.util.HashSet,
java.util.LinkedHashMap, java.util.LinkedHashSet, java.util.TreeMap, java.util.TreeSet"/>
</module>
<module name="IllegalType"/>
<module name="IllegalImport">
<property name="illegalClasses" value="jadx.core.utils.DebugUtils"/>
<!-- don't use nullable annotations from RxJava -->
<property name="illegalClasses" value="io.reactivex.rxjava3.annotations.NonNull"/>
<property name="illegalClasses" value="io.reactivex.rxjava3.annotations.Nullable"/>
</module>
<module name="RegexpSinglelineJava">
<property name="id" value="printstacktrace"/>
<property name="format" value="\.printStackTrace\(\)"/>
<property name="ignoreComments" value="true"/>
<property name="message"
value="Using Throwable.printStackTrace() is forbidden. Use logger to print exception"/>
<property name="message" value="Using Throwable.printStackTrace() is forbidden. Use logger to print exception"/>
</module>
</module>
+4 -2
View File
@@ -1,7 +1,7 @@
/*
* Generated on 11/22/21, 8:58 PM
*/
package jadx.gui.ui.codearea;
package jadx.gui.ui.codeearea;
import java.io.*;
import javax.swing.text.Segment;
@@ -9,7 +9,7 @@ import javax.swing.text.Segment;
import org.fife.ui.rsyntaxtextarea.*;
/*
/**
* 用于Smali代码高亮
* MartinKay@qq.com
*/
@@ -173,6 +173,7 @@ import org.fife.ui.rsyntaxtextarea.*;
zzAtEOF = false;
}
%}
Letter = [A-Za-z]
@@ -677,3 +678,4 @@ FLAG_ARRAY = (":array_"{SimpleName})
\n { addToken(start,zzStartRead-1, Token.COMMENT_EOL); addNullToken(); return firstToken; }
<<EOF>> { addToken(start,zzStartRead-1, Token.COMMENT_EOL); addNullToken(); return firstToken; }
}
+13 -16
View File
@@ -57,12 +57,12 @@
private int yychar;
/**
* the number of characters from the last newline up to the start of the
* the number of characters from the last newline up to the start of the
* matched text
*/
private int yycolumn;
/**
/**
* zzAtBOL == true <=> the scanner is currently at the beginning of a line
*/
private boolean zzAtBOL = true;
@@ -102,9 +102,6 @@
zzLexicalState = newState;
}
public final int yystate() {
return zzLexicalState;
}
/**
* Returns the text matched by the current regular expression.
@@ -115,12 +112,12 @@
/**
* Returns the character at position <tt>pos</tt> from the
* matched text.
*
* Returns the character at position <tt>pos</tt> from the
* matched text.
*
* It is equivalent to yytext().charAt(pos), but faster
*
* @param pos the position of the character to fetch.
* @param pos the position of the character to fetch.
* A value from 0 to yylength()-1.
*
* @return the character at position pos
@@ -141,8 +138,8 @@
/**
* Reports an error that occured while scanning.
*
* In a wellformed scanner (no or only correct usage of
* yypushback(int) and a match-all fallback rule) this method
* In a wellformed scanner (no or only correct usage of
* yypushback(int) and a match-all fallback rule) this method
* will only be called with things that "Can't Possibly Happen".
* If this method is called, something is seriously wrong
* (e.g. a JFlex bug producing a faulty scanner etc.).
@@ -162,7 +159,7 @@
}
--- throws clause
}
}
/**
@@ -209,12 +206,12 @@
zzAction = -1;
zzCurrentPosL = zzCurrentPos = zzStartRead = zzMarkedPosL;
--- start admin (lexstate etc)
zzForAction: {
while (true) {
--- next input, line, col, char count, next transition, isFinal action
zzAction = zzState;
zzMarkedPosL = zzCurrentPosL;
@@ -229,11 +226,11 @@
--- char count update
--- actions
default:
default:
if (zzInput == YYEOF && zzStartRead == zzCurrentPos) {
zzAtEOF = true;
--- eofvalue
}
}
else {
--- no match
}
-10
View File
@@ -1,10 +0,0 @@
[Desktop Entry]
Name=JADX GUI
Comment=Dex to Java decompiler
Icon=jadx
Exec=jadx-gui %f
Terminal=false
Type=Application
Categories=Development;Java;
Keywords=Java;Decompiler;
StartupWMClass=jadx-gui-JadxGUI
-4
View File
@@ -2,10 +2,6 @@ org.gradle.warning.mode=all
org.gradle.parallel=true
org.gradle.caching=true
### Disable configuration cache for now: causing issues with spotless and version plugins
# org.gradle.configuration-cache=true
# org.gradle.configuration-cache.problems=warn
# Flags for google-java-format (optimize imports by spotless) for Java >= 16.
# Java < 9 will ignore unsupported flags (thanks to -XX:+IgnoreUnrecognizedVMOptions)
org.gradle.jvmargs=-XX:+IgnoreUnrecognizedVMOptions \
Binary file not shown.
+2 -2
View File
@@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=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
Vendored
+3 -5
View File
@@ -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
View File
@@ -13,8 +13,6 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
+3 -9
View File
@@ -1,16 +1,14 @@
plugins {
id("jadx-java")
id("jadx-library")
id("application")
// use shadow only for application scripts, jar will be copied from jadx-gui
id("com.gradleup.shadow") version "8.3.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));
}
}
+17 -35
View File
@@ -1,8 +1,5 @@
package jadx.cli;
import java.util.function.Consumer;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -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) {
+31 -117
View File
@@ -1,8 +1,8 @@
package jadx.cli;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
@@ -14,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);
}
}
+92 -45
View File
@@ -1,76 +1,123 @@
package jadx.cli;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.assertj.core.api.Condition;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.utils.files.FileUtils;
import static org.assertj.core.api.Assertions.assertThat;
public class TestInput extends BaseCliIntegrationTest {
@Test
public void testHelp() {
int result = execJadxCli(new String[] { "--help" });
assertThat(result).isEqualTo(0);
}
@Test
public void testApkInput() throws Exception {
int result = execJadxCli(buildArgs(List.of(), "samples/small.apk"));
assertThat(result).isEqualTo(0);
assertThat(collectAllFilesInDir(outputDir))
.describedAs("check output files")
.map(p -> p.getFileName().toString())
.haveExactly(2, new Condition<>(f -> f.endsWith(".java"), "java classes"))
.haveExactly(9, new Condition<>(f -> f.endsWith(".xml"), "xml resources"))
.haveExactly(1, new Condition<>(f -> f.equals("AndroidManifest.xml"), "manifest"))
.hasSize(12);
}
public class TestInput {
private static final Logger LOG = LoggerFactory.getLogger(TestInput.class);
@Test
public void testDexInput() throws Exception {
decompile("samples/hello.dex");
decompile("dex", "samples/hello.dex");
}
@Test
public void testSmaliInput() throws Exception {
decompile("samples/HelloWorld.smali");
decompile("smali", "samples/HelloWorld.smali");
}
@Test
public void testClassInput() throws Exception {
decompile("samples/HelloWorld.class");
decompile("class", "samples/HelloWorld.class");
}
@Test
public void testMultipleInput() throws Exception {
decompile("samples/hello.dex", "samples/HelloWorld.smali");
}
@Test
public void testFallbackMode() throws Exception {
int result = execJadxCli(buildArgs(List.of("-f"), "samples/hello.dex"));
assertThat(result).isEqualTo(0);
List<Path> files = collectJavaFilesInDir(outputDir);
assertThat(files).hasSize(1);
}
@Test
public void testSimpleMode() throws Exception {
int result = execJadxCli(buildArgs(List.of("--decompilation-mode", "simple"), "samples/hello.dex"));
assertThat(result).isEqualTo(0);
List<Path> files = collectJavaFilesInDir(outputDir);
assertThat(files).hasSize(1);
decompile("multi", "samples/hello.dex", "samples/HelloWorld.smali");
}
@Test
public void testResourceOnly() throws Exception {
int result = execJadxCli(buildArgs(List.of(), "samples/resources-only.apk"));
decode("resourceOnly", "samples/resources-only.apk");
}
private void decode(String tmpDirName, String apkSample) throws URISyntaxException, IOException {
List<String> args = new ArrayList<>();
Path tempDir = FileUtils.createTempDir(tmpDirName);
args.add("-v");
args.add("-d");
args.add(tempDir.toAbsolutePath().toString());
URL resource = getClass().getClassLoader().getResource(apkSample);
assertThat(resource).isNotNull();
String sampleFile = resource.toURI().getRawPath();
args.add(sampleFile);
int result = JadxCLI.execute(args.toArray(new String[0]));
assertThat(result).isEqualTo(0);
List<Path> files = collectFilesInDir(outputDir,
path -> path.getFileName().toString().equalsIgnoreCase("AndroidManifest.xml"));
assertThat(files).isNotEmpty();
List<Path> files = Files.find(
tempDir,
3,
(file, attr) -> file.getFileName().toString().equalsIgnoreCase("AndroidManifest.xml"))
.collect(Collectors.toList());
assertThat(files.isEmpty()).isFalse();
}
private void decompile(String tmpDirName, String... inputSamples) throws URISyntaxException, IOException {
List<String> args = new ArrayList<>();
Path tempDir = FileUtils.createTempDir(tmpDirName);
args.add("-v");
args.add("-d");
args.add(tempDir.toAbsolutePath().toString());
for (String inputSample : inputSamples) {
URL resource = getClass().getClassLoader().getResource(inputSample);
assertThat(resource).isNotNull();
String sampleFile = resource.toURI().getRawPath();
args.add(sampleFile);
}
int result = JadxCLI.execute(args.toArray(new String[0]));
assertThat(result).isEqualTo(0);
List<Path> resultJavaFiles = collectJavaFilesInDir(tempDir);
assertThat(resultJavaFiles).isNotEmpty();
// do not copy input files as resources
PathMatcher logAllFiles = path -> {
LOG.debug("File in result dir: {}", path);
return true;
};
for (Path path : collectFilesInDir(tempDir, logAllFiles)) {
for (String inputSample : inputSamples) {
assertThat(path.toAbsolutePath().toString()).doesNotContain(inputSample);
}
}
}
private static List<Path> collectJavaFilesInDir(Path dir) throws IOException {
PathMatcher javaMatcher = dir.getFileSystem().getPathMatcher("glob:**.java");
return collectFilesInDir(dir, javaMatcher);
}
private static List<Path> collectFilesInDir(Path dir, PathMatcher matcher) throws IOException {
try (Stream<Path> pathStream = Files.walk(dir)) {
return pathStream
.filter(p -> Files.isRegularFile(p, LinkOption.NOFOLLOW_LINKS))
.filter(matcher::matches)
.collect(Collectors.toList());
}
}
@AfterAll
public static void cleanup() {
FileUtils.clearTempRootDir();
}
}
@@ -1,17 +0,0 @@
package jadx.plugins.tools.utils;
import org.junit.jupiter.api.Test;
import static jadx.plugins.tools.utils.PluginUtils.extractVersion;
import static org.assertj.core.api.Assertions.assertThat;
class PluginUtilsTest {
@Test
public void testExtractVersion() {
assertThat(extractVersion("plugin-name-v1.2.3.jar")).isEqualTo("1.2.3");
assertThat(extractVersion("plugin-name-v1.2.jar")).isEqualTo("1.2");
assertThat(extractVersion("1.2.3.jar")).isEqualTo("1.2.3");
}
}
Binary file not shown.
Binary file not shown.
@@ -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);
}
}
}
-3
View File
@@ -1,3 +0,0 @@
## jadx zip
Custom zip reader implementation to fight tampering and provide additional security checks
-3
View File
@@ -1,3 +0,0 @@
plugins {
id("jadx-library")
}
@@ -1,36 +0,0 @@
package jadx.zip;
import java.io.File;
import java.io.InputStream;
public interface IZipEntry {
/**
* Zip entry name
*/
String getName();
/**
* Uncompressed bytes
*/
byte[] getBytes();
/**
* Stream of uncompressed bytes.
*/
InputStream getInputStream();
long getCompressedSize();
long getUncompressedSize();
boolean isDirectory();
File getZipFile();
/**
* Return true if {@link #getBytes()} method is more optimal to use other than
* {@link #getInputStream()}
*/
boolean preferBytes();
}
@@ -1,9 +0,0 @@
package jadx.zip;
import java.io.Closeable;
import java.io.IOException;
public interface IZipParser extends Closeable {
ZipContent open() throws IOException;
}
@@ -1,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 -30
View File
@@ -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
}
+11 -132
View File
@@ -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
*/
@@ -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";
+4 -15
View File
@@ -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