package com.googlecode.d2j.tools.jar; import com.googlecode.dex2jar.tools.BaseCmd; import org.objectweb.asm.*; import org.objectweb.asm.commons.Remapper; import org.objectweb.asm.commons.RemappingClassAdapter; import java.io.*; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; import java.util.jar.Attributes.Name; import java.util.jar.JarFile; import java.util.jar.Manifest; /** * 1. Replace class A to another class B, include superclass, new for * * <pre> * class Test1 extends A ... * class Test2 implements A ... * void amethod(A a) ... * </pre> * * after * * <pre> * class Test1 extends B ... * class Test2 extends B ... * void amethod(B a) ... * </pre> * * 2. Replace method A to another method B, method B must be public static, and in either 'public static RET b(ARGs)' or * 'public RET b(Invocation inv)' RET: same return type with method A or Object ARGs: if method A is static, ARGs is * same with method A, if method A is non-static the ARGs is 'thiz, arguments in methodA' * * <pre> * public int a() { * Test t = new Test(); * return t.test(1, 2); * } * </pre> * * after * * <pre> * // direct replace * public int a(){ * Test t=new Test(); * return B(t,1,2); * } * // or by MethodInvocation * public int a(){ * Test t=new Test(); * return test_$$$_A_(t,1,2) * } * // the replaced invoke method * public static int test$$$_A_(Test t, int a, int b){ * MethodInvocation i=new MethodInvocation(t, new Object[]{a.b}) * return B(i).intValue(); * } * // the callback if MethodInvocation.proceed() is invoked * public static Object test$$$$_callback(Test t, Object[]args) { * return box(t.test(args[0].intValue(),args[1].intValue())); * } * </pre> * * 3. TODO, Replace Methods Implementations * * <pre> * public int test() { * ... * } * </pre> * * after * * <pre> * public int test(){ * MethodInvocation i=new MethodInvocation(t, new Object[]{a.b}) * return B(i).intValue(); * } * public int org_test(){ * ... * } * </pre> */ public class InvocationWeaver implements Opcodes { private static final String INVOCATION_INTERFACE = "com/googlecode/d2j/tools/jar/MethodInvocation"; private static final String DEFAULT_RET_TYPE = "Ld/$$$/j;"; private static final String DEFAULT_DESC = "(L;)" + DEFAULT_RET_TYPE; private static final Type OBJECT_TYPE = Type.getType(Object.class); private static final String BASE_INVOCATION_TYPE_FMT = "d2j/gen/MI_%03d"; List<Callback> callbacks = new ArrayList<Callback>(); int currentInvocationIdx = 0; private MtdInfo key = new MtdInfo(); private Remapper remapper = new Remapper() { @Override public String mapDesc(String desc) { if (desc.length() == 1) { return desc; } String nDesc = clzDescMap.get(desc); return nDesc == null ? desc : nDesc; } }; private Set<String> ignores = new HashSet<String>(); private Map<String, String> clzDescMap = new HashMap<String, String>(); private Map<MtdInfo, MtdInfo> mtdMap = new HashMap<MtdInfo, MtdInfo>(); static private void box(Type arg, MethodVisitor mv) { switch (arg.getSort()) { case Type.OBJECT: case Type.ARRAY: return; case Type.INT: mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;"); break; case Type.LONG: mv.visitMethodInsn(INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;"); break; case Type.FLOAT: mv.visitMethodInsn(INVOKESTATIC, "java/lang/Floag", "valueOf", "(F)Ljava/lang/Floag;"); break; case Type.DOUBLE: mv.visitMethodInsn(INVOKESTATIC, "java/lang/Double", "valueOf", "(D)Ljava/lang/Double;"); break; case Type.SHORT: mv.visitMethodInsn(INVOKESTATIC, "java/lang/Short", "valueOf", "(S)Ljava/lang/Short;"); break; case Type.CHAR: mv.visitMethodInsn(INVOKESTATIC, "java/lang/Character", "valueOf", "(C)Ljava/lang/Character;"); break; case Type.BOOLEAN: mv.visitMethodInsn(INVOKESTATIC, "java/lang/Boolean", "valueOf", "(Z)Ljava/lang/Boolean;"); break; case Type.BYTE: mv.visitMethodInsn(INVOKESTATIC, "java/lang/Byte", "valueOf", "(B)Ljava/lang/Byte;"); break; case Type.VOID: mv.visitInsn(ACONST_NULL); break; } } static private void unBox(Type orgRet, Type nRet, MethodVisitor mv) { if (orgRet.equals(nRet)) { return; } if (orgRet.getSort() == Type.VOID) { mv.visitInsn(nRet.getSize() == 1 ? POP : POP2); } if (nRet.getSort() != Type.OBJECT) { throw new RuntimeException("invalid ret type:" + nRet); } switch (orgRet.getSort()) { case Type.OBJECT: case Type.ARRAY: mv.visitTypeInsn(CHECKCAST, orgRet.getInternalName()); break; case Type.INT: mv.visitTypeInsn(CHECKCAST, "java/lang/Number"); mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Number", "intValue", "()I"); break; case Type.FLOAT: mv.visitTypeInsn(CHECKCAST, "java/lang/Number"); mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Number", "floatValue", "()F"); break; case Type.LONG: mv.visitTypeInsn(CHECKCAST, "java/lang/Number"); mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Number", "longValue", "()J"); break; case Type.DOUBLE: mv.visitTypeInsn(CHECKCAST, "java/lang/Number"); mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Number", "doubleValue", "()D"); break; case Type.BYTE: mv.visitTypeInsn(CHECKCAST, "java/lang/Number"); mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Number", "byteValue", "()B"); break; case Type.SHORT: mv.visitTypeInsn(CHECKCAST, "java/lang/Number"); mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Number", "shortValue", "()S"); break; case Type.CHAR: mv.visitTypeInsn(CHECKCAST, "java/lang/Character"); mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Character", "charValue", "()C"); break; case Type.BOOLEAN: mv.visitTypeInsn(CHECKCAST, "java/lang/Boolean"); mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Boolean", "booleanValue", "()Z"); break; } } private byte[] wave0(byte[] data) throws IOException { final ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); new ClassReader(data).accept(new RemappingClassAdapter(cw, remapper) { Map<MtdInfo, MtdInfo> toCreate = new HashMap<MtdInfo, MtdInfo>(); String clzName; @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { super.visit(version, access, name, signature, superName, interfaces); clzName = name; } public MethodVisitor visitMethod(int access, final String name, String desc, String signature, String[] exceptions) { return new MethodVisitor(Opcodes.ASM4, super.visitMethod(access, name, desc, signature, exceptions)) { @Override public void visitMethodInsn(int opcode, String owner, String name, String desc) { MtdInfo mapTo = findTargetMethod(owner, name, desc); if (mapTo != null) { boolean isStatic = opcode == INVOKESTATIC; Type orgRet = Type.getReturnType(desc); Type orgArgs[] = Type.getArgumentTypes(desc); Type nRet = Type.getReturnType(mapTo.desc); Type nArgs[] = Type.getArgumentTypes(mapTo.desc); if (orgRet.getSort() != Type.VOID && nRet.getSort() == Type.VOID) { throw new RuntimeException("can't cast " + nRet + " to " + orgRet); } if (nArgs.length == 1 && nArgs[0].getInternalName().equals(INVOCATION_INTERFACE)) { MtdInfo t = new MtdInfo(); t.owner = owner; t.name = name; t.desc = desc; MtdInfo n = newMethodA(opcode, t, mapTo); super.visitMethodInsn(INVOKESTATIC, clzName, n.name, n.desc); } else { // simple replace // checking for invalid replace if (isStatic) { if (!Arrays.deepEquals(orgArgs, nArgs)) { throw new RuntimeException("arguments not equal: " + owner + "." + name + desc + " <> " + mapTo.owner + "." + mapTo.name + mapTo.desc); } } else { if (nArgs.length != orgArgs.length + 1) { throw new RuntimeException("arguments not equal: " + owner + "." + name + desc + " <> " + mapTo.owner + "." + mapTo.name + mapTo.desc); } if (orgArgs.length > 0) { for (int i = 0; i < orgArgs.length; i++) { if (!orgArgs[i].equals(nArgs[i + 1])) { throw new RuntimeException("arguments not equal: " + owner + "." + name + desc + " <> " + mapTo.owner + "." + mapTo.name + mapTo.desc); } } } } // replace it! super.visitMethodInsn(INVOKESTATIC, mapTo.owner, mapTo.name, mapTo.desc); unBox(orgRet, nRet, this.mv); } } else { super.visitMethodInsn(opcode, owner, name, desc); } } private MtdInfo newMethodA(int opcode, MtdInfo t, MtdInfo mapTo) { MtdInfo n = toCreate.get(t); if (n != null) { return n; } n = new MtdInfo(); n.owner = t.owner; n.name = t.name + "$$$_A_"; String nDesc = t.desc; boolean hasThis = opcode != INVOKESTATIC; Type[] args = Type.getArgumentTypes(t.desc); Type ret = Type.getReturnType(t.desc); if (hasThis) { List<Type> ts = new ArrayList<>(5); ts.add(Type.getObjectType(t.owner)); ts.addAll(Arrays.asList(args)); nDesc = Type.getMethodDescriptor(ret, ts.toArray(new Type[ts.size()])); } n.desc = nDesc; toCreate.put(t, n); MethodVisitor mv = cw.visitMethod(ACC_SYNTHETIC | ACC_PRIVATE | ACC_STATIC, n.name, n.desc, null, null); mv.visitCode(); final int start; mv.visitTypeInsn(NEW, getCurrentInvocationName()); mv.visitInsn(DUP); if (hasThis) { mv.visitVarInsn(ALOAD, 0); start = 1; } else { mv.visitInsn(ACONST_NULL); start = 0; } mv.visitLdcInsn(args.length); mv.visitTypeInsn(ANEWARRAY, "java/lang/Object"); for (int i = 0; i < args.length; i++) { mv.visitInsn(DUP); mv.visitLdcInsn(i); mv.visitVarInsn(args[i].getOpcode(ILOAD), i + start); box(args[i], mv); mv.visitInsn(AASTORE); } int nextIdx = callbacks.size(); mv.visitLdcInsn(nextIdx); mv.visitMethodInsn(INVOKESPECIAL, getCurrentInvocationName(), "<init>", "(Ljava/lang/Object;[Ljava/lang/Object;I)V"); mv.visitMethodInsn(INVOKESTATIC, mapTo.owner, mapTo.name, mapTo.desc); unBox(ret, Type.getReturnType(mapTo.desc), mv); mv.visitInsn(ret.getOpcode(IRETURN)); mv.visitMaxs(-1, -1); mv.visitEnd(); Callback cb = new Callback(); cb.idx = nextIdx; cb.callback = newMethodCallback(opcode, t); cb.target = t; cb.isSpecial = opcode == INVOKESPECIAL; cb.isStatic = opcode == INVOKESTATIC; callbacks.add(cb); return n; } private MtdInfo newMethodCallback(int opcode, MtdInfo t) { MtdInfo n = new MtdInfo(); n.owner = className; n.name = t.name + "$$$$_callback"; if (opcode == INVOKESPECIAL || opcode == INVOKESTATIC) { n.desc = "([Ljava/lang/Object;)Ljava/lang/Object;"; } else { n.desc = "(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;"; } MethodVisitor mv = cw.visitMethod(opcode == INVOKESPECIAL ? ACC_PUBLIC : ACC_PUBLIC | ACC_STATIC, n.name, n.desc, null, null); mv.visitCode(); int start; if (opcode != INVOKESTATIC) { mv.visitVarInsn(ALOAD, 0); if (opcode != INVOKESPECIAL) { mv.visitTypeInsn(CHECKCAST, t.owner); } start = 1; } else { start = 0; } Type[] args = Type.getArgumentTypes(t.desc); for (int i = 0; i < args.length; i++) { mv.visitVarInsn(ALOAD, start); mv.visitLdcInsn(i); mv.visitInsn(AALOAD); unBox(args[i], OBJECT_TYPE, mv); } mv.visitMethodInsn(opcode, t.owner, t.name, t.desc); Type ret = Type.getReturnType(t.desc); box(ret, mv); mv.visitInsn(ARETURN); mv.visitMaxs(-1, -1); mv.visitEnd(); return n; } }; } }, ClassReader.EXPAND_FRAMES); return cw.toByteArray(); } private MtdInfo findTargetMethod(String owner, String name, String desc) { key.name = name; key.owner = owner; key.desc = desc; MtdInfo v = mtdMap.get(key); if (v != null) { return v; } // try with default ret key.desc = Type.getMethodDescriptor(Type.getType(DEFAULT_RET_TYPE), Type.getArgumentTypes(desc)); v = mtdMap.get(key); if (v != null) { return v; } // try with default desc key.desc = DEFAULT_DESC; v = mtdMap.get(key); return v; } public void wave(Path from, final Path to) throws IOException { BaseCmd.walkJarOrDir(from, new BaseCmd.FileVisitorX() { @Override public void visitFile(Path file, Path relative) throws IOException { String name = relative.toString(); Path targetPath = to.resolve(relative); BaseCmd.createParentDirectories(targetPath); if (name.endsWith(".class")) { String clzName = name.substring(0, name.length() - ".class".length()); if (ignores.contains(clzName)) { Files.copy(file, targetPath); } else { byte[] out = wave0(Files.readAllBytes(file)); Files.write(targetPath, out); } } else { if (name.startsWith("META-INF/")) { if (name.equals(JarFile.MANIFEST_NAME)) { try (InputStream in = Files.newInputStream(file)) { Manifest mf = new Manifest(in); mf.getMainAttributes().put(new Name("X-NOTICE"), "Modified"); mf.getEntries().clear(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); mf.write(baos); baos.flush(); Files.write(targetPath, baos.toByteArray()); } } else if (name.endsWith(".DSA") || name.endsWith(".RSA") || name.endsWith(".SF") || name.endsWith(".ECDSA")) { // ignored } else { Files.copy(file, targetPath); } } else { Files.copy(file, targetPath); } } } }); if (callbacks.size() > 0) { String type = getCurrentInvocationName(); byte[] data = buildInvocationClz(type); Path target = to.resolve(type + ".class"); BaseCmd.createParentDirectories(target); Files.write(target, data); nextInvocationName(); } } private byte[] buildInvocationClz(String typeName) { ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); cw.visit(V1_6, ACC_PUBLIC, typeName, null, "java/lang/Object", new String[] { INVOCATION_INTERFACE }); cw.visitField(ACC_PRIVATE | ACC_FINAL, "thiz", "Ljava/lang/Object;", null, null).visitEnd(); cw.visitField(ACC_PRIVATE | ACC_FINAL, "args", "[Ljava/lang/Object;", null, null).visitEnd(); cw.visitField(ACC_PRIVATE | ACC_FINAL, "idx", "I", null, null).visitEnd(); { MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "(Ljava/lang/Object;[Ljava/lang/Object;I)V", null, null); mv.visitCode(); mv.visitVarInsn(ALOAD, 0); mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V"); mv.visitVarInsn(ALOAD, 0); mv.visitVarInsn(ALOAD, 1); mv.visitFieldInsn(PUTFIELD, typeName, "thiz", "Ljava/lang/Object;"); mv.visitVarInsn(ALOAD, 0); mv.visitVarInsn(ALOAD, 2); mv.visitFieldInsn(PUTFIELD, typeName, "args", "[Ljava/lang/Object;"); mv.visitVarInsn(ALOAD, 0); mv.visitVarInsn(ILOAD, 3); mv.visitFieldInsn(PUTFIELD, typeName, "idx", "I"); mv.visitInsn(RETURN); mv.visitMaxs(-1, -1); mv.visitEnd(); } { MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "getMethodOwner", "()Ljava/lang/String;", null, null); mv.visitCode(); mv.visitVarInsn(ALOAD, 0); mv.visitFieldInsn(GETFIELD, typeName, "idx", "I"); Label def = new Label(); Label[] labels = new Label[callbacks.size()]; for (int i = 0; i < labels.length; i++) { labels[i] = new Label(); } mv.visitTableSwitchInsn(0, callbacks.size() - 1, def, labels); for (int i = 0; i < labels.length; i++) { mv.visitLabel(labels[i]); Callback cb = callbacks.get(i); MtdInfo m = cb.target; mv.visitLdcInsn("L" + m.owner + ";"); mv.visitInsn(ARETURN); } mv.visitLabel(def); mv.visitTypeInsn(NEW, "java/lang/RuntimeException"); mv.visitInsn(DUP); mv.visitLdcInsn("invalid idx"); mv.visitMethodInsn(INVOKESPECIAL, "java/lang/RuntimeException", "<init>", "(Ljava/lang/String;)V"); mv.visitInsn(ATHROW); mv.visitMaxs(-1, -1); mv.visitEnd(); } { MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "getMethodName", "()Ljava/lang/String;", null, null); mv.visitCode(); mv.visitVarInsn(ALOAD, 0); mv.visitFieldInsn(GETFIELD, typeName, "idx", "I"); Label def = new Label(); Label[] labels = new Label[callbacks.size()]; for (int i = 0; i < labels.length; i++) { labels[i] = new Label(); } mv.visitTableSwitchInsn(0, callbacks.size() - 1, def, labels); for (int i = 0; i < labels.length; i++) { mv.visitLabel(labels[i]); Callback cb = callbacks.get(i); MtdInfo m = cb.target; mv.visitLdcInsn(m.name); mv.visitInsn(ARETURN); } mv.visitLabel(def); mv.visitTypeInsn(NEW, "java/lang/RuntimeException"); mv.visitInsn(DUP); mv.visitLdcInsn("invalid idx"); mv.visitMethodInsn(INVOKESPECIAL, "java/lang/RuntimeException", "<init>", "(Ljava/lang/String;)V"); mv.visitInsn(ATHROW); mv.visitMaxs(-1, -1); mv.visitEnd(); } { MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "getMethodDesc", "()Ljava/lang/String;", null, null); mv.visitCode(); mv.visitVarInsn(ALOAD, 0); mv.visitFieldInsn(GETFIELD, typeName, "idx", "I"); Label def = new Label(); Label[] labels = new Label[callbacks.size()]; for (int i = 0; i < labels.length; i++) { labels[i] = new Label(); } mv.visitTableSwitchInsn(0, callbacks.size() - 1, def, labels); for (int i = 0; i < labels.length; i++) { mv.visitLabel(labels[i]); Callback cb = callbacks.get(i); MtdInfo m = cb.target; mv.visitLdcInsn(m.desc); mv.visitInsn(ARETURN); } mv.visitLabel(def); mv.visitTypeInsn(NEW, "java/lang/RuntimeException"); mv.visitInsn(DUP); mv.visitLdcInsn("invalid idx"); mv.visitMethodInsn(INVOKESPECIAL, "java/lang/RuntimeException", "<init>", "(Ljava/lang/String;)V"); mv.visitInsn(ATHROW); mv.visitMaxs(-1, -1); mv.visitEnd(); } { MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "getArguments", "()[Ljava/lang/Object;", null, null); mv.visitCode(); mv.visitVarInsn(ALOAD, 0); mv.visitFieldInsn(GETFIELD, typeName, "args", "[Ljava/lang/Object;"); mv.visitInsn(ARETURN); mv.visitMaxs(-1, -1); mv.visitEnd(); } { MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "getThis", "()Ljava/lang/Object;", null, null); mv.visitCode(); mv.visitVarInsn(ALOAD, 0); mv.visitFieldInsn(GETFIELD, typeName, "thiz", "Ljava/lang/Object;"); mv.visitInsn(ARETURN); mv.visitMaxs(-1, -1); mv.visitEnd(); } { MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "proceed", "()Ljava/lang/Object;", null, new String[] { "java/lang/Throwable" }); mv.visitCode(); mv.visitVarInsn(ALOAD, 0); mv.visitFieldInsn(GETFIELD, typeName, "idx", "I"); Label def = new Label(); Label[] labels = new Label[callbacks.size()]; for (int i = 0; i < labels.length; i++) { labels[i] = new Label(); } mv.visitTableSwitchInsn(0, callbacks.size() - 1, def, labels); for (int i = 0; i < labels.length; i++) { mv.visitLabel(labels[i]); Callback cb = callbacks.get(i); MtdInfo m = cb.callback; if (cb.isStatic) { mv.visitVarInsn(ALOAD, 0); mv.visitFieldInsn(GETFIELD, typeName, "args", "[Ljava/lang/Object;"); mv.visitMethodInsn(INVOKESTATIC, m.owner, m.name, m.desc); } else if (cb.isSpecial) { mv.visitVarInsn(ALOAD, 0); mv.visitFieldInsn(GETFIELD, typeName, "thiz", "Ljava/lang/Object;"); mv.visitTypeInsn(CHECKCAST, m.owner); mv.visitVarInsn(ALOAD, 0); mv.visitFieldInsn(GETFIELD, typeName, "args", "[Ljava/lang/Object;"); mv.visitMethodInsn(INVOKEVIRTUAL, m.owner, m.name, m.desc); } else { mv.visitVarInsn(ALOAD, 0); mv.visitFieldInsn(GETFIELD, typeName, "thiz", "Ljava/lang/Object;"); mv.visitVarInsn(ALOAD, 0); mv.visitFieldInsn(GETFIELD, typeName, "args", "[Ljava/lang/Object;"); mv.visitMethodInsn(INVOKESTATIC, m.owner, m.name, m.desc); } Type ret = Type.getReturnType(m.desc); box(ret, mv); mv.visitInsn(ret.getOpcode(IRETURN)); } mv.visitLabel(def); mv.visitTypeInsn(NEW, "java/lang/RuntimeException"); mv.visitInsn(DUP); mv.visitLdcInsn("invalid idx"); mv.visitMethodInsn(INVOKESPECIAL, "java/lang/RuntimeException", "<init>", "(Ljava/lang/String;)V"); mv.visitInsn(ATHROW); mv.visitMaxs(-1, -1); mv.visitEnd(); } return cw.toByteArray(); } public InvocationWeaver withConfig(Path is) throws IOException { return withConfig(Files.readAllLines(is, StandardCharsets.UTF_8)); } public InvocationWeaver withConfig(InputStream is) throws IOException { try (BufferedReader r = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { List<String> list = new ArrayList<>(); for (String ln = r.readLine(); ln != null; ln = r.readLine()) { list.add(ln); } return withConfig(list); } } public InvocationWeaver withConfig(List<String> lines) throws IOException { for (String ln : lines) { if ("".equals(ln) || ln.startsWith("#")) { continue; } switch (ln.charAt(0)) { case 'i': case 'I': ignores.add(ln.substring(2)); break; case 'c': case 'C': int index = ln.lastIndexOf('='); if (index > 0) { String key = toInternal(ln.substring(2, index)); String value = toInternal(ln.substring(index + 1)); clzDescMap.put(key, value); ignores.add(value); } break; case 'R': case 'r': index = ln.lastIndexOf('='); if (index > 0) { String key = ln.substring(2, index); String value = ln.substring(index + 1); MtdInfo mi = buildMethodInfo(key); index = value.indexOf('.'); MtdInfo mtdValue = new MtdInfo(); mtdValue.owner = toInternal(value.substring(0, index)); int index2 = value.indexOf('(', index); mtdValue.name = value.substring(index + 1, index2); mtdValue.desc = value.substring(index2); mtdMap.put(mi, mtdValue); } break; } } return this; } private String toInternal(String key) { if (key.endsWith(";")) { key = key.substring(1, key.length() - 1); } return key; } private MtdInfo buildMethodInfo(String value) { int index = value.indexOf('.'); MtdInfo mtdValue = new MtdInfo(); mtdValue.owner = toInternal(value.substring(0, index)); int index2 = value.indexOf('(', index); if (index2 >= 0) { mtdValue.name = value.substring(index + 1, index2); int index3 = value.indexOf(')'); if (index3 == value.length() - 1) { mtdValue.desc = value.substring(index2) + DEFAULT_RET_TYPE; } else { mtdValue.desc = value.substring(index2); } } else { mtdValue.name = value.substring(index + 1); mtdValue.desc = DEFAULT_DESC; } return mtdValue; } public String getCurrentInvocationName() { return String.format(BASE_INVOCATION_TYPE_FMT, currentInvocationIdx); } private void nextInvocationName() { currentInvocationIdx++; callbacks.clear(); } static class Callback { int idx; MtdInfo callback; MtdInfo target; boolean isSpecial; boolean isStatic; } public static class MtdInfo { public String desc; public String name; public String owner; @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; MtdInfo mtdInfo = (MtdInfo) o; if (!desc.equals(mtdInfo.desc)) return false; if (!name.equals(mtdInfo.name)) return false; if (!owner.equals(mtdInfo.owner)) return false; return true; } @Override public int hashCode() { int result = desc.hashCode(); result = 31 * result + name.hashCode(); result = 31 * result + owner.hashCode(); return result; } } }