diff options
Diffstat (limited to 'okio-fakefilesystem/src/commonMain/kotlin/okio/fakefilesystem/FakeFileSystem.kt')
-rw-r--r-- | okio-fakefilesystem/src/commonMain/kotlin/okio/fakefilesystem/FakeFileSystem.kt | 679 |
1 files changed, 528 insertions, 151 deletions
diff --git a/okio-fakefilesystem/src/commonMain/kotlin/okio/fakefilesystem/FakeFileSystem.kt b/okio-fakefilesystem/src/commonMain/kotlin/okio/fakefilesystem/FakeFileSystem.kt index fb426744..fb2bd655 100644 --- a/okio-fakefilesystem/src/commonMain/kotlin/okio/fakefilesystem/FakeFileSystem.kt +++ b/okio-fakefilesystem/src/commonMain/kotlin/okio/fakefilesystem/FakeFileSystem.kt @@ -15,11 +15,15 @@ */ package okio.fakefilesystem +import kotlin.jvm.JvmField +import kotlin.jvm.JvmName +import kotlin.reflect.KClass import kotlinx.datetime.Clock import kotlinx.datetime.Instant +import okio.ArrayIndexOutOfBoundsException import okio.Buffer import okio.ByteString -import okio.ExperimentalFileSystem +import okio.FileHandle import okio.FileMetadata import okio.FileNotFoundException import okio.FileSystem @@ -28,11 +32,11 @@ import okio.Path import okio.Path.Companion.toPath import okio.Sink import okio.Source -import okio.Timeout import okio.fakefilesystem.FakeFileSystem.Element.Directory import okio.fakefilesystem.FakeFileSystem.Element.File -import kotlin.jvm.JvmField -import kotlin.jvm.JvmName +import okio.fakefilesystem.FakeFileSystem.Element.Symlink +import okio.fakefilesystem.FakeFileSystem.Operation.READ +import okio.fakefilesystem.FakeFileSystem.Operation.WRITE /** * A fully in-memory file system useful for testing. It includes features to support writing @@ -41,32 +45,82 @@ import kotlin.jvm.JvmName * Use [openPaths] to see which paths have been opened for read or write, but not yet closed. Tests * should call [checkNoOpenFiles] in `tearDown()` to confirm that no file streams were leaked. * - * By default this file system permits deletion and removal of open files. Configure - * [windowsLimitations] to true to throw an [IOException] when asked to delete or rename an open - * file. + * Strict By Default + * ----------------- + * + * These actions are not allowed and throw an [IOException] if attempted: + * + * * Moving a file that is currently open for reading or writing. + * * Deleting a file that is currently open for reading or writing. + * * Moving a file to a path that currently resolves to an empty directory. + * * Reading and writing the same file at the same time. + * * Opening a file for writing that is already open for writing. + * + * Programs that do not attempt any of the above operations should work fine on both UNIX and + * Windows systems. Relax these constraints individually or call [emulateWindows] or [emulateUnix]; + * to apply the constraints of a particular operating system. */ -@ExperimentalFileSystem class FakeFileSystem( - private val windowsLimitations: Boolean = false, - private val workingDirectory: Path = (if (windowsLimitations) "F:\\".toPath() else "/".toPath()), - @JvmField - val clock: Clock = Clock.System + val clock: Clock = Clock.System, ) : FileSystem() { - init { - require(workingDirectory.isAbsolute) { - "expected an absolute path but was $workingDirectory" - } - } - - /** Keys are canonical paths. Each value is either a [Directory] or a [ByteString]. */ - private val elements = mutableMapOf<Path, Element>() + /** File system roots. Each element is a Directory and is created on-demand. */ + private val roots = mutableMapOf<Path, Directory>() /** Files that are currently open and need to be closed to avoid resource leaks. */ private val openFiles = mutableListOf<OpenFile>() /** + * An absolute path with this file system's current working directory. Relative paths will be + * resolved against this directory when they are used. + */ + var workingDirectory: Path = "/".toPath() + set(value) { + require(value.isAbsolute) { + "expected an absolute path but was $value" + } + field = value + } + + /** + * True to allow files to be moved even if they're currently open for read or write. UNIX file + * systems typically allow open files to be moved; Windows file systems do not. + */ + var allowMovingOpenFiles = false + + /** + * True to allow files to be deleted even if they're currently open for read or write. UNIX file + * systems typically allow open files to be deleted; Windows file systems do not. + */ + var allowDeletingOpenFiles = false + + /** + * True to allow the target of an [atomicMove] operation to be an empty directory. Windows file + * systems typically allow files to replace empty directories; UNIX file systems do not. + */ + var allowClobberingEmptyDirectories = false + + /** + * True to permit a file to have multiple [sinks][sink] open at the same time. Both Windows and + * UNIX file systems permit this but the result may be undefined. + */ + var allowWritesWhileWriting = false + + /** + * True to permit a file to have a [source] and [sink] open at the same time. Both Windows and + * UNIX file systems permit this but the result may be undefined. + */ + var allowReadsWhileWriting = false + + /** + * True to allow symlinks to be created. UNIX file systems typically allow symlinks; Windows file + * systems do not. Setting this to false after creating a symlink does not prevent that symlink + * from being returned or used. + */ + var allowSymlinks = false + + /** * Canonical paths for every file and directory in this file system. This omits file system roots * like `C:\` and `/`. */ @@ -74,9 +128,8 @@ class FakeFileSystem( val allPaths: Set<Path> get() { val result = mutableListOf<Path>() - for (path in elements.keys) { - if (path.isRoot) continue - result += path + for (path in roots.keys) { + result += listRecursively(path) } result.sort() return result.toSet() @@ -115,180 +168,433 @@ class FakeFileSystem( |expected 0 open files, but found: | ${openFiles.joinToString(separator = "\n ") { it.canonicalPath.toString() }} """.trimMargin(), - firstOpenFile.backtrace + firstOpenFile.backtrace, ) } + /** + * Configure this file system to use a Windows-like working directory (`F:\`, unless the working + * directory is already Windows-like) and to follow a Windows-like policy on what operations + * are permitted. + */ + fun emulateWindows() { + if ("\\" !in workingDirectory.toString()) { + workingDirectory = "F:\\".toPath() + } + allowMovingOpenFiles = false + allowDeletingOpenFiles = false + allowClobberingEmptyDirectories = true + allowWritesWhileWriting = true + allowReadsWhileWriting = true + } + + /** + * Configure this file system to use a UNIX-like working directory (`/`, unless the working + * directory is already UNIX-like) and to follow a UNIX-like policy on what operations are + * permitted. + */ + fun emulateUnix() { + if ("/" !in workingDirectory.toString()) { + workingDirectory = "/".toPath() + } + allowMovingOpenFiles = true + allowDeletingOpenFiles = true + allowClobberingEmptyDirectories = false + allowWritesWhileWriting = true + allowReadsWhileWriting = true + allowSymlinks = true + } + override fun canonicalize(path: Path): Path { - val canonicalPath = workingDirectory / path + val canonicalPath = canonicalizeInternal(path) - if (canonicalPath !in elements) { + val lookupResult = lookupPath(canonicalPath) + if (lookupResult?.element == null) { throw FileNotFoundException("no such file: $path") } - return canonicalPath + return lookupResult.path } - override fun metadataOrNull(path: Path): FileMetadata? { - val canonicalPath = workingDirectory / path - var element = elements[canonicalPath] + /** Don't throw [FileNotFoundException] if the path doesn't identify a file. */ + private fun canonicalizeInternal(path: Path): Path { + return workingDirectory.resolve(path, normalize = true) + } - // If the path is a root, create it on demand. - if (element == null && path.isRoot) { - element = Directory(createdAt = clock.now()) - elements[path] = element + /** + * Sets the metadata of type [type] on [path] to [value]. If [value] is null this clears that + * metadata. + * + * Extras are not copied by [copy] but they are moved with [atomicMove]. + * + * @throws IOException if [path] does not exist. + */ + @Throws(IOException::class) + fun <T : Any> setExtra(path: Path, type: KClass<out T>, value: T?) { + val canonicalPath = canonicalizeInternal(path) + val lookupResult = lookupPath( + canonicalPath = canonicalPath, + createRootOnDemand = canonicalPath.isRoot, + resolveLastSymlink = false, + ) + val element = lookupResult?.element ?: throw FileNotFoundException("no such file: $path") + if (value == null) { + element.extras.remove(type) + } else { + element.extras[type] = value } + } - return element?.metadata + override fun metadataOrNull(path: Path): FileMetadata? { + val canonicalPath = canonicalizeInternal(path) + val lookupResult = lookupPath( + canonicalPath = canonicalPath, + createRootOnDemand = canonicalPath.isRoot, + resolveLastSymlink = false, + ) + return lookupResult?.element?.metadata } - override fun list(dir: Path): List<Path> { - val canonicalPath = workingDirectory / dir - val element = requireDirectory(canonicalPath) + override fun list(dir: Path): List<Path> = list(dir, throwOnFailure = true)!! + + override fun listOrNull(dir: Path): List<Path>? = list(dir, throwOnFailure = false) + + private fun list(dir: Path, throwOnFailure: Boolean): List<Path>? { + val canonicalPath = canonicalizeInternal(dir) + val lookupResult = lookupPath(canonicalPath) + if (lookupResult?.element == null) { + if (throwOnFailure) throw FileNotFoundException("no such directory: $dir") else return null + } + val element = lookupResult.element as? Directory + ?: if (throwOnFailure) throw IOException("not a directory: $dir") else return null element.access(now = clock.now()) - val paths = elements.keys.filterTo(mutableListOf()) { it.parent == canonicalPath } - paths.sort() - return paths + return element.children.keys.map { dir / it }.sorted() } override fun source(file: Path): Source { - val canonicalPath = workingDirectory / file - val element = elements[canonicalPath] ?: throw FileNotFoundException("no such file: $file") + val fileHandle = openReadOnly(file) + return fileHandle.source() + .also { fileHandle.close() } + } - if (element !is File) { - throw IOException("not a file: $file") - } + override fun sink(file: Path, mustCreate: Boolean): Sink { + val fileHandle = open(file, readWrite = true, mustCreate = mustCreate) + fileHandle.resize(0L) // If the file already has data, get rid of it. + return fileHandle.sink() + .also { fileHandle.close() } + } - val openFile = OpenFile(canonicalPath, Exception("file opened for reading here")) - openFiles += openFile - element.access(now = clock.now()) - return FakeFileSource(openFile, Buffer().write(element.data)) + override fun appendingSink(file: Path, mustExist: Boolean): Sink { + val fileHandle = open(file, readWrite = true, mustExist = mustExist) + return fileHandle.appendingSink() + .also { fileHandle.close() } } - override fun sink(file: Path): Sink { - return newSink(file, append = false) + override fun openReadOnly(file: Path): FileHandle { + return open(file, readWrite = false) } - override fun appendingSink(file: Path): Sink { - return newSink(file, append = true) + override fun openReadWrite(file: Path, mustCreate: Boolean, mustExist: Boolean): FileHandle { + return open(file, readWrite = true, mustCreate = mustCreate, mustExist = mustExist) } - private fun newSink(file: Path, append: Boolean): Sink { - val canonicalPath = workingDirectory / file + private fun open( + file: Path, + readWrite: Boolean, + mustCreate: Boolean = false, + mustExist: Boolean = false, + ): FileHandle { + require(!mustCreate || !mustExist) { + "Cannot require mustCreate and mustExist at the same time." + } + + val canonicalPath = canonicalizeInternal(file) + val lookupResult = lookupPath(canonicalPath, createRootOnDemand = readWrite) val now = clock.now() + val element: File + val operation: Operation + + if (lookupResult?.element == null && mustExist) { + throw IOException("$file doesn't exist.") + } + if (lookupResult?.element != null && mustCreate) { + throw IOException("$file already exists.") + } - val existing = elements[canonicalPath] - if (existing is Directory) { - throw IOException("destination is a directory: $file") + if (readWrite) { + // Note that this case is used for both write and read/write. + if (lookupResult?.element is Directory) { + throw IOException("destination is a directory: $file") + } + if (!allowWritesWhileWriting) { + findOpenFile(canonicalPath, operation = WRITE)?.let { + throw IOException("file is already open for writing $file", it.backtrace) + } + } + if (!allowReadsWhileWriting) { + findOpenFile(canonicalPath, operation = READ)?.let { + throw IOException("file is already open for reading $file", it.backtrace) + } + } + + val parent = lookupResult?.parent + ?: throw FileNotFoundException("parent directory does not exist") + parent.access(now, true) + + val existing = lookupResult.element + element = File(createdAt = existing?.createdAt ?: now) + parent.children[lookupResult.segment!!] = element + operation = WRITE + + if (existing is File) { + element.data = existing.data + } + } else { + val existing = lookupResult?.element + ?: throw FileNotFoundException("no such file: $file") + element = existing as? File ?: throw IOException("not a file: $file") + operation = READ + + if (!allowReadsWhileWriting) { + findOpenFile(canonicalPath, operation = WRITE)?.let { + throw IOException("file is already open for writing $file", it.backtrace) + } + } } - val parent = requireDirectory(canonicalPath.parent) - parent.access(now, true) - val openFile = OpenFile(canonicalPath, Exception("file opened for writing here")) + element.access(now = clock.now(), modified = readWrite) + + val openFile = OpenFile(canonicalPath, operation, Exception("file opened for $operation here")) openFiles += openFile - val regularFile = File(createdAt = existing?.createdAt ?: now) - val result = FakeFileSink(openFile, regularFile) - if (append && existing is File) { - result.buffer.write(existing.data) - regularFile.data = existing.data - } - elements[canonicalPath] = regularFile - regularFile.access(now = now, modified = true) - return result + + return FakeFileHandle( + readWrite = readWrite, + openFile = openFile, + file = element, + ) } - override fun createDirectory(dir: Path) { - val canonicalPath = workingDirectory / dir + override fun createDirectory(dir: Path, mustCreate: Boolean) { + val canonicalPath = canonicalizeInternal(dir) + + val lookupResult = lookupPath(canonicalPath, createRootOnDemand = true) + + if (canonicalPath.isRoot) { + // Looking it up was sufficient. Don't crash when creating roots that already exist. + return + } - if (elements[canonicalPath] != null) { + if (mustCreate && lookupResult?.element != null) { throw IOException("already exists: $dir") } - requireDirectory(canonicalPath.parent) - elements[canonicalPath] = Directory(createdAt = clock.now()) + val parentDirectory = lookupResult.requireParent() + parentDirectory.children[canonicalPath.nameBytes] = Directory(createdAt = clock.now()) } - override fun atomicMove(source: Path, target: Path) { - val canonicalSource = workingDirectory / source - val canonicalTarget = workingDirectory / target + override fun atomicMove( + source: Path, + target: Path, + ) { + val canonicalSource = canonicalizeInternal(source) + val canonicalTarget = canonicalizeInternal(target) - val targetElement = elements[canonicalTarget] - val sourceElement = elements[canonicalSource] + val targetLookupResult = lookupPath(canonicalTarget, createRootOnDemand = true) + val sourceLookupResult = lookupPath(canonicalSource, resolveLastSymlink = false) // Universal constraints. - if (targetElement is Directory) { + if (targetLookupResult?.element is Directory) { throw IOException("target is a directory: $target") } - requireDirectory(canonicalTarget.parent) - if (windowsLimitations) { - // Windows-only constraints. - openFileOrNull(canonicalSource)?.let { + val targetParent = targetLookupResult.requireParent() + if (!allowMovingOpenFiles) { + findOpenFile(canonicalSource)?.let { throw IOException("source is open $source", it.backtrace) } - openFileOrNull(canonicalTarget)?.let { + findOpenFile(canonicalTarget)?.let { throw IOException("target is open $target", it.backtrace) } - } else { - // UNIX-only constraints. - if (sourceElement is Directory && targetElement is File) { + } + if (!allowClobberingEmptyDirectories) { + if (sourceLookupResult?.element is Directory && targetLookupResult?.element is File) { throw IOException("source is a directory and target is a file") } } - val removed = elements.remove(canonicalSource) + val sourceParent = sourceLookupResult.requireParent() + val removed = sourceParent.children.remove(canonicalSource.nameBytes) ?: throw FileNotFoundException("source doesn't exist: $source") - elements[canonicalTarget] = removed + targetParent.children[canonicalTarget.nameBytes] = removed } - override fun delete(path: Path) { - val canonicalPath = workingDirectory / path + override fun delete(path: Path, mustExist: Boolean) { + val canonicalPath = canonicalizeInternal(path) + + val lookupResult = lookupPath( + canonicalPath = canonicalPath, + createRootOnDemand = true, + resolveLastSymlink = false, + ) - if (elements.keys.any { it.parent == canonicalPath }) { + if (lookupResult?.element == null) { + if (mustExist) { + throw FileNotFoundException("no such file: $path") + } else { + return + } + } + + if (lookupResult.element is Directory && lookupResult.element.children.isNotEmpty()) { throw IOException("non-empty directory") } - if (windowsLimitations) { - openFileOrNull(canonicalPath)?.let { + if (!allowDeletingOpenFiles) { + findOpenFile(canonicalPath)?.let { throw IOException("file is open $path", it.backtrace) } } - if (elements.remove(canonicalPath) == null) { - throw FileNotFoundException("no such file: $path") + val directory = lookupResult.requireParent() + directory.children.remove(canonicalPath.nameBytes) + } + + override fun createSymlink( + source: Path, + target: Path, + ) { + val canonicalSource = canonicalizeInternal(source) + + val existingLookupResult = lookupPath(canonicalSource, createRootOnDemand = true) + if (existingLookupResult?.element != null) { + throw IOException("already exists: $source") } + val parent = existingLookupResult.requireParent() + + if (!allowSymlinks) { + throw IOException("symlinks are not supported") + } + + parent.children[canonicalSource.nameBytes] = Symlink(createdAt = clock.now(), target) } /** - * Gets the directory at [path], creating it if [path] is a file system root. + * Walks the file system looking for [canonicalPath], following symlinks encountered along the + * way. This function is designed to be used both when looking up existing files and when creating + * new files into an existing directory. + * + * It returns either: + * + * * a path lookup result with an element if that file or directory or symlink exists. This is + * useful when reading or writing an existing fie. + * + * * a path lookup result that only got as far as the canonical path's parent, if the parent + * exists but the child file does not. This is useful when creating a new file. * - * @throws IOException if the named directory is not a root and does not exist, or if it does - * exist but is not a directory. + * * null, if not even the parent directory exists. A file cannot yet be created with this path + * because there is no parent to attach it to. + * + * This will create the root of the returned path if it does not exist. + * + * @param canonicalPath a normalized path, typically the result of [FakeFileSystem.canonicalizeInternal]. + * @param recurseCount used internally to detect cycles. + * @param resolveLastSymlink true if the result's element must not itself be a symlink. Use this + * for looking up metadata, or operations that apply to the path like delete and move. We + * always follow symlinks for enclosing directories. + * @param createRootOnDemand true to create a root directory like `C:\` or `/` if it doesn't + * exist. Pass true for mutating operations. */ - private fun requireDirectory(path: Path?): Directory { - if (path == null) throw IOException("directory does not exist") + private fun lookupPath( + canonicalPath: Path, + recurseCount: Int = 0, + resolveLastSymlink: Boolean = true, + createRootOnDemand: Boolean = false, + ): PathLookupResult? { + // 40 is chosen for consistency with the Linux kernel (which previously used 8). + if (recurseCount > 40) { + throw IOException("symlink cycle?") + } - // If the path is a directory, return it! - val element = elements[path] - if (element is Directory) return element + val rootPath = canonicalPath.root!! + var root = roots[rootPath] - // If the path is a root, create a directory for it on demand. - if (path.isRoot) { - val root = Directory(createdAt = clock.now()) - elements[path] = root - return root + // If the path is a root, create it on demand. + if (root == null) { + if (!createRootOnDemand) return null + root = Directory(createdAt = clock.now()) + roots[rootPath] = root } - if (element == null) throw FileNotFoundException("no such directory: $path") + var parent: Directory? = null + var lastSegment: ByteString? = null + var current: Element = root + var currentPath: Path = rootPath + + var segmentsTraversed = 0 + val segments = canonicalPath.segmentsBytes + for (segment in segments) { + lastSegment = segment - throw IOException("not a directory: $path") + // Push the newest segment. + if (current !is Directory) { + throw IOException("not a directory: $currentPath") + } + parent = current + current = current.children[segment] ?: break + currentPath /= segment + segmentsTraversed++ + + // If it's a symlink, recurse to follow it. + val isLastSegment = segmentsTraversed == segments.size + val followSymlinks = !isLastSegment || resolveLastSymlink + if (current is Symlink && followSymlinks) { + current.access(now = clock.now()) + // We wanna normalize it in case the target is relative and starts with `..`. + currentPath = currentPath.parent!!.resolve(current.target, normalize = true) + val symlinkLookupResult = lookupPath( + canonicalPath = currentPath, + recurseCount = recurseCount + 1, + createRootOnDemand = createRootOnDemand, + ) ?: break + parent = symlinkLookupResult.parent + lastSegment = symlinkLookupResult.segment + current = symlinkLookupResult.element ?: break + currentPath = symlinkLookupResult.path + } + } + + return when (segmentsTraversed) { + segments.size -> { + PathLookupResult(currentPath, parent, lastSegment, current) // The file. + } + segments.size - 1 -> { + PathLookupResult(currentPath, parent, lastSegment, null) // The enclosing directory. + } + else -> null // We found nothing. + } + } + + private class PathLookupResult( + /** The canonical path for the looked up path or its enclosing directory. */ + val path: Path, + /** Only null if the looked up path is a root. */ + val parent: Directory?, + /** Only null if the looked up path is a root. */ + val segment: ByteString?, + /** Non-null if this is a root. Also not null if this file exists. */ + val element: Element?, + ) + + private fun PathLookupResult?.requireParent(): Directory { + return this?.parent ?: throw IOException("parent directory does not exist") } private sealed class Element( - val createdAt: Instant + val createdAt: Instant, ) { var lastModifiedAt: Instant = createdAt var lastAccessedAt: Instant = createdAt + val extras = mutableMapOf<KClass<*>, Any>() class File(createdAt: Instant) : Element(createdAt) { var data: ByteString = ByteString.EMPTY @@ -299,21 +605,44 @@ class FakeFileSystem( size = data.size.toLong(), createdAt = createdAt, lastModifiedAt = lastModifiedAt, - lastAccessedAt = lastAccessedAt + lastAccessedAt = lastAccessedAt, + extras = extras, ) } class Directory(createdAt: Instant) : Element(createdAt) { + /** Keys are path segments. */ + val children = mutableMapOf<ByteString, Element>() + override val metadata: FileMetadata get() = FileMetadata( isDirectory = true, createdAt = createdAt, lastModifiedAt = lastModifiedAt, - lastAccessedAt = lastAccessedAt + lastAccessedAt = lastAccessedAt, + extras = extras, + ) + } + + class Symlink( + createdAt: Instant, + /** This may be an absolute or relative path. */ + val target: Path, + ) : Element(createdAt) { + override val metadata: FileMetadata + get() = FileMetadata( + symlinkTarget = target, + createdAt = createdAt, + lastModifiedAt = lastModifiedAt, + lastAccessedAt = lastAccessedAt, + extras = extras, ) } - fun access(now: Instant, modified: Boolean = false) { + fun access( + now: Instant, + modified: Boolean = false, + ) { lastAccessedAt = now if (modified) { lastModifiedAt = now @@ -323,68 +652,116 @@ class FakeFileSystem( abstract val metadata: FileMetadata } - private fun openFileOrNull(canonicalPath: Path): OpenFile? { - return openFiles.firstOrNull { it.canonicalPath == canonicalPath } + private fun findOpenFile( + canonicalPath: Path, + operation: Operation? = null, + ): OpenFile? { + return openFiles.firstOrNull { + it.canonicalPath == canonicalPath && (operation == null || operation == it.operation) + } + } + + private fun checkOffsetAndCount( + size: Long, + offset: Long, + byteCount: Long, + ) { + if (offset or byteCount < 0 || offset > size || size - offset < byteCount) { + throw ArrayIndexOutOfBoundsException("size=$size offset=$offset byteCount=$byteCount") + } } private class OpenFile( val canonicalPath: Path, - val backtrace: Throwable + val operation: Operation, + val backtrace: Throwable, ) - /** Reads data from [buffer], removing itself from [openPathsMutable] when closed. */ - private inner class FakeFileSource( + private enum class Operation { + READ, + WRITE, + } + + private inner class FakeFileHandle( + readWrite: Boolean, private val openFile: OpenFile, - private val buffer: Buffer - ) : Source { + private val file: File, + ) : FileHandle(readWrite) { private var closed = false - override fun read(sink: Buffer, byteCount: Long): Long { + override fun protectedResize(size: Long) { check(!closed) { "closed" } - return buffer.read(sink, byteCount) - } - override fun timeout() = Timeout.NONE + val delta = size - file.data.size + if (delta > 0) { + file.data = Buffer() + .write(file.data) + .write(ByteArray(delta.toInt())) + .readByteString() + } else { + file.data = file.data.substring(0, size.toInt()) + } - override fun close() { - if (closed) return - closed = true - openFiles -= openFile + file.access(now = clock.now(), modified = true) } - override fun toString() = "source(${openFile.canonicalPath})" - } - - /** Writes data to [path]. */ - private inner class FakeFileSink( - private val openFile: OpenFile, - private val file: File - ) : Sink { - val buffer = Buffer() - private var closed = false + override fun protectedSize(): Long { + check(!closed) { "closed" } + return file.data.size.toLong() + } - override fun write(source: Buffer, byteCount: Long) { + override fun protectedRead( + fileOffset: Long, + array: ByteArray, + arrayOffset: Int, + byteCount: Int, + ): Int { check(!closed) { "closed" } - buffer.write(source, byteCount) + checkOffsetAndCount(array.size.toLong(), arrayOffset.toLong(), byteCount.toLong()) + + val fileOffsetInt = fileOffset.toInt() + val toCopy = minOf(file.data.size - fileOffsetInt, byteCount) + if (toCopy <= 0) return -1 + for (i in 0 until toCopy) { + array[i + arrayOffset] = file.data[i + fileOffsetInt] + } + return toCopy } - override fun flush() { + override fun protectedWrite( + fileOffset: Long, + array: ByteArray, + arrayOffset: Int, + byteCount: Int, + ) { check(!closed) { "closed" } + checkOffsetAndCount(array.size.toLong(), arrayOffset.toLong(), byteCount.toLong()) + + val buffer = Buffer() + buffer.write(file.data, 0, minOf(fileOffset.toInt(), file.data.size)) + while (buffer.size < fileOffset) { + buffer.writeByte(0) + } + buffer.write(array, arrayOffset, byteCount) + if (buffer.size < file.data.size) { + buffer.write(file.data, buffer.size.toInt(), file.data.size - buffer.size.toInt()) + } file.data = buffer.snapshot() file.access(now = clock.now(), modified = true) } - override fun timeout() = Timeout.NONE + override fun protectedFlush() { + check(!closed) { "closed" } + } - override fun close() { + override fun protectedClose() { if (closed) return closed = true - file.data = buffer.snapshot() - file.access(now = clock.now(), modified = true) + file.access(now = clock.now(), modified = readWrite) openFiles -= openFile } - override fun toString() = "sink(${openFile.canonicalPath})" + override fun toString() = "FileHandler(${openFile.canonicalPath})" } override fun toString() = "FakeFileSystem" |