recipe-nope/src/main/java/dev/pfaff/recipe_nope/injector/UnconstrainedRedirectInject...

1157 lines
40 KiB
Java

package dev.pfaff.recipe_nope.injector;
import com.google.common.collect.ObjectArrays;
import dev.pfaff.recipe_nope.injector.util.InsnConsumer;
import dev.pfaff.recipe_nope.injector.util.ReflectUtil;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.*;
import org.objectweb.asm.util.Textifier;
import org.objectweb.asm.util.TraceMethodVisitor;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.injection.InjectionPoint;
import org.spongepowered.asm.mixin.injection.code.Injector;
import org.spongepowered.asm.mixin.injection.code.InsnListReadOnly;
import org.spongepowered.asm.mixin.injection.invoke.InvokeInjector;
import org.spongepowered.asm.mixin.injection.points.BeforeFieldAccess;
import org.spongepowered.asm.mixin.injection.points.BeforeNew;
import org.spongepowered.asm.mixin.injection.struct.InjectionInfo;
import org.spongepowered.asm.mixin.injection.struct.InjectionNodes;
import org.spongepowered.asm.mixin.injection.struct.Target;
import org.spongepowered.asm.mixin.injection.throwables.InjectionError;
import org.spongepowered.asm.mixin.injection.throwables.InvalidInjectionException;
import org.spongepowered.asm.util.Annotations;
import org.spongepowered.asm.util.Bytecode;
import org.spongepowered.asm.util.Constants;
import org.spongepowered.asm.util.SignaturePrinter;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.StringJoiner;
import static java.lang.invoke.MethodType.methodType;
public final class UnconstrainedRedirectInjector extends InvokeInjector {
private static final Logger LOGGER = LogManager.getLogger();
private static final String GET_CLASS_METHOD = "getClass";
private static final String IS_ASSIGNABLE_FROM_METHOD = "isAssignableFrom";
private static final String NPE = Type.getInternalName(NullPointerException.class);
private static final String KEY_NOMINATORS = "nominators";
private static final String KEY_FUZZ = "fuzz";
private static final String KEY_OPCODE = "opcode";
private static final String SHADOW_CONSTRUCTOR_DESC = Type.getDescriptor(ShadowConstructor.class);
private static final InsnListReadOnly INSN_DEAD_METHOD;
private static final boolean ENFORCE_CONTRACTS = false;
static {
var insns = new InsnList();
var owner = Type.getInternalName(UnsupportedOperationException.class);
insns.add(new TypeInsnNode(Opcodes.NEW, owner));
insns.add(new InsnNode(Opcodes.DUP));
insns.add(new LdcInsnNode("Dead method (inlined or otherwise consumed by mixin)"));
insns.add(new MethodInsnNode(Opcodes.INVOKESPECIAL,
owner,
"<init>",
ReflectUtil.methodDescriptor(methodType(void.class, String.class), false)
));
insns.add(new InsnNode(Opcodes.ATHROW));
INSN_DEAD_METHOD = new InsnListReadOnly(insns);
}
/**
* Meta decoration object for redirector target nodes
*/
class Meta {
public static final String KEY = "redirector";
final int priority;
final boolean isFinal;
final String name;
final String desc;
public Meta(int priority, boolean isFinal, String name, String desc) {
this.priority = priority;
this.isFinal = isFinal;
this.name = name;
this.desc = desc;
}
UnconstrainedRedirectInjector getOwner() {
return UnconstrainedRedirectInjector.this;
}
}
/**
* Meta decoration for wildcard ctor redirects
*/
static class ConstructorRedirectData {
public static final String KEY = "ctor";
boolean wildcard = false;
int injected = 0;
InvalidInjectionException lastException;
public void throwOrCollect(InvalidInjectionException ex) {
if (!this.wildcard) {
throw ex;
}
this.lastException = ex;
}
}
/**
* Data bundle for invoke redirectors
*/
static class RedirectedInvokeData extends InjectorData {
final MethodInsnNode node;
final Type returnType;
final Type[] targetArgs;
final Type[] handlerArgs;
RedirectedInvokeData(Target target, MethodInsnNode node) {
super(target);
this.node = node;
this.returnType = Type.getReturnType(node.desc);
this.targetArgs = Type.getArgumentTypes(node.desc);
this.handlerArgs = node.getOpcode() == Opcodes.INVOKESTATIC
? this.targetArgs
: ObjectArrays.concat(Type.getObjectType(node.owner), this.targetArgs);
}
}
/**
* Data bundle for field redirectors
*/
static class RedirectedFieldData extends InjectorData {
final FieldInsnNode node;
final int opcode;
final Type owner;
final Type type;
final int dimensions;
final boolean isStatic;
final boolean isGetter;
final boolean isSetter;
// This is actually the return type for array access, might be int for
// array length redirectors
Type elementType;
int extraDimensions = 1;
RedirectedFieldData(Target target, FieldInsnNode node) {
super(target);
this.node = node;
this.opcode = node.getOpcode();
this.owner = Type.getObjectType(node.owner);
this.type = Type.getType(node.desc);
this.dimensions = (this.type.getSort() == Type.ARRAY) ? this.type.getDimensions() : 0;
this.isStatic = this.opcode == Opcodes.GETSTATIC || this.opcode == Opcodes.PUTSTATIC;
this.isGetter = this.opcode == Opcodes.GETSTATIC || this.opcode == Opcodes.GETFIELD;
this.isSetter = this.opcode == Opcodes.PUTSTATIC || this.opcode == Opcodes.PUTFIELD;
this.description = this.isGetter ? "field getter" : this.isSetter ? "field setter" : "handler";
}
int getTotalDimensions() {
return this.dimensions + this.extraDimensions;
}
Type[] getArrayArgs(Type... extra) {
int dimensions = this.getTotalDimensions();
Type[] args = new Type[dimensions + extra.length];
for (int i = 0; i < args.length; i++) {
args[i] = i == 0 ? this.type : i < dimensions ? Type.INT_TYPE : extra[dimensions - i];
}
return args;
}
}
/**
* Meta is used to decorate the target node with information about this injection
*/
private final Meta meta;
private final Map<BeforeNew, ConstructorRedirectData> ctorRedirectors = new HashMap<>();
public UnconstrainedRedirectInjector(InjectionInfo info, String annotationType) {
super(info, annotationType);
int priority = info.getMixin().getPriority();
boolean isFinal = Annotations.getVisible(this.methodNode, Final.class) != null;
this.meta = new Meta(priority, isFinal, this.info.toString(), this.methodNode.desc);
}
@Override
protected void checkTarget(Target target) {
// Overridden so we can do this check later in a location-aware manner
}
@Override
protected void addTargetNode(Target target,
List<InjectionNodes.InjectionNode> myNodes,
AbstractInsnNode insn,
Set<InjectionPoint> nominators) {
InjectionNodes.InjectionNode node = target.getInjectionNode(insn);
ConstructorRedirectData ctorData = null;
int fuzz = BeforeFieldAccess.ARRAY_SEARCH_FUZZ_DEFAULT;
int opcode = 0;
if (node != null) {
Meta other = node.getDecoration(Meta.KEY);
if (other != null && other.getOwner() != this) {
if (other.priority >= this.meta.priority) {
Injector.logger.warn(
"{} conflict. Skipping {} with priority {}, already redirected by {} with priority {}",
this.annotationType,
this.info,
this.meta.priority,
other.name,
other.priority
);
return;
} else if (other.isFinal) {
throw new InvalidInjectionException(
this.info,
this.annotationType + " conflict: " + this + " failed because target was already remapped by " + other.name
);
}
}
}
for (InjectionPoint ip : nominators) {
if (ip instanceof BeforeNew) {
ctorData = this.getCtorRedirect((BeforeNew) ip);
ctorData.wildcard = !((BeforeNew) ip).hasDescriptor();
} else if (ip instanceof BeforeFieldAccess bfa) {
fuzz = bfa.getFuzzFactor();
opcode = bfa.getArrayOpcode();
}
}
InjectionNodes.InjectionNode targetNode = target.addInjectionNode(insn);
targetNode.decorate(Meta.KEY, this.meta);
targetNode.decorate(KEY_NOMINATORS, nominators);
if (insn instanceof TypeInsnNode && insn.getOpcode() == Opcodes.NEW) {
targetNode.decorate(ConstructorRedirectData.KEY, ctorData);
} else {
targetNode.decorate(KEY_FUZZ, fuzz);
targetNode.decorate(KEY_OPCODE, opcode);
}
myNodes.add(targetNode);
}
private ConstructorRedirectData getCtorRedirect(BeforeNew ip) {
ConstructorRedirectData ctorRedirect = this.ctorRedirectors.get(ip);
if (ctorRedirect == null) {
ctorRedirect = new ConstructorRedirectData();
this.ctorRedirectors.put(ip, ctorRedirect);
}
return ctorRedirect;
}
@Override
protected void inject(Target target, InjectionNodes.InjectionNode node) {
if (!this.preInject(node)) {
return;
}
if (node.isReplaced()) {
throw new UnsupportedOperationException("Redirector target failure for " + this.info);
}
if (node.getCurrentTarget() instanceof MethodInsnNode) {
this.checkTargetForNode(target, node, InjectionPoint.RestrictTargetLevel.ALLOW_ALL);
this.injectAtInvoke(target, node);
return;
}
if (node.getCurrentTarget() instanceof FieldInsnNode) {
this.checkTargetForNode(target, node, InjectionPoint.RestrictTargetLevel.ALLOW_ALL);
this.injectAtFieldAccess(target, node);
return;
}
if (node.getCurrentTarget() instanceof TypeInsnNode) {
int opcode = node.getCurrentTarget().getOpcode();
if (opcode == Opcodes.NEW) {
if (!this.isStatic && target.isStatic) {
throw new InvalidInjectionException(
this.info,
"non-static callback method " + this + " has a static target which is not supported"
);
}
this.injectAtConstructor(target, node);
return;
} else if (opcode == Opcodes.INSTANCEOF) {
this.checkTargetModifiers(target, false);
this.injectAtInstanceOf(target, node);
return;
}
}
throw new InvalidInjectionException(
this.info,
this.annotationType + " annotation on is targetting an invalid insn in " + target + " in " + this
);
}
@Override
protected void checkTargetForNode(Target target,
InjectionNodes.InjectionNode node,
InjectionPoint.RestrictTargetLevel targetLevel) {
if (target.isCtor) {
if (targetLevel == InjectionPoint.RestrictTargetLevel.METHODS_ONLY) {
throw new InvalidInjectionException(
this.info,
"Found " + this.annotationType + " targeting a constructor in injector " + this
);
}
Bytecode.DelegateInitialiser superCall = target.findDelegateInitNode();
if (!superCall.isPresent) {
throw new InjectionError("Delegate constructor lookup failed for " + this.annotationType + " target on " + this.info);
}
if (node.getCurrentTarget() instanceof MethodInsnNode methodInsn && Constants.CTOR.equals(methodInsn.name)) {
// the redirect is targeting the `this` or `super` invocation. Skip the rest of the checks.
this.checkTargetModifiers(target, true);
return;
}
int superCallIndex = target.indexOf(superCall.insn);
int targetIndex = target.indexOf(node.getCurrentTarget());
if (targetIndex <= superCallIndex) {
if (targetLevel == InjectionPoint.RestrictTargetLevel.CONSTRUCTORS_AFTER_DELEGATE) {
throw new InvalidInjectionException(
this.info,
"Found " + this.annotationType + " targeting a constructor before " + superCall + "() in injector " + this
);
}
if (!this.isStatic) {
throw new InvalidInjectionException(
this.info,
this.annotationType + " handler before " + superCall + "() invocation must be static in injector " + this
);
}
return;
}
}
this.checkTargetModifiers(target, true);
}
private boolean preInject(InjectionNodes.InjectionNode node) {
Meta other = node.getDecoration(Meta.KEY);
if (other.getOwner() != this) {
Injector.logger.warn("{} conflict. Skipping {} with priority {}, already redirected by {} with priority {}",
this.annotationType,
this.info,
this.meta.priority,
other.name,
other.priority
);
return false;
}
return true;
}
@Override
protected void postInject(Target target, InjectionNodes.InjectionNode node) {
super.postInject(target, node);
if (node.getOriginalTarget() instanceof TypeInsnNode && node.getOriginalTarget().getOpcode() == Opcodes.NEW) {
ConstructorRedirectData meta = node.getDecoration(ConstructorRedirectData.KEY);
if (meta.wildcard && meta.injected == 0) {
throw new InvalidInjectionException(
this.info,
this.annotationType + " ctor invocation was not found in " + target,
meta.lastException
);
}
}
}
/**
* Redirect a method invocation
*/
@Override
protected void injectAtInvoke(Target target, InjectionNodes.InjectionNode node) {
RedirectedInvokeData invoke = new RedirectedInvokeData(target, (MethodInsnNode) node.getCurrentTarget());
LOGGER.info(() -> "Redirecting\n" + "call to " + methodInsnToString(invoke.node) + "\n\tin " + target.classNode.name + '.' + methodNodeToString(target.method) + "\nwith " + this.classNode.name + '.' + methodNodeToString(this.methodNode));
LOGGER.info(() -> "Target method LVT (pre replace): " + localVariablesToString(target.method.localVariables));
boolean isCtor;
if (node.getCurrentTarget() instanceof MethodInsnNode methodInsn && Constants.CTOR.equals(methodInsn.name)) {
if (this.isStatic) {
throw new InvalidInjectionException(this.info,
"Illegal " + this.annotationType + " of constructor specified on " + this + ": Is static"
);
}
isCtor = true;
} else {
isCtor = false;
}
this.validateParams(invoke,
invoke.returnType,
isCtor
? invoke.targetArgs
: invoke.handlerArgs
);
var clonedLabels = cloneLabelsFresh(this.methodNode.instructions);
var replacementInsns = new InsnList();
var labelStart = new LabelNode();
var labelEnd = new LabelNode();
// add the label for new lvt allocation "frame"
replacementInsns.add(labelStart);
for (var insn : this.methodNode.instructions) {
replacementInsns.add(insn.clone(clonedLabels));
}
if (isCtor) {
final var mixinTargetDesc = this.info.getClassNode().name;
boolean seenCtor = false;
// this initializer is duplicative, but the compiler insists.
for (var insn : replacementInsns) {
if ((insn instanceof MethodInsnNode methodInsn)) {
// remap constructor invocations
if (methodInsn.owner.equals(mixinTargetDesc)) {
var method = this.classNode.methods.stream()
.filter(m -> methodInsn.name.equals(m.name)
&& methodInsn.desc.equals(m.desc))
.findFirst()
.get();
LOGGER.info(" visible annotations: " + method.visibleAnnotations.stream().map(a -> a.desc).toList());
LOGGER.info("invisible annotations: " + method.invisibleAnnotations.stream().map(a -> a.desc).toList());
var annotation = method.invisibleAnnotations.stream()
.filter(a -> a.desc.equals(SHADOW_CONSTRUCTOR_DESC))
.findFirst();
if (annotation.isPresent()) {
var vals = annotation.get().values;
if (vals.size() != 2) {
throw new InvalidInjectionException(this.info,
"Illegal " + this.annotationType + " of constructor specified on " + this + ": Invoked this or super constructor is invalid");
}
if (!"type".equals(vals.get(0))) {
throw new InvalidInjectionException(this.info,
"Illegal " + this.annotationType + " of constructor specified on " + this + ": Invoked this or super constructor is invalid");
}
var enumValue = vals.get(1);
if (!(enumValue instanceof String[] a)) {
throw new InvalidInjectionException(this.info,
"Illegal " + this.annotationType + " of constructor specified on " + this + ": Invoked this or super constructor is invalid");
}
var owner = switch (a[1]) {
case "This" -> this.classNode.name;
case "Super" -> this.classNode.superName;
default -> throw new AssertionError();
};
seenCtor = true;
replacementInsns.set(insn, new MethodInsnNode(Opcodes.INVOKESPECIAL,
owner,
"<init>",
methodInsn.desc
));
}
}
}
}
if (!seenCtor) {
throw new InvalidInjectionException(this.info,
"Illegal " + this.annotationType + " of constructor specified on " + this + ": Missing super or this constructor invocation"
);
}
}
// get the args for the handler
Type[] handlerArgs = this.methodArgs;
//if (isCtor) {
// // skip the `this` argument
// handlerArgs = invoke.targetArgs;
//} else {
// handlerArgs = invoke.handlerArgs;
//}
LOGGER.info(() -> "Handler args: size=" + Bytecode.getArgsSize(handlerArgs) + ' ' + Arrays.toString(handlerArgs));
int skipLv = isStatic || isCtor ? 0 : 0;
{
// handled down below
// allocate N locals for the args
//target.allocateLocals(Bytecode.getArgsSize(handlerArgs));
// no need to extend the stack past that point, because we already have those slots from the original constructor invocation.
int n = this.methodNode.maxStack - handlerArgs.length;
if (n > 0) {
target.extendStack().add(n).apply();
}
for (int j = 0; j < handlerArgs.length; j++) {
Type arg = handlerArgs[j];
// handled down below
//// add a local variable for each
//target.addLocalVariable(target.getMaxLocals() + j,
// "__redirectionParam" + j,
// arg.getDescriptor(),
// labelStart,
// labelEnd
//);
// we need these in reverse order, so insert at labelStart (thus before previous) works perfectly.
replacementInsns.insert(labelStart,
new VarInsnNode(ReflectUtil.varStoreInsn(arg), target.method.maxLocals + j)
);
}
}
int varOffset = target.allocateLocals(this.methodNode.maxLocals - skipLv);
List<LocalVariableNode> localVariables = this.methodNode.localVariables;
for (int i = skipLv; i < localVariables.size(); i++) {
LocalVariableNode lv = localVariables.get(i);
int i1 = i;
int i2 = varOffset + lv.index - skipLv;
LOGGER.info(() -> "LV[" + i1 + " -> " + i2 + "] " + lv.name + ": " + lv.desc);
target.addLocalVariable(i2, "__redirectionLv" + i2 + '_' + lv.name, lv.desc, lv.start, lv.end);
}
int firstExplicit = isStatic ? 0 : 1;
// no instance argument
if (this.methodArgs.length == invoke.targetArgs.length) {
// pop unused instance
replacementInsns.add(new InsnNode(Opcodes.POP));
}
for (var insn : replacementInsns) {
if ((insn instanceof VarInsnNode varInsn)) {
int i = varInsn.var;
// i == 0 when accessing `this`. Don't change that.
if (isStatic || i != 0) varInsn.var = i + varOffset - skipLv;
}
}
if (!isCtor && invoke.coerceReturnType && invoke.returnType.getSort() >= Type.ARRAY) {
replacementInsns.add(new TypeInsnNode(Opcodes.CHECKCAST, invoke.returnType.getInternalName()));
}
// ~~replace return instructions with store and break~~ maybe not. just remove them for now.
for (ListIterator<AbstractInsnNode> iterator = replacementInsns.iterator(); iterator.hasNext(); ) {
AbstractInsnNode insn = iterator.next();
if ((insn instanceof InsnNode insn1) && insn1.getOpcode() >= Opcodes.IRETURN && insn1.getOpcode() <= Opcodes.RETURN) {
// does break pop from the stack? I suppose only if it creates a new stack frame? Might need to introduce new locals for compat
//iterator.remove();
// skip the immediate jump if we're at the end of the injected code
if (iterator.hasNext()) {
replacementInsns.set(insn, new JumpInsnNode(Opcodes.GOTO, labelEnd));
}
}
}
// add the label to end the lvt allocation "frame"
replacementInsns.add(labelEnd);
// useful for debugging
//new ClassReader(b).accept(new TraceClassVisitor(new PrintWriter(System.out)), 0)
LOGGER.trace(() -> "Target method instructions (pre replace): " + instructionsToString(target.insns));
target.insertBefore(invoke.node, replacementInsns);
target.removeNode(invoke.node);
// TODO: why does this cause the method to have *no* instructions?
//this.methodNode.instructions = INSN_DEAD_METHOD;
this.info.addCallbackInvocation(null);
LOGGER.info(() -> "Handler method LVT: " + localVariablesToString(this.methodNode.localVariables));
LOGGER.info(() -> "Handler method maxes: " + this.methodNode.maxLocals + ", " + this.methodNode.maxStack);
LOGGER.trace(() -> "Handler method instructions (dead): " + instructionsToString(this.methodNode.instructions));
LOGGER.info(() -> "Target method LVT: " + localVariablesToString(target.method.localVariables));
LOGGER.info(() -> "Target method maxes: " + target.method.maxLocals + ", " + target.method.maxStack);
LOGGER.trace(() -> "Target method instructions: " + instructionsToString(target.insns));
}
@NotNull
private static String localVariablesToString(List<LocalVariableNode> localVariables) {
return "size=" + localVariables.size() + ' ' + Arrays.toString(localVariables
.stream()
.map(lv -> "[" + lv.index + ']' + lv.name + ": " + lv.desc)
.toArray(String[]::new));
}
private static String methodInsnToString(MethodInsnNode node) {
return node.owner + '.' + node.name + node.desc;
}
private static String methodNodeToString(MethodNode node) {
return node.name + node.desc;
}
@SuppressWarnings("unchecked")
@NotNull
private static String instructionsToString(InsnList theInsns) {
var printer = new Textifier();
var visitor = new TraceMethodVisitor(printer);
Arrays.stream(theInsns.toArray()).forEach(node1 -> node1.accept(visitor));
var joiner = new StringJoiner("\n");
((List<String>) (List<?>) printer.text).forEach(joiner::add);
return joiner.toString();
}
/**
* Redirect a field get or set operation, or an array element access
*/
private void injectAtFieldAccess(Target target, InjectionNodes.InjectionNode node) {
RedirectedFieldData field = new RedirectedFieldData(target, (FieldInsnNode) node.getCurrentTarget());
int handlerDimensions = (this.returnType.getSort() == Type.ARRAY) ? this.returnType.getDimensions() : 0;
if (handlerDimensions > field.dimensions) {
throw new InvalidInjectionException(this.info,
"Dimensionality of handler method is greater than target array on " + this
);
} else if (handlerDimensions == 0 && field.dimensions > 0) {
int fuzz = node.<Integer>getDecoration(KEY_FUZZ);
int opcode = node.<Integer>getDecoration(KEY_OPCODE);
this.injectAtArrayField(field, fuzz, opcode);
} else {
this.injectAtScalarField(field);
}
}
/**
* Redirect an array element access
*/
private void injectAtArrayField(RedirectedFieldData field, int fuzz, int opcode) {
Type elementType = field.type.getElementType();
if (field.opcode != Opcodes.GETSTATIC && field.opcode != Opcodes.GETFIELD) {
throw new InvalidInjectionException(
this.info,
"Unsupported opcode " + Bytecode.getOpcodeName(field.opcode) + " for array access " + this.info
);
} else if (this.returnType.getSort() != Type.VOID) {
if (opcode != Opcodes.ARRAYLENGTH) {
opcode = elementType.getOpcode(Opcodes.IALOAD);
}
AbstractInsnNode varNode = BeforeFieldAccess.findArrayNode(field.target.insns, field.node, opcode, fuzz);
this.injectAtGetArray(field, varNode);
} else {
AbstractInsnNode varNode = BeforeFieldAccess.findArrayNode(field.target.insns,
field.node,
elementType.getOpcode(Opcodes.IASTORE),
fuzz
);
this.injectAtSetArray(field, varNode);
}
}
/**
* Array element read (xALOAD) or array.length (ARRAYLENGTH)
*/
private void injectAtGetArray(RedirectedFieldData field, AbstractInsnNode varNode) {
field.description = "array getter";
field.elementType = field.type.getElementType();
if (varNode != null && varNode.getOpcode() == Opcodes.ARRAYLENGTH) {
field.elementType = Type.INT_TYPE;
field.extraDimensions = 0;
}
this.validateParams(field, field.elementType, field.getArrayArgs());
this.injectArrayRedirect(field, varNode, "array getter");
}
/**
* Array element write (xASTORE)
*/
private void injectAtSetArray(RedirectedFieldData field, AbstractInsnNode varNode) {
field.description = "array setter";
Type elementType = field.type.getElementType();
int valueArgIndex = field.getTotalDimensions();
if (this.checkCoerce(
valueArgIndex,
elementType,
this.annotationType + " array setter method " + this + " from " + this.info.getMixin(),
true
)) {
elementType = this.methodArgs[valueArgIndex];
}
this.validateParams(field, Type.VOID_TYPE, field.getArrayArgs(elementType));
this.injectArrayRedirect(field, varNode, "array setter");
}
/**
* The code for actually redirecting the array element is the same regardless of whether it's a read or write
* because it just depends on the actual handler signature, the correct arguments are already on the stack thanks to
* the nature of xALOAD and xASTORE.
*
* @param varNode array access node
* @param type description of access type for use in error messages
* @param target target method
* @param fieldNode field node
*/
private void injectArrayRedirect(RedirectedFieldData field, AbstractInsnNode varNode, String type) {
if (varNode == null) {
throw new InvalidInjectionException(this.info, "Array element " + this.annotationType + " on " + this + " could not locate a matching " + type + " instruction in " + field.target + '.');
}
InsnList insns = new InsnList();
var consumer = InsnConsumer.into(insns, field.target);
if (!this.isStatic) {
VarInsnNode loadThis = new VarInsnNode(Opcodes.ALOAD, 0);
field.target.insns.insert(field.node, loadThis);
field.target.insns.insert(loadThis, new InsnNode(Opcodes.SWAP));
consumer.extraStack(1);
}
this.info.addCallbackInvocation(null);
AbstractInsnNode champion = invokeHandlerWithArgsAndCoerce(field, field.elementType, consumer);
field.target.replaceNode(varNode, champion, insns);
}
/**
* Redirect a field get or set
*
* @param target target method
* @param fieldNode field access node
* @param opCode field access type
* @param ownerType type of the field owner
* @param fieldType field type
*/
private void injectAtScalarField(RedirectedFieldData field) {
AbstractInsnNode invoke;
InsnList insns = new InsnList();
if (field.isGetter) {
invoke = this.injectAtGetField(field, InsnConsumer.into(insns, field.target));
} else if (field.isSetter) {
invoke = this.injectAtPutField(field, InsnConsumer.into(insns, field.target));
} else {
throw new InvalidInjectionException(
this.info,
"Unsupported opcode " + Bytecode.getOpcodeName(field.opcode) + " for " + this.info
);
}
field.target.replaceNode(field.node, invoke, insns);
}
/**
* Inject opcodes to redirect a field getter. The injection will vary based on the static-ness of the field and the
* handler, thus there are four possible scenarios based on the possible combinations of static on the handler and
* the field itself.
*/
private AbstractInsnNode injectAtGetField(RedirectedFieldData field, InsnConsumer consumer) {
this.validateParams(field, field.type, field.isStatic ? null : field.owner);
if (!this.isStatic) {
consumer.extraStack(1);
consumer.accept(new VarInsnNode(Opcodes.ALOAD, 0));
if (!field.isStatic) {
consumer.accept(new InsnNode(Opcodes.SWAP));
}
}
this.info.addCallbackInvocation(null);
return invokeHandlerWithArgsAndCoerce(field, field.type, consumer);
}
/**
* Inject opcodes to redirect a field setter. The injection will vary based on the static-ness of the field and the
* handler, thus there are four possible scenarios based on the possible combinations of static on the handler and
* the field itself.
*/
private AbstractInsnNode injectAtPutField(RedirectedFieldData field, InsnConsumer consumer) {
this.validateParams(field, Type.VOID_TYPE, field.isStatic ? null : field.owner, field.type);
if (!this.isStatic) {
if (field.isStatic) {
consumer.accept(new VarInsnNode(Opcodes.ALOAD, 0));
consumer.accept(new InsnNode(Opcodes.SWAP));
} else {
consumer.extraStack(1);
int marshallVar = field.target.allocateLocals(field.type.getSize());
consumer.accept(new VarInsnNode(field.type.getOpcode(Opcodes.ISTORE), marshallVar));
consumer.accept(new VarInsnNode(Opcodes.ALOAD, 0));
consumer.accept(new InsnNode(Opcodes.SWAP));
consumer.accept(new VarInsnNode(field.type.getOpcode(Opcodes.ILOAD), marshallVar));
}
}
if (field.captureTargetArgs > 0) {
pushArgs(field.target.arguments,
consumer,
field.target.getArgIndices(),
0,
field.captureTargetArgs
);
}
this.info.addCallbackInvocation(null);
return this.invokeHandler(consumer, this.methodNode);
}
private void injectAtConstructor(Target target, InjectionNodes.InjectionNode node) {
ConstructorRedirectData meta = node.getDecoration(ConstructorRedirectData.KEY);
if (meta == null) {
// This should never happen, but let's display a less obscure error if it does
throw new InvalidInjectionException(
this.info,
this.annotationType + " ctor redirector has no metadata, the injector failed a preprocessing phase"
);
}
final TypeInsnNode newNode = (TypeInsnNode) node.getCurrentTarget();
final AbstractInsnNode dupNode = target.get(target.indexOf(newNode) + 1);
final MethodInsnNode initNode = target.findInitNodeFor(newNode);
if (initNode == null) {
meta.throwOrCollect(new InvalidInjectionException(
this.info,
this.annotationType + " ctor invocation was not found in " + target
));
return;
}
// True if the result of the object construction is being assigned
boolean isAssigned = dupNode.getOpcode() == Opcodes.DUP;
RedirectedInvokeData ctor = new RedirectedInvokeData(target, initNode);
ctor.description = "factory";
try {
this.validateParams(ctor, Type.getObjectType(newNode.desc), ctor.targetArgs);
} catch (InvalidInjectionException ex) {
meta.throwOrCollect(ex);
return;
}
if (isAssigned) {
target.removeNode(dupNode);
}
if (this.isStatic) {
target.removeNode(newNode);
} else {
target.replaceNode(newNode, new VarInsnNode(Opcodes.ALOAD, 0));
}
InsnList insns = new InsnList();
var consumer = InsnConsumer.into(insns, target);
if (ctor.captureTargetArgs > 0) {
pushArgs(target.arguments, consumer, target.getArgIndices(), 0, ctor.captureTargetArgs);
}
this.invokeHandler(insns);
if (ctor.coerceReturnType) {
insns.add(new TypeInsnNode(Opcodes.CHECKCAST, newNode.desc));
}
if (isAssigned) {
if (ENFORCE_CONTRACTS) {
// Do a null-check following the redirect to ensure that the handler
// didn't return null. Since NEW cannot return null, this would break
// the contract of the target method!
this.doNullCheck(consumer,
"constructor handler",
newNode.desc.replace('/', '.')
);
}
} else {
// Result is not assigned, so just pop it from the operand stack
insns.add(new InsnNode(Opcodes.POP));
}
target.replaceNode(initNode, insns);
meta.injected++;
}
private void injectAtInstanceOf(Target target, InjectionNodes.InjectionNode node) {
this.injectAtInstanceOf(target, (TypeInsnNode) node.getCurrentTarget());
}
private void injectAtInstanceOf(Target target, TypeInsnNode typeNode) {
if (this.returnType.getSort() == Type.BOOLEAN) {
this.redirectInstanceOf(target, typeNode, false);
return;
}
if (this.returnType.equals(Type.getType(Constants.CLASS_DESC))) {
this.redirectInstanceOf(target, typeNode, true);
return;
}
// This syntax is neat but the inconsistency might be a step too far
// if (this.returnType.getSort() >= Type.ARRAY) {
// this.modifyInstanceOfType(target, typeNode);
// return;
// }
throw new InvalidInjectionException(this.info, this.annotationType + " on " + this + " has an invalid signature. Found unexpected return type " + SignaturePrinter.getTypeName(this.returnType) + ". INSTANCEOF" + " handler expects (Ljava/lang/Object;Ljava/lang/Class;)Z or (Ljava/lang/Object;Ljava/lang/Class;)Ljava/lang/Class;");
}
private void redirectInstanceOf(Target target, TypeInsnNode typeNode, boolean dynamic) {
final InsnList insns = new InsnList();
var consumer = InsnConsumer.into(insns, target);
InjectorData handler = new InjectorData(target, "instanceof handler", false /* do not coerce args */);
this.validateParams(handler,
this.returnType,
Type.getType(Constants.OBJECT_DESC),
Type.getType(Constants.CLASS_DESC)
);
if (dynamic) {
insns.add(new InsnNode(Opcodes.DUP));
consumer.extraStack(1);
}
if (!this.isStatic) {
insns.add(new VarInsnNode(Opcodes.ALOAD, 0));
insns.add(new InsnNode(Opcodes.SWAP));
consumer.extraStack(1);
}
// Add the class type from the original instanceof check
insns.add(new LdcInsnNode(Type.getObjectType(typeNode.desc)));
consumer.extraStack(1);
if (handler.captureTargetArgs > 0) {
pushArgs(target.arguments, consumer, target.getArgIndices(), 0, handler.captureTargetArgs);
}
AbstractInsnNode champion = this.invokeHandler(insns);
if (dynamic) {
// removed this duplicative null check.
//// First null-check the class value returned by the handler, if it's
//// null then the rest is going to go badly
//this.doNullCheck(InsnConsumer.into(insns, extraStack), "instanceof handler", "class type");
// Now do a null-check on the reference and isAssignableFrom check
checkIsAssignableFrom(consumer);
}
target.replaceNode(typeNode, champion, insns);
}
private static void checkIsAssignableFrom(InsnConsumer consumer) {
LabelNode objectIsNull = new LabelNode();
LabelNode checkComplete = new LabelNode();
// Swap the values (we duped the ref above) and check for null. If
// the reference is null, load FALSE per the contract of instanceof
consumer.accept(new InsnNode(Opcodes.SWAP));
consumer.accept(new InsnNode(Opcodes.DUP));
consumer.extraStack(1);
consumer.accept(new JumpInsnNode(Opcodes.IFNULL, objectIsNull));
// If it's not null, call getClass on the reference and then use
// isAssignableFrom on the result
consumer.accept(new MethodInsnNode(Opcodes.INVOKEVIRTUAL,
Constants.OBJECT,
GET_CLASS_METHOD,
"()" + Constants.CLASS_DESC,
false));
consumer.accept(new MethodInsnNode(Opcodes.INVOKEVIRTUAL,
Constants.CLASS,
IS_ASSIGNABLE_FROM_METHOD,
'(' + Constants.CLASS_DESC + ")Z",
false));
consumer.accept(new JumpInsnNode(Opcodes.GOTO, checkComplete));
consumer.accept(objectIsNull);
consumer.accept(new InsnNode(Opcodes.POP)); // remove ref
consumer.accept(new InsnNode(Opcodes.POP)); // remove class
consumer.accept(new InsnNode(Opcodes.ICONST_0));
consumer.accept(checkComplete);
consumer.extraStack(1);
}
// private void modifyInstanceOfType(Target target, TypeInsnNode typeNode) {
// if (this.methodArgs.length > 0) {
// throw new InvalidInjectionException(this.info, String.format("%s on %s has an invalid signature. Found %d unexpected additional method"
// + "arguments, expected 0. INSTANCEOF handler expects ()Lthe/replacement/Type; or (Ljava/lang/Object;Ljava/lang/Class;)Z",
// this.annotationType, this, this.methodArgs.length));
// }
//
// // Already know that returnType is an object or array so no need to check again
// typeNode.desc = this.returnType.getInternalName();
// this.info.addCallbackInvocation(this.methodNode);
// }
private static void throwException(InsnConsumer consumer, String exceptionType, String message) {
consumer.accept(new TypeInsnNode(Opcodes.NEW, exceptionType));
consumer.accept(new InsnNode(Opcodes.DUP));
consumer.accept(new LdcInsnNode(message));
consumer.accept(new MethodInsnNode(Opcodes.INVOKESPECIAL,
exceptionType,
"<init>",
"(Ljava/lang/String;)V",
false
));
consumer.accept(new InsnNode(Opcodes.ATHROW));
consumer.extraStack(3);
}
private void doNullCheck(InsnConsumer consumer, String type, String value) {
LabelNode nullCheckSucceeded = new LabelNode();
consumer.accept(new InsnNode(Opcodes.DUP));
consumer.accept(new JumpInsnNode(Opcodes.IFNONNULL, nullCheckSucceeded));
throwException(consumer, NPE, this.annotationType + ' ' + type + ' ' + this + " returned null for " + value);
consumer.accept(nullCheckSucceeded);
consumer.extraStack(1);
}
private AbstractInsnNode invokeHandler(InsnConsumer consumer, MethodNode handler) {
boolean isInterface = Bytecode.hasFlag(classNode, Opcodes.ACC_INTERFACE);
boolean isPrivate = (handler.access & Opcodes.ACC_PRIVATE) != 0;
int invokeOpcode = this.isStatic ? Opcodes.INVOKESTATIC : isInterface ? Opcodes.INVOKEINTERFACE : isPrivate ? Opcodes.INVOKESPECIAL : Opcodes.INVOKEVIRTUAL;
MethodInsnNode insn = new MethodInsnNode(invokeOpcode, this.classNode.name, handler.name, handler.desc, isInterface);
consumer.accept(insn);
return insn;
}
private AbstractInsnNode invokeHandlerWithArgs(Type[] args, InsnConsumer consumer, int[] argMap) {
return invokeHandlerWithArgs(args, consumer, argMap, 0, args.length);
}
private AbstractInsnNode invokeHandlerWithArgs(Type[] args, InsnConsumer consumer, int[] argMap, int startArg, int endArg) {
if (!this.isStatic) {
consumer.accept(new VarInsnNode(Opcodes.ALOAD, 0));
}
pushArgs(args, consumer, argMap, startArg, endArg);
return invokeHandler(consumer, this.methodNode);
}
private AbstractInsnNode invokeHandlerWithArgsAndCoerce(RedirectedFieldData field,
Type coerceType,
InsnConsumer consumer) {
if (field.captureTargetArgs > 0) {
pushArgs(
field.target.arguments,
consumer,
field.target.getArgIndices(),
0,
field.captureTargetArgs
);
}
AbstractInsnNode champion = this.invokeHandler(consumer, this.methodNode);
if (field.coerceReturnType && field.type.getSort() >= Type.ARRAY) {
consumer.accept(new TypeInsnNode(Opcodes.CHECKCAST, coerceType.getInternalName()));
}
return champion;
}
private static void pushArgs(Type[] args, InsnConsumer consumer, int[] argMap, int start, int end) {
for (int arg = start; arg < end && arg < args.length; arg++) {
consumer.accept(new VarInsnNode(args[arg].getOpcode(Opcodes.ILOAD), argMap[arg]));
consumer.extraStack(args[arg].getSize());
}
}
//@Deprecated
//@Override
//protected AbstractInsnNode invokeHandler(InsnList insns) {
// throw new AssertionError();
//}
//@Deprecated
//@Override
//protected AbstractInsnNode invokeHandler(InsnList insns, MethodNode handler) {
// throw new AssertionError();
//}
@Deprecated
@Override
protected AbstractInsnNode invokeHandlerWithArgs(Type[] args, InsnList insns, int[] argMap) {
throw new AssertionError();
}
@Deprecated
@Override
protected AbstractInsnNode invokeHandlerWithArgs(Type[] args,
InsnList insns,
int[] argMap,
int startArg,
int endArg) {
throw new AssertionError();
}
@Deprecated
@Override
protected int[] storeArgs(Target target, Type[] args, InsnList insns, int start) {
throw new AssertionError();
}
@Deprecated
@Override
protected int[] storeArgs(Target target, Type[] args, InsnList insns, int start, LabelNode from, LabelNode to) {
throw new AssertionError();
}
@Deprecated
@Override
protected void storeArgs(Target target, Type[] args, InsnList insns, int[] argMap, int start, int end) {
throw new AssertionError();
}
@Deprecated
@Override
protected void storeArgs(Target target,
Type[] args,
InsnList insns,
int[] argMap,
int start,
int end,
LabelNode from,
LabelNode to) {
throw new AssertionError();
}
@Deprecated
@Override
protected void pushArgs(Type[] args, InsnList insns, int[] argMap, int start, int end) {
throw new AssertionError();
}
@Deprecated
@Override
protected void pushArgs(Type[] args, InsnList insns, int[] argMap, int start, int end, Target.Extension extension) {
throw new AssertionError();
}
private static Map<LabelNode, LabelNode> cloneLabelsFresh(InsnList source) {
Map<LabelNode, LabelNode> labels = new HashMap<LabelNode, LabelNode>();
for (AbstractInsnNode insn : source) {
if (insn instanceof LabelNode labelInsn) {
labels.put(labelInsn, new LabelNode());
}
}
return labels;
}
}