feat: implemented abstraction logic

This commit is contained in:
zaaarf 2023-08-26 18:09:36 +02:00
parent c3cc90d774
commit 7898882bc5
No known key found for this signature in database
GPG key ID: 6445A5CD15E5B40C
12 changed files with 567 additions and 185 deletions

View file

@ -0,0 +1,23 @@
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 no
* mapper was able to read it.
*/
public InvalidResourceException() {
super("The given resource was not claimed by any mapper!");
}
/**
* 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));
}
}

View file

@ -0,0 +1,7 @@
package ftbsc.lll.exceptions;
public class MalformedMappingsException extends Exception {
public MalformedMappingsException(String mapping, String type) {
super(String.format("Unexpected token at line %s for mapper type %s!", mapping, type));
}
}

View file

@ -9,19 +9,10 @@ public class MappingNotFoundException extends RuntimeException {
/**
* Constructs a new mapping not found exception for the specified mapping.
* @param type the type of 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);
public MappingNotFoundException(String type, String mapping) {
super(String.format("Could not find mapping for %s %s!", type, mapping));
}
}

View file

@ -0,0 +1,142 @@
package ftbsc.lll.mapper;
import ftbsc.lll.exceptions.MalformedMappingsException;
import ftbsc.lll.exceptions.MappingNotFoundException;
import ftbsc.lll.mapper.tools.data.ClassData;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A default implementation of {@link IMapper} meant to
* recycle as much code as possible.
*/
public abstract class AbstractMapper implements IMapper {
/**
* A {@link Map} tying each plain class name to its class data.
*/
protected final Map<String, ClassData> mappings = new HashMap<>();
/**
* A {@link Map} tying each obfuscated name to its class data.
*/
protected final Map<String, ClassData> mappingsInverted = new HashMap<>();
/**
* Populates the {@link IMapper} given the lines, ignoring errors depending on the
* given ignoreErrors flag.
* @param lines the lines to read
* @param ignoreErrors try to ignore errors and keep going
* @throws MalformedMappingsException if an error is encountered and ignoreErrors is false
*/
@Override
public void populate(List<String> lines, boolean ignoreErrors) throws MalformedMappingsException {
this.processLines(lines, ignoreErrors);
this.mappings.forEach((name, data) -> {
ClassData reverse = data.generateReverseMappings(this);
this.mappingsInverted.put(data.nameMapped, reverse);
});
}
/**
* Reads the given lines of text and attempts to interpret them as
* mappings of the given type.
* @param lines the lines to read
* @param ignoreErrors try to ignore errors and keep going
* @throws MalformedMappingsException if an error is encountered and ignoreErrors is false
*/
protected abstract void processLines(List<String> lines, boolean ignoreErrors) throws MalformedMappingsException;
/**
* Completely resets the mapper, clearing it of all existing mappings.
*/
@Override
public void reset() {
this.mappings.clear();
this.mappingsInverted.clear();
}
/**
* Gets a name of a class from the given {@link Map}.
* @param name the name
* @param mappings the {@link Map} to pull data from
* @return the mapped name
* @throws MappingNotFoundException if no mapping is found
*/
private static String mapClass(String name, Map<String, ClassData> mappings) {
ClassData data = mappings.get(name.replace('.', '/'));
if(data == null)
throw new MappingNotFoundException("class", name);
else return data.nameMapped;
}
/**
* Gets the obfuscated name of the class.
* @param name the plain internal name of the desired class
* @return the obfuscated name of the class
* @throws MappingNotFoundException if no mapping is found
*/
@Override
public String obfuscateClass(String name) {
return mapClass(name, this.mappings);
}
/**
* Gets the plain name of the class.
* @param nameObf the obfuscated internal name of the desired class
* @return the plain name of the class
* @throws MappingNotFoundException if no mapping is found
*/
@Override
public String deobfuscateClass(String nameObf) throws MappingNotFoundException {
return mapClass(nameObf, this.mappingsInverted);
}
/**
* Gets the name of a member from the given {@link Map}.
* @param parentName the parent class
* @param mappings the {@link Map} to pull data from
* @param memberName the field or method name
* @param methodDescriptor the method descriptor, may be null or partial
* @return the mapped member name
* @throws MappingNotFoundException if no mapping is found
*/
private static String mapMember(String parentName, Map<String, ClassData> mappings,
String memberName, String methodDescriptor) {
ClassData data = mappings.get(parentName.replace('.', '/'));
if(data == null)
throw new MappingNotFoundException("class", parentName);
if(methodDescriptor == null)
return data.mapField(memberName).name;
else return data.mapMethod(memberName, methodDescriptor).signature.name;
}
/**
* Gets the obfuscated name of a class member (field or method).
* @param parentName the unobfuscated internal name of the parent class
* @param memberName the field or method name
* @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
*/
@Override
public String obfuscateMember(String parentName, String memberName, String methodDescriptor) {
return mapMember(parentName, this.mappings, memberName, methodDescriptor);
}
/**
* Gets the plain name of a class member (field or method).
* @param parentName the obfuscated internal name of the parent class
* @param memberName the obfuscated field name or method signature
* @param methodDescriptor the obfuscated descriptor of the member (only for methods)
* @return the plain name of the given member
* @throws MappingNotFoundException if no mapping is found
*/
@Override
public String deobfuscateMember(String parentName, String memberName, String methodDescriptor) throws MappingNotFoundException {
return mapMember(parentName, this.mappingsInverted, memberName, methodDescriptor);
}
}

View file

@ -1,56 +1,80 @@
package ftbsc.lll.mapper;
import ftbsc.lll.exceptions.MalformedMappingsException;
import ftbsc.lll.exceptions.MappingNotFoundException;
import java.util.HashSet;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.*;
/**
* A generic obfuscation mapper.
*/
public interface IMapper {
/**
* Checks whether this mapper can process the given lines.
* @param lines the lines to read
* @return whether this type of mapper can process these lines
*/
boolean claim(List<String> lines);
/**
* Reads the given lines of text and attempts to interpret them as
* mappings of the given type.
* @param lines the lines to read
* Defines a priority for this implementation: the higher the number,
* the higher the priority.
* This is used to resolve conflicts when multiple mappers attempt to
* {@link #claim(List) claim} a given mapping file.
* @return the priority
*/
void populate(Iterable<String> lines);
default int priority() {
return 0;
}
/**
* Populates the {@link IMapper} given the lines, ignoring errors depending on the
* given ignoreErrors flag.
* @param lines the lines to read
* @param ignoreErrors try to ignore errors and keep going
* @throws MalformedMappingsException if an error is encountered and ignoreErrors is false
*/
void populate(List<String> lines, boolean ignoreErrors) throws MalformedMappingsException;
/**
* Completely resets the mapper, clearing it of all existing mappings.
*/
void reset();
/**
* Gets the obfuscated name of the class.
* @param name the unobfuscated internal name of the desired class
* @param name the plain internal name of the desired class
* @return the obfuscated name of the class
* @throws MappingNotFoundException if no mapping is found
*/
String obfuscateClass(String name);
String obfuscateClass(String name) throws MappingNotFoundException;
/**
* Gets the plain name of the class.
* @param nameObf the obfuscated internal name of the desired class
* @return the plain name of the class
* @throws MappingNotFoundException if no mapping is found
*/
String deobfuscateClass(String nameObf) throws MappingNotFoundException;
/**
* Gets the obfuscated name of a class member (field or method).
* @param parentName the unobfuscated internal name of the parent class
* @param parentName the plain internal name of the parent class
* @param memberName the field name or method signature
* @param methodDescriptor the descriptor of the member
* @param methodDescriptor the descriptor of the member (only for methods)
* @return the obfuscated name of the given member
* @throws MappingNotFoundException if no mapping is found
*/
String obfuscateMember(String parentName, String memberName, String methodDescriptor);
String obfuscateMember(String parentName, String memberName, String methodDescriptor) throws MappingNotFoundException;
/**
* Loads all valid parsers available in the classpath (via the Java Service API),
* attempts to parse the given lines into mappings, and returns all built mappers
* that succeeded without throwing errors or ftbsc.lll.exceptions.
* @param lines the lines of the mapping file
* @return a {@link Set} of mappers that could interpret the given input
* Gets the plain name of a class member (field or method).
* @param parentName the obfuscated internal name of the parent class
* @param memberName the obfuscated field name or method signature
* @param methodDescriptor the obfuscated descriptor of the member (only for methods)
* @return the plain name of the given member
* @throws MappingNotFoundException if no mapping is found
*/
static Set<IMapper> getMappers(Iterable<String> lines) {
Set<IMapper> parsed = new HashSet<>();
for(IMapper mapper: ServiceLoader.load(IMapper.class)) {
try {
mapper.populate(lines);
parsed.add(mapper);
} catch(Throwable ignored) {}
}
return parsed;
}
String deobfuscateMember(String parentName, String memberName, String methodDescriptor) throws MappingNotFoundException;
}

View file

@ -0,0 +1,73 @@
package ftbsc.lll.mapper;
import ftbsc.lll.exceptions.InvalidResourceException;
import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
/**
* The main class of the mapper library. It loads all the
* valid {@link IMapper}s and gets information from them.
*/
public class MapperProvider {
private static MapperProvider INSTANCE = null;
private static MapperProvider getInstance() {
return INSTANCE == null ? (INSTANCE = new MapperProvider()) : INSTANCE;
}
private Set<IMapper> loadedMappers = null;
private void loadMappers() {
this.loadedMappers = new HashSet<>();
for(IMapper mapper: ServiceLoader.load(IMapper.class))
this.loadedMappers.add(mapper);
if(this.loadedMappers.isEmpty())
throw new RuntimeException("Something went wrong: no mapper types were loaded successfully!");
}
/**
* Loads all valid parsers available in the classpath (via the Java Service API),
* attempts to load the resource at given location and to populate a mapper with
* its data.
* @param data the file as a list of strings
* @return a {@link IMapper} (populating it is left to the user)
*/
public static IMapper getMapper(List<String> data) {
if(getInstance().loadedMappers == null)
getInstance().loadMappers();
return getInstance().loadedMappers.stream()
.filter(m -> m.claim(data))
.max(Comparator.comparingInt(IMapper::priority))
.orElseThrow(InvalidResourceException::new);
}
/**
* Gets a resource and parses it into a {@link List} of {@link String}s.
* @param location either a URL or a local path
* @return a {@link List} containing the lines of the resource
* @throws InvalidResourceException if provided an invalid resource
*/
public static List<String> fetchFromLocalOrRemote(String location) {
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);
try {
targetStream = new FileInputStream(f);
} catch(FileNotFoundException ex) {
throw new InvalidResourceException(location);
}
}
return new BufferedReader(new InputStreamReader(targetStream,
StandardCharsets.UTF_8)).lines().collect(Collectors.toList());
}
}

View file

@ -1,75 +1,60 @@
package ftbsc.lll.mapper.impl;
import com.google.auto.service.AutoService;
import ftbsc.lll.exceptions.MappingNotFoundException;
import ftbsc.lll.exceptions.MalformedMappingsException;
import ftbsc.lll.mapper.AbstractMapper;
import ftbsc.lll.mapper.IMapper;
import ftbsc.lll.mapper.tools.ClassData;
import ftbsc.lll.mapper.tools.data.ClassData;
import java.util.HashMap;
import java.util.Map;
import java.util.ArrayList;
import java.util.List;
/**
* Parses a .tsrg file into a mapper capable of converting from
* plain names to obfuscated ones and vice versa.
* A {@link IMapper} capable of parsing TSRG (an intermediary
* format used by Forge) files.
*/
@AutoService(IMapper.class)
public class TSRGMapper implements IMapper {
public class TSRGMapper extends AbstractMapper {
/**
* A Map containing the deobfuscated names as keys and information about
* each class as values.
* Checks whether this mapper can process the given lines.
* @param lines the lines to read
* @return whether this type of mapper can process these lines
*/
private final Map<String, ClassData> mappings = new HashMap<>();
@Override
public boolean claim(List<String> lines) {
return lines.get(0).startsWith("tsrg2 left right");
}
/**
* Reads the given lines of text and attempts to interpret them as
* mappings of the given type.
* @param lines the lines to read
* @param ignoreErrors try to ignore errors and keep going
* @throws MalformedMappingsException if an error is encountered and ignoreErrors is false
*/
@Override
public void populate(Iterable<String> lines) {
protected void processLines(List<String> lines, boolean ignoreErrors) throws MalformedMappingsException {
//skip the first line ("tsrg2 left right")
lines = new ArrayList<>(lines);
lines.remove(0);
String currentClass = "";
for(String l : lines) {
if(l == null) continue;
if(l.startsWith("\t"))
mappings.get(currentClass).addMember(l);
else {
if(l.startsWith("\t") || l.startsWith(" ")) {
String[] split = l.trim().split(" ");
if(split.length == 2) //field
this.mappings.get(currentClass).addField(split[0], split[1]);
else if (split.length == 3)//method
this.mappings.get(currentClass).addMethod(split[0], split[2], split[1]); //add child
} else {
String[] sp = l.split(" ");
ClassData s = new ClassData(sp[0], sp[1]);
currentClass = s.unobf;
mappings.put(s.unobf, s);
currentClass = s.name;
this.mappings.put(s.name, 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
*/
@Override
public String obfuscateClass(String name) {
ClassData data = mappings.get(name.replace('.', '/'));
if(data == null)
throw new MappingNotFoundException(name);
else return data.obf;
}
/**
* Gets the obfuscated name of a class member (field or method).
* @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
*/
@Override
public String obfuscateMember(String parentName, String memberName, String methodDescriptor) {
ClassData data = mappings.get(parentName.replace('.', '/'));
if(data == null)
throw new MappingNotFoundException(parentName + "::" + memberName);
return data.get(memberName, methodDescriptor);
}
}

View file

@ -1,100 +0,0 @@
package ftbsc.lll.mapper.tools;
import ftbsc.lll.exceptions.AmbiguousMappingException;
import ftbsc.lll.exceptions.MappingNotFoundException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Container class used to store information about classes.
*/
public class ClassData {
/**
* The unobfuscated name (FQN with '/' instad of '.') of the class.
*/
public final String unobf;
/**
* The obfuscated internal name (FQN with '/' instad of '.') of the class.
*/
public final String obf;
/**
* A {@link Map} tying each member's name or signature to its
* obfuscated counterpart.
*/
public 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
*/
public ClassData(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 AmbiguousMappingException if not enough data was given to uniquely identify a mapping
*/
public String get(String memberName, String methodDescriptor) {
//find all keys that start with the name
List<String> candidates = members.keySet().stream().filter(
m -> m.split(" ")[0].equals(memberName)
).collect(Collectors.toList());
if(methodDescriptor != null) {
String signature = String.format("%s %s", memberName, methodDescriptor);
candidates = candidates.stream().filter(
m -> m.equals(signature)
).collect(Collectors.toList());
}
switch(candidates.size()) {
case 0:
throw new MappingNotFoundException(String.format(
"%s.%s%s",
this.unobf,
memberName,
methodDescriptor == null ? "" : "()"
));
case 1:
return members.get(candidates.get(0));
default:
throw new AmbiguousMappingException(String.format(
"Mapper could not uniquely identify member %s.%s%s, found %d!",
this.unobf,
memberName,
methodDescriptor == null ? "" : "()",
candidates.size()
));
}
}
}

View file

@ -0,0 +1,117 @@
package ftbsc.lll.mapper.tools.data;
import ftbsc.lll.exceptions.MappingNotFoundException;
import ftbsc.lll.mapper.IMapper;
import ftbsc.lll.mapper.tools.MappingUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Container class used to store information about classes.
*/
public class ClassData {
/**
* The internal (like the fully-qualified name, but with '/' instead
* of '.') of the class.
*/
public final String name;
/**
* The mapped internal (like the fully-qualified name, but with '/'
* instead of '.') of the class.
*/
public final String nameMapped;
/**
* A {@link Map} tying each method's signature to its data class.
*/
private final Map<MethodSignature, MethodData> methods;
/**
* A {@link Map} tying each field's name to its data class.
*/
private final Map<String, FieldData> fields;
/**
* The constructor. It takes in the names (plain and mapped) of a class.
* @param name the plain name
* @param nameMapped the mapped name
*/
public ClassData(String name, String nameMapped) {
this.name = name;
this.nameMapped = nameMapped;
this.methods = new HashMap<>();
this.fields = new HashMap<>();
}
/**
* Adds a method to the target class.
* @param name the method name
* @param nameMapped the mapped method name
* @param descriptor the descriptor of the method
*/
public void addMethod(String name, String nameMapped, String descriptor) {
MethodData data = new MethodData(this, name, nameMapped, descriptor);
this.methods.put(data.signature, data);
}
/**
* Adds a field to the target class.
* @param plain the name of the field
* @param mapped the mapped name of the field
*/
public void addField(String plain, String mapped) {
this.fields.put(plain, new FieldData(this, plain, mapped));
}
/**
* Generates the reverse mappings for this class.
* Should always be called only after the given mapper has finished
* processing all classes.
* @param mapper the mapper that generated this data
*/
public ClassData generateReverseMappings(IMapper mapper) {
ClassData reverse = new ClassData(this.nameMapped, this.name);
this.methods.forEach((signature, data) -> reverse.addMethod(nameMapped, signature.name,
MappingUtils.obfuscateMethodDescriptor(signature.descriptor, mapper)));
this.fields.forEach((name, data) -> reverse.addField(data.nameMapped, name));
return reverse;
}
/**
* Gets the {@link MethodData} from its name and descriptor, which may be partial
* (i.e. not include the return type).
* @param methodName the method name
* @param methodDescriptor the method descriptor, which may be partial
* @return the requested {@link MethodData}
* @throws MappingNotFoundException if the mapping wasn't found
*/
public MethodData mapMethod(String methodName, String methodDescriptor) {
List<MethodSignature> signatures = this.methods.keySet().stream().filter(
s -> s.name.equals(methodName) && s.descriptor.startsWith(methodDescriptor)
).collect(Collectors.toList());
if(signatures.size() > 1)
throw new RuntimeException(); //should never happen unless something goes horribly wrong
else if(signatures.isEmpty())
throw new MappingNotFoundException("method",
String.format("%s::%s%s", this.name, methodName, methodDescriptor));
return this.methods.get(signatures.get(0));
}
/**
* Gets the {@link FieldData} its name.
* @param fieldName the field name
* @return the requested {@link FieldData}
* @throws MappingNotFoundException if the mapping wasn't found
*/
public FieldData mapField(String fieldName) {
FieldData data = this.fields.get(fieldName);
if(data == null)
throw new MappingNotFoundException("field", String.format("%s.%s", this.name, fieldName));
else return data;
}
}

View file

@ -0,0 +1,34 @@
package ftbsc.lll.mapper.tools.data;
/**
* Container class for method data.
*/
public class FieldData {
/**
* The internal name of the parent class.
*/
public final ClassData parentClass;
/**
* The name of the method.
*/
public final String name;
/**
* The name mapped.
*/
public final String nameMapped;
/**
* Constructs a new {@link FieldData}.
* @param parentClass the {@link ClassData} representation of the parent class
* @param name the field name
* @param nameMapped the mapped field name
*/
public FieldData(ClassData parentClass, String name, String nameMapped) {
this.parentClass = parentClass;
this.name = name;
this.nameMapped = nameMapped;
}
}

View file

@ -0,0 +1,35 @@
package ftbsc.lll.mapper.tools.data;
/**
* Container class for method data.
*/
public class MethodData {
/**
* The internal name of the parent class.
*/
public final ClassData parentClass;
/**
* The signature of the method.
*/
public final MethodSignature signature;
/**
* The mapped name of the method.
*/
final String nameMapped;
/**
* Constructs a new {@link MethodData}.
* @param parentClass the {@link ClassData} representation of the parent class
* @param name the method name
* @param nameMapped the mapped method name
* @param descriptor the method's descriptor
*/
public MethodData(ClassData parentClass, String name, String nameMapped, String descriptor) {
this.parentClass = parentClass;
this.signature = new MethodSignature(name, descriptor);
this.nameMapped = nameMapped;
}
}

View file

@ -0,0 +1,51 @@
package ftbsc.lll.mapper.tools.data;
import java.util.Objects;
/**
* Container class for method signature data.
*/
public class MethodSignature {
/**
* The name of the method.
*/
public final String name;
/**
* The descriptor of the method.
*/
public final String descriptor;
/**
* Constructs a new {@link MethodSignature}. The parameters should be
* either plain or obfuscated in the same way;
* @param name the method name
* @param descriptor the method descriptor
*/
public MethodSignature(String name, String descriptor) {
this.name = name;
this.descriptor = descriptor;
}
/**
* Checks if two {@link MethodSignature}s represent the same method.
* @param o the other signature
* @return whether they represent the same method
*/
@Override
public boolean equals(Object o) {
if(this == o) return true;
if(o == null || getClass() != o.getClass()) return false;
MethodSignature signature = (MethodSignature) o;
return Objects.equals(name, signature.name) && Objects.equals(descriptor, signature.descriptor);
}
/**
* Calculates a hash based on name and descriptor.
* @return the hash code
*/
@Override
public int hashCode() {
return Objects.hash(name, descriptor);
}
}