/* * 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.struct; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.Deque; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.spongepowered.asm.lib.Opcodes; import org.spongepowered.asm.lib.tree.AnnotationNode; import org.spongepowered.asm.lib.tree.MethodNode; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.MixinEnvironment; import org.spongepowered.asm.mixin.MixinEnvironment.Option; import org.spongepowered.asm.mixin.extensibility.IMixinInfo; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.InjectionNodes.InjectionNode; import org.spongepowered.asm.mixin.injection.InjectionPoint; import org.spongepowered.asm.mixin.injection.InjectorGroupInfo; import org.spongepowered.asm.mixin.injection.ModifyArg; import org.spongepowered.asm.mixin.injection.ModifyConstant; import org.spongepowered.asm.mixin.injection.ModifyVariable; import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.code.ISliceContext; import org.spongepowered.asm.mixin.injection.code.Injector; import org.spongepowered.asm.mixin.injection.code.InjectorTarget; import org.spongepowered.asm.mixin.injection.code.MethodSlice; import org.spongepowered.asm.mixin.injection.code.MethodSlices; import org.spongepowered.asm.mixin.injection.throwables.InjectionError; import org.spongepowered.asm.mixin.injection.throwables.InvalidInjectionException; import org.spongepowered.asm.mixin.refmap.IMixinContext; import org.spongepowered.asm.mixin.struct.SpecialMethodInfo; import org.spongepowered.asm.mixin.transformer.MixinTargetContext; import org.spongepowered.asm.mixin.transformer.meta.MixinMerged; import org.spongepowered.asm.mixin.transformer.throwables.InvalidMixinException; import org.spongepowered.asm.util.Bytecode; import org.spongepowered.asm.util.Annotations; /** * Contructs information about an injection from an {@link Inject} annotation * and allows the injection to be processed. */ public abstract class InjectionInfo extends SpecialMethodInfo implements ISliceContext { /** * Annotated method is static */ protected final boolean isStatic; /** * Target method(s) */ protected final Deque<MethodNode> targets = new ArrayDeque<MethodNode>(); /** * Method slice descriptors parsed from the annotation */ protected final MethodSlices slices; /** * Injection points parsed from * {@link org.spongepowered.asm.mixin.injection.At} annotations */ protected final List<InjectionPoint> injectionPoints = new ArrayList<InjectionPoint>(); /** * Map of lists of nodes enumerated by calling {@link #prepare} */ protected final Map<Target, List<InjectionNode>> targetNodes = new LinkedHashMap<Target, List<InjectionNode>>(); /** * Bytecode injector */ protected Injector injector; /** * Injection group */ protected InjectorGroupInfo group; /** * Methods injected by injectors */ private final List<MethodNode> injectedMethods = new ArrayList<MethodNode>(0); /** * Number of callbacks we expect to inject into targets */ private int expectedCallbackCount = 1; /** * Number of callbacks we require injected */ private int requiredCallbackCount = 0; /** * Actual number of injected callbacks */ private int injectedCallbackCount = 0; /** * ctor * * @param mixin Mixin data * @param method Injector method * @param annotation Annotation to parse */ protected InjectionInfo(MixinTargetContext mixin, MethodNode method, AnnotationNode annotation) { super(mixin, method, annotation); this.isStatic = Bytecode.methodIsStatic(method); this.slices = MethodSlices.parse(this); this.readAnnotation(); } /** * Parse the info from the supplied annotation */ protected void readAnnotation() { if (this.annotation == null) { return; } String type = "@" + Bytecode.getSimpleName(this.annotation); List<AnnotationNode> injectionPoints = this.readInjectionPoints(type); this.findMethods(this.parseTarget(type), type); this.parseInjectionPoints(injectionPoints); this.parseRequirements(); this.injector = this.parseInjector(this.annotation); } protected MemberInfo parseTarget(String type) { String method = Annotations.<String>getValue(this.annotation, "method"); if (method == null) { throw new InvalidInjectionException(this, type + " annotation on " + this.method.name + " is missing method name"); } try { MemberInfo targetMember = MemberInfo.parseAndValidate(method, this.mixin); if (targetMember.owner != null && !targetMember.owner.equals(this.mixin.getTargetClassRef())) { throw new InvalidInjectionException(this, type + " annotation on " + this.method.name + " specifies a target class '" + targetMember.owner + "', which is not supported"); } return targetMember; } catch (InvalidMemberDescriptorException ex) { throw new InvalidInjectionException(this, type + " annotation on " + this.method.name + ", has invalid target descriptor: \"" + method + "\""); } } protected List<AnnotationNode> readInjectionPoints(String type) { List<AnnotationNode> ats = Annotations.<AnnotationNode>getValue(this.annotation, "at", false); if (ats == null) { throw new InvalidInjectionException(this, type + " annotation on " + this.method.name + " is missing 'at' value(s)"); } return ats; } protected void parseInjectionPoints(List<AnnotationNode> ats) { this.injectionPoints.addAll(InjectionPoint.parse(this.mixin, this.method, this.annotation, ats)); } protected void parseRequirements() { this.group = this.mixin.getInjectorGroups().parseGroup(this.method, this.mixin.getDefaultInjectorGroup()).add(this); Integer expect = Annotations.<Integer>getValue(this.annotation, "expect"); if (expect != null) { this.expectedCallbackCount = expect.intValue(); } Integer require = Annotations.<Integer>getValue(this.annotation, "require"); if (require != null && require.intValue() > -1) { this.requiredCallbackCount = require.intValue(); } else if (this.group.isDefault()) { this.requiredCallbackCount = this.mixin.getDefaultRequiredInjections(); } } // stub protected abstract Injector parseInjector(AnnotationNode injectAnnotation); /** * Get whether there is enough valid information in this info to actually * perform an injection. * * @return true if this InjectionInfo was successfully parsed */ public boolean isValid() { return this.targets.size() > 0 && this.injectionPoints.size() > 0; } /** * Discover injection points */ public void prepare() { this.targetNodes.clear(); for (MethodNode targetMethod : this.targets) { Target target = this.mixin.getTargetMethod(targetMethod); InjectorTarget injectorTarget = new InjectorTarget(this, target); this.targetNodes.put(target, this.injector.find(injectorTarget, this.injectionPoints)); injectorTarget.dispose(); } } /** * Perform injections */ public void inject() { for (Entry<Target, List<InjectionNode>> entry : this.targetNodes.entrySet()) { this.injector.inject(entry.getKey(), entry.getValue()); } this.targets.clear(); } /** * Perform cleanup and post-injection tasks */ public void postInject() { for (MethodNode method : this.injectedMethods) { this.classNode.methods.add(method); } if ((MixinEnvironment.getCurrentEnvironment().getOption(Option.DEBUG_INJECTORS) && this.injectedCallbackCount < this.expectedCallbackCount)) { throw new InvalidInjectionException(this, String.format("Injection validation failed: %s %s%s in %s expected %d invocation(s) but %d succeeded", this.getDescription(), this.method.name, this.method.desc, this.mixin, this.expectedCallbackCount, this.injectedCallbackCount)); } else if (this.injectedCallbackCount < this.requiredCallbackCount) { throw new InjectionError( String.format("Critical injection failure: %s %s%s in %s failed injection check, (%d/%d) succeeded", this.getDescription(), this.method.name, this.method.desc, this.mixin, this.injectedCallbackCount, this.requiredCallbackCount)); } } /** * Callback from injector which notifies us that a callback was injected. No * longer used. * * @param target target into which the injector injected */ public void notifyInjected(Target target) { // this.targets.remove(target.method); } protected String getDescription() { return "Callback method"; } @Override public String toString() { return InjectionInfo.describeInjector(this.mixin, this.annotation, this.method); } /** * Get methods being injected into * * @return methods being injected into */ public Collection<MethodNode> getTargets() { return this.targets; } /** * Get the slice descriptors */ @Override public MethodSlice getSlice(String id) { return this.slices.get(this.getSliceId(id)); } /** * Return the mapped slice id for the specified ID. Injectors which only * support use of a single slice will always return the default id (an empty * string) * * @param id slice id * @return mapped id */ public String getSliceId(String id) { return ""; } /** * Get the injected callback count * * @return the injected callback count */ public int getInjectedCallbackCount() { return this.injectedCallbackCount; } /** * Inject a method into the target class * * @param access Method access flags, synthetic will be automatically added * @param name Method name * @param desc Method descriptor * * @return new method */ public MethodNode addMethod(int access, String name, String desc) { MethodNode method = new MethodNode(Opcodes.ASM5, access | Opcodes.ACC_SYNTHETIC, name, desc, null, null); this.injectedMethods.add(method); return method; } /** * Notify method, called by injector when adding a callback into a target * * @param handler callback handler being invoked */ public void addCallbackInvocation(MethodNode handler) { this.injectedCallbackCount++; } /** * Finds methods in the target class which match searchFor * * @param searchFor member info to search for * @param type annotation type */ private void findMethods(MemberInfo searchFor, String type) { this.targets.clear(); int ordinal = 0; for (MethodNode target : this.classNode.methods) { if (searchFor.matches(target.name, target.desc, ordinal)) { boolean isMixinMethod = Annotations.getVisible(target, MixinMerged.class) != null; if (searchFor.matchAll && (Bytecode.methodIsStatic(target) != this.isStatic || target == this.method || isMixinMethod)) { continue; } this.checkTarget(target); this.targets.add(target); ordinal++; } } if (this.targets.size() == 0) { throw new InvalidInjectionException(this, type + " annotation on " + this.method.name + " could not find '" + searchFor.name + "'"); } } private void checkTarget(MethodNode target) { AnnotationNode merged = Annotations.getVisible(target, MixinMerged.class); if (merged == null) { return; } String owner = Annotations.<String>getValue(merged, "mixin"); int priority = Annotations.<Integer>getValue(merged, "priority"); if (priority >= this.mixin.getPriority() && !owner.equals(this.mixin.getClassName())) { throw new InvalidInjectionException(this, this + " cannot inject into " + this.classNode.name + "::" + target.name + target.desc + " merged by " + owner + " with priority " + priority); } if (Annotations.getVisible(target, Final.class) != null) { throw new InvalidInjectionException(this, this + " cannot inject into @Final method " + this.classNode.name + "::" + target.name + target.desc + " merged by " + owner); } } /** * Parse an injector from the specified method (if an injector annotation is * present). If no injector annotation is present then <tt>null</tt> is * returned. * * @param mixin context * @param method mixin method * @return parsed InjectionInfo or null */ public static InjectionInfo parse(MixinTargetContext mixin, MethodNode method) { AnnotationNode annotation = InjectionInfo.getInjectorAnnotation(mixin.getMixin(), method); if (annotation == null) { return null; } if (annotation.desc.endsWith(Inject.class.getSimpleName() + ";")) { return new CallbackInjectionInfo(mixin, method, annotation); } else if (annotation.desc.endsWith(ModifyArg.class.getSimpleName() + ";")) { return new ModifyArgInjectionInfo(mixin, method, annotation); } else if (annotation.desc.endsWith(Redirect.class.getSimpleName() + ";")) { return new RedirectInjectionInfo(mixin, method, annotation); } else if (annotation.desc.endsWith(ModifyVariable.class.getSimpleName() + ";")) { return new ModifyVariableInjectionInfo(mixin, method, annotation); } else if (annotation.desc.endsWith(ModifyConstant.class.getSimpleName() + ";")) { return new ModifyConstantInjectionInfo(mixin, method, annotation); } return null; } /** * Returns any injector annotation found on the specified method. If * multiple matching annotations are found then an exception is thrown. If * no annotations are present then <tt>null</tt> is returned. * * @param mixin context * @param method mixin method * @return annotation or null */ @SuppressWarnings("unchecked") public static AnnotationNode getInjectorAnnotation(IMixinInfo mixin, MethodNode method) { AnnotationNode annotation = null; try { annotation = Annotations.getSingleVisible(method, Inject.class, ModifyArg.class, Redirect.class, ModifyVariable.class, ModifyConstant.class ); } catch (IllegalArgumentException ex) { throw new InvalidMixinException(mixin, "Error parsing annotations on " + method.name + " in " + mixin.getClassName() + ": " + ex.getMessage()); } return annotation; } /** * Get the conform prefix for an injector handler by type * * @param annotation Annotation to inspect * @return conform prefix */ public static String getInjectorPrefix(AnnotationNode annotation) { if (annotation != null) { if (annotation.desc.endsWith(ModifyArg.class.getSimpleName() + ";")) { return "modify"; } else if (annotation.desc.endsWith(Redirect.class.getSimpleName() + ";")) { return "redirect"; } else if (annotation.desc.endsWith(ModifyVariable.class.getSimpleName() + ";")) { return "localvar"; } else if (annotation.desc.endsWith(ModifyConstant.class.getSimpleName() + ";")) { return "constant"; } } return "handler"; } static String describeInjector(IMixinContext mixin, AnnotationNode annotation, MethodNode method) { return String.format("%s->@%s::%s%s", mixin.toString(), Bytecode.getSimpleName(annotation), method.name, method.desc); } }