Skip to content

Commit ee95cde

Browse files
committed
wip: functional pure function analysis
1 parent 365bee4 commit ee95cde

30 files changed

Lines changed: 557 additions & 1433 deletions
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package dev.skidfuscator.pureanalysis;
2+
3+
import org.objectweb.asm.tree.AbstractInsnNode;
4+
import org.objectweb.asm.tree.ClassNode;
5+
import org.objectweb.asm.tree.MethodNode;
6+
7+
public abstract class Analyzer {
8+
protected final PurityContext context;
9+
protected final PurityAnalyzer analyzer;
10+
protected final String name;
11+
12+
protected Analyzer(String name, PurityContext context, PurityAnalyzer analyzer) {
13+
this.name = name;
14+
this.context = context;
15+
this.analyzer = analyzer;
16+
}
17+
18+
public abstract PurityReport analyze(Context ctx);
19+
20+
protected PurityReport pure() {
21+
return new PurityReport(true, name, null, null);
22+
}
23+
24+
protected PurityReport impure(String reason, AbstractInsnNode insn) {
25+
return new PurityReport(false, name, reason, insn);
26+
}
27+
28+
protected PurityReport impure(String reason) {
29+
return new PurityReport(false, name, reason, null);
30+
}
31+
32+
public static class Context {
33+
MethodNode method;
34+
ClassNode classNode;
35+
36+
public Context(MethodNode method, ClassNode classNode) {
37+
this.method = method;
38+
this.classNode = classNode;
39+
}
40+
41+
public MethodNode method() {
42+
return method;
43+
}
44+
45+
public ClassNode parent() {
46+
return classNode;
47+
}
48+
}
49+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package dev.skidfuscator.pureanalysis;
2+
3+
public enum Purity {
4+
IMPURE,
5+
PURE,
6+
MUD;
7+
8+
boolean isPure() {
9+
return this == PURE || this == MUD;
10+
}
11+
}
Lines changed: 27 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,50 @@
11
package dev.skidfuscator.pureanalysis;
22

3-
import dev.skidfuscator.pureanalysis.condition.PurityCondition;
4-
import org.objectweb.asm.tree.ClassNode;
5-
import org.objectweb.asm.tree.MethodNode;
3+
import dev.skidfuscator.pureanalysis.impl.*;
4+
import org.objectweb.asm.tree.*;
65

7-
import java.io.IOException;
8-
import java.util.ArrayList;
96
import java.util.HashSet;
10-
import java.util.List;
117
import java.util.Set;
12-
import java.util.concurrent.ConcurrentHashMap;
138

149
public class PurityAnalyzer {
15-
private final ConcurrentHashMap<String, Boolean> pureClasses = new ConcurrentHashMap<>();
16-
private final ConcurrentHashMap<String, Boolean> methodPurityCache = new ConcurrentHashMap<>();
10+
private final Set<Analyzer> analyzers = new HashSet<>();
11+
private final PurityContext context;
1712
private final ClassHierarchyAnalyzer hierarchyAnalyzer;
18-
private final List<PurityCondition> conditions;
1913

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);
14+
public PurityAnalyzer(ClassHierarchyAnalyzer hierarchyAnalyzer) {
15+
this.hierarchyAnalyzer = hierarchyAnalyzer;
16+
this.context = new PurityContext(this);
17+
initializeAnalyzers();
3018
}
3119

32-
public void registerPureClass(String className) {
33-
pureClasses.put(className, true);
20+
public PurityContext getContext() {
21+
return context;
3422
}
3523

36-
public boolean isPureClass(String className) {
37-
return pureClasses.getOrDefault(className, false);
24+
private void initializeAnalyzers() {
25+
analyzers.add(new TypeInstructionAnalyzer(context, this));
26+
analyzers.add(new MethodInstructionAnalyzer(context, this));
27+
analyzers.add(new FieldInstructionAnalyzer(context, this));
28+
analyzers.add(new DynamicInstructionAnalyzer(context, this));
29+
analyzers.add(new NativeMethodAnalyzer(context, this));
30+
analyzers.add(new PrimitiveParametersAnalyzer(context, this));
3831
}
3932

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-
}
33+
public PurityReport analyzeMethodPurity(MethodNode method, ClassNode classNode) {
34+
final PurityReport report = new PurityReport(true, "Method Analysis", null, null);
35+
final Analyzer.Context methodCtx = new Analyzer.Context(
36+
method,
37+
classNode
38+
);
7939

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-
}
40+
for (Analyzer analyzer : analyzers) {
41+
report.addNested(analyzer.analyze(methodCtx));
8942
}
43+
44+
return report;
9045
}
9146

9247
public ClassHierarchyAnalyzer getHierarchyAnalyzer() {
9348
return hierarchyAnalyzer;
9449
}
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-
}
11250
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package dev.skidfuscator.pureanalysis;
2+
3+
import java.util.*;
4+
5+
import org.objectweb.asm.tree.ClassNode;
6+
7+
public class PurityContext {
8+
private final Set<String> impureObjects = new HashSet<>();
9+
private final Map<String, Purity> pureStaticMethods = new HashMap<>();
10+
private final Map<String, Purity> pureMethods = new HashMap<>();
11+
private final Map<String, Set<String>> hierarchyCache = new HashMap<>();
12+
13+
private final PurityAnalyzer analyzer;
14+
15+
public PurityContext(PurityAnalyzer analyzer) {
16+
this.analyzer = analyzer;
17+
}
18+
19+
private Set<String> computeHierarchy(String type) {
20+
try {
21+
Set<String> hierarchy = new HashSet<>();
22+
String current = type;
23+
24+
while (current != null && !current.equals("java/lang/Object")) {
25+
hierarchy.add(current);
26+
ClassNode classNode = analyzer.getHierarchyAnalyzer().getClass(current);
27+
current = classNode.superName;
28+
29+
// Add interfaces
30+
for (String iface : classNode.interfaces) {
31+
hierarchy.add(iface);
32+
// Recursively add interface hierarchies
33+
hierarchy.addAll(getHierarchy(iface));
34+
}
35+
}
36+
37+
return hierarchy;
38+
} catch (Exception e) {
39+
return Collections.singleton(type);
40+
}
41+
}
42+
43+
public Set<String> getHierarchy(String type) {
44+
return hierarchyCache.computeIfAbsent(type, this::computeHierarchy);
45+
}
46+
47+
public void markImpure(String type) {
48+
impureObjects.add(type);
49+
}
50+
51+
public boolean isPure(String type) {
52+
return !impureObjects.contains(type);
53+
}
54+
55+
public boolean isPureStaticMethod(String signature) {
56+
return pureStaticMethods.getOrDefault(signature, Purity.MUD).isPure();
57+
}
58+
59+
public void addPureStaticMethod(String signature) {
60+
pureStaticMethods.put(signature, Purity.PURE);
61+
}
62+
63+
public boolean isPureMethods(String signature) {
64+
return pureMethods.getOrDefault(signature, Purity.MUD).isPure();
65+
}
66+
67+
public void addPureMethods(String signature) {
68+
pureMethods.put(signature, Purity.PURE);
69+
}
70+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package dev.skidfuscator.pureanalysis;
2+
3+
import org.objectweb.asm.tree.AbstractInsnNode;
4+
import org.objectweb.asm.tree.FieldInsnNode;
5+
import org.objectweb.asm.tree.MethodInsnNode;
6+
7+
import java.util.ArrayList;
8+
import java.util.List;
9+
10+
public class PurityReport {
11+
private final boolean pure;
12+
private final String condition;
13+
private final String reason;
14+
private final AbstractInsnNode failedInsn;
15+
private final List<PurityReport> nested = new ArrayList<>();
16+
17+
public PurityReport(boolean pure, String condition, String reason, AbstractInsnNode failedInsn) {
18+
this.pure = pure;
19+
this.condition = condition;
20+
this.reason = reason;
21+
this.failedInsn = failedInsn;
22+
}
23+
24+
public void addNested(PurityReport report) {
25+
nested.add(report);
26+
}
27+
28+
public boolean isPure() {
29+
return pure && nested.stream().allMatch(PurityReport::isPure);
30+
}
31+
32+
@Override
33+
public String toString() {
34+
StringBuilder sb = new StringBuilder();
35+
toString(sb, 0);
36+
return sb.toString();
37+
}
38+
39+
private void toString(StringBuilder sb, int depth) {
40+
String indent = " ".repeat(depth);
41+
sb.append(indent).append(condition).append(": ").append(pure ? "PURE" : "IMPURE");
42+
if (!pure && reason != null) {
43+
sb.append("\n").append(indent).append("Reason: ").append(reason);
44+
if (failedInsn != null) {
45+
sb.append("\n").append(indent).append("At instruction: ").append(formatInstruction(failedInsn));
46+
}
47+
}
48+
for (PurityReport nested : this.nested) {
49+
sb.append("\n");
50+
nested.toString(sb, depth + 1);
51+
}
52+
}
53+
54+
private String formatInstruction(AbstractInsnNode insn) {
55+
if (insn instanceof MethodInsnNode) {
56+
MethodInsnNode min = (MethodInsnNode) insn;
57+
return String.format("%s.%s%s", min.owner, min.name, min.desc);
58+
}
59+
if (insn instanceof FieldInsnNode) {
60+
FieldInsnNode fin = (FieldInsnNode) insn;
61+
return String.format("%s.%s:%s", fin.owner, fin.name, fin.desc);
62+
}
63+
return insn.toString();
64+
}
65+
}

dev.skidfuscator.obfuscator.pureanalysis/src/main/java/dev/skidfuscator/pureanalysis/condition/CompositeCondition.java

Lines changed: 0 additions & 46 deletions
This file was deleted.

0 commit comments

Comments
 (0)