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