updater.builder.SoftwarePatchBuilder.java Source code

Java tutorial

Introduction

Here is the source code for updater.builder.SoftwarePatchBuilder.java

Source

// Copyright (c) 2012 Chan Wai Shing
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package updater.builder;

import com.nothome.delta.Delta;
import com.nothome.delta.DiffWriter;
import com.nothome.delta.GDiffPatcher;
import com.nothome.delta.GDiffWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.math.BigInteger;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.HashMap;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.tukaani.xz.LZMA2Options;
import org.tukaani.xz.XZInputStream;
import org.tukaani.xz.XZOutputStream;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import updater.crypto.AESKey;
import updater.crypto.KeyGenerator;
import updater.crypto.RSAKey;
import updater.patch.PatchCreator;
import updater.patch.PatchExtractor;
import updater.patch.PatchPacker;
import updater.patch.Patcher;
import updater.patch.PatcherListener;
import updater.script.Client;
import updater.script.Patch;
import updater.util.CommonUtil;
import updater.util.XMLUtil;

/**
 * Tool that contain general functions to build the patch.
 * @author Chan Wai Shing <cws1989@gmail.com>
 */
public class SoftwarePatchBuilder {

    protected SoftwarePatchBuilder() {
    }

    public static void main(String[] args) {
        Options options = new Options();

        // utilities
        options.addOption(OptionBuilder.hasArg().withArgName("file")
                .withDescription("generate SHA-256 checksum of the file").create("sha256"));
        // cipher key
        options.addOption(OptionBuilder.hasArgs(2).withArgName("method length").withValueSeparator(' ')
                .withDescription("AES|RSA for 'method'; generate cipher key with specified key length in bits")
                .create("genkey"));
        options.addOption(OptionBuilder.hasArg().withArgName("file")
                .withDescription("renew the IV in the AES key file").create("renew"));
        // diff
        options.addOption(OptionBuilder.hasArgs(2).withArgName("old new").withValueSeparator(' ')
                .withDescription("generate a binary diff file of 'new' from 'old'").create("diff"));
        options.addOption(OptionBuilder.hasArgs(2).withArgName("file patch").withValueSeparator(' ')
                .withDescription("patch the 'file' with the 'patch'").create("diffpatch"));
        // compression
        options.addOption(OptionBuilder.hasArg().withArgName("file")
                .withDescription("compress the 'file' using XZ/LZMA2").create("compress"));
        options.addOption(OptionBuilder.hasArg().withArgName("file")
                .withDescription("decompress the 'file' using XZ/LZMA2").create("decompress"));

        // create & apply patch
        options.addOption(OptionBuilder.hasArgs(2).withArgName("folder patch").withValueSeparator(' ')
                .withDescription("apply the patch to the specified folder").create("do"));
        options.addOption(OptionBuilder.hasArg().withArgName("folder")
                .withDescription("create a full patch for upgrade from all version (unless specified)")
                .create("full"));
        options.addOption(OptionBuilder.hasArgs(2).withArgName("old new").withValueSeparator(' ').withDescription(
                "create a patch for upgrade from 'old' to 'new'; 'old' and 'new' are the directory of the two versions")
                .create("patch"));

        // patch packer, extractor
        options.addOption(OptionBuilder.hasArgs(2).withArgName("file folder").withValueSeparator(' ')
                .withDescription("extract the patch 'file' to the folder").create("extract"));
        options.addOption(OptionBuilder.hasArg().withArgName("folder").withDescription("pack the folder to a patch")
                .create("pack"));

        // catalog
        options.addOption(OptionBuilder.hasArgs(2).withArgName("mode file").withValueSeparator(' ')
                .withDescription("e|d for 'mode', e for encrypt, d for decrypt; 'file' is the catalog file")
                .create("catalog"));

        // script validation
        options.addOption(OptionBuilder.hasArg().withArgName("file").withDescription("validate a XML script file")
                .create("validate"));

        // subsidary options
        options.addOption(OptionBuilder.hasArg().withArgName("file").withDescription("specify output to which file")
                .withLongOpt("output").create("o"));
        options.addOption(OptionBuilder.hasArg().withArgName("file").withDescription("specify the key file to use")
                .withLongOpt("key").create("k"));
        options.addOption(OptionBuilder.hasArg().withArgName("version").withDescription("specify the version-from")
                .withLongOpt("from").create("f"));
        options.addOption(OptionBuilder.hasArg().withArgName("version")
                .withDescription("specify the version-from-subsequent").withLongOpt("from-sub").create("fs"));
        options.addOption(OptionBuilder.hasArg().withArgName("version").withDescription("specify the version-to")
                .withLongOpt("to").create("t"));

        options.addOption(new Option("h", "help", false, "print this message"));
        options.addOption(new Option("v", "version", false, "show the version of this software"));
        options.addOption(
                new Option("vb", "verbose", false, "turn on verbose mode, output details when encounter error"));

        CommandLineParser parser = new GnuParser();
        CommandLine line = null;
        try {
            line = parser.parse(options, args);
            if (line.hasOption("sha256")) {
                sha256(line, options);
            } else if (line.hasOption("genkey")) {
                genkey(line, options);
            } else if (line.hasOption("renew")) {
                renew(line, options);
            } else if (line.hasOption("diff")) {
                diff(line, options);
            } else if (line.hasOption("diffpatch")) {
                diffpatch(line, options);
            } else if (line.hasOption("compress")) {
                compress(line, options);
            } else if (line.hasOption("decompress")) {
                decompress(line, options);
            } else if (line.hasOption("do")) {
                doPatch(line, options);
            } else if (line.hasOption("full")) {
                full(line, options);
            } else if (line.hasOption("patch")) {
                patch(line, options);
            } else if (line.hasOption("extract")) {
                extract(line, options);
            } else if (line.hasOption("pack")) {
                pack(line, options);
            } else if (line.hasOption("catalog")) {
                catalog(line, options);
            } else if (line.hasOption("validate")) {
                validate(line, options);
            } else if (line.hasOption("version")) {
                version();
            } else if (line.hasOption("help")) {
                showHelp(options);
            } else {
                version();
                System.out.println();
                showHelp(options);
            }
        } catch (ParseException ex) {
            System.out.println(ex.getMessage());
            showHelp(options);
        } catch (Exception ex) {
            if (line.hasOption("verbose")) {
                ex.printStackTrace(System.out);
            } else {
                System.out.println(ex.getMessage());
            }
        }
    }

    public static void sha256(CommandLine line, Options options) throws ParseException, Exception {
        String sha256Arg = line.getOptionValue("sha256");
        String outputArg = line.getOptionValue("output");

        System.out.println("File: " + sha256Arg);
        if (outputArg != null) {
            System.out.println("Output file: " + outputArg);
        }
        System.out.println();

        String sha256 = Util.getSHA256String(new File(sha256Arg));
        if (outputArg != null) {
            Util.writeFile(new File(outputArg), sha256);
        }

        System.out.println("Checksum: " + sha256);
    }

    public static void genkey(CommandLine line, Options options) throws ParseException, Exception {
        if (!line.hasOption("output")) {
            throw new Exception("Please specify the path to output the key file using --output or -o");
        }

        String[] genkeyArgs = line.getOptionValues("genkey");
        String outputArg = line.getOptionValue("output");

        if (genkeyArgs.length != 2) {
            throw new ParseException("Wrong arguments for 'genkey', expecting 2 arguments");
        }
        genkeyArgs[0] = genkeyArgs[0].toLowerCase();
        if (!genkeyArgs[0].equals("aes") && !genkeyArgs[0].equals("rsa")) {
            throw new ParseException("Key generation only support AES and RSA.");
        }

        int keySize = 0;
        try {
            keySize = Integer.parseInt(genkeyArgs[1]);
            if (genkeyArgs[0].equals("rsa") && keySize < 512) {
                throw new Exception("Key length should at least 512 bits for RSA.");
            }
            if (keySize % 8 != 0) {
                throw new ParseException("Key length should be a multiple of 8.");
            }
        } catch (NumberFormatException ex) {
            throw new ParseException("Key length should be a valid integer, your input: " + genkeyArgs[1]);
        }

        System.out.println("Method: " + genkeyArgs[0]);
        System.out.println("Key size: " + keySize);
        System.out.println("Output path: " + outputArg);
        System.out.println();

        if (genkeyArgs[0].equals("aes")) {
            KeyGenerator.generateAES(keySize, new File(outputArg));
        } else {
            KeyGenerator.generateRSA(keySize, new File(outputArg));
        }

        System.out.println("Key generated and saved to " + outputArg);
    }

    public static void renew(CommandLine line, Options options) throws ParseException, Exception {
        String renewArg = line.getOptionValue("renew");

        System.out.println("Key file: " + renewArg);
        System.out.println();

        KeyGenerator.renewAESIV(new File(renewArg));

        System.out.println("AES IV renewal succeed.");
    }

    public static void diff(CommandLine line, Options options) throws ParseException, Exception {
        if (!line.hasOption("output")) {
            throw new Exception("Please specify the path to output the diff file using --output or -o");
        }

        String[] diffArgs = line.getOptionValues("diff");
        String outputArg = line.getOptionValue("output");

        if (diffArgs.length != 2) {
            throw new ParseException("Wrong arguments for 'diff', expecting 2 arguments");
        }

        System.out.println("Old file: " + diffArgs[0]);
        System.out.println("New file: " + diffArgs[1]);
        System.out.println("Diff file: " + outputArg);
        System.out.println();

        FileOutputStream fout = null;
        try {
            fout = new FileOutputStream(new File(outputArg));
            DiffWriter diffOut = new GDiffWriter(fout);
            Delta delta = new Delta();
            delta.compute(new File(diffArgs[0]), new File(diffArgs[1]), diffOut);
        } finally {
            Util.closeQuietly(fout);
        }

        System.out.println("Diff file generated.");
    }

    public static void diffpatch(CommandLine line, Options options) throws ParseException, Exception {
        if (!line.hasOption("output")) {
            throw new Exception("Please specify the path to output the patched file using --output or -o");
        }

        String[] diffpatchArgs = line.getOptionValues("diffpatch");
        String outputArg = line.getOptionValue("output");

        if (diffpatchArgs.length != 2) {
            throw new ParseException("Wrong arguments for 'diffpatch', expecting 2 arguments");
        }

        System.out.println("File to apply patch to: " + diffpatchArgs[0]);
        System.out.println("Patch file: " + diffpatchArgs[1]);
        System.out.println("Output file: " + outputArg);
        System.out.println();

        GDiffPatcher diffPatcher = new GDiffPatcher();
        diffPatcher.patch(new File(diffpatchArgs[0]), new File(diffpatchArgs[1]), new File(outputArg));

        System.out.println("Patching completed.");
    }

    public static void compress(CommandLine line, Options options) throws ParseException, Exception {
        // file patch
        if (!line.hasOption("output")) {
            throw new Exception("Please specify the path to output the compressed file using --output or -o");
        }

        String compressArg = line.getOptionValue("compress");
        String outputArg = line.getOptionValue("output");

        System.out.println("File to compress: " + compressArg);
        System.out.println("Output file: " + outputArg);
        System.out.println();

        FileInputStream fin = null;
        FileOutputStream fout = null;
        try {
            File inFile = new File(compressArg);
            long inFileLength = inFile.length();

            fin = new FileInputStream(inFile);
            fout = new FileOutputStream(new File(outputArg));
            XZOutputStream xzOut = new XZOutputStream(fout, new LZMA2Options());

            int byteRead, cumulateByteRead = 0;
            byte[] b = new byte[32768];
            while ((byteRead = fin.read(b)) != -1) {
                xzOut.write(b, 0, byteRead);

                cumulateByteRead += byteRead;
                if (cumulateByteRead >= inFileLength) {
                    break;
                }
            }

            if (cumulateByteRead != inFileLength) {
                throw new Exception("Error occurred when reading the input file.");
            }

            xzOut.finish();
        } finally {
            Util.closeQuietly(fin);
            Util.closeQuietly(fout);
        }

        System.out.println("Compression completed.");
    }

    public static void decompress(CommandLine line, Options options) throws ParseException, Exception {
        // file patch
        if (!line.hasOption("output")) {
            throw new Exception("Please specify the path to output the decompressed file using --output or -o");
        }

        String decompressArg = line.getOptionValue("decompress");
        String outputArg = line.getOptionValue("output");

        System.out.println("File to decompress: " + decompressArg);
        System.out.println("Output file: " + outputArg);
        System.out.println();

        FileInputStream fin = null;
        FileOutputStream fout = null;
        try {
            File inFile = new File(decompressArg);
            long inFileLength = inFile.length();

            fin = new FileInputStream(inFile);
            XZInputStream xzIn = new XZInputStream(fin);
            fout = new FileOutputStream(new File(outputArg));

            int byteRead, cumulateByteRead = 0;
            byte[] b = new byte[32768];
            while ((byteRead = xzIn.read(b)) != -1) {
                fout.write(b, 0, byteRead);

                cumulateByteRead += byteRead;
                if (cumulateByteRead >= inFileLength) {
                    break;
                }
            }

            if (cumulateByteRead != inFileLength) {
                throw new Exception("Error occurred when reading the input file.");
            }
        } finally {
            Util.closeQuietly(fin);
            Util.closeQuietly(fout);
        }

        System.out.println("Decompression completed.");
    }

    public static void doPatch(CommandLine line, Options options) throws ParseException, Exception {
        String[] doArgs = line.getOptionValues("do");

        if (doArgs.length != 2) {
            throw new ParseException("Wrong arguments for 'do', expecting 2 arguments");
        }

        System.out.println("Target folder: " + doArgs[0]);
        System.out.println("Patch file: " + doArgs[1]);
        System.out.println();

        File patchFile = new File(doArgs[1]);

        File tempDir = new File("tmp/" + System.currentTimeMillis());
        tempDir.mkdirs();

        AESKey aesKey = null;
        File decryptedPatchFile = null;
        if (line.hasOption("key")) {
            aesKey = AESKey.read(Util.readFile(new File(line.getOptionValue("key"))));
            if (aesKey.getKey().length != 32) {
                throw new Exception("Currently only support 256 bits AES key.");
            }

            decryptedPatchFile = new File(
                    tempDir.getAbsolutePath() + File.separator + patchFile.getName() + ".decrypted");
            decryptedPatchFile.delete();
            decryptedPatchFile.deleteOnExit();

            patchFile = decryptedPatchFile;
        }

        Patcher patcher = new Patcher(new File(tempDir.getAbsolutePath() + "/action.log"));
        patcher.doPatch(new PatcherListener() {

            @Override
            public void patchProgress(int percentage, String message) {
                System.out.println(percentage + "%, " + message);
            }

            @Override
            public void patchEnableCancel(boolean enable) {
            }
        }, patchFile, 0, aesKey, new File(doArgs[0]), tempDir, new HashMap<String, String>());

        System.out.println("Patch completed.");

        // preserve the log
        //        Util.truncateFolder(tempDir);
        //        tempDir.delete();

        System.out.println();
        System.out.println("Patch applied successfully.");
    }

    public static void full(CommandLine line, Options options) throws ParseException, Exception {
        if (!line.hasOption("output")) {
            throw new Exception("Please specify the path to output the patch using --output");
        }
        if (!line.hasOption("from") && !line.hasOption("from-sub")) {
            throw new Exception("Please specify the version number of the old version --from or --from-sub");
        }
        if (!line.hasOption("to")) {
            throw new Exception("Please specify the version number of the new version using --to");
        }

        String fullArg = line.getOptionValue("full");
        String outputArg = line.getOptionValue("output");
        String fromArg = line.getOptionValue("from");
        String fromSubsequentArg = line.getOptionValue("from-sub");
        String toArg = line.getOptionValue("to");

        System.out.println("Software version: " + toArg);
        System.out.println("Software directory: " + fullArg);
        if (fromArg != null) {
            System.out.println("For software with version == " + fromArg);
        }
        if (fromSubsequentArg != null) {
            System.out.println("For software with version >= " + fromSubsequentArg);
        }
        System.out.println("Path to save the generated patch: " + outputArg);
        System.out.println();

        File tempDir = new File("tmp/" + System.currentTimeMillis());
        tempDir.mkdirs();

        AESKey aesKey = null;
        if (line.hasOption("key")) {
            aesKey = AESKey.read(Util.readFile(new File(line.getOptionValue("key"))));
            if (aesKey.getKey().length != 32) {
                throw new Exception("Currently only support 256 bits AES key.");
            }
        }
        File patchFile = new File(outputArg);
        File encryptedPatchFile = new File(
                tempDir.getAbsolutePath() + File.separator + patchFile.getName() + ".encrypted");
        encryptedPatchFile.delete();
        encryptedPatchFile.deleteOnExit();

        PatchCreator.createFullPatch(new File(fullArg), new File(outputArg), -1, fromArg, fromSubsequentArg, toArg,
                aesKey, encryptedPatchFile);

        Util.truncateFolder(tempDir);
        tempDir.delete();

        System.out.println("Patch created.");
    }

    public static void patch(CommandLine line, Options options) throws ParseException, Exception {
        if (!line.hasOption("output")) {
            throw new Exception("Please specify the path to output the patch using --output");
        }
        if (!line.hasOption("from")) {
            throw new Exception("Please specify the version number of the old version using --from");
        }
        if (!line.hasOption("to")) {
            throw new Exception("Please specify the version number of the new version using --to");
        }

        String[] patchArgs = line.getOptionValues("patch");
        String outputArg = line.getOptionValue("output");
        String fromArg = line.getOptionValue("from");
        String toArg = line.getOptionValue("to");

        if (patchArgs.length != 2) {
            throw new ParseException("Wrong arguments for 'patch', expecting 2 arguments");
        }

        System.out.println("Old software version: " + fromArg);
        System.out.println("Old software directory: " + patchArgs[0]);
        System.out.println("New software version: " + toArg);
        System.out.println("New software directory: " + patchArgs[1]);
        System.out.println("Path to save the generated patch: " + outputArg);
        if (line.hasOption("key")) {
            System.out.println("AES key file: " + line.getOptionValue("key"));
        }
        System.out.println();

        File tempDir = new File("tmp/" + System.currentTimeMillis());
        tempDir.mkdirs();

        AESKey aesKey = null;
        if (line.hasOption("key")) {
            aesKey = AESKey.read(Util.readFile(new File(line.getOptionValue("key"))));
            if (aesKey.getKey().length != 32) {
                throw new Exception("Currently only support 256 bits AES key.");
            }
        }
        File patchFile = new File(outputArg);
        File encryptedPatchFile = new File(
                tempDir.getAbsolutePath() + File.separator + patchFile.getName() + ".encrypted");
        encryptedPatchFile.delete();
        encryptedPatchFile.deleteOnExit();

        PatchCreator.createPatch(new File(patchArgs[0]), new File(patchArgs[1]), tempDir, patchFile, -1, fromArg,
                toArg, aesKey, encryptedPatchFile);

        Util.truncateFolder(tempDir);
        tempDir.delete();

        System.out.println("Patch created.");
    }

    public static void extract(CommandLine line, Options options) throws ParseException, Exception {
        // file folder
        String[] extractArgs = line.getOptionValues("extract");

        if (extractArgs.length != 2) {
            throw new ParseException("Wrong arguments for 'extract', expecting 2 arguments");
        }

        System.out.println("Patch path: " + extractArgs[0]);
        System.out.println("Path to save the extracted files: " + extractArgs[1]);
        System.out.println();

        File tempDir = new File("tmp/" + System.currentTimeMillis());
        tempDir.mkdirs();

        AESKey aesKey = null;
        if (line.hasOption("key")) {
            aesKey = AESKey.read(Util.readFile(new File(line.getOptionValue("key"))));
            if (aesKey.getKey().length != 32) {
                throw new Exception("Currently only support 256 bits AES key.");
            }
        }
        File patchFile = new File(extractArgs[0]);
        File decryptedPatchFile = new File(
                tempDir.getAbsolutePath() + File.separator + patchFile.getName() + ".decrypted");
        decryptedPatchFile.delete();
        decryptedPatchFile.deleteOnExit();

        PatchExtractor.extract(patchFile, new File(extractArgs[1]), aesKey, decryptedPatchFile);

        Util.truncateFolder(tempDir);
        tempDir.delete();

        System.out.println("Extraction completed.");
    }

    public static void pack(CommandLine line, Options options) throws ParseException, Exception {
        if (!line.hasOption("output")) {
            throw new Exception("Please specify the path to output the patch using --output");
        }

        String packArg = line.getOptionValue("pack");
        String outputArg = line.getOptionValue("output");

        System.out.println("Folder to pack: " + packArg);
        System.out.println("Path to save the packed file: " + outputArg);
        System.out.println();

        AESKey aesKey = null;

        if (line.hasOption("key")) {
            aesKey = AESKey.read(Util.readFile(new File(line.getOptionValue("key"))));
            if (aesKey.getKey().length != 32) {
                throw new Exception("Currently only support 256 bits AES key.");
            }
        }
        File sourceFolder = new File(packArg);
        File encryptedPatchFile = new File("tmp/" + sourceFolder.getName() + ".enrypted");
        encryptedPatchFile.delete();
        encryptedPatchFile.deleteOnExit();

        PatchPacker.pack(sourceFolder, new File(outputArg), aesKey, encryptedPatchFile);

        System.out.println("Packing completed.");
    }

    public static void catalog(CommandLine line, Options options) throws ParseException, Exception {
        if (!line.hasOption("key")) {
            throw new Exception("Please specify the key file to use using --key");
        }
        if (!line.hasOption("output")) {
            throw new Exception("Please specify the path to output the XML file using --output");
        }

        String[] catalogArgs = line.getOptionValues("catalog");
        String keyArg = line.getOptionValue("key");
        String outputArg = line.getOptionValue("output");

        if (catalogArgs.length != 2) {
            throw new ParseException("Wrong arguments for 'catalog', expecting 2 arguments");
        }
        if (!catalogArgs[0].equals("e") && !catalogArgs[0].equals("d")) {
            throw new ParseException("Catalog mode should be either 'e' or 'd' but not " + catalogArgs[0]);
        }

        RSAKey rsaKey = RSAKey.read(Util.readFile(new File(keyArg)));

        System.out.println("Mode: " + (catalogArgs[0].equals("e") ? "encrypt" : "decrypt"));
        System.out.println("Catalog file: " + catalogArgs[1]);
        System.out.println("Key file: " + keyArg);
        System.out.println("Output file: " + outputArg);
        System.out.println();

        File in = new File(catalogArgs[1]);
        File out = new File(outputArg);
        BigInteger mod = new BigInteger(rsaKey.getModulus());

        if (catalogArgs[0].equals("e")) {
            BigInteger privateExp = new BigInteger(rsaKey.getPrivateExponent());

            RSAPrivateKey privateKey = CommonUtil.getPrivateKey(mod, privateExp);

            // compress
            ByteArrayOutputStream bout = new ByteArrayOutputStream();
            GZIPOutputStream gout = new GZIPOutputStream(bout);
            gout.write(Util.readFile(in));
            gout.finish();
            byte[] compressedData = bout.toByteArray();

            // encrypt
            int blockSize = mod.bitLength() / 8;
            byte[] encrypted = Util.rsaEncrypt(privateKey, blockSize, blockSize - 11, compressedData);

            // write to file
            Util.writeFile(out, encrypted);
        } else {
            BigInteger publicExp = new BigInteger(rsaKey.getPublicExponent());
            RSAPublicKey publicKey = CommonUtil.getPublicKey(mod, publicExp);

            // decrypt
            int blockSize = mod.bitLength() / 8;
            byte[] decrypted = Util.rsaDecrypt(publicKey, blockSize, Util.readFile(in));

            // decompress
            ByteArrayOutputStream bout = new ByteArrayOutputStream();
            ByteArrayInputStream bin = new ByteArrayInputStream(decrypted);
            GZIPInputStream gin = new GZIPInputStream(bin);

            int byteRead;
            byte[] b = new byte[1024];
            while ((byteRead = gin.read(b)) != -1) {
                bout.write(b, 0, byteRead);
            }
            byte[] decompressedData = bout.toByteArray();

            // write to file
            Util.writeFile(out, decompressedData);
        }

        System.out.println("Manipulation succeed.");
    }

    public static void validate(CommandLine line, Options options) throws ParseException, Exception {
        String validateArg = line.getOptionValue("validate");
        String outputArg = line.getOptionValue("output");

        System.out.println("Script file: " + validateArg);
        if (outputArg != null) {
            System.out.println("Output file: " + outputArg);
        }
        System.out.println();

        byte[] scriptContent = Util.readFile(new File(validateArg));
        Document doc = XMLUtil.readDocument(scriptContent);
        Element rootElement = doc.getDocumentElement();

        byte[] contentToOutput = null;
        String rootElementTag = rootElement.getTagName();
        if (rootElementTag.equals("patches")) {
            contentToOutput = updater.script.Catalog.read(scriptContent).output();
        } else if (rootElementTag.equals("patch")) {
            contentToOutput = Patch.read(scriptContent).output();
        } else if (rootElementTag.equals("root")) {
            contentToOutput = Client.read(scriptContent).output();
        } else {
            throw new Exception("Failed to recognize the script file.");
        }

        if (outputArg != null) {
            Util.writeFile(new File(outputArg), contentToOutput);
        }

        System.out.println("Validation finished.");
    }

    public static void version() {
        System.out.println("Software Updater - Patch Builder\r\nversion: 0.9.4 beta");
    }

    public static void showHelp(Options options) {
        HelpFormatter formatter = new HelpFormatter();
        formatter.printHelp("builder", options);
    }
}