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)
+}