summaryrefslogtreecommitdiff
path: root/src/cuchaz/enigma/convert
diff options
context:
space:
mode:
authorGravatar jeff2015-02-03 22:00:53 -0500
committerGravatar jeff2015-02-03 22:00:53 -0500
commit52ab426d8fad3dbee7e728f523a35af94facebda (patch)
tree146fadfd8e639a909d6c1d6a193e7eddeab0be4a /src/cuchaz/enigma/convert
downloadenigma-fork-52ab426d8fad3dbee7e728f523a35af94facebda.tar.gz
enigma-fork-52ab426d8fad3dbee7e728f523a35af94facebda.tar.xz
enigma-fork-52ab426d8fad3dbee7e728f523a35af94facebda.zip
oops, don't depend on local procyon project
Diffstat (limited to 'src/cuchaz/enigma/convert')
-rw-r--r--src/cuchaz/enigma/convert/ClassIdentity.java411
-rw-r--r--src/cuchaz/enigma/convert/ClassMatcher.java415
-rw-r--r--src/cuchaz/enigma/convert/ClassMatching.java173
-rw-r--r--src/cuchaz/enigma/convert/ClassNamer.java64
4 files changed, 1063 insertions, 0 deletions
diff --git a/src/cuchaz/enigma/convert/ClassIdentity.java b/src/cuchaz/enigma/convert/ClassIdentity.java
new file mode 100644
index 0000000..7340403
--- /dev/null
+++ b/src/cuchaz/enigma/convert/ClassIdentity.java
@@ -0,0 +1,411 @@
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.UnsupportedEncodingException;
14import java.security.MessageDigest;
15import java.security.NoSuchAlgorithmException;
16import java.util.Enumeration;
17import java.util.List;
18import java.util.Map;
19
20import javassist.CannotCompileException;
21import javassist.CtBehavior;
22import javassist.CtClass;
23import javassist.CtConstructor;
24import javassist.CtField;
25import javassist.CtMethod;
26import javassist.bytecode.BadBytecode;
27import javassist.bytecode.CodeIterator;
28import javassist.bytecode.ConstPool;
29import javassist.bytecode.Descriptor;
30import javassist.bytecode.Opcode;
31import javassist.expr.ConstructorCall;
32import javassist.expr.ExprEditor;
33import javassist.expr.FieldAccess;
34import javassist.expr.MethodCall;
35import javassist.expr.NewExpr;
36
37import com.google.common.collect.HashMultiset;
38import com.google.common.collect.Lists;
39import com.google.common.collect.Maps;
40import com.google.common.collect.Multiset;
41
42import cuchaz.enigma.Constants;
43import cuchaz.enigma.Util;
44import cuchaz.enigma.analysis.ClassImplementationsTreeNode;
45import cuchaz.enigma.analysis.EntryReference;
46import cuchaz.enigma.analysis.JarIndex;
47import cuchaz.enigma.bytecode.ConstPoolEditor;
48import cuchaz.enigma.bytecode.InfoType;
49import cuchaz.enigma.bytecode.accessors.ConstInfoAccessor;
50import cuchaz.enigma.convert.ClassNamer.SidedClassNamer;
51import cuchaz.enigma.mapping.BehaviorEntry;
52import cuchaz.enigma.mapping.ClassEntry;
53import cuchaz.enigma.mapping.ConstructorEntry;
54import cuchaz.enigma.mapping.Entry;
55import cuchaz.enigma.mapping.FieldEntry;
56import cuchaz.enigma.mapping.MethodEntry;
57import cuchaz.enigma.mapping.SignatureUpdater;
58import cuchaz.enigma.mapping.SignatureUpdater.ClassNameUpdater;
59
60public class ClassIdentity {
61
62 private ClassEntry m_classEntry;
63 private SidedClassNamer m_namer;
64 private Multiset<String> m_fields;
65 private Multiset<String> m_methods;
66 private Multiset<String> m_constructors;
67 private String m_staticInitializer;
68 private String m_extends;
69 private Multiset<String> m_implements;
70 private Multiset<String> m_implementations;
71 private Multiset<String> m_references;
72
73 public ClassIdentity(CtClass c, SidedClassNamer namer, JarIndex index, boolean useReferences) {
74 m_namer = namer;
75
76 // stuff from the bytecode
77
78 m_classEntry = new ClassEntry(Descriptor.toJvmName(c.getName()));
79 m_fields = HashMultiset.create();
80 for (CtField field : c.getDeclaredFields()) {
81 m_fields.add(scrubSignature(field.getSignature()));
82 }
83 m_methods = HashMultiset.create();
84 for (CtMethod method : c.getDeclaredMethods()) {
85 m_methods.add(scrubSignature(method.getSignature()) + "0x" + getBehaviorSignature(method));
86 }
87 m_constructors = HashMultiset.create();
88 for (CtConstructor constructor : c.getDeclaredConstructors()) {
89 m_constructors.add(scrubSignature(constructor.getSignature()) + "0x" + getBehaviorSignature(constructor));
90 }
91 m_staticInitializer = "";
92 if (c.getClassInitializer() != null) {
93 m_staticInitializer = getBehaviorSignature(c.getClassInitializer());
94 }
95 m_extends = "";
96 if (c.getClassFile().getSuperclass() != null) {
97 m_extends = scrubClassName(c.getClassFile().getSuperclass());
98 }
99 m_implements = HashMultiset.create();
100 for (String interfaceName : c.getClassFile().getInterfaces()) {
101 m_implements.add(scrubClassName(interfaceName));
102 }
103
104 // stuff from the jar index
105
106 m_implementations = HashMultiset.create();
107 ClassImplementationsTreeNode implementationsNode = index.getClassImplementations(null, m_classEntry);
108 if (implementationsNode != null) {
109 @SuppressWarnings("unchecked")
110 Enumeration<ClassImplementationsTreeNode> implementations = implementationsNode.children();
111 while (implementations.hasMoreElements()) {
112 ClassImplementationsTreeNode node = implementations.nextElement();
113 m_implementations.add(scrubClassName(node.getClassEntry().getName()));
114 }
115 }
116
117 m_references = HashMultiset.create();
118 if (useReferences) {
119 for (CtField field : c.getDeclaredFields()) {
120 FieldEntry fieldEntry = new FieldEntry(m_classEntry, field.getName());
121 for (EntryReference<FieldEntry,BehaviorEntry> reference : index.getFieldReferences(fieldEntry)) {
122 addReference(reference);
123 }
124 }
125 for (CtMethod method : c.getDeclaredMethods()) {
126 MethodEntry methodEntry = new MethodEntry(m_classEntry, method.getName(), method.getSignature());
127 for (EntryReference<BehaviorEntry,BehaviorEntry> reference : index.getBehaviorReferences(methodEntry)) {
128 addReference(reference);
129 }
130 }
131 for (CtConstructor constructor : c.getDeclaredConstructors()) {
132 ConstructorEntry constructorEntry = new ConstructorEntry(m_classEntry, constructor.getSignature());
133 for (EntryReference<BehaviorEntry,BehaviorEntry> reference : index.getBehaviorReferences(constructorEntry)) {
134 addReference(reference);
135 }
136 }
137 }
138 }
139
140 private void addReference(EntryReference<? extends Entry,BehaviorEntry> reference) {
141 if (reference.context.getSignature() != null) {
142 m_references.add(String.format("%s_%s", scrubClassName(reference.context.getClassName()), scrubSignature(reference.context.getSignature())));
143 } else {
144 m_references.add(String.format("%s_<clinit>", scrubClassName(reference.context.getClassName())));
145 }
146 }
147
148 public ClassEntry getClassEntry() {
149 return m_classEntry;
150 }
151
152 @Override
153 public String toString() {
154 StringBuilder buf = new StringBuilder();
155 buf.append("class: ");
156 buf.append(m_classEntry.getName());
157 buf.append(" ");
158 buf.append(hashCode());
159 buf.append("\n");
160 for (String field : m_fields) {
161 buf.append("\tfield ");
162 buf.append(field);
163 buf.append("\n");
164 }
165 for (String method : m_methods) {
166 buf.append("\tmethod ");
167 buf.append(method);
168 buf.append("\n");
169 }
170 for (String constructor : m_constructors) {
171 buf.append("\tconstructor ");
172 buf.append(constructor);
173 buf.append("\n");
174 }
175 if (m_staticInitializer.length() > 0) {
176 buf.append("\tinitializer ");
177 buf.append(m_staticInitializer);
178 buf.append("\n");
179 }
180 if (m_extends.length() > 0) {
181 buf.append("\textends ");
182 buf.append(m_extends);
183 buf.append("\n");
184 }
185 for (String interfaceName : m_implements) {
186 buf.append("\timplements ");
187 buf.append(interfaceName);
188 buf.append("\n");
189 }
190 for (String implementation : m_implementations) {
191 buf.append("\timplemented by ");
192 buf.append(implementation);
193 buf.append("\n");
194 }
195 for (String reference : m_references) {
196 buf.append("\treference ");
197 buf.append(reference);
198 buf.append("\n");
199 }
200 return buf.toString();
201 }
202
203 private String scrubClassName(String className) {
204 return scrubSignature("L" + Descriptor.toJvmName(className) + ";");
205 }
206
207 private String scrubSignature(String signature) {
208 return SignatureUpdater.update(signature, new ClassNameUpdater() {
209 private Map<String,String> m_classNames = Maps.newHashMap();
210
211 @Override
212 public String update(String className) {
213 // classes not in the none package can be passed through
214 ClassEntry classEntry = new ClassEntry(className);
215 if (!classEntry.getPackageName().equals(Constants.NonePackage)) {
216 return className;
217 }
218
219 // is this class ourself?
220 if (className.equals(m_classEntry.getName())) {
221 return "CSelf";
222 }
223
224 // try the namer
225 if (m_namer != null) {
226 String newName = m_namer.getName(className);
227 if (newName != null) {
228 return newName;
229 }
230 }
231
232 // otherwise, use local naming
233 if (!m_classNames.containsKey(className)) {
234 m_classNames.put(className, getNewClassName());
235 }
236 return m_classNames.get(className);
237 }
238
239 private String getNewClassName() {
240 return String.format("C%03d", m_classNames.size());
241 }
242 });
243 }
244
245 private boolean isClassMatchedUniquely(String className) {
246 return m_namer != null && m_namer.getName(Descriptor.toJvmName(className)) != null;
247 }
248
249 private String getBehaviorSignature(CtBehavior behavior) {
250 try {
251 // does this method have an implementation?
252 if (behavior.getMethodInfo().getCodeAttribute() == null) {
253 return "(none)";
254 }
255
256 // compute the hash from the opcodes
257 ConstPool constants = behavior.getMethodInfo().getConstPool();
258 final MessageDigest digest = MessageDigest.getInstance("MD5");
259 CodeIterator iter = behavior.getMethodInfo().getCodeAttribute().iterator();
260 while (iter.hasNext()) {
261 int pos = iter.next();
262
263 // update the hash with the opcode
264 int opcode = iter.byteAt(pos);
265 digest.update((byte)opcode);
266
267 switch (opcode) {
268 case Opcode.LDC: {
269 int constIndex = iter.byteAt(pos + 1);
270 updateHashWithConstant(digest, constants, constIndex);
271 }
272 break;
273
274 case Opcode.LDC_W:
275 case Opcode.LDC2_W: {
276 int constIndex = (iter.byteAt(pos + 1) << 8) | iter.byteAt(pos + 2);
277 updateHashWithConstant(digest, constants, constIndex);
278 }
279 break;
280 }
281 }
282
283 // update hash with method and field accesses
284 behavior.instrument(new ExprEditor() {
285 @Override
286 public void edit(MethodCall call) {
287 updateHashWithString(digest, scrubClassName(call.getClassName()));
288 updateHashWithString(digest, scrubSignature(call.getSignature()));
289 if (isClassMatchedUniquely(call.getClassName())) {
290 updateHashWithString(digest, call.getMethodName());
291 }
292 }
293
294 @Override
295 public void edit(FieldAccess access) {
296 updateHashWithString(digest, scrubClassName(access.getClassName()));
297 updateHashWithString(digest, scrubSignature(access.getSignature()));
298 if (isClassMatchedUniquely(access.getClassName())) {
299 updateHashWithString(digest, access.getFieldName());
300 }
301 }
302
303 @Override
304 public void edit(ConstructorCall call) {
305 updateHashWithString(digest, scrubClassName(call.getClassName()));
306 updateHashWithString(digest, scrubSignature(call.getSignature()));
307 }
308
309 @Override
310 public void edit(NewExpr expr) {
311 updateHashWithString(digest, scrubClassName(expr.getClassName()));
312 }
313 });
314
315 // convert the hash to a hex string
316 return toHex(digest.digest());
317 } catch (BadBytecode | NoSuchAlgorithmException | CannotCompileException ex) {
318 throw new Error(ex);
319 }
320 }
321
322 private void updateHashWithConstant(MessageDigest digest, ConstPool constants, int index) {
323 ConstPoolEditor editor = new ConstPoolEditor(constants);
324 ConstInfoAccessor item = editor.getItem(index);
325 if (item.getType() == InfoType.StringInfo) {
326 updateHashWithString(digest, constants.getStringInfo(index));
327 }
328 // TODO: other constants
329 }
330
331 private void updateHashWithString(MessageDigest digest, String val) {
332 try {
333 digest.update(val.getBytes("UTF8"));
334 } catch (UnsupportedEncodingException ex) {
335 throw new Error(ex);
336 }
337 }
338
339 private String toHex(byte[] bytes) {
340 // function taken from:
341 // http://stackoverflow.com/questions/9655181/convert-from-byte-array-to-hex-string-in-java
342 final char[] hexArray = "0123456789ABCDEF".toCharArray();
343 char[] hexChars = new char[bytes.length * 2];
344 for (int j = 0; j < bytes.length; j++) {
345 int v = bytes[j] & 0xFF;
346 hexChars[j * 2] = hexArray[v >>> 4];
347 hexChars[j * 2 + 1] = hexArray[v & 0x0F];
348 }
349 return new String(hexChars);
350 }
351
352 @Override
353 public boolean equals(Object other) {
354 if (other instanceof ClassIdentity) {
355 return equals((ClassIdentity)other);
356 }
357 return false;
358 }
359
360 public boolean equals(ClassIdentity other) {
361 return m_fields.equals(other.m_fields)
362 && m_methods.equals(other.m_methods)
363 && m_constructors.equals(other.m_constructors)
364 && m_staticInitializer.equals(other.m_staticInitializer)
365 && m_extends.equals(other.m_extends)
366 && m_implements.equals(other.m_implements)
367 && m_implementations.equals(other.m_implementations)
368 && m_references.equals(other.m_references);
369 }
370
371 @Override
372 public int hashCode() {
373 List<Object> objs = Lists.newArrayList();
374 objs.addAll(m_fields);
375 objs.addAll(m_methods);
376 objs.addAll(m_constructors);
377 objs.add(m_staticInitializer);
378 objs.add(m_extends);
379 objs.addAll(m_implements);
380 objs.addAll(m_implementations);
381 objs.addAll(m_references);
382 return Util.combineHashesOrdered(objs);
383 }
384
385 public int getMatchScore(ClassIdentity other) {
386 return getNumMatches(m_fields, other.m_fields)
387 + getNumMatches(m_methods, other.m_methods)
388 + getNumMatches(m_constructors, other.m_constructors);
389 }
390
391 public int getMaxMatchScore() {
392 return m_fields.size() + m_methods.size() + m_constructors.size();
393 }
394
395 public boolean matches(CtClass c) {
396 // just compare declaration counts
397 return m_fields.size() == c.getDeclaredFields().length
398 && m_methods.size() == c.getDeclaredMethods().length
399 && m_constructors.size() == c.getDeclaredConstructors().length;
400 }
401
402 private int getNumMatches(Multiset<String> a, Multiset<String> b) {
403 int numMatches = 0;
404 for (String val : a) {
405 if (b.contains(val)) {
406 numMatches++;
407 }
408 }
409 return numMatches;
410 }
411}
diff --git a/src/cuchaz/enigma/convert/ClassMatcher.java b/src/cuchaz/enigma/convert/ClassMatcher.java
new file mode 100644
index 0000000..fc39ed0
--- /dev/null
+++ b/src/cuchaz/enigma/convert/ClassMatcher.java
@@ -0,0 +1,415 @@
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.MappingParseException;
46import cuchaz.enigma.mapping.Mappings;
47import cuchaz.enigma.mapping.MappingsReader;
48import cuchaz.enigma.mapping.MappingsWriter;
49import cuchaz.enigma.mapping.MethodEntry;
50import cuchaz.enigma.mapping.MethodMapping;
51
52public class ClassMatcher {
53
54 public static void main(String[] args) throws IOException, MappingParseException {
55 // TEMP
56 JarFile sourceJar = new JarFile(new File("input/1.8-pre3.jar"));
57 JarFile destJar = new JarFile(new File("input/1.8.jar"));
58 File inMappingsFile = new File("../Enigma Mappings/1.8-pre3.mappings");
59 File outMappingsFile = new File("../Enigma Mappings/1.8.mappings");
60
61 // define a matching to use when the automated system cannot find a match
62 Map<String,String> fallbackMatching = Maps.newHashMap();
63 fallbackMatching.put("none/ayb", "none/ayf");
64 fallbackMatching.put("none/ayd", "none/ayd");
65 fallbackMatching.put("none/bgk", "unknown/bgk");
66
67 // do the conversion
68 Mappings mappings = new MappingsReader().read(new FileReader(inMappingsFile));
69 convertMappings(sourceJar, destJar, mappings, fallbackMatching);
70
71 // write out the converted mappings
72 FileWriter writer = new FileWriter(outMappingsFile);
73 new MappingsWriter().write(writer, mappings);
74 writer.close();
75 System.out.println("Wrote converted mappings to:\n\t" + outMappingsFile.getAbsolutePath());
76 }
77
78 private static void convertMappings(JarFile sourceJar, JarFile destJar, Mappings mappings, Map<String,String> fallbackMatching) {
79 // index jars
80 System.out.println("Indexing source jar...");
81 JarIndex sourceIndex = new JarIndex();
82 sourceIndex.indexJar(sourceJar, false);
83 System.out.println("Indexing dest jar...");
84 JarIndex destIndex = new JarIndex();
85 destIndex.indexJar(destJar, false);
86 TranslatingTypeLoader sourceLoader = new TranslatingTypeLoader(sourceJar, sourceIndex);
87 TranslatingTypeLoader destLoader = new TranslatingTypeLoader(destJar, destIndex);
88
89 // compute the matching
90 ClassMatching matching = computeMatching(sourceIndex, sourceLoader, destIndex, destLoader);
91 Map<String,Map.Entry<ClassIdentity,List<ClassIdentity>>> matchingIndex = matching.getIndex();
92
93 // get all the obf class names used in the mappings
94 Set<String> usedClassNames = mappings.getAllObfClassNames();
95 Set<String> allClassNames = Sets.newHashSet();
96 for (ClassEntry classEntry : sourceIndex.getObfClassEntries()) {
97 allClassNames.add(classEntry.getName());
98 }
99 usedClassNames.retainAll(allClassNames);
100 System.out.println("Used " + usedClassNames.size() + " classes in the mappings");
101
102 // probabilistically match the non-uniquely-matched source classes
103 for (Map.Entry<ClassIdentity,List<ClassIdentity>> entry : matchingIndex.values()) {
104 ClassIdentity sourceClass = entry.getKey();
105 List<ClassIdentity> destClasses = entry.getValue();
106
107 // skip classes that are uniquely matched
108 if (destClasses.size() == 1) {
109 continue;
110 }
111
112 // skip classes that aren't used in the mappings
113 if (!usedClassNames.contains(sourceClass.getClassEntry().getName())) {
114 continue;
115 }
116
117 System.out.println("No exact match for source class " + sourceClass.getClassEntry());
118
119 // find the closest classes
120 Multimap<Integer,ClassIdentity> scoredMatches = ArrayListMultimap.create();
121 for (ClassIdentity c : destClasses) {
122 scoredMatches.put(sourceClass.getMatchScore(c), c);
123 }
124 List<Integer> scores = new ArrayList<Integer>(scoredMatches.keySet());
125 Collections.sort(scores, Collections.reverseOrder());
126 printScoredMatches(sourceClass.getMaxMatchScore(), scores, scoredMatches);
127
128 // does the best match have a non-zero score and the same name?
129 int bestScore = scores.get(0);
130 Collection<ClassIdentity> bestMatches = scoredMatches.get(bestScore);
131 if (bestScore > 0 && bestMatches.size() == 1) {
132 ClassIdentity bestMatch = bestMatches.iterator().next();
133 if (bestMatch.getClassEntry().equals(sourceClass.getClassEntry())) {
134 // use it
135 System.out.println("\tAutomatically choosing likely match: " + bestMatch.getClassEntry().getName());
136 destClasses.clear();
137 destClasses.add(bestMatch);
138 }
139 }
140 }
141
142 // group the matching into unique and non-unique matches
143 BiMap<String,String> matchedClassNames = HashBiMap.create();
144 Set<String> unmatchedSourceClassNames = Sets.newHashSet();
145 for (String className : usedClassNames) {
146 // is there a match for this class?
147 Map.Entry<ClassIdentity,List<ClassIdentity>> entry = matchingIndex.get(className);
148 ClassIdentity sourceClass = entry.getKey();
149 List<ClassIdentity> matches = entry.getValue();
150
151 if (matches.size() == 1) {
152 // unique match! We're good to go!
153 matchedClassNames.put(sourceClass.getClassEntry().getName(), matches.get(0).getClassEntry().getName());
154 } else {
155 // no match, check the fallback matching
156 String fallbackMatch = fallbackMatching.get(className);
157 if (fallbackMatch != null) {
158 matchedClassNames.put(sourceClass.getClassEntry().getName(), fallbackMatch);
159 } else {
160 unmatchedSourceClassNames.add(className);
161 }
162 }
163 }
164
165 // report unmatched classes
166 if (!unmatchedSourceClassNames.isEmpty()) {
167 System.err.println("ERROR: there were unmatched classes!");
168 for (String className : unmatchedSourceClassNames) {
169 System.err.println("\t" + className);
170 }
171 return;
172 }
173
174 // get the class name changes from the matched class names
175 Map<String,String> classChanges = Maps.newHashMap();
176 for (Map.Entry<String,String> entry : matchedClassNames.entrySet()) {
177 if (!entry.getKey().equals(entry.getValue())) {
178 classChanges.put(entry.getKey(), entry.getValue());
179 System.out.println(String.format("Class change: %s -> %s", entry.getKey(), entry.getValue()));
180 /* DEBUG
181 System.out.println(String.format("\n%s\n%s",
182 new ClassIdentity(sourceLoader.loadClass(entry.getKey()), null, sourceIndex, false, false),
183 new ClassIdentity( destLoader.loadClass(entry.getValue()), null, destIndex, false, false)
184 ));
185 */
186 }
187 }
188
189 // sort the changes so classes are renamed in the correct order
190 // ie. if we have the mappings a->b, b->c, we have to apply b->c before a->b
191 LinkedHashMap<String,String> orderedClassChanges = Maps.newLinkedHashMap();
192 int numChangesLeft = classChanges.size();
193 while (!classChanges.isEmpty()) {
194 Iterator<Map.Entry<String,String>> iter = classChanges.entrySet().iterator();
195 while (iter.hasNext()) {
196 Map.Entry<String,String> entry = iter.next();
197 if (classChanges.get(entry.getValue()) == null) {
198 orderedClassChanges.put(entry.getKey(), entry.getValue());
199 iter.remove();
200 }
201 }
202
203 // did we remove any changes?
204 if (numChangesLeft - classChanges.size() > 0) {
205 // keep going
206 numChangesLeft = classChanges.size();
207 } else {
208 // can't sort anymore. There must be a loop
209 break;
210 }
211 }
212 if (classChanges.size() > 0) {
213 throw new Error(String.format("Unable to sort %d/%d class changes!", classChanges.size(), matchedClassNames.size()));
214 }
215
216 // convert the mappings in the correct class order
217 for (Map.Entry<String,String> entry : orderedClassChanges.entrySet()) {
218 mappings.renameObfClass(entry.getKey(), entry.getValue());
219 }
220
221 // check the method matches
222 System.out.println("Checking methods...");
223 for (ClassMapping classMapping : mappings.classes()) {
224 ClassEntry classEntry = new ClassEntry(classMapping.getObfName());
225 for (MethodMapping methodMapping : classMapping.methods()) {
226
227 // skip constructors
228 if (methodMapping.getObfName().equals("<init>")) {
229 continue;
230 }
231
232 MethodEntry methodEntry = new MethodEntry(
233 classEntry,
234 methodMapping.getObfName(),
235 methodMapping.getObfSignature()
236 );
237 if (!destIndex.containsObfBehavior(methodEntry)) {
238 System.err.println("WARNING: method doesn't match: " + methodEntry);
239
240 // show the available methods
241 System.err.println("\tAvailable dest methods:");
242 CtClass c = destLoader.loadClass(classMapping.getObfName());
243 for (CtBehavior behavior : c.getDeclaredBehaviors()) {
244 MethodEntry declaredMethodEntry = new MethodEntry(
245 new ClassEntry(classMapping.getObfName()),
246 behavior.getName(),
247 behavior.getSignature()
248 );
249 System.err.println("\t\t" + declaredMethodEntry);
250 }
251
252 System.err.println("\tAvailable source methods:");
253 c = sourceLoader.loadClass(matchedClassNames.inverse().get(classMapping.getObfName()));
254 for (CtBehavior behavior : c.getDeclaredBehaviors()) {
255 MethodEntry declaredMethodEntry = new MethodEntry(
256 new ClassEntry(classMapping.getObfName()),
257 behavior.getName(),
258 behavior.getSignature()
259 );
260 System.err.println("\t\t" + declaredMethodEntry);
261 }
262 }
263 }
264 }
265
266 System.out.println("Done!");
267 }
268
269 public static ClassMatching computeMatching(JarIndex sourceIndex, TranslatingTypeLoader sourceLoader, JarIndex destIndex, TranslatingTypeLoader destLoader) {
270
271 System.out.println("Matching classes...");
272
273 ClassMatching matching = null;
274 for (boolean useReferences : Arrays.asList(false, true)) {
275 int numMatches = 0;
276 do {
277 SidedClassNamer sourceNamer = null;
278 SidedClassNamer destNamer = null;
279 if (matching != null) {
280 // build a class namer
281 ClassNamer namer = new ClassNamer(matching.getUniqueMatches());
282 sourceNamer = namer.getSourceNamer();
283 destNamer = namer.getDestNamer();
284
285 // note the number of matches
286 numMatches = matching.getUniqueMatches().size();
287 }
288
289 // get the entries left to match
290 Set<ClassEntry> sourceClassEntries = Sets.newHashSet();
291 Set<ClassEntry> destClassEntries = Sets.newHashSet();
292 if (matching == null) {
293 sourceClassEntries.addAll(sourceIndex.getObfClassEntries());
294 destClassEntries.addAll(destIndex.getObfClassEntries());
295 matching = new ClassMatching();
296 } else {
297 for (Map.Entry<List<ClassIdentity>,List<ClassIdentity>> entry : matching.getAmbiguousMatches().entrySet()) {
298 for (ClassIdentity c : entry.getKey()) {
299 sourceClassEntries.add(c.getClassEntry());
300 matching.removeSource(c);
301 }
302 for (ClassIdentity c : entry.getValue()) {
303 destClassEntries.add(c.getClassEntry());
304 matching.removeDest(c);
305 }
306 }
307 for (ClassIdentity c : matching.getUnmatchedSourceClasses()) {
308 sourceClassEntries.add(c.getClassEntry());
309 matching.removeSource(c);
310 }
311 for (ClassIdentity c : matching.getUnmatchedDestClasses()) {
312 destClassEntries.add(c.getClassEntry());
313 matching.removeDest(c);
314 }
315 }
316
317 // compute a matching for the classes
318 for (ClassEntry classEntry : sourceClassEntries) {
319 CtClass c = sourceLoader.loadClass(classEntry.getName());
320 ClassIdentity sourceClass = new ClassIdentity(c, sourceNamer, sourceIndex, useReferences);
321 matching.addSource(sourceClass);
322 }
323 for (ClassEntry classEntry : destClassEntries) {
324 CtClass c = destLoader.loadClass(classEntry.getName());
325 ClassIdentity destClass = new ClassIdentity(c, destNamer, destIndex, useReferences);
326 matching.matchDestClass(destClass);
327 }
328
329 // TEMP
330 System.out.println(matching);
331 } while (matching.getUniqueMatches().size() - numMatches > 0);
332 }
333
334 // check the class matches
335 System.out.println("Checking class matches...");
336 ClassNamer namer = new ClassNamer(matching.getUniqueMatches());
337 SidedClassNamer sourceNamer = namer.getSourceNamer();
338 SidedClassNamer destNamer = namer.getDestNamer();
339 for (Map.Entry<ClassIdentity,ClassIdentity> entry : matching.getUniqueMatches().entrySet()) {
340
341 // check source
342 ClassIdentity sourceClass = entry.getKey();
343 CtClass sourceC = sourceLoader.loadClass(sourceClass.getClassEntry().getName());
344 assert (sourceC != null) : "Unable to load source class " + sourceClass.getClassEntry();
345 assert (sourceClass.matches(sourceC)) : "Source " + sourceClass + " doesn't match " + new ClassIdentity(sourceC, sourceNamer, sourceIndex, false);
346
347 // check dest
348 ClassIdentity destClass = entry.getValue();
349 CtClass destC = destLoader.loadClass(destClass.getClassEntry().getName());
350 assert (destC != null) : "Unable to load dest class " + destClass.getClassEntry();
351 assert (destClass.matches(destC)) : "Dest " + destClass + " doesn't match " + new ClassIdentity(destC, destNamer, destIndex, false);
352 }
353
354 // warn about the ambiguous matchings
355 List<Map.Entry<List<ClassIdentity>,List<ClassIdentity>>> ambiguousMatches = new ArrayList<Map.Entry<List<ClassIdentity>,List<ClassIdentity>>>(matching.getAmbiguousMatches().entrySet());
356 Collections.sort(ambiguousMatches, new Comparator<Map.Entry<List<ClassIdentity>,List<ClassIdentity>>>() {
357 @Override
358 public int compare(Map.Entry<List<ClassIdentity>,List<ClassIdentity>> a, Map.Entry<List<ClassIdentity>,List<ClassIdentity>> b) {
359 String aName = a.getKey().get(0).getClassEntry().getName();
360 String bName = b.getKey().get(0).getClassEntry().getName();
361 return aName.compareTo(bName);
362 }
363 });
364 for (Map.Entry<List<ClassIdentity>,List<ClassIdentity>> entry : ambiguousMatches) {
365 System.out.println("Ambiguous matching:");
366 System.out.println("\tSource: " + getClassNames(entry.getKey()));
367 System.out.println("\tDest: " + getClassNames(entry.getValue()));
368 }
369
370 /* DEBUG
371 Map.Entry<List<ClassIdentity>,List<ClassIdentity>> entry = ambiguousMatches.get( 7 );
372 for (ClassIdentity c : entry.getKey()) {
373 System.out.println(c);
374 }
375 for(ClassIdentity c : entry.getKey()) {
376 System.out.println(decompile(sourceLoader, c.getClassEntry()));
377 }
378 */
379
380 return matching;
381 }
382
383 private static void printScoredMatches(int maxScore, List<Integer> scores, Multimap<Integer,ClassIdentity> scoredMatches) {
384 int numScoredMatchesShown = 0;
385 for (int score : scores) {
386 for (ClassIdentity scoredMatch : scoredMatches.get(score)) {
387 System.out.println(String.format("\tScore: %3d %3.0f%% %s", score, 100.0 * score / maxScore, scoredMatch.getClassEntry().getName()));
388 if (numScoredMatchesShown++ > 10) {
389 return;
390 }
391 }
392 }
393 }
394
395 private static List<String> getClassNames(Collection<ClassIdentity> classes) {
396 List<String> out = Lists.newArrayList();
397 for (ClassIdentity c : classes) {
398 out.add(c.getClassEntry().getName());
399 }
400 Collections.sort(out);
401 return out;
402 }
403
404 /* DEBUG
405 private static String decompile(TranslatingTypeLoader loader, ClassEntry classEntry) {
406 PlainTextOutput output = new PlainTextOutput();
407 DecompilerSettings settings = DecompilerSettings.javaDefaults();
408 settings.setForceExplicitImports(true);
409 settings.setShowSyntheticMembers(true);
410 settings.setTypeLoader(loader);
411 Decompiler.decompile(classEntry.getName(), output, settings);
412 return output.toString();
413 }
414 */
415}
diff --git a/src/cuchaz/enigma/convert/ClassMatching.java b/src/cuchaz/enigma/convert/ClassMatching.java
new file mode 100644
index 0000000..53b6f7f
--- /dev/null
+++ b/src/cuchaz/enigma/convert/ClassMatching.java
@@ -0,0 +1,173 @@
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.util.AbstractMap;
14import java.util.ArrayList;
15import java.util.Arrays;
16import java.util.Collection;
17import java.util.List;
18import java.util.Map;
19
20import com.google.common.collect.ArrayListMultimap;
21import com.google.common.collect.BiMap;
22import com.google.common.collect.HashBiMap;
23import com.google.common.collect.Lists;
24import com.google.common.collect.Maps;
25import com.google.common.collect.Multimap;
26
27public class ClassMatching {
28
29 private Multimap<ClassIdentity,ClassIdentity> m_sourceClasses;
30 private Multimap<ClassIdentity,ClassIdentity> m_matchedDestClasses;
31 private List<ClassIdentity> m_unmatchedDestClasses;
32
33 public ClassMatching() {
34 m_sourceClasses = ArrayListMultimap.create();
35 m_matchedDestClasses = ArrayListMultimap.create();
36 m_unmatchedDestClasses = Lists.newArrayList();
37 }
38
39 public void addSource(ClassIdentity c) {
40 m_sourceClasses.put(c, c);
41 }
42
43 public void matchDestClass(ClassIdentity destClass) {
44 Collection<ClassIdentity> matchedSourceClasses = m_sourceClasses.get(destClass);
45 if (matchedSourceClasses.isEmpty()) {
46 // no match
47 m_unmatchedDestClasses.add(destClass);
48 } else {
49 // found a match
50 m_matchedDestClasses.put(destClass, destClass);
51
52 // DEBUG
53 ClassIdentity sourceClass = matchedSourceClasses.iterator().next();
54 assert (sourceClass.hashCode() == destClass.hashCode());
55 assert (sourceClass.equals(destClass));
56 }
57 }
58
59 public void removeSource(ClassIdentity sourceClass) {
60 m_sourceClasses.remove(sourceClass, sourceClass);
61 }
62
63 public void removeDest(ClassIdentity destClass) {
64 m_matchedDestClasses.remove(destClass, destClass);
65 m_unmatchedDestClasses.remove(destClass);
66 }
67
68 public List<ClassIdentity> getSourceClasses() {
69 return new ArrayList<ClassIdentity>(m_sourceClasses.values());
70 }
71
72 public List<ClassIdentity> getDestClasses() {
73 List<ClassIdentity> classes = Lists.newArrayList();
74 classes.addAll(m_matchedDestClasses.values());
75 classes.addAll(m_unmatchedDestClasses);
76 return classes;
77 }
78
79 public BiMap<ClassIdentity,ClassIdentity> getUniqueMatches() {
80 BiMap<ClassIdentity,ClassIdentity> uniqueMatches = HashBiMap.create();
81 for (ClassIdentity sourceClass : m_sourceClasses.keySet()) {
82 Collection<ClassIdentity> matchedSourceClasses = m_sourceClasses.get(sourceClass);
83 Collection<ClassIdentity> matchedDestClasses = m_matchedDestClasses.get(sourceClass);
84 if (matchedSourceClasses.size() == 1 && matchedDestClasses.size() == 1) {
85 ClassIdentity matchedSourceClass = matchedSourceClasses.iterator().next();
86 ClassIdentity matchedDestClass = matchedDestClasses.iterator().next();
87 uniqueMatches.put(matchedSourceClass, matchedDestClass);
88 }
89 }
90 return uniqueMatches;
91 }
92
93 public BiMap<List<ClassIdentity>,List<ClassIdentity>> getAmbiguousMatches() {
94 BiMap<List<ClassIdentity>,List<ClassIdentity>> ambiguousMatches = HashBiMap.create();
95 for (ClassIdentity sourceClass : m_sourceClasses.keySet()) {
96 Collection<ClassIdentity> matchedSourceClasses = m_sourceClasses.get(sourceClass);
97 Collection<ClassIdentity> matchedDestClasses = m_matchedDestClasses.get(sourceClass);
98 if (matchedSourceClasses.size() > 1 && matchedDestClasses.size() > 1) {
99 ambiguousMatches.put(
100 new ArrayList<ClassIdentity>(matchedSourceClasses),
101 new ArrayList<ClassIdentity>(matchedDestClasses)
102 );
103 }
104 }
105 return ambiguousMatches;
106 }
107
108 public int getNumAmbiguousSourceMatches() {
109 int num = 0;
110 for (Map.Entry<List<ClassIdentity>,List<ClassIdentity>> entry : getAmbiguousMatches().entrySet()) {
111 num += entry.getKey().size();
112 }
113 return num;
114 }
115
116 public int getNumAmbiguousDestMatches() {
117 int num = 0;
118 for (Map.Entry<List<ClassIdentity>,List<ClassIdentity>> entry : getAmbiguousMatches().entrySet()) {
119 num += entry.getValue().size();
120 }
121 return num;
122 }
123
124 public List<ClassIdentity> getUnmatchedSourceClasses() {
125 List<ClassIdentity> classes = Lists.newArrayList();
126 for (ClassIdentity sourceClass : getSourceClasses()) {
127 if (m_matchedDestClasses.get(sourceClass).isEmpty()) {
128 classes.add(sourceClass);
129 }
130 }
131 return classes;
132 }
133
134 public List<ClassIdentity> getUnmatchedDestClasses() {
135 return new ArrayList<ClassIdentity>(m_unmatchedDestClasses);
136 }
137
138 public Map<String,Map.Entry<ClassIdentity,List<ClassIdentity>>> getIndex() {
139 Map<String,Map.Entry<ClassIdentity,List<ClassIdentity>>> conversion = Maps.newHashMap();
140 for (Map.Entry<ClassIdentity,ClassIdentity> entry : getUniqueMatches().entrySet()) {
141 conversion.put(
142 entry.getKey().getClassEntry().getName(),
143 new AbstractMap.SimpleEntry<ClassIdentity,List<ClassIdentity>>(entry.getKey(), Arrays.asList(entry.getValue()))
144 );
145 }
146 for (Map.Entry<List<ClassIdentity>,List<ClassIdentity>> entry : getAmbiguousMatches().entrySet()) {
147 for (ClassIdentity sourceClass : entry.getKey()) {
148 conversion.put(
149 sourceClass.getClassEntry().getName(),
150 new AbstractMap.SimpleEntry<ClassIdentity,List<ClassIdentity>>(sourceClass, entry.getValue())
151 );
152 }
153 }
154 for (ClassIdentity sourceClass : getUnmatchedSourceClasses()) {
155 conversion.put(
156 sourceClass.getClassEntry().getName(),
157 new AbstractMap.SimpleEntry<ClassIdentity,List<ClassIdentity>>(sourceClass, getUnmatchedDestClasses())
158 );
159 }
160 return conversion;
161 }
162
163 @Override
164 public String toString() {
165 StringBuilder buf = new StringBuilder();
166 buf.append(String.format("%12s%8s%8s\n", "", "Source", "Dest"));
167 buf.append(String.format("%12s%8d%8d\n", "Classes", getSourceClasses().size(), getDestClasses().size()));
168 buf.append(String.format("%12s%8d%8d\n", "Unique", getUniqueMatches().size(), getUniqueMatches().size()));
169 buf.append(String.format("%12s%8d%8d\n", "Ambiguous", getNumAmbiguousSourceMatches(), getNumAmbiguousDestMatches()));
170 buf.append(String.format("%12s%8d%8d\n", "Unmatched", getUnmatchedSourceClasses().size(), getUnmatchedDestClasses().size()));
171 return buf.toString();
172 }
173}
diff --git a/src/cuchaz/enigma/convert/ClassNamer.java b/src/cuchaz/enigma/convert/ClassNamer.java
new file mode 100644
index 0000000..1b6e81c
--- /dev/null
+++ b/src/cuchaz/enigma/convert/ClassNamer.java
@@ -0,0 +1,64 @@
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.util.Map;
14
15import com.google.common.collect.BiMap;
16import com.google.common.collect.Maps;
17
18public class ClassNamer {
19
20 public interface SidedClassNamer {
21 String getName(String name);
22 }
23
24 private Map<String,String> m_sourceNames;
25 private Map<String,String> m_destNames;
26
27 public ClassNamer(BiMap<ClassIdentity,ClassIdentity> mappings) {
28 // convert the identity mappings to name maps
29 m_sourceNames = Maps.newHashMap();
30 m_destNames = Maps.newHashMap();
31 int i = 0;
32 for (Map.Entry<ClassIdentity,ClassIdentity> entry : mappings.entrySet()) {
33 String name = String.format("M%04d", i++);
34 m_sourceNames.put(entry.getKey().getClassEntry().getName(), name);
35 m_destNames.put(entry.getValue().getClassEntry().getName(), name);
36 }
37 }
38
39 public String getSourceName(String name) {
40 return m_sourceNames.get(name);
41 }
42
43 public String getDestName(String name) {
44 return m_destNames.get(name);
45 }
46
47 public SidedClassNamer getSourceNamer() {
48 return new SidedClassNamer() {
49 @Override
50 public String getName(String name) {
51 return getSourceName(name);
52 }
53 };
54 }
55
56 public SidedClassNamer getDestNamer() {
57 return new SidedClassNamer() {
58 @Override
59 public String getName(String name) {
60 return getDestName(name);
61 }
62 };
63 }
64}