package org.stianloader.remapper;
import java.util.ArrayDeque;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.BiFunction;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NonBlocking;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.MethodNode;
import org.stianloader.remapper.HierarchyAwareMappingDelegator.TopLevelMemberLookup;
// FIXME the javadocs document an edge case (colliding interface declarations) that we may want to get rid of
/**
* An implementation of {@link TopLevelMemberLookup} that works based on a statically computed map of
* {@link MemberRef} to {@link MemberRealm}. The {@link MemberRealm} can be used to obtain the realm members,
* that is the uses of a realm or the types where the member can be used in (note that in cases where two interfaces
* define the same method, the set of realm members may overlap - at this point in time there is no good approach of handling
* that edge-case besides praying it does not occur). A {@link MemberRealm} also documents the root definition of a member
* (in case of the interface edge case, this is by default the most frequently implemented interface - if both interfaces
* are implemented the same amount of times, it falls back to lexicographic ordering)
*/
public class SimpleTopLevelLookup implements TopLevelMemberLookup {
/**
* A member realm is a group of class members (so either fields or methods) with the same name
* (but not necessarily the same descriptor or signature as it is permissible for subclasses to be more/less
* strict on what they return or consume) but different classes. They form a unit (here referred as a realm)
* as they must have the same name in both source and destination namespace, otherwise the method hierarchy
* may be disjointed, resulting in mapping anomalies which may cause the application to no longer run as intended.
*
*
There may be multiple units with the same name, spanning different classes if they never intersect with each other.
* However, one currently known edge-case (technically a bug)
*/
public static class MemberRealm {
/**
* All classes (written as an {@link Type#getInternalName() internal name}) where the member
* is accessible from.
*/
@NotNull
@Unmodifiable
public final Set<@NotNull String> realmMembers;
/**
* The top level declaration of the member realm. In case where two different interfaces might define the
* same method, then the most frequently used interface is defined as the declarer.
*/
@NotNull
public final MemberRef rootDefinition;
/**
* Create a {@link MemberRealm} with the supplied {@link #rootDefinition} and {@link #realmMembers}.
*
* @param rootDefinition The top level declaration of the member realm.
* @param realmMembers The list of all classes in which the member realm is active.
*/
public MemberRealm(@NotNull MemberRef rootDefinition, @Unmodifiable @NotNull Set<@NotNull String> realmMembers) {
this.rootDefinition = rootDefinition;
this.realmMembers = realmMembers;
}
@Override
public String toString() {
return "MemberRealm[root='" + this.rootDefinition + "',members='" + this.realmMembers + "']";
}
}
/**
*
A helper method that assembles a map where for a Map with a key of A with a Set as a value, a Map
* is returned where the keys are the same but the Set also contains all values that would be addressable
* by either A or a value of the Set represented by key A (even recursively).
*
*
A small generic method I wrote since I am fed up writing such algorithms by hand all the time
* (it is remarkable how often one needs to assemble dependency trees)
*
*
Not very performant, but should do the job as a temporary band-aid fix until I feel like improving the performance
*
*
Null set values are ignored and treated like an empty set (which in turn has the same behaviour as
* there being no entry in the first place).
*
* @param The type of the key as well as the values of the Set
* @param input The input map to do the computation on. May not be modified while running the method
* @return The output map which follows the semantics described above
*/
@NotNull
@Contract(pure = true, value = "null -> fail; !null -> new")
@Unmodifiable
private static Map> assembleInvertedTree(@Unmodifiable @NotNull Map> input) {
Map> mapOut = new HashMap<>();
Queue queue = new ArrayDeque<>();
for (T key : input.keySet()) {
Set collected = new HashSet<>();
queue.addAll(input.get(key));
while (!queue.isEmpty()) {
T queued = queue.remove();
if (!collected.add(queued)) {
continue;
}
Set elements = mapOut.get(queued);
if (!Objects.isNull(elements)) {
collected.addAll(elements);
continue;
}
Set set = input.get(queued);
if (set != null) {
queue.addAll(set);
}
}
mapOut.put(key, collected);
}
return Collections.unmodifiableMap(mapOut);
}
/**
* Compute a map of class member to member realm relations from a list of ClassNodes.
*
* The classnodes within the list should include everything that is relevant to the remapping process -
* so the obfuscated application as well optionally the application that needs to be remapped (e.g. when remapping mods).
*
*
However, one currently known edge-case (technically a bug) is that when two interfaces define a method with the same
* name and descriptor, then if the method hierarchies were to intersect, then the method realms would still be treated
* separately, even though in reality they are the same realm.
*
* @param nodes The list of {@link ClassNode ClassNodes} to process. Members will only be in that list.
* @return A {@link Map} that maps {@link MemberRef member references} to their respective {@link MemberRealm}.
*/
@NotNull
@Unmodifiable
public static Map<@NotNull MemberRef, @NotNull MemberRealm> realmsOf(@Unmodifiable @NotNull List<@NotNull ClassNode> nodes) {
// FIXME Inner classes can make use of private methods and fields without an accessor in never versions of java.
// The question here is - does that also apply to overrides?
Map<@NotNull String, Set<@NotNull String>> immediateChildren = new HashMap<>();
Map<@NotNull String, ClassNode> nodeLookup = new HashMap<>();
for (ClassNode node : nodes) {
nodeLookup.put(node.name, node);
BiFunction, Set> combiner = (key, children) -> {
if (children == null) {
children = new TreeSet<>();
}
children.add(node.name);
return children;
};
immediateChildren.compute(node.superName, combiner);
for (String interfaceName : node.interfaces) {
immediateChildren.compute(interfaceName, combiner);
}
}
Map<@NotNull String, @NotNull Set<@NotNull String>> allChildren = SimpleTopLevelLookup.assembleInvertedTree(immediateChildren);
// Ensure that we go by parent classes first, then go to the respective children
TreeSet<@NotNull String> applyOrder = new TreeSet<>((e1, e0) -> {
int hiOrder = allChildren.getOrDefault(e0, Collections.emptySet()).size() - allChildren.getOrDefault(e1, Collections.emptySet()).size();
return hiOrder == 0 ? e1.compareTo(e0) : hiOrder;
});
// Ensure only obfuscated nodes are applied (e.g. ignore java/lang/Object).
// This assumes that users won't try to map deobfuscated names, but that could be handled separately in the future
applyOrder.addAll(nodeLookup.keySet());
Map<@NotNull MemberRef, @NotNull MemberRealm> realms = new HashMap<>();
for (String superType : applyOrder) {
// Note: non-obfuscated classes won't be present here, nor will they be in the set of children classes
ClassNode superNode = nodeLookup.get(superType);
for (MethodNode superMethod : superNode.methods) {
MemberRef myLoc = new MemberRef(superType, superMethod.name, superMethod.desc);
if (realms.containsKey(myLoc)) {
// Someone (likely a supertype) already added the entry - safe to assume it is being overwritten by this class
continue;
}
if ((superMethod.access & Opcodes.ACC_PRIVATE) != 0) {
// ACC_PRIVATE is set - no need for inheritance
// -> The list is as such immutable
realms.put(myLoc, new MemberRealm(myLoc, Collections.singleton(superType)));
} else if ((superMethod.access & Opcodes.ACC_PUBLIC) != 0 || (superMethod.access & Opcodes.ACC_PROTECTED) != 0) {
// ACC_PUBLIC or ACC_PROTECTED is set (both behave the same as far as overrides are concerned)
// -> Exposed to all children (ACC_FINAL is irrelevant as far as I know, nor are bridge methods)
Set<@NotNull String> children = allChildren.getOrDefault(superType, Collections.emptySet());
Set<@NotNull String> realmMembers;
if (children.isEmpty()) {
realmMembers = Collections.singleton(superType);
} else {
realmMembers = new HashSet<>(children);
realmMembers.add(superType);
}
MemberRealm realm = new MemberRealm(myLoc, realmMembers);
realms.put(myLoc, realm);
for (String child : children) {
realms.put(new MemberRef(child, superMethod.name, superMethod.desc), realm);
}
} else {
// package-protected access (no explicit access flags set) - this is where it gets more complicated
// as children could expand the access to ACC_PUBLIC or ACC_PROTECTED
// However, one still needs to be aware that in order for ACC_PUBLIC or ACC_PROTECTED to work in that way,
// the class that widens the access must be in the same package as the defining class.
Set<@NotNull String> realmAccess = new TreeSet<>();
Set<@NotNull String> children = allChildren.getOrDefault(superType, Collections.emptySet());
int lastSlashSuper = superType.lastIndexOf('/');
realmAccess.add(superType);
for (String child : children) {
int lastSlashChild = child.lastIndexOf('/');
if (lastSlashChild != lastSlashSuper || !child.regionMatches(0, superType, 0, lastSlashChild)) {
continue;
}
realmAccess.add(child);
ClassNode childNode = nodeLookup.get(child);
if (childNode == null) {
// Won't happen, but doesn't hurt to have it in there regardless
continue;
}
for (MethodNode method : childNode.methods) {
if (!method.name.equals(superMethod.name) || !method.desc.equals(superMethod.desc)) {
continue;
}
if ((method.access & Opcodes.ACC_PUBLIC) != 0 || (method.access & Opcodes.ACC_PROTECTED) != 0) {
// Widened access
realmAccess.addAll(allChildren.getOrDefault(child, Collections.emptySet()));
}
}
}
MemberRealm realm = new MemberRealm(myLoc, realmAccess);
for (String realmType : realmAccess) {
realms.put(new MemberRef(realmType, superMethod.name, superMethod.desc), realm);
}
}
if (!realms.containsKey(myLoc)) {
throw new IllegalStateException("Reference not in list of realms: " + myLoc);
}
}
for (FieldNode superField : superNode.fields) {
MemberRef myLoc = new MemberRef(superType, superField.name, superField.desc);
if (realms.containsKey(myLoc)) {
// Someone (likely a supertype) already added the entry - safe to assume it is being overwritten by this class
continue;
}
if ((superField.access & Opcodes.ACC_PRIVATE) != 0) {
// ACC_PRIVATE is set - no need for inheritance
// -> The list is as such immutable
realms.put(myLoc, new MemberRealm(myLoc, Collections.singleton(superType)));
} else if ((superField.access & Opcodes.ACC_PUBLIC) != 0 || (superField.access & Opcodes.ACC_PROTECTED) != 0) {
// ACC_PUBLIC or ACC_PROTECTED is set (both behave the same as far as overrides are concerned)
// -> Exposed to all children (ACC_FINAL is irrelevant as far as I know, nor are bridge methods)
Set<@NotNull String> children = allChildren.getOrDefault(superType, Collections.emptySet());
Set<@NotNull String> realmMembers;
if (children.isEmpty()) {
realmMembers = Collections.singleton(superType);
} else {
realmMembers = new HashSet<>(children);
realmMembers.add(superType);
}
MemberRealm realm = new MemberRealm(myLoc, realmMembers);
realms.put(myLoc, realm);
for (String child : children) {
realms.put(new MemberRef(child, superField.name, superField.desc), realm);
}
} else {
// package-protected access (no explicit access flags set) - this is where it gets more complicated
// as children could expand the access to ACC_PUBLIC or ACC_PROTECTED
// However, one still needs to be aware that in order for ACC_PUBLIC or ACC_PROTECTED to work in that way,
// the class that widens the access must be in the same package as the defining class.
Set<@NotNull String> realmAccess = new TreeSet<>();
Set<@NotNull String> children = allChildren.getOrDefault(superType, Collections.emptySet());
int lastSlashSuper = superType.lastIndexOf('/');
realmAccess.add(superType);
for (String child : children) {
int lastSlashChild = child.lastIndexOf('/');
if (lastSlashChild != lastSlashSuper || !child.regionMatches(0, superType, 0, lastSlashChild)) {
continue;
}
realmAccess.add(child);
ClassNode childNode = nodeLookup.get(child);
if (childNode == null) {
// Won't happen, but doesn't hurt to have it in there regardless
continue;
}
for (FieldNode field : childNode.fields) {
if (!field.name.equals(superField.name) || !field.desc.equals(superField.desc)) {
continue;
}
if ((field.access & Opcodes.ACC_PUBLIC) != 0 || (field.access & Opcodes.ACC_PROTECTED) != 0) {
// Widened access
realmAccess.addAll(allChildren.getOrDefault(child, Collections.emptySet()));
}
}
}
MemberRealm realm = new MemberRealm(myLoc, realmAccess);
for (String realmType : realmAccess) {
realms.put(new MemberRef(realmType, superField.name, superField.desc), realm);
}
}
if (!realms.containsKey(myLoc)) {
throw new IllegalStateException("Reference not in list of realms: " + myLoc);
}
}
}
return Collections.unmodifiableMap(realms);
}
@Unmodifiable
@NotNull
private final Map realms;
/**
* Create a {@link SimpleTopLevelLookup} based on a list of {@link ClassNode ClassNodes} that are used to
* compute the member realms.
*
* JDK Classes or library classes that are irrelevant to the remapping process should be left out for performance
* (especially memory-related) reasons.
*
* @param applicationClasses The list of classes to analyze
*/
public SimpleTopLevelLookup(@Unmodifiable @NotNull List<@NotNull ClassNode> applicationClasses) {
this(SimpleTopLevelLookup.realmsOf(applicationClasses));
}
/**
* Create a {@link SimpleTopLevelLookup} from an immutable map of {@link MemberRef member references} to
* their respective {@link MemberRealm}.
*
*
JDK Classes or library classes that are irrelevant to the remapping process should be left out for performance
* (especially memory-related) reasons.
*
* @param realms The map to use to lookup the realm of members.
*/
public SimpleTopLevelLookup(@Unmodifiable @NotNull Map realms) {
this.realms = realms;
}
@Override
@NotNull
@Contract(pure = true, value = "!null -> !null; null -> fail")
@NonBlocking
public MemberRef getDefinition(@NotNull MemberRef reference) {
MemberRealm realm = this.realmOf(reference);
if (realm == null) {
return reference;
}
return realm.rootDefinition;
}
/**
* Lookup the {@link MemberRealm} of a {@link MemberRef}.
*
* @param reference The reference to look up
* @return The corresponding {@link MemberRef}, or null if not found.
*/
@Nullable
@Contract(pure = true)
public MemberRealm realmOf(@NotNull MemberRef reference) {
return this.realms.get(reference);
}
}