/*******************************************************************************
* Copyright (c) 2015 Jeff Martin.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Lesser General Public
* License v3.0 which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/lgpl.html
*
* Contributors:
* Jeff Martin - initial API and implementation
******************************************************************************/
package cuchaz.enigma.bytecode;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import cuchaz.enigma.mapping.ClassEntry;
import cuchaz.enigma.mapping.ClassNameReplacer;
import cuchaz.enigma.mapping.Translator;
import javassist.CtClass;
import javassist.bytecode.*;
import javassist.bytecode.SignatureAttribute.*;
public class ClassRenamer {
private enum SignatureType {
Class {
@Override
public String rename(String signature, ReplacerClassMap map) {
return renameClassSignature(signature, map);
}
},
Field {
@Override
public String rename(String signature, ReplacerClassMap map) {
return renameFieldSignature(signature, map);
}
},
Method {
@Override
public String rename(String signature, ReplacerClassMap map) {
return renameMethodSignature(signature, map);
}
};
public abstract String rename(String signature, ReplacerClassMap map);
}
private static class ReplacerClassMap extends HashMap {
private ClassNameReplacer m_replacer;
public ReplacerClassMap(ClassNameReplacer replacer) {
m_replacer = replacer;
}
@Override
public String get(Object obj) {
if (obj instanceof String) {
return get((String) obj);
}
return null;
}
public String get(String className) {
return m_replacer.replace(className);
}
}
public static void renameClasses(CtClass c, final Translator translator) {
renameClasses(c, className -> {
ClassEntry entry = translator.translateEntry(new ClassEntry(className));
if (entry != null) {
return entry.getName();
}
return null;
});
}
public static void moveAllClassesOutOfDefaultPackage(CtClass c, final String newPackageName) {
renameClasses(c, className -> {
ClassEntry entry = new ClassEntry(className);
if (entry.isInDefaultPackage()) {
return newPackageName + "/" + entry.getName();
}
return null;
});
}
@SuppressWarnings("unchecked")
public static void renameClasses(CtClass c, ClassNameReplacer replacer) {
// sadly, we can't use CtClass.renameClass() because SignatureAttribute.renameClass() is extremely buggy =(
ReplacerClassMap map = new ReplacerClassMap(replacer);
ClassFile classFile = c.getClassFile();
// rename the constant pool (covers ClassInfo, MethodTypeInfo, and NameAndTypeInfo)
ConstPool constPool = c.getClassFile().getConstPool();
constPool.renameClass(map);
// rename class attributes
renameAttributes(classFile.getAttributes(), map, SignatureType.Class);
// rename methods
for (MethodInfo methodInfo : (List) classFile.getMethods()) {
methodInfo.setDescriptor(Descriptor.rename(methodInfo.getDescriptor(), map));
renameAttributes(methodInfo.getAttributes(), map, SignatureType.Method);
}
// rename fields
for (FieldInfo fieldInfo : (List) classFile.getFields()) {
fieldInfo.setDescriptor(Descriptor.rename(fieldInfo.getDescriptor(), map));
renameAttributes(fieldInfo.getAttributes(), map, SignatureType.Field);
}
// rename the class name itself last
// NOTE: don't use the map here, because setName() calls the buggy SignatureAttribute.renameClass()
// we only want to replace exactly this class name
String newName = renameClassName(c.getName(), map);
if (newName != null) {
c.setName(newName);
}
// replace simple names in the InnerClasses attribute too
InnerClassesAttribute attr = (InnerClassesAttribute) c.getClassFile().getAttribute(InnerClassesAttribute.tag);
if (attr != null) {
for (int i = 0; i < attr.tableLength(); i++) {
// get the inner class full name (which has already been translated)
ClassEntry classEntry = new ClassEntry(Descriptor.toJvmName(attr.innerClass(i)));
if (attr.innerNameIndex(i) != 0) {
// update the inner name
attr.setInnerNameIndex(i, constPool.addUtf8Info(classEntry.getInnermostClassName()));
}
/* DEBUG
System.out.println(String.format("\tDEOBF: %s-> ATTR: %s,%s,%s", classEntry, attr.outerClass(i), attr.innerClass(i), attr.innerName(i)));
*/
}
}
}
@SuppressWarnings("unchecked")
private static void renameAttributes(List attributes, ReplacerClassMap map, SignatureType type) {
try {
// make the rename class method accessible
Method renameClassMethod = AttributeInfo.class.getDeclaredMethod("renameClass", Map.class);
renameClassMethod.setAccessible(true);
for (AttributeInfo attribute : attributes) {
if (attribute instanceof SignatureAttribute) {
// this has to be handled specially because SignatureAttribute.renameClass() is buggy as hell
SignatureAttribute signatureAttribute = (SignatureAttribute) attribute;
String newSignature = type.rename(signatureAttribute.getSignature(), map);
if (newSignature != null) {
signatureAttribute.setSignature(newSignature);
}
} else if (attribute instanceof CodeAttribute) {
// code attributes have signature attributes too (indirectly)
CodeAttribute codeAttribute = (CodeAttribute) attribute;
renameAttributes(codeAttribute.getAttributes(), map, type);
} else if (attribute instanceof LocalVariableTypeAttribute) {
// lvt attributes have signature attributes too
LocalVariableTypeAttribute localVariableAttribute = (LocalVariableTypeAttribute) attribute;
renameLocalVariableTypeAttribute(localVariableAttribute, map);
} else {
renameClassMethod.invoke(attribute, map);
}
}
} catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
throw new Error("Unable to call javassist methods by reflection!", ex);
}
}
private static void renameLocalVariableTypeAttribute(LocalVariableTypeAttribute attribute, ReplacerClassMap map) {
// adapted from LocalVariableAttribute.renameClass()
ConstPool cp = attribute.getConstPool();
int n = attribute.tableLength();
byte[] info = attribute.get();
for (int i = 0; i < n; ++i) {
int pos = i * 10 + 2;
int index = ByteArray.readU16bit(info, pos + 6);
if (index != 0) {
String signature = cp.getUtf8Info(index);
String newSignature = renameLocalVariableSignature(signature, map);
if (newSignature != null) {
ByteArray.write16bit(cp.addUtf8Info(newSignature), info, pos + 6);
}
}
}
}
private static String renameLocalVariableSignature(String signature, ReplacerClassMap map) {
// for some reason, signatures with . in them don't count as field signatures
// looks like anonymous classes delimit with . in stead of $
// convert the . to $, but keep track of how many we replace
// we need to put them back after we translate
int start = signature.lastIndexOf('$') + 1;
int numConverted = 0;
StringBuilder buf = new StringBuilder(signature);
for (int i = buf.length() - 1; i >= start; i--) {
char c = buf.charAt(i);
if (c == '.') {
buf.setCharAt(i, '$');
numConverted++;
}
}
signature = buf.toString();
// translate
String newSignature = renameFieldSignature(signature, map);
if (newSignature != null) {
// put the delimiters back
buf = new StringBuilder(newSignature);
for (int i = buf.length() - 1; i >= 0 && numConverted > 0; i--) {
char c = buf.charAt(i);
if (c == '$') {
buf.setCharAt(i, '.');
numConverted--;
}
}
assert (numConverted == 0);
newSignature = buf.toString();
return newSignature;
}
return null;
}
private static String renameClassSignature(String signature, ReplacerClassMap map) {
try {
ClassSignature type = renameType(SignatureAttribute.toClassSignature(signature), map);
return type.encode();
} catch (BadBytecode ex) {
throw new Error("Can't parse field signature: " + signature);
}
}
private static String renameFieldSignature(String signature, ReplacerClassMap map) {
try {
ObjectType type = renameType(SignatureAttribute.toFieldSignature(signature), map);
if (type != null) {
return type.encode();
}
return null;
} catch (BadBytecode ex) {
throw new Error("Can't parse class signature: " + signature);
}
}
private static String renameMethodSignature(String signature, ReplacerClassMap map) {
try {
MethodSignature type = renameType(SignatureAttribute.toMethodSignature(signature), map);
return type.encode();
} catch (BadBytecode ex) {
throw new Error("Can't parse method signature: " + signature);
}
}
private static ClassSignature renameType(ClassSignature type, ReplacerClassMap map) {
TypeParameter[] typeParamTypes = type.getParameters();
if (typeParamTypes != null) {
typeParamTypes = Arrays.copyOf(typeParamTypes, typeParamTypes.length);
for (int i = 0; i < typeParamTypes.length; i++) {
TypeParameter newParamType = renameType(typeParamTypes[i], map);
if (newParamType != null) {
typeParamTypes[i] = newParamType;
}
}
}
ClassType superclassType = type.getSuperClass();
if (superclassType != ClassType.OBJECT) {
ClassType newSuperclassType = renameType(superclassType, map);
if (newSuperclassType != null) {
superclassType = newSuperclassType;
}
}
ClassType[] interfaceTypes = type.getInterfaces();
if (interfaceTypes != null) {
interfaceTypes = Arrays.copyOf(interfaceTypes, interfaceTypes.length);
for (int i = 0; i < interfaceTypes.length; i++) {
ClassType newInterfaceType = renameType(interfaceTypes[i], map);
if (newInterfaceType != null) {
interfaceTypes[i] = newInterfaceType;
}
}
}
return new ClassSignature(typeParamTypes, superclassType, interfaceTypes);
}
private static MethodSignature renameType(MethodSignature type, ReplacerClassMap map) {
TypeParameter[] typeParamTypes = type.getTypeParameters();
if (typeParamTypes != null) {
typeParamTypes = Arrays.copyOf(typeParamTypes, typeParamTypes.length);
for (int i = 0; i < typeParamTypes.length; i++) {
TypeParameter newParamType = renameType(typeParamTypes[i], map);
if (newParamType != null) {
typeParamTypes[i] = newParamType;
}
}
}
Type[] paramTypes = type.getParameterTypes();
if (paramTypes != null) {
paramTypes = Arrays.copyOf(paramTypes, paramTypes.length);
for (int i = 0; i < paramTypes.length; i++) {
Type newParamType = renameType(paramTypes[i], map);
if (newParamType != null) {
paramTypes[i] = newParamType;
}
}
}
Type returnType = type.getReturnType();
if (returnType != null) {
Type newReturnType = renameType(returnType, map);
if (newReturnType != null) {
returnType = newReturnType;
}
}
ObjectType[] exceptionTypes = type.getExceptionTypes();
if (exceptionTypes != null) {
exceptionTypes = Arrays.copyOf(exceptionTypes, exceptionTypes.length);
for (int i = 0; i < exceptionTypes.length; i++) {
ObjectType newExceptionType = renameType(exceptionTypes[i], map);
if (newExceptionType != null) {
exceptionTypes[i] = newExceptionType;
}
}
}
return new MethodSignature(typeParamTypes, paramTypes, returnType, exceptionTypes);
}
private static Type renameType(Type type, ReplacerClassMap map) {
if (type instanceof ObjectType) {
return renameType((ObjectType) type, map);
} else if (type instanceof BaseType) {
return renameType((BaseType) type, map);
} else {
throw new Error("Don't know how to rename type " + type.getClass());
}
}
private static ObjectType renameType(ObjectType type, ReplacerClassMap map) {
if (type instanceof ArrayType) {
return renameType((ArrayType) type, map);
} else if (type instanceof ClassType) {
return renameType((ClassType) type, map);
} else if (type instanceof TypeVariable) {
return renameType((TypeVariable) type, map);
} else {
throw new Error("Don't know how to rename type " + type.getClass());
}
}
private static BaseType renameType(BaseType type, ReplacerClassMap map) {
// don't have to rename primitives
return null;
}
private static TypeVariable renameType(TypeVariable type, ReplacerClassMap map) {
// don't have to rename template args
return null;
}
private static ClassType renameType(ClassType type, ReplacerClassMap map) {
// translate type args
TypeArgument[] args = type.getTypeArguments();
if (args != null) {
args = Arrays.copyOf(args, args.length);
for (int i = 0; i < args.length; i++) {
TypeArgument newType = renameType(args[i], map);
if (newType != null) {
args[i] = newType;
}
}
}
if (type instanceof NestedClassType) {
NestedClassType nestedType = (NestedClassType) type;
// translate the name
String name = getClassName(type);
String newName = map.get(name);
if (newName != null) {
name = new ClassEntry(newName).getInnermostClassName();
}
// translate the parent class too
ClassType parent = renameType(nestedType.getDeclaringClass(), map);
if (parent == null) {
parent = nestedType.getDeclaringClass();
}
return new NestedClassType(parent, name, args);
} else {
// translate the name
String name = type.getName();
String newName = renameClassName(name, map);
if (newName != null) {
name = newName;
}
return new ClassType(name, args);
}
}
private static String getClassName(ClassType type) {
if (type instanceof NestedClassType) {
NestedClassType nestedType = (NestedClassType) type;
return getClassName(nestedType.getDeclaringClass()) + "$" + Descriptor.toJvmName(type.getName().replace('.', '$'));
} else {
return Descriptor.toJvmName(type.getName());
}
}
private static String renameClassName(String name, ReplacerClassMap map) {
String newName = map.get(Descriptor.toJvmName(name));
if (newName != null) {
return Descriptor.toJavaName(newName);
}
return null;
}
private static TypeArgument renameType(TypeArgument type, ReplacerClassMap map) {
ObjectType subType = type.getType();
if (subType != null) {
ObjectType newSubType = renameType(subType, map);
if (newSubType != null) {
switch (type.getKind()) {
case ' ':
return new TypeArgument(newSubType);
case '+':
return TypeArgument.subclassOf(newSubType);
case '-':
return TypeArgument.superOf(newSubType);
default:
throw new Error("Unknown type kind: " + type.getKind());
}
}
}
return null;
}
private static ArrayType renameType(ArrayType type, ReplacerClassMap map) {
Type newSubType = renameType(type.getComponentType(), map);
if (newSubType != null) {
return new ArrayType(type.getDimension(), newSubType);
}
return null;
}
private static TypeParameter renameType(TypeParameter type, ReplacerClassMap map) {
ObjectType superclassType = type.getClassBound();
if (superclassType != null) {
ObjectType newSuperclassType = renameType(superclassType, map);
if (newSuperclassType != null) {
superclassType = newSuperclassType;
}
}
ObjectType[] interfaceTypes = type.getInterfaceBound();
if (interfaceTypes != null) {
interfaceTypes = Arrays.copyOf(interfaceTypes, interfaceTypes.length);
for (int i = 0; i < interfaceTypes.length; i++) {
ObjectType newInterfaceType = renameType(interfaceTypes[i], map);
if (newInterfaceType != null) {
interfaceTypes[i] = newInterfaceType;
}
}
}
return new TypeParameter(type.getName(), superclassType, interfaceTypes);
}
}