From e3f452250e51b7271f3989c7dfd12e4422934942 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Thu, 21 May 2015 23:30:00 +0100 Subject: Support Gradle alongside SSJB This makes builds faster, simpler and better automated but still keeps Cuchaz happy. :) --- src/cuchaz/enigma/convert/ClassForest.java | 60 +++ src/cuchaz/enigma/convert/ClassIdentifier.java | 54 +++ src/cuchaz/enigma/convert/ClassIdentity.java | 468 +++++++++++++++++++ src/cuchaz/enigma/convert/ClassMatch.java | 88 ++++ src/cuchaz/enigma/convert/ClassMatches.java | 163 +++++++ src/cuchaz/enigma/convert/ClassMatching.java | 155 +++++++ src/cuchaz/enigma/convert/ClassNamer.java | 66 +++ src/cuchaz/enigma/convert/FieldMatches.java | 155 +++++++ src/cuchaz/enigma/convert/MappingsConverter.java | 559 +++++++++++++++++++++++ src/cuchaz/enigma/convert/MatchesReader.java | 113 +++++ src/cuchaz/enigma/convert/MatchesWriter.java | 121 +++++ src/cuchaz/enigma/convert/MemberMatches.java | 159 +++++++ 12 files changed, 2161 insertions(+) create mode 100644 src/cuchaz/enigma/convert/ClassForest.java create mode 100644 src/cuchaz/enigma/convert/ClassIdentifier.java create mode 100644 src/cuchaz/enigma/convert/ClassIdentity.java create mode 100644 src/cuchaz/enigma/convert/ClassMatch.java create mode 100644 src/cuchaz/enigma/convert/ClassMatches.java create mode 100644 src/cuchaz/enigma/convert/ClassMatching.java create mode 100644 src/cuchaz/enigma/convert/ClassNamer.java create mode 100644 src/cuchaz/enigma/convert/FieldMatches.java create mode 100644 src/cuchaz/enigma/convert/MappingsConverter.java create mode 100644 src/cuchaz/enigma/convert/MatchesReader.java create mode 100644 src/cuchaz/enigma/convert/MatchesWriter.java create mode 100644 src/cuchaz/enigma/convert/MemberMatches.java (limited to 'src/cuchaz/enigma/convert') diff --git a/src/cuchaz/enigma/convert/ClassForest.java b/src/cuchaz/enigma/convert/ClassForest.java new file mode 100644 index 0000000..0407730 --- /dev/null +++ b/src/cuchaz/enigma/convert/ClassForest.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ +package cuchaz.enigma.convert; + +import java.util.Collection; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; + +import cuchaz.enigma.mapping.ClassEntry; + + +public class ClassForest { + + private ClassIdentifier m_identifier; + private Multimap m_forest; + + public ClassForest(ClassIdentifier identifier) { + m_identifier = identifier; + m_forest = HashMultimap.create(); + } + + public void addAll(Iterable entries) { + for (ClassEntry entry : entries) { + add(entry); + } + } + + public void add(ClassEntry entry) { + try { + m_forest.put(m_identifier.identify(entry), entry); + } catch (ClassNotFoundException ex) { + throw new Error("Unable to find class " + entry.getName()); + } + } + + public Collection identities() { + return m_forest.keySet(); + } + + public Collection classes() { + return m_forest.values(); + } + + public Collection getClasses(ClassIdentity identity) { + return m_forest.get(identity); + } + + public boolean containsIdentity(ClassIdentity identity) { + return m_forest.containsKey(identity); + } +} diff --git a/src/cuchaz/enigma/convert/ClassIdentifier.java b/src/cuchaz/enigma/convert/ClassIdentifier.java new file mode 100644 index 0000000..ee5e903 --- /dev/null +++ b/src/cuchaz/enigma/convert/ClassIdentifier.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ +package cuchaz.enigma.convert; + +import java.util.Map; +import java.util.jar.JarFile; + +import com.google.common.collect.Maps; + +import javassist.CtClass; +import cuchaz.enigma.TranslatingTypeLoader; +import cuchaz.enigma.analysis.JarIndex; +import cuchaz.enigma.convert.ClassNamer.SidedClassNamer; +import cuchaz.enigma.mapping.ClassEntry; + + +public class ClassIdentifier { + + private JarIndex m_index; + private SidedClassNamer m_namer; + private boolean m_useReferences; + private TranslatingTypeLoader m_loader; + private Map m_cache; + + public ClassIdentifier(JarFile jar, JarIndex index, SidedClassNamer namer, boolean useReferences) { + m_index = index; + m_namer = namer; + m_useReferences = useReferences; + m_loader = new TranslatingTypeLoader(jar, index); + m_cache = Maps.newHashMap(); + } + + public ClassIdentity identify(ClassEntry classEntry) + throws ClassNotFoundException { + ClassIdentity identity = m_cache.get(classEntry); + if (identity == null) { + CtClass c = m_loader.loadClass(classEntry.getName()); + if (c == null) { + throw new ClassNotFoundException(classEntry.getName()); + } + identity = new ClassIdentity(c, m_namer, m_index, m_useReferences); + m_cache.put(classEntry, identity); + } + return identity; + } +} diff --git a/src/cuchaz/enigma/convert/ClassIdentity.java b/src/cuchaz/enigma/convert/ClassIdentity.java new file mode 100644 index 0000000..2e164ae --- /dev/null +++ b/src/cuchaz/enigma/convert/ClassIdentity.java @@ -0,0 +1,468 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.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 java.util.Set; + +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 com.google.common.collect.Sets; + +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.ClassNameReplacer; +import cuchaz.enigma.mapping.Entry; +import cuchaz.enigma.mapping.EntryFactory; +import cuchaz.enigma.mapping.FieldEntry; +import cuchaz.enigma.mapping.Signature; +import cuchaz.enigma.mapping.Type; + +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 Set m_stringLiterals; + private Multiset m_implementations; + private Multiset m_references; + private String m_outer; + + private final ClassNameReplacer m_classNameReplacer = new ClassNameReplacer() { + + private Map m_classNames = Maps.newHashMap(); + + @Override + public String replace(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()); + } + }; + + 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(scrubType(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(Descriptor.toJvmName(c.getClassFile().getSuperclass())); + } + m_implements = HashMultiset.create(); + for (String interfaceName : c.getClassFile().getInterfaces()) { + m_implements.add(scrubClassName(Descriptor.toJvmName(interfaceName))); + } + + m_stringLiterals = Sets.newHashSet(); + ConstPool constants = c.getClassFile().getConstPool(); + for (int i=1; i 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 = EntryFactory.getFieldEntry(field); + for (EntryReference reference : index.getFieldReferences(fieldEntry)) { + addReference(reference); + } + } + for (CtBehavior behavior : c.getDeclaredBehaviors()) { + BehaviorEntry behaviorEntry = EntryFactory.getBehaviorEntry(behavior); + for (EntryReference reference : index.getBehaviorReferences(behaviorEntry)) { + addReference(reference); + } + } + } + + m_outer = EntryFactory.getClassEntry(c).getOuterClassName(); + } + + 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"); + } + buf.append("\touter "); + buf.append(m_outer); + buf.append("\n"); + return buf.toString(); + } + + private String scrubClassName(String className) { + return m_classNameReplacer.replace(className); + } + + private String scrubType(String typeName) { + return scrubType(new Type(typeName)).toString(); + } + + private Type scrubType(Type type) { + if (type.hasClass()) { + return new Type(type, m_classNameReplacer); + } else { + return type; + } + } + + private String scrubSignature(String signature) { + return scrubSignature(new Signature(signature)).toString(); + } + + private Signature scrubSignature(Signature signature) { + return new Signature(signature, m_classNameReplacer); + } + + 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(Descriptor.toJvmName(call.getClassName()))); + updateHashWithString(digest, scrubSignature(call.getSignature())); + if (isClassMatchedUniquely(call.getClassName())) { + updateHashWithString(digest, call.getMethodName()); + } + } + + @Override + public void edit(FieldAccess access) { + updateHashWithString(digest, scrubClassName(Descriptor.toJvmName(access.getClassName()))); + updateHashWithString(digest, scrubType(access.getSignature())); + if (isClassMatchedUniquely(access.getClassName())) { + updateHashWithString(digest, access.getFieldName()); + } + } + + @Override + public void edit(ConstructorCall call) { + updateHashWithString(digest, scrubClassName(Descriptor.toJvmName(call.getClassName()))); + updateHashWithString(digest, scrubSignature(call.getSignature())); + } + + @Override + public void edit(NewExpr expr) { + updateHashWithString(digest, scrubClassName(Descriptor.toJvmName(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 2*getNumMatches(m_extends, other.m_extends) + + 2*getNumMatches(m_outer, other.m_outer) + + 2*getNumMatches(m_implements, other.m_implements) + + getNumMatches(m_stringLiterals, other.m_stringLiterals) + + getNumMatches(m_fields, other.m_fields) + + getNumMatches(m_methods, other.m_methods) + + getNumMatches(m_constructors, other.m_constructors); + } + + public int getMaxMatchScore() { + return 2 + 2 + 2*m_implements.size() + m_stringLiterals.size() + 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(Set a, Set b) { + int numMatches = 0; + for (String val : a) { + if (b.contains(val)) { + numMatches++; + } + } + return numMatches; + } + + private int getNumMatches(Multiset a, Multiset b) { + int numMatches = 0; + for (String val : a) { + if (b.contains(val)) { + numMatches++; + } + } + return numMatches; + } + + private int getNumMatches(String a, String b) { + if (a.equals(b)) { + return 1; + } + return 0; + } +} diff --git a/src/cuchaz/enigma/convert/ClassMatch.java b/src/cuchaz/enigma/convert/ClassMatch.java new file mode 100644 index 0000000..8c50a62 --- /dev/null +++ b/src/cuchaz/enigma/convert/ClassMatch.java @@ -0,0 +1,88 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ +package cuchaz.enigma.convert; + +import java.util.Collection; +import java.util.Set; + +import com.google.common.collect.Sets; + +import cuchaz.enigma.Util; +import cuchaz.enigma.mapping.ClassEntry; + + +public class ClassMatch { + + public Set sourceClasses; + public Set destClasses; + + public ClassMatch(Collection sourceClasses, Collection destClasses) { + this.sourceClasses = Sets.newHashSet(sourceClasses); + this.destClasses = Sets.newHashSet(destClasses); + } + + public ClassMatch(ClassEntry sourceClass, ClassEntry destClass) { + sourceClasses = Sets.newHashSet(); + if (sourceClass != null) { + sourceClasses.add(sourceClass); + } + destClasses = Sets.newHashSet(); + if (destClass != null) { + destClasses.add(destClass); + } + } + + public boolean isMatched() { + return sourceClasses.size() > 0 && destClasses.size() > 0; + } + + public boolean isAmbiguous() { + return sourceClasses.size() > 1 || destClasses.size() > 1; + } + + public ClassEntry getUniqueSource() { + if (sourceClasses.size() != 1) { + throw new IllegalStateException("Match has ambiguous source!"); + } + return sourceClasses.iterator().next(); + } + + public ClassEntry getUniqueDest() { + if (destClasses.size() != 1) { + throw new IllegalStateException("Match has ambiguous source!"); + } + return destClasses.iterator().next(); + } + + public Set intersectSourceClasses(Set classes) { + Set intersection = Sets.newHashSet(sourceClasses); + intersection.retainAll(classes); + return intersection; + } + + @Override + public int hashCode() { + return Util.combineHashesOrdered(sourceClasses, destClasses); + } + + @Override + public boolean equals(Object other) { + if (other instanceof ClassMatch) { + return equals((ClassMatch)other); + } + return false; + } + + public boolean equals(ClassMatch other) { + return this.sourceClasses.equals(other.sourceClasses) + && this.destClasses.equals(other.destClasses); + } +} diff --git a/src/cuchaz/enigma/convert/ClassMatches.java b/src/cuchaz/enigma/convert/ClassMatches.java new file mode 100644 index 0000000..f70c180 --- /dev/null +++ b/src/cuchaz/enigma/convert/ClassMatches.java @@ -0,0 +1,163 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ +package cuchaz.enigma.convert; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; + +import cuchaz.enigma.mapping.ClassEntry; + + +public class ClassMatches implements Iterable { + + Collection m_matches; + Map m_matchesBySource; + Map m_matchesByDest; + BiMap m_uniqueMatches; + Map m_ambiguousMatchesBySource; + Map m_ambiguousMatchesByDest; + Set m_unmatchedSourceClasses; + Set m_unmatchedDestClasses; + + public ClassMatches() { + this(new ArrayList()); + } + + public ClassMatches(Collection matches) { + m_matches = matches; + m_matchesBySource = Maps.newHashMap(); + m_matchesByDest = Maps.newHashMap(); + m_uniqueMatches = HashBiMap.create(); + m_ambiguousMatchesBySource = Maps.newHashMap(); + m_ambiguousMatchesByDest = Maps.newHashMap(); + m_unmatchedSourceClasses = Sets.newHashSet(); + m_unmatchedDestClasses = Sets.newHashSet(); + + for (ClassMatch match : matches) { + indexMatch(match); + } + } + + public void add(ClassMatch match) { + m_matches.add(match); + indexMatch(match); + } + + public void remove(ClassMatch match) { + for (ClassEntry sourceClass : match.sourceClasses) { + m_matchesBySource.remove(sourceClass); + m_uniqueMatches.remove(sourceClass); + m_ambiguousMatchesBySource.remove(sourceClass); + m_unmatchedSourceClasses.remove(sourceClass); + } + for (ClassEntry destClass : match.destClasses) { + m_matchesByDest.remove(destClass); + m_uniqueMatches.inverse().remove(destClass); + m_ambiguousMatchesByDest.remove(destClass); + m_unmatchedDestClasses.remove(destClass); + } + m_matches.remove(match); + } + + public int size() { + return m_matches.size(); + } + + @Override + public Iterator iterator() { + return m_matches.iterator(); + } + + private void indexMatch(ClassMatch match) { + if (!match.isMatched()) { + // unmatched + m_unmatchedSourceClasses.addAll(match.sourceClasses); + m_unmatchedDestClasses.addAll(match.destClasses); + } else { + if (match.isAmbiguous()) { + // ambiguously matched + for (ClassEntry entry : match.sourceClasses) { + m_ambiguousMatchesBySource.put(entry, match); + } + for (ClassEntry entry : match.destClasses) { + m_ambiguousMatchesByDest.put(entry, match); + } + } else { + // uniquely matched + m_uniqueMatches.put(match.getUniqueSource(), match.getUniqueDest()); + } + } + for (ClassEntry entry : match.sourceClasses) { + m_matchesBySource.put(entry, match); + } + for (ClassEntry entry : match.destClasses) { + m_matchesByDest.put(entry, match); + } + } + + public BiMap getUniqueMatches() { + return m_uniqueMatches; + } + + public Set getUnmatchedSourceClasses() { + return m_unmatchedSourceClasses; + } + + public Set getUnmatchedDestClasses() { + return m_unmatchedDestClasses; + } + + public Set getAmbiguouslyMatchedSourceClasses() { + return m_ambiguousMatchesBySource.keySet(); + } + + public ClassMatch getAmbiguousMatchBySource(ClassEntry sourceClass) { + return m_ambiguousMatchesBySource.get(sourceClass); + } + + public ClassMatch getMatchBySource(ClassEntry sourceClass) { + return m_matchesBySource.get(sourceClass); + } + + public ClassMatch getMatchByDest(ClassEntry destClass) { + return m_matchesByDest.get(destClass); + } + + public void removeSource(ClassEntry sourceClass) { + ClassMatch match = m_matchesBySource.get(sourceClass); + if (match != null) { + remove(match); + match.sourceClasses.remove(sourceClass); + if (!match.sourceClasses.isEmpty() || !match.destClasses.isEmpty()) { + add(match); + } + } + } + + public void removeDest(ClassEntry destClass) { + ClassMatch match = m_matchesByDest.get(destClass); + if (match != null) { + remove(match); + match.destClasses.remove(destClass); + if (!match.sourceClasses.isEmpty() || !match.destClasses.isEmpty()) { + add(match); + } + } + } +} diff --git a/src/cuchaz/enigma/convert/ClassMatching.java b/src/cuchaz/enigma/convert/ClassMatching.java new file mode 100644 index 0000000..633d1ac --- /dev/null +++ b/src/cuchaz/enigma/convert/ClassMatching.java @@ -0,0 +1,155 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ +package cuchaz.enigma.convert; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map.Entry; +import java.util.Set; + +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; + +import cuchaz.enigma.mapping.ClassEntry; + +public class ClassMatching { + + private ClassForest m_sourceClasses; + private ClassForest m_destClasses; + private BiMap m_knownMatches; + + public ClassMatching(ClassIdentifier sourceIdentifier, ClassIdentifier destIdentifier) { + m_sourceClasses = new ClassForest(sourceIdentifier); + m_destClasses = new ClassForest(destIdentifier); + m_knownMatches = HashBiMap.create(); + } + + public void addKnownMatches(BiMap knownMatches) { + m_knownMatches.putAll(knownMatches); + } + + public void match(Iterable sourceClasses, Iterable destClasses) { + for (ClassEntry sourceClass : sourceClasses) { + if (!m_knownMatches.containsKey(sourceClass)) { + m_sourceClasses.add(sourceClass); + } + } + for (ClassEntry destClass : destClasses) { + if (!m_knownMatches.containsValue(destClass)) { + m_destClasses.add(destClass); + } + } + } + + public Collection matches() { + List matches = Lists.newArrayList(); + for (Entry entry : m_knownMatches.entrySet()) { + matches.add(new ClassMatch( + entry.getKey(), + entry.getValue() + )); + } + for (ClassIdentity identity : m_sourceClasses.identities()) { + matches.add(new ClassMatch( + m_sourceClasses.getClasses(identity), + m_destClasses.getClasses(identity) + )); + } + for (ClassIdentity identity : m_destClasses.identities()) { + if (!m_sourceClasses.containsIdentity(identity)) { + matches.add(new ClassMatch( + new ArrayList(), + m_destClasses.getClasses(identity) + )); + } + } + return matches; + } + + public Collection sourceClasses() { + Set classes = Sets.newHashSet(); + for (ClassMatch match : matches()) { + classes.addAll(match.sourceClasses); + } + return classes; + } + + public Collection destClasses() { + Set classes = Sets.newHashSet(); + for (ClassMatch match : matches()) { + classes.addAll(match.destClasses); + } + return classes; + } + + public BiMap uniqueMatches() { + BiMap uniqueMatches = HashBiMap.create(); + for (ClassMatch match : matches()) { + if (match.isMatched() && !match.isAmbiguous()) { + uniqueMatches.put(match.getUniqueSource(), match.getUniqueDest()); + } + } + return uniqueMatches; + } + + public Collection ambiguousMatches() { + List ambiguousMatches = Lists.newArrayList(); + for (ClassMatch match : matches()) { + if (match.isMatched() && match.isAmbiguous()) { + ambiguousMatches.add(match); + } + } + return ambiguousMatches; + } + + public Collection unmatchedSourceClasses() { + List classes = Lists.newArrayList(); + for (ClassMatch match : matches()) { + if (!match.isMatched() && !match.sourceClasses.isEmpty()) { + classes.addAll(match.sourceClasses); + } + } + return classes; + } + + public Collection unmatchedDestClasses() { + List classes = Lists.newArrayList(); + for (ClassMatch match : matches()) { + if (!match.isMatched() && !match.destClasses.isEmpty()) { + classes.addAll(match.destClasses); + } + } + return classes; + } + + @Override + public String toString() { + + // count the ambiguous classes + int numAmbiguousSource = 0; + int numAmbiguousDest = 0; + for (ClassMatch match : ambiguousMatches()) { + numAmbiguousSource += match.sourceClasses.size(); + numAmbiguousDest += match.destClasses.size(); + } + + StringBuilder buf = new StringBuilder(); + buf.append(String.format("%20s%8s%8s\n", "", "Source", "Dest")); + buf.append(String.format("%20s%8d%8d\n", "Classes", sourceClasses().size(), destClasses().size())); + buf.append(String.format("%20s%8d%8d\n", "Uniquely matched", uniqueMatches().size(), uniqueMatches().size())); + buf.append(String.format("%20s%8d%8d\n", "Ambiguously matched", numAmbiguousSource, numAmbiguousDest)); + buf.append(String.format("%20s%8d%8d\n", "Unmatched", unmatchedSourceClasses().size(), unmatchedDestClasses().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..e8fa730 --- /dev/null +++ b/src/cuchaz/enigma/convert/ClassNamer.java @@ -0,0 +1,66 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.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; + +import cuchaz.enigma.mapping.ClassEntry; + +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().getName(), name); + m_destNames.put(entry.getValue().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); + } + }; + } +} diff --git a/src/cuchaz/enigma/convert/FieldMatches.java b/src/cuchaz/enigma/convert/FieldMatches.java new file mode 100644 index 0000000..8439a84 --- /dev/null +++ b/src/cuchaz/enigma/convert/FieldMatches.java @@ -0,0 +1,155 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ +package cuchaz.enigma.convert; + +import java.util.Collection; +import java.util.Set; + +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import com.google.common.collect.Sets; + +import cuchaz.enigma.mapping.ClassEntry; +import cuchaz.enigma.mapping.FieldEntry; + + +public class FieldMatches { + + private BiMap m_matches; + private Multimap m_matchedSourceFields; + private Multimap m_unmatchedSourceFields; + private Multimap m_unmatchedDestFields; + private Multimap m_unmatchableSourceFields; + + public FieldMatches() { + m_matches = HashBiMap.create(); + m_matchedSourceFields = HashMultimap.create(); + m_unmatchedSourceFields = HashMultimap.create(); + m_unmatchedDestFields = HashMultimap.create(); + m_unmatchableSourceFields = HashMultimap.create(); + } + + public void addMatch(FieldEntry srcField, FieldEntry destField) { + boolean wasAdded = m_matches.put(srcField, destField) == null; + assert (wasAdded); + wasAdded = m_matchedSourceFields.put(srcField.getClassEntry(), srcField); + assert (wasAdded); + } + + public void addUnmatchedSourceField(FieldEntry fieldEntry) { + boolean wasAdded = m_unmatchedSourceFields.put(fieldEntry.getClassEntry(), fieldEntry); + assert (wasAdded); + } + + public void addUnmatchedSourceFields(Iterable fieldEntries) { + for (FieldEntry fieldEntry : fieldEntries) { + addUnmatchedSourceField(fieldEntry); + } + } + + public void addUnmatchedDestField(FieldEntry fieldEntry) { + boolean wasAdded = m_unmatchedDestFields.put(fieldEntry.getClassEntry(), fieldEntry); + assert (wasAdded); + } + + public void addUnmatchedDestFields(Iterable fieldEntries) { + for (FieldEntry fieldEntry : fieldEntries) { + addUnmatchedDestField(fieldEntry); + } + } + + public void addUnmatchableSourceField(FieldEntry sourceField) { + boolean wasAdded = m_unmatchableSourceFields.put(sourceField.getClassEntry(), sourceField); + assert (wasAdded); + } + + public Set getSourceClassesWithUnmatchedFields() { + return m_unmatchedSourceFields.keySet(); + } + + public Collection getSourceClassesWithoutUnmatchedFields() { + Set out = Sets.newHashSet(); + out.addAll(m_matchedSourceFields.keySet()); + out.removeAll(m_unmatchedSourceFields.keySet()); + return out; + } + + public Collection getUnmatchedSourceFields() { + return m_unmatchedSourceFields.values(); + } + + public Collection getUnmatchedSourceFields(ClassEntry sourceClass) { + return m_unmatchedSourceFields.get(sourceClass); + } + + public Collection getUnmatchedDestFields() { + return m_unmatchedDestFields.values(); + } + + public Collection getUnmatchedDestFields(ClassEntry destClass) { + return m_unmatchedDestFields.get(destClass); + } + + public Collection getUnmatchableSourceFields() { + return m_unmatchableSourceFields.values(); + } + + public boolean hasSource(FieldEntry fieldEntry) { + return m_matches.containsKey(fieldEntry) || m_unmatchedSourceFields.containsValue(fieldEntry); + } + + public boolean hasDest(FieldEntry fieldEntry) { + return m_matches.containsValue(fieldEntry) || m_unmatchedDestFields.containsValue(fieldEntry); + } + + public BiMap matches() { + return m_matches; + } + + public boolean isMatchedSourceField(FieldEntry sourceField) { + return m_matches.containsKey(sourceField); + } + + public boolean isMatchedDestField(FieldEntry destField) { + return m_matches.containsValue(destField); + } + + public void makeMatch(FieldEntry sourceField, FieldEntry destField) { + boolean wasRemoved = m_unmatchedSourceFields.remove(sourceField.getClassEntry(), sourceField); + assert (wasRemoved); + wasRemoved = m_unmatchedDestFields.remove(destField.getClassEntry(), destField); + assert (wasRemoved); + addMatch(sourceField, destField); + } + + public boolean isMatched(FieldEntry sourceField, FieldEntry destField) { + FieldEntry match = m_matches.get(sourceField); + return match != null && match.equals(destField); + } + + public void unmakeMatch(FieldEntry sourceField, FieldEntry destField) { + boolean wasRemoved = m_matches.remove(sourceField) != null; + assert (wasRemoved); + wasRemoved = m_matchedSourceFields.remove(sourceField.getClassEntry(), sourceField); + assert (wasRemoved); + addUnmatchedSourceField(sourceField); + addUnmatchedDestField(destField); + } + + public void makeSourceUnmatchable(FieldEntry sourceField) { + assert(!isMatchedSourceField(sourceField)); + boolean wasRemoved = m_unmatchedSourceFields.remove(sourceField.getClassEntry(), sourceField); + assert (wasRemoved); + addUnmatchableSourceField(sourceField); + } +} diff --git a/src/cuchaz/enigma/convert/MappingsConverter.java b/src/cuchaz/enigma/convert/MappingsConverter.java new file mode 100644 index 0000000..b457d6c --- /dev/null +++ b/src/cuchaz/enigma/convert/MappingsConverter.java @@ -0,0 +1,559 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ +package cuchaz.enigma.convert; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +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 com.google.common.collect.BiMap; +import com.google.common.collect.HashMultimap; +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.Deobfuscator; +import cuchaz.enigma.analysis.JarIndex; +import cuchaz.enigma.convert.ClassNamer.SidedClassNamer; +import cuchaz.enigma.mapping.BehaviorEntry; +import cuchaz.enigma.mapping.ClassEntry; +import cuchaz.enigma.mapping.ClassMapping; +import cuchaz.enigma.mapping.ClassNameReplacer; +import cuchaz.enigma.mapping.ConstructorEntry; +import cuchaz.enigma.mapping.Entry; +import cuchaz.enigma.mapping.FieldEntry; +import cuchaz.enigma.mapping.FieldMapping; +import cuchaz.enigma.mapping.Mappings; +import cuchaz.enigma.mapping.MappingsChecker; +import cuchaz.enigma.mapping.MemberMapping; +import cuchaz.enigma.mapping.MethodEntry; +import cuchaz.enigma.mapping.MethodMapping; +import cuchaz.enigma.mapping.Signature; +import cuchaz.enigma.mapping.Type; + +public class MappingsConverter { + + public static ClassMatches computeClassMatches(JarFile sourceJar, JarFile destJar, Mappings mappings) { + + // 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); + + // compute the matching + ClassMatching matching = computeMatching(sourceJar, sourceIndex, destJar, destIndex, null); + return new ClassMatches(matching.matches()); + } + + public static ClassMatching computeMatching(JarFile sourceJar, JarIndex sourceIndex, JarFile destJar, JarIndex destIndex, BiMap knownMatches) { + + System.out.println("Iteratively matching classes"); + + ClassMatching lastMatching = null; + int round = 0; + SidedClassNamer sourceNamer = null; + SidedClassNamer destNamer = null; + for (boolean useReferences : Arrays.asList(false, true)) { + + int numUniqueMatchesLastTime = 0; + if (lastMatching != null) { + numUniqueMatchesLastTime = lastMatching.uniqueMatches().size(); + } + + while (true) { + + System.out.println("Round " + (++round) + "..."); + + // init the matching with identity settings + ClassMatching matching = new ClassMatching( + new ClassIdentifier(sourceJar, sourceIndex, sourceNamer, useReferences), + new ClassIdentifier(destJar, destIndex, destNamer, useReferences) + ); + + if (knownMatches != null) { + matching.addKnownMatches(knownMatches); + } + + if (lastMatching == null) { + // search all classes + matching.match(sourceIndex.getObfClassEntries(), destIndex.getObfClassEntries()); + } else { + // we already know about these matches from last time + matching.addKnownMatches(lastMatching.uniqueMatches()); + + // search unmatched and ambiguously-matched classes + matching.match(lastMatching.unmatchedSourceClasses(), lastMatching.unmatchedDestClasses()); + for (ClassMatch match : lastMatching.ambiguousMatches()) { + matching.match(match.sourceClasses, match.destClasses); + } + } + System.out.println(matching); + BiMap uniqueMatches = matching.uniqueMatches(); + + // did we match anything new this time? + if (uniqueMatches.size() > numUniqueMatchesLastTime) { + numUniqueMatchesLastTime = uniqueMatches.size(); + lastMatching = matching; + } else { + break; + } + + // update the namers + ClassNamer namer = new ClassNamer(uniqueMatches); + sourceNamer = namer.getSourceNamer(); + destNamer = namer.getDestNamer(); + } + } + + return lastMatching; + } + + public static Mappings newMappings(ClassMatches matches, Mappings oldMappings, Deobfuscator sourceDeobfuscator, Deobfuscator destDeobfuscator) { + + // sort the unique matches by size of inner class chain + Multimap> matchesByDestChainSize = HashMultimap.create(); + for (java.util.Map.Entry match : matches.getUniqueMatches().entrySet()) { + int chainSize = destDeobfuscator.getJarIndex().getObfClassChain(match.getValue()).size(); + matchesByDestChainSize.put(chainSize, match); + } + + // build the mappings (in order of small-to-large inner chains) + Mappings newMappings = new Mappings(); + List chainSizes = Lists.newArrayList(matchesByDestChainSize.keySet()); + Collections.sort(chainSizes); + for (int chainSize : chainSizes) { + for (java.util.Map.Entry match : matchesByDestChainSize.get(chainSize)) { + + // get class info + ClassEntry obfSourceClassEntry = match.getKey(); + ClassEntry obfDestClassEntry = match.getValue(); + List destClassChain = destDeobfuscator.getJarIndex().getObfClassChain(obfDestClassEntry); + + ClassMapping sourceMapping = sourceDeobfuscator.getMappings().getClassByObf(obfSourceClassEntry); + if (sourceMapping == null) { + // if this class was never deobfuscated, don't try to match it + continue; + } + + // find out where to make the dest class mapping + if (destClassChain.size() == 1) { + // not an inner class, add directly to mappings + newMappings.addClassMapping(migrateClassMapping(obfDestClassEntry, sourceMapping, matches, false)); + } else { + // inner class, find the outer class mapping + ClassMapping destMapping = null; + for (int i=0; i changes) { + + // 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 sortedChanges = Maps.newLinkedHashMap(); + int numChangesLeft = changes.size(); + while (!changes.isEmpty()) { + Iterator> iter = changes.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry change = iter.next(); + if (changes.containsKey(change.getValue())) { + sortedChanges.put(change.getKey(), change.getValue()); + iter.remove(); + } + } + + // did we remove any changes? + if (numChangesLeft - changes.size() > 0) { + // keep going + numChangesLeft = changes.size(); + } else { + // can't sort anymore. There must be a loop + break; + } + } + if (!changes.isEmpty()) { + throw new Error("Unable to sort class changes! There must be a cycle."); + } + + // convert the mappings in the correct class order + for (Map.Entry entry : sortedChanges.entrySet()) { + mappings.renameObfClass(entry.getKey().getName(), entry.getValue().getName()); + } + } + + public static interface Doer { + Collection getDroppedEntries(MappingsChecker checker); + Collection getObfEntries(JarIndex jarIndex); + Collection> getMappings(ClassMapping destClassMapping); + Set filterEntries(Collection obfEntries, T obfSourceEntry, ClassMatches classMatches); + void setUpdateObfMember(ClassMapping classMapping, MemberMapping memberMapping, T newEntry); + boolean hasObfMember(ClassMapping classMapping, T obfEntry); + void removeMemberByObf(ClassMapping classMapping, T obfEntry); + } + + public static Doer getFieldDoer() { + return new Doer() { + + @Override + public Collection getDroppedEntries(MappingsChecker checker) { + return checker.getDroppedFieldMappings().keySet(); + } + + @Override + public Collection getObfEntries(JarIndex jarIndex) { + return jarIndex.getObfFieldEntries(); + } + + @Override + public Collection> getMappings(ClassMapping destClassMapping) { + return (Collection>)destClassMapping.fields(); + } + + @Override + public Set filterEntries(Collection obfDestFields, FieldEntry obfSourceField, ClassMatches classMatches) { + Set out = Sets.newHashSet(); + for (FieldEntry obfDestField : obfDestFields) { + Type translatedDestType = translate(obfDestField.getType(), classMatches.getUniqueMatches().inverse()); + if (translatedDestType.equals(obfSourceField.getType())) { + out.add(obfDestField); + } + } + return out; + } + + @Override + public void setUpdateObfMember(ClassMapping classMapping, MemberMapping memberMapping, FieldEntry newField) { + FieldMapping fieldMapping = (FieldMapping)memberMapping; + classMapping.setFieldObfNameAndType(fieldMapping.getObfName(), fieldMapping.getObfType(), newField.getName(), newField.getType()); + } + + @Override + public boolean hasObfMember(ClassMapping classMapping, FieldEntry obfField) { + return classMapping.getFieldByObf(obfField.getName(), obfField.getType()) != null; + } + + @Override + public void removeMemberByObf(ClassMapping classMapping, FieldEntry obfField) { + classMapping.removeFieldMapping(classMapping.getFieldByObf(obfField.getName(), obfField.getType())); + } + }; + } + + public static Doer getMethodDoer() { + return new Doer() { + + @Override + public Collection getDroppedEntries(MappingsChecker checker) { + return checker.getDroppedMethodMappings().keySet(); + } + + @Override + public Collection getObfEntries(JarIndex jarIndex) { + return jarIndex.getObfBehaviorEntries(); + } + + @Override + public Collection> getMappings(ClassMapping destClassMapping) { + return (Collection>)destClassMapping.methods(); + } + + @Override + public Set filterEntries(Collection obfDestFields, BehaviorEntry obfSourceField, ClassMatches classMatches) { + Set out = Sets.newHashSet(); + for (BehaviorEntry obfDestField : obfDestFields) { + Signature translatedDestSignature = translate(obfDestField.getSignature(), classMatches.getUniqueMatches().inverse()); + if (translatedDestSignature == null && obfSourceField.getSignature() == null) { + out.add(obfDestField); + } else if (translatedDestSignature == null || obfSourceField.getSignature() == null) { + // skip it + } else if (translatedDestSignature.equals(obfSourceField.getSignature())) { + out.add(obfDestField); + } + } + return out; + } + + @Override + public void setUpdateObfMember(ClassMapping classMapping, MemberMapping memberMapping, BehaviorEntry newBehavior) { + MethodMapping methodMapping = (MethodMapping)memberMapping; + classMapping.setMethodObfNameAndSignature(methodMapping.getObfName(), methodMapping.getObfSignature(), newBehavior.getName(), newBehavior.getSignature()); + } + + @Override + public boolean hasObfMember(ClassMapping classMapping, BehaviorEntry obfBehavior) { + return classMapping.getMethodByObf(obfBehavior.getName(), obfBehavior.getSignature()) != null; + } + + @Override + public void removeMemberByObf(ClassMapping classMapping, BehaviorEntry obfBehavior) { + classMapping.removeMethodMapping(classMapping.getMethodByObf(obfBehavior.getName(), obfBehavior.getSignature())); + } + }; + } + + public static MemberMatches computeMemberMatches(Deobfuscator destDeobfuscator, Mappings destMappings, ClassMatches classMatches, Doer doer) { + + MemberMatches memberMatches = new MemberMatches(); + + // unmatched source fields are easy + MappingsChecker checker = new MappingsChecker(destDeobfuscator.getJarIndex()); + checker.dropBrokenMappings(destMappings); + for (T destObfEntry : doer.getDroppedEntries(checker)) { + T srcObfEntry = translate(destObfEntry, classMatches.getUniqueMatches().inverse()); + memberMatches.addUnmatchedSourceEntry(srcObfEntry); + } + + // get matched fields (anything that's left after the checks/drops is matched( + for (ClassMapping classMapping : destMappings.classes()) { + collectMatchedFields(memberMatches, classMapping, classMatches, doer); + } + + // get unmatched dest fields + for (T destEntry : doer.getObfEntries(destDeobfuscator.getJarIndex())) { + if (!memberMatches.isMatchedDestEntry(destEntry)) { + memberMatches.addUnmatchedDestEntry(destEntry); + } + } + + System.out.println("Automatching " + memberMatches.getUnmatchedSourceEntries().size() + " unmatched source entries..."); + + // go through the unmatched source fields and try to pick out the easy matches + for (ClassEntry obfSourceClass : Lists.newArrayList(memberMatches.getSourceClassesWithUnmatchedEntries())) { + for (T obfSourceEntry : Lists.newArrayList(memberMatches.getUnmatchedSourceEntries(obfSourceClass))) { + + // get the possible dest matches + ClassEntry obfDestClass = classMatches.getUniqueMatches().get(obfSourceClass); + + // filter by type/signature + Set obfDestEntries = doer.filterEntries(memberMatches.getUnmatchedDestEntries(obfDestClass), obfSourceEntry, classMatches); + + if (obfDestEntries.size() == 1) { + // make the easy match + memberMatches.makeMatch(obfSourceEntry, obfDestEntries.iterator().next()); + } else if (obfDestEntries.isEmpty()) { + // no match is possible =( + memberMatches.makeSourceUnmatchable(obfSourceEntry); + } + } + } + + System.out.println(String.format("Ended up with %d ambiguous and %d unmatchable source entries", + memberMatches.getUnmatchedSourceEntries().size(), + memberMatches.getUnmatchableSourceEntries().size() + )); + + return memberMatches; + } + + private static void collectMatchedFields(MemberMatches memberMatches, ClassMapping destClassMapping, ClassMatches classMatches, Doer doer) { + + // get the fields for this class + for (MemberMapping destEntryMapping : doer.getMappings(destClassMapping)) { + T destObfField = destEntryMapping.getObfEntry(destClassMapping.getObfEntry()); + T srcObfField = translate(destObfField, classMatches.getUniqueMatches().inverse()); + memberMatches.addMatch(srcObfField, destObfField); + } + + // recurse + for (ClassMapping destInnerClassMapping : destClassMapping.innerClasses()) { + collectMatchedFields(memberMatches, destInnerClassMapping, classMatches, doer); + } + } + + @SuppressWarnings("unchecked") + private static T translate(T in, BiMap map) { + if (in instanceof FieldEntry) { + return (T)new FieldEntry( + map.get(in.getClassEntry()), + in.getName(), + translate(((FieldEntry)in).getType(), map) + ); + } else if (in instanceof MethodEntry) { + return (T)new MethodEntry( + map.get(in.getClassEntry()), + in.getName(), + translate(((MethodEntry)in).getSignature(), map) + ); + } else if (in instanceof ConstructorEntry) { + return (T)new ConstructorEntry( + map.get(in.getClassEntry()), + translate(((ConstructorEntry)in).getSignature(), map) + ); + } + throw new Error("Unhandled entry type: " + in.getClass()); + } + + private static Type translate(Type type, final BiMap map) { + return new Type(type, new ClassNameReplacer() { + @Override + public String replace(String inClassName) { + ClassEntry outClassEntry = map.get(new ClassEntry(inClassName)); + if (outClassEntry == null) { + return null; + } + return outClassEntry.getName(); + } + }); + } + + private static Signature translate(Signature signature, final BiMap map) { + if (signature == null) { + return null; + } + return new Signature(signature, new ClassNameReplacer() { + @Override + public String replace(String inClassName) { + ClassEntry outClassEntry = map.get(new ClassEntry(inClassName)); + if (outClassEntry == null) { + return null; + } + return outClassEntry.getName(); + } + }); + } + + public static void applyMemberMatches(Mappings mappings, ClassMatches classMatches, MemberMatches memberMatches, Doer doer) { + for (ClassMapping classMapping : mappings.classes()) { + applyMemberMatches(classMapping, classMatches, memberMatches, doer); + } + } + + private static void applyMemberMatches(ClassMapping classMapping, ClassMatches classMatches, MemberMatches memberMatches, Doer doer) { + + // get the classes + ClassEntry obfDestClass = classMapping.getObfEntry(); + + // make a map of all the renames we need to make + Map renames = Maps.newHashMap(); + for (MemberMapping memberMapping : Lists.newArrayList(doer.getMappings(classMapping))) { + T obfOldDestEntry = memberMapping.getObfEntry(obfDestClass); + T obfSourceEntry = getSourceEntryFromDestMapping(memberMapping, obfDestClass, classMatches); + + // but drop the unmatchable things + if (memberMatches.isUnmatchableSourceEntry(obfSourceEntry)) { + doer.removeMemberByObf(classMapping, obfOldDestEntry); + continue; + } + + T obfNewDestEntry = memberMatches.matches().get(obfSourceEntry); + if (obfNewDestEntry != null && !obfOldDestEntry.getName().equals(obfNewDestEntry.getName())) { + renames.put(obfOldDestEntry, obfNewDestEntry); + } + } + + if (!renames.isEmpty()) { + + // apply to this class (should never need more than n passes) + int numRenamesAppliedThisRound; + do { + numRenamesAppliedThisRound = 0; + + for (MemberMapping memberMapping : Lists.newArrayList(doer.getMappings(classMapping))) { + T obfOldDestEntry = memberMapping.getObfEntry(obfDestClass); + T obfNewDestEntry = renames.get(obfOldDestEntry); + if (obfNewDestEntry != null) { + // make sure this rename won't cause a collision + // otherwise, save it for the next round and try again next time + if (!doer.hasObfMember(classMapping, obfNewDestEntry)) { + doer.setUpdateObfMember(classMapping, memberMapping, obfNewDestEntry); + renames.remove(obfOldDestEntry); + numRenamesAppliedThisRound++; + } + } + } + } while(numRenamesAppliedThisRound > 0); + + if (!renames.isEmpty()) { + System.err.println(String.format("WARNING: Couldn't apply all the renames for class %s. %d renames left.", + classMapping.getObfFullName(), renames.size() + )); + for (Map.Entry entry : renames.entrySet()) { + System.err.println(String.format("\t%s -> %s", entry.getKey().getName(), entry.getValue().getName())); + } + } + } + + // recurse + for (ClassMapping innerClassMapping : classMapping.innerClasses()) { + applyMemberMatches(innerClassMapping, classMatches, memberMatches, doer); + } + } + + private static T getSourceEntryFromDestMapping(MemberMapping destMemberMapping, ClassEntry obfDestClass, ClassMatches classMatches) { + return translate(destMemberMapping.getObfEntry(obfDestClass), classMatches.getUniqueMatches().inverse()); + } +} diff --git a/src/cuchaz/enigma/convert/MatchesReader.java b/src/cuchaz/enigma/convert/MatchesReader.java new file mode 100644 index 0000000..7514e2a --- /dev/null +++ b/src/cuchaz/enigma/convert/MatchesReader.java @@ -0,0 +1,113 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ +package cuchaz.enigma.convert; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.Collection; +import java.util.List; + +import com.google.common.collect.Lists; + +import cuchaz.enigma.mapping.ClassEntry; +import cuchaz.enigma.mapping.Entry; +import cuchaz.enigma.mapping.EntryFactory; +import cuchaz.enigma.mapping.FieldEntry; +import cuchaz.enigma.mapping.Type; + + +public class MatchesReader { + + public static ClassMatches readClasses(File file) + throws IOException { + try (BufferedReader in = new BufferedReader(new FileReader(file))) { + ClassMatches matches = new ClassMatches(); + String line = null; + while ((line = in.readLine()) != null) { + matches.add(readClassMatch(line)); + } + return matches; + } + } + + private static ClassMatch readClassMatch(String line) + throws IOException { + String[] sides = line.split(":", 2); + return new ClassMatch(readClasses(sides[0]), readClasses(sides[1])); + } + + private static Collection readClasses(String in) { + List entries = Lists.newArrayList(); + for (String className : in.split(",")) { + className = className.trim(); + if (className.length() > 0) { + entries.add(new ClassEntry(className)); + } + } + return entries; + } + + public static MemberMatches readMembers(File file) + throws IOException { + try (BufferedReader in = new BufferedReader(new FileReader(file))) { + MemberMatches matches = new MemberMatches(); + String line = null; + while ((line = in.readLine()) != null) { + readMemberMatch(matches, line); + } + return matches; + } + } + + private static void readMemberMatch(MemberMatches matches, String line) { + if (line.startsWith("!")) { + T source = readEntry(line.substring(1)); + matches.addUnmatchableSourceEntry(source); + } else { + String[] parts = line.split(":", 2); + T source = readEntry(parts[0]); + T dest = readEntry(parts[1]); + if (source != null && dest != null) { + matches.addMatch(source, dest); + } else if (source != null) { + matches.addUnmatchedSourceEntry(source); + } else if (dest != null) { + matches.addUnmatchedDestEntry(dest); + } + } + } + + @SuppressWarnings("unchecked") + private static T readEntry(String in) { + if (in.length() <= 0) { + return null; + } + String[] parts = in.split(" "); + if (parts.length == 3 && parts[2].indexOf('(') < 0) { + return (T)new FieldEntry( + new ClassEntry(parts[0]), + parts[1], + new Type(parts[2]) + ); + } else { + assert(parts.length == 2 || parts.length == 3); + if (parts.length == 2) { + return (T)EntryFactory.getBehaviorEntry(parts[0], parts[1]); + } else if (parts.length == 3) { + return (T)EntryFactory.getBehaviorEntry(parts[0], parts[1], parts[2]); + } else { + throw new Error("Malformed behavior entry: " + in); + } + } + } +} diff --git a/src/cuchaz/enigma/convert/MatchesWriter.java b/src/cuchaz/enigma/convert/MatchesWriter.java new file mode 100644 index 0000000..42c6b61 --- /dev/null +++ b/src/cuchaz/enigma/convert/MatchesWriter.java @@ -0,0 +1,121 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ +package cuchaz.enigma.convert; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Map; + +import cuchaz.enigma.mapping.BehaviorEntry; +import cuchaz.enigma.mapping.ClassEntry; +import cuchaz.enigma.mapping.Entry; +import cuchaz.enigma.mapping.FieldEntry; + + +public class MatchesWriter { + + public static void writeClasses(ClassMatches matches, File file) + throws IOException { + try (FileWriter out = new FileWriter(file)) { + for (ClassMatch match : matches) { + writeClassMatch(out, match); + } + } + } + + private static void writeClassMatch(FileWriter out, ClassMatch match) + throws IOException { + writeClasses(out, match.sourceClasses); + out.write(":"); + writeClasses(out, match.destClasses); + out.write("\n"); + } + + private static void writeClasses(FileWriter out, Iterable classes) + throws IOException { + boolean isFirst = true; + for (ClassEntry entry : classes) { + if (isFirst) { + isFirst = false; + } else { + out.write(","); + } + out.write(entry.toString()); + } + } + + public static void writeMembers(MemberMatches matches, File file) + throws IOException { + try (FileWriter out = new FileWriter(file)) { + for (Map.Entry match : matches.matches().entrySet()) { + writeMemberMatch(out, match.getKey(), match.getValue()); + } + for (T entry : matches.getUnmatchedSourceEntries()) { + writeMemberMatch(out, entry, null); + } + for (T entry : matches.getUnmatchedDestEntries()) { + writeMemberMatch(out, null, entry); + } + for (T entry : matches.getUnmatchableSourceEntries()) { + writeUnmatchableEntry(out, entry); + } + } + } + + private static void writeMemberMatch(FileWriter out, T source, T dest) + throws IOException { + if (source != null) { + writeEntry(out, source); + } + out.write(":"); + if (dest != null) { + writeEntry(out, dest); + } + out.write("\n"); + } + + private static void writeUnmatchableEntry(FileWriter out, T entry) + throws IOException { + out.write("!"); + writeEntry(out, entry); + out.write("\n"); + } + + private static void writeEntry(FileWriter out, T entry) + throws IOException { + if (entry instanceof FieldEntry) { + writeField(out, (FieldEntry)entry); + } else if (entry instanceof BehaviorEntry) { + writeBehavior(out, (BehaviorEntry)entry); + } + } + + private static void writeField(FileWriter out, FieldEntry fieldEntry) + throws IOException { + out.write(fieldEntry.getClassName()); + out.write(" "); + out.write(fieldEntry.getName()); + out.write(" "); + out.write(fieldEntry.getType().toString()); + } + + private static void writeBehavior(FileWriter out, BehaviorEntry behaviorEntry) + throws IOException { + out.write(behaviorEntry.getClassName()); + out.write(" "); + out.write(behaviorEntry.getName()); + out.write(" "); + if (behaviorEntry.getSignature() != null) { + out.write(behaviorEntry.getSignature().toString()); + } + } +} diff --git a/src/cuchaz/enigma/convert/MemberMatches.java b/src/cuchaz/enigma/convert/MemberMatches.java new file mode 100644 index 0000000..29def15 --- /dev/null +++ b/src/cuchaz/enigma/convert/MemberMatches.java @@ -0,0 +1,159 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ +package cuchaz.enigma.convert; + +import java.util.Collection; +import java.util.Set; + +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import com.google.common.collect.Sets; + +import cuchaz.enigma.mapping.ClassEntry; +import cuchaz.enigma.mapping.Entry; + + +public class MemberMatches { + + private BiMap m_matches; + private Multimap m_matchedSourceEntries; + private Multimap m_unmatchedSourceEntries; + private Multimap m_unmatchedDestEntries; + private Multimap m_unmatchableSourceEntries; + + public MemberMatches() { + m_matches = HashBiMap.create(); + m_matchedSourceEntries = HashMultimap.create(); + m_unmatchedSourceEntries = HashMultimap.create(); + m_unmatchedDestEntries = HashMultimap.create(); + m_unmatchableSourceEntries = HashMultimap.create(); + } + + public void addMatch(T srcEntry, T destEntry) { + boolean wasAdded = m_matches.put(srcEntry, destEntry) == null; + assert (wasAdded); + wasAdded = m_matchedSourceEntries.put(srcEntry.getClassEntry(), srcEntry); + assert (wasAdded); + } + + public void addUnmatchedSourceEntry(T sourceEntry) { + boolean wasAdded = m_unmatchedSourceEntries.put(sourceEntry.getClassEntry(), sourceEntry); + assert (wasAdded); + } + + public void addUnmatchedSourceEntries(Iterable sourceEntries) { + for (T sourceEntry : sourceEntries) { + addUnmatchedSourceEntry(sourceEntry); + } + } + + public void addUnmatchedDestEntry(T destEntry) { + boolean wasAdded = m_unmatchedDestEntries.put(destEntry.getClassEntry(), destEntry); + assert (wasAdded); + } + + public void addUnmatchedDestEntries(Iterable destEntriesntries) { + for (T entry : destEntriesntries) { + addUnmatchedDestEntry(entry); + } + } + + public void addUnmatchableSourceEntry(T sourceEntry) { + boolean wasAdded = m_unmatchableSourceEntries.put(sourceEntry.getClassEntry(), sourceEntry); + assert (wasAdded); + } + + public Set getSourceClassesWithUnmatchedEntries() { + return m_unmatchedSourceEntries.keySet(); + } + + public Collection getSourceClassesWithoutUnmatchedEntries() { + Set out = Sets.newHashSet(); + out.addAll(m_matchedSourceEntries.keySet()); + out.removeAll(m_unmatchedSourceEntries.keySet()); + return out; + } + + public Collection getUnmatchedSourceEntries() { + return m_unmatchedSourceEntries.values(); + } + + public Collection getUnmatchedSourceEntries(ClassEntry sourceClass) { + return m_unmatchedSourceEntries.get(sourceClass); + } + + public Collection getUnmatchedDestEntries() { + return m_unmatchedDestEntries.values(); + } + + public Collection getUnmatchedDestEntries(ClassEntry destClass) { + return m_unmatchedDestEntries.get(destClass); + } + + public Collection getUnmatchableSourceEntries() { + return m_unmatchableSourceEntries.values(); + } + + public boolean hasSource(T sourceEntry) { + return m_matches.containsKey(sourceEntry) || m_unmatchedSourceEntries.containsValue(sourceEntry); + } + + public boolean hasDest(T destEntry) { + return m_matches.containsValue(destEntry) || m_unmatchedDestEntries.containsValue(destEntry); + } + + public BiMap matches() { + return m_matches; + } + + public boolean isMatchedSourceEntry(T sourceEntry) { + return m_matches.containsKey(sourceEntry); + } + + public boolean isMatchedDestEntry(T destEntry) { + return m_matches.containsValue(destEntry); + } + + public boolean isUnmatchableSourceEntry(T sourceEntry) { + return m_unmatchableSourceEntries.containsEntry(sourceEntry.getClassEntry(), sourceEntry); + } + + public void makeMatch(T sourceEntry, T destEntry) { + boolean wasRemoved = m_unmatchedSourceEntries.remove(sourceEntry.getClassEntry(), sourceEntry); + assert (wasRemoved); + wasRemoved = m_unmatchedDestEntries.remove(destEntry.getClassEntry(), destEntry); + assert (wasRemoved); + addMatch(sourceEntry, destEntry); + } + + public boolean isMatched(T sourceEntry, T destEntry) { + T match = m_matches.get(sourceEntry); + return match != null && match.equals(destEntry); + } + + public void unmakeMatch(T sourceEntry, T destEntry) { + boolean wasRemoved = m_matches.remove(sourceEntry) != null; + assert (wasRemoved); + wasRemoved = m_matchedSourceEntries.remove(sourceEntry.getClassEntry(), sourceEntry); + assert (wasRemoved); + addUnmatchedSourceEntry(sourceEntry); + addUnmatchedDestEntry(destEntry); + } + + public void makeSourceUnmatchable(T sourceEntry) { + assert(!isMatchedSourceEntry(sourceEntry)); + boolean wasRemoved = m_unmatchedSourceEntries.remove(sourceEntry.getClassEntry(), sourceEntry); + assert (wasRemoved); + addUnmatchableSourceEntry(sourceEntry); + } +} -- cgit v1.2.3