Skip to content

Commit

Permalink
wip: functional pure function analysis
Browse files Browse the repository at this point in the history
  • Loading branch information
terminalsin committed Nov 19, 2024
1 parent 365bee4 commit ee95cde
Show file tree
Hide file tree
Showing 30 changed files with 557 additions and 1,433 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package dev.skidfuscator.pureanalysis;

import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodNode;

public abstract class Analyzer {
protected final PurityContext context;
protected final PurityAnalyzer analyzer;
protected final String name;

protected Analyzer(String name, PurityContext context, PurityAnalyzer analyzer) {
this.name = name;
this.context = context;
this.analyzer = analyzer;
}

public abstract PurityReport analyze(Context ctx);

protected PurityReport pure() {
return new PurityReport(true, name, null, null);
}

protected PurityReport impure(String reason, AbstractInsnNode insn) {
return new PurityReport(false, name, reason, insn);
}

protected PurityReport impure(String reason) {
return new PurityReport(false, name, reason, null);
}

public static class Context {
MethodNode method;
ClassNode classNode;

public Context(MethodNode method, ClassNode classNode) {
this.method = method;
this.classNode = classNode;
}

public MethodNode method() {
return method;
}

public ClassNode parent() {
return classNode;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package dev.skidfuscator.pureanalysis;

public enum Purity {
IMPURE,
PURE,
MUD;

boolean isPure() {
return this == PURE || this == MUD;
}
}
Original file line number Diff line number Diff line change
@@ -1,112 +1,50 @@
package dev.skidfuscator.pureanalysis;

import dev.skidfuscator.pureanalysis.condition.PurityCondition;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodNode;
import dev.skidfuscator.pureanalysis.impl.*;
import org.objectweb.asm.tree.*;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

public class PurityAnalyzer {
private final ConcurrentHashMap<String, Boolean> pureClasses = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, Boolean> methodPurityCache = new ConcurrentHashMap<>();
private final Set<Analyzer> analyzers = new HashSet<>();
private final PurityContext context;
private final ClassHierarchyAnalyzer hierarchyAnalyzer;
private final List<PurityCondition> conditions;

// ThreadLocal set to track methods being analyzed in the current thread
private final ThreadLocal<Set<String>> methodsUnderAnalysis = ThreadLocal.withInitial(HashSet::new);

public PurityAnalyzer(ClassLoader classLoader) {
this.hierarchyAnalyzer = new ClassHierarchyAnalyzer(classLoader);
this.conditions = new ArrayList<>();
}

public void addCondition(PurityCondition condition) {
conditions.add(condition);
public PurityAnalyzer(ClassHierarchyAnalyzer hierarchyAnalyzer) {
this.hierarchyAnalyzer = hierarchyAnalyzer;
this.context = new PurityContext(this);
initializeAnalyzers();
}

public void registerPureClass(String className) {
pureClasses.put(className, true);
public PurityContext getContext() {
return context;
}

public boolean isPureClass(String className) {
return pureClasses.getOrDefault(className, false);
private void initializeAnalyzers() {
analyzers.add(new TypeInstructionAnalyzer(context, this));
analyzers.add(new MethodInstructionAnalyzer(context, this));
analyzers.add(new FieldInstructionAnalyzer(context, this));
analyzers.add(new DynamicInstructionAnalyzer(context, this));
analyzers.add(new NativeMethodAnalyzer(context, this));
analyzers.add(new PrimitiveParametersAnalyzer(context, this));
}

public boolean isPureMethod(String owner, String name, String desc) {
String key = owner + "." + name + desc;

// If the method is currently being analyzed, assume it's pure to break recursion
if (methodsUnderAnalysis.get().contains(key)) {
return true;
}

return methodPurityCache.getOrDefault(key, false);
}

public boolean analyzeMethod(MethodNode method, ClassNode classNode) {
String methodKey = classNode.name + "." + method.name + method.desc;

// If the method is already cached, return the cached result
Boolean cachedResult = methodPurityCache.get(methodKey);
if (cachedResult != null) {
return cachedResult;
}

// If we're already analyzing this method, return true to break recursion
Set<String> currentMethods = methodsUnderAnalysis.get();
if (currentMethods.contains(methodKey)) {
return true;
}

// Add this method to the set of methods being analyzed
currentMethods.add(methodKey);

try {
// Evaluate all conditions
boolean isPure = true;
for (PurityCondition condition : conditions) {
boolean result = condition.evaluateAndPrint(method, classNode, this);
if (!result) {
isPure = false;
break;
}
}
public PurityReport analyzeMethodPurity(MethodNode method, ClassNode classNode) {
final PurityReport report = new PurityReport(true, "Method Analysis", null, null);
final Analyzer.Context methodCtx = new Analyzer.Context(
method,
classNode
);

// Cache the result
methodPurityCache.put(methodKey, isPure);
return isPure;
} finally {
// Remove this method from the set of methods being analyzed
currentMethods.remove(methodKey);
if (currentMethods.isEmpty()) {
methodsUnderAnalysis.remove();
}
for (Analyzer analyzer : analyzers) {
report.addNested(analyzer.analyze(methodCtx));
}

return report;
}

public ClassHierarchyAnalyzer getHierarchyAnalyzer() {
return hierarchyAnalyzer;
}

private final Set<String> analyzedClasses = ConcurrentHashMap.newKeySet();

public void analyzeClass(String className) throws IOException {
if (analyzedClasses.contains(className)) {
return;
}

ClassNode classNode = hierarchyAnalyzer.getClass(className);

// Analyze all methods in the class
for (MethodNode method : classNode.methods) {
analyzeMethod(method, classNode);
}

analyzedClasses.add(className);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package dev.skidfuscator.pureanalysis;

import java.util.*;

import org.objectweb.asm.tree.ClassNode;

public class PurityContext {
private final Set<String> impureObjects = new HashSet<>();
private final Map<String, Purity> pureStaticMethods = new HashMap<>();
private final Map<String, Purity> pureMethods = new HashMap<>();
private final Map<String, Set<String>> hierarchyCache = new HashMap<>();

private final PurityAnalyzer analyzer;

public PurityContext(PurityAnalyzer analyzer) {
this.analyzer = analyzer;
}

private Set<String> computeHierarchy(String type) {
try {
Set<String> hierarchy = new HashSet<>();
String current = type;

while (current != null && !current.equals("java/lang/Object")) {
hierarchy.add(current);
ClassNode classNode = analyzer.getHierarchyAnalyzer().getClass(current);
current = classNode.superName;

// Add interfaces
for (String iface : classNode.interfaces) {
hierarchy.add(iface);
// Recursively add interface hierarchies
hierarchy.addAll(getHierarchy(iface));
}
}

return hierarchy;
} catch (Exception e) {
return Collections.singleton(type);
}
}

public Set<String> getHierarchy(String type) {
return hierarchyCache.computeIfAbsent(type, this::computeHierarchy);
}

public void markImpure(String type) {
impureObjects.add(type);
}

public boolean isPure(String type) {
return !impureObjects.contains(type);
}

public boolean isPureStaticMethod(String signature) {
return pureStaticMethods.getOrDefault(signature, Purity.MUD).isPure();
}

public void addPureStaticMethod(String signature) {
pureStaticMethods.put(signature, Purity.PURE);
}

public boolean isPureMethods(String signature) {
return pureMethods.getOrDefault(signature, Purity.MUD).isPure();
}

public void addPureMethods(String signature) {
pureMethods.put(signature, Purity.PURE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package dev.skidfuscator.pureanalysis;

import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;

import java.util.ArrayList;
import java.util.List;

public class PurityReport {
private final boolean pure;
private final String condition;
private final String reason;
private final AbstractInsnNode failedInsn;
private final List<PurityReport> nested = new ArrayList<>();

public PurityReport(boolean pure, String condition, String reason, AbstractInsnNode failedInsn) {
this.pure = pure;
this.condition = condition;
this.reason = reason;
this.failedInsn = failedInsn;
}

public void addNested(PurityReport report) {
nested.add(report);
}

public boolean isPure() {
return pure && nested.stream().allMatch(PurityReport::isPure);
}

@Override
public String toString() {
StringBuilder sb = new StringBuilder();
toString(sb, 0);
return sb.toString();
}

private void toString(StringBuilder sb, int depth) {
String indent = " ".repeat(depth);
sb.append(indent).append(condition).append(": ").append(pure ? "PURE" : "IMPURE");
if (!pure && reason != null) {
sb.append("\n").append(indent).append("Reason: ").append(reason);
if (failedInsn != null) {
sb.append("\n").append(indent).append("At instruction: ").append(formatInstruction(failedInsn));
}
}
for (PurityReport nested : this.nested) {
sb.append("\n");
nested.toString(sb, depth + 1);
}
}

private String formatInstruction(AbstractInsnNode insn) {
if (insn instanceof MethodInsnNode) {
MethodInsnNode min = (MethodInsnNode) insn;
return String.format("%s.%s%s", min.owner, min.name, min.desc);
}
if (insn instanceof FieldInsnNode) {
FieldInsnNode fin = (FieldInsnNode) insn;
return String.format("%s.%s:%s", fin.owner, fin.name, fin.desc);
}
return insn.toString();
}
}

This file was deleted.

Loading

0 comments on commit ee95cde

Please sign in to comment.