fix: handle super case for invokespecial opcode (#1300)

This commit is contained in:
Skylot
2021-12-02 18:13:19 +00:00
parent 59ef569a63
commit 4cc00bdaf2
9 changed files with 165 additions and 9 deletions
@@ -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);
@@ -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;
}
@@ -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();
}
@@ -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<IntegrationTest> {
DEX {
@Override
public void accept(IntegrationTest test) {
test.useDexInput();
}
},
JAVA {
@Override
public void accept(IntegrationTest test) {
test.useJavaInput();
}
};
}
@@ -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<TestTemplateInvocationContext> 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<Extension> getAdditionalExtensions() {
return Collections.singletonList(beforeTest());
}
private BeforeTestExecutionCallback beforeTest() {
return execContext -> plugin.accept((IntegrationTest) execContext.getRequiredTestInstance());
}
}
}
@@ -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();
}
@@ -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();");
}
}
@@ -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);
@@ -97,6 +97,7 @@ public enum Opcode {
INVOKE_SUPER_RANGE,
INVOKE_VIRTUAL,
INVOKE_VIRTUAL_RANGE,
INVOKE_SPECIAL,
IGET,
IPUT,