From e4753338d60396c00f80b494366b2869f8541944 Mon Sep 17 00:00:00 2001 From: nea Date: Fri, 13 Aug 2021 15:46:45 +0200 Subject: tests und shit --- src/jsMain/kotlin/WebOS.kt | 10 +- src/jsMain/kotlin/io/Path.kt | 2 +- src/jsMain/kotlin/io/files.kt | 257 ++++++++++++++++++++++++++++----------- src/jsTest/kotlin/io/PathTest.kt | 6 +- 4 files changed, 195 insertions(+), 80 deletions(-) (limited to 'src') diff --git a/src/jsMain/kotlin/WebOS.kt b/src/jsMain/kotlin/WebOS.kt index b7c4cfb..f762d3a 100644 --- a/src/jsMain/kotlin/WebOS.kt +++ b/src/jsMain/kotlin/WebOS.kt @@ -1,5 +1,7 @@ -import io.IOHandler +import io.FileService import io.Path +import io.PrimitiveFileService +import io.PrimitiveINode import kotlinx.browser.document import kotlinx.browser.window import org.w3c.dom.Element @@ -69,7 +71,7 @@ class Console(val os: WebOS, val renderElement: Element?) { class WebOS { private val _consoles = mutableListOf() val consoles get() = _consoles.toList() - val files = IOHandler() + val files: FileService = PrimitiveFileService() fun registerConsole(element: Element) { _consoles.add(Console(this, element)) } @@ -77,6 +79,6 @@ class WebOS { data class User( val name: String, - val homeDirectory: Path.Absolute + val homeDirectory: Path.Absolute, + val isRoot: Boolean = false, ) - diff --git a/src/jsMain/kotlin/io/Path.kt b/src/jsMain/kotlin/io/Path.kt index 8f77203..f486581 100644 --- a/src/jsMain/kotlin/io/Path.kt +++ b/src/jsMain/kotlin/io/Path.kt @@ -68,7 +68,7 @@ sealed interface Path { } partList.add(part) } - Relative(List(returns) { "" } + partList) + Relative(List(returns) { ".." } + partList) } } } diff --git a/src/jsMain/kotlin/io/files.kt b/src/jsMain/kotlin/io/files.kt index d37035d..9e3e6a0 100644 --- a/src/jsMain/kotlin/io/files.kt +++ b/src/jsMain/kotlin/io/files.kt @@ -2,89 +2,202 @@ package io import User -class IOHandler { - val mounts = mutableListOf() - fun mount(absolutePath: Path.Absolute, fileSystem: FileSystem) { - if (mounts.any { it.mountPoint == absolutePath }) - return // TODO sensible error message handling - mounts += Mount(absolutePath, fileSystem) - } - - fun unmount(mountPoint: Path.Absolute) { - mounts.removeAll { it.mountPoint == mountPoint } - } - - fun findMountFor( - workingDirectory: Path.Absolute, - path: Path, - operation: FileSystem.(relativePath: Path) -> T - ): T { - val absolutPath = path.toAbsolutePath(workingDirectory) - val mount = mounts.filter { - it.mountPoint.parts.zip(absolutPath.parts).all { (a, b) -> a == b } - }.maxByOrNull { it.mountPoint.parts.size } ?: throw IllegalStateException("No mount present") - return mount.fileSystem.operation( - Path.Absolute( - absolutPath.parts.subList( - mount.mountPoint.parts.size, - absolutPath.parts.size - ) - ) // TODO: unangenehm - ) - } - - fun findINode(absolutePath: Path.Absolute): INode { - val mount = mounts.filter { - it.mountPoint.parts.zip(absolutePath.parts).all { (a, b) -> a == b } - }.maxByOrNull { it.mountPoint.parts.size } ?: throw IllegalStateException("No mount present") - val iNode = mount.fileSystem.getINode(absolutePath.relativize(mount.mountPoint)) - return when (iNode) { - is INodeResult.File -> iNode.op - is INodeResult.ResolveAgain -> findINode(absolutePath.resolve(iNode.relativeToOriginal)) - } +sealed interface CreateFileResult { + object Created : CreateFileResult + sealed interface Failure : CreateFileResult { + object NoPermission : Failure + object NoParent : Failure + object AlreadyExists : Failure + } +} + +sealed interface WriteFileResult { + object Written : WriteFileResult + sealed interface Failure : WriteFileResult { + object NoPermission : Failure + object NotAFile : Failure + data class NotEnoughSpace( + val dataSize: Long, + val spaceLeft: Long + ) : Failure } +} - fun read(workingDirectory: Path.Absolute, path: Path): ReadResult = - findMountFor(workingDirectory, path) { read(it) } +sealed interface ReadFileResult { + data class Read(val data: ByteArray) : ReadFileResult { + override fun equals(other: Any?): Boolean = (other as? Read)?.let { it.data.contentEquals(this.data) } ?: false - fun write(workingDirectory: Path.Absolute, path: Path, data: ByteArray): Unit = - findMountFor(workingDirectory, path) { write(it, data) } + override fun hashCode(): Int { + return data.contentHashCode() + } + } - fun stat(workingDirectory: Path.Absolute, path: Path): Unit = - findMountFor(workingDirectory, path) { stat(it) } + sealed interface Failure : ReadFileResult { + object NoPermission : Failure + object NotAFile : Failure + } } -interface INode { - val fs: FileSystem +sealed interface DeleteFileResult { + sealed interface Failure : DeleteFileResult { + object NoPermission : Failure + object NotAFile : Failure + } + + object Deleted : DeleteFileResult } -sealed interface INodeResult { - class File(val op: INode) : INodeResult - class ResolveAgain(val relativeToOriginal: Path): INodeResult +interface FileService { + fun getPath(iNode: INode): Path.Absolute + fun getINode(path: Path.Absolute): INode + fun createFile(iNode: INode, user: User): CreateFileResult + fun createSymlink(iNode: INode, user: User, path: Path): CreateFileResult + fun createDirectory(iNode: INode, user: User): CreateFileResult + fun writeToFile(iNode: INode, user: User, data: ByteArray): WriteFileResult + fun readFromFile(iNode: INode, user: User): ReadFileResult + fun deleteFile(iNode: INode, user: User): DeleteFileResult + fun exists(iNode: INode): Boolean + fun isFile(iNode: INode): Boolean + fun isDirectory(iNode: INode): Boolean + fun isSymlink(iNode: INode): Boolean + fun resolve(iNode: INode, fragment: String): INode + fun resolve(iNode: INode, path: Path.Relative): INode + fun changePermissions(iNode: INode, user: User, permissionUpdate: Map) } -interface FileSystem { - fun getINode(relativePath: Path.Relative): INodeResult - fun read(relativePath: Path): ReadResult - fun write(path: Path, data: ByteArray): Unit // Write result - fun stat(path: Path): Unit // TODO io.Stat result +data class Permission( + val read: Boolean, + val write: Boolean, + val execute: Boolean +) { + companion object { + val default get() = Permission(read = true, write = true, execute = false) + } } -sealed class ReadResult { - class Success(val text: String) : ReadResult() - object NotFound : ReadResult() - object NoAccess : ReadResult() +sealed interface PrimitiveStorageBlob { + val permissions: MutableMap + + class File(var data: ByteArray, override val permissions: MutableMap) : PrimitiveStorageBlob + class Symlink(val path: Path, override val permissions: MutableMap) : PrimitiveStorageBlob + class Directory(override val permissions: MutableMap) : PrimitiveStorageBlob } -data class Mount( - val mountPoint: Path.Absolute, - val fileSystem: FileSystem -) - -data class Stat( - val exists: Boolean, - var owner: User, - val created: Long, - val edited: Long, - val size: Long -) +data class PrimitiveINode internal constructor(internal val internalPath: String) +class PrimitiveFileService : FileService { + private val storageBlobs = mutableMapOf( + "/" to PrimitiveStorageBlob.Directory(mutableMapOf()) + ) + + override fun getPath(iNode: PrimitiveINode): Path.Absolute = Path.of(iNode.internalPath) as Path.Absolute + + override fun getINode(path: Path.Absolute): PrimitiveINode { + return resolve(PrimitiveINode("/"), Path.Relative(path.parts)) + } + + override fun resolve(iNode: PrimitiveINode, fragment: String): PrimitiveINode { + if (fragment == "..") { + val up = iNode.internalPath.substringBeforeLast('/') + if (up.isEmpty()) return PrimitiveINode("/") + return PrimitiveINode(up) + } + if (fragment.isEmpty() || fragment == ".") + return iNode + val blob = storageBlobs[iNode.internalPath] + return when (blob) { + is PrimitiveStorageBlob.Symlink -> { + when (blob.path) { + is Path.Absolute -> getINode(blob.path) + is Path.Relative -> resolve(resolve(iNode, ".."), blob.path) + } + } + else -> { + PrimitiveINode(iNode.internalPath + "/" + fragment) + } + } + } + + override fun resolve(iNode: PrimitiveINode, path: Path.Relative): PrimitiveINode = + path.parts.fold(iNode) { node, fragment -> resolve(node, fragment) } + + private fun getStorageBlob(iNode: PrimitiveINode): PrimitiveStorageBlob? = storageBlobs[iNode.internalPath] + + override fun writeToFile(iNode: PrimitiveINode, user: User, data: ByteArray): WriteFileResult { + val file = getStorageBlob(iNode) as? PrimitiveStorageBlob.File ?: return WriteFileResult.Failure.NotAFile + if (!hasPermission(user, file) { read }) + return WriteFileResult.Failure.NoPermission + file.data = data + return WriteFileResult.Written + } + + override fun readFromFile(iNode: PrimitiveINode, user: User): ReadFileResult { + val file = getStorageBlob(iNode) as? PrimitiveStorageBlob.File ?: return ReadFileResult.Failure.NotAFile + if (!hasPermission(user, file) { read }) + return ReadFileResult.Failure.NoPermission + return ReadFileResult.Read(file.data) + } + + override fun exists(iNode: PrimitiveINode): Boolean = + getStorageBlob(iNode) != null + + override fun isFile(iNode: PrimitiveINode): Boolean = + getStorageBlob(iNode) is PrimitiveStorageBlob.File + + override fun isDirectory(iNode: PrimitiveINode): Boolean = + getStorageBlob(iNode) is PrimitiveStorageBlob.Directory + + override fun isSymlink(iNode: PrimitiveINode): Boolean = + getStorageBlob(iNode) is PrimitiveStorageBlob.Symlink + + override fun changePermissions(iNode: PrimitiveINode, user: User, permissionUpdate: Map) { + val file = getStorageBlob(iNode) ?: return // TODO Results + if (!hasPermission(user, file) { write }) + return // TODO Results + file.permissions.putAll(permissionUpdate) + return // TODO Results + } + + override fun deleteFile(iNode: PrimitiveINode, user: User): DeleteFileResult { + val file = getStorageBlob(iNode) ?: return DeleteFileResult.Failure.NotAFile + if (!hasPermission(user, file) { write }) + return DeleteFileResult.Failure.NoPermission + (storageBlobs.keys.filter { it.startsWith(iNode.internalPath + "/") } + listOf(iNode.internalPath)).forEach { + storageBlobs.remove(it) + } + return DeleteFileResult.Deleted + } + + private fun hasPermission(user: User, blob: PrimitiveStorageBlob, check: Permission.() -> Boolean): Boolean { + return user.isRoot || blob.permissions[user]?.let(check) ?: false + } + + private fun checkCreationPreconditions(iNode: PrimitiveINode, user: User): CreateFileResult? { + if (storageBlobs.containsKey(iNode.internalPath)) return CreateFileResult.Failure.AlreadyExists + val parent = getStorageBlob(resolve(iNode, "..")) + if (parent !is PrimitiveStorageBlob.Directory) return CreateFileResult.Failure.NoParent + if (hasPermission(user, parent) { write }) return CreateFileResult.Failure.NoPermission + return null + } + + override fun createFile(iNode: PrimitiveINode, user: User): CreateFileResult { + val preconditions = checkCreationPreconditions(iNode, user) + if (preconditions != null) return preconditions + storageBlobs[iNode.internalPath] = PrimitiveStorageBlob.File(byteArrayOf(), mutableMapOf(user to Permission.default)) + return CreateFileResult.Created + } + + override fun createSymlink(iNode: PrimitiveINode, user: User, path: Path): CreateFileResult { + val preconditions = checkCreationPreconditions(iNode, user) + if (preconditions != null) return preconditions + storageBlobs[iNode.internalPath] = PrimitiveStorageBlob.Symlink(path, mutableMapOf(user to Permission.default)) + return CreateFileResult.Created + } + + override fun createDirectory(iNode: PrimitiveINode, user: User): CreateFileResult { + val preconditions = checkCreationPreconditions(iNode, user) + if (preconditions != null) return preconditions + storageBlobs[iNode.internalPath] = PrimitiveStorageBlob.Directory(mutableMapOf(user to Permission.default)) + return CreateFileResult.Created + } + +} diff --git a/src/jsTest/kotlin/io/PathTest.kt b/src/jsTest/kotlin/io/PathTest.kt index 73667a6..8ea577c 100644 --- a/src/jsTest/kotlin/io/PathTest.kt +++ b/src/jsTest/kotlin/io/PathTest.kt @@ -1,7 +1,7 @@ package io +import io.kotest.assertions.assertSoftly import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.booleans.shouldBeFalse import io.kotest.matchers.types.shouldBeTypeOf class PathTest : FunSpec({ @@ -16,7 +16,7 @@ class PathTest : FunSpec({ Path.ofShell("a", "b", userHome = homeDir), Path.ofShell(listOf("a", "b"), userHome = homeDir), ).forEach { - it.shouldBeTypeOf() + assertSoftly(it) { shouldBeTypeOf() } } } test("recognize absolute paths as such") { @@ -25,7 +25,7 @@ class PathTest : FunSpec({ Path.of("/"), Path.ofShell("/b/c", userHome = homeDir), ).forEach { - it.shouldBeTypeOf() + assertSoftly(it) { shouldBeTypeOf() } } } }) -- cgit