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 android.util.apk; 18 19 import android.util.Pair; 20 21 import java.io.IOException; 22 import java.io.RandomAccessFile; 23 import java.nio.ByteBuffer; 24 import java.nio.ByteOrder; 25 26 /** 27 * Assorted ZIP format helpers. 28 * 29 * <p>NOTE: Most helper methods operating on {@code ByteBuffer} instances expect that the byte 30 * order of these buffers is little-endian. 31 */ 32 abstract class ZipUtils { ZipUtils()33 private ZipUtils() {} 34 35 private static final int ZIP_EOCD_REC_MIN_SIZE = 22; 36 private static final int ZIP_EOCD_REC_SIG = 0x06054b50; 37 private static final int ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET = 12; 38 private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16; 39 private static final int ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20; 40 41 private static final int ZIP64_EOCD_LOCATOR_SIZE = 20; 42 private static final int ZIP64_EOCD_LOCATOR_SIG_REVERSE_BYTE_ORDER = 0x504b0607; 43 44 private static final int UINT16_MAX_VALUE = 0xffff; 45 46 /** 47 * Returns the ZIP End of Central Directory record of the provided ZIP file. 48 * 49 * @return contents of the ZIP End of Central Directory record and the record's offset in the 50 * file or {@code null} if the file does not contain the record. 51 * 52 * @throws IOException if an I/O error occurs while reading the file. 53 */ findZipEndOfCentralDirectoryRecord(RandomAccessFile zip)54 static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord(RandomAccessFile zip) 55 throws IOException { 56 // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. 57 // The record can be identified by its 4-byte signature/magic which is located at the very 58 // beginning of the record. A complication is that the record is variable-length because of 59 // the comment field. 60 // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from 61 // end of the buffer for the EOCD record signature. Whenever we find a signature, we check 62 // the candidate record's comment length is such that the remainder of the record takes up 63 // exactly the remaining bytes in the buffer. The search is bounded because the maximum 64 // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number. 65 66 long fileSize = zip.length(); 67 if (fileSize < ZIP_EOCD_REC_MIN_SIZE) { 68 return null; 69 } 70 71 // Optimization: 99.99% of APKs have a zero-length comment field in the EoCD record and thus 72 // the EoCD record offset is known in advance. Try that offset first to avoid unnecessarily 73 // reading more data. 74 Pair<ByteBuffer, Long> result = findZipEndOfCentralDirectoryRecord(zip, 0); 75 if (result != null) { 76 return result; 77 } 78 79 // EoCD does not start where we expected it to. Perhaps it contains a non-empty comment 80 // field. Expand the search. The maximum size of the comment field in EoCD is 65535 because 81 // the comment length field is an unsigned 16-bit number. 82 return findZipEndOfCentralDirectoryRecord(zip, UINT16_MAX_VALUE); 83 } 84 85 /** 86 * Returns the ZIP End of Central Directory record of the provided ZIP file. 87 * 88 * @param maxCommentSize maximum accepted size (in bytes) of EoCD comment field. The permitted 89 * value is from 0 to 65535 inclusive. The smaller the value, the faster this method 90 * locates the record, provided its comment field is no longer than this value. 91 * 92 * @return contents of the ZIP End of Central Directory record and the record's offset in the 93 * file or {@code null} if the file does not contain the record. 94 * 95 * @throws IOException if an I/O error occurs while reading the file. 96 */ findZipEndOfCentralDirectoryRecord( RandomAccessFile zip, int maxCommentSize)97 private static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord( 98 RandomAccessFile zip, int maxCommentSize) throws IOException { 99 // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. 100 // The record can be identified by its 4-byte signature/magic which is located at the very 101 // beginning of the record. A complication is that the record is variable-length because of 102 // the comment field. 103 // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from 104 // end of the buffer for the EOCD record signature. Whenever we find a signature, we check 105 // the candidate record's comment length is such that the remainder of the record takes up 106 // exactly the remaining bytes in the buffer. The search is bounded because the maximum 107 // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number. 108 109 if ((maxCommentSize < 0) || (maxCommentSize > UINT16_MAX_VALUE)) { 110 throw new IllegalArgumentException("maxCommentSize: " + maxCommentSize); 111 } 112 113 long fileSize = zip.length(); 114 if (fileSize < ZIP_EOCD_REC_MIN_SIZE) { 115 // No space for EoCD record in the file. 116 return null; 117 } 118 // Lower maxCommentSize if the file is too small. 119 maxCommentSize = (int) Math.min(maxCommentSize, fileSize - ZIP_EOCD_REC_MIN_SIZE); 120 121 ByteBuffer buf = ByteBuffer.allocate(ZIP_EOCD_REC_MIN_SIZE + maxCommentSize); 122 buf.order(ByteOrder.LITTLE_ENDIAN); 123 long bufOffsetInFile = fileSize - buf.capacity(); 124 zip.seek(bufOffsetInFile); 125 zip.readFully(buf.array(), buf.arrayOffset(), buf.capacity()); 126 int eocdOffsetInBuf = findZipEndOfCentralDirectoryRecord(buf); 127 if (eocdOffsetInBuf == -1) { 128 // No EoCD record found in the buffer 129 return null; 130 } 131 // EoCD found 132 buf.position(eocdOffsetInBuf); 133 ByteBuffer eocd = buf.slice(); 134 eocd.order(ByteOrder.LITTLE_ENDIAN); 135 return Pair.create(eocd, bufOffsetInFile + eocdOffsetInBuf); 136 } 137 138 /** 139 * Returns the position at which ZIP End of Central Directory record starts in the provided 140 * buffer or {@code -1} if the record is not present. 141 * 142 * <p>NOTE: Byte order of {@code zipContents} must be little-endian. 143 */ findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents)144 private static int findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents) { 145 assertByteOrderLittleEndian(zipContents); 146 147 // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. 148 // The record can be identified by its 4-byte signature/magic which is located at the very 149 // beginning of the record. A complication is that the record is variable-length because of 150 // the comment field. 151 // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from 152 // end of the buffer for the EOCD record signature. Whenever we find a signature, we check 153 // the candidate record's comment length is such that the remainder of the record takes up 154 // exactly the remaining bytes in the buffer. The search is bounded because the maximum 155 // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number. 156 157 int archiveSize = zipContents.capacity(); 158 if (archiveSize < ZIP_EOCD_REC_MIN_SIZE) { 159 return -1; 160 } 161 int maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE); 162 int eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE; 163 for (int expectedCommentLength = 0; expectedCommentLength <= maxCommentLength; 164 expectedCommentLength++) { 165 int eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength; 166 if (zipContents.getInt(eocdStartPos) == ZIP_EOCD_REC_SIG) { 167 int actualCommentLength = 168 getUnsignedInt16( 169 zipContents, eocdStartPos + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET); 170 if (actualCommentLength == expectedCommentLength) { 171 return eocdStartPos; 172 } 173 } 174 } 175 176 return -1; 177 } 178 179 /** 180 * Returns {@code true} if the provided file contains a ZIP64 End of Central Directory 181 * Locator. 182 * 183 * @param zipEndOfCentralDirectoryPosition offset of the ZIP End of Central Directory record 184 * in the file. 185 * 186 * @throws IOException if an I/O error occurs while reading the file. 187 */ isZip64EndOfCentralDirectoryLocatorPresent( RandomAccessFile zip, long zipEndOfCentralDirectoryPosition)188 public static final boolean isZip64EndOfCentralDirectoryLocatorPresent( 189 RandomAccessFile zip, long zipEndOfCentralDirectoryPosition) throws IOException { 190 191 // ZIP64 End of Central Directory Locator immediately precedes the ZIP End of Central 192 // Directory Record. 193 long locatorPosition = zipEndOfCentralDirectoryPosition - ZIP64_EOCD_LOCATOR_SIZE; 194 if (locatorPosition < 0) { 195 return false; 196 } 197 198 zip.seek(locatorPosition); 199 // RandomAccessFile.readInt assumes big-endian byte order, but ZIP format uses 200 // little-endian. 201 return zip.readInt() == ZIP64_EOCD_LOCATOR_SIG_REVERSE_BYTE_ORDER; 202 } 203 204 /** 205 * Returns the offset of the start of the ZIP Central Directory in the archive. 206 * 207 * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. 208 */ getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory)209 public static long getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory) { 210 assertByteOrderLittleEndian(zipEndOfCentralDirectory); 211 return getUnsignedInt32( 212 zipEndOfCentralDirectory, 213 zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET); 214 } 215 216 /** 217 * Sets the offset of the start of the ZIP Central Directory in the archive. 218 * 219 * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. 220 */ setZipEocdCentralDirectoryOffset( ByteBuffer zipEndOfCentralDirectory, long offset)221 public static void setZipEocdCentralDirectoryOffset( 222 ByteBuffer zipEndOfCentralDirectory, long offset) { 223 assertByteOrderLittleEndian(zipEndOfCentralDirectory); 224 setUnsignedInt32( 225 zipEndOfCentralDirectory, 226 zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET, 227 offset); 228 } 229 230 /** 231 * Returns the size (in bytes) of the ZIP Central Directory. 232 * 233 * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. 234 */ getZipEocdCentralDirectorySizeBytes(ByteBuffer zipEndOfCentralDirectory)235 public static long getZipEocdCentralDirectorySizeBytes(ByteBuffer zipEndOfCentralDirectory) { 236 assertByteOrderLittleEndian(zipEndOfCentralDirectory); 237 return getUnsignedInt32( 238 zipEndOfCentralDirectory, 239 zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET); 240 } 241 assertByteOrderLittleEndian(ByteBuffer buffer)242 private 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 private static int getUnsignedInt16(ByteBuffer buffer, int offset) { 249 return buffer.getShort(offset) & 0xffff; 250 } 251 getUnsignedInt32(ByteBuffer buffer, int offset)252 private static long getUnsignedInt32(ByteBuffer buffer, int offset) { 253 return buffer.getInt(offset) & 0xffffffffL; 254 } 255 setUnsignedInt32(ByteBuffer buffer, int offset, long value)256 private static void setUnsignedInt32(ByteBuffer buffer, int offset, long value) { 257 if ((value < 0) || (value > 0xffffffffL)) { 258 throw new IllegalArgumentException("uint32 value of out range: " + value); 259 } 260 buffer.putInt(buffer.position() + offset, (int) value); 261 } 262 } 263