diff options
Diffstat (limited to 'src/main/kotlin/io')
-rw-r--r-- | src/main/kotlin/io/Path.kt | 78 | ||||
-rw-r--r-- | src/main/kotlin/io/files.kt | 203 |
2 files changed, 281 insertions, 0 deletions
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<String> + 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<String>, 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<String>): 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<String>) : 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<String>) : 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<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>) +} + +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<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 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 + } + +} |