From f6539d4a078e4cc37a56b9cdb548ba6a61a73b93 Mon Sep 17 00:00:00 2001 From: zaaarf Date: Wed, 8 Mar 2023 15:37:17 +0100 Subject: feat: initial implementation of new system - multiple injectors/target - removed strict limitation of one patch per class - made finders far smarter - added boolean for obfuscation environment (currently never changed from default) --- .../exceptions/AmbiguousDefinitionException.java | 25 ++ .../lll/exceptions/MappingNotFoundException.java | 17 + .../lll/exceptions/TargetNotFoundException.java | 15 + src/main/java/ftbsc/lll/processor/ASTUtils.java | 126 ------ .../java/ftbsc/lll/processor/LilleroProcessor.java | 498 ++++++++++++++++----- .../ftbsc/lll/processor/annotations/FindField.java | 2 +- .../lll/processor/annotations/FindMethod.java | 4 +- .../ftbsc/lll/processor/annotations/Injector.java | 7 +- .../processor/annotations/MultipleInjectors.java | 11 + .../ftbsc/lll/processor/annotations/Target.java | 13 +- .../java/ftbsc/lll/processor/tools/ASTUtils.java | 151 +++++++ .../java/ftbsc/lll/processor/tools/SrgMapper.java | 211 +++++++++ 12 files changed, 829 insertions(+), 251 deletions(-) create mode 100644 src/main/java/ftbsc/lll/exceptions/AmbiguousDefinitionException.java create mode 100644 src/main/java/ftbsc/lll/exceptions/MappingNotFoundException.java create mode 100644 src/main/java/ftbsc/lll/exceptions/TargetNotFoundException.java delete mode 100644 src/main/java/ftbsc/lll/processor/ASTUtils.java create mode 100644 src/main/java/ftbsc/lll/processor/annotations/MultipleInjectors.java create mode 100644 src/main/java/ftbsc/lll/processor/tools/ASTUtils.java create mode 100644 src/main/java/ftbsc/lll/processor/tools/SrgMapper.java (limited to 'src') diff --git a/src/main/java/ftbsc/lll/exceptions/AmbiguousDefinitionException.java b/src/main/java/ftbsc/lll/exceptions/AmbiguousDefinitionException.java new file mode 100644 index 0000000..1befaa8 --- /dev/null +++ b/src/main/java/ftbsc/lll/exceptions/AmbiguousDefinitionException.java @@ -0,0 +1,25 @@ +package ftbsc.lll.exceptions; + +/** + * Thrown when the processor finds multiple methods matching the + * given criteria. + */ +public class AmbiguousDefinitionException extends RuntimeException { + + /** + * Constructs a new ambiguous definition exception with the specified detail message. + * @param message the detail message + */ + public AmbiguousDefinitionException(String message) { + super(message); + } + + /** + * Constructs a new ambiguous definition exception with the specified detail message and cause. + * @param message the detail message + * @param cause the cause, may be null (indicating nonexistent or unknown cause) + */ + public AmbiguousDefinitionException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/ftbsc/lll/exceptions/MappingNotFoundException.java b/src/main/java/ftbsc/lll/exceptions/MappingNotFoundException.java new file mode 100644 index 0000000..817761b --- /dev/null +++ b/src/main/java/ftbsc/lll/exceptions/MappingNotFoundException.java @@ -0,0 +1,17 @@ +package ftbsc.lll.exceptions; + +import ftbsc.lll.processor.tools.SrgMapper; + +/** + * Thrown upon failure to find the requested mapping within a loaded {@link SrgMapper}. + */ +public class MappingNotFoundException extends RuntimeException { + + /** + * Constructs a new mapping not found exception for the specified mapping. + * @param mapping the detail message + */ + public MappingNotFoundException(String mapping) { + super("Could not find mapping for " + mapping + "!"); + } +} diff --git a/src/main/java/ftbsc/lll/exceptions/TargetNotFoundException.java b/src/main/java/ftbsc/lll/exceptions/TargetNotFoundException.java new file mode 100644 index 0000000..5be3d77 --- /dev/null +++ b/src/main/java/ftbsc/lll/exceptions/TargetNotFoundException.java @@ -0,0 +1,15 @@ +package ftbsc.lll.exceptions; + +/** + * Thrown upon failure to find an existing method from a stub. + */ +public class TargetNotFoundException extends RuntimeException { + + /** + * Constructs a new target not found exception for the specified method stub. + * @param stub the stub's name (and descriptor possibly) + */ + public TargetNotFoundException(String stub) { + super("Could not find member corresponding to stub: " + stub); + } +} diff --git a/src/main/java/ftbsc/lll/processor/ASTUtils.java b/src/main/java/ftbsc/lll/processor/ASTUtils.java deleted file mode 100644 index eba5d89..0000000 --- a/src/main/java/ftbsc/lll/processor/ASTUtils.java +++ /dev/null @@ -1,126 +0,0 @@ -package ftbsc.lll.processor; - -import com.squareup.javapoet.ArrayTypeName; -import com.squareup.javapoet.ClassName; -import com.squareup.javapoet.MethodSpec; -import com.squareup.javapoet.TypeName; -import ftbsc.lll.tools.DescriptorBuilder; - -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.Modifier; -import javax.lang.model.element.TypeElement; -import javax.lang.model.type.TypeMirror; -import java.lang.annotation.Annotation; -import java.util.List; -import java.util.stream.Collectors; - -/** - * Collection of static utils that didn't really fit into the main class. - */ -public class ASTUtils { - /** - * Finds, among the methods of a class cl, the one annotated with ann, and tries to build - * a {@link ExecutableElement} from it. - * @param cl the {@link ExecutableElement} for the class containing the desired method - * @param ann the {@link Class} corresponding to the desired annotation - * @return a {@link List} of {@link MethodSpec}s annotated with the given annotation - * @since 0.2.0 - */ - public static List findAnnotatedMethods(TypeElement cl, Class ann) { - return cl.getEnclosedElements() - .stream() - .filter(e -> e.getAnnotation(ann) != null) - .map(e -> (ExecutableElement) e) - .collect(Collectors.toList()); - } - - /** - * Builds a type descriptor from the given {@link TypeMirror} - * @param t the {@link TypeMirror} representing the desired type - * @return a {@link String} containing the relevant descriptor - */ - public static String descriptorFromType(TypeMirror t) { - TypeName type = TypeName.get(t); - StringBuilder desc = new StringBuilder(); - //add array brackets - while(type instanceof ArrayTypeName) { - desc.append("["); - type = ((ArrayTypeName) type).componentType; - } - if(type instanceof ClassName) { - ClassName var = (ClassName) type; - desc.append(DescriptorBuilder.nameToDescriptor(var.canonicalName(), 0)); - } else { - if(TypeName.BOOLEAN.equals(type)) - desc.append("Z"); - else if(TypeName.CHAR.equals(type)) - desc.append("C"); - else if(TypeName.BYTE.equals(type)) - desc.append("B"); - else if(TypeName.SHORT.equals(type)) - desc.append("S"); - else if(TypeName.INT.equals(type)) - desc.append("I"); - else if(TypeName.FLOAT.equals(type)) - desc.append("F"); - else if(TypeName.LONG.equals(type)) - desc.append("J"); - else if(TypeName.DOUBLE.equals(type)) - desc.append("D"); - else if(TypeName.VOID.equals(type)) - desc.append("V"); - } - return desc.toString(); - } - - /** - * Builds a method descriptor from the given {@link ExecutableElement}. - * @param m the {@link ExecutableElement} for the method - * @return a {@link String} containing the relevant descriptor - */ - public static String descriptorFromExecutableElement(ExecutableElement m) { - StringBuilder methodSignature = new StringBuilder(); - methodSignature.append("("); - m.getParameters().forEach(p -> methodSignature.append(descriptorFromType(p.asType()))); - methodSignature.append(")"); - methodSignature.append(descriptorFromType(m.getReturnType())); - return methodSignature.toString(); - } - - /** - * Maps a {@link javax.lang.model.element.Modifier} to its reflective - * {@link java.lang.reflect.Modifier} equivalent. - * @param m the {@link Modifier} to map - * @return an integer representing the modifier - * @see java.lang.reflect.Modifier - * @since 0.2.0 - */ - public static int mapModifier(Modifier m) { - switch(m) { - case PUBLIC: - return java.lang.reflect.Modifier.PUBLIC; - case PROTECTED: - return java.lang.reflect.Modifier.PROTECTED; - case PRIVATE: - return java.lang.reflect.Modifier.PRIVATE; - case ABSTRACT: - return java.lang.reflect.Modifier.ABSTRACT; - case STATIC: - return java.lang.reflect.Modifier.STATIC; - case FINAL: - return java.lang.reflect.Modifier.FINAL; - case TRANSIENT: - return java.lang.reflect.Modifier.TRANSIENT; - case VOLATILE: - return java.lang.reflect.Modifier.VOLATILE; - case SYNCHRONIZED: - return java.lang.reflect.Modifier.SYNCHRONIZED; - case NATIVE: - return java.lang.reflect.Modifier.NATIVE; - case STRICTFP: - return java.lang.reflect.Modifier.STRICT; - default: - return 0; - } - } -} diff --git a/src/main/java/ftbsc/lll/processor/LilleroProcessor.java b/src/main/java/ftbsc/lll/processor/LilleroProcessor.java index 501200a..0277211 100644 --- a/src/main/java/ftbsc/lll/processor/LilleroProcessor.java +++ b/src/main/java/ftbsc/lll/processor/LilleroProcessor.java @@ -2,33 +2,30 @@ package ftbsc.lll.processor; import com.squareup.javapoet.*; import ftbsc.lll.IInjector; +import ftbsc.lll.exceptions.AmbiguousDefinitionException; +import ftbsc.lll.exceptions.MappingNotFoundException; +import ftbsc.lll.exceptions.TargetNotFoundException; import ftbsc.lll.processor.annotations.*; +import ftbsc.lll.processor.tools.SrgMapper; import ftbsc.lll.proxies.FieldProxy; import ftbsc.lll.proxies.MethodProxy; -import ftbsc.lll.tools.SrgMapper; import javax.annotation.processing.*; import javax.lang.model.SourceVersion; import javax.lang.model.element.*; import javax.lang.model.type.ExecutableType; -import javax.lang.model.type.MirroredTypeException; import javax.lang.model.type.TypeMirror; import javax.tools.Diagnostic; import javax.tools.FileObject; import javax.tools.JavaFileObject; import javax.tools.StandardLocation; import java.io.*; -import java.lang.reflect.Field; -import java.lang.reflect.Method; import java.net.URL; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; -import static ftbsc.lll.processor.ASTUtils.*; +import static ftbsc.lll.processor.tools.ASTUtils.*; /** * The actual annotation processor behind the magic. @@ -43,6 +40,11 @@ public class LilleroProcessor extends AbstractProcessor { */ private final Set generatedInjectors = new HashSet<>(); + /** + * A static boolean that should be set to true when ran in a non-obfuscated environment. + */ + public static boolean obfuscatedEnvironment = false; //todo: set this + /** * Where the actual processing happens. * It filters through whatever annotated class it's fed, and checks whether it contains @@ -64,7 +66,7 @@ public class LilleroProcessor extends AbstractProcessor { .filter(this::isValidInjector) .collect(Collectors.toSet()); if(!validInjectors.isEmpty()) { - validInjectors.forEach(this::generateInjector); + validInjectors.forEach(this::generateInjectors); if (!this.generatedInjectors.isEmpty()) { generateServiceProvider(); return true; @@ -102,11 +104,190 @@ public class LilleroProcessor extends AbstractProcessor { } /** - * Generates the Injector corresponding to the given class. + * Finds the class name and maps it to the correct format. + * @param patchAnn the {@link Patch} annotation containing target class info + * @param methodAnn the {@link FindMethod} annotation to fall back on, may be null + * @param mapper the {@link SrgMapper} to use + * @implNote De facto, there is never any difference between the SRG and MCP name of a class. + * In theory, differences only arise between SRG/MCP names and Notch (fully obfuscated) + * names. However, this method still performs a conversion - just in case there is an + * odd one out. + * @return the fully qualified class name + * @since 0.3.0 + */ + private static String findClassName(Patch patchAnn, FindMethod methodAnn, SrgMapper mapper) { + String fullyQualifiedName = + methodAnn == null || methodAnn.parent() == Object.class + ? getClassFullyQualifiedName(patchAnn.value()) + : getClassFullyQualifiedName(methodAnn.parent()); + return mapper.mapClass(fullyQualifiedName, obfuscatedEnvironment).replace('/', '.'); + } + + /** + * Finds the class name and maps it to the correct format. + * @param patchAnn the {@link Patch} annotation containing target class info + * @param mapper the {@link SrgMapper} to use + * @return the internal class name + * @since 0.3.0 + */ + private static String findClassName(Patch patchAnn, SrgMapper mapper) { + return findClassName(patchAnn, null, mapper); + } + + /** + * Finds the method name and maps it to the correct format. + * @param parentFQN the already mapped FQN of the parent class + * @param methodAnn the {@link FindMethod} annotation to fall back on, may be null + * @param stub the {@link ExecutableElement} for the stub + * @param mapper the {@link SrgMapper} to use + * @return the internal class name + * @since 0.3.0 + */ + private static String findMethodName(String parentFQN, FindMethod methodAnn, ExecutableElement stub, SrgMapper mapper) { + String methodName = methodAnn == null ? stub.getSimpleName().toString() : methodAnn.name(); + try { + methodName = mapper.mapMember( + parentFQN, + methodName, + obfuscatedEnvironment + ); + } catch(MappingNotFoundException e) { + //not found: try again with the name of the annotated method + if(methodAnn == null) { + methodName = mapper.mapMember( + parentFQN, + stub.getSimpleName().toString(), + obfuscatedEnvironment + ); + } else throw e; + } + return methodName; + } + + /** + * Finds the method name and maps it to the correct format. + * @param patchAnn the {@link Patch} annotation containing target class info + * @param methodAnn the {@link FindMethod} annotation to fall back on, may be null + * @param stub the {@link ExecutableElement} for the stub + * @param mapper the {@link SrgMapper} to use + * @return the internal class name + * @since 0.3.0 + */ + private static String findMethodName(Patch patchAnn, FindMethod methodAnn, ExecutableElement stub, SrgMapper mapper) { + return findMethodName(findClassName(patchAnn, methodAnn, mapper), methodAnn, stub, mapper); + } + + /** + * Finds a method given name, container and descriptor. + * @param fullyQualifiedNameParent the fully qualified name of the parent class of the method + * @param name the name to search for + * @param descr the descriptor to search for + * @param strict whether the search should be strict (see {@link Target#strict()} for more info) + * @return the desired method, if it exists + * @throws AmbiguousDefinitionException if it finds more than one candidate + * @throws TargetNotFoundException if it finds no valid candidate + * @since 0.3.0 + */ + private ExecutableElement findMethod(String fullyQualifiedNameParent, String name, String descr, boolean strict) { + TypeElement parent = processingEnv.getElementUtils().getTypeElement(fullyQualifiedNameParent); + if(parent == null) + throw new AmbiguousDefinitionException("Could not find parent class " + fullyQualifiedNameParent + "!"); + + //try to find by name + List candidates = parent.getEnclosedElements() + .stream() + .filter(e -> e instanceof ExecutableElement) + .map(e -> (ExecutableElement) e) + .filter(e -> e.getSimpleName().contentEquals(name)) + .collect(Collectors.toList()); + if(candidates.size() == 0) + throw new TargetNotFoundException(name + " " + descr); + if(candidates.size() == 1 && !strict) + return candidates.get(0); + if(descr == null) { + throw new AmbiguousDefinitionException( + "Found " + candidates.size() + + " methods named " + name + + " in class " + fullyQualifiedNameParent + "!" + ); + } else { + candidates = candidates.stream() + .filter(strict + ? c -> descr.equals(descriptorFromExecutableElement(c)) + : c -> descr.split("\\)")[0].equalsIgnoreCase(descriptorFromExecutableElement(c).split("\\)")[0]) + ).collect(Collectors.toList()); + if(candidates.size() == 0) + throw new TargetNotFoundException(name + " " + descr); + if(candidates.size() > 1) + throw new AmbiguousDefinitionException( + "Found " + candidates.size() + + " methods named " + name + + " in class " + fullyQualifiedNameParent + "!" + ); + return candidates.get(0); + } + } + + /** + * Finds the real method corresponding to a stub. + * @param stub the {@link ExecutableElement} for the stub + * @param mapper the {@link SrgMapper} to use + * @return the desired method, if it exists + * @throws AmbiguousDefinitionException if it finds more than one candidate + * @throws TargetNotFoundException if it finds no valid candidate + * @since 0.3.0 + */ + private ExecutableElement findRealMethod(ExecutableElement stub, SrgMapper mapper) { + Patch patchAnn = stub.getEnclosingElement().getAnnotation(Patch.class); + FindMethod findAnn = stub.getAnnotation(FindMethod.class); //this may be null, it means no fallback info + Target target = stub.getAnnotation(Target.class); //if this is null strict mode is always disabled + String parentFQN = findClassName(patchAnn, findAnn, mapper); + String methodName = findMethodName(patchAnn, findAnn, stub, mapper); + return findMethod( + parentFQN, + methodName, + descriptorFromExecutableElement(stub), + target != null && target.strict()); + } + + /** + * Finds the real field corresponding to a stub. + * @param stub the {@link ExecutableElement} for the stub + * @param mapper the {@link SrgMapper} to use + * @return the desired method, if it exists + * @throws TargetNotFoundException if it finds no valid candidate + * @since 0.3.0 + */ + private VariableElement findField(ExecutableElement stub, SrgMapper mapper) { + Patch patchAnn = stub.getEnclosingElement().getAnnotation(Patch.class); + FindField fieldAnn = stub.getAnnotation(FindField.class); + String parentName = mapper.mapClass(getClassFullyQualifiedName( + fieldAnn.parent().equals(Object.class) + ? patchAnn.value() + : fieldAnn.parent() + ), obfuscatedEnvironment); + String name = fieldAnn.name().equals("") + ? stub.getSimpleName().toString() + : fieldAnn.name(); + TypeElement parent = processingEnv.getElementUtils().getTypeElement(parentName); + List candidates = + parent.getEnclosedElements() + .stream() + .filter(f -> f instanceof VariableElement) + .filter(f -> f.getSimpleName().contentEquals(name)) + .map(f -> (VariableElement) f) + .collect(Collectors.toList()); + if(candidates.size() == 0) + throw new TargetNotFoundException(stub.getSimpleName().toString()); + else return candidates.get(0); //there can only ever be one + } + + /** + * Generates the Injector(s) contained in the given class. * Basically implements the {@link IInjector} interface for you. * @param cl the {@link TypeElement} for the given class */ - private void generateInjector(TypeElement cl) { + private void generateInjectors(TypeElement cl) { SrgMapper mapper; try { //TODO: cant we get it from local? URL url = new URL("https://data.fantabos.co/output.tsrg"); @@ -116,79 +297,149 @@ public class LilleroProcessor extends AbstractProcessor { is.close(); } catch(IOException e) { throw new RuntimeException("Could not open the specified TSRG file!", e); - } + } //todo attempt to proceed without mappings - Patch ann = cl.getAnnotation(Patch.class); - String targetClassCanonicalName; - try { - targetClassCanonicalName = ann.value().getCanonicalName(); - } catch(MirroredTypeException e) { - targetClassCanonicalName = e.getTypeMirror().toString(); - } //pretty sure class names de facto never change but better safe than sorry - String targetClassSrgName = mapper.getMcpClass(targetClassCanonicalName.replace('.', '/')); - - ExecutableElement targetMethod = findAnnotatedMethods(cl, Target.class).get(0); //there should only be one - String targetMethodDescriptor = descriptorFromMethodSpec(targetMethod); - String targetMethodSrgName = mapper.getSrgMember( - targetClassCanonicalName.replace('.', '/'), - targetMethod.getSimpleName() + " " + targetMethodDescriptor - ); - - ExecutableElement injectorMethod = findAnnotatedMethods(cl, Injector.class).get(0); //there should only be one + //find class information + Patch patchAnn = cl.getAnnotation(Patch.class); + String targetClassSrgName = findClassName(patchAnn, mapper); + //find package information Element packageElement = cl.getEnclosingElement(); while (packageElement.getKind() != ElementKind.PACKAGE) packageElement = packageElement.getEnclosingElement(); - String packageName = packageElement.toString(); - String injectorSimpleClassName = cl.getSimpleName().toString() + "Injector"; - String injectorClassName = packageName + "." + injectorSimpleClassName; - MethodSpec stubOverride = MethodSpec.overriding(targetMethod) - .addStatement("throw new $T($S)", RuntimeException.class, "This is a stub and should not have been called") - .build(); + //find injector(s) and target(s) + List injectors = findAnnotatedMethods(cl, MultipleInjectors.class); - MethodSpec inject = MethodSpec.methodBuilder("inject") - .addModifiers(Modifier.PUBLIC) - .returns(void.class) - .addAnnotation(Override.class) - .addParameter(ParameterSpec.builder( - TypeName.get(processingEnv - .getElementUtils() - .getTypeElement("org.objectweb.asm.tree.ClassNode").asType()), "clazz").build()) - .addParameter(ParameterSpec.builder( - TypeName.get(processingEnv - .getElementUtils() - .getTypeElement("org.objectweb.asm.tree.MethodNode").asType()), "main").build()) - .addStatement("super." + injectorMethod.getSimpleName() + "(clazz, main)", TypeName.get(cl.asType())) - .build(); + List targets = findAnnotatedMethods(cl, Target.class); - TypeSpec injectorClass = TypeSpec.classBuilder(injectorSimpleClassName) - .addModifiers(Modifier.PUBLIC) - .superclass(cl.asType()) - .addSuperinterface(ClassName.get(IInjector.class)) - .addMethod(buildStringReturnMethod("name", cl.getSimpleName().toString())) - .addMethod(buildStringReturnMethod("reason", ann.reason())) - .addMethod(buildStringReturnMethod("targetClass", targetClassSrgName.replace('/', '.'))) - .addMethod(buildStringReturnMethod("methodName", targetMethodSrgName)) - .addMethod(buildStringReturnMethod("methodDesc", targetMethodDescriptor)) - .addMethods(generateRequestedProxies(cl, mapper)) - .addMethod(stubOverride) - .addMethod(inject) - .build(); + //declare it once for efficiency + List targetNames = + targets.stream() + .map(ExecutableElement::getSimpleName) + .map(Object::toString) + .collect(Collectors.toList()); - JavaFile javaFile = JavaFile.builder(packageName, injectorClass).build(); + //this will contain the classes to generate: the key is the class name + Map toGenerate = new HashMap<>(); - try { - JavaFileObject injectorFile = processingEnv.getFiler().createSourceFile(injectorClassName); - PrintWriter out = new PrintWriter(injectorFile.openWriter()); - javaFile.writeTo(out); - out.close(); - } catch(IOException e) { - throw new RuntimeException(e); + for(ExecutableElement inj : injectors) { + MultipleInjectors minjAnn = inj.getAnnotation(MultipleInjectors.class); + int iterationNumber = 1; + for(Injector injectorAnn : minjAnn.value()) { //java is dumb + List injectionCandidates = targets; + + //case 1: it has a name, try to match it + if(!injectorAnn.targetName().equals("") && targetNames.contains(injectorAnn.targetName())) + injectionCandidates = + injectionCandidates + .stream() + .filter(i -> i.getSimpleName().toString().equals(injectorAnn.targetName())) + .collect(Collectors.toList()); + + //case 2: try to match by injectTargetName + String inferredName = inj.getSimpleName() + .toString() + .replaceFirst("inject", ""); + injectionCandidates = + injectionCandidates + .stream() + .filter(t -> t.getSimpleName().toString().equalsIgnoreCase(inferredName)) + .collect(Collectors.toList()); + + //case 3: there is only one target + if(targets.size() == 1) + injectionCandidates.add(targets.get(0)); + + ExecutableElement injectionTarget = null; + + if(injectionCandidates.size() == 1) + injectionTarget = injectionCandidates.get(0); + + if(injectorAnn.params().length != 0) { + StringBuilder descr = new StringBuilder("("); + for(Class p : injectorAnn.params()) + descr.append(descriptorFromType(TypeName.get(p))); + descr.append(")"); + injectionCandidates = + injectionCandidates + .stream() + .filter(t -> //we care about arguments but not really about return type + descr.toString() + .split("\\)")[0] + .equalsIgnoreCase(descriptorFromExecutableElement(t).split("\\)")[0]) + ).collect(Collectors.toList()); + } + + if(injectionCandidates.size() == 1) + injectionTarget = injectionCandidates.get(0); + + //if we haven't found it yet, it's an ambiguity + if(injectionTarget == null) + throw new AmbiguousDefinitionException("Unclear target for injector " + inj.getSimpleName().toString() + "!"); + else toGenerate.put( + cl.getSimpleName().toString() + "Injector" + iterationNumber, + new InjectorInfo( + inj, findRealMethod( + injectionTarget, + mapper + ) + ) + ); + iterationNumber++; + } } - this.generatedInjectors.add(injectorClassName); + //iterate over the map and generate the classes + for(String injName : toGenerate.keySet()) { + MethodSpec stubOverride = MethodSpec.overriding(toGenerate.get(injName).target) + .addStatement("throw new $T($S)", RuntimeException.class, "This is a stub and should not have been called") + .build(); + + MethodSpec inject = MethodSpec.methodBuilder("inject") + .addModifiers(Modifier.PUBLIC) + .returns(void.class) + .addAnnotation(Override.class) + .addParameter(ParameterSpec.builder( + TypeName.get(processingEnv + .getElementUtils() + .getTypeElement("org.objectweb.asm.tree.ClassNode").asType()), "clazz").build()) + .addParameter(ParameterSpec.builder( + TypeName.get(processingEnv + .getElementUtils() + .getTypeElement("org.objectweb.asm.tree.MethodNode").asType()), "main").build()) + .addStatement("super." + toGenerate.get(injName).injector.getSimpleName() + "(clazz, main)", TypeName.get(cl.asType())) + .build(); + + TypeSpec injectorClass = TypeSpec.classBuilder(injName) + .addModifiers(Modifier.PUBLIC) + .superclass(cl.asType()) + .addSuperinterface(ClassName.get(IInjector.class)) + .addMethod(buildStringReturnMethod("name", cl.getSimpleName().toString())) + .addMethod(buildStringReturnMethod("reason", patchAnn.reason())) + .addMethod(buildStringReturnMethod("targetClass", targetClassSrgName.replace('/', '.'))) + .addMethod(buildStringReturnMethod("methodName", toGenerate.get(injName).target.getSimpleName().toString())) + .addMethod(buildStringReturnMethod("methodDesc", descriptorFromExecutableElement(toGenerate.get(injName).target))) + .addMethods(generateRequestedProxies(cl, mapper)) + .addMethod(stubOverride) + .addMethod(inject) + .build(); + + JavaFile javaFile = JavaFile.builder(packageName, injectorClass).build(); + String injectorClassName = packageName + "." + injName; + + try { + JavaFileObject injectorFile = processingEnv.getFiler().createSourceFile(injectorClassName); + PrintWriter out = new PrintWriter(injectorFile.openWriter()); + javaFile.writeTo(out); + out.close(); + } catch(IOException e) { + throw new RuntimeException(e); + } + + this.generatedInjectors.add(injectorClassName); + } } /** @@ -220,64 +471,44 @@ public class LilleroProcessor extends AbstractProcessor { .filter(m -> !m.getModifiers().contains(Modifier.STATIC)) //skip static stuff as we can't override it .filter(m -> !m.getModifiers().contains(Modifier.FINAL)) //in case someone is trying to be funny .forEach(m -> { - FindMethod ann = m.getAnnotation(FindMethod.class); - String targetMethodName = ann.name().equals("") ? m.getSimpleName().toString() : ann.name(); - try { - MethodSpec.Builder b = MethodSpec.overriding(m); - Method targetMethod = ann.parent().getMethod( - targetMethodName, - ann.params() - ); - b.addStatement("$T bd = $T.builder($S)", - MethodProxy.Builder.class, - MethodProxy.class, - targetMethodName - ); - b.addStatement("bd.setParent($S)", targetMethod.getDeclaringClass().getCanonicalName()); - b.addStatement("bd.setModifier($L)", targetMethod.getModifiers()); - for(Class p : targetMethod.getParameterTypes()) - b.addStatement("bd.addParameter($T.class)", p); - b.addStatement("bd.setReturnType($T.class)", targetMethod.getReturnType()); - b.addStatement("return bd.build()"); - generated.add(b.build()); - } catch(NoSuchMethodException e) { - processingEnv.getMessager().printMessage( - Diagnostic.Kind.ERROR, - "Method not found: " + targetMethodName - ); - } + ExecutableElement targetMethod = findRealMethod(m, mapper); + MethodSpec.Builder b = MethodSpec.overriding(m); + b.addStatement("$T bd = $T.builder($S)", + MethodProxy.Builder.class, + MethodProxy.class, + m.getSimpleName().toString() + ); + b.addStatement("bd.setParent($S)", ((TypeElement) targetMethod.getEnclosingElement()).getQualifiedName().toString()); + for(Modifier mod : targetMethod.getModifiers()) + b.addStatement("bd.addModifier($L)", mapModifier(mod)); + for(TypeParameterElement p : targetMethod.getTypeParameters()) + b.addStatement("bd.addParameter($T.class)", p.asType()); + b.addStatement("bd.setReturnType($T.class)", targetMethod.getReturnType()); + b.addStatement("return bd.build()"); + generated.add(b.build()); }); findAnnotatedMethods(cl, FindField.class) .stream() .filter(m -> !m.getModifiers().contains(Modifier.STATIC)) .filter(m -> !m.getModifiers().contains(Modifier.FINAL)) .forEach(m -> { - FindField ann = m.getAnnotation(FindField.class); - String targetFieldName = ann.name().equals("") ? m.getSimpleName().toString() : ann.name(); - try { - MethodSpec.Builder b = MethodSpec.overriding(m); - Field targetField = ann.parent().getField(targetFieldName); - b.addStatement("$T bd = $T.builder($S)", - FieldProxy.Builder.class, - FieldProxy.class, - targetFieldName - ); - b.addStatement("bd.setParent($S)", targetField.getDeclaringClass().getCanonicalName()); - b.addStatement("bd.setModifier($L)", targetField.getModifiers()); - b.addStatement("bd.setType($T.class)", targetField.getType()); - b.addStatement("return bd.build()"); - generated.add(b.build()); - } catch(NoSuchFieldException e) { - processingEnv.getMessager().printMessage( - Diagnostic.Kind.ERROR, - "Field not found: " + targetFieldName + " in class " + ann.parent().getCanonicalName() - ); - } + VariableElement targetField = findField(m, mapper); + MethodSpec.Builder b = MethodSpec.overriding(m); + b.addStatement("$T bd = $T.builder($S)", + FieldProxy.Builder.class, + FieldProxy.class, + targetField.getSimpleName().toString() + ); + b.addStatement("bd.setParent($S)", ((TypeElement) targetField.getEnclosingElement()).getQualifiedName().toString()); + for(Modifier mod : targetField.getModifiers()) + b.addStatement("bd.addModifier($L)", mapModifier(mod)); + b.addStatement("bd.setType($T.class)", targetField.asType()); + b.addStatement("return bd.build()"); + generated.add(b.build()); }); return generated; } - /** * Generates the Service Provider file for the generated injectors. */ @@ -294,4 +525,31 @@ public class LilleroProcessor extends AbstractProcessor { throw new RuntimeException(e); } } + + /** + * Container for information about a class that is to be generated. + * Only used internally. + */ + private static class InjectorInfo { + /** + * The {@link ExecutableElement} corresponding to the injector method. + */ + public final ExecutableElement injector; + + + /** + * The {@link ExecutableElement} corresponding to the target method. + */ + public final ExecutableElement target; + + /** + * Public constructor. + * @param injector the injector {@link ExecutableElement} + * @param target the target {@link ExecutableElement} + */ + public InjectorInfo(ExecutableElement injector, ExecutableElement target) { + this.injector = injector; + this.target = target; + } + } } \ No newline at end of file diff --git a/src/main/java/ftbsc/lll/processor/annotations/FindField.java b/src/main/java/ftbsc/lll/processor/annotations/FindField.java index 9b5a824..c13fd46 100644 --- a/src/main/java/ftbsc/lll/processor/annotations/FindField.java +++ b/src/main/java/ftbsc/lll/processor/annotations/FindField.java @@ -16,6 +16,6 @@ import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.CLASS) @java.lang.annotation.Target(ElementType.METHOD) public @interface FindField { - Class parent(); + Class parent() default Object.class; String name() default ""; } diff --git a/src/main/java/ftbsc/lll/processor/annotations/FindMethod.java b/src/main/java/ftbsc/lll/processor/annotations/FindMethod.java index bf93442..76fe560 100644 --- a/src/main/java/ftbsc/lll/processor/annotations/FindMethod.java +++ b/src/main/java/ftbsc/lll/processor/annotations/FindMethod.java @@ -17,7 +17,7 @@ import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.CLASS) @java.lang.annotation.Target(ElementType.METHOD) public @interface FindMethod { - Class parent(); + Class parent() default Object.class; String name() default ""; - Class[] params(); + Class[] params() default {}; } diff --git a/src/main/java/ftbsc/lll/processor/annotations/Injector.java b/src/main/java/ftbsc/lll/processor/annotations/Injector.java index f5e22aa..c26f704 100644 --- a/src/main/java/ftbsc/lll/processor/annotations/Injector.java +++ b/src/main/java/ftbsc/lll/processor/annotations/Injector.java @@ -1,6 +1,7 @@ package ftbsc.lll.processor.annotations; import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -14,5 +15,9 @@ import java.lang.annotation.RetentionPolicy; * @see Target */ @Retention(RetentionPolicy.CLASS) +@Repeatable(MultipleInjectors.class) @java.lang.annotation.Target(ElementType.METHOD) -public @interface Injector {} +public @interface Injector { + String targetName() default ""; + Class[] params() default {}; +} diff --git a/src/main/java/ftbsc/lll/processor/annotations/MultipleInjectors.java b/src/main/java/ftbsc/lll/processor/annotations/MultipleInjectors.java new file mode 100644 index 0000000..5c6382e --- /dev/null +++ b/src/main/java/ftbsc/lll/processor/annotations/MultipleInjectors.java @@ -0,0 +1,11 @@ +package ftbsc.lll.processor.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.CLASS) +@java.lang.annotation.Target(ElementType.METHOD) +public @interface MultipleInjectors { + Injector[] value(); +} diff --git a/src/main/java/ftbsc/lll/processor/annotations/Target.java b/src/main/java/ftbsc/lll/processor/annotations/Target.java index 885bd9c..38477d1 100644 --- a/src/main/java/ftbsc/lll/processor/annotations/Target.java +++ b/src/main/java/ftbsc/lll/processor/annotations/Target.java @@ -15,4 +15,15 @@ import java.lang.annotation.RetentionPolicy; */ @Retention(RetentionPolicy.CLASS) @java.lang.annotation.Target(ElementType.METHOD) -public @interface Target {} +public @interface Target { + + /** + * When set to false, tells the processor to first try to match a single method by name, + * and to only check parameters if further clarification is needed. + * @implNote While non-strict mode is more computationally efficient, it's ultimately not + * relevant, as it only matters at compile time. Do not set this to true unless + * you are sure know what you're doing. + * @since 0.3.0 + */ + boolean strict() default true; +} diff --git a/src/main/java/ftbsc/lll/processor/tools/ASTUtils.java b/src/main/java/ftbsc/lll/processor/tools/ASTUtils.java new file mode 100644 index 0000000..c35e008 --- /dev/null +++ b/src/main/java/ftbsc/lll/processor/tools/ASTUtils.java @@ -0,0 +1,151 @@ +package ftbsc.lll.processor.tools; + +import com.squareup.javapoet.ArrayTypeName; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeName; +import ftbsc.lll.tools.DescriptorBuilder; + +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.MirroredTypeException; +import javax.lang.model.type.TypeMirror; +import java.lang.annotation.Annotation; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Collection of static utils that didn't really fit into the main class. + */ +public class ASTUtils { + /** + * Finds, among the methods of a class cl, the one annotated with ann, and tries to build + * a {@link ExecutableElement} from it. + * @param cl the {@link ExecutableElement} for the class containing the desired method + * @param ann the {@link Class} corresponding to the desired annotation + * @return a {@link List} of {@link MethodSpec}s annotated with the given annotation + * @since 0.2.0 + */ + public static List findAnnotatedMethods(TypeElement cl, Class ann) { + return cl.getEnclosedElements() + .stream() + .filter(e -> e.getAnnotation(ann) != null) + .map(e -> (ExecutableElement) e) + .collect(Collectors.toList()); + } + + /** + * Builds a type descriptor from the given {@link TypeName}. + * @param type the {@link TypeName} representing the desired type + * @return a {@link String} containing the relevant descriptor + */ + public static String descriptorFromType(TypeName type) { + StringBuilder desc = new StringBuilder(); + //add array brackets + while(type instanceof ArrayTypeName) { + desc.append("["); + type = ((ArrayTypeName) type).componentType; + } + if(type instanceof ClassName) { + ClassName var = (ClassName) type; + desc.append(DescriptorBuilder.nameToDescriptor(var.canonicalName(), 0)); + } else { + if(TypeName.BOOLEAN.equals(type)) + desc.append("Z"); + else if(TypeName.CHAR.equals(type)) + desc.append("C"); + else if(TypeName.BYTE.equals(type)) + desc.append("B"); + else if(TypeName.SHORT.equals(type)) + desc.append("S"); + else if(TypeName.INT.equals(type)) + desc.append("I"); + else if(TypeName.FLOAT.equals(type)) + desc.append("F"); + else if(TypeName.LONG.equals(type)) + desc.append("J"); + else if(TypeName.DOUBLE.equals(type)) + desc.append("D"); + else if(TypeName.VOID.equals(type)) + desc.append("V"); + } + return desc.toString(); + } + + /** + * Builds a type descriptor from the given {@link TypeMirror}. + * @param t the {@link TypeMirror} representing the desired type + * @return a {@link String} containing the relevant descriptor + */ + public static String descriptorFromType(TypeMirror t) { + return descriptorFromType(TypeName.get(t)); + } + + /** + * Builds a method descriptor from the given {@link ExecutableElement}. + * @param m the {@link ExecutableElement} for the method + * @return a {@link String} containing the relevant descriptor + */ + public static String descriptorFromExecutableElement(ExecutableElement m) { + StringBuilder methodSignature = new StringBuilder(); + methodSignature.append("("); + m.getParameters().forEach(p -> methodSignature.append(descriptorFromType(p.asType()))); + methodSignature.append(")"); + methodSignature.append(descriptorFromType(m.getReturnType())); + return methodSignature.toString(); + } + + /** + * Maps a {@link javax.lang.model.element.Modifier} to its reflective + * {@link java.lang.reflect.Modifier} equivalent. + * @param m the {@link Modifier} to map + * @return an integer representing the modifier + * @see java.lang.reflect.Modifier + * @since 0.2.0 + */ + public static int mapModifier(Modifier m) { + switch(m) { + case PUBLIC: + return java.lang.reflect.Modifier.PUBLIC; + case PROTECTED: + return java.lang.reflect.Modifier.PROTECTED; + case PRIVATE: + return java.lang.reflect.Modifier.PRIVATE; + case ABSTRACT: + return java.lang.reflect.Modifier.ABSTRACT; + case STATIC: + return java.lang.reflect.Modifier.STATIC; + case FINAL: + return java.lang.reflect.Modifier.FINAL; + case TRANSIENT: + return java.lang.reflect.Modifier.TRANSIENT; + case VOLATILE: + return java.lang.reflect.Modifier.VOLATILE; + case SYNCHRONIZED: + return java.lang.reflect.Modifier.SYNCHRONIZED; + case NATIVE: + return java.lang.reflect.Modifier.NATIVE; + case STRICTFP: + return java.lang.reflect.Modifier.STRICT; + default: + return 0; + } + } + + /** + * Safely converts a {@link Class} to its fully qualified name. See + * this blogpost + * for more information. + * @param clazz the class to get the name for + * @return the fully qualified name of the given class + * @since 0.3.0 + */ + public static String getClassFullyQualifiedName(Class clazz) { + try { + return clazz.getCanonicalName(); + } catch(MirroredTypeException e) { + return e.getTypeMirror().toString(); + } + } +} diff --git a/src/main/java/ftbsc/lll/processor/tools/SrgMapper.java b/src/main/java/ftbsc/lll/processor/tools/SrgMapper.java new file mode 100644 index 0000000..d3c9f79 --- /dev/null +++ b/src/main/java/ftbsc/lll/processor/tools/SrgMapper.java @@ -0,0 +1,211 @@ +package ftbsc.lll.processor.tools; + +import ftbsc.lll.exceptions.MappingNotFoundException; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; + +/** + * Parses a .tsrg file into a mapper capable of converting from + * deobfuscated names to SRG names. + * Obviously, it may only be used at runtime if the .tsrg file is + * included in the resources. However, in that case, I'd recommend + * using the built-in Forge one and refrain from including an extra + * resource for no good reason. + * @since 0.2.0 + */ +public class SrgMapper { + + /** + * A Map using the deobfuscated names as keys, + * holding information for that Srg class as value. + */ + private final Map mapper = new HashMap<>(); + + /** + * The public constructor. + * Should be passed a {@link Stream} of Strings, one representing each line. + * Whether they contain line endings or not is irrelevant. + * @param str a {@link Stream} of strings + */ + public SrgMapper(Stream str) { + AtomicReference currentClass = new AtomicReference<>(""); + str.forEach(l -> { + if(l.startsWith("\t")) + mapper.get(currentClass.get()).addMember(l); + else { + ObfuscationData s = new ObfuscationData(l); + currentClass.set(s.mcpName); + mapper.put(s.mcpName, s); + } + }); + } + + /** + * Gets the SRG-obfuscated name of the class. + * @param mcp the MCP (deobfuscated) internal name of the desired class + * @return the SRG name of the class + * @throws MappingNotFoundException if no mapping is found + */ + public String getSrgClass(String mcp) { + ObfuscationData data = mapper.get(mcp); + if(data == null) + throw new MappingNotFoundException(mcp); + else return data.srgName; + } + + /** + * Gets the MCP (deobfuscated) name of the class. + * Due to how it's implemented, it's considerably less efficient than its + * opposite operation. + * @param srg the SRG-obfuscated internal name of the desired class + * @return the MCP name of the class + */ + public String getMcpClass(String srg) { + ObfuscationData data = getObfuscationData(srg); + return data.mcpName; + } + + /** + * Gets one between the SRG and MCP names. + * @param name the internal name of the desired class in either format + * @param obf whether it should return the obfuscated name + * @return a {@link String} containing the internal name of the class + * @throws MappingNotFoundException if no mapping is found + * @since 0.3.0 + */ + public String mapClass(String name, boolean obf) { + String srg; + try { + srg = this.getSrgClass(name); + } catch(MappingNotFoundException e) { + srg = name; + name = this.getMcpClass(srg); + } + if(obf) return srg; + else return name; + } + + /** + * Gets the SRG-obfuscated name of a class member (field or method). + * The method signature must be in this format: "methodName methodDescriptor", + * with a space, because that's how it is in .tsrg files. + * @param mcpClass the MCP (deobfuscated) internal name of the container class + * @param member the field name or method signature + * @return the SRG name of the given member + * @throws MappingNotFoundException if no mapping is found + */ + public String getSrgMember(String mcpClass, String member) { + ObfuscationData data = mapper.get(mcpClass); + if(data == null) + throw new MappingNotFoundException(mcpClass + "::" + member); + return data.members.get(member); + } + + /** + * Gets the MCP (deobfuscated) name of the given member. + * Due to how it's implemented, it's considerably less efficient than its + * opposite operation. + * @param srgClass the SRG-obfuscated internal name of the container class + * @param member the field name or method signature + * @return the MCP name of the given member + */ + public String getMcpMember(String srgClass, String member) { + ObfuscationData data = getObfuscationData(srgClass); + for(String mcp : data.members.keySet()) + if(data.members.get(mcp).equals(member)) + return mcp; + return null; + } + + /** + * Obfuscates or deobfuscates a member, given one of its names and the effective. + * @param className the internal or fully qualified name of the container class + * @param memberName the member of the class + * @param obf whether it should return the obfuscated name + * @return the mapped member name + * @throws MappingNotFoundException if no mapping is found + * @since 0.3.0 + */ + public String mapMember(String className, String memberName, boolean obf) { + className = className.replace('.', '/'); + String effectiveClassName = this.mapClass(className, obf); + String srgMemberName; + try { + srgMemberName = this.getSrgMember(effectiveClassName, memberName); + } catch(MappingNotFoundException e) { + srgMemberName = memberName; + memberName = this.getMcpMember(effectiveClassName, memberName); + } + if(obf) return srgMemberName; + else return memberName; + } + + /** + * Used internally. Gets the obfuscation data corresponding to the given SRG name. + * @return the desired {@link ObfuscationData} object + * @throws MappingNotFoundException if no {@link ObfuscationData} object is found + */ + private ObfuscationData getObfuscationData(String srg) { + for(ObfuscationData s : mapper.values()) + if(s.srgName.equals(srg)) + return s; + throw new MappingNotFoundException(srg); + } + + /** + * Private class used internally for storing information about each + * class. It's private because there is no good reason anyone would + * want to access this outside of this class. + */ + private static class ObfuscationData { + /** + * The MCP internal name (FQN with '/' instad of '.') of the class. + */ + private final String mcpName; + + /** + * The SRG internal name (FQN with '/' instad of '.') of the class. + */ + private final String srgName; + + /** + * A {@link Map} tying each member's deobfuscated name or signature to its + * SRG name. + */ + private final Map members; + + + /** + * The constructor. It takes in the line where the class is declared, + * which looks something like this: + * {@code internal/name/mcp internal/name/srg } + * @param s the String represeting the declaration line + */ + private ObfuscationData(String s) { + String[] split = s.trim().split(" "); + this.mcpName = split[0]; + this.srgName = split[1]; + this.members = new HashMap<>(); + } + + /** + * Adds a member to the target class. It takes in the line where the + * member is declared. + * For fields it looks like this: + * {@code fieldMcpName field_srg_name} + * For methods it looks like this: + * {@code methodName methodDescriptor method_srg_name} + * @param s the String representing the declaration line + */ + public void addMember(String s) { + String[] split = s.trim().split(" "); + if(split.length == 2) //field + members.put(split[0], split[1]); + else if (split.length == 3) //method + members.put(split[0] + " " + split[1], split[2]); + } + } +} -- cgit v1.2.3-56-ga3b1