blob: 5c12c2e9b97b00ec706b0e6df88ebc9ac1946254 [file] [log] [blame]
// 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;
}
}