From 00fcd0550fcdda621c2e4662f6ddd55ce673b931 Mon Sep 17 00:00:00 2001 From: Gegy Date: Thu, 24 Jan 2019 14:48:32 +0200 Subject: [WIP] Mapping rework (#91) * Move packages * Mapping & entry refactor: first pass * Fix deobf -> obf tree remapping * Resolve various issues * Give all entries the potential for parents and treat inner classes as children * Deobf UI tree elements * Tests pass * Sort mapping output * Fix delta tracking * Index separation and first pass for #97 * Keep track of remapped jar index * Fix child entries not being remapped * Drop non-root entries * Track dropped mappings * Fix enigma mapping ordering * EntryTreeNode interface * Small tweaks * Naive full index remap on rename * Entries can resolve to more than one root entry * Support alternative resolution strategies * Bridge method resolution * Tests pass * Fix mappings being used where there are none * Fix methods with different descriptors being considered unique. closes #89 --- .../enigma/translation/MappingTranslator.java | 24 ++ .../enigma/translation/SignatureUpdater.java | 92 +++++++ .../cuchaz/enigma/translation/Translatable.java | 9 + .../enigma/translation/TranslationDirection.java | 36 +++ .../java/cuchaz/enigma/translation/Translator.java | 54 +++++ .../cuchaz/enigma/translation/VoidTranslator.java | 10 + .../enigma/translation/mapping/AccessModifier.java | 25 ++ .../enigma/translation/mapping/EntryMap.java | 24 ++ .../enigma/translation/mapping/EntryMapping.java | 30 +++ .../enigma/translation/mapping/EntryRemapper.java | 201 ++++++++++++++++ .../enigma/translation/mapping/EntryResolver.java | 41 ++++ .../translation/mapping/IndexEntryResolver.java | 225 +++++++++++++++++ .../enigma/translation/mapping/MappingDelta.java | 56 +++++ .../enigma/translation/mapping/MappingPair.java | 28 +++ .../translation/mapping/MappingValidator.java | 45 ++++ .../translation/mapping/MappingsChecker.java | 91 +++++++ .../enigma/translation/mapping/NameValidator.java | 56 +++++ .../translation/mapping/ResolutionStrategy.java | 6 + .../translation/mapping/VoidEntryResolver.java | 27 +++ .../mapping/serde/EnigmaMappingsReader.java | 260 ++++++++++++++++++++ .../mapping/serde/EnigmaMappingsWriter.java | 260 ++++++++++++++++++++ .../translation/mapping/serde/MappingFormat.java | 54 +++++ .../translation/mapping/serde/MappingsReader.java | 12 + .../translation/mapping/serde/MappingsWriter.java | 12 + .../mapping/serde/SrgMappingsWriter.java | 115 +++++++++ .../mapping/serde/TinyMappingsReader.java | 100 ++++++++ .../mapping/tree/DeltaTrackingTree.java | 113 +++++++++ .../enigma/translation/mapping/tree/EntryTree.java | 20 ++ .../translation/mapping/tree/EntryTreeNode.java | 36 +++ .../translation/mapping/tree/HashEntryTree.java | 159 ++++++++++++ .../translation/mapping/tree/HashTreeNode.java | 72 ++++++ .../translation/representation/AccessFlags.java | 112 +++++++++ .../representation/MethodDescriptor.java | 132 ++++++++++ .../representation/ProcyonEntryFactory.java | 45 ++++ .../representation/ReferencedEntryPool.java | 60 +++++ .../translation/representation/Signature.java | 93 +++++++ .../translation/representation/TypeDescriptor.java | 268 +++++++++++++++++++++ .../representation/entry/ClassDefEntry.java | 92 +++++++ .../representation/entry/ClassEntry.java | 180 ++++++++++++++ .../translation/representation/entry/DefEntry.java | 7 + .../translation/representation/entry/Entry.java | 99 ++++++++ .../representation/entry/FieldDefEntry.java | 61 +++++ .../representation/entry/FieldEntry.java | 86 +++++++ .../entry/LocalVariableDefEntry.java | 45 ++++ .../representation/entry/LocalVariableEntry.java | 92 +++++++ .../representation/entry/MethodDefEntry.java | 77 ++++++ .../representation/entry/MethodEntry.java | 95 ++++++++ .../representation/entry/ParentedEntry.java | 71 ++++++ 48 files changed, 3908 insertions(+) create mode 100644 src/main/java/cuchaz/enigma/translation/MappingTranslator.java create mode 100644 src/main/java/cuchaz/enigma/translation/SignatureUpdater.java create mode 100644 src/main/java/cuchaz/enigma/translation/Translatable.java create mode 100644 src/main/java/cuchaz/enigma/translation/TranslationDirection.java create mode 100644 src/main/java/cuchaz/enigma/translation/Translator.java create mode 100644 src/main/java/cuchaz/enigma/translation/VoidTranslator.java create mode 100644 src/main/java/cuchaz/enigma/translation/mapping/AccessModifier.java create mode 100644 src/main/java/cuchaz/enigma/translation/mapping/EntryMap.java create mode 100644 src/main/java/cuchaz/enigma/translation/mapping/EntryMapping.java create mode 100644 src/main/java/cuchaz/enigma/translation/mapping/EntryRemapper.java create mode 100644 src/main/java/cuchaz/enigma/translation/mapping/EntryResolver.java create mode 100644 src/main/java/cuchaz/enigma/translation/mapping/IndexEntryResolver.java create mode 100644 src/main/java/cuchaz/enigma/translation/mapping/MappingDelta.java create mode 100644 src/main/java/cuchaz/enigma/translation/mapping/MappingPair.java create mode 100644 src/main/java/cuchaz/enigma/translation/mapping/MappingValidator.java create mode 100644 src/main/java/cuchaz/enigma/translation/mapping/MappingsChecker.java create mode 100644 src/main/java/cuchaz/enigma/translation/mapping/NameValidator.java create mode 100644 src/main/java/cuchaz/enigma/translation/mapping/ResolutionStrategy.java create mode 100644 src/main/java/cuchaz/enigma/translation/mapping/VoidEntryResolver.java create mode 100644 src/main/java/cuchaz/enigma/translation/mapping/serde/EnigmaMappingsReader.java create mode 100644 src/main/java/cuchaz/enigma/translation/mapping/serde/EnigmaMappingsWriter.java create mode 100644 src/main/java/cuchaz/enigma/translation/mapping/serde/MappingFormat.java create mode 100644 src/main/java/cuchaz/enigma/translation/mapping/serde/MappingsReader.java create mode 100644 src/main/java/cuchaz/enigma/translation/mapping/serde/MappingsWriter.java create mode 100644 src/main/java/cuchaz/enigma/translation/mapping/serde/SrgMappingsWriter.java create mode 100644 src/main/java/cuchaz/enigma/translation/mapping/serde/TinyMappingsReader.java create mode 100644 src/main/java/cuchaz/enigma/translation/mapping/tree/DeltaTrackingTree.java create mode 100644 src/main/java/cuchaz/enigma/translation/mapping/tree/EntryTree.java create mode 100644 src/main/java/cuchaz/enigma/translation/mapping/tree/EntryTreeNode.java create mode 100644 src/main/java/cuchaz/enigma/translation/mapping/tree/HashEntryTree.java create mode 100644 src/main/java/cuchaz/enigma/translation/mapping/tree/HashTreeNode.java create mode 100644 src/main/java/cuchaz/enigma/translation/representation/AccessFlags.java create mode 100644 src/main/java/cuchaz/enigma/translation/representation/MethodDescriptor.java create mode 100644 src/main/java/cuchaz/enigma/translation/representation/ProcyonEntryFactory.java create mode 100644 src/main/java/cuchaz/enigma/translation/representation/ReferencedEntryPool.java create mode 100644 src/main/java/cuchaz/enigma/translation/representation/Signature.java create mode 100644 src/main/java/cuchaz/enigma/translation/representation/TypeDescriptor.java create mode 100644 src/main/java/cuchaz/enigma/translation/representation/entry/ClassDefEntry.java create mode 100644 src/main/java/cuchaz/enigma/translation/representation/entry/ClassEntry.java create mode 100644 src/main/java/cuchaz/enigma/translation/representation/entry/DefEntry.java create mode 100644 src/main/java/cuchaz/enigma/translation/representation/entry/Entry.java create mode 100644 src/main/java/cuchaz/enigma/translation/representation/entry/FieldDefEntry.java create mode 100644 src/main/java/cuchaz/enigma/translation/representation/entry/FieldEntry.java create mode 100644 src/main/java/cuchaz/enigma/translation/representation/entry/LocalVariableDefEntry.java create mode 100644 src/main/java/cuchaz/enigma/translation/representation/entry/LocalVariableEntry.java create mode 100644 src/main/java/cuchaz/enigma/translation/representation/entry/MethodDefEntry.java create mode 100644 src/main/java/cuchaz/enigma/translation/representation/entry/MethodEntry.java create mode 100644 src/main/java/cuchaz/enigma/translation/representation/entry/ParentedEntry.java (limited to 'src/main/java/cuchaz/enigma/translation') diff --git a/src/main/java/cuchaz/enigma/translation/MappingTranslator.java b/src/main/java/cuchaz/enigma/translation/MappingTranslator.java new file mode 100644 index 0000000..529d0ed --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/MappingTranslator.java @@ -0,0 +1,24 @@ +package cuchaz.enigma.translation; + +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.EntryResolver; +import cuchaz.enigma.translation.mapping.EntryMap; + +public class MappingTranslator implements Translator { + private final EntryMap mappings; + private final EntryResolver resolver; + + public MappingTranslator(EntryMap mappings, EntryResolver resolver) { + this.mappings = mappings; + this.resolver = resolver; + } + + @SuppressWarnings("unchecked") + @Override + public T translate(T translatable) { + if (translatable == null) { + return null; + } + return (T) translatable.translate(this, resolver, mappings); + } +} diff --git a/src/main/java/cuchaz/enigma/translation/SignatureUpdater.java b/src/main/java/cuchaz/enigma/translation/SignatureUpdater.java new file mode 100644 index 0000000..3783053 --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/SignatureUpdater.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.translation; + +import com.google.common.collect.Lists; + +import java.io.IOException; +import java.io.StringReader; +import java.util.List; + +public class SignatureUpdater { + + 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; + 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; + 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, className -> { + classNames.add(className); + return className; + }); + return classNames; + } + + public interface ClassNameUpdater { + String update(String className); + } +} diff --git a/src/main/java/cuchaz/enigma/translation/Translatable.java b/src/main/java/cuchaz/enigma/translation/Translatable.java new file mode 100644 index 0000000..0370ef1 --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/Translatable.java @@ -0,0 +1,9 @@ +package cuchaz.enigma.translation; + +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.EntryResolver; +import cuchaz.enigma.translation.mapping.EntryMap; + +public interface Translatable { + Translatable translate(Translator translator, EntryResolver resolver, EntryMap mappings); +} diff --git a/src/main/java/cuchaz/enigma/translation/TranslationDirection.java b/src/main/java/cuchaz/enigma/translation/TranslationDirection.java new file mode 100644 index 0000000..2ecb30b --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/TranslationDirection.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * 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.translation; + +public enum TranslationDirection { + + DEOBFUSCATING { + @Override + public T choose(T deobfChoice, T obfChoice) { + if (deobfChoice == null) { + return obfChoice; + } + return deobfChoice; + } + }, + OBFUSCATING { + @Override + public T choose(T deobfChoice, T obfChoice) { + if (obfChoice == null) { + return deobfChoice; + } + return obfChoice; + } + }; + + public abstract T choose(T deobfChoice, T obfChoice); +} diff --git a/src/main/java/cuchaz/enigma/translation/Translator.java b/src/main/java/cuchaz/enigma/translation/Translator.java new file mode 100644 index 0000000..de2003e --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/Translator.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.translation; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +public interface Translator { + T translate(T translatable); + + default Collection translate(Collection translatable) { + return translatable.stream() + .map(this::translate) + .collect(Collectors.toList()); + } + + default Map translateKeys(Map translatable) { + Map result = new HashMap<>(translatable.size()); + for (Map.Entry entry : translatable.entrySet()) { + result.put(translate(entry.getKey()), entry.getValue()); + } + return result; + } + + default Map translate(Map translatable) { + Map result = new HashMap<>(translatable.size()); + for (Map.Entry entry : translatable.entrySet()) { + result.put(translate(entry.getKey()), translate(entry.getValue())); + } + return result; + } + + default Multimap translate(Multimap translatable) { + Multimap result = HashMultimap.create(translatable.size(), 1); + for (Map.Entry> entry : translatable.asMap().entrySet()) { + result.putAll(translate(entry.getKey()), translate(entry.getValue())); + } + return result; + } +} diff --git a/src/main/java/cuchaz/enigma/translation/VoidTranslator.java b/src/main/java/cuchaz/enigma/translation/VoidTranslator.java new file mode 100644 index 0000000..c010833 --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/VoidTranslator.java @@ -0,0 +1,10 @@ +package cuchaz.enigma.translation; + +public enum VoidTranslator implements Translator { + INSTANCE; + + @Override + public T translate(T translatable) { + return translatable; + } +} diff --git a/src/main/java/cuchaz/enigma/translation/mapping/AccessModifier.java b/src/main/java/cuchaz/enigma/translation/mapping/AccessModifier.java new file mode 100644 index 0000000..5b79b79 --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/mapping/AccessModifier.java @@ -0,0 +1,25 @@ +package cuchaz.enigma.translation.mapping; + +import cuchaz.enigma.translation.representation.AccessFlags; + +public enum AccessModifier { + UNCHANGED, PUBLIC, PROTECTED, PRIVATE; + + public String getFormattedName() { + return "ACC:" + super.toString(); + } + + public AccessFlags transform(AccessFlags access) { + switch (this) { + case PUBLIC: + return access.setPublic(); + case PROTECTED: + return access.setProtected(); + case PRIVATE: + return access.setPrivate(); + case UNCHANGED: + default: + return access; + } + } +} diff --git a/src/main/java/cuchaz/enigma/translation/mapping/EntryMap.java b/src/main/java/cuchaz/enigma/translation/mapping/EntryMap.java new file mode 100644 index 0000000..6af4846 --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/mapping/EntryMap.java @@ -0,0 +1,24 @@ +package cuchaz.enigma.translation.mapping; + +import cuchaz.enigma.translation.representation.entry.Entry; + +import javax.annotation.Nullable; +import java.util.Collection; + +public interface EntryMap { + void insert(Entry entry, T value); + + @Nullable + T remove(Entry entry); + + @Nullable + T get(Entry entry); + + default boolean contains(Entry entry) { + return get(entry) != null; + } + + Collection> getAllEntries(); + + boolean isEmpty(); +} diff --git a/src/main/java/cuchaz/enigma/translation/mapping/EntryMapping.java b/src/main/java/cuchaz/enigma/translation/mapping/EntryMapping.java new file mode 100644 index 0000000..f11cdef --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/mapping/EntryMapping.java @@ -0,0 +1,30 @@ +package cuchaz.enigma.translation.mapping; + +import javax.annotation.Nonnull; + +public class EntryMapping { + private final String targetName; + private final AccessModifier accessModifier; + + public EntryMapping(@Nonnull String targetName) { + this(targetName, AccessModifier.UNCHANGED); + } + + public EntryMapping(@Nonnull String targetName, AccessModifier accessModifier) { + this.targetName = targetName; + this.accessModifier = accessModifier; + } + + @Nonnull + public String getTargetName() { + return targetName; + } + + @Nonnull + public AccessModifier getAccessModifier() { + if (accessModifier == null) { + return AccessModifier.UNCHANGED; + } + return accessModifier; + } +} diff --git a/src/main/java/cuchaz/enigma/translation/mapping/EntryRemapper.java b/src/main/java/cuchaz/enigma/translation/mapping/EntryRemapper.java new file mode 100644 index 0000000..b7d8d17 --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/mapping/EntryRemapper.java @@ -0,0 +1,201 @@ +package cuchaz.enigma.translation.mapping; + +import cuchaz.enigma.analysis.index.JarIndex; +import cuchaz.enigma.translation.MappingTranslator; +import cuchaz.enigma.translation.Translatable; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.tree.DeltaTrackingTree; +import cuchaz.enigma.translation.mapping.tree.EntryTree; +import cuchaz.enigma.translation.mapping.tree.EntryTreeNode; +import cuchaz.enigma.translation.mapping.tree.HashEntryTree; +import cuchaz.enigma.translation.representation.entry.Entry; + +import javax.annotation.Nullable; +import java.util.Collection; + +public class EntryRemapper { + private final EntryTree obfToDeobf; + private final DeltaTrackingTree deobfToObf; + + private final JarIndex obfIndex; + + private final EntryResolver obfResolver; + private EntryResolver deobfResolver; + + private final Translator deobfuscator; + private Translator obfuscator; + + private final MappingValidator validator; + + private EntryRemapper(JarIndex jarIndex, EntryTree obfToDeobf, EntryTree deobfToObf) { + this.obfToDeobf = obfToDeobf; + this.deobfToObf = new DeltaTrackingTree<>(deobfToObf); + + this.obfIndex = jarIndex; + this.obfResolver = jarIndex.getEntryResolver(); + + this.deobfuscator = new MappingTranslator(obfToDeobf, obfResolver); + rebuildDeobfIndex(); + + this.validator = new MappingValidator(this.deobfToObf, deobfuscator, obfResolver); + } + + public EntryRemapper(JarIndex jarIndex) { + this(jarIndex, new HashEntryTree<>(), new HashEntryTree<>()); + } + + public EntryRemapper(JarIndex jarIndex, EntryTree deobfuscationTrees) { + this(jarIndex, deobfuscationTrees, inverse(deobfuscationTrees)); + } + + private static EntryTree inverse(EntryTree tree) { + Translator translator = new MappingTranslator(tree, VoidEntryResolver.INSTANCE); + EntryTree inverse = new HashEntryTree<>(); + + // Naive approach, could operate on the nodes of the tree. However, this runs infrequently. + Collection> entries = tree.getAllEntries(); + for (Entry sourceEntry : entries) { + Entry targetEntry = translator.translate(sourceEntry); + inverse.insert(targetEntry, new EntryMapping(sourceEntry.getName())); + } + + return inverse; + } + + private void rebuildDeobfIndex() { + JarIndex deobfIndex = obfIndex.remapped(deobfuscator); + + this.deobfResolver = deobfIndex.getEntryResolver(); + this.obfuscator = new MappingTranslator(deobfToObf, deobfResolver); + } + + public > void mapFromObf(E obfuscatedEntry, @Nullable EntryMapping deobfMapping) { + Collection resolvedEntries = obfResolver.resolveEntry(obfuscatedEntry, ResolutionStrategy.RESOLVE_ROOT); + for (E resolvedEntry : resolvedEntries) { + if (deobfMapping != null) { + validator.validateRename(resolvedEntry, deobfMapping.getTargetName()); + } + + setObfToDeobf(resolvedEntry, deobfMapping); + } + + // Temporary hack, not very performant + rebuildDeobfIndex(); + } + + public > void mapFromDeobf(E deobfuscatedEntry, @Nullable EntryMapping deobfMapping) { + E obfuscatedEntry = obfuscate(deobfuscatedEntry); + mapFromObf(obfuscatedEntry, deobfMapping); + } + + public void removeByObf(Entry obfuscatedEntry) { + mapFromObf(obfuscatedEntry, null); + } + + public void removeByDeobf(Entry deobfuscatedEntry) { + mapFromObf(obfuscate(deobfuscatedEntry), null); + } + + private > void setObfToDeobf(E obfuscatedEntry, @Nullable EntryMapping deobfMapping) { + E prevDeobf = deobfuscate(obfuscatedEntry); + obfToDeobf.insert(obfuscatedEntry, deobfMapping); + + E newDeobf = deobfuscate(obfuscatedEntry); + + // Reconstruct the children of this node in the deobf -> obf tree with our new mapping + // We only need to do this for deobf -> obf because the obf tree is always consistent on the left hand side + // We lookup by obf, and the obf never changes. This is not the case for deobf so we need to update the tree. + + EntryTreeNode node = deobfToObf.findNode(prevDeobf); + if (node != null) { + for (EntryTreeNode child : node.getNodesRecursively()) { + Entry entry = child.getEntry(); + EntryMapping mapping = new EntryMapping(obfuscate(entry).getName()); + + deobfToObf.insert(entry.replaceAncestor(prevDeobf, newDeobf), mapping); + deobfToObf.remove(entry); + } + } else { + deobfToObf.insert(newDeobf, new EntryMapping(obfuscatedEntry.getName())); + } + } + + @Nullable + public EntryMapping getDeobfMapping(Entry entry) { + return obfToDeobf.get(entry); + } + + @Nullable + public EntryMapping getObfMapping(Entry entry) { + return deobfToObf.get(entry); + } + + public boolean hasDeobfMapping(Entry obfEntry) { + return obfToDeobf.contains(obfEntry); + } + + public boolean hasObfMapping(Entry deobfEntry) { + return deobfToObf.contains(deobfEntry); + } + + public T deobfuscate(T translatable) { + return deobfuscator.translate(translatable); + } + + public T obfuscate(T translatable) { + return obfuscator.translate(translatable); + } + + public Translator getDeobfuscator() { + return deobfuscator; + } + + public Translator getObfuscator() { + return obfuscator; + } + + public Collection> getObfEntries() { + return obfToDeobf.getAllEntries(); + } + + public Collection> getObfRootEntries() { + return obfToDeobf.getRootEntries(); + } + + public Collection> getDeobfEntries() { + return deobfToObf.getAllEntries(); + } + + public Collection> getObfChildren(Entry obfuscatedEntry) { + return obfToDeobf.getChildren(obfuscatedEntry); + } + + public Collection> getDeobfChildren(Entry deobfuscatedEntry) { + return deobfToObf.getChildren(deobfuscatedEntry); + } + + public EntryTree getObfToDeobf() { + return obfToDeobf; + } + + public DeltaTrackingTree getDeobfToObf() { + return deobfToObf; + } + + public MappingDelta takeMappingDelta() { + MappingDelta delta = deobfToObf.takeDelta(); + return delta.translate(obfuscator, VoidEntryResolver.INSTANCE, deobfToObf); + } + + public boolean isDirty() { + return deobfToObf.isDirty(); + } + + public EntryResolver getObfResolver() { + return obfResolver; + } + + public EntryResolver getDeobfResolver() { + return deobfResolver; + } +} diff --git a/src/main/java/cuchaz/enigma/translation/mapping/EntryResolver.java b/src/main/java/cuchaz/enigma/translation/mapping/EntryResolver.java new file mode 100644 index 0000000..521f72d --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/mapping/EntryResolver.java @@ -0,0 +1,41 @@ +package cuchaz.enigma.translation.mapping; + +import com.google.common.collect.Streams; +import cuchaz.enigma.analysis.EntryReference; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; + +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; + +public interface EntryResolver { + > Collection resolveEntry(E entry, ResolutionStrategy strategy); + + default > E resolveFirstEntry(E entry, ResolutionStrategy strategy) { + return resolveEntry(entry, strategy).stream().findFirst().orElse(entry); + } + + default , C extends Entry> Collection> resolveReference(EntryReference reference, ResolutionStrategy strategy) { + Collection entry = resolveEntry(reference.entry, strategy); + if (reference.context != null) { + Collection context = resolveEntry(reference.context, strategy); + return Streams.zip(entry.stream(), context.stream(), (e, c) -> new EntryReference<>(e, c, reference)) + .collect(Collectors.toList()); + } else { + return entry.stream() + .map(e -> new EntryReference<>(e, null, reference)) + .collect(Collectors.toList()); + } + } + + default , C extends Entry> EntryReference resolveFirstReference(EntryReference reference, ResolutionStrategy strategy) { + E entry = resolveFirstEntry(reference.entry, strategy); + C context = resolveFirstEntry(reference.context, strategy); + return new EntryReference<>(entry, context, reference); + } + + Set> resolveEquivalentEntries(Entry entry); + + Set resolveEquivalentMethods(MethodEntry methodEntry); +} diff --git a/src/main/java/cuchaz/enigma/translation/mapping/IndexEntryResolver.java b/src/main/java/cuchaz/enigma/translation/mapping/IndexEntryResolver.java new file mode 100644 index 0000000..1f2290a --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/mapping/IndexEntryResolver.java @@ -0,0 +1,225 @@ +package cuchaz.enigma.translation.mapping; + +import com.google.common.collect.Sets; +import cuchaz.enigma.analysis.IndexTreeBuilder; +import cuchaz.enigma.analysis.MethodImplementationsTreeNode; +import cuchaz.enigma.analysis.MethodInheritanceTreeNode; +import cuchaz.enigma.analysis.index.BridgeMethodIndex; +import cuchaz.enigma.analysis.index.EntryIndex; +import cuchaz.enigma.analysis.index.InheritanceIndex; +import cuchaz.enigma.analysis.index.JarIndex; +import cuchaz.enigma.translation.VoidTranslator; +import cuchaz.enigma.translation.representation.AccessFlags; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; + +import javax.annotation.Nullable; +import java.util.*; +import java.util.stream.Collectors; + +public class IndexEntryResolver implements EntryResolver { + private final EntryIndex entryIndex; + private final InheritanceIndex inheritanceIndex; + private final BridgeMethodIndex bridgeMethodIndex; + + private final IndexTreeBuilder treeBuilder; + + public IndexEntryResolver(JarIndex index) { + this.entryIndex = index.getEntryIndex(); + this.inheritanceIndex = index.getInheritanceIndex(); + this.bridgeMethodIndex = index.getBridgeMethodIndex(); + + this.treeBuilder = new IndexTreeBuilder(index); + } + + @Override + @SuppressWarnings("unchecked") + public > Collection resolveEntry(E entry, ResolutionStrategy strategy) { + if (entry == null) { + return Collections.emptySet(); + } + + Entry classChild = getClassChild(entry); + if (classChild != null && !(classChild instanceof ClassEntry)) { + AccessFlags access = entryIndex.getEntryAccess(classChild); + + // If we're looking for the closest and this entry exists, we're done looking + if (strategy == ResolutionStrategy.RESOLVE_CLOSEST && access != null) { + return Collections.singleton(entry); + } + + if (access == null || !access.isPrivate()) { + Collection> resolvedChildren = resolveChildEntry(classChild, strategy); + if (!resolvedChildren.isEmpty()) { + return resolvedChildren.stream() + .map(resolvedChild -> (E) entry.replaceAncestor(classChild, resolvedChild)) + .collect(Collectors.toList()); + } + } + } + + return Collections.singleton(entry); + } + + @Nullable + private Entry getClassChild(Entry entry) { + if (entry instanceof ClassEntry) { + return null; + } + + // get the entry in the hierarchy that is the child of a class + List> ancestry = entry.getAncestry(); + for (int i = ancestry.size() - 1; i > 0; i--) { + Entry child = ancestry.get(i); + Entry cast = child.castParent(ClassEntry.class); + if (cast != null && !(cast instanceof ClassEntry)) { + // we found the entry which is a child of a class, we are now able to resolve the owner of this entry + return cast; + } + } + + return null; + } + + private Set> resolveChildEntry(Entry entry, ResolutionStrategy strategy) { + ClassEntry ownerClass = entry.getParent(); + + if (entry instanceof MethodEntry) { + MethodEntry bridgeMethod = bridgeMethodIndex.getBridgeFromAccessed((MethodEntry) entry); + if (bridgeMethod != null && ownerClass.equals(bridgeMethod.getParent())) { + Set> resolvedBridge = resolveChildEntry(bridgeMethod, strategy); + if (!resolvedBridge.isEmpty()) { + return resolvedBridge; + } + } + } + + Set> resolvedEntries = new HashSet<>(); + + for (ClassEntry parentClass : inheritanceIndex.getParents(ownerClass)) { + Entry parentEntry = entry.withParent(parentClass); + + if (strategy == ResolutionStrategy.RESOLVE_ROOT) { + resolvedEntries.addAll(resolveRoot(parentEntry, strategy)); + } else { + resolvedEntries.addAll(resolveClosest(parentEntry, strategy)); + } + } + + return resolvedEntries; + } + + private Collection> resolveRoot(Entry entry, ResolutionStrategy strategy) { + // When resolving root, we want to first look for the lowest entry before returning ourselves + Set> parentResolution = resolveChildEntry(entry, strategy); + + if (parentResolution.isEmpty()) { + AccessFlags parentAccess = entryIndex.getEntryAccess(entry); + if (parentAccess != null && !parentAccess.isPrivate()) { + return Collections.singleton(entry); + } + } + + return parentResolution; + } + + private Collection> resolveClosest(Entry entry, ResolutionStrategy strategy) { + // When resolving closest, we want to first check if we exist before looking further down + AccessFlags parentAccess = entryIndex.getEntryAccess(entry); + if (parentAccess != null && !parentAccess.isPrivate()) { + return Collections.singleton(entry); + } else { + return resolveChildEntry(entry, strategy); + } + } + + @Override + public Set> resolveEquivalentEntries(Entry entry) { + MethodEntry relevantMethod = entry.findAncestor(MethodEntry.class); + if (relevantMethod == null || !entryIndex.hasMethod(relevantMethod)) { + return Collections.singleton(entry); + } + + Set equivalentMethods = resolveEquivalentMethods(relevantMethod); + Set> equivalentEntries = new HashSet<>(equivalentMethods.size()); + + for (MethodEntry equivalentMethod : equivalentMethods) { + Entry equivalentEntry = entry.replaceAncestor(relevantMethod, equivalentMethod); + equivalentEntries.add(equivalentEntry); + } + + return equivalentEntries; + } + + @Override + public Set resolveEquivalentMethods(MethodEntry methodEntry) { + AccessFlags access = entryIndex.getMethodAccess(methodEntry); + if (access == null) { + throw new IllegalArgumentException("Could not find method " + methodEntry); + } + + if (!canInherit(methodEntry, access)) { + return Collections.singleton(methodEntry); + } + + Set methodEntries = Sets.newHashSet(); + resolveEquivalentMethods(methodEntries, treeBuilder.buildMethodInheritance(VoidTranslator.INSTANCE, methodEntry)); + return methodEntries; + } + + private void resolveEquivalentMethods(Set methodEntries, MethodInheritanceTreeNode node) { + MethodEntry methodEntry = node.getMethodEntry(); + if (methodEntries.contains(methodEntry)) { + return; + } + + AccessFlags flags = entryIndex.getMethodAccess(methodEntry); + if (flags != null && canInherit(methodEntry, flags)) { + // collect the entry + methodEntries.add(methodEntry); + } + + // look at bridge methods! + MethodEntry bridgedMethod = bridgeMethodIndex.getBridgeFromAccessed(methodEntry); + while (bridgedMethod != null) { + methodEntries.addAll(resolveEquivalentMethods(bridgedMethod)); + bridgedMethod = bridgeMethodIndex.getBridgeFromAccessed(bridgedMethod); + } + + // look at interface methods too + for (MethodImplementationsTreeNode implementationsNode : treeBuilder.buildMethodImplementations(VoidTranslator.INSTANCE, methodEntry)) { + resolveEquivalentMethods(methodEntries, implementationsNode); + } + + // recurse + for (int i = 0; i < node.getChildCount(); i++) { + resolveEquivalentMethods(methodEntries, (MethodInheritanceTreeNode) node.getChildAt(i)); + } + } + + private void resolveEquivalentMethods(Set methodEntries, MethodImplementationsTreeNode node) { + MethodEntry methodEntry = node.getMethodEntry(); + AccessFlags flags = entryIndex.getMethodAccess(methodEntry); + if (flags != null && !flags.isPrivate() && !flags.isStatic()) { + // collect the entry + methodEntries.add(methodEntry); + } + + // look at bridge methods! + MethodEntry bridgedMethod = bridgeMethodIndex.getBridgeFromAccessed(methodEntry); + while (bridgedMethod != null) { + methodEntries.addAll(resolveEquivalentMethods(bridgedMethod)); + bridgedMethod = bridgeMethodIndex.getBridgeFromAccessed(bridgedMethod); + } + + // recurse + for (int i = 0; i < node.getChildCount(); i++) { + resolveEquivalentMethods(methodEntries, (MethodImplementationsTreeNode) node.getChildAt(i)); + } + } + + private boolean canInherit(MethodEntry entry, AccessFlags access) { + return !entry.isConstructor() && !access.isPrivate() && !access.isStatic() && !access.isFinal(); + } +} diff --git a/src/main/java/cuchaz/enigma/translation/mapping/MappingDelta.java b/src/main/java/cuchaz/enigma/translation/mapping/MappingDelta.java new file mode 100644 index 0000000..4fba49d --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/mapping/MappingDelta.java @@ -0,0 +1,56 @@ +package cuchaz.enigma.translation.mapping; + +import cuchaz.enigma.translation.Translatable; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.tree.HashEntryTree; +import cuchaz.enigma.translation.mapping.tree.EntryTree; +import cuchaz.enigma.translation.representation.entry.Entry; + +public class MappingDelta implements Translatable { + public static final Object PLACEHOLDER = new Object(); + + private final EntryTree additions; + private final EntryTree deletions; + + public MappingDelta(EntryTree additions, EntryTree deletions) { + this.additions = additions; + this.deletions = deletions; + } + + public MappingDelta() { + this(new HashEntryTree<>(), new HashEntryTree<>()); + } + + public static MappingDelta added(EntryTree mappings) { + EntryTree additions = new HashEntryTree<>(); + for (Entry entry : mappings.getAllEntries()) { + additions.insert(entry, PLACEHOLDER); + } + + return new MappingDelta(additions, new HashEntryTree<>()); + } + + public EntryTree getAdditions() { + return additions; + } + + public EntryTree getDeletions() { + return deletions; + } + + @Override + public MappingDelta translate(Translator translator, EntryResolver resolver, EntryMap mappings) { + return new MappingDelta( + translate(translator, additions), + translate(translator, deletions) + ); + } + + private EntryTree translate(Translator translator, EntryTree tree) { + EntryTree translatedTree = new HashEntryTree<>(); + for (Entry entry : tree.getAllEntries()) { + translatedTree.insert(translator.translate(entry), PLACEHOLDER); + } + return translatedTree; + } +} diff --git a/src/main/java/cuchaz/enigma/translation/mapping/MappingPair.java b/src/main/java/cuchaz/enigma/translation/mapping/MappingPair.java new file mode 100644 index 0000000..9ed7e8a --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/mapping/MappingPair.java @@ -0,0 +1,28 @@ +package cuchaz.enigma.translation.mapping; + +import cuchaz.enigma.translation.representation.entry.Entry; + +import javax.annotation.Nullable; + +public class MappingPair, M> { + private final E entry; + private final M mapping; + + public MappingPair(E entry, @Nullable M mapping) { + this.entry = entry; + this.mapping = mapping; + } + + public MappingPair(E entry) { + this(entry, null); + } + + public E getEntry() { + return entry; + } + + @Nullable + public M getMapping() { + return mapping; + } +} diff --git a/src/main/java/cuchaz/enigma/translation/mapping/MappingValidator.java b/src/main/java/cuchaz/enigma/translation/mapping/MappingValidator.java new file mode 100644 index 0000000..422bf38 --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/mapping/MappingValidator.java @@ -0,0 +1,45 @@ +package cuchaz.enigma.translation.mapping; + +import cuchaz.enigma.throwables.IllegalNameException; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.tree.EntryTree; +import cuchaz.enigma.translation.representation.entry.Entry; + +import java.util.Collection; + +public class MappingValidator { + private final EntryTree deobfToObf; + private final Translator deobfuscator; + private final EntryResolver entryResolver; + + public MappingValidator(EntryTree deobfToObf, Translator deobfuscator, EntryResolver entryResolver) { + this.deobfToObf = deobfToObf; + this.deobfuscator = deobfuscator; + this.entryResolver = entryResolver; + } + + public void validateRename(Entry entry, String name) throws IllegalNameException { + Collection> equivalentEntries = entryResolver.resolveEquivalentEntries(entry); + for (Entry equivalentEntry : equivalentEntries) { + equivalentEntry.validateName(name); + validateUnique(equivalentEntry, name); + } + } + + private void validateUnique(Entry entry, String name) { + Entry translatedEntry = deobfuscator.translate(entry); + Collection> siblings = deobfToObf.getSiblings(translatedEntry); + if (!isUnique(translatedEntry, siblings, name)) { + throw new IllegalNameException(name, "Name is not unique in " + translatedEntry.getParent() + "!"); + } + } + + private boolean isUnique(Entry entry, Collection> siblings, String name) { + for (Entry child : siblings) { + if (entry.canConflictWith(child) && child.getName().equals(name)) { + return false; + } + } + return true; + } +} diff --git a/src/main/java/cuchaz/enigma/translation/mapping/MappingsChecker.java b/src/main/java/cuchaz/enigma/translation/mapping/MappingsChecker.java new file mode 100644 index 0000000..77d75ec --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/mapping/MappingsChecker.java @@ -0,0 +1,91 @@ +/******************************************************************************* + * 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.translation.mapping; + +import cuchaz.enigma.analysis.index.JarIndex; +import cuchaz.enigma.translation.mapping.tree.EntryTree; +import cuchaz.enigma.translation.mapping.tree.EntryTreeNode; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.translation.representation.entry.FieldEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +public class MappingsChecker { + private final JarIndex index; + private final EntryTree mappings; + + public MappingsChecker(JarIndex index, EntryTree mappings) { + this.index = index; + this.mappings = mappings; + } + + public Dropped dropBrokenMappings() { + Dropped dropped = new Dropped(); + + Collection> obfEntries = mappings.getAllEntries(); + for (Entry entry : obfEntries) { + if (entry instanceof ClassEntry || entry instanceof MethodEntry || entry instanceof FieldEntry) { + tryDropEntry(dropped, entry); + } + } + + dropped.apply(mappings); + + return dropped; + } + + private void tryDropEntry(Dropped dropped, Entry entry) { + if (shouldDropEntry(entry)) { + EntryMapping mapping = mappings.get(entry); + if (mapping != null) { + dropped.drop(entry, mapping); + } + } + } + + private boolean shouldDropEntry(Entry entry) { + if (!index.getEntryIndex().hasEntry(entry)) { + return true; + } + Collection> resolvedEntries = index.getEntryResolver().resolveEntry(entry, ResolutionStrategy.RESOLVE_ROOT); + return !resolvedEntries.contains(entry); + } + + public static class Dropped { + private final Map, String> droppedMappings = new HashMap<>(); + + public void drop(Entry entry, EntryMapping mapping) { + droppedMappings.put(entry, mapping.getTargetName()); + } + + void apply(EntryTree mappings) { + for (Entry entry : droppedMappings.keySet()) { + EntryTreeNode node = mappings.findNode(entry); + if (node == null) { + continue; + } + + for (Entry childEntry : node.getChildrenRecursively()) { + mappings.remove(childEntry); + } + } + } + + public Map, String> getDroppedMappings() { + return droppedMappings; + } + } +} diff --git a/src/main/java/cuchaz/enigma/translation/mapping/NameValidator.java b/src/main/java/cuchaz/enigma/translation/mapping/NameValidator.java new file mode 100644 index 0000000..19473ea --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/mapping/NameValidator.java @@ -0,0 +1,56 @@ +/******************************************************************************* + * 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.translation.mapping; + +import cuchaz.enigma.throwables.IllegalNameException; +import cuchaz.enigma.translation.representation.entry.ClassEntry; + +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; + +public class NameValidator { + private static final Pattern IDENTIFIER_PATTERN; + private static final Pattern CLASS_PATTERN; + private static final List ILLEGAL_IDENTIFIERS = 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 { + String identifierRegex = "[A-Za-z_<][A-Za-z0-9_>]*"; + IDENTIFIER_PATTERN = Pattern.compile(identifierRegex); + CLASS_PATTERN = Pattern.compile(String.format("^(%s(\\.|/))*(%s)$", identifierRegex, identifierRegex)); + } + + public static void validateClassName(String name, boolean packageRequired) { + if (!CLASS_PATTERN.matcher(name).matches() || ILLEGAL_IDENTIFIERS.contains(name)) { + throw new IllegalNameException(name, "This doesn't look like a legal class name"); + } + if (packageRequired && ClassEntry.getPackageName(name) == null) { + throw new IllegalNameException(name, "Class must be in a package"); + } + } + + public static void validateIdentifier(String name) { + if (!IDENTIFIER_PATTERN.matcher(name).matches() || ILLEGAL_IDENTIFIERS.contains(name)) { + throw new IllegalNameException(name, "This doesn't look like a legal identifier"); + } + } + + public static boolean isReserved(String name) { + return ILLEGAL_IDENTIFIERS.contains(name); + } +} diff --git a/src/main/java/cuchaz/enigma/translation/mapping/ResolutionStrategy.java b/src/main/java/cuchaz/enigma/translation/mapping/ResolutionStrategy.java new file mode 100644 index 0000000..1c28e02 --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/mapping/ResolutionStrategy.java @@ -0,0 +1,6 @@ +package cuchaz.enigma.translation.mapping; + +public enum ResolutionStrategy { + RESOLVE_ROOT, + RESOLVE_CLOSEST +} diff --git a/src/main/java/cuchaz/enigma/translation/mapping/VoidEntryResolver.java b/src/main/java/cuchaz/enigma/translation/mapping/VoidEntryResolver.java new file mode 100644 index 0000000..2eab55f --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/mapping/VoidEntryResolver.java @@ -0,0 +1,27 @@ +package cuchaz.enigma.translation.mapping; + +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; + +import java.util.Collection; +import java.util.Collections; +import java.util.Set; + +public enum VoidEntryResolver implements EntryResolver { + INSTANCE; + + @Override + public > Collection resolveEntry(E entry, ResolutionStrategy strategy) { + return Collections.singleton(entry); + } + + @Override + public Set> resolveEquivalentEntries(Entry entry) { + return Collections.singleton(entry); + } + + @Override + public Set resolveEquivalentMethods(MethodEntry methodEntry) { + return Collections.singleton(methodEntry); + } +} diff --git a/src/main/java/cuchaz/enigma/translation/mapping/serde/EnigmaMappingsReader.java b/src/main/java/cuchaz/enigma/translation/mapping/serde/EnigmaMappingsReader.java new file mode 100644 index 0000000..d36bc0b --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/mapping/serde/EnigmaMappingsReader.java @@ -0,0 +1,260 @@ +package cuchaz.enigma.translation.mapping.serde; + +import com.google.common.base.Charsets; +import cuchaz.enigma.throwables.MappingParseException; +import cuchaz.enigma.translation.mapping.AccessModifier; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.MappingPair; +import cuchaz.enigma.translation.mapping.tree.HashEntryTree; +import cuchaz.enigma.translation.mapping.tree.EntryTree; +import cuchaz.enigma.translation.representation.MethodDescriptor; +import cuchaz.enigma.translation.representation.TypeDescriptor; +import cuchaz.enigma.translation.representation.entry.*; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; + +public enum EnigmaMappingsReader implements MappingsReader { + FILE { + @Override + public EntryTree read(Path path) throws IOException, MappingParseException { + EntryTree mappings = new HashEntryTree<>(); + readFile(path, mappings); + return mappings; + } + }, + DIRECTORY { + @Override + public EntryTree read(Path path) throws IOException, MappingParseException { + EntryTree mappings = new HashEntryTree<>(); + + List files = Files.walk(path) + .filter(f -> !Files.isDirectory(f)) + .filter(f -> f.toString().endsWith(".mapping")) + .collect(Collectors.toList()); + for (Path file : files) { + if (Files.isHidden(file)) { + continue; + } + readFile(file, mappings); + } + + return mappings; + } + }; + + protected void readFile(Path path, EntryTree mappings) throws IOException, MappingParseException { + List lines = Files.readAllLines(path, Charsets.UTF_8); + Deque> mappingStack = new ArrayDeque<>(); + + for (int lineNumber = 0; lineNumber < lines.size(); lineNumber++) { + String line = lines.get(lineNumber); + int indentation = countIndentation(line); + + line = formatLine(line); + if (line == null) { + continue; + } + + while (indentation < mappingStack.size()) { + mappingStack.pop(); + } + + try { + MappingPair pair = parseLine(mappingStack.peek(), line); + mappingStack.push(pair.getEntry()); + if (pair.getMapping() != null) { + mappings.insert(pair.getEntry(), pair.getMapping()); + } + } catch (Throwable t) { + t.printStackTrace(); + throw new MappingParseException(path::toString, lineNumber, t.toString()); + } + } + } + + @Nullable + private String formatLine(String line) { + line = stripComment(line); + line = line.trim(); + + if (line.isEmpty()) { + return null; + } + + return line; + } + + private String stripComment(String line) { + int commentPos = line.indexOf('#'); + if (commentPos >= 0) { + return line.substring(0, commentPos); + } + return line; + } + + private int countIndentation(String line) { + int indent = 0; + for (int i = 0; i < line.length(); i++) { + if (line.charAt(i) != '\t') { + break; + } + indent++; + } + return indent; + } + + private MappingPair parseLine(@Nullable Entry parent, String line) { + String[] tokens = line.trim().split("\\s"); + String keyToken = tokens[0].toLowerCase(Locale.ROOT); + + switch (keyToken) { + case "class": + return parseClass(parent, tokens); + case "field": + return parseField(parent, tokens); + case "method": + return parseMethod(parent, tokens); + case "arg": + return parseArgument(parent, tokens); + default: + throw new RuntimeException("Unknown token '" + keyToken + "'"); + } + } + + private MappingPair parseClass(@Nullable Entry parent, String[] tokens) { + String obfuscatedName = ClassEntry.getInnerName(tokens[1]); + ClassEntry obfuscatedEntry; + if (parent instanceof ClassEntry) { + obfuscatedEntry = new ClassEntry((ClassEntry) parent, obfuscatedName); + } else { + obfuscatedEntry = new ClassEntry(obfuscatedName); + } + + String mapping = null; + AccessModifier modifier = AccessModifier.UNCHANGED; + + if (tokens.length == 3) { + AccessModifier parsedModifier = parseModifier(tokens[2]); + if (parsedModifier != null) { + modifier = parsedModifier; + mapping = obfuscatedName; + } else { + mapping = tokens[2]; + } + } else if (tokens.length == 4) { + mapping = tokens[2]; + modifier = parseModifier(tokens[3]); + } + + if (mapping != null) { + return new MappingPair<>(obfuscatedEntry, new EntryMapping(mapping, modifier)); + } else { + return new MappingPair<>(obfuscatedEntry); + } + } + + private MappingPair parseField(@Nullable Entry parent, String[] tokens) { + if (!(parent instanceof ClassEntry)) { + throw new RuntimeException("Field must be a child of a class!"); + } + + ClassEntry ownerEntry = (ClassEntry) parent; + + String obfuscatedName = tokens[1]; + String mapping = obfuscatedName; + AccessModifier modifier = AccessModifier.UNCHANGED; + TypeDescriptor descriptor; + + if (tokens.length == 4) { + AccessModifier parsedModifier = parseModifier(tokens[3]); + if (parsedModifier != null) { + descriptor = new TypeDescriptor(tokens[2]); + modifier = parsedModifier; + } else { + mapping = tokens[2]; + descriptor = new TypeDescriptor(tokens[3]); + } + } else if (tokens.length == 5) { + descriptor = new TypeDescriptor(tokens[3]); + mapping = tokens[2]; + modifier = parseModifier(tokens[4]); + } else { + throw new RuntimeException("Invalid method declaration"); + } + + FieldEntry obfuscatedEntry = new FieldEntry(ownerEntry, obfuscatedName, descriptor); + if (mapping != null) { + return new MappingPair<>(obfuscatedEntry, new EntryMapping(mapping, modifier)); + } else { + return new MappingPair<>(obfuscatedEntry); + } + } + + private MappingPair parseMethod(@Nullable Entry parent, String[] tokens) { + if (!(parent instanceof ClassEntry)) { + throw new RuntimeException("Method must be a child of a class!"); + } + + ClassEntry ownerEntry = (ClassEntry) parent; + + String obfuscatedName = tokens[1]; + String mapping = null; + AccessModifier modifier = AccessModifier.UNCHANGED; + MethodDescriptor descriptor; + + if (tokens.length == 3) { + descriptor = new MethodDescriptor(tokens[2]); + } else if (tokens.length == 4) { + AccessModifier parsedModifier = parseModifier(tokens[3]); + if (parsedModifier != null) { + modifier = parsedModifier; + mapping = obfuscatedName; + descriptor = new MethodDescriptor(tokens[2]); + } else { + mapping = tokens[2]; + descriptor = new MethodDescriptor(tokens[3]); + } + } else if (tokens.length == 5) { + mapping = tokens[2]; + modifier = parseModifier(tokens[4]); + descriptor = new MethodDescriptor(tokens[3]); + } else { + throw new RuntimeException("Invalid method declaration"); + } + + MethodEntry obfuscatedEntry = new MethodEntry(ownerEntry, obfuscatedName, descriptor); + if (mapping != null) { + return new MappingPair<>(obfuscatedEntry, new EntryMapping(mapping, modifier)); + } else { + return new MappingPair<>(obfuscatedEntry); + } + } + + private MappingPair parseArgument(@Nullable Entry parent, String[] tokens) { + if (!(parent instanceof MethodEntry)) { + throw new RuntimeException("Method arg must be a child of a method!"); + } + + MethodEntry ownerEntry = (MethodEntry) parent; + LocalVariableEntry obfuscatedEntry = new LocalVariableEntry(ownerEntry, Integer.parseInt(tokens[1]), "", true); + String mapping = tokens[2]; + + return new MappingPair<>(obfuscatedEntry, new EntryMapping(mapping)); + } + + @Nullable + private AccessModifier parseModifier(String token) { + if (token.startsWith("ACC:")) { + return AccessModifier.valueOf(token.substring(4)); + } + return null; + } +} diff --git a/src/main/java/cuchaz/enigma/translation/mapping/serde/EnigmaMappingsWriter.java b/src/main/java/cuchaz/enigma/translation/mapping/serde/EnigmaMappingsWriter.java new file mode 100644 index 0000000..3eef739 --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/mapping/serde/EnigmaMappingsWriter.java @@ -0,0 +1,260 @@ +/******************************************************************************* + * 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.translation.mapping.serde; + +import cuchaz.enigma.ProgressListener; +import cuchaz.enigma.translation.MappingTranslator; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.AccessModifier; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.MappingDelta; +import cuchaz.enigma.translation.mapping.VoidEntryResolver; +import cuchaz.enigma.translation.mapping.tree.EntryTree; +import cuchaz.enigma.translation.mapping.tree.EntryTreeNode; +import cuchaz.enigma.translation.representation.entry.*; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +public enum EnigmaMappingsWriter implements MappingsWriter { + FILE { + @Override + public void write(EntryTree mappings, MappingDelta delta, Path path, ProgressListener progress) { + Collection classes = mappings.getRootEntries().stream() + .filter(entry -> entry instanceof ClassEntry) + .map(entry -> (ClassEntry) entry) + .collect(Collectors.toList()); + + progress.init(classes.size(), "Writing classes"); + + int steps = 0; + try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter(path))) { + for (ClassEntry classEntry : classes) { + progress.step(steps++, classEntry.getFullName()); + writeRoot(writer, mappings, classEntry); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + }, + DIRECTORY { + @Override + public void write(EntryTree mappings, MappingDelta delta, Path path, ProgressListener progress) { + applyDeletions(delta.getDeletions(), path); + + Collection classes = delta.getAdditions().getRootEntries().stream() + .filter(entry -> entry instanceof ClassEntry) + .map(entry -> (ClassEntry) entry) + .collect(Collectors.toList()); + + progress.init(classes.size(), "Writing classes"); + + Translator translator = new MappingTranslator(mappings, VoidEntryResolver.INSTANCE); + AtomicInteger steps = new AtomicInteger(); + + classes.parallelStream().forEach(classEntry -> { + progress.step(steps.getAndIncrement(), classEntry.getFullName()); + + try { + Path classPath = resolve(path, translator.translate(classEntry)); + Files.deleteIfExists(classPath); + Files.createDirectories(classPath.getParent()); + + try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter(classPath))) { + writeRoot(writer, mappings, classEntry); + } + } catch (Throwable t) { + System.err.println("Failed to write class '" + classEntry.getFullName() + "'"); + t.printStackTrace(); + } + }); + } + + private void applyDeletions(EntryTree deletions, Path root) { + Collection deletedClasses = deletions.getRootEntries().stream() + .filter(e -> e instanceof ClassEntry) + .map(e -> (ClassEntry) e) + .collect(Collectors.toList()); + + for (ClassEntry classEntry : deletedClasses) { + try { + Files.deleteIfExists(resolve(root, classEntry)); + } catch (IOException e) { + System.err.println("Failed to delete deleted class '" + classEntry + "'"); + e.printStackTrace(); + } + } + + for (ClassEntry classEntry : deletedClasses) { + String packageName = classEntry.getPackageName(); + if (packageName != null) { + Path packagePath = Paths.get(packageName); + try { + deleteDeadPackages(root, packagePath); + } catch (IOException e) { + System.err.println("Failed to delete dead package '" + packageName + "'"); + e.printStackTrace(); + } + } + } + } + + private void deleteDeadPackages(Path root, Path packagePath) throws IOException { + for (int i = packagePath.getNameCount() - 1; i >= 0; i--) { + Path subPath = packagePath.subpath(0, i + 1); + Path packagePart = root.resolve(subPath); + if (isEmpty(packagePart)) { + Files.deleteIfExists(packagePart); + } + } + } + + private boolean isEmpty(Path path) { + try (DirectoryStream stream = Files.newDirectoryStream(path)) { + return !stream.iterator().hasNext(); + } catch (IOException e) { + return false; + } + } + + private Path resolve(Path root, ClassEntry classEntry) { + return root.resolve(classEntry.getFullName() + ".mapping"); + } + }; + + protected void writeRoot(PrintWriter writer, EntryTree mappings, ClassEntry classEntry) { + Collection> children = groupChildren(mappings.getChildren(classEntry)); + + writer.println(writeClass(classEntry, mappings.get(classEntry)).trim()); + for (Entry child : children) { + writeEntry(writer, mappings, child, 1); + } + } + + protected void writeEntry(PrintWriter writer, EntryTree mappings, Entry entry, int depth) { + EntryTreeNode node = mappings.findNode(entry); + if (node == null) { + return; + } + + EntryMapping mapping = node.getValue(); + if (entry instanceof ClassEntry) { + String line = writeClass((ClassEntry) entry, mapping); + writer.println(indent(line, depth)); + } else if (entry instanceof MethodEntry) { + String line = writeMethod((MethodEntry) entry, mapping); + writer.println(indent(line, depth)); + } else if (entry instanceof FieldEntry) { + String line = writeField((FieldEntry) entry, mapping); + writer.println(indent(line, depth)); + } else if (entry instanceof LocalVariableEntry) { + String line = writeArgument((LocalVariableEntry) entry, mapping); + writer.println(indent(line, depth)); + } + + Collection> children = groupChildren(node.getChildren()); + for (Entry child : children) { + writeEntry(writer, mappings, child, depth + 1); + } + } + + private Collection> groupChildren(Collection> children) { + Collection> result = new ArrayList<>(children.size()); + + children.stream().filter(e -> e instanceof ClassEntry) + .map(e -> (ClassEntry) e) + .sorted() + .forEach(result::add); + + children.stream().filter(e -> e instanceof FieldEntry) + .map(e -> (FieldEntry) e) + .sorted() + .forEach(result::add); + + children.stream().filter(e -> e instanceof MethodEntry) + .map(e -> (MethodEntry) e) + .sorted() + .forEach(result::add); + + children.stream().filter(e -> e instanceof LocalVariableEntry) + .map(e -> (LocalVariableEntry) e) + .sorted() + .forEach(result::add); + + return result; + } + + protected String writeClass(ClassEntry entry, EntryMapping mapping) { + StringBuilder builder = new StringBuilder("CLASS "); + builder.append(entry.getFullName()).append(' '); + writeMapping(builder, mapping); + + return builder.toString(); + } + + protected String writeMethod(MethodEntry entry, EntryMapping mapping) { + StringBuilder builder = new StringBuilder("METHOD "); + builder.append(entry.getName()).append(' '); + writeMapping(builder, mapping); + + builder.append(entry.getDesc().toString()); + + return builder.toString(); + } + + protected String writeField(FieldEntry entry, EntryMapping mapping) { + StringBuilder builder = new StringBuilder("FIELD "); + builder.append(entry.getName()).append(' '); + writeMapping(builder, mapping); + + builder.append(entry.getDesc().toString()); + + return builder.toString(); + } + + protected String writeArgument(LocalVariableEntry entry, EntryMapping mapping) { + StringBuilder builder = new StringBuilder("ARG "); + builder.append(entry.getIndex()).append(' '); + + String mappedName = mapping != null ? mapping.getTargetName() : entry.getName(); + builder.append(mappedName); + + return builder.toString(); + } + + private void writeMapping(StringBuilder builder, EntryMapping mapping) { + if (mapping != null) { + builder.append(mapping.getTargetName()).append(' '); + if (mapping.getAccessModifier() != AccessModifier.UNCHANGED) { + builder.append(mapping.getAccessModifier().getFormattedName()).append(' '); + } + } + } + + private String indent(String line, int depth) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < depth; i++) { + builder.append("\t"); + } + builder.append(line.trim()); + return builder.toString(); + } +} diff --git a/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingFormat.java b/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingFormat.java new file mode 100644 index 0000000..4db1645 --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingFormat.java @@ -0,0 +1,54 @@ +package cuchaz.enigma.translation.mapping.serde; + +import cuchaz.enigma.ProgressListener; +import cuchaz.enigma.throwables.MappingParseException; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.MappingDelta; +import cuchaz.enigma.translation.mapping.tree.EntryTree; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.nio.file.Path; + +public enum MappingFormat { + ENIGMA_FILE(EnigmaMappingsWriter.FILE, EnigmaMappingsReader.FILE), + ENIGMA_DIRECTORY(EnigmaMappingsWriter.DIRECTORY, EnigmaMappingsReader.DIRECTORY), + TINY_FILE(null, TinyMappingsReader.INSTANCE), + SRG_FILE(SrgMappingsWriter.INSTANCE, null); + + private final MappingsWriter writer; + private final MappingsReader reader; + + MappingFormat(MappingsWriter writer, MappingsReader reader) { + this.writer = writer; + this.reader = reader; + } + + public void write(EntryTree mappings, Path path, ProgressListener progressListener) { + write(mappings, MappingDelta.added(mappings), path, progressListener); + } + + public void write(EntryTree mappings, MappingDelta delta, Path path, ProgressListener progressListener) { + if (writer == null) { + throw new IllegalStateException(name() + " does not support writing"); + } + writer.write(mappings, delta, path, progressListener); + } + + public EntryTree read(Path path) throws IOException, MappingParseException { + if (reader == null) { + throw new IllegalStateException(name() + " does not support reading"); + } + return reader.read(path); + } + + @Nullable + public MappingsWriter getWriter() { + return writer; + } + + @Nullable + public MappingsReader getReader() { + return reader; + } +} diff --git a/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingsReader.java b/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingsReader.java new file mode 100644 index 0000000..f239ee6 --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingsReader.java @@ -0,0 +1,12 @@ +package cuchaz.enigma.translation.mapping.serde; + +import cuchaz.enigma.throwables.MappingParseException; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.tree.EntryTree; + +import java.io.IOException; +import java.nio.file.Path; + +public interface MappingsReader { + EntryTree read(Path path) throws MappingParseException, IOException; +} diff --git a/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingsWriter.java b/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingsWriter.java new file mode 100644 index 0000000..b519668 --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingsWriter.java @@ -0,0 +1,12 @@ +package cuchaz.enigma.translation.mapping.serde; + +import cuchaz.enigma.ProgressListener; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.MappingDelta; +import cuchaz.enigma.translation.mapping.tree.EntryTree; + +import java.nio.file.Path; + +public interface MappingsWriter { + void write(EntryTree mappings, MappingDelta delta, Path path, ProgressListener progress); +} diff --git a/src/main/java/cuchaz/enigma/translation/mapping/serde/SrgMappingsWriter.java b/src/main/java/cuchaz/enigma/translation/mapping/serde/SrgMappingsWriter.java new file mode 100644 index 0000000..15ba4d7 --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/mapping/serde/SrgMappingsWriter.java @@ -0,0 +1,115 @@ +package cuchaz.enigma.translation.mapping.serde; + +import com.google.common.collect.Lists; +import cuchaz.enigma.ProgressListener; +import cuchaz.enigma.translation.MappingTranslator; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.MappingDelta; +import cuchaz.enigma.translation.mapping.VoidEntryResolver; +import cuchaz.enigma.translation.mapping.tree.EntryTree; +import cuchaz.enigma.translation.mapping.tree.EntryTreeNode; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.translation.representation.entry.FieldEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +public enum SrgMappingsWriter implements MappingsWriter { + INSTANCE; + + @Override + public void write(EntryTree mappings, MappingDelta delta, Path path, ProgressListener progress) { + try { + Files.deleteIfExists(path); + Files.createFile(path); + } catch (IOException e) { + e.printStackTrace(); + } + + List classLines = new ArrayList<>(); + List fieldLines = new ArrayList<>(); + List methodLines = new ArrayList<>(); + + Collection> rootEntries = Lists.newArrayList(mappings).stream() + .map(EntryTreeNode::getEntry) + .collect(Collectors.toList()); + progress.init(rootEntries.size(), "Generating mappings"); + + int steps = 0; + for (Entry entry : sorted(rootEntries)) { + progress.step(steps++, entry.getName()); + writeEntry(classLines, fieldLines, methodLines, mappings, entry); + } + + progress.init(3, "Writing mappings"); + try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter(path))) { + progress.step(0, "Classes"); + classLines.forEach(writer::println); + progress.step(1, "Fields"); + fieldLines.forEach(writer::println); + progress.step(2, "Methods"); + methodLines.forEach(writer::println); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private void writeEntry(List classes, List fields, List methods, EntryTree mappings, Entry entry) { + EntryTreeNode node = mappings.findNode(entry); + if (node == null) { + return; + } + + Translator translator = new MappingTranslator(mappings, VoidEntryResolver.INSTANCE); + if (entry instanceof ClassEntry) { + classes.add(generateClassLine((ClassEntry) entry, translator)); + } else if (entry instanceof FieldEntry) { + fields.add(generateFieldLine((FieldEntry) entry, translator)); + } else if (entry instanceof MethodEntry) { + methods.add(generateMethodLine((MethodEntry) entry, translator)); + } + + for (Entry child : sorted(node.getChildren())) { + writeEntry(classes, fields, methods, mappings, child); + } + } + + private String generateClassLine(ClassEntry sourceEntry, Translator translator) { + ClassEntry targetEntry = translator.translate(sourceEntry); + return "CL: " + sourceEntry.getFullName() + " " + targetEntry.getFullName(); + } + + private String generateMethodLine(MethodEntry sourceEntry, Translator translator) { + MethodEntry targetEntry = translator.translate(sourceEntry); + return "MD: " + describeMethod(sourceEntry) + " " + describeMethod(targetEntry); + } + + private String describeMethod(MethodEntry entry) { + return entry.getParent().getFullName() + "/" + entry.getName() + " " + entry.getDesc(); + } + + private String generateFieldLine(FieldEntry sourceEntry, Translator translator) { + FieldEntry targetEntry = translator.translate(sourceEntry); + return "FD: " + describeField(sourceEntry) + " " + describeField(targetEntry); + } + + private String describeField(FieldEntry entry) { + return entry.getParent().getFullName() + "/" + entry.getName(); + } + + private Collection> sorted(Iterable> iterable) { + ArrayList> sorted = Lists.newArrayList(iterable); + sorted.sort(Comparator.comparing(Entry::getName)); + return sorted; + } +} diff --git a/src/main/java/cuchaz/enigma/translation/mapping/serde/TinyMappingsReader.java b/src/main/java/cuchaz/enigma/translation/mapping/serde/TinyMappingsReader.java new file mode 100644 index 0000000..e0afc3e --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/mapping/serde/TinyMappingsReader.java @@ -0,0 +1,100 @@ +package cuchaz.enigma.translation.mapping.serde; + +import com.google.common.base.Charsets; +import cuchaz.enigma.throwables.MappingParseException; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.MappingPair; +import cuchaz.enigma.translation.mapping.tree.HashEntryTree; +import cuchaz.enigma.translation.mapping.tree.EntryTree; +import cuchaz.enigma.translation.representation.MethodDescriptor; +import cuchaz.enigma.translation.representation.TypeDescriptor; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.FieldEntry; +import cuchaz.enigma.translation.representation.entry.LocalVariableEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +public enum TinyMappingsReader implements MappingsReader { + INSTANCE; + + @Override + public EntryTree read(Path path) throws IOException, MappingParseException { + return read(path, Files.readAllLines(path, Charsets.UTF_8)); + } + + private EntryTree read(Path path, List lines) throws MappingParseException { + EntryTree mappings = new HashEntryTree<>(); + lines.remove(0); + + for (int lineNumber = 0; lineNumber < lines.size(); lineNumber++) { + String line = lines.get(lineNumber); + + try { + MappingPair mapping = parseLine(line); + mappings.insert(mapping.getEntry(), mapping.getMapping()); + } catch (Throwable t) { + t.printStackTrace(); + throw new MappingParseException(path::toString, lineNumber, t.toString()); + } + } + + return mappings; + } + + private MappingPair parseLine(String line) { + String[] tokens = line.split("\t"); + + String key = tokens[0]; + switch (key) { + case "CLASS": + return parseClass(tokens); + case "FIELD": + return parseField(tokens); + case "METHOD": + return parseMethod(tokens); + case "MTH-ARG": + return parseArgument(tokens); + default: + throw new RuntimeException("Unknown token '" + key + "'!"); + } + } + + private MappingPair parseClass(String[] tokens) { + ClassEntry obfuscatedEntry = new ClassEntry(tokens[1]); + String mapping = tokens[2]; + return new MappingPair<>(obfuscatedEntry, new EntryMapping(mapping)); + } + + private MappingPair parseField(String[] tokens) { + ClassEntry ownerClass = new ClassEntry(tokens[1]); + TypeDescriptor descriptor = new TypeDescriptor(tokens[2]); + + FieldEntry obfuscatedEntry = new FieldEntry(ownerClass, tokens[3], descriptor); + String mapping = tokens[4]; + return new MappingPair<>(obfuscatedEntry, new EntryMapping(mapping)); + } + + private MappingPair parseMethod(String[] tokens) { + ClassEntry ownerClass = new ClassEntry(tokens[1]); + MethodDescriptor descriptor = new MethodDescriptor(tokens[2]); + + MethodEntry obfuscatedEntry = new MethodEntry(ownerClass, tokens[3], descriptor); + String mapping = tokens[4]; + return new MappingPair<>(obfuscatedEntry, new EntryMapping(mapping)); + } + + private MappingPair parseArgument(String[] tokens) { + ClassEntry ownerClass = new ClassEntry(tokens[1]); + MethodDescriptor ownerDescriptor = new MethodDescriptor(tokens[2]); + MethodEntry ownerMethod = new MethodEntry(ownerClass, tokens[3], ownerDescriptor); + int variableIndex = Integer.parseInt(tokens[4]); + + String mapping = tokens[5]; + LocalVariableEntry obfuscatedEntry = new LocalVariableEntry(ownerMethod, variableIndex, "", true); + return new MappingPair<>(obfuscatedEntry, new EntryMapping(mapping)); + } +} diff --git a/src/main/java/cuchaz/enigma/translation/mapping/tree/DeltaTrackingTree.java b/src/main/java/cuchaz/enigma/translation/mapping/tree/DeltaTrackingTree.java new file mode 100644 index 0000000..98a01df --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/mapping/tree/DeltaTrackingTree.java @@ -0,0 +1,113 @@ +package cuchaz.enigma.translation.mapping.tree; + +import cuchaz.enigma.translation.mapping.MappingDelta; +import cuchaz.enigma.translation.representation.entry.Entry; + +import javax.annotation.Nullable; +import java.util.Collection; +import java.util.Iterator; + +public class DeltaTrackingTree implements EntryTree { + private final EntryTree delegate; + + private EntryTree additions = new HashEntryTree<>(); + private EntryTree deletions = new HashEntryTree<>(); + + public DeltaTrackingTree(EntryTree delegate) { + this.delegate = delegate; + } + + public DeltaTrackingTree() { + this(new HashEntryTree<>()); + } + + @Override + public void insert(Entry entry, T value) { + if (value != null) { + trackAddition(entry); + } else { + trackDeletion(entry); + } + delegate.insert(entry, value); + } + + @Nullable + @Override + public T remove(Entry entry) { + T value = delegate.remove(entry); + trackDeletion(entry); + return value; + } + + public void trackAddition(Entry entry) { + deletions.remove(entry); + additions.insert(entry, MappingDelta.PLACEHOLDER); + } + + public void trackDeletion(Entry entry) { + additions.remove(entry); + deletions.insert(entry, MappingDelta.PLACEHOLDER); + } + + @Nullable + @Override + public T get(Entry entry) { + return delegate.get(entry); + } + + @Override + public Collection> getChildren(Entry entry) { + return delegate.getChildren(entry); + } + + @Override + public Collection> getSiblings(Entry entry) { + return delegate.getSiblings(entry); + } + + @Nullable + @Override + public EntryTreeNode findNode(Entry entry) { + return delegate.findNode(entry); + } + + @Override + public Collection> getAllNodes() { + return delegate.getAllNodes(); + } + + @Override + public Collection> getRootEntries() { + return delegate.getRootEntries(); + } + + @Override + public Collection> getAllEntries() { + return delegate.getAllEntries(); + } + + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public Iterator> iterator() { + return delegate.iterator(); + } + + public MappingDelta takeDelta() { + MappingDelta delta = new MappingDelta(additions, deletions); + resetDelta(); + return delta; + } + + private void resetDelta() { + additions = new HashEntryTree<>(); + deletions = new HashEntryTree<>(); + } + + public boolean isDirty() { + return !additions.isEmpty() || !deletions.isEmpty(); + } +} diff --git a/src/main/java/cuchaz/enigma/translation/mapping/tree/EntryTree.java b/src/main/java/cuchaz/enigma/translation/mapping/tree/EntryTree.java new file mode 100644 index 0000000..73fe12d --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/mapping/tree/EntryTree.java @@ -0,0 +1,20 @@ +package cuchaz.enigma.translation.mapping.tree; + +import cuchaz.enigma.translation.mapping.EntryMap; +import cuchaz.enigma.translation.representation.entry.Entry; + +import javax.annotation.Nullable; +import java.util.Collection; + +public interface EntryTree extends EntryMap, Iterable> { + Collection> getChildren(Entry entry); + + Collection> getSiblings(Entry entry); + + @Nullable + EntryTreeNode findNode(Entry entry); + + Collection> getAllNodes(); + + Collection> getRootEntries(); +} diff --git a/src/main/java/cuchaz/enigma/translation/mapping/tree/EntryTreeNode.java b/src/main/java/cuchaz/enigma/translation/mapping/tree/EntryTreeNode.java new file mode 100644 index 0000000..734b60c --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/mapping/tree/EntryTreeNode.java @@ -0,0 +1,36 @@ +package cuchaz.enigma.translation.mapping.tree; + +import cuchaz.enigma.translation.representation.entry.Entry; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.stream.Collectors; + +public interface EntryTreeNode { + @Nullable + T getValue(); + + Entry getEntry(); + + boolean isEmpty(); + + Collection> getChildren(); + + Collection> getChildNodes(); + + default Collection> getNodesRecursively() { + Collection> nodes = new ArrayList<>(); + nodes.add(this); + for (EntryTreeNode node : getChildNodes()) { + nodes.addAll(node.getNodesRecursively()); + } + return nodes; + } + + default Collection> getChildrenRecursively() { + return getNodesRecursively().stream() + .map(EntryTreeNode::getEntry) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/cuchaz/enigma/translation/mapping/tree/HashEntryTree.java b/src/main/java/cuchaz/enigma/translation/mapping/tree/HashEntryTree.java new file mode 100644 index 0000000..ff88bf9 --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/mapping/tree/HashEntryTree.java @@ -0,0 +1,159 @@ +package cuchaz.enigma.translation.mapping.tree; + +import cuchaz.enigma.translation.representation.entry.Entry; + +import javax.annotation.Nullable; +import java.util.*; +import java.util.stream.Collectors; + +public class HashEntryTree implements EntryTree { + private final Map, HashTreeNode> root = new HashMap<>(); + + @Override + public void insert(Entry entry, T value) { + List> path = computePath(entry); + path.get(path.size() - 1).putValue(value); + if (value == null) { + removeDeadAlong(path); + } + } + + @Override + @Nullable + public T remove(Entry entry) { + List> path = computePath(entry); + T value = path.get(path.size() - 1).removeValue(); + + removeDeadAlong(path); + + return value; + } + + @Override + @Nullable + public T get(Entry entry) { + HashTreeNode node = findNode(entry); + if (node == null) { + return null; + } + return node.getValue(); + } + + @Override + public boolean contains(Entry entry) { + return get(entry) != null; + } + + @Override + public Collection> getChildren(Entry entry) { + HashTreeNode leaf = findNode(entry); + if (leaf == null) { + return Collections.emptyList(); + } + return leaf.getChildren(); + } + + @Override + public Collection> getSiblings(Entry entry) { + List> path = computePath(entry); + if (path.size() <= 1) { + return getSiblings(entry, root.keySet()); + } + HashTreeNode parent = path.get(path.size() - 2); + return getSiblings(entry, parent.getChildren()); + } + + private Collection> getSiblings(Entry entry, Collection> children) { + Set> siblings = new HashSet<>(children); + siblings.remove(entry); + return siblings; + } + + @Override + @Nullable + public HashTreeNode findNode(Entry target) { + List> parentChain = target.getAncestry(); + if (parentChain.isEmpty()) { + return null; + } + + HashTreeNode node = root.get(parentChain.get(0)); + for (int i = 1; i < parentChain.size(); i++) { + if (node == null) { + return null; + } + node = node.getChild(parentChain.get(i), false); + } + + return node; + } + + private List> computePath(Entry target) { + List> ancestry = target.getAncestry(); + if (ancestry.isEmpty()) { + return Collections.emptyList(); + } + + List> path = new ArrayList<>(ancestry.size()); + + Entry rootEntry = ancestry.get(0); + HashTreeNode node = root.computeIfAbsent(rootEntry, HashTreeNode::new); + path.add(node); + + for (int i = 1; i < ancestry.size(); i++) { + node = node.getChild(ancestry.get(i), true); + path.add(node); + } + + return path; + } + + private void removeDeadAlong(List> path) { + for (int i = path.size() - 1; i >= 0; i--) { + HashTreeNode node = path.get(i); + if (node.isEmpty()) { + if (i > 0) { + HashTreeNode parentNode = path.get(i - 1); + parentNode.remove(node.getEntry()); + } else { + root.remove(node.getEntry()); + } + } else { + break; + } + } + } + + @Override + @SuppressWarnings("unchecked") + public Iterator> iterator() { + Collection> values = (Collection) root.values(); + return values.iterator(); + } + + @Override + public Collection> getAllNodes() { + Collection> nodes = new ArrayList<>(); + for (EntryTreeNode node : root.values()) { + nodes.addAll(node.getNodesRecursively()); + } + return nodes; + } + + @Override + public Collection> getAllEntries() { + return getAllNodes().stream() + .map(EntryTreeNode::getEntry) + .collect(Collectors.toList()); + } + + @Override + public Collection> getRootEntries() { + return root.keySet(); + } + + @Override + public boolean isEmpty() { + return root.isEmpty(); + } +} diff --git a/src/main/java/cuchaz/enigma/translation/mapping/tree/HashTreeNode.java b/src/main/java/cuchaz/enigma/translation/mapping/tree/HashTreeNode.java new file mode 100644 index 0000000..90e9164 --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/mapping/tree/HashTreeNode.java @@ -0,0 +1,72 @@ +package cuchaz.enigma.translation.mapping.tree; + +import cuchaz.enigma.translation.representation.entry.Entry; + +import javax.annotation.Nullable; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +public class HashTreeNode implements EntryTreeNode, Iterable> { + private final Entry entry; + private final Map, HashTreeNode> children = new HashMap<>(); + private T value; + + HashTreeNode(Entry entry) { + this.entry = entry; + } + + void putValue(T value) { + this.value = value; + } + + T removeValue() { + T value = this.value; + this.value = null; + return value; + } + + HashTreeNode getChild(Entry entry, boolean create) { + if (create) { + return children.computeIfAbsent(entry, HashTreeNode::new); + } else { + return children.get(entry); + } + } + + void remove(Entry entry) { + children.remove(entry); + } + + @Override + @Nullable + public T getValue() { + return value; + } + + @Override + public Entry getEntry() { + return entry; + } + + @Override + public boolean isEmpty() { + return children.isEmpty() && value == null; + } + + @Override + public Collection> getChildren() { + return children.keySet(); + } + + @Override + public Collection> getChildNodes() { + return children.values(); + } + + @Override + public Iterator> iterator() { + return children.values().iterator(); + } +} diff --git a/src/main/java/cuchaz/enigma/translation/representation/AccessFlags.java b/src/main/java/cuchaz/enigma/translation/representation/AccessFlags.java new file mode 100644 index 0000000..0534edd --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/representation/AccessFlags.java @@ -0,0 +1,112 @@ +package cuchaz.enigma.translation.representation; + +import cuchaz.enigma.analysis.Access; +import org.objectweb.asm.Opcodes; + +import java.lang.reflect.Modifier; + +public class AccessFlags { + public static final AccessFlags PRIVATE = new AccessFlags(Opcodes.ACC_PRIVATE); + public static final AccessFlags PUBLIC = new AccessFlags(Opcodes.ACC_PUBLIC); + + private int flags; + + public AccessFlags(int flags) { + this.flags = flags; + } + + public boolean isPrivate() { + return Modifier.isPrivate(this.flags); + } + + public boolean isProtected() { + return Modifier.isProtected(this.flags); + } + + public boolean isPublic() { + return Modifier.isPublic(this.flags); + } + + public boolean isSynthetic() { + return (this.flags & Opcodes.ACC_SYNTHETIC) != 0; + } + + public boolean isStatic() { + return Modifier.isStatic(this.flags); + } + + public boolean isEnum() { + return (flags & Opcodes.ACC_ENUM) != 0; + } + + public boolean isBridge() { + return (flags & Opcodes.ACC_BRIDGE) != 0; + } + + public boolean isFinal() { + return (flags & Opcodes.ACC_FINAL) != 0; + } + + public AccessFlags setPrivate() { + this.setVisibility(Opcodes.ACC_PRIVATE); + return this; + } + + public AccessFlags setProtected() { + this.setVisibility(Opcodes.ACC_PROTECTED); + return this; + } + + public AccessFlags setPublic() { + this.setVisibility(Opcodes.ACC_PUBLIC); + return this; + } + + public AccessFlags setBridge() { + flags |= Opcodes.ACC_BRIDGE; + return this; + } + + @Deprecated + public AccessFlags setBridged() { + return setBridge(); + } + + public void setVisibility(int visibility) { + this.resetVisibility(); + this.flags |= visibility; + } + + private void resetVisibility() { + this.flags &= ~(Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED | Opcodes.ACC_PUBLIC); + } + + public int getFlags() { + return this.flags; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof AccessFlags && ((AccessFlags) obj).flags == flags; + } + + @Override + public int hashCode() { + return flags; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(Access.get(this).toString().toLowerCase()); + if (isStatic()) { + builder.append(" static"); + } + if (isSynthetic()) { + builder.append(" synthetic"); + } + if (isBridge()) { + builder.append(" bridge"); + } + return builder.toString(); + } +} diff --git a/src/main/java/cuchaz/enigma/translation/representation/MethodDescriptor.java b/src/main/java/cuchaz/enigma/translation/representation/MethodDescriptor.java new file mode 100644 index 0000000..c59751f --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/representation/MethodDescriptor.java @@ -0,0 +1,132 @@ +/******************************************************************************* + * 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.translation.representation; + +import com.google.common.collect.Lists; +import cuchaz.enigma.translation.Translatable; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.EntryResolver; +import cuchaz.enigma.translation.mapping.EntryMap; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.utils.Utils; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +public class MethodDescriptor implements Translatable { + + private List argumentDescs; + private TypeDescriptor returnDesc; + + public MethodDescriptor(String desc) { + try { + this.argumentDescs = Lists.newArrayList(); + int i = 0; + while (i < desc.length()) { + char c = desc.charAt(i); + if (c == '(') { + assert (this.argumentDescs.isEmpty()); + assert (this.returnDesc == null); + i++; + } else if (c == ')') { + i++; + break; + } else { + String type = TypeDescriptor.parseFirst(desc.substring(i)); + this.argumentDescs.add(new TypeDescriptor(type)); + i += type.length(); + } + } + this.returnDesc = new TypeDescriptor(TypeDescriptor.parseFirst(desc.substring(i))); + } catch (Exception ex) { + throw new IllegalArgumentException("Unable to parse method descriptor: " + desc, ex); + } + } + + public MethodDescriptor(List argumentDescs, TypeDescriptor returnDesc) { + this.argumentDescs = argumentDescs; + this.returnDesc = returnDesc; + } + + public List getArgumentDescs() { + return this.argumentDescs; + } + + public TypeDescriptor getReturnDesc() { + return this.returnDesc; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append("("); + for (TypeDescriptor desc : this.argumentDescs) { + buf.append(desc); + } + buf.append(")"); + buf.append(this.returnDesc); + return buf.toString(); + } + + public Iterable types() { + List descs = Lists.newArrayList(); + descs.addAll(this.argumentDescs); + descs.add(this.returnDesc); + return descs; + } + + @Override + public boolean equals(Object other) { + return other instanceof MethodDescriptor && equals((MethodDescriptor) other); + } + + public boolean equals(MethodDescriptor other) { + return this.argumentDescs.equals(other.argumentDescs) && this.returnDesc.equals(other.returnDesc); + } + + @Override + public int hashCode() { + return Utils.combineHashesOrdered(this.argumentDescs.hashCode(), this.returnDesc.hashCode()); + } + + public boolean hasClass(ClassEntry classEntry) { + for (TypeDescriptor desc : types()) { + if (desc.containsType() && desc.getTypeEntry().equals(classEntry)) { + return true; + } + } + return false; + } + + public MethodDescriptor remap(Function remapper) { + List argumentDescs = new ArrayList<>(this.argumentDescs.size()); + for (TypeDescriptor desc : this.argumentDescs) { + argumentDescs.add(desc.remap(remapper)); + } + return new MethodDescriptor(argumentDescs, returnDesc.remap(remapper)); + } + + @Override + public Translatable translate(Translator translator, EntryResolver resolver, EntryMap mappings) { + List translatedArguments = new ArrayList<>(argumentDescs.size()); + for (TypeDescriptor argument : argumentDescs) { + translatedArguments.add(translator.translate(argument)); + } + return new MethodDescriptor(translatedArguments, translator.translate(returnDesc)); + } + + public boolean canConflictWith(MethodDescriptor descriptor) { + return descriptor.argumentDescs.equals(argumentDescs); + } +} diff --git a/src/main/java/cuchaz/enigma/translation/representation/ProcyonEntryFactory.java b/src/main/java/cuchaz/enigma/translation/representation/ProcyonEntryFactory.java new file mode 100644 index 0000000..9c9fa3d --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/representation/ProcyonEntryFactory.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * 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.translation.representation; + +import com.strobel.assembler.metadata.FieldDefinition; +import com.strobel.assembler.metadata.MemberReference; +import com.strobel.assembler.metadata.MethodDefinition; +import cuchaz.enigma.translation.representation.entry.*; + +public class ProcyonEntryFactory { + private final ReferencedEntryPool entryPool; + + public ProcyonEntryFactory(ReferencedEntryPool entryPool) { + this.entryPool = entryPool; + } + + public FieldEntry getFieldEntry(MemberReference def) { + ClassEntry classEntry = entryPool.getClass(def.getDeclaringType().getInternalName()); + return entryPool.getField(classEntry, def.getName(), def.getErasedSignature()); + } + + public FieldDefEntry getFieldDefEntry(FieldDefinition def) { + ClassEntry classEntry = entryPool.getClass(def.getDeclaringType().getInternalName()); + return new FieldDefEntry(classEntry, def.getName(), new TypeDescriptor(def.getErasedSignature()), Signature.createTypedSignature(def.getSignature()), new AccessFlags(def.getModifiers())); + } + + public MethodEntry getMethodEntry(MemberReference def) { + ClassEntry classEntry = entryPool.getClass(def.getDeclaringType().getInternalName()); + return entryPool.getMethod(classEntry, def.getName(), def.getErasedSignature()); + } + + public MethodDefEntry getMethodDefEntry(MethodDefinition def) { + ClassEntry classEntry = entryPool.getClass(def.getDeclaringType().getInternalName()); + return new MethodDefEntry(classEntry, def.getName(), new MethodDescriptor(def.getErasedSignature()), Signature.createSignature(def.getSignature()), new AccessFlags(def.getModifiers())); + } +} diff --git a/src/main/java/cuchaz/enigma/translation/representation/ReferencedEntryPool.java b/src/main/java/cuchaz/enigma/translation/representation/ReferencedEntryPool.java new file mode 100644 index 0000000..631b375 --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/representation/ReferencedEntryPool.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.translation.representation; + +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.FieldEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; + +import java.util.HashMap; +import java.util.Map; + +public class ReferencedEntryPool { + private final Map classEntries = new HashMap<>(); + private final Map> methodEntries = new HashMap<>(); + private final Map> fieldEntries = new HashMap<>(); + + public ClassEntry getClass(String name) { + // TODO: FIXME - I'm a hack! + if ("[T".equals(name) || "[[T".equals(name) || "[[[T".equals(name)) { + name = name.replaceAll("T", "Ljava/lang/Object;"); + } + + final String computeName = name; + return this.classEntries.computeIfAbsent(name, s -> new ClassEntry(computeName)); + } + + public MethodEntry getMethod(ClassEntry ownerEntry, String name, String desc) { + return getMethod(ownerEntry, name, new MethodDescriptor(desc)); + } + + public MethodEntry getMethod(ClassEntry ownerEntry, String name, MethodDescriptor desc) { + String key = name + desc.toString(); + return getClassMethods(ownerEntry.getFullName()).computeIfAbsent(key, s -> new MethodEntry(ownerEntry, name, desc)); + } + + public FieldEntry getField(ClassEntry ownerEntry, String name, String desc) { + return getField(ownerEntry, name, new TypeDescriptor(desc)); + } + + public FieldEntry getField(ClassEntry ownerEntry, String name, TypeDescriptor desc) { + return getClassFields(ownerEntry.getFullName()).computeIfAbsent(name, s -> new FieldEntry(ownerEntry, name, desc)); + } + + private Map getClassMethods(String name) { + return methodEntries.computeIfAbsent(name, s -> new HashMap<>()); + } + + private Map getClassFields(String name) { + return fieldEntries.computeIfAbsent(name, s -> new HashMap<>()); + } +} diff --git a/src/main/java/cuchaz/enigma/translation/representation/Signature.java b/src/main/java/cuchaz/enigma/translation/representation/Signature.java new file mode 100644 index 0000000..dc241b7 --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/representation/Signature.java @@ -0,0 +1,93 @@ +package cuchaz.enigma.translation.representation; + +import cuchaz.enigma.bytecode.translators.TranslationSignatureVisitor; +import cuchaz.enigma.translation.Translatable; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.EntryResolver; +import cuchaz.enigma.translation.mapping.EntryMap; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import org.objectweb.asm.signature.SignatureReader; +import org.objectweb.asm.signature.SignatureVisitor; +import org.objectweb.asm.signature.SignatureWriter; + +import java.util.function.Function; +import java.util.regex.Pattern; + +public class Signature implements Translatable { + private static final Pattern OBJECT_PATTERN = Pattern.compile(".*:Ljava/lang/Object;:.*"); + + private final String signature; + private final boolean isType; + + private Signature(String signature, boolean isType) { + if (signature != null && OBJECT_PATTERN.matcher(signature).matches()) { + signature = signature.replaceAll(":Ljava/lang/Object;:", "::"); + } + + this.signature = signature; + this.isType = isType; + } + + public static Signature createTypedSignature(String signature) { + if (signature != null && !signature.isEmpty()) { + return new Signature(signature, true); + } + return new Signature(null, true); + } + + public static Signature createSignature(String signature) { + if (signature != null && !signature.isEmpty()) { + return new Signature(signature, false); + } + return new Signature(null, false); + } + + public String getSignature() { + return signature; + } + + public boolean isType() { + return isType; + } + + public Signature remap(Function remapper) { + if (signature == null) { + return this; + } + SignatureWriter writer = new SignatureWriter(); + SignatureVisitor visitor = new TranslationSignatureVisitor(remapper, writer); + if (isType) { + new SignatureReader(signature).acceptType(visitor); + } else { + new SignatureReader(signature).accept(visitor); + } + return new Signature(writer.toString(), isType); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Signature) { + Signature other = (Signature) obj; + return (other.signature == null && signature == null || other.signature != null + && signature != null && other.signature.equals(signature)) + && other.isType == this.isType; + } + return false; + } + + @Override + public int hashCode() { + return signature.hashCode() | (isType ? 1 : 0) << 16; + } + + @Override + public String toString() { + return signature; + } + + @Override + public Translatable translate(Translator translator, EntryResolver resolver, EntryMap mappings) { + return remap(name -> translator.translate(new ClassEntry(name)).getFullName()); + } +} diff --git a/src/main/java/cuchaz/enigma/translation/representation/TypeDescriptor.java b/src/main/java/cuchaz/enigma/translation/representation/TypeDescriptor.java new file mode 100644 index 0000000..f7ba849 --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/representation/TypeDescriptor.java @@ -0,0 +1,268 @@ +/******************************************************************************* + * 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.translation.representation; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Maps; +import cuchaz.enigma.translation.Translatable; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.EntryResolver; +import cuchaz.enigma.translation.mapping.EntryMap; +import cuchaz.enigma.translation.representation.entry.ClassEntry; + +import java.util.Map; +import java.util.function.Function; + +public class TypeDescriptor implements Translatable { + + protected final String desc; + + public TypeDescriptor(String desc) { + Preconditions.checkNotNull(desc, "Desc cannot be null"); + + // don't deal with generics + // this is just for raw jvm types + if (desc.charAt(0) == 'T' || desc.indexOf('<') >= 0 || desc.indexOf('>') >= 0) { + throw new IllegalArgumentException("don't use with generic types or templates: " + desc); + } + + this.desc = desc; + } + + public static String parseFirst(String in) { + + if (in == null || in.length() <= 0) { + throw new IllegalArgumentException("No desc to parse, input is empty!"); + } + + // read one desc 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 = TypeDescriptor.parseFirst(in.substring(dim)); + return in.substring(0, dim + arrayType.length()); + } + + throw new IllegalArgumentException("don't know how to parse: " + in); + } + + private static int countArrayDimension(String in) { + int i = 0; + while (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; + } + + public static TypeDescriptor of(String name) { + return new TypeDescriptor("L" + name + ";"); + } + + @Override + public String toString() { + return this.desc; + } + + public boolean isVoid() { + return this.desc.length() == 1 && this.desc.charAt(0) == 'V'; + } + + public boolean isPrimitive() { + return this.desc.length() == 1 && Primitive.get(this.desc.charAt(0)) != null; + } + + public Primitive getPrimitive() { + if (!isPrimitive()) { + throw new IllegalStateException("not a primitive"); + } + return Primitive.get(this.desc.charAt(0)); + } + + public boolean isType() { + return this.desc.charAt(0) == 'L' && this.desc.charAt(this.desc.length() - 1) == ';'; + } + + public ClassEntry getTypeEntry() { + if (isType()) { + String name = this.desc.substring(1, this.desc.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().isType()) { + return getArrayType().getTypeEntry(); + } else { + throw new IllegalStateException("desc doesn't have a class"); + } + } + + public boolean isArray() { + return this.desc.charAt(0) == '['; + } + + public int getArrayDimension() { + if (!isArray()) { + throw new IllegalStateException("not an array"); + } + return countArrayDimension(this.desc); + } + + public TypeDescriptor getArrayType() { + if (!isArray()) { + throw new IllegalStateException("not an array"); + } + return new TypeDescriptor(this.desc.substring(getArrayDimension())); + } + + public boolean containsType() { + return isType() || (isArray() && getArrayType().containsType()); + } + + @Override + public boolean equals(Object other) { + return other instanceof TypeDescriptor && equals((TypeDescriptor) other); + } + + public boolean equals(TypeDescriptor other) { + return this.desc.equals(other.desc); + } + + @Override + public int hashCode() { + return this.desc.hashCode(); + } + + public TypeDescriptor remap(Function remapper) { + String desc = this.desc; + if (isType() || (isArray() && containsType())) { + String replacedName = remapper.apply(this.getTypeEntry().getFullName()); + if (replacedName != null) { + if (this.isType()) { + desc = "L" + replacedName + ";"; + } else { + desc = getArrayPrefix(this.getArrayDimension()) + "L" + replacedName + ";"; + } + } + } + return new TypeDescriptor(desc); + } + + private static String getArrayPrefix(int dimension) { + StringBuilder buf = new StringBuilder(); + for (int i = 0; i < dimension; i++) { + buf.append("["); + } + return buf.toString(); + } + + public int getSize() { + switch (desc.charAt(0)) { + case 'J': + case 'D': + if (desc.length() == 1) { + return 2; + } else { + return 1; + } + default: + return 1; + } + } + + @Override + public Translatable translate(Translator translator, EntryResolver resolver, EntryMap mappings) { + return remap(name -> translator.translate(new ClassEntry(name)).getFullName()); + } + + public enum Primitive { + BYTE('B'), + CHARACTER('C'), + SHORT('S'), + INTEGER('I'), + LONG('J'), + FLOAT('F'), + DOUBLE('D'), + BOOLEAN('Z'); + + private static final Map lookup; + + static { + lookup = Maps.newTreeMap(); + for (Primitive val : values()) { + lookup.put(val.getCode(), val); + } + } + + private char code; + + Primitive(char code) { + this.code = code; + } + + public static Primitive get(char code) { + return lookup.get(code); + } + + public char getCode() { + return this.code; + } + } +} diff --git a/src/main/java/cuchaz/enigma/translation/representation/entry/ClassDefEntry.java b/src/main/java/cuchaz/enigma/translation/representation/entry/ClassDefEntry.java new file mode 100644 index 0000000..b9391b0 --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/representation/entry/ClassDefEntry.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.translation.representation.entry; + +import com.google.common.base.Preconditions; +import com.strobel.assembler.metadata.TypeDefinition; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.representation.AccessFlags; +import cuchaz.enigma.translation.representation.Signature; + +import javax.annotation.Nullable; +import java.util.Arrays; + +public class ClassDefEntry extends ClassEntry implements DefEntry { + private final AccessFlags access; + private final Signature signature; + private final ClassEntry superClass; + private final ClassEntry[] interfaces; + + public ClassDefEntry(String className, Signature signature, AccessFlags access, @Nullable ClassEntry superClass, ClassEntry[] interfaces) { + this(getOuterClass(className), getInnerName(className), signature, access, superClass, interfaces); + } + + public ClassDefEntry(ClassEntry parent, String className, Signature signature, AccessFlags access, @Nullable ClassEntry superClass, ClassEntry[] interfaces) { + super(parent, className); + Preconditions.checkNotNull(signature, "Class signature cannot be null"); + Preconditions.checkNotNull(access, "Class access cannot be null"); + + this.signature = signature; + this.access = access; + this.superClass = superClass; + this.interfaces = interfaces != null ? interfaces : new ClassEntry[0]; + } + + public static ClassDefEntry parse(int access, String name, String signature, String superName, String[] interfaces) { + ClassEntry superClass = superName != null ? new ClassEntry(superName) : null; + ClassEntry[] interfaceClasses = Arrays.stream(interfaces).map(ClassEntry::new).toArray(ClassEntry[]::new); + return new ClassDefEntry(name, Signature.createSignature(signature), new AccessFlags(access), superClass, interfaceClasses); + } + + public static ClassDefEntry parse(TypeDefinition def) { + String name = def.getInternalName(); + Signature signature = Signature.createSignature(def.getSignature()); + AccessFlags access = new AccessFlags(def.getModifiers()); + ClassEntry superClass = def.getBaseType() != null ? ClassEntry.parse(def.getBaseType()) : null; + ClassEntry[] interfaces = def.getExplicitInterfaces().stream().map(ClassEntry::parse).toArray(ClassEntry[]::new); + return new ClassDefEntry(name, signature, access, superClass, interfaces); + } + + public Signature getSignature() { + return signature; + } + + @Override + public AccessFlags getAccess() { + return access; + } + + @Nullable + public ClassEntry getSuperClass() { + return superClass; + } + + public ClassEntry[] getInterfaces() { + return interfaces; + } + + @Override + public ClassDefEntry translate(Translator translator, @Nullable EntryMapping mapping) { + Signature translatedSignature = translator.translate(signature); + String translatedName = mapping != null ? mapping.getTargetName() : name; + AccessFlags translatedAccess = mapping != null ? mapping.getAccessModifier().transform(access) : access; + ClassEntry translatedSuper = translator.translate(superClass); + ClassEntry[] translatedInterfaces = Arrays.stream(interfaces).map(translator::translate).toArray(ClassEntry[]::new); + return new ClassDefEntry(parent, translatedName, translatedSignature, translatedAccess, translatedSuper, translatedInterfaces); + } + + @Override + public ClassDefEntry withParent(ClassEntry parent) { + return new ClassDefEntry(parent, name, signature, access, superClass, interfaces); + } +} diff --git a/src/main/java/cuchaz/enigma/translation/representation/entry/ClassEntry.java b/src/main/java/cuchaz/enigma/translation/representation/entry/ClassEntry.java new file mode 100644 index 0000000..dcbb8d9 --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/representation/entry/ClassEntry.java @@ -0,0 +1,180 @@ +/******************************************************************************* + * 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.translation.representation.entry; + +import com.strobel.assembler.metadata.TypeReference; +import cuchaz.enigma.throwables.IllegalNameException; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.NameValidator; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.Objects; + +public class ClassEntry extends ParentedEntry implements Comparable { + private final String fullName; + + public ClassEntry(String className) { + this(getOuterClass(className), getInnerName(className)); + } + + public ClassEntry(@Nullable ClassEntry parent, String className) { + super(parent, className); + if (parent != null) { + fullName = parent.getFullName() + "$" + name; + } else { + fullName = name; + } + + if (parent == null && className.indexOf('.') >= 0) { + throw new IllegalArgumentException("Class name must be in JVM format. ie, path/to/package/class$inner : " + className); + } + } + + public static ClassEntry parse(TypeReference typeReference) { + return new ClassEntry(typeReference.getInternalName()); + } + + @Override + public Class getParentType() { + return ClassEntry.class; + } + + @Override + public String getName() { + return this.name; + } + + public String getFullName() { + return fullName; + } + + @Override + public ClassEntry translate(Translator translator, @Nullable EntryMapping mapping) { + String translatedName = mapping != null ? mapping.getTargetName() : name; + return new ClassEntry(parent, translatedName); + } + + @Override + public ClassEntry getContainingClass() { + return this; + } + + @Override + public int hashCode() { + return fullName.hashCode(); + } + + @Override + public boolean equals(Object other) { + return other instanceof ClassEntry && equals((ClassEntry) other); + } + + public boolean equals(ClassEntry other) { + return other != null && Objects.equals(parent, other.parent) && this.name.equals(other.name); + } + + @Override + public boolean canConflictWith(Entry entry) { + return true; + } + + @Override + public void validateName(String name) throws IllegalNameException { + NameValidator.validateClassName(name, !isInnerClass()); + } + + @Override + public ClassEntry withParent(ClassEntry parent) { + return new ClassEntry(parent, name); + } + + @Override + public String toString() { + return getFullName(); + } + + public String getPackageName() { + return getPackageName(this.name); + } + + public String getSimpleName() { + int packagePos = name.lastIndexOf('/'); + if (packagePos > 0) { + return name.substring(packagePos + 1); + } + return name; + } + + public boolean isInnerClass() { + return parent != null; + } + + @Nullable + public ClassEntry getOuterClass() { + return parent; + } + + public ClassEntry buildClassEntry(List classChain) { + assert (classChain.contains(this)); + StringBuilder buf = new StringBuilder(); + for (ClassEntry chainEntry : classChain) { + if (buf.length() == 0) { + buf.append(chainEntry.getFullName()); + } else { + buf.append("$"); + buf.append(chainEntry.getSimpleName()); + } + + if (chainEntry == this) { + break; + } + } + return new ClassEntry(buf.toString()); + } + + public boolean isJre() { + String packageName = getPackageName(); + return packageName != null && (packageName.startsWith("java") || packageName.startsWith("javax")); + } + + public static String getPackageName(String name) { + int pos = name.lastIndexOf('/'); + if (pos > 0) { + return name.substring(0, pos); + } + return null; + } + + @Nullable + public static ClassEntry getOuterClass(String name) { + int index = name.lastIndexOf('$'); + if (index >= 0) { + return new ClassEntry(name.substring(0, index)); + } + return null; + } + + public static String getInnerName(String name) { + int innerClassPos = name.lastIndexOf('$'); + if (innerClassPos > 0) { + return name.substring(innerClassPos + 1); + } + return name; + } + + @Override + public int compareTo(ClassEntry entry) { + return name.compareTo(entry.name); + } +} diff --git a/src/main/java/cuchaz/enigma/translation/representation/entry/DefEntry.java b/src/main/java/cuchaz/enigma/translation/representation/entry/DefEntry.java new file mode 100644 index 0000000..82536c7 --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/representation/entry/DefEntry.java @@ -0,0 +1,7 @@ +package cuchaz.enigma.translation.representation.entry; + +import cuchaz.enigma.translation.representation.AccessFlags; + +public interface DefEntry

> extends Entry

{ + AccessFlags getAccess(); +} diff --git a/src/main/java/cuchaz/enigma/translation/representation/entry/Entry.java b/src/main/java/cuchaz/enigma/translation/representation/entry/Entry.java new file mode 100644 index 0000000..1a2ca78 --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/representation/entry/Entry.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.translation.representation.entry; + +import cuchaz.enigma.throwables.IllegalNameException; +import cuchaz.enigma.translation.Translatable; +import cuchaz.enigma.translation.mapping.NameValidator; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; + +public interface Entry

> extends Translatable { + String getName(); + + @Nullable + P getParent(); + + Class

getParentType(); + + Entry

withParent(P parent); + + boolean canConflictWith(Entry entry); + + @Nullable + default ClassEntry getContainingClass() { + P parent = getParent(); + if (parent == null) { + return null; + } + if (parent instanceof ClassEntry) { + return (ClassEntry) parent; + } + return parent.getContainingClass(); + } + + default List> getAncestry() { + P parent = getParent(); + List> entries = new ArrayList<>(); + if (parent != null) { + entries.addAll(parent.getAncestry()); + } + entries.add(this); + return entries; + } + + @Nullable + @SuppressWarnings("unchecked") + default > E findAncestor(Class type) { + List> ancestry = getAncestry(); + for (int i = ancestry.size() - 1; i >= 0; i--) { + Entry ancestor = ancestry.get(i); + if (type.isAssignableFrom(ancestor.getClass())) { + return (E) ancestor; + } + } + return null; + } + + @SuppressWarnings("unchecked") + default > Entry

replaceAncestor(E target, E replacement) { + if (replacement.equals(target)) { + return this; + } + + if (equals(target)) { + return (Entry

) replacement; + } + + P parent = getParent(); + if (parent == null) { + return this; + } + + return withParent((P) parent.replaceAncestor(target, replacement)); + } + + default void validateName(String name) throws IllegalNameException { + NameValidator.validateIdentifier(name); + } + + @SuppressWarnings("unchecked") + @Nullable + default > Entry castParent(Class parentType) { + if (parentType.equals(getParentType())) { + return (Entry) this; + } + return null; + } +} diff --git a/src/main/java/cuchaz/enigma/translation/representation/entry/FieldDefEntry.java b/src/main/java/cuchaz/enigma/translation/representation/entry/FieldDefEntry.java new file mode 100644 index 0000000..d487f71 --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/representation/entry/FieldDefEntry.java @@ -0,0 +1,61 @@ +/******************************************************************************* + * 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.translation.representation.entry; + +import com.google.common.base.Preconditions; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.representation.AccessFlags; +import cuchaz.enigma.translation.representation.Signature; +import cuchaz.enigma.translation.representation.TypeDescriptor; + +import javax.annotation.Nullable; + +public class FieldDefEntry extends FieldEntry implements DefEntry { + private final AccessFlags access; + private final Signature signature; + + public FieldDefEntry(ClassEntry owner, String name, TypeDescriptor desc, Signature signature, AccessFlags access) { + super(owner, name, desc); + Preconditions.checkNotNull(access, "Field access cannot be null"); + Preconditions.checkNotNull(signature, "Field signature cannot be null"); + this.access = access; + this.signature = signature; + } + + public static FieldDefEntry parse(ClassEntry owner, int access, String name, String desc, String signature) { + return new FieldDefEntry(owner, name, new TypeDescriptor(desc), Signature.createTypedSignature(signature), new AccessFlags(access)); + } + + @Override + public AccessFlags getAccess() { + return access; + } + + public Signature getSignature() { + return signature; + } + + @Override + public FieldDefEntry translate(Translator translator, @Nullable EntryMapping mapping) { + TypeDescriptor translatedDesc = translator.translate(desc); + Signature translatedSignature = translator.translate(signature); + String translatedName = mapping != null ? mapping.getTargetName() : name; + AccessFlags translatedAccess = mapping != null ? mapping.getAccessModifier().transform(access) : access; + return new FieldDefEntry(parent, translatedName, translatedDesc, translatedSignature, translatedAccess); + } + + @Override + public FieldDefEntry withParent(ClassEntry owner) { + return new FieldDefEntry(owner, this.name, this.desc, signature, access); + } +} diff --git a/src/main/java/cuchaz/enigma/translation/representation/entry/FieldEntry.java b/src/main/java/cuchaz/enigma/translation/representation/entry/FieldEntry.java new file mode 100644 index 0000000..2ec2471 --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/representation/entry/FieldEntry.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * 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.translation.representation.entry; + +import com.google.common.base.Preconditions; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.representation.TypeDescriptor; +import cuchaz.enigma.utils.Utils; + +import javax.annotation.Nullable; + +public class FieldEntry extends ParentedEntry implements Comparable { + protected final TypeDescriptor desc; + + public FieldEntry(ClassEntry parent, String name, TypeDescriptor desc) { + super(parent, name); + + Preconditions.checkNotNull(parent, "Owner cannot be null"); + Preconditions.checkNotNull(desc, "Field descriptor cannot be null"); + + this.desc = desc; + } + + public static FieldEntry parse(String owner, String name, String desc) { + return new FieldEntry(new ClassEntry(owner), name, new TypeDescriptor(desc)); + } + + @Override + public Class getParentType() { + return ClassEntry.class; + } + + public TypeDescriptor getDesc() { + return this.desc; + } + + @Override + public FieldEntry withParent(ClassEntry parent) { + return new FieldEntry(parent, this.name, this.desc); + } + + @Override + protected FieldEntry translate(Translator translator, @Nullable EntryMapping mapping) { + String translatedName = mapping != null ? mapping.getTargetName() : name; + return new FieldEntry(parent, translatedName, translator.translate(desc)); + } + + @Override + public int hashCode() { + return Utils.combineHashesOrdered(this.parent, this.name, this.desc); + } + + @Override + public boolean equals(Object other) { + return other instanceof FieldEntry && equals((FieldEntry) other); + } + + public boolean equals(FieldEntry other) { + return this.parent.equals(other.parent) && name.equals(other.name) && desc.equals(other.desc); + } + + @Override + public boolean canConflictWith(Entry entry) { + return entry instanceof FieldEntry && ((FieldEntry) entry).parent.equals(parent); + } + + @Override + public String toString() { + return this.parent.getFullName() + "." + this.name + ":" + this.desc; + } + + @Override + public int compareTo(FieldEntry entry) { + return (name + desc.toString()).compareTo(entry.name + entry.desc.toString()); + } +} diff --git a/src/main/java/cuchaz/enigma/translation/representation/entry/LocalVariableDefEntry.java b/src/main/java/cuchaz/enigma/translation/representation/entry/LocalVariableDefEntry.java new file mode 100644 index 0000000..86bdf61 --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/representation/entry/LocalVariableDefEntry.java @@ -0,0 +1,45 @@ +package cuchaz.enigma.translation.representation.entry; + +import com.google.common.base.Preconditions; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.representation.TypeDescriptor; + +import javax.annotation.Nullable; + +/** + * TypeDescriptor... + * Created by Thog + * 19/10/2016 + */ +public class LocalVariableDefEntry extends LocalVariableEntry { + protected final TypeDescriptor desc; + + public LocalVariableDefEntry(MethodEntry ownerEntry, int index, String name, boolean parameter, TypeDescriptor desc) { + super(ownerEntry, index, name, parameter); + Preconditions.checkNotNull(desc, "Variable desc cannot be null"); + + this.desc = desc; + } + + public TypeDescriptor getDesc() { + return desc; + } + + @Override + public LocalVariableDefEntry translate(Translator translator, @Nullable EntryMapping mapping) { + TypeDescriptor translatedDesc = translator.translate(desc); + String translatedName = mapping != null ? mapping.getTargetName() : name; + return new LocalVariableDefEntry(parent, index, translatedName, parameter, translatedDesc); + } + + @Override + public LocalVariableDefEntry withParent(MethodEntry entry) { + return new LocalVariableDefEntry(entry, index, name, parameter, desc); + } + + @Override + public String toString() { + return this.parent + "(" + this.index + ":" + this.name + ":" + this.desc + ")"; + } +} diff --git a/src/main/java/cuchaz/enigma/translation/representation/entry/LocalVariableEntry.java b/src/main/java/cuchaz/enigma/translation/representation/entry/LocalVariableEntry.java new file mode 100644 index 0000000..df96b59 --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/representation/entry/LocalVariableEntry.java @@ -0,0 +1,92 @@ +package cuchaz.enigma.translation.representation.entry; + +import com.google.common.base.Preconditions; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.utils.Utils; + +import javax.annotation.Nullable; + +/** + * TypeDescriptor... + * Created by Thog + * 19/10/2016 + */ +public class LocalVariableEntry extends ParentedEntry implements Comparable { + + protected final int index; + protected final boolean parameter; + + @Deprecated + public LocalVariableEntry(MethodEntry parent, int index, String name) { + this(parent, index, name, true); + } + + public LocalVariableEntry(MethodEntry parent, int index, String name, boolean parameter) { + super(parent, name); + + Preconditions.checkNotNull(parent, "Variable owner cannot be null"); + Preconditions.checkArgument(index >= 0, "Index must be positive"); + + this.index = index; + this.parameter = parameter; + } + + @Override + public Class getParentType() { + return MethodEntry.class; + } + + public boolean isParameter() { + return this.parameter; + } + + public int getIndex() { + return index; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public LocalVariableEntry translate(Translator translator, @Nullable EntryMapping mapping) { + String translatedName = mapping != null ? mapping.getTargetName() : name; + return new LocalVariableEntry(parent, index, translatedName, parameter); + } + + @Override + public LocalVariableEntry withParent(MethodEntry parent) { + return new LocalVariableEntry(parent, index, name, parameter); + } + + @Override + public int hashCode() { + return Utils.combineHashesOrdered(this.parent, this.index); + } + + @Override + public boolean equals(Object other) { + return other instanceof LocalVariableEntry && equals((LocalVariableEntry) other); + } + + public boolean equals(LocalVariableEntry other) { + return this.parent.equals(other.parent) && this.index == other.index; + } + + @Override + public boolean canConflictWith(Entry entry) { + return entry instanceof LocalVariableEntry && ((LocalVariableEntry) entry).parent.equals(parent); + } + + @Override + public String toString() { + return this.parent + "(" + this.index + ":" + this.name + ")"; + } + + @Override + public int compareTo(LocalVariableEntry entry) { + return Integer.compare(index, entry.index); + } +} diff --git a/src/main/java/cuchaz/enigma/translation/representation/entry/MethodDefEntry.java b/src/main/java/cuchaz/enigma/translation/representation/entry/MethodDefEntry.java new file mode 100644 index 0000000..3ecd470 --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/representation/entry/MethodDefEntry.java @@ -0,0 +1,77 @@ +/******************************************************************************* + * 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.translation.representation.entry; + +import com.google.common.base.Preconditions; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.representation.AccessFlags; +import cuchaz.enigma.translation.representation.MethodDescriptor; +import cuchaz.enigma.translation.representation.Signature; + +import javax.annotation.Nullable; + +public class MethodDefEntry extends MethodEntry implements DefEntry { + private final AccessFlags access; + private final Signature signature; + + public MethodDefEntry(ClassEntry owner, String name, MethodDescriptor descriptor, Signature signature, AccessFlags access) { + super(owner, name, descriptor); + Preconditions.checkNotNull(access, "Method access cannot be null"); + Preconditions.checkNotNull(signature, "Method signature cannot be null"); + this.access = access; + this.signature = signature; + } + + public static MethodDefEntry parse(ClassEntry owner, int access, String name, String desc, String signature) { + return new MethodDefEntry(owner, name, new MethodDescriptor(desc), Signature.createSignature(signature), new AccessFlags(access)); + } + + @Override + public AccessFlags getAccess() { + return access; + } + + public Signature getSignature() { + return signature; + } + + @Override + public MethodDefEntry translate(Translator translator, @Nullable EntryMapping mapping) { + MethodDescriptor translatedDesc = translator.translate(descriptor); + Signature translatedSignature = translator.translate(signature); + String translatedName = mapping != null ? mapping.getTargetName() : name; + AccessFlags translatedAccess = mapping != null ? mapping.getAccessModifier().transform(access) : access; + return new MethodDefEntry(parent, translatedName, translatedDesc, translatedSignature, translatedAccess); + } + + @Override + public MethodDefEntry withParent(ClassEntry parent) { + return new MethodDefEntry(new ClassEntry(parent.getFullName()), name, descriptor, signature, access); + } + + public int getArgumentIndex(ClassDefEntry ownerEntry, int localVariableIndex) { + int argumentIndex = localVariableIndex; + + // Enum constructors have an implicit "name" and "ordinal" parameter as well as "this" + if (ownerEntry.getAccess().isEnum() && getName().startsWith("<")) { + argumentIndex -= 2; + } + + // If we're not static, "this" is bound to index 0 + if (!getAccess().isStatic()) { + argumentIndex -= 1; + } + + return argumentIndex; + } +} diff --git a/src/main/java/cuchaz/enigma/translation/representation/entry/MethodEntry.java b/src/main/java/cuchaz/enigma/translation/representation/entry/MethodEntry.java new file mode 100644 index 0000000..3a1dbb3 --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/representation/entry/MethodEntry.java @@ -0,0 +1,95 @@ +/******************************************************************************* + * 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.translation.representation.entry; + +import com.google.common.base.Preconditions; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.representation.MethodDescriptor; +import cuchaz.enigma.utils.Utils; + +import javax.annotation.Nullable; + +public class MethodEntry extends ParentedEntry implements Comparable { + + protected final MethodDescriptor descriptor; + + public MethodEntry(ClassEntry parent, String name, MethodDescriptor descriptor) { + super(parent, name); + + Preconditions.checkNotNull(parent, "Parent cannot be null"); + Preconditions.checkNotNull(descriptor, "Method descriptor cannot be null"); + + this.descriptor = descriptor; + } + + public static MethodEntry parse(String owner, String name, String desc) { + return new MethodEntry(new ClassEntry(owner), name, new MethodDescriptor(desc)); + } + + @Override + public Class getParentType() { + return ClassEntry.class; + } + + public MethodDescriptor getDesc() { + return this.descriptor; + } + + public boolean isConstructor() { + return name.equals("") || name.equals(""); + } + + @Override + public MethodEntry translate(Translator translator, @Nullable EntryMapping mapping) { + String translatedName = mapping != null ? mapping.getTargetName() : name; + return new MethodEntry(parent, translatedName, translator.translate(descriptor)); + } + + @Override + public MethodEntry withParent(ClassEntry parent) { + return new MethodEntry(new ClassEntry(parent.getFullName()), name, descriptor); + } + + @Override + public int hashCode() { + return Utils.combineHashesOrdered(this.parent, this.name, this.descriptor); + } + + @Override + public boolean equals(Object other) { + return other instanceof MethodEntry && equals((MethodEntry) other); + } + + public boolean equals(MethodEntry other) { + return this.parent.equals(other.getParent()) && this.name.equals(other.getName()) && this.descriptor.equals(other.getDesc()); + } + + @Override + public boolean canConflictWith(Entry entry) { + if (entry instanceof MethodEntry) { + MethodEntry methodEntry = (MethodEntry) entry; + return methodEntry.parent.equals(parent) && methodEntry.descriptor.canConflictWith(descriptor); + } + return false; + } + + @Override + public String toString() { + return this.parent.getFullName() + "." + this.name + this.descriptor; + } + + @Override + public int compareTo(MethodEntry entry) { + return (name + descriptor.toString()).compareTo(entry.name + entry.descriptor.toString()); + } +} diff --git a/src/main/java/cuchaz/enigma/translation/representation/entry/ParentedEntry.java b/src/main/java/cuchaz/enigma/translation/representation/entry/ParentedEntry.java new file mode 100644 index 0000000..7ba7c19 --- /dev/null +++ b/src/main/java/cuchaz/enigma/translation/representation/entry/ParentedEntry.java @@ -0,0 +1,71 @@ +/******************************************************************************* + * 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.translation.representation.entry; + +import com.google.common.base.Preconditions; +import cuchaz.enigma.translation.Translatable; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryMap; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.EntryResolver; +import cuchaz.enigma.translation.mapping.ResolutionStrategy; + +import javax.annotation.Nullable; + +public abstract class ParentedEntry

> implements Entry

{ + protected final P parent; + protected final String name; + + protected ParentedEntry(P parent, String name) { + this.parent = parent; + this.name = name; + + Preconditions.checkNotNull(name, "Name cannot be null"); + } + + @Override + public abstract ParentedEntry

withParent(P parent); + + protected abstract ParentedEntry

translate(Translator translator, @Nullable EntryMapping mapping); + + @Override + public String getName() { + return name; + } + + @Override + @Nullable + public P getParent() { + return parent; + } + + @Override + public Translatable translate(Translator translator, EntryResolver resolver, EntryMap mappings) { + P parent = getParent(); + EntryMapping mapping = resolveMapping(resolver, mappings); + if (parent == null) { + return translate(translator, mapping); + } + P translatedParent = translator.translate(parent); + return withParent(translatedParent).translate(translator, mapping); + } + + private EntryMapping resolveMapping(EntryResolver resolver, EntryMap mappings) { + for (ParentedEntry

entry : resolver.resolveEntry(this, ResolutionStrategy.RESOLVE_ROOT)) { + EntryMapping mapping = mappings.get(entry); + if (mapping != null) { + return mapping; + } + } + return null; + } +} -- cgit v1.2.3