package org.stianloader.remapper; import java.util.Arrays; import java.util.List; import java.util.Objects; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.objectweb.asm.Handle; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.AnnotationNode; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.FieldInsnNode; import org.objectweb.asm.tree.FieldNode; import org.objectweb.asm.tree.FrameNode; import org.objectweb.asm.tree.InnerClassNode; import org.objectweb.asm.tree.InsnList; import org.objectweb.asm.tree.InvokeDynamicInsnNode; import org.objectweb.asm.tree.LdcInsnNode; import org.objectweb.asm.tree.LocalVariableNode; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodNode; import org.objectweb.asm.tree.ModuleNode; import org.objectweb.asm.tree.MultiANewArrayInsnNode; import org.objectweb.asm.tree.ParameterNode; import org.objectweb.asm.tree.RecordComponentNode; import org.objectweb.asm.tree.TryCatchBlockNode; import org.objectweb.asm.tree.TypeInsnNode; /** * Simple in-memory remapping engine. Unlike many other remappers it is able to take in already parsed * {@link org.objectweb.asm.tree.ClassNode Objectweb ASM Classnodes} as input and output them without having * to go through an intermediary store-to-file mode. * *
Additionally, this is a remapper that is only a remapper. More specifically, it will only remap - but not * change your access flags, LVT entries or anything else that might not be intuitive. That being said, * existing LVT entries might get edited in order to support parameter remapping, though it is not guaranteed * to do so at this time (2025-12-20) and it will depend on the structure of the method. Further, it will not generate * LVT entries for parameters without LVT entries. * *
The names of the destination namespace (or the remapped names in laymen's terms) are provided by the * {@link MappingLookup} instance supplied through the {@link Remapper#Remapper(MappingLookup) constructor} * of this class. After construction, the remapper itself cannot be mutated and all changes need to be performed * through the {@link MappingLookup} instance. * *
ClassNodes can be remapped via {@link Remapper#remapNode(ClassNode, StringBuilder)}. A similar method also exists * to remap MethodNodes and FieldNodes. * *
While a single instance of this class can be used in a concurrent environment and be shared across multiple threads, * the same may not apply to a {@link MappingLookup} instance. On a similar note a single call to the remapNode * methods do not cause any parallelisation to happen. As such if it is known that the remapper is called on larger * classes, it might be useful to be aware that methods can be individually remapped via * {@link Remapper#remapNode(String, MethodNode, StringBuilder)}. However, {@link Remapper#remapNode(ClassNode, StringBuilder)} * implies {@link Remapper#remapNode(String, MethodNode, StringBuilder)} so this strategy has serious flaws. * If a serious performance updraft is expected when employing parallelisation on a ClassNode level, then please * open up an issue on the project's repository. * *
The burden of handling restrictions on method overloading and inheritance falls * upon the used {@link MappingLookup} instance. If the {@link MappingLookup} instance is erroneously implemented * or used (i.e. in the case of {@link SimpleMappingLookup} the instance being fed invalid data), it is possible to * modify the way inheritance and overloading behaves - potentially causing a method to no longer override another * or creating an override where no one existed beforehand. More acutely, it is also possible that inheritance applies * to fields, too - this is notably the case when making use of anonymous classes at enum members. Unlike sl-deobf, * the burden of handling inheritance for fields also falls upon the {@link MappingLookup} instance. * *
The {@link Remapper} instance is unable to verify mapping collisions and it is the job of the {@link MappingLookup} * implementation to ensure that such events do not happen - note that some implementations such as {@link SimpleMappingLookup} * do not check for such inconsistencies; for more information on this topic, see the manual of your lookup implementation. * *
While the {@link Remapper} instance allows nonsensical remapping requests (such as remapping methods from/to <clinit> * or <init>), it is imperative that this behaviour is not relied on and that {@link MappingLookup} instances take * the necessary precautions to prohibit such requests. * *
Layered mappings, that is mappings that are built ontop of other mappings, are not directly supported by this remapper. * However, it is possible to easily obtain this behaviour by calling the remapNode methods multiple times - more specifically * once per layer of mappings. That being said, some {@link MappingLookup} instances might not necessarily support such behaviour, * especially when it comes to computing the hierarchy of classes as the class name might not necessarily be known to the * {@link MappingLookup} instance. * As such the alternative is directly "squashing" the mapping layers into a single {@link MappingLookup}. In case of doubt, * consult the manual of the used lookup implementation for further guidance on how layered mappings may be implemented. * *
Remapping reflective calls are not supported due to the complexity required for such a niche feature. * If absolutely needed (we generally recommend wrapping the reflective operations in a way that they are redirected as needed * at runtime), 3rd party tools should be used. The same applies to method handles or other string constants. That being said, * {@code java.lang.Class} constants will get remapped so very simple reflective operations might still behave as intended. */ public final class Remapper { /** * Remaps a field descriptor. * * @param lookup The {@link MappingLookup} to use in order to remap the descriptor. * @param fieldDesc The old (unmapped) field descriptor * @param sharedBuilder A shared cached string builder. The contents of the string builder are wiped and after the invocation the contents are undefined * @return The new (remapped) field descriptor. It can be identity identical to the "fieldDesc" if it didn't need to be altered */ @SuppressWarnings("null") @NotNull public static String getRemappedFieldDescriptor(@NotNull MappingLookup lookup, @NotNull String fieldDesc, @NotNull StringBuilder sharedBuilder) { sharedBuilder.setLength(0); return Remapper.remapSingleDesc(lookup, fieldDesc, sharedBuilder); } /** * Remaps a method descriptor. * *
Note: This method completely disregards bridges or other context-specific circumstances. * Overall, it aims to be the most generically applicable method. * *
Although this method was initially written to remap {@link MethodNode#desc method descriptors}, * this method also can work with {@link MethodNode#signature method signatures}. * * @param lookup The {@link MappingLookup} to use in order to remap the descriptor. * @param methodDesc The old (unmapped) method descriptor * @param sharedBuilder A shared cached string builder. The contents of the string builder are wiped and after the invocation the contents are undefined * @return The new (remapped) method descriptor. It can be identity identical to the "methodDesc" if it didn't need to be altered */ @NotNull public static String getRemappedMethodDescriptor(@NotNull MappingLookup lookup, @NotNull String methodDesc, @NotNull StringBuilder sharedBuilder) { sharedBuilder.setLength(0); if (!Remapper.remapSignature(lookup, methodDesc, sharedBuilder)) { return methodDesc; } return sharedBuilder.toString(); } /** * Remap an internal name or array {@link String}, meaning that this method accepts the same kind * of strings as {@link Type#getObjectType(String)}. * *
The contents of the {@link StringBuilder} instance passed to this method might be overwritten and * the contents afterwards should be considered unknown. It is especially not guaranteed (in fact, it usually won't be) * that the content of the {@link StringBuilder} is equal to the returned {@link String}. * * @param lookup The {@link MappingLookup} to use in order to remap the descriptor. * @param internalName The internal name in the source namespace. * @param sharedStringBuilder A shared {@link StringBuilder} instance of object pooling purposes (note: The instance should not be used across multiple threads!) * @return The remapped internal name in the destination namespace. * @see Type#getInternalName() */ @NotNull public static String remapInternalName(@NotNull MappingLookup lookup, @NotNull String internalName, @NotNull StringBuilder sharedStringBuilder) { if (internalName.codePointAt(0) == '[') { return Remapper.remapSingleDesc(lookup, internalName, sharedStringBuilder); } else { return lookup.getRemappedClassName(internalName); } } /** * Remap a generic signature string, as used for example in {@link MethodNode#signature}, {@link FieldNode#signature} * or {@link ClassNode#signature}. As this method is fairly generic it is even capable of remapping method, field or * type descriptors. However, this method is not capable of remapping internal names. If internal names should * be remapped, use {@link #remapInternalName(MappingLookup, String, StringBuilder)} instead. * *
Internally, this method is recursive (in order to be able to correctly remap nested generics), which is * why this method accepts a start and end pointer, which are the {@link String#codePointAt(int) codepoints} * which should be remapped and pushed to the {@link StringBuilder} buffer. This algorithm evaluates the * input signature from left to right. * *
In the case that end is greater than start, a crash is likely, although the type of crash is not defined.
* It may also deadlock or cause an {@link OutOfMemoryError OOM situation}. Furthermore, if end does not correctly
* align with a type boundary (usually a semicolon or a character that represents a primitive), then unexpected
* behaviour is likely - more likely than not it will cause a crash, deadlock or {@link OutOfMemoryError}.
* As similar behaviour also applies to start, both start and end should be chosen carefully.
* More often than not, this method can be considered overkill and instead {@link Remapper#remapSignature(MappingLookup, String, StringBuilder)}
* can be used safely as an alternative - however that method will remap the entire signature while
* this method can (if start and end are chosen accordingly) remap parts of it.
*
* @param lookup The {@link MappingLookup} to use in order to remap the descriptor.
* @param signature The signature to remap in the source namespace.
* @param start The start of signature.
* @param end The last codepoint of the signature that should be handled by this method. Everything beyond it is plainly ignored.
* @param signatureOut The {@link StringBuilder} instance to which the remapped signature should be stored into.
* @return True if a modification happened while remapping the signature, false otherwise.
*/
public static boolean remapSignature(@NotNull MappingLookup lookup, @NotNull String signature, int start, int end, @NotNull StringBuilder signatureOut) {
if (start == end) {
return false;
}
int type = signature.codePointAt(start++);
switch (type) {
case 'T':
// generics type parameter
// fall-through intended as they are similar enough in format compared to objects
case 'L':
// object
// find the end of the internal name of the object
int endObject = start;
while(true) {
// this will skip a character, but this is not interesting as class names have to be at least 1 character long
int codepoint = signature.codePointAt(++endObject);
if (codepoint == ';') {
String name = signature.substring(start, endObject);
String newName = lookup.getRemappedClassNameFast(name);
boolean modified = false;
if (newName != null) {
name = newName;
modified = true;
}
signatureOut.appendCodePoint(type);
signatureOut.append(name);
signatureOut.append(';');
modified |= Remapper.remapSignature(lookup, signature, ++endObject, end, signatureOut);
return modified;
} else if (codepoint == '<') {
// generics - please no
// post scriptum: well, that was a bit easier than expected
int openingBrackets = 1;
int endGenerics = endObject;
while(true) {
codepoint = signature.codePointAt(++endGenerics);
if (codepoint == '>' ) {
if (--openingBrackets == 0) {
break;
}
} else if (codepoint == '<') {
openingBrackets++;
}
}
String name = signature.substring(start, endObject);
String newName = lookup.getRemappedClassNameFast(name);
boolean modified = false;
if (newName != null) {
name = newName;
modified = true;
}
signatureOut.append('L');
signatureOut.append(name);
signatureOut.append('<');
modified |= Remapper.remapSignature(lookup, signature, endObject + 1, endGenerics++, signatureOut);
signatureOut.append('>');
// apparently that can be rarely be a '.', don't ask when or why exactly this occurs
signatureOut.appendCodePoint(signature.codePointAt(endGenerics));
modified |= Remapper.remapSignature(lookup, signature, ++endGenerics, end, signatureOut);
return modified;
}
}
/*
case '+':
// I do not know what this one does - but it appears that it works good just like it does right now
case '*':
// wildcard - this can also be read like a regular primitive
// fall-through intended
case '(':
case ')':
// apparently our method does not break even in these cases, so we will consider them raw primitives
case '[':
// array - fall through intended as in this case they behave the same
*/
default:
// primitive
signatureOut.appendCodePoint(type);
return Remapper.remapSignature(lookup, signature, start, end, signatureOut); // Did not modify the signature - but following operations could
}
}
/**
* Remap a generic signature string, as used for example in {@link MethodNode#signature}, {@link FieldNode#signature}
* or {@link ClassNode#signature}. As this method is fairly generic it is even capable of remapping method, field or
* type descriptors. However, this method is not capable of remapping internal names. If internal names should
* be remapped, use {@link #remapInternalName(MappingLookup, String, StringBuilder)} instead.
*
*
Internally, this method is recursive (in order to be able to correctly remap nested generics).
* This algorithm evaluates the input signature from left to right.
*
* @param lookup The {@link MappingLookup} to use in order to remap the descriptor.
* @param signature The signature to remap in the source namespace.
* @param out The {@link StringBuilder} instance to which the remapped signature should be stored into.
* @return True if a modification happened while remapping the signature, false otherwise - that is if false,
* {@link StringBuilder#toString()} of out will be equal to signature.
*/
public static boolean remapSignature(@NotNull MappingLookup lookup, @NotNull String signature, @NotNull StringBuilder out) {
return Remapper.remapSignature(lookup, signature, 0, signature.length(), out);
}
@NotNull
private static String remapSingleDesc(@NotNull MappingLookup lookup, @NotNull String input, StringBuilder sharedBuilder) {
int indexofL = input.indexOf('L');
if (indexofL == -1) {
return input; // Primitive or array of primitives
}
int length = input.length();
String internalName = input.substring(indexofL + 1, length - 1);
String newInternalName = lookup.getRemappedClassNameFast(internalName);
if (newInternalName == null) {
return input;
}
sharedBuilder.setLength(indexofL + 1);
sharedBuilder.setCharAt(indexofL, 'L');
while(indexofL != 0) {
sharedBuilder.setCharAt(--indexofL, '[');
}
sharedBuilder.append(newInternalName);
sharedBuilder.append(';');
return sharedBuilder.toString();
}
@NotNull
private final MappingLookup lookup;
/**
* Constructor. Create a {@link Remapper} instance which uses a given {@link MappingLookup} instance
* to remap nodes.
*
* @param lookup The lookup to use for all remapping requests, may not be null
*/
public Remapper(@NotNull MappingLookup lookup) {
this.lookup = lookup;
}
/**
* Obtain the {@link MappingLookup} instance from which this {@link Remapper} sources all source to destination
* namespace name mappings. This instance is set through the constructor.
*
* @return The {@link MappingLookup} instance used by this {@link Remapper}.
*/
@NotNull
@Contract(pure = true)
public final MappingLookup getLookup() {
return this.lookup;
}
private void remapAnnotation(AnnotationNode annotation, StringBuilder sharedStringBuilder) {
String internalName = annotation.desc.substring(1, annotation.desc.length() - 1);
String newInternalName = this.lookup.getRemappedClassNameFast(internalName);
if (newInternalName != null) {
annotation.desc = 'L' + newInternalName + ';';
}
if (annotation.values != null) {
int size = annotation.values.size();
for (int i = 0; i < size; i++) {
@SuppressWarnings("unused") // We are using the cast as a kind of built-in automatic unit test
String bitvoid = (String) annotation.values.get(i++);
this.remapAnnotationValue(annotation.values.get(i), i, annotation.values, sharedStringBuilder);
}
}
}
private void remapAnnotations(List extends AnnotationNode> annotations, StringBuilder sharedStringBuilder) {
if (annotations == null) {
return;
}
for (AnnotationNode annotation : annotations) {
this.remapAnnotation(annotation, sharedStringBuilder);
}
}
private void remapAnnotationValue(Object value, int index, List