From 85c2c63aa3f1b962d13ef3845df5443e217f2b0e Mon Sep 17 00:00:00 2001 From: Skylot Date: Sat, 11 Feb 2023 15:03:15 +0000 Subject: [PATCH] fix: output unknown `invoke-custom` as polymorphic call (#1760) --- .../main/java/jadx/core/codegen/InsnGen.java | 31 +++++ .../dex/instructions/InvokeCustomBuilder.java | 17 ++- .../dex/instructions/InvokeCustomRawNode.java | 98 ++++++++++++++ .../core/dex/instructions/InvokeType.java | 1 + .../dex/instructions/args/PrimitiveType.java | 30 +++-- .../invokedynamic/CustomLambdaCall.java | 26 +--- .../invokedynamic/CustomRawCall.java | 70 ++++++++++ .../invokedynamic/InvokeCustomUtils.java | 25 ++++ .../jadx/core/utils/EncodedValueUtils.java | 126 ++++++++++++++++++ .../java/jadx/tests/api/IntegrationTest.java | 29 ++-- .../invoke/TestRawCustomInvoke.java | 66 +++++++++ .../integration/java8/TestLambdaExtVar.java | 2 - .../smali/invoke/TestRawCustomInvoke.smali | 62 +++++++++ .../input/data/annotations/EncodedValue.java | 6 +- 14 files changed, 541 insertions(+), 48 deletions(-) create mode 100644 jadx-core/src/main/java/jadx/core/dex/instructions/InvokeCustomRawNode.java create mode 100644 jadx-core/src/main/java/jadx/core/dex/instructions/invokedynamic/CustomRawCall.java create mode 100644 jadx-core/src/main/java/jadx/core/dex/instructions/invokedynamic/InvokeCustomUtils.java create mode 100644 jadx-core/src/test/java/jadx/tests/integration/invoke/TestRawCustomInvoke.java create mode 100644 jadx-core/src/test/smali/invoke/TestRawCustomInvoke.smali 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 34763ad6810..e5250e82f62 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.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.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 @@ private void makeInvoke(InvokeNode insn, ICodeWriter code) throws CodegenExcepti 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 @@ private void makeInvoke(InvokeNode insn, ICodeWriter code) throws CodegenExcepti 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 216d72aaed0..7510224232a 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 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 static InsnNode build(MethodNode mth, InsnData insn, boolean isRange) { 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 00000000000..a8c847cdf6e --- /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 5692ba0199c..08da759c84d 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 4a8bdf65fcc..55b1ac470cb 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 String getLongName() { 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 9410b279ed1..40188d9af8d 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.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 static boolean isLambdaInvoke(List values) { 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 @@ private static InvokeCustomNode buildMethodCall(MethodNode mth, InsnData insn, b @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 @@ private static InvokeNode buildInvokeNode(MethodHandleType methodHandleType, Inv } 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 00000000000..b7d1fa4f286 --- /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 00000000000..c5dbd75c88c --- /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 05f53163c8d..0bc949c8e2e 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 static Object convertToConstValue(EncodedValue encodedValue) { 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 f6adf786be7..f78a61b032f 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 @@ private void runAutoCheck(ClassNode cls) { 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 @@ private void runAutoCheck(ClassNode cls) { 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 @@ protected void disableCompilation() { 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 00000000000..bbc76c5aa1c --- /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 285d837118b..269157e54ad 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 void test() { .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 00000000000..d1e75d6278d --- /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 dfc4821cf2e..77b54554cd8 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 String toString() { 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 + '}'; } } }