From 1a3c9d7f65a7559f14a70de8cea4246b2d1477f0 Mon Sep 17 00:00:00 2001 From: Song Minjae Date: Fri, 7 Apr 2017 19:12:27 +0900 Subject: [PATCH] vt: new filesystem using tevd --- .idea/markdown-navigator.xml | 70 ++ .../markdown-navigator/profiles_settings.xml | 3 + src/net/torvald/terrarum/StateVTTest.kt | 2 +- src/net/torvald/terrarum/Terrarum.kt | 10 +- .../terrarum/debuggerapp/ActorValueTracker.kt | 19 +- .../terrarum/gameactors/ActorInventory.kt | 18 +- .../terrarum/gameactors/DecodeTapestry.kt | 14 +- .../virtualcomputer/assets/lua/BOOT.lua | 4 +- .../computer/TerrarumComputer.kt | 46 +- .../virtualcomputer/luaapi/FilesystemDir.kt | 522 +++++++++++ .../virtualcomputer/luaapi/FilesystemTEVD.kt | 513 +++++++++++ .../terrarum/virtualcomputer/luaapi/Term.kt | 3 +- .../terrarum/virtualcomputer/tvd/VDUtil.kt | 816 ++++++++++++++++++ .../virtualcomputer/tvd/VirtualDisk.kt | 246 ++++++ 14 files changed, 2227 insertions(+), 59 deletions(-) create mode 100644 .idea/markdown-navigator.xml create mode 100644 .idea/markdown-navigator/profiles_settings.xml create mode 100644 src/net/torvald/terrarum/virtualcomputer/luaapi/FilesystemDir.kt create mode 100644 src/net/torvald/terrarum/virtualcomputer/luaapi/FilesystemTEVD.kt create mode 100644 src/net/torvald/terrarum/virtualcomputer/tvd/VDUtil.kt create mode 100644 src/net/torvald/terrarum/virtualcomputer/tvd/VirtualDisk.kt diff --git a/.idea/markdown-navigator.xml b/.idea/markdown-navigator.xml new file mode 100644 index 000000000..4fdc309a4 --- /dev/null +++ b/.idea/markdown-navigator.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/markdown-navigator/profiles_settings.xml b/.idea/markdown-navigator/profiles_settings.xml new file mode 100644 index 000000000..57927c5a7 --- /dev/null +++ b/.idea/markdown-navigator/profiles_settings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/net/torvald/terrarum/StateVTTest.kt b/src/net/torvald/terrarum/StateVTTest.kt index 0779a597e..409416027 100644 --- a/src/net/torvald/terrarum/StateVTTest.kt +++ b/src/net/torvald/terrarum/StateVTTest.kt @@ -21,7 +21,7 @@ class StateVTTest : BasicGameState() { // HiRes: 100x64, LoRes: 80x25 val computerInside = TerrarumComputer(peripheralSlots = 8) - val vt = SimpleTextTerminal(SimpleTextTerminal.AMBER, 80, 25, + val vt = SimpleTextTerminal(SimpleTextTerminal.BLUE_NOVELTY, 80, 25, computerInside, colour = false, hires = false) diff --git a/src/net/torvald/terrarum/Terrarum.kt b/src/net/torvald/terrarum/Terrarum.kt index 38de3b0d1..456b3e08c 100644 --- a/src/net/torvald/terrarum/Terrarum.kt +++ b/src/net/torvald/terrarum/Terrarum.kt @@ -189,7 +189,7 @@ object Terrarum : StateBasedGame(GAME_NAME) { * * e.g. 0x02010034 can be translated as 2.1.52 */ - const val VERSION_RAW = 0x000200E1 + const val VERSION_RAW = 0x0002018E const val VERSION_STRING: String = "${VERSION_RAW.ushr(24)}.${VERSION_RAW.and(0xFF0000).ushr(16)}.${VERSION_RAW.and(0xFFFF)}" const val NAME = "Terrarum" @@ -303,7 +303,7 @@ object Terrarum : StateBasedGame(GAME_NAME) { gc.graphics.clear() // clean up any 'dust' in the buffer - addState(StateVTTest()) + //addState(StateVTTest()) //addState(StateGraphicComputerTest()) //addState(StateTestingLightning()) //addState(StateSplash()) @@ -318,7 +318,7 @@ object Terrarum : StateBasedGame(GAME_NAME) { //addState(StateMidiInputTest()) //addState(StateNewRunesTest()) - //ingame = StateInGame(); addState(ingame) + ingame = StateInGame(); addState(ingame) // foolproof @@ -650,4 +650,6 @@ operator fun Color.minus(other: Color) = Color( this.a - other.a ) -fun Int.toHex() = Integer.toHexString(this) +fun Int.toHex() = this.toString(16) + + diff --git a/src/net/torvald/terrarum/debuggerapp/ActorValueTracker.kt b/src/net/torvald/terrarum/debuggerapp/ActorValueTracker.kt index e1e3003c1..6281077ab 100644 --- a/src/net/torvald/terrarum/debuggerapp/ActorValueTracker.kt +++ b/src/net/torvald/terrarum/debuggerapp/ActorValueTracker.kt @@ -9,6 +9,7 @@ import net.torvald.terrarum.gameactors.ActorWithSprite import net.torvald.terrarum.mapdrawer.FeaturesDrawer import java.awt.BorderLayout import java.awt.GridLayout +import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import java.awt.event.MouseListener import javax.swing.* @@ -58,11 +59,7 @@ class ActorValueTracker constructor() : JFrame() { } // button listener for buttons - buttonAddAV.addMouseListener(object : MouseListener { - override fun mouseEntered(e: MouseEvent?) { } - override fun mouseClicked(e: MouseEvent?) { } - override fun mouseReleased(e: MouseEvent?) { } - override fun mouseExited(e: MouseEvent?) { } + buttonAddAV.addMouseListener(object : MouseAdapter() { override fun mousePressed(e: MouseEvent?) { if (actor != null && modavInputKey.text.isNotBlank() && modavInputValue.text.isNotBlank()) { SetAV.execute(( @@ -74,11 +71,7 @@ class ActorValueTracker constructor() : JFrame() { } } }) - buttonDelAV.addMouseListener(object : MouseListener { - override fun mouseEntered(e: MouseEvent?) { } - override fun mouseClicked(e: MouseEvent?) { } - override fun mouseReleased(e: MouseEvent?) { } - override fun mouseExited(e: MouseEvent?) { } + buttonDelAV.addMouseListener(object : MouseAdapter() { override fun mousePressed(e: MouseEvent?) { if (actorValue != null && modavInputKey.text.isNotBlank()) { actorValue!!.remove(modavInputKey.text) @@ -87,11 +80,7 @@ class ActorValueTracker constructor() : JFrame() { } } }) - buttonChangeActor.addMouseListener(object : MouseListener { - override fun mouseEntered(e: MouseEvent?) { } - override fun mouseClicked(e: MouseEvent?) { } - override fun mouseReleased(e: MouseEvent?) { } - override fun mouseExited(e: MouseEvent?) { } + buttonChangeActor.addMouseListener(object : MouseAdapter() { override fun mousePressed(e: MouseEvent?) { if (actorIDField.text.toLowerCase() == "player") { actor = Terrarum.ingame!!.player diff --git a/src/net/torvald/terrarum/gameactors/ActorInventory.kt b/src/net/torvald/terrarum/gameactors/ActorInventory.kt index 6dedc51cb..fd9b38635 100644 --- a/src/net/torvald/terrarum/gameactors/ActorInventory.kt +++ b/src/net/torvald/terrarum/gameactors/ActorInventory.kt @@ -121,29 +121,17 @@ class ActorInventory() { return capacityMode } - fun getTotalWeight(): Double { - var weight = 0.0 - itemList.forEach { weight += it.item.mass * it.amount } - - return weight - } + fun getTotalWeight(): Double = itemList.map { it.item.mass * it.amount }.sum() /** * Real amount */ - fun getTotalCount(): Int { - var count = 0 - itemList.forEach { count += it.amount } - - return count - } + fun getTotalCount(): Int = itemList.map { it.amount }.sum() /** * Unique amount, multiple items are calculated as one */ - fun getTotalUniqueCount(): Int { - return itemList.size - } + fun getTotalUniqueCount(): Int = itemList.size /** * Check whether the itemList contains too many items diff --git a/src/net/torvald/terrarum/gameactors/DecodeTapestry.kt b/src/net/torvald/terrarum/gameactors/DecodeTapestry.kt index 04dd0dc85..cb737184a 100644 --- a/src/net/torvald/terrarum/gameactors/DecodeTapestry.kt +++ b/src/net/torvald/terrarum/gameactors/DecodeTapestry.kt @@ -111,19 +111,15 @@ object DecodeTapestry { val FORMAT_64 = 2 operator fun invoke(fileObj: File): TapestryObject { - fun magicMismatch(magic: ByteArray): Boolean { - MAGIC.forEachIndexed { i, byte -> - if (byte != magic[i]) - return true - } - return false + fun magicMismatch(magic: ByteArray, array: ByteArray): Boolean { + return !Arrays.equals(array.sliceArray(0..magic.lastIndex), magic) } val file = fileObj.readBytes() val magic = file.copyOfRange(0, 4) - if (magicMismatch(magic)) + if (magicMismatch(MAGIC, magic)) throw RuntimeException("Invalid file -- type mismatch: expected header " + "${MAGIC[0]} ${MAGIC[1]} ${MAGIC[2]} ${MAGIC[3]}; got " + "${magic[0]} ${magic[1]} ${magic[2]} ${magic[3]}") @@ -156,8 +152,8 @@ object DecodeTapestry { - val artName = kotlin.text.String(artNameBytes.toByteArray(), charset = Charset.forName("UTF-8")) - val authorName = kotlin.text.String(authorNameBytes.toByteArray(), charset = Charset.forName("UTF-8")) + val artName = String(artNameBytes.toByteArray(), charset = Charset.forName("UTF-8")) + val authorName = String(authorNameBytes.toByteArray(), charset = Charset.forName("UTF-8")) val imageDataSize = file.size - readCounter val height = imageDataSize / width diff --git a/src/net/torvald/terrarum/virtualcomputer/assets/lua/BOOT.lua b/src/net/torvald/terrarum/virtualcomputer/assets/lua/BOOT.lua index 7efa9ebd4..acde7de77 100644 --- a/src/net/torvald/terrarum/virtualcomputer/assets/lua/BOOT.lua +++ b/src/net/torvald/terrarum/virtualcomputer/assets/lua/BOOT.lua @@ -1015,8 +1015,6 @@ sandbox._G = sandbox -- path for any ingame libraries package.path = "/net/torvald/terrarum/virtualcomputer/assets/lua/?.lua;" .. package.path -_G.__scanMode__ = "UNINIT" -- part of inputstream implementation - local screenbufferdim = term.width() * term.height() local screencolours = 4 if term.isCol() then screencolours = 8 @@ -1027,7 +1025,7 @@ if not computer.prompt then computer.prompt = DC3.."> "..DC4 end if not computer.verbose then computer.verbose = true end -- print debug info if not computer.loadedCLayer then computer.loadedCLayer = {} end -- list of loaded compatibility layers -- if no bootloader is pre-defined via EFI, use default one -if not computer.bootloader then computer.bootloader = "/boot/efi" end +if not computer.bootloader then computer.bootloader = "boot/efi" end if not computer.OEM then computer.OEM = "" end machine.totalMemory = _G.totalMemory if not computer.bellpitch then computer.bellpitch = 950 end diff --git a/src/net/torvald/terrarum/virtualcomputer/computer/TerrarumComputer.kt b/src/net/torvald/terrarum/virtualcomputer/computer/TerrarumComputer.kt index 499e13701..863223288 100644 --- a/src/net/torvald/terrarum/virtualcomputer/computer/TerrarumComputer.kt +++ b/src/net/torvald/terrarum/virtualcomputer/computer/TerrarumComputer.kt @@ -9,10 +9,13 @@ import org.luaj.vm2.lib.ZeroArgFunction import org.luaj.vm2.lib.jse.JsePlatform import net.torvald.terrarum.KVHashMap import net.torvald.terrarum.Millisec +import net.torvald.terrarum.Terrarum import net.torvald.terrarum.gameactors.roundInt import net.torvald.terrarum.virtualcomputer.luaapi.* import net.torvald.terrarum.virtualcomputer.peripheral.* import net.torvald.terrarum.virtualcomputer.terminal.* +import net.torvald.terrarum.virtualcomputer.tvd.VDUtil +import net.torvald.terrarum.virtualcomputer.tvd.VirtualDisk import net.torvald.terrarum.virtualcomputer.worldobject.ComputerPartsCodex import org.lwjgl.BufferUtils import org.lwjgl.openal.AL @@ -22,6 +25,8 @@ import org.newdawn.slick.Input import java.io.* import java.nio.ByteBuffer import java.util.* +import java.util.logging.Level +import kotlin.collections.HashMap /** * A part that makes "computer fixture" actually work @@ -87,6 +92,26 @@ class TerrarumComputer(peripheralSlots: Int) { val milliTime: Int get() = (System.currentTimeMillis() - startupTimestamp).toInt() + /** String: + * if it's UUID, formatted UUID as string, always 36 chars + * if not (test purpose only!), just String + */ + val diskRack = HashMap() + + fun attachDisk(slot: String, filename: String) { + computerValue[slot] = filename + + // put disk in diskRack + if (filename.isNotEmpty() && filename.isNotBlank()) { + diskRack[slot] = VDUtil.readDiskArchive( + File(Terrarum.currentSaveDir.path + "/computers/$filename").absoluteFile, + Level.WARNING, + { }, + Filesystem.sysCharset + ) + } + } + init { computerValue["memslot0"] = 4864 // -1 indicates mem slot is empty computerValue["memslot1"] = -1 // put index of item here @@ -96,20 +121,20 @@ class TerrarumComputer(peripheralSlots: Int) { computerValue["processor"] = -1 // do. // as in "dev/hda"; refers hard disk drive (and no partitioning) - computerValue["hda"] = "uuid_testhda" // 'UUID rendered as String' or "none" - computerValue["hdb"] = "uuid_testhdb" - computerValue["hdc"] = "none" - computerValue["hdd"] = "none" + attachDisk("hda", "uuid_testhda") + attachDisk("hdb", "") + attachDisk("hdc", "") + attachDisk("hdd", "") // as in "dev/fd1"; refers floppy disk drive - computerValue["fd1"] = "uuid_testfd1" - computerValue["fd2"] = "none" - computerValue["fd3"] = "none" - computerValue["fd4"] = "none" + attachDisk("fd1", "") + attachDisk("fd2", "") + attachDisk("fd3", "") + attachDisk("fd4", "") // SCSI connected optical drive - computerValue["sda"] = "none" + attachDisk("sda", "") // boot device - computerValue["boot"] = computerValue.getAsString("hda")!! + computerValue["boot"] = "hda" } fun getPeripheral(tableName: String): Peripheral? { @@ -168,7 +193,6 @@ class TerrarumComputer(peripheralSlots: Int) { PcSpeakerDriver(luaJ_globals, this) WorldInformationProvider(luaJ_globals) - // secure the sandbox //luaJ_globals["io"] = LuaValue.NIL // dubug should be sandboxed in BOOT.lua (use OpenComputers code) diff --git a/src/net/torvald/terrarum/virtualcomputer/luaapi/FilesystemDir.kt b/src/net/torvald/terrarum/virtualcomputer/luaapi/FilesystemDir.kt new file mode 100644 index 000000000..87222168d --- /dev/null +++ b/src/net/torvald/terrarum/virtualcomputer/luaapi/FilesystemDir.kt @@ -0,0 +1,522 @@ +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.Terrarum +import net.torvald.terrarum.toHex +import net.torvald.terrarum.virtualcomputer.computer.TerrarumComputer +import net.torvald.terrarum.virtualcomputer.luaapi.Term.Companion.checkIBM437 +import java.io.* +import java.nio.file.Files +import java.nio.file.NoSuchFileException +import java.nio.file.Path +import java.nio.file.Paths +import java.util.* + +/** + * computer directory: + * .../computers/ + * media/hda/ -> .../computers// + * + * Created by minjaesong on 16-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) + */ +@Deprecated("Fuck permission and shit, we go virtual. Use FilesystemTar") +internal class FilesystemDir(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 { + fun ensurePathSanity(path: LuaValue) { + if (path.checkIBM437().contains(Regex("""\.\."""))) + throw LuaError("'..' on path is not supported.") + if (!isValidFilename(path.checkIBM437())) + throw IOException("path contains invalid characters") + } + + var isCaseInsensitive: Boolean + + init { + try { + val uuid = UUID.randomUUID().toString() + val lowerCase = File(Terrarum.currentSaveDir, uuid + "oc_rox") + val upperCase = File(Terrarum.currentSaveDir, uuid + "OC_ROX") + // This should NEVER happen but could also lead to VERY weird bugs, so we + // make sure the files don't exist. + if (lowerCase.exists()) lowerCase.delete() + if (upperCase.exists()) upperCase.delete() + + lowerCase.createNewFile() + + val insensitive = upperCase.exists() + lowerCase.delete() + + isCaseInsensitive = insensitive + + println("[Filesystem] Case insensitivity: $isCaseInsensitive") + } + catch (e: IOException) { + System.err.println("[Filesystem] Couldn't determine if the file system is case sensitive, falling back to insensitive.") + e.printStackTrace(System.out) + isCaseInsensitive = true + } + } + + // 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 + } + + /** actual directory: /Saves//computers// + * directs media/ directory to / directory + */ + fun TerrarumComputer.getRealPath(luapath: LuaValue) : String { + // direct mounted paths to real path + val computerDir = Terrarum.currentSaveDir.absolutePath + "/computers/" + /* if not begins with "(/?)media/", direct to boot + * else, to corresponding drives + * + * List of device names (these are auto-mounted. why? primitivism :p): + * = hda - hdd: hard disks + * = fd1 - fd4: floppy drives + * = sda: whatever external drives, usually a CD + * = boot: current boot device + */ + + // remove first '/' in path + var path = luapath.checkIBM437().validatePath() + if (path.startsWith('/')) path = path.substring(1) + + val finalPath: String + + if (path.startsWith("media/")) { + val device = path.substring(6, 9) + val subPath = path.substring(9) + finalPath = computerDir + this.computerValue.getAsString("device") + subPath + } + else { + finalPath = computerDir + this.computerValue.getAsString("boot") + "/" + path + } + + // remove trailing slash + return if (finalPath.endsWith("\\") || finalPath.endsWith("/")) + finalPath.substring(0, finalPath.length - 1) + else + finalPath + } + + fun combinePath(base: String, local: String) : String { + return "$base$local".replace("//", "/") + } + } + + /** + * @param cname == UUID of the drive + * + * actual directory: /Saves//computers// + */ + class ListFiles(val computer: TerrarumComputer) : OneArgFunction() { + override fun call(path: LuaValue) : LuaValue { + FilesystemDir.ensurePathSanity(path) + + println("ListFiles: got path ${path.checkIBM437()}") + + val table = LuaTable() + val file = File(computer.getRealPath(path)).absoluteFile + try { + file.list().forEachIndexed { i, s -> table.insert(i, LuaValue.valueOf(s)) } + } + catch (e: NullPointerException) {} + return table + } + } + + /** Don't use this. Use isFile */ + class FileExists(val computer: TerrarumComputer) : OneArgFunction() { + override fun call(path: LuaValue) : LuaValue { + FilesystemDir.ensurePathSanity(path) + + return LuaValue.valueOf(Files.exists(Paths.get(computer.getRealPath(path)).toAbsolutePath())) + } + } + + class IsDirectory(val computer: TerrarumComputer) : OneArgFunction() { + override fun call(path: LuaValue) : LuaValue { + FilesystemDir.ensurePathSanity(path) + + val isDir = Files.isDirectory(Paths.get(computer.getRealPath(path)).toAbsolutePath()) + val exists = Files.exists(Paths.get(computer.getRealPath(path)).toAbsolutePath()) + + return LuaValue.valueOf(isDir || exists) + } + } + + class IsFile(val computer: TerrarumComputer) : OneArgFunction() { + override fun call(path: LuaValue) : LuaValue { + FilesystemDir.ensurePathSanity(path) + + // check if the path is file by checking: + // 1. isfile + // 2. canwrite + // 3. length + // Why? Our Java simply wants to fuck you. + + val path = Paths.get(computer.getRealPath(path)).toAbsolutePath() + var result = false + result = Files.isRegularFile(path) + + if (!result) result = Files.isWritable(path) + + if (!result) + try { result = Files.size(path) > 0 } + catch (e: NoSuchFileException) { result = false } + + return LuaValue.valueOf(result) + } + } + + class IsReadOnly(val computer: TerrarumComputer) : OneArgFunction() { + override fun call(path: LuaValue) : LuaValue { + FilesystemDir.ensurePathSanity(path) + + return LuaValue.valueOf(!Files.isWritable(Paths.get(computer.getRealPath(path)).toAbsolutePath())) + } + } + + /** we have 4GB file size limit */ + class GetSize(val computer: TerrarumComputer) : OneArgFunction() { + override fun call(path: LuaValue) : LuaValue { + FilesystemDir.ensurePathSanity(path) + + return LuaValue.valueOf(Files.size(Paths.get(computer.getRealPath(path)).toAbsolutePath()).toInt()) + } + } + + // TODO class GetFreeSpace + + /** + * difference with ComputerCraft: it returns boolean, true on successful. + */ + class Mkdir(val computer: TerrarumComputer) : OneArgFunction() { + override fun call(path: LuaValue) : LuaValue { + FilesystemDir.ensurePathSanity(path) + + return LuaValue.valueOf(File(computer.getRealPath(path)).absoluteFile.mkdir()) + } + } + + /** + * moves a directory, overwrites the target + */ + class Mv(val computer: TerrarumComputer) : TwoArgFunction() { + override fun call(from: LuaValue, to: LuaValue) : LuaValue { + FilesystemDir.ensurePathSanity(from) + FilesystemDir.ensurePathSanity(to) + + val fromFile = File(computer.getRealPath(from)).absoluteFile + var success = fromFile.copyRecursively( + File(computer.getRealPath(to)).absoluteFile, overwrite = true + ) + if (success) success = fromFile.deleteRecursively() + else return LuaValue.valueOf(false) + return LuaValue.valueOf(success) + } + } + + /** + * 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 { + FilesystemDir.ensurePathSanity(from) + FilesystemDir.ensurePathSanity(to) + + return LuaValue.valueOf( + File(computer.getRealPath(from)).absoluteFile.copyRecursively( + File(computer.getRealPath(to)).absoluteFile, overwrite = true + ) + ) + } + } + + /** + * difference with ComputerCraft: it returns boolean, true on successful. + */ + class Rm(val computer: TerrarumComputer) : OneArgFunction() { + override fun call(path: LuaValue) : LuaValue { + FilesystemDir.ensurePathSanity(path) + + return LuaValue.valueOf( + File(computer.getRealPath(path)).absoluteFile.deleteRecursively() + ) + } + } + + class ConcatPath(val computer: TerrarumComputer) : TwoArgFunction() { + override fun call(base: LuaValue, local: LuaValue) : LuaValue { + FilesystemDir.ensurePathSanity(base) + FilesystemDir.ensurePathSanity(local) + + val combinedPath = combinePath(base.checkIBM437().validatePath(), local.checkIBM437().validatePath()) + return LuaValue.valueOf(combinedPath) + } + } + + /** + * @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 { + FilesystemDir.ensurePathSanity(path) + + + + val mode = mode.checkIBM437().toLowerCase() + val luaClass = LuaTable() + val file = File(computer.getRealPath(path)).absoluteFile + + if (mode.contains(Regex("""[aw]""")) && !file.canWrite()) + throw LuaError("Cannot open file for " + + "${if (mode.startsWith('w')) "read" else "append"} mode" + + ": is readonly.") + + + + + when (mode) { + "r" -> { + try { + val fr = FileReader(file) + luaClass["close"] = FileClassClose(fr) + luaClass["readLine"] = FileClassReadLine(fr) + luaClass["readAll"] = FileClassReadAll(file.toPath()) + } + 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 = FileInputStream(file) + luaClass["close"] = FileClassClose(fis) + luaClass["read"] = FileClassReadByte(fis) + luaClass["readAll"] = FileClassReadAll(file.toPath()) + } + catch (e: FileNotFoundException) { + e.printStackTrace() + throw LuaError("$path: no such file.") + } + } + "w", "a" -> { + try { + val fw = FileWriter(file, (mode.startsWith('a'))) + 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 = FileOutputStream(file, (mode.startsWith('a'))) + 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 { + FilesystemDir.ensurePathSanity(path) + + var pathSB = StringBuilder(path.checkIBM437()) + + // backward travel, drop chars until '/' has encountered + while (!pathSB.endsWith('/')) + pathSB.deleteCharAt(pathSB.lastIndex - 1) + + // drop trailing '/' + if (pathSB.endsWith('/')) + pathSB.deleteCharAt(pathSB.lastIndex - 1) + + return LuaValue.valueOf(pathSB.toString()) + } + } + + + ////////////////////////////// + // OpenFile implementations // + ////////////////////////////// + + private class FileClassClose(val fo: Any) : ZeroArgFunction() { + override fun call() : LuaValue { + if (fo is FileOutputStream) + fo.close() + else if (fo is FileWriter) + fo.close() + else if (fo is FileReader) + fo.close() + else if (fo is FileInputStream) + fo.close() + else + throw IllegalArgumentException("Unacceptable file output: must be either Input/OutputStream or Reader/Writer.") + + return LuaValue.NONE + } + } + + private class FileClassWriteByte(val fos: FileOutputStream) : OneArgFunction() { + override fun call(byte: LuaValue) : LuaValue { + fos.write(byte.checkint()) + + return LuaValue.NONE + } + } + + private class FileClassWriteBytes(val fos: FileOutputStream) : 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: FileWriter) : OneArgFunction() { + override fun call(string: LuaValue) : LuaValue { + val text = string.checkIBM437() + fw.write(text) + return LuaValue.NONE + } + } + + private class FileClassPrintlnText(val fw: FileWriter) : OneArgFunction() { + override fun call(string: LuaValue) : LuaValue { + val text = string.checkIBM437() + "\n" + fw.write(text) + return LuaValue.NONE + } + } + + private class FileClassFlush(val fo: Any) : ZeroArgFunction() { + override fun call() : LuaValue { + if (fo is FileOutputStream) + fo.flush() + else if (fo is FileWriter) + fo.flush() + else + throw IllegalArgumentException("Unacceptable file output: must be either OutputStream or Writer.") + + return LuaValue.NONE + } + } + + private class FileClassReadByte(val fis: FileInputStream) : ZeroArgFunction() { + override fun call() : LuaValue { + val readByte = fis.read() + return if (readByte == -1) LuaValue.NIL else LuaValue.valueOf(readByte) + } + } + + private class FileClassReadAllBytes(val path: Path) : ZeroArgFunction() { + override fun call() : LuaValue { + val byteArr = Files.readAllBytes(path) + val s: String = java.lang.String(byteArr, "IBM437").toString() + return LuaValue.valueOf(s) + } + } + + private class FileClassReadAll(val path: Path) : ZeroArgFunction() { + override fun call() : LuaValue { + return FileClassReadAllBytes(path).call() + } + } + + /** returns NO line separator! */ + private class FileClassReadLine(val fr: FileReader) : 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 + } + } +} \ No newline at end of file diff --git a/src/net/torvald/terrarum/virtualcomputer/luaapi/FilesystemTEVD.kt b/src/net/torvald/terrarum/virtualcomputer/luaapi/FilesystemTEVD.kt new file mode 100644 index 000000000..87edc08af --- /dev/null +++ b/src/net/torvald/terrarum/virtualcomputer/luaapi/FilesystemTEVD.kt @@ -0,0 +1,513 @@ +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 16-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() == "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)?.file + } + + /** + * 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() + 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) -> Unit): LuaValue { + try { + action(Unit) + 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)!! + directoryContents.forEachIndexed { index, diskEntry -> + table.insert(index + 1, LuaValue.valueOf(diskEntry.filename.toCanonicalString())) + } + } + 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()) + else if (file.contents is EntryDirectory) + return LuaValue.valueOf(file.contents.entries.size) + } + 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)!! + + 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.file) + } + catch (e: FileNotFoundException) { + // roll back delete on disk2 + if (oldFile != null) { + VDUtil.addFile(disk2, oldFile.parent.entryID, oldFile.file) + 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, 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) + 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, sysCharset)) + } + } + + private class FileClassReadAll(val file: EntryFile) : ZeroArgFunction() { + override fun call() : LuaValue { + return LuaValue.valueOf(String(file.bytes, 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 + } + } +} + +/** + * drops appended NULs and return resulting ByteArray as String + */ +private fun ByteArray.toCanonicalString(): String { + var lastIndexOfRealStr = 0 + for (i in this.lastIndex downTo 0) { + if (this[i] != 0.toByte()) { + lastIndexOfRealStr = i + break + } + } + return String(this.sliceArray(0..lastIndexOfRealStr)) +} diff --git a/src/net/torvald/terrarum/virtualcomputer/luaapi/Term.kt b/src/net/torvald/terrarum/virtualcomputer/luaapi/Term.kt index d1c6cb878..b3b685f74 100644 --- a/src/net/torvald/terrarum/virtualcomputer/luaapi/Term.kt +++ b/src/net/torvald/terrarum/virtualcomputer/luaapi/Term.kt @@ -54,8 +54,9 @@ internal class Term(globals: Globals, term: Teletype) { companion object { fun LuaValue.checkIBM437(): String { if (this is LuaString) - return m_bytes.copyOfRange(m_offset, m_length).toString(Charset.forName("ISO-8859-1")) + return m_bytes.copyOfRange(m_offset, m_offset + m_length).toString(Charset.forName("CP437")) // it only works if Charset is ISO-8859, despite of the name "IBM437" + // --> then would "CP437" work? -- Torvald at 2017-04-05 else throw LuaError("bad argument (string expected, got ${this.typename()})") } diff --git a/src/net/torvald/terrarum/virtualcomputer/tvd/VDUtil.kt b/src/net/torvald/terrarum/virtualcomputer/tvd/VDUtil.kt new file mode 100644 index 000000000..2363e3436 --- /dev/null +++ b/src/net/torvald/terrarum/virtualcomputer/tvd/VDUtil.kt @@ -0,0 +1,816 @@ +package net.torvald.terrarum.virtualcomputer.tvd + +import java.io.* +import java.nio.charset.Charset +import java.util.* +import java.util.logging.Level +import javax.naming.OperationNotSupportedException +import kotlin.collections.ArrayList + +/** + * Created by SKYHi14 on 2017-04-01. + */ +object VDUtil { + class VDPath() { + /** + * input: (root)->etc->boot in Constructor + * output: ByteArrayListOf( + * e t c \0 \0 \0 \0 \0 ... , + * b o o t \0 \0 \0 \0 ... + * ) + * + * input: "/" + * interpretation: (root) + * output: ByteArrayListOf( + * ) + */ + var hierarchy = ArrayList() + + val lastIndex: Int + get() = hierarchy.lastIndex + fun last(): ByteArray = hierarchy.last() + + constructor(strPath: String, charset: Charset) : this() { + val unsanitisedHierarchy = ArrayList() + strPath.sanitisePath().split('/').forEach { unsanitisedHierarchy.add(it) } + + // deal with bad slashes (will drop '' and tail '') + // "/bin/boot/drivers/" -> "bin/boot/drivers" + // removes head slash + if (unsanitisedHierarchy[0].isEmpty()) + unsanitisedHierarchy.removeAt(0) + // removes tail slash + if (unsanitisedHierarchy.size > 0 && + unsanitisedHierarchy[unsanitisedHierarchy.lastIndex].isEmpty()) + unsanitisedHierarchy.removeAt(unsanitisedHierarchy.lastIndex) + + unsanitisedHierarchy.forEach { + hierarchy.add(it.toEntryName(DiskEntry.NAME_LENGTH, charset)) + } + } + + private constructor(newHierarchy: ArrayList) : this() { + hierarchy = newHierarchy + } + + override fun toString(): String { + val sb = StringBuilder() + if (hierarchy.size > 0) { + sb.append(hierarchy[0].toCanonicalString()) + } + if (hierarchy.size > 1) { + (1..hierarchy.lastIndex).forEach { + sb.append('/') + sb.append(hierarchy[it].toCanonicalString()) + } + } + + return sb.toString() + } + + operator fun get(i: Int) = hierarchy[i] + fun forEach(action: (ByteArray) -> Unit) = hierarchy.forEach(action) + fun forEachIndexed(action: (Int, ByteArray) -> Unit) = hierarchy.forEachIndexed(action) + + fun getParent(ancestorCount: Int = 1): VDPath { + val newPath = ArrayList() + hierarchy.forEach { newPath.add(it) } + + repeat(ancestorCount) { newPath.removeAt(newPath.lastIndex) } + return VDPath(newPath) + } + } + + fun dumpToRealMachine(disk: VirtualDisk, outfile: File) { + if (!outfile.exists()) outfile.createNewFile() + outfile.writeBytes(disk.serialize().array) + } + + /** + * Reads serialised binary and returns corresponding VirtualDisk instance. + * + * @param crcWarnLevel Level.OFF -- no warning, Level.WARNING -- print out warning, Level.SEVERE -- throw error + */ + fun readDiskArchive(infile: File, crcWarnLevel: Level = Level.SEVERE, warningFunc: ((String) -> Unit)? = null, charset: Charset): VirtualDisk { + val inbytes = infile.readBytes() + + if (magicMismatch(VirtualDisk.MAGIC, inbytes)) + throw RuntimeException("Invalid Virtual Disk file!") + + val diskSize = inbytes.sliceArray(4..7).toIntBig() + val diskName = inbytes.sliceArray(8..8 + 31) + val diskCRC = inbytes.sliceArray(8 + 32..8 + 32 + 3).toIntBig() // to check with completed vdisk + + val vdisk = VirtualDisk(diskSize, diskName) + + //println("[VDUtil] currentUnixtime = $currentUnixtime") + + var entryOffset = 44 + while (!Arrays.equals(inbytes.sliceArray(entryOffset..entryOffset + 3), VirtualDisk.FOOTER_START_MARK)) { + //println("[VDUtil] entryOffset = $entryOffset") + // read and prepare all the shits + val entryIndexNum = inbytes.sliceArray(entryOffset..entryOffset + 3).toIntBig() + val entryTypeFlag = inbytes[entryOffset + 4] + val entryFileName = inbytes.sliceArray(entryOffset + 5..entryOffset + 260) + val entryCreationTime = inbytes.sliceArray(entryOffset + 261..entryOffset + 268).toLongBig() + val entryModifyTime = inbytes.sliceArray(entryOffset + 269..entryOffset + 276).toLongBig() + val entryCRC = inbytes.sliceArray(entryOffset + 277..entryOffset + 280).toIntBig() // to check with completed entry + + val entryData = when (entryTypeFlag) { + DiskEntry.NORMAL_FILE -> { + val filesize = inbytes.sliceArray(entryOffset + 281..entryOffset + 284).toIntBig() + //println("[VDUtil] --> is file; filesize = $filesize") + inbytes.sliceArray(entryOffset + 285..entryOffset + 284 + filesize) + } + DiskEntry.DIRECTORY -> { + val entryCount = inbytes.sliceArray(entryOffset + 281..entryOffset + 282).toShortBig() + //println("[VDUtil] --> is directory; entryCount = $entryCount") + inbytes.sliceArray(entryOffset + 283..entryOffset + 282 + entryCount * 4) + } + DiskEntry.SYMLINK -> { + inbytes.sliceArray(entryOffset + 281..entryOffset + 284) + } + else -> throw RuntimeException("Unknown entry with type $entryTypeFlag") + } + + + + // update entryOffset so that we can fetch next entry in the binary + entryOffset += 281 + entryData.size + when (entryTypeFlag) { + DiskEntry.NORMAL_FILE -> 4 + DiskEntry.DIRECTORY -> 2 + DiskEntry.SYMLINK -> 0 + else -> throw RuntimeException("Unknown entry with type $entryTypeFlag") + } + + + // create entry + val diskEntry = DiskEntry( + entryID = entryIndexNum, + filename = entryFileName, + creationDate = entryCreationTime, + modificationDate = entryModifyTime, + contents = if (entryTypeFlag == DiskEntry.NORMAL_FILE) { + EntryFile(entryData) + } + else if (entryTypeFlag == DiskEntry.DIRECTORY) { + val entryList = ArrayList() + (0..entryData.size / 4 - 1).forEach { + entryList.add(entryData.sliceArray(4 * it..4 * it + 3).toIntBig()) + } + + EntryDirectory(entryList) + } + else if (entryTypeFlag == DiskEntry.SYMLINK) { + EntrySymlink(entryData.toIntBig()) + } + else + throw RuntimeException("Unknown entry with type $entryTypeFlag") + ) + + // check CRC of entry + if (crcWarnLevel == Level.SEVERE || crcWarnLevel == Level.WARNING) { + val calculatedCRC = diskEntry.hashCode() + + val crcMsg = "CRC failed: expected ${entryCRC.toHex()}, got ${calculatedCRC.toHex()}\n" + + "at file \"${diskEntry.getFilenameString(charset)}\" (entry ID ${diskEntry.entryID})" + + if (calculatedCRC != entryCRC) { + if (crcWarnLevel == Level.SEVERE) + throw IOException(crcMsg) + else if (warningFunc != null) + warningFunc(crcMsg) + } + } + + // add entry to disk + vdisk.entries[entryIndexNum] = diskEntry + } + + + // check CRC of disk + if (crcWarnLevel == Level.SEVERE || crcWarnLevel == Level.WARNING) { + val calculatedCRC = vdisk.hashCode() + + val crcMsg = "Disk CRC failed: expected ${diskCRC.toHex()}, got ${calculatedCRC.toHex()}" + + if (calculatedCRC != diskCRC) { + if (crcWarnLevel == Level.SEVERE) + throw IOException(crcMsg) + else if (warningFunc != null) + warningFunc(crcMsg) + } + } + + return vdisk + } + + + /** + * Get list of entries of directory. + */ + fun getDirectoryEntries(disk: VirtualDisk, entry: DiskEntry): Array { + if (entry.contents !is EntryDirectory) + throw IllegalArgumentException("The entry is not directory") + + val entriesList = ArrayList() + entry.contents.entries.forEach { + val entry = disk.entries[it] + if (entry != null) entriesList.add(entry) + } + + return entriesList.toTypedArray() + } + /** + * Get list of entries of directory. + */ + fun getDirectoryEntries(disk: VirtualDisk, entryID: EntryID): Array { + val entry = disk.entries[entryID] + if (entry == null) { + throw IOException("Entry does not exist") + } + else { + return getDirectoryEntries(disk, entry) + } + } + + /** + * Search a entry using path + * @return Pair of , or null if not found + */ + fun getFile(disk: VirtualDisk, path: VDPath): EntrySearchResult? { + val searchHierarchy = ArrayList() + fun getCurrentEntry(): DiskEntry = searchHierarchy.last() + //var currentDirectory = disk.root + + searchHierarchy.add(disk.entries[0]!!) + + // path of root + if (path.hierarchy.size == 0) { + return EntrySearchResult( + disk.entries[0]!!, + disk.entries[0]!! + ) + } + + try { + // search for the file + path.forEachIndexed { i, nameToSearch -> + // if we hit the last elem, we won't search more + if (i <= path.lastIndex) { + val currentDirEntries = getDirectoryEntries(disk, getCurrentEntry()) + + var fileFound: DiskEntry? = null + for (entry in currentDirEntries) { + if (Arrays.equals(entry.filename, nameToSearch)) { + fileFound = entry + break + } + } + if (fileFound == null) { // file not found + throw KotlinNullPointerException() + } + else { // file found + searchHierarchy.add(fileFound) + } + } + } + } + catch (e1: KotlinNullPointerException) { + return null + } + + // file found + return EntrySearchResult( + searchHierarchy[searchHierarchy.lastIndex], + searchHierarchy[searchHierarchy.lastIndex - 1] + ) + } + + /** + * SYNOPSIS disk.getFile("bin/msh.lua")!!.file.getAsNormalFile(disk) + * + * Use VirtualDisk.getAsNormalFile(path) + */ + private fun DiskEntry.getAsNormalFile(disk: VirtualDisk): EntryFile = + this.contents as? EntryFile ?: + if (this.contents is EntryDirectory) + throw RuntimeException("this is directory") + else if (this.contents is EntrySymlink) + disk.entries[this.contents.target]!!.getAsNormalFile(disk) + else + throw RuntimeException("Unknown entry type") + /** + * SYNOPSIS disk.getFile("bin/msh.lua")!!.first.getAsNormalFile(disk) + * + * Use VirtualDisk.getAsNormalFile(path) + */ + private fun DiskEntry.getAsDirectory(disk: VirtualDisk): EntryDirectory = + this.contents as? EntryDirectory ?: + if (this.contents is EntrySymlink) + disk.entries[this.contents.target]!!.getAsDirectory(disk) + else if (this.contents is EntryFile) + throw RuntimeException("this is not directory") + else + throw RuntimeException("Unknown entry type") + + /** + * Search for the file and returns a instance of normal file. + */ + fun getAsNormalFile(disk: VirtualDisk, path: VDPath) = + getFile(disk, path)!!.file.getAsNormalFile(disk) + /** + * Fetch the file and returns a instance of normal file. + */ + fun getAsNormalFile(disk: VirtualDisk, entryIndex: EntryID) = + disk.entries[entryIndex]!!.getAsNormalFile(disk) + /** + * Search for the file and returns a instance of directory. + */ + fun getAsDirectory(disk: VirtualDisk, path: VDPath) = + getFile(disk, path)!!.file.getAsDirectory(disk) + /** + * Fetch the file and returns a instance of directory. + */ + fun getAsDirectory(disk: VirtualDisk, entryIndex: EntryID) = + disk.entries[entryIndex]!!.getAsDirectory(disk) + /** + * Deletes file on the disk safely. + */ + fun deleteFile(disk: VirtualDisk, path: VDPath) { + disk.checkReadOnly() + + val fileSearchResult = getFile(disk, path)!! + + return deleteFile(disk, fileSearchResult.parent.entryID, fileSearchResult.file.entryID) + } + /** + * Deletes file on the disk safely. + */ + fun deleteFile(disk: VirtualDisk, parentID: EntryID, targetID: EntryID) { + disk.checkReadOnly() + + val file = disk.entries[targetID] + val parentDir = disk.entries[parentID] + + fun rollback() { + if (!disk.entries.contains(targetID)) { + disk.entries[targetID] = file!! + } + if (!(parentDir!!.contents as EntryDirectory).entries.contains(targetID)) { + (parentDir.contents as EntryDirectory).entries.add(targetID) + } + } + + if (parentDir == null || parentDir.contents !is EntryDirectory) { + throw FileNotFoundException("No such parent directory") + } + else if (file == null || !directoryContains(disk, parentID, targetID)) { + throw FileNotFoundException("No such file to delete") + } + else if (targetID == 0) { + throw IOException("Cannot delete root file system") + } + else if (file.contents is EntryDirectory && file.contents.entries.size > 0) { + throw IOException("Cannot delete directory that contains something") + } + else { + try { + // delete file record + disk.entries.remove(targetID) + // unlist file from parent directly + (disk.entries[parentID]!!.contents as EntryDirectory).entries.remove(targetID) + } + catch (e: Exception) { + rollback() + throw InternalError("Unknown error *sigh* It's annoying, I know.") + } + } + } + /** + * Changes the name of the entry. + */ + fun renameFile(disk: VirtualDisk, path: VDPath, newName: String, charset: Charset) { + val file = getFile(disk, path)?.file + + if (file != null) { + file.filename = newName.sanitisePath().toEntryName(DiskEntry.NAME_LENGTH, charset) + } + else { + throw FileNotFoundException() + } + } + /** + * Changes the name of the entry. + */ + fun renameFile(disk: VirtualDisk, fileID: EntryID, newName: String, charset: Charset) { + val file = disk.entries[fileID] + + if (file != null) { + file.filename = newName.sanitisePath().toEntryName(DiskEntry.NAME_LENGTH, charset) + } + else { + throw FileNotFoundException() + } + } + /** + * Add file to the specified directory. + */ + fun addFile(disk: VirtualDisk, parentPath: VDPath, file: DiskEntry) { + disk.checkReadOnly() + disk.checkCapacity(file.serialisedSize) + + try { + // add record to the directory + getAsDirectory(disk, parentPath).entries.add(file.entryID) + // add entry on the disk + disk.entries[file.entryID] = file + } + catch (e: KotlinNullPointerException) { + throw FileNotFoundException("No such directory") + } + } + /** + * Add file to the specified directory. + */ + fun addFile(disk: VirtualDisk, directoryID: EntryID, file: DiskEntry) { + disk.checkReadOnly() + disk.checkCapacity(file.serialisedSize) + + try { + // add record to the directory + getAsDirectory(disk, directoryID).entries.add(file.entryID) + // add entry on the disk + disk.entries[file.entryID] = file + } + catch (e: KotlinNullPointerException) { + throw FileNotFoundException("No such directory") + } + } + /** + * Add subdirectory to the specified directory. + */ + fun addDir(disk: VirtualDisk, parentPath: VDPath, name: ByteArray) { + disk.checkReadOnly() + disk.checkCapacity(EntryDirectory.NEW_ENTRY_SIZE) + + val newID = disk.generateUniqueID() + + try { + // add record to the directory + getAsDirectory(disk, parentPath).entries.add(newID) + // add entry on the disk + disk.entries[newID] = DiskEntry( + newID, + name, + currentUnixtime, + currentUnixtime, + EntryDirectory() + ) + } + catch (e: KotlinNullPointerException) { + throw FileNotFoundException("No such directory") + } + } + /** + * Add file to the specified directory. + */ + fun addDir(disk: VirtualDisk, directoryID: EntryID, name: ByteArray) { + disk.checkReadOnly() + disk.checkCapacity(EntryDirectory.NEW_ENTRY_SIZE) + + val newID = disk.generateUniqueID() + + try { + // add record to the directory + getAsDirectory(disk, directoryID).entries.add(newID) + // add entry on the disk + disk.entries[newID] = DiskEntry( + newID, + name, + currentUnixtime, + currentUnixtime, + EntryDirectory() + ) + } + catch (e: KotlinNullPointerException) { + throw FileNotFoundException("No such directory") + } + } + + + /** + * Imports external file and returns corresponding DiskEntry. + */ + fun importFile(file: File, id: EntryID): DiskEntry { + if (file.isDirectory) { + throw IOException("The file is a directory") + } + + return DiskEntry( + entryID = id, + filename = file.name.toByteArray(), + creationDate = currentUnixtime, + modificationDate = currentUnixtime, + contents = EntryFile(file.readBytes()) + ) + } + /** + * Export file on the virtual disk into real disk. + */ + fun exportFile(entryFile: EntryFile, outfile: File) { + outfile.createNewFile() + outfile.writeBytes(entryFile.bytes) + } + + /** + * Check for name collision in specified directory. + */ + fun nameExists(disk: VirtualDisk, name: String, directoryID: EntryID, charset: Charset): Boolean { + return nameExists(disk, name.toEntryName(256, charset), directoryID) + } + /** + * Check for name collision in specified directory. + */ + fun nameExists(disk: VirtualDisk, name: ByteArray, directoryID: EntryID): Boolean { + val directoryContents = getDirectoryEntries(disk, directoryID) + directoryContents.forEach { + if (Arrays.equals(name, it.filename)) + return true + } + return false + } + + /** + * Move file from to there, overwrite + */ + fun moveFile(disk1: VirtualDisk, fromPath: VDPath, disk2: VirtualDisk, toPath: VDPath) { + val file = getFile(disk1, fromPath) + + if (file != null) { + if (file.file.contents is EntryDirectory) { + throw IOException("Cannot move directory") + } + + disk2.checkCapacity(file.file.contents.getSizeEntry()) + + try { + deleteFile(disk2, toPath) + } + catch (e: KotlinNullPointerException) { "Nothing to delete beforehand" } + + deleteFile(disk1, fromPath) // any uncaught no_from_file will be caught here + try { + addFile(disk2, toPath.getParent(), file.file) + } + catch (e: FileNotFoundException) { + // roll back delete on disk1 + addFile(disk1, file.parent.entryID, file.file) + throw FileNotFoundException("No such destination") + } + } + else { + throw FileNotFoundException("No such file to move") + } + } + + + /** + * Creates new disk with given name and capacity + */ + fun createNewDisk(diskSize: Int, diskName: String, charset: Charset): VirtualDisk { + val newdisk = VirtualDisk(diskSize, diskName.toEntryName(VirtualDisk.NAME_LENGTH, charset)) + val rootDir = DiskEntry( + entryID = 0, + filename = DiskEntry.ROOTNAME.toByteArray(charset), + creationDate = currentUnixtime, + modificationDate = currentUnixtime, + contents = EntryDirectory() + ) + + newdisk.entries[0] = rootDir + + return newdisk + } + /** + * Creates new zero-filled file with given name and size + */ + fun createNewBlankFile(disk: VirtualDisk, directoryID: EntryID, fileSize: Int, filename: String, charset: Charset) { + disk.checkReadOnly() + disk.checkCapacity(fileSize + DiskEntry.HEADER_SIZE + 4) + + addFile(disk, directoryID, DiskEntry( + disk.generateUniqueID(), + filename.toEntryName(DiskEntry.NAME_LENGTH, charset = charset), + currentUnixtime, + currentUnixtime, + EntryFile(fileSize) + )) + } + + + /** + * Throws an exception if the disk is read-only + */ + fun VirtualDisk.checkReadOnly() { + if (this.isReadOnly) + throw IOException("Disk is read-only") + } + /** + * Throws an exception if specified size cannot fit into the disk + */ + fun VirtualDisk.checkCapacity(newSize: Int) { + if (this.usedBytes + newSize > this.capacity) + throw IOException("Not enough space on the disk") + } + fun ByteArray.toIntBig(): Int { + if (this.size != 4) + throw OperationNotSupportedException("ByteArray is not Int") + + var i = 0 + this.forEachIndexed { index, byte -> i += byte.toUint().shl(24 - index * 8)} + return i + } + fun ByteArray.toLongBig(): Long { + if (this.size != 8) + throw OperationNotSupportedException("ByteArray is not Long") + + var i = 0L + this.forEachIndexed { index, byte -> i += byte.toUint().shl(56 - index * 8)} + return i + } + fun ByteArray.toShortBig(): Short { + if (this.size != 2) + throw OperationNotSupportedException("ByteArray is not Long") + + return (this[0].toUint().shl(256) + this[1].toUint()).toShort() + } + fun String.sanitisePath(): String { + val invalidChars = Regex("""[<>:"|?*\u0000-\u001F]""") + if (this.contains(invalidChars)) + throw IOException("path contains invalid characters") + + val path1 = this.replace('\\', '/') + return path1 + } + data class EntrySearchResult(val file: DiskEntry, val parent: DiskEntry) + + fun resolveIfSymlink(disk: VirtualDisk, indexNumber: EntryID, recurse: Boolean = false): DiskEntry { + var entry: DiskEntry? = disk.entries[indexNumber] + if (entry == null) throw IOException("File does not exist") + if (entry.contents !is EntrySymlink) return entry + if (recurse) { + while (entry!!.contents is EntrySymlink) { + entry = disk.entries[(entry.contents as EntrySymlink).target] + if (entry == null) break + } + } + else { + entry = disk.entries[(entry.contents as EntrySymlink).target] + } + if (entry == null) throw IOException("Pointing file does not exist") + return entry + } + + val currentUnixtime: Long + get() = System.currentTimeMillis() / 1000 + + fun directoryContains(disk: VirtualDisk, dirID: EntryID, targetID: EntryID): Boolean { + val dir = resolveIfSymlink(disk, dirID) + + if (dir.contents !is EntryDirectory) { + throw FileNotFoundException("Not a directory") + } + else { + return dir.contents.entries.contains(targetID) + } + } +} + +fun Byte.toUint() = java.lang.Byte.toUnsignedInt(this) +fun magicMismatch(magic: ByteArray, array: ByteArray): Boolean { + return !Arrays.equals(array.sliceArray(0..magic.lastIndex), magic) +} +fun String.toEntryName(length: Int, charset: Charset): ByteArray { + val buffer = AppendableByteBuffer(length) + val stringByteArray = this.toByteArray(charset) + buffer.put(stringByteArray.sliceArray(0..minOf(length, stringByteArray.size) - 1)) + return buffer.array +} +fun ByteArray.toCanonicalString(): String { + var lastIndexOfRealStr = 0 + for (i in this.lastIndex downTo 0) { + if (this[i] != 0.toByte()) { + lastIndexOfRealStr = i + break + } + } + return String(this.sliceArray(0..lastIndexOfRealStr)) +} + +/** + * Writes String to the file + * + * @param fileEntry must be File, resolve symlink beforehand + * @param mode "w" or "a" + */ +class VDFileWriter(private val fileEntry: DiskEntry, private val append: Boolean, val charset: Charset) : Writer() { + + private @Volatile var newFileBuffer = ArrayList() + + private @Volatile var closed = false + + init { + if (fileEntry.contents !is EntryFile) { + throw FileNotFoundException("Not a file") + } + } + + override fun write(cbuf: CharArray, off: Int, len: Int) { + if (!closed) { + val newByteArray = String(cbuf).toByteArray(charset) + newFileBuffer.addAll(newByteArray.asIterable()) + } + else { + throw IOException() + } + } + + override fun flush() { + if (!closed) { + val newByteArray = newFileBuffer.toByteArray() + + if (!append) { + (fileEntry.contents as EntryFile).bytes = newByteArray + } + else { + val oldByteArray = (fileEntry.contents as EntryFile).bytes.copyOf() + val newFileBuffer = ByteArray(oldByteArray.size + newByteArray.size) + + System.arraycopy(oldByteArray, 0, newFileBuffer, 0, oldByteArray.size) + System.arraycopy(newByteArray, 0, newFileBuffer, oldByteArray.size, newByteArray.size) + + (fileEntry.contents as EntryFile).bytes = newByteArray + } + + newFileBuffer = ArrayList() + + fileEntry.modificationDate = VDUtil.currentUnixtime + } + else { + throw IOException() + } + } + + override fun close() { + flush() + closed = true + } +} + +class VDFileOutputStream(private val fileEntry: DiskEntry, private val append: Boolean, val charset: Charset) : OutputStream() { + + private @Volatile var newFileBuffer = ArrayList() + + private @Volatile var closed = false + + override fun write(b: Int) { + if (!closed) { + newFileBuffer.add(b.toByte()) + } + else { + throw IOException() + } + } + + override fun flush() { + if (!closed) { + val newByteArray = newFileBuffer.toByteArray() + + if (!append) { + (fileEntry.contents as EntryFile).bytes = newByteArray + } + else { + val oldByteArray = (fileEntry.contents as EntryFile).bytes.copyOf() + val newFileBuffer = ByteArray(oldByteArray.size + newByteArray.size) + + System.arraycopy(oldByteArray, 0, newFileBuffer, 0, oldByteArray.size) + System.arraycopy(newByteArray, 0, newFileBuffer, oldByteArray.size, newByteArray.size) + + (fileEntry.contents as EntryFile).bytes = newByteArray + } + + newFileBuffer = ArrayList() + + fileEntry.modificationDate = VDUtil.currentUnixtime + } + else { + throw IOException() + } + } + + override fun close() { + flush() + closed = true + } +} \ No newline at end of file diff --git a/src/net/torvald/terrarum/virtualcomputer/tvd/VirtualDisk.kt b/src/net/torvald/terrarum/virtualcomputer/tvd/VirtualDisk.kt new file mode 100644 index 000000000..885f08568 --- /dev/null +++ b/src/net/torvald/terrarum/virtualcomputer/tvd/VirtualDisk.kt @@ -0,0 +1,246 @@ +package net.torvald.terrarum.virtualcomputer.tvd + +import java.nio.charset.Charset +import java.util.* +import java.util.function.Consumer +import java.util.zip.CRC32 + +/** + * Created by SKYHi14 on 2017-03-31. + */ + +typealias EntryID = Int + +class VirtualDisk( + /** capacity of 0 makes the disk read-only */ + var capacity: Int, + var diskName: ByteArray = ByteArray(NAME_LENGTH) +) { + val entries = HashMap() + val isReadOnly: Boolean + get() = capacity == 0 + fun getDiskNameString(charset: Charset) = String(diskName, charset) + val root: DiskEntry + get() = entries[0]!! + + + private fun serializeEntriesOnly(): ByteArray { + val bufferList = ArrayList() + entries.forEach { + val serialised = it.value.serialize() + serialised.forEach { bufferList.add(it) } + } + + return ByteArray(bufferList.size, { bufferList[it] }) + } + + fun serialize(): AppendableByteBuffer { + val entriesBuffer = serializeEntriesOnly() + val buffer = AppendableByteBuffer(HEADER_SIZE + entriesBuffer.size + FOOTER_SIZE) + val crc = hashCode().toBigEndian() + + buffer.put(MAGIC) + buffer.put(capacity.toBigEndian()) + buffer.put(diskName.forceSize(NAME_LENGTH)) + buffer.put(crc) + buffer.put(entriesBuffer) + buffer.put(FOOTER_START_MARK) + buffer.put(EOF_MARK) + + return buffer + } + + override fun hashCode(): Int { + val crcList = IntArray(entries.size) + var crcListAppendCursor = 0 + entries.forEach { t, u -> + crcList[crcListAppendCursor] = u.hashCode() + crcListAppendCursor++ + } + crcList.sort() + val crc = CRC32() + crcList.forEach { crc.update(it) } + + return crc.value.toInt() + } + + /** Expected size of the virtual disk */ + val usedBytes: Int + get() = entries.map { it.value.serialisedSize }.sum() + HEADER_SIZE + FOOTER_SIZE + + fun generateUniqueID(): Int { + var id: Int + do { + id = Random().nextInt() + } while (null != entries[id] || id == FOOTER_MARKER) + return id + } + + override fun equals(other: Any?) = if (other == null) false else this.hashCode() == other.hashCode() + override fun toString() = "VirtualDisk(name: ${getDiskNameString(Charsets.UTF_8)}, capacity: $capacity bytes, crc: ${hashCode().toHex()})" + + companion object { + val HEADER_SIZE = 44 // according to the spec + val FOOTER_SIZE = 6 // footer mark + EOF + val NAME_LENGTH = 32 + + val MAGIC = "TEVd".toByteArray() + val FOOTER_MARKER = 0xFEFEFEFE.toInt() + val FOOTER_START_MARK = FOOTER_MARKER.toBigEndian() + val EOF_MARK = byteArrayOf(0xFF.toByte(), 0x19.toByte()) + } +} + + +class DiskEntry( + // header + var entryID: EntryID, + var filename: ByteArray = ByteArray(NAME_LENGTH), + var creationDate: Long, + var modificationDate: Long, + + // content + val contents: DiskEntryContent +) { + fun getFilenameString(charset: Charset) = if (entryID == 0) ROOTNAME else String(filename, charset) + + val serialisedSize: Int + get() = contents.getSizeEntry() + HEADER_SIZE + + companion object { + val HEADER_SIZE = 281 // according to the spec + val ROOTNAME = "(root)" + val NAME_LENGTH = 256 + + val NORMAL_FILE = 1.toByte() + val DIRECTORY = 2.toByte() + val SYMLINK = 3.toByte() + private fun DiskEntryContent.getTypeFlag() = + if (this is EntryFile) NORMAL_FILE + else if (this is EntryDirectory) DIRECTORY + else if (this is EntrySymlink) SYMLINK + else 0 // NULL + + fun getTypeString(entry: DiskEntryContent) = when(entry.getTypeFlag()) { + NORMAL_FILE -> "File" + DIRECTORY -> "Directory" + SYMLINK -> "Symbolic Link" + else -> "(unknown type)" + } + } + + fun serialize(): AppendableByteBuffer { + val serialisedContents = contents.serialize() + val buffer = AppendableByteBuffer(281 + serialisedContents.size) + + buffer.put(entryID.toBigEndian()) + buffer.put(contents.getTypeFlag()) + buffer.put(filename.forceSize(NAME_LENGTH)) + buffer.put(creationDate.toBigEndian()) + buffer.put(modificationDate.toBigEndian()) + buffer.put(this.hashCode().toBigEndian()) + buffer.put(serialisedContents.array) + + return buffer + } + + override fun hashCode() = contents.serialize().getCRC32() + + override fun equals(other: Any?) = if (other == null) false else this.hashCode() == other.hashCode() + + override fun toString() = "DiskEntry(name: ${getFilenameString(Charsets.UTF_8)}, index: $entryID, type: ${contents.getTypeFlag()}, crc: ${hashCode().toHex()})" +} + + +fun ByteArray.forceSize(size: Int): ByteArray { + return ByteArray(size, { if (it < this.size) this[it] else 0.toByte() }) +} +interface DiskEntryContent { + fun serialize(): AppendableByteBuffer + fun getSizePure(): Int + fun getSizeEntry(): Int +} +class EntryFile(var bytes: ByteArray) : DiskEntryContent { + + override fun getSizePure() = bytes.size + override fun getSizeEntry() = getSizePure() + 4 + + /** Create new blank file */ + constructor(size: Int): this(ByteArray(size)) + + override fun serialize(): AppendableByteBuffer { + val buffer = AppendableByteBuffer(getSizeEntry()) + buffer.put(getSizePure().toBigEndian()) + buffer.put(bytes) + return buffer + } +} +class EntryDirectory(val entries: ArrayList = ArrayList()) : DiskEntryContent { + + override fun getSizePure() = entries.size * 4 + override fun getSizeEntry() = getSizePure() + 2 + + override fun serialize(): AppendableByteBuffer { + val buffer = AppendableByteBuffer(getSizeEntry()) + buffer.put(entries.size.toShort().toBigEndian()) + entries.forEach { indexNumber -> buffer.put(indexNumber.toBigEndian()) } + return buffer + } + + companion object { + val NEW_ENTRY_SIZE = DiskEntry.HEADER_SIZE + 4 + } +} +class EntrySymlink(val target: EntryID) : DiskEntryContent { + + override fun getSizePure() = 4 + override fun getSizeEntry() = 4 + + override fun serialize(): AppendableByteBuffer { + val buffer = AppendableByteBuffer(4) + return buffer.put(target.toBigEndian()) + } +} + + +fun Int.toHex() = this.toLong().and(0xFFFFFFFF).toString(16).padStart(8, '0').toUpperCase() +fun Int.toBigEndian(): ByteArray { + return ByteArray(4, { this.ushr(24 - (8 * it)).toByte() }) +} +fun Long.toBigEndian(): ByteArray { + return ByteArray(8, { this.ushr(56 - (8 * it)).toByte() }) + +} +fun Short.toBigEndian(): ByteArray { + return byteArrayOf( + this.div(256).toByte(), + this.toByte() + ) +} +fun AppendableByteBuffer.getCRC32(): Int { + val crc = CRC32() + crc.update(this.array) + return crc.value.toInt() +} +class AppendableByteBuffer(val size: Int) { + val array = ByteArray(size, { 0.toByte() }) + private var offset = 0 + + fun put(byteArray: ByteArray): AppendableByteBuffer { + System.arraycopy( + byteArray, // source + 0, // source pos + array, // destination + offset, // destination pos + byteArray.size // length + ) + offset += byteArray.size + return this + } + fun put(byte: Byte): AppendableByteBuffer { + array[offset] = byte + offset += 1 + return this + } + fun forEach(consumer: (Byte) -> Unit) = array.forEach(consumer) +}