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 
15 package com.google.archivepatcher.applier;
16 
17 import com.google.archivepatcher.shared.JreDeflateParameters;
18 import com.google.archivepatcher.shared.PatchConstants;
19 import com.google.archivepatcher.shared.TypedRange;
20 
21 import java.io.DataInputStream;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.util.ArrayList;
25 import java.util.Arrays;
26 import java.util.Collections;
27 import java.util.List;
28 
29 /**
30  * Reads patches.
31  */
32 public 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    */
readPatchApplyPlan(InputStream in)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    */
checkNonNegative(long value, String description)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    */
checkRange(long value, long min, long max, String description)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