All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.minecraftforge.gradle.tasks.PatchSourcesTask Maven / Gradle / Ivy

/*
 * A Gradle plugin for the creation of Minecraft mods and MinecraftForge plugins.
 * Copyright (C) 2013-2019 Minecraft Forge
 * Copyright (C) 2020-2021 anatawa12 and other contributors
 *
 * 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 java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import net.minecraftforge.gradle.common.Constants;
import net.minecraftforge.gradle.util.GradleConfigurationException;
import net.minecraftforge.gradle.util.ThrowableUtil;
import net.minecraftforge.gradle.util.patching.ContextualPatch;
import net.minecraftforge.gradle.util.patching.ContextualPatch.PatchStatus;

import org.gradle.api.file.FileCollection;
import org.gradle.api.file.FileVisitDetails;
import org.gradle.api.file.FileVisitor;
import org.gradle.api.logging.LogLevel;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputDirectory;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.Optional;

import com.cloudbees.diff.PatchException;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import com.google.common.io.Files;

public class PatchSourcesTask extends AbstractEditJarTask
{
    /*
     * TODO: optimization plans
     * 1) doStufBefore> read all the patch files into a map as strings stored by relative path.
     * 2) doStufBefore> Create a threaded Executor to patch the files
     * 3) AsRead > kick off the patching threads
     * 4) ????
     * 5) profit from multithreaded oatching. Its CPU bound anyways.
     */
    private int                    maxFuzz       = 0;
    private int                    patchStrip    = 3;
    private boolean                makeRejects   = true;
    private boolean                failOnError   = false;
    private Object                 patches;
    private List           injects       = Lists.newArrayList();

    // stateful pieces of this task
    private ContextProvider        context;
    private ArrayList loadedPatches = Lists.newArrayList();

    @Override
    public void doStuffBefore() throws IOException
    {
        getLogger().info("Reading patches");

        // create context provider
        context = new ContextProvider(null, patchStrip); // add in the map later.

        // collect patchFiles and add them to the listing
        File patchThingy = getPatches(); // cached for the if statements
        final int fuzz = getMaxFuzz();

        if (patchThingy.isDirectory())
        {
            for (File f : getProject().fileTree(getPatches()))
            {
                if (!f.exists() || f.isDirectory() || !f.getName().endsWith("patch"))
                {
                    continue;
                }

                loadedPatches.add(new PatchedFile(f, context, fuzz));
            }
        }
        else if (patchThingy.getName().endsWith(".jar") || patchThingy.getName().endsWith(".zip"))
        {
            // no rejects from a jar
            makeRejects = false;

            getProject().zipTree(patchThingy).visit(new FileVisitor() {

                @Override
                public void visitDir(FileVisitDetails arg0)
                {
                    // nope.
                }

                @Override
                public void visitFile(FileVisitDetails details)
                {
                    ByteArrayOutputStream stream = new ByteArrayOutputStream();
                    details.copyTo(stream);
                    String file = new String(stream.toByteArray(), Constants.CHARSET);
                    loadedPatches.add(new PatchedFile(file, context, fuzz));
                }

            });
            ;
        }
        else
        {
            throw new GradleConfigurationException("Patches (" + patchThingy.getPath() + ") is not a valid type! only zips, jars, and directories are allowed.");
        }
    }

    @Override
    public void doStuffMiddle(final Map sourceMap, final Map resourceMap) throws Exception
    {
        // Inject injects
        getLogger().info("Injecting injects (sources and resources)");
        this.inject(getInjects(), sourceMap, resourceMap);

        // fix the context provider
        context.fileMap = sourceMap;

        // apply patches
        getLogger().info("Applying patches");
        applyPatches();
    }

    private void inject(FileCollection injects, final Map sourceMap, final Map resourceMap) throws IOException
    {
        FileVisitor visitor = new FileVisitor() {

            @Override
            public void visitDir(FileVisitDetails arg0)
            {
                // nope.
            }

            @Override
            public void visitFile(FileVisitDetails details)
            {
                ByteArrayOutputStream stream = new ByteArrayOutputStream();
                details.copyTo(stream);
                // BAOS dont need to be clsoed.

                byte[] array = stream.toByteArray();
                String path = details.getRelativePath().getPathString().replace('\\', '/');

                if (details.getName().endsWith(".java"))
                {
                    sourceMap.put(path, new String(array, Constants.CHARSET));
                }
                else
                {
                    resourceMap.put(path, array);
                }
            }

        };

        for (File inject : injects)
        {
            if (inject.isDirectory())
            {
                getProject().fileTree(inject).visit(visitor);
            }
            else if (inject.getName().endsWith(".jar") || inject.getName().endsWith(".zip"))
            {
                getProject().zipTree(inject).visit(visitor);
            }
            else if (inject.getName().endsWith(".java"))
            {
                sourceMap.put(inject.getName(), Files.toString(inject, Constants.CHARSET));
            }
            else
            {
                resourceMap.put(inject.getName(), Files.toByteArray(inject));
            }
        }
    }

    private void applyPatches() throws IOException, PatchException
    {
        boolean fuzzed = false;
        Throwable failure = null;

        for (PatchedFile patch : loadedPatches)
        {
            List errors = patch.patch.patch(false);
            for (ContextualPatch.PatchReport report : errors)
            {
                // catch failed patches
                if (!report.getStatus().isSuccess())
                {
                    StringBuilder rejectBuilder = new StringBuilder();

                    getLogger().log(LogLevel.ERROR, "Patching failed: {} {}", context.strip(report.getTarget()), report.getFailure().getMessage());
                    // now spit the hunks
                    int failed = 0;
                    for (ContextualPatch.HunkReport hunk : report.getHunks())
                    {
                        // catch the failed hunks
                        if (!hunk.getStatus().isSuccess())
                        {
                            failed++;
                            getLogger().error("  " + hunk.getHunkID() + ": " + (hunk.getFailure() != null ? hunk.getFailure().getMessage() : "") + " @ " + hunk.getIndex());

                            if (makeRejects)
                            {
                                rejectBuilder.append(String.format("++++ REJECTED PATCH %d\n", hunk.getHunkID()));
                                rejectBuilder.append(Joiner.on('\n').join(hunk.hunk.lines));
                                rejectBuilder.append(String.format("\n++++ END PATCH\n"));
                            }
                        }
                        else if (hunk.getStatus() == PatchStatus.Fuzzed)
                        {
                            getLogger().info("  " + hunk.getHunkID() + " fuzzed " + hunk.getFuzz() + "!");
                        }
                    }

                    getLogger().log(LogLevel.ERROR, "  {}/{} failed", failed, report.getHunks().size());

                    if (makeRejects)
                    {
                        File reject = patch.makeRejectFile();
                        if (reject.exists())
                        {
                            reject.delete();
                        }
                        Files.append(rejectBuilder.toString(),reject, Charsets.UTF_8);
                        getLogger().log(LogLevel.ERROR, "  Rejects written to {}", reject.getAbsolutePath());
                    }

                    if (failure == null)
                        failure = report.getFailure();
                }
                // catch fuzzed patches
                else if (report.getStatus() == ContextualPatch.PatchStatus.Fuzzed)
                {
                    getLogger().log(LogLevel.INFO, "Patching fuzzed: {}", context.strip(report.getTarget()));

                    // set the boolean for later use
                    fuzzed = true;

                    // now spit the hunks
                    for (ContextualPatch.HunkReport hunk : report.getHunks())
                    {
                        // catch the failed hunks
                        if (hunk.getStatus() == PatchStatus.Fuzzed)
                        {
                            getLogger().info("  {} fuzzed {}!", hunk.getHunkID(), hunk.getFuzz());
                        }
                    }

                    if (failure == null)
                        failure = report.getFailure();
                }

                // sucesful patches
                else
                {
                    getLogger().info("Patch succeeded: {}", context.strip(report.getTarget()));
                }
            }
        }

        if (failure != null && failOnError)
        {
            ThrowableUtil.propagate(failure);
        }

        if (fuzzed)
        {
            getLogger().lifecycle("Patches Fuzzed!");
        }
    }

    // START GETTERS/SETTERS HERE

    @Input
    public int getMaxFuzz()
    {
        return maxFuzz;
    }

    public void setMaxFuzz(int maxFuzz)
    {
        this.maxFuzz = maxFuzz;
    }

    @Input
    public int getPatchStrip()
    {
        return patchStrip;
    }

    public void setPatchStrip(int patchStrip)
    {
        this.patchStrip = patchStrip;
    }

    @Input
    public boolean isMakeRejects()
    {
        return makeRejects;
    }

    public void setMakeRejects(boolean makeRejects)
    {
        this.makeRejects = makeRejects;
    }

    @Input
    public boolean isFailOnError()
    {
        return failOnError;
    }

    public void setFailOnError(boolean failOnError)
    {
        this.failOnError = failOnError;
    }

    @Optional
    @InputDirectory
    private File getPatchesDir()
    {
        File patch = getPatches();
        if (patch.isDirectory())
            return getPatches();
        else
            return null;
    }

    @Optional
    @InputFile
    private File getPatchesZip()
    {
        File patch = getPatches();
        if (patch.isDirectory())
            return null;
        else
            return getPatches();
    }

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

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

    @InputFiles
    public FileCollection getInjects()
    {
        return getProject().files(injects);
    }

    public void setInjects(List injects)
    {
        this.injects = injects;
    }

    public void addInject(Object obj)
    {
        injects.add(obj);
    }

    // OVERRIDEN GARBAGE

    //@formatter:off
    @Override protected boolean storeJarInRam() { return true; }
    @Override public String asRead(String fileName, String file) { return file; }
    @Override public void doStuffAfter() { }
    //@formatter:on

    // START INNER CLASSES

    private static class ContextProvider implements ContextualPatch.IContextProvider
    {
        public Map fileMap;

        private final int          stripAmmount;

        public ContextProvider(Map fileMap, int stripAmmount)
        {
            this.fileMap = fileMap;
            this.stripAmmount = stripAmmount;
        }

        public String strip(String target)
        {
            target = target.replace('\\', '/');
            int index = 0;
            for (int x = 0; x < stripAmmount; x++)
            {
                index = target.indexOf('/', index) + 1;
            }
            return target.substring(index);
        }

        @Override
        public List getData(String target)
        {
            target = strip(target);

            if (fileMap.containsKey(target))
            {
                String[] lines = fileMap.get(target).split("\r\n|\r|\n");
                List ret = new ArrayList();
                for (String line : lines)
                {
                    ret.add(line);
                }
                return ret;
            }

            return null;
        }

        @Override
        public void setData(String target, List data)
        {
            target = strip(target);
            fileMap.put(target, Joiner.on(Constants.NEWLINE).join(data));
        }
    }

    private static class PatchedFile
    {
        public final File            fileToPatch;
        public final ContextualPatch patch;

        public PatchedFile(File file, ContextProvider provider, int maxFuzz) throws IOException
        {
            this.fileToPatch = file;
            this.patch = ContextualPatch.create(Files.toString(file, Charset.defaultCharset()), provider).setAccessC14N(true).setMaxFuzz(maxFuzz);
        }

        public PatchedFile(String file, ContextProvider provider, int maxFuzz)
        {
            this.fileToPatch = null;
            this.patch = ContextualPatch.create(file, provider).setAccessC14N(true).setMaxFuzz(maxFuzz);
        }

        public File makeRejectFile()
        {
            if (fileToPatch == null)
            {
                return null;
            }

            return new File(fileToPatch.getParentFile(), fileToPatch.getName() + ".rej");
        }
    }
}