From 165ae2472269e89bc5d5ec9fecafa064a2393084 Mon Sep 17 00:00:00 2001 From: Skylot <118523+skylot@users.noreply.github.com> Date: Mon, 16 Mar 2026 22:25:26 +0000 Subject: [PATCH] fix: support enum restore for constructor without args (#2821) --- .../main/java/jadx/core/codegen/ClassGen.java | 20 +++-- .../java/jadx/core/codegen/MethodGen.java | 52 ++++------- .../java/jadx/core/dex/attributes/AFlag.java | 1 + .../attributes/nodes/CodeFeaturesAttr.java | 5 ++ .../dex/attributes/nodes/EnumClassAttr.java | 10 ++- .../attributes/nodes/SkipMethodArgsAttr.java | 4 + .../jadx/core/dex/visitors/ClassModifier.java | 6 +- .../core/dex/visitors/ConstructorVisitor.java | 2 +- .../jadx/core/dex/visitors/EnumVisitor.java | 88 ++++++++++++++----- .../tests/integration/enums/TestEnums11.java | 29 ++++++ .../src/test/raung/enums/TestEnums11.raung | 74 ++++++++++++++++ 11 files changed, 228 insertions(+), 63 deletions(-) create mode 100644 jadx-core/src/test/java/jadx/tests/integration/enums/TestEnums11.java create mode 100644 jadx-core/src/test/raung/enums/TestEnums11.raung diff --git a/jadx-core/src/main/java/jadx/core/codegen/ClassGen.java b/jadx-core/src/main/java/jadx/core/codegen/ClassGen.java index 11bde57ab..5ac0cf7f4 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/ClassGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/ClassGen.java @@ -34,6 +34,7 @@ import jadx.core.dex.attributes.nodes.EnumClassAttr; import jadx.core.dex.attributes.nodes.EnumClassAttr.EnumField; import jadx.core.dex.attributes.nodes.LineAttrNode; import jadx.core.dex.attributes.nodes.MethodInlineAttr; +import jadx.core.dex.attributes.nodes.NotificationAttrNode; import jadx.core.dex.attributes.nodes.SkipMethodArgsAttr; import jadx.core.dex.info.AccessInfo; import jadx.core.dex.info.ClassInfo; @@ -164,12 +165,7 @@ public class ClassGen { if (af.isInterface()) { af = af.remove(AccessFlags.ABSTRACT) .remove(AccessFlags.STATIC); - } else if (af.isEnum()) { - af = af.remove(AccessFlags.FINAL) - .remove(AccessFlags.ABSTRACT) - .remove(AccessFlags.STATIC); } - // 'static' and 'private' modifier not allowed for top classes (not inner) if (!cls.getClassInfo().isInner()) { af = af.remove(AccessFlags.STATIC).remove(AccessFlags.PRIVATE); @@ -294,7 +290,7 @@ public class ClassGen { private void addInnerClsAndMethods(ICodeWriter clsCode) { Stream.of(cls.getInnerClasses(), cls.getMethods()) .flatMap(Collection::stream) - .filter(node -> !node.contains(AFlag.DONT_GENERATE) || fallback) + .filter(node -> !skipNode(node)) .sorted(Comparator.comparingInt(LineAttrNode::getSourceLine)) .forEach(node -> { if (node instanceof ClassNode) { @@ -305,6 +301,18 @@ public class ClassGen { }); } + private boolean skipNode(NotificationAttrNode node) { + if (fallback) { + return false; + } + if (Consts.DEBUG_ATTRIBUTES) { + if (node.contains(AType.JADX_COMMENTS)) { + return false; + } + } + return node.contains(AFlag.DONT_GENERATE); + } + private void addInnerClass(ICodeWriter code, ClassNode innerCls) { try { ClassGen inClGen = new ClassGen(innerCls, getParentGen()); diff --git a/jadx-core/src/main/java/jadx/core/codegen/MethodGen.java b/jadx-core/src/main/java/jadx/core/codegen/MethodGen.java index d4f3d71f5..5e7027960 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/MethodGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/MethodGen.java @@ -1,7 +1,5 @@ package jadx.core.codegen; -import java.util.Collections; -import java.util.Iterator; import java.util.List; import org.jetbrains.annotations.Nullable; @@ -29,6 +27,7 @@ import jadx.core.dex.attributes.nodes.JadxError; import jadx.core.dex.attributes.nodes.JumpInfo; import jadx.core.dex.attributes.nodes.MethodOverrideAttr; import jadx.core.dex.attributes.nodes.MethodReplaceAttr; +import jadx.core.dex.attributes.nodes.SkipMethodArgsAttr; import jadx.core.dex.info.AccessInfo; import jadx.core.dex.instructions.ConstStringNode; import jadx.core.dex.instructions.IfNode; @@ -109,10 +108,6 @@ public class MethodGen { if (clsAccFlags.isAnnotation()) { ai = ai.remove(AccessFlags.PUBLIC); } - if (mth.getMethodInfo().isConstructor() && mth.getParentClass().isEnum()) { - ai = ai.remove(AccessInfo.VISIBILITY_FLAGS); - } - if (mth.getMethodInfo().hasAlias() && !ai.isConstructor()) { CodeGenUtils.addRenamedComment(code, mth, mth.getName()); } @@ -152,21 +147,7 @@ public class MethodGen { code.add(defMth.getAlias()); } code.add('('); - - List args = mth.getArgRegs(); - if (mth.getMethodInfo().isConstructor() - && mth.getParentClass().contains(AType.ENUM_CLASS)) { - if (args.size() == 2) { - args = Collections.emptyList(); - } else if (args.size() > 2) { - args = args.subList(2, args.size()); - } else { - mth.addWarnComment("Incorrect number of args for enum constructor: " + args.size() + " (expected >= 2)"); - } - } else if (mth.contains(AFlag.SKIP_FIRST_ARG)) { - args = args.subList(1, args.size()); - } - addMethodArguments(code, args); + addMethodArguments(code); code.add(')'); annotationGen.addThrows(mth, code); @@ -209,12 +190,22 @@ public class MethodGen { } } - private void addMethodArguments(ICodeWriter code, List args) { + private void addMethodArguments(ICodeWriter code) { + List args = mth.getArgRegs(); AnnotationMethodParamsAttr paramsAnnotation = mth.get(JadxAttrType.ANNOTATION_MTH_PARAMETERS); - int i = 0; - Iterator it = args.iterator(); - while (it.hasNext()) { - RegisterArg mthArg = it.next(); + int argNum = -1; + int lastArgNum = args.size() - 1; + boolean first = true; + for (RegisterArg mthArg : args) { + argNum++; + if (SkipMethodArgsAttr.isSkip(mth, argNum)) { + continue; + } + if (first) { + first = false; + } else { + code.add(", "); + } SSAVar ssaVar = mthArg.getSVar(); CodeVar var; if (ssaVar == null) { @@ -226,7 +217,7 @@ public class MethodGen { // add argument annotation if (paramsAnnotation != null) { - annotationGen.addForParameter(code, paramsAnnotation, i); + annotationGen.addForParameter(code, paramsAnnotation, argNum); } if (var.isFinal()) { code.add("final "); @@ -239,7 +230,7 @@ public class MethodGen { } else { argType = varType; } - if (!it.hasNext() && mth.getAccessFlags().isVarArgs()) { + if (argNum == lastArgNum && mth.getAccessFlags().isVarArgs()) { // change last array argument to varargs if (argType.isArray()) { ArgType elType = argType.getArrayElement(); @@ -258,11 +249,6 @@ public class MethodGen { code.attachDefinition(VarNode.get(mth, var)); } code.add(varName); - - i++; - if (it.hasNext()) { - code.add(", "); - } } } diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/AFlag.java b/jadx-core/src/main/java/jadx/core/dex/attributes/AFlag.java index afb15e133..e06aceedf 100644 --- a/jadx-core/src/main/java/jadx/core/dex/attributes/AFlag.java +++ b/jadx-core/src/main/java/jadx/core/dex/attributes/AFlag.java @@ -25,6 +25,7 @@ public enum AFlag { REMOVE_SUPER_CLASS, // don't add super class HIDDEN, // instruction used inside other instruction but not listed in args + CONVERTED_ENUM, // enum class successfully restored to original form DONT_RENAME, // do not rename during deobfuscation FORCE_RAW_NAME, // force use of raw name instead alias diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/CodeFeaturesAttr.java b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/CodeFeaturesAttr.java index cc25bd087..17b8ea341 100644 --- a/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/CodeFeaturesAttr.java +++ b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/CodeFeaturesAttr.java @@ -53,4 +53,9 @@ public class CodeFeaturesAttr implements IJadxAttribute { public String toAttrString() { return "CodeFeatures{" + codeFeatures + '}'; } + + @Override + public String toString() { + return toAttrString(); + } } diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/EnumClassAttr.java b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/EnumClassAttr.java index 1ed478124..eb8b4d027 100644 --- a/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/EnumClassAttr.java +++ b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/EnumClassAttr.java @@ -2,6 +2,8 @@ package jadx.core.dex.attributes.nodes; import java.util.List; +import org.jetbrains.annotations.Nullable; + import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.core.dex.attributes.AType; import jadx.core.dex.instructions.mods.ConstructorInsn; @@ -14,11 +16,13 @@ public class EnumClassAttr implements IJadxAttribute { public static class EnumField { private final FieldNode field; private final ConstructorInsn constrInsn; + private final @Nullable String nameStr; private ClassNode cls; - public EnumField(FieldNode field, ConstructorInsn co) { + public EnumField(FieldNode field, ConstructorInsn co, @Nullable String nameStr) { this.field = field; this.constrInsn = co; + this.nameStr = nameStr; } public FieldNode getField() { @@ -37,6 +41,10 @@ public class EnumClassAttr implements IJadxAttribute { this.cls = cls; } + public @Nullable String getNameStr() { + return nameStr; + } + @Override public String toString() { return field + "(" + constrInsn + ") " + cls; diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/SkipMethodArgsAttr.java b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/SkipMethodArgsAttr.java index c7c743ba8..663bbaf8a 100644 --- a/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/SkipMethodArgsAttr.java +++ b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/SkipMethodArgsAttr.java @@ -5,6 +5,7 @@ import java.util.BitSet; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.attributes.PinnedAttribute; +import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.MethodNode; @@ -34,6 +35,9 @@ public class SkipMethodArgsAttr extends PinnedAttribute { if (mth == null) { return false; } + if (argNum == 0 && mth.contains(AFlag.SKIP_FIRST_ARG)) { + return true; + } SkipMethodArgsAttr attr = mth.get(AType.SKIP_MTH_ARGS); if (attr == null) { return false; diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/ClassModifier.java b/jadx-core/src/main/java/jadx/core/dex/visitors/ClassModifier.java index 493e24678..b5c905821 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/ClassModifier.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/ClassModifier.java @@ -44,7 +44,8 @@ import jadx.core.utils.exceptions.JadxException; runAfter = { ModVisitor.class, FixAccessModifiers.class, - ProcessAnonymous.class + ProcessAnonymous.class, + ExtractFieldInit.class } ) public class ClassModifier extends AbstractVisitor { @@ -326,8 +327,9 @@ public class ClassModifier extends AbstractVisitor { } AccessInfo af = mth.getAccessFlags(); boolean publicConstructor = mth.isConstructor() && af.isPublic(); + boolean enumDefConstructor = mth.isConstructor() && mth.getParentClass().contains(AFlag.CONVERTED_ENUM); boolean clsInit = mth.getMethodInfo().isClassInit() && af.isStatic(); - if (publicConstructor || clsInit) { + if (publicConstructor || enumDefConstructor || clsInit) { if (!BlockUtils.isAllBlocksEmpty(mth.getBasicBlocks())) { return; } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/ConstructorVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/ConstructorVisitor.java index 22d8ef591..cede8df53 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/ConstructorVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/ConstructorVisitor.java @@ -164,7 +164,7 @@ public class ConstructorVisitor extends AbstractVisitor { private static boolean canRemoveConstructor(MethodNode mth, ConstructorInsn co) { ClassNode parentClass = mth.getParentClass(); - if (co.isSuper() && (co.getArgsCount() == 0 || parentClass.isEnum())) { + if (co.isSuper() && co.getArgsCount() == 0) { return true; } if (co.isThis() && co.getArgsCount() == 0) { 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 955601829..d5ea09db2 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 @@ -73,6 +73,7 @@ import static jadx.core.utils.InsnUtils.getWrappedInsn; } ) public class EnumVisitor extends AbstractVisitor { + private static final String ENUM_SUPER_CONSTRUCTOR_ID = "java.lang.Enum.(Ljava/lang/String;I)V"; private MethodInfo enumValueOfMth; private MethodInfo cloneMth; @@ -171,11 +172,8 @@ public class EnumVisitor extends AbstractVisitor { cls.addAttr(attr); for (EnumField enumField : attr.getFields()) { - ConstructorInsn co = enumField.getConstrInsn(); FieldNode fieldNode = enumField.getField(); - - // use string arg from the constructor as enum field name - String name = getConstString(cls.root(), co.getArg(0)); + String name = enumField.getNameStr(); if (name != null && !fieldNode.getAlias().equals(name) && NameMapper.isValidAndPrintable(name) @@ -193,9 +191,24 @@ public class EnumVisitor extends AbstractVisitor { CodeShrinkVisitor.shrinkMethod(classInitMth); } removeEnumMethods(cls, data.valuesField); + fixAccessFlags(cls); + cls.add(AFlag.CONVERTED_ENUM); return true; } + private static void fixAccessFlags(ClassNode cls) { + // remove invalid access flags + cls.setAccessFlags(cls.getAccessFlags() + .remove(AccessFlags.FINAL) + .remove(AccessFlags.ABSTRACT) + .remove(AccessFlags.STATIC)); + for (MethodNode mth : cls.getMethods()) { + if (mth.getMethodInfo().isConstructor()) { + mth.setAccessFlags(mth.getAccessFlags().remove(AccessInfo.VISIBILITY_FLAGS)); + } + } + } + /** * Search "$VALUES" field (holds all enum values) */ @@ -433,13 +446,9 @@ public class EnumVisitor extends AbstractVisitor { return enumFieldNode; } - @SuppressWarnings("StatementWithEmptyBody") private EnumField createEnumFieldByConstructor(EnumData data, FieldNode enumFieldNode, ConstructorInsn co) { - // usually constructor signature is '(Ljava/lang/String;I)V'. - // sometimes for one field enum second arg can be omitted - if (co.getArgsCount() < 1) { - return null; - } + // usually constructor signature is '(Ljava/lang/String;I)V', sometimes one or both args can + // be omitted ClassNode cls = data.cls; ClassInfo clsInfo = co.getClassType(); ClassNode constrCls = cls.root().resolveClass(clsInfo); @@ -457,17 +466,45 @@ public class EnumVisitor extends AbstractVisitor { if (ctrMth == null) { return null; } - List regs = new ArrayList<>(); - co.getRegisterArgs(regs); - if (!regs.isEmpty()) { - ConstructorInsn replacedCo = inlineExternalRegs(data, co); - if (replacedCo == null) { - throw new JadxRuntimeException("Init of enum field '" + enumFieldNode.getName() + "' uses external variables"); + // usually constructor signature is '(Ljava/lang/String;I)V' + // sometimes one or both args can be inlined or omitted + String nameStr = null; + if (co.getArgsCount() == 0) { + ConstructorInsn ctrInsn = searchEnumSuperCtrInsn(ctrMth); + if (ctrInsn != null && ctrInsn.getArgsCount() != 0) { + nameStr = getConstString(ctrMth.root(), ctrInsn.getArg(0)); + } + } else { + nameStr = getConstString(cls.root(), co.getArg(0)); + // verify and try to inline additional constructor args + List regs = new ArrayList<>(); + co.getRegisterArgs(regs); + if (!regs.isEmpty()) { + ConstructorInsn replacedCo = inlineExternalRegs(data, co); + if (replacedCo == null) { + throw new JadxRuntimeException("Init of enum field '" + enumFieldNode.getName() + "' uses external variables"); + } + data.toRemove.add(co); + co = replacedCo; } - data.toRemove.add(co); - co = replacedCo; } - return new EnumField(enumFieldNode, co); + return new EnumField(enumFieldNode, co, nameStr); + } + + private @Nullable ConstructorInsn searchEnumSuperCtrInsn(MethodNode ctrMth) { + for (BlockNode block : ctrMth.getBasicBlocks()) { + for (InsnNode insn : block.getInstructions()) { + if (insn.getType() == InsnType.CONSTRUCTOR) { + ConstructorInsn ctrCall = (ConstructorInsn) insn; + if (ctrCall.isSuper() + && ctrCall.getArgsCount() != 0 + && ctrCall.getCallMth().getRawFullId().equals(ENUM_SUPER_CONSTRUCTOR_ID)) { + return ctrCall; + } + } + } + } + return null; } private ConstructorInsn inlineExternalRegs(EnumData data, ConstructorInsn co) { @@ -574,10 +611,16 @@ public class EnumVisitor extends AbstractVisitor { } String shortId = mi.getShortId(); if (mi.isConstructor()) { + markArgsForSkip(mth); + // remove super constructor call + ConstructorInsn superCtrInsn = searchEnumSuperCtrInsn(mth); + if (superCtrInsn != null) { + superCtrInsn.add(AFlag.DONT_GENERATE); + InsnRemover.remove(mth, superCtrInsn); + } if (isDefaultConstructor(mth, shortId)) { mth.add(AFlag.DONT_GENERATE); } - markArgsForSkip(mth); } else if (mi.getShortId().equals(valuesMethodShortId)) { if (isValuesMethod(mth, clsType)) { valuesMethod = mth; @@ -757,4 +800,9 @@ public class EnumVisitor extends AbstractVisitor { this.staticBlocks = staticBlocks; } } + + @Override + public String getName() { + return "EnumVisitor"; + } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/enums/TestEnums11.java b/jadx-core/src/test/java/jadx/tests/integration/enums/TestEnums11.java new file mode 100644 index 000000000..4f60c53f0 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/enums/TestEnums11.java @@ -0,0 +1,29 @@ +package jadx.tests.integration.enums; + +import org.junit.jupiter.api.Test; + +import jadx.tests.api.RaungTest; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +public class TestEnums11 extends RaungTest { + + @Test + public void test() { + assertThat(getClassNodeFromRaung()) + .code() + .containsLines("public enum TestEnums11 {", indent(1) + "UNKNOWN;") + .containsOne("public final int a = -99;") + .doesNotContain("TestEnums11() {"); + } + + @Test + public void testDisableEnumRestore() { + // constructor method incorrectly removed + getArgs().getDisabledPasses().add("EnumVisitor"); + disableCompilation(); + assertThat(getClassNodeFromRaung()) + .code() + .containsOne("public TestEnums11() {"); + } +} diff --git a/jadx-core/src/test/raung/enums/TestEnums11.raung b/jadx-core/src/test/raung/enums/TestEnums11.raung new file mode 100644 index 000000000..ba91f6363 --- /dev/null +++ b/jadx-core/src/test/raung/enums/TestEnums11.raung @@ -0,0 +1,74 @@ +.version 52 # Java 8 +.class public final enum enums/TestEnums11 +.super java/lang/Enum +.signature Ljava/lang/Enum; +.source "SourceFile" + +.field public static final enum A Lenums/TestEnums11; +.field public static final synthetic b [Lenums/TestEnums11; +.field public final a I + +.method public static values()[Lenums/TestEnums11; + .max stack 1 + .max locals 1 + + getstatic enums/TestEnums11 b [Lenums/TestEnums11; + invokevirtual [Lenums/TestEnums11; clone ()Ljava/lang/Object; + checkcast [Lenums/TestEnums11; + areturn +.end method + +.method public static valueOf(Ljava/lang/String;)Lenums/TestEnums11; + .max stack 2 + .max locals 1 + + ldc Lenums/TestEnums11; + aload 0 + invokestatic java/lang/Enum valueOf (Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum; + checkcast enums/TestEnums11 + areturn +.end method + +.method public ()V + .signature (I)V + .max stack 4 + .max locals 1 + + aload 0 + dup + ldc "UNKNOWN" + iconst_0 + invokespecial java/lang/Enum (Ljava/lang/String;I)V + bipush -99 + putfield enums/TestEnums11 a I + return +.end method + +.method public static ()V + .max stack 4 + .max locals 1 + + new enums/TestEnums11 + dup + dup + astore 0 + invokespecial enums/TestEnums11 ()V + putstatic enums/TestEnums11 A Lenums/TestEnums11; + iconst_1 + anewarray enums/TestEnums11 + dup + iconst_0 + aload 0 + aastore + putstatic enums/TestEnums11 b [Lenums/TestEnums11; + return +.end method + +.method public getErrorCode()I + .max stack 1 + .max locals 1 + + aload 0 + getfield enums/TestEnums11 a I + ireturn +.end method