blob: 38b8400fd58b84e93a20a0699bc594c86dab9372 [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.applier;
import com.google.archivepatcher.shared.JreDeflateParameters;
import com.google.archivepatcher.shared.PatchConstants;
import com.google.archivepatcher.shared.TypedRange;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Reads patches.
*/
public class PatchReader {
/**
* Reads patch data from the specified {@link InputStream} up to but not including the first byte
* of delta bytes, and returns a {@link PatchApplyPlan} that describes all the operations that
* need to be performed in order to apply the patch. When this method returns, the stream is
* positioned so that the next read will be the first byte of delta bytes corresponding to the
* first {@link DeltaDescriptor} in the returned plan.
* @param in the stream to read from
* @return the plan for applying the patch
* @throws IOException if anything goes wrong
*/
public PatchApplyPlan readPatchApplyPlan(InputStream in) throws IOException {
// Use DataOutputStream for ease of writing. This is deliberately left open, as closing it would
// close the output stream that was passed in and that is not part of the method's documented
// behavior.
@SuppressWarnings("resource")
DataInputStream dataIn = new DataInputStream(in);
// Read header and flags.
byte[] expectedIdentifier = PatchConstants.IDENTIFIER.getBytes("US-ASCII");
byte[] actualIdentifier = new byte[expectedIdentifier.length];
dataIn.readFully(actualIdentifier);
if (!Arrays.equals(expectedIdentifier, actualIdentifier)) {
throw new PatchFormatException("Bad identifier");
}
dataIn.skip(4); // Flags (ignored in v1)
long deltaFriendlyOldFileSize = checkNonNegative(
dataIn.readLong(), "delta-friendly old file size");
// Read old file uncompression instructions.
int numOldFileUncompressionInstructions = (int) checkNonNegative(
dataIn.readInt(), "old file uncompression instruction count");
List<TypedRange<Void>> oldFileUncompressionPlan =
new ArrayList<TypedRange<Void>>(numOldFileUncompressionInstructions);
long lastReadOffset = -1;
for (int x = 0; x < numOldFileUncompressionInstructions; x++) {
long offset = checkNonNegative(dataIn.readLong(), "old file uncompression range offset");
long length = checkNonNegative(dataIn.readLong(), "old file uncompression range length");
if (offset < lastReadOffset) {
throw new PatchFormatException("old file uncompression ranges out of order or overlapping");
}
TypedRange<Void> range = new TypedRange<Void>(offset, length, null);
oldFileUncompressionPlan.add(range);
lastReadOffset = offset + length; // To check that the next range starts after the current one
}
// Read new file recompression instructions
int numDeltaFriendlyNewFileRecompressionInstructions = dataIn.readInt();
checkNonNegative(
numDeltaFriendlyNewFileRecompressionInstructions,
"delta-friendly new file recompression instruction count");
List<TypedRange<JreDeflateParameters>> deltaFriendlyNewFileRecompressionPlan =
new ArrayList<TypedRange<JreDeflateParameters>>(
numDeltaFriendlyNewFileRecompressionInstructions);
lastReadOffset = -1;
for (int x = 0; x < numDeltaFriendlyNewFileRecompressionInstructions; x++) {
long offset = checkNonNegative(
dataIn.readLong(), "delta-friendly new file recompression range offset");
long length = checkNonNegative(
dataIn.readLong(), "delta-friendly new file recompression range length");
if (offset < lastReadOffset) {
throw new PatchFormatException(
"delta-friendly new file recompression ranges out of order or overlapping");
}
lastReadOffset = offset + length; // To check that the next range starts after the current one
// Read the JreDeflateParameters
// Note that v1 only supports the default deflate compatibility window.
checkRange(
dataIn.readByte(),
PatchConstants.CompatibilityWindowId.DEFAULT_DEFLATE.patchValue,
PatchConstants.CompatibilityWindowId.DEFAULT_DEFLATE.patchValue,
"compatibility window id");
int level = (int) checkRange(dataIn.readUnsignedByte(), 1, 9, "recompression level");
int strategy = (int) checkRange(dataIn.readUnsignedByte(), 0, 2, "recompression strategy");
int nowrapInt = (int) checkRange(dataIn.readUnsignedByte(), 0, 1, "recompression nowrap");
TypedRange<JreDeflateParameters> range =
new TypedRange<JreDeflateParameters>(
offset,
length,
JreDeflateParameters.of(level, strategy, nowrapInt == 0 ? false : true));
deltaFriendlyNewFileRecompressionPlan.add(range);
}
// Read the delta metadata, but stop before the first byte of the actual delta.
// V1 has exactly one delta and it must be bsdiff.
int numDeltaRecords = (int) checkRange(dataIn.readInt(), 1, 1, "num delta records");
List<DeltaDescriptor> deltaDescriptors = new ArrayList<DeltaDescriptor>(numDeltaRecords);
for (int x = 0; x < numDeltaRecords; x++) {
byte deltaFormatByte = (byte)
checkRange(
dataIn.readByte(),
PatchConstants.DeltaFormat.BSDIFF.patchValue,
PatchConstants.DeltaFormat.BSDIFF.patchValue,
"delta format");
long deltaFriendlyOldFileWorkRangeOffset = checkNonNegative(
dataIn.readLong(), "delta-friendly old file work range offset");
long deltaFriendlyOldFileWorkRangeLength = checkNonNegative(
dataIn.readLong(), "delta-friendly old file work range length");
long deltaFriendlyNewFileWorkRangeOffset = checkNonNegative(
dataIn.readLong(), "delta-friendly new file work range offset");
long deltaFriendlyNewFileWorkRangeLength = checkNonNegative(
dataIn.readLong(), "delta-friendly new file work range length");
long deltaLength = checkNonNegative(dataIn.readLong(), "delta length");
DeltaDescriptor descriptor =
new DeltaDescriptor(
PatchConstants.DeltaFormat.fromPatchValue(deltaFormatByte),
new TypedRange<Void>(
deltaFriendlyOldFileWorkRangeOffset, deltaFriendlyOldFileWorkRangeLength, null),
new TypedRange<Void>(
deltaFriendlyNewFileWorkRangeOffset, deltaFriendlyNewFileWorkRangeLength, null),
deltaLength);
deltaDescriptors.add(descriptor);
}
return new PatchApplyPlan(
Collections.unmodifiableList(oldFileUncompressionPlan),
deltaFriendlyOldFileSize,
Collections.unmodifiableList(deltaFriendlyNewFileRecompressionPlan),
Collections.unmodifiableList(deltaDescriptors));
}
/**
* Assert that the value isn't negative.
* @param value the value to check
* @param description the description to use in error messages if the value is not ok
* @return the value
* @throws PatchFormatException if the value is not ok
*/
private static final long checkNonNegative(long value, String description)
throws PatchFormatException {
if (value < 0) {
throw new PatchFormatException("Bad value for " + description + ": " + value);
}
return value;
}
/**
* Assert that the value is in the specified range.
* @param value the value to check
* @param min the minimum (inclusive) value to allow
* @param max the maximum (inclusive) value to allow
* @param description the description to use in error messages if the value is not ok
* @return the value
* @throws PatchFormatException if the value is not ok
*/
private static final long checkRange(long value, long min, long max, String description)
throws PatchFormatException {
if (value < min || value > max) {
throw new PatchFormatException(
"Bad value for "
+ description
+ ": "
+ value
+ " (valid range: ["
+ min
+ ","
+ max
+ "]");
}
return value;
}
}