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 * Returns the offset of the start of the ZIP Central Directory in the archive. 74 * 75 * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. 76 */ getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory)77 public static long getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory) { 78 assertByteOrderLittleEndian(zipEndOfCentralDirectory); 79 return getUnsignedInt32( 80 zipEndOfCentralDirectory, 81 zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET); 82 } 83 84 /** 85 * Returns the size (in bytes) of the ZIP Central Directory. 86 * 87 * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. 88 */ getZipEocdCentralDirectorySizeBytes(ByteBuffer zipEndOfCentralDirectory)89 public static long getZipEocdCentralDirectorySizeBytes(ByteBuffer zipEndOfCentralDirectory) { 90 assertByteOrderLittleEndian(zipEndOfCentralDirectory); 91 return getUnsignedInt32( 92 zipEndOfCentralDirectory, 93 zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET); 94 } 95 96 /** 97 * Returns the total number of records in ZIP Central Directory. 98 * 99 * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. 100 */ getZipEocdCentralDirectoryTotalRecordCount( ByteBuffer zipEndOfCentralDirectory)101 public static int getZipEocdCentralDirectoryTotalRecordCount( 102 ByteBuffer zipEndOfCentralDirectory) { 103 assertByteOrderLittleEndian(zipEndOfCentralDirectory); 104 return getUnsignedInt16( 105 zipEndOfCentralDirectory, 106 zipEndOfCentralDirectory.position() 107 + ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET); 108 } 109 110 /** 111 * Returns the ZIP End of Central Directory record of the provided ZIP file. 112 * 113 * @return contents of the ZIP End of Central Directory record and the record's offset in the 114 * file or {@code null} if the file does not contain the record. 115 * 116 * @throws IOException if an I/O error occurs while reading the file. 117 */ findZipEndOfCentralDirectoryRecord(DataSource zip)118 public static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord(DataSource zip) 119 throws IOException { 120 // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. 121 // The record can be identified by its 4-byte signature/magic which is located at the very 122 // beginning of the record. A complication is that the record is variable-length because of 123 // the comment field. 124 // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from 125 // end of the buffer for the EOCD record signature. Whenever we find a signature, we check 126 // the candidate record's comment length is such that the remainder of the record takes up 127 // exactly the remaining bytes in the buffer. The search is bounded because the maximum 128 // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number. 129 130 long fileSize = zip.size(); 131 if (fileSize < ZIP_EOCD_REC_MIN_SIZE) { 132 return null; 133 } 134 135 // Optimization: 99.99% of APKs have a zero-length comment field in the EoCD record and thus 136 // the EoCD record offset is known in advance. Try that offset first to avoid unnecessarily 137 // reading more data. 138 Pair<ByteBuffer, Long> result = findZipEndOfCentralDirectoryRecord(zip, 0); 139 if (result != null) { 140 return result; 141 } 142 143 // EoCD does not start where we expected it to. Perhaps it contains a non-empty comment 144 // field. Expand the search. The maximum size of the comment field in EoCD is 65535 because 145 // the comment length field is an unsigned 16-bit number. 146 return findZipEndOfCentralDirectoryRecord(zip, UINT16_MAX_VALUE); 147 } 148 149 /** 150 * Returns the ZIP End of Central Directory record of the provided ZIP file. 151 * 152 * @param maxCommentSize maximum accepted size (in bytes) of EoCD comment field. The permitted 153 * value is from 0 to 65535 inclusive. The smaller the value, the faster this method 154 * locates the record, provided its comment field is no longer than this value. 155 * 156 * @return contents of the ZIP End of Central Directory record and the record's offset in the 157 * file or {@code null} if the file does not contain the record. 158 * 159 * @throws IOException if an I/O error occurs while reading the file. 160 */ findZipEndOfCentralDirectoryRecord( DataSource zip, int maxCommentSize)161 private static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord( 162 DataSource zip, int maxCommentSize) throws IOException { 163 // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. 164 // The record can be identified by its 4-byte signature/magic which is located at the very 165 // beginning of the record. A complication is that the record is variable-length because of 166 // the comment field. 167 // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from 168 // end of the buffer for the EOCD record signature. Whenever we find a signature, we check 169 // the candidate record's comment length is such that the remainder of the record takes up 170 // exactly the remaining bytes in the buffer. The search is bounded because the maximum 171 // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number. 172 173 if ((maxCommentSize < 0) || (maxCommentSize > UINT16_MAX_VALUE)) { 174 throw new IllegalArgumentException("maxCommentSize: " + maxCommentSize); 175 } 176 177 long fileSize = zip.size(); 178 if (fileSize < ZIP_EOCD_REC_MIN_SIZE) { 179 // No space for EoCD record in the file. 180 return null; 181 } 182 // Lower maxCommentSize if the file is too small. 183 maxCommentSize = (int) Math.min(maxCommentSize, fileSize - ZIP_EOCD_REC_MIN_SIZE); 184 185 int maxEocdSize = ZIP_EOCD_REC_MIN_SIZE + maxCommentSize; 186 long bufOffsetInFile = fileSize - maxEocdSize; 187 ByteBuffer buf = zip.getByteBuffer(bufOffsetInFile, maxEocdSize); 188 buf.order(ByteOrder.LITTLE_ENDIAN); 189 int eocdOffsetInBuf = findZipEndOfCentralDirectoryRecord(buf); 190 if (eocdOffsetInBuf == -1) { 191 // No EoCD record found in the buffer 192 return null; 193 } 194 // EoCD found 195 buf.position(eocdOffsetInBuf); 196 ByteBuffer eocd = buf.slice(); 197 eocd.order(ByteOrder.LITTLE_ENDIAN); 198 return Pair.of(eocd, bufOffsetInFile + eocdOffsetInBuf); 199 } 200 201 /** 202 * Returns the position at which ZIP End of Central Directory record starts in the provided 203 * buffer or {@code -1} if the record is not present. 204 * 205 * <p>NOTE: Byte order of {@code zipContents} must be little-endian. 206 */ findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents)207 private static int findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents) { 208 assertByteOrderLittleEndian(zipContents); 209 210 // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. 211 // The record can be identified by its 4-byte signature/magic which is located at the very 212 // beginning of the record. A complication is that the record is variable-length because of 213 // the comment field. 214 // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from 215 // end of the buffer for the EOCD record signature. Whenever we find a signature, we check 216 // the candidate record's comment length is such that the remainder of the record takes up 217 // exactly the remaining bytes in the buffer. The search is bounded because the maximum 218 // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number. 219 220 int archiveSize = zipContents.capacity(); 221 if (archiveSize < ZIP_EOCD_REC_MIN_SIZE) { 222 return -1; 223 } 224 int maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE); 225 int eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE; 226 for (int expectedCommentLength = 0; expectedCommentLength <= maxCommentLength; 227 expectedCommentLength++) { 228 int eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength; 229 if (zipContents.getInt(eocdStartPos) == ZIP_EOCD_REC_SIG) { 230 int actualCommentLength = 231 getUnsignedInt16( 232 zipContents, eocdStartPos + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET); 233 if (actualCommentLength == expectedCommentLength) { 234 return eocdStartPos; 235 } 236 } 237 } 238 239 return -1; 240 } 241 assertByteOrderLittleEndian(ByteBuffer buffer)242 static void assertByteOrderLittleEndian(ByteBuffer buffer) { 243 if (buffer.order() != ByteOrder.LITTLE_ENDIAN) { 244 throw new IllegalArgumentException("ByteBuffer byte order must be little endian"); 245 } 246 } 247 getUnsignedInt16(ByteBuffer buffer, int offset)248 public static int getUnsignedInt16(ByteBuffer buffer, int offset) { 249 return buffer.getShort(offset) & 0xffff; 250 } 251 getUnsignedInt16(ByteBuffer buffer)252 public static int getUnsignedInt16(ByteBuffer buffer) { 253 return buffer.getShort() & 0xffff; 254 } 255 parseZipCentralDirectory( DataSource apk, ZipSections apkSections)256 public static List<CentralDirectoryRecord> parseZipCentralDirectory( 257 DataSource apk, 258 ZipSections apkSections) 259 throws IOException, ApkFormatException { 260 // Read the ZIP Central Directory 261 long cdSizeBytes = apkSections.getZipCentralDirectorySizeBytes(); 262 if (cdSizeBytes > Integer.MAX_VALUE) { 263 throw new ApkFormatException("ZIP Central Directory too large: " + cdSizeBytes); 264 } 265 long cdOffset = apkSections.getZipCentralDirectoryOffset(); 266 ByteBuffer cd = apk.getByteBuffer(cdOffset, (int) cdSizeBytes); 267 cd.order(ByteOrder.LITTLE_ENDIAN); 268 269 // Parse the ZIP Central Directory 270 int expectedCdRecordCount = apkSections.getZipCentralDirectoryRecordCount(); 271 List<CentralDirectoryRecord> cdRecords = new ArrayList<>(expectedCdRecordCount); 272 for (int i = 0; i < expectedCdRecordCount; i++) { 273 CentralDirectoryRecord cdRecord; 274 int offsetInsideCd = cd.position(); 275 try { 276 cdRecord = CentralDirectoryRecord.getRecord(cd); 277 } catch (ZipFormatException e) { 278 throw new ApkFormatException( 279 "Malformed ZIP Central Directory record #" + (i + 1) 280 + " at file offset " + (cdOffset + offsetInsideCd), 281 e); 282 } 283 String entryName = cdRecord.getName(); 284 if (entryName.endsWith("/")) { 285 // Ignore directory entries 286 continue; 287 } 288 cdRecords.add(cdRecord); 289 } 290 // There may be more data in Central Directory, but we don't warn or throw because Android 291 // ignores unused CD data. 292 293 return cdRecords; 294 } 295 setUnsignedInt16(ByteBuffer buffer, int offset, int value)296 static void setUnsignedInt16(ByteBuffer buffer, int offset, int value) { 297 if ((value < 0) || (value > 0xffff)) { 298 throw new IllegalArgumentException("uint16 value of out range: " + value); 299 } 300 buffer.putShort(offset, (short) value); 301 } 302 setUnsignedInt32(ByteBuffer buffer, int offset, long value)303 static void setUnsignedInt32(ByteBuffer buffer, int offset, long value) { 304 if ((value < 0) || (value > 0xffffffffL)) { 305 throw new IllegalArgumentException("uint32 value of out range: " + value); 306 } 307 buffer.putInt(offset, (int) value); 308 } 309 putUnsignedInt16(ByteBuffer buffer, int value)310 public static void putUnsignedInt16(ByteBuffer buffer, int value) { 311 if ((value < 0) || (value > 0xffff)) { 312 throw new IllegalArgumentException("uint16 value of out range: " + value); 313 } 314 buffer.putShort((short) value); 315 } 316 getUnsignedInt32(ByteBuffer buffer, int offset)317 static long getUnsignedInt32(ByteBuffer buffer, int offset) { 318 return buffer.getInt(offset) & 0xffffffffL; 319 } 320 getUnsignedInt32(ByteBuffer buffer)321 static long getUnsignedInt32(ByteBuffer buffer) { 322 return buffer.getInt() & 0xffffffffL; 323 } 324 putUnsignedInt32(ByteBuffer buffer, long value)325 static void putUnsignedInt32(ByteBuffer buffer, long value) { 326 if ((value < 0) || (value > 0xffffffffL)) { 327 throw new IllegalArgumentException("uint32 value of out range: " + value); 328 } 329 buffer.putInt((int) value); 330 } 331 deflate(ByteBuffer input)332 public static DeflateResult deflate(ByteBuffer input) { 333 byte[] inputBuf; 334 int inputOffset; 335 int inputLength = input.remaining(); 336 if (input.hasArray()) { 337 inputBuf = input.array(); 338 inputOffset = input.arrayOffset() + input.position(); 339 input.position(input.limit()); 340 } else { 341 inputBuf = new byte[inputLength]; 342 inputOffset = 0; 343 input.get(inputBuf); 344 } 345 CRC32 crc32 = new CRC32(); 346 crc32.update(inputBuf, inputOffset, inputLength); 347 long crc32Value = crc32.getValue(); 348 ByteArrayOutputStream out = new ByteArrayOutputStream(); 349 Deflater deflater = new Deflater(9, true); 350 deflater.setInput(inputBuf, inputOffset, inputLength); 351 deflater.finish(); 352 byte[] buf = new byte[65536]; 353 while (!deflater.finished()) { 354 int chunkSize = deflater.deflate(buf); 355 out.write(buf, 0, chunkSize); 356 } 357 return new DeflateResult(inputLength, crc32Value, out.toByteArray()); 358 } 359 360 public static class DeflateResult { 361 public final int inputSizeBytes; 362 public final long inputCrc32; 363 public final byte[] output; 364 DeflateResult(int inputSizeBytes, long inputCrc32, byte[] output)365 public DeflateResult(int inputSizeBytes, long inputCrc32, byte[] output) { 366 this.inputSizeBytes = inputSizeBytes; 367 this.inputCrc32 = inputCrc32; 368 this.output = output; 369 } 370 } 371 }