From 5042498c8fe12751c1b6560ae1fde07e7cd78259 Mon Sep 17 00:00:00 2001 From: nea Date: Fri, 20 Aug 2021 15:33:32 +0200 Subject: aaaaaaaa --- build.gradle.kts | 31 +-- gradle.properties | 2 + src/jsMain/kotlin/WebOS.kt | 345 ------------------------------ src/jsMain/kotlin/io/Path.kt | 78 ------- src/jsMain/kotlin/io/files.kt | 203 ------------------ src/jsMain/kotlin/util/sequence.kt | 5 - src/jsMain/resources/index.html | 17 -- src/jsTest/kotlin/ProjectConfig.kt | 3 - src/jsTest/kotlin/io/FileServiceTest.kt | 59 ----- src/jsTest/kotlin/io/PathTest.kt | 104 --------- src/main/kotlin/WebOS.kt | 366 ++++++++++++++++++++++++++++++++ src/main/kotlin/asyncio.kt | 61 ++++++ src/main/kotlin/io/Path.kt | 78 +++++++ src/main/kotlin/io/files.kt | 203 ++++++++++++++++++ src/main/kotlin/util/sequence.kt | 5 + src/main/resources/index.html | 17 ++ src/test/kotlin/ProjectConfig.kt | 3 + src/test/kotlin/io/FileServiceTest.kt | 59 +++++ src/test/kotlin/io/PathTest.kt | 104 +++++++++ 19 files changed, 916 insertions(+), 827 deletions(-) create mode 100644 gradle.properties delete mode 100644 src/jsMain/kotlin/WebOS.kt delete mode 100644 src/jsMain/kotlin/io/Path.kt delete mode 100644 src/jsMain/kotlin/io/files.kt delete mode 100644 src/jsMain/kotlin/util/sequence.kt delete mode 100644 src/jsMain/resources/index.html delete mode 100644 src/jsTest/kotlin/ProjectConfig.kt delete mode 100644 src/jsTest/kotlin/io/FileServiceTest.kt delete mode 100644 src/jsTest/kotlin/io/PathTest.kt create mode 100644 src/main/kotlin/WebOS.kt create mode 100644 src/main/kotlin/asyncio.kt create mode 100644 src/main/kotlin/io/Path.kt create mode 100644 src/main/kotlin/io/files.kt create mode 100644 src/main/kotlin/util/sequence.kt create mode 100644 src/main/resources/index.html create mode 100644 src/test/kotlin/ProjectConfig.kt create mode 100644 src/test/kotlin/io/FileServiceTest.kt create mode 100644 src/test/kotlin/io/PathTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 99c6139..c657335 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - kotlin("multiplatform") version "1.5.21" + kotlin("js") version "1.5.21" id("io.kotest.multiplatform") version "5.0.0.3" id("com.bnorm.power.kotlin-power-assert") version "0.10.0" } @@ -14,24 +14,19 @@ val kotestVersion: String by project configure { functions = listOf("kotlin.assert", "kotlin.test.assertTrue", "kotlin.test.assertFalse", "kotlin.test.assertEquals") } - kotlin { - targets { - js(IR) { - nodejs { } - browser { - webpackTask { - output.libraryTarget = "umd" - } - testTask { useMocha() } - } + js(IR) { + binaries.executable() + useCommonJs() + browser { + testTask { useMocha() } } } sourceSets { - val jsMain by getting { + val main by getting { } - val jsTest by getting { + val test by getting { dependencies { implementation("io.kotest:kotest-assertions-core:5.0.0.376-SNAPSHOT") implementation("io.kotest:kotest-framework-api:5.0.0.376-SNAPSHOT") @@ -40,3 +35,13 @@ kotlin { } } } + +tasks.withType() { + kotlinOptions { + moduleKind = "commonjs" + sourceMap = true + sourceMapEmbedSources = "always" + freeCompilerArgs += "-Xopt-in=kotlin.contracts.ExperimentalContracts" + + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..6890d45 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,2 @@ +kotlin.js.webpack.major.version=4 + diff --git a/src/jsMain/kotlin/WebOS.kt b/src/jsMain/kotlin/WebOS.kt deleted file mode 100644 index 4ad2f86..0000000 --- a/src/jsMain/kotlin/WebOS.kt +++ /dev/null @@ -1,345 +0,0 @@ -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 -import org.w3c.dom.Node -import org.w3c.dom.asList -import org.w3c.dom.events.Event -import org.w3c.dom.events.KeyboardEvent -import kotlin.properties.Delegates - -fun main() { - console.log("Hello from Kotlin") - val webos = WebOS() - document.body?.addEventListener("load", { - document.body?.querySelectorAll(".webosconsole")?.asList()?.forEach { - if (it !is Element) return@forEach - webos.registerConsole(it) - } - }) -} - -data class CharacterRun(val text: String, val color: String) - -abstract class Activity(val console: Console) { - - open fun handleKeyPress(key: Key) {} - open fun update() {} - abstract fun render(columns: Int, rows: Int): List> -} - -class ShellRunner(console: Console) : Activity(console) { - - val history = mutableListOf() - - val shellProgramStack = ArrayDeque() - - var lastLine = "" - - fun newLine() { - history.add(lastLine) - lastLine = "" - console.invalidateRender() - } - - fun println(it: String = "") { - print(it) - newLine() - } - - fun print(it: String) { - lastLine += it - console.invalidateRender() - } - - override fun update() { - shellProgramStack.lastOrNull()?.update() - } - - fun getInput(): String? { - if (inputBuffer.contains("\n")) { - val r = inputBuffer.substringBefore("\n") - inputBuffer = inputBuffer.substringAfter("\n") - return r - } - return null - } - - override fun render(columns: Int, rows: Int): List> = - history.map { listOf(CharacterRun(it.take(columns), "white")) }.takeLast(rows - 1) + listOf( - listOf( - CharacterRun( - lastLine + inputBuffer, - "white" - ) - ) - ) - - var inputBuffer = "" - - override fun handleKeyPress(key: Key) = when (key) { - is Key.Printable -> { - inputBuffer += key.char - } - is Key.Enter -> { - inputBuffer += "\n" - } - else -> Unit - } - - fun openShellProgram(program: (ShellRunner) -> T): ShellRunner = openShellProgram(program(this)) - - fun openShellProgram(program: T): ShellRunner { - shellProgramStack.addLast(program) - return this - } - -} - -abstract class ShellProgram(val shellRunner: ShellRunner) { - - abstract fun update(): Unit -} - -sealed class Key { - object Alt : Key() - object AltGraph : Key() - object CapsLock : Key() - object Control : Key() - object Function : Key() - object FunctionLock : Key() - object Hyper : Key() - object Meta : Key() - object NumLock : Key() - object Shift : Key() - object Super : Key() - object Symbol : Key() - object SymbolLock : Key() - object Enter : Key() - object Tab : Key() - sealed class Arrow : Key() { - object Down : Key() - object Left : Key() - object Right : Key() - object Up : Key() - } - - object End : Key() - object Home : Key() - object PageUp : Key() - object PageDown : Key() - object Backspace : Key() - object Clear : Key() - object Copy : Key() - object CrSel : Key() - object Cut : Key() - object Delete : Key() - object EraseEof : Key() - object ExSel : Key() - object Insert : Key() - object Paste : Key() - object Redo : Key() - object Undo : Key() - object Accept : Key() - object Again : Key() - object Attn : Key() - object Cancel : Key() - object ContextMenu : Key() - object Escape : Key() - object Execute : Key() - object Find : Key() - object Finish : Key() - object Help : Key() - class Printable(val char: Char) : Key() - class FunctionN(val n: Int) : Key() - companion object { - - fun from(string: String) = when (string) { - "Alt" -> Alt - "AltGraph" -> AltGraph - "CapsLock" -> CapsLock - "Control" -> Control - "Fn" -> Function - "FnLock" -> FunctionLock - "Hyper" -> Hyper - "Meta" -> Meta - "NumLock" -> NumLock - "Shift" -> Shift - "Super" -> Super - "Symbol" -> Symbol - "SymbolLock" -> SymbolLock - "Enter" -> Enter - "Tab" -> Tab - "Down" -> Arrow.Down - "Left" -> Arrow.Left - "Right" -> Arrow.Right - "Up" -> Arrow.Up - "End" -> End - "Home" -> Home - "PageUp" -> PageUp - "PageDown" -> PageDown - "Backspace" -> Backspace - "Clear" -> Clear - "Copy" -> Copy - "CrSel" -> CrSel - "Cut" -> Cut - "Delete" -> Delete - "EraseEof" -> EraseEof - "ExSel" -> ExSel - "Insert" -> Insert - "Paste" -> Paste - "Redo" -> Redo - "Undo" -> Undo - "Accept" -> Accept - "Again" -> Again - "Attn" -> Attn - "Cancel" -> Cancel - "ContextMenu" -> ContextMenu - "Escape" -> Escape - "Execute" -> Execute - "Find" -> Find - "Finish" -> Finish - "Help" -> Help - else -> if (string.length == 1) - Printable(string.first()) - else if (string.first() == 'F') - FunctionN(string.substring(1).toInt()) - else throw TODO() - } - } -} - -class Login(shellRunner: ShellRunner) : ShellProgram(shellRunner) { - - override fun update() { - while (true) - when (state) { - 0 -> { - shellRunner.print("Username: ") - state = 1 - } - 1 -> { - val inp = shellRunner.getInput() ?: return - shellRunner.println(inp) - shellRunner.print("Password: ") - username = inp - state = 2 - } - 2 -> { - val inp = shellRunner.getInput() ?: return - shellRunner.println(inp) - shellRunner.println("Login complete)") - password = inp - // TODO: check password and set user - state = 3 - } - 3 -> { - TODO() - } - } - - } - - var state = 0 - var username = "" - var password = "" - -} - -class Console(val os: WebOS, val renderElement: Element?) { - - val isVirtual get() = renderElement == null - val activityStack = ArrayDeque() - - var columns: Int = 80 - var rows: Int = 46 - - var shouldRerender = true - - var currentUser: User? = null - - private var _workingDirectory: Path.Absolute? = null - - var workingDirectory: Path.Absolute - get() = _workingDirectory ?: currentUser?.homeDirectory ?: Path.root - set(value) { - _workingDirectory = value - } - - init { - renderElement?.addEventListener("keydown", ::computeKeypressEvent) - } - - fun computeKeypressEvent(event: Event) { - if (!isInFocus()) return - if (event !is KeyboardEvent) return - activityStack.lastOrNull()?.handleKeyPress(Key.from(event.key)) - } - - private fun isInFocus(): Boolean = renderElement.containsOrIs(document.activeElement) - - private fun Node?.containsOrIs(node: Node?) = this == node || this?.contains(node) ?: false - - fun openActivity(activity: (Console) -> T): Console = openActivity(activity(this)) - - fun openActivity(activity: T): Console { - activityStack.addLast(activity) - invalidateRender() - return this - } - - fun render() { - if (renderElement == null) return - if (!shouldRerender) return - shouldRerender = false - val x = activityStack.lastOrNull()?.render(columns, rows) - console.log(x) - } - - fun invalidateRender() { - shouldRerender = true - window.requestAnimationFrame { render() } - } - - fun resize(newColumns: Int, newRows: Int) { - invalidateRender() - } - - // TODO: Handle resizes of the renderElement - - fun update(): Unit { - activityStack.lastOrNull()?.update() - } - - var updateInterval by Delegates.notNull() - - fun start(): Console { - updateInterval = window.setInterval(::update, 10) - return openActivity(ShellRunner(this).also { it.openShellProgram(Login(it)) }) - } - - fun destroy() { - window.clearInterval(updateInterval) - } - -} - -class WebOS { - - val hostname: String = "host" - private val _consoles = mutableListOf() - val consoles get() = _consoles.toList() - val files: FileService = PrimitiveFileService() - fun registerConsole(element: Element) { - _consoles.add(Console(this, element)) - } -} - -data class User( - val name: String, - val homeDirectory: Path.Absolute, - val isRoot: Boolean = false, -) diff --git a/src/jsMain/kotlin/io/Path.kt b/src/jsMain/kotlin/io/Path.kt deleted file mode 100644 index 241c559..0000000 --- a/src/jsMain/kotlin/io/Path.kt +++ /dev/null @@ -1,78 +0,0 @@ -package io - -sealed class Path { - abstract val parts: List - fun toAbsolutePath(relativeTo: Absolute): Absolute { - return relativeTo.resolve(this) - } - - abstract fun resolve(path: Path): Path - - abstract val stringPath: String - - companion object { - val root = Absolute(listOf()) - - fun ofShell(string: String, userHome: Absolute): Path = - ofShell(string.split("/"), userHome) - - fun ofShell(vararg parts: String, userHome: Absolute): Path = - ofShell(parts.toList(), userHome) - - fun of(vararg parts: String): Path = - of(parts.toList()) - - fun of(string: String): Path = - of(string.split("/")) - - fun ofShell(parts: List, userHome: Absolute): Path { - if (parts.firstOrNull() == "~") - return userHome.resolve(Relative(parts.subList(1, parts.size).filter { it.isNotEmpty() })) - return of(parts) - } - - fun of(parts: List): Path { - if (parts.isEmpty()) - return root - if (parts[0] == "") // Starts with a / - return Absolute(parts.subList(1, parts.size).filter { it.isNotEmpty() }) - return Relative(parts.filter { it.isNotEmpty() }) - } - } - - data class Relative internal constructor(override val parts: List) : Path() { - override fun resolve(path: Path): Path { - if (path is Absolute) return path - return Relative(this.parts + path.parts) - } - - override val stringPath: String get() = parts.joinToString("/") - } - - data class Absolute internal constructor(override val parts: List) : Path() { - override fun resolve(path: Path): Absolute { - if (path is Absolute) return path - return Absolute(this.parts + path.parts) - } - - override val stringPath: String get() = "/" + parts.joinToString("/") - - fun relativize(path: Path): Relative = when (path) { - is Relative -> path - is Absolute -> { - var idx = 0 - while (idx < path.parts.size && idx < parts.size && path.parts[idx] == parts[idx]) { - idx++ - } - val returns = if (idx < parts.size) { - parts.size - idx - } else { - 0 - } - Relative(List(returns) { ".." } + path.parts.subList(idx, path.parts.size)) - } - } - } - - override fun toString(): String = "Path($stringPath)" -} diff --git a/src/jsMain/kotlin/io/files.kt b/src/jsMain/kotlin/io/files.kt deleted file mode 100644 index 4c53c4e..0000000 --- a/src/jsMain/kotlin/io/files.kt +++ /dev/null @@ -1,203 +0,0 @@ -package io - -import User - -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 - } -} - -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 - - override fun hashCode(): Int { - return data.contentHashCode() - } - } - - sealed interface Failure : ReadFileResult { - object NoPermission : Failure - object NotAFile : Failure - } -} - -sealed interface DeleteFileResult { - sealed interface Failure : DeleteFileResult { - object NoPermission : Failure - object NotAFile : Failure - } - - object Deleted : DeleteFileResult -} - -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) -} - -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 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 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/jsMain/kotlin/util/sequence.kt b/src/jsMain/kotlin/util/sequence.kt deleted file mode 100644 index 72b07dc..0000000 --- a/src/jsMain/kotlin/util/sequence.kt +++ /dev/null @@ -1,5 +0,0 @@ -package util - -fun Iterable.expandWith(t: T): Sequence = - this.asSequence() + generateSequence { t }.asSequence() - diff --git a/src/jsMain/resources/index.html b/src/jsMain/resources/index.html deleted file mode 100644 index 562ee45..0000000 --- a/src/jsMain/resources/index.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - WebOs - - - -
-
-
- - - diff --git a/src/jsTest/kotlin/ProjectConfig.kt b/src/jsTest/kotlin/ProjectConfig.kt deleted file mode 100644 index faa4d69..0000000 --- a/src/jsTest/kotlin/ProjectConfig.kt +++ /dev/null @@ -1,3 +0,0 @@ -import io.kotest.core.config.AbstractProjectConfig - -class ProjectConfig : AbstractProjectConfig() diff --git a/src/jsTest/kotlin/io/FileServiceTest.kt b/src/jsTest/kotlin/io/FileServiceTest.kt deleted file mode 100644 index 9c9c8b9..0000000 --- a/src/jsTest/kotlin/io/FileServiceTest.kt +++ /dev/null @@ -1,59 +0,0 @@ -package io - -import User -import io.kotest.core.spec.style.FunSpec -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -class FileServiceTest : FunSpec({ - generateTests("Primitive", ::PrimitiveFileService) -}) - -fun FunSpec.generateTests(name: String, provider: () -> FileService) { - val aPath = Path.of("/a") as Path.Absolute - val bPath = Path.of("/a/b") as Path.Absolute - val homePath = Path.of("/roothome") as Path.Absolute - val dataA = "a".encodeToByteArray() - val rootUser = User("root", homePath, true) - test("$name: root inode exists") { - val fileService = provider() - val rootInode = fileService.getINode(Path.root) - assertTrue(fileService.exists(rootInode)) - assertEquals(fileService.getPath(rootInode), Path.root) - } - test("$name: CRUD a file") { - val fileService = provider() - val aInode = fileService.getINode(aPath) - assertFalse(fileService.exists(aInode)) - assertEquals(CreateFileResult.Created, CreateFileResult.Created) - assertEquals(fileService.createFile(aInode, rootUser), CreateFileResult.Created) - assertTrue(fileService.exists(aInode)) - assertTrue(fileService.isFile(aInode)) - assertFalse(fileService.isSymlink(aInode)) - assertFalse(fileService.isDirectory(aInode)) - assertEquals(fileService.readFromFile(aInode, rootUser), ReadFileResult.Read(ByteArray(0))) - assertEquals(fileService.writeToFile(aInode, rootUser, dataA), WriteFileResult.Written) - assertEquals(fileService.readFromFile(aInode, rootUser), ReadFileResult.Read(dataA)) - assertTrue(fileService.isFile(aInode)) - assertEquals(fileService.deleteFile(aInode, rootUser), DeleteFileResult.Deleted) - assertFalse(fileService.isFile(aInode)) - assertFalse(fileService.exists(aInode)) - } - test("$name: CRUD a directory structure") { - val fileService = provider() - val aINode = fileService.getINode(aPath) - val bINode = fileService.getINode(bPath) - assertFalse(fileService.exists(aINode)) - assertFalse(fileService.exists(bINode)) - assertEquals(fileService.createDirectory(aINode, rootUser), CreateFileResult.Created) - assertEquals(fileService.createFile(bINode, rootUser), CreateFileResult.Created) - assertTrue(fileService.exists(aINode)) - assertTrue(fileService.exists(bINode)) - assertEquals(fileService.writeToFile(bINode, rootUser, dataA), WriteFileResult.Written) - assertEquals(fileService.readFromFile(bINode, rootUser), ReadFileResult.Read(dataA)) - assertEquals(fileService.deleteFile(aINode, rootUser), DeleteFileResult.Deleted) - assertFalse(fileService.exists(bINode)) - assertFalse(fileService.exists(aINode)) - } -} diff --git a/src/jsTest/kotlin/io/PathTest.kt b/src/jsTest/kotlin/io/PathTest.kt deleted file mode 100644 index 9ba8a9e..0000000 --- a/src/jsTest/kotlin/io/PathTest.kt +++ /dev/null @@ -1,104 +0,0 @@ -package io - -import io.kotest.assertions.assertSoftly -import io.kotest.core.spec.style.FunSpec -import io.kotest.data.forAll -import io.kotest.data.row -import io.kotest.matchers.be -import io.kotest.matchers.should -import io.kotest.matchers.shouldBe -import io.kotest.matchers.types.beOfType -import io.kotest.matchers.types.shouldBeTypeOf - -class PathTest : FunSpec({ - val homeDir = Path.of("/home") as Path.Absolute - test("recognize relative paths as such") { - forAll( - row(Path.of("a/b")), - row(Path.of(".")), - row(Path.of("a", "b")), - row(Path.ofShell("a/b", userHome = homeDir)), - row(Path.ofShell(".", userHome = homeDir)), - row(Path.ofShell("a", "b", userHome = homeDir)), - row(Path.ofShell(listOf("a", "b"), userHome = homeDir)), - ) { - assertSoftly(it) { shouldBeTypeOf() } - } - } - test("recognize absolute paths as such") { - forAll( - row(Path.of("/a/b")), - row(Path.of("/")), - row(Path.ofShell("/b/c", userHome = homeDir)), - ) { - assertSoftly(it) { shouldBeTypeOf() } - } - } - test("Path.of(x).stringPath == x") { - forAll( - row("/"), - row("/a/b"), - row("a/b"), - row("."), - ) { name -> - assertSoftly(Path.of(name).stringPath) { - shouldBe(name) - } - assertSoftly(Path.ofShell(name, homeDir).stringPath) { - shouldBe(name) - } - } - } - test("Shell resolution of home directory") { - forAll( - row("~/a", "/home/a"), - row("~", "/home"), - row("~/.", "/home/."), - row("/a", "/a"), - row("a", "a"), - ) { a, b -> - assertSoftly(Path.ofShell(a, homeDir).stringPath) { - shouldBe(b) - } - } - } - test("Relative path resolution works") { - forAll( - row("/a/b", "c/d", "/a/b/c/d"), - row("/a/b", "/c/d", "/c/d"), - row("/a/", "c", "/a/c"), - ) { a, b, c -> - val x = Path.of(a) - val y = Path.of(b) - val z = x.resolve(y) - assertSoftly { - x should beOfType() - z should beOfType() - z.stringPath should be(c) - } - } - } - test("Equality checks should work") { - forAll( - row("a"), - row("a/b"), - row("/a/b"), - row("c//d") - ) { - assertSoftly { - Path.of(it) should be(Path.of(it)) - } - } - } - test("relaitivization works") { - forAll( - row("/a/b", "/a", ".."), - row("/a", "/a/b", "b"), - row("/a/b", "/a/c", "../c"), - ) { a, b, c -> - assertSoftly { - Path.of(a).shouldBeTypeOf().relativize(Path.of(b)) shouldBe Path.of(c) - } - } - } -}) diff --git a/src/main/kotlin/WebOS.kt b/src/main/kotlin/WebOS.kt new file mode 100644 index 0000000..5bb788c --- /dev/null +++ b/src/main/kotlin/WebOS.kt @@ -0,0 +1,366 @@ +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 +import org.w3c.dom.Node +import org.w3c.dom.asList +import org.w3c.dom.events.Event +import org.w3c.dom.events.KeyboardEvent +import kotlin.properties.Delegates + +fun main() { + console.log("Hello from Kotlin") + + val runner = IORunner.runOnIO { + println("ON START") + wait() + println("WAIT CALLED ONCE") + val x = getOneKey() + println("INPUT GET: $x") + println("EXITING NOW") + } + println("runOnIO called -> CALLING WHATEVER") + runner._CALLED_BY_JS_onWhatever() + println("CALLED WHATEVER -> CALLING INPUT") + runner._CALLED_BY_JS_onInput(window.prompt("INPUT") ?: "NO INPUT") + println("CALLED INPUT") + return + + + + + + + val webos = WebOS() + document.body?.addEventListener("load", { + document.body?.querySelectorAll(".webosconsole")?.asList()?.forEach { + if (it !is Element) return@forEach + webos.registerConsole(it) + } + }) +} + +data class CharacterRun(val text: String, val color: String) + +abstract class Activity(val console: Console) { + + open fun handleKeyPress(key: Key) {} + open fun update() {} + abstract fun render(columns: Int, rows: Int): List> +} + +class ShellRunner(console: Console) : Activity(console) { + + val history = mutableListOf() + + val shellProgramStack = ArrayDeque() + + var lastLine = "" + + fun newLine() { + history.add(lastLine) + lastLine = "" + console.invalidateRender() + } + + fun println(it: String = "") { + print(it) + newLine() + } + + fun print(it: String) { + lastLine += it + console.invalidateRender() + } + + override fun update() { + shellProgramStack.lastOrNull()?.update() + } + + fun getInput(): String? { + if (inputBuffer.contains("\n")) { + val r = inputBuffer.substringBefore("\n") + inputBuffer = inputBuffer.substringAfter("\n") + return r + } + return null + } + + override fun render(columns: Int, rows: Int): List> = + history.map { listOf(CharacterRun(it.take(columns), "white")) }.takeLast(rows - 1) + listOf( + listOf( + CharacterRun( + lastLine + inputBuffer, + "white" + ) + ) + ) + + var inputBuffer = "" + + override fun handleKeyPress(key: Key) = when (key) { + is Key.Printable -> { + inputBuffer += key.char + } + is Key.Enter -> { + inputBuffer += "\n" + } + else -> Unit + } + + fun openShellProgram(program: (ShellRunner) -> T): ShellRunner = openShellProgram(program(this)) + + fun openShellProgram(program: T): ShellRunner { + shellProgramStack.addLast(program) + return this + } + +} + +abstract class ShellProgram(val shellRunner: ShellRunner) { + + abstract fun update(): Unit +} + +sealed class Key { + object Alt : Key() + object AltGraph : Key() + object CapsLock : Key() + object Control : Key() + object Function : Key() + object FunctionLock : Key() + object Hyper : Key() + object Meta : Key() + object NumLock : Key() + object Shift : Key() + object Super : Key() + object Symbol : Key() + object SymbolLock : Key() + object Enter : Key() + object Tab : Key() + sealed class Arrow : Key() { + object Down : Key() + object Left : Key() + object Right : Key() + object Up : Key() + } + + object End : Key() + object Home : Key() + object PageUp : Key() + object PageDown : Key() + object Backspace : Key() + object Clear : Key() + object Copy : Key() + object CrSel : Key() + object Cut : Key() + object Delete : Key() + object EraseEof : Key() + object ExSel : Key() + object Insert : Key() + object Paste : Key() + object Redo : Key() + object Undo : Key() + object Accept : Key() + object Again : Key() + object Attn : Key() + object Cancel : Key() + object ContextMenu : Key() + object Escape : Key() + object Execute : Key() + object Find : Key() + object Finish : Key() + object Help : Key() + class Printable(val char: Char) : Key() + class FunctionN(val n: Int) : Key() + companion object { + + fun from(string: String) = when (string) { + "Alt" -> Alt + "AltGraph" -> AltGraph + "CapsLock" -> CapsLock + "Control" -> Control + "Fn" -> Function + "FnLock" -> FunctionLock + "Hyper" -> Hyper + "Meta" -> Meta + "NumLock" -> NumLock + "Shift" -> Shift + "Super" -> Super + "Symbol" -> Symbol + "SymbolLock" -> SymbolLock + "Enter" -> Enter + "Tab" -> Tab + "Down" -> Arrow.Down + "Left" -> Arrow.Left + "Right" -> Arrow.Right + "Up" -> Arrow.Up + "End" -> End + "Home" -> Home + "PageUp" -> PageUp + "PageDown" -> PageDown + "Backspace" -> Backspace + "Clear" -> Clear + "Copy" -> Copy + "CrSel" -> CrSel + "Cut" -> Cut + "Delete" -> Delete + "EraseEof" -> EraseEof + "ExSel" -> ExSel + "Insert" -> Insert + "Paste" -> Paste + "Redo" -> Redo + "Undo" -> Undo + "Accept" -> Accept + "Again" -> Again + "Attn" -> Attn + "Cancel" -> Cancel + "ContextMenu" -> ContextMenu + "Escape" -> Escape + "Execute" -> Execute + "Find" -> Find + "Finish" -> Finish + "Help" -> Help + else -> if (string.length == 1) + Printable(string.first()) + else if (string.first() == 'F') + FunctionN(string.substring(1).toInt()) + else throw TODO() + } + } +} + +class Login(shellRunner: ShellRunner) : ShellProgram(shellRunner) { + + override fun update() { + while (true) + when (state) { + 0 -> { + shellRunner.print("Username: ") + state = 1 + } + 1 -> { + val inp = shellRunner.getInput() ?: return + shellRunner.println(inp) + shellRunner.print("Password: ") + username = inp + state = 2 + } + 2 -> { + val inp = shellRunner.getInput() ?: return + shellRunner.println(inp) + shellRunner.println("Login complete)") + password = inp + // TODO: check password and set user + state = 3 + } + 3 -> { + TODO() + } + } + + } + + var state = 0 + var username = "" + var password = "" + +} + +class Console(val os: WebOS, val renderElement: Element?) { + + val isVirtual get() = renderElement == null + val activityStack = ArrayDeque() + + var columns: Int = 80 + var rows: Int = 46 + + var shouldRerender = true + + var currentUser: User? = null + + private var _workingDirectory: Path.Absolute? = null + + var workingDirectory: Path.Absolute + get() = _workingDirectory ?: currentUser?.homeDirectory ?: Path.root + set(value) { + _workingDirectory = value + } + + init { + renderElement?.addEventListener("keydown", ::computeKeypressEvent) + } + + fun computeKeypressEvent(event: Event) { + if (!isInFocus()) return + if (event !is KeyboardEvent) return + activityStack.lastOrNull()?.handleKeyPress(Key.from(event.key)) + } + + private fun isInFocus(): Boolean = renderElement.containsOrIs(document.activeElement) + + private fun Node?.containsOrIs(node: Node?) = this == node || this?.contains(node) ?: false + + fun openActivity(activity: (Console) -> T): Console = openActivity(activity(this)) + + fun openActivity(activity: T): Console { + activityStack.addLast(activity) + invalidateRender() + return this + } + + fun render() { + if (renderElement == null) return + if (!shouldRerender) return + shouldRerender = false + val x = activityStack.lastOrNull()?.render(columns, rows) + console.log(x) + } + + fun invalidateRender() { + shouldRerender = true + window.requestAnimationFrame { render() } + } + + fun resize(newColumns: Int, newRows: Int) { + invalidateRender() + } + + // TODO: Handle resizes of the renderElement + + fun update(): Unit { + activityStack.lastOrNull()?.update() + } + + var updateInterval by Delegates.notNull() + + fun start(): Console { + updateInterval = window.setInterval(::update, 10) + return openActivity(ShellRunner(this).also { it.openShellProgram(Login(it)) }) + } + + fun destroy() { + window.clearInterval(updateInterval) + } + +} + +class WebOS { + + val hostname: String = "host" + private val _consoles = mutableListOf() + val consoles get() = _consoles.toList() + val files: FileService = PrimitiveFileService() + fun registerConsole(element: Element) { + _consoles.add(Console(this, element)) + } +} + +data class User( + val name: String, + val homeDirectory: Path.Absolute, + val isRoot: Boolean = false, +) diff --git a/src/main/kotlin/asyncio.kt b/src/main/kotlin/asyncio.kt new file mode 100644 index 0000000..e14fea9 --- /dev/null +++ b/src/main/kotlin/asyncio.kt @@ -0,0 +1,61 @@ +import kotlin.coroutines.* + +class IORunner : Continuation { + var nextStep: Continuation? = null + val eventQueue = ArrayDeque() + fun _CALLED_BY_JS_onInput(str: String) { + eventQueue.addLast(str) + awake() + } + + fun _CALLED_BY_JS_onWhatever() { + awake() + } + + private fun awake() { + val step = nextStep + if (step != null) { + nextStep = null + step.resume(Unit) + } + } + + suspend fun wait() { + return suspendCoroutine { c -> + console.log("SUSPENDING COROUTINE") + nextStep = c + } + } + + override val context: CoroutineContext + get() = EmptyCoroutineContext + + override fun resumeWith(result: Result) { + if (result.isFailure) { + result.exceptionOrNull()?.printStackTrace() + } else { + println("IORunner exited successfully") + } + } + + companion object { + fun runOnIO(block: suspend IORunner.() -> Unit): IORunner { + val r = IORunner() + block.startCoroutine(r, r) + return r + } + } +} + + +suspend fun IORunner.getOneKey(): String { + while (true) { + val x = eventQueue.removeFirstOrNull() + if (x == null) + wait() + else { + return x + } + } +} + diff --git a/src/main/kotlin/io/Path.kt b/src/main/kotlin/io/Path.kt new file mode 100644 index 0000000..241c559 --- /dev/null +++ b/src/main/kotlin/io/Path.kt @@ -0,0 +1,78 @@ +package io + +sealed class Path { + abstract val parts: List + fun toAbsolutePath(relativeTo: Absolute): Absolute { + return relativeTo.resolve(this) + } + + abstract fun resolve(path: Path): Path + + abstract val stringPath: String + + companion object { + val root = Absolute(listOf()) + + fun ofShell(string: String, userHome: Absolute): Path = + ofShell(string.split("/"), userHome) + + fun ofShell(vararg parts: String, userHome: Absolute): Path = + ofShell(parts.toList(), userHome) + + fun of(vararg parts: String): Path = + of(parts.toList()) + + fun of(string: String): Path = + of(string.split("/")) + + fun ofShell(parts: List, userHome: Absolute): Path { + if (parts.firstOrNull() == "~") + return userHome.resolve(Relative(parts.subList(1, parts.size).filter { it.isNotEmpty() })) + return of(parts) + } + + fun of(parts: List): Path { + if (parts.isEmpty()) + return root + if (parts[0] == "") // Starts with a / + return Absolute(parts.subList(1, parts.size).filter { it.isNotEmpty() }) + return Relative(parts.filter { it.isNotEmpty() }) + } + } + + data class Relative internal constructor(override val parts: List) : Path() { + override fun resolve(path: Path): Path { + if (path is Absolute) return path + return Relative(this.parts + path.parts) + } + + override val stringPath: String get() = parts.joinToString("/") + } + + data class Absolute internal constructor(override val parts: List) : Path() { + override fun resolve(path: Path): Absolute { + if (path is Absolute) return path + return Absolute(this.parts + path.parts) + } + + override val stringPath: String get() = "/" + parts.joinToString("/") + + fun relativize(path: Path): Relative = when (path) { + is Relative -> path + is Absolute -> { + var idx = 0 + while (idx < path.parts.size && idx < parts.size && path.parts[idx] == parts[idx]) { + idx++ + } + val returns = if (idx < parts.size) { + parts.size - idx + } else { + 0 + } + Relative(List(returns) { ".." } + path.parts.subList(idx, path.parts.size)) + } + } + } + + override fun toString(): String = "Path($stringPath)" +} diff --git a/src/main/kotlin/io/files.kt b/src/main/kotlin/io/files.kt new file mode 100644 index 0000000..4c53c4e --- /dev/null +++ b/src/main/kotlin/io/files.kt @@ -0,0 +1,203 @@ +package io + +import User + +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 + } +} + +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 + + override fun hashCode(): Int { + return data.contentHashCode() + } + } + + sealed interface Failure : ReadFileResult { + object NoPermission : Failure + object NotAFile : Failure + } +} + +sealed interface DeleteFileResult { + sealed interface Failure : DeleteFileResult { + object NoPermission : Failure + object NotAFile : Failure + } + + object Deleted : DeleteFileResult +} + +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) +} + +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 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 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/main/kotlin/util/sequence.kt b/src/main/kotlin/util/sequence.kt new file mode 100644 index 0000000..72b07dc --- /dev/null +++ b/src/main/kotlin/util/sequence.kt @@ -0,0 +1,5 @@ +package util + +fun Iterable.expandWith(t: T): Sequence = + this.asSequence() + generateSequence { t }.asSequence() + diff --git a/src/main/resources/index.html b/src/main/resources/index.html new file mode 100644 index 0000000..562ee45 --- /dev/null +++ b/src/main/resources/index.html @@ -0,0 +1,17 @@ + + + + + + + WebOs + + + +
+
+
+ + + diff --git a/src/test/kotlin/ProjectConfig.kt b/src/test/kotlin/ProjectConfig.kt new file mode 100644 index 0000000..faa4d69 --- /dev/null +++ b/src/test/kotlin/ProjectConfig.kt @@ -0,0 +1,3 @@ +import io.kotest.core.config.AbstractProjectConfig + +class ProjectConfig : AbstractProjectConfig() diff --git a/src/test/kotlin/io/FileServiceTest.kt b/src/test/kotlin/io/FileServiceTest.kt new file mode 100644 index 0000000..9c9c8b9 --- /dev/null +++ b/src/test/kotlin/io/FileServiceTest.kt @@ -0,0 +1,59 @@ +package io + +import User +import io.kotest.core.spec.style.FunSpec +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class FileServiceTest : FunSpec({ + generateTests("Primitive", ::PrimitiveFileService) +}) + +fun FunSpec.generateTests(name: String, provider: () -> FileService) { + val aPath = Path.of("/a") as Path.Absolute + val bPath = Path.of("/a/b") as Path.Absolute + val homePath = Path.of("/roothome") as Path.Absolute + val dataA = "a".encodeToByteArray() + val rootUser = User("root", homePath, true) + test("$name: root inode exists") { + val fileService = provider() + val rootInode = fileService.getINode(Path.root) + assertTrue(fileService.exists(rootInode)) + assertEquals(fileService.getPath(rootInode), Path.root) + } + test("$name: CRUD a file") { + val fileService = provider() + val aInode = fileService.getINode(aPath) + assertFalse(fileService.exists(aInode)) + assertEquals(CreateFileResult.Created, CreateFileResult.Created) + assertEquals(fileService.createFile(aInode, rootUser), CreateFileResult.Created) + assertTrue(fileService.exists(aInode)) + assertTrue(fileService.isFile(aInode)) + assertFalse(fileService.isSymlink(aInode)) + assertFalse(fileService.isDirectory(aInode)) + assertEquals(fileService.readFromFile(aInode, rootUser), ReadFileResult.Read(ByteArray(0))) + assertEquals(fileService.writeToFile(aInode, rootUser, dataA), WriteFileResult.Written) + assertEquals(fileService.readFromFile(aInode, rootUser), ReadFileResult.Read(dataA)) + assertTrue(fileService.isFile(aInode)) + assertEquals(fileService.deleteFile(aInode, rootUser), DeleteFileResult.Deleted) + assertFalse(fileService.isFile(aInode)) + assertFalse(fileService.exists(aInode)) + } + test("$name: CRUD a directory structure") { + val fileService = provider() + val aINode = fileService.getINode(aPath) + val bINode = fileService.getINode(bPath) + assertFalse(fileService.exists(aINode)) + assertFalse(fileService.exists(bINode)) + assertEquals(fileService.createDirectory(aINode, rootUser), CreateFileResult.Created) + assertEquals(fileService.createFile(bINode, rootUser), CreateFileResult.Created) + assertTrue(fileService.exists(aINode)) + assertTrue(fileService.exists(bINode)) + assertEquals(fileService.writeToFile(bINode, rootUser, dataA), WriteFileResult.Written) + assertEquals(fileService.readFromFile(bINode, rootUser), ReadFileResult.Read(dataA)) + assertEquals(fileService.deleteFile(aINode, rootUser), DeleteFileResult.Deleted) + assertFalse(fileService.exists(bINode)) + assertFalse(fileService.exists(aINode)) + } +} diff --git a/src/test/kotlin/io/PathTest.kt b/src/test/kotlin/io/PathTest.kt new file mode 100644 index 0000000..9ba8a9e --- /dev/null +++ b/src/test/kotlin/io/PathTest.kt @@ -0,0 +1,104 @@ +package io + +import io.kotest.assertions.assertSoftly +import io.kotest.core.spec.style.FunSpec +import io.kotest.data.forAll +import io.kotest.data.row +import io.kotest.matchers.be +import io.kotest.matchers.should +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.beOfType +import io.kotest.matchers.types.shouldBeTypeOf + +class PathTest : FunSpec({ + val homeDir = Path.of("/home") as Path.Absolute + test("recognize relative paths as such") { + forAll( + row(Path.of("a/b")), + row(Path.of(".")), + row(Path.of("a", "b")), + row(Path.ofShell("a/b", userHome = homeDir)), + row(Path.ofShell(".", userHome = homeDir)), + row(Path.ofShell("a", "b", userHome = homeDir)), + row(Path.ofShell(listOf("a", "b"), userHome = homeDir)), + ) { + assertSoftly(it) { shouldBeTypeOf() } + } + } + test("recognize absolute paths as such") { + forAll( + row(Path.of("/a/b")), + row(Path.of("/")), + row(Path.ofShell("/b/c", userHome = homeDir)), + ) { + assertSoftly(it) { shouldBeTypeOf() } + } + } + test("Path.of(x).stringPath == x") { + forAll( + row("/"), + row("/a/b"), + row("a/b"), + row("."), + ) { name -> + assertSoftly(Path.of(name).stringPath) { + shouldBe(name) + } + assertSoftly(Path.ofShell(name, homeDir).stringPath) { + shouldBe(name) + } + } + } + test("Shell resolution of home directory") { + forAll( + row("~/a", "/home/a"), + row("~", "/home"), + row("~/.", "/home/."), + row("/a", "/a"), + row("a", "a"), + ) { a, b -> + assertSoftly(Path.ofShell(a, homeDir).stringPath) { + shouldBe(b) + } + } + } + test("Relative path resolution works") { + forAll( + row("/a/b", "c/d", "/a/b/c/d"), + row("/a/b", "/c/d", "/c/d"), + row("/a/", "c", "/a/c"), + ) { a, b, c -> + val x = Path.of(a) + val y = Path.of(b) + val z = x.resolve(y) + assertSoftly { + x should beOfType() + z should beOfType() + z.stringPath should be(c) + } + } + } + test("Equality checks should work") { + forAll( + row("a"), + row("a/b"), + row("/a/b"), + row("c//d") + ) { + assertSoftly { + Path.of(it) should be(Path.of(it)) + } + } + } + test("relaitivization works") { + forAll( + row("/a/b", "/a", ".."), + row("/a", "/a/b", "b"), + row("/a/b", "/a/c", "../c"), + ) { a, b, c -> + assertSoftly { + Path.of(a).shouldBeTypeOf().relativize(Path.of(b)) shouldBe Path.of(c) + } + } + } +}) -- cgit