diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/InsnDecoder.java b/jadx-core/src/main/java/jadx/core/dex/instructions/InsnDecoder.java index 2bd1c5668..af978a78d 100644 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/InsnDecoder.java +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/InsnDecoder.java @@ -437,7 +437,9 @@ public class InsnDecoder { case INVOKE_VIRTUAL: return invoke(insn, InvokeType.VIRTUAL, false); case INVOKE_CUSTOM: - return invoke(insn, InvokeType.CUSTOM, false); + return invokeCustom(insn, false); + case INVOKE_SPECIAL: + return invokeSpecial(insn); case INVOKE_DIRECT_RANGE: return invoke(insn, InvokeType.DIRECT, true); @@ -448,7 +450,7 @@ public class InsnDecoder { case INVOKE_VIRTUAL_RANGE: return invoke(insn, InvokeType.VIRTUAL, true); case INVOKE_CUSTOM_RANGE: - return invoke(insn, InvokeType.CUSTOM, true); + return invokeCustom(insn, true); case NEW_INSTANCE: ArgType clsType = ArgType.parse(insn.getIndexAsType()); @@ -565,10 +567,27 @@ public class InsnDecoder { return inode; } - private InsnNode invoke(InsnData insn, InvokeType type, boolean isRange) { - if (type == InvokeType.CUSTOM) { - return InvokeCustomBuilder.build(method, insn, isRange); + private InsnNode invokeCustom(InsnData insn, boolean isRange) { + return InvokeCustomBuilder.build(method, insn, isRange); + } + + private InsnNode invokeSpecial(InsnData insn) { + IMethodRef mthRef = InsnDataUtils.getMethodRef(insn); + if (mthRef == null) { + throw new JadxRuntimeException("Failed to load method reference for insn: " + insn); } + MethodInfo mthInfo = MethodInfo.fromRef(root, mthRef); + // convert 'special' to 'direct/super' same as dx + InvokeType type; + if (mthInfo.isConstructor() || Objects.equals(mthInfo.getDeclClass(), method.getParentClass().getClassInfo())) { + type = InvokeType.DIRECT; + } else { + type = InvokeType.SUPER; + } + return new InvokeNode(mthInfo, insn, type, false); + } + + private InsnNode invoke(InsnData insn, InvokeType type, boolean isRange) { IMethodRef mthRef = InsnDataUtils.getMethodRef(insn); if (mthRef == null) { throw new JadxRuntimeException("Failed to load method reference for insn: " + insn); diff --git a/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java b/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java index de1b581a6..5d2a160c2 100644 --- a/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java +++ b/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java @@ -20,6 +20,7 @@ import java.util.concurrent.TimeoutException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeEach; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -74,7 +75,7 @@ public abstract class IntegrationTest extends TestUtils { /** * Set 'TEST_INPUT_PLUGIN' env variable to use 'java' or 'dx' input in tests */ - private static final boolean USE_JAVA_INPUT = Utils.getOrElse(System.getenv("TEST_INPUT_PLUGIN"), DEFAULT_INPUT_PLUGIN).equals("java"); + static final boolean USE_JAVA_INPUT = Utils.getOrElse(System.getenv("TEST_INPUT_PLUGIN"), DEFAULT_INPUT_PLUGIN).equals("java"); /** * Run auto check method if defined: @@ -523,11 +524,12 @@ public abstract class IntegrationTest extends TestUtils { printOffsets = true; } - protected void useJavaInput() { + public void useJavaInput() { this.useJavaInput = true; } - protected void useDexInput() { + public void useDexInput() { + Assumptions.assumeFalse(USE_JAVA_INPUT, "skip dex input tests"); this.useJavaInput = false; } diff --git a/jadx-core/src/test/java/jadx/tests/api/SmaliTest.java b/jadx-core/src/test/java/jadx/tests/api/SmaliTest.java index 0b332e0b5..29cc8fc1d 100644 --- a/jadx-core/src/test/java/jadx/tests/api/SmaliTest.java +++ b/jadx-core/src/test/java/jadx/tests/api/SmaliTest.java @@ -7,6 +7,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeEach; import jadx.api.JadxInternalAccess; @@ -24,6 +25,7 @@ public abstract class SmaliTest extends IntegrationTest { @BeforeEach public void init() { + Assumptions.assumeFalse(USE_JAVA_INPUT, "skip smali test for java input tests"); super.init(); this.useDexInput(); } diff --git a/jadx-core/src/test/java/jadx/tests/api/extensions/inputs/InputPlugin.java b/jadx-core/src/test/java/jadx/tests/api/extensions/inputs/InputPlugin.java new file mode 100644 index 000000000..534434071 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/api/extensions/inputs/InputPlugin.java @@ -0,0 +1,20 @@ +package jadx.tests.api.extensions.inputs; + +import java.util.function.Consumer; + +import jadx.tests.api.IntegrationTest; + +public enum InputPlugin implements Consumer { + DEX { + @Override + public void accept(IntegrationTest test) { + test.useDexInput(); + } + }, + JAVA { + @Override + public void accept(IntegrationTest test) { + test.useJavaInput(); + } + }; +} diff --git a/jadx-core/src/test/java/jadx/tests/api/extensions/inputs/JadxInputPluginsExtension.java b/jadx-core/src/test/java/jadx/tests/api/extensions/inputs/JadxInputPluginsExtension.java new file mode 100644 index 000000000..175bd63fa --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/api/extensions/inputs/JadxInputPluginsExtension.java @@ -0,0 +1,65 @@ +package jadx.tests.api.extensions.inputs; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.TestTemplateInvocationContext; +import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider; +import org.junit.platform.commons.util.AnnotationUtils; +import org.junit.platform.commons.util.Preconditions; + +import jadx.tests.api.IntegrationTest; + +import static org.junit.platform.commons.util.AnnotationUtils.isAnnotated; + +public class JadxInputPluginsExtension implements TestTemplateInvocationContextProvider { + + @Override + public boolean supportsTestTemplate(ExtensionContext context) { + return isAnnotated(context.getTestMethod(), TestWithInputPlugins.class); + } + + @Override + public Stream provideTestTemplateInvocationContexts(ExtensionContext context) { + Preconditions.condition(IntegrationTest.class.isAssignableFrom(context.getRequiredTestClass()), + "@TestWithInputPlugins should be used only in IntegrationTest subclasses"); + + Method testMethod = context.getRequiredTestMethod(); + boolean testAnnAdded = AnnotationUtils.findAnnotation(testMethod, Test.class).isPresent(); + Preconditions.condition(!testAnnAdded, "@Test annotation should be removed"); + + TestWithInputPlugins inputPluginAnn = AnnotationUtils.findAnnotation(testMethod, TestWithInputPlugins.class).get(); + return Stream.of(inputPluginAnn.value()) + .sorted() + .map(RunWithInputPlugin::new); + } + + private static class RunWithInputPlugin implements TestTemplateInvocationContext { + private final InputPlugin plugin; + + public RunWithInputPlugin(InputPlugin plugin) { + this.plugin = plugin; + } + + @Override + public String getDisplayName(int invocationIndex) { + return plugin.name().toLowerCase(Locale.ROOT) + " input"; + } + + @Override + public List getAdditionalExtensions() { + return Collections.singletonList(beforeTest()); + } + + private BeforeTestExecutionCallback beforeTest() { + return execContext -> plugin.accept((IntegrationTest) execContext.getRequiredTestInstance()); + } + } +} diff --git a/jadx-core/src/test/java/jadx/tests/api/extensions/inputs/TestWithInputPlugins.java b/jadx-core/src/test/java/jadx/tests/api/extensions/inputs/TestWithInputPlugins.java new file mode 100644 index 000000000..72db8f77b --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/api/extensions/inputs/TestWithInputPlugins.java @@ -0,0 +1,18 @@ +package jadx.tests.api.extensions.inputs; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; + +@TestTemplate +@ExtendWith(JadxInputPluginsExtension.class) +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE }) +public @interface TestWithInputPlugins { + + InputPlugin[] value(); +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/invoke/TestSuperInvoke2.java b/jadx-core/src/test/java/jadx/tests/integration/invoke/TestSuperInvoke2.java new file mode 100644 index 000000000..110bb8789 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/invoke/TestSuperInvoke2.java @@ -0,0 +1,29 @@ +package jadx.tests.integration.invoke; + +import jadx.tests.api.IntegrationTest; +import jadx.tests.api.extensions.inputs.InputPlugin; +import jadx.tests.api.extensions.inputs.TestWithInputPlugins; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +public class TestSuperInvoke2 extends IntegrationTest { + + public static class TestCls { + @Override + public String toString() { + return super.toString(); + } + + public void check() { + assertThat(new TestCls().toString()).containsOne("@"); + } + } + + @TestWithInputPlugins({ InputPlugin.DEX, InputPlugin.JAVA }) + public void test() { + noDebugInfo(); + assertThat(getClassNode(TestCls.class)) + .code() + .containsOne("return super.toString();"); + } +} diff --git a/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/code/JavaInsnsRegister.java b/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/code/JavaInsnsRegister.java index 1f53210e0..45a05c91d 100644 --- a/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/code/JavaInsnsRegister.java +++ b/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/code/JavaInsnsRegister.java @@ -283,7 +283,7 @@ public class JavaInsnsRegister { register(arr, 0xb5, "putfield", 2, 2, Opcode.IPUT, InsnIndexType.FIELD_REF, s -> s.idx(s.u2()).pop(0).pop(1)); invoke(arr, 0xb6, "invokevirtual", 2, Opcode.INVOKE_VIRTUAL); - invoke(arr, 0xb7, "invokespecial", 2, Opcode.INVOKE_DIRECT); + invoke(arr, 0xb7, "invokespecial", 2, Opcode.INVOKE_SPECIAL); invoke(arr, 0xb8, "invokestatic", 2, Opcode.INVOKE_STATIC); invoke(arr, 0xb9, "invokeinterface", 4, Opcode.INVOKE_INTERFACE); invoke(arr, 0xba, "invokedynamic", 4, Opcode.INVOKE_CUSTOM); diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/insns/Opcode.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/insns/Opcode.java index adf2a15d7..94dc539c0 100644 --- a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/insns/Opcode.java +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/insns/Opcode.java @@ -97,6 +97,7 @@ public enum Opcode { INVOKE_SUPER_RANGE, INVOKE_VIRTUAL, INVOKE_VIRTUAL_RANGE, + INVOKE_SPECIAL, IGET, IPUT,