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.internal.util.Pair; 20 import com.android.apksig.util.DataSource; 21 import java.io.ByteArrayOutputStream; 22 import java.io.IOException; 23 import java.nio.ByteBuffer; 24 import java.nio.ByteOrder; 25 import java.util.zip.CRC32; 26 import java.util.zip.Deflater; 27 28 /** 29 * Assorted ZIP format helpers. 30 * 31 * <p>NOTE: Most helper methods operating on {@code ByteBuffer} instances expect that the byte 32 * order of these buffers is little-endian. 33 */ 34 public abstract class ZipUtils { ZipUtils()35 private ZipUtils() {} 36 37 public static final short COMPRESSION_METHOD_STORED = 0; 38 public static final short COMPRESSION_METHOD_DEFLATED = 8; 39 40 public static final short GP_FLAG_DATA_DESCRIPTOR_USED = 0x08; 41 public static final short GP_FLAG_EFS = 0x0800; 42 43 private static final int ZIP_EOCD_REC_MIN_SIZE = 22; 44 private static final int ZIP_EOCD_REC_SIG = 0x06054b50; 45 private static final int ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET = 10; 46 private static final int ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET = 12; 47 private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16; 48 private static final int ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20; 49 50 private static final int UINT16_MAX_VALUE = 0xffff; 51 52 /** 53 * Sets the offset of the start of the ZIP Central Directory in the archive. 54 * 55 * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. 56 */ setZipEocdCentralDirectoryOffset( ByteBuffer zipEndOfCentralDirectory, long offset)57 public static void setZipEocdCentralDirectoryOffset( 58 ByteBuffer zipEndOfCentralDirectory, long offset) { 59 assertByteOrderLittleEndian(zipEndOfCentralDirectory); 60 setUnsignedInt32( 61 zipEndOfCentralDirectory, 62 zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET, 63 offset); 64 } 65 66 /** 67 * Returns the offset of the start of the ZIP Central Directory in the archive. 68 * 69 * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. 70 */ getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory)71 public static long getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory) { 72 assertByteOrderLittleEndian(zipEndOfCentralDirectory); 73 return getUnsignedInt32( 74 zipEndOfCentralDirectory, 75 zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET); 76 } 77 78 /** 79 * Returns the size (in bytes) of the ZIP Central Directory. 80 * 81 * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. 82 */ getZipEocdCentralDirectorySizeBytes(ByteBuffer zipEndOfCentralDirectory)83 public static long getZipEocdCentralDirectorySizeBytes(ByteBuffer zipEndOfCentralDirectory) { 84 assertByteOrderLittleEndian(zipEndOfCentralDirectory); 85 return getUnsignedInt32( 86 zipEndOfCentralDirectory, 87 zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET); 88 } 89 90 /** 91 * Returns the total number of records in ZIP Central Directory. 92 * 93 * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. 94 */ getZipEocdCentralDirectoryTotalRecordCount( ByteBuffer zipEndOfCentralDirectory)95 public static int getZipEocdCentralDirectoryTotalRecordCount( 96 ByteBuffer zipEndOfCentralDirectory) { 97 assertByteOrderLittleEndian(zipEndOfCentralDirectory); 98 return getUnsignedInt16( 99 zipEndOfCentralDirectory, 100 zipEndOfCentralDirectory.position() 101 + ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET); 102 } 103 104 /** 105 * Returns the ZIP End of Central Directory record of the provided ZIP file. 106 * 107 * @return contents of the ZIP End of Central Directory record and the record's offset in the 108 * file or {@code null} if the file does not contain the record. 109 * 110 * @throws IOException if an I/O error occurs while reading the file. 111 */ findZipEndOfCentralDirectoryRecord(DataSource zip)112 public static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord(DataSource zip) 113 throws IOException { 114 // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. 115 // The record can be identified by its 4-byte signature/magic which is located at the very 116 // beginning of the record. A complication is that the record is variable-length because of 117 // the comment field. 118 // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from 119 // end of the buffer for the EOCD record signature. Whenever we find a signature, we check 120 // the candidate record's comment length is such that the remainder of the record takes up 121 // exactly the remaining bytes in the buffer. The search is bounded because the maximum 122 // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number. 123 124 long fileSize = zip.size(); 125 if (fileSize < ZIP_EOCD_REC_MIN_SIZE) { 126 return null; 127 } 128 129 // Optimization: 99.99% of APKs have a zero-length comment field in the EoCD record and thus 130 // the EoCD record offset is known in advance. Try that offset first to avoid unnecessarily 131 // reading more data. 132 Pair<ByteBuffer, Long> result = findZipEndOfCentralDirectoryRecord(zip, 0); 133 if (result != null) { 134 return result; 135 } 136 137 // EoCD does not start where we expected it to. Perhaps it contains a non-empty comment 138 // field. Expand the search. The maximum size of the comment field in EoCD is 65535 because 139 // the comment length field is an unsigned 16-bit number. 140 return findZipEndOfCentralDirectoryRecord(zip, UINT16_MAX_VALUE); 141 } 142 143 /** 144 * Returns the ZIP End of Central Directory record of the provided ZIP file. 145 * 146 * @param maxCommentSize maximum accepted size (in bytes) of EoCD comment field. The permitted 147 * value is from 0 to 65535 inclusive. The smaller the value, the faster this method 148 * locates the record, provided its comment field is no longer than this value. 149 * 150 * @return contents of the ZIP End of Central Directory record and the record's offset in the 151 * file or {@code null} if the file does not contain the record. 152 * 153 * @throws IOException if an I/O error occurs while reading the file. 154 */ findZipEndOfCentralDirectoryRecord( DataSource zip, int maxCommentSize)155 private static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord( 156 DataSource zip, int maxCommentSize) throws IOException { 157 // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. 158 // The record can be identified by its 4-byte signature/magic which is located at the very 159 // beginning of the record. A complication is that the record is variable-length because of 160 // the comment field. 161 // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from 162 // end of the buffer for the EOCD record signature. Whenever we find a signature, we check 163 // the candidate record's comment length is such that the remainder of the record takes up 164 // exactly the remaining bytes in the buffer. The search is bounded because the maximum 165 // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number. 166 167 if ((maxCommentSize < 0) || (maxCommentSize > UINT16_MAX_VALUE)) { 168 throw new IllegalArgumentException("maxCommentSize: " + maxCommentSize); 169 } 170 171 long fileSize = zip.size(); 172 if (fileSize < ZIP_EOCD_REC_MIN_SIZE) { 173 // No space for EoCD record in the file. 174 return null; 175 } 176 // Lower maxCommentSize if the file is too small. 177 maxCommentSize = (int) Math.min(maxCommentSize, fileSize - ZIP_EOCD_REC_MIN_SIZE); 178 179 int maxEocdSize = ZIP_EOCD_REC_MIN_SIZE + maxCommentSize; 180 long bufOffsetInFile = fileSize - maxEocdSize; 181 ByteBuffer buf = zip.getByteBuffer(bufOffsetInFile, maxEocdSize); 182 buf.order(ByteOrder.LITTLE_ENDIAN); 183 int eocdOffsetInBuf = findZipEndOfCentralDirectoryRecord(buf); 184 if (eocdOffsetInBuf == -1) { 185 // No EoCD record found in the buffer 186 return null; 187 } 188 // EoCD found 189 buf.position(eocdOffsetInBuf); 190 ByteBuffer eocd = buf.slice(); 191 eocd.order(ByteOrder.LITTLE_ENDIAN); 192 return Pair.of(eocd, bufOffsetInFile + eocdOffsetInBuf); 193 } 194 195 /** 196 * Returns the position at which ZIP End of Central Directory record starts in the provided 197 * buffer or {@code -1} if the record is not present. 198 * 199 * <p>NOTE: Byte order of {@code zipContents} must be little-endian. 200 */ findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents)201 private static int findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents) { 202 assertByteOrderLittleEndian(zipContents); 203 204 // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. 205 // The record can be identified by its 4-byte signature/magic which is located at the very 206 // beginning of the record. A complication is that the record is variable-length because of 207 // the comment field. 208 // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from 209 // end of the buffer for the EOCD record signature. Whenever we find a signature, we check 210 // the candidate record's comment length is such that the remainder of the record takes up 211 // exactly the remaining bytes in the buffer. The search is bounded because the maximum 212 // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number. 213 214 int archiveSize = zipContents.capacity(); 215 if (archiveSize < ZIP_EOCD_REC_MIN_SIZE) { 216 return -1; 217 } 218 int maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE); 219 int eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE; 220 for (int expectedCommentLength = 0; expectedCommentLength <= maxCommentLength; 221 expectedCommentLength++) { 222 int eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength; 223 if (zipContents.getInt(eocdStartPos) == ZIP_EOCD_REC_SIG) { 224 int actualCommentLength = 225 getUnsignedInt16( 226 zipContents, eocdStartPos + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET); 227 if (actualCommentLength == expectedCommentLength) { 228 return eocdStartPos; 229 } 230 } 231 } 232 233 return -1; 234 } 235 assertByteOrderLittleEndian(ByteBuffer buffer)236 static void assertByteOrderLittleEndian(ByteBuffer buffer) { 237 if (buffer.order() != ByteOrder.LITTLE_ENDIAN) { 238 throw new IllegalArgumentException("ByteBuffer byte order must be little endian"); 239 } 240 } 241 getUnsignedInt16(ByteBuffer buffer, int offset)242 public static int getUnsignedInt16(ByteBuffer buffer, int offset) { 243 return buffer.getShort(offset) & 0xffff; 244 } 245 getUnsignedInt16(ByteBuffer buffer)246 public static int getUnsignedInt16(ByteBuffer buffer) { 247 return buffer.getShort() & 0xffff; 248 } 249 setUnsignedInt16(ByteBuffer buffer, int offset, int value)250 static void setUnsignedInt16(ByteBuffer buffer, int offset, int value) { 251 if ((value < 0) || (value > 0xffff)) { 252 throw new IllegalArgumentException("uint16 value of out range: " + value); 253 } 254 buffer.putShort(offset, (short) value); 255 } 256 setUnsignedInt32(ByteBuffer buffer, int offset, long value)257 static void setUnsignedInt32(ByteBuffer buffer, int offset, long value) { 258 if ((value < 0) || (value > 0xffffffffL)) { 259 throw new IllegalArgumentException("uint32 value of out range: " + value); 260 } 261 buffer.putInt(offset, (int) value); 262 } 263 putUnsignedInt16(ByteBuffer buffer, int value)264 public static void putUnsignedInt16(ByteBuffer buffer, int value) { 265 if ((value < 0) || (value > 0xffff)) { 266 throw new IllegalArgumentException("uint16 value of out range: " + value); 267 } 268 buffer.putShort((short) value); 269 } 270 getUnsignedInt32(ByteBuffer buffer, int offset)271 static long getUnsignedInt32(ByteBuffer buffer, int offset) { 272 return buffer.getInt(offset) & 0xffffffffL; 273 } 274 getUnsignedInt32(ByteBuffer buffer)275 static long getUnsignedInt32(ByteBuffer buffer) { 276 return buffer.getInt() & 0xffffffffL; 277 } 278 putUnsignedInt32(ByteBuffer buffer, long value)279 static void putUnsignedInt32(ByteBuffer buffer, long value) { 280 if ((value < 0) || (value > 0xffffffffL)) { 281 throw new IllegalArgumentException("uint32 value of out range: " + value); 282 } 283 buffer.putInt((int) value); 284 } 285 deflate(ByteBuffer input)286 public static DeflateResult deflate(ByteBuffer input) { 287 byte[] inputBuf; 288 int inputOffset; 289 int inputLength = input.remaining(); 290 if (input.hasArray()) { 291 inputBuf = input.array(); 292 inputOffset = input.arrayOffset() + input.position(); 293 input.position(input.limit()); 294 } else { 295 inputBuf = new byte[inputLength]; 296 inputOffset = 0; 297 input.get(inputBuf); 298 } 299 CRC32 crc32 = new CRC32(); 300 crc32.update(inputBuf, inputOffset, inputLength); 301 long crc32Value = crc32.getValue(); 302 ByteArrayOutputStream out = new ByteArrayOutputStream(); 303 Deflater deflater = new Deflater(9, true); 304 deflater.setInput(inputBuf, inputOffset, inputLength); 305 deflater.finish(); 306 byte[] buf = new byte[65536]; 307 while (!deflater.finished()) { 308 int chunkSize = deflater.deflate(buf); 309 out.write(buf, 0, chunkSize); 310 } 311 return new DeflateResult(inputLength, crc32Value, out.toByteArray()); 312 } 313 314 public static class DeflateResult { 315 public final int inputSizeBytes; 316 public final long inputCrc32; 317 public final byte[] output; 318 DeflateResult(int inputSizeBytes, long inputCrc32, byte[] output)319 public DeflateResult(int inputSizeBytes, long inputCrc32, byte[] output) { 320 this.inputSizeBytes = inputSizeBytes; 321 this.inputCrc32 = inputCrc32; 322 this.output = output; 323 } 324 } 325 }