blob: a5589c505d31b0ebc39a06c6f40d4710bd792a88 [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.explainer.EntryExplanation;
import com.google.archivepatcher.explainer.PatchExplainer;
import com.google.archivepatcher.explainer.PatchExplanation;
import com.google.archivepatcher.generator.DeltaFriendlyOldBlobSizeLimiter;
import com.google.archivepatcher.generator.RecommendationModifier;
import com.google.archivepatcher.generator.RecommendationReason;
import com.google.archivepatcher.generator.TotalRecompressionLimiter;
import com.google.archivepatcher.generator.bsdiff.BsDiffDeltaGenerator;
import com.google.archivepatcher.shared.DeflateCompressor;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.text.NumberFormat;
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 explaining patches.
*/
public class PatchExplainerTool extends AbstractTool {
/** Usage instructions for the command line. */
private static final String USAGE =
"java -cp <classpath> com.google.archivepatcher.tools.PatchExplainerTool <options>\n"
+ "\nOptions:\n"
+ " --old the old file\n"
+ " --new the new 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"
+ " --json output JSON results instead of plain text\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.\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 explain a patch from OLD to NEW, dumping plain human-readable text output:\n"
+ " java -cp <classpath> com.google.archivepatcher.tools.PatchExplainerTool \\\n"
+ " --old OLD --new NEW\n"
+ " To explain a patch from OLD to NEW, dumping JSON-formatted output:\n"
+ " java -cp <classpath> com.google.archivepatcher.tools.PatchExplainerTool \\\n"
+ " --old OLD --new NEW --json\n"
+ " To explain a patch from OLD to NEW, limiting to 1,000,000 recompress bytes:\n"
+ " java -cp <classpath> com.google.archivepatcher.tools.PatchExplainerTool \\\n"
+ " --old OLD --new NEW --trl 1000000\n";
/**
* Runs the tool. See usage instructions for more information.
*
* @param args command line arguments
* @throws IOException if anything goes wrong
* @throws InterruptedException if the thread is interrupted
*/
public static void main(String... args) throws IOException, InterruptedException {
new PatchExplainerTool().run(args);
}
/**
* Used for pretty-printing sizes and counts.
*/
private final NumberFormat format = NumberFormat.getNumberInstance();
/**
* Runs the tool. See usage instructions for more information.
*
* @param args command line arguments
* @throws IOException if anything goes wrong
* @throws InterruptedException if the thread is interrupted
*/
public void run(String... args) throws IOException, InterruptedException {
String oldPath = null;
String newPath = null;
Long totalRecompressionLimit = null;
Long deltaFriendlyOldBlobSizeLimit = null;
boolean outputJson = false;
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 ("--json".equals(arg)) {
outputJson = true;
} 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) {
exitWithUsage("missing required argument(s)");
}
File oldFile = getRequiredFileOrDie(oldPath, "old file");
File newFile = getRequiredFileOrDie(newPath, "new file");
DeflateCompressor compressor = new DeflateCompressor();
compressor.setCaching(true);
compressor.setCompressionLevel(9);
PatchExplainer explainer =
new PatchExplainer(new DeflateCompressor(), new BsDiffDeltaGenerator());
List<RecommendationModifier> recommendationModifiers = new ArrayList<RecommendationModifier>();
if (totalRecompressionLimit != null) {
recommendationModifiers.add(new TotalRecompressionLimiter(totalRecompressionLimit));
}
if (deltaFriendlyOldBlobSizeLimit != null) {
recommendationModifiers.add(
new DeltaFriendlyOldBlobSizeLimiter(deltaFriendlyOldBlobSizeLimit));
}
PatchExplanation patchExplanation =
new PatchExplanation(
explainer.explainPatch(
oldFile,
newFile,
recommendationModifiers.toArray(new RecommendationModifier[] {})));
if (outputJson) {
patchExplanation.writeJson(new PrintWriter(System.out));
} else {
dumpPlainText(patchExplanation);
}
}
private void dumpPlainText(PatchExplanation patchExplanation) {
dumpPlainText(patchExplanation.getExplainedAsNew());
dumpPlainText(patchExplanation.getExplainedAsChanged());
dumpPlainText(patchExplanation.getExplainedAsUnchangedOrFree());
System.out.println("----------");
System.out.println(
"Num unchanged files: " + patchExplanation.getExplainedAsUnchangedOrFree().size());
System.out.println(
"Num changed files: "
+ patchExplanation.getExplainedAsChanged().size()
+ " (estimated patch size "
+ format.format(patchExplanation.getEstimatedChangedSize())
+ " bytes)");
System.out.println(
"Num new files: "
+ patchExplanation.getExplainedAsNew().size()
+ " (estimated patch size "
+ format.format(patchExplanation.getEstimatedNewSize())
+ " bytes)");
System.out.println(
"Num files changed but forced to stay compressed by the total recompression limit: "
+ patchExplanation.getExplainedAsResourceConstrained().size()
+ " (estimated patch size "
+ format.format(patchExplanation.getEstimatedResourceConstrainedSize())
+ " bytes)");
long estimatedTotalSize =
patchExplanation.getEstimatedChangedSize()
+ patchExplanation.getEstimatedNewSize()
+ patchExplanation.getEstimatedResourceConstrainedSize();
System.out.println(
"Estimated total patch size: " + format.format(estimatedTotalSize) + " bytes");
}
private void dumpPlainText(List<EntryExplanation> explanations) {
for (EntryExplanation entryExplanation : explanations) {
String text = toPlainText(entryExplanation);
if (text != null) {
System.out.println(text);
}
}
}
/**
* Returns the path from an {@link EntryExplanation} as a UTF-8 string.
* @param explanation the {@link EntryExplanation} to extract the path from
* @return as described
*/
private static String path(EntryExplanation explanation) {
try {
return new String(explanation.getPath().getData(), "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("System doesn't support UTF-8", e);
}
}
private static String toPlainText(EntryExplanation explanation) {
String path = path(explanation);
if (explanation.isNew()) {
return "New file '"
+ path
+ "', approximate size of data in patch: "
+ explanation.getCompressedSizeInPatch()
+ " bytes";
}
if (explanation.getCompressedSizeInPatch() > 0) {
String metadata = "";
if (explanation.getReasonIncludedIfNotNew() == RecommendationReason.RESOURCE_CONSTRAINED) {
metadata = " (forced to stay compressed by a limit)";
}
return "Changed file '"
+ path
+ "'"
+ metadata
+ ", approximate size of data in patch: "
+ explanation.getCompressedSizeInPatch()
+ " bytes";
} else {
return "Unchanged or zero-delta-cost file '" + path + "'";
}
}
@Override
protected String getUsage() {
return USAGE;
}
}