Compare commits

..

61 Commits

Author SHA1 Message Date
Skylot 6844a46c93 fix: disable HTML rendering in labels if not needed 2022-10-20 15:58:23 +01:00
Skylot e9e45707da chore: update dependencies 2022-10-20 14:54:31 +01:00
Skylot b9d02ff4c4 refactor: remove all LinkedList usage 2022-10-12 17:05:08 +01:00
Jan S 29b64300bc fix(gui): multi-threading issue in DebugController fixed (#1701) (PR #1702) 2022-10-11 19:21:06 +01:00
zhongqingsong 777355e86e fix(gui): update Messages_zh_CN.properties (PR #1700)
Add new text about logcat.
2022-10-10 18:57:58 +01:00
Skylot 620a177ce8 fix: restore enum class with custom code in static init (#1699) 2022-10-08 21:54:06 +01:00
Skylot 683c2dfbeb fix: improve ternary inline, resolve more enum cases (#1686) 2022-10-07 15:51:11 +01:00
Skylot 266cbcc6f4 fix(gui): migrate to fixed jdwp library fork (#1471) 2022-10-06 19:47:15 +01:00
Jan S 8a45602ae6 fix: improve logging messages for zip security errors (#750)(PR #1698)
Logging error messages on invalid file-names or path traversal attacks improved
2022-10-06 19:31:42 +01:00
Skylot 711419a797 fix: correct fix for all use places of incompatible primitives (#1688) 2022-10-03 00:11:04 +03:00
Skylot 603f3057eb chore: update dependencies 2022-10-03 00:11:01 +03:00
5idereal fa6fc1f871 fix(gui): update zh-TW translations (PR #1694) 2022-10-02 15:50:11 +01:00
Skylot 49fa320989 fix: handle possible concurrent exception in method codegen (#1685) 2022-09-29 20:28:01 +01:00
Skylot 2f301bf150 fix: don't mark constructor for inline if anonymous class inline is disabled (#1680) 2022-09-25 17:47:53 +01:00
Skylot b4892ce17f build: use fixed java version to build win artifacts 2022-09-23 21:08:16 +01:00
Skylot 151c171616 fix: handle empty block at end of else-if chain (#1674) 2022-09-23 20:40:56 +01:00
Skylot 79477a2de3 fix: don't rename bridged overridden methods (#1672) 2022-09-23 19:16:34 +01:00
dependabot[bot] 78aadda931 build(deps): bump actions/checkout from 2 to 3 (PR #1669)
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-12 18:30:35 +01:00
Jan S b50706505f fix(res): implemented parsing RES_TABLE_TYPE_LIBRARY chunks (#1663)(PR #1664)
* core: Implemented parsing RES_TABLE_TYPE_LIBRARY chunks

* skip unknown data at the end of type chunk
2022-09-10 16:58:26 +01:00
The Cobra Chicken 9114821fb1 feat(debugger): add logcat output (#1411)(PR #1666)
* Adding logcatController class and writing adb / debugger panel information to the controller.

* Finished parsing logcat binary output and writing an arraylist containing all events.

* added highlighting of logcat output based on type.  Added timestamp parsing.

* Updated code to only get new log messages.

* Added additional code for select all

* Completed Check and uncheckall options.

* Changed log highlighting to log color.  Changed from JTextArea to JTextPane. Logcat pane will now autoscroll only if it is already scrolled to the bottom.  Debugger exit will now stop logcat as well.

* Moved labels into NLS rather than using hardcoded strings.

* Implemented the ability to autoselect attached process.  Changed the formatting of logcat messages.

* Moved labels into NLS rather than using hardcoded strings.

* updating to use info getter methods rather than directly accessing variable

* Added Logcat Pause Button

* Added Clear button

* Updated clear icon

* Cleaning warnings

* cleaning

* Changed behavior to only show logcat for debugged process to start with.

* cleaning

* cleaning

* cleaning

* applying spotless

* Fixing bug with switch

* fixed formatting issue

* add missing localization strings

Co-authored-by: green9317 <38409554+green9317@users.noreply.github.com>
Co-authored-by: TheCobraChicken <jeffmlitfi@gmail.com>
Co-authored-by: Skylot <skylot@gmail.com>
2022-09-08 15:18:55 +01:00
Skylot 1195582da8 feat(gui): option for search results count per page (#1652) 2022-09-05 20:07:02 +01:00
Skylot 258987b0ff chore: update dependencies 2022-09-05 20:07:01 +01:00
Guilherme a6a734c70d fix(gui): update pt-BR translation (PR #1655) 2022-09-01 15:51:12 +01:00
Choiman1559 d6c23a2a9b fix(gui): update Korean translation (PR #1650)
* update korean
* Update Messages_ko_KR.properties
* Restore missing empty line

Co-authored-by: skylot <118523+skylot@users.noreply.github.com>
2022-08-20 17:59:37 +01:00
Skylot db028904d7 fix(gui): set legacy sort flag also for launch4j (#1628) 2022-08-20 17:37:26 +01:00
Skylot 63a571306c refactor: load resource table nodes in one change (#1648) 2022-08-19 22:15:04 +01:00
Skylot bc4db61e25 fix(gui): improve resources search (#1648) 2022-08-19 15:52:14 +01:00
Jan S c7e6e28830 fix(gui): improve log viewer dialog (#1311)(PR #1649)
* [gui]: improve log viewer dialog

* use method from UiUtils to set window icons

Co-authored-by: Skylot <skylot@gmail.com>
2022-08-18 19:55:41 +01:00
Skylot 1d7b6fdb2c fix(gui): additional checks on open search result (#1647) 2022-08-18 15:59:45 +01:00
Skylot ce5d8eeff8 fix: don't inline anonymous in self inner class (#1645) 2022-08-18 15:48:17 +01:00
Jan S 894e0e6132 fix: UnsupportedOperationException on adding a field (#1645)(PR #1646)
* fix: UnsupportedOperationException on adding a field

* changed list check and creation similar to safeAdd
2022-08-18 15:33:18 +01:00
Skylot 127f0ecf3f fix(gui): disable actions if files not loaded (#1644) 2022-08-16 21:28:57 +01:00
Skylot cf7767e702 fix(gui): handle null value in TableCellRenderer (#1642) 2022-08-16 20:48:23 +01:00
Skylot e0aedc7949 fix: improve top block search for try/catch (#1633) 2022-08-15 21:31:26 +01:00
Skylot bad78de74c perf: improve directory delete 2022-08-14 13:38:12 +01:00
Skylot 12df8a169f chore: update gradle and dependencies 2022-08-13 18:25:08 +01:00
Skylot 15c9d33339 fix(gui): handle possible classes overlap in disk cache (#1633) 2022-08-13 13:13:13 +00:00
Jiaxin Peng 7e0fafbaf1 fix(gui): fix broken FileDialog by using legacy sort (#1628)(PR #1630)
#1628 #1606 #1213 #1574 #1552
2022-08-11 13:59:46 +01:00
zhongqingsong 57b9c1dd7a fix(gui): update Messages_zh_CN.properties (PR #1627) 2022-08-11 13:29:15 +01:00
Skylot 8ba0c17259 fix: handle empty endless loop (#1611) 2022-08-10 22:07:52 +01:00
Areizen cd32151083 fix(gui): correct Frida snippet for constructor (PR #1605)
When hooking a constructor with Frida, call `$new` instead of `$init`. `$init` cannot be used to instantiate an object and is reserved for hooking.

Co-authored-by: Your Name <you@example.com>
2022-08-06 20:16:37 +01:00
Skylot 75b52d672e feat(gui): save project search history 2022-08-05 20:43:05 +01:00
Skylot 11d04508f7 feat(gui): add manual search, stop and sort actions to search dialog (#1600) 2022-08-05 20:09:33 +01:00
Skylot e641b773b5 fix(gui): improve search dialog performance 2022-08-05 14:53:48 +01:00
Skylot 6e5899c654 fix: checks for field init reorder (#1599) 2022-08-04 17:38:46 +01:00
Skylot c66ffaa7f9 feat(gui): show start page on jadx open 2022-08-03 16:44:55 +01:00
Skylot 5193c6a5d8 chore: add tool for automatically insert new i18n lines 2022-08-03 16:44:40 +01:00
Skylot e7212af547 chore: upgrade gradle wrapper to 7.5 2022-08-03 18:44:10 +03:00
FixTheBug 3ca1357af4 fix(gui): sort resources by deobfuscated name (#1595)(PR #1598)
Co-authored-by: /paul-nguyen-goldenowl <paul.nguyen.goldenowl@gmail.com>
2022-08-01 14:54:22 +01:00
Guilherme 90e95213e4 feat(gui): add Brazilian Portuguese translation (PR #1596)
* feat(translation): add pt-br
* fix: `adb_dialog` prefix deleted
2022-08-01 12:15:35 +01:00
Jan S ae2d4da585 fix(res): XML "@null" decoding (#1583)(PR #1594)
minor improvements
2022-07-31 13:50:32 +01:00
Skylot 691d5cd1e6 fix: hide unused label before exception handler in simple mode 2022-07-30 17:33:23 +01:00
Skylot 58a46c6417 fix(gui): add constructors usage into class usage (#1591) 2022-07-30 17:22:32 +01:00
Skylot d3f6160e62 feat: add option to disable finally block extraction (#1592) 2022-07-30 14:07:43 +01:00
Skylot 03e4afb12f fix: check variables before merge in finally block (#1592) 2022-07-30 13:48:53 +01:00
Hen_Ry 6802f6028e fix(gui): update german translation (PR #1554)
* german update
* Fix
* applied the latest changes as discussed

Co-authored-by: Jan Peter Stotz <jpstotz@users.noreply.github.com>
2022-07-27 17:45:36 +01:00
SiderealArt 5ca61cfe18 fix(gui): update zh-tw translation (PR #1589) 2022-07-27 11:16:11 +01:00
Julian Burner 32d55b48f2 fix(gui): replace mixed-up quotation marks with period (PR #1588) 2022-07-26 10:16:34 +01:00
Skylot ab4b6f9e54 feat: select better resource name (#1581) 2022-07-25 19:53:03 +01:00
Jan S 9100ad1220 fix(debugger): resolve NPE in adb device viewer (#1585) (PR #1586) 2022-07-25 17:44:55 +01:00
Skylot 8b4f8fb572 fix: resolve inherited method to use correct alias (#1582) 2022-07-24 19:15:52 +03:00
157 changed files with 5372 additions and 1349 deletions
+3 -1
View File
@@ -57,7 +57,9 @@ jobs:
fetch-depth: 0
- name: Set up JDK
uses: oracle-actions/setup-java@v1 # set latest java version by default
uses: oracle-actions/setup-java@v1
with:
release: 18
- name: Print Java version
shell: bash
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
@@ -6,5 +6,5 @@ jobs:
name: "Validation"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1
+5
View File
@@ -102,6 +102,7 @@ options:
--add-debug-lines - add comments with debug line numbers if available
--no-inline-anonymous - disable anonymous classes inline
--no-inline-methods - disable methods inline
--no-finally - don't extract finally block
--no-replace-consts - don't replace constant value with matching constant field
--escape-unicode - escape non latin characters in strings (with \u)
--respect-bytecode-access-modifiers - don't change original access modifiers
@@ -116,6 +117,10 @@ options:
'ignore' - don't read and don't save
--deobf-use-sourcename - use source file name as class name alias
--deobf-parse-kotlin-metadata - parse kotlin metadata to class and package names
--deobf-res-name-source - better name source for resources:
'auto' - automatically select best name (default)
'resources' - use resources names
'code' - use R class fields names
--use-kotlin-methods-for-var-names - use kotlin intrinsic methods to rename variables, values: disable, apply, apply-and-hide, default: apply
--rename-flags - fix options (comma-separated list of):
'case' - fix case sensitivity issues (according to --fs-case-sensitive option),
+7 -7
View File
@@ -1,6 +1,6 @@
plugins {
id 'com.github.ben-manes.versions' version '0.42.0'
id 'com.diffplug.spotless' version '6.8.0'
id 'com.github.ben-manes.versions' version '0.43.0'
id 'com.diffplug.spotless' version '6.11.0'
}
ext.jadxVersion = System.getenv('JADX_VERSION') ?: "dev"
@@ -26,16 +26,16 @@ allprojects {
}
dependencies {
implementation 'org.slf4j:slf4j-api:1.7.36'
implementation 'org.slf4j:slf4j-api:2.0.3'
compileOnly 'org.jetbrains:annotations:23.0.0'
testImplementation 'ch.qos.logback:logback-classic:1.2.11'
testImplementation 'ch.qos.logback:logback-classic:1.3.4'
testImplementation 'org.hamcrest:hamcrest-library:2.2'
testImplementation 'org.mockito:mockito-core:4.6.1'
testImplementation 'org.mockito:mockito-core:4.8.0'
testImplementation 'org.assertj:assertj-core:3.23.1'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.1'
testCompileOnly 'org.jetbrains:annotations:23.0.0'
}
Binary file not shown.
+2 -2
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=29e49b10984e585d8118b7d0bc452f944e386458df27371b49b4ac1dec4b7fda
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
distributionSha256Sum=f6b8596b10cce501591e92f229816aa4046424f3b24d771751b06779d58c8ec4
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+6
View File
@@ -205,6 +205,12 @@ set -- \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
echo "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
Vendored
+8 -6
View File
@@ -14,7 +14,7 @@
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -25,7 +25,7 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
if "%DIRNAME%"=="" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -75,13 +75,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
+1 -1
View File
@@ -11,7 +11,7 @@ dependencies {
runtimeOnly(project(':jadx-plugins:jadx-smali-input'))
implementation 'com.beust:jcommander:1.82'
implementation 'ch.qos.logback:logback-classic:1.2.11'
implementation 'ch.qos.logback:logback-classic:1.3.4'
}
application {
@@ -21,6 +21,7 @@ import jadx.api.JadxArgs.RenameEnum;
import jadx.api.JadxArgs.UseKotlinMethodsForVarNames;
import jadx.api.JadxDecompiler;
import jadx.api.args.DeobfuscationMapFileMode;
import jadx.api.args.ResourceNameSource;
import jadx.core.utils.exceptions.JadxException;
import jadx.core.utils.files.FileUtils;
@@ -88,6 +89,9 @@ public class JadxCLIArgs {
@Parameter(names = { "--no-inline-methods" }, description = "disable methods inline")
protected boolean inlineMethods = true;
@Parameter(names = "--no-finally", description = "don't extract finally block")
protected boolean extractFinally = true;
@Parameter(names = "--no-replace-consts", description = "don't replace constant value with matching constant field")
protected boolean replaceConsts = true;
@@ -129,6 +133,16 @@ public class JadxCLIArgs {
@Parameter(names = { "--deobf-parse-kotlin-metadata" }, description = "parse kotlin metadata to class and package names")
protected boolean deobfuscationParseKotlinMetadata = false;
@Parameter(
names = { "--deobf-res-name-source" },
description = "better name source for resources:"
+ "\n 'auto' - automatically select best name (default)"
+ "\n 'resources' - use resources names"
+ "\n 'code' - use R class fields names",
converter = ResourceNameSourceConverter.class
)
protected ResourceNameSource resourceNameSource = ResourceNameSource.AUTO;
@Parameter(
names = { "--use-kotlin-methods-for-var-names" },
description = "use kotlin intrinsic methods to rename variables, values: disable, apply, apply-and-hide",
@@ -262,6 +276,7 @@ public class JadxCLIArgs {
args.setUseSourceNameAsClassAlias(deobfuscationUseSourceNameAsAlias);
args.setParseKotlinMetadata(deobfuscationParseKotlinMetadata);
args.setUseKotlinMethodsForVarNames(useKotlinMethodsForVarNames);
args.setResourceNameSource(resourceNameSource);
args.setEscapeUnicode(escapeUnicode);
args.setRespectBytecodeAccModifiers(respectBytecodeAccessModifiers);
args.setExportAsGradleProject(exportAsGradleProject);
@@ -270,6 +285,7 @@ public class JadxCLIArgs {
args.setInsertDebugLines(addDebugLines);
args.setInlineAnonymousClasses(inlineAnonymousClasses);
args.setInlineMethods(inlineMethods);
args.setExtractFinally(extractFinally);
args.setRenameFlags(renameFlags);
args.setFsCaseSensitive(fsCaseSensitive);
args.setCommentsLevel(commentsLevel);
@@ -350,6 +366,10 @@ public class JadxCLIArgs {
return inlineMethods;
}
public boolean isExtractFinally() {
return extractFinally;
}
public boolean isDeobfuscationOn() {
return deobfuscationOn;
}
@@ -378,6 +398,10 @@ public class JadxCLIArgs {
return deobfuscationParseKotlinMetadata;
}
public ResourceNameSource getResourceNameSource() {
return resourceNameSource;
}
public UseKotlinMethodsForVarNames getUseKotlinMethodsForVarNames() {
return useKotlinMethodsForVarNames;
}
@@ -502,6 +526,19 @@ public class JadxCLIArgs {
}
}
public static class ResourceNameSourceConverter implements IStringConverter<ResourceNameSource> {
@Override
public ResourceNameSource convert(String value) {
try {
return ResourceNameSource.valueOf(value.toUpperCase());
} catch (Exception e) {
throw new IllegalArgumentException(
'\'' + value + "' is unknown, possible values are: "
+ JadxCLIArgs.enumValuesString(ResourceNameSource.values()));
}
}
}
public static class DecompilationModeConverter implements IStringConverter<DecompilationMode> {
@Override
public DecompilationMode convert(String value) {
+4 -4
View File
@@ -5,11 +5,11 @@ plugins {
dependencies {
api(project(':jadx-plugins:jadx-plugins-api'))
implementation 'com.google.code.gson:gson:2.9.0'
implementation 'com.google.code.gson:gson:2.9.1'
// TODO: move resources decoding to separate plugin module
implementation 'com.android.tools.build:aapt2-proto:7.2.1-7984345'
implementation 'com.google.protobuf:protobuf-java:3.21.2' // forcing latest version
implementation 'com.android.tools.build:aapt2-proto:7.3.1-8691043'
implementation 'com.google.protobuf:protobuf-java:3.21.8' // forcing latest version
testImplementation 'org.apache.commons:commons-lang3:3.12.0'
@@ -19,7 +19,7 @@ dependencies {
testRuntimeOnly(project(':jadx-plugins:jadx-java-input'))
testRuntimeOnly(project(':jadx-plugins:jadx-raung-input'))
testImplementation 'org.eclipse.jdt:ecj:3.30.0'
testImplementation 'org.eclipse.jdt:ecj:3.31.0'
testImplementation 'tools.profiler:async-profiler:1.8.3'
}
@@ -16,6 +16,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.args.DeobfuscationMapFileMode;
import jadx.api.args.ResourceNameSource;
import jadx.api.data.ICodeData;
import jadx.api.impl.AnnotatedCodeWriter;
import jadx.api.impl.InMemoryCodeCache;
@@ -72,6 +73,7 @@ public class JadxArgs {
private File deobfuscationMapFile = null;
private DeobfuscationMapFileMode deobfuscationMapFileMode = DeobfuscationMapFileMode.READ;
private ResourceNameSource resourceNameSource = ResourceNameSource.AUTO;
private int deobfuscationMinLength = 0;
private int deobfuscationMaxLength = Integer.MAX_VALUE;
@@ -369,6 +371,14 @@ public class JadxArgs {
this.deobfuscationMapFile = deobfuscationMapFile;
}
public ResourceNameSource getResourceNameSource() {
return resourceNameSource;
}
public void setResourceNameSource(ResourceNameSource resourceNameSource) {
this.resourceNameSource = resourceNameSource;
}
public boolean isEscapeUnicode() {
return escapeUnicode;
}
@@ -540,6 +550,7 @@ public class JadxArgs {
String argStr = "args:" + decompilationMode + useImports + showInconsistentCode
+ inlineAnonymousClasses + inlineMethods
+ deobfuscationOn + deobfuscationMinLength + deobfuscationMaxLength
+ resourceNameSource
+ parseKotlinMetadata + useKotlinMethodsForVarNames
+ insertDebugLines + extractFinally
+ debugInfo + useSourceNameAsClassAlias + escapeUnicode + replaceConsts
@@ -564,6 +575,7 @@ public class JadxArgs {
+ ", deobfuscationOn=" + deobfuscationOn
+ ", deobfuscationMapFile=" + deobfuscationMapFile
+ ", deobfuscationMapFileMode=" + deobfuscationMapFileMode
+ ", resourceNameSource=" + resourceNameSource
+ ", useSourceNameAsClassAlias=" + useSourceNameAsClassAlias
+ ", parseKotlinMetadata=" + parseKotlinMetadata
+ ", useKotlinMethodsForVarNames=" + useKotlinMethodsForVarNames
@@ -0,0 +1,22 @@
package jadx.api.args;
/**
* Resources original name source (for deobfuscation)
*/
public enum ResourceNameSource {
/**
* Automatically select best name (default)
*/
AUTO,
/**
* Force use resources provided names
*/
RESOURCES,
/**
* Force use resources names from R class
*/
CODE,
}
@@ -191,7 +191,6 @@ public class Jadx {
passes.add(new ProcessInstructionsVisitor());
passes.add(new BlockSplitter());
passes.add(new MethodVisitor(mth -> mth.add(AFlag.DISABLE_BLOCKS_LOCK)));
if (args.isRawCFGOutput()) {
passes.add(DotGraphVisitor.dumpRaw());
}
@@ -215,9 +214,6 @@ public class Jadx {
passes.add(new CodeShrinkVisitor());
passes.add(new SimplifyVisitor());
passes.add(new MethodVisitor(mth -> mth.remove(AFlag.DONT_GENERATE)));
if (args.isRawCFGOutput()) {
passes.add(DotGraphVisitor.dumpRaw());
}
if (args.isCfgOutput()) {
passes.add(DotGraphVisitor.dump());
}
@@ -171,7 +171,7 @@ public class ClassGen {
ArgType sup = cls.getSuperClass();
if (sup != null
&& !sup.equals(ArgType.OBJECT)
&& !cls.isEnum()) {
&& !cls.contains(AFlag.REMOVE_SUPER_CLASS)) {
clsCode.add("extends ");
useClass(clsCode, sup);
clsCode.add(' ');
@@ -322,19 +322,25 @@ public class ClassGen {
if (inlineAttr == null || inlineAttr.notNeeded()) {
return false;
}
if (mth.getUseIn().isEmpty()) {
mth.add(AFlag.DONT_GENERATE);
return true;
try {
if (mth.getUseIn().isEmpty()) {
mth.add(AFlag.DONT_GENERATE);
return true;
}
List<MethodNode> useInCompleted = mth.getUseIn().stream()
.filter(m -> m.getTopParentClass().getState().isProcessComplete())
.collect(Collectors.toList());
if (useInCompleted.isEmpty()) {
mth.add(AFlag.DONT_GENERATE);
return true;
}
mth.addDebugComment("Method not inlined, still used in: " + useInCompleted);
return false;
} catch (Exception e) {
// check failed => keep method
mth.addWarnComment("Failed to check method usage", e);
return false;
}
List<MethodNode> useInCompleted = mth.getUseIn().stream()
.filter(m -> m.getTopParentClass().getState().isProcessComplete())
.collect(Collectors.toList());
if (useInCompleted.isEmpty()) {
mth.add(AFlag.DONT_GENERATE);
return true;
}
mth.addDebugComment("Method not inlined, still used in: " + useInCompleted);
return false;
}
private boolean isMethodsPresents() {
@@ -1,7 +1,7 @@
package jadx.core.codegen;
import java.util.ArrayDeque;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Queue;
import jadx.api.ICodeWriter;
@@ -23,7 +23,7 @@ import jadx.core.utils.exceptions.JadxRuntimeException;
public class ConditionGen extends InsnGen {
private static class CondStack {
private final Queue<IfCondition> stack = new LinkedList<>();
private final Queue<IfCondition> stack = new ArrayDeque<>();
public Queue<IfCondition> getStack() {
return stack;
@@ -828,7 +828,11 @@ public class InsnGen {
if (insn.contains(AFlag.FORCE_RAW_NAME)) {
code.add(callMth.getName());
} else {
code.add(callMth.getAlias());
if (callMthNode != null) {
code.add(callMthNode.getAlias());
} else {
code.add(callMth.getAlias());
}
}
generateMethodArguments(code, insn, k, callMthNode);
}
@@ -53,7 +53,9 @@ public class SimpleModeHelper {
startLabel.set(block.getId());
} else if (predsCount == 1 && prev != null) {
if (!prev.equals(preds.get(0))) {
startLabel.set(block.getId());
if (!block.contains(AFlag.EXC_BOTTOM_SPLITTER)) {
startLabel.set(block.getId());
}
if (prev.getSuccessors().size() == 1 && !mth.isPreExitBlocks(prev)) {
endGoto.set(prev.getId());
}
@@ -428,7 +428,9 @@ public class Deobfuscator {
return "Enum";
}
String result = "";
if (cls.getAccessFlags().isAbstract()) {
if (cls.getAccessFlags().isInterface()) {
result += "Interface";
} else if (cls.getAccessFlags().isAbstract()) {
result += "Abstract";
}
@@ -21,6 +21,7 @@ public enum AFlag {
DONT_GENERATE, // process as usual, but don't output to generated code
COMMENT_OUT, // process as usual, but comment insn in generated code
REMOVE, // can be completely removed
REMOVE_SUPER_CLASS, // don't add super class
HIDDEN, // instruction used inside other instruction but not listed in args
@@ -91,6 +91,19 @@ public class LoopInfo {
this.parentLoop = parentLoop;
}
public boolean hasParent(LoopInfo searchLoop) {
LoopInfo parent = parentLoop;
while (true) {
if (parent == null) {
return false;
}
if (parent == searchLoop) {
return true;
}
parent = parent.getParentLoop();
}
}
@Override
public String toString() {
return "LOOP:" + id + ": " + start + "->" + end;
@@ -1,6 +1,6 @@
package jadx.core.dex.attributes.nodes;
import java.util.LinkedList;
import java.util.ArrayList;
import java.util.List;
import jadx.api.ICodeWriter;
@@ -10,7 +10,7 @@ import jadx.core.dex.instructions.PhiInsn;
public class PhiListAttr implements IJadxAttribute {
private final List<PhiInsn> list = new LinkedList<>();
private final List<PhiInsn> list = new ArrayList<>();
@Override
public AType<PhiListAttr> getAttrType() {
@@ -19,29 +19,59 @@ import static jadx.core.utils.Utils.lockList;
public final class BlockNode extends AttrNode implements IBlock, Comparable<BlockNode> {
/**
* Const ID
*/
private final int cid;
/**
* ID linked to position in blocks list (easier to use BitSet)
* TODO: rename to avoid confusion
*/
private int id;
/**
* Offset in methods bytecode
*/
private final int startOffset;
private final List<InsnNode> instructions = new ArrayList<>(2);
private List<BlockNode> predecessors = new ArrayList<>(1);
private List<BlockNode> successors = new ArrayList<>(1);
private List<BlockNode> cleanSuccessors;
// all dominators
/**
* All dominators, excluding self
*/
private BitSet doms = EmptyBitSet.EMPTY;
// dominance frontier
/**
* Dominance frontier
*/
private BitSet domFrontier;
// immediate dominator
/**
* Immediate dominator
*/
private BlockNode idom;
// blocks on which dominates this block
/**
* Blocks on which dominates this block
*/
private List<BlockNode> dominatesOn = new ArrayList<>(3);
public BlockNode(int id, int offset) {
public BlockNode(int cid, int id, int offset) {
this.cid = cid;
this.id = id;
this.startOffset = offset;
}
public void setId(int id) {
public int getCId() {
return cid;
}
void setId(int id) {
this.id = id;
}
@@ -188,12 +218,12 @@ public final class BlockNode extends AttrNode implements IBlock, Comparable<Bloc
return false;
}
BlockNode other = (BlockNode) obj;
return id == other.id && startOffset == other.startOffset;
return cid == other.cid && startOffset == other.startOffset;
}
@Override
public int compareTo(@NotNull BlockNode o) {
return Integer.compare(id, o.id);
return Integer.compare(cid, o.cid);
}
@Override
@@ -203,6 +233,6 @@ public final class BlockNode extends AttrNode implements IBlock, Comparable<Bloc
@Override
public String toString() {
return "B:" + id + ':' + InsnUtils.formatOffset(startOffset);
return "B:" + cid + ':' + InsnUtils.formatOffset(startOffset);
}
}
@@ -469,6 +469,9 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
}
public void addField(FieldNode fld) {
if (fields == null || fields.isEmpty()) {
fields = new ArrayList<>(1);
}
fields.add(fld);
}
@@ -657,6 +660,10 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
return contains(AType.ANONYMOUS_CLASS);
}
public boolean isSynthetic() {
return contains(AFlag.SYNTHETIC);
}
public boolean isInner() {
return parentClass != this;
}
@@ -798,7 +805,9 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
}
public void addCodegenDep(ClassNode dep) {
this.codegenDeps = ListUtils.safeAdd(this.codegenDeps, dep);
if (!codegenDeps.contains(dep)) {
this.codegenDeps = ListUtils.safeAdd(this.codegenDeps, dep);
}
}
public int getTotalDepsCount() {
@@ -57,6 +57,10 @@ public class FieldNode extends NotificationAttrNode implements ICodeNode {
return accFlags.isStatic();
}
public boolean isInstance() {
return !accFlags.isStatic();
}
public String getName() {
return fieldInfo.getName();
}
@@ -65,6 +69,10 @@ public class FieldNode extends NotificationAttrNode implements ICodeNode {
return fieldInfo.getAlias();
}
public void rename(String alias) {
fieldInfo.setAlias(alias);
}
public ArgType getType() {
return type;
}
@@ -73,6 +81,10 @@ public class FieldNode extends NotificationAttrNode implements ICodeNode {
return parentClass;
}
public ClassNode getTopParentClass() {
return parentClass.getTopParentClass();
}
public List<MethodNode> getUseIn() {
return useIn;
}
@@ -264,21 +264,6 @@ public class InsnNode extends LineAttrNode {
}
}
public boolean canReorderRecursive() {
if (!canReorder()) {
return false;
}
for (InsnArg arg : this.getArguments()) {
if (arg.isInsnWrap()) {
InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn();
if (!wrapInsn.canReorderRecursive()) {
return false;
}
}
}
return true;
}
public boolean containsWrappedInsn() {
for (InsnArg arg : this.getArguments()) {
if (arg.isInsnWrap()) {
@@ -61,6 +61,7 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
private List<RegisterArg> argsList;
private InsnNode[] instructions;
private List<BlockNode> blocks;
private int blocksMaxCId;
private BlockNode enterBlock;
private BlockNode exitBlock;
private List<SSAVar> sVars;
@@ -318,6 +319,15 @@ 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++;
}
}
public int getNextBlockCId() {
return blocksMaxCId++;
}
public BlockNode getEnterBlock() {
@@ -156,7 +156,7 @@ public final class IfCondition extends AttrNode {
return i;
}
if (c.getOp() == IfOp.EQ && c.getB().isFalse()) {
cond = not(new IfCondition(c.invert()));
cond = new IfCondition(Mode.NOT, Collections.singletonList(new IfCondition(c.invert())));
} else {
c.normalize();
}
@@ -49,6 +49,10 @@ public final class LoopRegion extends ConditionRegion {
return header;
}
public boolean isEndless() {
return header == null;
}
public IRegion getBody() {
return body;
}
@@ -60,6 +60,10 @@ public class TryCatchBlockAttr implements IJadxAttribute {
return throwFound;
}
public int getId() {
return id;
}
public List<ExceptionHandler> getHandlers() {
return handlers;
}
@@ -62,45 +62,47 @@ public class ConstInlineVisitor extends AbstractVisitor {
|| insn.getResult() == null) {
return;
}
SSAVar sVar = insn.getResult().getSVar();
InsnArg constArg;
Runnable onSuccess = null;
InsnType insnType = insn.getType();
if (insnType == InsnType.CONST || insnType == InsnType.MOVE) {
constArg = insn.getArg(0);
if (!constArg.isLiteral()) {
switch (insn.getType()) {
case CONST:
case MOVE: {
constArg = insn.getArg(0);
if (!constArg.isLiteral()) {
return;
}
long lit = ((LiteralArg) constArg).getLiteral();
if (lit == 0 && forbidNullInlines(sVar)) {
// all usages forbids inlining
return;
}
break;
}
case CONST_STR: {
String s = ((ConstStringNode) insn).getString();
FieldNode f = mth.getParentClass().getConstField(s);
if (f == null) {
InsnNode copy = insn.copyWithoutResult();
constArg = InsnArg.wrapArg(copy);
} else {
InsnNode constGet = new IndexInsnNode(InsnType.SGET, f.getFieldInfo(), 0);
constArg = InsnArg.wrapArg(constGet);
constArg.setType(ArgType.STRING);
onSuccess = () -> f.addUseIn(mth);
}
break;
}
case CONST_CLASS: {
if (sVar.isUsedInPhi()) {
return;
}
constArg = InsnArg.wrapArg(insn.copyWithoutResult());
constArg.setType(ArgType.CLASS);
break;
}
default:
return;
}
long lit = ((LiteralArg) constArg).getLiteral();
if (lit == 0 && forbidNullInlines(sVar)) {
// all usages forbids inlining
return;
}
} else if (insnType == InsnType.CONST_STR) {
if (sVar.isUsedInPhi()) {
return;
}
String s = ((ConstStringNode) insn).getString();
FieldNode f = mth.getParentClass().getConstField(s);
if (f == null) {
InsnNode copy = insn.copyWithoutResult();
constArg = InsnArg.wrapArg(copy);
} else {
InsnNode constGet = new IndexInsnNode(InsnType.SGET, f.getFieldInfo(), 0);
constArg = InsnArg.wrapArg(constGet);
constArg.setType(ArgType.STRING);
onSuccess = () -> f.addUseIn(mth);
}
} else if (insnType == InsnType.CONST_CLASS) {
if (sVar.isUsedInPhi()) {
return;
}
constArg = InsnArg.wrapArg(insn.copyWithoutResult());
constArg.setType(ArgType.CLASS);
} else {
return;
}
// all check passed, run replace
@@ -100,7 +100,7 @@ public class DotGraphVisitor extends AbstractVisitor {
if (insnArr == null) {
return;
}
BlockNode block = new BlockNode(0, 0);
BlockNode block = new BlockNode(0, 0, 0);
List<InsnNode> insnList = block.getInstructions();
for (InsnNode insn : insnArr) {
if (insn != null) {
@@ -199,7 +199,7 @@ public class DotGraphVisitor extends AbstractVisitor {
dot.add("color=red,");
}
dot.add("label=\"{");
dot.add(String.valueOf(block.getId())).add("\\:\\ ");
dot.add(String.valueOf(block.getCId())).add("\\:\\ ");
dot.add(InsnUtils.formatOffset(block.getStartOffset()));
if (!attrs.isEmpty()) {
dot.add('|').add(attrs);
@@ -230,10 +230,10 @@ public class DotGraphVisitor extends AbstractVisitor {
if (PRINT_DOMINATORS) {
for (BlockNode c : block.getDominatesOn()) {
conn.startLine(block.getId() + " -> " + c.getId() + "[color=green];");
conn.startLine(block.getCId() + " -> " + c.getCId() + "[color=green];");
}
for (BlockNode dom : BlockUtils.bitSetToBlocks(mth, block.getDomFrontier())) {
conn.startLine("f_" + block.getId() + " -> f_" + dom.getId() + "[color=blue];");
conn.startLine("f_" + block.getCId() + " -> f_" + dom.getCId() + "[color=blue];");
}
}
}
@@ -273,7 +273,7 @@ public class DotGraphVisitor extends AbstractVisitor {
private String makeName(IContainer c) {
String name;
if (c instanceof BlockNode) {
name = "Node_" + ((BlockNode) c).getId();
name = "Node_" + ((BlockNode) c).getCId();
} else if (c instanceof IBlock) {
name = "Node_" + c.getClass().getSimpleName() + '_' + c.hashCode();
} else {
@@ -37,9 +37,13 @@ import jadx.core.dex.instructions.mods.ConstructorInsn;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.IContainer;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.regions.Region;
import jadx.core.dex.visitors.regions.CheckRegions;
import jadx.core.dex.visitors.regions.IfRegionVisitor;
import jadx.core.dex.visitors.shrink.CodeShrinkVisitor;
import jadx.core.utils.BlockInsnPair;
import jadx.core.utils.BlockUtils;
@@ -47,6 +51,7 @@ import jadx.core.utils.InsnRemover;
import jadx.core.utils.InsnUtils;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxException;
import jadx.core.utils.exceptions.JadxRuntimeException;
import static jadx.core.utils.InsnUtils.checkInsnType;
import static jadx.core.utils.InsnUtils.getSingleArg;
@@ -55,8 +60,16 @@ import static jadx.core.utils.InsnUtils.getWrappedInsn;
@JadxVisitor(
name = "EnumVisitor",
desc = "Restore enum classes",
runAfter = { CodeShrinkVisitor.class, ModVisitor.class, ReSugarCode.class },
runBefore = { ExtractFieldInit.class }
runAfter = {
CodeShrinkVisitor.class, // all possible instructions already inlined
ModVisitor.class,
ReSugarCode.class,
IfRegionVisitor.class, // ternary operator inlined
CheckRegions.class // regions processing finished
},
runBefore = {
ExtractFieldInit.class
}
)
public class EnumVisitor extends AbstractVisitor {
@@ -81,82 +94,67 @@ public class EnumVisitor extends AbstractVisitor {
@Override
public boolean visit(ClassNode cls) throws JadxException {
boolean converted;
try {
converted = convertToEnum(cls);
} catch (Exception e) {
cls.addWarnComment("Enum visitor error", e);
converted = false;
}
if (!converted) {
AccessInfo accessFlags = cls.getAccessFlags();
if (accessFlags.isEnum()) {
cls.setAccessFlags(accessFlags.remove(AccessFlags.ENUM));
cls.addWarnComment("Failed to restore enum class, 'enum' modifier removed");
if (cls.isEnum()) {
boolean converted;
try {
converted = convertToEnum(cls);
} catch (Exception e) {
cls.addWarnComment("Enum visitor error", e);
converted = false;
}
if (!converted) {
AccessInfo accessFlags = cls.getAccessFlags();
if (accessFlags.isEnum()) {
cls.setAccessFlags(accessFlags.remove(AccessFlags.ENUM));
cls.addWarnComment("Failed to restore enum class, 'enum' modifier and super class removed");
}
}
}
return true;
}
private boolean convertToEnum(ClassNode cls) {
if (!cls.isEnum()) {
return false;
ArgType superType = cls.getSuperClass();
if (superType != null && superType.getObject().equals(ArgType.ENUM.getObject())) {
cls.add(AFlag.REMOVE_SUPER_CLASS);
}
MethodNode classInitMth = cls.getClassInitMth();
if (classInitMth == null) {
cls.addWarnComment("Enum class init method not found");
return false;
}
if (classInitMth.getBasicBlocks().isEmpty()) {
Region staticRegion = classInitMth.getRegion();
if (staticRegion == null || classInitMth.getBasicBlocks().isEmpty()) {
return false;
}
ArgType clsType = cls.getClassInfo().getType();
// search "$VALUES" field (holds all enum values)
List<FieldNode> valuesCandidates = cls.getFields().stream()
.filter(f -> f.getAccessFlags().isStatic())
.filter(f -> f.getType().isArray())
.filter(f -> Objects.equals(f.getType().getArrayRootElement(), clsType))
.collect(Collectors.toList());
if (valuesCandidates.isEmpty()) {
return false;
}
if (valuesCandidates.size() > 1) {
valuesCandidates.removeIf(f -> !f.getAccessFlags().isSynthetic());
}
if (valuesCandidates.size() > 1) {
Optional<FieldNode> valuesOpt = valuesCandidates.stream().filter(f -> f.getName().equals("$VALUES")).findAny();
if (valuesOpt.isPresent()) {
valuesCandidates.clear();
valuesCandidates.add(valuesOpt.get());
// collect blocks on linear part of static method (ignore branching on method end)
List<BlockNode> staticBlocks = new ArrayList<>();
for (IContainer subBlock : staticRegion.getSubBlocks()) {
if (subBlock instanceof BlockNode) {
staticBlocks.add((BlockNode) subBlock);
} else {
break;
}
}
if (valuesCandidates.size() != 1) {
cls.addWarnComment("Found several \"values\" enum fields: " + valuesCandidates);
if (staticBlocks.isEmpty()) {
cls.addWarnComment("Unexpected branching in enum static init block");
return false;
}
FieldNode valuesField = valuesCandidates.get(0);
List<InsnNode> toRemove = new ArrayList<>();
// search "$VALUES" array init and collect enum fields
BlockInsnPair valuesInitPair = getValuesInitInsn(classInitMth, valuesField);
if (valuesInitPair == null) {
EnumData data = new EnumData(cls, classInitMth, staticBlocks);
if (!searchValuesField(data)) {
return false;
}
BlockNode staticBlock = valuesInitPair.getBlock();
InsnNode valuesInitInsn = valuesInitPair.getInsn();
List<EnumField> enumFields = null;
InsnArg arrArg = valuesInitInsn.getArg(0);
InsnArg arrArg = data.valuesInitInsn.getArg(0);
if (arrArg.isInsnWrap()) {
InsnNode wrappedInsn = ((InsnWrapArg) arrArg).getWrapInsn();
enumFields = extractEnumFieldsFromInsn(cls, staticBlock, wrappedInsn, toRemove);
enumFields = extractEnumFieldsFromInsn(data, wrappedInsn);
}
if (enumFields == null) {
cls.addWarnComment("Unknown enum class pattern. Please report as an issue!");
return false;
}
toRemove.add(valuesInitInsn);
data.toRemove.add(data.valuesInitInsn);
// all checks complete, perform transform
EnumClassAttr attr = new EnumClassAttr(enumFields);
@@ -176,59 +174,92 @@ public class EnumVisitor extends AbstractVisitor {
fieldNode.getFieldInfo().setAlias(name);
}
fieldNode.add(AFlag.DONT_GENERATE);
processConstructorInsn(cls, enumField, classInitMth, staticBlock, toRemove);
processConstructorInsn(data, enumField, classInitMth);
}
valuesField.add(AFlag.DONT_GENERATE);
InsnRemover.removeAllAndUnbind(classInitMth, staticBlock, toRemove);
data.valuesField.add(AFlag.DONT_GENERATE);
InsnRemover.removeAllAndUnbind(classInitMth, data.toRemove);
if (classInitMth.countInsns() == 0) {
classInitMth.add(AFlag.DONT_GENERATE);
} else if (!toRemove.isEmpty()) {
} else if (!data.toRemove.isEmpty()) {
CodeShrinkVisitor.shrinkMethod(classInitMth);
}
removeEnumMethods(cls, clsType, valuesField);
removeEnumMethods(cls, data.valuesField);
return true;
}
private void processConstructorInsn(ClassNode cls, EnumField enumField, MethodNode classInitMth,
BlockNode staticBlock, List<InsnNode> toRemove) {
ConstructorInsn co = enumField.getConstrInsn();
ClassInfo enumClsInfo = co.getClassType();
if (!enumClsInfo.equals(cls.getClassInfo())) {
ClassNode enumCls = cls.root().resolveClass(enumClsInfo);
if (enumCls != null) {
processEnumCls(cls, enumField, enumCls);
/**
* Search "$VALUES" field (holds all enum values)
*/
private boolean searchValuesField(EnumData data) {
ArgType clsType = data.cls.getClassInfo().getType();
List<FieldNode> valuesCandidates = data.cls.getFields().stream()
.filter(f -> f.getAccessFlags().isStatic())
.filter(f -> f.getType().isArray())
.filter(f -> Objects.equals(f.getType().getArrayRootElement(), clsType))
.collect(Collectors.toList());
if (valuesCandidates.isEmpty()) {
data.cls.addWarnComment("$VALUES field not found");
return false;
}
if (valuesCandidates.size() > 1) {
valuesCandidates.removeIf(f -> !f.getAccessFlags().isSynthetic());
}
if (valuesCandidates.size() > 1) {
Optional<FieldNode> valuesOpt = valuesCandidates.stream().filter(f -> f.getName().equals("$VALUES")).findAny();
if (valuesOpt.isPresent()) {
valuesCandidates.clear();
valuesCandidates.add(valuesOpt.get());
}
}
List<RegisterArg> regs = new ArrayList<>();
co.getRegisterArgs(regs);
if (!regs.isEmpty()) {
cls.addWarnComment("Init of enum " + enumField.getField().getName() + " can be incorrect");
if (valuesCandidates.size() != 1) {
data.cls.addWarnComment("Found several \"values\" enum fields: " + valuesCandidates);
return false;
}
MethodNode ctrMth = cls.root().resolveMethod(co.getCallMth());
data.valuesField = valuesCandidates.get(0);
// search "$VALUES" array init and collect enum fields
BlockInsnPair valuesInitPair = getValuesInitInsn(data);
if (valuesInitPair == null) {
return false;
}
data.valuesInitInsn = valuesInitPair.getInsn();
return true;
}
private void processConstructorInsn(EnumData data, EnumField enumField, MethodNode classInitMth) {
ConstructorInsn co = enumField.getConstrInsn();
ClassInfo enumClsInfo = co.getClassType();
if (!enumClsInfo.equals(data.cls.getClassInfo())) {
ClassNode enumCls = data.cls.root().resolveClass(enumClsInfo);
if (enumCls != null) {
processEnumCls(data.cls, enumField, enumCls);
}
}
MethodNode ctrMth = data.cls.root().resolveMethod(co.getCallMth());
if (ctrMth != null) {
markArgsForSkip(ctrMth);
}
RegisterArg coResArg = co.getResult();
if (coResArg == null || coResArg.getSVar().getUseList().size() <= 2) {
toRemove.add(co);
data.toRemove.add(co);
} else {
// constructor result used in other places -> replace constructor with enum field get (SGET)
IndexInsnNode enumGet = new IndexInsnNode(InsnType.SGET, enumField.getField().getFieldInfo(), 0);
enumGet.setResult(coResArg.duplicate());
BlockUtils.replaceInsn(classInitMth, staticBlock, co, enumGet);
BlockUtils.replaceInsn(classInitMth, co, enumGet);
}
}
@Nullable
private List<EnumField> extractEnumFieldsFromInsn(ClassNode cls, BlockNode staticBlock,
InsnNode wrappedInsn, List<InsnNode> toRemove) {
private List<EnumField> extractEnumFieldsFromInsn(EnumData enumData, InsnNode wrappedInsn) {
switch (wrappedInsn.getType()) {
case FILLED_NEW_ARRAY:
return extractEnumFieldsFromFilledArray(cls, wrappedInsn, staticBlock, toRemove);
return extractEnumFieldsFromFilledArray(enumData, wrappedInsn);
case INVOKE:
// handle redirection of values array fill (added in java 15)
return extractEnumFieldsFromInvoke(cls, staticBlock, (InvokeNode) wrappedInsn, toRemove);
return extractEnumFieldsFromInvoke(enumData, (InvokeNode) wrappedInsn);
case NEW_ARRAY:
InsnArg arg = wrappedInsn.getArg(0);
@@ -243,10 +274,9 @@ public class EnumVisitor extends AbstractVisitor {
}
}
private List<EnumField> extractEnumFieldsFromInvoke(ClassNode cls, BlockNode staticBlock,
InvokeNode invokeNode, List<InsnNode> toRemove) {
private List<EnumField> extractEnumFieldsFromInvoke(EnumData enumData, InvokeNode invokeNode) {
MethodInfo callMth = invokeNode.getCallMth();
MethodNode valuesMth = cls.root().resolveMethod(callMth);
MethodNode valuesMth = enumData.cls.root().resolveMethod(callMth);
if (valuesMth == null || valuesMth.isVoidReturn()) {
return null;
}
@@ -256,16 +286,16 @@ public class EnumVisitor extends AbstractVisitor {
if (wrappedInsn == null) {
return null;
}
List<EnumField> enumFields = extractEnumFieldsFromInsn(cls, staticBlock, wrappedInsn, toRemove);
List<EnumField> enumFields = extractEnumFieldsFromInsn(enumData, wrappedInsn);
if (enumFields != null) {
valuesMth.add(AFlag.DONT_GENERATE);
}
return enumFields;
}
private BlockInsnPair getValuesInitInsn(MethodNode classInitMth, FieldNode valuesField) {
FieldInfo searchField = valuesField.getFieldInfo();
for (BlockNode blockNode : classInitMth.getBasicBlocks()) {
private BlockInsnPair getValuesInitInsn(EnumData data) {
FieldInfo searchField = data.valuesField.getFieldInfo();
for (BlockNode blockNode : data.staticBlocks) {
for (InsnNode insn : blockNode.getInstructions()) {
if (insn.getType() == InsnType.SPUT) {
IndexInsnNode indexInsnNode = (IndexInsnNode) insn;
@@ -279,50 +309,49 @@ public class EnumVisitor extends AbstractVisitor {
return null;
}
private List<EnumField> extractEnumFieldsFromFilledArray(ClassNode cls, InsnNode arrFillInsn, BlockNode staticBlock,
List<InsnNode> toRemove) {
private List<EnumField> extractEnumFieldsFromFilledArray(EnumData enumData, InsnNode arrFillInsn) {
List<EnumField> enumFields = new ArrayList<>();
for (InsnArg arg : arrFillInsn.getArguments()) {
EnumField field = null;
if (arg.isInsnWrap()) {
InsnNode wrappedInsn = ((InsnWrapArg) arg).getWrapInsn();
field = processEnumFieldByWrappedInsn(cls, wrappedInsn, staticBlock, toRemove);
field = processEnumFieldByWrappedInsn(enumData, wrappedInsn);
} else if (arg.isRegister()) {
field = processEnumFieldByRegister(cls, (RegisterArg) arg, staticBlock, toRemove);
field = processEnumFieldByRegister(enumData, (RegisterArg) arg);
}
if (field == null) {
return null;
}
enumFields.add(field);
}
toRemove.add(arrFillInsn);
enumData.toRemove.add(arrFillInsn);
return enumFields;
}
private EnumField processEnumFieldByWrappedInsn(ClassNode cls, InsnNode wrappedInsn, BlockNode staticBlock, List<InsnNode> toRemove) {
private EnumField processEnumFieldByWrappedInsn(EnumData data, InsnNode wrappedInsn) {
if (wrappedInsn.getType() == InsnType.SGET) {
return processEnumFieldByField(cls, wrappedInsn, staticBlock, toRemove);
return processEnumFieldByField(data, wrappedInsn);
}
ConstructorInsn constructorInsn = castConstructorInsn(wrappedInsn);
if (constructorInsn != null) {
FieldNode enumFieldNode = createFakeField(cls, "EF" + constructorInsn.getOffset());
cls.addField(enumFieldNode);
return createEnumFieldByConstructor(cls, enumFieldNode, constructorInsn);
FieldNode enumFieldNode = createFakeField(data.cls, "EF" + constructorInsn.getOffset());
data.cls.addField(enumFieldNode);
return createEnumFieldByConstructor(data.cls, enumFieldNode, constructorInsn);
}
return null;
}
@Nullable
private EnumField processEnumFieldByField(ClassNode cls, InsnNode sgetInsn, BlockNode staticBlock, List<InsnNode> toRemove) {
private EnumField processEnumFieldByField(EnumData data, InsnNode sgetInsn) {
if (sgetInsn.getType() != InsnType.SGET) {
return null;
}
FieldInfo fieldInfo = (FieldInfo) ((IndexInsnNode) sgetInsn).getIndex();
FieldNode enumFieldNode = cls.searchField(fieldInfo);
FieldNode enumFieldNode = data.cls.searchField(fieldInfo);
if (enumFieldNode == null) {
return null;
}
InsnNode sputInsn = searchFieldPutInsn(cls, staticBlock, enumFieldNode);
InsnNode sputInsn = searchFieldPutInsn(data, enumFieldNode);
if (sputInsn == null) {
return null;
}
@@ -333,17 +362,17 @@ public class EnumVisitor extends AbstractVisitor {
}
RegisterArg sgetResult = sgetInsn.getResult();
if (sgetResult == null || sgetResult.getSVar().getUseCount() == 1) {
toRemove.add(sgetInsn);
data.toRemove.add(sgetInsn);
}
toRemove.add(sputInsn);
return createEnumFieldByConstructor(cls, enumFieldNode, co);
data.toRemove.add(sputInsn);
return createEnumFieldByConstructor(data.cls, enumFieldNode, co);
}
@Nullable
private EnumField processEnumFieldByRegister(ClassNode cls, RegisterArg arg, BlockNode staticBlock, List<InsnNode> toRemove) {
private EnumField processEnumFieldByRegister(EnumData data, RegisterArg arg) {
InsnNode assignInsn = arg.getAssignInsn();
if (assignInsn != null && assignInsn.getType() == InsnType.SGET) {
return processEnumFieldByField(cls, assignInsn, staticBlock, toRemove);
return processEnumFieldByField(data, assignInsn);
}
SSAVar ssaVar = arg.getSVar();
@@ -354,12 +383,12 @@ public class EnumVisitor extends AbstractVisitor {
if (constrInsn == null || constrInsn.getType() != InsnType.CONSTRUCTOR) {
return null;
}
FieldNode enumFieldNode = searchEnumField(cls, ssaVar, toRemove);
FieldNode enumFieldNode = searchEnumField(data, ssaVar);
if (enumFieldNode == null) {
enumFieldNode = createFakeField(cls, "EF" + arg.getRegNum());
cls.addField(enumFieldNode);
enumFieldNode = createFakeField(data.cls, "EF" + arg.getRegNum());
data.cls.addField(enumFieldNode);
}
return createEnumFieldByConstructor(cls, enumFieldNode, (ConstructorInsn) constrInsn);
return createEnumFieldByConstructor(data.cls, enumFieldNode, (ConstructorInsn) constrInsn);
}
private FieldNode createFakeField(ClassNode cls, String name) {
@@ -372,17 +401,17 @@ public class EnumVisitor extends AbstractVisitor {
}
@Nullable
private FieldNode searchEnumField(ClassNode cls, SSAVar ssaVar, List<InsnNode> toRemove) {
private FieldNode searchEnumField(EnumData data, SSAVar ssaVar) {
InsnNode sputInsn = ssaVar.getUseList().get(0).getParentInsn();
if (sputInsn == null || sputInsn.getType() != InsnType.SPUT) {
return null;
}
FieldInfo fieldInfo = (FieldInfo) ((IndexInsnNode) sputInsn).getIndex();
FieldNode enumFieldNode = cls.searchField(fieldInfo);
FieldNode enumFieldNode = data.cls.searchField(fieldInfo);
if (enumFieldNode == null) {
return null;
}
toRemove.add(sputInsn);
data.toRemove.add(sputInsn);
return enumFieldNode;
}
@@ -409,24 +438,32 @@ public class EnumVisitor extends AbstractVisitor {
if (ctrMth == null) {
return null;
}
List<RegisterArg> regs = new ArrayList<>();
co.getRegisterArgs(regs);
if (!regs.isEmpty()) {
throw new JadxRuntimeException("Init of enum " + enumFieldNode.getName() + " uses external variables");
}
return new EnumField(enumFieldNode, co);
}
@Nullable
private InsnNode searchFieldPutInsn(ClassNode cls, BlockNode staticBlock, FieldNode enumFieldNode) {
for (InsnNode sputInsn : staticBlock.getInstructions()) {
if (sputInsn != null && sputInsn.getType() == InsnType.SPUT) {
FieldInfo f = (FieldInfo) ((IndexInsnNode) sputInsn).getIndex();
FieldNode fieldNode = cls.searchField(f);
if (Objects.equals(fieldNode, enumFieldNode)) {
return sputInsn;
private InsnNode searchFieldPutInsn(EnumData data, FieldNode enumFieldNode) {
for (BlockNode block : data.staticBlocks) {
for (InsnNode sputInsn : block.getInstructions()) {
if (sputInsn != null && sputInsn.getType() == InsnType.SPUT) {
FieldInfo f = (FieldInfo) ((IndexInsnNode) sputInsn).getIndex();
FieldNode fieldNode = data.cls.searchField(f);
if (Objects.equals(fieldNode, enumFieldNode)) {
return sputInsn;
}
}
}
}
return null;
}
private void removeEnumMethods(ClassNode cls, ArgType clsType, FieldNode valuesField) {
private void removeEnumMethods(ClassNode cls, FieldNode valuesField) {
ArgType clsType = cls.getClassInfo().getType();
String valuesMethodShortId = "values()" + TypeGen.signature(ArgType.array(clsType));
MethodNode valuesMethod = null;
// remove compiler generated methods
@@ -605,4 +642,19 @@ public class EnumVisitor extends AbstractVisitor {
}
return null;
}
private static class EnumData {
final ClassNode cls;
final MethodNode classInitMth;
final List<BlockNode> staticBlocks;
final List<InsnNode> toRemove = new ArrayList<>();
FieldNode valuesField;
InsnNode valuesInitInsn;
public EnumData(ClassNode cls, MethodNode classInitMth, List<BlockNode> staticBlocks) {
this.cls = cls;
this.classInitMth = classInitMth;
this.staticBlocks = staticBlocks;
}
}
}
@@ -29,6 +29,7 @@ import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.visitors.shrink.CodeShrinkVisitor;
import jadx.core.utils.BlockUtils;
import jadx.core.utils.InsnRemover;
import jadx.core.utils.ListUtils;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxException;
@@ -45,20 +46,22 @@ public class ExtractFieldInit extends AbstractVisitor {
for (ClassNode inner : cls.getInnerClasses()) {
visit(inner);
}
moveStaticFieldsInit(cls);
moveCommonFieldsInit(cls);
if (!cls.getFields().isEmpty()) {
moveStaticFieldsInit(cls);
moveCommonFieldsInit(cls);
}
return false;
}
private static final class FieldInitInfo {
final FieldNode fieldNode;
final IndexInsnNode putInsn;
final boolean singlePath;
final boolean canMove;
public FieldInitInfo(FieldNode fieldNode, IndexInsnNode putInsn, boolean singlePath) {
public FieldInitInfo(FieldNode fieldNode, IndexInsnNode putInsn, boolean canMove) {
this.fieldNode = fieldNode;
this.putInsn = putInsn;
this.singlePath = singlePath;
this.canMove = canMove;
}
}
@@ -80,6 +83,9 @@ public class ExtractFieldInit extends AbstractVisitor {
|| classInitMth.getBasicBlocks() == null) {
return;
}
if (ListUtils.noneMatch(cls.getFields(), FieldNode::isStatic)) {
return;
}
while (processStaticFields(cls, classInitMth)) {
// sometimes instructions moved to field init prevent from vars inline -> inline and try again
CodeShrinkVisitor.shrinkMethod(classInitMth);
@@ -116,15 +122,15 @@ public class ExtractFieldInit extends AbstractVisitor {
}
private static void moveCommonFieldsInit(ClassNode cls) {
if (ListUtils.noneMatch(cls.getFields(), FieldNode::isInstance)) {
return;
}
List<MethodNode> constructors = getConstructorsList(cls);
if (constructors.isEmpty()) {
return;
}
List<ConstructorInitInfo> infoList = new ArrayList<>(constructors.size());
for (MethodNode constructorMth : constructors) {
if (constructorMth.isNoCode()) {
return;
}
List<FieldInitInfo> inits = collectFieldsInit(cls, constructorMth, InsnType.IPUT);
filterFieldsInit(inits);
if (inits.isEmpty()) {
@@ -168,19 +174,25 @@ public class ExtractFieldInit extends AbstractVisitor {
Set<BlockNode> singlePathBlocks = new HashSet<>();
BlockUtils.visitSinglePath(mth.getEnterBlock(), singlePathBlocks::add);
boolean canReorder = true;
for (BlockNode block : mth.getBasicBlocks()) {
for (InsnNode insn : block.getInstructions()) {
boolean fieldInsn = false;
if (insn.getType() == putType) {
IndexInsnNode putInsn = (IndexInsnNode) insn;
FieldInfo field = (FieldInfo) putInsn.getIndex();
if (field.getDeclClass().equals(cls.getClassInfo())) {
FieldNode fn = cls.searchField(field);
if (fn != null) {
boolean singlePath = singlePathBlocks.contains(block);
fieldsInit.add(new FieldInitInfo(fn, putInsn, singlePath));
boolean canMove = canReorder && singlePathBlocks.contains(block);
fieldsInit.add(new FieldInitInfo(fn, putInsn, canMove));
fieldInsn = true;
}
}
}
if (!fieldInsn && canReorder && !insn.canReorder()) {
canReorder = false;
}
}
}
return fieldsInit;
@@ -226,14 +238,14 @@ public class ExtractFieldInit extends AbstractVisitor {
}
private static boolean checkInsn(FieldInitInfo initInfo) {
if (!initInfo.singlePath) {
if (!initInfo.canMove) {
return false;
}
IndexInsnNode insn = initInfo.putInsn;
InsnArg arg = insn.getArg(0);
if (arg.isInsnWrap()) {
InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn();
if (!wrapInsn.canReorderRecursive() && insn.contains(AType.EXC_CATCH)) {
if (!wrapInsn.canReorder() && insn.contains(AType.EXC_CATCH)) {
return false;
}
} else {
@@ -364,7 +376,7 @@ public class ExtractFieldInit extends AbstractVisitor {
AccessInfo accFlags = mth.getAccessFlags();
if (!accFlags.isStatic() && accFlags.isConstructor()) {
list.add(mth);
if (BlockUtils.isAllBlocksEmpty(mth.getBasicBlocks())) {
if (mth.isNoCode() || BlockUtils.isAllBlocksEmpty(mth.getBasicBlocks())) {
return Collections.emptyList();
}
}
@@ -222,6 +222,10 @@ public class ProcessAnonymous extends AbstractVisitor {
// exclude self usage
return null;
}
if (ctrUseCls.getTopParentClass().equals(cls)) {
// exclude usage inside inner classes
return null;
}
for (MethodNode mth : cls.getMethods()) {
if (mth == ctr) {
continue;
@@ -1,12 +1,12 @@
package jadx.core.dex.visitors;
import jadx.core.Consts;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.visitors.usage.UsageInfoVisitor;
import jadx.core.utils.ListUtils;
import jadx.core.utils.exceptions.JadxException;
@JadxVisitor(
@@ -45,7 +45,14 @@ public class ProcessMethodsForInline extends AbstractVisitor {
}
AccessInfo accessFlags = mth.getAccessFlags();
boolean isSynthetic = accessFlags.isSynthetic() || mth.getName().contains("$");
return isSynthetic && (accessFlags.isStatic() || mth.isConstructor());
return isSynthetic && canInlineMethod(mth, accessFlags);
}
private static boolean canInlineMethod(MethodNode mth, AccessInfo accessFlags) {
if (accessFlags.isStatic()) {
return true;
}
return mth.isConstructor() && mth.root().getArgs().isInlineAnonymousClasses();
}
private static void fixClassDependencies(MethodNode mth) {
@@ -54,8 +61,14 @@ public class ProcessMethodsForInline extends AbstractVisitor {
// remove possible cross dependency
// to force class with inline method to be processed before its usage
ClassNode useTopCls = useInMth.getTopParentClass();
parentClass.setDependencies(ListUtils.safeRemoveAndTrim(parentClass.getDependencies(), useTopCls));
useTopCls.addCodegenDep(parentClass);
if (useTopCls != parentClass) {
parentClass.removeDependency(useTopCls);
useTopCls.addCodegenDep(parentClass);
if (Consts.DEBUG_USAGE) {
parentClass.addDebugComment("Remove dependency: " + useTopCls + " to inline " + mth);
useTopCls.addDebugComment("Add dependency: " + parentClass + " to inline " + mth);
}
}
}
}
}
@@ -171,10 +171,6 @@ public class BlockExceptionHandler {
}
}
protected static void removeTmpConnections(MethodNode mth) {
mth.getBasicBlocks().forEach(BlockExceptionHandler::removeTmpConnection);
}
private static void removeTmpConnection(BlockNode block) {
TmpEdgeAttr tmpEdgeAttr = block.get(AType.TMP_EDGE);
if (tmpEdgeAttr != null) {
@@ -402,6 +398,13 @@ public class BlockExceptionHandler {
}
BlockNode topDom = BlockUtils.getCommonDominator(mth, blocks);
if (topDom != null) {
// dominator always return one up block if blocks already contains dominator, use successor instead
if (topDom.getSuccessors().size() == 1) {
BlockNode upBlock = topDom.getSuccessors().get(0);
if (blocks.contains(upBlock)) {
return upBlock;
}
}
return adjustTopBlock(topDom);
}
throw new JadxRuntimeException("Failed to find top block for try-catch from: " + blocks);
@@ -2,7 +2,6 @@ package jadx.core.dex.visitors.blocks;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
@@ -432,7 +431,7 @@ public class BlockProcessor extends AbstractVisitor {
BlockNode loopHeader = loop.getStart();
List<BlockNode> preds = loopHeader.getPredecessors();
if (preds.size() > 2) {
List<BlockNode> blocks = new LinkedList<>(preds);
List<BlockNode> blocks = new ArrayList<>(preds);
blocks.removeIf(block -> block.contains(AFlag.LOOP_END));
BlockNode first = blocks.remove(0);
BlockNode preHeader = BlockSplitter.insertBlockBetween(mth, first, loopHeader);
@@ -137,8 +137,9 @@ public class BlockSplitter extends AbstractVisitor {
}
static BlockNode startNewBlock(MethodNode mth, int offset) {
BlockNode block = new BlockNode(mth.getBasicBlocks().size(), offset);
mth.getBasicBlocks().add(block);
List<BlockNode> blocks = mth.getBasicBlocks();
BlockNode block = new BlockNode(mth.getNextBlockCId(), blocks.size(), offset);
blocks.add(block);
return block;
}
@@ -391,7 +392,8 @@ public class BlockSplitter extends AbstractVisitor {
&& block.getSuccessors().size() <= 1
&& !block.getPredecessors().isEmpty()
&& !block.contains(AFlag.MTH_ENTER_BLOCK)
&& !block.contains(AFlag.MTH_EXIT_BLOCK);
&& !block.contains(AFlag.MTH_EXIT_BLOCK)
&& !block.getSuccessors().contains(block); // no self loop
}
static void collectSuccessors(BlockNode startBlock, BlockNode methodEnterBlock, Set<BlockNode> toRemove) {
@@ -25,23 +25,16 @@ public class DominatorTree {
List<BlockNode> sorted = sortBlocks(mth);
BlockNode[] doms = build(sorted);
apply(sorted, doms);
mth.setBasicBlocks(sorted);
}
private static List<BlockNode> sortBlocks(MethodNode mth) {
int blocksCount = mth.getBasicBlocks().size();
BitSet reachSet = new BitSet(blocksCount);
List<BlockNode> sorted = new ArrayList<>(blocksCount);
BlockUtils.dfsVisit(mth, b -> {
sorted.add(b);
reachSet.set(b.getId());
});
if (reachSet.cardinality() != blocksCount) {
BlockUtils.dfsVisit(mth, sorted::add);
if (sorted.size() != blocksCount) {
throw new JadxRuntimeException("Found unreachable blocks");
}
for (int i = 0; i < blocksCount; i++) {
sorted.get(i).setId(i);
}
mth.setBasicBlocks(sorted);
return sorted;
}
@@ -6,6 +6,7 @@ import java.util.List;
import java.util.Set;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.trycatch.ExceptionHandler;
import jadx.core.utils.Utils;
@@ -19,6 +20,10 @@ public class FinallyExtractInfo {
private final InsnsSlice finallyInsnsSlice = new InsnsSlice();
private final BlockNode startBlock;
private InsnsSlice curDupSlice;
private List<InsnNode> curDupInsns;
private int curDupInsnsOffset;
public FinallyExtractInfo(MethodNode mth, ExceptionHandler finallyHandler, BlockNode startBlock, List<BlockNode> allHandlerBlocks) {
this.mth = mth;
this.finallyHandler = finallyHandler;
@@ -54,6 +59,27 @@ public class FinallyExtractInfo {
return startBlock;
}
public InsnsSlice getCurDupSlice() {
return curDupSlice;
}
public void setCurDupSlice(InsnsSlice curDupSlice) {
this.curDupSlice = curDupSlice;
}
public List<InsnNode> getCurDupInsns() {
return curDupInsns;
}
public int getCurDupInsnsOffset() {
return curDupInsnsOffset;
}
public void setCurDupInsns(List<InsnNode> insns, int offset) {
this.curDupInsns = insns;
this.curDupInsnsOffset = offset;
}
@Override
public String toString() {
return "FinallyExtractInfo{"
@@ -12,6 +12,7 @@ import org.slf4j.LoggerFactory;
import jadx.core.Consts;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.RegDebugInfoAttr;
import jadx.core.dex.instructions.InsnType;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.RegisterArg;
@@ -28,6 +29,7 @@ import jadx.core.dex.visitors.IDexTreeVisitor;
import jadx.core.dex.visitors.JadxVisitor;
import jadx.core.dex.visitors.ssa.SSATransform;
import jadx.core.utils.BlockUtils;
import jadx.core.utils.InsnList;
import jadx.core.utils.ListUtils;
import jadx.core.utils.Utils;
@@ -376,6 +378,7 @@ public class MarkFinallyVisitor extends AbstractVisitor {
* 'Finally' instructions can start in the middle of the first block.
*/
private static InsnsSlice isStartBlock(BlockNode dupBlock, BlockNode finallyBlock, FinallyExtractInfo extractInfo) {
extractInfo.setCurDupSlice(null);
List<InsnNode> dupInsns = dupBlock.getInstructions();
List<InsnNode> finallyInsns = finallyBlock.getInstructions();
int dupSize = dupInsns.size();
@@ -386,7 +389,7 @@ public class MarkFinallyVisitor extends AbstractVisitor {
int startPos;
int endPos = 0;
if (dupSize == finSize) {
if (!checkInsns(dupInsns, finallyInsns, 0)) {
if (!checkInsns(extractInfo, dupInsns, finallyInsns, 0)) {
return null;
}
startPos = 0;
@@ -394,11 +397,11 @@ public class MarkFinallyVisitor extends AbstractVisitor {
// dupSize > finSize
startPos = dupSize - finSize;
// fast check from end of block
if (!checkInsns(dupInsns, finallyInsns, startPos)) {
if (!checkInsns(extractInfo, dupInsns, finallyInsns, startPos)) {
// search start insn
boolean found = false;
for (int i = 1; i < startPos; i++) {
if (checkInsns(dupInsns, finallyInsns, i)) {
if (checkInsns(extractInfo, dupInsns, finallyInsns, i)) {
startPos = i;
endPos = finSize + i;
found = true;
@@ -414,6 +417,7 @@ public class MarkFinallyVisitor extends AbstractVisitor {
// put instructions into slices
boolean complete;
InsnsSlice slice = new InsnsSlice();
extractInfo.setCurDupSlice(slice);
int endIndex;
if (endPos != 0) {
endIndex = endPos + 1;
@@ -453,11 +457,12 @@ public class MarkFinallyVisitor extends AbstractVisitor {
return slice;
}
private static boolean checkInsns(List<InsnNode> remInsns, List<InsnNode> finallyInsns, int delta) {
private static boolean checkInsns(FinallyExtractInfo extractInfo, List<InsnNode> dupInsns, List<InsnNode> finallyInsns, int delta) {
extractInfo.setCurDupInsns(dupInsns, delta);
for (int i = finallyInsns.size() - 1; i >= 0; i--) {
InsnNode startInsn = finallyInsns.get(i);
InsnNode remInsn = remInsns.get(delta + i);
if (!sameInsns(remInsn, startInsn)) {
InsnNode dupInsn = dupInsns.get(delta + i);
if (!sameInsns(extractInfo, dupInsn, startInsn)) {
return false;
}
}
@@ -509,8 +514,9 @@ public class MarkFinallyVisitor extends AbstractVisitor {
if (dupInsnCount < finallyInsnCount) {
return false;
}
extractInfo.setCurDupInsns(dupInsns, 0);
for (int i = 0; i < finallyInsnCount; i++) {
if (!sameInsns(dupInsns.get(i), finallyInsns.get(i))) {
if (!sameInsns(extractInfo, dupInsns.get(i), finallyInsns.get(i))) {
return false;
}
}
@@ -524,26 +530,85 @@ public class MarkFinallyVisitor extends AbstractVisitor {
return true;
}
private static boolean sameInsns(InsnNode remInsn, InsnNode fInsn) {
if (!remInsn.isSame(fInsn)) {
private static boolean sameInsns(FinallyExtractInfo extractInfo, InsnNode dupInsn, InsnNode fInsn) {
if (!dupInsn.isSame(fInsn)) {
return false;
}
// TODO: check instance arg in ConstructorInsn
// TODO: compare literals
for (int i = 0; i < remInsn.getArgsCount(); i++) {
InsnArg remArg = remInsn.getArg(i);
for (int i = 0; i < dupInsn.getArgsCount(); i++) {
InsnArg dupArg = dupInsn.getArg(i);
InsnArg fArg = fInsn.getArg(i);
if (remArg.isRegister() != fArg.isRegister()) {
if (!isSameArgs(extractInfo, dupArg, fArg)) {
return false;
}
boolean remConst = remArg.isConst();
if (remConst != fArg.isConst()) {
return false;
}
if (remConst && !remArg.isSameConst(fArg)) {
}
return true;
}
@SuppressWarnings("RedundantIfStatement")
private static boolean isSameArgs(FinallyExtractInfo extractInfo, InsnArg dupArg, InsnArg fArg) {
boolean isReg = dupArg.isRegister();
if (isReg != fArg.isRegister()) {
return false;
}
if (isReg) {
RegisterArg dupReg = (RegisterArg) dupArg;
RegisterArg fReg = (RegisterArg) fArg;
if (!dupReg.sameCodeVar(fReg)
&& !sameDebugInfo(dupReg, fReg)
&& assignedOutsideHandler(extractInfo, dupReg, fReg)
&& assignInsnDifferent(dupReg, fReg)) {
return false;
}
}
boolean remConst = dupArg.isConst();
if (remConst != fArg.isConst()) {
return false;
}
if (remConst && !dupArg.isSameConst(fArg)) {
return false;
}
return true;
}
private static boolean sameDebugInfo(RegisterArg dupReg, RegisterArg fReg) {
RegDebugInfoAttr fDbgInfo = fReg.get(AType.REG_DEBUG_INFO);
RegDebugInfoAttr dupDbgInfo = dupReg.get(AType.REG_DEBUG_INFO);
if (fDbgInfo == null || dupDbgInfo == null) {
return false;
}
return dupDbgInfo.equals(fDbgInfo);
}
private static boolean assignInsnDifferent(RegisterArg dupReg, RegisterArg fReg) {
InsnNode assignInsn = fReg.getAssignInsn();
InsnNode dupAssign = dupReg.getAssignInsn();
if (assignInsn == null || dupAssign == null) {
return true;
}
if (!assignInsn.isSame(dupAssign)) {
return true;
}
if (assignInsn.isConstInsn() && dupAssign.isConstInsn()) {
return !assignInsn.isDeepEquals(dupAssign);
}
return false;
}
@SuppressWarnings("RedundantIfStatement")
private static boolean assignedOutsideHandler(FinallyExtractInfo extractInfo, RegisterArg dupReg, RegisterArg fReg) {
if (InsnList.contains(extractInfo.getFinallyInsnsSlice().getInsnsList(), fReg.getAssignInsn())) {
return false;
}
InsnNode dupAssign = dupReg.getAssignInsn();
InsnsSlice curDupSlice = extractInfo.getCurDupSlice();
if (curDupSlice != null && InsnList.contains(curDupSlice.getInsnsList(), dupAssign)) {
return false;
}
List<InsnNode> curDupInsns = extractInfo.getCurDupInsns();
if (Utils.notEmpty(curDupInsns) && InsnList.contains(curDupInsns, dupAssign, extractInfo.getCurDupInsnsOffset())) {
return false;
}
return true;
}
@@ -8,6 +8,7 @@ import jadx.core.dex.nodes.IContainer;
import jadx.core.dex.nodes.IRegion;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.regions.Region;
import jadx.core.dex.regions.loops.LoopRegion;
import jadx.core.dex.visitors.AbstractVisitor;
public class CleanRegions extends AbstractVisitor {
@@ -42,6 +43,13 @@ public class CleanRegions extends AbstractVisitor {
BlockNode block = (BlockNode) container;
return block.getInstructions().isEmpty();
}
if (container instanceof LoopRegion) {
LoopRegion loopRegion = (LoopRegion) container;
if (loopRegion.isEndless()) {
// keep empty endless loops
return false;
}
}
if (container instanceof IRegion) {
List<IContainer> subBlocks = ((IRegion) container).getSubBlocks();
for (IContainer subBlock : subBlocks) {
@@ -19,6 +19,10 @@ public class DepthRegionTraversal {
traverseInternal(mth, visitor, mth.getRegion());
}
public static void traverse(MethodNode mth, IContainer container, IRegionVisitor visitor) {
traverseInternal(mth, visitor, container);
}
public static void traverseIterative(MethodNode mth, IRegionIterativeVisitor visitor) {
boolean repeat;
int k = 0;
@@ -3,8 +3,10 @@ package jadx.core.dex.visitors.regions;
import java.util.List;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.instructions.InsnType;
import jadx.core.dex.nodes.IContainer;
import jadx.core.dex.nodes.IRegion;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.regions.Region;
import jadx.core.dex.regions.conditions.IfCondition;
@@ -45,7 +47,7 @@ public class IfRegionVisitor extends AbstractVisitor {
}
}
@SuppressWarnings("UnnecessaryReturnStatement")
@SuppressWarnings({ "UnnecessaryReturnStatement", "StatementWithEmptyBody" })
private static void orderBranches(MethodNode mth, IfRegion ifRegion) {
if (RegionUtils.isEmpty(ifRegion.getElseRegion())) {
return;
@@ -79,9 +81,15 @@ public class IfRegionVisitor extends AbstractVisitor {
return;
}
}
boolean lastRegion = ifRegion == RegionUtils.getLastRegion(mth.getRegion());
boolean lastRegion = RegionUtils.hasExitEdge(ifRegion);
if (elseSize == 1 && lastRegion && mth.isVoidReturn()) {
// single return at method end will be removed later
InsnNode lastElseInsn = RegionUtils.getLastInsn(ifRegion.getElseRegion());
if (lastElseInsn != null && lastElseInsn.getType() == InsnType.THROW) {
// move `throw` into `then` block
invertIfRegion(ifRegion);
} else {
// single return at method end will be removed later
}
return;
}
if (!lastRegion) {
@@ -1,6 +1,6 @@
package jadx.core.dex.visitors.regions;
import java.util.LinkedList;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
@@ -125,7 +125,7 @@ public class LoopRegionVisitor extends AbstractVisitor implements IRegionVisitor
return false;
}
// can't make loop if argument from increment instruction is assign in loop
List<RegisterArg> args = new LinkedList<>();
List<RegisterArg> args = new ArrayList<>();
incrInsn.getRegisterArgs(args);
for (RegisterArg iArg : args) {
try {
@@ -268,7 +268,7 @@ public class LoopRegionVisitor extends AbstractVisitor implements IRegionVisitor
|| !checkInvoke(nextCall, "java.util.Iterator", "next()Ljava/lang/Object;")) {
return false;
}
List<InsnNode> toSkip = new LinkedList<>();
List<InsnNode> toSkip = new ArrayList<>();
RegisterArg iterVar;
if (nextCall.contains(AFlag.WRAPPED)) {
InsnArg wrapArg = BlockUtils.searchWrappedInsnParent(mth, nextCall);
@@ -464,7 +464,7 @@ public class RegionMaker {
BlockNode exitEnd = BlockUtils.followEmptyPath(exit);
List<LoopInfo> loops = exitEnd.getAll(AType.LOOP);
for (LoopInfo loopAtEnd : loops) {
if (loopAtEnd != loop) {
if (loopAtEnd != loop && loop.hasParent(loopAtEnd)) {
insertEdge = exitEdge;
confirm = true;
break;
@@ -10,6 +10,7 @@ import jadx.core.dex.instructions.PhiInsn;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.InsnWrapArg;
import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.instructions.args.SSAVar;
import jadx.core.dex.instructions.mods.TernaryInsn;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.IContainer;
@@ -73,11 +74,7 @@ public class TernaryMod extends AbstractRegionVisitor implements IRegionIterativ
return false;
}
if (elseRegion == null) {
if (mth.isConstructor()) {
// force ternary conversion to inline all code in 'super' or 'this' calls
return processOneBranchTernary(mth, ifRegion);
}
return false;
return processOneBranchTernary(mth, ifRegion);
}
BlockNode tb = getTernaryInsnBlock(thenRegion);
BlockNode eb = getTernaryInsnBlock(elseRegion);
@@ -93,21 +90,8 @@ public class TernaryMod extends AbstractRegionVisitor implements IRegionIterativ
InsnNode thenInsn = tb.getInstructions().get(0);
InsnNode elseInsn = eb.getInstructions().get(0);
if (mth.contains(AFlag.USE_LINES_HINTS)
&& thenInsn.getSourceLine() != elseInsn.getSourceLine()) {
if (thenInsn.getSourceLine() != 0 && elseInsn.getSourceLine() != 0) {
// sometimes source lines incorrect
if (!checkLineStats(thenInsn, elseInsn)) {
return false;
}
} else {
// no debug info
if (containsTernary(thenInsn) || containsTernary(elseInsn)) {
// don't make nested ternary by default
// TODO: add addition checks
return false;
}
}
if (!verifyLineHints(mth, thenInsn, elseInsn)) {
return false;
}
RegisterArg thenResArg = thenInsn.getResult();
@@ -184,6 +168,20 @@ public class TernaryMod extends AbstractRegionVisitor implements IRegionIterativ
return false;
}
private static boolean verifyLineHints(MethodNode mth, InsnNode thenInsn, InsnNode elseInsn) {
if (mth.contains(AFlag.USE_LINES_HINTS)
&& thenInsn.getSourceLine() != elseInsn.getSourceLine()) {
if (thenInsn.getSourceLine() != 0 && elseInsn.getSourceLine() != 0) {
// sometimes source lines incorrect
return checkLineStats(thenInsn, elseInsn);
}
// don't make nested ternary by default
// TODO: add addition checks
return !containsTernary(thenInsn) && !containsTernary(elseInsn);
}
return true;
}
private static void clearConditionBlocks(List<BlockNode> conditionBlocks, BlockNode header) {
for (BlockNode block : conditionBlocks) {
if (block != header) {
@@ -277,6 +275,7 @@ public class TernaryMod extends AbstractRegionVisitor implements IRegionIterativ
return false;
}
@SuppressWarnings("StatementWithEmptyBody")
private static void replaceWithTernary(MethodNode mth, IfRegion ifRegion, BlockNode block, InsnNode insn) {
RegisterArg resArg = insn.getResult();
if (resArg.getSVar().getUseList().size() != 1) {
@@ -296,17 +295,45 @@ public class TernaryMod extends AbstractRegionVisitor implements IRegionIterativ
if (otherArg == null) {
return;
}
InsnNode elseAssign = otherArg.getAssignInsn();
if (mth.isConstructor() || (mth.getParentClass().isEnum() && mth.getMethodInfo().isClassInit())) {
// forcing ternary inline for constructors (will help in moving super call to the top) and enums
// skip code style checks
} else {
if (elseAssign != null && elseAssign.isConstInsn()) {
if (!verifyLineHints(mth, insn, elseAssign)) {
return;
}
} else {
if (insn.getResult().sameCodeVar(otherArg)) {
// don't use same variable in else branch to prevent: l = (l == 0) ? 1 : l
return;
}
}
}
// all checks passed
BlockNode header = ifRegion.getConditionBlocks().get(0);
if (!ifRegion.getParent().replaceSubBlock(ifRegion, header)) {
return;
}
InsnArg elseArg;
if (elseAssign != null && elseAssign.isConstInsn()) {
// inline constant
SSAVar elseVar = elseAssign.getResult().getSVar();
if (elseVar.getUseCount() == 1 && elseVar.getOnlyOneUseInPhi() == phiInsn) {
InsnRemover.remove(mth, elseAssign);
}
elseArg = InsnArg.wrapInsnIntoArg(elseAssign);
} else {
elseArg = otherArg;
}
TernaryInsn ternInsn = new TernaryInsn(ifRegion.getCondition(),
phiInsn.getResult(), InsnArg.wrapInsnIntoArg(insn), otherArg);
phiInsn.getResult(), InsnArg.wrapInsnIntoArg(insn), elseArg);
ternInsn.simplifyCondition();
InsnRemover.unbindResult(mth, insn);
InsnList.remove(block, insn);
InsnRemover.unbindAllArgs(mth, phiInsn);
header.getInstructions().clear();
ternInsn.rebindArgs();
@@ -14,6 +14,9 @@ import jadx.core.Consts;
import jadx.core.codegen.json.JsonMappingGen;
import jadx.core.deobf.Deobfuscator;
import jadx.core.deobf.NameMapper;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.MethodOverrideAttr;
import jadx.core.dex.attributes.nodes.RenameReasonAttr;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.info.FieldInfo;
@@ -195,7 +198,7 @@ public class RenameVisitor extends AbstractVisitor {
Set<String> names = new HashSet<>(methods.size());
for (MethodNode mth : methods) {
String signature = mth.getMethodInfo().makeSignature(true, false);
if (!names.add(signature)) {
if (!names.add(signature) && canRename(mth)) {
deobfuscator.forceRenameMethod(mth);
mth.addAttr(new RenameReasonAttr("collision with other method in class"));
}
@@ -203,6 +206,23 @@ public class RenameVisitor extends AbstractVisitor {
}
}
private static boolean canRename(MethodNode mth) {
if (mth.contains(AFlag.DONT_RENAME)) {
return false;
}
MethodOverrideAttr overrideAttr = mth.get(AType.METHOD_OVERRIDE);
if (overrideAttr != null) {
for (MethodNode relatedMth : overrideAttr.getRelatedMthNodes()) {
if (relatedMth != mth && mth.getParentClass().equals(relatedMth.getParentClass())) {
// ignore rename if exists related method from same class (bridge method in most cases)
// such rename will also rename current method and will not help to resolve name collision
return false;
}
}
}
return true;
}
private static void processRootPackages(Deobfuscator deobfuscator, RootNode root, List<ClassNode> classes) {
Set<String> rootPkgs = collectRootPkgs(classes);
root.getCacheStorage().setRootPkgs(rootPkgs);
@@ -1,7 +1,7 @@
package jadx.core.dex.visitors.shrink;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.LinkedList;
import java.util.List;
import jadx.core.dex.instructions.InsnType;
@@ -30,7 +30,7 @@ final class ArgsInfo {
}
public static List<RegisterArg> getArgs(InsnNode insn) {
List<RegisterArg> args = new LinkedList<>();
List<RegisterArg> args = new ArrayList<>();
addArgs(insn, args);
return args;
}
@@ -1,10 +1,10 @@
package jadx.core.dex.visitors.ssa;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Deque;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import jadx.core.dex.attributes.AFlag;
@@ -81,7 +81,7 @@ public class SSATransform extends AbstractVisitor {
int blocksCount = blocks.size();
BitSet hasPhi = new BitSet(blocksCount);
BitSet processed = new BitSet(blocksCount);
Deque<BlockNode> workList = new LinkedList<>();
Deque<BlockNode> workList = new ArrayDeque<>();
BitSet assignBlocks = la.getAssignBlocks(regNum);
for (int id = assignBlocks.nextSetBit(0); id >= 0; id = assignBlocks.nextSetBit(id + 1)) {
@@ -136,7 +136,7 @@ public class SSATransform extends AbstractVisitor {
RenameState initState = RenameState.init(mth);
initPhiInEnterBlock(initState);
Deque<RenameState> stack = new LinkedList<>();
Deque<RenameState> stack = new ArrayDeque<>();
stack.push(initState);
while (!stack.isEmpty()) {
RenameState state = stack.pop();
@@ -913,25 +913,20 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
}
boolean fixed = false;
for (ITypeBound bound : typeInfo.getBounds()) {
if (bound.getBound() == BoundEnum.USE
&& fixBooleanUsage(mth, bound)) {
for (RegisterArg arg : new ArrayList<>(var.getUseList())) {
if (fixBooleanUsage(mth, arg)) {
fixed = true;
}
}
return fixed;
}
private boolean fixBooleanUsage(MethodNode mth, ITypeBound bound) {
ArgType boundType = bound.getType();
private boolean fixBooleanUsage(MethodNode mth, RegisterArg boundArg) {
ArgType boundType = boundArg.getInitType();
if (boundType == ArgType.BOOLEAN
|| (boundType.isTypeKnown() && !boundType.isPrimitive())) {
return false;
}
RegisterArg boundArg = bound.getArg();
if (boundArg == null) {
return false;
}
InsnNode insn = boundArg.getParentInsn();
if (insn == null || insn.getType() == InsnType.IF) {
return false;
@@ -0,0 +1,62 @@
package jadx.core.utils;
import java.util.HashSet;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.deobf.NameMapper;
import jadx.core.deobf.TldHelper;
public class BetterName {
private static final Logger LOG = LoggerFactory.getLogger(BetterName.class);
private static final boolean DEBUG = true;
public static String compareAndGet(String first, String second) {
if (Objects.equals(first, second)) {
return first;
}
int firstRating = calcRating(first);
int secondRating = calcRating(second);
boolean firstBetter = firstRating >= secondRating;
if (DEBUG) {
if (firstBetter) {
LOG.info("Better name: '{}' > '{}' ({} > {})", first, second, firstRating, secondRating);
} else {
LOG.info("Better name: '{}' > '{}' ({} > {})", second, first, secondRating, firstRating);
}
}
return firstBetter ? first : second;
}
public static int calcRating(String str) {
int rating = str.length() * 3;
rating += differentCharsCount(str) * 20;
if (NameMapper.isAllCharsPrintable(str)) {
rating += 100;
}
if (NameMapper.isValidIdentifier(str)) {
rating += 50;
}
if (TldHelper.contains(str)) {
rating += 20;
}
if (str.contains("_")) {
// rare in obfuscated names
rating += 100;
}
return rating;
}
private static int differentCharsCount(String str) {
String lower = str.toLowerCase(Locale.ROOT);
Set<Integer> chars = new HashSet<>();
StringUtils.visitCodePoints(lower, chars::add);
return chars.size();
}
}
@@ -697,7 +697,7 @@ public class BlockUtils {
}
/**
* Search lowest common ancestor in dominator tree for input set.
* Search the lowest common ancestor in dominator tree for input set.
*/
@Nullable
public static BlockNode getCommonDominator(MethodNode mth, List<BlockNode> blocks) {
@@ -29,8 +29,12 @@ public final class InsnList implements Iterable<InsnNode> {
}
public static int getIndex(List<InsnNode> list, InsnNode insn) {
return getIndex(list, insn, 0);
}
public static int getIndex(List<InsnNode> list, InsnNode insn, int startOffset) {
int size = list.size();
for (int i = 0; i < size; i++) {
for (int i = startOffset; i < size; i++) {
if (list.get(i) == insn) {
return i;
}
@@ -38,6 +42,14 @@ public final class InsnList implements Iterable<InsnNode> {
return -1;
}
public static boolean contains(List<InsnNode> list, InsnNode insn) {
return getIndex(list, insn, 0) != -1;
}
public static boolean contains(List<InsnNode> list, InsnNode insn, int startOffset) {
return getIndex(list, insn, startOffset) != -1;
}
public int getIndex(InsnNode insn) {
return getIndex(list, insn);
}
@@ -17,6 +17,7 @@ import jadx.core.dex.instructions.args.InsnWrapArg;
import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.instructions.args.SSAVar;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.IContainer;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.exceptions.JadxRuntimeException;
@@ -228,6 +229,18 @@ public class InsnRemover {
removeAll(block.getInstructions(), insns);
}
public static void removeAllAndUnbind(MethodNode mth, IContainer container, List<InsnNode> insns) {
unbindInsns(mth, insns);
RegionUtils.visitBlocks(mth, container, b -> removeAll(b.getInstructions(), insns));
}
public static void removeAllAndUnbind(MethodNode mth, List<InsnNode> insns) {
unbindInsns(mth, insns);
for (BlockNode block : mth.getBasicBlocks()) {
removeAll(block.getInstructions(), insns);
}
}
public static void removeAllWithoutUnbind(BlockNode block, List<InsnNode> insns) {
removeAll(block.getInstructions(), insns);
}
@@ -160,6 +160,10 @@ public class ListUtils {
return true;
}
public static <T> boolean noneMatch(Collection<T> list, Predicate<T> test) {
return !anyMatch(list, test);
}
public static <T> boolean anyMatch(Collection<T> list, Predicate<T> test) {
if (list == null || list.isEmpty()) {
return false;
@@ -4,6 +4,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import org.jetbrains.annotations.Nullable;
@@ -22,9 +23,12 @@ import jadx.core.dex.nodes.IRegion;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.regions.Region;
import jadx.core.dex.regions.loops.LoopRegion;
import jadx.core.dex.trycatch.CatchAttr;
import jadx.core.dex.trycatch.ExceptionHandler;
import jadx.core.dex.trycatch.TryCatchBlockAttr;
import jadx.core.dex.visitors.regions.AbstractRegionVisitor;
import jadx.core.dex.visitors.regions.DepthRegionTraversal;
import jadx.core.utils.exceptions.JadxRuntimeException;
public class RegionUtils {
@@ -145,20 +149,6 @@ public class RegionUtils {
}
}
@Nullable
public static IContainer getLastRegion(@Nullable IContainer container) {
if (container == null) {
return null;
}
if (container instanceof IBlock || container instanceof IBranchRegion) {
return container;
}
if (container instanceof IRegion) {
return getLastRegion(Utils.last(((IRegion) container).getSubBlocks()));
}
throw new JadxRuntimeException(unknownContainerType(container));
}
public static boolean isExitBlock(MethodNode mth, IContainer container) {
if (container instanceof BlockNode) {
return BlockUtils.isExitBlock(mth, (BlockNode) container);
@@ -289,7 +279,11 @@ public class RegionUtils {
}
}
return false;
} else if (container instanceof IRegion) {
}
if (container instanceof LoopRegion) {
return true;
}
if (container instanceof IRegion) {
IRegion region = (IRegion) container;
for (IContainer block : region.getSubBlocks()) {
if (notEmpty(block)) {
@@ -297,9 +291,8 @@ public class RegionUtils {
}
}
return false;
} else {
throw new JadxRuntimeException(unknownContainerType(container));
}
throw new JadxRuntimeException(unknownContainerType(container));
}
public static void getAllRegionBlocks(IContainer container, Set<IBlock> blocks) {
@@ -483,4 +476,13 @@ public class RegionUtils {
}
return "Unknown container type: " + container.getClass();
}
public static void visitBlocks(MethodNode mth, IContainer container, Consumer<IBlock> visitor) {
DepthRegionTraversal.traverse(mth, container, new AbstractRegionVisitor() {
@Override
public void processBlock(MethodNode mth, IBlock block) {
visitor.accept(block);
}
});
}
}
@@ -129,7 +129,7 @@ public class AndroidResourcesUtils {
FieldNode newResField = new FieldNode(typeCls, rFieldInfo,
AccessFlags.PUBLIC | AccessFlags.STATIC | AccessFlags.FINAL);
newResField.addAttr(new EncodedValue(EncodedType.ENCODED_INT, resource.getId()));
typeCls.getFields().add(newResField);
typeCls.addField(newResField);
if (rClsExists) {
newResField.addInfoComment("Added by JADX");
}
@@ -10,12 +10,16 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.jar.JarEntry;
@@ -108,13 +112,8 @@ public class FileUtils {
}
public static boolean deleteDir(File dir) {
File[] content = dir.listFiles();
if (content != null) {
for (File file : content) {
deleteDir(file);
}
}
return dir.delete();
deleteDir(dir.toPath());
return true;
}
public static void deleteDirIfExists(Path dir) {
@@ -127,16 +126,23 @@ public class FileUtils {
}
}
private static final SimpleFileVisitor<Path> FILE_DELETE_VISITOR = new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
};
private static void deleteDir(Path dir) {
try (Stream<Path> pathStream = Files.walk(dir)) {
pathStream.sorted(Comparator.reverseOrder())
.forEach(path -> {
try {
Files.delete(path);
} catch (Exception e) {
throw new JadxRuntimeException("Failed to delete path: " + path.toAbsolutePath(), e);
}
});
try {
Files.walkFileTree(dir, Collections.emptySet(), Integer.MAX_VALUE, FILE_DELETE_VISITOR);
} catch (Exception e) {
throw new JadxRuntimeException("Failed to delete directory " + dir, e);
}
@@ -228,7 +234,8 @@ public class FileUtils {
public static void writeFile(Path file, String data) throws IOException {
FileUtils.makeDirsForFile(file);
Files.write(file, data.getBytes(StandardCharsets.UTF_8));
Files.write(file, data.getBytes(StandardCharsets.UTF_8),
StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
}
public static String readFile(Path textFile) throws IOException {
@@ -4,10 +4,8 @@ import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.xml.parsers.DocumentBuilder;
@@ -175,7 +173,7 @@ public class ManifestAttributes {
if (attr.getType() == MAttrType.ENUM) {
return attr.getValues().get(value);
} else if (attr.getType() == MAttrType.FLAG) {
List<String> flagList = new LinkedList<>();
List<String> flagList = new ArrayList<>();
List<Long> attrKeys = new ArrayList<>(attr.getValues().keySet());
attrKeys.sort((a, b) -> Long.compare(b, a)); // sort descending
for (Long key : attrKeys) {
@@ -188,7 +186,7 @@ public class ManifestAttributes {
value ^= key;
}
}
return flagList.stream().collect(Collectors.joining("|"));
return String.join("|", flagList);
}
return null;
}
@@ -28,9 +28,12 @@ public class ParserConstants {
protected static final int RES_XML_LAST_CHUNK_TYPE = 0x017f;
protected static final int RES_XML_RESOURCE_MAP_TYPE = 0x0180;
protected static final int RES_TABLE_PACKAGE_TYPE = 0x0200;
protected static final int RES_TABLE_TYPE_TYPE = 0x0201;
protected static final int RES_TABLE_TYPE_SPEC_TYPE = 0x0202;
protected static final int RES_TABLE_PACKAGE_TYPE = 0x0200; // 512
protected static final int RES_TABLE_TYPE_TYPE = 0x0201; // 513
protected static final int RES_TABLE_TYPE_SPEC_TYPE = 0x0202; // 514
protected static final int RES_TABLE_TYPE_LIBRARY = 0x0203; // 515
protected static final int RES_TABLE_TYPE_OVERLAY = 0x0204; // 516
protected static final int RES_TABLE_TYPE_STAGED_ALIAS = 0x0206; // 517
/**
* Type constants
@@ -10,14 +10,18 @@ import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.ICodeInfo;
import jadx.api.args.ResourceNameSource;
import jadx.core.deobf.NameMapper;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.BetterName;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.xmlgen.entry.EntryConfig;
import jadx.core.xmlgen.entry.RawNamedValue;
import jadx.core.xmlgen.entry.RawValue;
@@ -149,13 +153,28 @@ public class ResTableParser extends CommonBinaryParser implements IResParser {
while (is.getPos() < endPos) {
long chunkStart = is.getPos();
int type = is.readInt16();
if (type == RES_NULL_TYPE) {
continue;
}
if (type == RES_TABLE_TYPE_SPEC_TYPE) {
parseTypeSpecChunk();
} else if (type == RES_TABLE_TYPE_TYPE) {
parseTypeChunk(chunkStart, pkg);
LOG.trace("res package chunk start at {} type {}", chunkStart, type);
switch (type) {
case RES_NULL_TYPE:
LOG.info("Null chunk type encountered at offset {}", chunkStart);
break;
case RES_TABLE_TYPE_TYPE: // 0x0201
parseTypeChunk(chunkStart, pkg);
break;
case RES_TABLE_TYPE_SPEC_TYPE: // 0x0202
parseTypeSpecChunk(chunkStart);
break;
case RES_TABLE_TYPE_LIBRARY: // 0x0203
parseLibraryTypeChunk(chunkStart);
break;
case RES_TABLE_TYPE_OVERLAY: // 0x0204
throw new IOException(
String.format("Encountered unsupported chunk type TYPE_OVERLAY at offset 0x%x ", chunkStart));
case RES_TABLE_TYPE_STAGED_ALIAS: // 0x0206
throw new IOException(
String.format("Encountered unsupported chunk type TYPE_STAGED_ALIAS at offset 0x%x ", chunkStart));
default:
LOG.warn("Unknown chunk type {} encountered at offset {}", type, chunkStart);
}
}
return pkg;
@@ -188,10 +207,10 @@ public class ResTableParser extends CommonBinaryParser implements IResParser {
}
@SuppressWarnings("unused")
private void parseTypeSpecChunk() throws IOException {
private void parseTypeSpecChunk(long chunkStart) throws IOException {
is.checkInt16(0x0010, "Unexpected type spec header size");
/* int size = */
is.readInt32();
int chunkSize = is.readInt32();
long expectedEndPos = chunkStart + chunkSize;
int id = is.readInt8();
is.skip(3);
@@ -199,6 +218,28 @@ public class ResTableParser extends CommonBinaryParser implements IResParser {
for (int i = 0; i < entryCount; i++) {
int entryFlag = is.readInt32();
}
if (is.getPos() != expectedEndPos) {
throw new IOException(String.format("Error reading type spec chunk at offset 0x%x", chunkStart));
}
}
private void parseLibraryTypeChunk(long chunkStart) throws IOException {
LOG.trace("parsing library type chunk starting at offset {}", chunkStart);
is.checkInt16(12, "Unexpected header size");
int chunkSize = is.readInt32();
long expectedEndPos = chunkStart + chunkSize;
int count = is.readInt32();
for (int i = 0; i < count; i++) {
int packageId = is.readInt32();
String packageName = is.readString16Fixed(128);
LOG.info("Found resource shared library {}, pkgId: {}", packageName, packageId);
if (is.getPos() > expectedEndPos) {
throw new IOException("reading after chunk end");
}
}
if (is.getPos() != expectedEndPos) {
throw new IOException(String.format("Error reading library chunk at offset 0x%x", chunkStart));
}
}
private void parseTypeChunk(long start, PackageChunk pkg) throws IOException {
@@ -237,6 +278,13 @@ public class ResTableParser extends CommonBinaryParser implements IResParser {
parseEntry(pkg, id, i, config.getQualifiers());
}
}
if (chunkEnd > is.getPos()) {
// Skip remaining unknown data in this chunk (e.g. type 8 entries")
long skipSize = chunkEnd - is.getPos();
LOG.debug("Unknown data at the end of type chunk encountered, skipping {} bytes and continuing at offset {}", skipSize,
chunkEnd);
is.skip(skipSize);
}
}
private void parseEntry(PackageChunk pkg, int typeId, int entryId, String config) throws IOException {
@@ -287,35 +335,66 @@ public class ResTableParser extends CommonBinaryParser implements IResParser {
if (renamedKey != null) {
return renamedKey;
}
FieldNode constField = root.getConstValues().getGlobalConstFields().get(resRef);
if (constField != null) {
constField.add(AFlag.DONT_RENAME);
return constField.getName();
}
// styles might contain dots in name, use VALID_RES_KEY_PATTERN only for resource file name
// styles might contain dots in name, search for alias only for resources names
if (typeName.equals("style")) {
return origKeyName;
} else if (VALID_RES_KEY_PATTERN.matcher(origKeyName).matches()) {
return origKeyName;
}
FieldNode constField = root.getConstValues().getGlobalConstFields().get(resRef);
String resAlias = getResAlias(resRef, origKeyName, constField);
resStorage.addRename(resRef, resAlias);
if (constField != null) {
constField.rename(resAlias);
constField.add(AFlag.DONT_RENAME);
}
return resAlias;
}
private String getResAlias(int resRef, String origKeyName, @Nullable FieldNode constField) {
String name;
if (constField == null || constField.getTopParentClass().isSynthetic()) {
name = origKeyName;
} else {
name = getBetterName(root.getArgs().getResourceNameSource(), origKeyName, constField.getName());
}
Matcher matcher = VALID_RES_KEY_PATTERN.matcher(name);
if (matcher.matches()) {
return name;
}
// Making sure origKeyName compliant with resource file name rules
Matcher m = VALID_RES_KEY_PATTERN.matcher(origKeyName);
String cleanedResName = cleanName(matcher);
String newResName = String.format("res_0x%08x", resRef);
if (cleanedResName.isEmpty()) {
return newResName;
}
// autogenerate key name, appended with cleaned origKeyName to be human-friendly
return newResName + "_" + cleanedResName.toLowerCase();
}
public static String getBetterName(ResourceNameSource nameSource, String resName, String codeName) {
switch (nameSource) {
case AUTO:
return BetterName.compareAndGet(resName, codeName);
case RESOURCES:
return resName;
case CODE:
return codeName;
default:
throw new JadxRuntimeException("Unexpected ResourceNameSource value: " + nameSource);
}
}
private String cleanName(Matcher matcher) {
StringBuilder sb = new StringBuilder();
boolean first = true;
while (m.find()) {
while (matcher.find()) {
if (!first) {
sb.append("_");
}
sb.append(m.group());
sb.append(matcher.group());
first = false;
}
// autogenerate key name, appended with cleaned origKeyName to be human-friendly
String newResName = String.format("res_0x%08x", resRef);
String cleanedResName = sb.toString();
if (!cleanedResName.isEmpty()) {
newResName += "_" + cleanedResName.toLowerCase();
}
return newResName;
return sb.toString();
}
private RawNamedValue parseValueMap() throws IOException {
@@ -168,11 +168,13 @@ public class ResXmlGen {
private void addItem(ICodeWriter cw, String itemTag, String typeName, RawNamedValue value) {
String nameStr = vp.decodeNameRef(value.getNameRef());
String valueStr = vp.decodeValue(value.getRawValue());
int dataType = value.getRawValue().getDataType();
if (!typeName.equals("attr")) {
if (valueStr == null || valueStr.equals("0")) {
if (dataType == ParserConstants.TYPE_REFERENCE && (valueStr == null || valueStr.equals("0"))) {
valueStr = "@null";
}
if (nameStr != null) {
if (dataType == ParserConstants.TYPE_INT_DEC && nameStr != null) {
try {
int intVal = Integer.parseInt(valueStr);
String newVal = ManifestAttributes.getInstance().decode(nameStr.replace("android:attr.", ""), intVal);
@@ -49,7 +49,7 @@ public class ResourcesSaver implements Runnable {
private void save(ResContainer rc, File outDir) {
File outFile = new File(outDir, rc.getFileName());
if (!ZipSecurity.isInSubDirectory(outDir, outFile)) {
LOG.error("Path traversal attack detected, invalid resource name: {}", outFile.getPath());
LOG.error("Invalid resource name or path traversal attack detected: {}", outFile.getPath());
return;
}
saveToFile(rc, outFile);
@@ -0,0 +1,22 @@
package jadx.core.utils;
import org.junit.jupiter.api.Test;
import static jadx.core.utils.BetterName.calcRating;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestBetterName {
@Test
public void test() {
expectFirst("color_main", "t0");
expectFirst("done", "oOo0oO0o");
}
private void expectFirst(String first, String second) {
String best = BetterName.compareAndGet(first, second);
assertThat(best)
.as(() -> String.format("'%s'=%d, '%s'=%d", first, calcRating(first), second, calcRating(second)))
.isEqualTo(first);
}
}
@@ -555,7 +555,7 @@ public abstract class IntegrationTest extends TestUtils {
protected void enableDeobfuscation() {
args.setDeobfuscationOn(true);
args.setDeobfuscationMapFileMode(DeobfuscationMapFileMode.OVERWRITE);
args.setDeobfuscationMapFileMode(DeobfuscationMapFileMode.IGNORE);
args.setDeobfuscationMinLength(2);
args.setDeobfuscationMaxLength(64);
}
@@ -113,7 +113,7 @@ public class TestCompiler implements Closeable {
assertNotNull(mth, "Failed to get method " + methodName + '(' + Arrays.toString(types) + ')');
return mth.invoke(inst, args);
} catch (Throwable e) {
IntegrationTest.rethrow("Invoke error", e);
IntegrationTest.rethrow("Invoke error for method: " + methodName, e);
return null;
}
}
@@ -6,6 +6,7 @@ import jadx.tests.api.SmaliTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
@SuppressWarnings("CommentedOutCode")
public class TestBooleanToInt extends SmaliTest {
// @formatter:off
@@ -0,0 +1,40 @@
package jadx.tests.integration.conditions;
import org.junit.jupiter.api.Test;
import jadx.tests.api.SmaliTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
@SuppressWarnings("CommentedOutCode")
public class TestBooleanToInt2 extends SmaliTest {
// @formatter:off
/*
public static class TestCls {
public void test() {
boolean v = getValue();
use1(Integer.valueOf(v));
use2(v);
}
private boolean getValue() {
return false;
}
private void use1(Integer v) {
}
private void use2(int v) {
}
}
*/
// @formatter:on
@Test
public void test() {
assertThat(getClassNodeFromSmali())
.code()
.containsOne("use1(Integer.valueOf(value ? 1 : 0));")
.containsOne("use2(value ? 1 : 0);");
}
}
@@ -0,0 +1,37 @@
package jadx.tests.integration.conditions;
import org.junit.jupiter.api.Test;
import jadx.api.ICodeWriter;
import jadx.tests.api.IntegrationTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestElseIfCodeStyle extends IntegrationTest {
@SuppressWarnings("unused")
public static class TestCls {
public void test(String str) {
if ("a".equals(str)) {
call(1);
} else if ("b".equals(str)) {
call(2);
} else if ("c".equals(str)) {
call(3);
}
}
private void call(int i) {
}
}
@Test
public void test() {
noDebugInfo();
assertThat(getClassNode(TestCls.class))
.code()
.doesNotContain("!\"c\".equals(str)")
.doesNotContain("{" + ICodeWriter.NL + indent(2) + "} else {"); // no empty `then` block
}
}
@@ -1,41 +0,0 @@
package jadx.tests.integration.conditions;
import org.junit.jupiter.api.Test;
import jadx.NotYetImplemented;
import jadx.tests.api.IntegrationTest;
public class TestSwitchTryBreak extends IntegrationTest {
public static class TestCls {
public void test(int x) {
switch (x) {
case 0:
return;
case 1:
String res;
if ("android".equals(toString())) {
res = "hello";
} else {
try {
if (String.CASE_INSENSITIVE_ORDER != null) {
break;
}
res = "hi";
} catch (Exception e) {
break;
}
}
System.out.println(res);
}
System.out.println("returning");
}
}
@Test
@NotYetImplemented
public void test() {
getClassNode(TestCls.class);
}
}
@@ -1,41 +0,0 @@
package jadx.tests.integration.conditions;
import org.junit.jupiter.api.Test;
import jadx.NotYetImplemented;
import jadx.tests.api.IntegrationTest;
public class TestSwitchTryBreak2 extends IntegrationTest {
public static class TestCls {
public void test(int x) {
switch (x) {
case 0:
return;
case 1:
String res;
if ("android".equals(toString())) {
res = "hello";
} else {
try {
if (x == 5) {
break;
}
res = "hi";
} catch (Exception e) {
break;
}
}
System.out.println(res);
}
System.out.println("returning");
}
}
@Test
@NotYetImplemented
public void test() {
getClassNode(TestCls.class);
}
}
@@ -0,0 +1,41 @@
package jadx.tests.integration.deobf;
import org.junit.jupiter.api.Test;
import jadx.tests.api.IntegrationTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestInheritedMethodRename extends IntegrationTest {
public static class TestCls {
public static class A extends B {
}
public static class B {
public void call() {
System.out.println("call");
}
}
public void test(A a) {
// reference to A.call() not renamed,
// should be resolved to B.call() and use alias
a.call();
}
}
@Test
public void test() {
noDebugInfo();
enableDeobfuscation();
getArgs().setDeobfuscationMinLength(99);
assertThat(getClassNode(TestCls.class))
.code()
.containsOne("public void m0call() {")
.doesNotContain(".call();")
.containsOne(".m0call();");
}
}
@@ -0,0 +1,45 @@
package jadx.tests.integration.enums;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
import jadx.tests.api.IntegrationTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestEnumsWithCustomInit extends IntegrationTest {
public enum TestCls {
ONE("I"),
TWO("II"),
THREE("III");
public static final Map<String, TestCls> MAP = new HashMap<>();
static {
for (TestCls value : values()) {
MAP.put(value.toString(), value);
}
}
private final String str;
TestCls(String str) {
this.str = str;
}
public String toString() {
return str;
}
}
@Test
public void test() {
assertThat(getClassNode(TestCls.class))
.code()
.containsOne("ONE(\"I\"),")
.doesNotContain("new TestEnumsWithCustomInit$TestCls(");
}
}
@@ -0,0 +1,39 @@
package jadx.tests.integration.enums;
import jadx.tests.api.IntegrationTest;
import jadx.tests.api.extensions.profiles.TestProfile;
import jadx.tests.api.extensions.profiles.TestWithProfiles;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestEnumsWithTernary extends IntegrationTest {
public enum TestCls {
FIRST(useNumber() ? "1" : "A"),
SECOND(useNumber() ? "2" : "B"),
ANY(useNumber() ? "1" : "2");
private final String str;
TestCls(String str) {
this.str = str;
}
public String getStr() {
return str;
}
public static boolean useNumber() {
return false;
}
}
@TestWithProfiles({ TestProfile.DX_J8, TestProfile.D8_J8 })
public void test() {
noDebugInfo();
assertThat(getClassNode(TestCls.class))
.code()
.containsOne("ANY(useNumber() ? \"1\" : \"2\");")
.doesNotContain("static {");
}
}
@@ -0,0 +1,47 @@
package jadx.tests.integration.inline;
import org.assertj.core.api.Condition;
import jadx.core.dex.nodes.ClassNode;
import jadx.tests.api.IntegrationTest;
import jadx.tests.api.extensions.profiles.TestProfile;
import jadx.tests.api.extensions.profiles.TestWithProfiles;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestSyntheticBridgeRename extends IntegrationTest {
@SuppressWarnings("InnerClassMayBeStatic")
public static class TestCls {
private abstract class Inner<V> {
public abstract V get(String value);
}
public class IntInner extends Inner<Integer> {
public Integer get(String value) {
return value.length();
}
}
public void test() {
IntInner inner = new IntInner();
call(inner.get("a"));
}
private static void call(Integer value) {
}
}
@TestWithProfiles({ TestProfile.DX_J8, TestProfile.JAVA8 })
public void test() {
ClassNode cls = getClassNode(TestCls.class);
assertThat(searchCls(cls.getInnerClasses(), "IntInner").getMethods())
.as("check that bridge method was generated by compiler")
.haveAtLeastOne(new Condition<>(mth -> mth.getAccessFlags().isBridge(), "bridge"));
assertThat(cls)
.code()
.doesNotContain("mo0get")
.containsOne("call(inner.get(\"a\"));");
}
}
@@ -0,0 +1,39 @@
package jadx.tests.integration.inner;
import org.junit.jupiter.api.Test;
import jadx.api.JadxInternalAccess;
import jadx.api.JavaClass;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.nodes.ClassNode;
import jadx.tests.api.IntegrationTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestAnonymousClass20 extends IntegrationTest {
@SuppressWarnings({ "unused", "checkstyle:TypeName", "Convert2Lambda", "Anonymous2MethodRef" })
public static class Test$Cls {
public Runnable test() {
return new Runnable() {
@Override
public void run() {
new Test$Cls();
}
};
}
}
@Test
public void test() {
ClassNode cls = getClassNode(Test$Cls.class);
assertThat(cls.get(AType.ANONYMOUS_CLASS)).isNull();
JavaClass javaClass = JadxInternalAccess.convertClassNode(jadxDecompiler, cls);
assertThat(javaClass.getTopParentClass()).isEqualTo(javaClass);
assertThat(cls)
.code()
.containsOne("new TestAnonymousClass20$Test$Cls();");
}
}
@@ -0,0 +1,20 @@
package jadx.tests.integration.loops;
import org.junit.jupiter.api.Test;
import jadx.tests.api.SmaliTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
/**
* Empty endless loop, issue #1611
*/
public class TestEndlessLoop2 extends SmaliTest {
@Test
public void test() {
disableCompilation();
assertThat(getClassNodeFromSmali())
.code()
.countString(2, "while (true) {");
}
}
@@ -1,7 +1,5 @@
package jadx.tests.integration.names;
import java.util.List;
import org.junit.jupiter.api.Test;
import jadx.api.CommentsLevel;
@@ -1,8 +1,8 @@
package jadx.tests.integration.names;
import java.util.ArrayDeque;
import java.util.BitSet;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import org.junit.jupiter.api.Test;
@@ -26,7 +26,7 @@ public class TestNameAssign2 extends IntegrationTest {
int blocksCount = blocks.size();
BitSet hasPhi = new BitSet(blocksCount);
BitSet processed = new BitSet(blocksCount);
Deque<BlockNode> workList = new LinkedList<>();
Deque<BlockNode> workList = new ArrayDeque<>();
BitSet assignBlocks = la.getAssignBlocks(regNum);
for (int id = assignBlocks.nextSetBit(0); id >= 0; id = assignBlocks.nextSetBit(id + 1)) {
@@ -0,0 +1,49 @@
package jadx.tests.integration.others;
import org.junit.jupiter.api.Test;
import jadx.tests.api.IntegrationTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
/**
* Negative case for field initialization move (#1599).
* Can't reorder with other instance methods.
*/
public class TestFieldInitNegative extends IntegrationTest {
public static class TestCls {
StringBuilder sb;
int field;
public TestCls() {
initBuilder(new StringBuilder("sb"));
this.field = initField();
this.sb.append(this.field);
}
private void initBuilder(StringBuilder sb) {
this.sb = sb;
}
private int initField() {
return sb.length();
}
public String getStr() {
return sb.toString();
}
public void check() {
assertThat(new TestCls().getStr()).isEqualTo("sb2"); // no NPE
}
}
@Test
public void test() {
assertThat(getClassNode(TestCls.class))
.code()
.doesNotContain("int field = initField();")
.containsOne("this.field = initField();");
}
}
@@ -0,0 +1,18 @@
package jadx.tests.integration.trycatch;
import org.junit.jupiter.api.Test;
import jadx.tests.api.SmaliTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestNestedTryCatch4 extends SmaliTest {
@Test
public void test() {
disableCompilation();
assertThat(getClassNodeFromSmali())
.code()
.doesNotContain("?? ");
}
}
@@ -0,0 +1,44 @@
package jadx.tests.integration.trycatch;
import org.junit.jupiter.api.Test;
import jadx.tests.api.SmaliTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
/**
* Negative test case for finally extract (issue 1592).
* Different registers incorrectly merged into one.
*/
@SuppressWarnings({ "CommentedOutCode", "GrazieInspection" })
public class TestTryCatchFinally15 extends SmaliTest {
// @formatter:off
/*
protected final Parcel test(int i, Parcel parcel) throws RemoteException {
Parcel obtain = Parcel.obtain();
try {
try {
this.zza.transact(i, parcel, obtain, 0);
obtain.readException();
return obtain;
} catch (RuntimeException e) {
obtain.recycle();
throw e;
}
} finally {
parcel.recycle();
}
}
*/
// @formatter:on
@Test
public void test() {
disableCompilation();
assertThat(getClassNodeFromSmali())
.code()
.doesNotContain("parcel = Parcel.obtain();")
.containsOne("this.zza.transact(i, parcel, obtain, 0);");
}
}
@@ -10,6 +10,7 @@ import jadx.core.dex.nodes.ClassNode;
import jadx.tests.api.IntegrationTest;
import static jadx.tests.api.utils.JadxMatchers.containsLines;
import static jadx.tests.api.utils.JadxMatchers.containsOne;
import static org.hamcrest.MatcherAssert.assertThat;
public class TestTryCatchFinally6 extends IntegrationTest {
@@ -54,15 +55,7 @@ public class TestTryCatchFinally6 extends IntegrationTest {
ClassNode cls = getClassNode(TestCls.class);
String code = cls.getCode().toString();
assertThat(code, containsLines(2,
"FileInputStream fileInputStream = null;",
"try {",
indent() + "call();",
indent() + "fileInputStream = new FileInputStream(\"1.txt\");",
"} finally {",
indent() + "if (fileInputStream != null) {",
indent() + indent() + "fileInputStream.close();",
indent() + '}',
"}"));
// impossible to proof that variables should be merged, so can't restore finally block here
assertThat(code, containsOne("if (0 != 0) {"));
}
}
@@ -0,0 +1,29 @@
.class public Lconditions/TestBooleanToInt2;
.super Ljava/lang/Object;
.method public test()V
.registers 3
invoke-direct {p0}, Lconditions/TestBooleanToInt2;->getValue()Z
move-result v0
invoke-static {v0}, Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer;
move-result-object v1
invoke-direct {p0, v1}, Lconditions/TestBooleanToInt2;->use1(Ljava/lang/Integer;)V
invoke-direct {p0, v0}, Lconditions/TestBooleanToInt2;->use2(I)V
return-void
.end method
.method private getValue()Z
.registers 2
const/4 v0, 0x0
return v0
.end method
.method private use1(Ljava/lang/Integer;)V
.registers 2
return-void
.end method
.method private use2(I)V
.registers 2
return-void
.end method
@@ -0,0 +1,90 @@
.class Lloops/TestEndlessLoop2;
.super Ljava/lang/Object;
.field instanceCount:J
.method test([Ljava/lang/String;)V
.registers 10
const/16 p1, 0xb
invoke-virtual {p0, p1}, Lloops/TestEndlessLoop2;->vMeth(I)V
const/16 v0, 0xf1
const-wide/high16 v1, 0x4032000000000000L # 18.0
:goto_a
const-wide/high16 v3, 0x4076000000000000L # 352.0
const/4 v5, 0x1
cmpg-double v6, v1, v3
if-gez v6, :cond_1c
const/4 v0, 0x1
:goto_12
add-int/2addr v0, v5
const/16 v3, 0x4b
if-ge v0, v3, :cond_18
goto :goto_12
:cond_18
const-wide/high16 v3, 0x3ff0000000000000L # 1.0
add-double/2addr v1, v3
goto :goto_a
:cond_1c
iget-wide v3, p0, Lloops/TestEndlessLoop2;->instanceCount:J
long-to-int v4, v3
const/16 v3, 0xb
:goto_21
const/16 v6, 0xf3
if-ge v5, v6, :cond_41
rem-int/lit8 v6, v5, 0x9
add-int/lit8 v6, v6, 0x12
if-eq v6, p1, :cond_3e
const/16 v7, 0x15
if-eq v6, v7, :cond_36
const/16 v7, 0x16
if-eq v6, v7, :cond_34
goto :goto_3b
:cond_34
add-int/2addr v4, v0
goto :goto_3b
:cond_36
const v6, 0xeed9
div-int/2addr v3, v6
nop
:goto_3b
add-int/lit8 v5, v5, 0x1
goto :goto_21
:cond_3e
nop
:goto_3f
nop
# endless loop with empty body
goto :goto_3f
:cond_41
sget-object p1, Ljava/lang/System;->out:Ljava/io/PrintStream;
invoke-static {v1, v2}, Ljava/lang/Double;->doubleToLongBits(D)J
move-result-wide v0
new-instance v2, Ljava/lang/StringBuilder;
invoke-direct {v2}, Ljava/lang/StringBuilder;-><init>()V
const-string v5, "i21 d2 i22 = "
invoke-virtual {v2, v5}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v2, v3}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;
const-string v3, ","
invoke-virtual {v2, v3}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v2, v0, v1}, Ljava/lang/StringBuilder;->append(J)Ljava/lang/StringBuilder;
invoke-virtual {v2, v3}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v2, v4}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;
invoke-virtual {v2}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0
invoke-virtual {p1, v0}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
return-void
.end method
@@ -0,0 +1,601 @@
.class public Ltrycatch/TestNestedTryCatch4;
.super Landroid/app/NativeActivity;
.method private test(Landroid/content/Intent;)V
.registers 11
.annotation system Ldalvik/annotation/MethodParameters;
accessFlags = {
0x0
}
names = {
"intent"
}
.end annotation
const-string v0, "IOException while closing input stream\n"
if-nez p1, :cond_5
return-void
:cond_5
const-string v1, "intent_cmd"
.line 1740
invoke-virtual {p1, v1}, Landroid/content/Intent;->getStringExtra(Ljava/lang/String;)Ljava/lang/String;
move-result-object v1
const-string v2, "MCPE"
if-eqz v1, :cond_80
.line 1741
invoke-virtual {v1}, Ljava/lang/String;->length()I
move-result v3
if-lez v3, :cond_80
.line 1743
:try_start_15
new-instance p1, Lorg/json/JSONObject;
invoke-direct {p1, v1}, Lorg/json/JSONObject;-><init>(Ljava/lang/String;)V
const-string v0, "Command"
.line 1744
invoke-virtual {p1, v0}, Lorg/json/JSONObject;->getString(Ljava/lang/String;)Ljava/lang/String;
move-result-object v0
const-string v1, "keyboardResult"
.line 1745
invoke-virtual {v0, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v1
if-eqz v1, :cond_33
const-string v0, "Text"
.line 1746
invoke-virtual {p1, v0}, Lorg/json/JSONObject;->getString(Ljava/lang/String;)Ljava/lang/String;
move-result-object p1
invoke-virtual {p0, p1}, Ltrycatch/TestNestedTryCatch4;->nativeSetTextboxText(Ljava/lang/String;)V
goto/16 :goto_208
:cond_33
const-string v1, "fileDialogResult"
.line 1748
invoke-virtual {v0, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v0
if-eqz v0, :cond_208
iget-wide v0, p0, Ltrycatch/TestNestedTryCatch4;->mFileDialogCallback:J
const-wide/16 v3, 0x0
cmp-long v5, v0, v3
if-eqz v5, :cond_208
const-string v0, "Result"
.line 1749
invoke-virtual {p1, v0}, Lorg/json/JSONObject;->getString(Ljava/lang/String;)Ljava/lang/String;
move-result-object v0
const-string v1, "Ok"
invoke-virtual {v0, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v0
if-eqz v0, :cond_5d
.line 1750
iget-wide v0, p0, Ltrycatch/TestNestedTryCatch4;->mFileDialogCallback:J
const-string v5, "Path"
invoke-virtual {p1, v5}, Lorg/json/JSONObject;->getString(Ljava/lang/String;)Ljava/lang/String;
move-result-object p1
invoke-virtual {p0, v0, v1, p1}, Ltrycatch/TestNestedTryCatch4;->nativeOnPickImageSuccess(JLjava/lang/String;)V
goto :goto_62
.line 1753
:cond_5d
iget-wide v0, p0, Ltrycatch/TestNestedTryCatch4;->mFileDialogCallback:J
invoke-virtual {p0, v0, v1}, Ltrycatch/TestNestedTryCatch4;->nativeOnPickImageCanceled(J)V
.line 1755
:goto_62
iput-wide v3, p0, Ltrycatch/TestNestedTryCatch4;->mFileDialogCallback:J
:try_end_64
.catch Lorg/json/JSONException; {:try_start_15 .. :try_end_64} :catch_66
goto/16 :goto_208
:catch_66
move-exception p1
.line 1759
new-instance v0, Ljava/lang/StringBuilder;
invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V
const-string v1, "JSONObject exception:"
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {p1}, Lorg/json/JSONException;->toString()Ljava/lang/String;
move-result-object p1
invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object p1
invoke-static {v2, p1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
return-void
.line 1765
:cond_80
invoke-virtual {p1}, Landroid/content/Intent;->getAction()Ljava/lang/String;
move-result-object v1
.line 1766
invoke-virtual {p1}, Landroid/content/Intent;->getType()Ljava/lang/String;
const-string/jumbo v3, "xbox_live_game_invite"
.line 1768
invoke-virtual {v3, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v3
if-eqz v3, :cond_b0
const-string/jumbo v0, "xbl"
.line 1771
invoke-virtual {p1, v0}, Landroid/content/Intent;->getStringExtra(Ljava/lang/String;)Ljava/lang/String;
move-result-object p1
.line 1772
new-instance v0, Ljava/lang/StringBuilder;
invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V
const-string v3, "[XboxLive] Received Invite "
invoke-virtual {v0, v3}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0
invoke-static {v2, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 1776
invoke-virtual {p0, v1, p1}, Ltrycatch/TestNestedTryCatch4;->nativeProcessIntentUriQuery(Ljava/lang/String;Ljava/lang/String;)V
goto/16 :goto_208
:cond_b0
const-string v3, "android.intent.action.VIEW"
.line 1779
invoke-virtual {v3, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v3
if-nez v3, :cond_c0
const-string v3, "org.chromium.arc.intent.action.VIEW"
invoke-virtual {v3, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v1
if-eqz v1, :cond_208
.line 1780
:cond_c0
invoke-virtual {p1}, Landroid/content/Intent;->getScheme()Ljava/lang/String;
move-result-object v1
.line 1781
invoke-virtual {p1}, Landroid/content/Intent;->getData()Landroid/net/Uri;
move-result-object p1
if-nez p1, :cond_cb
return-void
:cond_cb
const-string v3, "minecraft"
.line 1787
invoke-virtual {v3, v1}, Ljava/lang/String;->equalsIgnoreCase(Ljava/lang/String;)Z
move-result v3
if-nez v3, :cond_1f9
const-string v3, "minecraftedu"
invoke-virtual {v3, v1}, Ljava/lang/String;->equalsIgnoreCase(Ljava/lang/String;)Z
move-result v3
if-eqz v3, :cond_dd
goto/16 :goto_1f9
:cond_dd
const-string v3, "file"
.line 1795
invoke-virtual {v3, v1}, Ljava/lang/String;->equalsIgnoreCase(Ljava/lang/String;)Z
move-result v3
const-string v4, "&"
if-eqz v3, :cond_108
.line 1798
new-instance v0, Ljava/lang/StringBuilder;
invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V
invoke-virtual {p1}, Landroid/net/Uri;->getPath()Ljava/lang/String;
move-result-object v1
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v0, v4}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {p1}, Landroid/net/Uri;->getPath()Ljava/lang/String;
move-result-object p1
invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object p1
const-string v0, "fileIntent"
invoke-virtual {p0, v0, p1}, Ltrycatch/TestNestedTryCatch4;->nativeProcessIntentUriQuery(Ljava/lang/String;Ljava/lang/String;)V
goto/16 :goto_208
:cond_108
const-string v3, "content"
.line 1800
invoke-virtual {v3, v1}, Ljava/lang/String;->equalsIgnoreCase(Ljava/lang/String;)Z
move-result v1
if-eqz v1, :cond_208
.line 1803
new-instance v1, Ljava/io/File;
invoke-virtual {p1}, Landroid/net/Uri;->getPath()Ljava/lang/String;
move-result-object v3
invoke-direct {v1, v3}, Ljava/io/File;-><init>(Ljava/lang/String;)V
invoke-virtual {v1}, Ljava/io/File;->getName()Ljava/lang/String;
move-result-object v1
.line 1804
new-instance v3, Ljava/io/File;
new-instance v5, Ljava/lang/StringBuilder;
invoke-direct {v5}, Ljava/lang/StringBuilder;-><init>()V
invoke-virtual {p0}, Ltrycatch/TestNestedTryCatch4;->getApplicationContext()Landroid/content/Context;
move-result-object v6
invoke-virtual {v6}, Landroid/content/Context;->getCacheDir()Ljava/io/File;
move-result-object v6
invoke-virtual {v5, v6}, Ljava/lang/StringBuilder;->append(Ljava/lang/Object;)Ljava/lang/StringBuilder;
const-string v6, "/"
invoke-virtual {v5, v6}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v5, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v5}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v1
invoke-direct {v3, v1}, Ljava/io/File;-><init>(Ljava/lang/String;)V
.line 1806
invoke-virtual {p0}, Ltrycatch/TestNestedTryCatch4;->getContentResolver()Landroid/content/ContentResolver;
move-result-object v1
.line 1810
:try_start_142
invoke-virtual {v1, p1}, Landroid/content/ContentResolver;->openInputStream(Landroid/net/Uri;)Ljava/io/InputStream;
move-result-object v1
:try_end_146
.catch Ljava/io/IOException; {:try_start_142 .. :try_end_146} :catch_1df
.line 1818
:try_start_146
new-instance v5, Ljava/io/FileOutputStream;
invoke-direct {v5, v3}, Ljava/io/FileOutputStream;-><init>(Ljava/io/File;)V
const/high16 v6, 0x100000
new-array v6, v6, [B
.line 1824
:goto_14f
invoke-virtual {v1, v6}, Ljava/io/InputStream;->read([B)I
move-result v7
const/4 v8, -0x1
if-eq v7, v8, :cond_15b
const/4 v8, 0x0
.line 1825
invoke-virtual {v5, v6, v8, v7}, Ljava/io/OutputStream;->write([BII)V
goto :goto_14f
.line 1828
:cond_15b
invoke-virtual {v5}, Ljava/io/OutputStream;->close()V
const-string v5, "contentIntent"
.line 1831
new-instance v6, Ljava/lang/StringBuilder;
invoke-direct {v6}, Ljava/lang/StringBuilder;-><init>()V
invoke-virtual {p1}, Landroid/net/Uri;->getPath()Ljava/lang/String;
move-result-object p1
invoke-virtual {v6, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v6, v4}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v3}, Ljava/io/File;->getAbsolutePath()Ljava/lang/String;
move-result-object p1
invoke-virtual {v6, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v6}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object p1
invoke-virtual {p0, v5, p1}, Ltrycatch/TestNestedTryCatch4;->nativeProcessIntentUriQuery(Ljava/lang/String;Ljava/lang/String;)V
:try_end_17d
.catch Ljava/io/IOException; {:try_start_146 .. :try_end_17d} :catch_18b
.catchall {:try_start_146 .. :try_end_17d} :catchall_189
.line 1842
:try_start_17d
invoke-virtual {v1}, Ljava/io/InputStream;->close()V
:try_end_180
.catch Ljava/io/IOException; {:try_start_17d .. :try_end_180} :catch_182
goto/16 :goto_208
:catch_182
move-exception p1
.line 1845
new-instance v1, Ljava/lang/StringBuilder;
invoke-direct {v1}, Ljava/lang/StringBuilder;-><init>()V
goto :goto_1b1
:catchall_189
move-exception p1
goto :goto_1c3
:catch_18b
move-exception p1
.line 1833
:try_start_18c
new-instance v4, Ljava/lang/StringBuilder;
invoke-direct {v4}, Ljava/lang/StringBuilder;-><init>()V
const-string v5, "IOException while copying file from content intent\n"
invoke-virtual {v4, v5}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {p1}, Ljava/io/IOException;->toString()Ljava/lang/String;
move-result-object p1
invoke-virtual {v4, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v4}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object p1
invoke-static {v2, p1}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I
:try_end_1a4
.catchall {:try_start_18c .. :try_end_1a4} :catchall_189
.line 1837
:try_start_1a4
invoke-virtual {v3}, Ljava/io/File;->delete()Z
:try_end_1a7
.catch Ljava/lang/Exception; {:try_start_1a4 .. :try_end_1a7} :catch_1a7
.catchall {:try_start_1a4 .. :try_end_1a7} :catchall_189
.line 1842
:catch_1a7
:try_start_1a7
invoke-virtual {v1}, Ljava/io/InputStream;->close()V
:try_end_1aa
.catch Ljava/io/IOException; {:try_start_1a7 .. :try_end_1aa} :catch_1ab
goto :goto_208
:catch_1ab
move-exception p1
.line 1845
new-instance v1, Ljava/lang/StringBuilder;
invoke-direct {v1}, Ljava/lang/StringBuilder;-><init>()V
:goto_1b1
invoke-virtual {v1, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {p1}, Ljava/io/IOException;->toString()Ljava/lang/String;
move-result-object p1
invoke-virtual {v1, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object p1
invoke-static {v2, p1}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I
goto :goto_208
.line 1842
:goto_1c3
:try_start_1c3
invoke-virtual {v1}, Ljava/io/InputStream;->close()V
:try_end_1c6
.catch Ljava/io/IOException; {:try_start_1c3 .. :try_end_1c6} :catch_1c7
goto :goto_1de
:catch_1c7
move-exception v1
.line 1845
new-instance v3, Ljava/lang/StringBuilder;
invoke-direct {v3}, Ljava/lang/StringBuilder;-><init>()V
invoke-virtual {v3, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v1}, Ljava/io/IOException;->toString()Ljava/lang/String;
move-result-object v0
invoke-virtual {v3, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v3}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0
invoke-static {v2, v0}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I
.line 1847
:goto_1de
throw p1
:catch_1df
move-exception p1
.line 1813
new-instance v0, Ljava/lang/StringBuilder;
invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V
const-string v1, "IOException while opening file from content intent\n"
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {p1}, Ljava/io/IOException;->toString()Ljava/lang/String;
move-result-object p1
invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object p1
invoke-static {v2, p1}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I
return-void
.line 1788
:cond_1f9
:goto_1f9
invoke-virtual {p1}, Landroid/net/Uri;->getHost()Ljava/lang/String;
move-result-object v0
.line 1789
invoke-virtual {p1}, Landroid/net/Uri;->getQuery()Ljava/lang/String;
move-result-object p1
if-nez v0, :cond_205
if-eqz p1, :cond_208
.line 1792
:cond_205
invoke-virtual {p0, v0, p1}, Ltrycatch/TestNestedTryCatch4;->nativeProcessIntentUriQuery(Ljava/lang/String;Ljava/lang/String;)V
:cond_208
:goto_208
return-void
.end method
@@ -0,0 +1,60 @@
.class public Ltrycatch/TestTryCatchFinally15;
.super Ljava/lang/Object;
.implements Landroid/os/IInterface;
.field private final zza:Landroid/os/IBinder;
.field private final zzb:Ljava/lang/String;
.method protected final test(ILandroid/os/Parcel;)Landroid/os/Parcel;
.registers 6
.annotation system Ldalvik/annotation/Throws;
value = {
Landroid/os/RemoteException;
}
.end annotation
.line 1
invoke-static {}, Landroid/os/Parcel;->obtain()Landroid/os/Parcel;
move-result-object v0
:try_start_4
iget-object v1, p0, Ltrycatch/TestTryCatchFinally15;->zza:Landroid/os/IBinder;
const/4 v2, 0x0
.line 2
invoke-interface {v1, p1, p2, v0, v2}, Landroid/os/IBinder;->transact(ILandroid/os/Parcel;Landroid/os/Parcel;I)Z
.line 3
invoke-virtual {v0}, Landroid/os/Parcel;->readException()V
:try_end_d
.catch Ljava/lang/RuntimeException; {:try_start_4 .. :try_end_d} :catch_13
.catchall {:try_start_4 .. :try_end_d} :catchall_11
.line 6
invoke-virtual {p2}, Landroid/os/Parcel;->recycle()V
return-object v0
:catchall_11
move-exception p1
goto :goto_18
.line 5
:catch_13
move-exception p1
.line 4
:try_start_14
invoke-virtual {v0}, Landroid/os/Parcel;->recycle()V
.line 5
throw p1
:try_end_18
.catchall {:try_start_14 .. :try_end_18} :catchall_11
.line 6
:goto_18
invoke-virtual {p2}, Landroid/os/Parcel;->recycle()V
.line 7
throw p1
.end method
+18 -12
View File
@@ -9,25 +9,25 @@ dependencies {
implementation(project(':jadx-core'))
implementation(project(":jadx-cli"))
implementation 'com.beust:jcommander:1.82'
implementation 'ch.qos.logback:logback-classic:1.2.11'
implementation 'ch.qos.logback:logback-classic:1.3.4'
implementation 'com.fifesoft:rsyntaxtextarea:3.2.0'
implementation 'com.fifesoft:rsyntaxtextarea:3.3.0'
implementation files('libs/jfontchooser-1.0.5.jar')
implementation 'hu.kazocsaba:image-viewer:1.2.3'
implementation 'com.formdev:flatlaf:2.4'
implementation 'com.formdev:flatlaf-intellij-themes:2.4'
implementation 'com.formdev:flatlaf-extras:2.4'
implementation 'com.formdev:svgSalamander:1.1.3'
implementation 'com.formdev:flatlaf:2.6'
implementation 'com.formdev:flatlaf-intellij-themes:2.6'
implementation 'com.formdev:flatlaf-extras:2.6'
implementation 'com.formdev:svgSalamander:1.1.4'
implementation 'com.google.code.gson:gson:2.9.0'
implementation 'com.google.code.gson:gson:2.9.1'
implementation 'org.apache.commons:commons-lang3:3.12.0'
implementation 'org.apache.commons:commons-text:1.9'
implementation 'org.apache.commons:commons-text:1.10.0'
implementation 'io.reactivex.rxjava2:rxjava:2.2.21'
implementation "com.github.akarnokd:rxjava2-swing:0.3.7"
implementation 'com.android.tools.build:apksig:7.2.1'
implementation 'io.github.hqktech:jdwp:1.0'
implementation 'com.android.tools.build:apksig:7.3.1'
implementation 'io.github.skylot:jdwp:2.0.0'
// TODO: Switch back to upstream once this PR gets merged:
// https://github.com/FabricMC/mapping-io/pull/19
@@ -42,7 +42,8 @@ application {
mainClass.set('jadx.gui.JadxGUI')
// The option -XX:+UseG1GC is only relevant for Java 8. Starting with Java 9 G1GC is already the default GC
applicationDefaultJvmArgs = ['-Xms128M', '-XX:MaxRAMPercentage=70.0', '-XX:+UseG1GC',
'-Dawt.useSystemAAFontSettings=lcd', '-Dswing.aatext=true']
'-Dawt.useSystemAAFontSettings=lcd', '-Dswing.aatext=true',
'-Djava.util.Arrays.useLegacyMergeSort=true']
}
applicationDistribution.with {
@@ -89,7 +90,7 @@ launch4j {
windowTitle = 'jadx'
companyName = 'jadx'
jreMinVersion = '11'
jvmOptions = ['-Dawt.useSystemAAFontSettings=lcd', '-Dswing.aatext=true', '-XX:+UseG1GC']
jvmOptions = ['-Dawt.useSystemAAFontSettings=lcd', '-Dswing.aatext=true', '-XX:+UseG1GC', '-Djava.util.Arrays.useLegacyMergeSort=true']
jreRuntimeBits = "64"
bundledJre64Bit = true
initialHeapPercent = 5
@@ -138,3 +139,8 @@ task distWinWithJre(type: Zip, dependsOn: ['copyDistWinWithJre']) {
}
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
task addNewNLSLines(type: JavaExec) {
classpath = sourceSets.main.runtimeClasspath
mainClass.set('jadx.gui.utils.tools.NLSAddNewLines')
}
@@ -44,11 +44,12 @@ import jadx.gui.ui.panel.JDebuggerPanel.ValueTreeNode;
import jadx.gui.utils.NLS;
public final class DebugController implements SmaliDebugger.SuspendListener, IDebugController {
private static final Logger LOG = LoggerFactory.getLogger(DebugController.class);
private static final Logger LOG = LoggerFactory.getLogger(DebugController.class);
private static final String ONCREATE_SIGNATURE = "onCreate(Landroid/os/Bundle;)V";
private static final Map<String, RuntimeType> TYPE_MAP = new HashMap<>();
private static final RuntimeType[] POSSIBLE_TYPES = { RuntimeType.OBJECT, RuntimeType.INT, RuntimeType.LONG };
private static final int DEFAULT_CACHE_SIZE = 512;
private JDebuggerPanel debuggerPanel;
private SmaliDebugger debugger;
@@ -1115,7 +1116,7 @@ public final class DebugController implements SmaliDebugger.SuspendListener, IDe
private long thisID;
public FrameNode(long threadID, SmaliDebugger.Frame frame) {
cache = new StringBuilder(16);
cache = new StringBuilder(DEFAULT_CACHE_SIZE);
this.frame = frame;
this.threadID = threadID;
regNodes = Collections.emptyList();
@@ -1153,7 +1154,7 @@ public final class DebugController implements SmaliDebugger.SuspendListener, IDe
public void setSignatures(String clsSig, String mthSig) {
this.clsSig = clsSig;
this.mthSig = mthSig;
this.cache.delete(0, this.cache.length());
resetCache();
}
public String getClsSig() {
@@ -1167,7 +1168,7 @@ public final class DebugController implements SmaliDebugger.SuspendListener, IDe
public void updateCodeOffset(long codeOffset) {
this.codeOffset = codeOffset;
if (this.codeOffset > -1) {
this.cache.delete(0, this.cache.length());
resetCache();
}
}
@@ -1209,27 +1210,34 @@ public final class DebugController implements SmaliDebugger.SuspendListener, IDe
}
}
private void resetCache() {
// Do not reuse thee existing cache instance as this can result in
// multi-threading access issues in case toString() method is active
this.cache = new StringBuilder(DEFAULT_CACHE_SIZE);
}
@Override
public String toString() {
if (cache.length() == 0) {
StringBuilder sbCache = cache;
if (sbCache.length() == 0) {
long off = getCodeOffset();
if (off < 0) {
cache.append(String.format("index: %-4d ", off));
sbCache.append(String.format("index: %-4d ", off));
} else {
cache.append(String.format("index: %04x ", off));
sbCache.append(String.format("index: %04x ", off));
}
if (clsSig == null) {
cache.append("clsID: ").append(frame.getClassID());
sbCache.append("clsID: ").append(frame.getClassID());
} else {
cache.append(clsSig).append("->");
sbCache.append(clsSig).append("->");
}
if (mthSig == null) {
cache.append(" mthID: ").append(frame.getMethodID());
sbCache.append(" mthID: ").append(frame.getMethodID());
} else {
cache.append(mthSig);
sbCache.append(mthSig);
}
}
return cache.toString();
return sbCache.toString();
}
}
@@ -1,23 +1,23 @@
package jadx.gui.device.debugger;
import io.github.hqktech.JDWP.Event.Composite.BreakpointEvent;
import io.github.hqktech.JDWP.Event.Composite.ClassPrepareEvent;
import io.github.hqktech.JDWP.Event.Composite.ClassUnloadEvent;
import io.github.hqktech.JDWP.Event.Composite.ExceptionEvent;
import io.github.hqktech.JDWP.Event.Composite.FieldAccessEvent;
import io.github.hqktech.JDWP.Event.Composite.FieldModificationEvent;
import io.github.hqktech.JDWP.Event.Composite.MethodEntryEvent;
import io.github.hqktech.JDWP.Event.Composite.MethodExitEvent;
import io.github.hqktech.JDWP.Event.Composite.MethodExitWithReturnValueEvent;
import io.github.hqktech.JDWP.Event.Composite.MonitorContendedEnterEvent;
import io.github.hqktech.JDWP.Event.Composite.MonitorContendedEnteredEvent;
import io.github.hqktech.JDWP.Event.Composite.MonitorWaitEvent;
import io.github.hqktech.JDWP.Event.Composite.MonitorWaitedEvent;
import io.github.hqktech.JDWP.Event.Composite.SingleStepEvent;
import io.github.hqktech.JDWP.Event.Composite.ThreadDeathEvent;
import io.github.hqktech.JDWP.Event.Composite.ThreadStartEvent;
import io.github.hqktech.JDWP.Event.Composite.VMDeathEvent;
import io.github.hqktech.JDWP.Event.Composite.VMStartEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.BreakpointEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.ClassPrepareEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.ClassUnloadEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.ExceptionEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.FieldAccessEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.FieldModificationEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.MethodEntryEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.MethodExitEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.MethodExitWithReturnValueEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.MonitorContendedEnterEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.MonitorContendedEnteredEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.MonitorWaitEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.MonitorWaitedEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.SingleStepEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.ThreadDeathEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.ThreadStartEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.VMDeathEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.VMStartEvent;
abstract class EventListenerAdapter {
void onVMStart(VMStartEvent event) {
@@ -0,0 +1,390 @@
package jadx.gui.device.debugger;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Timer;
import java.util.TimerTask;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.gui.device.protocol.ADBDevice;
import jadx.gui.ui.panel.LogcatPanel;
public class LogcatController {
private static final Logger LOG = LoggerFactory.getLogger(LogcatController.class);
private final ADBDevice adbDevice;
private final LogcatPanel logcatPanel;
private Timer timer;
private final String timezone;
private LogcatInfo recent = null;
private ArrayList<LogcatInfo> events = new ArrayList<>();
private LogcatFilter filter = new LogcatFilter(null, null);
private String status = "null";
public LogcatController(LogcatPanel logcatPanel, ADBDevice adbDevice) throws IOException {
this.adbDevice = adbDevice;
this.logcatPanel = logcatPanel;
this.timezone = adbDevice.getTimezone();
this.startLogcat();
}
public void startLogcat() {
timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
getLog();
}
}, 0, 1000);
this.status = "running";
}
public void stopLogcat() {
timer.cancel();
this.status = "stopped";
}
public String getStatus() {
return this.status;
}
public void clearLogcat() {
try {
adbDevice.clearLogcat();
clearEvents();
} catch (IOException e) {
LOG.error("Failed to clear Logcat", e);
}
}
private void getLog() {
if (!logcatPanel.isReady()) {
return;
}
try {
byte[] buf;
if (recent == null) {
buf = adbDevice.getBinaryLogcat();
} else {
buf = adbDevice.getBinaryLogcat(recent.getAfterTimestamp());
}
if (buf == null) {
return;
}
ByteBuffer in = ByteBuffer.wrap(buf);
in.order(ByteOrder.LITTLE_ENDIAN);
while (in.remaining() > 20) {
LogcatInfo eInfo = null;
byte[] msgBuf;
short eLen = in.getShort();
short eHdrLen = in.getShort();
if (eLen + eHdrLen > in.remaining()) {
return;
}
switch (eHdrLen) {
case 20: // header length 20 == version 1
eInfo = new LogcatInfo(eLen, eHdrLen, in.getInt(), in.getInt(), in.getInt(), in.getInt(), in.get());
msgBuf = new byte[eLen];
in.get(msgBuf, 0, eLen - 1);
eInfo.setMsg(msgBuf);
break;
case 24: // header length 24 == version 2 / 3
eInfo = new LogcatInfo(eLen, eHdrLen, in.getInt(), in.getInt(), in.getInt(), in.getInt(), in.getInt(), in.get());
msgBuf = new byte[eLen];
in.get(msgBuf, 0, eLen - 1);
eInfo.setMsg(msgBuf);
break;
case 28: // header length 28 == version 4
eInfo = new LogcatInfo(eLen, eHdrLen, in.getInt(), in.getInt(), in.getInt(), in.getInt(), in.getInt(), in.getInt(),
in.get());
msgBuf = new byte[eLen];
in.get(msgBuf, 0, eLen - 1);
eInfo.setMsg(msgBuf);
break;
default:
break;
}
if (eInfo == null) {
return;
}
if (recent == null) {
recent = eInfo;
} else if (recent.getInstant().isBefore(eInfo.getInstant())) {
recent = eInfo;
}
if (filter.doFilter(eInfo)) {
logcatPanel.log(eInfo);
}
events.add(eInfo);
}
} catch (Exception e) {
LOG.error("Failed to get logcat message", e);
}
}
public boolean reload() {
stopLogcat();
boolean ok = logcatPanel.clearLogcatArea();
if (ok) {
events.forEach((eInfo) -> {
if (filter.doFilter(eInfo)) {
logcatPanel.log(eInfo);
}
});
startLogcat();
}
return true;
}
public void clearEvents() {
this.recent = null;
this.events = new ArrayList<>();
}
public void exit() {
stopLogcat();
filter = new LogcatFilter(null, null);
recent = null;
}
public LogcatFilter getFilter() {
return this.filter;
}
public class LogcatFilter {
private final ArrayList<Integer> pid;
private ArrayList<Byte> msgType = new ArrayList<Byte>() {
{
add((byte) 1);
add((byte) 2);
add((byte) 3);
add((byte) 4);
add((byte) 5);
add((byte) 6);
add((byte) 7);
add((byte) 8);
}
};
public LogcatFilter(ArrayList<Integer> pid, ArrayList<Byte> msgType) {
if (pid != null) {
this.pid = pid;
} else {
this.pid = new ArrayList<>();
}
if (msgType != null) {
this.msgType = msgType;
}
}
public void addPid(int pid) {
if (!this.pid.contains(pid)) {
this.pid.add(pid);
}
}
public void removePid(int pid) {
int pidPos = this.pid.indexOf(pid);
if (pidPos >= 0) {
this.pid.remove(pidPos);
}
}
public void togglePid(int pid, boolean state) {
if (state) {
addPid(pid);
} else {
removePid(pid);
}
}
public void addMsgType(byte msgType) {
if (!this.msgType.contains(msgType)) {
this.msgType.add(msgType);
}
}
public void removeMsgType(byte msgType) {
int typePos = this.msgType.indexOf(msgType);
if (typePos >= 0) {
this.msgType.remove(typePos);
}
}
public void toggleMsgType(byte msgType, boolean state) {
if (state) {
addMsgType(msgType);
} else {
removeMsgType(msgType);
}
}
public boolean doFilter(LogcatInfo inInfo) {
if (pid.contains(inInfo.getPid())) {
return msgType.contains(inInfo.getMsgType());
}
return false;
}
public ArrayList<LogcatInfo> getFilteredList(ArrayList<LogcatInfo> inInfoList) {
ArrayList<LogcatInfo> outInfoList = new ArrayList<LogcatInfo>();
inInfoList.forEach((inInfo) -> {
if (doFilter(inInfo)) {
outInfoList.add(inInfo);
}
});
return outInfoList;
}
}
public class LogcatInfo {
private String msg;
private final byte msgType;
private final int nsec;
private final int pid;
private final int sec;
private final int tid;
private final short hdrSize;
private final short len;
private final short version;
private int lid;
private int uid;
public LogcatInfo(short len, short hdrSize, int pid, int tid, int sec, int nsec, byte msgType) {
this.hdrSize = hdrSize;
this.len = len;
this.msgType = msgType;
this.nsec = nsec;
this.pid = pid;
this.sec = sec;
this.tid = tid;
this.version = 1;
}
// Version 2 and 3 both have the same arguments
public LogcatInfo(short len, short hdrSize, int pid, int tid, int sec, int nsec, int lid, byte msgType) {
this.hdrSize = hdrSize;
this.len = len;
this.lid = lid;
this.msgType = msgType;
this.nsec = nsec;
this.pid = pid;
this.sec = sec;
this.tid = tid;
this.version = 3;
}
public LogcatInfo(short len, short hdrSize, int pid, int tid, int sec, int nsec, int lid, int uid, byte msgType) {
this.hdrSize = hdrSize;
this.len = len;
this.lid = lid;
this.msgType = msgType;
this.nsec = nsec;
this.pid = pid;
this.sec = sec;
this.tid = tid;
this.uid = uid;
this.version = 4;
}
public void setMsg(byte[] msg) {
this.msg = new String(msg);
}
public short getVersion() {
return this.version;
}
public short getLen() {
return this.len;
}
public short getHeaderLen() {
return this.hdrSize;
}
public int getPid() {
return this.pid;
}
public int getTid() {
return this.tid;
}
public int getSec() {
return this.sec;
}
public int getNSec() {
return this.nsec;
}
public int getLid() {
return this.lid;
}
public int getUid() {
return this.uid;
}
public Instant getInstant() {
return Instant.ofEpochSecond(getSec(), getNSec());
}
public String getTimestamp() {
DateTimeFormatter dtFormat = DateTimeFormatter.ofPattern("MM-dd HH:mm:ss.SSS").withZone(ZoneId.of(timezone));
return dtFormat.format(getInstant());
}
public String getAfterTimestamp() {
DateTimeFormatter dtFormat = DateTimeFormatter.ofPattern("MM-dd HH:mm:ss.SSS").withZone(ZoneId.of(timezone));
return dtFormat.format(getInstant().plusMillis(1));
}
public byte getMsgType() {
return this.msgType;
}
public String getMsgTypeString() {
switch (getMsgType()) {
case 0:
return "Unknown";
case 1:
return "Default";
case 2:
return "Verbose";
case 3:
return "Debug";
case 4:
return "Info";
case 5:
return "Warn";
case 6:
return "Error";
case 7:
return "Fatal";
case 8:
return "Silent";
default:
return "Unknown";
}
}
public String getMsg() {
return this.msg;
}
}
}
@@ -1,6 +1,6 @@
package jadx.gui.device.debugger;
import io.github.hqktech.JDWP;
import io.github.skylot.jdwp.JDWP;
public enum RuntimeType {
ARRAY(91, "[]"),
@@ -21,56 +21,56 @@ import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.github.hqktech.JDWP;
import io.github.hqktech.JDWP.ArrayReference.Length.LengthReplyData;
import io.github.hqktech.JDWP.ByteBuffer;
import io.github.hqktech.JDWP.Event.Composite.BreakpointEvent;
import io.github.hqktech.JDWP.Event.Composite.ClassPrepareEvent;
import io.github.hqktech.JDWP.Event.Composite.ClassUnloadEvent;
import io.github.hqktech.JDWP.Event.Composite.EventData;
import io.github.hqktech.JDWP.Event.Composite.ExceptionEvent;
import io.github.hqktech.JDWP.Event.Composite.FieldAccessEvent;
import io.github.hqktech.JDWP.Event.Composite.FieldModificationEvent;
import io.github.hqktech.JDWP.Event.Composite.MethodEntryEvent;
import io.github.hqktech.JDWP.Event.Composite.MethodExitEvent;
import io.github.hqktech.JDWP.Event.Composite.MethodExitWithReturnValueEvent;
import io.github.hqktech.JDWP.Event.Composite.MonitorContendedEnterEvent;
import io.github.hqktech.JDWP.Event.Composite.MonitorContendedEnteredEvent;
import io.github.hqktech.JDWP.Event.Composite.MonitorWaitEvent;
import io.github.hqktech.JDWP.Event.Composite.MonitorWaitedEvent;
import io.github.hqktech.JDWP.Event.Composite.SingleStepEvent;
import io.github.hqktech.JDWP.Event.Composite.ThreadDeathEvent;
import io.github.hqktech.JDWP.Event.Composite.ThreadStartEvent;
import io.github.hqktech.JDWP.Event.Composite.VMDeathEvent;
import io.github.hqktech.JDWP.Event.Composite.VMStartEvent;
import io.github.hqktech.JDWP.EventRequest.Set.ClassMatchRequest;
import io.github.hqktech.JDWP.EventRequest.Set.CountRequest;
import io.github.hqktech.JDWP.EventRequest.Set.LocationOnlyRequest;
import io.github.hqktech.JDWP.EventRequest.Set.StepRequest;
import io.github.hqktech.JDWP.Method.VariableTableWithGeneric.VarTableWithGenericData;
import io.github.hqktech.JDWP.Method.VariableTableWithGeneric.VarWithGenericSlot;
import io.github.hqktech.JDWP.ObjectReference;
import io.github.hqktech.JDWP.ObjectReference.ReferenceType.ReferenceTypeReplyData;
import io.github.hqktech.JDWP.ObjectReference.SetValues.FieldValueSetter;
import io.github.hqktech.JDWP.Packet;
import io.github.hqktech.JDWP.ReferenceType.FieldsWithGeneric.FieldsWithGenericData;
import io.github.hqktech.JDWP.ReferenceType.FieldsWithGeneric.FieldsWithGenericReplyData;
import io.github.hqktech.JDWP.ReferenceType.MethodsWithGeneric.MethodsWithGenericData;
import io.github.hqktech.JDWP.ReferenceType.MethodsWithGeneric.MethodsWithGenericReplyData;
import io.github.hqktech.JDWP.ReferenceType.Signature.SignatureReplyData;
import io.github.hqktech.JDWP.StackFrame.GetValues.GetValuesReplyData;
import io.github.hqktech.JDWP.StackFrame.GetValues.GetValuesSlots;
import io.github.hqktech.JDWP.StackFrame.SetValues.SlotValueSetter;
import io.github.hqktech.JDWP.StackFrame.ThisObject.ThisObjectReplyData;
import io.github.hqktech.JDWP.StringReference.Value.ValueReplyData;
import io.github.hqktech.JDWP.ThreadReference.Frames.FramesReplyData;
import io.github.hqktech.JDWP.ThreadReference.Frames.FramesReplyDataFrames;
import io.github.hqktech.JDWP.ThreadReference.Name.NameReplyData;
import io.github.hqktech.JDWP.VirtualMachine.AllClassesWithGeneric.AllClassesWithGenericData;
import io.github.hqktech.JDWP.VirtualMachine.AllClassesWithGeneric.AllClassesWithGenericReplyData;
import io.github.hqktech.JDWP.VirtualMachine.AllThreads.AllThreadsReplyData;
import io.github.hqktech.JDWP.VirtualMachine.AllThreads.AllThreadsReplyDataThreads;
import io.github.hqktech.JDWP.VirtualMachine.CreateString.CreateStringReplyData;
import io.github.skylot.jdwp.JDWP;
import io.github.skylot.jdwp.JDWP.ArrayReference.Length.LengthReplyData;
import io.github.skylot.jdwp.JDWP.ByteBuffer;
import io.github.skylot.jdwp.JDWP.Event.Composite.BreakpointEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.ClassPrepareEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.ClassUnloadEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.EventData;
import io.github.skylot.jdwp.JDWP.Event.Composite.ExceptionEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.FieldAccessEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.FieldModificationEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.MethodEntryEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.MethodExitEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.MethodExitWithReturnValueEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.MonitorContendedEnterEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.MonitorContendedEnteredEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.MonitorWaitEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.MonitorWaitedEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.SingleStepEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.ThreadDeathEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.ThreadStartEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.VMDeathEvent;
import io.github.skylot.jdwp.JDWP.Event.Composite.VMStartEvent;
import io.github.skylot.jdwp.JDWP.EventRequest.Set.ClassMatchRequest;
import io.github.skylot.jdwp.JDWP.EventRequest.Set.CountRequest;
import io.github.skylot.jdwp.JDWP.EventRequest.Set.LocationOnlyRequest;
import io.github.skylot.jdwp.JDWP.EventRequest.Set.StepRequest;
import io.github.skylot.jdwp.JDWP.Method.VariableTableWithGeneric.VarTableWithGenericData;
import io.github.skylot.jdwp.JDWP.Method.VariableTableWithGeneric.VarWithGenericSlot;
import io.github.skylot.jdwp.JDWP.ObjectReference;
import io.github.skylot.jdwp.JDWP.ObjectReference.ReferenceType.ReferenceTypeReplyData;
import io.github.skylot.jdwp.JDWP.ObjectReference.SetValues.FieldValueSetter;
import io.github.skylot.jdwp.JDWP.Packet;
import io.github.skylot.jdwp.JDWP.ReferenceType.FieldsWithGeneric.FieldsWithGenericData;
import io.github.skylot.jdwp.JDWP.ReferenceType.FieldsWithGeneric.FieldsWithGenericReplyData;
import io.github.skylot.jdwp.JDWP.ReferenceType.MethodsWithGeneric.MethodsWithGenericData;
import io.github.skylot.jdwp.JDWP.ReferenceType.MethodsWithGeneric.MethodsWithGenericReplyData;
import io.github.skylot.jdwp.JDWP.ReferenceType.Signature.SignatureReplyData;
import io.github.skylot.jdwp.JDWP.StackFrame.GetValues.GetValuesReplyData;
import io.github.skylot.jdwp.JDWP.StackFrame.GetValues.GetValuesSlots;
import io.github.skylot.jdwp.JDWP.StackFrame.SetValues.SlotValueSetter;
import io.github.skylot.jdwp.JDWP.StackFrame.ThisObject.ThisObjectReplyData;
import io.github.skylot.jdwp.JDWP.StringReference.Value.ValueReplyData;
import io.github.skylot.jdwp.JDWP.ThreadReference.Frames.FramesReplyData;
import io.github.skylot.jdwp.JDWP.ThreadReference.Frames.FramesReplyDataFrames;
import io.github.skylot.jdwp.JDWP.ThreadReference.Name.NameReplyData;
import io.github.skylot.jdwp.JDWP.VirtualMachine.AllClassesWithGeneric.AllClassesWithGenericData;
import io.github.skylot.jdwp.JDWP.VirtualMachine.AllClassesWithGeneric.AllClassesWithGenericReplyData;
import io.github.skylot.jdwp.JDWP.VirtualMachine.AllThreads.AllThreadsReplyData;
import io.github.skylot.jdwp.JDWP.VirtualMachine.AllThreads.AllThreadsReplyDataThreads;
import io.github.skylot.jdwp.JDWP.VirtualMachine.CreateString.CreateStringReplyData;
import io.reactivex.annotations.NonNull;
import jadx.api.plugins.input.data.AccessFlags;
@@ -103,7 +103,7 @@ public class ADB {
}
return result;
} catch (SocketException e) {
LOG.error("Aborting readServiceProtocol: socket closed");
LOG.warn("Aborting readServiceProtocol: {}", e.toString());
} catch (IOException e) {
LOG.error("Failed to read readServiceProtocol", e);
}
@@ -207,7 +207,7 @@ public class ADB {
List<ADBDeviceInfo> deviceInfoList = new ArrayList<>(deviceLines.length);
for (String deviceLine : deviceLines) {
if (!deviceLine.trim().isEmpty()) {
deviceInfoList.add(ADBDeviceInfo.make(deviceLine, host, port));
deviceInfoList.add(new ADBDeviceInfo(deviceLine, host, port));
}
}
listener.onDeviceStatusChange(deviceInfoList);
@@ -10,6 +10,8 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -25,7 +27,7 @@ public class ADBDevice {
private static final Logger LOG = LoggerFactory.getLogger(ADBDevice.class);
private static final String CMD_TRACK_JDWP = "000atrack-jdwp";
private static final Pattern TIMESTAMP_FORMAT = Pattern.compile("^[0-9]{2}\\-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\\.[0-9]{3}$");
ADBDeviceInfo info;
String androidReleaseVer;
volatile Socket jdwpListenerSock;
@@ -39,7 +41,10 @@ public class ADBDevice {
}
public boolean updateDeviceInfo(ADBDeviceInfo info) {
boolean matched = this.info.serial.equals(info.serial);
if (info.getSerial() == null || info.getSerial().isEmpty()) {
return false;
}
boolean matched = this.info.getSerial().equals(info.getSerial());
if (matched) {
this.info = info;
}
@@ -47,21 +52,21 @@ public class ADBDevice {
}
public String getSerial() {
return info.serial;
return info.getSerial();
}
public boolean removeForward(String localPort) throws IOException {
return ADB.removeForward(info.adbHost, info.adbPort, info.serial, localPort);
return ADB.removeForward(info.getAdbHost(), info.getAdbPort(), info.getSerial(), localPort);
}
public ForwardResult forwardJDWP(String localPort, String jdwpPid) throws IOException {
try (Socket socket = ADB.connect(info.adbHost, info.adbPort)) {
try (Socket socket = ADB.connect(info.getAdbHost(), info.getAdbPort())) {
String cmd = String.format("host:forward:tcp:%s;jdwp:%s", localPort, jdwpPid);
cmd = String.format("%04x%s", cmd.length(), cmd);
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
ForwardResult rst;
if (ADB.setSerial(info.serial, outputStream, inputStream)) {
if (ADB.setSerial(info.getSerial(), outputStream, inputStream)) {
outputStream.write(cmd.getBytes());
if (!ADB.isOkay(inputStream)) {
rst = new ForwardResult(1, ADB.readServiceProtocol(inputStream));
@@ -99,9 +104,9 @@ public class ADBDevice {
*/
public int launchApp(String fullAppName) throws IOException, InterruptedException {
byte[] res;
try (Socket socket = ADB.connect(info.adbHost, info.adbPort)) {
try (Socket socket = ADB.connect(info.getAdbHost(), info.getAdbPort())) {
String cmd = "am start -D -n " + fullAppName;
res = ADB.execShellCommandRaw(info.serial, cmd, socket.getOutputStream(), socket.getInputStream());
res = ADB.execShellCommandRaw(info.getSerial(), cmd, socket.getOutputStream(), socket.getInputStream());
if (res == null) {
return -1;
}
@@ -117,6 +122,49 @@ public class ADBDevice {
return -1;
}
/**
* @Return binary output of logcat
*/
public byte[] getBinaryLogcat() throws IOException {
Socket socket = ADB.connect(info.getAdbHost(), info.getAdbPort());
String cmd = "logcat -dB";
return ADB.execShellCommandRaw(info.getSerial(), cmd, socket.getOutputStream(), socket.getInputStream());
}
/**
* @Return binary output of logcat after provided timestamp
* Timestamp is in the format 09-08 02:18:03.131
*/
public byte[] getBinaryLogcat(String timestamp) throws IOException {
Socket socket = ADB.connect(info.getAdbHost(), info.getAdbPort());
Matcher matcher = TIMESTAMP_FORMAT.matcher(timestamp);
if (!matcher.find()) {
LOG.error("Invalid Logcat Timestamp " + timestamp);
}
String cmd = "logcat -dB -t \"" + timestamp + "\"";
return ADB.execShellCommandRaw(info.getSerial(), cmd, socket.getOutputStream(), socket.getInputStream());
}
/**
* @Return binary output of logcat -c
*/
public void clearLogcat() throws IOException {
Socket socket = ADB.connect(info.getAdbHost(), info.getAdbPort());
String cmd = "logcat -c";
ADB.execShellCommandRaw(info.getSerial(), cmd, socket.getOutputStream(), socket.getInputStream());
}
/**
* @return Timezone for the attached android device
*/
public String getTimezone() throws IOException {
Socket socket = ADB.connect(info.getAdbHost(), info.getAdbPort());
String cmd = "getprop persist.sys.timezone";
byte[] tz = ADB.execShellCommandRaw(info.getSerial(), cmd, socket.getOutputStream(), socket.getInputStream());
return new String(tz).trim();
}
public String getAndroidReleaseVersion() {
if (!StringUtils.isEmpty(androidReleaseVer)) {
return androidReleaseVer;
@@ -134,13 +182,13 @@ public class ADBDevice {
}
public List<String> getProp(String entry) throws IOException {
try (Socket socket = ADB.connect(info.adbHost, info.adbPort)) {
try (Socket socket = ADB.connect(info.getAdbHost(), info.getAdbPort())) {
List<String> props = Collections.emptyList();
String cmd = "getprop";
if (!StringUtils.isEmpty(entry)) {
cmd += " " + entry;
}
byte[] payload = ADB.execShellCommandRaw(info.serial, cmd,
byte[] payload = ADB.execShellCommandRaw(info.getSerial(), cmd,
socket.getOutputStream(), socket.getInputStream());
if (payload != null) {
props = new ArrayList<>();
@@ -157,18 +205,18 @@ public class ADBDevice {
}
public List<Process> getProcessByPkg(String pkg) throws IOException {
return getProcessList("ps | grep " + pkg, 0);
return getProcessList("ps | grep " + pkg);
}
@NonNull
public List<Process> getProcessList() throws IOException {
return getProcessList("ps", 1);
return getProcessList("ps");
}
private List<Process> getProcessList(String cmd, int index) throws IOException {
try (Socket socket = ADB.connect(info.adbHost, info.adbPort)) {
private List<Process> getProcessList(String cmd) throws IOException {
try (Socket socket = ADB.connect(info.getAdbHost(), info.getAdbPort())) {
List<Process> procs = new ArrayList<>();
byte[] payload = ADB.execShellCommandRaw(info.serial, cmd,
byte[] payload = ADB.execShellCommandRaw(info.getSerial(), cmd,
socket.getOutputStream(), socket.getInputStream());
if (payload != null) {
String ps = new String(payload);
@@ -194,10 +242,10 @@ public class ADBDevice {
if (this.jdwpListenerSock != null) {
return false;
}
jdwpListenerSock = ADB.connect(this.info.adbHost, this.info.adbPort);
jdwpListenerSock = ADB.connect(this.info.getAdbHost(), this.info.getAdbPort());
InputStream inputStream = jdwpListenerSock.getInputStream();
OutputStream outputStream = jdwpListenerSock.getOutputStream();
if (ADB.setSerial(info.serial, outputStream, inputStream)
if (ADB.setSerial(info.getSerial(), outputStream, inputStream)
&& ADB.execCommandAsync(outputStream, inputStream, CMD_TRACK_JDWP)) {
Executors.newFixedThreadPool(1).execute(() -> {
for (;;) {
@@ -244,19 +292,20 @@ public class ADBDevice {
@Override
public int hashCode() {
return info.serial.hashCode();
return info.getSerial().hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof ADBDevice) {
return ((ADBDevice) obj).getDeviceInfo().serial.equals(info.serial);
String otherSerial = ((ADBDevice) obj).getDeviceInfo().getSerial();
return otherSerial.equals(info.getSerial());
}
return false;
}
@Override
public String toString() {
return info.allInfo;
return info.getAllInfo();
}
}
@@ -1,42 +1,91 @@
package jadx.gui.device.protocol;
import java.util.Map;
import java.util.TreeMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.utils.log.LogUtils;
public class ADBDeviceInfo {
public String adbHost;
public int adbPort;
public String serial;
public String state;
public String model;
public String allInfo;
private static final Logger LOG = LoggerFactory.getLogger(ADBDeviceInfo.class);
private final String adbHost;
private final int adbPort;
private final String serial;
private final String state;
private final String model;
private final String allInfo;
/**
* Store the device info property values like "device" "model" "product" or "transport_id"
*/
private final Map<String, String> propertiesMap = new TreeMap<>();
ADBDeviceInfo(String info, String host, int port) {
String[] infoFields = info.trim().split("\\s+");
allInfo = String.join(" ", infoFields);
if (infoFields.length > 2) {
serial = infoFields[0];
state = infoFields[1];
for (int i = 2; i < infoFields.length; i++) {
String field = infoFields[i];
int idx = field.indexOf(':');
if (idx > 0) {
String key = field.substring(0, idx);
String value = field.substring(idx + 1);
if (!value.isEmpty()) {
propertiesMap.put(key, value);
}
}
}
model = propertiesMap.getOrDefault("model", serial);
} else {
LOG.error("Unable to extract device information from {}", LogUtils.escape(info));
serial = "";
state = "unknown";
model = "unknown";
}
adbHost = host;
adbPort = port;
}
public boolean isOnline() {
return state.equals("device");
}
public String getAdbHost() {
return adbHost;
}
public int getAdbPort() {
return adbPort;
}
public String getSerial() {
return serial;
}
public String getState() {
return state;
}
public String getModel() {
return model;
}
public String getAllInfo() {
return allInfo;
}
public String getProperty(String key) {
return this.propertiesMap.get(key);
}
@Override
public String toString() {
return allInfo;
}
static ADBDeviceInfo make(String info, String host, int port) {
ADBDeviceInfo deviceInfo = new ADBDeviceInfo();
String[] infoFields = info.trim().split("\\s+");
deviceInfo.allInfo = String.join(" ", infoFields);
if (infoFields.length > 2) {
deviceInfo.serial = infoFields[0];
deviceInfo.state = infoFields[1];
}
int pos = info.indexOf("model:");
if (pos != -1) {
int spacePos = info.indexOf(" ", pos);
if (spacePos != -1) {
deviceInfo.model = info.substring(pos + "model:".length(), spacePos);
}
}
if (deviceInfo.model == null || deviceInfo.model.equals("")) {
deviceInfo.model = deviceInfo.serial;
}
deviceInfo.adbHost = host;
deviceInfo.adbPort = port;
return deviceInfo;
}
}
@@ -145,8 +145,8 @@ public class BackgroundExecutor {
task.onDone(this);
// treat UI task operations as part of the task to not mix with others
UiUtils.uiRunAndWait(() -> {
task.onFinish(this);
progressPane.setVisible(false);
task.onFinish(this);
});
} finally {
taskComplete(id);
@@ -190,13 +190,21 @@ public class BackgroundExecutor {
performCancel(executor);
return cancelStatus;
}
updateProgress(executor);
k++;
Thread.sleep(k < 10 ? 200 : 1000); // faster update for short tasks
if (jobsCount == 1 && k == 3) {
if (k < 10) {
// faster update for short tasks
Thread.sleep(200);
if (k == 5) {
updateProgress(executor);
}
} else {
updateProgress(executor);
Thread.sleep(1000);
}
if (jobsCount == 1 && k == 5) {
// small delay before show progress to reduce blinking on short tasks
progressPane.changeVisibility(this, true);
}
k++;
}
} catch (InterruptedException e) {
LOG.debug("Task wait interrupted");
@@ -210,7 +218,7 @@ public class BackgroundExecutor {
}
private void updateProgress(ThreadPoolExecutor executor) {
Consumer<ITaskProgress> onProgressListener = task.getOnProgressListener();
Consumer<ITaskProgress> onProgressListener = task.getProgressListener();
ITaskProgress taskProgress = task.getTaskProgress();
if (taskProgress == null) {
setProgress(calcProgress(executor.getCompletedTaskCount(), jobsCount));
@@ -231,13 +239,16 @@ public class BackgroundExecutor {
// force termination
task.cancel();
executor.shutdown();
if (executor.awaitTermination(2, TimeUnit.SECONDS)) {
LOG.debug("Task cancel complete");
return;
int cancelTimeout = task.getCancelTimeoutMS();
if (cancelTimeout != 0) {
if (executor.awaitTermination(cancelTimeout, TimeUnit.MILLISECONDS)) {
LOG.debug("Task cancel complete");
return;
}
}
LOG.debug("Forcing tasks cancel");
executor.shutdownNow();
boolean complete = executor.awaitTermination(5, TimeUnit.SECONDS);
boolean complete = executor.awaitTermination(task.getShutdownTimeoutMS(), TimeUnit.MILLISECONDS);
LOG.debug("Forced task cancel status: {}",
complete ? "success" : "fail, still active: " + executor.getActiveCount());
}
@@ -301,5 +312,13 @@ public class BackgroundExecutor {
public long getTime() {
return time;
}
@Override
public String toString() {
return "TaskWorker{status=" + status
+ ", jobsCount=" + jobsCount
+ ", jobsComplete=" + jobsComplete
+ ", time=" + time + '}';
}
}
}

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