Java tutorial
/* * 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.tasks; import static org.objectweb.asm.Opcodes.ACC_FINAL; import static org.objectweb.asm.Opcodes.ACC_PRIVATE; import static org.objectweb.asm.Opcodes.ACC_PROTECTED; import static org.objectweb.asm.Opcodes.ACC_PUBLIC; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; import org.gradle.api.file.FileCollection; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.InputFile; import org.gradle.api.tasks.InputFiles; import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.OutputFile; import org.gradle.api.tasks.TaskAction; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.FieldNode; import org.objectweb.asm.tree.MethodNode; import com.google.common.base.Charsets; 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.MCInjectorImpl; import groovy.lang.Closure; import net.md_5.specialsource.AccessMap; import net.md_5.specialsource.Jar; import net.md_5.specialsource.JarMapping; import net.md_5.specialsource.JarRemapper; import net.md_5.specialsource.RemapperProcessor; import net.md_5.specialsource.provider.JarProvider; import net.md_5.specialsource.provider.JointProvider; import net.minecraftforge.gradle.common.Constants; import net.minecraftforge.gradle.util.caching.Cached; import net.minecraftforge.gradle.util.caching.CachedTask; import net.minecraftforge.gradle.util.json.JsonFactory; import net.minecraftforge.gradle.util.json.MCInjectorStruct; import net.minecraftforge.gradle.util.json.MCInjectorStruct.InnerClass; public class DeobfuscateJar extends CachedTask { @InputFile @Optional private Object fieldCsv; @InputFile @Optional private Object methodCsv; @InputFile private Object inJar; @InputFile private Object srg; @InputFile private Object exceptorCfg; @InputFile private Object exceptorJson; @Input private boolean applyMarkers = false; @Optional @Input private boolean stripSynthetics = false; @Input private boolean failOnAtError = true; private Object outJar; @InputFiles private ArrayList<Object> ats = Lists.newArrayList(); private Object log; @TaskAction public void doTask() throws IOException { // make stuff into files. File tempObfJar = new File(getTemporaryDir(), "deobfed.jar"); // courtesy of gradle temp dir. File out = getOutJar(); File tempExcJar = stripSynthetics ? new File(getTemporaryDir(), "excpeted.jar") : out; // courtesy of gradle temp dir. // make the ATs list.. its a Set to avoid duplication. Set<File> ats = new HashSet<File>(); for (Object obj : this.ats) { ats.add(getProject().file(obj).getCanonicalFile()); } // deobf getLogger().lifecycle("Applying SpecialSource..."); deobfJar(getInJar(), tempObfJar, getSrg(), ats); File log = getLog(); if (log == null) log = new File(getTemporaryDir(), "exceptor.log"); // apply exceptor getLogger().lifecycle("Applying Exceptor..."); applyExceptor(tempObfJar, tempExcJar, getExceptorCfg(), log, ats); if (stripSynthetics) { // strip out synthetics that arnt from enums.. getLogger().lifecycle("Stripping synthetics..."); stripSynthetics(tempExcJar, out); } } private void deobfJar(File inJar, File outJar, File srg, Collection<File> ats) throws IOException { // load mapping JarMapping mapping = new JarMapping(); mapping.loadMappings(srg); // load in ATs ErroringRemappingAccessMap accessMap = new ErroringRemappingAccessMap( new File[] { getMethodCsv(), getFieldCsv() }); getLogger().info("Using AccessTransformers..."); //Make SS shutup about access maps for (File at : ats) { getLogger().info("" + at); accessMap.loadAccessTransformer(at); } // System.setOut(tmp); // make a processor out of the ATS and mappings. RemapperProcessor srgProcessor = new RemapperProcessor(null, mapping, null); RemapperProcessor atProcessor = new RemapperProcessor(null, null, accessMap); // make remapper JarRemapper remapper = new JarRemapper(srgProcessor, mapping, atProcessor); // load jar Jar input = Jar.init(inJar); // ensure that inheritance provider is used JointProvider inheritanceProviders = new JointProvider(); inheritanceProviders.add(new JarProvider(input)); mapping.setFallbackInheritanceProvider(inheritanceProviders); // remap jar remapper.remapJar(input, outJar); // throw error for broken AT lines if (accessMap.brokenLines.size() > 0 && failOnAtError) { getLogger().error("{} Broken Access Transformer lines:", accessMap.brokenLines.size()); for (String line : accessMap.brokenLines.values()) { getLogger().error(" --- {}", line); } // TODO: add info for disabling throw new RuntimeException("Your Access Transformers be broke!"); } } private int fixAccess(int access, String target) { int ret = access & ~7; int t = 0; if (target.startsWith("public")) t = ACC_PUBLIC; else if (target.startsWith("private")) t = ACC_PRIVATE; else if (target.startsWith("protected")) t = ACC_PROTECTED; switch (access & 7) { case ACC_PRIVATE: ret |= t; break; case 0: ret |= (t != ACC_PRIVATE ? t : 0); break; case ACC_PROTECTED: ret |= (t != ACC_PRIVATE && t != 0 ? t : ACC_PROTECTED); break; case ACC_PUBLIC: ret |= ACC_PUBLIC; break; } if (target.endsWith("-f")) ret &= ~ACC_FINAL; else if (target.endsWith("+f")) ret |= ACC_FINAL; return ret; } public void applyExceptor(File inJar, File outJar, File config, File log, Set<File> ats) throws IOException { String json = null; File getJson = getExceptorJson(); if (getJson != null) { final Map<String, MCInjectorStruct> struct = JsonFactory.loadMCIJson(getJson); for (File at : ats) { getLogger().info("loading AT: " + at.getCanonicalPath()); Files.readLines(at, Charset.defaultCharset(), new LineProcessor<Object>() { @Override public boolean processLine(String line) throws IOException { if (line.indexOf('#') != -1) line = line.substring(0, line.indexOf('#')); line = line.trim().replace('.', '/'); if (line.isEmpty()) return true; String[] s = line.split(" "); if (s.length == 2 && s[1].indexOf('$') > 0) { String parent = s[1].substring(0, s[1].indexOf('$')); for (MCInjectorStruct cls : new MCInjectorStruct[] { struct.get(parent), struct.get(s[1]) }) { if (cls != null && cls.innerClasses != null) { for (InnerClass inner : cls.innerClasses) { if (inner.inner_class.equals(s[1])) { int access = fixAccess(inner.getAccess(), s[0]); inner.access = (access == 0 ? null : Integer.toHexString(access)); } } } } } return true; } @Override public Object getResult() { return null; } }); } // Remove unknown classes from configuration removeUnknownClasses(inJar, struct); File jsonTmp = new File(this.getTemporaryDir(), "transformed.json"); json = jsonTmp.getCanonicalPath(); Files.write(JsonFactory.GSON.toJson(struct).getBytes(), jsonTmp); } getLogger().debug("INPUT: " + inJar); getLogger().debug("OUTPUT: " + outJar); getLogger().debug("CONFIG: " + config); getLogger().debug("JSON: " + json); getLogger().debug("LOG: " + log); getLogger().debug("PARAMS: true"); MCInjectorImpl.process(inJar.getCanonicalPath(), outJar.getCanonicalPath(), config.getCanonicalPath(), log.getCanonicalPath(), null, 0, json, isApplyMarkers(), true); } private void removeUnknownClasses(File inJar, Map<String, MCInjectorStruct> config) throws IOException { ZipFile zip = new ZipFile(inJar); try { Iterator<Map.Entry<String, MCInjectorStruct>> entries = config.entrySet().iterator(); while (entries.hasNext()) { Map.Entry<String, MCInjectorStruct> entry = entries.next(); String className = entry.getKey(); // Verify the configuration contains only classes we actually have if (zip.getEntry(className + ".class") == null) { getLogger().info("Removing unknown class {}", className); entries.remove(); continue; } MCInjectorStruct struct = entry.getValue(); // Verify the inner classes in the configuration actually exist in our deobfuscated JAR file if (struct.innerClasses != null) { Iterator<InnerClass> innerClasses = struct.innerClasses.iterator(); while (innerClasses.hasNext()) { InnerClass innerClass = innerClasses.next(); if (zip.getEntry(innerClass.inner_class + ".class") == null) { getLogger().info("Removing unknown inner class {} from {}", innerClass.inner_class, className); innerClasses.remove(); } } } } } finally { zip.close(); } } private void stripSynthetics(File inJar, File outJar) throws IOException { ZipFile in = new ZipFile(inJar); final ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(outJar))); for (ZipEntry e : Collections.list(in.entries())) { if (e.getName().contains("META-INF")) continue; if (e.isDirectory()) { out.putNextEntry(e); } else { ZipEntry n = new ZipEntry(e.getName()); n.setTime(e.getTime()); out.putNextEntry(n); byte[] data = ByteStreams.toByteArray(in.getInputStream(e)); // correct source name if (e.getName().endsWith(".class")) data = stripSynthetics(e.getName(), data); out.write(data); } } out.flush(); out.close(); in.close(); } private byte[] stripSynthetics(String name, byte[] data) { ClassReader reader = new ClassReader(data); ClassNode node = new ClassNode(); reader.accept(node, 0); if ((node.access & Opcodes.ACC_ENUM) == 0 && !node.superName.equals("java/lang/Enum") && (node.access & Opcodes.ACC_SYNTHETIC) == 0) { // ^^ is for ignoring enums. for (FieldNode f : ((List<FieldNode>) node.fields)) { f.access = f.access & (0xffffffff - Opcodes.ACC_SYNTHETIC); //getLogger().lifecycle("Stripping field: "+f.name); } for (MethodNode m : ((List<MethodNode>) node.methods)) { m.access = m.access & (0xffffffff - Opcodes.ACC_SYNTHETIC); //getLogger().lifecycle("Stripping method: "+m.name); } } ClassWriter writer = new ClassWriter(0); node.accept(writer); return writer.toByteArray(); } public File getExceptorCfg() { return getProject().file(exceptorCfg); } public void setExceptorCfg(Object exceptorCfg) { this.exceptorCfg = exceptorCfg; } public File getExceptorJson() { if (exceptorJson == null) return null; else return getProject().file(exceptorJson); } public void setExceptorJson(Object exceptorJson) { this.exceptorJson = exceptorJson; } public boolean isApplyMarkers() { return applyMarkers; } public void setApplyMarkers(boolean applyMarkers) { this.applyMarkers = applyMarkers; } public boolean isFailOnAtError() { return failOnAtError; } public void setFailOnAtError(boolean failOnAtError) { this.failOnAtError = failOnAtError; } public File getInJar() { return getProject().file(inJar); } public void setInJar(Object inJar) { this.inJar = inJar; } public File getLog() { if (log == null) return null; else return getProject().file(log); } public void setLog(Object log) { this.log = log; } public File getSrg() { return getProject().file(srg); } public void setSrg(Object srg) { this.srg = srg; } /** * returns the actual output file depending on Clean status * @return File representing output jar */ @Cached @OutputFile public File getOutJar() { return getProject().file(outJar); } public void setOutJar(Object outJar) { this.outJar = outJar; } /** * returns the actual output Object depending on Clean status * Unlike getOutputJar() this method does not resolve the files. * @return Object that will resolve to */ @SuppressWarnings("serial") public Closure<File> getDelayedOutput() { return new Closure<File>(getProject(), this) { public File call() { return getOutJar(); } }; } /** * adds an access transformer to the deobfuscation of this * @param obj access transformers */ public void addAt(Object obj) { ats.add(obj); } /** * adds access transformers to the deobfuscation of this * @param objs access transformers */ public void addAts(Object... objs) { for (Object object : objs) { ats.add(object); } } /** * adds access transformers to the deobfuscation of this * @param objs access transformers */ public void addAts(Iterable<Object> objs) { for (Object object : objs) { ats.add(object); } } public FileCollection getAts() { return getProject().files(ats.toArray()); } public File getFieldCsv() { return fieldCsv == null ? null : getProject().file(fieldCsv); } public void setFieldCsv(Object fieldCsv) { this.fieldCsv = fieldCsv; } public File getMethodCsv() { return methodCsv == null ? null : getProject().file(methodCsv); } public void setMethodCsv(Object methodCsv) { this.methodCsv = methodCsv; } public boolean getStripSynthetics() { return stripSynthetics; } public void setStripSynthetics(boolean stripSynthetics) { this.stripSynthetics = stripSynthetics; } private static final class ErroringRemappingAccessMap extends AccessMap { private final Map<String, String> renames = Maps.newHashMap(); public final Map<String, String> brokenLines = Maps.newHashMap(); public ErroringRemappingAccessMap(File[] renameCsvs) throws IOException { super(); for (File f : renameCsvs) { if (f == null) continue; Files.readLines(f, Charsets.UTF_8, new LineProcessor<String>() { @Override public boolean processLine(String line) throws IOException { String[] pts = line.split(","); if (!"searge".equals(pts[0])) { renames.put(pts[0], pts[1]); } return true; } @Override public String getResult() { return null; } }); } } @Override public void loadAccessTransformer(File file) throws IOException { // because SS doesnt close its freaking reader... BufferedReader reader = Files.newReader(file, Constants.CHARSET); loadAccessTransformer(reader); reader.close(); } @Override public void addAccessChange(String symbolString, String accessString) { String[] pts = symbolString.split(" "); if (pts.length >= 2) { int idx = pts[1].indexOf('('); String start = pts[1]; String end = ""; if (idx != -1) { start = pts[1].substring(0, idx); end = pts[1].substring(idx); } String rename = renames.get(start); if (rename != null) { pts[1] = rename + end; } } String joinedString = Joiner.on('.').join(pts); super.addAccessChange(joinedString, accessString); // convert package.class to package/class brokenLines.put(joinedString.replace('.', '/'), symbolString); } @Override protected void accessApplied(String key, int oldAccess, int newAccess) { // if the access' are equal, then the line is broken, and we dont want to remove it.\ // or not... it still applied.. just applied twice somehow.. not an issue. // if (oldAccess != newAccess) { // key added before is in format: package/class{method/field sig} // and the key here is in format: package/class {method/field sig} brokenLines.remove(key.replace(" ", "")); } } } }