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