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