From 52ab426d8fad3dbee7e728f523a35af94facebda Mon Sep 17 00:00:00 2001 From: jeff Date: Tue, 3 Feb 2015 22:00:53 -0500 Subject: oops, don't depend on local procyon project --- src/cuchaz/enigma/convert/ClassIdentity.java | 411 ++++++++++++++++++++++++++ src/cuchaz/enigma/convert/ClassMatcher.java | 415 +++++++++++++++++++++++++++ src/cuchaz/enigma/convert/ClassMatching.java | 173 +++++++++++ src/cuchaz/enigma/convert/ClassNamer.java | 64 +++++ 4 files changed, 1063 insertions(+) create mode 100644 src/cuchaz/enigma/convert/ClassIdentity.java create mode 100644 src/cuchaz/enigma/convert/ClassMatcher.java create mode 100644 src/cuchaz/enigma/convert/ClassMatching.java create mode 100644 src/cuchaz/enigma/convert/ClassNamer.java (limited to 'src/cuchaz/enigma/convert') 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 @@ +/******************************************************************************* + * Copyright (c) 2014 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ +package cuchaz.enigma.convert; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; + +import javassist.CannotCompileException; +import javassist.CtBehavior; +import javassist.CtClass; +import javassist.CtConstructor; +import javassist.CtField; +import javassist.CtMethod; +import javassist.bytecode.BadBytecode; +import javassist.bytecode.CodeIterator; +import javassist.bytecode.ConstPool; +import javassist.bytecode.Descriptor; +import javassist.bytecode.Opcode; +import javassist.expr.ConstructorCall; +import javassist.expr.ExprEditor; +import javassist.expr.FieldAccess; +import javassist.expr.MethodCall; +import javassist.expr.NewExpr; + +import com.google.common.collect.HashMultiset; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Multiset; + +import cuchaz.enigma.Constants; +import cuchaz.enigma.Util; +import cuchaz.enigma.analysis.ClassImplementationsTreeNode; +import cuchaz.enigma.analysis.EntryReference; +import cuchaz.enigma.analysis.JarIndex; +import cuchaz.enigma.bytecode.ConstPoolEditor; +import cuchaz.enigma.bytecode.InfoType; +import cuchaz.enigma.bytecode.accessors.ConstInfoAccessor; +import cuchaz.enigma.convert.ClassNamer.SidedClassNamer; +import cuchaz.enigma.mapping.BehaviorEntry; +import cuchaz.enigma.mapping.ClassEntry; +import cuchaz.enigma.mapping.ConstructorEntry; +import cuchaz.enigma.mapping.Entry; +import cuchaz.enigma.mapping.FieldEntry; +import cuchaz.enigma.mapping.MethodEntry; +import cuchaz.enigma.mapping.SignatureUpdater; +import cuchaz.enigma.mapping.SignatureUpdater.ClassNameUpdater; + +public class ClassIdentity { + + private ClassEntry m_classEntry; + private SidedClassNamer m_namer; + private Multiset m_fields; + private Multiset m_methods; + private Multiset m_constructors; + private String m_staticInitializer; + private String m_extends; + private Multiset m_implements; + private Multiset m_implementations; + private Multiset m_references; + + public ClassIdentity(CtClass c, SidedClassNamer namer, JarIndex index, boolean useReferences) { + m_namer = namer; + + // stuff from the bytecode + + m_classEntry = new ClassEntry(Descriptor.toJvmName(c.getName())); + m_fields = HashMultiset.create(); + for (CtField field : c.getDeclaredFields()) { + m_fields.add(scrubSignature(field.getSignature())); + } + m_methods = HashMultiset.create(); + for (CtMethod method : c.getDeclaredMethods()) { + m_methods.add(scrubSignature(method.getSignature()) + "0x" + getBehaviorSignature(method)); + } + m_constructors = HashMultiset.create(); + for (CtConstructor constructor : c.getDeclaredConstructors()) { + m_constructors.add(scrubSignature(constructor.getSignature()) + "0x" + getBehaviorSignature(constructor)); + } + m_staticInitializer = ""; + if (c.getClassInitializer() != null) { + m_staticInitializer = getBehaviorSignature(c.getClassInitializer()); + } + m_extends = ""; + if (c.getClassFile().getSuperclass() != null) { + m_extends = scrubClassName(c.getClassFile().getSuperclass()); + } + m_implements = HashMultiset.create(); + for (String interfaceName : c.getClassFile().getInterfaces()) { + m_implements.add(scrubClassName(interfaceName)); + } + + // stuff from the jar index + + m_implementations = HashMultiset.create(); + ClassImplementationsTreeNode implementationsNode = index.getClassImplementations(null, m_classEntry); + if (implementationsNode != null) { + @SuppressWarnings("unchecked") + Enumeration implementations = implementationsNode.children(); + while (implementations.hasMoreElements()) { + ClassImplementationsTreeNode node = implementations.nextElement(); + m_implementations.add(scrubClassName(node.getClassEntry().getName())); + } + } + + m_references = HashMultiset.create(); + if (useReferences) { + for (CtField field : c.getDeclaredFields()) { + FieldEntry fieldEntry = new FieldEntry(m_classEntry, field.getName()); + for (EntryReference reference : index.getFieldReferences(fieldEntry)) { + addReference(reference); + } + } + for (CtMethod method : c.getDeclaredMethods()) { + MethodEntry methodEntry = new MethodEntry(m_classEntry, method.getName(), method.getSignature()); + for (EntryReference reference : index.getBehaviorReferences(methodEntry)) { + addReference(reference); + } + } + for (CtConstructor constructor : c.getDeclaredConstructors()) { + ConstructorEntry constructorEntry = new ConstructorEntry(m_classEntry, constructor.getSignature()); + for (EntryReference reference : index.getBehaviorReferences(constructorEntry)) { + addReference(reference); + } + } + } + } + + private void addReference(EntryReference reference) { + if (reference.context.getSignature() != null) { + m_references.add(String.format("%s_%s", scrubClassName(reference.context.getClassName()), scrubSignature(reference.context.getSignature()))); + } else { + m_references.add(String.format("%s_", scrubClassName(reference.context.getClassName()))); + } + } + + public ClassEntry getClassEntry() { + return m_classEntry; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append("class: "); + buf.append(m_classEntry.getName()); + buf.append(" "); + buf.append(hashCode()); + buf.append("\n"); + for (String field : m_fields) { + buf.append("\tfield "); + buf.append(field); + buf.append("\n"); + } + for (String method : m_methods) { + buf.append("\tmethod "); + buf.append(method); + buf.append("\n"); + } + for (String constructor : m_constructors) { + buf.append("\tconstructor "); + buf.append(constructor); + buf.append("\n"); + } + if (m_staticInitializer.length() > 0) { + buf.append("\tinitializer "); + buf.append(m_staticInitializer); + buf.append("\n"); + } + if (m_extends.length() > 0) { + buf.append("\textends "); + buf.append(m_extends); + buf.append("\n"); + } + for (String interfaceName : m_implements) { + buf.append("\timplements "); + buf.append(interfaceName); + buf.append("\n"); + } + for (String implementation : m_implementations) { + buf.append("\timplemented by "); + buf.append(implementation); + buf.append("\n"); + } + for (String reference : m_references) { + buf.append("\treference "); + buf.append(reference); + buf.append("\n"); + } + return buf.toString(); + } + + private String scrubClassName(String className) { + return scrubSignature("L" + Descriptor.toJvmName(className) + ";"); + } + + private String scrubSignature(String signature) { + return SignatureUpdater.update(signature, new ClassNameUpdater() { + private Map m_classNames = Maps.newHashMap(); + + @Override + public String update(String className) { + // classes not in the none package can be passed through + ClassEntry classEntry = new ClassEntry(className); + if (!classEntry.getPackageName().equals(Constants.NonePackage)) { + return className; + } + + // is this class ourself? + if (className.equals(m_classEntry.getName())) { + return "CSelf"; + } + + // try the namer + if (m_namer != null) { + String newName = m_namer.getName(className); + if (newName != null) { + return newName; + } + } + + // otherwise, use local naming + if (!m_classNames.containsKey(className)) { + m_classNames.put(className, getNewClassName()); + } + return m_classNames.get(className); + } + + private String getNewClassName() { + return String.format("C%03d", m_classNames.size()); + } + }); + } + + private boolean isClassMatchedUniquely(String className) { + return m_namer != null && m_namer.getName(Descriptor.toJvmName(className)) != null; + } + + private String getBehaviorSignature(CtBehavior behavior) { + try { + // does this method have an implementation? + if (behavior.getMethodInfo().getCodeAttribute() == null) { + return "(none)"; + } + + // compute the hash from the opcodes + ConstPool constants = behavior.getMethodInfo().getConstPool(); + final MessageDigest digest = MessageDigest.getInstance("MD5"); + CodeIterator iter = behavior.getMethodInfo().getCodeAttribute().iterator(); + while (iter.hasNext()) { + int pos = iter.next(); + + // update the hash with the opcode + int opcode = iter.byteAt(pos); + digest.update((byte)opcode); + + switch (opcode) { + case Opcode.LDC: { + int constIndex = iter.byteAt(pos + 1); + updateHashWithConstant(digest, constants, constIndex); + } + break; + + case Opcode.LDC_W: + case Opcode.LDC2_W: { + int constIndex = (iter.byteAt(pos + 1) << 8) | iter.byteAt(pos + 2); + updateHashWithConstant(digest, constants, constIndex); + } + break; + } + } + + // update hash with method and field accesses + behavior.instrument(new ExprEditor() { + @Override + public void edit(MethodCall call) { + updateHashWithString(digest, scrubClassName(call.getClassName())); + updateHashWithString(digest, scrubSignature(call.getSignature())); + if (isClassMatchedUniquely(call.getClassName())) { + updateHashWithString(digest, call.getMethodName()); + } + } + + @Override + public void edit(FieldAccess access) { + updateHashWithString(digest, scrubClassName(access.getClassName())); + updateHashWithString(digest, scrubSignature(access.getSignature())); + if (isClassMatchedUniquely(access.getClassName())) { + updateHashWithString(digest, access.getFieldName()); + } + } + + @Override + public void edit(ConstructorCall call) { + updateHashWithString(digest, scrubClassName(call.getClassName())); + updateHashWithString(digest, scrubSignature(call.getSignature())); + } + + @Override + public void edit(NewExpr expr) { + updateHashWithString(digest, scrubClassName(expr.getClassName())); + } + }); + + // convert the hash to a hex string + return toHex(digest.digest()); + } catch (BadBytecode | NoSuchAlgorithmException | CannotCompileException ex) { + throw new Error(ex); + } + } + + private void updateHashWithConstant(MessageDigest digest, ConstPool constants, int index) { + ConstPoolEditor editor = new ConstPoolEditor(constants); + ConstInfoAccessor item = editor.getItem(index); + if (item.getType() == InfoType.StringInfo) { + updateHashWithString(digest, constants.getStringInfo(index)); + } + // TODO: other constants + } + + private void updateHashWithString(MessageDigest digest, String val) { + try { + digest.update(val.getBytes("UTF8")); + } catch (UnsupportedEncodingException ex) { + throw new Error(ex); + } + } + + private String toHex(byte[] bytes) { + // function taken from: + // http://stackoverflow.com/questions/9655181/convert-from-byte-array-to-hex-string-in-java + final char[] hexArray = "0123456789ABCDEF".toCharArray(); + char[] hexChars = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } + + @Override + public boolean equals(Object other) { + if (other instanceof ClassIdentity) { + return equals((ClassIdentity)other); + } + return false; + } + + public boolean equals(ClassIdentity other) { + return m_fields.equals(other.m_fields) + && m_methods.equals(other.m_methods) + && m_constructors.equals(other.m_constructors) + && m_staticInitializer.equals(other.m_staticInitializer) + && m_extends.equals(other.m_extends) + && m_implements.equals(other.m_implements) + && m_implementations.equals(other.m_implementations) + && m_references.equals(other.m_references); + } + + @Override + public int hashCode() { + List objs = Lists.newArrayList(); + objs.addAll(m_fields); + objs.addAll(m_methods); + objs.addAll(m_constructors); + objs.add(m_staticInitializer); + objs.add(m_extends); + objs.addAll(m_implements); + objs.addAll(m_implementations); + objs.addAll(m_references); + return Util.combineHashesOrdered(objs); + } + + public int getMatchScore(ClassIdentity other) { + return getNumMatches(m_fields, other.m_fields) + + getNumMatches(m_methods, other.m_methods) + + getNumMatches(m_constructors, other.m_constructors); + } + + public int getMaxMatchScore() { + return m_fields.size() + m_methods.size() + m_constructors.size(); + } + + public boolean matches(CtClass c) { + // just compare declaration counts + return m_fields.size() == c.getDeclaredFields().length + && m_methods.size() == c.getDeclaredMethods().length + && m_constructors.size() == c.getDeclaredConstructors().length; + } + + private int getNumMatches(Multiset a, Multiset b) { + int numMatches = 0; + for (String val : a) { + if (b.contains(val)) { + numMatches++; + } + } + return numMatches; + } +} 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 @@ +/******************************************************************************* + * Copyright (c) 2014 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ +package cuchaz.enigma.convert; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.jar.JarFile; + +import javassist.CtBehavior; +import javassist.CtClass; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; +import com.google.common.collect.Sets; + +import cuchaz.enigma.TranslatingTypeLoader; +import cuchaz.enigma.analysis.JarIndex; +import cuchaz.enigma.convert.ClassNamer.SidedClassNamer; +import cuchaz.enigma.mapping.ClassEntry; +import cuchaz.enigma.mapping.ClassMapping; +import cuchaz.enigma.mapping.MappingParseException; +import cuchaz.enigma.mapping.Mappings; +import cuchaz.enigma.mapping.MappingsReader; +import cuchaz.enigma.mapping.MappingsWriter; +import cuchaz.enigma.mapping.MethodEntry; +import cuchaz.enigma.mapping.MethodMapping; + +public class ClassMatcher { + + public static void main(String[] args) throws IOException, MappingParseException { + // TEMP + JarFile sourceJar = new JarFile(new File("input/1.8-pre3.jar")); + JarFile destJar = new JarFile(new File("input/1.8.jar")); + File inMappingsFile = new File("../Enigma Mappings/1.8-pre3.mappings"); + File outMappingsFile = new File("../Enigma Mappings/1.8.mappings"); + + // define a matching to use when the automated system cannot find a match + Map fallbackMatching = Maps.newHashMap(); + fallbackMatching.put("none/ayb", "none/ayf"); + fallbackMatching.put("none/ayd", "none/ayd"); + fallbackMatching.put("none/bgk", "unknown/bgk"); + + // do the conversion + Mappings mappings = new MappingsReader().read(new FileReader(inMappingsFile)); + convertMappings(sourceJar, destJar, mappings, fallbackMatching); + + // write out the converted mappings + FileWriter writer = new FileWriter(outMappingsFile); + new MappingsWriter().write(writer, mappings); + writer.close(); + System.out.println("Wrote converted mappings to:\n\t" + outMappingsFile.getAbsolutePath()); + } + + private static void convertMappings(JarFile sourceJar, JarFile destJar, Mappings mappings, Map fallbackMatching) { + // index jars + System.out.println("Indexing source jar..."); + JarIndex sourceIndex = new JarIndex(); + sourceIndex.indexJar(sourceJar, false); + System.out.println("Indexing dest jar..."); + JarIndex destIndex = new JarIndex(); + destIndex.indexJar(destJar, false); + TranslatingTypeLoader sourceLoader = new TranslatingTypeLoader(sourceJar, sourceIndex); + TranslatingTypeLoader destLoader = new TranslatingTypeLoader(destJar, destIndex); + + // compute the matching + ClassMatching matching = computeMatching(sourceIndex, sourceLoader, destIndex, destLoader); + Map>> matchingIndex = matching.getIndex(); + + // get all the obf class names used in the mappings + Set usedClassNames = mappings.getAllObfClassNames(); + Set allClassNames = Sets.newHashSet(); + for (ClassEntry classEntry : sourceIndex.getObfClassEntries()) { + allClassNames.add(classEntry.getName()); + } + usedClassNames.retainAll(allClassNames); + System.out.println("Used " + usedClassNames.size() + " classes in the mappings"); + + // probabilistically match the non-uniquely-matched source classes + for (Map.Entry> entry : matchingIndex.values()) { + ClassIdentity sourceClass = entry.getKey(); + List destClasses = entry.getValue(); + + // skip classes that are uniquely matched + if (destClasses.size() == 1) { + continue; + } + + // skip classes that aren't used in the mappings + if (!usedClassNames.contains(sourceClass.getClassEntry().getName())) { + continue; + } + + System.out.println("No exact match for source class " + sourceClass.getClassEntry()); + + // find the closest classes + Multimap scoredMatches = ArrayListMultimap.create(); + for (ClassIdentity c : destClasses) { + scoredMatches.put(sourceClass.getMatchScore(c), c); + } + List scores = new ArrayList(scoredMatches.keySet()); + Collections.sort(scores, Collections.reverseOrder()); + printScoredMatches(sourceClass.getMaxMatchScore(), scores, scoredMatches); + + // does the best match have a non-zero score and the same name? + int bestScore = scores.get(0); + Collection bestMatches = scoredMatches.get(bestScore); + if (bestScore > 0 && bestMatches.size() == 1) { + ClassIdentity bestMatch = bestMatches.iterator().next(); + if (bestMatch.getClassEntry().equals(sourceClass.getClassEntry())) { + // use it + System.out.println("\tAutomatically choosing likely match: " + bestMatch.getClassEntry().getName()); + destClasses.clear(); + destClasses.add(bestMatch); + } + } + } + + // group the matching into unique and non-unique matches + BiMap matchedClassNames = HashBiMap.create(); + Set unmatchedSourceClassNames = Sets.newHashSet(); + for (String className : usedClassNames) { + // is there a match for this class? + Map.Entry> entry = matchingIndex.get(className); + ClassIdentity sourceClass = entry.getKey(); + List matches = entry.getValue(); + + if (matches.size() == 1) { + // unique match! We're good to go! + matchedClassNames.put(sourceClass.getClassEntry().getName(), matches.get(0).getClassEntry().getName()); + } else { + // no match, check the fallback matching + String fallbackMatch = fallbackMatching.get(className); + if (fallbackMatch != null) { + matchedClassNames.put(sourceClass.getClassEntry().getName(), fallbackMatch); + } else { + unmatchedSourceClassNames.add(className); + } + } + } + + // report unmatched classes + if (!unmatchedSourceClassNames.isEmpty()) { + System.err.println("ERROR: there were unmatched classes!"); + for (String className : unmatchedSourceClassNames) { + System.err.println("\t" + className); + } + return; + } + + // get the class name changes from the matched class names + Map classChanges = Maps.newHashMap(); + for (Map.Entry entry : matchedClassNames.entrySet()) { + if (!entry.getKey().equals(entry.getValue())) { + classChanges.put(entry.getKey(), entry.getValue()); + System.out.println(String.format("Class change: %s -> %s", entry.getKey(), entry.getValue())); + /* DEBUG + System.out.println(String.format("\n%s\n%s", + new ClassIdentity(sourceLoader.loadClass(entry.getKey()), null, sourceIndex, false, false), + new ClassIdentity( destLoader.loadClass(entry.getValue()), null, destIndex, false, false) + )); + */ + } + } + + // sort the changes so classes are renamed in the correct order + // ie. if we have the mappings a->b, b->c, we have to apply b->c before a->b + LinkedHashMap orderedClassChanges = Maps.newLinkedHashMap(); + int numChangesLeft = classChanges.size(); + while (!classChanges.isEmpty()) { + Iterator> iter = classChanges.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry entry = iter.next(); + if (classChanges.get(entry.getValue()) == null) { + orderedClassChanges.put(entry.getKey(), entry.getValue()); + iter.remove(); + } + } + + // did we remove any changes? + if (numChangesLeft - classChanges.size() > 0) { + // keep going + numChangesLeft = classChanges.size(); + } else { + // can't sort anymore. There must be a loop + break; + } + } + if (classChanges.size() > 0) { + throw new Error(String.format("Unable to sort %d/%d class changes!", classChanges.size(), matchedClassNames.size())); + } + + // convert the mappings in the correct class order + for (Map.Entry entry : orderedClassChanges.entrySet()) { + mappings.renameObfClass(entry.getKey(), entry.getValue()); + } + + // check the method matches + System.out.println("Checking methods..."); + for (ClassMapping classMapping : mappings.classes()) { + ClassEntry classEntry = new ClassEntry(classMapping.getObfName()); + for (MethodMapping methodMapping : classMapping.methods()) { + + // skip constructors + if (methodMapping.getObfName().equals("")) { + continue; + } + + MethodEntry methodEntry = new MethodEntry( + classEntry, + methodMapping.getObfName(), + methodMapping.getObfSignature() + ); + if (!destIndex.containsObfBehavior(methodEntry)) { + System.err.println("WARNING: method doesn't match: " + methodEntry); + + // show the available methods + System.err.println("\tAvailable dest methods:"); + CtClass c = destLoader.loadClass(classMapping.getObfName()); + for (CtBehavior behavior : c.getDeclaredBehaviors()) { + MethodEntry declaredMethodEntry = new MethodEntry( + new ClassEntry(classMapping.getObfName()), + behavior.getName(), + behavior.getSignature() + ); + System.err.println("\t\t" + declaredMethodEntry); + } + + System.err.println("\tAvailable source methods:"); + c = sourceLoader.loadClass(matchedClassNames.inverse().get(classMapping.getObfName())); + for (CtBehavior behavior : c.getDeclaredBehaviors()) { + MethodEntry declaredMethodEntry = new MethodEntry( + new ClassEntry(classMapping.getObfName()), + behavior.getName(), + behavior.getSignature() + ); + System.err.println("\t\t" + declaredMethodEntry); + } + } + } + } + + System.out.println("Done!"); + } + + public static ClassMatching computeMatching(JarIndex sourceIndex, TranslatingTypeLoader sourceLoader, JarIndex destIndex, TranslatingTypeLoader destLoader) { + + System.out.println("Matching classes..."); + + ClassMatching matching = null; + for (boolean useReferences : Arrays.asList(false, true)) { + int numMatches = 0; + do { + SidedClassNamer sourceNamer = null; + SidedClassNamer destNamer = null; + if (matching != null) { + // build a class namer + ClassNamer namer = new ClassNamer(matching.getUniqueMatches()); + sourceNamer = namer.getSourceNamer(); + destNamer = namer.getDestNamer(); + + // note the number of matches + numMatches = matching.getUniqueMatches().size(); + } + + // get the entries left to match + Set sourceClassEntries = Sets.newHashSet(); + Set destClassEntries = Sets.newHashSet(); + if (matching == null) { + sourceClassEntries.addAll(sourceIndex.getObfClassEntries()); + destClassEntries.addAll(destIndex.getObfClassEntries()); + matching = new ClassMatching(); + } else { + for (Map.Entry,List> entry : matching.getAmbiguousMatches().entrySet()) { + for (ClassIdentity c : entry.getKey()) { + sourceClassEntries.add(c.getClassEntry()); + matching.removeSource(c); + } + for (ClassIdentity c : entry.getValue()) { + destClassEntries.add(c.getClassEntry()); + matching.removeDest(c); + } + } + for (ClassIdentity c : matching.getUnmatchedSourceClasses()) { + sourceClassEntries.add(c.getClassEntry()); + matching.removeSource(c); + } + for (ClassIdentity c : matching.getUnmatchedDestClasses()) { + destClassEntries.add(c.getClassEntry()); + matching.removeDest(c); + } + } + + // compute a matching for the classes + for (ClassEntry classEntry : sourceClassEntries) { + CtClass c = sourceLoader.loadClass(classEntry.getName()); + ClassIdentity sourceClass = new ClassIdentity(c, sourceNamer, sourceIndex, useReferences); + matching.addSource(sourceClass); + } + for (ClassEntry classEntry : destClassEntries) { + CtClass c = destLoader.loadClass(classEntry.getName()); + ClassIdentity destClass = new ClassIdentity(c, destNamer, destIndex, useReferences); + matching.matchDestClass(destClass); + } + + // TEMP + System.out.println(matching); + } while (matching.getUniqueMatches().size() - numMatches > 0); + } + + // check the class matches + System.out.println("Checking class matches..."); + ClassNamer namer = new ClassNamer(matching.getUniqueMatches()); + SidedClassNamer sourceNamer = namer.getSourceNamer(); + SidedClassNamer destNamer = namer.getDestNamer(); + for (Map.Entry entry : matching.getUniqueMatches().entrySet()) { + + // check source + ClassIdentity sourceClass = entry.getKey(); + CtClass sourceC = sourceLoader.loadClass(sourceClass.getClassEntry().getName()); + assert (sourceC != null) : "Unable to load source class " + sourceClass.getClassEntry(); + assert (sourceClass.matches(sourceC)) : "Source " + sourceClass + " doesn't match " + new ClassIdentity(sourceC, sourceNamer, sourceIndex, false); + + // check dest + ClassIdentity destClass = entry.getValue(); + CtClass destC = destLoader.loadClass(destClass.getClassEntry().getName()); + assert (destC != null) : "Unable to load dest class " + destClass.getClassEntry(); + assert (destClass.matches(destC)) : "Dest " + destClass + " doesn't match " + new ClassIdentity(destC, destNamer, destIndex, false); + } + + // warn about the ambiguous matchings + List,List>> ambiguousMatches = new ArrayList,List>>(matching.getAmbiguousMatches().entrySet()); + Collections.sort(ambiguousMatches, new Comparator,List>>() { + @Override + public int compare(Map.Entry,List> a, Map.Entry,List> b) { + String aName = a.getKey().get(0).getClassEntry().getName(); + String bName = b.getKey().get(0).getClassEntry().getName(); + return aName.compareTo(bName); + } + }); + for (Map.Entry,List> entry : ambiguousMatches) { + System.out.println("Ambiguous matching:"); + System.out.println("\tSource: " + getClassNames(entry.getKey())); + System.out.println("\tDest: " + getClassNames(entry.getValue())); + } + + /* DEBUG + Map.Entry,List> entry = ambiguousMatches.get( 7 ); + for (ClassIdentity c : entry.getKey()) { + System.out.println(c); + } + for(ClassIdentity c : entry.getKey()) { + System.out.println(decompile(sourceLoader, c.getClassEntry())); + } + */ + + return matching; + } + + private static void printScoredMatches(int maxScore, List scores, Multimap scoredMatches) { + int numScoredMatchesShown = 0; + for (int score : scores) { + for (ClassIdentity scoredMatch : scoredMatches.get(score)) { + System.out.println(String.format("\tScore: %3d %3.0f%% %s", score, 100.0 * score / maxScore, scoredMatch.getClassEntry().getName())); + if (numScoredMatchesShown++ > 10) { + return; + } + } + } + } + + private static List getClassNames(Collection classes) { + List out = Lists.newArrayList(); + for (ClassIdentity c : classes) { + out.add(c.getClassEntry().getName()); + } + Collections.sort(out); + return out; + } + + /* DEBUG + private static String decompile(TranslatingTypeLoader loader, ClassEntry classEntry) { + PlainTextOutput output = new PlainTextOutput(); + DecompilerSettings settings = DecompilerSettings.javaDefaults(); + settings.setForceExplicitImports(true); + settings.setShowSyntheticMembers(true); + settings.setTypeLoader(loader); + Decompiler.decompile(classEntry.getName(), output, settings); + return output.toString(); + } + */ +} 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 @@ +/******************************************************************************* + * Copyright (c) 2014 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ +package cuchaz.enigma.convert; + +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; + +public class ClassMatching { + + private Multimap m_sourceClasses; + private Multimap m_matchedDestClasses; + private List m_unmatchedDestClasses; + + public ClassMatching() { + m_sourceClasses = ArrayListMultimap.create(); + m_matchedDestClasses = ArrayListMultimap.create(); + m_unmatchedDestClasses = Lists.newArrayList(); + } + + public void addSource(ClassIdentity c) { + m_sourceClasses.put(c, c); + } + + public void matchDestClass(ClassIdentity destClass) { + Collection matchedSourceClasses = m_sourceClasses.get(destClass); + if (matchedSourceClasses.isEmpty()) { + // no match + m_unmatchedDestClasses.add(destClass); + } else { + // found a match + m_matchedDestClasses.put(destClass, destClass); + + // DEBUG + ClassIdentity sourceClass = matchedSourceClasses.iterator().next(); + assert (sourceClass.hashCode() == destClass.hashCode()); + assert (sourceClass.equals(destClass)); + } + } + + public void removeSource(ClassIdentity sourceClass) { + m_sourceClasses.remove(sourceClass, sourceClass); + } + + public void removeDest(ClassIdentity destClass) { + m_matchedDestClasses.remove(destClass, destClass); + m_unmatchedDestClasses.remove(destClass); + } + + public List getSourceClasses() { + return new ArrayList(m_sourceClasses.values()); + } + + public List getDestClasses() { + List classes = Lists.newArrayList(); + classes.addAll(m_matchedDestClasses.values()); + classes.addAll(m_unmatchedDestClasses); + return classes; + } + + public BiMap getUniqueMatches() { + BiMap uniqueMatches = HashBiMap.create(); + for (ClassIdentity sourceClass : m_sourceClasses.keySet()) { + Collection matchedSourceClasses = m_sourceClasses.get(sourceClass); + Collection matchedDestClasses = m_matchedDestClasses.get(sourceClass); + if (matchedSourceClasses.size() == 1 && matchedDestClasses.size() == 1) { + ClassIdentity matchedSourceClass = matchedSourceClasses.iterator().next(); + ClassIdentity matchedDestClass = matchedDestClasses.iterator().next(); + uniqueMatches.put(matchedSourceClass, matchedDestClass); + } + } + return uniqueMatches; + } + + public BiMap,List> getAmbiguousMatches() { + BiMap,List> ambiguousMatches = HashBiMap.create(); + for (ClassIdentity sourceClass : m_sourceClasses.keySet()) { + Collection matchedSourceClasses = m_sourceClasses.get(sourceClass); + Collection matchedDestClasses = m_matchedDestClasses.get(sourceClass); + if (matchedSourceClasses.size() > 1 && matchedDestClasses.size() > 1) { + ambiguousMatches.put( + new ArrayList(matchedSourceClasses), + new ArrayList(matchedDestClasses) + ); + } + } + return ambiguousMatches; + } + + public int getNumAmbiguousSourceMatches() { + int num = 0; + for (Map.Entry,List> entry : getAmbiguousMatches().entrySet()) { + num += entry.getKey().size(); + } + return num; + } + + public int getNumAmbiguousDestMatches() { + int num = 0; + for (Map.Entry,List> entry : getAmbiguousMatches().entrySet()) { + num += entry.getValue().size(); + } + return num; + } + + public List getUnmatchedSourceClasses() { + List classes = Lists.newArrayList(); + for (ClassIdentity sourceClass : getSourceClasses()) { + if (m_matchedDestClasses.get(sourceClass).isEmpty()) { + classes.add(sourceClass); + } + } + return classes; + } + + public List getUnmatchedDestClasses() { + return new ArrayList(m_unmatchedDestClasses); + } + + public Map>> getIndex() { + Map>> conversion = Maps.newHashMap(); + for (Map.Entry entry : getUniqueMatches().entrySet()) { + conversion.put( + entry.getKey().getClassEntry().getName(), + new AbstractMap.SimpleEntry>(entry.getKey(), Arrays.asList(entry.getValue())) + ); + } + for (Map.Entry,List> entry : getAmbiguousMatches().entrySet()) { + for (ClassIdentity sourceClass : entry.getKey()) { + conversion.put( + sourceClass.getClassEntry().getName(), + new AbstractMap.SimpleEntry>(sourceClass, entry.getValue()) + ); + } + } + for (ClassIdentity sourceClass : getUnmatchedSourceClasses()) { + conversion.put( + sourceClass.getClassEntry().getName(), + new AbstractMap.SimpleEntry>(sourceClass, getUnmatchedDestClasses()) + ); + } + return conversion; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append(String.format("%12s%8s%8s\n", "", "Source", "Dest")); + buf.append(String.format("%12s%8d%8d\n", "Classes", getSourceClasses().size(), getDestClasses().size())); + buf.append(String.format("%12s%8d%8d\n", "Unique", getUniqueMatches().size(), getUniqueMatches().size())); + buf.append(String.format("%12s%8d%8d\n", "Ambiguous", getNumAmbiguousSourceMatches(), getNumAmbiguousDestMatches())); + buf.append(String.format("%12s%8d%8d\n", "Unmatched", getUnmatchedSourceClasses().size(), getUnmatchedDestClasses().size())); + return buf.toString(); + } +} 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 @@ +/******************************************************************************* + * Copyright (c) 2014 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ +package cuchaz.enigma.convert; + +import java.util.Map; + +import com.google.common.collect.BiMap; +import com.google.common.collect.Maps; + +public class ClassNamer { + + public interface SidedClassNamer { + String getName(String name); + } + + private Map m_sourceNames; + private Map m_destNames; + + public ClassNamer(BiMap mappings) { + // convert the identity mappings to name maps + m_sourceNames = Maps.newHashMap(); + m_destNames = Maps.newHashMap(); + int i = 0; + for (Map.Entry entry : mappings.entrySet()) { + String name = String.format("M%04d", i++); + m_sourceNames.put(entry.getKey().getClassEntry().getName(), name); + m_destNames.put(entry.getValue().getClassEntry().getName(), name); + } + } + + public String getSourceName(String name) { + return m_sourceNames.get(name); + } + + public String getDestName(String name) { + return m_destNames.get(name); + } + + public SidedClassNamer getSourceNamer() { + return new SidedClassNamer() { + @Override + public String getName(String name) { + return getSourceName(name); + } + }; + } + + public SidedClassNamer getDestNamer() { + return new SidedClassNamer() { + @Override + public String getName(String name) { + return getDestName(name); + } + }; + } +} -- cgit v1.2.3