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

com.threerings.getdown.tools.Differ Maven / Gradle / Ivy

The newest version!
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2018 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE

package com.threerings.getdown.tools;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

import com.threerings.getdown.data.Application;
import com.threerings.getdown.data.Digest;
import com.threerings.getdown.data.EnvConfig;
import com.threerings.getdown.data.Resource;
import com.threerings.getdown.util.FileUtil;
import com.threerings.getdown.util.StreamUtil;

/**
 * Generates patch files between two particular revisions of an
 * application. The differences between all the files in the two
 * revisions are bundled into a single patch file which is placed into the
 * target version directory.
 */
public class Differ
{
    /**
     * Creates a single patch file that contains the differences between
     * the two specified application directories. The patch file will be
     * created in the {@code nvdir} directory with name
     * {@code patchV.dat} where V is the old application version.
     */
    public void createDiff (File nvdir, File ovdir, boolean verbose)
        throws IOException
    {
        // sanity check
        String nvers = nvdir.getName();
        String overs = ovdir.getName();
        try {
            if (Long.parseLong(nvers) <= Long.parseLong(overs)) {
                String err = "New version (" + nvers + ") must be greater " +
                    "than old version (" + overs + ").";
                throw new IOException(err);
            }
        } catch (NumberFormatException nfe) {
            throw new IOException("Non-numeric versions? [nvers=" + nvers +
                                  ", overs=" + overs + "].");
        }

        Application oapp = new Application(new EnvConfig(ovdir));
        oapp.init(false);
        List orsrcs = new ArrayList<>();
        orsrcs.addAll(oapp.getCodeResources());
        orsrcs.addAll(oapp.getResources());

        Application napp = new Application(new EnvConfig(nvdir));
        napp.init(false);
        List nrsrcs = new ArrayList<>();
        nrsrcs.addAll(napp.getCodeResources());
        nrsrcs.addAll(napp.getResources());

        // first create a patch for the main application
        File patch = new File(nvdir, "patch" + overs + ".dat");
        createPatch(patch, orsrcs, nrsrcs, verbose);

        // next create patches for any auxiliary resource groups
        for (Application.AuxGroup ag : napp.getAuxGroups()) {
            orsrcs = new ArrayList<>();
            Application.AuxGroup oag = oapp.getAuxGroup(ag.name);
            if (oag != null) {
                orsrcs.addAll(oag.codes);
                orsrcs.addAll(oag.rsrcs);
            }
            nrsrcs = new ArrayList<>();
            nrsrcs.addAll(ag.codes);
            nrsrcs.addAll(ag.rsrcs);
            patch = new File(nvdir, "patch-" + ag.name + overs + ".dat");
            createPatch(patch, orsrcs, nrsrcs, verbose);
        }
    }

    protected void createPatch (File patch, List orsrcs,
                                List nrsrcs, boolean verbose)
        throws IOException
    {
        int version = Digest.VERSION;
        MessageDigest md = Digest.getMessageDigest(version);
        try (FileOutputStream fos = new FileOutputStream(patch);
             BufferedOutputStream buffered = new BufferedOutputStream(fos);
             ZipOutputStream jout = new ZipOutputStream(buffered)) {

            // for each file in the new application, it either already exists
            // in the old application, or it is new
            for (Resource rsrc : nrsrcs) {
                int oidx = orsrcs.indexOf(rsrc);
                Resource orsrc = (oidx == -1) ? null : orsrcs.remove(oidx);
                if (orsrc != null) {
                    // first see if they are the same
                    String odig = orsrc.computeDigest(version, md, null);
                    String ndig = rsrc.computeDigest(version, md, null);
                    if (odig.equals(ndig)) {
                        if (verbose) {
                            System.out.println("Unchanged: " + rsrc.getPath());
                        }
                        // by leaving it out, it will be left as is during the
                        // patching process
                        continue;
                    }

                    // otherwise potentially create a jar diff
                    if (rsrc.getPath().endsWith(".jar")) {
                        if (verbose) {
                            System.out.println("JarDiff: " + rsrc.getPath());
                        }
                        // here's a juicy one: JarDiff blindly pulls ZipEntry
                        // objects out of one jar file and stuffs them into
                        // another without clearing out things like the
                        // compressed size, so if, for whatever reason (like
                        // different JRE versions or phase of the moon) the
                        // compressed size in the old jar file is different
                        // than the compressed size generated when creating the
                        // jardiff jar file, ZipOutputStream will choke and
                        // we'll be hosed; so we recreate the jar files in
                        // their entirety before running jardiff on 'em
                        File otemp = rebuildJar(orsrc.getLocal());
                        File temp = rebuildJar(rsrc.getLocal());
                        jout.putNextEntry(new ZipEntry(rsrc.getPath() + Patcher.PATCH));
                        jarDiff(otemp, temp, jout);
                        FileUtil.deleteHarder(otemp);
                        FileUtil.deleteHarder(temp);
                        continue;
                    }
                }

                if (verbose) {
                    System.out.println("Addition: " + rsrc.getPath());
                }
                jout.putNextEntry(new ZipEntry(rsrc.getPath() + Patcher.CREATE));
                pipe(rsrc.getLocal(), jout);
            }

            // now any file remaining in orsrcs needs to be removed
            for (Resource rsrc : orsrcs) {
                // add an entry with the resource name and the deletion suffix
                if (verbose) {
                    System.out.println("Removal: " + rsrc.getPath());
                }
                jout.putNextEntry(new ZipEntry(rsrc.getPath() + Patcher.DELETE));
            }

            System.out.println("Created patch file: " + patch);

        } catch (IOException ioe) {
            FileUtil.deleteHarder(patch);
            throw ioe;
        }
    }

    protected File rebuildJar (File target)
        throws IOException
    {
        File temp = File.createTempFile("differ", "jar");
        try (ZipFile jar = new ZipFile(target);
             FileOutputStream tempFos = new FileOutputStream(temp);
             BufferedOutputStream tempBos = new BufferedOutputStream(tempFos);
             ZipOutputStream jout = new ZipOutputStream(tempBos)) {
            byte[] buffer = new byte[4096];
            for (Enumeration iter = jar.entries(); iter.hasMoreElements();) {
                ZipEntry entry = iter.nextElement();
                entry.setCompressedSize(-1);
                jout.putNextEntry(entry);
                try (InputStream in = jar.getInputStream(entry)) {
                    int size = in.read(buffer);
                    while (size != -1) {
                        jout.write(buffer, 0, size);
                        size = in.read(buffer);
                    }
                }
            }
        }
        return temp;
    }

    protected void jarDiff (File ofile, File nfile, ZipOutputStream jout) throws IOException
    {
        JarDiff.createPatch(ofile.getPath(), nfile.getPath(), jout, false);
    }

    public static void main (String[] args)
    {
        if (args.length < 2) {
            System.err.println(
                "Usage: Differ [-verbose] new_vers_dir old_vers_dir");
            System.exit(255);
        }
        Differ differ = new Differ();
        boolean verbose = false;
        int aidx = 0;
        if ("-verbose".equals(args[0])) {
            verbose = true;
            aidx++;
        }
        try {
            differ.createDiff(new File(args[aidx++]),
                              new File(args[aidx++]), verbose);
        } catch (IOException ioe) {
            System.err.println("Error: " + ioe.getMessage());
            System.exit(255);
        }
    }

    protected static void pipe (File file, OutputStream out) throws IOException
    {
        try (FileInputStream fin = new FileInputStream(file)) {
            StreamUtil.copy(fin, out);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy