Skip to content

Commit

Permalink
Implement patch audit trail
Browse files Browse the repository at this point in the history
  • Loading branch information
Su5eD committed Jul 24, 2024
1 parent e61a58e commit ab5ba82
Show file tree
Hide file tree
Showing 35 changed files with 436 additions and 205 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.objectweb.asm.tree.*;
import org.sinytra.adapter.patch.analysis.LocalVariableLookup;
import org.sinytra.adapter.patch.api.MethodContext;
import org.sinytra.adapter.patch.api.MethodTransform;
import org.sinytra.adapter.patch.api.MixinConstants;
import org.sinytra.adapter.patch.api.PatchContext;
import org.sinytra.adapter.patch.analysis.selector.AnnotationHandle;
Expand Down Expand Up @@ -127,10 +128,10 @@ public List<AbstractInsnNode> findInjectionTargetInsns(@Nullable TargetPair targ
}

@Override
public void updateDescription(List<Type> parameters) {
public void updateDescription(MethodTransform transform, List<Type> parameters) {
Type returnType = Type.getReturnType(this.methodNode.desc);
String newDesc = Type.getMethodDescriptor(returnType, parameters.toArray(Type[]::new));
LOGGER.info(PatchInstance.MIXINPATCH, "Changing descriptor of method {}.{}{} to {}", this.classNode.name, this.methodNode.name, this.methodNode.desc, newDesc);
recordAudit(transform, "Change descriptor to %s", newDesc);
this.methodNode.desc = newDesc;
this.methodNode.signature = null;
}
Expand Down Expand Up @@ -224,6 +225,11 @@ public boolean hasInjectionPointValue(String value) {
return this.injectionPointAnnotation != null && this.injectionPointAnnotation.<String>getValue("value").map(v -> value.equals(v.get())).orElse(false);
}

@Override
public void recordAudit(Object transform, String message, Object... args) {
this.patchContext.environment().auditTrail().recordAudit(transform, this, message, args);
}

private InsnList getSlicedInsns(AnnotationHandle parentAnnotation, ClassNode classNode, MethodNode injectorMethod, ClassNode targetClass, MethodNode targetMethod, PatchContext context, Target mixinTarget) {
return parentAnnotation.<AnnotationNode>getValue("slice")
.map(handle -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package org.sinytra.adapter.patch;

import com.mojang.logging.LogUtils;
import it.unimi.dsi.fastutil.Pair;
import org.jetbrains.annotations.Nullable;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodNode;
import org.sinytra.adapter.patch.api.MethodContext;
import org.sinytra.adapter.patch.api.PatchAuditTrail;
import org.slf4j.Logger;

import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import static org.sinytra.adapter.patch.PatchInstance.MIXINPATCH;

public class PatchAuditTrailImpl implements PatchAuditTrail {
private static final DecimalFormat FORMAT = new DecimalFormat("##.00");
private static final Logger LOGGER = LogUtils.getLogger();
private final Map<Candidate, AuditLog> auditTrail = new LinkedHashMap<>();
private final Map<Candidate, Match> candidates = new ConcurrentHashMap<>();

public void prepareMethod(MethodContext methodContext) {
Candidate candidate = new Candidate(methodContext.getMixinClass(), methodContext.getMixinMethod());
synchronized (this.auditTrail) {
this.auditTrail.put(candidate, AuditLog.create(methodContext));
}
}

public void recordAudit(Object transform, ClassNode classNode, String message, Object... args) {
Candidate candidate = new Candidate(classNode, null);
recordAudit(transform, null, candidate, message.formatted(args));
}

public void recordAudit(Object transform, MethodContext methodContext, String message, Object... args) {
Candidate candidate = new Candidate(methodContext.getMixinClass(), methodContext.getMixinMethod());
recordAudit(transform, methodContext.getMixinMethod(), candidate, message.formatted(args));
}

private void recordAudit(Object transform, @Nullable MethodNode methodNode, Candidate candidate, String message) {
synchronized (this.auditTrail) {
AuditLog auditLog = this.auditTrail.computeIfAbsent(candidate, k -> new AuditLog(methodNode != null ? methodNode.name + methodNode.desc : null, new ArrayList<>()));
List<Pair<Object, StringBuilder>> entries = auditLog.entries();

StringBuilder builder;
if (entries.isEmpty() || entries.getLast().left() != transform) {
entries.add(Pair.of(transform, builder = new StringBuilder()));
builder.append("\n >> Using ").append(transform.getClass().getName());
} else {
builder = entries.getLast().right();
}

builder.append("\n - ").append(message);
LOGGER.info(MIXINPATCH, "Applying [{}] {}", transform.getClass().getSimpleName(), message);
}
}

public void recordResult(MethodContext methodContext, Match match) {
Candidate candidate = new Candidate(methodContext.getMixinClass(), methodContext.getMixinMethod());
this.candidates.compute(candidate, (key, prev) -> prev == null ? match : prev.or(match));
}

public String getCompleteReport() {
StringBuilder builder = new StringBuilder();

getSummaryLines().forEach(l -> builder.append(l).append("\n"));

List<Map.Entry<Candidate, Match>> failed = this.candidates.entrySet().stream().filter(m -> m.getValue() == Match.NONE).toList();
if (!failed.isEmpty()) {
builder.append("\n=============== Failed mixins ===============");
failed.forEach(e -> builder.append("\n").append(e.getKey().classNode().name).append("\t").append(e.getKey().methodNode().name).append(e.getKey().methodNode().desc));
builder.append("\n=============================================\n\n");
} else {
builder.append("\n");
}

this.auditTrail.forEach((candidate, auditLog) -> {
if (auditLog.entries().isEmpty()) {
return;
}

if (auditLog.originalMethod() == null) {
builder.append("Mixin class ").append(candidate.classNode().name);
} else {
builder.append("Mixin method ").append(candidate.classNode().name).append(" ").append(auditLog.originalMethod());
}

for (Pair<Object, StringBuilder> record : auditLog.entries()) {
builder.append(record.right());
}

builder.append("\n\n");
});

return builder.toString();
}

private List<String> getSummaryLines() {
int total = this.candidates.size();
int successful = (int) this.candidates.values().stream().filter(m -> m == Match.FULL).count();
int partial = (int) this.candidates.values().stream().filter(m -> m == Match.PARTIAL).count();
int failed = (int) this.candidates.values().stream().filter(m -> m == Match.NONE).count();
double rate = (successful + partial) / (double) total * 100;
double accuracy = (successful / (double) total + partial / (double) total / 2.0) * 100;

return List.of(
"==== Connector Mixin Patch Audit Summary ====",
"Successful: %s".formatted(successful),
"Partial: %s".formatted(partial),
"Failed: %s".formatted(failed),
"Success rate: %s%% Accuracy: %s%%".formatted(FORMAT.format(rate), FORMAT.format(accuracy)),
"============================================="
);
}

public boolean hasFailingMixins() {
return this.candidates.containsValue(Match.NONE);
}

record AuditLog(@Nullable String originalMethod, List<Pair<Object, StringBuilder>> entries) {
public static AuditLog create(MethodContext methodContext) {
return new AuditLog(methodContext.getMixinMethod().name + methodContext.getMixinMethod().desc, new ArrayList<>());
}
}

record Candidate(ClassNode classNode, MethodNode methodNode) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,28 @@
import org.jetbrains.annotations.Nullable;
import org.sinytra.adapter.patch.analysis.InheritanceHandler;
import org.sinytra.adapter.patch.api.MixinClassGenerator;
import org.sinytra.adapter.patch.api.PatchAuditTrail;
import org.sinytra.adapter.patch.api.PatchEnvironment;
import org.sinytra.adapter.patch.api.RefmapHolder;
import org.sinytra.adapter.patch.fixes.BytecodeFixerUpper;
import org.sinytra.adapter.patch.util.provider.ClassLookup;
import org.sinytra.adapter.patch.util.provider.MixinClassLookup;

public record PatchEnvironmentImpl(RefmapHolder refmapHolder, ClassLookup cleanClassLookup, ClassLookup dirtyClassLookup, @Nullable BytecodeFixerUpper bytecodeFixerUpper,
MixinClassGenerator classGenerator, InheritanceHandler inheritanceHandler, int fabricLVTCompatibility) implements PatchEnvironment {
public record PatchEnvironmentImpl(
RefmapHolder refmapHolder,
ClassLookup cleanClassLookup, ClassLookup dirtyClassLookup,
@Nullable BytecodeFixerUpper bytecodeFixerUpper,
MixinClassGenerator classGenerator,
InheritanceHandler inheritanceHandler,
int fabricLVTCompatibility,
PatchAuditTrail auditTrail
) implements PatchEnvironment {

public PatchEnvironmentImpl(RefmapHolder refmapHolder, ClassLookup cleanClassLookup, @Nullable BytecodeFixerUpper bytecodeFixerUpper, int fabricLVTCompatibility) {
this(refmapHolder, cleanClassLookup, MixinClassLookup.INSTANCE, bytecodeFixerUpper, new MixinClassGeneratorImpl(), new InheritanceHandler(MixinClassLookup.INSTANCE), fabricLVTCompatibility);
this(refmapHolder, cleanClassLookup, MixinClassLookup.INSTANCE, bytecodeFixerUpper, new MixinClassGeneratorImpl(), new InheritanceHandler(MixinClassLookup.INSTANCE), fabricLVTCompatibility, new PatchAuditTrailImpl());
}

public PatchEnvironmentImpl(RefmapHolder refmapHolder, ClassLookup cleanClassLookup, ClassLookup dirtyClassLookup, @Nullable BytecodeFixerUpper bytecodeFixerUpper, int fabricLVTCompatibility) {
this(refmapHolder, cleanClassLookup, dirtyClassLookup, bytecodeFixerUpper, new MixinClassGeneratorImpl(), new InheritanceHandler(MixinClassLookup.INSTANCE), fabricLVTCompatibility);
this(refmapHolder, cleanClassLookup, dirtyClassLookup, bytecodeFixerUpper, new MixinClassGeneratorImpl(), new InheritanceHandler(MixinClassLookup.INSTANCE), fabricLVTCompatibility, new PatchAuditTrailImpl());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ public Result apply(ClassNode classNode, PatchEnvironment environment) {
for (MethodNode method : classNode.methods) {
MethodContext methodContext = checkMethodTarget(classAnnotation, classNode, method, environment, classTarget.targetTypes(), context);
if (methodContext != null) {
if (!this.transforms.isEmpty()) {
environment.auditTrail().prepareMethod(methodContext);
}
for (MethodTransform transform : this.transforms) {
Collection<String> accepted = transform.getAcceptedAnnotations();
if (accepted.isEmpty() || accepted.contains(methodContext.methodAnnotation().getDesc())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public interface MethodContext {
@Nullable
Pair<ClassNode, List<MethodNode>> findInjectionTargetCandidates(ClassLookup lookup, boolean ignoreDesc);

void updateDescription(List<Type> parameters);
void updateDescription(MethodTransform transform, List<Type> parameters);

boolean isStatic();

Expand All @@ -80,6 +80,8 @@ default List<LocalVariable> getTargetMethodLocals(TargetPair target, int startPo

boolean hasInjectionPointValue(String value);

void recordAudit(Object transform, String message, Object... args);

record LocalVariable(int index, Type type) {}

record TargetPair(ClassNode classNode, MethodNode methodNode) {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,6 @@ public Result or(Result other) {
}
return this;
}

public Result orElseGet(Supplier<Result> other) {
return this == PASS ? other.get() : this;
}
}

interface Builder<T extends Builder<T>> extends MethodTransformBuilder<T> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.sinytra.adapter.patch.api;

import org.objectweb.asm.tree.ClassNode;

public interface PatchAuditTrail {
void prepareMethod(MethodContext methodContext);

void recordAudit(Object transform, ClassNode classNode, String message, Object... args);

void recordAudit(Object transform, MethodContext methodContext, String message, Object... args);

void recordResult(MethodContext methodContext, Match match);

String getCompleteReport();

boolean hasFailingMixins();

enum Match {
NONE,
PARTIAL,
FULL;

public Match or(Match other) {
if (this == NONE && other != NONE) {
return other;
}
if (this == PARTIAL && other == FULL) {
return FULL;
}
return this;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,6 @@ static PatchEnvironment create(RefmapHolder refmapHolder, ClassLookup cleanClass
RefmapHolder refmapHolder();

int fabricLVTCompatibility();

PatchAuditTrail auditTrail();
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
package org.sinytra.adapter.patch.fixes;

import com.google.common.collect.ImmutableList;
import com.mojang.datafixers.util.Pair;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.sinytra.adapter.patch.analysis.LocalVarAnalyzer;
import org.sinytra.adapter.patch.analysis.params.EnhancedParamsDiff;
import org.sinytra.adapter.patch.analysis.params.LayeredParamsDiffSnapshot;
import org.sinytra.adapter.patch.analysis.params.SimpleParamsDiffSnapshot;
import org.sinytra.adapter.patch.analysis.selector.AnnotationHandle;
import org.sinytra.adapter.patch.analysis.selector.AnnotationValueHandle;
import org.sinytra.adapter.patch.api.MethodContext;
import org.sinytra.adapter.patch.api.MethodTransform;
import org.sinytra.adapter.patch.api.MixinConstants;
import org.sinytra.adapter.patch.analysis.selector.AnnotationValueHandle;
import org.sinytra.adapter.patch.transformer.operation.param.ParamTransformTarget;
import org.sinytra.adapter.patch.transformer.operation.param.ParameterTransformer;
import org.sinytra.adapter.patch.transformer.operation.param.TransformParameters;
import org.sinytra.adapter.patch.util.AdapterUtil;
import org.sinytra.adapter.patch.util.MethodQualifier;

import java.util.Comparator;
import java.util.List;

public final class MethodUpgrader {
Expand All @@ -32,10 +36,47 @@ public static void upgradeMethod(MethodNode methodNode, MethodContext methodCont
if (dirtyQualifier == null) {
return;
}
if (methodContext.methodAnnotation().matchesDesc(MixinConstants.MODIFY_ARGS)) {
AnnotationHandle annotation = methodContext.methodAnnotation();
if (annotation.matchesDesc(MixinConstants.MODIFY_ARGS)) {
ModifyArgsOffsetTransformer.handleModifiedDesc(methodNode, cleanQualifier.desc(), dirtyQualifier.desc());
} else if (methodContext.methodAnnotation().matchesDesc(MixinConstants.WRAP_OPERATION)) {
} else if (annotation.matchesDesc(MixinConstants.WRAP_OPERATION)) {
upgradeWrapOperation(methodNode, methodContext, cleanQualifier, dirtyQualifier);
} else if (annotation.matchesDesc(MixinConstants.MODIFY_EXPR_VAL)) {
upgradeModifyExpValue(methodNode, methodContext, cleanQualifier, dirtyQualifier);
}
}

private static void upgradeModifyExpValue(MethodNode methodNode, MethodContext methodContext, MethodQualifier cleanQualifier, MethodQualifier dirtyQualifier) {
if (dirtyQualifier.desc() == null || cleanQualifier.desc() == null) {
return;
}
List<Type> originalTargetDesc = List.of(Type.getArgumentTypes(cleanQualifier.desc()));
List<Type> modifiedTargetDesc = List.of(Type.getArgumentTypes(dirtyQualifier.desc()));
List<Type> originalDesc = List.of(Type.getArgumentTypes(methodNode.desc));
final List<Type> originalDescRef = originalDesc;
List<Pair<AnnotationNode, Type>> localAnnotations = AdapterUtil.getAnnotatedParameters(methodNode, originalDesc.toArray(Type[]::new), MixinConstants.LOCAL, Pair::of).stream()
.sorted(Comparator.comparingInt(i -> originalDescRef.indexOf(i.getSecond())))
.toList();
if (!localAnnotations.isEmpty()) {
originalDesc = originalDesc.subList(0, originalDesc.indexOf(localAnnotations.getFirst().getSecond()));
}
if (originalDesc.size() == 1) {
return;
}

int capturedParams = Math.min(originalTargetDesc.size(), originalDesc.size() - 1);
int popParams = originalTargetDesc.size() - capturedParams;
List<Type> modifiedDesc = ImmutableList.<Type>builder()
// Add return object parameter
.add(originalDesc.getFirst())
// Add target parameters
.addAll(modifiedTargetDesc.subList(0, modifiedTargetDesc.size() - popParams))
.build();
// Create diff
SimpleParamsDiffSnapshot diff = EnhancedParamsDiff.create(originalDesc, modifiedDesc);
if (!diff.isEmpty()) {
MethodTransform patch = diff.asParameterTransformer(ParamTransformTarget.ALL, false, false);
patch.apply(methodContext);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,21 @@

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.mojang.logging.LogUtils;
import org.jetbrains.annotations.Nullable;
import org.objectweb.asm.tree.*;
import org.sinytra.adapter.patch.analysis.selector.AnnotationValueHandle;
import org.sinytra.adapter.patch.api.ClassTransform;
import org.sinytra.adapter.patch.api.MixinConstants;
import org.sinytra.adapter.patch.api.Patch;
import org.sinytra.adapter.patch.api.PatchContext;
import org.sinytra.adapter.patch.analysis.selector.AnnotationValueHandle;
import org.sinytra.adapter.patch.util.AdapterUtil;
import org.slf4j.Logger;

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

public class DynamicAnonymousShadowFieldTypePatch implements ClassTransform {
private static final Logger LOGGER = LogUtils.getLogger();

@Override
public Patch.Result apply(ClassNode classNode, @Nullable AnnotationValueHandle<?> annotation, PatchContext context) {
if (annotation == null || !annotation.getKey().equals("targets")) {
Expand Down Expand Up @@ -64,7 +60,7 @@ public Patch.Result apply(ClassNode classNode, @Nullable AnnotationValueHandle<?
}

if (!renames.isEmpty()) {
renames.forEach((from, to) -> LOGGER.info("Renaming anonymous class field {}.{} to {}", classNode.name, from, to));
renames.forEach((from, to) -> context.environment().auditTrail().recordAudit(this, classNode, "Rename anonymous class field %s to %s", from, to));
for (MethodNode method : classNode.methods) {
for (AbstractInsnNode insn : method.instructions) {
if (insn instanceof FieldInsnNode finsn && finsn.owner.equals(classNode.name)) {
Expand Down
Loading

0 comments on commit ab5ba82

Please sign in to comment.