diff options
| author | 2019-01-24 14:48:32 +0200 | |
|---|---|---|
| committer | 2019-01-24 13:48:32 +0100 | |
| commit | 00fcd0550fcdda621c2e4662f6ddd55ce673b931 (patch) | |
| tree | 6f9e4c24dbcc6d118fceec56adf7bf9d747a485c /src/main/java/cuchaz/enigma/translation/mapping | |
| parent | mark as 0.13.0-SNAPSHOT for preliminary development (diff) | |
| download | enigma-fork-00fcd0550fcdda621c2e4662f6ddd55ce673b931.tar.gz enigma-fork-00fcd0550fcdda621c2e4662f6ddd55ce673b931.tar.xz enigma-fork-00fcd0550fcdda621c2e4662f6ddd55ce673b931.zip | |
[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
Diffstat (limited to 'src/main/java/cuchaz/enigma/translation/mapping')
25 files changed, 2068 insertions, 0 deletions
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 @@ | |||
| 1 | package cuchaz.enigma.translation.mapping; | ||
| 2 | |||
| 3 | import cuchaz.enigma.translation.representation.AccessFlags; | ||
| 4 | |||
| 5 | public enum AccessModifier { | ||
| 6 | UNCHANGED, PUBLIC, PROTECTED, PRIVATE; | ||
| 7 | |||
| 8 | public String getFormattedName() { | ||
| 9 | return "ACC:" + super.toString(); | ||
| 10 | } | ||
| 11 | |||
| 12 | public AccessFlags transform(AccessFlags access) { | ||
| 13 | switch (this) { | ||
| 14 | case PUBLIC: | ||
| 15 | return access.setPublic(); | ||
| 16 | case PROTECTED: | ||
| 17 | return access.setProtected(); | ||
| 18 | case PRIVATE: | ||
| 19 | return access.setPrivate(); | ||
| 20 | case UNCHANGED: | ||
| 21 | default: | ||
| 22 | return access; | ||
| 23 | } | ||
| 24 | } | ||
| 25 | } | ||
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 @@ | |||
| 1 | package cuchaz.enigma.translation.mapping; | ||
| 2 | |||
| 3 | import cuchaz.enigma.translation.representation.entry.Entry; | ||
| 4 | |||
| 5 | import javax.annotation.Nullable; | ||
| 6 | import java.util.Collection; | ||
| 7 | |||
| 8 | public interface EntryMap<T> { | ||
| 9 | void insert(Entry<?> entry, T value); | ||
| 10 | |||
| 11 | @Nullable | ||
| 12 | T remove(Entry<?> entry); | ||
| 13 | |||
| 14 | @Nullable | ||
| 15 | T get(Entry<?> entry); | ||
| 16 | |||
| 17 | default boolean contains(Entry<?> entry) { | ||
| 18 | return get(entry) != null; | ||
| 19 | } | ||
| 20 | |||
| 21 | Collection<Entry<?>> getAllEntries(); | ||
| 22 | |||
| 23 | boolean isEmpty(); | ||
| 24 | } | ||
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 @@ | |||
| 1 | package cuchaz.enigma.translation.mapping; | ||
| 2 | |||
| 3 | import javax.annotation.Nonnull; | ||
| 4 | |||
| 5 | public class EntryMapping { | ||
| 6 | private final String targetName; | ||
| 7 | private final AccessModifier accessModifier; | ||
| 8 | |||
| 9 | public EntryMapping(@Nonnull String targetName) { | ||
| 10 | this(targetName, AccessModifier.UNCHANGED); | ||
| 11 | } | ||
| 12 | |||
| 13 | public EntryMapping(@Nonnull String targetName, AccessModifier accessModifier) { | ||
| 14 | this.targetName = targetName; | ||
| 15 | this.accessModifier = accessModifier; | ||
| 16 | } | ||
| 17 | |||
| 18 | @Nonnull | ||
| 19 | public String getTargetName() { | ||
| 20 | return targetName; | ||
| 21 | } | ||
| 22 | |||
| 23 | @Nonnull | ||
| 24 | public AccessModifier getAccessModifier() { | ||
| 25 | if (accessModifier == null) { | ||
| 26 | return AccessModifier.UNCHANGED; | ||
| 27 | } | ||
| 28 | return accessModifier; | ||
| 29 | } | ||
| 30 | } | ||
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 @@ | |||
| 1 | package cuchaz.enigma.translation.mapping; | ||
| 2 | |||
| 3 | import cuchaz.enigma.analysis.index.JarIndex; | ||
| 4 | import cuchaz.enigma.translation.MappingTranslator; | ||
| 5 | import cuchaz.enigma.translation.Translatable; | ||
| 6 | import cuchaz.enigma.translation.Translator; | ||
| 7 | import cuchaz.enigma.translation.mapping.tree.DeltaTrackingTree; | ||
| 8 | import cuchaz.enigma.translation.mapping.tree.EntryTree; | ||
| 9 | import cuchaz.enigma.translation.mapping.tree.EntryTreeNode; | ||
| 10 | import cuchaz.enigma.translation.mapping.tree.HashEntryTree; | ||
| 11 | import cuchaz.enigma.translation.representation.entry.Entry; | ||
| 12 | |||
| 13 | import javax.annotation.Nullable; | ||
| 14 | import java.util.Collection; | ||
| 15 | |||
| 16 | public class EntryRemapper { | ||
| 17 | private final EntryTree<EntryMapping> obfToDeobf; | ||
| 18 | private final DeltaTrackingTree<EntryMapping> deobfToObf; | ||
| 19 | |||
| 20 | private final JarIndex obfIndex; | ||
| 21 | |||
| 22 | private final EntryResolver obfResolver; | ||
| 23 | private EntryResolver deobfResolver; | ||
| 24 | |||
| 25 | private final Translator deobfuscator; | ||
| 26 | private Translator obfuscator; | ||
| 27 | |||
| 28 | private final MappingValidator validator; | ||
| 29 | |||
| 30 | private EntryRemapper(JarIndex jarIndex, EntryTree<EntryMapping> obfToDeobf, EntryTree<EntryMapping> deobfToObf) { | ||
| 31 | this.obfToDeobf = obfToDeobf; | ||
| 32 | this.deobfToObf = new DeltaTrackingTree<>(deobfToObf); | ||
| 33 | |||
| 34 | this.obfIndex = jarIndex; | ||
| 35 | this.obfResolver = jarIndex.getEntryResolver(); | ||
| 36 | |||
| 37 | this.deobfuscator = new MappingTranslator(obfToDeobf, obfResolver); | ||
| 38 | rebuildDeobfIndex(); | ||
| 39 | |||
| 40 | this.validator = new MappingValidator(this.deobfToObf, deobfuscator, obfResolver); | ||
| 41 | } | ||
| 42 | |||
| 43 | public EntryRemapper(JarIndex jarIndex) { | ||
| 44 | this(jarIndex, new HashEntryTree<>(), new HashEntryTree<>()); | ||
| 45 | } | ||
| 46 | |||
| 47 | public EntryRemapper(JarIndex jarIndex, EntryTree<EntryMapping> deobfuscationTrees) { | ||
| 48 | this(jarIndex, deobfuscationTrees, inverse(deobfuscationTrees)); | ||
| 49 | } | ||
| 50 | |||
| 51 | private static EntryTree<EntryMapping> inverse(EntryTree<EntryMapping> tree) { | ||
| 52 | Translator translator = new MappingTranslator(tree, VoidEntryResolver.INSTANCE); | ||
| 53 | EntryTree<EntryMapping> inverse = new HashEntryTree<>(); | ||
| 54 | |||
| 55 | // Naive approach, could operate on the nodes of the tree. However, this runs infrequently. | ||
| 56 | Collection<Entry<?>> entries = tree.getAllEntries(); | ||
| 57 | for (Entry<?> sourceEntry : entries) { | ||
| 58 | Entry<?> targetEntry = translator.translate(sourceEntry); | ||
| 59 | inverse.insert(targetEntry, new EntryMapping(sourceEntry.getName())); | ||
| 60 | } | ||
| 61 | |||
| 62 | return inverse; | ||
| 63 | } | ||
| 64 | |||
| 65 | private void rebuildDeobfIndex() { | ||
| 66 | JarIndex deobfIndex = obfIndex.remapped(deobfuscator); | ||
| 67 | |||
| 68 | this.deobfResolver = deobfIndex.getEntryResolver(); | ||
| 69 | this.obfuscator = new MappingTranslator(deobfToObf, deobfResolver); | ||
| 70 | } | ||
| 71 | |||
| 72 | public <E extends Entry<?>> void mapFromObf(E obfuscatedEntry, @Nullable EntryMapping deobfMapping) { | ||
| 73 | Collection<E> resolvedEntries = obfResolver.resolveEntry(obfuscatedEntry, ResolutionStrategy.RESOLVE_ROOT); | ||
| 74 | for (E resolvedEntry : resolvedEntries) { | ||
| 75 | if (deobfMapping != null) { | ||
| 76 | validator.validateRename(resolvedEntry, deobfMapping.getTargetName()); | ||
| 77 | } | ||
| 78 | |||
| 79 | setObfToDeobf(resolvedEntry, deobfMapping); | ||
| 80 | } | ||
| 81 | |||
| 82 | // Temporary hack, not very performant | ||
| 83 | rebuildDeobfIndex(); | ||
| 84 | } | ||
| 85 | |||
| 86 | public <E extends Entry<?>> void mapFromDeobf(E deobfuscatedEntry, @Nullable EntryMapping deobfMapping) { | ||
| 87 | E obfuscatedEntry = obfuscate(deobfuscatedEntry); | ||
| 88 | mapFromObf(obfuscatedEntry, deobfMapping); | ||
| 89 | } | ||
| 90 | |||
| 91 | public void removeByObf(Entry<?> obfuscatedEntry) { | ||
| 92 | mapFromObf(obfuscatedEntry, null); | ||
| 93 | } | ||
| 94 | |||
| 95 | public void removeByDeobf(Entry<?> deobfuscatedEntry) { | ||
| 96 | mapFromObf(obfuscate(deobfuscatedEntry), null); | ||
| 97 | } | ||
| 98 | |||
| 99 | private <E extends Entry<?>> void setObfToDeobf(E obfuscatedEntry, @Nullable EntryMapping deobfMapping) { | ||
| 100 | E prevDeobf = deobfuscate(obfuscatedEntry); | ||
| 101 | obfToDeobf.insert(obfuscatedEntry, deobfMapping); | ||
| 102 | |||
| 103 | E newDeobf = deobfuscate(obfuscatedEntry); | ||
| 104 | |||
| 105 | // Reconstruct the children of this node in the deobf -> obf tree with our new mapping | ||
| 106 | // We only need to do this for deobf -> obf because the obf tree is always consistent on the left hand side | ||
| 107 | // We lookup by obf, and the obf never changes. This is not the case for deobf so we need to update the tree. | ||
| 108 | |||
| 109 | EntryTreeNode<EntryMapping> node = deobfToObf.findNode(prevDeobf); | ||
| 110 | if (node != null) { | ||
| 111 | for (EntryTreeNode<EntryMapping> child : node.getNodesRecursively()) { | ||
| 112 | Entry<?> entry = child.getEntry(); | ||
| 113 | EntryMapping mapping = new EntryMapping(obfuscate(entry).getName()); | ||
| 114 | |||
| 115 | deobfToObf.insert(entry.replaceAncestor(prevDeobf, newDeobf), mapping); | ||
| 116 | deobfToObf.remove(entry); | ||
| 117 | } | ||
| 118 | } else { | ||
| 119 | deobfToObf.insert(newDeobf, new EntryMapping(obfuscatedEntry.getName())); | ||
| 120 | } | ||
| 121 | } | ||
| 122 | |||
| 123 | @Nullable | ||
| 124 | public EntryMapping getDeobfMapping(Entry<?> entry) { | ||
| 125 | return obfToDeobf.get(entry); | ||
| 126 | } | ||
| 127 | |||
| 128 | @Nullable | ||
| 129 | public EntryMapping getObfMapping(Entry<?> entry) { | ||
| 130 | return deobfToObf.get(entry); | ||
| 131 | } | ||
| 132 | |||
| 133 | public boolean hasDeobfMapping(Entry<?> obfEntry) { | ||
| 134 | return obfToDeobf.contains(obfEntry); | ||
| 135 | } | ||
| 136 | |||
| 137 | public boolean hasObfMapping(Entry<?> deobfEntry) { | ||
| 138 | return deobfToObf.contains(deobfEntry); | ||
| 139 | } | ||
| 140 | |||
| 141 | public <T extends Translatable> T deobfuscate(T translatable) { | ||
| 142 | return deobfuscator.translate(translatable); | ||
| 143 | } | ||
| 144 | |||
| 145 | public <T extends Translatable> T obfuscate(T translatable) { | ||
| 146 | return obfuscator.translate(translatable); | ||
| 147 | } | ||
| 148 | |||
| 149 | public Translator getDeobfuscator() { | ||
| 150 | return deobfuscator; | ||
| 151 | } | ||
| 152 | |||
| 153 | public Translator getObfuscator() { | ||
| 154 | return obfuscator; | ||
| 155 | } | ||
| 156 | |||
| 157 | public Collection<Entry<?>> getObfEntries() { | ||
| 158 | return obfToDeobf.getAllEntries(); | ||
| 159 | } | ||
| 160 | |||
| 161 | public Collection<Entry<?>> getObfRootEntries() { | ||
| 162 | return obfToDeobf.getRootEntries(); | ||
| 163 | } | ||
| 164 | |||
| 165 | public Collection<Entry<?>> getDeobfEntries() { | ||
| 166 | return deobfToObf.getAllEntries(); | ||
| 167 | } | ||
| 168 | |||
| 169 | public Collection<Entry<?>> getObfChildren(Entry<?> obfuscatedEntry) { | ||
| 170 | return obfToDeobf.getChildren(obfuscatedEntry); | ||
| 171 | } | ||
| 172 | |||
| 173 | public Collection<Entry<?>> getDeobfChildren(Entry<?> deobfuscatedEntry) { | ||
| 174 | return deobfToObf.getChildren(deobfuscatedEntry); | ||
| 175 | } | ||
| 176 | |||
| 177 | public EntryTree<EntryMapping> getObfToDeobf() { | ||
| 178 | return obfToDeobf; | ||
| 179 | } | ||
| 180 | |||
| 181 | public DeltaTrackingTree<EntryMapping> getDeobfToObf() { | ||
| 182 | return deobfToObf; | ||
| 183 | } | ||
| 184 | |||
| 185 | public MappingDelta takeMappingDelta() { | ||
| 186 | MappingDelta delta = deobfToObf.takeDelta(); | ||
| 187 | return delta.translate(obfuscator, VoidEntryResolver.INSTANCE, deobfToObf); | ||
| 188 | } | ||
| 189 | |||
| 190 | public boolean isDirty() { | ||
| 191 | return deobfToObf.isDirty(); | ||
| 192 | } | ||
| 193 | |||
| 194 | public EntryResolver getObfResolver() { | ||
| 195 | return obfResolver; | ||
| 196 | } | ||
| 197 | |||
| 198 | public EntryResolver getDeobfResolver() { | ||
| 199 | return deobfResolver; | ||
| 200 | } | ||
| 201 | } | ||
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 @@ | |||
| 1 | package cuchaz.enigma.translation.mapping; | ||
| 2 | |||
| 3 | import com.google.common.collect.Streams; | ||
| 4 | import cuchaz.enigma.analysis.EntryReference; | ||
| 5 | import cuchaz.enigma.translation.representation.entry.Entry; | ||
| 6 | import cuchaz.enigma.translation.representation.entry.MethodEntry; | ||
| 7 | |||
| 8 | import java.util.Collection; | ||
| 9 | import java.util.Set; | ||
| 10 | import java.util.stream.Collectors; | ||
| 11 | |||
| 12 | public interface EntryResolver { | ||
| 13 | <E extends Entry<?>> Collection<E> resolveEntry(E entry, ResolutionStrategy strategy); | ||
| 14 | |||
| 15 | default <E extends Entry<?>> E resolveFirstEntry(E entry, ResolutionStrategy strategy) { | ||
| 16 | return resolveEntry(entry, strategy).stream().findFirst().orElse(entry); | ||
| 17 | } | ||
| 18 | |||
| 19 | default <E extends Entry<?>, C extends Entry<?>> Collection<EntryReference<E, C>> resolveReference(EntryReference<E, C> reference, ResolutionStrategy strategy) { | ||
| 20 | Collection<E> entry = resolveEntry(reference.entry, strategy); | ||
| 21 | if (reference.context != null) { | ||
| 22 | Collection<C> context = resolveEntry(reference.context, strategy); | ||
| 23 | return Streams.zip(entry.stream(), context.stream(), (e, c) -> new EntryReference<>(e, c, reference)) | ||
| 24 | .collect(Collectors.toList()); | ||
| 25 | } else { | ||
| 26 | return entry.stream() | ||
| 27 | .map(e -> new EntryReference<>(e, null, reference)) | ||
| 28 | .collect(Collectors.toList()); | ||
| 29 | } | ||
| 30 | } | ||
| 31 | |||
| 32 | default <E extends Entry<?>, C extends Entry<?>> EntryReference<E, C> resolveFirstReference(EntryReference<E, C> reference, ResolutionStrategy strategy) { | ||
| 33 | E entry = resolveFirstEntry(reference.entry, strategy); | ||
| 34 | C context = resolveFirstEntry(reference.context, strategy); | ||
| 35 | return new EntryReference<>(entry, context, reference); | ||
| 36 | } | ||
| 37 | |||
| 38 | Set<Entry<?>> resolveEquivalentEntries(Entry<?> entry); | ||
| 39 | |||
| 40 | Set<MethodEntry> resolveEquivalentMethods(MethodEntry methodEntry); | ||
| 41 | } | ||
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 @@ | |||
| 1 | package cuchaz.enigma.translation.mapping; | ||
| 2 | |||
| 3 | import com.google.common.collect.Sets; | ||
| 4 | import cuchaz.enigma.analysis.IndexTreeBuilder; | ||
| 5 | import cuchaz.enigma.analysis.MethodImplementationsTreeNode; | ||
| 6 | import cuchaz.enigma.analysis.MethodInheritanceTreeNode; | ||
| 7 | import cuchaz.enigma.analysis.index.BridgeMethodIndex; | ||
| 8 | import cuchaz.enigma.analysis.index.EntryIndex; | ||
| 9 | import cuchaz.enigma.analysis.index.InheritanceIndex; | ||
| 10 | import cuchaz.enigma.analysis.index.JarIndex; | ||
| 11 | import cuchaz.enigma.translation.VoidTranslator; | ||
| 12 | import cuchaz.enigma.translation.representation.AccessFlags; | ||
| 13 | import cuchaz.enigma.translation.representation.entry.ClassEntry; | ||
| 14 | import cuchaz.enigma.translation.representation.entry.Entry; | ||
| 15 | import cuchaz.enigma.translation.representation.entry.MethodEntry; | ||
| 16 | |||
| 17 | import javax.annotation.Nullable; | ||
| 18 | import java.util.*; | ||
| 19 | import java.util.stream.Collectors; | ||
| 20 | |||
| 21 | public class IndexEntryResolver implements EntryResolver { | ||
| 22 | private final EntryIndex entryIndex; | ||
| 23 | private final InheritanceIndex inheritanceIndex; | ||
| 24 | private final BridgeMethodIndex bridgeMethodIndex; | ||
| 25 | |||
| 26 | private final IndexTreeBuilder treeBuilder; | ||
| 27 | |||
| 28 | public IndexEntryResolver(JarIndex index) { | ||
| 29 | this.entryIndex = index.getEntryIndex(); | ||
| 30 | this.inheritanceIndex = index.getInheritanceIndex(); | ||
| 31 | this.bridgeMethodIndex = index.getBridgeMethodIndex(); | ||
| 32 | |||
| 33 | this.treeBuilder = new IndexTreeBuilder(index); | ||
| 34 | } | ||
| 35 | |||
| 36 | @Override | ||
| 37 | @SuppressWarnings("unchecked") | ||
| 38 | public <E extends Entry<?>> Collection<E> resolveEntry(E entry, ResolutionStrategy strategy) { | ||
| 39 | if (entry == null) { | ||
| 40 | return Collections.emptySet(); | ||
| 41 | } | ||
| 42 | |||
| 43 | Entry<ClassEntry> classChild = getClassChild(entry); | ||
| 44 | if (classChild != null && !(classChild instanceof ClassEntry)) { | ||
| 45 | AccessFlags access = entryIndex.getEntryAccess(classChild); | ||
| 46 | |||
| 47 | // If we're looking for the closest and this entry exists, we're done looking | ||
| 48 | if (strategy == ResolutionStrategy.RESOLVE_CLOSEST && access != null) { | ||
| 49 | return Collections.singleton(entry); | ||
| 50 | } | ||
| 51 | |||
| 52 | if (access == null || !access.isPrivate()) { | ||
| 53 | Collection<Entry<ClassEntry>> resolvedChildren = resolveChildEntry(classChild, strategy); | ||
| 54 | if (!resolvedChildren.isEmpty()) { | ||
| 55 | return resolvedChildren.stream() | ||
| 56 | .map(resolvedChild -> (E) entry.replaceAncestor(classChild, resolvedChild)) | ||
| 57 | .collect(Collectors.toList()); | ||
| 58 | } | ||
| 59 | } | ||
| 60 | } | ||
| 61 | |||
| 62 | return Collections.singleton(entry); | ||
| 63 | } | ||
| 64 | |||
| 65 | @Nullable | ||
| 66 | private Entry<ClassEntry> getClassChild(Entry<?> entry) { | ||
| 67 | if (entry instanceof ClassEntry) { | ||
| 68 | return null; | ||
| 69 | } | ||
| 70 | |||
| 71 | // get the entry in the hierarchy that is the child of a class | ||
| 72 | List<Entry<?>> ancestry = entry.getAncestry(); | ||
| 73 | for (int i = ancestry.size() - 1; i > 0; i--) { | ||
| 74 | Entry<?> child = ancestry.get(i); | ||
| 75 | Entry<ClassEntry> cast = child.castParent(ClassEntry.class); | ||
| 76 | if (cast != null && !(cast instanceof ClassEntry)) { | ||
| 77 | // we found the entry which is a child of a class, we are now able to resolve the owner of this entry | ||
| 78 | return cast; | ||
| 79 | } | ||
| 80 | } | ||
| 81 | |||
| 82 | return null; | ||
| 83 | } | ||
| 84 | |||
| 85 | private Set<Entry<ClassEntry>> resolveChildEntry(Entry<ClassEntry> entry, ResolutionStrategy strategy) { | ||
| 86 | ClassEntry ownerClass = entry.getParent(); | ||
| 87 | |||
| 88 | if (entry instanceof MethodEntry) { | ||
| 89 | MethodEntry bridgeMethod = bridgeMethodIndex.getBridgeFromAccessed((MethodEntry) entry); | ||
| 90 | if (bridgeMethod != null && ownerClass.equals(bridgeMethod.getParent())) { | ||
| 91 | Set<Entry<ClassEntry>> resolvedBridge = resolveChildEntry(bridgeMethod, strategy); | ||
| 92 | if (!resolvedBridge.isEmpty()) { | ||
| 93 | return resolvedBridge; | ||
| 94 | } | ||
| 95 | } | ||
| 96 | } | ||
| 97 | |||
| 98 | Set<Entry<ClassEntry>> resolvedEntries = new HashSet<>(); | ||
| 99 | |||
| 100 | for (ClassEntry parentClass : inheritanceIndex.getParents(ownerClass)) { | ||
| 101 | Entry<ClassEntry> parentEntry = entry.withParent(parentClass); | ||
| 102 | |||
| 103 | if (strategy == ResolutionStrategy.RESOLVE_ROOT) { | ||
| 104 | resolvedEntries.addAll(resolveRoot(parentEntry, strategy)); | ||
| 105 | } else { | ||
| 106 | resolvedEntries.addAll(resolveClosest(parentEntry, strategy)); | ||
| 107 | } | ||
| 108 | } | ||
| 109 | |||
| 110 | return resolvedEntries; | ||
| 111 | } | ||
| 112 | |||
| 113 | private Collection<Entry<ClassEntry>> resolveRoot(Entry<ClassEntry> entry, ResolutionStrategy strategy) { | ||
| 114 | // When resolving root, we want to first look for the lowest entry before returning ourselves | ||
| 115 | Set<Entry<ClassEntry>> parentResolution = resolveChildEntry(entry, strategy); | ||
| 116 | |||
| 117 | if (parentResolution.isEmpty()) { | ||
| 118 | AccessFlags parentAccess = entryIndex.getEntryAccess(entry); | ||
| 119 | if (parentAccess != null && !parentAccess.isPrivate()) { | ||
| 120 | return Collections.singleton(entry); | ||
| 121 | } | ||
| 122 | } | ||
| 123 | |||
| 124 | return parentResolution; | ||
| 125 | } | ||
| 126 | |||
| 127 | private Collection<Entry<ClassEntry>> resolveClosest(Entry<ClassEntry> entry, ResolutionStrategy strategy) { | ||
| 128 | // When resolving closest, we want to first check if we exist before looking further down | ||
| 129 | AccessFlags parentAccess = entryIndex.getEntryAccess(entry); | ||
| 130 | if (parentAccess != null && !parentAccess.isPrivate()) { | ||
| 131 | return Collections.singleton(entry); | ||
| 132 | } else { | ||
| 133 | return resolveChildEntry(entry, strategy); | ||
| 134 | } | ||
| 135 | } | ||
| 136 | |||
| 137 | @Override | ||
| 138 | public Set<Entry<?>> resolveEquivalentEntries(Entry<?> entry) { | ||
| 139 | MethodEntry relevantMethod = entry.findAncestor(MethodEntry.class); | ||
| 140 | if (relevantMethod == null || !entryIndex.hasMethod(relevantMethod)) { | ||
| 141 | return Collections.singleton(entry); | ||
| 142 | } | ||
| 143 | |||
| 144 | Set<MethodEntry> equivalentMethods = resolveEquivalentMethods(relevantMethod); | ||
| 145 | Set<Entry<?>> equivalentEntries = new HashSet<>(equivalentMethods.size()); | ||
| 146 | |||
| 147 | for (MethodEntry equivalentMethod : equivalentMethods) { | ||
| 148 | Entry<?> equivalentEntry = entry.replaceAncestor(relevantMethod, equivalentMethod); | ||
| 149 | equivalentEntries.add(equivalentEntry); | ||
| 150 | } | ||
| 151 | |||
| 152 | return equivalentEntries; | ||
| 153 | } | ||
| 154 | |||
| 155 | @Override | ||
| 156 | public Set<MethodEntry> resolveEquivalentMethods(MethodEntry methodEntry) { | ||
| 157 | AccessFlags access = entryIndex.getMethodAccess(methodEntry); | ||
| 158 | if (access == null) { | ||
| 159 | throw new IllegalArgumentException("Could not find method " + methodEntry); | ||
| 160 | } | ||
| 161 | |||
| 162 | if (!canInherit(methodEntry, access)) { | ||
| 163 | return Collections.singleton(methodEntry); | ||
| 164 | } | ||
| 165 | |||
| 166 | Set<MethodEntry> methodEntries = Sets.newHashSet(); | ||
| 167 | resolveEquivalentMethods(methodEntries, treeBuilder.buildMethodInheritance(VoidTranslator.INSTANCE, methodEntry)); | ||
| 168 | return methodEntries; | ||
| 169 | } | ||
| 170 | |||
| 171 | private void resolveEquivalentMethods(Set<MethodEntry> methodEntries, MethodInheritanceTreeNode node) { | ||
| 172 | MethodEntry methodEntry = node.getMethodEntry(); | ||
| 173 | if (methodEntries.contains(methodEntry)) { | ||
| 174 | return; | ||
| 175 | } | ||
| 176 | |||
| 177 | AccessFlags flags = entryIndex.getMethodAccess(methodEntry); | ||
| 178 | if (flags != null && canInherit(methodEntry, flags)) { | ||
| 179 | // collect the entry | ||
| 180 | methodEntries.add(methodEntry); | ||
| 181 | } | ||
| 182 | |||
| 183 | // look at bridge methods! | ||
| 184 | MethodEntry bridgedMethod = bridgeMethodIndex.getBridgeFromAccessed(methodEntry); | ||
| 185 | while (bridgedMethod != null) { | ||
| 186 | methodEntries.addAll(resolveEquivalentMethods(bridgedMethod)); | ||
| 187 | bridgedMethod = bridgeMethodIndex.getBridgeFromAccessed(bridgedMethod); | ||
| 188 | } | ||
| 189 | |||
| 190 | // look at interface methods too | ||
| 191 | for (MethodImplementationsTreeNode implementationsNode : treeBuilder.buildMethodImplementations(VoidTranslator.INSTANCE, methodEntry)) { | ||
| 192 | resolveEquivalentMethods(methodEntries, implementationsNode); | ||
| 193 | } | ||
| 194 | |||
| 195 | // recurse | ||
| 196 | for (int i = 0; i < node.getChildCount(); i++) { | ||
| 197 | resolveEquivalentMethods(methodEntries, (MethodInheritanceTreeNode) node.getChildAt(i)); | ||
| 198 | } | ||
| 199 | } | ||
| 200 | |||
| 201 | private void resolveEquivalentMethods(Set<MethodEntry> methodEntries, MethodImplementationsTreeNode node) { | ||
| 202 | MethodEntry methodEntry = node.getMethodEntry(); | ||
| 203 | AccessFlags flags = entryIndex.getMethodAccess(methodEntry); | ||
| 204 | if (flags != null && !flags.isPrivate() && !flags.isStatic()) { | ||
| 205 | // collect the entry | ||
| 206 | methodEntries.add(methodEntry); | ||
| 207 | } | ||
| 208 | |||
| 209 | // look at bridge methods! | ||
| 210 | MethodEntry bridgedMethod = bridgeMethodIndex.getBridgeFromAccessed(methodEntry); | ||
| 211 | while (bridgedMethod != null) { | ||
| 212 | methodEntries.addAll(resolveEquivalentMethods(bridgedMethod)); | ||
| 213 | bridgedMethod = bridgeMethodIndex.getBridgeFromAccessed(bridgedMethod); | ||
| 214 | } | ||
| 215 | |||
| 216 | // recurse | ||
| 217 | for (int i = 0; i < node.getChildCount(); i++) { | ||
| 218 | resolveEquivalentMethods(methodEntries, (MethodImplementationsTreeNode) node.getChildAt(i)); | ||
| 219 | } | ||
| 220 | } | ||
| 221 | |||
| 222 | private boolean canInherit(MethodEntry entry, AccessFlags access) { | ||
| 223 | return !entry.isConstructor() && !access.isPrivate() && !access.isStatic() && !access.isFinal(); | ||
| 224 | } | ||
| 225 | } | ||
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 @@ | |||
| 1 | package cuchaz.enigma.translation.mapping; | ||
| 2 | |||
| 3 | import cuchaz.enigma.translation.Translatable; | ||
| 4 | import cuchaz.enigma.translation.Translator; | ||
| 5 | import cuchaz.enigma.translation.mapping.tree.HashEntryTree; | ||
| 6 | import cuchaz.enigma.translation.mapping.tree.EntryTree; | ||
| 7 | import cuchaz.enigma.translation.representation.entry.Entry; | ||
| 8 | |||
| 9 | public class MappingDelta implements Translatable { | ||
| 10 | public static final Object PLACEHOLDER = new Object(); | ||
| 11 | |||
| 12 | private final EntryTree<Object> additions; | ||
| 13 | private final EntryTree<Object> deletions; | ||
| 14 | |||
| 15 | public MappingDelta(EntryTree<Object> additions, EntryTree<Object> deletions) { | ||
| 16 | this.additions = additions; | ||
| 17 | this.deletions = deletions; | ||
| 18 | } | ||
| 19 | |||
| 20 | public MappingDelta() { | ||
| 21 | this(new HashEntryTree<>(), new HashEntryTree<>()); | ||
| 22 | } | ||
| 23 | |||
| 24 | public static MappingDelta added(EntryTree<EntryMapping> mappings) { | ||
| 25 | EntryTree<Object> additions = new HashEntryTree<>(); | ||
| 26 | for (Entry<?> entry : mappings.getAllEntries()) { | ||
| 27 | additions.insert(entry, PLACEHOLDER); | ||
| 28 | } | ||
| 29 | |||
| 30 | return new MappingDelta(additions, new HashEntryTree<>()); | ||
| 31 | } | ||
| 32 | |||
| 33 | public EntryTree<?> getAdditions() { | ||
| 34 | return additions; | ||
| 35 | } | ||
| 36 | |||
| 37 | public EntryTree<?> getDeletions() { | ||
| 38 | return deletions; | ||
| 39 | } | ||
| 40 | |||
| 41 | @Override | ||
| 42 | public MappingDelta translate(Translator translator, EntryResolver resolver, EntryMap<EntryMapping> mappings) { | ||
| 43 | return new MappingDelta( | ||
| 44 | translate(translator, additions), | ||
| 45 | translate(translator, deletions) | ||
| 46 | ); | ||
| 47 | } | ||
| 48 | |||
| 49 | private EntryTree<Object> translate(Translator translator, EntryTree<Object> tree) { | ||
| 50 | EntryTree<Object> translatedTree = new HashEntryTree<>(); | ||
| 51 | for (Entry<?> entry : tree.getAllEntries()) { | ||
| 52 | translatedTree.insert(translator.translate(entry), PLACEHOLDER); | ||
| 53 | } | ||
| 54 | return translatedTree; | ||
| 55 | } | ||
| 56 | } | ||
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 @@ | |||
| 1 | package cuchaz.enigma.translation.mapping; | ||
| 2 | |||
| 3 | import cuchaz.enigma.translation.representation.entry.Entry; | ||
| 4 | |||
| 5 | import javax.annotation.Nullable; | ||
| 6 | |||
| 7 | public class MappingPair<E extends Entry<?>, M> { | ||
| 8 | private final E entry; | ||
| 9 | private final M mapping; | ||
| 10 | |||
| 11 | public MappingPair(E entry, @Nullable M mapping) { | ||
| 12 | this.entry = entry; | ||
| 13 | this.mapping = mapping; | ||
| 14 | } | ||
| 15 | |||
| 16 | public MappingPair(E entry) { | ||
| 17 | this(entry, null); | ||
| 18 | } | ||
| 19 | |||
| 20 | public E getEntry() { | ||
| 21 | return entry; | ||
| 22 | } | ||
| 23 | |||
| 24 | @Nullable | ||
| 25 | public M getMapping() { | ||
| 26 | return mapping; | ||
| 27 | } | ||
| 28 | } | ||
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 @@ | |||
| 1 | package cuchaz.enigma.translation.mapping; | ||
| 2 | |||
| 3 | import cuchaz.enigma.throwables.IllegalNameException; | ||
| 4 | import cuchaz.enigma.translation.Translator; | ||
| 5 | import cuchaz.enigma.translation.mapping.tree.EntryTree; | ||
| 6 | import cuchaz.enigma.translation.representation.entry.Entry; | ||
| 7 | |||
| 8 | import java.util.Collection; | ||
| 9 | |||
| 10 | public class MappingValidator { | ||
| 11 | private final EntryTree<EntryMapping> deobfToObf; | ||
| 12 | private final Translator deobfuscator; | ||
| 13 | private final EntryResolver entryResolver; | ||
| 14 | |||
| 15 | public MappingValidator(EntryTree<EntryMapping> deobfToObf, Translator deobfuscator, EntryResolver entryResolver) { | ||
| 16 | this.deobfToObf = deobfToObf; | ||
| 17 | this.deobfuscator = deobfuscator; | ||
| 18 | this.entryResolver = entryResolver; | ||
| 19 | } | ||
| 20 | |||
| 21 | public void validateRename(Entry<?> entry, String name) throws IllegalNameException { | ||
| 22 | Collection<Entry<?>> equivalentEntries = entryResolver.resolveEquivalentEntries(entry); | ||
| 23 | for (Entry<?> equivalentEntry : equivalentEntries) { | ||
| 24 | equivalentEntry.validateName(name); | ||
| 25 | validateUnique(equivalentEntry, name); | ||
| 26 | } | ||
| 27 | } | ||
| 28 | |||
| 29 | private void validateUnique(Entry<?> entry, String name) { | ||
| 30 | Entry<?> translatedEntry = deobfuscator.translate(entry); | ||
| 31 | Collection<Entry<?>> siblings = deobfToObf.getSiblings(translatedEntry); | ||
| 32 | if (!isUnique(translatedEntry, siblings, name)) { | ||
| 33 | throw new IllegalNameException(name, "Name is not unique in " + translatedEntry.getParent() + "!"); | ||
| 34 | } | ||
| 35 | } | ||
| 36 | |||
| 37 | private boolean isUnique(Entry<?> entry, Collection<Entry<?>> siblings, String name) { | ||
| 38 | for (Entry<?> child : siblings) { | ||
| 39 | if (entry.canConflictWith(child) && child.getName().equals(name)) { | ||
| 40 | return false; | ||
| 41 | } | ||
| 42 | } | ||
| 43 | return true; | ||
| 44 | } | ||
| 45 | } | ||
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 @@ | |||
| 1 | /******************************************************************************* | ||
| 2 | * Copyright (c) 2015 Jeff Martin. | ||
| 3 | * All rights reserved. This program and the accompanying materials | ||
| 4 | * are made available under the terms of the GNU Lesser General Public | ||
| 5 | * License v3.0 which accompanies this distribution, and is available at | ||
| 6 | * http://www.gnu.org/licenses/lgpl.html | ||
| 7 | * <p> | ||
| 8 | * Contributors: | ||
| 9 | * Jeff Martin - initial API and implementation | ||
| 10 | ******************************************************************************/ | ||
| 11 | |||
| 12 | package cuchaz.enigma.translation.mapping; | ||
| 13 | |||
| 14 | import cuchaz.enigma.analysis.index.JarIndex; | ||
| 15 | import cuchaz.enigma.translation.mapping.tree.EntryTree; | ||
| 16 | import cuchaz.enigma.translation.mapping.tree.EntryTreeNode; | ||
| 17 | import cuchaz.enigma.translation.representation.entry.ClassEntry; | ||
| 18 | import cuchaz.enigma.translation.representation.entry.Entry; | ||
| 19 | import cuchaz.enigma.translation.representation.entry.FieldEntry; | ||
| 20 | import cuchaz.enigma.translation.representation.entry.MethodEntry; | ||
| 21 | |||
| 22 | import java.util.Collection; | ||
| 23 | import java.util.HashMap; | ||
| 24 | import java.util.Map; | ||
| 25 | |||
| 26 | public class MappingsChecker { | ||
| 27 | private final JarIndex index; | ||
| 28 | private final EntryTree<EntryMapping> mappings; | ||
| 29 | |||
| 30 | public MappingsChecker(JarIndex index, EntryTree<EntryMapping> mappings) { | ||
| 31 | this.index = index; | ||
| 32 | this.mappings = mappings; | ||
| 33 | } | ||
| 34 | |||
| 35 | public Dropped dropBrokenMappings() { | ||
| 36 | Dropped dropped = new Dropped(); | ||
| 37 | |||
| 38 | Collection<Entry<?>> obfEntries = mappings.getAllEntries(); | ||
| 39 | for (Entry<?> entry : obfEntries) { | ||
| 40 | if (entry instanceof ClassEntry || entry instanceof MethodEntry || entry instanceof FieldEntry) { | ||
| 41 | tryDropEntry(dropped, entry); | ||
| 42 | } | ||
| 43 | } | ||
| 44 | |||
| 45 | dropped.apply(mappings); | ||
| 46 | |||
| 47 | return dropped; | ||
| 48 | } | ||
| 49 | |||
| 50 | private void tryDropEntry(Dropped dropped, Entry<?> entry) { | ||
| 51 | if (shouldDropEntry(entry)) { | ||
| 52 | EntryMapping mapping = mappings.get(entry); | ||
| 53 | if (mapping != null) { | ||
| 54 | dropped.drop(entry, mapping); | ||
| 55 | } | ||
| 56 | } | ||
| 57 | } | ||
| 58 | |||
| 59 | private boolean shouldDropEntry(Entry<?> entry) { | ||
| 60 | if (!index.getEntryIndex().hasEntry(entry)) { | ||
| 61 | return true; | ||
| 62 | } | ||
| 63 | Collection<Entry<?>> resolvedEntries = index.getEntryResolver().resolveEntry(entry, ResolutionStrategy.RESOLVE_ROOT); | ||
| 64 | return !resolvedEntries.contains(entry); | ||
| 65 | } | ||
| 66 | |||
| 67 | public static class Dropped { | ||
| 68 | private final Map<Entry<?>, String> droppedMappings = new HashMap<>(); | ||
| 69 | |||
| 70 | public void drop(Entry<?> entry, EntryMapping mapping) { | ||
| 71 | droppedMappings.put(entry, mapping.getTargetName()); | ||
| 72 | } | ||
| 73 | |||
| 74 | void apply(EntryTree<EntryMapping> mappings) { | ||
| 75 | for (Entry<?> entry : droppedMappings.keySet()) { | ||
| 76 | EntryTreeNode<EntryMapping> node = mappings.findNode(entry); | ||
| 77 | if (node == null) { | ||
| 78 | continue; | ||
| 79 | } | ||
| 80 | |||
| 81 | for (Entry<?> childEntry : node.getChildrenRecursively()) { | ||
| 82 | mappings.remove(childEntry); | ||
| 83 | } | ||
| 84 | } | ||
| 85 | } | ||
| 86 | |||
| 87 | public Map<Entry<?>, String> getDroppedMappings() { | ||
| 88 | return droppedMappings; | ||
| 89 | } | ||
| 90 | } | ||
| 91 | } | ||
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 @@ | |||
| 1 | /******************************************************************************* | ||
| 2 | * Copyright (c) 2015 Jeff Martin. | ||
| 3 | * All rights reserved. This program and the accompanying materials | ||
| 4 | * are made available under the terms of the GNU Lesser General Public | ||
| 5 | * License v3.0 which accompanies this distribution, and is available at | ||
| 6 | * http://www.gnu.org/licenses/lgpl.html | ||
| 7 | * <p> | ||
| 8 | * Contributors: | ||
| 9 | * Jeff Martin - initial API and implementation | ||
| 10 | ******************************************************************************/ | ||
| 11 | |||
| 12 | package cuchaz.enigma.translation.mapping; | ||
| 13 | |||
| 14 | import cuchaz.enigma.throwables.IllegalNameException; | ||
| 15 | import cuchaz.enigma.translation.representation.entry.ClassEntry; | ||
| 16 | |||
| 17 | import java.util.Arrays; | ||
| 18 | import java.util.List; | ||
| 19 | import java.util.regex.Pattern; | ||
| 20 | |||
| 21 | public class NameValidator { | ||
| 22 | private static final Pattern IDENTIFIER_PATTERN; | ||
| 23 | private static final Pattern CLASS_PATTERN; | ||
| 24 | private static final List<String> ILLEGAL_IDENTIFIERS = Arrays.asList( | ||
| 25 | "abstract", "continue", "for", "new", "switch", "assert", "default", "goto", "package", "synchronized", | ||
| 26 | "boolean", "do", "if", "private", "this", "break", "double", "implements", "protected", "throw", "byte", | ||
| 27 | "else", "import", "public", "throws", "case", "enum", "instanceof", "return", "transient", "catch", | ||
| 28 | "extends", "int", "short", "try", "char", "final", "interface", "static", "void", "class", "finally", | ||
| 29 | "long", "strictfp", "volatile", "const", "float", "native", "super", "while" | ||
| 30 | ); | ||
| 31 | |||
| 32 | static { | ||
| 33 | String identifierRegex = "[A-Za-z_<][A-Za-z0-9_>]*"; | ||
| 34 | IDENTIFIER_PATTERN = Pattern.compile(identifierRegex); | ||
| 35 | CLASS_PATTERN = Pattern.compile(String.format("^(%s(\\.|/))*(%s)$", identifierRegex, identifierRegex)); | ||
| 36 | } | ||
| 37 | |||
| 38 | public static void validateClassName(String name, boolean packageRequired) { | ||
| 39 | if (!CLASS_PATTERN.matcher(name).matches() || ILLEGAL_IDENTIFIERS.contains(name)) { | ||
| 40 | throw new IllegalNameException(name, "This doesn't look like a legal class name"); | ||
| 41 | } | ||
| 42 | if (packageRequired && ClassEntry.getPackageName(name) == null) { | ||
| 43 | throw new IllegalNameException(name, "Class must be in a package"); | ||
| 44 | } | ||
| 45 | } | ||
| 46 | |||
| 47 | public static void validateIdentifier(String name) { | ||
| 48 | if (!IDENTIFIER_PATTERN.matcher(name).matches() || ILLEGAL_IDENTIFIERS.contains(name)) { | ||
| 49 | throw new IllegalNameException(name, "This doesn't look like a legal identifier"); | ||
| 50 | } | ||
| 51 | } | ||
| 52 | |||
| 53 | public static boolean isReserved(String name) { | ||
| 54 | return ILLEGAL_IDENTIFIERS.contains(name); | ||
| 55 | } | ||
| 56 | } | ||
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 @@ | |||
| 1 | package cuchaz.enigma.translation.mapping; | ||
| 2 | |||
| 3 | public enum ResolutionStrategy { | ||
| 4 | RESOLVE_ROOT, | ||
| 5 | RESOLVE_CLOSEST | ||
| 6 | } | ||
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 @@ | |||
| 1 | package cuchaz.enigma.translation.mapping; | ||
| 2 | |||
| 3 | import cuchaz.enigma.translation.representation.entry.Entry; | ||
| 4 | import cuchaz.enigma.translation.representation.entry.MethodEntry; | ||
| 5 | |||
| 6 | import java.util.Collection; | ||
| 7 | import java.util.Collections; | ||
| 8 | import java.util.Set; | ||
| 9 | |||
| 10 | public enum VoidEntryResolver implements EntryResolver { | ||
| 11 | INSTANCE; | ||
| 12 | |||
| 13 | @Override | ||
| 14 | public <E extends Entry<?>> Collection<E> resolveEntry(E entry, ResolutionStrategy strategy) { | ||
| 15 | return Collections.singleton(entry); | ||
| 16 | } | ||
| 17 | |||
| 18 | @Override | ||
| 19 | public Set<Entry<?>> resolveEquivalentEntries(Entry<?> entry) { | ||
| 20 | return Collections.singleton(entry); | ||
| 21 | } | ||
| 22 | |||
| 23 | @Override | ||
| 24 | public Set<MethodEntry> resolveEquivalentMethods(MethodEntry methodEntry) { | ||
| 25 | return Collections.singleton(methodEntry); | ||
| 26 | } | ||
| 27 | } | ||
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 @@ | |||
| 1 | package cuchaz.enigma.translation.mapping.serde; | ||
| 2 | |||
| 3 | import com.google.common.base.Charsets; | ||
| 4 | import cuchaz.enigma.throwables.MappingParseException; | ||
| 5 | import cuchaz.enigma.translation.mapping.AccessModifier; | ||
| 6 | import cuchaz.enigma.translation.mapping.EntryMapping; | ||
| 7 | import cuchaz.enigma.translation.mapping.MappingPair; | ||
| 8 | import cuchaz.enigma.translation.mapping.tree.HashEntryTree; | ||
| 9 | import cuchaz.enigma.translation.mapping.tree.EntryTree; | ||
| 10 | import cuchaz.enigma.translation.representation.MethodDescriptor; | ||
| 11 | import cuchaz.enigma.translation.representation.TypeDescriptor; | ||
| 12 | import cuchaz.enigma.translation.representation.entry.*; | ||
| 13 | |||
| 14 | import javax.annotation.Nullable; | ||
| 15 | import java.io.IOException; | ||
| 16 | import java.nio.file.Files; | ||
| 17 | import java.nio.file.Path; | ||
| 18 | import java.util.ArrayDeque; | ||
| 19 | import java.util.Deque; | ||
| 20 | import java.util.List; | ||
| 21 | import java.util.Locale; | ||
| 22 | import java.util.stream.Collectors; | ||
| 23 | |||
| 24 | public enum EnigmaMappingsReader implements MappingsReader { | ||
| 25 | FILE { | ||
| 26 | @Override | ||
| 27 | public EntryTree<EntryMapping> read(Path path) throws IOException, MappingParseException { | ||
| 28 | EntryTree<EntryMapping> mappings = new HashEntryTree<>(); | ||
| 29 | readFile(path, mappings); | ||
| 30 | return mappings; | ||
| 31 | } | ||
| 32 | }, | ||
| 33 | DIRECTORY { | ||
| 34 | @Override | ||
| 35 | public EntryTree<EntryMapping> read(Path path) throws IOException, MappingParseException { | ||
| 36 | EntryTree<EntryMapping> mappings = new HashEntryTree<>(); | ||
| 37 | |||
| 38 | List<Path> files = Files.walk(path) | ||
| 39 | .filter(f -> !Files.isDirectory(f)) | ||
| 40 | .filter(f -> f.toString().endsWith(".mapping")) | ||
| 41 | .collect(Collectors.toList()); | ||
| 42 | for (Path file : files) { | ||
| 43 | if (Files.isHidden(file)) { | ||
| 44 | continue; | ||
| 45 | } | ||
| 46 | readFile(file, mappings); | ||
| 47 | } | ||
| 48 | |||
| 49 | return mappings; | ||
| 50 | } | ||
| 51 | }; | ||
| 52 | |||
| 53 | protected void readFile(Path path, EntryTree<EntryMapping> mappings) throws IOException, MappingParseException { | ||
| 54 | List<String> lines = Files.readAllLines(path, Charsets.UTF_8); | ||
| 55 | Deque<Entry<?>> mappingStack = new ArrayDeque<>(); | ||
| 56 | |||
| 57 | for (int lineNumber = 0; lineNumber < lines.size(); lineNumber++) { | ||
| 58 | String line = lines.get(lineNumber); | ||
| 59 | int indentation = countIndentation(line); | ||
| 60 | |||
| 61 | line = formatLine(line); | ||
| 62 | if (line == null) { | ||
| 63 | continue; | ||
| 64 | } | ||
| 65 | |||
| 66 | while (indentation < mappingStack.size()) { | ||
| 67 | mappingStack.pop(); | ||
| 68 | } | ||
| 69 | |||
| 70 | try { | ||
| 71 | MappingPair<?, EntryMapping> pair = parseLine(mappingStack.peek(), line); | ||
| 72 | mappingStack.push(pair.getEntry()); | ||
| 73 | if (pair.getMapping() != null) { | ||
| 74 | mappings.insert(pair.getEntry(), pair.getMapping()); | ||
| 75 | } | ||
| 76 | } catch (Throwable t) { | ||
| 77 | t.printStackTrace(); | ||
| 78 | throw new MappingParseException(path::toString, lineNumber, t.toString()); | ||
| 79 | } | ||
| 80 | } | ||
| 81 | } | ||
| 82 | |||
| 83 | @Nullable | ||
| 84 | private String formatLine(String line) { | ||
| 85 | line = stripComment(line); | ||
| 86 | line = line.trim(); | ||
| 87 | |||
| 88 | if (line.isEmpty()) { | ||
| 89 | return null; | ||
| 90 | } | ||
| 91 | |||
| 92 | return line; | ||
| 93 | } | ||
| 94 | |||
| 95 | private String stripComment(String line) { | ||
| 96 | int commentPos = line.indexOf('#'); | ||
| 97 | if (commentPos >= 0) { | ||
| 98 | return line.substring(0, commentPos); | ||
| 99 | } | ||
| 100 | return line; | ||
| 101 | } | ||
| 102 | |||
| 103 | private int countIndentation(String line) { | ||
| 104 | int indent = 0; | ||
| 105 | for (int i = 0; i < line.length(); i++) { | ||
| 106 | if (line.charAt(i) != '\t') { | ||
| 107 | break; | ||
| 108 | } | ||
| 109 | indent++; | ||
| 110 | } | ||
| 111 | return indent; | ||
| 112 | } | ||
| 113 | |||
| 114 | private MappingPair<?, EntryMapping> parseLine(@Nullable Entry<?> parent, String line) { | ||
| 115 | String[] tokens = line.trim().split("\\s"); | ||
| 116 | String keyToken = tokens[0].toLowerCase(Locale.ROOT); | ||
| 117 | |||
| 118 | switch (keyToken) { | ||
| 119 | case "class": | ||
| 120 | return parseClass(parent, tokens); | ||
| 121 | case "field": | ||
| 122 | return parseField(parent, tokens); | ||
| 123 | case "method": | ||
| 124 | return parseMethod(parent, tokens); | ||
| 125 | case "arg": | ||
| 126 | return parseArgument(parent, tokens); | ||
| 127 | default: | ||
| 128 | throw new RuntimeException("Unknown token '" + keyToken + "'"); | ||
| 129 | } | ||
| 130 | } | ||
| 131 | |||
| 132 | private MappingPair<ClassEntry, EntryMapping> parseClass(@Nullable Entry<?> parent, String[] tokens) { | ||
| 133 | String obfuscatedName = ClassEntry.getInnerName(tokens[1]); | ||
| 134 | ClassEntry obfuscatedEntry; | ||
| 135 | if (parent instanceof ClassEntry) { | ||
| 136 | obfuscatedEntry = new ClassEntry((ClassEntry) parent, obfuscatedName); | ||
| 137 | } else { | ||
| 138 | obfuscatedEntry = new ClassEntry(obfuscatedName); | ||
| 139 | } | ||
| 140 | |||
| 141 | String mapping = null; | ||
| 142 | AccessModifier modifier = AccessModifier.UNCHANGED; | ||
| 143 | |||
| 144 | if (tokens.length == 3) { | ||
| 145 | AccessModifier parsedModifier = parseModifier(tokens[2]); | ||
| 146 | if (parsedModifier != null) { | ||
| 147 | modifier = parsedModifier; | ||
| 148 | mapping = obfuscatedName; | ||
| 149 | } else { | ||
| 150 | mapping = tokens[2]; | ||
| 151 | } | ||
| 152 | } else if (tokens.length == 4) { | ||
| 153 | mapping = tokens[2]; | ||
| 154 | modifier = parseModifier(tokens[3]); | ||
| 155 | } | ||
| 156 | |||
| 157 | if (mapping != null) { | ||
| 158 | return new MappingPair<>(obfuscatedEntry, new EntryMapping(mapping, modifier)); | ||
| 159 | } else { | ||
| 160 | return new MappingPair<>(obfuscatedEntry); | ||
| 161 | } | ||
| 162 | } | ||
| 163 | |||
| 164 | private MappingPair<FieldEntry, EntryMapping> parseField(@Nullable Entry<?> parent, String[] tokens) { | ||
| 165 | if (!(parent instanceof ClassEntry)) { | ||
| 166 | throw new RuntimeException("Field must be a child of a class!"); | ||
| 167 | } | ||
| 168 | |||
| 169 | ClassEntry ownerEntry = (ClassEntry) parent; | ||
| 170 | |||
| 171 | String obfuscatedName = tokens[1]; | ||
| 172 | String mapping = obfuscatedName; | ||
| 173 | AccessModifier modifier = AccessModifier.UNCHANGED; | ||
| 174 | TypeDescriptor descriptor; | ||
| 175 | |||
| 176 | if (tokens.length == 4) { | ||
| 177 | AccessModifier parsedModifier = parseModifier(tokens[3]); | ||
| 178 | if (parsedModifier != null) { | ||
| 179 | descriptor = new TypeDescriptor(tokens[2]); | ||
| 180 | modifier = parsedModifier; | ||
| 181 | } else { | ||
| 182 | mapping = tokens[2]; | ||
| 183 | descriptor = new TypeDescriptor(tokens[3]); | ||
| 184 | } | ||
| 185 | } else if (tokens.length == 5) { | ||
| 186 | descriptor = new TypeDescriptor(tokens[3]); | ||
| 187 | mapping = tokens[2]; | ||
| 188 | modifier = parseModifier(tokens[4]); | ||
| 189 | } else { | ||
| 190 | throw new RuntimeException("Invalid method declaration"); | ||
| 191 | } | ||
| 192 | |||
| 193 | FieldEntry obfuscatedEntry = new FieldEntry(ownerEntry, obfuscatedName, descriptor); | ||
| 194 | if (mapping != null) { | ||
| 195 | return new MappingPair<>(obfuscatedEntry, new EntryMapping(mapping, modifier)); | ||
| 196 | } else { | ||
| 197 | return new MappingPair<>(obfuscatedEntry); | ||
| 198 | } | ||
| 199 | } | ||
| 200 | |||
| 201 | private MappingPair<MethodEntry, EntryMapping> parseMethod(@Nullable Entry<?> parent, String[] tokens) { | ||
| 202 | if (!(parent instanceof ClassEntry)) { | ||
| 203 | throw new RuntimeException("Method must be a child of a class!"); | ||
| 204 | } | ||
| 205 | |||
| 206 | ClassEntry ownerEntry = (ClassEntry) parent; | ||
| 207 | |||
| 208 | String obfuscatedName = tokens[1]; | ||
| 209 | String mapping = null; | ||
| 210 | AccessModifier modifier = AccessModifier.UNCHANGED; | ||
| 211 | MethodDescriptor descriptor; | ||
| 212 | |||
| 213 | if (tokens.length == 3) { | ||
| 214 | descriptor = new MethodDescriptor(tokens[2]); | ||
| 215 | } else if (tokens.length == 4) { | ||
| 216 | AccessModifier parsedModifier = parseModifier(tokens[3]); | ||
| 217 | if (parsedModifier != null) { | ||
| 218 | modifier = parsedModifier; | ||
| 219 | mapping = obfuscatedName; | ||
| 220 | descriptor = new MethodDescriptor(tokens[2]); | ||
| 221 | } else { | ||
| 222 | mapping = tokens[2]; | ||
| 223 | descriptor = new MethodDescriptor(tokens[3]); | ||
| 224 | } | ||
| 225 | } else if (tokens.length == 5) { | ||
| 226 | mapping = tokens[2]; | ||
| 227 | modifier = parseModifier(tokens[4]); | ||
| 228 | descriptor = new MethodDescriptor(tokens[3]); | ||
| 229 | } else { | ||
| 230 | throw new RuntimeException("Invalid method declaration"); | ||
| 231 | } | ||
| 232 | |||
| 233 | MethodEntry obfuscatedEntry = new MethodEntry(ownerEntry, obfuscatedName, descriptor); | ||
| 234 | if (mapping != null) { | ||
| 235 | return new MappingPair<>(obfuscatedEntry, new EntryMapping(mapping, modifier)); | ||
| 236 | } else { | ||
| 237 | return new MappingPair<>(obfuscatedEntry); | ||
| 238 | } | ||
| 239 | } | ||
| 240 | |||
| 241 | private MappingPair<LocalVariableEntry, EntryMapping> parseArgument(@Nullable Entry<?> parent, String[] tokens) { | ||
| 242 | if (!(parent instanceof MethodEntry)) { | ||
| 243 | throw new RuntimeException("Method arg must be a child of a method!"); | ||
| 244 | } | ||
| 245 | |||
| 246 | MethodEntry ownerEntry = (MethodEntry) parent; | ||
| 247 | LocalVariableEntry obfuscatedEntry = new LocalVariableEntry(ownerEntry, Integer.parseInt(tokens[1]), "", true); | ||
| 248 | String mapping = tokens[2]; | ||
| 249 | |||
| 250 | return new MappingPair<>(obfuscatedEntry, new EntryMapping(mapping)); | ||
| 251 | } | ||
| 252 | |||
| 253 | @Nullable | ||
| 254 | private AccessModifier parseModifier(String token) { | ||
| 255 | if (token.startsWith("ACC:")) { | ||
| 256 | return AccessModifier.valueOf(token.substring(4)); | ||
| 257 | } | ||
| 258 | return null; | ||
| 259 | } | ||
| 260 | } | ||
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 @@ | |||
| 1 | /******************************************************************************* | ||
| 2 | * Copyright (c) 2015 Jeff Martin. | ||
| 3 | * All rights reserved. This program and the accompanying materials | ||
| 4 | * are made available under the terms of the GNU Lesser General Public | ||
| 5 | * License v3.0 which accompanies this distribution, and is available at | ||
| 6 | * http://www.gnu.org/licenses/lgpl.html | ||
| 7 | * | ||
| 8 | * Contributors: | ||
| 9 | * Jeff Martin - initial API and implementation | ||
| 10 | ******************************************************************************/ | ||
| 11 | |||
| 12 | package cuchaz.enigma.translation.mapping.serde; | ||
| 13 | |||
| 14 | import cuchaz.enigma.ProgressListener; | ||
| 15 | import cuchaz.enigma.translation.MappingTranslator; | ||
| 16 | import cuchaz.enigma.translation.Translator; | ||
| 17 | import cuchaz.enigma.translation.mapping.AccessModifier; | ||
| 18 | import cuchaz.enigma.translation.mapping.EntryMapping; | ||
| 19 | import cuchaz.enigma.translation.mapping.MappingDelta; | ||
| 20 | import cuchaz.enigma.translation.mapping.VoidEntryResolver; | ||
| 21 | import cuchaz.enigma.translation.mapping.tree.EntryTree; | ||
| 22 | import cuchaz.enigma.translation.mapping.tree.EntryTreeNode; | ||
| 23 | import cuchaz.enigma.translation.representation.entry.*; | ||
| 24 | |||
| 25 | import java.io.IOException; | ||
| 26 | import java.io.PrintWriter; | ||
| 27 | import java.nio.file.DirectoryStream; | ||
| 28 | import java.nio.file.Files; | ||
| 29 | import java.nio.file.Path; | ||
| 30 | import java.nio.file.Paths; | ||
| 31 | import java.util.ArrayList; | ||
| 32 | import java.util.Collection; | ||
| 33 | import java.util.concurrent.atomic.AtomicInteger; | ||
| 34 | import java.util.stream.Collectors; | ||
| 35 | |||
| 36 | public enum EnigmaMappingsWriter implements MappingsWriter { | ||
| 37 | FILE { | ||
| 38 | @Override | ||
| 39 | public void write(EntryTree<EntryMapping> mappings, MappingDelta delta, Path path, ProgressListener progress) { | ||
| 40 | Collection<ClassEntry> classes = mappings.getRootEntries().stream() | ||
| 41 | .filter(entry -> entry instanceof ClassEntry) | ||
| 42 | .map(entry -> (ClassEntry) entry) | ||
| 43 | .collect(Collectors.toList()); | ||
| 44 | |||
| 45 | progress.init(classes.size(), "Writing classes"); | ||
| 46 | |||
| 47 | int steps = 0; | ||
| 48 | try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter(path))) { | ||
| 49 | for (ClassEntry classEntry : classes) { | ||
| 50 | progress.step(steps++, classEntry.getFullName()); | ||
| 51 | writeRoot(writer, mappings, classEntry); | ||
| 52 | } | ||
| 53 | } catch (IOException e) { | ||
| 54 | e.printStackTrace(); | ||
| 55 | } | ||
| 56 | } | ||
| 57 | }, | ||
| 58 | DIRECTORY { | ||
| 59 | @Override | ||
| 60 | public void write(EntryTree<EntryMapping> mappings, MappingDelta delta, Path path, ProgressListener progress) { | ||
| 61 | applyDeletions(delta.getDeletions(), path); | ||
| 62 | |||
| 63 | Collection<ClassEntry> classes = delta.getAdditions().getRootEntries().stream() | ||
| 64 | .filter(entry -> entry instanceof ClassEntry) | ||
| 65 | .map(entry -> (ClassEntry) entry) | ||
| 66 | .collect(Collectors.toList()); | ||
| 67 | |||
| 68 | progress.init(classes.size(), "Writing classes"); | ||
| 69 | |||
| 70 | Translator translator = new MappingTranslator(mappings, VoidEntryResolver.INSTANCE); | ||
| 71 | AtomicInteger steps = new AtomicInteger(); | ||
| 72 | |||
| 73 | classes.parallelStream().forEach(classEntry -> { | ||
| 74 | progress.step(steps.getAndIncrement(), classEntry.getFullName()); | ||
| 75 | |||
| 76 | try { | ||
| 77 | Path classPath = resolve(path, translator.translate(classEntry)); | ||
| 78 | Files.deleteIfExists(classPath); | ||
| 79 | Files.createDirectories(classPath.getParent()); | ||
| 80 | |||
| 81 | try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter(classPath))) { | ||
| 82 | writeRoot(writer, mappings, classEntry); | ||
| 83 | } | ||
| 84 | } catch (Throwable t) { | ||
| 85 | System.err.println("Failed to write class '" + classEntry.getFullName() + "'"); | ||
| 86 | t.printStackTrace(); | ||
| 87 | } | ||
| 88 | }); | ||
| 89 | } | ||
| 90 | |||
| 91 | private void applyDeletions(EntryTree<?> deletions, Path root) { | ||
| 92 | Collection<ClassEntry> deletedClasses = deletions.getRootEntries().stream() | ||
| 93 | .filter(e -> e instanceof ClassEntry) | ||
| 94 | .map(e -> (ClassEntry) e) | ||
| 95 | .collect(Collectors.toList()); | ||
| 96 | |||
| 97 | for (ClassEntry classEntry : deletedClasses) { | ||
| 98 | try { | ||
| 99 | Files.deleteIfExists(resolve(root, classEntry)); | ||
| 100 | } catch (IOException e) { | ||
| 101 | System.err.println("Failed to delete deleted class '" + classEntry + "'"); | ||
| 102 | e.printStackTrace(); | ||
| 103 | } | ||
| 104 | } | ||
| 105 | |||
| 106 | for (ClassEntry classEntry : deletedClasses) { | ||
| 107 | String packageName = classEntry.getPackageName(); | ||
| 108 | if (packageName != null) { | ||
| 109 | Path packagePath = Paths.get(packageName); | ||
| 110 | try { | ||
| 111 | deleteDeadPackages(root, packagePath); | ||
| 112 | } catch (IOException e) { | ||
| 113 | System.err.println("Failed to delete dead package '" + packageName + "'"); | ||
| 114 | e.printStackTrace(); | ||
| 115 | } | ||
| 116 | } | ||
| 117 | } | ||
| 118 | } | ||
| 119 | |||
| 120 | private void deleteDeadPackages(Path root, Path packagePath) throws IOException { | ||
| 121 | for (int i = packagePath.getNameCount() - 1; i >= 0; i--) { | ||
| 122 | Path subPath = packagePath.subpath(0, i + 1); | ||
| 123 | Path packagePart = root.resolve(subPath); | ||
| 124 | if (isEmpty(packagePart)) { | ||
| 125 | Files.deleteIfExists(packagePart); | ||
| 126 | } | ||
| 127 | } | ||
| 128 | } | ||
| 129 | |||
| 130 | private boolean isEmpty(Path path) { | ||
| 131 | try (DirectoryStream<Path> stream = Files.newDirectoryStream(path)) { | ||
| 132 | return !stream.iterator().hasNext(); | ||
| 133 | } catch (IOException e) { | ||
| 134 | return false; | ||
| 135 | } | ||
| 136 | } | ||
| 137 | |||
| 138 | private Path resolve(Path root, ClassEntry classEntry) { | ||
| 139 | return root.resolve(classEntry.getFullName() + ".mapping"); | ||
| 140 | } | ||
| 141 | }; | ||
| 142 | |||
| 143 | protected void writeRoot(PrintWriter writer, EntryTree<EntryMapping> mappings, ClassEntry classEntry) { | ||
| 144 | Collection<Entry<?>> children = groupChildren(mappings.getChildren(classEntry)); | ||
| 145 | |||
| 146 | writer.println(writeClass(classEntry, mappings.get(classEntry)).trim()); | ||
| 147 | for (Entry<?> child : children) { | ||
| 148 | writeEntry(writer, mappings, child, 1); | ||
| 149 | } | ||
| 150 | } | ||
| 151 | |||
| 152 | protected void writeEntry(PrintWriter writer, EntryTree<EntryMapping> mappings, Entry<?> entry, int depth) { | ||
| 153 | EntryTreeNode<EntryMapping> node = mappings.findNode(entry); | ||
| 154 | if (node == null) { | ||
| 155 | return; | ||
| 156 | } | ||
| 157 | |||
| 158 | EntryMapping mapping = node.getValue(); | ||
| 159 | if (entry instanceof ClassEntry) { | ||
| 160 | String line = writeClass((ClassEntry) entry, mapping); | ||
| 161 | writer.println(indent(line, depth)); | ||
| 162 | } else if (entry instanceof MethodEntry) { | ||
| 163 | String line = writeMethod((MethodEntry) entry, mapping); | ||
| 164 | writer.println(indent(line, depth)); | ||
| 165 | } else if (entry instanceof FieldEntry) { | ||
| 166 | String line = writeField((FieldEntry) entry, mapping); | ||
| 167 | writer.println(indent(line, depth)); | ||
| 168 | } else if (entry instanceof LocalVariableEntry) { | ||
| 169 | String line = writeArgument((LocalVariableEntry) entry, mapping); | ||
| 170 | writer.println(indent(line, depth)); | ||
| 171 | } | ||
| 172 | |||
| 173 | Collection<Entry<?>> children = groupChildren(node.getChildren()); | ||
| 174 | for (Entry<?> child : children) { | ||
| 175 | writeEntry(writer, mappings, child, depth + 1); | ||
| 176 | } | ||
| 177 | } | ||
| 178 | |||
| 179 | private Collection<Entry<?>> groupChildren(Collection<Entry<?>> children) { | ||
| 180 | Collection<Entry<?>> result = new ArrayList<>(children.size()); | ||
| 181 | |||
| 182 | children.stream().filter(e -> e instanceof ClassEntry) | ||
| 183 | .map(e -> (ClassEntry) e) | ||
| 184 | .sorted() | ||
| 185 | .forEach(result::add); | ||
| 186 | |||
| 187 | children.stream().filter(e -> e instanceof FieldEntry) | ||
| 188 | .map(e -> (FieldEntry) e) | ||
| 189 | .sorted() | ||
| 190 | .forEach(result::add); | ||
| 191 | |||
| 192 | children.stream().filter(e -> e instanceof MethodEntry) | ||
| 193 | .map(e -> (MethodEntry) e) | ||
| 194 | .sorted() | ||
| 195 | .forEach(result::add); | ||
| 196 | |||
| 197 | children.stream().filter(e -> e instanceof LocalVariableEntry) | ||
| 198 | .map(e -> (LocalVariableEntry) e) | ||
| 199 | .sorted() | ||
| 200 | .forEach(result::add); | ||
| 201 | |||
| 202 | return result; | ||
| 203 | } | ||
| 204 | |||
| 205 | protected String writeClass(ClassEntry entry, EntryMapping mapping) { | ||
| 206 | StringBuilder builder = new StringBuilder("CLASS "); | ||
| 207 | builder.append(entry.getFullName()).append(' '); | ||
| 208 | writeMapping(builder, mapping); | ||
| 209 | |||
| 210 | return builder.toString(); | ||
| 211 | } | ||
| 212 | |||
| 213 | protected String writeMethod(MethodEntry entry, EntryMapping mapping) { | ||
| 214 | StringBuilder builder = new StringBuilder("METHOD "); | ||
| 215 | builder.append(entry.getName()).append(' '); | ||
| 216 | writeMapping(builder, mapping); | ||
| 217 | |||
| 218 | builder.append(entry.getDesc().toString()); | ||
| 219 | |||
| 220 | return builder.toString(); | ||
| 221 | } | ||
| 222 | |||
| 223 | protected String writeField(FieldEntry entry, EntryMapping mapping) { | ||
| 224 | StringBuilder builder = new StringBuilder("FIELD "); | ||
| 225 | builder.append(entry.getName()).append(' '); | ||
| 226 | writeMapping(builder, mapping); | ||
| 227 | |||
| 228 | builder.append(entry.getDesc().toString()); | ||
| 229 | |||
| 230 | return builder.toString(); | ||
| 231 | } | ||
| 232 | |||
| 233 | protected String writeArgument(LocalVariableEntry entry, EntryMapping mapping) { | ||
| 234 | StringBuilder builder = new StringBuilder("ARG "); | ||
| 235 | builder.append(entry.getIndex()).append(' '); | ||
| 236 | |||
| 237 | String mappedName = mapping != null ? mapping.getTargetName() : entry.getName(); | ||
| 238 | builder.append(mappedName); | ||
| 239 | |||
| 240 | return builder.toString(); | ||
| 241 | } | ||
| 242 | |||
| 243 | private void writeMapping(StringBuilder builder, EntryMapping mapping) { | ||
| 244 | if (mapping != null) { | ||
| 245 | builder.append(mapping.getTargetName()).append(' '); | ||
| 246 | if (mapping.getAccessModifier() != AccessModifier.UNCHANGED) { | ||
| 247 | builder.append(mapping.getAccessModifier().getFormattedName()).append(' '); | ||
| 248 | } | ||
| 249 | } | ||
| 250 | } | ||
| 251 | |||
| 252 | private String indent(String line, int depth) { | ||
| 253 | StringBuilder builder = new StringBuilder(); | ||
| 254 | for (int i = 0; i < depth; i++) { | ||
| 255 | builder.append("\t"); | ||
| 256 | } | ||
| 257 | builder.append(line.trim()); | ||
| 258 | return builder.toString(); | ||
| 259 | } | ||
| 260 | } | ||
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 @@ | |||
| 1 | package cuchaz.enigma.translation.mapping.serde; | ||
| 2 | |||
| 3 | import cuchaz.enigma.ProgressListener; | ||
| 4 | import cuchaz.enigma.throwables.MappingParseException; | ||
| 5 | import cuchaz.enigma.translation.mapping.EntryMapping; | ||
| 6 | import cuchaz.enigma.translation.mapping.MappingDelta; | ||
| 7 | import cuchaz.enigma.translation.mapping.tree.EntryTree; | ||
| 8 | |||
| 9 | import javax.annotation.Nullable; | ||
| 10 | import java.io.IOException; | ||
| 11 | import java.nio.file.Path; | ||
| 12 | |||
| 13 | public enum MappingFormat { | ||
| 14 | ENIGMA_FILE(EnigmaMappingsWriter.FILE, EnigmaMappingsReader.FILE), | ||
| 15 | ENIGMA_DIRECTORY(EnigmaMappingsWriter.DIRECTORY, EnigmaMappingsReader.DIRECTORY), | ||
| 16 | TINY_FILE(null, TinyMappingsReader.INSTANCE), | ||
| 17 | SRG_FILE(SrgMappingsWriter.INSTANCE, null); | ||
| 18 | |||
| 19 | private final MappingsWriter writer; | ||
| 20 | private final MappingsReader reader; | ||
| 21 | |||
| 22 | MappingFormat(MappingsWriter writer, MappingsReader reader) { | ||
| 23 | this.writer = writer; | ||
| 24 | this.reader = reader; | ||
| 25 | } | ||
| 26 | |||
| 27 | public void write(EntryTree<EntryMapping> mappings, Path path, ProgressListener progressListener) { | ||
| 28 | write(mappings, MappingDelta.added(mappings), path, progressListener); | ||
| 29 | } | ||
| 30 | |||
| 31 | public void write(EntryTree<EntryMapping> mappings, MappingDelta delta, Path path, ProgressListener progressListener) { | ||
| 32 | if (writer == null) { | ||
| 33 | throw new IllegalStateException(name() + " does not support writing"); | ||
| 34 | } | ||
| 35 | writer.write(mappings, delta, path, progressListener); | ||
| 36 | } | ||
| 37 | |||
| 38 | public EntryTree<EntryMapping> read(Path path) throws IOException, MappingParseException { | ||
| 39 | if (reader == null) { | ||
| 40 | throw new IllegalStateException(name() + " does not support reading"); | ||
| 41 | } | ||
| 42 | return reader.read(path); | ||
| 43 | } | ||
| 44 | |||
| 45 | @Nullable | ||
| 46 | public MappingsWriter getWriter() { | ||
| 47 | return writer; | ||
| 48 | } | ||
| 49 | |||
| 50 | @Nullable | ||
| 51 | public MappingsReader getReader() { | ||
| 52 | return reader; | ||
| 53 | } | ||
| 54 | } | ||
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 @@ | |||
| 1 | package cuchaz.enigma.translation.mapping.serde; | ||
| 2 | |||
| 3 | import cuchaz.enigma.throwables.MappingParseException; | ||
| 4 | import cuchaz.enigma.translation.mapping.EntryMapping; | ||
| 5 | import cuchaz.enigma.translation.mapping.tree.EntryTree; | ||
| 6 | |||
| 7 | import java.io.IOException; | ||
| 8 | import java.nio.file.Path; | ||
| 9 | |||
| 10 | public interface MappingsReader { | ||
| 11 | EntryTree<EntryMapping> read(Path path) throws MappingParseException, IOException; | ||
| 12 | } | ||
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 @@ | |||
| 1 | package cuchaz.enigma.translation.mapping.serde; | ||
| 2 | |||
| 3 | import cuchaz.enigma.ProgressListener; | ||
| 4 | import cuchaz.enigma.translation.mapping.EntryMapping; | ||
| 5 | import cuchaz.enigma.translation.mapping.MappingDelta; | ||
| 6 | import cuchaz.enigma.translation.mapping.tree.EntryTree; | ||
| 7 | |||
| 8 | import java.nio.file.Path; | ||
| 9 | |||
| 10 | public interface MappingsWriter { | ||
| 11 | void write(EntryTree<EntryMapping> mappings, MappingDelta delta, Path path, ProgressListener progress); | ||
| 12 | } | ||
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 @@ | |||
| 1 | package cuchaz.enigma.translation.mapping.serde; | ||
| 2 | |||
| 3 | import com.google.common.collect.Lists; | ||
| 4 | import cuchaz.enigma.ProgressListener; | ||
| 5 | import cuchaz.enigma.translation.MappingTranslator; | ||
| 6 | import cuchaz.enigma.translation.Translator; | ||
| 7 | import cuchaz.enigma.translation.mapping.EntryMapping; | ||
| 8 | import cuchaz.enigma.translation.mapping.MappingDelta; | ||
| 9 | import cuchaz.enigma.translation.mapping.VoidEntryResolver; | ||
| 10 | import cuchaz.enigma.translation.mapping.tree.EntryTree; | ||
| 11 | import cuchaz.enigma.translation.mapping.tree.EntryTreeNode; | ||
| 12 | import cuchaz.enigma.translation.representation.entry.ClassEntry; | ||
| 13 | import cuchaz.enigma.translation.representation.entry.Entry; | ||
| 14 | import cuchaz.enigma.translation.representation.entry.FieldEntry; | ||
| 15 | import cuchaz.enigma.translation.representation.entry.MethodEntry; | ||
| 16 | |||
| 17 | import java.io.IOException; | ||
| 18 | import java.io.PrintWriter; | ||
| 19 | import java.nio.file.Files; | ||
| 20 | import java.nio.file.Path; | ||
| 21 | import java.util.ArrayList; | ||
| 22 | import java.util.Collection; | ||
| 23 | import java.util.Comparator; | ||
| 24 | import java.util.List; | ||
| 25 | import java.util.stream.Collectors; | ||
| 26 | |||
| 27 | public enum SrgMappingsWriter implements MappingsWriter { | ||
| 28 | INSTANCE; | ||
| 29 | |||
| 30 | @Override | ||
| 31 | public void write(EntryTree<EntryMapping> mappings, MappingDelta delta, Path path, ProgressListener progress) { | ||
| 32 | try { | ||
| 33 | Files.deleteIfExists(path); | ||
| 34 | Files.createFile(path); | ||
| 35 | } catch (IOException e) { | ||
| 36 | e.printStackTrace(); | ||
| 37 | } | ||
| 38 | |||
| 39 | List<String> classLines = new ArrayList<>(); | ||
| 40 | List<String> fieldLines = new ArrayList<>(); | ||
| 41 | List<String> methodLines = new ArrayList<>(); | ||
| 42 | |||
| 43 | Collection<Entry<?>> rootEntries = Lists.newArrayList(mappings).stream() | ||
| 44 | .map(EntryTreeNode::getEntry) | ||
| 45 | .collect(Collectors.toList()); | ||
| 46 | progress.init(rootEntries.size(), "Generating mappings"); | ||
| 47 | |||
| 48 | int steps = 0; | ||
| 49 | for (Entry<?> entry : sorted(rootEntries)) { | ||
| 50 | progress.step(steps++, entry.getName()); | ||
| 51 | writeEntry(classLines, fieldLines, methodLines, mappings, entry); | ||
| 52 | } | ||
| 53 | |||
| 54 | progress.init(3, "Writing mappings"); | ||
| 55 | try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter(path))) { | ||
| 56 | progress.step(0, "Classes"); | ||
| 57 | classLines.forEach(writer::println); | ||
| 58 | progress.step(1, "Fields"); | ||
| 59 | fieldLines.forEach(writer::println); | ||
| 60 | progress.step(2, "Methods"); | ||
| 61 | methodLines.forEach(writer::println); | ||
| 62 | } catch (IOException e) { | ||
| 63 | e.printStackTrace(); | ||
| 64 | } | ||
| 65 | } | ||
| 66 | |||
| 67 | private void writeEntry(List<String> classes, List<String> fields, List<String> methods, EntryTree<EntryMapping> mappings, Entry<?> entry) { | ||
| 68 | EntryTreeNode<EntryMapping> node = mappings.findNode(entry); | ||
| 69 | if (node == null) { | ||
| 70 | return; | ||
| 71 | } | ||
| 72 | |||
| 73 | Translator translator = new MappingTranslator(mappings, VoidEntryResolver.INSTANCE); | ||
| 74 | if (entry instanceof ClassEntry) { | ||
| 75 | classes.add(generateClassLine((ClassEntry) entry, translator)); | ||
| 76 | } else if (entry instanceof FieldEntry) { | ||
| 77 | fields.add(generateFieldLine((FieldEntry) entry, translator)); | ||
| 78 | } else if (entry instanceof MethodEntry) { | ||
| 79 | methods.add(generateMethodLine((MethodEntry) entry, translator)); | ||
| 80 | } | ||
| 81 | |||
| 82 | for (Entry<?> child : sorted(node.getChildren())) { | ||
| 83 | writeEntry(classes, fields, methods, mappings, child); | ||
| 84 | } | ||
| 85 | } | ||
| 86 | |||
| 87 | private String generateClassLine(ClassEntry sourceEntry, Translator translator) { | ||
| 88 | ClassEntry targetEntry = translator.translate(sourceEntry); | ||
| 89 | return "CL: " + sourceEntry.getFullName() + " " + targetEntry.getFullName(); | ||
| 90 | } | ||
| 91 | |||
| 92 | private String generateMethodLine(MethodEntry sourceEntry, Translator translator) { | ||
| 93 | MethodEntry targetEntry = translator.translate(sourceEntry); | ||
| 94 | return "MD: " + describeMethod(sourceEntry) + " " + describeMethod(targetEntry); | ||
| 95 | } | ||
| 96 | |||
| 97 | private String describeMethod(MethodEntry entry) { | ||
| 98 | return entry.getParent().getFullName() + "/" + entry.getName() + " " + entry.getDesc(); | ||
| 99 | } | ||
| 100 | |||
| 101 | private String generateFieldLine(FieldEntry sourceEntry, Translator translator) { | ||
| 102 | FieldEntry targetEntry = translator.translate(sourceEntry); | ||
| 103 | return "FD: " + describeField(sourceEntry) + " " + describeField(targetEntry); | ||
| 104 | } | ||
| 105 | |||
| 106 | private String describeField(FieldEntry entry) { | ||
| 107 | return entry.getParent().getFullName() + "/" + entry.getName(); | ||
| 108 | } | ||
| 109 | |||
| 110 | private Collection<Entry<?>> sorted(Iterable<Entry<?>> iterable) { | ||
| 111 | ArrayList<Entry<?>> sorted = Lists.newArrayList(iterable); | ||
| 112 | sorted.sort(Comparator.comparing(Entry::getName)); | ||
| 113 | return sorted; | ||
| 114 | } | ||
| 115 | } | ||
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 @@ | |||
| 1 | package cuchaz.enigma.translation.mapping.serde; | ||
| 2 | |||
| 3 | import com.google.common.base.Charsets; | ||
| 4 | import cuchaz.enigma.throwables.MappingParseException; | ||
| 5 | import cuchaz.enigma.translation.mapping.EntryMapping; | ||
| 6 | import cuchaz.enigma.translation.mapping.MappingPair; | ||
| 7 | import cuchaz.enigma.translation.mapping.tree.HashEntryTree; | ||
| 8 | import cuchaz.enigma.translation.mapping.tree.EntryTree; | ||
| 9 | import cuchaz.enigma.translation.representation.MethodDescriptor; | ||
| 10 | import cuchaz.enigma.translation.representation.TypeDescriptor; | ||
| 11 | import cuchaz.enigma.translation.representation.entry.ClassEntry; | ||
| 12 | import cuchaz.enigma.translation.representation.entry.FieldEntry; | ||
| 13 | import cuchaz.enigma.translation.representation.entry.LocalVariableEntry; | ||
| 14 | import cuchaz.enigma.translation.representation.entry.MethodEntry; | ||
| 15 | |||
| 16 | import java.io.IOException; | ||
| 17 | import java.nio.file.Files; | ||
| 18 | import java.nio.file.Path; | ||
| 19 | import java.util.List; | ||
| 20 | |||
| 21 | public enum TinyMappingsReader implements MappingsReader { | ||
| 22 | INSTANCE; | ||
| 23 | |||
| 24 | @Override | ||
| 25 | public EntryTree<EntryMapping> read(Path path) throws IOException, MappingParseException { | ||
| 26 | return read(path, Files.readAllLines(path, Charsets.UTF_8)); | ||
| 27 | } | ||
| 28 | |||
| 29 | private EntryTree<EntryMapping> read(Path path, List<String> lines) throws MappingParseException { | ||
| 30 | EntryTree<EntryMapping> mappings = new HashEntryTree<>(); | ||
| 31 | lines.remove(0); | ||
| 32 | |||
| 33 | for (int lineNumber = 0; lineNumber < lines.size(); lineNumber++) { | ||
| 34 | String line = lines.get(lineNumber); | ||
| 35 | |||
| 36 | try { | ||
| 37 | MappingPair<?, EntryMapping> mapping = parseLine(line); | ||
| 38 | mappings.insert(mapping.getEntry(), mapping.getMapping()); | ||
| 39 | } catch (Throwable t) { | ||
| 40 | t.printStackTrace(); | ||
| 41 | throw new MappingParseException(path::toString, lineNumber, t.toString()); | ||
| 42 | } | ||
| 43 | } | ||
| 44 | |||
| 45 | return mappings; | ||
| 46 | } | ||
| 47 | |||
| 48 | private MappingPair<?, EntryMapping> parseLine(String line) { | ||
| 49 | String[] tokens = line.split("\t"); | ||
| 50 | |||
| 51 | String key = tokens[0]; | ||
| 52 | switch (key) { | ||
| 53 | case "CLASS": | ||
| 54 | return parseClass(tokens); | ||
| 55 | case "FIELD": | ||
| 56 | return parseField(tokens); | ||
| 57 | case "METHOD": | ||
| 58 | return parseMethod(tokens); | ||
| 59 | case "MTH-ARG": | ||
| 60 | return parseArgument(tokens); | ||
| 61 | default: | ||
| 62 | throw new RuntimeException("Unknown token '" + key + "'!"); | ||
| 63 | } | ||
| 64 | } | ||
| 65 | |||
| 66 | private MappingPair<ClassEntry, EntryMapping> parseClass(String[] tokens) { | ||
| 67 | ClassEntry obfuscatedEntry = new ClassEntry(tokens[1]); | ||
| 68 | String mapping = tokens[2]; | ||
| 69 | return new MappingPair<>(obfuscatedEntry, new EntryMapping(mapping)); | ||
| 70 | } | ||
| 71 | |||
| 72 | private MappingPair<FieldEntry, EntryMapping> parseField(String[] tokens) { | ||
| 73 | ClassEntry ownerClass = new ClassEntry(tokens[1]); | ||
| 74 | TypeDescriptor descriptor = new TypeDescriptor(tokens[2]); | ||
| 75 | |||
| 76 | FieldEntry obfuscatedEntry = new FieldEntry(ownerClass, tokens[3], descriptor); | ||
| 77 | String mapping = tokens[4]; | ||
| 78 | return new MappingPair<>(obfuscatedEntry, new EntryMapping(mapping)); | ||
| 79 | } | ||
| 80 | |||
| 81 | private MappingPair<MethodEntry, EntryMapping> parseMethod(String[] tokens) { | ||
| 82 | ClassEntry ownerClass = new ClassEntry(tokens[1]); | ||
| 83 | MethodDescriptor descriptor = new MethodDescriptor(tokens[2]); | ||
| 84 | |||
| 85 | MethodEntry obfuscatedEntry = new MethodEntry(ownerClass, tokens[3], descriptor); | ||
| 86 | String mapping = tokens[4]; | ||
| 87 | return new MappingPair<>(obfuscatedEntry, new EntryMapping(mapping)); | ||
| 88 | } | ||
| 89 | |||
| 90 | private MappingPair<LocalVariableEntry, EntryMapping> parseArgument(String[] tokens) { | ||
| 91 | ClassEntry ownerClass = new ClassEntry(tokens[1]); | ||
| 92 | MethodDescriptor ownerDescriptor = new MethodDescriptor(tokens[2]); | ||
| 93 | MethodEntry ownerMethod = new MethodEntry(ownerClass, tokens[3], ownerDescriptor); | ||
| 94 | int variableIndex = Integer.parseInt(tokens[4]); | ||
| 95 | |||
| 96 | String mapping = tokens[5]; | ||
| 97 | LocalVariableEntry obfuscatedEntry = new LocalVariableEntry(ownerMethod, variableIndex, "", true); | ||
| 98 | return new MappingPair<>(obfuscatedEntry, new EntryMapping(mapping)); | ||
| 99 | } | ||
| 100 | } | ||
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 @@ | |||
| 1 | package cuchaz.enigma.translation.mapping.tree; | ||
| 2 | |||
| 3 | import cuchaz.enigma.translation.mapping.MappingDelta; | ||
| 4 | import cuchaz.enigma.translation.representation.entry.Entry; | ||
| 5 | |||
| 6 | import javax.annotation.Nullable; | ||
| 7 | import java.util.Collection; | ||
| 8 | import java.util.Iterator; | ||
| 9 | |||
| 10 | public class DeltaTrackingTree<T> implements EntryTree<T> { | ||
| 11 | private final EntryTree<T> delegate; | ||
| 12 | |||
| 13 | private EntryTree<Object> additions = new HashEntryTree<>(); | ||
| 14 | private EntryTree<Object> deletions = new HashEntryTree<>(); | ||
| 15 | |||
| 16 | public DeltaTrackingTree(EntryTree<T> delegate) { | ||
| 17 | this.delegate = delegate; | ||
| 18 | } | ||
| 19 | |||
| 20 | public DeltaTrackingTree() { | ||
| 21 | this(new HashEntryTree<>()); | ||
| 22 | } | ||
| 23 | |||
| 24 | @Override | ||
| 25 | public void insert(Entry<?> entry, T value) { | ||
| 26 | if (value != null) { | ||
| 27 | trackAddition(entry); | ||
| 28 | } else { | ||
| 29 | trackDeletion(entry); | ||
| 30 | } | ||
| 31 | delegate.insert(entry, value); | ||
| 32 | } | ||
| 33 | |||
| 34 | @Nullable | ||
| 35 | @Override | ||
| 36 | public T remove(Entry<?> entry) { | ||
| 37 | T value = delegate.remove(entry); | ||
| 38 | trackDeletion(entry); | ||
| 39 | return value; | ||
| 40 | } | ||
| 41 | |||
| 42 | public void trackAddition(Entry<?> entry) { | ||
| 43 | deletions.remove(entry); | ||
| 44 | additions.insert(entry, MappingDelta.PLACEHOLDER); | ||
| 45 | } | ||
| 46 | |||
| 47 | public void trackDeletion(Entry<?> entry) { | ||
| 48 | additions.remove(entry); | ||
| 49 | deletions.insert(entry, MappingDelta.PLACEHOLDER); | ||
| 50 | } | ||
| 51 | |||
| 52 | @Nullable | ||
| 53 | @Override | ||
| 54 | public T get(Entry<?> entry) { | ||
| 55 | return delegate.get(entry); | ||
| 56 | } | ||
| 57 | |||
| 58 | @Override | ||
| 59 | public Collection<Entry<?>> getChildren(Entry<?> entry) { | ||
| 60 | return delegate.getChildren(entry); | ||
| 61 | } | ||
| 62 | |||
| 63 | @Override | ||
| 64 | public Collection<Entry<?>> getSiblings(Entry<?> entry) { | ||
| 65 | return delegate.getSiblings(entry); | ||
| 66 | } | ||
| 67 | |||
| 68 | @Nullable | ||
| 69 | @Override | ||
| 70 | public EntryTreeNode<T> findNode(Entry<?> entry) { | ||
| 71 | return delegate.findNode(entry); | ||
| 72 | } | ||
| 73 | |||
| 74 | @Override | ||
| 75 | public Collection<EntryTreeNode<T>> getAllNodes() { | ||
| 76 | return delegate.getAllNodes(); | ||
| 77 | } | ||
| 78 | |||
| 79 | @Override | ||
| 80 | public Collection<Entry<?>> getRootEntries() { | ||
| 81 | return delegate.getRootEntries(); | ||
| 82 | } | ||
| 83 | |||
| 84 | @Override | ||
| 85 | public Collection<Entry<?>> getAllEntries() { | ||
| 86 | return delegate.getAllEntries(); | ||
| 87 | } | ||
| 88 | |||
| 89 | @Override | ||
| 90 | public boolean isEmpty() { | ||
| 91 | return delegate.isEmpty(); | ||
| 92 | } | ||
| 93 | |||
| 94 | @Override | ||
| 95 | public Iterator<EntryTreeNode<T>> iterator() { | ||
| 96 | return delegate.iterator(); | ||
| 97 | } | ||
| 98 | |||
| 99 | public MappingDelta takeDelta() { | ||
| 100 | MappingDelta delta = new MappingDelta(additions, deletions); | ||
| 101 | resetDelta(); | ||
| 102 | return delta; | ||
| 103 | } | ||
| 104 | |||
| 105 | private void resetDelta() { | ||
| 106 | additions = new HashEntryTree<>(); | ||
| 107 | deletions = new HashEntryTree<>(); | ||
| 108 | } | ||
| 109 | |||
| 110 | public boolean isDirty() { | ||
| 111 | return !additions.isEmpty() || !deletions.isEmpty(); | ||
| 112 | } | ||
| 113 | } | ||
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 @@ | |||
| 1 | package cuchaz.enigma.translation.mapping.tree; | ||
| 2 | |||
| 3 | import cuchaz.enigma.translation.mapping.EntryMap; | ||
| 4 | import cuchaz.enigma.translation.representation.entry.Entry; | ||
| 5 | |||
| 6 | import javax.annotation.Nullable; | ||
| 7 | import java.util.Collection; | ||
| 8 | |||
| 9 | public interface EntryTree<T> extends EntryMap<T>, Iterable<EntryTreeNode<T>> { | ||
| 10 | Collection<Entry<?>> getChildren(Entry<?> entry); | ||
| 11 | |||
| 12 | Collection<Entry<?>> getSiblings(Entry<?> entry); | ||
| 13 | |||
| 14 | @Nullable | ||
| 15 | EntryTreeNode<T> findNode(Entry<?> entry); | ||
| 16 | |||
| 17 | Collection<EntryTreeNode<T>> getAllNodes(); | ||
| 18 | |||
| 19 | Collection<Entry<?>> getRootEntries(); | ||
| 20 | } | ||
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 @@ | |||
| 1 | package cuchaz.enigma.translation.mapping.tree; | ||
| 2 | |||
| 3 | import cuchaz.enigma.translation.representation.entry.Entry; | ||
| 4 | |||
| 5 | import javax.annotation.Nullable; | ||
| 6 | import java.util.ArrayList; | ||
| 7 | import java.util.Collection; | ||
| 8 | import java.util.stream.Collectors; | ||
| 9 | |||
| 10 | public interface EntryTreeNode<T> { | ||
| 11 | @Nullable | ||
| 12 | T getValue(); | ||
| 13 | |||
| 14 | Entry<?> getEntry(); | ||
| 15 | |||
| 16 | boolean isEmpty(); | ||
| 17 | |||
| 18 | Collection<Entry<?>> getChildren(); | ||
| 19 | |||
| 20 | Collection<? extends EntryTreeNode<T>> getChildNodes(); | ||
| 21 | |||
| 22 | default Collection<? extends EntryTreeNode<T>> getNodesRecursively() { | ||
| 23 | Collection<EntryTreeNode<T>> nodes = new ArrayList<>(); | ||
| 24 | nodes.add(this); | ||
| 25 | for (EntryTreeNode<T> node : getChildNodes()) { | ||
| 26 | nodes.addAll(node.getNodesRecursively()); | ||
| 27 | } | ||
| 28 | return nodes; | ||
| 29 | } | ||
| 30 | |||
| 31 | default Collection<Entry<?>> getChildrenRecursively() { | ||
| 32 | return getNodesRecursively().stream() | ||
| 33 | .map(EntryTreeNode::getEntry) | ||
| 34 | .collect(Collectors.toList()); | ||
| 35 | } | ||
| 36 | } | ||
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 @@ | |||
| 1 | package cuchaz.enigma.translation.mapping.tree; | ||
| 2 | |||
| 3 | import cuchaz.enigma.translation.representation.entry.Entry; | ||
| 4 | |||
| 5 | import javax.annotation.Nullable; | ||
| 6 | import java.util.*; | ||
| 7 | import java.util.stream.Collectors; | ||
| 8 | |||
| 9 | public class HashEntryTree<T> implements EntryTree<T> { | ||
| 10 | private final Map<Entry<?>, HashTreeNode<T>> root = new HashMap<>(); | ||
| 11 | |||
| 12 | @Override | ||
| 13 | public void insert(Entry<?> entry, T value) { | ||
| 14 | List<HashTreeNode<T>> path = computePath(entry); | ||
| 15 | path.get(path.size() - 1).putValue(value); | ||
| 16 | if (value == null) { | ||
| 17 | removeDeadAlong(path); | ||
| 18 | } | ||
| 19 | } | ||
| 20 | |||
| 21 | @Override | ||
| 22 | @Nullable | ||
| 23 | public T remove(Entry<?> entry) { | ||
| 24 | List<HashTreeNode<T>> path = computePath(entry); | ||
| 25 | T value = path.get(path.size() - 1).removeValue(); | ||
| 26 | |||
| 27 | removeDeadAlong(path); | ||
| 28 | |||
| 29 | return value; | ||
| 30 | } | ||
| 31 | |||
| 32 | @Override | ||
| 33 | @Nullable | ||
| 34 | public T get(Entry<?> entry) { | ||
| 35 | HashTreeNode<T> node = findNode(entry); | ||
| 36 | if (node == null) { | ||
| 37 | return null; | ||
| 38 | } | ||
| 39 | return node.getValue(); | ||
| 40 | } | ||
| 41 | |||
| 42 | @Override | ||
| 43 | public boolean contains(Entry<?> entry) { | ||
| 44 | return get(entry) != null; | ||
| 45 | } | ||
| 46 | |||
| 47 | @Override | ||
| 48 | public Collection<Entry<?>> getChildren(Entry<?> entry) { | ||
| 49 | HashTreeNode<T> leaf = findNode(entry); | ||
| 50 | if (leaf == null) { | ||
| 51 | return Collections.emptyList(); | ||
| 52 | } | ||
| 53 | return leaf.getChildren(); | ||
| 54 | } | ||
| 55 | |||
| 56 | @Override | ||
| 57 | public Collection<Entry<?>> getSiblings(Entry<?> entry) { | ||
| 58 | List<HashTreeNode<T>> path = computePath(entry); | ||
| 59 | if (path.size() <= 1) { | ||
| 60 | return getSiblings(entry, root.keySet()); | ||
| 61 | } | ||
| 62 | HashTreeNode<T> parent = path.get(path.size() - 2); | ||
| 63 | return getSiblings(entry, parent.getChildren()); | ||
| 64 | } | ||
| 65 | |||
| 66 | private Collection<Entry<?>> getSiblings(Entry<?> entry, Collection<Entry<?>> children) { | ||
| 67 | Set<Entry<?>> siblings = new HashSet<>(children); | ||
| 68 | siblings.remove(entry); | ||
| 69 | return siblings; | ||
| 70 | } | ||
| 71 | |||
| 72 | @Override | ||
| 73 | @Nullable | ||
| 74 | public HashTreeNode<T> findNode(Entry<?> target) { | ||
| 75 | List<Entry<?>> parentChain = target.getAncestry(); | ||
| 76 | if (parentChain.isEmpty()) { | ||
| 77 | return null; | ||
| 78 | } | ||
| 79 | |||
| 80 | HashTreeNode<T> node = root.get(parentChain.get(0)); | ||
| 81 | for (int i = 1; i < parentChain.size(); i++) { | ||
| 82 | if (node == null) { | ||
| 83 | return null; | ||
| 84 | } | ||
| 85 | node = node.getChild(parentChain.get(i), false); | ||
| 86 | } | ||
| 87 | |||
| 88 | return node; | ||
| 89 | } | ||
| 90 | |||
| 91 | private List<HashTreeNode<T>> computePath(Entry<?> target) { | ||
| 92 | List<Entry<?>> ancestry = target.getAncestry(); | ||
| 93 | if (ancestry.isEmpty()) { | ||
| 94 | return Collections.emptyList(); | ||
| 95 | } | ||
| 96 | |||
| 97 | List<HashTreeNode<T>> path = new ArrayList<>(ancestry.size()); | ||
| 98 | |||
| 99 | Entry<?> rootEntry = ancestry.get(0); | ||
| 100 | HashTreeNode<T> node = root.computeIfAbsent(rootEntry, HashTreeNode::new); | ||
| 101 | path.add(node); | ||
| 102 | |||
| 103 | for (int i = 1; i < ancestry.size(); i++) { | ||
| 104 | node = node.getChild(ancestry.get(i), true); | ||
| 105 | path.add(node); | ||
| 106 | } | ||
| 107 | |||
| 108 | return path; | ||
| 109 | } | ||
| 110 | |||
| 111 | private void removeDeadAlong(List<HashTreeNode<T>> path) { | ||
| 112 | for (int i = path.size() - 1; i >= 0; i--) { | ||
| 113 | HashTreeNode<T> node = path.get(i); | ||
| 114 | if (node.isEmpty()) { | ||
| 115 | if (i > 0) { | ||
| 116 | HashTreeNode<T> parentNode = path.get(i - 1); | ||
| 117 | parentNode.remove(node.getEntry()); | ||
| 118 | } else { | ||
| 119 | root.remove(node.getEntry()); | ||
| 120 | } | ||
| 121 | } else { | ||
| 122 | break; | ||
| 123 | } | ||
| 124 | } | ||
| 125 | } | ||
| 126 | |||
| 127 | @Override | ||
| 128 | @SuppressWarnings("unchecked") | ||
| 129 | public Iterator<EntryTreeNode<T>> iterator() { | ||
| 130 | Collection<EntryTreeNode<T>> values = (Collection) root.values(); | ||
| 131 | return values.iterator(); | ||
| 132 | } | ||
| 133 | |||
| 134 | @Override | ||
| 135 | public Collection<EntryTreeNode<T>> getAllNodes() { | ||
| 136 | Collection<EntryTreeNode<T>> nodes = new ArrayList<>(); | ||
| 137 | for (EntryTreeNode<T> node : root.values()) { | ||
| 138 | nodes.addAll(node.getNodesRecursively()); | ||
| 139 | } | ||
| 140 | return nodes; | ||
| 141 | } | ||
| 142 | |||
| 143 | @Override | ||
| 144 | public Collection<Entry<?>> getAllEntries() { | ||
| 145 | return getAllNodes().stream() | ||
| 146 | .map(EntryTreeNode::getEntry) | ||
| 147 | .collect(Collectors.toList()); | ||
| 148 | } | ||
| 149 | |||
| 150 | @Override | ||
| 151 | public Collection<Entry<?>> getRootEntries() { | ||
| 152 | return root.keySet(); | ||
| 153 | } | ||
| 154 | |||
| 155 | @Override | ||
| 156 | public boolean isEmpty() { | ||
| 157 | return root.isEmpty(); | ||
| 158 | } | ||
| 159 | } | ||
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 @@ | |||
| 1 | package cuchaz.enigma.translation.mapping.tree; | ||
| 2 | |||
| 3 | import cuchaz.enigma.translation.representation.entry.Entry; | ||
| 4 | |||
| 5 | import javax.annotation.Nullable; | ||
| 6 | import java.util.Collection; | ||
| 7 | import java.util.HashMap; | ||
| 8 | import java.util.Iterator; | ||
| 9 | import java.util.Map; | ||
| 10 | |||
| 11 | public class HashTreeNode<T> implements EntryTreeNode<T>, Iterable<HashTreeNode<T>> { | ||
| 12 | private final Entry<?> entry; | ||
| 13 | private final Map<Entry<?>, HashTreeNode<T>> children = new HashMap<>(); | ||
| 14 | private T value; | ||
| 15 | |||
| 16 | HashTreeNode(Entry<?> entry) { | ||
| 17 | this.entry = entry; | ||
| 18 | } | ||
| 19 | |||
| 20 | void putValue(T value) { | ||
| 21 | this.value = value; | ||
| 22 | } | ||
| 23 | |||
| 24 | T removeValue() { | ||
| 25 | T value = this.value; | ||
| 26 | this.value = null; | ||
| 27 | return value; | ||
| 28 | } | ||
| 29 | |||
| 30 | HashTreeNode<T> getChild(Entry<?> entry, boolean create) { | ||
| 31 | if (create) { | ||
| 32 | return children.computeIfAbsent(entry, HashTreeNode::new); | ||
| 33 | } else { | ||
| 34 | return children.get(entry); | ||
| 35 | } | ||
| 36 | } | ||
| 37 | |||
| 38 | void remove(Entry<?> entry) { | ||
| 39 | children.remove(entry); | ||
| 40 | } | ||
| 41 | |||
| 42 | @Override | ||
| 43 | @Nullable | ||
| 44 | public T getValue() { | ||
| 45 | return value; | ||
| 46 | } | ||
| 47 | |||
| 48 | @Override | ||
| 49 | public Entry<?> getEntry() { | ||
| 50 | return entry; | ||
| 51 | } | ||
| 52 | |||
| 53 | @Override | ||
| 54 | public boolean isEmpty() { | ||
| 55 | return children.isEmpty() && value == null; | ||
| 56 | } | ||
| 57 | |||
| 58 | @Override | ||
| 59 | public Collection<Entry<?>> getChildren() { | ||
| 60 | return children.keySet(); | ||
| 61 | } | ||
| 62 | |||
| 63 | @Override | ||
| 64 | public Collection<? extends EntryTreeNode<T>> getChildNodes() { | ||
| 65 | return children.values(); | ||
| 66 | } | ||
| 67 | |||
| 68 | @Override | ||
| 69 | public Iterator<HashTreeNode<T>> iterator() { | ||
| 70 | return children.values().iterator(); | ||
| 71 | } | ||
| 72 | } | ||