/*
* 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.code;
import java.util.Deque;
import java.util.LinkedList;
import java.util.ListIterator;
import java.util.NoSuchElementException;
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.At;
import org.spongepowered.asm.mixin.injection.InjectionPoint;
import org.spongepowered.asm.mixin.injection.InjectionPoint.Selector;
import org.spongepowered.asm.mixin.injection.Slice;
import org.spongepowered.asm.mixin.injection.throwables.InjectionError;
import org.spongepowered.asm.mixin.injection.throwables.InvalidSliceException;
import org.spongepowered.asm.util.Bytecode;
import org.spongepowered.asm.util.Annotations;
import com.google.common.base.Strings;
/**
* Stores information about a defined method slice for a particular injector.
*/
public final class MethodSlice {
/**
* A read-only wrapper for an {@link InsnList} which only allows the segment
* identified by {@link #start} and {@link end} to be accessed. In essence
* this class provides a <em>view</em> of the underlying InsnList.
*/
static final class InsnListSlice extends ReadOnlyInsnList {
/**
* ListIterator for the slice view, wraps an iterator returned by the
* underlying {@link InsnList} and ensures that consumers can only
* traverse the slice.
*
* <p>Note that this doesn't handle changes in the underlying InsnList
* which occur after instatiation, care should be taken not to modify
* the list via other means whilst this iterator is in use.</p>
*/
static class SliceIterator implements ListIterator<AbstractInsnNode> {
/**
* The underlying ListIterator returned by the parent list
*/
private final ListIterator<AbstractInsnNode> iter;
/**
* Brackets
*/
private int start, end;
/**
* Virtual index, used to keep track of bounds
*/
private int index;
public SliceIterator(ListIterator<AbstractInsnNode> iter, int start, int end, int index) {
this.iter = iter;
this.start = start;
this.end = end;
this.index = index;
}
/* (non-Javadoc)
* @see java.util.ListIterator#hasNext()
*/
@Override
public boolean hasNext() {
return this.index <= this.end && this.iter.hasNext();
}
/* (non-Javadoc)
* @see java.util.ListIterator#next()
*/
@Override
public AbstractInsnNode next() {
if (this.index > this.end) {
throw new NoSuchElementException();
}
this.index++;
return this.iter.next();
}
/* (non-Javadoc)
* @see java.util.ListIterator#hasPrevious()
*/
@Override
public boolean hasPrevious() {
return this.index > this.start ;
}
/* (non-Javadoc)
* @see java.util.ListIterator#previous()
*/
@Override
public AbstractInsnNode previous() {
if (this.index <= this.start) {
throw new NoSuchElementException();
}
this.index--;
return this.iter.previous();
}
/* (non-Javadoc)
* @see java.util.ListIterator#nextIndex()
*/
@Override
public int nextIndex() {
return this.index - this.start;
}
/* (non-Javadoc)
* @see java.util.ListIterator#previousIndex()
*/
@Override
public int previousIndex() {
return this.index - this.start - 1;
}
/* (non-Javadoc)
* @see java.util.ListIterator#remove()
*/
@Override
public void remove() {
throw new UnsupportedOperationException("Cannot remove insn from slice");
}
/* (non-Javadoc)
* @see java.util.ListIterator#set(java.lang.Object)
*/
@Override
public void set(AbstractInsnNode e) {
throw new UnsupportedOperationException("Cannot set insn using slice");
}
/* (non-Javadoc)
* @see java.util.ListIterator#add(java.lang.Object)
*/
@Override
public void add(AbstractInsnNode e) {
throw new UnsupportedOperationException("Cannot add insn using slice");
}
}
/**
* Brackets
*/
private final int start, end;
protected InsnListSlice(InsnList inner, int start, int end) {
super(inner);
// Start and end are validated prior to construction
this.start = start;
this.end = end;
}
/* (non-Javadoc)
* @see org.spongepowered.asm.mixin.injection.code.ReadOnlyInsnList
* #iterator()
*/
@Override
public ListIterator<AbstractInsnNode> iterator() {
return this.iterator(0);
}
/* (non-Javadoc)
* @see org.spongepowered.asm.mixin.injection.code.ReadOnlyInsnList
* #iterator(int)
*/
@Override
public ListIterator<AbstractInsnNode> iterator(int index) {
// Return the bracketed iterator
return new SliceIterator(super.iterator(this.start + index), this.start, this.end, this.start + index);
}
/* (non-Javadoc)
* @see org.spongepowered.asm.mixin.injection.code.ReadOnlyInsnList
* #toArray()
*/
@Override
public AbstractInsnNode[] toArray() {
AbstractInsnNode[] all = super.toArray();
AbstractInsnNode[] subset = new AbstractInsnNode[this.size()];
System.arraycopy(all, this.start, subset, 0, subset.length);
return subset;
}
/* (non-Javadoc)
* @see org.spongepowered.asm.mixin.injection.code.ReadOnlyInsnList
* #size()
*/
@Override
public int size() {
return (this.end - this.start) + 1;
}
/* (non-Javadoc)
* @see org.spongepowered.asm.mixin.injection.code.ReadOnlyInsnList
* #getFirst()
*/
@Override
public AbstractInsnNode getFirst() {
return super.get(this.start);
}
/* (non-Javadoc)
* @see org.spongepowered.asm.mixin.injection.code.ReadOnlyInsnList
* #getLast()
*/
@Override
public AbstractInsnNode getLast() {
return super.get(this.end);
}
/* (non-Javadoc)
* @see org.spongepowered.asm.mixin.injection.code.ReadOnlyInsnList
* #get(int)
*/
@Override
public AbstractInsnNode get(int index) {
return super.get(this.start + index);
}
/* (non-Javadoc)
* @see org.spongepowered.asm.mixin.injection.code.ReadOnlyInsnList
* #contains(org.spongepowered.asm.lib.tree.AbstractInsnNode)
*/
@Override
public boolean contains(AbstractInsnNode insn) {
for (AbstractInsnNode node : this.toArray()) {
if (node == insn) {
return true;
}
}
return false;
}
/**
* Returns the index of the specified instruction in the slice, use
* {@link #realIndexOf} to determine the index of the instruction in the
* underlying InsnList.
*
* @param insn Instruction to inspect
* @return instruction's index in the list
*/
@Override
public int indexOf(AbstractInsnNode insn) {
int index = super.indexOf(insn);
return index >= this.start && index <= this.end ? index - this.start : -1;
}
/**
* Returns the index of the instruction in the underlying InsnLis
*
* @param insn Instruction to inspect
* @return instruction's index in the list
*/
public int realIndexOf(AbstractInsnNode insn) {
return super.indexOf(insn);
}
}
/**
* Owner of this slice
*/
private final ISliceContext owner;
/**
* Slice ID as declared, slice ID in the parent {@link MethodSlices}
* collection may be different
*/
private final String id;
/**
* Injection point which defines the start of this slice (inclusive), may be
* null as long as {@link #to} is not null
*/
private final InjectionPoint from;
/**
* Injection point which defines the end of this slice (inclusive), may be
* null as long as {@link #from} is not null
*/
private final InjectionPoint to;
/**
* Descriptive name of the slice, used in exceptions
*/
private final String name;
/**
* ctor
*
* @param owner owner
* @param id declared id
* @param from start point, may be null as long as {@link #to} is not null
* @param to end point, may be null as long as {@link #from} is not null
*/
private MethodSlice(ISliceContext owner, String id, InjectionPoint from, InjectionPoint to) {
if (from == null && to == null) {
throw new InvalidSliceException(owner, String.format("%s is redundant. No 'from' or 'to' value specified", this));
}
this.owner = owner;
this.id = Strings.nullToEmpty(id);
this.from = from;
this.to = to;
this.name = MethodSlice.getSliceName(id);
}
/**
* Get the <em>declared</em> id of this slice
*/
public String getId() {
return this.id;
}
/**
* Get a sliced insn list based on the parameters specified in this slice
*
* @param method method to slice
* @return read only slice
*/
public ReadOnlyInsnList getSlice(MethodNode method) {
int max = method.instructions.size() - 1;
int start = this.find(method, this.from, 0, this.name + "(from)");
int end = this.find(method, this.to, max, this.name + "(to)");
if (start > end) {
throw new InvalidSliceException(this.owner, String.format("%s is negative size. Range(%d -> %d)", this.describe(), start, end));
}
if (start < 0 || end < 0 || start > max || end > max) {
throw new InjectionError("Unexpected critical error in " + this + ": out of bounds start=" + start + " end=" + end + " lim=" + max);
}
if (start == 0 && end == max) {
return new ReadOnlyInsnList(method.instructions);
}
return new InsnListSlice(method.instructions, start, end);
}
/**
* Runs the specified injection point as a query on the method and returns
* the index of the instruction matching the query. Returns the default
* value if the query returns zero results.
*
* @param method Method to query
* @param injectionPoint Query to run
* @param defaultValue Value to return if query fails
* @param description Description for error message
* @return matching insn index
*/
private int find(MethodNode method, InjectionPoint injectionPoint, int defaultValue, String description) {
if (injectionPoint == null) {
return defaultValue;
}
Deque<AbstractInsnNode> nodes = new LinkedList<AbstractInsnNode>();
ReadOnlyInsnList insns = new ReadOnlyInsnList(method.instructions);
boolean result = injectionPoint.find(method.desc, insns, nodes);
Selector select = injectionPoint.getSelector();
if (nodes.size() != 1 && select == Selector.ONE) {
throw new InvalidSliceException(this.owner, String.format("%s requires 1 result but found %d", this.describe(description), nodes.size()));
}
if (!result) {
return defaultValue;
}
return method.instructions.indexOf(select == Selector.FIRST ? nodes.getFirst() : nodes.getLast());
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return this.describe();
}
private String describe() {
return this.describe(this.name);
}
private String describe(String description) {
return MethodSlice.describeSlice(description, this.owner);
}
private static String describeSlice(String description, ISliceContext owner) {
String annotation = Bytecode.getSimpleName(owner.getAnnotation());
MethodNode method = owner.getMethod();
return String.format("%s->%s(%s)::%s%s", owner.getContext(), annotation, description, method.name, method.desc);
}
private static String getSliceName(String id) {
return String.format("@Slice[%s]", Strings.nullToEmpty(id));
}
/**
* Parses the supplied annotation into a MethodSlice
*
* @param owner Owner injection info
* @param slice Annotation to parse
* @return parsed MethodSlice
*/
public static MethodSlice parse(ISliceContext owner, Slice slice) {
String id = slice.id();
At from = slice.from();
At to = slice.to();
InjectionPoint fromPoint = from != null ? InjectionPoint.parse(owner, from) : null;
InjectionPoint toPoint = to != null ? InjectionPoint.parse(owner, to) : null;
return new MethodSlice(owner, id, fromPoint, toPoint);
}
/**
* Parses the supplied annotation into a MethodSlice
*
* @param info Owner injection info
* @param node Annotation to parse
* @return parsed MethodSlice
*/
public static MethodSlice parse(ISliceContext info, AnnotationNode node) {
String id = Annotations.<String>getValue(node, "id");
AnnotationNode from = Annotations.<AnnotationNode>getValue(node, "from");
AnnotationNode to = Annotations.<AnnotationNode>getValue(node, "to");
InjectionPoint fromPoint = from != null ? InjectionPoint.parse(info, from) : null;
InjectionPoint toPoint = to != null ? InjectionPoint.parse(info, to) : null;
return new MethodSlice(info, id, fromPoint, toPoint);
}
}