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.fakefilesystem
17 
18 import kotlin.jvm.JvmField
19 import kotlin.jvm.JvmName
20 import kotlin.reflect.KClass
21 import kotlinx.datetime.Clock
22 import kotlinx.datetime.Instant
23 import okio.ArrayIndexOutOfBoundsException
24 import okio.Buffer
25 import okio.ByteString
26 import okio.FileHandle
27 import okio.FileMetadata
28 import okio.FileNotFoundException
29 import okio.FileSystem
30 import okio.IOException
31 import okio.Path
32 import okio.Path.Companion.toPath
33 import okio.Sink
34 import okio.Source
35 import okio.fakefilesystem.FakeFileSystem.Element.Directory
36 import okio.fakefilesystem.FakeFileSystem.Element.File
37 import okio.fakefilesystem.FakeFileSystem.Element.Symlink
38 import okio.fakefilesystem.FakeFileSystem.Operation.READ
39 import okio.fakefilesystem.FakeFileSystem.Operation.WRITE
40 
41 /**
42  * A fully in-memory file system useful for testing. It includes features to support writing
43  * better tests.
44  *
45  * Use [openPaths] to see which paths have been opened for read or write, but not yet closed. Tests
46  * should call [checkNoOpenFiles] in `tearDown()` to confirm that no file streams were leaked.
47  *
48  * Strict By Default
49  * -----------------
50  *
51  * These actions are not allowed and throw an [IOException] if attempted:
52  *
53  *  * Moving a file that is currently open for reading or writing.
54  *  * Deleting a file that is currently open for reading or writing.
55  *  * Moving a file to a path that currently resolves to an empty directory.
56  *  * Reading and writing the same file at the same time.
57  *  * Opening a file for writing that is already open for writing.
58  *
59  * Programs that do not attempt any of the above operations should work fine on both UNIX and
60  * Windows systems. Relax these constraints individually or call [emulateWindows] or [emulateUnix];
61  * to apply the constraints of a particular operating system.
62  */
63 class FakeFileSystem(
64   @JvmField
65   val clock: Clock = Clock.System,
66 ) : FileSystem() {
67 
68   /** File system roots. Each element is a Directory and is created on-demand. */
69   private val roots = mutableMapOf<Path, Directory>()
70 
71   /** Files that are currently open and need to be closed to avoid resource leaks. */
72   private val openFiles = mutableListOf<OpenFile>()
73 
74   /**
75    * An absolute path with this file system's current working directory. Relative paths will be
76    * resolved against this directory when they are used.
77    */
78   var workingDirectory: Path = "/".toPath()
79     set(value) {
<lambda>null80       require(value.isAbsolute) {
81         "expected an absolute path but was $value"
82       }
83       field = value
84     }
85 
86   /**
87    * True to allow files to be moved even if they're currently open for read or write. UNIX file
88    * systems typically allow open files to be moved; Windows file systems do not.
89    */
90   var allowMovingOpenFiles = false
91 
92   /**
93    * True to allow files to be deleted even if they're currently open for read or write. UNIX file
94    * systems typically allow open files to be deleted; Windows file systems do not.
95    */
96   var allowDeletingOpenFiles = false
97 
98   /**
99    * True to allow the target of an [atomicMove] operation to be an empty directory. Windows file
100    * systems typically allow files to replace empty directories; UNIX file systems do not.
101    */
102   var allowClobberingEmptyDirectories = false
103 
104   /**
105    * True to permit a file to have multiple [sinks][sink] open at the same time. Both Windows and
106    * UNIX file systems permit this but the result may be undefined.
107    */
108   var allowWritesWhileWriting = false
109 
110   /**
111    * True to permit a file to have a [source] and [sink] open at the same time. Both Windows and
112    * UNIX file systems permit this but the result may be undefined.
113    */
114   var allowReadsWhileWriting = false
115 
116   /**
117    * True to allow symlinks to be created. UNIX file systems typically allow symlinks; Windows file
118    * systems do not. Setting this to false after creating a symlink does not prevent that symlink
119    * from being returned or used.
120    */
121   var allowSymlinks = false
122 
123   /**
124    * Canonical paths for every file and directory in this file system. This omits file system roots
125    * like `C:\` and `/`.
126    */
127   @get:JvmName("allPaths")
128   val allPaths: Set<Path>
129     get() {
130       val result = mutableListOf<Path>()
131       for (path in roots.keys) {
132         result += listRecursively(path)
133       }
134       result.sort()
135       return result.toSet()
136     }
137 
138   /**
139    * Canonical paths currently opened for reading or writing in the order they were opened. This may
140    * contain duplicates if a single path is open by multiple readers.
141    *
142    * Note that this may contain paths not present in [allPaths]. This occurs if a file is deleted
143    * while it is still open.
144    *
145    * The returned list is ordered by the order that the paths were opened.
146    */
147   @get:JvmName("openPaths")
148   val openPaths: List<Path>
<lambda>null149     get() = openFiles.map { it.canonicalPath }
150 
151   /**
152    * Confirm that all files that have been opened on this file system (with [source], [sink], and
153    * [appendingSink]) have since been closed. Call this in your test's `tearDown()` function to
154    * confirm that your program hasn't leaked any open files.
155    *
156    * Forgetting to close a file on a real file system is a severe error that may lead to a program
157    * crash. The operating system enforces a limit on how many files may be open simultaneously. On
158    * Linux this is [getrlimit] and is commonly adjusted with the `ulimit` command.
159    *
160    * [getrlimit]: https://man7.org/linux/man-pages/man2/getrlimit.2.html
161    *
162    * @throws IllegalStateException if any files are open when this function is called.
163    */
checkNoOpenFilesnull164   fun checkNoOpenFiles() {
165     val firstOpenFile = openFiles.firstOrNull() ?: return
166     throw IllegalStateException(
167       """
168       |expected 0 open files, but found:
169       |    ${openFiles.joinToString(separator = "\n    ") { it.canonicalPath.toString() }}
170       """.trimMargin(),
171       firstOpenFile.backtrace,
172     )
173   }
174 
175   /**
176    * Configure this file system to use a Windows-like working directory (`F:\`, unless the working
177    * directory is already Windows-like) and to follow a Windows-like policy on what operations
178    * are permitted.
179    */
emulateWindowsnull180   fun emulateWindows() {
181     if ("\\" !in workingDirectory.toString()) {
182       workingDirectory = "F:\\".toPath()
183     }
184     allowMovingOpenFiles = false
185     allowDeletingOpenFiles = false
186     allowClobberingEmptyDirectories = true
187     allowWritesWhileWriting = true
188     allowReadsWhileWriting = true
189   }
190 
191   /**
192    * Configure this file system to use a UNIX-like working directory (`/`, unless the working
193    * directory is already UNIX-like) and to follow a UNIX-like policy on what operations are
194    * permitted.
195    */
emulateUnixnull196   fun emulateUnix() {
197     if ("/" !in workingDirectory.toString()) {
198       workingDirectory = "/".toPath()
199     }
200     allowMovingOpenFiles = true
201     allowDeletingOpenFiles = true
202     allowClobberingEmptyDirectories = false
203     allowWritesWhileWriting = true
204     allowReadsWhileWriting = true
205     allowSymlinks = true
206   }
207 
canonicalizenull208   override fun canonicalize(path: Path): Path {
209     val canonicalPath = canonicalizeInternal(path)
210 
211     val lookupResult = lookupPath(canonicalPath)
212     if (lookupResult?.element == null) {
213       throw FileNotFoundException("no such file: $path")
214     }
215 
216     return lookupResult.path
217   }
218 
219   /** Don't throw [FileNotFoundException] if the path doesn't identify a file. */
canonicalizeInternalnull220   private fun canonicalizeInternal(path: Path): Path {
221     return workingDirectory.resolve(path, normalize = true)
222   }
223 
224   /**
225    * Sets the metadata of type [type] on [path] to [value]. If [value] is null this clears that
226    * metadata.
227    *
228    * Extras are not copied by [copy] but they are moved with [atomicMove].
229    *
230    * @throws IOException if [path] does not exist.
231    */
232   @Throws(IOException::class)
setExtranull233   fun <T : Any> setExtra(path: Path, type: KClass<out T>, value: T?) {
234     val canonicalPath = canonicalizeInternal(path)
235     val lookupResult = lookupPath(
236       canonicalPath = canonicalPath,
237       createRootOnDemand = canonicalPath.isRoot,
238       resolveLastSymlink = false,
239     )
240     val element = lookupResult?.element ?: throw FileNotFoundException("no such file: $path")
241     if (value == null) {
242       element.extras.remove(type)
243     } else {
244       element.extras[type] = value
245     }
246   }
247 
metadataOrNullnull248   override fun metadataOrNull(path: Path): FileMetadata? {
249     val canonicalPath = canonicalizeInternal(path)
250     val lookupResult = lookupPath(
251       canonicalPath = canonicalPath,
252       createRootOnDemand = canonicalPath.isRoot,
253       resolveLastSymlink = false,
254     )
255     return lookupResult?.element?.metadata
256   }
257 
listnull258   override fun list(dir: Path): List<Path> = list(dir, throwOnFailure = true)!!
259 
260   override fun listOrNull(dir: Path): List<Path>? = list(dir, throwOnFailure = false)
261 
262   private fun list(dir: Path, throwOnFailure: Boolean): List<Path>? {
263     val canonicalPath = canonicalizeInternal(dir)
264     val lookupResult = lookupPath(canonicalPath)
265     if (lookupResult?.element == null) {
266       if (throwOnFailure) throw FileNotFoundException("no such directory: $dir") else return null
267     }
268     val element = lookupResult.element as? Directory
269       ?: if (throwOnFailure) throw IOException("not a directory: $dir") else return null
270 
271     element.access(now = clock.now())
272     return element.children.keys.map { dir / it }.sorted()
273   }
274 
sourcenull275   override fun source(file: Path): Source {
276     val fileHandle = openReadOnly(file)
277     return fileHandle.source()
278       .also { fileHandle.close() }
279   }
280 
sinknull281   override fun sink(file: Path, mustCreate: Boolean): Sink {
282     val fileHandle = open(file, readWrite = true, mustCreate = mustCreate)
283     fileHandle.resize(0L) // If the file already has data, get rid of it.
284     return fileHandle.sink()
285       .also { fileHandle.close() }
286   }
287 
appendingSinknull288   override fun appendingSink(file: Path, mustExist: Boolean): Sink {
289     val fileHandle = open(file, readWrite = true, mustExist = mustExist)
290     return fileHandle.appendingSink()
291       .also { fileHandle.close() }
292   }
293 
openReadOnlynull294   override fun openReadOnly(file: Path): FileHandle {
295     return open(file, readWrite = false)
296   }
297 
openReadWritenull298   override fun openReadWrite(file: Path, mustCreate: Boolean, mustExist: Boolean): FileHandle {
299     return open(file, readWrite = true, mustCreate = mustCreate, mustExist = mustExist)
300   }
301 
opennull302   private fun open(
303     file: Path,
304     readWrite: Boolean,
305     mustCreate: Boolean = false,
306     mustExist: Boolean = false,
307   ): FileHandle {
308     require(!mustCreate || !mustExist) {
309       "Cannot require mustCreate and mustExist at the same time."
310     }
311 
312     val canonicalPath = canonicalizeInternal(file)
313     val lookupResult = lookupPath(canonicalPath, createRootOnDemand = readWrite)
314     val now = clock.now()
315     val element: File
316     val operation: Operation
317 
318     if (lookupResult?.element == null && mustExist) {
319       throw IOException("$file doesn't exist.")
320     }
321     if (lookupResult?.element != null && mustCreate) {
322       throw IOException("$file already exists.")
323     }
324 
325     if (readWrite) {
326       // Note that this case is used for both write and read/write.
327       if (lookupResult?.element is Directory) {
328         throw IOException("destination is a directory: $file")
329       }
330       if (!allowWritesWhileWriting) {
331         findOpenFile(canonicalPath, operation = WRITE)?.let {
332           throw IOException("file is already open for writing $file", it.backtrace)
333         }
334       }
335       if (!allowReadsWhileWriting) {
336         findOpenFile(canonicalPath, operation = READ)?.let {
337           throw IOException("file is already open for reading $file", it.backtrace)
338         }
339       }
340 
341       val parent = lookupResult?.parent
342         ?: throw FileNotFoundException("parent directory does not exist")
343       parent.access(now, true)
344 
345       val existing = lookupResult.element
346       element = File(createdAt = existing?.createdAt ?: now)
347       parent.children[lookupResult.segment!!] = element
348       operation = WRITE
349 
350       if (existing is File) {
351         element.data = existing.data
352       }
353     } else {
354       val existing = lookupResult?.element
355         ?: throw FileNotFoundException("no such file: $file")
356       element = existing as? File ?: throw IOException("not a file: $file")
357       operation = READ
358 
359       if (!allowReadsWhileWriting) {
360         findOpenFile(canonicalPath, operation = WRITE)?.let {
361           throw IOException("file is already open for writing $file", it.backtrace)
362         }
363       }
364     }
365 
366     element.access(now = clock.now(), modified = readWrite)
367 
368     val openFile = OpenFile(canonicalPath, operation, Exception("file opened for $operation here"))
369     openFiles += openFile
370 
371     return FakeFileHandle(
372       readWrite = readWrite,
373       openFile = openFile,
374       file = element,
375     )
376   }
377 
createDirectorynull378   override fun createDirectory(dir: Path, mustCreate: Boolean) {
379     val canonicalPath = canonicalizeInternal(dir)
380 
381     val lookupResult = lookupPath(canonicalPath, createRootOnDemand = true)
382 
383     if (canonicalPath.isRoot) {
384       // Looking it up was sufficient. Don't crash when creating roots that already exist.
385       return
386     }
387 
388     if (mustCreate && lookupResult?.element != null) {
389       throw IOException("already exists: $dir")
390     }
391 
392     val parentDirectory = lookupResult.requireParent()
393     parentDirectory.children[canonicalPath.nameBytes] = Directory(createdAt = clock.now())
394   }
395 
atomicMovenull396   override fun atomicMove(
397     source: Path,
398     target: Path,
399   ) {
400     val canonicalSource = canonicalizeInternal(source)
401     val canonicalTarget = canonicalizeInternal(target)
402 
403     val targetLookupResult = lookupPath(canonicalTarget, createRootOnDemand = true)
404     val sourceLookupResult = lookupPath(canonicalSource, resolveLastSymlink = false)
405 
406     // Universal constraints.
407     if (targetLookupResult?.element is Directory) {
408       throw IOException("target is a directory: $target")
409     }
410     val targetParent = targetLookupResult.requireParent()
411     if (!allowMovingOpenFiles) {
412       findOpenFile(canonicalSource)?.let {
413         throw IOException("source is open $source", it.backtrace)
414       }
415       findOpenFile(canonicalTarget)?.let {
416         throw IOException("target is open $target", it.backtrace)
417       }
418     }
419     if (!allowClobberingEmptyDirectories) {
420       if (sourceLookupResult?.element is Directory && targetLookupResult?.element is File) {
421         throw IOException("source is a directory and target is a file")
422       }
423     }
424 
425     val sourceParent = sourceLookupResult.requireParent()
426     val removed = sourceParent.children.remove(canonicalSource.nameBytes)
427       ?: throw FileNotFoundException("source doesn't exist: $source")
428     targetParent.children[canonicalTarget.nameBytes] = removed
429   }
430 
deletenull431   override fun delete(path: Path, mustExist: Boolean) {
432     val canonicalPath = canonicalizeInternal(path)
433 
434     val lookupResult = lookupPath(
435       canonicalPath = canonicalPath,
436       createRootOnDemand = true,
437       resolveLastSymlink = false,
438     )
439 
440     if (lookupResult?.element == null) {
441       if (mustExist) {
442         throw FileNotFoundException("no such file: $path")
443       } else {
444         return
445       }
446     }
447 
448     if (lookupResult.element is Directory && lookupResult.element.children.isNotEmpty()) {
449       throw IOException("non-empty directory")
450     }
451 
452     if (!allowDeletingOpenFiles) {
453       findOpenFile(canonicalPath)?.let {
454         throw IOException("file is open $path", it.backtrace)
455       }
456     }
457 
458     val directory = lookupResult.requireParent()
459     directory.children.remove(canonicalPath.nameBytes)
460   }
461 
createSymlinknull462   override fun createSymlink(
463     source: Path,
464     target: Path,
465   ) {
466     val canonicalSource = canonicalizeInternal(source)
467 
468     val existingLookupResult = lookupPath(canonicalSource, createRootOnDemand = true)
469     if (existingLookupResult?.element != null) {
470       throw IOException("already exists: $source")
471     }
472     val parent = existingLookupResult.requireParent()
473 
474     if (!allowSymlinks) {
475       throw IOException("symlinks are not supported")
476     }
477 
478     parent.children[canonicalSource.nameBytes] = Symlink(createdAt = clock.now(), target)
479   }
480 
481   /**
482    * Walks the file system looking for [canonicalPath], following symlinks encountered along the
483    * way. This function is designed to be used both when looking up existing files and when creating
484    * new files into an existing directory.
485    *
486    * It returns either:
487    *
488    *  * a path lookup result with an element if that file or directory or symlink exists. This is
489    *    useful when reading or writing an existing fie.
490    *
491    *  * a path lookup result that only got as far as the canonical path's parent, if the parent
492    *    exists but the child file does not. This is useful when creating a new file.
493    *
494    *  * null, if not even the parent directory exists. A file cannot yet be created with this path
495    *    because there is no parent to attach it to.
496    *
497    * This will create the root of the returned path if it does not exist.
498    *
499    * @param canonicalPath a normalized path, typically the result of [FakeFileSystem.canonicalizeInternal].
500    * @param recurseCount used internally to detect cycles.
501    * @param resolveLastSymlink true if the result's element must not itself be a symlink. Use this
502    *     for looking up metadata, or operations that apply to the path like delete and move. We
503    *     always follow symlinks for enclosing directories.
504    * @param createRootOnDemand true to create a root directory like `C:\` or `/` if it doesn't
505    *     exist. Pass true for mutating operations.
506    */
lookupPathnull507   private fun lookupPath(
508     canonicalPath: Path,
509     recurseCount: Int = 0,
510     resolveLastSymlink: Boolean = true,
511     createRootOnDemand: Boolean = false,
512   ): PathLookupResult? {
513     // 40 is chosen for consistency with the Linux kernel (which previously used 8).
514     if (recurseCount > 40) {
515       throw IOException("symlink cycle?")
516     }
517 
518     val rootPath = canonicalPath.root!!
519     var root = roots[rootPath]
520 
521     // If the path is a root, create it on demand.
522     if (root == null) {
523       if (!createRootOnDemand) return null
524       root = Directory(createdAt = clock.now())
525       roots[rootPath] = root
526     }
527 
528     var parent: Directory? = null
529     var lastSegment: ByteString? = null
530     var current: Element = root
531     var currentPath: Path = rootPath
532 
533     var segmentsTraversed = 0
534     val segments = canonicalPath.segmentsBytes
535     for (segment in segments) {
536       lastSegment = segment
537 
538       // Push the newest segment.
539       if (current !is Directory) {
540         throw IOException("not a directory: $currentPath")
541       }
542       parent = current
543       current = current.children[segment] ?: break
544       currentPath /= segment
545       segmentsTraversed++
546 
547       // If it's a symlink, recurse to follow it.
548       val isLastSegment = segmentsTraversed == segments.size
549       val followSymlinks = !isLastSegment || resolveLastSymlink
550       if (current is Symlink && followSymlinks) {
551         current.access(now = clock.now())
552         // We wanna normalize it in case the target is relative and starts with `..`.
553         currentPath = currentPath.parent!!.resolve(current.target, normalize = true)
554         val symlinkLookupResult = lookupPath(
555           canonicalPath = currentPath,
556           recurseCount = recurseCount + 1,
557           createRootOnDemand = createRootOnDemand,
558         ) ?: break
559         parent = symlinkLookupResult.parent
560         lastSegment = symlinkLookupResult.segment
561         current = symlinkLookupResult.element ?: break
562         currentPath = symlinkLookupResult.path
563       }
564     }
565 
566     return when (segmentsTraversed) {
567       segments.size -> {
568         PathLookupResult(currentPath, parent, lastSegment, current) // The file.
569       }
570       segments.size - 1 -> {
571         PathLookupResult(currentPath, parent, lastSegment, null) // The enclosing directory.
572       }
573       else -> null // We found nothing.
574     }
575   }
576 
577   private class PathLookupResult(
578     /** The canonical path for the looked up path or its enclosing directory. */
579     val path: Path,
580     /** Only null if the looked up path is a root. */
581     val parent: Directory?,
582     /** Only null if the looked up path is a root. */
583     val segment: ByteString?,
584     /** Non-null if this is a root. Also not null if this file exists. */
585     val element: Element?,
586   )
587 
requireParentnull588   private fun PathLookupResult?.requireParent(): Directory {
589     return this?.parent ?: throw IOException("parent directory does not exist")
590   }
591 
592   private sealed class Element(
593     val createdAt: Instant,
594   ) {
595     var lastModifiedAt: Instant = createdAt
596     var lastAccessedAt: Instant = createdAt
597     val extras = mutableMapOf<KClass<*>, Any>()
598 
599     class File(createdAt: Instant) : Element(createdAt) {
600       var data: ByteString = ByteString.EMPTY
601 
602       override val metadata: FileMetadata
603         get() = FileMetadata(
604           isRegularFile = true,
605           size = data.size.toLong(),
606           createdAt = createdAt,
607           lastModifiedAt = lastModifiedAt,
608           lastAccessedAt = lastAccessedAt,
609           extras = extras,
610         )
611     }
612 
613     class Directory(createdAt: Instant) : Element(createdAt) {
614       /** Keys are path segments. */
615       val children = mutableMapOf<ByteString, Element>()
616 
617       override val metadata: FileMetadata
618         get() = FileMetadata(
619           isDirectory = true,
620           createdAt = createdAt,
621           lastModifiedAt = lastModifiedAt,
622           lastAccessedAt = lastAccessedAt,
623           extras = extras,
624         )
625     }
626 
627     class Symlink(
628       createdAt: Instant,
629       /** This may be an absolute or relative path. */
630       val target: Path,
631     ) : Element(createdAt) {
632       override val metadata: FileMetadata
633         get() = FileMetadata(
634           symlinkTarget = target,
635           createdAt = createdAt,
636           lastModifiedAt = lastModifiedAt,
637           lastAccessedAt = lastAccessedAt,
638           extras = extras,
639         )
640     }
641 
accessnull642     fun access(
643       now: Instant,
644       modified: Boolean = false,
645     ) {
646       lastAccessedAt = now
647       if (modified) {
648         lastModifiedAt = now
649       }
650     }
651 
652     abstract val metadata: FileMetadata
653   }
654 
findOpenFilenull655   private fun findOpenFile(
656     canonicalPath: Path,
657     operation: Operation? = null,
658   ): OpenFile? {
659     return openFiles.firstOrNull {
660       it.canonicalPath == canonicalPath && (operation == null || operation == it.operation)
661     }
662   }
663 
checkOffsetAndCountnull664   private fun checkOffsetAndCount(
665     size: Long,
666     offset: Long,
667     byteCount: Long,
668   ) {
669     if (offset or byteCount < 0 || offset > size || size - offset < byteCount) {
670       throw ArrayIndexOutOfBoundsException("size=$size offset=$offset byteCount=$byteCount")
671     }
672   }
673 
674   private class OpenFile(
675     val canonicalPath: Path,
676     val operation: Operation,
677     val backtrace: Throwable,
678   )
679 
680   private enum class Operation {
681     READ,
682     WRITE,
683   }
684 
685   private inner class FakeFileHandle(
686     readWrite: Boolean,
687     private val openFile: OpenFile,
688     private val file: File,
689   ) : FileHandle(readWrite) {
690     private var closed = false
691 
protectedResizenull692     override fun protectedResize(size: Long) {
693       check(!closed) { "closed" }
694 
695       val delta = size - file.data.size
696       if (delta > 0) {
697         file.data = Buffer()
698           .write(file.data)
699           .write(ByteArray(delta.toInt()))
700           .readByteString()
701       } else {
702         file.data = file.data.substring(0, size.toInt())
703       }
704 
705       file.access(now = clock.now(), modified = true)
706     }
707 
protectedSizenull708     override fun protectedSize(): Long {
709       check(!closed) { "closed" }
710       return file.data.size.toLong()
711     }
712 
protectedReadnull713     override fun protectedRead(
714       fileOffset: Long,
715       array: ByteArray,
716       arrayOffset: Int,
717       byteCount: Int,
718     ): Int {
719       check(!closed) { "closed" }
720       checkOffsetAndCount(array.size.toLong(), arrayOffset.toLong(), byteCount.toLong())
721 
722       val fileOffsetInt = fileOffset.toInt()
723       val toCopy = minOf(file.data.size - fileOffsetInt, byteCount)
724       if (toCopy <= 0) return -1
725       for (i in 0 until toCopy) {
726         array[i + arrayOffset] = file.data[i + fileOffsetInt]
727       }
728       return toCopy
729     }
730 
protectedWritenull731     override fun protectedWrite(
732       fileOffset: Long,
733       array: ByteArray,
734       arrayOffset: Int,
735       byteCount: Int,
736     ) {
737       check(!closed) { "closed" }
738       checkOffsetAndCount(array.size.toLong(), arrayOffset.toLong(), byteCount.toLong())
739 
740       val buffer = Buffer()
741       buffer.write(file.data, 0, minOf(fileOffset.toInt(), file.data.size))
742       while (buffer.size < fileOffset) {
743         buffer.writeByte(0)
744       }
745       buffer.write(array, arrayOffset, byteCount)
746       if (buffer.size < file.data.size) {
747         buffer.write(file.data, buffer.size.toInt(), file.data.size - buffer.size.toInt())
748       }
749       file.data = buffer.snapshot()
750       file.access(now = clock.now(), modified = true)
751     }
752 
protectedFlushnull753     override fun protectedFlush() {
754       check(!closed) { "closed" }
755     }
756 
protectedClosenull757     override fun protectedClose() {
758       if (closed) return
759       closed = true
760       file.access(now = clock.now(), modified = readWrite)
761       openFiles -= openFile
762     }
763 
toStringnull764     override fun toString() = "FileHandler(${openFile.canonicalPath})"
765   }
766 
767   override fun toString() = "FakeFileSystem"
768 }
769