Skip to content

Commit c7a9dda

Browse files
committed
Begin work on basic transformer system
Will be expanded upon later with some built-in implementations for basic deobfuscation capabilities
1 parent 3147acf commit c7a9dda

12 files changed

+999
-4
lines changed

recaf-core/src/main/java/software/coley/recaf/config/ConfigGroups.java

+4
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ public final class ConfigGroups {
5252
* Group for plugin components.
5353
*/
5454
public static final String SERVICE_PLUGIN = SERVICE + PACKAGE_SPLIT + "plugin";
55+
/**
56+
* Group for transformation components.
57+
*/
58+
public static final String SERVICE_TRANSFORM = SERVICE + PACKAGE_SPLIT + "transform";
5559
/**
5660
* Group base for UI classes.
5761
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package software.coley.recaf.services.transform;
2+
3+
import jakarta.annotation.Nonnull;
4+
import org.objectweb.asm.tree.ClassNode;
5+
import software.coley.recaf.info.JvmClassInfo;
6+
import software.coley.recaf.workspace.model.Workspace;
7+
import software.coley.recaf.workspace.model.bundle.JvmClassBundle;
8+
import software.coley.recaf.workspace.model.resource.WorkspaceResource;
9+
10+
import java.util.Collections;
11+
import java.util.Set;
12+
13+
/**
14+
* Outlines the base JVM transformation contract.
15+
*
16+
* @author Matt Coley
17+
*/
18+
public interface JvmClassTransformer {
19+
/**
20+
* Implementations can {@link #dependencies() depend on other transformers} and access them
21+
* via {@link JvmTransformerContext#getJvmTransformer(Class)}. This may be useful in cases where you want to have
22+
* one transformer act as a shared data-storage between multiple transformers.
23+
* <p>
24+
* To record changes to the given {@code classInfo} you can:
25+
* <ul>
26+
* <li>Record a {@link ClassNode} via {@link JvmTransformerContext#setNode(JvmClassBundle, JvmClassInfo, ClassNode)}</li>
27+
* <li>Record bytecode via {@link JvmTransformerContext#setBytecode(JvmClassBundle, JvmClassInfo, byte[])}</li>
28+
* </ul>
29+
*
30+
* @param context
31+
* Transformation context for access to other transformers and recording class changes.
32+
* @param workspace
33+
* Workspace containing the class.
34+
* @param resource
35+
* Resource containing the class.
36+
* @param bundle
37+
* Bundle containing the class.
38+
* @param classInfo
39+
* The class to transform.
40+
*
41+
* @throws TransformationException
42+
* When the class cannot be transformed for any reason.
43+
*/
44+
void transform(@Nonnull JvmTransformerContext context, @Nonnull Workspace workspace,
45+
@Nonnull WorkspaceResource resource, @Nonnull JvmClassBundle bundle,
46+
@Nonnull JvmClassInfo classInfo) throws TransformationException;
47+
48+
/**
49+
* @return Name of the transformer.
50+
*/
51+
@Nonnull
52+
String name();
53+
54+
/**
55+
* @return Set of transformer classes that must run before this one.
56+
*/
57+
@Nonnull
58+
default Set<Class<? extends JvmClassTransformer>> dependencies() {
59+
return Collections.emptySet();
60+
}
61+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package software.coley.recaf.services.transform;
2+
3+
import jakarta.annotation.Nonnull;
4+
import software.coley.recaf.info.JvmClassInfo;
5+
import software.coley.recaf.workspace.model.Workspace;
6+
import software.coley.recaf.workspace.model.bundle.JvmClassBundle;
7+
import software.coley.recaf.workspace.model.resource.WorkspaceResource;
8+
9+
/**
10+
* Predicate for preventing transformation of classes in {@link TransformationApplier}.
11+
*
12+
* @author Matt Coley
13+
*/
14+
public interface JvmClassTransformerPredicate {
15+
/**
16+
* @param workspace
17+
* Workspace containing the class.
18+
* @param resource
19+
* Resource containing the class.
20+
* @param bundle
21+
* Bundle containing the class.
22+
* @param classInfo
23+
* The class to transform.
24+
*
25+
* @return {@code true} to allow the class to be transformed.
26+
* {@code false} to skip transforming the given class.
27+
*/
28+
boolean shouldTransform(@Nonnull Workspace workspace, @Nonnull WorkspaceResource resource,
29+
@Nonnull JvmClassBundle bundle, @Nonnull JvmClassInfo classInfo);
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
package software.coley.recaf.services.transform;
2+
3+
import jakarta.annotation.Nonnull;
4+
import org.objectweb.asm.ClassReader;
5+
import org.objectweb.asm.ClassWriter;
6+
import org.objectweb.asm.tree.ClassNode;
7+
import software.coley.recaf.info.JvmClassInfo;
8+
import software.coley.recaf.services.inheritance.InheritanceGraph;
9+
import software.coley.recaf.util.visitors.WorkspaceClassWriter;
10+
import software.coley.recaf.workspace.model.bundle.JvmClassBundle;
11+
12+
import java.util.Arrays;
13+
import java.util.Collection;
14+
import java.util.Collections;
15+
import java.util.IdentityHashMap;
16+
import java.util.Map;
17+
import java.util.concurrent.ConcurrentHashMap;
18+
19+
/**
20+
* Context for holding a number of JVM class transformers and shared state for transformation.
21+
*
22+
* @author Matt Coley
23+
*/
24+
public class JvmTransformerContext {
25+
private final Map<Class<? extends JvmClassTransformer>, JvmClassTransformer> transformerMap;
26+
private final Map<String, JvmClassData> classData = new ConcurrentHashMap<>();
27+
28+
/**
29+
* Constructs a new context from an array of transformers.
30+
*
31+
* @param transformers
32+
* Transformers to associate with this context.
33+
*/
34+
public JvmTransformerContext(@Nonnull JvmClassTransformer... transformers) {
35+
this.transformerMap = buildMap(transformers);
36+
}
37+
38+
/**
39+
* Constructs a new context from a collection of transformers.
40+
*
41+
* @param transformers
42+
* Transformers to associate with this context.
43+
*/
44+
public JvmTransformerContext(@Nonnull Collection<? extends JvmClassTransformer> transformers) {
45+
this.transformerMap = buildMap(transformers);
46+
}
47+
48+
/**
49+
* Apply any of the recorded changes within this context to the associated workspace.
50+
*
51+
* @param graph
52+
* Inheritance graph tied to the workspace the transformed classes belong to.
53+
*/
54+
protected void applyChanges(@Nonnull InheritanceGraph graph) {
55+
for (JvmClassData data : classData.values()) {
56+
if (data.isDirty()) {
57+
if (data.node != null) {
58+
// Emit bytecode from the current node
59+
ClassWriter writer = new WorkspaceClassWriter(graph, data.initialClass.getClassReader(), 0);
60+
data.node.accept(writer);
61+
byte[] modifiedBytes = writer.toByteArray();
62+
63+
// Update workspace
64+
JvmClassInfo modifiedClass = data.initialClass.toJvmClassBuilder()
65+
.adaptFrom(modifiedBytes)
66+
.build();
67+
data.bundle.put(modifiedClass);
68+
} else {
69+
// Update workspace if the bytecode is not the same as the initial state
70+
byte[] bytecode = data.getBytecode();
71+
if (!Arrays.equals(bytecode, data.initialClass.getBytecode()))
72+
data.bundle.put(data.initialClass.toJvmClassBuilder()
73+
.adaptFrom(bytecode)
74+
.build());
75+
}
76+
}
77+
}
78+
}
79+
80+
/**
81+
* Gets the current ASM node representation of the given class.
82+
* Transformers can update the <i>"current"</i> state of the node via
83+
* {@link #setNode(JvmClassBundle, JvmClassInfo, ClassNode)}.
84+
*
85+
* @param bundle
86+
* Bundle containing the class.
87+
* @param info
88+
* The class's model in the workspace.
89+
*
90+
* @return The current tracked/transformed {@link ClassNode} for the associated class.
91+
*
92+
* @see #setNode(JvmClassBundle, JvmClassInfo, ClassNode)
93+
*/
94+
@Nonnull
95+
public ClassNode getNode(@Nonnull JvmClassBundle bundle, @Nonnull JvmClassInfo info) {
96+
return getJvmClassData(bundle, info).getOrCreateNode();
97+
}
98+
99+
/**
100+
* Gets the current bytecode of the given class.
101+
* Transformers can update the <i>"current"</i> state of the bytecode via
102+
* {@link #setBytecode(JvmClassBundle, JvmClassInfo, byte[])}.
103+
*
104+
* @param bundle
105+
* Bundle containing the class.
106+
* @param info
107+
* The class's model in the workspace.
108+
*
109+
* @return The current tracked/transformed bytecode for the associated class.
110+
*
111+
* @see #setBytecode(JvmClassBundle, JvmClassInfo, byte[])
112+
*/
113+
@Nonnull
114+
public byte[] getBytecode(@Nonnull JvmClassBundle bundle, @Nonnull JvmClassInfo info) {
115+
return getJvmClassData(bundle, info).getBytecode();
116+
}
117+
118+
/**
119+
* Updates the transformed state of a class by recording an ASM node representation of the class.
120+
* Once the transformation application is completed, the latest value recorded here
121+
* <i>(or in {@link #setBytecode(JvmClassBundle, JvmClassInfo, byte[])}</i> will be dumped into the workspace.
122+
*
123+
* @param bundle
124+
* Bundle containing the class.
125+
* @param info
126+
* The class's model in the workspace.
127+
* @param node
128+
* ASM node representation of the class to store.
129+
*
130+
* @see #getNode(JvmClassBundle, JvmClassInfo)
131+
*/
132+
public void setNode(@Nonnull JvmClassBundle bundle, @Nonnull JvmClassInfo info, @Nonnull ClassNode node) {
133+
getJvmClassData(bundle, info).setNode(node);
134+
}
135+
136+
/**
137+
* Updates the transformed state of a class by recording new bytecode of the class.
138+
* Once the transformation application is completed, the latest value recorded here
139+
* <i>(or in {@link #setNode(JvmClassBundle, JvmClassInfo, ClassNode)})</i> will be dumped into the workspace.
140+
*
141+
* @param bundle
142+
* Bundle containing the class.
143+
* @param info
144+
* The class's model in the workspace.
145+
* @param bytecode
146+
* Bytecode of the class to store.
147+
*
148+
* @see #getBytecode(JvmClassBundle, JvmClassInfo)
149+
*/
150+
public void setBytecode(@Nonnull JvmClassBundle bundle, @Nonnull JvmClassInfo info, @Nonnull byte[] bytecode) {
151+
getJvmClassData(bundle, info).setBytecode(bytecode);
152+
}
153+
154+
/**
155+
* Clears any transformations applied to the given class.
156+
*
157+
* @param bundle
158+
* Bundle containing the class.
159+
* @param info
160+
* The class's model in the workspace.
161+
*/
162+
public void clear(@Nonnull JvmClassBundle bundle, @Nonnull JvmClassInfo info) {
163+
JvmClassData data = getJvmClassData(bundle, info);
164+
data.setBytecode(data.initialClass.getBytecode());
165+
data.dirty = false;
166+
}
167+
168+
/**
169+
* @param key
170+
* Transformer class.
171+
* @param <T>
172+
* Transformer type.
173+
*
174+
* @return Shared instance of the transformer within this context.
175+
*
176+
* @throws TransformationException
177+
* When the transformer was not found within this context.
178+
*/
179+
@Nonnull
180+
@SuppressWarnings("unchecked")
181+
public <T extends JvmClassTransformer> T getJvmTransformer(Class<T> key) throws TransformationException {
182+
JvmClassTransformer transformer = transformerMap.get(key);
183+
if (transformer == null)
184+
throw new TransformationException("Transformation context attempted lookup of class '"
185+
+ key.getSimpleName() + "' but did not have an associated entry");
186+
return (T) transformer;
187+
}
188+
189+
@Nonnull
190+
private JvmClassData getJvmClassData(@Nonnull JvmClassBundle bundle, @Nonnull JvmClassInfo info) {
191+
return classData.computeIfAbsent(info.getName(), ignored -> new JvmClassData(bundle, info));
192+
}
193+
194+
@Nonnull
195+
private static Map<Class<? extends JvmClassTransformer>, JvmClassTransformer> buildMap(@Nonnull JvmClassTransformer[] transformers) {
196+
return buildMap(Arrays.asList(transformers));
197+
}
198+
199+
@Nonnull
200+
private static Map<Class<? extends JvmClassTransformer>, JvmClassTransformer> buildMap(@Nonnull Collection<? extends JvmClassTransformer> transformers) {
201+
Map<Class<? extends JvmClassTransformer>, JvmClassTransformer> map = new IdentityHashMap<>();
202+
for (JvmClassTransformer transformer : transformers)
203+
map.put(transformer.getClass(), transformer);
204+
return Collections.unmodifiableMap(map);
205+
}
206+
207+
/**
208+
* Container of per-class transformation state.
209+
*/
210+
private static class JvmClassData {
211+
private final JvmClassBundle bundle;
212+
private final JvmClassInfo initialClass;
213+
private byte[] bytecode;
214+
private volatile ClassNode node;
215+
private boolean dirty;
216+
217+
/**
218+
* @param bundle
219+
* Bundle containing the class.
220+
* @param initialClass
221+
* Initial state of the class before transformation.
222+
*/
223+
public JvmClassData(@Nonnull JvmClassBundle bundle, @Nonnull JvmClassInfo initialClass) {
224+
this.initialClass = initialClass;
225+
this.bundle = bundle;
226+
bytecode = initialClass.getBytecode();
227+
}
228+
229+
/**
230+
* @return Node representation of the {@link #getBytecode() current bytecode}.
231+
*/
232+
@Nonnull
233+
public ClassNode getOrCreateNode() {
234+
if (node == null) {
235+
synchronized (this) {
236+
if (node == null) {
237+
node = new ClassNode();
238+
new ClassReader(bytecode).accept(node, 0);
239+
}
240+
}
241+
}
242+
return node;
243+
}
244+
245+
/**
246+
* The current bytecode of the class as set by {@link JvmTransformerContext#setBytecode(JvmClassBundle, JvmClassInfo, byte[])}.
247+
* This value does not update when using {@link JvmTransformerContext#setNode(JvmClassBundle, JvmClassInfo, ClassNode)}.
248+
*
249+
* @return Current bytecode of the class.
250+
*/
251+
@Nonnull
252+
public byte[] getBytecode() {
253+
return bytecode;
254+
}
255+
256+
/**
257+
* @param node
258+
* Current node representation to set for this class.
259+
*/
260+
public void setNode(@Nonnull ClassNode node) {
261+
this.node = node;
262+
dirty = true;
263+
}
264+
265+
/**
266+
* @param bytecode
267+
* Current bytecode to set for this class.
268+
*/
269+
public void setBytecode(@Nonnull byte[] bytecode) {
270+
this.bytecode = bytecode;
271+
node = null; // Invalidate node state
272+
dirty = true;
273+
}
274+
275+
/**
276+
* @return {@code true} when changes have been applied to this class.
277+
*/
278+
public boolean isDirty() {
279+
return dirty;
280+
}
281+
}
282+
}

0 commit comments

Comments
 (0)