summaryrefslogtreecommitdiff
path: root/src/main/java/cuchaz/enigma/EnigmaProject.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/cuchaz/enigma/EnigmaProject.java')
-rw-r--r--src/main/java/cuchaz/enigma/EnigmaProject.java285
1 files changed, 285 insertions, 0 deletions
diff --git a/src/main/java/cuchaz/enigma/EnigmaProject.java b/src/main/java/cuchaz/enigma/EnigmaProject.java
new file mode 100644
index 0000000..82fc0bd
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/EnigmaProject.java
@@ -0,0 +1,285 @@
1package cuchaz.enigma;
2
3import com.google.common.base.Functions;
4import com.strobel.assembler.metadata.ITypeLoader;
5import com.strobel.assembler.metadata.MetadataSystem;
6import com.strobel.decompiler.DecompilerSettings;
7import com.strobel.decompiler.languages.java.ast.CompilationUnit;
8import cuchaz.enigma.analysis.ClassCache;
9import cuchaz.enigma.analysis.EntryReference;
10import cuchaz.enigma.analysis.index.JarIndex;
11import cuchaz.enigma.bytecode.translators.SourceFixVisitor;
12import cuchaz.enigma.bytecode.translators.TranslationClassVisitor;
13import cuchaz.enigma.translation.Translator;
14import cuchaz.enigma.translation.mapping.EntryMapping;
15import cuchaz.enigma.translation.mapping.EntryRemapper;
16import cuchaz.enigma.translation.mapping.MappingsChecker;
17import cuchaz.enigma.translation.mapping.tree.DeltaTrackingTree;
18import cuchaz.enigma.translation.mapping.tree.EntryTree;
19import cuchaz.enigma.translation.representation.entry.ClassEntry;
20import cuchaz.enigma.translation.representation.entry.Entry;
21import cuchaz.enigma.translation.representation.entry.LocalVariableEntry;
22import cuchaz.enigma.translation.representation.entry.MethodEntry;
23import org.objectweb.asm.ClassWriter;
24import org.objectweb.asm.Opcodes;
25import org.objectweb.asm.tree.ClassNode;
26
27import java.io.BufferedWriter;
28import java.io.IOException;
29import java.io.StringWriter;
30import java.nio.file.Files;
31import java.nio.file.Path;
32import java.util.Collection;
33import java.util.Map;
34import java.util.Objects;
35import java.util.concurrent.atomic.AtomicInteger;
36import java.util.jar.JarEntry;
37import java.util.jar.JarOutputStream;
38import java.util.stream.Collectors;
39
40// TODO: Naming?
41public class EnigmaProject {
42 private final Enigma enigma;
43
44 private final ClassCache classCache;
45 private final JarIndex jarIndex;
46
47 private EntryRemapper mapper;
48
49 public EnigmaProject(Enigma enigma, ClassCache classCache, JarIndex jarIndex) {
50 this.enigma = enigma;
51 this.classCache = classCache;
52 this.jarIndex = jarIndex;
53
54 this.mapper = EntryRemapper.empty(jarIndex);
55 }
56
57 public void setMappings(EntryTree<EntryMapping> mappings) {
58 if (mappings != null) {
59 mapper = EntryRemapper.mapped(jarIndex, mappings);
60 } else {
61 mapper = EntryRemapper.empty(jarIndex);
62 }
63 }
64
65 public Enigma getEnigma() {
66 return enigma;
67 }
68
69 public ClassCache getClassCache() {
70 return classCache;
71 }
72
73 public JarIndex getJarIndex() {
74 return jarIndex;
75 }
76
77 public EntryRemapper getMapper() {
78 return mapper;
79 }
80
81 public void dropMappings(ProgressListener progress) {
82 DeltaTrackingTree<EntryMapping> mappings = mapper.getObfToDeobf();
83
84 Collection<Entry<?>> dropped = dropMappings(mappings, progress);
85 for (Entry<?> entry : dropped) {
86 mappings.trackChange(entry);
87 }
88 }
89
90 private Collection<Entry<?>> dropMappings(EntryTree<EntryMapping> mappings, ProgressListener progress) {
91 // drop mappings that don't match the jar
92 MappingsChecker checker = new MappingsChecker(jarIndex, mappings);
93 MappingsChecker.Dropped dropped = checker.dropBrokenMappings(progress);
94
95 Map<Entry<?>, String> droppedMappings = dropped.getDroppedMappings();
96 for (Map.Entry<Entry<?>, String> mapping : droppedMappings.entrySet()) {
97 System.out.println("WARNING: Couldn't find " + mapping.getKey() + " (" + mapping.getValue() + ") in jar. Mapping was dropped.");
98 }
99
100 return droppedMappings.keySet();
101 }
102
103 public boolean isRenamable(Entry<?> obfEntry) {
104 if (obfEntry instanceof MethodEntry) {
105 // HACKHACK: Object methods are not obfuscated identifiers
106 MethodEntry obfMethodEntry = (MethodEntry) obfEntry;
107 String name = obfMethodEntry.getName();
108 String sig = obfMethodEntry.getDesc().toString();
109 if (name.equals("clone") && sig.equals("()Ljava/lang/Object;")) {
110 return false;
111 } else if (name.equals("equals") && sig.equals("(Ljava/lang/Object;)Z")) {
112 return false;
113 } else if (name.equals("finalize") && sig.equals("()V")) {
114 return false;
115 } else if (name.equals("getClass") && sig.equals("()Ljava/lang/Class;")) {
116 return false;
117 } else if (name.equals("hashCode") && sig.equals("()I")) {
118 return false;
119 } else if (name.equals("notify") && sig.equals("()V")) {
120 return false;
121 } else if (name.equals("notifyAll") && sig.equals("()V")) {
122 return false;
123 } else if (name.equals("toString") && sig.equals("()Ljava/lang/String;")) {
124 return false;
125 } else if (name.equals("wait") && sig.equals("()V")) {
126 return false;
127 } else if (name.equals("wait") && sig.equals("(J)V")) {
128 return false;
129 } else if (name.equals("wait") && sig.equals("(JI)V")) {
130 return false;
131 }
132 } else if (obfEntry instanceof LocalVariableEntry && !((LocalVariableEntry) obfEntry).isArgument()) {
133 return false;
134 }
135
136 return this.jarIndex.getEntryIndex().hasEntry(obfEntry);
137 }
138
139 public boolean isRenamable(EntryReference<Entry<?>, Entry<?>> obfReference) {
140 return obfReference.isNamed() && isRenamable(obfReference.getNameableEntry());
141 }
142
143 public JarExport exportRemappedJar(ProgressListener progress) {
144 Collection<ClassEntry> classEntries = jarIndex.getEntryIndex().getClasses();
145 Translator deobfuscator = mapper.getDeobfuscator();
146
147 AtomicInteger count = new AtomicInteger();
148 progress.init(classEntries.size(), "Deobfuscating classes...");
149
150 Map<String, ClassNode> compiled = classEntries.parallelStream()
151 .map(entry -> {
152 ClassEntry translatedEntry = deobfuscator.translate(entry);
153 progress.step(count.getAndIncrement(), translatedEntry.toString());
154
155 ClassNode node = classCache.getClassNode(entry.getFullName());
156 if (node != null) {
157 ClassNode translatedNode = new ClassNode();
158 node.accept(new TranslationClassVisitor(deobfuscator, Opcodes.ASM5, translatedNode));
159 return translatedNode;
160 }
161
162 return null;
163 })
164 .filter(Objects::nonNull)
165 .collect(Collectors.toMap(n -> n.name, Functions.identity()));
166
167 return new JarExport(jarIndex, compiled);
168 }
169
170 public static final class JarExport {
171 private final JarIndex jarIndex;
172 private final Map<String, ClassNode> compiled;
173
174 JarExport(JarIndex jarIndex, Map<String, ClassNode> compiled) {
175 this.jarIndex = jarIndex;
176 this.compiled = compiled;
177 }
178
179 public void write(Path path, ProgressListener progress) throws IOException {
180 progress.init(this.compiled.size(), "Writing jar...");
181
182 try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(path))) {
183 AtomicInteger count = new AtomicInteger();
184
185 for (ClassNode node : this.compiled.values()) {
186 progress.step(count.getAndIncrement(), node.name);
187
188 String entryName = node.name.replace('.', '/') + ".class";
189
190 ClassWriter writer = new ClassWriter(0);
191 node.accept(writer);
192
193 out.putNextEntry(new JarEntry(entryName));
194 out.write(writer.toByteArray());
195 out.closeEntry();
196 }
197 }
198 }
199
200 public SourceExport decompile(ProgressListener progress) {
201 Collection<ClassNode> classes = this.compiled.values().stream()
202 .filter(classNode -> classNode.name.indexOf('$') == -1)
203 .collect(Collectors.toList());
204
205 progress.init(classes.size(), "Decompiling classes...");
206
207 //create a common instance outside the loop as mappings shouldn't be changing while this is happening
208 CompiledSourceTypeLoader typeLoader = new CompiledSourceTypeLoader(this.compiled::get);
209 typeLoader.addVisitor(visitor -> new SourceFixVisitor(Opcodes.ASM5, visitor, jarIndex));
210
211 //synchronized to make sure the parallelStream doesn't CME with the cache
212 ITypeLoader synchronizedTypeLoader = new SynchronizedTypeLoader(typeLoader);
213
214 MetadataSystem metadataSystem = new NoRetryMetadataSystem(synchronizedTypeLoader);
215
216 //ensures methods are loaded on classload and prevents race conditions
217 metadataSystem.setEagerMethodLoadingEnabled(true);
218
219 DecompilerSettings settings = SourceProvider.createSettings();
220 SourceProvider sourceProvider = new SourceProvider(settings, synchronizedTypeLoader, metadataSystem);
221
222 AtomicInteger count = new AtomicInteger();
223
224 Collection<ClassSource> decompiled = classes.parallelStream()
225 .map(translatedNode -> {
226 progress.step(count.getAndIncrement(), translatedNode.name);
227
228 String source = decompileClass(translatedNode, sourceProvider);
229 return new ClassSource(translatedNode.name, source);
230 })
231 .collect(Collectors.toList());
232
233 return new SourceExport(decompiled);
234 }
235
236 private String decompileClass(ClassNode translatedNode, SourceProvider sourceProvider) {
237 CompilationUnit sourceTree = sourceProvider.getSources(translatedNode.name);
238
239 StringWriter writer = new StringWriter();
240 sourceProvider.writeSource(writer, sourceTree);
241
242 return writer.toString();
243 }
244 }
245
246 public static final class SourceExport {
247 private final Collection<ClassSource> decompiled;
248
249 SourceExport(Collection<ClassSource> decompiled) {
250 this.decompiled = decompiled;
251 }
252
253 public void write(Path path, ProgressListener progress) throws IOException {
254 progress.init(decompiled.size(), "Writing sources...");
255
256 int count = 0;
257 for (ClassSource source : decompiled) {
258 progress.step(count++, source.name);
259
260 Path sourcePath = source.resolvePath(path);
261 source.writeTo(sourcePath);
262 }
263 }
264 }
265
266 private static class ClassSource {
267 private final String name;
268 private final String source;
269
270 ClassSource(String name, String source) {
271 this.name = name;
272 this.source = source;
273 }
274
275 void writeTo(Path path) throws IOException {
276 try (BufferedWriter writer = Files.newBufferedWriter(path)) {
277 writer.write(source);
278 }
279 }
280
281 Path resolvePath(Path root) {
282 return root.resolve(name.replace('.', '/') + ".java");
283 }
284 }
285}