Skip to content

Commit 365bee4

Browse files
committed
wip: pure function analysis
1 parent 52eb0ec commit 365bee4

34 files changed

+2487
-0
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
plugins {
2+
id 'java'
3+
}
4+
5+
group = 'dev.skidfuscator.community'
6+
version = '2.0.0-SNAPSHOT'
7+
8+
repositories {
9+
mavenCentral()
10+
}
11+
12+
dependencies {
13+
api project(':modasm')
14+
15+
testImplementation platform('org.junit:junit-bom:5.10.0')
16+
testImplementation 'org.junit.jupiter:junit-jupiter'
17+
}
18+
19+
test {
20+
useJUnitPlatform()
21+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package dev.skidfuscator.pureanalysis;
2+
3+
import org.objectweb.asm.ClassReader;
4+
import org.objectweb.asm.tree.ClassNode;
5+
6+
import java.io.IOException;
7+
import java.util.HashSet;
8+
import java.util.Set;
9+
import java.util.concurrent.ConcurrentHashMap;
10+
11+
public class ClassHierarchyAnalyzer {
12+
private final ConcurrentHashMap<String, ClassNode> classCache = new ConcurrentHashMap<>();
13+
private final ClassLoader classLoader;
14+
private final Set<String> analyzedClasses = ConcurrentHashMap.newKeySet();
15+
16+
public ClassHierarchyAnalyzer(ClassLoader classLoader) {
17+
this.classLoader = classLoader;
18+
}
19+
20+
public ClassNode getClass(String className) throws IOException {
21+
return classCache.computeIfAbsent(className, this::loadClass);
22+
}
23+
24+
private ClassNode loadClass(String className) {
25+
try {
26+
ClassReader reader = new ClassReader(classLoader.getResourceAsStream(
27+
className.replace('.', '/') + ".class"));
28+
ClassNode classNode = new ClassNode();
29+
reader.accept(classNode, ClassReader.EXPAND_FRAMES);
30+
return classNode;
31+
} catch (IOException e) {
32+
throw new RuntimeException("Failed to load class: " + className, e);
33+
}
34+
}
35+
36+
public Set<String> getAllSuperClasses(String className) throws IOException {
37+
Set<String> superClasses = new HashSet<>();
38+
collectSuperClasses(className, superClasses);
39+
return superClasses;
40+
}
41+
42+
private void collectSuperClasses(String className, Set<String> collected) throws IOException {
43+
if (className == null || className.equals("java/lang/Object")) return;
44+
45+
ClassNode classNode = getClass(className);
46+
collected.add(className);
47+
48+
// Collect superclass
49+
if (classNode.superName != null) {
50+
collectSuperClasses(classNode.superName, collected);
51+
}
52+
53+
// Collect interfaces
54+
for (String interfaceName : classNode.interfaces) {
55+
collectSuperClasses(interfaceName, collected);
56+
}
57+
}
58+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package dev.skidfuscator.pureanalysis;
2+
3+
import dev.skidfuscator.pureanalysis.condition.PurityCondition;
4+
import org.objectweb.asm.tree.ClassNode;
5+
import org.objectweb.asm.tree.MethodNode;
6+
7+
import java.io.IOException;
8+
import java.util.ArrayList;
9+
import java.util.HashSet;
10+
import java.util.List;
11+
import java.util.Set;
12+
import java.util.concurrent.ConcurrentHashMap;
13+
14+
public class PurityAnalyzer {
15+
private final ConcurrentHashMap<String, Boolean> pureClasses = new ConcurrentHashMap<>();
16+
private final ConcurrentHashMap<String, Boolean> methodPurityCache = new ConcurrentHashMap<>();
17+
private final ClassHierarchyAnalyzer hierarchyAnalyzer;
18+
private final List<PurityCondition> conditions;
19+
20+
// ThreadLocal set to track methods being analyzed in the current thread
21+
private final ThreadLocal<Set<String>> methodsUnderAnalysis = ThreadLocal.withInitial(HashSet::new);
22+
23+
public PurityAnalyzer(ClassLoader classLoader) {
24+
this.hierarchyAnalyzer = new ClassHierarchyAnalyzer(classLoader);
25+
this.conditions = new ArrayList<>();
26+
}
27+
28+
public void addCondition(PurityCondition condition) {
29+
conditions.add(condition);
30+
}
31+
32+
public void registerPureClass(String className) {
33+
pureClasses.put(className, true);
34+
}
35+
36+
public boolean isPureClass(String className) {
37+
return pureClasses.getOrDefault(className, false);
38+
}
39+
40+
public boolean isPureMethod(String owner, String name, String desc) {
41+
String key = owner + "." + name + desc;
42+
43+
// If the method is currently being analyzed, assume it's pure to break recursion
44+
if (methodsUnderAnalysis.get().contains(key)) {
45+
return true;
46+
}
47+
48+
return methodPurityCache.getOrDefault(key, false);
49+
}
50+
51+
public boolean analyzeMethod(MethodNode method, ClassNode classNode) {
52+
String methodKey = classNode.name + "." + method.name + method.desc;
53+
54+
// If the method is already cached, return the cached result
55+
Boolean cachedResult = methodPurityCache.get(methodKey);
56+
if (cachedResult != null) {
57+
return cachedResult;
58+
}
59+
60+
// If we're already analyzing this method, return true to break recursion
61+
Set<String> currentMethods = methodsUnderAnalysis.get();
62+
if (currentMethods.contains(methodKey)) {
63+
return true;
64+
}
65+
66+
// Add this method to the set of methods being analyzed
67+
currentMethods.add(methodKey);
68+
69+
try {
70+
// Evaluate all conditions
71+
boolean isPure = true;
72+
for (PurityCondition condition : conditions) {
73+
boolean result = condition.evaluateAndPrint(method, classNode, this);
74+
if (!result) {
75+
isPure = false;
76+
break;
77+
}
78+
}
79+
80+
// Cache the result
81+
methodPurityCache.put(methodKey, isPure);
82+
return isPure;
83+
} finally {
84+
// Remove this method from the set of methods being analyzed
85+
currentMethods.remove(methodKey);
86+
if (currentMethods.isEmpty()) {
87+
methodsUnderAnalysis.remove();
88+
}
89+
}
90+
}
91+
92+
public ClassHierarchyAnalyzer getHierarchyAnalyzer() {
93+
return hierarchyAnalyzer;
94+
}
95+
96+
private final Set<String> analyzedClasses = ConcurrentHashMap.newKeySet();
97+
98+
public void analyzeClass(String className) throws IOException {
99+
if (analyzedClasses.contains(className)) {
100+
return;
101+
}
102+
103+
ClassNode classNode = hierarchyAnalyzer.getClass(className);
104+
105+
// Analyze all methods in the class
106+
for (MethodNode method : classNode.methods) {
107+
analyzeMethod(method, classNode);
108+
}
109+
110+
analyzedClasses.add(className);
111+
}
112+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package dev.skidfuscator.pureanalysis.condition;
2+
3+
import dev.skidfuscator.pureanalysis.PurityAnalyzer;
4+
import org.objectweb.asm.tree.ClassNode;
5+
import org.objectweb.asm.tree.MethodNode;
6+
7+
import java.util.ArrayList;
8+
import java.util.List;
9+
10+
public class CompositeCondition extends PurityCondition {
11+
public enum Operation { AND, OR }
12+
13+
private final List<PurityCondition> conditions = new ArrayList<>();
14+
private final Operation operation;
15+
16+
public CompositeCondition(Operation operation) {
17+
super("Composite");
18+
this.operation = operation;
19+
}
20+
21+
public void addCondition(PurityCondition condition) {
22+
conditions.add(condition);
23+
}
24+
25+
@Override
26+
public boolean evaluate(MethodNode method, ClassNode classNode, PurityAnalyzer analyzer) {
27+
if (conditions.isEmpty()) return true;
28+
29+
boolean result = operation == Operation.AND;
30+
for (PurityCondition condition : conditions) {
31+
if (operation == Operation.AND) {
32+
result &= condition.evaluateAndPrint(method, classNode, analyzer);
33+
if (!result) break; // Short circuit AND
34+
} else {
35+
result |= condition.evaluateAndPrint(method, classNode, analyzer);
36+
if (result) break; // Short circuit OR
37+
}
38+
}
39+
return result && evaluateNested(method, classNode, analyzer);
40+
}
41+
42+
@Override
43+
public String getName() {
44+
return "Composite: \n" + conditions.stream().map(PurityCondition::getName).reduce((a, b) -> a + "\n" + b).orElse("");
45+
}
46+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package dev.skidfuscator.pureanalysis.condition;
2+
3+
import dev.skidfuscator.pureanalysis.PurityAnalyzer;
4+
import org.objectweb.asm.tree.ClassNode;
5+
import org.objectweb.asm.tree.MethodNode;
6+
7+
import java.util.ArrayList;
8+
import java.util.List;
9+
10+
public abstract class PurityCondition {
11+
private List<PurityCondition> nestedConditions = new ArrayList<>();
12+
private final String name;
13+
14+
public PurityCondition(String name) {
15+
this.name = name;
16+
}
17+
18+
public String getName() {
19+
return name;
20+
}
21+
22+
public void addNestedCondition(PurityCondition condition) {
23+
nestedConditions.add(condition);
24+
}
25+
26+
protected boolean evaluateNested(MethodNode method, ClassNode classNode, PurityAnalyzer analyzer) {
27+
return nestedConditions.isEmpty() ||
28+
nestedConditions.stream()
29+
.allMatch(c -> c.evaluateAndPrint(method, classNode, analyzer));
30+
}
31+
32+
public boolean evaluateAndPrint(MethodNode method, ClassNode classNode, PurityAnalyzer analyzer) {
33+
boolean output = evaluate(method, classNode, analyzer);
34+
35+
if (!output) {
36+
System.out.println("Failed condition: " + getName() + " for " + classNode.name + "." + method.name + method.desc);
37+
}
38+
39+
return output;
40+
}
41+
42+
public abstract boolean evaluate(MethodNode method, ClassNode classNode, PurityAnalyzer analyzer);
43+
}

0 commit comments

Comments
 (0)