1 /*
<lambda>null2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package okio.internal
18
19 import java.util.Calendar
20 import java.util.GregorianCalendar
21 import okio.BufferedSource
22 import okio.FileMetadata
23 import okio.FileSystem
24 import okio.IOException
25 import okio.Path
26 import okio.Path.Companion.toPath
27 import okio.ZipFileSystem
28 import okio.buffer
29
30 private const val LOCAL_FILE_HEADER_SIGNATURE = 0x4034b50
31 private const val CENTRAL_FILE_HEADER_SIGNATURE = 0x2014b50
32 private const val END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x6054b50
33 private const val ZIP64_LOCATOR_SIGNATURE = 0x07064b50
34 private const val ZIP64_EOCD_RECORD_SIGNATURE = 0x06064b50
35
36 internal const val COMPRESSION_METHOD_DEFLATED = 8
37 internal const val COMPRESSION_METHOD_STORED = 0
38
39 /** General Purpose Bit Flags, Bit 0. Set if the file is encrypted. */
40 private const val BIT_FLAG_ENCRYPTED = 1 shl 0
41
42 /**
43 * General purpose bit flags that this implementation handles. Strict enforcement of additional
44 * flags may break legitimate use cases.
45 */
46 private const val BIT_FLAG_UNSUPPORTED_MASK = BIT_FLAG_ENCRYPTED
47
48 /** Max size of entries and archives without zip64. */
49 private const val MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE = 0xffffffffL
50
51 private const val HEADER_ID_ZIP64_EXTENDED_INFO = 0x1
52 private const val HEADER_ID_EXTENDED_TIMESTAMP = 0x5455
53
54 /**
55 * Opens the file at [zipPath] for use as a file system. This uses UTF-8 to comments and names in
56 * the zip file.
57 *
58 * @param predicate a function that returns false for entries that should be omitted from the file
59 * system.
60 */
61 @Throws(IOException::class)
62 internal fun openZip(
63 zipPath: Path,
64 fileSystem: FileSystem,
65 predicate: (ZipEntry) -> Boolean = { true },
66 ): ZipFileSystem {
fileHandlenull67 fileSystem.openReadOnly(zipPath).use { fileHandle ->
68 // Scan backwards from the end of the file looking for the END_OF_CENTRAL_DIRECTORY_SIGNATURE.
69 // If this file has no comment we'll see it on the first attempt; otherwise we have to go
70 // backwards byte-by-byte until we reach it. (The number of bytes scanned will equal the comment
71 // size).
72 var scanOffset = fileHandle.size() - 22 // end of central directory record size is 22 bytes.
73 if (scanOffset < 0L) {
74 throw IOException("not a zip: size=${fileHandle.size()}")
75 }
76 val stopOffset = maxOf(scanOffset - 65_536L, 0L)
77 val eocdOffset: Long
78 var record: EocdRecord
79 val comment: String
80 while (true) {
81 val source = fileHandle.source(scanOffset).buffer()
82 try {
83 if (source.readIntLe() == END_OF_CENTRAL_DIRECTORY_SIGNATURE) {
84 eocdOffset = scanOffset
85 record = source.readEocdRecord()
86 comment = source.readUtf8(record.commentByteCount.toLong())
87 break
88 }
89 } finally {
90 source.close()
91 }
92
93 scanOffset--
94 if (scanOffset < stopOffset) {
95 throw IOException("not a zip: end of central directory signature not found")
96 }
97 }
98
99 // If this is a zip64, read a zip64 central directory record.
100 val zip64LocatorOffset = eocdOffset - 20 // zip64 end of central directory locator is 20 bytes.
101 if (zip64LocatorOffset > 0L) {
102 fileHandle.source(zip64LocatorOffset).buffer().use { zip64LocatorSource ->
103 if (zip64LocatorSource.readIntLe() == ZIP64_LOCATOR_SIGNATURE) {
104 val diskWithCentralDir = zip64LocatorSource.readIntLe()
105 val zip64EocdRecordOffset = zip64LocatorSource.readLongLe()
106 val numDisks = zip64LocatorSource.readIntLe()
107 if (numDisks != 1 || diskWithCentralDir != 0) {
108 throw IOException("unsupported zip: spanned")
109 }
110 fileHandle.source(zip64EocdRecordOffset).buffer().use { zip64EocdSource ->
111 val zip64EocdSignature = zip64EocdSource.readIntLe()
112 if (zip64EocdSignature != ZIP64_EOCD_RECORD_SIGNATURE) {
113 throw IOException(
114 "bad zip: expected ${ZIP64_EOCD_RECORD_SIGNATURE.hex} " +
115 "but was ${zip64EocdSignature.hex}",
116 )
117 }
118 record = zip64EocdSource.readZip64EocdRecord(record)
119 }
120 }
121 }
122 }
123
124 // Seek to the first central directory entry and read all of the entries.
125 val entries = mutableListOf<ZipEntry>()
126 fileHandle.source(record.centralDirectoryOffset).buffer().use { source ->
127 for (i in 0 until record.entryCount) {
128 val entry = source.readEntry()
129 if (entry.offset >= record.centralDirectoryOffset) {
130 throw IOException("bad zip: local file header offset >= central directory offset")
131 }
132 if (predicate(entry)) {
133 entries += entry
134 }
135 }
136 }
137
138 // Organize the entries into a tree.
139 val index = buildIndex(entries)
140
141 return ZipFileSystem(zipPath, fileSystem, index, comment)
142 }
143 }
144
145 /**
146 * Returns a map containing all of [entries], plus parent entries required so that all entries
147 * (other than the file system root `/`) have a parent.
148 */
buildIndexnull149 private fun buildIndex(entries: List<ZipEntry>): Map<Path, ZipEntry> {
150 val root = "/".toPath()
151 val result = mutableMapOf(
152 root to ZipEntry(canonicalPath = root, isDirectory = true),
153 )
154
155 // Iterate in sorted order so each path is preceded by its parent.
156 for (entry in entries.sortedBy { it.canonicalPath }) {
157 // Note that this may clobber an existing element in the map. For consistency with java.util.zip
158 // and java.nio.file.FileSystem, this prefers the last-encountered element.
159 val replaced = result.put(entry.canonicalPath, entry)
160 if (replaced != null) continue
161
162 // Make sure this parent directories exist all the way up to the file system root.
163 var child = entry
164 while (true) {
165 val parentPath = child.canonicalPath.parent ?: break // child is '/'.
166 var parentEntry = result[parentPath]
167
168 // We've found a parent that already exists! Add the child; we're done.
169 if (parentEntry != null) {
170 parentEntry.children += child.canonicalPath
171 break
172 }
173
174 // A parent is missing! Synthesize one.
175 parentEntry = ZipEntry(
176 canonicalPath = parentPath,
177 isDirectory = true,
178 )
179 result[parentPath] = parentEntry
180 parentEntry.children += child.canonicalPath
181 child = parentEntry
182 }
183 }
184
185 return result
186 }
187
188 /** When this returns, [this] will be positioned at the start of the next entry. */
189 @Throws(IOException::class)
readEntrynull190 internal fun BufferedSource.readEntry(): ZipEntry {
191 val signature = readIntLe()
192 if (signature != CENTRAL_FILE_HEADER_SIGNATURE) {
193 throw IOException(
194 "bad zip: expected ${CENTRAL_FILE_HEADER_SIGNATURE.hex} but was ${signature.hex}",
195 )
196 }
197
198 skip(4) // version made by (2) + version to extract (2).
199 val bitFlag = readShortLe().toInt() and 0xffff
200 if (bitFlag and BIT_FLAG_UNSUPPORTED_MASK != 0) {
201 throw IOException("unsupported zip: general purpose bit flag=${bitFlag.hex}")
202 }
203
204 val compressionMethod = readShortLe().toInt() and 0xffff
205 val time = readShortLe().toInt() and 0xffff
206 val date = readShortLe().toInt() and 0xffff
207 // TODO(jwilson): decode NTFS and UNIX extra metadata to return better timestamps.
208 val lastModifiedAtMillis = dosDateTimeToEpochMillis(date, time)
209
210 // These are 32-bit values in the file, but 64-bit fields in this object.
211 val crc = readIntLe().toLong() and 0xffffffffL
212 var compressedSize = readIntLe().toLong() and 0xffffffffL
213 var size = readIntLe().toLong() and 0xffffffffL
214 val nameSize = readShortLe().toInt() and 0xffff
215 val extraSize = readShortLe().toInt() and 0xffff
216 val commentByteCount = readShortLe().toInt() and 0xffff
217
218 skip(8) // disk number start (2) + internal file attributes (2) + external file attributes (4).
219 var offset = readIntLe().toLong() and 0xffffffffL
220 val name = readUtf8(nameSize.toLong())
221 if ('\u0000' in name) throw IOException("bad zip: filename contains 0x00")
222
223 val requiredZip64ExtraSize = run {
224 var result = 0L
225 if (size == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) result += 8
226 if (compressedSize == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) result += 8
227 if (offset == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) result += 8
228 return@run result
229 }
230
231 var hasZip64Extra = false
232 readExtra(extraSize) { headerId, dataSize ->
233 when (headerId) {
234 HEADER_ID_ZIP64_EXTENDED_INFO -> {
235 if (hasZip64Extra) {
236 throw IOException("bad zip: zip64 extra repeated")
237 }
238 hasZip64Extra = true
239
240 if (dataSize < requiredZip64ExtraSize) {
241 throw IOException("bad zip: zip64 extra too short")
242 }
243
244 // Read each field if it has a sentinel value in the regular header.
245 size = if (size == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) readLongLe() else size
246 compressedSize = if (compressedSize == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) readLongLe() else 0L
247 offset = if (offset == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) readLongLe() else 0L
248 }
249 }
250 }
251
252 if (requiredZip64ExtraSize > 0L && !hasZip64Extra) {
253 throw IOException("bad zip: zip64 extra required but absent")
254 }
255
256 val comment = readUtf8(commentByteCount.toLong())
257 val canonicalPath = "/".toPath() / name
258 val isDirectory = name.endsWith("/")
259
260 return ZipEntry(
261 canonicalPath = canonicalPath,
262 isDirectory = isDirectory,
263 comment = comment,
264 crc = crc,
265 compressedSize = compressedSize,
266 size = size,
267 compressionMethod = compressionMethod,
268 lastModifiedAtMillis = lastModifiedAtMillis,
269 offset = offset,
270 )
271 }
272
273 @Throws(IOException::class)
readEocdRecordnull274 private fun BufferedSource.readEocdRecord(): EocdRecord {
275 val diskNumber = readShortLe().toInt() and 0xffff
276 val diskWithCentralDir = readShortLe().toInt() and 0xffff
277 val entryCount = (readShortLe().toInt() and 0xffff).toLong()
278 val totalEntryCount = (readShortLe().toInt() and 0xffff).toLong()
279 if (entryCount != totalEntryCount || diskNumber != 0 || diskWithCentralDir != 0) {
280 throw IOException("unsupported zip: spanned")
281 }
282 skip(4) // central directory size.
283 val centralDirectoryOffset = readIntLe().toLong() and 0xffffffffL
284 val commentByteCount = readShortLe().toInt() and 0xffff
285
286 return EocdRecord(
287 entryCount = entryCount,
288 centralDirectoryOffset = centralDirectoryOffset,
289 commentByteCount = commentByteCount,
290 )
291 }
292
293 @Throws(IOException::class)
readZip64EocdRecordnull294 private fun BufferedSource.readZip64EocdRecord(regularRecord: EocdRecord): EocdRecord {
295 skip(12) // size of central directory record (8) + version made by (2) + version to extract (2).
296 val diskNumber = readIntLe()
297 val diskWithCentralDirStart = readIntLe()
298 val entryCount = readLongLe()
299 val totalEntryCount = readLongLe()
300 if (entryCount != totalEntryCount || diskNumber != 0 || diskWithCentralDirStart != 0) {
301 throw IOException("unsupported zip: spanned")
302 }
303 skip(8) // central directory size.
304 val centralDirectoryOffset = readLongLe()
305
306 return EocdRecord(
307 entryCount = entryCount,
308 centralDirectoryOffset = centralDirectoryOffset,
309 commentByteCount = regularRecord.commentByteCount,
310 )
311 }
312
313 /**
314 * Read a sequence of 0 or more extra fields. Each field has this structure:
315 *
316 * * 2-byte header ID
317 * * 2-byte data size
318 * * variable-byte data value
319 *
320 * This reads each extra field and calls [block] for each. The parameters are the header ID and
321 * data size. It is an error for [block] to process more bytes than the data size.
322 */
BufferedSourcenull323 private fun BufferedSource.readExtra(extraSize: Int, block: (Int, Long) -> Unit) {
324 var remaining = extraSize.toLong()
325 while (remaining != 0L) {
326 if (remaining < 4) {
327 throw IOException("bad zip: truncated header in extra field")
328 }
329 val headerId = readShortLe().toInt() and 0xffff
330 val dataSize = readShortLe().toLong() and 0xffff
331 remaining -= 4
332 if (remaining < dataSize) {
333 throw IOException("bad zip: truncated value in extra field")
334 }
335 require(dataSize)
336 val sizeBefore = buffer.size
337 block(headerId, dataSize)
338 val fieldRemaining = dataSize + buffer.size - sizeBefore
339 when {
340 fieldRemaining < 0 -> {
341 throw IOException("unsupported zip: too many bytes processed for $headerId")
342 }
343 fieldRemaining > 0 -> {
344 buffer.skip(fieldRemaining)
345 }
346 }
347 remaining -= dataSize
348 }
349 }
350
skipLocalHeadernull351 internal fun BufferedSource.skipLocalHeader() {
352 readOrSkipLocalHeader(null)
353 }
354
readLocalHeadernull355 internal fun BufferedSource.readLocalHeader(basicMetadata: FileMetadata): FileMetadata {
356 return readOrSkipLocalHeader(basicMetadata)!!
357 }
358
359 /**
360 * If [basicMetadata] is null this will return null. Otherwise it will return a new header which
361 * updates [basicMetadata] with information from the local header.
362 */
readOrSkipLocalHeadernull363 private fun BufferedSource.readOrSkipLocalHeader(basicMetadata: FileMetadata?): FileMetadata? {
364 var lastModifiedAtMillis = basicMetadata?.lastModifiedAtMillis
365 var lastAccessedAtMillis: Long? = null
366 var createdAtMillis: Long? = null
367
368 val signature = readIntLe()
369 if (signature != LOCAL_FILE_HEADER_SIGNATURE) {
370 throw IOException(
371 "bad zip: expected ${LOCAL_FILE_HEADER_SIGNATURE.hex} but was ${signature.hex}",
372 )
373 }
374 skip(2) // version to extract.
375 val bitFlag = readShortLe().toInt() and 0xffff
376 if (bitFlag and BIT_FLAG_UNSUPPORTED_MASK != 0) {
377 throw IOException("unsupported zip: general purpose bit flag=${bitFlag.hex}")
378 }
379 skip(18) // compression method (2) + time+date (4) + crc32 (4) + compressed size (4) + size (4).
380 val fileNameLength = readShortLe().toLong() and 0xffff
381 val extraSize = readShortLe().toInt() and 0xffff
382 skip(fileNameLength)
383
384 if (basicMetadata == null) {
385 skip(extraSize.toLong())
386 return null
387 }
388
389 readExtra(extraSize) { headerId, dataSize ->
390 when (headerId) {
391 HEADER_ID_EXTENDED_TIMESTAMP -> {
392 if (dataSize < 1) {
393 throw IOException("bad zip: extended timestamp extra too short")
394 }
395 val flags = readByte().toInt() and 0xff
396
397 val hasLastModifiedAtMillis = (flags and 0x1) == 0x1
398 val hasLastAccessedAtMillis = (flags and 0x2) == 0x2
399 val hasCreatedAtMillis = (flags and 0x4) == 0x4
400 val requiredSize = run {
401 var result = 1L
402 if (hasLastModifiedAtMillis) result += 4L
403 if (hasLastAccessedAtMillis) result += 4L
404 if (hasCreatedAtMillis) result += 4L
405 return@run result
406 }
407 if (dataSize < requiredSize) {
408 throw IOException("bad zip: extended timestamp extra too short")
409 }
410
411 if (hasLastModifiedAtMillis) lastModifiedAtMillis = readIntLe() * 1000L
412 if (hasLastAccessedAtMillis) lastAccessedAtMillis = readIntLe() * 1000L
413 if (hasCreatedAtMillis) createdAtMillis = readIntLe() * 1000L
414 }
415 }
416 }
417
418 return FileMetadata(
419 isRegularFile = basicMetadata.isRegularFile,
420 isDirectory = basicMetadata.isDirectory,
421 symlinkTarget = null,
422 size = basicMetadata.size,
423 createdAtMillis = createdAtMillis,
424 lastModifiedAtMillis = lastModifiedAtMillis,
425 lastAccessedAtMillis = lastAccessedAtMillis,
426 )
427 }
428
429 /**
430 * Converts a 32-bit DOS date+time to milliseconds since epoch. Note that this function interprets
431 * a value with no time zone as a value with the local time zone.
432 */
dosDateTimeToEpochMillisnull433 private fun dosDateTimeToEpochMillis(date: Int, time: Int): Long? {
434 if (time == -1) {
435 return null
436 }
437
438 // Note that this inherits the local time zone.
439 val cal = GregorianCalendar()
440 cal.set(Calendar.MILLISECOND, 0)
441 val year = 1980 + (date shr 9 and 0x7f)
442 val month = date shr 5 and 0xf
443 val day = date and 0x1f
444 val hour = time shr 11 and 0x1f
445 val minute = time shr 5 and 0x3f
446 val second = time and 0x1f shl 1
447 cal.set(year, month - 1, day, hour, minute, second)
448 return cal.time.time
449 }
450
451 private class EocdRecord(
452 val entryCount: Long,
453 val centralDirectoryOffset: Long,
454 val commentByteCount: Int,
455 )
456
457 private val Int.hex: String
458 get() = "0x${this.toString(16)}"
459