/*
* Copyright (C) 2015 Sebastian Daschner, sebastian-daschner.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.sebastian_daschner.jaxrs_analyzer.analysis.bytecode.simulation;
import com.sebastian_daschner.jaxrs_analyzer.model.Types;
import com.sebastian_daschner.jaxrs_analyzer.model.elements.Element;
import com.sebastian_daschner.jaxrs_analyzer.model.elements.MethodHandle;
import com.sebastian_daschner.jaxrs_analyzer.model.instructions.*;
import com.sebastian_daschner.jaxrs_analyzer.model.methods.Method;
import com.sebastian_daschner.jaxrs_analyzer.model.methods.MethodIdentifier;
import org.objectweb.asm.Label;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
import java.util.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static com.sebastian_daschner.jaxrs_analyzer.model.JavaUtils.determineLeastSpecificType;
import static com.sebastian_daschner.jaxrs_analyzer.model.JavaUtils.toType;
/**
* Simulates the instructions of a method. This class is thread-safe.
*
* @author Sebastian Daschner
*/
public class MethodSimulator {
private final Lock lock = new ReentrantLock();
private final MethodPool methodPool = MethodPool.getInstance();
private final Stack<Element> runtimeStack = new Stack<>();
private final MultivaluedMap<Label, Integer> variableInvalidation = new MultivaluedHashMap<>();
private Label active;
Map<Integer, Element> localVariables = new HashMap<>();
private Element returnElement;
/**
* Simulates the instructions and collects information about the resource method.
*
* @param instructions The instructions of the method
* @return The return element merged with all possible values
*/
public Element simulate(final List<Instruction> instructions) {
lock.lock();
try {
returnElement = null;
return simulateInternal(instructions);
} finally {
lock.unlock();
}
}
/**
* Simulates the instructions of the method.
*
* @param instructions The instructions to simulate
* @return The return element of the method
*/
Element simulateInternal(final List<Instruction> instructions) {
instructions.forEach(this::simulate);
return returnElement;
}
/**
* Simulates the instruction.
*
* @param instruction The instruction to simulate
*/
private void simulate(final Instruction instruction) {
switch (instruction.getType()) {
case PUSH:
final PushInstruction pushInstruction = (PushInstruction) instruction;
runtimeStack.push(new Element(pushInstruction.getValueType(), pushInstruction.getValue()));
break;
case METHOD_HANDLE:
simulateMethodHandle((InvokeDynamicInstruction) instruction);
break;
case INVOKE:
simulateInvoke((InvokeInstruction) instruction);
break;
case GET_FIELD:
runtimeStack.pop();
runtimeStack.push(new Element(((GetFieldInstruction) instruction).getPropertyType()));
break;
case GET_STATIC:
final GetStaticInstruction getStaticInstruction = (GetStaticInstruction) instruction;
final Object value = getStaticInstruction.getValue();
if (value != null)
runtimeStack.push(new Element(getStaticInstruction.getPropertyType(), value));
else
runtimeStack.push(new Element(getStaticInstruction.getPropertyType()));
break;
case LOAD:
final LoadInstruction loadInstruction = (LoadInstruction) instruction;
runtimeStack.push(localVariables.getOrDefault(loadInstruction.getNumber(), new Element(loadInstruction.getVariableType())));
runtimeStack.peek().getTypes().add(loadInstruction.getVariableType());
variableInvalidation.add(loadInstruction.getValidUntil(), loadInstruction.getNumber());
break;
case STORE:
simulateStore((StoreInstruction) instruction);
break;
case SIZE_CHANGE:
simulateSizeChange((SizeChangingInstruction) instruction);
break;
case NEW:
final NewInstruction newInstruction = (NewInstruction) instruction;
runtimeStack.push(new Element(toType(newInstruction.getClassName())));
break;
case DUP:
runtimeStack.push(runtimeStack.peek());
break;
case OTHER:
// do nothing
break;
case RETURN:
mergeReturnElement(runtimeStack.pop());
case THROW:
mergePossibleResponse();
// stack has to be empty for further analysis
runtimeStack.clear();
break;
default:
throw new IllegalArgumentException("Instruction without type!");
}
if (instruction.getLabel() != active && variableInvalidation.containsKey(active)) {
variableInvalidation.get(active).forEach(localVariables::remove);
}
active = instruction.getLabel();
}
/**
* Simulates the invoke dynamic call. Pushes a method handle on the stack.
*
* @param instruction The instruction to simulate
*/
private void simulateMethodHandle(final InvokeDynamicInstruction instruction) {
final List<Element> arguments = IntStream.range(0, instruction.getDynamicIdentifier().getParameters().size())
.mapToObj(t -> runtimeStack.pop()).collect(Collectors.toList());
Collections.reverse(arguments);
if (!instruction.getDynamicIdentifier().isStaticMethod())
// first parameter is `this`
arguments.remove(0);
// adds the transferred arguments of the bootstrap call
runtimeStack.push(new MethodHandle(instruction.getDynamicIdentifier().getReturnType(), instruction.getIdentifier(), arguments));
}
/**
* Simulates the invoke instruction.
*
* @param instruction The instruction to simulate
*/
private void simulateInvoke(final InvokeInstruction instruction) {
final List<Element> arguments = new LinkedList<>();
MethodIdentifier identifier = instruction.getIdentifier();
IntStream.range(0, identifier.getParameters().size()).forEach(i -> arguments.add(runtimeStack.pop()));
Collections.reverse(arguments);
Element object = null;
Method method;
if (!identifier.isStaticMethod()) {
object = runtimeStack.pop();
if (object instanceof MethodHandle) {
method = (Method) object;
} else {
method = methodPool.get(identifier);
}
} else {
method = methodPool.get(identifier);
}
final Element returnedElement = method.invoke(object, arguments);
if (returnedElement != null)
runtimeStack.push(returnedElement);
else if (!identifier.getReturnType().equals(Types.PRIMITIVE_VOID))
runtimeStack.push(new Element(identifier.getReturnType()));
}
/**
* Simulates the store instruction.
*
* @param instruction The instruction to simulate
*/
private void simulateStore(final StoreInstruction instruction) {
final int index = instruction.getNumber();
final Element elementToStore = runtimeStack.pop();
if (elementToStore instanceof MethodHandle)
mergeMethodHandleStore(index, (MethodHandle) elementToStore);
else
mergeElementStore(index, instruction.getVariableType(), elementToStore);
}
/**
* Merges a stored element to the local variables.
*
* @param index The index of the variable
* @param type The type of the variable or the element (whatever is more specific)
* @param element The element to merge
*/
private void mergeElementStore(final int index, final String type, final Element element) {
// new element must be created for immutability
final String elementType = type.equals(Types.OBJECT) ? determineLeastSpecificType(element.getTypes().toArray(new String[element.getTypes().size()])) : type;
final Element created = new Element(elementType);
created.merge(element);
localVariables.merge(index, created, Element::merge);
}
/**
* Merges a stored method handle to the local variables.
*
* @param index The index of the variable
* @param methodHandle The method handle to merge
*/
private void mergeMethodHandleStore(final int index, final MethodHandle methodHandle) {
localVariables.merge(index, new MethodHandle(methodHandle), Element::merge);
}
/**
* Checks if the current stack element is eligible for being merged with the returned element.
*/
private void mergePossibleResponse() {
// TODO only HttpResponse element?
if (!runtimeStack.isEmpty() && runtimeStack.peek().getTypes().contains(Types.RESPONSE)) {
mergeReturnElement(runtimeStack.peek());
}
}
/**
* Simulates the size change instruction.
*
* @param instruction The instruction to simulate
*/
private void simulateSizeChange(final SizeChangingInstruction instruction) {
IntStream.range(0, instruction.getNumberOfPops()).forEach(i -> runtimeStack.pop());
IntStream.range(0, instruction.getNumberOfPushes()).forEach(i -> runtimeStack.push(new Element()));
}
/**
* Merges the {@code returnElement} with the given element which was popped from the stack.
* If the {@code returnElement} existed before, the values are merged.
*
* @param stackElement The popped element
*/
private void mergeReturnElement(final Element stackElement) {
if (returnElement != null)
stackElement.merge(returnElement);
returnElement = stackElement;
}
}