/******************************************************************************* * Copyright (c) 2014 Jeff Martin. * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Public License v3.0 * which accompanies this distribution, and is available at * http://www.gnu.org/licenses/gpl.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 javassist.CtClass; import javassist.bytecode.AttributeInfo; import javassist.bytecode.BadBytecode; import javassist.bytecode.ByteArray; import javassist.bytecode.ClassFile; import javassist.bytecode.CodeAttribute; import javassist.bytecode.ConstPool; import javassist.bytecode.Descriptor; import javassist.bytecode.FieldInfo; import javassist.bytecode.InnerClassesAttribute; import javassist.bytecode.LocalVariableTypeAttribute; import javassist.bytecode.MethodInfo; import javassist.bytecode.SignatureAttribute; import javassist.bytecode.SignatureAttribute.ArrayType; import javassist.bytecode.SignatureAttribute.BaseType; import javassist.bytecode.SignatureAttribute.ClassSignature; import javassist.bytecode.SignatureAttribute.ClassType; import javassist.bytecode.SignatureAttribute.MethodSignature; import javassist.bytecode.SignatureAttribute.NestedClassType; import javassist.bytecode.SignatureAttribute.ObjectType; import javassist.bytecode.SignatureAttribute.Type; import javassist.bytecode.SignatureAttribute.TypeArgument; import javassist.bytecode.SignatureAttribute.TypeParameter; import javassist.bytecode.SignatureAttribute.TypeVariable; import cuchaz.enigma.mapping.ClassEntry; import cuchaz.enigma.mapping.ClassNameReplacer; import cuchaz.enigma.mapping.Translator; public class ClassRenamer { private static 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 static final long serialVersionUID = 317915213205066168L; 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, new ClassNameReplacer() { @Override public String replace(String 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, new ClassNameReplacer() { @Override public String replace(String className) { ClassEntry entry = new ClassEntry(className); if (entry.isInDefaultPackage()) { return newPackageName + "/" + entry.getName(); } return null; } }); } public static void moveAllClassesIntoDefaultPackage(CtClass c, final String oldPackageName) { renameClasses(c, new ClassNameReplacer() { @Override public String replace(String className) { ClassEntry entry = new ClassEntry(className); if (entry.getPackageName().equals(oldPackageName)) { return entry.getSimpleName(); } 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); if (type != null) { return type.encode(); } return null; } 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); if (type != null) { return type.encode(); } return null; } 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