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