Skip to content

Commit

Permalink
wip: pure function analysis
Browse files Browse the repository at this point in the history
  • Loading branch information
terminalsin committed Nov 19, 2024
1 parent 52eb0ec commit 365bee4
Show file tree
Hide file tree
Showing 34 changed files with 2,487 additions and 0 deletions.
21 changes: 21 additions & 0 deletions dev.skidfuscator.obfuscator.pureanalysis/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
plugins {
id 'java'
}

group = 'dev.skidfuscator.community'
version = '2.0.0-SNAPSHOT'

repositories {
mavenCentral()
}

dependencies {
api project(':modasm')

testImplementation platform('org.junit:junit-bom:5.10.0')
testImplementation 'org.junit.jupiter:junit-jupiter'
}

test {
useJUnitPlatform()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package dev.skidfuscator.pureanalysis;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.tree.ClassNode;

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

public class ClassHierarchyAnalyzer {
private final ConcurrentHashMap<String, ClassNode> classCache = new ConcurrentHashMap<>();
private final ClassLoader classLoader;
private final Set<String> analyzedClasses = ConcurrentHashMap.newKeySet();

public ClassHierarchyAnalyzer(ClassLoader classLoader) {
this.classLoader = classLoader;
}

public ClassNode getClass(String className) throws IOException {
return classCache.computeIfAbsent(className, this::loadClass);
}

private ClassNode loadClass(String className) {
try {
ClassReader reader = new ClassReader(classLoader.getResourceAsStream(
className.replace('.', '/') + ".class"));
ClassNode classNode = new ClassNode();
reader.accept(classNode, ClassReader.EXPAND_FRAMES);
return classNode;
} catch (IOException e) {
throw new RuntimeException("Failed to load class: " + className, e);
}
}

public Set<String> getAllSuperClasses(String className) throws IOException {
Set<String> superClasses = new HashSet<>();
collectSuperClasses(className, superClasses);
return superClasses;
}

private void collectSuperClasses(String className, Set<String> collected) throws IOException {
if (className == null || className.equals("java/lang/Object")) return;

ClassNode classNode = getClass(className);
collected.add(className);

// Collect superclass
if (classNode.superName != null) {
collectSuperClasses(classNode.superName, collected);
}

// Collect interfaces
for (String interfaceName : classNode.interfaces) {
collectSuperClasses(interfaceName, collected);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package dev.skidfuscator.pureanalysis;

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

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 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 void registerPureClass(String className) {
pureClasses.put(className, true);
}

public boolean isPureClass(String className) {
return pureClasses.getOrDefault(className, false);
}

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;
}
}

// 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();
}
}
}

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,46 @@
package dev.skidfuscator.pureanalysis.condition;

import dev.skidfuscator.pureanalysis.PurityAnalyzer;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodNode;

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

public class CompositeCondition extends PurityCondition {
public enum Operation { AND, OR }

private final List<PurityCondition> conditions = new ArrayList<>();
private final Operation operation;

public CompositeCondition(Operation operation) {
super("Composite");
this.operation = operation;
}

public void addCondition(PurityCondition condition) {
conditions.add(condition);
}

@Override
public boolean evaluate(MethodNode method, ClassNode classNode, PurityAnalyzer analyzer) {
if (conditions.isEmpty()) return true;

boolean result = operation == Operation.AND;
for (PurityCondition condition : conditions) {
if (operation == Operation.AND) {
result &= condition.evaluateAndPrint(method, classNode, analyzer);
if (!result) break; // Short circuit AND
} else {
result |= condition.evaluateAndPrint(method, classNode, analyzer);
if (result) break; // Short circuit OR
}
}
return result && evaluateNested(method, classNode, analyzer);
}

@Override
public String getName() {
return "Composite: \n" + conditions.stream().map(PurityCondition::getName).reduce((a, b) -> a + "\n" + b).orElse("");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package dev.skidfuscator.pureanalysis.condition;

import dev.skidfuscator.pureanalysis.PurityAnalyzer;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodNode;

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

public abstract class PurityCondition {
private List<PurityCondition> nestedConditions = new ArrayList<>();
private final String name;

public PurityCondition(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void addNestedCondition(PurityCondition condition) {
nestedConditions.add(condition);
}

protected boolean evaluateNested(MethodNode method, ClassNode classNode, PurityAnalyzer analyzer) {
return nestedConditions.isEmpty() ||
nestedConditions.stream()
.allMatch(c -> c.evaluateAndPrint(method, classNode, analyzer));
}

public boolean evaluateAndPrint(MethodNode method, ClassNode classNode, PurityAnalyzer analyzer) {
boolean output = evaluate(method, classNode, analyzer);

if (!output) {
System.out.println("Failed condition: " + getName() + " for " + classNode.name + "." + method.name + method.desc);
}

return output;
}

public abstract boolean evaluate(MethodNode method, ClassNode classNode, PurityAnalyzer analyzer);
}
Loading

0 comments on commit 365bee4

Please sign in to comment.