1 /*
2 * Copyright (C) 2023 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.assetfilesystem
17
18 import android.content.res.AssetManager
19 import java.io.FileNotFoundException
20 import java.io.IOException
21 import java.io.InputStream
22 import okio.FileHandle
23 import okio.FileMetadata
24 import okio.FileSystem
25 import okio.Path
26 import okio.Path.Companion.toPath
27 import okio.Sink
28 import okio.Source
29 import okio.source
30
31 /**
32 * Expose this [AssetManager] as an Okio [FileSystem].
33 *
34 * Note: Assets are a read-only view on a file system and so any attempt to mutate
35 * will throw an [IOException].
36 */
AssetManagernull37 fun AssetManager.asFileSystem(): FileSystem = AssetFileSystem(this)
38
39 private class AssetFileSystem(
40 private val assets: AssetManager,
41 ) : FileSystem() {
42 override fun canonicalize(path: Path): Path {
43 val canonical = canonicalizeInternal(path)
44 if (canonical.existsInternal()) {
45 return canonical
46 }
47 throw FileNotFoundException("$path")
48 }
49
50 private fun canonicalizeInternal(path: Path) = ROOT.resolve(path, normalize = true)
51
52 private fun Path.toAssetRelativePathString(): String {
53 return toString().removePrefix("/")
54 }
55
56 /**
57 * Determine if [this] is a valid path to a file or directory.
58 *
59 * If this function returns true, a call to [AssetManager.open] will either return successfully
60 * or throw [FileNotFoundException] based on whether [this] is a file or directory, respectively.
61 */
62 private fun Path.existsInternal(): Boolean {
63 if (this == ROOT) {
64 return true
65 }
66
67 // Both non-existent paths and paths to existing files return an empty array when listing.
68 // Determine if a path exists by checking if its name is present in the parent's list.
69 val parent = checkNotNull(parent) { "Path has no parent. Did you canonicalize? $this" }
70 val children = assets.list(parent.toAssetRelativePathString()).orEmpty()
71 return name in children
72 }
73
74 override fun metadataOrNull(path: Path): FileMetadata? {
75 val canonical = canonicalizeInternal(path)
76 if (canonical.existsInternal()) {
77 val pathString = canonical.toAssetRelativePathString()
78 return try {
79 assets.open(pathString).close()
80 FileMetadata(
81 isRegularFile = true,
82 isDirectory = false,
83 )
84 } catch (_: FileNotFoundException) {
85 FileMetadata(
86 isRegularFile = false,
87 isDirectory = true,
88 )
89 }
90 }
91 return null
92 }
93
94 override fun list(dir: Path): List<Path> {
95 val canonical = canonicalizeInternal(dir)
96 if (canonical.existsInternal()) {
97 val pathString = canonical.toAssetRelativePathString()
98 try {
99 // This will throw if the path points to a file.
100 assets.open(pathString).close()
101 } catch (_: FileNotFoundException) {
102 return assets.list(pathString)
103 ?.map { it.toPath() }
104 .orEmpty()
105 }
106 }
107 throw FileNotFoundException("$dir")
108 }
109
110 override fun listOrNull(dir: Path): List<Path>? {
111 return try {
112 list(dir)
113 } catch (_: IOException) {
114 null
115 }
116 }
117
118 override fun openReadOnly(file: Path): FileHandle {
119 val pathString = canonicalizeInternal(file).toAssetRelativePathString()
120 val inputStream = assets.open(pathString)
121 return AssetFileHandle(assets, pathString, inputStream)
122 }
123
124 override fun openReadWrite(file: Path, mustCreate: Boolean, mustExist: Boolean): FileHandle {
125 throw IOException("asset file systems are read-only")
126 }
127
128 override fun source(file: Path): Source {
129 return assets.open(canonicalizeInternal(file).toAssetRelativePathString()).source()
130 }
131
132 override fun sink(file: Path, mustCreate: Boolean): Sink {
133 throw IOException("asset file systems are read-only")
134 }
135
136 override fun appendingSink(file: Path, mustExist: Boolean): Sink {
137 throw IOException("asset file systems are read-only")
138 }
139
140 override fun createDirectory(dir: Path, mustCreate: Boolean) {
141 throw IOException("asset file systems are read-only")
142 }
143
144 override fun atomicMove(source: Path, target: Path) {
145 throw IOException("asset file systems are read-only")
146 }
147
148 override fun delete(path: Path, mustExist: Boolean) {
149 throw IOException("asset file systems are read-only")
150 }
151
152 override fun createSymlink(source: Path, target: Path) {
153 throw IOException("asset file systems are read-only")
154 }
155
156 private companion object {
157 val ROOT = "/".toPath()
158 }
159 }
160
161 private class AssetFileHandle(
162 private val assets: AssetManager,
163 private val pathString: String,
164 private var inputStream: InputStream,
165 ) : FileHandle(false) {
166 private var currentOffset = 0
167 private var size = -1
168
protectedReadnull169 override fun protectedRead(
170 fileOffset: Long,
171 array: ByteArray,
172 arrayOffset: Int,
173 byteCount: Int,
174 ): Int {
175 // If we need to jump backwards or have reached the end of the file,
176 // close the existing stream and open a new one.
177 if (currentOffset > fileOffset || currentOffset == size) {
178 inputStream.close()
179 inputStream = assets.open(pathString)
180 currentOffset = 0
181 }
182
183 while (true) {
184 val skip = fileOffset - currentOffset
185 if (skip == 0L) break
186 val skipped = inputStream.skip(skip).toInt()
187 if (skipped == 0) {
188 // Since we know skip is never negative, a skip of 0 means EOF.
189 // Record this as the file size to trigger stream recreation.
190 size = currentOffset
191 throw IllegalArgumentException("fileOffset $fileOffset > size $size")
192 }
193 currentOffset += skipped
194 }
195
196 val read = inputStream.read(array, arrayOffset, byteCount)
197 if (read == -1) {
198 // A read of -1 means EOF. Record this as the file size to trigger stream recreation.
199 size = currentOffset
200 } else {
201 currentOffset += read
202 }
203 return read
204 }
205
protectedSizenull206 override fun protectedSize(): Long {
207 if (size == -1) {
208 while (true) {
209 val skipped = inputStream.skip(1024 * 1024).toInt()
210 if (skipped == 0) {
211 size = currentOffset
212 break
213 }
214 currentOffset += skipped
215 }
216 }
217 return size.toLong()
218 }
219
protectedClosenull220 override fun protectedClose() {
221 inputStream.close()
222 }
223
protectedWritenull224 override fun protectedWrite(
225 fileOffset: Long,
226 array: ByteArray,
227 arrayOffset: Int,
228 byteCount: Int,
229 ) {
230 throw AssertionError()
231 }
232
protectedFlushnull233 override fun protectedFlush() {
234 throw AssertionError()
235 }
236
protectedResizenull237 override fun protectedResize(size: Long) {
238 throw AssertionError()
239 }
240 }
241