net.minecraftforge.gradle.user.patcherUser.TaskApplyBinPatches.java Source code

Java tutorial

Introduction

Here is the source code for net.minecraftforge.gradle.user.patcherUser.TaskApplyBinPatches.java

Source

/*
 * 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.user.patcherUser;

import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.jar.JarOutputStream;
import java.util.jar.Pack200;
import java.util.regex.Pattern;
import java.util.zip.Adler32;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

import org.gradle.api.file.FileVisitDetails;
import org.gradle.api.file.FileVisitor;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;

import com.google.common.base.Joiner;
import com.google.common.base.Throwables;
import com.google.common.collect.Maps;
import com.google.common.io.ByteArrayDataInput;
import com.google.common.io.ByteStreams;
import com.nothome.delta.GDiffPatcher;

import lzma.sdk.lzma.Decoder;
import lzma.streams.LzmaInputStream;
import net.minecraftforge.gradle.util.ZipFileTree;
import net.minecraftforge.gradle.util.caching.Cached;
import net.minecraftforge.gradle.util.caching.CachedTask;

public class TaskApplyBinPatches extends CachedTask {
    //@formatter:off
    @InputFile
    Object inJar;
    @InputFile
    Object classesJar;
    @InputFile
    Object resourcesJar;
    @InputFile
    Object patches;
    //@formatter:on

    @OutputFile
    @Cached
    Object outJar;

    private HashMap<String, ClassPatch> patchlist = Maps.newHashMap();
    private GDiffPatcher patcher = new GDiffPatcher();

    @TaskAction
    public void doTask() throws IOException {
        setup();

        if (getOutJar().exists()) {
            getOutJar().delete();
        }

        ZipFile in = new ZipFile(getInJar());
        ZipInputStream classesIn = new ZipInputStream(new FileInputStream(getClassJar()));
        final ZipOutputStream out = new ZipOutputStream(
                new BufferedOutputStream(new FileOutputStream(getOutJar())));
        final HashSet<String> entries = new HashSet<String>();

        try {
            // DO PATCHES
            log("Patching Class:");
            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));
                    ClassPatch patch = patchlist.get(e.getName().replace('\\', '/'));

                    if (patch != null) {
                        log("\t%s (%s) (input size %d)", patch.targetClassName, patch.sourceClassName, data.length);
                        int inputChecksum = adlerHash(data);
                        if (patch.inputChecksum != inputChecksum) {
                            throw new RuntimeException(String.format(
                                    "There is a binary discrepency between the expected input class %s (%s) and the actual class. Checksum on disk is %x, in patch %x. Things are probably about to go very wrong. Did you put something into the jar file?",
                                    patch.targetClassName, patch.sourceClassName, inputChecksum,
                                    patch.inputChecksum));
                        }
                        synchronized (patcher) {
                            data = patcher.patch(data, patch.patch);
                        }
                    }

                    out.write(data);
                }

                // add the names to the hashset
                entries.add(e.getName());
            }

            // COPY DATA
            ZipEntry entry = null;
            while ((entry = classesIn.getNextEntry()) != null) {
                if (entries.contains(entry.getName()))
                    continue;

                out.putNextEntry(entry);
                out.write(ByteStreams.toByteArray(classesIn));
                entries.add(entry.getName());
            }

            new ZipFileTree(getResourceJar()).visit(new FileVisitor() {
                @Override
                public void visitDir(FileVisitDetails dirDetails) {
                }

                @Override
                public void visitFile(FileVisitDetails file) {
                    try {
                        String name = file.getRelativePath().toString().replace('\\', '/');
                        if (!entries.contains(name)) {
                            ZipEntry n = new ZipEntry(name);
                            n.setTime(file.getLastModified());
                            out.putNextEntry(n);
                            ByteStreams.copy(file.open(), out);
                            entries.add(name);
                        }
                    } catch (IOException e) {
                        Throwables.propagateIfPossible(e);
                    }
                }

            });
        } finally {
            classesIn.close();
            in.close();
            out.close();
        }
    }

    private int adlerHash(byte[] input) {
        Adler32 hasher = new Adler32();
        hasher.update(input);
        return (int) hasher.getValue();
    }

    public void setup() {
        Pattern matcher = Pattern.compile(String.format("binpatch/merged/.*.binpatch"));

        JarInputStream jis;
        try {
            LzmaInputStream binpatchesDecompressed = new LzmaInputStream(new FileInputStream(getPatches()),
                    new Decoder());
            ByteArrayOutputStream jarBytes = new ByteArrayOutputStream();
            JarOutputStream jos = new JarOutputStream(jarBytes);
            Pack200.newUnpacker().unpack(binpatchesDecompressed, jos);
            jis = new JarInputStream(new ByteArrayInputStream(jarBytes.toByteArray()));
        } catch (Exception e) {
            throw Throwables.propagate(e);
        }

        log("Reading Patches:");
        do {
            try {
                JarEntry entry = jis.getNextJarEntry();
                if (entry == null) {
                    break;
                }

                if (matcher.matcher(entry.getName()).matches()) {
                    ClassPatch cp = readPatch(entry, jis);
                    patchlist.put(cp.sourceClassName.replace('.', '/') + ".class", cp);
                } else {
                    jis.closeEntry();
                }
            } catch (IOException e) {
            }
        } while (true);
        log("Read %d binary patches", patchlist.size());
        log("Patch list :\n\t%s", Joiner.on("\n\t").join(patchlist.entrySet()));
    }

    private ClassPatch readPatch(JarEntry patchEntry, JarInputStream jis) throws IOException {
        log("\t%s", patchEntry.getName());
        ByteArrayDataInput input = ByteStreams.newDataInput(ByteStreams.toByteArray(jis));

        String name = input.readUTF();
        String sourceClassName = input.readUTF();
        String targetClassName = input.readUTF();
        boolean exists = input.readBoolean();
        int inputChecksum = 0;
        if (exists) {
            inputChecksum = input.readInt();
        }
        int patchLength = input.readInt();
        byte[] patchBytes = new byte[patchLength];
        input.readFully(patchBytes);

        return new ClassPatch(name, sourceClassName, targetClassName, exists, inputChecksum, patchBytes);
    }

    private void log(String format, Object... args) {
        getLogger().debug(String.format(format, args));
    }

    public File getInJar() {
        return getProject().file(inJar);
    }

    public void setInJar(Object inJar) {
        this.inJar = inJar;
    }

    public File getOutJar() {
        return getProject().file(outJar);
    }

    public void setOutJar(Object outJar) {
        this.outJar = outJar;
    }

    public File getPatches() {
        return getProject().file(patches);
    }

    public void setPatches(Object patchesJar) {
        this.patches = patchesJar;
    }

    public File getClassJar() {
        return getProject().file(classesJar);
    }

    public void setClassJar(Object extraJar) {
        this.classesJar = extraJar;
    }

    public File getResourceJar() {
        return getProject().file(resourcesJar);
    }

    public void setResourceJar(Object resources) {
        this.resourcesJar = resources;
    }

    public static class ClassPatch {
        public final String name;
        public final String sourceClassName;
        public final String targetClassName;
        public final boolean existsAtTarget;
        public final byte[] patch;
        public final int inputChecksum;

        public ClassPatch(String name, String sourceClassName, String targetClassName, boolean existsAtTarget,
                int inputChecksum, byte[] patch) {
            this.name = name;
            this.sourceClassName = sourceClassName;
            this.targetClassName = targetClassName;
            this.existsAtTarget = existsAtTarget;
            this.inputChecksum = inputChecksum;
            this.patch = patch;
        }

        @Override
        public String toString() {
            return String.format("%s : %s => %s (%b) size %d", name, sourceClassName, targetClassName,
                    existsAtTarget, patch.length);
        }
    }
}