1// Copyright 2016 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package com.google.archivepatcher.applier;
16
17import com.google.archivepatcher.shared.JreDeflateParameters;
18import com.google.archivepatcher.shared.PatchConstants;
19import com.google.archivepatcher.shared.TypedRange;
20
21import java.io.DataInputStream;
22import java.io.IOException;
23import java.io.InputStream;
24import java.util.ArrayList;
25import java.util.Arrays;
26import java.util.Collections;
27import java.util.List;
28
29/**
30 * Reads patches.
31 */
32public class PatchReader {
33
34  /**
35   * Reads patch data from the specified {@link InputStream} up to but not including the first byte
36   * of delta bytes, and returns a {@link PatchApplyPlan} that describes all the operations that
37   * need to be performed in order to apply the patch. When this method returns, the stream is
38   * positioned so that the next read will be the first byte of delta bytes corresponding to the
39   * first {@link DeltaDescriptor} in the returned plan.
40   * @param in the stream to read from
41   * @return the plan for applying the patch
42   * @throws IOException if anything goes wrong
43   */
44  public PatchApplyPlan readPatchApplyPlan(InputStream in) throws IOException {
45    // Use DataOutputStream for ease of writing. This is deliberately left open, as closing it would
46    // close the output stream that was passed in and that is not part of the method's documented
47    // behavior.
48    @SuppressWarnings("resource")
49    DataInputStream dataIn = new DataInputStream(in);
50
51    // Read header and flags.
52    byte[] expectedIdentifier = PatchConstants.IDENTIFIER.getBytes("US-ASCII");
53    byte[] actualIdentifier = new byte[expectedIdentifier.length];
54    dataIn.readFully(actualIdentifier);
55    if (!Arrays.equals(expectedIdentifier, actualIdentifier)) {
56      throw new PatchFormatException("Bad identifier");
57    }
58    dataIn.skip(4); // Flags (ignored in v1)
59    long deltaFriendlyOldFileSize = checkNonNegative(
60        dataIn.readLong(), "delta-friendly old file size");
61
62    // Read old file uncompression instructions.
63    int numOldFileUncompressionInstructions = (int) checkNonNegative(
64        dataIn.readInt(), "old file uncompression instruction count");
65    List<TypedRange<Void>> oldFileUncompressionPlan =
66        new ArrayList<TypedRange<Void>>(numOldFileUncompressionInstructions);
67    long lastReadOffset = -1;
68    for (int x = 0; x < numOldFileUncompressionInstructions; x++) {
69      long offset = checkNonNegative(dataIn.readLong(), "old file uncompression range offset");
70      long length = checkNonNegative(dataIn.readLong(), "old file uncompression range length");
71      if (offset < lastReadOffset) {
72        throw new PatchFormatException("old file uncompression ranges out of order or overlapping");
73      }
74      TypedRange<Void> range = new TypedRange<Void>(offset, length, null);
75      oldFileUncompressionPlan.add(range);
76      lastReadOffset = offset + length; // To check that the next range starts after the current one
77    }
78
79    // Read new file recompression instructions
80    int numDeltaFriendlyNewFileRecompressionInstructions = dataIn.readInt();
81    checkNonNegative(
82        numDeltaFriendlyNewFileRecompressionInstructions,
83        "delta-friendly new file recompression instruction count");
84    List<TypedRange<JreDeflateParameters>> deltaFriendlyNewFileRecompressionPlan =
85        new ArrayList<TypedRange<JreDeflateParameters>>(
86            numDeltaFriendlyNewFileRecompressionInstructions);
87    lastReadOffset = -1;
88    for (int x = 0; x < numDeltaFriendlyNewFileRecompressionInstructions; x++) {
89      long offset = checkNonNegative(
90          dataIn.readLong(), "delta-friendly new file recompression range offset");
91      long length = checkNonNegative(
92          dataIn.readLong(), "delta-friendly new file recompression range length");
93      if (offset < lastReadOffset) {
94        throw new PatchFormatException(
95            "delta-friendly new file recompression ranges out of order or overlapping");
96      }
97      lastReadOffset = offset + length; // To check that the next range starts after the current one
98
99      // Read the JreDeflateParameters
100      // Note that v1 only supports the default deflate compatibility window.
101      checkRange(
102          dataIn.readByte(),
103          PatchConstants.CompatibilityWindowId.DEFAULT_DEFLATE.patchValue,
104          PatchConstants.CompatibilityWindowId.DEFAULT_DEFLATE.patchValue,
105          "compatibility window id");
106      int level = (int) checkRange(dataIn.readUnsignedByte(), 1, 9, "recompression level");
107      int strategy = (int) checkRange(dataIn.readUnsignedByte(), 0, 2, "recompression strategy");
108      int nowrapInt = (int) checkRange(dataIn.readUnsignedByte(), 0, 1, "recompression nowrap");
109      TypedRange<JreDeflateParameters> range =
110          new TypedRange<JreDeflateParameters>(
111              offset,
112              length,
113              JreDeflateParameters.of(level, strategy, nowrapInt == 0 ? false : true));
114      deltaFriendlyNewFileRecompressionPlan.add(range);
115    }
116
117    // Read the delta metadata, but stop before the first byte of the actual delta.
118    // V1 has exactly one delta and it must be bsdiff.
119    int numDeltaRecords = (int) checkRange(dataIn.readInt(), 1, 1, "num delta records");
120
121    List<DeltaDescriptor> deltaDescriptors = new ArrayList<DeltaDescriptor>(numDeltaRecords);
122    for (int x = 0; x < numDeltaRecords; x++) {
123      byte deltaFormatByte = (byte)
124      checkRange(
125          dataIn.readByte(),
126          PatchConstants.DeltaFormat.BSDIFF.patchValue,
127          PatchConstants.DeltaFormat.BSDIFF.patchValue,
128          "delta format");
129      long deltaFriendlyOldFileWorkRangeOffset = checkNonNegative(
130          dataIn.readLong(), "delta-friendly old file work range offset");
131      long deltaFriendlyOldFileWorkRangeLength = checkNonNegative(
132          dataIn.readLong(), "delta-friendly old file work range length");
133      long deltaFriendlyNewFileWorkRangeOffset = checkNonNegative(
134          dataIn.readLong(), "delta-friendly new file work range offset");
135      long deltaFriendlyNewFileWorkRangeLength = checkNonNegative(
136          dataIn.readLong(), "delta-friendly new file work range length");
137      long deltaLength = checkNonNegative(dataIn.readLong(), "delta length");
138      DeltaDescriptor descriptor =
139          new DeltaDescriptor(
140              PatchConstants.DeltaFormat.fromPatchValue(deltaFormatByte),
141              new TypedRange<Void>(
142                  deltaFriendlyOldFileWorkRangeOffset, deltaFriendlyOldFileWorkRangeLength, null),
143              new TypedRange<Void>(
144                  deltaFriendlyNewFileWorkRangeOffset, deltaFriendlyNewFileWorkRangeLength, null),
145              deltaLength);
146      deltaDescriptors.add(descriptor);
147    }
148
149    return new PatchApplyPlan(
150        Collections.unmodifiableList(oldFileUncompressionPlan),
151        deltaFriendlyOldFileSize,
152        Collections.unmodifiableList(deltaFriendlyNewFileRecompressionPlan),
153        Collections.unmodifiableList(deltaDescriptors));
154  }
155
156  /**
157   * Assert that the value isn't negative.
158   * @param value the value to check
159   * @param description the description to use in error messages if the value is not ok
160   * @return the value
161   * @throws PatchFormatException if the value is not ok
162   */
163  private static final long checkNonNegative(long value, String description)
164      throws PatchFormatException {
165    if (value < 0) {
166      throw new PatchFormatException("Bad value for " + description + ": " + value);
167    }
168    return value;
169  }
170
171  /**
172   * Assert that the value is in the specified range.
173   * @param value the value to check
174   * @param min the minimum (inclusive) value to allow
175   * @param max the maximum (inclusive) value to allow
176   * @param description the description to use in error messages if the value is not ok
177   * @return the value
178   * @throws PatchFormatException if the value is not ok
179   */
180  private static final long checkRange(long value, long min, long max, String description)
181      throws PatchFormatException {
182    if (value < min || value > max) {
183      throw new PatchFormatException(
184          "Bad value for "
185              + description
186              + ": "
187              + value
188              + " (valid range: ["
189              + min
190              + ","
191              + max
192              + "]");
193    }
194    return value;
195  }
196}
197