diff options
Diffstat (limited to 'src/main/kotlin')
-rw-r--r-- | src/main/kotlin/moe/nea89/website/Command.kt | 9 | ||||
-rw-r--r-- | src/main/kotlin/moe/nea89/website/KConsole.kt | 16 | ||||
-rw-r--r-- | src/main/kotlin/moe/nea89/website/KFiles.kt | 151 | ||||
-rw-r--r-- | src/main/kotlin/moe/nea89/website/index.kt | 101 |
4 files changed, 267 insertions, 10 deletions
diff --git a/src/main/kotlin/moe/nea89/website/Command.kt b/src/main/kotlin/moe/nea89/website/Command.kt index f896987..cb59d0a 100644 --- a/src/main/kotlin/moe/nea89/website/Command.kt +++ b/src/main/kotlin/moe/nea89/website/Command.kt @@ -4,4 +4,13 @@ interface Command { val name: String val aliases: Set<String> fun run(console: KConsole, name: String, args: List<String>) +} + +data class CommandContext(val console: KConsole, val name: String, val args: List<String>) + +fun command(name: String, vararg aliases: String, block: CommandContext. () -> Unit) = object : Command { + override val name: String = name + override val aliases: Set<String> = aliases.toSet() + + override fun run(console: KConsole, name: String, args: List<String>) = block(CommandContext(console, name, args)) }
\ No newline at end of file diff --git a/src/main/kotlin/moe/nea89/website/KConsole.kt b/src/main/kotlin/moe/nea89/website/KConsole.kt index b0ae06f..51a6734 100644 --- a/src/main/kotlin/moe/nea89/website/KConsole.kt +++ b/src/main/kotlin/moe/nea89/website/KConsole.kt @@ -3,20 +3,27 @@ package moe.nea89.website import kotlinx.browser.document import kotlinx.html.dom.append import kotlinx.html.js.pre -import org.w3c.dom.* +import org.w3c.dom.HTMLElement +import org.w3c.dom.HTMLPreElement import org.w3c.dom.events.KeyboardEvent import kotlin.collections.set -class KConsole(private val root: HTMLElement, private val text: HTMLPreElement) { +class KConsole( + private val root: HTMLElement, + private val text: HTMLPreElement, + private val fileSystem: KFileSystem?, +) { + + val fileAccessor = fileSystem?.let { FileAccessor(it) } companion object { val shlexRegex = """"([^"\\]+|\\.)+"|([^ "'\\]+|\\.)+|'([^'\\]+|\\.)+'""".toRegex() - fun createFor(element: HTMLElement): KConsole { + fun createFor(element: HTMLElement, fileSystem: KFileSystem? = null): KConsole { val text = element.append.pre() element.classList.add(Styles.consoleClass) - val console = KConsole(element, text) + val console = KConsole(element, text, fileSystem) document.body!!.onkeydown = console::keydown console.addLine("Starting up terminal.") console.rerender() @@ -86,6 +93,7 @@ class KConsole(private val root: HTMLElement, private val text: HTMLPreElement) println("Could not shlex: $command") return null } + // TODO: Proper string unescaping parts.add(match.groupValues.drop(1).firstOrNull { it != "" } ?: "") i += match.value.length while (command[i] == ' ' && i < command.length) diff --git a/src/main/kotlin/moe/nea89/website/KFiles.kt b/src/main/kotlin/moe/nea89/website/KFiles.kt new file mode 100644 index 0000000..e9d04cf --- /dev/null +++ b/src/main/kotlin/moe/nea89/website/KFiles.kt @@ -0,0 +1,151 @@ +package moe.nea89.website + +sealed interface KFile { + val fileType: String + get() = when (this) { + is KFile.Directory -> "directory" + is KFile.Download -> "download" + is KFile.Image -> "image" + is KFile.Text -> "text file" + } + + data class Text(val text: String) : KFile + data class Image(val url: String) : KFile + data class Download(val url: String) : KFile + data class Directory(val files: Map<String, KFile>) : KFile +} + +data class KFileSystem(val root: KFile.Directory) { + /** + * Uses normalized paths + * */ + fun resolve(parts: List<String>): KFile? = + parts.fold<String, KFile?>(root) { current, part -> + if (current is KFile.Directory) { + current.files[part] + } else + null + } +} + + +enum class FSError { + ENOENT, EISNOTDIR +} + +class FileAccessor(val fileSystem: KFileSystem, var implicitPushD: Boolean = false) { // TODO implicit pushd support + val dirStack = mutableListOf<List<String>>() + var currentDir = listOf<String>() + + private fun directoryUp(): FSError? { + if (currentDir.isEmpty()) return FSError.ENOENT + currentDir = currentDir.dropLast(1) + return null + } + + private fun tryEnterDirectory(path: String): FSError? { + val cwd = fileSystem.resolve(currentDir) + ?: throw RuntimeException("Current working directory $currentDir does not exist in filesystem") + if (cwd !is KFile.Directory) + throw RuntimeException("Current working directory $currentDir is not a directory in filesystem") + val file = cwd.files[path] ?: return FSError.ENOENT + if (file !is KFile.Directory) return FSError.EISNOTDIR + currentDir = currentDir + path + return null + } + + fun cdSingle(path: String): FSError? { + if ('/' in path) throw RuntimeException("Cannot single cd with path: $path") + return when (path) { + "." -> null + ".." -> directoryUp() + "" -> null + else -> tryEnterDirectory(path) + } + } + + fun cd(path: String): FSError? { + val parts = path.split("/").filter { it.isNotEmpty() } + val rollbackPath = currentDir + if (path.startsWith("/")) { + currentDir = emptyList() + } + parts.forEach { + val error = cdSingle(it) + if (error != null) { + currentDir = rollbackPath + return error + } + } + return null + } + + fun resolve(path: String): KFile? { + val parts = path.split("/").filter { it.isNotEmpty() && it != "." } + return if (path.startsWith("/")) { + fileSystem.resolve(parts) + } else { + fileSystem.resolve(currentDir + parts) + } + } + + fun pushD() { + dirStack.add(currentDir) + } + + fun useD(block: () -> Unit) { + val d = currentDir + try { + block() + } finally { + currentDir = d + } + } + + fun popD(): Boolean { + currentDir = dirStack.removeLastOrNull() ?: return false + return true + } +} + +@DslMarker +annotation class KFileDsl + +fun fileSystem(block: FileSystemBuilder.() -> Unit): KFileSystem = + KFileSystem(FileSystemBuilder().also(block).build()) + + +@KFileDsl +class FileSystemBuilder { + private val files = mutableMapOf<String, KFile>() + + fun addNode(name: String, file: KFile): FileSystemBuilder { + val parts = name.split("/", limit = 2) + if (parts.size != 1) { + return addNode(parts[0], FileSystemBuilder().addNode(parts[1], file).build()) + } + if (files.containsKey(name)) { + throw RuntimeException("Tried to double set file: $name") + } + files[name] = file + return this + } + + infix fun String.text(rawText: String) { + addNode(this, KFile.Text(rawText)) + } + + infix fun String.image(dataUrl: String) { + addNode(this, KFile.Image(dataUrl)) + } + + infix fun String.download(url: String) { + addNode(this, KFile.Download(url)) + } + + operator fun String.invoke(block: FileSystemBuilder.() -> Unit) { + addNode(this, FileSystemBuilder().also(block).build()) + } + + fun build() = KFile.Directory(files) +} diff --git a/src/main/kotlin/moe/nea89/website/index.kt b/src/main/kotlin/moe/nea89/website/index.kt index 8496b3a..b122cec 100644 --- a/src/main/kotlin/moe/nea89/website/index.kt +++ b/src/main/kotlin/moe/nea89/website/index.kt @@ -10,13 +10,102 @@ fun main() { require("@fontsource/comic-mono/index.css") injectGlobal(Styles.global) val root = document.body!!.append.div() - val console = KConsole.createFor(root) - console.registerCommand(object : Command { - override val name: String = "dick" - override val aliases: Set<String> = setOf("cock") - override fun run(console: KConsole, name: String, args: List<String>) { - console.addMultilineText("Hehe") + val console = KConsole.createFor(root, fileSystem = fileSystem { + "etc" { + "passwd" text "hunter2" + } + "home/nea" { + "todo" text """ + | - git gud + | - finish this website + | - convince the general public that comic sans is a viable font + """.trimMargin() + } + "flag" text "CTF{12345abcdefghijklmonp3.1.4.1.5.9.2.8}" + }) + console.registerCommand(command("cwd", "pwd") { + val fa = console.fileAccessor + if (fa == null) { + console.addLine("There is no file accessor present :(") + return@command + } + console.addLine(fa.currentDir.joinToString(separator = "/", prefix = "/")) + }) + console.registerCommand(command("cd") { + val fa = console.fileAccessor + if (fa == null) { + console.addLine("There is no file accessor present :(") + return@command + } + val path = args.singleOrNull() + if (path == null) { + console.addLine("Usage: cd <directory>") + return@command + } + val error = fa.cd(path) + if (error != null) { + console.addLine("cd: ${error.name}") + } + }) + console.registerCommand(command("ls") { + val fa = console.fileAccessor + if (fa == null) { + console.addLine("There is no file accessor present :(") + return@command + } + val path = when (args.size) { + 0 -> "." + 1 -> args[0] + else -> { + console.addLine("Usage: ls [directory or file]") + return@command + } + } + val file = fa.resolve(path) + if (file == null) { + console.addLine("ls: Could not find file or directory") + return@command + } + when (file) { + is KFile.Directory -> { + val longestName = file.files.keys.maxOf { it.length } + file.files.forEach { (name, file) -> + console.addLine( + name + " ".repeat(longestName + 1 - name.length) + file.fileType + ) + } + } + else -> "is a ${file.fileType}" + } + }) + console.registerCommand(command("cat") { + val fa = console.fileAccessor + if (fa == null) { + console.addLine("There is no file accessor present :(") + return@command + } + val path = when (args.size) { + 0 -> "." + 1 -> args[0] + else -> { + console.addLine("Usage: cat [directory or file]") + return@command + } + } + val file = fa.resolve(path) + if (file == null) { + console.addLine("cat: Could not find file or directory") + return@command } + when (file) { + is KFile.Directory -> console.addLine("cat: Is a directory") + is KFile.Text -> console.addMultilineText(file.text) + is KFile.Image -> console.addMultilineText("Imagine here was an image: ${file.url}") + is KFile.Download -> console.addMultilineText("Imageine here was a download: ${file.url}") + } + }) + console.registerCommand(command("dick", "cock") { + console.addMultilineText("Hehe") }) console.registerCommand(object : Command { override val name: String = "booob" |