/* * The MIT License (MIT) * * Copyright (c) 2016. Diorite (by Bartłomiej Mazur (aka GotoFinal)) * * 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.diorite.inject.impl.controller; import javax.annotation.Nullable; import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.FieldInsnNode; import org.objectweb.asm.tree.InsnList; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodNode; import org.objectweb.asm.tree.TypeInsnNode; import org.objectweb.asm.tree.VarInsnNode; import org.diorite.inject.impl.utils.AsmUtils; final class TransformerInjectTracker implements Opcodes { private final Transformer transformer; private final FieldInsnNode fieldInsnNode; private final boolean isStatic; private InsnList resultNodeList; private final InsnList initNodeList; private InjectionType injectionType = InjectionType.UNKNOWN; private PlaceholderType placeholderType = PlaceholderType.UNKNOWN; @Nullable private LoadCode loadCode = null; @Nullable private MethodInsnNode placeholderNode = null; private TransformerInjectTracker(Transformer transformer, FieldInsnNode fieldInsnNode, InsnList insnList) { this.transformer = transformer; this.fieldInsnNode = fieldInsnNode; this.isStatic = fieldInsnNode.getOpcode() == PUTSTATIC; this.resultNodeList = insnList; this.initNodeList = insnList; } public static TransformerInjectTracker trackFromField(Transformer transformer, FieldInsnNode fieldInsnNode, InsnList insnList) { TransformerInjectTracker injectTracker = new TransformerInjectTracker(transformer, fieldInsnNode, insnList); injectTracker.run(); return injectTracker; } public InjectionType getInjectionType() { return this.injectionType; } public PlaceholderType getPlaceholderType() { return this.placeholderType; } public FieldInsnNode getFieldInsnNode() { return this.fieldInsnNode; } public InsnList getInitNodeList() { return this.initNodeList; } public InsnList getResultNodeList() { return this.resultNodeList; } public MethodInsnNode getResult() { if (this.placeholderNode == null) { throw new IllegalStateException("Can't get result from invalid tracker!"); } return this.placeholderNode; } private AbstractInsnNode skipCheckCastBackwards(AbstractInsnNode node) { // skip possible (?) ALOAD 0 if not static if (! this.isStatic && (node instanceof VarInsnNode) && (node.getOpcode() == ALOAD) && (((VarInsnNode) node).var == 0)) { node = node.getPrevious(); } // skip possible check cast if ((node instanceof TypeInsnNode) && (node.getOpcode() == CHECKCAST)) { node = node.getPrevious(); } // skip possible (?) ALOAD 0 if not static if (! this.isStatic && (node instanceof VarInsnNode) && (node.getOpcode() == ALOAD) && (((VarInsnNode) node).var == 0)) { node = node.getPrevious(); } return node; } private AbstractInsnNode getPrevious(boolean skipCheckCast) { AbstractInsnNode previous = this.fieldInsnNode.getPrevious(); if (skipCheckCast) { previous = this.skipCheckCastBackwards(previous); } return previous; } // example: // ALOAD 0 // might be in other place? // INVOKESTATIC org/diorite/inject/InjectionLibrary.inject ()Ljava/lang/Object; // CHECKCAST org/diorite/inject/injections/Module // optional? // PUTFIELD/PUTSTATIC org/diorite/inject/injections/AdvancedExampleObject.module3 : Lorg/diorite/inject/injections/Module; private InjectionType checkDirect() { AbstractInsnNode previous = this.getPrevious(true); PlaceholderType injectPlaceholder = Transformer.isInjectPlaceholder(previous); if (injectPlaceholder != PlaceholderType.INVALID) { assert previous instanceof MethodInsnNode; this.placeholderNode = (MethodInsnNode) previous; this.placeholderType = injectPlaceholder; this.injectionType = InjectionType.DIRECT; return this.injectionType; } return InjectionType.UNKNOWN; } // indirect example: // INVOKESTATIC org/diorite/inject/InjectionLibrary.inject ()Ljava/lang/Object; // CHECKCAST org/diorite/inject/injections/Module // XSTORE 1 // inject result // <code to skip, or track local variable changes?> // XLOAD 0 // this // XLOAD 1 // inject result // PUTFIELD org/diorite/inject/injections/AdvancedExampleObject.module2 : Lorg/diorite/inject/injections/Module; private InjectionType checkIndirect() { AbstractInsnNode previous = this.getPrevious(true); // find var index if ((previous instanceof VarInsnNode) && AsmUtils.isLoadCode(previous.getOpcode())) { this.loadCode = new LoadCode((VarInsnNode) previous); // XLOAD 0 don't exist here in any case. if (! this.isStatic && (this.loadCode.getIndex() == 0)) { throw new AnalyzeError("Unexpected loading of `this` variable!"); } previous = previous.getPrevious(); } else { return InjectionType.UNKNOWN; } // track all local variable changes previous = this.trackLoadBackwards(previous); // final check for placeholder method PlaceholderType injectPlaceholder = Transformer.isInjectPlaceholder(previous); if (injectPlaceholder != PlaceholderType.INVALID) { assert previous instanceof MethodInsnNode; this.placeholderNode = (MethodInsnNode) previous; this.placeholderType = injectPlaceholder; this.injectionType = InjectionType.INDIRECT; return this.injectionType; } return InjectionType.UNKNOWN; } private AbstractInsnNode trackLoadBackwards(AbstractInsnNode node) { assert this.loadCode != null; // skip checkcast and possible XLOAD 0 node = this.skipCheckCastBackwards(node); // find compatible STORE node. while ((! (node instanceof VarInsnNode)) || ! this.loadCode.isCompatible((VarInsnNode) node)) { node = node.getPrevious(); } // one more to skip STORE node = node.getPrevious(); // node is STORE opcode for our variable, there might be checkcast here: node = this.skipCheckCastBackwards(node); // now there might be INVOKESTATIC or some other indirections: // if there is XLOAD different than 0 (if not static), then we have next layer of indirect store. if ((node instanceof VarInsnNode) && (! this.isStatic && (((VarInsnNode) node).var != 0)) && (((VarInsnNode) node).var != this.loadCode.getIndex()) && AsmUtils.isLoadCode(node.getOpcode())) { this.loadCode = new LoadCode((VarInsnNode) node); node = this.trackLoadBackwards(node); } // but if there is INVOKE* instead, then we need to go inside invoked method and keep looking for injection placeholder! But skip void methods. else if ((node instanceof MethodInsnNode) && AsmUtils.isInvokeCode(node.getOpcode()) && ! ((MethodInsnNode) node).desc.endsWith("V")) { MethodInsnNode methodInvoke = (MethodInsnNode) node; // maybe this is already placeholder invoke? if (Transformer.isInjectPlaceholder(methodInvoke) != PlaceholderType.INVALID) { return node; } node = this.trackMethod(methodInvoke); } else { throw new AnalyzeError("Can't track given field!"); } // now there should be finally INVOKESTATIC, so we can return. return node; } // track method invoke private AbstractInsnNode trackMethod(MethodInsnNode methodInvoke) { // it must be from this same class if (! methodInvoke.owner.equals(this.fieldInsnNode.owner)) { throw new AnalyzeError("Can't use injections across different classes! Found indirect injection from " + methodInvoke.owner + "#" + methodInvoke.name + " " + methodInvoke.desc + "."); } // get and check method MethodNode method = this.transformer.getMethod(methodInvoke); if (method == null) { throw new AnalyzeError("Can't find delegated injection method! Found indirect injection from " + methodInvoke.owner + "#" + methodInvoke.name + " " + methodInvoke.desc + "."); } // this is our node, probably return this.trackMethod(method); } // when tracing delegated methods we do this from top to bottom, so this time we need to find first placeholder invoke, and it should be it. private AbstractInsnNode trackMethod(MethodNode methodNode) { // get first instruction and go down until invoke is found. AbstractInsnNode node = methodNode.instructions.getFirst(); do { if (node == null) { throw new AnalyzeError("Can't track given field in delegated method (" + methodNode.name + " " + methodNode.desc + ")! Missing invoke placeholder! Remember that you can't use multiple method delegations!"); } node = node.getNext(); if (Transformer.isInjectPlaceholder(node) != PlaceholderType.INVALID) { this.resultNodeList = methodNode.instructions; return node; } } while (true); } // if injection is delegated to other method, we need to track this method. // INVOKESPECIAL org/diorite/inject/injections/AdvancedExampleObject.injectEdit (I)Lorg/diorite/inject/injections/Module; // PUTFIELD org/diorite/inject/injections/AdvancedExampleObject.edit : Lorg/diorite/inject/injections/Module; private InjectionType checkDelegated() { // skip any checks and get previous node AbstractInsnNode previous = this.getPrevious(true); // now this should be INVOKE* opcode, if it isn't we are all doomed. if (! (previous instanceof MethodInsnNode) || ! AsmUtils.isInvokeCode(previous.getOpcode())) { return InjectionType.UNKNOWN; } MethodInsnNode methodInvoke = (MethodInsnNode) previous; previous = this.trackMethod(methodInvoke); // final check for placeholder method PlaceholderType injectPlaceholder = Transformer.isInjectPlaceholder(previous); if (injectPlaceholder != PlaceholderType.INVALID) { assert previous instanceof MethodInsnNode; this.placeholderNode = (MethodInsnNode) previous; this.placeholderType = injectPlaceholder; this.injectionType = InjectionType.DELEGATED; return this.injectionType; } return InjectionType.UNKNOWN; } public void run() { if (this.isStatic) { throw new AnalyzeError("Can't inject static field!"); } this.checkDirect(); if (this.injectionType == InjectionType.DIRECT) { return; } this.checkIndirect(); if (this.injectionType == InjectionType.INDIRECT) { return; } this.checkDelegated(); if (this.injectionType == InjectionType.UNKNOWN) { throw new AnalyzeError("Can't find inject invoke, make sure that your code is valid, maybe try use simpler structure for this field!"); } } @SuppressWarnings("rawtypes") abstract class VarCode<T extends VarCode> { final VarInsnNode varInsnNode; VarCode(VarInsnNode varInsnNode) { this.varInsnNode = varInsnNode; } public VarInsnNode getVarInsnNode() { return this.varInsnNode; } public int getOpcode() { return this.varInsnNode.getOpcode(); } public int getIndex() { return this.varInsnNode.var; } public boolean isCompatible(VarInsnNode varInsnNode) { VarCode varCode; if (AsmUtils.isLoadCode(varInsnNode.getOpcode())) { if (AsmUtils.isLoadCode(this.getOpcode())) { return false; } return ((StoreCode) this).isCompatible(new LoadCode(varInsnNode)); } else if (AsmUtils.isStoreCode(varInsnNode.getOpcode())) { if (AsmUtils.isStoreCode(this.getOpcode())) { return false; } return ((LoadCode) this).isCompatible(new StoreCode(varInsnNode)); } else { throw new AnalyzeError("Unexpected VarInsnNode Opcode!"); } } public boolean isCompatible(T varCode) { return this.getIndex() == varCode.getIndex(); } } class LoadCode extends VarCode<StoreCode> { LoadCode(VarInsnNode varInsnNode) { super(varInsnNode); if (! AsmUtils.isLoadCode(this.getOpcode())) { throw new IllegalArgumentException("Expected STORE opcode"); } } @Override public boolean isCompatible(StoreCode varCode) { if (! super.isCompatible(varCode)) { return false; } switch (this.getOpcode()) { case ALOAD: return varCode.getOpcode() == ASTORE; case ILOAD: return varCode.getOpcode() == ISTORE; case LLOAD: return varCode.getOpcode() == LSTORE; case FLOAD: return varCode.getOpcode() == FSTORE; case DLOAD: return varCode.getOpcode() == DSTORE; default: return false; } } } class StoreCode extends VarCode<LoadCode> { StoreCode(VarInsnNode varInsnNode) { super(varInsnNode); if (! AsmUtils.isStoreCode(this.getOpcode())) { throw new IllegalArgumentException("Expected STORE opcode"); } } @Override public boolean isCompatible(LoadCode varCode) { if (! super.isCompatible(varCode)) { return false; } switch (this.getOpcode()) { case ASTORE: return varCode.getOpcode() == ALOAD; case ISTORE: return varCode.getOpcode() == ILOAD; case LSTORE: return varCode.getOpcode() == LLOAD; case FSTORE: return varCode.getOpcode() == FLOAD; case DSTORE: return varCode.getOpcode() == DLOAD; default: return false; } } } enum InjectionType { UNKNOWN, // when you directly put result of injection into field. DIRECT, // when result is first saved to some variable. INDIRECT, // when result is returned by other method DELEGATED, } enum PlaceholderType { UNKNOWN, INVALID, NULLABLE, NONNULL } String fieldNodeToString() { return this.fieldInsnNode.owner + "#" + this.fieldInsnNode.name + " " + this.fieldInsnNode.desc; } public class AnalyzeError extends InternalError { private static final long serialVersionUID = 0; public AnalyzeError() { this(fixErrorMessage(TransformerInjectTracker.this, "AnalyzeError")); } public AnalyzeError(String message) { super(fixErrorMessage(TransformerInjectTracker.this, message)); } public AnalyzeError(String message, Throwable cause) { super(fixErrorMessage(TransformerInjectTracker.this, message), cause); } public AnalyzeError(Throwable cause) { super(cause); } } static String fixErrorMessage(TransformerInjectTracker tracker, String message) { return message + " (Source: " + tracker.fieldNodeToString() + ")"; } }