Compare commits

...

58 Commits

Author SHA1 Message Date
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
Skylot 87e0e5bf16 fix: correct inline/merge with overriden bridge method (#1580) 2022-07-20 14:49:37 +01:00
Skylot e4c2d6cf6e fix(gui): use editor font for usage label 2022-07-20 14:36:51 +01:00
Skylot fb0bdb5112 fix(gui): allow to use empty name to reset rename 2022-07-20 14:35:53 +01:00
Skylot f4b3645435 fix: ignore anonymous classes in enclosing node search (#1580) 2022-07-19 19:25:17 +01:00
Skylot c27f2badf7 fix(gui): resolve payload offset for switch insns in debug smali code (#1575) 2022-07-18 18:50:48 +01:00
Skylot 1a877d6535 fix: resolve possible decompilation double execution 2022-07-16 22:29:59 +03:00
Skylot 5ada9331b6 chore: update dependencies 2022-07-14 14:33:04 +01:00
Skylot a0f4ccb7a4 fix: update deps and fix proto resource loading (AAB) (#1129) 2022-07-14 14:33:04 +01:00
Skylot 5b5524a7dd fix: handle parent of inlined/moved classes (#1578) 2022-07-14 14:40:13 +03:00
Jan S 3cc464c9c9 fix: IndexOutOfBoundsException in JumpManager (#1576) (PR #1577) 2022-07-13 17:24:20 +01:00
Skylot 51555667cf fix: add more checks before remove or rename enum methods (#1572) 2022-07-07 16:55:32 +01:00
Skylot e01ea7010f fix: save classes with code generation error into cache (#1568) 2022-07-03 19:32:41 +01:00
Skylot 77732c83c9 fix(gui): ignore/limit waiting of canceled search task (#1568) 2022-07-01 17:57:59 +01:00
Skylot a67fc83949 fix: better dominators algorithms 2022-07-01 17:26:54 +01:00
Skylot 3d920725aa fix: check synthetic methods before remove/inline (#1560) 2022-06-29 19:19:54 +01:00
Skylot 2f2fbea558 fix(gui): check user renames (#1557) 2022-06-29 16:21:49 +01:00
Skylot e7a86a2960 fix(gui): forbid rename method args in fallback mode (#1558) 2022-06-29 15:25:23 +01:00
Skylot b282d97ffe fix(gui): set current dir directly in file chooser constructor (#1553) 2022-06-28 16:57:57 +01:00
Skylot e4ca52a95f chore: update dependencies 2022-06-28 16:23:31 +01:00
Skylot d972d9ec74 fix(gui): ignore errors on code area dispose (#1545) 2022-06-28 16:20:31 +01:00
Jan S 0721a6b050 fix(gui): QuarkReport data validation added and other minor improvements (PR #1556)
* QuarkReport: data validation added and other minor improvements
* checkStyle
2022-06-25 22:24:53 +03:00
zhongqingsong 762ee6550e fix(gui):complete Chinese translation (PR #1549)
1、Complete Chinese translation
2、Previous word polish
2022-06-25 22:15:22 +03:00
154 changed files with 4573 additions and 1186 deletions
+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),
+5 -5
View File
@@ -1,6 +1,6 @@
plugins {
id 'com.github.ben-manes.versions' version '0.42.0'
id 'com.diffplug.spotless' version '6.6.1'
id 'com.diffplug.spotless' version '6.9.1'
}
ext.jadxVersion = System.getenv('JADX_VERSION') ?: "dev"
@@ -31,11 +31,11 @@ allprojects {
testImplementation 'ch.qos.logback:logback-classic:1.2.11'
testImplementation 'org.hamcrest:hamcrest-library:2.2'
testImplementation 'org.mockito:mockito-core:4.6.0'
testImplementation 'org.assertj:assertj-core:3.22.0'
testImplementation 'org.mockito:mockito-core:4.7.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.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0'
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
@@ -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) {
+6 -7
View File
@@ -5,12 +5,11 @@ plugins {
dependencies {
api(project(':jadx-plugins:jadx-plugins-api'))
implementation 'com.google.code.gson:gson:2.9.0'
implementation 'com.android.tools.build:aapt2-proto:4.2.1-7147631'
constraints {
// Force protobuf version to prevent Java-7 issue
implementation 'com.google.protobuf:protobuf-java:3.11.4'
}
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.2-7984345'
implementation 'com.google.protobuf:protobuf-java:3.21.5' // forcing latest version
testImplementation 'org.apache.commons:commons-lang3:3.12.0'
@@ -20,7 +19,7 @@ dependencies {
testRuntimeOnly(project(':jadx-plugins:jadx-java-input'))
testRuntimeOnly(project(':jadx-plugins:jadx-raung-input'))
testImplementation 'org.eclipse.jdt:ecj:3.29.0'
testImplementation 'org.eclipse.jdt:ecj:3.30.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
@@ -37,8 +37,6 @@ import jadx.api.plugins.input.data.ILoadResult;
import jadx.api.plugins.options.JadxPluginOptions;
import jadx.core.Jadx;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.InlinedAttr;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.MethodNode;
@@ -358,8 +356,9 @@ public final class JadxDecompiler implements Closeable {
tasks.add(() -> {
for (JavaClass cls : decompileBatch) {
try {
ICodeInfo code = cls.getCodeInfo();
SaveCode.save(outDir, cls.getClassNode(), code);
ClassNode clsNode = cls.getClassNode();
ICodeInfo code = clsNode.getCode();
SaveCode.save(outDir, clsNode, code);
} catch (Exception e) {
LOG.error("Error saving class: {}", cls, e);
}
@@ -496,25 +495,11 @@ public final class JadxDecompiler implements Closeable {
@ApiStatus.Internal
JavaMethod convertMethodNode(MethodNode method) {
return methodsMap.computeIfAbsent(method, mthNode -> {
ClassNode codeCls = getCodeParentClass(mthNode.getParentClass());
return new JavaMethod(convertClassNode(codeCls), mthNode);
ClassNode parentCls = mthNode.getParentClass();
return new JavaMethod(convertClassNode(parentCls), mthNode);
});
}
private static ClassNode getCodeParentClass(ClassNode cls) {
ClassNode codeCls;
InlinedAttr inlinedAttr = cls.get(AType.INLINED);
if (inlinedAttr != null) {
codeCls = inlinedAttr.getInlineCls().getTopParentClass();
} else {
codeCls = cls.getTopParentClass();
}
if (codeCls == cls) {
return codeCls;
}
return getCodeParentClass(codeCls);
}
@Nullable
public JavaClass searchJavaClassByOrigFullName(String fullName) {
return getRoot().getClasses().stream()
+47 -19
View File
@@ -18,6 +18,7 @@ import jadx.api.metadata.ICodeNodeRef;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.AnonymousClassAttr;
import jadx.core.dex.attributes.nodes.InlinedAttr;
import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
@@ -57,7 +58,10 @@ public final class JavaClass implements JavaNode {
}
public @NotNull ICodeInfo getCodeInfo() {
load();
ICodeInfo code = load();
if (code != null) {
return code;
}
return cls.decompile();
}
@@ -106,19 +110,24 @@ public final class JavaClass implements JavaNode {
/**
* Decompile class and loads internal lists of fields, methods, etc.
* Do nothing if already loaded.
*
* @return code info if decompilation was executed, null otherwise
*/
@Nullable
private synchronized void load() {
private synchronized @Nullable ICodeInfo load() {
if (listsLoaded) {
return;
return null;
}
listsLoaded = true;
JadxDecompiler rootDecompiler = getRootDecompiler();
ICodeCache codeCache = rootDecompiler.getArgs().getCodeCache();
if (!codeCache.contains(cls.getRawName())) {
cls.decompile();
ICodeInfo code;
if (cls.getState().isProcessComplete()) {
// already decompiled -> class internals loaded
code = null;
} else {
code = cls.decompile();
}
JadxDecompiler rootDecompiler = getRootDecompiler();
int inClsCount = cls.getInnerClasses().size();
if (inClsCount != 0) {
List<JavaClass> list = new ArrayList<>(inClsCount);
@@ -164,6 +173,7 @@ public final class JavaClass implements JavaNode {
mths.sort(Comparator.comparing(JavaMethod::getName));
this.methods = Collections.unmodifiableList(mths);
}
return code;
}
JadxDecompiler getRootDecompiler() {
@@ -242,19 +252,37 @@ public final class JavaClass implements JavaNode {
return parent;
}
@Override
public JavaClass getTopParentClass() {
if (cls.contains(AType.ANONYMOUS_CLASS)) {
// moved to usage class
return getParentForAnonymousClass();
}
return parent == null ? this : parent.getTopParentClass();
public JavaClass getOriginalTopParentClass() {
return parent == null ? this : parent.getOriginalTopParentClass();
}
private JavaClass getParentForAnonymousClass() {
AnonymousClassAttr attr = cls.get(AType.ANONYMOUS_CLASS);
ClassNode topParentClass = attr.getOuterCls().getTopParentClass();
return getRootDecompiler().convertClassNode(topParentClass);
/**
* Return top parent class which contains code of this class.
* Code parent can be different from original parent after move or inline
*
* @return this if already a top class
*/
@Override
public JavaClass getTopParentClass() {
JavaClass codeParent = getCodeParent();
return codeParent == null ? this : codeParent.getTopParentClass();
}
/**
* Return parent class which contains code of this class.
* Code parent can be different for original parent after move or inline
*/
public @Nullable JavaClass getCodeParent() {
AnonymousClassAttr anonymousClsAttr = cls.get(AType.ANONYMOUS_CLASS);
if (anonymousClsAttr != null) {
// moved to usage class
return getRootDecompiler().convertClassNode(anonymousClsAttr.getOuterCls());
}
InlinedAttr inlinedAttr = cls.get(AType.INLINED);
if (inlinedAttr != null) {
return getRootDecompiler().convertClassNode(inlinedAttr.getInlineCls());
}
return parent;
}
public AccessInfo getAccessInfo() {
@@ -4,6 +4,7 @@ import java.util.Collections;
import java.util.List;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import jadx.api.metadata.ICodeAnnotation;
import jadx.api.metadata.annotations.VarNode;
@@ -32,7 +33,7 @@ public class JavaVariable implements JavaNode {
}
@Override
public String getName() {
public @Nullable String getName() {
return varNode.getName();
}
@@ -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,
}
@@ -9,7 +9,8 @@ public interface ICodeAnnotation {
VAR,
VAR_REF,
DECLARATION,
OFFSET
OFFSET,
END // class or method body end
}
AnnType getAnnType();
@@ -32,6 +32,22 @@ public class NodeDeclareRef implements ICodeAnnotation {
return AnnType.DECLARATION;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof NodeDeclareRef)) {
return false;
}
return node.equals(((NodeDeclareRef) o).node);
}
@Override
public int hashCode() {
return node.hashCode();
}
@Override
public String toString() {
return "NodeDeclareRef{" + node + '}';
@@ -0,0 +1,21 @@
package jadx.api.metadata.annotations;
import jadx.api.metadata.ICodeAnnotation;
public class NodeEnd implements ICodeAnnotation {
public static final NodeEnd VALUE = new NodeEnd();
private NodeEnd() {
}
@Override
public AnnType getAnnType() {
return AnnType.END;
}
@Override
public String toString() {
return "END";
}
}
@@ -6,16 +6,14 @@ import java.util.Map;
import java.util.NavigableMap;
import java.util.TreeMap;
import java.util.function.BiFunction;
import java.util.stream.Stream;
import org.jetbrains.annotations.Nullable;
import jadx.api.metadata.ICodeAnnotation;
import jadx.api.metadata.ICodeAnnotation.AnnType;
import jadx.api.metadata.ICodeMetadata;
import jadx.api.metadata.ICodeNodeRef;
import jadx.api.metadata.annotations.NodeDeclareRef;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.Utils;
public class CodeMetadataStorage implements ICodeMetadata {
@@ -55,7 +53,7 @@ public class CodeMetadataStorage implements ICodeMetadata {
}
@Override
public @Nullable ICodeAnnotation searchUp(int position, ICodeAnnotation.AnnType annType) {
public @Nullable ICodeAnnotation searchUp(int position, AnnType annType) {
for (ICodeAnnotation v : navMap.tailMap(position, true).values()) {
if (v.getAnnType() == annType) {
return v;
@@ -65,7 +63,7 @@ public class CodeMetadataStorage implements ICodeMetadata {
}
@Override
public @Nullable ICodeAnnotation searchUp(int position, int limitPos, ICodeAnnotation.AnnType annType) {
public @Nullable ICodeAnnotation searchUp(int position, int limitPos, AnnType annType) {
for (ICodeAnnotation v : navMap.subMap(position, true, limitPos, true).values()) {
if (v.getAnnType() == annType) {
return v;
@@ -99,28 +97,40 @@ public class CodeMetadataStorage implements ICodeMetadata {
@Override
public ICodeNodeRef getNodeAt(int position) {
return navMap.tailMap(position, true)
.values().stream()
.flatMap(CodeMetadataStorage::mapEnclosingNode)
.findFirst().orElse(null);
int nesting = 0;
for (ICodeAnnotation ann : navMap.tailMap(position, true).values()) {
switch (ann.getAnnType()) {
case END:
nesting++;
break;
case DECLARATION:
ICodeNodeRef node = ((NodeDeclareRef) ann).getNode();
AnnType nodeType = node.getAnnType();
if (nodeType == AnnType.CLASS || nodeType == AnnType.METHOD) {
if (nesting == 0) {
return node;
}
nesting--;
}
break;
}
}
return null;
}
@Override
public ICodeNodeRef getNodeBelow(int position) {
return navMap.headMap(position, true).descendingMap()
.values().stream()
.flatMap(CodeMetadataStorage::mapEnclosingNode)
.findFirst().orElse(null);
}
private static Stream<ICodeNodeRef> mapEnclosingNode(ICodeAnnotation ann) {
if (ann instanceof NodeDeclareRef) {
ICodeNodeRef node = ((NodeDeclareRef) ann).getNode();
if (node instanceof ClassNode || node instanceof MethodNode) {
return Stream.of(node);
for (ICodeAnnotation ann : navMap.headMap(position, true).descendingMap().values()) {
if (ann.getAnnType() == AnnType.DECLARATION) {
ICodeNodeRef node = ((NodeDeclareRef) ann).getNode();
AnnType nodeType = node.getAnnType();
if (nodeType == AnnType.CLASS || nodeType == AnnType.METHOD) {
return node;
}
}
}
return Stream.empty();
return null;
}
@Override
@@ -135,7 +145,7 @@ public class CodeMetadataStorage implements ICodeMetadata {
@Override
public String toString() {
return "CodeMetadata{lines=" + lines
+ ", annotations=\n" + Utils.listToString(navMap.entrySet(), "\n") + "\n}";
return "CodeMetadata{\nlines=" + lines
+ "\nannotations=\n " + Utils.listToString(navMap.descendingMap().entrySet(), "\n ") + "\n}";
}
}
@@ -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());
}
@@ -19,6 +19,7 @@ import jadx.api.CommentsLevel;
import jadx.api.ICodeInfo;
import jadx.api.ICodeWriter;
import jadx.api.JadxArgs;
import jadx.api.metadata.annotations.NodeEnd;
import jadx.api.plugins.input.data.AccessFlags;
import jadx.api.plugins.input.data.annotations.EncodedType;
import jadx.api.plugins.input.data.annotations.EncodedValue;
@@ -256,6 +257,7 @@ public class ClassGen {
addInnerClsAndMethods(clsCode);
clsCode.decIndent();
clsCode.startLine('}');
clsCode.attachAnnotation(NodeEnd.VALUE);
}
private void addInnerClsAndMethods(ICodeWriter clsCode) {
@@ -369,6 +371,7 @@ public class ClassGen {
mthGen.addInstructions(code);
code.decIndent();
code.startLine('}');
code.attachAnnotation(NodeEnd.VALUE);
}
}
@@ -761,6 +761,7 @@ public class InsnGen {
ctor.add(AFlag.DONT_GENERATE);
}
}
code.attachDefinition(cls);
code.add("new ");
useClass(code, parent);
MethodNode callMth = mth.root().resolveMethod(insn.getCallMth());
@@ -823,9 +824,15 @@ public class InsnGen {
}
if (callMthNode != null) {
code.attachAnnotation(callMthNode);
code.add(callMthNode.getAlias());
}
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);
}
@@ -26,6 +26,7 @@ import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.JadxError;
import jadx.core.dex.attributes.nodes.JumpInfo;
import jadx.core.dex.attributes.nodes.MethodOverrideAttr;
import jadx.core.dex.attributes.nodes.MethodReplaceAttr;
import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.instructions.ConstStringNode;
import jadx.core.dex.instructions.IfNode;
@@ -144,8 +145,9 @@ public class MethodGen {
} else {
classGen.useType(code, mth.getReturnType());
code.add(' ');
code.attachDefinition(mth);
code.add(mth.getAlias());
MethodNode defMth = getMethodForDefinition();
code.attachDefinition(defMth);
code.add(defMth.getAlias());
}
code.add('(');
@@ -178,6 +180,14 @@ public class MethodGen {
return true;
}
private MethodNode getMethodForDefinition() {
MethodReplaceAttr replaceAttr = mth.get(AType.METHOD_REPLACE);
if (replaceAttr != null) {
return replaceAttr.getReplaceMth();
}
return mth;
}
private void addOverrideAnnotation(ICodeWriter code, MethodNode mth) {
MethodOverrideAttr overrideAttr = mth.get(AType.METHOD_OVERRIDE);
if (overrideAttr == null) {
@@ -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";
}
@@ -25,6 +25,8 @@ public enum AFlag {
HIDDEN, // instruction used inside other instruction but not listed in args
DONT_RENAME, // do not rename during deobfuscation
FORCE_RAW_NAME, // force use of raw name instead alias
ADDED_TO_REGION,
EXC_TOP_SPLITTER,
@@ -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;
@@ -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);
}
}
@@ -21,6 +21,7 @@ import jadx.api.ICodeCache;
import jadx.api.ICodeInfo;
import jadx.api.ICodeWriter;
import jadx.api.JadxArgs;
import jadx.api.impl.SimpleCodeInfo;
import jadx.api.plugins.input.data.IClassData;
import jadx.api.plugins.input.data.IFieldData;
import jadx.api.plugins.input.data.IMethodData;
@@ -378,7 +379,13 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
return code;
}
}
ICodeInfo codeInfo = root.getProcessClasses().generateCode(this);
ICodeInfo codeInfo;
try {
codeInfo = root.getProcessClasses().generateCode(this);
} catch (Throwable e) {
addError("Code generation failed", e);
codeInfo = new SimpleCodeInfo(Utils.getStackTrace(e));
}
if (codeInfo != ICodeInfo.EMPTY) {
codeCache.add(clsRawName, codeInfo);
}
@@ -462,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);
}
@@ -650,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;
}
@@ -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;
@@ -316,6 +317,19 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
return blocks;
}
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() {
return enterBlock;
}
@@ -42,7 +42,8 @@ import jadx.core.utils.StringUtils;
import jadx.core.utils.Utils;
import jadx.core.utils.android.AndroidResourcesUtils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.xmlgen.ResTableParser;
import jadx.core.xmlgen.IResParser;
import jadx.core.xmlgen.ResDecoder;
import jadx.core.xmlgen.ResourceStorage;
import jadx.core.xmlgen.entry.ResourceEntry;
import jadx.core.xmlgen.entry.ValuesParser;
@@ -163,23 +164,13 @@ public class RootNode {
}
public void loadResources(List<ResourceFile> resources) {
ResourceFile arsc = null;
for (ResourceFile rf : resources) {
if (rf.getType() == ResourceType.ARSC) {
arsc = rf;
break;
}
}
ResourceFile arsc = getResourceFile(resources);
if (arsc == null) {
LOG.debug("'.arsc' file not found");
return;
}
try {
ResTableParser parser = ResourcesLoader.decodeStream(arsc, (size, is) -> {
ResTableParser tableParser = new ResTableParser(this);
tableParser.decode(is);
return tableParser;
});
IResParser parser = ResourcesLoader.decodeStream(arsc, (size, is) -> ResDecoder.decode(this, arsc, is));
if (parser != null) {
processResources(parser.getResStorage());
updateObfuscatedFiles(parser, resources);
@@ -189,6 +180,15 @@ public class RootNode {
}
}
private @Nullable ResourceFile getResourceFile(List<ResourceFile> resources) {
for (ResourceFile rf : resources) {
if (rf.getType() == ResourceType.ARSC) {
return rf;
}
}
return null;
}
public void processResources(ResourceStorage resStorage) {
constValues.setResourcesNames(resStorage.getResourcesNames());
appPackage = resStorage.getAppPackage();
@@ -209,7 +209,7 @@ public class RootNode {
}
}
private void updateObfuscatedFiles(ResTableParser parser, List<ResourceFile> resources) {
private void updateObfuscatedFiles(IResParser parser, List<ResourceFile> resources) {
if (args.isSkipResources()) {
return;
}
@@ -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;
}
@@ -10,6 +10,7 @@ import java.util.Objects;
import jadx.api.plugins.input.data.AccessFlags;
import jadx.core.Consts;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.FieldReplaceAttr;
import jadx.core.dex.attributes.nodes.MethodReplaceAttr;
import jadx.core.dex.attributes.nodes.SkipMethodArgsAttr;
@@ -225,7 +226,7 @@ public class ClassModifier extends AbstractVisitor {
}
private static boolean removeBridgeMethod(ClassNode cls, MethodNode mth) {
if (cls.root().getArgs().isRenameValid()) {
if (cls.root().getArgs().isInlineMethods()) { // simple wrapper remove is same as inline
List<InsnNode> allInsns = BlockUtils.collectAllInsns(mth.getBasicBlocks());
if (allInsns.size() == 1) {
InsnNode wrappedInsn = allInsns.get(0);
@@ -235,12 +236,10 @@ public class ClassModifier extends AbstractVisitor {
wrappedInsn = ((InsnWrapArg) arg).getWrapInsn();
}
}
if (checkSyntheticWrapper(mth, wrappedInsn)) {
return true;
}
return checkSyntheticWrapper(mth, wrappedInsn);
}
}
return !isMethodUnique(cls, mth);
return false;
}
private static boolean checkSyntheticWrapper(MethodNode mth, InsnNode insn) {
@@ -283,6 +282,9 @@ public class ClassModifier extends AbstractVisitor {
if (!Objects.equals(wrappedMth.getAlias(), alias)) {
wrappedMth.getMethodInfo().setAlias(alias);
}
wrappedMth.addAttr(new MethodReplaceAttr(mth));
wrappedMth.copyAttributeFrom(mth, AType.METHOD_OVERRIDE);
wrappedMth.addDebugComment("Method merged with bridge method");
return true;
}
@@ -299,20 +301,6 @@ public class ClassModifier extends AbstractVisitor {
return false;
}
private static boolean isMethodUnique(ClassNode cls, MethodNode mth) {
MethodInfo mi = mth.getMethodInfo();
for (MethodNode otherMth : cls.getMethods()) {
if (otherMth != mth) {
MethodInfo omi = otherMth.getMethodInfo();
if (omi.getName().equals(mi.getName())
&& Objects.equals(omi.getArgumentsTypes(), mi.getArgumentsTypes())) {
return false;
}
}
}
return true;
}
/**
* Remove public empty constructors (static or default)
*/
@@ -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 {
@@ -18,6 +18,7 @@ import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.EnumClassAttr;
import jadx.core.dex.attributes.nodes.EnumClassAttr.EnumField;
import jadx.core.dex.attributes.nodes.RenameReasonAttr;
import jadx.core.dex.attributes.nodes.SkipMethodArgsAttr;
import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.info.ClassInfo;
@@ -26,6 +27,7 @@ import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.instructions.IndexInsnNode;
import jadx.core.dex.instructions.InsnType;
import jadx.core.dex.instructions.InvokeNode;
import jadx.core.dex.instructions.InvokeType;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.InsnWrapArg;
@@ -59,6 +61,7 @@ import static jadx.core.utils.InsnUtils.getWrappedInsn;
public class EnumVisitor extends AbstractVisitor {
private MethodInfo enumValueOfMth;
private MethodInfo cloneMth;
@Override
public void init(RootNode root) {
@@ -68,6 +71,12 @@ public class EnumVisitor extends AbstractVisitor {
"valueOf",
Arrays.asList(ArgType.CLASS, ArgType.STRING),
ArgType.ENUM);
cloneMth = MethodInfo.fromDetails(root,
ClassInfo.fromType(root, ArgType.OBJECT),
"clone",
Collections.emptyList(),
ArgType.OBJECT);
}
@Override
@@ -377,6 +386,7 @@ public class EnumVisitor extends AbstractVisitor {
return enumFieldNode;
}
@SuppressWarnings("StatementWithEmptyBody")
private EnumField createEnumFieldByConstructor(ClassNode cls, FieldNode enumFieldNode, ConstructorInsn co) {
// usually constructor signature is '<init>(Ljava/lang/String;I)V'.
// sometimes for one field enum second arg can be omitted
@@ -417,13 +427,12 @@ public class EnumVisitor extends AbstractVisitor {
}
private void removeEnumMethods(ClassNode cls, ArgType clsType, FieldNode valuesField) {
String valuesMethod = "values()" + TypeGen.signature(ArgType.array(clsType));
FieldInfo valuesFieldInfo = valuesField.getFieldInfo();
String valuesMethodShortId = "values()" + TypeGen.signature(ArgType.array(clsType));
MethodNode valuesMethod = null;
// remove compiler generated methods
for (MethodNode mth : cls.getMethods()) {
MethodInfo mi = mth.getMethodInfo();
if (mi.isClassInit()) {
if (mi.isClassInit() || mth.isNoCode()) {
continue;
}
String shortId = mi.getShortId();
@@ -432,12 +441,33 @@ public class EnumVisitor extends AbstractVisitor {
mth.add(AFlag.DONT_GENERATE);
}
markArgsForSkip(mth);
} else if (shortId.equals(valuesMethod)
|| usesValuesField(mth, valuesFieldInfo)
|| simpleValueOfMth(mth, clsType)) {
} else if (mi.getShortId().equals(valuesMethodShortId)) {
if (isValuesMethod(mth, clsType)) {
valuesMethod = mth;
mth.add(AFlag.DONT_GENERATE);
} else {
// custom values method => rename to resolve conflict with enum method
mth.getMethodInfo().setAlias("valuesCustom");
mth.addAttr(new RenameReasonAttr(mth).append("to resolve conflict with enum method"));
}
} else if (isValuesMethod(mth, clsType)) {
if (!mth.getMethodInfo().getAlias().equals("values") && !mth.getUseIn().isEmpty()) {
// rename to use default values method
mth.getMethodInfo().setAlias("values");
mth.addAttr(new RenameReasonAttr(mth).append("to match enum method name"));
mth.add(AFlag.DONT_RENAME);
}
valuesMethod = mth;
mth.add(AFlag.DONT_GENERATE);
} else if (simpleValueOfMth(mth, clsType)) {
mth.add(AFlag.DONT_GENERATE);
}
}
FieldInfo valuesFieldInfo = valuesField.getFieldInfo();
for (MethodNode mth : cls.getMethods()) {
// fix access to 'values' field and 'values()' method
fixValuesAccess(mth, valuesFieldInfo, clsType, valuesMethod);
}
}
private void markArgsForSkip(MethodNode mth) {
@@ -458,6 +488,25 @@ public class EnumVisitor extends AbstractVisitor {
return false;
}
// TODO: support other method patterns ???
private boolean isValuesMethod(MethodNode mth, ArgType clsType) {
ArgType retType = mth.getReturnType();
if (!retType.isArray() || !retType.getArrayElement().equals(clsType)) {
return false;
}
InsnNode returnInsn = BlockUtils.getOnlyOneInsnFromMth(mth);
if (returnInsn == null || returnInsn.getType() != InsnType.RETURN || returnInsn.getArgsCount() != 1) {
return false;
}
InsnNode wrappedInsn = getWrappedInsn(getSingleArg(returnInsn));
IndexInsnNode castInsn = (IndexInsnNode) checkInsnType(wrappedInsn, InsnType.CHECK_CAST);
if (castInsn != null && Objects.equals(castInsn.getIndex(), ArgType.array(clsType))) {
InvokeNode invokeInsn = (InvokeNode) checkInsnType(getWrappedInsn(getSingleArg(castInsn)), InsnType.INVOKE);
return invokeInsn != null && invokeInsn.getCallMth().equals(cloneMth);
}
return false;
}
private boolean simpleValueOfMth(MethodNode mth, ArgType clsType) {
InsnNode returnInsn = InsnUtils.searchSingleReturnInsn(mth, insn -> insn.getArgsCount() == 1);
if (returnInsn == null) {
@@ -472,9 +521,41 @@ public class EnumVisitor extends AbstractVisitor {
return false;
}
private boolean usesValuesField(MethodNode mth, FieldInfo valuesFieldInfo) {
private void fixValuesAccess(MethodNode mth, FieldInfo valuesFieldInfo, ArgType clsType, @Nullable MethodNode valuesMethod) {
MethodInfo mi = mth.getMethodInfo();
if (mi.isConstructor() || mi.isClassInit() || mth.isNoCode() || mth == valuesMethod) {
return;
}
// search value field usage
Predicate<InsnNode> insnTest = insn -> Objects.equals(((IndexInsnNode) insn).getIndex(), valuesFieldInfo);
return InsnUtils.searchInsn(mth, InsnType.SGET, insnTest) != null;
InsnNode useInsn = InsnUtils.searchInsn(mth, InsnType.SGET, insnTest);
if (useInsn == null) {
return;
}
// replace 'values' field with 'values()' method
InsnUtils.replaceInsns(mth, insn -> {
if (insn.getType() == InsnType.SGET && insnTest.test(insn)) {
MethodInfo valueMth = valuesMethod == null
? getValueMthInfo(mth.root(), clsType)
: valuesMethod.getMethodInfo();
InvokeNode invokeNode = new InvokeNode(valueMth, InvokeType.STATIC, 0);
invokeNode.setResult(insn.getResult());
if (valuesMethod == null) {
// forcing enum method (can overlap and get renamed by custom method)
invokeNode.add(AFlag.FORCE_RAW_NAME);
}
mth.addDebugComment("Replace access to removed values field (" + valuesFieldInfo.getName() + ") with 'values()' method");
return invokeNode;
}
return null;
});
}
private MethodInfo getValueMthInfo(RootNode root, ArgType clsType) {
return MethodInfo.fromDetails(root,
ClassInfo.fromType(root, clsType),
"values",
Collections.emptyList(), ArgType.array(clsType));
}
private static void processEnumCls(ClassNode cls, EnumField field, ClassNode innerCls) {
@@ -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;
@@ -50,7 +50,7 @@ public class BlockExceptionHandler {
return false;
}
BlockProcessor.updateCleanSuccessors(mth);
BlockProcessor.computeDominanceFrontier(mth);
DominatorTree.computeDominanceFrontier(mth);
processCatchAttr(mth);
initExcHandlers(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);
@@ -1,9 +1,6 @@
package jadx.core.dex.visitors.blocks;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collections;
import java.util.Deque;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
@@ -31,7 +28,6 @@ import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import static jadx.core.dex.visitors.blocks.BlockSplitter.connect;
import static jadx.core.utils.EmptyBitSet.EMPTY;
public class BlockProcessor extends AbstractVisitor {
private static final Logger LOG = LoggerFactory.getLogger(BlockProcessor.class);
@@ -50,29 +46,23 @@ public class BlockProcessor extends AbstractVisitor {
computeDominators(mth);
if (independentBlockTreeMod(mth)) {
checkForUnreachableBlocks(mth);
clearBlocksState(mth);
computeDominators(mth);
}
if (FixMultiEntryLoops.process(mth)) {
clearBlocksState(mth);
computeDominators(mth);
}
updateCleanSuccessors(mth);
int i = 0;
while (modifyBlocksTree(mth)) {
// revert calculations
clearBlocksState(mth);
// recalculate dominators tree
computeDominators(mth);
if (i++ > 100) {
throw new JadxRuntimeException("CFG modification limit reached, blocks count: " + mth.getBasicBlocks().size());
}
}
checkForUnreachableBlocks(mth);
computeDominanceFrontier(mth);
DominatorTree.computeDominanceFrontier(mth);
registerLoops(mth);
processNestedLoops(mth);
@@ -209,139 +199,9 @@ public class BlockProcessor extends AbstractVisitor {
}
private static void computeDominators(MethodNode mth) {
List<BlockNode> basicBlocks = mth.getBasicBlocks();
int nBlocks = basicBlocks.size();
for (int i = 0; i < nBlocks; i++) {
BlockNode block = basicBlocks.get(i);
block.setId(i);
block.setDoms(new BitSet(nBlocks));
block.getDoms().set(0, nBlocks);
}
BlockNode entryBlock = mth.getEnterBlock();
calcDominators(basicBlocks, entryBlock);
clearBlocksState(mth);
DominatorTree.compute(mth);
markLoops(mth);
// clear self dominance
basicBlocks.forEach(block -> {
block.getDoms().clear(block.getId());
if (block.getDoms().isEmpty()) {
block.setDoms(EMPTY);
}
});
calcImmediateDominators(mth, basicBlocks, entryBlock);
}
private static void calcDominators(List<BlockNode> basicBlocks, BlockNode entryBlock) {
entryBlock.getDoms().clear();
entryBlock.getDoms().set(entryBlock.getId());
BitSet domSet = new BitSet(basicBlocks.size());
boolean changed;
do {
changed = false;
for (BlockNode block : basicBlocks) {
if (block == entryBlock) {
continue;
}
BitSet d = block.getDoms();
if (!changed) {
domSet.clear();
domSet.or(d);
}
for (BlockNode pred : block.getPredecessors()) {
d.and(pred.getDoms());
}
d.set(block.getId());
if (!changed && !d.equals(domSet)) {
changed = true;
}
}
} while (changed);
}
private static void calcImmediateDominators(MethodNode mth, List<BlockNode> basicBlocks, BlockNode entryBlock) {
for (BlockNode block : basicBlocks) {
if (block == entryBlock) {
continue;
}
BlockNode idom;
List<BlockNode> preds = block.getPredecessors();
if (preds.size() == 1) {
idom = preds.get(0);
} else {
BitSet bs = new BitSet(block.getDoms().length());
bs.or(block.getDoms());
for (int i = bs.nextSetBit(0); i >= 0; i = bs.nextSetBit(i + 1)) {
BlockNode dom = basicBlocks.get(i);
bs.andNot(dom.getDoms());
}
if (bs.cardinality() != 1) {
throw new JadxRuntimeException("Can't find immediate dominator for block " + block
+ " in " + bs + " preds:" + preds);
}
idom = basicBlocks.get(bs.nextSetBit(0));
}
block.setIDom(idom);
idom.addDominatesOn(block);
}
}
static void computeDominanceFrontier(MethodNode mth) {
mth.getExitBlock().setDomFrontier(EMPTY);
List<BlockNode> domSortedBlocks = new ArrayList<>(mth.getBasicBlocks().size());
Deque<BlockNode> stack = new LinkedList<>();
stack.push(mth.getEnterBlock());
while (!stack.isEmpty()) {
BlockNode node = stack.pop();
for (BlockNode dominated : node.getDominatesOn()) {
stack.push(dominated);
}
domSortedBlocks.add(node);
}
Collections.reverse(domSortedBlocks);
for (BlockNode block : domSortedBlocks) {
try {
computeBlockDF(mth, block);
} catch (Exception e) {
throw new JadxRuntimeException("Failed compute block dominance frontier", e);
}
}
}
private static void computeBlockDF(MethodNode mth, BlockNode block) {
if (block.getDomFrontier() != null) {
return;
}
List<BlockNode> blocks = mth.getBasicBlocks();
BitSet domFrontier = null;
for (BlockNode s : block.getSuccessors()) {
if (s.getIDom() != block) {
if (domFrontier == null) {
domFrontier = new BitSet(blocks.size());
}
domFrontier.set(s.getId());
}
}
for (BlockNode c : block.getDominatesOn()) {
BitSet frontier = c.getDomFrontier();
if (frontier == null) {
throw new JadxRuntimeException("Dominance frontier not calculated for dominated block: " + c + ", from: " + block);
}
for (int p = frontier.nextSetBit(0); p >= 0; p = frontier.nextSetBit(p + 1)) {
if (blocks.get(p).getIDom() != block) {
if (domFrontier == null) {
domFrontier = new BitSet(blocks.size());
}
domFrontier.set(p);
}
}
}
if (domFrontier == null || domFrontier.isEmpty()) {
domFrontier = EMPTY;
}
block.setDomFrontier(domFrontier);
}
private static void markLoops(MethodNode mth) {
@@ -349,7 +209,7 @@ public class BlockProcessor extends AbstractVisitor {
// Every successor that dominates its predecessor is a header of a loop,
// block -> successor is a back edge.
block.getSuccessors().forEach(successor -> {
if (block.getDoms().get(successor.getId())) {
if (block.getDoms().get(successor.getId()) || block == successor) {
successor.add(AFlag.LOOP_START);
block.add(AFlag.LOOP_END);
@@ -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) {
@@ -0,0 +1,168 @@
package jadx.core.dex.visitors.blocks;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.List;
import org.jetbrains.annotations.NotNull;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.BlockUtils;
import jadx.core.utils.EmptyBitSet;
import jadx.core.utils.exceptions.JadxRuntimeException;
/**
* Build dominator tree based on the algorithm described in paper:
* Cooper, Keith D.; Harvey, Timothy J; Kennedy, Ken (2001).
* "A Simple, Fast Dominance Algorithm"
* http://www.hipersoft.rice.edu/grads/publications/dom14.pdf
*/
@SuppressWarnings("JavadocLinkAsPlainText")
public class DominatorTree {
public static void compute(MethodNode mth) {
List<BlockNode> sorted = sortBlocks(mth);
BlockNode[] doms = build(sorted);
apply(sorted, doms);
}
private static List<BlockNode> sortBlocks(MethodNode mth) {
int blocksCount = mth.getBasicBlocks().size();
List<BlockNode> sorted = new ArrayList<>(blocksCount);
BlockUtils.dfsVisit(mth, sorted::add);
if (sorted.size() != blocksCount) {
throw new JadxRuntimeException("Found unreachable blocks");
}
mth.setBasicBlocks(sorted);
return sorted;
}
@NotNull
private static BlockNode[] build(List<BlockNode> sorted) {
int blocksCount = sorted.size();
BlockNode[] doms = new BlockNode[blocksCount];
doms[0] = sorted.get(0);
boolean changed = true;
while (changed) {
changed = false;
for (int blockId = 1; blockId < blocksCount; blockId++) {
BlockNode b = sorted.get(blockId);
List<BlockNode> preds = b.getPredecessors();
int pickedPred = -1;
BlockNode newIDom = null;
for (BlockNode pred : preds) {
int id = pred.getId();
if (doms[id] != null) {
newIDom = pred;
pickedPred = id;
break;
}
}
if (newIDom == null) {
throw new JadxRuntimeException("No predecessors for block: " + b);
}
for (BlockNode predBlock : preds) {
int predId = predBlock.getId();
if (predId == pickedPred) {
continue;
}
if (doms[predId] != null) {
newIDom = intersect(sorted, doms, predBlock, newIDom);
}
}
if (doms[blockId] != newIDom) {
doms[blockId] = newIDom;
changed = true;
}
}
}
return doms;
}
private static BlockNode intersect(List<BlockNode> sorted, BlockNode[] doms, BlockNode b1, BlockNode b2) {
int f1 = b1.getId();
int f2 = b2.getId();
while (f1 != f2) {
while (f1 > f2) {
f1 = doms[f1].getId();
}
while (f2 > f1) {
f2 = doms[f2].getId();
}
}
return sorted.get(f1);
}
private static void apply(List<BlockNode> sorted, BlockNode[] doms) {
BlockNode enterBlock = sorted.get(0);
enterBlock.setDoms(EmptyBitSet.EMPTY);
enterBlock.setIDom(null);
int blocksCount = sorted.size();
for (int i = 1; i < blocksCount; i++) {
BlockNode block = sorted.get(i);
BlockNode idom = doms[i];
block.setIDom(idom);
idom.addDominatesOn(block);
BitSet domBS = collectDoms(doms, idom);
domBS.clear(i);
block.setDoms(domBS);
}
}
private static BitSet collectDoms(BlockNode[] doms, BlockNode idom) {
BitSet domBS = new BitSet(doms.length);
BlockNode nextIDom = idom;
while (true) {
int id = nextIDom.getId();
if (domBS.get(id)) {
break;
}
domBS.set(id);
BitSet curDoms = nextIDom.getDoms();
if (curDoms != null) {
// use already collected set
domBS.or(curDoms);
break;
}
nextIDom = doms[id];
}
return domBS;
}
public static void computeDominanceFrontier(MethodNode mth) {
List<BlockNode> blocks = mth.getBasicBlocks();
for (BlockNode block : blocks) {
block.setDomFrontier(null);
}
int blocksCount = blocks.size();
for (BlockNode block : blocks) {
List<BlockNode> preds = block.getPredecessors();
if (preds.size() >= 2) {
BlockNode idom = block.getIDom();
for (BlockNode pred : preds) {
BlockNode runner = pred;
while (runner != idom) {
addToDF(runner, block, blocksCount);
runner = runner.getIDom();
}
}
}
}
for (BlockNode block : blocks) {
BitSet df = block.getDomFrontier();
if (df == null || df.isEmpty()) {
block.setDomFrontier(EmptyBitSet.EMPTY);
}
}
}
private static void addToDF(BlockNode block, BlockNode dfBlock, int blocksCount) {
BitSet df = block.getDomFrontier();
if (df == null) {
df = new BitSet(blocksCount);
block.setDomFrontier(df);
}
df.set(dfBlock.getId());
}
}
@@ -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) {
@@ -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;
@@ -14,9 +14,7 @@ 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.nodes.RenameReasonAttr;
import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.info.FieldInfo;
import jadx.core.dex.nodes.ClassNode;
@@ -45,8 +43,8 @@ public class RenameVisitor extends AbstractVisitor {
deobfuscator.execute();
}
checkClasses(deobfuscator, root, args);
UserRenames.applyForNodes(root);
checkClasses(deobfuscator, root, args);
if (args.isDeobfuscationOn() || !args.isJsonOutput()) {
deobfuscator.savePresets();
@@ -196,11 +194,6 @@ public class RenameVisitor extends AbstractVisitor {
if (args.isRenameValid()) {
Set<String> names = new HashSet<>(methods.size());
for (MethodNode mth : methods) {
AccessInfo accessFlags = mth.getAccessFlags();
if (accessFlags.isBridge() || accessFlags.isSynthetic()
|| mth.contains(AFlag.DONT_GENERATE) /* this flag not set yet */) {
continue;
}
String signature = mth.getMethodInfo().makeSignature(true, false);
if (!names.add(signature)) {
deobfuscator.forceRenameMethod(mth);
@@ -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) {
@@ -1013,6 +1013,9 @@ public class BlockUtils {
*/
@Nullable
public static InsnNode getOnlyOneInsnFromMth(MethodNode mth) {
if (mth.isNoCode()) {
return null;
}
InsnNode insn = null;
for (BlockNode block : mth.getBasicBlocks()) {
List<InsnNode> blockInsns = block.getInstructions();
@@ -246,4 +246,29 @@ public class DebugUtils {
Set<Object> seen = ConcurrentHashMap.newKeySet();
return t -> seen.add(keyExtractor.apply(t));
}
private static Map<String, Long> execTimes;
public static void initExecTimes() {
execTimes = new ConcurrentHashMap<>();
}
public static void mergeExecTimeFromStart(String tag, long startTimeMillis) {
mergeExecTime(tag, System.currentTimeMillis() - startTimeMillis);
}
public static void mergeExecTime(String tag, long execTimeMillis) {
execTimes.merge(tag, execTimeMillis, Long::sum);
}
public static void printExecTimes() {
System.out.println("Exec times:");
execTimes.forEach((tag, time) -> System.out.println(" " + tag + ": " + time + "ms"));
}
public static void printExecTimesWithTotal(long totalMillis) {
System.out.println("Exec times: total " + totalMillis + "ms");
execTimes.forEach((tag, time) -> System.out.println(" " + tag + ": " + time + "ms"
+ String.format(" (%.2f%%)", time * 100. / (double) totalMillis)));
}
}
@@ -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);
}
@@ -1,6 +1,7 @@
package jadx.core.utils;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import org.jetbrains.annotations.Nullable;
@@ -140,6 +141,37 @@ public class InsnUtils {
return null;
}
public static void replaceInsns(MethodNode mth, Function<InsnNode, InsnNode> replaceFunction) {
for (BlockNode block : mth.getBasicBlocks()) {
List<InsnNode> insns = block.getInstructions();
int insnsCount = insns.size();
for (int i = 0; i < insnsCount; i++) {
InsnNode insn = insns.get(i);
replaceInsnsInInsn(mth, insn, replaceFunction);
InsnNode replace = replaceFunction.apply(insn);
if (replace != null) {
BlockUtils.replaceInsn(mth, block, i, replace);
}
}
}
}
public static void replaceInsnsInInsn(MethodNode mth, InsnNode insn, Function<InsnNode, InsnNode> replaceFunction) {
int argsCount = insn.getArgsCount();
for (int i = 0; i < argsCount; i++) {
InsnArg arg = insn.getArg(i);
if (arg.isInsnWrap()) {
InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn();
replaceInsnsInInsn(mth, wrapInsn, replaceFunction);
InsnNode replace = replaceFunction.apply(wrapInsn);
if (replace != null) {
InsnRemover.unbindArgUsage(mth, arg);
insn.setArg(i, InsnArg.wrapInsnIntoArg(replace));
}
}
}
}
@Nullable
public static RegisterArg getRegFromInsn(List<RegisterArg> regs, InsnType insnType) {
for (RegisterArg reg : regs) {
@@ -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;
@@ -22,6 +22,7 @@ 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;
@@ -289,7 +290,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 +302,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) {
@@ -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 {
@@ -0,0 +1,13 @@
package jadx.core.xmlgen;
import java.io.IOException;
import java.io.InputStream;
public interface IResParser {
void decode(InputStream inputStream) throws IOException;
ResourceStorage getResStorage();
String[] getStrings();
}
@@ -0,0 +1,31 @@
package jadx.core.xmlgen;
import java.io.IOException;
import java.io.InputStream;
import jadx.api.ResourceFile;
import jadx.api.ResourceType;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.exceptions.JadxRuntimeException;
public class ResDecoder {
public static IResParser decode(RootNode root, ResourceFile resFile, InputStream is) throws IOException {
if (resFile.getType() != ResourceType.ARSC) {
throw new IllegalArgumentException("Unexpected resource type for decode: " + resFile.getType() + ", expect ARSC");
}
IResParser parser = null;
String fileName = resFile.getOriginalName();
if (fileName.endsWith(".arsc")) {
parser = new ResTableParser(root);
}
if (fileName.endsWith(".pb")) {
parser = new ResProtoParser(root);
}
if (parser == null) {
throw new JadxRuntimeException("Unknown type of resource file: " + fileName);
}
parser.decode(is);
return parser;
}
}
@@ -20,16 +20,16 @@ import com.android.aapt.Resources.Style;
import com.android.aapt.Resources.Styleable;
import com.android.aapt.Resources.Type;
import com.android.aapt.Resources.Value;
import com.google.protobuf.InvalidProtocolBufferException;
import jadx.api.ICodeInfo;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.files.FileUtils;
import jadx.core.xmlgen.entry.EntryConfig;
import jadx.core.xmlgen.entry.ProtoValue;
import jadx.core.xmlgen.entry.ResourceEntry;
import jadx.core.xmlgen.entry.ValuesParser;
public class ResProtoParser {
public class ResProtoParser implements IResParser {
private final RootNode root;
private final ResourceStorage resStorage = new ResourceStorage();
@@ -38,11 +38,7 @@ public class ResProtoParser {
}
public ResContainer decodeFiles(InputStream inputStream) throws IOException {
ResourceTable table = decodeProto(inputStream);
for (Package p : table.getPackageList()) {
parse(p);
}
resStorage.finish();
decode(inputStream);
ValuesParser vp = new ValuesParser(new String[0], resStorage.getResourcesNames());
ResXmlGen resGen = new ResXmlGen(resStorage, vp);
ICodeInfo content = XmlGenUtils.makeXmlDump(root.makeCodeWriter(), resStorage);
@@ -50,6 +46,15 @@ public class ResProtoParser {
return ResContainer.resourceTable("res", xmlFiles, content);
}
@Override
public void decode(InputStream inputStream) throws IOException {
ResourceTable table = ResourceTable.parseFrom(FileUtils.streamToByteArray(inputStream));
for (Package p : table.getPackageList()) {
parse(p);
}
resStorage.finish();
}
private void parse(Package p) {
String name = p.getPackageName();
resStorage.setAppPackage(name);
@@ -241,8 +246,13 @@ public class ResProtoParser {
return "";
}
private ResourceTable decodeProto(InputStream inputStream)
throws InvalidProtocolBufferException, IOException {
return ResourceTable.parseFrom(XmlGenUtils.readData(inputStream));
@Override
public ResourceStorage getResStorage() {
return resStorage;
}
@Override
public String[] getStrings() {
return new String[0];
}
}
@@ -10,21 +10,25 @@ 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;
import jadx.core.xmlgen.entry.ResourceEntry;
import jadx.core.xmlgen.entry.ValuesParser;
public class ResTableParser extends CommonBinaryParser {
public class ResTableParser extends CommonBinaryParser implements IResParser {
private static final Logger LOG = LoggerFactory.getLogger(ResTableParser.class);
private static final Pattern VALID_RES_KEY_PATTERN = Pattern.compile("[\\w\\d_]+");
@@ -76,6 +80,7 @@ public class ResTableParser extends CommonBinaryParser {
this.useRawResName = useRawResNames;
}
@Override
public void decode(InputStream inputStream) throws IOException {
is = new ParserStream(inputStream);
decodeTableChunk();
@@ -93,14 +98,6 @@ public class ResTableParser extends CommonBinaryParser {
return ResContainer.resourceTable("res", xmlFiles, content);
}
public ResourceStorage getResStorage() {
return resStorage;
}
public String[] getStrings() {
return strings;
}
void decodeTableChunk() throws IOException {
is.checkInt16(RES_TABLE_TYPE, "Not a table chunk");
is.checkInt16(0x000c, "Unexpected table header size");
@@ -294,35 +291,66 @@ public class ResTableParser extends CommonBinaryParser {
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 {
@@ -434,4 +462,14 @@ public class ResTableParser extends CommonBinaryParser {
is.skipToPos(start + length, "readScriptOrVariantChar");
return sb.toString();
}
@Override
public ResourceStorage getResStorage() {
return resStorage;
}
@Override
public String[] getStrings() {
return strings;
}
}
@@ -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);
@@ -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);
}
@@ -19,10 +19,15 @@ import static org.hamcrest.Matchers.notNullValue;
public abstract class SmaliTest extends IntegrationTest {
private static final String SMALI_TESTS_PROJECT = "jadx-core";
private static final String SMALI_TESTS_DIR = "src/test/smali";
private static final String SMALI_TESTS_EXT = ".smali";
private String currentProject = "jadx-core";
public void setCurrentProject(String currentProject) {
this.currentProject = currentProject;
}
@BeforeEach
public void init() {
Assumptions.assumeFalse(USE_JAVA_INPUT, "skip smali test for java input tests");
@@ -89,24 +94,24 @@ public abstract class SmaliTest extends IntegrationTest {
.collect(Collectors.toList());
}
private static File getSmaliFile(String baseName) {
private File getSmaliFile(String baseName) {
File smaliFile = new File(SMALI_TESTS_DIR, baseName + SMALI_TESTS_EXT);
if (smaliFile.exists()) {
return smaliFile;
}
File pathFromRoot = new File(SMALI_TESTS_PROJECT, smaliFile.getPath());
File pathFromRoot = new File(currentProject, smaliFile.getPath());
if (pathFromRoot.exists()) {
return pathFromRoot;
}
throw new AssertionError("Smali file not found: " + smaliFile.getPath());
}
private static File getSmaliDir(String baseName) {
private File getSmaliDir(String baseName) {
File smaliDir = new File(SMALI_TESTS_DIR, baseName);
if (smaliDir.exists()) {
return smaliDir;
}
File pathFromRoot = new File(SMALI_TESTS_PROJECT, smaliDir.getPath());
File pathFromRoot = new File(currentProject, smaliDir.getPath());
if (pathFromRoot.exists()) {
return pathFromRoot;
}
@@ -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;
}
}
@@ -8,7 +8,6 @@ import org.assertj.core.api.Assertions;
import jadx.api.ICodeInfo;
import jadx.api.metadata.ICodeAnnotation;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.ICodeNode;
import jadx.tests.api.IntegrationTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
@@ -58,11 +57,12 @@ public class JadxClassNodeAssertions extends AbstractObjectAssert<JadxClassNodeA
return this;
}
public void checkCodeAnnotationFor(String refStr, ICodeNode node) {
public JadxClassNodeAssertions checkCodeAnnotationFor(String refStr, ICodeAnnotation node) {
checkCodeAnnotationFor(refStr, 0, node);
return this;
}
public void checkCodeAnnotationFor(String refStr, int refOffset, ICodeNode node) {
public JadxClassNodeAssertions checkCodeAnnotationFor(String refStr, int refOffset, ICodeAnnotation node) {
ICodeInfo code = actual.getCode();
int codePos = code.getCodeStr().indexOf(refStr);
assertThat(codePos).describedAs("String '%s' not found", refStr).isNotEqualTo(-1);
@@ -70,9 +70,10 @@ public class JadxClassNodeAssertions extends AbstractObjectAssert<JadxClassNodeA
for (Map.Entry<Integer, ICodeAnnotation> entry : code.getCodeMetadata().getAsMap().entrySet()) {
if (entry.getKey() == refPos) {
Assertions.assertThat(entry.getValue()).isEqualTo(node);
return;
return this;
}
}
fail("Annotation for reference string: '%s' at position %d not found", refStr, refPos);
return this;
}
}
@@ -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();");
}
}
@@ -1,8 +1,10 @@
package jadx.tests.integration.enums;
import java.util.Collections;
import org.junit.jupiter.api.Test;
import jadx.core.dex.nodes.ClassNode;
import jadx.api.CommentsLevel;
import jadx.tests.api.SmaliTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
@@ -32,14 +34,31 @@ public class TestEnumObfuscated extends SmaliTest {
public synthetic int getNum() {
return this.num;
}
// custom values method
// should be kept and renamed to avoid collision to enum 'values()' method
public static int values() {
return new TestEnumObfuscated[0];
}
// usage of renamed 'values()' method, should be renamed back to 'values'
public static int valuesCount() {
return vs().length;
}
// usage of renamed '$VALUES' field, should be replaced with 'values()' method call
public static int valuesFieldUse() {
return $VLS.length;
}
}
*/
// @formatter:on
@Test
public void test() {
ClassNode cls = getClassNodeFromSmali();
assertThat(cls)
getArgs().setCommentsLevel(CommentsLevel.WARN);
getArgs().setRenameFlags(Collections.emptySet());
assertThat(getClassNodeFromSmali())
.code()
.doesNotContain("$VLS")
.doesNotContain("vo(")
@@ -56,8 +56,6 @@ public class TestGenericsMthOverride extends IntegrationTest {
assertThat(code, containsOne("public Y method(Exception x) {"));
assertThat(code, containsOne("public Object method(Object x) {"));
assertThat(code, countString(3, "@Override"));
// TODO: @Override missing for class C
// assertThat(code, countString(4, "@Override"));
assertThat(code, countString(4, "@Override"));
}
}
@@ -0,0 +1,47 @@
package jadx.tests.integration.inline;
import java.util.Collections;
import org.junit.jupiter.api.Test;
import jadx.tests.api.SmaliTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
@SuppressWarnings("CommentedOutCode")
public class TestOverlapSyntheticMethods extends SmaliTest {
// @formatter:off
/*
public String test(int i) {
return a(i) + "|" + a(i);
}
public int a(int i) {
return i;
}
public String a(int i) {
return "i:" + i;
}
*/
// @formatter:on
@Test
public void testSmali() {
assertThat(getClassNodeFromSmali())
.code()
.containsOne("int a(int i) {")
.containsOne("String m0a(int i) {");
}
@Test
public void testSmaliNoRename() {
getArgs().setRenameFlags(Collections.emptySet());
disableCompilation();
assertThat(getClassNodeFromSmali())
.code()
.containsOne("int a(int i) {")
.containsOne("String a(int i) {")
.containsOne("return a(i) + \"|\" + a(i);");
}
}
@@ -0,0 +1,45 @@
package jadx.tests.integration.inline;
import java.util.function.Function;
import org.junit.jupiter.api.Test;
import jadx.api.metadata.ICodeAnnotation;
import jadx.api.metadata.annotations.NodeDeclareRef;
import jadx.core.dex.nodes.ClassNode;
import jadx.tests.api.SmaliTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestOverrideBridgeMerge extends SmaliTest {
public static class TestCls implements Function<String, Integer> {
@Override
public /* bridge */ /* synthetic */ Integer apply(String str) {
return test(str);
}
public Integer test(String str) {
return str.length();
}
}
@Test
public void test() {
assertThat(getClassNode(TestCls.class))
.code()
.containsOne("Integer test(String str) {"); // not inlined
}
@Test
public void testSmali() {
ClassNode cls = getClassNodeFromSmali();
ICodeAnnotation mthDef = new NodeDeclareRef(getMethod(cls, "apply"));
assertThat(cls)
.checkCodeAnnotationFor("apply(String str) {", mthDef)
.code()
.containsOne("@Override")
.containsOne("public Integer apply(String str) {")
.doesNotContain("test(String str)");
}
}
@@ -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,6 +1,7 @@
package jadx.tests.integration.others;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Test;
@@ -8,6 +9,7 @@ import jadx.api.JadxInternalAccess;
import jadx.api.JavaClass;
import jadx.api.JavaMethod;
import jadx.api.metadata.ICodeAnnotation;
import jadx.api.metadata.ICodeAnnotation.AnnType;
import jadx.api.metadata.ICodeMetadata;
import jadx.api.metadata.ICodeNodeRef;
import jadx.core.dex.nodes.ClassNode;
@@ -53,14 +55,25 @@ public class TestCodeMetadata extends IntegrationTest {
int callUse = callUsePlaces.get(0);
ICodeMetadata metadata = cls.getCode().getCodeMetadata();
System.out.println(metadata);
ICodeNodeRef callDef = metadata.getNodeAt(callUse);
assertThat(callDef).isSameAs(testMth);
int beforeCallDef = callDefPos - 10;
ICodeAnnotation closest = metadata.getClosestUp(beforeCallDef);
AtomicInteger endPos = new AtomicInteger();
ICodeAnnotation testEnd = metadata.searchUp(callDefPos, (pos, ann) -> {
if (ann.getAnnType() == AnnType.END) {
endPos.set(pos);
return ann;
}
return null;
});
assertThat(testEnd).isNotNull();
int testEndPos = endPos.get();
ICodeAnnotation closest = metadata.getClosestUp(testEndPos);
assertThat(closest).isInstanceOf(FieldNode.class); // field reference from 'return a.str;'
ICodeNodeRef nodeBelow = metadata.getNodeBelow(beforeCallDef);
ICodeNodeRef nodeBelow = metadata.getNodeBelow(testEndPos);
assertThat(nodeBelow).isSameAs(callMth);
}
}
@@ -0,0 +1,62 @@
package jadx.tests.integration.others;
import java.util.List;
import org.junit.jupiter.api.Test;
import jadx.api.JavaClass;
import jadx.api.JavaMethod;
import jadx.api.metadata.ICodeMetadata;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.tests.api.IntegrationTest;
import static jadx.api.JadxInternalAccess.convertClassNode;
import static jadx.api.JadxInternalAccess.convertMethodNode;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestCodeMetadata2 extends IntegrationTest {
public static class TestCls {
@SuppressWarnings("Convert2Lambda")
public Runnable test(boolean a) {
if (a) {
return new Runnable() {
@Override
public void run() {
System.out.println("test");
}
};
}
System.out.println("another");
return empty();
}
public static Runnable empty() {
return new Runnable() {
@Override
public void run() {
// empty
}
};
}
}
@Test
public void test() {
ClassNode cls = getClassNode(TestCls.class);
assertThat(cls).code().containsOne("return empty();");
MethodNode testMth = getMethod(cls, "test");
MethodNode emptyMth = getMethod(cls, "empty");
JavaClass javaClass = convertClassNode(jadxDecompiler, cls);
JavaMethod emptyJavaMethod = convertMethodNode(jadxDecompiler, emptyMth);
List<Integer> emptyUsePlaces = javaClass.getUsePlacesFor(javaClass.getCodeInfo(), emptyJavaMethod);
assertThat(emptyUsePlaces).hasSize(1);
int callUse = emptyUsePlaces.get(0);
ICodeMetadata metadata = cls.getCode().getCodeMetadata();
assertThat(metadata.getNodeAt(callUse)).isSameAs(testMth);
}
}
@@ -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();");
}
}
@@ -26,7 +26,6 @@ public class TestMoveInline extends SmaliTest {
@Test
public void test() {
getArgs().setRawCFGOutput(true);
assertThat(getClassNodeFromSmali())
.code()
// check operations order
@@ -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) {"));
}
}
@@ -72,6 +72,27 @@
return-object v0
.end method
.method public static values()[Lenums/TestEnumObfuscated;
.registers 1
sget-object v0, Lenums/TestEnumObfuscated;->$VLS:[Lenums/TestEnumObfuscated;
return v0
.end method
.method public static valuesCount()I
.registers 2
invoke-static {v0, p0}, Lenums/TestEnumObfuscated;->vs()[Lenums/TestEnumObfuscated;
move-result-object v0
array-length v1, v0
return v1
.end method
.method public static valuesFieldUse()I
.registers 2
sget-object v0, Lenums/TestEnumObfuscated;->$VLS:[Lenums/TestEnumObfuscated;
array-length v1, v0
return v1
.end method
.method public synthetic getNum()I
.registers 2
@@ -0,0 +1,41 @@
.class public Linline/TestOverlapSyntheticMethods;
.super Ljava/lang/Object;
.method public synthetic a(I)I
.registers 2
return p1
.end method
.method public synthetic a(I)Ljava/lang/String;
.registers 4
new-instance v0, Ljava/lang/StringBuilder;
invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V
const-string v1, "i:"
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v0
invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;
move-result-object v0
invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0
return-object v0
.end method
.method public test(I)Ljava/lang/String;
.registers 4
new-instance v0, Ljava/lang/StringBuilder;
invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V
invoke-virtual {p0, p1}, Linline/TestOverlapSyntheticMethods;->a(I)I
move-result v1
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;
move-result-object v0
const-string v1, "|"
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v0
invoke-virtual {p0, p1}, Linline/TestOverlapSyntheticMethods;->a(I)Ljava/lang/String;
move-result-object v1
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v0
invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0
return-object v0
.end method
@@ -0,0 +1,39 @@
.class public Linline/TestOverrideBridgeMerge;
.super Ljava/lang/Object;
.implements Ljava/util/function/Function;
.annotation system Ldalvik/annotation/Signature;
value = {
"Ljava/lang/Object;",
"Ljava/util/function/Function",
"<",
"Ljava/lang/String;",
"Ljava/lang/Integer;",
">;"
}
.end annotation
.method public constructor <init>()V
.registers 1
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
.method public bridge synthetic apply(Ljava/lang/Object;)Ljava/lang/Object;
.registers 3
check-cast p1, Ljava/lang/String;
invoke-virtual {p0, p1}, Linline/TestOverrideBridgeMerge;->test(Ljava/lang/String;)Ljava/lang/Integer;
move-result-object v0
return-object v0
.end method
.method public test(Ljava/lang/String;)Ljava/lang/Integer;
.registers 3
.param p1, "str" # Ljava/lang/String;
invoke-virtual {p1}, Ljava/lang/String;->length()I
move-result v0
invoke-static {v0}, Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer;
move-result-object v0
return-object v0
.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
+12 -6
View File
@@ -15,18 +15,18 @@ dependencies {
implementation files('libs/jfontchooser-1.0.5.jar')
implementation 'hu.kazocsaba:image-viewer:1.2.3'
implementation 'com.formdev:flatlaf:2.3'
implementation 'com.formdev:flatlaf-intellij-themes:2.3'
implementation 'com.formdev:flatlaf-extras: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.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 'io.reactivex.rxjava2:rxjava:2.2.21'
implementation "com.github.akarnokd:rxjava2-swing:0.3.7"
implementation 'com.android.tools.build:apksig:4.2.1'
implementation 'com.android.tools.build:apksig:7.2.2'
implementation 'io.github.hqktech:jdwp:1.0'
// TODO: Switch back to upstream once this PR gets merged:
@@ -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 {
@@ -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')
}
@@ -228,7 +228,7 @@ public class JadxWrapper {
return getDecompiler().getJavaNodeByRef(nodeRef);
}
public JavaNode getEnclosingNode(ICodeInfo codeInfo, int pos) {
public @Nullable JavaNode getEnclosingNode(ICodeInfo codeInfo, int pos) {
return getDecompiler().getEnclosingNode(codeInfo, pos);
}
@@ -46,6 +46,7 @@ import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.exceptions.JadxRuntimeException;
import static jadx.api.plugins.input.data.AccessFlagsScope.FIELD;
import static jadx.api.plugins.input.data.AccessFlagsScope.METHOD;
@@ -60,7 +61,9 @@ import static jadx.api.plugins.input.insns.Opcode.INVOKE_CUSTOM;
import static jadx.api.plugins.input.insns.Opcode.INVOKE_CUSTOM_RANGE;
import static jadx.api.plugins.input.insns.Opcode.INVOKE_POLYMORPHIC;
import static jadx.api.plugins.input.insns.Opcode.INVOKE_POLYMORPHIC_RANGE;
import static jadx.api.plugins.input.insns.Opcode.PACKED_SWITCH;
import static jadx.api.plugins.input.insns.Opcode.PACKED_SWITCH_PAYLOAD;
import static jadx.api.plugins.input.insns.Opcode.SPARSE_SWITCH;
import static jadx.api.plugins.input.insns.Opcode.SPARSE_SWITCH_PAYLOAD;
public class Smali {
@@ -288,6 +291,14 @@ public class Smali {
if (codeReader.getDebugInfo() != null) {
formatDbgInfo(codeReader.getDebugInfo(), line);
}
// first pass to fill payload offsets for switch instructions
codeReader.visitInstructions(insn -> {
Opcode opcode = insn.getOpcode();
if (opcode == PACKED_SWITCH || opcode == SPARSE_SWITCH) {
insn.decode();
line.addPayloadOffset(insn.getOffset(), insn.getTarget());
}
});
codeReader.visitInstructions(insn -> {
InsnNode node = decodeInsn(insn, line);
nodes.put((long) insn.getOffset(), node);
@@ -404,7 +415,6 @@ public class Smali {
line.addTip(target, String.format(FMT_S_SWITCH_TAG, target), "");
line.getLineWriter().append(", ").append(String.format(FMT_S_SWITCH, target));
}
line.addPayloadOffset(insn.getOffset(), target);
return true;
}
}
@@ -733,7 +743,7 @@ public class Smali {
ISwitchPayload payload = (ISwitchPayload) insn.getPayload();
if (payload != null) {
fmtSwitchPayload(insn, FMT_P_SWITCH_CASE, FMT_P_SWITCH_CASE_TAG, line, payload, insn.getOffset());
fmtSwitchPayload(insn, FMT_P_SWITCH_CASE, FMT_P_SWITCH_CASE_TAG, line, payload);
}
return true;
}
@@ -743,7 +753,7 @@ public class Smali {
ISwitchPayload payload = (ISwitchPayload) insn.getPayload();
if (payload != null) {
fmtSwitchPayload(insn, FMT_S_SWITCH_CASE, FMT_S_SWITCH_CASE_TAG, line, payload, insn.getOffset());
fmtSwitchPayload(insn, FMT_S_SWITCH_CASE, FMT_S_SWITCH_CASE_TAG, line, payload);
}
return true;
}
@@ -755,17 +765,19 @@ public class Smali {
return false;
}
private void fmtSwitchPayload(InsnData insn, String fmtTarget, String fmtTag, LineInfo line,
ISwitchPayload payload, int curOffset) {
private void fmtSwitchPayload(InsnData insn, String fmtTarget, String fmtTag, LineInfo line, ISwitchPayload payload) {
int lineStart = getInsnColStart();
lineStart += CODE_OFFSET_COLUMN_WIDTH + 1 + 1; // plus 1s for space and the ':'
String basicIndent = new String(new byte[lineStart]).replace("\0", " ");
String indent = SmaliWriter.INDENT_STR + basicIndent;
int[] keys = payload.getKeys();
int[] targets = payload.getTargets();
int opcodeOffset = line.payloadOffsetMap.get(curOffset);
Integer switchOffset = line.payloadOffsetMap.get(insn.getOffset());
if (switchOffset == null) {
throw new JadxRuntimeException("Unknown switch insn for payload at " + insn.getOffset());
}
for (int i = 0; i < keys.length; i++) {
int target = opcodeOffset + targets[i];
int target = switchOffset + targets[i];
line.addInsnLine(insn.getOffset(),
String.format("%scase %d: -> " + fmtTarget, indent, keys[i], target));
line.addTip(target,
@@ -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);
@@ -39,7 +39,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 +50,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 +102,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;
}
@@ -134,13 +137,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<>();
@@ -166,9 +169,9 @@ public class ADBDevice {
}
private List<Process> getProcessList(String cmd, int index) throws IOException {
try (Socket socket = ADB.connect(info.adbHost, info.adbPort)) {
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 +197,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 +247,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 + '}';
}
}
}
@@ -4,4 +4,12 @@ public interface Cancelable {
boolean isCanceled();
void cancel();
default int getCancelTimeoutMS() {
return 2000;
}
default int getShutdownTimeoutMS() {
return 5000;
}
}
@@ -54,7 +54,7 @@ public interface IBackgroundTask extends Cancelable {
/**
* Return progress notifications listener (use executor tick rate and thread) (Optional)
*/
default @Nullable Consumer<ITaskProgress> getOnProgressListener() {
default @Nullable Consumer<ITaskProgress> getProgressListener() {
return null;
}
}
@@ -107,7 +107,7 @@ public class QuarkManager {
private void loadReport() {
try {
QuarkReportNode quarkNode = new QuarkReportNode(reportFile);
JRoot root = mainWindow.getCacheObject().getJRoot();
JRoot root = mainWindow.getTreeRoot();
root.replaceCustomNode(quarkNode);
root.update();
mainWindow.reloadTree();
@@ -10,6 +10,7 @@ import jadx.core.utils.Utils;
@SuppressWarnings("MemberName")
public class QuarkReportData {
public static class Crime {
public String crime;
public String confidence;
@@ -18,6 +19,23 @@ public class QuarkReportData {
List<Method> native_api;
List<JsonElement> combination;
List<Map<String, InvokePlace>> register;
public int parseConfidence() {
return Integer.parseInt(confidence.replace("%", ""));
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("Crime{");
sb.append("crime='").append(crime).append('\'');
sb.append(", confidence='").append(confidence).append('\'');
sb.append(", permissions=").append(permissions);
sb.append(", native_api=").append(native_api);
sb.append(", combination=").append(combination);
sb.append(", register=").append(register);
sb.append('}');
return sb.toString();
}
}
public static class Method {
@@ -46,4 +64,22 @@ public class QuarkReportData {
String threat_level;
int total_score;
List<Crime> crimes;
public void validate() {
if (crimes == null) {
throw new RuntimeException("Invalid data: \"crimes\" list missing");
}
for (Crime crime : crimes) {
if (crime.confidence == null) {
throw new RuntimeException("Confidence value missing: " + crime);
}
try {
crime.parseConfidence();
} catch (Exception e) {
throw new RuntimeException("Invalid crime entry: " + crime);
}
}
}
}
@@ -1,5 +1,6 @@
package jadx.gui.plugins.quark;
import java.io.BufferedReader;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -33,12 +34,12 @@ public class QuarkReportNode extends JNode {
private static final ImageIcon ICON = UiUtils.openSvgIcon("ui/quark");
private final Path apkFile;
private final Path reportFile;
private ICodeInfo errorContent;
public QuarkReportNode(Path apkFile) {
this.apkFile = apkFile;
public QuarkReportNode(Path reportFile) {
this.reportFile = reportFile;
}
@Override
@@ -59,7 +60,11 @@ public class QuarkReportNode extends JNode {
@Override
public ContentPanel getContentPanel(TabbedPane tabbedPane) {
try {
QuarkReportData data = GSON.fromJson(Files.newBufferedReader(apkFile), QuarkReportData.class);
QuarkReportData data;
try (BufferedReader reader = Files.newBufferedReader(reportFile)) {
data = GSON.fromJson(reader, QuarkReportData.class);
}
data.validate();
return new QuarkReportPanel(tabbedPane, this, data);
} catch (Exception e) {
LOG.error("Quark report parse error", e);
@@ -70,7 +70,7 @@ public class QuarkReportPanel extends ContentPanel {
}
private void prepareData() {
data.crimes.sort(Comparator.comparingInt(c -> -Integer.parseInt(c.confidence.replace("%", ""))));
data.crimes.sort(Comparator.comparingInt(c -> -c.parseConfidence()));
}
private void initUI() {
@@ -290,7 +290,7 @@ public class QuarkReportPanel extends ContentPanel {
}
return new MethodTreeNode(javaMethod);
} catch (Exception e) {
LOG.error("Failed to parse method descriptor string", e);
LOG.error("Failed to parse method descriptor string: {}", descr, e);
return new TextTreeNode(descr);
}
}

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