mirror of
https://github.com/zaaarf/lillero-processor.git
synced 2024-11-14 05:49:19 +01:00
Merge branch 'version3' into dev
This commit is contained in:
commit
344e66061b
15 changed files with 1052 additions and 279 deletions
|
@ -20,7 +20,7 @@ repositories {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'com.squareup:javapoet:1.13.0'
|
implementation 'com.squareup:javapoet:1.13.0'
|
||||||
implementation 'ftbsc:lll:0.3.0'
|
implementation 'ftbsc:lll:0.3.4'
|
||||||
}
|
}
|
||||||
|
|
||||||
jar {
|
jar {
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,33 +2,32 @@ package ftbsc.lll.processor;
|
||||||
|
|
||||||
import com.squareup.javapoet.*;
|
import com.squareup.javapoet.*;
|
||||||
import ftbsc.lll.IInjector;
|
import ftbsc.lll.IInjector;
|
||||||
|
import ftbsc.lll.exceptions.AmbiguousDefinitionException;
|
||||||
|
import ftbsc.lll.exceptions.InvalidResourceException;
|
||||||
import ftbsc.lll.processor.annotations.*;
|
import ftbsc.lll.processor.annotations.*;
|
||||||
|
import ftbsc.lll.processor.tools.obfuscation.ObfuscationMapper;
|
||||||
import ftbsc.lll.proxies.FieldProxy;
|
import ftbsc.lll.proxies.FieldProxy;
|
||||||
import ftbsc.lll.proxies.MethodProxy;
|
import ftbsc.lll.proxies.MethodProxy;
|
||||||
import ftbsc.lll.tools.SrgMapper;
|
|
||||||
|
|
||||||
import javax.annotation.processing.*;
|
import javax.annotation.processing.*;
|
||||||
import javax.lang.model.SourceVersion;
|
import javax.lang.model.SourceVersion;
|
||||||
import javax.lang.model.element.*;
|
import javax.lang.model.element.*;
|
||||||
import javax.lang.model.type.ExecutableType;
|
import javax.lang.model.type.ExecutableType;
|
||||||
import javax.lang.model.type.MirroredTypeException;
|
|
||||||
import javax.lang.model.type.TypeMirror;
|
import javax.lang.model.type.TypeMirror;
|
||||||
import javax.tools.Diagnostic;
|
import javax.tools.Diagnostic;
|
||||||
import javax.tools.FileObject;
|
import javax.tools.FileObject;
|
||||||
import javax.tools.JavaFileObject;
|
import javax.tools.JavaFileObject;
|
||||||
import javax.tools.StandardLocation;
|
import javax.tools.StandardLocation;
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.lang.reflect.Field;
|
import java.net.URI;
|
||||||
import java.lang.reflect.Method;
|
import java.net.URISyntaxException;
|
||||||
import java.net.URL;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
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.
|
* The actual annotation processor behind the magic.
|
||||||
|
@ -36,6 +35,7 @@ import static ftbsc.lll.processor.ASTUtils.*;
|
||||||
*/
|
*/
|
||||||
@SupportedAnnotationTypes("ftbsc.lll.processor.annotations.Patch")
|
@SupportedAnnotationTypes("ftbsc.lll.processor.annotations.Patch")
|
||||||
@SupportedSourceVersion(SourceVersion.RELEASE_8)
|
@SupportedSourceVersion(SourceVersion.RELEASE_8)
|
||||||
|
@SupportedOptions("mappingsFile")
|
||||||
public class LilleroProcessor extends AbstractProcessor {
|
public class LilleroProcessor extends AbstractProcessor {
|
||||||
/**
|
/**
|
||||||
* A {@link Set} of {@link String}s that will contain the fully qualified names
|
* A {@link Set} of {@link String}s that will contain the fully qualified names
|
||||||
|
@ -43,6 +43,50 @@ public class LilleroProcessor extends AbstractProcessor {
|
||||||
*/
|
*/
|
||||||
private final Set<String> generatedInjectors = new HashSet<>();
|
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.
|
* Where the actual processing happens.
|
||||||
* It filters through whatever annotated class it's fed, and checks whether it contains
|
* It filters through whatever annotated class it's fed, and checks whether it contains
|
||||||
|
@ -56,7 +100,7 @@ public class LilleroProcessor extends AbstractProcessor {
|
||||||
@Override
|
@Override
|
||||||
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
|
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
|
||||||
for (TypeElement annotation : annotations) {
|
for (TypeElement annotation : annotations) {
|
||||||
if(annotation.getQualifiedName().toString().equals(Patch.class.getName())) {
|
if(annotation.getQualifiedName().contentEquals(Patch.class.getName())) {
|
||||||
Set<TypeElement> validInjectors =
|
Set<TypeElement> validInjectors =
|
||||||
roundEnv.getElementsAnnotatedWith(annotation)
|
roundEnv.getElementsAnnotatedWith(annotation)
|
||||||
.stream()
|
.stream()
|
||||||
|
@ -64,7 +108,7 @@ public class LilleroProcessor extends AbstractProcessor {
|
||||||
.filter(this::isValidInjector)
|
.filter(this::isValidInjector)
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
if(!validInjectors.isEmpty()) {
|
if(!validInjectors.isEmpty()) {
|
||||||
validInjectors.forEach(this::generateInjector);
|
validInjectors.forEach(this::generateInjectors);
|
||||||
if (!this.generatedInjectors.isEmpty()) {
|
if (!this.generatedInjectors.isEmpty()) {
|
||||||
generateServiceProvider();
|
generateServiceProvider();
|
||||||
return true;
|
return true;
|
||||||
|
@ -96,114 +140,163 @@ public class LilleroProcessor extends AbstractProcessor {
|
||||||
})) return true;
|
})) return true;
|
||||||
else {
|
else {
|
||||||
processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,
|
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;
|
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.
|
* Basically implements the {@link IInjector} interface for you.
|
||||||
* @param cl the {@link TypeElement} for the given class
|
* @param cl the {@link TypeElement} for the given class
|
||||||
*/
|
*/
|
||||||
private void generateInjector(TypeElement cl) {
|
private void generateInjectors(TypeElement cl) {
|
||||||
SrgMapper mapper;
|
//find class information
|
||||||
try { //TODO: cant we get it from local?
|
Patch patchAnn = cl.getAnnotation(Patch.class);
|
||||||
URL url = new URL("https://data.fantabos.co/output.tsrg");
|
String targetClassFQN =
|
||||||
InputStream is = url.openStream();
|
findClassName(getClassFullyQualifiedName(patchAnn, Patch::value), this.mapper)
|
||||||
mapper = new SrgMapper(new BufferedReader(new InputStreamReader(is,
|
.replace('/', '.');
|
||||||
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
|
|
||||||
|
|
||||||
|
//find package information
|
||||||
Element packageElement = cl.getEnclosingElement();
|
Element packageElement = cl.getEnclosingElement();
|
||||||
while (packageElement.getKind() != ElementKind.PACKAGE)
|
while (packageElement.getKind() != ElementKind.PACKAGE)
|
||||||
packageElement = packageElement.getEnclosingElement();
|
packageElement = packageElement.getEnclosingElement();
|
||||||
|
|
||||||
String packageName = packageElement.toString();
|
String packageName = packageElement.toString();
|
||||||
String injectorSimpleClassName = cl.getSimpleName().toString() + "Injector";
|
|
||||||
String injectorClassName = packageName + "." + injectorSimpleClassName;
|
|
||||||
|
|
||||||
MethodSpec stubOverride = MethodSpec.overriding(targetMethod)
|
//find injector(s) and target(s)
|
||||||
.addStatement("throw new $T($S)", RuntimeException.class, "This is a stub and should not have been called")
|
List<ExecutableElement> injectors = findAnnotatedMethods(cl, Injector.class);
|
||||||
.build();
|
|
||||||
|
|
||||||
MethodSpec inject = MethodSpec.methodBuilder("inject")
|
List<ExecutableElement> targets = findAnnotatedMethods(cl, Target.class);
|
||||||
.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)
|
//declare it once for efficiency
|
||||||
.addModifiers(Modifier.PUBLIC)
|
List<String> targetNames =
|
||||||
.superclass(cl.asType())
|
targets.stream()
|
||||||
.addSuperinterface(ClassName.get(IInjector.class))
|
.map(ExecutableElement::getSimpleName)
|
||||||
.addMethod(buildStringReturnMethod("name", cl.getSimpleName().toString()))
|
.map(Object::toString)
|
||||||
.addMethod(buildStringReturnMethod("reason", ann.reason()))
|
.collect(Collectors.toList());
|
||||||
.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();
|
//this will contain the classes to generate: the key is the class name
|
||||||
|
Map<String, InjectorInfo> toGenerate = new HashMap<>();
|
||||||
|
|
||||||
try {
|
for(ExecutableElement inj : injectors) {
|
||||||
JavaFileObject injectorFile = processingEnv.getFiler().createSourceFile(injectorClassName);
|
Injector[] minjAnn = inj.getAnnotationsByType(Injector.class);
|
||||||
PrintWriter out = new PrintWriter(injectorFile.openWriter());
|
int iterationNumber = 1;
|
||||||
javaFile.writeTo(out);
|
for(Injector injectorAnn : minjAnn) { //java is dumb
|
||||||
out.close();
|
List<ExecutableElement> injectionCandidates = targets;
|
||||||
} catch(IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
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);
|
||||||
|
|
||||||
/**
|
MethodSpec stubOverride = MethodSpec.overriding(toGenerate.get(injName).targetStub)
|
||||||
* Builds a {@link MethodSpec} for a public method whose body simply returns a {@link String}.
|
.addStatement("throw new $T($S)", RuntimeException.class, "This is a stub and should not have been called")
|
||||||
* @param name the name of the method
|
.build();
|
||||||
* @param returnString the {@link String} to return
|
|
||||||
* @return the built {@link MethodSpec}
|
MethodSpec inject = MethodSpec.methodBuilder("inject")
|
||||||
*/
|
.addModifiers(Modifier.PUBLIC)
|
||||||
private static MethodSpec buildStringReturnMethod(String name, String returnString) {
|
.returns(void.class)
|
||||||
return MethodSpec.methodBuilder(name)
|
.addAnnotation(Override.class)
|
||||||
.addModifiers(Modifier.PUBLIC)
|
.addParameter(ParameterSpec.builder(
|
||||||
.addAnnotation(Override.class)
|
TypeName.get(processingEnv
|
||||||
.returns(String.class)
|
.getElementUtils()
|
||||||
.addStatement("return $S", returnString)
|
.getTypeElement("org.objectweb.asm.tree.ClassNode").asType()), "clazz").build())
|
||||||
.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
|
* @return a {@link List} of method specs
|
||||||
* @since 0.2.0
|
* @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<>();
|
List<MethodSpec> generated = new ArrayList<>();
|
||||||
findAnnotatedMethods(cl, FindMethod.class)
|
findAnnotatedMethods(cl, FindMethod.class)
|
||||||
.stream()
|
.stream()
|
||||||
.filter(m -> !m.getModifiers().contains(Modifier.STATIC)) //skip static stuff as we can't override it
|
.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
|
.filter(m -> !m.getModifiers().contains(Modifier.FINAL)) //in case someone is trying to be funny
|
||||||
.forEach(m -> {
|
.forEach(m -> {
|
||||||
FindMethod ann = m.getAnnotation(FindMethod.class);
|
ExecutableElement targetMethod = (ExecutableElement) findMemberFromStub(m, processingEnv);
|
||||||
String targetMethodName = ann.name().equals("") ? m.getSimpleName().toString() : ann.name();
|
MethodSpec.Builder b = MethodSpec.overriding(m);
|
||||||
try {
|
|
||||||
MethodSpec.Builder b = MethodSpec.overriding(m);
|
String targetParentFQN = findClassName(((TypeElement) targetMethod.getEnclosingElement()).getQualifiedName().toString(), mapper);
|
||||||
Method targetMethod = ann.parent().getMethod(
|
|
||||||
targetMethodName,
|
b.addStatement("$T bd = $T.builder($S)",
|
||||||
ann.params()
|
MethodProxy.Builder.class,
|
||||||
);
|
MethodProxy.class,
|
||||||
b.addStatement("$T bd = $T.builder($S)",
|
findMemberName(targetParentFQN, targetMethod.getSimpleName().toString(), descriptorFromExecutableElement(targetMethod), mapper)
|
||||||
MethodProxy.Builder.class,
|
);
|
||||||
MethodProxy.class,
|
|
||||||
targetMethodName
|
b.addStatement("bd.setParent($S)", targetParentFQN);
|
||||||
);
|
|
||||||
b.addStatement("bd.setParent($S)", targetMethod.getDeclaringClass().getCanonicalName());
|
for(Modifier mod : targetMethod.getModifiers())
|
||||||
b.addStatement("bd.setModifier($L)", targetMethod.getModifiers());
|
b.addStatement("bd.addModifier($L)", mapModifier(mod));
|
||||||
for(Class<?> p : targetMethod.getParameterTypes())
|
|
||||||
b.addStatement("bd.addParameter($T.class)", p);
|
for(VariableElement p : targetMethod.getParameters()) {
|
||||||
b.addStatement("bd.setReturnType($T.class)", targetMethod.getReturnType());
|
if(p.asType().getKind().isPrimitive())
|
||||||
b.addStatement("return bd.build()");
|
b.addStatement("bd.addParameter($T.class)", p.asType());
|
||||||
generated.add(b.build());
|
else b.addStatement("bd.addParameter($S, $L)", getInnermostComponentType(p.asType()), getArrayLevel(p.asType()));
|
||||||
} catch(NoSuchMethodException e) {
|
|
||||||
processingEnv.getMessager().printMessage(
|
|
||||||
Diagnostic.Kind.ERROR,
|
|
||||||
"Method not found: " + targetMethodName
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
findAnnotatedMethods(cl, FindField.class)
|
||||||
.stream()
|
.stream()
|
||||||
.filter(m -> !m.getModifiers().contains(Modifier.STATIC))
|
.filter(m -> !m.getModifiers().contains(Modifier.STATIC))
|
||||||
.filter(m -> !m.getModifiers().contains(Modifier.FINAL))
|
.filter(m -> !m.getModifiers().contains(Modifier.FINAL))
|
||||||
.forEach(m -> {
|
.forEach(m -> {
|
||||||
FindField ann = m.getAnnotation(FindField.class);
|
VariableElement targetField = (VariableElement) findMemberFromStub(m, processingEnv);
|
||||||
String targetFieldName = ann.name().equals("") ? m.getSimpleName().toString() : ann.name();
|
MethodSpec.Builder b = MethodSpec.overriding(m);
|
||||||
try {
|
|
||||||
MethodSpec.Builder b = MethodSpec.overriding(m);
|
String targetParentFQN = findClassName(((TypeElement) targetField.getEnclosingElement()).getQualifiedName().toString(), mapper);
|
||||||
Field targetField = ann.parent().getField(targetFieldName);
|
|
||||||
b.addStatement("$T bd = $T.builder($S)",
|
b.addStatement("$T bd = $T.builder($S)",
|
||||||
FieldProxy.Builder.class,
|
FieldProxy.Builder.class,
|
||||||
FieldProxy.class,
|
FieldProxy.class,
|
||||||
targetFieldName
|
findMemberName(targetParentFQN, targetField.getSimpleName().toString(), null, mapper)
|
||||||
);
|
);
|
||||||
b.addStatement("bd.setParent($S)", targetField.getDeclaringClass().getCanonicalName());
|
|
||||||
b.addStatement("bd.setModifier($L)", targetField.getModifiers());
|
b.addStatement("bd.setParent($S)", ((TypeElement) targetField.getEnclosingElement()).getQualifiedName().toString());
|
||||||
b.addStatement("bd.setType($T.class)", targetField.getType());
|
|
||||||
b.addStatement("return bd.build()");
|
for(Modifier mod : targetField.getModifiers())
|
||||||
generated.add(b.build());
|
b.addStatement("bd.addModifier($L)", mapModifier(mod));
|
||||||
} catch(NoSuchFieldException e) {
|
|
||||||
processingEnv.getMessager().printMessage(
|
if(targetField.asType().getKind().isPrimitive())
|
||||||
Diagnostic.Kind.ERROR,
|
b.addStatement("bd.setType($T.class)", targetField.asType());
|
||||||
"Field not found: " + targetFieldName + " in class " + ann.parent().getCanonicalName()
|
else b.addStatement("bd.setType($S, $L)", getInnermostComponentType(targetField.asType()), getArrayLevel(targetField.asType()));
|
||||||
);
|
|
||||||
}
|
b.addStatement("return bd.build()");
|
||||||
|
|
||||||
|
generated.add(b.build());
|
||||||
});
|
});
|
||||||
return generated;
|
return generated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates the Service Provider file for the generated injectors.
|
* Generates the Service Provider file for the generated injectors.
|
||||||
*/
|
*/
|
||||||
|
@ -294,4 +391,36 @@ public class LilleroProcessor extends AbstractProcessor {
|
||||||
throw new RuntimeException(e);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -16,6 +16,16 @@ import java.lang.annotation.RetentionPolicy;
|
||||||
@Retention(RetentionPolicy.CLASS)
|
@Retention(RetentionPolicy.CLASS)
|
||||||
@java.lang.annotation.Target(ElementType.METHOD)
|
@java.lang.annotation.Target(ElementType.METHOD)
|
||||||
public @interface FindField {
|
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 "";
|
String name() default "";
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,23 @@ import java.lang.annotation.RetentionPolicy;
|
||||||
@Retention(RetentionPolicy.CLASS)
|
@Retention(RetentionPolicy.CLASS)
|
||||||
@java.lang.annotation.Target(ElementType.METHOD)
|
@java.lang.annotation.Target(ElementType.METHOD)
|
||||||
public @interface FindMethod {
|
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 "";
|
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 {};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package ftbsc.lll.processor.annotations;
|
package ftbsc.lll.processor.annotations;
|
||||||
|
|
||||||
import java.lang.annotation.ElementType;
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Repeatable;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
|
||||||
|
@ -10,9 +11,24 @@ import java.lang.annotation.RetentionPolicy;
|
||||||
* as parameters. It will be discarded otherwise.
|
* as parameters. It will be discarded otherwise.
|
||||||
* It will also be discarded unless the containing class is annotated with {@link Patch}
|
* 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}.
|
* 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 Patch
|
||||||
* @see Target
|
* @see Target
|
||||||
*/
|
*/
|
||||||
@Retention(RetentionPolicy.CLASS)
|
@Retention(RetentionPolicy.CLASS)
|
||||||
|
@Repeatable(MultipleInjectors.class)
|
||||||
@java.lang.annotation.Target(ElementType.METHOD)
|
@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 {};
|
||||||
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
|
@ -15,4 +15,15 @@ import java.lang.annotation.RetentionPolicy;
|
||||||
*/
|
*/
|
||||||
@Retention(RetentionPolicy.CLASS)
|
@Retention(RetentionPolicy.CLASS)
|
||||||
@java.lang.annotation.Target(ElementType.METHOD)
|
@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;
|
||||||
|
}
|
||||||
|
|
299
src/main/java/ftbsc/lll/processor/tools/ASTUtils.java
Normal file
299
src/main/java/ftbsc/lll/processor/tools/ASTUtils.java
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
110
src/main/java/ftbsc/lll/processor/tools/JavaPoetUtils.java
Normal file
110
src/main/java/ftbsc/lll/processor/tools/JavaPoetUtils.java
Normal file
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue