summaryrefslogtreecommitdiff
path: root/src/cuchaz/enigma/convert/ClassMatcher.java
diff options
context:
space:
mode:
authorGravatar jeff2015-02-08 21:29:25 -0500
committerGravatar jeff2015-02-08 21:29:25 -0500
commited9b5cdfc648e86fd463bfa8d86b94c41671e14c (patch)
tree2619bbc7e04dfa3b82f8dfd3b1d31f529766cd4b /src/cuchaz/enigma/convert/ClassMatcher.java
downloadenigma-fork-ed9b5cdfc648e86fd463bfa8d86b94c41671e14c.tar.gz
enigma-fork-ed9b5cdfc648e86fd463bfa8d86b94c41671e14c.tar.xz
enigma-fork-ed9b5cdfc648e86fd463bfa8d86b94c41671e14c.zip
switch all classes to new signature/type system
Diffstat (limited to 'src/cuchaz/enigma/convert/ClassMatcher.java')
-rw-r--r--src/cuchaz/enigma/convert/ClassMatcher.java406
1 files changed, 406 insertions, 0 deletions
diff --git a/src/cuchaz/enigma/convert/ClassMatcher.java b/src/cuchaz/enigma/convert/ClassMatcher.java
new file mode 100644
index 0000000..ccf6b78
--- /dev/null
+++ b/src/cuchaz/enigma/convert/ClassMatcher.java
@@ -0,0 +1,406 @@
1/*******************************************************************************
2 * Copyright (c) 2014 Jeff Martin.
3 * All rights reserved. This program and the accompanying materials
4 * are made available under the terms of the GNU Public License v3.0
5 * which accompanies this distribution, and is available at
6 * http://www.gnu.org/licenses/gpl.html
7 *
8 * Contributors:
9 * Jeff Martin - initial API and implementation
10 ******************************************************************************/
11package cuchaz.enigma.convert;
12
13import java.io.File;
14import java.io.FileReader;
15import java.io.FileWriter;
16import java.io.IOException;
17import java.util.ArrayList;
18import java.util.Arrays;
19import java.util.Collection;
20import java.util.Collections;
21import java.util.Comparator;
22import java.util.Iterator;
23import java.util.LinkedHashMap;
24import java.util.List;
25import java.util.Map;
26import java.util.Set;
27import java.util.jar.JarFile;
28
29import javassist.CtBehavior;
30import javassist.CtClass;
31
32import com.google.common.collect.ArrayListMultimap;
33import com.google.common.collect.BiMap;
34import com.google.common.collect.HashBiMap;
35import com.google.common.collect.Lists;
36import com.google.common.collect.Maps;
37import com.google.common.collect.Multimap;
38import com.google.common.collect.Sets;
39
40import cuchaz.enigma.TranslatingTypeLoader;
41import cuchaz.enigma.analysis.JarIndex;
42import cuchaz.enigma.convert.ClassNamer.SidedClassNamer;
43import cuchaz.enigma.mapping.ClassEntry;
44import cuchaz.enigma.mapping.ClassMapping;
45import cuchaz.enigma.mapping.JavassistUtil;
46import cuchaz.enigma.mapping.MappingParseException;
47import cuchaz.enigma.mapping.Mappings;
48import cuchaz.enigma.mapping.MappingsReader;
49import cuchaz.enigma.mapping.MappingsWriter;
50import cuchaz.enigma.mapping.MethodEntry;
51import cuchaz.enigma.mapping.MethodMapping;
52
53public class ClassMatcher {
54
55 public static void main(String[] args) throws IOException, MappingParseException {
56 // TEMP
57 JarFile sourceJar = new JarFile(new File("input/1.8-pre3.jar"));
58 JarFile destJar = new JarFile(new File("input/1.8.jar"));
59 File inMappingsFile = new File("../Enigma Mappings/1.8-pre3.mappings");
60 File outMappingsFile = new File("../Enigma Mappings/1.8.mappings");
61
62 // define a matching to use when the automated system cannot find a match
63 Map<String,String> fallbackMatching = Maps.newHashMap();
64 fallbackMatching.put("none/ayb", "none/ayf");
65 fallbackMatching.put("none/ayd", "none/ayd");
66 fallbackMatching.put("none/bgk", "unknown/bgk");
67
68 // do the conversion
69 Mappings mappings = new MappingsReader().read(new FileReader(inMappingsFile));
70 convertMappings(sourceJar, destJar, mappings, fallbackMatching);
71
72 // write out the converted mappings
73 FileWriter writer = new FileWriter(outMappingsFile);
74 new MappingsWriter().write(writer, mappings);
75 writer.close();
76 System.out.println("Wrote converted mappings to:\n\t" + outMappingsFile.getAbsolutePath());
77 }
78
79 private static void convertMappings(JarFile sourceJar, JarFile destJar, Mappings mappings, Map<String,String> fallbackMatching) {
80 // index jars
81 System.out.println("Indexing source jar...");
82 JarIndex sourceIndex = new JarIndex();
83 sourceIndex.indexJar(sourceJar, false);
84 System.out.println("Indexing dest jar...");
85 JarIndex destIndex = new JarIndex();
86 destIndex.indexJar(destJar, false);
87 TranslatingTypeLoader sourceLoader = new TranslatingTypeLoader(sourceJar, sourceIndex);
88 TranslatingTypeLoader destLoader = new TranslatingTypeLoader(destJar, destIndex);
89
90 // compute the matching
91 ClassMatching matching = computeMatching(sourceIndex, sourceLoader, destIndex, destLoader);
92 Map<String,Map.Entry<ClassIdentity,List<ClassIdentity>>> matchingIndex = matching.getIndex();
93
94 // get all the obf class names used in the mappings
95 Set<String> usedClassNames = mappings.getAllObfClassNames();
96 Set<String> allClassNames = Sets.newHashSet();
97 for (ClassEntry classEntry : sourceIndex.getObfClassEntries()) {
98 allClassNames.add(classEntry.getName());
99 }
100 usedClassNames.retainAll(allClassNames);
101 System.out.println("Used " + usedClassNames.size() + " classes in the mappings");
102
103 // probabilistically match the non-uniquely-matched source classes
104 for (Map.Entry<ClassIdentity,List<ClassIdentity>> entry : matchingIndex.values()) {
105 ClassIdentity sourceClass = entry.getKey();
106 List<ClassIdentity> destClasses = entry.getValue();
107
108 // skip classes that are uniquely matched
109 if (destClasses.size() == 1) {
110 continue;
111 }
112
113 // skip classes that aren't used in the mappings
114 if (!usedClassNames.contains(sourceClass.getClassEntry().getName())) {
115 continue;
116 }
117
118 System.out.println("No exact match for source class " + sourceClass.getClassEntry());
119
120 // find the closest classes
121 Multimap<Integer,ClassIdentity> scoredMatches = ArrayListMultimap.create();
122 for (ClassIdentity c : destClasses) {
123 scoredMatches.put(sourceClass.getMatchScore(c), c);
124 }
125 List<Integer> scores = new ArrayList<Integer>(scoredMatches.keySet());
126 Collections.sort(scores, Collections.reverseOrder());
127 printScoredMatches(sourceClass.getMaxMatchScore(), scores, scoredMatches);
128
129 // does the best match have a non-zero score and the same name?
130 int bestScore = scores.get(0);
131 Collection<ClassIdentity> bestMatches = scoredMatches.get(bestScore);
132 if (bestScore > 0 && bestMatches.size() == 1) {
133 ClassIdentity bestMatch = bestMatches.iterator().next();
134 if (bestMatch.getClassEntry().equals(sourceClass.getClassEntry())) {
135 // use it
136 System.out.println("\tAutomatically choosing likely match: " + bestMatch.getClassEntry().getName());
137 destClasses.clear();
138 destClasses.add(bestMatch);
139 }
140 }
141 }
142
143 // group the matching into unique and non-unique matches
144 BiMap<String,String> matchedClassNames = HashBiMap.create();
145 Set<String> unmatchedSourceClassNames = Sets.newHashSet();
146 for (String className : usedClassNames) {
147 // is there a match for this class?
148 Map.Entry<ClassIdentity,List<ClassIdentity>> entry = matchingIndex.get(className);
149 ClassIdentity sourceClass = entry.getKey();
150 List<ClassIdentity> matches = entry.getValue();
151
152 if (matches.size() == 1) {
153 // unique match! We're good to go!
154 matchedClassNames.put(sourceClass.getClassEntry().getName(), matches.get(0).getClassEntry().getName());
155 } else {
156 // no match, check the fallback matching
157 String fallbackMatch = fallbackMatching.get(className);
158 if (fallbackMatch != null) {
159 matchedClassNames.put(sourceClass.getClassEntry().getName(), fallbackMatch);
160 } else {
161 unmatchedSourceClassNames.add(className);
162 }
163 }
164 }
165
166 // report unmatched classes
167 if (!unmatchedSourceClassNames.isEmpty()) {
168 System.err.println("ERROR: there were unmatched classes!");
169 for (String className : unmatchedSourceClassNames) {
170 System.err.println("\t" + className);
171 }
172 return;
173 }
174
175 // get the class name changes from the matched class names
176 Map<String,String> classChanges = Maps.newHashMap();
177 for (Map.Entry<String,String> entry : matchedClassNames.entrySet()) {
178 if (!entry.getKey().equals(entry.getValue())) {
179 classChanges.put(entry.getKey(), entry.getValue());
180 System.out.println(String.format("Class change: %s -> %s", entry.getKey(), entry.getValue()));
181 /* DEBUG
182 System.out.println(String.format("\n%s\n%s",
183 new ClassIdentity(sourceLoader.loadClass(entry.getKey()), null, sourceIndex, false, false),
184 new ClassIdentity( destLoader.loadClass(entry.getValue()), null, destIndex, false, false)
185 ));
186 */
187 }
188 }
189
190 // sort the changes so classes are renamed in the correct order
191 // ie. if we have the mappings a->b, b->c, we have to apply b->c before a->b
192 LinkedHashMap<String,String> orderedClassChanges = Maps.newLinkedHashMap();
193 int numChangesLeft = classChanges.size();
194 while (!classChanges.isEmpty()) {
195 Iterator<Map.Entry<String,String>> iter = classChanges.entrySet().iterator();
196 while (iter.hasNext()) {
197 Map.Entry<String,String> entry = iter.next();
198 if (classChanges.get(entry.getValue()) == null) {
199 orderedClassChanges.put(entry.getKey(), entry.getValue());
200 iter.remove();
201 }
202 }
203
204 // did we remove any changes?
205 if (numChangesLeft - classChanges.size() > 0) {
206 // keep going
207 numChangesLeft = classChanges.size();
208 } else {
209 // can't sort anymore. There must be a loop
210 break;
211 }
212 }
213 if (classChanges.size() > 0) {
214 throw new Error(String.format("Unable to sort %d/%d class changes!", classChanges.size(), matchedClassNames.size()));
215 }
216
217 // convert the mappings in the correct class order
218 for (Map.Entry<String,String> entry : orderedClassChanges.entrySet()) {
219 mappings.renameObfClass(entry.getKey(), entry.getValue());
220 }
221
222 // check the method matches
223 System.out.println("Checking methods...");
224 for (ClassMapping classMapping : mappings.classes()) {
225 ClassEntry classEntry = new ClassEntry(classMapping.getObfName());
226 for (MethodMapping methodMapping : classMapping.methods()) {
227
228 // skip constructors
229 if (methodMapping.getObfName().equals("<init>")) {
230 continue;
231 }
232
233 MethodEntry methodEntry = new MethodEntry(
234 classEntry,
235 methodMapping.getObfName(),
236 methodMapping.getObfSignature()
237 );
238 if (!destIndex.containsObfBehavior(methodEntry)) {
239 System.err.println("WARNING: method doesn't match: " + methodEntry);
240
241 // show the available methods
242 System.err.println("\tAvailable dest methods:");
243 CtClass c = destLoader.loadClass(classMapping.getObfName());
244 for (CtBehavior behavior : c.getDeclaredBehaviors()) {
245 System.err.println("\t\t" + JavassistUtil.getBehaviorEntry(behavior));
246 }
247
248 System.err.println("\tAvailable source methods:");
249 c = sourceLoader.loadClass(matchedClassNames.inverse().get(classMapping.getObfName()));
250 for (CtBehavior behavior : c.getDeclaredBehaviors()) {
251 System.err.println("\t\t" + JavassistUtil.getBehaviorEntry(behavior));
252 }
253 }
254 }
255 }
256
257 System.out.println("Done!");
258 }
259
260 public static ClassMatching computeMatching(JarIndex sourceIndex, TranslatingTypeLoader sourceLoader, JarIndex destIndex, TranslatingTypeLoader destLoader) {
261
262 System.out.println("Matching classes...");
263
264 ClassMatching matching = null;
265 for (boolean useReferences : Arrays.asList(false, true)) {
266 int numMatches = 0;
267 do {
268 SidedClassNamer sourceNamer = null;
269 SidedClassNamer destNamer = null;
270 if (matching != null) {
271 // build a class namer
272 ClassNamer namer = new ClassNamer(matching.getUniqueMatches());
273 sourceNamer = namer.getSourceNamer();
274 destNamer = namer.getDestNamer();
275
276 // note the number of matches
277 numMatches = matching.getUniqueMatches().size();
278 }
279
280 // get the entries left to match
281 Set<ClassEntry> sourceClassEntries = Sets.newHashSet();
282 Set<ClassEntry> destClassEntries = Sets.newHashSet();
283 if (matching == null) {
284 sourceClassEntries.addAll(sourceIndex.getObfClassEntries());
285 destClassEntries.addAll(destIndex.getObfClassEntries());
286 matching = new ClassMatching();
287 } else {
288 for (Map.Entry<List<ClassIdentity>,List<ClassIdentity>> entry : matching.getAmbiguousMatches().entrySet()) {
289 for (ClassIdentity c : entry.getKey()) {
290 sourceClassEntries.add(c.getClassEntry());
291 matching.removeSource(c);
292 }
293 for (ClassIdentity c : entry.getValue()) {
294 destClassEntries.add(c.getClassEntry());
295 matching.removeDest(c);
296 }
297 }
298 for (ClassIdentity c : matching.getUnmatchedSourceClasses()) {
299 sourceClassEntries.add(c.getClassEntry());
300 matching.removeSource(c);
301 }
302 for (ClassIdentity c : matching.getUnmatchedDestClasses()) {
303 destClassEntries.add(c.getClassEntry());
304 matching.removeDest(c);
305 }
306 }
307
308 // compute a matching for the classes
309 for (ClassEntry classEntry : sourceClassEntries) {
310 CtClass c = sourceLoader.loadClass(classEntry.getName());
311 ClassIdentity sourceClass = new ClassIdentity(c, sourceNamer, sourceIndex, useReferences);
312 matching.addSource(sourceClass);
313 }
314 for (ClassEntry classEntry : destClassEntries) {
315 CtClass c = destLoader.loadClass(classEntry.getName());
316 ClassIdentity destClass = new ClassIdentity(c, destNamer, destIndex, useReferences);
317 matching.matchDestClass(destClass);
318 }
319
320 // TEMP
321 System.out.println(matching);
322 } while (matching.getUniqueMatches().size() - numMatches > 0);
323 }
324
325 // check the class matches
326 System.out.println("Checking class matches...");
327 ClassNamer namer = new ClassNamer(matching.getUniqueMatches());
328 SidedClassNamer sourceNamer = namer.getSourceNamer();
329 SidedClassNamer destNamer = namer.getDestNamer();
330 for (Map.Entry<ClassIdentity,ClassIdentity> entry : matching.getUniqueMatches().entrySet()) {
331
332 // check source
333 ClassIdentity sourceClass = entry.getKey();
334 CtClass sourceC = sourceLoader.loadClass(sourceClass.getClassEntry().getName());
335 assert (sourceC != null) : "Unable to load source class " + sourceClass.getClassEntry();
336 assert (sourceClass.matches(sourceC)) : "Source " + sourceClass + " doesn't match " + new ClassIdentity(sourceC, sourceNamer, sourceIndex, false);
337
338 // check dest
339 ClassIdentity destClass = entry.getValue();
340 CtClass destC = destLoader.loadClass(destClass.getClassEntry().getName());
341 assert (destC != null) : "Unable to load dest class " + destClass.getClassEntry();
342 assert (destClass.matches(destC)) : "Dest " + destClass + " doesn't match " + new ClassIdentity(destC, destNamer, destIndex, false);
343 }
344
345 // warn about the ambiguous matchings
346 List<Map.Entry<List<ClassIdentity>,List<ClassIdentity>>> ambiguousMatches = new ArrayList<Map.Entry<List<ClassIdentity>,List<ClassIdentity>>>(matching.getAmbiguousMatches().entrySet());
347 Collections.sort(ambiguousMatches, new Comparator<Map.Entry<List<ClassIdentity>,List<ClassIdentity>>>() {
348 @Override
349 public int compare(Map.Entry<List<ClassIdentity>,List<ClassIdentity>> a, Map.Entry<List<ClassIdentity>,List<ClassIdentity>> b) {
350 String aName = a.getKey().get(0).getClassEntry().getName();
351 String bName = b.getKey().get(0).getClassEntry().getName();
352 return aName.compareTo(bName);
353 }
354 });
355 for (Map.Entry<List<ClassIdentity>,List<ClassIdentity>> entry : ambiguousMatches) {
356 System.out.println("Ambiguous matching:");
357 System.out.println("\tSource: " + getClassNames(entry.getKey()));
358 System.out.println("\tDest: " + getClassNames(entry.getValue()));
359 }
360
361 /* DEBUG
362 Map.Entry<List<ClassIdentity>,List<ClassIdentity>> entry = ambiguousMatches.get( 7 );
363 for (ClassIdentity c : entry.getKey()) {
364 System.out.println(c);
365 }
366 for(ClassIdentity c : entry.getKey()) {
367 System.out.println(decompile(sourceLoader, c.getClassEntry()));
368 }
369 */
370
371 return matching;
372 }
373
374 private static void printScoredMatches(int maxScore, List<Integer> scores, Multimap<Integer,ClassIdentity> scoredMatches) {
375 int numScoredMatchesShown = 0;
376 for (int score : scores) {
377 for (ClassIdentity scoredMatch : scoredMatches.get(score)) {
378 System.out.println(String.format("\tScore: %3d %3.0f%% %s", score, 100.0 * score / maxScore, scoredMatch.getClassEntry().getName()));
379 if (numScoredMatchesShown++ > 10) {
380 return;
381 }
382 }
383 }
384 }
385
386 private static List<String> getClassNames(Collection<ClassIdentity> classes) {
387 List<String> out = Lists.newArrayList();
388 for (ClassIdentity c : classes) {
389 out.add(c.getClassEntry().getName());
390 }
391 Collections.sort(out);
392 return out;
393 }
394
395 /* DEBUG
396 private static String decompile(TranslatingTypeLoader loader, ClassEntry classEntry) {
397 PlainTextOutput output = new PlainTextOutput();
398 DecompilerSettings settings = DecompilerSettings.javaDefaults();
399 settings.setForceExplicitImports(true);
400 settings.setShowSyntheticMembers(true);
401 settings.setTypeLoader(loader);
402 Decompiler.decompile(classEntry.getName(), output, settings);
403 return output.toString();
404 }
405 */
406}