summaryrefslogtreecommitdiff
path: root/src/main/kotlin
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/kotlin')
-rw-r--r--src/main/kotlin/moe/nea89/website/Command.kt9
-rw-r--r--src/main/kotlin/moe/nea89/website/KConsole.kt16
-rw-r--r--src/main/kotlin/moe/nea89/website/KFiles.kt151
-rw-r--r--src/main/kotlin/moe/nea89/website/index.kt101
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"