xref: /aosp_15_r20/external/okio/okio-testing-support/src/commonMain/kotlin/okio/AbstractFileSystemTest.kt (revision f9742813c14b702d71392179818a9e591da8620c)
1 /*
<lambda>null2  * 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.test.BeforeTest
19 import kotlin.test.Ignore
20 import kotlin.test.Test
21 import kotlin.test.assertContentEquals
22 import kotlin.test.assertEquals
23 import kotlin.test.assertFailsWith
24 import kotlin.test.assertFalse
25 import kotlin.test.assertNull
26 import kotlin.test.assertTrue
27 import kotlin.test.fail
28 import kotlin.time.Duration.Companion.milliseconds
29 import kotlin.time.Duration.Companion.minutes
30 import kotlin.time.Duration.Companion.seconds
31 import okio.ByteString.Companion.encodeUtf8
32 import okio.ByteString.Companion.toByteString
33 import okio.Path.Companion.toPath
34 
35 /** This test assumes that okio-files/ is the current working directory when executed. */
36 abstract class AbstractFileSystemTest(
37   val clock: Clock,
38   val fileSystem: FileSystem,
39   val windowsLimitations: Boolean,
40   val allowClobberingEmptyDirectories: Boolean,
41   val allowAtomicMoveFromFileToDirectory: Boolean,
42   val allowRenameWhenTargetIsOpen: Boolean = !windowsLimitations,
43   temporaryDirectory: Path,
44 ) {
45   val base: Path = temporaryDirectory / "${this::class.simpleName}-${randomToken(16)}"
46   private val isNodeJsFileSystem = fileSystem::class.simpleName?.startsWith("NodeJs") ?: false
47   private val isWasiFileSystem = fileSystem::class.simpleName?.startsWith("Wasi") ?: false
48   private val isWrappingJimFileSystem = this::class.simpleName?.contains("JimFileSystem") ?: false
49 
50   @BeforeTest
51   fun setUp() {
52     fileSystem.createDirectories(base)
53   }
54 
55   @Test
56   fun doesNotExistsWithInvalidPathDoesNotThrow() {
57     if (isNodeJsFileSystemOnWindows()) return
58 
59     val slash = Path.DIRECTORY_SEPARATOR
60     // We are testing: `\\127.0.0.1\..\localhost\c$\Windows`.
61     val file =
62       "${slash}${slash}127.0.0.1$slash..${slash}localhost${slash}c\$${slash}Windows".toPath()
63 
64     assertFalse(fileSystem.exists(file))
65   }
66 
67   @Test
68   fun canonicalizeDotReturnsCurrentWorkingDirectory() {
69     if (fileSystem.isFakeFileSystem || fileSystem is ForwardingFileSystem) return
70     val cwd = fileSystem.canonicalize(".".toPath())
71     val cwdString = cwd.toString()
72     val slash = Path.DIRECTORY_SEPARATOR
73     assertTrue(cwdString) {
74       if (isWrappingJimFileSystem) {
75         cwdString.endsWith("work")
76       } else if (isWasiFileSystem) {
77         cwdString.endsWith("/tmp")
78       } else {
79         cwdString.endsWith("okio${slash}okio") ||
80           cwdString.endsWith("${slash}okio-parent-okio-nodefilesystem-test") ||
81           cwdString.contains("/CoreSimulator/Devices/") || // iOS simulator.
82           cwdString == "/" // Android emulator.
83       }
84     }
85   }
86 
87   @Test
88   fun currentWorkingDirectoryIsADirectory() {
89     val metadata = fileSystem.metadata(".".toPath())
90     assertTrue(metadata.isDirectory)
91     assertFalse(metadata.isRegularFile)
92   }
93 
94   @Test
95   fun canonicalizeAbsolutePathNoSuchFile() {
96     assertFailsWith<FileNotFoundException> {
97       fileSystem.canonicalize(base / "no-such-file")
98     }
99   }
100 
101   @Test
102   fun canonicalizeRelativePathNoSuchFile() {
103     assertFailsWith<FileNotFoundException> {
104       fileSystem.canonicalize("no-such-file".toPath())
105     }
106   }
107 
108   @Test
109   fun canonicalizeFollowsSymlinkDirectories() {
110     if (!supportsSymlink()) return
111     val base = fileSystem.canonicalize(base)
112 
113     fileSystem.createDirectory(base / "real-directory")
114 
115     val expected = base / "real-directory" / "real-file.txt"
116     expected.writeUtf8("hello")
117 
118     fileSystem.createSymlink(base / "symlink-directory", base / "real-directory")
119 
120     val canonicalPath = fileSystem.canonicalize(base / "symlink-directory" / "real-file.txt")
121     assertEquals(expected, canonicalPath)
122   }
123 
124   @Test
125   fun canonicalizeFollowsSymlinkFiles() {
126     if (!supportsSymlink()) return
127     val base = fileSystem.canonicalize(base)
128 
129     fileSystem.createDirectory(base / "real-directory")
130 
131     val expected = base / "real-directory" / "real-file.txt"
132     expected.writeUtf8("hello")
133 
134     fileSystem.createSymlink(
135       base / "real-directory" / "symlink-file.txt",
136       expected,
137     )
138 
139     val canonicalPath = fileSystem.canonicalize(base / "real-directory" / "symlink-file.txt")
140     assertEquals(expected, canonicalPath)
141   }
142 
143   @Test
144   fun canonicalizeFollowsMultipleDirectoriesAndMultipleFiles() {
145     if (!supportsSymlink()) return
146     val base = fileSystem.canonicalize(base)
147 
148     fileSystem.createDirectory(base / "real-directory")
149 
150     val expected = base / "real-directory" / "real-file.txt"
151     expected.writeUtf8("hello")
152 
153     fileSystem.createSymlink(
154       base / "real-directory" / "one-symlink-file.txt",
155       expected,
156     )
157 
158     fileSystem.createSymlink(
159       base / "real-directory" / "two-symlink-file.txt",
160       base / "real-directory" / "one-symlink-file.txt",
161     )
162 
163     fileSystem.createSymlink(
164       base / "one-symlink-directory",
165       base / "real-directory",
166     )
167 
168     fileSystem.createSymlink(
169       base / "two-symlink-directory",
170       base / "one-symlink-directory",
171     )
172 
173     assertEquals(
174       expected,
175       fileSystem.canonicalize(base / "two-symlink-directory" / "two-symlink-file.txt"),
176     )
177     assertEquals(
178       expected,
179       fileSystem.canonicalize(base / "two-symlink-directory" / "one-symlink-file.txt"),
180     )
181     assertEquals(
182       expected,
183       fileSystem.canonicalize(base / "two-symlink-directory" / "real-file.txt"),
184     )
185 
186     assertEquals(
187       expected,
188       fileSystem.canonicalize(base / "one-symlink-directory" / "two-symlink-file.txt"),
189     )
190     assertEquals(
191       expected,
192       fileSystem.canonicalize(base / "one-symlink-directory" / "one-symlink-file.txt"),
193     )
194     assertEquals(
195       expected,
196       fileSystem.canonicalize(base / "one-symlink-directory" / "real-file.txt"),
197     )
198 
199     assertEquals(
200       expected,
201       fileSystem.canonicalize(base / "real-directory" / "two-symlink-file.txt"),
202     )
203     assertEquals(
204       expected,
205       fileSystem.canonicalize(base / "real-directory" / "one-symlink-file.txt"),
206     )
207     assertEquals(
208       expected,
209       fileSystem.canonicalize(expected),
210     )
211   }
212 
213   @Test
214   fun canonicalizeReturnsDeeperPath() {
215     if (!supportsSymlink()) return
216     val base = fileSystem.canonicalize(base)
217 
218     fileSystem.createDirectories(base / "a" / "b" / "c")
219 
220     val expected = base / "a" / "b" / "c" / "d.txt"
221     expected.writeUtf8("hello")
222 
223     fileSystem.createSymlink(
224       base / "e.txt",
225       "a".toPath() / "b" / "c" / "d.txt",
226     )
227 
228     assertEquals(
229       expected,
230       fileSystem.canonicalize(base / "e.txt"),
231     )
232   }
233 
234   @Test
235   fun canonicalizeReturnsShallowerPath() {
236     if (!supportsSymlink()) return
237     val base = fileSystem.canonicalize(base)
238 
239     val expected = base / "a.txt"
240     expected.writeUtf8("hello")
241 
242     fileSystem.createDirectories(base / "b" / "c" / "d")
243     fileSystem.createSymlink(
244       base / "b" / "c" / "d" / "e.txt",
245       "../".toPath() / ".." / ".." / "a.txt",
246     )
247 
248     assertEquals(
249       expected,
250       fileSystem.canonicalize(base / "b" / "c" / "d" / "e.txt"),
251     )
252   }
253 
254   @Test
255   fun list() {
256     val target = base / "list"
257     target.writeUtf8("hello, world!")
258     val entries = fileSystem.list(base)
259     assertTrue(entries.toString()) { target in entries }
260   }
261 
262   @Test
263   fun listOnRelativePathReturnsRelativePaths() {
264     // Make sure there's always at least one file so our assertion is useful.
265     if (fileSystem.isFakeFileSystem) {
266       val workingDirectory = "/directory".toPath()
267       fileSystem.createDirectory(workingDirectory)
268       fileSystem.workingDirectory = workingDirectory
269       fileSystem.write("a.txt".toPath()) {
270         writeUtf8("hello, world!")
271       }
272     } else if (isWrappingJimFileSystem || isWasiFileSystem) {
273       fileSystem.write("a.txt".toPath()) {
274         writeUtf8("hello, world!")
275       }
276     }
277 
278     val entries = fileSystem.list(".".toPath())
279     assertTrue(entries.toString()) { entries.isNotEmpty() && entries.all { it.isRelative } }
280   }
281 
282   @Test
283   fun listOnRelativePathWhichIsNotDotReturnsRelativePaths() {
284     if (isNodeJsFileSystem) return
285 
286     // Make sure there's always at least one file so our assertion is useful. We copy the first 2
287     // entries of the real working directory of the JVM to validate the results on all environment.
288     if (
289       fileSystem.isFakeFileSystem ||
290       fileSystem is ForwardingFileSystem && fileSystem.delegate.isFakeFileSystem
291     ) {
292       val workingDirectory = "/directory".toPath()
293       fileSystem.createDirectory(workingDirectory)
294       fileSystem.workingDirectory = workingDirectory
295       val apiDir = "api".toPath()
296       fileSystem.createDirectory(apiDir)
297       fileSystem.write(apiDir / "okio.api".toPath()) {
298         writeUtf8("hello, world!")
299       }
300     } else if (isWrappingJimFileSystem || isWasiFileSystem) {
301       val apiDir = "api".toPath()
302       fileSystem.createDirectory(apiDir)
303       fileSystem.write(apiDir / "okio.api".toPath()) {
304         writeUtf8("hello, world!")
305       }
306     }
307 
308     try {
309       assertEquals(
310         listOf("api".toPath() / "okio.api".toPath()),
311         fileSystem.list("api".toPath()),
312         // List some entries to help debugging.
313         fileSystem.listRecursively(".".toPath()).take(5).toList().joinToString(),
314       )
315     } catch (e: Throwable) {
316       if (e !is AssertionError && e !is FileNotFoundException) { throw e }
317 
318       // Non JVM environments.
319       val firstChild = fileSystem.list("Library".toPath()).first()
320       assertTrue(
321         // List some entries to help debugging.
322         fileSystem.listRecursively(".".toPath()).take(5).toList().joinToString(),
323       ) {
324         // To avoid relying too much on the environment we check that the path contains its parent
325         // once and that it's relative.
326         firstChild.isRelative &&
327           firstChild.toString().startsWith("Library") &&
328           firstChild.toString().split("Library").size == 2
329       }
330     }
331   }
332 
333   @Test
334   fun listOrNullOnRelativePathWhichIsNotDotReturnsRelativePaths() {
335     if (isNodeJsFileSystem) return
336 
337     // Make sure there's always at least one file so our assertion is useful. We copy the first 2
338     // entries of the real working directory of the JVM to validate the results on all environment.
339     if (
340       fileSystem.isFakeFileSystem ||
341       fileSystem is ForwardingFileSystem && fileSystem.delegate.isFakeFileSystem
342     ) {
343       val workingDirectory = "/directory".toPath()
344       fileSystem.createDirectory(workingDirectory)
345       fileSystem.workingDirectory = workingDirectory
346       val apiDir = "api".toPath()
347       fileSystem.createDirectory(apiDir)
348       fileSystem.write(apiDir / "okio.api".toPath()) {
349         writeUtf8("hello, world!")
350       }
351     } else if (isWrappingJimFileSystem) {
352       val apiDir = "api".toPath()
353       fileSystem.createDirectory(apiDir)
354       fileSystem.write(apiDir / "okio.api".toPath()) {
355         writeUtf8("hello, world!")
356       }
357     }
358 
359     try {
360       assertEquals(
361         listOf("api".toPath() / "okio.api".toPath()),
362         fileSystem.listOrNull("api".toPath()),
363         // List some entries to help debugging.
364         fileSystem.listRecursively(".".toPath()).take(5).toList().joinToString(),
365       )
366     } catch (e: Throwable) {
367       if (e !is AssertionError && e !is FileNotFoundException) { throw e }
368 
369       // Non JVM environments.
370       val firstChild = fileSystem.list("Library".toPath()).first()
371       assertTrue(
372         // List some entries to help debugging.
373         fileSystem.listRecursively(".".toPath()).take(5).toList().joinToString(),
374       ) {
375         // To avoid relying too much on the environment we check that the path contains its parent
376         // once and that it's relative.
377         firstChild.isRelative &&
378           firstChild.toString().startsWith("Library") &&
379           firstChild.toString().split("Library").size == 2
380       }
381     }
382   }
383 
384   @Test
385   fun listResultsAreSorted() {
386     val fileA = base / "a"
387     val fileB = base / "b"
388     val fileC = base / "c"
389     val fileD = base / "d"
390 
391     // Create files in a different order than the sorted order, so a file system that returns files
392     // in creation-order or reverse-creation order won't pass by accident.
393     fileD.writeUtf8("fileD")
394     fileB.writeUtf8("fileB")
395     fileC.writeUtf8("fileC")
396     fileA.writeUtf8("fileA")
397 
398     val entries = fileSystem.list(base)
399     assertEquals(entries, listOf(fileA, fileB, fileC, fileD))
400   }
401 
402   @Test
403   fun listNoSuchDirectory() {
404     assertFailsWith<FileNotFoundException> {
405       fileSystem.list(base / "no-such-directory")
406     }
407   }
408 
409   @Test
410   fun listFile() {
411     val target = base / "list"
412     target.writeUtf8("hello, world!")
413     val exception = assertFailsWith<IOException> {
414       fileSystem.list(target)
415     }
416     assertTrue(exception !is FileNotFoundException)
417   }
418 
419   @Test
420   fun listOrNull() {
421     val target = base / "list"
422     target.writeUtf8("hello, world!")
423     val entries = fileSystem.listOrNull(base)!!
424     assertTrue(entries.toString()) { target in entries }
425   }
426 
427   @Test
428   fun listOrNullOnRelativePathReturnsRelativePaths() {
429     // Make sure there's always at least one file so our assertion is useful.
430     if (fileSystem.isFakeFileSystem) {
431       val workingDirectory = "/directory".toPath()
432       fileSystem.createDirectory(workingDirectory)
433       fileSystem.workingDirectory = workingDirectory
434       fileSystem.write("a.txt".toPath()) {
435         writeUtf8("hello, world!")
436       }
437     } else if (isWrappingJimFileSystem) {
438       fileSystem.write("a.txt".toPath()) {
439         writeUtf8("hello, world!")
440       }
441     }
442 
443     val entries = fileSystem.listOrNull(".".toPath())
444     assertTrue(entries.toString()) { entries!!.isNotEmpty() && entries.all { it.isRelative } }
445   }
446 
447   @Test
448   fun listOrNullResultsAreSorted() {
449     val fileA = base / "a"
450     val fileB = base / "b"
451     val fileC = base / "c"
452     val fileD = base / "d"
453 
454     // Create files in a different order than the sorted order, so a file system that returns files
455     // in creation-order or reverse-creation order won't pass by accident.
456     fileD.writeUtf8("fileD")
457     fileB.writeUtf8("fileB")
458     fileC.writeUtf8("fileC")
459     fileA.writeUtf8("fileA")
460 
461     val entries = fileSystem.listOrNull(base)
462     assertEquals(entries, listOf(fileA, fileB, fileC, fileD))
463   }
464 
465   @Test
466   fun listOrNullNoSuchDirectory() {
467     assertNull(fileSystem.listOrNull(base / "no-such-directory"))
468   }
469 
470   @Test
471   fun listOrNullFile() {
472     val target = base / "list"
473     target.writeUtf8("hello, world!")
474     assertNull(fileSystem.listOrNull(target))
475   }
476 
477   @Test
478   fun listRecursivelyReturnsEmpty() {
479     val entries = fileSystem.listRecursively(base)
480     assertEquals(entries.toList(), listOf())
481   }
482 
483   @Test
484   fun listRecursivelyReturnsSingleFile() {
485     val baseA = base / "a"
486     baseA.writeUtf8("a")
487     val entries = fileSystem.listRecursively(base)
488     assertEquals(entries.toList(), listOf(baseA))
489   }
490 
491   @Test
492   fun listRecursivelyRecurses() {
493     val baseA = base / "a"
494     val baseAB = baseA / "b"
495     baseA.createDirectory()
496     baseAB.writeUtf8("ab")
497     val entries = fileSystem.listRecursively(base)
498     assertEquals(listOf(baseA, baseAB), entries.toList())
499   }
500 
501   @Test
502   fun listRecursivelyNoSuchFile() {
503     val baseA = base / "a"
504     val sequence = fileSystem.listRecursively(baseA)
505     assertFailsWith<FileNotFoundException> {
506       sequence.first()
507     }
508   }
509 
510   /**
511    * Note that this is different from `Files.walk` in java.nio which returns the argument even if
512    * it is not a directory.
513    */
514   @Test
515   fun listRecursivelyNotADirectory() {
516     val baseA = base / "a"
517     baseA.writeUtf8("a")
518     val sequence = fileSystem.listRecursively(baseA)
519     val exception = assertFailsWith<IOException> {
520       sequence.first()
521     }
522     assertTrue(exception !is FileNotFoundException)
523   }
524 
525   @Test
526   fun listRecursivelyIsDepthFirst() {
527     val baseA = base / "a"
528     val baseB = base / "b"
529     val baseA1 = baseA / "1"
530     val baseA2 = baseA / "2"
531     val baseB1 = baseB / "1"
532     val baseB2 = baseB / "2"
533     baseA.createDirectory()
534     baseB.createDirectory()
535     baseA1.writeUtf8("a1")
536     baseA2.writeUtf8("a2")
537     baseB1.writeUtf8("b1")
538     baseB2.writeUtf8("b2")
539     val entries = fileSystem.listRecursively(base)
540     assertEquals(listOf(baseA, baseA1, baseA2, baseB, baseB1, baseB2), entries.toList())
541   }
542 
543   @Test
544   fun listRecursivelyIsLazy() {
545     val baseA = base / "a"
546     val baseB = base / "b"
547     baseA.createDirectory()
548     baseB.createDirectory()
549     val entries = fileSystem.listRecursively(base).iterator()
550 
551     // This call will enqueue up the children of base, baseA and baseB.
552     assertEquals(baseA, entries.next())
553     val baseA1 = baseA / "1"
554     val baseA2 = baseA / "2"
555     baseA1.writeUtf8("a1")
556     baseA2.writeUtf8("a2")
557 
558     // This call will enqueue the children of baseA, baseA1 and baseA2.
559     assertEquals(baseA1, entries.next())
560     assertEquals(baseA2, entries.next())
561     assertEquals(baseB, entries.next())
562 
563     // This call will enqueue the children of baseB, baseB1 and baseB2.
564     val baseB1 = baseB / "1"
565     val baseB2 = baseB / "2"
566     baseB1.writeUtf8("b1")
567     baseB2.writeUtf8("b2")
568     assertEquals(baseB1, entries.next())
569     assertEquals(baseB2, entries.next())
570     assertFalse(entries.hasNext())
571   }
572 
573   /**
574    * This test creates directories that should be listed lazily, and then deletes them! The test
575    * wants to confirm that the sequence is resilient to such changes.
576    */
577   @Test
578   fun listRecursivelySilentlyIgnoresListFailures() {
579     val baseA = base / "a"
580     val baseB = base / "b"
581     baseA.createDirectory()
582     baseB.createDirectory()
583     val entries = fileSystem.listRecursively(base).iterator()
584     assertEquals(baseA, entries.next())
585     assertEquals(baseB, entries.next())
586     fileSystem.delete(baseA)
587     fileSystem.delete(baseB)
588     assertFalse(entries.hasNext())
589   }
590 
591   @Test
592   fun listRecursivelySequenceIterationsAreIndependent() {
593     val sequence = fileSystem.listRecursively(base)
594     val iterator1 = sequence.iterator()
595     assertFalse(iterator1.hasNext())
596     val baseA = base / "a"
597     baseA.writeUtf8("a")
598     val iterator2 = sequence.iterator()
599     assertEquals(baseA, iterator2.next())
600     assertFalse(iterator2.hasNext())
601   }
602 
603   @Test
604   fun listRecursivelyFollowsSymlinks() {
605     if (!supportsSymlink()) return
606 
607     val baseA = base / "a"
608     val baseAA = baseA / "a"
609     val baseB = base / "b"
610     val baseBA = baseB / "a"
611     baseA.createDirectory()
612     baseAA.writeUtf8("aa")
613     fileSystem.createSymlink(baseB, baseA)
614 
615     val sequence = fileSystem.listRecursively(base, followSymlinks = true)
616     assertEquals(listOf(baseA, baseAA, baseB, baseBA), sequence.toList())
617   }
618 
619   @Test
620   fun listRecursivelyDoesNotFollowSymlinks() {
621     if (!supportsSymlink()) return
622 
623     val baseA = base / "a"
624     val baseAA = baseA / "a"
625     val baseB = base / "b"
626     baseA.createDirectory()
627     baseAA.writeUtf8("aa")
628     fileSystem.createSymlink(baseB, baseA)
629 
630     val sequence = fileSystem.listRecursively(base, followSymlinks = false)
631     assertEquals(listOf(baseA, baseAA, baseB), sequence.toList())
632   }
633 
634   @Test
635   fun listRecursivelyOnSymlink() {
636     if (!supportsSymlink()) return
637 
638     val baseA = base / "a"
639     val baseAA = baseA / "a"
640     val baseB = base / "b"
641     val baseBA = baseB / "a"
642 
643     baseA.createDirectory()
644     baseAA.writeUtf8("aa")
645     fileSystem.createSymlink(baseB, baseA)
646 
647     val sequence = fileSystem.listRecursively(baseB, followSymlinks = false)
648     assertEquals(listOf(baseBA), sequence.toList())
649   }
650 
651   @Test
652   fun listRecursiveWithSpecialCharacterNamedFiles() {
653     val baseA = base / "ä"
654     val baseASuperSaiyan = baseA / "超サイヤ人"
655     val baseB = base / "ß"
656     val baseBIliad = baseB / "Ἰλιάς"
657 
658     baseA.createDirectory()
659     baseASuperSaiyan.writeUtf8("カカロットよ!")
660     baseB.createDirectory()
661     baseBIliad.writeUtf8("μῆνιν ἄειδε θεὰ Πηληϊάδεω Ἀχιλῆος")
662 
663     val sequence = fileSystem.listRecursively(base)
664     assertEquals(listOf(baseB, baseBIliad, baseA, baseASuperSaiyan), sequence.toList())
665   }
666 
667   @Test
668   fun listRecursiveOnSymlinkWithSpecialCharacterNamedFiles() {
669     if (!supportsSymlink()) return
670 
671     val baseA = base / "ä"
672     val baseASuperSaiyan = baseA / "超サイヤ人"
673     val baseB = base / "ß"
674     val baseBSuperSaiyan = baseB / "超サイヤ人"
675 
676     baseA.createDirectory()
677     baseASuperSaiyan.writeUtf8("aa")
678     fileSystem.createSymlink(baseB, baseA)
679 
680     val sequence = fileSystem.listRecursively(baseB, followSymlinks = false)
681     assertEquals(listOf(baseBSuperSaiyan), sequence.toList())
682   }
683 
684   @Test
685   fun listRecursivelyOnSymlinkCycleThrows() {
686     if (!supportsSymlink()) return
687 
688     val baseA = base / "a"
689     val baseAB = baseA / "b"
690     val baseAC = baseA / "c"
691 
692     baseA.createDirectory()
693     baseAB.writeUtf8("ab")
694     fileSystem.createSymlink(baseAC, baseA)
695 
696     val iterator = fileSystem.listRecursively(base, followSymlinks = true).iterator()
697     assertEquals(baseA, iterator.next())
698     assertEquals(baseAB, iterator.next())
699     assertEquals(baseAC, iterator.next())
700     val exception = assertFailsWith<IOException> {
701       iterator.next() // This would fail because 'c' refers to a path we've already visited.
702     }
703     assertEquals("symlink cycle at $baseAC", exception.message)
704   }
705 
706   @Test
707   fun listRecursivelyDoesNotFollowRelativeSymlink() {
708     if (!supportsSymlink()) return
709 
710     val baseA = base / "a"
711     val baseAA = baseA / "a"
712     val baseB = base / "b"
713     baseA.createDirectory()
714     baseAA.writeUtf8("aa")
715     fileSystem.createSymlink(baseB, ".".toPath()) // Symlink to enclosing directory!
716 
717     val iterator = fileSystem.listRecursively(base, followSymlinks = true).iterator()
718     assertEquals(baseA, iterator.next())
719     assertEquals(baseAA, iterator.next())
720     assertEquals(baseB, iterator.next())
721     val exception = assertFailsWith<IOException> {
722       iterator.next()
723     }
724     assertEquals("symlink cycle at $baseB", exception.message)
725   }
726 
727   @Test
728   fun fileSourceNoSuchDirectory() {
729     assertFailsWith<FileNotFoundException> {
730       fileSystem.source(base / "no-such-directory" / "file")
731     }
732   }
733 
734   @Test
735   fun fileSource() {
736     val path = base / "file-source"
737     path.writeUtf8("hello, world!")
738 
739     val source = fileSystem.source(path)
740     val buffer = Buffer()
741     assertTrue(source.read(buffer, 100L) == 13L)
742     assertEquals(-1L, source.read(buffer, 100L))
743     assertEquals("hello, world!", buffer.readUtf8())
744     source.close()
745   }
746 
747   @Test
748   fun readPath() {
749     val path = base / "read-path"
750     val string = "hello, read with a Path"
751     path.writeUtf8(string)
752 
753     val result = fileSystem.read(path) {
754       assertEquals("hello", readUtf8(5))
755       assertEquals(", read with ", readUtf8(12))
756       assertEquals("a Path", readUtf8())
757       return@read "success"
758     }
759     assertEquals("success", result)
760   }
761 
762   @Test
763   fun fileSink() {
764     val path = base / "file-sink"
765     val sink = fileSystem.sink(path)
766     val buffer = Buffer().writeUtf8("hello, world!")
767     sink.write(buffer, buffer.size)
768     sink.close()
769     assertTrue(path in fileSystem.list(base))
770     assertEquals(0, buffer.size)
771     assertEquals("hello, world!", path.readUtf8())
772   }
773 
774   @Test
775   fun fileSinkWithSpecialCharacterNamedFiles() {
776     val path = base / "Ἰλιάς"
777     val sink = fileSystem.sink(path)
778     val buffer = Buffer().writeUtf8("μῆνιν ἄειδε θεὰ Πηληϊάδεω Ἀχιλῆος")
779     sink.write(buffer, buffer.size)
780     sink.close()
781     assertTrue(path in fileSystem.list(base))
782     assertEquals(0, buffer.size)
783     assertEquals("μῆνιν ἄειδε θεὰ Πηληϊάδεω Ἀχιλῆος", path.readUtf8())
784   }
785 
786   @Test
787   fun fileSinkMustCreate() {
788     val path = base / "file-sink"
789     val sink = fileSystem.sink(path, mustCreate = true)
790     val buffer = Buffer().writeUtf8("hello, world!")
791     sink.write(buffer, buffer.size)
792     sink.close()
793     assertTrue(path in fileSystem.list(base))
794     assertEquals(0, buffer.size)
795     assertEquals("hello, world!", path.readUtf8())
796   }
797 
798   @Test
799   fun fileSinkMustCreateThrowsIfAlreadyExists() {
800     val path = base / "file-sink"
801     path.writeUtf8("First!")
802     assertFailsWith<IOException> {
803       fileSystem.sink(path, mustCreate = true)
804     }
805   }
806 
807   /**
808    * Write a file by concatenating three mechanisms, then read it in its entirety using three other
809    * mechanisms. This is attempting to defend against unwanted use of Windows text mode.
810    *
811    * https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/fopen-wfopen?view=msvc-160
812    */
813   @Test
814   fun fileSinkSpecialCharacters() {
815     val path = base / "file-sink-special-characters"
816     val content = "[ctrl-z: \u001A][newline: \n][crlf: \r\n]".encodeUtf8()
817 
818     fileSystem.write(path) {
819       writeUtf8("FileSystem.write()\n")
820       write(content)
821     }
822 
823     fileSystem.openReadWrite(path).use { handle ->
824       handle.sink(fileOffset = handle.size()).buffer().use { sink ->
825         sink.writeUtf8("FileSystem.openReadWrite()\n")
826         sink.write(content)
827       }
828     }
829 
830     fileSystem.appendingSink(path).buffer().use { sink ->
831       sink.writeUtf8("FileSystem.appendingSink()\n")
832       sink.write(content)
833     }
834 
835     fileSystem.read(path) {
836       assertEquals("FileSystem.write()", readUtf8LineStrict())
837       assertEquals(content, readByteString(content.size.toLong()))
838       assertEquals("FileSystem.openReadWrite()", readUtf8LineStrict())
839       assertEquals(content, readByteString(content.size.toLong()))
840       assertEquals("FileSystem.appendingSink()", readUtf8LineStrict())
841       assertEquals(content, readByteString(content.size.toLong()))
842       assertTrue(exhausted())
843     }
844 
845     fileSystem.openReadWrite(path).use { handle ->
846       handle.source().buffer().use { source ->
847         assertEquals("FileSystem.write()", source.readUtf8LineStrict())
848         assertEquals(content, source.readByteString(content.size.toLong()))
849         assertEquals("FileSystem.openReadWrite()", source.readUtf8LineStrict())
850         assertEquals(content, source.readByteString(content.size.toLong()))
851         assertEquals("FileSystem.appendingSink()", source.readUtf8LineStrict())
852         assertEquals(content, source.readByteString(content.size.toLong()))
853         assertTrue(source.exhausted())
854       }
855     }
856 
857     fileSystem.openReadOnly(path).use { handle ->
858       handle.source().buffer().use { source ->
859         assertEquals("FileSystem.write()", source.readUtf8LineStrict())
860         assertEquals(content, source.readByteString(content.size.toLong()))
861         assertEquals("FileSystem.openReadWrite()", source.readUtf8LineStrict())
862         assertEquals(content, source.readByteString(content.size.toLong()))
863         assertEquals("FileSystem.appendingSink()", source.readUtf8LineStrict())
864         assertEquals(content, source.readByteString(content.size.toLong()))
865         assertTrue(source.exhausted())
866       }
867     }
868   }
869 
870   @Test
871   fun writePath() {
872     val path = base / "write-path"
873     val content = fileSystem.write(path) {
874       val string = "hello, write with a Path"
875       writeUtf8(string)
876       return@write string
877     }
878     assertTrue(path in fileSystem.list(base))
879     assertEquals(content, path.readUtf8())
880   }
881 
882   @Test
883   fun writePathMustCreate() {
884     val path = base / "write-path"
885     val content = fileSystem.write(path, mustCreate = true) {
886       val string = "hello, write with a Path"
887       writeUtf8(string)
888       return@write string
889     }
890     assertTrue(path in fileSystem.list(base))
891     assertEquals(content, path.readUtf8())
892   }
893 
894   @Test
895   fun writePathMustCreateThrowsIfAlreadyExists() {
896     val path = base / "write-path"
897     path.writeUtf8("First!")
898     assertFailsWith<IOException> {
899       fileSystem.write(path, mustCreate = true) {}
900     }
901   }
902 
903   @Test
904   fun appendingSinkAppendsToExistingFile() {
905     val path = base / "appending-sink-appends-to-existing-file"
906     path.writeUtf8("hello, world!\n")
907     val sink = fileSystem.appendingSink(path)
908     val buffer = Buffer().writeUtf8("this is added later!")
909     sink.write(buffer, buffer.size)
910     sink.close()
911     assertTrue(path in fileSystem.list(base))
912     assertEquals("hello, world!\nthis is added later!", path.readUtf8())
913   }
914 
915   @Test
916   fun appendingSinkDoesNotImpactExistingFile() {
917     if (fileSystem.isFakeFileSystem && !fileSystem.allowReadsWhileWriting) return
918 
919     val path = base / "appending-sink-does-not-impact-existing-file"
920     path.writeUtf8("hello, world!\n")
921     val sink = fileSystem.appendingSink(path)
922     assertEquals("hello, world!\n", path.readUtf8())
923     sink.close()
924     assertEquals("hello, world!\n", path.readUtf8())
925   }
926 
927   @Test
928   fun appendingSinkCreatesNewFile() {
929     val path = base / "appending-sink-creates-new-file"
930     val sink = fileSystem.appendingSink(path)
931     val buffer = Buffer().writeUtf8("this is all there is!")
932     sink.write(buffer, buffer.size)
933     sink.close()
934     assertTrue(path in fileSystem.list(base))
935     assertEquals("this is all there is!", path.readUtf8())
936   }
937 
938   @Test
939   fun appendingSinkExistingFileMustExist() {
940     val path = base / "appending-sink-creates-new-file"
941     path.writeUtf8("Hey, ")
942 
943     val sink = fileSystem.appendingSink(path, mustExist = true)
944     val buffer = Buffer().writeUtf8("this is all there is!")
945     sink.write(buffer, buffer.size)
946     sink.close()
947     assertTrue(path in fileSystem.list(base))
948     assertEquals("Hey, this is all there is!", path.readUtf8())
949   }
950 
951   @Test
952   fun appendingSinkMustExistThrowsIfAbsent() {
953     val path = base / "appending-sink-creates-new-file"
954     assertFailsWith<IOException> {
955       fileSystem.appendingSink(path, mustExist = true)
956     }
957   }
958 
959   @Test
960   fun fileSinkFlush() {
961     if (fileSystem.isFakeFileSystem && !fileSystem.allowReadsWhileWriting) return
962 
963     val path = base / "file-sink"
964     val sink = fileSystem.sink(path)
965 
966     val buffer = Buffer().writeUtf8("hello,")
967     sink.write(buffer, buffer.size)
968     sink.flush()
969     assertEquals("hello,", path.readUtf8())
970 
971     buffer.writeUtf8(" world!")
972     sink.write(buffer, buffer.size)
973     sink.close()
974     assertEquals("hello, world!", path.readUtf8())
975   }
976 
977   @Test
978   fun fileSinkNoSuchDirectory() {
979     assertFailsWith<FileNotFoundException> {
980       fileSystem.sink(base / "no-such-directory" / "file")
981     }
982   }
983 
984   @Test
985   fun createDirectory() {
986     val path = base / "create-directory"
987     fileSystem.createDirectory(path)
988     assertTrue(path in fileSystem.list(base))
989   }
990 
991   @Test
992   fun createDirectoryMustCreate() {
993     val path = base / "create-directory"
994     fileSystem.createDirectory(path, mustCreate = true)
995     assertTrue(path in fileSystem.list(base))
996   }
997 
998   @Test
999   fun createDirectoryAlreadyExists() {
1000     val path = base / "already-exists"
1001     fileSystem.createDirectory(path)
1002     fileSystem.createDirectory(path)
1003   }
1004 
1005   @Test
1006   fun createDirectoryAlreadyExistsMustCreateThrows() {
1007     val path = base / "already-exists"
1008     fileSystem.createDirectory(path)
1009     val exception = assertFailsWith<IOException> {
1010       fileSystem.createDirectory(path, mustCreate = true)
1011     }
1012     assertTrue(exception !is FileNotFoundException)
1013   }
1014 
1015   @Test
1016   fun createDirectoryParentDirectoryDoesNotExist() {
1017     val path = base / "no-such-directory" / "created"
1018     assertFailsWith<IOException> {
1019       fileSystem.createDirectory(path)
1020     }
1021   }
1022 
1023   @Test
1024   fun createDirectoriesSingle() {
1025     val path = base / "create-directories-single"
1026     fileSystem.createDirectories(path)
1027     assertTrue(path in fileSystem.list(base))
1028     assertTrue(fileSystem.metadata(path).isDirectory)
1029   }
1030 
1031   @Test
1032   fun createDirectoriesAlreadyExists() {
1033     val path = base / "already-exists"
1034     fileSystem.createDirectory(path)
1035     fileSystem.createDirectories(path)
1036     assertTrue(fileSystem.metadata(path).isDirectory)
1037   }
1038 
1039   @Test
1040   fun createDirectoriesAlreadyExistsMustCreateThrows() {
1041     val path = base / "already-exists"
1042     fileSystem.createDirectory(path)
1043     val exception = assertFailsWith<IOException> {
1044       fileSystem.createDirectories(path, mustCreate = true)
1045     }
1046     assertTrue(exception !is FileNotFoundException)
1047     assertTrue(fileSystem.metadata(path).isDirectory)
1048   }
1049 
1050   @Test
1051   fun createDirectoriesParentDirectoryDoesNotExist() {
1052     fileSystem.createDirectories(base / "a" / "b" / "c")
1053     assertTrue(base / "a" in fileSystem.list(base))
1054     assertTrue(base / "a" / "b" in fileSystem.list(base / "a"))
1055     assertTrue(base / "a" / "b" / "c" in fileSystem.list(base / "a" / "b"))
1056     assertTrue(fileSystem.metadata(base / "a" / "b" / "c").isDirectory)
1057   }
1058 
1059   @Test
1060   fun createDirectoriesParentIsFile() {
1061     val file = base / "simple-file"
1062     file.writeUtf8("just a file")
1063     assertFailsWith<IOException> {
1064       fileSystem.createDirectories(file / "child")
1065     }
1066   }
1067 
1068   @Test
1069   fun atomicMoveFile() {
1070     val source = base / "source"
1071     source.writeUtf8("hello, world!")
1072     val target = base / "target"
1073     fileSystem.atomicMove(source, target)
1074     assertEquals("hello, world!", target.readUtf8())
1075     assertTrue(source !in fileSystem.list(base))
1076     assertTrue(target in fileSystem.list(base))
1077   }
1078 
1079   @Test
1080   fun atomicMoveDirectory() {
1081     val source = base / "source"
1082     fileSystem.createDirectory(source)
1083     val target = base / "target"
1084     fileSystem.atomicMove(source, target)
1085     assertTrue(source !in fileSystem.list(base))
1086     assertTrue(target in fileSystem.list(base))
1087   }
1088 
1089   @Test
1090   fun atomicMoveSourceIsTarget() {
1091     val source = base / "source"
1092     source.writeUtf8("hello, world!")
1093     fileSystem.atomicMove(source, source)
1094     assertEquals("hello, world!", source.readUtf8())
1095     assertTrue(source in fileSystem.list(base))
1096   }
1097 
1098   @Test
1099   fun atomicMoveClobberExistingFile() {
1100     // `java.io` on Windows doesn't allow file renaming if the target already exists.
1101     if (isJvmFileSystemOnWindows()) return
1102 
1103     val source = base / "source"
1104     source.writeUtf8("hello, world!")
1105     val target = base / "target"
1106     target.writeUtf8("this file will be clobbered!")
1107     fileSystem.atomicMove(source, target)
1108     assertEquals("hello, world!", target.readUtf8())
1109     assertTrue(source !in fileSystem.list(base))
1110     assertTrue(target in fileSystem.list(base))
1111   }
1112 
1113   @Test
1114   fun atomicMoveSourceDoesNotExist() {
1115     val source = base / "source"
1116     val target = base / "target"
1117     if (fileSystem::class.simpleName == "JvmSystemFileSystem") {
1118       assertFailsWith<IOException> {
1119         fileSystem.atomicMove(source, target)
1120       }
1121     } else {
1122       assertFailsWith<FileNotFoundException> {
1123         fileSystem.atomicMove(source, target)
1124       }
1125     }
1126   }
1127 
1128   @Test
1129   fun atomicMoveSourceIsFileAndTargetIsDirectory() {
1130     val source = base / "source"
1131     source.writeUtf8("hello, world!")
1132     val target = base / "target"
1133     fileSystem.createDirectory(target)
1134 
1135     if (allowAtomicMoveFromFileToDirectory) {
1136       fileSystem.atomicMove(source, target)
1137       assertEquals("hello, world!", target.readUtf8())
1138     } else {
1139       val exception = assertFailsWith<IOException> {
1140         fileSystem.atomicMove(source, target)
1141       }
1142       assertTrue(exception !is FileNotFoundException)
1143     }
1144   }
1145 
1146   @Test
1147   fun atomicMoveSourceIsDirectoryAndTargetIsFile() {
1148     // `java.io` on Windows doesn't allow file renaming if the target already exists.
1149     if (isJvmFileSystemOnWindows()) return
1150 
1151     val source = base / "source"
1152     fileSystem.createDirectory(source)
1153     val target = base / "target"
1154     target.writeUtf8("hello, world!")
1155     try {
1156       fileSystem.atomicMove(source, target)
1157       assertTrue(allowClobberingEmptyDirectories)
1158     } catch (e: IOException) {
1159       assertFalse(allowClobberingEmptyDirectories)
1160     }
1161   }
1162 
1163   @Test
1164   fun copyFile() {
1165     val source = base / "source"
1166     source.writeUtf8("hello, world!")
1167     val target = base / "target"
1168     fileSystem.copy(source, target)
1169     assertTrue(target in fileSystem.list(base))
1170     assertEquals("hello, world!", source.readUtf8())
1171     assertEquals("hello, world!", target.readUtf8())
1172   }
1173 
1174   @Test
1175   fun copySourceDoesNotExist() {
1176     val source = base / "source"
1177     val target = base / "target"
1178     assertFailsWith<FileNotFoundException> {
1179       fileSystem.copy(source, target)
1180     }
1181     assertFalse(target in fileSystem.list(base))
1182   }
1183 
1184   @Test
1185   fun copyTargetIsClobbered() {
1186     val source = base / "source"
1187     source.writeUtf8("hello, world!")
1188     val target = base / "target"
1189     target.writeUtf8("this file will be clobbered!")
1190     fileSystem.copy(source, target)
1191     assertTrue(target in fileSystem.list(base))
1192     assertEquals("hello, world!", target.readUtf8())
1193   }
1194 
1195   @Test
1196   fun deleteFile() {
1197     val path = base / "delete-file"
1198     path.writeUtf8("delete me")
1199     fileSystem.delete(path)
1200     assertTrue(path !in fileSystem.list(base))
1201   }
1202 
1203   @Test
1204   fun deleteFileMustExist() {
1205     val path = base / "delete-file"
1206     path.writeUtf8("delete me")
1207     fileSystem.delete(path, mustExist = true)
1208     assertTrue(path !in fileSystem.list(base))
1209   }
1210 
1211   @Test
1212   fun deleteEmptyDirectory() {
1213     val path = base / "delete-empty-directory"
1214     fileSystem.createDirectory(path)
1215     fileSystem.delete(path)
1216     assertTrue(path !in fileSystem.list(base))
1217   }
1218 
1219   @Test
1220   fun deleteEmptyDirectoryMustExist() {
1221     val path = base / "delete-empty-directory"
1222     fileSystem.createDirectory(path)
1223     fileSystem.delete(path, mustExist = true)
1224     assertTrue(path !in fileSystem.list(base))
1225   }
1226 
1227   @Test
1228   fun deleteDoesNotExist() {
1229     val path = base / "no-such-file"
1230     fileSystem.delete(path)
1231   }
1232 
1233   @Test
1234   fun deleteFailsOnNoSuchFileIfMustExist() {
1235     val path = base / "no-such-file"
1236     assertFailsWith<FileNotFoundException> {
1237       fileSystem.delete(path, mustExist = true)
1238     }
1239   }
1240 
1241   @Test
1242   fun deleteFailsOnNonEmptyDirectory() {
1243     val path = base / "non-empty-directory"
1244     fileSystem.createDirectory(path)
1245     (path / "file.txt").writeUtf8("inside directory")
1246     val exception = assertFailsWith<IOException> {
1247       fileSystem.delete(path)
1248     }
1249     assertTrue(exception !is FileNotFoundException)
1250   }
1251 
1252   @Test
1253   fun deleteFailsOnNonEmptyDirectoryMustExist() {
1254     val path = base / "non-empty-directory"
1255     fileSystem.createDirectory(path)
1256     (path / "file.txt").writeUtf8("inside directory")
1257     val exception = assertFailsWith<IOException> {
1258       fileSystem.delete(path, mustExist = true)
1259     }
1260     assertTrue(exception !is FileNotFoundException)
1261   }
1262 
1263   @Test
1264   fun deleteRecursivelyFile() {
1265     val path = base / "delete-recursively-file"
1266     path.writeUtf8("delete me")
1267     fileSystem.deleteRecursively(path)
1268     assertTrue(path !in fileSystem.list(base))
1269   }
1270 
1271   @Test
1272   fun deleteRecursivelyFileMustExist() {
1273     val path = base / "delete-recursively-file"
1274     path.writeUtf8("delete me")
1275     fileSystem.deleteRecursively(path, mustExist = true)
1276     assertTrue(path !in fileSystem.list(base))
1277   }
1278 
1279   @Test
1280   fun deleteRecursivelyEmptyDirectory() {
1281     val path = base / "delete-recursively-empty-directory"
1282     fileSystem.createDirectory(path)
1283     fileSystem.deleteRecursively(path)
1284     assertTrue(path !in fileSystem.list(base))
1285   }
1286 
1287   @Test
1288   fun deleteRecursivelyEmptyDirectoryMustExist() {
1289     val path = base / "delete-recursively-empty-directory"
1290     fileSystem.createDirectory(path)
1291     fileSystem.deleteRecursively(path, mustExist = true)
1292     assertTrue(path !in fileSystem.list(base))
1293   }
1294 
1295   @Test
1296   fun deleteRecursivelyNoSuchFile() {
1297     val path = base / "no-such-file"
1298     fileSystem.deleteRecursively(path)
1299   }
1300 
1301   @Test
1302   fun deleteRecursivelyMustExistFailsOnNoSuchFile() {
1303     val path = base / "no-such-file"
1304     assertFailsWith<IOException> {
1305       fileSystem.deleteRecursively(path, mustExist = true)
1306     }
1307   }
1308 
1309   @Test
1310   fun deleteRecursivelyNonEmptyDirectory() {
1311     val path = base / "delete-recursively-non-empty-directory"
1312     fileSystem.createDirectory(path)
1313     (path / "file.txt").writeUtf8("inside directory")
1314     fileSystem.deleteRecursively(path)
1315     assertTrue(path !in fileSystem.list(base))
1316     assertTrue((path / "file.txt") !in fileSystem.list(base))
1317   }
1318 
1319   @Test
1320   fun deleteRecursivelyNonEmptyDirectoryMustExist() {
1321     val path = base / "delete-recursively-non-empty-directory"
1322     fileSystem.createDirectory(path)
1323     (path / "file.txt").writeUtf8("inside directory")
1324     fileSystem.deleteRecursively(path, mustExist = true)
1325     assertTrue(path !in fileSystem.list(base))
1326     assertTrue((path / "file.txt") !in fileSystem.list(base))
1327   }
1328 
1329   @Test
1330   fun deleteRecursivelyDeepHierarchy() {
1331     fileSystem.createDirectory(base / "a")
1332     fileSystem.createDirectory(base / "a" / "b")
1333     fileSystem.createDirectory(base / "a" / "b" / "c")
1334     (base / "a" / "b" / "c" / "d.txt").writeUtf8("inside deep hierarchy")
1335     fileSystem.deleteRecursively(base / "a")
1336     assertEquals(listOf(), fileSystem.list(base))
1337   }
1338 
1339   @Test
1340   fun deleteRecursivelyDeepHierarchyMustExist() {
1341     fileSystem.createDirectory(base / "a")
1342     fileSystem.createDirectory(base / "a" / "b")
1343     fileSystem.createDirectory(base / "a" / "b" / "c")
1344     (base / "a" / "b" / "c" / "d.txt").writeUtf8("inside deep hierarchy")
1345     fileSystem.deleteRecursively(base / "a", mustExist = true)
1346     assertEquals(listOf(), fileSystem.list(base))
1347   }
1348 
1349   @Test
1350   fun deleteRecursivelyOnSymlinkToFileDeletesOnlyThatSymlink() {
1351     if (!supportsSymlink()) return
1352 
1353     val baseA = base / "a"
1354     val baseB = base / "b"
1355     baseB.writeUtf8("b")
1356     fileSystem.createSymlink(baseA, baseB)
1357     fileSystem.deleteRecursively(baseA)
1358     assertEquals("b", baseB.readUtf8())
1359   }
1360 
1361   @Test
1362   fun deleteRecursivelyOnSymlinkToFileDeletesOnlyThatSymlinkMustExist() {
1363     if (!supportsSymlink()) return
1364 
1365     val baseA = base / "a"
1366     val baseB = base / "b"
1367     baseB.writeUtf8("b")
1368     fileSystem.createSymlink(baseA, baseB)
1369     fileSystem.deleteRecursively(baseA, mustExist = true)
1370     assertEquals("b", baseB.readUtf8())
1371   }
1372 
1373   @Test
1374   fun deleteRecursivelyOnSymlinkToDirectoryDeletesOnlyThatSymlink() {
1375     if (!supportsSymlink()) return
1376 
1377     val baseA = base / "a"
1378     val baseB = base / "b"
1379     val baseBC = base / "b" / "c"
1380     fileSystem.createDirectory(baseB)
1381     baseBC.writeUtf8("c")
1382     fileSystem.createSymlink(baseA, baseB)
1383     fileSystem.deleteRecursively(baseA)
1384     assertEquals("c", baseBC.readUtf8())
1385   }
1386 
1387   @Test
1388   fun deleteRecursivelyOnSymlinkToDirectoryDeletesOnlyThatSymlinkMustExist() {
1389     if (!supportsSymlink()) return
1390 
1391     val baseA = base / "a"
1392     val baseB = base / "b"
1393     val baseBC = base / "b" / "c"
1394     fileSystem.createDirectory(baseB)
1395     baseBC.writeUtf8("c")
1396     fileSystem.createSymlink(baseA, baseB)
1397     fileSystem.deleteRecursively(baseA, mustExist = true)
1398     assertEquals("c", baseBC.readUtf8())
1399   }
1400 
1401   @Test
1402   fun deleteRecursivelyOnSymlinkCycleSucceeds() {
1403     if (!supportsSymlink()) return
1404 
1405     val baseA = base / "a"
1406     val baseAB = baseA / "b"
1407     val baseAC = baseA / "c"
1408 
1409     baseA.createDirectory()
1410     baseAB.writeUtf8("ab")
1411     fileSystem.createSymlink(baseAC, baseA)
1412 
1413     fileSystem.deleteRecursively(base)
1414     assertFalse(fileSystem.exists(base))
1415   }
1416 
1417   @Test
1418   fun deleteRecursivelyOnSymlinkCycleSucceedsMustExist() {
1419     if (!supportsSymlink()) return
1420 
1421     val baseA = base / "a"
1422     val baseAB = baseA / "b"
1423     val baseAC = baseA / "c"
1424 
1425     baseA.createDirectory()
1426     baseAB.writeUtf8("ab")
1427     fileSystem.createSymlink(baseAC, baseA)
1428 
1429     fileSystem.deleteRecursively(base, mustExist = true)
1430     assertFalse(fileSystem.exists(base))
1431   }
1432 
1433   @Test
1434   fun deleteRecursivelyOnSymlinkToEnclosingDirectorySucceeds() {
1435     if (!supportsSymlink()) return
1436 
1437     val baseA = base / "a"
1438     fileSystem.createSymlink(baseA, ".".toPath())
1439 
1440     fileSystem.deleteRecursively(baseA)
1441     assertFalse(fileSystem.exists(baseA))
1442     assertTrue(fileSystem.exists(base))
1443   }
1444 
1445   @Test
1446   fun deleteRecursivelyOnSymlinkToEnclosingDirectorySucceedsMustExist() {
1447     if (!supportsSymlink()) return
1448 
1449     val baseA = base / "a"
1450     fileSystem.createSymlink(baseA, ".".toPath())
1451 
1452     fileSystem.deleteRecursively(baseA, mustExist = true)
1453     assertFalse(fileSystem.exists(baseA))
1454     assertTrue(fileSystem.exists(base))
1455   }
1456 
1457   @Test
1458   fun fileMetadata() {
1459     val minTime = clock.now()
1460     val path = base / "file-metadata"
1461     path.writeUtf8("hello, world!")
1462     val maxTime = clock.now()
1463 
1464     val metadata = fileSystem.metadata(path)
1465     assertTrue(metadata.isRegularFile)
1466     assertFalse(metadata.isDirectory)
1467     assertEquals(13, metadata.size)
1468     assertInRange(metadata.createdAt, minTime, maxTime)
1469     assertInRange(metadata.lastModifiedAt, minTime, maxTime)
1470     assertInRange(metadata.lastAccessedAt, minTime, maxTime)
1471   }
1472 
1473   @Test
1474   fun directoryMetadata() {
1475     val minTime = clock.now()
1476     val path = base / "directory-metadata"
1477     fileSystem.createDirectory(path)
1478     val maxTime = clock.now()
1479 
1480     val metadata = fileSystem.metadata(path)
1481     assertFalse(metadata.isRegularFile)
1482     assertTrue(metadata.isDirectory)
1483     // Note that the size check is omitted; we'd expect null but the JVM returns values like 64.
1484     assertInRange(metadata.createdAt, minTime, maxTime)
1485     assertInRange(metadata.lastModifiedAt, minTime, maxTime)
1486     assertInRange(metadata.lastAccessedAt, minTime, maxTime)
1487   }
1488 
1489   @Test
1490   fun fileMetadataWithSpecialCharacterNamedFiles() {
1491     val minTime = clock.now()
1492     val path = base / "超サイヤ人"
1493     path.writeUtf8("カカロットよ!")
1494     val maxTime = clock.now()
1495 
1496     val metadata = fileSystem.metadata(path)
1497     assertTrue(metadata.isRegularFile)
1498     assertFalse(metadata.isDirectory)
1499     assertEquals(21, metadata.size)
1500     assertInRange(metadata.createdAt, minTime, maxTime)
1501     assertInRange(metadata.lastModifiedAt, minTime, maxTime)
1502     assertInRange(metadata.lastAccessedAt, minTime, maxTime)
1503   }
1504 
1505   @Test
1506   fun directoryMetadataWithSpecialCharacterNamedFiles() {
1507     val minTime = clock.now()
1508     val path = base / "Ἰλιάς"
1509     fileSystem.createDirectory(path)
1510     val maxTime = clock.now()
1511 
1512     val metadata = fileSystem.metadata(path)
1513     assertFalse(metadata.isRegularFile)
1514     assertTrue(metadata.isDirectory)
1515     // Note that the size check is omitted; we'd expect null but the JVM returns values like 64.
1516     assertInRange(metadata.createdAt, minTime, maxTime)
1517     assertInRange(metadata.lastModifiedAt, minTime, maxTime)
1518     assertInRange(metadata.lastAccessedAt, minTime, maxTime)
1519   }
1520 
1521   @Test
1522   fun absentMetadataOrNull() {
1523     val path = base / "no-such-file"
1524     assertNull(fileSystem.metadataOrNull(path))
1525   }
1526 
1527   @Test
1528   fun absentParentDirectoryMetadataOrNull() {
1529     val path = base / "no-such-directory" / "no-such-file"
1530     assertNull(fileSystem.metadataOrNull(path))
1531   }
1532 
1533   @Test
1534   fun parentDirectoryIsFileMetadataOrNull() {
1535     val parent = base / "regular-file"
1536     val path = parent / "no-such-file"
1537     parent.writeUtf8("just a regular file")
1538     // This returns null on Windows and throws IOException on other platforms.
1539     try {
1540       assertNull(fileSystem.metadataOrNull(path))
1541     } catch (e: IOException) {
1542       // Also okay.
1543     }
1544   }
1545 
1546   @Test
1547   @Ignore
1548   fun inaccessibleMetadata() {
1549     // TODO(swankjesse): configure a test directory in CI that exists, but that this process doesn't
1550     //     have permission to read metadata of. Perhaps a file in another user's /home directory?
1551   }
1552 
1553   @Test
1554   fun absentMetadata() {
1555     val path = base / "no-such-file"
1556     assertFailsWith<FileNotFoundException> {
1557       fileSystem.metadata(path)
1558     }
1559   }
1560 
1561   @Test
1562   fun fileExists() {
1563     val path = base / "file-exists"
1564     assertFalse(fileSystem.exists(path))
1565     path.writeUtf8("hello, world!")
1566     assertTrue(fileSystem.exists(path))
1567   }
1568 
1569   @Test
1570   fun directoryExists() {
1571     val path = base / "directory-exists"
1572     assertFalse(fileSystem.exists(path))
1573     fileSystem.createDirectory(path)
1574     assertTrue(fileSystem.exists(path))
1575   }
1576 
1577   @Test
1578   fun deleteOpenForWritingFailsOnWindows() {
1579     val file = base / "file.txt"
1580     expectIOExceptionOnWindows(exceptJs = true) {
1581       fileSystem.sink(file).use {
1582         fileSystem.delete(file)
1583       }
1584     }
1585   }
1586 
1587   @Test
1588   fun deleteOpenForReadingFailsOnWindows() {
1589     val file = base / "file.txt"
1590     file.writeUtf8("abc")
1591     expectIOExceptionOnWindows(exceptJs = true) {
1592       fileSystem.source(file).use {
1593         fileSystem.delete(file)
1594       }
1595     }
1596   }
1597 
1598   @Test
1599   fun renameSourceIsOpenFailsOnWindows() {
1600     val from = base / "from.txt"
1601     val to = base / "to.txt"
1602     from.writeUtf8("source file")
1603     to.writeUtf8("target file")
1604     expectIOExceptionOnWindows(exceptJs = true) {
1605       fileSystem.source(from).use {
1606         fileSystem.atomicMove(from, to)
1607       }
1608     }
1609   }
1610 
1611   @Test
1612   fun renameTargetIsOpenFailsOnWindows() {
1613     val from = base / "from.txt"
1614     val to = base / "to.txt"
1615     from.writeUtf8("source file")
1616     to.writeUtf8("target file")
1617 
1618     val expectCrash = !allowRenameWhenTargetIsOpen
1619     try {
1620       fileSystem.source(to).use {
1621         fileSystem.atomicMove(from, to)
1622       }
1623       assertFalse(expectCrash)
1624     } catch (_: IOException) {
1625       assertTrue(expectCrash)
1626     }
1627   }
1628 
1629   @Test
1630   fun deleteContentsOfParentOfFileOpenForReadingFailsOnWindows() {
1631     val parentA = (base / "a")
1632     fileSystem.createDirectory(parentA)
1633     val parentAB = parentA / "b"
1634     fileSystem.createDirectory(parentAB)
1635     val parentABC = parentAB / "c"
1636     fileSystem.createDirectory(parentABC)
1637     val file = parentABC / "file.txt"
1638     file.writeUtf8("child file")
1639     expectIOExceptionOnWindows {
1640       fileSystem.source(file).use {
1641         fileSystem.delete(file)
1642         fileSystem.delete(parentABC)
1643         fileSystem.delete(parentAB)
1644         fileSystem.delete(parentA)
1645       }
1646     }
1647   }
1648 
1649   @Test fun fileHandleWriteAndRead() {
1650     val path = base / "file-handle-write-and-read"
1651     fileSystem.openReadWrite(path).use { handle ->
1652 
1653       handle.sink().buffer().use { sink ->
1654         sink.writeUtf8("abcdefghijklmnop")
1655       }
1656 
1657       handle.source().buffer().use { source ->
1658         assertEquals("abcde", source.readUtf8(5))
1659         assertEquals("fghijklmnop", source.readUtf8())
1660       }
1661     }
1662   }
1663 
1664   @Test fun fileHandleWriteAndOverwrite() {
1665     val path = base / "file-handle-write-and-overwrite"
1666     fileSystem.openReadWrite(path).use { handle ->
1667 
1668       handle.sink().buffer().use { sink ->
1669         sink.writeUtf8("abcdefghij")
1670       }
1671 
1672       handle.sink(fileOffset = handle.size() - 3).buffer().use { sink ->
1673         sink.writeUtf8("HIJKLMNOP")
1674       }
1675 
1676       handle.source().buffer().use { source ->
1677         assertEquals("abcdefgHIJKLMNOP", source.readUtf8())
1678       }
1679     }
1680   }
1681 
1682   @Test fun fileHandleWriteBeyondEnd() {
1683     val path = base / "file-handle-write-beyond-end"
1684     fileSystem.openReadWrite(path).use { handle ->
1685 
1686       handle.sink(fileOffset = 10).buffer().use { sink ->
1687         sink.writeUtf8("klmnop")
1688       }
1689 
1690       handle.source().buffer().use { source ->
1691         assertEquals("00000000000000000000", source.readByteString(10).hex())
1692         assertEquals("klmnop", source.readUtf8())
1693       }
1694     }
1695   }
1696 
1697   @Test fun fileHandleResizeSmaller() {
1698     val path = base / "file-handle-resize-smaller"
1699     fileSystem.openReadWrite(path).use { handle ->
1700 
1701       handle.sink().buffer().use { sink ->
1702         sink.writeUtf8("abcdefghijklmnop")
1703       }
1704 
1705       handle.resize(10)
1706 
1707       handle.source().buffer().use { source ->
1708         assertEquals("abcdefghij", source.readUtf8())
1709       }
1710     }
1711   }
1712 
1713   @Test fun fileHandleResizeLarger() {
1714     val path = base / "file-handle-resize-larger"
1715     fileSystem.openReadWrite(path).use { handle ->
1716 
1717       handle.sink().buffer().use { sink ->
1718         sink.writeUtf8("abcde")
1719       }
1720 
1721       handle.resize(15)
1722 
1723       handle.source().buffer().use { source ->
1724         assertEquals("abcde", source.readUtf8(5))
1725         assertEquals("00000000000000000000", source.readByteString().hex())
1726       }
1727     }
1728   }
1729 
1730   @Test fun fileHandleFlush() {
1731     if (windowsLimitations) return // Open for reading and writing simultaneously.
1732 
1733     val path = base / "file-handle-flush"
1734     fileSystem.openReadWrite(path).use { handleA ->
1735       handleA.sink().buffer().use { sink ->
1736         sink.writeUtf8("abcde")
1737       }
1738       handleA.flush()
1739 
1740       fileSystem.openReadWrite(path).use { handleB ->
1741         handleB.source().buffer().use { source ->
1742           assertEquals("abcde", source.readUtf8())
1743         }
1744       }
1745     }
1746   }
1747 
1748   @Test fun fileHandleLargeBufferedWriteAndRead() {
1749     if (isBrowser()) return // This test errors on browsers in CI.
1750 
1751     val data = randomBytes(1024 * 1024 * 8)
1752 
1753     val path = base / "file-handle-large-buffered-write-and-read"
1754     fileSystem.openReadWrite(path).use { handle ->
1755       handle.sink().buffer().use { sink ->
1756         sink.write(data)
1757       }
1758     }
1759 
1760     fileSystem.openReadWrite(path).use { handle ->
1761       handle.source().buffer().use { source ->
1762         assertEquals(data, source.readByteString())
1763       }
1764     }
1765   }
1766 
1767   @Test fun fileHandleLargeArrayWriteAndRead() {
1768     if (isBrowser()) return // This test errors on browsers in CI.
1769 
1770     val path = base / "file-handle-large-array-write-and-read"
1771 
1772     val writtenBytes = randomBytes(1024 * 1024 * 8)
1773     fileSystem.openReadWrite(path).use { handle ->
1774       handle.write(0, writtenBytes.toByteArray(), 0, writtenBytes.size)
1775     }
1776 
1777     val readBytes = fileSystem.openReadWrite(path).use { handle ->
1778       val byteArray = ByteArray(writtenBytes.size)
1779       handle.read(0, byteArray, 0, byteArray.size)
1780       return@use byteArray.toByteString(0, byteArray.size) // Parameters necessary for issue 910.
1781     }
1782 
1783     assertEquals(writtenBytes, readBytes)
1784   }
1785 
1786   @Test fun fileHandleEmptyArrayWriteAndRead() {
1787     val path = base / "file-handle-empty-array-write-and-read"
1788 
1789     val writtenBytes = ByteArray(0)
1790     fileSystem.openReadWrite(path).use { handle ->
1791       handle.write(0, writtenBytes, 0, writtenBytes.size)
1792     }
1793 
1794     val readBytes = fileSystem.openReadWrite(path).use { handle ->
1795       val byteArray = ByteArray(writtenBytes.size)
1796       handle.read(0, byteArray, 0, byteArray.size)
1797       return@use byteArray
1798     }
1799 
1800     assertContentEquals(writtenBytes, readBytes)
1801   }
1802 
1803   @Test fun fileHandleSinkPosition() {
1804     val path = base / "file-handle-sink-position"
1805 
1806     fileSystem.openReadWrite(path).use { handle ->
1807       handle.sink().use { sink ->
1808         sink.write(Buffer().writeUtf8("abcde"), 5)
1809         assertEquals(5, handle.position(sink))
1810         sink.write(Buffer().writeUtf8("fghijklmno"), 10)
1811         assertEquals(15, handle.position(sink))
1812       }
1813 
1814       handle.sink(200).use { sink ->
1815         sink.write(Buffer().writeUtf8("abcde"), 5)
1816         assertEquals(205, handle.position(sink))
1817         sink.write(Buffer().writeUtf8("fghijklmno"), 10)
1818         assertEquals(215, handle.position(sink))
1819       }
1820     }
1821   }
1822 
1823   @Test fun fileHandleBufferedSinkPosition() {
1824     val path = base / "file-handle-buffered-sink-position"
1825 
1826     fileSystem.openReadWrite(path).use { handle ->
1827       handle.sink().buffer().use { sink ->
1828         sink.writeUtf8("abcde")
1829         assertEquals(5, handle.position(sink))
1830         sink.writeUtf8("fghijklmno")
1831         assertEquals(15, handle.position(sink))
1832       }
1833 
1834       handle.sink(200).buffer().use { sink ->
1835         sink.writeUtf8("abcde")
1836         assertEquals(205, handle.position(sink))
1837         sink.writeUtf8("fghijklmno")
1838         assertEquals(215, handle.position(sink))
1839       }
1840     }
1841   }
1842 
1843   @Test fun fileHandleSinkReposition() {
1844     val path = base / "file-handle-sink-reposition"
1845 
1846     fileSystem.openReadWrite(path).use { handle ->
1847       handle.sink().use { sink ->
1848         sink.write(Buffer().writeUtf8("abcdefghij"), 10)
1849         handle.reposition(sink, 5)
1850         assertEquals(5, handle.position(sink))
1851         sink.write(Buffer().writeUtf8("KLM"), 3)
1852         assertEquals(8, handle.position(sink))
1853 
1854         handle.reposition(sink, 200)
1855         sink.write(Buffer().writeUtf8("ABCDEFGHIJ"), 10)
1856         handle.reposition(sink, 205)
1857         assertEquals(205, handle.position(sink))
1858         sink.write(Buffer().writeUtf8("klm"), 3)
1859         assertEquals(208, handle.position(sink))
1860       }
1861 
1862       Buffer().also {
1863         handle.read(fileOffset = 0, sink = it, byteCount = 10)
1864         assertEquals("abcdeKLMij", it.readUtf8())
1865       }
1866 
1867       Buffer().also {
1868         handle.read(fileOffset = 200, sink = it, byteCount = 15)
1869         assertEquals("ABCDEklmIJ", it.readUtf8())
1870       }
1871     }
1872   }
1873 
1874   @Test fun fileHandleBufferedSinkReposition() {
1875     val path = base / "file-handle-buffered-sink-reposition"
1876 
1877     fileSystem.openReadWrite(path).use { handle ->
1878       handle.sink().buffer().use { sink ->
1879         sink.write(Buffer().writeUtf8("abcdefghij"), 10)
1880         handle.reposition(sink, 5)
1881         assertEquals(5, handle.position(sink))
1882         sink.write(Buffer().writeUtf8("KLM"), 3)
1883         assertEquals(8, handle.position(sink))
1884 
1885         handle.reposition(sink, 200)
1886         sink.write(Buffer().writeUtf8("ABCDEFGHIJ"), 10)
1887         handle.reposition(sink, 205)
1888         assertEquals(205, handle.position(sink))
1889         sink.write(Buffer().writeUtf8("klm"), 3)
1890         assertEquals(208, handle.position(sink))
1891       }
1892 
1893       Buffer().also {
1894         handle.read(fileOffset = 0, sink = it, byteCount = 10)
1895         assertEquals("abcdeKLMij", it.readUtf8())
1896       }
1897 
1898       Buffer().also {
1899         handle.read(fileOffset = 200, sink = it, byteCount = 15)
1900         assertEquals("ABCDEklmIJ", it.readUtf8())
1901       }
1902     }
1903   }
1904 
1905   @Test fun fileHandleSourceHappyPath() {
1906     val path = base / "file-handle-source"
1907     fileSystem.write(path) {
1908       writeUtf8("abcdefghijklmnop")
1909     }
1910 
1911     fileSystem.openReadOnly(path).use { handle ->
1912       assertEquals(16L, handle.size())
1913       val buffer = Buffer()
1914 
1915       handle.source().use { source ->
1916         assertEquals(0L, handle.position(source))
1917         assertEquals(4L, source.read(buffer, 4L))
1918         assertEquals("abcd", buffer.readUtf8())
1919         assertEquals(4L, handle.position(source))
1920       }
1921 
1922       handle.source(fileOffset = 8L).use { source ->
1923         assertEquals(8L, handle.position(source))
1924         assertEquals(4L, source.read(buffer, 4L))
1925         assertEquals("ijkl", buffer.readUtf8())
1926         assertEquals(12L, handle.position(source))
1927       }
1928 
1929       handle.source(fileOffset = 16L).use { source ->
1930         assertEquals(16L, handle.position(source))
1931         assertEquals(-1L, source.read(buffer, 4L))
1932         assertEquals("", buffer.readUtf8())
1933         assertEquals(16L, handle.position(source))
1934       }
1935     }
1936   }
1937 
1938   @Test fun fileHandleSourceReposition() {
1939     val path = base / "file-handle-source-reposition"
1940     fileSystem.write(path) {
1941       writeUtf8("abcdefghijklmnop")
1942     }
1943 
1944     fileSystem.openReadOnly(path).use { handle ->
1945       assertEquals(16L, handle.size())
1946       val buffer = Buffer()
1947 
1948       handle.source().use { source ->
1949         handle.reposition(source, 12L)
1950         assertEquals(12L, handle.position(source))
1951         assertEquals(4L, source.read(buffer, 4L))
1952         assertEquals("mnop", buffer.readUtf8())
1953         assertEquals(-1L, source.read(buffer, 4L))
1954         assertEquals("", buffer.readUtf8())
1955         assertEquals(16L, handle.position(source))
1956 
1957         handle.reposition(source, 0L)
1958         assertEquals(0L, handle.position(source))
1959         assertEquals(4L, source.read(buffer, 4L))
1960         assertEquals("abcd", buffer.readUtf8())
1961         assertEquals(4L, handle.position(source))
1962 
1963         handle.reposition(source, 8L)
1964         assertEquals(8L, handle.position(source))
1965         assertEquals(4L, source.read(buffer, 4L))
1966         assertEquals("ijkl", buffer.readUtf8())
1967         assertEquals(12L, handle.position(source))
1968 
1969         handle.reposition(source, 16L)
1970         assertEquals(16L, handle.position(source))
1971         assertEquals(-1L, source.read(buffer, 4L))
1972         assertEquals("", buffer.readUtf8())
1973         assertEquals(16L, handle.position(source))
1974       }
1975     }
1976   }
1977 
1978   @Test fun fileHandleBufferedSourceReposition() {
1979     val path = base / "file-handle-buffered-source-reposition"
1980     fileSystem.write(path) {
1981       writeUtf8("abcdefghijklmnop")
1982     }
1983 
1984     fileSystem.openReadOnly(path).use { handle ->
1985       assertEquals(16L, handle.size())
1986       val buffer = Buffer()
1987 
1988       handle.source().buffer().use { source ->
1989         handle.reposition(source, 12L)
1990         assertEquals(0L, source.buffer.size)
1991         assertEquals(12L, handle.position(source))
1992         assertEquals(4L, source.read(buffer, 4L))
1993         assertEquals(0L, source.buffer.size)
1994         assertEquals("mnop", buffer.readUtf8())
1995         assertEquals(-1L, source.read(buffer, 4L))
1996         assertEquals("", buffer.readUtf8())
1997         assertEquals(16L, handle.position(source))
1998 
1999         handle.reposition(source, 0L)
2000         assertEquals(0L, source.buffer.size)
2001         assertEquals(0L, handle.position(source))
2002         assertEquals(4L, source.read(buffer, 4L))
2003         assertEquals(12L, source.buffer.size) // Buffered bytes accumulated.
2004         assertEquals("abcd", buffer.readUtf8())
2005         assertEquals(4L, handle.position(source))
2006 
2007         handle.reposition(source, 8L)
2008         assertEquals(8L, source.buffer.size) // Buffered bytes preserved.
2009         assertEquals(8L, handle.position(source))
2010         assertEquals(4L, source.read(buffer, 4L))
2011         assertEquals(4L, source.buffer.size)
2012         assertEquals("ijkl", buffer.readUtf8())
2013         assertEquals(12L, handle.position(source))
2014 
2015         handle.reposition(source, 16L)
2016         assertEquals(0L, source.buffer.size)
2017         assertEquals(16L, handle.position(source))
2018         assertEquals(-1L, source.read(buffer, 4L))
2019         assertEquals(0L, source.buffer.size)
2020         assertEquals("", buffer.readUtf8())
2021         assertEquals(16L, handle.position(source))
2022       }
2023     }
2024   }
2025 
2026   @Test fun fileHandleSourceSeekBackwards() {
2027     val path = base / "file-handle-source-backwards"
2028     fileSystem.write(path) {
2029       writeUtf8("abcdefghijklmnop")
2030     }
2031     fileSystem.openReadOnly(path).use { handle ->
2032       assertEquals(16L, handle.size())
2033       val buffer = Buffer()
2034 
2035       handle.source().use { source ->
2036         assertEquals(0L, handle.position(source))
2037         assertEquals(16L, source.read(buffer, 16L))
2038         assertEquals("abcdefghijklmnop", buffer.readUtf8())
2039         assertEquals(16L, handle.position(source))
2040       }
2041 
2042       handle.source(0L).use { source ->
2043         assertEquals(0L, handle.position(source))
2044         assertEquals(16L, source.read(buffer, 16L))
2045         assertEquals("abcdefghijklmnop", buffer.readUtf8())
2046         assertEquals(16L, handle.position(source))
2047       }
2048     }
2049   }
2050 
2051   @Test fun bufferedFileHandleSourceHappyPath() {
2052     val path = base / "file-handle-source"
2053     fileSystem.write(path) {
2054       writeUtf8("abcdefghijklmnop")
2055     }
2056 
2057     fileSystem.openReadOnly(path).use { handle ->
2058       assertEquals(16L, handle.size())
2059       val buffer = Buffer()
2060 
2061       handle.source().buffer().use { source ->
2062         assertEquals(0L, handle.position(source))
2063         assertEquals(4L, source.read(buffer, 4L))
2064         assertEquals("abcd", buffer.readUtf8())
2065         assertEquals(4L, handle.position(source))
2066       }
2067 
2068       handle.source(fileOffset = 8L).buffer().use { source ->
2069         assertEquals(8L, handle.position(source))
2070         assertEquals(4L, source.read(buffer, 4L))
2071         assertEquals("ijkl", buffer.readUtf8())
2072         assertEquals(12L, handle.position(source))
2073       }
2074 
2075       handle.source(fileOffset = 16L).buffer().use { source ->
2076         assertEquals(16L, handle.position(source))
2077         assertEquals(-1L, source.read(buffer, 4L))
2078         assertEquals("", buffer.readUtf8())
2079         assertEquals(16L, handle.position(source))
2080       }
2081     }
2082   }
2083 
2084   @Test fun bufferedFileHandleSourceSeekBackwards() {
2085     val path = base / "file-handle-source-backwards"
2086     fileSystem.write(path) {
2087       writeUtf8("abcdefghijklmnop")
2088     }
2089     fileSystem.openReadOnly(path).use { handle ->
2090       assertEquals(16L, handle.size())
2091       val buffer = Buffer()
2092 
2093       handle.source().buffer().use { source ->
2094         assertEquals(0L, handle.position(source))
2095         assertEquals(16L, source.read(buffer, 16L))
2096         assertEquals("abcdefghijklmnop", buffer.readUtf8())
2097         assertEquals(16L, handle.position(source))
2098       }
2099 
2100       handle.source(0L).buffer().use { source ->
2101         assertEquals(0L, handle.position(source))
2102         assertEquals(16L, source.read(buffer, 16L))
2103         assertEquals("abcdefghijklmnop", buffer.readUtf8())
2104         assertEquals(16L, handle.position(source))
2105       }
2106     }
2107   }
2108 
2109   @Test fun openReadOnlyThrowsOnAttemptToWrite() {
2110     val path = base / "file-handle-source"
2111     fileSystem.write(path) {
2112       writeUtf8("abcdefghijklmnop")
2113     }
2114 
2115     fileSystem.openReadOnly(path).use { handle ->
2116       try {
2117         handle.sink()
2118         fail()
2119       } catch (_: IllegalStateException) {
2120       }
2121 
2122       try {
2123         handle.flush()
2124         fail()
2125       } catch (_: IllegalStateException) {
2126       }
2127 
2128       try {
2129         handle.resize(0L)
2130         fail()
2131       } catch (_: IllegalStateException) {
2132       }
2133 
2134       try {
2135         handle.write(0L, Buffer().writeUtf8("hello"), 5L)
2136         fail()
2137       } catch (_: IllegalStateException) {
2138       }
2139     }
2140   }
2141 
2142   @Test fun openReadOnlyFailsOnAbsentFile() {
2143     val path = base / "file-handle-source"
2144 
2145     try {
2146       fileSystem.openReadOnly(path)
2147       fail()
2148     } catch (_: IOException) {
2149     }
2150   }
2151 
2152   @Test fun openReadWriteCreatesAbsentFile() {
2153     val path = base / "file-handle-source"
2154 
2155     fileSystem.openReadWrite(path).use {
2156     }
2157 
2158     assertEquals("", path.readUtf8())
2159   }
2160 
2161   @Test fun openReadWriteCreatesAbsentFileMustCreate() {
2162     val path = base / "file-handle-source"
2163 
2164     fileSystem.openReadWrite(path, mustCreate = true).use {
2165     }
2166 
2167     assertEquals("", path.readUtf8())
2168   }
2169 
2170   @Test fun openReadWriteMustCreateThrowsIfAlreadyExists() {
2171     val path = base / "file-handle-source"
2172     path.writeUtf8("First!")
2173 
2174     assertFailsWith<IOException> {
2175       fileSystem.openReadWrite(path, mustCreate = true).use {}
2176     }
2177   }
2178 
2179   @Test fun openReadWriteMustExist() {
2180     val path = base / "file-handle-source"
2181     path.writeUtf8("one")
2182 
2183     fileSystem.openReadWrite(path, mustExist = true).use { handle ->
2184       handle.write(3L, Buffer().writeUtf8(" two"), 4L)
2185     }
2186 
2187     assertEquals("one two", path.readUtf8())
2188   }
2189 
2190   @Test fun openReadWriteMustExistThrowsIfAbsent() {
2191     val path = base / "file-handle-source"
2192 
2193     assertFailsWith<IOException> {
2194       fileSystem.openReadWrite(path, mustExist = true).use {}
2195     }
2196   }
2197 
2198   @Test fun openReadWriteThrowsIfBothMustCreateAndMustExist() {
2199     val path = base / "file-handle-source"
2200 
2201     assertFailsWith<IllegalArgumentException> {
2202       fileSystem.openReadWrite(path, mustCreate = true, mustExist = true).use {}
2203     }
2204   }
2205 
2206   @Test fun sinkPositionFailsAfterClose() {
2207     val path = base / "sink-position-fails-after-close"
2208 
2209     fileSystem.openReadWrite(path).use { handle ->
2210       val sink = handle.sink()
2211       sink.close()
2212       try {
2213         handle.position(sink)
2214         fail()
2215       } catch (_: IllegalStateException) {
2216       }
2217       try {
2218         handle.position(sink.buffer())
2219         fail()
2220       } catch (_: IllegalStateException) {
2221       }
2222     }
2223   }
2224 
2225   @Test fun sinkRepositionFailsAfterClose() {
2226     val path = base / "sink-reposition-fails-after-close"
2227 
2228     fileSystem.openReadWrite(path).use { handle ->
2229       val sink = handle.sink()
2230       sink.close()
2231       try {
2232         handle.reposition(sink, 1L)
2233         fail()
2234       } catch (_: IllegalStateException) {
2235       }
2236       try {
2237         handle.reposition(sink.buffer(), 1L)
2238         fail()
2239       } catch (_: IllegalStateException) {
2240       }
2241     }
2242   }
2243 
2244   @Test fun sourcePositionFailsAfterClose() {
2245     val path = base / "source-position-fails-after-close"
2246 
2247     fileSystem.openReadWrite(path).use { handle ->
2248       val source = handle.source()
2249       source.close()
2250       try {
2251         handle.position(source)
2252         fail()
2253       } catch (_: IllegalStateException) {
2254       }
2255       try {
2256         handle.position(source.buffer())
2257         fail()
2258       } catch (_: IllegalStateException) {
2259       }
2260     }
2261   }
2262 
2263   @Test fun sourceRepositionFailsAfterClose() {
2264     val path = base / "source-reposition-fails-after-close"
2265 
2266     fileSystem.openReadWrite(path).use { handle ->
2267       val source = handle.source()
2268       source.close()
2269       try {
2270         handle.reposition(source, 1L)
2271         fail()
2272       } catch (_: IllegalStateException) {
2273       }
2274       try {
2275         handle.reposition(source.buffer(), 1L)
2276         fail()
2277       } catch (_: IllegalStateException) {
2278       }
2279     }
2280   }
2281 
2282   @Test fun sizeFailsAfterClose() {
2283     val path = base / "size-fails-after-close"
2284 
2285     val handle = fileSystem.openReadWrite(path)
2286     handle.close()
2287     try {
2288       handle.size()
2289       fail()
2290     } catch (_: IllegalStateException) {
2291     }
2292   }
2293 
2294   @Test
2295   fun absoluteSymlinkMetadata() {
2296     if (!supportsSymlink()) return
2297 
2298     val target = base / "symlink-target"
2299     val source = base / "symlink-source"
2300 
2301     val minTime = clock.now()
2302     fileSystem.createSymlink(source, target)
2303     val maxTime = clock.now()
2304 
2305     val sourceMetadata = fileSystem.metadata(source)
2306     // Okio's WasiFileSystem only creates relative symlinks.
2307     assertEquals(
2308       when {
2309         isWasiFileSystem -> target.relativeTo(source.parent!!)
2310         else -> target
2311       },
2312       sourceMetadata.symlinkTarget,
2313     )
2314     assertInRange(sourceMetadata.createdAt, minTime, maxTime)
2315   }
2316 
2317   @Test
2318   fun relativeSymlinkMetadata() {
2319     if (!supportsSymlink()) return
2320 
2321     val target = "symlink-target".toPath()
2322     val source = base / "symlink-source"
2323 
2324     val minTime = clock.now()
2325     fileSystem.createSymlink(source, target)
2326     val maxTime = clock.now()
2327 
2328     val sourceMetadata = fileSystem.metadata(source)
2329     assertEquals(target, sourceMetadata.symlinkTarget)
2330     assertInRange(sourceMetadata.createdAt, minTime, maxTime)
2331   }
2332 
2333   @Test
2334   fun createSymlinkSourceAlreadyExists() {
2335     if (!supportsSymlink()) return
2336 
2337     val target = base / "symlink-target"
2338     val source = base / "symlink-source"
2339     source.writeUtf8("hello")
2340     val exception = assertFailsWith<IOException> {
2341       fileSystem.createSymlink(source, target)
2342     }
2343     assertTrue(exception !is FileNotFoundException)
2344   }
2345 
2346   @Test
2347   fun createSymlinkParentDirectoryDoesNotExist() {
2348     if (!supportsSymlink()) return
2349 
2350     val source = base / "no-such-directory" / "source"
2351     val target = base / "target"
2352     val e = assertFailsWith<IOException> {
2353       fileSystem.createSymlink(source, target)
2354     }
2355     assertTrue(e !is FileNotFoundException)
2356   }
2357 
2358   @Test
2359   fun openSymlinkSource() {
2360     if (!supportsSymlink()) return
2361 
2362     val target = base / "symlink-target"
2363     val source = base / "symlink-source"
2364     fileSystem.createSymlink(source, target)
2365     target.writeUtf8("I am the target file")
2366     val sourceContent = fileSystem.source(source).buffer().use { it.readUtf8() }
2367     assertEquals("I am the target file", sourceContent)
2368   }
2369 
2370   @Test
2371   fun openSymlinkSink() {
2372     if (!supportsSymlink()) return
2373     if (isJimFileSystem()) return
2374 
2375     val target = base / "symlink-target"
2376     val source = base / "symlink-source"
2377     fileSystem.createSymlink(source, target)
2378     fileSystem.sink(source).buffer().use {
2379       it.writeUtf8("This writes to the the source file")
2380     }
2381     assertEquals("This writes to the the source file", target.readUtf8())
2382   }
2383 
2384   @Test
2385   fun openFileWithDirectorySymlink() {
2386     if (!supportsSymlink()) return
2387 
2388     val baseA = base / "a"
2389     val baseAA = base / "a" / "a"
2390     val baseB = base / "b"
2391     val baseBA = base / "b" / "a"
2392     fileSystem.createDirectory(baseA)
2393     baseAA.writeUtf8("aa")
2394     fileSystem.createSymlink(baseB, baseA)
2395     assertEquals("aa", baseAA.readUtf8())
2396     assertEquals("aa", baseBA.readUtf8())
2397   }
2398 
2399   @Test
2400   fun openSymlinkFileHandle() {
2401     if (!supportsSymlink()) return
2402 
2403     val target = base / "symlink-target"
2404     val source = base / "symlink-source"
2405     fileSystem.createSymlink(source, target)
2406     target.writeUtf8("I am the target file")
2407     val sourceContent = fileSystem.openReadOnly(source).use { fileHandle ->
2408       fileHandle.source().buffer().use { it.readUtf8() }
2409     }
2410     assertEquals("I am the target file", sourceContent)
2411   }
2412 
2413   @Test
2414   fun listSymlinkDirectory() {
2415     if (!supportsSymlink()) return
2416 
2417     val baseA = base / "a"
2418     val baseAA = base / "a" / "a"
2419     val baseAB = base / "a" / "b"
2420     val baseB = base / "b"
2421     val baseBA = base / "b" / "a"
2422     val baseBB = base / "b" / "b"
2423     fileSystem.createDirectory(baseA)
2424     baseAA.writeUtf8("aa")
2425     baseAB.writeUtf8("ab")
2426     fileSystem.createSymlink(baseB, baseA)
2427     assertEquals(listOf(baseBA, baseBB), fileSystem.list(baseB))
2428   }
2429 
2430   @Test
2431   fun symlinkFileLastAccessedAt() {
2432     if (!supportsSymlink()) return
2433 
2434     val target = base / "symlink-target"
2435     val source = base / "symlink-source"
2436     target.writeUtf8("a")
2437     fileSystem.createSymlink(source, target)
2438     tryAdvanceTime()
2439     val minTime = clock.now()
2440     assertEquals("a", source.readUtf8())
2441     val maxTime = clock.now()
2442     assertInRange(fileSystem.metadata(source).lastAccessedAt, minTime, maxTime)
2443   }
2444 
2445   @Test
2446   fun symlinkDirectoryLastAccessedAt() {
2447     if (!supportsSymlink()) return
2448 
2449     val baseA = base / "a"
2450     val baseAA = base / "a" / "a"
2451     val baseB = base / "b"
2452     val baseBA = base / "b" / "a"
2453     fileSystem.createDirectory(baseA)
2454     baseAA.writeUtf8("aa")
2455     fileSystem.createSymlink(baseB, baseA)
2456     tryAdvanceTime()
2457     val minTime = clock.now()
2458     assertEquals("aa", baseBA.readUtf8())
2459     val maxTime = clock.now()
2460     assertInRange(fileSystem.metadata(baseB).lastAccessedAt, minTime, maxTime)
2461   }
2462 
2463   @Test
2464   fun deleteSymlinkDoesntDeleteTargetFile() {
2465     if (!supportsSymlink()) return
2466 
2467     val target = base / "symlink-target"
2468     val source = base / "symlink-source"
2469     target.writeUtf8("I am the target file")
2470     fileSystem.createSymlink(source, target)
2471     fileSystem.delete(source)
2472     assertEquals("I am the target file", target.readUtf8())
2473   }
2474 
2475   @Test
2476   fun moveSymlinkDoesntMoveTargetFile() {
2477     if (!supportsSymlink()) return
2478 
2479     val target = base / "symlink-target"
2480     val source1 = base / "symlink-source-1"
2481     val source2 = base / "symlink-source-2"
2482     target.writeUtf8("I am the target file")
2483     fileSystem.createSymlink(source1, target)
2484     fileSystem.atomicMove(source1, source2)
2485     assertEquals("I am the target file", target.readUtf8())
2486     assertEquals("I am the target file", source2.readUtf8())
2487     // Okio's WasiFileSystem only creates relative symlinks.
2488     assertEquals(
2489       when {
2490         isWasiFileSystem -> target.relativeTo(source1.parent!!)
2491         else -> target
2492       },
2493       fileSystem.metadata(source2).symlinkTarget,
2494     )
2495   }
2496 
2497   @Test
2498   fun symlinkCanBeRelative() {
2499     if (!supportsSymlink()) return
2500 
2501     val relativeTarget = "symlink-target".toPath()
2502     val absoluteTarget = base / relativeTarget
2503     val source = base / "symlink-source"
2504     absoluteTarget.writeUtf8("I am the target file")
2505     fileSystem.createSymlink(source, relativeTarget)
2506     assertEquals("I am the target file", source.readUtf8())
2507   }
2508 
2509   @Test
2510   fun symlinkCanBeRelativeWithDotDots() {
2511     if (!supportsSymlink()) return
2512 
2513     val relativeTarget = "../b/symlink-target".toPath()
2514     val absoluteTarget = base / "b" / "symlink-target"
2515     val absoluteSource = base / "a" / "symlink-source"
2516     fileSystem.createDirectory(absoluteSource.parent!!)
2517     fileSystem.createDirectory(absoluteTarget.parent!!)
2518     absoluteTarget.writeUtf8("I am the target file")
2519     fileSystem.createSymlink(absoluteSource, relativeTarget)
2520     assertEquals("I am the target file", absoluteSource.readUtf8())
2521   }
2522 
2523   @Test
2524   fun followingRecursiveSymlinksIsOkay() {
2525     if (!supportsSymlink()) return
2526 
2527     val pathA = base / "symlink-a"
2528     val pathB = base / "symlink-b"
2529     val pathC = base / "symlink-c"
2530     val target = base / "symlink-target"
2531     fileSystem.createSymlink(pathA, pathB)
2532     fileSystem.createSymlink(pathB, pathC)
2533     fileSystem.createSymlink(pathC, target)
2534     target.writeUtf8("I am the target file")
2535     assertEquals("I am the target file", pathC.readUtf8())
2536     assertEquals("I am the target file", pathB.readUtf8())
2537     assertEquals("I am the target file", pathA.readUtf8())
2538   }
2539 
2540   @Test
2541   fun symlinkCycle() {
2542     if (!supportsSymlink()) return
2543 
2544     val pathA = base / "symlink-a"
2545     val pathB = base / "symlink-b"
2546     fileSystem.createSymlink(pathA, pathB)
2547     fileSystem.createSymlink(pathB, pathA)
2548     assertFailsWith<IOException> {
2549       pathB.writeUtf8("This should not work")
2550     }
2551     assertFailsWith<IOException> {
2552       pathB.readUtf8()
2553     }
2554   }
2555 
2556   protected fun supportsSymlink(): Boolean {
2557     if (fileSystem.isFakeFileSystem) return fileSystem.allowSymlinks
2558     if (windowsLimitations) return false
2559     return when (fileSystem::class.simpleName) {
2560       "JvmSystemFileSystem",
2561       -> false
2562       else -> true
2563     }
2564   }
2565 
2566   private fun expectIOExceptionOnWindows(
2567     exceptJs: Boolean = false,
2568     block: () -> Unit,
2569   ) {
2570     val expectCrash = windowsLimitations && (!isNodeJsFileSystem || !exceptJs)
2571     try {
2572       block()
2573       assertFalse(expectCrash)
2574     } catch (_: IOException) {
2575       assertTrue(expectCrash)
2576     }
2577   }
2578 
2579   fun Path.readUtf8(): String {
2580     return fileSystem.source(this).buffer().use {
2581       it.readUtf8()
2582     }
2583   }
2584 
2585   fun Path.writeUtf8(string: String) {
2586     fileSystem.sink(this).buffer().use {
2587       it.writeUtf8(string)
2588     }
2589   }
2590 
2591   fun Path.createDirectory() {
2592     fileSystem.createDirectory(this)
2593   }
2594 
2595   /**
2596    * Returns the earliest file system time that could be recorded for an event occurring at this
2597    * instant. This truncates fractional seconds because most host file systems do not use precise
2598    * timestamps for file metadata.
2599    *
2600    * It also pads the result by 200 milliseconds because the host and file system may use different
2601    * clocks, allowing the time on the CPU to drift ahead of the time on the file system.
2602    */
2603   private fun Instant.minFileSystemTime(): Instant {
2604     val paddedInstant = minus(200.milliseconds)
2605     return fromEpochSeconds(paddedInstant.epochSeconds)
2606   }
2607 
2608   /**
2609    * Returns the latest file system time that could be recorded for an event occurring at this
2610    * instant. This adds 2 seconds and truncates fractional seconds because file systems may defer
2611    * assigning the timestamp.
2612    *
2613    * It also pads the result by 200 milliseconds because the host and file system may use different
2614    * clocks, allowing the time on the CPU to drift behind the time on the file system.
2615    *
2616    * https://docs.microsoft.com/en-us/windows/win32/sysinfo/file-times
2617    */
2618   private fun Instant.maxFileSystemTime(): Instant {
2619     val paddedInstant = plus(200.milliseconds)
2620     return fromEpochSeconds(paddedInstant.plus(2.seconds).epochSeconds)
2621   }
2622 
2623   /**
2624    * Attempt to advance the clock so that any already-issued timestamps will not collide with
2625    * timestamps yet to be issued.
2626    */
2627   private fun tryAdvanceTime() {
2628     if (clock is FakeClock) clock.sleep(1.minutes)
2629   }
2630 
2631   private fun assertInRange(sampled: Instant?, minTime: Instant, maxTime: Instant) {
2632     if (sampled == null) return
2633     val minFsTime = minTime.minFileSystemTime()
2634     val maxFsTime = maxTime.maxFileSystemTime()
2635     assertTrue("expected $sampled in $minFsTime..$maxFsTime (relaxed from $minTime..$maxTime)") {
2636       sampled in minFsTime..maxFsTime
2637     }
2638   }
2639 
2640   private fun isJvmFileSystemOnWindows(): Boolean {
2641     return windowsLimitations && fileSystem::class.simpleName == "JvmSystemFileSystem"
2642   }
2643 
2644   private fun isJimFileSystem(): Boolean {
2645     return "JimfsFileSystem" in fileSystem.toString()
2646   }
2647 
2648   private fun isNodeJsFileSystemOnWindows(): Boolean {
2649     return windowsLimitations && fileSystem::class.simpleName == "NodeJsFileSystem"
2650   }
2651 }
2652