From 4be005617b3b8c3578cca07c5d085d12916f0d1d Mon Sep 17 00:00:00 2001 From: lclc98 Date: Thu, 30 Jun 2016 00:49:21 +1000 Subject: Json format (#2) * Added new format * Fixed bug * Updated Version --- .../java/cuchaz/enigma/mapping/ArgumentEntry.java | 116 ++++++ .../cuchaz/enigma/mapping/ArgumentMapping.java | 49 +++ .../java/cuchaz/enigma/mapping/BehaviorEntry.java | 15 + .../java/cuchaz/enigma/mapping/ClassEntry.java | 172 ++++++++ .../java/cuchaz/enigma/mapping/ClassMapping.java | 460 +++++++++++++++++++++ .../cuchaz/enigma/mapping/ClassNameReplacer.java | 15 + .../cuchaz/enigma/mapping/ConstructorEntry.java | 116 ++++++ src/main/java/cuchaz/enigma/mapping/Entry.java | 21 + .../java/cuchaz/enigma/mapping/EntryFactory.java | 162 ++++++++ src/main/java/cuchaz/enigma/mapping/EntryPair.java | 22 + .../java/cuchaz/enigma/mapping/FieldEntry.java | 99 +++++ .../java/cuchaz/enigma/mapping/FieldMapping.java | 89 ++++ .../enigma/mapping/IllegalNameException.java | 44 ++ .../enigma/mapping/MappingParseException.java | 29 ++ src/main/java/cuchaz/enigma/mapping/Mappings.java | 201 +++++++++ .../cuchaz/enigma/mapping/MappingsChecker.java | 107 +++++ .../java/cuchaz/enigma/mapping/MappingsReader.java | 92 +++++ .../cuchaz/enigma/mapping/MappingsReaderOld.java | 122 ++++++ .../cuchaz/enigma/mapping/MappingsRenamer.java | 237 +++++++++++ .../java/cuchaz/enigma/mapping/MappingsWriter.java | 82 ++++ .../java/cuchaz/enigma/mapping/MemberMapping.java | 18 + .../java/cuchaz/enigma/mapping/MethodEntry.java | 104 +++++ .../java/cuchaz/enigma/mapping/MethodMapping.java | 191 +++++++++ .../java/cuchaz/enigma/mapping/NameValidator.java | 80 ++++ .../cuchaz/enigma/mapping/ProcyonEntryFactory.java | 55 +++ src/main/java/cuchaz/enigma/mapping/Signature.java | 117 ++++++ .../cuchaz/enigma/mapping/SignatureUpdater.java | 94 +++++ .../enigma/mapping/TranslationDirection.java | 29 ++ .../java/cuchaz/enigma/mapping/Translator.java | 289 +++++++++++++ src/main/java/cuchaz/enigma/mapping/Type.java | 249 +++++++++++ 30 files changed, 3476 insertions(+) create mode 100644 src/main/java/cuchaz/enigma/mapping/ArgumentEntry.java create mode 100644 src/main/java/cuchaz/enigma/mapping/ArgumentMapping.java create mode 100644 src/main/java/cuchaz/enigma/mapping/BehaviorEntry.java create mode 100644 src/main/java/cuchaz/enigma/mapping/ClassEntry.java create mode 100644 src/main/java/cuchaz/enigma/mapping/ClassMapping.java create mode 100644 src/main/java/cuchaz/enigma/mapping/ClassNameReplacer.java create mode 100644 src/main/java/cuchaz/enigma/mapping/ConstructorEntry.java create mode 100644 src/main/java/cuchaz/enigma/mapping/Entry.java create mode 100644 src/main/java/cuchaz/enigma/mapping/EntryFactory.java create mode 100644 src/main/java/cuchaz/enigma/mapping/EntryPair.java create mode 100644 src/main/java/cuchaz/enigma/mapping/FieldEntry.java create mode 100644 src/main/java/cuchaz/enigma/mapping/FieldMapping.java create mode 100644 src/main/java/cuchaz/enigma/mapping/IllegalNameException.java create mode 100644 src/main/java/cuchaz/enigma/mapping/MappingParseException.java create mode 100644 src/main/java/cuchaz/enigma/mapping/Mappings.java create mode 100644 src/main/java/cuchaz/enigma/mapping/MappingsChecker.java create mode 100644 src/main/java/cuchaz/enigma/mapping/MappingsReader.java create mode 100644 src/main/java/cuchaz/enigma/mapping/MappingsReaderOld.java create mode 100644 src/main/java/cuchaz/enigma/mapping/MappingsRenamer.java create mode 100644 src/main/java/cuchaz/enigma/mapping/MappingsWriter.java create mode 100644 src/main/java/cuchaz/enigma/mapping/MemberMapping.java create mode 100644 src/main/java/cuchaz/enigma/mapping/MethodEntry.java create mode 100644 src/main/java/cuchaz/enigma/mapping/MethodMapping.java create mode 100644 src/main/java/cuchaz/enigma/mapping/NameValidator.java create mode 100644 src/main/java/cuchaz/enigma/mapping/ProcyonEntryFactory.java create mode 100644 src/main/java/cuchaz/enigma/mapping/Signature.java create mode 100644 src/main/java/cuchaz/enigma/mapping/SignatureUpdater.java create mode 100644 src/main/java/cuchaz/enigma/mapping/TranslationDirection.java create mode 100644 src/main/java/cuchaz/enigma/mapping/Translator.java create mode 100644 src/main/java/cuchaz/enigma/mapping/Type.java (limited to 'src/main/java/cuchaz/enigma/mapping') diff --git a/src/main/java/cuchaz/enigma/mapping/ArgumentEntry.java b/src/main/java/cuchaz/enigma/mapping/ArgumentEntry.java new file mode 100644 index 0000000..886e7be --- /dev/null +++ b/src/main/java/cuchaz/enigma/mapping/ArgumentEntry.java @@ -0,0 +1,116 @@ +/******************************************************************************* + * 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.mapping; + +import java.io.Serializable; + +import cuchaz.enigma.Util; + +public class ArgumentEntry implements Entry, Serializable { + + private static final long serialVersionUID = 4472172468162696006L; + + private BehaviorEntry m_behaviorEntry; + private int m_index; + private String m_name; + + public ArgumentEntry(BehaviorEntry behaviorEntry, int index, String name) { + if (behaviorEntry == null) { + throw new IllegalArgumentException("Behavior cannot be null!"); + } + if (index < 0) { + throw new IllegalArgumentException("Index must be non-negative!"); + } + if (name == null) { + throw new IllegalArgumentException("Argument name cannot be null!"); + } + + m_behaviorEntry = behaviorEntry; + m_index = index; + m_name = name; + } + + public ArgumentEntry(ArgumentEntry other) { + m_behaviorEntry = (BehaviorEntry) m_behaviorEntry.cloneToNewClass(getClassEntry()); + m_index = other.m_index; + m_name = other.m_name; + } + + public ArgumentEntry(ArgumentEntry other, String newClassName) { + m_behaviorEntry = (BehaviorEntry) other.m_behaviorEntry.cloneToNewClass(new ClassEntry(newClassName)); + m_index = other.m_index; + m_name = other.m_name; + } + + public BehaviorEntry getBehaviorEntry() { + return m_behaviorEntry; + } + + public int getIndex() { + return m_index; + } + + @Override + public String getName() { + return m_name; + } + + @Override + public ClassEntry getClassEntry() { + return m_behaviorEntry.getClassEntry(); + } + + @Override + public String getClassName() { + return m_behaviorEntry.getClassName(); + } + + @Override + public ArgumentEntry cloneToNewClass(ClassEntry classEntry) { + return new ArgumentEntry(this, classEntry.getName()); + } + + public String getMethodName() { + return m_behaviorEntry.getName(); + } + + public Signature getMethodSignature() { + return m_behaviorEntry.getSignature(); + } + + @Override + public int hashCode() { + return Util.combineHashesOrdered( + m_behaviorEntry, + Integer.valueOf(m_index).hashCode(), + m_name.hashCode() + ); + } + + @Override + public boolean equals(Object other) { + if (other instanceof ArgumentEntry) { + return equals((ArgumentEntry) other); + } + return false; + } + + public boolean equals(ArgumentEntry other) { + return m_behaviorEntry.equals(other.m_behaviorEntry) + && m_index == other.m_index + && m_name.equals(other.m_name); + } + + @Override + public String toString() { + return m_behaviorEntry.toString() + "(" + m_index + ":" + m_name + ")"; + } +} diff --git a/src/main/java/cuchaz/enigma/mapping/ArgumentMapping.java b/src/main/java/cuchaz/enigma/mapping/ArgumentMapping.java new file mode 100644 index 0000000..2b77d6e --- /dev/null +++ b/src/main/java/cuchaz/enigma/mapping/ArgumentMapping.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * 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.mapping; + +import java.io.Serializable; + +public class ArgumentMapping implements Serializable, Comparable { + + private static final long serialVersionUID = 8610742471440861315L; + + private int m_index; + private String m_name; + + // NOTE: this argument order is important for the MethodReader/MethodWriter + public ArgumentMapping(int index, String name) { + m_index = index; + m_name = NameValidator.validateArgumentName(name); + } + + public ArgumentMapping(ArgumentMapping other) { + m_index = other.m_index; + m_name = other.m_name; + } + + public int getIndex() { + return m_index; + } + + public String getName() { + return m_name; + } + + public void setName(String val) { + m_name = NameValidator.validateArgumentName(val); + } + + @Override + public int compareTo(ArgumentMapping other) { + return Integer.compare(m_index, other.m_index); + } +} diff --git a/src/main/java/cuchaz/enigma/mapping/BehaviorEntry.java b/src/main/java/cuchaz/enigma/mapping/BehaviorEntry.java new file mode 100644 index 0000000..f5c6c05 --- /dev/null +++ b/src/main/java/cuchaz/enigma/mapping/BehaviorEntry.java @@ -0,0 +1,15 @@ +/******************************************************************************* + * 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.mapping; + +public interface BehaviorEntry extends Entry { + Signature getSignature(); +} diff --git a/src/main/java/cuchaz/enigma/mapping/ClassEntry.java b/src/main/java/cuchaz/enigma/mapping/ClassEntry.java new file mode 100644 index 0000000..2e7711b --- /dev/null +++ b/src/main/java/cuchaz/enigma/mapping/ClassEntry.java @@ -0,0 +1,172 @@ +/******************************************************************************* + * 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.mapping; + +import com.google.common.collect.Lists; + +import java.io.Serializable; +import java.util.List; + +public class ClassEntry implements Entry, Serializable { + + private static final long serialVersionUID = 4235460580973955811L; + + private String m_name; + + public ClassEntry(String className) { + if (className == null) { + throw new IllegalArgumentException("Class name cannot be null!"); + } + if (className.indexOf('.') >= 0) { + throw new IllegalArgumentException("Class name must be in JVM format. ie, path/to/package/class$inner : " + className); + } + + m_name = className; + + if (isInnerClass() && getInnermostClassName().indexOf('/') >= 0) { + throw new IllegalArgumentException("Inner class must not have a package: " + className); + } + } + + public ClassEntry(ClassEntry other) { + m_name = other.m_name; + } + + @Override + public String getName() { + return m_name; + } + + @Override + public String getClassName() { + return m_name; + } + + @Override + public ClassEntry getClassEntry() { + return this; + } + + @Override + public ClassEntry cloneToNewClass(ClassEntry classEntry) { + return classEntry; + } + + @Override + public int hashCode() { + return m_name.hashCode(); + } + + @Override + public boolean equals(Object other) { + if (other instanceof ClassEntry) { + return equals((ClassEntry) other); + } + return false; + } + + public boolean equals(ClassEntry other) { + return m_name.equals(other.m_name); + } + + @Override + public String toString() { + return m_name; + } + + public boolean isInnerClass() { + return m_name.lastIndexOf('$') >= 0; + } + + public List getClassChainNames() { + return Lists.newArrayList(m_name.split("\\$")); + } + + public List getClassChain() { + List entries = Lists.newArrayList(); + StringBuilder buf = new StringBuilder(); + for (String name : getClassChainNames()) { + if (buf.length() > 0) { + buf.append("$"); + } + buf.append(name); + entries.add(new ClassEntry(buf.toString())); + } + return entries; + } + + public String getOutermostClassName() { + if (isInnerClass()) { + return m_name.substring(0, m_name.indexOf('$')); + } + return m_name; + } + + public ClassEntry getOutermostClassEntry() { + return new ClassEntry(getOutermostClassName()); + } + + public String getOuterClassName() { + if (!isInnerClass()) { + throw new Error("This is not an inner class!"); + } + return m_name.substring(0, m_name.lastIndexOf('$')); + } + + public ClassEntry getOuterClassEntry() { + return new ClassEntry(getOuterClassName()); + } + + public String getInnermostClassName() { + if (!isInnerClass()) { + throw new Error("This is not an inner class!"); + } + return m_name.substring(m_name.lastIndexOf('$') + 1); + } + + public boolean isInDefaultPackage() { + return m_name.indexOf('/') < 0; + } + + public String getPackageName() { + int pos = m_name.lastIndexOf('/'); + if (pos > 0) { + return m_name.substring(0, pos); + } + return null; + } + + public String getSimpleName() { + int pos = m_name.lastIndexOf('/'); + if (pos > 0) { + return m_name.substring(pos + 1); + } + return m_name; + } + + public ClassEntry buildClassEntry(List classChain) { + assert (classChain.contains(this)); + StringBuilder buf = new StringBuilder(); + for (ClassEntry chainEntry : classChain) { + if (buf.length() == 0) { + buf.append(chainEntry.getName()); + } else { + buf.append("$"); + buf.append(chainEntry.isInnerClass() ? chainEntry.getInnermostClassName() : chainEntry.getSimpleName()); + } + + if (chainEntry == this) { + break; + } + } + return new ClassEntry(buf.toString()); + } +} diff --git a/src/main/java/cuchaz/enigma/mapping/ClassMapping.java b/src/main/java/cuchaz/enigma/mapping/ClassMapping.java new file mode 100644 index 0000000..9258ec7 --- /dev/null +++ b/src/main/java/cuchaz/enigma/mapping/ClassMapping.java @@ -0,0 +1,460 @@ +/******************************************************************************* + * 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.mapping; + +import com.google.common.collect.Maps; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Map; + +public class ClassMapping implements Serializable, Comparable { + + private static final long serialVersionUID = -5148491146902340107L; + + private String m_obfFullName; + private String m_obfSimpleName; + private String m_deobfName; + private Map m_innerClassesByObfSimple; + private Map m_innerClassesByDeobf; + private Map m_fieldsByObf; + private Map m_fieldsByDeobf; + private Map m_methodsByObf; + private Map m_methodsByDeobf; + + public ClassMapping(String obfFullName) { + this(obfFullName, null); + } + + public ClassMapping(String obfFullName, String deobfName) { + m_obfFullName = obfFullName; + ClassEntry classEntry = new ClassEntry(obfFullName); + m_obfSimpleName = classEntry.isInnerClass() ? classEntry.getInnermostClassName() : classEntry.getSimpleName(); + m_deobfName = NameValidator.validateClassName(deobfName, false); + m_innerClassesByObfSimple = Maps.newHashMap(); + m_innerClassesByDeobf = Maps.newHashMap(); + m_fieldsByObf = Maps.newHashMap(); + m_fieldsByDeobf = Maps.newHashMap(); + m_methodsByObf = Maps.newHashMap(); + m_methodsByDeobf = Maps.newHashMap(); + } + + public String getObfFullName() { + return m_obfFullName; + } + + public String getObfSimpleName() { + return m_obfSimpleName; + } + + public String getDeobfName() { + return m_deobfName; + } + + public void setDeobfName(String val) { + m_deobfName = NameValidator.validateClassName(val, false); + } + + //// INNER CLASSES //////// + + public Iterable innerClasses() { + assert (m_innerClassesByObfSimple.size() >= m_innerClassesByDeobf.size()); + return m_innerClassesByObfSimple.values(); + } + + public void addInnerClassMapping(ClassMapping classMapping) { + boolean obfWasAdded = m_innerClassesByObfSimple.put(classMapping.getObfSimpleName(), classMapping) == null; + assert (obfWasAdded); + if (classMapping.getDeobfName() != null) { + assert (isSimpleClassName(classMapping.getDeobfName())); + boolean deobfWasAdded = m_innerClassesByDeobf.put(classMapping.getDeobfName(), classMapping) == null; + assert (deobfWasAdded); + } + } + + public void removeInnerClassMapping(ClassMapping classMapping) { + boolean obfWasRemoved = m_innerClassesByObfSimple.remove(classMapping.getObfSimpleName()) != null; + assert (obfWasRemoved); + if (classMapping.getDeobfName() != null) { + boolean deobfWasRemoved = m_innerClassesByDeobf.remove(classMapping.getDeobfName()) != null; + assert (deobfWasRemoved); + } + } + + public ClassMapping getOrCreateInnerClass(ClassEntry obfInnerClass) { + ClassMapping classMapping = m_innerClassesByObfSimple.get(obfInnerClass.getInnermostClassName()); + if (classMapping == null) { + classMapping = new ClassMapping(obfInnerClass.getName()); + boolean wasAdded = m_innerClassesByObfSimple.put(classMapping.getObfSimpleName(), classMapping) == null; + assert (wasAdded); + } + return classMapping; + } + + public ClassMapping getInnerClassByObfSimple(String obfSimpleName) { + assert (isSimpleClassName(obfSimpleName)); + return m_innerClassesByObfSimple.get(obfSimpleName); + } + + public ClassMapping getInnerClassByDeobf(String deobfName) { + assert (isSimpleClassName(deobfName)); + return m_innerClassesByDeobf.get(deobfName); + } + + public ClassMapping getInnerClassByDeobfThenObfSimple(String name) { + ClassMapping classMapping = getInnerClassByDeobf(name); + if (classMapping == null) { + classMapping = getInnerClassByObfSimple(name); + } + return classMapping; + } + + public String getDeobfInnerClassName(String obfSimpleName) { + assert (isSimpleClassName(obfSimpleName)); + ClassMapping classMapping = m_innerClassesByObfSimple.get(obfSimpleName); + if (classMapping != null) { + return classMapping.getDeobfName(); + } + return null; + } + + public void setInnerClassName(ClassEntry obfInnerClass, String deobfName) { + ClassMapping classMapping = getOrCreateInnerClass(obfInnerClass); + if (classMapping.getDeobfName() != null) { + boolean wasRemoved = m_innerClassesByDeobf.remove(classMapping.getDeobfName()) != null; + assert (wasRemoved); + } + classMapping.setDeobfName(deobfName); + if (deobfName != null) { + assert (isSimpleClassName(deobfName)); + boolean wasAdded = m_innerClassesByDeobf.put(deobfName, classMapping) == null; + assert (wasAdded); + } + } + + public boolean hasInnerClassByObfSimple(String obfSimpleName) { + return m_innerClassesByObfSimple.containsKey(obfSimpleName); + } + + public boolean hasInnerClassByDeobf(String deobfName) { + return m_innerClassesByDeobf.containsKey(deobfName); + } + + + //// FIELDS //////// + + public Iterable fields() { + assert (m_fieldsByObf.size() == m_fieldsByDeobf.size()); + return m_fieldsByObf.values(); + } + + public boolean containsObfField(String obfName, Type obfType) { + return m_fieldsByObf.containsKey(getFieldKey(obfName, obfType)); + } + + public boolean containsDeobfField(String deobfName, Type deobfType) { + return m_fieldsByDeobf.containsKey(getFieldKey(deobfName, deobfType)); + } + + public void addFieldMapping(FieldMapping fieldMapping) { + String obfKey = getFieldKey(fieldMapping.getObfName(), fieldMapping.getObfType()); + if (m_fieldsByObf.containsKey(obfKey)) { + throw new Error("Already have mapping for " + m_obfFullName + "." + obfKey); + } + String deobfKey = getFieldKey(fieldMapping.getDeobfName(), fieldMapping.getObfType()); + if (m_fieldsByDeobf.containsKey(deobfKey)) { + throw new Error("Already have mapping for " + m_deobfName + "." + deobfKey); + } + boolean obfWasAdded = m_fieldsByObf.put(obfKey, fieldMapping) == null; + assert (obfWasAdded); + boolean deobfWasAdded = m_fieldsByDeobf.put(deobfKey, fieldMapping) == null; + assert (deobfWasAdded); + assert (m_fieldsByObf.size() == m_fieldsByDeobf.size()); + } + + public void removeFieldMapping(FieldMapping fieldMapping) { + boolean obfWasRemoved = m_fieldsByObf.remove(getFieldKey(fieldMapping.getObfName(), fieldMapping.getObfType())) != null; + assert (obfWasRemoved); + if (fieldMapping.getDeobfName() != null) { + boolean deobfWasRemoved = m_fieldsByDeobf.remove(getFieldKey(fieldMapping.getDeobfName(), fieldMapping.getObfType())) != null; + assert (deobfWasRemoved); + } + } + + public FieldMapping getFieldByObf(String obfName, Type obfType) { + return m_fieldsByObf.get(getFieldKey(obfName, obfType)); + } + + public FieldMapping getFieldByDeobf(String deobfName, Type obfType) { + return m_fieldsByDeobf.get(getFieldKey(deobfName, obfType)); + } + + public String getObfFieldName(String deobfName, Type obfType) { + FieldMapping fieldMapping = m_fieldsByDeobf.get(getFieldKey(deobfName, obfType)); + if (fieldMapping != null) { + return fieldMapping.getObfName(); + } + return null; + } + + public String getDeobfFieldName(String obfName, Type obfType) { + FieldMapping fieldMapping = m_fieldsByObf.get(getFieldKey(obfName, obfType)); + if (fieldMapping != null) { + return fieldMapping.getDeobfName(); + } + return null; + } + + private String getFieldKey(String name, Type type) { + if (name == null) { + throw new IllegalArgumentException("name cannot be null!"); + } + if (type == null) { + throw new IllegalArgumentException("type cannot be null!"); + } + return name + ":" + type; + } + + + public void setFieldName(String obfName, Type obfType, String deobfName) { + assert (deobfName != null); + FieldMapping fieldMapping = m_fieldsByObf.get(getFieldKey(obfName, obfType)); + if (fieldMapping == null) { + fieldMapping = new FieldMapping(obfName, obfType, deobfName); + boolean obfWasAdded = m_fieldsByObf.put(getFieldKey(obfName, obfType), fieldMapping) == null; + assert (obfWasAdded); + } else { + boolean wasRemoved = m_fieldsByDeobf.remove(getFieldKey(fieldMapping.getDeobfName(), obfType)) != null; + assert (wasRemoved); + } + fieldMapping.setDeobfName(deobfName); + if (deobfName != null) { + boolean wasAdded = m_fieldsByDeobf.put(getFieldKey(deobfName, obfType), fieldMapping) == null; + assert (wasAdded); + } + } + + public void setFieldObfNameAndType(String oldObfName, Type obfType, String newObfName, Type newObfType) { + assert (newObfName != null); + FieldMapping fieldMapping = m_fieldsByObf.remove(getFieldKey(oldObfName, obfType)); + assert (fieldMapping != null); + fieldMapping.setObfName(newObfName); + fieldMapping.setObfType(newObfType); + boolean obfWasAdded = m_fieldsByObf.put(getFieldKey(newObfName, newObfType), fieldMapping) == null; + assert (obfWasAdded); + } + + + //// METHODS //////// + + public Iterable methods() { + assert (m_methodsByObf.size() >= m_methodsByDeobf.size()); + return m_methodsByObf.values(); + } + + public boolean containsObfMethod(String obfName, Signature obfSignature) { + return m_methodsByObf.containsKey(getMethodKey(obfName, obfSignature)); + } + + public boolean containsDeobfMethod(String deobfName, Signature obfSignature) { + return m_methodsByDeobf.containsKey(getMethodKey(deobfName, obfSignature)); + } + + public void addMethodMapping(MethodMapping methodMapping) { + String obfKey = getMethodKey(methodMapping.getObfName(), methodMapping.getObfSignature()); + if (m_methodsByObf.containsKey(obfKey)) { + throw new Error("Already have mapping for " + m_obfFullName + "." + obfKey); + } + boolean wasAdded = m_methodsByObf.put(obfKey, methodMapping) == null; + assert (wasAdded); + if (methodMapping.getDeobfName() != null) { + String deobfKey = getMethodKey(methodMapping.getDeobfName(), methodMapping.getObfSignature()); + if (m_methodsByDeobf.containsKey(deobfKey)) { + throw new Error("Already have mapping for " + m_deobfName + "." + deobfKey); + } + boolean deobfWasAdded = m_methodsByDeobf.put(deobfKey, methodMapping) == null; + assert (deobfWasAdded); + } + assert (m_methodsByObf.size() >= m_methodsByDeobf.size()); + } + + public void removeMethodMapping(MethodMapping methodMapping) { + boolean obfWasRemoved = m_methodsByObf.remove(getMethodKey(methodMapping.getObfName(), methodMapping.getObfSignature())) != null; + assert (obfWasRemoved); + if (methodMapping.getDeobfName() != null) { + boolean deobfWasRemoved = m_methodsByDeobf.remove(getMethodKey(methodMapping.getDeobfName(), methodMapping.getObfSignature())) != null; + assert (deobfWasRemoved); + } + } + + public MethodMapping getMethodByObf(String obfName, Signature obfSignature) { + return m_methodsByObf.get(getMethodKey(obfName, obfSignature)); + } + + public MethodMapping getMethodByDeobf(String deobfName, Signature obfSignature) { + return m_methodsByDeobf.get(getMethodKey(deobfName, obfSignature)); + } + + private String getMethodKey(String name, Signature signature) { + if (name == null) { + throw new IllegalArgumentException("name cannot be null!"); + } + if (signature == null) { + throw new IllegalArgumentException("signature cannot be null!"); + } + return name + signature; + } + + public void setMethodName(String obfName, Signature obfSignature, String deobfName) { + MethodMapping methodMapping = m_methodsByObf.get(getMethodKey(obfName, obfSignature)); + if (methodMapping == null) { + methodMapping = createMethodMapping(obfName, obfSignature); + } else if (methodMapping.getDeobfName() != null) { + boolean wasRemoved = m_methodsByDeobf.remove(getMethodKey(methodMapping.getDeobfName(), methodMapping.getObfSignature())) != null; + assert (wasRemoved); + } + methodMapping.setDeobfName(deobfName); + if (deobfName != null) { + boolean wasAdded = m_methodsByDeobf.put(getMethodKey(deobfName, obfSignature), methodMapping) == null; + assert (wasAdded); + } + } + + public void setMethodObfNameAndSignature(String oldObfName, Signature obfSignature, String newObfName, Signature newObfSignature) { + assert (newObfName != null); + MethodMapping methodMapping = m_methodsByObf.remove(getMethodKey(oldObfName, obfSignature)); + assert (methodMapping != null); + methodMapping.setObfName(newObfName); + methodMapping.setObfSignature(newObfSignature); + boolean obfWasAdded = m_methodsByObf.put(getMethodKey(newObfName, newObfSignature), methodMapping) == null; + assert (obfWasAdded); + } + + //// ARGUMENTS //////// + + public void setArgumentName(String obfMethodName, Signature obfMethodSignature, int argumentIndex, String argumentName) { + assert (argumentName != null); + MethodMapping methodMapping = m_methodsByObf.get(getMethodKey(obfMethodName, obfMethodSignature)); + if (methodMapping == null) { + methodMapping = createMethodMapping(obfMethodName, obfMethodSignature); + } + methodMapping.setArgumentName(argumentIndex, argumentName); + } + + public void removeArgumentName(String obfMethodName, Signature obfMethodSignature, int argumentIndex) { + m_methodsByObf.get(getMethodKey(obfMethodName, obfMethodSignature)).removeArgumentName(argumentIndex); + } + + private MethodMapping createMethodMapping(String obfName, Signature obfSignature) { + MethodMapping methodMapping = new MethodMapping(obfName, obfSignature); + boolean wasAdded = m_methodsByObf.put(getMethodKey(obfName, obfSignature), methodMapping) == null; + assert (wasAdded); + return methodMapping; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append(m_obfFullName); + buf.append(" <-> "); + buf.append(m_deobfName); + buf.append("\n"); + buf.append("Fields:\n"); + for (FieldMapping fieldMapping : fields()) { + buf.append("\t"); + buf.append(fieldMapping.getObfName()); + buf.append(" <-> "); + buf.append(fieldMapping.getDeobfName()); + buf.append("\n"); + } + buf.append("Methods:\n"); + for (MethodMapping methodMapping : m_methodsByObf.values()) { + buf.append(methodMapping.toString()); + buf.append("\n"); + } + buf.append("Inner Classes:\n"); + for (ClassMapping classMapping : m_innerClassesByObfSimple.values()) { + buf.append("\t"); + buf.append(classMapping.getObfSimpleName()); + buf.append(" <-> "); + buf.append(classMapping.getDeobfName()); + buf.append("\n"); + } + return buf.toString(); + } + + @Override + public int compareTo(ClassMapping other) { + // sort by a, b, c, ... aa, ab, etc + if (m_obfFullName.length() != other.m_obfFullName.length()) { + return m_obfFullName.length() - other.m_obfFullName.length(); + } + return m_obfFullName.compareTo(other.m_obfFullName); + } + + public boolean renameObfClass(String oldObfClassName, String newObfClassName) { + + // rename inner classes + for (ClassMapping innerClassMapping : new ArrayList(m_innerClassesByObfSimple.values())) { + if (innerClassMapping.renameObfClass(oldObfClassName, newObfClassName)) { + boolean wasRemoved = m_innerClassesByObfSimple.remove(oldObfClassName) != null; + assert (wasRemoved); + boolean wasAdded = m_innerClassesByObfSimple.put(newObfClassName, innerClassMapping) == null; + assert (wasAdded); + } + } + + // rename field types + for (FieldMapping fieldMapping : new ArrayList(m_fieldsByObf.values())) { + String oldFieldKey = getFieldKey(fieldMapping.getObfName(), fieldMapping.getObfType()); + if (fieldMapping.renameObfClass(oldObfClassName, newObfClassName)) { + boolean wasRemoved = m_fieldsByObf.remove(oldFieldKey) != null; + assert (wasRemoved); + boolean wasAdded = m_fieldsByObf.put(getFieldKey(fieldMapping.getObfName(), fieldMapping.getObfType()), fieldMapping) == null; + assert (wasAdded); + } + } + + // rename method signatures + for (MethodMapping methodMapping : new ArrayList(m_methodsByObf.values())) { + String oldMethodKey = getMethodKey(methodMapping.getObfName(), methodMapping.getObfSignature()); + if (methodMapping.renameObfClass(oldObfClassName, newObfClassName)) { + boolean wasRemoved = m_methodsByObf.remove(oldMethodKey) != null; + assert (wasRemoved); + boolean wasAdded = m_methodsByObf.put(getMethodKey(methodMapping.getObfName(), methodMapping.getObfSignature()), methodMapping) == null; + assert (wasAdded); + } + } + + if (m_obfFullName.equals(oldObfClassName)) { + // rename this class + m_obfFullName = newObfClassName; + return true; + } + return false; + } + + public boolean containsArgument(BehaviorEntry obfBehaviorEntry, String name) { + MethodMapping methodMapping = m_methodsByObf.get(getMethodKey(obfBehaviorEntry.getName(), obfBehaviorEntry.getSignature())); + if (methodMapping != null) { + return methodMapping.containsArgument(name); + } + return false; + } + + public static boolean isSimpleClassName(String name) { + return name.indexOf('/') < 0 && name.indexOf('$') < 0; + } + + public ClassEntry getObfEntry() { + return new ClassEntry(m_obfFullName); + } +} diff --git a/src/main/java/cuchaz/enigma/mapping/ClassNameReplacer.java b/src/main/java/cuchaz/enigma/mapping/ClassNameReplacer.java new file mode 100644 index 0000000..dc833bb --- /dev/null +++ b/src/main/java/cuchaz/enigma/mapping/ClassNameReplacer.java @@ -0,0 +1,15 @@ +/******************************************************************************* + * 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.mapping; + +public interface ClassNameReplacer { + String replace(String className); +} diff --git a/src/main/java/cuchaz/enigma/mapping/ConstructorEntry.java b/src/main/java/cuchaz/enigma/mapping/ConstructorEntry.java new file mode 100644 index 0000000..907bd4c --- /dev/null +++ b/src/main/java/cuchaz/enigma/mapping/ConstructorEntry.java @@ -0,0 +1,116 @@ +/******************************************************************************* + * 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.mapping; + +import java.io.Serializable; + +import cuchaz.enigma.Util; + +public class ConstructorEntry implements BehaviorEntry, Serializable { + + private static final long serialVersionUID = -868346075317366758L; + + private ClassEntry m_classEntry; + private Signature m_signature; + + public ConstructorEntry(ClassEntry classEntry) { + this(classEntry, null); + } + + public ConstructorEntry(ClassEntry classEntry, Signature signature) { + if (classEntry == null) { + throw new IllegalArgumentException("Class cannot be null!"); + } + + m_classEntry = classEntry; + m_signature = signature; + } + + public ConstructorEntry(ConstructorEntry other) { + m_classEntry = new ClassEntry(other.m_classEntry); + m_signature = other.m_signature; + } + + public ConstructorEntry(ConstructorEntry other, String newClassName) { + m_classEntry = new ClassEntry(newClassName); + m_signature = other.m_signature; + } + + @Override + public ClassEntry getClassEntry() { + return m_classEntry; + } + + @Override + public String getName() { + if (isStatic()) { + return ""; + } + return ""; + } + + public boolean isStatic() { + return m_signature == null; + } + + @Override + public Signature getSignature() { + return m_signature; + } + + @Override + public String getClassName() { + return m_classEntry.getName(); + } + + @Override + public ConstructorEntry cloneToNewClass(ClassEntry classEntry) { + return new ConstructorEntry(this, classEntry.getName()); + } + + @Override + public int hashCode() { + if (isStatic()) { + return Util.combineHashesOrdered(m_classEntry); + } else { + return Util.combineHashesOrdered(m_classEntry, m_signature); + } + } + + @Override + public boolean equals(Object other) { + if (other instanceof ConstructorEntry) { + return equals((ConstructorEntry) other); + } + return false; + } + + public boolean equals(ConstructorEntry other) { + if (isStatic() != other.isStatic()) { + return false; + } + + if (isStatic()) { + return m_classEntry.equals(other.m_classEntry); + } else { + return m_classEntry.equals(other.m_classEntry) && m_signature.equals(other.m_signature); + } + } + + @Override + public String toString() { + if (isStatic()) { + return m_classEntry.getName() + "." + getName(); + } else { + return m_classEntry.getName() + "." + getName() + m_signature; + } + } +} diff --git a/src/main/java/cuchaz/enigma/mapping/Entry.java b/src/main/java/cuchaz/enigma/mapping/Entry.java new file mode 100644 index 0000000..95747d5 --- /dev/null +++ b/src/main/java/cuchaz/enigma/mapping/Entry.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * 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.mapping; + +public interface Entry { + String getName(); + + String getClassName(); + + ClassEntry getClassEntry(); + + Entry cloneToNewClass(ClassEntry classEntry); +} diff --git a/src/main/java/cuchaz/enigma/mapping/EntryFactory.java b/src/main/java/cuchaz/enigma/mapping/EntryFactory.java new file mode 100644 index 0000000..bd6ce4e --- /dev/null +++ b/src/main/java/cuchaz/enigma/mapping/EntryFactory.java @@ -0,0 +1,162 @@ +/******************************************************************************* + * 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.mapping; + +import cuchaz.enigma.analysis.JarIndex; +import javassist.*; +import javassist.bytecode.Descriptor; +import javassist.expr.ConstructorCall; +import javassist.expr.FieldAccess; +import javassist.expr.MethodCall; +import javassist.expr.NewExpr; + +public class EntryFactory { + + public static ClassEntry getClassEntry(CtClass c) { + return new ClassEntry(Descriptor.toJvmName(c.getName())); + } + + public static ClassEntry getObfClassEntry(JarIndex jarIndex, ClassMapping classMapping) { + ClassEntry obfClassEntry = new ClassEntry(classMapping.getObfFullName()); + return obfClassEntry.buildClassEntry(jarIndex.getObfClassChain(obfClassEntry)); + } + + private static ClassEntry getObfClassEntry(ClassMapping classMapping) { + return new ClassEntry(classMapping.getObfFullName()); + } + + public static ClassEntry getDeobfClassEntry(ClassMapping classMapping) { + return new ClassEntry(classMapping.getDeobfName()); + } + + public static ClassEntry getSuperclassEntry(CtClass c) { + return new ClassEntry(Descriptor.toJvmName(c.getClassFile().getSuperclass())); + } + + public static FieldEntry getFieldEntry(CtField field) { + return new FieldEntry( + getClassEntry(field.getDeclaringClass()), + field.getName(), + new Type(field.getFieldInfo().getDescriptor()) + ); + } + + public static FieldEntry getFieldEntry(FieldAccess call) { + return new FieldEntry( + new ClassEntry(Descriptor.toJvmName(call.getClassName())), + call.getFieldName(), + new Type(call.getSignature()) + ); + } + + public static FieldEntry getFieldEntry(String className, String name, String type) { + return new FieldEntry(new ClassEntry(className), name, new Type(type)); + } + + public static FieldEntry getObfFieldEntry(ClassMapping classMapping, FieldMapping fieldMapping) { + return new FieldEntry( + getObfClassEntry(classMapping), + fieldMapping.getObfName(), + fieldMapping.getObfType() + ); + } + + public static MethodEntry getMethodEntry(CtMethod method) { + return new MethodEntry( + getClassEntry(method.getDeclaringClass()), + method.getName(), + new Signature(method.getMethodInfo().getDescriptor()) + ); + } + + public static MethodEntry getMethodEntry(MethodCall call) { + return new MethodEntry( + new ClassEntry(Descriptor.toJvmName(call.getClassName())), + call.getMethodName(), + new Signature(call.getSignature()) + ); + } + + public static ConstructorEntry getConstructorEntry(CtConstructor constructor) { + if (constructor.isClassInitializer()) { + return new ConstructorEntry( + getClassEntry(constructor.getDeclaringClass()) + ); + } else { + return new ConstructorEntry( + getClassEntry(constructor.getDeclaringClass()), + new Signature(constructor.getMethodInfo().getDescriptor()) + ); + } + } + + public static ConstructorEntry getConstructorEntry(ConstructorCall call) { + return new ConstructorEntry( + new ClassEntry(Descriptor.toJvmName(call.getClassName())), + new Signature(call.getSignature()) + ); + } + + public static ConstructorEntry getConstructorEntry(NewExpr call) { + return new ConstructorEntry( + new ClassEntry(Descriptor.toJvmName(call.getClassName())), + new Signature(call.getSignature()) + ); + } + + public static BehaviorEntry getBehaviorEntry(CtBehavior behavior) { + if (behavior instanceof CtMethod) { + return getMethodEntry((CtMethod) behavior); + } else if (behavior instanceof CtConstructor) { + return getConstructorEntry((CtConstructor) behavior); + } + throw new Error("behavior is neither Method nor Constructor!"); + } + + public static BehaviorEntry getBehaviorEntry(String className, String behaviorName, String behaviorSignature) { + return getBehaviorEntry(new ClassEntry(className), behaviorName, new Signature(behaviorSignature)); + } + + public static BehaviorEntry getBehaviorEntry(String className, String behaviorName) { + return getBehaviorEntry(new ClassEntry(className), behaviorName); + } + + public static BehaviorEntry getBehaviorEntry(String className) { + return new ConstructorEntry(new ClassEntry(className)); + } + + public static BehaviorEntry getBehaviorEntry(ClassEntry classEntry, String behaviorName, Signature behaviorSignature) { + switch (behaviorName) { + case "": + return new ConstructorEntry(classEntry, behaviorSignature); + case "": + return new ConstructorEntry(classEntry); + default: + return new MethodEntry(classEntry, behaviorName, behaviorSignature); + } + } + + public static BehaviorEntry getBehaviorEntry(ClassEntry classEntry, String behaviorName) { + if (behaviorName.equals("")) { + return new ConstructorEntry(classEntry); + } else { + throw new IllegalArgumentException("Only class initializers don't have signatures"); + } + } + + public static BehaviorEntry getObfBehaviorEntry(ClassEntry classEntry, MethodMapping methodMapping) { + return getBehaviorEntry(classEntry, methodMapping.getObfName(), methodMapping.getObfSignature()); + } + + public static BehaviorEntry getObfBehaviorEntry(ClassMapping classMapping, MethodMapping methodMapping) { + return getObfBehaviorEntry(getObfClassEntry(classMapping), methodMapping); + } +} diff --git a/src/main/java/cuchaz/enigma/mapping/EntryPair.java b/src/main/java/cuchaz/enigma/mapping/EntryPair.java new file mode 100644 index 0000000..1c93d53 --- /dev/null +++ b/src/main/java/cuchaz/enigma/mapping/EntryPair.java @@ -0,0 +1,22 @@ +/******************************************************************************* + * 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.mapping; + +public class EntryPair { + + public T obf; + public T deobf; + + public EntryPair(T obf, T deobf) { + this.obf = obf; + this.deobf = deobf; + } +} diff --git a/src/main/java/cuchaz/enigma/mapping/FieldEntry.java b/src/main/java/cuchaz/enigma/mapping/FieldEntry.java new file mode 100644 index 0000000..3de7223 --- /dev/null +++ b/src/main/java/cuchaz/enigma/mapping/FieldEntry.java @@ -0,0 +1,99 @@ +/******************************************************************************* + * 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.mapping; + +import java.io.Serializable; + +import cuchaz.enigma.Util; + +public class FieldEntry implements Entry, Serializable { + + private static final long serialVersionUID = 3004663582802885451L; + + private ClassEntry m_classEntry; + private String m_name; + private Type m_type; + + // NOTE: this argument order is important for the MethodReader/MethodWriter + public FieldEntry(ClassEntry classEntry, String name, Type type) { + if (classEntry == null) { + throw new IllegalArgumentException("Class cannot be null!"); + } + if (name == null) { + throw new IllegalArgumentException("Field name cannot be null!"); + } + if (type == null) { + throw new IllegalArgumentException("Field type cannot be null!"); + } + + m_classEntry = classEntry; + m_name = name; + m_type = type; + } + + public FieldEntry(FieldEntry other) { + this(other, new ClassEntry(other.m_classEntry)); + } + + public FieldEntry(FieldEntry other, ClassEntry newClassEntry) { + m_classEntry = newClassEntry; + m_name = other.m_name; + m_type = other.m_type; + } + + @Override + public ClassEntry getClassEntry() { + return m_classEntry; + } + + @Override + public String getName() { + return m_name; + } + + @Override + public String getClassName() { + return m_classEntry.getName(); + } + + public Type getType() { + return m_type; + } + + @Override + public FieldEntry cloneToNewClass(ClassEntry classEntry) { + return new FieldEntry(this, classEntry); + } + + @Override + public int hashCode() { + return Util.combineHashesOrdered(m_classEntry, m_name, m_type); + } + + @Override + public boolean equals(Object other) { + if (other instanceof FieldEntry) { + return equals((FieldEntry) other); + } + return false; + } + + public boolean equals(FieldEntry other) { + return m_classEntry.equals(other.m_classEntry) + && m_name.equals(other.m_name) + && m_type.equals(other.m_type); + } + + @Override + public String toString() { + return m_classEntry.getName() + "." + m_name + ":" + m_type; + } +} diff --git a/src/main/java/cuchaz/enigma/mapping/FieldMapping.java b/src/main/java/cuchaz/enigma/mapping/FieldMapping.java new file mode 100644 index 0000000..3f5a382 --- /dev/null +++ b/src/main/java/cuchaz/enigma/mapping/FieldMapping.java @@ -0,0 +1,89 @@ +/******************************************************************************* + * 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.mapping; + +import java.io.Serializable; + +public class FieldMapping implements Serializable, Comparable, MemberMapping { + + private static final long serialVersionUID = 8610742471440861315L; + + private String m_obfName; + private String m_deobfName; + private Type m_obfType; + + public FieldMapping(String obfName, Type obfType, String deobfName) { + m_obfName = obfName; + m_deobfName = NameValidator.validateFieldName(deobfName); + m_obfType = obfType; + } + + public FieldMapping(FieldMapping other, ClassNameReplacer obfClassNameReplacer) { + m_obfName = other.m_obfName; + m_deobfName = other.m_deobfName; + m_obfType = new Type(other.m_obfType, obfClassNameReplacer); + } + + @Override + public String getObfName() { + return m_obfName; + } + + public void setObfName(String val) { + m_obfName = NameValidator.validateFieldName(val); + } + + public String getDeobfName() { + return m_deobfName; + } + + public void setDeobfName(String val) { + m_deobfName = NameValidator.validateFieldName(val); + } + + public Type getObfType() { + return m_obfType; + } + + public void setObfType(Type val) { + m_obfType = val; + } + + @Override + public int compareTo(FieldMapping other) { + return (m_obfName + m_obfType).compareTo(other.m_obfName + other.m_obfType); + } + + public boolean renameObfClass(final String oldObfClassName, final String newObfClassName) { + + // rename obf classes in the type + Type newType = new Type(m_obfType, new ClassNameReplacer() { + @Override + public String replace(String className) { + if (className.equals(oldObfClassName)) { + return newObfClassName; + } + return null; + } + }); + + if (!newType.equals(m_obfType)) { + m_obfType = newType; + return true; + } + return false; + } + + @Override + public FieldEntry getObfEntry(ClassEntry classEntry) { + return new FieldEntry(classEntry, m_obfName, new Type(m_obfType)); + } +} diff --git a/src/main/java/cuchaz/enigma/mapping/IllegalNameException.java b/src/main/java/cuchaz/enigma/mapping/IllegalNameException.java new file mode 100644 index 0000000..f2119d8 --- /dev/null +++ b/src/main/java/cuchaz/enigma/mapping/IllegalNameException.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * 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.mapping; + +public class IllegalNameException extends RuntimeException { + + private static final long serialVersionUID = -2279910052561114323L; + + private String m_name; + private String m_reason; + + public IllegalNameException(String name) { + this(name, null); + } + + public IllegalNameException(String name, String reason) { + m_name = name; + m_reason = reason; + } + + public String getReason() { + return m_reason; + } + + @Override + public String getMessage() { + StringBuilder buf = new StringBuilder(); + buf.append("Illegal name: "); + buf.append(m_name); + if (m_reason != null) { + buf.append(" because "); + buf.append(m_reason); + } + return buf.toString(); + } +} diff --git a/src/main/java/cuchaz/enigma/mapping/MappingParseException.java b/src/main/java/cuchaz/enigma/mapping/MappingParseException.java new file mode 100644 index 0000000..3c25ea5 --- /dev/null +++ b/src/main/java/cuchaz/enigma/mapping/MappingParseException.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * 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.mapping; + +public class MappingParseException extends Exception { + + private static final long serialVersionUID = -5487280332892507236L; + + private int m_line; + private String m_message; + + public MappingParseException(int line, String message) { + m_line = line; + m_message = message; + } + + @Override + public String getMessage() { + return "Line " + m_line + ": " + m_message; + } +} diff --git a/src/main/java/cuchaz/enigma/mapping/Mappings.java b/src/main/java/cuchaz/enigma/mapping/Mappings.java new file mode 100644 index 0000000..a48ec3f --- /dev/null +++ b/src/main/java/cuchaz/enigma/mapping/Mappings.java @@ -0,0 +1,201 @@ +/******************************************************************************* + * 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.mapping; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; + +import java.io.Serializable; +import java.util.*; + +import cuchaz.enigma.analysis.TranslationIndex; + +public class Mappings implements Serializable { + + private static final long serialVersionUID = 4649790259460259026L; + + protected Map m_classesByObf; + protected Map m_classesByDeobf; + + public Mappings() { + m_classesByObf = Maps.newHashMap(); + m_classesByDeobf = Maps.newHashMap(); + } + + public Mappings(Iterable classes) { + this(); + + for (ClassMapping classMapping : classes) { + m_classesByObf.put(classMapping.getObfFullName(), classMapping); + if (classMapping.getDeobfName() != null) { + m_classesByDeobf.put(classMapping.getDeobfName(), classMapping); + } + } + } + + public Collection classes() { + assert (m_classesByObf.size() >= m_classesByDeobf.size()); + return m_classesByObf.values(); + } + + public void addClassMapping(ClassMapping classMapping) { + if (m_classesByObf.containsKey(classMapping.getObfFullName())) { + throw new Error("Already have mapping for " + classMapping.getObfFullName()); + } + boolean obfWasAdded = m_classesByObf.put(classMapping.getObfFullName(), classMapping) == null; + assert (obfWasAdded); + if (classMapping.getDeobfName() != null) { + if (m_classesByDeobf.containsKey(classMapping.getDeobfName())) { + throw new Error("Already have mapping for " + classMapping.getDeobfName()); + } + boolean deobfWasAdded = m_classesByDeobf.put(classMapping.getDeobfName(), classMapping) == null; + assert (deobfWasAdded); + } + } + + public void removeClassMapping(ClassMapping classMapping) { + boolean obfWasRemoved = m_classesByObf.remove(classMapping.getObfFullName()) != null; + assert (obfWasRemoved); + if (classMapping.getDeobfName() != null) { + boolean deobfWasRemoved = m_classesByDeobf.remove(classMapping.getDeobfName()) != null; + assert (deobfWasRemoved); + } + } + + public ClassMapping getClassByObf(ClassEntry entry) { + return getClassByObf(entry.getName()); + } + + public ClassMapping getClassByObf(String obfName) { + return m_classesByObf.get(obfName); + } + + public ClassMapping getClassByDeobf(ClassEntry entry) { + return getClassByDeobf(entry.getName()); + } + + public ClassMapping getClassByDeobf(String deobfName) { + return m_classesByDeobf.get(deobfName); + } + + public void setClassDeobfName(ClassMapping classMapping, String deobfName) { + if (classMapping.getDeobfName() != null) { + boolean wasRemoved = m_classesByDeobf.remove(classMapping.getDeobfName()) != null; + assert (wasRemoved); + } + classMapping.setDeobfName(deobfName); + if (deobfName != null) { + boolean wasAdded = m_classesByDeobf.put(deobfName, classMapping) == null; + assert (wasAdded); + } + } + + public Translator getTranslator(TranslationDirection direction, TranslationIndex index) { + switch (direction) { + case Deobfuscating: + + return new Translator(direction, m_classesByObf, index); + + case Obfuscating: + + // fill in the missing deobf class entries with obf entries + Map classes = Maps.newHashMap(); + for (ClassMapping classMapping : classes()) { + if (classMapping.getDeobfName() != null) { + classes.put(classMapping.getDeobfName(), classMapping); + } else { + classes.put(classMapping.getObfFullName(), classMapping); + } + } + + // translate the translation index + // NOTE: this isn't actually recursive + TranslationIndex deobfIndex = new TranslationIndex(index, getTranslator(TranslationDirection.Deobfuscating, index)); + + return new Translator(direction, classes, deobfIndex); + + default: + throw new Error("Invalid translation direction!"); + } + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + for (ClassMapping classMapping : m_classesByObf.values()) { + buf.append(classMapping.toString()); + buf.append("\n"); + } + return buf.toString(); + } + + public void renameObfClass(String oldObfName, String newObfName) { + new ArrayList<>(classes()).stream().filter(classMapping -> classMapping.renameObfClass(oldObfName, newObfName)).forEach(classMapping -> { + boolean wasRemoved = m_classesByObf.remove(oldObfName) != null; + assert (wasRemoved); + boolean wasAdded = m_classesByObf.put(newObfName, classMapping) == null; + assert (wasAdded); + }); + } + + public Set getAllObfClassNames() { + final Set classNames = Sets.newHashSet(); + for (ClassMapping classMapping : classes()) { + + // add the class name + classNames.add(classMapping.getObfFullName()); + + // add classes from method signatures + for (MethodMapping methodMapping : classMapping.methods()) { + for (Type type : methodMapping.getObfSignature().types()) { + if (type.hasClass()) { + classNames.add(type.getClassEntry().getClassName()); + } + } + } + } + return classNames; + } + + public boolean containsDeobfClass(String deobfName) { + return m_classesByDeobf.containsKey(deobfName); + } + + public boolean containsDeobfField(ClassEntry obfClassEntry, String deobfName, Type obfType) { + ClassMapping classMapping = m_classesByObf.get(obfClassEntry.getName()); + return classMapping != null && classMapping.containsDeobfField(deobfName, obfType); + } + + public boolean containsDeobfMethod(ClassEntry obfClassEntry, String deobfName, Signature deobfSignature) { + ClassMapping classMapping = m_classesByObf.get(obfClassEntry.getName()); + return classMapping != null && classMapping.containsDeobfMethod(deobfName, deobfSignature); + } + + public boolean containsArgument(BehaviorEntry obfBehaviorEntry, String name) { + ClassMapping classMapping = m_classesByObf.get(obfBehaviorEntry.getClassName()); + return classMapping != null && classMapping.containsArgument(obfBehaviorEntry, name); + } + + public List getClassMappingChain(ClassEntry obfClass) { + List mappingChain = Lists.newArrayList(); + ClassMapping classMapping = null; + for (ClassEntry obfClassEntry : obfClass.getClassChain()) { + if (mappingChain.isEmpty()) { + classMapping = m_classesByObf.get(obfClassEntry.getName()); + } else if (classMapping != null) { + classMapping = classMapping.getInnerClassByObfSimple(obfClassEntry.getInnermostClassName()); + } + mappingChain.add(classMapping); + } + return mappingChain; + } +} diff --git a/src/main/java/cuchaz/enigma/mapping/MappingsChecker.java b/src/main/java/cuchaz/enigma/mapping/MappingsChecker.java new file mode 100644 index 0000000..ab68682 --- /dev/null +++ b/src/main/java/cuchaz/enigma/mapping/MappingsChecker.java @@ -0,0 +1,107 @@ +/******************************************************************************* + * 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.mapping; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; + +import java.util.Map; + +import cuchaz.enigma.analysis.JarIndex; +import cuchaz.enigma.analysis.RelatedMethodChecker; + + +public class MappingsChecker { + + private JarIndex m_index; + private RelatedMethodChecker m_relatedMethodChecker; + private Map m_droppedClassMappings; + private Map m_droppedInnerClassMappings; + private Map m_droppedFieldMappings; + private Map m_droppedMethodMappings; + + public MappingsChecker(JarIndex index) { + m_index = index; + m_relatedMethodChecker = new RelatedMethodChecker(m_index); + m_droppedClassMappings = Maps.newHashMap(); + m_droppedInnerClassMappings = Maps.newHashMap(); + m_droppedFieldMappings = Maps.newHashMap(); + m_droppedMethodMappings = Maps.newHashMap(); + } + + public RelatedMethodChecker getRelatedMethodChecker() { + return m_relatedMethodChecker; + } + + public Map getDroppedClassMappings() { + return m_droppedClassMappings; + } + + public Map getDroppedInnerClassMappings() { + return m_droppedInnerClassMappings; + } + + public Map getDroppedFieldMappings() { + return m_droppedFieldMappings; + } + + public Map getDroppedMethodMappings() { + return m_droppedMethodMappings; + } + + public void dropBrokenMappings(Mappings mappings) { + for (ClassMapping classMapping : Lists.newArrayList(mappings.classes())) { + if (!checkClassMapping(classMapping)) { + mappings.removeClassMapping(classMapping); + m_droppedClassMappings.put(EntryFactory.getObfClassEntry(m_index, classMapping), classMapping); + } + } + } + + private boolean checkClassMapping(ClassMapping classMapping) { + + // check the class + ClassEntry classEntry = EntryFactory.getObfClassEntry(m_index, classMapping); + if (!m_index.getObfClassEntries().contains(classEntry)) { + return false; + } + + // check the fields + for (FieldMapping fieldMapping : Lists.newArrayList(classMapping.fields())) { + FieldEntry obfFieldEntry = EntryFactory.getObfFieldEntry(classMapping, fieldMapping); + if (!m_index.containsObfField(obfFieldEntry)) { + classMapping.removeFieldMapping(fieldMapping); + m_droppedFieldMappings.put(obfFieldEntry, fieldMapping); + } + } + + // check methods + for (MethodMapping methodMapping : Lists.newArrayList(classMapping.methods())) { + BehaviorEntry obfBehaviorEntry = EntryFactory.getObfBehaviorEntry(classEntry, methodMapping); + if (!m_index.containsObfBehavior(obfBehaviorEntry)) { + classMapping.removeMethodMapping(methodMapping); + m_droppedMethodMappings.put(obfBehaviorEntry, methodMapping); + } + + m_relatedMethodChecker.checkMethod(classEntry, methodMapping); + } + + // check inner classes + for (ClassMapping innerClassMapping : Lists.newArrayList(classMapping.innerClasses())) { + if (!checkClassMapping(innerClassMapping)) { + classMapping.removeInnerClassMapping(innerClassMapping); + m_droppedInnerClassMappings.put(EntryFactory.getObfClassEntry(m_index, innerClassMapping), innerClassMapping); + } + } + + return true; + } +} diff --git a/src/main/java/cuchaz/enigma/mapping/MappingsReader.java b/src/main/java/cuchaz/enigma/mapping/MappingsReader.java new file mode 100644 index 0000000..c790eed --- /dev/null +++ b/src/main/java/cuchaz/enigma/mapping/MappingsReader.java @@ -0,0 +1,92 @@ +/******************************************************************************* + * 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.mapping; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; + +import cuchaz.enigma.json.JsonClass; + +public class MappingsReader { + + public Mappings read(File in) throws IOException, MappingParseException { + Mappings mappings = new Mappings(); + readDirectory(mappings, in); + return mappings; + } + + public void readDirectory(Mappings mappings, File in) throws IOException, MappingParseException { + + File[] fList = in.listFiles(); + for (File file : fList) { + if (file.isFile()) { + readFile(mappings, new BufferedReader(new FileReader(file))); + } else if (file.isDirectory()) { + readDirectory(mappings, file.getAbsoluteFile()); + } + } + } + + public void readFile(Mappings mappings, BufferedReader in) throws IOException, MappingParseException { + + String builder = ""; + String line = null; + while ((line = in.readLine()) != null) { + builder += line; + } + + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + JsonClass jsonClass = gson.fromJson(builder, JsonClass.class); + load(null, jsonClass, mappings); + } + + public void load(ClassMapping parent, JsonClass jsonClass, Mappings mappings) { + ClassMapping classMapping = readClass(jsonClass.getObf(), jsonClass.getName()); + if (parent != null) { + parent.addInnerClassMapping(classMapping); + } else { + mappings.addClassMapping(classMapping); + } + jsonClass.getField().forEach(jsonField -> classMapping.addFieldMapping(readField(jsonField.getObf(), jsonField.getName(), jsonField.getType()))); + + jsonClass.getMethod().forEach(jsonMethod -> { + MethodMapping methodMapping = readMethod(jsonMethod.getObf(), jsonMethod.getName(), jsonMethod.getSignature()); + jsonMethod.getArgs().forEach(jsonArgument -> methodMapping.addArgumentMapping(readArgument(jsonArgument.getIndex(), jsonArgument.getName()))); + classMapping.addMethodMapping(methodMapping); + }); + + jsonClass.getInnerClass().forEach(jsonInnerClasses -> { + load(classMapping, jsonInnerClasses, mappings); + }); + } + + private ArgumentMapping readArgument(int index, String name) { + return new ArgumentMapping(index, name); + } + + private ClassMapping readClass(String obf, String deobf) { + return new ClassMapping("none/" + obf, deobf); + } + + /* TEMP */ + protected FieldMapping readField(String obf, String deobf, String sig) { + return new FieldMapping(obf, new Type(sig), deobf); + } + + private MethodMapping readMethod(String obf, String deobf, String sig) { + return new MethodMapping(obf, new Signature(sig), deobf); + } +} diff --git a/src/main/java/cuchaz/enigma/mapping/MappingsReaderOld.java b/src/main/java/cuchaz/enigma/mapping/MappingsReaderOld.java new file mode 100644 index 0000000..1a93604 --- /dev/null +++ b/src/main/java/cuchaz/enigma/mapping/MappingsReaderOld.java @@ -0,0 +1,122 @@ +package cuchaz.enigma.mapping; + +import com.google.common.collect.Queues; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.Reader; +import java.util.Deque; + +public class MappingsReaderOld { + + public Mappings read(Reader in) throws IOException, MappingParseException { + return read(new BufferedReader(in)); + } + + public Mappings read(BufferedReader in) throws IOException, MappingParseException { + Mappings mappings = new Mappings(); + Deque mappingStack = Queues.newArrayDeque(); + + int lineNumber = 0; + String line = null; + while ((line = in.readLine()) != null) { + lineNumber++; + + // strip comments + int commentPos = line.indexOf('#'); + if (commentPos >= 0) { + line = line.substring(0, commentPos); + } + + // skip blank lines + if (line.trim().length() <= 0) { + continue; + } + + // get the indent of this line + int indent = 0; + for (int i = 0; i < line.length(); i++) { + if (line.charAt(i) != '\t') { + break; + } + indent++; + } + + // handle stack pops + while (indent < mappingStack.size()) { + mappingStack.pop(); + } + + String[] parts = line.trim().split("\\s"); + try { + // read the first token + String token = parts[0]; + + if (token.equalsIgnoreCase("CLASS")) { + ClassMapping classMapping; + if (indent <= 0) { + // outer class + classMapping = readClass(parts, false); + mappings.addClassMapping(classMapping); + } else { + + // inner class + if (!(mappingStack.peek() instanceof ClassMapping)) { + throw new MappingParseException(lineNumber, "Unexpected CLASS entry here!"); + } + + classMapping = readClass(parts, true); + ((ClassMapping) mappingStack.peek()).addInnerClassMapping(classMapping); + } + mappingStack.push(classMapping); + } else if (token.equalsIgnoreCase("FIELD")) { + if (mappingStack.isEmpty() || !(mappingStack.peek() instanceof ClassMapping)) { + throw new MappingParseException(lineNumber, "Unexpected FIELD entry here!"); + } + ((ClassMapping) mappingStack.peek()).addFieldMapping(readField(parts)); + } else if (token.equalsIgnoreCase("METHOD")) { + if (mappingStack.isEmpty() || !(mappingStack.peek() instanceof ClassMapping)) { + throw new MappingParseException(lineNumber, "Unexpected METHOD entry here!"); + } + MethodMapping methodMapping = readMethod(parts); + ((ClassMapping) mappingStack.peek()).addMethodMapping(methodMapping); + mappingStack.push(methodMapping); + } else if (token.equalsIgnoreCase("ARG")) { + if (mappingStack.isEmpty() || !(mappingStack.peek() instanceof MethodMapping)) { + throw new MappingParseException(lineNumber, "Unexpected ARG entry here!"); + } + ((MethodMapping) mappingStack.peek()).addArgumentMapping(readArgument(parts)); + } + } catch (ArrayIndexOutOfBoundsException | IllegalArgumentException ex) { + throw new MappingParseException(lineNumber, "Malformed line:\n" + line); + } + } + + return mappings; + } + + private ArgumentMapping readArgument(String[] parts) { + return new ArgumentMapping(Integer.parseInt(parts[1]), parts[2]); + } + + private ClassMapping readClass(String[] parts, boolean makeSimple) { + if (parts.length == 2) { + return new ClassMapping(parts[1]); + } else { + return new ClassMapping(parts[1], parts[2]); + } + } + + /* TEMP */ + protected FieldMapping readField(String[] parts) { + return new FieldMapping(parts[1], new Type(parts[3]), parts[2]); + } + + private MethodMapping readMethod(String[] parts) { + if (parts.length == 3) { + return new MethodMapping(parts[1], new Signature(parts[2])); + } else { + return new MethodMapping(parts[1], new Signature(parts[3]), parts[2]); + } + } +} diff --git a/src/main/java/cuchaz/enigma/mapping/MappingsRenamer.java b/src/main/java/cuchaz/enigma/mapping/MappingsRenamer.java new file mode 100644 index 0000000..de1635a --- /dev/null +++ b/src/main/java/cuchaz/enigma/mapping/MappingsRenamer.java @@ -0,0 +1,237 @@ +/******************************************************************************* + * 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.mapping; + +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.util.List; +import java.util.Set; +import java.util.zip.GZIPOutputStream; + +import cuchaz.enigma.analysis.JarIndex; + +public class MappingsRenamer { + + private JarIndex m_index; + private Mappings m_mappings; + + public MappingsRenamer(JarIndex index, Mappings mappings) { + m_index = index; + m_mappings = mappings; + } + + public void setClassName(ClassEntry obf, String deobfName) { + + deobfName = NameValidator.validateClassName(deobfName, !obf.isInnerClass()); + + List mappingChain = getOrCreateClassMappingChain(obf); + if (mappingChain.size() == 1) { + + if (deobfName != null) { + // make sure we don't rename to an existing obf or deobf class + if (m_mappings.containsDeobfClass(deobfName) || m_index.containsObfClass(new ClassEntry(deobfName))) { + throw new IllegalNameException(deobfName, "There is already a class with that name"); + } + } + + ClassMapping classMapping = mappingChain.get(0); + m_mappings.setClassDeobfName(classMapping, deobfName); + + } else { + + ClassMapping outerClassMapping = mappingChain.get(mappingChain.size() - 2); + + if (deobfName != null) { + // make sure we don't rename to an existing obf or deobf inner class + if (outerClassMapping.hasInnerClassByDeobf(deobfName) || outerClassMapping.hasInnerClassByObfSimple(deobfName)) { + throw new IllegalNameException(deobfName, "There is already a class with that name"); + } + } + + outerClassMapping.setInnerClassName(obf, deobfName); + } + } + + public void removeClassMapping(ClassEntry obf) { + setClassName(obf, null); + } + + public void markClassAsDeobfuscated(ClassEntry obf) { + String deobfName = obf.isInnerClass() ? obf.getInnermostClassName() : obf.getName(); + List mappingChain = getOrCreateClassMappingChain(obf); + if (mappingChain.size() == 1) { + ClassMapping classMapping = mappingChain.get(0); + m_mappings.setClassDeobfName(classMapping, deobfName); + } else { + ClassMapping outerClassMapping = mappingChain.get(mappingChain.size() - 2); + outerClassMapping.setInnerClassName(obf, deobfName); + } + } + + public void setFieldName(FieldEntry obf, String deobfName) { + deobfName = NameValidator.validateFieldName(deobfName); + FieldEntry targetEntry = new FieldEntry(obf.getClassEntry(), deobfName, obf.getType()); + if (m_mappings.containsDeobfField(obf.getClassEntry(), deobfName, obf.getType()) || m_index.containsObfField(targetEntry)) { + throw new IllegalNameException(deobfName, "There is already a field with that name"); + } + + ClassMapping classMapping = getOrCreateClassMapping(obf.getClassEntry()); + classMapping.setFieldName(obf.getName(), obf.getType(), deobfName); + } + + public void removeFieldMapping(FieldEntry obf) { + ClassMapping classMapping = getOrCreateClassMapping(obf.getClassEntry()); + classMapping.removeFieldMapping(classMapping.getFieldByObf(obf.getName(), obf.getType())); + } + + public void markFieldAsDeobfuscated(FieldEntry obf) { + ClassMapping classMapping = getOrCreateClassMapping(obf.getClassEntry()); + classMapping.setFieldName(obf.getName(), obf.getType(), obf.getName()); + } + + public void setMethodTreeName(MethodEntry obf, String deobfName) { + Set implementations = m_index.getRelatedMethodImplementations(obf); + + deobfName = NameValidator.validateMethodName(deobfName); + for (MethodEntry entry : implementations) { + Signature deobfSignature = m_mappings.getTranslator(TranslationDirection.Deobfuscating, m_index.getTranslationIndex()).translateSignature(obf.getSignature()); + MethodEntry targetEntry = new MethodEntry(entry.getClassEntry(), deobfName, deobfSignature); + if (m_mappings.containsDeobfMethod(entry.getClassEntry(), deobfName, entry.getSignature()) || m_index.containsObfBehavior(targetEntry)) { + String deobfClassName = m_mappings.getTranslator(TranslationDirection.Deobfuscating, m_index.getTranslationIndex()).translateClass(entry.getClassName()); + throw new IllegalNameException(deobfName, "There is already a method with that name and signature in class " + deobfClassName); + } + } + + for (MethodEntry entry : implementations) { + setMethodName(entry, deobfName); + } + } + + public void setMethodName(MethodEntry obf, String deobfName) { + deobfName = NameValidator.validateMethodName(deobfName); + MethodEntry targetEntry = new MethodEntry(obf.getClassEntry(), deobfName, obf.getSignature()); + if (m_mappings.containsDeobfMethod(obf.getClassEntry(), deobfName, obf.getSignature()) || m_index.containsObfBehavior(targetEntry)) { + String deobfClassName = m_mappings.getTranslator(TranslationDirection.Deobfuscating, m_index.getTranslationIndex()).translateClass(obf.getClassName()); + throw new IllegalNameException(deobfName, "There is already a method with that name and signature in class " + deobfClassName); + } + + ClassMapping classMapping = getOrCreateClassMapping(obf.getClassEntry()); + classMapping.setMethodName(obf.getName(), obf.getSignature(), deobfName); + } + + public void removeMethodTreeMapping(MethodEntry obf) { + for (MethodEntry implementation : m_index.getRelatedMethodImplementations(obf)) { + removeMethodMapping(implementation); + } + } + + public void removeMethodMapping(MethodEntry obf) { + ClassMapping classMapping = getOrCreateClassMapping(obf.getClassEntry()); + classMapping.setMethodName(obf.getName(), obf.getSignature(), null); + } + + public void markMethodTreeAsDeobfuscated(MethodEntry obf) { + for (MethodEntry implementation : m_index.getRelatedMethodImplementations(obf)) { + markMethodAsDeobfuscated(implementation); + } + } + + public void markMethodAsDeobfuscated(MethodEntry obf) { + ClassMapping classMapping = getOrCreateClassMapping(obf.getClassEntry()); + classMapping.setMethodName(obf.getName(), obf.getSignature(), obf.getName()); + } + + public void setArgumentName(ArgumentEntry obf, String deobfName) { + deobfName = NameValidator.validateArgumentName(deobfName); + // NOTE: don't need to check arguments for name collisions with names determined by Procyon + if (m_mappings.containsArgument(obf.getBehaviorEntry(), deobfName)) { + throw new IllegalNameException(deobfName, "There is already an argument with that name"); + } + + ClassMapping classMapping = getOrCreateClassMapping(obf.getClassEntry()); + classMapping.setArgumentName(obf.getMethodName(), obf.getMethodSignature(), obf.getIndex(), deobfName); + } + + public void removeArgumentMapping(ArgumentEntry obf) { + ClassMapping classMapping = getOrCreateClassMapping(obf.getClassEntry()); + classMapping.removeArgumentName(obf.getMethodName(), obf.getMethodSignature(), obf.getIndex()); + } + + public void markArgumentAsDeobfuscated(ArgumentEntry obf) { + ClassMapping classMapping = getOrCreateClassMapping(obf.getClassEntry()); + classMapping.setArgumentName(obf.getMethodName(), obf.getMethodSignature(), obf.getIndex(), obf.getName()); + } + + public boolean moveFieldToObfClass(ClassMapping classMapping, FieldMapping fieldMapping, ClassEntry obfClass) { + classMapping.removeFieldMapping(fieldMapping); + ClassMapping targetClassMapping = getOrCreateClassMapping(obfClass); + if (!targetClassMapping.containsObfField(fieldMapping.getObfName(), fieldMapping.getObfType())) { + if (!targetClassMapping.containsDeobfField(fieldMapping.getDeobfName(), fieldMapping.getObfType())) { + targetClassMapping.addFieldMapping(fieldMapping); + return true; + } else { + System.err.println("WARNING: deobf field was already there: " + obfClass + "." + fieldMapping.getDeobfName()); + } + } + return false; + } + + public boolean moveMethodToObfClass(ClassMapping classMapping, MethodMapping methodMapping, ClassEntry obfClass) { + classMapping.removeMethodMapping(methodMapping); + ClassMapping targetClassMapping = getOrCreateClassMapping(obfClass); + if (!targetClassMapping.containsObfMethod(methodMapping.getObfName(), methodMapping.getObfSignature())) { + if (!targetClassMapping.containsDeobfMethod(methodMapping.getDeobfName(), methodMapping.getObfSignature())) { + targetClassMapping.addMethodMapping(methodMapping); + return true; + } else { + System.err.println("WARNING: deobf method was already there: " + obfClass + "." + methodMapping.getDeobfName() + methodMapping.getObfSignature()); + } + } + return false; + } + + public void write(OutputStream out) throws IOException { + // TEMP: just use the object output for now. We can find a more efficient storage format later + GZIPOutputStream gzipout = new GZIPOutputStream(out); + ObjectOutputStream oout = new ObjectOutputStream(gzipout); + oout.writeObject(this); + gzipout.finish(); + } + + private ClassMapping getOrCreateClassMapping(ClassEntry obfClassEntry) { + List mappingChain = getOrCreateClassMappingChain(obfClassEntry); + return mappingChain.get(mappingChain.size() - 1); + } + + private List getOrCreateClassMappingChain(ClassEntry obfClassEntry) { + List classChain = obfClassEntry.getClassChain(); + List mappingChain = m_mappings.getClassMappingChain(obfClassEntry); + for (int i = 0; i < classChain.size(); i++) { + ClassEntry classEntry = classChain.get(i); + ClassMapping classMapping = mappingChain.get(i); + if (classMapping == null) { + + // create it + classMapping = new ClassMapping(classEntry.getName()); + mappingChain.set(i, classMapping); + + // add it to the right parent + if (i == 0) { + m_mappings.addClassMapping(classMapping); + } else { + mappingChain.get(i - 1).addInnerClassMapping(classMapping); + } + } + } + return mappingChain; + } +} diff --git a/src/main/java/cuchaz/enigma/mapping/MappingsWriter.java b/src/main/java/cuchaz/enigma/mapping/MappingsWriter.java new file mode 100644 index 0000000..aa37f16 --- /dev/null +++ b/src/main/java/cuchaz/enigma/mapping/MappingsWriter.java @@ -0,0 +1,82 @@ +/******************************************************************************* + * 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.mapping; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import cuchaz.enigma.json.*; + +public class MappingsWriter { + + + public void write(File file, Mappings mappings) throws IOException { + if (!file.isDirectory()) { + //TODO Error + } + + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + for (ClassMapping classMapping : sorted(mappings.classes())) { + JsonClass jsonClass = new JsonClass(classMapping.getObfSimpleName(), classMapping.getDeobfName()); + write(jsonClass, classMapping); + + File f = new File(file, jsonClass.getName() + ".json"); + f.getParentFile().mkdirs(); + f.createNewFile(); + FileWriter writer = new FileWriter(f); + writer.write(gson.toJson(jsonClass)); + writer.close(); + } + } + + private void write(JsonClass jsonClass, ClassMapping classMapping) throws IOException { + if (classMapping.getDeobfName() != null && !classMapping.getDeobfName().equalsIgnoreCase("") && !classMapping.getDeobfName().equalsIgnoreCase("null")) { + + for (ClassMapping innerClassMapping : sorted(classMapping.innerClasses())) { + JsonClass innerClass = new JsonClass(classMapping.getObfSimpleName() + "$" + innerClassMapping.getObfSimpleName().replace("nome/", ""), innerClassMapping.getDeobfName()); + write(innerClass, innerClassMapping); + jsonClass.addInnerClass(innerClass); + } + + for (FieldMapping fieldMapping : sorted(classMapping.fields())) { + jsonClass.addField(new JsonField(fieldMapping.getObfName(), fieldMapping.getDeobfName(), fieldMapping.getObfType().toString())); + } + + for (MethodMapping methodMapping : sorted(classMapping.methods())) { + List args = new ArrayList<>(); + for (ArgumentMapping argumentMapping : sorted(methodMapping.arguments())) { + args.add(new JsonArgument(argumentMapping.getIndex(), argumentMapping.getName())); + } + if (methodMapping.getObfName().contains("") || methodMapping.getObfName().contains("")) { + jsonClass.addConstructor(new JsonConstructor(methodMapping.getObfSignature().toString(), args, methodMapping.getObfName().contains(""))); + } else { + jsonClass.addMethod(new JsonMethod(methodMapping.getObfName(), methodMapping.getDeobfName(), methodMapping.getObfSignature().toString(), args)); + } + } + } + } + + private > List sorted(Iterable classes) { + List out = new ArrayList(); + for (T t : classes) { + out.add(t); + } + Collections.sort(out); + return out; + } +} diff --git a/src/main/java/cuchaz/enigma/mapping/MemberMapping.java b/src/main/java/cuchaz/enigma/mapping/MemberMapping.java new file mode 100644 index 0000000..90c096f --- /dev/null +++ b/src/main/java/cuchaz/enigma/mapping/MemberMapping.java @@ -0,0 +1,18 @@ +/******************************************************************************* + * 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.mapping; + + +public interface MemberMapping { + T getObfEntry(ClassEntry classEntry); + + String getObfName(); +} diff --git a/src/main/java/cuchaz/enigma/mapping/MethodEntry.java b/src/main/java/cuchaz/enigma/mapping/MethodEntry.java new file mode 100644 index 0000000..301da61 --- /dev/null +++ b/src/main/java/cuchaz/enigma/mapping/MethodEntry.java @@ -0,0 +1,104 @@ +/******************************************************************************* + * 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.mapping; + +import java.io.Serializable; + +import cuchaz.enigma.Util; + +public class MethodEntry implements BehaviorEntry, Serializable { + + private static final long serialVersionUID = 4770915224467247458L; + + private ClassEntry m_classEntry; + private String m_name; + private Signature m_signature; + + public MethodEntry(ClassEntry classEntry, String name, Signature signature) { + if (classEntry == null) { + throw new IllegalArgumentException("Class cannot be null!"); + } + if (name == null) { + throw new IllegalArgumentException("Method name cannot be null!"); + } + if (signature == null) { + throw new IllegalArgumentException("Method signature cannot be null!"); + } + if (name.startsWith("<")) { + throw new IllegalArgumentException("Don't use MethodEntry for a constructor!"); + } + + m_classEntry = classEntry; + m_name = name; + m_signature = signature; + } + + public MethodEntry(MethodEntry other) { + m_classEntry = new ClassEntry(other.m_classEntry); + m_name = other.m_name; + m_signature = other.m_signature; + } + + public MethodEntry(MethodEntry other, String newClassName) { + m_classEntry = new ClassEntry(newClassName); + m_name = other.m_name; + m_signature = other.m_signature; + } + + @Override + public ClassEntry getClassEntry() { + return m_classEntry; + } + + @Override + public String getName() { + return m_name; + } + + @Override + public Signature getSignature() { + return m_signature; + } + + @Override + public String getClassName() { + return m_classEntry.getName(); + } + + @Override + public MethodEntry cloneToNewClass(ClassEntry classEntry) { + return new MethodEntry(this, classEntry.getName()); + } + + @Override + public int hashCode() { + return Util.combineHashesOrdered(m_classEntry, m_name, m_signature); + } + + @Override + public boolean equals(Object other) { + if (other instanceof MethodEntry) { + return equals((MethodEntry) other); + } + return false; + } + + public boolean equals(MethodEntry other) { + return m_classEntry.equals(other.m_classEntry) + && m_name.equals(other.m_name) + && m_signature.equals(other.m_signature); + } + + @Override + public String toString() { + return m_classEntry.getName() + "." + m_name + m_signature; + } +} diff --git a/src/main/java/cuchaz/enigma/mapping/MethodMapping.java b/src/main/java/cuchaz/enigma/mapping/MethodMapping.java new file mode 100644 index 0000000..d1beddd --- /dev/null +++ b/src/main/java/cuchaz/enigma/mapping/MethodMapping.java @@ -0,0 +1,191 @@ +/******************************************************************************* + * 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.mapping; + +import com.google.common.collect.Maps; + +import java.io.Serializable; +import java.util.Map; +import java.util.Map.Entry; + +public class MethodMapping implements Serializable, Comparable, MemberMapping { + + private static final long serialVersionUID = -4409570216084263978L; + + private String m_obfName; + private String m_deobfName; + private Signature m_obfSignature; + private Map m_arguments; + + public MethodMapping(String obfName, Signature obfSignature) { + this(obfName, obfSignature, null); + } + + public MethodMapping(String obfName, Signature obfSignature, String deobfName) { + if (obfName == null) { + throw new IllegalArgumentException("obf name cannot be null!"); + } + if (obfSignature == null) { + throw new IllegalArgumentException("obf signature cannot be null!"); + } + m_obfName = obfName; + m_deobfName = NameValidator.validateMethodName(deobfName); + m_obfSignature = obfSignature; + m_arguments = Maps.newTreeMap(); + } + + public MethodMapping(MethodMapping other, ClassNameReplacer obfClassNameReplacer) { + m_obfName = other.m_obfName; + m_deobfName = other.m_deobfName; + m_obfSignature = new Signature(other.m_obfSignature, obfClassNameReplacer); + m_arguments = Maps.newTreeMap(); + for (Entry entry : other.m_arguments.entrySet()) { + m_arguments.put(entry.getKey(), new ArgumentMapping(entry.getValue())); + } + } + + @Override + public String getObfName() { + return m_obfName; + } + + public void setObfName(String val) { + m_obfName = NameValidator.validateMethodName(val); + } + + public String getDeobfName() { + return m_deobfName; + } + + public void setDeobfName(String val) { + m_deobfName = NameValidator.validateMethodName(val); + } + + public Signature getObfSignature() { + return m_obfSignature; + } + + public void setObfSignature(Signature val) { + m_obfSignature = val; + } + + public Iterable arguments() { + return m_arguments.values(); + } + + public boolean isConstructor() { + return m_obfName.startsWith("<"); + } + + public void addArgumentMapping(ArgumentMapping argumentMapping) { + boolean wasAdded = m_arguments.put(argumentMapping.getIndex(), argumentMapping) == null; + assert (wasAdded); + } + + public String getObfArgumentName(int index) { + ArgumentMapping argumentMapping = m_arguments.get(index); + if (argumentMapping != null) { + return argumentMapping.getName(); + } + + return null; + } + + public String getDeobfArgumentName(int index) { + ArgumentMapping argumentMapping = m_arguments.get(index); + if (argumentMapping != null) { + return argumentMapping.getName(); + } + + return null; + } + + public void setArgumentName(int index, String name) { + ArgumentMapping argumentMapping = m_arguments.get(index); + if (argumentMapping == null) { + argumentMapping = new ArgumentMapping(index, name); + boolean wasAdded = m_arguments.put(index, argumentMapping) == null; + assert (wasAdded); + } else { + argumentMapping.setName(name); + } + } + + public void removeArgumentName(int index) { + boolean wasRemoved = m_arguments.remove(index) != null; + assert (wasRemoved); + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append("\t"); + buf.append(m_obfName); + buf.append(" <-> "); + buf.append(m_deobfName); + buf.append("\n"); + buf.append("\t"); + buf.append(m_obfSignature); + buf.append("\n"); + buf.append("\tArguments:\n"); + for (ArgumentMapping argumentMapping : m_arguments.values()) { + buf.append("\t\t"); + buf.append(argumentMapping.getIndex()); + buf.append(" -> "); + buf.append(argumentMapping.getName()); + buf.append("\n"); + } + return buf.toString(); + } + + @Override + public int compareTo(MethodMapping other) { + return (m_obfName + m_obfSignature).compareTo(other.m_obfName + other.m_obfSignature); + } + + public boolean renameObfClass(final String oldObfClassName, final String newObfClassName) { + + // rename obf classes in the signature + Signature newSignature = new Signature(m_obfSignature, new ClassNameReplacer() { + @Override + public String replace(String className) { + if (className.equals(oldObfClassName)) { + return newObfClassName; + } + return null; + } + }); + + if (!newSignature.equals(m_obfSignature)) { + m_obfSignature = newSignature; + return true; + } + return false; + } + + public boolean containsArgument(String name) { + for (ArgumentMapping argumentMapping : m_arguments.values()) { + if (argumentMapping.getName().equals(name)) { + return true; + } + } + return false; + } + + @Override + public BehaviorEntry getObfEntry(ClassEntry classEntry) { + if (isConstructor()) { + return new ConstructorEntry(classEntry, m_obfSignature); + } else { + return new MethodEntry(classEntry, m_obfName, m_obfSignature); + } + } +} diff --git a/src/main/java/cuchaz/enigma/mapping/NameValidator.java b/src/main/java/cuchaz/enigma/mapping/NameValidator.java new file mode 100644 index 0000000..a3b9a78 --- /dev/null +++ b/src/main/java/cuchaz/enigma/mapping/NameValidator.java @@ -0,0 +1,80 @@ +/******************************************************************************* + * 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.mapping; + +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; + +import javassist.bytecode.Descriptor; + +public class NameValidator { + + private static final Pattern IdentifierPattern; + private static final Pattern ClassPattern; + private static final List ReservedWords = Arrays.asList( + "abstract", "continue", "for", "new", "switch", "assert", "default", "goto", "package", "synchronized", + "boolean", "do", "if", "private", "this", "break", "double", "implements", "protected", "throw", "byte", + "else", "import", "public", "throws", "case", "enum", "instanceof", "return", "transient", "catch", + "extends", "int", "short", "try", "char", "final", "interface", "static", "void", "class", "finally", + "long", "strictfp", "volatile", "const", "float", "native", "super", "while" + ); + + static { + + // java allows all kinds of weird characters... + StringBuilder startChars = new StringBuilder(); + StringBuilder partChars = new StringBuilder(); + for (int i = Character.MIN_CODE_POINT; i <= Character.MAX_CODE_POINT; i++) { + if (Character.isJavaIdentifierStart(i)) { + startChars.appendCodePoint(i); + } + if (Character.isJavaIdentifierPart(i)) { + partChars.appendCodePoint(i); + } + } + + String identifierRegex = "[A-Za-z_<][A-Za-z0-9_>]*"; + IdentifierPattern = Pattern.compile(identifierRegex); + ClassPattern = Pattern.compile(String.format("^(%s(\\.|/))*(%s)$", identifierRegex, identifierRegex)); + } + + public static String validateClassName(String name, boolean packageRequired) { + if (name == null) { + return null; + } + if (!ClassPattern.matcher(name).matches() || ReservedWords.contains(name)) { + throw new IllegalNameException(name, "This doesn't look like a legal class name"); + } + if (packageRequired && new ClassEntry(name).getPackageName() == null) { + throw new IllegalNameException(name, "Class must be in a package"); + } + return Descriptor.toJvmName(name); + } + + public static String validateFieldName(String name) { + if (name == null) { + return null; + } + if (!IdentifierPattern.matcher(name).matches() || ReservedWords.contains(name)) { + throw new IllegalNameException(name, "This doesn't look like a legal identifier"); + } + return name; + } + + public static String validateMethodName(String name) { + return validateFieldName(name); + } + + public static String validateArgumentName(String name) { + return validateFieldName(name); + } +} diff --git a/src/main/java/cuchaz/enigma/mapping/ProcyonEntryFactory.java b/src/main/java/cuchaz/enigma/mapping/ProcyonEntryFactory.java new file mode 100644 index 0000000..ac42499 --- /dev/null +++ b/src/main/java/cuchaz/enigma/mapping/ProcyonEntryFactory.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * 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.mapping; + +import com.strobel.assembler.metadata.FieldDefinition; +import com.strobel.assembler.metadata.MethodDefinition; + + +public class ProcyonEntryFactory { + + public static FieldEntry getFieldEntry(FieldDefinition def) { + return new FieldEntry( + new ClassEntry(def.getDeclaringType().getInternalName()), + def.getName(), + new Type(def.getErasedSignature()) + ); + } + + public static MethodEntry getMethodEntry(MethodDefinition def) { + return new MethodEntry( + new ClassEntry(def.getDeclaringType().getInternalName()), + def.getName(), + new Signature(def.getErasedSignature()) + ); + } + + public static ConstructorEntry getConstructorEntry(MethodDefinition def) { + if (def.isTypeInitializer()) { + return new ConstructorEntry( + new ClassEntry(def.getDeclaringType().getInternalName()) + ); + } else { + return new ConstructorEntry( + new ClassEntry(def.getDeclaringType().getInternalName()), + new Signature(def.getErasedSignature()) + ); + } + } + + public static BehaviorEntry getBehaviorEntry(MethodDefinition def) { + if (def.isConstructor() || def.isTypeInitializer()) { + return getConstructorEntry(def); + } else { + return getMethodEntry(def); + } + } +} diff --git a/src/main/java/cuchaz/enigma/mapping/Signature.java b/src/main/java/cuchaz/enigma/mapping/Signature.java new file mode 100644 index 0000000..117018a --- /dev/null +++ b/src/main/java/cuchaz/enigma/mapping/Signature.java @@ -0,0 +1,117 @@ +/******************************************************************************* + * 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.mapping; + +import com.google.common.collect.Lists; + +import java.io.Serializable; +import java.util.List; + +import cuchaz.enigma.Util; + +public class Signature implements Serializable { + + private static final long serialVersionUID = -5843719505729497539L; + + private List m_argumentTypes; + private Type m_returnType; + + public Signature(String signature) { + try { + m_argumentTypes = Lists.newArrayList(); + int i = 0; + while (i < signature.length()) { + char c = signature.charAt(i); + if (c == '(') { + assert (m_argumentTypes.isEmpty()); + assert (m_returnType == null); + i++; + } else if (c == ')') { + i++; + break; + } else { + String type = Type.parseFirst(signature.substring(i)); + m_argumentTypes.add(new Type(type)); + i += type.length(); + } + } + m_returnType = new Type(Type.parseFirst(signature.substring(i))); + } catch (Exception ex) { + throw new IllegalArgumentException("Unable to parse signature: " + signature, ex); + } + } + + public Signature(Signature other) { + m_argumentTypes = Lists.newArrayList(other.m_argumentTypes); + m_returnType = new Type(other.m_returnType); + } + + public Signature(Signature other, ClassNameReplacer replacer) { + m_argumentTypes = Lists.newArrayList(other.m_argumentTypes); + for (int i = 0; i < m_argumentTypes.size(); i++) { + m_argumentTypes.set(i, new Type(m_argumentTypes.get(i), replacer)); + } + m_returnType = new Type(other.m_returnType, replacer); + } + + public List getArgumentTypes() { + return m_argumentTypes; + } + + public Type getReturnType() { + return m_returnType; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append("("); + for (Type type : m_argumentTypes) { + buf.append(type.toString()); + } + buf.append(")"); + buf.append(m_returnType.toString()); + return buf.toString(); + } + + public Iterable types() { + List types = Lists.newArrayList(); + types.addAll(m_argumentTypes); + types.add(m_returnType); + return types; + } + + @Override + public boolean equals(Object other) { + if (other instanceof Signature) { + return equals((Signature) other); + } + return false; + } + + public boolean equals(Signature other) { + return m_argumentTypes.equals(other.m_argumentTypes) && m_returnType.equals(other.m_returnType); + } + + @Override + public int hashCode() { + return Util.combineHashesOrdered(m_argumentTypes.hashCode(), m_returnType.hashCode()); + } + + public boolean hasClass(ClassEntry classEntry) { + for (Type type : types()) { + if (type.hasClass() && type.getClassEntry().equals(classEntry)) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/cuchaz/enigma/mapping/SignatureUpdater.java b/src/main/java/cuchaz/enigma/mapping/SignatureUpdater.java new file mode 100644 index 0000000..dac692e --- /dev/null +++ b/src/main/java/cuchaz/enigma/mapping/SignatureUpdater.java @@ -0,0 +1,94 @@ +/******************************************************************************* + * 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.mapping; + +import com.google.common.collect.Lists; + +import java.io.IOException; +import java.io.StringReader; +import java.util.List; + +public class SignatureUpdater { + + public interface ClassNameUpdater { + String update(String className); + } + + public static String update(String signature, ClassNameUpdater updater) { + try { + StringBuilder buf = new StringBuilder(); + + // read the signature character-by-character + StringReader reader = new StringReader(signature); + int i = -1; + while ((i = reader.read()) != -1) { + char c = (char) i; + + // does this character start a class name? + if (c == 'L') { + // update the class name and add it to the buffer + buf.append('L'); + String className = readClass(reader); + if (className == null) { + throw new IllegalArgumentException("Malformed signature: " + signature); + } + buf.append(updater.update(className)); + buf.append(';'); + } else { + // copy the character into the buffer + buf.append(c); + } + } + + return buf.toString(); + } catch (IOException ex) { + // I'm pretty sure a StringReader will never throw one of these + throw new Error(ex); + } + } + + private static String readClass(StringReader reader) throws IOException { + // read all the characters in the buffer until we hit a ';' + // remember to treat generics correctly + StringBuilder buf = new StringBuilder(); + int depth = 0; + int i = -1; + while ((i = reader.read()) != -1) { + char c = (char) i; + + if (c == '<') { + depth++; + } else if (c == '>') { + depth--; + } else if (depth == 0) { + if (c == ';') { + return buf.toString(); + } else { + buf.append(c); + } + } + } + + return null; + } + + public static List getClasses(String signature) { + final List classNames = Lists.newArrayList(); + update(signature, new ClassNameUpdater() { + @Override + public String update(String className) { + classNames.add(className); + return className; + } + }); + return classNames; + } +} diff --git a/src/main/java/cuchaz/enigma/mapping/TranslationDirection.java b/src/main/java/cuchaz/enigma/mapping/TranslationDirection.java new file mode 100644 index 0000000..8329d0d --- /dev/null +++ b/src/main/java/cuchaz/enigma/mapping/TranslationDirection.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * 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.mapping; + +public enum TranslationDirection { + + Deobfuscating { + @Override + public T choose(T deobfChoice, T obfChoice) { + return deobfChoice; + } + }, + Obfuscating { + @Override + public T choose(T deobfChoice, T obfChoice) { + return obfChoice; + } + }; + + public abstract T choose(T deobfChoice, T obfChoice); +} diff --git a/src/main/java/cuchaz/enigma/mapping/Translator.java b/src/main/java/cuchaz/enigma/mapping/Translator.java new file mode 100644 index 0000000..2829a75 --- /dev/null +++ b/src/main/java/cuchaz/enigma/mapping/Translator.java @@ -0,0 +1,289 @@ +/******************************************************************************* + * 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.mapping; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; + +import java.util.List; +import java.util.Map; + +import cuchaz.enigma.analysis.TranslationIndex; + +public class Translator { + + private TranslationDirection m_direction; + private Map m_classes; + private TranslationIndex m_index; + + private ClassNameReplacer m_classNameReplacer = new ClassNameReplacer() { + @Override + public String replace(String className) { + return translateEntry(new ClassEntry(className)).getName(); + } + }; + + public Translator() { + m_direction = null; + m_classes = Maps.newHashMap(); + m_index = new TranslationIndex(); + } + + public Translator(TranslationDirection direction, Map classes, TranslationIndex index) { + m_direction = direction; + m_classes = classes; + m_index = index; + } + + public TranslationDirection getDirection() { + return m_direction; + } + + public TranslationIndex getTranslationIndex() { + return m_index; + } + + @SuppressWarnings("unchecked") + public T translateEntry(T entry) { + if (entry instanceof ClassEntry) { + return (T) translateEntry((ClassEntry) entry); + } else if (entry instanceof FieldEntry) { + return (T) translateEntry((FieldEntry) entry); + } else if (entry instanceof MethodEntry) { + return (T) translateEntry((MethodEntry) entry); + } else if (entry instanceof ConstructorEntry) { + return (T) translateEntry((ConstructorEntry) entry); + } else if (entry instanceof ArgumentEntry) { + return (T) translateEntry((ArgumentEntry) entry); + } else { + throw new Error("Unknown entry type: " + entry.getClass().getName()); + } + } + + public String translate(T entry) { + if (entry instanceof ClassEntry) { + return translate((ClassEntry) entry); + } else if (entry instanceof FieldEntry) { + return translate((FieldEntry) entry); + } else if (entry instanceof MethodEntry) { + return translate((MethodEntry) entry); + } else if (entry instanceof ConstructorEntry) { + return translate((ConstructorEntry) entry); + } else if (entry instanceof ArgumentEntry) { + return translate((ArgumentEntry) entry); + } else { + throw new Error("Unknown entry type: " + entry.getClass().getName()); + } + } + + public String translate(ClassEntry in) { + ClassEntry translated = translateEntry(in); + if (translated.equals(in)) { + return null; + } + return translated.getName(); + } + + public String translateClass(String className) { + return translate(new ClassEntry(className)); + } + + public ClassEntry translateEntry(ClassEntry in) { + + if (in.isInnerClass()) { + + // translate as much of the class chain as we can + List mappingsChain = getClassMappingChain(in); + String[] obfClassNames = in.getName().split("\\$"); + StringBuilder buf = new StringBuilder(); + for (int i = 0; i < obfClassNames.length; i++) { + boolean isFirstClass = buf.length() == 0; + String className = null; + ClassMapping classMapping = mappingsChain.get(i); + if (classMapping != null) { + className = m_direction.choose( + classMapping.getDeobfName(), + isFirstClass ? classMapping.getObfFullName() : classMapping.getObfSimpleName() + ); + } + if (className == null) { + className = obfClassNames[i]; + } + if (!isFirstClass) { + buf.append("$"); + } + buf.append(className); + } + return new ClassEntry(buf.toString()); + + } else { + + // normal classes are easy + ClassMapping classMapping = m_classes.get(in.getName()); + if (classMapping == null) { + return in; + } + return m_direction.choose( + classMapping.getDeobfName() != null ? new ClassEntry(classMapping.getDeobfName()) : in, + new ClassEntry(classMapping.getObfFullName()) + ); + } + } + + public String translate(FieldEntry in) { + + // resolve the class entry + ClassEntry resolvedClassEntry = m_index.resolveEntryClass(in); + if (resolvedClassEntry != null) { + + // look for the class + ClassMapping classMapping = findClassMapping(resolvedClassEntry); + if (classMapping != null) { + + // look for the field + String translatedName = m_direction.choose( + classMapping.getDeobfFieldName(in.getName(), in.getType()), + classMapping.getObfFieldName(in.getName(), translateType(in.getType())) + ); + if (translatedName != null) { + return translatedName; + } + } + } + return null; + } + + public FieldEntry translateEntry(FieldEntry in) { + String name = translate(in); + if (name == null) { + name = in.getName(); + } + return new FieldEntry(translateEntry(in.getClassEntry()), name, translateType(in.getType())); + } + + public String translate(MethodEntry in) { + + // resolve the class entry + ClassEntry resolvedClassEntry = m_index.resolveEntryClass(in); + if (resolvedClassEntry != null) { + + // look for class + ClassMapping classMapping = findClassMapping(resolvedClassEntry); + if (classMapping != null) { + + // look for the method + MethodMapping methodMapping = m_direction.choose( + classMapping.getMethodByObf(in.getName(), in.getSignature()), + classMapping.getMethodByDeobf(in.getName(), translateSignature(in.getSignature())) + ); + if (methodMapping != null) { + return m_direction.choose(methodMapping.getDeobfName(), methodMapping.getObfName()); + } + } + } + return null; + } + + public MethodEntry translateEntry(MethodEntry in) { + String name = translate(in); + if (name == null) { + name = in.getName(); + } + return new MethodEntry(translateEntry(in.getClassEntry()), name, translateSignature(in.getSignature())); + } + + public ConstructorEntry translateEntry(ConstructorEntry in) { + if (in.isStatic()) { + return new ConstructorEntry(translateEntry(in.getClassEntry())); + } else { + return new ConstructorEntry(translateEntry(in.getClassEntry()), translateSignature(in.getSignature())); + } + } + + public BehaviorEntry translateEntry(BehaviorEntry in) { + if (in instanceof MethodEntry) { + return translateEntry((MethodEntry) in); + } else if (in instanceof ConstructorEntry) { + return translateEntry((ConstructorEntry) in); + } + throw new Error("Wrong entry type!"); + } + + public String translate(ArgumentEntry in) { + + // look for the class + ClassMapping classMapping = findClassMapping(in.getClassEntry()); + if (classMapping != null) { + + // look for the method + MethodMapping methodMapping = m_direction.choose( + classMapping.getMethodByObf(in.getMethodName(), in.getMethodSignature()), + classMapping.getMethodByDeobf(in.getMethodName(), translateSignature(in.getMethodSignature())) + ); + if (methodMapping != null) { + return m_direction.choose( + methodMapping.getDeobfArgumentName(in.getIndex()), + methodMapping.getObfArgumentName(in.getIndex()) + ); + } + } + return null; + } + + public ArgumentEntry translateEntry(ArgumentEntry in) { + String name = translate(in); + if (name == null) { + name = in.getName(); + } + return new ArgumentEntry(translateEntry(in.getBehaviorEntry()), in.getIndex(), name); + } + + public Type translateType(Type type) { + return new Type(type, m_classNameReplacer); + } + + public Signature translateSignature(Signature signature) { + return new Signature(signature, m_classNameReplacer); + } + + private ClassMapping findClassMapping(ClassEntry in) { + List mappingChain = getClassMappingChain(in); + return mappingChain.get(mappingChain.size() - 1); + } + + private List getClassMappingChain(ClassEntry in) { + + // get a list of all the classes in the hierarchy + String[] parts = in.getName().split("\\$"); + List mappingsChain = Lists.newArrayList(); + + // get mappings for the outer class + ClassMapping outerClassMapping = m_classes.get(parts[0]); + mappingsChain.add(outerClassMapping); + + for (int i = 1; i < parts.length; i++) { + + // get mappings for the inner class + ClassMapping innerClassMapping = null; + if (outerClassMapping != null) { + innerClassMapping = m_direction.choose( + outerClassMapping.getInnerClassByObfSimple(parts[i]), + outerClassMapping.getInnerClassByDeobfThenObfSimple(parts[i]) + ); + } + mappingsChain.add(innerClassMapping); + outerClassMapping = innerClassMapping; + } + + assert (mappingsChain.size() == parts.length); + return mappingsChain; + } +} diff --git a/src/main/java/cuchaz/enigma/mapping/Type.java b/src/main/java/cuchaz/enigma/mapping/Type.java new file mode 100644 index 0000000..bfd836c --- /dev/null +++ b/src/main/java/cuchaz/enigma/mapping/Type.java @@ -0,0 +1,249 @@ +/******************************************************************************* + * 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.mapping; + +import com.google.common.collect.Maps; + +import java.io.Serializable; +import java.util.Map; + +public class Type implements Serializable { + + private static final long serialVersionUID = 7862257669347104063L; + + public enum Primitive { + Byte('B'), + Character('C'), + Short('S'), + Integer('I'), + Long('J'), + Float('F'), + Double('D'), + Boolean('Z'); + + private static final Map m_lookup; + + static { + m_lookup = Maps.newTreeMap(); + for (Primitive val : values()) { + m_lookup.put(val.getCode(), val); + } + } + + public static Primitive get(char code) { + return m_lookup.get(code); + } + + private char m_code; + + Primitive(char code) { + m_code = code; + } + + public char getCode() { + return m_code; + } + } + + public static String parseFirst(String in) { + + if (in == null || in.length() <= 0) { + throw new IllegalArgumentException("No type to parse, input is empty!"); + } + + // read one type from the input + + char c = in.charAt(0); + + // first check for void + if (c == 'V') { + return "V"; + } + + // then check for primitives + Primitive primitive = Primitive.get(c); + if (primitive != null) { + return in.substring(0, 1); + } + + // then check for classes + if (c == 'L') { + return readClass(in); + } + + // then check for templates + if (c == 'T') { + return readClass(in); + } + + // then check for arrays + int dim = countArrayDimension(in); + if (dim > 0) { + String arrayType = Type.parseFirst(in.substring(dim)); + return in.substring(0, dim + arrayType.length()); + } + + throw new IllegalArgumentException("don't know how to parse: " + in); + } + + protected String m_name; + + public Type(String name) { + + // don't deal with generics + // this is just for raw jvm types + if (name.charAt(0) == 'T' || name.indexOf('<') >= 0 || name.indexOf('>') >= 0) { + throw new IllegalArgumentException("don't use with generic types or templates: " + name); + } + + m_name = name; + } + + public Type(Type other) { + m_name = other.m_name; + } + + public Type(ClassEntry classEntry) { + m_name = "L" + classEntry.getClassName() + ";"; + } + + public Type(Type other, ClassNameReplacer replacer) { + m_name = other.m_name; + if (other.isClass()) { + String replacedName = replacer.replace(other.getClassEntry().getClassName()); + if (replacedName != null) { + m_name = "L" + replacedName + ";"; + } + } else if (other.isArray() && other.hasClass()) { + String replacedName = replacer.replace(other.getClassEntry().getClassName()); + if (replacedName != null) { + m_name = Type.getArrayPrefix(other.getArrayDimension()) + "L" + replacedName + ";"; + } + } + } + + @Override + public String toString() { + return m_name; + } + + public boolean isVoid() { + return m_name.length() == 1 && m_name.charAt(0) == 'V'; + } + + public boolean isPrimitive() { + return m_name.length() == 1 && Primitive.get(m_name.charAt(0)) != null; + } + + public Primitive getPrimitive() { + if (!isPrimitive()) { + throw new IllegalStateException("not a primitive"); + } + return Primitive.get(m_name.charAt(0)); + } + + public boolean isClass() { + return m_name.charAt(0) == 'L' && m_name.charAt(m_name.length() - 1) == ';'; + } + + public ClassEntry getClassEntry() { + if (isClass()) { + String name = m_name.substring(1, m_name.length() - 1); + + int pos = name.indexOf('<'); + if (pos >= 0) { + // remove the parameters from the class name + name = name.substring(0, pos); + } + + return new ClassEntry(name); + + } else if (isArray() && getArrayType().isClass()) { + return getArrayType().getClassEntry(); + } else { + throw new IllegalStateException("type doesn't have a class"); + } + } + + public boolean isArray() { + return m_name.charAt(0) == '['; + } + + public int getArrayDimension() { + if (!isArray()) { + throw new IllegalStateException("not an array"); + } + return countArrayDimension(m_name); + } + + public Type getArrayType() { + if (!isArray()) { + throw new IllegalStateException("not an array"); + } + return new Type(m_name.substring(getArrayDimension(), m_name.length())); + } + + private static String getArrayPrefix(int dimension) { + StringBuilder buf = new StringBuilder(); + for (int i = 0; i < dimension; i++) { + buf.append("["); + } + return buf.toString(); + } + + public boolean hasClass() { + return isClass() || (isArray() && getArrayType().hasClass()); + } + + @Override + public boolean equals(Object other) { + if (other instanceof Type) { + return equals((Type) other); + } + return false; + } + + public boolean equals(Type other) { + return m_name.equals(other.m_name); + } + + public int hashCode() { + return m_name.hashCode(); + } + + private static int countArrayDimension(String in) { + int i = 0; + for (; i < in.length() && in.charAt(i) == '['; i++) { + ; + } + return i; + } + + private static String readClass(String in) { + // read all the characters in the buffer until we hit a ';' + // include the parameters too + StringBuilder buf = new StringBuilder(); + int depth = 0; + for (int i = 0; i < in.length(); i++) { + char c = in.charAt(i); + buf.append(c); + + if (c == '<') { + depth++; + } else if (c == '>') { + depth--; + } else if (depth == 0 && c == ';') { + return buf.toString(); + } + } + return null; + } +} -- cgit v1.2.3