From f09025bd2cd79cc477dae8451b0e2feea5b69c5f Mon Sep 17 00:00:00 2001 From: zaxar163 Date: Fri, 21 Sep 2018 19:42:36 +0300 Subject: [PATCH] SafeClassWriter --- .../launchserver/asm/ClassMetadataReader.java | 220 ++++++++++++++++++ .../launchserver/asm/SafeClassWriter.java | 34 +++ 2 files changed, 254 insertions(+) create mode 100644 LaunchServer/src/main/java/ru/gravit/launchserver/asm/ClassMetadataReader.java create mode 100644 LaunchServer/src/main/java/ru/gravit/launchserver/asm/SafeClassWriter.java diff --git a/LaunchServer/src/main/java/ru/gravit/launchserver/asm/ClassMetadataReader.java b/LaunchServer/src/main/java/ru/gravit/launchserver/asm/ClassMetadataReader.java new file mode 100644 index 00000000..6acb2dbf --- /dev/null +++ b/LaunchServer/src/main/java/ru/gravit/launchserver/asm/ClassMetadataReader.java @@ -0,0 +1,220 @@ +package ru.gravit.launchserver.asm; + +import org.objectweb.asm.*; + +import launcher.helper.IOHelper; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.jar.JarFile; + +/** + * Позволяет при помощи велосипеда из костылей искать методы внутри незагруженных классов + * и общие суперклассы для чего угодно. Работает через поиск class-файлов в classpath, и, в случае провала - + * ищет через рефлексию. + */ +public class ClassMetadataReader { + private static final Method m; + + static { + try { + m = ClassLoader.class.getDeclaredMethod("findLoadedClass", String.class); + m.setAccessible(true); + } catch (NoSuchMethodException e) { + throw new InternalError(e); + } + } + + private final List list; + + public ClassMetadataReader() { + this.list = new ArrayList<>(); + } + + public ClassMetadataReader(List list) { + this.list = new ArrayList<>(list); + } + + public void add(JarFile jar) { + list.add(jar); + } + + public byte[] getClassData(String className) throws IOException { + String classResourceName = className.replace('.', '/') + ".class"; + for (JarFile jar : list) { + if (jar.getJarEntry(classResourceName) != null) return IOHelper.read(jar.getInputStream(jar.getJarEntry(classResourceName))); + } + return IOHelper.read(ClassMetadataReader.class.getResourceAsStream('/' + classResourceName)); + } + + public void acceptVisitor(byte[] classData, ClassVisitor visitor) { + new ClassReader(classData).accept(visitor, 0); + } + + public void acceptVisitor(String className, ClassVisitor visitor) throws IOException { + acceptVisitor(getClassData(className), visitor); + } + + public MethodReference findVirtualMethod(String owner, String name, String desc) { + ArrayList superClasses = getSuperClasses(owner); + for (int i = superClasses.size() - 1; i > 0; i--) { // чекать текущий класс смысла нет + String className = superClasses.get(i); + MethodReference methodReference = getMethodReference(className, name, desc); + if (methodReference != null) { + System.out.println("found virtual method: " + methodReference); + return methodReference; + } + } + return null; + } + + private MethodReference getMethodReference(String type, String methodName, String desc) { + try { + return getMethodReferenceASM(type, methodName, desc); + } catch (Exception e) { + return getMethodReferenceReflect(type, methodName, desc); + } + } + + protected MethodReference getMethodReferenceASM(String type, String methodName, String desc) throws IOException { + FindMethodClassVisitor cv = new FindMethodClassVisitor(methodName, desc); + acceptVisitor(type, cv); + if (cv.found) { + return new MethodReference(type, cv.targetName, cv.targetDesc); + } + return null; + } + + protected MethodReference getMethodReferenceReflect(String type, String methodName, String desc) { + Class loadedClass = getLoadedClass(type); + if (loadedClass != null) { + for (Method m : loadedClass.getDeclaredMethods()) { + if (checkSameMethod(methodName, desc, m.getName(), Type.getMethodDescriptor(m))) { + return new MethodReference(type, m.getName(), Type.getMethodDescriptor(m)); + } + } + } + return null; + } + + protected boolean checkSameMethod(String sourceName, String sourceDesc, String targetName, String targetDesc) { + return sourceName.equals(targetName) && sourceDesc.equals(targetDesc); + } + + /** + * Возвращает суперклассы в порядке возрастающей конкретности (начиная с java/lang/Object + * и заканчивая данным типом) + */ + public ArrayList getSuperClasses(String type) { + ArrayList superclasses = new ArrayList(1); + superclasses.add(type); + while ((type = getSuperClass(type)) != null) { + superclasses.add(type); + } + Collections.reverse(superclasses); + return superclasses; + } + + private Class getLoadedClass(String type) { + if (m != null) { + try { + ClassLoader classLoader = ClassMetadataReader.class.getClassLoader(); + return (Class) m.invoke(classLoader, type.replace('/', '.')); + } catch (Exception e) { + e.printStackTrace(); + } + } + return null; + } + + public String getSuperClass(String type) { + try { + return getSuperClassASM(type); + } catch (Exception e) { + return getSuperClassReflect(type); + } + } + + protected String getSuperClassASM(String type) throws IOException { + CheckSuperClassVisitor cv = new CheckSuperClassVisitor(); + acceptVisitor(type, cv); + return cv.superClassName; + } + + protected String getSuperClassReflect(String type) { + Class loadedClass = getLoadedClass(type); + if (loadedClass != null) { + if (loadedClass.getSuperclass() == null) return null; + return loadedClass.getSuperclass().getName().replace('.', '/'); + } + return "java/lang/Object"; + } + + private class CheckSuperClassVisitor extends ClassVisitor { + + String superClassName; + + public CheckSuperClassVisitor() { + super(Opcodes.ASM5); + } + + @Override + public void visit(int version, int access, String name, String signature, + String superName, String[] interfaces) { + this.superClassName = superName; + } + } + + protected class FindMethodClassVisitor extends ClassVisitor { + + public String targetName; + public String targetDesc; + public boolean found; + + public FindMethodClassVisitor(String name, String desc) { + super(Opcodes.ASM5); + this.targetName = name; + this.targetDesc = desc; + } + + @Override + public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { + System.out.println("visiting " + name + "#" + desc); + if ((access & Opcodes.ACC_PRIVATE) == 0 && checkSameMethod(name, desc, targetName, targetDesc)) { + found = true; + targetName = name; + targetDesc = desc; + } + return null; + } + } + + public static class MethodReference { + + public final String owner; + public final String name; + public final String desc; + + public MethodReference(String owner, String name, String desc) { + this.owner = owner; + this.name = name; + this.desc = desc; + } + + public Type getType() { + return Type.getMethodType(desc); + } + + @Override public String toString() { + return "MethodReference{" + + "owner='" + owner + '\'' + + ", name='" + name + '\'' + + ", desc='" + desc + '\'' + + '}'; + } + } + +} \ No newline at end of file diff --git a/LaunchServer/src/main/java/ru/gravit/launchserver/asm/SafeClassWriter.java b/LaunchServer/src/main/java/ru/gravit/launchserver/asm/SafeClassWriter.java new file mode 100644 index 00000000..552c6741 --- /dev/null +++ b/LaunchServer/src/main/java/ru/gravit/launchserver/asm/SafeClassWriter.java @@ -0,0 +1,34 @@ +package ru.gravit.launchserver.asm; + +import org.objectweb.asm.ClassWriter; + +import java.util.ArrayList; + +/** + * ClassWriter с другой реализацией метода getCommonSuperClass: при его использовании не происходит загрузки классов. + * Однако, сама по себе загрузка классов редко является проблемой, потому что инициализация класса (вызов статических + * блоков) происходит не при загрузке класса. Проблемы появляются, когда изменения вставляются в зависимые друг от друга + * классы, тогда стандартная реализация отваливается с ClassCircularityError. + */ +public class SafeClassWriter extends ClassWriter { + private final ClassMetadataReader classMetadataReader; + + public SafeClassWriter(ClassMetadataReader classMetadataReader, int flags) { + super(flags); + this.classMetadataReader = classMetadataReader; + } + + @Override + protected String getCommonSuperClass(String type1, String type2) { + ArrayList superClasses1 = classMetadataReader.getSuperClasses(type1); + ArrayList superClasses2 = classMetadataReader.getSuperClasses(type2); + int size = Math.min(superClasses1.size(), superClasses2.size()); + int i; + for (i = 0; i < size && superClasses1.get(i).equals(superClasses2.get(i)); i++); + if (i == 0) { + return "java/lang/Object"; + } else { + return superClasses1.get(i-1); + } + } +} \ No newline at end of file