summaryrefslogtreecommitdiff
path: root/src/cuchaz/enigma/convert/ClassIdentity.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/cuchaz/enigma/convert/ClassIdentity.java')
-rw-r--r--src/cuchaz/enigma/convert/ClassIdentity.java468
1 files changed, 468 insertions, 0 deletions
diff --git a/src/cuchaz/enigma/convert/ClassIdentity.java b/src/cuchaz/enigma/convert/ClassIdentity.java
new file mode 100644
index 0000000..2e164ae
--- /dev/null
+++ b/src/cuchaz/enigma/convert/ClassIdentity.java
@@ -0,0 +1,468 @@
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 ******************************************************************************/
11package cuchaz.enigma.convert;
12
13import java.io.UnsupportedEncodingException;
14import java.security.MessageDigest;
15import java.security.NoSuchAlgorithmException;
16import java.util.Enumeration;
17import java.util.List;
18import java.util.Map;
19import java.util.Set;
20
21import javassist.CannotCompileException;
22import javassist.CtBehavior;
23import javassist.CtClass;
24import javassist.CtConstructor;
25import javassist.CtField;
26import javassist.CtMethod;
27import javassist.bytecode.BadBytecode;
28import javassist.bytecode.CodeIterator;
29import javassist.bytecode.ConstPool;
30import javassist.bytecode.Descriptor;
31import javassist.bytecode.Opcode;
32import javassist.expr.ConstructorCall;
33import javassist.expr.ExprEditor;
34import javassist.expr.FieldAccess;
35import javassist.expr.MethodCall;
36import javassist.expr.NewExpr;
37
38import com.google.common.collect.HashMultiset;
39import com.google.common.collect.Lists;
40import com.google.common.collect.Maps;
41import com.google.common.collect.Multiset;
42import com.google.common.collect.Sets;
43
44import cuchaz.enigma.Constants;
45import cuchaz.enigma.Util;
46import cuchaz.enigma.analysis.ClassImplementationsTreeNode;
47import cuchaz.enigma.analysis.EntryReference;
48import cuchaz.enigma.analysis.JarIndex;
49import cuchaz.enigma.bytecode.ConstPoolEditor;
50import cuchaz.enigma.bytecode.InfoType;
51import cuchaz.enigma.bytecode.accessors.ConstInfoAccessor;
52import cuchaz.enigma.convert.ClassNamer.SidedClassNamer;
53import cuchaz.enigma.mapping.BehaviorEntry;
54import cuchaz.enigma.mapping.ClassEntry;
55import cuchaz.enigma.mapping.ClassNameReplacer;
56import cuchaz.enigma.mapping.Entry;
57import cuchaz.enigma.mapping.EntryFactory;
58import cuchaz.enigma.mapping.FieldEntry;
59import cuchaz.enigma.mapping.Signature;
60import cuchaz.enigma.mapping.Type;
61
62public class ClassIdentity {
63
64 private ClassEntry m_classEntry;
65 private SidedClassNamer m_namer;
66 private Multiset<String> m_fields;
67 private Multiset<String> m_methods;
68 private Multiset<String> m_constructors;
69 private String m_staticInitializer;
70 private String m_extends;
71 private Multiset<String> m_implements;
72 private Set<String> m_stringLiterals;
73 private Multiset<String> m_implementations;
74 private Multiset<String> m_references;
75 private String m_outer;
76
77 private final ClassNameReplacer m_classNameReplacer = new ClassNameReplacer() {
78
79 private Map<String,String> m_classNames = Maps.newHashMap();
80
81 @Override
82 public String replace(String className) {
83
84 // classes not in the none package can be passed through
85 ClassEntry classEntry = new ClassEntry(className);
86 if (!classEntry.getPackageName().equals(Constants.NonePackage)) {
87 return className;
88 }
89
90 // is this class ourself?
91 if (className.equals(m_classEntry.getName())) {
92 return "CSelf";
93 }
94
95 // try the namer
96 if (m_namer != null) {
97 String newName = m_namer.getName(className);
98 if (newName != null) {
99 return newName;
100 }
101 }
102
103 // otherwise, use local naming
104 if (!m_classNames.containsKey(className)) {
105 m_classNames.put(className, getNewClassName());
106 }
107 return m_classNames.get(className);
108 }
109
110 private String getNewClassName() {
111 return String.format("C%03d", m_classNames.size());
112 }
113 };
114
115 public ClassIdentity(CtClass c, SidedClassNamer namer, JarIndex index, boolean useReferences) {
116 m_namer = namer;
117
118 // stuff from the bytecode
119
120 m_classEntry = new ClassEntry(Descriptor.toJvmName(c.getName()));
121 m_fields = HashMultiset.create();
122 for (CtField field : c.getDeclaredFields()) {
123 m_fields.add(scrubType(field.getSignature()));
124 }
125 m_methods = HashMultiset.create();
126 for (CtMethod method : c.getDeclaredMethods()) {
127 m_methods.add(scrubSignature(method.getSignature()) + "0x" + getBehaviorSignature(method));
128 }
129 m_constructors = HashMultiset.create();
130 for (CtConstructor constructor : c.getDeclaredConstructors()) {
131 m_constructors.add(scrubSignature(constructor.getSignature()) + "0x" + getBehaviorSignature(constructor));
132 }
133 m_staticInitializer = "";
134 if (c.getClassInitializer() != null) {
135 m_staticInitializer = getBehaviorSignature(c.getClassInitializer());
136 }
137 m_extends = "";
138 if (c.getClassFile().getSuperclass() != null) {
139 m_extends = scrubClassName(Descriptor.toJvmName(c.getClassFile().getSuperclass()));
140 }
141 m_implements = HashMultiset.create();
142 for (String interfaceName : c.getClassFile().getInterfaces()) {
143 m_implements.add(scrubClassName(Descriptor.toJvmName(interfaceName)));
144 }
145
146 m_stringLiterals = Sets.newHashSet();
147 ConstPool constants = c.getClassFile().getConstPool();
148 for (int i=1; i<constants.getSize(); i++) {
149 if (constants.getTag(i) == ConstPool.CONST_String) {
150 m_stringLiterals.add(constants.getStringInfo(i));
151 }
152 }
153
154 // stuff from the jar index
155
156 m_implementations = HashMultiset.create();
157 ClassImplementationsTreeNode implementationsNode = index.getClassImplementations(null, m_classEntry);
158 if (implementationsNode != null) {
159 @SuppressWarnings("unchecked")
160 Enumeration<ClassImplementationsTreeNode> implementations = implementationsNode.children();
161 while (implementations.hasMoreElements()) {
162 ClassImplementationsTreeNode node = implementations.nextElement();
163 m_implementations.add(scrubClassName(node.getClassEntry().getName()));
164 }
165 }
166
167 m_references = HashMultiset.create();
168 if (useReferences) {
169 for (CtField field : c.getDeclaredFields()) {
170 FieldEntry fieldEntry = EntryFactory.getFieldEntry(field);
171 for (EntryReference<FieldEntry,BehaviorEntry> reference : index.getFieldReferences(fieldEntry)) {
172 addReference(reference);
173 }
174 }
175 for (CtBehavior behavior : c.getDeclaredBehaviors()) {
176 BehaviorEntry behaviorEntry = EntryFactory.getBehaviorEntry(behavior);
177 for (EntryReference<BehaviorEntry,BehaviorEntry> reference : index.getBehaviorReferences(behaviorEntry)) {
178 addReference(reference);
179 }
180 }
181 }
182
183 m_outer = EntryFactory.getClassEntry(c).getOuterClassName();
184 }
185
186 private void addReference(EntryReference<? extends Entry,BehaviorEntry> reference) {
187 if (reference.context.getSignature() != null) {
188 m_references.add(String.format("%s_%s",
189 scrubClassName(reference.context.getClassName()),
190 scrubSignature(reference.context.getSignature())
191 ));
192 } else {
193 m_references.add(String.format("%s_<clinit>",
194 scrubClassName(reference.context.getClassName())
195 ));
196 }
197 }
198
199 public ClassEntry getClassEntry() {
200 return m_classEntry;
201 }
202
203 @Override
204 public String toString() {
205 StringBuilder buf = new StringBuilder();
206 buf.append("class: ");
207 buf.append(m_classEntry.getName());
208 buf.append(" ");
209 buf.append(hashCode());
210 buf.append("\n");
211 for (String field : m_fields) {
212 buf.append("\tfield ");
213 buf.append(field);
214 buf.append("\n");
215 }
216 for (String method : m_methods) {
217 buf.append("\tmethod ");
218 buf.append(method);
219 buf.append("\n");
220 }
221 for (String constructor : m_constructors) {
222 buf.append("\tconstructor ");
223 buf.append(constructor);
224 buf.append("\n");
225 }
226 if (m_staticInitializer.length() > 0) {
227 buf.append("\tinitializer ");
228 buf.append(m_staticInitializer);
229 buf.append("\n");
230 }
231 if (m_extends.length() > 0) {
232 buf.append("\textends ");
233 buf.append(m_extends);
234 buf.append("\n");
235 }
236 for (String interfaceName : m_implements) {
237 buf.append("\timplements ");
238 buf.append(interfaceName);
239 buf.append("\n");
240 }
241 for (String implementation : m_implementations) {
242 buf.append("\timplemented by ");
243 buf.append(implementation);
244 buf.append("\n");
245 }
246 for (String reference : m_references) {
247 buf.append("\treference ");
248 buf.append(reference);
249 buf.append("\n");
250 }
251 buf.append("\touter ");
252 buf.append(m_outer);
253 buf.append("\n");
254 return buf.toString();
255 }
256
257 private String scrubClassName(String className) {
258 return m_classNameReplacer.replace(className);
259 }
260
261 private String scrubType(String typeName) {
262 return scrubType(new Type(typeName)).toString();
263 }
264
265 private Type scrubType(Type type) {
266 if (type.hasClass()) {
267 return new Type(type, m_classNameReplacer);
268 } else {
269 return type;
270 }
271 }
272
273 private String scrubSignature(String signature) {
274 return scrubSignature(new Signature(signature)).toString();
275 }
276
277 private Signature scrubSignature(Signature signature) {
278 return new Signature(signature, m_classNameReplacer);
279 }
280
281 private boolean isClassMatchedUniquely(String className) {
282 return m_namer != null && m_namer.getName(Descriptor.toJvmName(className)) != null;
283 }
284
285 private String getBehaviorSignature(CtBehavior behavior) {
286 try {
287 // does this method have an implementation?
288 if (behavior.getMethodInfo().getCodeAttribute() == null) {
289 return "(none)";
290 }
291
292 // compute the hash from the opcodes
293 ConstPool constants = behavior.getMethodInfo().getConstPool();
294 final MessageDigest digest = MessageDigest.getInstance("MD5");
295 CodeIterator iter = behavior.getMethodInfo().getCodeAttribute().iterator();
296 while (iter.hasNext()) {
297 int pos = iter.next();
298
299 // update the hash with the opcode
300 int opcode = iter.byteAt(pos);
301 digest.update((byte)opcode);
302
303 switch (opcode) {
304 case Opcode.LDC: {
305 int constIndex = iter.byteAt(pos + 1);
306 updateHashWithConstant(digest, constants, constIndex);
307 }
308 break;
309
310 case Opcode.LDC_W:
311 case Opcode.LDC2_W: {
312 int constIndex = (iter.byteAt(pos + 1) << 8) | iter.byteAt(pos + 2);
313 updateHashWithConstant(digest, constants, constIndex);
314 }
315 break;
316 }
317 }
318
319 // update hash with method and field accesses
320 behavior.instrument(new ExprEditor() {
321 @Override
322 public void edit(MethodCall call) {
323 updateHashWithString(digest, scrubClassName(Descriptor.toJvmName(call.getClassName())));
324 updateHashWithString(digest, scrubSignature(call.getSignature()));
325 if (isClassMatchedUniquely(call.getClassName())) {
326 updateHashWithString(digest, call.getMethodName());
327 }
328 }
329
330 @Override
331 public void edit(FieldAccess access) {
332 updateHashWithString(digest, scrubClassName(Descriptor.toJvmName(access.getClassName())));
333 updateHashWithString(digest, scrubType(access.getSignature()));
334 if (isClassMatchedUniquely(access.getClassName())) {
335 updateHashWithString(digest, access.getFieldName());
336 }
337 }
338
339 @Override
340 public void edit(ConstructorCall call) {
341 updateHashWithString(digest, scrubClassName(Descriptor.toJvmName(call.getClassName())));
342 updateHashWithString(digest, scrubSignature(call.getSignature()));
343 }
344
345 @Override
346 public void edit(NewExpr expr) {
347 updateHashWithString(digest, scrubClassName(Descriptor.toJvmName(expr.getClassName())));
348 }
349 });
350
351 // convert the hash to a hex string
352 return toHex(digest.digest());
353 } catch (BadBytecode | NoSuchAlgorithmException | CannotCompileException ex) {
354 throw new Error(ex);
355 }
356 }
357
358 private void updateHashWithConstant(MessageDigest digest, ConstPool constants, int index) {
359 ConstPoolEditor editor = new ConstPoolEditor(constants);
360 ConstInfoAccessor item = editor.getItem(index);
361 if (item.getType() == InfoType.StringInfo) {
362 updateHashWithString(digest, constants.getStringInfo(index));
363 }
364 // TODO: other constants
365 }
366
367 private void updateHashWithString(MessageDigest digest, String val) {
368 try {
369 digest.update(val.getBytes("UTF8"));
370 } catch (UnsupportedEncodingException ex) {
371 throw new Error(ex);
372 }
373 }
374
375 private String toHex(byte[] bytes) {
376 // function taken from:
377 // http://stackoverflow.com/questions/9655181/convert-from-byte-array-to-hex-string-in-java
378 final char[] hexArray = "0123456789ABCDEF".toCharArray();
379 char[] hexChars = new char[bytes.length * 2];
380 for (int j = 0; j < bytes.length; j++) {
381 int v = bytes[j] & 0xFF;
382 hexChars[j * 2] = hexArray[v >>> 4];
383 hexChars[j * 2 + 1] = hexArray[v & 0x0F];
384 }
385 return new String(hexChars);
386 }
387
388 @Override
389 public boolean equals(Object other) {
390 if (other instanceof ClassIdentity) {
391 return equals((ClassIdentity)other);
392 }
393 return false;
394 }
395
396 public boolean equals(ClassIdentity other) {
397 return m_fields.equals(other.m_fields)
398 && m_methods.equals(other.m_methods)
399 && m_constructors.equals(other.m_constructors)
400 && m_staticInitializer.equals(other.m_staticInitializer)
401 && m_extends.equals(other.m_extends)
402 && m_implements.equals(other.m_implements)
403 && m_implementations.equals(other.m_implementations)
404 && m_references.equals(other.m_references);
405 }
406
407 @Override
408 public int hashCode() {
409 List<Object> objs = Lists.newArrayList();
410 objs.addAll(m_fields);
411 objs.addAll(m_methods);
412 objs.addAll(m_constructors);
413 objs.add(m_staticInitializer);
414 objs.add(m_extends);
415 objs.addAll(m_implements);
416 objs.addAll(m_implementations);
417 objs.addAll(m_references);
418 return Util.combineHashesOrdered(objs);
419 }
420
421 public int getMatchScore(ClassIdentity other) {
422 return 2*getNumMatches(m_extends, other.m_extends)
423 + 2*getNumMatches(m_outer, other.m_outer)
424 + 2*getNumMatches(m_implements, other.m_implements)
425 + getNumMatches(m_stringLiterals, other.m_stringLiterals)
426 + getNumMatches(m_fields, other.m_fields)
427 + getNumMatches(m_methods, other.m_methods)
428 + getNumMatches(m_constructors, other.m_constructors);
429 }
430
431 public int getMaxMatchScore() {
432 return 2 + 2 + 2*m_implements.size() + m_stringLiterals.size() + m_fields.size() + m_methods.size() + m_constructors.size();
433 }
434
435 public boolean matches(CtClass c) {
436 // just compare declaration counts
437 return m_fields.size() == c.getDeclaredFields().length
438 && m_methods.size() == c.getDeclaredMethods().length
439 && m_constructors.size() == c.getDeclaredConstructors().length;
440 }
441
442 private int getNumMatches(Set<String> a, Set<String> b) {
443 int numMatches = 0;
444 for (String val : a) {
445 if (b.contains(val)) {
446 numMatches++;
447 }
448 }
449 return numMatches;
450 }
451
452 private int getNumMatches(Multiset<String> a, Multiset<String> b) {
453 int numMatches = 0;
454 for (String val : a) {
455 if (b.contains(val)) {
456 numMatches++;
457 }
458 }
459 return numMatches;
460 }
461
462 private int getNumMatches(String a, String b) {
463 if (a.equals(b)) {
464 return 1;
465 }
466 return 0;
467 }
468}