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