1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.apksig.internal.zip; 18 19 import com.android.apksig.apk.ApkFormatException; 20 import com.android.apksig.internal.util.Pair; 21 import com.android.apksig.util.DataSource; 22 import com.android.apksig.zip.ZipFormatException; 23 import com.android.apksig.zip.ZipSections; 24 25 import java.io.ByteArrayOutputStream; 26 import java.io.IOException; 27 import java.nio.ByteBuffer; 28 import java.nio.ByteOrder; 29 import java.util.ArrayList; 30 import java.util.List; 31 import java.util.zip.CRC32; 32 import java.util.zip.Deflater; 33 34 /** 35 * Assorted ZIP format helpers. 36 * 37 * <p>NOTE: Most helper methods operating on {@code ByteBuffer} instances expect that the byte 38 * order of these buffers is little-endian. 39 */ 40 public abstract class ZipUtils { ZipUtils()41 private ZipUtils() {} 42 43 public static final short COMPRESSION_METHOD_STORED = 0; 44 public static final short COMPRESSION_METHOD_DEFLATED = 8; 45 46 public static final short GP_FLAG_DATA_DESCRIPTOR_USED = 0x08; 47 public static final short GP_FLAG_EFS = 0x0800; 48 49 private static final int ZIP_EOCD_REC_MIN_SIZE = 22; 50 private static final int ZIP_EOCD_REC_SIG = 0x06054b50; 51 private static final int ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET = 10; 52 private static final int ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET = 12; 53 private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16; 54 private static final int ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20; 55 56 private static final int UINT16_MAX_VALUE = 0xffff; 57 58 /** 59 * Sets the offset of the start of the ZIP Central Directory in the archive. 60 * 61 * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. 62 */ setZipEocdCentralDirectoryOffset( ByteBuffer zipEndOfCentralDirectory, long offset)63 public static void setZipEocdCentralDirectoryOffset( 64 ByteBuffer zipEndOfCentralDirectory, long offset) { 65 assertByteOrderLittleEndian(zipEndOfCentralDirectory); 66 setUnsignedInt32( 67 zipEndOfCentralDirectory, 68 zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET, 69 offset); 70 } 71 72 /** 73 * Sets the length of EOCD comment. 74 * 75 * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. 76 */ updateZipEocdCommentLen(ByteBuffer zipEndOfCentralDirectory)77 public static void updateZipEocdCommentLen(ByteBuffer zipEndOfCentralDirectory) { 78 assertByteOrderLittleEndian(zipEndOfCentralDirectory); 79 int commentLen = zipEndOfCentralDirectory.remaining() - ZIP_EOCD_REC_MIN_SIZE; 80 setUnsignedInt16( 81 zipEndOfCentralDirectory, 82 zipEndOfCentralDirectory.position() + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET, 83 commentLen); 84 } 85 86 /** 87 * Returns the offset of the start of the ZIP Central Directory in the archive. 88 * 89 * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. 90 */ getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory)91 public static long getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory) { 92 assertByteOrderLittleEndian(zipEndOfCentralDirectory); 93 return getUnsignedInt32( 94 zipEndOfCentralDirectory, 95 zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET); 96 } 97 98 /** 99 * Returns the size (in bytes) of the ZIP Central Directory. 100 * 101 * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. 102 */ getZipEocdCentralDirectorySizeBytes(ByteBuffer zipEndOfCentralDirectory)103 public static long getZipEocdCentralDirectorySizeBytes(ByteBuffer zipEndOfCentralDirectory) { 104 assertByteOrderLittleEndian(zipEndOfCentralDirectory); 105 return getUnsignedInt32( 106 zipEndOfCentralDirectory, 107 zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET); 108 } 109 110 /** 111 * Returns the total number of records in ZIP Central Directory. 112 * 113 * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. 114 */ getZipEocdCentralDirectoryTotalRecordCount( ByteBuffer zipEndOfCentralDirectory)115 public static int getZipEocdCentralDirectoryTotalRecordCount( 116 ByteBuffer zipEndOfCentralDirectory) { 117 assertByteOrderLittleEndian(zipEndOfCentralDirectory); 118 return getUnsignedInt16( 119 zipEndOfCentralDirectory, 120 zipEndOfCentralDirectory.position() 121 + ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET); 122 } 123 124 /** 125 * Returns the ZIP End of Central Directory record of the provided ZIP file. 126 * 127 * @return contents of the ZIP End of Central Directory record and the record's offset in the 128 * file or {@code null} if the file does not contain the record. 129 * 130 * @throws IOException if an I/O error occurs while reading the file. 131 */ findZipEndOfCentralDirectoryRecord(DataSource zip)132 public static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord(DataSource zip) 133 throws IOException { 134 // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. 135 // The record can be identified by its 4-byte signature/magic which is located at the very 136 // beginning of the record. A complication is that the record is variable-length because of 137 // the comment field. 138 // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from 139 // end of the buffer for the EOCD record signature. Whenever we find a signature, we check 140 // the candidate record's comment length is such that the remainder of the record takes up 141 // exactly the remaining bytes in the buffer. The search is bounded because the maximum 142 // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number. 143 144 long fileSize = zip.size(); 145 if (fileSize < ZIP_EOCD_REC_MIN_SIZE) { 146 return null; 147 } 148 149 // Optimization: 99.99% of APKs have a zero-length comment field in the EoCD record and thus 150 // the EoCD record offset is known in advance. Try that offset first to avoid unnecessarily 151 // reading more data. 152 Pair<ByteBuffer, Long> result = findZipEndOfCentralDirectoryRecord(zip, 0); 153 if (result != null) { 154 return result; 155 } 156 157 // EoCD does not start where we expected it to. Perhaps it contains a non-empty comment 158 // field. Expand the search. The maximum size of the comment field in EoCD is 65535 because 159 // the comment length field is an unsigned 16-bit number. 160 return findZipEndOfCentralDirectoryRecord(zip, UINT16_MAX_VALUE); 161 } 162 163 /** 164 * Returns the ZIP End of Central Directory record of the provided ZIP file. 165 * 166 * @param maxCommentSize maximum accepted size (in bytes) of EoCD comment field. The permitted 167 * value is from 0 to 65535 inclusive. The smaller the value, the faster this method 168 * locates the record, provided its comment field is no longer than this value. 169 * 170 * @return contents of the ZIP End of Central Directory record and the record's offset in the 171 * file or {@code null} if the file does not contain the record. 172 * 173 * @throws IOException if an I/O error occurs while reading the file. 174 */ findZipEndOfCentralDirectoryRecord( DataSource zip, int maxCommentSize)175 private static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord( 176 DataSource zip, int maxCommentSize) throws IOException { 177 // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. 178 // The record can be identified by its 4-byte signature/magic which is located at the very 179 // beginning of the record. A complication is that the record is variable-length because of 180 // the comment field. 181 // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from 182 // end of the buffer for the EOCD record signature. Whenever we find a signature, we check 183 // the candidate record's comment length is such that the remainder of the record takes up 184 // exactly the remaining bytes in the buffer. The search is bounded because the maximum 185 // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number. 186 187 if ((maxCommentSize < 0) || (maxCommentSize > UINT16_MAX_VALUE)) { 188 throw new IllegalArgumentException("maxCommentSize: " + maxCommentSize); 189 } 190 191 long fileSize = zip.size(); 192 if (fileSize < ZIP_EOCD_REC_MIN_SIZE) { 193 // No space for EoCD record in the file. 194 return null; 195 } 196 // Lower maxCommentSize if the file is too small. 197 maxCommentSize = (int) Math.min(maxCommentSize, fileSize - ZIP_EOCD_REC_MIN_SIZE); 198 199 int maxEocdSize = ZIP_EOCD_REC_MIN_SIZE + maxCommentSize; 200 long bufOffsetInFile = fileSize - maxEocdSize; 201 ByteBuffer buf = zip.getByteBuffer(bufOffsetInFile, maxEocdSize); 202 buf.order(ByteOrder.LITTLE_ENDIAN); 203 int eocdOffsetInBuf = findZipEndOfCentralDirectoryRecord(buf); 204 if (eocdOffsetInBuf == -1) { 205 // No EoCD record found in the buffer 206 return null; 207 } 208 // EoCD found 209 buf.position(eocdOffsetInBuf); 210 ByteBuffer eocd = buf.slice(); 211 eocd.order(ByteOrder.LITTLE_ENDIAN); 212 return Pair.of(eocd, bufOffsetInFile + eocdOffsetInBuf); 213 } 214 215 /** 216 * Returns the position at which ZIP End of Central Directory record starts in the provided 217 * buffer or {@code -1} if the record is not present. 218 * 219 * <p>NOTE: Byte order of {@code zipContents} must be little-endian. 220 */ findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents)221 private static int findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents) { 222 assertByteOrderLittleEndian(zipContents); 223 224 // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. 225 // The record can be identified by its 4-byte signature/magic which is located at the very 226 // beginning of the record. A complication is that the record is variable-length because of 227 // the comment field. 228 // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from 229 // end of the buffer for the EOCD record signature. Whenever we find a signature, we check 230 // the candidate record's comment length is such that the remainder of the record takes up 231 // exactly the remaining bytes in the buffer. The search is bounded because the maximum 232 // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number. 233 234 int archiveSize = zipContents.capacity(); 235 if (archiveSize < ZIP_EOCD_REC_MIN_SIZE) { 236 return -1; 237 } 238 int maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE); 239 int eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE; 240 for (int expectedCommentLength = 0; expectedCommentLength <= maxCommentLength; 241 expectedCommentLength++) { 242 int eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength; 243 if (zipContents.getInt(eocdStartPos) == ZIP_EOCD_REC_SIG) { 244 int actualCommentLength = 245 getUnsignedInt16( 246 zipContents, eocdStartPos + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET); 247 if (actualCommentLength == expectedCommentLength) { 248 return eocdStartPos; 249 } 250 } 251 } 252 253 return -1; 254 } 255 assertByteOrderLittleEndian(ByteBuffer buffer)256 static void assertByteOrderLittleEndian(ByteBuffer buffer) { 257 if (buffer.order() != ByteOrder.LITTLE_ENDIAN) { 258 throw new IllegalArgumentException("ByteBuffer byte order must be little endian"); 259 } 260 } 261 getUnsignedInt16(ByteBuffer buffer, int offset)262 public static int getUnsignedInt16(ByteBuffer buffer, int offset) { 263 return buffer.getShort(offset) & 0xffff; 264 } 265 getUnsignedInt16(ByteBuffer buffer)266 public static int getUnsignedInt16(ByteBuffer buffer) { 267 return buffer.getShort() & 0xffff; 268 } 269 parseZipCentralDirectory( DataSource apk, ZipSections apkSections)270 public static List<CentralDirectoryRecord> parseZipCentralDirectory( 271 DataSource apk, 272 ZipSections apkSections) 273 throws IOException, ApkFormatException { 274 // Read the ZIP Central Directory 275 long cdSizeBytes = apkSections.getZipCentralDirectorySizeBytes(); 276 if (cdSizeBytes > Integer.MAX_VALUE) { 277 throw new ApkFormatException("ZIP Central Directory too large: " + cdSizeBytes); 278 } 279 long cdOffset = apkSections.getZipCentralDirectoryOffset(); 280 ByteBuffer cd = apk.getByteBuffer(cdOffset, (int) cdSizeBytes); 281 cd.order(ByteOrder.LITTLE_ENDIAN); 282 283 // Parse the ZIP Central Directory 284 int expectedCdRecordCount = apkSections.getZipCentralDirectoryRecordCount(); 285 List<CentralDirectoryRecord> cdRecords = new ArrayList<>(expectedCdRecordCount); 286 for (int i = 0; i < expectedCdRecordCount; i++) { 287 CentralDirectoryRecord cdRecord; 288 int offsetInsideCd = cd.position(); 289 try { 290 cdRecord = CentralDirectoryRecord.getRecord(cd); 291 } catch (ZipFormatException e) { 292 throw new ApkFormatException( 293 "Malformed ZIP Central Directory record #" + (i + 1) 294 + " at file offset " + (cdOffset + offsetInsideCd), 295 e); 296 } 297 String entryName = cdRecord.getName(); 298 if (entryName.endsWith("/")) { 299 // Ignore directory entries 300 continue; 301 } 302 cdRecords.add(cdRecord); 303 } 304 // There may be more data in Central Directory, but we don't warn or throw because Android 305 // ignores unused CD data. 306 307 return cdRecords; 308 } 309 setUnsignedInt16(ByteBuffer buffer, int offset, int value)310 static void setUnsignedInt16(ByteBuffer buffer, int offset, int value) { 311 if ((value < 0) || (value > 0xffff)) { 312 throw new IllegalArgumentException("uint16 value of out range: " + value); 313 } 314 buffer.putShort(offset, (short) value); 315 } 316 setUnsignedInt32(ByteBuffer buffer, int offset, long value)317 static void setUnsignedInt32(ByteBuffer buffer, int offset, long value) { 318 if ((value < 0) || (value > 0xffffffffL)) { 319 throw new IllegalArgumentException("uint32 value of out range: " + value); 320 } 321 buffer.putInt(offset, (int) value); 322 } 323 putUnsignedInt16(ByteBuffer buffer, int value)324 public static void putUnsignedInt16(ByteBuffer buffer, int value) { 325 if ((value < 0) || (value > 0xffff)) { 326 throw new IllegalArgumentException("uint16 value of out range: " + value); 327 } 328 buffer.putShort((short) value); 329 } 330 getUnsignedInt32(ByteBuffer buffer, int offset)331 static long getUnsignedInt32(ByteBuffer buffer, int offset) { 332 return buffer.getInt(offset) & 0xffffffffL; 333 } 334 getUnsignedInt32(ByteBuffer buffer)335 static long getUnsignedInt32(ByteBuffer buffer) { 336 return buffer.getInt() & 0xffffffffL; 337 } 338 putUnsignedInt32(ByteBuffer buffer, long value)339 static void putUnsignedInt32(ByteBuffer buffer, long value) { 340 if ((value < 0) || (value > 0xffffffffL)) { 341 throw new IllegalArgumentException("uint32 value of out range: " + value); 342 } 343 buffer.putInt((int) value); 344 } 345 deflate(ByteBuffer input)346 public static DeflateResult deflate(ByteBuffer input) { 347 byte[] inputBuf; 348 int inputOffset; 349 int inputLength = input.remaining(); 350 if (input.hasArray()) { 351 inputBuf = input.array(); 352 inputOffset = input.arrayOffset() + input.position(); 353 input.position(input.limit()); 354 } else { 355 inputBuf = new byte[inputLength]; 356 inputOffset = 0; 357 input.get(inputBuf); 358 } 359 CRC32 crc32 = new CRC32(); 360 crc32.update(inputBuf, inputOffset, inputLength); 361 long crc32Value = crc32.getValue(); 362 ByteArrayOutputStream out = new ByteArrayOutputStream(); 363 Deflater deflater = new Deflater(9, true); 364 deflater.setInput(inputBuf, inputOffset, inputLength); 365 deflater.finish(); 366 byte[] buf = new byte[65536]; 367 while (!deflater.finished()) { 368 int chunkSize = deflater.deflate(buf); 369 out.write(buf, 0, chunkSize); 370 } 371 return new DeflateResult(inputLength, crc32Value, out.toByteArray()); 372 } 373 374 public static class DeflateResult { 375 public final int inputSizeBytes; 376 public final long inputCrc32; 377 public final byte[] output; 378 DeflateResult(int inputSizeBytes, long inputCrc32, byte[] output)379 public DeflateResult(int inputSizeBytes, long inputCrc32, byte[] output) { 380 this.inputSizeBytes = inputSizeBytes; 381 this.inputCrc32 = inputCrc32; 382 this.output = output; 383 } 384 } 385 }