Compare commits

..

23 Commits

Author SHA1 Message Date
Skylot fdf170529f fix: use strict patterns for synthetic methods inline (#1829) 2023-04-19 17:52:37 +01:00
Skylot 50283ab543 fix: additional checks to forbid inline of null consts (#1828) 2023-04-19 15:49:06 +01:00
Skylot 3fa3e5acec fix: correct args shift for instance invoke-custom (#1816) 2023-04-16 20:10:57 +01:00
Skylot 4230cd5b5a feat(plugins): allow to load classes using input stream or byte array in jadx-input plugin (#1457) 2023-04-10 21:28:53 +01:00
nitram84 1ad6527de5 fix(xml): use parent attibute only for styles (PR #1815) 2023-04-10 18:34:17 +01:00
nitram84 0421ad80c1 fix: filter invalid chars in app name for gradle export (PR #1813) 2023-04-08 18:16:14 +01:00
nitram84 35e0201f06 fix(gradle): fix gradle build with version 7.4.2 - 7.6 2023-04-08 19:49:15 +03:00
nitram84 118eea5e77 fix(res): set empty parent for styles without a parent, remove duplicated code 2023-04-08 19:49:15 +03:00
nitram84 7f317be325 fix(res): resolve declare-styleable atrributes 2023-04-08 19:49:15 +03:00
nitram84 e1aa9f6de4 fix(res): resolve custom attributes 2023-04-08 19:49:15 +03:00
nitram84 058a5e3bb2 fix(res): resolve int hex attributes 2023-04-08 19:49:15 +03:00
JustFor 92b49ec2b5 fix(gui): update Messages_zh_CN.properties (PR #1811)
sync new text, and Some symbols are translated
2023-04-07 19:20:15 +01:00
Skylot 583a04b092 fix(gui): show skipped resources count during search (#1808) 2023-03-24 22:00:07 +00:00
Skylot 444a04e2f7 fix(gui): redirect jump from search for inlined classes 2023-03-24 15:34:46 +00:00
Skylot 157e702ffd feat: inline lambdas by instance field (#1800) 2023-03-24 15:34:45 +00:00
Jan S 77892f41ec fix(res): parsing of sparse RES_TABLE_TYPE_TYPE and RES_TABLE_TYPE_STAGED_ALIAS chunks (#1806 #1803)(PR #1807) 2023-03-23 17:30:29 +00:00
Jan S 6ba0e1dbf6 fix(res): handle RES_TABLE_TYPE_OVERLAY (#1748) (PR #1804) 2023-03-18 14:23:49 +00:00
Skylot 950fbbaa83 fix: restore missing type parameter declarations (#1800) 2023-03-17 20:28:42 +00:00
Skylot 912c431511 fix(debugger): process UI updates in correct thread (#1796) 2023-03-17 14:23:36 +00:00
Skylot 5d6b82724a fix(gui): search constant fields usage in all classes (#1801) 2023-03-16 17:41:23 +00:00
Jan S 78c976ad4f fix(res): resolve manifest decoding error Expected strings start (#1797)(PR #1798) 2023-03-10 18:46:31 +03:00
Skylot fbdfd135da fix(cli): use common enum args parser (#1787) 2023-02-27 19:35:49 +00:00
Jacob Davis-Hansson dd51783d9e fix(cli): make enum CLI arguments match documented format (PR #1787)
Currently if you do `jadx --help`, it says the `--deobf-cfg-file-mode` option accepts the value `read-or-save`. 

However, if you give it that option, it instead prints the following error message:

```
java.lang.IllegalArgumentException: 'read-or-save' is unknown, possible values are: read, read-or-save, overwrite, ignore
	at jadx.cli.JadxCLIArgs$DeobfuscationMapFileModeConverter.convert(JadxCLIArgs.java:524)
	at jadx.cli.JadxCLIArgs$DeobfuscationMapFileModeConverter.convert(JadxCLIArgs.java:516)
	at com.beust.jcommander.JCommander.convertValue(JCommander.java:1340)
	at com.beust.jcommander.ParameterDescription.addValue(ParameterDescription.java:249)
	at com.beust.jcommander.JCommander.processFixedArity(JCommander.java:920)
	at com.beust.jcommander.JCommander.processFixedArity(JCommander.java:901)
	at com.beust.jcommander.JCommander.parseValues(JCommander.java:731)
	at com.beust.jcommander.JCommander.parse(JCommander.java:363)
	at com.beust.jcommander.JCommander.parse(JCommander.java:342)
	at jadx.cli.JCommanderWrapper.parse(JCommanderWrapper.java:37)
	at jadx.cli.JadxCLIArgs.processArgs(JadxCLIArgs.java:211)
	at jadx.cli.JadxCLI.execute(JadxCLI.java:35)
	at jadx.cli.JadxCLI.main(JadxCLI.java:20)
```

This commit changes all the enum parsers to do the inverse string of `enumValuesString`, so the documented behavior works.
2023-02-27 19:08:07 +00:00
68 changed files with 1420 additions and 317 deletions
@@ -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()) {
@@ -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=
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"))
}
@@ -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);
@@ -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);
}
}
@@ -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();
}
@@ -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);
}
}
}