| // 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; |
| } |
| } |