Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fdf170529f | |||
| 50283ab543 | |||
| 3fa3e5acec | |||
| 4230cd5b5a | |||
| 1ad6527de5 | |||
| 0421ad80c1 | |||
| 35e0201f06 | |||
| 118eea5e77 | |||
| 7f317be325 | |||
| e1aa9f6de4 | |||
| 058a5e3bb2 | |||
| 92b49ec2b5 | |||
| 583a04b092 | |||
| 444a04e2f7 | |||
| 157e702ffd | |||
| 77892f41ec | |||
| 6ba0e1dbf6 | |||
| 950fbbaa83 | |||
| 912c431511 | |||
| 5d6b82724a | |||
| 78c976ad4f | |||
| fbdfd135da | |||
| dd51783d9e | |||
| 158fc2fca3 | |||
| 24284a6f3a | |||
| 85c2c63aa3 | |||
| f354f7de63 | |||
| 540c0a8100 | |||
| 4d00fede56 | |||
| b1bc5c08ff | |||
| 305d4f4fe5 | |||
| 2d149e9a5d | |||
| 87b9ff3c35 | |||
| 1c36b3c74c | |||
| 068e4b8e3d | |||
| df38a6424f | |||
| 5d186e56a5 | |||
| 0fafcfa006 | |||
| e3fdbafd86 | |||
| 07c2b14479 | |||
| cdc844aaf3 | |||
| e1b7d361b9 | |||
| 12ef29bebc | |||
| 22ed241d50 | |||
| 28e5a3c5be | |||
| bb4d88cc68 | |||
| 4aaea2b93f | |||
| bc8d7c4fc3 | |||
| 5ea6c46778 | |||
| b28f8ba85b | |||
| 4db50fb749 | |||
| 1dd0c90a04 | |||
| 2bace2bde2 | |||
| 1a9cb832ab |
@@ -59,7 +59,7 @@ jobs:
|
||||
- name: Set up JDK
|
||||
uses: oracle-actions/setup-java@v1
|
||||
with:
|
||||
release: 18
|
||||
release: 17
|
||||
|
||||
- name: Print Java version
|
||||
shell: bash
|
||||
|
||||
+10
-10
@@ -1,6 +1,6 @@
|
||||
plugins {
|
||||
id 'com.github.ben-manes.versions' version '0.43.0'
|
||||
id 'com.diffplug.spotless' version '6.11.0'
|
||||
id 'com.github.ben-manes.versions' version '0.45.0'
|
||||
id 'com.diffplug.spotless' version '6.13.0'
|
||||
}
|
||||
|
||||
ext.jadxVersion = System.getenv('JADX_VERSION') ?: "dev"
|
||||
@@ -26,18 +26,18 @@ allprojects {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.slf4j:slf4j-api:2.0.3'
|
||||
compileOnly 'org.jetbrains:annotations:23.0.0'
|
||||
implementation 'org.slf4j:slf4j-api:2.0.6'
|
||||
compileOnly 'org.jetbrains:annotations:24.0.0'
|
||||
|
||||
testImplementation 'ch.qos.logback:logback-classic:1.3.4'
|
||||
testImplementation 'ch.qos.logback:logback-classic:1.3.5'
|
||||
testImplementation 'org.hamcrest:hamcrest-library:2.2'
|
||||
testImplementation 'org.mockito:mockito-core:4.8.0'
|
||||
testImplementation 'org.assertj:assertj-core:3.23.1'
|
||||
testImplementation 'org.mockito:mockito-core:4.10.0'
|
||||
testImplementation 'org.assertj:assertj-core:3.24.2'
|
||||
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.1'
|
||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.1'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.2'
|
||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.2'
|
||||
|
||||
testCompileOnly 'org.jetbrains:annotations:23.0.0'
|
||||
testCompileOnly 'org.jetbrains:annotations:24.0.0'
|
||||
}
|
||||
|
||||
test {
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=f6b8596b10cce501591e92f229816aa4046424f3b24d771751b06779d58c8ec4
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
|
||||
distributionSha256Sum=7ba68c54029790ab444b39d7e293d3236b2632631fb5f2e012bb28b4ff669e4b
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -11,7 +11,7 @@ dependencies {
|
||||
runtimeOnly(project(':jadx-plugins:jadx-smali-input'))
|
||||
|
||||
implementation 'com.beust:jcommander:1.82'
|
||||
implementation 'ch.qos.logback:logback-classic:1.3.4'
|
||||
implementation 'ch.qos.logback:logback-classic:1.3.5'
|
||||
}
|
||||
|
||||
application {
|
||||
|
||||
@@ -7,6 +7,8 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -89,6 +91,9 @@ public class JadxCLIArgs {
|
||||
@Parameter(names = { "--no-inline-methods" }, description = "disable methods inline")
|
||||
protected boolean inlineMethods = true;
|
||||
|
||||
@Parameter(names = { "--no-inline-kotlin-lambda" }, description = "disable inline for Kotlin lambdas")
|
||||
protected boolean allowInlineKotlinLambda = true;
|
||||
|
||||
@Parameter(names = "--no-finally", description = "don't extract finally block")
|
||||
protected boolean extractFinally = true;
|
||||
|
||||
@@ -187,7 +192,7 @@ public class JadxCLIArgs {
|
||||
@Parameter(
|
||||
names = { "--log-level" },
|
||||
description = "set log level, values: quiet, progress, error, warn, info, debug",
|
||||
converter = LogHelper.LogLevelConverter.class
|
||||
converter = LogLevelConverter.class
|
||||
)
|
||||
protected LogHelper.LogLevelEnum logLevel = LogHelper.LogLevelEnum.PROGRESS;
|
||||
|
||||
@@ -285,6 +290,7 @@ public class JadxCLIArgs {
|
||||
args.setInsertDebugLines(addDebugLines);
|
||||
args.setInlineAnonymousClasses(inlineAnonymousClasses);
|
||||
args.setInlineMethods(inlineMethods);
|
||||
args.setAllowInlineKotlinLambda(allowInlineKotlinLambda);
|
||||
args.setExtractFinally(extractFinally);
|
||||
args.setRenameFlags(renameFlags);
|
||||
args.setFsCaseSensitive(fsCaseSensitive);
|
||||
@@ -366,6 +372,10 @@ public class JadxCLIArgs {
|
||||
return inlineMethods;
|
||||
}
|
||||
|
||||
public boolean isAllowInlineKotlinLambda() {
|
||||
return allowInlineKotlinLambda;
|
||||
}
|
||||
|
||||
public boolean isExtractFinally() {
|
||||
return extractFinally;
|
||||
}
|
||||
@@ -487,67 +497,58 @@ public class JadxCLIArgs {
|
||||
}
|
||||
}
|
||||
|
||||
public static class CommentsLevelConverter implements IStringConverter<CommentsLevel> {
|
||||
@Override
|
||||
public CommentsLevel convert(String value) {
|
||||
try {
|
||||
return CommentsLevel.valueOf(value.toUpperCase());
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException(
|
||||
'\'' + value + "' is unknown comments level, possible values are: "
|
||||
+ JadxCLIArgs.enumValuesString(CommentsLevel.values()));
|
||||
}
|
||||
public static class CommentsLevelConverter extends BaseEnumConverter<CommentsLevel> {
|
||||
public CommentsLevelConverter() {
|
||||
super(CommentsLevel::valueOf, CommentsLevel::values);
|
||||
}
|
||||
}
|
||||
|
||||
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 class UseKotlinMethodsForVarNamesConverter extends BaseEnumConverter<UseKotlinMethodsForVarNames> {
|
||||
public UseKotlinMethodsForVarNamesConverter() {
|
||||
super(UseKotlinMethodsForVarNames::valueOf, UseKotlinMethodsForVarNames::values);
|
||||
}
|
||||
}
|
||||
|
||||
public static class DeobfuscationMapFileModeConverter implements IStringConverter<DeobfuscationMapFileMode> {
|
||||
@Override
|
||||
public DeobfuscationMapFileMode convert(String value) {
|
||||
try {
|
||||
return DeobfuscationMapFileMode.valueOf(value.toUpperCase());
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException(
|
||||
'\'' + value + "' is unknown, possible values are: "
|
||||
+ JadxCLIArgs.enumValuesString(DeobfuscationMapFileMode.values()));
|
||||
}
|
||||
public static class DeobfuscationMapFileModeConverter extends BaseEnumConverter<DeobfuscationMapFileMode> {
|
||||
public DeobfuscationMapFileModeConverter() {
|
||||
super(DeobfuscationMapFileMode::valueOf, DeobfuscationMapFileMode::values);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ResourceNameSourceConverter implements IStringConverter<ResourceNameSource> {
|
||||
@Override
|
||||
public ResourceNameSource convert(String value) {
|
||||
try {
|
||||
return ResourceNameSource.valueOf(value.toUpperCase());
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException(
|
||||
'\'' + value + "' is unknown, possible values are: "
|
||||
+ JadxCLIArgs.enumValuesString(ResourceNameSource.values()));
|
||||
}
|
||||
public static class ResourceNameSourceConverter extends BaseEnumConverter<ResourceNameSource> {
|
||||
public ResourceNameSourceConverter() {
|
||||
super(ResourceNameSource::valueOf, ResourceNameSource::values);
|
||||
}
|
||||
}
|
||||
|
||||
public static class DecompilationModeConverter implements IStringConverter<DecompilationMode> {
|
||||
public static class DecompilationModeConverter extends BaseEnumConverter<DecompilationMode> {
|
||||
public DecompilationModeConverter() {
|
||||
super(DecompilationMode::valueOf, DecompilationMode::values);
|
||||
}
|
||||
}
|
||||
|
||||
public static class LogLevelConverter extends BaseEnumConverter<LogHelper.LogLevelEnum> {
|
||||
public LogLevelConverter() {
|
||||
super(LogHelper.LogLevelEnum::valueOf, LogHelper.LogLevelEnum::values);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract static class BaseEnumConverter<E extends Enum<E>> implements IStringConverter<E> {
|
||||
private final Function<String, E> parse;
|
||||
private final Supplier<E[]> values;
|
||||
|
||||
public BaseEnumConverter(Function<String, E> parse, Supplier<E[]> values) {
|
||||
this.parse = parse;
|
||||
this.values = values;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DecompilationMode convert(String value) {
|
||||
public E convert(String value) {
|
||||
try {
|
||||
return DecompilationMode.valueOf(value.toUpperCase());
|
||||
return parse.apply(stringAsEnumName(value));
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException(
|
||||
'\'' + value + "' is unknown, possible values are: "
|
||||
+ JadxCLIArgs.enumValuesString(DecompilationMode.values()));
|
||||
'\'' + value + "' is unknown, possible values are: " + enumValuesString(values.get()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -557,4 +558,9 @@ public class JadxCLIArgs {
|
||||
.map(v -> v.name().replace('_', '-').toLowerCase(Locale.ROOT))
|
||||
.collect(Collectors.joining(", "));
|
||||
}
|
||||
|
||||
private static String stringAsEnumName(String value) {
|
||||
// inverse of enumValuesString conversion
|
||||
return value.replace('-', '_').toUpperCase(Locale.ROOT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.beust.jcommander.IStringConverter;
|
||||
|
||||
import ch.qos.logback.classic.Level;
|
||||
import ch.qos.logback.classic.Logger;
|
||||
|
||||
@@ -119,18 +117,4 @@ public class LogHelper {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static class LogLevelConverter implements IStringConverter<LogLevelEnum> {
|
||||
|
||||
@Override
|
||||
public LogLevelEnum convert(String value) {
|
||||
try {
|
||||
return LogLevelEnum.valueOf(value.toUpperCase());
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException(
|
||||
'\'' + value + "' is unknown log level, possible values are "
|
||||
+ JadxCLIArgs.enumValuesString(LogLevelEnum.values()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@ plugins {
|
||||
dependencies {
|
||||
api(project(':jadx-plugins:jadx-plugins-api'))
|
||||
|
||||
implementation 'com.google.code.gson:gson:2.9.1'
|
||||
implementation 'com.google.code.gson:gson:2.10.1'
|
||||
|
||||
// TODO: move resources decoding to separate plugin module
|
||||
implementation 'com.android.tools.build:aapt2-proto:7.3.1-8691043'
|
||||
implementation 'com.google.protobuf:protobuf-java:3.21.8' // forcing latest version
|
||||
implementation 'com.google.protobuf:protobuf-java:3.21.12' // forcing latest version
|
||||
|
||||
testImplementation 'org.apache.commons:commons-lang3:3.12.0'
|
||||
|
||||
@@ -19,8 +19,8 @@ dependencies {
|
||||
testRuntimeOnly(project(':jadx-plugins:jadx-java-input'))
|
||||
testRuntimeOnly(project(':jadx-plugins:jadx-raung-input'))
|
||||
|
||||
testImplementation 'org.eclipse.jdt:ecj:3.31.0'
|
||||
testImplementation 'tools.profiler:async-profiler:1.8.3'
|
||||
testImplementation 'org.eclipse.jdt:ecj:3.32.0'
|
||||
testImplementation 'tools.profiler:async-profiler:2.9'
|
||||
}
|
||||
|
||||
test {
|
||||
|
||||
@@ -53,6 +53,7 @@ public class JadxArgs {
|
||||
private boolean extractFinally = true;
|
||||
private boolean inlineAnonymousClasses = true;
|
||||
private boolean inlineMethods = true;
|
||||
private boolean allowInlineKotlinLambda = true;
|
||||
|
||||
private boolean skipResources = false;
|
||||
private boolean skipSources = false;
|
||||
@@ -263,6 +264,14 @@ public class JadxArgs {
|
||||
this.inlineMethods = inlineMethods;
|
||||
}
|
||||
|
||||
public boolean isAllowInlineKotlinLambda() {
|
||||
return allowInlineKotlinLambda;
|
||||
}
|
||||
|
||||
public void setAllowInlineKotlinLambda(boolean allowInlineKotlinLambda) {
|
||||
this.allowInlineKotlinLambda = allowInlineKotlinLambda;
|
||||
}
|
||||
|
||||
public boolean isExtractFinally() {
|
||||
return extractFinally;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
@@ -93,10 +92,6 @@ public final class JadxDecompiler implements Closeable {
|
||||
private BinaryXMLParser binaryXmlParser;
|
||||
private ProtoXMLParser protoXmlParser;
|
||||
|
||||
private final Map<ClassNode, JavaClass> classesMap = new ConcurrentHashMap<>();
|
||||
private final Map<MethodNode, JavaMethod> methodsMap = new ConcurrentHashMap<>();
|
||||
private final Map<FieldNode, JavaField> fieldsMap = new ConcurrentHashMap<>();
|
||||
|
||||
private final IDecompileScheduler decompileScheduler = new DecompilerScheduler();
|
||||
|
||||
private final List<ILoadResult> customLoads = new ArrayList<>();
|
||||
@@ -155,10 +150,6 @@ public final class JadxDecompiler implements Closeable {
|
||||
resources = null;
|
||||
binaryXmlParser = null;
|
||||
protoXmlParser = null;
|
||||
|
||||
classesMap.clear();
|
||||
methodsMap.clear();
|
||||
fieldsMap.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -318,9 +309,21 @@ public final class JadxDecompiler implements Closeable {
|
||||
if (args.isSkipFilesSave()) {
|
||||
return;
|
||||
}
|
||||
// process AndroidManifest.xml first to load complete resource ids table
|
||||
for (ResourceFile resourceFile : getResources()) {
|
||||
if (resourceFile.getType() == ResourceType.MANIFEST) {
|
||||
new ResourcesSaver(outDir, resourceFile).run();
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> inputFileNames = args.getInputFiles().stream().map(File::getAbsolutePath).collect(Collectors.toSet());
|
||||
for (ResourceFile resourceFile : getResources()) {
|
||||
if (resourceFile.getType() != ResourceType.ARSC
|
||||
ResourceType resType = resourceFile.getType();
|
||||
if (resType == ResourceType.MANIFEST) {
|
||||
// already processed
|
||||
continue;
|
||||
}
|
||||
if (resType != ResourceType.ARSC
|
||||
&& inputFileNames.contains(resourceFile.getOriginalName())) {
|
||||
// ignore resource made from input file
|
||||
continue;
|
||||
@@ -391,7 +394,7 @@ public final class JadxDecompiler implements Closeable {
|
||||
return Utils.collectionMap(root.getClasses(), this::convertClassNode);
|
||||
}
|
||||
|
||||
public List<ResourceFile> getResources() {
|
||||
public synchronized List<ResourceFile> getResources() {
|
||||
if (resources == null) {
|
||||
if (root == null) {
|
||||
return Collections.emptyList();
|
||||
@@ -471,33 +474,36 @@ public final class JadxDecompiler implements Closeable {
|
||||
* Get JavaClass by ClassNode without loading and decompilation
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
JavaClass convertClassNode(ClassNode cls) {
|
||||
return classesMap.compute(cls, (node, prevJavaCls) -> {
|
||||
if (prevJavaCls != null && prevJavaCls.getClassNode() == cls) {
|
||||
// keep previous variable
|
||||
return prevJavaCls;
|
||||
}
|
||||
if (cls.isInner()) {
|
||||
return new JavaClass(cls, convertClassNode(cls.getParentClass()));
|
||||
}
|
||||
return new JavaClass(cls, this);
|
||||
});
|
||||
synchronized JavaClass convertClassNode(ClassNode cls) {
|
||||
JavaClass javaClass = cls.getJavaNode();
|
||||
if (javaClass == null) {
|
||||
javaClass = cls.isInner()
|
||||
? new JavaClass(cls, convertClassNode(cls.getParentClass()))
|
||||
: new JavaClass(cls, this);
|
||||
cls.setJavaNode(javaClass);
|
||||
}
|
||||
return javaClass;
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
JavaField convertFieldNode(FieldNode field) {
|
||||
return fieldsMap.computeIfAbsent(field, fldNode -> {
|
||||
JavaClass parentCls = convertClassNode(fldNode.getParentClass());
|
||||
return new JavaField(parentCls, fldNode);
|
||||
});
|
||||
synchronized JavaField convertFieldNode(FieldNode fld) {
|
||||
JavaField javaField = fld.getJavaNode();
|
||||
if (javaField == null) {
|
||||
JavaClass parentCls = convertClassNode(fld.getParentClass());
|
||||
javaField = new JavaField(parentCls, fld);
|
||||
fld.setJavaNode(javaField);
|
||||
}
|
||||
return javaField;
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
JavaMethod convertMethodNode(MethodNode method) {
|
||||
return methodsMap.computeIfAbsent(method, mthNode -> {
|
||||
ClassNode parentCls = mthNode.getParentClass();
|
||||
return new JavaMethod(convertClassNode(parentCls), mthNode);
|
||||
});
|
||||
synchronized JavaMethod convertMethodNode(MethodNode mth) {
|
||||
JavaMethod javaMethod = mth.getJavaNode();
|
||||
if (javaMethod == null) {
|
||||
javaMethod = new JavaMethod(convertClassNode(mth.getParentClass()), mth);
|
||||
mth.setJavaNode(javaMethod);
|
||||
}
|
||||
return javaMethod;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -574,14 +580,9 @@ public final class JadxDecompiler implements Closeable {
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private JavaVariable resolveVarNode(VarNode varNode) {
|
||||
MethodNode mthNode = varNode.getMth();
|
||||
JavaMethod mth = convertMethodNode(mthNode);
|
||||
if (mth == null) {
|
||||
return null;
|
||||
}
|
||||
return new JavaVariable(mth, varNode);
|
||||
JavaMethod javaNode = convertMethodNode(varNode.getMth());
|
||||
return new JavaVariable(javaNode, varNode);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
||||
@@ -14,8 +14,10 @@ import jadx.api.ICodeWriter;
|
||||
import jadx.api.metadata.annotations.InsnCodeOffset;
|
||||
import jadx.api.metadata.annotations.VarNode;
|
||||
import jadx.api.plugins.input.data.MethodHandleType;
|
||||
import jadx.api.plugins.input.data.annotations.EncodedValue;
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.attributes.AType;
|
||||
import jadx.core.dex.attributes.FieldInitInsnAttr;
|
||||
import jadx.core.dex.attributes.nodes.FieldReplaceAttr;
|
||||
import jadx.core.dex.attributes.nodes.GenericInfoAttr;
|
||||
import jadx.core.dex.attributes.nodes.LoopLabelAttr;
|
||||
@@ -36,6 +38,7 @@ import jadx.core.dex.instructions.IfNode;
|
||||
import jadx.core.dex.instructions.IndexInsnNode;
|
||||
import jadx.core.dex.instructions.InsnType;
|
||||
import jadx.core.dex.instructions.InvokeCustomNode;
|
||||
import jadx.core.dex.instructions.InvokeCustomRawNode;
|
||||
import jadx.core.dex.instructions.InvokeNode;
|
||||
import jadx.core.dex.instructions.InvokeType;
|
||||
import jadx.core.dex.instructions.NewArrayNode;
|
||||
@@ -208,7 +211,31 @@ public class InsnGen {
|
||||
}
|
||||
}
|
||||
|
||||
protected void staticField(ICodeWriter code, FieldInfo field) throws CodegenException {
|
||||
FieldNode fieldNode = root.resolveField(field);
|
||||
if (fieldNode != null
|
||||
&& fieldNode.contains(AFlag.INLINE_INSTANCE_FIELD)
|
||||
&& fieldNode.getParentClass().contains(AType.ANONYMOUS_CLASS)) {
|
||||
FieldInitInsnAttr initInsnAttr = fieldNode.get(AType.FIELD_INIT_INSN);
|
||||
if (initInsnAttr != null) {
|
||||
InsnNode insn = initInsnAttr.getInsn();
|
||||
if (insn instanceof ConstructorInsn) {
|
||||
fieldNode.add(AFlag.DONT_GENERATE);
|
||||
inlineAnonymousConstructor(code, fieldNode.getParentClass(), (ConstructorInsn) insn);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
makeStaticFieldAccess(code, field, fieldNode, mgen.getClassGen());
|
||||
}
|
||||
|
||||
public static void makeStaticFieldAccess(ICodeWriter code, FieldInfo field, ClassGen clsGen) {
|
||||
FieldNode fieldNode = clsGen.getClassNode().root().resolveField(field);
|
||||
makeStaticFieldAccess(code, field, fieldNode, clsGen);
|
||||
}
|
||||
|
||||
private static void makeStaticFieldAccess(ICodeWriter code,
|
||||
FieldInfo field, @Nullable FieldNode fieldNode, ClassGen clsGen) {
|
||||
ClassInfo declClass = field.getDeclClass();
|
||||
// TODO
|
||||
boolean fieldFromThisClass = clsGen.getClassNode().getClassInfo().equals(declClass);
|
||||
@@ -219,7 +246,6 @@ public class InsnGen {
|
||||
}
|
||||
code.add('.');
|
||||
}
|
||||
FieldNode fieldNode = clsGen.getClassNode().root().resolveField(field);
|
||||
if (fieldNode != null) {
|
||||
code.attachAnnotation(fieldNode);
|
||||
}
|
||||
@@ -230,10 +256,6 @@ public class InsnGen {
|
||||
}
|
||||
}
|
||||
|
||||
protected void staticField(ICodeWriter code, FieldInfo field) {
|
||||
makeStaticFieldAccess(code, field, mgen.getClassGen());
|
||||
}
|
||||
|
||||
public void useClass(ICodeWriter code, ArgType type) {
|
||||
mgen.getClassGen().useClass(code, type);
|
||||
}
|
||||
@@ -693,9 +715,7 @@ public class InsnGen {
|
||||
private void makeConstructor(ConstructorInsn insn, ICodeWriter code) throws CodegenException {
|
||||
ClassNode cls = mth.root().resolveClass(insn.getClassType());
|
||||
if (cls != null && cls.isAnonymous() && !fallback) {
|
||||
cls.ensureProcessed();
|
||||
inlineAnonymousConstructor(code, cls, insn);
|
||||
mth.getParentClass().addInlinedClass(cls);
|
||||
return;
|
||||
}
|
||||
if (insn.isSelf()) {
|
||||
@@ -746,6 +766,7 @@ public class InsnGen {
|
||||
}
|
||||
|
||||
private void inlineAnonymousConstructor(ICodeWriter code, ClassNode cls, ConstructorInsn insn) throws CodegenException {
|
||||
cls.ensureProcessed();
|
||||
if (this.mth.getParentClass() == cls) {
|
||||
cls.remove(AType.ANONYMOUS_CLASS);
|
||||
cls.remove(AFlag.DONT_GENERATE);
|
||||
@@ -784,6 +805,8 @@ public class InsnGen {
|
||||
ClassGen classGen = new ClassGen(cls, mgen.getClassGen().getParentGen());
|
||||
classGen.setOuterNameGen(mgen.getNameGen());
|
||||
classGen.addClassBody(code, true);
|
||||
|
||||
mth.getParentClass().addInlinedClass(cls);
|
||||
}
|
||||
|
||||
private void makeInvoke(InvokeNode insn, ICodeWriter code) throws CodegenException {
|
||||
@@ -795,11 +818,23 @@ public class InsnGen {
|
||||
MethodInfo callMth = insn.getCallMth();
|
||||
MethodNode callMthNode = mth.root().resolveMethod(callMth);
|
||||
|
||||
if (type == InvokeType.CUSTOM_RAW) {
|
||||
makeInvokeCustomRaw((InvokeCustomRawNode) insn, callMthNode, code);
|
||||
return;
|
||||
}
|
||||
if (insn.isPolymorphicCall()) {
|
||||
// add missing cast
|
||||
code.add('(');
|
||||
useType(code, callMth.getReturnType());
|
||||
code.add(") ");
|
||||
}
|
||||
|
||||
int k = 0;
|
||||
switch (type) {
|
||||
case DIRECT:
|
||||
case VIRTUAL:
|
||||
case INTERFACE:
|
||||
case POLYMORPHIC:
|
||||
InsnArg arg = insn.getArg(0);
|
||||
if (needInvokeArg(arg)) {
|
||||
addArgDot(code, arg);
|
||||
@@ -837,6 +872,31 @@ public class InsnGen {
|
||||
generateMethodArguments(code, insn, k, callMthNode);
|
||||
}
|
||||
|
||||
private void makeInvokeCustomRaw(InvokeCustomRawNode insn,
|
||||
@Nullable MethodNode callMthNode, ICodeWriter code) throws CodegenException {
|
||||
if (isFallback()) {
|
||||
code.add("call_site(");
|
||||
code.incIndent();
|
||||
for (EncodedValue value : insn.getCallSiteValues()) {
|
||||
code.startLine(value.toString());
|
||||
}
|
||||
code.decIndent();
|
||||
code.startLine(").invoke");
|
||||
generateMethodArguments(code, insn, 0, callMthNode);
|
||||
} else {
|
||||
ArgType returnType = insn.getCallMth().getReturnType();
|
||||
if (!returnType.isVoid()) {
|
||||
code.add('(');
|
||||
useType(code, returnType);
|
||||
code.add(") ");
|
||||
}
|
||||
makeInvoke(insn.getResolveInvoke(), code);
|
||||
code.add(".dynamicInvoker().invoke");
|
||||
generateMethodArguments(code, insn, 0, callMthNode);
|
||||
code.add(" /* invoke-custom */");
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: add 'this' for equals methods in scope
|
||||
private boolean needInvokeArg(InsnArg arg) {
|
||||
if (arg.isAnyThis()) {
|
||||
@@ -954,9 +1014,10 @@ public class InsnGen {
|
||||
// force set external arg names into call method args
|
||||
int extArgsCount = customNode.getArgsCount();
|
||||
int startArg = customNode.getHandleType() == MethodHandleType.INVOKE_STATIC ? 0 : 1; // skip 'this' arg
|
||||
int callArg = 0;
|
||||
for (int i = startArg; i < extArgsCount; i++) {
|
||||
RegisterArg extArg = (RegisterArg) customNode.getArg(i);
|
||||
RegisterArg callRegArg = callArgs.get(i);
|
||||
RegisterArg callRegArg = callArgs.get(callArg++);
|
||||
callRegArg.getSVar().setCodeVar(extArg.getSVar().getCodeVar());
|
||||
}
|
||||
code.add(" -> {");
|
||||
|
||||
@@ -270,7 +270,7 @@ public class RegionGen extends InsnGen {
|
||||
code.startLine('}');
|
||||
}
|
||||
|
||||
private void addCaseKey(ICodeWriter code, InsnArg arg, Object k) {
|
||||
private void addCaseKey(ICodeWriter code, InsnArg arg, Object k) throws CodegenException {
|
||||
if (k instanceof FieldNode) {
|
||||
FieldNode fn = (FieldNode) k;
|
||||
if (fn.getParentClass().isEnum()) {
|
||||
|
||||
@@ -37,7 +37,9 @@ public enum AFlag {
|
||||
SKIP_FIRST_ARG,
|
||||
SKIP_ARG, // skip argument in invoke call
|
||||
NO_SKIP_ARGS,
|
||||
|
||||
ANONYMOUS_CONSTRUCTOR,
|
||||
INLINE_INSTANCE_FIELD,
|
||||
|
||||
THIS,
|
||||
SUPER,
|
||||
|
||||
@@ -7,12 +7,19 @@ import jadx.core.dex.nodes.ClassNode;
|
||||
|
||||
public class AnonymousClassAttr extends PinnedAttribute {
|
||||
|
||||
public enum InlineType {
|
||||
CONSTRUCTOR,
|
||||
INSTANCE_FIELD,
|
||||
}
|
||||
|
||||
private final ClassNode outerCls;
|
||||
private final ArgType baseType;
|
||||
private final InlineType inlineType;
|
||||
|
||||
public AnonymousClassAttr(ClassNode outerCls, ArgType baseType) {
|
||||
public AnonymousClassAttr(ClassNode outerCls, ArgType baseType, InlineType inlineType) {
|
||||
this.outerCls = outerCls;
|
||||
this.baseType = baseType;
|
||||
this.inlineType = inlineType;
|
||||
}
|
||||
|
||||
public ClassNode getOuterCls() {
|
||||
@@ -23,6 +30,10 @@ public class AnonymousClassAttr extends PinnedAttribute {
|
||||
return baseType;
|
||||
}
|
||||
|
||||
public InlineType getInlineType() {
|
||||
return inlineType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AType<AnonymousClassAttr> getAttrType() {
|
||||
return AType.ANONYMOUS_CLASS;
|
||||
@@ -30,6 +41,6 @@ public class AnonymousClassAttr extends PinnedAttribute {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AnonymousClass{" + outerCls + ", base: " + baseType + '}';
|
||||
return "AnonymousClass{" + outerCls + ", base: " + baseType + ", inline type: " + inlineType + '}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package jadx.core.dex.info;
|
||||
|
||||
import org.intellij.lang.annotations.MagicConstant;
|
||||
|
||||
import jadx.api.plugins.input.data.AccessFlags;
|
||||
import jadx.core.Consts;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
@@ -20,10 +22,21 @@ public class AccessInfo {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@MagicConstant(valuesFromClass = AccessFlags.class)
|
||||
public boolean containsFlag(int flag) {
|
||||
return (accFlags & flag) != 0;
|
||||
}
|
||||
|
||||
@MagicConstant(valuesFromClass = AccessFlags.class)
|
||||
public boolean containsFlags(int... flags) {
|
||||
for (int flag : flags) {
|
||||
if ((accFlags & flag) == 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public AccessInfo remove(int flag) {
|
||||
if (containsFlag(flag)) {
|
||||
return new AccessInfo(accFlags & ~flag, type);
|
||||
|
||||
@@ -82,16 +82,24 @@ public class ConstStorage {
|
||||
return;
|
||||
}
|
||||
for (FieldNode f : staticFields) {
|
||||
AccessInfo accFlags = f.getAccessFlags();
|
||||
if (accFlags.isStatic() && accFlags.isFinal()) {
|
||||
EncodedValue constVal = f.get(JadxAttrType.CONSTANT_VALUE);
|
||||
if (constVal != null && constVal.getValue() != null) {
|
||||
addConstField(cls, f, constVal.getValue(), accFlags.isPublic());
|
||||
}
|
||||
Object value = getFieldConstValue(f);
|
||||
if (value != null) {
|
||||
addConstField(cls, f, value, f.getAccessFlags().isPublic());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static @Nullable Object getFieldConstValue(FieldNode fld) {
|
||||
AccessInfo accFlags = fld.getAccessFlags();
|
||||
if (accFlags.isStatic() && accFlags.isFinal()) {
|
||||
EncodedValue constVal = fld.get(JadxAttrType.CONSTANT_VALUE);
|
||||
if (constVal != null) {
|
||||
return constVal.getValue();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void removeForClass(ClassNode cls) {
|
||||
classes.remove(cls);
|
||||
globalValues.removeForCls(cls);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package jadx.core.dex.instructions;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -7,6 +8,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.plugins.input.data.ICodeReader;
|
||||
import jadx.api.plugins.input.data.IMethodProto;
|
||||
import jadx.api.plugins.input.data.IMethodRef;
|
||||
import jadx.api.plugins.input.insns.InsnData;
|
||||
import jadx.api.plugins.input.insns.custom.IArrayPayload;
|
||||
@@ -25,6 +27,7 @@ import jadx.core.dex.nodes.FieldNode;
|
||||
import jadx.core.dex.nodes.InsnNode;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
import jadx.core.utils.Utils;
|
||||
import jadx.core.utils.exceptions.DecodeException;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.core.utils.input.InsnDataUtils;
|
||||
@@ -440,6 +443,8 @@ public class InsnDecoder {
|
||||
return invokeCustom(insn, false);
|
||||
case INVOKE_SPECIAL:
|
||||
return invokeSpecial(insn);
|
||||
case INVOKE_POLYMORPHIC:
|
||||
return invokePolymorphic(insn, false);
|
||||
|
||||
case INVOKE_DIRECT_RANGE:
|
||||
return invoke(insn, InvokeType.DIRECT, true);
|
||||
@@ -451,6 +456,8 @@ public class InsnDecoder {
|
||||
return invoke(insn, InvokeType.VIRTUAL, true);
|
||||
case INVOKE_CUSTOM_RANGE:
|
||||
return invokeCustom(insn, true);
|
||||
case INVOKE_POLYMORPHIC_RANGE:
|
||||
return invokePolymorphic(insn, true);
|
||||
|
||||
case NEW_INSTANCE:
|
||||
ArgType clsType = ArgType.parse(insn.getIndexAsType());
|
||||
@@ -581,6 +588,22 @@ public class InsnDecoder {
|
||||
return InvokeCustomBuilder.build(method, insn, isRange);
|
||||
}
|
||||
|
||||
private InsnNode invokePolymorphic(InsnData insn, boolean isRange) {
|
||||
IMethodRef mthRef = InsnDataUtils.getMethodRef(insn);
|
||||
if (mthRef == null) {
|
||||
throw new JadxRuntimeException("Failed to load method reference for insn: " + insn);
|
||||
}
|
||||
MethodInfo callMth = MethodInfo.fromRef(root, mthRef);
|
||||
IMethodProto proto = insn.getIndexAsProto(insn.getTarget());
|
||||
|
||||
// expand call args
|
||||
List<ArgType> args = Utils.collectionMap(proto.getArgTypes(), ArgType::parse);
|
||||
ArgType returnType = ArgType.parse(proto.getReturnType());
|
||||
MethodInfo effectiveCallMth = MethodInfo.fromDetails(root, callMth.getDeclClass(),
|
||||
callMth.getName(), args, returnType);
|
||||
return new InvokePolymorphicNode(effectiveCallMth, insn, proto, callMth, isRange);
|
||||
}
|
||||
|
||||
private InsnNode invokeSpecial(InsnData insn) {
|
||||
IMethodRef mthRef = InsnDataUtils.getMethodRef(insn);
|
||||
if (mthRef == null) {
|
||||
|
||||
@@ -5,10 +5,15 @@ import java.util.List;
|
||||
import jadx.api.plugins.input.data.ICallSite;
|
||||
import jadx.api.plugins.input.data.annotations.EncodedValue;
|
||||
import jadx.api.plugins.input.insns.InsnData;
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.attributes.AType;
|
||||
import jadx.core.dex.attributes.nodes.JadxError;
|
||||
import jadx.core.dex.instructions.invokedynamic.CustomLambdaCall;
|
||||
import jadx.core.dex.instructions.invokedynamic.CustomRawCall;
|
||||
import jadx.core.dex.instructions.invokedynamic.CustomStringConcat;
|
||||
import jadx.core.dex.nodes.InsnNode;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
import jadx.core.utils.Utils;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.core.utils.input.InsnDataUtils;
|
||||
|
||||
@@ -28,8 +33,16 @@ public class InvokeCustomBuilder {
|
||||
if (CustomStringConcat.isStringConcat(values)) {
|
||||
return CustomStringConcat.buildStringConcat(insn, isRange, values);
|
||||
}
|
||||
// TODO: output raw dynamic call
|
||||
throw new JadxRuntimeException("Failed to process invoke-custom instruction: " + callSite);
|
||||
try {
|
||||
return CustomRawCall.build(mth, insn, isRange, values);
|
||||
} catch (Exception e) {
|
||||
mth.addWarn("Failed to decode invoke-custom: \n" + Utils.listToString(values, "\n")
|
||||
+ ",\n exception: " + Utils.getStackTrace(e));
|
||||
InsnNode nop = new InsnNode(InsnType.NOP, 0);
|
||||
nop.add(AFlag.SYNTHETIC);
|
||||
nop.addAttr(AType.JADX_ERROR, new JadxError("Failed to decode invoke-custom: " + values, e));
|
||||
return nop;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new JadxRuntimeException("'invoke-custom' instruction processing error: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package jadx.core.dex.instructions;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.api.plugins.input.data.annotations.EncodedValue;
|
||||
import jadx.api.plugins.input.insns.InsnData;
|
||||
import jadx.core.dex.info.MethodInfo;
|
||||
import jadx.core.dex.instructions.args.InsnArg;
|
||||
import jadx.core.dex.instructions.invokedynamic.CustomRawCall;
|
||||
import jadx.core.dex.nodes.InsnNode;
|
||||
import jadx.core.utils.InsnUtils;
|
||||
import jadx.core.utils.Utils;
|
||||
|
||||
/**
|
||||
* Information for raw invoke-custom instruction.<br>
|
||||
* Output will be formatted as polymorphic call with equivalent semantic
|
||||
* Contains two parts:
|
||||
* - resolve: treated as additional invoke insn (uses only constant args)
|
||||
* - invoke: call of resolved method (base for this invoke)
|
||||
* <br>
|
||||
* See {@link CustomRawCall} class for build details
|
||||
*/
|
||||
public class InvokeCustomRawNode extends InvokeNode {
|
||||
private final InvokeNode resolve;
|
||||
private List<EncodedValue> callSiteValues;
|
||||
|
||||
public InvokeCustomRawNode(InvokeNode resolve, MethodInfo mthInfo, InsnData insn, boolean isRange) {
|
||||
super(mthInfo, insn, InvokeType.CUSTOM_RAW, false, isRange);
|
||||
this.resolve = resolve;
|
||||
}
|
||||
|
||||
public InvokeCustomRawNode(InvokeNode resolve, MethodInfo mthInfo, InvokeType invokeType, int argsCount) {
|
||||
super(mthInfo, invokeType, argsCount);
|
||||
this.resolve = resolve;
|
||||
}
|
||||
|
||||
public InvokeNode getResolveInvoke() {
|
||||
return resolve;
|
||||
}
|
||||
|
||||
public void setCallSiteValues(List<EncodedValue> callSiteValues) {
|
||||
this.callSiteValues = callSiteValues;
|
||||
}
|
||||
|
||||
public List<EncodedValue> getCallSiteValues() {
|
||||
return callSiteValues;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InsnNode copy() {
|
||||
InvokeCustomRawNode copy = new InvokeCustomRawNode(resolve, getCallMth(), getInvokeType(), getArgsCount());
|
||||
copyCommonParams(copy);
|
||||
copy.setCallSiteValues(callSiteValues);
|
||||
return copy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStaticCall() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFirstArgOffset() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable InsnArg getInstanceArg() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSame(InsnNode obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj instanceof InvokeCustomRawNode) {
|
||||
return super.isSame(obj) && resolve.isSame(((InvokeCustomRawNode) obj).resolve);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(InsnUtils.formatOffset(offset)).append(": INVOKE_CUSTOM ");
|
||||
if (getResult() != null) {
|
||||
sb.append(getResult()).append(" = ");
|
||||
}
|
||||
if (!appendArgs(sb)) {
|
||||
sb.append('\n');
|
||||
}
|
||||
sb.append(" call-site: \n ").append(Utils.listToString(callSiteValues, "\n ")).append('\n');
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -67,6 +67,19 @@ public class InvokeNode extends BaseInvokeNode {
|
||||
return type == InvokeType.STATIC;
|
||||
}
|
||||
|
||||
public boolean isPolymorphicCall() {
|
||||
if (type == InvokeType.POLYMORPHIC) {
|
||||
return true;
|
||||
}
|
||||
// java bytecode uses virtual call with modified method info
|
||||
if (type == InvokeType.VIRTUAL
|
||||
&& mth.getDeclClass().getFullName().equals("java.lang.invoke.MethodHandle")
|
||||
&& (mth.getName().equals("invoke") || mth.getName().equals("invokeExact"))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public int getFirstArgOffset() {
|
||||
return type == InvokeType.STATIC ? 0 : 1;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package jadx.core.dex.instructions;
|
||||
|
||||
import jadx.api.plugins.input.data.IMethodProto;
|
||||
import jadx.api.plugins.input.insns.InsnData;
|
||||
import jadx.core.dex.info.MethodInfo;
|
||||
import jadx.core.dex.nodes.InsnNode;
|
||||
import jadx.core.utils.InsnUtils;
|
||||
|
||||
public class InvokePolymorphicNode extends InvokeNode {
|
||||
private final IMethodProto proto;
|
||||
private final MethodInfo baseCallRef;
|
||||
|
||||
public InvokePolymorphicNode(MethodInfo callMth, InsnData insn, IMethodProto proto, MethodInfo baseRef, boolean isRange) {
|
||||
super(callMth, insn, InvokeType.POLYMORPHIC, true, isRange);
|
||||
this.proto = proto;
|
||||
this.baseCallRef = baseRef;
|
||||
}
|
||||
|
||||
public InvokePolymorphicNode(MethodInfo callMth, int argsCount, IMethodProto proto, MethodInfo baseRef) {
|
||||
super(callMth, InvokeType.POLYMORPHIC, argsCount);
|
||||
this.proto = proto;
|
||||
this.baseCallRef = baseRef;
|
||||
}
|
||||
|
||||
public IMethodProto getProto() {
|
||||
return proto;
|
||||
}
|
||||
|
||||
public MethodInfo getBaseCallRef() {
|
||||
return baseCallRef;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InsnNode copy() {
|
||||
InvokePolymorphicNode copy = new InvokePolymorphicNode(getCallMth(), getArgsCount(), proto, baseCallRef);
|
||||
copyCommonParams(copy);
|
||||
return copy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSame(InsnNode obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (!(obj instanceof InvokePolymorphicNode) || !super.isSame(obj)) {
|
||||
return false;
|
||||
}
|
||||
InvokePolymorphicNode other = (InvokePolymorphicNode) obj;
|
||||
return proto.equals(other.proto);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(InsnUtils.formatOffset(offset)).append(": INVOKE_POLYMORPHIC ");
|
||||
if (getResult() != null) {
|
||||
sb.append(getResult()).append(" = ");
|
||||
}
|
||||
if (!appendArgs(sb)) {
|
||||
sb.append('\n');
|
||||
}
|
||||
sb.append(" base: ").append(baseCallRef).append('\n');
|
||||
sb.append(" proto: ").append(proto).append('\n');
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -8,4 +8,5 @@ public enum InvokeType {
|
||||
SUPER,
|
||||
POLYMORPHIC,
|
||||
CUSTOM,
|
||||
CUSTOM_RAW,
|
||||
}
|
||||
|
||||
@@ -264,6 +264,13 @@ public abstract class InsnArg extends Typed {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isSameVar(RegisterArg arg) {
|
||||
if (isRegister()) {
|
||||
return ((RegisterArg) this).sameRegAndSVar(arg);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected final <T extends InsnArg> T copyCommonParams(T copy) {
|
||||
copy.copyAttributesFrom(this);
|
||||
copy.setParentInsn(parentInsn);
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
package jadx.core.dex.instructions.args;
|
||||
|
||||
public enum PrimitiveType {
|
||||
BOOLEAN("Z", "boolean"),
|
||||
CHAR("C", "char"),
|
||||
BYTE("B", "byte"),
|
||||
SHORT("S", "short"),
|
||||
INT("I", "int"),
|
||||
FLOAT("F", "float"),
|
||||
LONG("J", "long"),
|
||||
DOUBLE("D", "double"),
|
||||
OBJECT("L", "OBJECT"),
|
||||
ARRAY("[", "ARRAY"),
|
||||
VOID("V", "void");
|
||||
BOOLEAN("Z", "boolean", ArgType.object("java.lang.Boolean")),
|
||||
CHAR("C", "char", ArgType.object("java.lang.Character")),
|
||||
BYTE("B", "byte", ArgType.object("java.lang.Byte")),
|
||||
SHORT("S", "short", ArgType.object("java.lang.Short")),
|
||||
INT("I", "int", ArgType.object("java.lang.Integer")),
|
||||
FLOAT("F", "float", ArgType.object("java.lang.Float")),
|
||||
LONG("J", "long", ArgType.object("java.lang.Long")),
|
||||
DOUBLE("D", "double", ArgType.object("java.lang.Double")),
|
||||
OBJECT("L", "OBJECT", ArgType.OBJECT),
|
||||
ARRAY("[", "ARRAY", ArgType.OBJECT_ARRAY),
|
||||
VOID("V", "void", ArgType.object("java.lang.Void"));
|
||||
|
||||
private final String shortName;
|
||||
private final String longName;
|
||||
private final ArgType boxType;
|
||||
|
||||
PrimitiveType(String shortName, String longName) {
|
||||
PrimitiveType(String shortName, String longName, ArgType boxType) {
|
||||
this.shortName = shortName;
|
||||
this.longName = longName;
|
||||
this.boxType = boxType;
|
||||
}
|
||||
|
||||
public String getShortName() {
|
||||
@@ -29,6 +31,10 @@ public enum PrimitiveType {
|
||||
return longName;
|
||||
}
|
||||
|
||||
public ArgType getBoxType() {
|
||||
return boxType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return longName;
|
||||
|
||||
+9
-23
@@ -8,6 +8,7 @@ import jadx.api.plugins.input.data.IMethodHandle;
|
||||
import jadx.api.plugins.input.data.IMethodProto;
|
||||
import jadx.api.plugins.input.data.IMethodRef;
|
||||
import jadx.api.plugins.input.data.MethodHandleType;
|
||||
import jadx.api.plugins.input.data.annotations.EncodedType;
|
||||
import jadx.api.plugins.input.data.annotations.EncodedValue;
|
||||
import jadx.api.plugins.input.insns.InsnData;
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
@@ -34,18 +35,20 @@ public class CustomLambdaCall {
|
||||
if (values.size() < 6) {
|
||||
return false;
|
||||
}
|
||||
IMethodHandle methodHandle = (IMethodHandle) values.get(0).getValue();
|
||||
EncodedValue mthRef = values.get(0);
|
||||
if (mthRef.getType() != EncodedType.ENCODED_METHOD_HANDLE) {
|
||||
return false;
|
||||
}
|
||||
IMethodHandle methodHandle = (IMethodHandle) mthRef.getValue();
|
||||
if (methodHandle.getType() != MethodHandleType.INVOKE_STATIC) {
|
||||
return false;
|
||||
}
|
||||
IMethodRef methodRef = methodHandle.getMethodRef();
|
||||
if (!methodRef.getName().equals("metafactory")) {
|
||||
return false;
|
||||
}
|
||||
if (!methodRef.getParentClassType().equals("Ljava/lang/invoke/LambdaMetafactory;")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
String mthName = methodRef.getName();
|
||||
return mthName.equals("metafactory") || mthName.equals("altMetafactory");
|
||||
}
|
||||
|
||||
public static InvokeCustomNode buildLambdaMethodCall(MethodNode mth, InsnData insn, boolean isRange, List<EncodedValue> values) {
|
||||
@@ -115,7 +118,7 @@ public class CustomLambdaCall {
|
||||
@NotNull
|
||||
private static InvokeNode buildInvokeNode(MethodHandleType methodHandleType, InvokeCustomNode invokeCustomNode,
|
||||
MethodInfo callMthInfo) {
|
||||
InvokeType invokeType = convertInvokeType(methodHandleType);
|
||||
InvokeType invokeType = InvokeCustomUtils.convertInvokeType(methodHandleType);
|
||||
int callArgsCount = callMthInfo.getArgsCount();
|
||||
boolean instanceCall = invokeType != InvokeType.STATIC;
|
||||
if (instanceCall) {
|
||||
@@ -149,21 +152,4 @@ public class CustomLambdaCall {
|
||||
}
|
||||
return invokeNode;
|
||||
}
|
||||
|
||||
private static InvokeType convertInvokeType(MethodHandleType type) {
|
||||
switch (type) {
|
||||
case INVOKE_STATIC:
|
||||
return InvokeType.STATIC;
|
||||
case INVOKE_INSTANCE:
|
||||
return InvokeType.VIRTUAL;
|
||||
case INVOKE_DIRECT:
|
||||
case INVOKE_CONSTRUCTOR:
|
||||
return InvokeType.DIRECT;
|
||||
case INVOKE_INTERFACE:
|
||||
return InvokeType.INTERFACE;
|
||||
|
||||
default:
|
||||
throw new JadxRuntimeException("Unsupported method handle type: " + type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package jadx.core.dex.instructions.invokedynamic;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import jadx.api.plugins.input.data.IMethodHandle;
|
||||
import jadx.api.plugins.input.data.IMethodProto;
|
||||
import jadx.api.plugins.input.data.annotations.EncodedValue;
|
||||
import jadx.api.plugins.input.insns.InsnData;
|
||||
import jadx.core.dex.info.ClassInfo;
|
||||
import jadx.core.dex.info.MethodInfo;
|
||||
import jadx.core.dex.instructions.ConstStringNode;
|
||||
import jadx.core.dex.instructions.InvokeCustomRawNode;
|
||||
import jadx.core.dex.instructions.InvokeNode;
|
||||
import jadx.core.dex.instructions.InvokeType;
|
||||
import jadx.core.dex.instructions.args.ArgType;
|
||||
import jadx.core.dex.instructions.args.InsnArg;
|
||||
import jadx.core.dex.nodes.InsnNode;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
|
||||
import static jadx.core.utils.EncodedValueUtils.buildLookupArg;
|
||||
import static jadx.core.utils.EncodedValueUtils.convertToInsnArg;
|
||||
|
||||
/**
|
||||
* Show `invoke-custom` similar to polymorphic call
|
||||
*/
|
||||
public class CustomRawCall {
|
||||
|
||||
public static InsnNode build(MethodNode mth, InsnData insn, boolean isRange, List<EncodedValue> values) {
|
||||
IMethodHandle resolveHandle = (IMethodHandle) values.get(0).getValue();
|
||||
String invokeName = (String) values.get(1).getValue();
|
||||
IMethodProto invokeProto = (IMethodProto) values.get(2).getValue();
|
||||
List<InsnArg> resolveArgs = buildArgs(mth, values);
|
||||
|
||||
if (resolveHandle.getType().isField()) {
|
||||
throw new JadxRuntimeException("Field handle not yet supported");
|
||||
}
|
||||
|
||||
RootNode root = mth.root();
|
||||
MethodInfo resolveMth = MethodInfo.fromRef(root, resolveHandle.getMethodRef());
|
||||
InvokeType resolveInvokeType = InvokeCustomUtils.convertInvokeType(resolveHandle.getType());
|
||||
InvokeNode resolve = new InvokeNode(resolveMth, resolveInvokeType, resolveArgs.size());
|
||||
resolveArgs.forEach(resolve::addArg);
|
||||
|
||||
ClassInfo invokeCls = ClassInfo.fromType(root, ArgType.OBJECT); // type will be known at runtime
|
||||
MethodInfo invokeMth = MethodInfo.fromMethodProto(root, invokeCls, invokeName, invokeProto);
|
||||
InvokeCustomRawNode customRawNode = new InvokeCustomRawNode(resolve, invokeMth, insn, isRange);
|
||||
customRawNode.setCallSiteValues(values);
|
||||
return customRawNode;
|
||||
}
|
||||
|
||||
private static List<InsnArg> buildArgs(MethodNode mth, List<EncodedValue> values) {
|
||||
int valuesCount = values.size();
|
||||
List<InsnArg> list = new ArrayList<>(valuesCount);
|
||||
RootNode root = mth.root();
|
||||
list.add(buildLookupArg(root)); // use `java.lang.invoke.MethodHandles.lookup()` as first arg
|
||||
for (int i = 1; i < valuesCount; i++) {
|
||||
EncodedValue value = values.get(i);
|
||||
try {
|
||||
list.add(convertToInsnArg(root, value));
|
||||
} catch (Exception e) {
|
||||
mth.addWarnComment("Failed to build arg in invoke-custom insn: " + value, e);
|
||||
list.add(InsnArg.wrapArg(new ConstStringNode(value.toString())));
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package jadx.core.dex.instructions.invokedynamic;
|
||||
|
||||
import jadx.api.plugins.input.data.MethodHandleType;
|
||||
import jadx.core.dex.instructions.InvokeType;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
|
||||
public class InvokeCustomUtils {
|
||||
|
||||
public static InvokeType convertInvokeType(MethodHandleType type) {
|
||||
switch (type) {
|
||||
case INVOKE_STATIC:
|
||||
return InvokeType.STATIC;
|
||||
case INVOKE_INSTANCE:
|
||||
return InvokeType.VIRTUAL;
|
||||
case INVOKE_DIRECT:
|
||||
case INVOKE_CONSTRUCTOR:
|
||||
return InvokeType.DIRECT;
|
||||
case INVOKE_INTERFACE:
|
||||
return InvokeType.INTERFACE;
|
||||
|
||||
default:
|
||||
throw new JadxRuntimeException("Unsupported method handle type: " + type);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,14 +13,13 @@ import java.util.stream.Collectors;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.DecompilationMode;
|
||||
import jadx.api.ICodeCache;
|
||||
import jadx.api.ICodeInfo;
|
||||
import jadx.api.ICodeWriter;
|
||||
import jadx.api.JadxArgs;
|
||||
import jadx.api.JavaClass;
|
||||
import jadx.api.impl.SimpleCodeInfo;
|
||||
import jadx.api.plugins.input.data.IClassData;
|
||||
import jadx.api.plugins.input.data.IFieldData;
|
||||
@@ -55,8 +54,6 @@ import static jadx.core.dex.nodes.ProcessState.LOADED;
|
||||
import static jadx.core.dex.nodes.ProcessState.NOT_LOADED;
|
||||
|
||||
public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeNode, Comparable<ClassNode> {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ClassNode.class);
|
||||
|
||||
private final RootNode root;
|
||||
private final IClassData clsData;
|
||||
|
||||
@@ -100,6 +97,8 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
|
||||
// cache maps
|
||||
private Map<MethodInfo, MethodNode> mthInfoMap = Collections.emptyMap();
|
||||
|
||||
private JavaClass javaNode;
|
||||
|
||||
public ClassNode(RootNode root, IClassData cls) {
|
||||
this.root = root;
|
||||
this.clsInfo = ClassInfo.fromType(root, ArgType.object(cls.getType()));
|
||||
@@ -171,10 +170,10 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
|
||||
return ArgType.object(superType);
|
||||
}
|
||||
|
||||
public void updateGenericClsData(ArgType superClass, List<ArgType> interfaces, List<ArgType> generics) {
|
||||
public void updateGenericClsData(List<ArgType> generics, ArgType superClass, List<ArgType> interfaces) {
|
||||
this.generics = generics;
|
||||
this.superClass = superClass;
|
||||
this.interfaces = interfaces;
|
||||
this.generics = generics;
|
||||
}
|
||||
|
||||
private static void processAttributes(ClassNode cls) {
|
||||
@@ -835,6 +834,14 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
|
||||
return clsData == null ? "synthetic" : clsData.getInputFileName();
|
||||
}
|
||||
|
||||
public JavaClass getJavaNode() {
|
||||
return javaNode;
|
||||
}
|
||||
|
||||
public void setJavaNode(JavaClass javaNode) {
|
||||
this.javaNode = javaNode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AnnType getAnnType() {
|
||||
return AnnType.CLASS;
|
||||
|
||||
@@ -3,6 +3,7 @@ package jadx.core.dex.nodes;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import jadx.api.JavaField;
|
||||
import jadx.api.plugins.input.data.IFieldData;
|
||||
import jadx.core.dex.attributes.nodes.NotificationAttrNode;
|
||||
import jadx.core.dex.info.AccessInfo;
|
||||
@@ -21,6 +22,8 @@ public class FieldNode extends NotificationAttrNode implements ICodeNode {
|
||||
|
||||
private List<MethodNode> useIn = Collections.emptyList();
|
||||
|
||||
private JavaField javaNode;
|
||||
|
||||
public static FieldNode build(ClassNode cls, IFieldData fieldData) {
|
||||
FieldInfo fieldInfo = FieldInfo.fromRef(cls.root(), fieldData);
|
||||
FieldNode fieldNode = new FieldNode(cls, fieldInfo, fieldData.getAccessFlags());
|
||||
@@ -112,6 +115,14 @@ public class FieldNode extends NotificationAttrNode implements ICodeNode {
|
||||
return parentClass.root();
|
||||
}
|
||||
|
||||
public JavaField getJavaNode() {
|
||||
return javaNode;
|
||||
}
|
||||
|
||||
public void setJavaNode(JavaField javaNode) {
|
||||
this.javaNode = javaNode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AnnType getAnnType() {
|
||||
return AnnType.FIELD;
|
||||
|
||||
@@ -539,19 +539,25 @@ public class InsnNode extends LineAttrNode {
|
||||
return super.equals(obj);
|
||||
}
|
||||
|
||||
protected void appendArgs(StringBuilder sb) {
|
||||
/**
|
||||
* Append arguments type, wrap line if too long
|
||||
*
|
||||
* @return true if args wrapped
|
||||
*/
|
||||
protected boolean appendArgs(StringBuilder sb) {
|
||||
if (arguments.isEmpty()) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
String argsStr = Utils.listToString(arguments);
|
||||
if (argsStr.length() < 120) {
|
||||
sb.append(argsStr);
|
||||
} else {
|
||||
// wrap args
|
||||
String separator = ICodeWriter.NL + " ";
|
||||
sb.append(separator).append(Utils.listToString(arguments, separator));
|
||||
sb.append(ICodeWriter.NL);
|
||||
return false;
|
||||
}
|
||||
// wrap args
|
||||
String separator = ICodeWriter.NL + " ";
|
||||
sb.append(separator).append(Utils.listToString(arguments, separator));
|
||||
sb.append(ICodeWriter.NL);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.JavaMethod;
|
||||
import jadx.api.plugins.input.data.ICodeReader;
|
||||
import jadx.api.plugins.input.data.IDebugInfo;
|
||||
import jadx.api.plugins.input.data.IMethodData;
|
||||
@@ -71,6 +72,8 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
|
||||
|
||||
private List<MethodNode> useIn = Collections.emptyList();
|
||||
|
||||
private JavaMethod javaNode;
|
||||
|
||||
public static MethodNode build(ClassNode classNode, IMethodData methodData) {
|
||||
MethodNode methodNode = new MethodNode(classNode, methodData);
|
||||
methodNode.addAttrs(methodData.getAttributes());
|
||||
@@ -610,6 +613,14 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
|
||||
this.useIn = useIn;
|
||||
}
|
||||
|
||||
public JavaMethod getJavaNode() {
|
||||
return javaNode;
|
||||
}
|
||||
|
||||
public void setJavaNode(JavaMethod javaNode) {
|
||||
this.javaNode = javaNode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AnnType getAnnType() {
|
||||
return AnnType.METHOD;
|
||||
|
||||
@@ -43,6 +43,7 @@ import jadx.core.utils.Utils;
|
||||
import jadx.core.utils.android.AndroidResourcesUtils;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.core.xmlgen.IResParser;
|
||||
import jadx.core.xmlgen.ManifestAttributes;
|
||||
import jadx.core.xmlgen.ResDecoder;
|
||||
import jadx.core.xmlgen.ResourceStorage;
|
||||
import jadx.core.xmlgen.entry.ResourceEntry;
|
||||
@@ -174,12 +175,18 @@ public class RootNode {
|
||||
if (parser != null) {
|
||||
processResources(parser.getResStorage());
|
||||
updateObfuscatedFiles(parser, resources);
|
||||
updateManifestAttribMap(parser);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to parse '.arsc' file", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateManifestAttribMap(IResParser parser) {
|
||||
ManifestAttributes manifestAttributes = ManifestAttributes.getInstance();
|
||||
manifestAttributes.updateAttributes(parser);
|
||||
}
|
||||
|
||||
private @Nullable ResourceFile getResourceFile(List<ResourceFile> resources) {
|
||||
for (ResourceFile rf : resources) {
|
||||
if (rf.getType() == ResourceType.ARSC) {
|
||||
|
||||
@@ -125,17 +125,30 @@ public class ConstInlineVisitor extends AbstractVisitor {
|
||||
int k = 0;
|
||||
for (RegisterArg useArg : useList) {
|
||||
InsnNode insn = useArg.getParentInsn();
|
||||
if (insn == null) {
|
||||
continue;
|
||||
}
|
||||
if (!canUseNull(insn, useArg)) {
|
||||
useArg.add(AFlag.DONT_INLINE_CONST);
|
||||
if (insn != null && forbidNullArgInline(insn, useArg)) {
|
||||
k++;
|
||||
}
|
||||
}
|
||||
return k == useList.size();
|
||||
}
|
||||
|
||||
private static boolean forbidNullArgInline(InsnNode insn, RegisterArg useArg) {
|
||||
switch (insn.getType()) {
|
||||
case MOVE:
|
||||
case CAST:
|
||||
case CHECK_CAST:
|
||||
// result is null, chain checks
|
||||
return forbidNullInlines(insn.getResult().getSVar());
|
||||
|
||||
default:
|
||||
if (!canUseNull(insn, useArg)) {
|
||||
useArg.add(AFlag.DONT_INLINE_CONST);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean canUseNull(InsnNode insn, RegisterArg useArg) {
|
||||
switch (insn.getType()) {
|
||||
case INVOKE:
|
||||
|
||||
@@ -384,8 +384,14 @@ public class ExtractFieldInit extends AbstractVisitor {
|
||||
return list;
|
||||
}
|
||||
|
||||
private static void addFieldInitAttr(MethodNode mth, FieldNode field, InsnNode insn) {
|
||||
InsnNode assignInsn = InsnNode.wrapArg(insn.getArg(0));
|
||||
private static void addFieldInitAttr(MethodNode mth, FieldNode field, IndexInsnNode putInsn) {
|
||||
InsnNode assignInsn;
|
||||
InsnArg fldArg = putInsn.getArg(0);
|
||||
if (fldArg.isInsnWrap()) {
|
||||
assignInsn = ((InsnWrapArg) fldArg).getWrapInsn();
|
||||
} else {
|
||||
assignInsn = InsnNode.wrapArg(fldArg);
|
||||
}
|
||||
field.addAttr(new FieldInitInsnAttr(mth, assignInsn));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,13 +80,47 @@ public class MarkMethodsForInline extends AbstractVisitor {
|
||||
return addInlineAttr(mth, insn);
|
||||
}
|
||||
if (insnsCount == 2 && insns.get(1).getType() == InsnType.RETURN) {
|
||||
// synthetic field setter
|
||||
return addInlineAttr(mth, insns.get(0));
|
||||
InsnNode firstInsn = insns.get(0);
|
||||
InsnNode retInsn = insns.get(1);
|
||||
if (retInsn.getArgsCount() == 0
|
||||
|| isSyntheticAccessPattern(mth, firstInsn, retInsn)) {
|
||||
return addInlineAttr(mth, firstInsn);
|
||||
}
|
||||
}
|
||||
// TODO: inline field arithmetics. Disabled tests: TestAnonymousClass3a and TestAnonymousClass5
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isSyntheticAccessPattern(MethodNode mth, InsnNode firstInsn, InsnNode retInsn) {
|
||||
List<RegisterArg> mthRegs = mth.getArgRegs();
|
||||
switch (firstInsn.getType()) {
|
||||
case IGET:
|
||||
return mthRegs.size() == 1
|
||||
&& retInsn.getArg(0).isSameVar(firstInsn.getResult())
|
||||
&& firstInsn.getArg(0).isSameVar(mthRegs.get(0));
|
||||
case SGET:
|
||||
return mthRegs.size() == 0
|
||||
&& retInsn.getArg(0).isSameVar(firstInsn.getResult());
|
||||
|
||||
case IPUT:
|
||||
return mthRegs.size() == 2
|
||||
&& retInsn.getArg(0).isSameVar(mthRegs.get(1))
|
||||
&& firstInsn.getArg(0).isSameVar(mthRegs.get(1))
|
||||
&& firstInsn.getArg(1).isSameVar(mthRegs.get(0));
|
||||
case SPUT:
|
||||
return mthRegs.size() == 1
|
||||
&& retInsn.getArg(0).isSameVar(mthRegs.get(0))
|
||||
&& firstInsn.getArg(0).isSameVar(mthRegs.get(0));
|
||||
|
||||
case INVOKE:
|
||||
return mthRegs.size() >= 1
|
||||
&& firstInsn.getArg(0).isSameVar(mthRegs.get(0))
|
||||
&& retInsn.getArg(0).isSameVar(firstInsn.getResult());
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static MethodInlineAttr addInlineAttr(MethodNode mth, InsnNode insn) {
|
||||
if (!fixVisibilityOfInlineCode(mth, insn)) {
|
||||
return null;
|
||||
|
||||
@@ -90,7 +90,7 @@ public class OverrideMethodVisitor extends AbstractVisitor {
|
||||
for (ArgType superType : superData.getSuperTypes()) {
|
||||
ClassNode classNode = mth.root().resolveClass(superType);
|
||||
if (classNode != null) {
|
||||
MethodNode ovrdMth = searchOverriddenMethod(classNode, signature);
|
||||
MethodNode ovrdMth = searchOverriddenMethod(classNode, mth, signature);
|
||||
if (ovrdMth != null) {
|
||||
if (isMethodVisibleInCls(ovrdMth, cls)) {
|
||||
overrideList.add(ovrdMth);
|
||||
@@ -107,6 +107,8 @@ public class OverrideMethodVisitor extends AbstractVisitor {
|
||||
Map<String, ClspMethod> methodsMap = clsDetails.getMethodsMap();
|
||||
for (Map.Entry<String, ClspMethod> entry : methodsMap.entrySet()) {
|
||||
String mthShortId = entry.getKey();
|
||||
// do not check full signature, classpath methods can be trusted
|
||||
// i.e. doesn't contain methods with same signature in one class
|
||||
if (mthShortId.startsWith(signature)) {
|
||||
overrideList.add(entry.getValue());
|
||||
break;
|
||||
@@ -130,12 +132,30 @@ public class OverrideMethodVisitor extends AbstractVisitor {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private MethodNode searchOverriddenMethod(ClassNode cls, String signature) {
|
||||
private MethodNode searchOverriddenMethod(ClassNode cls, MethodNode mth, String signature) {
|
||||
// search by exact full signature (with return value) to fight obfuscation (see test
|
||||
// 'TestOverrideWithSameName')
|
||||
String shortId = mth.getMethodInfo().getShortId();
|
||||
for (MethodNode supMth : cls.getMethods()) {
|
||||
if (!supMth.getAccessFlags().isStatic() && supMth.getMethodInfo().getShortId().startsWith(signature)) {
|
||||
if (supMth.getMethodInfo().getShortId().equals(shortId) && !supMth.getAccessFlags().isStatic()) {
|
||||
return supMth;
|
||||
}
|
||||
}
|
||||
// search by signature without return value and check if return value is wider type
|
||||
for (MethodNode supMth : cls.getMethods()) {
|
||||
if (supMth.getMethodInfo().getShortId().startsWith(signature) && !supMth.getAccessFlags().isStatic()) {
|
||||
TypeCompare typeCompare = cls.root().getTypeCompare();
|
||||
ArgType supRetType = supMth.getMethodInfo().getReturnType();
|
||||
ArgType mthRetType = mth.getMethodInfo().getReturnType();
|
||||
TypeCompareEnum res = typeCompare.compareTypes(supRetType, mthRetType);
|
||||
if (res.isWider()) {
|
||||
return supMth;
|
||||
}
|
||||
if (res == TypeCompareEnum.UNKNOWN || res == TypeCompareEnum.CONFLICT) {
|
||||
mth.addDebugComment("Possible override for method " + supMth.getMethodInfo().getFullId());
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,9 +10,11 @@ import java.util.Set;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.api.plugins.input.data.AccessFlags;
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.attributes.AType;
|
||||
import jadx.core.dex.attributes.nodes.AnonymousClassAttr;
|
||||
import jadx.core.dex.attributes.nodes.AnonymousClassAttr.InlineType;
|
||||
import jadx.core.dex.info.AccessInfo;
|
||||
import jadx.core.dex.instructions.args.ArgType;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
@@ -30,6 +32,7 @@ import jadx.core.utils.exceptions.JadxException;
|
||||
UsageInfoVisitor.class
|
||||
}
|
||||
)
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public class ProcessAnonymous extends AbstractVisitor {
|
||||
|
||||
private boolean inlineAnonymousClasses;
|
||||
@@ -64,17 +67,26 @@ public class ProcessAnonymous extends AbstractVisitor {
|
||||
if (!canBeAnonymous(cls)) {
|
||||
return;
|
||||
}
|
||||
MethodNode anonymousConstructor = checkUsage(cls);
|
||||
MethodNode anonymousConstructor = ListUtils.filterOnlyOne(cls.getMethods(), MethodNode::isConstructor);
|
||||
if (anonymousConstructor == null) {
|
||||
return;
|
||||
}
|
||||
InlineType inlineType = checkUsage(cls, anonymousConstructor);
|
||||
if (inlineType == null) {
|
||||
return;
|
||||
}
|
||||
ArgType baseType = getBaseType(cls);
|
||||
if (baseType == null) {
|
||||
return;
|
||||
}
|
||||
ClassNode outerCls = anonymousConstructor.getUseIn().get(0).getParentClass();
|
||||
ClassNode outerCls;
|
||||
if (inlineType == InlineType.INSTANCE_FIELD) {
|
||||
outerCls = cls.getUseInMth().get(0).getParentClass();
|
||||
} else {
|
||||
outerCls = anonymousConstructor.getUseIn().get(0).getParentClass();
|
||||
}
|
||||
outerCls.addInlinedClass(cls);
|
||||
cls.addAttr(new AnonymousClassAttr(outerCls, baseType));
|
||||
cls.addAttr(new AnonymousClassAttr(outerCls, baseType, inlineType));
|
||||
cls.add(AFlag.DONT_GENERATE);
|
||||
anonymousConstructor.add(AFlag.ANONYMOUS_CONSTRUCTOR);
|
||||
|
||||
@@ -202,14 +214,11 @@ public class ProcessAnonymous extends AbstractVisitor {
|
||||
* 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)
|
||||
* - if constructor used only in class init check if possible inline by instance field
|
||||
*
|
||||
* @return anonymous constructor method
|
||||
* @return decided inline type
|
||||
*/
|
||||
private static MethodNode checkUsage(ClassNode cls) {
|
||||
MethodNode ctr = ListUtils.filterOnlyOne(cls.getMethods(), MethodNode::isConstructor);
|
||||
if (ctr == null) {
|
||||
return null;
|
||||
}
|
||||
private static InlineType checkUsage(ClassNode cls, MethodNode ctr) {
|
||||
if (ctr.getUseIn().size() != 1) {
|
||||
// check if used in common field init in all constructors
|
||||
if (!checkForCommonFieldInit(ctr)) {
|
||||
@@ -219,6 +228,9 @@ public class ProcessAnonymous extends AbstractVisitor {
|
||||
MethodNode ctrUseMth = ctr.getUseIn().get(0);
|
||||
ClassNode ctrUseCls = ctrUseMth.getParentClass();
|
||||
if (ctrUseCls.equals(cls)) {
|
||||
if (checkForInstanceFieldUsage(cls, ctr)) {
|
||||
return InlineType.INSTANCE_FIELD;
|
||||
}
|
||||
// exclude self usage
|
||||
return null;
|
||||
}
|
||||
@@ -226,6 +238,20 @@ public class ProcessAnonymous extends AbstractVisitor {
|
||||
// exclude usage inside inner classes
|
||||
return null;
|
||||
}
|
||||
if (!checkMethodsUsage(cls, ctr, ctrUseMth)) {
|
||||
return null;
|
||||
}
|
||||
for (FieldNode field : cls.getFields()) {
|
||||
for (MethodNode useMth : field.getUseIn()) {
|
||||
if (badMethodUsage(cls, useMth, field.getAccessFlags())) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return InlineType.CONSTRUCTOR;
|
||||
}
|
||||
|
||||
private static boolean checkMethodsUsage(ClassNode cls, MethodNode ctr, MethodNode ctrUseMth) {
|
||||
for (MethodNode mth : cls.getMethods()) {
|
||||
if (mth == ctr) {
|
||||
continue;
|
||||
@@ -235,18 +261,46 @@ public class ProcessAnonymous extends AbstractVisitor {
|
||||
continue;
|
||||
}
|
||||
if (badMethodUsage(cls, useMth, mth.getAccessFlags())) {
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean checkForInstanceFieldUsage(ClassNode cls, MethodNode ctr) {
|
||||
MethodNode ctrUseMth = ctr.getUseIn().get(0);
|
||||
if (!ctrUseMth.getMethodInfo().isClassInit()) {
|
||||
return false;
|
||||
}
|
||||
FieldNode instFld = ListUtils.filterOnlyOne(cls.getFields(),
|
||||
f -> f.getAccessFlags().containsFlags(AccessFlags.PUBLIC, AccessFlags.STATIC, AccessFlags.FINAL)
|
||||
&& f.getFieldInfo().getType().equals(cls.getClassInfo().getType()));
|
||||
if (instFld == null) {
|
||||
return false;
|
||||
}
|
||||
List<MethodNode> instFldUseIn = instFld.getUseIn();
|
||||
if (instFldUseIn.size() != 2
|
||||
|| !instFldUseIn.contains(ctrUseMth) // initialized in class init
|
||||
|| !instFldUseIn.containsAll(cls.getUseInMth()) // class used only with this field
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (!checkMethodsUsage(cls, ctr, ctrUseMth)) {
|
||||
return false;
|
||||
}
|
||||
for (FieldNode field : cls.getFields()) {
|
||||
if (field == instFld) {
|
||||
continue;
|
||||
}
|
||||
for (MethodNode useMth : field.getUseIn()) {
|
||||
if (badMethodUsage(cls, useMth, field.getAccessFlags())) {
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return ctr;
|
||||
instFld.add(AFlag.INLINE_INSTANCE_FIELD);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean badMethodUsage(ClassNode cls, MethodNode useMth, AccessInfo accessFlags) {
|
||||
@@ -297,6 +351,13 @@ public class ProcessAnonymous extends AbstractVisitor {
|
||||
if (cls.root().getClsp().isImplements(superCls.getObject(), interfaceType.getObject())) {
|
||||
return superCls;
|
||||
}
|
||||
if (cls.root().getArgs().isAllowInlineKotlinLambda()) {
|
||||
if (superCls.getObject().equals("kotlin.jvm.internal.Lambda")) {
|
||||
// Inline such class with have different semantic: missing 'arity' property.
|
||||
// For now, it is unclear how it may affect code execution.
|
||||
return interfaceType;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,12 @@ package jadx.core.dex.visitors;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.core.dex.info.MethodInfo;
|
||||
import jadx.core.dex.instructions.args.ArgType;
|
||||
@@ -18,7 +22,6 @@ import jadx.core.utils.Utils;
|
||||
import jadx.core.utils.exceptions.JadxException;
|
||||
|
||||
public class SignatureProcessor extends AbstractVisitor {
|
||||
|
||||
private RootNode root;
|
||||
|
||||
@Override
|
||||
@@ -55,12 +58,54 @@ public class SignatureProcessor extends AbstractVisitor {
|
||||
break;
|
||||
}
|
||||
}
|
||||
cls.updateGenericClsData(superClass, interfaces, generics);
|
||||
generics = fixTypeParamDeclarations(cls, generics, superClass, interfaces);
|
||||
cls.updateGenericClsData(generics, superClass, interfaces);
|
||||
} catch (Exception e) {
|
||||
cls.addWarnComment("Failed to parse class signature: " + sp.getSignature(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add missing type parameters from super type and interfaces to make code compilable
|
||||
*/
|
||||
private static List<ArgType> fixTypeParamDeclarations(ClassNode cls,
|
||||
List<ArgType> generics, ArgType superClass, List<ArgType> interfaces) {
|
||||
if (interfaces.isEmpty() && superClass.equals(ArgType.OBJECT)) {
|
||||
return generics;
|
||||
}
|
||||
Set<String> typeParams = new HashSet<>();
|
||||
superClass.visitTypes(t -> addGenericType(typeParams, t));
|
||||
interfaces.forEach(i -> i.visitTypes(t -> addGenericType(typeParams, t)));
|
||||
if (typeParams.isEmpty()) {
|
||||
return generics;
|
||||
}
|
||||
List<ArgType> knownTypeParams;
|
||||
if (cls.isInner()) {
|
||||
knownTypeParams = new ArrayList<>(generics);
|
||||
cls.visitParentClasses(p -> knownTypeParams.addAll(p.getGenericTypeParameters()));
|
||||
} else {
|
||||
knownTypeParams = generics;
|
||||
}
|
||||
for (ArgType declTypeParam : knownTypeParams) {
|
||||
typeParams.remove(declTypeParam.getObject());
|
||||
}
|
||||
if (typeParams.isEmpty()) {
|
||||
return generics;
|
||||
}
|
||||
cls.addInfoComment("Add missing generic type declarations: " + typeParams);
|
||||
List<ArgType> fixedGenerics = new ArrayList<>(generics.size() + typeParams.size());
|
||||
fixedGenerics.addAll(generics);
|
||||
typeParams.stream().sorted().map(ArgType::genericType).forEach(fixedGenerics::add);
|
||||
return fixedGenerics;
|
||||
}
|
||||
|
||||
private static @Nullable Object addGenericType(Set<String> usedTypeParameters, ArgType t) {
|
||||
if (t.isGenericType()) {
|
||||
usedTypeParameters.add(t.getObject());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private ArgType validateClsType(ClassNode cls, ArgType candidateType, ArgType currentType) {
|
||||
if (!candidateType.isObject()) {
|
||||
cls.addWarnComment("Incorrect class signature, class is not object: " + SignatureParser.getSignature(cls));
|
||||
|
||||
@@ -9,6 +9,8 @@ import java.util.Set;
|
||||
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.instructions.InsnType;
|
||||
import jadx.core.dex.instructions.InvokeCustomNode;
|
||||
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.Named;
|
||||
@@ -123,6 +125,9 @@ public class CodeShrinkVisitor extends AbstractVisitor {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!checkLambdaInline(arg, assignInsn)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int assignPos = insnList.getIndex(assignInsn);
|
||||
if (assignPos != -1) {
|
||||
@@ -145,6 +150,26 @@ public class CodeShrinkVisitor extends AbstractVisitor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forbid inline lambda into invoke as an instance arg, i.e. this will not compile:
|
||||
* {@code () -> { ... }.apply(); }
|
||||
*/
|
||||
private static boolean checkLambdaInline(RegisterArg arg, InsnNode assignInsn) {
|
||||
if (assignInsn.getType() == InsnType.INVOKE && assignInsn instanceof InvokeCustomNode) {
|
||||
for (RegisterArg useArg : arg.getSVar().getUseList()) {
|
||||
InsnNode parentInsn = useArg.getParentInsn();
|
||||
if (parentInsn != null && parentInsn.getType() == InsnType.INVOKE) {
|
||||
InvokeNode invokeNode = (InvokeNode) parentInsn;
|
||||
InsnArg instArg = invokeNode.getInstanceArg();
|
||||
if (instArg != null && instArg == useArg) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean varWithSameNameExists(MethodNode mth, SSAVar inlineVar) {
|
||||
for (SSAVar ssaVar : mth.getSVars()) {
|
||||
if (ssaVar == inlineVar || ssaVar.getCodeVar() == inlineVar.getCodeVar()) {
|
||||
|
||||
+1
-1
@@ -325,7 +325,7 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
|
||||
ClassNode ctrCls = root.resolveClass(ctr.getClassType());
|
||||
if (ctrCls != null && ctrCls.contains(AFlag.DONT_GENERATE)) {
|
||||
AnonymousClassAttr baseTypeAttr = ctrCls.get(AType.ANONYMOUS_CLASS);
|
||||
if (baseTypeAttr != null) {
|
||||
if (baseTypeAttr != null && baseTypeAttr.getInlineType() == AnonymousClassAttr.InlineType.CONSTRUCTOR) {
|
||||
return baseTypeAttr.getBaseType();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import java.io.StringReader;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
|
||||
@@ -29,6 +30,8 @@ public class ExportGradleProject {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ExportGradleProject.class);
|
||||
|
||||
private static final Pattern ILLEGAL_GRADLE_CHARS = Pattern.compile("[/\\\\:>\"?*|]");
|
||||
|
||||
private static final Set<String> IGNORE_CLS_NAMES = new HashSet<>(Arrays.asList(
|
||||
"R",
|
||||
"BuildConfig"));
|
||||
@@ -72,7 +75,7 @@ public class ExportGradleProject {
|
||||
private void saveSettingsGradle() throws IOException {
|
||||
TemplateFile tmpl = TemplateFile.fromResources("/export/settings.gradle.tmpl");
|
||||
|
||||
tmpl.add("applicationName", applicationParams.getApplicationName());
|
||||
tmpl.add("applicationName", ILLEGAL_GRADLE_CHARS.matcher(applicationParams.getApplicationName()).replaceAll(""));
|
||||
tmpl.save(new File(projectDir, "settings.gradle"));
|
||||
}
|
||||
|
||||
|
||||
@@ -139,11 +139,13 @@ public class DecompilerScheduler implements IDecompileScheduler {
|
||||
}
|
||||
|
||||
private void dumpBatchesStats(List<JavaClass> classes, List<List<JavaClass>> result, List<DepInfo> deps) {
|
||||
int clsInBatches = result.stream().mapToInt(List::size).sum();
|
||||
double avg = result.stream().mapToInt(List::size).average().orElse(-1);
|
||||
int maxSingleDeps = classes.stream().mapToInt(JavaClass::getTotalDepsCount).max().orElse(-1);
|
||||
int maxSubDeps = deps.stream().mapToInt(DepInfo::getDepsCount).max().orElse(-1);
|
||||
LOG.info("Batches stats:"
|
||||
+ "\n input classes: " + classes.size()
|
||||
+ ",\n classes in batches: " + clsInBatches
|
||||
+ ",\n batches: " + result.size()
|
||||
+ ",\n average batch size: " + String.format("%.2f", avg)
|
||||
+ ",\n max single deps count: " + maxSingleDeps
|
||||
|
||||
@@ -1,11 +1,33 @@
|
||||
package jadx.core.utils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.api.plugins.input.data.IMethodHandle;
|
||||
import jadx.api.plugins.input.data.IMethodProto;
|
||||
import jadx.api.plugins.input.data.IMethodRef;
|
||||
import jadx.api.plugins.input.data.MethodHandleType;
|
||||
import jadx.api.plugins.input.data.annotations.EncodedValue;
|
||||
import jadx.core.dex.info.ClassInfo;
|
||||
import jadx.core.dex.info.FieldInfo;
|
||||
import jadx.core.dex.info.MethodInfo;
|
||||
import jadx.core.dex.instructions.ConstClassNode;
|
||||
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.InvokeType;
|
||||
import jadx.core.dex.instructions.args.ArgType;
|
||||
import jadx.core.dex.instructions.args.InsnArg;
|
||||
import jadx.core.dex.instructions.args.LiteralArg;
|
||||
import jadx.core.dex.instructions.args.PrimitiveType;
|
||||
import jadx.core.dex.nodes.InsnNode;
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
|
||||
public class EncodedValueUtils {
|
||||
|
||||
@@ -50,4 +72,108 @@ public class EncodedValueUtils {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static InsnArg convertToInsnArg(RootNode root, EncodedValue value) {
|
||||
Object obj = value.getValue();
|
||||
switch (value.getType()) {
|
||||
case ENCODED_NULL:
|
||||
case ENCODED_BYTE:
|
||||
case ENCODED_SHORT:
|
||||
case ENCODED_CHAR:
|
||||
case ENCODED_INT:
|
||||
case ENCODED_LONG:
|
||||
case ENCODED_FLOAT:
|
||||
case ENCODED_DOUBLE:
|
||||
return (InsnArg) convertToConstValue(value);
|
||||
|
||||
case ENCODED_BOOLEAN:
|
||||
return InsnArg.lit(((Boolean) obj) ? 0 : 1, ArgType.BOOLEAN);
|
||||
case ENCODED_STRING:
|
||||
return InsnArg.wrapArg(new ConstStringNode((String) obj));
|
||||
case ENCODED_TYPE:
|
||||
return InsnArg.wrapArg(new ConstClassNode(ArgType.parse((String) obj)));
|
||||
case ENCODED_METHOD_TYPE:
|
||||
return InsnArg.wrapArg(buildMethodType(root, (IMethodProto) obj));
|
||||
case ENCODED_METHOD_HANDLE:
|
||||
return InsnArg.wrapArg(buildMethodHandle(root, (IMethodHandle) obj));
|
||||
|
||||
}
|
||||
throw new JadxRuntimeException("Unsupported type for raw invoke-custom: " + value.getType());
|
||||
}
|
||||
|
||||
private static InvokeNode buildMethodType(RootNode root, IMethodProto methodProto) {
|
||||
ArgType retType = ArgType.parse(methodProto.getReturnType());
|
||||
List<ArgType> argTypes = Utils.collectionMap(methodProto.getArgTypes(), ArgType::parse);
|
||||
List<ArgType> callTypes = new ArrayList<>(1 + argTypes.size());
|
||||
callTypes.add(retType);
|
||||
callTypes.addAll(argTypes);
|
||||
ArgType mthType = ArgType.object("java.lang.invoke.MethodType");
|
||||
ClassInfo cls = ClassInfo.fromType(root, mthType);
|
||||
MethodInfo mth = MethodInfo.fromDetails(root, cls, "methodType", callTypes, mthType);
|
||||
InvokeNode invoke = new InvokeNode(mth, InvokeType.STATIC, callTypes.size());
|
||||
for (ArgType type : callTypes) {
|
||||
InsnNode argInsn;
|
||||
if (type.isPrimitive()) {
|
||||
argInsn = new IndexInsnNode(InsnType.SGET, getTypeField(root, type.getPrimitiveType()), 0);
|
||||
} else {
|
||||
argInsn = new ConstClassNode(type);
|
||||
}
|
||||
invoke.addArg(InsnArg.wrapArg(argInsn));
|
||||
}
|
||||
return invoke;
|
||||
}
|
||||
|
||||
public static FieldInfo getTypeField(RootNode root, PrimitiveType type) {
|
||||
ArgType boxType = type.getBoxType();
|
||||
ClassInfo boxCls = ClassInfo.fromType(root, boxType);
|
||||
return FieldInfo.from(root, boxCls, "TYPE", boxType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build `MethodHandles.lookup().find{type}(methodCls, methodName, methodType)`
|
||||
*/
|
||||
private static InsnNode buildMethodHandle(RootNode root, IMethodHandle methodHandle) {
|
||||
if (methodHandle.getType().isField()) {
|
||||
// TODO: lookup for field
|
||||
return new ConstStringNode("FIELD:" + methodHandle.getFieldRef());
|
||||
}
|
||||
IMethodRef methodRef = methodHandle.getMethodRef();
|
||||
methodRef.load();
|
||||
|
||||
ClassInfo lookupCls = ClassInfo.fromName(root, "java.lang.invoke.MethodHandles.Lookup");
|
||||
MethodInfo findMethod = MethodInfo.fromDetails(root, lookupCls,
|
||||
getFindMethodName(methodHandle.getType()),
|
||||
Arrays.asList(ArgType.CLASS, ArgType.STRING, ArgType.object("java.lang.invoke.MethodType")),
|
||||
ArgType.object("java.lang.invoke.MethodHandle"));
|
||||
|
||||
InvokeNode invoke = new InvokeNode(findMethod, InvokeType.DIRECT, 4);
|
||||
invoke.addArg(buildLookupArg(root));
|
||||
invoke.addArg(InsnArg.wrapArg(new ConstClassNode(ArgType.object(methodRef.getParentClassType()))));
|
||||
invoke.addArg(InsnArg.wrapArg(new ConstStringNode(methodRef.getName())));
|
||||
invoke.addArg(InsnArg.wrapArg(buildMethodType(root, methodRef)));
|
||||
return invoke;
|
||||
}
|
||||
|
||||
public static InsnArg buildLookupArg(RootNode root) {
|
||||
ArgType lookupType = ArgType.object("java.lang.invoke.MethodHandles.Lookup");
|
||||
ClassInfo cls = ClassInfo.fromName(root, "java.lang.invoke.MethodHandles");
|
||||
MethodInfo mth = MethodInfo.fromDetails(root, cls, "lookup", Collections.emptyList(), lookupType);
|
||||
return InsnArg.wrapArg(new InvokeNode(mth, InvokeType.STATIC, 0));
|
||||
}
|
||||
|
||||
private static String getFindMethodName(MethodHandleType type) {
|
||||
switch (type) {
|
||||
case INVOKE_STATIC:
|
||||
return "findStatic";
|
||||
case INVOKE_CONSTRUCTOR:
|
||||
return "findConstructor";
|
||||
case INVOKE_INSTANCE:
|
||||
case INVOKE_DIRECT:
|
||||
case INVOKE_INTERFACE:
|
||||
return "findVirtual";
|
||||
|
||||
default:
|
||||
return "<" + type + '>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -51,7 +52,7 @@ public class BinaryXMLParser extends CommonBinaryParser {
|
||||
private boolean isLastEnd = true;
|
||||
private boolean isOneLine = true;
|
||||
private int namespaceDepth = 0;
|
||||
private int[] resourceIds;
|
||||
private @Nullable int[] resourceIds;
|
||||
|
||||
private final RootNode rootNode;
|
||||
private String appPackageName;
|
||||
@@ -358,7 +359,7 @@ public class BinaryXMLParser extends CommonBinaryParser {
|
||||
// As the outcome of https://github.com/skylot/jadx/issues/1208
|
||||
// Android seems to favor entries from AndroidResMap and only if
|
||||
// there is no entry uses the values form the XML string pool
|
||||
if (0 <= id && id < resourceIds.length) {
|
||||
if (resourceIds != null && 0 <= id && id < resourceIds.length) {
|
||||
int resId = resourceIds[id];
|
||||
String str = ValuesParser.getAndroidResMap().get(resId);
|
||||
if (str != null) {
|
||||
|
||||
@@ -26,7 +26,7 @@ public class CommonBinaryParser extends ParserConstants {
|
||||
int[] stringsOffset = is.readInt32Array(stringCount);
|
||||
int[] stylesOffset = is.readInt32Array(styleCount);
|
||||
|
||||
is.checkPos(start + stringsStart, "Expected strings start");
|
||||
is.skipToPos(start + stringsStart, "Expected strings start");
|
||||
String[] strings = new String[stringCount];
|
||||
byte[] strData = is.readInt8Array((int) (chunkEnd - is.getPos()));
|
||||
if ((flags & UTF8_FLAG) != 0) {
|
||||
|
||||
@@ -17,6 +17,9 @@ import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.core.xmlgen.entry.RawNamedValue;
|
||||
import jadx.core.xmlgen.entry.ResourceEntry;
|
||||
import jadx.core.xmlgen.entry.ValuesParser;
|
||||
|
||||
public class ManifestAttributes {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ManifestAttributes.class);
|
||||
@@ -52,6 +55,8 @@ public class ManifestAttributes {
|
||||
|
||||
private final Map<String, MAttr> attrMap = new HashMap<>();
|
||||
|
||||
private final Map<String, MAttr> appAttrMap = new HashMap<>();
|
||||
|
||||
private static ManifestAttributes instance;
|
||||
|
||||
public static ManifestAttributes getInstance() {
|
||||
@@ -168,7 +173,10 @@ public class ManifestAttributes {
|
||||
public String decode(String attrName, long value) {
|
||||
MAttr attr = attrMap.get(attrName);
|
||||
if (attr == null) {
|
||||
return null;
|
||||
attr = appAttrMap.get(attrName);
|
||||
if (attr == null) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (attr.getType() == MAttrType.ENUM) {
|
||||
return attr.getValues().get(value);
|
||||
@@ -190,4 +198,32 @@ public class ManifestAttributes {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void updateAttributes(IResParser parser) {
|
||||
appAttrMap.clear();
|
||||
|
||||
ResourceStorage resStorage = parser.getResStorage();
|
||||
ValuesParser vp = new ValuesParser(parser.getStrings(), resStorage.getResourcesNames());
|
||||
|
||||
for (ResourceEntry ri : resStorage.getResources()) {
|
||||
if (ri.getTypeName().equals("attr") && ri.getNamedValues().size() > 1) {
|
||||
RawNamedValue first = ri.getNamedValues().get(0);
|
||||
MAttrType attrTyp;
|
||||
if (first.getRawValue().getData() == ValuesParser.ATTR_TYPE_FLAGS) {
|
||||
attrTyp = MAttrType.FLAG;
|
||||
} else if (first.getRawValue().getData() == ValuesParser.ATTR_TYPE_ENUM || first.getRawValue().getData() == 65600) {
|
||||
attrTyp = MAttrType.ENUM;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
MAttr attr = new MAttr(attrTyp);
|
||||
for (int i = 1; i < ri.getNamedValues().size(); i++) {
|
||||
RawNamedValue rv = ri.getNamedValues().get(i);
|
||||
String value = vp.decodeNameRef(rv.getNameRef());
|
||||
attr.getValues().put((long) rv.getRawValue().getData(), value.startsWith("id.") ? value.substring(3) : value);
|
||||
}
|
||||
appAttrMap.put(ri.getKeyName(), attr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,8 @@ public class ParserConstants {
|
||||
protected static final int RES_TABLE_TYPE_SPEC_TYPE = 0x0202; // 514
|
||||
protected static final int RES_TABLE_TYPE_LIBRARY = 0x0203; // 515
|
||||
protected static final int RES_TABLE_TYPE_OVERLAY = 0x0204; // 516
|
||||
protected static final int RES_TABLE_TYPE_STAGED_ALIAS = 0x0206; // 517
|
||||
protected static final int RES_TABLE_TYPE_OVERLAY_POLICY = 0x0205; // 517
|
||||
protected static final int RES_TABLE_TYPE_STAGED_ALIAS = 0x0206; // 518
|
||||
|
||||
/**
|
||||
* Type constants
|
||||
|
||||
@@ -127,6 +127,10 @@ public class ParserStream {
|
||||
|
||||
public void skipToPos(long expectedOffset, String error) throws IOException {
|
||||
long pos = getPos();
|
||||
if (pos > expectedOffset) {
|
||||
throw new IOException(error + ", expected offset not reachable: 0x" + Long.toHexString(expectedOffset)
|
||||
+ ", actual: 0x" + Long.toHexString(getPos()));
|
||||
}
|
||||
if (pos < expectedOffset) {
|
||||
skip(expectedOffset - pos);
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ public class ProtoXMLParser {
|
||||
}
|
||||
String name = a.getName();
|
||||
String value = deobfClassName(a.getValue());
|
||||
writer.add(name).add("=\"").add(value).add('\"');
|
||||
writer.add(name).add("=\"").add(StringUtils.escapeXML(value)).add('\"');
|
||||
memorizePackageName(name, value);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
@@ -168,11 +170,14 @@ public class ResTableParser extends CommonBinaryParser implements IResParser {
|
||||
parseLibraryTypeChunk(chunkStart);
|
||||
break;
|
||||
case RES_TABLE_TYPE_OVERLAY: // 0x0204
|
||||
parseOverlayTypeChunk(chunkStart);
|
||||
break;
|
||||
case RES_TABLE_TYPE_OVERLAY_POLICY: // 0x0205
|
||||
throw new IOException(
|
||||
String.format("Encountered unsupported chunk type TYPE_OVERLAY at offset 0x%x ", chunkStart));
|
||||
String.format("Encountered unsupported chunk type RES_TABLE_TYPE_OVERLAY_POLICY at offset 0x%x ", chunkStart));
|
||||
case RES_TABLE_TYPE_STAGED_ALIAS: // 0x0206
|
||||
throw new IOException(
|
||||
String.format("Encountered unsupported chunk type TYPE_STAGED_ALIAS at offset 0x%x ", chunkStart));
|
||||
parseStagedAliasChunk(chunkStart);
|
||||
break;
|
||||
default:
|
||||
LOG.warn("Unknown chunk type {} encountered at offset {}", type, chunkStart);
|
||||
}
|
||||
@@ -242,6 +247,13 @@ public class ResTableParser extends CommonBinaryParser implements IResParser {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an <code>ResTable_type</code> (except for the 2 bytes <code>uint16_t</code>
|
||||
* from <code>ResChunk_header</code>).
|
||||
*
|
||||
* @see <a href=
|
||||
* "https://github.com/aosp-mirror/platform_frameworks_base/blob/master/libs/androidfw/include/androidfw/ResourceTypes.h"></a>ResourceTypes.h</a>
|
||||
*/
|
||||
private void parseTypeChunk(long start, PackageChunk pkg) throws IOException {
|
||||
/* int headerSize = */
|
||||
is.readInt16();
|
||||
@@ -249,9 +261,13 @@ public class ResTableParser extends CommonBinaryParser implements IResParser {
|
||||
long chunkSize = is.readUInt32();
|
||||
long chunkEnd = start + chunkSize;
|
||||
|
||||
// The type identifier this chunk is holding. Type IDs start at 1 (corresponding
|
||||
// to the value of the type bits in a resource identifier). 0 is invalid.
|
||||
int id = is.readInt8();
|
||||
is.checkInt8(0, "type chunk, res0");
|
||||
is.checkInt16(0, "type chunk, res1");
|
||||
int flags = is.readInt8(); // 0 or 1
|
||||
boolean flagSparse = (flags == 1);
|
||||
|
||||
is.checkInt16(0, "type chunk, reserved");
|
||||
int entryCount = is.readInt32();
|
||||
long entriesStart = start + is.readInt32();
|
||||
|
||||
@@ -262,21 +278,30 @@ public class ResTableParser extends CommonBinaryParser implements IResParser {
|
||||
LOG.warn("Invalid config flags detected: {}{}", typeName, config.getQualifiers());
|
||||
}
|
||||
|
||||
int[] entryIndexes = new int[entryCount];
|
||||
for (int i = 0; i < entryCount; i++) {
|
||||
entryIndexes[i] = is.readInt32();
|
||||
Map<Integer, Integer> entryOffsetMap = new LinkedHashMap<>(entryCount);
|
||||
if (flagSparse) {
|
||||
for (int i = 0; i < entryCount; i++) {
|
||||
entryOffsetMap.put(is.readInt16(), is.readInt16());
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < entryCount; i++) {
|
||||
entryOffsetMap.put(i, is.readInt32());
|
||||
}
|
||||
}
|
||||
is.checkPos(entriesStart, "Expected entry start");
|
||||
for (int i = 0; i < entryCount; i++) {
|
||||
if (entryIndexes[i] != NO_ENTRY) {
|
||||
int processed = 0;
|
||||
for (int index : entryOffsetMap.keySet()) {
|
||||
int offset = entryOffsetMap.get(index);
|
||||
if (offset != NO_ENTRY) {
|
||||
if (is.getPos() >= chunkEnd) {
|
||||
// Certain resource obfuscated apps like com.facebook.orca have more entries defined
|
||||
// than actually fit into the chunk size -> ignore the remaining entries
|
||||
LOG.warn("End of chunk reached - ignoring remaining {} entries", entryCount - i);
|
||||
LOG.warn("End of chunk reached - ignoring remaining {} entries", entryCount - processed);
|
||||
break;
|
||||
}
|
||||
parseEntry(pkg, id, i, config.getQualifiers());
|
||||
parseEntry(pkg, id, index, config.getQualifiers());
|
||||
}
|
||||
processed++;
|
||||
}
|
||||
if (chunkEnd > is.getPos()) {
|
||||
// Skip remaining unknown data in this chunk (e.g. type 8 entries")
|
||||
@@ -287,6 +312,36 @@ public class ResTableParser extends CommonBinaryParser implements IResParser {
|
||||
}
|
||||
}
|
||||
|
||||
private void parseOverlayTypeChunk(long chunkStart) throws IOException {
|
||||
LOG.trace("parsing overlay type chunk starting at offset {}", chunkStart);
|
||||
// read ResTable_overlayable_header
|
||||
/* headerSize = */ is.readInt16(); // usually 1032 bytes
|
||||
int chunkSize = is.readInt32(); // e.g. 1056 bytes
|
||||
long expectedEndPos = chunkStart + chunkSize;
|
||||
String name = is.readString16Fixed(256); // 512 bytes
|
||||
String actor = is.readString16Fixed(256); // 512 bytes
|
||||
LOG.trace("Overlay header data: name={} actor={}", name, actor);
|
||||
// skip: ResTable_overlayable_policy_header + ResTable_ref * x
|
||||
is.skipToPos(expectedEndPos, "overlay chunk end");
|
||||
}
|
||||
|
||||
private void parseStagedAliasChunk(long chunkStart) throws IOException {
|
||||
// read ResTable_staged_alias_header
|
||||
LOG.trace("parsing staged alias chunk starting at offset {}", chunkStart);
|
||||
/* headerSize = */ is.readInt16();
|
||||
int chunkSize = is.readInt32();
|
||||
long expectedEndPos = chunkStart + chunkSize;
|
||||
int count = is.readInt32();
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
// read ResTable_staged_alias_entry
|
||||
int stagedResId = is.readInt32();
|
||||
int finalizedResId = is.readInt32();
|
||||
LOG.debug("Staged alias: stagedResId {} finalizedResId {}", stagedResId, finalizedResId);
|
||||
}
|
||||
is.skipToPos(expectedEndPos, "staged alias chunk end");
|
||||
}
|
||||
|
||||
private void parseEntry(PackageChunk pkg, int typeId, int entryId, String config) throws IOException {
|
||||
int size = is.readInt16();
|
||||
int flags = is.readInt16();
|
||||
|
||||
@@ -109,10 +109,10 @@ public class ResXmlGen {
|
||||
addSimpleValue(cw, ri.getTypeName(), ri.getTypeName(), "name", ri.getKeyName(), valueStr);
|
||||
} else {
|
||||
cw.startLine();
|
||||
cw.add('<').add(ri.getTypeName()).add(' ');
|
||||
cw.add('<').add(ri.getTypeName()).add(" name=\"");
|
||||
String itemTag = "item";
|
||||
if (ri.getTypeName().equals("attr") && !ri.getNamedValues().isEmpty()) {
|
||||
cw.add("name=\"").add(ri.getKeyName());
|
||||
cw.add(ri.getKeyName());
|
||||
int type = ri.getNamedValues().get(0).getRawValue().getData();
|
||||
if ((type & ValuesParser.ATTR_TYPE_ENUM) != 0) {
|
||||
itemTag = "enum";
|
||||
@@ -123,15 +123,17 @@ public class ResXmlGen {
|
||||
if (formatValue != null) {
|
||||
cw.add("\" format=\"").add(formatValue);
|
||||
}
|
||||
cw.add("\"");
|
||||
} else {
|
||||
cw.add("name=\"").add(ri.getKeyName()).add('\"');
|
||||
cw.add(ri.getKeyName());
|
||||
}
|
||||
if (ri.getParentRef() != 0) {
|
||||
String parent = vp.decodeValue(TYPE_REFERENCE, ri.getParentRef());
|
||||
cw.add(" parent=\"").add(parent).add('\"');
|
||||
if (ri.getTypeName().equals("style") || ri.getParentRef() != 0) {
|
||||
cw.add("\" parent=\"");
|
||||
if (ri.getParentRef() != 0) {
|
||||
String parent = vp.decodeValue(TYPE_REFERENCE, ri.getParentRef());
|
||||
cw.add(parent);
|
||||
}
|
||||
}
|
||||
cw.add(">");
|
||||
cw.add("\">");
|
||||
|
||||
cw.incIndent();
|
||||
for (RawNamedValue value : ri.getNamedValues()) {
|
||||
@@ -177,7 +179,18 @@ public class ResXmlGen {
|
||||
if (dataType == ParserConstants.TYPE_INT_DEC && nameStr != null) {
|
||||
try {
|
||||
int intVal = Integer.parseInt(valueStr);
|
||||
String newVal = ManifestAttributes.getInstance().decode(nameStr.replace("android:attr.", ""), intVal);
|
||||
String newVal = ManifestAttributes.getInstance().decode(nameStr.replace("android:", "").replace("attr.", ""), intVal);
|
||||
if (newVal != null) {
|
||||
valueStr = newVal;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if (dataType == ParserConstants.TYPE_INT_HEX && nameStr != null) {
|
||||
try {
|
||||
int intVal = Integer.decode(valueStr);
|
||||
String newVal = ManifestAttributes.getInstance().decode(nameStr.replace("android:", "").replace("attr.", ""), intVal);
|
||||
if (newVal != null) {
|
||||
valueStr = newVal;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,11 @@ public class ResourcesSaver implements Runnable {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
saveResources(resourceFile.loadContent());
|
||||
try {
|
||||
saveResources(resourceFile.loadContent());
|
||||
} catch (Throwable e) {
|
||||
LOG.warn("Failed to save resource: {}", resourceFile.getOriginalName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveResources(ResContainer rc) {
|
||||
|
||||
@@ -4,7 +4,7 @@ buildscript {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.0.0'
|
||||
classpath 'com.android.tools.build:gradle:4.2.2'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -67,4 +67,10 @@ public abstract class ExportGradleTest {
|
||||
assertThat(appBuildGradle.exists());
|
||||
return loadFileContent(appBuildGradle);
|
||||
}
|
||||
|
||||
protected String getSettingsGradle() {
|
||||
File settingsGradle = new File(exportDir, "settings.gradle");
|
||||
assertThat(settingsGradle.exists());
|
||||
return loadFileContent(settingsGradle);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.CommentsLevel;
|
||||
import jadx.api.DecompilationMode;
|
||||
import jadx.api.ICodeInfo;
|
||||
import jadx.api.ICodeWriter;
|
||||
import jadx.api.JadxArgs;
|
||||
@@ -109,6 +110,12 @@ public abstract class IntegrationTest extends TestUtils {
|
||||
private @Nullable TestCompiler sourceCompiler;
|
||||
private @Nullable TestCompiler decompiledCompiler;
|
||||
|
||||
/**
|
||||
* Run check method on decompiled code even if source check method not found.
|
||||
* Useful for smali test if check method added to smali code
|
||||
*/
|
||||
private boolean forceDecompiledCheck = false;
|
||||
|
||||
static {
|
||||
// enable debug checks
|
||||
DebugChecks.checksEnabled = true;
|
||||
@@ -217,6 +224,11 @@ public abstract class IntegrationTest extends TestUtils {
|
||||
return sortedClsNodes;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public ClassNode searchTestCls(List<ClassNode> list, String shortClsName) {
|
||||
return searchCls(list, getTestPkg() + '.' + shortClsName);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public ClassNode searchCls(List<ClassNode> list, String clsName) {
|
||||
for (ClassNode cls : list) {
|
||||
@@ -346,11 +358,10 @@ public abstract class IntegrationTest extends TestUtils {
|
||||
String clsName = cls.getClassInfo().getRawName().replace('/', '.');
|
||||
try {
|
||||
// run 'check' method from original class
|
||||
if (runSourceAutoCheck(clsName)) {
|
||||
return;
|
||||
}
|
||||
boolean sourceCheckFound = runSourceAutoCheck(clsName);
|
||||
|
||||
// run 'check' method from decompiled class
|
||||
if (compile) {
|
||||
if (compile && (sourceCheckFound || forceDecompiledCheck)) {
|
||||
runDecompiledAutoCheck(cls);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@@ -361,36 +372,36 @@ public abstract class IntegrationTest extends TestUtils {
|
||||
|
||||
private boolean runSourceAutoCheck(String clsName) {
|
||||
if (sourceCompiler == null) {
|
||||
// no source code (smali case)
|
||||
return true;
|
||||
System.out.println("Source check: no code");
|
||||
return false;
|
||||
}
|
||||
Class<?> origCls;
|
||||
try {
|
||||
origCls = sourceCompiler.getClass(clsName);
|
||||
} catch (ClassNotFoundException e) {
|
||||
rethrow("Missing class: " + clsName, e);
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
Method checkMth;
|
||||
try {
|
||||
checkMth = sourceCompiler.getMethod(origCls, CHECK_METHOD_NAME, new Class[] {});
|
||||
} catch (NoSuchMethodException e) {
|
||||
// ignore
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
if (!checkMth.getReturnType().equals(void.class)
|
||||
|| !Modifier.isPublic(checkMth.getModifiers())
|
||||
|| Modifier.isStatic(checkMth.getModifiers())) {
|
||||
fail("Wrong 'check' method");
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
limitExecTime(() -> checkMth.invoke(origCls.getConstructor().newInstance()));
|
||||
System.out.println("Source check: PASSED");
|
||||
return true;
|
||||
} catch (Throwable e) {
|
||||
throw new JadxRuntimeException("Source check failed", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void runDecompiledAutoCheck(ClassNode cls) {
|
||||
@@ -546,13 +557,17 @@ public abstract class IntegrationTest extends TestUtils {
|
||||
|
||||
protected void setFallback() {
|
||||
disableCompilation();
|
||||
this.args.setFallbackMode(true);
|
||||
this.args.setDecompilationMode(DecompilationMode.FALLBACK);
|
||||
}
|
||||
|
||||
protected void disableCompilation() {
|
||||
this.compile = false;
|
||||
}
|
||||
|
||||
protected void forceDecompiledCheck() {
|
||||
this.forceDecompiledCheck = true;
|
||||
}
|
||||
|
||||
protected void enableDeobfuscation() {
|
||||
args.setDeobfuscationOn(true);
|
||||
args.setDeobfuscationMapFileMode(DeobfuscationMapFileMode.IGNORE);
|
||||
|
||||
@@ -34,6 +34,13 @@ public class JadxClassNodeAssertions extends AbstractObjectAssert<JadxClassNodeA
|
||||
return new JadxCodeAssertions(codeStr);
|
||||
}
|
||||
|
||||
public JadxCodeAssertions disasmCode() {
|
||||
isNotNull();
|
||||
String disasmCode = actual.getDisassembledCode();
|
||||
assertThat(disasmCode).isNotNull().isNotBlank();
|
||||
return new JadxCodeAssertions(disasmCode);
|
||||
}
|
||||
|
||||
public JadxCodeAssertions reloadCode(IntegrationTest testInstance) {
|
||||
isNotNull();
|
||||
ICodeInfo code = actual.reloadCode();
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package jadx.tests.export;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import jadx.tests.api.ExportGradleTest;
|
||||
|
||||
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
|
||||
|
||||
class IllegalCharsForGradleWrapper extends ExportGradleTest {
|
||||
|
||||
@Test
|
||||
void test() {
|
||||
exportGradle("IllegalCharsForGradleWrapper.xml", "strings.xml");
|
||||
|
||||
assertThat(getSettingsGradle()).contains("'JadxTestApp'");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package jadx.tests.integration.inline;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.core.dex.visitors.ProcessAnonymous;
|
||||
import jadx.core.utils.ListUtils;
|
||||
import jadx.tests.api.SmaliTest;
|
||||
|
||||
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
|
||||
|
||||
public class TestInstanceLambda extends SmaliTest {
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes", "SameParameterValue" })
|
||||
public static class TestCls {
|
||||
|
||||
public <T> Map<T, T> test(List<? extends T> list) {
|
||||
return toMap(list, Lambda$1.INSTANCE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Smali test missing 'T' definition in 'Lambda<T>'
|
||||
* Note: use '$1' so class looks like generated by compiler and pass check in
|
||||
* {@link ProcessAnonymous#canBeAnonymous(ClassNode)}
|
||||
*/
|
||||
@SuppressWarnings({ "CheckStyle", "checkstyle:TypeName" })
|
||||
private static class Lambda$1<T> implements Function<T, T> {
|
||||
public static final Lambda$1 INSTANCE = new Lambda$1();
|
||||
|
||||
@Override
|
||||
public T apply(T t) {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> Map<T, T> toMap(List<? extends T> list, Function<T, T> valueMap) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
useJavaInput();
|
||||
noDebugInfo();
|
||||
assertThat(getClassNode(TestCls.class))
|
||||
.code();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSmaliDisableInline() {
|
||||
args.setInlineAnonymousClasses(false);
|
||||
List<ClassNode> classNodes = loadFromSmaliFiles();
|
||||
assertThat(searchTestCls(classNodes, "Lambda$1"))
|
||||
.code()
|
||||
.containsOne("class Lambda$1<T> implements Function<T, T> {");
|
||||
assertThat(searchTestCls(classNodes, "TestCls"))
|
||||
.code()
|
||||
.containsOne("Lambda$1.INSTANCE");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSmali() {
|
||||
List<ClassNode> classNodes = loadFromSmaliFiles();
|
||||
assertThat(ListUtils.filter(classNodes, c -> !c.contains(AFlag.DONT_GENERATE)))
|
||||
.describedAs("Expect lambda to be inlined")
|
||||
.hasSize(1);
|
||||
assertThat(searchTestCls(classNodes, "TestCls"))
|
||||
.code()
|
||||
.doesNotContain("Lambda$1.INSTANCE")
|
||||
.containsOne("toMap(list, new Function<T, T>() {");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package jadx.tests.integration.invoke;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.tests.api.SmaliTest;
|
||||
import jadx.tests.api.extensions.profiles.TestProfile;
|
||||
import jadx.tests.api.extensions.profiles.TestWithProfiles;
|
||||
|
||||
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
public class TestPolymorphicInvoke extends SmaliTest {
|
||||
|
||||
public static class TestCls {
|
||||
public String func(int a, int c) {
|
||||
return String.valueOf(a + c);
|
||||
}
|
||||
|
||||
public String test() {
|
||||
try {
|
||||
MethodType methodType = MethodType.methodType(String.class, Integer.TYPE, Integer.TYPE);
|
||||
MethodHandle methodHandle = MethodHandles.lookup().findVirtual(TestCls.class, "func", methodType);
|
||||
return (String) methodHandle.invoke(this, 1, 2);
|
||||
} catch (Throwable e) {
|
||||
fail(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void check() {
|
||||
assertThat(test()).isEqualTo("3");
|
||||
}
|
||||
}
|
||||
|
||||
@TestWithProfiles({ TestProfile.DX_J8, TestProfile.D8_J11 })
|
||||
public void test() {
|
||||
ClassNode cls = getClassNode(TestCls.class);
|
||||
assertThat(cls).code()
|
||||
.containsOne("return (String) methodHandle.invoke(this, 1, 2);");
|
||||
assertThat(cls).disasmCode()
|
||||
.containsOne("invoke-polymorphic");
|
||||
}
|
||||
|
||||
@TestWithProfiles({ TestProfile.JAVA8, TestProfile.JAVA11 })
|
||||
public void testJava() {
|
||||
assertThat(getClassNode(TestCls.class))
|
||||
.code()
|
||||
.containsOne("return (String) methodHandle.invoke(this, 1, 2);");
|
||||
// java uses 'invokevirtual'
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSmali() {
|
||||
assertThat(getClassNodeFromSmali())
|
||||
.code()
|
||||
.containsOne("String ret = (String) methodHandle.invoke(this, 10, 20);");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package jadx.tests.integration.invoke;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
|
||||
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;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
public class TestPolymorphicRangeInvoke extends IntegrationTest {
|
||||
|
||||
public static class TestCls {
|
||||
public String func2(int a, int b, int c, int d, int e, int f) {
|
||||
return String.valueOf(a + b + c + d + e + f);
|
||||
}
|
||||
|
||||
public String test() {
|
||||
try {
|
||||
MethodHandles.Lookup lookup = MethodHandles.lookup();
|
||||
MethodType methodType = MethodType.methodType(String.class, Integer.TYPE, Integer.TYPE, Integer.TYPE, Integer.TYPE,
|
||||
Integer.TYPE, Integer.TYPE);
|
||||
MethodHandle methodHandle = lookup.findVirtual(TestCls.class, "func2", methodType);
|
||||
return (String) methodHandle.invoke(this, 10, 20, 30, 40, 50, 60);
|
||||
} catch (Throwable e) {
|
||||
fail(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void check() {
|
||||
assertThat(test()).isEqualTo("210");
|
||||
}
|
||||
}
|
||||
|
||||
@TestWithProfiles({ TestProfile.DX_J8 })
|
||||
public void test() {
|
||||
ClassNode cls = getClassNode(TestCls.class);
|
||||
assertThat(cls).code()
|
||||
.containsOne("return (String) methodHandle.invoke(this, 10, 20, 30, 40, 50, 60);");
|
||||
assertThat(cls).disasmCode()
|
||||
.containsOne("invoke-polymorphic/range");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package jadx.tests.integration.invoke;
|
||||
|
||||
import java.lang.invoke.CallSite;
|
||||
import java.lang.invoke.ConstantCallSite;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import jadx.tests.api.SmaliTest;
|
||||
|
||||
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
public class TestRawCustomInvoke extends SmaliTest {
|
||||
|
||||
public static class TestCls {
|
||||
|
||||
public static String func(int a, double b) {
|
||||
return String.valueOf(a + b);
|
||||
}
|
||||
|
||||
private static CallSite staticBootstrap(MethodHandles.Lookup lookup, String name, MethodType type) {
|
||||
try {
|
||||
return new ConstantCallSite(lookup.findStatic(lookup.lookupClass(), name, type));
|
||||
} catch (NoSuchMethodException | IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public String test() {
|
||||
try {
|
||||
return (String) staticBootstrap(MethodHandles.lookup(), "func",
|
||||
MethodType.methodType(String.class, Integer.TYPE, Double.TYPE))
|
||||
.dynamicInvoker().invoke(1, 2.0d);
|
||||
} catch (Throwable e) {
|
||||
fail(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void check() {
|
||||
assertThat(test()).isEqualTo("3.0");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
noDebugInfo();
|
||||
// this code does not contain `invoke-custom` instruction
|
||||
// only check if equivalent polymorphic call is correct
|
||||
assertThat(getClassNode(TestCls.class))
|
||||
.code()
|
||||
.containsOne(
|
||||
"return (String) staticBootstrap(MethodHandles.lookup(), \"func\", MethodType.methodType(String.class, Integer.TYPE, Double.TYPE)).dynamicInvoker().invoke(1, 2.0d);");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSmali() {
|
||||
forceDecompiledCheck();
|
||||
assertThat(getClassNodeFromSmali())
|
||||
.code()
|
||||
.containsOne(
|
||||
"return (String) staticBootstrap(MethodHandles.lookup(), \"func\", MethodType.methodType(String.class, Integer.TYPE, Double.TYPE)).dynamicInvoker().invoke(1, 2.0d) /* invoke-custom */;");
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,5 @@ public class TestLambdaExtVar extends IntegrationTest {
|
||||
.code()
|
||||
.doesNotContain("lambda$")
|
||||
.containsOne("return s.equals(str);"); // TODO: simplify to expression
|
||||
|
||||
System.out.println(cls.getCode().getCodeMetadata());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package jadx.tests.integration.java8;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import jadx.tests.api.IntegrationTest;
|
||||
|
||||
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
|
||||
|
||||
public class TestLambdaInstance2 extends IntegrationTest {
|
||||
|
||||
public static class TestCls {
|
||||
private String field;
|
||||
|
||||
public Runnable test(String str, int i) {
|
||||
return () -> call(str, i);
|
||||
}
|
||||
|
||||
public void call(String str, int i) {
|
||||
field = str + '=' + i;
|
||||
}
|
||||
|
||||
public void check() throws Exception {
|
||||
field = "";
|
||||
test("num", 7).run();
|
||||
assertThat(field).isEqualTo("num=7");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
assertThat(getClassNode(TestCls.class))
|
||||
.code()
|
||||
.doesNotContain("lambda$")
|
||||
.containsOne("call(str, i)");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package jadx.tests.integration.java8;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
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 TestLambdaReturn extends IntegrationTest {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static class TestCls {
|
||||
interface Function0<R> {
|
||||
R apply();
|
||||
}
|
||||
|
||||
public static class T2 {
|
||||
public long l;
|
||||
|
||||
public T2(long l) {
|
||||
this.l = l;
|
||||
}
|
||||
|
||||
public void w() {
|
||||
}
|
||||
}
|
||||
|
||||
public Byte test(Byte b1) {
|
||||
Function0<Void> f1 = () -> {
|
||||
new T2(94L).w();
|
||||
return null;
|
||||
};
|
||||
f1.apply();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@TestWithProfiles(TestProfile.DX_J8)
|
||||
public void test() {
|
||||
assertThat(getClassNode(TestCls.class))
|
||||
.code()
|
||||
.containsLines(2,
|
||||
"Function0<Void> f1 = () -> {",
|
||||
indent() + "new T2(94L).w();",
|
||||
indent() + "return null;",
|
||||
"};");
|
||||
}
|
||||
|
||||
@TestWithProfiles(TestProfile.D8_J11_DESUGAR)
|
||||
public void testLambda() {
|
||||
getClassNode(TestCls.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoDebug() {
|
||||
noDebugInfo();
|
||||
getClassNode(TestCls.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
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 TestNullInline extends IntegrationTest {
|
||||
|
||||
@SuppressWarnings({ "RedundantCast", "DataFlowIssue", "unused" })
|
||||
public static class TestCls {
|
||||
public static Long test(Double d1) {
|
||||
T1<T2, Byte> t1 = (T1<T2, Byte>) null;
|
||||
return t1.t2.l;
|
||||
}
|
||||
|
||||
static class T2 {
|
||||
public long l;
|
||||
}
|
||||
|
||||
static class T1<H, P extends Byte> {
|
||||
public T2 t2;
|
||||
|
||||
public T1(T2 t2) {
|
||||
this.t2 = t2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
assertThat(getClassNode(TestCls.class))
|
||||
.code()
|
||||
.containsOne("Long.valueOf(t1.t2.l);");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoDebug() {
|
||||
noDebugInfo();
|
||||
getClassNode(TestCls.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package jadx.tests.integration.others;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import jadx.core.dex.attributes.AType;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.tests.api.SmaliTest;
|
||||
|
||||
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
|
||||
|
||||
@SuppressWarnings("CommentedOutCode")
|
||||
public class TestOverrideWithSameName extends SmaliTest {
|
||||
|
||||
//@formatter:off
|
||||
/*
|
||||
interface A {
|
||||
B a();
|
||||
C a();
|
||||
}
|
||||
|
||||
abstract class B implements A {
|
||||
@Override
|
||||
public C a() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public class C extends B {
|
||||
@Override
|
||||
public B a() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
*/
|
||||
//@formatter:on
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
List<ClassNode> clsNodes = loadFromSmaliFiles();
|
||||
assertThat(searchCls(clsNodes, "test.A"))
|
||||
.code()
|
||||
.containsOne("C mo0a();") // assume second method was renamed
|
||||
.doesNotContain("@Override");
|
||||
|
||||
ClassNode bCls = searchCls(clsNodes, "test.B");
|
||||
assertThat(bCls)
|
||||
.code()
|
||||
.containsOne("C mo0a() {")
|
||||
.containsOne("@Override");
|
||||
|
||||
assertThat(getMethod(bCls, "a").get(AType.METHOD_OVERRIDE).getOverrideList())
|
||||
.singleElement()
|
||||
.satisfies(mth -> assertThat(mth.getMethodInfo().getDeclClass().getShortName()).isEqualTo("A"));
|
||||
|
||||
ClassNode cCls = searchCls(clsNodes, "test.C");
|
||||
assertThat(cCls)
|
||||
.code()
|
||||
.containsOne("B a() {")
|
||||
.containsOne("@Override");
|
||||
|
||||
assertThat(getMethod(cCls, "a").get(AType.METHOD_OVERRIDE).getOverrideList())
|
||||
.singleElement()
|
||||
.satisfies(mth -> assertThat(mth.getMethodInfo().getDeclClass().getShortName()).isEqualTo("A"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" android:compileSdkVersion="33" android:compileSdkVersionCodename="13" package="jadx.test.app" platformBuildVersionCode="33" platformBuildVersionName="13">
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="32"/>
|
||||
<application android:label="JadxTestApp/\:?*|">
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,40 @@
|
||||
.class public Linline/Lambda$1;
|
||||
.super Ljava/lang/Object;
|
||||
|
||||
.implements Ljava/util/function/Function;
|
||||
|
||||
|
||||
.annotation system Ldalvik/annotation/Signature;
|
||||
value = {
|
||||
"Ljava/lang/Object;",
|
||||
"Ljava/util/function/Function",
|
||||
"<TT;TT;>;"
|
||||
}
|
||||
.end annotation
|
||||
|
||||
.field public static final INSTANCE:Linline/Lambda$1;
|
||||
|
||||
.method static constructor <clinit>()V
|
||||
.registers 1
|
||||
new-instance v0, Linline/Lambda$1;
|
||||
invoke-direct {v0}, Linline/Lambda$1;-><init>()V
|
||||
sput-object v0, Linline/Lambda$1;->INSTANCE:Linline/Lambda$1;
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method private constructor <init>()V
|
||||
.registers 1
|
||||
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public final apply(Ljava/lang/Object;)Ljava/lang/Object;
|
||||
.registers 2
|
||||
.annotation system Ldalvik/annotation/Signature;
|
||||
value = {
|
||||
"(TT;)TT;"
|
||||
}
|
||||
.end annotation
|
||||
|
||||
return-object p1
|
||||
.end method
|
||||
@@ -0,0 +1,42 @@
|
||||
.class public Linline/TestCls;
|
||||
.super Ljava/lang/Object;
|
||||
|
||||
.method public test(Ljava/util/List;)Ljava/util/Map;
|
||||
.registers 3
|
||||
.annotation system Ldalvik/annotation/Signature;
|
||||
value = {
|
||||
"<T:",
|
||||
"Ljava/lang/Object;",
|
||||
">(",
|
||||
"Ljava/util/List",
|
||||
"<+TT;>;)",
|
||||
"Ljava/util/Map",
|
||||
"<TT;TT;>;"
|
||||
}
|
||||
.end annotation
|
||||
|
||||
sget-object v0, Linline/Lambda$1;->INSTANCE:Linline/Lambda$1;
|
||||
invoke-static {p1, v0}, Linline/TestCls;->toMap(Ljava/util/List;Ljava/util/function/Function;)Ljava/util/Map;
|
||||
move-result-object v0
|
||||
return-object v0
|
||||
.end method
|
||||
|
||||
.method private static toMap(Ljava/util/List;Ljava/util/function/Function;)Ljava/util/Map;
|
||||
.registers 4
|
||||
.annotation system Ldalvik/annotation/Signature;
|
||||
value = {
|
||||
"<T:",
|
||||
"Ljava/lang/Object;",
|
||||
">(",
|
||||
"Ljava/util/List",
|
||||
"<+TT;>;",
|
||||
"Ljava/util/function/Function",
|
||||
"<TT;TT;>;)",
|
||||
"Ljava/util/Map",
|
||||
"<TT;TT;>;"
|
||||
}
|
||||
.end annotation
|
||||
|
||||
const/4 v0, 0x0
|
||||
return-object v0
|
||||
.end method
|
||||
@@ -0,0 +1,77 @@
|
||||
.class public Linvoke/TestPolymorphicInvoke;
|
||||
.super Ljava/lang/Object;
|
||||
|
||||
.method public func(II)Ljava/lang/String;
|
||||
.registers 4
|
||||
.param p1, "a" # I
|
||||
.param p2, "c" # I
|
||||
|
||||
.line 23
|
||||
add-int v0, p1, p2
|
||||
invoke-static {v0}, Ljava/lang/String;->valueOf(I)Ljava/lang/String;
|
||||
move-result-object v0
|
||||
return-object v0
|
||||
.end method
|
||||
|
||||
.method public test()V
|
||||
.registers 7
|
||||
|
||||
.line 32
|
||||
:try_start_0
|
||||
invoke-static {}, Ljava/lang/invoke/MethodHandles;->lookup()Ljava/lang/invoke/MethodHandles$Lookup;
|
||||
move-result-object v0
|
||||
|
||||
.line 33
|
||||
.local v0, "lookup":Ljava/lang/invoke/MethodHandles$Lookup;
|
||||
const-class v1, Ljava/lang/String;
|
||||
sget-object v2, Ljava/lang/Integer;->TYPE:Ljava/lang/Class;
|
||||
const/4 v3, 0x1
|
||||
new-array v3, v3, [Ljava/lang/Class;
|
||||
const/4 v4, 0x0
|
||||
sget-object v5, Ljava/lang/Integer;->TYPE:Ljava/lang/Class;
|
||||
aput-object v5, v3, v4
|
||||
invoke-static {v1, v2, v3}, Ljava/lang/invoke/MethodType;->methodType(Ljava/lang/Class;Ljava/lang/Class;[Ljava/lang/Class;)Ljava/lang/invoke/MethodType;
|
||||
move-result-object v1
|
||||
|
||||
.line 34
|
||||
.local v1, "methodType":Ljava/lang/invoke/MethodType;
|
||||
const-class v2, Linvoke/TestPolymorphicInvoke;
|
||||
const-string v3, "func"
|
||||
invoke-virtual {v0, v2, v3, v1}, Ljava/lang/invoke/MethodHandles$Lookup;->findVirtual(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle;
|
||||
move-result-object v2
|
||||
|
||||
.line 35
|
||||
.local v2, "methodHandle":Ljava/lang/invoke/MethodHandle;
|
||||
const/16 v3, 0xa
|
||||
const/16 v4, 0x14
|
||||
|
||||
invoke-polymorphic {v2, p0, v3, v4}, Ljava/lang/invoke/MethodHandle;->invoke([Ljava/lang/Object;)Ljava/lang/Object;, (Linvoke/TestPolymorphicInvoke;II)Ljava/lang/String;
|
||||
move-result-object v3
|
||||
|
||||
.line 36
|
||||
.local v3, "ret":Ljava/lang/String;
|
||||
sget-object v4, Ljava/lang/System;->out:Ljava/io/PrintStream;
|
||||
invoke-virtual {v4, v3}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
|
||||
:try_end_2a
|
||||
.catchall {:try_start_0 .. :try_end_2a} :catchall_2b
|
||||
|
||||
.line 39
|
||||
.end local v0 # "lookup":Ljava/lang/invoke/MethodHandles$Lookup;
|
||||
.end local v1 # "methodType":Ljava/lang/invoke/MethodType;
|
||||
.end local v2 # "methodHandle":Ljava/lang/invoke/MethodHandle;
|
||||
.end local v3 # "ret":Ljava/lang/String;
|
||||
goto :goto_2f
|
||||
|
||||
.line 37
|
||||
:catchall_2b
|
||||
move-exception v0
|
||||
|
||||
.line 38
|
||||
.local v0, "e":Ljava/lang/Throwable;
|
||||
invoke-virtual {v0}, Ljava/lang/Throwable;->printStackTrace()V
|
||||
|
||||
.line 40
|
||||
.end local v0 # "e":Ljava/lang/Throwable;
|
||||
:goto_2f
|
||||
return-void
|
||||
.end method
|
||||
@@ -0,0 +1,62 @@
|
||||
.class public Linvoke/TestRawCustomInvoke;
|
||||
.super Ljava/lang/Object;
|
||||
|
||||
.method public static func(ID)Ljava/lang/String;
|
||||
.registers 5
|
||||
int-to-double v0, p0
|
||||
add-double/2addr v0, p1
|
||||
invoke-static {v0, v1}, Ljava/lang/String;->valueOf(D)Ljava/lang/String;
|
||||
move-result-object p0
|
||||
return-object p0
|
||||
.end method
|
||||
|
||||
.method private static staticBootstrap(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
|
||||
.registers 5
|
||||
:try_start_0
|
||||
new-instance v0, Ljava/lang/invoke/ConstantCallSite;
|
||||
invoke-virtual {p0}, Ljava/lang/invoke/MethodHandles$Lookup;->lookupClass()Ljava/lang/Class;
|
||||
move-result-object v1
|
||||
invoke-virtual {p0, v1, p1, p2}, Ljava/lang/invoke/MethodHandles$Lookup;->findStatic(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle;
|
||||
move-result-object p0
|
||||
invoke-direct {v0, p0}, Ljava/lang/invoke/ConstantCallSite;-><init>(Ljava/lang/invoke/MethodHandle;)V
|
||||
:try_end_d
|
||||
.catch Ljava/lang/NoSuchMethodException; {:try_start_0 .. :try_end_d} :catch_e
|
||||
.catch Ljava/lang/IllegalAccessException; {:try_start_0 .. :try_end_d} :catch_e
|
||||
return-object v0
|
||||
:catch_e
|
||||
move-exception p0
|
||||
new-instance p1, Ljava/lang/RuntimeException;
|
||||
invoke-direct {p1, p0}, Ljava/lang/RuntimeException;-><init>(Ljava/lang/Throwable;)V
|
||||
throw p1
|
||||
.end method
|
||||
|
||||
.method public test()Ljava/lang/String;
|
||||
.registers 3
|
||||
:try_start_0
|
||||
|
||||
const/4 v0, 0x1
|
||||
const-wide/high16 v1, 0x4000000000000000L # 2.0
|
||||
invoke-custom {v0, v1}, call_site_0("func", (ID)Ljava/lang/String;)@Linvoke/TestRawCustomInvoke;->staticBootstrap(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
|
||||
|
||||
move-result-object v0
|
||||
|
||||
:try_end_25
|
||||
.catchall {:try_start_0 .. :try_end_25} :catchall_26
|
||||
return-object v0
|
||||
:catchall_26
|
||||
move-exception v0
|
||||
invoke-static {v0}, Lorg/junit/jupiter/api/Assertions;->fail(Ljava/lang/Throwable;)Ljava/lang/Object;
|
||||
const/4 v0, 0x0
|
||||
return-object v0
|
||||
.end method
|
||||
|
||||
.method public check()V
|
||||
.registers 3
|
||||
invoke-virtual {p0}, Linvoke/TestRawCustomInvoke;->test()Ljava/lang/String;
|
||||
move-result-object v0
|
||||
invoke-static {v0}, Ljadx/tests/api/utils/assertj/JadxAssertions;->assertThat(Ljava/lang/String;)Ljadx/tests/api/utils/assertj/JadxCodeAssertions;
|
||||
move-result-object v0
|
||||
const-string v1, "3.0"
|
||||
invoke-virtual {v0, v1}, Ljadx/tests/api/utils/assertj/JadxCodeAssertions;->isEqualTo(Ljava/lang/String;)Lorg/assertj/core/api/AbstractStringAssert;
|
||||
return-void
|
||||
.end method
|
||||
@@ -0,0 +1,8 @@
|
||||
.class interface abstract Ltest/A;
|
||||
.super Ljava/lang/Object;
|
||||
|
||||
.method public abstract a()Ltest/B;
|
||||
.end method
|
||||
|
||||
.method public abstract a()Ltest/C;
|
||||
.end method
|
||||
@@ -0,0 +1,10 @@
|
||||
.class abstract Ltest/B;
|
||||
.super Ljava/lang/Object;
|
||||
|
||||
.implements Ltest/A;
|
||||
|
||||
.method public a()Ltest/C;
|
||||
.registers 2
|
||||
const/4 v0, 0x0
|
||||
return-object v0
|
||||
.end method
|
||||
@@ -0,0 +1,8 @@
|
||||
.class public Ltest/C;
|
||||
.super Ltest/B;
|
||||
|
||||
.method public a()Ltest/B;
|
||||
.registers 2
|
||||
const/4 v0, 0x0
|
||||
return-object v0
|
||||
.end method
|
||||
@@ -1,32 +1,32 @@
|
||||
plugins {
|
||||
id 'application'
|
||||
id 'edu.sc.seis.launch4j' version '2.5.3'
|
||||
id 'edu.sc.seis.launch4j' version '2.5.4'
|
||||
id 'com.github.johnrengelman.shadow' version '7.1.2'
|
||||
id 'org.beryx.runtime' version '1.12.7'
|
||||
id 'org.beryx.runtime' version '1.13.0'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(':jadx-core'))
|
||||
implementation(project(":jadx-cli"))
|
||||
implementation 'com.beust:jcommander:1.82'
|
||||
implementation 'ch.qos.logback:logback-classic:1.3.4'
|
||||
implementation 'ch.qos.logback:logback-classic:1.3.5'
|
||||
|
||||
implementation 'com.fifesoft:rsyntaxtextarea:3.3.0'
|
||||
implementation 'com.fifesoft:rsyntaxtextarea:3.3.2'
|
||||
implementation files('libs/jfontchooser-1.0.5.jar')
|
||||
implementation 'hu.kazocsaba:image-viewer:1.2.3'
|
||||
|
||||
implementation 'com.formdev:flatlaf:2.6'
|
||||
implementation 'com.formdev:flatlaf-intellij-themes:2.6'
|
||||
implementation 'com.formdev:flatlaf-extras:2.6'
|
||||
implementation 'com.formdev:flatlaf:3.0'
|
||||
implementation 'com.formdev:flatlaf-intellij-themes:3.0'
|
||||
implementation 'com.formdev:flatlaf-extras:3.0'
|
||||
implementation 'com.formdev:svgSalamander:1.1.4'
|
||||
|
||||
implementation 'com.google.code.gson:gson:2.9.1'
|
||||
implementation 'com.google.code.gson:gson:2.10.1'
|
||||
implementation 'org.apache.commons:commons-lang3:3.12.0'
|
||||
implementation 'org.apache.commons:commons-text:1.10.0'
|
||||
|
||||
implementation 'io.reactivex.rxjava2:rxjava:2.2.21'
|
||||
implementation "com.github.akarnokd:rxjava2-swing:0.3.7"
|
||||
implementation 'com.android.tools.build:apksig:7.3.1'
|
||||
implementation 'com.android.tools.build:apksig:7.4.1'
|
||||
implementation 'io.github.skylot:jdwp:2.0.0'
|
||||
|
||||
// TODO: Switch back to upstream once this PR gets merged:
|
||||
|
||||
@@ -14,12 +14,10 @@ import java.util.concurrent.Executors;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.tree.DefaultMutableTreeNode;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.reactivex.annotations.NonNull;
|
||||
import io.reactivex.annotations.Nullable;
|
||||
|
||||
import jadx.core.dex.info.FieldInfo;
|
||||
import jadx.core.dex.instructions.args.ArgType;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
@@ -42,6 +40,7 @@ import jadx.gui.ui.panel.JDebuggerPanel;
|
||||
import jadx.gui.ui.panel.JDebuggerPanel.IListElement;
|
||||
import jadx.gui.ui.panel.JDebuggerPanel.ValueTreeNode;
|
||||
import jadx.gui.utils.NLS;
|
||||
import jadx.gui.utils.UiUtils;
|
||||
|
||||
public final class DebugController implements SmaliDebugger.SuspendListener, IDebugController {
|
||||
|
||||
@@ -78,7 +77,7 @@ public final class DebugController implements SmaliDebugger.SuspendListener, IDe
|
||||
initTypeMap();
|
||||
}
|
||||
this.debuggerPanel = debuggerPanel;
|
||||
debuggerPanel.resetUI();
|
||||
UiUtils.uiRunAndWait(debuggerPanel::resetUI);
|
||||
try {
|
||||
debugger = SmaliDebugger.attach(adbHost, adbPort, this);
|
||||
} catch (SmaliDebuggerException e) {
|
||||
@@ -251,7 +250,6 @@ public final class DebugController implements SmaliDebugger.SuspendListener, IDe
|
||||
throw new JadxRuntimeException("Unexpected type: " + type);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected static RuntimeType castType(String type) {
|
||||
RuntimeType rt = null;
|
||||
if (!StringUtils.isEmpty(type)) {
|
||||
@@ -663,22 +661,22 @@ public final class DebugController implements SmaliDebugger.SuspendListener, IDe
|
||||
}
|
||||
|
||||
private void updateAllRegisters(FrameNode frame) {
|
||||
if (buildRegTreeNodes(frame).size() > 0) {
|
||||
fetchAllRegisters(frame);
|
||||
}
|
||||
UiUtils.uiRun(() -> {
|
||||
if (!buildRegTreeNodes(frame).isEmpty()) {
|
||||
fetchAllRegisters(frame);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void fetchAllRegisters(FrameNode frame) {
|
||||
List<SmaliRegister> regs = cur.regAdapter.getInitializedList(frame.getCodeOffset());
|
||||
for (SmaliRegister reg : regs) {
|
||||
lazyQueue.execute(() -> {
|
||||
Entry<String, String> info = cur.regAdapter.getInfo(reg.getRuntimeRegNum(), frame.getCodeOffset());
|
||||
RegTreeNode regNode = frame.getRegNodes().get(reg.getRegNum());
|
||||
if (info != null) {
|
||||
applyDbgInfo(regNode, info);
|
||||
}
|
||||
updateRegister(regNode, null, true);
|
||||
});
|
||||
Entry<String, String> info = cur.regAdapter.getInfo(reg.getRuntimeRegNum(), frame.getCodeOffset());
|
||||
RegTreeNode regNode = frame.getRegNodes().get(reg.getRegNum());
|
||||
if (info != null) {
|
||||
applyDbgInfo(regNode, info);
|
||||
}
|
||||
updateRegister(regNode, null, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1286,7 +1286,10 @@ public class SmaliDebugger {
|
||||
@Override
|
||||
public String getType() {
|
||||
String gen = getSignature();
|
||||
return gen.isEmpty() ? this.slot.signature : gen;
|
||||
if (gen == null || gen.isEmpty()) {
|
||||
return this.slot.signature;
|
||||
}
|
||||
return gen;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@@ -1304,6 +1307,11 @@ public class SmaliDebugger {
|
||||
public int getEndOffset() {
|
||||
return (int) (slot.codeIndex + slot.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMarkedAsParameter() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static class RuntimeDebugInfo {
|
||||
|
||||
@@ -13,6 +13,8 @@ import java.util.Map.Entry;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.ICodeInfo;
|
||||
import jadx.api.plugins.input.data.AccessFlags;
|
||||
@@ -46,6 +48,7 @@ import jadx.core.dex.instructions.args.RegisterArg;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.core.dex.nodes.InsnNode;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
import jadx.core.utils.Utils;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
|
||||
import static jadx.api.plugins.input.data.AccessFlagsScope.FIELD;
|
||||
@@ -67,6 +70,7 @@ import static jadx.api.plugins.input.insns.Opcode.SPARSE_SWITCH;
|
||||
import static jadx.api.plugins.input.insns.Opcode.SPARSE_SWITCH_PAYLOAD;
|
||||
|
||||
public class Smali {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Smali.class);
|
||||
|
||||
private static SmaliInsnDecoder insnDecoder = null;
|
||||
|
||||
@@ -219,7 +223,15 @@ public class Smali {
|
||||
writeFields(smali, clsData, fields, colWidths);
|
||||
fields.clear();
|
||||
}
|
||||
writeMethod(smali, cls.getMethods().get(mthIndex[0]++), m, line);
|
||||
try {
|
||||
writeMethod(smali, cls.getMethods().get(mthIndex[0]++), m, line);
|
||||
} catch (Throwable e) {
|
||||
IMethodRef methodRef = m.getMethodRef();
|
||||
String mthFullName = methodRef.getParentClassType() + "->" + methodRef.getName();
|
||||
smali.setIndent(0);
|
||||
smali.startLine("Failed to write method: " + mthFullName + "\n" + Utils.getStackTrace(e));
|
||||
LOG.error("Failed to write smali code for method: {}", mthFullName, e);
|
||||
}
|
||||
line.reset();
|
||||
});
|
||||
|
||||
@@ -273,24 +285,25 @@ public class Smali {
|
||||
writeMethodDef(smali, mth, line);
|
||||
ICodeReader codeReader = mth.getCodeReader();
|
||||
if (codeReader != null) {
|
||||
int regsCount = codeReader.getRegistersCount();
|
||||
line.smaliMthNode.setParamRegStart(getParamStartRegNum(mth));
|
||||
line.smaliMthNode.setRegCount(codeReader.getRegistersCount());
|
||||
line.smaliMthNode.setRegCount(regsCount);
|
||||
Map<Long, InsnNode> nodes = new HashMap<>(codeReader.getUnitsCount() / 2);
|
||||
line.smaliMthNode.setInsnNodes(nodes, codeReader.getUnitsCount());
|
||||
line.smaliMthNode.initRegInfoList(codeReader.getRegistersCount(), codeReader.getUnitsCount());
|
||||
line.smaliMthNode.initRegInfoList(regsCount, codeReader.getUnitsCount());
|
||||
|
||||
smali.incIndent();
|
||||
smali.startLine(".registers ")
|
||||
.add("" + codeReader.getRegistersCount())
|
||||
.startLine();
|
||||
smali.startLine(".registers ").add(Integer.toString(regsCount));
|
||||
|
||||
writeTries(codeReader, line);
|
||||
if (formatMthParamInfo(mth, smali, codeReader, line)) {
|
||||
smali.startLine();
|
||||
IDebugInfo debugInfo = codeReader.getDebugInfo();
|
||||
List<ILocalVar> localVars = debugInfo != null ? debugInfo.getLocalVars() : Collections.emptyList();
|
||||
formatMthParamInfo(mth, smali, line, regsCount, localVars);
|
||||
if (debugInfo != null) {
|
||||
formatDbgInfo(debugInfo, localVars, line);
|
||||
}
|
||||
smali.newLine();
|
||||
smali.startLine();
|
||||
if (codeReader.getDebugInfo() != null) {
|
||||
formatDbgInfo(codeReader.getDebugInfo(), line);
|
||||
}
|
||||
// first pass to fill payload offsets for switch instructions
|
||||
codeReader.visitInstructions(insn -> {
|
||||
Opcode opcode = insn.getOpcode();
|
||||
@@ -446,53 +459,45 @@ public class Smali {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean formatMthParamInfo(IMethodData mth, SmaliWriter smali, ICodeReader codeReader, LineInfo line) {
|
||||
private void formatMthParamInfo(IMethodData mth, SmaliWriter smali, LineInfo line,
|
||||
int regsCount, List<ILocalVar> localVars) {
|
||||
List<String> types = mth.getMethodRef().getArgTypes();
|
||||
if (types.isEmpty()) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
int paramCount = 0;
|
||||
int paramStart = 0;
|
||||
int regNum = line.smaliMthNode.getParamRegStart();
|
||||
if (!hasStaticFlag(mth.getAccessFlags())) {
|
||||
// add 'this' register
|
||||
line.addRegName(regNum, "p0");
|
||||
line.smaliMthNode.setParamReg(regNum, "p0");
|
||||
regNum += 1;
|
||||
paramStart = 1;
|
||||
regNum++;
|
||||
paramStart++;
|
||||
}
|
||||
IDebugInfo dbgInfo = codeReader.getDebugInfo();
|
||||
if (dbgInfo != null) {
|
||||
for (ILocalVar var : dbgInfo.getLocalVars()) {
|
||||
if (var.getStartOffset() == -1) {
|
||||
int i = writeParamInfo(smali, line, regNum, paramStart, var.getName(), var.getType());
|
||||
regNum += i;
|
||||
paramStart += i;
|
||||
paramCount++;
|
||||
}
|
||||
if (localVars.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
ILocalVar[] params = new ILocalVar[regsCount];
|
||||
for (ILocalVar var : localVars) {
|
||||
if (var.isMarkedAsParameter()) {
|
||||
params[var.getRegNum()] = var;
|
||||
}
|
||||
}
|
||||
for (; paramCount < types.size(); paramCount++) {
|
||||
int i = writeParamInfo(smali, line, regNum, paramStart, "", types.get(paramCount));
|
||||
regNum += i;
|
||||
paramStart += i;
|
||||
smali.newLine();
|
||||
for (String paramType : types) {
|
||||
ILocalVar param = params[regNum];
|
||||
if (param != null) {
|
||||
String name = Utils.getOrElse(param.getName(), "");
|
||||
String type = Utils.getOrElse(param.getSignature(), paramType);
|
||||
String varName = "p" + paramStart;
|
||||
smali.startLine(String.format(".param %s, \"%s\" # %s", varName, name, type));
|
||||
line.addRegName(regNum, varName);
|
||||
line.smaliMthNode.setParamReg(regNum, varName);
|
||||
}
|
||||
int regSize = isWideType(paramType) ? 2 : 1;
|
||||
regNum += regSize;
|
||||
paramStart += regSize;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int writeParamInfo(SmaliWriter smali, LineInfo line,
|
||||
int regNum, int paramNum, String dbgInfoName, String type) {
|
||||
smali.startLine(String.format(".param p%d, \"%s\":%s", paramNum, dbgInfoName, type));
|
||||
String pName = "p" + paramNum;
|
||||
line.addRegName(regNum, pName);
|
||||
line.smaliMthNode.setParamReg(regNum, pName);
|
||||
if (isWideType(type)) {
|
||||
regNum++;
|
||||
dbgInfoName = "p" + (paramNum + 1);
|
||||
line.addRegName(regNum, dbgInfoName);
|
||||
line.smaliMthNode.setParamReg(regNum, dbgInfoName);
|
||||
return 2;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int getParamStartRegNum(IMethodData mth) {
|
||||
@@ -549,32 +554,44 @@ public class Smali {
|
||||
smali.startLine(".end annotation");
|
||||
}
|
||||
|
||||
private void formatDbgInfo(IDebugInfo dbgInfo, LineInfo line) {
|
||||
private void formatDbgInfo(IDebugInfo dbgInfo, List<ILocalVar> localVars, LineInfo line) {
|
||||
dbgInfo.getSourceLineMapping().forEach((codeOffset, srcLine) -> {
|
||||
if (codeOffset > -1) {
|
||||
line.addDebugLineTip(codeOffset, String.format(".line %d", srcLine), "");
|
||||
}
|
||||
});
|
||||
for (ILocalVar localVar : dbgInfo.getLocalVars()) {
|
||||
String type = localVar.getSignature();
|
||||
if (type == null || type.trim().isEmpty()) {
|
||||
type = localVar.getType();
|
||||
for (ILocalVar localVar : localVars) {
|
||||
if (localVar.isMarkedAsParameter()) {
|
||||
continue;
|
||||
}
|
||||
if (localVar.getStartOffset() > -1) {
|
||||
line.addTip(
|
||||
localVar.getStartOffset(),
|
||||
String.format(".local v%d", localVar.getRegNum()),
|
||||
String.format(", \"%s\":%s", localVar.getName(), type));
|
||||
}
|
||||
if (localVar.getEndOffset() > -1) {
|
||||
line.addTip(
|
||||
localVar.getEndOffset(),
|
||||
String.format(".end local v%d", localVar.getRegNum()),
|
||||
String.format(" # \"%s\":%s", localVar.getName(), type));
|
||||
String type = localVar.getType();
|
||||
String sign = localVar.getSignature();
|
||||
String longTypeStr;
|
||||
if (sign == null || sign.trim().isEmpty()) {
|
||||
longTypeStr = String.format(", \"%s\":%s", localVar.getName(), type);
|
||||
} else {
|
||||
longTypeStr = String.format(", \"%s\":%s, \"%s\"", localVar.getName(), type, localVar.getSignature());
|
||||
}
|
||||
line.addTip(
|
||||
localVar.getStartOffset(),
|
||||
".local " + formatVarName(line.smaliMthNode, localVar),
|
||||
longTypeStr);
|
||||
line.addTip(
|
||||
localVar.getEndOffset(),
|
||||
".end local " + formatVarName(line.smaliMthNode, localVar),
|
||||
String.format(" # \"%s\":%s", localVar.getName(), type));
|
||||
}
|
||||
}
|
||||
|
||||
private String formatVarName(SmaliMethodNode smaliMthNode, ILocalVar localVar) {
|
||||
int paramRegStart = smaliMthNode.getParamRegStart();
|
||||
int regNum = localVar.getRegNum();
|
||||
if (regNum < paramRegStart) {
|
||||
return "v" + regNum;
|
||||
}
|
||||
return "p" + (regNum - paramRegStart);
|
||||
}
|
||||
|
||||
private void writeEncodedValue(SmaliWriter smali, EncodedValue value, boolean wrapArray) {
|
||||
switch (value.getType()) {
|
||||
case ENCODED_ARRAY:
|
||||
|
||||
@@ -94,7 +94,6 @@ class SmaliMethodNode {
|
||||
protected void setParamReg(int regNum, String name) {
|
||||
SmaliRegister r = regList.get(regNum);
|
||||
r.setParam(name);
|
||||
r.setStartOffset(-1);
|
||||
}
|
||||
|
||||
protected void setParamRegStart(int paramRegStart) {
|
||||
|
||||
@@ -33,10 +33,6 @@ public class SmaliRegister extends RegisterInfo {
|
||||
}
|
||||
|
||||
protected void setStartOffset(int off) {
|
||||
if (startOffset == -1 && !isParam) {
|
||||
startOffset = off;
|
||||
return;
|
||||
}
|
||||
if (off < startOffset) {
|
||||
startOffset = off;
|
||||
}
|
||||
@@ -71,4 +67,9 @@ public class SmaliRegister extends RegisterInfo {
|
||||
public int getEndOffset() {
|
||||
return endOffset;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMarkedAsParameter() {
|
||||
return isParam;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import javax.swing.SwingWorker;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.gui.settings.JadxSettings;
|
||||
import jadx.gui.ui.MainWindow;
|
||||
import jadx.gui.ui.panel.ProgressPanel;
|
||||
@@ -60,14 +59,6 @@ public class BackgroundExecutor {
|
||||
return taskWorker;
|
||||
}
|
||||
|
||||
public TaskStatus executeAndWait(IBackgroundTask task) {
|
||||
try {
|
||||
return execute(task).get();
|
||||
} catch (Exception e) {
|
||||
throw new JadxRuntimeException("Task execution error", e);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void cancelAll() {
|
||||
try {
|
||||
taskRunning.values().forEach(Cancelable::cancel);
|
||||
|
||||
@@ -5,12 +5,15 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import javax.swing.JOptionPane;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.ICodeCache;
|
||||
import jadx.api.JavaClass;
|
||||
import jadx.gui.JadxWrapper;
|
||||
import jadx.gui.ui.MainWindow;
|
||||
import jadx.gui.utils.NLS;
|
||||
import jadx.gui.utils.UiUtils;
|
||||
|
||||
@@ -23,14 +26,16 @@ public class DecompileTask extends CancelableBackgroundTask {
|
||||
return classCount * CLS_LIMIT + 5000;
|
||||
}
|
||||
|
||||
private final MainWindow mainWindow;
|
||||
private final JadxWrapper wrapper;
|
||||
private final AtomicInteger complete = new AtomicInteger(0);
|
||||
private int expectedCompleteCount;
|
||||
|
||||
private ProcessResult result;
|
||||
|
||||
public DecompileTask(JadxWrapper wrapper) {
|
||||
this.wrapper = wrapper;
|
||||
public DecompileTask(MainWindow mainWindow) {
|
||||
this.mainWindow = mainWindow;
|
||||
this.wrapper = mainWindow.getWrapper();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -40,6 +45,10 @@ public class DecompileTask extends CancelableBackgroundTask {
|
||||
|
||||
@Override
|
||||
public List<Runnable> scheduleJobs() {
|
||||
if (mainWindow.getCacheObject().isFullDecompilationFinished()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<JavaClass> classes = wrapper.getIncludedClasses();
|
||||
expectedCompleteCount = classes.size();
|
||||
complete.set(0);
|
||||
@@ -87,7 +96,41 @@ public class DecompileTask extends CancelableBackgroundTask {
|
||||
+ ", time limit:{ total: " + timeLimit + "ms, per cls: " + CLS_LIMIT + "ms }"
|
||||
+ ", status: " + taskInfo.getStatus());
|
||||
}
|
||||
this.result = new ProcessResult(skippedCls, taskInfo.getStatus(), timeLimit);
|
||||
result = new ProcessResult(skippedCls, taskInfo.getStatus(), timeLimit);
|
||||
|
||||
wrapper.unloadClasses();
|
||||
processDecompilationResults();
|
||||
System.gc();
|
||||
|
||||
mainWindow.getCacheObject().setFullDecompilationFinished(skippedCls == 0);
|
||||
}
|
||||
|
||||
private void processDecompilationResults() {
|
||||
int skippedCls = result.getSkipped();
|
||||
if (skippedCls == 0) {
|
||||
return;
|
||||
}
|
||||
TaskStatus status = result.getStatus();
|
||||
LOG.warn("Decompile and indexing of some classes skipped: {}, status: {}", skippedCls, status);
|
||||
switch (status) {
|
||||
case CANCEL_BY_USER: {
|
||||
String reason = NLS.str("message.userCancelTask");
|
||||
String message = NLS.str("message.indexIncomplete", reason, skippedCls);
|
||||
JOptionPane.showMessageDialog(mainWindow, message);
|
||||
break;
|
||||
}
|
||||
case CANCEL_BY_TIMEOUT: {
|
||||
String reason = NLS.str("message.taskTimeout", result.getTimeLimit());
|
||||
String message = NLS.str("message.indexIncomplete", reason, skippedCls);
|
||||
JOptionPane.showMessageDialog(mainWindow, message);
|
||||
break;
|
||||
}
|
||||
case CANCEL_BY_MEMORY: {
|
||||
mainWindow.showHeapUsageBar();
|
||||
JOptionPane.showMessageDialog(mainWindow, NLS.str("message.indexingClassesSkipped", skippedCls));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -5,6 +5,7 @@ import java.util.regex.Pattern;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.gui.treemodel.JClass;
|
||||
import jadx.gui.treemodel.JResource;
|
||||
|
||||
public class SearchSettings {
|
||||
|
||||
@@ -13,6 +14,7 @@ public class SearchSettings {
|
||||
private final boolean ignoreCase;
|
||||
|
||||
private JClass activeCls;
|
||||
private JResource activeResource;
|
||||
private Pattern regexPattern;
|
||||
private ISearchMethod searchMethod;
|
||||
|
||||
@@ -64,6 +66,14 @@ public class SearchSettings {
|
||||
this.activeCls = activeCls;
|
||||
}
|
||||
|
||||
public JResource getActiveResource() {
|
||||
return activeResource;
|
||||
}
|
||||
|
||||
public void setActiveResource(JResource activeResource) {
|
||||
this.activeResource = activeResource;
|
||||
}
|
||||
|
||||
public ISearchMethod getSearchMethod() {
|
||||
return searchMethod;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import java.util.Objects;
|
||||
|
||||
import javax.swing.Icon;
|
||||
|
||||
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
@@ -197,7 +198,7 @@ public class CommentSearchProvider implements ISearchProvider {
|
||||
|
||||
@Override
|
||||
public String getSyntaxName() {
|
||||
return node.getSyntaxName();
|
||||
return SyntaxConstants.SYNTAX_STYLE_NONE; // comment is always plain text
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -220,6 +221,11 @@ public class CommentSearchProvider implements ISearchProvider {
|
||||
return node.makeLongStringHtml();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean disableHtml() {
|
||||
return node.disableHtml();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPos() {
|
||||
return node.getPos();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package jadx.gui.search.providers;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Collections;
|
||||
import java.util.Deque;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashSet;
|
||||
@@ -24,12 +25,15 @@ import jadx.gui.treemodel.JResSearchNode;
|
||||
import jadx.gui.treemodel.JResource;
|
||||
import jadx.gui.treemodel.JRoot;
|
||||
import jadx.gui.ui.MainWindow;
|
||||
import jadx.gui.ui.dialog.SearchDialog;
|
||||
import jadx.gui.utils.NLS;
|
||||
|
||||
public class ResourceSearchProvider implements ISearchProvider {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ResourceSearchProvider.class);
|
||||
|
||||
private final SearchSettings searchSettings;
|
||||
private final Set<String> extSet;
|
||||
private final SearchDialog searchDialog;
|
||||
private final int sizeLimit;
|
||||
private boolean anyExt;
|
||||
|
||||
@@ -39,11 +43,20 @@ public class ResourceSearchProvider implements ISearchProvider {
|
||||
private final Deque<JResource> resQueue;
|
||||
private int pos;
|
||||
|
||||
public ResourceSearchProvider(MainWindow mw, SearchSettings searchSettings) {
|
||||
private int loadErrors = 0;
|
||||
private int skipBySize = 0;
|
||||
|
||||
public ResourceSearchProvider(MainWindow mw, SearchSettings searchSettings, SearchDialog searchDialog) {
|
||||
this.searchSettings = searchSettings;
|
||||
this.sizeLimit = mw.getSettings().getSrhResourceSkipSize() * 1048576;
|
||||
this.extSet = buildAllowedFilesExtensions(mw.getSettings().getSrhResourceFileExt());
|
||||
this.resQueue = initResQueue(mw);
|
||||
this.searchDialog = searchDialog;
|
||||
JResource activeResource = searchSettings.getActiveResource();
|
||||
if (activeResource != null) {
|
||||
this.resQueue = new ArrayDeque<>(Collections.singleton(activeResource));
|
||||
} else {
|
||||
this.resQueue = initResQueue(mw);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -93,32 +106,49 @@ public class ResourceSearchProvider implements ISearchProvider {
|
||||
private @Nullable JResource getNextResFile(Cancelable cancelable) {
|
||||
while (true) {
|
||||
JResource node = resQueue.peekLast();
|
||||
if (node == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
node.loadNode();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Error load resource node: {}", node, e);
|
||||
resQueue.removeLast();
|
||||
continue;
|
||||
}
|
||||
if (cancelable.isCanceled()) {
|
||||
if (node == null || cancelable.isCanceled()) {
|
||||
return null;
|
||||
}
|
||||
if (node.getType() == JResource.JResType.FILE) {
|
||||
if (shouldProcess(node)) {
|
||||
if (shouldProcess(node) && loadResNode(node)) {
|
||||
return node;
|
||||
}
|
||||
resQueue.removeLast();
|
||||
} else {
|
||||
// dir
|
||||
resQueue.removeLast();
|
||||
loadResNode(node);
|
||||
addChildren(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateProgressInfo() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (loadErrors != 0) {
|
||||
sb.append(" ").append(NLS.str("search_dialog.resources_load_errors", loadErrors));
|
||||
}
|
||||
if (skipBySize != 0) {
|
||||
sb.append(" ").append(NLS.str("search_dialog.resources_skip_by_size", skipBySize));
|
||||
}
|
||||
if (sb.length() != 0) {
|
||||
sb.append(" ").append(NLS.str("search_dialog.resources_check_logs"));
|
||||
}
|
||||
searchDialog.updateProgressLabel(sb.toString());
|
||||
}
|
||||
|
||||
private boolean loadResNode(JResource node) {
|
||||
try {
|
||||
node.loadNode();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
LOG.error("Error load resource node: {}", node, e);
|
||||
loadErrors++;
|
||||
updateProgressInfo();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void addChildren(JResource resNode) {
|
||||
resQueue.addAll(resNode.getSubNodes());
|
||||
}
|
||||
@@ -167,19 +197,24 @@ public class ResourceSearchProvider implements ISearchProvider {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (sizeLimit == 0) {
|
||||
if (sizeLimit <= 0) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
int charsCount = resNode.getCodeInfo().getCodeStr().length();
|
||||
long size = charsCount * 8L;
|
||||
if (size > sizeLimit) {
|
||||
LOG.debug("Resource search skipped because of size limit: {} res size {} bytes", resNode, size);
|
||||
LOG.info("Resource search skipped because of size limit. Resource '{}' size {} bytes, limit: {}",
|
||||
resNode.getName(), size, sizeLimit);
|
||||
skipBySize++;
|
||||
updateProgressInfo();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Resource load error: {}", resNode, e);
|
||||
loadErrors++;
|
||||
updateProgressInfo();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ public class JadxSettings extends JadxCLIArgs {
|
||||
|
||||
private boolean showHeapUsageBar = false;
|
||||
private boolean alwaysSelectOpened = false;
|
||||
private boolean useAlternativeFileDialog = false;
|
||||
|
||||
private Map<String, WindowLocation> windowPos = new HashMap<>();
|
||||
private int mainWindowExtendedState = JFrame.NORMAL;
|
||||
@@ -268,6 +269,14 @@ public class JadxSettings extends JadxCLIArgs {
|
||||
partialSync(settings -> settings.alwaysSelectOpened = alwaysSelectOpened);
|
||||
}
|
||||
|
||||
public boolean isUseAlternativeFileDialog() {
|
||||
return useAlternativeFileDialog;
|
||||
}
|
||||
|
||||
public void setUseAlternativeFileDialog(boolean useAlternativeFileDialog) {
|
||||
this.useAlternativeFileDialog = useAlternativeFileDialog;
|
||||
}
|
||||
|
||||
public String getExcludedPackages() {
|
||||
return excludedPackages;
|
||||
}
|
||||
@@ -392,6 +401,10 @@ public class JadxSettings extends JadxCLIArgs {
|
||||
this.inlineMethods = inlineMethods;
|
||||
}
|
||||
|
||||
public void setAllowInlineKotlinLambda(boolean allowInlineKotlinLambda) {
|
||||
this.allowInlineKotlinLambda = allowInlineKotlinLambda;
|
||||
}
|
||||
|
||||
public void setExtractFinally(boolean extractFinally) {
|
||||
this.extractFinally = extractFinally;
|
||||
}
|
||||
|
||||
@@ -538,6 +538,13 @@ public class JadxSettingsWindow extends JDialog {
|
||||
needReload();
|
||||
});
|
||||
|
||||
JCheckBox inlineKotlinLambdas = new JCheckBox();
|
||||
inlineKotlinLambdas.setSelected(settings.isAllowInlineKotlinLambda());
|
||||
inlineKotlinLambdas.addItemListener(e -> {
|
||||
settings.setAllowInlineKotlinLambda(e.getStateChange() == ItemEvent.SELECTED);
|
||||
needReload();
|
||||
});
|
||||
|
||||
JCheckBox extractFinally = new JCheckBox();
|
||||
extractFinally.setSelected(settings.isExtractFinally());
|
||||
extractFinally.addItemListener(e -> {
|
||||
@@ -581,6 +588,7 @@ public class JadxSettingsWindow extends JDialog {
|
||||
other.addRow(NLS.str("preferences.useDebugInfo"), useDebugInfo);
|
||||
other.addRow(NLS.str("preferences.inlineAnonymous"), inlineAnonymous);
|
||||
other.addRow(NLS.str("preferences.inlineMethods"), inlineMethods);
|
||||
other.addRow(NLS.str("preferences.inlineKotlinLambdas"), inlineKotlinLambdas);
|
||||
other.addRow(NLS.str("preferences.extractFinally"), extractFinally);
|
||||
other.addRow(NLS.str("preferences.fsCaseSensitive"), fsCaseSensitive);
|
||||
other.addRow(NLS.str("preferences.useDx"), useDx);
|
||||
@@ -643,6 +651,10 @@ public class JadxSettingsWindow extends JDialog {
|
||||
jumpOnDoubleClick.setSelected(settings.isJumpOnDoubleClick());
|
||||
jumpOnDoubleClick.addItemListener(e -> settings.setJumpOnDoubleClick(e.getStateChange() == ItemEvent.SELECTED));
|
||||
|
||||
JCheckBox useAltFileDialog = new JCheckBox();
|
||||
useAltFileDialog.setSelected(settings.isUseAlternativeFileDialog());
|
||||
useAltFileDialog.addItemListener(e -> settings.setUseAlternativeFileDialog(e.getStateChange() == ItemEvent.SELECTED));
|
||||
|
||||
JCheckBox update = new JCheckBox();
|
||||
update.setSelected(settings.isCheckForUpdates());
|
||||
update.addItemListener(e -> settings.setCheckForUpdates(e.getStateChange() == ItemEvent.SELECTED));
|
||||
@@ -665,6 +677,7 @@ public class JadxSettingsWindow extends JDialog {
|
||||
group.addRow(NLS.str("preferences.language"), languageCbx);
|
||||
group.addRow(NLS.str("preferences.lineNumbersMode"), lineNumbersMode);
|
||||
group.addRow(NLS.str("preferences.jumpOnDoubleClick"), jumpOnDoubleClick);
|
||||
group.addRow(NLS.str("preferences.useAlternativeFileDialog"), useAltFileDialog);
|
||||
group.addRow(NLS.str("preferences.check_for_updates"), update);
|
||||
group.addRow(NLS.str("preferences.cfg"), cfg);
|
||||
group.addRow(NLS.str("preferences.raw_cfg"), rawCfg);
|
||||
|
||||
@@ -61,6 +61,11 @@ public class JVariable extends JNode {
|
||||
return UiUtils.typeFormatHtml(var.getName(), var.getType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean disableHtml() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTooltip() {
|
||||
String name = var.getName() + " (r" + var.getReg() + "v" + var.getSsa() + ")";
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
@@ -94,7 +95,6 @@ import jadx.gui.device.debugger.BreakpointManager;
|
||||
import jadx.gui.jobs.BackgroundExecutor;
|
||||
import jadx.gui.jobs.DecompileTask;
|
||||
import jadx.gui.jobs.ExportTask;
|
||||
import jadx.gui.jobs.ProcessResult;
|
||||
import jadx.gui.jobs.TaskStatus;
|
||||
import jadx.gui.plugins.mappings.MappingExporter;
|
||||
import jadx.gui.plugins.quark.QuarkDialog;
|
||||
@@ -116,10 +116,11 @@ import jadx.gui.ui.codearea.EditorTheme;
|
||||
import jadx.gui.ui.codearea.EditorViewState;
|
||||
import jadx.gui.ui.dialog.ADBDialog;
|
||||
import jadx.gui.ui.dialog.AboutDialog;
|
||||
import jadx.gui.ui.dialog.FileDialog;
|
||||
import jadx.gui.ui.dialog.LogViewerDialog;
|
||||
import jadx.gui.ui.dialog.RenameDialog;
|
||||
import jadx.gui.ui.dialog.SearchDialog;
|
||||
import jadx.gui.ui.filedialog.FileDialogWrapper;
|
||||
import jadx.gui.ui.filedialog.FileOpenMode;
|
||||
import jadx.gui.ui.panel.ContentPanel;
|
||||
import jadx.gui.ui.panel.IssuesPanel;
|
||||
import jadx.gui.ui.panel.JDebuggerPanel;
|
||||
@@ -171,6 +172,7 @@ public class MainWindow extends JFrame {
|
||||
private static final ImageIcon ICON_QUARK = UiUtils.openSvgIcon("ui/quark");
|
||||
private static final ImageIcon ICON_PREF = UiUtils.openSvgIcon("ui/settings");
|
||||
private static final ImageIcon ICON_DEOBF = UiUtils.openSvgIcon("ui/helmChartLock");
|
||||
private static final ImageIcon ICON_DECOMPILE_ALL = UiUtils.openSvgIcon("ui/runAll");
|
||||
private static final ImageIcon ICON_LOG = UiUtils.openSvgIcon("ui/logVerbose");
|
||||
private static final ImageIcon ICON_INFO = UiUtils.openSvgIcon("ui/showInfos");
|
||||
private static final ImageIcon ICON_DEBUGGER = UiUtils.openSvgIcon("ui/startDebugger");
|
||||
@@ -293,19 +295,19 @@ public class MainWindow extends JFrame {
|
||||
}
|
||||
|
||||
public void openFileDialog() {
|
||||
showOpenDialog(FileDialog.OpenMode.OPEN);
|
||||
showOpenDialog(FileOpenMode.OPEN);
|
||||
}
|
||||
|
||||
public void openProjectDialog() {
|
||||
showOpenDialog(FileDialog.OpenMode.OPEN_PROJECT);
|
||||
showOpenDialog(FileOpenMode.OPEN_PROJECT);
|
||||
}
|
||||
|
||||
private void showOpenDialog(FileDialog.OpenMode mode) {
|
||||
private void showOpenDialog(FileOpenMode mode) {
|
||||
saveAll();
|
||||
if (!ensureProjectIsSaved()) {
|
||||
return;
|
||||
}
|
||||
FileDialog fileDialog = new FileDialog(this, mode);
|
||||
FileDialogWrapper fileDialog = new FileDialogWrapper(this, mode);
|
||||
List<Path> openPaths = fileDialog.show();
|
||||
if (!openPaths.isEmpty()) {
|
||||
settings.setLastOpenFilePath(fileDialog.getCurrentDir());
|
||||
@@ -314,7 +316,7 @@ public class MainWindow extends JFrame {
|
||||
}
|
||||
|
||||
public void addFiles() {
|
||||
FileDialog fileDialog = new FileDialog(this, FileDialog.OpenMode.ADD);
|
||||
FileDialogWrapper fileDialog = new FileDialogWrapper(this, FileOpenMode.ADD);
|
||||
List<Path> addPaths = fileDialog.show();
|
||||
if (!addPaths.isEmpty()) {
|
||||
addFiles(addPaths);
|
||||
@@ -346,7 +348,7 @@ public class MainWindow extends JFrame {
|
||||
}
|
||||
|
||||
private void saveProjectAs() {
|
||||
FileDialog fileDialog = new FileDialog(this, FileDialog.OpenMode.SAVE_PROJECT);
|
||||
FileDialogWrapper fileDialog = new FileDialogWrapper(this, FileOpenMode.SAVE_PROJECT);
|
||||
if (project.getFilePaths().size() == 1) {
|
||||
// If there is only one file loaded we suggest saving the jadx project file next to the loaded file
|
||||
Path projectPath = getProjectPathForFile(this.project.getFilePaths().get(0));
|
||||
@@ -377,7 +379,7 @@ public class MainWindow extends JFrame {
|
||||
}
|
||||
|
||||
private void exportMappings(MappingFormat mappingFormat) {
|
||||
FileDialog fileDialog = new FileDialog(this, FileDialog.OpenMode.CUSTOM_SAVE);
|
||||
FileDialogWrapper fileDialog = new FileDialogWrapper(this, FileOpenMode.CUSTOM_SAVE);
|
||||
fileDialog.setTitle(NLS.str("file.export_mappings_as"));
|
||||
Path workingDir = project.getWorkingDir();
|
||||
Path baseDir = workingDir != null ? workingDir : settings.getLastSaveFilePath();
|
||||
@@ -530,9 +532,11 @@ public class MainWindow extends JFrame {
|
||||
updateLiveReload(project.isEnableLiveReload());
|
||||
BreakpointManager.init(project.getFilePaths().get(0).toAbsolutePath().getParent());
|
||||
|
||||
List<EditorViewState> openTabs = project.getOpenTabs(this);
|
||||
backgroundExecutor.execute(NLS.str("progress.load"),
|
||||
this::restoreOpenTabs,
|
||||
() -> preLoadOpenTabs(openTabs),
|
||||
status -> {
|
||||
restoreOpenTabs(openTabs);
|
||||
runInitialBackgroundJobs();
|
||||
notifyLoadListeners(true);
|
||||
});
|
||||
@@ -604,54 +608,17 @@ public class MainWindow extends JFrame {
|
||||
new Timer().schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
waitDecompileTask();
|
||||
requestFullDecompilation();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
private static final Object DECOMPILER_TASK_SYNC = new Object();
|
||||
|
||||
public void waitDecompileTask() {
|
||||
synchronized (DECOMPILER_TASK_SYNC) {
|
||||
try {
|
||||
DecompileTask decompileTask = new DecompileTask(wrapper);
|
||||
backgroundExecutor.executeAndWait(decompileTask);
|
||||
backgroundExecutor.execute(decompileTask.getTitle(), wrapper::unloadClasses).get();
|
||||
processDecompilationResults(decompileTask.getResult());
|
||||
System.gc();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Decompile task execution failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processDecompilationResults(ProcessResult decompile) {
|
||||
int skippedCls = decompile.getSkipped();
|
||||
if (skippedCls == 0) {
|
||||
public void requestFullDecompilation() {
|
||||
if (cacheObject.isFullDecompilationFinished()) {
|
||||
return;
|
||||
}
|
||||
TaskStatus status = decompile.getStatus();
|
||||
LOG.warn("Decompile and indexing of some classes skipped: {}, status: {}", skippedCls, status);
|
||||
switch (status) {
|
||||
case CANCEL_BY_USER: {
|
||||
String reason = NLS.str("message.userCancelTask");
|
||||
String message = NLS.str("message.indexIncomplete", reason, skippedCls);
|
||||
JOptionPane.showMessageDialog(this, message);
|
||||
break;
|
||||
}
|
||||
case CANCEL_BY_TIMEOUT: {
|
||||
String reason = NLS.str("message.taskTimeout", decompile.getTimeLimit());
|
||||
String message = NLS.str("message.indexIncomplete", reason, skippedCls);
|
||||
JOptionPane.showMessageDialog(this, message);
|
||||
break;
|
||||
}
|
||||
case CANCEL_BY_MEMORY: {
|
||||
showHeapUsageBar();
|
||||
JOptionPane.showMessageDialog(this, NLS.str("message.indexingClassesSkipped", skippedCls));
|
||||
break;
|
||||
}
|
||||
}
|
||||
backgroundExecutor.execute(new DecompileTask(this));
|
||||
}
|
||||
|
||||
public void cancelBackgroundJobs() {
|
||||
@@ -659,7 +626,7 @@ public class MainWindow extends JFrame {
|
||||
}
|
||||
|
||||
private void saveAll(boolean export) {
|
||||
FileDialog fileDialog = new FileDialog(this, FileDialog.OpenMode.EXPORT);
|
||||
FileDialogWrapper fileDialog = new FileDialogWrapper(this, FileOpenMode.EXPORT);
|
||||
List<Path> saveDirs = fileDialog.show();
|
||||
if (saveDirs.isEmpty()) {
|
||||
return;
|
||||
@@ -1037,6 +1004,10 @@ public class MainWindow extends JFrame {
|
||||
commentSearchAction.putValue(Action.ACCELERATOR_KEY, getKeyStroke(KeyEvent.VK_SEMICOLON,
|
||||
UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK));
|
||||
|
||||
ActionHandler decompileAllAction = new ActionHandler(ev -> requestFullDecompilation());
|
||||
decompileAllAction.setNameAndDesc(NLS.str("menu.decompile_all"));
|
||||
decompileAllAction.setIcon(ICON_DECOMPILE_ALL);
|
||||
|
||||
Action deobfAction = new AbstractAction(NLS.str("menu.deobfuscation"), ICON_DEOBF) {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
@@ -1148,6 +1119,7 @@ public class MainWindow extends JFrame {
|
||||
|
||||
JMenu tools = new JMenu(NLS.str("menu.tools"));
|
||||
tools.setMnemonic(KeyEvent.VK_T);
|
||||
tools.add(decompileAllAction);
|
||||
tools.add(deobfMenuItem);
|
||||
tools.add(quarkAction);
|
||||
tools.add(openDeviceAction);
|
||||
@@ -1227,6 +1199,7 @@ public class MainWindow extends JFrame {
|
||||
exportAction.setEnabled(loaded);
|
||||
saveProjectAsAction.setEnabled(loaded);
|
||||
reload.setEnabled(loaded);
|
||||
decompileAllAction.setEnabled(loaded);
|
||||
deobfAction.setEnabled(loaded);
|
||||
quarkAction.setEnabled(loaded);
|
||||
return false;
|
||||
@@ -1422,8 +1395,9 @@ public class MainWindow extends JFrame {
|
||||
}
|
||||
GraphicsDevice gd = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
|
||||
DisplayMode mode = gd.getDisplayMode();
|
||||
int w = mode.getWidth();
|
||||
int h = mode.getHeight();
|
||||
AffineTransform trans = gd.getDefaultConfiguration().getDefaultTransform();
|
||||
int w = (int) (mode.getWidth() / trans.getScaleX());
|
||||
int h = (int) (mode.getHeight() / trans.getScaleY());
|
||||
setBounds((int) (w * BORDER_RATIO), (int) (h * BORDER_RATIO),
|
||||
(int) (w * WINDOW_RATIO), (int) (h * WINDOW_RATIO));
|
||||
setLocationRelativeTo(null);
|
||||
@@ -1500,8 +1474,8 @@ public class MainWindow extends JFrame {
|
||||
project.saveOpenTabs(tabbedPane.getEditorViewStates(), tabbedPane.getSelectedIndex());
|
||||
}
|
||||
|
||||
private void restoreOpenTabs() {
|
||||
List<EditorViewState> openTabs = project.getOpenTabs(this);
|
||||
private void restoreOpenTabs(List<EditorViewState> openTabs) {
|
||||
UiUtils.uiThreadGuard();
|
||||
if (openTabs.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
@@ -1515,6 +1489,18 @@ public class MainWindow extends JFrame {
|
||||
}
|
||||
}
|
||||
|
||||
private void preLoadOpenTabs(List<EditorViewState> openTabs) {
|
||||
UiUtils.notUiThreadGuard();
|
||||
for (EditorViewState tabState : openTabs) {
|
||||
JNode node = tabState.getNode();
|
||||
try {
|
||||
node.getCodeInfo();
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Failed to preload code for node: {}", node, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void saveSplittersInfo() {
|
||||
settings.setMainWindowVerticalSplitterLoc(verticalSplitter.getDividerLocation());
|
||||
settings.setDebuggerStackFrameSplitterLoc(debuggerPanel.getLeftSplitterLocation());
|
||||
|
||||
@@ -73,6 +73,7 @@ public class TabbedPane extends JTabbedPane {
|
||||
}
|
||||
});
|
||||
interceptTabKey();
|
||||
interceptCloseKey();
|
||||
enableSwitchingTabs();
|
||||
}
|
||||
|
||||
@@ -120,6 +121,34 @@ public class TabbedPane extends JTabbedPane {
|
||||
});
|
||||
}
|
||||
|
||||
private void interceptCloseKey() {
|
||||
KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(new KeyEventDispatcher() {
|
||||
private static final int closeKey = KeyEvent.VK_W;
|
||||
private boolean canClose = true;
|
||||
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent e) {
|
||||
if (!FocusManager.isActive()) {
|
||||
return false; // do nothing when tab is not on focus.
|
||||
}
|
||||
if (e.getKeyCode() != closeKey) {
|
||||
return false; // only intercept the events of the close key
|
||||
}
|
||||
if (e.getID() == KeyEvent.KEY_RELEASED) {
|
||||
canClose = true; // after the close key is lifted we allow to use it again
|
||||
return false;
|
||||
}
|
||||
if (e.isControlDown() && canClose) {
|
||||
// close the current tab
|
||||
closeCodePanel(curTab);
|
||||
canClose = false; // make sure we dont close more tabs until the close key is lifted
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void enableSwitchingTabs() {
|
||||
addChangeListener(e -> {
|
||||
ContentPanel tab = getSelectedCodePanel();
|
||||
|
||||
@@ -2,6 +2,7 @@ package jadx.gui.ui.codearea;
|
||||
|
||||
import java.awt.Component;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Insets;
|
||||
import java.awt.Point;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.event.ActionEvent;
|
||||
@@ -396,4 +397,24 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea {
|
||||
LOG.debug("Error on code area dispose", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dimension getPreferredSize() {
|
||||
try {
|
||||
return super.getPreferredSize();
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Failed to calculate preferred size for code area", e);
|
||||
// copied from javax.swing.JTextArea.getPreferredSize (super call above)
|
||||
// as a fallback for returned null size
|
||||
Dimension d = new Dimension(400, 400);
|
||||
Insets insets = getInsets();
|
||||
if (getColumns() != 0) {
|
||||
d.width = Math.max(d.width, getColumns() * getColumnWidth() + insets.left + insets.right);
|
||||
}
|
||||
if (getRows() != 0) {
|
||||
d.height = Math.max(d.height, getRows() * getRowHeight() + insets.top + insets.bottom);
|
||||
}
|
||||
return d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import jadx.api.utils.CodeUtils;
|
||||
import jadx.core.codegen.TypeGen;
|
||||
import jadx.core.dex.info.MethodInfo;
|
||||
import jadx.core.dex.instructions.args.ArgType;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.gui.treemodel.JClass;
|
||||
@@ -76,45 +75,50 @@ public final class FridaAction extends JNodeAction {
|
||||
}
|
||||
|
||||
private String generateMethodSnippet(JMethod jMth) {
|
||||
JavaMethod javaMethod = jMth.getJavaMethod();
|
||||
MethodInfo methodInfo = javaMethod.getMethodNode().getMethodInfo();
|
||||
String methodName = StringEscapeUtils.escapeEcmaScript(methodInfo.getName());
|
||||
String callMethodName = methodName;
|
||||
|
||||
MethodNode mth = jMth.getJavaMethod().getMethodNode();
|
||||
MethodInfo methodInfo = mth.getMethodInfo();
|
||||
String methodName;
|
||||
String newMethodName;
|
||||
if (methodInfo.isConstructor()) {
|
||||
methodName = "$init";
|
||||
callMethodName = "$new";
|
||||
}
|
||||
String shortClassName = javaMethod.getDeclaringClass().getName();
|
||||
|
||||
String functionUntilImplementation;
|
||||
if (isOverloaded(javaMethod.getMethodNode())) {
|
||||
List<ArgType> methodArgs = methodInfo.getArgumentsTypes();
|
||||
String overloadStr = methodArgs.stream().map(this::parseArgType).collect(Collectors.joining(", "));
|
||||
functionUntilImplementation = String.format("%s[\"%s\"].overload(%s).implementation", shortClassName, methodName, overloadStr);
|
||||
newMethodName = methodName;
|
||||
} else {
|
||||
functionUntilImplementation = String.format("%s[\"%s\"].implementation", shortClassName, methodName);
|
||||
methodName = StringEscapeUtils.escapeEcmaScript(methodInfo.getName());
|
||||
newMethodName = StringEscapeUtils.escapeEcmaScript(methodInfo.getAlias());
|
||||
}
|
||||
|
||||
List<String> methodArgNames = collectMethodArgNames(javaMethod);
|
||||
|
||||
String functionParametersString = String.join(", ", methodArgNames);
|
||||
String logParametersString =
|
||||
methodArgNames.stream().map(e -> String.format("'%s: ' + %s", e, e)).collect(Collectors.joining(" + ', ' + "));
|
||||
if (logParametersString.length() > 0) {
|
||||
logParametersString = " + ', ' + " + logParametersString;
|
||||
String overload;
|
||||
if (isOverloaded(mth)) {
|
||||
String overloadArgs = methodInfo.getArgumentsTypes().stream()
|
||||
.map(this::parseArgType).collect(Collectors.joining(", "));
|
||||
overload = ".overload(" + overloadArgs + ")";
|
||||
} else {
|
||||
overload = "";
|
||||
}
|
||||
String functionParameterAndBody = String.format(
|
||||
"%s = function (%s) {\n"
|
||||
+ " console.log('%s is called'%s);\n"
|
||||
+ " let ret = this.%s(%s);\n"
|
||||
+ " console.log('%s ret value is ' + ret);\n"
|
||||
+ " return ret;\n"
|
||||
+ "};",
|
||||
functionUntilImplementation, functionParametersString, methodName, logParametersString, callMethodName,
|
||||
functionParametersString, methodName);
|
||||
|
||||
return generateClassSnippet(jMth.getJParent()) + "\n" + functionParameterAndBody;
|
||||
List<String> argNames = collectMethodArgNames(jMth.getJavaMethod());
|
||||
String args = String.join(", ", argNames);
|
||||
String logArgs;
|
||||
if (argNames.isEmpty()) {
|
||||
logArgs = "";
|
||||
} else {
|
||||
logArgs = ": " + argNames.stream().map(arg -> arg + "=${" + arg + "}").collect(Collectors.joining(", "));
|
||||
}
|
||||
String shortClassName = mth.getParentClass().getShortName();
|
||||
String classSnippet = generateClassSnippet(jMth.getJParent());
|
||||
if (methodInfo.isConstructor() || methodInfo.getReturnType() == ArgType.VOID) {
|
||||
// no return value
|
||||
return classSnippet + "\n"
|
||||
+ shortClassName + "[\"" + methodName + "\"]" + overload + ".implementation = function (" + args + ") {\n"
|
||||
+ " console.log(`" + shortClassName + "." + newMethodName + " is called" + logArgs + "`);\n"
|
||||
+ " this[\"" + methodName + "\"](" + args + ");\n"
|
||||
+ "};";
|
||||
}
|
||||
return classSnippet + "\n"
|
||||
+ shortClassName + "[\"" + methodName + "\"]" + overload + ".implementation = function (" + args + ") {\n"
|
||||
+ " console.log(`" + shortClassName + "." + newMethodName + " is called" + logArgs + "`);\n"
|
||||
+ " let result = this[\"" + methodName + "\"](" + args + ");\n"
|
||||
+ " console.log(`" + shortClassName + "." + newMethodName + " result=${result}`);\n"
|
||||
+ " return result;\n"
|
||||
+ "};";
|
||||
}
|
||||
|
||||
private List<String> collectMethodArgNames(JavaMethod javaMethod) {
|
||||
@@ -137,6 +141,10 @@ public final class FridaAction extends JNodeAction {
|
||||
}
|
||||
return null;
|
||||
});
|
||||
int argsCount = javaMethod.getMethodNode().getMethodInfo().getArgsCount();
|
||||
if (argNames.size() != argsCount) {
|
||||
LOG.warn("Incorrect args count, expected: {}, got: {}", argsCount, argNames.size());
|
||||
}
|
||||
return argNames;
|
||||
}
|
||||
|
||||
@@ -159,27 +167,24 @@ public final class FridaAction extends JNodeAction {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
JClass jc = jf.getRootClass();
|
||||
String classSnippet = generateClassSnippet(jc);
|
||||
return String.format("%s\n%s = %s.%s.value;", classSnippet, fieldName, jc.getName(), rawFieldName);
|
||||
}
|
||||
|
||||
public Boolean isOverloaded(MethodNode methodNode) {
|
||||
ClassNode parentClass = methodNode.getParentClass();
|
||||
List<MethodNode> methods = parentClass.getMethods();
|
||||
return methods.stream()
|
||||
return methodNode.getParentClass().getMethods().stream()
|
||||
.anyMatch(m -> m.getName().equals(methodNode.getName())
|
||||
&& !Objects.equals(methodNode.getMethodInfo().getShortId(), m.getMethodInfo().getShortId()));
|
||||
}
|
||||
|
||||
private String parseArgType(ArgType x) {
|
||||
StringBuilder parsedArgType = new StringBuilder("'");
|
||||
String typeStr;
|
||||
if (x.isArray()) {
|
||||
parsedArgType.append(TypeGen.signature(x).replace("/", "."));
|
||||
typeStr = TypeGen.signature(x).replace("/", ".");
|
||||
} else {
|
||||
parsedArgType.append(x);
|
||||
typeStr = x.toString();
|
||||
}
|
||||
return parsedArgType.append("'").toString();
|
||||
return "'" + typeStr + "'";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,28 +475,28 @@ public class ADBDialog extends JDialog implements ADB.DeviceStateListener, ADB.J
|
||||
LOG.error("Failed to find device", e);
|
||||
return;
|
||||
}
|
||||
node.tNode.removeAllChildren();
|
||||
DefaultMutableTreeNode tempNode = null;
|
||||
for (String s : procList) {
|
||||
DefaultMutableTreeNode pnode = new DefaultMutableTreeNode(s);
|
||||
node.tNode.add(pnode);
|
||||
if (!debugSetter.expectPkg.isEmpty() && s.endsWith(debugSetter.expectPkg)) {
|
||||
if (debugSetter.autoAttachPkg && debugSetter.device.equals(node.device)) {
|
||||
debugSetter.set(node.device, debugSetter.ver, getPid(s), s);
|
||||
if (attachProcess(mainWindow)) {
|
||||
dispose();
|
||||
return;
|
||||
}
|
||||
}
|
||||
tempNode = pnode;
|
||||
}
|
||||
}
|
||||
DefaultMutableTreeNode theNode = tempNode;
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
node.tNode.removeAllChildren();
|
||||
DefaultMutableTreeNode foundNode = null;
|
||||
for (String procStr : procList) {
|
||||
DefaultMutableTreeNode pnode = new DefaultMutableTreeNode(procStr);
|
||||
node.tNode.add(pnode);
|
||||
if (!debugSetter.expectPkg.isEmpty() && procStr.endsWith(debugSetter.expectPkg)) {
|
||||
if (debugSetter.autoAttachPkg && debugSetter.device.equals(node.device)) {
|
||||
debugSetter.set(node.device, debugSetter.ver, getPid(procStr), procStr);
|
||||
if (attachProcess(mainWindow)) {
|
||||
dispose();
|
||||
return;
|
||||
}
|
||||
}
|
||||
foundNode = pnode;
|
||||
}
|
||||
}
|
||||
procTreeModel.reload(node.tNode);
|
||||
procTree.expandPath(new TreePath(node.tNode.getPath()));
|
||||
if (theNode != null) {
|
||||
TreePath thePath = new TreePath(theNode.getPath());
|
||||
if (foundNode != null) {
|
||||
TreePath thePath = new TreePath(foundNode.getPath());
|
||||
procTree.scrollPathToVisible(thePath);
|
||||
procTree.setSelectionPath(thePath);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.swing.AbstractAction;
|
||||
import javax.swing.BorderFactory;
|
||||
@@ -44,6 +45,12 @@ import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ch.qos.logback.classic.Level;
|
||||
|
||||
import jadx.api.JavaClass;
|
||||
import jadx.api.metadata.ICodeAnnotation;
|
||||
import jadx.api.metadata.annotations.NodeDeclareRef;
|
||||
import jadx.gui.treemodel.JClass;
|
||||
import jadx.gui.treemodel.JNode;
|
||||
import jadx.gui.treemodel.JResSearchNode;
|
||||
import jadx.gui.ui.MainWindow;
|
||||
@@ -73,6 +80,7 @@ public abstract class CommonSearchDialog extends JFrame {
|
||||
protected ResultsModel resultsModel;
|
||||
protected ResultsTable resultsTable;
|
||||
protected JLabel resultsInfoLabel;
|
||||
protected JLabel progressInfoLabel;
|
||||
protected JLabel warnLabel;
|
||||
protected ProgressPanel progressPane;
|
||||
|
||||
@@ -142,13 +150,45 @@ public abstract class CommonSearchDialog extends JFrame {
|
||||
JumpPosition jmpPos = new JumpPosition(((JResSearchNode) node).getResNode(), node.getPos());
|
||||
tabbedPane.codeJump(jmpPos);
|
||||
} else {
|
||||
tabbedPane.codeJump(node);
|
||||
if (!checkForRedirects(node)) {
|
||||
tabbedPane.codeJump(node);
|
||||
}
|
||||
}
|
||||
if (!mainWindow.getSettings().getKeepCommonDialogOpen()) {
|
||||
dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: temp solution, move implementation into corresponding nodes
|
||||
private boolean checkForRedirects(JNode node) {
|
||||
if (node instanceof JClass) {
|
||||
JavaClass cls = ((JClass) node).getCls();
|
||||
JavaClass origTopCls = cls.getOriginalTopParentClass();
|
||||
JavaClass codeParent = cls.getTopParentClass();
|
||||
if (Objects.equals(codeParent, origTopCls)) {
|
||||
return false;
|
||||
}
|
||||
JClass jumpCls = mainWindow.getCacheObject().getNodeCache().makeFrom(codeParent);
|
||||
mainWindow.getBackgroundExecutor().execute(
|
||||
NLS.str("progress.load"),
|
||||
jumpCls::loadNode, // load code in background
|
||||
status -> {
|
||||
// search original node in jump class
|
||||
codeParent.getCodeInfo().getCodeMetadata().searchDown(0, (pos, ann) -> {
|
||||
if (ann.getAnnType() == ICodeAnnotation.AnnType.DECLARATION) {
|
||||
if (((NodeDeclareRef) ann).getNode().equals(cls.getClassNode())) {
|
||||
tabbedPane.codeJump(new JumpPosition(jumpCls, pos));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private JNode getSelectedNode() {
|
||||
try {
|
||||
@@ -260,6 +300,15 @@ public abstract class CommonSearchDialog extends JFrame {
|
||||
resultsInfoLabel = new JLabel("");
|
||||
resultsInfoLabel.setFont(mainWindow.getSettings().getFont());
|
||||
|
||||
progressInfoLabel = new JLabel("");
|
||||
progressInfoLabel.setFont(mainWindow.getSettings().getFont());
|
||||
progressInfoLabel.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
LogViewerDialog.openWithLevel(mainWindow, Level.INFO);
|
||||
}
|
||||
});
|
||||
|
||||
JPanel resultsActionsPanel = new JPanel();
|
||||
resultsActionsPanel.setLayout(new BoxLayout(resultsActionsPanel, BoxLayout.LINE_AXIS));
|
||||
resultsActionsPanel.setBorder(BorderFactory.createEmptyBorder(10, 0, 10, 0));
|
||||
@@ -276,6 +325,8 @@ public abstract class CommonSearchDialog extends JFrame {
|
||||
protected void addResultsActions(JPanel resultsActionsPanel) {
|
||||
resultsActionsPanel.add(Box.createRigidArea(new Dimension(20, 0)));
|
||||
resultsActionsPanel.add(resultsInfoLabel);
|
||||
resultsActionsPanel.add(Box.createRigidArea(new Dimension(20, 0)));
|
||||
resultsActionsPanel.add(progressInfoLabel);
|
||||
resultsActionsPanel.add(Box.createHorizontalGlue());
|
||||
}
|
||||
|
||||
@@ -454,10 +505,10 @@ public abstract class CommonSearchDialog extends JFrame {
|
||||
|
||||
private Component makeCell(JNode node, int column) {
|
||||
if (column == 0) {
|
||||
label.disableHtml(node.disableHtml());
|
||||
label.setText(node.makeLongStringHtml());
|
||||
label.setToolTipText(node.getTooltip());
|
||||
label.setIcon(node.getIcon());
|
||||
label.disableHtml(node.disableHtml());
|
||||
return label;
|
||||
}
|
||||
if (!node.hasDescString()) {
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
package jadx.gui.ui.dialog;
|
||||
|
||||
import java.awt.Component;
|
||||
import java.awt.HeadlessException;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.swing.JDialog;
|
||||
import javax.swing.JFileChooser;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.filechooser.FileNameExtensionFilter;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.api.plugins.utils.CommonFileUtils;
|
||||
import jadx.core.utils.Utils;
|
||||
import jadx.core.utils.files.FileUtils;
|
||||
import jadx.gui.settings.JadxProject;
|
||||
import jadx.gui.ui.MainWindow;
|
||||
import jadx.gui.utils.NLS;
|
||||
|
||||
public class FileDialog {
|
||||
|
||||
public enum OpenMode {
|
||||
OPEN,
|
||||
OPEN_PROJECT,
|
||||
ADD,
|
||||
SAVE_PROJECT,
|
||||
EXPORT,
|
||||
CUSTOM_SAVE,
|
||||
CUSTOM_OPEN
|
||||
}
|
||||
|
||||
private final MainWindow mainWindow;
|
||||
|
||||
private boolean isOpen;
|
||||
private String title;
|
||||
private List<String> fileExtList;
|
||||
private int selectionMode = JFileChooser.FILES_AND_DIRECTORIES;
|
||||
private @Nullable Path currentDir;
|
||||
private @Nullable Path selectedFile;
|
||||
|
||||
public FileDialog(MainWindow mainWindow, OpenMode mode) {
|
||||
this.mainWindow = mainWindow;
|
||||
initForMode(mode);
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public void setFileExtList(List<String> fileExtList) {
|
||||
this.fileExtList = fileExtList;
|
||||
}
|
||||
|
||||
public void setSelectionMode(int selectionMode) {
|
||||
this.selectionMode = selectionMode;
|
||||
}
|
||||
|
||||
public void setSelectedFile(Path path) {
|
||||
this.selectedFile = path;
|
||||
}
|
||||
|
||||
public void setCurrentDir(Path currentDir) {
|
||||
this.currentDir = currentDir;
|
||||
}
|
||||
|
||||
public List<Path> show() {
|
||||
FileChooser fileChooser = buildFileChooser();
|
||||
int ret = isOpen ? fileChooser.showOpenDialog(mainWindow) : fileChooser.showSaveDialog(mainWindow);
|
||||
if (ret != JFileChooser.APPROVE_OPTION) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
currentDir = fileChooser.getCurrentDirectory().toPath();
|
||||
File[] selectedFiles = fileChooser.getSelectedFiles();
|
||||
if (selectedFiles.length != 0) {
|
||||
return FileUtils.toPaths(selectedFiles);
|
||||
}
|
||||
File chosenFile = fileChooser.getSelectedFile();
|
||||
if (chosenFile != null) {
|
||||
return Collections.singletonList(chosenFile.toPath());
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public Path getCurrentDir() {
|
||||
return currentDir;
|
||||
}
|
||||
|
||||
private void initForMode(OpenMode mode) {
|
||||
switch (mode) {
|
||||
case OPEN:
|
||||
case OPEN_PROJECT:
|
||||
case ADD:
|
||||
if (mode == OpenMode.OPEN_PROJECT) {
|
||||
fileExtList = Collections.singletonList(JadxProject.PROJECT_EXTENSION);
|
||||
title = NLS.str("file.open_title");
|
||||
} else {
|
||||
fileExtList = new ArrayList<>(Arrays.asList("apk", "dex", "jar", "class", "smali", "zip", "xapk", "aar", "arsc"));
|
||||
if (mode == OpenMode.OPEN) {
|
||||
fileExtList.addAll(Arrays.asList(JadxProject.PROJECT_EXTENSION, "aab"));
|
||||
title = NLS.str("file.open_title");
|
||||
} else {
|
||||
title = NLS.str("file.add_files_action");
|
||||
}
|
||||
}
|
||||
selectionMode = JFileChooser.FILES_AND_DIRECTORIES;
|
||||
currentDir = mainWindow.getSettings().getLastOpenFilePath();
|
||||
isOpen = true;
|
||||
break;
|
||||
|
||||
case SAVE_PROJECT:
|
||||
title = NLS.str("file.save_project");
|
||||
fileExtList = Collections.singletonList(JadxProject.PROJECT_EXTENSION);
|
||||
selectionMode = JFileChooser.FILES_ONLY;
|
||||
currentDir = mainWindow.getSettings().getLastSaveFilePath();
|
||||
isOpen = false;
|
||||
break;
|
||||
|
||||
case EXPORT:
|
||||
title = NLS.str("file.save_all_msg");
|
||||
fileExtList = Collections.emptyList();
|
||||
selectionMode = JFileChooser.DIRECTORIES_ONLY;
|
||||
currentDir = mainWindow.getSettings().getLastSaveFilePath();
|
||||
isOpen = false;
|
||||
break;
|
||||
|
||||
case CUSTOM_SAVE:
|
||||
isOpen = false;
|
||||
break;
|
||||
|
||||
case CUSTOM_OPEN:
|
||||
isOpen = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private FileChooser buildFileChooser() {
|
||||
FileChooser fileChooser = new FileChooser(currentDir);
|
||||
fileChooser.setToolTipText(title);
|
||||
fileChooser.setFileSelectionMode(selectionMode);
|
||||
fileChooser.setMultiSelectionEnabled(isOpen);
|
||||
fileChooser.setAcceptAllFileFilterUsed(true);
|
||||
if (Utils.notEmpty(fileExtList)) {
|
||||
String description = NLS.str("file_dialog.supported_files") + ": (" + Utils.listToString(fileExtList) + ')';
|
||||
fileChooser.setFileFilter(new FileNameExtensionFilter(description, fileExtList.toArray(new String[0])));
|
||||
}
|
||||
if (selectedFile != null) {
|
||||
fileChooser.setSelectedFile(selectedFile.toFile());
|
||||
}
|
||||
return fileChooser;
|
||||
}
|
||||
|
||||
private class FileChooser extends JFileChooser {
|
||||
|
||||
public FileChooser(@Nullable Path currentDirectory) {
|
||||
super(currentDirectory == null ? CommonFileUtils.CWD : currentDirectory.toFile());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JDialog createDialog(Component parent) throws HeadlessException {
|
||||
JDialog dialog = super.createDialog(parent);
|
||||
dialog.setTitle(title);
|
||||
dialog.setLocationRelativeTo(null);
|
||||
mainWindow.getSettings().loadWindowPos(dialog);
|
||||
dialog.addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosed(WindowEvent e) {
|
||||
mainWindow.getSettings().saveWindowPos(dialog);
|
||||
super.windowClosed(e);
|
||||
}
|
||||
});
|
||||
return dialog;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void approveSelection() {
|
||||
if (selectionMode == FILES_AND_DIRECTORIES) {
|
||||
File currentFile = getSelectedFile();
|
||||
if (currentFile.isDirectory()) {
|
||||
int option = JOptionPane.showConfirmDialog(
|
||||
mainWindow,
|
||||
NLS.str("file_dialog.load_dir_confirm") + "\n " + currentFile,
|
||||
NLS.str("file_dialog.load_dir_title"),
|
||||
JOptionPane.YES_NO_OPTION);
|
||||
if (option != JOptionPane.YES_OPTION) {
|
||||
this.setCurrentDirectory(currentFile);
|
||||
this.updateUI();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
super.approveSelection();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,7 @@ import jadx.gui.search.providers.MethodSearchProvider;
|
||||
import jadx.gui.search.providers.ResourceSearchProvider;
|
||||
import jadx.gui.treemodel.JClass;
|
||||
import jadx.gui.treemodel.JNode;
|
||||
import jadx.gui.treemodel.JResource;
|
||||
import jadx.gui.ui.MainWindow;
|
||||
import jadx.gui.utils.JumpPosition;
|
||||
import jadx.gui.utils.NLS;
|
||||
@@ -452,9 +453,18 @@ public class SearchDialog extends CommonSearchDialog {
|
||||
resultsInfoLabel.setText("Can't search in current tab");
|
||||
return false;
|
||||
}
|
||||
JClass activeCls = currentPos.getNode().getRootClass();
|
||||
searchSettings.setActiveCls(activeCls);
|
||||
allClasses = Collections.singletonList(activeCls.getCls());
|
||||
JNode currentNode = currentPos.getNode();
|
||||
if (currentNode instanceof JClass) {
|
||||
JClass activeCls = currentNode.getRootClass();
|
||||
searchSettings.setActiveCls(activeCls);
|
||||
allClasses = Collections.singletonList(activeCls.getCls());
|
||||
} else if (currentNode instanceof JResource) {
|
||||
searchSettings.setActiveResource((JResource) currentNode);
|
||||
allClasses = Collections.emptyList();
|
||||
} else {
|
||||
resultsInfoLabel.setText("Can't search in current tab");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
allClasses = mainWindow.getWrapper().getIncludedClassesWithInners();
|
||||
}
|
||||
@@ -475,9 +485,10 @@ public class SearchDialog extends CommonSearchDialog {
|
||||
merged.add(new FieldSearchProvider(mainWindow, searchSettings, allClasses));
|
||||
}
|
||||
if (options.contains(CODE)) {
|
||||
if (allClasses.size() == 1) {
|
||||
int clsCount = allClasses.size();
|
||||
if (clsCount == 1) {
|
||||
newSearchTask.addProviderJob(new CodeSearchProvider(mainWindow, searchSettings, allClasses));
|
||||
} else {
|
||||
} else if (clsCount > 1) {
|
||||
List<List<JavaClass>> batches = mainWindow.getCacheObject().getDecompileBatches();
|
||||
if (batches == null) {
|
||||
List<JavaClass> topClasses = ListUtils.filter(allClasses, c -> !c.isInner());
|
||||
@@ -490,7 +501,7 @@ public class SearchDialog extends CommonSearchDialog {
|
||||
}
|
||||
}
|
||||
if (options.contains(RESOURCE)) {
|
||||
newSearchTask.addProviderJob(new ResourceSearchProvider(mainWindow, searchSettings));
|
||||
newSearchTask.addProviderJob(new ResourceSearchProvider(mainWindow, searchSettings, this));
|
||||
}
|
||||
if (options.contains(COMMENT)) {
|
||||
newSearchTask.addProviderJob(new CommentSearchProvider(mainWindow, searchSettings));
|
||||
@@ -549,6 +560,7 @@ public class SearchDialog extends CommonSearchDialog {
|
||||
synchronized (pendingResults) {
|
||||
pendingResults.clear();
|
||||
}
|
||||
updateProgressLabel("");
|
||||
progressPane.setVisible(false);
|
||||
warnLabel.setVisible(false);
|
||||
loadAllButton.setEnabled(false);
|
||||
@@ -598,6 +610,10 @@ public class SearchDialog extends CommonSearchDialog {
|
||||
});
|
||||
}
|
||||
|
||||
public void updateProgressLabel(String text) {
|
||||
UiUtils.uiRun(() -> progressInfoLabel.setText(text));
|
||||
}
|
||||
|
||||
private void searchFinished(ITaskInfo status, Boolean complete) {
|
||||
UiUtils.uiThreadGuard();
|
||||
LOG.debug("Search complete: {}, complete: {}", status, complete);
|
||||
|
||||
@@ -19,11 +19,14 @@ import jadx.api.JavaClass;
|
||||
import jadx.api.JavaMethod;
|
||||
import jadx.api.JavaNode;
|
||||
import jadx.api.utils.CodeUtils;
|
||||
import jadx.core.dex.info.ConstStorage;
|
||||
import jadx.core.dex.nodes.FieldNode;
|
||||
import jadx.gui.JadxWrapper;
|
||||
import jadx.gui.jobs.TaskStatus;
|
||||
import jadx.gui.settings.JadxSettings;
|
||||
import jadx.gui.treemodel.CodeNode;
|
||||
import jadx.gui.treemodel.JClass;
|
||||
import jadx.gui.treemodel.JField;
|
||||
import jadx.gui.treemodel.JMethod;
|
||||
import jadx.gui.treemodel.JNode;
|
||||
import jadx.gui.ui.MainWindow;
|
||||
@@ -51,6 +54,7 @@ public class UsageDialog extends CommonSearchDialog {
|
||||
@Override
|
||||
protected void openInit() {
|
||||
progressStartCommon();
|
||||
prepareUsageData();
|
||||
mainWindow.getBackgroundExecutor().execute(NLS.str("progress.load"),
|
||||
this::collectUsageData,
|
||||
(status) -> {
|
||||
@@ -63,26 +67,39 @@ public class UsageDialog extends CommonSearchDialog {
|
||||
});
|
||||
}
|
||||
|
||||
private void prepareUsageData() {
|
||||
if (mainWindow.getSettings().isReplaceConsts() && node instanceof JField) {
|
||||
FieldNode fld = ((JField) node).getJavaField().getFieldNode();
|
||||
boolean constField = ConstStorage.getFieldConstValue(fld) != null;
|
||||
if (constField && !fld.getAccessFlags().isPrivate()) {
|
||||
// run full decompilation to prepare for full code scan
|
||||
mainWindow.requestFullDecompilation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void collectUsageData() {
|
||||
usageList = new ArrayList<>();
|
||||
Map<JavaNode, List<JavaNode>> usageQuery = buildUsageQuery();
|
||||
usageQuery.forEach((searchNode, useNodes) -> useNodes.stream()
|
||||
.map(JavaNode::getTopParentClass)
|
||||
.distinct()
|
||||
.forEach(u -> processUsage(searchNode, u)));
|
||||
buildUsageQuery().forEach(
|
||||
(searchNode, useNodes) -> useNodes.stream()
|
||||
.map(JavaNode::getTopParentClass)
|
||||
.distinct()
|
||||
.forEach(u -> processUsage(searchNode, u)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return mapping of 'node to search' to 'use places'
|
||||
*/
|
||||
private Map<JavaNode, List<JavaNode>> buildUsageQuery() {
|
||||
Map<JavaNode, List<JavaNode>> map = new HashMap<>();
|
||||
private Map<JavaNode, List<? extends JavaNode>> buildUsageQuery() {
|
||||
Map<JavaNode, List<? extends JavaNode>> map = new HashMap<>();
|
||||
if (node instanceof JMethod) {
|
||||
JavaMethod javaMethod = ((JMethod) node).getJavaMethod();
|
||||
for (JavaMethod mth : getMethodWithOverrides(javaMethod)) {
|
||||
map.put(mth, mth.getUseIn());
|
||||
}
|
||||
} else if (node instanceof JClass) {
|
||||
return map;
|
||||
}
|
||||
if (node instanceof JClass) {
|
||||
JavaClass javaCls = ((JClass) node).getCls();
|
||||
map.put(javaCls, javaCls.getUseIn());
|
||||
// add constructors usage into class usage
|
||||
@@ -91,10 +108,19 @@ public class UsageDialog extends CommonSearchDialog {
|
||||
map.put(javaMth, javaMth.getUseIn());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
JavaNode javaNode = node.getJavaNode();
|
||||
map.put(javaNode, javaNode.getUseIn());
|
||||
return map;
|
||||
}
|
||||
if (node instanceof JField && mainWindow.getSettings().isReplaceConsts()) {
|
||||
FieldNode fld = ((JField) node).getJavaField().getFieldNode();
|
||||
boolean constField = ConstStorage.getFieldConstValue(fld) != null;
|
||||
if (constField && !fld.getAccessFlags().isPrivate()) {
|
||||
// search all classes to collect usage of replaced constants
|
||||
map.put(fld.getJavaNode(), mainWindow.getWrapper().getIncludedClasses());
|
||||
return map;
|
||||
}
|
||||
}
|
||||
JavaNode javaNode = node.getJavaNode();
|
||||
map.put(javaNode, javaNode.getUseIn());
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -108,9 +134,12 @@ public class UsageDialog extends CommonSearchDialog {
|
||||
|
||||
private void processUsage(JavaNode searchNode, JavaClass topUseClass) {
|
||||
ICodeInfo codeInfo = topUseClass.getCodeInfo();
|
||||
List<Integer> usePositions = topUseClass.getUsePlacesFor(codeInfo, searchNode);
|
||||
if (usePositions.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
String code = codeInfo.getCodeStr();
|
||||
JadxWrapper wrapper = mainWindow.getWrapper();
|
||||
List<Integer> usePositions = topUseClass.getUsePlacesFor(codeInfo, searchNode);
|
||||
for (int pos : usePositions) {
|
||||
String line = CodeUtils.getLineForPos(code, pos);
|
||||
if (line.startsWith("import ")) {
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package jadx.gui.ui.filedialog;
|
||||
|
||||
import java.awt.Component;
|
||||
import java.awt.HeadlessException;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.swing.JDialog;
|
||||
import javax.swing.JFileChooser;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.UIManager;
|
||||
import javax.swing.filechooser.FileNameExtensionFilter;
|
||||
|
||||
import jadx.api.plugins.utils.CommonFileUtils;
|
||||
import jadx.core.utils.Utils;
|
||||
import jadx.core.utils.files.FileUtils;
|
||||
import jadx.gui.ui.MainWindow;
|
||||
import jadx.gui.utils.NLS;
|
||||
|
||||
class CustomFileChooser extends JFileChooser {
|
||||
|
||||
static {
|
||||
// disable left shortcut panel, can crush in "Win32ShellFolderManager2.getNetwork()" or similar call
|
||||
UIManager.put("FileChooser.noPlacesBar", Boolean.TRUE);
|
||||
}
|
||||
|
||||
private final FileDialogWrapper data;
|
||||
|
||||
public CustomFileChooser(FileDialogWrapper data) {
|
||||
super(data.getCurrentDir() == null ? CommonFileUtils.CWD : data.getCurrentDir().toFile());
|
||||
putClientProperty("FileChooser.useShellFolder", Boolean.FALSE);
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public List<Path> showDialog() {
|
||||
setToolTipText(data.getTitle());
|
||||
setFileSelectionMode(data.getSelectionMode());
|
||||
setMultiSelectionEnabled(data.isOpen());
|
||||
setAcceptAllFileFilterUsed(true);
|
||||
List<String> fileExtList = data.getFileExtList();
|
||||
if (Utils.notEmpty(fileExtList)) {
|
||||
String description = NLS.str("file_dialog.supported_files") + ": (" + Utils.listToString(fileExtList) + ')';
|
||||
setFileFilter(new FileNameExtensionFilter(description, fileExtList.toArray(new String[0])));
|
||||
}
|
||||
if (data.getSelectedFile() != null) {
|
||||
setSelectedFile(data.getSelectedFile().toFile());
|
||||
}
|
||||
MainWindow mainWindow = data.getMainWindow();
|
||||
int ret = data.isOpen() ? showOpenDialog(mainWindow) : showSaveDialog(mainWindow);
|
||||
if (ret != JFileChooser.APPROVE_OPTION) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
data.setCurrentDir(getCurrentDirectory().toPath());
|
||||
File[] selectedFiles = getSelectedFiles();
|
||||
if (selectedFiles.length != 0) {
|
||||
return FileUtils.toPaths(selectedFiles);
|
||||
}
|
||||
File chosenFile = getSelectedFile();
|
||||
if (chosenFile != null) {
|
||||
return Collections.singletonList(chosenFile.toPath());
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JDialog createDialog(Component parent) throws HeadlessException {
|
||||
JDialog dialog = super.createDialog(parent);
|
||||
dialog.setTitle(data.getTitle());
|
||||
dialog.setLocationRelativeTo(null);
|
||||
data.getMainWindow().getSettings().loadWindowPos(dialog);
|
||||
dialog.addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosed(WindowEvent e) {
|
||||
data.getMainWindow().getSettings().saveWindowPos(dialog);
|
||||
super.windowClosed(e);
|
||||
}
|
||||
});
|
||||
return dialog;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void approveSelection() {
|
||||
if (data.getSelectionMode() == FILES_AND_DIRECTORIES) {
|
||||
File currentFile = getSelectedFile();
|
||||
if (currentFile.isDirectory()) {
|
||||
int option = JOptionPane.showConfirmDialog(
|
||||
data.getMainWindow(),
|
||||
NLS.str("file_dialog.load_dir_confirm") + "\n " + currentFile,
|
||||
NLS.str("file_dialog.load_dir_title"),
|
||||
JOptionPane.YES_NO_OPTION);
|
||||
if (option != JOptionPane.YES_OPTION) {
|
||||
this.setCurrentDirectory(currentFile);
|
||||
this.updateUI();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
super.approveSelection();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package jadx.gui.ui.filedialog;
|
||||
|
||||
import java.awt.FileDialog;
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import jadx.core.utils.ListUtils;
|
||||
import jadx.core.utils.Utils;
|
||||
import jadx.core.utils.files.FileUtils;
|
||||
|
||||
class CustomFileDialog {
|
||||
|
||||
private final FileDialogWrapper data;
|
||||
|
||||
public CustomFileDialog(FileDialogWrapper data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public List<Path> showDialog() {
|
||||
FileDialog fileDialog = new FileDialog(data.getMainWindow(), data.getTitle());
|
||||
fileDialog.setMode(data.isOpen() ? FileDialog.LOAD : FileDialog.SAVE);
|
||||
fileDialog.setMultipleMode(true);
|
||||
List<String> fileExtList = data.getFileExtList();
|
||||
if (Utils.notEmpty(fileExtList)) {
|
||||
fileDialog.setFilenameFilter((dir, name) -> ListUtils.anyMatch(fileExtList, name::endsWith));
|
||||
}
|
||||
if (data.getSelectedFile() != null) {
|
||||
fileDialog.setFile(data.getSelectedFile().toAbsolutePath().toString());
|
||||
}
|
||||
if (data.getCurrentDir() != null) {
|
||||
fileDialog.setDirectory(data.getCurrentDir().toAbsolutePath().toString());
|
||||
}
|
||||
fileDialog.setVisible(true);
|
||||
File[] selectedFiles = fileDialog.getFiles();
|
||||
if (!Utils.isEmpty(selectedFiles)) {
|
||||
data.setCurrentDir(Paths.get(fileDialog.getDirectory()));
|
||||
return FileUtils.toPaths(selectedFiles);
|
||||
}
|
||||
if (fileDialog.getFile() != null) {
|
||||
return Collections.singletonList(Paths.get(fileDialog.getFile()));
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package jadx.gui.ui.filedialog;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.swing.JFileChooser;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.gui.settings.JadxProject;
|
||||
import jadx.gui.ui.MainWindow;
|
||||
import jadx.gui.utils.NLS;
|
||||
|
||||
public class FileDialogWrapper {
|
||||
|
||||
private final MainWindow mainWindow;
|
||||
|
||||
private boolean isOpen;
|
||||
private String title;
|
||||
private List<String> fileExtList;
|
||||
private int selectionMode = JFileChooser.FILES_AND_DIRECTORIES;
|
||||
private @Nullable Path currentDir;
|
||||
private @Nullable Path selectedFile;
|
||||
|
||||
public FileDialogWrapper(MainWindow mainWindow, FileOpenMode mode) {
|
||||
this.mainWindow = mainWindow;
|
||||
initForMode(mode);
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public void setFileExtList(List<String> fileExtList) {
|
||||
this.fileExtList = fileExtList;
|
||||
}
|
||||
|
||||
public void setSelectionMode(int selectionMode) {
|
||||
this.selectionMode = selectionMode;
|
||||
}
|
||||
|
||||
public void setSelectedFile(Path path) {
|
||||
this.selectedFile = path;
|
||||
}
|
||||
|
||||
public void setCurrentDir(Path currentDir) {
|
||||
this.currentDir = currentDir;
|
||||
}
|
||||
|
||||
public List<Path> show() {
|
||||
if (mainWindow.getSettings().isUseAlternativeFileDialog()) {
|
||||
return new CustomFileDialog(this).showDialog();
|
||||
} else {
|
||||
return new CustomFileChooser(this).showDialog();
|
||||
}
|
||||
}
|
||||
|
||||
private void initForMode(FileOpenMode mode) {
|
||||
switch (mode) {
|
||||
case OPEN:
|
||||
case OPEN_PROJECT:
|
||||
case ADD:
|
||||
if (mode == FileOpenMode.OPEN_PROJECT) {
|
||||
fileExtList = Collections.singletonList(JadxProject.PROJECT_EXTENSION);
|
||||
title = NLS.str("file.open_title");
|
||||
} else {
|
||||
fileExtList = new ArrayList<>(Arrays.asList("apk", "dex", "jar", "class", "smali", "zip", "xapk", "aar", "arsc"));
|
||||
if (mode == FileOpenMode.OPEN) {
|
||||
fileExtList.addAll(Arrays.asList(JadxProject.PROJECT_EXTENSION, "aab"));
|
||||
title = NLS.str("file.open_title");
|
||||
} else {
|
||||
title = NLS.str("file.add_files_action");
|
||||
}
|
||||
}
|
||||
selectionMode = JFileChooser.FILES_AND_DIRECTORIES;
|
||||
currentDir = mainWindow.getSettings().getLastOpenFilePath();
|
||||
isOpen = true;
|
||||
break;
|
||||
|
||||
case SAVE_PROJECT:
|
||||
title = NLS.str("file.save_project");
|
||||
fileExtList = Collections.singletonList(JadxProject.PROJECT_EXTENSION);
|
||||
selectionMode = JFileChooser.FILES_ONLY;
|
||||
currentDir = mainWindow.getSettings().getLastSaveFilePath();
|
||||
isOpen = false;
|
||||
break;
|
||||
|
||||
case EXPORT:
|
||||
title = NLS.str("file.save_all_msg");
|
||||
fileExtList = Collections.emptyList();
|
||||
selectionMode = JFileChooser.DIRECTORIES_ONLY;
|
||||
currentDir = mainWindow.getSettings().getLastSaveFilePath();
|
||||
isOpen = false;
|
||||
break;
|
||||
|
||||
case CUSTOM_SAVE:
|
||||
isOpen = false;
|
||||
currentDir = mainWindow.getSettings().getLastSaveFilePath();
|
||||
break;
|
||||
|
||||
case CUSTOM_OPEN:
|
||||
isOpen = true;
|
||||
currentDir = mainWindow.getSettings().getLastOpenFilePath();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public Path getCurrentDir() {
|
||||
return currentDir;
|
||||
}
|
||||
|
||||
public MainWindow getMainWindow() {
|
||||
return mainWindow;
|
||||
}
|
||||
|
||||
public boolean isOpen() {
|
||||
return isOpen;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public List<String> getFileExtList() {
|
||||
return fileExtList;
|
||||
}
|
||||
|
||||
public int getSelectionMode() {
|
||||
return selectionMode;
|
||||
}
|
||||
|
||||
public Path getSelectedFile() {
|
||||
return selectedFile;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package jadx.gui.ui.filedialog;
|
||||
|
||||
public enum FileOpenMode {
|
||||
OPEN,
|
||||
OPEN_PROJECT,
|
||||
ADD,
|
||||
SAVE_PROJECT,
|
||||
EXPORT,
|
||||
CUSTOM_SAVE,
|
||||
CUSTOM_OPEN
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user