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 18 19 import java.io.FileNotFoundException 20 import java.util.zip.Inflater 21 import okio.Path.Companion.toPath 22 import okio.internal.COMPRESSION_METHOD_STORED 23 import okio.internal.FixedLengthSource 24 import okio.internal.ZipEntry 25 import okio.internal.readLocalHeader 26 import okio.internal.skipLocalHeader 27 28 /** 29 * Read only access to a [zip file][zip_format] and common [extra fields][extra_fields]. 30 * 31 * Zip Timestamps 32 * -------------- 33 * 34 * The base zip format tracks the [last modified timestamp][FileMetadata.lastModifiedAtMillis]. It 35 * does not track [created timestamps][FileMetadata.createdAtMillis] or [last accessed 36 * timestamps][FileMetadata.lastAccessedAtMillis]. This format has limitations: 37 * 38 * * Timestamps are 16-bit values stored with 2-second precision. Some zip encoders (WinZip, PKZIP) 39 * round up to the nearest 2 seconds; other encoders (Java) round down. 40 * 41 * * Timestamps before 1980-01-01 cannot be represented. They cannot represent dates after 42 * 2107-12-31. 43 * 44 * * Timestamps are stored in local time with no time zone offset. If the time zone offset changes 45 * – due to daylight savings time or the zip file being sent to another time zone – file times 46 * will be incorrect. The file time will be shifted by the difference in time zone offsets 47 * between the encoder and decoder. 48 * 49 * The zip format has optional extensions for timestamps. 50 * 51 * * UNIX timestamps (0x000d) support both last-access time and last modification time. These 52 * timestamps are stored with 1-second precision using UTC. 53 * 54 * * NTFS timestamps (0x000a) support creation time, last access time, and last modified time. 55 * These timestamps are stored with 100-millisecond precision using UTC. 56 * 57 * * Extended timestamps (0x5455) are stored as signed 32-bit timestamps with 1-second precision. 58 * These cannot express dates beyond 2038-01-19. 59 * 60 * This class currently supports base timestamps and extended timestamps. 61 * 62 * [zip_format]: https://pkware.cachefly.net/webdocs/APPNOTE/APPNOTE_6.2.0.txt 63 * [extra_fields]: https://opensource.apple.com/source/zip/zip-6/unzip/unzip/proginfo/extra.fld 64 */ 65 internal class ZipFileSystem internal constructor( 66 private val zipPath: Path, 67 private val fileSystem: FileSystem, 68 private val entries: Map<Path, ZipEntry>, 69 private val comment: String?, 70 ) : FileSystem() { 71 override fun canonicalize(path: Path): Path { 72 val canonical = canonicalizeInternal(path) 73 if (canonical !in entries) { 74 throw FileNotFoundException("$path") 75 } 76 return canonical 77 } 78 79 /** Don't throw [FileNotFoundException] if the path doesn't identify a file. */ 80 private fun canonicalizeInternal(path: Path): Path { 81 return ROOT.resolve(path, normalize = true) 82 } 83 84 override fun metadataOrNull(path: Path): FileMetadata? { 85 val canonicalPath = canonicalizeInternal(path) 86 val entry = entries[canonicalPath] ?: return null 87 88 val basicMetadata = FileMetadata( 89 isRegularFile = !entry.isDirectory, 90 isDirectory = entry.isDirectory, 91 symlinkTarget = null, 92 size = if (entry.isDirectory) null else entry.size, 93 createdAtMillis = null, 94 lastModifiedAtMillis = entry.lastModifiedAtMillis, 95 lastAccessedAtMillis = null, 96 ) 97 98 if (entry.offset == -1L) { 99 return basicMetadata 100 } 101 102 return fileSystem.openReadOnly(zipPath).use { fileHandle -> 103 return@use fileHandle.source(entry.offset).buffer().use { source -> 104 source.readLocalHeader(basicMetadata) 105 } 106 } 107 } 108 109 override fun openReadOnly(file: Path): FileHandle { 110 throw UnsupportedOperationException("not implemented yet!") 111 } 112 113 override fun openReadWrite(file: Path, mustCreate: Boolean, mustExist: Boolean): FileHandle { 114 throw IOException("zip entries are not writable") 115 } 116 117 override fun list(dir: Path): List<Path> = list(dir, throwOnFailure = true)!! 118 119 override fun listOrNull(dir: Path): List<Path>? = list(dir, throwOnFailure = false) 120 121 private fun list(dir: Path, throwOnFailure: Boolean): List<Path>? { 122 val canonicalDir = canonicalizeInternal(dir) 123 val entry = entries[canonicalDir] 124 ?: if (throwOnFailure) throw IOException("not a directory: $dir") else return null 125 return entry.children.toList() 126 } 127 128 @Throws(IOException::class) 129 override fun source(file: Path): Source { 130 val canonicalPath = canonicalizeInternal(file) 131 val entry = entries[canonicalPath] ?: throw FileNotFoundException("no such file: $file") 132 val source = fileSystem.openReadOnly(zipPath).use { fileHandle -> 133 fileHandle.source(entry.offset).buffer() 134 } 135 source.skipLocalHeader() 136 137 return when (entry.compressionMethod) { 138 COMPRESSION_METHOD_STORED -> { 139 FixedLengthSource(source, entry.size, truncate = true) 140 } 141 else -> { 142 val inflaterSource = InflaterSource( 143 FixedLengthSource(source, entry.compressedSize, truncate = true), 144 Inflater(true), 145 ) 146 FixedLengthSource(inflaterSource, entry.size, truncate = false) 147 } 148 } 149 } 150 151 override fun sink(file: Path, mustCreate: Boolean): Sink { 152 throw IOException("zip file systems are read-only") 153 } 154 155 override fun appendingSink(file: Path, mustExist: Boolean): Sink { 156 throw IOException("zip file systems are read-only") 157 } 158 159 override fun createDirectory(dir: Path, mustCreate: Boolean): Unit = 160 throw IOException("zip file systems are read-only") 161 162 override fun atomicMove(source: Path, target: Path): Unit = 163 throw IOException("zip file systems are read-only") 164 165 override fun delete(path: Path, mustExist: Boolean): Unit = 166 throw IOException("zip file systems are read-only") 167 168 override fun createSymlink(source: Path, target: Path): Unit = 169 throw IOException("zip file systems are read-only") 170 171 private companion object { 172 val ROOT = "/".toPath() 173 } 174 } 175