Compare commits

...

51 Commits

Author SHA1 Message Date
Skylot f2ea6415c9 fix(cli): don't print stacktrace for incorrect options (#2140) 2024-04-20 18:06:30 +01:00
Skylot bc70f8eabb fix: use correct new line string for simple code writer 2024-04-20 17:37:45 +01:00
Skylot be25cbf8c2 fix: use common parser for manifest, verify app package 2024-04-20 17:37:45 +01:00
Skylot f9c0cad146 chore: update dependencies 2024-04-19 20:14:25 +01:00
Skylot b356ff76e1 fix: improve StringBuilder elimination (#2148) 2024-04-19 20:14:25 +01:00
Skylot ec9244a635 fix(gui): use common code for manifest parsing in debugger 2024-04-19 20:14:25 +01:00
omerfarukkykc a5bd64461d fix(gui): remember selected device in debugger (PR #2153)
* ADBDialog->launchApp() if multiple devices presented should let user select the one they desire.

* compare objects directly instead parsing

---------

Co-authored-by: Ömer Faruk KAYIKCI <omer.kayikci@tubitak.gov.tr>
Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
2024-04-19 19:02:12 +01:00
dependabot[bot] 54bf79ccc5 build(deps): bump gradle/wrapper-validation-action from 2 to 3 (#2149) 2024-04-15 18:17:48 +00:00
Skylot 6182332eef fix: avoid self-loop for exception handlers (#2147) 2024-04-11 23:07:45 +03:00
Skylot 37b57096ec fix: allow use FieldInfo as switch key (#2147) 2024-04-11 23:07:44 +03:00
Skylot 6aab8fabc9 chore: update dependencies 2024-04-11 23:07:41 +03:00
JustFor 665c1e57d2 fix(gui): update Messages_zh_CN.properties (PR #2146)
Sync new Jadx text.
2024-04-09 22:11:24 +01:00
Skylot 6e8affcbdc feat: add options to JadxArgs to change code new line and indent (#1945, #1948) 2024-04-08 21:51:24 +01:00
Skylot 41d6b0018e fix: add missing " * " on new line for block comments, flip addCodeComment args (#2145) 2024-04-08 21:34:34 +01:00
Skylot dbadbb01fc refactor: rename method collectArgsWithoutLoading into collectArgNodes in MethodNode (#2142) 2024-04-07 23:09:02 +01:00
Skylot 0f52077c5c feat: allow to set style for code comments (#2145) 2024-04-07 23:06:32 +01:00
Skylot ea861829c7 fix: support end block entry for mutli-entry loops (#889) 2024-04-06 22:49:32 +01:00
Skylot c1de235289 fix: in anonymous class checks ignore instance fields not used outside 2024-04-06 22:45:30 +01:00
Skylot 8f969d4e89 chore: update gradle and dependencies 2024-04-03 21:03:48 +01:00
Skylot 0c1f830f94 fix: lambda decoding and code generation (#2139) 2024-04-03 21:03:48 +01:00
Skylot 43c082e4da feat: replace Android resource ids with android.R fields (#2119) 2024-03-31 20:37:33 +01:00
Skylot ecdc4e6757 refactor: move constant collection into separate pass (#2119) 2024-03-30 21:51:02 +00:00
Skylot b865c9c687 refactor: allow store unresolved fields in ConstStorage (#2119) 2024-03-30 20:52:31 +00:00
xnumad 6b4976c593 fix(gui): handle paths where file name is null (#2136)(PR #2137)
* fix: Ignore invalid files

Avoid NullPointerException when using "Open files" or drag-n-drop

* refactor: Replace Stream API chain with loop

IntelliJ

* fix: Ignore invalid files

Avoid NullPointerException when using "Add files"

* fix: Fall back to complete path string

Instead of empty project name

* fix: Render tree

Project tree (sidebar) didn’t load
Toggling "View > Show flatten packages" threw a NPE here

* fix code formatting

---------

Co-authored-by: Skylot <skylot@gmail.com>
2024-03-29 22:30:01 +00:00
Skylot 2807dc5090 fix(script): add example script for resources rename (#2126) 2024-03-20 18:46:44 +00:00
Skylot 463d2b90fa fix: don't apply node positions and prevent eager loading for custom decompile modes (#2116) 2024-03-19 20:23:04 +00:00
Skylot bff00d101f fix(script): add option flags, fix missing script options in help 2024-03-19 20:22:50 +00:00
Skylot 1290ef63a2 fix(build): enable publish to maven for rename-mappings plugin 2024-03-16 21:58:03 +03:00
Skylot 49d2b34d84 chore: update dependencies 2024-03-16 21:58:00 +03:00
CKCat eecdfae73f fix(res): resolve some manifest decode errors (PR #2122)
* The elementSize may be larger than the actual size of the element chunk.

* end namespace chunk size can be any value.

* keep at least a warning.
2024-03-16 18:57:10 +00:00
JustFor 8760b4ddde fix(gui): copy strings without quotes (PR #2121)
* Update AbstractCodeArea.java

In general, we need data, not text in code. But now every time you copy the highlighted text, you copy the highlighted quotes as well. This often results in an extra need to delete the quotation marks around the sides, which is confusing.
Now when copying selected highlighted text, quotes are not copied in.

* Update AbstractCodeArea.java

fix code format

* additional checks, move to common method

---------

Co-authored-by: Skylot <skylot@gmail.com>
2024-03-16 18:55:57 +00:00
Andrei Kudryavtsev 3599b248a4 feat(gui): dragging tab appearance settings (#2120)(PR #2118) 2024-03-08 23:11:58 +03:00
bagipro 2fdd496518 fix(res): add indents for namespace declarations (PR #2114)
Co-authored-by: bagipro <bugi@bugi>
2024-03-01 16:32:47 +00:00
bagipro 278e3c2d47 fix(res): avoid duplicated XML attributes (PR #2112)
Co-authored-by: bagipro <bugi@bugi>
2024-02-27 18:49:09 +00:00
bagipro 881a716b8e fix(res): fixed XML proto parsing for removed debug data (PR #2111)
* Fixed XML proto parsing for removed debug data

* Fixed codestyle check

---------

Co-authored-by: bagipro <bugi@bugi>
2024-02-27 17:34:37 +00:00
Skylot a73c9e90fc fix(dex-input): improve error report message for invalid dex checksum 2024-02-26 19:36:28 +00:00
Skylot 56749b2afb chore: update dependencies 2024-02-25 22:38:00 +03:00
Andrei Kudryavtsev d7ec35791b feat(gui): tabs drag and drop reorder support (#1212) (PR #2109) 2024-02-25 19:36:46 +00:00
Skylot d51362ed50 fix: don't remove exception handlers (#2104) 2024-02-19 20:18:44 +00:00
Skylot 5c0c1daa71 fix(gui): use new RSTA line number formatter API to show source lines 2024-02-16 18:38:56 +00:00
Skylot 603ea3989a chore: update dependencies 2024-02-16 17:41:37 +00:00
Emiel Matthys 018ff98df7 feat(gui): remember save preference decision (PR #2103)
* First version

* Use dropdown

* Spotless

* Language strings and tests

* Comment out translated versions

* Remove more translations

---------

Co-authored-by: Emiel Matthys <emiel.matthys@guardsquare.com>
2024-02-15 18:00:37 +00:00
nitram84 5fbabdefca fix: NPE in unused EcxHandler block removal code (#2086) (PR #2104) 2024-02-15 17:57:30 +00:00
nitram84 13607fc8b6 fix(res): add missing namespace declarations (PR #2102)
* fix(res): add missing namespace declarations

* remove `writer.getIndent() == 0`

---------

Co-authored-by: skylot <118523+skylot@users.noreply.github.com>
2024-02-15 17:56:39 +00:00
DanielFi 0c33d723c8 fix: optimize switch fallthrough (PR #2054)
* cache post dom map between switch cases
* cache post dom map of whole methods
* calculate full post dom tree, fix switch out block detection

---------

Co-authored-by: Skylot <skylot@gmail.com>
2024-02-14 18:31:38 +00:00
Skylot 0143423dc9 fix: use empty line before field in correct place (#2101) 2024-02-12 15:39:41 +00:00
Skylot 21b1452485 chore: update gradle and dependencies 2024-02-12 15:39:40 +00:00
Skylot ecb8abb98e fix: correct rollback if type update failed (#2090) 2024-02-10 16:08:51 +00:00
Skylot a3a4fabd5a fix: store classes access flags in class set 2024-02-07 22:10:31 +03:00
Skylot edf6ce273c fix: clear node flags for custom decompilation mode 2024-02-06 21:54:41 +03:00
dependabot[bot] 1bb956a8b0 build(deps): bump gradle/wrapper-validation-action from 1 to 2 (PR #2099)
Bumps [gradle/wrapper-validation-action](https://github.com/gradle/wrapper-validation-action) from 1 to 2.
- [Release notes](https://github.com/gradle/wrapper-validation-action/releases)
- [Commits](https://github.com/gradle/wrapper-validation-action/compare/v1...v2)

---
updated-dependencies:
- dependency-name: gradle/wrapper-validation-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-05 18:38:02 +00:00
234 changed files with 4946 additions and 2529 deletions
@@ -12,4 +12,4 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: gradle/wrapper-validation-action@v1
- uses: gradle/actions/wrapper-validation@v3
+10 -6
View File
@@ -8,16 +8,19 @@
![GitHub release (latest by SemVer)](https://img.shields.io/github/downloads/skylot/jadx/latest/total)
![Latest release](https://img.shields.io/github/release/skylot/jadx.svg)
[![Maven Central](https://img.shields.io/maven-central/v/io.github.skylot/jadx-core)](https://search.maven.org/search?q=g:io.github.skylot%20AND%20jadx)
![Java 11+](https://img.shields.io/badge/Java-11%2B-blue)
[![License](http://img.shields.io/:license-apache-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0.html)
**jadx** - Dex to Java decompiler
Command line and GUI tools for producing Java source code from Android Dex and Apk files
:exclamation::exclamation::exclamation: Please note that in most cases **jadx** can't decompile all 100% of the code, so errors will occur. Check [Troubleshooting guide](https://github.com/skylot/jadx/wiki/Troubleshooting-Q&A#decompilation-issues) for workarounds
> [!WARNING]
> Please note that in most cases **jadx** can't decompile all 100% of the code, so errors will occur.<br />
> Check [Troubleshooting guide](https://github.com/skylot/jadx/wiki/Troubleshooting-Q&A#decompilation-issues) for workarounds.
**Main features:**
- decompile Dalvik bytecode to java classes from APK, dex, aar, aab and zip files
- decompile Dalvik bytecode to Java code from APK, dex, aar, aab and zip files
- decode `AndroidManifest.xml` and other resources from `resources.arsc`
- deobfuscator included
@@ -79,7 +82,7 @@ and also packed to `build/jadx-<version>.zip`
### Usage
```
jadx[-gui] [command] [options] <input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk)
jadx[-gui] [command] [options] <input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk, .jadx.kts)
commands (use '<command> --help' for command options):
plugins - manage jadx plugins
@@ -171,10 +174,11 @@ Plugin options (-P<name>=<value>):
- kotlin-metadata.to-string - rename fields using toString, values: [yes, no], default: yes
- kotlin-metadata.getters - rename simple getters to field names, values: [yes, no], default: yes
4) rename-mappings: various mappings support
- rename-mappings.format - mapping format, values: [auto, TINY, TINY_2, ENIGMA, ENIGMA_DIR, MCP, SRG, TSRG, TSRG2, PROGUARD], default: auto
- rename-mappings.invert - invert mapping, values: [yes, no], default: no
- rename-mappings.format - mapping format, values: [AUTO, TINY_FILE, TINY_2_FILE, ENIGMA_FILE, ENIGMA_DIR, SRG_FILE, XSRG_FILE, CSRG_FILE, TSRG_FILE, TSRG_2_FILE, PROGUARD_FILE], default: AUTO
- rename-mappings.invert - invert mapping on load, values: [yes, no], default: no
Environment variables:
JADX_DISABLE_XML_SECURITY - set to 'true' to disable all security checks for XML files
JADX_DISABLE_ZIP_SECURITY - set to 'true' to disable all security checks for zip files
JADX_ZIP_MAX_ENTRIES_COUNT - maximum allowed number of entries in zip files (default: 100 000)
JADX_TMP_DIR - custom temp directory, using system by default
@@ -186,7 +190,7 @@ Examples:
jadx --log-level ERROR app.apk
jadx -Pdex-input.verify-checksum=no app.apk
```
These options also worked on jadx-gui running from command line and override options from preferences dialog
These options also work in jadx-gui running from command line and override options from preferences' dialog
### Troubleshooting
Please check wiki page [Troubleshooting Q&A](https://github.com/skylot/jadx/wiki/Troubleshooting-Q&A)
+1 -1
View File
@@ -3,7 +3,7 @@ plugins {
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22")
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.23")
}
repositories {
@@ -11,15 +11,15 @@ group = "io.github.skylot"
version = jadxVersion
dependencies {
implementation("org.slf4j:slf4j-api:2.0.11")
implementation("org.slf4j:slf4j-api:2.0.13")
compileOnly("org.jetbrains:annotations:24.1.0")
testImplementation("ch.qos.logback:logback-classic:1.4.14")
testImplementation("ch.qos.logback:logback-classic:1.5.6")
testImplementation("org.hamcrest:hamcrest-library:2.2")
testImplementation("org.mockito:mockito-core:5.10.0")
testImplementation("org.assertj:assertj-core:3.25.2")
testImplementation("org.mockito:mockito-core:5.11.0")
testImplementation("org.assertj:assertj-core:3.25.3")
testImplementation("org.junit.jupiter:junit-jupiter:5.10.1")
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testCompileOnly("org.jetbrains:annotations:24.1.0")
Binary file not shown.
+2 -2
View File
@@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=9d926787066a081739e8200858338b4a69e837c3a821a33aca9db09dd4a41026
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
distributionSha256Sum=544c35d6bd849ae8a5ed0bcea39ba677dc40f49df7d1835561582da2009b961d
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Vendored
+10 -10
View File
@@ -43,11 +43,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
@@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
+1 -1
View File
@@ -20,7 +20,7 @@ dependencies {
runtimeOnly(project(":jadx-plugins:jadx-xapk-input"))
implementation("org.jcommander:jcommander:1.83")
implementation("ch.qos.logback:logback-classic:1.4.14")
implementation("ch.qos.logback:logback-classic:1.5.6")
}
application {
@@ -17,7 +17,6 @@ import com.beust.jcommander.ParameterDescription;
import com.beust.jcommander.ParameterException;
import com.beust.jcommander.Parameterized;
import jadx.api.JadxArgs;
import jadx.api.JadxDecompiler;
import jadx.api.plugins.JadxPluginInfo;
import jadx.api.plugins.options.JadxPluginOptions;
@@ -109,6 +108,7 @@ public class JCommanderWrapper<T> {
out.println(appendPluginOptions(maxNamesLen));
out.println();
out.println("Environment variables:");
out.println(" JADX_DISABLE_XML_SECURITY - set to 'true' to disable all security checks for XML files");
out.println(" JADX_DISABLE_ZIP_SECURITY - set to 'true' to disable all security checks for zip files");
out.println(" JADX_ZIP_MAX_ENTRIES_COUNT - maximum allowed number of entries in zip files (default: 100 000)");
out.println(" JADX_TMP_DIR - custom temp directory, using system by default");
@@ -165,7 +165,7 @@ public class JCommanderWrapper<T> {
opt.append("- ").append(description);
}
if (addDefaults) {
String defaultValue = getDefaultValue(args, f, opt);
String defaultValue = getDefaultValue(args, f);
if (defaultValue != null && !description.contains("(default)")) {
opt.append(", default: ").append(defaultValue);
}
@@ -188,7 +188,7 @@ public class JCommanderWrapper<T> {
}
@Nullable
private static String getDefaultValue(Object args, Field f, StringBuilder opt) {
private static String getDefaultValue(Object args, Field f) {
try {
Class<?> fieldType = f.getType();
if (fieldType == int.class) {
@@ -219,7 +219,7 @@ public class JCommanderWrapper<T> {
StringBuilder sb = new StringBuilder();
int k = 1;
// load and init all options plugins to print all options
try (JadxDecompiler decompiler = new JadxDecompiler(new JadxArgs())) {
try (JadxDecompiler decompiler = new JadxDecompiler(argsObj.toJadxArgs())) {
JadxPluginManager pluginManager = decompiler.getPluginManager();
pluginManager.load(new JadxExternalPluginsLoader());
pluginManager.initAll();
@@ -29,12 +29,12 @@ import jadx.api.args.IntegerFormat;
import jadx.api.args.ResourceNameSource;
import jadx.api.args.UserRenamesMappingsMode;
import jadx.core.deobf.conditions.DeobfWhitelist;
import jadx.core.utils.exceptions.JadxException;
import jadx.core.utils.exceptions.JadxArgsValidateException;
import jadx.core.utils.files.FileUtils;
public class JadxCLIArgs {
@Parameter(description = "<input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk)")
@Parameter(description = "<input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk, .jadx.kts)")
protected List<String> files = new ArrayList<>(1);
@Parameter(names = { "-d", "--output-dir" }, description = "output directory")
@@ -287,14 +287,13 @@ public class JadxCLIArgs {
System.out.println(JadxDecompiler.getVersion());
return false;
}
try {
if (threadsCount <= 0) {
throw new JadxException("Threads count must be positive, got: " + threadsCount);
if (threadsCount <= 0) {
throw new JadxArgsValidateException("Threads count must be positive, got: " + threadsCount);
}
for (String fileName : files) {
if (fileName.startsWith("-")) {
throw new JadxArgsValidateException("Unknown option: " + fileName);
}
} catch (JadxException e) {
System.err.println("ERROR: " + e.getMessage());
jcw.printUsage();
return false;
}
return true;
}
@@ -559,8 +558,8 @@ public class JadxCLIArgs {
for (String s : value.split(",")) {
try {
set.add(RenameEnum.valueOf(s.trim().toUpperCase(Locale.ROOT)));
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(
} catch (Exception e) {
throw new JadxArgsValidateException(
'\'' + s + "' is unknown for parameter " + paramName
+ ", possible values are " + enumValuesString(RenameEnum.values()));
}
@@ -625,7 +624,7 @@ public class JadxCLIArgs {
try {
return parse.apply(stringAsEnumName(value));
} catch (Exception e) {
throw new IllegalArgumentException(
throw new JadxArgsValidateException(
'\'' + value + "' is unknown, possible values are: " + enumValuesString(values.get()));
}
}
@@ -7,6 +7,7 @@ import com.beust.jcommander.JCommander;
import jadx.cli.commands.CommandPlugins;
import jadx.cli.commands.ICommand;
import jadx.core.utils.exceptions.JadxArgsValidateException;
public class JadxCLICommands {
private static final Map<String, ICommand> COMMANDS_MAP = new TreeMap<>();
@@ -26,7 +27,8 @@ public class JadxCLICommands {
public static boolean process(JCommanderWrapper<?> jcw, JCommander jc, String parsedCommand) {
ICommand command = COMMANDS_MAP.get(parsedCommand);
if (command == null) {
throw new IllegalArgumentException("Unknown command: " + parsedCommand);
throw new JadxArgsValidateException("Unknown command: " + parsedCommand
+ ". Expected one of: " + COMMANDS_MAP.keySet());
}
JCommander subCommander = jc.getCommands().get(parsedCommand);
command.process(jcw, subCommander);
@@ -12,6 +12,7 @@ import jadx.api.JadxDecompiler;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.visitors.SaveCode;
import jadx.core.utils.exceptions.JadxArgsValidateException;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
@@ -33,10 +34,10 @@ public class SingleClassMode {
.findFirst().orElse(null);
}
if (clsForProcess == null) {
throw new JadxRuntimeException("Input class not found: " + singleClass);
throw new JadxArgsValidateException("Input class not found: " + singleClass);
}
if (clsForProcess.contains(AFlag.DONT_GENERATE)) {
throw new JadxRuntimeException("Input class can't be saved by current jadx settings (marked as DONT_GENERATE)");
throw new JadxArgsValidateException("Input class can't be saved by current jadx settings (marked as DONT_GENERATE)");
}
if (clsForProcess.isInner()) {
clsForProcess = clsForProcess.getTopParentClass();
@@ -52,7 +53,7 @@ public class SingleClassMode {
if (size == 1) {
clsForProcess = classes.get(0);
} else {
throw new JadxRuntimeException("Found " + size + " classes, single class output can't be used");
throw new JadxArgsValidateException("Found " + size + " classes, single class output can't be used");
}
}
ICodeInfo codeInfo;
@@ -23,8 +23,9 @@ public class ConvertToClsSet {
private static final Logger LOG = LoggerFactory.getLogger(ConvertToClsSet.class);
public static void usage() {
LOG.info("<output .jcst file> <several input dex or jar files> ");
LOG.info("<android API level (number)> <output .jcst file> <several input dex or jar files> ");
LOG.info("Arguments to update core.jcst: "
+ "<android API level (number)> "
+ "<jadx root>/jadx-core/src/main/resources/clst/core.jcst "
+ "<sdk_root>/platforms/android-<api level>/android.jar"
+ "<sdk_root>/platforms/android-<api level>/optional/android.car.jar "
@@ -32,11 +33,12 @@ public class ConvertToClsSet {
}
public static void main(String[] args) {
if (args.length < 2) {
if (args.length != 5) {
usage();
System.exit(1);
}
List<Path> inputPaths = Stream.of(args).map(Paths::get).collect(Collectors.toList());
int androidApiLevel = Integer.parseInt(args[0]);
List<Path> inputPaths = Stream.of(args).skip(1).map(Paths::get).collect(Collectors.toList());
Path output = inputPaths.remove(0);
JadxArgs jadxArgs = new JadxArgs();
@@ -57,6 +59,7 @@ public class ConvertToClsSet {
decompiler.load();
RootNode root = decompiler.getRoot();
ClsSet set = new ClsSet(root);
set.setAndroidApiLevel(androidApiLevel);
set.loadFrom(root);
set.save(output);
@@ -7,6 +7,7 @@ import org.junit.jupiter.api.Test;
import jadx.api.JadxArgs.RenameEnum;
import jadx.cli.JadxCLIArgs.RenameConverter;
import jadx.core.utils.exceptions.JadxArgsValidateException;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -38,7 +39,7 @@ public class RenameConverterTest {
@Test
public void wrong() {
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class,
JadxArgsValidateException thrown = assertThrows(JadxArgsValidateException.class,
() -> converter.convert("wrong"),
"Expected convert() to throw, but it didn't");
+6 -2
View File
@@ -8,8 +8,12 @@ dependencies {
implementation("com.google.code.gson:gson:2.10.1")
// TODO: move resources decoding to separate plugin module
implementation("com.android.tools.build:aapt2-proto:8.2.2-10154469")
implementation("com.google.protobuf:protobuf-java:3.25.2") // forcing latest version
implementation("com.android.tools.build:aapt2-proto:8.3.2-10880808")
implementation("com.google.protobuf:protobuf-java") {
version {
require("3.25.3") // version 4 conflict with `aapt2-proto`
}
}
testImplementation("org.apache.commons:commons-lang3:3.14.0")
@@ -8,8 +8,6 @@ import jadx.api.metadata.ICodeAnnotation;
import jadx.api.metadata.ICodeNodeRef;
public interface ICodeWriter {
String NL = System.getProperty("line.separator");
String INDENT_STR = " ";
boolean isMetadataSupported();
@@ -42,6 +42,9 @@ public class JadxArgs implements Closeable {
public static final int DEFAULT_THREADS_COUNT = Math.max(1, Runtime.getRuntime().availableProcessors() / 2);
public static final String DEFAULT_NEW_LINE_STR = System.lineSeparator();
public static final String DEFAULT_INDENT_STR = " ";
public static final String DEFAULT_OUT_DIR = "jadx-output";
public static final String DEFAULT_SRC_DIR = "sources";
public static final String DEFAULT_RES_DIR = "resources";
@@ -144,6 +147,10 @@ public class JadxArgs implements Closeable {
private ICodeData codeData;
private String codeNewLineStr = DEFAULT_NEW_LINE_STR;
private String codeIndentStr = DEFAULT_INDENT_STR;
private CommentsLevel commentsLevel = CommentsLevel.INFO;
private IntegerFormat integerFormat = IntegerFormat.AUTO;
@@ -622,6 +629,22 @@ public class JadxArgs implements Closeable {
this.codeData = codeData;
}
public String getCodeIndentStr() {
return codeIndentStr;
}
public void setCodeIndentStr(String codeIndentStr) {
this.codeIndentStr = codeIndentStr;
}
public String getCodeNewLineStr() {
return codeNewLineStr;
}
public void setCodeNewLineStr(String codeNewLineStr) {
this.codeNewLineStr = codeNewLineStr;
}
public CommentsLevel getCommentsLevel() {
return commentsLevel;
}
@@ -28,12 +28,6 @@ public class JadxArgsValidator {
if (inputFiles.isEmpty() && jadx.getCustomCodeLoaders().isEmpty()) {
throw new JadxArgsValidateException("Please specify input file");
}
for (File inputFile : inputFiles) {
String fileName = inputFile.getName();
if (fileName.startsWith("--")) {
throw new JadxArgsValidateException("Unknown argument: " + fileName);
}
}
for (File file : inputFiles) {
checkFile(file);
}
@@ -62,6 +62,10 @@ public class ResourceFile {
return deobfName != null ? deobfName : name;
}
public void setDeobfName(String resFullName) {
this.deobfName = resFullName;
}
public ResourceType getType() {
return type;
}
@@ -84,7 +88,7 @@ public class ResourceFile {
}
String alias = sb.toString();
if (!alias.equals(name)) {
deobfName = alias;
setDeobfName(alias);
return true;
}
return false;
@@ -0,0 +1,69 @@
package jadx.api.data;
public enum CommentStyle {
/**
* <pre>
* // comment
* </pre>
*/
LINE("// ", "// ", ""),
// @formatter:off
/**
* <pre>
* /*
* * comment
* *&#47;
* </pre>
*/
// @formatter:on
BLOCK("/*\n * ", " * ", "\n */"),
/**
* <pre>
* /* comment *&#47;
* </pre>
*/
BLOCK_CONDENSED("/* ", " * ", " */"),
// @formatter:off
/**
* <pre>
* /**
* * comment
* *&#47;
* </pre>
*/
// @formatter:on
JAVADOC("/**\n * ", " * ", "\n */"),
/**
* <pre>
* /** comment *&#47;
* </pre>
*/
JAVADOC_CONDENSED("/** ", " * ", " */");
private final String start;
private final String onNewLine;
private final String end;
CommentStyle(String start, String onNewLine, String end) {
this.start = start;
this.onNewLine = onNewLine;
this.end = end;
}
public String getStart() {
return start;
}
public String getOnNewLine() {
return onNewLine;
}
public String getEnd() {
return end;
}
}
@@ -10,4 +10,6 @@ public interface ICodeComment extends Comparable<ICodeComment> {
IJavaCodeRef getCodeRef();
String getComment();
CommentStyle getStyle();
}
@@ -3,6 +3,7 @@ package jadx.api.data.impl;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import jadx.api.data.CommentStyle;
import jadx.api.data.ICodeComment;
import jadx.api.data.IJavaCodeRef;
import jadx.api.data.IJavaNodeRef;
@@ -13,15 +14,25 @@ public class JadxCodeComment implements ICodeComment {
@Nullable
private IJavaCodeRef codeRef;
private String comment;
private CommentStyle style = CommentStyle.LINE;
public JadxCodeComment(IJavaNodeRef nodeRef, String comment) {
this(nodeRef, null, comment);
}
public JadxCodeComment(IJavaNodeRef nodeRef, String comment, CommentStyle style) {
this(nodeRef, null, comment, style);
}
public JadxCodeComment(IJavaNodeRef nodeRef, @Nullable IJavaCodeRef codeRef, String comment) {
this(nodeRef, codeRef, comment, CommentStyle.LINE);
}
public JadxCodeComment(IJavaNodeRef nodeRef, @Nullable IJavaCodeRef codeRef, String comment, CommentStyle style) {
this.nodeRef = nodeRef;
this.codeRef = codeRef;
this.comment = comment;
this.style = style;
}
public JadxCodeComment() {
@@ -56,6 +67,15 @@ public class JadxCodeComment implements ICodeComment {
this.comment = comment;
}
@Override
public CommentStyle getStyle() {
return style;
}
public void setStyle(CommentStyle style) {
this.style = style;
}
@Override
public int compareTo(@NotNull ICodeComment other) {
int cmpNodeRef = this.getNodeRef().compareTo(other.getNodeRef());
@@ -73,6 +93,7 @@ public class JadxCodeComment implements ICodeComment {
return "JadxCodeComment{" + nodeRef
+ ", ref=" + codeRef
+ ", comment='" + comment + '\''
+ ", style=" + style
+ '}';
}
}
@@ -21,9 +21,6 @@ public class AnnotatedCodeWriter extends SimpleCodeWriter implements ICodeWriter
private Map<Integer, ICodeAnnotation> annotations = Collections.emptyMap();
private Map<Integer, Integer> lineMap = Collections.emptyMap();
public AnnotatedCodeWriter() {
}
public AnnotatedCodeWriter(JadxArgs args) {
super(args);
}
@@ -35,9 +32,9 @@ public class AnnotatedCodeWriter extends SimpleCodeWriter implements ICodeWriter
@Override
public AnnotatedCodeWriter addMultiLine(String str) {
if (str.contains(NL)) {
buf.append(str.replace(NL, NL + indentStr));
line += StringUtils.countMatches(str, NL);
if (str.contains(newLineStr)) {
buf.append(str.replace(newLineStr, newLineStr + indentStr));
line += StringUtils.countMatches(str, newLineStr);
offset = 0;
} else {
buf.append(str);
@@ -84,7 +81,7 @@ public class AnnotatedCodeWriter extends SimpleCodeWriter implements ICodeWriter
@Override
protected void addLine() {
buf.append(NL);
buf.append(newLineStr);
line++;
offset = 0;
}
@@ -154,7 +151,6 @@ public class AnnotatedCodeWriter extends SimpleCodeWriter implements ICodeWriter
@Override
public ICodeInfo finish() {
processDefinitionAnnotations();
validateAnnotations();
String code = buf.toString();
buf = null;
@@ -166,18 +162,6 @@ public class AnnotatedCodeWriter extends SimpleCodeWriter implements ICodeWriter
return annotations;
}
private void processDefinitionAnnotations() {
if (!annotations.isEmpty()) {
annotations.forEach((k, v) -> {
if (v instanceof NodeDeclareRef) {
NodeDeclareRef declareRef = (NodeDeclareRef) v;
declareRef.setDefPos(k);
declareRef.getNode().setDefPosition(k);
}
});
}
}
private void validateAnnotations() {
if (annotations.isEmpty()) {
return;
@@ -14,38 +14,39 @@ import jadx.api.metadata.ICodeNodeRef;
import jadx.core.utils.Utils;
/**
* CodeWriter implementation without meta information support (only strings builder)
* CodeWriter implementation without meta information support
*/
public class SimpleCodeWriter implements ICodeWriter {
private static final Logger LOG = LoggerFactory.getLogger(SimpleCodeWriter.class);
private static final String[] INDENT_CACHE = {
"",
INDENT_STR,
INDENT_STR + INDENT_STR,
INDENT_STR + INDENT_STR + INDENT_STR,
INDENT_STR + INDENT_STR + INDENT_STR + INDENT_STR,
INDENT_STR + INDENT_STR + INDENT_STR + INDENT_STR + INDENT_STR,
};
protected StringBuilder buf = new StringBuilder();
protected String indentStr = "";
protected int indent = 0;
private final boolean insertLineNumbers;
public SimpleCodeWriter() {
this.insertLineNumbers = false;
}
protected final boolean insertLineNumbers;
protected final String singleIndentStr;
protected final String newLineStr;
public SimpleCodeWriter(JadxArgs args) {
this.insertLineNumbers = args.isInsertDebugLines();
this.singleIndentStr = args.getCodeIndentStr();
this.newLineStr = args.getCodeNewLineStr();
if (insertLineNumbers) {
incIndent(3);
add(indentStr);
}
}
/**
* Constructor with JadxArgs should be used.
*/
@Deprecated
public SimpleCodeWriter() {
this.insertLineNumbers = false;
this.singleIndentStr = JadxArgs.DEFAULT_INDENT_STR;
this.newLineStr = JadxArgs.DEFAULT_NEW_LINE_STR;
}
@Override
public boolean isMetadataSupported() {
return false;
@@ -96,8 +97,8 @@ public class SimpleCodeWriter implements ICodeWriter {
@Override
public SimpleCodeWriter addMultiLine(String str) {
if (str.contains(NL)) {
buf.append(str.replace(NL, NL + indentStr));
if (str.contains(newLineStr)) {
buf.append(str.replace(newLineStr, newLineStr + indentStr));
} else {
buf.append(str);
}
@@ -130,12 +131,12 @@ public class SimpleCodeWriter implements ICodeWriter {
@Override
public SimpleCodeWriter addIndent() {
add(INDENT_STR);
add(singleIndentStr);
return this;
}
protected void addLine() {
buf.append(NL);
buf.append(newLineStr);
}
protected SimpleCodeWriter addLineIndent() {
@@ -144,12 +145,7 @@ public class SimpleCodeWriter implements ICodeWriter {
}
private void updateIndent() {
int curIndent = indent;
if (curIndent < INDENT_CACHE.length) {
this.indentStr = INDENT_CACHE[curIndent];
} else {
this.indentStr = Utils.strRepeat(INDENT_STR, curIndent);
}
this.indentStr = Utils.strRepeat(singleIndentStr, indent);
}
@Override
@@ -219,17 +215,17 @@ public class SimpleCodeWriter implements ICodeWriter {
@Override
public ICodeInfo finish() {
removeFirstEmptyLine();
String code = buf.toString();
String code = getStringWithoutFirstEmptyLine();
buf = null;
return new SimpleCodeInfo(code);
}
protected void removeFirstEmptyLine() {
int len = NL.length();
if (buf.length() > len && buf.substring(0, len).equals(NL)) {
buf.delete(0, len);
private String getStringWithoutFirstEmptyLine() {
int len = newLineStr.length();
if (buf.length() > len && buf.substring(0, len).equals(newLineStr)) {
return buf.substring(len);
}
return buf.toString();
}
@Override
@@ -12,12 +12,12 @@ public enum OptionFlag {
HIDE_IN_GUI,
/**
* Do not show this option in jadx-gui (useful if option is configured with custom ui)
* Option will be read-only in jadx-gui (can be used for calculated properties)
*/
DISABLE_IN_GUI,
/**
* Add this flag only if option do not affect generated code.
* Add this flag only if the option does not affect generated code.
* If added, option value change will not cause code cache reset.
*/
NOT_CHANGING_CODE,
@@ -1,7 +1,5 @@
package jadx.api.utils;
import jadx.api.ICodeWriter;
public class CodeUtils {
public static String getLineForPos(String code, int pos) {
@@ -11,18 +9,32 @@ public class CodeUtils {
}
public static int getLineStartForPos(String code, int pos) {
String newLine = ICodeWriter.NL;
int start = code.lastIndexOf(newLine, pos);
return start == -1 ? 0 : start + newLine.length();
int start = getNewLinePosBefore(code, pos);
return start == -1 ? 0 : start + 1;
}
public static int getLineEndForPos(String code, int pos) {
int end = code.indexOf(ICodeWriter.NL, pos);
int end = getNewLinePosAfter(code, pos);
return end == -1 ? code.length() : end;
}
public static int getLineNumForPos(String code, int pos) {
String newLine = ICodeWriter.NL;
public static int getNewLinePosAfter(String code, int startPos) {
int pos = code.indexOf('\n', startPos);
if (pos != -1) {
// check for '\r\n'
int prev = pos - 1;
if (code.charAt(prev) == '\r') {
return prev;
}
}
return pos;
}
public static int getNewLinePosBefore(String code, int startPos) {
return code.lastIndexOf('\n', startPos);
}
public static int getLineNumForPos(String code, int pos, String newLine) {
int newLineLen = newLine.length();
int line = 1;
int prev = 0;
@@ -54,6 +54,8 @@ import jadx.core.dex.visitors.debuginfo.DebugInfoApplyVisitor;
import jadx.core.dex.visitors.debuginfo.DebugInfoAttachVisitor;
import jadx.core.dex.visitors.finaly.MarkFinallyVisitor;
import jadx.core.dex.visitors.kotlin.ProcessKotlinInternals;
import jadx.core.dex.visitors.prepare.AddAndroidConstants;
import jadx.core.dex.visitors.prepare.CollectConstValues;
import jadx.core.dex.visitors.regions.CheckRegions;
import jadx.core.dex.visitors.regions.CleanRegions;
import jadx.core.dex.visitors.regions.IfRegionVisitor;
@@ -67,6 +69,7 @@ import jadx.core.dex.visitors.rename.SourceFileRename;
import jadx.core.dex.visitors.shrink.CodeShrinkVisitor;
import jadx.core.dex.visitors.ssa.SSATransform;
import jadx.core.dex.visitors.typeinference.FinishTypeInference;
import jadx.core.dex.visitors.typeinference.FixTypesVisitor;
import jadx.core.dex.visitors.typeinference.TypeInferenceVisitor;
import jadx.core.dex.visitors.usage.UsageInfoVisitor;
import jadx.core.utils.exceptions.JadxRuntimeException;
@@ -95,6 +98,8 @@ public class Jadx {
List<IDexTreeVisitor> passes = new ArrayList<>();
passes.add(new SignatureProcessor());
passes.add(new OverrideMethodVisitor());
passes.add(new AddAndroidConstants());
passes.add(new CollectConstValues());
// rename and deobfuscation
passes.add(new DeobfuscatorVisitor());
@@ -141,7 +146,9 @@ public class Jadx {
if (args.isDebugInfo()) {
passes.add(new DebugInfoApplyVisitor());
}
passes.add(new FixTypesVisitor());
passes.add(new FinishTypeInference());
if (args.getUseKotlinMethodsForVarNames() != JadxArgs.UseKotlinMethodsForVarNames.DISABLE) {
passes.add(new ProcessKotlinInternals());
}
@@ -216,6 +223,7 @@ public class Jadx {
if (args.isDebugInfo()) {
passes.add(new DebugInfoApplyVisitor());
}
passes.add(new FixTypesVisitor());
passes.add(new FinishTypeInference());
passes.add(new CodeRenameVisitor());
passes.add(new DeboxingVisitor());
@@ -44,14 +44,17 @@ public class ClsSet {
private static final String CLST_PATH = "/clst/" + CLST_FILENAME;
private static final String JADX_CLS_SET_HEADER = "jadx-cst";
private static final int VERSION = 4;
private static final int VERSION = 5;
private static final String STRING_CHARSET = "US-ASCII";
private static final ArgType[] EMPTY_ARGTYPE_ARRAY = new ArgType[0];
private static final ArgType[] OBJECT_ARGTYPE_ARRAY = new ArgType[] { ArgType.OBJECT };
private final RootNode root;
private int androidApiLevel;
public ClsSet(RootNode root) {
this.root = root;
}
@@ -79,7 +82,8 @@ public class ClsSet {
if (LOG.isDebugEnabled()) {
long time = System.currentTimeMillis() - startTime;
int methodsCount = Stream.of(classes).mapToInt(clspClass -> clspClass.getMethodsMap().size()).sum();
LOG.debug("Clst file loaded in {}ms, classes: {}, methods: {}", time, classes.length, methodsCount);
LOG.debug("Clst file loaded in {}ms, android api: {}, classes: {}, methods: {}",
time, androidApiLevel, classes.length, methodsCount);
}
}
@@ -93,7 +97,7 @@ public class ClsSet {
cls.load();
ClspClassSource source = getClspClassSource(cls);
ClspClass nClass = new ClspClass(clsType, k, source);
ClspClass nClass = new ClspClass(clsType, k, cls.getAccessFlags().rawValue(), source);
if (names.put(clsRawName, nClass) != null) {
throw new JadxRuntimeException("Duplicate class: " + clsRawName);
}
@@ -151,7 +155,11 @@ public class ClsSet {
// cls is java.lang.Object
return EMPTY_ARGTYPE_ARRAY;
}
ArgType[] parents = new ArgType[1 + cls.getInterfaces().size()];
int interfacesCount = cls.getInterfaces().size();
if (interfacesCount == 0 && superClass == ArgType.OBJECT) {
return OBJECT_ARGTYPE_ARRAY;
}
ArgType[] parents = new ArgType[1 + interfacesCount];
parents[0] = superClass;
int k = 1;
for (ArgType iface : cls.getInterfaces()) {
@@ -193,10 +201,12 @@ public class ClsSet {
DataOutputStream out = new DataOutputStream(output);
out.writeBytes(JADX_CLS_SET_HEADER);
out.writeByte(VERSION);
out.writeInt(androidApiLevel);
Map<String, ClspClass> names = new HashMap<>(classes.length);
out.writeInt(classes.length);
for (ClspClass cls : classes) {
out.writeInt(cls.getAccFlags());
writeUnsignedByte(out, cls.getSource().ordinal());
String clsName = cls.getName();
writeString(out, clsName);
@@ -243,6 +253,10 @@ public class ClsSet {
out.writeByte(-1);
return;
}
if (arr == OBJECT_ARGTYPE_ARRAY) {
out.writeByte(-2);
return;
}
int size = arr.length;
out.writeByte(size);
if (size != 0) {
@@ -294,22 +308,22 @@ public class ClsSet {
try (DataInputStream in = new DataInputStream(new BufferedInputStream(input))) {
byte[] header = new byte[JADX_CLS_SET_HEADER.length()];
int readHeaderLength = in.read(header);
int version = in.readByte();
if (readHeaderLength != JADX_CLS_SET_HEADER.length()
|| !JADX_CLS_SET_HEADER.equals(new String(header, STRING_CHARSET))
|| version != VERSION) {
|| !JADX_CLS_SET_HEADER.equals(new String(header, STRING_CHARSET))) {
throw new DecodeException("Wrong jadx class set header");
}
int version = in.readByte();
if (version != VERSION) {
throw new DecodeException("Wrong jadx class set version, got: " + version + ", expect: " + VERSION);
}
androidApiLevel = in.readInt();
int clsCount = in.readInt();
classes = new ClspClass[clsCount];
ClspClassSource[] clspClassSources = ClspClassSource.values();
for (int i = 0; i < clsCount; i++) {
int source = readUnsignedByte(in);
if (source < 0 || source > clspClassSources.length) {
throw new DecodeException("Wrong jadx source identifier");
}
int accFlags = in.readInt();
ClspClassSource clsSource = readClsSource(in);
String name = readString(in);
classes[i] = new ClspClass(ArgType.object(name), i, clspClassSources[source]);
classes[i] = new ClspClass(ArgType.object(name), i, accFlags, clsSource);
}
for (int i = 0; i < clsCount; i++) {
ClspClass nClass = classes[i];
@@ -321,6 +335,15 @@ public class ClsSet {
}
}
private static ClspClassSource readClsSource(DataInputStream in) throws IOException, DecodeException {
int source = readUnsignedByte(in);
ClspClassSource[] clspClassSources = ClspClassSource.values();
if (source < 0 || source > clspClassSources.length) {
throw new DecodeException("Wrong jadx source identifier: " + source);
}
return clspClassSources[source];
}
private List<ClspMethod> readClsMethods(DataInputStream in, ClassInfo clsInfo) throws IOException {
int mCount = in.readShort();
List<ClspMethod> methods = new ArrayList<>(mCount);
@@ -366,17 +389,20 @@ public class ClsSet {
@Nullable
private ArgType[] readArgTypesArray(DataInputStream in) throws IOException {
int count = in.readByte();
if (count == -1) {
return null;
switch (count) {
case -1:
return null;
case -2:
return OBJECT_ARGTYPE_ARRAY;
case 0:
return EMPTY_ARGTYPE_ARRAY;
default:
ArgType[] arr = new ArgType[count];
for (int i = 0; i < count; i++) {
arr[i] = readArgType(in);
}
return arr;
}
if (count == 0) {
return EMPTY_ARGTYPE_ARRAY;
}
ArgType[] arr = new ArgType[count];
for (int i = 0; i < count; i++) {
arr[i] = readArgType(in);
}
return arr;
}
private ArgType readArgType(DataInputStream in) throws IOException {
@@ -384,9 +410,6 @@ public class ClsSet {
if (ordinal == -1) {
return null;
}
if (ordinal >= TypeEnum.values().length) {
throw new JadxRuntimeException("Incorrect ordinal for type enum: " + ordinal);
}
switch (TypeEnum.values()[ordinal]) {
case WILDCARD:
ArgType.WildcardBound bound = ArgType.WildcardBound.getByNum(in.readByte());
@@ -414,7 +437,7 @@ public class ClsSet {
return classes[in.readInt()].getClsType();
case ARRAY:
return ArgType.array(readArgType(in));
return ArgType.array(Objects.requireNonNull(readArgType(in)));
case PRIMITIVE:
char shortName = (char) in.readByte();
@@ -474,4 +497,12 @@ public class ClsSet {
nameMap.put(cls.getName(), cls);
}
}
public int getAndroidApiLevel() {
return androidApiLevel;
}
public void setAndroidApiLevel(int androidApiLevel) {
this.androidApiLevel = androidApiLevel;
}
}
@@ -7,6 +7,9 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.intellij.lang.annotations.MagicConstant;
import jadx.api.plugins.input.data.AccessFlags;
import jadx.core.dex.instructions.args.ArgType;
/**
@@ -16,21 +19,17 @@ public class ClspClass {
private final ArgType clsType;
private final int id;
private final int accFlags;
private ArgType[] parents;
private Map<String, ClspMethod> methodsMap = Collections.emptyMap();
private List<ArgType> typeParameters = Collections.emptyList();
private ClspClassSource source;
public ClspClass(ArgType clsType, int id) {
this.clsType = clsType;
this.id = id;
this.source = ClspClassSource.APP;
}
public ClspClass(ArgType clsType, int id, ClspClassSource source) {
public ClspClass(ArgType clsType, int id, int accFlags, ClspClassSource source) {
this.clsType = clsType;
this.id = id;
this.accFlags = accFlags;
this.source = source;
}
@@ -46,6 +45,18 @@ public class ClspClass {
return id;
}
public int getAccFlags() {
return accFlags;
}
public boolean isInterface() {
return AccessFlags.hasFlag(accFlags, AccessFlags.INTERFACE);
}
public boolean hasAccFlag(@MagicConstant(flagsFromClass = AccessFlags.class) int flags) {
return AccessFlags.hasFlag(accFlags, flags);
}
public ArgType[] getParents() {
return parents;
}
@@ -13,6 +13,7 @@ import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.Consts;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.nodes.ClassNode;
@@ -106,7 +107,7 @@ public class ClspGraph {
private void addClass(ClassNode cls) {
ArgType clsType = cls.getClassInfo().getType();
String rawName = clsType.getObject();
ClspClass clspClass = new ClspClass(clsType, -1);
ClspClass clspClass = new ClspClass(clsType, -1, cls.getAccessFlags().rawValue(), ClspClassSource.APP);
clspClass.setParents(ClsSet.makeParentsArray(cls));
nameMap.put(rawName, clspClass);
}
@@ -174,6 +175,8 @@ public class ClspGraph {
return result == null ? Collections.emptySet() : result;
}
private static final Set<String> OBJECT_SINGLE_SET = Collections.singleton(Consts.CLASS_OBJECT);
private void fillSuperTypesCache() {
Map<String, Set<String>> map = new HashMap<>(nameMap.size());
Set<String> tmpSet = new HashSet<>();
@@ -182,10 +185,25 @@ public class ClspGraph {
tmpSet.clear();
addSuperTypes(cls, tmpSet);
Set<String> result;
if (tmpSet.isEmpty()) {
result = Collections.emptySet();
} else {
result = new HashSet<>(tmpSet);
int size = tmpSet.size();
switch (size) {
case 0: {
result = Collections.emptySet();
break;
}
case 1: {
String supCls = tmpSet.iterator().next();
if (supCls.equals(Consts.CLASS_OBJECT)) {
result = OBJECT_SINGLE_SET;
} else {
result = Collections.singleton(supCls);
}
break;
}
default: {
result = new HashSet<>(tmpSet);
break;
}
}
map.put(cls.getName(), result);
}
@@ -26,6 +26,7 @@ import jadx.api.plugins.input.data.annotations.EncodedType;
import jadx.api.plugins.input.data.annotations.EncodedValue;
import jadx.api.plugins.input.data.attributes.JadxAttrType;
import jadx.core.Consts;
import jadx.core.codegen.utils.CodeGenUtils;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.FieldInitInsnAttr;
@@ -45,7 +46,6 @@ import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.CodeGenUtils;
import jadx.core.utils.EncodedValueUtils;
import jadx.core.utils.Utils;
import jadx.core.utils.android.AndroidResourcesUtils;
@@ -421,18 +421,22 @@ public class ClassGen {
if (f.contains(AFlag.DONT_GENERATE)) {
return;
}
if (f.contains(JadxAttrType.ANNOTATION_LIST)
|| f.contains(AType.JADX_COMMENTS)
|| f.contains(AType.CODE_COMMENTS)
|| f.getFieldInfo().hasAlias()) {
code.newLine();
}
if (Consts.DEBUG_USAGE) {
addFieldUsageInfo(code, f);
}
if (f.getFieldInfo().hasAlias()) {
CodeGenUtils.addRenamedComment(code, f, f.getName());
}
CodeGenUtils.addComments(code, f);
annotationGen.addForField(code, f);
boolean addInfoComments = f.checkCommentsLevel(CommentsLevel.INFO);
if (f.getFieldInfo().hasAlias() && addInfoComments) {
code.newLine();
CodeGenUtils.addRenamedComment(code, f, f.getName());
}
code.startLine(f.getAccessFlags().makeString(addInfoComments));
code.startLine(f.getAccessFlags().makeString(f.checkCommentsLevel(CommentsLevel.INFO)));
useType(code, f.getType());
code.add(' ');
code.attachDefinition(f);
@@ -15,6 +15,7 @@ import jadx.api.metadata.annotations.InsnCodeOffset;
import jadx.api.metadata.annotations.VarNode;
import jadx.api.plugins.input.data.MethodHandleType;
import jadx.api.plugins.input.data.annotations.EncodedValue;
import jadx.core.codegen.utils.CodeGenUtils;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.FieldInitInsnAttr;
@@ -60,7 +61,6 @@ import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.CodeGenUtils;
import jadx.core.utils.RegionUtils;
import jadx.core.utils.exceptions.CodegenException;
import jadx.core.utils.exceptions.JadxRuntimeException;
@@ -936,7 +936,7 @@ public class InsnGen {
makeInlinedLambdaMethod(code, customNode, callMth);
}
private void makeRefLambda(ICodeWriter code, InvokeCustomNode customNode) {
private void makeRefLambda(ICodeWriter code, InvokeCustomNode customNode) throws CodegenException {
InsnNode callInsn = customNode.getCallInsn();
if (callInsn instanceof ConstructorInsn) {
MethodInfo callMth = ((ConstructorInsn) callInsn).getCallMth();
@@ -950,7 +950,7 @@ public class InsnGen {
if (customNode.getHandleType() == MethodHandleType.INVOKE_STATIC) {
useClass(code, callMth.getDeclClass());
} else {
code.add("this");
addArg(code, customNode.getArg(0));
}
code.add("::").add(callMth.getAlias());
}
@@ -22,6 +22,7 @@ import jadx.api.plugins.input.data.attributes.JadxAttrType;
import jadx.api.plugins.input.data.attributes.types.AnnotationMethodParamsAttr;
import jadx.core.Consts;
import jadx.core.Jadx;
import jadx.core.codegen.utils.CodeGenUtils;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.JadxError;
@@ -43,7 +44,6 @@ import jadx.core.dex.trycatch.CatchAttr;
import jadx.core.dex.trycatch.ExceptionHandler;
import jadx.core.dex.visitors.DepthTraversal;
import jadx.core.dex.visitors.IDexTreeVisitor;
import jadx.core.utils.CodeGenUtils;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.CodegenException;
import jadx.core.utils.exceptions.JadxOverflowException;
@@ -5,6 +5,7 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -12,14 +13,18 @@ import jadx.api.CommentsLevel;
import jadx.api.ICodeWriter;
import jadx.api.metadata.annotations.InsnCodeOffset;
import jadx.api.metadata.annotations.VarNode;
import jadx.api.plugins.input.data.AccessFlags;
import jadx.api.plugins.input.data.annotations.EncodedValue;
import jadx.api.plugins.input.data.attributes.JadxAttrType;
import jadx.core.clsp.ClspClass;
import jadx.core.codegen.utils.CodeGenUtils;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.DeclareVariablesAttr;
import jadx.core.dex.attributes.nodes.ForceReturnAttr;
import jadx.core.dex.attributes.nodes.LoopLabelAttr;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.info.FieldInfo;
import jadx.core.dex.instructions.SwitchInsn;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.instructions.args.CodeVar;
@@ -44,7 +49,6 @@ import jadx.core.dex.regions.loops.LoopRegion;
import jadx.core.dex.regions.loops.LoopType;
import jadx.core.dex.trycatch.ExceptionHandler;
import jadx.core.utils.BlockUtils;
import jadx.core.utils.CodeGenUtils;
import jadx.core.utils.RegionUtils;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.CodegenException;
@@ -268,19 +272,10 @@ public class RegionGen extends InsnGen {
private void addCaseKey(ICodeWriter code, InsnArg arg, Object k) throws CodegenException {
if (k instanceof FieldNode) {
FieldNode fn = (FieldNode) k;
if (fn.getParentClass().isEnum()) {
code.add(fn.getAlias());
} else {
staticField(code, fn.getFieldInfo());
if (mth.checkCommentsLevel(CommentsLevel.INFO)) {
// print original value, sometimes replaced with incorrect field
EncodedValue constVal = fn.get(JadxAttrType.CONSTANT_VALUE);
if (constVal != null && constVal.getValue() != null) {
code.add(" /* ").add(constVal.getValue().toString()).add(" */");
}
}
}
FieldNode fld = (FieldNode) k;
useField(code, fld.getFieldInfo(), fld);
} else if (k instanceof FieldInfo) {
useField(code, (FieldInfo) k, null);
} else if (k instanceof Integer) {
code.add(TypeGen.literalToString((Integer) k, arg.getType(), mth, fallback));
} else {
@@ -288,6 +283,28 @@ public class RegionGen extends InsnGen {
}
}
private void useField(ICodeWriter code, FieldInfo fldInfo, @Nullable FieldNode fld) throws CodegenException {
boolean isEnum;
if (fld != null) {
isEnum = fld.getParentClass().isEnum();
} else {
ClspClass clsDetails = root.getClsp().getClsDetails(fldInfo.getDeclClass().getType());
isEnum = clsDetails != null && clsDetails.hasAccFlag(AccessFlags.ENUM);
}
if (isEnum) {
code.add(fldInfo.getAlias());
return;
}
staticField(code, fldInfo);
if (fld != null && mth.checkCommentsLevel(CommentsLevel.INFO)) {
// print original value, sometimes replaced with incorrect field
EncodedValue constVal = fld.get(JadxAttrType.CONSTANT_VALUE);
if (constVal != null && constVal.getValue() != null) {
code.add(" /* ").add(constVal.getValue().toString()).add(" */");
}
}
}
public void makeTryCatch(TryCatchRegion region, ICodeWriter code) throws CodegenException {
code.startLine("try {");
@@ -56,7 +56,7 @@ public class SimpleModeHelper {
if (!block.contains(AFlag.EXC_BOTTOM_SPLITTER)) {
startLabel.set(block.getId());
}
if (prev.getSuccessors().size() == 1 && !mth.isPreExitBlocks(prev)) {
if (prev.getSuccessors().size() == 1 && !mth.isPreExitBlock(prev)) {
endGoto.set(prev.getId());
}
}
@@ -68,7 +68,7 @@ public class SimpleModeHelper {
if (block.contains(AType.EXC_HANDLER)) {
startLabel.set(block.getId());
}
if (nextBlock == null && !mth.isPreExitBlocks(block)) {
if (nextBlock == null && !mth.isPreExitBlock(block)) {
endGoto.set(block.getId());
}
prev = block;
@@ -145,7 +145,7 @@ public class SimpleModeHelper {
// DFS sort blocks to reduce goto count
private List<BlockNode> getSortedBlocks() {
List<BlockNode> list = new ArrayList<>(mth.getBasicBlocks().size());
BlockUtils.dfsVisit(mth, list::add);
BlockUtils.visitDFS(mth, list::add);
return list;
}
}
@@ -24,6 +24,7 @@ import jadx.core.codegen.json.cls.JsonClass;
import jadx.core.codegen.json.cls.JsonCodeLine;
import jadx.core.codegen.json.cls.JsonField;
import jadx.core.codegen.json.cls.JsonMethod;
import jadx.core.codegen.utils.CodeGenUtils;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.instructions.args.ArgType;
@@ -31,7 +32,6 @@ import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.CodeGenUtils;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException;
@@ -86,7 +86,7 @@ public class JsonCodeGen {
jsonCls.setInterfaces(Utils.collectionMap(cls.getInterfaces(), clsType -> getTypeAlias(classGen, clsType)));
}
ICodeWriter cw = new SimpleCodeWriter();
ICodeWriter cw = new SimpleCodeWriter(args);
CodeGenUtils.addErrorsAndComments(cw, cls);
classGen.addClassDeclaration(cw);
jsonCls.setDeclaration(cw.getCodeStr());
@@ -130,7 +130,7 @@ public class JsonCodeGen {
jsonField.setAlias(field.getAlias());
}
ICodeWriter cw = new SimpleCodeWriter();
ICodeWriter cw = new SimpleCodeWriter(args);
classGen.addField(cw, field);
jsonField.setDeclaration(cw.getCodeStr());
jsonField.setAccessFlags(field.getAccessFlags().rawValue());
@@ -154,7 +154,7 @@ public class JsonCodeGen {
jsonMth.setArguments(Utils.collectionMap(mth.getMethodInfo().getArgumentsTypes(), clsType -> getTypeAlias(classGen, clsType)));
MethodGen mthGen = new MethodGen(classGen, mth);
ICodeWriter cw = new AnnotatedCodeWriter();
ICodeWriter cw = new AnnotatedCodeWriter(args);
mthGen.addDefinition(cw);
jsonMth.setDeclaration(cw.getCodeStr());
jsonMth.setAccessFlags(mth.getAccessFlags().rawValue());
@@ -181,7 +181,7 @@ public class JsonCodeGen {
return Collections.emptyList();
}
String[] lines = codeStr.split(ICodeWriter.NL);
String[] lines = codeStr.split(args.getCodeNewLineStr());
Map<Integer, Integer> lineMapping = code.getCodeMetadata().getLineMapping();
ICodeMetadata metadata = code.getCodeMetadata();
long mthCodeOffset = mth.getMethodCodeOffset() + 16;
@@ -189,7 +189,7 @@ public class JsonCodeGen {
int linesCount = lines.length;
List<JsonCodeLine> codeLines = new ArrayList<>(linesCount);
int lineStartPos = 0;
int newLineLen = ICodeWriter.NL.length();
int newLineLen = args.getCodeNewLineStr().length();
for (int i = 0; i < linesCount; i++) {
String codeLine = lines[i];
int line = i + 2;
@@ -208,7 +208,7 @@ public class JsonCodeGen {
}
private String getTypeAlias(ClassGen classGen, ArgType clsType) {
ICodeWriter code = new SimpleCodeWriter();
ICodeWriter code = new SimpleCodeWriter(args);
classGen.useType(code, clsType);
return code.getCodeStr();
}
@@ -0,0 +1,32 @@
package jadx.core.codegen.utils;
import jadx.api.data.CommentStyle;
import jadx.api.data.ICodeComment;
public class CodeComment {
private final String comment;
private final CommentStyle style;
public CodeComment(String comment, CommentStyle style) {
this.comment = comment;
this.style = style;
}
public CodeComment(ICodeComment comment) {
this(comment.getComment(), comment.getStyle());
}
public String getComment() {
return comment;
}
public CommentStyle getStyle() {
return style;
}
@Override
public String toString() {
return "CodeComment{" + style + ": '" + comment + "'}";
}
}
@@ -1,12 +1,13 @@
package jadx.core.utils;
package jadx.core.codegen.utils;
import java.util.List;
import java.util.regex.Pattern;
import org.jetbrains.annotations.Nullable;
import jadx.api.CommentsLevel;
import jadx.api.ICodeWriter;
import jadx.api.metadata.ICodeAnnotation;
import jadx.api.data.CommentStyle;
import jadx.api.plugins.input.data.attributes.JadxAttrType;
import jadx.api.plugins.input.data.attributes.types.SourceFileAttr;
import jadx.core.dex.attributes.AType;
@@ -20,6 +21,7 @@ import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.instructions.args.SSAVar;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.ICodeNode;
import jadx.core.utils.Utils;
public class CodeGenUtils {
@@ -72,48 +74,36 @@ public class CodeGenUtils {
if (node == null) {
return;
}
List<String> comments = node.getAll(AType.CODE_COMMENTS);
if (comments.isEmpty()) {
return;
boolean startNewLine = node instanceof ICodeNode; // add on same line for instructions
for (CodeComment comment : node.getAll(AType.CODE_COMMENTS)) {
addCodeComment(code, comment, startNewLine);
}
if (node instanceof ICodeNode) {
// for classes, fields and methods add one line before node declaration
}
private static void addCodeComment(ICodeWriter code, CodeComment comment, boolean startNewLine) {
if (startNewLine) {
code.startLine();
} else {
code.add(' ');
}
if (comments.size() == 1) {
String comment = comments.get(0);
if (!comment.contains("\n")) {
code.add("// ").add(comment);
return;
}
}
addMultiLineComment(code, comments);
CommentStyle style = comment.getStyle();
appendMultiLineString(code, "", style.getStart());
appendMultiLineString(code, style.getOnNewLine(), comment.getComment());
appendMultiLineString(code, "", style.getEnd());
}
private static void addMultiLineComment(ICodeWriter code, List<String> comments) {
boolean first = true;
String indent = "";
ICodeAnnotation lineAnn = null;
for (String comment : comments) {
for (String line : comment.split("\n")) {
if (first) {
first = false;
StringBuilder buf = code.getRawBuf();
int startLinePos = buf.lastIndexOf(ICodeWriter.NL) + 1;
indent = Utils.strRepeat(" ", buf.length() - startLinePos);
if (code.isMetadataSupported()) {
lineAnn = code.getRawAnnotations().get(startLinePos);
}
} else {
code.newLine().add(indent);
if (lineAnn != null) {
code.attachLineAnnotation(lineAnn);
}
}
code.add("// ").add(line);
}
private static final Pattern NEW_LINE_PATTERN = Pattern.compile("\\R");
private static void appendMultiLineString(ICodeWriter code, String onNewLine, String str) {
String[] lines = NEW_LINE_PATTERN.split(str);
int linesCount = lines.length;
if (linesCount == 0) {
return;
}
code.add(lines[0]);
for (int i = 1; i < linesCount; i++) {
code.startLine(onNewLine);
code.add(lines[i]);
}
}
@@ -124,8 +114,7 @@ public class CodeGenUtils {
code.startLine("/* renamed from: ").add(origName);
RenameReasonAttr renameReasonAttr = node.get(AType.RENAME_REASON);
if (renameReasonAttr != null) {
code.add(" reason: ");
code.add(renameReasonAttr.getDescription());
code.add(", reason: ").add(renameReasonAttr.getDescription());
}
code.add(" */");
}
@@ -106,4 +106,5 @@ public enum AFlag {
DONT_UNLOAD_CLASS, // don't unload class after code generation (only for tests and debug!)
RESOLVE_JAVA_JSR,
COMPUTE_POST_DOM,
}
@@ -2,6 +2,7 @@ package jadx.core.dex.attributes;
import jadx.api.plugins.input.data.attributes.IJadxAttrType;
import jadx.api.plugins.input.data.attributes.IJadxAttribute;
import jadx.core.codegen.utils.CodeComment;
import jadx.core.dex.attributes.nodes.AnonymousClassAttr;
import jadx.core.dex.attributes.nodes.ClassTypeVarsAttr;
import jadx.core.dex.attributes.nodes.DeclareVariablesAttr;
@@ -44,7 +45,7 @@ import jadx.core.dex.trycatch.TryCatchBlockAttr;
public final class AType<T extends IJadxAttribute> implements IJadxAttrType<T> {
// class, method, field, insn
public static final AType<AttrList<String>> CODE_COMMENTS = new AType<>();
public static final AType<AttrList<CodeComment>> CODE_COMMENTS = new AType<>();
// class, method, field
public static final AType<RenameReasonAttr> RENAME_REASON = new AType<>();
@@ -139,14 +139,12 @@ public abstract class AttrNode implements IAttributeNode {
storage = EMPTY_ATTR_STORAGE;
}
/**
* Remove all attribute
*/
public void unloadAttributes() {
if (storage == EMPTY_ATTR_STORAGE) {
return;
}
storage.unloadAttributes();
storage.clearFlags();
unloadIfEmpty();
}
@@ -102,6 +102,10 @@ public class AttributeStorage {
flags.remove(flag);
}
public void clearFlags() {
flags.clear();
}
public <T extends IJadxAttribute> void remove(IJadxAttrType<T> type) {
if (!attributes.isEmpty()) {
writeAttributes(map -> map.remove(type));
@@ -4,7 +4,6 @@ import java.util.Objects;
import org.jetbrains.annotations.NotNull;
import jadx.api.ICodeWriter;
import jadx.core.utils.Utils;
public class JadxError implements Comparable<JadxError> {
@@ -55,7 +54,7 @@ public class JadxError implements Comparable<JadxError> {
str.append(cause.getClass());
str.append(':');
str.append(cause.getMessage());
str.append(ICodeWriter.NL);
str.append('\n');
str.append(Utils.getStackTrace(cause));
}
return str.toString();
@@ -2,7 +2,6 @@ package jadx.core.dex.attributes.nodes;
import java.util.List;
import jadx.api.ICodeWriter;
import jadx.api.plugins.input.data.ILocalVar;
import jadx.api.plugins.input.data.attributes.IJadxAttribute;
import jadx.core.dex.attributes.AType;
@@ -26,6 +25,6 @@ public class LocalVarsDebugInfoAttr implements IJadxAttribute {
@Override
public String toString() {
return "Debug Info:" + ICodeWriter.NL + " " + Utils.listToString(localVars, ICodeWriter.NL + " ");
return "Debug Info:\n " + Utils.listToString(localVars, "\n ");
}
}
@@ -1,7 +1,8 @@
package jadx.core.dex.attributes.nodes;
import jadx.api.CommentsLevel;
import jadx.api.ICodeWriter;
import jadx.api.data.CommentStyle;
import jadx.core.codegen.utils.CodeComment;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.nodes.ICodeNode;
@@ -25,7 +26,11 @@ public abstract class NotificationAttrNode extends LineAttrNode implements ICode
}
public void addCodeComment(String comment) {
addAttr(AType.CODE_COMMENTS, comment);
addAttr(AType.CODE_COMMENTS, new CodeComment(comment, CommentStyle.LINE));
}
public void addCodeComment(String comment, CommentStyle style) {
addAttr(AType.CODE_COMMENTS, new CodeComment(comment, style));
}
public void addWarnComment(String warn) {
@@ -33,7 +38,7 @@ public abstract class NotificationAttrNode extends LineAttrNode implements ICode
}
public void addWarnComment(String warn, Throwable exc) {
String commentStr = warn + ICodeWriter.NL + Utils.getStackTrace(exc);
String commentStr = warn + root().getArgs().getCodeNewLineStr() + Utils.getStackTrace(exc);
JadxCommentsAttr.add(this, CommentsLevel.WARN, commentStr);
}
@@ -3,7 +3,6 @@ package jadx.core.dex.attributes.nodes;
import java.util.ArrayList;
import java.util.List;
import jadx.api.ICodeWriter;
import jadx.api.plugins.input.data.attributes.IJadxAttribute;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.instructions.PhiInsn;
@@ -33,7 +32,7 @@ public class PhiListAttr implements IJadxAttribute {
}
}
for (PhiInsn phiInsn : list) {
sb.append(ICodeWriter.NL).append(" ").append(phiInsn);
sb.append('\n').append(" ").append(phiInsn);
}
return sb.toString();
}
@@ -2,47 +2,44 @@ package jadx.core.dex.info;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.jetbrains.annotations.Nullable;
import jadx.api.JadxArgs;
import jadx.api.plugins.input.data.annotations.EncodedValue;
import jadx.api.plugins.input.data.attributes.JadxAttrType;
import jadx.core.dex.instructions.args.LiteralArg;
import jadx.core.dex.instructions.args.PrimitiveType;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.IFieldInfoRef;
import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.visitors.prepare.CollectConstValues;
public class ConstStorage {
private static final class ValueStorage {
private final Map<Object, FieldNode> values = new ConcurrentHashMap<>();
private final Map<Object, IFieldInfoRef> values = new ConcurrentHashMap<>();
private final Set<Object> duplicates = new HashSet<>();
public Map<Object, FieldNode> getValues() {
public Map<Object, IFieldInfoRef> getValues() {
return values;
}
public FieldNode get(Object key) {
public IFieldInfoRef get(Object key) {
return values.get(key);
}
/**
* @return true if this value is duplicated
*/
public boolean put(Object value, FieldNode fld) {
public boolean put(Object value, IFieldInfoRef fld) {
if (duplicates.contains(value)) {
values.remove(value);
return true;
}
FieldNode prev = values.put(value, fld);
IFieldInfoRef prev = values.put(value, fld);
if (prev != null) {
values.remove(value);
duplicates.add(value);
@@ -56,14 +53,13 @@ public class ConstStorage {
}
void removeForCls(ClassNode cls) {
Iterator<Entry<Object, FieldNode>> it = values.entrySet().iterator();
while (it.hasNext()) {
Entry<Object, FieldNode> entry = it.next();
FieldNode field = entry.getValue();
if (field.getParentClass().equals(cls)) {
it.remove();
values.entrySet().removeIf(entry -> {
IFieldInfoRef field = entry.getValue();
if (field instanceof FieldNode) {
return ((FieldNode) field).getParentClass().equals(cls);
}
}
return false;
});
}
}
@@ -77,27 +73,24 @@ public class ConstStorage {
this.replaceEnabled = args.isReplaceConsts();
}
public void processConstFields(ClassNode cls, List<FieldNode> staticFields) {
if (!replaceEnabled || staticFields.isEmpty()) {
return;
}
for (FieldNode f : staticFields) {
Object value = getFieldConstValue(f);
if (value != null) {
addConstField(cls, f, value, f.getAccessFlags().isPublic());
}
public void addConstField(FieldNode fld, Object value, boolean isPublic) {
if (isPublic) {
addGlobalConstField(fld, value);
} else {
getClsValues(fld.getParentClass()).put(value, fld);
}
}
public void addGlobalConstField(IFieldInfoRef fld, Object value) {
globalValues.put(value, fld);
}
/**
* Use method from CollectConstValues class
*/
@Deprecated
public static @Nullable Object getFieldConstValue(FieldNode fld) {
AccessInfo accFlags = fld.getAccessFlags();
if (accFlags.isStatic() && accFlags.isFinal()) {
EncodedValue constVal = fld.get(JadxAttrType.CONSTANT_VALUE);
if (constVal != null) {
return constVal.getValue();
}
}
return null;
return CollectConstValues.getFieldConstValue(fld);
}
public void removeForClass(ClassNode cls) {
@@ -105,20 +98,11 @@ public class ConstStorage {
globalValues.removeForCls(cls);
}
private void addConstField(ClassNode cls, FieldNode fld, Object value, boolean isPublic) {
if (isPublic) {
globalValues.put(value, fld);
} else {
getClsValues(cls).put(value, fld);
}
}
private ValueStorage getClsValues(ClassNode cls) {
return classes.computeIfAbsent(cls, c -> new ValueStorage());
}
@Nullable
public FieldNode getConstField(ClassNode cls, Object value, boolean searchGlobal) {
public @Nullable IFieldInfoRef getConstField(ClassNode cls, Object value, boolean searchGlobal) {
if (!replaceEnabled) {
return null;
}
@@ -137,7 +121,7 @@ public class ConstStorage {
while (current != null) {
ValueStorage classValues = classes.get(current);
if (classValues != null) {
FieldNode field = classValues.get(value);
IFieldInfoRef field = classValues.get(value);
if (field != null) {
if (foundInGlobal) {
return null;
@@ -182,8 +166,7 @@ public class ConstStorage {
return null;
}
@Nullable
public FieldNode getConstFieldByLiteralArg(ClassNode cls, LiteralArg arg) {
public @Nullable IFieldInfoRef getConstFieldByLiteralArg(ClassNode cls, LiteralArg arg) {
if (!replaceEnabled) {
return null;
}
@@ -225,7 +208,7 @@ public class ConstStorage {
return resourcesNames;
}
public Map<Object, FieldNode> getGlobalConstFields() {
public Map<Object, IFieldInfoRef> getGlobalConstFields() {
return globalValues.getValues();
}
@@ -5,9 +5,10 @@ import java.util.Objects;
import jadx.api.plugins.input.data.IFieldRef;
import jadx.core.codegen.TypeGen;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.nodes.IFieldInfoRef;
import jadx.core.dex.nodes.RootNode;
public final class FieldInfo {
public final class FieldInfo implements IFieldInfoRef {
private final ClassInfo declClass;
private final String name;
@@ -76,6 +77,11 @@ public final class FieldInfo {
return name.equals(other.name) && type.equals(other.type);
}
@Override
public FieldInfo getFieldInfo() {
return this;
}
@Override
public boolean equals(Object o) {
if (this == o) {
@@ -2,6 +2,7 @@ package jadx.core.dex.instructions;
import java.util.Objects;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.utils.InsnUtils;
import jadx.core.utils.Utils;
@@ -23,6 +24,10 @@ public class IndexInsnNode extends InsnNode {
this.index = index;
}
public ArgType getIndexAsType() {
return (ArgType) index;
}
@Override
public IndexInsnNode copy() {
return copyCommonParams(new IndexInsnNode(insnType, index, getArgsCount()));
@@ -521,6 +521,7 @@ public class InsnDecoder {
if (payload != null) {
swInsn.attachSwitchData(new SwitchData((ISwitchPayload) payload), insn.getTarget());
}
method.add(AFlag.COMPUTE_POST_DOM);
return swInsn;
}
@@ -103,6 +103,6 @@ public class InvokeNode extends BaseInvokeNode {
@Override
public String toString() {
return baseString() + " type: " + type + " call: " + mth + attributesString();
return baseString() + " " + type + " call: " + mth + attributesString();
}
}
@@ -5,7 +5,6 @@ import java.util.List;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import jadx.api.ICodeWriter;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.InsnNode;
@@ -111,20 +110,20 @@ public class SwitchInsn extends TargetInsnNode {
int[] keys = switchData.getKeys();
if (targetBlocks != null) {
for (int i = 0; i < size; i++) {
sb.append(ICodeWriter.NL);
sb.append('\n');
sb.append(" case ").append(keys[i]).append(": goto ").append(targetBlocks[i]);
}
if (def != -1) {
sb.append(ICodeWriter.NL).append(" default: goto ").append(defTargetBlock);
sb.append('\n').append(" default: goto ").append(defTargetBlock);
}
} else {
int[] targets = switchData.getTargets();
for (int i = 0; i < size; i++) {
sb.append(ICodeWriter.NL);
sb.append('\n');
sb.append(" case ").append(keys[i]).append(": goto ").append(InsnUtils.formatOffset(targets[i]));
}
if (def != -1) {
sb.append(ICodeWriter.NL);
sb.append('\n');
sb.append(" default: goto ").append(InsnUtils.formatOffset(def));
}
}
@@ -16,8 +16,8 @@ import jadx.core.utils.InsnRemover;
import jadx.core.utils.exceptions.JadxRuntimeException;
/**
* Instruction argument,
* argument can be register, literal or instruction
* Instruction argument.
* Can be: register, literal, instruction or name
*/
public abstract class InsnArg extends Typed {
@@ -132,6 +132,10 @@ public abstract class InsnArg extends Typed {
}
InsnArg arg = wrapInsnIntoArg(insn);
InsnArg oldArg = parent.getArg(i);
if (arg.getType() == ArgType.UNKNOWN) {
// restore arg type if wrapped insn missing result
arg.setType(oldArg.getType());
}
parent.setArg(i, arg);
InsnRemover.unbindArgUsage(mth, oldArg);
if (unbind) {
@@ -209,7 +213,20 @@ public abstract class InsnArg extends Typed {
}
public boolean isZeroLiteral() {
return isLiteral() && (((LiteralArg) this)).getLiteral() == 0;
return false;
}
public boolean isZeroConst() {
if (isZeroLiteral()) {
return true;
}
if (isInsnWrap()) {
InsnNode wrapInsn = ((InsnWrapArg) this).getWrapInsn();
if (wrapInsn.getType() == InsnType.CONST) {
return wrapInsn.getArg(0).isZeroLiteral();
}
}
return false;
}
public boolean isFalse() {
@@ -265,6 +282,9 @@ public abstract class InsnArg extends Typed {
}
public boolean isSameVar(RegisterArg arg) {
if (arg == null) {
return false;
}
if (isRegister()) {
return ((RegisterArg) this).sameRegAndSVar(arg);
}
@@ -280,4 +300,8 @@ public abstract class InsnArg extends Typed {
public InsnArg duplicate() {
return this;
}
public String toShortString() {
return this.toString();
}
}
@@ -1,7 +1,5 @@
package jadx.core.dex.instructions.args;
import java.util.Objects;
import org.jetbrains.annotations.NotNull;
import jadx.core.dex.instructions.ConstStringNode;
@@ -75,10 +73,18 @@ public final class InsnWrapArg extends InsnArg {
}
@Override
public String toString() {
if (wrappedInsn.getType() == InsnType.CONST_STR && Objects.equals(type, ArgType.STRING)) {
public String toShortString() {
if (wrappedInsn.getType() == InsnType.CONST_STR) {
return "(\"" + ((ConstStringNode) wrappedInsn).getString() + "\")";
}
return "(wrap: " + type + " : " + wrappedInsn + ')';
return "(wrap:" + type + ":" + wrappedInsn.getType() + ')';
}
@Override
public String toString() {
if (wrappedInsn.getType() == InsnType.CONST_STR) {
return "(\"" + ((ConstStringNode) wrappedInsn).getString() + "\")";
}
return "(wrap:" + type + ":" + wrappedInsn + ')';
}
}
@@ -58,6 +58,11 @@ public final class LiteralArg extends InsnArg {
return true;
}
@Override
public boolean isZeroLiteral() {
return literal == 0;
}
public boolean isInteger() {
switch (type.getPrimitiveType()) {
case INT:
@@ -125,6 +130,11 @@ public final class LiteralArg extends InsnArg {
return literal == that.literal && getType().equals(that.getType());
}
@Override
public String toShortString() {
return Long.toString(literal);
}
@Override
public String toString() {
try {
@@ -48,6 +48,11 @@ public final class NamedArg extends InsnArg implements Named {
return name.equals(((NamedArg) o).name);
}
@Override
public String toShortString() {
return name;
}
@Override
public String toString() {
return '(' + name + ' ' + type + ')';
@@ -215,6 +215,16 @@ public class RegisterArg extends InsnArg implements Named {
&& Objects.equals(sVar, other.getSVar());
}
@Override
public String toShortString() {
StringBuilder sb = new StringBuilder();
sb.append("r").append(regNum);
if (sVar != null) {
sb.append('v').append(sVar.getVersion());
}
return sb.toString();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
@@ -2,6 +2,7 @@ package jadx.core.dex.instructions.args;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -23,9 +24,12 @@ import jadx.core.dex.visitors.typeinference.TypeInfo;
import jadx.core.utils.StringUtils;
import jadx.core.utils.exceptions.JadxRuntimeException;
public class SSAVar {
public class SSAVar implements Comparable<SSAVar> {
private static final Logger LOG = LoggerFactory.getLogger(SSAVar.class);
private static final Comparator<SSAVar> SSA_VAR_COMPARATOR =
Comparator.comparingInt(SSAVar::getRegNum).thenComparingInt(SSAVar::getVersion);
private final int regNum;
private final int version;
@@ -256,34 +260,6 @@ public class SSAVar {
return codeVar != null;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof SSAVar)) {
return false;
}
SSAVar ssaVar = (SSAVar) o;
return regNum == ssaVar.regNum && version == ssaVar.version;
}
@Override
public int hashCode() {
return 31 * regNum + version;
}
public String toShortString() {
return "r" + regNum + 'v' + version;
}
@Override
public String toString() {
return toShortString()
+ (StringUtils.notEmpty(getName()) ? " '" + getName() + "' " : "")
+ ' ' + typeInfo.getType();
}
public String getDetailedVarInfo(MethodNode mth) {
Set<ArgType> types = new HashSet<>();
Set<String> names = Collections.emptySet();
@@ -323,4 +299,37 @@ public class SSAVar {
}
return sb.toString();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof SSAVar)) {
return false;
}
SSAVar ssaVar = (SSAVar) o;
return regNum == ssaVar.regNum && version == ssaVar.version;
}
@Override
public int hashCode() {
return 31 * regNum + version;
}
@Override
public int compareTo(@NotNull SSAVar o) {
return SSA_VAR_COMPARATOR.compare(this, o);
}
public String toShortString() {
return "r" + regNum + 'v' + version;
}
@Override
public String toString() {
return toShortString()
+ (StringUtils.notEmpty(getName()) ? " '" + getName() + "' " : "")
+ ' ' + typeInfo.getType();
}
}
@@ -46,6 +46,11 @@ public final class BlockNode extends AttrNode implements IBlock, Comparable<Bloc
*/
private BitSet doms = EmptyBitSet.EMPTY;
/**
* Post dominators, excluding self
*/
private BitSet postDoms = EmptyBitSet.EMPTY;
/**
* Dominance frontier
*/
@@ -56,6 +61,11 @@ public final class BlockNode extends AttrNode implements IBlock, Comparable<Bloc
*/
private BlockNode idom;
/**
* Immediate post dominator
*/
private BlockNode iPostDom;
/**
* Blocks on which dominates this block
*/
@@ -165,6 +175,14 @@ public final class BlockNode extends AttrNode implements IBlock, Comparable<Bloc
this.doms = doms;
}
public BitSet getPostDoms() {
return postDoms;
}
public void setPostDoms(BitSet postDoms) {
this.postDoms = postDoms;
}
public BitSet getDomFrontier() {
return domFrontier;
}
@@ -184,6 +202,14 @@ public final class BlockNode extends AttrNode implements IBlock, Comparable<Bloc
this.idom = idom;
}
public BlockNode getIPostDom() {
return iPostDom;
}
public void setIPostDom(BlockNode iPostDom) {
this.iPostDom = iPostDom;
}
public List<BlockNode> getDominatesOn() {
return dominatesOn;
}
@@ -233,6 +259,6 @@ public final class BlockNode extends AttrNode implements IBlock, Comparable<Bloc
@Override
public String toString() {
return "B:" + cid + ':' + InsnUtils.formatOffset(startOffset);
return "B:" + id + ':' + InsnUtils.formatOffset(startOffset);
}
}
@@ -9,7 +9,6 @@ import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -19,10 +18,12 @@ import org.slf4j.LoggerFactory;
import jadx.api.DecompilationMode;
import jadx.api.ICodeCache;
import jadx.api.ICodeInfo;
import jadx.api.ICodeWriter;
import jadx.api.JadxArgs;
import jadx.api.JavaClass;
import jadx.api.impl.SimpleCodeInfo;
import jadx.api.impl.SimpleCodeWriter;
import jadx.api.metadata.ICodeAnnotation;
import jadx.api.metadata.annotations.NodeDeclareRef;
import jadx.api.plugins.input.data.IClassData;
import jadx.api.plugins.input.data.IFieldData;
import jadx.api.plugins.input.data.IMethodData;
@@ -246,19 +247,15 @@ public class ClassNode extends NotificationAttrNode
if (fields.isEmpty()) {
return;
}
List<FieldNode> staticFields = fields.stream().filter(FieldNode::isStatic).collect(Collectors.toList());
for (FieldNode f : staticFields) {
if (f.getAccessFlags().isFinal() && f.get(JadxAttrType.CONSTANT_VALUE) == null) {
// incorrect initialization will be removed if assign found in constructor
f.addAttr(EncodedValue.NULL);
// bytecode can omit field initialization to 0 (of any type)
// add explicit init to all static final fields
// incorrect initializations will be removed if assign found in class init
for (FieldNode fld : fields) {
AccessInfo accFlags = fld.getAccessFlags();
if (accFlags.isStatic() && accFlags.isFinal() && fld.get(JadxAttrType.CONSTANT_VALUE) == null) {
fld.addAttr(EncodedValue.NULL);
}
}
try {
// process const fields
root().getConstValues().processConstFields(this, staticFields);
} catch (Exception e) {
this.addWarnComment("Failed to load initial values for static fields", e);
}
}
private boolean checkSourceFilenameAttr() {
@@ -312,6 +309,8 @@ public class ClassNode extends NotificationAttrNode
return decompile(true);
}
private static final Object DECOMPILE_WITH_MODE_SYNC = new Object();
/**
* WARNING: Slow operation! Use with caution!
*/
@@ -320,15 +319,18 @@ public class ClassNode extends NotificationAttrNode
if (mode == baseMode) {
return decompile(true);
}
JadxArgs args = root.getArgs();
try {
unload();
args.setDecompilationMode(mode);
ProcessClass process = new ProcessClass(args);
process.initPasses(root);
return process.generateCode(this);
} finally {
args.setDecompilationMode(baseMode);
synchronized (DECOMPILE_WITH_MODE_SYNC) {
JadxArgs args = root.getArgs();
try {
unload();
args.setDecompilationMode(mode);
ProcessClass process = new ProcessClass(args);
process.initPasses(root);
return process.generateCode(this);
} finally {
args.setDecompilationMode(baseMode);
unload();
}
}
}
@@ -383,22 +385,46 @@ public class ClassNode extends NotificationAttrNode
return code;
}
}
ICodeInfo codeInfo;
try {
if (Consts.DEBUG) {
LOG.debug("Decompiling class: {}", this);
}
codeInfo = root.getProcessClasses().generateCode(this);
} catch (Throwable e) {
addError("Code generation failed", e);
codeInfo = new SimpleCodeInfo(Utils.getStackTrace(e));
}
ICodeInfo codeInfo = generateClassCode();
if (codeInfo != ICodeInfo.EMPTY) {
codeCache.add(clsRawName, codeInfo);
}
return codeInfo;
}
private ICodeInfo generateClassCode() {
try {
if (Consts.DEBUG) {
LOG.debug("Decompiling class: {}", this);
}
ICodeInfo codeInfo = root.getProcessClasses().generateCode(this);
processDefinitionAnnotations(codeInfo);
return codeInfo;
} catch (Throwable e) {
addError("Code generation failed", e);
return new SimpleCodeInfo(Utils.getStackTrace(e));
}
}
/**
* Save node definition positions found in code
*/
private static void processDefinitionAnnotations(ICodeInfo codeInfo) {
Map<Integer, ICodeAnnotation> annotations = codeInfo.getCodeMetadata().getAsMap();
if (annotations.isEmpty()) {
return;
}
for (Map.Entry<Integer, ICodeAnnotation> entry : annotations.entrySet()) {
ICodeAnnotation ann = entry.getValue();
if (ann.getAnnType() == AnnType.DECLARATION) {
NodeDeclareRef declareRef = (NodeDeclareRef) ann;
int pos = entry.getKey();
declareRef.setDefPos(pos);
declareRef.getNode().setDefPosition(pos);
}
}
}
@Nullable
public ICodeInfo getCodeFromCache() {
ICodeCache codeCache = root().getCodeCache();
@@ -432,7 +458,7 @@ public class ClassNode extends NotificationAttrNode
}
methods.forEach(MethodNode::unload);
innerClasses.forEach(ClassNode::unload);
fields.forEach(FieldNode::unloadAttributes);
fields.forEach(FieldNode::unload);
unloadAttributes();
setState(NOT_LOADED);
this.loadStage = LoadStage.NONE;
@@ -482,17 +508,15 @@ public class ClassNode extends NotificationAttrNode
fields.add(fld);
}
public FieldNode getConstField(Object obj) {
public @Nullable IFieldInfoRef getConstField(Object obj) {
return getConstField(obj, true);
}
@Nullable
public FieldNode getConstField(Object obj, boolean searchGlobal) {
public @Nullable IFieldInfoRef getConstField(Object obj, boolean searchGlobal) {
return root().getConstValues().getConstField(this, obj, searchGlobal);
}
@Nullable
public FieldNode getConstFieldByLiteralArg(LiteralArg arg) {
public @Nullable IFieldInfoRef getConstFieldByLiteralArg(LiteralArg arg) {
return root().getConstValues().getConstFieldByLiteralArg(this, arg);
}
@@ -807,33 +831,29 @@ public class ClassNode extends NotificationAttrNode
public String getDisassembledCode() {
if (smali == null) {
StringBuilder sb = new StringBuilder();
getDisassembledCode(sb);
sb.append(ICodeWriter.NL);
SimpleCodeWriter code = new SimpleCodeWriter(root.getArgs());
getDisassembledCode(code);
Set<ClassNode> allInlinedClasses = new LinkedHashSet<>();
getInnerAndInlinedClassesRecursive(allInlinedClasses);
for (ClassNode innerClass : allInlinedClasses) {
innerClass.getDisassembledCode(sb);
sb.append(ICodeWriter.NL);
innerClass.getDisassembledCode(code);
}
smali = sb.toString();
smali = code.finish().getCodeStr();
}
return smali;
}
protected void getDisassembledCode(StringBuilder sb) {
protected void getDisassembledCode(SimpleCodeWriter code) {
if (clsData == null) {
sb.append(String.format("###### Class %s is created by jadx", getFullName()));
code.startLine(String.format("###### Class %s is created by jadx", getFullName()));
return;
}
sb.append(String.format("###### Class %s (%s)", getFullName(), getRawName()));
sb.append(ICodeWriter.NL);
code.startLine(String.format("###### Class %s (%s)", getFullName(), getRawName()));
try {
sb.append(clsData.getDisassembledCode());
code.startLine(clsData.getDisassembledCode());
} catch (Throwable e) {
sb.append("Failed to disassemble class:");
sb.append(ICodeWriter.NL);
sb.append(Utils.getStackTrace(e));
code.startLine("Failed to disassemble class:");
code.startLine(Utils.getStackTrace(e));
}
}
@@ -12,7 +12,7 @@ import jadx.core.dex.info.FieldInfo;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.utils.ListUtils;
public class FieldNode extends NotificationAttrNode implements ICodeNode {
public class FieldNode extends NotificationAttrNode implements ICodeNode, IFieldInfoRef {
private final ClassNode parentClass;
private final FieldInfo fieldInfo;
@@ -38,10 +38,15 @@ public class FieldNode extends NotificationAttrNode implements ICodeNode {
this.accFlags = new AccessInfo(accessFlags, AFType.FIELD);
}
public void unload() {
unloadAttributes();
}
public void updateType(ArgType type) {
this.type = type;
}
@Override
public FieldInfo getFieldInfo() {
return fieldInfo;
}
@@ -0,0 +1,10 @@
package jadx.core.dex.nodes;
import jadx.core.dex.info.FieldInfo;
/**
* Common interface for FieldInfo and FieldNode
*/
public interface IFieldInfoRef {
FieldInfo getFieldInfo();
}
@@ -10,7 +10,6 @@ import java.util.function.Function;
import org.jetbrains.annotations.Nullable;
import jadx.api.ICodeWriter;
import jadx.api.plugins.input.insns.InsnData;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
@@ -563,9 +562,9 @@ public class InsnNode extends LineAttrNode {
return false;
}
// wrap args
String separator = ICodeWriter.NL + " ";
String separator = "\n ";
sb.append(separator).append(Utils.listToString(arguments, separator));
sb.append(ICodeWriter.NL);
sb.append('\n');
return true;
}
@@ -253,7 +253,7 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
return mthInfo.getReturnType().equals(ArgType.VOID);
}
public List<VarNode> collectArgsWithoutLoading() {
public List<VarNode> collectArgNodes() {
ICodeInfo codeInfo = getTopParentClass().getCode();
int mthDefPos = getDefPosition();
int lineEndPos = CodeUtils.getLineEndForPos(codeInfo.getCodeStr(), mthDefPos);
@@ -360,10 +360,13 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
public void setBasicBlocks(List<BlockNode> blocks) {
this.blocks = blocks;
int i = 0;
for (BlockNode block : blocks) {
block.setId(i);
i++;
updateBlockIds(blocks);
}
public void updateBlockIds(List<BlockNode> blocks) {
int count = blocks.size();
for (int i = 0; i < count; i++) {
blocks.get(i).setId(i);
}
}
@@ -391,7 +394,7 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
return exitBlock.getPredecessors();
}
public boolean isPreExitBlocks(BlockNode block) {
public boolean isPreExitBlock(BlockNode block) {
List<BlockNode> successors = block.getSuccessors();
if (successors.size() == 1) {
return successors.get(0).equals(exitBlock);
@@ -15,7 +15,12 @@ import jadx.core.utils.exceptions.CodegenException;
public final class SwitchRegion extends AbstractRegion implements IBranchRegion {
public static final Object DEFAULT_CASE_KEY = new Object();
public static final Object DEFAULT_CASE_KEY = new Object() {
@Override
public String toString() {
return "default";
}
};
private final BlockNode header;
@@ -91,7 +96,7 @@ public final class SwitchRegion extends AbstractRegion implements IBranchRegion
for (CaseInfo caseInfo : cases) {
List<String> keyStrings = Utils.collectionMap(caseInfo.getKeys(),
k -> k == DEFAULT_CASE_KEY ? "default" : k.toString());
sb.append(ICodeWriter.NL).append(" case ")
sb.append("\n case ")
.append(Utils.listToString(keyStrings))
.append(" -> ").append(caseInfo.getContainer());
}
@@ -14,6 +14,7 @@ import jadx.api.data.ICodeComment;
import jadx.api.data.ICodeData;
import jadx.api.data.IJavaCodeRef;
import jadx.api.data.IJavaNodeRef;
import jadx.core.codegen.utils.CodeComment;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.IAttributeNode;
import jadx.core.dex.nodes.ClassNode;
@@ -58,7 +59,7 @@ public class AttachCommentsVisitor extends AbstractVisitor {
IJavaNodeRef nodeRef = comment.getNodeRef();
switch (nodeRef.getType()) {
case CLASS:
addComment(cls, comment.getComment());
addComment(cls, comment);
break;
case FIELD:
@@ -66,7 +67,7 @@ public class AttachCommentsVisitor extends AbstractVisitor {
if (fieldNode == null) {
LOG.warn("Field reference not found: {}", nodeRef);
} else {
addComment(fieldNode, comment.getComment());
addComment(fieldNode, comment);
}
break;
@@ -77,7 +78,7 @@ public class AttachCommentsVisitor extends AbstractVisitor {
} else {
IJavaCodeRef codeRef = comment.getCodeRef();
if (codeRef == null) {
addComment(methodNode, comment.getComment());
addComment(methodNode, comment);
} else {
processCustomAttach(methodNode, codeRef, comment);
}
@@ -101,7 +102,7 @@ public class AttachCommentsVisitor extends AbstractVisitor {
switch (attachType) {
case INSN: {
InsnNode insn = getInsnByOffset(mth, codeRef.getIndex());
addComment(insn, comment.getComment());
addComment(insn, comment);
break;
}
default:
@@ -109,11 +110,11 @@ public class AttachCommentsVisitor extends AbstractVisitor {
}
}
private static void addComment(@Nullable IAttributeNode node, String comment) {
private static void addComment(@Nullable IAttributeNode node, ICodeComment comment) {
if (node == null) {
return;
}
node.addAttr(AType.CODE_COMMENTS, comment);
node.addAttr(AType.CODE_COMMENTS, new CodeComment(comment));
}
private List<ICodeComment> getCommentsData(ClassNode cls) {
@@ -1,7 +1,6 @@
package jadx.core.dex.visitors;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.jetbrains.annotations.Nullable;
@@ -16,16 +15,12 @@ import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.instructions.InsnType;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.trycatch.CatchAttr;
import jadx.core.dex.trycatch.ExcHandlerAttr;
import jadx.core.dex.trycatch.ExceptionHandler;
import jadx.core.dex.visitors.typeinference.TypeCompare;
import jadx.core.dex.visitors.typeinference.TypeCompareEnum;
import jadx.core.utils.exceptions.JadxException;
import jadx.core.utils.exceptions.JadxRuntimeException;
import static jadx.core.dex.visitors.ProcessInstructionsVisitor.getNextInsnOffset;
@@ -122,7 +117,6 @@ public class AttachTryCatchVisitor extends AbstractVisitor {
if (allHandlerOffset >= 0) {
Utils.addToList(list, createHandler(mth, insnByOffset, allHandlerOffset, null));
}
checkAndFilterHandlers(mth, list);
return list;
}
@@ -149,45 +143,6 @@ public class AttachTryCatchVisitor extends AbstractVisitor {
return handler;
}
private static void checkAndFilterHandlers(MethodNode mth, List<ExceptionHandler> list) {
if (list.size() <= 1) {
return;
}
// Remove shadowed handlers (with same or narrow type compared to previous)
TypeCompare typeCompare = mth.root().getTypeCompare();
Iterator<ExceptionHandler> it = list.iterator();
ArgType maxType = null;
while (it.hasNext()) {
ExceptionHandler handler = it.next();
ArgType maxCatch = maxCatchFromHandler(handler, typeCompare);
if (maxType == null) {
maxType = maxCatch;
} else {
TypeCompareEnum result = typeCompare.compareObjects(maxType, maxCatch);
if (result.isWiderOrEqual()) {
if (Consts.DEBUG_EXC_HANDLERS) {
LOG.debug("Removed shadowed catch handler: {}, from list: {}", handler, list);
}
it.remove();
}
}
}
}
private static ArgType maxCatchFromHandler(ExceptionHandler handler, TypeCompare typeCompare) {
List<ClassInfo> catchTypes = handler.getCatchTypes();
if (catchTypes.isEmpty()) {
return ArgType.THROWABLE;
}
if (catchTypes.size() == 1) {
return catchTypes.get(0).getType();
}
return catchTypes.stream()
.map(ClassInfo::getType)
.max(typeCompare.getComparator())
.orElseThrow(() -> new JadxRuntimeException("Failed to get max type from catch list: " + catchTypes));
}
private static InsnNode insertNOP(InsnNode[] insnByOffset, int offset) {
InsnNode nop = new InsnNode(InsnType.NOP, 0);
nop.setOffset(offset);
@@ -18,7 +18,7 @@ import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.instructions.args.SSAVar;
import jadx.core.dex.instructions.mods.ConstructorInsn;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.IFieldInfoRef;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.visitors.finaly.MarkFinallyVisitor;
@@ -26,6 +26,7 @@ import jadx.core.dex.visitors.ssa.SSATransform;
import jadx.core.dex.visitors.typeinference.TypeInferenceVisitor;
import jadx.core.utils.InsnRemover;
import jadx.core.utils.exceptions.JadxException;
import jadx.core.utils.exceptions.JadxRuntimeException;
@JadxVisitor(
name = "Constants Inline",
@@ -73,8 +74,7 @@ public class ConstInlineVisitor extends AbstractVisitor {
if (!constArg.isLiteral()) {
return;
}
long lit = ((LiteralArg) constArg).getLiteral();
if (lit == 0 && forbidNullInlines(sVar)) {
if (constArg.isZeroLiteral() && forbidNullInlines(sVar)) {
// all usages forbids inlining
return;
}
@@ -82,7 +82,7 @@ public class ConstInlineVisitor extends AbstractVisitor {
}
case CONST_STR: {
String s = ((ConstStringNode) insn).getString();
FieldNode f = mth.getParentClass().getConstField(s);
IFieldInfoRef f = mth.getParentClass().getConstField(s);
if (f == null) {
InsnNode copy = insn.copyWithoutResult();
constArg = InsnArg.wrapArg(copy);
@@ -90,7 +90,7 @@ public class ConstInlineVisitor extends AbstractVisitor {
InsnNode constGet = new IndexInsnNode(InsnType.SGET, f.getFieldInfo(), 0);
constArg = InsnArg.wrapArg(constGet);
constArg.setType(ArgType.STRING);
onSuccess = () -> f.addUseIn(mth);
onSuccess = () -> ModVisitor.addFieldUsage(f, mth);
}
break;
}
@@ -134,20 +134,15 @@ public class ConstInlineVisitor extends AbstractVisitor {
}
private static boolean forbidNullArgInline(InsnNode insn, RegisterArg useArg) {
switch (insn.getType()) {
case MOVE:
case CAST:
case CHECK_CAST:
// result is null, chain checks
return forbidNullInlines(insn.getResult().getSVar());
default:
if (!canUseNull(insn, useArg)) {
useArg.add(AFlag.DONT_INLINE_CONST);
return true;
}
return false;
if (insn.getType() == InsnType.MOVE) {
// result is null, chain checks
return forbidNullInlines(insn.getResult().getSVar());
}
if (!canUseNull(insn, useArg)) {
useArg.add(AFlag.DONT_INLINE_CONST);
return true;
}
return false;
}
private static boolean canUseNull(InsnNode insn, RegisterArg useArg) {
@@ -256,7 +251,7 @@ public class ConstInlineVisitor extends AbstractVisitor {
return false;
}
// arg replaced, made some optimizations
FieldNode fieldNode = null;
IFieldInfoRef fieldNode = null;
ArgType litArgType = litArg.getType();
if (litArgType.isTypeKnown()) {
fieldNode = mth.getParentClass().getConstFieldByLiteralArg(litArg);
@@ -266,12 +261,10 @@ public class ConstInlineVisitor extends AbstractVisitor {
if (fieldNode != null) {
IndexInsnNode sgetInsn = new IndexInsnNode(InsnType.SGET, fieldNode.getFieldInfo(), 0);
if (litArg.wrapInstruction(mth, sgetInsn) != null) {
fieldNode.addUseIn(mth);
ModVisitor.addFieldUsage(fieldNode, mth);
}
} else {
if (needExplicitCast(useInsn, litArg)) {
litArg.add(AFlag.EXPLICIT_PRIMITIVE_TYPE);
}
addExplicitCast(useInsn, litArg);
}
} else {
if (!useInsn.replaceArg(arg, constArg.duplicate())) {
@@ -282,18 +275,33 @@ public class ConstInlineVisitor extends AbstractVisitor {
return true;
}
private static boolean needExplicitCast(InsnNode insn, LiteralArg arg) {
private static void addExplicitCast(InsnNode insn, LiteralArg arg) {
if (insn instanceof BaseInvokeNode) {
BaseInvokeNode callInsn = (BaseInvokeNode) insn;
MethodInfo callMth = callInsn.getCallMth();
int offset = callInsn.getFirstArgOffset();
int argIndex = insn.getArgIndex(arg);
ArgType argType = callMth.getArgumentsTypes().get(argIndex - offset);
if (argType.isPrimitive()) {
arg.setType(argType);
return argType.equals(ArgType.BYTE);
if (callInsn.getInstanceArg() == arg) {
// instance arg is null, force cast
if (!arg.isZeroLiteral()) {
throw new JadxRuntimeException("Unexpected instance arg in invoke");
}
ArgType castType = callMth.getDeclClass().getType();
InsnNode castInsn = new IndexInsnNode(InsnType.CAST, castType, 1);
castInsn.addArg(arg);
castInsn.add(AFlag.EXPLICIT_CAST);
InsnArg wrapCast = InsnArg.wrapArg(castInsn);
wrapCast.setType(castType);
insn.replaceArg(arg, wrapCast);
} else {
int offset = callInsn.getFirstArgOffset();
int argIndex = insn.getArgIndex(arg);
ArgType argType = callMth.getArgumentsTypes().get(argIndex - offset);
if (argType.isPrimitive()) {
arg.setType(argType);
if (argType.equals(ArgType.BYTE)) {
arg.add(AFlag.EXPLICIT_PRIMITIVE_TYPE);
}
}
}
}
return false;
}
}
@@ -199,7 +199,7 @@ public class DotGraphVisitor extends AbstractVisitor {
dot.add("color=red,");
}
dot.add("label=\"{");
dot.add(String.valueOf(block.getCId())).add("\\:\\ ");
dot.add(String.valueOf(block.getId())).add("\\:\\ ");
dot.add(InsnUtils.formatOffset(block.getStartOffset()));
if (!attrs.isEmpty()) {
dot.add('|').add(attrs);
@@ -208,6 +208,8 @@ public class DotGraphVisitor extends AbstractVisitor {
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");
@@ -230,10 +232,10 @@ public class DotGraphVisitor extends AbstractVisitor {
if (PRINT_DOMINATORS) {
for (BlockNode c : block.getDominatesOn()) {
conn.startLine(block.getCId() + " -> " + c.getCId() + "[color=green];");
conn.startLine(block.getId() + " -> " + c.getId() + "[color=green];");
}
for (BlockNode dom : BlockUtils.bitSetToBlocks(mth, block.getDomFrontier())) {
conn.startLine("f_" + block.getCId() + " -> f_" + dom.getCId() + "[color=blue];");
conn.startLine("f_" + block.getId() + " -> f_" + dom.getId() + "[color=blue];");
}
}
}
@@ -273,7 +275,7 @@ public class DotGraphVisitor extends AbstractVisitor {
private String makeName(IContainer c) {
String name;
if (c instanceof BlockNode) {
name = "Node_" + ((BlockNode) c).getCId();
name = "Node_" + ((BlockNode) c).getId();
} else if (c instanceof IBlock) {
name = "Node_" + c.getClass().getSimpleName() + '_' + c.hashCode();
} else {
@@ -317,8 +319,7 @@ public class DotGraphVisitor extends AbstractVisitor {
.replace("\"", "\\\"")
.replace("-", "\\-")
.replace("|", "\\|")
.replace(ICodeWriter.NL, NL)
.replace("\n", NL);
.replaceAll("\\R", NL);
}
}
}
@@ -99,7 +99,7 @@ public class MarkMethodsForInline extends AbstractVisitor {
&& retInsn.getArg(0).isSameVar(firstInsn.getResult())
&& firstInsn.getArg(0).isSameVar(mthRegs.get(0));
case SGET:
return mthRegs.size() == 0
return mthRegs.isEmpty()
&& retInsn.getArg(0).isSameVar(firstInsn.getResult());
case IPUT:
@@ -113,7 +113,7 @@ public class MarkMethodsForInline extends AbstractVisitor {
&& firstInsn.getArg(0).isSameVar(mthRegs.get(0));
case INVOKE:
return mthRegs.size() >= 1
return !mthRegs.isEmpty()
&& firstInsn.getArg(0).isSameVar(mthRegs.get(0))
&& retInsn.getArg(0).isSameVar(firstInsn.getResult());
default:
@@ -5,7 +5,9 @@ import java.util.List;
import java.util.Map;
import java.util.function.Function;
import jadx.api.ICodeWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.Consts;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.info.MethodInfo;
@@ -28,6 +30,7 @@ import jadx.core.dex.visitors.methods.MutableMethodDetails;
import jadx.core.dex.visitors.shrink.CodeShrinkVisitor;
import jadx.core.dex.visitors.typeinference.TypeCompare;
import jadx.core.dex.visitors.typeinference.TypeCompareEnum;
import jadx.core.utils.InsnUtils;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException;
@@ -43,6 +46,8 @@ import jadx.core.utils.exceptions.JadxRuntimeException;
}
)
public class MethodInvokeVisitor extends AbstractVisitor {
private static final Logger LOG = LoggerFactory.getLogger(MethodInvokeVisitor.class);
private RootNode root;
@Override
@@ -116,7 +121,8 @@ public class MethodInvokeVisitor extends AbstractVisitor {
int argsOffset = invokeInsn.getFirstArgOffset();
List<ArgType> compilerVarTypes = collectCompilerVarTypes(invokeInsn, argsOffset);
List<ArgType> castTypes = searchCastTypes(parentMth, effectiveMthDetails, effectiveOverloadMethods, compilerVarTypes);
applyArgsCast(invokeInsn, argsOffset, compilerVarTypes, castTypes);
List<ArgType> resultCastTypes = expandTypes(parentMth, effectiveMthDetails, castTypes);
applyArgsCast(invokeInsn, argsOffset, compilerVarTypes, resultCastTypes);
}
/**
@@ -180,7 +186,13 @@ public class MethodInvokeVisitor extends AbstractVisitor {
if (arg.isLiteral() && compilerType.isPrimitive() && castType.isPrimitive()) {
arg.setType(castType);
arg.add(AFlag.EXPLICIT_PRIMITIVE_TYPE);
} else if (InsnUtils.isWrapped(arg, InsnType.CHECK_CAST)) {
IndexInsnNode wrapInsn = ((IndexInsnNode) ((InsnWrapArg) arg).getWrapInsn());
wrapInsn.updateIndex(castType);
} else {
if (Consts.DEBUG_TYPE_INFERENCE) {
LOG.info("Insert cast for invoke insn arg: {}, insn: {}", arg, invokeInsn);
}
InsnNode castInsn = new IndexInsnNode(InsnType.CAST, castType, 1);
castInsn.addArg(arg);
castInsn.add(AFlag.EXPLICIT_CAST);
@@ -243,7 +255,7 @@ public class MethodInvokeVisitor extends AbstractVisitor {
private List<ArgType> searchCastTypes(MethodNode parentMth, IMethodDetails mthDetails, List<IMethodDetails> overloadedMethods,
List<ArgType> compilerVarTypes) {
// try compile types
// try compiler types
if (isOverloadResolved(mthDetails, overloadedMethods, compilerVarTypes)) {
return compilerVarTypes;
}
@@ -277,10 +289,10 @@ public class MethodInvokeVisitor extends AbstractVisitor {
if (Consts.DEBUG_OVERLOADED_CASTS) {
// TODO: try to minimize casts count
parentMth.addDebugComment("Failed to find minimal casts for resolve overloaded methods, cast all args instead"
+ ICodeWriter.NL + " method: " + mthDetails
+ ICodeWriter.NL + " arg types: " + compilerVarTypes
+ ICodeWriter.NL + " candidates:"
+ ICodeWriter.NL + " " + Utils.listToString(overloadedMethods, ICodeWriter.NL + " "));
+ "\n method: " + mthDetails
+ "\n arg types: " + compilerVarTypes
+ "\n candidates:"
+ "\n " + Utils.listToString(overloadedMethods, "\n "));
}
// not resolved -> cast all args
return mthDetails.getArgTypes();
@@ -300,6 +312,27 @@ public class MethodInvokeVisitor extends AbstractVisitor {
return changed;
}
/**
* Use generified types if available
*/
private List<ArgType> expandTypes(MethodNode parentMth, IMethodDetails methodDetails, List<ArgType> castTypes) {
TypeCompare typeCompare = parentMth.root().getTypeCompare();
List<ArgType> mthArgTypes = methodDetails.getArgTypes();
int argsCount = castTypes.size();
List<ArgType> list = new ArrayList<>(argsCount);
for (int i = 0; i < argsCount; i++) {
ArgType mthType = mthArgTypes.get(i);
ArgType castType = castTypes.get(i);
TypeCompareEnum result = typeCompare.compareTypes(mthType, castType);
if (result == TypeCompareEnum.NARROW_BY_GENERIC) {
list.add(mthType);
} else {
list.add(castType);
}
}
return list;
}
private boolean isOverloadResolved(IMethodDetails expectedMthDetails, List<IMethodDetails> overloadedMethods, List<ArgType> castTypes) {
if (overloadedMethods.isEmpty()) {
return false;
@@ -387,12 +420,22 @@ public class MethodInvokeVisitor extends AbstractVisitor {
}
if (arg instanceof InsnWrapArg) {
InsnWrapArg wrapArg = (InsnWrapArg) arg;
InsnNode wrapInsn = wrapArg.getWrapInsn();
if (wrapInsn.getResult() != null) {
return wrapInsn.getResult().getType();
}
return arg.getType();
return getInsnCompilerType(arg, wrapArg.getWrapInsn());
}
throw new JadxRuntimeException("Unknown var type for: " + arg);
}
private static ArgType getInsnCompilerType(InsnArg arg, InsnNode insn) {
switch (insn.getType()) {
case CAST:
case CHECK_CAST:
return ((IndexInsnNode) insn).getIndexAsType();
default:
if (insn.getResult() != null) {
return insn.getResult().getType();
}
return arg.getType();
}
}
}
@@ -44,6 +44,7 @@ import jadx.core.dex.instructions.mods.TernaryInsn;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.IFieldInfoRef;
import jadx.core.dex.nodes.IMethodDetails;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
@@ -239,10 +240,10 @@ public class ModVisitor extends AbstractVisitor {
int[] keys = insn.getKeys();
int len = keys.length;
for (int k = 0; k < len; k++) {
FieldNode f = parentClass.getConstField(keys[k]);
IFieldInfoRef f = parentClass.getConstField(keys[k]);
if (f != null) {
insn.modifyKey(k, f);
f.addUseIn(mth);
addFieldUsage(f, mth);
}
}
}
@@ -313,7 +314,7 @@ public class ModVisitor extends AbstractVisitor {
}
return new EncodedValue(EncodedType.ENCODED_ARRAY, listVal);
}
FieldNode constField = parentCls.getConstField(encodedValue.getValue());
IFieldInfoRef constField = parentCls.getConstField(encodedValue.getValue());
if (constField != null) {
return new EncodedValue(EncodedType.ENCODED_FIELD, constField.getFieldInfo());
}
@@ -321,7 +322,7 @@ public class ModVisitor extends AbstractVisitor {
}
private static void replaceConst(MethodNode mth, ClassNode parentClass, BlockNode block, int i, InsnNode insn) {
FieldNode f;
IFieldInfoRef f;
if (insn.getType() == InsnType.CONST_STR) {
String s = ((ConstStringNode) insn).getString();
f = parentClass.getConstField(s);
@@ -335,7 +336,7 @@ public class ModVisitor extends AbstractVisitor {
InsnNode inode = new IndexInsnNode(InsnType.SGET, f.getFieldInfo(), 0);
inode.setResult(insn.getResult());
replaceInsn(mth, block, i, inode);
f.addUseIn(mth);
addFieldUsage(f, mth);
}
}
@@ -345,11 +346,11 @@ public class ModVisitor extends AbstractVisitor {
}
InsnArg litArg = arithNode.getArg(1);
if (litArg.isLiteral()) {
FieldNode f = parentClass.getConstFieldByLiteralArg((LiteralArg) litArg);
IFieldInfoRef f = parentClass.getConstFieldByLiteralArg((LiteralArg) litArg);
if (f != null) {
InsnNode fGet = new IndexInsnNode(InsnType.SGET, f.getFieldInfo(), 0);
if (arithNode.replaceArg(litArg, InsnArg.wrapArg(fGet))) {
f.addUseIn(mth);
addFieldUsage(f, mth);
}
}
}
@@ -389,6 +390,11 @@ public class ModVisitor extends AbstractVisitor {
private static void removeCheckCast(MethodNode mth, BlockNode block, int i, IndexInsnNode insn) {
InsnArg castArg = insn.getArg(0);
if (castArg.isZeroLiteral()) {
// always keep cast for 'null'
insn.add(AFlag.EXPLICIT_CAST);
return;
}
ArgType castType = (ArgType) insn.getIndex();
if (!ArgType.isCastNeeded(mth.root(), castArg.getType(), castType)) {
RegisterArg result = insn.getResult();
@@ -561,11 +567,11 @@ public class ModVisitor extends AbstractVisitor {
InsnNode filledArr = new FilledNewArrayNode(elType, list.size());
filledArr.setResult(newArrayNode.getResult().duplicate());
for (LiteralArg arg : list) {
FieldNode f = mth.getParentClass().getConstFieldByLiteralArg(arg);
IFieldInfoRef f = mth.getParentClass().getConstFieldByLiteralArg(arg);
if (f != null) {
InsnNode fGet = new IndexInsnNode(InsnType.SGET, f.getFieldInfo(), 0);
filledArr.addArg(InsnArg.wrapArg(fGet));
f.addUseIn(mth);
addFieldUsage(f, mth);
} else {
filledArr.addArg(arg.duplicate());
}
@@ -602,4 +608,10 @@ public class ModVisitor extends AbstractVisitor {
}
block.copyAttributeFrom(insn, AType.CODE_COMMENTS); // save comment
}
public static void addFieldUsage(IFieldInfoRef fieldData, MethodNode mth) {
if (fieldData instanceof FieldNode) {
((FieldNode) fieldData).addUseIn(mth);
}
}
}
@@ -25,7 +25,9 @@ import jadx.core.dex.attributes.nodes.LineAttrNode;
import jadx.core.dex.info.FieldInfo;
import jadx.core.dex.instructions.ArithNode;
import jadx.core.dex.instructions.ArithOp;
import jadx.core.dex.instructions.IndexInsnNode;
import jadx.core.dex.instructions.InsnType;
import jadx.core.dex.instructions.InvokeNode;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.InsnWrapArg;
@@ -82,6 +84,7 @@ public class PrepareForCodeGen extends AbstractVisitor {
removeParenthesis(block);
modifyArith(block);
checkConstUsage(block);
addNullCasts(mth, block);
}
moveConstructorInConstructor(mth);
collectFieldsUsageInAnnotations(mth, mth);
@@ -379,4 +382,27 @@ public class PrepareForCodeGen extends AbstractVisitor {
break;
}
}
private void addNullCasts(MethodNode mth, BlockNode block) {
for (InsnNode insn : block.getInstructions()) {
switch (insn.getType()) {
case INVOKE:
verifyNullCast(mth, ((InvokeNode) insn).getInstanceArg());
break;
case ARRAY_LENGTH:
verifyNullCast(mth, insn.getArg(0));
break;
}
}
}
private void verifyNullCast(MethodNode mth, InsnArg arg) {
if (arg != null && arg.isZeroConst()) {
ArgType castType = arg.getType();
IndexInsnNode castInsn = new IndexInsnNode(InsnType.CAST, castType, 1);
castInsn.addArg(InsnArg.lit(0, castType));
arg.wrapInstruction(mth, castInsn);
}
}
}
@@ -43,9 +43,7 @@ public class ProcessAnonymous extends AbstractVisitor {
if (!inlineAnonymousClasses) {
return;
}
for (ClassNode cls : root.getClasses()) {
markAnonymousClass(cls);
}
root.getClasses().forEach(ProcessAnonymous::processClass);
mergeAnonymousDeps(root);
}
@@ -59,10 +57,18 @@ public class ProcessAnonymous extends AbstractVisitor {
}
private void visitClassAndInners(ClassNode cls) {
markAnonymousClass(cls);
processClass(cls);
cls.getInnerClasses().forEach(this::visitClassAndInners);
}
private static void processClass(ClassNode cls) {
try {
markAnonymousClass(cls);
} catch (Throwable e) {
cls.addError("Anonymous visitor error", e);
}
}
private static void markAnonymousClass(ClassNode cls) {
if (!canBeAnonymous(cls)) {
return;
@@ -273,6 +279,10 @@ public class ProcessAnonymous extends AbstractVisitor {
if (!ctrUseMth.getMethodInfo().isClassInit()) {
return false;
}
if (cls.getUseInMth().isEmpty()) {
// no outside usage, inline not needed
return false;
}
FieldNode instFld = ListUtils.filterOnlyOne(cls.getFields(),
f -> f.getAccessFlags().containsFlags(AccessFlags.PUBLIC, AccessFlags.STATIC, AccessFlags.FINAL)
&& f.getFieldInfo().getType().equals(cls.getClassInfo().getType()));
@@ -16,7 +16,7 @@ import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.LiteralArg;
import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.IFieldInfoRef;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.visitors.shrink.CodeShrinkVisitor;
@@ -167,11 +167,11 @@ public class ReplaceNewArray extends AbstractVisitor {
private static InsnArg replaceConstInArg(MethodNode mth, InsnArg valueArg) {
if (valueArg.isLiteral()) {
FieldNode f = mth.getParentClass().getConstFieldByLiteralArg((LiteralArg) valueArg);
IFieldInfoRef f = mth.getParentClass().getConstFieldByLiteralArg((LiteralArg) valueArg);
if (f != null) {
InsnNode fGet = new IndexInsnNode(InsnType.SGET, f.getFieldInfo(), 0);
InsnArg arg = InsnArg.wrapArg(fGet);
f.addUseIn(mth);
ModVisitor.addFieldUsage(f, mth);
return arg;
}
}
@@ -402,7 +402,8 @@ public class SimplifyVisitor extends AbstractVisitor {
}
}
if (!stringArgFound) {
mth.addDebugComment("TODO: convert one arg to string using `String.valueOf()`, args: " + args);
String argStr = Utils.listToString(args, InsnArg::toShortString);
mth.addDebugComment("TODO: convert one arg to string using `String.valueOf()`, args: " + argStr);
return null;
}
@@ -625,7 +626,9 @@ public class SimplifyVisitor extends AbstractVisitor {
for (int i = 1; i < argsCount; i++) {
concat.addArg(wrap.getArg(i));
}
return ArithNode.oneArgOp(ArithOp.ADD, fArg, InsnArg.wrapArg(concat));
InsnArg concatArg = InsnArg.wrapArg(concat);
concatArg.setType(ArgType.STRING);
return ArithNode.oneArgOp(ArithOp.ADD, fArg, concatArg);
} catch (Exception e) {
LOG.debug("Can't convert field arith insn: {}, mth: {}", insn, mth, e);
}
@@ -68,7 +68,7 @@ public class BlockExceptionHandler {
BlockProcessor.removeMarkedBlocks(mth);
BlockSet sorted = new BlockSet(mth);
BlockUtils.dfsVisit(mth, sorted::set);
BlockUtils.visitDFS(mth, sorted::set);
removeUnusedExcHandlers(mth, tryBlocks, sorted);
return true;
}
@@ -331,6 +331,17 @@ public class BlockExceptionHandler {
return false;
}
BlockNode bottom = searchBottomBlock(mth, blocks);
BlockNode splitReturn;
if (bottom != null && bottom.isReturnBlock()) {
if (Consts.DEBUG_EXC_HANDLERS) {
LOG.debug("TryCatch #{} bottom block ({}) is return, split", tryCatchBlock.id(), bottom);
}
splitReturn = bottom;
bottom = BlockSplitter.blockSplitTop(mth, bottom);
bottom.add(AFlag.SYNTHETIC);
} else {
splitReturn = null;
}
if (Consts.DEBUG_EXC_HANDLERS) {
LOG.debug("TryCatch #{} split: top {}, bottom: {}", tryCatchBlock.id(), top, bottom);
}
@@ -349,6 +360,18 @@ public class BlockExceptionHandler {
bottomSplitterBlock.add(AFlag.EXC_BOTTOM_SPLITTER);
bottomSplitterBlock.add(AFlag.SYNTHETIC);
BlockSplitter.connect(bottom, bottomSplitterBlock);
if (splitReturn != null) {
// redirect handler to return block instead synthetic split block to avoid self-loop
BlockSet bottomPreds = BlockSet.from(mth, bottom.getPredecessors());
for (ExceptionHandler handler : tryCatchBlock.getHandlers()) {
if (bottomPreds.intersects(handler.getBlocks())) {
BlockNode lastBlock = bottomPreds.intersect(handler.getBlocks()).getOne();
if (lastBlock != null) {
BlockSplitter.replaceConnection(lastBlock, bottom, splitReturn);
}
}
}
}
}
if (Consts.DEBUG_EXC_HANDLERS) {
@@ -600,7 +623,7 @@ public class BlockExceptionHandler {
for (ExceptionHandler eh : mth.getExceptionHandlers()) {
boolean notProcessed = true;
BlockNode handlerBlock = eh.getHandlerBlock();
if (blocks.get(handlerBlock)) {
if (handlerBlock == null || blocks.get(handlerBlock)) {
continue;
}
for (TryCatchBlockAttr tcb : tryBlocks) {
@@ -70,6 +70,8 @@ public class BlockProcessor extends AbstractVisitor {
registerLoops(mth);
processNestedLoops(mth);
PostDominatorTree.compute(mth);
updateCleanSuccessors(mth);
if (!mth.contains(AFlag.DISABLE_BLOCKS_LOCK)) {
mth.finishBasicBlocks();
@@ -3,8 +3,7 @@ package jadx.core.dex.visitors.blocks;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.List;
import org.jetbrains.annotations.NotNull;
import java.util.function.Function;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.MethodNode;
@@ -23,14 +22,14 @@ public class DominatorTree {
public static void compute(MethodNode mth) {
List<BlockNode> sorted = sortBlocks(mth);
BlockNode[] doms = build(sorted);
BlockNode[] doms = build(sorted, BlockNode::getPredecessors);
apply(sorted, doms);
}
private static List<BlockNode> sortBlocks(MethodNode mth) {
int blocksCount = mth.getBasicBlocks().size();
List<BlockNode> sorted = new ArrayList<>(blocksCount);
BlockUtils.dfsVisit(mth, sorted::add);
BlockUtils.visitDFS(mth, sorted::add);
if (sorted.size() != blocksCount) {
throw new JadxRuntimeException("Found unreachable blocks");
}
@@ -38,8 +37,7 @@ public class DominatorTree {
return sorted;
}
@NotNull
private static BlockNode[] build(List<BlockNode> sorted) {
static BlockNode[] build(List<BlockNode> sorted, Function<BlockNode, List<BlockNode>> predFunc) {
int blocksCount = sorted.size();
BlockNode[] doms = new BlockNode[blocksCount];
doms[0] = sorted.get(0);
@@ -48,7 +46,7 @@ public class DominatorTree {
changed = false;
for (int blockId = 1; blockId < blocksCount; blockId++) {
BlockNode b = sorted.get(blockId);
List<BlockNode> preds = b.getPredecessors();
List<BlockNode> preds = predFunc.apply(b);
int pickedPred = -1;
BlockNode newIDom = null;
for (BlockNode pred : preds) {
@@ -60,7 +58,7 @@ public class DominatorTree {
}
}
if (newIDom == null) {
throw new JadxRuntimeException("No predecessors for block: " + b);
throw new JadxRuntimeException("No immediate dominator for block: " + b);
}
for (BlockNode predBlock : preds) {
int predId = predBlock.getId();
@@ -110,7 +108,7 @@ public class DominatorTree {
}
}
private static BitSet collectDoms(BlockNode[] doms, BlockNode idom) {
static BitSet collectDoms(BlockNode[] doms, BlockNode idom) {
BitSet domBS = new BitSet(doms.length);
BlockNode nextIDom = idom;
while (true) {
@@ -22,8 +22,7 @@ public class FixMultiEntryLoops {
}
List<SpecialEdgeAttr> specialEdges = mth.getAll(AType.SPECIAL_EDGE);
List<SpecialEdgeAttr> multiEntryLoops = specialEdges.stream()
.filter(e -> e.getType() == SpecialEdgeType.BACK_EDGE)
.filter(e -> !isSingleEntryLoop(e))
.filter(e -> e.getType() == SpecialEdgeType.BACK_EDGE && !isSingleEntryLoop(e))
.collect(Collectors.toList());
if (multiEntryLoops.isEmpty()) {
return false;
@@ -42,12 +41,21 @@ public class FixMultiEntryLoops {
}
private static boolean fixLoop(MethodNode mth, SpecialEdgeAttr backEdge, List<SpecialEdgeAttr> crossEdges) {
if (isHeaderSuccessorEntry(mth, backEdge, crossEdges)) {
return true;
}
if (isEndBlockEntry(mth, backEdge, crossEdges)) {
return true;
}
mth.addWarnComment("Unsupported multi-entry loop pattern (" + backEdge + "). Please report as a decompilation issue!!!");
return false;
}
private static boolean isHeaderSuccessorEntry(MethodNode mth, SpecialEdgeAttr backEdge, List<SpecialEdgeAttr> crossEdges) {
BlockNode header = backEdge.getEnd();
BlockNode headerIDom = header.getIDom();
SpecialEdgeAttr subEntry = ListUtils.filterOnlyOne(crossEdges, e -> e.getStart() == headerIDom);
if (subEntry == null || !isSupportedPattern(header, subEntry)) {
// TODO: for now only sub entry in header successor is supported
mth.addWarnComment("Unsupported multi-entry loop pattern (" + backEdge + "). Please submit an issue!!!");
if (subEntry == null || !ListUtils.isSingleElement(header.getSuccessors(), subEntry.getEnd())) {
return false;
}
BlockNode loopEnd = backEdge.getStart();
@@ -55,12 +63,28 @@ public class FixMultiEntryLoops {
BlockNode copyHeader = BlockSplitter.insertBlockBetween(mth, loopEnd, header);
BlockSplitter.copyBlockData(header, copyHeader);
BlockSplitter.replaceConnection(copyHeader, header, subEntryBlock);
mth.addDebugComment("Duplicate block to fix multi-entry loop: " + backEdge);
mth.addDebugComment("Duplicate block (" + header + ") to fix multi-entry loop: " + backEdge);
return true;
}
private static boolean isSupportedPattern(BlockNode header, SpecialEdgeAttr subEntry) {
return ListUtils.isSingleElement(header.getSuccessors(), subEntry.getEnd());
private static boolean isEndBlockEntry(MethodNode mth, SpecialEdgeAttr backEdge, List<SpecialEdgeAttr> crossEdges) {
BlockNode loopEnd = backEdge.getStart();
SpecialEdgeAttr subEntry = ListUtils.filterOnlyOne(crossEdges, e -> e.getEnd() == loopEnd);
if (subEntry == null) {
return false;
}
dupPath(mth, subEntry.getStart(), loopEnd, backEdge.getEnd());
mth.addDebugComment("Duplicate block (" + loopEnd + ") to fix multi-entry loop: " + backEdge);
return true;
}
/**
* Duplicate 'center' block on path from 'start' to 'end'
*/
private static void dupPath(MethodNode mth, BlockNode start, BlockNode center, BlockNode end) {
BlockNode copyCenter = BlockSplitter.insertBlockBetween(mth, start, end);
BlockSplitter.copyBlockData(center, copyCenter);
BlockSplitter.removeConnection(start, center);
}
private static boolean isSingleEntryLoop(SpecialEdgeAttr e) {
@@ -75,21 +99,18 @@ public class FixMultiEntryLoops {
}
private static void detectSpecialEdges(MethodNode mth) {
List<BlockNode> blocks = mth.getBasicBlocks();
BlockColor[] colors = new BlockColor[blocks.size()];
BlockColor[] colors = new BlockColor[mth.getBasicBlocks().size()];
Arrays.fill(colors, BlockColor.WHITE);
colorDFS(mth, blocks, colors, mth.getEnterBlock().getId());
colorDFS(mth, colors, mth.getEnterBlock());
}
// TODO: transform to non-recursive form
private static void colorDFS(MethodNode mth, List<BlockNode> blocks, BlockColor[] colors, int cur) {
colors[cur] = BlockColor.GRAY;
BlockNode block = blocks.get(cur);
private static void colorDFS(MethodNode mth, BlockColor[] colors, BlockNode block) {
colors[block.getId()] = BlockColor.GRAY;
for (BlockNode v : block.getSuccessors()) {
int vId = v.getId();
switch (colors[vId]) {
switch (colors[v.getId()]) {
case WHITE:
colorDFS(mth, blocks, colors, vId);
colorDFS(mth, colors, v);
break;
case GRAY:
mth.addAttr(AType.SPECIAL_EDGE, new SpecialEdgeAttr(SpecialEdgeType.BACK_EDGE, block, v));
@@ -99,6 +120,6 @@ public class FixMultiEntryLoops {
break;
}
}
colors[cur] = BlockColor.BLACK;
colors[block.getId()] = BlockColor.BLACK;
}
}
@@ -0,0 +1,72 @@
package jadx.core.dex.visitors.blocks;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.List;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.BlockUtils;
import jadx.core.utils.EmptyBitSet;
public class PostDominatorTree {
public static void compute(MethodNode mth) {
if (!mth.contains(AFlag.COMPUTE_POST_DOM)) {
return;
}
try {
int mthBlocksCount = mth.getBasicBlocks().size();
List<BlockNode> sorted = new ArrayList<>(mthBlocksCount);
BlockUtils.visitReverseDFS(mth, sorted::add);
// temporary set block ids to match reverse sorted order
// save old ids for later remapping
int blocksCount = sorted.size();
int[] ids = new int[mthBlocksCount];
for (int i = 0; i < blocksCount; i++) {
ids[i] = sorted.get(i).getId();
}
mth.updateBlockIds(sorted);
BlockNode[] postDoms = DominatorTree.build(sorted, BlockNode::getSuccessors);
BlockNode firstBlock = sorted.get(0);
firstBlock.setPostDoms(EmptyBitSet.EMPTY);
firstBlock.setIPostDom(null);
for (int i = 1; i < blocksCount; i++) {
BlockNode block = sorted.get(i);
BlockNode iPostDom = postDoms[i];
block.setIPostDom(iPostDom);
BitSet postDomBS = DominatorTree.collectDoms(postDoms, iPostDom);
block.setPostDoms(postDomBS);
}
for (int i = 1; i < blocksCount; i++) {
BlockNode block = sorted.get(i);
BitSet bs = new BitSet(blocksCount);
block.getPostDoms().stream().forEach(n -> bs.set(ids[n]));
bs.clear(ids[i]);
block.setPostDoms(bs);
}
// check for missing blocks in 'sorted' list
// can be caused by infinite loops
int blocksDelta = mthBlocksCount - blocksCount;
if (blocksDelta != 0) {
int insnsCount = 0;
for (BlockNode block : mth.getBasicBlocks()) {
if (block.getPostDoms() == null) {
block.setPostDoms(EmptyBitSet.EMPTY);
block.setIPostDom(null);
insnsCount += block.getInstructions().size();
}
}
mth.addInfoComment("Infinite loop detected, blocks: " + blocksDelta + ", insns: " + insnsCount);
}
} catch (Throwable e) {
// show error as a warning because this info not always used
mth.addWarnComment("Failed to build post-dominance tree", e);
} finally {
// revert block ids change
mth.updateBlockIds(mth.getBasicBlocks());
}
}
}
@@ -89,7 +89,7 @@ public class DebugInfoApplyVisitor extends AbstractVisitor {
return;
}
OptionalInt max = ssaVar.getUseList().stream().mapToInt(DebugInfoApplyVisitor::getInsnOffsetByArg).max();
if (!max.isPresent()) {
if (max.isEmpty()) {
return;
}
int startOffset = getInsnOffsetByArg(ssaVar.getAssign());
@@ -0,0 +1,45 @@
package jadx.core.dex.visitors.prepare;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.info.ConstStorage;
import jadx.core.dex.info.FieldInfo;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.visitors.AbstractVisitor;
import jadx.core.dex.visitors.JadxVisitor;
import jadx.core.utils.android.AndroidResourcesMap;
import jadx.core.utils.exceptions.JadxException;
// TODO: move this pass to separate "Android plugin"
@JadxVisitor(
name = "AddAndroidConstants",
desc = "Insert Android constants from resource mapping file",
runBefore = {
CollectConstValues.class
}
)
public class AddAndroidConstants extends AbstractVisitor {
private static final String R_CLS = "android.R";
private static final String R_INNER_CLS = R_CLS + '$';
@Override
public void init(RootNode root) throws JadxException {
if (!root.getArgs().isReplaceConsts()) {
return;
}
if (root.resolveClass(R_CLS) != null) {
// Android R class already loaded
return;
}
ConstStorage constStorage = root.getConstValues();
AndroidResourcesMap.getMap().forEach((resId, path) -> {
int sep = path.indexOf('/');
String clsName = R_INNER_CLS + path.substring(0, sep);
String resName = path.substring(sep + 1);
ClassInfo cls = ClassInfo.fromName(root, clsName);
FieldInfo field = FieldInfo.from(root, cls, resName, ArgType.INT);
constStorage.addGlobalConstField(field, resId);
});
}
}
@@ -0,0 +1,55 @@
package jadx.core.dex.visitors.prepare;
import org.jetbrains.annotations.Nullable;
import jadx.api.plugins.input.data.annotations.EncodedValue;
import jadx.api.plugins.input.data.attributes.JadxAttrType;
import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.info.ConstStorage;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.visitors.AbstractVisitor;
import jadx.core.dex.visitors.JadxVisitor;
import jadx.core.utils.exceptions.JadxException;
@JadxVisitor(
name = "CollectConstValues",
desc = "Collect and store values from static final fields"
)
public class CollectConstValues extends AbstractVisitor {
@Override
public boolean visit(ClassNode cls) throws JadxException {
RootNode root = cls.root();
if (!root.getArgs().isReplaceConsts()) {
return true;
}
if (cls.getFields().isEmpty()) {
return true;
}
ConstStorage constStorage = root.getConstValues();
for (FieldNode fld : cls.getFields()) {
try {
Object value = getFieldConstValue(fld);
if (value != null) {
constStorage.addConstField(fld, value, fld.getAccessFlags().isPublic());
}
} catch (Exception e) {
cls.addWarnComment("Failed to process value of field: " + fld, e);
}
}
return true;
}
public static @Nullable Object getFieldConstValue(FieldNode fld) {
AccessInfo accFlags = fld.getAccessFlags();
if (accFlags.isStatic() && accFlags.isFinal()) {
EncodedValue constVal = fld.get(JadxAttrType.CONSTANT_VALUE);
if (constVal != null && constVal != EncodedValue.NULL) {
return constVal.getValue();
}
}
return null;
}
}
@@ -2,7 +2,6 @@ package jadx.core.dex.visitors.regions;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
@@ -14,8 +13,6 @@ import java.util.Optional;
import java.util.Set;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
@@ -60,8 +57,6 @@ import static jadx.core.utils.BlockUtils.getNextBlock;
import static jadx.core.utils.BlockUtils.isPathExists;
public class RegionMaker {
private static final Logger LOG = LoggerFactory.getLogger(RegionMaker.class);
private final MethodNode mth;
private final int regionsLimit;
private final BitSet processedBlocks;
@@ -794,53 +789,32 @@ public class RegionMaker {
keys.add(SwitchRegion.DEFAULT_CASE_KEY);
}
// search 'out' block - 'next' block after whole switch statement
BlockNode out;
LoopInfo loop = mth.getLoopForBlock(block);
if (loop == null) {
out = calcPostDomOut(mth, block, mth.getPreExitBlocks());
} else {
BlockNode loopEnd = loop.getEnd();
stack.addExit(loop.getStart());
if (stack.containsExit(block)
|| block == loopEnd
|| loopEnd.getPredecessors().contains(block)) {
// in exits or last insn in loop => no 'out' block
out = null;
} else {
// treat 'continue' as exit
out = calcPostDomOut(mth, block, loopEnd.getPredecessors());
if (out != null) {
insertContinueInSwitch(block, out, loopEnd);
} else {
// no 'continue'
out = calcPostDomOut(mth, block, Collections.singletonList(loopEnd));
}
}
if (out == loop.getStart()) {
// no other outs instead back edge to loop start
out = null;
}
}
if (out != null && processedBlocks.get(out.getId())) {
// out block already processed, prevent endless loop
throw new JadxRuntimeException("Failed to find switch 'out' block");
}
SwitchRegion sw = new SwitchRegion(currentRegion, block);
insn.addAttr(new RegionRefAttr(sw));
currentRegion.getSubBlocks().add(sw);
stack.push(sw);
BlockNode out = calcSwitchOut(block, stack);
stack.addExit(out);
// detect fallthrough cases
processFallThroughCases(sw, out, stack, blocksMap);
removeEmptyCases(insn, sw, defCase);
stack.pop();
return out;
}
private void processFallThroughCases(SwitchRegion sw, @Nullable BlockNode out,
RegionStack stack, Map<BlockNode, List<Object>> blocksMap) {
Map<BlockNode, BlockNode> fallThroughCases = new LinkedHashMap<>();
if (out != null) {
// detect fallthrough cases
BitSet caseBlocks = BlockUtils.blocksToBitSet(mth, blocksMap.keySet());
caseBlocks.clear(out.getId());
for (BlockNode successor : block.getCleanSuccessors()) {
BlockNode fallThroughBlock = searchFallThroughCase(successor, out, caseBlocks);
if (fallThroughBlock != null) {
for (BlockNode successor : sw.getHeader().getCleanSuccessors()) {
BitSet df = successor.getDomFrontier();
if (df.intersects(caseBlocks)) {
BlockNode fallThroughBlock = getOneIntersectionBlock(out, caseBlocks, df);
fallThroughCases.put(successor, fallThroughBlock);
}
}
@@ -874,26 +848,6 @@ public class RegionMaker {
// 'break' instruction will be inserted in RegionMakerVisitor.PostRegionVisitor
}
}
removeEmptyCases(insn, sw, defCase);
stack.pop();
return out;
}
@Nullable
private BlockNode searchFallThroughCase(BlockNode successor, BlockNode out, BitSet caseBlocks) {
BitSet df = successor.getDomFrontier();
if (df.intersects(caseBlocks)) {
return getOneIntersectionBlock(out, caseBlocks, df);
}
Set<BlockNode> allPathsBlocks = BlockUtils.getAllPathsBlocks(successor, out);
Map<BlockNode, BitSet> bitSetMap = BlockUtils.calcPartialPostDominance(mth, allPathsBlocks, out);
BitSet pdoms = bitSetMap.get(successor);
if (pdoms != null && pdoms.intersects(caseBlocks)) {
return getOneIntersectionBlock(out, caseBlocks, pdoms);
}
return null;
}
@Nullable
@@ -904,35 +858,71 @@ public class RegionMaker {
return BlockUtils.bitSetToOneBlock(mth, caseExits);
}
@Nullable
private static BlockNode calcPostDomOut(MethodNode mth, BlockNode block, List<BlockNode> exits) {
if (exits.size() == 1 && mth.getExitBlock().equals(exits.get(0))) {
// simple case: for only one exit which is equal to method exit block
return BlockUtils.calcImmediatePostDominator(mth, block);
}
// fast search: union of blocks dominance frontier
// work if no fallthrough cases and no returns inside switch
BitSet outs = BlockUtils.copyBlocksBitSet(mth, block.getDomFrontier());
private @Nullable BlockNode calcSwitchOut(BlockNode block, RegionStack stack) {
// union of case blocks dominance frontier
// works if no fallthrough cases and no returns inside switch
BitSet outs = BlockUtils.newBlocksBitSet(mth);
for (BlockNode s : block.getCleanSuccessors()) {
outs.or(s.getDomFrontier());
}
outs.clear(block.getId());
if (outs.isEmpty()) {
// switch already contains method exit
// add everything, out block not needed
return mth.getExitBlock();
}
if (outs.cardinality() != 1) {
// slow search: calculate partial post-dominance for every exit node
BitSet ipdoms = BlockUtils.newBlocksBitSet(mth);
for (BlockNode exitBlock : exits) {
if (BlockUtils.isAnyPathExists(block, exitBlock)) {
Set<BlockNode> pathBlocks = BlockUtils.getAllPathsBlocks(block, exitBlock);
BlockNode ipdom = BlockUtils.calcPartialImmediatePostDominator(mth, block, pathBlocks, exitBlock);
if (ipdom != null) {
ipdoms.set(ipdom.getId());
BlockNode out;
if (outs.cardinality() == 1) {
// single exit
out = BlockUtils.bitSetToOneBlock(mth, outs);
} else {
// several switch exits
// possible 'return', 'continue' or fallthrough in one of the cases
LoopInfo loop = mth.getLoopForBlock(block);
if (loop != null) {
outs.andNot(block.getPostDoms());
out = BlockUtils.bitSetToOneBlock(mth, outs);
if (out != null) {
insertContinueInSwitch(block, out, loop.getEnd());
if (out == loop.getStart()) {
// no other outs instead back edge to loop start
return null;
}
}
} else {
outs.clear(mth.getExitBlock().getId());
BlockNode imPostDom = block.getIPostDom();
if (outs.get(imPostDom.getId())) {
return imPostDom;
}
outs.andNot(block.getPostDoms());
out = BlockUtils.bitSetToOneBlock(mth, outs);
}
outs.and(ipdoms);
}
return BlockUtils.bitSetToOneBlock(mth, outs);
if (out != null && mth.isPreExitBlock(out)) {
// include 'return' or 'throw' in case blocks
out = mth.getExitBlock();
}
BlockNode imPostDom = block.getIPostDom();
if (out != imPostDom && !mth.isPreExitBlock(imPostDom)) {
// stop other paths at common exit
stack.addExit(imPostDom);
}
if (block.getCleanSuccessors().contains(imPostDom)) {
// add exit to stop on empty 'default' block
stack.addExit(imPostDom);
}
if (out == null) {
mth.addWarnComment("Failed to find 'out' block for switch in " + block + ". Please report as an issue.");
// fallback option; should work in most cases
out = block.getIPostDom();
}
if (out != null && processedBlocks.get(out.getId())) {
// 'out' block already processed, prevent endless loop
throw new JadxRuntimeException("Failed to find switch 'out' block (already processed)");
}
return out;
}
/**
@@ -999,18 +989,21 @@ public class RegionMaker {
return newBlocksMap;
}
private void insertContinueInSwitch(BlockNode block, BlockNode out, BlockNode end) {
int endId = end.getId();
for (BlockNode s : block.getCleanSuccessors()) {
if (s.getDomFrontier().get(endId) && s != out) {
private void insertContinueInSwitch(BlockNode switchBlock, BlockNode switchOut, BlockNode loopEnd) {
for (BlockNode caseBlock : switchBlock.getCleanSuccessors()) {
if (caseBlock.getDomFrontier().get(loopEnd.getId()) && caseBlock != switchOut) {
// search predecessor of loop end on path from this successor
List<BlockNode> list = BlockUtils.collectBlocksDominatedBy(mth, s, s);
for (BlockNode p : end.getPredecessors()) {
if (list.contains(p)) {
if (p.isSynthetic()) {
p.getInstructions().add(new InsnNode(InsnType.CONTINUE, 0));
Set<BlockNode> list = new HashSet<>(BlockUtils.collectBlocksDominatedBy(mth, caseBlock, caseBlock));
if (list.contains(switchOut) || switchOut.getPredecessors().stream().anyMatch(list::contains)) {
// 'continue' not needed
} else {
for (BlockNode p : loopEnd.getPredecessors()) {
if (list.contains(p)) {
if (p.isSynthetic()) {
p.getInstructions().add(new InsnNode(InsnType.CONTINUE, 0));
}
break;
}
break;
}
}
}
@@ -84,7 +84,7 @@ public class ProcessVariables extends AbstractVisitor {
if (insn.canRemoveResult()) {
// remove unused result
remove = true;
} else if (insn.isConstInsn()) {
} else if (canRemoveInsn(insn)) {
// remove whole insn
insn.add(AFlag.REMOVE);
insn.add(AFlag.DONT_GENERATE);
@@ -101,6 +101,22 @@ public class ProcessVariables extends AbstractVisitor {
}
}
/**
* Remove insn if a result is not used
*/
private boolean canRemoveInsn(InsnNode insn) {
if (insn.isConstInsn()) {
return true;
}
switch (insn.getType()) {
case CAST:
case CHECK_CAST:
return true;
default:
return false;
}
}
private boolean isVarUnused(MethodNode mth, @Nullable SSAVar ssaVar) {
if (ssaVar == null) {
return true;
@@ -18,6 +18,7 @@ import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.PackageNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.StringUtils;
public class UserRenames {
private static final Logger LOG = LoggerFactory.getLogger(UserRenames.class);
@@ -57,7 +58,12 @@ public class UserRenames {
case FIELD:
FieldNode fieldNode = cls.searchFieldByShortId(nodeRef.getShortId());
if (fieldNode == null) {
LOG.warn("Field reference not found: {}", nodeRef);
String fieldName = StringUtils.getPrefix(nodeRef.getShortId(), ":");
String fieldSign = cls.getFields().stream()
.filter(f -> f.getFieldInfo().getName().equals(fieldName))
.map(f -> f.getFieldInfo().getShortId())
.collect(Collectors.joining());
LOG.warn("Field reference not found: {}. Fields with same name: {}", nodeRef, fieldSign);
} else {
fieldNode.rename(rename.getNewName());
}
@@ -3,7 +3,6 @@ package jadx.core.dex.visitors.shrink;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Objects;
import java.util.Set;
@@ -64,12 +63,9 @@ public class CodeShrinkVisitor extends AbstractVisitor {
List<WrapInfo> wrapList = new ArrayList<>();
for (ArgsInfo argsInfo : argsList) {
List<RegisterArg> args = argsInfo.getArgs();
if (!args.isEmpty()) {
ListIterator<RegisterArg> it = args.listIterator(args.size());
while (it.hasPrevious()) {
RegisterArg arg = it.previous();
checkInline(mth, block, insnList, wrapList, argsInfo, arg);
}
for (int i = args.size() - 1; i >= 0; i--) {
RegisterArg arg = args.get(i);
checkInline(mth, block, insnList, wrapList, argsInfo, arg);
}
}
if (!wrapList.isEmpty()) {
@@ -30,4 +30,9 @@ public final class FinishTypeInference extends AbstractVisitor {
}
});
}
@Override
public String getName() {
return "FinishTypeInference";
}
}
@@ -0,0 +1,844 @@
package jadx.core.dex.visitors.typeinference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.Consts;
import jadx.core.clsp.ClspGraph;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.PhiListAttr;
import jadx.core.dex.instructions.ArithNode;
import jadx.core.dex.instructions.ArithOp;
import jadx.core.dex.instructions.IndexInsnNode;
import jadx.core.dex.instructions.InsnType;
import jadx.core.dex.instructions.PhiInsn;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.LiteralArg;
import jadx.core.dex.instructions.args.PrimitiveType;
import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.instructions.args.SSAVar;
import jadx.core.dex.instructions.mods.TernaryInsn;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.visitors.AbstractVisitor;
import jadx.core.dex.visitors.InitCodeVariables;
import jadx.core.dex.visitors.JadxVisitor;
import jadx.core.dex.visitors.ModVisitor;
import jadx.core.dex.visitors.blocks.BlockSplitter;
import jadx.core.utils.BlockUtils;
import jadx.core.utils.InsnList;
import jadx.core.utils.InsnUtils;
import jadx.core.utils.ListUtils;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxOverflowException;
@JadxVisitor(
name = "Fix Types Visitor",
desc = "Try various methods to fix unresolved types",
runAfter = {
TypeInferenceVisitor.class
},
runBefore = {
FinishTypeInference.class
}
)
public final class FixTypesVisitor extends AbstractVisitor {
private static final Logger LOG = LoggerFactory.getLogger(FixTypesVisitor.class);
private final TypeInferenceVisitor typeInference = new TypeInferenceVisitor();
private TypeUpdate typeUpdate;
private List<Function<MethodNode, Boolean>> resolvers;
@Override
public void init(RootNode root) {
this.typeUpdate = root.getTypeUpdate();
this.typeInference.init(root);
this.resolvers = Arrays.asList(
this::tryRestoreTypeVarCasts,
this::tryInsertCasts,
this::tryDeduceTypes,
this::trySplitConstInsns,
this::tryToFixIncompatiblePrimitives,
this::tryToForceImmutableTypes,
this::tryInsertAdditionalMove,
this::runMultiVariableSearch,
this::tryRemoveGenerics);
}
@Override
public void visit(MethodNode mth) {
if (mth.isNoCode() || checkTypes(mth)) {
return;
}
try {
for (Function<MethodNode, Boolean> resolver : resolvers) {
if (resolver.apply(mth) && checkTypes(mth)) {
break;
}
}
} catch (Exception e) {
mth.addError("Types fix failed", e);
}
}
/**
* Check if all types resolved
*/
private static boolean checkTypes(MethodNode mth) {
for (SSAVar var : mth.getSVars()) {
ArgType type = var.getTypeInfo().getType();
if (!type.isTypeKnown()) {
return false;
}
}
return true;
}
private boolean runMultiVariableSearch(MethodNode mth) {
try {
TypeSearch typeSearch = new TypeSearch(mth);
if (!typeSearch.run()) {
mth.addWarnComment("Multi-variable type inference failed");
}
for (SSAVar var : mth.getSVars()) {
if (!var.getTypeInfo().getType().isTypeKnown()) {
return false;
}
}
return true;
} catch (Exception e) {
mth.addWarnComment("Multi-variable type inference failed. Error: " + Utils.getStackTrace(e));
return false;
}
}
private boolean setBestType(MethodNode mth, SSAVar ssaVar) {
try {
return calculateFromBounds(mth, ssaVar);
} catch (JadxOverflowException e) {
throw e;
} catch (Exception e) {
mth.addWarnComment("Failed to calculate best type for var: " + ssaVar, e);
return false;
}
}
private boolean calculateFromBounds(MethodNode mth, SSAVar ssaVar) {
TypeInfo typeInfo = ssaVar.getTypeInfo();
Set<ITypeBound> bounds = typeInfo.getBounds();
Optional<ArgType> bestTypeOpt = selectBestTypeFromBounds(bounds);
if (bestTypeOpt.isEmpty()) {
if (Consts.DEBUG_TYPE_INFERENCE) {
LOG.warn("Failed to select best type from bounds, count={} : ", bounds.size());
for (ITypeBound bound : bounds) {
LOG.warn(" {}", bound);
}
}
return false;
}
ArgType candidateType = bestTypeOpt.get();
TypeUpdateResult result = typeUpdate.apply(mth, ssaVar, candidateType);
if (result == TypeUpdateResult.REJECT) {
if (Consts.DEBUG_TYPE_INFERENCE) {
if (ssaVar.getTypeInfo().getType().equals(candidateType)) {
LOG.info("Same type rejected: {} -> {}, bounds: {}", ssaVar, candidateType, bounds);
} else if (candidateType.isTypeKnown()) {
LOG.debug("Type rejected: {} -> {}, bounds: {}", ssaVar, candidateType, bounds);
}
}
return false;
}
return result == TypeUpdateResult.CHANGED;
}
private Optional<ArgType> selectBestTypeFromBounds(Set<ITypeBound> bounds) {
return bounds.stream()
.map(ITypeBound::getType)
.filter(Objects::nonNull)
.max(typeUpdate.getTypeCompare().getComparator());
}
private boolean tryPossibleTypes(MethodNode mth, SSAVar var, ArgType type) {
List<ArgType> types = makePossibleTypesList(type, var);
if (types.isEmpty()) {
return false;
}
for (ArgType candidateType : types) {
TypeUpdateResult result = typeUpdate.apply(mth, var, candidateType);
if (result == TypeUpdateResult.CHANGED) {
return true;
}
}
return false;
}
private List<ArgType> makePossibleTypesList(ArgType type, @Nullable SSAVar var) {
if (type.isArray()) {
List<ArgType> list = new ArrayList<>();
for (ArgType arrElemType : makePossibleTypesList(type.getArrayElement(), null)) {
list.add(ArgType.array(arrElemType));
}
return list;
}
if (var != null) {
for (ITypeBound b : var.getTypeInfo().getBounds()) {
ArgType boundType = b.getType();
if (boundType.isObject() || boundType.isArray()) {
// don't add primitive types
return Collections.emptyList();
}
}
}
List<ArgType> list = new ArrayList<>();
for (PrimitiveType possibleType : type.getPossibleTypes()) {
if (possibleType == PrimitiveType.VOID) {
continue;
}
list.add(ArgType.convertFromPrimitiveType(possibleType));
}
return list;
}
private boolean tryDeduceTypes(MethodNode mth) {
boolean fixed = false;
for (SSAVar ssaVar : mth.getSVars()) {
if (deduceType(mth, ssaVar)) {
fixed = true;
}
}
return fixed;
}
@SuppressWarnings("RedundantIfStatement")
private boolean deduceType(MethodNode mth, SSAVar var) {
if (var.isTypeImmutable()) {
return false;
}
ArgType type = var.getTypeInfo().getType();
if (type.isTypeKnown()) {
return false;
}
// try best type from bounds again
if (setBestType(mth, var)) {
return true;
}
// try all possible types (useful for primitives)
if (tryPossibleTypes(mth, var, type)) {
return true;
}
// for objects try super types
if (tryWiderObjects(mth, var)) {
return true;
}
return false;
}
private boolean tryRemoveGenerics(MethodNode mth) {
boolean resolved = true;
for (SSAVar var : mth.getSVars()) {
ArgType type = var.getTypeInfo().getType();
if (!type.isTypeKnown()
&& !var.isTypeImmutable()
&& !tryRawType(mth, var)) {
resolved = false;
}
}
return resolved;
}
private boolean tryRawType(MethodNode mth, SSAVar var) {
Set<ArgType> objTypes = new LinkedHashSet<>();
for (ITypeBound bound : var.getTypeInfo().getBounds()) {
ArgType boundType = bound.getType();
if (boundType.isTypeKnown() && boundType.isObject()) {
objTypes.add(boundType);
}
}
if (objTypes.isEmpty()) {
return false;
}
for (ArgType objType : objTypes) {
if (checkRawType(mth, var, objType)) {
mth.addDebugComment("Type inference failed for " + var.toShortString() + "."
+ " Raw type applied. Possible types: " + Utils.listToString(objTypes));
return true;
}
}
return false;
}
private boolean checkRawType(MethodNode mth, SSAVar var, ArgType objType) {
if (objType.isObject() && objType.containsGeneric()) {
ArgType rawType = objType.isGenericType() ? ArgType.OBJECT : ArgType.object(objType.getObject());
TypeUpdateResult result = typeUpdate.applyWithWiderAllow(mth, var, rawType);
return result == TypeUpdateResult.CHANGED;
}
return false;
}
/**
* Fix check casts to type var extend type:
* <br>
* {@code <T extends Comparable> T var = (Comparable) obj; => T var = (T) obj; }
*/
private boolean tryRestoreTypeVarCasts(MethodNode mth) {
int changed = 0;
List<SSAVar> mthSVars = mth.getSVars();
for (SSAVar var : mthSVars) {
changed += restoreTypeVarCasts(var);
}
if (changed == 0) {
return false;
}
if (Consts.DEBUG_TYPE_INFERENCE) {
mth.addDebugComment("Restore " + changed + " type vars casts");
}
typeInference.initTypeBounds(mth);
return typeInference.runTypePropagation(mth);
}
private int restoreTypeVarCasts(SSAVar var) {
TypeInfo typeInfo = var.getTypeInfo();
Set<ITypeBound> bounds = typeInfo.getBounds();
if (!ListUtils.anyMatch(bounds, t -> t.getType().isGenericType())) {
return 0;
}
List<ITypeBound> casts = ListUtils.filter(bounds, TypeBoundCheckCastAssign.class::isInstance);
if (casts.isEmpty()) {
return 0;
}
ArgType bestType = selectBestTypeFromBounds(bounds).orElse(ArgType.UNKNOWN);
if (!bestType.isGenericType()) {
return 0;
}
List<ArgType> extendTypes = bestType.getExtendTypes();
if (extendTypes.size() != 1) {
return 0;
}
int fixed = 0;
ArgType extendType = extendTypes.get(0);
for (ITypeBound bound : casts) {
TypeBoundCheckCastAssign cast = (TypeBoundCheckCastAssign) bound;
ArgType castType = cast.getType();
TypeCompareEnum result = typeUpdate.getTypeCompare().compareTypes(extendType, castType);
if (result.isEqual() || result == TypeCompareEnum.NARROW_BY_GENERIC) {
cast.getInsn().updateIndex(bestType);
fixed++;
}
}
return fixed;
}
@SuppressWarnings({ "ForLoopReplaceableByWhile", "ForLoopReplaceableByForEach" })
private boolean tryInsertCasts(MethodNode mth) {
int added = 0;
List<SSAVar> mthSVars = mth.getSVars();
int varsCount = mthSVars.size();
for (int i = 0; i < varsCount; i++) {
SSAVar var = mthSVars.get(i);
ArgType type = var.getTypeInfo().getType();
if (!type.isTypeKnown() && !var.isTypeImmutable()) {
added += tryInsertVarCast(mth, var);
}
}
if (added != 0) {
InitCodeVariables.rerun(mth);
typeInference.initTypeBounds(mth);
return typeInference.runTypePropagation(mth);
}
return false;
}
private int tryInsertVarCast(MethodNode mth, SSAVar var) {
for (ITypeBound bound : var.getTypeInfo().getBounds()) {
ArgType boundType = bound.getType();
if (boundType.isTypeKnown()
&& !boundType.equals(var.getTypeInfo().getType())
&& boundType.containsTypeVariable()
&& !mth.root().getTypeUtils().containsUnknownTypeVar(mth, boundType)) {
if (insertAssignCast(mth, var, boundType)) {
return 1;
}
return insertUseCasts(mth, var);
}
}
return 0;
}
private int insertUseCasts(MethodNode mth, SSAVar var) {
List<RegisterArg> useList = var.getUseList();
if (useList.isEmpty()) {
return 0;
}
int useCasts = 0;
for (RegisterArg useReg : new ArrayList<>(useList)) {
if (insertSoftUseCast(mth, useReg)) {
useCasts++;
}
}
return useCasts;
}
private boolean insertAssignCast(MethodNode mth, SSAVar var, ArgType castType) {
RegisterArg assignArg = var.getAssign();
InsnNode assignInsn = assignArg.getParentInsn();
if (assignInsn == null || assignInsn.getType() == InsnType.PHI) {
return false;
}
BlockNode assignBlock = BlockUtils.getBlockByInsn(mth, assignInsn);
if (assignBlock == null) {
return false;
}
assignInsn.setResult(assignArg.duplicateWithNewSSAVar(mth));
IndexInsnNode castInsn = makeSoftCastInsn(assignArg.duplicate(), assignInsn.getResult().duplicate(), castType);
return BlockUtils.insertAfterInsn(assignBlock, assignInsn, castInsn);
}
private boolean insertSoftUseCast(MethodNode mth, RegisterArg useArg) {
InsnNode useInsn = useArg.getParentInsn();
if (useInsn == null || useInsn.getType() == InsnType.PHI) {
return false;
}
if (useInsn.getType() == InsnType.IF && useInsn.getArg(1).isZeroConst()) {
// cast isn't needed if compare with null
return false;
}
BlockNode useBlock = BlockUtils.getBlockByInsn(mth, useInsn);
if (useBlock == null) {
return false;
}
IndexInsnNode castInsn = makeSoftCastInsn(
useArg.duplicateWithNewSSAVar(mth),
useArg.duplicate(),
useArg.getInitType());
useInsn.replaceArg(useArg, castInsn.getResult().duplicate());
boolean success = BlockUtils.insertBeforeInsn(useBlock, useInsn, castInsn);
if (Consts.DEBUG_TYPE_INFERENCE && success) {
LOG.info("Insert soft cast for {} before {} in {}", useArg, useInsn, useBlock);
}
return success;
}
private IndexInsnNode makeSoftCastInsn(RegisterArg result, RegisterArg arg, ArgType castType) {
IndexInsnNode castInsn = new IndexInsnNode(InsnType.CHECK_CAST, castType, 1);
castInsn.setResult(result);
castInsn.addArg(arg);
castInsn.add(AFlag.SOFT_CAST);
castInsn.add(AFlag.SYNTHETIC);
return castInsn;
}
private boolean trySplitConstInsns(MethodNode mth) {
boolean constSplit = false;
for (SSAVar var : new ArrayList<>(mth.getSVars())) {
if (checkAndSplitConstInsn(mth, var)) {
constSplit = true;
}
}
if (!constSplit) {
return false;
}
InitCodeVariables.rerun(mth);
typeInference.initTypeBounds(mth);
return typeInference.runTypePropagation(mth);
}
private boolean checkAndSplitConstInsn(MethodNode mth, SSAVar var) {
ArgType type = var.getTypeInfo().getType();
if (type.isTypeKnown() || var.isTypeImmutable()) {
return false;
}
return splitByPhi(mth, var) || dupConst(mth, var);
}
private boolean dupConst(MethodNode mth, SSAVar var) {
InsnNode assignInsn = var.getAssign().getAssignInsn();
if (!InsnUtils.isInsnType(assignInsn, InsnType.CONST)) {
return false;
}
if (var.getUseList().size() < 2) {
return false;
}
BlockNode assignBlock = BlockUtils.getBlockByInsn(mth, assignInsn);
if (assignBlock == null) {
return false;
}
assignInsn.remove(AFlag.DONT_INLINE);
int insertIndex = 1 + BlockUtils.getInsnIndexInBlock(assignBlock, assignInsn);
List<RegisterArg> useList = new ArrayList<>(var.getUseList());
for (int i = 0, useCount = useList.size(); i < useCount; i++) {
RegisterArg useArg = useList.get(i);
useArg.remove(AFlag.DONT_INLINE_CONST);
if (i == 0) {
continue;
}
InsnNode useInsn = useArg.getParentInsn();
if (useInsn == null) {
continue;
}
InsnNode newInsn = assignInsn.copyWithNewSsaVar(mth);
assignBlock.getInstructions().add(insertIndex, newInsn);
useInsn.replaceArg(useArg, newInsn.getResult().duplicate());
}
if (Consts.DEBUG_TYPE_INFERENCE) {
LOG.debug("Duplicate const insn {} times: {} in {}", useList.size(), assignInsn, assignBlock);
}
return true;
}
/**
* For every PHI make separate CONST insn
*/
private static boolean splitByPhi(MethodNode mth, SSAVar var) {
if (var.getUsedInPhi().size() < 2) {
return false;
}
InsnNode assignInsn = var.getAssign().getAssignInsn();
InsnNode constInsn = InsnUtils.checkInsnType(assignInsn, InsnType.CONST);
if (constInsn == null) {
return false;
}
BlockNode blockNode = BlockUtils.getBlockByInsn(mth, constInsn);
if (blockNode == null) {
return false;
}
boolean first = true;
for (PhiInsn phiInsn : var.getUsedInPhi()) {
if (first) {
first = false;
continue;
}
InsnNode copyInsn = constInsn.copyWithNewSsaVar(mth);
copyInsn.add(AFlag.SYNTHETIC);
BlockUtils.insertAfterInsn(blockNode, constInsn, copyInsn);
RegisterArg phiArg = phiInsn.getArgBySsaVar(var);
phiInsn.replaceArg(phiArg, copyInsn.getResult().duplicate());
}
return true;
}
private boolean tryInsertAdditionalMove(MethodNode mth) {
int insnsAdded = 0;
for (BlockNode block : mth.getBasicBlocks()) {
PhiListAttr phiListAttr = block.get(AType.PHI_LIST);
if (phiListAttr != null) {
for (PhiInsn phiInsn : phiListAttr.getList()) {
insnsAdded += tryInsertAdditionalInsn(mth, phiInsn);
}
}
}
if (insnsAdded == 0) {
return false;
}
if (Consts.DEBUG_TYPE_INFERENCE) {
mth.addDebugComment("Additional " + insnsAdded + " move instructions added to help type inference");
}
InitCodeVariables.rerun(mth);
typeInference.initTypeBounds(mth);
if (typeInference.runTypePropagation(mth) && checkTypes(mth)) {
return true;
}
return tryDeduceTypes(mth);
}
/**
* Add MOVE instruction before PHI in bound blocks to make 'soft' type link.
* This allows using different types in blocks merged by PHI.
*/
private int tryInsertAdditionalInsn(MethodNode mth, PhiInsn phiInsn) {
ArgType phiType = getCommonTypeForPhiArgs(phiInsn);
if (phiType != null && phiType.isTypeKnown()) {
// all args have the same known type => nothing to do here
return 0;
}
// check if instructions can be inserted
if (insertMovesForPhi(mth, phiInsn, false) == 0) {
return 0;
}
// check passed => apply
return insertMovesForPhi(mth, phiInsn, true);
}
@Nullable
private ArgType getCommonTypeForPhiArgs(PhiInsn phiInsn) {
ArgType phiArgType = null;
for (InsnArg arg : phiInsn.getArguments()) {
ArgType type = arg.getType();
if (phiArgType == null) {
phiArgType = type;
} else if (!phiArgType.equals(type)) {
return null;
}
}
return phiArgType;
}
private int insertMovesForPhi(MethodNode mth, PhiInsn phiInsn, boolean apply) {
int argsCount = phiInsn.getArgsCount();
int count = 0;
for (int argIndex = 0; argIndex < argsCount; argIndex++) {
RegisterArg reg = phiInsn.getArg(argIndex);
BlockNode startBlock = phiInsn.getBlockByArgIndex(argIndex);
BlockNode blockNode = checkBlockForInsnInsert(startBlock);
if (blockNode == null) {
mth.addDebugComment("Failed to insert an additional move for type inference into block " + startBlock);
return 0;
}
boolean add = true;
SSAVar var = reg.getSVar();
InsnNode assignInsn = var.getAssign().getAssignInsn();
if (assignInsn != null) {
InsnType assignType = assignInsn.getType();
if (assignType == InsnType.CONST
|| (assignType == InsnType.MOVE && var.getUseCount() == 1)) {
add = false;
}
}
if (add) {
count++;
if (apply) {
insertMove(mth, blockNode, phiInsn, reg);
}
}
}
return count;
}
private void insertMove(MethodNode mth, BlockNode blockNode, PhiInsn phiInsn, RegisterArg reg) {
SSAVar var = reg.getSVar();
int regNum = reg.getRegNum();
RegisterArg resultArg = reg.duplicate(regNum, null);
SSAVar newSsaVar = mth.makeNewSVar(resultArg);
RegisterArg arg = reg.duplicate(regNum, var);
InsnNode moveInsn = new InsnNode(InsnType.MOVE, 1);
moveInsn.setResult(resultArg);
moveInsn.addArg(arg);
moveInsn.add(AFlag.SYNTHETIC);
blockNode.getInstructions().add(moveInsn);
phiInsn.replaceArg(reg, reg.duplicate(regNum, newSsaVar));
}
@Nullable
private BlockNode checkBlockForInsnInsert(BlockNode blockNode) {
if (blockNode.isSynthetic()) {
return null;
}
InsnNode lastInsn = BlockUtils.getLastInsn(blockNode);
if (lastInsn != null && BlockSplitter.isSeparate(lastInsn.getType())) {
// can't insert move in a block with 'separate' instruction => try previous block by simple path
List<BlockNode> preds = blockNode.getPredecessors();
if (preds.size() == 1) {
return checkBlockForInsnInsert(preds.get(0));
}
return null;
}
return blockNode;
}
private boolean tryWiderObjects(MethodNode mth, SSAVar var) {
Set<ArgType> objTypes = new LinkedHashSet<>();
for (ITypeBound bound : var.getTypeInfo().getBounds()) {
ArgType boundType = bound.getType();
if (boundType.isTypeKnown() && boundType.isObject()) {
objTypes.add(boundType);
}
}
if (objTypes.isEmpty()) {
return false;
}
ClspGraph clsp = mth.root().getClsp();
for (ArgType objType : objTypes) {
for (String ancestor : clsp.getSuperTypes(objType.getObject())) {
ArgType ancestorType = ArgType.object(ancestor);
TypeUpdateResult result = typeUpdate.applyWithWiderAllow(mth, var, ancestorType);
if (result == TypeUpdateResult.CHANGED) {
return true;
}
}
}
return false;
}
@SuppressWarnings("ForLoopReplaceableByForEach")
private boolean tryToFixIncompatiblePrimitives(MethodNode mth) {
boolean fixed = false;
List<SSAVar> ssaVars = mth.getSVars();
int ssaVarsCount = ssaVars.size();
// new vars will be added at a list end if fix is applied (can't use for-each loop here)
for (int i = 0; i < ssaVarsCount; i++) {
if (processIncompatiblePrimitives(mth, ssaVars.get(i))) {
fixed = true;
}
}
if (!fixed) {
return false;
}
InitCodeVariables.rerun(mth);
typeInference.initTypeBounds(mth);
return typeInference.runTypePropagation(mth);
}
private boolean processIncompatiblePrimitives(MethodNode mth, SSAVar var) {
TypeInfo typeInfo = var.getTypeInfo();
if (typeInfo.getType().isTypeKnown()) {
return false;
}
boolean assigned = false;
for (ITypeBound bound : typeInfo.getBounds()) {
ArgType boundType = bound.getType();
switch (bound.getBound()) {
case ASSIGN:
if (!boundType.contains(PrimitiveType.BOOLEAN)) {
return false;
}
assigned = true;
break;
case USE:
if (!boundType.canBeAnyNumber()) {
return false;
}
break;
}
}
if (!assigned) {
return false;
}
boolean fixed = false;
for (RegisterArg arg : new ArrayList<>(var.getUseList())) {
if (fixBooleanUsage(mth, arg)) {
fixed = true;
if (Consts.DEBUG_TYPE_INFERENCE) {
LOG.info("Fixed boolean usage for arg {} from {}", arg, arg.getParentInsn());
}
}
}
return fixed;
}
private boolean fixBooleanUsage(MethodNode mth, RegisterArg boundArg) {
ArgType boundType = boundArg.getInitType();
if (boundType == ArgType.BOOLEAN
|| (boundType.isTypeKnown() && !boundType.isPrimitive())) {
return false;
}
InsnNode insn = boundArg.getParentInsn();
if (insn == null || insn.getType() == InsnType.IF) {
return false;
}
BlockNode blockNode = BlockUtils.getBlockByInsn(mth, insn);
if (blockNode == null) {
return false;
}
List<InsnNode> insnList = blockNode.getInstructions();
int insnIndex = InsnList.getIndex(insnList, insn);
if (insnIndex == -1) {
return false;
}
InsnType insnType = insn.getType();
if (insnType == InsnType.CAST) {
// replace cast
ArgType type = (ArgType) ((IndexInsnNode) insn).getIndex();
TernaryInsn convertInsn = prepareBooleanConvertInsn(insn.getResult(), boundArg, type);
BlockUtils.replaceInsn(mth, blockNode, insnIndex, convertInsn);
return true;
}
if (insnType == InsnType.ARITH) {
ArithNode arithInsn = (ArithNode) insn;
if (arithInsn.getOp() == ArithOp.XOR && arithInsn.getArgsCount() == 2) {
// replace (boolean ^ 1) with (!boolean)
InsnArg secondArg = arithInsn.getArg(1);
if (secondArg.isLiteral() && ((LiteralArg) secondArg).getLiteral() == 1) {
InsnNode convertInsn = notBooleanToInt(arithInsn, boundArg);
BlockUtils.replaceInsn(mth, blockNode, insnIndex, convertInsn);
return true;
}
}
}
// insert before insn
RegisterArg resultArg = boundArg.duplicateWithNewSSAVar(mth);
TernaryInsn convertInsn = prepareBooleanConvertInsn(resultArg, boundArg, boundType);
insnList.add(insnIndex, convertInsn);
insn.replaceArg(boundArg, convertInsn.getResult().duplicate());
return true;
}
private InsnNode notBooleanToInt(ArithNode insn, RegisterArg boundArg) {
InsnNode notInsn = new InsnNode(InsnType.NOT, 1);
notInsn.addArg(boundArg.duplicate());
notInsn.add(AFlag.SYNTHETIC);
ArgType resType = insn.getResult().getType();
if (resType.canBePrimitive(PrimitiveType.BOOLEAN)) {
notInsn.setResult(insn.getResult());
return notInsn;
}
InsnArg notArg = InsnArg.wrapArg(notInsn);
notArg.setType(ArgType.BOOLEAN);
TernaryInsn convertInsn = ModVisitor.makeBooleanConvertInsn(insn.getResult(), notArg, ArgType.INT);
convertInsn.add(AFlag.SYNTHETIC);
return convertInsn;
}
private TernaryInsn prepareBooleanConvertInsn(RegisterArg resultArg, RegisterArg boundArg, ArgType useType) {
RegisterArg useArg = boundArg.getSVar().getAssign().duplicate();
TernaryInsn convertInsn = ModVisitor.makeBooleanConvertInsn(resultArg, useArg, useType);
convertInsn.add(AFlag.SYNTHETIC);
return convertInsn;
}
private boolean tryToForceImmutableTypes(MethodNode mth) {
boolean fixed = false;
for (SSAVar ssaVar : mth.getSVars()) {
ArgType type = ssaVar.getTypeInfo().getType();
if (!type.isTypeKnown() && ssaVar.isTypeImmutable()) {
if (forceImmutableType(ssaVar)) {
fixed = true;
}
}
}
if (!fixed) {
return false;
}
return typeInference.runTypePropagation(mth);
}
private boolean forceImmutableType(SSAVar ssaVar) {
for (RegisterArg useArg : ssaVar.getUseList()) {
InsnNode parentInsn = useArg.getParentInsn();
if (parentInsn != null) {
InsnType insnType = parentInsn.getType();
if (insnType == InsnType.AGET || insnType == InsnType.APUT) {
ssaVar.setType(ssaVar.getImmutableType());
return true;
}
}
}
return false;
}
@Override
public String getName() {
return "FixTypesVisitor";
}
}
@@ -8,7 +8,7 @@ import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.nodes.RootNode;
/**
* Allow to ignore down casts (return arg type instead cast type)
* Allow ignoring down casts (return arg type instead cast type)
* Such casts will be removed later.
*/
public final class TypeBoundCheckCastAssign implements ITypeBoundDynamic {
@@ -36,7 +36,7 @@ public final class TypeBoundCheckCastAssign implements ITypeBoundDynamic {
}
private ArgType getReturnType(ArgType argType) {
ArgType castType = (ArgType) insn.getIndex();
ArgType castType = insn.getIndexAsType();
TypeCompareEnum result = root.getTypeCompare().compareTypes(argType, castType);
return result.isNarrow() ? argType : castType;
}
@@ -18,6 +18,7 @@ import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.exceptions.JadxRuntimeException;
import static jadx.core.dex.visitors.typeinference.TypeCompareEnum.CONFLICT;
import static jadx.core.dex.visitors.typeinference.TypeCompareEnum.CONFLICT_BY_GENERIC;
import static jadx.core.dex.visitors.typeinference.TypeCompareEnum.EQUAL;
import static jadx.core.dex.visitors.typeinference.TypeCompareEnum.NARROW;
import static jadx.core.dex.visitors.typeinference.TypeCompareEnum.NARROW_BY_GENERIC;
@@ -265,6 +266,9 @@ public class TypeCompare {
if (objType.isGenericType()) {
return compareTypeVariables(genericType, objType);
}
if (objType.isWildcard()) {
return CONFLICT_BY_GENERIC;
}
boolean rootObject = objType.equals(ArgType.OBJECT);
List<ArgType> extendTypes = genericType.getExtendTypes();
if (extendTypes.isEmpty()) {
@@ -7,6 +7,7 @@ public enum TypeCompareEnum {
WIDER,
WIDER_BY_GENERIC, // same basic type without generic
CONFLICT,
CONFLICT_BY_GENERIC, // same basic type, conflict in generics
UNKNOWN;
public TypeCompareEnum invert() {
@@ -24,6 +25,7 @@ public enum TypeCompareEnum {
return NARROW_BY_GENERIC;
case CONFLICT:
case CONFLICT_BY_GENERIC:
case EQUAL:
case UNKNOWN:
default:
@@ -51,7 +53,7 @@ public enum TypeCompareEnum {
return isEqual() || isNarrow();
}
public boolean isGeneric() {
return this == WIDER_BY_GENERIC || this == NARROW_BY_GENERIC;
public boolean isConflict() {
return this == CONFLICT || this == CONFLICT_BY_GENERIC;
}
}
@@ -1,29 +1,19 @@
package jadx.core.dex.visitors.typeinference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.Consts;
import jadx.core.clsp.ClspGraph;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.AnonymousClassAttr;
import jadx.core.dex.attributes.nodes.PhiListAttr;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.instructions.ArithNode;
import jadx.core.dex.instructions.ArithOp;
import jadx.core.dex.instructions.BaseInvokeNode;
import jadx.core.dex.instructions.IndexInsnNode;
import jadx.core.dex.instructions.InsnType;
@@ -33,12 +23,9 @@ import jadx.core.dex.instructions.PhiInsn;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.LiteralArg;
import jadx.core.dex.instructions.args.PrimitiveType;
import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.instructions.args.SSAVar;
import jadx.core.dex.instructions.mods.ConstructorInsn;
import jadx.core.dex.instructions.mods.TernaryInsn;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.IMethodDetails;
import jadx.core.dex.nodes.InsnNode;
@@ -49,16 +36,8 @@ import jadx.core.dex.trycatch.ExcHandlerAttr;
import jadx.core.dex.visitors.AbstractVisitor;
import jadx.core.dex.visitors.AttachMethodDetails;
import jadx.core.dex.visitors.ConstInlineVisitor;
import jadx.core.dex.visitors.InitCodeVariables;
import jadx.core.dex.visitors.JadxVisitor;
import jadx.core.dex.visitors.ModVisitor;
import jadx.core.dex.visitors.blocks.BlockSplitter;
import jadx.core.dex.visitors.ssa.SSATransform;
import jadx.core.utils.BlockUtils;
import jadx.core.utils.InsnList;
import jadx.core.utils.InsnUtils;
import jadx.core.utils.ListUtils;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxOverflowException;
@JadxVisitor(
@@ -75,24 +54,11 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
private RootNode root;
private TypeUpdate typeUpdate;
private List<Function<MethodNode, Boolean>> resolvers;
@Override
public void init(RootNode root) {
this.root = root;
this.typeUpdate = root.getTypeUpdate();
this.resolvers = Arrays.asList(
this::initTypeBounds,
this::runTypePropagation,
this::tryRestoreTypeVarCasts,
this::tryInsertCasts,
this::tryDeduceTypes,
this::trySplitConstInsns,
this::tryToFixIncompatiblePrimitives,
this::tryToForceImmutableTypes,
this::tryInsertAdditionalMove,
this::runMultiVariableSearch,
this::tryRemoveGenerics);
}
@Override
@@ -103,73 +69,39 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
if (Consts.DEBUG_TYPE_INFERENCE) {
LOG.info("Start type inference in method: {}", mth);
}
assignImmutableTypes(mth);
try {
for (Function<MethodNode, Boolean> resolver : resolvers) {
if (resolver.apply(mth) && checkTypes(mth)) {
return;
}
}
assignImmutableTypes(mth);
initTypeBounds(mth);
runTypePropagation(mth);
} catch (Exception e) {
mth.addError("Type inference failed with exception", e);
mth.addError("Type inference failed", e);
}
}
/**
* Check if all types resolved
*/
private static boolean checkTypes(MethodNode mth) {
for (SSAVar var : mth.getSVars()) {
ArgType type = var.getTypeInfo().getType();
if (!type.isTypeKnown()) {
return false;
}
}
return true;
}
/**
* Collect initial type bounds from assign and usages
*/
private boolean initTypeBounds(MethodNode mth) {
void initTypeBounds(MethodNode mth) {
List<SSAVar> ssaVars = mth.getSVars();
ssaVars.forEach(this::attachBounds);
ssaVars.forEach(this::mergePhiBounds);
if (Consts.DEBUG_TYPE_INFERENCE) {
ssaVars.forEach(ssaVar -> LOG.debug("Type bounds for {}: {}", ssaVar.toShortString(), ssaVar.getTypeInfo().getBounds()));
ssaVars.stream().sorted()
.forEach(ssaVar -> LOG.debug("Type bounds for {}: {}", ssaVar.toShortString(), ssaVar.getTypeInfo().getBounds()));
}
return false;
}
/**
* Guess type from usage and try to set it to current variable
* and all connected instructions with {@link TypeUpdate#apply(MethodNode, SSAVar, ArgType)}
*/
private boolean runTypePropagation(MethodNode mth) {
boolean runTypePropagation(MethodNode mth) {
List<SSAVar> ssaVars = mth.getSVars();
ssaVars.forEach(var -> setImmutableType(mth, var));
ssaVars.forEach(var -> setBestType(mth, var));
return true;
}
private boolean runMultiVariableSearch(MethodNode mth) {
try {
TypeSearch typeSearch = new TypeSearch(mth);
if (!typeSearch.run()) {
mth.addWarnComment("Multi-variable type inference failed");
}
for (SSAVar var : mth.getSVars()) {
if (!var.getTypeInfo().getType().isTypeKnown()) {
return false;
}
}
return true;
} catch (Exception e) {
mth.addWarnComment("Multi-variable type inference failed. Error: " + Utils.getStackTrace(e));
return false;
}
}
private void setImmutableType(MethodNode mth, SSAVar ssaVar) {
try {
ArgType immutableType = ssaVar.getImmutableType();
@@ -186,18 +118,17 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
}
}
private boolean setBestType(MethodNode mth, SSAVar ssaVar) {
private void setBestType(MethodNode mth, SSAVar ssaVar) {
try {
return calculateFromBounds(mth, ssaVar);
calculateFromBounds(mth, ssaVar);
} catch (JadxOverflowException e) {
throw e;
} catch (Exception e) {
mth.addWarnComment("Failed to calculate best type for var: " + ssaVar, e);
return false;
}
}
private boolean calculateFromBounds(MethodNode mth, SSAVar ssaVar) {
private void calculateFromBounds(MethodNode mth, SSAVar ssaVar) {
TypeInfo typeInfo = ssaVar.getTypeInfo();
Set<ITypeBound> bounds = typeInfo.getBounds();
Optional<ArgType> bestTypeOpt = selectBestTypeFromBounds(bounds);
@@ -208,21 +139,17 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
LOG.warn(" {}", bound);
}
}
return false;
return;
}
ArgType candidateType = bestTypeOpt.get();
TypeUpdateResult result = typeUpdate.apply(mth, ssaVar, candidateType);
if (result == TypeUpdateResult.REJECT) {
if (Consts.DEBUG_TYPE_INFERENCE) {
if (ssaVar.getTypeInfo().getType().equals(candidateType)) {
LOG.info("Same type rejected: {} -> {}, bounds: {}", ssaVar, candidateType, bounds);
} else if (candidateType.isTypeKnown()) {
LOG.debug("Type rejected: {} -> {}, bounds: {}", ssaVar, candidateType, bounds);
}
if (Consts.DEBUG_TYPE_INFERENCE && result == TypeUpdateResult.REJECT) {
if (ssaVar.getTypeInfo().getType().equals(candidateType)) {
LOG.info("Same type rejected: {} -> {}, bounds: {}", ssaVar, candidateType, bounds);
} else if (candidateType.isTypeKnown()) {
LOG.debug("Type rejected: {} -> {}, bounds: {}", ssaVar, candidateType, bounds);
}
return false;
}
return result == TypeUpdateResult.CHANGED;
}
private Optional<ArgType> selectBestTypeFromBounds(Set<ITypeBound> bounds) {
@@ -310,7 +237,11 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
break;
case CHECK_CAST:
addBound(typeInfo, new TypeBoundCheckCastAssign(root, (IndexInsnNode) insn));
if (insn.contains(AFlag.SOFT_CAST)) {
// ignore bound, will run checks on update
} else {
addBound(typeInfo, new TypeBoundCheckCastAssign(root, (IndexInsnNode) insn));
}
break;
default:
@@ -396,7 +327,7 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
return new TypeBoundInvokeUse(root, invoke, regArg, argType);
}
// for override methods use origin declared class as type
// for override methods use origin declared class as a type
if (methodDetails instanceof MethodNode) {
MethodNode callMth = (MethodNode) methodDetails;
ClassInfo declCls = methodUtils.getMethodOriginDeclClass(callMth);
@@ -405,621 +336,7 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
return null;
}
private boolean tryPossibleTypes(MethodNode mth, SSAVar var, ArgType type) {
List<ArgType> types = makePossibleTypesList(type, var);
if (types.isEmpty()) {
return false;
}
for (ArgType candidateType : types) {
TypeUpdateResult result = typeUpdate.apply(mth, var, candidateType);
if (result == TypeUpdateResult.CHANGED) {
return true;
}
}
return false;
}
private List<ArgType> makePossibleTypesList(ArgType type, @Nullable SSAVar var) {
if (type.isArray()) {
List<ArgType> list = new ArrayList<>();
for (ArgType arrElemType : makePossibleTypesList(type.getArrayElement(), null)) {
list.add(ArgType.array(arrElemType));
}
return list;
}
if (var != null) {
for (ITypeBound b : var.getTypeInfo().getBounds()) {
ArgType boundType = b.getType();
if (boundType.isObject() || boundType.isArray()) {
// don't add primitive types
return Collections.emptyList();
}
}
}
List<ArgType> list = new ArrayList<>();
for (PrimitiveType possibleType : type.getPossibleTypes()) {
if (possibleType == PrimitiveType.VOID) {
continue;
}
list.add(ArgType.convertFromPrimitiveType(possibleType));
}
return list;
}
private boolean tryDeduceTypes(MethodNode mth) {
boolean fixed = false;
for (SSAVar ssaVar : mth.getSVars()) {
if (deduceType(mth, ssaVar)) {
fixed = true;
}
}
return fixed;
}
@SuppressWarnings("RedundantIfStatement")
private boolean deduceType(MethodNode mth, SSAVar var) {
if (var.isTypeImmutable()) {
return false;
}
ArgType type = var.getTypeInfo().getType();
if (type.isTypeKnown()) {
return false;
}
// try best type from bounds again
if (setBestType(mth, var)) {
return true;
}
// try all possible types (useful for primitives)
if (tryPossibleTypes(mth, var, type)) {
return true;
}
// for objects try super types
if (tryWiderObjects(mth, var)) {
return true;
}
return false;
}
private boolean tryRemoveGenerics(MethodNode mth) {
boolean resolved = true;
for (SSAVar var : mth.getSVars()) {
ArgType type = var.getTypeInfo().getType();
if (!type.isTypeKnown()
&& !var.isTypeImmutable()
&& !tryRawType(mth, var)) {
resolved = false;
}
}
return resolved;
}
private boolean tryRawType(MethodNode mth, SSAVar var) {
Set<ArgType> objTypes = new LinkedHashSet<>();
for (ITypeBound bound : var.getTypeInfo().getBounds()) {
ArgType boundType = bound.getType();
if (boundType.isTypeKnown() && boundType.isObject()) {
objTypes.add(boundType);
}
}
if (objTypes.isEmpty()) {
return false;
}
for (ArgType objType : objTypes) {
if (checkRawType(mth, var, objType)) {
mth.addDebugComment("Type inference failed for " + var.toShortString() + "."
+ " Raw type applied. Possible types: " + Utils.listToString(objTypes));
return true;
}
}
return false;
}
private boolean checkRawType(MethodNode mth, SSAVar var, ArgType objType) {
if (objType.isObject() && objType.containsGeneric()) {
ArgType rawType = objType.isGenericType() ? ArgType.OBJECT : ArgType.object(objType.getObject());
TypeUpdateResult result = typeUpdate.applyWithWiderAllow(mth, var, rawType);
return result == TypeUpdateResult.CHANGED;
}
return false;
}
/**
* Fix check casts to type var extend type:
* <br>
* {@code <T extends Comparable> T var = (Comparable) obj; => T var = (T) obj; }
*/
private boolean tryRestoreTypeVarCasts(MethodNode mth) {
int changed = 0;
List<SSAVar> mthSVars = mth.getSVars();
for (SSAVar var : mthSVars) {
changed += restoreTypeVarCasts(var);
}
if (changed == 0) {
return false;
}
if (Consts.DEBUG_TYPE_INFERENCE) {
mth.addDebugComment("Restore " + changed + " type vars casts");
}
initTypeBounds(mth);
return runTypePropagation(mth);
}
private int restoreTypeVarCasts(SSAVar var) {
TypeInfo typeInfo = var.getTypeInfo();
Set<ITypeBound> bounds = typeInfo.getBounds();
if (!ListUtils.anyMatch(bounds, t -> t.getType().isGenericType())) {
return 0;
}
List<ITypeBound> casts = ListUtils.filter(bounds, TypeBoundCheckCastAssign.class::isInstance);
if (casts.isEmpty()) {
return 0;
}
ArgType bestType = selectBestTypeFromBounds(bounds).orElse(ArgType.UNKNOWN);
if (!bestType.isGenericType()) {
return 0;
}
List<ArgType> extendTypes = bestType.getExtendTypes();
if (extendTypes.size() != 1) {
return 0;
}
int fixed = 0;
ArgType extendType = extendTypes.get(0);
for (ITypeBound bound : casts) {
TypeBoundCheckCastAssign cast = (TypeBoundCheckCastAssign) bound;
ArgType castType = cast.getType();
TypeCompareEnum result = typeUpdate.getTypeCompare().compareTypes(extendType, castType);
if (result.isEqual() || result == TypeCompareEnum.NARROW_BY_GENERIC) {
cast.getInsn().updateIndex(bestType);
fixed++;
}
}
return fixed;
}
@SuppressWarnings({ "ForLoopReplaceableByWhile", "ForLoopReplaceableByForEach" })
private boolean tryInsertCasts(MethodNode mth) {
int added = 0;
List<SSAVar> mthSVars = mth.getSVars();
int varsCount = mthSVars.size();
for (int i = 0; i < varsCount; i++) {
SSAVar var = mthSVars.get(i);
ArgType type = var.getTypeInfo().getType();
if (!type.isTypeKnown() && !var.isTypeImmutable()) {
added += tryInsertVarCast(mth, var);
}
}
if (added != 0) {
if (Consts.DEBUG_TYPE_INFERENCE) {
mth.addDebugComment("Additional " + added + " cast instructions added to help type inference");
}
InitCodeVariables.rerun(mth);
initTypeBounds(mth);
return runTypePropagation(mth);
}
return false;
}
private int tryInsertVarCast(MethodNode mth, SSAVar var) {
for (ITypeBound bound : var.getTypeInfo().getBounds()) {
ArgType boundType = bound.getType();
if (boundType.isTypeKnown()
&& !boundType.equals(var.getTypeInfo().getType())
&& boundType.containsTypeVariable()
&& !root.getTypeUtils().containsUnknownTypeVar(mth, boundType)) {
if (insertAssignCast(mth, var, boundType)) {
return 1;
}
return insertUseCasts(mth, var);
}
}
return 0;
}
private int insertUseCasts(MethodNode mth, SSAVar var) {
List<RegisterArg> useList = var.getUseList();
if (useList.isEmpty()) {
return 0;
}
int useCasts = 0;
for (RegisterArg useReg : new ArrayList<>(useList)) {
if (insertSoftUseCast(mth, useReg)) {
useCasts++;
}
}
return useCasts;
}
private boolean insertAssignCast(MethodNode mth, SSAVar var, ArgType castType) {
RegisterArg assignArg = var.getAssign();
InsnNode assignInsn = assignArg.getParentInsn();
if (assignInsn == null || assignInsn.getType() == InsnType.PHI) {
return false;
}
BlockNode assignBlock = BlockUtils.getBlockByInsn(mth, assignInsn);
if (assignBlock == null) {
return false;
}
assignInsn.setResult(assignArg.duplicateWithNewSSAVar(mth));
IndexInsnNode castInsn = makeSoftCastInsn(assignArg.duplicate(), assignInsn.getResult().duplicate(), castType);
return BlockUtils.insertAfterInsn(assignBlock, assignInsn, castInsn);
}
private boolean insertSoftUseCast(MethodNode mth, RegisterArg useArg) {
InsnNode useInsn = useArg.getParentInsn();
if (useInsn == null || useInsn.getType() == InsnType.PHI) {
return false;
}
if (useInsn.getType() == InsnType.IF && useInsn.getArg(1).isZeroLiteral()) {
// cast not needed if compare with null
return false;
}
BlockNode useBlock = BlockUtils.getBlockByInsn(mth, useInsn);
if (useBlock == null) {
return false;
}
IndexInsnNode castInsn = makeSoftCastInsn(
useArg.duplicateWithNewSSAVar(mth),
useArg.duplicate(),
useArg.getInitType());
useInsn.replaceArg(useArg, castInsn.getResult().duplicate());
return BlockUtils.insertBeforeInsn(useBlock, useInsn, castInsn);
}
@NotNull
private IndexInsnNode makeSoftCastInsn(RegisterArg result, RegisterArg arg, ArgType castType) {
IndexInsnNode castInsn = new IndexInsnNode(InsnType.CHECK_CAST, castType, 1);
castInsn.setResult(result);
castInsn.addArg(arg);
castInsn.add(AFlag.SOFT_CAST);
castInsn.add(AFlag.SYNTHETIC);
return castInsn;
}
private boolean trySplitConstInsns(MethodNode mth) {
boolean constSplit = false;
for (SSAVar var : new ArrayList<>(mth.getSVars())) {
if (checkAndSplitConstInsn(mth, var)) {
constSplit = true;
}
}
if (!constSplit) {
return false;
}
InitCodeVariables.rerun(mth);
initTypeBounds(mth);
return runTypePropagation(mth);
}
private boolean checkAndSplitConstInsn(MethodNode mth, SSAVar var) {
ArgType type = var.getTypeInfo().getType();
if (type.isTypeKnown() || var.isTypeImmutable()) {
return false;
}
if (var.getUsedInPhi().size() < 2) {
return false;
}
InsnNode assignInsn = var.getAssign().getAssignInsn();
InsnNode constInsn = InsnUtils.checkInsnType(assignInsn, InsnType.CONST);
if (constInsn == null) {
return false;
}
BlockNode blockNode = BlockUtils.getBlockByInsn(mth, constInsn);
if (blockNode == null) {
return false;
}
// for every PHI make separate CONST insn
boolean first = true;
for (PhiInsn phiInsn : var.getUsedInPhi()) {
if (first) {
first = false;
continue;
}
InsnNode copyInsn = constInsn.copyWithNewSsaVar(mth);
copyInsn.add(AFlag.SYNTHETIC);
BlockUtils.insertAfterInsn(blockNode, constInsn, copyInsn);
RegisterArg phiArg = phiInsn.getArgBySsaVar(var);
phiInsn.replaceArg(phiArg, copyInsn.getResult().duplicate());
}
return true;
}
private boolean tryInsertAdditionalMove(MethodNode mth) {
int insnsAdded = 0;
for (BlockNode block : mth.getBasicBlocks()) {
PhiListAttr phiListAttr = block.get(AType.PHI_LIST);
if (phiListAttr != null) {
for (PhiInsn phiInsn : phiListAttr.getList()) {
insnsAdded += tryInsertAdditionalInsn(mth, phiInsn);
}
}
}
if (insnsAdded == 0) {
return false;
}
if (Consts.DEBUG_TYPE_INFERENCE) {
mth.addDebugComment("Additional " + insnsAdded + " move instructions added to help type inference");
}
InitCodeVariables.rerun(mth);
initTypeBounds(mth);
if (runTypePropagation(mth) && checkTypes(mth)) {
return true;
}
return tryDeduceTypes(mth);
}
/**
* Add MOVE instruction before PHI in bound blocks to make 'soft' type link.
* This allows to use different types in blocks merged by PHI.
*/
private int tryInsertAdditionalInsn(MethodNode mth, PhiInsn phiInsn) {
ArgType phiType = getCommonTypeForPhiArgs(phiInsn);
if (phiType != null && phiType.isTypeKnown()) {
// all args have same known type => nothing to do here
return 0;
}
// check if instructions can be inserted
if (insertMovesForPhi(mth, phiInsn, false) == 0) {
return 0;
}
// check passed => apply
return insertMovesForPhi(mth, phiInsn, true);
}
@Nullable
private ArgType getCommonTypeForPhiArgs(PhiInsn phiInsn) {
ArgType phiArgType = null;
for (InsnArg arg : phiInsn.getArguments()) {
ArgType type = arg.getType();
if (phiArgType == null) {
phiArgType = type;
} else if (!phiArgType.equals(type)) {
return null;
}
}
return phiArgType;
}
private int insertMovesForPhi(MethodNode mth, PhiInsn phiInsn, boolean apply) {
int argsCount = phiInsn.getArgsCount();
int count = 0;
for (int argIndex = 0; argIndex < argsCount; argIndex++) {
RegisterArg reg = phiInsn.getArg(argIndex);
BlockNode startBlock = phiInsn.getBlockByArgIndex(argIndex);
BlockNode blockNode = checkBlockForInsnInsert(startBlock);
if (blockNode == null) {
mth.addDebugComment("Failed to insert an additional move for type inference into block " + startBlock);
return 0;
}
boolean add = true;
SSAVar var = reg.getSVar();
InsnNode assignInsn = var.getAssign().getAssignInsn();
if (assignInsn != null) {
InsnType assignType = assignInsn.getType();
if (assignType == InsnType.CONST
|| (assignType == InsnType.MOVE && var.getUseCount() == 1)) {
add = false;
}
}
if (add) {
count++;
if (apply) {
insertMove(mth, blockNode, phiInsn, reg);
}
}
}
return count;
}
private void insertMove(MethodNode mth, BlockNode blockNode, PhiInsn phiInsn, RegisterArg reg) {
SSAVar var = reg.getSVar();
int regNum = reg.getRegNum();
RegisterArg resultArg = reg.duplicate(regNum, null);
SSAVar newSsaVar = mth.makeNewSVar(resultArg);
RegisterArg arg = reg.duplicate(regNum, var);
InsnNode moveInsn = new InsnNode(InsnType.MOVE, 1);
moveInsn.setResult(resultArg);
moveInsn.addArg(arg);
moveInsn.add(AFlag.SYNTHETIC);
blockNode.getInstructions().add(moveInsn);
phiInsn.replaceArg(reg, reg.duplicate(regNum, newSsaVar));
}
@Nullable
private BlockNode checkBlockForInsnInsert(BlockNode blockNode) {
if (blockNode.isSynthetic()) {
return null;
}
InsnNode lastInsn = BlockUtils.getLastInsn(blockNode);
if (lastInsn != null && BlockSplitter.isSeparate(lastInsn.getType())) {
// can't insert move in a block with 'separate' instruction => try previous block by simple path
List<BlockNode> preds = blockNode.getPredecessors();
if (preds.size() == 1) {
return checkBlockForInsnInsert(preds.get(0));
}
return null;
}
return blockNode;
}
private boolean tryWiderObjects(MethodNode mth, SSAVar var) {
Set<ArgType> objTypes = new LinkedHashSet<>();
for (ITypeBound bound : var.getTypeInfo().getBounds()) {
ArgType boundType = bound.getType();
if (boundType.isTypeKnown() && boundType.isObject()) {
objTypes.add(boundType);
}
}
if (objTypes.isEmpty()) {
return false;
}
ClspGraph clsp = mth.root().getClsp();
for (ArgType objType : objTypes) {
for (String ancestor : clsp.getSuperTypes(objType.getObject())) {
ArgType ancestorType = ArgType.object(ancestor);
TypeUpdateResult result = typeUpdate.applyWithWiderAllow(mth, var, ancestorType);
if (result == TypeUpdateResult.CHANGED) {
return true;
}
}
}
return false;
}
@SuppressWarnings("ForLoopReplaceableByForEach")
private boolean tryToFixIncompatiblePrimitives(MethodNode mth) {
boolean fixed = false;
List<SSAVar> ssaVars = mth.getSVars();
int ssaVarsCount = ssaVars.size();
// new vars will be added at list end if fix is applied (can't use for-each loop)
for (int i = 0; i < ssaVarsCount; i++) {
if (processIncompatiblePrimitives(mth, ssaVars.get(i))) {
fixed = true;
}
}
if (!fixed) {
return false;
}
InitCodeVariables.rerun(mth);
initTypeBounds(mth);
return runTypePropagation(mth);
}
private boolean processIncompatiblePrimitives(MethodNode mth, SSAVar var) {
TypeInfo typeInfo = var.getTypeInfo();
if (typeInfo.getType().isTypeKnown()) {
return false;
}
boolean assigned = false;
for (ITypeBound bound : typeInfo.getBounds()) {
ArgType boundType = bound.getType();
switch (bound.getBound()) {
case ASSIGN:
if (!boundType.contains(PrimitiveType.BOOLEAN)) {
return false;
}
assigned = true;
break;
case USE:
if (!boundType.canBeAnyNumber()) {
return false;
}
break;
}
}
if (!assigned) {
return false;
}
boolean fixed = false;
for (RegisterArg arg : new ArrayList<>(var.getUseList())) {
if (fixBooleanUsage(mth, arg)) {
fixed = true;
}
}
return fixed;
}
private boolean fixBooleanUsage(MethodNode mth, RegisterArg boundArg) {
ArgType boundType = boundArg.getInitType();
if (boundType == ArgType.BOOLEAN
|| (boundType.isTypeKnown() && !boundType.isPrimitive())) {
return false;
}
InsnNode insn = boundArg.getParentInsn();
if (insn == null || insn.getType() == InsnType.IF) {
return false;
}
BlockNode blockNode = BlockUtils.getBlockByInsn(mth, insn);
if (blockNode == null) {
return false;
}
List<InsnNode> insnList = blockNode.getInstructions();
int insnIndex = InsnList.getIndex(insnList, insn);
if (insnIndex == -1) {
return false;
}
InsnType insnType = insn.getType();
if (insnType == InsnType.CAST) {
// replace cast
ArgType type = (ArgType) ((IndexInsnNode) insn).getIndex();
TernaryInsn convertInsn = prepareBooleanConvertInsn(insn.getResult(), boundArg, type);
BlockUtils.replaceInsn(mth, blockNode, insnIndex, convertInsn);
return true;
}
if (insnType == InsnType.ARITH) {
ArithNode arithInsn = (ArithNode) insn;
if (arithInsn.getOp() == ArithOp.XOR && arithInsn.getArgsCount() == 2) {
// replace (boolean ^ 1) with (!boolean)
InsnArg secondArg = arithInsn.getArg(1);
if (secondArg.isLiteral() && ((LiteralArg) secondArg).getLiteral() == 1) {
InsnNode convertInsn = notBooleanToInt(arithInsn, boundArg);
BlockUtils.replaceInsn(mth, blockNode, insnIndex, convertInsn);
return true;
}
}
}
// insert before insn
RegisterArg resultArg = boundArg.duplicateWithNewSSAVar(mth);
TernaryInsn convertInsn = prepareBooleanConvertInsn(resultArg, boundArg, boundType);
insnList.add(insnIndex, convertInsn);
insn.replaceArg(boundArg, convertInsn.getResult().duplicate());
return true;
}
private InsnNode notBooleanToInt(ArithNode insn, RegisterArg boundArg) {
InsnNode notInsn = new InsnNode(InsnType.NOT, 1);
notInsn.addArg(boundArg.duplicate());
notInsn.add(AFlag.SYNTHETIC);
InsnArg notArg = InsnArg.wrapArg(notInsn);
notArg.setType(ArgType.BOOLEAN);
TernaryInsn convertInsn = ModVisitor.makeBooleanConvertInsn(insn.getResult(), notArg, ArgType.INT);
convertInsn.add(AFlag.SYNTHETIC);
return convertInsn;
}
private TernaryInsn prepareBooleanConvertInsn(RegisterArg resultArg, RegisterArg boundArg, ArgType useType) {
RegisterArg useArg = boundArg.getSVar().getAssign().duplicate();
TernaryInsn convertInsn = ModVisitor.makeBooleanConvertInsn(resultArg, useArg, useType);
convertInsn.add(AFlag.SYNTHETIC);
return convertInsn;
}
private boolean tryToForceImmutableTypes(MethodNode mth) {
boolean fixed = false;
for (SSAVar ssaVar : mth.getSVars()) {
ArgType type = ssaVar.getTypeInfo().getType();
if (!type.isTypeKnown() && ssaVar.isTypeImmutable()) {
if (forceImmutableType(ssaVar)) {
fixed = true;
}
}
}
if (!fixed) {
return false;
}
return runTypePropagation(mth);
}
private boolean forceImmutableType(SSAVar ssaVar) {
for (RegisterArg useArg : ssaVar.getUseList()) {
InsnNode parentInsn = useArg.getParentInsn();
if (parentInsn != null) {
InsnType insnType = parentInsn.getType();
if (insnType == InsnType.AGET || insnType == InsnType.APUT) {
ssaVar.setType(ssaVar.getImmutableType());
return true;
}
}
}
return false;
}
private static void assignImmutableTypes(MethodNode mth) {
private void assignImmutableTypes(MethodNode mth) {
for (SSAVar ssaVar : mth.getSVars()) {
ArgType immutableType = getSsaImmutableType(ssaVar);
if (immutableType != null) {
@@ -1040,4 +357,9 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
}
return null;
}
@Override
public String getName() {
return "TypeInferenceVisitor";
}
}
@@ -67,10 +67,9 @@ public class TypeSearch {
if (vars.isEmpty()) {
searchSuccess = true;
} else {
search(vars);
searchSuccess = fullCheck(vars);
searchSuccess = search(vars) && fullCheck(vars);
if (Consts.DEBUG_TYPE_INFERENCE && !searchSuccess) {
LOG.debug("Multi-variable search failed in {}", mth);
LOG.debug("Multi-variable search failed");
}
}
if (searchSuccess) {
@@ -110,7 +109,7 @@ public class TypeSearch {
private boolean search(List<TypeSearchVarInfo> vars) {
int len = vars.size();
if (Consts.DEBUG_TYPE_INFERENCE) {
LOG.debug("Run search for {} vars: ", len);
LOG.debug("Run multi-variable search for {} vars: ", len);
StringBuilder sb = new StringBuilder();
long count = 1;
for (TypeSearchVarInfo var : vars) {
@@ -120,7 +119,7 @@ public class TypeSearch {
count *= size;
}
sb.append(" = ").append(count);
LOG.debug(" > max iterations count = {}", sb);
LOG.debug(" max iterations count = {}", sb);
}
// prepare vars
@@ -8,12 +8,13 @@ import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.Consts;
import jadx.core.clsp.ClspClass;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.instructions.ArithNode;
import jadx.core.dex.instructions.BaseInvokeNode;
import jadx.core.dex.instructions.IndexInsnNode;
@@ -83,14 +84,13 @@ public final class TypeUpdate {
if (result == REJECT) {
return result;
}
List<TypeUpdateEntry> updates = updateInfo.getUpdates();
if (updates.isEmpty()) {
if (updateInfo.isEmpty()) {
return SAME;
}
if (Consts.DEBUG_TYPE_INFERENCE) {
LOG.debug("Applying type {} to {}", ssaVar.toShortString(), candidateType);
updates.forEach(updateEntry -> LOG.debug(" {} -> {} in {}",
updateEntry.getType(), updateEntry.getArg(), updateEntry.getArg().getParentInsn()));
LOG.debug("Applying type {} to {}:", candidateType, ssaVar.toShortString());
updateInfo.getSortedUpdates().forEach(upd -> LOG.debug(" {} -> {} in {}",
upd.getType(), upd.getArg().toShortString(), upd.getArg().getParentInsn()));
}
updateInfo.applyUpdates();
return CHANGED;
@@ -100,6 +100,21 @@ public final class TypeUpdate {
if (candidateType == null) {
throw new JadxRuntimeException("Null type update for arg: " + arg);
}
if (updateInfo.isProcessed(arg)) {
return CHANGED;
}
TypeUpdateResult res = verifyType(updateInfo, arg, candidateType);
if (res != null) {
return res;
}
if (arg instanceof RegisterArg) {
RegisterArg reg = (RegisterArg) arg;
return updateTypeForSsaVar(updateInfo, reg.getSVar(), candidateType);
}
return requestUpdate(updateInfo, arg, candidateType);
}
private @Nullable TypeUpdateResult verifyType(TypeUpdateInfo updateInfo, InsnArg arg, ArgType candidateType) {
ArgType currentType = arg.getType();
if (Objects.equals(currentType, candidateType)) {
if (!updateInfo.getFlags().isIgnoreSame()) {
@@ -114,6 +129,12 @@ public final class TypeUpdate {
}
TypeCompareEnum compareResult = comparator.compareTypes(candidateType, currentType);
if (compareResult.isConflict()) {
if (Consts.DEBUG_TYPE_INFERENCE) {
LOG.debug("Type rejected for {}: candidate={} in conflict with current={}", arg, candidateType, currentType);
}
return REJECT;
}
if (compareResult == TypeCompareEnum.UNKNOWN && updateInfo.getFlags().isIgnoreUnknown()) {
return REJECT;
}
@@ -144,11 +165,7 @@ public final class TypeUpdate {
}
}
}
if (arg instanceof RegisterArg) {
RegisterArg reg = (RegisterArg) arg;
return updateTypeForSsaVar(updateInfo, reg.getSVar(), candidateType);
}
return requestUpdate(updateInfo, arg, candidateType);
return null;
}
private TypeUpdateResult updateTypeForSsaVar(TypeUpdateInfo updateInfo, SSAVar ssaVar, ArgType candidateType) {
@@ -163,25 +180,25 @@ public final class TypeUpdate {
if (!inBounds(updateInfo, ssaVar, typeInfo.getBounds(), candidateType)) {
return REJECT;
}
return requestUpdateForSsaVar(updateInfo, ssaVar, candidateType);
}
@NotNull
private TypeUpdateResult requestUpdateForSsaVar(TypeUpdateInfo updateInfo, SSAVar ssaVar, ArgType candidateType) {
boolean allSame = true;
TypeUpdateResult result = requestUpdate(updateInfo, ssaVar.getAssign(), candidateType);
if (result == REJECT) {
return result;
boolean allSame = result == SAME;
if (result != REJECT) {
List<RegisterArg> useList = ssaVar.getUseList();
for (RegisterArg arg : useList) {
result = requestUpdate(updateInfo, arg, candidateType);
if (result == REJECT) {
break;
}
if (result != SAME) {
allSame = false;
}
}
}
List<RegisterArg> useList = ssaVar.getUseList();
for (RegisterArg arg : useList) {
TypeUpdateResult useResult = requestUpdate(updateInfo, arg, candidateType);
if (useResult == REJECT) {
return REJECT;
}
if (useResult != SAME) {
allSame = false;
}
if (result == REJECT) {
// rollback update for all registers in current SSA var
updateInfo.rollbackUpdate(ssaVar.getAssign());
ssaVar.getUseList().forEach(updateInfo::rollbackUpdate);
return REJECT;
}
return allSame ? SAME : CHANGED;
}
@@ -191,7 +208,6 @@ public final class TypeUpdate {
return CHANGED;
}
updateInfo.requestUpdate(arg, candidateType);
updateInfo.checkUpdatesCount();
try {
TypeUpdateResult result = runListeners(updateInfo, arg, candidateType);
if (result == REJECT) {
@@ -199,7 +215,8 @@ public final class TypeUpdate {
}
return result;
} catch (StackOverflowError | BootstrapMethodError error) {
throw new JadxOverflowException("Type update terminated with stack overflow, arg: " + arg);
throw new JadxOverflowException("Type update terminated with stack overflow, arg: " + arg
+ ", method size: " + updateInfo.getMth().getInsnsCount());
}
}
@@ -235,7 +252,7 @@ public final class TypeUpdate {
}
if (boundType != null && !checkBound(candidateType, bound, boundType)) {
if (Consts.DEBUG_TYPE_INFERENCE) {
LOG.debug("Reject type '{}' for {} by bound: {}", candidateType, ssaVar, bound);
LOG.debug("Reject type '{}' for {} by bound: {} from {}", candidateType, ssaVar, boundType, bound);
}
return false;
}
@@ -265,6 +282,7 @@ public final class TypeUpdate {
return true;
case CONFLICT:
case CONFLICT_BY_GENERIC:
return false;
case UNKNOWN:
@@ -407,6 +425,9 @@ public final class TypeUpdate {
private TypeUpdateResult sameFirstArgListener(TypeUpdateInfo updateInfo, InsnNode insn, InsnArg arg, ArgType candidateType) {
InsnArg changeArg = isAssign(insn, arg) ? insn.getArg(0) : insn.getResult();
if (updateInfo.hasUpdateWithType(changeArg, candidateType)) {
return CHANGED;
}
return updateTypeChecked(updateInfo, changeArg, candidateType);
}
@@ -500,15 +521,43 @@ public final class TypeUpdate {
TypeUpdateResult result = updateTypeChecked(updateInfo, insnArg, candidateType);
return result == REJECT ? SAME : result;
}
if (candidateType.containsGeneric()) {
ArgType castType = (ArgType) checkCast.getIndex();
TypeCompareEnum compResult = comparator.compareTypes(candidateType, castType);
if (compResult == TypeCompareEnum.NARROW_BY_GENERIC) {
// propagate generic type to result
return updateTypeChecked(updateInfo, checkCast.getResult(), candidateType);
ArgType castType = (ArgType) checkCast.getIndex();
TypeCompareEnum res = comparator.compareTypes(candidateType, castType);
if (res == TypeCompareEnum.CONFLICT) {
// allow casting one interface to another
if (!isInterfaces(candidateType, castType)) {
return REJECT;
}
}
return SAME;
if (res == TypeCompareEnum.CONFLICT_BY_GENERIC) {
if (!insn.contains(AFlag.SOFT_CAST)) {
return REJECT;
}
}
if (res == TypeCompareEnum.NARROW_BY_GENERIC && candidateType.containsGeneric()) {
// propagate generic type to result
return updateTypeChecked(updateInfo, checkCast.getResult(), candidateType);
}
ArgType currentType = checkCast.getArg(0).getType();
return candidateType.equals(currentType) ? SAME : CHANGED;
}
private boolean isInterfaces(ArgType firstType, ArgType secondType) {
if (!firstType.isObject() || !secondType.isObject()) {
return false;
}
ClspClass firstCls = root.getClsp().getClsDetails(firstType);
ClspClass secondCls = root.getClsp().getClsDetails(secondType);
if (firstCls != null && !firstCls.isInterface()) {
return false;
}
if (secondCls != null && !secondCls.isInterface()) {
return false;
}
if (firstCls == null || secondCls == null) {
return true;
}
return secondCls.isInterface() && firstCls.isInterface();
}
private TypeUpdateResult arrayGetListener(TypeUpdateInfo updateInfo, InsnNode insn, InsnArg arg, ArgType candidateType) {
@@ -1,17 +1,25 @@
package jadx.core.dex.visitors.typeinference;
import org.jetbrains.annotations.NotNull;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.instructions.args.InsnArg;
public final class TypeUpdateEntry {
public final class TypeUpdateEntry implements Comparable<TypeUpdateEntry> {
private final int seq;
private final InsnArg arg;
private final ArgType type;
public TypeUpdateEntry(InsnArg arg, ArgType type) {
public TypeUpdateEntry(int seq, InsnArg arg, ArgType type) {
this.seq = seq;
this.arg = arg;
this.type = type;
}
public int getSeq() {
return seq;
}
public InsnArg getArg() {
return arg;
}
@@ -20,8 +28,13 @@ public final class TypeUpdateEntry {
return type;
}
@Override
public int compareTo(@NotNull TypeUpdateEntry other) {
return Integer.compare(this.seq, other.seq);
}
@Override
public String toString() {
return "TypeUpdateEntry{" + arg + " -> " + type + '}';
return type + " -> " + arg.toShortString() + " in " + arg.getParentInsn();
}
}
@@ -1,7 +1,5 @@
package jadx.core.dex.visitors.typeinference;
import org.jetbrains.annotations.NotNull;
public class TypeUpdateFlags {
private static final int ALLOW_WIDER = 1;
private static final int IGNORE_SAME = 2;
@@ -14,7 +12,6 @@ public class TypeUpdateFlags {
private final int flags;
@NotNull
private static TypeUpdateFlags build(int flags) {
return new TypeUpdateFlags(flags);
}
@@ -1,73 +1,84 @@
package jadx.core.dex.visitors.typeinference;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.exceptions.JadxOverflowException;
import jadx.core.utils.exceptions.JadxRuntimeException;
public class TypeUpdateInfo {
private final MethodNode mth;
private final TypeUpdateFlags flags;
private final List<TypeUpdateEntry> updates = new ArrayList<>();
private final Map<InsnArg, TypeUpdateEntry> updateMap = new IdentityHashMap<>();
private final int updatesLimitCount;
private int updateSeq = 0;
public TypeUpdateInfo(MethodNode mth, TypeUpdateFlags flags) {
this.mth = mth;
this.flags = flags;
this.updatesLimitCount = mth.getInsnsCount() * 5; // maximum registers count to update at once
this.updatesLimitCount = mth.getInsnsCount() * 10;
}
public void requestUpdate(InsnArg arg, ArgType changeType) {
updates.add(new TypeUpdateEntry(arg, changeType));
TypeUpdateEntry prev = updateMap.put(arg, new TypeUpdateEntry(updateSeq++, arg, changeType));
if (prev != null) {
throw new JadxRuntimeException("Unexpected type update override for arg: " + arg
+ " types: prev=" + prev.getType() + ", new=" + changeType
+ ", insn: " + arg.getParentInsn());
}
if (updateSeq > updatesLimitCount) {
throw new JadxOverflowException("Type inference error: updates count limit reached");
}
}
public void rollbackUpdate(InsnArg arg) {
TypeUpdateEntry removed = updateMap.remove(arg);
if (removed != null) {
int seq = removed.getSeq();
updateMap.values().removeIf(upd -> upd.getSeq() > seq);
}
}
public void applyUpdates() {
for (TypeUpdateEntry updateEntry : updates) {
InsnArg arg = updateEntry.getArg();
arg.setType(updateEntry.getType());
}
updateMap.values().stream().sorted()
.forEach(upd -> upd.getArg().setType(upd.getType()));
}
public boolean isProcessed(InsnArg arg) {
if (updates.isEmpty()) {
return false;
}
for (TypeUpdateEntry entry : updates) {
if (entry.getArg() == arg) {
return true;
}
return updateMap.containsKey(arg);
}
public boolean hasUpdateWithType(InsnArg arg, ArgType type) {
TypeUpdateEntry updateEntry = updateMap.get(arg);
if (updateEntry != null) {
return updateEntry.getType().equals(type);
}
return false;
}
public ArgType getType(InsnArg arg) {
for (TypeUpdateEntry update : updates) {
if (update.getArg() == arg) {
return update.getType();
}
TypeUpdateEntry updateEntry = updateMap.get(arg);
if (updateEntry != null) {
return updateEntry.getType();
}
return arg.getType();
}
public void rollbackUpdate(InsnArg arg) {
updates.removeIf(updateEntry -> updateEntry.getArg() == arg);
}
public void checkUpdatesCount() {
if (updates.size() > updatesLimitCount) {
throw new JadxOverflowException("Type inference error: update tree size limit reached");
}
}
public MethodNode getMth() {
return mth;
}
public List<TypeUpdateEntry> getUpdates() {
return updates;
public boolean isEmpty() {
return updateMap.isEmpty();
}
public List<TypeUpdateEntry> getSortedUpdates() {
return updateMap.values().stream().sorted().collect(Collectors.toList());
}
public TypeUpdateFlags getFlags() {
@@ -76,6 +87,6 @@ public class TypeUpdateInfo {
@Override
public String toString() {
return "TypeUpdateInfo{" + flags + ", updates=" + updates + '}';
return "TypeUpdateInfo{" + flags + ' ' + getSortedUpdates() + '}';
}
}
@@ -7,14 +7,13 @@ import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import org.jetbrains.annotations.Nullable;
@@ -37,6 +36,7 @@ import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.regions.conditions.IfCondition;
import jadx.core.dex.trycatch.CatchAttr;
import jadx.core.dex.trycatch.ExceptionHandler;
import jadx.core.utils.blocks.BlockSet;
import jadx.core.utils.exceptions.JadxRuntimeException;
public class BlockUtils {
@@ -483,33 +483,37 @@ public class BlockUtils {
public static List<BlockNode> collectAllSuccessors(MethodNode mth, BlockNode startBlock, boolean clean) {
List<BlockNode> list = new ArrayList<>(mth.getBasicBlocks().size());
dfsVisit(mth, startBlock, clean, list::add);
Function<BlockNode, List<BlockNode>> nextFunc = clean ? BlockNode::getCleanSuccessors : BlockNode::getSuccessors;
visitDFS(mth, startBlock, nextFunc, list::add);
return list;
}
public static void dfsVisit(MethodNode mth, Consumer<BlockNode> visitor) {
dfsVisit(mth, mth.getEnterBlock(), false, visitor);
public static void visitDFS(MethodNode mth, Consumer<BlockNode> visitor) {
visitDFS(mth, mth.getEnterBlock(), BlockNode::getSuccessors, visitor);
}
private static void dfsVisit(MethodNode mth, BlockNode startBlock, boolean clean, Consumer<BlockNode> visitor) {
BitSet visited = newBlocksBitSet(mth);
public static void visitReverseDFS(MethodNode mth, Consumer<BlockNode> visitor) {
visitDFS(mth, mth.getExitBlock(), BlockNode::getPredecessors, visitor);
}
private static void visitDFS(MethodNode mth, BlockNode startBlock,
Function<BlockNode, List<BlockNode>> nextFunc, Consumer<BlockNode> visitor) {
BlockSet visited = new BlockSet(mth);
Deque<BlockNode> queue = new ArrayDeque<>();
queue.addLast(startBlock);
visited.set(startBlock.getId());
visited.set(startBlock);
while (true) {
BlockNode current = queue.pollLast();
if (current == null) {
return;
}
visitor.accept(current);
List<BlockNode> successors = clean ? current.getCleanSuccessors() : current.getSuccessors();
int count = successors.size();
List<BlockNode> nextBlocks = nextFunc.apply(current);
int count = nextBlocks.size();
for (int i = count - 1; i >= 0; i--) { // to preserve order in queue
BlockNode next = successors.get(i);
int nextId = next.getId();
if (!visited.get(nextId)) {
BlockNode next = nextBlocks.get(i);
if (!visited.checkAndSet(next)) {
queue.addLast(next);
visited.set(nextId);
}
}
}
@@ -1156,104 +1160,6 @@ public class BlockUtils {
return false;
}
public static Map<BlockNode, BitSet> calcPostDominance(MethodNode mth) {
return calcPartialPostDominance(mth, mth.getBasicBlocks(), mth.getPreExitBlocks().get(0));
}
public static Map<BlockNode, BitSet> calcPartialPostDominance(MethodNode mth, Collection<BlockNode> blockNodes, BlockNode exitBlock) {
int blocksCount = mth.getBasicBlocks().size();
Map<BlockNode, BitSet> map = new HashMap<>(blocksCount);
BitSet initSet = new BitSet(blocksCount);
for (BlockNode block : blockNodes) {
initSet.set(block.getId());
}
for (BlockNode block : blockNodes) {
BitSet postDoms = new BitSet(blocksCount);
postDoms.or(initSet);
map.put(block, postDoms);
}
BitSet exitBitSet = map.get(exitBlock);
exitBitSet.clear();
exitBitSet.set(exitBlock.getId());
BitSet domSet = new BitSet(blocksCount);
boolean changed;
do {
changed = false;
for (BlockNode block : blockNodes) {
if (block == exitBlock) {
continue;
}
BitSet d = map.get(block);
if (!changed) {
domSet.clear();
domSet.or(d);
}
for (BlockNode scc : block.getSuccessors()) {
BitSet scPDoms = map.get(scc);
if (scPDoms != null) {
d.and(scPDoms);
}
}
d.set(block.getId());
if (!changed && !d.equals(domSet)) {
changed = true;
map.put(block, d);
}
}
} while (changed);
blockNodes.forEach(block -> {
BitSet postDoms = map.get(block);
postDoms.clear(block.getId());
if (postDoms.isEmpty()) {
map.put(block, EmptyBitSet.EMPTY);
}
});
return map;
}
@Nullable
public static BlockNode calcImmediatePostDominator(MethodNode mth, BlockNode block) {
BlockNode oneSuccessor = Utils.getOne(block.getSuccessors());
if (oneSuccessor != null) {
return oneSuccessor;
}
return calcImmediatePostDominator(mth, block, calcPostDominance(mth));
}
@Nullable
public static BlockNode calcPartialImmediatePostDominator(MethodNode mth, BlockNode block,
Collection<BlockNode> blockNodes, BlockNode exitBlock) {
BlockNode oneSuccessor = Utils.getOne(block.getSuccessors());
if (oneSuccessor != null) {
return oneSuccessor;
}
Map<BlockNode, BitSet> pDomsMap = calcPartialPostDominance(mth, blockNodes, exitBlock);
return calcImmediatePostDominator(mth, block, pDomsMap);
}
@Nullable
public static BlockNode calcImmediatePostDominator(MethodNode mth, BlockNode block, Map<BlockNode, BitSet> postDomsMap) {
BlockNode oneSuccessor = Utils.getOne(block.getSuccessors());
if (oneSuccessor != null) {
return oneSuccessor;
}
List<BlockNode> basicBlocks = mth.getBasicBlocks();
BitSet postDoms = postDomsMap.get(block);
BitSet bs = copyBlocksBitSet(mth, postDoms);
for (int i = bs.nextSetBit(0); i >= 0; i = bs.nextSetBit(i + 1)) {
BlockNode pdomBlock = basicBlocks.get(i);
BitSet pdoms = postDomsMap.get(pdomBlock);
if (pdoms != null) {
bs.andNot(pdoms);
}
}
return bitSetToOneBlock(mth, bs);
}
public static BlockNode getTopSplitterForHandler(BlockNode handlerBlock) {
BlockNode block = getBlockWithFlag(handlerBlock.getPredecessors(), AFlag.EXC_TOP_SPLITTER);
if (block == null) {

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