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