Home | History | Annotate | Download | only in zip
      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 {
     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      */
     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      */
     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      */
     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      */
     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      */
    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      */
    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      */
    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 
    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 
    242     public static int getUnsignedInt16(ByteBuffer buffer, int offset) {
    243         return buffer.getShort(offset) & 0xffff;
    244     }
    245 
    246     public static int getUnsignedInt16(ByteBuffer buffer) {
    247         return buffer.getShort() & 0xffff;
    248     }
    249 
    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 
    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 
    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 
    271     static long getUnsignedInt32(ByteBuffer buffer, int offset) {
    272         return buffer.getInt(offset) & 0xffffffffL;
    273     }
    274 
    275     static long getUnsignedInt32(ByteBuffer buffer) {
    276         return buffer.getInt() & 0xffffffffL;
    277     }
    278 
    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 
    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 
    319         public DeflateResult(int inputSizeBytes, long inputCrc32, byte[] output) {
    320             this.inputSizeBytes = inputSizeBytes;
    321             this.inputCrc32 = inputCrc32;
    322             this.output = output;
    323         }
    324     }
    325 }