/*
 * Paperclip - Paper Minecraft launcher
 *
 * Copyright (c) 2017 Kyle Wood (DemonWav)
 * https://github.com/PaperMC/Paper
 *
 * MIT License
 */

package com.destroystokyo.paperclip;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.jar.JarInputStream;
import org.apache.commons.compress.compressors.CompressorException;
import org.jbsdiff.InvalidHeaderException;
import org.jbsdiff.Patch;

class Paperclip {

    private final static File cache;
    private static MessageDigest digest;
    private final static File customPatchInfo = new File("paperclip.json");

    static File paperJar;

    static {
        try {
            cache = new File(new File(Paperclip.class.getProtectionDomain().getCodeSource().getLocation().toURI()).getParentFile(), "cache");
        } catch (final URISyntaxException e) {
            System.err.println("Error finding jar location to set cache directory");
            e.printStackTrace();
            System.exit(1);
            // Make compiler happy
            throw new RuntimeException();
        }
    }

    static void run(final String[] args) {
        try {
            digest = MessageDigest.getInstance("SHA-256");
        } catch (NoSuchAlgorithmException e) {
            System.err.println("Could not create hashing instance");
            e.printStackTrace();
            System.exit(1);
        }

        final PatchData patchInfo;
        try (final InputStream is = getConfig()) {
            patchInfo = PatchData.parse(is);
        } catch (final IllegalArgumentException e) {
            System.err.println("Invalid patch file");
            e.printStackTrace();
            System.exit(1);
            return;
        } catch (final IOException e) {
            System.err.println("Error reading patch file");
            e.printStackTrace();
            System.exit(1);
            return;
        }

        final File vanillaJar = new File(cache, "mojang_" + patchInfo.getVersion() + ".jar");
        paperJar = new File(cache, "patched_" + patchInfo.getVersion() + ".jar");

        final boolean vanillaValid;
        final boolean paperValid;
        try {
            vanillaValid = checkJar(vanillaJar, patchInfo.getOriginalHash());
            paperValid = checkJar(paperJar, patchInfo.getPatchedHash());
        } catch (final IOException e) {
            System.err.println("Error reading jar");
            e.printStackTrace();
            System.exit(1);
            return;
        }

        if (!paperValid) {
            if (!vanillaValid) {
                System.out.println("Downloading original jar...");
                //noinspection ResultOfMethodCallIgnored
                cache.mkdirs();
                //noinspection ResultOfMethodCallIgnored
                vanillaJar.delete();

                try (final InputStream stream = patchInfo.getOriginalUrl().openStream()) {
                    final ReadableByteChannel rbc = Channels.newChannel(stream);
                    try (final FileOutputStream fos = new FileOutputStream(vanillaJar)) {
                        fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
                    }
                } catch (final IOException e) {
                    System.err.println("Error downloading original jar");
                    e.printStackTrace();
                    System.exit(1);
                }

                // Only continue from here if the downloaded jar is correct
                try {
                    if (!checkJar(vanillaJar, patchInfo.getOriginalHash())) {
                        System.err.println("Invalid original jar, quitting.");
                        System.exit(1);
                    }
                } catch (final IOException e) {
                    System.err.println("Error reading jar");
                    e.printStackTrace();
                    System.exit(1);
                }
            }

            if (paperJar.exists()) {
                if (!paperJar.delete()) {
                    System.err.println("Error deleting invalid jar");
                    System.exit(1);
                }
            }

            System.out.println("Patching original jar...");
            final byte[] vanillaJarBytes;
            final byte[] patch;
            try {
                vanillaJarBytes = getBytes(vanillaJar);
                patch = Utils.readFully(patchInfo.getPatchFile().openStream());
            } catch (final IOException e) {
                System.err.println("Error patching original jar");
                e.printStackTrace();
                System.exit(1);
                return;
            }

            // Patch the jar to create the final jar to run
            try (final FileOutputStream jarOutput = new FileOutputStream(paperJar)) {
                Patch.patch(vanillaJarBytes, patch, jarOutput);
            } catch (final CompressorException | InvalidHeaderException | IOException e) {
                System.err.println("Error patching origin jar");
                e.printStackTrace();
                System.exit(1);
            }
        }

        // Exit if user has set `paperclip.patchonly` system property to `true`
        if (Boolean.getBoolean("paperclip.patchonly")) {
            System.exit(0);
        }

        // Get main class info from jar
        final String main;
        try (final FileInputStream fs = new FileInputStream(paperJar);
             final JarInputStream js = new JarInputStream(fs)) {
            main = js.getManifest().getMainAttributes().getValue("Main-Class");
        } catch (final IOException e) {
            System.err.println("Error reading from patched jar");
            e.printStackTrace();
            System.exit(1);
            return;
        }

        // Run the jar
        Utils.invoke(main, args);
    }

    private static InputStream getConfig() throws IOException {
        if (customPatchInfo.exists()) {
            return new FileInputStream(customPatchInfo);
        } else {
            return Paperclip.class.getResource("/patch.json").openStream();
        }
    }

    private static byte[] getBytes(final File file) throws IOException {
        final byte[] b = new byte[(int) file.length()];

        try (final FileInputStream fis = new FileInputStream(file)) {
            if (fis.read(b) != b.length) {
                System.err.println("Error reading all the data from " + file.getAbsolutePath());
                System.exit(1);
            }
        }

        return b;
    }

    private static boolean checkJar(final File jar, final byte[] hash) throws IOException {
        if (jar.exists()) {
            final byte[] jarBytes = getBytes(jar);
            return Arrays.equals(hash, digest.digest(jarBytes));
        }
        return false;
    }
}

@@ -775,18 +775,38 @@
 le cache
+ = new File(%22cache%22)
 ;%0A
-
     priv
@@ -911,24 +911,24 @@
 ip.json%22);%0A%0A
+
     static F
@@ -946,464 +946,8 @@
 r;%0A%0A
-    static %7B%0A        try %7B%0A            cache = new File(new File(Paperclip.class.getProtectionDomain().getCodeSource().getLocation().toURI()).getParentFile(), %22cache%22);%0A        %7D catch (final URISyntaxException e) %7B%0A            System.err.println(%22Error finding jar location to set cache directory%22);%0A            e.printStackTrace();%0A            System.exit(1);%0A            // Make compiler happy%0A            throw new RuntimeException();%0A        %7D%0A    %7D%0A%0A
     

Undo placing cached jar with Paperclip