summaryrefslogtreecommitdiff
path: root/src/main/java/cuchaz/enigma/translation/mapping
diff options
context:
space:
mode:
authorGravatar Gegy2019-01-24 14:48:32 +0200
committerGravatar Adrian Siekierka2019-01-24 13:48:32 +0100
commit00fcd0550fcdda621c2e4662f6ddd55ce673b931 (patch)
tree6f9e4c24dbcc6d118fceec56adf7bf9d747a485c /src/main/java/cuchaz/enigma/translation/mapping
parentmark as 0.13.0-SNAPSHOT for preliminary development (diff)
downloadenigma-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')
-rw-r--r--src/main/java/cuchaz/enigma/translation/mapping/AccessModifier.java25
-rw-r--r--src/main/java/cuchaz/enigma/translation/mapping/EntryMap.java24
-rw-r--r--src/main/java/cuchaz/enigma/translation/mapping/EntryMapping.java30
-rw-r--r--src/main/java/cuchaz/enigma/translation/mapping/EntryRemapper.java201
-rw-r--r--src/main/java/cuchaz/enigma/translation/mapping/EntryResolver.java41
-rw-r--r--src/main/java/cuchaz/enigma/translation/mapping/IndexEntryResolver.java225
-rw-r--r--src/main/java/cuchaz/enigma/translation/mapping/MappingDelta.java56
-rw-r--r--src/main/java/cuchaz/enigma/translation/mapping/MappingPair.java28
-rw-r--r--src/main/java/cuchaz/enigma/translation/mapping/MappingValidator.java45
-rw-r--r--src/main/java/cuchaz/enigma/translation/mapping/MappingsChecker.java91
-rw-r--r--src/main/java/cuchaz/enigma/translation/mapping/NameValidator.java56
-rw-r--r--src/main/java/cuchaz/enigma/translation/mapping/ResolutionStrategy.java6
-rw-r--r--src/main/java/cuchaz/enigma/translation/mapping/VoidEntryResolver.java27
-rw-r--r--src/main/java/cuchaz/enigma/translation/mapping/serde/EnigmaMappingsReader.java260
-rw-r--r--src/main/java/cuchaz/enigma/translation/mapping/serde/EnigmaMappingsWriter.java260
-rw-r--r--src/main/java/cuchaz/enigma/translation/mapping/serde/MappingFormat.java54
-rw-r--r--src/main/java/cuchaz/enigma/translation/mapping/serde/MappingsReader.java12
-rw-r--r--src/main/java/cuchaz/enigma/translation/mapping/serde/MappingsWriter.java12
-rw-r--r--src/main/java/cuchaz/enigma/translation/mapping/serde/SrgMappingsWriter.java115
-rw-r--r--src/main/java/cuchaz/enigma/translation/mapping/serde/TinyMappingsReader.java100
-rw-r--r--src/main/java/cuchaz/enigma/translation/mapping/tree/DeltaTrackingTree.java113
-rw-r--r--src/main/java/cuchaz/enigma/translation/mapping/tree/EntryTree.java20
-rw-r--r--src/main/java/cuchaz/enigma/translation/mapping/tree/EntryTreeNode.java36
-rw-r--r--src/main/java/cuchaz/enigma/translation/mapping/tree/HashEntryTree.java159
-rw-r--r--src/main/java/cuchaz/enigma/translation/mapping/tree/HashTreeNode.java72
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 @@
1package cuchaz.enigma.translation.mapping;
2
3import cuchaz.enigma.translation.representation.AccessFlags;
4
5public 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 @@
1package cuchaz.enigma.translation.mapping;
2
3import cuchaz.enigma.translation.representation.entry.Entry;
4
5import javax.annotation.Nullable;
6import java.util.Collection;
7
8public 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 @@
1package cuchaz.enigma.translation.mapping;
2
3import javax.annotation.Nonnull;
4
5public 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 @@
1package cuchaz.enigma.translation.mapping;
2
3import cuchaz.enigma.analysis.index.JarIndex;
4import cuchaz.enigma.translation.MappingTranslator;
5import cuchaz.enigma.translation.Translatable;
6import cuchaz.enigma.translation.Translator;
7import cuchaz.enigma.translation.mapping.tree.DeltaTrackingTree;
8import cuchaz.enigma.translation.mapping.tree.EntryTree;
9import cuchaz.enigma.translation.mapping.tree.EntryTreeNode;
10import cuchaz.enigma.translation.mapping.tree.HashEntryTree;
11import cuchaz.enigma.translation.representation.entry.Entry;
12
13import javax.annotation.Nullable;
14import java.util.Collection;
15
16public 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 @@
1package cuchaz.enigma.translation.mapping;
2
3import com.google.common.collect.Streams;
4import cuchaz.enigma.analysis.EntryReference;
5import cuchaz.enigma.translation.representation.entry.Entry;
6import cuchaz.enigma.translation.representation.entry.MethodEntry;
7
8import java.util.Collection;
9import java.util.Set;
10import java.util.stream.Collectors;
11
12public 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 @@
1package cuchaz.enigma.translation.mapping;
2
3import com.google.common.collect.Sets;
4import cuchaz.enigma.analysis.IndexTreeBuilder;
5import cuchaz.enigma.analysis.MethodImplementationsTreeNode;
6import cuchaz.enigma.analysis.MethodInheritanceTreeNode;
7import cuchaz.enigma.analysis.index.BridgeMethodIndex;
8import cuchaz.enigma.analysis.index.EntryIndex;
9import cuchaz.enigma.analysis.index.InheritanceIndex;
10import cuchaz.enigma.analysis.index.JarIndex;
11import cuchaz.enigma.translation.VoidTranslator;
12import cuchaz.enigma.translation.representation.AccessFlags;
13import cuchaz.enigma.translation.representation.entry.ClassEntry;
14import cuchaz.enigma.translation.representation.entry.Entry;
15import cuchaz.enigma.translation.representation.entry.MethodEntry;
16
17import javax.annotation.Nullable;
18import java.util.*;
19import java.util.stream.Collectors;
20
21public 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 @@
1package cuchaz.enigma.translation.mapping;
2
3import cuchaz.enigma.translation.Translatable;
4import cuchaz.enigma.translation.Translator;
5import cuchaz.enigma.translation.mapping.tree.HashEntryTree;
6import cuchaz.enigma.translation.mapping.tree.EntryTree;
7import cuchaz.enigma.translation.representation.entry.Entry;
8
9public 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 @@
1package cuchaz.enigma.translation.mapping;
2
3import cuchaz.enigma.translation.representation.entry.Entry;
4
5import javax.annotation.Nullable;
6
7public 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 @@
1package cuchaz.enigma.translation.mapping;
2
3import cuchaz.enigma.throwables.IllegalNameException;
4import cuchaz.enigma.translation.Translator;
5import cuchaz.enigma.translation.mapping.tree.EntryTree;
6import cuchaz.enigma.translation.representation.entry.Entry;
7
8import java.util.Collection;
9
10public 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
12package cuchaz.enigma.translation.mapping;
13
14import cuchaz.enigma.analysis.index.JarIndex;
15import cuchaz.enigma.translation.mapping.tree.EntryTree;
16import cuchaz.enigma.translation.mapping.tree.EntryTreeNode;
17import cuchaz.enigma.translation.representation.entry.ClassEntry;
18import cuchaz.enigma.translation.representation.entry.Entry;
19import cuchaz.enigma.translation.representation.entry.FieldEntry;
20import cuchaz.enigma.translation.representation.entry.MethodEntry;
21
22import java.util.Collection;
23import java.util.HashMap;
24import java.util.Map;
25
26public 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
12package cuchaz.enigma.translation.mapping;
13
14import cuchaz.enigma.throwables.IllegalNameException;
15import cuchaz.enigma.translation.representation.entry.ClassEntry;
16
17import java.util.Arrays;
18import java.util.List;
19import java.util.regex.Pattern;
20
21public 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 @@
1package cuchaz.enigma.translation.mapping;
2
3public 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 @@
1package cuchaz.enigma.translation.mapping;
2
3import cuchaz.enigma.translation.representation.entry.Entry;
4import cuchaz.enigma.translation.representation.entry.MethodEntry;
5
6import java.util.Collection;
7import java.util.Collections;
8import java.util.Set;
9
10public 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 @@
1package cuchaz.enigma.translation.mapping.serde;
2
3import com.google.common.base.Charsets;
4import cuchaz.enigma.throwables.MappingParseException;
5import cuchaz.enigma.translation.mapping.AccessModifier;
6import cuchaz.enigma.translation.mapping.EntryMapping;
7import cuchaz.enigma.translation.mapping.MappingPair;
8import cuchaz.enigma.translation.mapping.tree.HashEntryTree;
9import cuchaz.enigma.translation.mapping.tree.EntryTree;
10import cuchaz.enigma.translation.representation.MethodDescriptor;
11import cuchaz.enigma.translation.representation.TypeDescriptor;
12import cuchaz.enigma.translation.representation.entry.*;
13
14import javax.annotation.Nullable;
15import java.io.IOException;
16import java.nio.file.Files;
17import java.nio.file.Path;
18import java.util.ArrayDeque;
19import java.util.Deque;
20import java.util.List;
21import java.util.Locale;
22import java.util.stream.Collectors;
23
24public 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
12package cuchaz.enigma.translation.mapping.serde;
13
14import cuchaz.enigma.ProgressListener;
15import cuchaz.enigma.translation.MappingTranslator;
16import cuchaz.enigma.translation.Translator;
17import cuchaz.enigma.translation.mapping.AccessModifier;
18import cuchaz.enigma.translation.mapping.EntryMapping;
19import cuchaz.enigma.translation.mapping.MappingDelta;
20import cuchaz.enigma.translation.mapping.VoidEntryResolver;
21import cuchaz.enigma.translation.mapping.tree.EntryTree;
22import cuchaz.enigma.translation.mapping.tree.EntryTreeNode;
23import cuchaz.enigma.translation.representation.entry.*;
24
25import java.io.IOException;
26import java.io.PrintWriter;
27import java.nio.file.DirectoryStream;
28import java.nio.file.Files;
29import java.nio.file.Path;
30import java.nio.file.Paths;
31import java.util.ArrayList;
32import java.util.Collection;
33import java.util.concurrent.atomic.AtomicInteger;
34import java.util.stream.Collectors;
35
36public 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 @@
1package cuchaz.enigma.translation.mapping.serde;
2
3import cuchaz.enigma.ProgressListener;
4import cuchaz.enigma.throwables.MappingParseException;
5import cuchaz.enigma.translation.mapping.EntryMapping;
6import cuchaz.enigma.translation.mapping.MappingDelta;
7import cuchaz.enigma.translation.mapping.tree.EntryTree;
8
9import javax.annotation.Nullable;
10import java.io.IOException;
11import java.nio.file.Path;
12
13public 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 @@
1package cuchaz.enigma.translation.mapping.serde;
2
3import cuchaz.enigma.throwables.MappingParseException;
4import cuchaz.enigma.translation.mapping.EntryMapping;
5import cuchaz.enigma.translation.mapping.tree.EntryTree;
6
7import java.io.IOException;
8import java.nio.file.Path;
9
10public 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 @@
1package cuchaz.enigma.translation.mapping.serde;
2
3import cuchaz.enigma.ProgressListener;
4import cuchaz.enigma.translation.mapping.EntryMapping;
5import cuchaz.enigma.translation.mapping.MappingDelta;
6import cuchaz.enigma.translation.mapping.tree.EntryTree;
7
8import java.nio.file.Path;
9
10public 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 @@
1package cuchaz.enigma.translation.mapping.serde;
2
3import com.google.common.collect.Lists;
4import cuchaz.enigma.ProgressListener;
5import cuchaz.enigma.translation.MappingTranslator;
6import cuchaz.enigma.translation.Translator;
7import cuchaz.enigma.translation.mapping.EntryMapping;
8import cuchaz.enigma.translation.mapping.MappingDelta;
9import cuchaz.enigma.translation.mapping.VoidEntryResolver;
10import cuchaz.enigma.translation.mapping.tree.EntryTree;
11import cuchaz.enigma.translation.mapping.tree.EntryTreeNode;
12import cuchaz.enigma.translation.representation.entry.ClassEntry;
13import cuchaz.enigma.translation.representation.entry.Entry;
14import cuchaz.enigma.translation.representation.entry.FieldEntry;
15import cuchaz.enigma.translation.representation.entry.MethodEntry;
16
17import java.io.IOException;
18import java.io.PrintWriter;
19import java.nio.file.Files;
20import java.nio.file.Path;
21import java.util.ArrayList;
22import java.util.Collection;
23import java.util.Comparator;
24import java.util.List;
25import java.util.stream.Collectors;
26
27public 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 @@
1package cuchaz.enigma.translation.mapping.serde;
2
3import com.google.common.base.Charsets;
4import cuchaz.enigma.throwables.MappingParseException;
5import cuchaz.enigma.translation.mapping.EntryMapping;
6import cuchaz.enigma.translation.mapping.MappingPair;
7import cuchaz.enigma.translation.mapping.tree.HashEntryTree;
8import cuchaz.enigma.translation.mapping.tree.EntryTree;
9import cuchaz.enigma.translation.representation.MethodDescriptor;
10import cuchaz.enigma.translation.representation.TypeDescriptor;
11import cuchaz.enigma.translation.representation.entry.ClassEntry;
12import cuchaz.enigma.translation.representation.entry.FieldEntry;
13import cuchaz.enigma.translation.representation.entry.LocalVariableEntry;
14import cuchaz.enigma.translation.representation.entry.MethodEntry;
15
16import java.io.IOException;
17import java.nio.file.Files;
18import java.nio.file.Path;
19import java.util.List;
20
21public 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 @@
1package cuchaz.enigma.translation.mapping.tree;
2
3import cuchaz.enigma.translation.mapping.MappingDelta;
4import cuchaz.enigma.translation.representation.entry.Entry;
5
6import javax.annotation.Nullable;
7import java.util.Collection;
8import java.util.Iterator;
9
10public 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 @@
1package cuchaz.enigma.translation.mapping.tree;
2
3import cuchaz.enigma.translation.mapping.EntryMap;
4import cuchaz.enigma.translation.representation.entry.Entry;
5
6import javax.annotation.Nullable;
7import java.util.Collection;
8
9public 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 @@
1package cuchaz.enigma.translation.mapping.tree;
2
3import cuchaz.enigma.translation.representation.entry.Entry;
4
5import javax.annotation.Nullable;
6import java.util.ArrayList;
7import java.util.Collection;
8import java.util.stream.Collectors;
9
10public 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 @@
1package cuchaz.enigma.translation.mapping.tree;
2
3import cuchaz.enigma.translation.representation.entry.Entry;
4
5import javax.annotation.Nullable;
6import java.util.*;
7import java.util.stream.Collectors;
8
9public 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 @@
1package cuchaz.enigma.translation.mapping.tree;
2
3import cuchaz.enigma.translation.representation.entry.Entry;
4
5import javax.annotation.Nullable;
6import java.util.Collection;
7import java.util.HashMap;
8import java.util.Iterator;
9import java.util.Map;
10
11public 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}