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
17package android.util.apk;
18
19import android.util.Pair;
20
21import java.io.IOException;
22import java.io.RandomAccessFile;
23import java.nio.ByteBuffer;
24import 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 */
32abstract 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