blob: fa5477e4190bc2ed78b0fd13aad0e0f067c6026f [file] [log] [blame]
Alex Klyubine4157182016-01-05 13:27:05 -08001/*
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
Alex Klyubin0722ffc2016-03-18 16:09:06 -070019import android.util.Pair;
20
21import java.io.IOException;
22import java.io.RandomAccessFile;
Alex Klyubine4157182016-01-05 13:27:05 -080023import java.nio.ByteBuffer;
24import java.nio.ByteOrder;
25
26/**
27 * Assorted ZIP format helpers.
28 *
Alex Klyubin0722ffc2016-03-18 16:09:06 -070029 * <p>NOTE: Most helper methods operating on {@code ByteBuffer} instances expect that the byte
Alex Klyubine4157182016-01-05 13:27:05 -080030 * 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;
Alex Klyubin0722ffc2016-03-18 16:09:06 -070042 private static final int ZIP64_EOCD_LOCATOR_SIG_REVERSE_BYTE_ORDER = 0x504b0607;
Alex Klyubine4157182016-01-05 13:27:05 -080043
Alex Klyubin0722ffc2016-03-18 16:09:06 -070044 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 }
Alex Klyubine4157182016-01-05 13:27:05 -0800137
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 */
Alex Klyubin0722ffc2016-03-18 16:09:06 -0700144 private static int findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents) {
Alex Klyubine4157182016-01-05 13:27:05 -0800145 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
Alex Klyubin0722ffc2016-03-18 16:09:06 -0700155 // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
Alex Klyubine4157182016-01-05 13:27:05 -0800156
157 int archiveSize = zipContents.capacity();
158 if (archiveSize < ZIP_EOCD_REC_MIN_SIZE) {
Alex Klyubine4157182016-01-05 13:27:05 -0800159 return -1;
160 }
Alex Klyubin0722ffc2016-03-18 16:09:06 -0700161 int maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE);
Alex Klyubine4157182016-01-05 13:27:05 -0800162 int eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE;
Alex Klyubin96946572016-12-19 10:52:52 -0800163 for (int expectedCommentLength = 0; expectedCommentLength <= maxCommentLength;
Alex Klyubine4157182016-01-05 13:27:05 -0800164 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 /**
Alex Klyubin0722ffc2016-03-18 16:09:06 -0700180 * Returns {@code true} if the provided file contains a ZIP64 End of Central Directory
Alex Klyubine4157182016-01-05 13:27:05 -0800181 * Locator.
182 *
Alex Klyubin0722ffc2016-03-18 16:09:06 -0700183 * @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.
Alex Klyubine4157182016-01-05 13:27:05 -0800187 */
188 public static final boolean isZip64EndOfCentralDirectoryLocatorPresent(
Alex Klyubin0722ffc2016-03-18 16:09:06 -0700189 RandomAccessFile zip, long zipEndOfCentralDirectoryPosition) throws IOException {
Alex Klyubine4157182016-01-05 13:27:05 -0800190
191 // ZIP64 End of Central Directory Locator immediately precedes the ZIP End of Central
192 // Directory Record.
Alex Klyubin0722ffc2016-03-18 16:09:06 -0700193 long locatorPosition = zipEndOfCentralDirectoryPosition - ZIP64_EOCD_LOCATOR_SIZE;
Alex Klyubine4157182016-01-05 13:27:05 -0800194 if (locatorPosition < 0) {
195 return false;
196 }
197
Alex Klyubin0722ffc2016-03-18 16:09:06 -0700198 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;
Alex Klyubine4157182016-01-05 13:27:05 -0800202 }
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}