/*******************************************************************************
* Copyright (c) 2015 Jeff Martin.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Lesser General Public
* License v3.0 which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/lgpl.html
*
* Contributors:
* Jeff Martin - initial API and implementation
******************************************************************************/
package cuchaz.enigma.convert;
import com.google.common.collect.*;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Set;
import cuchaz.enigma.analysis.ClassImplementationsTreeNode;
import cuchaz.enigma.analysis.EntryReference;
import cuchaz.enigma.analysis.JarIndex;
import cuchaz.enigma.bytecode.ConstPoolEditor;
import cuchaz.enigma.bytecode.InfoType;
import cuchaz.enigma.bytecode.accessors.ConstInfoAccessor;
import cuchaz.enigma.convert.ClassNamer.SidedClassNamer;
import cuchaz.enigma.mapping.*;
import cuchaz.enigma.utils.Utils;
import javassist.*;
import javassist.bytecode.*;
import javassist.expr.*;
public class ClassIdentity {
private ClassEntry classEntry;
private SidedClassNamer namer;
private Multiset fields;
private Multiset methods;
private Multiset constructors;
private String staticInitializer;
private String extendz;
private Multiset implementz;
private Set stringLiterals;
private Multiset implementations;
private Multiset references;
private String outer;
private final ClassNameReplacer classNameReplacer = new ClassNameReplacer() {
private Map classNames = Maps.newHashMap();
@Override
public String replace(String className) {
// classes not in the none package can be passed through
ClassEntry classEntry = new ClassEntry(className);
if (classEntry.getPackageName() != null) {
return className;
}
// is this class ourself?
if (className.equals(classEntry.getName())) {
return "CSelf";
}
// try the namer
if (namer != null) {
String newName = namer.getName(className);
if (newName != null) {
return newName;
}
}
// otherwise, use local naming
if (!classNames.containsKey(className)) {
classNames.put(className, getNewClassName());
}
return classNames.get(className);
}
private String getNewClassName() {
return String.format("C%03d", classNames.size());
}
};
public ClassIdentity(CtClass c, SidedClassNamer namer, JarIndex index, boolean useReferences) {
this.namer = namer;
// stuff from the bytecode
this.classEntry = EntryFactory.getClassEntry(c);
this.fields = HashMultiset.create();
for (CtField field : c.getDeclaredFields()) {
this.fields.add(scrubType(field.getSignature()));
}
this.methods = HashMultiset.create();
for (CtMethod method : c.getDeclaredMethods()) {
this.methods.add(scrubSignature(method.getSignature()) + "0x" + getBehaviorSignature(method));
}
this.constructors = HashMultiset.create();
for (CtConstructor constructor : c.getDeclaredConstructors()) {
this.constructors.add(scrubSignature(constructor.getSignature()) + "0x" + getBehaviorSignature(constructor));
}
this.staticInitializer = "";
if (c.getClassInitializer() != null) {
this.staticInitializer = getBehaviorSignature(c.getClassInitializer());
}
this.extendz = "";
if (c.getClassFile().getSuperclass() != null) {
this.extendz = scrubClassName(Descriptor.toJvmName(c.getClassFile().getSuperclass()));
}
this.implementz = HashMultiset.create();
for (String interfaceName : c.getClassFile().getInterfaces()) {
this.implementz.add(scrubClassName(Descriptor.toJvmName(interfaceName)));
}
this.stringLiterals = Sets.newHashSet();
ConstPool constants = c.getClassFile().getConstPool();
for (int i = 1; i < constants.getSize(); i++) {
if (constants.getTag(i) == ConstPool.CONST_String) {
this.stringLiterals.add(constants.getStringInfo(i));
}
}
// stuff from the jar index
this.implementations = HashMultiset.create();
ClassImplementationsTreeNode implementationsNode = index.getClassImplementations(null, this.classEntry);
if (implementationsNode != null) {
@SuppressWarnings("unchecked")
Enumeration implementations = implementationsNode.children();
while (implementations.hasMoreElements()) {
ClassImplementationsTreeNode node = implementations.nextElement();
this.implementations.add(scrubClassName(node.getClassEntry().getName()));
}
}
this.references = HashMultiset.create();
if (useReferences) {
for (CtField field : c.getDeclaredFields()) {
FieldEntry fieldEntry = EntryFactory.getFieldEntry(field);
index.getFieldReferences(fieldEntry).forEach(this::addReference);
}
for (CtBehavior behavior : c.getDeclaredBehaviors()) {
BehaviorEntry behaviorEntry = EntryFactory.getBehaviorEntry(behavior);
index.getBehaviorReferences(behaviorEntry).forEach(this::addReference);
}
}
this.outer = null;
if (this.classEntry.isInnerClass()) {
this.outer = this.classEntry.getOuterClassName();
}
}
private void addReference(EntryReference extends Entry, BehaviorEntry> reference) {
if (reference.context.getSignature() != null) {
this.references.add(String.format("%s_%s",
scrubClassName(reference.context.getClassName()),
scrubSignature(reference.context.getSignature())
));
} else {
this.references.add(String.format("%s_",
scrubClassName(reference.context.getClassName())
));
}
}
public ClassEntry getClassEntry() {
return this.classEntry;
}
@Override
public String toString() {
StringBuilder buf = new StringBuilder();
buf.append("class: ");
buf.append(this.classEntry.getName());
buf.append(" ");
buf.append(hashCode());
buf.append("\n");
for (String field : this.fields) {
buf.append("\tfield ");
buf.append(field);
buf.append("\n");
}
for (String method : this.methods) {
buf.append("\tmethod ");
buf.append(method);
buf.append("\n");
}
for (String constructor : this.constructors) {
buf.append("\tconstructor ");
buf.append(constructor);
buf.append("\n");
}
if (this.staticInitializer.length() > 0) {
buf.append("\tinitializer ");
buf.append(this.staticInitializer);
buf.append("\n");
}
if (this.extendz.length() > 0) {
buf.append("\textends ");
buf.append(this.extendz);
buf.append("\n");
}
for (String interfaceName : this.implementz) {
buf.append("\timplements ");
buf.append(interfaceName);
buf.append("\n");
}
for (String implementation : this.implementations) {
buf.append("\timplemented by ");
buf.append(implementation);
buf.append("\n");
}
for (String reference : this.references) {
buf.append("\treference ");
buf.append(reference);
buf.append("\n");
}
buf.append("\touter ");
buf.append(this.outer);
buf.append("\n");
return buf.toString();
}
private String scrubClassName(String className) {
return classNameReplacer.replace(className);
}
private String scrubType(String typeName) {
return scrubType(new Type(typeName)).toString();
}
private Type scrubType(Type type) {
if (type.hasClass()) {
return new Type(type, classNameReplacer);
} else {
return type;
}
}
private String scrubSignature(String signature) {
return scrubSignature(new Signature(signature)).toString();
}
private Signature scrubSignature(Signature signature) {
return new Signature(signature, classNameReplacer);
}
private boolean isClassMatchedUniquely(String className) {
return this.namer != null && this.namer.getName(Descriptor.toJvmName(className)) != null;
}
private String getBehaviorSignature(CtBehavior behavior) {
try {
// does this method have an implementation?
if (behavior.getMethodInfo().getCodeAttribute() == null) {
return "(none)";
}
// compute the hash from the opcodes
ConstPool constants = behavior.getMethodInfo().getConstPool();
final MessageDigest digest = MessageDigest.getInstance("MD5");
CodeIterator iter = behavior.getMethodInfo().getCodeAttribute().iterator();
while (iter.hasNext()) {
int pos = iter.next();
// update the hash with the opcode
int opcode = iter.byteAt(pos);
digest.update((byte) opcode);
int constIndex;
switch (opcode) {
case Opcode.LDC:
constIndex = iter.byteAt(pos + 1);
updateHashWithConstant(digest, constants, constIndex);
break;
case Opcode.LDC_W:
case Opcode.LDC2_W:
constIndex = (iter.byteAt(pos + 1) << 8) | iter.byteAt(pos + 2);
updateHashWithConstant(digest, constants, constIndex);
break;
default:
break;
}
}
// update hash with method and field accesses
behavior.instrument(new ExprEditor() {
@Override
public void edit(MethodCall call) {
updateHashWithString(digest, scrubClassName(Descriptor.toJvmName(call.getClassName())));
updateHashWithString(digest, scrubSignature(call.getSignature()));
if (isClassMatchedUniquely(call.getClassName())) {
updateHashWithString(digest, call.getMethodName());
}
}
@Override
public void edit(FieldAccess access) {
updateHashWithString(digest, scrubClassName(Descriptor.toJvmName(access.getClassName())));
updateHashWithString(digest, scrubType(access.getSignature()));
if (isClassMatchedUniquely(access.getClassName())) {
updateHashWithString(digest, access.getFieldName());
}
}
@Override
public void edit(ConstructorCall call) {
updateHashWithString(digest, scrubClassName(Descriptor.toJvmName(call.getClassName())));
updateHashWithString(digest, scrubSignature(call.getSignature()));
}
@Override
public void edit(NewExpr expr) {
updateHashWithString(digest, scrubClassName(Descriptor.toJvmName(expr.getClassName())));
}
});
// convert the hash to a hex string
return toHex(digest.digest());
} catch (BadBytecode | NoSuchAlgorithmException | CannotCompileException ex) {
throw new Error(ex);
}
}
private void updateHashWithConstant(MessageDigest digest, ConstPool constants, int index) {
ConstPoolEditor editor = new ConstPoolEditor(constants);
ConstInfoAccessor item = editor.getItem(index);
if (item.getType() == InfoType.StringInfo) {
updateHashWithString(digest, constants.getStringInfo(index));
}
// TODO: other constants
}
private void updateHashWithString(MessageDigest digest, String val) {
try {
digest.update(val.getBytes("UTF8"));
} catch (UnsupportedEncodingException ex) {
throw new Error(ex);
}
}
private String toHex(byte[] bytes) {
// function taken from:
// http://stackoverflow.com/questions/9655181/convert-from-byte-array-to-hex-string-in-java
final char[] hexArray = "0123456789ABCDEF".toCharArray();
char[] hexChars = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = hexArray[v >>> 4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}
@Override
public boolean equals(Object other) {
return other instanceof ClassIdentity && equals((ClassIdentity) other);
}
public boolean equals(ClassIdentity other) {
return this.fields.equals(other.fields)
&& this.methods.equals(other.methods)
&& this.constructors.equals(other.constructors)
&& this.staticInitializer.equals(other.staticInitializer)
&& this.extendz.equals(other.extendz)
&& this.implementz.equals(other.implementz)
&& this.implementations.equals(other.implementations)
&& this.references.equals(other.references);
}
@Override
public int hashCode() {
List