summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/jsMain/kotlin/WebOS.kt10
-rw-r--r--src/jsMain/kotlin/io/Path.kt2
-rw-r--r--src/jsMain/kotlin/io/files.kt257
-rw-r--r--src/jsTest/kotlin/io/PathTest.kt6
4 files changed, 195 insertions, 80 deletions
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<Console>()
val consoles get() = _consoles.toList()
- val files = IOHandler()
+ val files: FileService<PrimitiveINode> = 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<Mount>()
- 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 <T> 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<INode> {
+ 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<User, Permission>)
}
-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<User, Permission>
+
+ class File(var data: ByteArray, override val permissions: MutableMap<User, Permission>) : PrimitiveStorageBlob
+ class Symlink(val path: Path, override val permissions: MutableMap<User, Permission>) : PrimitiveStorageBlob
+ class Directory(override val permissions: MutableMap<User, Permission>) : 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<PrimitiveINode> {
+ private val storageBlobs = mutableMapOf<String, PrimitiveStorageBlob>(
+ "/" 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<User, Permission>) {
+ 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<Path.Relative>()
+ assertSoftly(it) { shouldBeTypeOf<Path.Relative>() }
}
}
test("recognize absolute paths as such") {
@@ -25,7 +25,7 @@ class PathTest : FunSpec({
Path.of("/"),
Path.ofShell("/b/c", userHome = homeDir),
).forEach {
- it.shouldBeTypeOf<Path.Absolute>()
+ assertSoftly(it) { shouldBeTypeOf<Path.Absolute>() }
}
}
})