mirror of
https://github.com/curioustorvald/Terrarum.git
synced 2026-03-07 12:21:52 +09:00
vt: new filesystem using tevd
This commit is contained in:
70
.idea/markdown-navigator.xml
generated
Normal file
70
.idea/markdown-navigator.xml
generated
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="MarkdownProjectSettings">
|
||||||
|
<PreviewSettings splitEditorLayout="SPLIT" splitEditorPreview="PREVIEW" useGrayscaleRendering="false" zoomFactor="1.0" maxImageWidth="0" showGitHubPageIfSynced="false" allowBrowsingInPreview="false" synchronizePreviewPosition="true" highlightPreviewType="NONE" highlightFadeOut="5" highlightOnTyping="true" synchronizeSourcePosition="true" verticallyAlignSourceAndPreviewSyncPosition="true" showSearchHighlightsInPreview="false" showSelectionInPreview="true">
|
||||||
|
<PanelProvider>
|
||||||
|
<provider providerId="com.vladsch.idea.multimarkdown.editor.swing.html.panel" providerName="Default - Swing" />
|
||||||
|
</PanelProvider>
|
||||||
|
</PreviewSettings>
|
||||||
|
<ParserSettings>
|
||||||
|
<PegdownExtensions>
|
||||||
|
<option name="ABBREVIATIONS" value="false" />
|
||||||
|
<option name="ANCHORLINKS" value="true" />
|
||||||
|
<option name="ASIDE" value="false" />
|
||||||
|
<option name="ATXHEADERSPACE" value="true" />
|
||||||
|
<option name="AUTOLINKS" value="true" />
|
||||||
|
<option name="DEFINITIONS" value="false" />
|
||||||
|
<option name="FENCED_CODE_BLOCKS" value="true" />
|
||||||
|
<option name="FOOTNOTES" value="false" />
|
||||||
|
<option name="HARDWRAPS" value="false" />
|
||||||
|
<option name="INSERTED" value="false" />
|
||||||
|
<option name="QUOTES" value="false" />
|
||||||
|
<option name="RELAXEDHRULES" value="true" />
|
||||||
|
<option name="SMARTS" value="false" />
|
||||||
|
<option name="STRIKETHROUGH" value="true" />
|
||||||
|
<option name="SUBSCRIPT" value="false" />
|
||||||
|
<option name="SUPERSCRIPT" value="false" />
|
||||||
|
<option name="SUPPRESS_HTML_BLOCKS" value="false" />
|
||||||
|
<option name="SUPPRESS_INLINE_HTML" value="false" />
|
||||||
|
<option name="TABLES" value="true" />
|
||||||
|
<option name="TASKLISTITEMS" value="true" />
|
||||||
|
<option name="TOC" value="false" />
|
||||||
|
<option name="WIKILINKS" value="true" />
|
||||||
|
</PegdownExtensions>
|
||||||
|
<ParserOptions>
|
||||||
|
<option name="COMMONMARK_LISTS" value="false" />
|
||||||
|
<option name="DUMMY" value="false" />
|
||||||
|
<option name="EMOJI_SHORTCUTS" value="true" />
|
||||||
|
<option name="FLEXMARK_FRONT_MATTER" value="false" />
|
||||||
|
<option name="GFM_LOOSE_BLANK_LINE_AFTER_ITEM_PARA" value="true" />
|
||||||
|
<option name="GFM_TABLE_RENDERING" value="true" />
|
||||||
|
<option name="GITBOOK_URL_ENCODING" value="false" />
|
||||||
|
<option name="GITHUB_EMOJI_URL" value="false" />
|
||||||
|
<option name="GITHUB_LISTS" value="true" />
|
||||||
|
<option name="GITHUB_WIKI_LINKS" value="true" />
|
||||||
|
<option name="JEKYLL_FRONT_MATTER" value="false" />
|
||||||
|
<option name="SIM_TOC_BLANK_LINE_SPACER" value="true" />
|
||||||
|
</ParserOptions>
|
||||||
|
</ParserSettings>
|
||||||
|
<HtmlSettings headerTopEnabled="false" headerBottomEnabled="false" bodyTopEnabled="false" bodyBottomEnabled="false" embedUrlContent="false" addPageHeader="true">
|
||||||
|
<GeneratorProvider>
|
||||||
|
<provider providerId="com.vladsch.idea.multimarkdown.editor.swing.html.generator" providerName="Default Swing HTML Generator" />
|
||||||
|
</GeneratorProvider>
|
||||||
|
<headerTop />
|
||||||
|
<headerBottom />
|
||||||
|
<bodyTop />
|
||||||
|
<bodyBottom />
|
||||||
|
</HtmlSettings>
|
||||||
|
<CssSettings previewScheme="UI_SCHEME" cssUri="" isCssUriEnabled="false" isCssTextEnabled="false" isDynamicPageWidth="true">
|
||||||
|
<StylesheetProvider>
|
||||||
|
<provider providerId="com.vladsch.idea.multimarkdown.editor.swing.html.css" providerName="Default Swing Stylesheet" />
|
||||||
|
</StylesheetProvider>
|
||||||
|
<ScriptProviders />
|
||||||
|
<cssText />
|
||||||
|
</CssSettings>
|
||||||
|
<HtmlExportSettings updateOnSave="false" parentDir="$ProjectFileDir$" targetDir="$ProjectFileDir$" cssDir="" scriptDir="" plainHtml="false" imageDir="" copyLinkedImages="false" imageUniquifyType="0" targetExt="" useTargetExt="false" noCssNoScripts="false" linkToExportedHtml="true" exportOnSettingsChange="true" regenerateOnProjectOpen="false" />
|
||||||
|
<LinkMapSettings>
|
||||||
|
<textMaps />
|
||||||
|
</LinkMapSettings>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
3
.idea/markdown-navigator/profiles_settings.xml
generated
Normal file
3
.idea/markdown-navigator/profiles_settings.xml
generated
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<component name="MarkdownNavigator.ProfileManager">
|
||||||
|
<settings default="" pdf-export="" />
|
||||||
|
</component>
|
||||||
@@ -21,7 +21,7 @@ class StateVTTest : BasicGameState() {
|
|||||||
|
|
||||||
// HiRes: 100x64, LoRes: 80x25
|
// HiRes: 100x64, LoRes: 80x25
|
||||||
val computerInside = TerrarumComputer(peripheralSlots = 8)
|
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)
|
computerInside, colour = false, hires = false)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ object Terrarum : StateBasedGame(GAME_NAME) {
|
|||||||
*
|
*
|
||||||
* e.g. 0x02010034 can be translated as 2.1.52
|
* 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 =
|
const val VERSION_STRING: String =
|
||||||
"${VERSION_RAW.ushr(24)}.${VERSION_RAW.and(0xFF0000).ushr(16)}.${VERSION_RAW.and(0xFFFF)}"
|
"${VERSION_RAW.ushr(24)}.${VERSION_RAW.and(0xFF0000).ushr(16)}.${VERSION_RAW.and(0xFFFF)}"
|
||||||
const val NAME = "Terrarum"
|
const val NAME = "Terrarum"
|
||||||
@@ -303,7 +303,7 @@ object Terrarum : StateBasedGame(GAME_NAME) {
|
|||||||
|
|
||||||
gc.graphics.clear() // clean up any 'dust' in the buffer
|
gc.graphics.clear() // clean up any 'dust' in the buffer
|
||||||
|
|
||||||
addState(StateVTTest())
|
//addState(StateVTTest())
|
||||||
//addState(StateGraphicComputerTest())
|
//addState(StateGraphicComputerTest())
|
||||||
//addState(StateTestingLightning())
|
//addState(StateTestingLightning())
|
||||||
//addState(StateSplash())
|
//addState(StateSplash())
|
||||||
@@ -318,7 +318,7 @@ object Terrarum : StateBasedGame(GAME_NAME) {
|
|||||||
//addState(StateMidiInputTest())
|
//addState(StateMidiInputTest())
|
||||||
//addState(StateNewRunesTest())
|
//addState(StateNewRunesTest())
|
||||||
|
|
||||||
//ingame = StateInGame(); addState(ingame)
|
ingame = StateInGame(); addState(ingame)
|
||||||
|
|
||||||
|
|
||||||
// foolproof
|
// foolproof
|
||||||
@@ -650,4 +650,6 @@ operator fun Color.minus(other: Color) = Color(
|
|||||||
this.a - other.a
|
this.a - other.a
|
||||||
)
|
)
|
||||||
|
|
||||||
fun Int.toHex() = Integer.toHexString(this)
|
fun Int.toHex() = this.toString(16)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import net.torvald.terrarum.gameactors.ActorWithSprite
|
|||||||
import net.torvald.terrarum.mapdrawer.FeaturesDrawer
|
import net.torvald.terrarum.mapdrawer.FeaturesDrawer
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.GridLayout
|
import java.awt.GridLayout
|
||||||
|
import java.awt.event.MouseAdapter
|
||||||
import java.awt.event.MouseEvent
|
import java.awt.event.MouseEvent
|
||||||
import java.awt.event.MouseListener
|
import java.awt.event.MouseListener
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
@@ -58,11 +59,7 @@ class ActorValueTracker constructor() : JFrame() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// button listener for buttons
|
// button listener for buttons
|
||||||
buttonAddAV.addMouseListener(object : MouseListener {
|
buttonAddAV.addMouseListener(object : MouseAdapter() {
|
||||||
override fun mouseEntered(e: MouseEvent?) { }
|
|
||||||
override fun mouseClicked(e: MouseEvent?) { }
|
|
||||||
override fun mouseReleased(e: MouseEvent?) { }
|
|
||||||
override fun mouseExited(e: MouseEvent?) { }
|
|
||||||
override fun mousePressed(e: MouseEvent?) {
|
override fun mousePressed(e: MouseEvent?) {
|
||||||
if (actor != null && modavInputKey.text.isNotBlank() && modavInputValue.text.isNotBlank()) {
|
if (actor != null && modavInputKey.text.isNotBlank() && modavInputValue.text.isNotBlank()) {
|
||||||
SetAV.execute((
|
SetAV.execute((
|
||||||
@@ -74,11 +71,7 @@ class ActorValueTracker constructor() : JFrame() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
buttonDelAV.addMouseListener(object : MouseListener {
|
buttonDelAV.addMouseListener(object : MouseAdapter() {
|
||||||
override fun mouseEntered(e: MouseEvent?) { }
|
|
||||||
override fun mouseClicked(e: MouseEvent?) { }
|
|
||||||
override fun mouseReleased(e: MouseEvent?) { }
|
|
||||||
override fun mouseExited(e: MouseEvent?) { }
|
|
||||||
override fun mousePressed(e: MouseEvent?) {
|
override fun mousePressed(e: MouseEvent?) {
|
||||||
if (actorValue != null && modavInputKey.text.isNotBlank()) {
|
if (actorValue != null && modavInputKey.text.isNotBlank()) {
|
||||||
actorValue!!.remove(modavInputKey.text)
|
actorValue!!.remove(modavInputKey.text)
|
||||||
@@ -87,11 +80,7 @@ class ActorValueTracker constructor() : JFrame() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
buttonChangeActor.addMouseListener(object : MouseListener {
|
buttonChangeActor.addMouseListener(object : MouseAdapter() {
|
||||||
override fun mouseEntered(e: MouseEvent?) { }
|
|
||||||
override fun mouseClicked(e: MouseEvent?) { }
|
|
||||||
override fun mouseReleased(e: MouseEvent?) { }
|
|
||||||
override fun mouseExited(e: MouseEvent?) { }
|
|
||||||
override fun mousePressed(e: MouseEvent?) {
|
override fun mousePressed(e: MouseEvent?) {
|
||||||
if (actorIDField.text.toLowerCase() == "player") {
|
if (actorIDField.text.toLowerCase() == "player") {
|
||||||
actor = Terrarum.ingame!!.player
|
actor = Terrarum.ingame!!.player
|
||||||
|
|||||||
@@ -121,29 +121,17 @@ class ActorInventory() {
|
|||||||
return capacityMode
|
return capacityMode
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getTotalWeight(): Double {
|
fun getTotalWeight(): Double = itemList.map { it.item.mass * it.amount }.sum()
|
||||||
var weight = 0.0
|
|
||||||
itemList.forEach { weight += it.item.mass * it.amount }
|
|
||||||
|
|
||||||
return weight
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Real amount
|
* Real amount
|
||||||
*/
|
*/
|
||||||
fun getTotalCount(): Int {
|
fun getTotalCount(): Int = itemList.map { it.amount }.sum()
|
||||||
var count = 0
|
|
||||||
itemList.forEach { count += it.amount }
|
|
||||||
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unique amount, multiple items are calculated as one
|
* Unique amount, multiple items are calculated as one
|
||||||
*/
|
*/
|
||||||
fun getTotalUniqueCount(): Int {
|
fun getTotalUniqueCount(): Int = itemList.size
|
||||||
return itemList.size
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether the itemList contains too many items
|
* Check whether the itemList contains too many items
|
||||||
|
|||||||
@@ -111,19 +111,15 @@ object DecodeTapestry {
|
|||||||
val FORMAT_64 = 2
|
val FORMAT_64 = 2
|
||||||
|
|
||||||
operator fun invoke(fileObj: File): TapestryObject {
|
operator fun invoke(fileObj: File): TapestryObject {
|
||||||
fun magicMismatch(magic: ByteArray): Boolean {
|
fun magicMismatch(magic: ByteArray, array: ByteArray): Boolean {
|
||||||
MAGIC.forEachIndexed { i, byte ->
|
return !Arrays.equals(array.sliceArray(0..magic.lastIndex), magic)
|
||||||
if (byte != magic[i])
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val file = fileObj.readBytes()
|
val file = fileObj.readBytes()
|
||||||
|
|
||||||
val magic = file.copyOfRange(0, 4)
|
val magic = file.copyOfRange(0, 4)
|
||||||
|
|
||||||
if (magicMismatch(magic))
|
if (magicMismatch(MAGIC, magic))
|
||||||
throw RuntimeException("Invalid file -- type mismatch: expected header " +
|
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]}; got " +
|
||||||
"${magic[0]} ${magic[1]} ${magic[2]} ${magic[3]}")
|
"${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 artName = String(artNameBytes.toByteArray(), charset = Charset.forName("UTF-8"))
|
||||||
val authorName = kotlin.text.String(authorNameBytes.toByteArray(), charset = Charset.forName("UTF-8"))
|
val authorName = String(authorNameBytes.toByteArray(), charset = Charset.forName("UTF-8"))
|
||||||
|
|
||||||
val imageDataSize = file.size - readCounter
|
val imageDataSize = file.size - readCounter
|
||||||
val height = imageDataSize / width
|
val height = imageDataSize / width
|
||||||
|
|||||||
@@ -1015,8 +1015,6 @@ sandbox._G = sandbox
|
|||||||
-- path for any ingame libraries
|
-- path for any ingame libraries
|
||||||
package.path = "/net/torvald/terrarum/virtualcomputer/assets/lua/?.lua;" .. package.path
|
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 screenbufferdim = term.width() * term.height()
|
||||||
local screencolours = 4
|
local screencolours = 4
|
||||||
if term.isCol() then screencolours = 8
|
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.verbose then computer.verbose = true end -- print debug info
|
||||||
if not computer.loadedCLayer then computer.loadedCLayer = {} end -- list of loaded compatibility layers
|
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 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
|
if not computer.OEM then computer.OEM = "" end
|
||||||
machine.totalMemory = _G.totalMemory
|
machine.totalMemory = _G.totalMemory
|
||||||
if not computer.bellpitch then computer.bellpitch = 950 end
|
if not computer.bellpitch then computer.bellpitch = 950 end
|
||||||
|
|||||||
@@ -9,10 +9,13 @@ import org.luaj.vm2.lib.ZeroArgFunction
|
|||||||
import org.luaj.vm2.lib.jse.JsePlatform
|
import org.luaj.vm2.lib.jse.JsePlatform
|
||||||
import net.torvald.terrarum.KVHashMap
|
import net.torvald.terrarum.KVHashMap
|
||||||
import net.torvald.terrarum.Millisec
|
import net.torvald.terrarum.Millisec
|
||||||
|
import net.torvald.terrarum.Terrarum
|
||||||
import net.torvald.terrarum.gameactors.roundInt
|
import net.torvald.terrarum.gameactors.roundInt
|
||||||
import net.torvald.terrarum.virtualcomputer.luaapi.*
|
import net.torvald.terrarum.virtualcomputer.luaapi.*
|
||||||
import net.torvald.terrarum.virtualcomputer.peripheral.*
|
import net.torvald.terrarum.virtualcomputer.peripheral.*
|
||||||
import net.torvald.terrarum.virtualcomputer.terminal.*
|
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 net.torvald.terrarum.virtualcomputer.worldobject.ComputerPartsCodex
|
||||||
import org.lwjgl.BufferUtils
|
import org.lwjgl.BufferUtils
|
||||||
import org.lwjgl.openal.AL
|
import org.lwjgl.openal.AL
|
||||||
@@ -22,6 +25,8 @@ import org.newdawn.slick.Input
|
|||||||
import java.io.*
|
import java.io.*
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import java.util.logging.Level
|
||||||
|
import kotlin.collections.HashMap
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A part that makes "computer fixture" actually work
|
* A part that makes "computer fixture" actually work
|
||||||
@@ -87,6 +92,26 @@ class TerrarumComputer(peripheralSlots: Int) {
|
|||||||
val milliTime: Int
|
val milliTime: Int
|
||||||
get() = (System.currentTimeMillis() - startupTimestamp).toInt()
|
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<String, VirtualDisk>()
|
||||||
|
|
||||||
|
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 {
|
init {
|
||||||
computerValue["memslot0"] = 4864 // -1 indicates mem slot is empty
|
computerValue["memslot0"] = 4864 // -1 indicates mem slot is empty
|
||||||
computerValue["memslot1"] = -1 // put index of item here
|
computerValue["memslot1"] = -1 // put index of item here
|
||||||
@@ -96,20 +121,20 @@ class TerrarumComputer(peripheralSlots: Int) {
|
|||||||
computerValue["processor"] = -1 // do.
|
computerValue["processor"] = -1 // do.
|
||||||
|
|
||||||
// as in "dev/hda"; refers hard disk drive (and no partitioning)
|
// as in "dev/hda"; refers hard disk drive (and no partitioning)
|
||||||
computerValue["hda"] = "uuid_testhda" // 'UUID rendered as String' or "none"
|
attachDisk("hda", "uuid_testhda")
|
||||||
computerValue["hdb"] = "uuid_testhdb"
|
attachDisk("hdb", "")
|
||||||
computerValue["hdc"] = "none"
|
attachDisk("hdc", "")
|
||||||
computerValue["hdd"] = "none"
|
attachDisk("hdd", "")
|
||||||
// as in "dev/fd1"; refers floppy disk drive
|
// as in "dev/fd1"; refers floppy disk drive
|
||||||
computerValue["fd1"] = "uuid_testfd1"
|
attachDisk("fd1", "")
|
||||||
computerValue["fd2"] = "none"
|
attachDisk("fd2", "")
|
||||||
computerValue["fd3"] = "none"
|
attachDisk("fd3", "")
|
||||||
computerValue["fd4"] = "none"
|
attachDisk("fd4", "")
|
||||||
// SCSI connected optical drive
|
// SCSI connected optical drive
|
||||||
computerValue["sda"] = "none"
|
attachDisk("sda", "")
|
||||||
|
|
||||||
// boot device
|
// boot device
|
||||||
computerValue["boot"] = computerValue.getAsString("hda")!!
|
computerValue["boot"] = "hda"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPeripheral(tableName: String): Peripheral? {
|
fun getPeripheral(tableName: String): Peripheral? {
|
||||||
@@ -168,7 +193,6 @@ class TerrarumComputer(peripheralSlots: Int) {
|
|||||||
PcSpeakerDriver(luaJ_globals, this)
|
PcSpeakerDriver(luaJ_globals, this)
|
||||||
WorldInformationProvider(luaJ_globals)
|
WorldInformationProvider(luaJ_globals)
|
||||||
|
|
||||||
|
|
||||||
// secure the sandbox
|
// secure the sandbox
|
||||||
//luaJ_globals["io"] = LuaValue.NIL
|
//luaJ_globals["io"] = LuaValue.NIL
|
||||||
// dubug should be sandboxed in BOOT.lua (use OpenComputers code)
|
// dubug should be sandboxed in BOOT.lua (use OpenComputers code)
|
||||||
|
|||||||
522
src/net/torvald/terrarum/virtualcomputer/luaapi/FilesystemDir.kt
Normal file
522
src/net/torvald/terrarum/virtualcomputer/luaapi/FilesystemDir.kt
Normal file
@@ -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/<uuid for the hda>/
|
||||||
|
*
|
||||||
|
* 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: <appdata>/Saves/<savename>/computers/<drivename>/
|
||||||
|
* directs media/ directory to /<uuid> 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: <appdata>/Saves/<savename>/computers/<drivename>/
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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/<uuid for the hda>/
|
||||||
|
*
|
||||||
|
* 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[<boot device>]
|
||||||
|
*/
|
||||||
|
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<DiskEntry>? {
|
||||||
|
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: <appdata>/Saves/<savename>/computers/<drivename>/
|
||||||
|
*/
|
||||||
|
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))
|
||||||
|
}
|
||||||
@@ -54,8 +54,9 @@ internal class Term(globals: Globals, term: Teletype) {
|
|||||||
companion object {
|
companion object {
|
||||||
fun LuaValue.checkIBM437(): String {
|
fun LuaValue.checkIBM437(): String {
|
||||||
if (this is LuaString)
|
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"
|
// it only works if Charset is ISO-8859, despite of the name "IBM437"
|
||||||
|
// --> then would "CP437" work? -- Torvald at 2017-04-05
|
||||||
else
|
else
|
||||||
throw LuaError("bad argument (string expected, got ${this.typename()})")
|
throw LuaError("bad argument (string expected, got ${this.typename()})")
|
||||||
}
|
}
|
||||||
|
|||||||
816
src/net/torvald/terrarum/virtualcomputer/tvd/VDUtil.kt
Normal file
816
src/net/torvald/terrarum/virtualcomputer/tvd/VDUtil.kt
Normal file
@@ -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<ByteArray>()
|
||||||
|
|
||||||
|
val lastIndex: Int
|
||||||
|
get() = hierarchy.lastIndex
|
||||||
|
fun last(): ByteArray = hierarchy.last()
|
||||||
|
|
||||||
|
constructor(strPath: String, charset: Charset) : this() {
|
||||||
|
val unsanitisedHierarchy = ArrayList<String>()
|
||||||
|
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<ByteArray>) : 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<ByteArray>()
|
||||||
|
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<EntryID>()
|
||||||
|
(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<DiskEntry> {
|
||||||
|
if (entry.contents !is EntryDirectory)
|
||||||
|
throw IllegalArgumentException("The entry is not directory")
|
||||||
|
|
||||||
|
val entriesList = ArrayList<DiskEntry>()
|
||||||
|
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<DiskEntry> {
|
||||||
|
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 <The file, Parent file>, or null if not found
|
||||||
|
*/
|
||||||
|
fun getFile(disk: VirtualDisk, path: VDPath): EntrySearchResult? {
|
||||||
|
val searchHierarchy = ArrayList<DiskEntry>()
|
||||||
|
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<Byte>()
|
||||||
|
|
||||||
|
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<Byte>()
|
||||||
|
|
||||||
|
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<Byte>()
|
||||||
|
|
||||||
|
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<Byte>()
|
||||||
|
|
||||||
|
fileEntry.modificationDate = VDUtil.currentUnixtime
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw IOException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
flush()
|
||||||
|
closed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
246
src/net/torvald/terrarum/virtualcomputer/tvd/VirtualDisk.kt
Normal file
246
src/net/torvald/terrarum/virtualcomputer/tvd/VirtualDisk.kt
Normal file
@@ -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<EntryID, DiskEntry>()
|
||||||
|
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<Byte>()
|
||||||
|
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<EntryID> = ArrayList<EntryID>()) : 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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user