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