Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 03009b5159 | |||
| d25eda4839 | |||
| 59003bdb1f | |||
| 9d4babcdce | |||
| 8c28a8530e | |||
| 6775cc5a93 | |||
| e648e60af9 | |||
| e6b7db35c1 | |||
| c3f7027bdd | |||
| 2fe95da570 | |||
| 62fa2735dc | |||
| bce6611aaf | |||
| 21aa90c5d1 | |||
| 55e79fb70f | |||
| 044c75ab9f | |||
| 97fa8ff210 | |||
| 27c283fb11 | |||
| bd1c3fffde | |||
| 169ad2901f | |||
| 869422b424 | |||
| ccc4164d54 | |||
| b61642a646 | |||
| 189e4181de | |||
| 5b960db77e | |||
| 3119e8c893 | |||
| f95c5e8f39 | |||
| 1e908f7af3 | |||
| 7ce40baacb | |||
| 7d689a85ea | |||
| c7a162d827 | |||
| 325b3ac991 | |||
| 9a8a11619b | |||
| 7ae6bd737c | |||
| 57fd9b5bdb | |||
| cdd5bf536d | |||
| dcce3aaa39 | |||
| b3d86ae908 | |||
| 8b7d3f497e | |||
| 462e582bb8 | |||
| 14a7b63707 | |||
| 4051a50146 | |||
| b3db337abd | |||
| 15ea9a56b9 | |||
| 165ae24722 | |||
| 11b38ffed2 | |||
| 81dfac6d83 | |||
| a3bd3b09fd | |||
| 13d306024a | |||
| ff64da705c | |||
| 00196e412b | |||
| 06c4fea4d2 | |||
| 69fd88d883 | |||
| f2f145019d | |||
| 7b3563fb62 | |||
| 22ee9a216c | |||
| 06b4467fdc | |||
| ff778ab372 | |||
| ff4dde62ae | |||
| 468b52342d | |||
| 09d39de604 | |||
| eb079bd435 | |||
| d1b1fc0ab6 | |||
| 859d479569 | |||
| b0954e9620 | |||
| dfe1d0477d | |||
| a1aa6d7ecd | |||
| 6f01e9f76b | |||
| be8b96280e |
@@ -25,7 +25,7 @@ jobs:
|
|||||||
echo "JADX_VERSION=$JADX_VERSION" >> $GITHUB_ENV
|
echo "JADX_VERSION=$JADX_VERSION" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@v5
|
uses: gradle/actions/setup-gradle@v6
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: ./gradlew dist distWin
|
run: ./gradlew dist distWin
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
JADX_BUILD_JAVA_VERSION: 11
|
JADX_BUILD_JAVA_VERSION: 11
|
||||||
|
|
||||||
- name: Save bundle artifact
|
- name: Save bundle artifact
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: ${{ format('jadx-{0}', env.JADX_VERSION) }}
|
name: ${{ format('jadx-{0}', env.JADX_VERSION) }}
|
||||||
# Waiting fix for https://github.com/actions/upload-artifact/issues/39 to upload zip file
|
# Waiting fix for https://github.com/actions/upload-artifact/issues/39 to upload zip file
|
||||||
@@ -43,7 +43,7 @@ jobs:
|
|||||||
retention-days: 14
|
retention-days: 14
|
||||||
|
|
||||||
- name: Save Windows bundle artifact
|
- name: Save Windows bundle artifact
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: ${{ format('jadx-gui-{0}-no-jre-win', env.JADX_VERSION) }}
|
name: ${{ format('jadx-gui-{0}-no-jre-win', env.JADX_VERSION) }}
|
||||||
# Upload unpacked files for now
|
# Upload unpacked files for now
|
||||||
@@ -75,13 +75,13 @@ jobs:
|
|||||||
echo "JADX_VERSION=$JADX_VERSION" >> $GITHUB_ENV
|
echo "JADX_VERSION=$JADX_VERSION" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@v5
|
uses: gradle/actions/setup-gradle@v6
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: ./gradlew dist -PbundleJRE=true
|
run: ./gradlew dist -PbundleJRE=true
|
||||||
|
|
||||||
- name: Save Windows with JRE bundle artifact
|
- name: Save Windows with JRE bundle artifact
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: ${{ format('jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }}
|
name: ${{ format('jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }}
|
||||||
# Upload unpacked files for now
|
# Upload unpacked files for now
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
java-version: 25
|
java-version: 25
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@v5
|
uses: gradle/actions/setup-gradle@v6
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: ./gradlew build dist distWin
|
run: ./gradlew build dist distWin
|
||||||
|
|||||||
@@ -21,20 +21,20 @@ jobs:
|
|||||||
release: 25
|
release: 25
|
||||||
|
|
||||||
- name: Set jadx version
|
- name: Set jadx version
|
||||||
uses: actions/github-script@v8
|
uses: actions/github-script@v9
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const jadxVersion = context.ref.split('/').pop().substring(1)
|
const jadxVersion = context.ref.split('/').pop().substring(1)
|
||||||
core.exportVariable('JADX_VERSION', jadxVersion);
|
core.exportVariable('JADX_VERSION', jadxVersion);
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@v5
|
uses: gradle/actions/setup-gradle@v6
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: ./gradlew dist -PbundleJRE=true
|
run: ./gradlew dist -PbundleJRE=true
|
||||||
|
|
||||||
- name: Save JRE bundle artifact
|
- name: Save JRE bundle artifact
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: ${{ format('jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }}
|
name: ${{ format('jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }}
|
||||||
path: ${{ format('build/distWinWithJre/jadx-gui-{0}-with-jre-win.zip', env.JADX_VERSION) }}
|
path: ${{ format('build/distWinWithJre/jadx-gui-{0}-with-jre-win.zip', env.JADX_VERSION) }}
|
||||||
@@ -54,14 +54,14 @@ jobs:
|
|||||||
java-version: 25
|
java-version: 25
|
||||||
|
|
||||||
- name: Set jadx version and release name
|
- name: Set jadx version and release name
|
||||||
uses: actions/github-script@v8
|
uses: actions/github-script@v9
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const jadxVersion = context.ref.split('/').pop().substring(1)
|
const jadxVersion = context.ref.split('/').pop().substring(1)
|
||||||
core.exportVariable('JADX_VERSION', jadxVersion);
|
core.exportVariable('JADX_VERSION', jadxVersion);
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@v5
|
uses: gradle/actions/setup-gradle@v6
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: ./gradlew dist distWin
|
run: ./gradlew dist distWin
|
||||||
@@ -69,7 +69,7 @@ jobs:
|
|||||||
JADX_BUILD_JAVA_VERSION: 11
|
JADX_BUILD_JAVA_VERSION: 11
|
||||||
|
|
||||||
- name: Download Windows JRE bundle
|
- name: Download Windows JRE bundle
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: ${{ format('jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }}
|
name: ${{ format('jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }}
|
||||||
path: ${{ format('build/jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }}
|
path: ${{ format('build/jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }}
|
||||||
@@ -84,7 +84,7 @@ jobs:
|
|||||||
ls -l *.zip
|
ls -l *.zip
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v3
|
||||||
with:
|
with:
|
||||||
name: ${{ env.JADX_VERSION }}
|
name: ${{ env.JADX_VERSION }}
|
||||||
draft: true
|
draft: true
|
||||||
|
|||||||
+4
-1
@@ -37,7 +37,10 @@ jadx-output/
|
|||||||
*.log
|
*.log
|
||||||
*.cfg
|
*.cfg
|
||||||
*.orig
|
*.orig
|
||||||
quark.json
|
*.json
|
||||||
|
*.dot
|
||||||
|
|
||||||
|
.env
|
||||||
|
|
||||||
cliff.toml
|
cliff.toml
|
||||||
jadx-gui/src/main/resources/logback.xml
|
jadx-gui/src/main/resources/logback.xml
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ For Windows, you can download it from [oracle.com](https://www.oracle.com/java/t
|
|||||||
You can use jadx in your java projects, check details on [wiki page](https://github.com/skylot/jadx/wiki/Use-jadx-as-a-library)
|
You can use jadx in your java projects, check details on [wiki page](https://github.com/skylot/jadx/wiki/Use-jadx-as-a-library)
|
||||||
|
|
||||||
### Build from source
|
### Build from source
|
||||||
JDK 11 or higher must be installed:
|
JDK 17 or higher must be installed:
|
||||||
```
|
```
|
||||||
git clone https://github.com/skylot/jadx.git
|
git clone https://github.com/skylot/jadx.git
|
||||||
cd jadx
|
cd jadx
|
||||||
@@ -166,6 +166,7 @@ options:
|
|||||||
--fs-case-sensitive - treat filesystem as case sensitive, false by default
|
--fs-case-sensitive - treat filesystem as case sensitive, false by default
|
||||||
--cfg - save methods control flow graph to dot file
|
--cfg - save methods control flow graph to dot file
|
||||||
--raw-cfg - save methods control flow graph (use raw instructions)
|
--raw-cfg - save methods control flow graph (use raw instructions)
|
||||||
|
--call-graph - save app call graph in format: 'dot' or 'json', default: none
|
||||||
-f, --fallback - set '--decompilation-mode' to 'fallback' (deprecated)
|
-f, --fallback - set '--decompilation-mode' to 'fallback' (deprecated)
|
||||||
--use-dx - use dx/d8 to convert java bytecode
|
--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
|
--comments-level - set code comments level, values: error, warn, info, debug, user-only, none, default: info
|
||||||
|
|||||||
+58
-5
@@ -6,12 +6,14 @@ 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.53.0"
|
id("com.github.ben-manes.versions") version "0.54.0"
|
||||||
id("se.patrikerdes.use-latest-versions") version "0.2.19"
|
id("se.patrikerdes.use-latest-versions") version "0.2.19"
|
||||||
id("com.diffplug.spotless") version "6.25.0"
|
id("com.diffplug.spotless") version "8.7.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
val jadxVersion by extra { System.getenv("JADX_VERSION") ?: "dev" }
|
val jadxEnv = loadEnv(file("$rootDir/.env"))
|
||||||
|
|
||||||
|
val jadxVersion by extra { jadxEnv["JADX_VERSION"] ?: "dev" }
|
||||||
println("jadx version: $jadxVersion")
|
println("jadx version: $jadxVersion")
|
||||||
version = jadxVersion
|
version = jadxVersion
|
||||||
|
|
||||||
@@ -19,7 +21,7 @@ val jadxBuildJavaVersion by extra { getBuildJavaVersion() }
|
|||||||
|
|
||||||
fun getBuildJavaVersion(): Int? {
|
fun getBuildJavaVersion(): Int? {
|
||||||
val envVarName = "JADX_BUILD_JAVA_VERSION"
|
val envVarName = "JADX_BUILD_JAVA_VERSION"
|
||||||
val buildJavaVer = System.getenv(envVarName)?.toInt() ?: return null
|
val buildJavaVer = jadxEnv[envVarName]?.toInt() ?: return null
|
||||||
if (buildJavaVer < 11) {
|
if (buildJavaVer < 11) {
|
||||||
throw GradleException("'$envVarName' can't be set to lower than 11")
|
throw GradleException("'$envVarName' can't be set to lower than 11")
|
||||||
}
|
}
|
||||||
@@ -27,6 +29,24 @@ fun getBuildJavaVersion(): Int? {
|
|||||||
return buildJavaVer
|
return buildJavaVer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// control ErrorProne checks level, can be: off, warn, error
|
||||||
|
val jadxBuildChecksMode: String by extra { getBuildChecksMode() }
|
||||||
|
|
||||||
|
fun getBuildChecksMode(): String {
|
||||||
|
val buildChecksMode = jadxEnv["JADX_BUILD_CHECKS_MODE"]?.lowercase() ?: "off"
|
||||||
|
val expectedValues = listOf("off", "warn", "error")
|
||||||
|
if (!expectedValues.contains(buildChecksMode)) {
|
||||||
|
throw GradleException("Unknown check mode: '$buildChecksMode', should be one of $expectedValues")
|
||||||
|
}
|
||||||
|
if (buildChecksMode != "off") {
|
||||||
|
val javaVersion = jadxBuildJavaVersion?.let { JavaVersion.toVersion(it) } ?: JavaVersion.current()
|
||||||
|
if (!javaVersion.isCompatibleWith(JavaVersion.VERSION_21)) {
|
||||||
|
throw GradleException("Error Prone requires Java 21")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buildChecksMode
|
||||||
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
apply(plugin = "java")
|
apply(plugin = "java")
|
||||||
apply(plugin = "checkstyle")
|
apply(plugin = "checkstyle")
|
||||||
@@ -82,6 +102,30 @@ fun isNonStable(version: String): Boolean {
|
|||||||
return isStable.not()
|
return isStable.not()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun loadEnv(file: File): Map<String, String> {
|
||||||
|
val envMap = HashMap<String, String>()
|
||||||
|
System
|
||||||
|
.getenv()
|
||||||
|
.filter { it.key.startsWith("JADX_") }
|
||||||
|
.forEach { envMap[it.key] = it.value }
|
||||||
|
if (file.exists()) {
|
||||||
|
file
|
||||||
|
.readLines()
|
||||||
|
.map { it.trim() }
|
||||||
|
.filter { it.isNotEmpty() && !it.startsWith("#") }
|
||||||
|
.forEach {
|
||||||
|
val (k, v) = it.split("=", limit = 2)
|
||||||
|
envMap[k.trim()] = v.trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println(
|
||||||
|
"Loaded env vars (${envMap.size}):\n${
|
||||||
|
envMap.toList().sortedBy { it.first }.joinToString(separator = "\n") { "${it.first}=${it.second}" }
|
||||||
|
}\n",
|
||||||
|
)
|
||||||
|
return envMap
|
||||||
|
}
|
||||||
|
|
||||||
val distWinConfiguration: Configuration by configurations.creating {
|
val distWinConfiguration: Configuration by configurations.creating {
|
||||||
isCanBeConsumed = false
|
isCanBeConsumed = false
|
||||||
}
|
}
|
||||||
@@ -98,7 +142,9 @@ val copyArtifacts by tasks.registering(Copy::class) {
|
|||||||
from(tasks.getByPath(":jadx-cli:installShadowDist")) {
|
from(tasks.getByPath(":jadx-cli:installShadowDist")) {
|
||||||
exclude("**/*.jar")
|
exclude("**/*.jar")
|
||||||
filter { line ->
|
filter { line ->
|
||||||
jarCliPattern.matcher(line).replaceAll("jadx-$1-all.jar")
|
jarCliPattern
|
||||||
|
.matcher(line)
|
||||||
|
.replaceAll("jadx-$1-all.jar")
|
||||||
.replace("-jar \"\\\"\$CLASSPATH\\\"\"", "-cp \"\\\"\$CLASSPATH\\\"\" jadx.cli.JadxCLI")
|
.replace("-jar \"\\\"\$CLASSPATH\\\"\"", "-cp \"\\\"\$CLASSPATH\\\"\" jadx.cli.JadxCLI")
|
||||||
.replace("-jar \"%CLASSPATH%\"", "-cp \"%CLASSPATH%\" jadx.cli.JadxCLI")
|
.replace("-jar \"%CLASSPATH%\"", "-cp \"%CLASSPATH%\" jadx.cli.JadxCLI")
|
||||||
}
|
}
|
||||||
@@ -124,6 +170,13 @@ val pack by tasks.registering(Zip::class) {
|
|||||||
from(copyArtifacts)
|
from(copyArtifacts)
|
||||||
archiveFileName.set("jadx-$jadxVersion.zip")
|
archiveFileName.set("jadx-$jadxVersion.zip")
|
||||||
destinationDirectory.set(layout.buildDirectory)
|
destinationDirectory.set(layout.buildDirectory)
|
||||||
|
eachFile {
|
||||||
|
if (path == "bin/jadx" || path == "bin/jadx-gui") {
|
||||||
|
permissions {
|
||||||
|
unix("rwxr-xr-x")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val distWin by tasks.registering(Zip::class) {
|
val distWin by tasks.registering(Zip::class) {
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ dependencies {
|
|||||||
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.10")
|
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.10")
|
||||||
|
|
||||||
implementation("org.openrewrite:plugin:6.19.1")
|
implementation("org.openrewrite:plugin:6.19.1")
|
||||||
|
implementation("net.ltgt.errorprone:net.ltgt.errorprone.gradle.plugin:4.2.0")
|
||||||
|
implementation("net.ltgt.nullaway:net.ltgt.nullaway.gradle.plugin:2.3.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
|
|||||||
@@ -1,29 +1,38 @@
|
|||||||
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
|
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
|
||||||
|
import net.ltgt.gradle.errorprone.CheckSeverity
|
||||||
|
import net.ltgt.gradle.errorprone.errorprone
|
||||||
|
import net.ltgt.gradle.nullaway.nullaway
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
java
|
java
|
||||||
checkstyle
|
checkstyle
|
||||||
|
|
||||||
id("jadx-rewrite")
|
id("jadx-rewrite")
|
||||||
|
id("net.ltgt.errorprone")
|
||||||
|
id("net.ltgt.nullaway")
|
||||||
}
|
}
|
||||||
|
|
||||||
val jadxVersion: String by rootProject.extra
|
val jadxVersion: String by rootProject.extra
|
||||||
val jadxBuildJavaVersion: Int? by rootProject.extra
|
val jadxBuildJavaVersion: Int? by rootProject.extra
|
||||||
|
val jadxBuildChecksMode: String by rootProject.extra
|
||||||
|
|
||||||
group = "io.github.skylot"
|
group = "io.github.skylot"
|
||||||
version = jadxVersion
|
version = jadxVersion
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("org.slf4j:slf4j-api:2.0.17")
|
implementation("org.slf4j:slf4j-api:2.0.18")
|
||||||
compileOnly("org.jetbrains:annotations:26.0.2")
|
compileOnly("org.jetbrains:annotations:26.1.0")
|
||||||
|
|
||||||
testImplementation("ch.qos.logback:logback-classic:1.5.22")
|
testImplementation("ch.qos.logback:logback-classic:1.5.34")
|
||||||
testImplementation("org.assertj:assertj-core:3.27.6")
|
testImplementation("org.assertj:assertj-core:3.27.7")
|
||||||
|
|
||||||
testImplementation("org.junit.jupiter:junit-jupiter:5.13.3")
|
testImplementation("org.junit.jupiter:junit-jupiter:5.13.3")
|
||||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||||
|
|
||||||
testCompileOnly("org.jetbrains:annotations:26.0.2")
|
testCompileOnly("org.jetbrains:annotations:26.1.0")
|
||||||
|
|
||||||
|
errorprone("com.google.errorprone:error_prone_core:2.50.0")
|
||||||
|
errorprone("com.uber.nullaway:nullaway:0.13.7")
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
@@ -62,3 +71,27 @@ tasks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.withType<JavaCompile>().configureEach {
|
||||||
|
val checkEnabled = jadxBuildChecksMode != "off"
|
||||||
|
if (checkEnabled) {
|
||||||
|
options.compilerArgs.add("-XDaddTypeAnnotationsToSymbol=true")
|
||||||
|
}
|
||||||
|
options.errorprone {
|
||||||
|
isEnabled = checkEnabled
|
||||||
|
allErrorsAsWarnings = jadxBuildChecksMode == "warn"
|
||||||
|
excludedPaths = ".*/test/.*"
|
||||||
|
nullaway {
|
||||||
|
if (jadxBuildChecksMode == "error") {
|
||||||
|
error()
|
||||||
|
}
|
||||||
|
annotatedPackages.add("jadx")
|
||||||
|
}
|
||||||
|
// TODO: fix and enable all checks
|
||||||
|
disable("MixedMutabilityReturnType")
|
||||||
|
disable("EqualsGetClass")
|
||||||
|
disable("OperatorPrecedence")
|
||||||
|
disable("UnusedVariable")
|
||||||
|
disable("ImmutableEnumChecker")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
rewrite("org.openrewrite.recipe:rewrite-testing-frameworks:3.24.0")
|
rewrite("org.openrewrite.recipe:rewrite-testing-frameworks:3.38.0")
|
||||||
rewrite("org.openrewrite.recipe:rewrite-logging-frameworks:3.20.0")
|
rewrite("org.openrewrite.recipe:rewrite-logging-frameworks:3.29.1")
|
||||||
rewrite("org.openrewrite.recipe:rewrite-migrate-java:3.24.0")
|
rewrite("org.openrewrite.recipe:rewrite-migrate-java:3.37.0")
|
||||||
rewrite("org.openrewrite.recipe:rewrite-static-analysis:2.24.0")
|
rewrite("org.openrewrite.recipe:rewrite-static-analysis:2.37.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
|
|||||||
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=72f44c9f8ebcb1af43838f45ee5c4aa9c5444898b3468ab3f4af7b6076c5bc3f
|
distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
# Darwin, MinGW, and NonStop.
|
# Darwin, MinGW, and NonStop.
|
||||||
#
|
#
|
||||||
# (3) This script is generated from the Groovy template
|
# (3) This script is generated from the Groovy template
|
||||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
# within the Gradle project.
|
# within the Gradle project.
|
||||||
#
|
#
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ dependencies {
|
|||||||
implementation(project(":jadx-core"))
|
implementation(project(":jadx-core"))
|
||||||
implementation(project(":jadx-plugins-tools"))
|
implementation(project(":jadx-plugins-tools"))
|
||||||
implementation(project(":jadx-commons:jadx-app-commons"))
|
implementation(project(":jadx-commons:jadx-app-commons"))
|
||||||
|
implementation(project(":jadx-commons:jadx-analysis"))
|
||||||
|
|
||||||
runtimeOnly(project(":jadx-plugins:jadx-dex-input"))
|
runtimeOnly(project(":jadx-plugins:jadx-dex-input"))
|
||||||
runtimeOnly(project(":jadx-plugins:jadx-java-input"))
|
runtimeOnly(project(":jadx-plugins:jadx-java-input"))
|
||||||
@@ -25,8 +26,8 @@ dependencies {
|
|||||||
runtimeOnly(project(":jadx-plugins:jadx-apks-input"))
|
runtimeOnly(project(":jadx-plugins:jadx-apks-input"))
|
||||||
|
|
||||||
implementation("org.jcommander:jcommander:2.0")
|
implementation("org.jcommander:jcommander:2.0")
|
||||||
implementation("ch.qos.logback:logback-classic:1.5.22")
|
implementation("ch.qos.logback:logback-classic:1.5.34")
|
||||||
implementation("com.google.code.gson:gson:2.13.2")
|
implementation("com.google.code.gson:gson:2.14.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package jadx.cli;
|
package jadx.cli;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import jadx.analysis.callgraph.JadxCallGraph;
|
||||||
|
import jadx.analysis.callgraph.api.ICallGraph;
|
||||||
import jadx.api.JadxArgs;
|
import jadx.api.JadxArgs;
|
||||||
import jadx.api.JadxDecompiler;
|
import jadx.api.JadxDecompiler;
|
||||||
import jadx.api.impl.AnnotatedCodeWriter;
|
import jadx.api.impl.AnnotatedCodeWriter;
|
||||||
@@ -16,6 +19,7 @@ import jadx.cli.LogHelper.LogLevelEnum;
|
|||||||
import jadx.cli.config.JadxConfigAdapter;
|
import jadx.cli.config.JadxConfigAdapter;
|
||||||
import jadx.cli.plugins.JadxFilesGetter;
|
import jadx.cli.plugins.JadxFilesGetter;
|
||||||
import jadx.core.utils.exceptions.JadxArgsValidateException;
|
import jadx.core.utils.exceptions.JadxArgsValidateException;
|
||||||
|
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||||
import jadx.plugins.tools.JadxExternalPluginsLoader;
|
import jadx.plugins.tools.JadxExternalPluginsLoader;
|
||||||
|
|
||||||
public class JadxCLI {
|
public class JadxCLI {
|
||||||
@@ -73,6 +77,7 @@ public class JadxCLI {
|
|||||||
if (checkForErrors(jadx)) {
|
if (checkForErrors(jadx)) {
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
writeCallGraph(jadx, cliArgs);
|
||||||
if (!SingleClassMode.process(jadx, cliArgs)) {
|
if (!SingleClassMode.process(jadx, cliArgs)) {
|
||||||
save(jadx);
|
save(jadx);
|
||||||
}
|
}
|
||||||
@@ -131,4 +136,29 @@ public class JadxCLI {
|
|||||||
System.out.print(" \r");
|
System.out.print(" \r");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void writeCallGraph(JadxDecompiler jadx, JadxCLIArgs cliArgs) {
|
||||||
|
JadxCLIArgs.CallGraphSaveMode mode = cliArgs.callGraphSaveMode;
|
||||||
|
if (mode == null || mode == JadxCLIArgs.CallGraphSaveMode.NONE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Path outPath = jadx.getArgs().getOutDir().toPath();
|
||||||
|
ICallGraph callGraph = JadxCallGraph.builder(jadx)
|
||||||
|
.resolvedOnly(true)
|
||||||
|
.build();
|
||||||
|
Path cgPath;
|
||||||
|
switch (mode) {
|
||||||
|
case JSON:
|
||||||
|
cgPath = outPath.resolve("callgraph.json");
|
||||||
|
callGraph.writeJson(cgPath);
|
||||||
|
break;
|
||||||
|
case DOT:
|
||||||
|
cgPath = outPath.resolve("callgraph.dot");
|
||||||
|
callGraph.writeDot(cgPath);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new JadxRuntimeException("Unexpected call graph save mode: " + mode);
|
||||||
|
}
|
||||||
|
LOG.info("Call graph saved: {}", cgPath.toAbsolutePath());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -283,6 +283,13 @@ public class JadxCLIArgs implements IJadxConfig {
|
|||||||
@Parameter(names = { "--raw-cfg" }, description = "save methods control flow graph (use raw instructions)")
|
@Parameter(names = { "--raw-cfg" }, description = "save methods control flow graph (use raw instructions)")
|
||||||
protected boolean rawCfgOutput = false;
|
protected boolean rawCfgOutput = false;
|
||||||
|
|
||||||
|
@Parameter(
|
||||||
|
names = { "--call-graph" },
|
||||||
|
description = "save app call graph in format: 'dot' or 'json'",
|
||||||
|
converter = CallGraphSaveModeConverter.class
|
||||||
|
)
|
||||||
|
protected CallGraphSaveMode callGraphSaveMode = CallGraphSaveMode.NONE;
|
||||||
|
|
||||||
@Parameter(names = { "-f", "--fallback" }, description = "set '--decompilation-mode' to 'fallback' (deprecated)")
|
@Parameter(names = { "-f", "--fallback" }, description = "set '--decompilation-mode' to 'fallback' (deprecated)")
|
||||||
protected boolean fallbackMode = false;
|
protected boolean fallbackMode = false;
|
||||||
|
|
||||||
@@ -827,6 +834,14 @@ public class JadxCLIArgs implements IJadxConfig {
|
|||||||
this.rawCfgOutput = rawCfgOutput;
|
this.rawCfgOutput = rawCfgOutput;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CallGraphSaveMode getCallGraphSaveMode() {
|
||||||
|
return callGraphSaveMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCallGraphSaveMode(CallGraphSaveMode callGraphSaveMode) {
|
||||||
|
this.callGraphSaveMode = callGraphSaveMode;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isReplaceConsts() {
|
public boolean isReplaceConsts() {
|
||||||
return replaceConsts;
|
return replaceConsts;
|
||||||
}
|
}
|
||||||
@@ -1022,6 +1037,18 @@ public class JadxCLIArgs implements IJadxConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum CallGraphSaveMode {
|
||||||
|
NONE,
|
||||||
|
DOT,
|
||||||
|
JSON,
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class CallGraphSaveModeConverter extends BaseEnumConverter<CallGraphSaveMode> {
|
||||||
|
public CallGraphSaveModeConverter() {
|
||||||
|
super(CallGraphSaveMode::valueOf, CallGraphSaveMode::values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public abstract static class BaseEnumConverter<E extends Enum<E>> implements IStringConverter<E> {
|
public abstract static class BaseEnumConverter<E extends Enum<E>> implements IStringConverter<E> {
|
||||||
private final Function<String, E> parse;
|
private final Function<String, E> parse;
|
||||||
private final Supplier<E[]> values;
|
private final Supplier<E[]> values;
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ public class LogHelper {
|
|||||||
setLevelForClass(JadxCLI.class, Level.INFO);
|
setLevelForClass(JadxCLI.class, Level.INFO);
|
||||||
setLevelForClass(JadxDecompiler.class, Level.INFO);
|
setLevelForClass(JadxDecompiler.class, Level.INFO);
|
||||||
setLevelForClass(SingleClassMode.class, Level.INFO);
|
setLevelForClass(SingleClassMode.class, Level.INFO);
|
||||||
|
|
||||||
|
// show warnings and errors from input plugins
|
||||||
|
setLevelForPackage("jadx.plugins.input", Level.WARN);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void applyLogLevel(@NotNull LogLevelEnum logLevel) {
|
private static void applyLogLevel(@NotNull LogLevelEnum logLevel) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
<!-- jadx-gui -->
|
<!-- jadx-gui -->
|
||||||
<logger name="com.pinterest.ktlint" level="INFO"/>
|
<logger name="com.pinterest.ktlint" level="INFO"/>
|
||||||
|
<logger name="guru.nidi.graphviz" level="WARN"/>
|
||||||
|
|
||||||
<root level="INFO">
|
<root level="INFO">
|
||||||
<appender-ref ref="STDOUT"/>
|
<appender-ref ref="STDOUT"/>
|
||||||
|
|||||||
@@ -73,4 +73,12 @@ public class TestInput extends BaseCliIntegrationTest {
|
|||||||
path -> path.getFileName().toString().equalsIgnoreCase("AndroidManifest.xml"));
|
path -> path.getFileName().toString().equalsIgnoreCase("AndroidManifest.xml"));
|
||||||
assertThat(files).isNotEmpty();
|
assertThat(files).isNotEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNoRenameForDefPkg() throws Exception {
|
||||||
|
int result = execJadxCli(buildArgs(List.of("--rename-flags", "none"), "samples/defpkg.smali"));
|
||||||
|
assertThat(result).isEqualTo(0);
|
||||||
|
List<Path> files = collectJavaFilesInDir(outputDir);
|
||||||
|
assertThat(files).hasSize(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
.class public LA;
|
||||||
|
.super Ljava/lang/Object;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
## jadx analysis
|
||||||
|
|
||||||
|
Various utilities for analyze and process code and related information.
|
||||||
|
|
||||||
|
|
||||||
|
### Call graph
|
||||||
|
|
||||||
|
Full app code usage/call graph.
|
||||||
|
Usage:
|
||||||
|
```java
|
||||||
|
JadxArgs args = new JadxArgs();
|
||||||
|
args.addInputFile(new File("input.apk"));
|
||||||
|
try (JadxDecompiler jadx = new JadxDecompiler(args)) {
|
||||||
|
jadx.load();
|
||||||
|
|
||||||
|
ICallGraph callGraph = JadxCallGraph.builder(jadx)
|
||||||
|
.includePackages("com.example") // filter nodes by package
|
||||||
|
.resolvedOnly(true) // add nodes only from app (exclude framework/lib calls)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
for (ICallGraphEdge edge : callGraph.edges()) {
|
||||||
|
if (edge.isResolved()) {
|
||||||
|
System.out.printf("Edge from '%s' to '%s'%n", edge.from(), edge.to());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callGraph.writeDot(Path.of("test.dot")); // export to '.dot'
|
||||||
|
callGraph.writeJson(Path.of("test.json")); // export to JSON
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
plugins {
|
||||||
|
id("jadx-library")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":jadx-core"))
|
||||||
|
|
||||||
|
implementation("com.google.code.gson:gson:2.14.0")
|
||||||
|
|
||||||
|
testRuntimeOnly(project(":jadx-plugins:jadx-dex-input"))
|
||||||
|
testRuntimeOnly(project(":jadx-plugins:jadx-smali-input"))
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package jadx.analysis.callgraph;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import jadx.analysis.callgraph.api.ICallGraph;
|
||||||
|
import jadx.analysis.callgraph.api.ICallGraphEdge;
|
||||||
|
import jadx.api.JadxArgs;
|
||||||
|
|
||||||
|
class CallGraph implements ICallGraph {
|
||||||
|
|
||||||
|
private final JadxArgs args;
|
||||||
|
private final List<ICallGraphEdge> edges;
|
||||||
|
|
||||||
|
public CallGraph(JadxArgs args, List<ICallGraphEdge> edges) {
|
||||||
|
this.args = args;
|
||||||
|
this.edges = edges;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ICallGraphEdge> edges() {
|
||||||
|
return edges;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeDot(Path path) {
|
||||||
|
new CallGraphExportDot(args, this).writeTo(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeJson(Path path) {
|
||||||
|
new CallGraphExportJson(this).writeTo(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
package jadx.analysis.callgraph;
|
||||||
|
|
||||||
|
import jadx.core.dex.attributes.AttrNode;
|
||||||
|
|
||||||
|
class CallGraphAttrNode extends AttrNode {
|
||||||
|
}
|
||||||
+92
@@ -0,0 +1,92 @@
|
|||||||
|
package jadx.analysis.callgraph;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import jadx.analysis.callgraph.api.ICallGraph;
|
||||||
|
import jadx.analysis.callgraph.api.ICallGraphBuilder;
|
||||||
|
import jadx.analysis.callgraph.api.ICallGraphEdge;
|
||||||
|
import jadx.api.JadxDecompiler;
|
||||||
|
import jadx.core.dex.info.ClassInfo;
|
||||||
|
import jadx.core.dex.info.MethodInfo;
|
||||||
|
import jadx.core.dex.nodes.ClassNode;
|
||||||
|
import jadx.core.dex.nodes.MethodNode;
|
||||||
|
|
||||||
|
class CallGraphBuilder implements ICallGraphBuilder {
|
||||||
|
private final JadxDecompiler decompiler;
|
||||||
|
private boolean resolvedOnly = false;
|
||||||
|
private @Nullable String pkgFilter;
|
||||||
|
|
||||||
|
public CallGraphBuilder(JadxDecompiler decompiler) {
|
||||||
|
this.decompiler = decompiler;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ICallGraphBuilder resolvedOnly(boolean resolved) {
|
||||||
|
this.resolvedOnly = resolved;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ICallGraphBuilder includePackages(String pkgFilter) {
|
||||||
|
this.pkgFilter = pkgFilter.endsWith(".") ? pkgFilter : pkgFilter + '.';
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ICallGraph build() {
|
||||||
|
return new CallGraph(decompiler.getArgs(), collectEdges());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ICallGraphEdge> collectEdges() {
|
||||||
|
AtomicInteger nodeId = new AtomicInteger();
|
||||||
|
Map<MethodInfo, CallGraphNode> nodes = new HashMap<>();
|
||||||
|
List<ICallGraphEdge> edges = new ArrayList<>();
|
||||||
|
|
||||||
|
for (ClassNode cls : decompiler.getRoot().getClasses(true)) {
|
||||||
|
if (ignorePkg(cls.getClassInfo())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (MethodNode mth : cls.getMethods()) {
|
||||||
|
CallGraphNode thisNode = getCallGraphNode(mth, nodes, nodeId);
|
||||||
|
for (MethodNode use : mth.getUseIn()) {
|
||||||
|
if (ignorePkg(use.getDeclaringClass().getClassInfo())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
CallGraphNode useInNode = getCallGraphNode(use, nodes, nodeId);
|
||||||
|
edges.add(new CallGraphEdge(useInNode, thisNode));
|
||||||
|
}
|
||||||
|
if (!resolvedOnly) {
|
||||||
|
for (MethodInfo used : mth.getUnresolvedUsed()) {
|
||||||
|
if (ignorePkg(used.getDeclClass())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
CallGraphNode usedNode = getCallGraphNode(used, nodes, nodeId);
|
||||||
|
edges.add(new CallGraphEdge(thisNode, usedNode));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return edges;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean ignorePkg(ClassInfo cls) {
|
||||||
|
if (pkgFilter == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !cls.getFullName().startsWith(pkgFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CallGraphNode getCallGraphNode(MethodNode mth, Map<MethodInfo, CallGraphNode> nodes, AtomicInteger nodeId) {
|
||||||
|
return nodes.computeIfAbsent(mth.getMethodInfo(), i -> new CallGraphNode(nodeId.incrementAndGet(), mth));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CallGraphNode getCallGraphNode(MethodInfo mth, Map<MethodInfo, CallGraphNode> nodes, AtomicInteger nodeId) {
|
||||||
|
return nodes.computeIfAbsent(mth, i -> new CallGraphNode(nodeId.incrementAndGet(), mth));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package jadx.analysis.callgraph;
|
||||||
|
|
||||||
|
import jadx.analysis.callgraph.api.ICallGraphEdge;
|
||||||
|
import jadx.analysis.callgraph.api.ICallGraphNode;
|
||||||
|
import jadx.core.dex.attributes.IAttributeNode;
|
||||||
|
|
||||||
|
class CallGraphEdge implements ICallGraphEdge {
|
||||||
|
private final ICallGraphNode from;
|
||||||
|
private final ICallGraphNode to;
|
||||||
|
private final CallGraphAttrNode attrNode = new CallGraphAttrNode();
|
||||||
|
|
||||||
|
public CallGraphEdge(ICallGraphNode from, ICallGraphNode to) {
|
||||||
|
this.from = from;
|
||||||
|
this.to = to;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ICallGraphNode from() {
|
||||||
|
return from;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ICallGraphNode to() {
|
||||||
|
return to;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isResolved() {
|
||||||
|
return to.isResolved();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IAttributeNode attributes() {
|
||||||
|
return attrNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "CallGraphEdge{from=" + from + ", to=" + to + '}';
|
||||||
|
}
|
||||||
|
}
|
||||||
+92
@@ -0,0 +1,92 @@
|
|||||||
|
package jadx.analysis.callgraph;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import jadx.analysis.callgraph.api.ICallGraph;
|
||||||
|
import jadx.analysis.callgraph.api.ICallGraphEdge;
|
||||||
|
import jadx.analysis.callgraph.api.ICallGraphNode;
|
||||||
|
import jadx.api.ICodeWriter;
|
||||||
|
import jadx.api.JadxArgs;
|
||||||
|
import jadx.api.impl.SimpleCodeWriter;
|
||||||
|
import jadx.core.utils.DotGraphUtils;
|
||||||
|
import jadx.core.utils.files.FileUtils;
|
||||||
|
|
||||||
|
import static java.nio.file.StandardOpenOption.CREATE;
|
||||||
|
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
|
||||||
|
import static java.nio.file.StandardOpenOption.WRITE;
|
||||||
|
|
||||||
|
public class CallGraphExportDot {
|
||||||
|
private final JadxArgs args;
|
||||||
|
private final ICallGraph callGraph;
|
||||||
|
|
||||||
|
public CallGraphExportDot(JadxArgs args, ICallGraph callGraph) {
|
||||||
|
this.args = args;
|
||||||
|
this.callGraph = callGraph;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeTo(Path path) {
|
||||||
|
try {
|
||||||
|
FileUtils.makeDirsForFile(path);
|
||||||
|
Files.writeString(path, writeToString(), StandardCharsets.UTF_8,
|
||||||
|
WRITE, TRUNCATE_EXISTING, CREATE);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to save JSON file: " + path, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String writeToString() {
|
||||||
|
// collect nodes
|
||||||
|
Map<Integer, Node> nodeMap = new HashMap<>();
|
||||||
|
for (ICallGraphEdge edge : callGraph.edges()) {
|
||||||
|
addNode(edge.from(), nodeMap);
|
||||||
|
addNode(edge.to(), nodeMap);
|
||||||
|
}
|
||||||
|
List<Node> nodes = new ArrayList<>(nodeMap.values());
|
||||||
|
nodes.sort(Comparator.comparingInt(o -> o.id));
|
||||||
|
|
||||||
|
SimpleCodeWriter cw = new SimpleCodeWriter(args);
|
||||||
|
cw.add("digraph CallGraph {");
|
||||||
|
for (Node node : nodes) {
|
||||||
|
cw.startLine();
|
||||||
|
addNodeName(cw, node.id);
|
||||||
|
cw.add("[shape=record,label=\"{");
|
||||||
|
cw.add(DotGraphUtils.escape(node.method));
|
||||||
|
cw.add("}\"];");
|
||||||
|
}
|
||||||
|
for (ICallGraphEdge edge : callGraph.edges()) {
|
||||||
|
cw.startLine();
|
||||||
|
addNodeName(cw, edge.from().getId());
|
||||||
|
cw.add(" -> ");
|
||||||
|
addNodeName(cw, edge.to().getId());
|
||||||
|
cw.add(';');
|
||||||
|
}
|
||||||
|
cw.startLine('}');
|
||||||
|
return cw.getCodeStr();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addNodeName(ICodeWriter cw, int id) {
|
||||||
|
cw.add('N').add(Integer.toString(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addNode(ICallGraphNode cgNode, Map<Integer, Node> nodeMap) {
|
||||||
|
nodeMap.computeIfAbsent(cgNode.getId(), id -> {
|
||||||
|
Node node = new Node();
|
||||||
|
node.id = id;
|
||||||
|
node.method = cgNode.getMethodInfo().getRawFullId();
|
||||||
|
return node;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class Node {
|
||||||
|
int id;
|
||||||
|
String method;
|
||||||
|
}
|
||||||
|
}
|
||||||
+97
@@ -0,0 +1,97 @@
|
|||||||
|
package jadx.analysis.callgraph;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.google.gson.Strictness;
|
||||||
|
|
||||||
|
import jadx.analysis.callgraph.api.ICallGraph;
|
||||||
|
import jadx.analysis.callgraph.api.ICallGraphEdge;
|
||||||
|
import jadx.analysis.callgraph.api.ICallGraphNode;
|
||||||
|
import jadx.core.utils.files.FileUtils;
|
||||||
|
|
||||||
|
import static java.nio.file.StandardOpenOption.CREATE;
|
||||||
|
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
|
||||||
|
import static java.nio.file.StandardOpenOption.WRITE;
|
||||||
|
|
||||||
|
public class CallGraphExportJson {
|
||||||
|
private final ICallGraph callGraph;
|
||||||
|
private final Gson gson;
|
||||||
|
|
||||||
|
public CallGraphExportJson(ICallGraph callGraph) {
|
||||||
|
this.callGraph = callGraph;
|
||||||
|
this.gson = new GsonBuilder()
|
||||||
|
.disableJdkUnsafe()
|
||||||
|
.disableInnerClassSerialization()
|
||||||
|
.setStrictness(Strictness.STRICT)
|
||||||
|
// .setPrettyPrinting() // TODO: add option for pretty print?
|
||||||
|
.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeTo(Path path) {
|
||||||
|
try {
|
||||||
|
FileUtils.makeDirsForFile(path);
|
||||||
|
Files.writeString(path, writeToString(), StandardCharsets.UTF_8,
|
||||||
|
WRITE, TRUNCATE_EXISTING, CREATE);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to save JSON file: " + path, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String writeToString() {
|
||||||
|
List<Edge> edges = new ArrayList<>();
|
||||||
|
Map<Integer, Node> nodeMap = new HashMap<>();
|
||||||
|
for (ICallGraphEdge edge : callGraph.edges()) {
|
||||||
|
ICallGraphNode from = edge.from();
|
||||||
|
ICallGraphNode to = edge.to();
|
||||||
|
addNode(from, nodeMap);
|
||||||
|
addNode(to, nodeMap);
|
||||||
|
Edge jsonEdge = new Edge();
|
||||||
|
jsonEdge.from = from.getId();
|
||||||
|
jsonEdge.to = to.getId();
|
||||||
|
jsonEdge.resolved = edge.isResolved();
|
||||||
|
edges.add(jsonEdge);
|
||||||
|
}
|
||||||
|
List<Node> nodes = new ArrayList<>(nodeMap.values());
|
||||||
|
nodes.sort(Comparator.comparingInt(o -> o.id));
|
||||||
|
|
||||||
|
RootNode rootNode = new RootNode();
|
||||||
|
rootNode.nodes = nodes;
|
||||||
|
rootNode.edges = edges;
|
||||||
|
return gson.toJson(rootNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addNode(ICallGraphNode cgNode, Map<Integer, Node> nodeMap) {
|
||||||
|
nodeMap.computeIfAbsent(cgNode.getId(), id -> {
|
||||||
|
Node node = new Node();
|
||||||
|
node.id = id;
|
||||||
|
node.method = cgNode.getMethodInfo().getRawFullId();
|
||||||
|
return node;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class RootNode {
|
||||||
|
List<Node> nodes;
|
||||||
|
List<Edge> edges;
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class Node {
|
||||||
|
int id;
|
||||||
|
String method;
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class Edge {
|
||||||
|
int from;
|
||||||
|
int to;
|
||||||
|
boolean resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package jadx.analysis.callgraph;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import jadx.analysis.callgraph.api.ICallGraphNode;
|
||||||
|
import jadx.core.dex.attributes.IAttributeNode;
|
||||||
|
import jadx.core.dex.info.MethodInfo;
|
||||||
|
import jadx.core.dex.nodes.MethodNode;
|
||||||
|
|
||||||
|
class CallGraphNode implements ICallGraphNode {
|
||||||
|
private final int id;
|
||||||
|
private final MethodInfo mthInfo;
|
||||||
|
private final @Nullable MethodNode mthNode;
|
||||||
|
private final CallGraphAttrNode attrNode;
|
||||||
|
|
||||||
|
public CallGraphNode(int id, MethodInfo mthInfo) {
|
||||||
|
this(id, mthInfo, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CallGraphNode(int id, MethodNode mthNode) {
|
||||||
|
this(id, mthNode.getMethodInfo(), mthNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CallGraphNode(int id, MethodInfo mthInfo, @Nullable MethodNode mthNode) {
|
||||||
|
this.id = id;
|
||||||
|
this.mthInfo = mthInfo;
|
||||||
|
this.mthNode = mthNode;
|
||||||
|
this.attrNode = new CallGraphAttrNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MethodInfo getMethodInfo() {
|
||||||
|
return mthInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable MethodNode getMethodNode() {
|
||||||
|
return mthNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isResolved() {
|
||||||
|
return mthNode != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IAttributeNode attributes() {
|
||||||
|
return attrNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return mthInfo.getFullId();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package jadx.analysis.callgraph;
|
||||||
|
|
||||||
|
import jadx.analysis.callgraph.api.ICallGraphBuilder;
|
||||||
|
import jadx.api.JadxDecompiler;
|
||||||
|
|
||||||
|
public class JadxCallGraph {
|
||||||
|
|
||||||
|
public static ICallGraphBuilder builder(JadxDecompiler decompiler) {
|
||||||
|
return new CallGraphBuilder(decompiler);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package jadx.analysis.callgraph.api;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface ICallGraph {
|
||||||
|
|
||||||
|
List<ICallGraphEdge> edges();
|
||||||
|
|
||||||
|
void writeDot(Path path);
|
||||||
|
|
||||||
|
void writeJson(Path path);
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
package jadx.analysis.callgraph.api;
|
||||||
|
|
||||||
|
public interface ICallGraphBuilder {
|
||||||
|
|
||||||
|
ICallGraphBuilder includePackages(String pkgFilter);
|
||||||
|
|
||||||
|
ICallGraphBuilder resolvedOnly(boolean resolved);
|
||||||
|
|
||||||
|
ICallGraph build();
|
||||||
|
}
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
package jadx.analysis.callgraph.api;
|
||||||
|
|
||||||
|
import jadx.core.dex.attributes.IAttributeNode;
|
||||||
|
|
||||||
|
public interface ICallGraphEdge {
|
||||||
|
|
||||||
|
ICallGraphNode from();
|
||||||
|
|
||||||
|
ICallGraphNode to();
|
||||||
|
|
||||||
|
boolean isResolved();
|
||||||
|
|
||||||
|
IAttributeNode attributes();
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
package jadx.analysis.callgraph.api;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import jadx.core.dex.attributes.IAttributeNode;
|
||||||
|
import jadx.core.dex.info.MethodInfo;
|
||||||
|
import jadx.core.dex.nodes.MethodNode;
|
||||||
|
|
||||||
|
public interface ICallGraphNode {
|
||||||
|
|
||||||
|
int getId();
|
||||||
|
|
||||||
|
MethodInfo getMethodInfo();
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
MethodNode getMethodNode();
|
||||||
|
|
||||||
|
boolean isResolved();
|
||||||
|
|
||||||
|
IAttributeNode attributes();
|
||||||
|
}
|
||||||
+86
@@ -0,0 +1,86 @@
|
|||||||
|
package jadx.analysis.callgraph.test;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import jadx.analysis.callgraph.CallGraphExportDot;
|
||||||
|
import jadx.analysis.callgraph.CallGraphExportJson;
|
||||||
|
import jadx.analysis.callgraph.JadxCallGraph;
|
||||||
|
import jadx.analysis.callgraph.api.ICallGraph;
|
||||||
|
import jadx.analysis.callgraph.api.ICallGraphEdge;
|
||||||
|
import jadx.api.JadxArgs;
|
||||||
|
import jadx.api.JadxDecompiler;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class JadxCallGraphTest {
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
void usageExample() {
|
||||||
|
JadxArgs args = new JadxArgs();
|
||||||
|
args.addInputFile(new File("input.apk"));
|
||||||
|
try (JadxDecompiler jadx = new JadxDecompiler(args)) {
|
||||||
|
jadx.load();
|
||||||
|
|
||||||
|
ICallGraph callGraph = JadxCallGraph.builder(jadx)
|
||||||
|
.includePackages("com.example")
|
||||||
|
.resolvedOnly(false)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
for (ICallGraphEdge edge : callGraph.edges()) {
|
||||||
|
if (edge.isResolved()) {
|
||||||
|
System.out.printf("Edge from '%s' to '%s'%n", edge.from(), edge.to());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callGraph.writeDot(Path.of("test.dot"));
|
||||||
|
callGraph.writeJson(Path.of("test.json"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void simpleTest() {
|
||||||
|
JadxArgs args = new JadxArgs();
|
||||||
|
args.addInputFile(getSampleFile("simple.smali"));
|
||||||
|
try (JadxDecompiler jadx = new JadxDecompiler(args)) {
|
||||||
|
jadx.load();
|
||||||
|
|
||||||
|
ICallGraph callGraph = JadxCallGraph.builder(jadx)
|
||||||
|
.includePackages("test.pkg")
|
||||||
|
.resolvedOnly(false)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assertThat(callGraph.edges()).hasSize(1);
|
||||||
|
|
||||||
|
for (ICallGraphEdge edge : callGraph.edges()) {
|
||||||
|
System.out.println("Edge from " + edge.from() + " to " + edge.to());
|
||||||
|
}
|
||||||
|
|
||||||
|
String dotStr = new CallGraphExportDot(jadx.getArgs(), callGraph).writeToString();
|
||||||
|
System.out.println("dot: " + dotStr);
|
||||||
|
|
||||||
|
String jsonStr = new CallGraphExportJson(callGraph).writeToString();
|
||||||
|
System.out.println("json: " + jsonStr);
|
||||||
|
|
||||||
|
callGraph.writeDot(tempDir.resolve("test.dot"));
|
||||||
|
callGraph.writeJson(tempDir.resolve("test.json"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private File getSampleFile(String sampleName) {
|
||||||
|
try {
|
||||||
|
URL resource = getClass().getResource("/samples/" + sampleName);
|
||||||
|
assertThat(resource).describedAs("Sample not found: %s", sampleName).isNotNull();
|
||||||
|
return new File(resource.toURI().toURL().getFile());
|
||||||
|
} catch (MalformedURLException | URISyntaxException e) {
|
||||||
|
throw new RuntimeException("Failed to load sample file: " + sampleName, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
.class Ltest/pkg/HelloWorld;
|
||||||
|
.super Ljava/lang/Object;
|
||||||
|
.source "HelloWorld.java"
|
||||||
|
|
||||||
|
.method public static main([Ljava/lang/String;)V
|
||||||
|
.registers 2
|
||||||
|
|
||||||
|
const-string v0, "Hello, World"
|
||||||
|
invoke-static {p0, v0}, Ltest/pkg/HelloWorld;->hello(Ljava/lang/String;)V
|
||||||
|
return-void
|
||||||
|
.end method
|
||||||
|
|
||||||
|
.method public static hello(Ljava/lang/String;)V
|
||||||
|
.registers 2
|
||||||
|
|
||||||
|
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
|
||||||
|
invoke-virtual {v0, p0}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
|
||||||
|
return-void
|
||||||
|
.end method
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
package jadx.commons.app;
|
package jadx.commons.app;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
public class JadxCommonEnv {
|
public class JadxCommonEnv {
|
||||||
|
|
||||||
public static String get(String varName, String defValue) {
|
public static @Nullable String get(String varName, @Nullable String defValue) {
|
||||||
String strValue = System.getenv(varName);
|
String strValue = System.getenv(varName);
|
||||||
return isNullOrEmpty(strValue) ? defValue : strValue;
|
return isNullOrEmpty(strValue) ? defValue : strValue;
|
||||||
}
|
}
|
||||||
@@ -23,7 +25,7 @@ public class JadxCommonEnv {
|
|||||||
return Integer.parseInt(strValue);
|
return Integer.parseInt(strValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isNullOrEmpty(String value) {
|
private static boolean isNullOrEmpty(@Nullable String value) {
|
||||||
return value == null || value.isEmpty();
|
return value == null || value.isEmpty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ package jadx.commons.app;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.function.Function;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -30,40 +31,39 @@ public class JadxCommonFiles {
|
|||||||
|
|
||||||
static {
|
static {
|
||||||
DirsLoader loader = new DirsLoader();
|
DirsLoader loader = new DirsLoader();
|
||||||
loader.init();
|
|
||||||
CONFIG_DIR = loader.getConfigDir();
|
CONFIG_DIR = loader.getConfigDir();
|
||||||
CACHE_DIR = loader.getCacheDir();
|
CACHE_DIR = loader.getCacheDir();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class DirsLoader {
|
private static final class DirsLoader {
|
||||||
private @Nullable ProjectDirectories dirs;
|
private final Path configDir;
|
||||||
private Path configDir;
|
private final Path cacheDir;
|
||||||
private Path cacheDir;
|
|
||||||
|
|
||||||
public void init() {
|
DirsLoader() {
|
||||||
try {
|
try {
|
||||||
configDir = loadEnvDir("JADX_CONFIG_DIR", pd -> pd.configDir);
|
AtomicReference<@Nullable ProjectDirectories> pdRef = new AtomicReference<>();
|
||||||
cacheDir = loadEnvDir("JADX_CACHE_DIR", pd -> pd.cacheDir);
|
configDir = loadEnvDir("JADX_CONFIG_DIR", () -> loadDirs(pdRef).configDir);
|
||||||
|
cacheDir = loadEnvDir("JADX_CACHE_DIR", () -> loadDirs(pdRef).cacheDir);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("Failed to init common directories", e);
|
throw new RuntimeException("Failed to init common directories", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Path loadEnvDir(String envVar, Function<ProjectDirectories, String> dirFunc) throws IOException {
|
private static Path loadEnvDir(String envVar, Supplier<String> dirFunc) throws IOException {
|
||||||
String envDir = JadxCommonEnv.get(envVar, null);
|
String envDir = JadxCommonEnv.get(envVar, null);
|
||||||
String dirStr;
|
String dirStr;
|
||||||
if (envDir != null) {
|
if (envDir != null) {
|
||||||
dirStr = envDir;
|
dirStr = envDir;
|
||||||
} else {
|
} else {
|
||||||
dirStr = dirFunc.apply(loadDirs());
|
dirStr = dirFunc.get();
|
||||||
}
|
}
|
||||||
Path path = Path.of(dirStr).toAbsolutePath();
|
Path path = Path.of(dirStr).toAbsolutePath();
|
||||||
Files.createDirectories(path);
|
Files.createDirectories(path);
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized ProjectDirectories loadDirs() {
|
private static ProjectDirectories loadDirs(AtomicReference<@Nullable ProjectDirectories> pdRef) {
|
||||||
ProjectDirectories currentDirs = dirs;
|
ProjectDirectories currentDirs = pdRef.get();
|
||||||
if (currentDirs != null) {
|
if (currentDirs != null) {
|
||||||
return currentDirs;
|
return currentDirs;
|
||||||
}
|
}
|
||||||
@@ -76,7 +76,7 @@ public class JadxCommonFiles {
|
|||||||
LOG.debug("Loaded system dirs ({}ms): config: {}, cache: {}",
|
LOG.debug("Loaded system dirs ({}ms): config: {}, cache: {}",
|
||||||
System.currentTimeMillis() - start, loadedDirs.configDir, loadedDirs.cacheDir);
|
System.currentTimeMillis() - start, loadedDirs.configDir, loadedDirs.cacheDir);
|
||||||
}
|
}
|
||||||
dirs = loadedDirs;
|
pdRef.set(loadedDirs);
|
||||||
return loadedDirs;
|
return loadedDirs;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,21 +84,22 @@ public class JadxCommonFiles {
|
|||||||
* Return JNI, Foreign or PowerShell implementation
|
* Return JNI, Foreign or PowerShell implementation
|
||||||
*/
|
*/
|
||||||
private static Windows getWinDirs() {
|
private static Windows getWinDirs() {
|
||||||
Windows defSup = Windows.getDefaultSupplier().get();
|
Windows impl = Windows.getDefaultSupplier().get();
|
||||||
if (defSup instanceof WindowsPowerShell) {
|
if (impl instanceof WindowsPowerShell) {
|
||||||
if (JadxSystemInfo.IS_AMD64) {
|
if (JadxSystemInfo.IS_AMD64) {
|
||||||
// JNI library compiled for x86-64
|
// JNI library compiled only for x86-64
|
||||||
return new WindowsJni();
|
impl = new WindowsJni();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return defSup;
|
LOG.debug("Using win dirs implementation: {}", impl.getClass().getSimpleName());
|
||||||
|
return impl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Path getCacheDir() {
|
Path getCacheDir() {
|
||||||
return cacheDir;
|
return cacheDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Path getConfigDir() {
|
Path getConfigDir() {
|
||||||
return configDir;
|
return configDir;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package jadx.commons.app;
|
|||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
public class JadxSystemInfo {
|
public class JadxSystemInfo {
|
||||||
public static final String JAVA_VM = System.getProperty("java.vm.name", "?");
|
public static final String JAVA_VM = System.getProperty("java.vm.name", "?");
|
||||||
public static final String JAVA_VER = System.getProperty("java.version", "?");
|
public static final String JAVA_VER = System.getProperty("java.version", "?");
|
||||||
@@ -16,7 +17,7 @@ public class JadxSystemInfo {
|
|||||||
public static final boolean IS_LINUX = !IS_WINDOWS && !IS_MAC;
|
public static final boolean IS_LINUX = !IS_WINDOWS && !IS_MAC;
|
||||||
public static final boolean IS_UNIX = !IS_WINDOWS;
|
public static final boolean IS_UNIX = !IS_WINDOWS;
|
||||||
|
|
||||||
private static final String OS_ARCH_LOWER = OS_NAME.toLowerCase(Locale.ENGLISH);
|
private static final String OS_ARCH_LOWER = OS_ARCH.toLowerCase(Locale.ENGLISH);
|
||||||
public static final boolean IS_AMD64 = OS_ARCH_LOWER.equals("amd64");
|
public static final boolean IS_AMD64 = OS_ARCH_LOWER.equals("amd64");
|
||||||
public static final boolean IS_ARM64 = OS_ARCH_LOWER.equals("aarch64");
|
public static final boolean IS_ARM64 = OS_ARCH_LOWER.equals("aarch64");
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package jadx.zip;
|
package jadx.zip;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@@ -9,6 +10,7 @@ import java.util.function.Function;
|
|||||||
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import jadx.zip.fallback.FallbackException;
|
||||||
import jadx.zip.fallback.FallbackZipParser;
|
import jadx.zip.fallback.FallbackZipParser;
|
||||||
import jadx.zip.parser.JadxZipParser;
|
import jadx.zip.parser.JadxZipParser;
|
||||||
import jadx.zip.security.IJadxZipSecurity;
|
import jadx.zip.security.IJadxZipSecurity;
|
||||||
@@ -39,13 +41,15 @@ public class ZipReader {
|
|||||||
|
|
||||||
@SuppressWarnings("resource")
|
@SuppressWarnings("resource")
|
||||||
public ZipContent open(File zipFile) throws IOException {
|
public ZipContent open(File zipFile) throws IOException {
|
||||||
|
if (!zipFile.exists()) {
|
||||||
|
throw new FileNotFoundException(zipFile.getAbsolutePath());
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
JadxZipParser jadxParser = new JadxZipParser(zipFile, options);
|
JadxZipParser jadxParser = new JadxZipParser(zipFile, options);
|
||||||
IZipParser detectedParser = detectParser(zipFile, jadxParser);
|
IZipParser detectedParser = detectParser(zipFile, jadxParser);
|
||||||
if (detectedParser != jadxParser) {
|
|
||||||
jadxParser.close();
|
|
||||||
}
|
|
||||||
return detectedParser.open();
|
return detectedParser.open();
|
||||||
|
} catch (FallbackException e) {
|
||||||
|
throw e;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
if (options.getFlags().contains(ZipReaderFlags.DONT_USE_FALLBACK)) {
|
if (options.getFlags().contains(ZipReaderFlags.DONT_USE_FALLBACK)) {
|
||||||
throw new IOException("Failed to open zip: " + zipFile, e);
|
throw new IOException("Failed to open zip: " + zipFile, e);
|
||||||
@@ -90,7 +94,7 @@ public class ZipReader {
|
|||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IZipParser detectParser(File zipFile, JadxZipParser jadxParser) {
|
private IZipParser detectParser(File zipFile, JadxZipParser jadxParser) throws IOException {
|
||||||
if (zipFile.getName().endsWith(".apk")
|
if (zipFile.getName().endsWith(".apk")
|
||||||
|| options.getFlags().contains(ZipReaderFlags.DONT_USE_FALLBACK)) {
|
|| options.getFlags().contains(ZipReaderFlags.DONT_USE_FALLBACK)) {
|
||||||
return jadxParser;
|
return jadxParser;
|
||||||
@@ -105,7 +109,7 @@ public class ZipReader {
|
|||||||
return jadxParser;
|
return jadxParser;
|
||||||
}
|
}
|
||||||
|
|
||||||
private FallbackZipParser buildFallbackParser(File zipFile) {
|
private FallbackZipParser buildFallbackParser(File zipFile) throws IOException {
|
||||||
return new FallbackZipParser(zipFile, options);
|
return new FallbackZipParser(zipFile, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package jadx.zip.fallback;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class FallbackException extends IOException {
|
||||||
|
public FallbackException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,39 +22,45 @@ import jadx.zip.security.IJadxZipSecurity;
|
|||||||
|
|
||||||
public class FallbackZipParser implements IZipParser {
|
public class FallbackZipParser implements IZipParser {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(FallbackZipParser.class);
|
private static final Logger LOG = LoggerFactory.getLogger(FallbackZipParser.class);
|
||||||
|
|
||||||
private final File file;
|
private final File file;
|
||||||
|
private final ZipFile zipFile;
|
||||||
private final IJadxZipSecurity zipSecurity;
|
private final IJadxZipSecurity zipSecurity;
|
||||||
private final boolean useLimitedDataStream;
|
private final boolean useLimitedDataStream;
|
||||||
|
|
||||||
private ZipFile zipFile;
|
public FallbackZipParser(File file, ZipReaderOptions options) throws FallbackException {
|
||||||
|
try {
|
||||||
public FallbackZipParser(File file, ZipReaderOptions options) {
|
this.file = file;
|
||||||
this.file = file;
|
this.zipFile = new ZipFile(file);
|
||||||
this.zipSecurity = options.getZipSecurity();
|
this.zipSecurity = options.getZipSecurity();
|
||||||
this.useLimitedDataStream = zipSecurity.useLimitedDataStream();
|
this.useLimitedDataStream = zipSecurity.useLimitedDataStream();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new FallbackException("Error opening zip file: " + file.getAbsolutePath(), e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ZipContent open() throws IOException {
|
public ZipContent open() throws IOException {
|
||||||
zipFile = new ZipFile(file);
|
try {
|
||||||
|
int maxEntriesCount = zipSecurity.getMaxEntriesCount();
|
||||||
int maxEntriesCount = zipSecurity.getMaxEntriesCount();
|
if (maxEntriesCount == -1) {
|
||||||
if (maxEntriesCount == -1) {
|
maxEntriesCount = Integer.MAX_VALUE;
|
||||||
maxEntriesCount = Integer.MAX_VALUE;
|
}
|
||||||
}
|
List<IZipEntry> list = new ArrayList<>();
|
||||||
|
Enumeration<? extends ZipEntry> entries = zipFile.entries();
|
||||||
List<IZipEntry> list = new ArrayList<>();
|
while (entries.hasMoreElements()) {
|
||||||
Enumeration<? extends ZipEntry> entries = zipFile.entries();
|
FallbackZipEntry zipEntry = new FallbackZipEntry(this, entries.nextElement());
|
||||||
while (entries.hasMoreElements()) {
|
if (isValidEntry(zipEntry)) {
|
||||||
FallbackZipEntry zipEntry = new FallbackZipEntry(this, entries.nextElement());
|
list.add(zipEntry);
|
||||||
if (isValidEntry(zipEntry)) {
|
if (list.size() > maxEntriesCount) {
|
||||||
list.add(zipEntry);
|
throw new IllegalStateException("Max entries count limit exceeded: " + list.size());
|
||||||
if (list.size() > maxEntriesCount) {
|
}
|
||||||
throw new IllegalStateException("Max entries count limit exceeded: " + list.size());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return new ZipContent(this, list);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new FallbackException("Error opening zip file: " + file.getAbsolutePath(), e);
|
||||||
}
|
}
|
||||||
return new ZipContent(this, list);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isValidEntry(IZipEntry zipEntry) {
|
private boolean isValidEntry(IZipEntry zipEntry) {
|
||||||
@@ -98,12 +104,8 @@ public class FallbackZipParser implements IZipParser {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() throws IOException {
|
public void close() throws IOException {
|
||||||
try {
|
if (zipFile != null) {
|
||||||
if (zipFile != null) {
|
zipFile.close();
|
||||||
zipFile.close();
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
zipFile = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ public class ByteBufferBackedInputStream extends InputStream {
|
|||||||
this.buf = buf;
|
this.buf = buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public int read() throws IOException {
|
public int read() throws IOException {
|
||||||
if (!buf.hasRemaining()) {
|
if (!buf.hasRemaining()) {
|
||||||
return -1;
|
return -1;
|
||||||
@@ -19,6 +20,7 @@ public class ByteBufferBackedInputStream extends InputStream {
|
|||||||
return buf.get() & 0xFF;
|
return buf.get() & 0xFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
@SuppressWarnings("NullableProblems")
|
@SuppressWarnings("NullableProblems")
|
||||||
public int read(byte[] bytes, int off, int len) throws IOException {
|
public int read(byte[] bytes, int off, int len) throws IOException {
|
||||||
if (!buf.hasRemaining()) {
|
if (!buf.hasRemaining()) {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ public final class JadxZipEntry implements IZipEntry {
|
|||||||
return compressedSize <= uncompressedSize;
|
return compressedSize <= uncompressedSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return fileName;
|
return fileName;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,9 +49,9 @@ public final class JadxZipParser implements IZipParser {
|
|||||||
private final boolean verify;
|
private final boolean verify;
|
||||||
private final boolean useLimitedDataStream;
|
private final boolean useLimitedDataStream;
|
||||||
|
|
||||||
private RandomAccessFile file;
|
private @Nullable RandomAccessFile file;
|
||||||
private FileChannel fileChannel;
|
private @Nullable FileChannel fileChannel;
|
||||||
private ByteBuffer byteBuffer;
|
private @Nullable ByteBuffer byteBuffer;
|
||||||
|
|
||||||
private int endOfCDStart = -2;
|
private int endOfCDStart = -2;
|
||||||
|
|
||||||
@@ -90,23 +90,25 @@ public final class JadxZipParser implements IZipParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("RedundantIfStatement")
|
|
||||||
public boolean canOpen() {
|
public boolean canOpen() {
|
||||||
try {
|
try {
|
||||||
load();
|
load();
|
||||||
int eocdStart = searchEndOfCDStart();
|
int eocdStart = searchEndOfCDStart();
|
||||||
ByteBuffer buf = byteBuffer;
|
ByteBuffer buf = getBuffer();
|
||||||
buf.position(eocdStart + 4);
|
buf.position(eocdStart + 4);
|
||||||
int diskNum = readU2(buf);
|
int diskNum = readU2(buf);
|
||||||
if (diskNum == 0xFFFF) {
|
if (diskNum != 0xFFFF) { // Zip64 not supported
|
||||||
// Zip64
|
return true;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOG.warn("Jadx parser can't open zip file: {}", zipFile, e);
|
LOG.warn("Jadx parser can't open zip file: {}", zipFile, e);
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.warn("Failed to close jadx parser, zip file: {}", zipFile, e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isValidEntry(JadxZipEntry zipEntry) {
|
private boolean isValidEntry(JadxZipEntry zipEntry) {
|
||||||
@@ -117,13 +119,21 @@ public final class JadxZipParser implements IZipParser {
|
|||||||
return validEntry;
|
return validEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ByteBuffer getBuffer() {
|
||||||
|
ByteBuffer buf = byteBuffer;
|
||||||
|
if (buf == null) {
|
||||||
|
throw new RuntimeException("File not opened: " + zipFile);
|
||||||
|
}
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
private void load() throws IOException {
|
private void load() throws IOException {
|
||||||
if (byteBuffer != null) {
|
if (byteBuffer != null) {
|
||||||
// already loaded
|
// already loaded
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
file = new RandomAccessFile(zipFile, "r");
|
RandomAccessFile raFile = new RandomAccessFile(zipFile, "r");
|
||||||
long size = file.length();
|
long size = raFile.length();
|
||||||
if (size >= Integer.MAX_VALUE) {
|
if (size >= Integer.MAX_VALUE) {
|
||||||
throw new IOException("Zip file is too big");
|
throw new IOException("Zip file is too big");
|
||||||
}
|
}
|
||||||
@@ -131,16 +141,16 @@ public final class JadxZipParser implements IZipParser {
|
|||||||
if (fileLen < 100 * 1024 * 1024) {
|
if (fileLen < 100 * 1024 * 1024) {
|
||||||
// load files smaller than 100MB directly into memory
|
// load files smaller than 100MB directly into memory
|
||||||
byte[] bytes = new byte[fileLen];
|
byte[] bytes = new byte[fileLen];
|
||||||
file.readFully(bytes);
|
raFile.readFully(bytes);
|
||||||
byteBuffer = ByteBuffer.wrap(bytes).asReadOnlyBuffer();
|
byteBuffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
file.close();
|
raFile.close();
|
||||||
file = null;
|
|
||||||
} else {
|
} else {
|
||||||
// for big files - use a memory mapped file
|
// for big files - use a memory mapped file
|
||||||
fileChannel = file.getChannel();
|
file = raFile;
|
||||||
|
fileChannel = raFile.getChannel();
|
||||||
byteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
|
byteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
|
||||||
|
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
}
|
}
|
||||||
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<IZipEntry> searchLocalFileHeaders(int maxEntriesCount) {
|
private List<IZipEntry> searchLocalFileHeaders(int maxEntriesCount) {
|
||||||
@@ -165,7 +175,7 @@ public final class JadxZipParser implements IZipParser {
|
|||||||
if (eocdStart < 0) {
|
if (eocdStart < 0) {
|
||||||
throw new RuntimeException("End of central directory not found");
|
throw new RuntimeException("End of central directory not found");
|
||||||
}
|
}
|
||||||
ByteBuffer buf = byteBuffer;
|
ByteBuffer buf = getBuffer();
|
||||||
buf.position(eocdStart + 10);
|
buf.position(eocdStart + 10);
|
||||||
int entriesCount = readU2(buf);
|
int entriesCount = readU2(buf);
|
||||||
buf.position(eocdStart + 16);
|
buf.position(eocdStart + 16);
|
||||||
@@ -186,7 +196,7 @@ public final class JadxZipParser implements IZipParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private JadxZipEntry loadCDEntry() {
|
private JadxZipEntry loadCDEntry() {
|
||||||
ByteBuffer buf = byteBuffer;
|
ByteBuffer buf = getBuffer();
|
||||||
int start = buf.position();
|
int start = buf.position();
|
||||||
buf.position(start + 28);
|
buf.position(start + 28);
|
||||||
int fileNameLen = readU2(buf);
|
int fileNameLen = readU2(buf);
|
||||||
@@ -207,7 +217,7 @@ public final class JadxZipParser implements IZipParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private JadxZipEntry fixEntryFromCD(JadxZipEntry entry, int start) {
|
private JadxZipEntry fixEntryFromCD(JadxZipEntry entry, int start) {
|
||||||
ByteBuffer buf = byteBuffer;
|
ByteBuffer buf = getBuffer();
|
||||||
buf.position(start + 10);
|
buf.position(start + 10);
|
||||||
int comprMethod = readU2(buf);
|
int comprMethod = readU2(buf);
|
||||||
buf.position(start + 20);
|
buf.position(start + 20);
|
||||||
@@ -237,7 +247,7 @@ public final class JadxZipParser implements IZipParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private JadxZipEntry loadFileEntry(int start) {
|
private JadxZipEntry loadFileEntry(int start) {
|
||||||
ByteBuffer buf = byteBuffer;
|
ByteBuffer buf = getBuffer();
|
||||||
buf.position(start + 8);
|
buf.position(start + 8);
|
||||||
int comprMethod = readU2(buf);
|
int comprMethod = readU2(buf);
|
||||||
buf.position(start + 18);
|
buf.position(start + 18);
|
||||||
@@ -255,7 +265,7 @@ public final class JadxZipParser implements IZipParser {
|
|||||||
if (endOfCDStart != -2) {
|
if (endOfCDStart != -2) {
|
||||||
return endOfCDStart;
|
return endOfCDStart;
|
||||||
}
|
}
|
||||||
ByteBuffer buf = byteBuffer;
|
ByteBuffer buf = getBuffer();
|
||||||
int pos = buf.limit() - 22;
|
int pos = buf.limit() - 22;
|
||||||
int minPos = Math.max(0, pos - 0xffff);
|
int minPos = Math.max(0, pos - 0xffff);
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -273,7 +283,7 @@ public final class JadxZipParser implements IZipParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private int searchEntryStart() {
|
private int searchEntryStart() {
|
||||||
ByteBuffer buf = byteBuffer;
|
ByteBuffer buf = getBuffer();
|
||||||
while (true) {
|
while (true) {
|
||||||
int start = buf.position();
|
int start = buf.position();
|
||||||
if (start + 4 > buf.limit()) {
|
if (start + 4 > buf.limit()) {
|
||||||
@@ -297,14 +307,14 @@ public final class JadxZipParser implements IZipParser {
|
|||||||
InputStream stream;
|
InputStream stream;
|
||||||
if (entry.getCompressMethod() == 8) {
|
if (entry.getCompressMethod() == 8) {
|
||||||
try {
|
try {
|
||||||
stream = ZipDeflate.decompressEntryToStream(byteBuffer, entry);
|
stream = ZipDeflate.decompressEntryToStream(getBuffer(), entry);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
entryParseFailed(entry, e);
|
entryParseFailed(entry, e);
|
||||||
return useFallbackParser(entry).getInputStream();
|
return useFallbackParser(entry).getInputStream();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// treat any other compression methods values as UNCOMPRESSED
|
// treat any other compression methods values as UNCOMPRESSED
|
||||||
stream = bufferToStream(byteBuffer, entry.getDataStart(), (int) entry.getUncompressedSize());
|
stream = bufferToStream(getBuffer(), entry.getDataStart(), (int) entry.getUncompressedSize());
|
||||||
}
|
}
|
||||||
if (useLimitedDataStream) {
|
if (useLimitedDataStream) {
|
||||||
return new LimitedInputStream(stream, entry.getUncompressedSize());
|
return new LimitedInputStream(stream, entry.getUncompressedSize());
|
||||||
@@ -318,14 +328,14 @@ public final class JadxZipParser implements IZipParser {
|
|||||||
}
|
}
|
||||||
if (entry.getCompressMethod() == 8) {
|
if (entry.getCompressMethod() == 8) {
|
||||||
try {
|
try {
|
||||||
return ZipDeflate.decompressEntryToBytes(byteBuffer, entry);
|
return ZipDeflate.decompressEntryToBytes(getBuffer(), entry);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
entryParseFailed(entry, e);
|
entryParseFailed(entry, e);
|
||||||
return useFallbackParser(entry).getBytes();
|
return useFallbackParser(entry).getBytes();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// treat any other compression methods values as UNCOMPRESSED
|
// treat any other compression methods values as UNCOMPRESSED
|
||||||
return bufferToBytes(byteBuffer, entry.getDataStart(), (int) entry.getUncompressedSize());
|
return bufferToBytes(getBuffer(), entry.getDataStart(), (int) entry.getUncompressedSize());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void verifyEntry(JadxZipEntry entry) {
|
private static void verifyEntry(JadxZipEntry entry) {
|
||||||
@@ -361,7 +371,7 @@ public final class JadxZipParser implements IZipParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("resource")
|
@SuppressWarnings("resource")
|
||||||
private ZipContent initFallbackParser() {
|
private synchronized ZipContent initFallbackParser() {
|
||||||
if (fallbackZipContent == null) {
|
if (fallbackZipContent == null) {
|
||||||
try {
|
try {
|
||||||
fallbackZipContent = new FallbackZipParser(zipFile, options).open();
|
fallbackZipContent = new FallbackZipParser(zipFile, options).open();
|
||||||
@@ -378,7 +388,7 @@ public final class JadxZipParser implements IZipParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private int readFlags(JadxZipEntry entry) {
|
private int readFlags(JadxZipEntry entry) {
|
||||||
ByteBuffer buf = byteBuffer;
|
ByteBuffer buf = getBuffer();
|
||||||
buf.position(entry.getEntryStart() + 6);
|
buf.position(entry.getEntryStart() + 6);
|
||||||
return readU2(buf);
|
return readU2(buf);
|
||||||
}
|
}
|
||||||
@@ -407,6 +417,7 @@ public final class JadxZipParser implements IZipParser {
|
|||||||
return new String(bytes, StandardCharsets.UTF_8);
|
return new String(bytes, StandardCharsets.UTF_8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("DataFlowIssue")
|
||||||
@Override
|
@Override
|
||||||
public void close() throws IOException {
|
public void close() throws IOException {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -6,15 +6,17 @@ dependencies {
|
|||||||
api(project(":jadx-plugins:jadx-input-api"))
|
api(project(":jadx-plugins:jadx-input-api"))
|
||||||
api(project(":jadx-commons:jadx-zip"))
|
api(project(":jadx-commons:jadx-zip"))
|
||||||
|
|
||||||
implementation("com.google.code.gson:gson:2.13.2")
|
implementation("com.google.code.gson:gson:2.14.0")
|
||||||
|
|
||||||
testImplementation("org.apache.commons:commons-lang3:3.20.0")
|
testImplementation("org.apache.commons:commons-lang3:3.20.0")
|
||||||
|
|
||||||
testImplementation(project(":jadx-plugins:jadx-dex-input"))
|
testImplementation(project(":jadx-plugins:jadx-dex-input"))
|
||||||
testRuntimeOnly(project(":jadx-plugins:jadx-smali-input"))
|
// 'ClassNotFound' error is raised if set as 'testRuntime'
|
||||||
testRuntimeOnly(project(":jadx-plugins:jadx-java-convert"))
|
// for the plugins below when running the tests from vscode.
|
||||||
testRuntimeOnly(project(":jadx-plugins:jadx-java-input"))
|
testImplementation(project(":jadx-plugins:jadx-smali-input"))
|
||||||
testRuntimeOnly(project(":jadx-plugins:jadx-raung-input"))
|
testImplementation(project(":jadx-plugins:jadx-java-convert"))
|
||||||
|
testImplementation(project(":jadx-plugins:jadx-java-input"))
|
||||||
|
testImplementation(project(":jadx-plugins:jadx-raung-input"))
|
||||||
|
|
||||||
testImplementation("org.eclipse.jdt:ecj") {
|
testImplementation("org.eclipse.jdt:ecj") {
|
||||||
version {
|
version {
|
||||||
@@ -22,7 +24,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:4.2")
|
testImplementation("tools.profiler:async-profiler:4.4")
|
||||||
}
|
}
|
||||||
|
|
||||||
val jadxTestJavaVersion = getTestJavaVersion()
|
val jadxTestJavaVersion = getTestJavaVersion()
|
||||||
@@ -30,7 +32,10 @@ val jadxTestJavaVersion = getTestJavaVersion()
|
|||||||
fun getTestJavaVersion(): Int? {
|
fun getTestJavaVersion(): Int? {
|
||||||
val envVarName = "JADX_TEST_JAVA_VERSION"
|
val envVarName = "JADX_TEST_JAVA_VERSION"
|
||||||
val testJavaVer = System.getenv(envVarName)?.toInt() ?: return null
|
val testJavaVer = System.getenv(envVarName)?.toInt() ?: return null
|
||||||
val currentJavaVer = java.toolchain.languageVersion.get().asInt()
|
val currentJavaVer =
|
||||||
|
java.toolchain.languageVersion
|
||||||
|
.get()
|
||||||
|
.asInt()
|
||||||
if (testJavaVer < currentJavaVer) {
|
if (testJavaVer < currentJavaVer) {
|
||||||
throw GradleException("'$envVarName' can't be set to lower version than $currentJavaVer")
|
throw GradleException("'$envVarName' can't be set to lower version than $currentJavaVer")
|
||||||
}
|
}
|
||||||
@@ -52,4 +57,6 @@ tasks.named<Test>("test") {
|
|||||||
|
|
||||||
// exclude temp tests
|
// exclude temp tests
|
||||||
exclude("**/tmp/*")
|
exclude("**/tmp/*")
|
||||||
|
|
||||||
|
// maxHeapSize = "4g"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ public class JadxArgs implements Closeable {
|
|||||||
/**
|
/**
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
private Predicate<String> classFilter = null;
|
private @Nullable Predicate<String> classFilter = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save dependencies for classes accepted by {@code classFilter}
|
* Save dependencies for classes accepted by {@code classFilter}
|
||||||
@@ -227,7 +227,6 @@ public class JadxArgs implements Closeable {
|
|||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
try {
|
try {
|
||||||
inputFiles = null;
|
|
||||||
if (codeCache != null) {
|
if (codeCache != null) {
|
||||||
codeCache.close();
|
codeCache.close();
|
||||||
}
|
}
|
||||||
@@ -239,9 +238,6 @@ public class JadxArgs implements Closeable {
|
|||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOG.error("Failed to close JadxArgs", e);
|
LOG.error("Failed to close JadxArgs", e);
|
||||||
} finally {
|
|
||||||
codeCache = null;
|
|
||||||
usageInfoCache = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,7 +494,7 @@ public class JadxArgs implements Closeable {
|
|||||||
*/
|
*/
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public void setUseSourceNameAsClassAlias(boolean useSourceNameAsClassAlias) {
|
public void setUseSourceNameAsClassAlias(boolean useSourceNameAsClassAlias) {
|
||||||
final var useSourceNameAsClassNameAlias = UseSourceNameAsClassNameAlias.create(useSourceNameAsClassAlias);
|
var useSourceNameAsClassNameAlias = UseSourceNameAsClassNameAlias.create(useSourceNameAsClassAlias);
|
||||||
setUseSourceNameAsClassNameAlias(useSourceNameAsClassNameAlias);
|
setUseSourceNameAsClassNameAlias(useSourceNameAsClassNameAlias);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,11 @@ import java.util.Comparator;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
import org.jetbrains.annotations.ApiStatus;
|
import org.jetbrains.annotations.ApiStatus;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import jadx.api.metadata.ICodeAnnotation;
|
import jadx.api.metadata.ICodeAnnotation;
|
||||||
import jadx.api.metadata.ICodeNodeRef;
|
import jadx.api.metadata.ICodeNodeRef;
|
||||||
@@ -26,11 +25,9 @@ import jadx.core.dex.nodes.MethodNode;
|
|||||||
import jadx.core.utils.ListUtils;
|
import jadx.core.utils.ListUtils;
|
||||||
|
|
||||||
public final class JavaClass implements JavaNode {
|
public final class JavaClass implements JavaNode {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(JavaClass.class);
|
private final @Nullable JadxDecompiler decompiler;
|
||||||
|
|
||||||
private final JadxDecompiler decompiler;
|
|
||||||
private final ClassNode cls;
|
private final ClassNode cls;
|
||||||
private final JavaClass parent;
|
private final @Nullable JavaClass parent;
|
||||||
|
|
||||||
private List<JavaClass> innerClasses = Collections.emptyList();
|
private List<JavaClass> innerClasses = Collections.emptyList();
|
||||||
private List<JavaClass> inlinedClasses = Collections.emptyList();
|
private List<JavaClass> inlinedClasses = Collections.emptyList();
|
||||||
@@ -38,7 +35,7 @@ public final class JavaClass implements JavaNode {
|
|||||||
private List<JavaMethod> methods = Collections.emptyList();
|
private List<JavaMethod> methods = Collections.emptyList();
|
||||||
private boolean listsLoaded;
|
private boolean listsLoaded;
|
||||||
|
|
||||||
JavaClass(ClassNode classNode, JadxDecompiler decompiler) {
|
JavaClass(ClassNode classNode, @NotNull JadxDecompiler decompiler) {
|
||||||
this.decompiler = decompiler;
|
this.decompiler = decompiler;
|
||||||
this.cls = classNode;
|
this.cls = classNode;
|
||||||
this.parent = null;
|
this.parent = null;
|
||||||
@@ -47,7 +44,7 @@ public final class JavaClass implements JavaNode {
|
|||||||
/**
|
/**
|
||||||
* Inner classes constructor
|
* Inner classes constructor
|
||||||
*/
|
*/
|
||||||
JavaClass(ClassNode classNode, JavaClass parent) {
|
JavaClass(ClassNode classNode, @NotNull JavaClass parent) {
|
||||||
this.decompiler = null;
|
this.decompiler = null;
|
||||||
this.cls = classNode;
|
this.cls = classNode;
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
@@ -69,6 +66,21 @@ public final class JavaClass implements JavaNode {
|
|||||||
load();
|
load();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect if calling load() would trigger a potentially expensive decompilation operation.
|
||||||
|
*/
|
||||||
|
public boolean loadingWouldRequireDecompilation() {
|
||||||
|
if (listsLoaded) {
|
||||||
|
// lists are already populated, so it's safe regardless of the state of the class itself
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (cls.getState().isProcessComplete()) {
|
||||||
|
// decompilation has already finished
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public synchronized ICodeInfo reload() {
|
public synchronized ICodeInfo reload() {
|
||||||
listsLoaded = false;
|
listsLoaded = false;
|
||||||
return cls.reloadCode();
|
return cls.reloadCode();
|
||||||
@@ -187,10 +199,10 @@ public final class JavaClass implements JavaNode {
|
|||||||
if (parent != null) {
|
if (parent != null) {
|
||||||
return parent.getRootDecompiler();
|
return parent.getRootDecompiler();
|
||||||
}
|
}
|
||||||
return decompiler;
|
return Objects.requireNonNull(decompiler);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ICodeAnnotation getAnnotationAt(int pos) {
|
public @Nullable ICodeAnnotation getAnnotationAt(int pos) {
|
||||||
return getCodeInfo().getCodeMetadata().getAt(pos);
|
return getCodeInfo().getCodeMetadata().getAt(pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,7 +271,7 @@ public final class JavaClass implements JavaNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JavaClass getDeclaringClass() {
|
public @Nullable JavaClass getDeclaringClass() {
|
||||||
return parent;
|
return parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,21 +5,18 @@ import java.util.List;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.jetbrains.annotations.ApiStatus;
|
import org.jetbrains.annotations.ApiStatus;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import jadx.api.metadata.ICodeAnnotation;
|
import jadx.api.metadata.ICodeAnnotation;
|
||||||
import jadx.api.metadata.ICodeNodeRef;
|
import jadx.api.metadata.ICodeNodeRef;
|
||||||
import jadx.core.dex.attributes.AType;
|
import jadx.core.dex.attributes.AType;
|
||||||
import jadx.core.dex.attributes.nodes.MethodOverrideAttr;
|
import jadx.core.dex.attributes.nodes.MethodOverrideAttr;
|
||||||
import jadx.core.dex.info.AccessInfo;
|
import jadx.core.dex.info.AccessInfo;
|
||||||
|
import jadx.core.dex.info.MethodInfo;
|
||||||
import jadx.core.dex.instructions.args.ArgType;
|
import jadx.core.dex.instructions.args.ArgType;
|
||||||
import jadx.core.dex.nodes.MethodNode;
|
import jadx.core.dex.nodes.MethodNode;
|
||||||
import jadx.core.utils.Utils;
|
import jadx.core.utils.Utils;
|
||||||
|
|
||||||
public final class JavaMethod implements JavaNode {
|
public final class JavaMethod implements JavaNode {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(JavaMethod.class);
|
|
||||||
|
|
||||||
private final MethodNode mth;
|
private final MethodNode mth;
|
||||||
private final JavaClass parent;
|
private final JavaClass parent;
|
||||||
|
|
||||||
@@ -72,6 +69,18 @@ public final class JavaMethod implements JavaNode {
|
|||||||
return getDeclaringClass().getRootDecompiler().convertNodes(mth.getUseIn());
|
return getDeclaringClass().getRootDecompiler().convertNodes(mth.getUseIn());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<JavaNode> getUsed() {
|
||||||
|
return getDeclaringClass().getRootDecompiler().convertNodes(mth.getUsed());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<MethodInfo> getUnresolvedUsed() {
|
||||||
|
return mth.getUnresolvedUsed();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean callsSelf() {
|
||||||
|
return mth.callsSelf();
|
||||||
|
}
|
||||||
|
|
||||||
public List<JavaMethod> getOverrideRelatedMethods() {
|
public List<JavaMethod> getOverrideRelatedMethods() {
|
||||||
MethodOverrideAttr ovrdAttr = mth.get(AType.METHOD_OVERRIDE);
|
MethodOverrideAttr ovrdAttr = mth.get(AType.METHOD_OVERRIDE);
|
||||||
if (ovrdAttr == null) {
|
if (ovrdAttr == null) {
|
||||||
|
|||||||
@@ -32,8 +32,11 @@ public class CodeMetadataStorage implements ICodeMetadata {
|
|||||||
return new CodeMetadataStorage(Collections.emptyMap(), Collections.emptyNavigableMap());
|
return new CodeMetadataStorage(Collections.emptyMap(), Collections.emptyNavigableMap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// <decomp file line number> -> <dex debug line number>
|
||||||
private final Map<Integer, Integer> lines;
|
private final Map<Integer, Integer> lines;
|
||||||
|
|
||||||
|
// <character index into the file> -> <code annotation>
|
||||||
|
// the key is what is returned by AbstractCodeArea#getCaretPos() when clicking in a code panel.
|
||||||
private final NavigableMap<Integer, ICodeAnnotation> navMap;
|
private final NavigableMap<Integer, ICodeAnnotation> navMap;
|
||||||
|
|
||||||
private CodeMetadataStorage(Map<Integer, Integer> lines, NavigableMap<Integer, ICodeAnnotation> navMap) {
|
private CodeMetadataStorage(Map<Integer, Integer> lines, NavigableMap<Integer, ICodeAnnotation> navMap) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package jadx.api.usage;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import jadx.core.dex.info.MethodInfo;
|
||||||
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.MethodNode;
|
import jadx.core.dex.nodes.MethodNode;
|
||||||
@@ -18,5 +19,11 @@ public interface IUsageInfoVisitor {
|
|||||||
|
|
||||||
void visitMethodsUsage(MethodNode mth, List<MethodNode> methods);
|
void visitMethodsUsage(MethodNode mth, List<MethodNode> methods);
|
||||||
|
|
||||||
|
void visitMethodsUses(MethodNode mth, List<MethodNode> methods);
|
||||||
|
|
||||||
|
void visitUnresolvedMethodsUsage(MethodNode mth, List<MethodInfo> methods);
|
||||||
|
|
||||||
|
void visitIsSelfCall(MethodNode mth, boolean isSelfCall);
|
||||||
|
|
||||||
void visitComplete();
|
void visitComplete();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import jadx.api.JadxArgs;
|
|||||||
import jadx.core.deobf.DeobfuscatorVisitor;
|
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.AdjustForIfMergeVisitor;
|
||||||
import jadx.core.dex.visitors.AnonymousClassVisitor;
|
import jadx.core.dex.visitors.AnonymousClassVisitor;
|
||||||
import jadx.core.dex.visitors.ApplyVariableNames;
|
import jadx.core.dex.visitors.ApplyVariableNames;
|
||||||
import jadx.core.dex.visitors.AttachCommentsVisitor;
|
import jadx.core.dex.visitors.AttachCommentsVisitor;
|
||||||
@@ -156,6 +157,8 @@ public class Jadx {
|
|||||||
passes.add(new FixTypesVisitor());
|
passes.add(new FixTypesVisitor());
|
||||||
passes.add(new FinishTypeInference());
|
passes.add(new FinishTypeInference());
|
||||||
|
|
||||||
|
passes.add(new AdjustForIfMergeVisitor());
|
||||||
|
|
||||||
if (args.getUseKotlinMethodsForVarNames() != JadxArgs.UseKotlinMethodsForVarNames.DISABLE) {
|
if (args.getUseKotlinMethodsForVarNames() != JadxArgs.UseKotlinMethodsForVarNames.DISABLE) {
|
||||||
passes.add(new ProcessKotlinInternals());
|
passes.add(new ProcessKotlinInternals());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import jadx.core.dex.attributes.AType;
|
|||||||
import jadx.core.dex.attributes.nodes.DecompileModeOverrideAttr;
|
import jadx.core.dex.attributes.nodes.DecompileModeOverrideAttr;
|
||||||
import jadx.core.dex.nodes.ClassNode;
|
import jadx.core.dex.nodes.ClassNode;
|
||||||
import jadx.core.dex.nodes.LoadStage;
|
import jadx.core.dex.nodes.LoadStage;
|
||||||
|
import jadx.core.dex.nodes.MethodNode;
|
||||||
import jadx.core.dex.nodes.RootNode;
|
import jadx.core.dex.nodes.RootNode;
|
||||||
import jadx.core.dex.visitors.DepthTraversal;
|
import jadx.core.dex.visitors.DepthTraversal;
|
||||||
import jadx.core.dex.visitors.IDexTreeVisitor;
|
import jadx.core.dex.visitors.IDexTreeVisitor;
|
||||||
@@ -207,6 +208,44 @@ public class ProcessClass {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean processMethodUntilVisitor(MethodNode mth, String visitorName, boolean includeVisitor) {
|
||||||
|
IDexTreeVisitor foundPass = null;
|
||||||
|
IDexTreeVisitor prevPass = null;
|
||||||
|
for (IDexTreeVisitor pass : passes) {
|
||||||
|
if (pass.getName().equals(visitorName)) {
|
||||||
|
if (includeVisitor) {
|
||||||
|
foundPass = pass;
|
||||||
|
} else {
|
||||||
|
foundPass = prevPass;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
prevPass = pass;
|
||||||
|
}
|
||||||
|
if (foundPass == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return processMethodToVisitor(mth, foundPass);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean processMethodToVisitor(MethodNode mth, IDexTreeVisitor lastPassToProcess) {
|
||||||
|
synchronized (mth.getTopParentClass().getClassInfo()) {
|
||||||
|
try {
|
||||||
|
mth.unload();
|
||||||
|
mth.load();
|
||||||
|
for (IDexTreeVisitor pass : passes) {
|
||||||
|
DepthTraversal.visit(pass, mth);
|
||||||
|
if (pass == lastPassToProcess) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new JadxRuntimeException("Failed to process method to visitor: " + lastPassToProcess, e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: make passes list private and not visible
|
// TODO: make passes list private and not visible
|
||||||
public List<IDexTreeVisitor> getPasses() {
|
public List<IDexTreeVisitor> getPasses() {
|
||||||
return passes;
|
return passes;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import jadx.core.dex.attributes.nodes.EnumClassAttr;
|
|||||||
import jadx.core.dex.attributes.nodes.EnumClassAttr.EnumField;
|
import jadx.core.dex.attributes.nodes.EnumClassAttr.EnumField;
|
||||||
import jadx.core.dex.attributes.nodes.LineAttrNode;
|
import jadx.core.dex.attributes.nodes.LineAttrNode;
|
||||||
import jadx.core.dex.attributes.nodes.MethodInlineAttr;
|
import jadx.core.dex.attributes.nodes.MethodInlineAttr;
|
||||||
|
import jadx.core.dex.attributes.nodes.NotificationAttrNode;
|
||||||
import jadx.core.dex.attributes.nodes.SkipMethodArgsAttr;
|
import jadx.core.dex.attributes.nodes.SkipMethodArgsAttr;
|
||||||
import jadx.core.dex.info.AccessInfo;
|
import jadx.core.dex.info.AccessInfo;
|
||||||
import jadx.core.dex.info.ClassInfo;
|
import jadx.core.dex.info.ClassInfo;
|
||||||
@@ -164,12 +165,7 @@ public class ClassGen {
|
|||||||
if (af.isInterface()) {
|
if (af.isInterface()) {
|
||||||
af = af.remove(AccessFlags.ABSTRACT)
|
af = af.remove(AccessFlags.ABSTRACT)
|
||||||
.remove(AccessFlags.STATIC);
|
.remove(AccessFlags.STATIC);
|
||||||
} else if (af.isEnum()) {
|
|
||||||
af = af.remove(AccessFlags.FINAL)
|
|
||||||
.remove(AccessFlags.ABSTRACT)
|
|
||||||
.remove(AccessFlags.STATIC);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 'static' and 'private' modifier not allowed for top classes (not inner)
|
// 'static' and 'private' modifier not allowed for top classes (not inner)
|
||||||
if (!cls.getClassInfo().isInner()) {
|
if (!cls.getClassInfo().isInner()) {
|
||||||
af = af.remove(AccessFlags.STATIC).remove(AccessFlags.PRIVATE);
|
af = af.remove(AccessFlags.STATIC).remove(AccessFlags.PRIVATE);
|
||||||
@@ -294,7 +290,7 @@ public class ClassGen {
|
|||||||
private void addInnerClsAndMethods(ICodeWriter clsCode) {
|
private void addInnerClsAndMethods(ICodeWriter clsCode) {
|
||||||
Stream.of(cls.getInnerClasses(), cls.getMethods())
|
Stream.of(cls.getInnerClasses(), cls.getMethods())
|
||||||
.flatMap(Collection::stream)
|
.flatMap(Collection::stream)
|
||||||
.filter(node -> !node.contains(AFlag.DONT_GENERATE) || fallback)
|
.filter(node -> !skipNode(node))
|
||||||
.sorted(Comparator.comparingInt(LineAttrNode::getSourceLine))
|
.sorted(Comparator.comparingInt(LineAttrNode::getSourceLine))
|
||||||
.forEach(node -> {
|
.forEach(node -> {
|
||||||
if (node instanceof ClassNode) {
|
if (node instanceof ClassNode) {
|
||||||
@@ -305,6 +301,18 @@ public class ClassGen {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean skipNode(NotificationAttrNode node) {
|
||||||
|
if (fallback) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (Consts.DEBUG_ATTRIBUTES) {
|
||||||
|
if (node.contains(AType.JADX_COMMENTS)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return node.contains(AFlag.DONT_GENERATE);
|
||||||
|
}
|
||||||
|
|
||||||
private void addInnerClass(ICodeWriter code, ClassNode innerCls) {
|
private void addInnerClass(ICodeWriter code, ClassNode innerCls) {
|
||||||
try {
|
try {
|
||||||
ClassGen inClGen = new ClassGen(innerCls, getParentGen());
|
ClassGen inClGen = new ClassGen(innerCls, getParentGen());
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package jadx.core.codegen;
|
package jadx.core.codegen;
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
@@ -29,6 +27,7 @@ import jadx.core.dex.attributes.nodes.JadxError;
|
|||||||
import jadx.core.dex.attributes.nodes.JumpInfo;
|
import jadx.core.dex.attributes.nodes.JumpInfo;
|
||||||
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.SkipMethodArgsAttr;
|
||||||
import jadx.core.dex.info.AccessInfo;
|
import jadx.core.dex.info.AccessInfo;
|
||||||
import jadx.core.dex.instructions.ConstStringNode;
|
import jadx.core.dex.instructions.ConstStringNode;
|
||||||
import jadx.core.dex.instructions.IfNode;
|
import jadx.core.dex.instructions.IfNode;
|
||||||
@@ -109,10 +108,6 @@ public class MethodGen {
|
|||||||
if (clsAccFlags.isAnnotation()) {
|
if (clsAccFlags.isAnnotation()) {
|
||||||
ai = ai.remove(AccessFlags.PUBLIC);
|
ai = ai.remove(AccessFlags.PUBLIC);
|
||||||
}
|
}
|
||||||
if (mth.getMethodInfo().isConstructor() && mth.getParentClass().isEnum()) {
|
|
||||||
ai = ai.remove(AccessInfo.VISIBILITY_FLAGS);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mth.getMethodInfo().hasAlias() && !ai.isConstructor()) {
|
if (mth.getMethodInfo().hasAlias() && !ai.isConstructor()) {
|
||||||
CodeGenUtils.addRenamedComment(code, mth, mth.getName());
|
CodeGenUtils.addRenamedComment(code, mth, mth.getName());
|
||||||
}
|
}
|
||||||
@@ -152,21 +147,7 @@ public class MethodGen {
|
|||||||
code.add(defMth.getAlias());
|
code.add(defMth.getAlias());
|
||||||
}
|
}
|
||||||
code.add('(');
|
code.add('(');
|
||||||
|
addMethodArguments(code);
|
||||||
List<RegisterArg> args = mth.getArgRegs();
|
|
||||||
if (mth.getMethodInfo().isConstructor()
|
|
||||||
&& mth.getParentClass().contains(AType.ENUM_CLASS)) {
|
|
||||||
if (args.size() == 2) {
|
|
||||||
args = Collections.emptyList();
|
|
||||||
} else if (args.size() > 2) {
|
|
||||||
args = args.subList(2, args.size());
|
|
||||||
} else {
|
|
||||||
mth.addWarnComment("Incorrect number of args for enum constructor: " + args.size() + " (expected >= 2)");
|
|
||||||
}
|
|
||||||
} else if (mth.contains(AFlag.SKIP_FIRST_ARG)) {
|
|
||||||
args = args.subList(1, args.size());
|
|
||||||
}
|
|
||||||
addMethodArguments(code, args);
|
|
||||||
code.add(')');
|
code.add(')');
|
||||||
|
|
||||||
annotationGen.addThrows(mth, code);
|
annotationGen.addThrows(mth, code);
|
||||||
@@ -209,12 +190,22 @@ public class MethodGen {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addMethodArguments(ICodeWriter code, List<RegisterArg> args) {
|
private void addMethodArguments(ICodeWriter code) {
|
||||||
|
List<RegisterArg> args = mth.getArgRegs();
|
||||||
AnnotationMethodParamsAttr paramsAnnotation = mth.get(JadxAttrType.ANNOTATION_MTH_PARAMETERS);
|
AnnotationMethodParamsAttr paramsAnnotation = mth.get(JadxAttrType.ANNOTATION_MTH_PARAMETERS);
|
||||||
int i = 0;
|
int argNum = -1;
|
||||||
Iterator<RegisterArg> it = args.iterator();
|
int lastArgNum = args.size() - 1;
|
||||||
while (it.hasNext()) {
|
boolean first = true;
|
||||||
RegisterArg mthArg = it.next();
|
for (RegisterArg mthArg : args) {
|
||||||
|
argNum++;
|
||||||
|
if (SkipMethodArgsAttr.isSkip(mth, argNum)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (first) {
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
code.add(", ");
|
||||||
|
}
|
||||||
SSAVar ssaVar = mthArg.getSVar();
|
SSAVar ssaVar = mthArg.getSVar();
|
||||||
CodeVar var;
|
CodeVar var;
|
||||||
if (ssaVar == null) {
|
if (ssaVar == null) {
|
||||||
@@ -226,7 +217,7 @@ public class MethodGen {
|
|||||||
|
|
||||||
// add argument annotation
|
// add argument annotation
|
||||||
if (paramsAnnotation != null) {
|
if (paramsAnnotation != null) {
|
||||||
annotationGen.addForParameter(code, paramsAnnotation, i);
|
annotationGen.addForParameter(code, paramsAnnotation, argNum);
|
||||||
}
|
}
|
||||||
if (var.isFinal()) {
|
if (var.isFinal()) {
|
||||||
code.add("final ");
|
code.add("final ");
|
||||||
@@ -239,7 +230,7 @@ public class MethodGen {
|
|||||||
} else {
|
} else {
|
||||||
argType = varType;
|
argType = varType;
|
||||||
}
|
}
|
||||||
if (!it.hasNext() && mth.getAccessFlags().isVarArgs()) {
|
if (argNum == lastArgNum && mth.getAccessFlags().isVarArgs()) {
|
||||||
// change last array argument to varargs
|
// change last array argument to varargs
|
||||||
if (argType.isArray()) {
|
if (argType.isArray()) {
|
||||||
ArgType elType = argType.getArrayElement();
|
ArgType elType = argType.getArrayElement();
|
||||||
@@ -258,11 +249,6 @@ public class MethodGen {
|
|||||||
code.attachDefinition(VarNode.get(mth, var));
|
code.attachDefinition(VarNode.get(mth, var));
|
||||||
}
|
}
|
||||||
code.add(varName);
|
code.add(varName);
|
||||||
|
|
||||||
i++;
|
|
||||||
if (it.hasNext()) {
|
|
||||||
code.add(", ");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,13 +397,12 @@ public class MethodGen {
|
|||||||
for (IDexTreeVisitor visitor : Jadx.getFallbackPassesList()) {
|
for (IDexTreeVisitor visitor : Jadx.getFallbackPassesList()) {
|
||||||
DepthTraversal.visit(visitor, mth);
|
DepthTraversal.visit(visitor, mth);
|
||||||
}
|
}
|
||||||
errors.forEach(err -> mth.addAttr(AType.JADX_ERROR, err));
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOG.error("Error reload instructions in fallback mode:", e);
|
LOG.error("Error reload instructions in fallback mode:", e);
|
||||||
code.startLine("// Can't load method instructions: " + e.getMessage());
|
code.startLine("// Can't load method instructions: " + e.getMessage());
|
||||||
return;
|
return;
|
||||||
} finally {
|
} finally {
|
||||||
errors.forEach(err -> mth.addAttr(AType.JADX_ERROR, err));
|
mth.addAttr(AType.JADX_ERROR, errors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
InsnNode[] insnArr = mth.getInstructions();
|
InsnNode[] insnArr = mth.getInstructions();
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package jadx.core.codegen;
|
|||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import jadx.core.deobf.NameMapper;
|
import jadx.core.deobf.NameMapper;
|
||||||
import jadx.core.dex.attributes.nodes.LoopLabelAttr;
|
import jadx.core.dex.attributes.nodes.LoopLabelAttr;
|
||||||
@@ -84,15 +86,32 @@ public class NameGen {
|
|||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final Pattern ENDS_WITH_NUMBER = Pattern.compile(".*(\\d+)$");
|
||||||
|
|
||||||
private String getUniqueVarName(String name) {
|
private String getUniqueVarName(String name) {
|
||||||
String r = name;
|
if (!varNames.contains(name)) {
|
||||||
int i = 2;
|
varNames.add(name);
|
||||||
while (varNames.contains(r)) {
|
return name;
|
||||||
r = name + i;
|
}
|
||||||
i++;
|
// code duplication reuse same variable in different places
|
||||||
|
// parse variable name and increment index
|
||||||
|
String base;
|
||||||
|
int i;
|
||||||
|
Matcher matcher = ENDS_WITH_NUMBER.matcher(name);
|
||||||
|
if (matcher.matches()) {
|
||||||
|
base = name.substring(0, matcher.start(1));
|
||||||
|
i = 1 + Integer.parseInt(matcher.group(1));
|
||||||
|
} else {
|
||||||
|
base = name;
|
||||||
|
i = 2;
|
||||||
|
}
|
||||||
|
while (true) {
|
||||||
|
String newName = base + i++;
|
||||||
|
if (!varNames.contains(newName)) {
|
||||||
|
varNames.add(newName);
|
||||||
|
return newName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
varNames.add(r);
|
|
||||||
return r;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String makeArgName(CodeVar var) {
|
private String makeArgName(CodeVar var) {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package jadx.core.codegen;
|
package jadx.core.codegen;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -30,6 +32,10 @@ public class TypeGen {
|
|||||||
return stype.getShortName();
|
return stype.getShortName();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static List<String> signatures(List<ArgType> types) {
|
||||||
|
return Utils.collectionMap(types, TypeGen::signature);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert literal arg to string (preferred method)
|
* Convert literal arg to string (preferred method)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -83,9 +83,12 @@ public class FileTypeDetector {
|
|||||||
try {
|
try {
|
||||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||||
factory.setNamespaceAware(true);
|
factory.setNamespaceAware(true);
|
||||||
|
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
|
||||||
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
|
factory.setFeature("http://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-general-entities", false);
|
||||||
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
|
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
|
||||||
|
factory.setXIncludeAware(false);
|
||||||
|
factory.setExpandEntityReferences(false);
|
||||||
|
|
||||||
DocumentBuilder builder = factory.newDocumentBuilder();
|
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||||
Document doc = builder.parse(new java.io.ByteArrayInputStream(data));
|
Document doc = builder.parse(new java.io.ByteArrayInputStream(data));
|
||||||
|
|||||||
@@ -18,17 +18,23 @@ public enum AFlag {
|
|||||||
DONT_WRAP,
|
DONT_WRAP,
|
||||||
DONT_INLINE,
|
DONT_INLINE,
|
||||||
DONT_INLINE_CONST,
|
DONT_INLINE_CONST,
|
||||||
|
DONT_INVERT, // don't invert this if statement
|
||||||
DONT_GENERATE, // process as usual, but don't output to generated code
|
DONT_GENERATE, // process as usual, but don't output to generated code
|
||||||
COMMENT_OUT, // process as usual, but comment insn in generated code
|
COMMENT_OUT, // process as usual, but comment insn in generated code
|
||||||
REMOVE, // can be completely removed
|
REMOVE, // can be completely removed
|
||||||
REMOVE_SUPER_CLASS, // don't add super class
|
REMOVE_SUPER_CLASS, // don't add super class
|
||||||
|
|
||||||
HIDDEN, // instruction used inside other instruction but not listed in args
|
HIDDEN, // instruction used inside other instruction but not listed in args
|
||||||
|
CONVERTED_ENUM, // enum class successfully restored to original form
|
||||||
|
|
||||||
DONT_RENAME, // do not rename during deobfuscation
|
DONT_RENAME, // do not rename during deobfuscation
|
||||||
FORCE_RAW_NAME, // force use of raw name instead alias
|
FORCE_RAW_NAME, // force use of raw name instead alias
|
||||||
|
|
||||||
ADDED_TO_REGION,
|
ADDED_TO_REGION,
|
||||||
|
DUPLICATED,
|
||||||
|
|
||||||
|
// this loop condition has been merged or otherwise shouldn't be subject to the 1 instruction limit
|
||||||
|
ALLOW_MULTIPLE_INSNS_LOOP_COND,
|
||||||
|
|
||||||
EXC_TOP_SPLITTER,
|
EXC_TOP_SPLITTER,
|
||||||
EXC_BOTTOM_SPLITTER,
|
EXC_BOTTOM_SPLITTER,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import jadx.core.dex.attributes.nodes.DecompileModeOverrideAttr;
|
|||||||
import jadx.core.dex.attributes.nodes.EdgeInsnAttr;
|
import jadx.core.dex.attributes.nodes.EdgeInsnAttr;
|
||||||
import jadx.core.dex.attributes.nodes.EnumClassAttr;
|
import jadx.core.dex.attributes.nodes.EnumClassAttr;
|
||||||
import jadx.core.dex.attributes.nodes.EnumMapAttr;
|
import jadx.core.dex.attributes.nodes.EnumMapAttr;
|
||||||
|
import jadx.core.dex.attributes.nodes.ExcSplitCrossAttr;
|
||||||
import jadx.core.dex.attributes.nodes.FieldReplaceAttr;
|
import jadx.core.dex.attributes.nodes.FieldReplaceAttr;
|
||||||
import jadx.core.dex.attributes.nodes.ForceReturnAttr;
|
import jadx.core.dex.attributes.nodes.ForceReturnAttr;
|
||||||
import jadx.core.dex.attributes.nodes.GenericInfoAttr;
|
import jadx.core.dex.attributes.nodes.GenericInfoAttr;
|
||||||
@@ -92,6 +93,7 @@ public final class AType<T extends IJadxAttribute> implements IJadxAttrType<T> {
|
|||||||
public static final AType<AttrList<SpecialEdgeAttr>> SPECIAL_EDGE = new AType<>();
|
public static final AType<AttrList<SpecialEdgeAttr>> SPECIAL_EDGE = new AType<>();
|
||||||
public static final AType<TmpEdgeAttr> TMP_EDGE = new AType<>();
|
public static final AType<TmpEdgeAttr> TMP_EDGE = new AType<>();
|
||||||
public static final AType<TryCatchBlockAttr> TRY_BLOCK = new AType<>();
|
public static final AType<TryCatchBlockAttr> TRY_BLOCK = new AType<>();
|
||||||
|
public static final AType<ExcSplitCrossAttr> EXC_SPLIT_CROSS = new AType<>();
|
||||||
|
|
||||||
// block or insn
|
// block or insn
|
||||||
public static final AType<ExcHandlerAttr> EXC_HANDLER = new AType<>();
|
public static final AType<ExcHandlerAttr> EXC_HANDLER = new AType<>();
|
||||||
|
|||||||
@@ -9,11 +9,19 @@ import jadx.core.utils.Utils;
|
|||||||
|
|
||||||
public class AttrList<T> implements IJadxAttribute {
|
public class AttrList<T> implements IJadxAttribute {
|
||||||
|
|
||||||
|
private static final int MAX_ATTRLIST_LENGTH = 300;
|
||||||
|
|
||||||
private final IJadxAttrType<AttrList<T>> type;
|
private final IJadxAttrType<AttrList<T>> type;
|
||||||
private final List<T> list = new ArrayList<>();
|
private final List<T> list;
|
||||||
|
|
||||||
|
public AttrList(IJadxAttrType<AttrList<T>> type, List<T> attrList) {
|
||||||
|
this.type = type;
|
||||||
|
this.list = attrList;
|
||||||
|
}
|
||||||
|
|
||||||
public AttrList(IJadxAttrType<AttrList<T>> type) {
|
public AttrList(IJadxAttrType<AttrList<T>> type) {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
|
this.list = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<T> getList() {
|
public List<T> getList() {
|
||||||
@@ -27,6 +35,11 @@ public class AttrList<T> implements IJadxAttribute {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return Utils.listToString(list, ", ");
|
String commaDelimited = Utils.listToString(list, ", ");
|
||||||
|
// if the comma delimited list is too long, use newlines instead to maintain readability
|
||||||
|
if (commaDelimited.length() > MAX_ATTRLIST_LENGTH) {
|
||||||
|
return Utils.listToString(list, "\n ");
|
||||||
|
}
|
||||||
|
return commaDelimited;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,8 +50,7 @@ public abstract class AttrNode implements IAttributeNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public <T> void addAttr(IJadxAttrType<AttrList<T>> type, List<T> list) {
|
public <T> void addAttr(IJadxAttrType<AttrList<T>> type, List<T> list) {
|
||||||
AttributeStorage strg = initStorage();
|
initStorage().addAttrList(type, list);
|
||||||
list.forEach(attr -> strg.add(type, attr));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import jadx.api.plugins.input.data.attributes.IJadxAttrType;
|
|||||||
import jadx.api.plugins.input.data.attributes.IJadxAttribute;
|
import jadx.api.plugins.input.data.attributes.IJadxAttribute;
|
||||||
import jadx.api.plugins.input.data.attributes.JadxAttrType;
|
import jadx.api.plugins.input.data.attributes.JadxAttrType;
|
||||||
import jadx.api.plugins.input.data.attributes.types.AnnotationsAttr;
|
import jadx.api.plugins.input.data.attributes.types.AnnotationsAttr;
|
||||||
|
import jadx.core.utils.ListUtils;
|
||||||
import jadx.core.utils.Utils;
|
import jadx.core.utils.Utils;
|
||||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||||
|
|
||||||
@@ -62,11 +63,20 @@ public class AttributeStorage {
|
|||||||
|
|
||||||
public <T> void add(IJadxAttrType<AttrList<T>> type, T obj) {
|
public <T> void add(IJadxAttrType<AttrList<T>> type, T obj) {
|
||||||
AttrList<T> list = get(type);
|
AttrList<T> list = get(type);
|
||||||
if (list == null) {
|
if (list != null) {
|
||||||
list = new AttrList<>(type);
|
list.getList().add(obj);
|
||||||
add(list);
|
} else {
|
||||||
|
add(new AttrList<>(type, ListUtils.mutableListOf(obj)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> void addAttrList(IJadxAttrType<AttrList<T>> type, List<T> attrList) {
|
||||||
|
AttrList<T> list = get(type);
|
||||||
|
if (list != null) {
|
||||||
|
list.getList().addAll(attrList);
|
||||||
|
} else {
|
||||||
|
add(new AttrList<>(type, attrList));
|
||||||
}
|
}
|
||||||
list.getList().add(obj);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addAll(AttributeStorage otherList) {
|
public void addAll(AttributeStorage otherList) {
|
||||||
|
|||||||
@@ -53,4 +53,9 @@ public class CodeFeaturesAttr implements IJadxAttribute {
|
|||||||
public String toAttrString() {
|
public String toAttrString() {
|
||||||
return "CodeFeatures{" + codeFeatures + '}';
|
return "CodeFeatures{" + codeFeatures + '}';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return toAttrString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package jadx.core.dex.attributes.nodes;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import jadx.api.plugins.input.data.attributes.IJadxAttribute;
|
import jadx.api.plugins.input.data.attributes.IJadxAttribute;
|
||||||
import jadx.core.dex.attributes.AType;
|
import jadx.core.dex.attributes.AType;
|
||||||
import jadx.core.dex.instructions.mods.ConstructorInsn;
|
import jadx.core.dex.instructions.mods.ConstructorInsn;
|
||||||
@@ -14,11 +16,13 @@ public class EnumClassAttr implements IJadxAttribute {
|
|||||||
public static class EnumField {
|
public static class EnumField {
|
||||||
private final FieldNode field;
|
private final FieldNode field;
|
||||||
private final ConstructorInsn constrInsn;
|
private final ConstructorInsn constrInsn;
|
||||||
|
private final @Nullable String nameStr;
|
||||||
private ClassNode cls;
|
private ClassNode cls;
|
||||||
|
|
||||||
public EnumField(FieldNode field, ConstructorInsn co) {
|
public EnumField(FieldNode field, ConstructorInsn co, @Nullable String nameStr) {
|
||||||
this.field = field;
|
this.field = field;
|
||||||
this.constrInsn = co;
|
this.constrInsn = co;
|
||||||
|
this.nameStr = nameStr;
|
||||||
}
|
}
|
||||||
|
|
||||||
public FieldNode getField() {
|
public FieldNode getField() {
|
||||||
@@ -37,6 +41,10 @@ public class EnumClassAttr implements IJadxAttribute {
|
|||||||
this.cls = cls;
|
this.cls = cls;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public @Nullable String getNameStr() {
|
||||||
|
return nameStr;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return field + "(" + constrInsn + ") " + cls;
|
return field + "(" + constrInsn + ") " + cls;
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package jadx.core.dex.attributes.nodes;
|
||||||
|
|
||||||
|
import jadx.api.plugins.input.data.attributes.IJadxAttrType;
|
||||||
|
import jadx.api.plugins.input.data.attributes.IJadxAttribute;
|
||||||
|
import jadx.core.dex.attributes.AType;
|
||||||
|
import jadx.core.dex.nodes.BlockNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This attribute is set on the new synthetic node that BlockExceptionHandler creates at the bottom
|
||||||
|
* of certain try regions. It stores a reference to the original path cross of the bottom of the try
|
||||||
|
* region, so that blocks can be restructured to not pass through it when that would create an
|
||||||
|
* erroneous loop.
|
||||||
|
*/
|
||||||
|
public class ExcSplitCrossAttr implements IJadxAttribute {
|
||||||
|
|
||||||
|
private final BlockNode originalPathCross;
|
||||||
|
|
||||||
|
public ExcSplitCrossAttr(BlockNode originalPathCross) {
|
||||||
|
this.originalPathCross = originalPathCross;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BlockNode getOriginalPathCross() {
|
||||||
|
return this.originalPathCross;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IJadxAttrType<? extends IJadxAttribute> getAttrType() {
|
||||||
|
return AType.EXC_SPLIT_CROSS;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "ExcSplitCross -> " + originalPathCross.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import java.util.BitSet;
|
|||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import jadx.api.plugins.input.data.attributes.PinnedAttribute;
|
import jadx.api.plugins.input.data.attributes.PinnedAttribute;
|
||||||
|
import jadx.core.dex.attributes.AFlag;
|
||||||
import jadx.core.dex.attributes.AType;
|
import jadx.core.dex.attributes.AType;
|
||||||
import jadx.core.dex.instructions.args.RegisterArg;
|
import jadx.core.dex.instructions.args.RegisterArg;
|
||||||
import jadx.core.dex.nodes.MethodNode;
|
import jadx.core.dex.nodes.MethodNode;
|
||||||
@@ -34,6 +35,9 @@ public class SkipMethodArgsAttr extends PinnedAttribute {
|
|||||||
if (mth == null) {
|
if (mth == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (argNum == 0 && mth.contains(AFlag.SKIP_FIRST_ARG)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
SkipMethodArgsAttr attr = mth.get(AType.SKIP_MTH_ARGS);
|
SkipMethodArgsAttr attr = mth.get(AType.SKIP_MTH_ARGS);
|
||||||
if (attr == null) {
|
if (attr == null) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -214,9 +214,12 @@ public final class ClassInfo implements Comparable<ClassInfo> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String getAliasFullPath() {
|
public String getAliasFullPath() {
|
||||||
return getAliasPkg().replace('.', File.separatorChar)
|
String fileName = getAliasNameWithoutPackage().replace('.', '_');
|
||||||
+ File.separatorChar
|
String aliasPkg = getAliasPkg();
|
||||||
+ getAliasNameWithoutPackage().replace('.', '_');
|
if (aliasPkg.isEmpty()) {
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
return aliasPkg.replace('.', File.separatorChar) + File.separatorChar + fileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getFullName() {
|
public String getFullName() {
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import jadx.core.dex.nodes.ClassNode;
|
|||||||
import jadx.core.dex.nodes.FieldNode;
|
import jadx.core.dex.nodes.FieldNode;
|
||||||
import jadx.core.dex.nodes.IFieldInfoRef;
|
import jadx.core.dex.nodes.IFieldInfoRef;
|
||||||
import jadx.core.dex.nodes.RootNode;
|
import jadx.core.dex.nodes.RootNode;
|
||||||
import jadx.core.dex.visitors.prepare.CollectConstValues;
|
|
||||||
|
|
||||||
public class ConstStorage {
|
public class ConstStorage {
|
||||||
|
|
||||||
@@ -23,18 +22,18 @@ public class ConstStorage {
|
|||||||
private final Map<Object, IFieldInfoRef> values = new ConcurrentHashMap<>();
|
private final Map<Object, IFieldInfoRef> values = new ConcurrentHashMap<>();
|
||||||
private final Set<Object> duplicates = new HashSet<>();
|
private final Set<Object> duplicates = new HashSet<>();
|
||||||
|
|
||||||
public Map<Object, IFieldInfoRef> getValues() {
|
Map<Object, IFieldInfoRef> getValues() {
|
||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IFieldInfoRef get(Object key) {
|
IFieldInfoRef get(Object key) {
|
||||||
return values.get(key);
|
return values.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return true if this value is duplicated
|
* @return true if this value is duplicated
|
||||||
*/
|
*/
|
||||||
public boolean put(Object value, IFieldInfoRef fld) {
|
boolean put(Object value, IFieldInfoRef fld) {
|
||||||
if (duplicates.contains(value)) {
|
if (duplicates.contains(value)) {
|
||||||
values.remove(value);
|
values.remove(value);
|
||||||
return true;
|
return true;
|
||||||
@@ -85,14 +84,6 @@ public class ConstStorage {
|
|||||||
globalValues.put(value, fld);
|
globalValues.put(value, fld);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Use method from CollectConstValues class
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public static @Nullable Object getFieldConstValue(FieldNode fld) {
|
|
||||||
return CollectConstValues.getFieldConstValue(fld);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeForClass(ClassNode cls) {
|
public void removeForClass(ClassNode cls) {
|
||||||
classes.remove(cls);
|
classes.remove(cls);
|
||||||
globalValues.removeForCls(cls);
|
globalValues.removeForCls(cls);
|
||||||
|
|||||||
@@ -35,6 +35,6 @@ public final class ConstStringNode extends InsnNode {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return super.toString() + ' ' + StringUtils.getInstance().unescapeString(str);
|
return super.baseString() + StringUtils.getInstance().unescapeString(str) + super.attributesString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -348,6 +348,7 @@ public class InsnDecoder {
|
|||||||
return insn(InsnType.THROW, null, InsnArg.reg(insn, 0, ArgType.THROWABLE));
|
return insn(InsnType.THROW, null, InsnArg.reg(insn, 0, ArgType.THROWABLE));
|
||||||
|
|
||||||
case MOVE_EXCEPTION:
|
case MOVE_EXCEPTION:
|
||||||
|
method.add(AFlag.COMPUTE_POST_DOM); // Post dominators required for try/catch block processing
|
||||||
return insn(InsnType.MOVE_EXCEPTION, InsnArg.reg(insn, 0, ArgType.UNKNOWN_OBJECT_NO_ARRAY));
|
return insn(InsnType.MOVE_EXCEPTION, InsnArg.reg(insn, 0, ArgType.UNKNOWN_OBJECT_NO_ARRAY));
|
||||||
|
|
||||||
case RETURN_VOID:
|
case RETURN_VOID:
|
||||||
@@ -527,18 +528,10 @@ public class InsnDecoder {
|
|||||||
|
|
||||||
private InsnNode makeNewArray(InsnData insn) {
|
private InsnNode makeNewArray(InsnData insn) {
|
||||||
ArgType indexType = ArgType.parse(insn.getIndexAsType());
|
ArgType indexType = ArgType.parse(insn.getIndexAsType());
|
||||||
|
// NEW_ARRAY literal = dimensions to wrap the operand by: 0 if it is already the full array type
|
||||||
|
// (dalvik new-array, java multianewarray), 1 for newarray/anewarray (operand is the element type)
|
||||||
int dim = (int) insn.getLiteral();
|
int dim = (int) insn.getLiteral();
|
||||||
ArgType arrType;
|
ArgType arrType = dim == 0 ? indexType : ArgType.array(indexType, dim);
|
||||||
if (dim == 0) {
|
|
||||||
arrType = indexType;
|
|
||||||
} else {
|
|
||||||
if (indexType.isArray()) {
|
|
||||||
// java bytecode can pass array as a base type
|
|
||||||
arrType = indexType;
|
|
||||||
} else {
|
|
||||||
arrType = ArgType.array(indexType, dim);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
int regsCount = insn.getRegsCount();
|
int regsCount = insn.getRegsCount();
|
||||||
NewArrayNode newArr = new NewArrayNode(arrType, regsCount - 1);
|
NewArrayNode newArr = new NewArrayNode(arrType, regsCount - 1);
|
||||||
newArr.setResult(InsnArg.reg(insn, 0, arrType));
|
newArr.setResult(InsnArg.reg(insn, 0, arrType));
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import jadx.core.dex.instructions.args.InsnArg;
|
|||||||
import jadx.core.dex.instructions.args.RegisterArg;
|
import jadx.core.dex.instructions.args.RegisterArg;
|
||||||
import jadx.core.dex.instructions.args.SSAVar;
|
import jadx.core.dex.instructions.args.SSAVar;
|
||||||
import jadx.core.dex.nodes.BlockNode;
|
import jadx.core.dex.nodes.BlockNode;
|
||||||
|
import jadx.core.dex.nodes.IBlock;
|
||||||
import jadx.core.dex.nodes.InsnNode;
|
import jadx.core.dex.nodes.InsnNode;
|
||||||
import jadx.core.utils.InsnRemover;
|
import jadx.core.utils.InsnRemover;
|
||||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||||
@@ -110,6 +111,18 @@ public final class PhiInsn extends InsnNode {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public RegisterArg getArgByBlock(IBlock block) {
|
||||||
|
if (getArgsCount() == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int index = blockBinds.indexOf(block);
|
||||||
|
if (index == -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return getArg(index);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean replaceArg(InsnArg from, InsnArg to) {
|
public boolean replaceArg(InsnArg from, InsnArg to) {
|
||||||
if (!(from instanceof RegisterArg) || !(to instanceof RegisterArg)) {
|
if (!(from instanceof RegisterArg) || !(to instanceof RegisterArg)) {
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
import org.jetbrains.annotations.TestOnly;
|
import org.jetbrains.annotations.TestOnly;
|
||||||
|
|
||||||
|
import com.google.errorprone.annotations.Immutable;
|
||||||
|
|
||||||
import jadx.core.Consts;
|
import jadx.core.Consts;
|
||||||
import jadx.core.dex.info.ClassInfo;
|
import jadx.core.dex.info.ClassInfo;
|
||||||
import jadx.core.dex.nodes.RootNode;
|
import jadx.core.dex.nodes.RootNode;
|
||||||
@@ -18,6 +20,7 @@ import jadx.core.utils.ListUtils;
|
|||||||
import jadx.core.utils.Utils;
|
import jadx.core.utils.Utils;
|
||||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||||
|
|
||||||
|
@Immutable
|
||||||
public abstract class ArgType {
|
public abstract class ArgType {
|
||||||
public static final ArgType INT = primitive(PrimitiveType.INT);
|
public static final ArgType INT = primitive(PrimitiveType.INT);
|
||||||
public static final ArgType BOOLEAN = primitive(PrimitiveType.BOOLEAN);
|
public static final ArgType BOOLEAN = primitive(PrimitiveType.BOOLEAN);
|
||||||
@@ -200,7 +203,7 @@ public abstract class ArgType {
|
|||||||
private static final class PrimitiveArg extends KnownType {
|
private static final class PrimitiveArg extends KnownType {
|
||||||
private final PrimitiveType type;
|
private final PrimitiveType type;
|
||||||
|
|
||||||
public PrimitiveArg(PrimitiveType type) {
|
PrimitiveArg(PrimitiveType type) {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.hash = type.hashCode();
|
this.hash = type.hashCode();
|
||||||
}
|
}
|
||||||
@@ -229,7 +232,7 @@ public abstract class ArgType {
|
|||||||
private static class ObjectType extends KnownType {
|
private static class ObjectType extends KnownType {
|
||||||
protected final String objName;
|
protected final String objName;
|
||||||
|
|
||||||
public ObjectType(String obj) {
|
ObjectType(String obj) {
|
||||||
this.objName = obj;
|
this.objName = obj;
|
||||||
this.hash = objName.hashCode();
|
this.hash = objName.hashCode();
|
||||||
}
|
}
|
||||||
@@ -263,15 +266,15 @@ public abstract class ArgType {
|
|||||||
private static final class GenericType extends ObjectType {
|
private static final class GenericType extends ObjectType {
|
||||||
private List<ArgType> extendTypes;
|
private List<ArgType> extendTypes;
|
||||||
|
|
||||||
public GenericType(String obj) {
|
GenericType(String obj) {
|
||||||
this(obj, Collections.emptyList());
|
this(obj, Collections.emptyList());
|
||||||
}
|
}
|
||||||
|
|
||||||
public GenericType(String obj, ArgType extendType) {
|
GenericType(String obj, ArgType extendType) {
|
||||||
this(obj, Collections.singletonList(extendType));
|
this(obj, Collections.singletonList(extendType));
|
||||||
}
|
}
|
||||||
|
|
||||||
public GenericType(String obj, List<ArgType> extendTypes) {
|
GenericType(String obj, List<ArgType> extendTypes) {
|
||||||
super(obj);
|
super(obj);
|
||||||
this.extendTypes = extendTypes;
|
this.extendTypes = extendTypes;
|
||||||
}
|
}
|
||||||
@@ -337,7 +340,7 @@ public abstract class ArgType {
|
|||||||
private final ArgType type;
|
private final ArgType type;
|
||||||
private final WildcardBound bound;
|
private final WildcardBound bound;
|
||||||
|
|
||||||
public WildcardType(ArgType obj, WildcardBound bound) {
|
WildcardType(ArgType obj, WildcardBound bound) {
|
||||||
super(OBJECT.getObject());
|
super(OBJECT.getObject());
|
||||||
this.type = Objects.requireNonNull(obj);
|
this.type = Objects.requireNonNull(obj);
|
||||||
this.bound = Objects.requireNonNull(bound);
|
this.bound = Objects.requireNonNull(bound);
|
||||||
@@ -382,7 +385,7 @@ public abstract class ArgType {
|
|||||||
private static class GenericObject extends ObjectType {
|
private static class GenericObject extends ObjectType {
|
||||||
private final List<ArgType> generics;
|
private final List<ArgType> generics;
|
||||||
|
|
||||||
public GenericObject(String obj, List<ArgType> generics) {
|
GenericObject(String obj, List<ArgType> generics) {
|
||||||
super(obj);
|
super(obj);
|
||||||
this.generics = Objects.requireNonNull(generics);
|
this.generics = Objects.requireNonNull(generics);
|
||||||
this.hash = calcHash();
|
this.hash = calcHash();
|
||||||
@@ -418,7 +421,7 @@ public abstract class ArgType {
|
|||||||
private final ObjectType outerType;
|
private final ObjectType outerType;
|
||||||
private final ObjectType innerType;
|
private final ObjectType innerType;
|
||||||
|
|
||||||
public OuterGenericObject(ObjectType outerType, ObjectType innerType) {
|
OuterGenericObject(ObjectType outerType, ObjectType innerType) {
|
||||||
super(outerType.getObject() + '$' + innerType.getObject());
|
super(outerType.getObject() + '$' + innerType.getObject());
|
||||||
this.outerType = outerType;
|
this.outerType = outerType;
|
||||||
this.innerType = innerType;
|
this.innerType = innerType;
|
||||||
@@ -466,7 +469,7 @@ public abstract class ArgType {
|
|||||||
private static final PrimitiveType[] ARRAY_POSSIBLES = new PrimitiveType[] { PrimitiveType.ARRAY };
|
private static final PrimitiveType[] ARRAY_POSSIBLES = new PrimitiveType[] { PrimitiveType.ARRAY };
|
||||||
private final ArgType arrayElement;
|
private final ArgType arrayElement;
|
||||||
|
|
||||||
public ArrayArg(ArgType arrayElement) {
|
ArrayArg(ArgType arrayElement) {
|
||||||
this.arrayElement = arrayElement;
|
this.arrayElement = arrayElement;
|
||||||
this.hash = arrayElement.hashCode();
|
this.hash = arrayElement.hashCode();
|
||||||
}
|
}
|
||||||
@@ -526,7 +529,7 @@ public abstract class ArgType {
|
|||||||
private static final class UnknownArg extends ArgType {
|
private static final class UnknownArg extends ArgType {
|
||||||
private final PrimitiveType[] possibleTypes;
|
private final PrimitiveType[] possibleTypes;
|
||||||
|
|
||||||
public UnknownArg(PrimitiveType[] types) {
|
UnknownArg(PrimitiveType[] types) {
|
||||||
this.possibleTypes = types;
|
this.possibleTypes = types;
|
||||||
this.hash = Arrays.hashCode(possibleTypes);
|
this.hash = Arrays.hashCode(possibleTypes);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ public abstract class InsnArg extends Typed {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
RegisterArg resArg = insn.getResult();
|
||||||
InsnArg arg = wrapInsnIntoArg(insn);
|
InsnArg arg = wrapInsnIntoArg(insn);
|
||||||
InsnArg oldArg = parent.getArg(i);
|
InsnArg oldArg = parent.getArg(i);
|
||||||
if (arg.getType() == ArgType.UNKNOWN) {
|
if (arg.getType() == ArgType.UNKNOWN) {
|
||||||
@@ -141,6 +142,8 @@ public abstract class InsnArg extends Typed {
|
|||||||
InsnRemover.unbindArgUsage(mth, oldArg);
|
InsnRemover.unbindArgUsage(mth, oldArg);
|
||||||
if (unbind) {
|
if (unbind) {
|
||||||
InsnRemover.unbindArgUsage(mth, this);
|
InsnRemover.unbindArgUsage(mth, this);
|
||||||
|
}
|
||||||
|
if (resArg != null && !insn.contains(AFlag.FORCE_ASSIGN_INLINE)) {
|
||||||
// result not needed in wrapped insn
|
// result not needed in wrapped insn
|
||||||
InsnRemover.unbindResult(mth, insn);
|
InsnRemover.unbindResult(mth, insn);
|
||||||
insn.setResult(null);
|
insn.setResult(null);
|
||||||
@@ -292,6 +295,17 @@ public abstract class InsnArg extends Typed {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isSameVar(SSAVar ssaVar) {
|
||||||
|
if (ssaVar == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isRegister()) {
|
||||||
|
SSAVar thisSsaVar = ((RegisterArg) this).getSVar();
|
||||||
|
return Objects.equals(thisSsaVar, ssaVar);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isSameCodeVar(RegisterArg arg) {
|
public boolean isSameCodeVar(RegisterArg arg) {
|
||||||
if (arg == null) {
|
if (arg == null) {
|
||||||
return false;
|
return false;
|
||||||
@@ -312,9 +326,7 @@ public abstract class InsnArg extends Typed {
|
|||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
public InsnArg duplicate() {
|
public abstract InsnArg duplicate();
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String toShortString() {
|
public String toShortString() {
|
||||||
return this.toString();
|
return this.toString();
|
||||||
|
|||||||
@@ -41,7 +41,14 @@ public final class InsnWrapArg extends InsnArg {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InsnArg duplicate() {
|
public InsnArg duplicate() {
|
||||||
InsnWrapArg copy = new InsnWrapArg(wrappedInsn.copyWithoutResult());
|
InsnNode wrapInsn = wrappedInsn;
|
||||||
|
InsnNode wrapInsnCopy = wrapInsn.copyWithoutResult();
|
||||||
|
if (wrapInsn.getResult() != null && wrapInsn.contains(AFlag.FORCE_ASSIGN_INLINE)) {
|
||||||
|
// keep same SSA var in result arg, this will break previous version, mark it for removal
|
||||||
|
wrapInsnCopy.setResult(wrapInsn.getResult().duplicate());
|
||||||
|
wrapInsn.add(AFlag.DONT_GENERATE);
|
||||||
|
}
|
||||||
|
InsnWrapArg copy = new InsnWrapArg(wrapInsnCopy);
|
||||||
copy.setType(type);
|
copy.setType(type);
|
||||||
return copyCommonParams(copy);
|
return copyCommonParams(copy);
|
||||||
}
|
}
|
||||||
@@ -84,7 +91,7 @@ public final class InsnWrapArg extends InsnArg {
|
|||||||
if (wrappedInsn.getType() == InsnType.CONST_STR) {
|
if (wrappedInsn.getType() == InsnType.CONST_STR) {
|
||||||
return "(\"" + ((ConstStringNode) wrappedInsn).getString() + "\")";
|
return "(\"" + ((ConstStringNode) wrappedInsn).getString() + "\")";
|
||||||
}
|
}
|
||||||
return "(wrap:" + type + ":" + wrappedInsn.getType() + ')';
|
return "(wrap " + type + ":" + wrappedInsn.getType() + ')';
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -92,6 +99,6 @@ public final class InsnWrapArg extends InsnArg {
|
|||||||
if (wrappedInsn.getType() == InsnType.CONST_STR) {
|
if (wrappedInsn.getType() == InsnType.CONST_STR) {
|
||||||
return "(\"" + ((ConstStringNode) wrappedInsn).getString() + "\")";
|
return "(\"" + ((ConstStringNode) wrappedInsn).getString() + "\")";
|
||||||
}
|
}
|
||||||
return "(wrap:" + type + ":" + wrappedInsn + ')';
|
return "(wrap " + type + ":" + wrappedInsn + ')';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -746,6 +746,14 @@ public class ClassNode extends NotificationAttrNode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void getInnerClassesRecursive(Set<ClassNode> resultClassesSet) {
|
||||||
|
for (ClassNode innerCls : innerClasses) {
|
||||||
|
if (resultClassesSet.add(innerCls)) {
|
||||||
|
innerCls.getInnerAndInlinedClassesRecursive(resultClassesSet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void addInnerClass(ClassNode cls) {
|
public void addInnerClass(ClassNode cls) {
|
||||||
if (innerClasses.isEmpty()) {
|
if (innerClasses.isEmpty()) {
|
||||||
innerClasses = new ArrayList<>(5);
|
innerClasses = new ArrayList<>(5);
|
||||||
|
|||||||
@@ -1,12 +1,24 @@
|
|||||||
package jadx.core.dex.nodes;
|
package jadx.core.dex.nodes;
|
||||||
|
|
||||||
public class Edge {
|
import jadx.core.dex.attributes.AFlag;
|
||||||
|
import jadx.core.dex.attributes.AttrNode;
|
||||||
|
|
||||||
|
public class Edge extends AttrNode {
|
||||||
private final BlockNode source;
|
private final BlockNode source;
|
||||||
private final BlockNode target;
|
private final BlockNode target;
|
||||||
|
|
||||||
public Edge(BlockNode source, BlockNode target) {
|
public Edge(BlockNode source, BlockNode target) {
|
||||||
|
this(source, target, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Edge(BlockNode source, BlockNode target, boolean isSynthetic) {
|
||||||
|
if (isSynthetic) {
|
||||||
|
this.add(AFlag.SYNTHETIC);
|
||||||
|
}
|
||||||
|
|
||||||
this.source = source;
|
this.source = source;
|
||||||
this.target = target;
|
this.target = target;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public BlockNode getSource() {
|
public BlockNode getSource() {
|
||||||
@@ -17,6 +29,10 @@ public class Edge {
|
|||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isSynthetic() {
|
||||||
|
return this.contains(AFlag.SYNTHETIC);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (this == o) {
|
if (this == o) {
|
||||||
|
|||||||
@@ -406,6 +406,14 @@ public class InsnNode extends LineAttrNode {
|
|||||||
&& Objects.equals(arguments, other.arguments);
|
&& Objects.equals(arguments, other.arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static <T extends InsnArg> @Nullable T duplicateArg(@Nullable T arg) {
|
||||||
|
if (arg == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (T) arg.duplicate();
|
||||||
|
}
|
||||||
|
|
||||||
protected final <T extends InsnNode> T copyCommonParams(T copy) {
|
protected final <T extends InsnNode> T copyCommonParams(T copy) {
|
||||||
if (copy.getArgsCount() == 0) {
|
if (copy.getArgsCount() == 0) {
|
||||||
for (InsnArg arg : this.getArguments()) {
|
for (InsnArg arg : this.getArguments()) {
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ package jadx.core.dex.nodes;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import org.jetbrains.annotations.ApiStatus;
|
import org.jetbrains.annotations.ApiStatus;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
@@ -71,7 +73,7 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
|
|||||||
// decompilation data, reset on unload
|
// decompilation data, reset on unload
|
||||||
private RegisterArg thisArg;
|
private RegisterArg thisArg;
|
||||||
private List<RegisterArg> argsList;
|
private List<RegisterArg> argsList;
|
||||||
private InsnNode[] instructions;
|
private @Nullable InsnNode[] instructions;
|
||||||
private List<BlockNode> blocks;
|
private List<BlockNode> blocks;
|
||||||
private int blocksMaxCId;
|
private int blocksMaxCId;
|
||||||
private BlockNode enterBlock;
|
private BlockNode enterBlock;
|
||||||
@@ -81,7 +83,14 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
|
|||||||
private List<LoopInfo> loops;
|
private List<LoopInfo> loops;
|
||||||
private Region region;
|
private Region region;
|
||||||
|
|
||||||
|
// Methods that use this method
|
||||||
private List<MethodNode> useIn = Collections.emptyList();
|
private List<MethodNode> useIn = Collections.emptyList();
|
||||||
|
// Unresolved methods that use this method
|
||||||
|
private List<MethodInfo> unresolvedUsed = Collections.emptyList();
|
||||||
|
// Methods that this method uses
|
||||||
|
private Set<MethodNode> methodsUsed = new HashSet<>();
|
||||||
|
// True if this method contains a self call
|
||||||
|
private boolean callsSelf = false;
|
||||||
|
|
||||||
private JavaMethod javaNode;
|
private JavaMethod javaNode;
|
||||||
|
|
||||||
@@ -96,11 +105,12 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
|
|||||||
this.parentClass = classNode;
|
this.parentClass = classNode;
|
||||||
this.accFlags = new AccessInfo(mthData.getAccessFlags(), AFType.METHOD);
|
this.accFlags = new AccessInfo(mthData.getAccessFlags(), AFType.METHOD);
|
||||||
ICodeReader codeReader = mthData.getCodeReader();
|
ICodeReader codeReader = mthData.getCodeReader();
|
||||||
this.noCode = codeReader == null;
|
if (codeReader == null) {
|
||||||
if (noCode) {
|
this.noCode = true;
|
||||||
this.codeReader = null;
|
this.codeReader = null;
|
||||||
this.insnsCount = 0;
|
this.insnsCount = 0;
|
||||||
} else {
|
} else {
|
||||||
|
this.noCode = false;
|
||||||
this.codeReader = codeReader.copy();
|
this.codeReader = codeReader.copy();
|
||||||
this.insnsCount = codeReader.getUnitsCount();
|
this.insnsCount = codeReader.getUnitsCount();
|
||||||
}
|
}
|
||||||
@@ -120,6 +130,7 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
|
|||||||
sVars = Collections.emptyList();
|
sVars = Collections.emptyList();
|
||||||
instructions = null;
|
instructions = null;
|
||||||
blocks = null;
|
blocks = null;
|
||||||
|
blocksMaxCId = 0;
|
||||||
enterBlock = null;
|
enterBlock = null;
|
||||||
exitBlock = null;
|
exitBlock = null;
|
||||||
region = null;
|
region = null;
|
||||||
@@ -702,12 +713,56 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
|
|||||||
return codeReader;
|
return codeReader;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public List<MethodNode> getUseIn() {
|
public List<MethodNode> getUseIn() {
|
||||||
return useIn;
|
return useIn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Do not modify passed list after setting
|
||||||
public void setUseIn(List<MethodNode> useIn) {
|
public void setUseIn(List<MethodNode> useIn) {
|
||||||
this.useIn = useIn;
|
this.useIn = useIn;
|
||||||
|
|
||||||
|
// Notify all methods (callers) this method (callee) is used in
|
||||||
|
for (MethodNode methodUsedIn : useIn) {
|
||||||
|
methodUsedIn.addUsed(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addUsed(MethodNode used) {
|
||||||
|
if (used != null) {
|
||||||
|
this.methodsUsed.add(used);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUsed(List<MethodNode> methodsUsed) {
|
||||||
|
this.methodsUsed = new HashSet<>(methodsUsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<MethodNode> getUsed() {
|
||||||
|
this.removeInvalidMethodsUsed();
|
||||||
|
return methodsUsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<MethodInfo> getUnresolvedUsed() {
|
||||||
|
return unresolvedUsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUnresolvedUsed(List<MethodInfo> unresolvedUsed) {
|
||||||
|
this.unresolvedUsed = unresolvedUsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCallsSelf(boolean callsSelf) {
|
||||||
|
this.callsSelf = callsSelf;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean callsSelf() {
|
||||||
|
return this.callsSelf;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any methods from the list of used methods (calees) if this method (caller) has been
|
||||||
|
// removed from the calee's list of callers
|
||||||
|
private void removeInvalidMethodsUsed() {
|
||||||
|
methodsUsed.removeIf(methodUsed -> !methodUsed.getUseIn().contains(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
public JavaMethod getJavaNode() {
|
public JavaMethod getJavaNode() {
|
||||||
|
|||||||
@@ -74,7 +74,9 @@ public final class SwitchRegion extends AbstractRegion implements IBranchRegion
|
|||||||
public List<IContainer> getSubBlocks() {
|
public List<IContainer> getSubBlocks() {
|
||||||
List<IContainer> all = new ArrayList<>(cases.size() + 1);
|
List<IContainer> all = new ArrayList<>(cases.size() + 1);
|
||||||
all.add(header);
|
all.add(header);
|
||||||
all.addAll(getCaseContainers());
|
for (CaseInfo caseInfo : cases) {
|
||||||
|
all.add(caseInfo.container);
|
||||||
|
}
|
||||||
return Collections.unmodifiableList(all);
|
return Collections.unmodifiableList(all);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import java.util.HashSet;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
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;
|
||||||
@@ -18,7 +20,7 @@ public final class IfInfo {
|
|||||||
private final BlockNode elseBlock;
|
private final BlockNode elseBlock;
|
||||||
private final Set<BlockNode> skipBlocks;
|
private final Set<BlockNode> skipBlocks;
|
||||||
private final List<InsnNode> forceInlineInsns;
|
private final List<InsnNode> forceInlineInsns;
|
||||||
private BlockNode outBlock;
|
private @Nullable 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, BlockSet.empty(mth), new HashSet<>(), new ArrayList<>());
|
this(mth, condition, thenBlock, elseBlock, BlockSet.empty(mth), new HashSet<>(), new ArrayList<>());
|
||||||
@@ -84,11 +86,11 @@ public final class IfInfo {
|
|||||||
return elseBlock;
|
return elseBlock;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BlockNode getOutBlock() {
|
public @Nullable BlockNode getOutBlock() {
|
||||||
return outBlock;
|
return outBlock;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setOutBlock(BlockNode outBlock) {
|
public void setOutBlock(@Nullable BlockNode outBlock) {
|
||||||
this.outBlock = outBlock;
|
this.outBlock = outBlock;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import org.jetbrains.annotations.Nullable;
|
|||||||
|
|
||||||
import jadx.api.ICodeWriter;
|
import jadx.api.ICodeWriter;
|
||||||
import jadx.core.codegen.RegionGen;
|
import jadx.core.codegen.RegionGen;
|
||||||
|
import jadx.core.dex.attributes.AFlag;
|
||||||
import jadx.core.dex.attributes.nodes.LoopInfo;
|
import jadx.core.dex.attributes.nodes.LoopInfo;
|
||||||
import jadx.core.dex.instructions.args.RegisterArg;
|
import jadx.core.dex.instructions.args.RegisterArg;
|
||||||
import jadx.core.dex.nodes.BlockNode;
|
import jadx.core.dex.nodes.BlockNode;
|
||||||
@@ -126,6 +127,7 @@ public final class LoopRegion extends ConditionRegion {
|
|||||||
preCondInsns.addAll(condInsns);
|
preCondInsns.addAll(condInsns);
|
||||||
condInsns.clear();
|
condInsns.clear();
|
||||||
condInsns.addAll(preCondInsns);
|
condInsns.addAll(preCondInsns);
|
||||||
|
header.add(AFlag.ALLOW_MULTIPLE_INSNS_LOOP_COND);
|
||||||
preCondInsns.clear();
|
preCondInsns.clear();
|
||||||
preCondition = null;
|
preCondition = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,26 +6,31 @@ import java.util.List;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import jadx.core.Consts;
|
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.info.ClassInfo;
|
import jadx.core.dex.info.ClassInfo;
|
||||||
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.InsnArg;
|
||||||
import jadx.core.dex.nodes.BlockNode;
|
import jadx.core.dex.nodes.BlockNode;
|
||||||
import jadx.core.dex.nodes.IContainer;
|
import jadx.core.dex.nodes.IRegion;
|
||||||
import jadx.core.dex.nodes.MethodNode;
|
import jadx.core.dex.nodes.MethodNode;
|
||||||
import jadx.core.utils.InsnUtils;
|
import jadx.core.utils.InsnUtils;
|
||||||
import jadx.core.utils.Utils;
|
import jadx.core.utils.Utils;
|
||||||
|
|
||||||
public class ExceptionHandler {
|
public class ExceptionHandler {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(ExceptionHandler.class);
|
||||||
|
|
||||||
private final List<ClassInfo> catchTypes = new ArrayList<>(1);
|
private final List<ClassInfo> catchTypes = new ArrayList<>(1);
|
||||||
private final int handlerOffset;
|
private final int handlerOffset;
|
||||||
|
|
||||||
private BlockNode handlerBlock;
|
private BlockNode handlerBlock;
|
||||||
private final List<BlockNode> blocks = new ArrayList<>();
|
private final List<BlockNode> blocks = new ArrayList<>();
|
||||||
private IContainer handlerRegion;
|
private IRegion handlerRegion;
|
||||||
private InsnArg arg;
|
private InsnArg arg;
|
||||||
|
|
||||||
private TryCatchBlockAttr tryBlock;
|
private TryCatchBlockAttr tryBlock;
|
||||||
@@ -117,11 +122,11 @@ public class ExceptionHandler {
|
|||||||
blocks.add(node);
|
blocks.add(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IContainer getHandlerRegion() {
|
public IRegion getHandlerRegion() {
|
||||||
return handlerRegion;
|
return handlerRegion;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setHandlerRegion(IContainer handlerRegion) {
|
public void setHandlerRegion(IRegion handlerRegion) {
|
||||||
this.handlerRegion = handlerRegion;
|
this.handlerRegion = handlerRegion;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,6 +158,42 @@ public class ExceptionHandler {
|
|||||||
return removed;
|
return removed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public BlockNode getBottomSplitter() {
|
||||||
|
TryCatchBlockAttr handlerTryBlock = getTryBlock();
|
||||||
|
// TODO: Implement support for finding bottom splitter of catch with inner tries
|
||||||
|
if (handlerTryBlock.getInnerTryBlocks().size() > 1) {
|
||||||
|
LOG.warn("No support yet for finding bottom block of try body with multipe inner trys");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
TryCatchBlockAttr searchForTryBody;
|
||||||
|
if (handlerTryBlock.getInnerTryBlocks().isEmpty()) {
|
||||||
|
searchForTryBody = handlerTryBlock;
|
||||||
|
} else {
|
||||||
|
searchForTryBody = Utils.getOne(handlerTryBlock.getInnerTryBlocks());
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockNode splitter = null;
|
||||||
|
for (BlockNode handlerPredecessor : getHandlerBlock().getPredecessors()) {
|
||||||
|
if (!handlerPredecessor.contains(AFlag.EXC_BOTTOM_SPLITTER)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (BlockNode splitterPredecessor : handlerPredecessor.getPredecessors()) {
|
||||||
|
TryCatchBlockAttr tryBody = splitterPredecessor.get(AType.TRY_BLOCK);
|
||||||
|
if (tryBody == searchForTryBody) {
|
||||||
|
splitter = handlerPredecessor;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (splitter != null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return splitter;
|
||||||
|
}
|
||||||
|
|
||||||
public void markForRemove() {
|
public void markForRemove() {
|
||||||
this.removed = true;
|
this.removed = true;
|
||||||
this.blocks.forEach(b -> b.add(AFlag.REMOVE));
|
this.blocks.forEach(b -> b.add(AFlag.REMOVE));
|
||||||
|
|||||||
@@ -2,17 +2,34 @@ package jadx.core.dex.trycatch;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import jadx.api.plugins.input.data.attributes.IJadxAttrType;
|
import jadx.api.plugins.input.data.attributes.IJadxAttrType;
|
||||||
import jadx.api.plugins.input.data.attributes.IJadxAttribute;
|
import jadx.api.plugins.input.data.attributes.IJadxAttribute;
|
||||||
|
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.nodes.BlockNode;
|
import jadx.core.dex.nodes.BlockNode;
|
||||||
|
import jadx.core.dex.nodes.Edge;
|
||||||
import jadx.core.dex.nodes.InsnNode;
|
import jadx.core.dex.nodes.InsnNode;
|
||||||
|
import jadx.core.dex.nodes.MethodNode;
|
||||||
|
import jadx.core.utils.BlockUtils;
|
||||||
|
import jadx.core.utils.ListUtils;
|
||||||
import jadx.core.utils.Utils;
|
import jadx.core.utils.Utils;
|
||||||
|
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||||
|
|
||||||
public class TryCatchBlockAttr implements IJadxAttribute {
|
public class TryCatchBlockAttr implements IJadxAttribute {
|
||||||
|
|
||||||
|
public static boolean isImplicitOrMerged(TryCatchBlockAttr tryBlock) {
|
||||||
|
return tryBlock.isMerged() || tryBlock.getHandlers().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
private final int id;
|
private final int id;
|
||||||
private final List<ExceptionHandler> handlers;
|
private final List<ExceptionHandler> handlers;
|
||||||
private List<BlockNode> blocks;
|
private List<BlockNode> blocks;
|
||||||
@@ -134,6 +151,242 @@ public class TryCatchBlockAttr implements IJadxAttribute {
|
|||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<TryEdge> getHandlerTryEdges() {
|
||||||
|
List<ExceptionHandler> mergedHandlers = getMergedHandlers();
|
||||||
|
List<TryEdge> edges = new ArrayList<>(mergedHandlers.size());
|
||||||
|
for (ExceptionHandler handler : mergedHandlers) {
|
||||||
|
BlockNode handlerBlock = handler.getHandlerBlock();
|
||||||
|
BlockNode handlerSplitter = handler.getBottomSplitter();
|
||||||
|
if (handlerSplitter == null) {
|
||||||
|
// If we cannot find a bottom splitter, there might be none. In this case, assume that the top
|
||||||
|
// splitter of this try catch is the source of the exit.
|
||||||
|
List<BlockNode> allChildren = ListUtils.filter(handlerBlock.getPredecessors(), blk -> getBlocks().contains(blk));
|
||||||
|
handlerSplitter = BlockUtils.getBottomBlock(allChildren);
|
||||||
|
if (handlerSplitter == null) {
|
||||||
|
handlerSplitter = getTopSplitter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TryEdge edge = new TryEdge(handlerSplitter, handlerBlock, handler);
|
||||||
|
edges.add(edge);
|
||||||
|
}
|
||||||
|
return edges;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<TryEdge> getFallthroughTryEdges() {
|
||||||
|
List<TryEdge> edges = new LinkedList<>();
|
||||||
|
List<BlockNode> exploredBlocks = new ArrayList<>();
|
||||||
|
List<TryCatchBlockAttr> exploredTrys = new LinkedList<>();
|
||||||
|
|
||||||
|
getFallthroughTryEdges(edges, exploredBlocks, exploredTrys);
|
||||||
|
return edges;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void getFallthroughTryEdges(List<TryEdge> edges, List<BlockNode> exploredBlocks, List<TryCatchBlockAttr> exploredTrys) {
|
||||||
|
List<ExceptionHandler> mergedHandlers = getMergedHandlers();
|
||||||
|
Set<BlockNode> searchBlocks = new HashSet<>(getBlocks());
|
||||||
|
for (ExceptionHandler handler : mergedHandlers) {
|
||||||
|
handler.getBlocks().forEach(searchBlocks::remove);
|
||||||
|
}
|
||||||
|
BlockNode sourceBlock = BlockUtils.getTopBlock(new ArrayList<>(searchBlocks));
|
||||||
|
if (sourceBlock != null) {
|
||||||
|
exploredTrys.add(this);
|
||||||
|
exploreTryPath(edges, sourceBlock, searchBlocks, exploredBlocks, exploredTrys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<TryEdge> getTryEdges() {
|
||||||
|
List<TryEdge> handlerEdges = getHandlerTryEdges();
|
||||||
|
List<TryEdge> fallthroughEdges = getFallthroughTryEdges();
|
||||||
|
List<TryEdge> edges = new ArrayList<>(handlerEdges.size() + fallthroughEdges.size());
|
||||||
|
edges.addAll(handlerEdges);
|
||||||
|
edges.addAll(fallthroughEdges);
|
||||||
|
return Collections.unmodifiableList(edges);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void exploreTryPath(List<TryEdge> edges, BlockNode blk, Set<BlockNode> searchBlocks, List<BlockNode> exploredBlocks,
|
||||||
|
List<TryCatchBlockAttr> exploredTrys) {
|
||||||
|
for (BlockNode successor : blk.getSuccessors()) {
|
||||||
|
// If a separate branch has already explored this block, we don't need to recalculate its exits.
|
||||||
|
if (exploredBlocks.contains(successor)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is a bottom splitter, ignore - we only care about non-handler edges.
|
||||||
|
if (successor.contains(AFlag.EXC_BOTTOM_SPLITTER)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
exploredBlocks.add(successor);
|
||||||
|
|
||||||
|
if (successor.contains(AFlag.LOOP_END)) {
|
||||||
|
var loopsAttrList = successor.get(AType.LOOP);
|
||||||
|
List<LoopInfo> loops = loopsAttrList.getList();
|
||||||
|
List<BlockNode> loopStartBlocks = new LinkedList<>();
|
||||||
|
for (LoopInfo loop : loops) {
|
||||||
|
loopStartBlocks.add(loop.getStart());
|
||||||
|
List<Edge> loopEdges = loop.getExitEdges();
|
||||||
|
for (Edge loopEdge : loopEdges) {
|
||||||
|
if (loopEdge.getTarget() == successor) {
|
||||||
|
loopStartBlocks.add(loopEdge.getSource());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
boolean includesAllLoopStart = ListUtils.allMatch(loopStartBlocks, exploredBlocks::contains);
|
||||||
|
if (!includesAllLoopStart) {
|
||||||
|
edges.add(new TryEdge(blk, successor, TryEdgeType.LOOP_EXIT));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isPathToAnySearchBlock = false;
|
||||||
|
for (BlockNode searchBlock : searchBlocks) {
|
||||||
|
if (BlockUtils.isPathExists(successor, searchBlock)) {
|
||||||
|
isPathToAnySearchBlock = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!searchBlocks.contains(successor) && !isPathToAnySearchBlock) {
|
||||||
|
// This block is not contained within this try's block list. This can either be since it is an exit
|
||||||
|
// to the try or it is a block which leads to an exit (for example, an exception handler).
|
||||||
|
|
||||||
|
// If this block (successor) leads to an exit, then the "bottom block" of all try blocks and this
|
||||||
|
// block will be
|
||||||
|
// equal to the bottom block of all try blocks. If this block is an exit, then either:
|
||||||
|
// - a path does not exist from all try blocks to this block, thus making the bottom block null.
|
||||||
|
// - a path does exist from all try blocks to this block but no more try blocks follow, thus making
|
||||||
|
// the bottom block this block.
|
||||||
|
List<BlockNode> allBlocksWithCurrent = new ArrayList<>(getBlocks().size() + 1);
|
||||||
|
allBlocksWithCurrent.addAll(getBlocks());
|
||||||
|
allBlocksWithCurrent.add(successor);
|
||||||
|
BlockNode bottomBlock = BlockUtils.getBottomBlock(allBlocksWithCurrent);
|
||||||
|
|
||||||
|
if (!(bottomBlock == null || bottomBlock == successor)) {
|
||||||
|
// This block leads to an exit.
|
||||||
|
exploreTryPath(edges, successor, searchBlocks, exploredBlocks, exploredTrys);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockNode emptyPathEndOfSuccessor = BlockUtils.followEmptyPath(successor, false, false);
|
||||||
|
|
||||||
|
if (emptyPathEndOfSuccessor.contains(AFlag.EXC_TOP_SPLITTER)) {
|
||||||
|
// This block is an exit which enters another try catch. In this case, the next try catch is within
|
||||||
|
// the same scope. Thus, we will take all of the edges out of that try and add them to the list of
|
||||||
|
// edges of this try.
|
||||||
|
Set<TryCatchBlockAttr> nestedTrys = new HashSet<>();
|
||||||
|
List<BlockNode> allSuccessorsOnTryBody = ListUtils.filter(emptyPathEndOfSuccessor.getSuccessors(),
|
||||||
|
potentialTryBlock -> potentialTryBlock.contains(AFlag.TRY_ENTER));
|
||||||
|
for (BlockNode tryBodyEnter : allSuccessorsOnTryBody) {
|
||||||
|
TryCatchBlockAttr nestedTry = tryBodyEnter.get(AType.TRY_BLOCK);
|
||||||
|
if (nestedTry == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have already added a try's edges, skip over it to avoid infinite recursion.
|
||||||
|
if (exploredTrys.contains(nestedTry)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsure of why these top splitters have to be the same for them to be "nested" trys, but this
|
||||||
|
// seems to work (?)
|
||||||
|
if (nestedTry.getTopSplitter() != getTopSplitter()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
nestedTrys.add(nestedTry);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only will we attempt to add nested inners if there exists any. If none exist, perform normal
|
||||||
|
// handling of the edge.
|
||||||
|
if (!nestedTrys.isEmpty()) {
|
||||||
|
for (TryCatchBlockAttr nestedTry : nestedTrys) {
|
||||||
|
nestedTry.getFallthroughTryEdges(edges, exploredBlocks, exploredTrys);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bottomBlock == null) {
|
||||||
|
// This block is an exit which occurs before all try blocks are logically executed.
|
||||||
|
edges.add(new TryEdge(blk, successor, TryEdgeType.PREMATURE_EXIT));
|
||||||
|
} else if (bottomBlock == successor) {
|
||||||
|
// This block is an exit which occurs after all try blocks are logically executed.
|
||||||
|
edges.add(new TryEdge(blk, successor, TryEdgeType.TRUE_FALLTHROUGH));
|
||||||
|
} else {
|
||||||
|
// All possible cases should have been caught by the above if / else and the preceeding if.
|
||||||
|
// If this is hit, any changes made to this algorithm must aptly handle all possible code paths
|
||||||
|
// before executing this.
|
||||||
|
throw new JadxRuntimeException(
|
||||||
|
"Unexpected code execution branch taken during try edge resolution: blk="
|
||||||
|
+ blk + ",successor=" + successor);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
exploreTryPath(edges, successor, searchBlocks, exploredBlocks, exploredTrys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ExceptionHandler> getMergedHandlers() {
|
||||||
|
boolean hasInnerBlocks = !getInnerTryBlocks().isEmpty();
|
||||||
|
List<ExceptionHandler> mergedHandlers;
|
||||||
|
if (hasInnerBlocks) {
|
||||||
|
// collect handlers from this and all inner blocks
|
||||||
|
// (intentionally not using recursive collect for now)
|
||||||
|
mergedHandlers = new ArrayList<>(getHandlers());
|
||||||
|
for (TryCatchBlockAttr innerTryBlock : getInnerTryBlocks()) {
|
||||||
|
mergedHandlers.addAll(innerTryBlock.getHandlers());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mergedHandlers = getHandlers();
|
||||||
|
}
|
||||||
|
return Collections.unmodifiableList(mergedHandlers);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<TryEdge, BlockNode> getEdgeBlockMap() {
|
||||||
|
List<TryEdge> edges = getTryEdges();
|
||||||
|
Map<TryEdge, BlockNode> blockMap = new HashMap<>();
|
||||||
|
for (TryEdge edge : edges) {
|
||||||
|
blockMap.put(edge, edge.getTarget());
|
||||||
|
}
|
||||||
|
return blockMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TryEdgeScopeGroupMap getExecutionScopeGroups(MethodNode mth) {
|
||||||
|
Map<TryEdge, BlockNode> handlerBlocks = getEdgeBlockMap();
|
||||||
|
TryEdgeScopeGroupMap scopeGroups = new TryEdgeScopeGroupMap(mth, this, handlerBlocks.size());
|
||||||
|
scopeGroups.populateFromEdges(handlerBlocks);
|
||||||
|
|
||||||
|
return scopeGroups;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<BlockNode, List<TryEdge>> getHandlerFallthroughGroups(MethodNode mth, TryEdgeScopeGroupMap scopeGroups) {
|
||||||
|
return scopeGroups.getScopeEnds(mth);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<BlockNode> getSearchBlocksFromFallthroughGroups(MethodNode mth, ExceptionHandler finallyHandler,
|
||||||
|
Map<BlockNode, List<TryEdge>> fallthroughGroups) {
|
||||||
|
|
||||||
|
List<BlockNode> searchBlocks = new LinkedList<>();
|
||||||
|
for (Map.Entry<BlockNode, List<TryEdge>> entry : fallthroughGroups.entrySet()) {
|
||||||
|
BlockNode scopeEndBlock = entry.getKey();
|
||||||
|
List<TryEdge> sourceHandlers = entry.getValue();
|
||||||
|
|
||||||
|
for (BlockNode scopeEndPredecessor : scopeEndBlock.getPredecessors()) {
|
||||||
|
// Add all predecessors to the scope end which are connected to some handler's scope start
|
||||||
|
try (Stream<TryEdge> stream = sourceHandlers.stream()) {
|
||||||
|
Object[] matchedHandlerPaths =
|
||||||
|
stream.filter(handler -> !(handler.isHandlerExit() && handler.getExceptionHandler() == finallyHandler))
|
||||||
|
.map(handler -> handler.getTarget())
|
||||||
|
.filter(scopeStart -> BlockUtils.isPathExists(scopeStart, scopeEndPredecessor))
|
||||||
|
.toArray();
|
||||||
|
if (matchedHandlerPaths.length != 0) {
|
||||||
|
searchBlocks.add(scopeEndPredecessor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return searchBlocks;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IJadxAttrType<? extends IJadxAttribute> getAttrType() {
|
public IJadxAttrType<? extends IJadxAttribute> getAttrType() {
|
||||||
return AType.TRY_BLOCK;
|
return AType.TRY_BLOCK;
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
package jadx.core.dex.trycatch;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import jadx.core.dex.nodes.BlockNode;
|
||||||
|
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an edge between two blocks representing an exit out of a try body.
|
||||||
|
* The source block will be within the try body.
|
||||||
|
*/
|
||||||
|
public final class TryEdge {
|
||||||
|
|
||||||
|
private final BlockNode source;
|
||||||
|
private final BlockNode target;
|
||||||
|
private final Optional<ExceptionHandler> handler;
|
||||||
|
private final TryEdgeType type;
|
||||||
|
|
||||||
|
public TryEdge(BlockNode source, BlockNode target, TryEdgeType type) {
|
||||||
|
this(source, target, type, Optional.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
public TryEdge(BlockNode source, BlockNode target, @NotNull ExceptionHandler handler) {
|
||||||
|
this(source, target, TryEdgeType.HANDLER, Optional.of(handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
public TryEdge(BlockNode source, BlockNode target, TryEdgeType type, Optional<ExceptionHandler> handler) {
|
||||||
|
this.source = source;
|
||||||
|
this.target = target;
|
||||||
|
this.handler = handler;
|
||||||
|
this.type = type;
|
||||||
|
|
||||||
|
if (isHandlerExit() && handler.isEmpty()) {
|
||||||
|
throw new JadxRuntimeException("Attempted to add a null exception handler as an edge of \"" + type.toString() + "\" type");
|
||||||
|
} else if (isNotHandlerExit() && handler.isPresent()) {
|
||||||
|
throw new JadxRuntimeException("Attempted to add an exception handler as an edge of \"" + type.toString() + "\" type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder sb = new StringBuilder("TryEdge: [");
|
||||||
|
sb.append(type);
|
||||||
|
sb.append(' ');
|
||||||
|
sb.append(source.toString());
|
||||||
|
sb.append(" -> ");
|
||||||
|
sb.append(target.toString());
|
||||||
|
sb.append("] - Handler: ");
|
||||||
|
if (handler.isEmpty()) {
|
||||||
|
sb.append("None");
|
||||||
|
} else {
|
||||||
|
sb.append(handler.get().toString());
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (!(obj instanceof TryEdge)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TryEdge other = (TryEdge) obj;
|
||||||
|
|
||||||
|
return source.equals(other.source)
|
||||||
|
&& target.equals(other.target)
|
||||||
|
&& handler.equals(other.handler)
|
||||||
|
&& type.equals(other.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(source, target, type, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BlockNode getSource() {
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BlockNode getTarget() {
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TryEdgeType getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isHandlerExit() {
|
||||||
|
return type == TryEdgeType.HANDLER;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isNotHandlerExit() {
|
||||||
|
return !isHandlerExit();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExceptionHandler getExceptionHandler() {
|
||||||
|
if (!isHandlerExit()) {
|
||||||
|
throw new JadxRuntimeException("Attempted to get the exception handler of a non-handler edge type");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handler.isEmpty()) {
|
||||||
|
throw new JadxRuntimeException("Attempted to get the exception handler of a handler edge type, however none was present");
|
||||||
|
}
|
||||||
|
|
||||||
|
return handler.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,374 @@
|
|||||||
|
package jadx.core.dex.trycatch;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.BitSet;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import jadx.core.dex.nodes.BlockNode;
|
||||||
|
import jadx.core.dex.nodes.MethodNode;
|
||||||
|
import jadx.core.utils.BlockUtils;
|
||||||
|
import jadx.core.utils.Pair;
|
||||||
|
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map which stores the information of how try edges correlate with each other.
|
||||||
|
* K is a try edge and V contains all other try edges whose who share the same logical scope.
|
||||||
|
*/
|
||||||
|
public final class TryEdgeScopeGroupMap implements Map<TryEdge, Map<TryEdge, BlockNode>> {
|
||||||
|
|
||||||
|
private static final class TryEdgeScope {
|
||||||
|
|
||||||
|
private final TryEdge edge;
|
||||||
|
private final BlockNode block;
|
||||||
|
|
||||||
|
public TryEdgeScope(TryEdge edge, BlockNode block) {
|
||||||
|
this.edge = edge;
|
||||||
|
this.block = block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final List<Pair<TryEdge>> mergedEdges = new ArrayList<>();
|
||||||
|
private final TryCatchBlockAttr tryCatch;
|
||||||
|
private final Map<TryEdge, Map<TryEdge, BlockNode>> underlyingMap;
|
||||||
|
|
||||||
|
public TryEdgeScopeGroupMap(MethodNode mth, TryCatchBlockAttr tryCatch, int initialCapacity) {
|
||||||
|
this.tryCatch = tryCatch;
|
||||||
|
underlyingMap = new HashMap<>(initialCapacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clear() {
|
||||||
|
underlyingMap.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean containsKey(Object key) {
|
||||||
|
return underlyingMap.containsKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean containsValue(Object value) {
|
||||||
|
if (!(value instanceof TryEdge)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TryEdge edge = (TryEdge) value;
|
||||||
|
return underlyingMap.containsKey(edge);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<Entry<TryEdge, Map<TryEdge, BlockNode>>> entrySet() {
|
||||||
|
return underlyingMap.entrySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<TryEdge, BlockNode> get(Object key) {
|
||||||
|
return underlyingMap.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return underlyingMap.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<TryEdge> keySet() {
|
||||||
|
return underlyingMap.keySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<TryEdge, BlockNode> put(TryEdge key, Map<TryEdge, BlockNode> value) {
|
||||||
|
return underlyingMap.put(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void putAll(Map<? extends TryEdge, ? extends Map<TryEdge, BlockNode>> otherMap) {
|
||||||
|
underlyingMap.putAll(otherMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<TryEdge, BlockNode> remove(Object key) {
|
||||||
|
return underlyingMap.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int size() {
|
||||||
|
return underlyingMap.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<Map<TryEdge, BlockNode>> values() {
|
||||||
|
return underlyingMap.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasMergedEdges() {
|
||||||
|
return !mergedEdges.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Pair<TryEdge>> getMergedScopes() {
|
||||||
|
return mergedEdges;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void populateFromEdges(Map<TryEdge, BlockNode> edges) {
|
||||||
|
mergeSameScopes(edges);
|
||||||
|
|
||||||
|
for (TryEdge edge : edges.keySet()) {
|
||||||
|
BlockNode edgeBlock = edges.get(edge);
|
||||||
|
|
||||||
|
Map<TryEdge, BlockNode> handlerFallthroughMap = createEdgeTerminusMap(edges, edge, edgeBlock);
|
||||||
|
put(edge, handlerFallthroughMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a map of all points where edges meet with each other, dictating the end of that
|
||||||
|
* edge's scope.
|
||||||
|
*/
|
||||||
|
public Map<BlockNode, List<TryEdge>> getScopeEnds(MethodNode mth) {
|
||||||
|
Map<BlockNode, List<TryEdge>> groups = new HashMap<>();
|
||||||
|
|
||||||
|
// A list containing pairs of edges where there are no shared common clean successors between the
|
||||||
|
// two handlers. This usually indicates that these edge pairs must be processed differently.
|
||||||
|
List<TryEdge> isolatedEdgePairs = new LinkedList<>();
|
||||||
|
|
||||||
|
for (TryEdge mergeEdgeA : keySet()) {
|
||||||
|
Pair<TryEdge> edgeMergedPair = getMergedNodeFromEdge(mergeEdgeA);
|
||||||
|
|
||||||
|
if (edgeMergedPair != null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<TryEdge, BlockNode> handlerRelations = get(mergeEdgeA);
|
||||||
|
|
||||||
|
List<BlockNode> scopeEnds = new ArrayList<>(handlerRelations.size());
|
||||||
|
for (TryEdge mergeEdgeB : handlerRelations.keySet()) {
|
||||||
|
Pair<TryEdge> mergedPairFromRelation = getMergedNodeFromEdge(mergeEdgeB);
|
||||||
|
if (mergedPairFromRelation != null && mergedPairFromRelation.getFirst() == mergeEdgeA) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockNode sharedTerminator = handlerRelations.get(mergeEdgeB);
|
||||||
|
|
||||||
|
if (sharedTerminator == null) {
|
||||||
|
// There are no common clean succesors between the two handlers.
|
||||||
|
isolatedEdgePairs.add(mergeEdgeB);
|
||||||
|
} else {
|
||||||
|
scopeEnds.add(sharedTerminator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scopeEnds.isEmpty()) {
|
||||||
|
// Isolated edge pairs found - we will deal with them later
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockNode topGrouping = BlockUtils.getTopBlock(scopeEnds);
|
||||||
|
|
||||||
|
if (groups.containsKey(topGrouping)) {
|
||||||
|
groups.get(topGrouping).add(mergeEdgeA);
|
||||||
|
} else {
|
||||||
|
List<TryEdge> groupingHandlers = new LinkedList<>();
|
||||||
|
groupingHandlers.add(mergeEdgeA);
|
||||||
|
groups.put(topGrouping, groupingHandlers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (TryEdge isolatedEdge : isolatedEdgePairs) {
|
||||||
|
boolean isInList = false;
|
||||||
|
for (List<TryEdge> foundEdges : groups.values()) {
|
||||||
|
if (foundEdges.contains(isolatedEdge)) {
|
||||||
|
isInList = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInList) {
|
||||||
|
// The isolated edge is not isolated with another handler - we can ignore this edge.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If an isolated edge has not been added to the groupings, we will add it now.
|
||||||
|
// This will be added by locating the point where the search for a common successor stops.
|
||||||
|
// Since a common successor of all blocks which do have some clean path can be found in the method
|
||||||
|
// exit node, the mentioned point will be the farthest successor of the edge target which has no
|
||||||
|
// clean successors.
|
||||||
|
|
||||||
|
BlockNode target = isolatedEdge.getTarget();
|
||||||
|
List<BlockNode> successorBlocks = BlockUtils.collectAllSuccessors(mth, target, true);
|
||||||
|
BlockNode cleanSuccessorEnd = BlockUtils.getBottomBlock(successorBlocks);
|
||||||
|
if (cleanSuccessorEnd == null) {
|
||||||
|
throw new JadxRuntimeException("Could not find bottom clean successor for isolated try edge");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<TryEdge> scopeTerminusList;
|
||||||
|
if (groups.containsKey(cleanSuccessorEnd)) {
|
||||||
|
scopeTerminusList = groups.get(cleanSuccessorEnd);
|
||||||
|
} else {
|
||||||
|
scopeTerminusList = new LinkedList<>();
|
||||||
|
groups.put(cleanSuccessorEnd, scopeTerminusList);
|
||||||
|
}
|
||||||
|
scopeTerminusList.add(isolatedEdge);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groups.size() == 1) {
|
||||||
|
for (Pair<TryEdge> pair : mergedEdges) {
|
||||||
|
TryEdge keptEdge = pair.getFirst();
|
||||||
|
TryEdge removedEdge = pair.getSecond();
|
||||||
|
|
||||||
|
if (keptEdge.isHandlerExit() && !tryCatch.getHandlers().contains(keptEdge.getExceptionHandler())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (removedEdge.isHandlerExit() && !tryCatch.getHandlers().contains(removedEdge.getExceptionHandler())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If both handlers are not handler exits, we can assume that the code paths merge at some Phi node
|
||||||
|
// which begins the finally duplicated code.
|
||||||
|
if (keptEdge.isNotHandlerExit() && removedEdge.isNotHandlerExit()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (List<TryEdge> edgesWithTerminus : groups.values()) {
|
||||||
|
if (edgesWithTerminus.contains(keptEdge)) {
|
||||||
|
edgesWithTerminus.remove(keptEdge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockNode terminus = get(keptEdge).get(removedEdge);
|
||||||
|
List<TryEdge> terminusEdges;
|
||||||
|
if (!groups.containsKey(terminus)) {
|
||||||
|
terminusEdges = new LinkedList<>();
|
||||||
|
terminusEdges.add(keptEdge);
|
||||||
|
groups.put(terminus, terminusEdges);
|
||||||
|
} else {
|
||||||
|
terminusEdges = groups.get(terminus);
|
||||||
|
}
|
||||||
|
terminusEdges.add(removedEdge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private Pair<TryEdge> getMergedNodeFromEdge(TryEdge edge) {
|
||||||
|
for (Pair<TryEdge> pair : mergedEdges) {
|
||||||
|
if (pair.getSecond() == edge) {
|
||||||
|
return pair;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<TryEdge, BlockNode> createEdgeTerminusMap(Map<TryEdge, BlockNode> edgeStartMap, TryEdge edge,
|
||||||
|
BlockNode edgeStart) {
|
||||||
|
Map<TryEdge, BlockNode> scopeRelations = new HashMap<>(edgeStartMap.size() - 1);
|
||||||
|
for (TryEdge otherEdge : edgeStartMap.keySet()) {
|
||||||
|
if (edge == otherEdge) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockNode otherEdgeStart = edgeStartMap.get(otherEdge);
|
||||||
|
|
||||||
|
boolean eitherEdgeIsHandler = edge.isHandlerExit() || otherEdge.isHandlerExit();
|
||||||
|
if (otherEdgeStart == edgeStart && eitherEdgeIsHandler) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (otherEdgeStart.isMthExitBlock()) {
|
||||||
|
scopeRelations.put(otherEdge, otherEdgeStart);
|
||||||
|
// Everything leads to the exit node so merged edges are no longer needed
|
||||||
|
mergedEdges.clear();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (edgeStart.isMthExitBlock()) {
|
||||||
|
scopeRelations.put(otherEdge, edgeStart);
|
||||||
|
// Everything leads to the exit node so merged edges are no longer needed
|
||||||
|
mergedEdges.clear();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
BitSet sharedPostDominators = (BitSet) edgeStart.getPostDoms().clone();
|
||||||
|
BitSet otherPostDoms = otherEdgeStart.getPostDoms();
|
||||||
|
if (sharedPostDominators.isEmpty() || otherPostDoms.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
sharedPostDominators.and(otherPostDoms);
|
||||||
|
|
||||||
|
List<BlockNode> postDomHandler = new LinkedList<>();
|
||||||
|
BlockNode currentBlock = edgeStart;
|
||||||
|
while (currentBlock != null) {
|
||||||
|
postDomHandler.add(currentBlock);
|
||||||
|
currentBlock = currentBlock.getIPostDom();
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockNode commonPostDom = null;
|
||||||
|
currentBlock = otherEdgeStart;
|
||||||
|
while (currentBlock != null) {
|
||||||
|
if (postDomHandler.contains(currentBlock)) {
|
||||||
|
commonPostDom = currentBlock;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
currentBlock = currentBlock.getIPostDom();
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockNode scopeEnd = commonPostDom;
|
||||||
|
scopeRelations.put(otherEdge, scopeEnd);
|
||||||
|
}
|
||||||
|
return scopeRelations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If two scopes ever merge, as in if one edge leads to the same execution point as the target of
|
||||||
|
* another edge, this function will record it.
|
||||||
|
*
|
||||||
|
* @param handlers
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private Map<TryEdge, BlockNode> mergeSameScopes(Map<TryEdge, BlockNode> handlers) {
|
||||||
|
List<Entry<TryEdge, BlockNode>> exceptionHandlers = new ArrayList<>(handlers.entrySet());
|
||||||
|
|
||||||
|
List<Pair<TryEdgeScope>> handlerPairs = new LinkedList<>();
|
||||||
|
for (int i = 0; i < exceptionHandlers.size(); i++) {
|
||||||
|
for (int j = i + 1; j < exceptionHandlers.size(); j++) {
|
||||||
|
TryEdgeScope a = new TryEdgeScope(exceptionHandlers.get(i).getKey(), exceptionHandlers.get(i).getValue());
|
||||||
|
TryEdgeScope b = new TryEdgeScope(exceptionHandlers.get(j).getKey(), exceptionHandlers.get(j).getValue());
|
||||||
|
handlerPairs.add(new Pair<>(a, b));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<TryEdge, BlockNode> simplifiedScopes = new HashMap<>(handlers);
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
while (i < handlerPairs.size()) {
|
||||||
|
Pair<TryEdgeScope> handlerPair = handlerPairs.get(i);
|
||||||
|
|
||||||
|
TryEdgeScope edgeScopeA = handlerPair.getFirst();
|
||||||
|
TryEdgeScope edgeScopeB = handlerPair.getSecond();
|
||||||
|
BlockNode edgeBlockA = edgeScopeA.block;
|
||||||
|
BlockNode edgeBlockB = edgeScopeB.block;
|
||||||
|
boolean pathExists = BlockUtils.isPathExists(edgeBlockA, edgeBlockB) || BlockUtils.isPathExists(edgeBlockB, edgeBlockA);
|
||||||
|
if (pathExists) {
|
||||||
|
BlockNode bottomBlock = BlockUtils.getBottomBlock(List.of(edgeBlockA, edgeBlockB));
|
||||||
|
// The two blocks are within the same scope - remove these from the matrix
|
||||||
|
TryEdge removeHandler = edgeBlockA != bottomBlock ? edgeScopeA.edge : edgeScopeB.edge;
|
||||||
|
TryEdge keepHandler = edgeBlockA == bottomBlock ? edgeScopeA.edge : edgeScopeB.edge;
|
||||||
|
simplifiedScopes.remove(removeHandler);
|
||||||
|
handlerPairs.remove(i);
|
||||||
|
|
||||||
|
mergedEdges.add(new Pair<>(keepHandler, removeHandler));
|
||||||
|
} else {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return simplifiedScopes;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package jadx.core.dex.trycatch;
|
||||||
|
|
||||||
|
public enum TryEdgeType {
|
||||||
|
TRUE_FALLTHROUGH,
|
||||||
|
PREMATURE_EXIT,
|
||||||
|
LOOP_EXIT,
|
||||||
|
HANDLER
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package jadx.core.dex.visitors;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import jadx.core.dex.attributes.AFlag;
|
||||||
|
import jadx.core.dex.attributes.AType;
|
||||||
|
import jadx.core.dex.attributes.nodes.SpecialEdgeAttr;
|
||||||
|
import jadx.core.dex.attributes.nodes.SpecialEdgeAttr.SpecialEdgeType;
|
||||||
|
import jadx.core.dex.instructions.InsnType;
|
||||||
|
import jadx.core.dex.instructions.args.RegisterArg;
|
||||||
|
import jadx.core.dex.nodes.BlockNode;
|
||||||
|
import jadx.core.dex.nodes.InsnNode;
|
||||||
|
import jadx.core.dex.nodes.MethodNode;
|
||||||
|
import jadx.core.dex.visitors.regions.RegionMakerVisitor;
|
||||||
|
import jadx.core.dex.visitors.typeinference.FinishTypeInference;
|
||||||
|
import jadx.core.utils.BlockUtils;
|
||||||
|
|
||||||
|
@JadxVisitor(
|
||||||
|
name = "AdjustForIfMergeVisitor",
|
||||||
|
desc = "Move instructions between if blocks that can't be inlined but are safe to push through the if to allow the ifs to merge",
|
||||||
|
runBefore = { RegionMakerVisitor.class },
|
||||||
|
runAfter = { FinishTypeInference.class }
|
||||||
|
)
|
||||||
|
public class AdjustForIfMergeVisitor extends AbstractVisitor {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(MethodNode mth) {
|
||||||
|
if (mth.isNoCode() || mth.getBasicBlocks() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Find candidates for adjustment by selecting blocks between two if statements
|
||||||
|
List<BlockNode> blocks = mth.getBasicBlocks();
|
||||||
|
|
||||||
|
for (BlockNode blk : blocks) {
|
||||||
|
if (areSurroundingsCorrectShape(blk)) {
|
||||||
|
BlockNode pred = blk.getPredecessors().get(0);
|
||||||
|
BlockNode succ = blk.getCleanSuccessors().get(0);
|
||||||
|
|
||||||
|
if (isSimpleIf(pred) && isSimpleIf(succ)) {
|
||||||
|
List<InsnNode> movableInstructions = getMovableInstructions(blk, succ);
|
||||||
|
|
||||||
|
if (!movableInstructions.isEmpty() && couldMerge(mth, pred, blk, succ)) {
|
||||||
|
doMove(mth, blk, succ, movableInstructions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean areSurroundingsCorrectShape(BlockNode blk) {
|
||||||
|
return (blk.getPredecessors().size() == 1 && blk.getCleanSuccessors().size() == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isSimpleIf(BlockNode blk) {
|
||||||
|
return blk.getInstructions().size() == 1 && blk.getInstructions().get(0).getType() == InsnType.IF;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean couldMerge(MethodNode mth, BlockNode pred, BlockNode blk, BlockNode succ) {
|
||||||
|
// we cannot merge if the edge from blk to succ is a back edge
|
||||||
|
// there's a function in BlockUtils that purports to check if something is a back edge but it
|
||||||
|
// doesn't so do it by hand here
|
||||||
|
|
||||||
|
List<SpecialEdgeAttr> specialEdges = mth.getAll(AType.SPECIAL_EDGE);
|
||||||
|
for (SpecialEdgeAttr edge : specialEdges) {
|
||||||
|
if (edge.getStart() == blk && edge.getEnd() == succ && edge.getType() == SpecialEdgeType.BACK_EDGE) {
|
||||||
|
mth.addDebugComment("Refusing to push insns through at block " + blk.toString() + " : edge to successor is a back edge.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<InsnNode> getMovableInstructions(BlockNode blk, BlockNode succ) {
|
||||||
|
// A 'movable instruction' is one that does not impact either codegen or the semantics of the
|
||||||
|
// following block, so it can be pushed through into the new synthetics.
|
||||||
|
|
||||||
|
// For now, we just look for nop moves along the same register such that the target variable is not
|
||||||
|
// used in the succ block.
|
||||||
|
|
||||||
|
List<InsnNode> movableInstructions = new ArrayList<>();
|
||||||
|
for (InsnNode insn : blk.getInstructions()) {
|
||||||
|
if (insn.getType() == InsnType.MOVE) {
|
||||||
|
if (!(insn.getArg(0) instanceof RegisterArg)) {
|
||||||
|
// could be a LiteralArg
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
RegisterArg source = (RegisterArg) insn.getArg(0);
|
||||||
|
RegisterArg target = insn.getResult();
|
||||||
|
|
||||||
|
List<RegisterArg> uses = target.getSVar().getUseList();
|
||||||
|
for (RegisterArg use : uses) {
|
||||||
|
if (BlockUtils.blockContains(succ, use.getParentInsn())) {
|
||||||
|
// the target is used inside the successor, so we can't cleanly do the assignment afterwards
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we don't want to just push everything through, e.g.
|
||||||
|
// if (condition) { return; }
|
||||||
|
// x = 123456
|
||||||
|
// if (condition) { return; }
|
||||||
|
// would be a less clean result if the assignment was pushed into the block of the 2nd if.
|
||||||
|
|
||||||
|
if (source.getRegNum() == target.getRegNum()) {
|
||||||
|
movableInstructions.add(insn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return movableInstructions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doMove(MethodNode mth, BlockNode target, BlockNode bottomIf, List<InsnNode> movableInstructions) {
|
||||||
|
// Move instructions from the list out of blk and into new synthetics on each edge out of succ
|
||||||
|
|
||||||
|
// preserving instruction ordering, although it's unlikely that it would ever matter here
|
||||||
|
Collections.reverse(movableInstructions);
|
||||||
|
for (InsnNode insn : movableInstructions) {
|
||||||
|
target.getInstructions().remove(insn);
|
||||||
|
for (BlockNode succ : bottomIf.getCleanSuccessors()) {
|
||||||
|
succ.getInstructions().add(0, insn); // add at start
|
||||||
|
|
||||||
|
if (succ.contains(AFlag.LOOP_START)) {
|
||||||
|
// if we're merging into a loop condition, silence the warning when there's more than one
|
||||||
|
// instruction in the loop header
|
||||||
|
succ.add(AFlag.ALLOW_MULTIPLE_INSNS_LOOP_COND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -44,7 +44,8 @@ import jadx.core.utils.exceptions.JadxException;
|
|||||||
runAfter = {
|
runAfter = {
|
||||||
ModVisitor.class,
|
ModVisitor.class,
|
||||||
FixAccessModifiers.class,
|
FixAccessModifiers.class,
|
||||||
ProcessAnonymous.class
|
ProcessAnonymous.class,
|
||||||
|
ExtractFieldInit.class
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
public class ClassModifier extends AbstractVisitor {
|
public class ClassModifier extends AbstractVisitor {
|
||||||
@@ -326,8 +327,9 @@ public class ClassModifier extends AbstractVisitor {
|
|||||||
}
|
}
|
||||||
AccessInfo af = mth.getAccessFlags();
|
AccessInfo af = mth.getAccessFlags();
|
||||||
boolean publicConstructor = mth.isConstructor() && af.isPublic();
|
boolean publicConstructor = mth.isConstructor() && af.isPublic();
|
||||||
|
boolean enumDefConstructor = mth.isConstructor() && mth.getParentClass().contains(AFlag.CONVERTED_ENUM);
|
||||||
boolean clsInit = mth.getMethodInfo().isClassInit() && af.isStatic();
|
boolean clsInit = mth.getMethodInfo().isClassInit() && af.isStatic();
|
||||||
if (publicConstructor || clsInit) {
|
if (publicConstructor || enumDefConstructor || clsInit) {
|
||||||
if (!BlockUtils.isAllBlocksEmpty(mth.getBasicBlocks())) {
|
if (!BlockUtils.isAllBlocksEmpty(mth.getBasicBlocks())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ public class ConstructorVisitor extends AbstractVisitor {
|
|||||||
|
|
||||||
private static boolean canRemoveConstructor(MethodNode mth, ConstructorInsn co) {
|
private static boolean canRemoveConstructor(MethodNode mth, ConstructorInsn co) {
|
||||||
ClassNode parentClass = mth.getParentClass();
|
ClassNode parentClass = mth.getParentClass();
|
||||||
if (co.isSuper() && (co.getArgsCount() == 0 || parentClass.isEnum())) {
|
if (co.isSuper() && co.getArgsCount() == 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (co.isThis() && co.getArgsCount() == 0) {
|
if (co.isThis() && co.getArgsCount() == 0) {
|
||||||
|
|||||||
@@ -1,32 +1,12 @@
|
|||||||
package jadx.core.dex.visitors;
|
package jadx.core.dex.visitors;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.Collections;
|
import java.util.Optional;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
|
|
||||||
import jadx.api.ICodeWriter;
|
|
||||||
import jadx.api.impl.SimpleCodeWriter;
|
|
||||||
import jadx.core.codegen.MethodGen;
|
|
||||||
import jadx.core.dex.attributes.IAttributeNode;
|
|
||||||
import jadx.core.dex.instructions.IfNode;
|
|
||||||
import jadx.core.dex.instructions.InsnType;
|
|
||||||
import jadx.core.dex.nodes.BlockNode;
|
|
||||||
import jadx.core.dex.nodes.IBlock;
|
|
||||||
import jadx.core.dex.nodes.IContainer;
|
|
||||||
import jadx.core.dex.nodes.IRegion;
|
import jadx.core.dex.nodes.IRegion;
|
||||||
import jadx.core.dex.nodes.InsnNode;
|
|
||||||
import jadx.core.dex.nodes.MethodNode;
|
import jadx.core.dex.nodes.MethodNode;
|
||||||
import jadx.core.dex.trycatch.ExceptionHandler;
|
import jadx.core.utils.DotGraphUtils;
|
||||||
import jadx.core.utils.BlockUtils;
|
|
||||||
import jadx.core.utils.InsnUtils;
|
|
||||||
import jadx.core.utils.RegionUtils;
|
|
||||||
import jadx.core.utils.StringUtils;
|
|
||||||
import jadx.core.utils.Utils;
|
|
||||||
|
|
||||||
import static jadx.core.codegen.MethodGen.FallbackOption.BLOCK_DUMP;
|
|
||||||
|
|
||||||
public class DotGraphVisitor extends AbstractVisitor {
|
public class DotGraphVisitor extends AbstractVisitor {
|
||||||
|
|
||||||
@@ -38,6 +18,9 @@ public class DotGraphVisitor extends AbstractVisitor {
|
|||||||
private final boolean useRegions;
|
private final boolean useRegions;
|
||||||
private final boolean rawInsn;
|
private final boolean rawInsn;
|
||||||
|
|
||||||
|
// if present, this region and it's children will still be drawn when not in regions mode.
|
||||||
|
private Optional<IRegion> highlightRegion;
|
||||||
|
|
||||||
public static DotGraphVisitor dump() {
|
public static DotGraphVisitor dump() {
|
||||||
return new DotGraphVisitor(false, false);
|
return new DotGraphVisitor(false, false);
|
||||||
}
|
}
|
||||||
@@ -54,9 +37,26 @@ public class DotGraphVisitor extends AbstractVisitor {
|
|||||||
return new DotGraphVisitor(true, true);
|
return new DotGraphVisitor(true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to generate a cfg at a given point showing only one of the regions in the graph.
|
||||||
|
* Intended to be called during a debugging session to produce a CFG with only a region of interest,
|
||||||
|
* with DotGraphVisitor.debugDumpWithRegionHiglight(region).visit(mth);
|
||||||
|
*
|
||||||
|
* @param region the region to show
|
||||||
|
* @return the visitor, to be invoked with `.visit(mth);`
|
||||||
|
*/
|
||||||
|
public static DotGraphVisitor debugDumpWithRegionHighlight(IRegion region) {
|
||||||
|
return new DotGraphVisitor(false, false, Optional.of(region));
|
||||||
|
}
|
||||||
|
|
||||||
private DotGraphVisitor(boolean useRegions, boolean rawInsn) {
|
private DotGraphVisitor(boolean useRegions, boolean rawInsn) {
|
||||||
|
this(useRegions, rawInsn, Optional.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
private DotGraphVisitor(boolean useRegions, boolean rawInsn, Optional<IRegion> highlightRegion) {
|
||||||
this.useRegions = useRegions;
|
this.useRegions = useRegions;
|
||||||
this.rawInsn = rawInsn;
|
this.rawInsn = rawInsn;
|
||||||
|
this.highlightRegion = highlightRegion;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -69,264 +69,13 @@ public class DotGraphVisitor extends AbstractVisitor {
|
|||||||
if (mth.isNoCode()) {
|
if (mth.isNoCode()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
File outRootDir = mth.root().getArgs().getOutDir();
|
new DotGraphUtils(useRegions, rawInsn, highlightRegion).dumpToFile(mth);
|
||||||
new DumpDotGraph(outRootDir).process(mth);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void save(File dir, MethodNode mth) {
|
public void save(File dir, MethodNode mth) {
|
||||||
if (mth.isNoCode()) {
|
if (mth.isNoCode()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
new DumpDotGraph(dir).process(mth);
|
new DotGraphUtils(useRegions, rawInsn, highlightRegion).dumpToFile(mth, dir);
|
||||||
}
|
|
||||||
|
|
||||||
private class DumpDotGraph {
|
|
||||||
private final ICodeWriter dot = new SimpleCodeWriter();
|
|
||||||
private final ICodeWriter conn = new SimpleCodeWriter();
|
|
||||||
private final File dir;
|
|
||||||
|
|
||||||
public DumpDotGraph(File dir) {
|
|
||||||
this.dir = dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void process(MethodNode mth) {
|
|
||||||
dot.startLine("digraph \"CFG for");
|
|
||||||
dot.add(escape(mth.getMethodInfo().getFullId()));
|
|
||||||
dot.add("\" {");
|
|
||||||
|
|
||||||
BlockNode enterBlock = mth.getEnterBlock();
|
|
||||||
if (useRegions) {
|
|
||||||
if (mth.getRegion() == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
processMethodRegion(mth);
|
|
||||||
} else {
|
|
||||||
List<BlockNode> blocks = mth.getBasicBlocks();
|
|
||||||
if (blocks == null) {
|
|
||||||
InsnNode[] insnArr = mth.getInstructions();
|
|
||||||
if (insnArr == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
BlockNode block = new BlockNode(0, 0, 0);
|
|
||||||
List<InsnNode> insnList = block.getInstructions();
|
|
||||||
for (InsnNode insn : insnArr) {
|
|
||||||
if (insn != null) {
|
|
||||||
insnList.add(insn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
enterBlock = block;
|
|
||||||
blocks = Collections.singletonList(block);
|
|
||||||
}
|
|
||||||
for (BlockNode block : blocks) {
|
|
||||||
processBlock(mth, block, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dot.startLine("MethodNode[shape=record,label=\"{");
|
|
||||||
dot.add(escape(mth.getAccessFlags().makeString(true)));
|
|
||||||
dot.add(escape(mth.getReturnType() + " "
|
|
||||||
+ mth.getParentClass() + '.' + mth.getName()
|
|
||||||
+ '(' + Utils.listToString(mth.getAllArgRegs()) + ") "));
|
|
||||||
|
|
||||||
String attrs = attributesString(mth);
|
|
||||||
if (!attrs.isEmpty()) {
|
|
||||||
dot.add(" | ").add(attrs);
|
|
||||||
}
|
|
||||||
dot.add("}\"];");
|
|
||||||
|
|
||||||
dot.startLine("MethodNode -> ").add(makeName(enterBlock)).add(';');
|
|
||||||
|
|
||||||
dot.add(conn.toString());
|
|
||||||
|
|
||||||
dot.startLine('}');
|
|
||||||
dot.startLine();
|
|
||||||
|
|
||||||
String fileName = StringUtils.escape(mth.getMethodInfo().getShortId())
|
|
||||||
+ (useRegions ? ".regions" : "")
|
|
||||||
+ (rawInsn ? ".raw" : "")
|
|
||||||
+ ".dot";
|
|
||||||
File file = dir.toPath()
|
|
||||||
.resolve(mth.getParentClass().getClassInfo().getAliasFullPath() + "_graphs")
|
|
||||||
.resolve(fileName)
|
|
||||||
.toFile();
|
|
||||||
SaveCode.save(dot.finish(), file);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processMethodRegion(MethodNode mth) {
|
|
||||||
processRegion(mth, mth.getRegion());
|
|
||||||
for (ExceptionHandler h : mth.getExceptionHandlers()) {
|
|
||||||
if (h.getHandlerRegion() != null) {
|
|
||||||
processRegion(mth, h.getHandlerRegion());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Set<IBlock> regionsBlocks = new HashSet<>(mth.getBasicBlocks().size());
|
|
||||||
RegionUtils.getAllRegionBlocks(mth.getRegion(), regionsBlocks);
|
|
||||||
for (ExceptionHandler handler : mth.getExceptionHandlers()) {
|
|
||||||
IContainer handlerRegion = handler.getHandlerRegion();
|
|
||||||
if (handlerRegion != null) {
|
|
||||||
RegionUtils.getAllRegionBlocks(handlerRegion, regionsBlocks);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (BlockNode block : mth.getBasicBlocks()) {
|
|
||||||
if (!regionsBlocks.contains(block)) {
|
|
||||||
processBlock(mth, block, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processRegion(MethodNode mth, IContainer region) {
|
|
||||||
if (region instanceof IRegion) {
|
|
||||||
IRegion r = (IRegion) region;
|
|
||||||
dot.startLine("subgraph " + makeName(region) + " {");
|
|
||||||
dot.startLine("label = \"").add(r.toString());
|
|
||||||
String attrs = attributesString(r);
|
|
||||||
if (!attrs.isEmpty()) {
|
|
||||||
dot.add(" | ").add(attrs);
|
|
||||||
}
|
|
||||||
dot.add("\";");
|
|
||||||
dot.startLine("node [shape=record,color=blue];");
|
|
||||||
|
|
||||||
for (IContainer c : r.getSubBlocks()) {
|
|
||||||
processRegion(mth, c);
|
|
||||||
}
|
|
||||||
|
|
||||||
dot.startLine('}');
|
|
||||||
} else if (region instanceof BlockNode) {
|
|
||||||
processBlock(mth, (BlockNode) region, false);
|
|
||||||
} else if (region instanceof IBlock) {
|
|
||||||
processIBlock(mth, (IBlock) region, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processBlock(MethodNode mth, BlockNode block, boolean error) {
|
|
||||||
String attrs = attributesString(block);
|
|
||||||
dot.startLine(makeName(block));
|
|
||||||
dot.add(" [shape=record,");
|
|
||||||
if (error) {
|
|
||||||
dot.add("color=red,");
|
|
||||||
}
|
|
||||||
dot.add("label=\"{");
|
|
||||||
dot.add(String.valueOf(block.getCId())).add("\\:\\ ");
|
|
||||||
dot.add(InsnUtils.formatOffset(block.getStartOffset()));
|
|
||||||
if (!attrs.isEmpty()) {
|
|
||||||
dot.add('|').add(attrs);
|
|
||||||
}
|
|
||||||
if (PRINT_DOMINATORS_INFO) {
|
|
||||||
dot.add('|');
|
|
||||||
dot.startLine("doms: ").add(escape(block.getDoms()));
|
|
||||||
dot.startLine("\\lidom: ").add(escape(block.getIDom()));
|
|
||||||
dot.startLine("\\lpost-doms: ").add(escape(block.getPostDoms()));
|
|
||||||
dot.startLine("\\lpost-idom: ").add(escape(block.getIPostDom()));
|
|
||||||
dot.startLine("\\ldom-f: ").add(escape(block.getDomFrontier()));
|
|
||||||
dot.startLine("\\ldoms-on: ").add(escape(Utils.listToString(block.getDominatesOn())));
|
|
||||||
dot.startLine("\\l");
|
|
||||||
}
|
|
||||||
String insns = insertInsns(mth, block);
|
|
||||||
if (!insns.isEmpty()) {
|
|
||||||
dot.add('|').add(insns);
|
|
||||||
}
|
|
||||||
dot.add("}\"];");
|
|
||||||
|
|
||||||
BlockNode falsePath = null;
|
|
||||||
InsnNode lastInsn = BlockUtils.getLastInsn(block);
|
|
||||||
if (lastInsn != null && lastInsn.getType() == InsnType.IF) {
|
|
||||||
falsePath = ((IfNode) lastInsn).getElseBlock();
|
|
||||||
}
|
|
||||||
for (BlockNode next : block.getSuccessors()) {
|
|
||||||
String style = next == falsePath ? "[style=dashed]" : "";
|
|
||||||
addEdge(block, next, style);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (PRINT_DOMINATORS) {
|
|
||||||
for (BlockNode c : block.getDominatesOn()) {
|
|
||||||
conn.startLine(block.getCId() + " -> " + c.getCId() + "[color=green];");
|
|
||||||
}
|
|
||||||
for (BlockNode dom : BlockUtils.bitSetToBlocks(mth, block.getDomFrontier())) {
|
|
||||||
conn.startLine("f_" + block.getCId() + " -> f_" + dom.getCId() + "[color=blue];");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processIBlock(MethodNode mth, IBlock block, boolean error) {
|
|
||||||
String attrs = attributesString(block);
|
|
||||||
dot.startLine(makeName(block));
|
|
||||||
dot.add(" [shape=record,");
|
|
||||||
if (error) {
|
|
||||||
dot.add("color=red,");
|
|
||||||
}
|
|
||||||
dot.add("label=\"{");
|
|
||||||
if (!attrs.isEmpty()) {
|
|
||||||
dot.add(attrs);
|
|
||||||
}
|
|
||||||
String insns = insertInsns(mth, block);
|
|
||||||
if (!insns.isEmpty()) {
|
|
||||||
dot.add('|').add(insns);
|
|
||||||
}
|
|
||||||
dot.add("}\"];");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addEdge(BlockNode from, BlockNode to, String style) {
|
|
||||||
conn.startLine(makeName(from)).add(" -> ").add(makeName(to));
|
|
||||||
conn.add(style);
|
|
||||||
conn.add(';');
|
|
||||||
}
|
|
||||||
|
|
||||||
private String attributesString(IAttributeNode block) {
|
|
||||||
StringBuilder attrs = new StringBuilder();
|
|
||||||
for (String attr : block.getAttributesStringsList()) {
|
|
||||||
attrs.append(escape(attr)).append(NL);
|
|
||||||
}
|
|
||||||
return attrs.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String makeName(IContainer c) {
|
|
||||||
String name;
|
|
||||||
if (c instanceof BlockNode) {
|
|
||||||
name = "Node_" + ((BlockNode) c).getCId();
|
|
||||||
} else if (c instanceof IBlock) {
|
|
||||||
name = "Node_" + c.getClass().getSimpleName() + '_' + c.hashCode();
|
|
||||||
} else {
|
|
||||||
name = "cluster_" + c.getClass().getSimpleName() + '_' + c.hashCode();
|
|
||||||
}
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String insertInsns(MethodNode mth, IBlock block) {
|
|
||||||
if (rawInsn) {
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
for (InsnNode insn : block.getInstructions()) {
|
|
||||||
sb.append(escape(insn)).append(NL);
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
} else {
|
|
||||||
ICodeWriter code = new SimpleCodeWriter();
|
|
||||||
List<InsnNode> instructions = block.getInstructions();
|
|
||||||
MethodGen.addFallbackInsns(code, mth, instructions.toArray(new InsnNode[0]), BLOCK_DUMP);
|
|
||||||
String str = escape(code.newLine().toString());
|
|
||||||
if (str.startsWith(NL)) {
|
|
||||||
str = str.substring(NL.length());
|
|
||||||
}
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String escape(Object obj) {
|
|
||||||
if (obj == null) {
|
|
||||||
return "null";
|
|
||||||
}
|
|
||||||
return escape(obj.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
private String escape(String string) {
|
|
||||||
return string
|
|
||||||
.replace("\\", "") // TODO replace \"
|
|
||||||
.replace("/", "\\/")
|
|
||||||
.replace(">", "\\>").replace("<", "\\<")
|
|
||||||
.replace("{", "\\{").replace("}", "\\}")
|
|
||||||
.replace("\"", "\\\"")
|
|
||||||
.replace("-", "\\-")
|
|
||||||
.replace("|", "\\|")
|
|
||||||
.replaceAll("\\R", NLQR);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import jadx.core.utils.BlockInsnPair;
|
|||||||
import jadx.core.utils.BlockUtils;
|
import jadx.core.utils.BlockUtils;
|
||||||
import jadx.core.utils.InsnRemover;
|
import jadx.core.utils.InsnRemover;
|
||||||
import jadx.core.utils.InsnUtils;
|
import jadx.core.utils.InsnUtils;
|
||||||
|
import jadx.core.utils.ListUtils;
|
||||||
import jadx.core.utils.Utils;
|
import jadx.core.utils.Utils;
|
||||||
import jadx.core.utils.exceptions.JadxException;
|
import jadx.core.utils.exceptions.JadxException;
|
||||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||||
@@ -72,6 +73,7 @@ import static jadx.core.utils.InsnUtils.getWrappedInsn;
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
public class EnumVisitor extends AbstractVisitor {
|
public class EnumVisitor extends AbstractVisitor {
|
||||||
|
private static final String ENUM_SUPER_CONSTRUCTOR_ID = "java.lang.Enum.<init>(Ljava/lang/String;I)V";
|
||||||
|
|
||||||
private MethodInfo enumValueOfMth;
|
private MethodInfo enumValueOfMth;
|
||||||
private MethodInfo cloneMth;
|
private MethodInfo cloneMth;
|
||||||
@@ -149,6 +151,14 @@ public class EnumVisitor extends AbstractVisitor {
|
|||||||
if (arrArg.isInsnWrap()) {
|
if (arrArg.isInsnWrap()) {
|
||||||
InsnNode wrappedInsn = ((InsnWrapArg) arrArg).getWrapInsn();
|
InsnNode wrappedInsn = ((InsnWrapArg) arrArg).getWrapInsn();
|
||||||
enumFields = extractEnumFieldsFromInsn(data, wrappedInsn);
|
enumFields = extractEnumFieldsFromInsn(data, wrappedInsn);
|
||||||
|
} else if (arrArg.isRegister()) {
|
||||||
|
// Kotlin 1.9+ $ENTRIES pattern: array register has multiple uses,
|
||||||
|
// preventing CodeShrinkVisitor from inlining into the SPUT
|
||||||
|
RegisterArg regArg = (RegisterArg) arrArg;
|
||||||
|
InsnNode assignInsn = regArg.getAssignInsn();
|
||||||
|
if (assignInsn != null) {
|
||||||
|
enumFields = extractEnumFieldsFromInsn(data, assignInsn);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (enumFields == null) {
|
if (enumFields == null) {
|
||||||
cls.addWarnComment("Unknown enum class pattern. Please report as an issue!");
|
cls.addWarnComment("Unknown enum class pattern. Please report as an issue!");
|
||||||
@@ -162,11 +172,8 @@ public class EnumVisitor extends AbstractVisitor {
|
|||||||
cls.addAttr(attr);
|
cls.addAttr(attr);
|
||||||
|
|
||||||
for (EnumField enumField : attr.getFields()) {
|
for (EnumField enumField : attr.getFields()) {
|
||||||
ConstructorInsn co = enumField.getConstrInsn();
|
|
||||||
FieldNode fieldNode = enumField.getField();
|
FieldNode fieldNode = enumField.getField();
|
||||||
|
String name = enumField.getNameStr();
|
||||||
// use string arg from the constructor as enum field name
|
|
||||||
String name = getConstString(cls.root(), co.getArg(0));
|
|
||||||
if (name != null
|
if (name != null
|
||||||
&& !fieldNode.getAlias().equals(name)
|
&& !fieldNode.getAlias().equals(name)
|
||||||
&& NameMapper.isValidAndPrintable(name)
|
&& NameMapper.isValidAndPrintable(name)
|
||||||
@@ -184,9 +191,24 @@ public class EnumVisitor extends AbstractVisitor {
|
|||||||
CodeShrinkVisitor.shrinkMethod(classInitMth);
|
CodeShrinkVisitor.shrinkMethod(classInitMth);
|
||||||
}
|
}
|
||||||
removeEnumMethods(cls, data.valuesField);
|
removeEnumMethods(cls, data.valuesField);
|
||||||
|
fixAccessFlags(cls);
|
||||||
|
cls.add(AFlag.CONVERTED_ENUM);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void fixAccessFlags(ClassNode cls) {
|
||||||
|
// remove invalid access flags
|
||||||
|
cls.setAccessFlags(cls.getAccessFlags()
|
||||||
|
.remove(AccessFlags.FINAL)
|
||||||
|
.remove(AccessFlags.ABSTRACT)
|
||||||
|
.remove(AccessFlags.STATIC));
|
||||||
|
for (MethodNode mth : cls.getMethods()) {
|
||||||
|
if (mth.getMethodInfo().isConstructor()) {
|
||||||
|
mth.setAccessFlags(mth.getAccessFlags().remove(AccessInfo.VISIBILITY_FLAGS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search "$VALUES" field (holds all enum values)
|
* Search "$VALUES" field (holds all enum values)
|
||||||
*/
|
*/
|
||||||
@@ -291,8 +313,13 @@ public class EnumVisitor extends AbstractVisitor {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
List<EnumField> enumFields = extractEnumFieldsFromInsn(enumData, wrappedInsn);
|
List<EnumField> enumFields = extractEnumFieldsFromInsn(enumData, wrappedInsn);
|
||||||
if (enumFields != null) {
|
if (enumFields != null && ListUtils.isSingleElement(valuesMth.getUseIn(), enumData.classInitMth)) {
|
||||||
valuesMth.add(AFlag.DONT_GENERATE);
|
valuesMth.add(AFlag.DONT_GENERATE);
|
||||||
|
if (valuesMth.getName().equals("$values")) {
|
||||||
|
// Kotlin synthetic method used for init values
|
||||||
|
// rename to actual values method to use in $ENTRIES init code
|
||||||
|
valuesMth.getMethodInfo().setAlias("values");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return enumFields;
|
return enumFields;
|
||||||
}
|
}
|
||||||
@@ -419,13 +446,9 @@ public class EnumVisitor extends AbstractVisitor {
|
|||||||
return enumFieldNode;
|
return enumFieldNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("StatementWithEmptyBody")
|
|
||||||
private EnumField createEnumFieldByConstructor(EnumData data, FieldNode enumFieldNode, ConstructorInsn co) {
|
private EnumField createEnumFieldByConstructor(EnumData data, FieldNode enumFieldNode, ConstructorInsn co) {
|
||||||
// usually constructor signature is '<init>(Ljava/lang/String;I)V'.
|
// usually constructor signature is '<init>(Ljava/lang/String;I)V', sometimes one or both args can
|
||||||
// sometimes for one field enum second arg can be omitted
|
// be omitted
|
||||||
if (co.getArgsCount() < 1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
ClassNode cls = data.cls;
|
ClassNode cls = data.cls;
|
||||||
ClassInfo clsInfo = co.getClassType();
|
ClassInfo clsInfo = co.getClassType();
|
||||||
ClassNode constrCls = cls.root().resolveClass(clsInfo);
|
ClassNode constrCls = cls.root().resolveClass(clsInfo);
|
||||||
@@ -443,17 +466,45 @@ public class EnumVisitor extends AbstractVisitor {
|
|||||||
if (ctrMth == null) {
|
if (ctrMth == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
List<RegisterArg> regs = new ArrayList<>();
|
// usually constructor signature is '<init>(Ljava/lang/String;I)V'
|
||||||
co.getRegisterArgs(regs);
|
// sometimes one or both args can be inlined or omitted
|
||||||
if (!regs.isEmpty()) {
|
String nameStr = null;
|
||||||
ConstructorInsn replacedCo = inlineExternalRegs(data, co);
|
if (co.getArgsCount() == 0) {
|
||||||
if (replacedCo == null) {
|
ConstructorInsn ctrInsn = searchEnumSuperCtrInsn(ctrMth);
|
||||||
throw new JadxRuntimeException("Init of enum field '" + enumFieldNode.getName() + "' uses external variables");
|
if (ctrInsn != null && ctrInsn.getArgsCount() != 0) {
|
||||||
|
nameStr = getConstString(ctrMth.root(), ctrInsn.getArg(0));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nameStr = getConstString(cls.root(), co.getArg(0));
|
||||||
|
// verify and try to inline additional constructor args
|
||||||
|
List<RegisterArg> regs = new ArrayList<>();
|
||||||
|
co.getRegisterArgs(regs);
|
||||||
|
if (!regs.isEmpty()) {
|
||||||
|
ConstructorInsn replacedCo = inlineExternalRegs(data, co);
|
||||||
|
if (replacedCo == null) {
|
||||||
|
throw new JadxRuntimeException("Init of enum field '" + enumFieldNode.getName() + "' uses external variables");
|
||||||
|
}
|
||||||
|
data.toRemove.add(co);
|
||||||
|
co = replacedCo;
|
||||||
}
|
}
|
||||||
data.toRemove.add(co);
|
|
||||||
co = replacedCo;
|
|
||||||
}
|
}
|
||||||
return new EnumField(enumFieldNode, co);
|
return new EnumField(enumFieldNode, co, nameStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private @Nullable ConstructorInsn searchEnumSuperCtrInsn(MethodNode ctrMth) {
|
||||||
|
for (BlockNode block : ctrMth.getBasicBlocks()) {
|
||||||
|
for (InsnNode insn : block.getInstructions()) {
|
||||||
|
if (insn.getType() == InsnType.CONSTRUCTOR) {
|
||||||
|
ConstructorInsn ctrCall = (ConstructorInsn) insn;
|
||||||
|
if (ctrCall.isSuper()
|
||||||
|
&& ctrCall.getArgsCount() != 0
|
||||||
|
&& ctrCall.getCallMth().getRawFullId().equals(ENUM_SUPER_CONSTRUCTOR_ID)) {
|
||||||
|
return ctrCall;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ConstructorInsn inlineExternalRegs(EnumData data, ConstructorInsn co) {
|
private ConstructorInsn inlineExternalRegs(EnumData data, ConstructorInsn co) {
|
||||||
@@ -506,7 +557,18 @@ public class EnumVisitor extends AbstractVisitor {
|
|||||||
}
|
}
|
||||||
case FILLED_NEW_ARRAY: {
|
case FILLED_NEW_ARRAY: {
|
||||||
// allow usage in values init instruction
|
// allow usage in values init instruction
|
||||||
if (!data.valuesInitInsn.getArg(0).unwrap().equals(useInsn)) {
|
InsnArg valuesArg = data.valuesInitInsn.getArg(0);
|
||||||
|
InsnNode unwrapped = valuesArg.unwrap();
|
||||||
|
if (unwrapped != null) {
|
||||||
|
if (unwrapped != useInsn) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else if (valuesArg.isRegister()) {
|
||||||
|
InsnNode valuesAssign = ((RegisterArg) valuesArg).getAssignInsn();
|
||||||
|
if (valuesAssign != useInsn) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -549,10 +611,16 @@ public class EnumVisitor extends AbstractVisitor {
|
|||||||
}
|
}
|
||||||
String shortId = mi.getShortId();
|
String shortId = mi.getShortId();
|
||||||
if (mi.isConstructor()) {
|
if (mi.isConstructor()) {
|
||||||
|
markArgsForSkip(mth);
|
||||||
|
// remove super constructor call
|
||||||
|
ConstructorInsn superCtrInsn = searchEnumSuperCtrInsn(mth);
|
||||||
|
if (superCtrInsn != null) {
|
||||||
|
superCtrInsn.add(AFlag.DONT_GENERATE);
|
||||||
|
InsnRemover.remove(mth, superCtrInsn);
|
||||||
|
}
|
||||||
if (isDefaultConstructor(mth, shortId)) {
|
if (isDefaultConstructor(mth, shortId)) {
|
||||||
mth.add(AFlag.DONT_GENERATE);
|
mth.add(AFlag.DONT_GENERATE);
|
||||||
}
|
}
|
||||||
markArgsForSkip(mth);
|
|
||||||
} else if (mi.getShortId().equals(valuesMethodShortId)) {
|
} else if (mi.getShortId().equals(valuesMethodShortId)) {
|
||||||
if (isValuesMethod(mth, clsType)) {
|
if (isValuesMethod(mth, clsType)) {
|
||||||
valuesMethod = mth;
|
valuesMethod = mth;
|
||||||
@@ -732,4 +800,9 @@ public class EnumVisitor extends AbstractVisitor {
|
|||||||
this.staticBlocks = staticBlocks;
|
this.staticBlocks = staticBlocks;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "EnumVisitor";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,7 +158,9 @@ public class InlineMethods extends AbstractVisitor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateUsageInfo(MethodNode mth, MethodNode inlinedMth, InsnNode insn) {
|
private void updateUsageInfo(MethodNode mth, MethodNode inlinedMth, InsnNode insn) {
|
||||||
inlinedMth.getUseIn().remove(mth);
|
List<MethodNode> newUseIn = new ArrayList<>(inlinedMth.getUseIn());
|
||||||
|
newUseIn.remove(mth);
|
||||||
|
inlinedMth.setUseIn(newUseIn);
|
||||||
insn.visitInsns(innerInsn -> {
|
insn.visitInsns(innerInsn -> {
|
||||||
// TODO: share code with UsageInfoVisitor
|
// TODO: share code with UsageInfoVisitor
|
||||||
switch (innerInsn.getType()) {
|
switch (innerInsn.getType()) {
|
||||||
@@ -167,7 +169,7 @@ public class InlineMethods extends AbstractVisitor {
|
|||||||
MethodInfo callMth = ((BaseInvokeNode) innerInsn).getCallMth();
|
MethodInfo callMth = ((BaseInvokeNode) innerInsn).getCallMth();
|
||||||
MethodNode callMthNode = mth.root().resolveMethod(callMth);
|
MethodNode callMthNode = mth.root().resolveMethod(callMth);
|
||||||
if (callMthNode != null) {
|
if (callMthNode != null) {
|
||||||
callMthNode.setUseIn(ListUtils.safeReplace(callMthNode.getUseIn(), inlinedMth, mth));
|
callMthNode.setUseIn(ListUtils.safeReplace(new ArrayList<>(callMthNode.getUseIn()), inlinedMth, mth));
|
||||||
replaceClsUsage(mth, inlinedMth, callMthNode.getParentClass());
|
replaceClsUsage(mth, inlinedMth, callMthNode.getParentClass());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -179,7 +181,7 @@ public class InlineMethods extends AbstractVisitor {
|
|||||||
FieldInfo fieldInfo = (FieldInfo) ((IndexInsnNode) innerInsn).getIndex();
|
FieldInfo fieldInfo = (FieldInfo) ((IndexInsnNode) innerInsn).getIndex();
|
||||||
FieldNode fieldNode = mth.root().resolveField(fieldInfo);
|
FieldNode fieldNode = mth.root().resolveField(fieldInfo);
|
||||||
if (fieldNode != null) {
|
if (fieldNode != null) {
|
||||||
fieldNode.setUseIn(ListUtils.safeReplace(fieldNode.getUseIn(), inlinedMth, mth));
|
fieldNode.setUseIn(ListUtils.safeReplace(new ArrayList<>(fieldNode.getUseIn()), inlinedMth, mth));
|
||||||
replaceClsUsage(mth, inlinedMth, fieldNode.getParentClass());
|
replaceClsUsage(mth, inlinedMth, fieldNode.getParentClass());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ public class MethodThrowsVisitor extends AbstractVisitor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
blocks: for (final BlockNode block : mth.getBasicBlocks()) {
|
blocks: for (BlockNode block : mth.getBasicBlocks()) {
|
||||||
// Skip e.g. throw instructions of synchronized regions
|
// Skip e.g. throw instructions of synchronized regions
|
||||||
boolean skipExceptions = block.contains(AFlag.REMOVE) || block.contains(AFlag.DONT_GENERATE);
|
boolean skipExceptions = block.contains(AFlag.REMOVE) || block.contains(AFlag.DONT_GENERATE);
|
||||||
Set<String> excludedExceptions = new HashSet<>();
|
Set<String> excludedExceptions = new HashSet<>();
|
||||||
@@ -127,7 +127,7 @@ public class MethodThrowsVisitor extends AbstractVisitor {
|
|||||||
excludedExceptions.add(handler.getArgType().toString());
|
excludedExceptions.add(handler.getArgType().toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (final InsnNode insn : block.getInstructions()) {
|
for (InsnNode insn : block.getInstructions()) {
|
||||||
checkInsn(mth, insn, excludedExceptions, skipExceptions);
|
checkInsn(mth, insn, excludedExceptions, skipExceptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,7 +85,6 @@ public class SimplifyVisitor extends AbstractVisitor {
|
|||||||
int insnCount = list.size();
|
int insnCount = list.size();
|
||||||
InsnNode modInsn = simplifyInsn(mth, insn, null);
|
InsnNode modInsn = simplifyInsn(mth, insn, null);
|
||||||
if (modInsn != null) {
|
if (modInsn != null) {
|
||||||
modInsn.rebindArgs();
|
|
||||||
if (i < list.size() && list.get(i) == insn) {
|
if (i < list.size() && list.get(i) == insn) {
|
||||||
list.set(i, modInsn);
|
list.set(i, modInsn);
|
||||||
} else {
|
} else {
|
||||||
@@ -95,6 +94,8 @@ public class SimplifyVisitor extends AbstractVisitor {
|
|||||||
}
|
}
|
||||||
list.set(idx, modInsn);
|
list.set(idx, modInsn);
|
||||||
}
|
}
|
||||||
|
InsnRemover.unbindInsn(mth, insn);
|
||||||
|
modInsn.rebindArgs();
|
||||||
if (list.size() < insnCount) {
|
if (list.size() < insnCount) {
|
||||||
// some insns removed => restart block processing
|
// some insns removed => restart block processing
|
||||||
simplifyBlock(mth, block);
|
simplifyBlock(mth, block);
|
||||||
@@ -113,8 +114,7 @@ public class SimplifyVisitor extends AbstractVisitor {
|
|||||||
InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn();
|
InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn();
|
||||||
InsnNode replaceInsn = simplifyInsn(mth, wrapInsn, insn);
|
InsnNode replaceInsn = simplifyInsn(mth, wrapInsn, insn);
|
||||||
if (replaceInsn != null) {
|
if (replaceInsn != null) {
|
||||||
arg.wrapInstruction(mth, replaceInsn, false);
|
arg.wrapInstruction(mth, replaceInsn);
|
||||||
InsnRemover.unbindInsn(mth, wrapInsn);
|
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -239,8 +239,8 @@ public class SimplifyVisitor extends AbstractVisitor {
|
|||||||
|| shadowedByOuterCast(mth.root(), castToType, parentInsn)) {
|
|| shadowedByOuterCast(mth.root(), castToType, parentInsn)) {
|
||||||
InsnNode insnNode = new InsnNode(InsnType.MOVE, 1);
|
InsnNode insnNode = new InsnNode(InsnType.MOVE, 1);
|
||||||
insnNode.setOffset(castInsn.getOffset());
|
insnNode.setOffset(castInsn.getOffset());
|
||||||
insnNode.setResult(castInsn.getResult());
|
insnNode.setResult(InsnNode.duplicateArg(castInsn.getResult()));
|
||||||
insnNode.addArg(castArg);
|
insnNode.addArg(castArg.duplicate());
|
||||||
return insnNode;
|
return insnNode;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -576,7 +576,11 @@ public class SimplifyVisitor extends AbstractVisitor {
|
|||||||
if (litArg.isNegative()) {
|
if (litArg.isNegative()) {
|
||||||
LiteralArg negLitArg = litArg.negate();
|
LiteralArg negLitArg = litArg.negate();
|
||||||
if (negLitArg != null) {
|
if (negLitArg != null) {
|
||||||
return new ArithNode(ArithOp.SUB, arith.getResult(), arith.getArg(0), negLitArg);
|
RegisterArg resArg = InsnNode.duplicateArg(arith.getResult());
|
||||||
|
ArithNode newInsn = new ArithNode(ArithOp.SUB, resArg, arith.getArg(0).duplicate(), negLitArg);
|
||||||
|
newInsn.copyAttributesFrom(arith);
|
||||||
|
newInsn.setOffset(arith.getOffset());
|
||||||
|
return newInsn;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -586,10 +590,12 @@ public class SimplifyVisitor extends AbstractVisitor {
|
|||||||
InsnArg firstArg = arith.getArg(0);
|
InsnArg firstArg = arith.getArg(0);
|
||||||
long lit = litArg.getLiteral();
|
long lit = litArg.getLiteral();
|
||||||
if (firstArg.getType() == ArgType.BOOLEAN && (lit == 0 || lit == 1)) {
|
if (firstArg.getType() == ArgType.BOOLEAN && (lit == 0 || lit == 1)) {
|
||||||
InsnNode node = new InsnNode(lit == 0 ? InsnType.MOVE : InsnType.NOT, 1);
|
InsnNode newInsn = new InsnNode(lit == 0 ? InsnType.MOVE : InsnType.NOT, 1);
|
||||||
node.setResult(arith.getResult());
|
newInsn.setResult(InsnNode.duplicateArg(arith.getResult()));
|
||||||
node.addArg(firstArg);
|
newInsn.addArg(firstArg.duplicate());
|
||||||
return node;
|
newInsn.copyAttributesFrom(arith);
|
||||||
|
newInsn.setOffset(arith.getOffset());
|
||||||
|
return newInsn;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -637,16 +643,22 @@ public class SimplifyVisitor extends AbstractVisitor {
|
|||||||
}
|
}
|
||||||
if (wrapType == InsnType.ARITH) {
|
if (wrapType == InsnType.ARITH) {
|
||||||
ArithNode ar = (ArithNode) wrap;
|
ArithNode ar = (ArithNode) wrap;
|
||||||
return ArithNode.oneArgOp(ar.getOp(), fArg, ar.getArg(1));
|
ArithNode newInsn = ArithNode.oneArgOp(ar.getOp(), fArg, ar.getArg(1).duplicate());
|
||||||
|
newInsn.copyAttributesFrom(insn);
|
||||||
|
newInsn.setOffset(insn.getOffset());
|
||||||
|
return newInsn;
|
||||||
}
|
}
|
||||||
int argsCount = wrap.getArgsCount();
|
int argsCount = wrap.getArgsCount();
|
||||||
InsnNode concat = new InsnNode(InsnType.STR_CONCAT, argsCount - 1);
|
InsnNode concat = new InsnNode(InsnType.STR_CONCAT, argsCount - 1);
|
||||||
for (int i = 1; i < argsCount; i++) {
|
for (int i = 1; i < argsCount; i++) {
|
||||||
concat.addArg(wrap.getArg(i));
|
concat.addArg(wrap.getArg(i).duplicate());
|
||||||
}
|
}
|
||||||
InsnArg concatArg = InsnArg.wrapArg(concat);
|
InsnArg concatArg = InsnArg.wrapArg(concat);
|
||||||
concatArg.setType(ArgType.STRING);
|
concatArg.setType(ArgType.STRING);
|
||||||
return ArithNode.oneArgOp(ArithOp.ADD, fArg, concatArg);
|
ArithNode newInsn = ArithNode.oneArgOp(ArithOp.ADD, fArg, concatArg);
|
||||||
|
newInsn.copyAttributesFrom(wrap);
|
||||||
|
newInsn.setOffset(wrap.getOffset());
|
||||||
|
return newInsn;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOG.debug("Can't convert field arith insn: {}, mth: {}", insn, mth, e);
|
LOG.debug("Can't convert field arith insn: {}, mth: {}", insn, mth, e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import jadx.api.plugins.utils.Utils;
|
|||||||
import jadx.core.Consts;
|
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.ExcSplitCrossAttr;
|
||||||
import jadx.core.dex.attributes.nodes.TmpEdgeAttr;
|
import jadx.core.dex.attributes.nodes.TmpEdgeAttr;
|
||||||
import jadx.core.dex.info.ClassInfo;
|
import jadx.core.dex.info.ClassInfo;
|
||||||
import jadx.core.dex.instructions.InsnType;
|
import jadx.core.dex.instructions.InsnType;
|
||||||
@@ -380,6 +381,23 @@ public class BlockExceptionHandler {
|
|||||||
}
|
}
|
||||||
connectSplittersAndHandlers(tryCatchBlock, topSplitterBlock, bottomSplitterBlock);
|
connectSplittersAndHandlers(tryCatchBlock, topSplitterBlock, bottomSplitterBlock);
|
||||||
|
|
||||||
|
// At this point, it's possible that a cross edge to the original bottom has been turned into a back
|
||||||
|
// edge by the insertion of the new bottom. This causes problems because back edges usually signifiy
|
||||||
|
// loops, but this is not a loop. To fix this, predecessors of the bottom that also have a path from
|
||||||
|
// the bottom are rewritten to point to the original path crossing point (before synthetic blocks).
|
||||||
|
if (bottom != null && bottom.contains(AType.EXC_SPLIT_CROSS)) {
|
||||||
|
List<BlockNode> convertBlocks = new ArrayList<>();
|
||||||
|
for (BlockNode b : bottom.getPredecessors()) {
|
||||||
|
if (BlockUtils.isAnyPathExists(bottom, b)) {
|
||||||
|
convertBlocks.add(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (BlockNode b : convertBlocks) {
|
||||||
|
// The connection can't be replaced during the first loop because it would modify the preds list.
|
||||||
|
BlockSplitter.replaceConnection(b, bottom, bottom.get(AType.EXC_SPLIT_CROSS).getOriginalPathCross());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (BlockNode block : blocks) {
|
for (BlockNode block : blocks) {
|
||||||
TryCatchBlockAttr currentTCBAttr = block.get(AType.TRY_BLOCK);
|
TryCatchBlockAttr currentTCBAttr = block.get(AType.TRY_BLOCK);
|
||||||
if (currentTCBAttr == null || currentTCBAttr.getInnerTryBlocks().contains(tryCatchBlock)) {
|
if (currentTCBAttr == null || currentTCBAttr.getInnerTryBlocks().contains(tryCatchBlock)) {
|
||||||
@@ -460,12 +478,15 @@ public class BlockExceptionHandler {
|
|||||||
List<BlockNode> outsidePredecessors = preds.stream()
|
List<BlockNode> outsidePredecessors = preds.stream()
|
||||||
.filter(p -> !BlockUtils.atLeastOnePathExists(blocks, p))
|
.filter(p -> !BlockUtils.atLeastOnePathExists(blocks, p))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
if (outsidePredecessors.isEmpty()) {
|
// if we have no predecessors or every predecessor is outside (which would mean that inserting the
|
||||||
|
// new synthetic block does nothing), just return the existing path cross instead.
|
||||||
|
if (outsidePredecessors.isEmpty() || outsidePredecessors.size() == pathCross.getPredecessors().size()) {
|
||||||
return pathCross;
|
return pathCross;
|
||||||
}
|
}
|
||||||
// some predecessors outside of input set paths -> split block only for input set
|
// some predecessors outside of input set paths -> split block only for input set
|
||||||
BlockNode splitCross = BlockSplitter.blockSplitTop(mth, pathCross);
|
BlockNode splitCross = BlockSplitter.blockSplitTop(mth, pathCross);
|
||||||
splitCross.add(AFlag.SYNTHETIC);
|
splitCross.add(AFlag.SYNTHETIC);
|
||||||
|
splitCross.addAttr(new ExcSplitCrossAttr(pathCross));
|
||||||
for (BlockNode outsidePredecessor : outsidePredecessors) {
|
for (BlockNode outsidePredecessor : outsidePredecessors) {
|
||||||
// return predecessors to split bottom block (original)
|
// return predecessors to split bottom block (original)
|
||||||
BlockSplitter.replaceConnection(outsidePredecessor, splitCross, pathCross);
|
BlockSplitter.replaceConnection(outsidePredecessor, splitCross, pathCross);
|
||||||
|
|||||||
@@ -127,13 +127,55 @@ public class BlockProcessor extends AbstractVisitor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void checkForUnreachableBlocks(MethodNode mth) {
|
private static void checkForUnreachableBlocks(MethodNode mth) {
|
||||||
for (BlockNode block : mth.getBasicBlocks()) {
|
while (true) {
|
||||||
if (block.getPredecessors().isEmpty() && block != mth.getEnterBlock()) {
|
boolean fixed = false;
|
||||||
throw new JadxRuntimeException("Unreachable block: " + block);
|
for (BlockNode block : mth.getBasicBlocks()) {
|
||||||
|
if (block.getPredecessors().isEmpty() && block != mth.getEnterBlock()) {
|
||||||
|
// Sometimes a split cross block will have all it's predecessors moved elsewhere after it's been
|
||||||
|
// created. This is usually detected at the time of it's creation, but in certain edge cases it
|
||||||
|
// is difficult to do so. In those cases it will be cleanly removed here, along with the associated
|
||||||
|
// bottom splitter.
|
||||||
|
if (block.contains(AType.EXC_SPLIT_CROSS) && fixUnreachableSplitCross(mth, block)) {
|
||||||
|
mth.addInfoComment("Removed unreachable split cross block " + block);
|
||||||
|
fixed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
throw new JadxRuntimeException("Unreachable block: " + block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!fixed) {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to remove an unreachable synthetic split cross block that has been added previously,
|
||||||
|
* along with the associated bottom splitter.
|
||||||
|
*
|
||||||
|
* @param mth the method containing the unreachable block
|
||||||
|
* @param splitCross the unreachable block
|
||||||
|
* @return true if the operation was successful, false if a precondition was not satisfied and no
|
||||||
|
* changes were made.
|
||||||
|
*/
|
||||||
|
private static boolean fixUnreachableSplitCross(MethodNode mth, BlockNode splitCross) {
|
||||||
|
BlockNode bottomSplitter = null;
|
||||||
|
for (BlockNode succ : splitCross.getSuccessors()) {
|
||||||
|
if (succ.contains(AFlag.EXC_BOTTOM_SPLITTER)) {
|
||||||
|
bottomSplitter = succ;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bottomSplitter == null || bottomSplitter.getPredecessors().size() != 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Set<BlockNode> removeSet = new HashSet<>();
|
||||||
|
removeSet.add(bottomSplitter);
|
||||||
|
removeSet.add(splitCross);
|
||||||
|
removeFromMethod(removeSet, mth);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean deduplicateBlockInsns(MethodNode mth, BlockNode block) {
|
private static boolean deduplicateBlockInsns(MethodNode mth, BlockNode block) {
|
||||||
if (block.contains(AFlag.LOOP_START) || block.contains(AFlag.LOOP_END)) {
|
if (block.contains(AFlag.LOOP_START) || block.contains(AFlag.LOOP_END)) {
|
||||||
// search for same instruction at end of all predecessors blocks
|
// search for same instruction at end of all predecessors blocks
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ public class BlockSplitter extends AbstractVisitor {
|
|||||||
replaceTarget(source, oldDest, newDest);
|
replaceTarget(source, oldDest, newDest);
|
||||||
}
|
}
|
||||||
|
|
||||||
static BlockNode insertBlockBetween(MethodNode mth, BlockNode source, BlockNode target) {
|
public static BlockNode insertBlockBetween(MethodNode mth, BlockNode source, BlockNode target) {
|
||||||
BlockNode newBlock = startNewBlock(mth, target.getStartOffset());
|
BlockNode newBlock = startNewBlock(mth, target.getStartOffset());
|
||||||
newBlock.add(AFlag.SYNTHETIC);
|
newBlock.add(AFlag.SYNTHETIC);
|
||||||
removeConnection(source, target);
|
removeConnection(source, target);
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
package jadx.core.dex.visitors.finaly;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import jadx.core.dex.instructions.args.InsnArg;
|
||||||
|
import jadx.core.dex.instructions.args.RegisterArg;
|
||||||
|
import jadx.core.dex.nodes.InsnNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A centrality state is an object which helps track how instructions can be skipped.
|
||||||
|
* When looking for a finally, one of the things we have to do is make sure that instructions
|
||||||
|
* are not part of the return and are actually part of the "finally" block.
|
||||||
|
* This object helps keep track of registers, instructions etc to see if instructions can be
|
||||||
|
* skipped.
|
||||||
|
*/
|
||||||
|
public final class CentralityState {
|
||||||
|
|
||||||
|
private final Set<RegisterArg> allowableOutputArguments = new HashSet<>();
|
||||||
|
private final SameInstructionsStrategy sameInstructionsStrategy;
|
||||||
|
private boolean allowsCentral = true;
|
||||||
|
private boolean allowsNonStartingNode;
|
||||||
|
|
||||||
|
public CentralityState(SameInstructionsStrategy sameInstructionsStrategy, boolean allowsNonStartingNode) {
|
||||||
|
this.sameInstructionsStrategy = sameInstructionsStrategy;
|
||||||
|
this.allowsNonStartingNode = allowsNonStartingNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder sb = new StringBuilder("CentralityState - ");
|
||||||
|
if (allowsCentral) {
|
||||||
|
sb.append("allows central");
|
||||||
|
} else {
|
||||||
|
sb.append("disallows central");
|
||||||
|
}
|
||||||
|
sb.append(" | ");
|
||||||
|
for (RegisterArg registerArg : allowableOutputArguments) {
|
||||||
|
sb.append(registerArg.getName());
|
||||||
|
sb.append(" ");
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public SameInstructionsStrategy getSameInstructionsStrategy() {
|
||||||
|
return sameInstructionsStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getAllowsCentral() {
|
||||||
|
return allowsCentral;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowsCentral(boolean allowsCentral) {
|
||||||
|
this.allowsCentral = allowsCentral;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getAllowsNonStartingNode() {
|
||||||
|
return allowsNonStartingNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowsNonStartingNode(boolean allowsNonStartingNode) {
|
||||||
|
this.allowsNonStartingNode = allowsNonStartingNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addAllowableOutput(RegisterArg allowableOutput) {
|
||||||
|
allowableOutputArguments.add(allowableOutput);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addAllowableOutputs(Collection<RegisterArg> allowableOutputs) {
|
||||||
|
allowableOutputArguments.addAll(allowableOutputs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds all inputs register arguments from an instruction as allowable output arguments.
|
||||||
|
*
|
||||||
|
* @param allowableOutputInsn The instruction to retrieve the list of inputs from.
|
||||||
|
*/
|
||||||
|
public void addAllowableOutputs(InsnNode allowableOutputInsn) {
|
||||||
|
List<RegisterArg> registerArgs = new LinkedList<>();
|
||||||
|
for (InsnArg arg : allowableOutputInsn.getArgList()) {
|
||||||
|
if (!(arg instanceof RegisterArg)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
registerArgs.add((RegisterArg) arg);
|
||||||
|
}
|
||||||
|
registerArgs.forEach(this::addAllowableOutput);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasAllowableOutput(InsnNode insn) {
|
||||||
|
if (allowableOutputArguments.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
RegisterArg registerArg;
|
||||||
|
if (insn.getResult() != null) {
|
||||||
|
registerArg = insn.getResult();
|
||||||
|
} else {
|
||||||
|
registerArg = null;
|
||||||
|
}
|
||||||
|
if (registerArg == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (RegisterArg allowableOutput : allowableOutputArguments) {
|
||||||
|
if (allowableOutput.equals(registerArg)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public boolean hasAllowableInputs(InsnNode insn) {
|
||||||
|
if (allowableOutputArguments.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
List<RegisterArg> registerArgs = new ArrayList<>();
|
||||||
|
for (InsnArg arg : insn.getArgList()) {
|
||||||
|
if (arg instanceof RegisterArg) {
|
||||||
|
registerArgs.add((RegisterArg) arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (registerArgs.isEmpty() || allowableOutputArguments.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (RegisterArg regArg : registerArgs) {
|
||||||
|
boolean foundMatch = false;
|
||||||
|
for (RegisterArg allowableOutput : allowableOutputArguments) {
|
||||||
|
if (regArg.equals(allowableOutput)) {
|
||||||
|
foundMatch = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!foundMatch) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CentralityState duplicate() {
|
||||||
|
CentralityState state = new CentralityState(sameInstructionsStrategy, allowsNonStartingNode);
|
||||||
|
state.allowsCentral = allowsCentral;
|
||||||
|
state.allowableOutputArguments.addAll(allowableOutputArguments);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<RegisterArg> getAllowableOutputArguments() {
|
||||||
|
return allowableOutputArguments;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
package jadx.core.dex.visitors.finaly;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import jadx.core.dex.nodes.BlockNode;
|
|
||||||
import jadx.core.dex.nodes.InsnNode;
|
|
||||||
import jadx.core.dex.nodes.MethodNode;
|
|
||||||
import jadx.core.dex.trycatch.ExceptionHandler;
|
|
||||||
import jadx.core.utils.Utils;
|
|
||||||
|
|
||||||
public class FinallyExtractInfo {
|
|
||||||
private final MethodNode mth;
|
|
||||||
private final ExceptionHandler finallyHandler;
|
|
||||||
private final List<BlockNode> allHandlerBlocks;
|
|
||||||
private final List<InsnsSlice> duplicateSlices = new ArrayList<>();
|
|
||||||
private final Set<BlockNode> checkedBlocks = new HashSet<>();
|
|
||||||
private final InsnsSlice finallyInsnsSlice = new InsnsSlice();
|
|
||||||
private final BlockNode startBlock;
|
|
||||||
|
|
||||||
private InsnsSlice curDupSlice;
|
|
||||||
private List<InsnNode> curDupInsns;
|
|
||||||
private int curDupInsnsOffset;
|
|
||||||
|
|
||||||
public FinallyExtractInfo(MethodNode mth, ExceptionHandler finallyHandler, BlockNode startBlock, List<BlockNode> allHandlerBlocks) {
|
|
||||||
this.mth = mth;
|
|
||||||
this.finallyHandler = finallyHandler;
|
|
||||||
this.startBlock = startBlock;
|
|
||||||
this.allHandlerBlocks = allHandlerBlocks;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MethodNode getMth() {
|
|
||||||
return mth;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ExceptionHandler getFinallyHandler() {
|
|
||||||
return finallyHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<BlockNode> getAllHandlerBlocks() {
|
|
||||||
return allHandlerBlocks;
|
|
||||||
}
|
|
||||||
|
|
||||||
public InsnsSlice getFinallyInsnsSlice() {
|
|
||||||
return finallyInsnsSlice;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<InsnsSlice> getDuplicateSlices() {
|
|
||||||
return duplicateSlices;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<BlockNode> getCheckedBlocks() {
|
|
||||||
return checkedBlocks;
|
|
||||||
}
|
|
||||||
|
|
||||||
public BlockNode getStartBlock() {
|
|
||||||
return startBlock;
|
|
||||||
}
|
|
||||||
|
|
||||||
public InsnsSlice getCurDupSlice() {
|
|
||||||
return curDupSlice;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCurDupSlice(InsnsSlice curDupSlice) {
|
|
||||||
this.curDupSlice = curDupSlice;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<InsnNode> getCurDupInsns() {
|
|
||||||
return curDupInsns;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getCurDupInsnsOffset() {
|
|
||||||
return curDupInsnsOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCurDupInsns(List<InsnNode> insns, int offset) {
|
|
||||||
this.curDupInsns = insns;
|
|
||||||
this.curDupInsnsOffset = offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "FinallyExtractInfo{"
|
|
||||||
+ "\n finally:\n " + finallyInsnsSlice
|
|
||||||
+ "\n dups:\n " + Utils.listToString(duplicateSlices, "\n ")
|
|
||||||
+ "\n}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
package jadx.core.dex.visitors.finaly;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.IdentityHashMap;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import jadx.core.dex.nodes.BlockNode;
|
|
||||||
import jadx.core.dex.nodes.InsnNode;
|
|
||||||
|
|
||||||
public class InsnsSlice {
|
|
||||||
private final List<InsnNode> insnsList = new ArrayList<>();
|
|
||||||
private final Map<InsnNode, BlockNode> insnMap = new IdentityHashMap<>();
|
|
||||||
private boolean complete;
|
|
||||||
|
|
||||||
public void addInsn(InsnNode insn, BlockNode block) {
|
|
||||||
insnsList.add(insn);
|
|
||||||
insnMap.put(insn, block);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addBlock(BlockNode block) {
|
|
||||||
for (InsnNode insn : block.getInstructions()) {
|
|
||||||
addInsn(insn, block);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addInsns(BlockNode block, int startIndex, int endIndex) {
|
|
||||||
List<InsnNode> insns = block.getInstructions();
|
|
||||||
for (int i = startIndex; i < endIndex; i++) {
|
|
||||||
addInsn(insns.get(i), block);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public BlockNode getBlock(InsnNode insn) {
|
|
||||||
return insnMap.get(insn);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<InsnNode> getInsnsList() {
|
|
||||||
return insnsList;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<BlockNode> getBlocks() {
|
|
||||||
Set<BlockNode> set = new LinkedHashSet<>();
|
|
||||||
for (InsnNode insn : insnsList) {
|
|
||||||
set.add(insnMap.get(insn));
|
|
||||||
}
|
|
||||||
return set;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void resetIncomplete() {
|
|
||||||
if (!complete) {
|
|
||||||
insnsList.clear();
|
|
||||||
insnMap.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isComplete() {
|
|
||||||
return complete;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setComplete(boolean complete) {
|
|
||||||
this.complete = complete;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "{["
|
|
||||||
+ insnsList.stream().map(insn -> insn.getType().toString()).collect(Collectors.joining(", "))
|
|
||||||
+ ']'
|
|
||||||
+ (complete ? " complete" : "")
|
|
||||||
+ '}';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user