diff options
Diffstat (limited to 'src/main/java')
14 files changed, 1055 insertions, 282 deletions
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/InvalidResourceException.java b/src/main/java/ftbsc/lll/exceptions/InvalidResourceException.java new file mode 100644 index 0000000..76f12a5 --- /dev/null +++ b/src/main/java/ftbsc/lll/exceptions/InvalidResourceException.java @@ -0,0 +1,22 @@ +package ftbsc.lll.exceptions; + +/** + * Thrown when a resource passed as an argument is not found. + */ +public class InvalidResourceException extends RuntimeException { + + /** + * Empty constructor, used when the provided resource exists but is empty. + */ + public InvalidResourceException() { + super("The specified resource was empty!"); + } + + /** + * Named constructor, used when the specified resource doesn't exist. + * @param name the resource name + */ + public InvalidResourceException(String name) { + super(String.format("Specified resource %s was not found!", name)); + } +} 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..e943c01 --- /dev/null +++ b/src/main/java/ftbsc/lll/exceptions/MappingNotFoundException.java @@ -0,0 +1,27 @@ +package ftbsc.lll.exceptions; + +import ftbsc.lll.processor.tools.obfuscation.ObfuscationMapper; + +/** + * Thrown upon failure to find the requested mapping within a loaded {@link ObfuscationMapper}. + */ +public class MappingNotFoundException extends RuntimeException { + + /** + * Constructs a new mapping not found exception for the specified mapping. + * @param mapping the relevant mapping + */ + public MappingNotFoundException(String mapping) { + super(String.format("Could not find mapping for %s!", mapping)); + } + + /** + * Constructs a new mapping not found exception for the specified mapping + * with the specified reason. + * @param mapping the relevant mapping + * @param reason the reason message + */ + public MappingNotFoundException(String mapping, String reason) { + this(mapping + ": " + reason); + } +} 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..c82e0fc --- /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(String.format("Could not find member corresponding to stub: %s.", 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<ExecutableElement> findAnnotatedMethods(TypeElement cl, Class<? extends Annotation> 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..432738c 100644 --- a/src/main/java/ftbsc/lll/processor/LilleroProcessor.java +++ b/src/main/java/ftbsc/lll/processor/LilleroProcessor.java @@ -2,33 +2,32 @@ package ftbsc.lll.processor; import com.squareup.javapoet.*; import ftbsc.lll.IInjector; +import ftbsc.lll.exceptions.AmbiguousDefinitionException; +import ftbsc.lll.exceptions.InvalidResourceException; import ftbsc.lll.processor.annotations.*; +import ftbsc.lll.processor.tools.obfuscation.ObfuscationMapper; 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.net.URI; +import java.net.URISyntaxException; 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.*; +import static ftbsc.lll.processor.tools.ASTUtils.getClassFullyQualifiedName; +import static ftbsc.lll.processor.tools.JavaPoetUtils.*; /** * The actual annotation processor behind the magic. @@ -36,6 +35,7 @@ import static ftbsc.lll.processor.ASTUtils.*; */ @SupportedAnnotationTypes("ftbsc.lll.processor.annotations.Patch") @SupportedSourceVersion(SourceVersion.RELEASE_8) +@SupportedOptions("mappingsFile") public class LilleroProcessor extends AbstractProcessor { /** * A {@link Set} of {@link String}s that will contain the fully qualified names @@ -44,6 +44,50 @@ public class LilleroProcessor extends AbstractProcessor { private final Set<String> generatedInjectors = new HashSet<>(); /** + * The {@link ObfuscationMapper} used to convert classes and variables + * to their obfuscated equivalent. Will be null when no mapper is in use. + */ + private ObfuscationMapper mapper; + + /** + * Initializes the processor with the processing environment by + * setting the {@code processingEnv} field to the value of the + * {@code processingEnv} argument. + * @param processingEnv environment to access facilities the tool framework + * provides to the processor + * @throws IllegalStateException if this method is called more than once. + * @since 0.3.0 + */ + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + super.init(processingEnv); + String location = processingEnv.getOptions().get("mappingsFile"); + if(location == null) + mapper = null; + else { + InputStream targetStream; + try { + URI target = new URI(location); + targetStream = target.toURL().openStream(); + } catch(URISyntaxException | IOException e) { + //may be a local file path + File f = new File(location); + if(!f.exists()) + throw new InvalidResourceException(location); + try { + targetStream = new FileInputStream(f); + } catch(FileNotFoundException ex) { + throw new InvalidResourceException(location); + } + } + //assuming its tsrg file + //todo: replace crappy homebaked parser with actual library + this.mapper = new ObfuscationMapper(new BufferedReader(new InputStreamReader(targetStream, + StandardCharsets.UTF_8)).lines()); + } + } + + /** * Where the actual processing happens. * It filters through whatever annotated class it's fed, and checks whether it contains * the required information. It then generates injectors and a service provider for every @@ -56,7 +100,7 @@ public class LilleroProcessor extends AbstractProcessor { @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { for (TypeElement annotation : annotations) { - if(annotation.getQualifiedName().toString().equals(Patch.class.getName())) { + if(annotation.getQualifiedName().contentEquals(Patch.class.getName())) { Set<TypeElement> validInjectors = roundEnv.getElementsAnnotatedWith(annotation) .stream() @@ -64,7 +108,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; @@ -96,114 +140,163 @@ public class LilleroProcessor extends AbstractProcessor { })) return true; else { processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, - "Missing valid @Injector method in @Patch class " + elem + ", skipping."); + String.format("Missing valid @Injector method in @Patch class %s, skipping.", elem)); return false; } } /** - * Generates the Injector corresponding to the given class. + * 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) { - SrgMapper mapper; - try { //TODO: cant we get it from local? - URL url = new URL("https://data.fantabos.co/output.tsrg"); - InputStream is = url.openStream(); - mapper = new SrgMapper(new BufferedReader(new InputStreamReader(is, - StandardCharsets.UTF_8)).lines()); - is.close(); - } catch(IOException e) { - throw new RuntimeException("Could not open the specified TSRG file!", e); - } - - 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 + private void generateInjectors(TypeElement cl) { + //find class information + Patch patchAnn = cl.getAnnotation(Patch.class); + String targetClassFQN = + findClassName(getClassFullyQualifiedName(patchAnn, Patch::value), this.mapper) + .replace('/', '.'); + //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(); - - 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(); - - 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(); - - JavaFile javaFile = JavaFile.builder(packageName, injectorClass).build(); - 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); + //find injector(s) and target(s) + List<ExecutableElement> injectors = findAnnotatedMethods(cl, Injector.class); + + List<ExecutableElement> targets = findAnnotatedMethods(cl, Target.class); + + //declare it once for efficiency + List<String> targetNames = + targets.stream() + .map(ExecutableElement::getSimpleName) + .map(Object::toString) + .collect(Collectors.toList()); + + //this will contain the classes to generate: the key is the class name + Map<String, InjectorInfo> toGenerate = new HashMap<>(); + + for(ExecutableElement inj : injectors) { + Injector[] minjAnn = inj.getAnnotationsByType(Injector.class); + int iterationNumber = 1; + for(Injector injectorAnn : minjAnn) { //java is dumb + List<ExecutableElement> injectionCandidates = targets; + + if(!injectorAnn.targetName().equals("") && targetNames.contains(injectorAnn.targetName())) { + //case 1: it has a name, try to match it + injectionCandidates = + injectionCandidates + .stream() + .filter(i -> i.getSimpleName().contentEquals(injectorAnn.targetName())) + .collect(Collectors.toList()); + } else if(targets.size() == 1) { + //case 2: there is only one target + injectionCandidates = new ArrayList<>(); + injectionCandidates.add(targets.get(0)); + } else { + //case 3: 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()); + } + + ExecutableElement injectionTarget = null; + + if(injectionCandidates.size() == 1) + injectionTarget = injectionCandidates.get(0); + + else { + List<TypeMirror> params = classArrayFromAnnotation(injectorAnn, Injector::params, processingEnv.getElementUtils()); + + if(params.size() != 0) { + StringBuilder descr = new StringBuilder("("); + for(TypeMirror p : 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(String.format("Unclear target for injector %s::%s!", cl.getSimpleName(), inj.getSimpleName())); + else toGenerate.put( + String.format("%sInjector%d", cl.getSimpleName(), iterationNumber), + new InjectorInfo(inj, injectionTarget) + ); + iterationNumber++; + } } - this.generatedInjectors.add(injectorClassName); - } + //iterate over the map and generate the classes + for(String injName : toGenerate.keySet()) { + String targetMethodDescriptor = descriptorFromExecutableElement(toGenerate.get(injName).target); + String targetMethodName = findMemberName(targetClassFQN, toGenerate.get(injName).target.getSimpleName().toString(), targetMethodDescriptor, this.mapper); - /** - * Builds a {@link MethodSpec} for a public method whose body simply returns a {@link String}. - * @param name the name of the method - * @param returnString the {@link String} to return - * @return the built {@link MethodSpec} - */ - private static MethodSpec buildStringReturnMethod(String name, String returnString) { - return MethodSpec.methodBuilder(name) - .addModifiers(Modifier.PUBLIC) - .addAnnotation(Override.class) - .returns(String.class) - .addStatement("return $S", returnString) - .build(); + MethodSpec stubOverride = MethodSpec.overriding(toGenerate.get(injName).targetStub) + .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(String.format("super.%s(clazz, main)", toGenerate.get(injName).injector.getSimpleName()), 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", targetClassFQN)) + .addMethod(buildStringReturnMethod("methodName", targetMethodName)) + .addMethod(buildStringReturnMethod("methodDesc", targetMethodDescriptor)) + .addMethods(generateRequestedProxies(cl, this.mapper)) + .addMethod(stubOverride) + .addMethod(inject) + .build(); + + JavaFile javaFile = JavaFile.builder(packageName, injectorClass).build(); + String injectorClassName = String.format("%s.%s", 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); + } } /** @@ -213,71 +306,75 @@ public class LilleroProcessor extends AbstractProcessor { * @return a {@link List} of method specs * @since 0.2.0 */ - private List<MethodSpec> generateRequestedProxies(TypeElement cl, SrgMapper mapper) { + private List<MethodSpec> generateRequestedProxies(TypeElement cl, ObfuscationMapper mapper) { List<MethodSpec> generated = new ArrayList<>(); findAnnotatedMethods(cl, FindMethod.class) .stream() .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 = (ExecutableElement) findMemberFromStub(m, processingEnv); + MethodSpec.Builder b = MethodSpec.overriding(m); + + String targetParentFQN = findClassName(((TypeElement) targetMethod.getEnclosingElement()).getQualifiedName().toString(), mapper); + + b.addStatement("$T bd = $T.builder($S)", + MethodProxy.Builder.class, + MethodProxy.class, + findMemberName(targetParentFQN, targetMethod.getSimpleName().toString(), descriptorFromExecutableElement(targetMethod), mapper) + ); + + b.addStatement("bd.setParent($S)", targetParentFQN); + + for(Modifier mod : targetMethod.getModifiers()) + b.addStatement("bd.addModifier($L)", mapModifier(mod)); + + for(VariableElement p : targetMethod.getParameters()) { + if(p.asType().getKind().isPrimitive()) + b.addStatement("bd.addParameter($T.class)", p.asType()); + else b.addStatement("bd.addParameter($S, $L)", getInnermostComponentType(p.asType()), getArrayLevel(p.asType())); } + + if(targetMethod.getReturnType().getKind().isPrimitive()) + b.addStatement("bd.setReturnType($T.class)", targetMethod.getReturnType()); + else b.addStatement("bd.setReturnType($S, $L)", getInnermostComponentType(targetMethod.getReturnType()), getArrayLevel(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 = (VariableElement) findMemberFromStub(m, processingEnv); + MethodSpec.Builder b = MethodSpec.overriding(m); + + String targetParentFQN = findClassName(((TypeElement) targetField.getEnclosingElement()).getQualifiedName().toString(), mapper); + + b.addStatement("$T bd = $T.builder($S)", + FieldProxy.Builder.class, + FieldProxy.class, + findMemberName(targetParentFQN, targetField.getSimpleName().toString(), null, mapper) + ); + + b.addStatement("bd.setParent($S)", ((TypeElement) targetField.getEnclosingElement()).getQualifiedName().toString()); + + for(Modifier mod : targetField.getModifiers()) + b.addStatement("bd.addModifier($L)", mapModifier(mod)); + + if(targetField.asType().getKind().isPrimitive()) + b.addStatement("bd.setType($T.class)", targetField.asType()); + else b.addStatement("bd.setType($S, $L)", getInnermostComponentType(targetField.asType()), getArrayLevel(targetField.asType())); + + b.addStatement("return bd.build()"); + + generated.add(b.build()); }); return generated; } - /** * Generates the Service Provider file for the generated injectors. */ @@ -294,4 +391,36 @@ public class LilleroProcessor extends AbstractProcessor { throw new RuntimeException(e); } } + + /** + * Container for information about a class that is to be generated. + * Only used internally. + */ + private class InjectorInfo { + /** + * The {@link ExecutableElement} corresponding to the injector method. + */ + public final ExecutableElement injector; + + /** + * The {@link ExecutableElement} corresponding to the target method stub. + */ + public final ExecutableElement targetStub; + + /** + * The {@link ExecutableElement} corresponding to the target method. + */ + private final ExecutableElement target; + + /** + * Public constructor. + * @param injector the injector {@link ExecutableElement} + * @param targetStub the target {@link ExecutableElement} + */ + public InjectorInfo(ExecutableElement injector, ExecutableElement targetStub) { + this.injector = injector; + this.targetStub = targetStub; + this.target = (ExecutableElement) findMemberFromStub(targetStub, processingEnv); + } + } }
\ 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..40de173 100644 --- a/src/main/java/ftbsc/lll/processor/annotations/FindField.java +++ b/src/main/java/ftbsc/lll/processor/annotations/FindField.java @@ -16,6 +16,16 @@ import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.CLASS) @java.lang.annotation.Target(ElementType.METHOD) public @interface FindField { - Class<?> parent(); + /** + * @return the {@link Class} object containing the desired field, + * or the {@link Object} class if not specified (the {@link Class} + * from {@link Patch#value()} is instead used) + */ + Class<?> parent() default Object.class; + + /** + * @return the name of the field, will default to the empty string + * (the name of the annotated method will instead be used) + */ 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..1705619 100644 --- a/src/main/java/ftbsc/lll/processor/annotations/FindMethod.java +++ b/src/main/java/ftbsc/lll/processor/annotations/FindMethod.java @@ -17,7 +17,23 @@ import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.CLASS) @java.lang.annotation.Target(ElementType.METHOD) public @interface FindMethod { - Class<?> parent(); + /** + * @return the {@link Class} object containing the desired method, + * or the {@link Object} class if not specified (the {@link Class} + * from {@link Patch#value()} is instead used) + */ + Class<?> parent() default Object.class; + + /** + * @return the name of the method, will default to the empty string + * (the name of the annotated method will instead be used) + */ String name() default ""; - Class<?>[] params(); + + /** + * @return a list of the parameters of the method, will default to empty + * array (in that case, an attempt will be made to match a method without + * args first) + */ + 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..4b74961 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; @@ -10,9 +11,24 @@ import java.lang.annotation.RetentionPolicy; * as parameters. It will be discarded otherwise. * It will also be discarded unless the containing class is annotated with {@link Patch} * and another method within the class is annotated with {@link Target}. + * This annotation may be added multiple times, in order to target multiple methods. * @see Patch * @see Target */ @Retention(RetentionPolicy.CLASS) +@Repeatable(MultipleInjectors.class) @java.lang.annotation.Target(ElementType.METHOD) -public @interface Injector {} +public @interface Injector { + /** + * @return the name of the stub annotated with {@link Target} this is referring to. + * @since 0.3.0 + */ + String targetName() default ""; + + /** + * @return the parameters of the stub annotated with {@link Target} this is referring + * to (used to discern in case of method stubs by the same name) + * @since 0.3.0 + */ + 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..8b4b3f8 --- /dev/null +++ b/src/main/java/ftbsc/lll/processor/annotations/MultipleInjectors.java @@ -0,0 +1,19 @@ +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; + +/** + * Used to support {@link Injector} as a {@link Repeatable} annotation. + * @since 0.3.0 + */ +@Retention(RetentionPolicy.CLASS) +@java.lang.annotation.Target(ElementType.METHOD) +public @interface MultipleInjectors { + /** + * @return the {@link Injector} annotations, as an array + */ + 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..dbe7cf6 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 false unless + * you 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..1735b66 --- /dev/null +++ b/src/main/java/ftbsc/lll/processor/tools/ASTUtils.java @@ -0,0 +1,299 @@ +package ftbsc.lll.processor.tools; + +import com.squareup.javapoet.*; +import ftbsc.lll.exceptions.AmbiguousDefinitionException; +import ftbsc.lll.exceptions.MappingNotFoundException; +import ftbsc.lll.exceptions.TargetNotFoundException; +import ftbsc.lll.processor.annotations.FindField; +import ftbsc.lll.processor.annotations.FindMethod; +import ftbsc.lll.processor.annotations.Patch; +import ftbsc.lll.processor.annotations.Target; +import ftbsc.lll.processor.tools.obfuscation.ObfuscationMapper; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.*; +import javax.lang.model.type.*; +import javax.lang.model.util.Elements; +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static ftbsc.lll.processor.tools.JavaPoetUtils.descriptorFromExecutableElement; +import static ftbsc.lll.processor.tools.JavaPoetUtils.methodDescriptorFromParams; + +/** + * Collection of AST-related 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<ExecutableElement> findAnnotatedMethods(TypeElement cl, Class<? extends Annotation> ann) { + return cl.getEnclosedElements() + .stream() + .filter(e -> e.getAnnotationsByType(ann).length != 0) + .map(e -> (ExecutableElement) e) + .collect(Collectors.toList()); + } + + /** + * 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; + } + } + + /** + * Calculates the array nesting level for a {@link TypeMirror}. + * @param t the type mirror to get it for + * @return the array nesting level + * @since 0.3.0 + */ + public static int getArrayLevel(TypeMirror t) { + int arrayLevel = 0; + while(t.getKind() == TypeKind.ARRAY) { + t = ((ArrayType) t).getComponentType(); + arrayLevel++; + } + return arrayLevel; + } + + /** + * Calculates the array nesting level for a {@link TypeMirror}. + * @param t the type mirror to get it for + * @return the array nesting level + * @since 0.3.0 + */ + public static TypeMirror getInnermostComponentType(TypeMirror t) { + while(t.getKind() == TypeKind.ARRAY) + t = ((ArrayType) t).getComponentType(); + return t; + } + + /** + * Safely extracts a {@link Class} from an annotation and gets its fully qualified name. + * @param ann the annotation containing the class + * @param fun the annotation function returning the class + * @return the fully qualified name of the given class + * @since 0.3.0 + */ + public static <T extends Annotation> String getClassFullyQualifiedName(T ann, Function<T, Class<?>> fun) { + try { + return fun.apply(ann).getCanonicalName(); + } catch(MirroredTypeException e) { + return e.getTypeMirror().toString(); + } + } + + /** + * Safely extracts a {@link Class} array from an annotation. + * @param ann the annotation containing the class + * @param fun the annotation function returning the class + * @param elementUtils the element utils corresponding to the {@link ProcessingEnvironment} + * @return a list of {@link TypeMirror}s representing the classes + * @since 0.3.0 + */ + public static <T extends Annotation> List<TypeMirror> classArrayFromAnnotation(T ann, Function<T, Class<?>[]> fun, Elements elementUtils) { + List<TypeMirror> params = new ArrayList<>(); + try { + params.addAll(Arrays.stream(fun.apply(ann)) + .map(Class::getCanonicalName) + .map(fqn -> elementUtils.getTypeElement(fqn).asType()) + .collect(Collectors.toList())); + } catch(MirroredTypesException e) { + params.addAll(e.getTypeMirrors()); + } + return params; + } + + + /** + * Finds the class name and maps it to the correct format. + * @param name the fully qualified name of the class to convert + * @param mapper the {@link ObfuscationMapper} to use, may be null + * @return the fully qualified class name + * @since 0.3.0 + */ + public static String findClassName(String name, ObfuscationMapper mapper) { + try { + return mapper == null ? name : mapper.obfuscateClass(name).replace('/', '.'); + } catch(MappingNotFoundException e) { + return name; + } + } + + /** + * Finds the class name and maps it to the correct format. + * @param patchAnn the {@link Patch} annotation containing target class info + * @param finderAnn an annotation containing metadata about the target, may be null + * @param parentFun the function to get the parent from the finderAnn + * @return the fully qualified class name + * @since 0.3.0 + */ + private static <T extends Annotation> String findClassName(Patch patchAnn, T finderAnn, Function<T, Class<?>> parentFun) { + String fullyQualifiedName; + if(finderAnn != null) { + fullyQualifiedName = getClassFullyQualifiedName(finderAnn, parentFun); + if(!fullyQualifiedName.equals("java.lang.Object")) + return findClassName(fullyQualifiedName, null); + } + fullyQualifiedName = getClassFullyQualifiedName(patchAnn, Patch::value); + return findClassName(fullyQualifiedName, null); + } + + /** + * Finds the member name and maps it to the correct format. + * @param parentFQN the already mapped FQN of the parent class + * @param memberName the name of the member + * @param mapper the {@link ObfuscationMapper} to use, may be null + * @return the internal class name + * @since 0.3.0 + */ + public static String findMemberName(String parentFQN, String memberName, String methodDescriptor, ObfuscationMapper mapper) { + try { + return mapper == null ? memberName : mapper.obfuscateMember(parentFQN, memberName, methodDescriptor); + } catch(MappingNotFoundException e) { + return memberName; + } + } + + /** + * Finds a method given name, container and descriptor. + * @param parentFQN 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) + * @param env the {@link ProcessingEnvironment} to perform the operation in + * @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 static ExecutableElement findMethod(String parentFQN, String name, String descr, boolean strict, ProcessingEnvironment env) { + TypeElement parent = env.getElementUtils().getTypeElement(parentFQN); + if(parent == null) + throw new AmbiguousDefinitionException(String.format("Could not find parent class %s!", parentFQN)); + + //try to find by name + List<ExecutableElement> 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(String.format("%s %s", name, descr)); + if(candidates.size() == 1 && !strict) + return candidates.get(0); + if(descr == null) { + throw new AmbiguousDefinitionException( + String.format("Found %d methods named %s in class %s!", candidates.size(), name, parentFQN) + ); + } 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(String.format("%s %s", name, descr)); + if(candidates.size() > 1) + throw new AmbiguousDefinitionException( + String.format("Found %d methods named %s in class %s!", candidates.size(), name, parentFQN) + ); + return candidates.get(0); + } + } + + /** + * Finds the real class member (field or method) corresponding to a stub annotated with + * {@link Target} or {@link FindMethod} or {@link FindField}. + * @param stub the {@link ExecutableElement} for the stub + * @param env the {@link ProcessingEnvironment} to perform the operation in + * @return the {@link Element} corresponding to the method or field + * @throws AmbiguousDefinitionException if it finds more than one candidate + * @throws TargetNotFoundException if it finds no valid candidate + * @since 0.3.0 + */ + public static Element findMemberFromStub(ExecutableElement stub, ProcessingEnvironment env) { + //the parent always has a @Patch annotation + Patch patchAnn = stub.getEnclosingElement().getAnnotation(Patch.class); + //there should ever only be one of these + Target targetAnn = stub.getAnnotation(Target.class); //if this is null strict mode is always disabled + FindMethod findMethodAnn = stub.getAnnotation(FindMethod.class); //this may be null, it means no fallback info + FindField findFieldAnn = stub.getAnnotation(FindField.class); + String parentFQN, memberName; + if(findFieldAnn == null) { //methods + parentFQN = findClassName(patchAnn, findMethodAnn, FindMethod::parent); + String methodDescriptor = + findMethodAnn != null + ? methodDescriptorFromParams(findMethodAnn, FindMethod::params, env.getElementUtils()) + : descriptorFromExecutableElement(stub); + memberName = + findMethodAnn != null && !findMethodAnn.name().equals("") + ? findMethodAnn.name() + : stub.getSimpleName().toString(); + return findMethod( + parentFQN, + memberName, + methodDescriptor, + targetAnn != null && targetAnn.strict(), + env + ); + } else { //fields + parentFQN = findClassName(patchAnn, findFieldAnn, FindField::parent); + memberName = findFieldAnn.name().equals("") + ? stub.getSimpleName().toString() + : findFieldAnn.name(); + TypeElement parent = env.getElementUtils().getTypeElement(parentFQN); + List<VariableElement> candidates = + parent.getEnclosedElements() + .stream() + .filter(f -> f instanceof VariableElement) + .filter(f -> f.getSimpleName().contentEquals(memberName)) + .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 + } + } +} diff --git a/src/main/java/ftbsc/lll/processor/tools/JavaPoetUtils.java b/src/main/java/ftbsc/lll/processor/tools/JavaPoetUtils.java new file mode 100644 index 0000000..18f9ed6 --- /dev/null +++ b/src/main/java/ftbsc/lll/processor/tools/JavaPoetUtils.java @@ -0,0 +1,110 @@ +package ftbsc.lll.processor.tools; + +import com.squareup.javapoet.*; +import ftbsc.lll.tools.DescriptorBuilder; + +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Elements; +import java.lang.annotation.Annotation; +import java.util.List; +import java.util.function.Function; + +import static ftbsc.lll.processor.tools.ASTUtils.classArrayFromAnnotation; + +/** + * Collection of static utils that rely on JavaPoet to function. + */ +public class JavaPoetUtils { + /** + * Builds a {@link MethodSpec} for a public method whose body simply returns a {@link String}. + * @param name the name of the method + * @param returnString the {@link String} to return + * @return the built {@link MethodSpec} + */ + public static MethodSpec buildStringReturnMethod(String name, String returnString) { + return MethodSpec.methodBuilder(name) + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .returns(String.class) + .addStatement("return $S", returnString) + .build(); + } + + /** + * 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 || type instanceof ParameterizedTypeName) { + ClassName var = type instanceof ParameterizedTypeName ? ((ParameterizedTypeName) type).rawType : (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(); + } + + /** + * Builds a (partial, not including the return type) method descriptor from its parameters + * @param ann the annotation containing the class + * @param fun the annotation function returning the class + * @return the method descriptor + */ + public static <T extends Annotation> String methodDescriptorFromParams(T ann, Function<T, Class<?>[]> fun, Elements elementUtils) { + List<TypeMirror> mirrors = classArrayFromAnnotation(ann, fun, elementUtils); + StringBuilder sb = new StringBuilder("("); + for(TypeMirror t : mirrors) + sb.append(descriptorFromType(t)); + sb.append(")"); + return sb.toString(); + } +} diff --git a/src/main/java/ftbsc/lll/processor/tools/obfuscation/ObfuscationMapper.java b/src/main/java/ftbsc/lll/processor/tools/obfuscation/ObfuscationMapper.java new file mode 100644 index 0000000..ad4ced3 --- /dev/null +++ b/src/main/java/ftbsc/lll/processor/tools/obfuscation/ObfuscationMapper.java @@ -0,0 +1,200 @@ +package ftbsc.lll.processor.tools.obfuscation; + +import ftbsc.lll.exceptions.AmbiguousDefinitionException; +import ftbsc.lll.exceptions.MappingNotFoundException; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Parses a .tsrg file into a mapper capable of converting from + * deobfuscated names to obfuscated ones. + * 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. + * TODO: CSV format + * @since 0.2.0 + */ +public class ObfuscationMapper { + + /** + * A Map using the deobfuscated names as keys, + * holding information for that class as value. + */ + private final Map<String, ObfuscationData> 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 ObfuscationMapper(Stream<String> str) { + AtomicReference<String> currentClass = new AtomicReference<>(""); + str.forEach(l -> { + if(l.startsWith("\t")) + mapper.get(currentClass.get()).addMember(l); + else { + String[] sp = l.split(" "); + ObfuscationData s = new ObfuscationData(sp[0], sp[1]); + currentClass.set(s.unobf); + mapper.put(s.unobf, s); + } + }); + } + + /** + * Gets the obfuscated name of the class. + * @param name the unobfuscated internal name of the desired class + * @return the obfuscated name of the class + * @throws MappingNotFoundException if no mapping is found + */ + public String obfuscateClass(String name) { + ObfuscationData data = mapper.get(name.replace('.', '/')); + if(data == null) + throw new MappingNotFoundException(name); + else return data.obf; + } + + /** + * Gets the unobfuscated name of the class. + * Due to how it's implemented, it's considerably less efficient than its + * opposite operation. + * @param obfName the obfuscated internal name of the desired class + * @return the deobfuscated name of the class + */ + public String deobfuscateClass(String obfName) { + ObfuscationData data = getObfuscationData(obfName); + return data.unobf; + } + + /** + * Gets the 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 parentName the unobfuscated internal name of the parent class + * @param memberName the field name or method signature + * @param methodDescriptor the optional descriptor of the member, may be null or partial + * @return the obfuscated name of the given member + * @throws MappingNotFoundException if no mapping is found + */ + public String obfuscateMember(String parentName, String memberName, String methodDescriptor) { + ObfuscationData data = mapper.get(parentName.replace('.', '/')); + if(data == null) + throw new MappingNotFoundException(parentName + "::" + memberName); + String member = data.get(memberName, methodDescriptor); + if(member == null) + throw new MappingNotFoundException(parentName + "::" + memberName); + return member; + } + + /** + * Gets the unobfuscated name of the given member. + * Due to how it's implemented, it's considerably less efficient than its + * opposite operation. + * @param parentObf the obfuscated internal name of the container class + * @param memberObf the field name or method signature + * @return the deobfuscated name of the given member + */ + public String deobfuscateMember(String parentObf, String memberObf) { + ObfuscationData data = getObfuscationData(parentObf); + for(String unobf : data.members.keySet()) + if(data.members.get(unobf).equals(memberObf)) + return unobf; + return null; + } + + /** + * Used internally. Gets the obfuscation data corresponding to the given obfuscated class name. + * @param classObfuscatedName the internal name of the obfuscated class + * @return the desired {@link ObfuscationData} object + * @throws MappingNotFoundException if no {@link ObfuscationData} object is found + */ + private ObfuscationData getObfuscationData(String classObfuscatedName) { + for(ObfuscationData s : mapper.values()) + if(s.obf.equals(classObfuscatedName)) + return s; + throw new MappingNotFoundException(classObfuscatedName); + } + + /** + * 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 unobfuscated name (FQN with '/' instad of '.') of the class. + */ + private final String unobf; + + /** + * The obfuscated internal name (FQN with '/' instad of '.') of the class. + */ + private final String obf; + + /** + * A {@link Map} tying each member's name or signature to its + * obfuscated counterpart. + */ + private final Map<String, String> members; + + /** + * The constructor. It takes in the names (obfuscated and non-obfuscated) + * of a class. + * @param unobf the unobfuscated name + * @param obf the obfuscated name + */ + private ObfuscationData(String unobf, String obf) { + this.unobf = unobf; + this.obf = obf; + this.members = new HashMap<>(); + } + + /** + * Adds a member to the target class. + * For fields only the names are required; for methods, + * this takes in the full signature ({@code name + " " + space}). + * @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]); + } + + /** + * Gets an obfuscated member given the method name and a method descriptor, + * which may be partial (i.e. not include return type) or null if the member + * is not a method. + * @param memberName member name + * @param methodDescriptor the method descriptor, or null if it's not a method + * @return the requested obfuscated name, or null if nothing was found + * @throws AmbiguousDefinitionException if not enough data was given to uniquely identify a mapping + */ + public String get(String memberName, String methodDescriptor) { + if(methodDescriptor == null) + return members.get(memberName); + List<String> candidates = members.keySet().stream().filter(m -> m.startsWith(memberName)).collect(Collectors.toList()); + if(candidates.size() == 1) + return members.get(candidates.get(0)); + String signature = memberName + " " + methodDescriptor; + candidates = candidates.stream().filter(m -> m.startsWith(signature)).collect(Collectors.toList()); + switch(candidates.size()) { + case 0: + return null; + case 1: + return members.get(candidates.get(0)); + default: + throw new AmbiguousDefinitionException("Mapper could not uniquely identify method " + unobf + "::" + memberName); + } + } + } +} |