xref: /aosp_15_r20/external/okio/okio/src/jvmMain/kotlin/okio/internal/ResourceFileSystem.kt (revision f9742813c14b702d71392179818a9e591da8620c)
1 /*
<lambda>null2  * Copyright (C) 2021 Square, Inc.
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 package okio.internal
17 
18 import java.io.File
19 import java.io.IOException
20 import java.net.URI
21 import java.net.URL
22 import okio.FileHandle
23 import okio.FileMetadata
24 import okio.FileNotFoundException
25 import okio.FileSystem
26 import okio.Path
27 import okio.Path.Companion.toOkioPath
28 import okio.Path.Companion.toPath
29 import okio.Sink
30 import okio.Source
31 import okio.source
32 
33 /**
34  * A file system exposing Java classpath resources. It is equivalent to the files returned by
35  * [ClassLoader.getResource] but supports extra features like [metadataOrNull] and [list].
36  *
37  * If `.jar` files overlap, this returns an arbitrary element. For overlapping directories it unions
38  * their contents.
39  *
40  * ResourceFileSystem excludes `.class` files.
41  *
42  * This file system is read-only.
43  */
44 internal class ResourceFileSystem internal constructor(
45   private val classLoader: ClassLoader,
46   indexEagerly: Boolean,
47   private val systemFileSystem: FileSystem = SYSTEM,
48 ) : FileSystem() {
49   private val roots: List<Pair<FileSystem, Path>> by lazy { classLoader.toClasspathRoots() }
50 
51   init {
52     if (indexEagerly) {
53       roots.size
54     }
55   }
56 
57   override fun canonicalize(path: Path): Path {
58     // TODO(jwilson): throw FileNotFoundException if the canonical file doesn't exist.
59     return canonicalizeInternal(path)
60   }
61 
62   /** Don't throw [FileNotFoundException] if the path doesn't identify a file. */
63   private fun canonicalizeInternal(path: Path): Path {
64     return ROOT.resolve(path, normalize = true)
65   }
66 
67   override fun list(dir: Path): List<Path> {
68     val relativePath = dir.toRelativePath()
69     val result = mutableSetOf<Path>()
70     var foundAny = false
71     for ((fileSystem, base) in roots) {
72       try {
73         result += fileSystem.list(base / relativePath)
74           .filter { keepPath(it) }
75           .map { it.removeBase(base) }
76         foundAny = true
77       } catch (_: IOException) {
78       }
79     }
80     if (!foundAny) throw FileNotFoundException("file not found: $dir")
81     return result.toList()
82   }
83 
84   override fun listOrNull(dir: Path): List<Path>? {
85     val relativePath = dir.toRelativePath()
86     val result = mutableSetOf<Path>()
87     var foundAny = false
88     for ((fileSystem, base) in roots) {
89       val baseResult = fileSystem.listOrNull(base / relativePath)
90         ?.filter { keepPath(it) }
91         ?.map { it.removeBase(base) }
92       if (baseResult != null) {
93         result += baseResult
94         foundAny = true
95       }
96     }
97     return if (foundAny) result.toList() else null
98   }
99 
100   override fun openReadOnly(file: Path): FileHandle {
101     if (!keepPath(file)) throw FileNotFoundException("file not found: $file")
102     val relativePath = file.toRelativePath()
103     for ((fileSystem, base) in roots) {
104       try {
105         return fileSystem.openReadOnly(base / relativePath)
106       } catch (_: FileNotFoundException) {
107       }
108     }
109     throw FileNotFoundException("file not found: $file")
110   }
111 
112   override fun openReadWrite(file: Path, mustCreate: Boolean, mustExist: Boolean): FileHandle {
113     throw IOException("resources are not writable")
114   }
115 
116   override fun metadataOrNull(path: Path): FileMetadata? {
117     if (!keepPath(path)) return null
118     val relativePath = path.toRelativePath()
119     for ((fileSystem, base) in roots) {
120       return fileSystem.metadataOrNull(base / relativePath) ?: continue
121     }
122     return null
123   }
124 
125   override fun source(file: Path): Source {
126     if (!keepPath(file)) throw FileNotFoundException("file not found: $file")
127     // Make sure we have a path that doesn't start with '/'.
128     val relativePath = ROOT.resolve(file).relativeTo(ROOT)
129     return classLoader.getResourceAsStream(relativePath.toString())?.source()
130       ?: throw FileNotFoundException("file not found: $file")
131   }
132 
133   override fun sink(file: Path, mustCreate: Boolean): Sink {
134     throw IOException("$this is read-only")
135   }
136 
137   override fun appendingSink(file: Path, mustExist: Boolean): Sink {
138     throw IOException("$this is read-only")
139   }
140 
141   override fun createDirectory(dir: Path, mustCreate: Boolean): Unit =
142     throw IOException("$this is read-only")
143 
144   override fun atomicMove(source: Path, target: Path): Unit =
145     throw IOException("$this is read-only")
146 
147   override fun delete(path: Path, mustExist: Boolean): Unit =
148     throw IOException("$this is read-only")
149 
150   override fun createSymlink(source: Path, target: Path): Unit =
151     throw IOException("$this is read-only")
152 
153   private fun Path.toRelativePath(): String {
154     val canonicalThis = canonicalizeInternal(this)
155     return canonicalThis.relativeTo(ROOT).toString()
156   }
157 
158   /**
159    * Returns a search path of classpath roots. Each element contains a file system to use, and
160    * the base directory of that file system to search from.
161    */
162   private fun ClassLoader.toClasspathRoots(): List<Pair<FileSystem, Path>> {
163     // We'd like to build this upon an API like ClassLoader.getURLs() but unfortunately that
164     // API exists only on URLClassLoader (and that isn't the default class loader implementation).
165     //
166     // The closest we have is `ClassLoader.getResources("")`. It returns all classpath roots that
167     // are directories but none that are .jar files. To mitigate that we also search for all
168     // `META-INF/MANIFEST.MF` files, hastily assuming that every .jar file will have such an
169     // entry.
170     //
171     // Classpath entries that aren't directories and don't have a META-INF/MANIFEST.MF file will
172     // not be visible in this file system.
173     return getResources("").toList().mapNotNull { it.toFileRoot() } +
174       getResources("META-INF/MANIFEST.MF").toList().mapNotNull { it.toJarRoot() }
175   }
176 
177   private fun URL.toFileRoot(): Pair<FileSystem, Path>? {
178     if (protocol != "file") return null // Ignore unexpected URLs.
179     return systemFileSystem to File(toURI()).toOkioPath()
180   }
181 
182   private fun URL.toJarRoot(): Pair<FileSystem, Path>? {
183     val urlString = toString()
184     if (!urlString.startsWith("jar:file:")) return null // Ignore unexpected URLs.
185 
186     // Given a URL like `jar:file:/tmp/foo.jar!/META-INF/MANIFEST.MF`, get the path to the archive
187     // file, like `/tmp/foo.jar`.
188     val suffixStart = urlString.lastIndexOf("!")
189     if (suffixStart == -1) return null
190     val path = File(URI.create(urlString.substring("jar:".length, suffixStart))).toOkioPath()
191     val zip = openZip(
192       zipPath = path,
193       fileSystem = systemFileSystem,
194       predicate = { entry -> keepPath(entry.canonicalPath) },
195     )
196     return zip to ROOT
197   }
198 
199   private companion object {
200     val ROOT = "/".toPath()
201 
202     fun Path.removeBase(base: Path): Path {
203       val prefix = base.toString()
204       return ROOT / (toString().removePrefix(prefix).replace('\\', '/'))
205     }
206 
207     private fun keepPath(path: Path) = !path.name.endsWith(".class", ignoreCase = true)
208   }
209 }
210