aboutsummaryrefslogtreecommitdiff
path: root/tools/src/main/java/com/google/archivepatcher/tools/FileByFileTool.java
diff options
context:
space:
mode:
Diffstat (limited to 'tools/src/main/java/com/google/archivepatcher/tools/FileByFileTool.java')
-rw-r--r--tools/src/main/java/com/google/archivepatcher/tools/FileByFileTool.java232
1 files changed, 232 insertions, 0 deletions
diff --git a/tools/src/main/java/com/google/archivepatcher/tools/FileByFileTool.java b/tools/src/main/java/com/google/archivepatcher/tools/FileByFileTool.java
new file mode 100644
index 0000000..5c12c2e
--- /dev/null
+++ b/tools/src/main/java/com/google/archivepatcher/tools/FileByFileTool.java
@@ -0,0 +1,232 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.archivepatcher.tools;
+
+import com.google.archivepatcher.applier.FileByFileV1DeltaApplier;
+import com.google.archivepatcher.generator.DeltaFriendlyOldBlobSizeLimiter;
+import com.google.archivepatcher.generator.FileByFileV1DeltaGenerator;
+import com.google.archivepatcher.generator.RecommendationModifier;
+import com.google.archivepatcher.generator.TotalRecompressionLimiter;
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Simple command-line tool for generating and applying patches.
+ */
+public class FileByFileTool extends AbstractTool {
+
+ /** Usage instructions for the command line. */
+ private static final String USAGE =
+ "java -cp <classpath> com.google.archivepatcher.tools.FileByFileTool <options>\n"
+ + "\nOptions:\n"
+ + " --generate generate a patch\n"
+ + " --apply apply a patch\n"
+ + " --old the old file\n"
+ + " --new the new file\n"
+ + " --patch the patch file\n"
+ + " --trl optionally, the total bytes of recompression to allow (see below)\n"
+ + " --dfobsl optionally, a limit on the total size of the delta-friendly old blob (see below)\n"
+ + "\nTotal Recompression Limit (trl):\n"
+ + " When generating a patch, a limit can be specified on the total number of bytes to\n"
+ + " allow to be recompressed during the patch apply process. This can be for a variety\n"
+ + " of reasons, with the most obvious being to limit the amount of effort that has to\n"
+ + " be expended applying the patch on the target platform. To properly explain a\n"
+ + " patch that had such a limitation, it is necessary to specify the same limitation\n"
+ + " here. This argument is illegal for --apply, since it only applies to --generate.\n"
+ + "\nDelta Friendly Old Blob Size Limit (dfobsl):\n"
+ + " When generating a patch, a limit can be specified on the total size of the delta-\n"
+ + " friendly old blob. This implicitly limits the size of the temporary file that\n"
+ + " needs to be created when applying the patch. The size limit is \"soft\" in that \n"
+ + " the delta-friendly old blob needs to at least contain the original data that was\n"
+ + " within it; but the limit specified here will constrain any attempt to uncompress\n"
+ + " the content. If the limit is less than or equal to the size of the old file, no\n"
+ + " uncompression will be performed at all. Otherwise, the old file can expand into\n"
+ + " delta-friendly old blob until the size reaches this limit.\n"
+ + "\nExamples:\n"
+ + " To generate a patch from OLD to NEW, saving the patch in PATCH:\n"
+ + " java -cp <classpath> com.google.archivepatcher.tools.FileByFileTool --generate \\\n"
+ + " --old OLD --new NEW --patch PATCH\n"
+ + " To generate a patch from OLD to NEW, limiting to 1,000,000 recompress bytes:\n"
+ + " java -cp <classpath> com.google.archivepatcher.tools.FileByFileTool --generate \\\n"
+ + " --old OLD --new NEW --trl 1000000 --patch PATCH\n"
+ + " To apply a patch PATCH to OLD, saving the result in NEW:\n"
+ + " java -cp <classpath> com.google.archivepatcher.tools.FileByFileTool --apply \\\n"
+ + " --old OLD --patch PATCH --new NEW";
+
+ /**
+ * Modes of operation.
+ */
+ private static enum Mode {
+ /**
+ * Generate a patch.
+ */
+ GENERATE,
+
+ /**
+ * Apply a patch.
+ */
+ APPLY;
+ }
+
+ /**
+ * Runs the tool. See usage instructions for more information.
+ *
+ * @param args command line arguments
+ * @throws IOException if anything goes wrong
+ * @throws InterruptedException if any thread has interrupted the current thread
+ */
+ public static void main(String... args) throws IOException, InterruptedException {
+ new FileByFileTool().run(args);
+ }
+
+ /**
+ * Run the tool.
+ *
+ * @param args command line arguments
+ * @throws IOException if anything goes wrong
+ * @throws InterruptedException if any thread has interrupted the current thread
+ */
+ public void run(String... args) throws IOException, InterruptedException {
+ String oldPath = null;
+ String newPath = null;
+ String patchPath = null;
+ Long totalRecompressionLimit = null;
+ Long deltaFriendlyOldBlobSizeLimit = null;
+ Mode mode = null;
+ Iterator<String> argIterator = new LinkedList<String>(Arrays.asList(args)).iterator();
+ while (argIterator.hasNext()) {
+ String arg = argIterator.next();
+ if ("--old".equals(arg)) {
+ oldPath = popOrDie(argIterator, "--old");
+ } else if ("--new".equals(arg)) {
+ newPath = popOrDie(argIterator, "--new");
+ } else if ("--patch".equals(arg)) {
+ patchPath = popOrDie(argIterator, "--patch");
+ } else if ("--generate".equals(arg)) {
+ mode = Mode.GENERATE;
+ } else if ("--apply".equals(arg)) {
+ mode = Mode.APPLY;
+ } else if ("--trl".equals(arg)) {
+ totalRecompressionLimit = Long.parseLong(popOrDie(argIterator, "--trl"));
+ if (totalRecompressionLimit < 0) {
+ exitWithUsage("--trl cannot be negative: " + totalRecompressionLimit);
+ }
+ } else if ("--dfobsl".equals(arg)) {
+ deltaFriendlyOldBlobSizeLimit = Long.parseLong(popOrDie(argIterator, "--dfobsl"));
+ if (deltaFriendlyOldBlobSizeLimit < 0) {
+ exitWithUsage("--dfobsl cannot be negative: " + deltaFriendlyOldBlobSizeLimit);
+ }
+ } else {
+ exitWithUsage("unknown argument: " + arg);
+ }
+ }
+ if (oldPath == null || newPath == null || patchPath == null || mode == null) {
+ exitWithUsage("missing required argument(s)");
+ }
+ if (mode == Mode.APPLY && totalRecompressionLimit != null) {
+ exitWithUsage("--trl can only be used with --generate");
+ }
+ if (mode == Mode.APPLY && deltaFriendlyOldBlobSizeLimit != null) {
+ exitWithUsage("--dfobsl can only be used with --generate");
+ }
+ File oldFile = getRequiredFileOrDie(oldPath, "old file");
+ if (mode == Mode.GENERATE) {
+ File newFile = getRequiredFileOrDie(newPath, "new file");
+ generatePatch(
+ oldFile,
+ newFile,
+ new File(patchPath),
+ totalRecompressionLimit,
+ deltaFriendlyOldBlobSizeLimit);
+ } else { // mode == Mode.APPLY
+ File patchFile = getRequiredFileOrDie(patchPath, "patch file");
+ applyPatch(oldFile, patchFile, new File(newPath));
+ }
+ }
+
+ /**
+ * Generate a specified patch to transform the specified old file to the specified new file.
+ *
+ * @param oldFile the old file (will be read)
+ * @param newFile the new file (will be read)
+ * @param patchFile the patch file (will be written)
+ * @param totalRecompressionLimit optional limit for total number of bytes of recompression to
+ * allow in the resulting patch
+ * @param deltaFriendlyOldBlobSizeLimit optional limit for the size of the delta-friendly old
+ * blob, which implies a limit on the temporary space needed to apply the generated patch
+ * @throws IOException if anything goes wrong
+ * @throws InterruptedException if any thread has interrupted the current thread
+ */
+ public static void generatePatch(
+ File oldFile,
+ File newFile,
+ File patchFile,
+ Long totalRecompressionLimit,
+ Long deltaFriendlyOldBlobSizeLimit)
+ throws IOException, InterruptedException {
+ List<RecommendationModifier> recommendationModifiers = new ArrayList<RecommendationModifier>();
+ if (totalRecompressionLimit != null) {
+ recommendationModifiers.add(new TotalRecompressionLimiter(totalRecompressionLimit));
+ }
+ if (deltaFriendlyOldBlobSizeLimit != null) {
+ recommendationModifiers.add(
+ new DeltaFriendlyOldBlobSizeLimiter(deltaFriendlyOldBlobSizeLimit));
+ }
+ FileByFileV1DeltaGenerator generator =
+ new FileByFileV1DeltaGenerator(
+ recommendationModifiers.toArray(new RecommendationModifier[] {}));
+ try (FileOutputStream patchOut = new FileOutputStream(patchFile);
+ BufferedOutputStream bufferedPatchOut = new BufferedOutputStream(patchOut)) {
+ generator.generateDelta(oldFile, newFile, bufferedPatchOut);
+ bufferedPatchOut.flush();
+ }
+ }
+
+ /**
+ * Apply a specified patch to the specified old file, creating the specified new file.
+ * @param oldFile the old file (will be read)
+ * @param patchFile the patch file (will be read)
+ * @param newFile the new file (will be written)
+ * @throws IOException if anything goes wrong
+ */
+ public static void applyPatch(File oldFile, File patchFile, File newFile) throws IOException {
+ // Figure out temp directory
+ File tempFile = File.createTempFile("fbftool", "tmp");
+ File tempDir = tempFile.getParentFile();
+ tempFile.delete();
+ FileByFileV1DeltaApplier applier = new FileByFileV1DeltaApplier(tempDir);
+ try (FileInputStream patchIn = new FileInputStream(patchFile);
+ BufferedInputStream bufferedPatchIn = new BufferedInputStream(patchIn);
+ FileOutputStream newOut = new FileOutputStream(newFile);
+ BufferedOutputStream bufferedNewOut = new BufferedOutputStream(newOut)) {
+ applier.applyDelta(oldFile, bufferedPatchIn, bufferedNewOut);
+ bufferedNewOut.flush();
+ }
+ }
+
+ @Override
+ protected String getUsage() {
+ return USAGE;
+ }
+}