Compare commits
139 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 13e934ce4d | |||
| 7b54c3ae70 | |||
| b82791706a | |||
| e51a7fe417 | |||
| cf96fdec59 | |||
| 3374f9b64a | |||
| 6b54cde89c | |||
| 59b560b553 | |||
| 8b08ac3806 | |||
| d9d4180581 | |||
| 33f93ddc8a | |||
| bcd0c949dc | |||
| fc0f1f9a1c | |||
| abd64007e2 | |||
| fb02e32a6a | |||
| f33a2e4768 | |||
| 3d8e5e5851 | |||
| 646ee2d963 | |||
| b6f27d8a1a | |||
| 97d04edb01 | |||
| 2486c981a8 | |||
| b7a8a2879e | |||
| 092e897104 | |||
| a0a9f7fd41 | |||
| 00f0f5547b | |||
| 00608f8e51 | |||
| d0351a88ba | |||
| bee476895c | |||
| 580f25faae | |||
| 1d1ca7d0c0 | |||
| dbf4527ce6 | |||
| ec726d6ca1 | |||
| be1c02455f | |||
| aee1e86398 | |||
| c0d721bea1 | |||
| e19a456642 | |||
| acd57930cc | |||
| 73348e5183 | |||
| 32c92dd9a8 | |||
| 8bbdbfc563 | |||
| fd6cb2451b | |||
| 47647bbb9a | |||
| e31d697cd9 | |||
| a796d15289 | |||
| f56eb271a1 | |||
| fbebcb9845 | |||
| 79c91634ad | |||
| f6e12d71a0 | |||
| 62835fbade | |||
| 07c66b5c3c | |||
| e3aa49aaa9 | |||
| 9981949a2b | |||
| ea6492e5ba | |||
| 03d4cb134f | |||
| 37b0b09f25 | |||
| 1e75544636 | |||
| d4ce969fb7 | |||
| 9a692d6be4 | |||
| d3a8a43c74 | |||
| 518da3d8b5 | |||
| 61f5386fe5 | |||
| 20cb9c6a3b | |||
| 4a6784dc68 | |||
| bb6db25c9d | |||
| d0c496858e | |||
| b0ab702f9e | |||
| 5846e6d22e | |||
| 4daad2fc79 | |||
| cca6ca25d1 | |||
| dfa6a83f7c | |||
| d1a3935c9e | |||
| b4f1021885 | |||
| a163e5a1de | |||
| 6eeb303d73 | |||
| 3209dbb7a4 | |||
| d84f0389ec | |||
| 5d720dd29c | |||
| c9d650d186 | |||
| ce60aa8635 | |||
| 4a9276e904 | |||
| b78d3aa2f7 | |||
| 4644d1d8ac | |||
| 7b8fc01319 | |||
| ff66f95a8a | |||
| 4ef1f3b12b | |||
| 8873038b57 | |||
| bf58f03405 | |||
| acf1c8187e | |||
| 7bd1b14728 | |||
| 61f04d6b07 | |||
| 5d5bf325fe | |||
| afdd2d8b39 | |||
| b18604071a | |||
| 801890f0a8 | |||
| 3d36c93beb | |||
| 54fbbd9524 | |||
| 306547d499 | |||
| a43b475be7 | |||
| bc4bb0dc41 | |||
| ea5916452d | |||
| 19f3cdf501 | |||
| 45d320a596 | |||
| 6860a8be7e | |||
| 94915db739 | |||
| 29d114402d | |||
| 6889670b11 | |||
| fe7d527fcc | |||
| 84e211b809 | |||
| f4849d67cf | |||
| 60a8d8b9fd | |||
| 1449354c54 | |||
| 0df474f35a | |||
| de629544a3 | |||
| 7a2dad8ef2 | |||
| a46e35c15c | |||
| 73966fda89 | |||
| fe0ab5ebf8 | |||
| 8345edf76b | |||
| ff0fbba720 | |||
| fe41d6ed3d | |||
| ff95b9e999 | |||
| 87844d2193 | |||
| 2ac0cc62e6 | |||
| 2924bb259c | |||
| 17695babf4 | |||
| 0d105a5095 | |||
| 7eab3c8534 | |||
| 47f2e516e5 | |||
| fdad829c49 | |||
| 2fa7f84251 | |||
| 828cad3287 | |||
| cc0cb6a3d3 | |||
| a67dfcb7e1 | |||
| baa93bad63 | |||
| 45df80f036 | |||
| 792e0d6f3a | |||
| fe9d3bcab7 | |||
| 1e1036c049 | |||
| 60dcdc7096 |
@@ -61,7 +61,7 @@ jobs:
|
|||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: oracle-actions/setup-java@v1
|
uses: oracle-actions/setup-java@v1
|
||||||
with:
|
with:
|
||||||
release: 21
|
release: 24
|
||||||
|
|
||||||
- name: Print Java version
|
- name: Print Java version
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: oracle-actions/setup-java@v1
|
uses: oracle-actions/setup-java@v1
|
||||||
with:
|
with:
|
||||||
release: 23
|
release: 24
|
||||||
|
|
||||||
- name: Set jadx version
|
- name: Set jadx version
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v7
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ build/
|
|||||||
classes/
|
classes/
|
||||||
idea/
|
idea/
|
||||||
.gradle/
|
.gradle/
|
||||||
|
.kotlin/
|
||||||
node_modules/
|
node_modules/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ jadx-output/
|
|||||||
*.jadx
|
*.jadx
|
||||||
|
|
||||||
*.class
|
*.class
|
||||||
|
*.jar
|
||||||
*.dump
|
*.dump
|
||||||
*.log
|
*.log
|
||||||
*.cfg
|
*.cfg
|
||||||
|
|||||||
+2
-12
@@ -8,17 +8,7 @@ before_script:
|
|||||||
stages:
|
stages:
|
||||||
- test
|
- test
|
||||||
|
|
||||||
java-11:
|
build-test:
|
||||||
stage: test
|
|
||||||
image: eclipse-temurin:11
|
|
||||||
script: ./gradlew clean build dist distWin
|
|
||||||
|
|
||||||
java-17:
|
|
||||||
stage: test
|
|
||||||
image: eclipse-temurin:17
|
|
||||||
script: ./gradlew clean build dist distWin
|
|
||||||
|
|
||||||
java-21:
|
|
||||||
stage: test
|
stage: test
|
||||||
image: eclipse-temurin:21
|
image: eclipse-temurin:21
|
||||||
script: ./gradlew clean build dist distWin
|
script: JADX_BUILD_JAVA_VERSION=11 JADX_TEST_JAVA_VERSION=11 ./gradlew clean build dist distWin
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<module name="jadx.jadx-gui.main"/>
|
<module name="jadx.jadx-gui.main"/>
|
||||||
<option name="PROGRAM_PARAMETERS" value="-v"/>
|
<option name="PROGRAM_PARAMETERS" value="-v"/>
|
||||||
<option name="VM_PARAMETERS"
|
<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 -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 --enable-native-access=ALL-UNNAMED -Dsun.java2d.noddraw=true -Dsun.java2d.d3d=false -Dsun.java2d.ddforcevram=true -Dsun.java2d.ddblit=false -Dswing.useflipBufferStrategy=true"/>
|
||||||
<method v="2">
|
<method v="2">
|
||||||
<option name="Make" enabled="true"/>
|
<option name="Make" enabled="true"/>
|
||||||
</method>
|
</method>
|
||||||
|
|||||||
@@ -86,106 +86,115 @@ and also packed to `build/jadx-<version>.zip`
|
|||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
```
|
```
|
||||||
jadx[-gui] [command] [options] <input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk, .jadx.kts)
|
jadx[-gui] [command] [options] <input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk, .apkm, .jadx.kts)
|
||||||
commands (use '<command> --help' for command options):
|
commands (use '<command> --help' for command options):
|
||||||
plugins - manage jadx plugins
|
plugins - manage jadx plugins
|
||||||
|
|
||||||
options:
|
options:
|
||||||
-d, --output-dir - output directory
|
-d, --output-dir - output directory
|
||||||
-ds, --output-dir-src - output directory for sources
|
-ds, --output-dir-src - output directory for sources
|
||||||
-dr, --output-dir-res - output directory for resources
|
-dr, --output-dir-res - output directory for resources
|
||||||
-r, --no-res - do not decode resources
|
-r, --no-res - do not decode resources
|
||||||
-s, --no-src - do not decompile source code
|
-s, --no-src - do not decompile source code
|
||||||
--single-class - decompile a single class, full name, raw or alias
|
-j, --threads-count - processing threads count, default: 4
|
||||||
--single-class-output - file or dir for write if decompile a single class
|
--single-class - decompile a single class, full name, raw or alias
|
||||||
--output-format - can be 'java' or 'json', default: java
|
--single-class-output - file or dir for write if decompile a single class
|
||||||
-e, --export-gradle - save as android gradle project
|
--output-format - can be 'java' or 'json', default: java
|
||||||
-j, --threads-count - processing threads count, default: 4
|
-e, --export-gradle - save as gradle project (set '--export-gradle-type' to 'auto')
|
||||||
-m, --decompilation-mode - code output mode:
|
--export-gradle-type - Gradle project template for export:
|
||||||
'auto' - trying best options (default)
|
'auto' - detect automatically
|
||||||
'restructure' - restore code structure (normal java code)
|
'android-app' - Android Application (apk)
|
||||||
'simple' - simplified instructions (linear, with goto's)
|
'android-library' - Android Library (aar)
|
||||||
'fallback' - raw instructions without modifications
|
'simple-java' - simple Java
|
||||||
--show-bad-code - show inconsistent code (incorrectly decompiled)
|
-m, --decompilation-mode - code output mode:
|
||||||
--no-xml-pretty-print - do not prettify XML
|
'auto' - trying best options (default)
|
||||||
--no-imports - disable use of imports, always write entire package name
|
'restructure' - restore code structure (normal java code)
|
||||||
--no-debug-info - disable debug info parsing and processing
|
'simple' - simplified instructions (linear, with goto's)
|
||||||
--add-debug-lines - add comments with debug line numbers if available
|
'fallback' - raw instructions without modifications
|
||||||
--no-inline-anonymous - disable anonymous classes inline
|
--show-bad-code - show inconsistent code (incorrectly decompiled)
|
||||||
--no-inline-methods - disable methods inline
|
--no-xml-pretty-print - do not prettify XML
|
||||||
--no-move-inner-classes - disable move inner classes into parent
|
--no-imports - disable use of imports, always write entire package name
|
||||||
--no-inline-kotlin-lambda - disable inline for Kotlin lambdas
|
--no-debug-info - disable debug info parsing and processing
|
||||||
--no-finally - don't extract finally block
|
--add-debug-lines - add comments with debug line numbers if available
|
||||||
--no-restore-switch-over-string - don't restore switch over string
|
--no-inline-anonymous - disable anonymous classes inline
|
||||||
--no-replace-consts - don't replace constant value with matching constant field
|
--no-inline-methods - disable methods inline
|
||||||
--escape-unicode - escape non latin characters in strings (with \u)
|
--no-move-inner-classes - disable move inner classes into parent
|
||||||
--respect-bytecode-access-modifiers - don't change original access modifiers
|
--no-inline-kotlin-lambda - disable inline for Kotlin lambdas
|
||||||
--mappings-path - deobfuscation mappings file or directory. Allowed formats: Tiny and Tiny v2 (both '.tiny'), Enigma (.mapping) or Enigma directory
|
--no-finally - don't extract finally block
|
||||||
--mappings-mode - set mode for handling the deobfuscation mapping file:
|
--no-restore-switch-over-string - don't restore switch over string
|
||||||
'read' - just read, user can always save manually (default)
|
--no-replace-consts - don't replace constant value with matching constant field
|
||||||
'read-and-autosave-every-change' - read and autosave after every change
|
--escape-unicode - escape non latin characters in strings (with \u)
|
||||||
'read-and-autosave-before-closing' - read and autosave before exiting the app or closing the project
|
--respect-bytecode-access-modifiers - don't change original access modifiers
|
||||||
'ignore' - don't read or save (can be used to skip loading mapping files referenced in the project file)
|
--mappings-path - deobfuscation mappings file or directory. Allowed formats: Tiny and Tiny v2 (both '.tiny'), Enigma (.mapping) or Enigma directory
|
||||||
--deobf - activate deobfuscation
|
--mappings-mode - set mode for handling the deobfuscation mapping file:
|
||||||
--deobf-min - min length of name, renamed if shorter, default: 3
|
'read' - just read, user can always save manually (default)
|
||||||
--deobf-max - max length of name, renamed if longer, default: 64
|
'read-and-autosave-every-change' - read and autosave after every change
|
||||||
--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
|
'read-and-autosave-before-closing' - read and autosave before exiting the app or closing the project
|
||||||
--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
|
'ignore' - don't read or save (can be used to skip loading mapping files referenced in the project file)
|
||||||
--deobf-cfg-file-mode - set mode for handling the JADX auto-generated names' deobfuscation map file:
|
--deobf - activate deobfuscation
|
||||||
'read' - read if found, don't save (default)
|
--deobf-min - min length of name, renamed if shorter, default: 3
|
||||||
'read-or-save' - read if found, save otherwise (don't overwrite)
|
--deobf-max - max length of name, renamed if longer, default: 64
|
||||||
'overwrite' - don't read, always save
|
--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
|
||||||
'ignore' - don't read and don't save
|
--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-res-name-source - better name source for resources:
|
--deobf-cfg-file-mode - set mode for handling the JADX auto-generated names' deobfuscation map file:
|
||||||
'auto' - automatically select best name (default)
|
'read' - read if found, don't save (default)
|
||||||
'resources' - use resources names
|
'read-or-save' - read if found, save otherwise (don't overwrite)
|
||||||
'code' - use R class fields names
|
'overwrite' - don't read, always save
|
||||||
--use-source-name-as-class-name-alias - use source name as class name alias:
|
'ignore' - don't read and don't save
|
||||||
'always' - always use source name if it's available
|
--deobf-res-name-source - better name source for resources:
|
||||||
'if-better' - use source name if it seems better than the current one
|
'auto' - automatically select best name (default)
|
||||||
'never' - never use source name, even if it's available
|
'resources' - use resources names
|
||||||
--use-kotlin-methods-for-var-names - use kotlin intrinsic methods to rename variables, values: disable, apply, apply-and-hide, default: apply
|
'code' - use R class fields names
|
||||||
--rename-flags - fix options (comma-separated list of):
|
--use-source-name-as-class-name-alias - use source name as class name alias:
|
||||||
'case' - fix case sensitivity issues (according to --fs-case-sensitive option),
|
'always' - always use source name if it's available
|
||||||
'valid' - rename java identifiers to make them valid,
|
'if-better' - use source name if it seems better than the current one
|
||||||
'printable' - remove non-printable chars from identifiers,
|
'never' - never use source name, even if it's available
|
||||||
or single 'none' - to disable all renames
|
--source-name-repeat-limit - allow using source name if it appears less than a limit number, default: 10
|
||||||
or single 'all' - to enable all (default)
|
--use-kotlin-methods-for-var-names - use kotlin intrinsic methods to rename variables, values: disable, apply, apply-and-hide, default: apply
|
||||||
--integer-format - how integers are displayed:
|
--rename-flags - fix options (comma-separated list of):
|
||||||
'auto' - automatically select (default)
|
'case' - fix case sensitivity issues (according to --fs-case-sensitive option),
|
||||||
'decimal' - use decimal
|
'valid' - rename java identifiers to make them valid,
|
||||||
'hexadecimal' - use hexadecimal
|
'printable' - remove non-printable chars from identifiers,
|
||||||
--fs-case-sensitive - treat filesystem as case sensitive, false by default
|
or single 'none' - to disable all renames
|
||||||
--cfg - save methods control flow graph to dot file
|
or single 'all' - to enable all (default)
|
||||||
--raw-cfg - save methods control flow graph (use raw instructions)
|
--integer-format - how integers are displayed:
|
||||||
-f, --fallback - set '--decompilation-mode' to 'fallback' (deprecated)
|
'auto' - automatically select (default)
|
||||||
--use-dx - use dx/d8 to convert java bytecode
|
'decimal' - use decimal
|
||||||
--comments-level - set code comments level, values: error, warn, info, debug, user-only, none, default: info
|
'hexadecimal' - use hexadecimal
|
||||||
--log-level - set log level, values: quiet, progress, error, warn, info, debug, default: progress
|
--fs-case-sensitive - treat filesystem as case sensitive, false by default
|
||||||
-v, --verbose - verbose output (set --log-level to DEBUG)
|
--cfg - save methods control flow graph to dot file
|
||||||
-q, --quiet - turn off output (set --log-level to QUIET)
|
--raw-cfg - save methods control flow graph (use raw instructions)
|
||||||
--version - print jadx version
|
-f, --fallback - set '--decompilation-mode' to 'fallback' (deprecated)
|
||||||
-h, --help - print this help
|
--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
|
||||||
|
|
||||||
Plugin options (-P<name>=<value>):
|
Plugin options (-P<name>=<value>):
|
||||||
dex-input: Load .dex and .apk files
|
dex-input: Load .dex and .apk files
|
||||||
- dex-input.verify-checksum - verify dex file checksum before load, values: [yes, no], default: yes
|
- dex-input.verify-checksum - verify dex file checksum before load, values: [yes, no], default: yes
|
||||||
java-convert: Convert .class, .jar and .aar files to dex
|
java-convert: Convert .class, .jar and .aar files to dex
|
||||||
- java-convert.mode - convert mode, values: [dx, d8, both], default: both
|
- java-convert.mode - convert mode, values: [dx, d8, both], default: both
|
||||||
- java-convert.d8-desugar - use desugar in d8, values: [yes, no], default: no
|
- java-convert.d8-desugar - use desugar in d8, values: [yes, no], default: no
|
||||||
kotlin-metadata: Use kotlin.Metadata annotation for code generation
|
kotlin-metadata: Use kotlin.Metadata annotation for code generation
|
||||||
- kotlin-metadata.class-alias - rename class alias, values: [yes, no], default: yes
|
- kotlin-metadata.class-alias - rename class alias, values: [yes, no], default: yes
|
||||||
- kotlin-metadata.method-args - rename function arguments, values: [yes, no], default: yes
|
- kotlin-metadata.method-args - rename function arguments, values: [yes, no], default: yes
|
||||||
- kotlin-metadata.fields - rename fields, 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.companion - rename companion object, values: [yes, no], default: yes
|
||||||
- kotlin-metadata.data-class - add data class modifier, 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.to-string - rename fields using toString, values: [yes, no], default: yes
|
||||||
- kotlin-metadata.getters - rename simple getters to field names, values: [yes, no], default: yes
|
- kotlin-metadata.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: 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, RECAF_SIMPLE_FILE, JOBF_FILE], default: AUTO
|
- rename-mappings.format - mapping format, values: [AUTO, TINY_FILE, TINY_2_FILE, ENIGMA_FILE, ENIGMA_DIR, SRG_FILE, XSRG_FILE, JAM_FILE, CSRG_FILE, TSRG_FILE, TSRG_2_FILE, PROGUARD_FILE, INTELLIJ_MIGRATION_MAP_FILE, RECAF_SIMPLE_FILE, JOBF_FILE], default: AUTO
|
||||||
- rename-mappings.invert - invert mapping on load, values: [yes, no], default: no
|
- rename-mappings.invert - invert mapping on load, values: [yes, no], default: no
|
||||||
smali-input: Load .smali files
|
smali-input: Load .smali files
|
||||||
- smali-input.api-level - Android API level, default: 27
|
- smali-input.api-level - Android API level, default: 27
|
||||||
|
|
||||||
Environment variables:
|
Environment variables:
|
||||||
JADX_DISABLE_XML_SECURITY - set to 'true' to disable all security checks for XML files
|
JADX_DISABLE_XML_SECURITY - set to 'true' to disable all security checks for XML files
|
||||||
@@ -204,6 +213,24 @@ Examples:
|
|||||||
```
|
```
|
||||||
These options also work in jadx-gui running from command line and override options from preferences' dialog
|
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
|
### Troubleshooting
|
||||||
Please check wiki page [Troubleshooting Q&A](https://github.com/skylot/jadx/wiki/Troubleshooting-Q&A)
|
Please check wiki page [Troubleshooting Q&A](https://github.com/skylot/jadx/wiki/Troubleshooting-Q&A)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
|
|||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.github.ben-manes.versions") version "0.51.0"
|
id("com.github.ben-manes.versions") version "0.52.0"
|
||||||
id("se.patrikerdes.use-latest-versions") version "0.2.18"
|
id("se.patrikerdes.use-latest-versions") version "0.2.18"
|
||||||
id("com.diffplug.spotless") version "6.25.0"
|
id("com.diffplug.spotless") version "6.25.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.21")
|
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.21")
|
||||||
|
|
||||||
implementation("org.openrewrite:plugin:6.19.1")
|
implementation("org.openrewrite:plugin:6.19.1")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,16 +14,16 @@ group = "io.github.skylot"
|
|||||||
version = jadxVersion
|
version = jadxVersion
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("org.slf4j:slf4j-api:2.0.16")
|
implementation("org.slf4j:slf4j-api:2.0.17")
|
||||||
compileOnly("org.jetbrains:annotations:26.0.1")
|
compileOnly("org.jetbrains:annotations:26.0.2")
|
||||||
|
|
||||||
testImplementation("ch.qos.logback:logback-classic:1.5.12")
|
testImplementation("ch.qos.logback:logback-classic:1.5.18")
|
||||||
testImplementation("org.assertj:assertj-core:3.26.3")
|
testImplementation("org.assertj:assertj-core:3.27.3")
|
||||||
|
|
||||||
testImplementation("org.junit.jupiter:junit-jupiter:5.11.3")
|
testImplementation("org.junit.jupiter:junit-jupiter:5.12.2")
|
||||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||||
|
|
||||||
testCompileOnly("org.jetbrains:annotations:26.0.1")
|
testCompileOnly("org.jetbrains:annotations:26.0.2")
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
@@ -45,6 +45,7 @@ java {
|
|||||||
tasks {
|
tasks {
|
||||||
compileJava {
|
compileJava {
|
||||||
options.encoding = "UTF-8"
|
options.encoding = "UTF-8"
|
||||||
|
// options.compilerArgs = listOf("-Xlint:deprecation")
|
||||||
}
|
}
|
||||||
jar {
|
jar {
|
||||||
manifest {
|
manifest {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("org.openrewrite.rewrite")
|
id("org.openrewrite.rewrite")
|
||||||
}
|
}
|
||||||
@@ -9,10 +7,10 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
rewrite("org.openrewrite.recipe:rewrite-testing-frameworks:2.21.0")
|
rewrite("org.openrewrite.recipe:rewrite-testing-frameworks:3.8.0")
|
||||||
rewrite("org.openrewrite.recipe:rewrite-logging-frameworks:2.15.1")
|
rewrite("org.openrewrite.recipe:rewrite-logging-frameworks:3.8.0")
|
||||||
rewrite("org.openrewrite.recipe:rewrite-migrate-java:2.28.0")
|
rewrite("org.openrewrite.recipe:rewrite-migrate-java:3.9.0")
|
||||||
rewrite("org.openrewrite.recipe:rewrite-static-analysis:1.19.0")
|
rewrite("org.openrewrite.recipe:rewrite-static-analysis:2.9.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
|
|||||||
@@ -126,6 +126,9 @@
|
|||||||
</module>
|
</module>
|
||||||
<module name="IllegalImport">
|
<module name="IllegalImport">
|
||||||
<property name="illegalClasses" value="jadx.core.utils.DebugUtils"/>
|
<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>
|
||||||
<module name="RegexpSinglelineJava">
|
<module name="RegexpSinglelineJava">
|
||||||
<property name="id" value="printstacktrace"/>
|
<property name="id" value="printstacktrace"/>
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Name=JADX GUI
|
||||||
|
Comment=Dex to Java decompiler
|
||||||
|
Icon=jadx
|
||||||
|
Exec=jadx-gui %f
|
||||||
|
Terminal=false
|
||||||
|
Type=Application
|
||||||
|
Categories=Development;Java;
|
||||||
|
Keywords=Java;Decompiler;
|
||||||
|
StartupWMClass=jadx-gui-JadxGUI
|
||||||
@@ -2,6 +2,10 @@ org.gradle.warning.mode=all
|
|||||||
org.gradle.parallel=true
|
org.gradle.parallel=true
|
||||||
org.gradle.caching=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.
|
# Flags for google-java-format (optimize imports by spotless) for Java >= 16.
|
||||||
# Java < 9 will ignore unsupported flags (thanks to -XX:+IgnoreUnrecognizedVMOptions)
|
# Java < 9 will ignore unsupported flags (thanks to -XX:+IgnoreUnrecognizedVMOptions)
|
||||||
org.gradle.jvmargs=-XX:+IgnoreUnrecognizedVMOptions \
|
org.gradle.jvmargs=-XX:+IgnoreUnrecognizedVMOptions \
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionSha256Sum=31c55713e40233a8303827ceb42ca48a47267a0ad4bab9177123121e71524c26
|
distributionSha256Sum=61ad310d3c7d3e5da131b76bbf22b5a4c0786e9d892dae8c1658d4b484de3caa
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
@@ -86,8 +86,7 @@ done
|
|||||||
# shellcheck disable=SC2034
|
# shellcheck disable=SC2034
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
# 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
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||||
' "$PWD" ) || exit
|
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
@@ -206,7 +205,7 @@ fi
|
|||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Collect all arguments for the java command:
|
# Collect all arguments for the java command:
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
# and any embedded shellness will be escaped.
|
# 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
|
# * 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.
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ plugins {
|
|||||||
id("application")
|
id("application")
|
||||||
|
|
||||||
// use shadow only for application scripts, jar will be copied from jadx-gui
|
// use shadow only for application scripts, jar will be copied from jadx-gui
|
||||||
id("com.gradleup.shadow") version "8.3.5"
|
id("com.gradleup.shadow") version "8.3.6"
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
@@ -18,12 +18,14 @@ dependencies {
|
|||||||
runtimeOnly(project(":jadx-plugins:jadx-smali-input"))
|
runtimeOnly(project(":jadx-plugins:jadx-smali-input"))
|
||||||
runtimeOnly(project(":jadx-plugins:jadx-rename-mappings"))
|
runtimeOnly(project(":jadx-plugins:jadx-rename-mappings"))
|
||||||
runtimeOnly(project(":jadx-plugins:jadx-kotlin-metadata"))
|
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-script:jadx-script-plugin"))
|
||||||
runtimeOnly(project(":jadx-plugins:jadx-xapk-input"))
|
runtimeOnly(project(":jadx-plugins:jadx-xapk-input"))
|
||||||
runtimeOnly(project(":jadx-plugins:jadx-aab-input"))
|
runtimeOnly(project(":jadx-plugins:jadx-aab-input"))
|
||||||
|
runtimeOnly(project(":jadx-plugins:jadx-apkm-input"))
|
||||||
|
|
||||||
implementation("org.jcommander:jcommander:2.0")
|
implementation("org.jcommander:jcommander:2.0")
|
||||||
implementation("ch.qos.logback:logback-classic:1.5.12")
|
implementation("ch.qos.logback:logback-classic:1.5.18")
|
||||||
}
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
@@ -35,6 +37,8 @@ application {
|
|||||||
"-XX:MaxRAMPercentage=70.0",
|
"-XX:MaxRAMPercentage=70.0",
|
||||||
// disable zip checks (#1962)
|
// disable zip checks (#1962)
|
||||||
"-Djdk.util.zip.disableZip64ExtraFieldValidation=true",
|
"-Djdk.util.zip.disableZip64ExtraFieldValidation=true",
|
||||||
|
// Foreign API access for 'directories' library (Windows only)
|
||||||
|
"--enable-native-access=ALL-UNNAMED",
|
||||||
)
|
)
|
||||||
applicationDistribution.from("$rootDir") {
|
applicationDistribution.from("$rootDir") {
|
||||||
include("README.md")
|
include("README.md")
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import jadx.core.plugins.PluginContext;
|
|||||||
import jadx.core.utils.Utils;
|
import jadx.core.utils.Utils;
|
||||||
import jadx.plugins.tools.JadxExternalPluginsLoader;
|
import jadx.plugins.tools.JadxExternalPluginsLoader;
|
||||||
|
|
||||||
public class JCommanderWrapper<T> {
|
public class JCommanderWrapper {
|
||||||
private final JCommander jc;
|
private final JCommander jc;
|
||||||
private final JadxCLIArgs argsObj;
|
private final JadxCLIArgs argsObj;
|
||||||
|
|
||||||
@@ -42,6 +42,7 @@ public class JCommanderWrapper<T> {
|
|||||||
public boolean parse(String[] args) {
|
public boolean parse(String[] args) {
|
||||||
try {
|
try {
|
||||||
jc.parse(args);
|
jc.parse(args);
|
||||||
|
applyFiles(argsObj);
|
||||||
return true;
|
return true;
|
||||||
} catch (ParameterException e) {
|
} catch (ParameterException e) {
|
||||||
System.err.println("Arguments parse error: " + e.getMessage());
|
System.err.println("Arguments parse error: " + e.getMessage());
|
||||||
@@ -50,6 +51,15 @@ public class JCommanderWrapper<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void overrideProvided(JadxCLIArgs obj) {
|
||||||
|
applyFiles(obj);
|
||||||
|
for (ParameterDescription parameter : jc.getParameters()) {
|
||||||
|
if (parameter.isAssigned()) {
|
||||||
|
overrideProperty(obj, parameter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public boolean processCommands() {
|
public boolean processCommands() {
|
||||||
String parsedCommand = jc.getParsedCommand();
|
String parsedCommand = jc.getParsedCommand();
|
||||||
if (parsedCommand == null) {
|
if (parsedCommand == null) {
|
||||||
@@ -58,20 +68,21 @@ public class JCommanderWrapper<T> {
|
|||||||
return JadxCLICommands.process(this, jc, parsedCommand);
|
return JadxCLICommands.process(this, jc, parsedCommand);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void overrideProvided(JadxCLIArgs obj) {
|
/**
|
||||||
List<ParameterDescription> fieldsParams = jc.getParameters();
|
* The main parameter parsing doesn't work if accepting unknown options
|
||||||
List<ParameterDescription> parameters = new ArrayList<>(1 + fieldsParams.size());
|
*/
|
||||||
parameters.add(jc.getMainParameterValue());
|
private void applyFiles(JadxCLIArgs argsObj) {
|
||||||
parameters.addAll(fieldsParams);
|
argsObj.setFiles(jc.getUnknownOptions());
|
||||||
for (ParameterDescription parameter : parameters) {
|
}
|
||||||
if (parameter.isAssigned()) {
|
|
||||||
// copy assigned field value to obj
|
/**
|
||||||
Parameterized parameterized = parameter.getParameterized();
|
* Override assigned field value to obj
|
||||||
Object providedValue = parameterized.get(parameter.getObject());
|
*/
|
||||||
Object newValue = mergeValues(parameterized.getType(), providedValue, () -> parameterized.get(obj));
|
private static void overrideProperty(JadxCLIArgs obj, ParameterDescription parameter) {
|
||||||
parameterized.set(obj, newValue);
|
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" })
|
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||||
@@ -85,10 +96,6 @@ public class JCommanderWrapper<T> {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> getUnknownOptions() {
|
|
||||||
return jc.getUnknownOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void printUsage() {
|
public void printUsage() {
|
||||||
LogHelper.setLogLevel(LogHelper.LogLevelEnum.ERROR); // mute logger while printing help
|
LogHelper.setLogLevel(LogHelper.LogLevelEnum.ERROR); // mute logger while printing help
|
||||||
|
|
||||||
@@ -237,13 +244,17 @@ public class JCommanderWrapper<T> {
|
|||||||
JadxPluginManager pluginManager = decompiler.getPluginManager();
|
JadxPluginManager pluginManager = decompiler.getPluginManager();
|
||||||
pluginManager.load(new JadxExternalPluginsLoader());
|
pluginManager.load(new JadxExternalPluginsLoader());
|
||||||
pluginManager.initAll();
|
pluginManager.initAll();
|
||||||
for (PluginContext context : pluginManager.getAllPluginContexts()) {
|
try {
|
||||||
JadxPluginOptions options = context.getOptions();
|
for (PluginContext context : pluginManager.getAllPluginContexts()) {
|
||||||
if (options != null) {
|
JadxPluginOptions options = context.getOptions();
|
||||||
if (appendPlugin(context.getPluginInfo(), context.getOptions(), sb, maxNamesLen)) {
|
if (options != null) {
|
||||||
k++;
|
if (appendPlugin(context.getPluginInfo(), context.getOptions(), sb, maxNamesLen)) {
|
||||||
|
k++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
pluginManager.unloadAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (sb.length() == 0) {
|
if (sb.length() == 0) {
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package jadx.cli;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import jadx.api.JadxArgs;
|
||||||
|
import jadx.api.security.JadxSecurityFlag;
|
||||||
|
import jadx.api.security.impl.JadxSecurity;
|
||||||
|
import jadx.commons.app.JadxCommonEnv;
|
||||||
|
import jadx.zip.security.DisabledZipSecurity;
|
||||||
|
import jadx.zip.security.IJadxZipSecurity;
|
||||||
|
import jadx.zip.security.JadxZipSecurity;
|
||||||
|
|
||||||
|
public class JadxAppCommon {
|
||||||
|
|
||||||
|
public static void applyEnvVars(JadxArgs jadxArgs) {
|
||||||
|
Set<JadxSecurityFlag> flags = JadxSecurityFlag.all();
|
||||||
|
IJadxZipSecurity zipSecurity;
|
||||||
|
|
||||||
|
boolean disableXmlSecurity = JadxCommonEnv.getBool("JADX_DISABLE_XML_SECURITY", false);
|
||||||
|
if (disableXmlSecurity) {
|
||||||
|
flags.remove(JadxSecurityFlag.SECURE_XML_PARSER);
|
||||||
|
// TODO: not related to 'xml security', but kept for compatibility
|
||||||
|
flags.remove(JadxSecurityFlag.VERIFY_APP_PACKAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean disableZipSecurity = JadxCommonEnv.getBool("JADX_DISABLE_ZIP_SECURITY", false);
|
||||||
|
if (disableZipSecurity) {
|
||||||
|
flags.remove(JadxSecurityFlag.SECURE_ZIP_READER);
|
||||||
|
zipSecurity = DisabledZipSecurity.INSTANCE;
|
||||||
|
} else {
|
||||||
|
JadxZipSecurity jadxZipSecurity = new JadxZipSecurity();
|
||||||
|
int maxZipEntriesCount = JadxCommonEnv.getInt("JADX_ZIP_MAX_ENTRIES_COUNT", -2);
|
||||||
|
if (maxZipEntriesCount != -2) {
|
||||||
|
jadxZipSecurity.setMaxEntriesCount(maxZipEntriesCount);
|
||||||
|
}
|
||||||
|
int zipBombMinUncompressedSize = JadxCommonEnv.getInt("JADX_ZIP_BOMB_MIN_UNCOMPRESSED_SIZE", -2);
|
||||||
|
if (zipBombMinUncompressedSize != -2) {
|
||||||
|
jadxZipSecurity.setZipBombMinUncompressedSize(zipBombMinUncompressedSize);
|
||||||
|
}
|
||||||
|
int setZipBombDetectionFactor = JadxCommonEnv.getInt("JADX_ZIP_BOMB_DETECTION_FACTOR", -2);
|
||||||
|
if (setZipBombDetectionFactor != -2) {
|
||||||
|
jadxZipSecurity.setZipBombDetectionFactor(setZipBombDetectionFactor);
|
||||||
|
}
|
||||||
|
zipSecurity = jadxZipSecurity;
|
||||||
|
}
|
||||||
|
jadxArgs.setSecurity(new JadxSecurity(flags, zipSecurity));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
package jadx.cli;
|
package jadx.cli;
|
||||||
|
|
||||||
import java.util.Set;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -10,11 +11,8 @@ import jadx.api.JadxDecompiler;
|
|||||||
import jadx.api.impl.AnnotatedCodeWriter;
|
import jadx.api.impl.AnnotatedCodeWriter;
|
||||||
import jadx.api.impl.NoOpCodeCache;
|
import jadx.api.impl.NoOpCodeCache;
|
||||||
import jadx.api.impl.SimpleCodeWriter;
|
import jadx.api.impl.SimpleCodeWriter;
|
||||||
import jadx.api.security.JadxSecurityFlag;
|
|
||||||
import jadx.api.security.impl.JadxSecurity;
|
|
||||||
import jadx.cli.LogHelper.LogLevelEnum;
|
import jadx.cli.LogHelper.LogLevelEnum;
|
||||||
import jadx.cli.plugins.JadxFilesGetter;
|
import jadx.cli.plugins.JadxFilesGetter;
|
||||||
import jadx.commons.app.JadxCommonEnv;
|
|
||||||
import jadx.core.utils.exceptions.JadxArgsValidateException;
|
import jadx.core.utils.exceptions.JadxArgsValidateException;
|
||||||
import jadx.plugins.tools.JadxExternalPluginsLoader;
|
import jadx.plugins.tools.JadxExternalPluginsLoader;
|
||||||
|
|
||||||
@@ -31,10 +29,18 @@ public class JadxCLI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static int execute(String[] args) {
|
public static int execute(String[] args) {
|
||||||
|
return execute(args, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int execute(String[] args, @Nullable Consumer<JadxArgs> argsMod) {
|
||||||
try {
|
try {
|
||||||
JadxCLIArgs jadxArgs = new JadxCLIArgs();
|
JadxCLIArgs cliArgs = new JadxCLIArgs();
|
||||||
if (jadxArgs.processArgs(args)) {
|
if (cliArgs.processArgs(args)) {
|
||||||
return processAndSave(jadxArgs);
|
JadxArgs jadxArgs = buildArgs(cliArgs);
|
||||||
|
if (argsMod != null) {
|
||||||
|
argsMod.accept(jadxArgs);
|
||||||
|
}
|
||||||
|
return runSave(jadxArgs, cliArgs);
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
} catch (JadxArgsValidateException e) {
|
} catch (JadxArgsValidateException e) {
|
||||||
@@ -46,7 +52,7 @@ public class JadxCLI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int processAndSave(JadxCLIArgs cliArgs) {
|
private static JadxArgs buildArgs(JadxCLIArgs cliArgs) {
|
||||||
LogHelper.initLogLevel(cliArgs);
|
LogHelper.initLogLevel(cliArgs);
|
||||||
LogHelper.setLogLevelsForLoadingStage();
|
LogHelper.setLogLevelsForLoadingStage();
|
||||||
JadxArgs jadxArgs = cliArgs.toJadxArgs();
|
JadxArgs jadxArgs = cliArgs.toJadxArgs();
|
||||||
@@ -54,7 +60,11 @@ public class JadxCLI {
|
|||||||
jadxArgs.setPluginLoader(new JadxExternalPluginsLoader());
|
jadxArgs.setPluginLoader(new JadxExternalPluginsLoader());
|
||||||
jadxArgs.setFilesGetter(JadxFilesGetter.INSTANCE);
|
jadxArgs.setFilesGetter(JadxFilesGetter.INSTANCE);
|
||||||
initCodeWriterProvider(jadxArgs);
|
initCodeWriterProvider(jadxArgs);
|
||||||
applyEnvVars(jadxArgs);
|
JadxAppCommon.applyEnvVars(jadxArgs);
|
||||||
|
return jadxArgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int runSave(JadxArgs jadxArgs, JadxCLIArgs cliArgs) {
|
||||||
try (JadxDecompiler jadx = new JadxDecompiler(jadxArgs)) {
|
try (JadxDecompiler jadx = new JadxDecompiler(jadxArgs)) {
|
||||||
jadx.load();
|
jadx.load();
|
||||||
if (checkForErrors(jadx)) {
|
if (checkForErrors(jadx)) {
|
||||||
@@ -87,22 +97,6 @@ public class JadxCLI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void applyEnvVars(JadxArgs jadxArgs) {
|
|
||||||
Set<JadxSecurityFlag> flags = JadxSecurityFlag.all();
|
|
||||||
boolean modified = false;
|
|
||||||
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);
|
|
||||||
modified = true;
|
|
||||||
}
|
|
||||||
// TODO: migrate 'ZipSecurity'
|
|
||||||
if (modified) {
|
|
||||||
jadxArgs.setSecurity(new JadxSecurity(flags));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean checkForErrors(JadxDecompiler jadx) {
|
private static boolean checkForErrors(JadxDecompiler jadx) {
|
||||||
if (jadx.getRoot().getClasses().isEmpty()) {
|
if (jadx.getRoot().getClasses().isEmpty()) {
|
||||||
if (jadx.getArgs().isSkipResources()) {
|
if (jadx.getArgs().isSkipResources()) {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package jadx.cli;
|
package jadx.cli;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -14,6 +14,8 @@ import java.util.function.Supplier;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import com.beust.jcommander.DynamicParameter;
|
import com.beust.jcommander.DynamicParameter;
|
||||||
import com.beust.jcommander.IStringConverter;
|
import com.beust.jcommander.IStringConverter;
|
||||||
import com.beust.jcommander.Parameter;
|
import com.beust.jcommander.Parameter;
|
||||||
@@ -30,13 +32,14 @@ import jadx.api.args.ResourceNameSource;
|
|||||||
import jadx.api.args.UseSourceNameAsClassNameAlias;
|
import jadx.api.args.UseSourceNameAsClassNameAlias;
|
||||||
import jadx.api.args.UserRenamesMappingsMode;
|
import jadx.api.args.UserRenamesMappingsMode;
|
||||||
import jadx.core.deobf.conditions.DeobfWhitelist;
|
import jadx.core.deobf.conditions.DeobfWhitelist;
|
||||||
|
import jadx.core.export.ExportGradleType;
|
||||||
import jadx.core.utils.exceptions.JadxArgsValidateException;
|
import jadx.core.utils.exceptions.JadxArgsValidateException;
|
||||||
import jadx.core.utils.files.FileUtils;
|
import jadx.core.utils.files.FileUtils;
|
||||||
|
|
||||||
public class JadxCLIArgs {
|
public class JadxCLIArgs {
|
||||||
|
|
||||||
@Parameter(description = "<input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk, .jadx.kts)")
|
@Parameter(description = "<input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk, .apkm, .jadx.kts)")
|
||||||
protected List<String> files = new ArrayList<>(1);
|
protected List<String> files = Collections.emptyList();
|
||||||
|
|
||||||
@Parameter(names = { "-d", "--output-dir" }, description = "output directory")
|
@Parameter(names = { "-d", "--output-dir" }, description = "output directory")
|
||||||
protected String outDir;
|
protected String outDir;
|
||||||
@@ -53,6 +56,9 @@ public class JadxCLIArgs {
|
|||||||
@Parameter(names = { "-s", "--no-src" }, description = "do not decompile source code")
|
@Parameter(names = { "-s", "--no-src" }, description = "do not decompile source code")
|
||||||
protected boolean skipSources = false;
|
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")
|
@Parameter(names = { "--single-class" }, description = "decompile a single class, full name, raw or alias")
|
||||||
protected String singleClass = null;
|
protected String singleClass = null;
|
||||||
|
|
||||||
@@ -62,11 +68,19 @@ public class JadxCLIArgs {
|
|||||||
@Parameter(names = { "--output-format" }, description = "can be 'java' or 'json'")
|
@Parameter(names = { "--output-format" }, description = "can be 'java' or 'json'")
|
||||||
protected String outputFormat = "java";
|
protected String outputFormat = "java";
|
||||||
|
|
||||||
@Parameter(names = { "-e", "--export-gradle" }, description = "save as android gradle project")
|
@Parameter(names = { "-e", "--export-gradle" }, description = "save as gradle project (set '--export-gradle-type' to 'auto')")
|
||||||
protected boolean exportAsGradleProject = false;
|
protected boolean exportAsGradleProject = false;
|
||||||
|
|
||||||
@Parameter(names = { "-j", "--threads-count" }, description = "processing threads count")
|
@Parameter(
|
||||||
protected int threadsCount = JadxArgs.DEFAULT_THREADS_COUNT;
|
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(
|
@Parameter(
|
||||||
names = { "-m", "--decompilation-mode" },
|
names = { "-m", "--decompilation-mode" },
|
||||||
@@ -200,6 +214,12 @@ public class JadxCLIArgs {
|
|||||||
)
|
)
|
||||||
protected UseSourceNameAsClassNameAlias useSourceNameAsClassNameAlias = null;
|
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(
|
@Parameter(
|
||||||
names = { "--use-kotlin-methods-for-var-names" },
|
names = { "--use-kotlin-methods-for-var-names" },
|
||||||
description = "use kotlin intrinsic methods to rename variables, values: disable, apply, apply-and-hide",
|
description = "use kotlin intrinsic methods to rename variables, values: disable, apply, apply-and-hide",
|
||||||
@@ -207,6 +227,12 @@ public class JadxCLIArgs {
|
|||||||
)
|
)
|
||||||
protected UseKotlinMethodsForVarNames useKotlinMethodsForVarNames = UseKotlinMethodsForVarNames.APPLY;
|
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(
|
@Parameter(
|
||||||
names = { "--rename-flags" },
|
names = { "--rename-flags" },
|
||||||
description = "fix options (comma-separated list of):"
|
description = "fix options (comma-separated list of):"
|
||||||
@@ -277,29 +303,11 @@ public class JadxCLIArgs {
|
|||||||
protected Map<String, String> pluginOptions = new HashMap<>();
|
protected Map<String, String> pluginOptions = new HashMap<>();
|
||||||
|
|
||||||
public boolean processArgs(String[] args) {
|
public boolean processArgs(String[] args) {
|
||||||
JCommanderWrapper<JadxCLIArgs> jcw = new JCommanderWrapper<>(this);
|
JCommanderWrapper jcw = new JCommanderWrapper(this);
|
||||||
return jcw.parse(args) && process(jcw);
|
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()) {
|
if (jcw.processCommands()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -352,11 +360,16 @@ public class JadxCLIArgs {
|
|||||||
args.setDeobfuscationMaxLength(deobfuscationMaxLength);
|
args.setDeobfuscationMaxLength(deobfuscationMaxLength);
|
||||||
args.setDeobfuscationWhitelist(Arrays.asList(deobfuscationWhitelistStr.split(" ")));
|
args.setDeobfuscationWhitelist(Arrays.asList(deobfuscationWhitelistStr.split(" ")));
|
||||||
args.setUseSourceNameAsClassNameAlias(getUseSourceNameAsClassNameAlias());
|
args.setUseSourceNameAsClassNameAlias(getUseSourceNameAsClassNameAlias());
|
||||||
|
args.setUseHeadersForDetectResourceExtensions(useHeadersForDetectResourceExtensions);
|
||||||
|
args.setSourceNameRepeatLimit(sourceNameRepeatLimit);
|
||||||
args.setUseKotlinMethodsForVarNames(useKotlinMethodsForVarNames);
|
args.setUseKotlinMethodsForVarNames(useKotlinMethodsForVarNames);
|
||||||
args.setResourceNameSource(resourceNameSource);
|
args.setResourceNameSource(resourceNameSource);
|
||||||
args.setEscapeUnicode(escapeUnicode);
|
args.setEscapeUnicode(escapeUnicode);
|
||||||
args.setRespectBytecodeAccModifiers(respectBytecodeAccessModifiers);
|
args.setRespectBytecodeAccModifiers(respectBytecodeAccessModifiers);
|
||||||
args.setExportAsGradleProject(exportAsGradleProject);
|
args.setExportGradleType(exportGradleType);
|
||||||
|
if (exportAsGradleProject && exportGradleType == null) {
|
||||||
|
args.setExportGradleType(ExportGradleType.AUTO);
|
||||||
|
}
|
||||||
args.setSkipXmlPrettyPrint(skipXmlPrettyPrint);
|
args.setSkipXmlPrettyPrint(skipXmlPrettyPrint);
|
||||||
args.setUseImports(useImports);
|
args.setUseImports(useImports);
|
||||||
args.setDebugInfo(debugInfo);
|
args.setDebugInfo(debugInfo);
|
||||||
@@ -381,6 +394,10 @@ public class JadxCLIArgs {
|
|||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setFiles(List<String> files) {
|
||||||
|
this.files = files;
|
||||||
|
}
|
||||||
|
|
||||||
public String getOutDir() {
|
public String getOutDir() {
|
||||||
return outDir;
|
return outDir;
|
||||||
}
|
}
|
||||||
@@ -508,6 +525,10 @@ public class JadxCLIArgs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getSourceNameRepeatLimit() {
|
||||||
|
return sourceNameRepeatLimit;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Use {@link #getUseSourceNameAsClassNameAlias()} instead.
|
* @deprecated Use {@link #getUseSourceNameAsClassNameAlias()} instead.
|
||||||
*/
|
*/
|
||||||
@@ -572,6 +593,10 @@ public class JadxCLIArgs {
|
|||||||
return fsCaseSensitive;
|
return fsCaseSensitive;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isUseHeadersForDetectResourceExtensions() {
|
||||||
|
return useHeadersForDetectResourceExtensions;
|
||||||
|
}
|
||||||
|
|
||||||
public CommentsLevel getCommentsLevel() {
|
public CommentsLevel getCommentsLevel() {
|
||||||
return commentsLevel;
|
return commentsLevel;
|
||||||
}
|
}
|
||||||
@@ -653,6 +678,12 @@ public class JadxCLIArgs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class ExportGradleTypeConverter extends BaseEnumConverter<ExportGradleType> {
|
||||||
|
public ExportGradleTypeConverter() {
|
||||||
|
super(ExportGradleType::valueOf, ExportGradleType::values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static class LogLevelConverter extends BaseEnumConverter<LogHelper.LogLevelEnum> {
|
public static class LogLevelConverter extends BaseEnumConverter<LogHelper.LogLevelEnum> {
|
||||||
public LogLevelConverter() {
|
public LogLevelConverter() {
|
||||||
super(LogHelper.LogLevelEnum::valueOf, LogHelper.LogLevelEnum::values);
|
super(LogHelper.LogLevelEnum::valueOf, LogHelper.LogLevelEnum::values);
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public class JadxCLICommands {
|
|||||||
COMMANDS_MAP.forEach(builder::addCommand);
|
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);
|
ICommand command = COMMANDS_MAP.get(parsedCommand);
|
||||||
if (command == null) {
|
if (command == null) {
|
||||||
throw new JadxArgsValidateException("Unknown command: " + parsedCommand
|
throw new JadxArgsValidateException("Unknown command: " + parsedCommand
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ public class CommandPlugins implements ICommand {
|
|||||||
|
|
||||||
@SuppressWarnings("UnnecessaryReturnStatement")
|
@SuppressWarnings("UnnecessaryReturnStatement")
|
||||||
@Override
|
@Override
|
||||||
public void process(JCommanderWrapper<?> jcw, JCommander subCommander) {
|
public void process(JCommanderWrapper jcw, JCommander subCommander) {
|
||||||
if (printHelp) {
|
if (printHelp) {
|
||||||
jcw.printUsage(subCommander);
|
jcw.printUsage(subCommander);
|
||||||
return;
|
return;
|
||||||
@@ -156,7 +156,7 @@ public class CommandPlugins implements ICommand {
|
|||||||
sb.append(" (disabled)");
|
sb.append(" (disabled)");
|
||||||
}
|
}
|
||||||
sb.append(" - ").append(plugin.getName());
|
sb.append(" - ").append(plugin.getName());
|
||||||
sb.append(": ").append(plugin.getDescription());
|
sb.append(": ").append(formatDescription(plugin.getDescription()));
|
||||||
System.out.println(sb);
|
System.out.println(sb);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,11 +192,24 @@ public class CommandPlugins implements ICommand {
|
|||||||
if (!installedSet.contains(plugin.getPluginId())) {
|
if (!installedSet.contains(plugin.getPluginId())) {
|
||||||
System.out.println(" - " + plugin.getPluginId()
|
System.out.println(" - " + plugin.getPluginId()
|
||||||
+ " - " + plugin.getName()
|
+ " - " + plugin.getName()
|
||||||
+ ": " + plugin.getDescription());
|
+ ": " + 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) {
|
private void installPlugin(String locationId) {
|
||||||
JadxPluginMetadata plugin = JadxPluginsTools.getInstance().install(locationId);
|
JadxPluginMetadata plugin = JadxPluginsTools.getInstance().install(locationId);
|
||||||
System.out.println("Plugin installed: " + plugin.getPluginId() + ":" + plugin.getVersion());
|
System.out.println("Plugin installed: " + plugin.getPluginId() + ":" + plugin.getVersion());
|
||||||
|
|||||||
@@ -7,5 +7,5 @@ import jadx.cli.JCommanderWrapper;
|
|||||||
public interface ICommand {
|
public interface ICommand {
|
||||||
String name();
|
String name();
|
||||||
|
|
||||||
void process(JCommanderWrapper<?> jcw, JCommander subCommander);
|
void process(JCommanderWrapper jcw, JCommander subCommander);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package jadx.cli.tools;
|
package jadx.cli.tools;
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@@ -11,7 +10,6 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import java.util.zip.ZipEntry;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -19,8 +17,10 @@ import org.slf4j.LoggerFactory;
|
|||||||
import jadx.api.JadxArgs;
|
import jadx.api.JadxArgs;
|
||||||
import jadx.core.dex.nodes.RootNode;
|
import jadx.core.dex.nodes.RootNode;
|
||||||
import jadx.core.utils.android.TextResMapFile;
|
import jadx.core.utils.android.TextResMapFile;
|
||||||
import jadx.core.utils.files.ZipFile;
|
|
||||||
import jadx.core.xmlgen.ResTableBinaryParser;
|
import jadx.core.xmlgen.ResTableBinaryParser;
|
||||||
|
import jadx.zip.IZipEntry;
|
||||||
|
import jadx.zip.ZipContent;
|
||||||
|
import jadx.zip.ZipReader;
|
||||||
|
|
||||||
import static jadx.core.utils.files.FileUtils.expandDirs;
|
import static jadx.core.utils.files.FileUtils.expandDirs;
|
||||||
|
|
||||||
@@ -54,24 +54,25 @@ public class ConvertArscFile {
|
|||||||
LOG.info("Input entries count: {}", resMap.size());
|
LOG.info("Input entries count: {}", resMap.size());
|
||||||
|
|
||||||
RootNode root = new RootNode(new JadxArgs()); // not really needed
|
RootNode root = new RootNode(new JadxArgs()); // not really needed
|
||||||
|
ZipReader zipReader = new ZipReader();
|
||||||
rewritesCount = 0;
|
rewritesCount = 0;
|
||||||
for (Path resFile : inputResFiles) {
|
for (Path resFile : inputResFiles) {
|
||||||
ResTableBinaryParser resTableParser = new ResTableBinaryParser(root, true);
|
ResTableBinaryParser resTableParser = new ResTableBinaryParser(root, true);
|
||||||
if (resFile.getFileName().toString().endsWith(".jar")) {
|
if (resFile.getFileName().toString().endsWith(".jar")) {
|
||||||
// Load resources.arsc from android.jar
|
// Load resources.arsc from android.jar
|
||||||
try (ZipFile zip = new ZipFile(resFile.toFile())) {
|
try (ZipContent zip = zipReader.open(resFile.toFile())) {
|
||||||
ZipEntry entry = zip.getEntry("resources.arsc");
|
IZipEntry entry = zip.searchEntry("resources.arsc");
|
||||||
if (entry == null) {
|
if (entry == null) {
|
||||||
LOG.error("Failed to load \"resources.arsc\" from {}", resFile);
|
LOG.error("Failed to load \"resources.arsc\" from {}", resFile);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try (InputStream inputStream = zip.getInputStream(entry)) {
|
try (InputStream inputStream = entry.getInputStream()) {
|
||||||
resTableParser.decode(inputStream);
|
resTableParser.decode(inputStream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Load resources.arsc from extracted file
|
// Load resources.arsc from extracted file
|
||||||
try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(resFile))) {
|
try (InputStream inputStream = Files.newInputStream(resFile)) {
|
||||||
resTableParser.decode(inputStream);
|
resTableParser.decode(inputStream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -93,7 +93,16 @@ public class JadxCLIArgsTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private JadxCLIArgs override(JadxCLIArgs jadxArgs, String... args) {
|
private JadxCLIArgs override(JadxCLIArgs jadxArgs, String... args) {
|
||||||
return check(jadxArgs, jadxArgs.overrideProvided(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) {
|
private static JadxCLIArgs check(JadxCLIArgs jadxArgs, boolean res) {
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package jadx.cli;
|
||||||
|
|
||||||
|
import org.assertj.core.api.Condition;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
public class TestExport extends BaseCliIntegrationTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBasicExport() throws Exception {
|
||||||
|
int result = execJadxCli("samples/small.apk");
|
||||||
|
assertThat(result).isEqualTo(0);
|
||||||
|
assertThat(collectAllFilesInDir(outputDir))
|
||||||
|
.map(this::pathToUniformString)
|
||||||
|
.haveExactly(2, new Condition<>(f -> f.startsWith("sources/") && f.endsWith(".java"), "sources"))
|
||||||
|
.haveExactly(10, new Condition<>(f -> f.startsWith("resources/"), "resources"))
|
||||||
|
.haveExactly(1, new Condition<>(f -> f.equals("resources/AndroidManifest.xml"), "manifest"))
|
||||||
|
.hasSize(12);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGradleExportApk() throws Exception {
|
||||||
|
int result = execJadxCli("samples/small.apk", "--export-gradle");
|
||||||
|
assertThat(result).isEqualTo(0);
|
||||||
|
assertThat(collectAllFilesInDir(outputDir))
|
||||||
|
.describedAs("check output files")
|
||||||
|
.map(this::pathToUniformString)
|
||||||
|
.haveExactly(2, new Condition<>(f -> f.endsWith(".java"), "java classes"))
|
||||||
|
.haveExactly(0, new Condition<>(f -> f.endsWith("classes.dex"), "dex files"))
|
||||||
|
.hasSize(15);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGradleExportAAR() throws Exception {
|
||||||
|
int result = execJadxCli("samples/test-lib.aar", "--export-gradle");
|
||||||
|
assertThat(result).isEqualTo(0);
|
||||||
|
assertThat(collectAllFilesInDir(outputDir))
|
||||||
|
.describedAs("check output files")
|
||||||
|
.map(this::printFileContent)
|
||||||
|
.map(this::pathToUniformString)
|
||||||
|
.haveExactly(1, new Condition<>(f -> f.startsWith("lib/src/main/java/") && f.endsWith(".java"), "java"))
|
||||||
|
.haveExactly(0, new Condition<>(f -> f.endsWith(".jar"), "jar files"))
|
||||||
|
.hasSize(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGradleExportSimpleJava() throws Exception {
|
||||||
|
int result = execJadxCli("samples/HelloWorld.class", "--export-gradle");
|
||||||
|
assertThat(result).isEqualTo(0);
|
||||||
|
assertThat(collectAllFilesInDir(outputDir))
|
||||||
|
.describedAs("check output files")
|
||||||
|
.map(this::printFileContent)
|
||||||
|
.map(this::pathToUniformString)
|
||||||
|
.haveExactly(1, new Condition<>(f -> f.endsWith(".java") && f.startsWith("app/src/main/java/"), "java"))
|
||||||
|
.haveExactly(0, new Condition<>(f -> f.endsWith(".class"), "class files"))
|
||||||
|
.haveExactly(1, new Condition<>(f -> f.equals("settings.gradle.kts"), "settings"))
|
||||||
|
.haveExactly(1, new Condition<>(f -> f.equals("app/build.gradle.kts"), "build"))
|
||||||
|
.hasSize(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGradleExportInvalidType() throws Exception {
|
||||||
|
int result = execJadxCli("samples/HelloWorld.class", "--export-gradle-type", "android-app");
|
||||||
|
assertThat(result).isEqualTo(0);
|
||||||
|
// expect output in 'android-app' template, but most fields will be set to UNKNOWN.
|
||||||
|
assertThat(collectAllFilesInDir(outputDir))
|
||||||
|
.describedAs("check output files")
|
||||||
|
.map(this::printFileContent)
|
||||||
|
.map(this::pathToUniformString)
|
||||||
|
.haveExactly(1, new Condition<>(f -> f.endsWith(".java") && f.startsWith("app/src/main/java/"), "java"))
|
||||||
|
.haveExactly(1, new Condition<>(f -> f.equals("settings.gradle"), "settings"))
|
||||||
|
.haveExactly(1, new Condition<>(f -> f.equals("build.gradle"), "build"))
|
||||||
|
.haveExactly(1, new Condition<>(f -> f.equals("app/build.gradle"), "app build"))
|
||||||
|
.hasSize(4);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,56 +1,32 @@
|
|||||||
package jadx.cli;
|
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.Path;
|
||||||
import java.nio.file.PathMatcher;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import org.assertj.core.api.Condition;
|
import org.assertj.core.api.Condition;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
public class TestInput {
|
public class TestInput extends BaseCliIntegrationTest {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(TestInput.class);
|
|
||||||
|
|
||||||
private static final PathMatcher LOG_ALL_FILES = path -> {
|
|
||||||
LOG.debug("File in result dir: {}", path);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
@TempDir
|
|
||||||
Path testDir;
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testHelp() {
|
public void testHelp() {
|
||||||
int result = JadxCLI.execute(new String[] { "--help" });
|
int result = execJadxCli(new String[] { "--help" });
|
||||||
assertThat(result).isEqualTo(0);
|
assertThat(result).isEqualTo(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testApkInput() throws Exception {
|
public void testApkInput() throws Exception {
|
||||||
int result = JadxCLI.execute(buildArgs(List.of(), "samples/small.apk"));
|
int result = execJadxCli(buildArgs(List.of(), "samples/small.apk"));
|
||||||
assertThat(result).isEqualTo(0);
|
assertThat(result).isEqualTo(0);
|
||||||
List<Path> resultFiles = collectAllFilesInDir(testDir);
|
assertThat(collectAllFilesInDir(outputDir))
|
||||||
printFiles(resultFiles);
|
|
||||||
assertThat(resultFiles)
|
|
||||||
.describedAs("check output files")
|
.describedAs("check output files")
|
||||||
.map(p -> p.getFileName().toString())
|
.map(p -> p.getFileName().toString())
|
||||||
.haveExactly(2, new Condition<>(f -> f.endsWith(".java"), "java classes"))
|
.haveExactly(2, new Condition<>(f -> f.endsWith(".java"), "java classes"))
|
||||||
.haveExactly(9, new Condition<>(f -> f.endsWith(".xml"), "xml resources"))
|
.haveExactly(9, new Condition<>(f -> f.endsWith(".xml"), "xml resources"))
|
||||||
.haveExactly(1, new Condition<>(f -> f.equals("classes.dex"), "dex"))
|
|
||||||
.haveExactly(1, new Condition<>(f -> f.equals("AndroidManifest.xml"), "manifest"))
|
.haveExactly(1, new Condition<>(f -> f.equals("AndroidManifest.xml"), "manifest"))
|
||||||
.hasSize(13);
|
.hasSize(12);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -75,84 +51,26 @@ public class TestInput {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testFallbackMode() throws Exception {
|
public void testFallbackMode() throws Exception {
|
||||||
int result = JadxCLI.execute(buildArgs(List.of("-f"), "samples/hello.dex"));
|
int result = execJadxCli(buildArgs(List.of("-f"), "samples/hello.dex"));
|
||||||
assertThat(result).isEqualTo(0);
|
assertThat(result).isEqualTo(0);
|
||||||
List<Path> files = collectJavaFilesInDir(testDir);
|
List<Path> files = collectJavaFilesInDir(outputDir);
|
||||||
assertThat(files).hasSize(1);
|
assertThat(files).hasSize(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSimpleMode() throws Exception {
|
public void testSimpleMode() throws Exception {
|
||||||
int result = JadxCLI.execute(buildArgs(List.of("--decompilation-mode", "simple"), "samples/hello.dex"));
|
int result = execJadxCli(buildArgs(List.of("--decompilation-mode", "simple"), "samples/hello.dex"));
|
||||||
assertThat(result).isEqualTo(0);
|
assertThat(result).isEqualTo(0);
|
||||||
List<Path> files = collectJavaFilesInDir(testDir);
|
List<Path> files = collectJavaFilesInDir(outputDir);
|
||||||
assertThat(files).hasSize(1);
|
assertThat(files).hasSize(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testResourceOnly() throws Exception {
|
public void testResourceOnly() throws Exception {
|
||||||
int result = JadxCLI.execute(buildArgs(List.of(), "samples/resources-only.apk"));
|
int result = execJadxCli(buildArgs(List.of(), "samples/resources-only.apk"));
|
||||||
assertThat(result).isEqualTo(0);
|
assertThat(result).isEqualTo(0);
|
||||||
List<Path> files = collectFilesInDir(testDir,
|
List<Path> files = collectFilesInDir(outputDir,
|
||||||
path -> path.getFileName().toString().equalsIgnoreCase("AndroidManifest.xml"));
|
path -> path.getFileName().toString().equalsIgnoreCase("AndroidManifest.xml"));
|
||||||
assertThat(files).isNotEmpty();
|
assertThat(files).isNotEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void decompile(String... inputSamples) throws URISyntaxException, IOException {
|
|
||||||
int result = JadxCLI.execute(buildArgs(List.of(), inputSamples));
|
|
||||||
assertThat(result).isEqualTo(0);
|
|
||||||
List<Path> resultJavaFiles = collectJavaFilesInDir(testDir);
|
|
||||||
assertThat(resultJavaFiles).isNotEmpty();
|
|
||||||
|
|
||||||
// do not copy input files as resources
|
|
||||||
for (Path path : collectFilesInDir(testDir, LOG_ALL_FILES)) {
|
|
||||||
for (String inputSample : inputSamples) {
|
|
||||||
assertThat(path.toAbsolutePath().toString()).doesNotContain(inputSample);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String[] buildArgs(List<String> options, String... inputSamples) throws URISyntaxException {
|
|
||||||
List<String> args = new ArrayList<>(options);
|
|
||||||
args.add("-v");
|
|
||||||
args.add("-d");
|
|
||||||
args.add(testDir.toAbsolutePath().toString());
|
|
||||||
|
|
||||||
for (String inputSample : inputSamples) {
|
|
||||||
URL resource = getClass().getClassLoader().getResource(inputSample);
|
|
||||||
assertThat(resource).isNotNull();
|
|
||||||
String sampleFile = resource.toURI().getRawPath();
|
|
||||||
args.add(sampleFile);
|
|
||||||
}
|
|
||||||
return args.toArray(new String[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void printFiles(List<Path> files) {
|
|
||||||
LOG.info("Output files (count: {}):", files.size());
|
|
||||||
for (Path file : files) {
|
|
||||||
LOG.info(" {}", testDir.relativize(file));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<Path> collectJavaFilesInDir(Path dir) throws IOException {
|
|
||||||
PathMatcher javaMatcher = dir.getFileSystem().getPathMatcher("glob:**.java");
|
|
||||||
return collectFilesInDir(dir, javaMatcher);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<Path> collectAllFilesInDir(Path dir) throws IOException {
|
|
||||||
try (Stream<Path> pathStream = Files.walk(dir)) {
|
|
||||||
return pathStream
|
|
||||||
.filter(Files::isRegularFile)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
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.
@@ -3,5 +3,5 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("dev.dirs:directories:26")
|
implementation("io.get-coursier.util:directories-jni:0.1.3")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,16 @@ import java.nio.file.Path;
|
|||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import dev.dirs.ProjectDirectories;
|
import dev.dirs.ProjectDirectories;
|
||||||
|
import dev.dirs.impl.Windows;
|
||||||
|
import dev.dirs.impl.WindowsPowerShell;
|
||||||
|
import dev.dirs.jni.WindowsJni;
|
||||||
|
|
||||||
public class JadxCommonFiles {
|
public class JadxCommonFiles {
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(JadxCommonFiles.class);
|
||||||
|
|
||||||
private static final Path CONFIG_DIR;
|
private static final Path CONFIG_DIR;
|
||||||
private static final Path CACHE_DIR;
|
private static final Path CACHE_DIR;
|
||||||
@@ -57,10 +63,32 @@ public class JadxCommonFiles {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private synchronized ProjectDirectories loadDirs() {
|
private synchronized ProjectDirectories loadDirs() {
|
||||||
if (dirs == null) {
|
ProjectDirectories currentDirs = dirs;
|
||||||
dirs = ProjectDirectories.from("io.github", "skylot", "jadx");
|
if (currentDirs != null) {
|
||||||
|
return currentDirs;
|
||||||
}
|
}
|
||||||
return dirs;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Path getCacheDir() {
|
public Path getCacheDir() {
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
## jadx zip
|
||||||
|
|
||||||
|
Custom zip reader implementation to fight tampering and provide additional security checks
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
plugins {
|
||||||
|
id("jadx-library")
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package jadx.zip;
|
||||||
|
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public interface IZipParser extends Closeable {
|
||||||
|
|
||||||
|
ZipContent open() throws IOException;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
-11
@@ -1,21 +1,22 @@
|
|||||||
package jadx.api.plugins.utils;
|
package jadx.zip.io;
|
||||||
|
|
||||||
import java.io.FilterInputStream;
|
import java.io.FilterInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
||||||
public class LimitedInputStream extends FilterInputStream {
|
public class LimitedInputStream extends FilterInputStream {
|
||||||
|
|
||||||
private final long maxSize;
|
private final long maxSize;
|
||||||
|
|
||||||
private long currentPos;
|
private long currentPos;
|
||||||
|
private long markPos;
|
||||||
|
|
||||||
protected LimitedInputStream(InputStream in, long maxSize) {
|
public LimitedInputStream(InputStream in, long maxSize) {
|
||||||
super(in);
|
super(in);
|
||||||
this.maxSize = maxSize;
|
this.maxSize = maxSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkPos() {
|
private void addAndCheckPos(long count) {
|
||||||
|
currentPos += count;
|
||||||
if (currentPos > maxSize) {
|
if (currentPos > maxSize) {
|
||||||
throw new IllegalStateException("Read limit exceeded");
|
throw new IllegalStateException("Read limit exceeded");
|
||||||
}
|
}
|
||||||
@@ -25,18 +26,17 @@ public class LimitedInputStream extends FilterInputStream {
|
|||||||
public int read() throws IOException {
|
public int read() throws IOException {
|
||||||
int data = super.read();
|
int data = super.read();
|
||||||
if (data != -1) {
|
if (data != -1) {
|
||||||
currentPos++;
|
addAndCheckPos(1);
|
||||||
checkPos();
|
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("NullableProblems")
|
||||||
@Override
|
@Override
|
||||||
public int read(byte[] b, int off, int len) throws IOException {
|
public int read(byte[] b, int off, int len) throws IOException {
|
||||||
int count = super.read(b, off, len);
|
int count = super.read(b, off, len);
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
currentPos += count;
|
addAndCheckPos(count);
|
||||||
checkPos();
|
|
||||||
}
|
}
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
@@ -44,10 +44,21 @@ public class LimitedInputStream extends FilterInputStream {
|
|||||||
@Override
|
@Override
|
||||||
public long skip(long n) throws IOException {
|
public long skip(long n) throws IOException {
|
||||||
long skipped = super.skip(n);
|
long skipped = super.skip(n);
|
||||||
if (skipped != 0) {
|
if (skipped > 0) {
|
||||||
currentPos += skipped;
|
addAndCheckPos(skipped);
|
||||||
checkPos();
|
|
||||||
}
|
}
|
||||||
return skipped;
|
return skipped;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mark(int readLimit) {
|
||||||
|
super.mark(readLimit);
|
||||||
|
markPos = currentPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset() throws IOException {
|
||||||
|
super.reset();
|
||||||
|
currentPos = markPos;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,439 @@
|
|||||||
|
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 + '}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
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,8 +4,9 @@ plugins {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":jadx-plugins:jadx-input-api"))
|
api(project(":jadx-plugins:jadx-input-api"))
|
||||||
|
api(project(":jadx-commons:jadx-zip"))
|
||||||
|
|
||||||
implementation("com.google.code.gson:gson:2.11.0")
|
implementation("com.google.code.gson:gson:2.13.1")
|
||||||
|
|
||||||
testImplementation("org.apache.commons:commons-lang3:3.17.0")
|
testImplementation("org.apache.commons:commons-lang3:3.17.0")
|
||||||
|
|
||||||
@@ -21,7 +22,7 @@ dependencies {
|
|||||||
strictly("[3.33, 3.34[") // from 3.34 compiled with Java 17
|
strictly("[3.33, 3.34[") // from 3.34 compiled with Java 17
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
testImplementation("tools.profiler:async-profiler:3.0")
|
testImplementation("tools.profiler:async-profiler:4.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
val jadxTestJavaVersion = getTestJavaVersion()
|
val jadxTestJavaVersion = getTestJavaVersion()
|
||||||
|
|||||||
@@ -19,5 +19,18 @@ public enum DecompilationMode {
|
|||||||
/**
|
/**
|
||||||
* Raw instructions without modifications
|
* Raw instructions without modifications
|
||||||
*/
|
*/
|
||||||
FALLBACK
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import jadx.api.usage.impl.InMemoryUsageInfoCache;
|
|||||||
import jadx.core.deobf.DeobfAliasProvider;
|
import jadx.core.deobf.DeobfAliasProvider;
|
||||||
import jadx.core.deobf.conditions.DeobfWhitelist;
|
import jadx.core.deobf.conditions.DeobfWhitelist;
|
||||||
import jadx.core.deobf.conditions.JadxRenameConditions;
|
import jadx.core.deobf.conditions.JadxRenameConditions;
|
||||||
|
import jadx.core.export.ExportGradleType;
|
||||||
import jadx.core.plugins.PluginContext;
|
import jadx.core.plugins.PluginContext;
|
||||||
import jadx.core.plugins.files.IJadxFilesGetter;
|
import jadx.core.plugins.files.IJadxFilesGetter;
|
||||||
import jadx.core.plugins.files.TempFilesGetter;
|
import jadx.core.plugins.files.TempFilesGetter;
|
||||||
@@ -90,6 +91,7 @@ public class JadxArgs implements Closeable {
|
|||||||
|
|
||||||
private boolean skipResources = false;
|
private boolean skipResources = false;
|
||||||
private boolean skipSources = false;
|
private boolean skipSources = false;
|
||||||
|
private boolean useHeadersForDetectResourceExtensions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Predicate that allows to filter the classes to be process based on their full name
|
* Predicate that allows to filter the classes to be process based on their full name
|
||||||
@@ -106,6 +108,7 @@ public class JadxArgs implements Closeable {
|
|||||||
|
|
||||||
private boolean deobfuscationOn = false;
|
private boolean deobfuscationOn = false;
|
||||||
private UseSourceNameAsClassNameAlias useSourceNameAsClassNameAlias = UseSourceNameAsClassNameAlias.getDefault();
|
private UseSourceNameAsClassNameAlias useSourceNameAsClassNameAlias = UseSourceNameAsClassNameAlias.getDefault();
|
||||||
|
private int sourceNameRepeatLimit = 10;
|
||||||
|
|
||||||
private File generatedRenamesMappingFile = null;
|
private File generatedRenamesMappingFile = null;
|
||||||
private GeneratedRenamesMappingFileMode generatedRenamesMappingFileMode = GeneratedRenamesMappingFileMode.getDefault();
|
private GeneratedRenamesMappingFileMode generatedRenamesMappingFileMode = GeneratedRenamesMappingFileMode.getDefault();
|
||||||
@@ -132,7 +135,7 @@ public class JadxArgs implements Closeable {
|
|||||||
private boolean escapeUnicode = false;
|
private boolean escapeUnicode = false;
|
||||||
private boolean replaceConsts = true;
|
private boolean replaceConsts = true;
|
||||||
private boolean respectBytecodeAccModifiers = false;
|
private boolean respectBytecodeAccModifiers = false;
|
||||||
private boolean exportAsGradleProject = false;
|
private @Nullable ExportGradleType exportGradleType = null;
|
||||||
|
|
||||||
private boolean restoreSwitchOverString = true;
|
private boolean restoreSwitchOverString = true;
|
||||||
|
|
||||||
@@ -460,6 +463,14 @@ public class JadxArgs implements Closeable {
|
|||||||
this.useSourceNameAsClassNameAlias = useSourceNameAsClassNameAlias;
|
this.useSourceNameAsClassNameAlias = useSourceNameAsClassNameAlias;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getSourceNameRepeatLimit() {
|
||||||
|
return sourceNameRepeatLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSourceNameRepeatLimit(int sourceNameRepeatLimit) {
|
||||||
|
this.sourceNameRepeatLimit = sourceNameRepeatLimit;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Use {@link #getUseSourceNameAsClassNameAlias()} instead.
|
* @deprecated Use {@link #getUseSourceNameAsClassNameAlias()} instead.
|
||||||
*/
|
*/
|
||||||
@@ -558,11 +569,25 @@ public class JadxArgs implements Closeable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isExportAsGradleProject() {
|
public boolean isExportAsGradleProject() {
|
||||||
return exportAsGradleProject;
|
return exportGradleType != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setExportAsGradleProject(boolean exportAsGradleProject) {
|
public void setExportAsGradleProject(boolean exportAsGradleProject) {
|
||||||
this.exportAsGradleProject = 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() {
|
public boolean isRestoreSwitchOverString() {
|
||||||
@@ -793,6 +818,14 @@ public class JadxArgs implements Closeable {
|
|||||||
this.loadJadxClsSetFile = loadJadxClsSetFile;
|
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
|
* Hash of all options that can change result code
|
||||||
*/
|
*/
|
||||||
@@ -800,8 +833,8 @@ public class JadxArgs implements Closeable {
|
|||||||
String argStr = "args:" + decompilationMode + useImports + showInconsistentCode
|
String argStr = "args:" + decompilationMode + useImports + showInconsistentCode
|
||||||
+ inlineAnonymousClasses + inlineMethods + moveInnerClasses + allowInlineKotlinLambda
|
+ inlineAnonymousClasses + inlineMethods + moveInnerClasses + allowInlineKotlinLambda
|
||||||
+ deobfuscationOn + deobfuscationMinLength + deobfuscationMaxLength + deobfuscationWhitelist
|
+ deobfuscationOn + deobfuscationMinLength + deobfuscationMaxLength + deobfuscationWhitelist
|
||||||
+ useSourceNameAsClassNameAlias
|
+ useSourceNameAsClassNameAlias + sourceNameRepeatLimit
|
||||||
+ resourceNameSource
|
+ resourceNameSource + useHeadersForDetectResourceExtensions
|
||||||
+ useKotlinMethodsForVarNames
|
+ useKotlinMethodsForVarNames
|
||||||
+ insertDebugLines + extractFinally
|
+ insertDebugLines + extractFinally
|
||||||
+ debugInfo + escapeUnicode + replaceConsts + restoreSwitchOverString
|
+ debugInfo + escapeUnicode + replaceConsts + restoreSwitchOverString
|
||||||
@@ -841,6 +874,7 @@ public class JadxArgs implements Closeable {
|
|||||||
+ ", generatedRenamesMappingFileMode=" + generatedRenamesMappingFileMode
|
+ ", generatedRenamesMappingFileMode=" + generatedRenamesMappingFileMode
|
||||||
+ ", resourceNameSource=" + resourceNameSource
|
+ ", resourceNameSource=" + resourceNameSource
|
||||||
+ ", useSourceNameAsClassNameAlias=" + useSourceNameAsClassNameAlias
|
+ ", useSourceNameAsClassNameAlias=" + useSourceNameAsClassNameAlias
|
||||||
|
+ ", sourceNameRepeatLimit=" + sourceNameRepeatLimit
|
||||||
+ ", useKotlinMethodsForVarNames=" + useKotlinMethodsForVarNames
|
+ ", useKotlinMethodsForVarNames=" + useKotlinMethodsForVarNames
|
||||||
+ ", insertDebugLines=" + insertDebugLines
|
+ ", insertDebugLines=" + insertDebugLines
|
||||||
+ ", extractFinally=" + extractFinally
|
+ ", extractFinally=" + extractFinally
|
||||||
@@ -851,7 +885,7 @@ public class JadxArgs implements Closeable {
|
|||||||
+ ", replaceConsts=" + replaceConsts
|
+ ", replaceConsts=" + replaceConsts
|
||||||
+ ", restoreSwitchOverString=" + restoreSwitchOverString
|
+ ", restoreSwitchOverString=" + restoreSwitchOverString
|
||||||
+ ", respectBytecodeAccModifiers=" + respectBytecodeAccModifiers
|
+ ", respectBytecodeAccModifiers=" + respectBytecodeAccModifiers
|
||||||
+ ", exportAsGradleProject=" + exportAsGradleProject
|
+ ", exportGradleType=" + exportGradleType
|
||||||
+ ", skipXmlPrettyPrint=" + skipXmlPrettyPrint
|
+ ", skipXmlPrettyPrint=" + skipXmlPrettyPrint
|
||||||
+ ", fsCaseSensitive=" + fsCaseSensitive
|
+ ", fsCaseSensitive=" + fsCaseSensitive
|
||||||
+ ", renameFlags=" + renameFlags
|
+ ", renameFlags=" + renameFlags
|
||||||
@@ -863,6 +897,7 @@ public class JadxArgs implements Closeable {
|
|||||||
+ ", pluginOptions=" + pluginOptions
|
+ ", pluginOptions=" + pluginOptions
|
||||||
+ ", cfgOutput=" + cfgOutput
|
+ ", cfgOutput=" + cfgOutput
|
||||||
+ ", rawCFGOutput=" + rawCFGOutput
|
+ ", rawCFGOutput=" + rawCFGOutput
|
||||||
|
+ ", useHeadersForDetectResourceExtensions=" + useHeadersForDetectResourceExtensions
|
||||||
+ '}';
|
+ '}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@@ -42,7 +43,8 @@ import jadx.core.dex.nodes.MethodNode;
|
|||||||
import jadx.core.dex.nodes.PackageNode;
|
import jadx.core.dex.nodes.PackageNode;
|
||||||
import jadx.core.dex.nodes.RootNode;
|
import jadx.core.dex.nodes.RootNode;
|
||||||
import jadx.core.dex.visitors.SaveCode;
|
import jadx.core.dex.visitors.SaveCode;
|
||||||
import jadx.core.export.ExportGradleTask;
|
import jadx.core.export.ExportGradle;
|
||||||
|
import jadx.core.export.OutDirs;
|
||||||
import jadx.core.plugins.JadxPluginManager;
|
import jadx.core.plugins.JadxPluginManager;
|
||||||
import jadx.core.plugins.PluginContext;
|
import jadx.core.plugins.PluginContext;
|
||||||
import jadx.core.plugins.events.JadxEventsImpl;
|
import jadx.core.plugins.events.JadxEventsImpl;
|
||||||
@@ -50,9 +52,9 @@ import jadx.core.utils.DecompilerScheduler;
|
|||||||
import jadx.core.utils.Utils;
|
import jadx.core.utils.Utils;
|
||||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||||
import jadx.core.utils.files.FileUtils;
|
import jadx.core.utils.files.FileUtils;
|
||||||
import jadx.core.utils.files.ZipPatch;
|
|
||||||
import jadx.core.utils.tasks.TaskExecutor;
|
import jadx.core.utils.tasks.TaskExecutor;
|
||||||
import jadx.core.xmlgen.ResourcesSaver;
|
import jadx.core.xmlgen.ResourcesSaver;
|
||||||
|
import jadx.zip.ZipReader;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Jadx API usage example:
|
* Jadx API usage example:
|
||||||
@@ -87,6 +89,7 @@ public final class JadxDecompiler implements Closeable {
|
|||||||
private final JadxArgs args;
|
private final JadxArgs args;
|
||||||
private final JadxPluginManager pluginManager;
|
private final JadxPluginManager pluginManager;
|
||||||
private final List<ICodeLoader> loadedInputs = new ArrayList<>();
|
private final List<ICodeLoader> loadedInputs = new ArrayList<>();
|
||||||
|
private final ZipReader zipReader;
|
||||||
|
|
||||||
private RootNode root;
|
private RootNode root;
|
||||||
private List<JavaClass> classes;
|
private List<JavaClass> classes;
|
||||||
@@ -98,6 +101,7 @@ public final class JadxDecompiler implements Closeable {
|
|||||||
private final List<ICodeLoader> customCodeLoaders = new ArrayList<>();
|
private final List<ICodeLoader> customCodeLoaders = new ArrayList<>();
|
||||||
private final List<CustomResourcesLoader> customResourcesLoaders = new ArrayList<>();
|
private final List<CustomResourcesLoader> customResourcesLoaders = new ArrayList<>();
|
||||||
private final Map<JadxPassType, List<JadxPass>> customPasses = new HashMap<>();
|
private final Map<JadxPassType, List<JadxPass>> customPasses = new HashMap<>();
|
||||||
|
private final List<Closeable> closeableList = new ArrayList<>();
|
||||||
|
|
||||||
private IJadxEvents events = new JadxEventsImpl();
|
private IJadxEvents events = new JadxEventsImpl();
|
||||||
|
|
||||||
@@ -109,6 +113,7 @@ public final class JadxDecompiler implements Closeable {
|
|||||||
this.args = Objects.requireNonNull(args);
|
this.args = Objects.requireNonNull(args);
|
||||||
this.pluginManager = new JadxPluginManager(this);
|
this.pluginManager = new JadxPluginManager(this);
|
||||||
this.resourcesLoader = new ResourcesLoader(this);
|
this.resourcesLoader = new ResourcesLoader(this);
|
||||||
|
this.zipReader = new ZipReader(args.getSecurity());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void load() {
|
public void load() {
|
||||||
@@ -119,13 +124,15 @@ public final class JadxDecompiler implements Closeable {
|
|||||||
loadPlugins();
|
loadPlugins();
|
||||||
loadInputFiles();
|
loadInputFiles();
|
||||||
|
|
||||||
root = new RootNode(args);
|
root = new RootNode(this);
|
||||||
root.init();
|
root.init();
|
||||||
root.setDecompilerRef(this);
|
// load classes and resources
|
||||||
root.mergePasses(customPasses);
|
|
||||||
root.loadClasses(loadedInputs);
|
root.loadClasses(loadedInputs);
|
||||||
root.initClassPath();
|
|
||||||
root.loadResources(resourcesLoader, getResources());
|
root.loadResources(resourcesLoader, getResources());
|
||||||
|
root.finishClassLoad();
|
||||||
|
root.initClassPath();
|
||||||
|
// init passes
|
||||||
|
root.mergePasses(customPasses);
|
||||||
root.runPreDecompileStage();
|
root.runPreDecompileStage();
|
||||||
root.initPasses();
|
root.initPasses();
|
||||||
loadFinished();
|
loadFinished();
|
||||||
@@ -145,9 +152,7 @@ public final class JadxDecompiler implements Closeable {
|
|||||||
|
|
||||||
private void loadInputFiles() {
|
private void loadInputFiles() {
|
||||||
loadedInputs.clear();
|
loadedInputs.clear();
|
||||||
List<File> inputs = ZipPatch.patchZipFiles(args.getInputFiles());
|
List<Path> inputPaths = Utils.collectionMap(args.getInputFiles(), File::toPath);
|
||||||
args.setInputFiles(inputs);
|
|
||||||
List<Path> inputPaths = Utils.collectionMap(inputs, File::toPath);
|
|
||||||
List<Path> inputFiles = FileUtils.expandDirs(inputPaths);
|
List<Path> inputFiles = FileUtils.expandDirs(inputPaths);
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
for (PluginContext plugin : pluginManager.getResolvedPluginContexts()) {
|
for (PluginContext plugin : pluginManager.getResolvedPluginContexts()) {
|
||||||
@@ -158,7 +163,7 @@ public final class JadxDecompiler implements Closeable {
|
|||||||
loadedInputs.add(loader);
|
loadedInputs.add(loader);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new JadxRuntimeException("Failed to load code for plugin: " + plugin, e);
|
LOG.warn("Failed to load code for plugin: {}", plugin, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,6 +174,7 @@ public final class JadxDecompiler implements Closeable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void reset() {
|
private void reset() {
|
||||||
|
unloadPlugins();
|
||||||
root = null;
|
root = null;
|
||||||
classes = null;
|
classes = null;
|
||||||
resources = null;
|
resources = null;
|
||||||
@@ -178,32 +184,27 @@ public final class JadxDecompiler implements Closeable {
|
|||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
reset();
|
reset();
|
||||||
closeInputs();
|
closeAll(loadedInputs);
|
||||||
closeLoaders();
|
closeAll(customCodeLoaders);
|
||||||
|
closeAll(customResourcesLoaders);
|
||||||
|
closeAll(closeableList);
|
||||||
|
FileUtils.deleteDirIfExists(args.getFilesGetter().getTempDir());
|
||||||
args.close();
|
args.close();
|
||||||
FileUtils.clearTempRootDir();
|
FileUtils.clearTempRootDir();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void closeInputs() {
|
private void closeAll(List<? extends Closeable> list) {
|
||||||
loadedInputs.forEach(load -> {
|
try {
|
||||||
try {
|
for (Closeable closeable : list) {
|
||||||
load.close();
|
try {
|
||||||
} catch (Exception e) {
|
closeable.close();
|
||||||
LOG.error("Failed to close input", e);
|
} catch (Exception e) {
|
||||||
}
|
LOG.warn("Fail to close '{}'", closeable, 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() {
|
private void loadPlugins() {
|
||||||
@@ -220,6 +221,10 @@ public final class JadxDecompiler implements Closeable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void unloadPlugins() {
|
||||||
|
pluginManager.unloadResolved();
|
||||||
|
}
|
||||||
|
|
||||||
private void loadFinished() {
|
private void loadFinished() {
|
||||||
LOG.debug("Load finished");
|
LOG.debug("Load finished");
|
||||||
List<JadxPass> list = customPasses.get(JadxAfterLoadPass.TYPE);
|
List<JadxPass> list = customPasses.get(JadxAfterLoadPass.TYPE);
|
||||||
@@ -297,31 +302,28 @@ public final class JadxDecompiler implements Closeable {
|
|||||||
if (root == null) {
|
if (root == null) {
|
||||||
throw new JadxRuntimeException("No loaded files");
|
throw new JadxRuntimeException("No loaded files");
|
||||||
}
|
}
|
||||||
File sourcesOutDir;
|
OutDirs outDirs;
|
||||||
File resOutDir;
|
ExportGradle gradleExport;
|
||||||
ExportGradleTask gradleExportTask;
|
if (args.getExportGradleType() != null) {
|
||||||
if (args.isExportAsGradleProject()) {
|
gradleExport = new ExportGradle(root, args.getOutDir(), getResources());
|
||||||
gradleExportTask = new ExportGradleTask(resources, root, args.getOutDir());
|
outDirs = gradleExport.init();
|
||||||
gradleExportTask.init();
|
|
||||||
sourcesOutDir = gradleExportTask.getSrcOutDir();
|
|
||||||
resOutDir = gradleExportTask.getResOutDir();
|
|
||||||
} else {
|
} else {
|
||||||
sourcesOutDir = args.getOutDirSrc();
|
gradleExport = null;
|
||||||
resOutDir = args.getOutDirRes();
|
outDirs = new OutDirs(args.getOutDirSrc(), args.getOutDirRes());
|
||||||
gradleExportTask = null;
|
outDirs.makeDirs();
|
||||||
}
|
}
|
||||||
|
|
||||||
TaskExecutor executor = new TaskExecutor();
|
TaskExecutor executor = new TaskExecutor();
|
||||||
executor.setThreadsCount(args.getThreadsCount());
|
executor.setThreadsCount(args.getThreadsCount());
|
||||||
if (saveResources) {
|
if (saveResources) {
|
||||||
// save resources first because decompilation can stop or fail
|
// save resources first because decompilation can stop or fail
|
||||||
appendResourcesSaveTasks(executor, resOutDir);
|
appendResourcesSaveTasks(executor, outDirs.getResOutDir());
|
||||||
}
|
}
|
||||||
if (saveSources) {
|
if (saveSources) {
|
||||||
appendSourcesSave(executor, sourcesOutDir);
|
appendSourcesSave(executor, outDirs.getSrcOutDir());
|
||||||
}
|
}
|
||||||
if (gradleExportTask != null) {
|
if (gradleExport != null) {
|
||||||
executor.addSequentialTask(gradleExportTask);
|
executor.addSequentialTask(gradleExport::generateGradleFiles);
|
||||||
}
|
}
|
||||||
return executor;
|
return executor;
|
||||||
}
|
}
|
||||||
@@ -333,13 +335,15 @@ public final class JadxDecompiler implements Closeable {
|
|||||||
// process AndroidManifest.xml first to load complete resource ids table
|
// process AndroidManifest.xml first to load complete resource ids table
|
||||||
for (ResourceFile resourceFile : getResources()) {
|
for (ResourceFile resourceFile : getResources()) {
|
||||||
if (resourceFile.getType() == ResourceType.MANIFEST) {
|
if (resourceFile.getType() == ResourceType.MANIFEST) {
|
||||||
new ResourcesSaver(outDir, resourceFile).run();
|
new ResourcesSaver(this, outDir, resourceFile).run();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Set<String> inputFileNames = args.getInputFiles().stream()
|
Set<String> inputFileNames = args.getInputFiles().stream()
|
||||||
.map(File::getAbsolutePath)
|
.map(File::getAbsolutePath)
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
|
Set<String> codeSources = collectCodeSources();
|
||||||
|
|
||||||
List<Runnable> tasks = new ArrayList<>();
|
List<Runnable> tasks = new ArrayList<>();
|
||||||
for (ResourceFile resourceFile : getResources()) {
|
for (ResourceFile resourceFile : getResources()) {
|
||||||
ResourceType resType = resourceFile.getType();
|
ResourceType resType = resourceFile.getType();
|
||||||
@@ -347,16 +351,44 @@ public final class JadxDecompiler implements Closeable {
|
|||||||
// already processed
|
// already processed
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (resType != ResourceType.ARSC
|
String resOriginalName = resourceFile.getOriginalName();
|
||||||
&& inputFileNames.contains(resourceFile.getOriginalName())) {
|
if (resType != ResourceType.ARSC && inputFileNames.contains(resOriginalName)) {
|
||||||
// ignore resource made from input file
|
// ignore resource made from an input file
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
tasks.add(new ResourcesSaver(outDir, resourceFile));
|
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));
|
||||||
}
|
}
|
||||||
executor.addParallelTasks(tasks);
|
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) {
|
private void appendSourcesSave(ITaskExecutor executor, File outDir) {
|
||||||
List<JavaClass> classes = getClasses();
|
List<JavaClass> classes = getClasses();
|
||||||
List<JavaClass> processQueue = filterClasses(classes);
|
List<JavaClass> processQueue = filterClasses(classes);
|
||||||
@@ -587,6 +619,8 @@ public final class JadxDecompiler implements Closeable {
|
|||||||
return convertMethodNode((MethodNode) ann);
|
return convertMethodNode((MethodNode) ann);
|
||||||
case FIELD:
|
case FIELD:
|
||||||
return convertFieldNode((FieldNode) ann);
|
return convertFieldNode((FieldNode) ann);
|
||||||
|
case PKG:
|
||||||
|
return convertPackageNode((PackageNode) ann);
|
||||||
case DECLARATION:
|
case DECLARATION:
|
||||||
return getJavaNodeByCodeAnnotation(codeInfo, ((NodeDeclareRef) ann).getNode());
|
return getJavaNodeByCodeAnnotation(codeInfo, ((NodeDeclareRef) ann).getNode());
|
||||||
case VAR:
|
case VAR:
|
||||||
@@ -700,6 +734,14 @@ public final class JadxDecompiler implements Closeable {
|
|||||||
return resourcesLoader;
|
return resourcesLoader;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ZipReader getZipReader() {
|
||||||
|
return zipReader;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addCloseable(Closeable closeable) {
|
||||||
|
closeableList.add(closeable);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "jadx decompiler " + getVersion();
|
return "jadx decompiler " + getVersion();
|
||||||
|
|||||||
@@ -122,8 +122,6 @@ public final class JavaClass implements JavaNode {
|
|||||||
if (listsLoaded) {
|
if (listsLoaded) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
listsLoaded = true;
|
|
||||||
|
|
||||||
ICodeInfo code;
|
ICodeInfo code;
|
||||||
if (cls.getState().isProcessComplete()) {
|
if (cls.getState().isProcessComplete()) {
|
||||||
// already decompiled -> class internals loaded
|
// already decompiled -> class internals loaded
|
||||||
@@ -131,7 +129,12 @@ public final class JavaClass implements JavaNode {
|
|||||||
} else {
|
} else {
|
||||||
code = cls.decompile();
|
code = cls.decompile();
|
||||||
}
|
}
|
||||||
|
loadLists();
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadLists() {
|
||||||
|
listsLoaded = true;
|
||||||
JadxDecompiler rootDecompiler = getRootDecompiler();
|
JadxDecompiler rootDecompiler = getRootDecompiler();
|
||||||
int inClsCount = cls.getInnerClasses().size();
|
int inClsCount = cls.getInnerClasses().size();
|
||||||
if (inClsCount != 0) {
|
if (inClsCount != 0) {
|
||||||
@@ -139,7 +142,7 @@ public final class JavaClass implements JavaNode {
|
|||||||
for (ClassNode inner : cls.getInnerClasses()) {
|
for (ClassNode inner : cls.getInnerClasses()) {
|
||||||
if (!inner.contains(AFlag.DONT_GENERATE)) {
|
if (!inner.contains(AFlag.DONT_GENERATE)) {
|
||||||
JavaClass javaClass = rootDecompiler.convertClassNode(inner);
|
JavaClass javaClass = rootDecompiler.convertClassNode(inner);
|
||||||
javaClass.load();
|
javaClass.loadLists();
|
||||||
list.add(javaClass);
|
list.add(javaClass);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,7 +153,7 @@ public final class JavaClass implements JavaNode {
|
|||||||
List<JavaClass> list = new ArrayList<>(inlinedClsCount);
|
List<JavaClass> list = new ArrayList<>(inlinedClsCount);
|
||||||
for (ClassNode inner : cls.getInlinedClasses()) {
|
for (ClassNode inner : cls.getInlinedClasses()) {
|
||||||
JavaClass javaClass = rootDecompiler.convertClassNode(inner);
|
JavaClass javaClass = rootDecompiler.convertClassNode(inner);
|
||||||
javaClass.load();
|
javaClass.loadLists();
|
||||||
list.add(javaClass);
|
list.add(javaClass);
|
||||||
}
|
}
|
||||||
this.inlinedClasses = Collections.unmodifiableList(list);
|
this.inlinedClasses = Collections.unmodifiableList(list);
|
||||||
@@ -178,7 +181,6 @@ public final class JavaClass implements JavaNode {
|
|||||||
mths.sort(Comparator.comparing(JavaMethod::getName));
|
mths.sort(Comparator.comparing(JavaMethod::getName));
|
||||||
this.methods = Collections.unmodifiableList(mths);
|
this.methods = Collections.unmodifiableList(mths);
|
||||||
}
|
}
|
||||||
return code;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
JadxDecompiler getRootDecompiler() {
|
JadxDecompiler getRootDecompiler() {
|
||||||
|
|||||||
@@ -2,39 +2,21 @@ package jadx.api;
|
|||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
||||||
import jadx.api.plugins.utils.ZipSecurity;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import jadx.core.deobf.FileTypeDetector;
|
||||||
|
import jadx.core.utils.StringUtils;
|
||||||
|
import jadx.core.utils.exceptions.JadxException;
|
||||||
import jadx.core.xmlgen.ResContainer;
|
import jadx.core.xmlgen.ResContainer;
|
||||||
import jadx.core.xmlgen.entry.ResourceEntry;
|
import jadx.core.xmlgen.entry.ResourceEntry;
|
||||||
|
import jadx.zip.IZipEntry;
|
||||||
|
|
||||||
public class ResourceFile {
|
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 JadxDecompiler decompiler;
|
||||||
private final String name;
|
private final String name;
|
||||||
private final ResourceType type;
|
private ResourceType type;
|
||||||
private ZipRef zipRef;
|
|
||||||
|
private @Nullable IZipEntry zipEntry;
|
||||||
private String deobfName;
|
private String deobfName;
|
||||||
|
|
||||||
public static ResourceFile createResourceFile(JadxDecompiler decompiler, File file, ResourceType type) {
|
public static ResourceFile createResourceFile(JadxDecompiler decompiler, File file, ResourceType type) {
|
||||||
@@ -42,7 +24,7 @@ public class ResourceFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static ResourceFile createResourceFile(JadxDecompiler decompiler, String name, ResourceType type) {
|
public static ResourceFile createResourceFile(JadxDecompiler decompiler, String name, ResourceType type) {
|
||||||
if (!ZipSecurity.isValidZipEntryName(name)) {
|
if (!decompiler.getArgs().getSecurity().isValidEntryName(name)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return new ResourceFile(decompiler, name, type);
|
return new ResourceFile(decompiler, name, type);
|
||||||
@@ -74,28 +56,73 @@ public class ResourceFile {
|
|||||||
return ResourcesLoader.loadContent(decompiler, this);
|
return ResourcesLoader.loadContent(decompiler, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setZipRef(ZipRef zipRef) {
|
public boolean setAlias(ResourceEntry entry, boolean useHeaders) {
|
||||||
this.zipRef = zipRef;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean setAlias(ResourceEntry ri) {
|
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
sb.append("res/").append(ri.getTypeName()).append(ri.getConfig());
|
sb.append("res/").append(entry.getTypeName()).append(entry.getConfig());
|
||||||
sb.append("/").append(ri.getKeyName());
|
sb.append("/").append(entry.getKeyName());
|
||||||
int lastDot = name.lastIndexOf('.');
|
|
||||||
if (lastDot != -1) {
|
if (useHeaders) {
|
||||||
sb.append(name.substring(lastDot));
|
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));
|
||||||
}
|
}
|
||||||
String alias = sb.toString();
|
String alias = sb.toString();
|
||||||
if (!alias.equals(name)) {
|
if (!alias.equals(name)) {
|
||||||
setDeobfName(alias);
|
setDeobfName(alias);
|
||||||
|
type = ResourceType.getFileType(alias);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ZipRef getZipRef() {
|
private String getExtFromName(String name) {
|
||||||
return zipRef;
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
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,9 +10,15 @@ public enum ResourceType {
|
|||||||
CODE(".dex", ".jar", ".class"),
|
CODE(".dex", ".jar", ".class"),
|
||||||
XML(".xml"),
|
XML(".xml"),
|
||||||
ARSC(".arsc"),
|
ARSC(".arsc"),
|
||||||
FONT(".ttf", ".otf"),
|
APK(".apk", ".apkm", ".apks"),
|
||||||
IMG(".png", ".gif", ".jpg"),
|
FONT(".ttf", ".ttc", ".otf"),
|
||||||
MEDIA(".mp3", ".wav"),
|
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"),
|
||||||
LIB(".so"),
|
LIB(".so"),
|
||||||
MANIFEST,
|
MANIFEST,
|
||||||
UNKNOWN;
|
UNKNOWN;
|
||||||
|
|||||||
@@ -6,31 +6,30 @@ import java.io.File;
|
|||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.zip.ZipEntry;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import jadx.api.ResourceFile.ZipRef;
|
|
||||||
import jadx.api.impl.SimpleCodeInfo;
|
import jadx.api.impl.SimpleCodeInfo;
|
||||||
import jadx.api.plugins.CustomResourcesLoader;
|
import jadx.api.plugins.CustomResourcesLoader;
|
||||||
import jadx.api.plugins.resources.IResContainerFactory;
|
import jadx.api.plugins.resources.IResContainerFactory;
|
||||||
import jadx.api.plugins.resources.IResTableParserProvider;
|
import jadx.api.plugins.resources.IResTableParserProvider;
|
||||||
import jadx.api.plugins.resources.IResourcesLoader;
|
import jadx.api.plugins.resources.IResourcesLoader;
|
||||||
import jadx.api.plugins.utils.ZipSecurity;
|
|
||||||
import jadx.core.dex.nodes.RootNode;
|
import jadx.core.dex.nodes.RootNode;
|
||||||
import jadx.core.utils.Utils;
|
import jadx.core.utils.Utils;
|
||||||
import jadx.core.utils.android.Res9patchStreamDecoder;
|
import jadx.core.utils.android.Res9patchStreamDecoder;
|
||||||
import jadx.core.utils.exceptions.JadxException;
|
import jadx.core.utils.exceptions.JadxException;
|
||||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||||
import jadx.core.utils.files.FileUtils;
|
import jadx.core.utils.files.FileUtils;
|
||||||
import jadx.core.utils.files.ZipFile;
|
|
||||||
import jadx.core.xmlgen.BinaryXMLParser;
|
import jadx.core.xmlgen.BinaryXMLParser;
|
||||||
import jadx.core.xmlgen.IResTableParser;
|
import jadx.core.xmlgen.IResTableParser;
|
||||||
import jadx.core.xmlgen.ResContainer;
|
import jadx.core.xmlgen.ResContainer;
|
||||||
import jadx.core.xmlgen.ResTableBinaryParserProvider;
|
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.READ_BUFFER_SIZE;
|
||||||
import static jadx.core.utils.files.FileUtils.copyStream;
|
import static jadx.core.utils.files.FileUtils.copyStream;
|
||||||
@@ -39,21 +38,21 @@ import static jadx.core.utils.files.FileUtils.copyStream;
|
|||||||
public final class ResourcesLoader implements IResourcesLoader {
|
public final class ResourcesLoader implements IResourcesLoader {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(ResourcesLoader.class);
|
private static final Logger LOG = LoggerFactory.getLogger(ResourcesLoader.class);
|
||||||
|
|
||||||
private final JadxDecompiler jadxRef;
|
private final JadxDecompiler decompiler;
|
||||||
|
|
||||||
private final List<IResTableParserProvider> resTableParserProviders = new ArrayList<>();
|
private final List<IResTableParserProvider> resTableParserProviders = new ArrayList<>();
|
||||||
private final List<IResContainerFactory> resContainerFactories = new ArrayList<>();
|
private final List<IResContainerFactory> resContainerFactories = new ArrayList<>();
|
||||||
|
|
||||||
private BinaryXMLParser binaryXmlParser;
|
private BinaryXMLParser binaryXmlParser;
|
||||||
|
|
||||||
ResourcesLoader(JadxDecompiler jadxRef) {
|
ResourcesLoader(JadxDecompiler decompiler) {
|
||||||
this.jadxRef = jadxRef;
|
this.decompiler = decompiler;
|
||||||
this.resTableParserProviders.add(new ResTableBinaryParserProvider());
|
this.resTableParserProviders.add(new ResTableBinaryParserProvider());
|
||||||
}
|
}
|
||||||
|
|
||||||
List<ResourceFile> load(RootNode root) {
|
List<ResourceFile> load(RootNode root) {
|
||||||
init(root);
|
init(root);
|
||||||
List<File> inputFiles = jadxRef.getArgs().getInputFiles();
|
List<File> inputFiles = decompiler.getArgs().getInputFiles();
|
||||||
List<ResourceFile> list = new ArrayList<>(inputFiles.size());
|
List<ResourceFile> list = new ArrayList<>(inputFiles.size());
|
||||||
for (File file : inputFiles) {
|
for (File file : inputFiles) {
|
||||||
loadFile(list, file);
|
loadFile(list, file);
|
||||||
@@ -94,28 +93,19 @@ public final class ResourcesLoader implements IResourcesLoader {
|
|||||||
|
|
||||||
public static <T> T decodeStream(ResourceFile rf, ResourceDecoder<T> decoder) throws JadxException {
|
public static <T> T decodeStream(ResourceFile rf, ResourceDecoder<T> decoder) throws JadxException {
|
||||||
try {
|
try {
|
||||||
ZipRef zipRef = rf.getZipRef();
|
IZipEntry zipEntry = rf.getZipEntry();
|
||||||
if (zipRef == null) {
|
if (zipEntry != null) {
|
||||||
|
try (InputStream inputStream = zipEntry.getInputStream()) {
|
||||||
|
return decoder.decode(zipEntry.getUncompressedSize(), inputStream);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
File file = new File(rf.getOriginalName());
|
File file = new File(rf.getOriginalName());
|
||||||
try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) {
|
try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) {
|
||||||
return decoder.decode(file.length(), inputStream);
|
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) {
|
} catch (Exception e) {
|
||||||
throw new JadxException("Error decode: " + rf.getDeobfName(), e);
|
throw new JadxException("Error decode: " + rf.getOriginalName(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,12 +160,13 @@ public final class ResourcesLoader implements IResourcesLoader {
|
|||||||
if (parser == null) {
|
if (parser == null) {
|
||||||
throw new JadxRuntimeException("Unknown type of resource file: " + resFile.getOriginalName());
|
throw new JadxRuntimeException("Unknown type of resource file: " + resFile.getOriginalName());
|
||||||
}
|
}
|
||||||
|
parser.setBaseFileName(resFile.getDeobfName());
|
||||||
parser.decode(is);
|
parser.decode(is);
|
||||||
return parser;
|
return parser;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ResContainer decodeImage(ResourceFile rf, InputStream inputStream) {
|
private static ResContainer decodeImage(ResourceFile rf, InputStream inputStream) {
|
||||||
String name = rf.getOriginalName();
|
String name = rf.getDeobfName();
|
||||||
if (name.endsWith(".9.png")) {
|
if (name.endsWith(".9.png")) {
|
||||||
try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
|
try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
|
||||||
Res9patchStreamDecoder decoder = new Res9patchStreamDecoder();
|
Res9patchStreamDecoder decoder = new Res9patchStreamDecoder();
|
||||||
@@ -195,7 +186,7 @@ public final class ResourcesLoader implements IResourcesLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try to load the resources with a custom loader first
|
// Try to load the resources with a custom loader first
|
||||||
for (CustomResourcesLoader loader : jadxRef.getCustomResourcesLoaders()) {
|
for (CustomResourcesLoader loader : decompiler.getCustomResourcesLoaders()) {
|
||||||
if (loader.load(this, list, file)) {
|
if (loader.load(this, list, file)) {
|
||||||
LOG.debug("Custom loader used for {}", file.getAbsolutePath());
|
LOG.debug("Custom loader used for {}", file.getAbsolutePath());
|
||||||
return;
|
return;
|
||||||
@@ -208,25 +199,31 @@ public final class ResourcesLoader implements IResourcesLoader {
|
|||||||
|
|
||||||
public void defaultLoadFile(List<ResourceFile> list, File file, String subDir) {
|
public void defaultLoadFile(List<ResourceFile> list, File file, String subDir) {
|
||||||
if (FileUtils.isZipFile(file)) {
|
if (FileUtils.isZipFile(file)) {
|
||||||
ZipSecurity.visitZipEntries(file, (zipFile, entry) -> {
|
try {
|
||||||
addEntry(list, file, entry, subDir);
|
ZipContent zipContent = decompiler.getZipReader().open(file);
|
||||||
return null;
|
// 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);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ResourceType type = ResourceType.getFileType(file.getAbsolutePath());
|
ResourceType type = ResourceType.getFileType(file.getAbsolutePath());
|
||||||
list.add(ResourceFile.createResourceFile(jadxRef, file, type));
|
list.add(ResourceFile.createResourceFile(decompiler, file, type));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addEntry(List<ResourceFile> list, File zipFile, ZipEntry entry, String subDir) {
|
public void addEntry(List<ResourceFile> list, File zipFile, IZipEntry entry, String subDir) {
|
||||||
if (entry.isDirectory()) {
|
if (entry.isDirectory()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String name = entry.getName();
|
String name = entry.getName();
|
||||||
ResourceType type = ResourceType.getFileType(name);
|
ResourceType type = ResourceType.getFileType(name);
|
||||||
ResourceFile rf = ResourceFile.createResourceFile(jadxRef, subDir + name, type);
|
ResourceFile rf = ResourceFile.createResourceFile(decompiler, subDir + name, type);
|
||||||
if (rf != null) {
|
if (rf != null) {
|
||||||
rf.setZipRef(new ZipRef(zipFile, name));
|
rf.setZipEntry(entry);
|
||||||
list.add(rf);
|
list.add(rf);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -234,12 +231,12 @@ public final class ResourcesLoader implements IResourcesLoader {
|
|||||||
public static ICodeInfo loadToCodeWriter(InputStream is) throws IOException {
|
public static ICodeInfo loadToCodeWriter(InputStream is) throws IOException {
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream(READ_BUFFER_SIZE);
|
ByteArrayOutputStream baos = new ByteArrayOutputStream(READ_BUFFER_SIZE);
|
||||||
copyStream(is, baos);
|
copyStream(is, baos);
|
||||||
return new SimpleCodeInfo(baos.toString("UTF-8"));
|
return new SimpleCodeInfo(baos.toString(StandardCharsets.UTF_8));
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized BinaryXMLParser loadBinaryXmlParser() {
|
private synchronized BinaryXMLParser loadBinaryXmlParser() {
|
||||||
if (binaryXmlParser == null) {
|
if (binaryXmlParser == null) {
|
||||||
binaryXmlParser = new BinaryXMLParser(jadxRef.getRoot());
|
binaryXmlParser = new BinaryXMLParser(decompiler.getRoot());
|
||||||
}
|
}
|
||||||
return binaryXmlParser;
|
return binaryXmlParser;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
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,7 +11,6 @@ import jadx.api.JadxArgs;
|
|||||||
import jadx.api.metadata.ICodeAnnotation;
|
import jadx.api.metadata.ICodeAnnotation;
|
||||||
import jadx.api.metadata.ICodeNodeRef;
|
import jadx.api.metadata.ICodeNodeRef;
|
||||||
import jadx.api.metadata.annotations.NodeDeclareRef;
|
import jadx.api.metadata.annotations.NodeDeclareRef;
|
||||||
import jadx.api.metadata.annotations.VarRef;
|
|
||||||
import jadx.core.utils.StringUtils;
|
import jadx.core.utils.StringUtils;
|
||||||
|
|
||||||
public class AnnotatedCodeWriter extends SimpleCodeWriter implements ICodeWriter {
|
public class AnnotatedCodeWriter extends SimpleCodeWriter implements ICodeWriter {
|
||||||
@@ -151,7 +150,6 @@ public class AnnotatedCodeWriter extends SimpleCodeWriter implements ICodeWriter
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ICodeInfo finish() {
|
public ICodeInfo finish() {
|
||||||
validateAnnotations();
|
|
||||||
String code = buf.toString();
|
String code = buf.toString();
|
||||||
buf = null;
|
buf = null;
|
||||||
return new AnnotatedCodeInfo(code, lineMap, annotations);
|
return new AnnotatedCodeInfo(code, lineMap, annotations);
|
||||||
@@ -161,17 +159,4 @@ public class AnnotatedCodeWriter extends SimpleCodeWriter implements ICodeWriter
|
|||||||
public Map<Integer, ICodeAnnotation> getRawAnnotations() {
|
public Map<Integer, ICodeAnnotation> getRawAnnotations() {
|
||||||
return annotations;
|
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,4 +23,12 @@ public interface JadxPlugin {
|
|||||||
* For long operation, prefer {@link JadxPreparePass} or {@link JadxAfterLoadPass} instead.
|
* For long operation, prefer {@link JadxPreparePass} or {@link JadxAfterLoadPass} instead.
|
||||||
*/
|
*/
|
||||||
void init(JadxPluginContext context);
|
void init(JadxPluginContext context);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin unload handler.
|
||||||
|
* Can be used to clean up resources on plugin unloading.
|
||||||
|
*/
|
||||||
|
default void unload() {
|
||||||
|
// optional method
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import jadx.api.plugins.input.JadxCodeInput;
|
|||||||
import jadx.api.plugins.options.JadxPluginOptions;
|
import jadx.api.plugins.options.JadxPluginOptions;
|
||||||
import jadx.api.plugins.pass.JadxPass;
|
import jadx.api.plugins.pass.JadxPass;
|
||||||
import jadx.api.plugins.resources.IResourcesLoader;
|
import jadx.api.plugins.resources.IResourcesLoader;
|
||||||
|
import jadx.zip.ZipReader;
|
||||||
|
|
||||||
public interface JadxPluginContext {
|
public interface JadxPluginContext {
|
||||||
|
|
||||||
@@ -59,4 +60,9 @@ public interface JadxPluginContext {
|
|||||||
* Access to plugin specific files and directories
|
* Access to plugin specific files and directories
|
||||||
*/
|
*/
|
||||||
IJadxFiles files();
|
IJadxFiles files();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom jadx zip reader to fight tampering and provide additional security checks
|
||||||
|
*/
|
||||||
|
ZipReader getZipReader();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,4 +26,13 @@ public interface ISettingsGroup {
|
|||||||
default List<ISettingsGroup> getSubGroups() {
|
default List<ISettingsGroup> getSubGroups() {
|
||||||
return Collections.emptyList();
|
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,12 +2,16 @@ package jadx.api.plugins.gui;
|
|||||||
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
import javax.swing.ImageIcon;
|
||||||
import javax.swing.JFrame;
|
import javax.swing.JFrame;
|
||||||
import javax.swing.KeyStroke;
|
import javax.swing.KeyStroke;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.ApiStatus;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import jadx.api.gui.tree.ITreeNode;
|
||||||
import jadx.api.metadata.ICodeNodeRef;
|
import jadx.api.metadata.ICodeNodeRef;
|
||||||
|
|
||||||
public interface JadxGuiContext {
|
public interface JadxGuiContext {
|
||||||
@@ -34,6 +38,15 @@ public interface JadxGuiContext {
|
|||||||
@Nullable String keyBinding,
|
@Nullable String keyBinding,
|
||||||
Consumer<ICodeNodeRef> action);
|
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
|
* Attach new key binding to main window
|
||||||
*
|
*
|
||||||
@@ -57,6 +70,16 @@ public interface JadxGuiContext {
|
|||||||
*/
|
*/
|
||||||
JFrame getMainFrame();
|
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 getNodeUnderCaret();
|
||||||
|
|
||||||
ICodeNodeRef getNodeUnderMouse();
|
ICodeNodeRef getNodeUnderMouse();
|
||||||
@@ -72,6 +95,11 @@ public interface JadxGuiContext {
|
|||||||
*/
|
*/
|
||||||
boolean open(ICodeNodeRef ref);
|
boolean open(ICodeNodeRef ref);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open usage dialog for a node
|
||||||
|
*/
|
||||||
|
void openUsageDialog(ICodeNodeRef ref);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reload code in active tab
|
* Reload code in active tab
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -4,63 +4,52 @@ import java.io.BufferedInputStream;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.Enumeration;
|
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
import java.util.function.BiFunction;
|
import java.util.function.Function;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipFile;
|
||||||
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
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.core.utils.Utils;
|
||||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
import jadx.zip.IZipEntry;
|
||||||
import jadx.core.utils.files.ZipFile;
|
import jadx.zip.ZipReader;
|
||||||
|
import jadx.zip.io.LimitedInputStream;
|
||||||
|
import jadx.zip.security.DisabledZipSecurity;
|
||||||
|
import jadx.zip.security.IJadxZipSecurity;
|
||||||
|
import jadx.zip.security.JadxZipSecurity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deprecated, migrate to {@link ZipReader}. <br>
|
||||||
|
* Prefer already configured instance from {@link JadxDecompiler#getZipReader()} or
|
||||||
|
* {@link JadxPluginContext#getZipReader()}.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
public class ZipSecurity {
|
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);
|
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 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 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) {
|
public static boolean isInSubDirectory(File baseDir, File file) {
|
||||||
if (DISABLE_CHECKS) {
|
return ZIP_SECURITY.isInSubDirectory(baseDir, file);
|
||||||
return true;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return isInSubDirectoryInternal(baseDir.getCanonicalFile(), file.getCanonicalFile());
|
|
||||||
} catch (IOException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -68,48 +57,15 @@ public class ZipSecurity {
|
|||||||
* to limit output only to the specified directory
|
* to limit output only to the specified directory
|
||||||
*/
|
*/
|
||||||
public static boolean isValidZipEntryName(String entryName) {
|
public static boolean isValidZipEntryName(String entryName) {
|
||||||
if (DISABLE_CHECKS) {
|
return ZIP_SECURITY.isValidEntryName(entryName);
|
||||||
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(ZipEntry entry) {
|
public static boolean isZipBomb(IZipEntry entry) {
|
||||||
if (DISABLE_CHECKS) {
|
return !ZIP_SECURITY.isValidEntry(entry);
|
||||||
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(ZipEntry entry) {
|
public static boolean isValidZipEntry(IZipEntry entry) {
|
||||||
return isValidZipEntryName(entry.getName())
|
return ZIP_SECURITY.isValidEntry(entry);
|
||||||
&& !isZipBomb(entry);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static InputStream getInputStreamForEntry(ZipFile zipFile, ZipEntry entry) throws IOException {
|
public static InputStream getInputStreamForEntry(ZipFile zipFile, ZipEntry entry) throws IOException {
|
||||||
@@ -122,44 +78,15 @@ public class ZipSecurity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Visit valid entries in zip file.
|
* Visit valid entries in a zip file.
|
||||||
* Return not null value from visitor to stop iteration.
|
* Return not null value from visitor to stop iteration.
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
public static <R> R visitZipEntries(File file, BiFunction<ZipFile, ZipEntry, R> visitor) {
|
public static <R> R visitZipEntries(File file, Function<IZipEntry, R> visitor) {
|
||||||
try (ZipFile zip = new ZipFile(file)) {
|
return ZIP_READER.visitEntries(file, visitor);
|
||||||
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<ZipEntry, InputStream> visitor) {
|
public static void readZipEntries(File file, BiConsumer<IZipEntry, InputStream> visitor) {
|
||||||
visitZipEntries(file, (zip, entry) -> {
|
ZIP_READER.readEntries(file, visitor);
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import java.io.InputStream;
|
|||||||
|
|
||||||
import org.w3c.dom.Document;
|
import org.w3c.dom.Document;
|
||||||
|
|
||||||
public interface IJadxSecurity {
|
import jadx.zip.security.IJadxZipSecurity;
|
||||||
|
|
||||||
|
public interface IJadxSecurity extends IJadxZipSecurity {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if application package is safe
|
* Check if application package is safe
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import java.util.Set;
|
|||||||
public enum JadxSecurityFlag {
|
public enum JadxSecurityFlag {
|
||||||
|
|
||||||
VERIFY_APP_PACKAGE,
|
VERIFY_APP_PACKAGE,
|
||||||
SECURE_XML_PARSER;
|
SECURE_XML_PARSER,
|
||||||
|
SECURE_ZIP_READER;
|
||||||
|
|
||||||
public static Set<JadxSecurityFlag> all() {
|
public static Set<JadxSecurityFlag> all() {
|
||||||
return EnumSet.allOf(JadxSecurityFlag.class);
|
return EnumSet.allOf(JadxSecurityFlag.class);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package jadx.api.security.impl;
|
package jadx.api.security.impl;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@@ -12,14 +13,52 @@ import org.w3c.dom.Document;
|
|||||||
import jadx.api.security.IJadxSecurity;
|
import jadx.api.security.IJadxSecurity;
|
||||||
import jadx.api.security.JadxSecurityFlag;
|
import jadx.api.security.JadxSecurityFlag;
|
||||||
import jadx.core.deobf.NameMapper;
|
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 {
|
public class JadxSecurity implements IJadxSecurity {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(JadxSecurity.class);
|
private static final Logger LOG = LoggerFactory.getLogger(JadxSecurity.class);
|
||||||
|
|
||||||
private final Set<JadxSecurityFlag> flags;
|
private final Set<JadxSecurityFlag> flags;
|
||||||
|
private final IJadxZipSecurity zipSecurity;
|
||||||
|
|
||||||
public JadxSecurity(Set<JadxSecurityFlag> flags) {
|
public JadxSecurity(Set<JadxSecurityFlag> flags) {
|
||||||
this.flags = 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
|
@Override
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ public class Consts {
|
|||||||
public static final String CLASS_STRING = "java.lang.String";
|
public static final String CLASS_STRING = "java.lang.String";
|
||||||
public static final String CLASS_CLASS = "java.lang.Class";
|
public static final String CLASS_CLASS = "java.lang.Class";
|
||||||
public static final String CLASS_THROWABLE = "java.lang.Throwable";
|
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_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_ENUM = "java.lang.Enum";
|
||||||
|
|
||||||
public static final String CLASS_STRING_BUILDER = "java.lang.StringBuilder";
|
public static final String CLASS_STRING_BUILDER = "java.lang.StringBuilder";
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import jadx.core.deobf.DeobfuscatorVisitor;
|
|||||||
import jadx.core.deobf.SaveDeobfMapping;
|
import jadx.core.deobf.SaveDeobfMapping;
|
||||||
import jadx.core.dex.attributes.AFlag;
|
import jadx.core.dex.attributes.AFlag;
|
||||||
import jadx.core.dex.visitors.AnonymousClassVisitor;
|
import jadx.core.dex.visitors.AnonymousClassVisitor;
|
||||||
|
import jadx.core.dex.visitors.ApplyVariableNames;
|
||||||
import jadx.core.dex.visitors.AttachCommentsVisitor;
|
import jadx.core.dex.visitors.AttachCommentsVisitor;
|
||||||
import jadx.core.dex.visitors.AttachMethodDetails;
|
import jadx.core.dex.visitors.AttachMethodDetails;
|
||||||
import jadx.core.dex.visitors.AttachTryCatchVisitor;
|
import jadx.core.dex.visitors.AttachTryCatchVisitor;
|
||||||
@@ -35,6 +36,7 @@ import jadx.core.dex.visitors.InitCodeVariables;
|
|||||||
import jadx.core.dex.visitors.InlineMethods;
|
import jadx.core.dex.visitors.InlineMethods;
|
||||||
import jadx.core.dex.visitors.MarkMethodsForInline;
|
import jadx.core.dex.visitors.MarkMethodsForInline;
|
||||||
import jadx.core.dex.visitors.MethodInvokeVisitor;
|
import jadx.core.dex.visitors.MethodInvokeVisitor;
|
||||||
|
import jadx.core.dex.visitors.MethodThrowsVisitor;
|
||||||
import jadx.core.dex.visitors.MethodVisitor;
|
import jadx.core.dex.visitors.MethodVisitor;
|
||||||
import jadx.core.dex.visitors.ModVisitor;
|
import jadx.core.dex.visitors.ModVisitor;
|
||||||
import jadx.core.dex.visitors.MoveInlineVisitor;
|
import jadx.core.dex.visitors.MoveInlineVisitor;
|
||||||
@@ -54,6 +56,7 @@ import jadx.core.dex.visitors.debuginfo.DebugInfoApplyVisitor;
|
|||||||
import jadx.core.dex.visitors.debuginfo.DebugInfoAttachVisitor;
|
import jadx.core.dex.visitors.debuginfo.DebugInfoAttachVisitor;
|
||||||
import jadx.core.dex.visitors.finaly.MarkFinallyVisitor;
|
import jadx.core.dex.visitors.finaly.MarkFinallyVisitor;
|
||||||
import jadx.core.dex.visitors.fixaccessmodifiers.FixAccessModifiers;
|
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.kotlin.ProcessKotlinInternals;
|
||||||
import jadx.core.dex.visitors.prepare.AddAndroidConstants;
|
import jadx.core.dex.visitors.prepare.AddAndroidConstants;
|
||||||
import jadx.core.dex.visitors.prepare.CollectConstValues;
|
import jadx.core.dex.visitors.prepare.CollectConstValues;
|
||||||
@@ -101,7 +104,6 @@ public class Jadx {
|
|||||||
passes.add(new SignatureProcessor());
|
passes.add(new SignatureProcessor());
|
||||||
passes.add(new OverrideMethodVisitor());
|
passes.add(new OverrideMethodVisitor());
|
||||||
passes.add(new AddAndroidConstants());
|
passes.add(new AddAndroidConstants());
|
||||||
passes.add(new CollectConstValues());
|
|
||||||
|
|
||||||
// rename and deobfuscation
|
// rename and deobfuscation
|
||||||
passes.add(new DeobfuscatorVisitor());
|
passes.add(new DeobfuscatorVisitor());
|
||||||
@@ -110,6 +112,7 @@ public class Jadx {
|
|||||||
passes.add(new SaveDeobfMapping());
|
passes.add(new SaveDeobfMapping());
|
||||||
|
|
||||||
passes.add(new UsageInfoVisitor());
|
passes.add(new UsageInfoVisitor());
|
||||||
|
passes.add(new CollectConstValues());
|
||||||
passes.add(new ProcessAnonymous());
|
passes.add(new ProcessAnonymous());
|
||||||
passes.add(new ProcessMethodsForInline());
|
passes.add(new ProcessMethodsForInline());
|
||||||
return passes;
|
return passes;
|
||||||
@@ -179,6 +182,8 @@ public class Jadx {
|
|||||||
passes.add(new ReturnVisitor());
|
passes.add(new ReturnVisitor());
|
||||||
passes.add(new CleanRegions());
|
passes.add(new CleanRegions());
|
||||||
|
|
||||||
|
passes.add(new MethodThrowsVisitor());
|
||||||
|
|
||||||
passes.add(new CodeShrinkVisitor());
|
passes.add(new CodeShrinkVisitor());
|
||||||
passes.add(new MethodInvokeVisitor());
|
passes.add(new MethodInvokeVisitor());
|
||||||
passes.add(new SimplifyVisitor());
|
passes.add(new SimplifyVisitor());
|
||||||
@@ -186,6 +191,7 @@ public class Jadx {
|
|||||||
|
|
||||||
passes.add(new EnumVisitor());
|
passes.add(new EnumVisitor());
|
||||||
passes.add(new FixSwitchOverEnum());
|
passes.add(new FixSwitchOverEnum());
|
||||||
|
passes.add(new NonFinalResIdsVisitor());
|
||||||
passes.add(new ExtractFieldInit());
|
passes.add(new ExtractFieldInit());
|
||||||
passes.add(new FixAccessModifiers());
|
passes.add(new FixAccessModifiers());
|
||||||
passes.add(new ClassModifier());
|
passes.add(new ClassModifier());
|
||||||
@@ -195,6 +201,7 @@ public class Jadx {
|
|||||||
passes.add(new MarkMethodsForInline());
|
passes.add(new MarkMethodsForInline());
|
||||||
}
|
}
|
||||||
passes.add(new ProcessVariables());
|
passes.add(new ProcessVariables());
|
||||||
|
passes.add(new ApplyVariableNames());
|
||||||
passes.add(new PrepareForCodeGen());
|
passes.add(new PrepareForCodeGen());
|
||||||
if (args.isCfgOutput()) {
|
if (args.isCfgOutput()) {
|
||||||
passes.add(DotGraphVisitor.dumpRegions());
|
passes.add(DotGraphVisitor.dumpRegions());
|
||||||
|
|||||||
@@ -347,6 +347,10 @@ public class ClassGen {
|
|||||||
* Additional checks for inlined methods
|
* Additional checks for inlined methods
|
||||||
*/
|
*/
|
||||||
private boolean skipMethod(MethodNode mth) {
|
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);
|
MethodInlineAttr inlineAttr = mth.get(AType.METHOD_INLINE);
|
||||||
if (inlineAttr == null || inlineAttr.notNeeded()) {
|
if (inlineAttr == null || inlineAttr.notNeeded()) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -81,8 +81,9 @@ public class MethodGen {
|
|||||||
|
|
||||||
public boolean addDefinition(ICodeWriter code) {
|
public boolean addDefinition(ICodeWriter code) {
|
||||||
if (mth.getMethodInfo().isClassInit()) {
|
if (mth.getMethodInfo().isClassInit()) {
|
||||||
|
code.startLine();
|
||||||
code.attachDefinition(mth);
|
code.attachDefinition(mth);
|
||||||
code.startLine("static");
|
code.add("static");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (mth.contains(AFlag.ANONYMOUS_CONSTRUCTOR)) {
|
if (mth.contains(AFlag.ANONYMOUS_CONSTRUCTOR)) {
|
||||||
|
|||||||
@@ -2,57 +2,22 @@ package jadx.core.codegen;
|
|||||||
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import jadx.core.Consts;
|
|
||||||
import jadx.core.deobf.NameMapper;
|
import jadx.core.deobf.NameMapper;
|
||||||
import jadx.core.dex.attributes.AFlag;
|
|
||||||
import jadx.core.dex.attributes.nodes.LoopLabelAttr;
|
import jadx.core.dex.attributes.nodes.LoopLabelAttr;
|
||||||
import jadx.core.dex.info.ClassInfo;
|
|
||||||
import jadx.core.dex.info.MethodInfo;
|
|
||||||
import jadx.core.dex.instructions.InvokeNode;
|
|
||||||
import jadx.core.dex.instructions.args.ArgType;
|
|
||||||
import jadx.core.dex.instructions.args.CodeVar;
|
import jadx.core.dex.instructions.args.CodeVar;
|
||||||
import jadx.core.dex.instructions.args.InsnArg;
|
|
||||||
import jadx.core.dex.instructions.args.InsnWrapArg;
|
|
||||||
import jadx.core.dex.instructions.args.NamedArg;
|
import jadx.core.dex.instructions.args.NamedArg;
|
||||||
import jadx.core.dex.instructions.args.RegisterArg;
|
import jadx.core.dex.instructions.args.RegisterArg;
|
||||||
import jadx.core.dex.instructions.args.SSAVar;
|
import jadx.core.dex.instructions.args.SSAVar;
|
||||||
import jadx.core.dex.instructions.mods.ConstructorInsn;
|
|
||||||
import jadx.core.dex.nodes.ClassNode;
|
import jadx.core.dex.nodes.ClassNode;
|
||||||
import jadx.core.dex.nodes.FieldNode;
|
import jadx.core.dex.nodes.FieldNode;
|
||||||
import jadx.core.dex.nodes.InsnNode;
|
|
||||||
import jadx.core.dex.nodes.MethodNode;
|
import jadx.core.dex.nodes.MethodNode;
|
||||||
import jadx.core.utils.StringUtils;
|
|
||||||
import jadx.core.utils.Utils;
|
|
||||||
|
|
||||||
public class NameGen {
|
public class NameGen {
|
||||||
|
|
||||||
private static final Map<String, String> OBJ_ALIAS;
|
|
||||||
|
|
||||||
private final Set<String> varNames = new HashSet<>();
|
|
||||||
private final MethodNode mth;
|
private final MethodNode mth;
|
||||||
private final boolean fallback;
|
private final boolean fallback;
|
||||||
|
private final Set<String> varNames = new HashSet<>();
|
||||||
static {
|
|
||||||
OBJ_ALIAS = Utils.newConstStringMap(
|
|
||||||
Consts.CLASS_STRING, "str",
|
|
||||||
Consts.CLASS_CLASS, "cls",
|
|
||||||
Consts.CLASS_THROWABLE, "th",
|
|
||||||
Consts.CLASS_OBJECT, "obj",
|
|
||||||
"java.util.Iterator", "it",
|
|
||||||
"java.lang.Boolean", "bool",
|
|
||||||
"java.lang.Short", "sh",
|
|
||||||
"java.lang.Integer", "num",
|
|
||||||
"java.lang.Character", "ch",
|
|
||||||
"java.lang.Byte", "b",
|
|
||||||
"java.lang.Float", "f",
|
|
||||||
"java.lang.Long", "l",
|
|
||||||
"java.lang.Double", "d",
|
|
||||||
"java.lang.StringBuilder", "sb",
|
|
||||||
"java.lang.Exception", "exc");
|
|
||||||
}
|
|
||||||
|
|
||||||
public NameGen(MethodNode mth, ClassGen classGen) {
|
public NameGen(MethodNode mth, ClassGen classGen) {
|
||||||
this.mth = mth;
|
this.mth = mth;
|
||||||
@@ -99,9 +64,9 @@ public class NameGen {
|
|||||||
if (fallback) {
|
if (fallback) {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
name = getUniqueVarName(name);
|
String uniqName = getUniqueVarName(name);
|
||||||
arg.setName(name);
|
arg.setName(uniqName);
|
||||||
return name;
|
return uniqName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String useArg(RegisterArg arg) {
|
public String useArg(RegisterArg arg) {
|
||||||
@@ -132,13 +97,10 @@ public class NameGen {
|
|||||||
|
|
||||||
private String makeArgName(CodeVar var) {
|
private String makeArgName(CodeVar var) {
|
||||||
String name = var.getName();
|
String name = var.getName();
|
||||||
if (name == null) {
|
if (NameMapper.isValidAndPrintable(name)) {
|
||||||
name = guessName(var);
|
return name;
|
||||||
}
|
}
|
||||||
if (!NameMapper.isValidAndPrintable(name)) {
|
return getFallbackName(var);
|
||||||
name = getFallbackName(var);
|
|
||||||
}
|
|
||||||
return name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getFallbackName(CodeVar var) {
|
private String getFallbackName(CodeVar var) {
|
||||||
@@ -152,153 +114,4 @@ public class NameGen {
|
|||||||
private String getFallbackName(RegisterArg arg) {
|
private String getFallbackName(RegisterArg arg) {
|
||||||
return "r" + arg.getRegNum();
|
return "r" + arg.getRegNum();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String guessName(CodeVar var) {
|
|
||||||
List<SSAVar> ssaVars = var.getSsaVars();
|
|
||||||
if (ssaVars != null && !ssaVars.isEmpty()) {
|
|
||||||
// TODO: use all vars for better name generation
|
|
||||||
SSAVar ssaVar = ssaVars.get(0);
|
|
||||||
if (ssaVar != null && ssaVar.getName() == null) {
|
|
||||||
RegisterArg assignArg = ssaVar.getAssign();
|
|
||||||
InsnNode assignInsn = assignArg.getParentInsn();
|
|
||||||
if (assignInsn != null) {
|
|
||||||
String name = makeNameFromInsn(assignInsn);
|
|
||||||
if (name != null && NameMapper.isValidAndPrintable(name)) {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return makeNameForType(var.getType());
|
|
||||||
}
|
|
||||||
|
|
||||||
private String makeNameForType(ArgType type) {
|
|
||||||
if (type.isPrimitive()) {
|
|
||||||
return type.getPrimitiveType().getShortName().toLowerCase();
|
|
||||||
}
|
|
||||||
if (type.isArray()) {
|
|
||||||
return makeNameForType(type.getArrayRootElement()) + "Arr";
|
|
||||||
}
|
|
||||||
return makeNameForObject(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String makeNameForObject(ArgType type) {
|
|
||||||
if (type.isGenericType()) {
|
|
||||||
return StringUtils.escape(type.getObject().toLowerCase());
|
|
||||||
}
|
|
||||||
if (type.isObject()) {
|
|
||||||
String alias = getAliasForObject(type.getObject());
|
|
||||||
if (alias != null) {
|
|
||||||
return alias;
|
|
||||||
}
|
|
||||||
return makeNameForCheckedClass(ClassInfo.fromType(mth.root(), type));
|
|
||||||
}
|
|
||||||
return StringUtils.escape(type.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
private String makeNameForCheckedClass(ClassInfo classInfo) {
|
|
||||||
String shortName = classInfo.getAliasShortName();
|
|
||||||
String vName = fromName(shortName);
|
|
||||||
if (vName != null) {
|
|
||||||
return vName;
|
|
||||||
}
|
|
||||||
String lower = StringUtils.escape(shortName.toLowerCase());
|
|
||||||
if (shortName.equals(lower)) {
|
|
||||||
return lower + "Var";
|
|
||||||
}
|
|
||||||
return lower;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String makeNameForClass(ClassInfo classInfo) {
|
|
||||||
String alias = getAliasForObject(classInfo.getFullName());
|
|
||||||
if (alias != null) {
|
|
||||||
return alias;
|
|
||||||
}
|
|
||||||
return makeNameForCheckedClass(classInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String fromName(String name) {
|
|
||||||
if (name == null || name.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (name.toUpperCase().equals(name)) {
|
|
||||||
// all characters are upper case
|
|
||||||
return name.toLowerCase();
|
|
||||||
}
|
|
||||||
String v1 = Character.toLowerCase(name.charAt(0)) + name.substring(1);
|
|
||||||
if (!v1.equals(name)) {
|
|
||||||
return v1;
|
|
||||||
}
|
|
||||||
if (name.length() < 3) {
|
|
||||||
return name + "Var";
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getAliasForObject(String name) {
|
|
||||||
return OBJ_ALIAS.get(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String makeNameFromInsn(InsnNode insn) {
|
|
||||||
switch (insn.getType()) {
|
|
||||||
case INVOKE:
|
|
||||||
InvokeNode inv = (InvokeNode) insn;
|
|
||||||
return makeNameFromInvoke(inv.getCallMth());
|
|
||||||
|
|
||||||
case CONSTRUCTOR:
|
|
||||||
ConstructorInsn co = (ConstructorInsn) insn;
|
|
||||||
MethodNode callMth = mth.root().getMethodUtils().resolveMethod(co);
|
|
||||||
if (callMth != null && callMth.contains(AFlag.ANONYMOUS_CONSTRUCTOR)) {
|
|
||||||
// don't use name of anonymous class
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return makeNameForClass(co.getClassType());
|
|
||||||
|
|
||||||
case ARRAY_LENGTH:
|
|
||||||
return "length";
|
|
||||||
|
|
||||||
case ARITH:
|
|
||||||
case TERNARY:
|
|
||||||
case CAST:
|
|
||||||
for (InsnArg arg : insn.getArguments()) {
|
|
||||||
if (arg.isInsnWrap()) {
|
|
||||||
InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn();
|
|
||||||
String wName = makeNameFromInsn(wrapInsn);
|
|
||||||
if (wName != null) {
|
|
||||||
return wName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String makeNameFromInvoke(MethodInfo callMth) {
|
|
||||||
String name = callMth.getAlias();
|
|
||||||
ClassInfo declClass = callMth.getDeclClass();
|
|
||||||
if ("getInstance".equals(name)) {
|
|
||||||
// e.g. Cipher.getInstance
|
|
||||||
return makeNameForClass(declClass);
|
|
||||||
}
|
|
||||||
if (name.startsWith("get") || name.startsWith("set")) {
|
|
||||||
return fromName(name.substring(3));
|
|
||||||
}
|
|
||||||
if ("iterator".equals(name)) {
|
|
||||||
return "it";
|
|
||||||
}
|
|
||||||
if ("toString".equals(name)) {
|
|
||||||
return makeNameForClass(declClass);
|
|
||||||
}
|
|
||||||
if ("forName".equals(name) && declClass.getType().equals(ArgType.CLASS)) {
|
|
||||||
return OBJ_ALIAS.get(Consts.CLASS_CLASS);
|
|
||||||
}
|
|
||||||
if (name.startsWith("to")) {
|
|
||||||
return fromName(name.substring(2));
|
|
||||||
}
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import org.jetbrains.annotations.Nullable;
|
|||||||
|
|
||||||
import com.google.gson.FieldNamingPolicy;
|
import com.google.gson.FieldNamingPolicy;
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.GsonBuilder;
|
|
||||||
|
|
||||||
import jadx.api.ICodeInfo;
|
import jadx.api.ICodeInfo;
|
||||||
import jadx.api.ICodeWriter;
|
import jadx.api.ICodeWriter;
|
||||||
@@ -32,13 +31,13 @@ import jadx.core.dex.nodes.ClassNode;
|
|||||||
import jadx.core.dex.nodes.FieldNode;
|
import jadx.core.dex.nodes.FieldNode;
|
||||||
import jadx.core.dex.nodes.MethodNode;
|
import jadx.core.dex.nodes.MethodNode;
|
||||||
import jadx.core.dex.nodes.RootNode;
|
import jadx.core.dex.nodes.RootNode;
|
||||||
|
import jadx.core.utils.GsonUtils;
|
||||||
import jadx.core.utils.Utils;
|
import jadx.core.utils.Utils;
|
||||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||||
|
|
||||||
public class JsonCodeGen {
|
public class JsonCodeGen {
|
||||||
|
|
||||||
private static final Gson GSON = new GsonBuilder()
|
private static final Gson GSON = GsonUtils.defaultGsonBuilder()
|
||||||
.setPrettyPrinting()
|
|
||||||
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_DASHES)
|
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_DASHES)
|
||||||
.disableHtmlEscaping()
|
.disableHtmlEscaping()
|
||||||
.create();
|
.create();
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import org.slf4j.LoggerFactory;
|
|||||||
|
|
||||||
import com.google.gson.FieldNamingPolicy;
|
import com.google.gson.FieldNamingPolicy;
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.GsonBuilder;
|
|
||||||
|
|
||||||
import jadx.api.JadxArgs;
|
import jadx.api.JadxArgs;
|
||||||
import jadx.core.codegen.json.mapping.JsonClsMapping;
|
import jadx.core.codegen.json.mapping.JsonClsMapping;
|
||||||
@@ -24,14 +23,14 @@ import jadx.core.dex.nodes.ClassNode;
|
|||||||
import jadx.core.dex.nodes.FieldNode;
|
import jadx.core.dex.nodes.FieldNode;
|
||||||
import jadx.core.dex.nodes.MethodNode;
|
import jadx.core.dex.nodes.MethodNode;
|
||||||
import jadx.core.dex.nodes.RootNode;
|
import jadx.core.dex.nodes.RootNode;
|
||||||
|
import jadx.core.utils.GsonUtils;
|
||||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||||
import jadx.core.utils.files.FileUtils;
|
import jadx.core.utils.files.FileUtils;
|
||||||
|
|
||||||
public class JsonMappingGen {
|
public class JsonMappingGen {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(JsonMappingGen.class);
|
private static final Logger LOG = LoggerFactory.getLogger(JsonMappingGen.class);
|
||||||
|
|
||||||
private static final Gson GSON = new GsonBuilder()
|
private static final Gson GSON = GsonUtils.defaultGsonBuilder()
|
||||||
.setPrettyPrinting()
|
|
||||||
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_DASHES)
|
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_DASHES)
|
||||||
.disableHtmlEscaping()
|
.disableHtmlEscaping()
|
||||||
.create();
|
.create();
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
package jadx.core.deobf;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import javax.xml.parsers.DocumentBuilder;
|
||||||
|
import javax.xml.parsers.DocumentBuilderFactory;
|
||||||
|
|
||||||
|
import org.w3c.dom.Document;
|
||||||
|
|
||||||
|
import jadx.core.utils.FileSignature;
|
||||||
|
import jadx.core.utils.StringUtils;
|
||||||
|
|
||||||
|
public class FileTypeDetector {
|
||||||
|
private static final Pattern DOCTYPE_PATTERN = Pattern.compile("\\s*<!doctype *(\\w+)[ >]", Pattern.CASE_INSENSITIVE);
|
||||||
|
private static final List<FileSignature> FILE_SIGNATURES = new ArrayList<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
register("png", "89 50 4E 47");
|
||||||
|
register("jpg", "FF D8 FF");
|
||||||
|
register("gif", "47 49 46 38");
|
||||||
|
register("webp", "52 49 46 46 ?? ?? ?? ?? 57 45 42 50 56 50 38");
|
||||||
|
register("bmp", "42 4D");
|
||||||
|
register("bmp", "42 41");
|
||||||
|
register("bmp", "43 49");
|
||||||
|
register("bmp", "43 50");
|
||||||
|
register("bmp", "49 43");
|
||||||
|
register("bmp", "50 54");
|
||||||
|
register("mp4", "00 00 00 ?? 66 74 79 70 69 73 6F 36");
|
||||||
|
register("mp4", "00 00 00 ?? 66 74 79 70 6D 70 34 32");
|
||||||
|
register("m4a", "00 00 00 ?? 66 74 79 70 4D 34 41 20");
|
||||||
|
register("mp3", "49 44 33");
|
||||||
|
register("ogg", "4F 67 67 53");
|
||||||
|
register("wav", "52 49 46 46 ?? ?? ?? ?? 57 41 56 45");
|
||||||
|
register("ttf", "00 01 00 00");
|
||||||
|
register("ttc", "74 74 63 66");
|
||||||
|
register("otf", "4F 54 54 4F");
|
||||||
|
register("xml", "03 00 08 00");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void register(String fileType, String signature) {
|
||||||
|
FILE_SIGNATURES.add(new FileSignature(fileType, signature));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String detectByHeaders(byte[] data) {
|
||||||
|
for (FileSignature sig : FILE_SIGNATURES) {
|
||||||
|
if (FileSignature.matches(sig, data)) {
|
||||||
|
if (sig.getFileType().equals("png") && isNinePatch(data)) {
|
||||||
|
return ".9.png";
|
||||||
|
}
|
||||||
|
return "." + sig.getFileType();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String detectFileExtension(byte[] data) {
|
||||||
|
// detect ext by headers
|
||||||
|
String extByHeaders = detectByHeaders(data);
|
||||||
|
if (!StringUtils.isEmpty(extByHeaders)) {
|
||||||
|
return extByHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
// detect ext by readable text
|
||||||
|
String text = new String(data, StandardCharsets.UTF_8);
|
||||||
|
if (text.startsWith("-----BEGIN CERTIFICATE-----")) {
|
||||||
|
return ".cer";
|
||||||
|
}
|
||||||
|
if (text.startsWith("-----BEGIN PRIVATE KEY-----")) {
|
||||||
|
return ".key";
|
||||||
|
}
|
||||||
|
if (text.contains("<html>")) {
|
||||||
|
return ".html";
|
||||||
|
}
|
||||||
|
Matcher m = DOCTYPE_PATTERN.matcher(text);
|
||||||
|
if (m.lookingAt()) {
|
||||||
|
return "." + m.group(1).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||||
|
factory.setNamespaceAware(true);
|
||||||
|
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
|
||||||
|
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
|
||||||
|
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
|
||||||
|
|
||||||
|
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||||
|
Document doc = builder.parse(new java.io.ByteArrayInputStream(data));
|
||||||
|
String rootTag = doc.getDocumentElement().getNodeName();
|
||||||
|
|
||||||
|
if ("svg".equalsIgnoreCase(rootTag)) {
|
||||||
|
return ".svg";
|
||||||
|
}
|
||||||
|
if ("plist".equalsIgnoreCase(rootTag)) {
|
||||||
|
return ".plist";
|
||||||
|
}
|
||||||
|
if ("kml".equalsIgnoreCase(rootTag)) {
|
||||||
|
return ".kml";
|
||||||
|
}
|
||||||
|
return ".xml";
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int readInt(byte[] data, int offset) {
|
||||||
|
return (data[offset] & 0xFF) << 24
|
||||||
|
| (data[offset + 1] & 0xFF) << 16
|
||||||
|
| (data[offset + 2] & 0xFF) << 8
|
||||||
|
| (data[offset + 3] & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isNinePatch(byte[] data) {
|
||||||
|
int offset = 8;
|
||||||
|
while (offset + 8 < data.length) {
|
||||||
|
int chunkLength = readInt(data, offset);
|
||||||
|
int chunkType = readInt(data, offset + 4);
|
||||||
|
if (chunkType == 0x6e705463) { // 'npTc'
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
offset += 8 + chunkLength + 4; // chunk + data + CRC
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ import jadx.core.dex.attributes.nodes.MethodBridgeAttr;
|
|||||||
import jadx.core.dex.attributes.nodes.MethodInlineAttr;
|
import jadx.core.dex.attributes.nodes.MethodInlineAttr;
|
||||||
import jadx.core.dex.attributes.nodes.MethodOverrideAttr;
|
import jadx.core.dex.attributes.nodes.MethodOverrideAttr;
|
||||||
import jadx.core.dex.attributes.nodes.MethodReplaceAttr;
|
import jadx.core.dex.attributes.nodes.MethodReplaceAttr;
|
||||||
|
import jadx.core.dex.attributes.nodes.MethodThrowsAttr;
|
||||||
import jadx.core.dex.attributes.nodes.MethodTypeVarsAttr;
|
import jadx.core.dex.attributes.nodes.MethodTypeVarsAttr;
|
||||||
import jadx.core.dex.attributes.nodes.PhiListAttr;
|
import jadx.core.dex.attributes.nodes.PhiListAttr;
|
||||||
import jadx.core.dex.attributes.nodes.RegDebugInfoAttr;
|
import jadx.core.dex.attributes.nodes.RegDebugInfoAttr;
|
||||||
@@ -76,6 +77,7 @@ public final class AType<T extends IJadxAttribute> implements IJadxAttrType<T> {
|
|||||||
public static final AType<MethodTypeVarsAttr> METHOD_TYPE_VARS = new AType<>();
|
public static final AType<MethodTypeVarsAttr> METHOD_TYPE_VARS = new AType<>();
|
||||||
public static final AType<AttrList<TryCatchBlockAttr>> TRY_BLOCKS_LIST = new AType<>();
|
public static final AType<AttrList<TryCatchBlockAttr>> TRY_BLOCKS_LIST = new AType<>();
|
||||||
public static final AType<CodeFeaturesAttr> METHOD_CODE_FEATURES = new AType<>();
|
public static final AType<CodeFeaturesAttr> METHOD_CODE_FEATURES = new AType<>();
|
||||||
|
public static final AType<MethodThrowsAttr> METHOD_THROWS = new AType<>();
|
||||||
|
|
||||||
// region
|
// region
|
||||||
public static final AType<DeclareVariablesAttr> DECLARE_VARIABLES = new AType<>();
|
public static final AType<DeclareVariablesAttr> DECLARE_VARIABLES = new AType<>();
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import jadx.core.utils.Utils;
|
|||||||
|
|
||||||
public abstract class AttrNode implements IAttributeNode {
|
public abstract class AttrNode implements IAttributeNode {
|
||||||
|
|
||||||
private static final AttributeStorage EMPTY_ATTR_STORAGE = new EmptyAttrStorage();
|
private static final AttributeStorage EMPTY_ATTR_STORAGE = EmptyAttrStorage.INSTANCE;
|
||||||
|
|
||||||
private AttributeStorage storage = EMPTY_ATTR_STORAGE;
|
private AttributeStorage storage = EMPTY_ATTR_STORAGE;
|
||||||
|
|
||||||
@@ -35,6 +35,9 @@ public abstract class AttrNode implements IAttributeNode {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addAttrs(List<IJadxAttribute> list) {
|
public void addAttrs(List<IJadxAttribute> list) {
|
||||||
|
if (list.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
initStorage().add(list);
|
initStorage().add(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,12 +18,19 @@ import jadx.core.utils.Utils;
|
|||||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Storage for different attribute types:
|
* Storage for different attribute types:<br>
|
||||||
* 1. flags - boolean attribute (set or not)
|
* 1. Flags - boolean attribute (set or not)<br>
|
||||||
* 2. attribute - class instance associated with attribute type.
|
* 2. Attributes - class instance ({@link IJadxAttribute}) associated with an attribute type
|
||||||
|
* ({@link IJadxAttrType})<br>
|
||||||
*/
|
*/
|
||||||
public class AttributeStorage {
|
public class AttributeStorage {
|
||||||
|
|
||||||
|
public static AttributeStorage fromList(List<IJadxAttribute> list) {
|
||||||
|
AttributeStorage storage = new AttributeStorage();
|
||||||
|
storage.add(list);
|
||||||
|
return storage;
|
||||||
|
}
|
||||||
|
|
||||||
static {
|
static {
|
||||||
int flagsCount = AFlag.values().length;
|
int flagsCount = AFlag.values().length;
|
||||||
if (flagsCount >= 64) {
|
if (flagsCount >= 64) {
|
||||||
@@ -31,17 +38,14 @@ public class AttributeStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final Map<IJadxAttrType<?>, IJadxAttribute> EMPTY_ATTRIBUTES = Collections.emptyMap();
|
||||||
|
|
||||||
private final Set<AFlag> flags;
|
private final Set<AFlag> flags;
|
||||||
private Map<IJadxAttrType<?>, IJadxAttribute> attributes;
|
private Map<IJadxAttrType<?>, IJadxAttribute> attributes;
|
||||||
|
|
||||||
public AttributeStorage() {
|
public AttributeStorage() {
|
||||||
flags = EnumSet.noneOf(AFlag.class);
|
flags = EnumSet.noneOf(AFlag.class);
|
||||||
attributes = Collections.emptyMap();
|
attributes = EMPTY_ATTRIBUTES;
|
||||||
}
|
|
||||||
|
|
||||||
public AttributeStorage(List<IJadxAttribute> attributesList) {
|
|
||||||
this();
|
|
||||||
add(attributesList);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void add(AFlag flag) {
|
public void add(AFlag flag) {
|
||||||
@@ -125,11 +129,14 @@ public class AttributeStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void writeAttributes(Consumer<Map<IJadxAttrType<?>, IJadxAttribute>> mapConsumer) {
|
private void writeAttributes(Consumer<Map<IJadxAttrType<?>, IJadxAttribute>> mapConsumer) {
|
||||||
if (attributes.isEmpty()) {
|
|
||||||
attributes = new IdentityHashMap<>(5);
|
|
||||||
}
|
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
|
if (attributes == EMPTY_ATTRIBUTES) {
|
||||||
|
attributes = new IdentityHashMap<>(2); // only 1 or 2 attributes added in most cases
|
||||||
|
}
|
||||||
mapConsumer.accept(attributes);
|
mapConsumer.accept(attributes);
|
||||||
|
if (attributes.isEmpty()) {
|
||||||
|
attributes = EMPTY_ATTRIBUTES;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,9 +144,7 @@ public class AttributeStorage {
|
|||||||
if (attributes.isEmpty()) {
|
if (attributes.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
synchronized (this) {
|
writeAttributes(map -> map.entrySet().removeIf(entry -> !entry.getValue().keepLoaded()));
|
||||||
attributes.entrySet().removeIf(entry -> !entry.getValue().keepLoaded());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> getAttributeStrings() {
|
public List<String> getAttributeStrings() {
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ import jadx.api.plugins.input.data.attributes.IJadxAttribute;
|
|||||||
|
|
||||||
public final class EmptyAttrStorage extends AttributeStorage {
|
public final class EmptyAttrStorage extends AttributeStorage {
|
||||||
|
|
||||||
|
public static final AttributeStorage INSTANCE = new EmptyAttrStorage();
|
||||||
|
|
||||||
|
private EmptyAttrStorage() {
|
||||||
|
// singleton
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean contains(AFlag flag) {
|
public boolean contains(AFlag flag) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package jadx.core.dex.attributes.nodes;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import jadx.api.plugins.input.data.attributes.IJadxAttrType;
|
||||||
|
import jadx.api.plugins.input.data.attributes.PinnedAttribute;
|
||||||
|
import jadx.core.dex.attributes.AType;
|
||||||
|
|
||||||
|
public class MethodThrowsAttr extends PinnedAttribute {
|
||||||
|
private final Set<String> list;
|
||||||
|
|
||||||
|
private boolean visited;
|
||||||
|
|
||||||
|
public MethodThrowsAttr(Set<String> list) {
|
||||||
|
this.list = list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isVisited() {
|
||||||
|
return visited;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVisited(boolean visited) {
|
||||||
|
this.visited = visited;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<String> getList() {
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IJadxAttrType<MethodThrowsAttr> getAttrType() {
|
||||||
|
return AType.METHOD_THROWS;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "THROWS:" + list;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -34,7 +34,9 @@ public abstract class ArgType {
|
|||||||
public static final ArgType STRING = objectNoCache(Consts.CLASS_STRING);
|
public static final ArgType STRING = objectNoCache(Consts.CLASS_STRING);
|
||||||
public static final ArgType ENUM = objectNoCache(Consts.CLASS_ENUM);
|
public static final ArgType ENUM = objectNoCache(Consts.CLASS_ENUM);
|
||||||
public static final ArgType THROWABLE = objectNoCache(Consts.CLASS_THROWABLE);
|
public static final ArgType THROWABLE = objectNoCache(Consts.CLASS_THROWABLE);
|
||||||
|
public static final ArgType ERROR = objectNoCache(Consts.CLASS_ERROR);
|
||||||
public static final ArgType EXCEPTION = objectNoCache(Consts.CLASS_EXCEPTION);
|
public static final ArgType EXCEPTION = objectNoCache(Consts.CLASS_EXCEPTION);
|
||||||
|
public static final ArgType RUNTIME_EXCEPTION = objectNoCache(Consts.CLASS_RUNTIME_EXCEPTION);
|
||||||
public static final ArgType OBJECT_ARRAY = array(OBJECT);
|
public static final ArgType OBJECT_ARRAY = array(OBJECT);
|
||||||
public static final ArgType WILDCARD = wildcard();
|
public static final ArgType WILDCARD = wildcard();
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package jadx.core.dex.instructions.args;
|
|||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import jadx.core.dex.attributes.AFlag;
|
||||||
import jadx.core.dex.instructions.ConstStringNode;
|
import jadx.core.dex.instructions.ConstStringNode;
|
||||||
import jadx.core.dex.instructions.InsnType;
|
import jadx.core.dex.instructions.InsnType;
|
||||||
import jadx.core.dex.nodes.InsnNode;
|
import jadx.core.dex.nodes.InsnNode;
|
||||||
@@ -24,6 +25,12 @@ public final class InsnWrapArg extends InsnArg {
|
|||||||
return wrappedInsn;
|
return wrappedInsn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public InsnNode unWrapWithCopy() {
|
||||||
|
InsnNode copy = wrappedInsn.copyWithoutResult();
|
||||||
|
copy.remove(AFlag.WRAPPED);
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setParentInsn(InsnNode parentInsn) {
|
public void setParentInsn(InsnNode parentInsn) {
|
||||||
if (parentInsn == wrappedInsn) {
|
if (parentInsn == wrappedInsn) {
|
||||||
|
|||||||
@@ -59,13 +59,11 @@ public class SSAVar implements Comparable<SSAVar> {
|
|||||||
return version;
|
return version;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
public @NotNull RegisterArg getAssign() {
|
||||||
public RegisterArg getAssign() {
|
|
||||||
return assign;
|
return assign;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
public @Nullable InsnNode getAssignInsn() {
|
||||||
public InsnNode getAssignInsn() {
|
|
||||||
return assign.getParentInsn();
|
return assign.getParentInsn();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,14 @@ public final class TernaryInsn extends InsnNode {
|
|||||||
list.addAll(condition.getRegisterArgs());
|
list.addAll(condition.getRegisterArgs());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean replaceArg(InsnArg from, InsnArg to) {
|
||||||
|
if (super.replaceArg(from, to)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return condition.replaceArg(from, to);
|
||||||
|
}
|
||||||
|
|
||||||
public void visitInsns(Consumer<InsnNode> visitor) {
|
public void visitInsns(Consumer<InsnNode> visitor) {
|
||||||
super.visitInsns(visitor);
|
super.visitInsns(visitor);
|
||||||
condition.visitInsns(visitor);
|
condition.visitInsns(visitor);
|
||||||
|
|||||||
@@ -25,10 +25,9 @@ public final class BlockNode extends AttrNode implements IBlock, Comparable<Bloc
|
|||||||
private final int cid;
|
private final int cid;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ID linked to position in blocks list (easier to use BitSet)
|
* Position in blocks list (easier to use BitSet)
|
||||||
* TODO: rename to avoid confusion
|
|
||||||
*/
|
*/
|
||||||
private int id;
|
private int pos;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Offset in methods bytecode
|
* Offset in methods bytecode
|
||||||
@@ -71,9 +70,9 @@ public final class BlockNode extends AttrNode implements IBlock, Comparable<Bloc
|
|||||||
*/
|
*/
|
||||||
private List<BlockNode> dominatesOn = new ArrayList<>(3);
|
private List<BlockNode> dominatesOn = new ArrayList<>(3);
|
||||||
|
|
||||||
public BlockNode(int cid, int id, int offset) {
|
public BlockNode(int cid, int pos, int offset) {
|
||||||
this.cid = cid;
|
this.cid = cid;
|
||||||
this.id = id;
|
this.pos = pos;
|
||||||
this.startOffset = offset;
|
this.startOffset = offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,12 +80,20 @@ public final class BlockNode extends AttrNode implements IBlock, Comparable<Bloc
|
|||||||
return cid;
|
return cid;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setId(int id) {
|
void setPos(int id) {
|
||||||
this.id = id;
|
this.pos = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deprecated. Use {@link #getPos()}.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
public int getId() {
|
public int getId() {
|
||||||
return id;
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPos() {
|
||||||
|
return pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<BlockNode> getPredecessors() {
|
public List<BlockNode> getPredecessors() {
|
||||||
@@ -105,6 +112,13 @@ public final class BlockNode extends AttrNode implements IBlock, Comparable<Bloc
|
|||||||
cleanSuccessors = cleanSuccessors(this);
|
cleanSuccessors = cleanSuccessors(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void updateBlockPositions(List<BlockNode> blocks) {
|
||||||
|
int count = blocks.size();
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
blocks.get(i).setPos(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void lock() {
|
public void lock() {
|
||||||
try {
|
try {
|
||||||
List<BlockNode> successorsList = successors;
|
List<BlockNode> successorsList = successors;
|
||||||
@@ -161,7 +175,7 @@ public final class BlockNode extends AttrNode implements IBlock, Comparable<Bloc
|
|||||||
* Check if 'block' dominated on this node
|
* Check if 'block' dominated on this node
|
||||||
*/
|
*/
|
||||||
public boolean isDominator(BlockNode block) {
|
public boolean isDominator(BlockNode block) {
|
||||||
return doms.get(block.getId());
|
return doms.get(block.getPos());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -236,7 +250,7 @@ public final class BlockNode extends AttrNode implements IBlock, Comparable<Bloc
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return startOffset;
|
return cid;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -248,7 +262,7 @@ public final class BlockNode extends AttrNode implements IBlock, Comparable<Bloc
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
BlockNode other = (BlockNode) obj;
|
BlockNode other = (BlockNode) obj;
|
||||||
return cid == other.cid && startOffset == other.startOffset;
|
return cid == other.cid;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -258,11 +272,11 @@ public final class BlockNode extends AttrNode implements IBlock, Comparable<Bloc
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String baseString() {
|
public String baseString() {
|
||||||
return Integer.toString(id);
|
return Integer.toString(cid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "B:" + id + ':' + InsnUtils.formatOffset(startOffset);
|
return "B:" + cid + ':' + InsnUtils.formatOffset(startOffset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import jadx.api.impl.SimpleCodeInfo;
|
|||||||
import jadx.api.impl.SimpleCodeWriter;
|
import jadx.api.impl.SimpleCodeWriter;
|
||||||
import jadx.api.metadata.ICodeAnnotation;
|
import jadx.api.metadata.ICodeAnnotation;
|
||||||
import jadx.api.metadata.annotations.NodeDeclareRef;
|
import jadx.api.metadata.annotations.NodeDeclareRef;
|
||||||
|
import jadx.api.metadata.annotations.VarRef;
|
||||||
import jadx.api.plugins.input.data.IClassData;
|
import jadx.api.plugins.input.data.IClassData;
|
||||||
import jadx.api.plugins.input.data.IFieldData;
|
import jadx.api.plugins.input.data.IFieldData;
|
||||||
import jadx.api.plugins.input.data.IMethodData;
|
import jadx.api.plugins.input.data.IMethodData;
|
||||||
@@ -175,9 +176,7 @@ public class ClassNode extends NotificationAttrNode
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void processSpecialClasses(ClassNode cls) {
|
private static void processSpecialClasses(ClassNode cls) {
|
||||||
AccessInfo flags = cls.getAccessFlags();
|
if (cls.getName().equals("package-info") && cls.getFields().isEmpty() && cls.getMethods().isEmpty()) {
|
||||||
if (flags.isSynthetic() && flags.isInterface() && flags.isAbstract()
|
|
||||||
&& cls.getName().equals("package-info")) {
|
|
||||||
cls.add(AFlag.PACKAGE_INFO);
|
cls.add(AFlag.PACKAGE_INFO);
|
||||||
cls.add(AFlag.DONT_RENAME);
|
cls.add(AFlag.DONT_RENAME);
|
||||||
}
|
}
|
||||||
@@ -425,6 +424,20 @@ public class ClassNode extends NotificationAttrNode
|
|||||||
declareRef.getNode().setDefPosition(pos);
|
declareRef.getNode().setDefPosition(pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// validate var refs
|
||||||
|
annotations.values().removeIf(v -> {
|
||||||
|
if (v.getAnnType() == ICodeAnnotation.AnnType.VAR_REF) {
|
||||||
|
VarRef varRef = (VarRef) v;
|
||||||
|
if (varRef.getRefPos() == 0) {
|
||||||
|
if (LOG.isDebugEnabled()) {
|
||||||
|
LOG.debug("Var reference '{}' incorrect (ref pos is zero) and was removed from metadata", varRef);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@@ -458,13 +471,15 @@ public class ClassNode extends NotificationAttrNode
|
|||||||
if (state == NOT_LOADED) {
|
if (state == NOT_LOADED) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
methods.forEach(MethodNode::unload);
|
synchronized (clsInfo) { // decompilation sync
|
||||||
innerClasses.forEach(ClassNode::unload);
|
methods.forEach(MethodNode::unload);
|
||||||
fields.forEach(FieldNode::unload);
|
innerClasses.forEach(ClassNode::unload);
|
||||||
unloadAttributes();
|
fields.forEach(FieldNode::unload);
|
||||||
setState(NOT_LOADED);
|
unloadAttributes();
|
||||||
this.loadStage = LoadStage.NONE;
|
setState(NOT_LOADED);
|
||||||
this.smali = null;
|
this.loadStage = LoadStage.NONE;
|
||||||
|
this.smali = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void buildCache() {
|
private void buildCache() {
|
||||||
@@ -823,6 +838,9 @@ public class ClassNode extends NotificationAttrNode
|
|||||||
return clsInfo.getAliasShortName();
|
return clsInfo.getAliasShortName();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deprecated. Use {@link #getAlias()}
|
||||||
|
*/
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public String getShortName() {
|
public String getShortName() {
|
||||||
return clsInfo.getAliasShortName();
|
return clsInfo.getAliasShortName();
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import jadx.core.dex.attributes.AFlag;
|
|||||||
import jadx.core.dex.attributes.AType;
|
import jadx.core.dex.attributes.AType;
|
||||||
import jadx.core.dex.attributes.nodes.LoopInfo;
|
import jadx.core.dex.attributes.nodes.LoopInfo;
|
||||||
import jadx.core.dex.attributes.nodes.MethodOverrideAttr;
|
import jadx.core.dex.attributes.nodes.MethodOverrideAttr;
|
||||||
|
import jadx.core.dex.attributes.nodes.MethodThrowsAttr;
|
||||||
import jadx.core.dex.attributes.nodes.NotificationAttrNode;
|
import jadx.core.dex.attributes.nodes.NotificationAttrNode;
|
||||||
import jadx.core.dex.info.AccessInfo;
|
import jadx.core.dex.info.AccessInfo;
|
||||||
import jadx.core.dex.info.AccessInfo.AFType;
|
import jadx.core.dex.info.AccessInfo.AFType;
|
||||||
@@ -366,14 +367,11 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
|
|||||||
|
|
||||||
public void setBasicBlocks(List<BlockNode> blocks) {
|
public void setBasicBlocks(List<BlockNode> blocks) {
|
||||||
this.blocks = blocks;
|
this.blocks = blocks;
|
||||||
updateBlockIds(blocks);
|
updateBlockPositions();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateBlockIds(List<BlockNode> blocks) {
|
public void updateBlockPositions() {
|
||||||
int count = blocks.size();
|
BlockNode.updateBlockPositions(blocks);
|
||||||
for (int i = 0; i < count; i++) {
|
|
||||||
blocks.get(i).setId(i);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getNextBlockCId() {
|
public int getNextBlockCId() {
|
||||||
@@ -480,11 +478,15 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ArgType> getThrows() {
|
public List<ArgType> getThrows() {
|
||||||
ExceptionsAttr exceptionsAttr = get(JadxAttrType.EXCEPTIONS);
|
MethodThrowsAttr throwsAttr = get(AType.METHOD_THROWS);
|
||||||
if (exceptionsAttr == null) {
|
if (throwsAttr != null) {
|
||||||
return Collections.emptyList();
|
return Utils.collectionMap(throwsAttr.getList(), ArgType::object);
|
||||||
}
|
}
|
||||||
return Utils.collectionMap(exceptionsAttr.getList(), ArgType::object);
|
ExceptionsAttr exceptionsAttr = get(JadxAttrType.EXCEPTIONS);
|
||||||
|
if (exceptionsAttr != null) {
|
||||||
|
return Utils.collectionMap(exceptionsAttr.getList(), ArgType::object);
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -100,7 +100,20 @@ public class RootNode {
|
|||||||
|
|
||||||
private @Nullable ManifestAttributes manifestAttributes;
|
private @Nullable ManifestAttributes manifestAttributes;
|
||||||
|
|
||||||
|
public RootNode(JadxDecompiler decompiler) {
|
||||||
|
this(decompiler, decompiler.getArgs());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deprecated. Prefer {@link #RootNode(JadxDecompiler)}
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
public RootNode(JadxArgs args) {
|
public RootNode(JadxArgs args) {
|
||||||
|
this(null, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RootNode(@Nullable JadxDecompiler decompiler, JadxArgs args) {
|
||||||
|
this.decompiler = decompiler;
|
||||||
this.args = args;
|
this.args = args;
|
||||||
this.preDecompilePasses = Jadx.getPreDecompilePassesList();
|
this.preDecompilePasses = Jadx.getPreDecompilePassesList();
|
||||||
this.processClasses = new ProcessClass(Jadx.getPassesList(args));
|
this.processClasses = new ProcessClass(Jadx.getPassesList(args));
|
||||||
@@ -131,6 +144,9 @@ public class RootNode {
|
|||||||
Utils.checkThreadInterrupt();
|
Utils.checkThreadInterrupt();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void finishClassLoad() {
|
||||||
if (classes.size() != clsMap.size()) {
|
if (classes.size() != clsMap.size()) {
|
||||||
// class name duplication detected
|
// class name duplication detected
|
||||||
markDuplicatedClasses(classes);
|
markDuplicatedClasses(classes);
|
||||||
@@ -255,6 +271,7 @@ public class RootNode {
|
|||||||
if (args.isSkipResources()) {
|
if (args.isSkipResources()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
boolean useHeaders = args.isUseHeadersForDetectResourceExtensions();
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
int renamedCount = 0;
|
int renamedCount = 0;
|
||||||
ResourceStorage resStorage = parser.getResStorage();
|
ResourceStorage resStorage = parser.getResStorage();
|
||||||
@@ -269,7 +286,7 @@ public class RootNode {
|
|||||||
for (ResourceFile resource : resources) {
|
for (ResourceFile resource : resources) {
|
||||||
ResourceEntry resEntry = entryNames.get(resource.getOriginalName());
|
ResourceEntry resEntry = entryNames.get(resource.getOriginalName());
|
||||||
if (resEntry != null) {
|
if (resEntry != null) {
|
||||||
if (resource.setAlias(resEntry)) {
|
if (resource.setAlias(resEntry, useHeaders)) {
|
||||||
renamedCount++;
|
renamedCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -290,7 +307,7 @@ public class RootNode {
|
|||||||
List<ClassNode> updated = new ArrayList<>();
|
List<ClassNode> updated = new ArrayList<>();
|
||||||
for (ClassNode cls : inner) {
|
for (ClassNode cls : inner) {
|
||||||
ClassInfo clsInfo = cls.getClassInfo();
|
ClassInfo clsInfo = cls.getClassInfo();
|
||||||
ClassNode parent = resolveClass(clsInfo.getParentClass());
|
ClassNode parent = resolveParentClass(clsInfo);
|
||||||
if (parent == null) {
|
if (parent == null) {
|
||||||
clsMap.remove(clsInfo);
|
clsMap.remove(clsInfo);
|
||||||
clsInfo.notInner(this);
|
clsInfo.notInner(this);
|
||||||
@@ -482,6 +499,33 @@ public class RootNode {
|
|||||||
return rawClsMap.get(rawFullName);
|
return rawClsMap.get(rawFullName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find and correct the parent of an inner class.
|
||||||
|
* <br>
|
||||||
|
* Sometimes inner ClassInfo generated wrong parent info.
|
||||||
|
* e.g. inner is `Cls$mth$1`, current parent = `Cls$mth`, real parent = `Cls`
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public ClassNode resolveParentClass(ClassInfo clsInfo) {
|
||||||
|
ClassInfo parentInfo = clsInfo.getParentClass();
|
||||||
|
ClassNode parentNode = resolveClass(parentInfo);
|
||||||
|
if (parentNode == null && parentInfo != null) {
|
||||||
|
String parClsName = parentInfo.getFullName();
|
||||||
|
// strip last part as method name
|
||||||
|
int sep = parClsName.lastIndexOf('.');
|
||||||
|
if (sep > 0 && sep != parClsName.length() - 1) {
|
||||||
|
String mthName = parClsName.substring(sep + 1);
|
||||||
|
String upperParClsName = parClsName.substring(0, sep);
|
||||||
|
ClassNode tmpParent = resolveClass(upperParClsName);
|
||||||
|
if (tmpParent != null && tmpParent.searchMethodByShortName(mthName) != null) {
|
||||||
|
parentNode = tmpParent;
|
||||||
|
clsInfo.convertToInner(parentNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parentNode;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Searches for ClassNode by its full name (original or alias name)
|
* Searches for ClassNode by its full name (original or alias name)
|
||||||
* <br>
|
* <br>
|
||||||
@@ -688,10 +732,6 @@ public class RootNode {
|
|||||||
return args;
|
return args;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDecompilerRef(JadxDecompiler jadxDecompiler) {
|
|
||||||
this.decompiler = jadxDecompiler;
|
|
||||||
}
|
|
||||||
|
|
||||||
public @Nullable JadxDecompiler getDecompiler() {
|
public @Nullable JadxDecompiler getDecompiler() {
|
||||||
return decompiler;
|
return decompiler;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -226,6 +226,10 @@ public class TypeUtils {
|
|||||||
for (int i = 0; i < genericParamsCount; i++) {
|
for (int i = 0; i < genericParamsCount; i++) {
|
||||||
ArgType actualType = actualTypes.get(i);
|
ArgType actualType = actualTypes.get(i);
|
||||||
ArgType typeVar = typeParameters.get(i);
|
ArgType typeVar = typeParameters.get(i);
|
||||||
|
if (typeVar.getExtendTypes() != null) {
|
||||||
|
// force short form (only type var name)
|
||||||
|
typeVar = ArgType.genericType(typeVar.getObject());
|
||||||
|
}
|
||||||
replaceMap.put(typeVar, actualType);
|
replaceMap.put(typeVar, actualType);
|
||||||
}
|
}
|
||||||
return replaceMap;
|
return replaceMap;
|
||||||
|
|||||||
@@ -68,11 +68,11 @@ public abstract class ConditionRegion extends AbstractRegion implements IConditi
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prefer way for update condition info
|
* Preferred way to update condition info
|
||||||
*/
|
*/
|
||||||
public void updateCondition(IfInfo info) {
|
public void updateCondition(IfInfo info) {
|
||||||
this.condition = info.getCondition();
|
this.condition = info.getCondition();
|
||||||
this.conditionBlocks = info.getMergedBlocks();
|
this.conditionBlocks = info.getMergedBlocks().toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateCondition(IfCondition condition, List<BlockNode> conditionBlocks) {
|
public void updateCondition(IfCondition condition, List<BlockNode> conditionBlocks) {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import jadx.core.dex.instructions.ArithOp;
|
|||||||
import jadx.core.dex.instructions.IfNode;
|
import jadx.core.dex.instructions.IfNode;
|
||||||
import jadx.core.dex.instructions.IfOp;
|
import jadx.core.dex.instructions.IfOp;
|
||||||
import jadx.core.dex.instructions.args.ArgType;
|
import jadx.core.dex.instructions.args.ArgType;
|
||||||
|
import jadx.core.dex.instructions.args.InsnArg;
|
||||||
import jadx.core.dex.instructions.args.InsnWrapArg;
|
import jadx.core.dex.instructions.args.InsnWrapArg;
|
||||||
import jadx.core.dex.instructions.args.LiteralArg;
|
import jadx.core.dex.instructions.args.LiteralArg;
|
||||||
import jadx.core.dex.instructions.args.RegisterArg;
|
import jadx.core.dex.instructions.args.RegisterArg;
|
||||||
@@ -264,6 +265,18 @@ public final class IfCondition extends AttrNode {
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean replaceArg(InsnArg from, InsnArg to) {
|
||||||
|
if (mode == Mode.COMPARE) {
|
||||||
|
return compare.getInsn().replaceArg(from, to);
|
||||||
|
}
|
||||||
|
for (IfCondition arg : args) {
|
||||||
|
if (arg.replaceArg(from, to)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public void visitInsns(Consumer<InsnNode> visitor) {
|
public void visitInsns(Consumer<InsnNode> visitor) {
|
||||||
if (mode == Mode.COMPARE) {
|
if (mode == Mode.COMPARE) {
|
||||||
compare.getInsn().visitInsns(visitor);
|
compare.getInsn().visitInsns(visitor);
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ import java.util.Set;
|
|||||||
import jadx.core.dex.nodes.BlockNode;
|
import jadx.core.dex.nodes.BlockNode;
|
||||||
import jadx.core.dex.nodes.InsnNode;
|
import jadx.core.dex.nodes.InsnNode;
|
||||||
import jadx.core.dex.nodes.MethodNode;
|
import jadx.core.dex.nodes.MethodNode;
|
||||||
|
import jadx.core.utils.blocks.BlockSet;
|
||||||
|
|
||||||
public final class IfInfo {
|
public final class IfInfo {
|
||||||
private final MethodNode mth;
|
private final MethodNode mth;
|
||||||
private final IfCondition condition;
|
private final IfCondition condition;
|
||||||
private final List<BlockNode> mergedBlocks;
|
private final BlockSet mergedBlocks;
|
||||||
private final BlockNode thenBlock;
|
private final BlockNode thenBlock;
|
||||||
private final BlockNode elseBlock;
|
private final BlockNode elseBlock;
|
||||||
private final Set<BlockNode> skipBlocks;
|
private final Set<BlockNode> skipBlocks;
|
||||||
@@ -20,7 +21,7 @@ public final class IfInfo {
|
|||||||
private BlockNode outBlock;
|
private BlockNode outBlock;
|
||||||
|
|
||||||
public IfInfo(MethodNode mth, IfCondition condition, BlockNode thenBlock, BlockNode elseBlock) {
|
public IfInfo(MethodNode mth, IfCondition condition, BlockNode thenBlock, BlockNode elseBlock) {
|
||||||
this(mth, condition, thenBlock, elseBlock, new ArrayList<>(), new HashSet<>(), new ArrayList<>());
|
this(mth, condition, thenBlock, elseBlock, BlockSet.empty(mth), new HashSet<>(), new ArrayList<>());
|
||||||
}
|
}
|
||||||
|
|
||||||
public IfInfo(IfInfo info, BlockNode thenBlock, BlockNode elseBlock) {
|
public IfInfo(IfInfo info, BlockNode thenBlock, BlockNode elseBlock) {
|
||||||
@@ -29,7 +30,7 @@ public final class IfInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private IfInfo(MethodNode mth, IfCondition condition, BlockNode thenBlock, BlockNode elseBlock,
|
private IfInfo(MethodNode mth, IfCondition condition, BlockNode thenBlock, BlockNode elseBlock,
|
||||||
List<BlockNode> mergedBlocks, Set<BlockNode> skipBlocks, List<InsnNode> forceInlineInsns) {
|
BlockSet mergedBlocks, Set<BlockNode> skipBlocks, List<InsnNode> forceInlineInsns) {
|
||||||
this.mth = mth;
|
this.mth = mth;
|
||||||
this.condition = condition;
|
this.condition = condition;
|
||||||
this.thenBlock = thenBlock;
|
this.thenBlock = thenBlock;
|
||||||
@@ -56,7 +57,11 @@ public final class IfInfo {
|
|||||||
|
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public BlockNode getFirstIfBlock() {
|
public BlockNode getFirstIfBlock() {
|
||||||
return mergedBlocks.get(0);
|
return mergedBlocks.getFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BlockSet getMergedBlocks() {
|
||||||
|
return mergedBlocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MethodNode getMth() {
|
public MethodNode getMth() {
|
||||||
@@ -67,10 +72,6 @@ public final class IfInfo {
|
|||||||
return condition;
|
return condition;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<BlockNode> getMergedBlocks() {
|
|
||||||
return mergedBlocks;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<BlockNode> getSkipBlocks() {
|
public Set<BlockNode> getSkipBlocks() {
|
||||||
return skipBlocks;
|
return skipBlocks;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,272 @@
|
|||||||
|
package jadx.core.dex.visitors;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import jadx.core.Consts;
|
||||||
|
import jadx.core.deobf.NameMapper;
|
||||||
|
import jadx.core.dex.attributes.AFlag;
|
||||||
|
import jadx.core.dex.info.ClassInfo;
|
||||||
|
import jadx.core.dex.info.MethodInfo;
|
||||||
|
import jadx.core.dex.instructions.InvokeNode;
|
||||||
|
import jadx.core.dex.instructions.args.ArgType;
|
||||||
|
import jadx.core.dex.instructions.args.CodeVar;
|
||||||
|
import jadx.core.dex.instructions.args.InsnArg;
|
||||||
|
import jadx.core.dex.instructions.args.InsnWrapArg;
|
||||||
|
import jadx.core.dex.instructions.args.RegisterArg;
|
||||||
|
import jadx.core.dex.instructions.args.SSAVar;
|
||||||
|
import jadx.core.dex.instructions.mods.ConstructorInsn;
|
||||||
|
import jadx.core.dex.nodes.InsnNode;
|
||||||
|
import jadx.core.dex.nodes.MethodNode;
|
||||||
|
import jadx.core.dex.nodes.RootNode;
|
||||||
|
import jadx.core.dex.visitors.regions.variables.ProcessVariables;
|
||||||
|
import jadx.core.utils.StringUtils;
|
||||||
|
import jadx.core.utils.Utils;
|
||||||
|
import jadx.core.utils.exceptions.JadxException;
|
||||||
|
|
||||||
|
@JadxVisitor(
|
||||||
|
name = "ApplyVariableNames",
|
||||||
|
desc = "Try to guess variable name from usage",
|
||||||
|
runAfter = {
|
||||||
|
ProcessVariables.class
|
||||||
|
}
|
||||||
|
)
|
||||||
|
public class ApplyVariableNames extends AbstractVisitor {
|
||||||
|
|
||||||
|
private static final Map<String, String> OBJ_ALIAS = Utils.newConstStringMap(
|
||||||
|
Consts.CLASS_STRING, "str",
|
||||||
|
Consts.CLASS_CLASS, "cls",
|
||||||
|
Consts.CLASS_THROWABLE, "th",
|
||||||
|
Consts.CLASS_OBJECT, "obj",
|
||||||
|
"java.util.Iterator", "it",
|
||||||
|
"java.util.HashMap", "map",
|
||||||
|
"java.lang.Boolean", "bool",
|
||||||
|
"java.lang.Short", "sh",
|
||||||
|
"java.lang.Integer", "num",
|
||||||
|
"java.lang.Character", "ch",
|
||||||
|
"java.lang.Byte", "b",
|
||||||
|
"java.lang.Float", "f",
|
||||||
|
"java.lang.Long", "l",
|
||||||
|
"java.lang.Double", "d",
|
||||||
|
"java.lang.StringBuilder", "sb",
|
||||||
|
"java.lang.Exception", "exc");
|
||||||
|
|
||||||
|
private static final Set<String> GOOD_VAR_NAMES = Set.of(
|
||||||
|
"size", "length", "list", "map", "next");
|
||||||
|
private static final List<String> INVOKE_PREFIXES = List.of(
|
||||||
|
"get", "set", "to", "parse", "read", "format");
|
||||||
|
|
||||||
|
private RootNode root;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(RootNode root) throws JadxException {
|
||||||
|
this.root = root;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(MethodNode mth) throws JadxException {
|
||||||
|
for (SSAVar ssaVar : mth.getSVars()) {
|
||||||
|
CodeVar codeVar = ssaVar.getCodeVar();
|
||||||
|
String newName = guessName(codeVar);
|
||||||
|
if (newName != null) {
|
||||||
|
codeVar.setName(newName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private @Nullable String guessName(CodeVar var) {
|
||||||
|
if (var.isThis()) {
|
||||||
|
return RegisterArg.THIS_ARG_NAME;
|
||||||
|
}
|
||||||
|
if (!var.isDeclared()) {
|
||||||
|
// name is not used in code
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (NameMapper.isValidAndPrintable(var.getName())) {
|
||||||
|
// the current name is valid, keep it
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<SSAVar> ssaVars = var.getSsaVars();
|
||||||
|
if (Utils.notEmpty(ssaVars)) {
|
||||||
|
boolean mthArg = ssaVars.stream().anyMatch(ssaVar -> ssaVar.getAssign().contains(AFlag.METHOD_ARGUMENT));
|
||||||
|
if (mthArg) {
|
||||||
|
// for method args use defined type and ignore usage
|
||||||
|
return makeNameForType(var.getType());
|
||||||
|
}
|
||||||
|
for (SSAVar ssaVar : ssaVars) {
|
||||||
|
String name = makeNameForSSAVar(ssaVar);
|
||||||
|
if (name != null) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return makeNameForType(var.getType());
|
||||||
|
}
|
||||||
|
|
||||||
|
private @Nullable String makeNameForSSAVar(SSAVar ssaVar) {
|
||||||
|
String ssaVarName = ssaVar.getName();
|
||||||
|
if (ssaVarName != null) {
|
||||||
|
return ssaVarName;
|
||||||
|
}
|
||||||
|
InsnNode assignInsn = ssaVar.getAssignInsn();
|
||||||
|
if (assignInsn != null) {
|
||||||
|
String name = makeNameFromInsn(ssaVar, assignInsn);
|
||||||
|
if (NameMapper.isValidAndPrintable(name)) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String makeNameFromInsn(SSAVar ssaVar, InsnNode insn) {
|
||||||
|
switch (insn.getType()) {
|
||||||
|
case INVOKE:
|
||||||
|
return makeNameFromInvoke(ssaVar, (InvokeNode) insn);
|
||||||
|
|
||||||
|
case CONSTRUCTOR:
|
||||||
|
ConstructorInsn co = (ConstructorInsn) insn;
|
||||||
|
MethodNode callMth = root.getMethodUtils().resolveMethod(co);
|
||||||
|
if (callMth != null && callMth.contains(AFlag.ANONYMOUS_CONSTRUCTOR)) {
|
||||||
|
// don't use name of anonymous class
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return makeNameForClass(co.getClassType());
|
||||||
|
|
||||||
|
case ARRAY_LENGTH:
|
||||||
|
return "length";
|
||||||
|
|
||||||
|
case ARITH:
|
||||||
|
case TERNARY:
|
||||||
|
case CAST:
|
||||||
|
for (InsnArg arg : insn.getArguments()) {
|
||||||
|
if (arg.isInsnWrap()) {
|
||||||
|
InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn();
|
||||||
|
String wName = makeNameFromInsn(ssaVar, wrapInsn);
|
||||||
|
if (wName != null) {
|
||||||
|
return wName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String makeNameForType(ArgType type) {
|
||||||
|
if (type.isPrimitive()) {
|
||||||
|
return type.getPrimitiveType().getShortName().toLowerCase();
|
||||||
|
}
|
||||||
|
if (type.isArray()) {
|
||||||
|
return makeNameForType(type.getArrayRootElement()) + "Arr";
|
||||||
|
}
|
||||||
|
return makeNameForObject(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String makeNameForObject(ArgType type) {
|
||||||
|
if (type.isGenericType()) {
|
||||||
|
return StringUtils.escape(type.getObject().toLowerCase());
|
||||||
|
}
|
||||||
|
if (type.isObject()) {
|
||||||
|
String alias = getAliasForObject(type.getObject());
|
||||||
|
if (alias != null) {
|
||||||
|
return alias;
|
||||||
|
}
|
||||||
|
return makeNameForCheckedClass(ClassInfo.fromType(root, type));
|
||||||
|
}
|
||||||
|
return StringUtils.escape(type.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String makeNameForCheckedClass(ClassInfo classInfo) {
|
||||||
|
String shortName = classInfo.getAliasShortName();
|
||||||
|
String vName = fromName(shortName);
|
||||||
|
if (vName != null) {
|
||||||
|
return vName;
|
||||||
|
}
|
||||||
|
String lower = StringUtils.escape(shortName.toLowerCase());
|
||||||
|
if (shortName.equals(lower)) {
|
||||||
|
return lower + "Var";
|
||||||
|
}
|
||||||
|
return lower;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String makeNameForClass(ClassInfo classInfo) {
|
||||||
|
String alias = getAliasForObject(classInfo.getFullName());
|
||||||
|
if (alias != null) {
|
||||||
|
return alias;
|
||||||
|
}
|
||||||
|
return makeNameForCheckedClass(classInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String fromName(String name) {
|
||||||
|
if (name == null || name.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (name.toUpperCase().equals(name)) {
|
||||||
|
// all characters are upper case
|
||||||
|
return name.toLowerCase();
|
||||||
|
}
|
||||||
|
String v1 = Character.toLowerCase(name.charAt(0)) + name.substring(1);
|
||||||
|
if (!v1.equals(name)) {
|
||||||
|
return v1;
|
||||||
|
}
|
||||||
|
if (name.length() < 3) {
|
||||||
|
return name + "Var";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getAliasForObject(String name) {
|
||||||
|
return OBJ_ALIAS.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String makeNameFromInvoke(SSAVar ssaVar, InvokeNode inv) {
|
||||||
|
MethodInfo callMth = inv.getCallMth();
|
||||||
|
String name = callMth.getAlias();
|
||||||
|
ClassInfo declClass = callMth.getDeclClass();
|
||||||
|
if ("getInstance".equals(name)) {
|
||||||
|
// e.g. Cipher.getInstance
|
||||||
|
return makeNameForClass(declClass);
|
||||||
|
}
|
||||||
|
String shortName = cutPrefix(name);
|
||||||
|
if (shortName != null) {
|
||||||
|
return fromName(shortName);
|
||||||
|
}
|
||||||
|
if ("iterator".equals(name)) {
|
||||||
|
return "it";
|
||||||
|
}
|
||||||
|
if ("toString".equals(name)) {
|
||||||
|
return makeNameForClass(declClass);
|
||||||
|
}
|
||||||
|
if ("forName".equals(name) && declClass.getType().equals(ArgType.CLASS)) {
|
||||||
|
return OBJ_ALIAS.get(Consts.CLASS_CLASS);
|
||||||
|
}
|
||||||
|
// use method name as a variable name not the best idea in most cases
|
||||||
|
if (!GOOD_VAR_NAMES.contains(name)) {
|
||||||
|
String typeName = makeNameForType(ssaVar.getCodeVar().getType());
|
||||||
|
if (!typeName.equalsIgnoreCase(name)) {
|
||||||
|
return typeName + StringUtils.capitalizeFirstChar(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private @Nullable String cutPrefix(String name) {
|
||||||
|
for (String prefix : INVOKE_PREFIXES) {
|
||||||
|
if (name.startsWith(prefix)) {
|
||||||
|
return name.substring(prefix.length());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "ApplyVariableNames";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -86,8 +86,11 @@ public class ClassModifier extends AbstractVisitor {
|
|||||||
ClassInfo clsInfo = ClassInfo.fromType(cls.root(), fldType);
|
ClassInfo clsInfo = ClassInfo.fromType(cls.root(), fldType);
|
||||||
ClassNode fieldsCls = cls.root().resolveClass(clsInfo);
|
ClassNode fieldsCls = cls.root().resolveClass(clsInfo);
|
||||||
ClassInfo parentClass = cls.getClassInfo().getParentClass();
|
ClassInfo parentClass = cls.getClassInfo().getParentClass();
|
||||||
if (fieldsCls != null
|
if (fieldsCls == null) {
|
||||||
&& (inline || Objects.equals(parentClass, fieldsCls.getClassInfo()))) {
|
continue;
|
||||||
|
}
|
||||||
|
boolean isParentInst = Objects.equals(parentClass, fieldsCls.getClassInfo());
|
||||||
|
if (inline || isParentInst) {
|
||||||
int found = 0;
|
int found = 0;
|
||||||
for (MethodNode mth : cls.getMethods()) {
|
for (MethodNode mth : cls.getMethods()) {
|
||||||
if (removeFieldUsageFromConstructor(mth, field, fieldsCls)) {
|
if (removeFieldUsageFromConstructor(mth, field, fieldsCls)) {
|
||||||
@@ -95,7 +98,9 @@ public class ClassModifier extends AbstractVisitor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (found != 0) {
|
if (found != 0) {
|
||||||
field.addAttr(new FieldReplaceAttr(fieldsCls.getClassInfo()));
|
if (isParentInst) {
|
||||||
|
field.addAttr(new FieldReplaceAttr(fieldsCls.getClassInfo()));
|
||||||
|
}
|
||||||
field.add(AFlag.DONT_GENERATE);
|
field.add(AFlag.DONT_GENERATE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ public class DotGraphVisitor extends AbstractVisitor {
|
|||||||
|
|
||||||
public void process(MethodNode mth) {
|
public void process(MethodNode mth) {
|
||||||
dot.startLine("digraph \"CFG for");
|
dot.startLine("digraph \"CFG for");
|
||||||
dot.add(escape(mth.getParentClass() + "." + mth.getMethodInfo().getShortId()));
|
dot.add(escape(mth.getMethodInfo().getFullId()));
|
||||||
dot.add("\" {");
|
dot.add("\" {");
|
||||||
|
|
||||||
BlockNode enterBlock = mth.getEnterBlock();
|
BlockNode enterBlock = mth.getEnterBlock();
|
||||||
@@ -204,7 +204,7 @@ public class DotGraphVisitor extends AbstractVisitor {
|
|||||||
dot.add("color=red,");
|
dot.add("color=red,");
|
||||||
}
|
}
|
||||||
dot.add("label=\"{");
|
dot.add("label=\"{");
|
||||||
dot.add(String.valueOf(block.getId())).add("\\:\\ ");
|
dot.add(String.valueOf(block.getCId())).add("\\:\\ ");
|
||||||
dot.add(InsnUtils.formatOffset(block.getStartOffset()));
|
dot.add(InsnUtils.formatOffset(block.getStartOffset()));
|
||||||
if (!attrs.isEmpty()) {
|
if (!attrs.isEmpty()) {
|
||||||
dot.add('|').add(attrs);
|
dot.add('|').add(attrs);
|
||||||
@@ -237,10 +237,10 @@ public class DotGraphVisitor extends AbstractVisitor {
|
|||||||
|
|
||||||
if (PRINT_DOMINATORS) {
|
if (PRINT_DOMINATORS) {
|
||||||
for (BlockNode c : block.getDominatesOn()) {
|
for (BlockNode c : block.getDominatesOn()) {
|
||||||
conn.startLine(block.getId() + " -> " + c.getId() + "[color=green];");
|
conn.startLine(block.getCId() + " -> " + c.getCId() + "[color=green];");
|
||||||
}
|
}
|
||||||
for (BlockNode dom : BlockUtils.bitSetToBlocks(mth, block.getDomFrontier())) {
|
for (BlockNode dom : BlockUtils.bitSetToBlocks(mth, block.getDomFrontier())) {
|
||||||
conn.startLine("f_" + block.getId() + " -> f_" + dom.getId() + "[color=blue];");
|
conn.startLine("f_" + block.getCId() + " -> f_" + dom.getCId() + "[color=blue];");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -280,7 +280,7 @@ public class DotGraphVisitor extends AbstractVisitor {
|
|||||||
private String makeName(IContainer c) {
|
private String makeName(IContainer c) {
|
||||||
String name;
|
String name;
|
||||||
if (c instanceof BlockNode) {
|
if (c instanceof BlockNode) {
|
||||||
name = "Node_" + ((BlockNode) c).getId();
|
name = "Node_" + ((BlockNode) c).getCId();
|
||||||
} else if (c instanceof IBlock) {
|
} else if (c instanceof IBlock) {
|
||||||
name = "Node_" + c.getClass().getSimpleName() + '_' + c.hashCode();
|
name = "Node_" + c.getClass().getSimpleName() + '_' + c.hashCode();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import jadx.core.dex.nodes.InsnNode;
|
|||||||
import jadx.core.dex.nodes.MethodNode;
|
import jadx.core.dex.nodes.MethodNode;
|
||||||
import jadx.core.dex.visitors.typeinference.TypeInferenceVisitor;
|
import jadx.core.dex.visitors.typeinference.TypeInferenceVisitor;
|
||||||
import jadx.core.utils.BlockUtils;
|
import jadx.core.utils.BlockUtils;
|
||||||
|
import jadx.core.utils.InsnRemover;
|
||||||
import jadx.core.utils.ListUtils;
|
import jadx.core.utils.ListUtils;
|
||||||
import jadx.core.utils.exceptions.JadxException;
|
import jadx.core.utils.exceptions.JadxException;
|
||||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||||
@@ -46,7 +47,7 @@ public class InlineMethods extends AbstractVisitor {
|
|||||||
for (BlockNode block : mth.getBasicBlocks()) {
|
for (BlockNode block : mth.getBasicBlocks()) {
|
||||||
for (InsnNode insn : block.getInstructions()) {
|
for (InsnNode insn : block.getInstructions()) {
|
||||||
if (insn.getType() == InsnType.INVOKE) {
|
if (insn.getType() == InsnType.INVOKE) {
|
||||||
processInvokeInsn(mth, block, ((InvokeNode) insn));
|
processInvokeInsn(mth, block, (InvokeNode) insn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,47 +83,65 @@ public class InlineMethods extends AbstractVisitor {
|
|||||||
|
|
||||||
private void inlineMethod(MethodNode mth, MethodNode callMth, MethodInlineAttr mia, BlockNode block, InvokeNode insn) {
|
private void inlineMethod(MethodNode mth, MethodNode callMth, MethodInlineAttr mia, BlockNode block, InvokeNode insn) {
|
||||||
InsnNode inlCopy = mia.getInsn().copyWithoutResult();
|
InsnNode inlCopy = mia.getInsn().copyWithoutResult();
|
||||||
RegisterArg resultArg = insn.getResult();
|
if (replaceRegs(mth, callMth, mia, insn, inlCopy)) {
|
||||||
if (resultArg != null) {
|
IMethodDetails methodDetailsAttr = inlCopy.get(AType.METHOD_DETAILS);
|
||||||
inlCopy.setResult(resultArg.duplicate());
|
// replaceInsn replaces the attributes as well, make sure to preserve METHOD_DETAILS
|
||||||
} else if (isAssignNeeded(mia.getInsn(), insn, callMth)) {
|
if (BlockUtils.replaceInsn(mth, block, insn, inlCopy)) {
|
||||||
// add fake result to make correct java expression (see test TestGetterInlineNegative)
|
if (methodDetailsAttr != null) {
|
||||||
inlCopy.setResult(mth.makeSyntheticRegArg(callMth.getReturnType(), "unused"));
|
inlCopy.addAttr(methodDetailsAttr);
|
||||||
}
|
}
|
||||||
if (!callMth.getMethodInfo().getArgumentsTypes().isEmpty()) {
|
updateUsageInfo(mth, callMth, mia.getInsn());
|
||||||
// remap args
|
return;
|
||||||
InsnArg[] regs = new InsnArg[callMth.getRegsCount()];
|
|
||||||
int[] regNums = mia.getArgsRegNums();
|
|
||||||
for (int i = 0; i < regNums.length; i++) {
|
|
||||||
InsnArg arg = insn.getArg(i);
|
|
||||||
regs[regNums[i]] = arg;
|
|
||||||
}
|
}
|
||||||
// replace args
|
}
|
||||||
List<RegisterArg> inlArgs = new ArrayList<>();
|
mth.addWarnComment("Failed to inline method: " + callMth);
|
||||||
inlCopy.getRegisterArgs(inlArgs);
|
// undo changes to insn
|
||||||
for (RegisterArg r : inlArgs) {
|
InsnRemover.unbindInsn(mth, inlCopy);
|
||||||
int regNum = r.getRegNum();
|
insn.rebindArgs();
|
||||||
if (regNum >= regs.length) {
|
}
|
||||||
LOG.warn("Unknown register number {} in method call: {} from {}", r, callMth, mth);
|
|
||||||
} else {
|
private boolean replaceRegs(MethodNode mth, MethodNode callMth, MethodInlineAttr mia, InvokeNode insn, InsnNode inlCopy) {
|
||||||
|
try {
|
||||||
|
if (!callMth.getMethodInfo().getArgumentsTypes().isEmpty()) {
|
||||||
|
// remap args
|
||||||
|
InsnArg[] regs = new InsnArg[callMth.getRegsCount()];
|
||||||
|
int[] regNums = mia.getArgsRegNums();
|
||||||
|
for (int i = 0; i < regNums.length; i++) {
|
||||||
|
InsnArg arg = insn.getArg(i);
|
||||||
|
regs[regNums[i]] = arg;
|
||||||
|
}
|
||||||
|
// replace args
|
||||||
|
List<RegisterArg> inlArgs = new ArrayList<>();
|
||||||
|
inlCopy.getRegisterArgs(inlArgs);
|
||||||
|
for (RegisterArg r : inlArgs) {
|
||||||
|
int regNum = r.getRegNum();
|
||||||
|
if (regNum >= regs.length) {
|
||||||
|
mth.addWarnComment("Unknown register number '" + r + "' in method call: " + callMth);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
InsnArg repl = regs[regNum];
|
InsnArg repl = regs[regNum];
|
||||||
if (repl == null) {
|
if (repl == null) {
|
||||||
LOG.warn("Not passed register {} in method call: {} from {}", r, callMth, mth);
|
mth.addWarnComment("Not passed register '" + r + "' in method call: " + callMth);
|
||||||
} else {
|
return false;
|
||||||
inlCopy.replaceArg(r, repl);
|
}
|
||||||
|
if (!inlCopy.replaceArg(r, repl.duplicate())) {
|
||||||
|
mth.addWarnComment("Failed to replace arg " + r + " for method inline: " + callMth);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
RegisterArg resultArg = insn.getResult();
|
||||||
|
if (resultArg != null) {
|
||||||
|
inlCopy.setResult(resultArg.duplicate());
|
||||||
|
} else if (isAssignNeeded(mia.getInsn(), insn, callMth)) {
|
||||||
|
// add a fake result to make correct java expression (see test TestGetterInlineNegative)
|
||||||
|
inlCopy.setResult(mth.makeSyntheticRegArg(callMth.getReturnType(), "unused"));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
mth.addWarnComment("Method inline failed with exception", e);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
IMethodDetails methodDetailsAttr = inlCopy.get(AType.METHOD_DETAILS);
|
|
||||||
if (!BlockUtils.replaceInsn(mth, block, insn, inlCopy)) {
|
|
||||||
mth.addWarnComment("Failed to inline method: " + callMth);
|
|
||||||
}
|
|
||||||
// replaceInsn replaces the attributes as well, make sure to preserve METHOD_DETAILS
|
|
||||||
if (methodDetailsAttr != null) {
|
|
||||||
inlCopy.addAttr(methodDetailsAttr);
|
|
||||||
}
|
|
||||||
updateUsageInfo(mth, callMth, mia.getInsn());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isAssignNeeded(InsnNode inlineInsn, InvokeNode parentInsn, MethodNode callMthNode) {
|
private boolean isAssignNeeded(InsnNode inlineInsn, InvokeNode parentInsn, MethodNode callMthNode) {
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
package jadx.core.dex.visitors;
|
package jadx.core.dex.visitors;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import jadx.api.plugins.input.data.AccessFlags;
|
import jadx.api.plugins.input.data.AccessFlags;
|
||||||
import jadx.core.Consts;
|
|
||||||
import jadx.core.dex.attributes.AFlag;
|
import jadx.core.dex.attributes.AFlag;
|
||||||
import jadx.core.dex.attributes.AType;
|
import jadx.core.dex.attributes.AType;
|
||||||
import jadx.core.dex.attributes.nodes.MethodInlineAttr;
|
import jadx.core.dex.attributes.nodes.MethodInlineAttr;
|
||||||
@@ -17,6 +15,7 @@ import jadx.core.dex.instructions.InvokeNode;
|
|||||||
import jadx.core.dex.instructions.args.InsnArg;
|
import jadx.core.dex.instructions.args.InsnArg;
|
||||||
import jadx.core.dex.instructions.args.InsnWrapArg;
|
import jadx.core.dex.instructions.args.InsnWrapArg;
|
||||||
import jadx.core.dex.instructions.args.RegisterArg;
|
import jadx.core.dex.instructions.args.RegisterArg;
|
||||||
|
import jadx.core.dex.instructions.args.SSAVar;
|
||||||
import jadx.core.dex.nodes.InsnNode;
|
import jadx.core.dex.nodes.InsnNode;
|
||||||
import jadx.core.dex.nodes.MethodNode;
|
import jadx.core.dex.nodes.MethodNode;
|
||||||
import jadx.core.dex.visitors.fixaccessmodifiers.FixAccessModifiers;
|
import jadx.core.dex.visitors.fixaccessmodifiers.FixAccessModifiers;
|
||||||
@@ -80,17 +79,17 @@ public class MarkMethodsForInline extends AbstractVisitor {
|
|||||||
if (!arg.isInsnWrap()) {
|
if (!arg.isInsnWrap()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return addInlineAttr(mth, ((InsnWrapArg) arg).getWrapInsn());
|
return addInlineAttr(mth, ((InsnWrapArg) arg).unWrapWithCopy(), true);
|
||||||
}
|
}
|
||||||
// method invoke
|
// method invoke
|
||||||
return addInlineAttr(mth, insn);
|
return addInlineAttr(mth, insn, false);
|
||||||
}
|
}
|
||||||
if (insnsCount == 2 && insns.get(1).getType() == InsnType.RETURN) {
|
if (insnsCount == 2 && insns.get(1).getType() == InsnType.RETURN) {
|
||||||
InsnNode firstInsn = insns.get(0);
|
InsnNode firstInsn = insns.get(0);
|
||||||
InsnNode retInsn = insns.get(1);
|
InsnNode retInsn = insns.get(1);
|
||||||
if (retInsn.getArgsCount() == 0
|
if (retInsn.getArgsCount() == 0
|
||||||
|| isSyntheticAccessPattern(mth, firstInsn, retInsn)) {
|
|| isSyntheticAccessPattern(mth, firstInsn, retInsn)) {
|
||||||
return addInlineAttr(mth, firstInsn);
|
return addInlineAttr(mth, firstInsn, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO: inline field arithmetics. Disabled tests: TestAnonymousClass3a and TestAnonymousClass5
|
// TODO: inline field arithmetics. Disabled tests: TestAnonymousClass3a and TestAnonymousClass5
|
||||||
@@ -130,18 +129,29 @@ public class MarkMethodsForInline extends AbstractVisitor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MethodInlineAttr addInlineAttr(MethodNode mth, InsnNode insn) {
|
private static @Nullable MethodInlineAttr addInlineAttr(MethodNode mth, InsnNode insn, boolean isCopy) {
|
||||||
if (!fixVisibilityOfInlineCode(mth, insn)) {
|
if (!fixVisibilityOfInlineCode(mth, insn)) {
|
||||||
|
if (isCopy) {
|
||||||
|
unbindSsaVars(insn);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
InsnNode copy = insn.copyWithoutResult();
|
InsnNode inlInsn = isCopy ? insn : insn.copyWithoutResult();
|
||||||
// unbind SSA variables from copy instruction
|
unbindSsaVars(inlInsn);
|
||||||
List<RegisterArg> regArgs = new ArrayList<>();
|
return MethodInlineAttr.markForInline(mth, inlInsn);
|
||||||
copy.getRegisterArgs(regArgs);
|
}
|
||||||
for (RegisterArg regArg : regArgs) {
|
|
||||||
copy.replaceArg(regArg, regArg.duplicate(regArg.getRegNum(), null));
|
private static void unbindSsaVars(InsnNode insn) {
|
||||||
}
|
insn.visitArgs(arg -> {
|
||||||
return MethodInlineAttr.markForInline(mth, copy);
|
if (arg.isRegister()) {
|
||||||
|
RegisterArg reg = (RegisterArg) arg;
|
||||||
|
SSAVar ssaVar = reg.getSVar();
|
||||||
|
if (ssaVar != null) {
|
||||||
|
ssaVar.removeUse(reg);
|
||||||
|
reg.resetSSAVar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean fixVisibilityOfInlineCode(MethodNode mth, InsnNode insn) {
|
private static boolean fixVisibilityOfInlineCode(MethodNode mth, InsnNode insn) {
|
||||||
@@ -150,7 +160,7 @@ public class MarkMethodsForInline extends AbstractVisitor {
|
|||||||
if (insnType == InsnType.INVOKE) {
|
if (insnType == InsnType.INVOKE) {
|
||||||
InvokeNode invoke = (InvokeNode) insn;
|
InvokeNode invoke = (InvokeNode) insn;
|
||||||
MethodNode callMthNode = mth.root().resolveMethod(invoke.getCallMth());
|
MethodNode callMthNode = mth.root().resolveMethod(invoke.getCallMth());
|
||||||
if (callMthNode != null) {
|
if (callMthNode != null && !callMthNode.root().getArgs().isRespectBytecodeAccModifiers()) {
|
||||||
FixAccessModifiers.changeVisibility(callMthNode, newVisFlag);
|
FixAccessModifiers.changeVisibility(callMthNode, newVisFlag);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -169,9 +179,7 @@ public class MarkMethodsForInline extends AbstractVisitor {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (Consts.DEBUG) {
|
mth.addDebugComment("Can't inline method, not implemented redirect type for insn: " + insn);
|
||||||
mth.addDebugComment("can't inline method, not implemented redirect type: " + insn);
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user