/* * A Gradle plugin for the creation of Minecraft mods and MinecraftForge plugins. * Copyright (C) 2013 Minecraft Forge * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 * USA */ package net.minecraftforge.gradle.util.mcp; import static org.objectweb.asm.Opcodes.ACC_INTERFACE; import static org.objectweb.asm.Opcodes.GETFIELD; import static org.objectweb.asm.Opcodes.GETSTATIC; import static org.objectweb.asm.Opcodes.INVOKEINTERFACE; import static org.objectweb.asm.Opcodes.INVOKESPECIAL; import static org.objectweb.asm.Opcodes.INVOKESTATIC; import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL; import static org.objectweb.asm.Opcodes.PUTFIELD; import static org.objectweb.asm.Opcodes.PUTSTATIC; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.FieldVisitor; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import com.google.common.base.Joiner; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.io.ByteStreams; import com.google.common.io.Files; import com.google.common.io.LineProcessor; import de.oceanlabs.mcp.mcinjector.StringUtil; public class ReobfExceptor { // info supplied. public File toReobfJar; public File deobfJar; public File methodCSV; public File fieldCSV; public File excConfig; // state stuff Map<String, String> clsMap = Maps.newHashMap(); Map<String, String> access = Maps.newHashMap(); public void buildSrg(File inSrg, File outSrg) throws IOException { // build the SRG // delete if existing if (outSrg.isFile()) outSrg.delete(); // rewrite it. String fixed = Files.readLines(inSrg, Charset.defaultCharset(), new SrgLineProcessor(clsMap, access)); Files.write(fixed.getBytes(), outSrg); } /** * reads the Old jar, the EXC, and the CSVS * Hopefully, these things wont change. * @throws IOException because it reads the srg and jar files */ public void doFirstThings() throws IOException { Map<String, String> csvData = readCSVs(); JarInfo oldInfo = readJar(deobfJar); JarInfo newInfo = readJar(toReobfJar); clsMap = createClassMap(newInfo.map, newInfo.interfaces); renameAccess(oldInfo.access, csvData); access = mergeAccess(newInfo.access, oldInfo.access); } // Preliminary things here private Map<String, String> readCSVs() throws IOException { final Map<String, String> csvData = Maps.newHashMap(); File[] csvs = new File[] { fieldCSV == null ? null : fieldCSV, methodCSV == null ? null : methodCSV }; for (File f : csvs) { if (f == null) continue; Files.readLines(f, Charset.defaultCharset(), new LineProcessor<Object>() { @Override public boolean processLine(String line) throws IOException { String[] s = line.split(","); csvData.put(s[0], s[1]); return true; } @Override public Object getResult() { return null; } }); } return csvData; } // ACTUAL things here... private void renameAccess(Map<String, AccessInfo> data, Map<String, String> csvData) throws IOException { for (AccessInfo info : data.values()) { for (Insn i : info.insns) { String tmp = csvData.get(i.name); i.name = tmp == null ? i.name : tmp; } } } private JarInfo readJar(File inJar) throws IOException { ZipInputStream zip = null; try { try { zip = new ZipInputStream(new BufferedInputStream(new FileInputStream(inJar))); } catch (FileNotFoundException e) { throw new FileNotFoundException("Could not open input file: " + e.getMessage()); } JarInfo reader = new JarInfo(); while (true) { ZipEntry entry = zip.getNextEntry(); if (entry == null) break; if (entry.isDirectory() || !entry.getName().endsWith(".class")) continue; (new ClassReader(ByteStreams.toByteArray(zip))).accept(reader, 0); } return reader; } finally { if (zip != null) { try { zip.close(); } catch (IOException e){} } } } private Map<String, String> createClassMap(Map<String, String> markerMap, final List<String> interfaces) throws IOException { Map<String, String> excMap = Files.readLines(excConfig, Charset.defaultCharset(), new LineProcessor<Map<String, String>>() { Map<String, String> tmp = Maps.newHashMap(); @Override public boolean processLine(String line) throws IOException { if (line.contains(".") || !line.contains("=") || line.startsWith("#")) return true; String[] s = line.split("="); if (!interfaces.contains(s[0])) tmp.put(s[0], s[1] + "_"); return true; } @Override public Map<String, String> getResult() { return tmp; } }); Map<String, String> map = Maps.newHashMap(); for (Entry<String, String> e : excMap.entrySet()) { String renamed = markerMap.get(e.getValue()); if (renamed != null) { map.put(e.getKey(), renamed); } } return map; } private Map<String, String> mergeAccess(Map<String, AccessInfo> old_data, Map<String, AccessInfo> new_data) { // Lets remove things that are mapped exactly right: //System.out.println("Matches:"); Iterator<Entry<String, AccessInfo>> itr = old_data.entrySet().iterator(); while(itr.hasNext()) { Entry<String, AccessInfo> e = itr.next(); String key = e.getKey(); AccessInfo n = new_data.get(key); if (n != null && e.getValue().targetEquals(n)) { //System.out.println(" " + n.toString()); itr.remove(); new_data.remove(key); } } Map<String, String> matched = Maps.newHashMap(); //System.out.println("Matched: "); itr = old_data.entrySet().iterator(); while (itr.hasNext()) { AccessInfo _old = itr.next().getValue(); Iterator<Entry<String, AccessInfo>> itr2 = new_data.entrySet().iterator(); while (itr2.hasNext()) { Entry<String, AccessInfo> e2 = itr2.next(); AccessInfo _new = e2.getValue(); if (_old.targetEquals(_new) && _old.owner.equals(_new.owner) && _old.desc.equals(_new.desc)) { //System.out.println(" " + _old.name + " -> " + _new.name + " " + _old.toString()); matched.put(_old.owner + "/" + _old.name, _new.owner + "/" + _new.name); itr.remove(); itr2.remove(); break; } } } return matched; } private static class SrgLineProcessor implements LineProcessor<String> { Map<String, String> map; Map<String, String> access; StringBuilder out = new StringBuilder(); Pattern reg = Pattern.compile("L([^;]+);"); private SrgLineProcessor(Map<String, String> map, Map<String, String> access) { this.map = map; this.access = access; } private String rename(String cls) { String rename = map.get(cls); return rename == null ? cls : rename; } private String[] rsplit(String value, String delim) { int idx = value.lastIndexOf(delim); return new String[] { value.substring(0, idx), value.substring(idx + 1) }; } @Override public boolean processLine(String line) throws IOException { String[] split = line.split(" "); if (split[0].equals("CL:")) { split[2] = rename(split[2]); } else if (split[0].equals("FD:")) { String[] s = rsplit(split[2], "/"); split[2] = rename(s[0]) + "/" + s[1]; } else if (split[0].equals("MD:")) { String[] s = rsplit(split[3], "/"); split[3] = rename(s[0]) + "/" + s[1]; if (access.containsKey(split[3])) { split[3] = access.get(split[3]); } Matcher m = reg.matcher(split[4]); StringBuffer b = new StringBuffer(); while(m.find()) { m.appendReplacement(b, "L" + rename(m.group(1)).replace("$", "\\$") + ";"); } m.appendTail(b); split[4] = b.toString(); } out.append(StringUtil.joinString(Arrays.asList(split), " ")).append('\n'); return true; } @Override public String getResult() { return out.toString(); } } private static class JarInfo extends ClassVisitor { private final Map<String, String> map = Maps.newHashMap(); private final List<String> interfaces = Lists.newArrayList(); private final Map<String, AccessInfo> access = Maps.newHashMap(); public JarInfo() { super(Opcodes.ASM4, null); } private String className; @Override public void visit(int version, int access, String name, String signature, String superName, String[] ints) { //System.out.println("Class: " + name); this.className = name;; if ((access & ACC_INTERFACE) == ACC_INTERFACE) { interfaces.add(className); //System.out.println(" Interface: True"); } } @Override public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { if (name.equals("__OBFID")) { if (!className.startsWith("net/minecraft/")) { throw new RuntimeException("Modder stupidity detected, DO NOT USE __OBFID, Copy pasting code you don't understand is bad: " + className); } map.put(String.valueOf(value) + "_", className); //System.out.println(" Marker: " + String.valueOf(value)); } return null; } @Override public MethodVisitor visitMethod(int acc, String name, String desc, String signature, String[] exceptions) { if (className.startsWith("net/minecraft/") && name.startsWith("access$")) { String path = className + "/" + name + desc; final AccessInfo info = new AccessInfo(className, name, desc); info.access = acc; access.put(path, info); return new MethodVisitor(Opcodes.ASM5) { // GETSTATIC, PUTSTATIC, GETFIELD or PUTFIELD. @Override public void visitFieldInsn(int opcode, String owner, String name, String desc) { info.add(opcode, owner, name, desc); } // INVOKEVIRTUAL, INVOKESPECIAL, INVOKESTATIC or INVOKEINTERFACE. @Override public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { info.add(opcode, owner, name, desc); } }; } return null; } } @SuppressWarnings("unused") private static class AccessInfo { public String owner; public String name; public String desc; public int access; public List<Insn> insns = new ArrayList<Insn>(); private String cache = null; public AccessInfo(String owner, String name, String desc) { this.owner = owner; this.name = name; this.desc = desc; } public void add(int opcode, String owner, String name, String desc) { insns.add(new Insn(opcode, owner, name, desc)); cache = null; } @Override public String toString() { if (cache == null) { if (insns.size() < 1) throw new RuntimeException("Empty Intruction!!! IMPOSSIBURU"); cache = "[" + Joiner.on(", ").join(insns) + "]"; } return cache; } public boolean targetEquals(AccessInfo o) { return toString().equals(o.toString()); } } private static class Insn { public int opcode; public String owner; public String name; public String desc; Insn(int opcode, String owner, String name, String desc) { this.opcode = opcode; this.owner = owner; this.name = name; this.desc = desc; } @Override public String toString() { String op = "UNKNOWN_" + opcode; switch (opcode) { case GETSTATIC: op = "GETSTATIC"; break; case PUTSTATIC: op = "PUTSTATIC"; break; case GETFIELD: op = "GETFIELD"; break; case PUTFIELD: op = "PUTFIELD"; break; case INVOKEVIRTUAL: op = "INVOKEVIRTUAL"; break; case INVOKESPECIAL: op = "INVOKESPECIAL"; break; case INVOKESTATIC: op = "INVOKESTATIC"; break; case INVOKEINTERFACE: op = "INVOKEINTERFACE"; break; } return op + " " + owner + "/" + name + " " + desc; } } }