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/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 ++++++ 25 files changed, 2068 insertions(+) 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 (limited to 'src/main/java/cuchaz/enigma/translation/mapping') 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(); + } +} -- cgit v1.2.3