xref: /aosp_15_r20/external/okio/okio/src/jvmMain/kotlin/okio/ZipFileSystem.kt (revision f9742813c14b702d71392179818a9e591da8620c)
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