All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.archivepatcher.tools.PatchExplainerTool Maven / Gradle / Ivy

The newest version!
// 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.DefaultDeflater;
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;
import java.util.zip.Deflater;

/**
 * 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  com.google.archivepatcher.tools.PatchExplainerTool \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  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  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  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 argIterator = new LinkedList(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(DefaultDeflater::new);
    compressor.setCaching(true);
    compressor.setCompressionLevel(9);
    PatchExplainer explainer =
        new PatchExplainer(new DeflateCompressor(DefaultDeflater::new), new BsDiffDeltaGenerator());
    List recommendationModifiers = new ArrayList();
    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 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;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy