diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/EnumVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/EnumVisitor.java index 290ee1815..edfe3fd4b 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/EnumVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/EnumVisitor.java @@ -1,10 +1,12 @@ package jadx.core.dex.visitors; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.function.Predicate; import java.util.stream.Collectors; import org.jetbrains.annotations.Nullable; @@ -23,6 +25,7 @@ import jadx.core.dex.info.FieldInfo; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.IndexInsnNode; import jadx.core.dex.instructions.InsnType; +import jadx.core.dex.instructions.InvokeNode; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnWrapArg; @@ -36,12 +39,17 @@ import jadx.core.dex.nodes.DexNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; +import jadx.core.dex.nodes.RootNode; import jadx.core.dex.visitors.shrink.CodeShrinkVisitor; import jadx.core.utils.BlockInsnPair; import jadx.core.utils.InsnRemover; import jadx.core.utils.InsnUtils; import jadx.core.utils.exceptions.JadxException; +import static jadx.core.utils.InsnUtils.checkInsnType; +import static jadx.core.utils.InsnUtils.getSingleArg; +import static jadx.core.utils.InsnUtils.getWrappedInsn; + @JadxVisitor( name = "EnumVisitor", desc = "Restore enum classes", @@ -50,6 +58,18 @@ import jadx.core.utils.exceptions.JadxException; ) public class EnumVisitor extends AbstractVisitor { + private MethodInfo enumValueOfMth; + + @Override + public void init(RootNode root) { + enumValueOfMth = MethodInfo.fromDetails( + root, + ClassInfo.fromType(root, ArgType.ENUM), + "valueOf", + Arrays.asList(ArgType.CLASS, ArgType.STRING), + ArgType.ENUM); + } + @Override public boolean visit(ClassNode cls) throws JadxException { if (!convertToEnum(cls)) { @@ -163,7 +183,7 @@ public class EnumVisitor extends AbstractVisitor { if (classInitMth.countInsns() == 0) { classInitMth.add(AFlag.DONT_GENERATE); } - removeEnumMethods(cls, clsType); + removeEnumMethods(cls, clsType, valuesField); return true; } @@ -232,7 +252,7 @@ public class EnumVisitor extends AbstractVisitor { if (ssaVar.getUseCount() == 1) { return null; } - final InsnNode sputInsn = ssaVar.getUseList().get(0).getParentInsn(); + InsnNode sputInsn = ssaVar.getUseList().get(0).getParentInsn(); if (sputInsn == null || sputInsn.getType() != InsnType.SPUT) { return null; } @@ -283,14 +303,14 @@ public class EnumVisitor extends AbstractVisitor { return null; } - // TODO: detect these methods by analyzing method instructions - private void removeEnumMethods(ClassNode cls, ArgType clsType) { + private void removeEnumMethods(ClassNode cls, ArgType clsType, FieldNode valuesField) { String enumConstructor = "(Ljava/lang/String;I)V"; String enumConstructorAlt = "(Ljava/lang/String;)V"; - String valuesOfMethod = "valueOf(Ljava/lang/String;)" + TypeGen.signature(clsType); String valuesMethod = "values()" + TypeGen.signature(ArgType.array(clsType)); - // remove synthetic methods + FieldInfo valuesFieldInfo = valuesField.getFieldInfo(); + + // remove compiler generated methods for (MethodNode mth : cls.getMethods()) { MethodInfo mi = mth.getMethodInfo(); if (mi.isClassInit()) { @@ -303,12 +323,33 @@ public class EnumVisitor extends AbstractVisitor { || shortId.equals(enumConstructorAlt)) { mth.add(AFlag.DONT_GENERATE); } - } else if (shortId.equals(valuesMethod) || shortId.equals(valuesOfMethod)) { + } else if (shortId.equals(valuesMethod) + || usesValuesField(mth, valuesFieldInfo) + || simpleValueOfMth(mth, clsType)) { mth.add(AFlag.DONT_GENERATE); } } } + private boolean simpleValueOfMth(MethodNode mth, ArgType clsType) { + InsnNode returnInsn = InsnUtils.searchSingleReturnInsn(mth, insn -> insn.getArgsCount() == 1); + if (returnInsn == null) { + return false; + } + InsnNode wrappedInsn = getWrappedInsn(getSingleArg(returnInsn)); + IndexInsnNode castInsn = (IndexInsnNode) checkInsnType(wrappedInsn, InsnType.CHECK_CAST); + if (castInsn != null && Objects.equals(castInsn.getIndex(), clsType)) { + InvokeNode invokeInsn = (InvokeNode) checkInsnType(getWrappedInsn(getSingleArg(castInsn)), InsnType.INVOKE); + return invokeInsn != null && invokeInsn.getCallMth().equals(enumValueOfMth); + } + return false; + } + + private boolean usesValuesField(MethodNode mth, FieldInfo valuesFieldInfo) { + Predicate insnTest = insn -> Objects.equals(((IndexInsnNode) insn).getIndex(), valuesFieldInfo); + return InsnUtils.searchInsn(mth, InsnType.SGET, insnTest) != null; + } + private static void processEnumInnerCls(ConstructorInsn co, EnumField field, ClassNode innerCls) { if (!innerCls.getClassInfo().equals(co.getClassType())) { return; diff --git a/jadx-core/src/main/java/jadx/core/utils/InsnUtils.java b/jadx-core/src/main/java/jadx/core/utils/InsnUtils.java index 6b6afc190..0d9170644 100644 --- a/jadx-core/src/main/java/jadx/core/utils/InsnUtils.java +++ b/jadx-core/src/main/java/jadx/core/utils/InsnUtils.java @@ -1,5 +1,7 @@ package jadx.core.utils; +import java.util.function.Predicate; + import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -15,9 +17,11 @@ import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnWrapArg; import jadx.core.dex.instructions.args.RegisterArg; +import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.DexNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.InsnNode; +import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.parser.FieldInitAttr; import jadx.core.utils.exceptions.JadxRuntimeException; @@ -121,4 +125,72 @@ public class InsnUtils { return null; } } + + @Nullable + public static InsnNode searchSingleReturnInsn(MethodNode mth, Predicate test) { + if (!mth.isNoCode() && mth.getExitBlocks().size() == 1) { + return searchInsn(mth, InsnType.RETURN, test); + } + return null; + } + + /** + * Search instruction of specific type and condition in method. + * This method support inlined instructions. + */ + @Nullable + public static InsnNode searchInsn(MethodNode mth, InsnType insnType, Predicate test) { + if (mth.isNoCode()) { + return null; + } + for (BlockNode block : mth.getBasicBlocks()) { + for (InsnNode insn : block.getInstructions()) { + InsnNode foundInsn = recursiveInsnCheck(insn, insnType, test); + if (foundInsn != null) { + return foundInsn; + } + } + } + return null; + } + + private static InsnNode recursiveInsnCheck(InsnNode insn, InsnType insnType, Predicate test) { + if (insn.getType() == insnType && test.test(insn)) { + return insn; + } + for (InsnArg arg : insn.getArguments()) { + if (arg.isInsnWrap()) { + InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn(); + InsnNode foundInsn = recursiveInsnCheck(wrapInsn, insnType, test); + if (foundInsn != null) { + return foundInsn; + } + } + } + return null; + } + + @Nullable + public static InsnArg getSingleArg(InsnNode insn) { + if (insn != null && insn.getArgsCount() == 1) { + return insn.getArg(0); + } + return null; + } + + @Nullable + public static InsnNode checkInsnType(InsnNode insn, InsnType insnType) { + if (insn != null && insn.getType() == insnType) { + return insn; + } + return null; + } + + @Nullable + public static InsnNode getWrappedInsn(InsnArg arg) { + if (arg != null && arg.isInsnWrap()) { + return ((InsnWrapArg) arg).getWrapInsn(); + } + return null; + } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/enums/TestEnumObfuscated.java b/jadx-core/src/test/java/jadx/tests/integration/enums/TestEnumObfuscated.java new file mode 100644 index 000000000..f4e789e42 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/enums/TestEnumObfuscated.java @@ -0,0 +1,49 @@ +package jadx.tests.integration.enums; + +import org.junit.jupiter.api.Test; + +import jadx.core.dex.nodes.ClassNode; +import jadx.tests.api.SmaliTest; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +public class TestEnumObfuscated extends SmaliTest { + // @formatter:off + /* + public enum TestEnumObfuscated { + private static final synthetic TestEnumObfuscated[] $VLS = {ONE, TWO}; + public static final TestEnumObfuscated ONE = new TestEnumObfuscated("ONE", 0, 1); + public static final TestEnumObfuscated TWO = new TestEnumObfuscated("TWO", 1, 2); + private final int num; + + private TestEnumObfuscated(String str, int i, int i2) { + super(str, i); + this.num = i2; + } + + public static TestEnumObfuscated vo(String str) { + return (TestEnumObfuscated) Enum.valueOf(TestEnumObfuscated.class, str); + } + + public static TestEnumObfuscated[] vs() { + return (TestEnumObfuscated[]) $VLS.clone(); + } + + public synthetic int getNum() { + return this.num; + } + } + */ + // @formatter:on + + @Test + public void test() { + ClassNode cls = getClassNodeFromSmali(); + assertThat(cls) + .code() + .doesNotContain("$VLS") + .doesNotContain("vo(") + .doesNotContain("vs(") + .containsOne("int getNum() {"); + } +} diff --git a/jadx-core/src/test/smali/enums/TestEnumObfuscated.smali b/jadx-core/src/test/smali/enums/TestEnumObfuscated.smali new file mode 100644 index 000000000..99e91e69c --- /dev/null +++ b/jadx-core/src/test/smali/enums/TestEnumObfuscated.smali @@ -0,0 +1,81 @@ +###### Class jadx.tests.integration.enums.TestEnums3$TestCls.Numbers (jadx.tests.integration.enums.TestEnums3$TestCls$Numbers) +.class public final enum Lenums/TestEnumObfuscated; +.super Ljava/lang/Enum; + +# static fields +.field private static final synthetic $VLS:[Lenums/TestEnumObfuscated; +.field public static final enum ONE:Lenums/TestEnumObfuscated; +.field public static final enum TWO:Lenums/TestEnumObfuscated; + +# instance fields +.field private final num:I + +# direct methods +.method static constructor ()V + .registers 7 + + .prologue + const/4 v6, 0x3 + const/4 v5, 0x0 + const/4 v4, 0x2 + const/4 v3, 0x1 + + new-instance v0, Lenums/TestEnumObfuscated; + const-string v1, "ONE" + invoke-direct {v0, v1, v5, v3}, Lenums/TestEnumObfuscated;->(Ljava/lang/String;II)V + sput-object v0, Lenums/TestEnumObfuscated;->ONE:Lenums/TestEnumObfuscated; + new-instance v0, Lenums/TestEnumObfuscated; + + const-string v1, "TWO" + invoke-direct {v0, v1, v3, v4}, Lenums/TestEnumObfuscated;->(Ljava/lang/String;II)V + sput-object v0, Lenums/TestEnumObfuscated;->TWO:Lenums/TestEnumObfuscated; + const/4 v0, 0x2 + + new-array v0, v0, [Lenums/TestEnumObfuscated; + sget-object v1, Lenums/TestEnumObfuscated;->ONE:Lenums/TestEnumObfuscated; + aput-object v1, v0, v5 + sget-object v1, Lenums/TestEnumObfuscated;->TWO:Lenums/TestEnumObfuscated; + aput-object v1, v0, v3 + sput-object v0, Lenums/TestEnumObfuscated;->$VLS:[Lenums/TestEnumObfuscated; + + return-void +.end method + +.method private constructor (Ljava/lang/String;II)V + .registers 4 + + .prologue + invoke-direct {p0, p1, p2}, Ljava/lang/Enum;->(Ljava/lang/String;I)V + iput p3, p0, Lenums/TestEnumObfuscated;->num:I + return-void +.end method + +.method public static vo(Ljava/lang/String;)Lenums/TestEnumObfuscated; + .registers 2 + + .prologue + const-class v0, Lenums/TestEnumObfuscated; + invoke-static {v0, p0}, Ljava/lang/Enum;->valueOf(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum; + move-result-object v0 + check-cast v0, Lenums/TestEnumObfuscated; + return-object v0 +.end method + +.method public static vs()[Lenums/TestEnumObfuscated; + .registers 1 + + .prologue + sget-object v0, Lenums/TestEnumObfuscated;->$VLS:[Lenums/TestEnumObfuscated; + invoke-virtual {v0}, [Lenums/TestEnumObfuscated;->clone()Ljava/lang/Object; + move-result-object v0 + check-cast v0, [Lenums/TestEnumObfuscated; + return-object v0 +.end method + +.method public synthetic getNum()I + .registers 2 + + .prologue + iget v0, p0, Lenums/TestEnumObfuscated;->num:I + return v0 +.end method