diff --git a/base/src/main/java/proguard/classfile/util/ConstructorInvocationOffsetFinder.java b/base/src/main/java/proguard/classfile/util/ConstructorInvocationOffsetFinder.java new file mode 100644 index 000000000..159032603 --- /dev/null +++ b/base/src/main/java/proguard/classfile/util/ConstructorInvocationOffsetFinder.java @@ -0,0 +1,111 @@ +package proguard.classfile.util; + +import proguard.classfile.ClassConstants; +import proguard.classfile.Clazz; +import proguard.classfile.Method; +import proguard.classfile.ProgramClass; +import proguard.classfile.ProgramMethod; +import proguard.classfile.attribute.Attribute; +import proguard.classfile.attribute.CodeAttribute; +import proguard.classfile.attribute.visitor.AttributeNameFilter; +import proguard.classfile.attribute.visitor.AttributeVisitor; +import proguard.classfile.constant.Constant; +import proguard.classfile.constant.MethodrefConstant; +import proguard.classfile.constant.visitor.ConstantVisitor; +import proguard.classfile.instruction.ConstantInstruction; +import proguard.classfile.instruction.Instruction; +import proguard.classfile.instruction.visitor.AllInstructionVisitor; +import proguard.classfile.instruction.visitor.InstructionOpCodeFilter; +import proguard.classfile.instruction.visitor.InstructionVisitor; +import proguard.classfile.visitor.MemberVisitor; + +/** + * This utility class finds the offset of the invocation to the current or super class constructor + * after visiting an method. + * + * @author Kymeng Tang + */ +public class ConstructorInvocationOffsetFinder implements MemberVisitor { + private int initOffset = -1; + + public int getConstructorCallOffset() { + assert initOffset != -1 + : "The constructor invocation offset is being requested before visiting any member " + + "after instantiation or resetting."; + return initOffset; + } + + public void reset() { + this.initOffset = -1; + } + + // MemberVisitor implementation + @Override + public void visitProgramMethod(ProgramClass programClass, ProgramMethod programMethod) { + assert programMethod.getName(programClass).equals(ClassConstants.METHOD_NAME_INIT) + : this.getClass().getName() + + " only supports constructor but " + + programClass.getName() + + "." + + programMethod.getName(programClass) + + programMethod.getDescriptor(programClass) + + " is being visited."; + + assert initOffset == -1 + : "This instance of " + + this.getClass().getName() + + " has already visited an member; " + + "To avoid overriding the previously found offset, please store the return value of " + + "getConstructorCallOffset(), and call reset() method."; + + programMethod.attributesAccept( + programClass, new AttributeNameFilter(Attribute.CODE, new ConstructorOffsetFinderImpl())); + } + + private class ConstructorOffsetFinderImpl + implements AttributeVisitor, InstructionVisitor, ConstantVisitor { + // AttributeVisitor implementation + @Override + public void visitAnyAttribute(Clazz clazz, Attribute attribute) {} + + @Override + public void visitCodeAttribute(Clazz clazz, Method method, CodeAttribute codeAttribute) { + codeAttribute.accept( + clazz, + method, + new AllInstructionVisitor( + new InstructionOpCodeFilter(new int[] {Instruction.OP_INVOKESPECIAL}, this))); + } + + // InstructionVisitor implementation + @Override + public void visitAnyInstruction( + Clazz clazz, + Method method, + CodeAttribute codeAttribute, + int offset, + Instruction instruction) {} + + @Override + public void visitConstantInstruction( + Clazz clazz, + Method method, + CodeAttribute codeAttribute, + int offset, + ConstantInstruction constantInstruction) { + clazz.constantPoolEntryAccept( + constantInstruction.constantIndex, + new ConstantVisitor() { + @Override + public void visitAnyConstant(Clazz clazz, Constant constant) {} + + @Override + public void visitMethodrefConstant(Clazz clazz, MethodrefConstant methodrefConstant) { + if (methodrefConstant.getName(clazz).equals(ClassConstants.METHOD_NAME_INIT)) { + initOffset = offset; + } + } + }); + } + } +} diff --git a/base/src/main/java/proguard/classfile/util/inject/CodeInjector.java b/base/src/main/java/proguard/classfile/util/inject/CodeInjector.java new file mode 100644 index 000000000..0d9bbf3f3 --- /dev/null +++ b/base/src/main/java/proguard/classfile/util/inject/CodeInjector.java @@ -0,0 +1,229 @@ +package proguard.classfile.util.inject; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; +import proguard.classfile.AccessConstants; +import proguard.classfile.ClassConstants; +import proguard.classfile.Clazz; +import proguard.classfile.Method; +import proguard.classfile.ProgramClass; +import proguard.classfile.ProgramMethod; +import proguard.classfile.attribute.Attribute; +import proguard.classfile.attribute.CodeAttribute; +import proguard.classfile.attribute.visitor.AllAttributeVisitor; +import proguard.classfile.attribute.visitor.AttributeNameFilter; +import proguard.classfile.attribute.visitor.AttributeVisitor; +import proguard.classfile.editor.CodeAttributeEditor; +import proguard.classfile.editor.InstructionSequenceBuilder; +import proguard.classfile.instruction.Instruction; +import proguard.classfile.util.ClassUtil; +import proguard.classfile.util.InternalTypeEnumeration; +import proguard.classfile.util.inject.argument.InjectedArgument; +import proguard.classfile.util.inject.location.InjectStrategy; + +/** + * This utility class allow for injecting a method invocation instruction optionally with arguments + * modeled by instances of classes implementing {@link InjectedArgument} interface to the specified + * target method at an offset determined by the implementation of the {@link InjectStrategy} + * interface. + * + *

Example usage: new CodeInjector() .injectInvokeStatic(logUtilClass, logDebugMethod, new + * ConstantPrimitive(1), new ConstantString("Hello world")) .into(MainProgramClass, + * mainMethod) .at(new FirstBlock()) .commit(); + * + * @author Kymeng Tang + */ +public class CodeInjector { + private List targets; + private ClassMethodPair content; + private InjectStrategy injectStrategy; + private List arguments = new ArrayList<>(); + + /** + * Specify the static method to be invoked. + * + * @param clazz The class in which the static method belongs to. + * @param method The method to be invoked. + */ + public CodeInjector injectInvokeStatic(Clazz clazz, Method method) { + assert content == null + : "The injection content: `" + + renderInjectionContent(content.clazz, content.method, arguments) + + "` " + + "has already been specified."; + + assert (method.getAccessFlags() & AccessConstants.STATIC) != 0 + && !method.getName(clazz).equals(ClassConstants.METHOD_NAME_CLINIT) + : "The method to be injected must be a (non-class initializer) static method."; + + content = new ClassMethodPair(clazz, method); + return this; + } + + /** + * Specify the static method to be invoked. + * + * @param clazz The class in which the static method belongs to. + * @param method The method to be invoked. + * @param arguments a list of arguments to be passed to the method to be invoked. + */ + public CodeInjector injectInvokeStatic( + Clazz clazz, Method method, InjectedArgument... arguments) { + injectInvokeStatic(clazz, method); + + InternalTypeEnumeration parametersIterator = + new InternalTypeEnumeration(method.getDescriptor(clazz)); + Iterator argumentsIterator = Arrays.stream(arguments).iterator(); + + while (parametersIterator.hasNext() || argumentsIterator.hasNext()) { + String expectedType = parametersIterator.next(); + InjectedArgument provided = argumentsIterator.next(); + + assert expectedType.equals(provided.getInternalType()) + : String.format( + "Provided argument: `%s` doesn't match the expected parameter type: %s for method: ", + argumentsIterator, + expectedType, + renderMethodSignature(content.clazz, content.method)); + } + this.arguments = Arrays.asList(arguments); + + return this; + } + + /** + * Specify the method where a static method invocation shall be injected into. + * + * @param programClass The program class that has the method where a static method invocation + * shall be injected into. + * @param programMethod the method where a static method invocation shall be injected into. + */ + public CodeInjector into(ProgramClass programClass, ProgramMethod programMethod) { + assert targets == null : "The injection target has already been specified."; + + targets = Arrays.asList(new ClassMethodPair(programClass, programMethod)); + return this; + } + + /** + * Specify the location in which the invoke instruction should be injected into. + * + * @param injectStrategy The implementation of InjectStrategy interface which determine the offset + * to inject the invoke instruction. + * @return + */ + public CodeInjector at(InjectStrategy injectStrategy) { + assert this.injectStrategy == null + : "The injection strategy: " + injectStrategy + " has already been specified."; + + this.injectStrategy = injectStrategy; + return this; + } + + /** + * Apply the invoke instruction in accordance to the specifications provided via the + * `.injectInvokeStatic(...)`, `.into(...)` and `at(...)` method. + */ + public void commit() { + assert content != null + : "The injection content hasn't been provided; please use `.injectInvokeStatic(...)` " + + "to indicate the method invocation to be injected."; + + assert targets != null + : "The injection target hasn't been provided; please use `.into(...)` to indicate the method targeted for " + + "injecting " + + renderInjectionContent(content.clazz, content.method, arguments) + + "."; + + assert injectStrategy != null + : "The injection location hasn't been provided. please use `.at(...)` to indicate the place to inject " + + renderInjectionContent(content.clazz, content.method, arguments) + + " into the target method."; + + targets.forEach( + target -> { + CodeAttributeEditor editor = new CodeAttributeEditor(); + InstructionSequenceBuilder code = + new InstructionSequenceBuilder((ProgramClass) target.clazz); + + arguments.forEach( + argument -> + code.pushPrimitiveOrString(argument.getValue(), argument.getInternalType())); + code.invokestatic(content.clazz, content.method); + + target.method.accept( + target.clazz, + new AllAttributeVisitor( + new AttributeNameFilter( + Attribute.CODE, new InstructionInjector(editor, code, injectStrategy)))); + }); + } + + // Internal utility methods + private static String renderMethodSignature(Clazz clazz, Method method) { + return ClassUtil.externalFullMethodDescription( + clazz.getName(), + method.getAccessFlags(), + method.getName(clazz), + method.getDescriptor(clazz)); + } + + private static String renderInjectionContent( + Clazz clazz, Method method, List arguments) { + return new StringBuilder() + .append(clazz.getName()) + .append(method.getName(clazz)) + .append("(") + .append(arguments.stream().map(Object::toString).collect(Collectors.joining(","))) + .append("):") + .append(ClassUtil.externalMethodReturnType(method.getDescriptor(clazz))) + .toString(); + } + + // Internal utility classes + private static class InstructionInjector implements AttributeVisitor { + private final CodeAttributeEditor editor; + private final InstructionSequenceBuilder code; + private final InjectStrategy injectStrategy; + + private InstructionInjector( + CodeAttributeEditor editor, + InstructionSequenceBuilder code, + InjectStrategy injectStrategy) { + this.editor = editor; + this.code = code; + this.injectStrategy = injectStrategy; + } + + @Override + public void visitCodeAttribute(Clazz clazz, Method method, CodeAttribute codeAttribute) { + editor.reset(codeAttribute.u4codeLength); + InjectStrategy.InjectLocation[] injectLocations = + injectStrategy.getAllSuitableInjectionLocation( + (ProgramClass) clazz, (ProgramMethod) method); + for (InjectStrategy.InjectLocation location : injectLocations) { + final BiConsumer inserter = + location.shouldInjectBefore() + ? editor::insertBeforeOffset + : editor::insertAfterInstruction; + + inserter.accept(location.getOffset(), code.instructions()); + } + codeAttribute.accept(clazz, method, editor); + } + } + + private static class ClassMethodPair { + public Clazz clazz; + public Method method; + + public ClassMethodPair(Clazz clazz, Method method) { + this.clazz = clazz; + this.method = method; + } + } +} diff --git a/base/src/main/java/proguard/classfile/util/inject/argument/ConstantPrimitive.java b/base/src/main/java/proguard/classfile/util/inject/argument/ConstantPrimitive.java new file mode 100644 index 000000000..e3101bf2f --- /dev/null +++ b/base/src/main/java/proguard/classfile/util/inject/argument/ConstantPrimitive.java @@ -0,0 +1,51 @@ +package proguard.classfile.util.inject.argument; + +import proguard.classfile.util.ClassUtil; + +/** + * A model representing a constant value of primitive typed argument to be passed to the method + * invocation instructions that are injected by {@link proguard.classfile.util.inject.CodeInjector}. + * + * @author Kymeng Tang + */ +public class ConstantPrimitive implements InjectedArgument { + private final T numericConstant; + + public ConstantPrimitive(T constant) { + this.numericConstant = constant; + } + + @Override + public Object getValue() { + return numericConstant; + } + + @Override + public String getInternalType() { + switch (numericConstant.getClass().getName()) { + case "java.lang.Boolean": + return "Z"; + case "java.lang.Byte": + return "B"; + case "java.lang.Character": + return "C"; + case "java.lang.Short": + return "S"; + case "java.lang.Integer": + return "I"; + case "java.lang.Long": + return "J"; + case "java.lang.Float": + return "F"; + case "java.lang.Double": + return "D"; + default: + throw new RuntimeException("Unexpected type"); + } + } + + @Override + public String toString() { + return numericConstant.toString() + ":" + ClassUtil.externalType(getInternalType()); + } +} diff --git a/base/src/main/java/proguard/classfile/util/inject/argument/ConstantString.java b/base/src/main/java/proguard/classfile/util/inject/argument/ConstantString.java new file mode 100644 index 000000000..dd2d79850 --- /dev/null +++ b/base/src/main/java/proguard/classfile/util/inject/argument/ConstantString.java @@ -0,0 +1,36 @@ +package proguard.classfile.util.inject.argument; + +import proguard.classfile.ClassConstants; + +/** + * A model representing a constant string argument to be passed to the method invocation + * instructions that are injected by {@link proguard.classfile.util.inject.CodeInjector}. + * + * @author Kymeng Tang + */ +public class ConstantString implements InjectedArgument { + private final String constantString; + + public ConstantString(String constant) { + this.constantString = constant; + } + + public String getConstant() { + return this.constantString; + } + + @Override + public Object getValue() { + return constantString; + } + + @Override + public String getInternalType() { + return ClassConstants.TYPE_JAVA_LANG_STRING; + } + + @Override + public String toString() { + return String.format("\"%s\":String", constantString); + } +} diff --git a/base/src/main/java/proguard/classfile/util/inject/argument/InjectedArgument.java b/base/src/main/java/proguard/classfile/util/inject/argument/InjectedArgument.java new file mode 100644 index 000000000..a05316531 --- /dev/null +++ b/base/src/main/java/proguard/classfile/util/inject/argument/InjectedArgument.java @@ -0,0 +1,15 @@ +package proguard.classfile.util.inject.argument; + +/** + * This interface defines api for modeling arguments to be passed to the method invocation + * instructions that are injected by {@link proguard.classfile.util.inject.CodeInjector}. + * + * @author Kymeng Tang + */ +public interface InjectedArgument { + // A getter that returns a boxed value of the argument. + Object getValue(); + + // A getter indicating the internal JVM type that describes the argument. + String getInternalType(); +} diff --git a/base/src/main/java/proguard/classfile/util/inject/location/FirstBlock.java b/base/src/main/java/proguard/classfile/util/inject/location/FirstBlock.java new file mode 100644 index 000000000..dfdbeb39a --- /dev/null +++ b/base/src/main/java/proguard/classfile/util/inject/location/FirstBlock.java @@ -0,0 +1,54 @@ +package proguard.classfile.util.inject.location; + +import proguard.classfile.ClassConstants; +import proguard.classfile.ProgramClass; +import proguard.classfile.ProgramMethod; +import proguard.classfile.util.ConstructorInvocationOffsetFinder; + +/** + * An implementation of the InjectStrategy interface to find the earliest location suitable for + * injecting method invocation instructions. + */ +public class FirstBlock implements InjectStrategy { + ConstructorInvocationOffsetFinder offsetFinder = new ConstructorInvocationOffsetFinder(); + + /** + * Find the first offset to inject a method invocation. If the target method is a constructor, the + * offset is right after the `invokespecial` to the constructor of the current class or its super + * class. + * + * @param targetClass The class holding the method in which a method invocation shall be injected + * into. + * @param targetMethod the target method to have a method invocation injected into. + * @return An InjectLocation instance indicating the first offset suitable for injection. + */ + @Override + public InjectLocation getSingleInjectionLocation( + ProgramClass targetClass, ProgramMethod targetMethod) { + boolean isConstructor = + targetMethod.getName(targetClass).equals(ClassConstants.METHOD_NAME_INIT); + if (isConstructor) { + targetMethod.accept(targetClass, offsetFinder); + } + return isConstructor + ? new InjectLocation(offsetFinder.getConstructorCallOffset(), false) + : new InjectLocation(0, true); + } + + /** + * Find the first offset to inject a method invocation. If the target method is a constructor, the + * offset is right after the `invokespecial` to the constructor of the current class or its super + * class. + * + * @param targetClass The class holding the method in which a method invocation shall be injected + * into. + * @param targetMethod the target method to have a method invocation injected into. + * @return An array of one InjectLocation instance indicating the first offset suitable for + * injection. + */ + @Override + public InjectLocation[] getAllSuitableInjectionLocation( + ProgramClass targetClass, ProgramMethod targetMethod) { + return new InjectLocation[] {getSingleInjectionLocation(targetClass, targetMethod)}; + } +} diff --git a/base/src/main/java/proguard/classfile/util/inject/location/InjectStrategy.java b/base/src/main/java/proguard/classfile/util/inject/location/InjectStrategy.java new file mode 100644 index 000000000..c0c7ff11a --- /dev/null +++ b/base/src/main/java/proguard/classfile/util/inject/location/InjectStrategy.java @@ -0,0 +1,58 @@ +package proguard.classfile.util.inject.location; + +import proguard.classfile.ProgramClass; +import proguard.classfile.ProgramMethod; + +/** + * This interface defines methods for determining code attribute offsets suitable for injecting a + * method invocation. + * + * @author Kymeng Tang + */ +public interface InjectStrategy { + /** + * Determine one location (i.e., offset) suitable for injecting a method invocation. + * + * @param targetClass The class holding the method in which a method invocation shall be injected + * into. + * @param targetMethod the target method to have a method invocation injected into. + * @return An InjectLocation instance indicating an offset suitable for injection and whether the + * injection shall happen before or after the offset. + */ + InjectLocation getSingleInjectionLocation(ProgramClass targetClass, ProgramMethod targetMethod); + /** + * Determine all locations (i.e., offsets) suitable for injecting a method invocation. + * + * @param targetClass The class holding the method in which a method invocation shall be injected + * into. + * @param targetMethod the target method to have a method invocation injected into. + * @return An array of InjectLocation instances indicating offsets suitable for injection and + * whether the injection shall take place before or after each offset. + */ + InjectLocation[] getAllSuitableInjectionLocation( + ProgramClass targetClass, ProgramMethod targetMethod); + + /** + * A data structure indicating a suitable location for injecting a method invocation instruction. + */ + class InjectLocation { + // Instruction offset in the code attribute. + private final int offset; + // Indicate whether an invocation instruction shall be injected before an offset specified in + // the offset field. + private final boolean shouldInjectBefore; + + public InjectLocation(int offset, boolean shouldInjectBefore) { + this.offset = offset; + this.shouldInjectBefore = shouldInjectBefore; + } + + public int getOffset() { + return offset; + } + + public boolean shouldInjectBefore() { + return shouldInjectBefore; + } + } +} diff --git a/base/src/test/kotlin/proguard/classfile/editor/CodeInjectorTest.kt b/base/src/test/kotlin/proguard/classfile/editor/CodeInjectorTest.kt new file mode 100644 index 000000000..28df10645 --- /dev/null +++ b/base/src/test/kotlin/proguard/classfile/editor/CodeInjectorTest.kt @@ -0,0 +1,253 @@ +package proguard.classfile.editor + +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.string.shouldContain +import proguard.classfile.ClassConstants +import proguard.classfile.ProgramClass +import proguard.classfile.ProgramMethod +import proguard.classfile.util.inject.CodeInjector +import proguard.classfile.util.inject.argument.ConstantPrimitive +import proguard.classfile.util.inject.argument.ConstantString +import proguard.classfile.util.inject.location.FirstBlock +import proguard.classfile.visitor.ClassPrinter +import proguard.testutils.ClassPoolBuilder +import proguard.testutils.JavaSource +import java.io.PrintWriter +import java.io.StringWriter + +class CodeInjectorTest : BehaviorSpec({ + Given("a class targeted for injection and a class holding the injection content") { + val (programClassPool, _) = ClassPoolBuilder.fromSource( + JavaSource( + "InjectionTarget.java", + """ + public class InjectionTarget { + public InjectionTarget() {} + public InjectionTarget(int dummy) {} + public InjectionTarget(int dummyInt, float dummyFloat) { + this(dummyInt); + } + public static void main(String... args) {} + public int instanceMethod() { + return 0; + } + } + """.trimIndent(), + ), + JavaSource( + "InjectContent.java", + """ + public class InjectContent { + public static void log() { + System.out.println("log called"); + } + public static void logPrimitive(int intData, double doubleData) { + System.out.println(String.format("log param: %d:int, %d:double", intData, doubleData)); + } + public static void logString(String stringData) { + System.out.println(String.format("log param: %s:String", stringData)); + } + public static void logMixed(String stringData, int intData, double doubleData) { + System.out.println(String.format("log param: %s:String, %d:intData, %d:doubleData", stringData, intData, doubleData)); + } + } + """.trimIndent(), + ), + ) + val injectTargetClass = programClassPool.getClass("InjectionTarget") as ProgramClass + val injectContentClass = programClassPool.getClass("InjectContent") + + When("Injecting InjectContent.log() into static method InjectTarget.main()") { + val targetMethod = injectTargetClass.findMethod("main", "([Ljava/lang/String;)V") as ProgramMethod + CodeInjector() + .injectInvokeStatic(injectContentClass, injectContentClass.findMethod("log", "()V")) + .into(injectTargetClass, targetMethod) + .at(FirstBlock()) + .commit() + + Then("InjectTarget.main() should start with InjectContent.log()") { + val renderedMethod = StringWriter() + targetMethod.accept(injectTargetClass, ClassPrinter(PrintWriter(renderedMethod))) + + renderedMethod.toString() shouldContain Regex( + """ + \s*\[0\] invokestatic #\d+ = Methodref\(InjectContent\.log\(\)V\) + """.trimIndent(), + ) + } + } + + When("Injecting InjectContent.logPrimitive(...) into static method InjectTarget.main()") { + val targetMethod = injectTargetClass.findMethod("main", "([Ljava/lang/String;)V") as ProgramMethod + CodeInjector() + .injectInvokeStatic( + injectContentClass, + injectContentClass.findMethod("logPrimitive", "(ID)V"), + ConstantPrimitive(99), + ConstantPrimitive(42.24), + ) + .into(injectTargetClass, targetMethod) + .at(FirstBlock()) + .commit() + + Then("The first instruction of InjectTarget.main() is InjectContent.logPrimitive(99, 42.24)") { + val renderedMethod = StringWriter() + targetMethod.accept(injectTargetClass, ClassPrinter(PrintWriter(renderedMethod))) + + renderedMethod.toString() shouldContain Regex( + """ + \s*\[0\] bipush 99 + \s*\[\d+\] ldc2_w #\d+ = Double\(42.24\) + \s*\[\d+\] invokestatic #\d+ = Methodref\(InjectContent\.logPrimitive\(ID\)V\) + """.trimIndent(), + ) + } + } + + When("Injecting InjectContent.logString(...) into static method InjectTarget.main()") { + val targetMethod = injectTargetClass.findMethod("main", "([Ljava/lang/String;)V") as ProgramMethod + CodeInjector() + .injectInvokeStatic( + injectContentClass, + injectContentClass.findMethod("logString", "(Ljava/lang/String;)V"), + ConstantString("foo"), + ) + .into(injectTargetClass, targetMethod) + .at(FirstBlock()) + .commit() + + Then("The first instruction is InjectContent.logString(\"foo\")") { + val renderedMethod = StringWriter() + targetMethod.accept(injectTargetClass, ClassPrinter(PrintWriter(renderedMethod))) + + renderedMethod.toString() shouldContain Regex( + """ + \s*\[0\] ldc #\d+ = String\("foo"\) + \s*\[\d+\] invokestatic #\d+ = Methodref\(InjectContent\.logString\(Ljava/lang/String;\)V\) + """.trimIndent(), + ) + } + } + + When("Injecting InjectContent.logMixed(...) into static method InjectTarget.main()") { + val targetMethod = injectTargetClass.findMethod("main", "([Ljava/lang/String;)V") as ProgramMethod + CodeInjector() + .injectInvokeStatic( + injectContentClass, + injectContentClass.findMethod("logMixed", "(Ljava/lang/String;ID)V"), + ConstantString("bar"), + ConstantPrimitive(1), + ConstantPrimitive(12.49), + ) + .into(injectTargetClass, targetMethod) + .at(FirstBlock()) + .commit() + + Then("The first instruction is InjectContent.logMixed(\"bar\", 1, 12.49)") { + val renderedMethod = StringWriter() + targetMethod.accept(injectTargetClass, ClassPrinter(PrintWriter(renderedMethod))) + + renderedMethod.toString() shouldContain Regex( + """ + \s*\[0\] ldc #\d+ = String\("bar"\) + \s*\[\d+\] iconst_1 + \s*\[\d+\] ldc2_w #\d+ = Double\(12.49\) + \s*\[\d+\] invokestatic #\d+ = Methodref\(InjectContent\.logMixed\(Ljava/lang/String;ID\)V\) + """.trimIndent(), + ) + } + } + + When("Injecting InjectContent.logMixed(...) into instance method InjectTarget.instanceMethod()") { + val instanceMethod = injectTargetClass.findMethod("instanceMethod", "()I") as ProgramMethod + CodeInjector() + .injectInvokeStatic( + injectContentClass, + injectContentClass.findMethod("logMixed", "(Ljava/lang/String;ID)V"), + ConstantString("bar"), + ConstantPrimitive(1), + ConstantPrimitive(12.49), + ) + .into(injectTargetClass, injectTargetClass.findMethod("instanceMethod", "()I") as ProgramMethod) + .at(FirstBlock()) + .commit() + + Then("The first instruction is InjectContent.logMixed(\"bar\", 1, 12.49)") { + val renderedMethod = StringWriter() + instanceMethod.accept(injectTargetClass, ClassPrinter(PrintWriter(renderedMethod))) + + renderedMethod.toString() shouldContain Regex( + """ + \s*\[0\] ldc #\d+ = String\("bar"\) + \s*\[\d+\] iconst_1 + \s*\[\d+\] ldc2_w #\d+ = Double\(12.49\) + \s*\[\d+\] invokestatic #\d+ = Methodref\(InjectContent\.logMixed\(Ljava/lang/String;ID\)V\) + """.trimIndent(), + ) + } + } + + When("Injecting InjectContent.logMixed(...) into InjectTarget's default constructor") { + val defaultConstructor = injectTargetClass.findMethod(ClassConstants.METHOD_NAME_INIT, ClassConstants.METHOD_TYPE_INIT) as ProgramMethod + CodeInjector() + .injectInvokeStatic( + injectContentClass, + injectContentClass.findMethod("logMixed", "(Ljava/lang/String;ID)V"), + ConstantString("bar"), + ConstantPrimitive(1), + ConstantPrimitive(12.49), + ) + .into(injectTargetClass, defaultConstructor) + .at(FirstBlock()) + .commit() + + Then("InjectContent.logMixed(\"bar\", 1, 12.49) is injected after super class' constructor call") { + val renderedMethod = StringWriter() + defaultConstructor.accept(injectTargetClass, ClassPrinter(PrintWriter(renderedMethod))) + + renderedMethod.toString() shouldContain Regex( + """ + \s*\[0\] aload_0 v0 + \s*\[1\] invokespecial #1 = Methodref\(java/lang/Object.\(\)V\) + \s*\[\d+\] ldc #\d+ = String\("bar"\) + \s*\[\d+\] iconst_1 + \s*\[\d+\] ldc2_w #\d+ = Double\(12.49\) + \s*\[\d+\] invokestatic #\d+ = Methodref\(InjectContent\.logMixed\(Ljava/lang/String;ID\)V\) + """.trimIndent(), + ) + } + } + + When("Injecting InjectContent.logMixed(...) into InjectTarget's delegated constructor") { + val delegatedConstructor = injectTargetClass.findMethod(ClassConstants.METHOD_NAME_INIT, "(IF)V") as ProgramMethod + CodeInjector() + .injectInvokeStatic( + injectContentClass, + injectContentClass.findMethod("logMixed", "(Ljava/lang/String;ID)V"), + ConstantString("bar"), + ConstantPrimitive(1), + ConstantPrimitive(12.49), + ) + .into(injectTargetClass, delegatedConstructor) + .at(FirstBlock()) + .commit() + + Then("InjectContent.logMixed(\"bar\", 1, 12.49) is injected after constructor call") { + val renderedMethod = StringWriter() + delegatedConstructor.accept(injectTargetClass, ClassPrinter(PrintWriter(renderedMethod))) + + renderedMethod.toString() shouldContain Regex( + """ + \s*\[0\] aload_0 v0 + \s*\[1\] iload_1 v1 + \s*\[2\] invokespecial #\d+ = Methodref\(InjectionTarget.\(I\)V\) + \s*\[\d+\] ldc #\d+ = String\("bar"\) + \s*\[\d+\] iconst_1 + \s*\[\d+\] ldc2_w #\d+ = Double\(12.49\) + \s*\[\d+\] invokestatic #\d+ = Methodref\(InjectContent\.logMixed\(Ljava/lang/String;ID\)V\) + """.trimIndent(), + ) + } + } + } +})