package net.torvald.terrarum.virtualcomputer.luaapi import org.luaj.vm2.* import org.luaj.vm2.lib.OneArgFunction import org.luaj.vm2.lib.TwoArgFunction import org.luaj.vm2.lib.ZeroArgFunction import net.torvald.terrarum.virtualcomputer.tvd.VDUtil.VDPath import net.torvald.terrarum.virtualcomputer.computer.TerrarumComputer import net.torvald.terrarum.virtualcomputer.luaapi.Term.Companion.checkIBM437 import net.torvald.terrarum.virtualcomputer.tvd.VDUtil import net.torvald.terrarum.virtualcomputer.tvd.* import net.torvald.terrarum.virtualcomputer.tvd.VDFileWriter import java.io.* import java.nio.charset.Charset import java.util.* /** * computer directory: * .../computers/ * media/hda/ -> .../computers// * * Created by minjaesong on 2016-09-17. * * * NOTES: * Don't convert '\' to '/'! Rev-slash is used for escape character in sh, and we're sh-compatible! * Use .absoluteFile whenever possible; there's fuckin oddity! (http://bugs.java.com/bugdatabase/view_bug.do;:YfiG?bug_id=4483097) */ internal class Filesystem(globals: Globals, computer: TerrarumComputer) { init { // load things. WARNING: THIS IS MANUAL! globals["fs"] = LuaValue.tableOf() globals["fs"]["list"] = ListFiles(computer) // CC compliant globals["fs"]["exists"] = FileExists(computer) // CC/OC compliant globals["fs"]["isDir"] = IsDirectory(computer) // CC compliant globals["fs"]["isFile"] = IsFile(computer) globals["fs"]["isReadOnly"] = IsReadOnly(computer) // CC compliant globals["fs"]["getSize"] = GetSize(computer) // CC compliant globals["fs"]["mkdir"] = Mkdir(computer) globals["fs"]["mv"] = Mv(computer) globals["fs"]["cp"] = Cp(computer) globals["fs"]["rm"] = Rm(computer) globals["fs"]["concat"] = ConcatPath(computer) // OC compliant globals["fs"]["open"] = OpenFile(computer) //CC compliant globals["fs"]["parent"] = GetParentDir(computer) // fs.dofile defined in BOOT // fs.fetchText defined in ROMLIB } companion object { val sysCharset = Charset.forName("CP437") fun LuaValue.checkPath(): String { if (this.checkIBM437().contains(Regex("""\.\."""))) throw LuaError("'..' on path is not supported.") return this.checkIBM437().validatePath() } // Worst-case: we're on Windows or using a FAT32 partition mounted in *nix. // Note: we allow / as the path separator and expect all \s to be converted // accordingly before the path is passed to the file system. private val invalidChars = Regex("""[<>:"|?*\u0000-\u001F]""") // original OC uses Set(); we use regex fun isValidFilename(name: String) = !name.contains(invalidChars) fun String.validatePath() : String { if (!isValidFilename(this)) { throw IOException("path contains invalid characters") } return this } /** * return value is there for chaining only. */ fun VDPath.dropMount(): VDPath { if (this.hierarchy.size >= 2 && this[0].toCanonicalString(sysCharset) == "media") { this.hierarchy.removeAt(0) // drop "media" this.hierarchy.removeAt(0) // drop whatever mount symbol } return this } /** * if path is {media, someUUID, subpath}, redirects to * computer.diskRack[SOMEUUID]->subpath * else, computer.diskRack["hda"]->subpath */ fun TerrarumComputer.getFile(path: VDPath) : DiskEntry? { val disk = this.getTargetDisk(path) if (disk == null) return null path.dropMount() return VDUtil.getFile(disk, path) } /** * if path is like {media, fd1, subpath}, return * computer.diskRack["fd1"] * else, computer.diskRack[] */ fun TerrarumComputer.getTargetDisk(path: VDPath) : VirtualDisk? { if (path.hierarchy.size >= 2 && Arrays.equals(path[0], "media".toEntryName(DiskEntry.NAME_LENGTH, sysCharset))) { val diskName = path[1].toCanonicalString(sysCharset) val disk = this.diskRack[diskName] return disk } else { return this.diskRack[this.computerValue.getAsString("boot")] } } fun TerrarumComputer.getDirectoryEntries(path: VDPath) : Array? { val directory = this.getFile(path) if (directory == null) return null return VDUtil.getDirectoryEntries(this.getTargetDisk(path)!!, directory) } fun combinePath(base: String, local: String) : String { return "$base$local".replace("//", "/") } private fun tryBool(action: () -> Unit): LuaValue { try { action() return LuaValue.valueOf(true) } catch (gottaCatchemAll: Exception) { return LuaValue.valueOf(false) } } } // end of Companion Object /** * @param cname == UUID of the drive * * actual directory: /Saves//computers// */ class ListFiles(val computer: TerrarumComputer) : OneArgFunction() { override fun call(path: LuaValue) : LuaValue { val path = VDPath(path.checkPath(), sysCharset) val table = LuaTable() try { val directoryContents = computer.getDirectoryEntries(path)!! println("[Filesystem] directoryContents size: ${directoryContents.size}") directoryContents.forEachIndexed { index, diskEntry -> table.insert(index + 1, LuaValue.valueOf(diskEntry.filename.toCanonicalString(sysCharset))) } } catch (e: KotlinNullPointerException) {} return table } } class FileExists(val computer: TerrarumComputer) : OneArgFunction() { override fun call(path: LuaValue) : LuaValue { val path = VDPath(path.checkPath(), sysCharset) val disk = computer.getTargetDisk(path) if (disk == null) return LuaValue.valueOf(false) return LuaValue.valueOf( VDUtil.getFile(disk, path.dropMount()) != null ) } } class IsDirectory(val computer: TerrarumComputer) : OneArgFunction() { override fun call(path: LuaValue) : LuaValue { val path = VDPath(path.checkPath(), sysCharset) return LuaValue.valueOf(computer.getFile(path)?.contents is EntryDirectory) } } class IsFile(val computer: TerrarumComputer) : OneArgFunction() { override fun call(path: LuaValue) : LuaValue { val path = VDPath(path.checkPath(), sysCharset) return LuaValue.valueOf(computer.getFile(path)?.contents is EntryFile) } } class IsReadOnly(val computer: TerrarumComputer) : OneArgFunction() { override fun call(path: LuaValue) : LuaValue { return LuaValue.valueOf(false) } } /** we have 2 GB file size limit */ class GetSize(val computer: TerrarumComputer) : OneArgFunction() { override fun call(path: LuaValue) : LuaValue { val path = VDPath(path.checkPath(), sysCharset) val file = computer.getFile(path) try { if (file!!.contents is EntryFile) return LuaValue.valueOf(file.contents.getSizePure().toInt()) else if (file.contents is EntryDirectory) return LuaValue.valueOf(file.contents.entryCount) } catch (e: KotlinNullPointerException) { } return LuaValue.NONE } } // TODO class GetFreeSpace /** * returns true on success */ class Mkdir(val computer: TerrarumComputer) : OneArgFunction() { override fun call(path: LuaValue) : LuaValue { return tryBool { val path = VDPath(path.checkPath(), sysCharset) val disk = computer.getTargetDisk(path)!! val dirList = computer.getDirectoryEntries(path.getParent()) var makeNew = true // check dupes if (dirList != null) { for (entry in dirList) { if (Arrays.equals(path.last(), entry.filename)) { makeNew = false break } } } if (makeNew) { VDUtil.addDir(disk, path.getParent(), path.last()) } } } } /** * moves a directory, overwrites the target */ class Mv(val computer: TerrarumComputer) : TwoArgFunction() { override fun call(from: LuaValue, to: LuaValue) : LuaValue { return tryBool { val pathFrom = VDPath(from.checkPath(), sysCharset) val disk1 = computer.getTargetDisk(pathFrom) val pathTo = VDPath(to.checkPath(), sysCharset) val disk2 = computer.getTargetDisk(pathTo) VDUtil.moveFile(disk1!!, pathFrom, disk2!!, pathTo) } } } /** * copies a directory, overwrites the target * difference with ComputerCraft: it returns boolean, true on successful. */ class Cp(val computer: TerrarumComputer) : TwoArgFunction() { override fun call(from: LuaValue, to: LuaValue) : LuaValue { return tryBool { val pathFrom = VDPath(from.checkPath(), sysCharset) val disk1 = computer.getTargetDisk(pathFrom)!! val pathTo = VDPath(to.checkPath(), sysCharset) val disk2 = computer.getTargetDisk(pathTo)!! val oldFile = VDUtil.getFile(disk2, pathTo) try { VDUtil.deleteFile(disk2, pathTo) } catch (e: FileNotFoundException) { "Nothing to delete beforehand" } val file = VDUtil.getFile(disk1, pathFrom)!! try { VDUtil.addFile(disk2, pathTo.getParent(), file) } catch (e: FileNotFoundException) { // roll back delete on disk2 if (oldFile != null) { VDUtil.addFile(disk2, oldFile.parentEntryID, oldFile) throw FileNotFoundException("No such destination") } } } } } /** * difference with ComputerCraft: it returns boolean, true on successful. */ class Rm(val computer: TerrarumComputer) : OneArgFunction() { override fun call(path: LuaValue) : LuaValue { return tryBool { val path = VDPath(path.checkPath(), sysCharset) val disk = computer.getTargetDisk(path)!! VDUtil.deleteFile(disk, path) } } } class ConcatPath(val computer: TerrarumComputer) : TwoArgFunction() { override fun call(base: LuaValue, local: LuaValue) : LuaValue { TODO() } } /** * @param mode: r, rb, w, wb, a, ab * * Difference: TEXT MODE assumes CP437 instead of UTF-8! * * When you have opened a file you must always close the file handle, or else data may not be saved. * * FILE class in CC: * (when you look thru them using file = fs.open("./test", "w") * * file = { * close = function() * -- write mode * write = function(string) * flush = function() -- write, keep the handle * writeLine = function(string) -- text mode * -- read mode * readLine = function() -- text mode * readAll = function() * -- binary read mode * read = function() -- read single byte. return: number or nil * -- binary write mode * write = function(byte) * writeBytes = function(string as bytearray) * } */ class OpenFile(val computer: TerrarumComputer) : TwoArgFunction() { override fun call(path: LuaValue, mode: LuaValue) : LuaValue { val path = VDPath(path.checkPath(), sysCharset) val disk = computer.getTargetDisk(path)!! path.dropMount() val mode = mode.checkIBM437().toLowerCase() val luaClass = LuaTable() val fileEntry = computer.getFile(path)!! if (fileEntry.contents is EntryDirectory) { throw LuaError("File '${fileEntry.getFilenameString(sysCharset)}' is directory.") } val file = fileEntry.contents as EntryFile if (mode.contains(Regex("""[aw]"""))) throw LuaError("Cannot open file for " + "${if (mode.startsWith('w')) "read" else "append"} mode" + ": is readonly.") when (mode) { "r" -> { try { val fr = StringReader(String(file.bytes.toByteArray(), sysCharset))//FileReader(file) luaClass["close"] = FileClassClose(fr) luaClass["readLine"] = FileClassReadLine(fr) luaClass["readAll"] = FileClassReadAll(file) } catch (e: FileNotFoundException) { e.printStackTrace() throw LuaError( if (e.message != null && e.message!!.contains(Regex("""[Aa]ccess (is )?denied"""))) "$path: access denied." else "$path: no such file." ) } } "rb" -> { try { val fis = ByteArrayInputStream(file.bytes.toByteArray()) luaClass["close"] = FileClassClose(fis) luaClass["read"] = FileClassReadByte(fis) luaClass["readAll"] = FileClassReadAll(file) } catch (e: FileNotFoundException) { e.printStackTrace() throw LuaError("$path: no such file.") } } "w", "a" -> { try { val fw = VDFileWriter(fileEntry, mode.startsWith('a'), sysCharset) luaClass["close"] = FileClassClose(fw) luaClass["write"] = FileClassPrintText(fw) luaClass["writeLine"] = FileClassPrintlnText(fw) luaClass["flush"] = FileClassFlush(fw) } catch (e: FileNotFoundException) { e.printStackTrace() throw LuaError("$path: is a directory.") } } "wb", "ab" -> { try { val fos = VDFileOutputStream(fileEntry, mode.startsWith('a'), sysCharset) luaClass["close"] = FileClassClose(fos) luaClass["write"] = FileClassWriteByte(fos) luaClass["writeBytes"] = FileClassWriteBytes(fos) luaClass["flush"] = FileClassFlush(fos) } catch (e: FileNotFoundException) { e.printStackTrace() throw LuaError("$path: is a directory.") } } } return luaClass } } class GetParentDir(val computer: TerrarumComputer) : OneArgFunction() { override fun call(path: LuaValue) : LuaValue { val path = VDPath(path.checkPath(), sysCharset).getParent() return LuaValue.valueOf(path.toString()) } } ////////////////////////////// // OpenFile implementations // ////////////////////////////// private class FileClassClose(val fo: Closeable) : ZeroArgFunction() { override fun call() : LuaValue { fo.close() return LuaValue.NONE } } private class FileClassWriteByte(val fos: VDFileOutputStream) : OneArgFunction() { override fun call(byte: LuaValue) : LuaValue { fos.write(byte.checkint()) return LuaValue.NONE } } private class FileClassWriteBytes(val fos: VDFileOutputStream) : OneArgFunction() { override fun call(byteString: LuaValue) : LuaValue { val byteString = byteString.checkIBM437() val bytearr = ByteArray(byteString.length, { byteString[it].toByte() }) fos.write(bytearr) return LuaValue.NONE } } private class FileClassPrintText(val fw: VDFileWriter) : OneArgFunction() { override fun call(string: LuaValue) : LuaValue { val text = string.checkIBM437() fw.write(text) return LuaValue.NONE } } private class FileClassPrintlnText(val fw: VDFileWriter) : OneArgFunction() { override fun call(string: LuaValue) : LuaValue { val text = string.checkIBM437() + "\n" fw.write(text) return LuaValue.NONE } } private class FileClassFlush(val fo: Flushable) : ZeroArgFunction() { override fun call() : LuaValue { fo.flush() return LuaValue.NONE } } private class FileClassReadByte(val fis: ByteArrayInputStream) : ZeroArgFunction() { override fun call() : LuaValue { val readByte = fis.read() return if (readByte == -1) LuaValue.NIL else LuaValue.valueOf(readByte) } } private class FileClassReadAllBytes(val file: EntryFile) : ZeroArgFunction() { override fun call() : LuaValue { return LuaValue.valueOf(String(file.bytes.toByteArray(), sysCharset)) } } private class FileClassReadAll(val file: EntryFile) : ZeroArgFunction() { override fun call() : LuaValue { return LuaValue.valueOf(String(file.bytes.toByteArray(), sysCharset)) } } /** returns NO line separator! */ private class FileClassReadLine(fr: Reader) : ZeroArgFunction() { val scanner = Scanner(fr.readText()) // no closing; keep the scanner status persistent override fun call() : LuaValue { return if (scanner.hasNextLine()) LuaValue.valueOf(scanner.nextLine()) else LuaValue.NIL } } }