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