diff --git a/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java b/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java
index 34763ad68..e5250e82f 100644
--- a/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java
+++ b/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java
@@ -14,6 +14,7 @@ import jadx.api.ICodeWriter;
import jadx.api.metadata.annotations.InsnCodeOffset;
import jadx.api.metadata.annotations.VarNode;
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.nodes.FieldReplaceAttr;
@@ -36,6 +37,7 @@ import jadx.core.dex.instructions.IfNode;
import jadx.core.dex.instructions.IndexInsnNode;
import jadx.core.dex.instructions.InsnType;
import jadx.core.dex.instructions.InvokeCustomNode;
+import jadx.core.dex.instructions.InvokeCustomRawNode;
import jadx.core.dex.instructions.InvokeNode;
import jadx.core.dex.instructions.InvokeType;
import jadx.core.dex.instructions.NewArrayNode;
@@ -795,6 +797,10 @@ public class InsnGen {
MethodInfo callMth = insn.getCallMth();
MethodNode callMthNode = mth.root().resolveMethod(callMth);
+ if (type == InvokeType.CUSTOM_RAW) {
+ makeInvokeCustomRaw((InvokeCustomRawNode) insn, callMthNode, code);
+ return;
+ }
if (insn.isPolymorphicCall()) {
// add missing cast
code.add('(');
@@ -845,6 +851,31 @@ public class InsnGen {
generateMethodArguments(code, insn, k, callMthNode);
}
+ private void makeInvokeCustomRaw(InvokeCustomRawNode insn,
+ @Nullable MethodNode callMthNode, ICodeWriter code) throws CodegenException {
+ if (isFallback()) {
+ code.add("call_site(");
+ code.incIndent();
+ for (EncodedValue value : insn.getCallSiteValues()) {
+ code.startLine(value.toString());
+ }
+ code.decIndent();
+ code.startLine(").invoke");
+ generateMethodArguments(code, insn, 0, callMthNode);
+ } else {
+ ArgType returnType = insn.getCallMth().getReturnType();
+ if (!returnType.isVoid()) {
+ code.add('(');
+ useType(code, returnType);
+ code.add(") ");
+ }
+ makeInvoke(insn.getResolveInvoke(), code);
+ code.add(".dynamicInvoker().invoke");
+ generateMethodArguments(code, insn, 0, callMthNode);
+ code.add(" /* invoke-custom */");
+ }
+ }
+
// FIXME: add 'this' for equals methods in scope
private boolean needInvokeArg(InsnArg arg) {
if (arg.isAnyThis()) {
diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/InvokeCustomBuilder.java b/jadx-core/src/main/java/jadx/core/dex/instructions/InvokeCustomBuilder.java
index 216d72aae..751022423 100644
--- a/jadx-core/src/main/java/jadx/core/dex/instructions/InvokeCustomBuilder.java
+++ b/jadx-core/src/main/java/jadx/core/dex/instructions/InvokeCustomBuilder.java
@@ -5,10 +5,15 @@ import java.util.List;
import jadx.api.plugins.input.data.ICallSite;
import jadx.api.plugins.input.data.annotations.EncodedValue;
import jadx.api.plugins.input.insns.InsnData;
+import jadx.core.dex.attributes.AFlag;
+import jadx.core.dex.attributes.AType;
+import jadx.core.dex.attributes.nodes.JadxError;
import jadx.core.dex.instructions.invokedynamic.CustomLambdaCall;
+import jadx.core.dex.instructions.invokedynamic.CustomRawCall;
import jadx.core.dex.instructions.invokedynamic.CustomStringConcat;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
+import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.input.InsnDataUtils;
@@ -28,8 +33,16 @@ public class InvokeCustomBuilder {
if (CustomStringConcat.isStringConcat(values)) {
return CustomStringConcat.buildStringConcat(insn, isRange, values);
}
- // TODO: output raw dynamic call
- throw new JadxRuntimeException("Failed to process invoke-custom instruction: " + callSite);
+ try {
+ return CustomRawCall.build(mth, insn, isRange, values);
+ } catch (Exception e) {
+ mth.addWarn("Failed to decode invoke-custom: \n" + Utils.listToString(values, "\n")
+ + ",\n exception: " + Utils.getStackTrace(e));
+ InsnNode nop = new InsnNode(InsnType.NOP, 0);
+ nop.add(AFlag.SYNTHETIC);
+ nop.addAttr(AType.JADX_ERROR, new JadxError("Failed to decode invoke-custom: " + values, e));
+ return nop;
+ }
} catch (Exception e) {
throw new JadxRuntimeException("'invoke-custom' instruction processing error: " + e.getMessage(), e);
}
diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/InvokeCustomRawNode.java b/jadx-core/src/main/java/jadx/core/dex/instructions/InvokeCustomRawNode.java
new file mode 100644
index 000000000..a8c847cdf
--- /dev/null
+++ b/jadx-core/src/main/java/jadx/core/dex/instructions/InvokeCustomRawNode.java
@@ -0,0 +1,98 @@
+package jadx.core.dex.instructions;
+
+import java.util.List;
+
+import org.jetbrains.annotations.Nullable;
+
+import jadx.api.plugins.input.data.annotations.EncodedValue;
+import jadx.api.plugins.input.insns.InsnData;
+import jadx.core.dex.info.MethodInfo;
+import jadx.core.dex.instructions.args.InsnArg;
+import jadx.core.dex.instructions.invokedynamic.CustomRawCall;
+import jadx.core.dex.nodes.InsnNode;
+import jadx.core.utils.InsnUtils;
+import jadx.core.utils.Utils;
+
+/**
+ * Information for raw invoke-custom instruction.
+ * Output will be formatted as polymorphic call with equivalent semantic
+ * Contains two parts:
+ * - resolve: treated as additional invoke insn (uses only constant args)
+ * - invoke: call of resolved method (base for this invoke)
+ *
+ * See {@link CustomRawCall} class for build details
+ */
+public class InvokeCustomRawNode extends InvokeNode {
+ private final InvokeNode resolve;
+ private List callSiteValues;
+
+ public InvokeCustomRawNode(InvokeNode resolve, MethodInfo mthInfo, InsnData insn, boolean isRange) {
+ super(mthInfo, insn, InvokeType.CUSTOM_RAW, false, isRange);
+ this.resolve = resolve;
+ }
+
+ public InvokeCustomRawNode(InvokeNode resolve, MethodInfo mthInfo, InvokeType invokeType, int argsCount) {
+ super(mthInfo, invokeType, argsCount);
+ this.resolve = resolve;
+ }
+
+ public InvokeNode getResolveInvoke() {
+ return resolve;
+ }
+
+ public void setCallSiteValues(List callSiteValues) {
+ this.callSiteValues = callSiteValues;
+ }
+
+ public List getCallSiteValues() {
+ return callSiteValues;
+ }
+
+ @Override
+ public InsnNode copy() {
+ InvokeCustomRawNode copy = new InvokeCustomRawNode(resolve, getCallMth(), getInvokeType(), getArgsCount());
+ copyCommonParams(copy);
+ copy.setCallSiteValues(callSiteValues);
+ return copy;
+ }
+
+ @Override
+ public boolean isStaticCall() {
+ return true;
+ }
+
+ @Override
+ public int getFirstArgOffset() {
+ return 0;
+ }
+
+ @Override
+ public @Nullable InsnArg getInstanceArg() {
+ return null;
+ }
+
+ @Override
+ public boolean isSame(InsnNode obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof InvokeCustomRawNode) {
+ return super.isSame(obj) && resolve.isSame(((InvokeCustomRawNode) obj).resolve);
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(InsnUtils.formatOffset(offset)).append(": INVOKE_CUSTOM ");
+ if (getResult() != null) {
+ sb.append(getResult()).append(" = ");
+ }
+ if (!appendArgs(sb)) {
+ sb.append('\n');
+ }
+ sb.append(" call-site: \n ").append(Utils.listToString(callSiteValues, "\n ")).append('\n');
+ return sb.toString();
+ }
+}
diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/InvokeType.java b/jadx-core/src/main/java/jadx/core/dex/instructions/InvokeType.java
index 5692ba019..08da759c8 100644
--- a/jadx-core/src/main/java/jadx/core/dex/instructions/InvokeType.java
+++ b/jadx-core/src/main/java/jadx/core/dex/instructions/InvokeType.java
@@ -8,4 +8,5 @@ public enum InvokeType {
SUPER,
POLYMORPHIC,
CUSTOM,
+ CUSTOM_RAW,
}
diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/args/PrimitiveType.java b/jadx-core/src/main/java/jadx/core/dex/instructions/args/PrimitiveType.java
index 4a8bdf65f..55b1ac470 100644
--- a/jadx-core/src/main/java/jadx/core/dex/instructions/args/PrimitiveType.java
+++ b/jadx-core/src/main/java/jadx/core/dex/instructions/args/PrimitiveType.java
@@ -1,24 +1,26 @@
package jadx.core.dex.instructions.args;
public enum PrimitiveType {
- BOOLEAN("Z", "boolean"),
- CHAR("C", "char"),
- BYTE("B", "byte"),
- SHORT("S", "short"),
- INT("I", "int"),
- FLOAT("F", "float"),
- LONG("J", "long"),
- DOUBLE("D", "double"),
- OBJECT("L", "OBJECT"),
- ARRAY("[", "ARRAY"),
- VOID("V", "void");
+ BOOLEAN("Z", "boolean", ArgType.object("java.lang.Boolean")),
+ CHAR("C", "char", ArgType.object("java.lang.Character")),
+ BYTE("B", "byte", ArgType.object("java.lang.Byte")),
+ SHORT("S", "short", ArgType.object("java.lang.Short")),
+ INT("I", "int", ArgType.object("java.lang.Integer")),
+ FLOAT("F", "float", ArgType.object("java.lang.Float")),
+ LONG("J", "long", ArgType.object("java.lang.Long")),
+ DOUBLE("D", "double", ArgType.object("java.lang.Double")),
+ OBJECT("L", "OBJECT", ArgType.OBJECT),
+ ARRAY("[", "ARRAY", ArgType.OBJECT_ARRAY),
+ VOID("V", "void", ArgType.object("java.lang.Void"));
private final String shortName;
private final String longName;
+ private final ArgType boxType;
- PrimitiveType(String shortName, String longName) {
+ PrimitiveType(String shortName, String longName, ArgType boxType) {
this.shortName = shortName;
this.longName = longName;
+ this.boxType = boxType;
}
public String getShortName() {
@@ -29,6 +31,10 @@ public enum PrimitiveType {
return longName;
}
+ public ArgType getBoxType() {
+ return boxType;
+ }
+
@Override
public String toString() {
return longName;
diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/invokedynamic/CustomLambdaCall.java b/jadx-core/src/main/java/jadx/core/dex/instructions/invokedynamic/CustomLambdaCall.java
index 9410b279e..40188d9af 100644
--- a/jadx-core/src/main/java/jadx/core/dex/instructions/invokedynamic/CustomLambdaCall.java
+++ b/jadx-core/src/main/java/jadx/core/dex/instructions/invokedynamic/CustomLambdaCall.java
@@ -8,6 +8,7 @@ import jadx.api.plugins.input.data.IMethodHandle;
import jadx.api.plugins.input.data.IMethodProto;
import jadx.api.plugins.input.data.IMethodRef;
import jadx.api.plugins.input.data.MethodHandleType;
+import jadx.api.plugins.input.data.annotations.EncodedType;
import jadx.api.plugins.input.data.annotations.EncodedValue;
import jadx.api.plugins.input.insns.InsnData;
import jadx.core.dex.attributes.AFlag;
@@ -34,7 +35,11 @@ public class CustomLambdaCall {
if (values.size() < 6) {
return false;
}
- IMethodHandle methodHandle = (IMethodHandle) values.get(0).getValue();
+ EncodedValue mthRef = values.get(0);
+ if (mthRef.getType() != EncodedType.ENCODED_METHOD_HANDLE) {
+ return false;
+ }
+ IMethodHandle methodHandle = (IMethodHandle) mthRef.getValue();
if (methodHandle.getType() != MethodHandleType.INVOKE_STATIC) {
return false;
}
@@ -113,7 +118,7 @@ public class CustomLambdaCall {
@NotNull
private static InvokeNode buildInvokeNode(MethodHandleType methodHandleType, InvokeCustomNode invokeCustomNode,
MethodInfo callMthInfo) {
- InvokeType invokeType = convertInvokeType(methodHandleType);
+ InvokeType invokeType = InvokeCustomUtils.convertInvokeType(methodHandleType);
int callArgsCount = callMthInfo.getArgsCount();
boolean instanceCall = invokeType != InvokeType.STATIC;
if (instanceCall) {
@@ -147,21 +152,4 @@ public class CustomLambdaCall {
}
return invokeNode;
}
-
- private static InvokeType convertInvokeType(MethodHandleType type) {
- switch (type) {
- case INVOKE_STATIC:
- return InvokeType.STATIC;
- case INVOKE_INSTANCE:
- return InvokeType.VIRTUAL;
- case INVOKE_DIRECT:
- case INVOKE_CONSTRUCTOR:
- return InvokeType.DIRECT;
- case INVOKE_INTERFACE:
- return InvokeType.INTERFACE;
-
- default:
- throw new JadxRuntimeException("Unsupported method handle type: " + type);
- }
- }
}
diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/invokedynamic/CustomRawCall.java b/jadx-core/src/main/java/jadx/core/dex/instructions/invokedynamic/CustomRawCall.java
new file mode 100644
index 000000000..b7d1fa4f2
--- /dev/null
+++ b/jadx-core/src/main/java/jadx/core/dex/instructions/invokedynamic/CustomRawCall.java
@@ -0,0 +1,70 @@
+package jadx.core.dex.instructions.invokedynamic;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import jadx.api.plugins.input.data.IMethodHandle;
+import jadx.api.plugins.input.data.IMethodProto;
+import jadx.api.plugins.input.data.annotations.EncodedValue;
+import jadx.api.plugins.input.insns.InsnData;
+import jadx.core.dex.info.ClassInfo;
+import jadx.core.dex.info.MethodInfo;
+import jadx.core.dex.instructions.ConstStringNode;
+import jadx.core.dex.instructions.InvokeCustomRawNode;
+import jadx.core.dex.instructions.InvokeNode;
+import jadx.core.dex.instructions.InvokeType;
+import jadx.core.dex.instructions.args.ArgType;
+import jadx.core.dex.instructions.args.InsnArg;
+import jadx.core.dex.nodes.InsnNode;
+import jadx.core.dex.nodes.MethodNode;
+import jadx.core.dex.nodes.RootNode;
+import jadx.core.utils.exceptions.JadxRuntimeException;
+
+import static jadx.core.utils.EncodedValueUtils.buildLookupArg;
+import static jadx.core.utils.EncodedValueUtils.convertToInsnArg;
+
+/**
+ * Show `invoke-custom` similar to polymorphic call
+ */
+public class CustomRawCall {
+
+ public static InsnNode build(MethodNode mth, InsnData insn, boolean isRange, List values) {
+ IMethodHandle resolveHandle = (IMethodHandle) values.get(0).getValue();
+ String invokeName = (String) values.get(1).getValue();
+ IMethodProto invokeProto = (IMethodProto) values.get(2).getValue();
+ List resolveArgs = buildArgs(mth, values);
+
+ if (resolveHandle.getType().isField()) {
+ throw new JadxRuntimeException("Field handle not yet supported");
+ }
+
+ RootNode root = mth.root();
+ MethodInfo resolveMth = MethodInfo.fromRef(root, resolveHandle.getMethodRef());
+ InvokeType resolveInvokeType = InvokeCustomUtils.convertInvokeType(resolveHandle.getType());
+ InvokeNode resolve = new InvokeNode(resolveMth, resolveInvokeType, resolveArgs.size());
+ resolveArgs.forEach(resolve::addArg);
+
+ ClassInfo invokeCls = ClassInfo.fromType(root, ArgType.OBJECT); // type will be known at runtime
+ MethodInfo invokeMth = MethodInfo.fromMethodProto(root, invokeCls, invokeName, invokeProto);
+ InvokeCustomRawNode customRawNode = new InvokeCustomRawNode(resolve, invokeMth, insn, isRange);
+ customRawNode.setCallSiteValues(values);
+ return customRawNode;
+ }
+
+ private static List buildArgs(MethodNode mth, List values) {
+ int valuesCount = values.size();
+ List list = new ArrayList<>(valuesCount);
+ RootNode root = mth.root();
+ list.add(buildLookupArg(root)); // use `java.lang.invoke.MethodHandles.lookup()` as first arg
+ for (int i = 1; i < valuesCount; i++) {
+ EncodedValue value = values.get(i);
+ try {
+ list.add(convertToInsnArg(root, value));
+ } catch (Exception e) {
+ mth.addWarnComment("Failed to build arg in invoke-custom insn: " + value, e);
+ list.add(InsnArg.wrapArg(new ConstStringNode(value.toString())));
+ }
+ }
+ return list;
+ }
+}
diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/invokedynamic/InvokeCustomUtils.java b/jadx-core/src/main/java/jadx/core/dex/instructions/invokedynamic/InvokeCustomUtils.java
new file mode 100644
index 000000000..c5dbd75c8
--- /dev/null
+++ b/jadx-core/src/main/java/jadx/core/dex/instructions/invokedynamic/InvokeCustomUtils.java
@@ -0,0 +1,25 @@
+package jadx.core.dex.instructions.invokedynamic;
+
+import jadx.api.plugins.input.data.MethodHandleType;
+import jadx.core.dex.instructions.InvokeType;
+import jadx.core.utils.exceptions.JadxRuntimeException;
+
+public class InvokeCustomUtils {
+
+ public static InvokeType convertInvokeType(MethodHandleType type) {
+ switch (type) {
+ case INVOKE_STATIC:
+ return InvokeType.STATIC;
+ case INVOKE_INSTANCE:
+ return InvokeType.VIRTUAL;
+ case INVOKE_DIRECT:
+ case INVOKE_CONSTRUCTOR:
+ return InvokeType.DIRECT;
+ case INVOKE_INTERFACE:
+ return InvokeType.INTERFACE;
+
+ default:
+ throw new JadxRuntimeException("Unsupported method handle type: " + type);
+ }
+ }
+}
diff --git a/jadx-core/src/main/java/jadx/core/utils/EncodedValueUtils.java b/jadx-core/src/main/java/jadx/core/utils/EncodedValueUtils.java
index 05f53163c..0bc949c8e 100644
--- a/jadx-core/src/main/java/jadx/core/utils/EncodedValueUtils.java
+++ b/jadx-core/src/main/java/jadx/core/utils/EncodedValueUtils.java
@@ -1,11 +1,33 @@
package jadx.core.utils;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
import org.jetbrains.annotations.Nullable;
+import jadx.api.plugins.input.data.IMethodHandle;
+import jadx.api.plugins.input.data.IMethodProto;
+import jadx.api.plugins.input.data.IMethodRef;
+import jadx.api.plugins.input.data.MethodHandleType;
import jadx.api.plugins.input.data.annotations.EncodedValue;
+import jadx.core.dex.info.ClassInfo;
+import jadx.core.dex.info.FieldInfo;
+import jadx.core.dex.info.MethodInfo;
+import jadx.core.dex.instructions.ConstClassNode;
+import jadx.core.dex.instructions.ConstStringNode;
+import jadx.core.dex.instructions.IndexInsnNode;
+import jadx.core.dex.instructions.InsnType;
+import jadx.core.dex.instructions.InvokeNode;
+import jadx.core.dex.instructions.InvokeType;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.LiteralArg;
+import jadx.core.dex.instructions.args.PrimitiveType;
+import jadx.core.dex.nodes.InsnNode;
+import jadx.core.dex.nodes.RootNode;
+import jadx.core.utils.exceptions.JadxRuntimeException;
public class EncodedValueUtils {
@@ -50,4 +72,108 @@ public class EncodedValueUtils {
return null;
}
}
+
+ public static InsnArg convertToInsnArg(RootNode root, EncodedValue value) {
+ Object obj = value.getValue();
+ switch (value.getType()) {
+ case ENCODED_NULL:
+ case ENCODED_BYTE:
+ case ENCODED_SHORT:
+ case ENCODED_CHAR:
+ case ENCODED_INT:
+ case ENCODED_LONG:
+ case ENCODED_FLOAT:
+ case ENCODED_DOUBLE:
+ return (InsnArg) convertToConstValue(value);
+
+ case ENCODED_BOOLEAN:
+ return InsnArg.lit(((Boolean) obj) ? 0 : 1, ArgType.BOOLEAN);
+ case ENCODED_STRING:
+ return InsnArg.wrapArg(new ConstStringNode((String) obj));
+ case ENCODED_TYPE:
+ return InsnArg.wrapArg(new ConstClassNode(ArgType.parse((String) obj)));
+ case ENCODED_METHOD_TYPE:
+ return InsnArg.wrapArg(buildMethodType(root, (IMethodProto) obj));
+ case ENCODED_METHOD_HANDLE:
+ return InsnArg.wrapArg(buildMethodHandle(root, (IMethodHandle) obj));
+
+ }
+ throw new JadxRuntimeException("Unsupported type for raw invoke-custom: " + value.getType());
+ }
+
+ private static InvokeNode buildMethodType(RootNode root, IMethodProto methodProto) {
+ ArgType retType = ArgType.parse(methodProto.getReturnType());
+ List argTypes = Utils.collectionMap(methodProto.getArgTypes(), ArgType::parse);
+ List callTypes = new ArrayList<>(1 + argTypes.size());
+ callTypes.add(retType);
+ callTypes.addAll(argTypes);
+ ArgType mthType = ArgType.object("java.lang.invoke.MethodType");
+ ClassInfo cls = ClassInfo.fromType(root, mthType);
+ MethodInfo mth = MethodInfo.fromDetails(root, cls, "methodType", callTypes, mthType);
+ InvokeNode invoke = new InvokeNode(mth, InvokeType.STATIC, callTypes.size());
+ for (ArgType type : callTypes) {
+ InsnNode argInsn;
+ if (type.isPrimitive()) {
+ argInsn = new IndexInsnNode(InsnType.SGET, getTypeField(root, type.getPrimitiveType()), 0);
+ } else {
+ argInsn = new ConstClassNode(type);
+ }
+ invoke.addArg(InsnArg.wrapArg(argInsn));
+ }
+ return invoke;
+ }
+
+ public static FieldInfo getTypeField(RootNode root, PrimitiveType type) {
+ ArgType boxType = type.getBoxType();
+ ClassInfo boxCls = ClassInfo.fromType(root, boxType);
+ return FieldInfo.from(root, boxCls, "TYPE", boxType);
+ }
+
+ /**
+ * Build `MethodHandles.lookup().find{type}(methodCls, methodName, methodType)`
+ */
+ private static InsnNode buildMethodHandle(RootNode root, IMethodHandle methodHandle) {
+ if (methodHandle.getType().isField()) {
+ // TODO: lookup for field
+ return new ConstStringNode("FIELD:" + methodHandle.getFieldRef());
+ }
+ IMethodRef methodRef = methodHandle.getMethodRef();
+ methodRef.load();
+
+ ClassInfo lookupCls = ClassInfo.fromName(root, "java.lang.invoke.MethodHandles.Lookup");
+ MethodInfo findMethod = MethodInfo.fromDetails(root, lookupCls,
+ getFindMethodName(methodHandle.getType()),
+ Arrays.asList(ArgType.CLASS, ArgType.STRING, ArgType.object("java.lang.invoke.MethodType")),
+ ArgType.object("java.lang.invoke.MethodHandle"));
+
+ InvokeNode invoke = new InvokeNode(findMethod, InvokeType.DIRECT, 4);
+ invoke.addArg(buildLookupArg(root));
+ invoke.addArg(InsnArg.wrapArg(new ConstClassNode(ArgType.object(methodRef.getParentClassType()))));
+ invoke.addArg(InsnArg.wrapArg(new ConstStringNode(methodRef.getName())));
+ invoke.addArg(InsnArg.wrapArg(buildMethodType(root, methodRef)));
+ return invoke;
+ }
+
+ public static InsnArg buildLookupArg(RootNode root) {
+ ArgType lookupType = ArgType.object("java.lang.invoke.MethodHandles.Lookup");
+ ClassInfo cls = ClassInfo.fromName(root, "java.lang.invoke.MethodHandles");
+ MethodInfo mth = MethodInfo.fromDetails(root, cls, "lookup", Collections.emptyList(), lookupType);
+ return InsnArg.wrapArg(new InvokeNode(mth, InvokeType.STATIC, 0));
+ }
+
+ private static String getFindMethodName(MethodHandleType type) {
+ switch (type) {
+ case INVOKE_STATIC:
+ return "findStatic";
+ case INVOKE_CONSTRUCTOR:
+ return "findConstructor";
+ case INVOKE_INSTANCE:
+ case INVOKE_DIRECT:
+ case INVOKE_INTERFACE:
+ return "findVirtual";
+
+ default:
+ return "<" + type + '>';
+ }
+ }
}
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 f6adf786b..f78a61b03 100644
--- a/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java
+++ b/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java
@@ -110,6 +110,12 @@ public abstract class IntegrationTest extends TestUtils {
private @Nullable TestCompiler sourceCompiler;
private @Nullable TestCompiler decompiledCompiler;
+ /**
+ * Run check method on decompiled code even if source check method not found.
+ * Useful for smali test if check method added to smali code
+ */
+ private boolean forceDecompiledCheck = false;
+
static {
// enable debug checks
DebugChecks.checksEnabled = true;
@@ -347,11 +353,10 @@ public abstract class IntegrationTest extends TestUtils {
String clsName = cls.getClassInfo().getRawName().replace('/', '.');
try {
// run 'check' method from original class
- if (runSourceAutoCheck(clsName)) {
- return;
- }
+ boolean sourceCheckFound = runSourceAutoCheck(clsName);
+
// run 'check' method from decompiled class
- if (compile) {
+ if (compile && (sourceCheckFound || forceDecompiledCheck)) {
runDecompiledAutoCheck(cls);
}
} catch (Exception e) {
@@ -362,36 +367,36 @@ public abstract class IntegrationTest extends TestUtils {
private boolean runSourceAutoCheck(String clsName) {
if (sourceCompiler == null) {
- // no source code (smali case)
- return true;
+ System.out.println("Source check: no code");
+ return false;
}
Class> origCls;
try {
origCls = sourceCompiler.getClass(clsName);
} catch (ClassNotFoundException e) {
rethrow("Missing class: " + clsName, e);
- return true;
+ return false;
}
Method checkMth;
try {
checkMth = sourceCompiler.getMethod(origCls, CHECK_METHOD_NAME, new Class[] {});
} catch (NoSuchMethodException e) {
// ignore
- return true;
+ return false;
}
if (!checkMth.getReturnType().equals(void.class)
|| !Modifier.isPublic(checkMth.getModifiers())
|| Modifier.isStatic(checkMth.getModifiers())) {
fail("Wrong 'check' method");
- return true;
+ return false;
}
try {
limitExecTime(() -> checkMth.invoke(origCls.getConstructor().newInstance()));
System.out.println("Source check: PASSED");
+ return true;
} catch (Throwable e) {
throw new JadxRuntimeException("Source check failed", e);
}
- return false;
}
public void runDecompiledAutoCheck(ClassNode cls) {
@@ -554,6 +559,10 @@ public abstract class IntegrationTest extends TestUtils {
this.compile = false;
}
+ protected void forceDecompiledCheck() {
+ this.forceDecompiledCheck = true;
+ }
+
protected void enableDeobfuscation() {
args.setDeobfuscationOn(true);
args.setDeobfuscationMapFileMode(DeobfuscationMapFileMode.IGNORE);
diff --git a/jadx-core/src/test/java/jadx/tests/integration/invoke/TestRawCustomInvoke.java b/jadx-core/src/test/java/jadx/tests/integration/invoke/TestRawCustomInvoke.java
new file mode 100644
index 000000000..bbc76c5aa
--- /dev/null
+++ b/jadx-core/src/test/java/jadx/tests/integration/invoke/TestRawCustomInvoke.java
@@ -0,0 +1,66 @@
+package jadx.tests.integration.invoke;
+
+import java.lang.invoke.CallSite;
+import java.lang.invoke.ConstantCallSite;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+
+import org.junit.jupiter.api.Test;
+
+import jadx.tests.api.SmaliTest;
+
+import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class TestRawCustomInvoke extends SmaliTest {
+
+ public static class TestCls {
+
+ public static String func(int a, double b) {
+ return String.valueOf(a + b);
+ }
+
+ private static CallSite staticBootstrap(MethodHandles.Lookup lookup, String name, MethodType type) {
+ try {
+ return new ConstantCallSite(lookup.findStatic(lookup.lookupClass(), name, type));
+ } catch (NoSuchMethodException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public String test() {
+ try {
+ return (String) staticBootstrap(MethodHandles.lookup(), "func",
+ MethodType.methodType(String.class, Integer.TYPE, Double.TYPE))
+ .dynamicInvoker().invoke(1, 2.0d);
+ } catch (Throwable e) {
+ fail(e);
+ return null;
+ }
+ }
+
+ public void check() {
+ assertThat(test()).isEqualTo("3.0");
+ }
+ }
+
+ @Test
+ public void test() {
+ noDebugInfo();
+ // this code does not contain `invoke-custom` instruction
+ // only check if equivalent polymorphic call is correct
+ assertThat(getClassNode(TestCls.class))
+ .code()
+ .containsOne(
+ "return (String) staticBootstrap(MethodHandles.lookup(), \"func\", MethodType.methodType(String.class, Integer.TYPE, Double.TYPE)).dynamicInvoker().invoke(1, 2.0d);");
+ }
+
+ @Test
+ public void testSmali() {
+ forceDecompiledCheck();
+ assertThat(getClassNodeFromSmali())
+ .code()
+ .containsOne(
+ "return (String) staticBootstrap(MethodHandles.lookup(), \"func\", MethodType.methodType(String.class, Integer.TYPE, Double.TYPE)).dynamicInvoker().invoke(1, 2.0d) /* invoke-custom */;");
+ }
+}
diff --git a/jadx-core/src/test/java/jadx/tests/integration/java8/TestLambdaExtVar.java b/jadx-core/src/test/java/jadx/tests/integration/java8/TestLambdaExtVar.java
index 285d83711..269157e54 100644
--- a/jadx-core/src/test/java/jadx/tests/integration/java8/TestLambdaExtVar.java
+++ b/jadx-core/src/test/java/jadx/tests/integration/java8/TestLambdaExtVar.java
@@ -33,7 +33,5 @@ public class TestLambdaExtVar extends IntegrationTest {
.code()
.doesNotContain("lambda$")
.containsOne("return s.equals(str);"); // TODO: simplify to expression
-
- System.out.println(cls.getCode().getCodeMetadata());
}
}
diff --git a/jadx-core/src/test/smali/invoke/TestRawCustomInvoke.smali b/jadx-core/src/test/smali/invoke/TestRawCustomInvoke.smali
new file mode 100644
index 000000000..d1e75d627
--- /dev/null
+++ b/jadx-core/src/test/smali/invoke/TestRawCustomInvoke.smali
@@ -0,0 +1,62 @@
+.class public Linvoke/TestRawCustomInvoke;
+.super Ljava/lang/Object;
+
+.method public static func(ID)Ljava/lang/String;
+ .registers 5
+ int-to-double v0, p0
+ add-double/2addr v0, p1
+ invoke-static {v0, v1}, Ljava/lang/String;->valueOf(D)Ljava/lang/String;
+ move-result-object p0
+ return-object p0
+.end method
+
+.method private static staticBootstrap(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
+ .registers 5
+ :try_start_0
+ new-instance v0, Ljava/lang/invoke/ConstantCallSite;
+ invoke-virtual {p0}, Ljava/lang/invoke/MethodHandles$Lookup;->lookupClass()Ljava/lang/Class;
+ move-result-object v1
+ invoke-virtual {p0, v1, p1, p2}, Ljava/lang/invoke/MethodHandles$Lookup;->findStatic(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle;
+ move-result-object p0
+ invoke-direct {v0, p0}, Ljava/lang/invoke/ConstantCallSite;->(Ljava/lang/invoke/MethodHandle;)V
+ :try_end_d
+ .catch Ljava/lang/NoSuchMethodException; {:try_start_0 .. :try_end_d} :catch_e
+ .catch Ljava/lang/IllegalAccessException; {:try_start_0 .. :try_end_d} :catch_e
+ return-object v0
+ :catch_e
+ move-exception p0
+ new-instance p1, Ljava/lang/RuntimeException;
+ invoke-direct {p1, p0}, Ljava/lang/RuntimeException;->(Ljava/lang/Throwable;)V
+ throw p1
+.end method
+
+.method public test()Ljava/lang/String;
+ .registers 3
+ :try_start_0
+
+ const/4 v0, 0x1
+ const-wide/high16 v1, 0x4000000000000000L # 2.0
+ invoke-custom {v0, v1}, call_site_0("func", (ID)Ljava/lang/String;)@Linvoke/TestRawCustomInvoke;->staticBootstrap(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
+
+ move-result-object v0
+
+ :try_end_25
+ .catchall {:try_start_0 .. :try_end_25} :catchall_26
+ return-object v0
+ :catchall_26
+ move-exception v0
+ invoke-static {v0}, Lorg/junit/jupiter/api/Assertions;->fail(Ljava/lang/Throwable;)Ljava/lang/Object;
+ const/4 v0, 0x0
+ return-object v0
+.end method
+
+.method public check()V
+ .registers 3
+ invoke-virtual {p0}, Linvoke/TestRawCustomInvoke;->test()Ljava/lang/String;
+ move-result-object v0
+ invoke-static {v0}, Ljadx/tests/api/utils/assertj/JadxAssertions;->assertThat(Ljava/lang/String;)Ljadx/tests/api/utils/assertj/JadxCodeAssertions;
+ move-result-object v0
+ const-string v1, "3.0"
+ invoke-virtual {v0, v1}, Ljadx/tests/api/utils/assertj/JadxCodeAssertions;->isEqualTo(Ljava/lang/String;)Lorg/assertj/core/api/AbstractStringAssert;
+ return-void
+.end method
diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/annotations/EncodedValue.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/annotations/EncodedValue.java
index dfc4821cf..77b54554c 100644
--- a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/annotations/EncodedValue.java
+++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/annotations/EncodedValue.java
@@ -53,12 +53,12 @@ public class EncodedValue extends PinnedAttribute {
switch (type) {
case ENCODED_NULL:
return "null";
- case ENCODED_STRING:
- return (String) value;
case ENCODED_ARRAY:
return "[" + value + "]";
+ case ENCODED_STRING:
+ return "{STRING: \"" + value + "\"}";
default:
- return "{" + type + ": " + value + '}';
+ return "{" + type.toString().substring(8) + ": " + value + '}';
}
}
}