aboutsummaryrefslogtreecommitdiff
path: root/src/main
diff options
context:
space:
mode:
author zaaarf <zaaarf@proton.me>2023-03-18 17:24:43 +0100
committer zaaarf <zaaarf@proton.me>2023-03-18 17:24:43 +0100
commit344e66061b31a83f7ace2ab887e80f782f560297 (patch)
treeb8841cdf15a97376cc75206818a989b0e1918e36 /src/main
parent8695612c58141b1d5e0ee274027ebbd2050de6f8 (diff)
parent909f5cfa07464f35814da1686b0ac1a6c3ea03dd (diff)
Merge branch 'version3' into dev
Diffstat (limited to 'src/main')
-rw-r--r--src/main/java/ftbsc/lll/exceptions/AmbiguousDefinitionException.java25
-rw-r--r--src/main/java/ftbsc/lll/exceptions/InvalidResourceException.java22
-rw-r--r--src/main/java/ftbsc/lll/exceptions/MappingNotFoundException.java27
-rw-r--r--src/main/java/ftbsc/lll/exceptions/TargetNotFoundException.java15
-rw-r--r--src/main/java/ftbsc/lll/processor/ASTUtils.java126
-rw-r--r--src/main/java/ftbsc/lll/processor/LilleroProcessor.java431
-rw-r--r--src/main/java/ftbsc/lll/processor/annotations/FindField.java12
-rw-r--r--src/main/java/ftbsc/lll/processor/annotations/FindMethod.java20
-rw-r--r--src/main/java/ftbsc/lll/processor/annotations/Injector.java18
-rw-r--r--src/main/java/ftbsc/lll/processor/annotations/MultipleInjectors.java19
-rw-r--r--src/main/java/ftbsc/lll/processor/annotations/Target.java13
-rw-r--r--src/main/java/ftbsc/lll/processor/tools/ASTUtils.java299
-rw-r--r--src/main/java/ftbsc/lll/processor/tools/JavaPoetUtils.java110
-rw-r--r--src/main/java/ftbsc/lll/processor/tools/obfuscation/ObfuscationMapper.java200
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);
+ }
+ }
+ }
+}