• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.apk.ApkFormatException;
20 import com.android.apksig.internal.util.Pair;
21 import com.android.apksig.util.DataSource;
22 import com.android.apksig.zip.ZipFormatException;
23 import com.android.apksig.zip.ZipSections;
24 
25 import java.io.ByteArrayOutputStream;
26 import java.io.IOException;
27 import java.nio.ByteBuffer;
28 import java.nio.ByteOrder;
29 import java.util.ArrayList;
30 import java.util.List;
31 import java.util.zip.CRC32;
32 import java.util.zip.Deflater;
33 
34 /**
35  * Assorted ZIP format helpers.
36  *
37  * <p>NOTE: Most helper methods operating on {@code ByteBuffer} instances expect that the byte
38  * order of these buffers is little-endian.
39  */
40 public abstract class ZipUtils {
ZipUtils()41     private ZipUtils() {}
42 
43     public static final short COMPRESSION_METHOD_STORED = 0;
44     public static final short COMPRESSION_METHOD_DEFLATED = 8;
45 
46     public static final short GP_FLAG_DATA_DESCRIPTOR_USED = 0x08;
47     public static final short GP_FLAG_EFS = 0x0800;
48 
49     private static final int ZIP_EOCD_REC_MIN_SIZE = 22;
50     private static final int ZIP_EOCD_REC_SIG = 0x06054b50;
51     private static final int ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET = 10;
52     private static final int ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET = 12;
53     private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16;
54     private static final int ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20;
55 
56     private static final int UINT16_MAX_VALUE = 0xffff;
57 
58     /**
59      * Sets the offset of the start of the ZIP Central Directory in the archive.
60      *
61      * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
62      */
setZipEocdCentralDirectoryOffset( ByteBuffer zipEndOfCentralDirectory, long offset)63     public static void setZipEocdCentralDirectoryOffset(
64             ByteBuffer zipEndOfCentralDirectory, long offset) {
65         assertByteOrderLittleEndian(zipEndOfCentralDirectory);
66         setUnsignedInt32(
67                 zipEndOfCentralDirectory,
68                 zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET,
69                 offset);
70     }
71 
72     /**
73      * Sets the length of EOCD comment.
74      *
75      * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
76      */
updateZipEocdCommentLen(ByteBuffer zipEndOfCentralDirectory)77     public static void updateZipEocdCommentLen(ByteBuffer zipEndOfCentralDirectory) {
78         assertByteOrderLittleEndian(zipEndOfCentralDirectory);
79         int commentLen = zipEndOfCentralDirectory.remaining() - ZIP_EOCD_REC_MIN_SIZE;
80         setUnsignedInt16(
81                 zipEndOfCentralDirectory,
82                 zipEndOfCentralDirectory.position() + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET,
83                 commentLen);
84     }
85 
86     /**
87      * Returns the offset of the start of the ZIP Central Directory in the archive.
88      *
89      * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
90      */
getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory)91     public static long getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory) {
92         assertByteOrderLittleEndian(zipEndOfCentralDirectory);
93         return getUnsignedInt32(
94                 zipEndOfCentralDirectory,
95                 zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET);
96     }
97 
98     /**
99      * Returns the size (in bytes) of the ZIP Central Directory.
100      *
101      * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
102      */
getZipEocdCentralDirectorySizeBytes(ByteBuffer zipEndOfCentralDirectory)103     public static long getZipEocdCentralDirectorySizeBytes(ByteBuffer zipEndOfCentralDirectory) {
104         assertByteOrderLittleEndian(zipEndOfCentralDirectory);
105         return getUnsignedInt32(
106                 zipEndOfCentralDirectory,
107                 zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET);
108     }
109 
110     /**
111      * Returns the total number of records in ZIP Central Directory.
112      *
113      * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
114      */
getZipEocdCentralDirectoryTotalRecordCount( ByteBuffer zipEndOfCentralDirectory)115     public static int getZipEocdCentralDirectoryTotalRecordCount(
116             ByteBuffer zipEndOfCentralDirectory) {
117         assertByteOrderLittleEndian(zipEndOfCentralDirectory);
118         return getUnsignedInt16(
119                 zipEndOfCentralDirectory,
120                 zipEndOfCentralDirectory.position()
121                         + ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET);
122     }
123 
124     /**
125      * Returns the ZIP End of Central Directory record of the provided ZIP file.
126      *
127      * @return contents of the ZIP End of Central Directory record and the record's offset in the
128      *         file or {@code null} if the file does not contain the record.
129      *
130      * @throws IOException if an I/O error occurs while reading the file.
131      */
findZipEndOfCentralDirectoryRecord(DataSource zip)132     public static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord(DataSource zip)
133             throws IOException {
134         // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
135         // The record can be identified by its 4-byte signature/magic which is located at the very
136         // beginning of the record. A complication is that the record is variable-length because of
137         // the comment field.
138         // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
139         // end of the buffer for the EOCD record signature. Whenever we find a signature, we check
140         // the candidate record's comment length is such that the remainder of the record takes up
141         // exactly the remaining bytes in the buffer. The search is bounded because the maximum
142         // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
143 
144         long fileSize = zip.size();
145         if (fileSize < ZIP_EOCD_REC_MIN_SIZE) {
146             return null;
147         }
148 
149         // Optimization: 99.99% of APKs have a zero-length comment field in the EoCD record and thus
150         // the EoCD record offset is known in advance. Try that offset first to avoid unnecessarily
151         // reading more data.
152         Pair<ByteBuffer, Long> result = findZipEndOfCentralDirectoryRecord(zip, 0);
153         if (result != null) {
154             return result;
155         }
156 
157         // EoCD does not start where we expected it to. Perhaps it contains a non-empty comment
158         // field. Expand the search. The maximum size of the comment field in EoCD is 65535 because
159         // the comment length field is an unsigned 16-bit number.
160         return findZipEndOfCentralDirectoryRecord(zip, UINT16_MAX_VALUE);
161     }
162 
163     /**
164      * Returns the ZIP End of Central Directory record of the provided ZIP file.
165      *
166      * @param maxCommentSize maximum accepted size (in bytes) of EoCD comment field. The permitted
167      *        value is from 0 to 65535 inclusive. The smaller the value, the faster this method
168      *        locates the record, provided its comment field is no longer than this value.
169      *
170      * @return contents of the ZIP End of Central Directory record and the record's offset in the
171      *         file or {@code null} if the file does not contain the record.
172      *
173      * @throws IOException if an I/O error occurs while reading the file.
174      */
findZipEndOfCentralDirectoryRecord( DataSource zip, int maxCommentSize)175     private static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord(
176             DataSource zip, int maxCommentSize) throws IOException {
177         // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
178         // The record can be identified by its 4-byte signature/magic which is located at the very
179         // beginning of the record. A complication is that the record is variable-length because of
180         // the comment field.
181         // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
182         // end of the buffer for the EOCD record signature. Whenever we find a signature, we check
183         // the candidate record's comment length is such that the remainder of the record takes up
184         // exactly the remaining bytes in the buffer. The search is bounded because the maximum
185         // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
186 
187         if ((maxCommentSize < 0) || (maxCommentSize > UINT16_MAX_VALUE)) {
188             throw new IllegalArgumentException("maxCommentSize: " + maxCommentSize);
189         }
190 
191         long fileSize = zip.size();
192         if (fileSize < ZIP_EOCD_REC_MIN_SIZE) {
193             // No space for EoCD record in the file.
194             return null;
195         }
196         // Lower maxCommentSize if the file is too small.
197         maxCommentSize = (int) Math.min(maxCommentSize, fileSize - ZIP_EOCD_REC_MIN_SIZE);
198 
199         int maxEocdSize = ZIP_EOCD_REC_MIN_SIZE + maxCommentSize;
200         long bufOffsetInFile = fileSize - maxEocdSize;
201         ByteBuffer buf = zip.getByteBuffer(bufOffsetInFile, maxEocdSize);
202         buf.order(ByteOrder.LITTLE_ENDIAN);
203         int eocdOffsetInBuf = findZipEndOfCentralDirectoryRecord(buf);
204         if (eocdOffsetInBuf == -1) {
205             // No EoCD record found in the buffer
206             return null;
207         }
208         // EoCD found
209         buf.position(eocdOffsetInBuf);
210         ByteBuffer eocd = buf.slice();
211         eocd.order(ByteOrder.LITTLE_ENDIAN);
212         return Pair.of(eocd, bufOffsetInFile + eocdOffsetInBuf);
213     }
214 
215     /**
216      * Returns the position at which ZIP End of Central Directory record starts in the provided
217      * buffer or {@code -1} if the record is not present.
218      *
219      * <p>NOTE: Byte order of {@code zipContents} must be little-endian.
220      */
findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents)221     private static int findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents) {
222         assertByteOrderLittleEndian(zipContents);
223 
224         // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
225         // The record can be identified by its 4-byte signature/magic which is located at the very
226         // beginning of the record. A complication is that the record is variable-length because of
227         // the comment field.
228         // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
229         // end of the buffer for the EOCD record signature. Whenever we find a signature, we check
230         // the candidate record's comment length is such that the remainder of the record takes up
231         // exactly the remaining bytes in the buffer. The search is bounded because the maximum
232         // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
233 
234         int archiveSize = zipContents.capacity();
235         if (archiveSize < ZIP_EOCD_REC_MIN_SIZE) {
236             return -1;
237         }
238         int maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE);
239         int eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE;
240         for (int expectedCommentLength = 0; expectedCommentLength <= maxCommentLength;
241                 expectedCommentLength++) {
242             int eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength;
243             if (zipContents.getInt(eocdStartPos) == ZIP_EOCD_REC_SIG) {
244                 int actualCommentLength =
245                         getUnsignedInt16(
246                                 zipContents, eocdStartPos + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET);
247                 if (actualCommentLength == expectedCommentLength) {
248                     return eocdStartPos;
249                 }
250             }
251         }
252 
253         return -1;
254     }
255 
assertByteOrderLittleEndian(ByteBuffer buffer)256     static void assertByteOrderLittleEndian(ByteBuffer buffer) {
257         if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
258             throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
259         }
260     }
261 
getUnsignedInt16(ByteBuffer buffer, int offset)262     public static int getUnsignedInt16(ByteBuffer buffer, int offset) {
263         return buffer.getShort(offset) & 0xffff;
264     }
265 
getUnsignedInt16(ByteBuffer buffer)266     public static int getUnsignedInt16(ByteBuffer buffer) {
267         return buffer.getShort() & 0xffff;
268     }
269 
parseZipCentralDirectory( DataSource apk, ZipSections apkSections)270     public static List<CentralDirectoryRecord> parseZipCentralDirectory(
271             DataSource apk,
272             ZipSections apkSections)
273             throws IOException, ApkFormatException {
274         // Read the ZIP Central Directory
275         long cdSizeBytes = apkSections.getZipCentralDirectorySizeBytes();
276         if (cdSizeBytes > Integer.MAX_VALUE) {
277             throw new ApkFormatException("ZIP Central Directory too large: " + cdSizeBytes);
278         }
279         long cdOffset = apkSections.getZipCentralDirectoryOffset();
280         ByteBuffer cd = apk.getByteBuffer(cdOffset, (int) cdSizeBytes);
281         cd.order(ByteOrder.LITTLE_ENDIAN);
282 
283         // Parse the ZIP Central Directory
284         int expectedCdRecordCount = apkSections.getZipCentralDirectoryRecordCount();
285         List<CentralDirectoryRecord> cdRecords = new ArrayList<>(expectedCdRecordCount);
286         for (int i = 0; i < expectedCdRecordCount; i++) {
287             CentralDirectoryRecord cdRecord;
288             int offsetInsideCd = cd.position();
289             try {
290                 cdRecord = CentralDirectoryRecord.getRecord(cd);
291             } catch (ZipFormatException e) {
292                 throw new ApkFormatException(
293                         "Malformed ZIP Central Directory record #" + (i + 1)
294                                 + " at file offset " + (cdOffset + offsetInsideCd),
295                         e);
296             }
297             String entryName = cdRecord.getName();
298             if (entryName.endsWith("/")) {
299                 // Ignore directory entries
300                 continue;
301             }
302             cdRecords.add(cdRecord);
303         }
304         // There may be more data in Central Directory, but we don't warn or throw because Android
305         // ignores unused CD data.
306 
307         return cdRecords;
308     }
309 
setUnsignedInt16(ByteBuffer buffer, int offset, int value)310     static void setUnsignedInt16(ByteBuffer buffer, int offset, int value) {
311         if ((value < 0) || (value > 0xffff)) {
312             throw new IllegalArgumentException("uint16 value of out range: " + value);
313         }
314         buffer.putShort(offset, (short) value);
315     }
316 
setUnsignedInt32(ByteBuffer buffer, int offset, long value)317     static void setUnsignedInt32(ByteBuffer buffer, int offset, long value) {
318         if ((value < 0) || (value > 0xffffffffL)) {
319             throw new IllegalArgumentException("uint32 value of out range: " + value);
320         }
321         buffer.putInt(offset, (int) value);
322     }
323 
putUnsignedInt16(ByteBuffer buffer, int value)324     public static void putUnsignedInt16(ByteBuffer buffer, int value) {
325         if ((value < 0) || (value > 0xffff)) {
326             throw new IllegalArgumentException("uint16 value of out range: " + value);
327         }
328         buffer.putShort((short) value);
329     }
330 
getUnsignedInt32(ByteBuffer buffer, int offset)331     static long getUnsignedInt32(ByteBuffer buffer, int offset) {
332         return buffer.getInt(offset) & 0xffffffffL;
333     }
334 
getUnsignedInt32(ByteBuffer buffer)335     static long getUnsignedInt32(ByteBuffer buffer) {
336         return buffer.getInt() & 0xffffffffL;
337     }
338 
putUnsignedInt32(ByteBuffer buffer, long value)339     static void putUnsignedInt32(ByteBuffer buffer, long value) {
340         if ((value < 0) || (value > 0xffffffffL)) {
341             throw new IllegalArgumentException("uint32 value of out range: " + value);
342         }
343         buffer.putInt((int) value);
344     }
345 
deflate(ByteBuffer input)346     public static DeflateResult deflate(ByteBuffer input) {
347         byte[] inputBuf;
348         int inputOffset;
349         int inputLength = input.remaining();
350         if (input.hasArray()) {
351             inputBuf = input.array();
352             inputOffset = input.arrayOffset() + input.position();
353             input.position(input.limit());
354         } else {
355             inputBuf = new byte[inputLength];
356             inputOffset = 0;
357             input.get(inputBuf);
358         }
359         CRC32 crc32 = new CRC32();
360         crc32.update(inputBuf, inputOffset, inputLength);
361         long crc32Value = crc32.getValue();
362         ByteArrayOutputStream out = new ByteArrayOutputStream();
363         Deflater deflater = new Deflater(9, true);
364         deflater.setInput(inputBuf, inputOffset, inputLength);
365         deflater.finish();
366         byte[] buf = new byte[65536];
367         while (!deflater.finished()) {
368             int chunkSize = deflater.deflate(buf);
369             out.write(buf, 0, chunkSize);
370         }
371         return new DeflateResult(inputLength, crc32Value, out.toByteArray());
372     }
373 
374     public static class DeflateResult {
375         public final int inputSizeBytes;
376         public final long inputCrc32;
377         public final byte[] output;
378 
DeflateResult(int inputSizeBytes, long inputCrc32, byte[] output)379         public DeflateResult(int inputSizeBytes, long inputCrc32, byte[] output) {
380             this.inputSizeBytes = inputSizeBytes;
381             this.inputCrc32 = inputCrc32;
382             this.output = output;
383         }
384     }
385 }