package tv.porst.swfretools.utils.as3;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import tv.porst.swfretools.parser.structures.AS3Code;
import tv.porst.swfretools.parser.structures.AS3Data;
import tv.porst.swfretools.parser.structures.ClassInfo;
import tv.porst.swfretools.parser.structures.ConstantPool;
import tv.porst.swfretools.parser.structures.InstanceInfo;
import tv.porst.swfretools.parser.structures.MethodBody;
import tv.porst.swfretools.parser.structures.MethodBodyList;
import tv.porst.swfretools.parser.structures.MethodInfo;
import tv.porst.swfretools.parser.structures.MethodInfoList;
import tv.porst.swfretools.parser.structures.MultinameInfo;
import tv.porst.swfretools.parser.structures.MultinameInfoList;
import tv.porst.swfretools.parser.structures.NamespaceInfoList;
import tv.porst.swfretools.parser.structures.StringInfoList;
import tv.porst.swfretools.parser.structures.TraitMethod;
import tv.porst.swfretools.parser.structures.TraitsInfo;
import tv.porst.swfretools.parser.structures.TraitsInfoList;
import tv.porst.swfretools.utils.ActionScript3Helpers;
import tv.porst.swfretools.utils.ResolverException;
/**
* This class can take ActionScript 3 information and resolve all the classes
* and methods that belong to all of the classes. This is very important as the
* information that is stored in ActionScript 3 data in an SWF file is very
* fragmented and must be reconstructed before it can be used in any useful
* way, for example to display the ActionScript 3 code in the GUI.
*
* This class takes all the fragmented data and turns it into something saner
* that can easily be processed. While processing the data, this class is
* extremely conservative with errors. Even when, for example, the name of a
* method can not be resolved, the code will still continue and try to
* resolve other aspects of the method such as its return type or its
* arguments. This is important as we can expect this resolver class to try
* make sense of fuzzed ActionScript 3 code in the future.
*/
public final class ActionScript3Resolver {
/**
* This function creates a mapping between method headers and method bodies.
* This is important because whenever a method is referenced, it is
* referenced as an ID into the table of method headers. There is no quick
* way to map from method header to method body though, for example to
* find the code associated with a method. Therefore we just pre-calculate
* this mapping and pass it to all functions that require it.
*
* @param data The ActionScript 3 data that provides method header and
* method body information.
* @param errors Errors that occur during resolving are logged here.
*
* @return A mapping from method info objects to objects that give
* information about the method body.
*/
private static Map<MethodInfo, ResolvedMethod> createMethodInformationMapping(final AS3Data data, final List<String> errors) {
assert data != null : "Data argument must not be null";
assert errors != null : "Errors argument must not be null";
final Map<MethodInfo, ResolvedMethod> methodMapping = new HashMap<MethodInfo, ResolvedMethod>();
final MethodInfoList methodInfos = data.getMethodInfos();
final MethodBodyList methodBodies = data.getMethodBodies();
final ConstantPool constantPool = data.getConstantPool();
final StringInfoList constantStrings = constantPool.getStrings();
final MultinameInfoList multiNames = constantPool.getMultinames();
final NamespaceInfoList namespaces = constantPool.getNamespaces();
// In the first round we are using the existing mapping between method bodies
// and method information fields to create the inverse mapping.
for (final MethodBody methodBody : methodBodies) {
final int methodIndex = methodBody.getMethod().value();
if (methodIndex >= methodInfos.size()) {
errors.add(String.format("Can not find method header for method %d because method header ID is out of bounds", methodIndex));
}
else {
final MethodInfo methodInfo = data.getMethodInfos().get(methodIndex);
final AS3Code code = methodBody.getCode();
final ResolvedMethod resolvedMethod = resolveMethod(methodInfo, code, constantStrings, multiNames, namespaces, errors);
methodMapping.put(methodInfo, resolvedMethod);
}
}
// In the second round we are processing all those method information
// fields that do not have an associated method body. Those are the
// methods that do not contain any ActionScript 3 code.
for (final MethodInfo methodInfo : data.getMethodInfos()) {
if (methodMapping.containsKey(methodInfo)) {
continue;
}
final ResolvedMethod resolvedMethod = resolveMethod(methodInfo, null, constantStrings, multiNames, namespaces, errors);
methodMapping.put(methodInfo, resolvedMethod);
}
return methodMapping;
}
/**
* Reconstructs the given class and its methods.
*
* @param data The fragmented ActionScript 3 data.
* @param classInfo The class information of the class.
* @param instanceInfo The instance information of the class.
* @param methodMapping The previously generated mapping between method
* headers and their associated method bodies.
* @param errors Errors that occur during resolving are logged here.
*
* @return A list of all classes defined in the ActionScript 3 code.
*/
private static ResolvedClass resolveClass(final AS3Data data, final ClassInfo classInfo, final InstanceInfo instanceInfo, final Map<MethodInfo, ResolvedMethod> methodMapping, final List<String> errors) {
assert data != null : "Data argument must not be null";
assert classInfo != null : "Class Info argument must not be null";
assert instanceInfo != null : "Instance Info argument must not be null";
assert methodMapping != null : "Method mapping argument must not be null";
assert errors != null : "Errors list must not be null";
final ConstantPool constantPool = data.getConstantPool();
final StringInfoList constantStrings = constantPool.getStrings();
final MultinameInfoList multiNames = constantPool.getMultinames();
final NamespaceInfoList namespaces = constantPool.getNamespaces();
final MethodInfoList methodList = data.getMethodInfos();
final ResolvedMethod staticConstructor = resolveStaticConstructor(classInfo, data, methodMapping, errors);
final ResolvedMethod constructor = resolveConstructor(instanceInfo, data, methodMapping, errors);
final String[] className = resolveClassName(instanceInfo, constantStrings, multiNames, namespaces, errors);
final List<ResolvedMethod> methods = resolveMethods(instanceInfo.getTraits(), methodList, methodMapping, constantStrings, multiNames, namespaces, errors);
return new ResolvedClass(className, staticConstructor, constructor, methods);
}
/**
* Takes the fragmented information from the ActionScript 3 data and
* reconstructs the defined classes and their methods.
*
* @param data The fragmented ActionScript 3 data.
* @param methodMapping The previously generated mapping between method
* headers and their associated method bodies.
* @param errors Errors that occur during resolving are logged here.
*
* @return A list of all classes defined in the ActionScript 3 code.
*/
private static List<ResolvedClass> resolveClasses(final AS3Data data, final Map<MethodInfo, ResolvedMethod> methodMapping, final List<String> errors) {
assert data != null : "Data argument must not be null";
assert methodMapping != null : "Method mapping argument must not be null";
assert errors != null : "Errors list must not be null";
final List<ResolvedClass> classes = new ArrayList<ResolvedClass>();
for (int i=0;i<data.getClassCount().value();i++) {
if (i >= data.getClasses().size()) {
errors.add(String.format("Class at offset %08X could not be resolved as its class index is out of bounds"));
continue;
}
if (i >= data.getInstances().size()) {
errors.add(String.format("Class at offset %08X could not be resolved as its instance index is out of bounds"));
continue;
}
final ClassInfo classInfo = data.getClasses().get(i);
final InstanceInfo instanceInfo = data.getInstances().get(i);
classes.add(resolveClass(data, classInfo, instanceInfo, methodMapping, errors));
}
return classes;
}
/**
* Resolves the name of a class.
*
* @param instanceInfo The instance information of the class whose name is returned.
* @param constantStringList List of constant strings from the ActionScript 3 data.
* @param multinameList List of multinames from the ActionScript 3 data.
* @param namespaceList List of namespaces from the ActionScript 3 data.
* @param errors Errors that occur during resolving are logged here.
*
* @return The fully qualified name of the class.
*/
private static String[] resolveClassName(final InstanceInfo instanceInfo, final StringInfoList constantStringList, final MultinameInfoList multinameList, final NamespaceInfoList namespaceList, final List<String> errors) {
assert instanceInfo != null : "Instance information argument must not be null";
assert constantStringList != null : "Constant string list argument must not be null";
assert multinameList != null : "Multiname list argument must not be null";
assert namespaceList != null : "Namespace list argument must not be null";
assert errors != null : "Errors list must not be null";
final int classNameIndex = instanceInfo.getName().value();
if (classNameIndex == 0) {
// Not totally sure what to do here.
return new String[] { "???"};
}
else if (classNameIndex - 1 >= multinameList.size()) {
errors.add(String.format("The class name of the method at address %08X could not be resolved as the name index is out of bounds", instanceInfo.getBitPosition() / 8));
return null;
}
else {
final MultinameInfo multinameInfo = multinameList.get(classNameIndex - 1);
try {
return ActionScript3Helpers.resolveMultiname(multinameInfo, constantStringList, namespaceList);
} catch (final ResolverException e) {
errors.add(String.format("The class name of the method at address %08X could not be resolved as the name index is out of bounds", instanceInfo.getBitPosition() / 8));
return null;
}
}
}
/**
* Resolves the constructor of a class.
*
* @param instanceInfo The class whose constructor is resolved.
* @param data The fragmented ActionScript 3 data.
* @param methodMapping The previously generated mapping between method
* headers and their bodies.
* @param errors Errors that occur during resolving are logged here.
*
* @return The resolved constructor for the given class or null if
* the constructor could not be resolved.
*/
private static ResolvedMethod resolveConstructor(final InstanceInfo instanceInfo, final AS3Data data, final Map<MethodInfo, ResolvedMethod> methodMapping, final List<String> errors) {
assert instanceInfo != null : "Instance information argument must not be null";
assert data != null : "Data argument must not be null";
assert methodMapping != null : "Method mapping argument must not be null";
assert errors != null : "Errors list must not be null";
final int constructorId = instanceInfo.getIinit().value();
final MethodInfoList methodInfos = data.getMethodInfos();
if (constructorId >= methodInfos.size()) {
errors.add(String.format("The constructor of the class at offset %08X could not be resolved as its method index is out of bounds", instanceInfo.getBitPosition() / 8));
return null;
}
else {
final ResolvedMethod resolvedConstructor = methodMapping.get(methodInfos.get(constructorId));
return new ResolvedMethod(new String[0], new String[] { "constructor" }, resolvedConstructor.getArguments(), resolvedConstructor == null ? null : resolvedConstructor.getCode());
}
}
/**
* This function takes all the information that is required to create
* resolved method object that contains information about a method
* such as its name, return type, argument types, and method body.
*
* @param methodInfo Header information of the method to resolve.
* @param code ActionScript code that forms the method body.
* This value can be null for methods without associated code.
* @param constantStringList List of constant strings from the ActionScript 3 data.
* @param multinameList List of multinames from the ActionScript 3 data.
* @param namespaceList List of namespaces from the ActionScript 3 data.
* @param errors Errors that occur during resolving are logged here.
*
* @return The resolved method.
*/
private static ResolvedMethod resolveMethod(final MethodInfo methodInfo, final AS3Code code, final StringInfoList constantStringList, final MultinameInfoList multinameList, final NamespaceInfoList namespaceList, final List<String> errors) {
assert methodInfo != null : "Method information argument must not be null";
assert constantStringList != null : "Constant string list argument must not be null";
assert multinameList != null : "Multiname list argument must not be null";
assert namespaceList != null : "Namespace list argument must not be null";
assert errors != null : "Errors list must not be null";
final String name = resolveMethodName(methodInfo, constantStringList, errors);
final String[] returnType = resolveMethodReturnType(methodInfo, constantStringList, multinameList, namespaceList, errors);
final List<String[]> arguments = resolveMethodArguments(methodInfo, constantStringList, multinameList, namespaceList, errors);
return new ResolvedMethod(new String[] { name } , returnType, arguments, code);
}
/**
* Resolves a single class method.
*
* @param traitsInfo The trait that defines the method.
* @param methodList The methods list parsed from the SWF file.
* @param methodMapping The previously generated mapping between method
* headers and their associated method bodies.
* @param constantStringList List of constant strings from the ActionScript 3 data.
* @param multinameList List of multinames from the ActionScript 3 data.
* @param namespaceList List of namespaces from the ActionScript 3 data.
* @param errors Errors that occur during resolving are logged here.
*
* @return The resolved method or null if the method could not be resolved.
*/
private static ResolvedMethod resolveMethod(final TraitsInfo traitsInfo,
final MethodInfoList methodList,
final Map<MethodInfo, ResolvedMethod> methodMapping,
final StringInfoList constantStringList,
final MultinameInfoList multinameList,
final NamespaceInfoList namespaceList,
final List<String> errors) {
assert traitsInfo != null : "Traits information argument must not be null";
assert methodList != null : "Method information argument must not be null";
assert methodMapping != null : "Method mapping argument must not be null";
assert constantStringList != null : "Constant string list argument must not be null";
assert multinameList != null : "Multiname list argument must not be null";
assert namespaceList != null : "Namespace list argument must not be null";
assert errors != null : "Errors list must not be null";
final TraitMethod traitMethod = (TraitMethod) traitsInfo.getData();
final int methodIndex = traitMethod.getMethod().value();
if (methodIndex >= methodList.size()) {
errors.add(String.format("The method at offset %08X could not be resolved as its method index is out of bounds", traitMethod.getBitPosition() / 8));
return null;
}
final String[] methodName = resolveMethodName(traitsInfo, constantStringList, multinameList, namespaceList, errors);
final MethodInfo method = methodList.get(methodIndex);
final ResolvedMethod resolvedMethod = methodMapping.get(method);
final AS3Code code = resolvedMethod.getCode();
return new ResolvedMethod(resolvedMethod.getReturnType(), methodName, resolvedMethod.getArguments(), code);
}
/**
* Resolves all the argument value types of a method.
*
* @param methodInfo Header information of the method to resolve.
* This value can be null for methods without associated code.
* @param constantStringList List of constant strings from the ActionScript 3 data.
* @param multinameList List of multinames from the ActionScript 3 data.
* @param namespaceList List of namespaces from the ActionScript 3 data.
* @param errors Errors that occur during resolving are logged here.
*
* @return A list of the fully qualified types of the method arguments.
*/
private static List<String[]> resolveMethodArguments(final MethodInfo methodInfo, final StringInfoList constantStringList, final MultinameInfoList multinameList, final NamespaceInfoList namespaceList, final List<String> errors) {
assert methodInfo != null : "Method information argument must not be null";
assert constantStringList != null : "Constant string list argument must not be null";
assert multinameList != null : "Multiname list argument must not be null";
assert namespaceList != null : "Namespace list argument must not be null";
assert errors != null : "Errors list must not be null";
final List<String[]> arguments = new ArrayList<String[]>();
for (int i=0;i<methodInfo.getParamTypes().size();i++) {
final int paramTypeIndex = methodInfo.getParamTypes().get(i).value();
if (paramTypeIndex == 0) {
// Type index 0 means the argument can be of any type.
arguments.add(new String[] { "*" });
}
else if (paramTypeIndex - 1 >= multinameList.size()) {
errors.add(String.format("Could not resolve argument of method at offset %08X because name index is out of bounds", methodInfo.getBitPosition() / 8));
arguments.add(null);
}
else {
final MultinameInfo multinameInfo = multinameList.get(paramTypeIndex - 1);
try {
final String[] name = ActionScript3Helpers.resolveMultiname(multinameInfo, constantStringList, namespaceList);
arguments.add(name);
} catch (final ResolverException e) {
errors.add(String.format("Could not resolve argument of method at offset %08X because name index is out of bounds", methodInfo.getBitPosition() / 8));
}
}
}
return arguments;
}
/**
* Tries to resolve the name of a method.
*
* @param methodInfo Method header information of the method to process.
* @param constantStringList List of constant strings from the ActionScript 3 data.
* @param errors Errors that occur during resolving are logged here.
*
* @return The name of the method or null if it could not be resolved.
*/
private static String resolveMethodName(final MethodInfo methodInfo, final StringInfoList constantStringList, final List<String> errors) {
assert methodInfo != null : "Method information argument must not be null";
assert constantStringList != null : "Constant string list argument must not be null";
final int nameIndex = methodInfo.getName().value();
if (nameIndex >= constantStringList.size()) {
errors.add(String.format("Could not resolve name of method at offset %08X because name index is out of bounds", methodInfo.getBitPosition() / 8));
return null;
}
else {
return constantStringList.get(nameIndex).getName().value();
}
}
/**
* Resolves the name of a method.
*
* @param traitsInfo Identifies the method.
* @param constantStringList List of constant strings from the ActionScript 3 data.
* @param multinameList List of multinames from the ActionScript 3 data.
* @param namespaceList List of namespaces from the ActionScript 3 data.
* @param errors Errors that occur during resolving are logged here.
*
* @return The fully qualified method name.
*/
private static String[] resolveMethodName(final TraitsInfo traitsInfo, final StringInfoList constantStringList, final MultinameInfoList multinameList, final NamespaceInfoList namespaceList, final List<String> errors) {
assert traitsInfo != null : "Traits information argument must not be null";
assert constantStringList != null : "Constant string list argument must not be null";
assert multinameList != null : "Multiname list argument must not be null";
assert namespaceList != null : "Namespace list argument must not be null";
assert errors != null : "Errors list must not be null";
final int nameIndex = traitsInfo.getName().value();
if (nameIndex == 0) {
// Not sure what to do here.
return new String[] { "???" };
}
else if (nameIndex -1 >= multinameList.size()) {
errors.add(String.format("Name of method at offset %08X could not be resolved as name index is out of bounds", traitsInfo.getBitPosition() / 8));
return null;
}
else {
final MultinameInfo multiNameInfo = multinameList.get(traitsInfo.getName().value() - 1);
try {
return ActionScript3Helpers.resolveMultiname(multiNameInfo, constantStringList, namespaceList);
} catch (final ResolverException e) {
errors.add(String.format("Name of method at offset %08X could not be resolved as name index is out of bounds", traitsInfo.getBitPosition() / 8));
return null;
}
}
}
/**
* Resolves the return type of a method.
*
* @param methodInfo Method information of the methods whose return type is resolved.
* @param constantStringList List of constant strings from the ActionScript 3 data.
* @param multinameList List of multinames from the ActionScript 3 data.
* @param namespaceList List of namespaces from the ActionScript 3 data.
* @param errors Errors that occur during resolving are logged here.
*
* @return An array containing all the individual parts of the fully qualified
* name of the method return type.
*/
private static String[] resolveMethodReturnType(final MethodInfo methodInfo, final StringInfoList constantStringList, final MultinameInfoList multinameList, final NamespaceInfoList namespaceList, final List<String> errors) {
assert methodInfo != null : "Method information argument must not be null";
assert constantStringList != null : "Constant string list argument must not be null";
assert multinameList != null : "Multiname list argument must not be null";
assert namespaceList != null : "Namespace list argument must not be null";
assert errors != null : "Errors list must not be null";
final int returnTypeIndex = methodInfo.getReturnType().value();
if (returnTypeIndex == 0) {
// Return type index 0 means the return value can be of any type.
return new String[] { "*" };
}
else if (returnTypeIndex - 1 >= multinameList.size()) {
errors.add(String.format("Return type of method at offset %08X could not be resolved because its return type index is out of bounds", methodInfo.getBitPosition() / 8));
return null;
}
else {
final MultinameInfo multinameInfo = multinameList.get(returnTypeIndex - 1);
try {
return ActionScript3Helpers.resolveMultiname(multinameInfo, constantStringList, namespaceList);
} catch (final ResolverException e) {
errors.add(String.format("Return type of method at offset %08X could not be resolved because its return type index is out of bounds", methodInfo.getBitPosition() / 8));
return null;
}
}
}
/**
* Resolves the methods of a class.
*
* @param traits The traits of the class. These define the methods.
* @param methodList The methods list parsed from the SWF file.
* @param methodMapping The previously generated mapping between method
* headers and their associated method bodies.
* @param constantStringList List of constant strings from the ActionScript 3 data.
* @param multinameList List of multinames from the ActionScript 3 data.
* @param namespaceList List of namespaces from the ActionScript 3 data.
* @param errors Errors that occur during resolving are logged here.
*
* @return The resolved methods of the class.
*/
private static List<ResolvedMethod> resolveMethods(final TraitsInfoList traits, final MethodInfoList methodList, final Map<MethodInfo, ResolvedMethod> methodMapping, final StringInfoList constantStringList, final MultinameInfoList multinameList, final NamespaceInfoList namespaceList, final List<String> errors) {
assert traits != null : "Traits argument must not be null";
assert methodList != null : "Method list argument must not be null";
assert methodMapping != null : "Method mapping argument must not be null";
assert constantStringList != null : "Constant string list argument must not be null";
assert multinameList != null : "Multiname list argument must not be null";
assert namespaceList != null : "Namespace list argument must not be null";
assert errors != null : "Errors list must not be null";
final List<ResolvedMethod> methods = new ArrayList<ResolvedMethod>();
for (final TraitsInfo traitsInfo : traits) {
if (traitsInfo.getData() instanceof TraitMethod) {
methods.add(resolveMethod(traitsInfo, methodList, methodMapping, constantStringList, multinameList, namespaceList, errors));
}
}
return methods;
}
/**
* Resolves the static constructor of a class.
*
* @param classInfo The class whose static constructor is resolved.
* @param data The fragmented ActionScript 3 data.
* @param methodMapping The previously generated mapping between method
* headers and their bodies.
* @param errors Errors that occur during resolving are logged here.
*
* @return The resolved static constructor for the given class or null if
* the static constructor could not be resolved.
*/
private static ResolvedMethod resolveStaticConstructor(final ClassInfo classInfo, final AS3Data data, final Map<MethodInfo, ResolvedMethod> methodMapping, final List<String> errors) {
assert classInfo != null : "Class info argument must not be null";
assert data != null : "Data argument must not be null";
assert methodMapping != null : "Method mapping argument must not be null";
assert errors != null : "Errors list must not be null";
final int staticConstructorId = classInfo.getcInit().value();
final MethodInfoList methodInfos = data.getMethodInfos();
if (staticConstructorId >= methodInfos.size()) {
errors.add(String.format("The static constructor of the class at offset %08X could not be resolved as its method index is out of bounds", classInfo.getBitPosition() / 8));
return null;
}
else {
final ResolvedMethod resolvedStaticConstructor = methodMapping.get(methodInfos.get(staticConstructorId));
return new ResolvedMethod(new String[0], new String[] { "staticconstructor" }, new ArrayList<String[]>(), resolvedStaticConstructor == null ? null : resolvedStaticConstructor.getCode());
}
}
/**
* Takes fragmented ActionScript 3 data as parsed from a SWF file and turns it
* into something that fits the model of Flash code analysis better.
*
* @param data The fragmented data from the SWF file.
*
* @return The resolved ActionScript 3 code.
*/
public static ResolvedCode resolve(final AS3Data data) {
if (data == null) {
throw new IllegalArgumentException("Data argument must not be null.");
}
// This list is used to collect errors that occur during resolving.
final List<String> errors = new ArrayList<String>();
// In the first step we are creating a mapping between method headers and
// method bodies. This is needed to quickly access method bodies later.
final Map<MethodInfo, ResolvedMethod> methodMapping = createMethodInformationMapping(data, errors);
// In the second step we are going through all the classes defined in the
// input data and we are trying to recover the individual elements that
// make up that class.
final List<ResolvedClass> classes = resolveClasses(data, methodMapping, errors);
return new ResolvedCode(data, classes);
}
}