diff --git a/arclight-common/src/main/java/io/izzel/arclight/common/asm/ArclightImplementer.java b/arclight-common/src/main/java/io/izzel/arclight/common/asm/ArclightImplementer.java index b4d2c2d7..c6fa350f 100644 --- a/arclight-common/src/main/java/io/izzel/arclight/common/asm/ArclightImplementer.java +++ b/arclight-common/src/main/java/io/izzel/arclight/common/asm/ArclightImplementer.java @@ -35,6 +35,7 @@ public class ArclightImplementer implements ILaunchPluginService { public void initializeLaunch(ITransformerLoader transformerLoader, Path[] specialPaths) { this.transformerLoader = transformerLoader; this.implementers.put("inventory", new InventoryImplementer()); + this.implementers.put("switch", SwitchTableFixer.INSTANCE); } @Override diff --git a/arclight-common/src/main/java/io/izzel/arclight/common/asm/SwitchTableFixer.java b/arclight-common/src/main/java/io/izzel/arclight/common/asm/SwitchTableFixer.java new file mode 100644 index 00000000..0b09976e --- /dev/null +++ b/arclight-common/src/main/java/io/izzel/arclight/common/asm/SwitchTableFixer.java @@ -0,0 +1,202 @@ +package io.izzel.arclight.common.asm; + +import com.google.common.collect.ImmutableSet; +import cpw.mods.modlauncher.serviceapi.ILaunchPluginService; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.MarkerManager; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Opcodes; +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.InsnList; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.IntInsnNode; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.TryCatchBlockNode; + +import java.lang.reflect.Modifier; +import java.util.Set; + +public class SwitchTableFixer implements Implementer { + + public static final SwitchTableFixer INSTANCE = new SwitchTableFixer(); + private static final Marker MARKER = MarkerManager.getMarker("SWITCH_TABLE"); + private static final Set ENUMS = ImmutableSet.builder() + .add("org/bukkit/Material") + .build(); + + public byte[] processClass(byte[] bytes) { + ClassNode node = new ClassNode(); + new ClassReader(bytes).accept(node, ClassReader.SKIP_FRAMES); + processClass(node, null); + ClassWriter writer = new ClassWriter(0); + node.accept(writer); + return writer.toByteArray(); + } + + @Override + public boolean processClass(ClassNode node, ILaunchPluginService.ITransformerLoader transformerLoader) { + boolean success = false; + for (MethodNode method : node.methods) { + // There are two variants of switch map + if (inject1(node, method)) { + success = true; + } else if (inject2(node, method)) { + success = true; + } + } + return success; + } + + private boolean inject1(ClassNode node, MethodNode method) { + if (Modifier.isStatic(method.access) && (method.access & Opcodes.ACC_SYNTHETIC) != 0 && method.desc.equals("()[I")) { + boolean foundTryCatch = false; + for (TryCatchBlockNode tryCatchBlock : method.tryCatchBlocks) { + if ("java/lang/NoSuchFieldError".equals(tryCatchBlock.type)) { + foundTryCatch = true; + } else return false; + } + if (!foundTryCatch) return false; + ArclightImplementer.LOGGER.debug(MARKER, "Candidate switch enum method {} class {}", method.name + method.desc, node.name); + FieldInsnNode fieldInsnNode = null; + String enumType = null; + for (AbstractInsnNode insnNode : method.instructions) { + if (enumType != null) { + break; + } else { + if (insnNode.getOpcode() == Opcodes.GETSTATIC && ((FieldInsnNode) insnNode).desc.equals("[I")) { + fieldInsnNode = ((FieldInsnNode) insnNode); + } + if (insnNode.getOpcode() == Opcodes.INVOKESTATIC && ((MethodInsnNode) insnNode).name.equals("values")) { + Type methodType = Type.getMethodType(((MethodInsnNode) insnNode).desc); + Type returnType = methodType.getReturnType(); + if (returnType.getSort() == Type.ARRAY && returnType.getDimensions() == 1) { + String retType = returnType.getElementType().getInternalName(); + if (ENUMS.contains(retType)) { + AbstractInsnNode next = insnNode.getNext(); + if (next.getOpcode() == Opcodes.ARRAYLENGTH) { + AbstractInsnNode newArray = next.getNext(); + if (newArray.getOpcode() == Opcodes.NEWARRAY && ((IntInsnNode) newArray).operand == Opcodes.T_INT) { + enumType = retType; + } + } + } + } + } + } + } + if (fieldInsnNode != null && enumType != null) { + ArclightImplementer.LOGGER.debug(MARKER, "Find switch(enum {}) table method {} in class {}", enumType, method.name + method.desc, node.name); + AbstractInsnNode last = method.instructions.getLast(); + while (last != null && last.getOpcode() != Opcodes.ARETURN) { + last = last.getPrevious(); + } + if (last == null) return false; + InsnList list = new InsnList(); + list.add(new LdcInsnNode(Type.getObjectType(enumType))); + list.add(new MethodInsnNode(Opcodes.INVOKESTATIC, Type.getInternalName(SwitchTableFixer.class), "fillSwitchTable1", "([ILjava/lang/Class;)[I", false)); + list.add(new InsnNode(Opcodes.DUP)); + list.add(new FieldInsnNode(Opcodes.PUTSTATIC, fieldInsnNode.owner, fieldInsnNode.name, fieldInsnNode.desc)); + method.instructions.insertBefore(last, list); + ArclightImplementer.LOGGER.debug(MARKER, "Inject method in method {}:{}, switch table field is {}", node.name, method.name + method.desc, fieldInsnNode.name + fieldInsnNode.desc); + return true; + } + } + return false; + } + + @SuppressWarnings("unused") + public static int[] fillSwitchTable1(int[] arr, Class> cl) { + ArclightImplementer.LOGGER.debug(MARKER, "Filling switch table for {}", cl); + Enum[] enums = cl.getEnumConstants(); + if (arr.length < enums.length) { + int[] ints = new int[enums.length]; + System.arraycopy(arr, 0, ints, 0, arr.length); + arr = ints; + } + int i = -1; + for (int j : arr) { + if (j > i) i = j; + } + if (i != -1) { + for (int k = i; k < enums.length; k++) { + arr[k] = enums[k].ordinal(); + } + } + return arr; + } + + private boolean inject2(ClassNode node, MethodNode method) { + if ((node.access & Opcodes.ACC_SYNTHETIC) != 0) { + if (node.methods.size() == 1 && Modifier.isStatic(method.access) && method.name.equals("")) { + boolean foundTryCatch = false; + for (TryCatchBlockNode tryCatchBlock : method.tryCatchBlocks) { + if ("java/lang/NoSuchFieldError".equals(tryCatchBlock.type)) { + foundTryCatch = true; + } else return false; + } + if (!foundTryCatch) return false; + ArclightImplementer.LOGGER.debug(MARKER, "Candidate switch enum method {} class {}", method.name + method.desc, node.name); + FieldInsnNode fieldInsnNode = null; + String enumType = null; + for (AbstractInsnNode insnNode : method.instructions) { + if (insnNode.getOpcode() == Opcodes.INVOKESTATIC && ((MethodInsnNode) insnNode).name.equals("values")) { + Type methodType = Type.getMethodType(((MethodInsnNode) insnNode).desc); + Type returnType = methodType.getReturnType(); + if (returnType.getSort() == Type.ARRAY && returnType.getDimensions() == 1) { + String retType = returnType.getElementType().getInternalName(); + if (ENUMS.contains(retType)) { + AbstractInsnNode next = insnNode.getNext(); + if (next.getOpcode() == Opcodes.ARRAYLENGTH) { + AbstractInsnNode newArray = next.getNext(); + if (newArray.getOpcode() == Opcodes.NEWARRAY && ((IntInsnNode) newArray).operand == Opcodes.T_INT) { + AbstractInsnNode putStatic = newArray.getNext(); + if (putStatic.getOpcode() == Opcodes.PUTSTATIC && ((FieldInsnNode) putStatic).desc.equals("[I")) { + enumType = retType; + fieldInsnNode = ((FieldInsnNode) putStatic); + break; + } + } + } + } + } + } + } + if (fieldInsnNode != null) { + ArclightImplementer.LOGGER.debug(MARKER, "Find switch(enum {}) table method {} in class {}", enumType, method.name + method.desc, node.name); + AbstractInsnNode last = method.instructions.getLast(); + while (last != null && last.getOpcode() != Opcodes.RETURN) { + last = last.getPrevious(); + } + if (last == null) return false; + InsnList list = new InsnList(); + list.add(new FieldInsnNode(Opcodes.GETSTATIC, fieldInsnNode.owner, fieldInsnNode.name, fieldInsnNode.desc)); + list.add(new LdcInsnNode(Type.getObjectType(enumType))); + list.add(new MethodInsnNode(Opcodes.INVOKESTATIC, Type.getInternalName(SwitchTableFixer.class), "fillSwitchTable2", "([ILjava/lang/Class;)[I", false)); + list.add(new FieldInsnNode(Opcodes.PUTSTATIC, fieldInsnNode.owner, fieldInsnNode.name, fieldInsnNode.desc)); + method.instructions.insertBefore(last, list); + ArclightImplementer.LOGGER.debug(MARKER, "Inject method in method {}:{}, switch table field is {}", node.name, method.name + method.desc, fieldInsnNode.name + fieldInsnNode.desc); + return true; + } + } + } + return false; + } + + @SuppressWarnings("unused") + public static int[] fillSwitchTable2(int[] arr, Class> cl) { + ArclightImplementer.LOGGER.debug(MARKER, "Filling switch table for {}", cl); + Enum[] enums = cl.getEnumConstants(); + if (arr.length < enums.length) { + int[] ints = new int[enums.length]; + System.arraycopy(arr, 0, ints, 0, arr.length); + arr = ints; + } + return arr; + } +} diff --git a/arclight-common/src/main/java/io/izzel/arclight/common/mixin/bukkit/PluginClassLoaderMixin.java b/arclight-common/src/main/java/io/izzel/arclight/common/mixin/bukkit/PluginClassLoaderMixin.java index af44bc1e..46ea333f 100644 --- a/arclight-common/src/main/java/io/izzel/arclight/common/mixin/bukkit/PluginClassLoaderMixin.java +++ b/arclight-common/src/main/java/io/izzel/arclight/common/mixin/bukkit/PluginClassLoaderMixin.java @@ -1,6 +1,7 @@ package io.izzel.arclight.common.mixin.bukkit; import com.google.common.io.ByteStreams; +import io.izzel.arclight.common.asm.SwitchTableFixer; import io.izzel.arclight.common.bridge.bukkit.JavaPluginLoaderBridge; import io.izzel.arclight.common.mod.util.remapper.ArclightRemapper; import io.izzel.arclight.common.mod.util.remapper.PluginRemapper; @@ -70,6 +71,7 @@ public class PluginClassLoaderMixin extends URLClassLoader { throw new ClassNotFoundException(name, ex); } + classBytes = SwitchTableFixer.INSTANCE.processClass(classBytes); classBytes = Bukkit.getUnsafe().processClass(description, path, classBytes); if (remapper == null) { remapper = ArclightRemapper.INSTANCE.createPluginRemapper(this.loader, this);