From 46cd6de355009f97dfbe6f0a547ef3b565fa35e8 Mon Sep 17 00:00:00 2001 From: SuspiciousActivity <38253929+SuspiciousActivity@users.noreply.github.com> Date: Wed, 9 Oct 2019 17:15:27 +0200 Subject: [PATCH] Init --- README.md | 5 + me/Unmapper/ClassNodeRemapper.java | 394 +++++++++++++++++++++++++++++ me/Unmapper/ClassUtil.java | 45 ++++ me/Unmapper/Main.java | 133 ++++++++++ me/Unmapper/MappingReader.java | 135 ++++++++++ me/Unmapper/MemberData.java | 69 +++++ 6 files changed, 781 insertions(+) create mode 100644 README.md create mode 100644 me/Unmapper/ClassNodeRemapper.java create mode 100644 me/Unmapper/ClassUtil.java create mode 100644 me/Unmapper/Main.java create mode 100644 me/Unmapper/MappingReader.java create mode 100644 me/Unmapper/MemberData.java diff --git a/README.md b/README.md new file mode 100644 index 0000000..6d5b22b --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# Unmapper +This simple program allows you to input an obfuscated jar file together with the corresponding mapping.txt to deobfuscate the jar file. +It uses ObjectWeb ASM (http://asm.ow2.org/) + +ProGuard (https://www.guardsquare.com/en/products/proguard) diff --git a/me/Unmapper/ClassNodeRemapper.java b/me/Unmapper/ClassNodeRemapper.java new file mode 100644 index 0000000..a0e72f2 --- /dev/null +++ b/me/Unmapper/ClassNodeRemapper.java @@ -0,0 +1,394 @@ +package me.Unmapper; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.objectweb.asm.Handle; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.FrameNode; +import org.objectweb.asm.tree.InnerClassNode; +import org.objectweb.asm.tree.InvokeDynamicInsnNode; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.LocalVariableNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.MultiANewArrayInsnNode; +import org.objectweb.asm.tree.TryCatchBlockNode; +import org.objectweb.asm.tree.TypeInsnNode; + +public class ClassNodeRemapper { + + private static final Pattern CLASS_NAME_PATTERN = Pattern.compile("(L[^;<]+[;<])"); + + private final HashMap classMappings; + private final HashMap memberMappings; + + public ClassNodeRemapper(HashMap classMappings, HashMap memberMappings) { + this.classMappings = classMappings; + this.memberMappings = memberMappings; + } + + public void remap(ClassNode cn) { + remapMethods(cn); + remapFields(cn); + cn.name = getOldClassName(cn.name); + cn.superName = getOldClassName(cn.superName); + cn.signature = getOldSignature(cn.signature); + if (!cn.interfaces.isEmpty()) { + List newInterfaces = new ArrayList(); + for (String s : cn.interfaces) { + newInterfaces.add(getOldClassName(s)); + } + cn.interfaces = newInterfaces; + } + if (cn.innerClasses != null && !cn.innerClasses.isEmpty()) { + for (InnerClassNode icn : cn.innerClasses) { + String full = getOldClassName(icn.name); + if (icn.outerName != null) { + String[] split = full.split("\\$"); + icn.outerName = split[0]; + if (icn.innerName != null) { + icn.innerName = split[1]; + } + } + icn.name = full; + } + } + } + + private static final int ACC_STATIC = 0x0008; + + private void remapMethods(ClassNode cn) { + if (cn.methods == null) + return; + String clazz = cn.name; + for (MethodNode mn : cn.methods) { + MemberData data = new MemberData(cn.name, mn.desc, mn.name); + mn.name = getOldMemberName(data); + mn.desc = getOldMemberMethodDesc(mn.desc); + mn.signature = getOldSignature(mn.signature); + if (!mn.exceptions.isEmpty()) { + List newExceptions = new ArrayList(); + for (String s : mn.exceptions) { + newExceptions.add(getOldClassName(s)); + } + mn.exceptions = newExceptions; + } + if (mn.localVariables != null && !mn.localVariables.isEmpty()) { + Iterator lvns = mn.localVariables.iterator(); + while (lvns.hasNext()) { + LocalVariableNode lvn = lvns.next(); + if (Main.removeLocals) { + if ((mn.access & ACC_STATIC) == 0 && lvn.index == 0) { // "this" for non-static methods + lvn.name = "this"; // should always be "this" + } else if (!isAscii(lvn.name)) { + lvns.remove(); + continue; + } + } + lvn.desc = getOldMemberFieldDesc(lvn.desc); + lvn.signature = getOldSignature(lvn.signature); + } + } + if (mn.tryCatchBlocks != null && !mn.tryCatchBlocks.isEmpty()) { + for (TryCatchBlockNode tcbn : mn.tryCatchBlocks) { + tcbn.type = getOldClassName(tcbn.type); + } + } + int len = mn.instructions.size(); + for (int i = 0; i < len; i++) { + remapInstruction(clazz, mn.instructions.get(i)); + } + } + } + + private void remapFields(ClassNode cn) { + if (cn.fields == null) + return; + for (FieldNode fn : cn.fields) { + MemberData data = new MemberData(cn.name, fn.desc, fn.name); + fn.name = getOldMemberName(data); + fn.desc = getOldMemberFieldDesc(fn.desc); + fn.signature = getOldSignature(fn.signature); + } + } + + private boolean isAscii(String s) { + for (char c : s.toCharArray()) { + if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))) + return false; + } + return true; + } + + private String getOldClassName(String name) { + if (name == null) + return null; + String old = classMappings.get(name); + if (old != null) + return old; + return name; + } + + private String getOldMemberName(MemberData data) { + if (data == null) + return null; + String old = memberMappings.get(data); + if (old != null) + return old; + return data.getName(); + } + + private String getOldMemberMethodDesc(String desc) { + if (desc == null) + return null; + int argsEndIdx = desc.indexOf(')'); + String retType = getOldMemberFieldDesc(desc.substring(argsEndIdx + 1)); + String args = desc.substring(1, argsEndIdx); + + int idx = 0; + char[] arr = args.toCharArray(); + StringBuilder outerSb = new StringBuilder("("); + + while (idx < arr.length) { + int arrayCount = 0; + for (; idx < arr.length && arr[idx] == '['; idx++) + arrayCount++; + + if (idx >= arr.length) // weird input? + return desc; + + StringBuilder sb = new StringBuilder(); + + switch (arr[idx]) { + case 'B': + case 'Z': + case 'I': + case 'J': + case 'C': + case 'D': + case 'F': + case 'S': + case 'V': + sb.append(arr[idx]); + idx++; + break; + case 'L': { + idx++; + int startIdx = idx; + for (; idx < arr.length && arr[idx] != ';'; idx++) + ; + sb.append('L'); + sb.append(getOldClassName(args.substring(startIdx, idx))); + sb.append(';'); + idx++; + break; + } + default: + break; + } + + for (int i = 0; i < arrayCount; i++) + sb.insert(0, '['); + + outerSb.append(sb); + } + + outerSb.append(')'); + outerSb.append(retType); + + return outerSb.toString(); + } + + private String getOldMemberFieldDesc(String desc) { + if (desc == null) + return null; + int idx = 0; + char[] arr = desc.toCharArray(); + + int arrayCount = 0; + for (; idx < arr.length && arr[idx] == '['; idx++) + arrayCount++; + + if (idx >= arr.length) // weird input? + return desc; + + StringBuilder sb = new StringBuilder(); + + switch (arr[idx]) { + case 'B': + case 'Z': + case 'I': + case 'J': + case 'C': + case 'D': + case 'F': + case 'S': + case 'V': + sb.append(arr[idx]); + break; + case 'L': { + idx++; + int startIdx = idx; + for (; idx < arr.length && arr[idx] != ';'; idx++) + ; + sb.append('L'); + sb.append(getOldClassName(desc.substring(startIdx, idx))); + sb.append(';'); + break; + } + default: + break; + } + + for (int i = 0; i < arrayCount; i++) + sb.insert(0, '['); + + return sb.toString(); + } + + private String getOldSignature(String s) { + if (s == null || s.isEmpty()) + return s; + if (s.startsWith("(")) { // Methods + int startIdx = s.indexOf('('); + int endIdx = s.indexOf(')'); + if (endIdx < 0) // Weird? + return s; + String typeType = s.substring(0, startIdx); + String args = s.substring(startIdx + 1, endIdx); + String retType = s.substring(endIdx + 1); + return getOldSignature(typeType) + "(" + getOldSignature(args) + ")" + getOldSignature(retType); + } else { + StringBuffer sb = new StringBuffer(); + Matcher m = CLASS_NAME_PATTERN.matcher(s); + while (m.find()) { + m.appendReplacement(sb, Matcher.quoteReplacement(getOldSignatureFromMatcher(m.group()))); + } + m.appendTail(sb); + return sb.toString(); + } + } + + private String getOldSignatureFromMatcher(String s) { + boolean endsWithBracket = s.endsWith("<"); + s = s.substring(1, s.length() - 1); + return "L" + getOldClassName(s) + (endsWithBracket ? "<" : ";"); + } + + private void remapInstruction(String clazz, AbstractInsnNode n) { + if (FieldInsnNode.class.isInstance(n)) { + FieldInsnNode node = (FieldInsnNode) n; + node.name = getOldMemberName(new MemberData(clazz, node.desc, node.name)); + node.desc = getOldMemberFieldDesc(node.desc); + node.owner = getOldClassName(node.owner); + } else if (FrameNode.class.isInstance(n)) { + FrameNode node = (FrameNode) n; + if (node.local != null) { + int len = node.local.size(); + for (int i = 0; i < len; i++) { + Object o = node.local.get(i); + if (o == null) + continue; + if (o instanceof String) { + String s = (String) o; + if (s.startsWith("[")) + s = getOldMemberFieldDesc(s); + else + s = getOldClassName(s); + node.local.set(i, s); + } + } + } + if (node.stack != null) { + int len = node.stack.size(); + for (int i = 0; i < len; i++) { + Object o = node.stack.get(i); + if (o == null) + continue; + if (o instanceof String) { + String s = (String) o; + if (s.startsWith("[")) + s = getOldMemberFieldDesc(s); + else + s = getOldClassName(s); + node.stack.set(i, s); + } + } + } + } else if (InvokeDynamicInsnNode.class.isInstance(n)) { + InvokeDynamicInsnNode node = (InvokeDynamicInsnNode) n; + node.name = getOldMemberName(new MemberData(clazz, node.desc, node.name)); + node.desc = getOldMemberMethodDesc(node.desc); + node.bsm = new Handle(node.bsm.getTag(), getOldClassName(node.bsm.getOwner()), + getOldMemberName(new MemberData(node.bsm.getOwner(), node.bsm.getDesc(), node.bsm.getName())), + getOldMemberMethodDesc(node.bsm.getDesc()), node.bsm.isInterface()); + int len = node.bsmArgs.length; + for (int i = 0; i < len; i++) { + Object o = node.bsmArgs[i]; + if (o instanceof Type) { + Type type = (Type) o; + try { + Field f_off = Type.class.getDeclaredField("off"); + f_off.setAccessible(true); + Field f_len = Type.class.getDeclaredField("len"); + f_len.setAccessible(true); + Field f_buf = Type.class.getDeclaredField("buf"); + f_buf.setAccessible(true); + String buf = new String((char[]) f_buf.get(type)); + String oldBuf = getOldClassName(buf); + f_buf.set(type, oldBuf.toCharArray()); + f_len.set(type, oldBuf.length()); + } catch (Exception e) { + e.printStackTrace(); + } + } else if (o instanceof Handle) { + Handle h = (Handle) o; + node.bsmArgs[i] = new Handle(h.getTag(), getOldClassName(h.getOwner()), + getOldMemberName(new MemberData(h.getOwner(), h.getDesc(), h.getName())), + getOldMemberMethodDesc(h.getDesc()), h.isInterface()); + } + } + } else if (MethodInsnNode.class.isInstance(n)) { + MethodInsnNode node = (MethodInsnNode) n; + node.name = getOldMemberName(new MemberData(clazz, node.desc, node.name)); + node.desc = getOldMemberMethodDesc(node.desc); + node.owner = getOldClassName(node.owner); + } else if (TypeInsnNode.class.isInstance(n)) { + TypeInsnNode node = (TypeInsnNode) n; + node.desc = getOldClassName(node.desc); + } else if (MultiANewArrayInsnNode.class.isInstance(n)) { + MultiANewArrayInsnNode node = (MultiANewArrayInsnNode) n; + node.desc = getOldClassName(node.desc); + } else if (LdcInsnNode.class.isInstance(n)) { + LdcInsnNode node = (LdcInsnNode) n; + if (node.cst instanceof Type) { + Type type = (Type) node.cst; + try { + Field f_off = Type.class.getDeclaredField("off"); + f_off.setAccessible(true); + Field f_len = Type.class.getDeclaredField("len"); + f_len.setAccessible(true); + Field f_buf = Type.class.getDeclaredField("buf"); + f_buf.setAccessible(true); + String buf = new String((char[]) f_buf.get(type)); + String oldBuf = getOldClassName(buf); + f_buf.set(type, oldBuf.toCharArray()); + f_len.set(type, oldBuf.length()); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } + +} diff --git a/me/Unmapper/ClassUtil.java b/me/Unmapper/ClassUtil.java new file mode 100644 index 0000000..a04547f --- /dev/null +++ b/me/Unmapper/ClassUtil.java @@ -0,0 +1,45 @@ +package me.Unmapper; + +import java.util.HashMap; + +public class ClassUtil { + + public static String getSimpleDesc(String type) { + String desc = ""; + for (int i = 0; i < countArraySize(type); i++) { + desc += "["; + } + type = type.replace("[]", ""); + if (VARS.containsKey(type)) { + desc += VARS.get(type); + } else { + desc += "L" + type + ";"; + } + return desc; + } + + private static int countArraySize(String type) { + int size = 0; + for (byte b : type.getBytes()) { + if (b == ']') { + size++; + } + } + return size; + } + + public static final HashMap VARS = new HashMap<>(); + + static { + VARS.put("byte", "B"); + VARS.put("char", "C"); + VARS.put("double", "D"); + VARS.put("float", "F"); + VARS.put("int", "I"); + VARS.put("long", "J"); + VARS.put("short", "S"); + VARS.put("boolean", "Z"); + VARS.put("void", "V"); + } + +} diff --git a/me/Unmapper/Main.java b/me/Unmapper/Main.java new file mode 100644 index 0000000..9854783 --- /dev/null +++ b/me/Unmapper/Main.java @@ -0,0 +1,133 @@ +package me.Unmapper; + +import java.io.File; +import java.io.FileOutputStream; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map.Entry; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; + +import org.apache.commons.io.IOUtils; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.ClassNode; + +public class Main implements Opcodes { + + private static HashMap OTHER_FILES = new HashMap<>(); + private static ArrayList classNodes = new ArrayList<>(); + private static MappingReader mapping; + + public static boolean removeLocals = false; + + public static void main(String[] args) throws Exception { + if (args.length < 3) { + System.out.println("Syntax: input.jar mapping.txt output.jar (-l)"); + System.out.println("\t-l\tRemoves local variables which are not ASCII"); + System.out.println("\t\t\t(let the decompilers generate better names)"); + System.exit(1); + } + if (args.length > 3) { + removeLocals = args[3].equalsIgnoreCase("-l"); + System.out.println("Enabled local variable removal."); + } + load(new File(args[0])); + map(new File(args[1])); + save(new File(args[2])); + } + + public static void map(File mappingFile) { + System.out.println("Reading mapping..."); + mapping = new MappingReader(mappingFile); + try { + mapping.load(); + } catch (Exception ex) { + new Exception("Error loading mapping", ex).printStackTrace(); + System.exit(1); + } + System.out.println("Read " + mapping.getClassMappings().size() + " class mappings and " + + mapping.getMemberMappings().size() + " member mappings."); + System.out.println("Remapping..."); + + ClassNodeRemapper remapper = new ClassNodeRemapper(mapping.getClassMappings(), mapping.getMemberMappings()); + for (ClassNode cn : classNodes) { + remapper.remap(cn); + } + } + + public static void save(File jar) { + System.out.println("Saving..."); + try { + try (final JarOutputStream output = new JarOutputStream(new FileOutputStream(jar))) { + for (Entry entry : OTHER_FILES.entrySet()) { + output.putNextEntry(entry.getKey()); + output.write(entry.getValue()); + output.closeEntry(); + } + for (ClassNode element : classNodes) { + ClassWriter writer = new ClassWriter(0); + element.accept(writer); + output.putNextEntry(new JarEntry(element.name + ".class")); + output.write(writer.toByteArray()); + output.closeEntry(); + } + } + } catch (Exception ex) { + new Exception("Error saving jar file", ex).printStackTrace(); + } + System.out.println("Saved!"); + } + + public static void load(File file) { + System.out.println("Loading..."); + try { + JarFile jar = new JarFile(file); + ArrayList entrysLeft = new ArrayList<>(); + Enumeration enumeration = jar.entries(); + while (enumeration.hasMoreElements()) { + JarEntry next = enumeration.nextElement(); + byte[] data = IOUtils.toByteArray(jar.getInputStream(next)); + if (next.getName().endsWith(".class")) { + ClassReader reader = new ClassReader(data); + ClassNode node = new ClassNode(); + reader.accept(node, 0); + + try { + classNodes.add(node); + } catch (NoClassDefFoundError e) { + entrysLeft.add(next); + } + } else { + OTHER_FILES.put(new JarEntry(next.getName()), data); + } + } + while (!entrysLeft.isEmpty()) { + for (JarEntry next : (ArrayList) entrysLeft.clone()) { + byte[] data = IOUtils.toByteArray(jar.getInputStream(next)); + if (next.getName().endsWith(".class")) { + ClassReader reader = new ClassReader(data); + ClassNode node = new ClassNode(); + reader.accept(node, 0); + try { + classNodes.add(node); + entrysLeft.remove(next); + } catch (NoClassDefFoundError e) { + + } + } else { + OTHER_FILES.put(new JarEntry(next.getName()), data); + } + } + } + jar.close(); + } catch (Exception ex) { + new Exception("Error loading jar file", ex).printStackTrace(); + } + System.out.println("Loaded!"); + } + +} diff --git a/me/Unmapper/MappingReader.java b/me/Unmapper/MappingReader.java new file mode 100644 index 0000000..4e6c0cd --- /dev/null +++ b/me/Unmapper/MappingReader.java @@ -0,0 +1,135 @@ +package me.Unmapper; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.HashMap; + +public class MappingReader { + + private final File mappingFile; + + private final HashMap internalReversedClassMappings = new HashMap<>(); + private final HashMap classMappings = new HashMap<>(); + private final HashMap memberMappings = new HashMap<>(); + + public MappingReader(File mappingFile) { + this.mappingFile = mappingFile; + } + + public void load() throws IOException { + try (BufferedReader read = new BufferedReader(new FileReader(mappingFile))) { + String className = null; + String s; + + while ((s = read.readLine()) != null) { + s = s.trim(); + + if (s.startsWith("#")) + continue; + + if (s.endsWith(":")) { + className = parseClassMapping(s); + } else if (className != null) { + parseClassMemberMapping(className, s); + } + } + } + } + + private String parseClassMapping(String s) { + int splitIdx = s.indexOf("->"); + if (splitIdx < 0) { + return null; + } + + int colonIndex = s.indexOf(':', splitIdx + 2); + if (colonIndex < 0) { + return null; + } + + String originalName = s.substring(0, splitIdx).trim().replace('.', '/'); + String obfuscatedName = s.substring(splitIdx + 2, colonIndex).trim().replace('.', '/'); + + classMappings.put(obfuscatedName, originalName); + internalReversedClassMappings.put(originalName, obfuscatedName); + + return originalName; + } + + private void parseClassMemberMapping(String className, String s) { + int colonIdx1 = s.indexOf(':'); + int colonIdx2 = colonIdx1 < 0 ? -1 : s.indexOf(':', colonIdx1 + 1); + int spaceIdx = s.indexOf(' ', colonIdx2 + 2); + int argIdx1 = s.indexOf('(', spaceIdx + 1); + int argIdx2 = argIdx1 < 0 ? -1 : s.indexOf(')', argIdx1 + 1); + int colonIdx3 = argIdx2 < 0 ? -1 : s.indexOf(':', argIdx2 + 1); + int colonIdx4 = colonIdx3 < 0 ? -1 : s.indexOf(':', colonIdx3 + 1); + int arrowIdx = s.indexOf("->", + (colonIdx4 >= 0 ? colonIdx4 : colonIdx3 >= 0 ? colonIdx3 : argIdx2 >= 0 ? argIdx2 : spaceIdx) + 1); + + if (spaceIdx < 0 || arrowIdx < 0) { + return; + } + + String type = s.substring(colonIdx2 + 1, spaceIdx).trim(); + String originalName = s.substring(spaceIdx + 1, argIdx1 >= 0 ? argIdx1 : arrowIdx).trim(); + String obfuscatedName = s.substring(arrowIdx + 2).trim(); + + String newClassName = className; + int dotIndex = originalName.lastIndexOf('.'); + if (dotIndex >= 0) { + className = originalName.substring(0, dotIndex); + originalName = originalName.substring(dotIndex + 1); + // How does this work? + System.out.println("Don't know how to handle mapping " + className + " to " + newClassName); + } + + if (type.length() > 0 && originalName.length() > 0 && obfuscatedName.length() > 0) { + if (argIdx2 < 0) { + memberMappings.put( + new MemberData(getOldClassName(className), convertTypeToByteCode(type), obfuscatedName), + originalName); + } else { + String args = s.substring(argIdx1 + 1, argIdx2).trim(); + + memberMappings.put(new MemberData(getOldClassName(className), + "(" + convertMultipleTypesToByteCode(args) + ")" + convertTypeToByteCode(type), obfuscatedName), + originalName); + } + } + } + + public HashMap getClassMappings() { + return classMappings; + } + + public HashMap getMemberMappings() { + return memberMappings; + } + + private String getOldClassName(String name) { + name = name.replace('.', '/'); + String old = internalReversedClassMappings.get(name); + if (old != null) + return old; + return name; + } + + private String convertTypeToByteCode(String type) { + if (type.isEmpty()) + return ""; + return ClassUtil.getSimpleDesc(getOldClassName(type)); + } + + private String convertMultipleTypesToByteCode(String types) { + if (types.isEmpty()) + return ""; + String ret = ""; + for (String s : types.split(",")) { + ret += convertTypeToByteCode(s); + } + return ret; + } +} diff --git a/me/Unmapper/MemberData.java b/me/Unmapper/MemberData.java new file mode 100644 index 0000000..7a46fc9 --- /dev/null +++ b/me/Unmapper/MemberData.java @@ -0,0 +1,69 @@ +package me.Unmapper; + +public class MemberData { + + private final String clazz; + private final String desc; + private final String name; + + public MemberData(String clazz, String desc, String name) { + this.clazz = clazz; + this.desc = desc; + this.name = name; + } + + public String getClazz() { + return clazz; + } + + public String getDesc() { + return desc; + } + + public String getName() { + return name; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((clazz == null) ? 0 : clazz.hashCode()); + result = prime * result + ((desc == null) ? 0 : desc.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + MemberData other = (MemberData) obj; + if (clazz == null) { + if (other.clazz != null) + return false; + } else if (!clazz.equals(other.clazz)) + return false; + if (desc == null) { + if (other.desc != null) + return false; + } else if (!desc.equals(other.desc)) + return false; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + return true; + } + + @Override + public String toString() { + return "MemberData [clazz=" + clazz + ", desc=" + desc + ", name=" + name + "]"; + } + +}