Skip to content

Commit

Permalink
Mapping generator API support for variables
Browse files Browse the repository at this point in the history
Only applies to variables defined in the LVT, so no-op if no debug symbols are present
  • Loading branch information
Col-E committed Oct 28, 2024
1 parent 521748d commit 26e6693
Show file tree
Hide file tree
Showing 22 changed files with 466 additions and 66 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@
import software.coley.recaf.services.mapping.data.MethodMapping;
import software.coley.recaf.services.mapping.data.VariableMapping;

import java.util.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;

/**
* Collection of object representations of mappings.
Expand Down Expand Up @@ -81,8 +88,8 @@ public void addMethod(String ownerName, String desc, String oldName, String newN
* Post-mapping method name.
*/
public void addVariable(String ownerName, String methodName, String methodDesc,
String desc, String oldName, int index,
String newName) {
String desc, String oldName, int index,
String newName) {
if (Objects.equals(oldName, newName)) return; // Skip identity mappings
String key = varKey(ownerName, methodName, methodDesc);
variables.computeIfAbsent(key, n -> new ArrayList<>())
Expand Down Expand Up @@ -214,7 +221,7 @@ public String getMappedMethodName(@Nonnull String ownerName, @Nonnull String met
@Nullable
@Override
public String getMappedVariableName(@Nonnull String className, @Nonnull String methodName, @Nonnull String methodDesc,
@Nullable String name, @Nullable String desc, int index) {
@Nullable String name, @Nullable String desc, int index) {
List<VariableMapping> variablesInMethod = getMethodVariableMappings(className, methodName, methodDesc);
for (VariableMapping variable : variablesInMethod) {
if (equalsOrNull(desc, variable.getDesc()) && equalsOrNull(name, variable.getOldName())
Expand All @@ -231,15 +238,16 @@ public IntermediateMappings exportIntermediate() {
return this;
}

private static String varKey(String ownerName, String methodName, String methodDesc) {
return String.format("%s\t%s\t%s", ownerName, methodName, methodDesc);
@Nonnull
protected static String varKey(@Nonnull String ownerName, @Nonnull String methodName, @Nonnull String methodDesc) {
return ownerName + "\t" + methodName + "\t" + methodDesc;
}

private static boolean indexEqualsOrOOB(int a, int b) {
return a < 0 || b < 0 || a == b;
}

private static boolean equalsOrNull(String a, String b) {
private static boolean equalsOrNull(@Nullable String a, @Nullable String b) {
return a == null || b == null || a.equals(b);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
*/
public class WorkspaceClassRemapper extends ClassRemapper {
private final WorkspaceBackedRemapper workspaceRemapper;
private String visitedClassName;

/**
* @param cv
Expand All @@ -38,8 +39,32 @@ public WorkspaceClassRemapper(@Nullable ClassVisitor cv, @Nonnull Workspace work
}

@Override
protected MethodVisitor createMethodRemapper(MethodVisitor mv) {
return new BetterMethodRemapper(mv, workspaceRemapper);
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
// Record the class name
visitedClassName = name;
super.visit(version, access, name, signature, superName, interfaces);
}

@Override
public MethodVisitor visitMethod(int access, @Nonnull String name, @Nonnull String descriptor,
@Nullable String signature, @Nullable String[] exceptions) {
// Adapted from base ClassRemapper implementation.
// This allows us to skip calls to super 'visitMethod' to bypass calls to 'createMethodRemapper'
// since our visitor we want to make needs information accessible here, but not in that method.
String remappedDescriptor = remapper.mapMethodDesc(descriptor);
MethodVisitor mv =
cv == null ? null : cv.visitMethod(
access,
remapper.mapMethodName(className, name, descriptor),
remappedDescriptor,
remapper.mapSignature(signature, false),
exceptions == null ? null : remapper.mapTypes(exceptions));
return mv == null ? null : new VariableRenamingMethodVisitor(visitedClassName, name, descriptor, mv, workspaceRemapper);
}

@Override
protected MethodVisitor createMethodRemapper(@Nullable MethodVisitor mv) {
throw new IllegalStateException("Enhanced 'visitMethod(...)' usage required, 'createMethodMapper(...)' should never be called");
}

/**
Expand All @@ -51,10 +76,21 @@ public boolean hasMappingBeenApplied() {

/**
* {@link MethodRemapper} pointing to enhanced remapping methods to allow for more context-sensitive behavior.
* Method visitor that functions as an adapter for the
* {@link #visitMethod(int, String, String, String, String[]) standard method remapping visitor}
* that additionally supports variable renaming.
*/
private class BetterMethodRemapper extends MethodRemapper {
protected BetterMethodRemapper(MethodVisitor methodVisitor, Remapper remapper) {
super(RecafConstants.getAsmVersion(), methodVisitor, remapper);
private class VariableRenamingMethodVisitor extends MethodRemapper {
private final String methodOwner;
private final String methodName;
private final String methodDesc;

public VariableRenamingMethodVisitor(@Nonnull String methodOwner, @Nonnull String methodName, @Nonnull String methodDesc,
@Nullable MethodVisitor mv, @Nonnull Remapper remapper) {
super(RecafConstants.getAsmVersion(), mv, remapper);
this.methodOwner = methodOwner;
this.methodName = methodName;
this.methodDesc = methodDesc;
}

@Override
Expand All @@ -71,28 +107,10 @@ public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootst
(Handle) remapper.mapValue(bootstrapMethodHandle),
remappedBootstrapMethodArguments);
}
}

/**
* Method visitor that functions as an adapter for the
* {@link #visitMethod(int, String, String, String, String[]) standard method remapping visitor}
* that additionally supports variable renaming.
*/
private class VariableRenamingMethodVisitor extends MethodVisitor {
private final String className;
private final String methodName;
private final String methodDesc;

public VariableRenamingMethodVisitor(String className, String methodName, String methodDesc, MethodVisitor mv) {
super(RecafConstants.getAsmVersion(), mv);
this.className = className;
this.methodName = methodName;
this.methodDesc = methodDesc;
}

@Override
public void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index) {
String mappedName = workspaceRemapper.mapVariableName(className, methodName, methodDesc, name, desc, index);
String mappedName = workspaceRemapper.mapVariableName(methodOwner, methodName, methodDesc, name, desc, index);
super.visitLocalVariable(mappedName, desc, signature, start, end, index);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
package software.coley.recaf.services.mapping.aggregate;

import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import software.coley.recaf.services.mapping.IntermediateMappings;
import software.coley.recaf.services.mapping.Mappings;
import software.coley.recaf.services.mapping.WorkspaceBackedRemapper;
import software.coley.recaf.services.mapping.data.ClassMapping;
import software.coley.recaf.services.mapping.data.FieldMapping;
import software.coley.recaf.services.mapping.data.MemberMapping;
import software.coley.recaf.services.mapping.data.MethodMapping;
import software.coley.recaf.services.mapping.data.VariableMapping;
import software.coley.recaf.workspace.model.Workspace;

import java.util.*;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
* Mappings implementation for internal tracking of aggregated mappings.
Expand Down Expand Up @@ -57,7 +63,8 @@ public String map(String internalName) {
* @return Original class name.
* {@code null} if the class has not been mapped.
*/
public String getReverseClassMapping(String name) {
@Nullable
public String getReverseClassMapping(@Nonnull String name) {
return reverseOrderClassMapping.get(name);
}

Expand All @@ -72,17 +79,25 @@ public String getReverseClassMapping(String name) {
* @return Original name of the field if any mappings exist.
* {@code null} if the field has not been mapped.
*/
public String getReverseFieldMapping(String owner, String fieldName, String fieldDesc) {
@Nullable
public String getReverseFieldMapping(@Nonnull String owner, @Nonnull String fieldName, @Nonnull String fieldDesc) {
String originalOwnerName = getReverseClassMapping(owner);
if (originalOwnerName == null)
originalOwnerName = owner;

// Get fields in the original class
List<FieldMapping> fieldMappings = fields.get(originalOwnerName);
if (fieldMappings == null)
if (fieldMappings == null || fieldMappings.isEmpty())
return null;

// Find a matching field
String originalDesc = reverseMapper.mapDesc(fieldDesc);
for (FieldMapping fieldMapping : fieldMappings) {
// The current name must match the mappings "new" name
if (!fieldName.equals(fieldMapping.getNewName()))
continue;
String originalDesc = reverseMapper.mapDesc(fieldDesc);

// The original field descriptor must match the mapping key's
if (originalDesc.equals(fieldMapping.getDesc()))
return fieldMapping.getOldName();
}
Expand All @@ -100,25 +115,86 @@ public String getReverseFieldMapping(String owner, String fieldName, String fiel
* @return Original name of the method if any mappings exist.
* {@code null} if the method has not been mapped.
*/
public String getReverseMethodMapping(String owner, String methodName, String methodDesc) {
@Nullable
public String getReverseMethodMapping(@Nonnull String owner, @Nonnull String methodName, @Nonnull String methodDesc) {
String originalOwnerName = getReverseClassMapping(owner);
if (originalOwnerName == null)
originalOwnerName = owner;

// Get methods in the original class
List<MethodMapping> methodMappings = methods.get(originalOwnerName);
if (methodMappings == null)
if (methodMappings == null || methodMappings.isEmpty())
return null;

// Find a matching method
String originalDesc = reverseMapper.mapDesc(methodDesc);
for (MethodMapping methodMapping : methodMappings) {
// The current name must match the mappings "new" name
if (!methodName.equals(methodMapping.getNewName()))
continue;
String originalDesc = reverseMapper.mapDesc(methodDesc);

// The original method descriptor must match the mapping key's
if (originalDesc.equals(methodMapping.getDesc()))
return methodMapping.getOldName();
}
return null;
}

/**
* @param owner
* Current class name.
* @param methodName
* Current method name.
* @param methodDesc
* Current method descriptor.
* @param varName
* Current variable name.
* @param varDesc
* Current variable descriptor.
* @param varIndex
* Variable index.
*
* @return Original name of the variable if any mappings exist.
* {@code null} if the variable has not been mapped.
*/
@Nullable
public String getReverseVariableMapping(@Nonnull String owner, @Nonnull String methodName, @Nonnull String methodDesc,
@Nonnull String varName, @Nonnull String varDesc, int varIndex) {
String originalOwnerName = getReverseClassMapping(owner);
if (originalOwnerName == null)
originalOwnerName = owner;

// Get methods in the original class
List<MethodMapping> methodMappings = methods.get(originalOwnerName);
if (methodMappings == null || methodMappings.isEmpty())
return null;

String originalMethodDesc = reverseMapper.mapDesc(methodDesc);
String originalVarDesc = reverseMapper.mapDesc(varDesc);
for (MethodMapping methodMapping : methodMappings) {
// The current name must match the mappings "new" name
if (!methodName.equals(methodMapping.getNewName()))
continue;

// The original method descriptor must match the mapping key's
if (originalMethodDesc.equals(methodMapping.getDesc())) {
// Get the variables that were mapped under the original name
List<VariableMapping> variableMappings = variables.get(varKey(originalOwnerName, methodMapping.getOldName(), originalMethodDesc));
for (VariableMapping variableMapping : variableMappings) {
// If the variable index, name, and descriptor match, yield the variable mapping's original name
if (variableMapping.getIndex() == varIndex && variableMapping.getNewName().equals(varName)) {
if (varDesc.equals(originalVarDesc)) {
return variableMapping.getOldName();
}
}
}
}
}
return null;
}

@Override
public void addClass(String oldName, String newName) {
public void addClass(@Nonnull String oldName, @Nonnull String newName) {
super.addClass(oldName, newName);
reverseOrderClassMapping.put(newName, oldName);
}
Expand All @@ -142,7 +218,7 @@ public void clear() {
*
* @return {@code true} when the mapping operation required bridging a current class name to its original name.
*/
public boolean update(Mappings newMappings) {
public boolean update(@Nonnull Mappings newMappings) {
// ORIGINAL:
// a -> b
// a.f1 -> b.f2
Expand All @@ -157,6 +233,7 @@ public boolean update(Mappings newMappings) {
bridged = updateClasses(intermediate.getClasses());
bridged |= updateMembers(intermediate.getFields().values());
bridged |= updateMembers(intermediate.getMethods().values());
bridged |= updateVariables(intermediate.getVariables().values());
return bridged;
}

Expand All @@ -168,7 +245,7 @@ private boolean updateClasses(Map<String, ClassMapping> classes) {
String aName = reverseOrderClassMapping.get(bName);
if (aName != null) {
// There is a prior entry of the class, 'aName' thus we use it as the key
// and not 'bName' since that was the prior value the mapping for 'aName.
// and not 'bName' since that was the prior value the mapping for 'aName'.
bridged = true;
addClass(aName, cName);
} else {
Expand All @@ -179,27 +256,26 @@ private boolean updateClasses(Map<String, ClassMapping> classes) {
return bridged;
}


private <M extends MemberMapping> boolean updateMembers(Collection<List<M>> newMappings) {
// With members, we need to take special care, for example:
// 1. a --> b
// 2. b.x --> b.y
// Now we need to ensure the mapping "a.x --> y" exists.
// Now we need to ensure the mapping "a.x --> b.y" exists.
boolean bridged = false;
for (List<M> members : newMappings) {
for (MemberMapping newMemberMapping : members) {
String bName = newMemberMapping.getOwnerName();
String aName = reverseOrderClassMapping.get(bName);
String bOwnerName = newMemberMapping.getOwnerName();
String aOwnerName = reverseOrderClassMapping.get(bOwnerName);
String oldMemberName = newMemberMapping.getOldName();
String newMemberName = newMemberMapping.getNewName();
String desc = newMemberMapping.getDesc();
String owner = bName;
if (aName != null) {
String owner = bOwnerName;
if (aOwnerName != null) {
// We need to map the member current mapped owner name to the
// original owner's name.
bridged = true;
owner = aName;
oldMemberName = findPriorMemberName(aName, newMemberMapping);
owner = aOwnerName;
oldMemberName = findPriorMemberName(aOwnerName, newMemberMapping);
}

// Desc must always be checked for updates
Expand All @@ -216,6 +292,29 @@ private <M extends MemberMapping> boolean updateMembers(Collection<List<M>> newM
return bridged;
}

private boolean updateVariables(Collection<List<VariableMapping>> newMappings) {
// a.foo() var x
// ...
// a.foo() --> b.foo()
// b.foo() --> b.bar()
// b.bar() var x --> var z
// ...
// a.foo() var x --> var z
boolean bridged = false;

/*
TODO: Aggregate variable mappings
for (List<VariableMapping> variableMappings : newMappings) {
for (VariableMapping newVariableMapping : variableMappings) {
String bOwner = newVariableMapping.getOwnerName();
String bMethodName = newVariableMapping.getMethodName();
String bMethodDesc = newVariableMapping.getMethodDesc();
}
}
*/
return bridged;
}

private String applyReverseMappings(String desc) {
if (desc == null)
return null;
Expand Down
Loading

0 comments on commit 26e6693

Please sign in to comment.