/* * This file is part of Mixin, licensed under the MIT License (MIT). * * Copyright (c) SpongePowered <https://www.spongepowered.org> * Copyright (c) contributors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.spongepowered.asm.mixin.injection; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import org.apache.logging.log4j.LogManager; import org.spongepowered.asm.lib.tree.AbstractInsnNode; import org.spongepowered.asm.lib.tree.AnnotationNode; import org.spongepowered.asm.lib.tree.InsnList; import org.spongepowered.asm.lib.tree.MethodNode; import org.spongepowered.asm.mixin.injection.modify.AfterStoreLocal; import org.spongepowered.asm.mixin.injection.modify.BeforeLoadLocal; import org.spongepowered.asm.mixin.injection.points.AfterInvoke; import org.spongepowered.asm.mixin.injection.points.BeforeFieldAccess; import org.spongepowered.asm.mixin.injection.points.BeforeFinalReturn; import org.spongepowered.asm.mixin.injection.points.BeforeInvoke; import org.spongepowered.asm.mixin.injection.points.BeforeNew; import org.spongepowered.asm.mixin.injection.points.BeforeReturn; import org.spongepowered.asm.mixin.injection.points.BeforeStringInvoke; import org.spongepowered.asm.mixin.injection.points.JumpInsnPoint; import org.spongepowered.asm.mixin.injection.points.MethodHead; import org.spongepowered.asm.mixin.injection.struct.InjectionPointData; import org.spongepowered.asm.mixin.injection.throwables.InvalidInjectionException; import org.spongepowered.asm.mixin.refmap.IMixinContext; import org.spongepowered.asm.util.Annotations; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList.Builder; /** * <p>Base class for injection point discovery classes. Each subclass describes * a strategy for locating code injection points within a method, with the * {@link #find} method populating a collection with insn nodes from the method * which satisfy its strategy.</p> * * <p>This base class also contains composite strategy factory methods such as * {@link #and} and {@link #or} which allow strategies to be combined using * intersection (and) or union (or) relationships to allow multiple strategies * to be easily combined.</p> * * <p>You are free to create your own injection point subclasses, but take note * that it <b>is allowed</b> for a single InjectionPoint instance to be used for * multiple injections and thus implementing classes MUST NOT cache the insn * list, event, or nodes instance passed to the {@link #find} method, as each * call to {@link #find} must be considered a separate functional contract and * the InjectionPoint's lifespan is not linked to the discovery lifespan, * therefore it is important that the InjectionPoint implementation is fully * <b>stateless</b>.</p> */ public abstract class InjectionPoint { /** * Injection point code for {@link At} annotations to use */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface AtCode { /** * The string code used to specify the annotated injection point in At * annotations */ public String value(); } /** * Selector type for slice delmiters, ignored for normal injection points. * <tt>Selectors</tt> can be supplied in {@link At} annotations by including * a colon (<tt>:</tt>) character followed by the selector type * (case-sensitive), eg: * * <blockquote><pre>@At(value = "INVOKE:LAST", ... )</pre></blockquote> */ public enum Selector { /** * Use the <em>first</em> instruction from the query result. */ FIRST, /** * Use the <em>last</em> instruction from the query result. */ LAST, /** * The query <b>must return exactly one</b> instruction, if it returns * more than one instruction this should be considered a fail-fast error * state and a runtime exception will be thrown. */ ONE; /** * Default selector type used if no selector is explicitly specified. * <em>For internal use only. Currently {@link #FIRST}</em> */ public static final Selector DEFAULT = Selector.FIRST; } /** * Available injection point types */ private static Map<String, Class<? extends InjectionPoint>> types = new HashMap<String, Class<? extends InjectionPoint>>(); static { // Standard Injection Points InjectionPoint.register(BeforeFieldAccess.class); InjectionPoint.register(BeforeInvoke.class); InjectionPoint.register(BeforeNew.class); InjectionPoint.register(BeforeReturn.class); InjectionPoint.register(BeforeStringInvoke.class); InjectionPoint.register(JumpInsnPoint.class); InjectionPoint.register(MethodHead.class); InjectionPoint.register(AfterInvoke.class); InjectionPoint.register(BeforeLoadLocal.class); InjectionPoint.register(AfterStoreLocal.class); InjectionPoint.register(BeforeFinalReturn.class); } private final String slice; private final Selector selector; protected InjectionPoint() { this("", Selector.DEFAULT); } protected InjectionPoint(InjectionPointData data) { this(data.getSlice(), data.getSelector()); } public InjectionPoint(String slice, Selector selector) { this.slice = slice; this.selector = selector; } public String getSlice() { return this.slice; } public Selector getSelector() { return this.selector; } /** * Find injection points in the supplied insn list * * @param desc Method descriptor, supplied to allow return types and * arguments etc. to be determined * @param insns Insn list to search in, the strategy MUST ONLY add nodes * from this list to the {@code nodes} collection * @param nodes Collection of nodes to populate. Injectors should NOT make * any assumptions about the state of this collection and should only * call the <b>add()</b> method * @return true if one or more injection points were found */ public abstract boolean find(String desc, InsnList insns, Collection<AbstractInsnNode> nodes); /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return "InjectionPoint(" + this.getClass().getSimpleName() + ")"; } /** * Get the insn immediately following the specified insn, or return the same * insn if the insn is the last insn in the list * * @param insns Insn list to fetch from * @param insn Insn node * @return Next insn or the same insn if last in the list */ protected static AbstractInsnNode nextNode(InsnList insns, AbstractInsnNode insn) { int index = insns.indexOf(insn) + 1; if (index > 0 && index < insns.size()) { return insns.get(index); } return insn; } /** * Composite injection point */ abstract static class CompositeInjectionPoint extends InjectionPoint { protected final InjectionPoint[] components; protected CompositeInjectionPoint(InjectionPoint... components) { if (components == null || components.length < 2) { throw new IllegalArgumentException("Must supply two or more component injection points for composite point!"); } this.components = components; } /* (non-Javadoc) * @see org.spongepowered.asm.mixin.injection.InjectionPoint#toString() */ @Override public String toString() { return "CompositeInjectionPoint(" + this.getClass().getSimpleName() + ")[" + Joiner.on(',').join(this.components) + "]"; } } /** * Intersection of several injection points, returns common nodes that * appear in all children */ static final class Intersection extends InjectionPoint.CompositeInjectionPoint { public Intersection(InjectionPoint... points) { super(points); } @SuppressWarnings("unchecked") @Override public boolean find(String desc, InsnList insns, Collection<AbstractInsnNode> nodes) { boolean found = false; ArrayList<AbstractInsnNode>[] allNodes = (ArrayList<AbstractInsnNode>[]) Array.newInstance(ArrayList.class, this.components.length); for (int i = 0; i < this.components.length; i++) { allNodes[i] = new ArrayList<AbstractInsnNode>(); this.components[i].find(desc, insns, allNodes[i]); } ArrayList<AbstractInsnNode> alpha = allNodes[0]; for (int nodeIndex = 0; nodeIndex < alpha.size(); nodeIndex++) { AbstractInsnNode node = alpha.get(nodeIndex); boolean in = true; for (int b = 1; b < allNodes.length; b++) { if (!allNodes[b].contains(node)) { break; } } if (!in) { continue; } nodes.add(node); found = true; } return found; } } /** * Union of several injection points, returns all insns returned from all * injections */ static final class Union extends InjectionPoint.CompositeInjectionPoint { public Union(InjectionPoint... points) { super(points); } @Override public boolean find(String desc, InsnList insns, Collection<AbstractInsnNode> nodes) { LinkedHashSet<AbstractInsnNode> allNodes = new LinkedHashSet<AbstractInsnNode>(); for (int i = 0; i < this.components.length; i++) { this.components[i].find(desc, insns, allNodes); } nodes.addAll(allNodes); return allNodes.size() > 0; } } /** * Shift injection point, takes an input injection point and shifts all * returned nodes by a fixed amount */ static final class Shift extends InjectionPoint { private final InjectionPoint input; private final int shift; public Shift(InjectionPoint input, int shift) { if (input == null) { throw new IllegalArgumentException("Must supply an input injection point for SHIFT"); } this.input = input; this.shift = shift; } /* (non-Javadoc) * @see org.spongepowered.asm.mixin.injection.InjectionPoint#toString() */ @Override public String toString() { return "InjectionPoint(" + this.getClass().getSimpleName() + ")[" + this.input + "]"; } @Override public boolean find(String desc, InsnList insns, Collection<AbstractInsnNode> nodes) { List<AbstractInsnNode> list = (nodes instanceof List) ? (List<AbstractInsnNode>) nodes : new ArrayList<AbstractInsnNode>(nodes); this.input.find(desc, insns, nodes); for (int i = 0; i < list.size(); i++) { list.set(i, insns.get(insns.indexOf(list.get(i)) + this.shift)); } if (nodes != list) { nodes.clear(); nodes.addAll(list); } return nodes.size() > 0; } } /** * Returns a composite injection point which returns the intersection of * nodes from all component injection points * * @param operands injection points to perform intersection * @return adjusted InjectionPoint */ public static InjectionPoint and(InjectionPoint... operands) { return new InjectionPoint.Intersection(operands); } /** * Returns a composite injection point which returns the union of nodes from * all component injection points * * @param operands injection points to perform union * @return adjusted InjectionPoint */ public static InjectionPoint or(InjectionPoint... operands) { return new InjectionPoint.Union(operands); } /** * Returns an injection point which returns all insns immediately following * insns from the supplied injection point * * @param point injection points to perform shift * @return adjusted InjectionPoint */ public static InjectionPoint after(InjectionPoint point) { return new InjectionPoint.Shift(point, 1); } /** * Returns an injection point which returns all insns immediately prior to * insns from the supplied injection point * * @param point injection points to perform shift * @return adjusted InjectionPoint */ public static InjectionPoint before(InjectionPoint point) { return new InjectionPoint.Shift(point, -1); } /** * Returns an injection point which returns all insns offset by the * specified "count" from insns from the supplied injection point * * @param point injection points to perform shift * @param count amount to shift by * @return adjusted InjectionPoint */ public static InjectionPoint shift(InjectionPoint point, int count) { return new InjectionPoint.Shift(point, count); } /** * Parse a collection of InjectionPoints from the supplied {@link At} * annotations * * @param owner Data for the mixin containing the annotation, used to obtain * the refmap, amongst other things * @param ats {@link At} annotations to parse information from * @return InjectionPoint parsed from the supplied data or null if parsing * failed */ public static List<InjectionPoint> parse(IInjectionPointContext owner, List<AnnotationNode> ats) { return InjectionPoint.parse(owner.getContext(), owner.getMethod(), owner.getAnnotation(), ats); } /** * Parse a collection of InjectionPoints from the supplied {@link At} * annotations * * @param context Data for the mixin containing the annotation, used to * obtain the refmap, amongst other things * @param method The annotated handler method * @param parent The parent annotation which owns this {@link At} annotation * @param ats {@link At} annotations to parse information from * @return InjectionPoint parsed from the supplied data or null if parsing * failed */ public static List<InjectionPoint> parse(IMixinContext context, MethodNode method, AnnotationNode parent, List<AnnotationNode> ats) { Builder<InjectionPoint> injectionPoints = ImmutableList.<InjectionPoint>builder(); for (AnnotationNode at : ats) { InjectionPoint injectionPoint = InjectionPoint.parse(context, method, parent, at); if (injectionPoint != null) { injectionPoints.add(injectionPoint); } } return injectionPoints.build(); } /** * Parse an InjectionPoint from the supplied {@link At} annotation * * @param owner Data for the mixin containing the annotation, used to obtain * the refmap, amongst other things * @param at {@link At} annotation to parse information from * @return InjectionPoint parsed from the supplied data or null if parsing * failed */ public static InjectionPoint parse(IInjectionPointContext owner, At at) { return InjectionPoint.parse(owner.getContext(), owner.getMethod(), owner.getAnnotation(), at.value(), at.shift(), at.by(), Arrays.asList(at.args()), at.target(), at.slice(), at.ordinal(), at.opcode()); } /** * Parse an InjectionPoint from the supplied {@link At} annotation * * @param context Data for the mixin containing the annotation, used to * obtain the refmap, amongst other things * @param method The annotated handler method * @param parent The parent annotation which owns this {@link At} annotation * @param at {@link At} annotation to parse information from * @return InjectionPoint parsed from the supplied data or null if parsing * failed */ public static InjectionPoint parse(IMixinContext context, MethodNode method, AnnotationNode parent, At at) { return InjectionPoint.parse(context, method, parent, at.value(), at.shift(), at.by(), Arrays.asList(at.args()), at.target(), at.slice(), at.ordinal(), at.opcode()); } /** * Parse an InjectionPoint from the supplied {@link At} annotation supplied * as an AnnotationNode instance * * @param owner Data for the mixin containing the annotation, used to obtain * the refmap, amongst other things * @param node {@link At} annotation to parse information from * @return InjectionPoint parsed from the supplied data or null if parsing * failed */ public static InjectionPoint parse(IInjectionPointContext owner, AnnotationNode node) { return InjectionPoint.parse(owner.getContext(), owner.getMethod(), owner.getAnnotation(), node); } /** * Parse an InjectionPoint from the supplied {@link At} annotation supplied * as an AnnotationNode instance * * @param context Data for the mixin containing the annotation, used to * obtain the refmap, amongst other things * @param method The annotated handler method * @param parent The parent annotation which owns this {@link At} annotation * @param node {@link At} annotation to parse information from * @return InjectionPoint parsed from the supplied data or null if parsing * failed */ public static InjectionPoint parse(IMixinContext context, MethodNode method, AnnotationNode parent, AnnotationNode node) { String at = Annotations.<String>getValue(node, "value"); List<String> args = Annotations.<List<String>>getValue(node, "args"); String target = Annotations.<String>getValue(node, "target", ""); String slice = Annotations.<String>getValue(node, "slice", ""); At.Shift shift = Annotations.<At.Shift>getValue(node, "shift", At.Shift.class, At.Shift.NONE); int by = Annotations.<Integer>getValue(node, "by", Integer.valueOf(0)); int ordinal = Annotations.<Integer>getValue(node, "ordinal", Integer.valueOf(-1)); int opcode = Annotations.<Integer>getValue(node, "opcode", Integer.valueOf(0)); if (args == null) { args = ImmutableList.<String>of(); } return InjectionPoint.parse(context, method, parent, at, shift, by, args, target, slice, ordinal, opcode); } /** * Parse and instantiate an InjectionPoint from the supplied information. * Returns null if an InjectionPoint could not be created. * * @param context Data for the mixin containing the annotation, used to * obtain the refmap, amongst other things * @param method The annotated handler method * @param parent The parent annotation which owns this {@link At} annotation * @param at Injection point specifier * @param shift Shift type to apply * @param by Amount of shift to apply for the BY shift type * @param args Named parameters * @param target Target for supported injection points * @param slice Slice id for injectors which support multiple slices * @param ordinal Ordinal offset for supported injection points * @param opcode Bytecode opcode for supported injection points * @return InjectionPoint parsed from the supplied data or null if parsing * failed */ public static InjectionPoint parse(IMixinContext context, MethodNode method, AnnotationNode parent, String at, At.Shift shift, int by, List<String> args, String target, String slice, int ordinal, int opcode) { InjectionPointData data = new InjectionPointData(context, method, parent, at, args, target, slice, ordinal, opcode); Class<? extends InjectionPoint> ipClass = findClass(context, data); InjectionPoint point = InjectionPoint.create(context, data, ipClass); return InjectionPoint.shift(point, shift, by); } @SuppressWarnings("unchecked") private static Class<? extends InjectionPoint> findClass(IMixinContext context, InjectionPointData data) { String type = data.getType(); Class<? extends InjectionPoint> ipClass = InjectionPoint.types.get(type); if (ipClass == null) { if (type.matches("^([A-Za-z_][A-Za-z0-9_]*\\.)+[A-Za-z_][A-Za-z0-9_]*$")) { try { ipClass = (Class<? extends InjectionPoint>)Class.forName(type); InjectionPoint.types.put(type, ipClass); } catch (Exception ex) { throw new InvalidInjectionException(context, data + " could not be loaded or is not a valid InjectionPoint", ex); } } else { throw new InvalidInjectionException(context, data + " is not a valid injection point specifier"); } } return ipClass; } private static InjectionPoint create(IMixinContext context, InjectionPointData data, Class<? extends InjectionPoint> ipClass) { Constructor<? extends InjectionPoint> ipCtor = null; try { ipCtor = ipClass.getDeclaredConstructor(InjectionPointData.class); ipCtor.setAccessible(true); } catch (NoSuchMethodException ex) { throw new InvalidInjectionException(context, ipClass.getName() + " must contain a constructor which accepts an InjectionPointData", ex); } InjectionPoint point = null; try { point = ipCtor.newInstance(data); } catch (Exception ex) { throw new InvalidInjectionException(context, "Error whilst instancing injection point " + ipClass.getName() + " for " + data.getAt(), ex); } return point; } private static InjectionPoint shift(InjectionPoint point, At.Shift shift, int by) { if (point != null) { if (shift == At.Shift.BEFORE) { return InjectionPoint.before(point); } else if (shift == At.Shift.AFTER) { return InjectionPoint.after(point); } else if (shift == At.Shift.BY) { return InjectionPoint.shift(point, by); } } return point; } /** * Register an injection point class. The supplied class must be decorated * with an {@link AtCode} annotation for registration purposes. * * @param type injection point type to register */ public static void register(Class<? extends InjectionPoint> type) { AtCode code = type.<AtCode>getAnnotation(AtCode.class); if (code == null) { throw new IllegalArgumentException("Injection point class " + type + " is not annotated with @AtCode"); } Class<? extends InjectionPoint> existing = InjectionPoint.types.get(code.value()); if (existing != null && !existing.equals(type)) { LogManager.getLogger("mixin").debug("Overriding InjectionPoint {} with {} (previously {})", code.value(), type.getName(), existing.getName()); } InjectionPoint.types.put(code.value(), type); } }