Compare commits
23 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 |
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ 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;
|
||||
@@ -210,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);
|
||||
@@ -221,7 +246,6 @@ public class InsnGen {
|
||||
}
|
||||
code.add('.');
|
||||
}
|
||||
FieldNode fieldNode = clsGen.getClassNode().root().resolveField(field);
|
||||
if (fieldNode != null) {
|
||||
code.attachAnnotation(fieldNode);
|
||||
}
|
||||
@@ -232,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);
|
||||
}
|
||||
@@ -695,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()) {
|
||||
@@ -748,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);
|
||||
@@ -786,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 {
|
||||
@@ -993,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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -13,8 +13,6 @@ 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;
|
||||
@@ -56,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;
|
||||
|
||||
@@ -174,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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,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) {
|
||||
|
||||
@@ -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,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,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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,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);
|
||||
|
||||
@@ -95,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;
|
||||
@@ -173,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");
|
||||
@@ -608,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() {
|
||||
@@ -1041,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) {
|
||||
@@ -1152,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);
|
||||
@@ -1231,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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ")) {
|
||||
|
||||
@@ -88,6 +88,7 @@ public class JDebuggerPanel extends JPanel {
|
||||
private transient KeyEventDispatcher controllerShortCutDispatcher;
|
||||
|
||||
public JDebuggerPanel(MainWindow mainWindow) {
|
||||
UiUtils.uiThreadGuard();
|
||||
this.mainWindow = mainWindow;
|
||||
controller = new DebugController();
|
||||
this.setLayout(new BorderLayout());
|
||||
@@ -287,24 +288,26 @@ public class JDebuggerPanel extends JPanel {
|
||||
|
||||
@Override
|
||||
public void onStateChanged(boolean suspended, boolean stopped) {
|
||||
if (!stopped) {
|
||||
if (isGray) {
|
||||
stop.putValue(Action.SMALL_ICON, ICON_STOP);
|
||||
UiUtils.uiRun(() -> {
|
||||
if (!stopped) {
|
||||
if (isGray) {
|
||||
stop.putValue(Action.SMALL_ICON, ICON_STOP);
|
||||
}
|
||||
} else {
|
||||
stop.putValue(Action.SMALL_ICON, ICON_STOP_GRAY);
|
||||
run.putValue(Action.SMALL_ICON, ICON_RUN);
|
||||
run.putValue(Action.SHORT_DESCRIPTION, NLS.str("debugger.run"));
|
||||
isGray = true;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
stop.putValue(Action.SMALL_ICON, ICON_STOP_GRAY);
|
||||
run.putValue(Action.SMALL_ICON, ICON_RUN);
|
||||
run.putValue(Action.SHORT_DESCRIPTION, NLS.str("debugger.run"));
|
||||
isGray = true;
|
||||
return;
|
||||
}
|
||||
if (suspended) {
|
||||
run.putValue(Action.SMALL_ICON, ICON_RUN);
|
||||
run.putValue(Action.SHORT_DESCRIPTION, NLS.str("debugger.run"));
|
||||
} else {
|
||||
run.putValue(Action.SMALL_ICON, ICON_PAUSE);
|
||||
run.putValue(Action.SHORT_DESCRIPTION, NLS.str("debugger.pause"));
|
||||
}
|
||||
if (suspended) {
|
||||
run.putValue(Action.SMALL_ICON, ICON_RUN);
|
||||
run.putValue(Action.SHORT_DESCRIPTION, NLS.str("debugger.run"));
|
||||
} else {
|
||||
run.putValue(Action.SMALL_ICON, ICON_PAUSE);
|
||||
run.putValue(Action.SHORT_DESCRIPTION, NLS.str("debugger.pause"));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -387,16 +390,18 @@ public class JDebuggerPanel extends JPanel {
|
||||
public boolean showDebugger(String procName, String host, int port, int androidVer, ADBDevice device, String pid) {
|
||||
boolean ok = controller.startDebugger(this, host, port, androidVer);
|
||||
if (ok) {
|
||||
log(String.format("Attached %s %s:%d", procName, host, port));
|
||||
try {
|
||||
logcatPanel.init(device, pid);
|
||||
} catch (Exception e) {
|
||||
log(NLS.str("logcat.error_fail_start"));
|
||||
LOG.error("Logcat failed to start", e);
|
||||
}
|
||||
leftSplitter.setDividerLocation(mainWindow.getSettings().getDebuggerStackFrameSplitterLoc());
|
||||
rightSplitter.setDividerLocation(mainWindow.getSettings().getDebuggerVarTreeSplitterLoc());
|
||||
mainWindow.showDebuggerPanel();
|
||||
UiUtils.uiRun(() -> {
|
||||
log(String.format("Attached %s %s:%d", procName, host, port));
|
||||
try {
|
||||
logcatPanel.init(device, pid);
|
||||
} catch (Exception e) {
|
||||
log(NLS.str("logcat.error_fail_start"));
|
||||
LOG.error("Logcat failed to start", e);
|
||||
}
|
||||
leftSplitter.setDividerLocation(mainWindow.getSettings().getDebuggerStackFrameSplitterLoc());
|
||||
rightSplitter.setDividerLocation(mainWindow.getSettings().getDebuggerVarTreeSplitterLoc());
|
||||
mainWindow.showDebuggerPanel();
|
||||
});
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
@@ -414,6 +419,8 @@ public class JDebuggerPanel extends JPanel {
|
||||
}
|
||||
|
||||
public void loadSettings() {
|
||||
UiUtils.uiThreadGuard();
|
||||
|
||||
Font font = mainWindow.getSettings().getFont();
|
||||
variableTree.setFont(font.deriveFont(font.getSize() + 1.f));
|
||||
variableTree.setRowHeight(-1);
|
||||
@@ -423,6 +430,8 @@ public class JDebuggerPanel extends JPanel {
|
||||
}
|
||||
|
||||
public void resetUI() {
|
||||
UiUtils.uiThreadGuard();
|
||||
|
||||
thisTreeNode.removeAllChildren();
|
||||
regTreeNode.removeAllChildren();
|
||||
|
||||
@@ -464,12 +473,11 @@ public class JDebuggerPanel extends JPanel {
|
||||
}
|
||||
|
||||
public void refreshThreadBox(List<? extends IListElement> elements) {
|
||||
if (elements.size() > 0) {
|
||||
DefaultComboBoxModel<IListElement> model =
|
||||
(DefaultComboBoxModel<IListElement>) threadBox.getModel();
|
||||
elements.forEach(model::addElement);
|
||||
}
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
UiUtils.uiRun(() -> {
|
||||
if (!elements.isEmpty()) {
|
||||
DefaultComboBoxModel<IListElement> model = (DefaultComboBoxModel<IListElement>) threadBox.getModel();
|
||||
elements.forEach(model::addElement);
|
||||
}
|
||||
threadBox.updateUI();
|
||||
stackFrameList.setFont(mainWindow.getSettings().getFont());
|
||||
});
|
||||
|
||||
@@ -18,6 +18,8 @@ public class CacheObject {
|
||||
|
||||
private List<List<JavaClass>> decompileBatches;
|
||||
|
||||
private volatile boolean fullDecompilationFinished;
|
||||
|
||||
public CacheObject() {
|
||||
reset();
|
||||
}
|
||||
@@ -27,6 +29,7 @@ public class CacheObject {
|
||||
jNodeCache = new JNodeCache();
|
||||
lastSearchOptions = new HashMap<>();
|
||||
decompileBatches = null;
|
||||
fullDecompilationFinished = false;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -53,4 +56,12 @@ public class CacheObject {
|
||||
public void setDecompileBatches(List<List<JavaClass>> decompileBatches) {
|
||||
this.decompileBatches = decompileBatches;
|
||||
}
|
||||
|
||||
public boolean isFullDecompilationFinished() {
|
||||
return fullDecompilationFinished;
|
||||
}
|
||||
|
||||
public void setFullDecompilationFinished(boolean fullDecompilationFinished) {
|
||||
this.fullDecompilationFinished = fullDecompilationFinished;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ menu.text_search=Textsuche
|
||||
menu.class_search=Klassen-Suche
|
||||
menu.comment_search=Kommentar suchen
|
||||
menu.tools=Tools
|
||||
#menu.decompile_all=Decompile all classes
|
||||
menu.deobfuscation=Deobfuskierung
|
||||
menu.log=Log-Anzeige
|
||||
menu.help=Hilfe
|
||||
@@ -111,6 +112,9 @@ search_dialog.load_all=Alle laden
|
||||
#search_dialog.stop=Stop
|
||||
search_dialog.results_incomplete=%d+ gefunden
|
||||
search_dialog.results_complete=%d gefunden (komplett)
|
||||
#search_dialog.resources_load_errors=Load errors: %d
|
||||
#search_dialog.resources_skip_by_size=Skipped by size: %d
|
||||
#search_dialog.resources_check_logs=(click to check logs)
|
||||
search_dialog.col_node=Knoten
|
||||
search_dialog.col_code=Code
|
||||
#search_dialog.sort_results=Sort results
|
||||
@@ -157,6 +161,7 @@ preferences.useImports=Import statements generieren
|
||||
preferences.useDebugInfo=Debug-Infos verwenden
|
||||
preferences.inlineAnonymous=Anonyme Inline-Klassen
|
||||
preferences.inlineMethods=Inline-Methoden
|
||||
#preferences.inlineKotlinLambdas=Allow to inline Kotlin Lambdas
|
||||
#preferences.extractFinally=Extract finally block
|
||||
preferences.fsCaseSensitive=Dateisystem unterscheidet zwischen Groß/Kleinschreibung
|
||||
preferences.skipResourcesDecode=Keine Ressourcen dekodieren
|
||||
|
||||
@@ -14,6 +14,7 @@ menu.text_search=Text search
|
||||
menu.class_search=Class search
|
||||
menu.comment_search=Comment searchF
|
||||
menu.tools=Tools
|
||||
menu.decompile_all=Decompile all classes
|
||||
menu.deobfuscation=Deobfuscation
|
||||
menu.log=Log Viewer
|
||||
menu.help=Help
|
||||
@@ -111,6 +112,9 @@ search_dialog.load_all=Load all
|
||||
search_dialog.stop=Stop
|
||||
search_dialog.results_incomplete=Found %d+
|
||||
search_dialog.results_complete=Found %d (complete)
|
||||
search_dialog.resources_load_errors=Load errors: %d
|
||||
search_dialog.resources_skip_by_size=Skipped by size: %d
|
||||
search_dialog.resources_check_logs=(click to check logs)
|
||||
search_dialog.col_node=Node
|
||||
search_dialog.col_code=Code
|
||||
search_dialog.sort_results=Sort results
|
||||
@@ -157,6 +161,7 @@ preferences.useImports=Use import statements
|
||||
preferences.useDebugInfo=Use debug info
|
||||
preferences.inlineAnonymous=Inline anonymous classes
|
||||
preferences.inlineMethods=Inline methods
|
||||
preferences.inlineKotlinLambdas=Allow to inline Kotlin Lambdas
|
||||
preferences.extractFinally=Extract finally block
|
||||
preferences.fsCaseSensitive=File system is case-sensitive
|
||||
preferences.skipResourcesDecode=Don't decode resources
|
||||
@@ -198,7 +203,7 @@ preferences.rename_printable=To make printable
|
||||
preferences.search_group_title=Search
|
||||
preferences.search_results_per_page=Results per page (0 - no limit)
|
||||
preferences.res_file_ext=Resource files extensions ('xml|html', * for all)
|
||||
preferences.res_skip_file=Skip resources files if larger (MB)
|
||||
preferences.res_skip_file=Skip resources files if larger (MB) (0 - disable)
|
||||
|
||||
msg.open_file=Please open file
|
||||
msg.saving_sources=Saving sources
|
||||
|
||||
@@ -14,6 +14,7 @@ menu.text_search=Buscar texto
|
||||
menu.class_search=Buscar clase
|
||||
#menu.comment_search=Comment search
|
||||
menu.tools=Herramientas
|
||||
#menu.decompile_all=Decompile all classes
|
||||
menu.deobfuscation=Desofuscación
|
||||
menu.log=Visor log
|
||||
menu.help=Ayuda
|
||||
@@ -111,6 +112,9 @@ search_dialog.ignorecase=Ignorar minúsculas/mayúsculas
|
||||
#search_dialog.stop=Stop
|
||||
#search_dialog.results_incomplete=Found %d+
|
||||
#search_dialog.results_complete=Found %d (complete)
|
||||
#search_dialog.resources_load_errors=Load errors: %d
|
||||
#search_dialog.resources_skip_by_size=Skipped by size: %d
|
||||
#search_dialog.resources_check_logs=(click to check logs)
|
||||
search_dialog.col_node=Nodo
|
||||
search_dialog.col_code=Código
|
||||
#search_dialog.sort_results=Sort results
|
||||
@@ -157,6 +161,7 @@ preferences.replaceConsts=Reemplazar constantes
|
||||
#preferences.useDebugInfo=Use debug info
|
||||
#preferences.inlineAnonymous=
|
||||
#preferences.inlineMethods=Inline methods
|
||||
#preferences.inlineKotlinLambdas=Allow to inline Kotlin Lambdas
|
||||
#preferences.extractFinally=Extract finally block
|
||||
#preferences.fsCaseSensitive=
|
||||
preferences.skipResourcesDecode=No descodificar recursos
|
||||
|
||||
@@ -14,6 +14,7 @@ menu.text_search=텍스트 검색
|
||||
menu.class_search=클래스 검색
|
||||
menu.comment_search=주석 검색
|
||||
menu.tools=도구
|
||||
#menu.decompile_all=Decompile all classes
|
||||
menu.deobfuscation=난독화 해제
|
||||
menu.log=로그 뷰어
|
||||
menu.help=도움말
|
||||
@@ -111,6 +112,9 @@ search_dialog.load_all=모두 로드
|
||||
search_dialog.stop=정지
|
||||
search_dialog.results_incomplete=%d+개 찾음
|
||||
search_dialog.results_complete=%d개 찾음 (검색 완료)
|
||||
#search_dialog.resources_load_errors=Load errors: %d
|
||||
#search_dialog.resources_skip_by_size=Skipped by size: %d
|
||||
#search_dialog.resources_check_logs=(click to check logs)
|
||||
search_dialog.col_node=노드
|
||||
search_dialog.col_code=코드
|
||||
search_dialog.sort_results=결과 정렬
|
||||
@@ -157,6 +161,7 @@ preferences.useImports=import 문 사용
|
||||
preferences.useDebugInfo=디버그 정보 사용
|
||||
preferences.inlineAnonymous=인라인 익명 클래스
|
||||
preferences.inlineMethods=인라인 메서드
|
||||
#preferences.inlineKotlinLambdas=Allow to inline Kotlin Lambdas
|
||||
preferences.extractFinally=finally 블록 추출
|
||||
preferences.fsCaseSensitive=파일 시스템 대소문자 구별
|
||||
preferences.skipResourcesDecode=리소스 디코딩 하지 않기
|
||||
|
||||
@@ -14,6 +14,7 @@ menu.text_search=Buscar por texto
|
||||
menu.class_search=Buscar por classe
|
||||
menu.comment_search=Busca por comentário
|
||||
menu.tools=Ferramentas
|
||||
#menu.decompile_all=Decompile all classes
|
||||
menu.deobfuscation=Desofuscar
|
||||
menu.log=Visualizador de log
|
||||
menu.help=Ajuda
|
||||
@@ -111,6 +112,9 @@ search_dialog.load_all=Carregar todas
|
||||
search_dialog.stop=Parar
|
||||
search_dialog.results_incomplete=Encontradas %d+
|
||||
search_dialog.results_complete=Encontradas %d (completos)
|
||||
#search_dialog.resources_load_errors=Load errors: %d
|
||||
#search_dialog.resources_skip_by_size=Skipped by size: %d
|
||||
#search_dialog.resources_check_logs=(click to check logs)
|
||||
search_dialog.col_node=Nó
|
||||
search_dialog.col_code=Código
|
||||
search_dialog.sort_results=Ordenar resultados
|
||||
@@ -157,6 +161,7 @@ preferences.useImports=Utilizar declaração de imports
|
||||
preferences.useDebugInfo=Utilizar informação de depuração
|
||||
preferences.inlineAnonymous=Classes anônimas de uma linha
|
||||
preferences.inlineMethods=Métodos de uma linha
|
||||
#preferences.inlineKotlinLambdas=Allow to inline Kotlin Lambdas
|
||||
preferences.extractFinally=Extrair blocos finally
|
||||
preferences.fsCaseSensitive=Sistema de arquivo diferencia maiúsculas de minúsculas
|
||||
preferences.skipResourcesDecode=Não decodificar recursos
|
||||
|
||||
@@ -14,6 +14,7 @@ menu.text_search=Поиск строк
|
||||
menu.class_search=Поиск классов
|
||||
menu.comment_search=Поиск комментариев
|
||||
menu.tools=Инструменты
|
||||
#menu.decompile_all=Decompile all classes
|
||||
menu.deobfuscation=Деобфускация
|
||||
menu.log=Просмотр логов
|
||||
menu.help=Помощь
|
||||
@@ -111,6 +112,9 @@ search_dialog.load_all=Загрузить все
|
||||
search_dialog.stop=Стоп
|
||||
search_dialog.results_incomplete=Найдено %d+
|
||||
search_dialog.results_complete=Найдено %d (поиск завершен)
|
||||
#search_dialog.resources_load_errors=Load errors: %d
|
||||
#search_dialog.resources_skip_by_size=Skipped by size: %d
|
||||
#search_dialog.resources_check_logs=(click to check logs)
|
||||
search_dialog.col_node=Вхождения
|
||||
search_dialog.col_code=Код
|
||||
search_dialog.sort_results=Сортировка результатов
|
||||
@@ -157,6 +161,7 @@ preferences.useImports=Использовать импорты
|
||||
preferences.useDebugInfo=Отладочная информация
|
||||
preferences.inlineAnonymous=Объединять анонимные классы
|
||||
preferences.inlineMethods=Объединять методы
|
||||
#preferences.inlineKotlinLambdas=Allow to inline Kotlin Lambdas
|
||||
preferences.extractFinally=Вычленять finally блоки
|
||||
preferences.fsCaseSensitive=Учитывать регистр в файловой системе
|
||||
preferences.skipResourcesDecode=Не декодировать ресурсы
|
||||
|
||||
@@ -14,23 +14,24 @@ menu.text_search=文本搜索
|
||||
menu.class_search=类名搜索
|
||||
menu.comment_search=注释搜索
|
||||
menu.tools=工具
|
||||
menu.decompile_all=反编译所有类
|
||||
menu.deobfuscation=反混淆
|
||||
menu.log=日志查看器
|
||||
menu.help=帮助
|
||||
menu.about=关于
|
||||
menu.update_label=发现新版本 %s!
|
||||
|
||||
file.open_action=打开文件...
|
||||
file.open_action=打开文件…
|
||||
file.add_files_action=添加文件
|
||||
file.open_title=打开文件
|
||||
file.open_project=打开文件
|
||||
file.new_project=新建项目
|
||||
file.save_project=保存项目
|
||||
file.save_project_as=另存项目为...
|
||||
file.save_project_as=另存项目为…
|
||||
file.reload=重新加载文件
|
||||
file.live_reload=实时重加载
|
||||
file.live_reload_desc=文件变动时自动重载
|
||||
#file.export_mappings_as=
|
||||
file.export_mappings_as=导出映射为…
|
||||
file.save_all=全部保存
|
||||
file.export_gradle=另存为 Gradle 项目
|
||||
file.save_all_msg=请选择保存反编译资源的目录
|
||||
@@ -42,10 +43,10 @@ start_page.recent=最近项目
|
||||
|
||||
tree.sources_title=源代码
|
||||
tree.resources_title=资源文件
|
||||
tree.loading=加载中...
|
||||
tree.loading=加载中…
|
||||
|
||||
progress.load=正在加载
|
||||
#progress.export_mappings=
|
||||
progress.export_mappings=导出映射
|
||||
progress.decompile=反编译中
|
||||
progress.canceling=正在取消
|
||||
|
||||
@@ -111,6 +112,9 @@ search_dialog.load_all=加载所有
|
||||
search_dialog.stop=停止
|
||||
search_dialog.results_incomplete=已找到 %d+
|
||||
search_dialog.results_complete=全部找到 %d
|
||||
search_dialog.resources_load_errors=加载错误:%d
|
||||
search_dialog.resources_skip_by_size=按大小跳过:%d
|
||||
search_dialog.resources_check_logs=(点击查看日志)
|
||||
search_dialog.col_node=节点
|
||||
search_dialog.col_code=代码
|
||||
search_dialog.sort_results=结果分类
|
||||
@@ -119,7 +123,7 @@ search_dialog.active_tab=只在当前页搜索
|
||||
search_dialog.comments=注释
|
||||
search_dialog.resource=资源
|
||||
search_dialog.keep_open=保持窗口
|
||||
search_dialog.tip_searching=搜索中...
|
||||
search_dialog.tip_searching=搜索中…
|
||||
|
||||
usage_dialog.title=查找
|
||||
usage_dialog.label=查找用例:
|
||||
@@ -144,7 +148,7 @@ preferences.other=其他
|
||||
preferences.language=语言
|
||||
preferences.lineNumbersMode=编辑器行号模式
|
||||
preferences.jumpOnDoubleClick=启用双击跳转
|
||||
#preferences.useAlternativeFileDialog=Use alternative file dialog
|
||||
preferences.useAlternativeFileDialog=使用选择文件对话框
|
||||
preferences.check_for_updates=启动时检查更新
|
||||
preferences.useDx=使用 dx/d8 来转换java字节码
|
||||
preferences.decompilationMode=反编译模式
|
||||
@@ -157,6 +161,7 @@ preferences.useImports=使用 import 语句
|
||||
preferences.useDebugInfo=启用调试信息
|
||||
preferences.inlineAnonymous=内联匿名类
|
||||
preferences.inlineMethods=内联方法
|
||||
preferences.inlineKotlinLambdas=允许内联Kotlin Lambda
|
||||
preferences.extractFinally=提取finally块
|
||||
preferences.fsCaseSensitive=文件系统区分大小写
|
||||
preferences.skipResourcesDecode=不反编译资源文件
|
||||
@@ -168,7 +173,7 @@ preferences.excludedPackages=排除的包
|
||||
preferences.excludedPackages.tooltip=排除于反编译或索引的以空格分隔的包名列表(节省 RAM)
|
||||
preferences.excludedPackages.button=编辑
|
||||
preferences.excludedPackages.editDialog=<html>排除于反编译或索引的以空格分隔的包名列表(节省 RAM)<br>例如<code>android.support</code></html>
|
||||
preferences.cfg=生成方法的 CFG 图('.dot' 格式)
|
||||
preferences.cfg=生成方法的 CFG 图('.dot')
|
||||
preferences.raw_cfg=生成原始的 CFG 图
|
||||
preferences.font=编辑器字体
|
||||
preferences.smali_font=Smali编辑器字体
|
||||
@@ -192,13 +197,13 @@ preferences.reset_title=重置设置
|
||||
preferences.copy=复制到剪切板
|
||||
preferences.copy_message=所有设置都已复制
|
||||
preferences.rename=重命名标识符
|
||||
preferences.rename_case=需要标识符能区分大小写
|
||||
preferences.rename_valid=需要标识符能符合规范
|
||||
preferences.rename_printable=需要标识符可正常显示
|
||||
preferences.rename_case=标识符要能够区分大小写
|
||||
preferences.rename_valid=标识符应该符合标准规范
|
||||
preferences.rename_printable=标识符必须要能正常显示
|
||||
preferences.search_group_title=搜索资源
|
||||
preferences.search_results_per_page=每页结果数(0 - 无限制)
|
||||
preferences.res_file_ext=文件扩展名 (e.g. .xml|.html),* 表示所有
|
||||
preferences.res_skip_file=跳过文件大小(MB)
|
||||
preferences.res_file_ext=文件扩展名(e.g. .xml|.html),* 表示所有
|
||||
preferences.res_skip_file=跳过文件大小(MB)
|
||||
|
||||
msg.open_file=请打开文件
|
||||
msg.saving_sources=正在导出源代码
|
||||
@@ -274,10 +279,10 @@ issues_panel.warnings=%d 警告
|
||||
issues_panel.tooltip=点击查看日志
|
||||
|
||||
debugger.process_selector=选择要调试的进程
|
||||
debugger.step_into=步入 (F7)
|
||||
debugger.step_over=步过 (F8)
|
||||
debugger.step_out=步出 (Shift + F8)
|
||||
debugger.run=运行 (F9)
|
||||
debugger.step_into=步入(F7)
|
||||
debugger.step_over=步过(F8)
|
||||
debugger.step_out=步出(Shift + F8)
|
||||
debugger.run=运行(F9)
|
||||
debugger.stop=停止调试并终止应用
|
||||
debugger.pause=暂停
|
||||
debugger.rerun=重新运行
|
||||
@@ -324,8 +329,8 @@ adb_dialog.refresh=刷新
|
||||
adb_dialog.tip_devices=%d 台设备
|
||||
adb_dialog.device_node=设备
|
||||
adb_dialog.missing_path=必须提供ADB路径才能启动ADB服务。
|
||||
adb_dialog.waiting=正在等待连接到ADB服务...
|
||||
adb_dialog.connecting=正在连接ADB服务,地址: %s:%s...
|
||||
adb_dialog.waiting=正在等待连接到ADB服务…
|
||||
adb_dialog.connecting=正在连接ADB服务,地址: %s:%s…
|
||||
adb_dialog.connect_okay=已连接ADB服务,地址: %s:%s
|
||||
adb_dialog.connect_fail=连接ADB服务失败。
|
||||
adb_dialog.disconnected=ADB服务已断开连接。
|
||||
@@ -340,4 +345,4 @@ adb_dialog.msg_read_mani_fail=解码AndroidManifest.xml失败
|
||||
adb_dialog.no_devices=找不到任何设备用来启动APP。
|
||||
adb_dialog.restart_while_debugging_title=调试时重新启动
|
||||
adb_dialog.restart_while_debugging_msg=你正在调试一个APP,确定要重新启动一个会话吗?
|
||||
adb_dialog.starting_debugger=正在启动调试器...
|
||||
adb_dialog.starting_debugger=正在启动调试器…
|
||||
|
||||
@@ -14,6 +14,7 @@ menu.text_search=文字搜尋
|
||||
menu.class_search=類別搜尋
|
||||
menu.comment_search=註解搜尋
|
||||
menu.tools=工具
|
||||
#menu.decompile_all=Decompile all classes
|
||||
menu.deobfuscation=去模糊化
|
||||
menu.log=日誌檢視器
|
||||
menu.help=幫助
|
||||
@@ -111,6 +112,9 @@ search_dialog.load_all=載入全部
|
||||
search_dialog.stop=停止
|
||||
search_dialog.results_incomplete=找到 %d+
|
||||
search_dialog.results_complete=找到 %d (完整)
|
||||
#search_dialog.resources_load_errors=Load errors: %d
|
||||
#search_dialog.resources_skip_by_size=Skipped by size: %d
|
||||
#search_dialog.resources_check_logs=(click to check logs)
|
||||
search_dialog.col_node=無
|
||||
search_dialog.col_code=程式碼
|
||||
search_dialog.sort_results=排序結果
|
||||
@@ -157,6 +161,7 @@ preferences.useImports=使用 import 陳述式
|
||||
preferences.useDebugInfo=使用除錯資訊
|
||||
preferences.inlineAnonymous=內嵌匿名類別
|
||||
preferences.inlineMethods=內嵌方式
|
||||
#preferences.inlineKotlinLambdas=Allow to inline Kotlin Lambdas
|
||||
preferences.extractFinally=擷取 finally 區塊
|
||||
preferences.fsCaseSensitive=檔案系統區分大小寫
|
||||
preferences.skipResourcesDecode=不要為資源解碼
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<!-- Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<polygon fill="#59A869" points="2 2 10 8 2 14"/>
|
||||
<path fill="#59A869" d="M7,11.75 L12,8 L7,4.25 L7,2 L15,8 L7,14 L7,11.75 Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 436 B |
@@ -7,4 +7,6 @@ dependencies {
|
||||
|
||||
// show bytecode disassemble
|
||||
implementation 'io.github.skylot:raung-disasm:0.0.3'
|
||||
|
||||
testImplementation(project(":jadx-core"))
|
||||
}
|
||||
|
||||
+10
-2
@@ -19,8 +19,8 @@ import org.slf4j.LoggerFactory;
|
||||
import jadx.api.plugins.utils.CommonFileUtils;
|
||||
import jadx.api.plugins.utils.ZipSecurity;
|
||||
|
||||
public class JavaFileLoader {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(JavaFileLoader.class);
|
||||
public class JavaInputLoader {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(JavaInputLoader.class);
|
||||
|
||||
private static final int MAX_MAGIC_SIZE = 4;
|
||||
private static final byte[] JAVA_CLASS_FILE_MAGIC = { (byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE };
|
||||
@@ -37,6 +37,14 @@ public class JavaFileLoader {
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public List<JavaClassReader> loadInputStream(InputStream in, String name) throws IOException {
|
||||
return loadReader(in, name, null, null);
|
||||
}
|
||||
|
||||
public JavaClassReader loadClass(byte[] content, String fileName) {
|
||||
return new JavaClassReader(getNextUniqId(), fileName, content);
|
||||
}
|
||||
|
||||
private List<JavaClassReader> loadFromFile(File file) {
|
||||
try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) {
|
||||
return loadReader(inputStream, file.getName(), file, null);
|
||||
+46
-1
@@ -1,8 +1,11 @@
|
||||
package jadx.plugins.input.java;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -10,6 +13,7 @@ import jadx.api.plugins.JadxPluginInfo;
|
||||
import jadx.api.plugins.input.JadxInputPlugin;
|
||||
import jadx.api.plugins.input.data.ILoadResult;
|
||||
import jadx.api.plugins.input.data.impl.EmptyLoadResult;
|
||||
import jadx.plugins.input.java.utils.JavaClassParseException;
|
||||
|
||||
public class JavaInputPlugin implements JadxInputPlugin {
|
||||
|
||||
@@ -29,10 +33,51 @@ public class JavaInputPlugin implements JadxInputPlugin {
|
||||
}
|
||||
|
||||
public static ILoadResult loadClassFiles(List<Path> inputFiles, @Nullable Closeable closeable) {
|
||||
List<JavaClassReader> readers = new JavaFileLoader().collectFiles(inputFiles);
|
||||
List<JavaClassReader> readers = new JavaInputLoader().collectFiles(inputFiles);
|
||||
if (readers.isEmpty()) {
|
||||
return EmptyLoadResult.INSTANCE;
|
||||
}
|
||||
return new JavaLoadResult(readers, closeable);
|
||||
}
|
||||
|
||||
public static ILoadResult loadClassFiles(List<Path> inputFiles) {
|
||||
return loadClassFiles(inputFiles, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for provide several inputs by using load methods from {@link JavaInputLoader} class.
|
||||
*/
|
||||
public static ILoadResult load(Function<JavaInputLoader, List<JavaClassReader>> loader) {
|
||||
return wrapClassReaders(loader.apply(new JavaInputLoader()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient method for load class file or jar from input stream.
|
||||
* Should be used only once per JadxDecompiler instance.
|
||||
* For load several times use {@link JavaInputPlugin#load(Function)} method.
|
||||
*/
|
||||
public static ILoadResult loadFromInputStream(InputStream in, String fileName) {
|
||||
try {
|
||||
return wrapClassReaders(new JavaInputLoader().loadInputStream(in, fileName));
|
||||
} catch (Exception e) {
|
||||
throw new JavaClassParseException("Failed to read input stream", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient method for load single class file by content.
|
||||
* Should be used only once per JadxDecompiler instance.
|
||||
* For load several times use {@link JavaInputPlugin#load(Function)} method.
|
||||
*/
|
||||
public static ILoadResult loadSingleClass(byte[] content, String fileName) {
|
||||
JavaClassReader reader = new JavaInputLoader().loadClass(content, fileName);
|
||||
return new JavaLoadResult(Collections.singletonList(reader));
|
||||
}
|
||||
|
||||
public static ILoadResult wrapClassReaders(List<JavaClassReader> readers) {
|
||||
if (readers.isEmpty()) {
|
||||
return EmptyLoadResult.INSTANCE;
|
||||
}
|
||||
return new JavaLoadResult(readers);
|
||||
}
|
||||
}
|
||||
|
||||
+4
-1
@@ -20,6 +20,10 @@ public class JavaLoadResult implements ILoadResult {
|
||||
@Nullable
|
||||
private final Closeable closeable;
|
||||
|
||||
public JavaLoadResult(List<JavaClassReader> readers) {
|
||||
this(readers, null);
|
||||
}
|
||||
|
||||
public JavaLoadResult(List<JavaClassReader> readers, @Nullable Closeable closeable) {
|
||||
this.readers = readers;
|
||||
this.closeable = closeable;
|
||||
@@ -47,7 +51,6 @@ public class JavaLoadResult implements ILoadResult {
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
readers.clear();
|
||||
if (closeable != null) {
|
||||
closeable.close();
|
||||
}
|
||||
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
package jadx.plugins.input.java;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import jadx.api.JadxArgs;
|
||||
import jadx.api.JadxDecompiler;
|
||||
import jadx.api.plugins.input.data.ILoadResult;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
class CustomLoadTest {
|
||||
|
||||
private JadxDecompiler jadx;
|
||||
|
||||
@BeforeEach
|
||||
void init() {
|
||||
jadx = new JadxDecompiler(new JadxArgs());
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void close() {
|
||||
jadx.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadFiles() {
|
||||
List<Path> files = Stream.of("HelloWorld.class", "HelloWorld$HelloInner.class")
|
||||
.map(this::getSample)
|
||||
.collect(Collectors.toList());
|
||||
ILoadResult loadResult = JavaInputPlugin.loadClassFiles(files);
|
||||
loadDecompiler(loadResult);
|
||||
assertThat(jadx.getClassesWithInners())
|
||||
.hasSize(2)
|
||||
.satisfiesOnlyOnce(cls -> assertThat(cls.getName()).isEqualTo("HelloWorld"))
|
||||
.satisfiesOnlyOnce(cls -> assertThat(cls.getName()).isEqualTo("HelloInner"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadFromInputStream() throws IOException {
|
||||
String fileName = "HelloWorld$HelloInner.class";
|
||||
try (InputStream in = Files.newInputStream(getSample(fileName))) {
|
||||
ILoadResult loadResult = JavaInputPlugin.loadFromInputStream(in, fileName);
|
||||
loadDecompiler(loadResult);
|
||||
assertThat(jadx.getClassesWithInners())
|
||||
.hasSize(1)
|
||||
.satisfiesOnlyOnce(cls -> assertThat(cls.getName()).isEqualTo("HelloWorld$HelloInner"));
|
||||
|
||||
System.out.println(jadx.getClassesWithInners().get(0).getCode());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadSingleClass() throws IOException {
|
||||
String fileName = "HelloWorld.class";
|
||||
byte[] content = Files.readAllBytes(getSample(fileName));
|
||||
ILoadResult loadResult = JavaInputPlugin.loadSingleClass(content, fileName);
|
||||
loadDecompiler(loadResult);
|
||||
assertThat(jadx.getClassesWithInners())
|
||||
.hasSize(1)
|
||||
.satisfiesOnlyOnce(cls -> assertThat(cls.getName()).isEqualTo("HelloWorld"));
|
||||
|
||||
System.out.println(jadx.getClassesWithInners().get(0).getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void load() {
|
||||
ILoadResult loadResult = JavaInputPlugin.load(loader -> {
|
||||
List<JavaClassReader> inputs = new ArrayList<>(2);
|
||||
try {
|
||||
String hello = "HelloWorld.class";
|
||||
byte[] content = Files.readAllBytes(getSample(hello));
|
||||
inputs.add(loader.loadClass(content, hello));
|
||||
|
||||
String helloInner = "HelloWorld$HelloInner.class";
|
||||
InputStream in = Files.newInputStream(getSample(helloInner));
|
||||
inputs.addAll(loader.loadInputStream(in, helloInner));
|
||||
} catch (Exception e) {
|
||||
fail(e);
|
||||
}
|
||||
return inputs;
|
||||
});
|
||||
loadDecompiler(loadResult);
|
||||
assertThat(jadx.getClassesWithInners())
|
||||
.hasSize(2)
|
||||
.satisfiesOnlyOnce(cls -> assertThat(cls.getName()).isEqualTo("HelloWorld"))
|
||||
.satisfiesOnlyOnce(cls -> {
|
||||
assertThat(cls.getName()).isEqualTo("HelloInner");
|
||||
assertThat(cls.getCode()).isEqualTo(""); // no code for moved inner class
|
||||
});
|
||||
|
||||
assertThat(jadx.getClasses())
|
||||
.hasSize(1)
|
||||
.satisfiesOnlyOnce(cls -> assertThat(cls.getName()).isEqualTo("HelloWorld"))
|
||||
.satisfiesOnlyOnce(cls -> assertThat(cls.getInnerClasses()).hasSize(1)
|
||||
.satisfiesOnlyOnce(inner -> assertThat(inner.getName()).isEqualTo("HelloInner")));
|
||||
|
||||
jadx.getClassesWithInners().forEach(cls -> System.out.println(cls.getCode()));
|
||||
}
|
||||
|
||||
public void loadDecompiler(ILoadResult load) {
|
||||
try {
|
||||
jadx.addCustomLoad(load);
|
||||
jadx.load();
|
||||
} catch (Exception e) {
|
||||
fail(e);
|
||||
}
|
||||
}
|
||||
|
||||
public Path getSample(String name) {
|
||||
try {
|
||||
return Paths.get(ClassLoader.getSystemResource("samples/" + name).toURI());
|
||||
} catch (Exception e) {
|
||||
return fail(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user