Compare commits

..

28 Commits

Author SHA1 Message Date
Skylot d22db30166 fix: use secure xml parser for process manifest 2022-01-20 11:17:12 +00:00
Skylot 6db61e7a59 chore: update dependencies 2022-01-20 10:23:49 +00:00
Skylot 86582de521 feat: use kotlin intrinsic methods for variables rename (#1207) 2022-01-19 17:30:04 +00:00
Skylot a7c63c2eb3 fix: handle method override with several bases (#1234) 2022-01-18 18:27:09 +00:00
Skylot 081a0e21ee fix: precalculate class deps for inline methods (#1339) 2022-01-17 14:38:38 +00:00
Skylot 9ac9c05265 fix: simplify cascading casts (#1336) 2022-01-15 16:31:18 +00:00
Skylot b7daf79b26 fix: add explicit type for non-int constants (#1336) 2022-01-15 14:11:44 +00:00
Skylot b67a3561a4 build: add CodeQL analysis 2022-01-13 22:37:36 +03:00
Skylot 52ac6dbbaf docs: add security.md 2022-01-13 16:45:32 +00:00
Skylot 72381ad8f3 fix: correct literal negate for double and float (#1334) 2022-01-13 14:00:53 +00:00
Skylot 6a065c46f4 chore: update dependencies 2022-01-13 12:12:15 +00:00
Skylot 092d0d7e67 fix(gui): reduce tree focus switching 2022-01-12 19:57:38 +03:00
Skylot 5ca7285558 fix(gui): correct handling for tree row click (#1324) 2022-01-12 16:57:25 +00:00
Skylot 7576f9cd5e fix: wrap negative literals before cast (#1327) 2022-01-12 17:31:40 +03:00
Skylot 46b5725d98 refactor(test): replace inputs with test profiles 2022-01-12 17:31:37 +03:00
Jan S 72542fa6f9 fix(gui): processing threads spinner initialization (#1331)(PR #1332)
* fix: processing threads spinner initialization (#1331)
* fix: processing threads spinner initialization (#1331)
2022-01-12 14:23:07 +00:00
demonlol a250d0461b fix(dbg): support multiple main <action> and <activity-alias> tags (#1322)(PR #1323)
* fix(dgb): support multiple main <action> and <activity-alias> tags in manifest
* Update jadx-gui/src/main/java/jadx/gui/device/debugger/DbgUtils.java
2022-01-02 20:09:24 +03:00
Skylot c7795bfc48 fix: improve anonymous class inline (#523) 2021-12-26 13:06:49 +00:00
Skylot 5de46b7e40 chore: update gradle and dependencies 2021-12-24 12:53:30 +00:00
Skylot 99c70872c1 fix: use debug line numbers only at fixed offsets (#1315) 2021-12-22 22:55:14 +03:00
Skylot 3566669303 chore: update lgtm config 2021-12-22 12:24:01 +00:00
Skylot 4557d05256 fix: use correct type for anonymous class instance (#597) 2021-12-21 17:47:52 +00:00
Skylot fa421d165e build: disable missing warnings from javadoc 2021-12-21 12:52:52 +00:00
Skylot ecf20020d7 chore: cache current working dir in static field, other minor changes 2021-12-20 19:25:07 +00:00
Skylot ae85af61c7 fix: skip input file name checks by zip name validator (#1310) 2021-12-20 18:55:28 +00:00
Skylot 659bbbf4fb fix: correct usage of Path.getParent() 2021-12-20 16:48:50 +00:00
Jan S 427e2dddc4 fix: use relative file paths in .jadx project file (#1312) (PR #1313)
* chore: use relative file paths in .jadx project file (#1312)
* code beautified
* requested changes
2021-12-20 13:52:51 +00:00
skylot d47483f957 docs: use jadx as a library 2021-12-19 20:36:58 +00:00
106 changed files with 2108 additions and 622 deletions
+41
View File
@@ -0,0 +1,41 @@
name: "CodeQL"
on:
push:
branches: [master]
pull_request:
# The branches below must be a subset of the branches above
branches: [master]
schedule:
- cron: '0 9 * * 5'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: ['java']
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
queries: +security-extended
languages: ${{ matrix.language }}
# Don't build tests in jadx-core also skip tests execution and checkstyle tasks
- run: |
./gradlew clean build -x checkstyleTest -x checkstyleMain -x test -x ':jadx-core:testClasses'
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
+2
View File
@@ -34,3 +34,5 @@ jadx-output/
*.cfg
*.orig
quark.json
cliff.toml
+5 -1
View File
@@ -51,6 +51,9 @@ For windows you can download it from [oracle.com](https://www.oracle.com/java/te
brew install jadx
```
### Use jadx as a library
You can use jadx in your java projects, check details on [wiki page](https://github.com/skylot/jadx/wiki/Use-jadx-as-a-library)
### Build from source
JDK 8 or higher must be installed:
```
@@ -93,6 +96,7 @@ options:
--deobf-rewrite-cfg - force to ignore and overwrite deobfuscation map file
--deobf-use-sourcename - use source file name as class name alias
--deobf-parse-kotlin-metadata - parse kotlin metadata to class and package 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),
'valid' - rename java identifiers to make them valid,
@@ -104,7 +108,7 @@ options:
--raw-cfg - save methods control flow graph (use raw instructions)
-f, --fallback - make simple dump (using goto instead of 'if', 'for', etc)
--use-dx - use dx/d8 to convert java bytecode
--comments-level - set code comments level, values: none, user_only, error, warn, info, debug, default: info
--comments-level - set code comments level, values: error, warn, info, debug, user-only, none, default: info
--log-level - set log level, values: quiet, progress, error, warn, info, debug, default: progress
-v, --verbose - verbose output (set --log-level to DEBUG)
-q, --quiet - turn off output (set --log-level to QUIET)
+7
View File
@@ -0,0 +1,7 @@
# Security Policy
## Reporting a Vulnerability
To report a security issue, please email `skylot@gmail.com` with a description of the issue, the steps you took to create the issue, affected versions, and, if known, mitigations for the issue.
We will check and respond within 3 working days. If the issue is confirmed as a vulnerability, we will apply required mitigations at the next release.
This project follows a 90 day disclosure timeline.
+6 -6
View File
@@ -1,6 +1,6 @@
plugins {
id 'com.github.ben-manes.versions' version '0.39.0'
id 'com.diffplug.spotless' version '6.0.2'
id 'com.github.ben-manes.versions' version '0.41.0'
id 'com.diffplug.spotless' version '6.2.0'
}
ext.jadxVersion = System.getenv('JADX_VERSION') ?: "dev"
@@ -27,13 +27,13 @@ allprojects {
}
dependencies {
implementation 'org.slf4j:slf4j-api:1.7.32'
implementation 'org.slf4j:slf4j-api:1.7.33'
compileOnly 'org.jetbrains:annotations:23.0.0'
testImplementation 'ch.qos.logback:logback-classic:1.2.8'
testImplementation 'ch.qos.logback:logback-classic:1.2.10'
testImplementation 'org.hamcrest:hamcrest-library:2.2'
testImplementation 'org.mockito:mockito-core:4.1.0'
testImplementation 'org.assertj:assertj-core:3.21.0'
testImplementation 'org.mockito:mockito-core:4.2.0'
testImplementation 'org.assertj:assertj-core:3.22.0'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2'
@@ -73,4 +73,6 @@ javadoc {
if (JavaVersion.current().isJava9Compatible()) {
options.addBooleanOption('html5', true)
}
// disable 'missing' warnings
options.addStringOption('Xdoclint:all,-missing', '-quiet')
}
+2 -2
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=9afb3ca688fc12c761a0e9e4321e4d24e977a4a8916c8a768b1fe05ddb4d6b66
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.1-bin.zip
distributionSha256Sum=b586e04868a22fd817c8971330fec37e298f3242eb85c374181b12d637f80302
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+2 -2
View File
@@ -10,8 +10,8 @@ dependencies {
runtimeOnly(project(':jadx-plugins:jadx-java-convert'))
runtimeOnly(project(':jadx-plugins:jadx-smali-input'))
implementation 'com.beust:jcommander:1.81'
implementation 'ch.qos.logback:logback-classic:1.2.8'
implementation 'com.beust:jcommander:1.82'
implementation 'ch.qos.logback:logback-classic:1.2.10'
}
application {
@@ -14,6 +14,7 @@ import com.beust.jcommander.Parameter;
import jadx.api.CommentsLevel;
import jadx.api.JadxArgs;
import jadx.api.JadxArgs.RenameEnum;
import jadx.api.JadxArgs.UseKotlinMethodsForVarNames;
import jadx.api.JadxDecompiler;
import jadx.core.utils.exceptions.JadxException;
import jadx.core.utils.files.FileUtils;
@@ -101,6 +102,13 @@ public class JadxCLIArgs {
@Parameter(names = { "--deobf-parse-kotlin-metadata" }, description = "parse kotlin metadata to class and package names")
protected boolean deobfuscationParseKotlinMetadata = false;
@Parameter(
names = { "--use-kotlin-methods-for-var-names" },
description = "use kotlin intrinsic methods to rename variables, values: disable, apply, apply-and-hide",
converter = UseKotlinMethodsForVarNamesConverter.class
)
protected UseKotlinMethodsForVarNames useKotlinMethodsForVarNames = UseKotlinMethodsForVarNames.APPLY;
@Parameter(
names = { "--rename-flags" },
description = "fix options (comma-separated list of):"
@@ -130,7 +138,7 @@ public class JadxCLIArgs {
@Parameter(
names = { "--comments-level" },
description = "set code comments level, values: error, warn, info, debug, user_only, none",
description = "set code comments level, values: error, warn, info, debug, user-only, none",
converter = CommentsLevelConverter.class
)
protected CommentsLevel commentsLevel = CommentsLevel.INFO;
@@ -223,6 +231,7 @@ public class JadxCLIArgs {
args.setDeobfuscationMaxLength(deobfuscationMaxLength);
args.setUseSourceNameAsClassAlias(deobfuscationUseSourceNameAsAlias);
args.setParseKotlinMetadata(deobfuscationParseKotlinMetadata);
args.setUseKotlinMethodsForVarNames(useKotlinMethodsForVarNames);
args.setEscapeUnicode(escapeUnicode);
args.setRespectBytecodeAccModifiers(respectBytecodeAccessModifiers);
args.setExportAsGradleProject(exportAsGradleProject);
@@ -326,6 +335,10 @@ public class JadxCLIArgs {
return deobfuscationParseKotlinMetadata;
}
public UseKotlinMethodsForVarNames getUseKotlinMethodsForVarNames() {
return useKotlinMethodsForVarNames;
}
public boolean isEscapeUnicode() {
return escapeUnicode;
}
@@ -412,9 +425,22 @@ public class JadxCLIArgs {
}
}
public static class UseKotlinMethodsForVarNamesConverter implements IStringConverter<UseKotlinMethodsForVarNames> {
@Override
public UseKotlinMethodsForVarNames convert(String value) {
try {
return UseKotlinMethodsForVarNames.valueOf(value.replace('-', '_').toUpperCase());
} catch (Exception e) {
throw new IllegalArgumentException(
'\'' + value + "' is unknown, possible values are: "
+ JadxCLIArgs.enumValuesString(CommentsLevel.values()));
}
}
}
public static String enumValuesString(Enum<?>[] values) {
return Stream.of(values)
.map(v -> v.name().toLowerCase(Locale.ROOT))
.map(v -> v.name().replace('_', '-').toLowerCase(Locale.ROOT))
.collect(Collectors.joining(", "));
}
}
+16 -1
View File
@@ -87,6 +87,12 @@ public class JadxArgs {
private boolean useDxInput = false;
public enum UseKotlinMethodsForVarNames {
DISABLE, APPLY, APPLY_AND_HIDE
}
private UseKotlinMethodsForVarNames useKotlinMethodsForVarNames = UseKotlinMethodsForVarNames.APPLY;
public JadxArgs() {
// use default options
}
@@ -138,7 +144,7 @@ public class JadxArgs {
}
public void setThreadsCount(int threadsCount) {
this.threadsCount = threadsCount;
this.threadsCount = Math.max(1, threadsCount); // make sure threadsCount >= 1
}
public boolean isCfgOutput() {
@@ -433,6 +439,14 @@ public class JadxArgs {
this.useDxInput = useDxInput;
}
public UseKotlinMethodsForVarNames getUseKotlinMethodsForVarNames() {
return useKotlinMethodsForVarNames;
}
public void setUseKotlinMethodsForVarNames(UseKotlinMethodsForVarNames useKotlinMethodsForVarNames) {
this.useKotlinMethodsForVarNames = useKotlinMethodsForVarNames;
}
@Override
public String toString() {
return "JadxArgs{" + "inputFiles=" + inputFiles
@@ -452,6 +466,7 @@ public class JadxArgs {
+ ", deobfuscationForceSave=" + deobfuscationForceSave
+ ", useSourceNameAsClassAlias=" + useSourceNameAsClassAlias
+ ", parseKotlinMetadata=" + parseKotlinMetadata
+ ", useKotlinMethodsForVarNames=" + useKotlinMethodsForVarNames
+ ", deobfuscationMinLength=" + deobfuscationMinLength
+ ", deobfuscationMaxLength=" + deobfuscationMaxLength
+ ", escapeUnicode=" + escapeUnicode
@@ -37,6 +37,10 @@ public class ResourceFile {
private ZipRef zipRef;
private String deobfName;
public static ResourceFile createResourceFile(JadxDecompiler decompiler, File file, ResourceType type) {
return new ResourceFile(decompiler, file.getAbsolutePath(), type);
}
public static ResourceFile createResourceFile(JadxDecompiler decompiler, String name, ResourceType type) {
if (!ZipSecurity.isValidZipEntryName(name)) {
return null;
@@ -41,7 +41,7 @@ public enum ResourceType {
}
public static ResourceType getFileType(String fileName) {
if (fileName.matches("[^/]+/resources.pb")) {
if (fileName.endsWith("/resources.pb")) {
return ARSC;
}
int dot = fileName.lastIndexOf('.');
@@ -145,16 +145,8 @@ public final class ResourcesLoader {
return null;
});
} else {
addResourceFile(list, file);
}
}
private void addResourceFile(List<ResourceFile> list, File file) {
String name = file.getAbsolutePath();
ResourceType type = ResourceType.getFileType(name);
ResourceFile rf = ResourceFile.createResourceFile(jadxRef, name, type);
if (rf != null) {
list.add(rf);
ResourceType type = ResourceType.getFileType(file.getAbsolutePath());
list.add(ResourceFile.createResourceFile(jadxRef, file, type));
}
}
@@ -12,6 +12,7 @@ import org.slf4j.LoggerFactory;
import jadx.api.CommentsLevel;
import jadx.api.JadxArgs;
import jadx.core.dex.visitors.AnonymousClassVisitor;
import jadx.core.dex.visitors.AttachCommentsVisitor;
import jadx.core.dex.visitors.AttachMethodDetails;
import jadx.core.dex.visitors.AttachTryCatchVisitor;
@@ -37,6 +38,7 @@ import jadx.core.dex.visitors.OverrideMethodVisitor;
import jadx.core.dex.visitors.PrepareForCodeGen;
import jadx.core.dex.visitors.ProcessAnonymous;
import jadx.core.dex.visitors.ProcessInstructionsVisitor;
import jadx.core.dex.visitors.ProcessMethodsForInline;
import jadx.core.dex.visitors.ReSugarCode;
import jadx.core.dex.visitors.ShadowFieldVisitor;
import jadx.core.dex.visitors.SignatureProcessor;
@@ -46,6 +48,7 @@ import jadx.core.dex.visitors.blocks.BlockSplitter;
import jadx.core.dex.visitors.debuginfo.DebugInfoApplyVisitor;
import jadx.core.dex.visitors.debuginfo.DebugInfoAttachVisitor;
import jadx.core.dex.visitors.finaly.MarkFinallyVisitor;
import jadx.core.dex.visitors.kotlin.ProcessKotlinInternals;
import jadx.core.dex.visitors.regions.CheckRegions;
import jadx.core.dex.visitors.regions.CleanRegions;
import jadx.core.dex.visitors.regions.IfRegionVisitor;
@@ -88,6 +91,7 @@ public class Jadx {
passes.add(new RenameVisitor());
passes.add(new UsageInfoVisitor());
passes.add(new ProcessAnonymous());
passes.add(new ProcessMethodsForInline());
return passes;
}
@@ -128,6 +132,9 @@ public class Jadx {
if (args.isDebugInfo()) {
passes.add(new DebugInfoApplyVisitor());
}
if (args.getUseKotlinMethodsForVarNames() != JadxArgs.UseKotlinMethodsForVarNames.DISABLE) {
passes.add(new ProcessKotlinInternals());
}
passes.add(new CodeRenameVisitor());
if (args.isInlineMethods()) {
passes.add(new InlineMethods());
@@ -135,6 +142,7 @@ public class Jadx {
passes.add(new GenericTypesVisitor());
passes.add(new ShadowFieldVisitor());
passes.add(new DeboxingVisitor());
passes.add(new AnonymousClassVisitor());
passes.add(new ModVisitor());
passes.add(new CodeShrinkVisitor());
passes.add(new ReSugarCode());
@@ -1,10 +1,5 @@
package jadx.core;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -94,21 +89,13 @@ public final class ProcessClass {
return generateCode(topParentClass);
}
try {
Set<ClassNode> useIn = new HashSet<>(cls.getUseIn());
List<ClassNode> usedInDeps = new ArrayList<>();
for (ClassNode depCls : cls.getDependencies()) {
if (useIn.contains(depCls)) {
// postpone to resolve cross dependencies
usedInDeps.add(depCls);
} else {
process(depCls, false);
}
process(depCls, false);
}
if (!usedInDeps.isEmpty()) {
// process current class before its usage
if (!cls.getCodegenDeps().isEmpty()) {
process(cls, false);
for (ClassNode depCls : usedInDeps) {
process(depCls, false);
for (ClassNode codegenDep : cls.getCodegenDeps()) {
process(codegenDep, false);
}
}
ICodeInfo code = process(cls, true);
@@ -15,7 +15,6 @@ import jadx.api.data.annotations.InsnCodeOffset;
import jadx.api.data.annotations.VarDeclareRef;
import jadx.api.data.annotations.VarRef;
import jadx.api.plugins.input.data.MethodHandleType;
import jadx.core.deobf.NameMapper;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.FieldReplaceAttr;
@@ -112,7 +111,7 @@ public class InsnGen {
}
code.add(mgen.getNameGen().useArg(reg));
} else if (arg.isLiteral()) {
code.add(lit((LiteralArg) arg));
addLiteralArg(code, (LiteralArg) arg, flags);
} else if (arg.isInsnWrap()) {
addWrappedArg(code, (InsnWrapArg) arg, flags);
} else if (arg.isNamed()) {
@@ -122,6 +121,15 @@ public class InsnGen {
}
}
private void addLiteralArg(ICodeWriter code, LiteralArg litArg, Set<Flags> flags) {
String literalStr = lit(litArg);
if (!flags.contains(Flags.BODY_ONLY_NOWRAP) && literalStr.startsWith("-")) {
code.add('(').add(literalStr).add(')');
} else {
code.add(literalStr);
}
}
private void addWrappedArg(ICodeWriter code, InsnWrapArg arg, Set<Flags> flags) throws CodegenException {
InsnNode wrapInsn = arg.getWrapInsn();
if (wrapInsn.contains(AFlag.FORCE_ASSIGN_INLINE)) {
@@ -717,14 +725,7 @@ public class InsnGen {
throw new CodegenException("Anonymous inner class unlimited recursion detected."
+ " Convert class to inner: " + cls.getClassInfo().getFullName());
}
cls.add(AFlag.DONT_GENERATE);
ArgType parent;
if (cls.getInterfaces().size() == 1) {
parent = cls.getInterfaces().get(0);
} else {
parent = cls.getSuperClass();
}
ArgType parent = cls.get(AType.ANONYMOUS_CLASS_BASE).getBaseType();
// hide empty anonymous constructors
for (MethodNode ctor : cls.getMethods()) {
if (ctor.contains(AFlag.ANONYMOUS_CONSTRUCTOR)
@@ -732,14 +733,22 @@ public class InsnGen {
ctor.add(AFlag.DONT_GENERATE);
}
}
code.add("new ");
if (parent == null) {
code.add("Object");
} else {
useClass(code, parent);
}
useClass(code, parent);
MethodNode callMth = mth.root().resolveMethod(insn.getCallMth());
if (callMth != null) {
// copy var names
List<RegisterArg> mthArgs = callMth.getArgRegs();
int argsCount = Math.min(insn.getArgsCount(), mthArgs.size());
for (int i = 0; i < argsCount; i++) {
InsnArg arg = insn.getArg(i);
if (arg.isRegister()) {
RegisterArg mthArg = mthArgs.get(i);
RegisterArg insnArg = (RegisterArg) arg;
mthArg.getSVar().setCodeVar(insnArg.getSVar().getCodeVar());
}
}
}
generateMethodArguments(code, insn, 0, callMth);
code.add(' ');
@@ -1029,7 +1038,6 @@ public class InsnGen {
} else {
condGen.wrap(code, insn.getCondition());
code.add(" ? ");
addCastIfNeeded(code, first, second);
addArg(code, first, false);
code.add(" : ");
addArg(code, second, false);
@@ -1039,33 +1047,6 @@ public class InsnGen {
}
}
private void addCastIfNeeded(ICodeWriter code, InsnArg first, InsnArg second) {
if (first.isLiteral() && second.isLiteral()) {
if (first.getType() == ArgType.BYTE) {
long lit1 = ((LiteralArg) first).getLiteral();
long lit2 = ((LiteralArg) second).getLiteral();
if (lit1 != Byte.MAX_VALUE && lit1 != Byte.MIN_VALUE
&& lit2 != Byte.MAX_VALUE && lit2 != Byte.MIN_VALUE) {
code.add("(byte) ");
}
} else if (first.getType() == ArgType.SHORT) {
long lit1 = ((LiteralArg) first).getLiteral();
long lit2 = ((LiteralArg) second).getLiteral();
if (lit1 != Short.MAX_VALUE && lit1 != Short.MIN_VALUE
&& lit2 != Short.MAX_VALUE && lit2 != Short.MIN_VALUE) {
code.add("(short) ");
}
} else if (first.getType() == ArgType.CHAR) {
long lit1 = ((LiteralArg) first).getLiteral();
long lit2 = ((LiteralArg) second).getLiteral();
if (!NameMapper.isPrintableChar((char) (lit1))
&& !NameMapper.isPrintableChar((char) (lit2))) {
code.add("(char) ");
}
}
}
}
private void makeArith(ArithNode insn, ICodeWriter code, Set<Flags> state) throws CodegenException {
if (insn.contains(AFlag.ARITH_ONEARG)) {
makeArithOneArg(insn, code);
@@ -180,7 +180,7 @@ public class MethodGen {
if (overrideAttr == null) {
return;
}
if (!overrideAttr.isAtBaseMth()) {
if (!overrideAttr.getBaseMethods().contains(mth)) {
code.startLine("@Override");
if (mth.checkCommentsLevel(CommentsLevel.INFO)) {
code.add(" // ");
@@ -162,8 +162,7 @@ public class NameGen {
InsnNode assignInsn = assignArg.getParentInsn();
if (assignInsn != null) {
String name = makeNameFromInsn(assignInsn);
if (name != null && !NameMapper.isReserved(name)) {
assignArg.setName(name);
if (name != null && NameMapper.isValidAndPrintable(name)) {
return name;
}
}
@@ -202,7 +201,11 @@ public class NameGen {
return vName;
}
if (shortName != null) {
return StringUtils.escape(shortName.toLowerCase());
String lower = StringUtils.escape(shortName.toLowerCase());
if (shortName.equals(lower)) {
return lower + "Var";
}
return lower;
}
}
return StringUtils.escape(type.toString());
@@ -58,7 +58,7 @@ public class DeobfPresets {
if (inputFiles.isEmpty()) {
return null;
}
Path inputFilePath = inputFiles.get(0).getAbsoluteFile().toPath();
Path inputFilePath = inputFiles.get(0).toPath().toAbsolutePath();
String baseName = FileUtils.getPathBaseName(inputFilePath);
return inputFilePath.getParent().resolve(baseName + ".jobf");
}
@@ -143,19 +143,15 @@ public class NameMapper {
/**
* Return modified string with removed:
* <p>
* <ul>
* <li>not printable chars (including unicode)
* <li>chars not valid for java identifier part
* </ul>
* <p>
* Note: this 'middle' method must be used with prefixed string:
* <p>
* <ul>
* <li>can leave invalid chars for java identifier start (i.e numbers)
* <li>result not checked for reserved words
* </ul>
* <p>
*/
public static String removeInvalidCharsMiddle(String name) {
if (isValidIdentifier(name) && isAllCharsPrintable(name)) {
@@ -33,6 +33,7 @@ public enum AFlag {
SKIP_FIRST_ARG,
SKIP_ARG, // skip argument in invoke call
NO_SKIP_ARGS,
ANONYMOUS_CONSTRUCTOR,
ANONYMOUS_CLASS,
@@ -78,6 +79,8 @@ public enum AFlag {
REQUEST_IF_REGION_OPTIMIZE, // run if region visitor again
RERUN_SSA_TRANSFORM,
METHOD_CANDIDATE_FOR_INLINE,
// Class processing flags
RESTART_CODEGEN, // codegen must be executed again
RELOAD_AT_CODEGEN_STAGE, // class can't be analyzed at 'process' stage => unload before 'codegen' stage
@@ -2,6 +2,7 @@ package jadx.core.dex.attributes;
import jadx.api.plugins.input.data.attributes.IJadxAttrType;
import jadx.api.plugins.input.data.attributes.IJadxAttribute;
import jadx.core.dex.attributes.nodes.AnonymousClassBaseAttr;
import jadx.core.dex.attributes.nodes.ClassTypeVarsAttr;
import jadx.core.dex.attributes.nodes.DeclareVariablesAttr;
import jadx.core.dex.attributes.nodes.EdgeInsnAttr;
@@ -16,6 +17,7 @@ import jadx.core.dex.attributes.nodes.JumpInfo;
import jadx.core.dex.attributes.nodes.LocalVarsDebugInfoAttr;
import jadx.core.dex.attributes.nodes.LoopInfo;
import jadx.core.dex.attributes.nodes.LoopLabelAttr;
import jadx.core.dex.attributes.nodes.MethodBridgeAttr;
import jadx.core.dex.attributes.nodes.MethodInlineAttr;
import jadx.core.dex.attributes.nodes.MethodOverrideAttr;
import jadx.core.dex.attributes.nodes.MethodTypeVarsAttr;
@@ -51,6 +53,7 @@ public final class AType<T extends IJadxAttribute> implements IJadxAttrType<T> {
public static final AType<EnumClassAttr> ENUM_CLASS = new AType<>();
public static final AType<EnumMapAttr> ENUM_MAP = new AType<>();
public static final AType<ClassTypeVarsAttr> CLASS_TYPE_VARS = new AType<>();
public static final AType<AnonymousClassBaseAttr> ANONYMOUS_CLASS_BASE = new AType<>();
// field
public static final AType<FieldInitInsnAttr> FIELD_INIT_INSN = new AType<>();
@@ -63,6 +66,7 @@ public final class AType<T extends IJadxAttribute> implements IJadxAttrType<T> {
public static final AType<MethodOverrideAttr> METHOD_OVERRIDE = new AType<>();
public static final AType<MethodTypeVarsAttr> METHOD_TYPE_VARS = new AType<>();
public static final AType<AttrList<TryCatchBlockAttr>> TRY_BLOCKS_LIST = new AType<>();
public static final AType<MethodBridgeAttr> BRIDGED_BY = new AType<>();
// region
public static final AType<DeclareVariablesAttr> DECLARE_VARIABLES = new AType<>();
@@ -0,0 +1,28 @@
package jadx.core.dex.attributes.nodes;
import jadx.api.plugins.input.data.attributes.PinnedAttribute;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.instructions.args.ArgType;
public class AnonymousClassBaseAttr extends PinnedAttribute {
private final ArgType baseType;
public AnonymousClassBaseAttr(ArgType baseType) {
this.baseType = baseType;
}
public ArgType getBaseType() {
return baseType;
}
@Override
public AType<AnonymousClassBaseAttr> getAttrType() {
return AType.ANONYMOUS_CLASS_BASE;
}
@Override
public String toString() {
return "AnonymousClassBaseAttr{" + baseType + '}';
}
}
@@ -0,0 +1,28 @@
package jadx.core.dex.attributes.nodes;
import jadx.api.plugins.input.data.attributes.PinnedAttribute;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.nodes.MethodNode;
public class MethodBridgeAttr extends PinnedAttribute {
private final MethodNode bridgeMth;
public MethodBridgeAttr(MethodNode bridgeMth) {
this.bridgeMth = bridgeMth;
}
public MethodNode getBridgeMth() {
return bridgeMth;
}
@Override
public AType<MethodBridgeAttr> getAttrType() {
return AType.BRIDGED_BY;
}
@Override
public String toString() {
return "BRIDGED_BY: " + bridgeMth;
}
}
@@ -1,6 +1,7 @@
package jadx.core.dex.attributes.nodes;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import jadx.api.plugins.input.data.attributes.PinnedAttribute;
@@ -20,27 +21,26 @@ public class MethodOverrideAttr extends PinnedAttribute {
*/
private SortedSet<MethodNode> relatedMthNodes;
public MethodOverrideAttr(List<IMethodDetails> overrideList, SortedSet<MethodNode> relatedMthNodes) {
private Set<IMethodDetails> baseMethods;
public MethodOverrideAttr(List<IMethodDetails> overrideList, SortedSet<MethodNode> relatedMthNodes, Set<IMethodDetails> baseMethods) {
this.overrideList = overrideList;
this.relatedMthNodes = relatedMthNodes;
}
public boolean isAtBaseMth() {
return overrideList.isEmpty();
this.baseMethods = baseMethods;
}
public List<IMethodDetails> getOverrideList() {
return overrideList;
}
public void setOverrideList(List<IMethodDetails> overrideList) {
this.overrideList = overrideList;
}
public SortedSet<MethodNode> getRelatedMthNodes() {
return relatedMthNodes;
}
public Set<IMethodDetails> getBaseMethods() {
return baseMethods;
}
public void setRelatedMthNodes(SortedSet<MethodNode> relatedMthNodes) {
this.relatedMthNodes = relatedMthNodes;
}
@@ -52,6 +52,6 @@ public class MethodOverrideAttr extends PinnedAttribute {
@Override
public String toString() {
return "METHOD_OVERRIDE: " + overrideList;
return "METHOD_OVERRIDE: " + getBaseMethods();
}
}
@@ -508,7 +508,17 @@ public class InsnDecoder {
private InsnNode makeNewArray(InsnData insn) {
ArgType indexType = ArgType.parse(insn.getIndexAsType());
int dim = (int) insn.getLiteral();
ArgType arrType = dim == 0 ? indexType : ArgType.array(indexType, dim);
ArgType arrType;
if (dim == 0) {
arrType = indexType;
} else {
if (indexType.isArray()) {
// java bytecode can pass array as a base type
arrType = indexType;
} else {
arrType = ArgType.array(indexType, dim);
}
}
int regsCount = insn.getRegsCount();
NewArrayNode newArr = new NewArrayNode(arrType, regsCount - 1);
newArr.setResult(InsnArg.reg(insn, 0, arrType));
@@ -655,7 +655,7 @@ public abstract class ArgType {
if (from.equals(to)) {
return false;
}
TypeCompareEnum result = root.getTypeUpdate().getTypeCompare().compareTypes(from, to);
TypeCompareEnum result = root.getTypeCompare().compareTypes(from, to);
return !result.isNarrow();
}
@@ -1,5 +1,7 @@
package jadx.core.dex.instructions.args;
import org.jetbrains.annotations.Nullable;
import jadx.core.codegen.TypeGen;
import jadx.core.utils.StringUtils;
import jadx.core.utils.exceptions.JadxRuntimeException;
@@ -57,12 +59,48 @@ public final class LiteralArg extends InsnArg {
}
public boolean isInteger() {
PrimitiveType type = this.type.getPrimitiveType();
return type == PrimitiveType.INT
|| type == PrimitiveType.BYTE
|| type == PrimitiveType.CHAR
|| type == PrimitiveType.SHORT
|| type == PrimitiveType.LONG;
switch (type.getPrimitiveType()) {
case INT:
case BYTE:
case CHAR:
case SHORT:
case LONG:
return true;
default:
return false;
}
}
public boolean isNegative() {
if (isInteger()) {
return literal < 0;
}
if (type == ArgType.FLOAT) {
float val = Float.intBitsToFloat(((int) literal));
return val < 0 && Float.isFinite(val);
}
if (type == ArgType.DOUBLE) {
double val = Double.longBitsToDouble(literal);
return val < 0 && Double.isFinite(val);
}
return false;
}
@Nullable
public LiteralArg negate() {
long neg;
if (isInteger()) {
neg = -literal;
} else if (type == ArgType.FLOAT) {
float val = Float.intBitsToFloat(((int) literal));
neg = Float.floatToIntBits(-val);
} else if (type == ArgType.DOUBLE) {
double val = Double.longBitsToDouble(literal);
neg = Double.doubleToLongBits(-val);
} else {
return null;
}
return new LiteralArg(neg, type);
}
@Override
@@ -47,7 +47,6 @@ import jadx.core.utils.exceptions.JadxRuntimeException;
import static jadx.core.dex.nodes.ProcessState.LOADED;
import static jadx.core.dex.nodes.ProcessState.NOT_LOADED;
import static jadx.core.dex.nodes.ProcessState.PROCESS_COMPLETE;
public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeNode, Comparable<ClassNode> {
private static final Logger LOG = LoggerFactory.getLogger(ClassNode.class);
@@ -79,6 +78,10 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
* Top level classes used in this class (only for top level classes, empty for inners)
*/
private List<ClassNode> dependencies = Collections.emptyList();
/**
* Top level classes needed for code generation stage
*/
private List<ClassNode> codegenDeps = Collections.emptyList();
/**
* Classes which uses this class
*/
@@ -283,12 +286,15 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
return true;
}
public boolean checkProcessed() {
return getTopParentClass().getState().isProcessComplete();
}
public void ensureProcessed() {
ClassNode topClass = getTopParentClass();
ProcessState state = topClass.getState();
if (state != PROCESS_COMPLETE) {
if (!checkProcessed()) {
ClassNode topParentClass = getTopParentClass();
throw new JadxRuntimeException("Expected class to be processed at this point,"
+ " class: " + topClass + ", state: " + state);
+ " class: " + topParentClass + ", state: " + topParentClass.getState());
}
}
@@ -733,6 +739,14 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
this.dependencies = dependencies;
}
public List<ClassNode> getCodegenDeps() {
return codegenDeps;
}
public void setCodegenDeps(List<ClassNode> codegenDeps) {
this.codegenDeps = codegenDeps;
}
public List<ClassNode> getUseIn() {
return useIn;
}
@@ -300,6 +300,20 @@ public class InsnNode extends LineAttrNode {
}
}
/**
* Visit all args recursively (including inner instructions),
* but excluding wrapped args
*/
public void visitArgs(Consumer<InsnArg> visitor) {
for (InsnArg arg : getArguments()) {
if (arg.isInsnWrap()) {
((InsnWrapArg) arg).getWrapInsn().visitArgs(visitor);
} else {
visitor.accept(arg);
}
}
}
/**
* Visit this instruction and all inner (wrapped) instructions
* To terminate visiting return non-null value
@@ -385,17 +399,16 @@ public class InsnNode extends LineAttrNode {
/**
* Make copy of InsnNode object.
* <p>
* <br>
* NOTE: can't copy instruction with result argument
* (SSA variable can't be used in two different assigns).
* <p>
* <br>
* Prefer use next methods:
* <ul>
* <li>{@link #copyWithoutResult()} to explicitly state that result not needed
* <li>{@link #copy(RegisterArg)} to provide new result arg
* <li>{@link #copyWithNewSsaVar(MethodNode)} to make new SSA variable for result arg
* </ul>
* <p>
*/
public InsnNode copy() {
if (this.getClass() != InsnNode.class) {
@@ -336,12 +336,9 @@ public class RootNode {
/**
* Searches for ClassNode by its full name (original or alias name)
*
* <br>
* Warning: This method has a runtime of O(n) (n = number of classes).
* If you need to call it more than once consider {@link #buildFullAliasClassCache()} instead
*
* @param fullName
* @return
*/
@Nullable
public ClassNode searchClassByFullAlias(String fullName) {
@@ -355,10 +352,6 @@ public class RootNode {
return null;
}
/**
*
* @return
*/
public Map<String, ClassNode> buildFullAliasClassCache() {
Map<String, ClassNode> classNameCache = new HashMap<>(classes.size());
for (ClassNode cls : classes) {
@@ -8,6 +8,9 @@ import org.jetbrains.annotations.Nullable;
import jadx.core.clsp.ClspClass;
import jadx.core.clsp.ClspMethod;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.MethodBridgeAttr;
import jadx.core.dex.attributes.nodes.MethodOverrideAttr;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.instructions.BaseInvokeNode;
import jadx.core.dex.instructions.args.ArgType;
@@ -15,6 +18,7 @@ import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.IMethodDetails;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.Utils;
public class MethodUtils {
private final RootNode root;
@@ -67,7 +71,7 @@ public class MethodUtils {
return null;
}
public boolean processMethodArgsOverloaded(ArgType startCls, MethodInfo mthInfo, @Nullable List<IMethodDetails> collectedMths) {
private boolean processMethodArgsOverloaded(ArgType startCls, MethodInfo mthInfo, @Nullable List<IMethodDetails> collectedMths) {
if (startCls == null || !startCls.isObject()) {
return false;
}
@@ -122,4 +126,25 @@ public class MethodUtils {
}
return false;
}
@Nullable
public IMethodDetails getOverrideBaseMth(MethodNode mth) {
MethodOverrideAttr overrideAttr = mth.get(AType.METHOD_OVERRIDE);
if (overrideAttr == null) {
return null;
}
return Utils.getOne(overrideAttr.getBaseMethods());
}
public ClassInfo getMethodOriginDeclClass(MethodNode mth) {
IMethodDetails baseMth = getOverrideBaseMth(mth);
if (baseMth != null) {
return baseMth.getMethodInfo().getDeclClass();
}
MethodBridgeAttr bridgeAttr = mth.get(AType.BRIDGED_BY);
if (bridgeAttr != null) {
return getMethodOriginDeclClass(bridgeAttr.getBridgeMth());
}
return mth.getMethodInfo().getDeclClass();
}
}
@@ -0,0 +1,132 @@
package jadx.core.dex.visitors;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.nodes.FieldReplaceAttr;
import jadx.core.dex.attributes.nodes.SkipMethodArgsAttr;
import jadx.core.dex.info.FieldInfo;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.instructions.IndexInsnNode;
import jadx.core.dex.instructions.InsnType;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.instructions.args.SSAVar;
import jadx.core.dex.instructions.mods.ConstructorInsn;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.visitors.shrink.CodeShrinkVisitor;
import jadx.core.utils.exceptions.JadxException;
@JadxVisitor(
name = "AnonymousClassVisitor",
desc = "Prepare anonymous class for inline",
runBefore = {
ModVisitor.class,
CodeShrinkVisitor.class
}
)
public class AnonymousClassVisitor extends AbstractVisitor {
@Override
public boolean visit(ClassNode cls) throws JadxException {
if (cls.contains(AFlag.ANONYMOUS_CLASS)) {
for (MethodNode mth : cls.getMethods()) {
if (mth.contains(AFlag.ANONYMOUS_CONSTRUCTOR)) {
processAnonymousConstructor(mth);
break;
}
}
}
return true;
}
private static void processAnonymousConstructor(MethodNode mth) {
List<InsnNode> usedInsns = new ArrayList<>();
Map<InsnArg, FieldNode> argsMap = getArgsToFieldsMapping(mth, usedInsns);
if (argsMap.isEmpty()) {
mth.add(AFlag.NO_SKIP_ARGS);
} else {
for (Map.Entry<InsnArg, FieldNode> entry : argsMap.entrySet()) {
FieldNode field = entry.getValue();
if (field == null) {
continue;
}
InsnArg arg = entry.getKey();
field.addAttr(new FieldReplaceAttr(arg));
field.add(AFlag.DONT_GENERATE);
if (arg.isRegister()) {
arg.add(AFlag.SKIP_ARG);
SkipMethodArgsAttr.skipArg(mth, ((RegisterArg) arg));
}
}
}
for (InsnNode usedInsn : usedInsns) {
usedInsn.add(AFlag.DONT_GENERATE);
}
}
private static Map<InsnArg, FieldNode> getArgsToFieldsMapping(MethodNode mth, List<InsnNode> usedInsns) {
MethodInfo callMth = mth.getMethodInfo();
ClassNode cls = mth.getParentClass();
List<RegisterArg> argList = mth.getArgRegs();
ClassNode outerCls = mth.getUseIn().get(0).getParentClass();
int startArg = 0;
if (callMth.getArgsCount() != 0 && callMth.getArgumentsTypes().get(0).equals(outerCls.getClassInfo().getType())) {
startArg = 1;
}
Map<InsnArg, FieldNode> map = new LinkedHashMap<>();
int argsCount = argList.size();
for (int i = startArg; i < argsCount; i++) {
RegisterArg arg = argList.get(i);
InsnNode useInsn = getParentInsnSkipMove(arg);
if (useInsn == null) {
return Collections.emptyMap();
}
switch (useInsn.getType()) {
case IPUT:
FieldNode fieldNode = cls.searchField((FieldInfo) ((IndexInsnNode) useInsn).getIndex());
if (fieldNode == null || !fieldNode.getAccessFlags().isSynthetic()) {
return Collections.emptyMap();
}
map.put(arg, fieldNode);
usedInsns.add(useInsn);
break;
case CONSTRUCTOR:
ConstructorInsn superConstr = (ConstructorInsn) useInsn;
if (!superConstr.isSuper()) {
return Collections.emptyMap();
}
usedInsns.add(useInsn);
break;
default:
return Collections.emptyMap();
}
}
return map;
}
private static InsnNode getParentInsnSkipMove(RegisterArg arg) {
SSAVar sVar = arg.getSVar();
if (sVar.getUseCount() != 1) {
return null;
}
RegisterArg useArg = sVar.getUseList().get(0);
InsnNode parentInsn = useArg.getParentInsn();
if (parentInsn == null) {
return null;
}
if (parentInsn.getType() == InsnType.MOVE) {
return getParentInsnSkipMove(parentInsn.getResult());
}
return parentInsn;
}
}
@@ -1,7 +1,10 @@
package jadx.core.dex.visitors;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import jadx.api.plugins.input.data.AccessFlags;
@@ -55,7 +58,7 @@ public class ClassModifier extends AbstractVisitor {
removeSyntheticFields(cls);
cls.getMethods().forEach(ClassModifier::removeSyntheticMethods);
cls.getMethods().forEach(ClassModifier::removeEmptyMethods);
cls.getMethods().forEach(ClassModifier::cleanInsnsInAnonymousConstructor);
cls.getMethods().forEach(ClassModifier::processAnonymousConstructor);
return false;
}
@@ -326,27 +329,86 @@ public class ClassModifier extends AbstractVisitor {
/**
* Remove super call and put into removed fields from anonymous constructor
*/
private static void cleanInsnsInAnonymousConstructor(MethodNode mth) {
private static void processAnonymousConstructor(MethodNode mth) {
if (!mth.contains(AFlag.ANONYMOUS_CONSTRUCTOR)) {
return;
}
for (BlockNode block : mth.getBasicBlocks()) {
for (InsnNode insn : block.getInstructions()) {
InsnType type = insn.getType();
if (type == InsnType.CONSTRUCTOR) {
ConstructorInsn ctorInsn = (ConstructorInsn) insn;
if (ctorInsn.isSuper()) {
ctorInsn.add(AFlag.DONT_GENERATE);
}
} else if (type == InsnType.IPUT) {
FieldInfo fldInfo = (FieldInfo) ((IndexInsnNode) insn).getIndex();
FieldNode fieldNode = mth.root().resolveField(fldInfo);
if (fieldNode != null && fieldNode.contains(AFlag.DONT_GENERATE)) {
insn.add(AFlag.DONT_GENERATE);
}
}
List<InsnNode> usedInsns = new ArrayList<>();
Map<InsnArg, FieldNode> argsMap = getArgsToFieldsMapping(mth, usedInsns);
for (Map.Entry<InsnArg, FieldNode> entry : argsMap.entrySet()) {
FieldNode field = entry.getValue();
if (field == null) {
continue;
}
InsnArg arg = entry.getKey();
field.addAttr(new FieldReplaceAttr(arg));
field.add(AFlag.DONT_GENERATE);
if (arg.isRegister()) {
arg.add(AFlag.SKIP_ARG);
SkipMethodArgsAttr.skipArg(mth, ((RegisterArg) arg));
}
}
for (InsnNode usedInsn : usedInsns) {
usedInsn.add(AFlag.DONT_GENERATE);
}
}
private static Map<InsnArg, FieldNode> getArgsToFieldsMapping(MethodNode mth, List<InsnNode> usedInsns) {
MethodInfo callMth = mth.getMethodInfo();
ClassNode cls = mth.getParentClass();
List<RegisterArg> argList = mth.getArgRegs();
ClassNode outerCls = mth.getUseIn().get(0).getParentClass();
int startArg = 0;
if (callMth.getArgsCount() != 0 && callMth.getArgumentsTypes().get(0).equals(outerCls.getClassInfo().getType())) {
startArg = 1;
}
Map<InsnArg, FieldNode> map = new LinkedHashMap<>();
int argsCount = argList.size();
for (int i = startArg; i < argsCount; i++) {
RegisterArg arg = argList.get(i);
InsnNode useInsn = getParentInsnSkipMove(arg);
if (useInsn == null) {
return Collections.emptyMap();
}
switch (useInsn.getType()) {
case IPUT:
FieldNode fieldNode = cls.searchField((FieldInfo) ((IndexInsnNode) useInsn).getIndex());
if (fieldNode == null || !fieldNode.getAccessFlags().isSynthetic()) {
return Collections.emptyMap();
}
map.put(arg, fieldNode);
usedInsns.add(useInsn);
break;
case CONSTRUCTOR:
ConstructorInsn superConstr = (ConstructorInsn) useInsn;
if (!superConstr.isSuper()) {
return Collections.emptyMap();
}
usedInsns.add(useInsn);
break;
default:
return Collections.emptyMap();
}
}
return map;
}
private static InsnNode getParentInsnSkipMove(RegisterArg arg) {
SSAVar sVar = arg.getSVar();
if (sVar.getUseCount() != 1) {
return null;
}
RegisterArg useArg = sVar.getUseList().get(0);
InsnNode parentInsn = useArg.getParentInsn();
if (parentInsn == null) {
return null;
}
if (parentInsn.getType() == InsnType.MOVE) {
return getParentInsnSkipMove(parentInsn.getResult());
}
return parentInsn;
}
private static boolean isNonDefaultConstructorExists(MethodNode defCtor) {
@@ -86,7 +86,7 @@ public class FixAccessModifiers extends AbstractVisitor {
if (!accessFlags.isPublic()) {
// if class is used in inlinable method => make it public
for (MethodNode useMth : cls.getUseInMth()) {
boolean canInline = MarkMethodsForInline.canInline(useMth) || useMth.contains(AType.METHOD_INLINE);
boolean canInline = useMth.contains(AFlag.METHOD_CANDIDATE_FOR_INLINE) || useMth.contains(AType.METHOD_INLINE);
if (canInline && !useMth.getUseIn().isEmpty()) {
return AccessFlags.PUBLIC;
}
@@ -10,7 +10,6 @@ import jadx.core.Consts;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.MethodInlineAttr;
import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.info.FieldInfo;
import jadx.core.dex.instructions.IndexInsnNode;
import jadx.core.dex.instructions.InsnType;
@@ -47,7 +46,7 @@ public class MarkMethodsForInline extends AbstractVisitor {
if (mia != null) {
return mia;
}
if (canInline(mth)) {
if (mth.contains(AFlag.METHOD_CANDIDATE_FOR_INLINE)) {
if (mth.getBasicBlocks() == null) {
return null;
}
@@ -59,14 +58,6 @@ public class MarkMethodsForInline extends AbstractVisitor {
return MethodInlineAttr.inlineNotNeeded(mth);
}
public static boolean canInline(MethodNode mth) {
if (mth.isNoCode() || mth.contains(AFlag.DONT_GENERATE)) {
return false;
}
AccessInfo accessFlags = mth.getAccessFlags();
return accessFlags.isSynthetic() && accessFlags.isStatic();
}
@Nullable
private static MethodInlineAttr inlineMth(MethodNode mth) {
List<InsnNode> insns = BlockUtils.collectInsnsWithLimit(mth.getBasicBlocks(), 2);
@@ -79,7 +70,11 @@ public class MarkMethodsForInline extends AbstractVisitor {
if (insn.getType() == InsnType.RETURN && insn.getArgsCount() == 1) {
// synthetic field getter
// set arg from 'return' instruction
return addInlineAttr(mth, InsnNode.wrapArg(insn.getArg(0)));
InsnArg arg = insn.getArg(0);
if (!arg.isInsnWrap()) {
return null;
}
return addInlineAttr(mth, ((InsnWrapArg) arg).getWrapInsn());
}
// method invoke
return addInlineAttr(mth, insn);
@@ -1,7 +1,5 @@
package jadx.core.dex.visitors;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -19,10 +17,9 @@ import jadx.api.plugins.input.data.attributes.types.AnnotationsAttr;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.AttrNode;
import jadx.core.dex.attributes.nodes.FieldReplaceAttr;
import jadx.core.dex.attributes.nodes.SkipMethodArgsAttr;
import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.info.FieldInfo;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.instructions.ArithNode;
import jadx.core.dex.instructions.ConstClassNode;
import jadx.core.dex.instructions.ConstStringNode;
@@ -46,6 +43,7 @@ import jadx.core.dex.instructions.mods.TernaryInsn;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.IMethodDetails;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.regions.conditions.IfCondition;
@@ -432,96 +430,39 @@ public class ModVisitor extends AbstractVisitor {
return false;
}
/**
* For args in anonymous constructor invoke apply:
* - forbid inline into constructor call
* - make variables final (compiler require this implicitly)
*/
private static void processAnonymousConstructor(MethodNode mth, ConstructorInsn co) {
MethodInfo callMth = co.getCallMth();
MethodNode callMthNode = mth.root().resolveMethod(callMth);
if (callMthNode == null) {
IMethodDetails callMthDetails = mth.root().getMethodUtils().getMethodDetails(co);
if (!(callMthDetails instanceof MethodNode)) {
return;
}
ClassNode classNode = callMthNode.getParentClass();
if (!classNode.isAnonymous()) {
MethodNode callMth = (MethodNode) callMthDetails;
if (!callMth.contains(AFlag.ANONYMOUS_CONSTRUCTOR) || callMth.contains(AFlag.NO_SKIP_ARGS)) {
return;
}
if (!mth.getParentClass().getInnerClasses().contains(classNode)) {
return;
}
Map<InsnArg, FieldNode> argsMap = getArgsToFieldsMapping(callMthNode, co);
if (argsMap.isEmpty() && !callMthNode.getArgRegs().isEmpty()) {
return;
}
for (Map.Entry<InsnArg, FieldNode> entry : argsMap.entrySet()) {
FieldNode field = entry.getValue();
if (field == null) {
continue;
}
InsnArg arg = entry.getKey();
field.addAttr(new FieldReplaceAttr(arg));
field.add(AFlag.DONT_GENERATE);
if (arg.isRegister()) {
RegisterArg reg = (RegisterArg) arg;
SSAVar sVar = reg.getSVar();
if (sVar != null) {
sVar.getCodeVar().setFinal(true);
SkipMethodArgsAttr attr = callMth.get(AType.SKIP_MTH_ARGS);
if (attr != null) {
int argsCount = Math.min(callMth.getArgRegs().size(), co.getArgsCount());
for (int i = 0; i < argsCount; i++) {
if (attr.isSkip(i)) {
anonymousCallArgMod(co.getArg(i));
}
reg.add(AFlag.DONT_INLINE);
reg.add(AFlag.SKIP_ARG);
}
} else {
// additional info not available apply mods to all args (the safest solution)
co.getArguments().forEach(ModVisitor::anonymousCallArgMod);
}
}
private static Map<InsnArg, FieldNode> getArgsToFieldsMapping(MethodNode callMthNode, ConstructorInsn co) {
Map<InsnArg, FieldNode> map = new LinkedHashMap<>();
MethodInfo callMth = callMthNode.getMethodInfo();
ClassNode cls = callMthNode.getParentClass();
ClassNode parentClass = cls.getParentClass();
List<RegisterArg> argList = callMthNode.getArgRegs();
int startArg = 0;
if (callMth.getArgsCount() != 0 && callMth.getArgumentsTypes().get(0).equals(parentClass.getClassInfo().getType())) {
startArg = 1;
private static void anonymousCallArgMod(InsnArg arg) {
arg.add(AFlag.DONT_INLINE);
if (arg.isRegister()) {
((RegisterArg) arg).getSVar().getCodeVar().setFinal(true);
}
int argsCount = argList.size();
for (int i = startArg; i < argsCount; i++) {
RegisterArg arg = argList.get(i);
InsnNode useInsn = getParentInsnSkipMove(arg);
if (useInsn == null) {
return Collections.emptyMap();
}
FieldNode fieldNode = null;
if (useInsn.getType() == InsnType.IPUT) {
FieldInfo field = (FieldInfo) ((IndexInsnNode) useInsn).getIndex();
fieldNode = cls.searchField(field);
if (fieldNode == null || !fieldNode.getAccessFlags().isSynthetic()) {
return Collections.emptyMap();
}
} else if (useInsn.getType() == InsnType.CONSTRUCTOR) {
ConstructorInsn superConstr = (ConstructorInsn) useInsn;
if (!superConstr.isSuper()) {
return Collections.emptyMap();
}
} else {
return Collections.emptyMap();
}
map.put(co.getArg(i), fieldNode);
}
return map;
}
private static InsnNode getParentInsnSkipMove(RegisterArg arg) {
SSAVar sVar = arg.getSVar();
if (sVar.getUseCount() != 1) {
return null;
}
RegisterArg useArg = sVar.getUseList().get(0);
InsnNode parentInsn = useArg.getParentInsn();
if (parentInsn == null) {
return null;
}
if (parentInsn.getType() == InsnType.MOVE) {
return getParentInsnSkipMove(parentInsn.getResult());
}
return parentInsn;
}
/**
@@ -1,11 +1,11 @@
package jadx.core.dex.visitors;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.stream.Collectors;
@@ -17,6 +17,7 @@ import jadx.core.clsp.ClspClass;
import jadx.core.clsp.ClspMethod;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.MethodBridgeAttr;
import jadx.core.dex.attributes.nodes.MethodOverrideAttr;
import jadx.core.dex.attributes.nodes.RenameReasonAttr;
import jadx.core.dex.info.AccessInfo;
@@ -32,6 +33,7 @@ import jadx.core.dex.visitors.typeinference.TypeCompareEnum;
import jadx.core.dex.visitors.typeinference.TypeInferenceVisitor;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxException;
import jadx.core.utils.exceptions.JadxRuntimeException;
@JadxVisitor(
name = "OverrideMethodVisitor",
@@ -45,70 +47,86 @@ public class OverrideMethodVisitor extends AbstractVisitor {
@Override
public boolean visit(ClassNode cls) throws JadxException {
processCls(cls);
SuperTypesData superData = collectSuperTypes(cls);
if (superData != null) {
for (MethodNode mth : cls.getMethods()) {
processMth(mth, superData);
}
}
return true;
}
private void processCls(ClassNode cls) {
List<ArgType> superTypes = collectSuperTypes(cls);
if (!superTypes.isEmpty()) {
for (MethodNode mth : cls.getMethods()) {
processMth(cls, superTypes, mth);
}
}
}
private void processMth(ClassNode cls, List<ArgType> superTypes, MethodNode mth) {
private void processMth(MethodNode mth, SuperTypesData superData) {
if (mth.isConstructor() || mth.getAccessFlags().isStatic() || mth.getAccessFlags().isPrivate()) {
return;
}
MethodOverrideAttr attr = processOverrideMethods(cls, mth, superTypes);
MethodOverrideAttr attr = processOverrideMethods(mth, superData);
if (attr != null) {
if (attr.getBaseMethods().isEmpty()) {
throw new JadxRuntimeException("No base methods for override attribute: " + attr.getOverrideList());
}
mth.addAttr(attr);
IMethodDetails baseMth = Utils.last(attr.getOverrideList());
IMethodDetails baseMth = Utils.getOne(attr.getBaseMethods());
if (baseMth != null) {
boolean updated = fixMethodReturnType(mth, baseMth, superTypes);
updated |= fixMethodArgTypes(mth, baseMth, superTypes);
if (updated && cls.root().getArgs().isRenameValid()) {
boolean updated = fixMethodReturnType(mth, baseMth, superData);
updated |= fixMethodArgTypes(mth, baseMth, superData);
if (updated) {
// check if new signature cause method collisions
fixMethodSignatureCollisions(mth);
checkMethodSignatureCollisions(mth, mth.root().getArgs().isRenameValid());
}
}
}
}
private MethodOverrideAttr processOverrideMethods(ClassNode cls, MethodNode mth, List<ArgType> superTypes) {
private MethodOverrideAttr processOverrideMethods(MethodNode mth, SuperTypesData superData) {
MethodOverrideAttr result = mth.get(AType.METHOD_OVERRIDE);
if (result != null) {
return result;
}
ClassNode cls = mth.getParentClass();
String signature = mth.getMethodInfo().makeSignature(false);
List<IMethodDetails> overrideList = new ArrayList<>();
for (ArgType superType : superTypes) {
ClassNode classNode = cls.root().resolveClass(superType);
Set<IMethodDetails> baseMethods = new HashSet<>();
for (ArgType superType : superData.getSuperTypes()) {
ClassNode classNode = mth.root().resolveClass(superType);
if (classNode != null) {
MethodNode ovrdMth = searchOverriddenMethod(classNode, signature);
if (ovrdMth != null && isMethodVisibleInCls(ovrdMth, cls)) {
overrideList.add(ovrdMth);
MethodOverrideAttr attr = ovrdMth.get(AType.METHOD_OVERRIDE);
if (attr != null) {
return buildOverrideAttr(mth, overrideList, attr);
if (ovrdMth != null) {
if (isMethodVisibleInCls(ovrdMth, cls)) {
overrideList.add(ovrdMth);
MethodOverrideAttr attr = ovrdMth.get(AType.METHOD_OVERRIDE);
if (attr != null) {
addBaseMethod(superData, overrideList, baseMethods, superType);
return buildOverrideAttr(mth, overrideList, baseMethods, attr);
}
}
}
} else {
ClspClass clsDetails = cls.root().getClsp().getClsDetails(superType);
ClspClass clsDetails = mth.root().getClsp().getClsDetails(superType);
if (clsDetails != null) {
Map<String, ClspMethod> methodsMap = clsDetails.getMethodsMap();
for (Map.Entry<String, ClspMethod> entry : methodsMap.entrySet()) {
String mthShortId = entry.getKey();
if (mthShortId.startsWith(signature)) {
overrideList.add(entry.getValue());
break;
}
}
}
}
addBaseMethod(superData, overrideList, baseMethods, superType);
}
return buildOverrideAttr(mth, overrideList, baseMethods, null);
}
private void addBaseMethod(SuperTypesData superData, List<IMethodDetails> overrideList, Set<IMethodDetails> baseMethods,
ArgType superType) {
if (superData.getEndTypes().contains(superType.getObject())) {
IMethodDetails last = Utils.last(overrideList);
if (last != null) {
baseMethods.add(last);
}
}
return buildOverrideAttr(mth, overrideList, null);
}
@Nullable
@@ -123,22 +141,24 @@ public class OverrideMethodVisitor extends AbstractVisitor {
@Nullable
private MethodOverrideAttr buildOverrideAttr(MethodNode mth, List<IMethodDetails> overrideList,
@Nullable MethodOverrideAttr attr) {
Set<IMethodDetails> baseMethods, @Nullable MethodOverrideAttr attr) {
if (overrideList.isEmpty() && attr == null) {
return null;
}
if (attr == null) {
// traced to base method
List<IMethodDetails> cleanOverrideList = overrideList.stream().distinct().collect(Collectors.toList());
return applyOverrideAttr(mth, cleanOverrideList, false);
return applyOverrideAttr(mth, cleanOverrideList, baseMethods, false);
}
// trace stopped at already processed method -> start merging
List<IMethodDetails> mergedOverrideList = Utils.mergeLists(overrideList, attr.getOverrideList());
List<IMethodDetails> cleanOverrideList = mergedOverrideList.stream().distinct().collect(Collectors.toList());
return applyOverrideAttr(mth, cleanOverrideList, true);
Set<IMethodDetails> mergedBaseMethods = Utils.mergeSets(baseMethods, attr.getBaseMethods());
return applyOverrideAttr(mth, cleanOverrideList, mergedBaseMethods, true);
}
private MethodOverrideAttr applyOverrideAttr(MethodNode mth, List<IMethodDetails> overrideList, boolean update) {
private MethodOverrideAttr applyOverrideAttr(MethodNode mth, List<IMethodDetails> overrideList,
Set<IMethodDetails> baseMethods, boolean update) {
// don't rename method if override list contains not resolved method
boolean dontRename = overrideList.stream().anyMatch(m -> !(m instanceof MethodNode));
SortedSet<MethodNode> relatedMethods = null;
@@ -188,10 +208,10 @@ public class OverrideMethodVisitor extends AbstractVisitor {
continue;
}
}
mthNode.addAttr(new MethodOverrideAttr(Utils.listTail(overrideList, depth), relatedMethods));
mthNode.addAttr(new MethodOverrideAttr(Utils.listTail(overrideList, depth), relatedMethods, baseMethods));
depth++;
}
return new MethodOverrideAttr(overrideList, relatedMethods);
return new MethodOverrideAttr(overrideList, relatedMethods, baseMethods);
}
@NotNull
@@ -221,52 +241,92 @@ public class OverrideMethodVisitor extends AbstractVisitor {
return Objects.equals(superMth.getParentClass().getPackage(), cls.getPackage());
}
private List<ArgType> collectSuperTypes(ClassNode cls) {
Map<String, ArgType> superTypes = new LinkedHashMap<>();
collectSuperTypes(cls, superTypes);
if (superTypes.isEmpty()) {
return Collections.emptyList();
private static final class SuperTypesData {
private final List<ArgType> superTypes;
private final Set<String> endTypes;
private SuperTypesData(List<ArgType> superTypes, Set<String> endTypes) {
this.superTypes = superTypes;
this.endTypes = endTypes;
}
public List<ArgType> getSuperTypes() {
return superTypes;
}
public Set<String> getEndTypes() {
return endTypes;
}
return new ArrayList<>(superTypes.values());
}
private void collectSuperTypes(ClassNode cls, Map<String, ArgType> superTypes) {
@Nullable
private SuperTypesData collectSuperTypes(ClassNode cls) {
List<ArgType> superTypes = new ArrayList<>();
Set<String> endTypes = new HashSet<>();
collectSuperTypes(cls, superTypes, endTypes);
if (superTypes.isEmpty()) {
return null;
}
if (endTypes.isEmpty()) {
throw new JadxRuntimeException("No end types in class hierarchy: " + cls);
}
return new SuperTypesData(superTypes, endTypes);
}
private void collectSuperTypes(ClassNode cls, List<ArgType> superTypes, Set<String> endTypes) {
RootNode root = cls.root();
int k = 0;
ArgType superClass = cls.getSuperClass();
if (superClass != null && !Objects.equals(superClass, ArgType.OBJECT)) {
addSuperType(root, superTypes, superClass);
if (superClass != null) {
k += addSuperType(root, superTypes, endTypes, superClass);
}
for (ArgType iface : cls.getInterfaces()) {
addSuperType(root, superTypes, iface);
k += addSuperType(root, superTypes, endTypes, iface);
}
if (k == 0) {
endTypes.add(cls.getType().getObject());
}
}
private void addSuperType(RootNode root, Map<String, ArgType> superTypesMap, ArgType superType) {
superTypesMap.put(superType.getObject(), superType);
private int addSuperType(RootNode root, List<ArgType> superTypesMap, Set<String> endTypes, ArgType superType) {
if (Objects.equals(superType, ArgType.OBJECT)) {
return 0;
}
superTypesMap.add(superType);
ClassNode classNode = root.resolveClass(superType);
if (classNode == null) {
for (String superCls : root.getClsp().getSuperTypes(superType.getObject())) {
ArgType type = ArgType.object(superCls);
superTypesMap.put(type.getObject(), type);
}
} else {
collectSuperTypes(classNode, superTypesMap);
if (classNode != null) {
collectSuperTypes(classNode, superTypesMap, endTypes);
return 1;
}
ClspClass clsDetails = root.getClsp().getClsDetails(superType);
if (clsDetails != null) {
int k = 0;
for (ArgType parentType : clsDetails.getParents()) {
k += addSuperType(root, superTypesMap, endTypes, parentType);
}
if (k == 0) {
endTypes.add(superType.getObject());
}
return 1;
}
// no info found => treat as hierarchy end
endTypes.add(superType.getObject());
return 1;
}
private boolean fixMethodReturnType(MethodNode mth, IMethodDetails baseMth, List<ArgType> superTypes) {
private boolean fixMethodReturnType(MethodNode mth, IMethodDetails baseMth, SuperTypesData superData) {
ArgType returnType = mth.getReturnType();
if (returnType == ArgType.VOID) {
return false;
}
boolean updated = updateReturnType(mth, baseMth, superTypes);
boolean updated = updateReturnType(mth, baseMth, superData);
if (updated) {
mth.addDebugComment("Return type fixed from '" + returnType + "' to match base method");
}
return updated;
}
private boolean updateReturnType(MethodNode mth, IMethodDetails baseMth, List<ArgType> superTypes) {
private boolean updateReturnType(MethodNode mth, IMethodDetails baseMth, SuperTypesData superData) {
ArgType baseReturnType = baseMth.getReturnType();
if (mth.getReturnType().equals(baseReturnType)) {
return false;
@@ -276,7 +336,7 @@ public class OverrideMethodVisitor extends AbstractVisitor {
}
TypeCompare typeCompare = mth.root().getTypeUpdate().getTypeCompare();
ArgType baseCls = baseMth.getMethodInfo().getDeclClass().getType();
for (ArgType superType : superTypes) {
for (ArgType superType : superData.getSuperTypes()) {
TypeCompareEnum compareResult = typeCompare.compareTypes(superType, baseCls);
if (compareResult == TypeCompareEnum.NARROW_BY_GENERIC) {
ArgType targetRetType = mth.root().getTypeUtils().replaceClassGenerics(superType, baseReturnType);
@@ -291,7 +351,7 @@ public class OverrideMethodVisitor extends AbstractVisitor {
return false;
}
private boolean fixMethodArgTypes(MethodNode mth, IMethodDetails baseMth, List<ArgType> superTypes) {
private boolean fixMethodArgTypes(MethodNode mth, IMethodDetails baseMth, SuperTypesData superData) {
List<ArgType> mthArgTypes = mth.getArgTypes();
List<ArgType> baseArgTypes = baseMth.getArgTypes();
if (mthArgTypes.equals(baseArgTypes)) {
@@ -304,7 +364,7 @@ public class OverrideMethodVisitor extends AbstractVisitor {
boolean changed = false;
List<ArgType> newArgTypes = new ArrayList<>(argCount);
for (int argNum = 0; argNum < argCount; argNum++) {
ArgType newType = updateArgType(mth, baseMth, superTypes, argNum);
ArgType newType = updateArgType(mth, baseMth, superData, argNum);
if (newType != null) {
changed = true;
newArgTypes.add(newType);
@@ -318,7 +378,7 @@ public class OverrideMethodVisitor extends AbstractVisitor {
return changed;
}
private ArgType updateArgType(MethodNode mth, IMethodDetails baseMth, List<ArgType> superTypes, int argNum) {
private ArgType updateArgType(MethodNode mth, IMethodDetails baseMth, SuperTypesData superData, int argNum) {
ArgType arg = mth.getArgTypes().get(argNum);
ArgType baseArg = baseMth.getArgTypes().get(argNum);
if (arg.equals(baseArg)) {
@@ -329,7 +389,7 @@ public class OverrideMethodVisitor extends AbstractVisitor {
}
TypeCompare typeCompare = mth.root().getTypeUpdate().getTypeCompare();
ArgType baseCls = baseMth.getMethodInfo().getDeclClass().getType();
for (ArgType superType : superTypes) {
for (ArgType superType : superData.getSuperTypes()) {
TypeCompareEnum compareResult = typeCompare.compareTypes(superType, baseCls);
if (compareResult == TypeCompareEnum.NARROW_BY_GENERIC) {
ArgType targetArgType = mth.root().getTypeUtils().replaceClassGenerics(superType, baseArg);
@@ -343,7 +403,7 @@ public class OverrideMethodVisitor extends AbstractVisitor {
return null;
}
private void fixMethodSignatureCollisions(MethodNode mth) {
private void checkMethodSignatureCollisions(MethodNode mth, boolean rename) {
String mthName = mth.getMethodInfo().getAlias();
String newSignature = MethodInfo.makeShortId(mthName, mth.getArgTypes(), null);
for (MethodNode otherMth : mth.getParentClass().getMethods()) {
@@ -351,12 +411,16 @@ public class OverrideMethodVisitor extends AbstractVisitor {
if (otherMthName.equals(mthName) && otherMth != mth) {
String otherSignature = otherMth.getMethodInfo().makeSignature(true, false);
if (otherSignature.equals(newSignature)) {
if (otherMth.contains(AFlag.DONT_RENAME) || otherMth.contains(AType.METHOD_OVERRIDE)) {
otherMth.addWarnComment("Can't rename method to resolve collision");
} else {
otherMth.getMethodInfo().setAlias(makeNewAlias(otherMth));
otherMth.addAttr(new RenameReasonAttr("avoid collision after fix types in other method"));
if (rename) {
if (otherMth.contains(AFlag.DONT_RENAME) || otherMth.contains(AType.METHOD_OVERRIDE)) {
otherMth.addWarnComment("Can't rename method to resolve collision");
} else {
otherMth.getMethodInfo().setAlias(makeNewAlias(otherMth));
otherMth.addAttr(new RenameReasonAttr("avoid collision after fix types in other method"));
}
}
otherMth.addAttr(new MethodBridgeAttr(mth));
return;
}
}
}
@@ -17,6 +17,7 @@ import jadx.core.dex.attributes.nodes.LineAttrNode;
import jadx.core.dex.instructions.ArithNode;
import jadx.core.dex.instructions.ArithOp;
import jadx.core.dex.instructions.InsnType;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.InsnWrapArg;
import jadx.core.dex.instructions.args.RegisterArg;
@@ -69,6 +70,7 @@ public class PrepareForCodeGen extends AbstractVisitor {
checkInline(block);
removeParenthesis(block);
modifyArith(block);
checkConstUsage(block);
}
moveConstructorInConstructor(mth);
}
@@ -122,6 +124,38 @@ public class PrepareForCodeGen extends AbstractVisitor {
}
}
/**
* Add explicit type for non int constants
*/
private static void checkConstUsage(BlockNode block) {
for (InsnNode blockInsn : block.getInstructions()) {
blockInsn.visitInsns(insn -> {
if (forbidExplicitType(insn.getType())) {
return;
}
for (InsnArg arg : insn.getArguments()) {
if (arg.isLiteral() && arg.getType() != ArgType.INT) {
arg.add(AFlag.EXPLICIT_PRIMITIVE_TYPE);
}
}
});
}
}
private static boolean forbidExplicitType(InsnType type) {
switch (type) {
case CONST:
case CAST:
case IF:
case FILLED_NEW_ARRAY:
case APUT:
case ARITH:
return true;
default:
return false;
}
}
private static void removeParenthesis(BlockNode block) {
for (InsnNode insn : block.getInstructions()) {
removeParenthesis(insn);
@@ -1,11 +1,19 @@
package jadx.core.dex.visitors;
import java.util.List;
import org.jetbrains.annotations.Nullable;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.nodes.AnonymousClassBaseAttr;
import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.visitors.usage.UsageInfoVisitor;
import jadx.core.utils.ListUtils;
import jadx.core.utils.exceptions.JadxException;
@JadxVisitor(
@@ -34,78 +42,136 @@ public class ProcessAnonymous extends AbstractVisitor {
}
private static void markAnonymousClass(ClassNode cls) {
if (usedOnlyOnce(cls) || isAnonymous(cls) || isLambdaCls(cls)) {
if (isStaticFieldUsedOutside(cls)) {
return;
}
cls.add(AFlag.ANONYMOUS_CLASS);
cls.add(AFlag.DONT_GENERATE);
boolean synthetic = cls.getAccessFlags().isSynthetic()
|| cls.getClassInfo().getShortName().contains("$")
|| Character.isDigit(cls.getClassInfo().getShortName().charAt(0));
if (!synthetic) {
return;
}
MethodNode anonymousConstructor = checkUsage(cls);
if (anonymousConstructor == null) {
return;
}
ArgType baseType = getBaseType(cls);
if (baseType == null) {
return;
}
for (MethodNode mth : cls.getMethods()) {
if (mth.isConstructor()) {
mth.add(AFlag.ANONYMOUS_CONSTRUCTOR);
}
}
cls.add(AFlag.ANONYMOUS_CLASS);
cls.addAttr(new AnonymousClassBaseAttr(baseType));
cls.add(AFlag.DONT_GENERATE);
anonymousConstructor.add(AFlag.ANONYMOUS_CONSTRUCTOR);
// force anonymous class to be processed before outer class,
// actual usage of outer class will be removed at anonymous class process,
// see ModVisitor.processAnonymousConstructor method
ClassNode outerCls = anonymousConstructor.getUseIn().get(0).getParentClass();
ClassNode topOuterCls = outerCls.getTopParentClass();
ListUtils.safeRemove(cls.getDependencies(), topOuterCls);
ListUtils.safeRemove(outerCls.getUseIn(), cls);
// move dependency to codegen stage
if (cls.isTopClass()) {
topOuterCls.setDependencies(ListUtils.safeRemoveAndTrim(topOuterCls.getDependencies(), cls));
topOuterCls.setCodegenDeps(ListUtils.safeAdd(topOuterCls.getCodegenDeps(), cls));
}
}
private static boolean isStaticFieldUsedOutside(ClassNode cls) {
ClassNode topCls = cls.getTopParentClass();
/**
* Checks:
* - class have only one constructor which used only once (allow common code for field init)
* - methods or fields not used outside (allow only nested inner classes with synthetic usage)
*
* @return anonymous constructor method
*/
private static MethodNode checkUsage(ClassNode cls) {
MethodNode ctr = ListUtils.filterOnlyOne(cls.getMethods(), MethodNode::isConstructor);
if (ctr == null) {
return null;
}
if (ctr.getUseIn().size() != 1) {
// check if used in common field init in all constructors
if (!checkForCommonFieldInit(ctr)) {
return null;
}
}
MethodNode ctrUseMth = ctr.getUseIn().get(0);
ClassNode ctrUseCls = ctrUseMth.getParentClass();
if (ctrUseCls.equals(cls)) {
// exclude self usage
return null;
}
for (MethodNode mth : cls.getMethods()) {
if (mth == ctr) {
continue;
}
for (MethodNode useMth : mth.getUseIn()) {
if (useMth.equals(ctrUseMth)) {
continue;
}
if (badMethodUsage(cls, useMth, mth.getAccessFlags())) {
return null;
}
}
}
for (FieldNode field : cls.getFields()) {
if (field.isStatic()) {
for (MethodNode useMth : field.getUseIn()) {
ClassNode useCls = useMth.getParentClass().getTopParentClass();
if (!useCls.equals(topCls)) {
return true;
}
for (MethodNode useMth : field.getUseIn()) {
if (badMethodUsage(cls, useMth, field.getAccessFlags())) {
return null;
}
}
}
return false;
return ctr;
}
private static boolean usedOnlyOnce(ClassNode cls) {
if (cls.getUseIn().size() == 1 && cls.getUseInMth().size() == 1) {
// used only once
boolean synthetic = cls.getAccessFlags().isSynthetic() || cls.getClassInfo().getShortName().contains("$");
if (synthetic) {
// must have only one constructor which used only once
MethodNode ctr = null;
for (MethodNode mth : cls.getMethods()) {
if (mth.isConstructor()) {
if (ctr != null) {
ctr = null;
break;
}
ctr = mth;
}
}
return ctr != null && ctr.getUseIn().size() == 1;
}
private static boolean badMethodUsage(ClassNode cls, MethodNode useMth, AccessInfo accessFlags) {
ClassNode useCls = useMth.getParentClass();
if (useCls.equals(cls)) {
return false;
}
return false;
}
private static boolean isAnonymous(ClassNode cls) {
return cls.getClassInfo().isInner()
&& Character.isDigit(cls.getClassInfo().getShortName().charAt(0))
&& cls.getMethods().stream().filter(MethodNode::isConstructor).count() == 1;
}
private static boolean isLambdaCls(ClassNode cls) {
return cls.getAccessFlags().isSynthetic()
&& cls.getAccessFlags().isFinal()
&& cls.getClassInfo().getRawName().contains(".-$$Lambda$")
&& countStaticFields(cls) == 0;
}
private static int countStaticFields(ClassNode cls) {
int c = 0;
for (FieldNode field : cls.getFields()) {
if (field.getAccessFlags().isStatic()) {
c++;
}
if (accessFlags.isSynthetic()) {
// allow synthetic usage in inner class
return !useCls.getParentClass().equals(cls);
}
return c;
return true;
}
/**
* Checks:
* + all in constructors
* + all usage in one class
* - same field put (ignored: methods not loaded yet)
*/
private static boolean checkForCommonFieldInit(MethodNode ctrMth) {
List<MethodNode> ctrUse = ctrMth.getUseIn();
if (ctrUse.isEmpty()) {
return false;
}
ClassNode firstUseCls = ctrUse.get(0).getParentClass();
return ListUtils.allMatch(ctrUse, m -> m.isConstructor() && m.getParentClass().equals(firstUseCls));
}
@Nullable
private static ArgType getBaseType(ClassNode cls) {
int interfacesCount = cls.getInterfaces().size();
if (interfacesCount > 1) {
return null;
}
ArgType superCls = cls.getSuperClass();
if (superCls == null || superCls.equals(ArgType.OBJECT)) {
if (interfacesCount == 1) {
return cls.getInterfaces().get(0);
}
return ArgType.OBJECT;
}
if (interfacesCount == 0) {
return superCls;
}
// check if super class already implement that interface (weird case)
ArgType interfaceType = cls.getInterfaces().get(0);
if (cls.root().getClsp().isImplements(superCls.getObject(), interfaceType.getObject())) {
return superCls;
}
return null;
}
}
@@ -0,0 +1,60 @@
package jadx.core.dex.visitors;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.visitors.usage.UsageInfoVisitor;
import jadx.core.utils.ListUtils;
import jadx.core.utils.exceptions.JadxException;
@JadxVisitor(
name = "ProcessMethodsForInline",
desc = "Mark methods for future inline",
runAfter = {
UsageInfoVisitor.class
}
)
public class ProcessMethodsForInline extends AbstractVisitor {
private boolean inlineMethods;
@Override
public void init(RootNode root) {
inlineMethods = root.getArgs().isInlineMethods();
}
@Override
public boolean visit(ClassNode cls) throws JadxException {
if (!inlineMethods) {
return false;
}
for (MethodNode mth : cls.getMethods()) {
if (canInline(mth)) {
mth.add(AFlag.METHOD_CANDIDATE_FOR_INLINE);
fixClassDependencies(mth);
}
}
return true;
}
private static boolean canInline(MethodNode mth) {
if (mth.isNoCode() || mth.contains(AFlag.DONT_GENERATE)) {
return false;
}
AccessInfo accessFlags = mth.getAccessFlags();
boolean isSynthetic = accessFlags.isSynthetic() || mth.getName().contains("$");
return isSynthetic && accessFlags.isStatic();
}
private static void fixClassDependencies(MethodNode mth) {
ClassNode parentClass = mth.getTopParentClass();
for (MethodNode useInMth : mth.getUseIn()) {
// remove possible cross dependency to force class with inline method to be processed before its
// usage
ClassNode useTopCls = useInMth.getTopParentClass();
parentClass.setDependencies(ListUtils.safeRemoveAndTrim(parentClass.getDependencies(), useTopCls));
}
}
}
@@ -154,7 +154,7 @@ public class SignatureProcessor extends AbstractVisitor {
return newArgTypes;
}
}
mth.addWarnComment("Incorrect args count in method signature: " + sp.getSignature());
mth.addDebugComment("Incorrect args count in method signature: " + sp.getSignature());
return null;
}
for (int i = 0; i < len; i++) {
@@ -38,6 +38,7 @@ import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.regions.conditions.IfCondition;
import jadx.core.dex.visitors.shrink.CodeShrinkVisitor;
import jadx.core.dex.visitors.typeinference.TypeCompareEnum;
import jadx.core.utils.BlockUtils;
import jadx.core.utils.InsnList;
import jadx.core.utils.InsnRemover;
@@ -82,7 +83,7 @@ public class SimplifyVisitor extends AbstractVisitor {
for (int i = 0; i < list.size(); i++) {
InsnNode insn = list.get(i);
int insnCount = list.size();
InsnNode modInsn = simplifyInsn(mth, insn);
InsnNode modInsn = simplifyInsn(mth, insn, null);
if (modInsn != null) {
modInsn.rebindArgs();
if (i < list.size() && list.get(i) == insn) {
@@ -110,7 +111,7 @@ public class SimplifyVisitor extends AbstractVisitor {
for (InsnArg arg : insn.getArguments()) {
if (arg.isInsnWrap()) {
InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn();
InsnNode replaceInsn = simplifyInsn(mth, wrapInsn);
InsnNode replaceInsn = simplifyInsn(mth, wrapInsn, insn);
if (replaceInsn != null) {
arg.wrapInstruction(mth, replaceInsn);
InsnRemover.unbindInsn(mth, wrapInsn);
@@ -123,7 +124,7 @@ public class SimplifyVisitor extends AbstractVisitor {
}
}
private InsnNode simplifyInsn(MethodNode mth, InsnNode insn) {
private InsnNode simplifyInsn(MethodNode mth, InsnNode insn, @Nullable InsnNode parentInsn) {
if (insn.contains(AFlag.DONT_GENERATE)) {
return null;
}
@@ -146,8 +147,9 @@ public class SimplifyVisitor extends AbstractVisitor {
case SPUT:
return convertFieldArith(mth, insn);
case CAST:
case CHECK_CAST:
return processCast(mth, (IndexInsnNode) insn);
return processCast(mth, (IndexInsnNode) insn, parentInsn);
case MOVE:
InsnArg firstArg = insn.getArg(0);
@@ -212,7 +214,7 @@ public class SimplifyVisitor extends AbstractVisitor {
return null;
}
private static InsnNode processCast(MethodNode mth, IndexInsnNode castInsn) {
private static InsnNode processCast(MethodNode mth, IndexInsnNode castInsn, @Nullable InsnNode parentInsn) {
if (castInsn.contains(AFlag.EXPLICIT_CAST)) {
return null;
}
@@ -229,7 +231,8 @@ public class SimplifyVisitor extends AbstractVisitor {
ArgType castToType = (ArgType) castInsn.getIndex();
if (!ArgType.isCastNeeded(mth.root(), argType, castToType)
|| isCastDuplicate(castInsn)) {
|| isCastDuplicate(castInsn)
|| shadowedByOuterCast(mth.root(), castToType, parentInsn)) {
InsnNode insnNode = new InsnNode(InsnType.MOVE, 1);
insnNode.setOffset(castInsn.getOffset());
insnNode.setResult(castInsn.getResult());
@@ -254,6 +257,15 @@ public class SimplifyVisitor extends AbstractVisitor {
return false;
}
private static boolean shadowedByOuterCast(RootNode root, ArgType castType, @Nullable InsnNode parentInsn) {
if (parentInsn != null && parentInsn.getType() == InsnType.CAST) {
ArgType parentCastType = (ArgType) ((IndexInsnNode) parentInsn).getIndex();
TypeCompareEnum result = root.getTypeCompare().compareTypes(parentCastType, castType);
return result.isNarrow();
}
return false;
}
/**
* Simplify 'cmp' instruction in if condition
*/
@@ -532,32 +544,44 @@ public class SimplifyVisitor extends AbstractVisitor {
if (arith.getArgsCount() != 2) {
return null;
}
InsnArg litArg = null;
LiteralArg litArg = null;
InsnArg secondArg = arith.getArg(1);
if (secondArg.isInsnWrap()) {
InsnNode wr = ((InsnWrapArg) secondArg).getWrapInsn();
if (wr.getType() == InsnType.CONST) {
litArg = wr.getArg(0);
InsnArg arg = wr.getArg(0);
if (arg.isLiteral()) {
litArg = (LiteralArg) arg;
}
}
} else if (secondArg.isLiteral()) {
litArg = secondArg;
litArg = (LiteralArg) secondArg;
}
if (litArg != null) {
long lit = ((LiteralArg) litArg).getLiteral();
// fix 'c + (-1)' => 'c - (1)'
if (arith.getOp() == ArithOp.ADD && lit < 0) {
return new ArithNode(ArithOp.SUB,
arith.getResult(), arith.getArg(0),
InsnArg.lit(-lit, litArg.getType()));
}
InsnArg firstArg = arith.getArg(0);
if (arith.getOp() == ArithOp.XOR && firstArg.getType() == ArgType.BOOLEAN
&& (lit == 0 || lit == 1)) {
InsnNode node = new InsnNode(lit == 0 ? InsnType.MOVE : InsnType.NOT, 1);
node.setResult(arith.getResult());
node.addArg(firstArg);
return node;
}
if (litArg == null) {
return null;
}
switch (arith.getOp()) {
case ADD:
// fix 'c + (-1)' to 'c - (1)'
if (litArg.isNegative()) {
LiteralArg negLitArg = litArg.negate();
if (negLitArg != null) {
return new ArithNode(ArithOp.SUB, arith.getResult(), arith.getArg(0), negLitArg);
}
}
break;
case XOR:
// simplify xor on boolean
InsnArg firstArg = arith.getArg(0);
long lit = litArg.getLiteral();
if (firstArg.getType() == ArgType.BOOLEAN && (lit == 0 || lit == 1)) {
InsnNode node = new InsnNode(lit == 0 ? InsnType.MOVE : InsnType.NOT, 1);
node.setResult(arith.getResult());
node.addArg(firstArg);
return node;
}
break;
}
return null;
}
@@ -0,0 +1,226 @@
package jadx.core.dex.visitors.kotlin;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.JadxArgs.UseKotlinMethodsForVarNames;
import jadx.api.plugins.input.data.attributes.JadxAttrType;
import jadx.core.deobf.NameMapper;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.info.FieldInfo;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.instructions.ConstStringNode;
import jadx.core.dex.instructions.IndexInsnNode;
import jadx.core.dex.instructions.InsnType;
import jadx.core.dex.instructions.InvokeNode;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.InsnWrapArg;
import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.visitors.AbstractVisitor;
import jadx.core.dex.visitors.InitCodeVariables;
import jadx.core.dex.visitors.JadxVisitor;
import jadx.core.dex.visitors.debuginfo.DebugInfoApplyVisitor;
import jadx.core.dex.visitors.rename.CodeRenameVisitor;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxException;
@JadxVisitor(
name = "ProcessKotlinInternals",
desc = "Use variable names from Kotlin intrinsic1 methods",
runAfter = {
InitCodeVariables.class,
DebugInfoApplyVisitor.class
},
runBefore = {
CodeRenameVisitor.class
}
)
public class ProcessKotlinInternals extends AbstractVisitor {
private static final Logger LOG = LoggerFactory.getLogger(ProcessKotlinInternals.class);
private static final String KOTLIN_INTERNAL_PKG = "kotlin.jvm.internal.";
private static final String KOTLIN_INTRINSICS_CLS = KOTLIN_INTERNAL_PKG + "Intrinsics";
private static final String KOTLIN_VARNAME_SOURCE_MTH1 = "(Ljava/lang/Object;Ljava/lang/String;)V";
private static final String KOTLIN_VARNAME_SOURCE_MTH2 = "(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)V";
private @Nullable ClassInfo kotlinIntrinsicsCls;
private Set<MethodInfo> kotlinVarNameSourceMethods;
private boolean hideInsns;
@Override
public void init(RootNode root) throws JadxException {
ClassNode kotlinCls = searchKotlinIntrinsicsClass(root);
if (kotlinCls != null) {
kotlinIntrinsicsCls = kotlinCls.getClassInfo();
kotlinVarNameSourceMethods = collectMethods(kotlinCls);
LOG.debug("Kotlin Intrinsics class: {}, methods: {}", kotlinCls, kotlinVarNameSourceMethods.size());
} else {
kotlinIntrinsicsCls = null;
LOG.debug("Kotlin Intrinsics class not found");
}
hideInsns = root.getArgs().getUseKotlinMethodsForVarNames() == UseKotlinMethodsForVarNames.APPLY_AND_HIDE;
}
@Override
public boolean visit(ClassNode cls) {
if (kotlinIntrinsicsCls == null) {
return false;
}
for (MethodNode mth : cls.getMethods()) {
processMth(mth);
}
return true;
}
private void processMth(MethodNode mth) {
if (mth.isNoCode()) {
return;
}
for (BlockNode block : mth.getBasicBlocks()) {
for (InsnNode insn : block.getInstructions()) {
if (insn.getType() == InsnType.INVOKE) {
try {
processInvoke(mth, insn);
} catch (Exception e) {
mth.addWarnComment("Failed to extract var names", e);
}
}
}
}
}
private void processInvoke(MethodNode mth, InsnNode insn) {
int argsCount = insn.getArgsCount();
if (argsCount < 2) {
return;
}
MethodInfo invokeMth = ((InvokeNode) insn).getCallMth();
if (!kotlinVarNameSourceMethods.contains(invokeMth)) {
return;
}
InsnArg firstArg = insn.getArg(0);
if (!firstArg.isRegister()) {
return;
}
RegisterArg varArg = (RegisterArg) firstArg;
boolean renamed = false;
if (argsCount == 2) {
String str = getConstString(mth, insn, 1);
if (str != null) {
renamed = checkAndRename(varArg, str);
}
} else if (argsCount == 3) {
// TODO: use second arg for rename class
String str = getConstString(mth, insn, 2);
if (str != null) {
renamed = checkAndRename(varArg, str);
}
}
if (renamed && hideInsns) {
insn.add(AFlag.DONT_GENERATE);
}
}
private boolean checkAndRename(RegisterArg arg, String str) {
String name = trimName(str);
if (NameMapper.isValidAndPrintable(name)) {
arg.getSVar().getCodeVar().setName(name);
return true;
}
return false;
}
@Nullable
private String getConstString(MethodNode mth, InsnNode insn, int arg) {
InsnArg strArg = insn.getArg(arg);
if (!strArg.isInsnWrap()) {
return null;
}
InsnNode constInsn = ((InsnWrapArg) strArg).getWrapInsn();
InsnType insnType = constInsn.getType();
if (insnType == InsnType.CONST_STR) {
return ((ConstStringNode) constInsn).getString();
}
if (insnType == InsnType.SGET) {
// revert const field inline :(
FieldInfo fieldInfo = (FieldInfo) ((IndexInsnNode) constInsn).getIndex();
FieldNode fieldNode = mth.root().resolveField(fieldInfo);
if (fieldNode != null) {
String str = (String) fieldNode.get(JadxAttrType.CONSTANT_VALUE).getValue();
InsnArg newArg = InsnArg.wrapArg(new ConstStringNode(str));
insn.replaceArg(strArg, newArg);
return str;
}
}
return null;
}
private String trimName(String str) {
if (str.startsWith("$this$")) {
return str.substring(6);
}
if (str.startsWith("$")) {
return str.substring(1);
}
return str;
}
@Nullable
private static ClassNode searchKotlinIntrinsicsClass(RootNode root) {
ClassNode kotlinCls = root.resolveClass(KOTLIN_INTRINSICS_CLS);
if (kotlinCls != null) {
return kotlinCls;
}
List<ClassNode> candidates = new ArrayList<>();
for (ClassNode cls : root.getClasses()) {
if (isKotlinIntrinsicsClass(cls)) {
candidates.add(cls);
}
}
return Utils.getOne(candidates);
}
private static boolean isKotlinIntrinsicsClass(ClassNode cls) {
if (!cls.getClassInfo().getFullName().startsWith(KOTLIN_INTERNAL_PKG)) {
return false;
}
if (cls.getMethods().size() < 5) {
return false;
}
int mthCount = 0;
for (MethodNode mth : cls.getMethods()) {
if (mth.getAccessFlags().isStatic()
&& mth.getMethodInfo().getShortId().endsWith(KOTLIN_VARNAME_SOURCE_MTH1)) {
mthCount++;
}
}
return mthCount > 2;
}
private Set<MethodInfo> collectMethods(ClassNode kotlinCls) {
Set<MethodInfo> set = new HashSet<>();
for (MethodNode mth : kotlinCls.getMethods()) {
if (!mth.getAccessFlags().isStatic()) {
continue;
}
String shortId = mth.getMethodInfo().getShortId();
if (shortId.endsWith(KOTLIN_VARNAME_SOURCE_MTH1) || shortId.endsWith(KOTLIN_VARNAME_SOURCE_MTH2)) {
set.add(mth.getMethodInfo());
}
}
return set;
}
}
@@ -98,6 +98,10 @@ public class TypeCompare {
|| secondPrimitiveType == PrimitiveType.BOOLEAN) {
return CONFLICT;
}
if (swapEquals(firstPrimitiveType, secondPrimitiveType, PrimitiveType.CHAR, PrimitiveType.BYTE)
|| swapEquals(firstPrimitiveType, secondPrimitiveType, PrimitiveType.CHAR, PrimitiveType.SHORT)) {
return CONFLICT;
}
return firstPrimitiveType.compareTo(secondPrimitiveType) > 0 ? WIDER : NARROW;
}
@@ -105,6 +109,10 @@ public class TypeCompare {
return TypeCompareEnum.CONFLICT;
}
private boolean swapEquals(PrimitiveType first, PrimitiveType second, PrimitiveType a, PrimitiveType b) {
return (first == a && second == b) || (first == b && second == a);
}
private TypeCompareEnum compareArrayWithOtherType(ArgType array, ArgType other) {
if (!other.isTypeKnown()) {
if (other.contains(PrimitiveType.ARRAY)) {
@@ -19,6 +19,7 @@ import jadx.core.Consts;
import jadx.core.clsp.ClspGraph;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.AnonymousClassBaseAttr;
import jadx.core.dex.attributes.nodes.PhiListAttr;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.instructions.ArithNode;
@@ -35,12 +36,15 @@ import jadx.core.dex.instructions.args.LiteralArg;
import jadx.core.dex.instructions.args.PrimitiveType;
import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.instructions.args.SSAVar;
import jadx.core.dex.instructions.mods.ConstructorInsn;
import jadx.core.dex.instructions.mods.TernaryInsn;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.IMethodDetails;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.nodes.utils.MethodUtils;
import jadx.core.dex.trycatch.ExcHandlerAttr;
import jadx.core.dex.visitors.AbstractVisitor;
import jadx.core.dex.visitors.AttachMethodDetails;
@@ -273,6 +277,11 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
addBound(typeInfo, new TypeBoundConst(BoundEnum.ASSIGN, clsType));
break;
case CONSTRUCTOR:
ArgType ctrClsType = replaceAnonymousType((ConstructorInsn) insn);
addBound(typeInfo, new TypeBoundConst(BoundEnum.ASSIGN, ctrClsType));
break;
case CONST:
LiteralArg constLit = (LiteralArg) insn.getArg(0);
addBound(typeInfo, new TypeBoundConst(BoundEnum.ASSIGN, constLit.getType()));
@@ -308,6 +317,19 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
}
}
private ArgType replaceAnonymousType(ConstructorInsn ctr) {
if (ctr.isNewInstance()) {
ClassNode ctrCls = root.resolveClass(ctr.getClassType());
if (ctrCls != null && ctrCls.contains(AFlag.DONT_GENERATE)) {
AnonymousClassBaseAttr baseTypeAttr = ctrCls.get(AType.ANONYMOUS_CLASS_BASE);
if (baseTypeAttr != null) {
return baseTypeAttr.getBaseType();
}
}
}
return ctr.getClassType().getType();
}
private ITypeBound makeAssignFieldGetBound(IndexInsnNode insn) {
ArgType initType = insn.getResult().getInitType();
if (initType.containsTypeVariable()) {
@@ -340,7 +362,7 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
return null;
}
if (insn instanceof BaseInvokeNode) {
TypeBoundInvokeUse invokeUseBound = makeInvokeUseBound(regArg, (BaseInvokeNode) insn);
ITypeBound invokeUseBound = makeInvokeUseBound(regArg, (BaseInvokeNode) insn);
if (invokeUseBound != null) {
return invokeUseBound;
}
@@ -352,21 +374,32 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
return new TypeBoundConst(BoundEnum.USE, regArg.getInitType(), regArg);
}
private TypeBoundInvokeUse makeInvokeUseBound(RegisterArg regArg, BaseInvokeNode invoke) {
private ITypeBound makeInvokeUseBound(RegisterArg regArg, BaseInvokeNode invoke) {
InsnArg instanceArg = invoke.getInstanceArg();
if (instanceArg == null || instanceArg == regArg) {
if (instanceArg == null) {
return null;
}
IMethodDetails methodDetails = root.getMethodUtils().getMethodDetails(invoke);
MethodUtils methodUtils = root.getMethodUtils();
IMethodDetails methodDetails = methodUtils.getMethodDetails(invoke);
if (methodDetails == null) {
return null;
}
int argIndex = invoke.getArgIndex(regArg) - invoke.getFirstArgOffset();
ArgType argType = methodDetails.getArgTypes().get(argIndex);
if (!argType.containsTypeVariable()) {
return null;
if (instanceArg != regArg) {
int argIndex = invoke.getArgIndex(regArg) - invoke.getFirstArgOffset();
ArgType argType = methodDetails.getArgTypes().get(argIndex);
if (!argType.containsTypeVariable()) {
return null;
}
return new TypeBoundInvokeUse(root, invoke, regArg, argType);
}
return new TypeBoundInvokeUse(root, invoke, regArg, argType);
// for override methods use origin declared class as type
if (methodDetails instanceof MethodNode) {
MethodNode callMth = (MethodNode) methodDetails;
ClassInfo declCls = methodUtils.getMethodOriginDeclClass(callMth);
return new TypeBoundConst(BoundEnum.USE, declCls.getType(), regArg);
}
return null;
}
private boolean tryPossibleTypes(MethodNode mth, SSAVar var, ArgType type) {
@@ -8,7 +8,6 @@ import java.util.HashSet;
import java.util.Set;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -24,6 +23,7 @@ import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
import jadx.core.xmlgen.ResContainer;
import jadx.core.xmlgen.XmlSecurity;
public class ExportGradleProject {
@@ -139,7 +139,7 @@ public class ExportGradleProject {
private Document parseXml(String xmlContent) {
try {
DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
DocumentBuilder builder = XmlSecurity.getSecureDbf().newDocumentBuilder();
Document document = builder.parse(new InputSource(new StringReader(xmlContent)));
document.getDocumentElement().normalize();
@@ -7,6 +7,7 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Predicate;
import org.jetbrains.annotations.Nullable;
@@ -62,8 +63,75 @@ public class ListUtils {
newList.add(newObj);
return newList;
}
list.remove(oldObj);
list.add(newObj);
int idx = list.indexOf(oldObj);
if (idx != -1) {
list.set(idx, newObj);
} else {
list.add(newObj);
}
return list;
}
public static <T> void safeRemove(List<T> list, T obj) {
if (list != null && !list.isEmpty()) {
list.remove(obj);
}
}
public static <T> List<T> safeRemoveAndTrim(List<T> list, T obj) {
if (list == null || list.isEmpty()) {
return list;
}
if (list.remove(obj)) {
if (list.isEmpty()) {
return Collections.emptyList();
}
}
return list;
}
public static <T> List<T> safeAdd(List<T> list, T obj) {
if (list == null || list.isEmpty()) {
List<T> newList = new ArrayList<>(1);
newList.add(obj);
return newList;
}
list.add(obj);
return list;
}
/**
* Search exactly one element in list by filter
*
* @return null if found not exactly one element (zero or more than one)
*/
@Nullable
public static <T> T filterOnlyOne(List<T> list, Predicate<T> filter) {
if (list == null || list.isEmpty()) {
return null;
}
T found = null;
for (T element : list) {
if (filter.test(element)) {
if (found != null) {
// found second
return null;
}
found = element;
}
}
return found;
}
public static <T> boolean allMatch(List<T> list, Predicate<T> test) {
if (list == null || list.isEmpty()) {
return false;
}
for (T element : list) {
if (!test.test(element)) {
return false;
}
}
return true;
}
}
@@ -8,10 +8,12 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import org.jetbrains.annotations.Nullable;
@@ -284,6 +286,19 @@ public class Utils {
return result;
}
public static <T> Set<T> mergeSets(Set<T> first, Set<T> second) {
if (isEmpty(first)) {
return second;
}
if (isEmpty(second)) {
return first;
}
Set<T> result = new HashSet<>(first.size() + second.size());
result.addAll(first);
result.addAll(second);
return result;
}
public static Map<String, String> newConstStringMap(String... parameters) {
int len = parameters.length;
if (len == 0) {
@@ -338,6 +353,14 @@ public class Utils {
return list.get(0);
}
@Nullable
public static <T> T getOne(@Nullable Collection<T> collection) {
if (collection == null || collection.size() != 1) {
return null;
}
return collection.iterator().next();
}
@Nullable
public static <T> T first(List<T> list) {
if (list.isEmpty()) {
@@ -14,14 +14,11 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.List;
import java.util.Objects;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -75,7 +72,7 @@ public class FileUtils {
public static void makeDirsForFile(Path path) {
if (path != null) {
makeDirs(path.getParent().toFile());
makeDirs(path.toAbsolutePath().getParent().toFile());
}
}
@@ -117,7 +114,11 @@ public class FileUtils {
try (Stream<Path> pathStream = Files.walk(dir)) {
pathStream.sorted(Comparator.reverseOrder())
.map(Path::toFile)
.forEach(File::delete);
.forEach(file -> {
if (!file.delete()) {
LOG.warn("Failed to remove file: {}", file.getAbsolutePath());
}
});
} catch (Exception e) {
throw new JadxRuntimeException("Failed to delete directory " + dir, e);
}
@@ -259,45 +260,6 @@ public class FileUtils {
return false;
}
private static List<String> getZipFileList(File file) {
List<String> filesList = new ArrayList<>();
try (ZipFile zipFile = new ZipFile(file)) {
Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
filesList.add(entry.getName());
}
} catch (Exception e) {
LOG.error("Error read zip file '{}'", file.getAbsolutePath(), e);
}
return filesList;
}
public static boolean isApkFile(File file) {
if (!isZipFile(file)) {
return false;
}
List<String> filesList = getZipFileList(file);
return filesList.contains("AndroidManifest.xml")
&& filesList.contains("classes.dex");
}
public static boolean isZipDexFile(File file) {
if (!isZipFile(file) || !isZipFileCanBeOpen(file)) {
return false;
}
List<String> filesList = getZipFileList(file);
return filesList.contains("classes.dex");
}
private static boolean isZipFileCanBeOpen(File file) {
try (ZipFile zipFile = new ZipFile(file)) {
return zipFile.entries().hasMoreElements();
} catch (Exception e) {
return false;
}
}
public static String getPathBaseName(Path file) {
String fileName = file.getFileName().toString();
int extEndIndex = fileName.lastIndexOf('.');
@@ -63,10 +63,15 @@ public class TypeCompareTest {
public void comparePrimitives() {
check(INT, UNKNOWN_OBJECT, TypeCompareEnum.CONFLICT);
check(INT, OBJECT, TypeCompareEnum.CONFLICT);
check(INT, BOOLEAN, TypeCompareEnum.CONFLICT);
check(INT, CHAR, TypeCompareEnum.WIDER);
check(INT, SHORT, TypeCompareEnum.WIDER);
check(BOOLEAN, INT, TypeCompareEnum.CONFLICT);
check(BOOLEAN, CHAR, TypeCompareEnum.CONFLICT);
check(CHAR, BYTE, TypeCompareEnum.CONFLICT);
check(CHAR, SHORT, TypeCompareEnum.CONFLICT);
firstIsNarrow(CHAR, NARROW_INTEGRAL);
firstIsNarrow(array(CHAR), UNKNOWN_OBJECT);
}
@@ -490,7 +490,7 @@ public abstract class IntegrationTest extends TestUtils {
this.useEclipseCompiler = true;
}
protected void useTargetJavaVersion(int version) {
public void useTargetJavaVersion(int version) {
Assumptions.assumeTrue(JavaUtils.checkJavaVersion(version), "skip test for higher java version");
this.targetJavaVersion = version;
}
@@ -1,20 +0,0 @@
package jadx.tests.api.extensions.inputs;
import java.util.function.Consumer;
import jadx.tests.api.IntegrationTest;
public enum InputPlugin implements Consumer<IntegrationTest> {
DEX {
@Override
public void accept(IntegrationTest test) {
test.useDexInput();
}
},
JAVA {
@Override
public void accept(IntegrationTest test) {
test.useJavaInput();
}
};
}
@@ -1,9 +1,8 @@
package jadx.tests.api.extensions.inputs;
package jadx.tests.api.extensions.profiles;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
@@ -19,38 +18,38 @@ import jadx.tests.api.IntegrationTest;
import static org.junit.platform.commons.util.AnnotationUtils.isAnnotated;
public class JadxInputPluginsExtension implements TestTemplateInvocationContextProvider {
public class JadxTestProfilesExtension implements TestTemplateInvocationContextProvider {
@Override
public boolean supportsTestTemplate(ExtensionContext context) {
return isAnnotated(context.getTestMethod(), TestWithInputPlugins.class);
return isAnnotated(context.getTestMethod(), TestWithProfiles.class);
}
@Override
public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(ExtensionContext context) {
Preconditions.condition(IntegrationTest.class.isAssignableFrom(context.getRequiredTestClass()),
"@TestWithInputPlugins should be used only in IntegrationTest subclasses");
"@TestWithProfiles should be used only in IntegrationTest subclasses");
Method testMethod = context.getRequiredTestMethod();
boolean testAnnAdded = AnnotationUtils.findAnnotation(testMethod, Test.class).isPresent();
Preconditions.condition(!testAnnAdded, "@Test annotation should be removed");
TestWithInputPlugins inputPluginAnn = AnnotationUtils.findAnnotation(testMethod, TestWithInputPlugins.class).get();
return Stream.of(inputPluginAnn.value())
TestWithProfiles profilesAnn = AnnotationUtils.findAnnotation(testMethod, TestWithProfiles.class).get();
return Stream.of(profilesAnn.value())
.sorted()
.map(RunWithInputPlugin::new);
.map(RunWithProfile::new);
}
private static class RunWithInputPlugin implements TestTemplateInvocationContext {
private final InputPlugin plugin;
private static class RunWithProfile implements TestTemplateInvocationContext {
private final TestProfile testProfile;
public RunWithInputPlugin(InputPlugin plugin) {
this.plugin = plugin;
public RunWithProfile(TestProfile testProfile) {
this.testProfile = testProfile;
}
@Override
public String getDisplayName(int invocationIndex) {
return plugin.name().toLowerCase(Locale.ROOT) + " input";
return testProfile.getDescription();
}
@Override
@@ -59,7 +58,7 @@ public class JadxInputPluginsExtension implements TestTemplateInvocationContextP
}
private BeforeTestExecutionCallback beforeTest() {
return execContext -> plugin.accept((IntegrationTest) execContext.getRequiredTestInstance());
return execContext -> testProfile.accept((IntegrationTest) execContext.getRequiredTestInstance());
}
}
}
@@ -0,0 +1,41 @@
package jadx.tests.api.extensions.profiles;
import java.util.function.Consumer;
import jadx.tests.api.IntegrationTest;
public enum TestProfile implements Consumer<IntegrationTest> {
DX_J8("dx-java-8", test -> {
test.useTargetJavaVersion(8);
test.useDexInput();
}),
D8_J11("d8-java-11", test -> {
test.useTargetJavaVersion(11);
test.useDexInput();
}),
JAVA8("java-8", test -> {
test.useTargetJavaVersion(8);
test.useJavaInput();
}),
JAVA11("java-11", test -> {
test.useTargetJavaVersion(11);
test.useJavaInput();
});
private final String description;
private final Consumer<IntegrationTest> setup;
TestProfile(String description, Consumer<IntegrationTest> setup) {
this.description = description;
this.setup = setup;
}
@Override
public void accept(IntegrationTest integrationTest) {
this.setup.accept(integrationTest);
}
public String getDescription() {
return description;
}
}
@@ -1,4 +1,4 @@
package jadx.tests.api.extensions.inputs;
package jadx.tests.api.extensions.profiles;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -9,10 +9,10 @@ import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;
@TestTemplate
@ExtendWith(JadxInputPluginsExtension.class)
@ExtendWith(JadxTestProfilesExtension.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface TestWithInputPlugins {
@Target(ElementType.METHOD)
public @interface TestWithProfiles {
InputPlugin[] value();
TestProfile[] value();
}
@@ -0,0 +1,33 @@
package jadx.tests.integration.arith;
import jadx.tests.api.IntegrationTest;
import jadx.tests.api.extensions.profiles.TestProfile;
import jadx.tests.api.extensions.profiles.TestWithProfiles;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestPrimitivesNegate extends IntegrationTest {
@SuppressWarnings("UnnecessaryUnaryMinus")
public static class TestCls {
public double test() {
double[] arr = new double[5];
arr[0] = -20;
arr[0] += -79;
return arr[0];
}
public void check() {
assertThat(test()).isEqualTo(-99);
}
}
@TestWithProfiles({ TestProfile.DX_J8, TestProfile.JAVA8 })
public void test() {
noDebugInfo();
assertThat(getClassNode(TestCls.class))
.code()
.containsOne("dArr[0] = -20.0d;")
.containsOne("dArr[0] = dArr[0] - 79.0d;");
}
}
@@ -24,6 +24,6 @@ public class TestBooleanToByte extends SmaliTest {
public void test() {
assertThat(getClassNodeFromSmali())
.code()
.containsOne("write(this.showConsent ? (byte) 1 : 0);");
.containsOne("write(this.showConsent ? (byte) 1 : (byte) 0);");
}
}
@@ -24,6 +24,6 @@ public class TestBooleanToChar extends SmaliTest {
public void test() {
assertThat(getClassNodeFromSmali())
.code()
.containsOne("write(this.showConsent ? (char) 1 : 0);");
.containsOne("write(this.showConsent ? (char) 1 : (char) 0);");
}
}
@@ -24,6 +24,6 @@ public class TestBooleanToLong extends SmaliTest {
public void test() {
assertThat(getClassNodeFromSmali())
.code()
.containsOne("write(this.showConsent ? 1 : 0);");
.containsOne("write(this.showConsent ? 1L : 0L);");
}
}
@@ -24,6 +24,6 @@ public class TestBooleanToShort extends SmaliTest {
public void test() {
assertThat(getClassNodeFromSmali())
.code()
.containsOne("write(this.showConsent ? (short) 1 : 0);");
.containsOne("write(this.showConsent ? (short) 1 : (short) 0);");
}
}
@@ -51,12 +51,12 @@ public class TestCast extends IntegrationTest {
ClassNode cls = getClassNode(TestCls.class);
String code = cls.getCode().toString();
assertThat(code, containsString("write(a ? (byte) 0 : 1);"));
assertThat(code, containsString("write(a ? 0 : this.myByte);"));
assertThat(code, containsString("write(a ? 0 : Byte.MAX_VALUE);"));
assertThat(code, containsString("write(a ? (byte) 0 : (byte) 1);"));
assertThat(code, containsString("write(a ? (byte) 0 : this.myByte);"));
assertThat(code, containsString("write(a ? (byte) 0 : Byte.MAX_VALUE);"));
assertThat(code, containsString("write(a ? (short) 0 : 1);"));
assertThat(code, containsString("write(a ? this.myShort : 0);"));
assertThat(code, containsString("write(a ? Short.MIN_VALUE : 0);"));
assertThat(code, containsString("write(a ? (short) 0 : (short) 1);"));
assertThat(code, containsString("write(a ? this.myShort : (short) 0);"));
assertThat(code, containsString("write(a ? Short.MIN_VALUE : (short) 0);"));
}
}
@@ -0,0 +1,39 @@
package jadx.tests.integration.debuginfo;
import jadx.core.dex.nodes.ClassNode;
import jadx.tests.api.IntegrationTest;
import jadx.tests.api.extensions.profiles.TestProfile;
import jadx.tests.api.extensions.profiles.TestWithProfiles;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestLineNumbers3 extends IntegrationTest {
public static class TestCls extends Exception {
public TestCls(final Object message) {
super((message == null) ? "" : message.toString());
/*
* comment to increase line number in return instruction
* -
* -
* -
* -
* -
* -
* -
* -
* -
* -
*/
}
}
@TestWithProfiles({ TestProfile.DX_J8, TestProfile.JAVA8 })
public void test() {
ClassNode cls = getClassNode(TestCls.class);
assertThat(cls).code().containsOne("super(message == null ? \"\" : message.toString());");
String linesMapStr = cls.getCode().getLineMapping().toString();
assertThat(linesMapStr).isEqualTo("{4=13, 5=14, 6=15}");
}
}
@@ -31,6 +31,6 @@ public class TestInline2 extends IntegrationTest {
assertThat(code, containsOne("int[] a = {1, 2, 4, 6, 8};"));
assertThat(code, containsOne("for (int i = 0; i < a.length; i += 2) {"));
assertThat(code, containsOne("for (long i2 = (long) b; i2 > 0; i2--) {"));
assertThat(code, containsOne("for (long i2 = b; i2 > 0; i2--) {"));
}
}
@@ -2,9 +2,11 @@ package jadx.tests.integration.inner;
import org.junit.jupiter.api.Test;
import jadx.NotYetImplemented;
import jadx.api.CommentsLevel;
import jadx.tests.api.IntegrationTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestAnonymousClass16 extends IntegrationTest {
public static class TestCls {
@@ -26,9 +28,18 @@ public class TestAnonymousClass16 extends IntegrationTest {
}
@Test
@NotYetImplemented
public void test() {
getArgs().setCommentsLevel(CommentsLevel.NONE);
noDebugInfo();
getClassNode(TestCls.class);
assertThat(getClassNode(TestCls.class))
.code()
.doesNotContain("r0")
.doesNotContain("AnonymousClass1 r0 = ")
.containsLines(2,
"Something something = new Something() {",
indent() + "{",
indent(2) + "put(\"a\", \"b\");",
indent() + "}",
"};");
}
}
@@ -0,0 +1,41 @@
package jadx.tests.integration.inner;
import org.junit.jupiter.api.Test;
import jadx.tests.api.SmaliTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestAnonymousClass19 extends SmaliTest {
@SuppressWarnings({ "Convert2Lambda", "unused" })
public static class TestCls {
public void test(boolean a, boolean b) {
boolean c = a && b;
use(new Runnable() {
@Override
public void run() {
System.out.println(a + " && " + b + " = " + c);
}
});
}
public void use(Runnable r) {
}
}
@Test
public void test() {
assertThat(getClassNode(TestCls.class))
.code()
.containsOne("System.out.println(a + \" && \" + b + \" = \" + c);");
}
@Test
public void testSmali() {
assertThat(getClassNodeFromSmaliFiles("ATestCls"))
.code()
.containsOne("System.out.println(a + \" && \" + b + \" = \" + c);");
}
}
@@ -3,6 +3,7 @@ package jadx.tests.integration.inner;
import org.junit.jupiter.api.Test;
import jadx.NotYetImplemented;
import jadx.api.CommentsLevel;
import jadx.tests.api.IntegrationTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
@@ -41,9 +42,11 @@ public class TestAnonymousClass3a extends IntegrationTest {
@Test
@NotYetImplemented
public void test() {
getArgs().setCommentsLevel(CommentsLevel.NONE);
assertThat(getClassNode(TestCls.class))
.code()
.doesNotContain("synthetic")
.doesNotContain("access$00")
.doesNotContain("AnonymousClass_")
.doesNotContain("unused = ")
.containsLine(4, "public void run() {")
@@ -23,7 +23,7 @@ public class TestInnerClass2 extends IntegrationTest {
}
public void test() {
new Timer().schedule(new TerminateTask(), 1000);
new Timer().schedule(new TerminateTask(), 1000L);
}
}
@@ -32,7 +32,7 @@ public class TestInnerClass2 extends IntegrationTest {
ClassNode cls = getClassNode(TestCls.class);
String code = cls.getCode().toString();
assertThat(code, containsString("new Timer().schedule(new TerminateTask(), 1000);"));
assertThat(code, containsString("new Timer().schedule(new TerminateTask(), 1000L);"));
assertThat(code, not(containsString("synthetic")));
assertThat(code, not(containsString("this")));
assertThat(code, not(containsString("null")));
@@ -12,11 +12,11 @@ public class TestInvokeWithWideVars extends IntegrationTest {
public static class TestCls {
public long test1() {
return call(1, 2);
return call(1, 2L);
}
public long test2() {
return rangeCall(1, 2, 3.0d, (byte) 4);
return rangeCall(1L, 2, 3.0d, (byte) 4);
}
private long call(int a, long b) {
@@ -32,7 +32,7 @@ public class TestInvokeWithWideVars extends IntegrationTest {
public void test() {
assertThat(getClassNode(TestCls.class))
.code()
.containsOne("return call(1, 2);")
.containsOne("return rangeCall(1, 2, 3.0d, (byte) 4);");
.containsOne("return call(1, 2L);")
.containsOne("return rangeCall(1L, 2, 3.0d, (byte) 4);");
}
}
@@ -0,0 +1,39 @@
package jadx.tests.integration.invoke;
import jadx.tests.api.IntegrationTest;
import jadx.tests.api.extensions.profiles.TestProfile;
import jadx.tests.api.extensions.profiles.TestWithProfiles;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestOverloadedInvoke extends IntegrationTest {
public static class TestCls {
public static final int N = 10;
public void test() {
int[][][] arr = new int[N][N][N];
use(arr, -1);
use(arr[0], -2);
}
public void use(Object[][] arr, Object obj) {
}
public void use(int[][] arr, int i) {
}
}
@TestWithProfiles({ TestProfile.DX_J8, TestProfile.D8_J11, TestProfile.JAVA8 })
public void test() {
noDebugInfo();
assertThat(getClassNode(TestCls.class))
.code()
.containsOne("use(iArr[0], -2);")
.containsOne("use((Object[][]) iArr, (Object) (-1));");
// TODO: don't add unnecessary casts
// .containsOne("use(iArr, -1);");
// TODO: replace call `Array.newInstance` with new array creation: `new int[N][N][N]`
// .containsOne("new int[10][10][10];");
}
}
@@ -1,8 +1,8 @@
package jadx.tests.integration.invoke;
import jadx.tests.api.IntegrationTest;
import jadx.tests.api.extensions.inputs.InputPlugin;
import jadx.tests.api.extensions.inputs.TestWithInputPlugins;
import jadx.tests.api.extensions.profiles.TestProfile;
import jadx.tests.api.extensions.profiles.TestWithProfiles;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
@@ -19,7 +19,7 @@ public class TestSuperInvoke2 extends IntegrationTest {
}
}
@TestWithInputPlugins({ InputPlugin.DEX, InputPlugin.JAVA })
@TestWithProfiles({ TestProfile.DX_J8, TestProfile.JAVA8 })
public void test() {
noDebugInfo();
assertThat(getClassNode(TestCls.class))
@@ -13,6 +13,7 @@ import static org.hamcrest.Matchers.anyOf;
public class TestBreakInLoop2 extends IntegrationTest {
@SuppressWarnings({ "BusyWait", "ResultOfMethodCallIgnored" })
public static class TestCls {
public void test(List<Integer> data) throws Exception {
for (;;) {
@@ -25,7 +26,7 @@ public class TestBreakInLoop2 extends IntegrationTest {
}
data.clear();
}
Thread.sleep(100);
Thread.sleep(100L);
}
}
@@ -47,6 +48,6 @@ public class TestBreakInLoop2 extends IntegrationTest {
assertThat(code, anyOf(containsOne("break;"), containsOne("return;")));
assertThat(code, containsOne("throw ex;"));
assertThat(code, containsOne("data.clear();"));
assertThat(code, containsOne("Thread.sleep(100);"));
assertThat(code, containsOne("Thread.sleep(100L);"));
}
}
@@ -26,7 +26,7 @@ public class TestSynchronizedInEndlessLoop extends IntegrationTest {
}
try {
f++;
Thread.sleep(100);
Thread.sleep(100L);
} catch (Exception e) {
throw new RuntimeException(e);
}
@@ -42,7 +42,7 @@ public class TestSynchronizedInEndlessLoop extends IntegrationTest {
assertThat(code, containsOne("synchronized (this) {"));
assertThat(code, containsOne("try {"));
assertThat(code, containsOne("f++;"));
assertThat(code, containsOne("Thread.sleep(100);"));
assertThat(code, containsOne("Thread.sleep(100L);"));
assertThat(code, containsOne("} catch (Exception e) {"));
}
}
@@ -69,7 +69,7 @@ public class TestDeboxing extends IntegrationTest {
assertThat(code, containsOne("return 1;"));
assertThat(code, containsOne("return true;"));
assertThat(code, containsOne("return (byte) 2;"));
assertThat(code, containsOne("return 3;"));
assertThat(code, containsOne("return (short) 3;"));
assertThat(code, containsOne("return 'c';"));
assertThat(code, containsOne("return 4L;"));
assertThat(code, countString(2, "use(true);"));
@@ -0,0 +1,34 @@
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;
public class TestOverrideWithTwoBases extends IntegrationTest {
public static class TestCls {
public abstract static class BaseClass {
public abstract int a();
}
public interface I {
int a();
}
public static class Cls extends BaseClass implements I {
@Override
public int a() {
return 2;
}
}
}
@Test
public void test() {
assertThat(getClassNode(TestCls.class))
.code()
.containsOne("@Override");
}
}
@@ -0,0 +1,33 @@
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;
public class TestOverrideWithTwoBases2 extends IntegrationTest {
public static class TestCls {
public interface I {
int a();
}
public abstract static class BaseCls implements I {
}
public static class Cls extends BaseCls implements I {
@Override
public int a() {
return 2;
}
}
}
@Test
public void test() {
assertThat(getClassNode(TestCls.class))
.code()
.containsOne("@Override");
}
}
@@ -0,0 +1,73 @@
package jadx.tests.integration.others;
import jadx.tests.api.IntegrationTest;
import jadx.tests.api.extensions.profiles.TestProfile;
import jadx.tests.api.extensions.profiles.TestWithProfiles;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestPrimitiveCasts extends IntegrationTest {
public static class TestCls {
public void test() {
useShort((short) 0);
useShort((short) getInt());
useByte((byte) 0);
useByte((byte) getInt());
useChar((char) 0);
useChar((char) getInt());
useShort((short) 0L);
useShort((short) getLong());
useByte((byte) 0L);
useByte((byte) getLong());
useChar((char) 0L);
useChar((char) getLong());
useShort((short) ' ');
useShort((short) getChar());
useByte((byte) ' ');
useByte((byte) getChar());
useInt((byte) 7);
useInt((char) ' ');
useInt(getChar());
useInt((int) 2L);
useInt((int) getLong());
}
private long getLong() {
return 1L;
}
private char getChar() {
return ' ';
}
private int getInt() {
return 1;
}
private void useChar(char c) {
}
private void useByte(byte b) {
}
private void useShort(short s) {
}
private void useInt(int i) {
}
}
@TestWithProfiles({ TestProfile.DX_J8, TestProfile.JAVA8 })
public void test() {
noDebugInfo();
assertThat(getClassNode(TestCls.class))
.code()
.doesNotContain("(0)")
.doesNotContain(") ((int) getLong())");
}
}
@@ -3,8 +3,8 @@ package jadx.tests.integration.others;
import org.junit.jupiter.api.Test;
import jadx.tests.api.RaungTest;
import jadx.tests.api.extensions.inputs.InputPlugin;
import jadx.tests.api.extensions.inputs.TestWithInputPlugins;
import jadx.tests.api.extensions.profiles.TestProfile;
import jadx.tests.api.extensions.profiles.TestWithProfiles;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
@@ -52,7 +52,7 @@ public class TestStringConcatJava11 extends RaungTest {
"return str + \"test\" + str + \"7\";"); // dynamic concat add const to string recipe
}
@TestWithInputPlugins({ InputPlugin.DEX, InputPlugin.JAVA })
@TestWithProfiles({ TestProfile.DX_J8, TestProfile.JAVA8 })
public void testJava11() {
useTargetJavaVersion(11);
noDebugInfo();
@@ -14,9 +14,9 @@ public class TestNestedTryCatch extends IntegrationTest {
public static class TestCls {
public void test() {
try {
Thread.sleep(1);
Thread.sleep(1L);
try {
Thread.sleep(2);
Thread.sleep(2L);
} catch (InterruptedException ignored) {
System.out.println(2);
}
@@ -32,8 +32,8 @@ public class TestNestedTryCatch extends IntegrationTest {
String code = cls.getCode().toString();
assertThat(code, containsString("try {"));
assertThat(code, containsString("Thread.sleep(1);"));
assertThat(code, containsString("Thread.sleep(2);"));
assertThat(code, containsString("Thread.sleep(1L);"));
assertThat(code, containsString("Thread.sleep(2L);"));
assertThat(code, containsString("} catch (InterruptedException e) {"));
assertThat(code, containsString("} catch (Exception e2) {"));
assertThat(code, not(containsString("return")));
@@ -14,7 +14,7 @@ public class TestTryCatch extends IntegrationTest {
public static class TestCls {
public void f() {
try {
Thread.sleep(50);
Thread.sleep(50L);
} catch (InterruptedException e) {
// ignore
}
@@ -27,7 +27,7 @@ public class TestTryCatch extends IntegrationTest {
String code = cls.getCode().toString();
assertThat(code, containsString("try {"));
assertThat(code, containsString("Thread.sleep(50);"));
assertThat(code, containsString("Thread.sleep(50L);"));
assertThat(code, containsString("} catch (InterruptedException e) {"));
assertThat(code, not(containsString("return")));
}
@@ -16,7 +16,7 @@ public class TestTryCatch2 extends IntegrationTest {
public static boolean test() {
try {
synchronized (OBJ) {
OBJ.wait(5);
OBJ.wait(5L);
}
return true;
} catch (InterruptedException e) {
@@ -32,7 +32,7 @@ public class TestTryCatch2 extends IntegrationTest {
assertThat(code, containsString("try {"));
assertThat(code, containsString("synchronized (OBJ) {"));
assertThat(code, containsString("OBJ.wait(5);"));
assertThat(code, containsString("OBJ.wait(5L);"));
assertThat(code, containsString("return true;"));
assertThat(code, containsString("} catch (InterruptedException e) {"));
assertThat(code, containsString("return false;"));
@@ -1,8 +1,8 @@
package jadx.tests.integration.trycatch;
import jadx.tests.api.IntegrationTest;
import jadx.tests.api.extensions.inputs.InputPlugin;
import jadx.tests.api.extensions.inputs.TestWithInputPlugins;
import jadx.tests.api.extensions.profiles.TestProfile;
import jadx.tests.api.extensions.profiles.TestWithProfiles;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
@@ -34,7 +34,7 @@ public class TestTryCatch9 extends IntegrationTest {
}
}
@TestWithInputPlugins({ InputPlugin.DEX, InputPlugin.JAVA })
@TestWithProfiles({ TestProfile.DX_J8, TestProfile.JAVA8 })
public void test() {
assertThat(getClassNode(TestCls.class))
.code()
@@ -1,8 +1,8 @@
package jadx.tests.integration.trycatch;
import jadx.tests.api.IntegrationTest;
import jadx.tests.api.extensions.inputs.InputPlugin;
import jadx.tests.api.extensions.inputs.TestWithInputPlugins;
import jadx.tests.api.extensions.profiles.TestProfile;
import jadx.tests.api.extensions.profiles.TestWithProfiles;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
@@ -43,7 +43,7 @@ public class TestTryCatchFinally13 extends IntegrationTest {
}
}
@TestWithInputPlugins({ InputPlugin.DEX, InputPlugin.JAVA })
@TestWithProfiles({ TestProfile.DX_J8, TestProfile.JAVA8 })
public void test() {
assertThat(getClassNode(TestCls.class))
.code()
@@ -23,6 +23,6 @@ public class TestPrimitiveConversion extends SmaliTest {
assertThat(getClassNodeFromSmali())
.code()
.doesNotContain("putByte(j, z);")
.containsOne("putByte(j, z ? (byte) 1 : 0);");
.containsOne("putByte(j, z ? (byte) 1 : (byte) 0);");
}
}
@@ -1,8 +1,8 @@
package jadx.tests.integration.variables;
import jadx.tests.api.IntegrationTest;
import jadx.tests.api.extensions.inputs.InputPlugin;
import jadx.tests.api.extensions.inputs.TestWithInputPlugins;
import jadx.tests.api.extensions.profiles.TestProfile;
import jadx.tests.api.extensions.profiles.TestWithProfiles;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
@@ -21,7 +21,7 @@ public class TestVariablesInInlinedAssign extends IntegrationTest {
}
}
@TestWithInputPlugins({ InputPlugin.DEX, InputPlugin.JAVA })
@TestWithProfiles({ TestProfile.DX_J8, TestProfile.JAVA8 })
public void test() {
assertThat(getClassNode(TestCls.class))
.code()
@@ -0,0 +1,52 @@
.class public Linner/ATestCls;
.super Ljava/lang/Object;
.method public constructor <init>()V
.registers 1
.prologue
.line 11
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
# virtual methods
.method public test(ZZ)V
.registers 5
.param p1, "a" # Z
.param p2, "b" # Z
.prologue
.line 14
if-eqz p1, :cond_e
if-eqz p2, :cond_e
const/4 v0, 0x1
.line 15
.local v0, "c":Z
:goto_5
new-instance v1, Linner/Lambda$TestCls$1;
invoke-direct {v1, p0, p1, p2, v0}, Linner/Lambda$TestCls$1;-><init>(Linner/ATestCls;ZZZ)V
invoke-virtual {p0, v1}, Linner/ATestCls;->use(Ljava/lang/Runnable;)V
.line 21
return-void
.line 14
.end local v0 # "c":Z
:cond_e
const/4 v0, 0x0
goto :goto_5
.end method
.method public use(Ljava/lang/Runnable;)V
.registers 2
return-void
.end method
@@ -0,0 +1,58 @@
.class public final synthetic Linner/Lambda$TestCls$1;
.super Ljava/lang/Object;
.implements Ljava/lang/Runnable;
.field final synthetic this$0:Linner/ATestCls;
.field final synthetic val$a:Z
.field final synthetic val$b:Z
.field final synthetic val$c:Z
.method constructor <init>(Linner/ATestCls;ZZZ)V
.registers 5
.param p1, "this$0"
.annotation system Ldalvik/annotation/Signature;
value = {
"()V"
}
.end annotation
.prologue
.line 15
iput-object p1, p0, Linner/Lambda$TestCls$1;->this$0:Linner/ATestCls;
iput-boolean p2, p0, Linner/Lambda$TestCls$1;->val$a:Z
iput-boolean p3, p0, Linner/Lambda$TestCls$1;->val$b:Z
iput-boolean p4, p0, Linner/Lambda$TestCls$1;->val$c:Z
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
.method public run()V
.registers 4
.prologue
.line 18
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
new-instance v1, Ljava/lang/StringBuilder;
invoke-direct {v1}, Ljava/lang/StringBuilder;-><init>()V
iget-boolean v2, p0, Linner/Lambda$TestCls$1;->val$a:Z
invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Z)Ljava/lang/StringBuilder;
move-result-object v1
const-string v2, " && "
invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v1
iget-boolean v2, p0, Linner/Lambda$TestCls$1;->val$b:Z
invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Z)Ljava/lang/StringBuilder;
move-result-object v1
const-string v2, " = "
invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v1
iget-boolean v2, p0, Linner/Lambda$TestCls$1;->val$c:Z
invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Z)Ljava/lang/StringBuilder;
move-result-object v1
invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v1
invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
.line 19
return-void
.end method
+7 -7
View File
@@ -1,7 +1,7 @@
plugins {
id 'application'
id 'edu.sc.seis.launch4j' version '2.5.1'
id 'com.github.johnrengelman.shadow' version '7.1.1'
id 'com.github.johnrengelman.shadow' version '7.1.2'
id 'org.beryx.runtime' version '1.12.7'
}
@@ -9,16 +9,16 @@ dependencies {
implementation(project(':jadx-core'))
implementation(project(":jadx-cli"))
implementation 'com.beust:jcommander:1.81'
implementation 'ch.qos.logback:logback-classic:1.2.8'
implementation 'com.beust:jcommander:1.82'
implementation 'ch.qos.logback:logback-classic:1.2.10'
implementation 'com.fifesoft:rsyntaxtextarea:3.1.4'
implementation 'com.fifesoft:rsyntaxtextarea:3.1.6'
implementation files('libs/jfontchooser-1.0.5.jar')
implementation 'hu.kazocsaba:image-viewer:1.2.3'
implementation 'com.formdev:flatlaf:1.6.5'
implementation 'com.formdev:flatlaf-intellij-themes:1.6.5'
implementation 'com.formdev:flatlaf-extras:1.6.5'
implementation 'com.formdev:flatlaf:2.0'
implementation 'com.formdev:flatlaf-intellij-themes:2.0'
implementation 'com.formdev:flatlaf-extras:2.0'
implementation 'com.formdev:svgSalamander:1.1.3'
implementation 'com.google.code.gson:gson:2.8.9'
@@ -126,13 +126,21 @@ public class DbgUtils {
@Nullable
public static JClass searchMainActivity(MainWindow mainWindow) {
String content = getManifestContent(mainWindow);
int pos = content.indexOf("<action android:name=\"android.intent.action.MAIN\"");
if (pos > -1) {
pos = content.lastIndexOf("<activity ", pos);
if (pos > -1) {
pos = content.indexOf(" android:name=\"", pos);
int pos; // current position
int actionPos = 0; // last found action's index
String actionTag = "<action android:name=\"android.intent.action.MAIN\"";
int actionTagLen = 0; // beginning offset. suggested length set after first iteration
while (actionPos > -1) {
pos = content.indexOf(actionTag, actionPos + actionTagLen);
actionPos = pos;
int activityPos = content.lastIndexOf("<activity ", pos);
if (activityPos > -1) {
int aliasPos = content.lastIndexOf("<activity-alias ", pos);
boolean isAnAlias = aliasPos > -1 && aliasPos > activityPos;
String classPathAttribute = " android:" + (isAnAlias ? "targetActivity" : "name") + "=\"";
pos = content.indexOf(classPathAttribute, isAnAlias ? aliasPos : activityPos);
if (pos > -1) {
pos += " android:name=\"".length();
pos += classPathAttribute.length();
String classFullName = content.substring(pos, content.indexOf("\"", pos));
// in case the MainActivity class has been renamed before, we need raw name.
JavaClass cls = mainWindow.getWrapper().getDecompiler().searchJavaClassByAliasFullName(classFullName);
@@ -142,6 +150,9 @@ public class DbgUtils {
}
}
}
if (actionTagLen == 0) {
actionTagLen = actionTag.length();
}
}
return null;
}
@@ -31,7 +31,7 @@ import jadx.gui.settings.data.ProjectData;
import jadx.gui.settings.data.TabViewState;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.codearea.EditorViewState;
import jadx.gui.utils.PathTypeAdapter;
import jadx.gui.utils.RelativePathTypeAdapter;
public class JadxProject {
private static final Logger LOG = LoggerFactory.getLogger(JadxProject.class);
@@ -39,15 +39,6 @@ public class JadxProject {
private static final int CURRENT_PROJECT_VERSION = 1;
public static final String PROJECT_EXTENSION = "jadx";
private static final Gson GSON = new GsonBuilder()
.registerTypeHierarchyAdapter(Path.class, PathTypeAdapter.singleton())
.registerTypeAdapter(ICodeComment.class, GsonUtils.interfaceReplace(JadxCodeComment.class))
.registerTypeAdapter(ICodeRename.class, GsonUtils.interfaceReplace(JadxCodeRename.class))
.registerTypeAdapter(IJavaNodeRef.class, GsonUtils.interfaceReplace(JadxNodeRef.class))
.registerTypeAdapter(IJavaCodeRef.class, GsonUtils.interfaceReplace(JadxCodeRef.class))
.setPrettyPrinting()
.create();
private transient MainWindow mainWindow;
private transient JadxSettings settings;
@@ -179,9 +170,11 @@ public class JadxProject {
}
public void save() {
if (getProjectPath() != null) {
try (Writer writer = Files.newBufferedWriter(getProjectPath(), StandardCharsets.UTF_8)) {
GSON.toJson(data, writer);
Path savePath = getProjectPath();
if (savePath != null) {
Path basePath = savePath.toAbsolutePath().getParent();
try (Writer writer = Files.newBufferedWriter(savePath, StandardCharsets.UTF_8)) {
buildGson(basePath).toJson(data, writer);
saved = true;
} catch (Exception e) {
LOG.error("Error saving project", e);
@@ -190,9 +183,10 @@ public class JadxProject {
}
public static JadxProject from(Path path) {
Path basePath = path.toAbsolutePath().getParent();
try (Reader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
JadxProject project = new JadxProject();
project.data = GSON.fromJson(reader, ProjectData.class);
project.data = buildGson(basePath).fromJson(reader, ProjectData.class);
project.saved = true;
project.setProjectPath(path);
project.upgrade();
@@ -203,6 +197,17 @@ public class JadxProject {
}
}
private static Gson buildGson(Path basePath) {
return new GsonBuilder()
.registerTypeHierarchyAdapter(Path.class, new RelativePathTypeAdapter(basePath))
.registerTypeAdapter(ICodeComment.class, GsonUtils.interfaceReplace(JadxCodeComment.class))
.registerTypeAdapter(ICodeRename.class, GsonUtils.interfaceReplace(JadxCodeRename.class))
.registerTypeAdapter(IJavaNodeRef.class, GsonUtils.interfaceReplace(JadxNodeRef.class))
.registerTypeAdapter(IJavaCodeRef.class, GsonUtils.interfaceReplace(JadxCodeRef.class))
.setPrettyPrinting()
.create();
}
private void upgrade() {
int fromVersion = data.getProjectVersion();
LOG.debug("upgrade settings from version: {} to {}", fromVersion, CURRENT_PROJECT_VERSION);
@@ -43,7 +43,7 @@ public class JadxSettings extends JadxCLIArgs {
private static final Path USER_HOME = Paths.get(System.getProperty("user.home"));
private static final int RECENT_PROJECTS_COUNT = 15;
private static final int CURRENT_SETTINGS_VERSION = 14;
private static final int CURRENT_SETTINGS_VERSION = 15;
private static final Font DEFAULT_FONT = new RSyntaxTextArea().getFont();
@@ -338,6 +338,10 @@ public class JadxSettings extends JadxCLIArgs {
this.deobfuscationParseKotlinMetadata = deobfuscationParseKotlinMetadata;
}
public void setUseKotlinMethodsForVarNames(JadxArgs.UseKotlinMethodsForVarNames useKotlinMethodsForVarNames) {
this.useKotlinMethodsForVarNames = useKotlinMethodsForVarNames;
}
public void updateRenameFlag(JadxArgs.RenameEnum flag, boolean enabled) {
if (enabled) {
renameFlags.add(flag);
@@ -651,6 +655,10 @@ public class JadxSettings extends JadxCLIArgs {
lafTheme = LafManager.INITIAL_THEME_NAME;
fromVersion++;
}
if (fromVersion == 14) {
useKotlinMethodsForVarNames = JadxArgs.UseKotlinMethodsForVarNames.APPLY;
fromVersion++;
}
if (fromVersion != CURRENT_SETTINGS_VERSION) {
throw new JadxRuntimeException("Incorrect settings upgrade");
}
@@ -60,6 +60,7 @@ import say.swing.JFontChooser;
import jadx.api.CommentsLevel;
import jadx.api.JadxArgs;
import jadx.api.JadxArgs.UseKotlinMethodsForVarNames;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.codearea.EditorTheme;
import jadx.gui.utils.FontUtils;
@@ -433,8 +434,10 @@ public class JadxSettingsWindow extends JDialog {
needReload();
});
SpinnerNumberModel spinnerModel = new SpinnerNumberModel(
settings.getThreadsCount(), 1, Runtime.getRuntime().availableProcessors() * 2, 1);
// fix for #1331
int threadsCountValue = settings.getThreadsCount();
int threadsCountMax = Math.max(2, Math.max(threadsCountValue, Runtime.getRuntime().availableProcessors() * 2));
SpinnerNumberModel spinnerModel = new SpinnerNumberModel(threadsCountValue, 1, threadsCountMax, 1);
JSpinner threadsCount = new JSpinner(spinnerModel);
threadsCount.addChangeListener(e -> {
settings.setThreadsCount((Integer) threadsCount.getValue());
@@ -508,6 +511,13 @@ public class JadxSettingsWindow extends JDialog {
needReload();
});
JComboBox<UseKotlinMethodsForVarNames> kotlinRenameVars = new JComboBox<>(UseKotlinMethodsForVarNames.values());
kotlinRenameVars.setSelectedItem(settings.getUseKotlinMethodsForVarNames());
kotlinRenameVars.addActionListener(e -> {
settings.setUseKotlinMethodsForVarNames((UseKotlinMethodsForVarNames) kotlinRenameVars.getSelectedItem());
needReload();
});
JComboBox<CommentsLevel> commentsLevel = new JComboBox<>(CommentsLevel.values());
commentsLevel.setSelectedItem(settings.getCommentsLevel());
commentsLevel.addActionListener(e -> {
@@ -531,6 +541,7 @@ public class JadxSettingsWindow extends JDialog {
other.addRow(NLS.str("preferences.fallback"), fallback);
other.addRow(NLS.str("preferences.useDx"), useDx);
other.addRow(NLS.str("preferences.skipResourcesDecode"), resourceDecode);
other.addRow(NLS.str("preferences.useKotlinMethodsForVarNames"), kotlinRenameVars);
other.addRow(NLS.str("preferences.commentsLevel"), commentsLevel);
return other;
}
@@ -9,11 +9,14 @@ import java.awt.Font;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.HeadlessException;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTarget;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
@@ -361,6 +364,12 @@ public class MainWindow extends JFrame {
if (currentDirectory != null) {
fileChooser.setCurrentDirectory(currentDirectory.toFile());
}
if (this.project.getFilePaths().size() == 1) {
// If there is only one file loaded we suggest saving the jadx project file next to the loaded file
Path loadedFile = this.project.getFilePaths().get(0);
String fileName = loadedFile.getFileName() + "." + JadxProject.PROJECT_EXTENSION;
fileChooser.setSelectedFile(loadedFile.resolveSibling(fileName).toFile());
}
int ret = fileChooser.showSaveDialog(mainPanel);
if (ret == JFileChooser.APPROVE_OPTION) {
settings.setLastSaveProjectPath(fileChooser.getCurrentDirectory().toPath());
@@ -445,7 +454,7 @@ public class MainWindow extends JFrame {
update();
restoreOpenTabs();
runInitialBackgroundJobs();
BreakpointManager.init(paths.get(0).getParent());
BreakpointManager.init(paths.get(0).toAbsolutePath().getParent());
}
private void addTreeCustomNodes() {
@@ -511,7 +520,7 @@ public class MainWindow extends JFrame {
if (projectPath == null) {
pathString = "";
} else {
pathString = " [" + projectPath.getParent().toAbsolutePath() + ']';
pathString = " [" + projectPath.toAbsolutePath().getParent() + ']';
}
setTitle((project.isSaved() ? "" : '*')
+ project.getName() + pathString + " - " + DEFAULT_TITLE);
@@ -673,32 +682,33 @@ public class MainWindow extends JFrame {
reOpenFile();
}
private void nodeClickAction(@Nullable Object obj) {
private boolean nodeClickAction(@Nullable Object obj) {
if (obj == null) {
return false;
}
try {
if (obj == null) {
return;
}
if (obj instanceof JResource) {
JResource res = (JResource) obj;
ResourceFile resFile = res.getResFile();
if (resFile != null && JResource.isSupportedForView(resFile.getType())) {
tabbedPane.showNode(res);
return tabbedPane.showNode(res);
}
} else if (obj instanceof JNode) {
JNode node = (JNode) obj;
if (node.getRootClass() != null) {
tabbedPane.codeJump(new JumpPosition(node));
} else {
tabbedPane.showNode(node);
return true;
}
return tabbedPane.showNode(node);
}
} catch (Exception e) {
LOG.error("Content loading error", e);
}
return false;
}
private void treeRightClickAction(MouseEvent e) {
JNode obj = getJNodeUnderMouse(e, false);
JNode obj = getJNodeUnderMouse(e);
if (obj instanceof JPackage) {
JPackagePopupMenu menu = new JPackagePopupMenu(this, (JPackage) obj);
menu.show(e.getComponent(), e.getX(), e.getY());
@@ -712,17 +722,27 @@ public class MainWindow extends JFrame {
}
@Nullable
private JNode getJNodeUnderMouse(MouseEvent mouseEvent, boolean trySelection) {
TreePath path = tree.getPathForLocation(mouseEvent.getX(), mouseEvent.getY());
if (path == null && trySelection) {
// maybe click on node row (mouse pressed event should select this node in tree)
path = tree.getSelectionPath();
private JNode getJNodeUnderMouse(MouseEvent mouseEvent) {
TreePath path = tree.getClosestPathForLocation(mouseEvent.getX(), mouseEvent.getY());
if (path == null) {
return null;
}
if (path != null) {
Object obj = path.getLastPathComponent();
if (obj instanceof JNode) {
return (JNode) obj;
// allow 'closest' path only at the right of the item row
Rectangle pathBounds = tree.getPathBounds(path);
if (pathBounds != null) {
int y = mouseEvent.getY();
if (y < pathBounds.y || y > (pathBounds.y + pathBounds.height)) {
return null;
}
if (mouseEvent.getX() < pathBounds.x) {
// exclude expand/collapse events
return null;
}
}
Object obj = path.getLastPathComponent();
if (obj instanceof JNode) {
tree.setSelectionPath(path);
return (JNode) obj;
}
return null;
}
@@ -1074,17 +1094,22 @@ public class MainWindow extends JFrame {
tree = new JTree(treeModel);
ToolTipManager.sharedInstance().registerComponent(tree);
tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
tree.setFocusable(false);
tree.addFocusListener(new FocusAdapter() {
@Override
public void focusLost(FocusEvent e) {
tree.setFocusable(false);
}
});
tree.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
super.mousePressed(e);
}
@Override
public void mouseClicked(MouseEvent e) {
if (SwingUtilities.isLeftMouseButton(e)) {
nodeClickAction(getJNodeUnderMouse(e, true));
if (!nodeClickAction(getJNodeUnderMouse(e))) {
// click ignored -> switch to focusable mode
tree.setFocusable(true);
tree.requestFocus();
}
} else if (SwingUtilities.isRightMouseButton(e)) {
treeRightClickAction(e);
}
@@ -203,12 +203,13 @@ public class TabbedPane extends JTabbedPane {
});
}
public void showNode(JNode node) {
public boolean showNode(JNode node) {
final ContentPanel contentPanel = getContentPanel(node);
if (contentPanel == null) {
return;
return false;
}
SwingUtilities.invokeLater(() -> selectTab(contentPanel));
selectTab(contentPanel);
return true;
}
public void selectTab(ContentPanel contentPanel) {
@@ -8,6 +8,8 @@ import javax.swing.border.EmptyBorder;
import org.fife.ui.rsyntaxtextarea.AbstractTokenMakerFactory;
import org.fife.ui.rsyntaxtextarea.TokenMakerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.TabbedPane;
@@ -23,6 +25,7 @@ import jadx.gui.utils.NLS;
* </ul>
*/
public final class ClassCodeContentPanel extends AbstractCodeContentPanel implements IViewStateSupport {
private static final Logger LOG = LoggerFactory.getLogger(ClassCodeContentPanel.class);
private static final long serialVersionUID = -7229931102504634591L;
private final transient CodePanel javaCodePanel;
@@ -104,7 +107,15 @@ public final class ClassCodeContentPanel extends AbstractCodeContentPanel implem
boolean isJava = viewState.getSubPath().equals("java");
CodePanel activePanel = isJava ? javaCodePanel : smaliCodePanel;
areaTabbedPane.setSelectedComponent(activePanel);
activePanel.getCodeScrollPane().getViewport().setViewPosition(viewState.getViewPoint());
activePanel.getCodeArea().setCaretPosition(viewState.getCaretPos());
try {
activePanel.getCodeScrollPane().getViewport().setViewPosition(viewState.getViewPoint());
} catch (Exception e) {
LOG.debug("Failed to restore view position: {}", viewState.getViewPoint(), e);
}
try {
activePanel.getCodeArea().setCaretPosition(viewState.getCaretPos());
} catch (Exception e) {
LOG.debug("Failed to restore caret position: {}", viewState.getCaretPos(), e);
}
}
}
@@ -0,0 +1,44 @@
package jadx.gui.utils;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Objects;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
public class RelativePathTypeAdapter extends TypeAdapter<Path> {
private final Path basePath;
public RelativePathTypeAdapter(Path basePath) {
this.basePath = Objects.requireNonNull(basePath);
}
@Override
public void write(JsonWriter out, Path value) throws IOException {
if (value == null) {
out.nullValue();
} else {
value = value.toAbsolutePath().normalize();
String relativePath = basePath.relativize(value).toString();
out.value(relativePath);
}
}
@Override
public Path read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
Path p = Paths.get(in.nextString());
if (p.isAbsolute()) {
return p;
}
return basePath.resolve(p);
}
}
@@ -134,6 +134,7 @@ preferences.inlineAnonymous=Anonyme Inline-Klassen
preferences.inlineMethods=Inline-Methoden
preferences.fsCaseSensitive=Dateisystem unterscheidet zwischen Groß/Kleinschreibung
preferences.skipResourcesDecode=Keine Ressourcen dekodieren
#preferences.useKotlinMethodsForVarNames=Use kotlin methods for variables rename
#preferences.commentsLevel=Code comments level
preferences.autoSave=Autom. speichern
preferences.threads=Verarbeitungs-Thread-Anzahl
@@ -134,6 +134,7 @@ preferences.inlineAnonymous=Inline anonymous classes
preferences.inlineMethods=Inline methods
preferences.fsCaseSensitive=File system is case sensitive
preferences.skipResourcesDecode=Don't decode resources
preferences.useKotlinMethodsForVarNames=Use kotlin methods for variables rename
preferences.commentsLevel=Code comments level
preferences.autoSave=Auto save
preferences.threads=Processing threads count
@@ -134,6 +134,7 @@ preferences.replaceConsts=Reemplazar constantes
#preferences.inlineMethods=Inline methods
#preferences.fsCaseSensitive=
preferences.skipResourcesDecode=No descodificar recursos
#preferences.useKotlinMethodsForVarNames=Use kotlin methods for variables rename
#preferences.commentsLevel=Code comments level
#preferences.autoSave=
preferences.threads=Número de hilos a procesar
@@ -134,6 +134,7 @@ preferences.inlineAnonymous=인라인 익명 클래스
preferences.inlineMethods=인라인 메서드
preferences.fsCaseSensitive=파일 시스템 대소문자 구별
preferences.skipResourcesDecode=리소스 디코딩 하지 않기
#preferences.useKotlinMethodsForVarNames=Use kotlin methods for variables rename
preferences.commentsLevel=코드 주석 수준
preferences.autoSave=자동 저장
preferences.threads=처리 스레드 수

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