package net.fybertech.dynamicmappings; import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintWriter; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.net.JarURLConnection; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.jar.JarFile; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.objectweb.asm.ClassReader; import org.objectweb.asm.Type; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.FieldInsnNode; import org.objectweb.asm.tree.FieldNode; import org.objectweb.asm.tree.LdcInsnNode; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodNode; import net.fybertech.dynamicmappings.ParmParser.Parm; import net.fybertech.meddle.MeddleUtil; import net.minecraft.launchwrapper.Launch; public class DynamicMappings { public static final Logger LOGGER = LogManager.getLogger("Meddle"); public static List<String> MAPPINGS_CLASSES = new ArrayList<String>(Arrays.asList(new String[]{ "net.fybertech.dynamicmappings.mappers.SharedMappings", "net.fybertech.dynamicmappings.mappers.ClientMappings" })); /** Deobfuscated class -> obfuscated class */ public static final Map<String, String> classMappings = new HashMap<String, String>(); /** Obfuscated class -> deobfuscated class */ public static final Map<String, String> reverseClassMappings = new HashMap<String, String>(); /** Deobfuscated field -> obfuscated field */ public static final Map<String, String> fieldMappings = new HashMap<String, String>(); /** Obfuscated field -> deobfuscated field */ public static final Map<String, String> reverseFieldMappings = new HashMap<String, String>(); /** Deobfuscated method -> obfuscated method */ public static final Map<String, String> methodMappings = new HashMap<String, String>(); /** Obfuscated method -> deobfuscated method */ public static final Map<String, String> reverseMethodMappings = new HashMap<String, String>(); /** Only used when simulatedMappings is enabled, to help with ModMappings */ public static final Set<String> clientMappingsSet = new HashSet<>(); /** Only used when simulatedMappings is enabled, to help with ModMappings */ public static final Set<String> serverMappingsSet = new HashSet<>(); /** Used by getClassNode to avoid reloading the classes over and over */ private static Map<String, ClassNode> cachedClassNodes = new HashMap<String, ClassNode>(); /** * If set to true, fake mappings populate the map objects above, ignoring whether * they can actually be located in the Minecraft code. * * Used by ModMappings when determining all possible mappings provided without * needing a Minecraft jar to scan (since ModMappings needs a deobfuscated one, * which DynamicMappings can't process properly.) */ public static boolean simulatedMappings = false; /** * Called to initialize all mappings. */ public static void generateClassMappings() { generateClassLinkages(); for (String clazz : MAPPINGS_CLASSES) { Class c = null; try { c = Class.forName(clazz); } catch (ClassNotFoundException e) { System.out.println("[DynamicMappings] Error - Couldn't find mappings class \"" + clazz + "\""); } if (c != null) { DynamicMappings.registerMappingsClass(c); } } } /** * Stores relevant information about a mapper method in one structure. */ private static class MappingMethod { final Method method; final String[] provides; final String[] depends; final String[] providesMethods; final String[] dependsMethods; final String[] providesFields; final String[] dependsFields; public MappingMethod(Method m, Mapping mapping) { method = m; provides = mapping.provides(); depends = mapping.depends(); providesMethods = mapping.providesMethods(); dependsMethods = mapping.dependsMethods(); providesFields = mapping.providesFields(); dependsFields = mapping.dependsFields(); } } /** * Parses a class for dynamic mappings. * Methods adding mappings should use the @Mapping annotation. * * The class may optionally use @MappingsClass annotation if it includes * client or sever-side mappings. * * @param mappingsClass - The class to process for mapper methods. * @return True if all mappings were successfully discovered. */ public static boolean registerMappingsClass(Class<? extends Object> mappingsClass) { List<MappingMethod> mappingMethods = new ArrayList<MappingMethod>(); boolean clientSide = false; boolean serverSide = false; if (mappingsClass.isAnnotationPresent(MappingsClass.class)) { MappingsClass mc = mappingsClass.getAnnotation(MappingsClass.class); clientSide = mc.clientSide(); serverSide = mc.serverSide(); } if (!simulatedMappings && clientSide && !MeddleUtil.isClientJar()) { System.out.println("[DynamicMappings] Ignoring client-side class " + mappingsClass.getName()); return false; } if (!simulatedMappings && serverSide && MeddleUtil.isClientJar()) { System.out.println("[DynamicMappings] Ignoring server-side class " + mappingsClass.getName()); return false; } System.out.println("[DynamicMappings] Processing class " + mappingsClass.getName()); for (Method method : mappingsClass.getMethods()) { if (!method.isAnnotationPresent(Mapping.class)) continue; Mapping mapping = method.getAnnotation(Mapping.class); mappingMethods.add(new MappingMethod(method, mapping)); } Object mappingsObject = null; try { Constructor<? extends Object> mappingsConstructor = mappingsClass.getConstructor(new Class[0]); mappingsObject = mappingsConstructor.newInstance(); } catch (Exception e) { e.printStackTrace(); } while (true) { int startSize = mappingMethods.size(); for (Iterator<MappingMethod> it = mappingMethods.iterator(); it.hasNext();) { MappingMethod mm = it.next(); boolean isStatic = Modifier.isStatic(mm.method.getModifiers()); boolean hasDepends = true; for (String depend : mm.depends) { if (!classMappings.keySet().contains(depend)) hasDepends = false; } for (String depend : mm.dependsFields) { if (!fieldMappings.keySet().contains(depend)) hasDepends = false; } for (String depend : mm.dependsMethods) { if (!methodMappings.keySet().contains(depend)) hasDepends = false; } if (!hasDepends) continue; if (!simulatedMappings) { try { if (isStatic) mm.method.invoke(null); else mm.method.invoke(mappingsObject, (Object[])null); } catch (Exception e) { e.printStackTrace(); } } else { for (String s : mm.provides) { classMappings.put(s, "---"); if (clientSide) clientMappingsSet.add(s); if (serverSide) serverMappingsSet.add(s); } for (String s : mm.providesFields) { fieldMappings.put(s, "--- --- ---"); if (clientSide) clientMappingsSet.add(s); if (serverSide) serverMappingsSet.add(s); } for (String s : mm.providesMethods) { methodMappings.put(s, "--- --- ---"); if (clientSide) clientMappingsSet.add(s); if (serverSide) serverMappingsSet.add(s); } } for (String provider : mm.provides) { if (!classMappings.keySet().contains(provider)) System.out.println(mm.method.getName() + " didn't provide mapping for class " + provider); } for (String provider : mm.providesFields) { if (!fieldMappings.keySet().contains(provider)) System.out.println(mm.method.getName() + " didn't provide mapping for field " + provider); } for (String provider : mm.providesMethods) { if (!methodMappings.keySet().contains(provider)) System.out.println(mm.method.getName() + " didn't provide mapping for method " + provider); } it.remove(); } if (mappingMethods.size() == 0) return true; if (startSize == mappingMethods.size()) { System.out.println("Unmet mapping dependencies in " + mappingsClass.getName() + "!"); for (MappingMethod mm : mappingMethods) { System.out.println(" Mapper Method: " + mm.method.getName()); for (String depend : mm.depends) { if (!classMappings.keySet().contains(depend)) System.out.println(" Class: " + depend); } for (String depend : mm.dependsFields) { if (!fieldMappings.keySet().contains(depend)) System.out.println(" Field: " + depend); } for (String depend : mm.dependsMethods) { if (!methodMappings.keySet().contains(depend)) System.out.println(" Method: " + depend); } } return false; } } } public static Map<String, Set<String>> classDeps = new HashMap<>(); public static Map<String, Set<String>> classesExtendFrom = new HashMap<>(); public static Map<String, Set<String>> classesImplementFrom = new HashMap<>(); static String[] classSearchExceptions = new String[] { "java/", "javax/", "sun/", "com/google/", "org/apache/", "com/sun/", "io/netty/", "jdk/internal/", "org/xml/", "org/w3c/", "jdk/net/", "com/ibm/", "org/lwjgl/", "com/jcraft/", "joptsimple/", "net/java/", "paulscode/", "com/mojang/"}; public static void getConstantPoolClassesRecursive(String className) { if (startsWithAny(className, classSearchExceptions)) return; ClassReader reader = null; try { reader = new ClassReader(className); } catch (IOException e) {} if (reader != null) { String superName = reader.getSuperName(); Set<String> extendSet = classesExtendFrom.get(superName); if (extendSet == null) { extendSet = new HashSet<>(); classesExtendFrom.put(superName, extendSet); } extendSet.add(className); for (String iface : reader.getInterfaces()) { Set<String> implementSet = classesImplementFrom.get(iface); if (implementSet == null) { implementSet = new HashSet<>(); classesImplementFrom.put(iface, implementSet); } implementSet.add(className); } } Set<String> classes = classDeps.get(className); if (classes == null) { classes = getConstantPoolClasses(className, true); if (classes == null) return; classDeps.put(className, classes); } //System.out.println(className + " " + (classes != null)); for (String s : classes) { if (!classDeps.containsKey(s)) getConstantPoolClassesRecursive(s); } } public static boolean startsWithAny(String string, String[] list) { for (String s : list) { if (string.startsWith(s)) return true; } return false; } public static Set<String> getChildClasses(String className) { Set<String> outSet = new HashSet<>(); Set<String> tempSet = new HashSet<>(); if (classesExtendFrom.containsKey(className)) tempSet.addAll(classesExtendFrom.get(className)); if (classesImplementFrom.containsKey(className)) tempSet.addAll(classesImplementFrom.get(className)); outSet.addAll(tempSet); for (String s : tempSet) { outSet.addAll(getChildClasses(s)); } return outSet; } public static void generateClassLinkages() { System.out.print("[DynamicMappings] Generating linkages..."); getConstantPoolClassesRecursive("net/minecraft/server/MinecraftServer"); getConstantPoolClassesRecursive("net/minecraft/client/main/Main"); System.out.println("done"); } public static void log(boolean toConsole, PrintWriter writer, String text) { if (toConsole) System.out.println(text); if (writer != null) writer.println(text); } /** Used for debugging purposes, and to print out a full list * @throws FileNotFoundException */ public static void main(String[] args) { ParmParser pp = new ParmParser(); Parm clearMappersParm = pp.addParm("-clearmappers", 0); Parm addMapperParm = pp.addParm("-addmappers", 1); pp.processArgs(args); if (clearMappersParm.found) DynamicMappings.MAPPINGS_CLASSES.clear(); if (addMapperParm.found) { String split[] = addMapperParm.getFirstResult().split(":;,"); for (String mapper : split) DynamicMappings.MAPPINGS_CLASSES.add(mapper); } // If true, prints out the mappings boolean showMappings = false; // If true, saves mappings to currentmappings.txt boolean saveMappings = true; PrintWriter writer = null; try { if (saveMappings) writer = new PrintWriter("currentmappings.txt"); } catch (FileNotFoundException e) { e.printStackTrace(); } generateClassMappings(); log(true, writer, "[DynamicMappings] Minecraft version: " + MeddleUtil.findMinecraftVersion()); log(true, writer, "[DynamicMappings] Minecraft jar type: " + (MeddleUtil.isClientJar() ? "client" : "server")); if (!showMappings && !saveMappings) return; log(showMappings, writer, "\nCLASSES:"); List<String> sorted = new ArrayList<String>(); sorted.addAll(classMappings.keySet()); Collections.sort(sorted); for (String s : sorted) { log(showMappings, writer, s + " -> " + classMappings.get(s)); } log(showMappings, writer, "\nFIELDS:"); sorted.clear(); sorted.addAll(fieldMappings.keySet()); Collections.sort(sorted); for (String s : sorted) { log(showMappings, writer, s + " -> " + fieldMappings.get(s)); } log(showMappings, writer, "\nMETHODS:"); sorted.clear(); sorted.addAll(methodMappings.keySet()); Collections.sort(sorted); for (String s : sorted) { log(showMappings, writer, s + " -> " + methodMappings.get(s)); } writer.close(); } /** * Load a ClassNode by its name. This is for loading the original obfuscated * classes. * * Note: *Do not* edit classes you get from this. They're cached and used by * anyone doing analysis of vanilla class files. * * @param className - The normal (probably obfuscated) name of the class to load. * @return The ClassNode requested, or null if it doesn't exist. */ public static ClassNode getClassNode(String className) { if (className == null) return null; className = className.replace(".", "/"); if (cachedClassNodes.containsKey(className)) return cachedClassNodes.get(className); //InputStream stream = Launch.classLoader.getResourceAsStream(className + ".class"); InputStream stream = DynamicMappings.class.getClassLoader().getResourceAsStream(className + ".class"); if (stream == null) return null; ClassReader reader = null; try { reader = new ClassReader(stream); } catch (IOException e) { return null; } ClassNode cn = new ClassNode(); reader.accept(cn, 0); cachedClassNodes.put(className, cn); return cn; } /** * Get constant pool string that an LDC instruction is * loading. * * @param node The instruction node to check. * @return Returns the string if the LDC is accessing a String * constant pool item, else null. */ public static String getLdcString(AbstractInsnNode node) { if (!(node instanceof LdcInsnNode)) return null; LdcInsnNode ldc = (LdcInsnNode)node; if (!(ldc.cst instanceof String)) return null; return new String((String)ldc.cst); } /** * Get constant pool class that an LDC instruction is * referencing. * * @param node - The instruction node to check. * @return Returns the class name if the LDC is accessing a * Class constant pool item, else null. */ public static String getLdcClass(AbstractInsnNode node) { if (!(node instanceof LdcInsnNode)) return null; LdcInsnNode ldc = (LdcInsnNode)node; if (!(ldc.cst instanceof Type)) return null; return ((Type)ldc.cst).getClassName(); } /** * Get constant pool integer that an LDC instruction is * referencing. * * @param node - The instruction node to check. * @return Returns the class name if the LDC is accessing a * Class constant pool item, else null. */ public static Integer getLdcInteger(AbstractInsnNode node) { if (!(node instanceof LdcInsnNode)) return null; LdcInsnNode ldc = (LdcInsnNode)node; if (!(ldc.cst instanceof Integer)) return null; return (Integer)ldc.cst; } /** * Get constant pool float that an LDC instruction is * referencing. * * @param node - The instruction node to check. * @return Returns the class name if the LDC is accessing a * Class constant pool item, else null. */ public static Float getLdcFloat(AbstractInsnNode node) { if (!(node instanceof LdcInsnNode)) return null; LdcInsnNode ldc = (LdcInsnNode)node; if (!(ldc.cst instanceof Float)) return null; return (Float)ldc.cst; } /** * Check if an LDC instruction is loading the specified string. * * @param node The instruction node to check * @param string The string to compare to. * @return Returns true if the instruction is an LDC, is accessing * a String constant pool item, and the string matches the input string. */ public static boolean isLdcWithString(AbstractInsnNode node, String string) { String s = getLdcString(node); return (s != null && string.equals(s)); } /** * Check if an LDC instruction is loading specified int value. * * @param node The instruction node to check. * @param val The integer to compare to. * @return Returns true if the instruction is an LDC, is accessing * a Integer constant pool item, and its value matches the input value. */ public static boolean isLdcWithInteger(AbstractInsnNode node, int val) { if (!(node instanceof LdcInsnNode)) return false; LdcInsnNode ldc = (LdcInsnNode)node; if (!(ldc.cst instanceof Integer)) return false; return ((Integer)ldc.cst) == val; } /** * Check if an LDC instruction is loading specified float value. * * @param node The instruction node to check. * @param val The integer to compare to. * @return Returns true if the instruction is an LDC, is accessing * a Float constant pool item, and its value matches the input value. */ public static boolean isLdcWithFloat(AbstractInsnNode node, float val) { if (!(node instanceof LdcInsnNode)) return false; LdcInsnNode ldc = (LdcInsnNode)node; if (!(ldc.cst instanceof Float)) return false; return ((Float)ldc.cst) == val; } /** * Get the description of the specified field from a class. * * @param cn - The class to search. * @param fieldName - The name of the field. * @return The description of the field, or null if not found. */ public static String getFieldDesc(ClassNode cn, String fieldName) { for (FieldNode field : cn.fields) { if (field.name.equals(fieldName)) return field.desc; } return null; } /** * Get the specified field node from the class. * * @param cn - The class to search * @param fieldName - The name of the field to find. * @return The FieldNode if present, else null. */ public static FieldNode getFieldByName(ClassNode cn, String fieldName) { for (FieldNode field : cn.fields) { if (field.name.equals(fieldName)) return field; } return null; } /** * Searches a class's constant pool for the specified list of strings. * * NOTE: Constant pool strings are trimmed of whitespace! Take into * account when matching. * * @param className - The name of the class to search. * @param matchStrings - The list of strings to find. * @return True if all strings were found. */ public static boolean searchConstantPoolForStrings(String className, String... matchStrings) { if (className == null) return false; className = className.replace(".", "/"); InputStream stream = DynamicMappings.class.getClassLoader().getResourceAsStream(className + ".class"); if (stream == null) return false; ClassReader reader = null; try { reader = new ClassReader(stream); } catch (IOException e) { return false; } int itemCount = reader.getItemCount(); char[] buffer = new char[reader.getMaxStringLength()]; int matches = 0; for (int n = 1; n < itemCount; n++) { int pos = reader.getItem(n); if (pos == 0 || reader.b[pos - 1] != 8) continue; Arrays.fill(buffer, (char)0); String string = reader.readUTF8(pos, buffer).trim(); //String string = (new String(buffer)).trim(); for (int n2 = 0; n2 < matchStrings.length; n2++) { if (string.equals(matchStrings[n2].trim())) { matches++; break; } } } return (matches == matchStrings.length); } /** * Searches a class's constant pool for the specified list of class * references. * * @param className - The name of the class to search. * @param matchStrings - The list of class names to find. * @return True if all class names were found. */ public static boolean searchConstantPoolForClasses(String className, String... matchStrings) { className = className.replace(".", "/"); InputStream stream = DynamicMappings.class.getClassLoader().getResourceAsStream(className + ".class"); if (stream == null) return false; ClassReader reader = null; try { reader = new ClassReader(stream); } catch (IOException e) { return false; } int itemCount = reader.getItemCount(); char[] buffer = new char[reader.getMaxStringLength()]; int matches = 0; for (int n = 1; n < itemCount; n++) { int pos = reader.getItem(n); if (pos == 0 || reader.b[pos - 1] != 7) continue; Arrays.fill(buffer, (char)0); String string = reader.readUTF8(pos, buffer); //String string = (new String(buffer)).trim(); for (int n2 = 0; n2 < matchStrings.length; n2++) { if (string.equals(matchStrings[n2].replace(".", "/"))) { matches++; break; } } } return (matches == matchStrings.length); } /** * Returns a list of the 'String' types from the class's constant pool. * * @param className - Name of class to search. * @return The list of constant pool strings. */ public static List<String> getConstantPoolStrings(String className) { List<String> strings = new ArrayList<String>(); className = className.replace(".", "/"); InputStream stream = DynamicMappings.class.getClassLoader().getResourceAsStream(className + ".class"); if (stream == null) return null; ClassReader reader = null; try { reader = new ClassReader(stream); } catch (IOException e) { return null; } int itemCount = reader.getItemCount(); char[] buffer = new char[reader.getMaxStringLength()]; for (int n = 1; n < itemCount; n++) { int pos = reader.getItem(n); if (pos == 0 || reader.b[pos - 1] != 8) continue; Arrays.fill(buffer, (char)0); String string = reader.readUTF8(pos, buffer); //String string = (new String(buffer)).trim(); strings.add(string); } return strings; } /** * Returns a set of the 'Class' types from the class's constant pool. * * @param className - Name of class to search. * @return The list of constant pool strings. */ public static Set<String> getConstantPoolClasses(String className, boolean processArrays) { Set<String> strings = new HashSet<String>(); className = className.replace(".", "/"); InputStream stream = DynamicMappings.class.getClassLoader().getResourceAsStream(className + ".class"); if (stream == null) return null; ClassReader reader = null; try { reader = new ClassReader(stream); } catch (IOException e) { return null; } int itemCount = reader.getItemCount(); char[] buffer = new char[reader.getMaxStringLength()]; for (int n = 1; n < itemCount; n++) { int pos = reader.getItem(n); if (pos == 0 || reader.b[pos - 1] != 7) continue; Arrays.fill(buffer, (char)0); String string = reader.readUTF8(pos, buffer); //String string = (new String(buffer)).trim(); if (string.startsWith("[") && processArrays) { string = ModMappings.getArrayType(string); if (string == null) continue; } if (string.length() < 1) continue; strings.add(string); } return strings; } /** * Confirm the parameter types of a method's description. * * Uses org.objectweb.asm.Type for values. * * @param method - MethodNode to check. * @param types - Sequence of method parameter types. * @return True if the method description matches specified types. */ public static boolean checkMethodParameters(MethodNode method, int ... types) { Type t = Type.getMethodType(method.desc); Type[] args = t.getArgumentTypes(); if (args.length != types.length) return false; int len = args.length; for (int n = 0; n < len; n++) { if (args[n].getSort() != types[n]) return false; } return true; } /** * Finds all methods matching the specified name and/or description. * * @param cn - ClassNode to search. * @param name - Optional name of method(s) to find. * @param desc - Optional description of method(s) to find. * @return List of matching methods. */ public static List<MethodNode> getMatchingMethods(ClassNode cn, String name, String desc) { List<MethodNode> output = new ArrayList<MethodNode>(); for (MethodNode method : cn.methods) { if ((name == null || (name != null && method.name.equals(name))) && (desc == null || (desc != null && method.desc.equals(desc)))) output.add(method); } return output; } /** * Finds all methods matching the specified return and argument types * * @param cn - ClassNode to search. * @param accessCode - The Opcode for the access of the method. * @param returnType - The return type as integer * @param parameterTypes - The arguments of the method as integer. Can be null if the method haven't arguments * @return List of matching methods. * @author canitzp */ public static List<MethodNode> getMatchingMethods(ClassNode cn, int accessCode, int returnType, int... parameterTypes){ List<MethodNode> methodNodes = new ArrayList<>(); for(MethodNode method : cn.methods){ Type rt = Type.getReturnType(method.desc); Type[] params = Type.getArgumentTypes(method.desc); if(returnType == rt.getSort() && (method.access & accessCode) != 0){ if(parameterTypes != null){ if(parameterTypes.length == params.length){ boolean isSame = true; for(int i = 0; i < parameterTypes.length; i++){ if(parameterTypes[i] != params[i].getSort()){ isSame = false; break; } } if(isSame){ methodNodes.add(method); } } } else if(params.length == 0){ methodNodes.add(method); } } } return methodNodes; } /** * Finds all fields matching the specified name and/or description. * * @param cn - ClassNode to search. * @param name - Optional name of field(s) to find. * @param desc - Optional description of field(s) to find. * @return List of matching fields. */ public static List<FieldNode> getMatchingFields(ClassNode cn, String name, String desc) { List<FieldNode> output = new ArrayList<FieldNode>(); for (FieldNode field : cn.fields) { if ((name == null || (name != null && field.name.equals(name))) && (desc == null || (desc != null && field.desc.equals(desc)))) output.add(field); } return output; } /** * Gets a ClassNode after translating the specified deobfuscated class name to * the obfuscated name. * * @param deobfClass - Class name to translate and get. * @return The ClassNode, or null if it doesn't exist. */ public static ClassNode getClassNodeFromMapping(String deobfClass) { return getClassNode(getClassMapping(deobfClass)); } /** * Checks if the sequence of opcodes exists, starting at the specified * instruction. This ignores synthetic opcodes added by ASM, such as * labels. * * @param insn - Instruction node to start at. * @param opcodes - Sequence of opcodes to look for. * @return True if all opcodes are found and in the specifed order. */ public static boolean matchOpcodeSequence(AbstractInsnNode insn, int...opcodes) { for (int opcode : opcodes) { insn = getNextRealOpcode(insn); if (insn == null) return false; if (opcode != insn.getOpcode()) return false; insn = insn.getNext(); } return true; } /** * Identifies and extracts the specified opcode sequence as an array of * instruction nodes. Ignores synthetic opcodes added by ASM, such as * labels. * * @param insn - Instruction to start at. * @param opcodes - Sequence of opcodes to look for. * @return The list of instruction nodes, if one is found matching the * specified sequence. */ public static AbstractInsnNode[] getOpcodeSequenceArray(AbstractInsnNode insn, int...opcodes) { AbstractInsnNode[] outNodes = new AbstractInsnNode[opcodes.length]; int pos = 0; for (int opcode : opcodes) { insn = getNextRealOpcode(insn); if (insn == null) return null; if (opcode != insn.getOpcode()) return null; outNodes[pos++] = insn; insn = insn.getNext(); } return outNodes; } @SuppressWarnings("unchecked") public static boolean matchInsnNodeSequence(AbstractInsnNode insn, Class<? extends AbstractInsnNode>...nodeClasses) { for (Class<? extends AbstractInsnNode> nodeClass : nodeClasses) { insn = getNextRealOpcode(insn); if (insn == null) return false; if (nodeClass != insn.getClass()) return false; insn = insn.getNext(); } return true; } @SuppressWarnings("unchecked") public static AbstractInsnNode[] getInsnNodeSequenceArray(AbstractInsnNode insn, Class<? extends AbstractInsnNode>...nodeClasses) { AbstractInsnNode[] outNodes = new AbstractInsnNode[nodeClasses.length]; int pos = 0; for (Class<? extends AbstractInsnNode> nodeClass : nodeClasses) { insn = getNextRealOpcode(insn); if (insn == null) return null; if (nodeClass != insn.getClass()) return null; outNodes[pos++] = insn; insn = insn.getNext(); } return outNodes; } /** * Gets a list of all instruction nodes matching the specified class. * * @param startInsn - Instruction node to start at. * @param classType - Class to compare against. * @return The list of matching nodes. */ @SuppressWarnings("unchecked") public static <T> List<T> getAllInsnNodesOfType(AbstractInsnNode startInsn, Class<T> classType) { List<T> list = new ArrayList<>(); for (AbstractInsnNode insn = startInsn; insn != null; insn = insn.getNext()) { if (insn.getClass() == classType) list.add((T)insn); } return list; } /** * Gets the next matching instruction node matching the specified class. * * @param startInsn - Instruction node to start at. * @param classType - Class to compare against. * @return The first matching instruction node, or null if none. */ @SuppressWarnings("unchecked") public static <T> T getNextInsnNodeOfType(AbstractInsnNode startInsn, Class<T> classType) { for (AbstractInsnNode insn = startInsn; insn != null; insn = insn.getNext()) { if (insn.getClass() == classType) return (T) insn; } return null; } /** * Gets the obfuscated class name from a deobfuscated input. * * @param deobfClassName - The deobfuscated class name. * @return The obfuscated class name, or null */ public static String getClassMapping(String deobfClassName) { return classMappings.get(deobfClassName.replace(".", "/")); } /** * Gets the deobfuscated class name from an obfuscated input. * * @param obfClassName - The obfuscated class name. * @return The deobfuscated class name, or null */ public static String getReverseClassMapping(String obfClassName) { return reverseClassMappings.get(obfClassName.replace(".", "/")); } /** * Adds a class mapping. * * @param deobfClassName - The deobfuscated class name. * @param node - The obfuscated ClassNode. */ public static void addClassMapping(String deobfClassName, ClassNode node) { if (deobfClassName == null) return; deobfClassName = deobfClassName.replace(".", "/"); addClassMapping(deobfClassName, node.name); } /** * Adds a class mapping. * * @param deobfClassName - The deobfuscated class name. * @param obfClassName - The obfuscated class name. */ public static void addClassMapping(String deobfClassName, String obfClassName) { deobfClassName = deobfClassName.replace(".", "/"); obfClassName = obfClassName.replace(".", "/"); if (classMappings.containsKey(deobfClassName) && !classMappings.get(deobfClassName).equals(obfClassName)) System.out.println("WARNING: " + deobfClassName + " has been remapped from " + classMappings.get(deobfClassName) + " to " + obfClassName); if (reverseClassMappings.containsKey(obfClassName) && !reverseClassMappings.get(obfClassName).equals(deobfClassName)) System.out.println("WARNING: " + obfClassName + " has been remapped from " + reverseClassMappings.get(obfClassName) + " to " + deobfClassName); classMappings.put(deobfClassName, obfClassName); reverseClassMappings.put(obfClassName, deobfClassName); } // Both inputs in the format of "class_name method_name method_desc" public static void addMethodMapping(String deobfMethodDesc, String obfMethodDesc) { if (classMappings.containsKey(deobfMethodDesc) && !classMappings.get(deobfMethodDesc).equals(obfMethodDesc)) System.out.println("WARNING: " + deobfMethodDesc + " has been remapped from " + classMappings.get(deobfMethodDesc) + " to " + obfMethodDesc); if (reverseClassMappings.containsKey(obfMethodDesc) && !reverseClassMappings.get(obfMethodDesc).equals(deobfMethodDesc)) System.out.println("WARNING: " + obfMethodDesc + " has been remapped from " + reverseClassMappings.get(obfMethodDesc) + " to " + deobfMethodDesc); methodMappings.put(deobfMethodDesc, obfMethodDesc); reverseMethodMappings.put(obfMethodDesc, deobfMethodDesc); } // Both inputs in the format of "class_name field_name field_desc" public static void addFieldMapping(String deobfFieldDesc, String obfFieldDesc) { if (classMappings.containsKey(deobfFieldDesc) && !classMappings.get(deobfFieldDesc).equals(obfFieldDesc)) System.out.println("WARNING: " + deobfFieldDesc + " has been remapped from " + classMappings.get(deobfFieldDesc) + " to " + obfFieldDesc); if (reverseClassMappings.containsKey(obfFieldDesc) && !reverseClassMappings.get(obfFieldDesc).equals(deobfFieldDesc)) System.out.println("WARNING: " + obfFieldDesc + " has been remapped from " + reverseClassMappings.get(obfFieldDesc) + " to " + deobfFieldDesc); fieldMappings.put(deobfFieldDesc, obfFieldDesc); reverseFieldMappings.put(obfFieldDesc, deobfFieldDesc); } public static String getMethodMapping(String className, String methodName, String methodDesc) { return methodMappings.get(className + " " + methodName + " " + methodDesc); } public static String getMethodMapping(String mapping) { return methodMappings.get(mapping); } public static String getReverseMethodMapping(String obfMethod) { return reverseMethodMappings.get(obfMethod); } public static String getFieldMapping(String mapping) { return fieldMappings.get(mapping); } public static String getReverseFieldMapping(String obfField) { return reverseFieldMappings.get(obfField); } public static FieldNode getFieldNode(ClassNode cn, String obfMapping) { if (cn == null || obfMapping == null) return null; String[] split = obfMapping.split(" "); if (split.length < 3) return null; for (FieldNode field : cn.fields) { if (field.name.equals(split[1]) && field.desc.equals(split[2])) { return field; } } return null; } public static FieldNode getFieldNodeFromMapping(ClassNode cn, String deobfMapping) { String mapping = getFieldMapping(deobfMapping); if (cn == null || mapping == null) return null; String[] split = mapping.split(" "); if (split.length < 3) return null; for (FieldNode field : cn.fields) { if (field.name.equals(split[1]) && field.desc.equals(split[2])) { return field; } } return null; } // Returns just the obfuscated name of a method matching the deobfuscated input public static String getMethodMappingName(String className, String methodName, String methodDesc) { String mapping = getMethodMapping(className, methodName, methodDesc); if (mapping == null) return null; String [] split = mapping.split(" "); return split.length >= 3 ? split[1] : null; } public static MethodNode getMethodNode(ClassNode cn, String obfMapping) { if (cn == null || obfMapping == null) return null; String[] split = obfMapping.split(" "); if (split.length < 3) return null; for (MethodNode method : cn.methods) { if (method.name.equals(split[1]) && method.desc.equals(split[2])) { return method; } } return null; } public static MethodNode getMethodNodeFromMapping(ClassNode cn, String deobfMapping) { String mapping = getMethodMapping(deobfMapping); if (cn == null || mapping == null) return null; String[] split = mapping.split(" "); if (split.length < 3) return null; for (MethodNode method : cn.methods) { if (method.name.equals(split[1]) && method.desc.equals(split[2])) { return method; } } return null; } public static JarFile getMinecraftJar() { // For modern versions URL url = MeddleUtil.class.getClassLoader().getResource("net/minecraft/server/MinecraftServer.class"); // For older versions if (url == null) url = MeddleUtil.class.getClassLoader().getResource("net/minecraft/client/Minecraft.class"); if (url == null) return null; JarFile jar = null; if ("jar".equals(url.getProtocol())) { JarURLConnection connection = null; try { connection = (JarURLConnection) url.openConnection(); jar = connection.getJarFile(); } catch (IOException e) {} } return jar; } public static boolean isSubclassOf(String className, String superClassName) { InputStream stream = DynamicMappings.class.getClassLoader().getResourceAsStream(className + ".class"); if (stream == null) return false; ClassReader reader = null; try { reader = new ClassReader(stream); } catch (IOException e) {} if (reader == null) return false; String superName = reader.getSuperName(); if (superName.equals(superClassName)) return true; if (superName.equals("java/lang/Object")) return false; return isSubclassOf(superName, superClassName); } // Use untyped list in case someone compiles without debug version of ASM. @SuppressWarnings({ "unchecked", "rawtypes" }) public static List<MethodNode> getMethodsWithDescriptor(List methods, String desc) { List<MethodNode> list = new ArrayList<MethodNode>(); for (MethodNode method : (List<MethodNode>)methods) { if (method.desc.equals(desc)) list.add(method); } return list; } public static List<MethodNode> removeMethodsWithFlags(List<MethodNode> methods, int accFlags) { List<MethodNode> outList = new ArrayList<MethodNode>(); for (MethodNode mn : methods) { if ((mn.access & accFlags) == 0) outList.add(mn); } return outList; } public static List<MethodNode> removeMethodsWithoutFlags(List<MethodNode> methods, int accFlags) { List<MethodNode> outList = new ArrayList<MethodNode>(); for (MethodNode mn : methods) { if ((mn.access & accFlags) != 0) outList.add(mn); } return outList; } public static AbstractInsnNode findNextOpcodeNum(AbstractInsnNode insn, int opcode) { while (insn != null) { if (insn.getOpcode() == opcode) break; insn = insn.getNext(); } return insn; } public static AbstractInsnNode getNextRealOpcode(AbstractInsnNode insn) { while (insn != null && insn.getOpcode() < 0) insn = insn.getNext(); return insn; } public static String assembleDescriptor(Object... objects) { String output = ""; for (Object o : objects) { if (o instanceof String) output += (String)o; else if (o instanceof ClassNode) output += "L" + ((ClassNode)o).name + ";"; } return output; } public static boolean classHasInterfaces(ClassNode classNode, String... ifaces) { boolean implementsAll = true; List<String> implemented = classNode.interfaces; for (String iface : ifaces) { if (!implemented.contains(iface)) { implementsAll = false; break; } } return implementsAll; } public static boolean doesInheritFrom(String className, String inheritFrom) { if (className.equals(inheritFrom)) return true; ClassNode cn = getClassNode(className); if (cn == null) return false; List<String> classes = new ArrayList<>(); if (cn.superName != null) classes.add(cn.superName); classes.addAll(cn.interfaces); // First pass for (String c : classes) { if (c.equals(inheritFrom)) return true; } // Deeper pass for (String c : classes) { if (doesInheritFrom(c, inheritFrom)) return true; } return false; } public static List<String> getStringsFromMethod(MethodNode method) { List<String> list = new ArrayList<>(); for (AbstractInsnNode node : method.instructions.toArray()) { String s = getLdcString(node); if (s != null) list.add(s); } return list; } public static boolean doesMethodContainString(MethodNode method, String string) { return getStringsFromMethod(method).contains(string); } public static List<Integer> getIntegersFromMethod(MethodNode method) { List<Integer> list = new ArrayList<>(); for (AbstractInsnNode node : method.instructions.toArray()) { Integer i = getLdcInteger(node); if (i != null) list.add(i); } return list; } public static List<Float> getFloatsFromMethod(MethodNode method) { List<Float> list = new ArrayList<>(); for (AbstractInsnNode node : method.instructions.toArray()) { Float f = getLdcFloat(node); if (f != null) list.add(f); } return list; } public static boolean doesMethodContainInteger(MethodNode method, int i) { return getIntegersFromMethod(method).contains(i); } public static List<MethodNode> getMethodsContainingString(ClassNode cn, String string) { List<MethodNode> list = new ArrayList<>(); for (MethodNode method : cn.methods) { if (doesMethodContainString(method, string)) list.add(method); } return list; } public static boolean doesMethodUseField(MethodNode method, String owner, String name, String desc) { List<FieldInsnNode> nodes = getAllInsnNodesOfType(method.instructions.getFirst(), FieldInsnNode.class); for (FieldInsnNode fn : nodes) { if (fn.owner.equals(owner) && fn.name.equals(name) && fn.desc.equals(desc)) return true; } return false; } public static boolean doesMethodUseMethod(MethodNode method, String owner, String name, String desc) { List<MethodInsnNode> nodes = getAllInsnNodesOfType(method.instructions.getFirst(), MethodInsnNode.class); for (MethodInsnNode mn : nodes) { if (mn.owner.equals(owner) && mn.name.equals(name) && mn.desc.equals(desc)) return true; } return false; } public static List<MethodNode> filterMethodsUsingField(List<MethodNode> paramMethods, String owner, String name, String desc) { List<MethodNode> methods = new ArrayList<>(); for (MethodNode method : paramMethods) { if (doesMethodUseField(method, owner, name, desc)) methods.add(method); } return methods; } public static List<MethodNode> filterMethodsUsingMethod(List<MethodNode> paramMethods, String owner, String name, String desc) { List<MethodNode> methods = new ArrayList<>(); for (MethodNode method : paramMethods) { if (doesMethodUseMethod(method, owner, name, desc)) methods.add(method); } return methods; } public static void reset() { classMappings.clear(); reverseClassMappings.clear(); fieldMappings.clear(); reverseFieldMappings.clear(); methodMappings.clear(); reverseMethodMappings.clear(); clientMappingsSet.clear(); serverMappingsSet.clear(); cachedClassNodes.clear(); } public static void discoverMapperConfigs() { boolean clearMappers = false; List<String> mappers = new ArrayList<>(); try { Enumeration<URL> configs = Launch.classLoader.findResources("dynamicmappings.init"); while (configs.hasMoreElements()) { URL url = configs.nextElement(); // Catch errors separately to continue parsing files try { DynamicMappings.LOGGER.info("[DynamicMappings] Parsing " + url.toString()); BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream())); while (true) { String line = reader.readLine(); if (line == null) break; String[] split = line.split(" "); String cmd = split[0].toLowerCase(); switch (cmd) { case "clearallmappers": clearMappers = true; break; case "addmapper": if (split.length > 1) mappers.add(split[1]); break; } } reader.close(); } catch (IOException e) {} } } catch (IOException e) { } if (clearMappers) { DynamicMappings.LOGGER.info("[DynamicMappings] Clearing default class mappers"); DynamicMappings.MAPPINGS_CLASSES.clear(); } if (mappers.size() > 0) { for (String mapper : mappers) { DynamicMappings.LOGGER.info("[DynamicMappings] Adding custom class mapper: " + mapper); DynamicMappings.MAPPINGS_CLASSES.add(mapper); } } } }