Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d22db30166 | |||
| 6db61e7a59 | |||
| 86582de521 | |||
| a7c63c2eb3 | |||
| 081a0e21ee | |||
| 9ac9c05265 | |||
| b7daf79b26 | |||
| b67a3561a4 | |||
| 52ac6dbbaf | |||
| 72381ad8f3 | |||
| 6a065c46f4 | |||
| 092d0d7e67 | |||
| 5ca7285558 | |||
| 7576f9cd5e | |||
| 46b5725d98 | |||
| 72542fa6f9 | |||
| a250d0461b | |||
| c7795bfc48 | |||
| 5de46b7e40 | |||
| 99c70872c1 | |||
| 3566669303 | |||
| 4557d05256 | |||
| fa421d165e | |||
| ecf20020d7 | |||
| ae85af61c7 | |||
| 659bbbf4fb | |||
| 427e2dddc4 | |||
| d47483f957 |
@@ -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
|
||||
@@ -34,3 +34,5 @@ jadx-output/
|
||||
*.cfg
|
||||
*.orig
|
||||
quark.json
|
||||
|
||||
cliff.toml
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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(", "));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
+42
-9
@@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
+13
-14
@@ -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;
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -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);"));
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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);");
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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
|
||||
@@ -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
Reference in New Issue
Block a user