xref: /aosp_15_r20/external/okio/okio/src/commonMain/kotlin/okio/ForwardingFileSystem.kt (revision f9742813c14b702d71392179818a9e591da8620c)
1 /*
2  * Copyright (C) 2020 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
17 
18 import kotlin.jvm.JvmName
19 
20 /**
21  * A [FileSystem] that forwards calls to another, intended for subclassing.
22  *
23  * ### Fault Injection
24  *
25  * You can use this to deterministically trigger file system failures in tests. This is useful to
26  * confirm that your program behaves correctly even if its file system operations fail. For example,
27  * this subclass fails every access of files named `unlucky.txt`:
28  *
29  * ```
30  * val faultyFileSystem = object : ForwardingFileSystem(FileSystem.SYSTEM) {
31  *   override fun onPathParameter(path: Path, functionName: String, parameterName: String): Path {
32  *     if (path.name == "unlucky.txt") throw IOException("synthetic failure!")
33  *     return path
34  *   }
35  * }
36  * ```
37  *
38  * You can fail specific operations by overriding them directly:
39  *
40  * ```
41  * val faultyFileSystem = object : ForwardingFileSystem(FileSystem.SYSTEM) {
42  *   override fun delete(path: Path) {
43  *     throw IOException("synthetic failure!")
44  *   }
45  * }
46  * ```
47  *
48  * ### Observability
49  *
50  * You can extend this to verify which files your program accesses. This is a testing file system
51  * that records accesses as they happen:
52  *
53  * ```
54  * class LoggingFileSystem : ForwardingFileSystem(FileSystem.SYSTEM) {
55  *   val log = mutableListOf<String>()
56  *
57  *   override fun onPathParameter(path: Path, functionName: String, parameterName: String): Path {
58  *     log += "$functionName($parameterName=$path)"
59  *     return path
60  *   }
61  * }
62  * ```
63  *
64  * This makes it easy for tests to assert exactly which files were accessed.
65  *
66  * ```
67  * @Test
68  * fun testMergeJsonReports() {
69  *   createSampleJsonReports()
70  *   loggingFileSystem.log.clear()
71  *
72  *   mergeJsonReports()
73  *
74  *   assertThat(loggingFileSystem.log).containsExactly(
75  *     "list(dir=json_reports)",
76  *     "source(file=json_reports/2020-10.json)",
77  *     "source(file=json_reports/2020-12.json)",
78  *     "source(file=json_reports/2020-11.json)",
79  *     "sink(file=json_reports/2020-all.json)"
80  *   )
81  * }
82  * ```
83  *
84  * ### Transformations
85  *
86  * Subclasses can transform file names and content.
87  *
88  * For example, your program may be written to operate on a well-known directory like `/etc/` or
89  * `/System`. You can rewrite paths to make such operations safer to test.
90  *
91  * You may also transform file content to apply application-layer encryption or compression. This
92  * is particularly useful in situations where it's difficult or impossible to enable those features
93  * in the underlying file system.
94  *
95  * ### Abstract Functions Only
96  *
97  * Some file system functions like [copy] are implemented by using other features. These are the
98  * non-abstract functions in the [FileSystem] interface.
99  *
100  * **This class forwards only the abstract functions;** non-abstract functions delegate to the
101  * other functions of this class. If desired, subclasses may override non-abstract functions to
102  * forward them.
103  */
104 abstract class ForwardingFileSystem(
105   /** [FileSystem] to which this instance is delegating. */
106   @get:JvmName("delegate")
107   val delegate: FileSystem,
108 ) : FileSystem() {
109 
110   /**
111    * Invoked each time a path is passed as a parameter to this file system. This returns the path to
112    * pass to [delegate], which should be [path] itself or a path on [delegate] that corresponds to
113    * it.
114    *
115    * Subclasses may override this to log accesses, fail on unexpected accesses, or map paths across
116    * file systems.
117    *
118    * The base implementation returns [path].
119    *
120    * Note that this function will be called twice for calls to [atomicMove]; once for the source
121    * file and once for the target file.
122    *
123    * @param path the path passed to any of the functions of this.
124    * @param functionName a string like "canonicalize", "metadataOrNull", or "appendingSink".
125    * @param parameterName a string like "path", "file", "source", or "target".
126    * @return the path to pass to [delegate] for the same parameter.
127    */
onPathParameternull128   open fun onPathParameter(path: Path, functionName: String, parameterName: String): Path = path
129 
130   /**
131    * Invoked each time a path is returned by [delegate]. This returns the path to return to the
132    * caller, which should be [path] itself or a path on this that corresponds to it.
133    *
134    * Subclasses may override this to log accesses, fail on unexpected path accesses, or map
135    * directories or path names.
136    *
137    * The base implementation returns [path].
138    *
139    * @param path the path returned by any of the functions of this.
140    * @param functionName a string like "canonicalize" or "list".
141    * @return the path to return to the caller.
142    */
143   open fun onPathResult(path: Path, functionName: String): Path = path
144 
145   @Throws(IOException::class)
146   override fun canonicalize(path: Path): Path {
147     val path = onPathParameter(path, "canonicalize", "path")
148     val result = delegate.canonicalize(path)
149     return onPathResult(result, "canonicalize")
150   }
151 
152   @Throws(IOException::class)
metadataOrNullnull153   override fun metadataOrNull(path: Path): FileMetadata? {
154     val path = onPathParameter(path, "metadataOrNull", "path")
155     val metadataOrNull = delegate.metadataOrNull(path) ?: return null
156     if (metadataOrNull.symlinkTarget == null) return metadataOrNull
157 
158     val symlinkTarget = onPathResult(metadataOrNull.symlinkTarget, "metadataOrNull")
159     return metadataOrNull.copy(symlinkTarget = symlinkTarget)
160   }
161 
162   @Throws(IOException::class)
listnull163   override fun list(dir: Path): List<Path> {
164     val dir = onPathParameter(dir, "list", "dir")
165     val result = delegate.list(dir)
166     val paths = result.mapTo(mutableListOf()) { onPathResult(it, "list") }
167     paths.sort()
168     return paths
169   }
170 
listOrNullnull171   override fun listOrNull(dir: Path): List<Path>? {
172     val dir = onPathParameter(dir, "listOrNull", "dir")
173     val result = delegate.listOrNull(dir) ?: return null
174     val paths = result.mapTo(mutableListOf()) { onPathResult(it, "listOrNull") }
175     paths.sort()
176     return paths
177   }
178 
listRecursivelynull179   override fun listRecursively(dir: Path, followSymlinks: Boolean): Sequence<Path> {
180     val dir = onPathParameter(dir, "listRecursively", "dir")
181     val result = delegate.listRecursively(dir, followSymlinks)
182     return result.map { onPathResult(it, "listRecursively") }
183   }
184 
185   @Throws(IOException::class)
openReadOnlynull186   override fun openReadOnly(file: Path): FileHandle {
187     val file = onPathParameter(file, "openReadOnly", "file")
188     return delegate.openReadOnly(file)
189   }
190 
191   @Throws(IOException::class)
openReadWritenull192   override fun openReadWrite(file: Path, mustCreate: Boolean, mustExist: Boolean): FileHandle {
193     val file = onPathParameter(file, "openReadWrite", "file")
194     return delegate.openReadWrite(file, mustCreate, mustExist)
195   }
196 
197   @Throws(IOException::class)
sourcenull198   override fun source(file: Path): Source {
199     val file = onPathParameter(file, "source", "file")
200     return delegate.source(file)
201   }
202 
203   @Throws(IOException::class)
sinknull204   override fun sink(file: Path, mustCreate: Boolean): Sink {
205     val file = onPathParameter(file, "sink", "file")
206     return delegate.sink(file, mustCreate)
207   }
208 
209   @Throws(IOException::class)
appendingSinknull210   override fun appendingSink(file: Path, mustExist: Boolean): Sink {
211     val file = onPathParameter(file, "appendingSink", "file")
212     return delegate.appendingSink(file, mustExist)
213   }
214 
215   @Throws(IOException::class)
createDirectorynull216   override fun createDirectory(dir: Path, mustCreate: Boolean) {
217     val dir = onPathParameter(dir, "createDirectory", "dir")
218     delegate.createDirectory(dir, mustCreate)
219   }
220 
221   @Throws(IOException::class)
atomicMovenull222   override fun atomicMove(source: Path, target: Path) {
223     val source = onPathParameter(source, "atomicMove", "source")
224     val target = onPathParameter(target, "atomicMove", "target")
225     delegate.atomicMove(source, target)
226   }
227 
228   @Throws(IOException::class)
deletenull229   override fun delete(path: Path, mustExist: Boolean) {
230     val path = onPathParameter(path, "delete", "path")
231     delegate.delete(path, mustExist)
232   }
233 
234   @Throws(IOException::class)
createSymlinknull235   override fun createSymlink(source: Path, target: Path) {
236     val source = onPathParameter(source, "createSymlink", "source")
237     val target = onPathParameter(target, "createSymlink", "target")
238     delegate.createSymlink(source, target)
239   }
240 
toStringnull241   override fun toString() = "${this::class.simpleName}($delegate)"
242 }
243