Home | History | Annotate | Download | only in apk
      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 {
     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      */
     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      */
     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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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 
    248     private static int getUnsignedInt16(ByteBuffer buffer, int offset) {
    249         return buffer.getShort(offset) & 0xffff;
    250     }
    251 
    252     private static long getUnsignedInt32(ByteBuffer buffer, int offset) {
    253         return buffer.getInt(offset) & 0xffffffffL;
    254     }
    255 
    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