TEVD disk drive wip

This commit is contained in:
minjaesong
2022-12-17 16:06:14 +09:00
parent c8742980ff
commit e058d3999e
6 changed files with 512 additions and 15 deletions

View File

@@ -4,6 +4,8 @@
<root url="jar://$PROJECT_DIR$/lib/TerranVirtualDisk.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
<SOURCES>
<root url="jar://$PROJECT_DIR$/lib/TerranVirtualDisk-src.jar!/" />
</SOURCES>
</library>
</component>

Binary file not shown.

Binary file not shown.

View File

@@ -462,10 +462,10 @@ class TestDiskDrive(private val vm: VM, private val driveNum: Int, theRootPath:
private fun getSizeStr(): String {
val sb = StringBuilder()
val isRoot = (file.absolutePath == rootPath.absolutePath)
// val isRoot = (file.absolutePath == rootPath.absolutePath)
if (file.isFile) sb.append(file.length())
else sb.append(file.listFiles().size)
else sb.append(file.listFiles()!!.size)
return sb.toString()
}

View File

@@ -2,28 +2,30 @@ package net.torvald.tsvm.peripheral
import net.torvald.terrarum.modulecomputers.virtualcomputer.tvd.VDUtil
import net.torvald.tsvm.VM
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileNotFoundException
import java.io.*
import java.util.*
/**
* Created by minjaesong on 2022-12-15.
*/
class TevdDiskDrive(private val vm: VM, private val theRootPath: String) : BlockTransferInterface(false, true) {
class TevdDiskDrive(private val vm: VM, private val theTevdPath: String, val diskUUIDstr: String) : BlockTransferInterface(false, true) {
private val DBGPRN = true
val diskID: UUID = UUID.fromString(diskUUIDstr)
private fun printdbg(msg: Any) {
if (DBGPRN) println("[TestDiskDrive] $msg")
if (DBGPRN) println("[TevDiskDrive] $msg")
}
private val rootPath = File(theRootPath)
private val tevdPath = File(theTevdPath)
private val DOM = VDUtil.readDiskArchive(tevdPath, charset = VM.CHARSET)
private var fileOpen = false
private var fileOpenMode = -1 // 1: 'W", 2: 'A'
private var file = File(rootPath.toURI())
private var file: TevdFileDescriptor = TevdFileDescriptor(DOM, "")
//private var readModeLength = -1 // always 4096
private var writeMode = false
private var appendMode = false
@@ -37,12 +39,11 @@ class TevdDiskDrive(private val vm: VM, private val theRootPath: String) : Block
init {
statusCode = TestDiskDrive.STATE_CODE_STANDBY
if (!rootPath.exists()) {
throw FileNotFoundException("Disk file '${theRootPath}' not found")
if (!tevdPath.exists()) {
throw FileNotFoundException("Disk file '${theTevdPath}' not found")
}
}
private val DOM = VDUtil.readDiskArchive(rootPath, charset = VM.CHARSET)
companion object {
/** How often the changes in DOM (disk object model) should be saved to the physical drive when there are changes. Seconds. */
@@ -50,7 +51,7 @@ class TevdDiskDrive(private val vm: VM, private val theRootPath: String) : Block
}
fun commit() {
VDUtil.dumpToRealMachine(DOM, rootPath)
VDUtil.dumpToRealMachine(DOM, tevdPath)
}
@@ -91,9 +92,378 @@ class TevdDiskDrive(private val vm: VM, private val theRootPath: String) : Block
private lateinit var writeBuffer: ByteArray
private var writeBufferUsage = 0
/** Computer's attempt to startSend() will result in calling this very function.
* In such cases, `inputData` will be the message the computer sends.
*
* Disk drive must create desired side effects in accordance with the input message.
*/
override fun writeoutImpl(inputData: ByteArray) {
TODO("Not yet implemented")
if (writeMode || appendMode) {
//println("[DiskDrive] writeout with inputdata length of ${inputData.size}")
//println("[DiskDriveMsg] ${inputData.toString(Charsets.UTF_8)}")
if (!fileOpen) throw InternalError("File is not open but the drive is in write mode")
System.arraycopy(inputData, 0, writeBuffer, writeBufferUsage, minOf(writeModeLength - writeBufferUsage, inputData.size, BLOCK_SIZE))
writeBufferUsage += inputData.size
if (writeBufferUsage >= writeModeLength) {
// commit to the disk
if (appendMode)
file.appendBytes(writeBuffer)
else if (writeMode)
file.writeBytes(writeBuffer)
writeMode = false
appendMode = false
}
}
else {
val inputString = inputData.trimNull().toString(VM.CHARSET)
if (inputString.startsWith("DEVRST\u0017")) {
printdbg("Device Reset")
//readModeLength = -1
fileOpen = false
fileOpenMode = -1
file = TevdFileDescriptor(DOM, "")
blockSendCount = 0
statusCode = TestDiskDrive.STATE_CODE_STANDBY
writeMode = false
writeModeLength = -1
}
else if (inputString.startsWith("DEVSTU\u0017"))
recipient?.writeout(
TestDiskDrive.composePositiveAns(
"${statusCode.toChar()}",
TestDiskDrive.errorMsgs[statusCode]
)
)
else if (inputString.startsWith("DEVTYP\u0017"))
recipient?.writeout(TestDiskDrive.composePositiveAns("STOR"))
else if (inputString.startsWith("DEVNAM\u0017"))
recipient?.writeout(TestDiskDrive.composePositiveAns("Testtec Virtual Disk Drive"))
else if (inputString.startsWith("OPENR\"") || inputString.startsWith("OPENW\"") || inputString.startsWith("OPENA\"")) {
if (fileOpen) {
statusCode = TestDiskDrive.STATE_CODE_FILE_ALREADY_OPENED
return
}
printdbg("msg: $inputString, lastIndex: ${inputString.lastIndex}")
val openMode = inputString[4]
printdbg("open mode: $openMode")
// split inputstring into path and optional drive-number
// get position of latest delimeter (comma)
var commaIndex = inputString.lastIndex
while (commaIndex > 6) {
if (inputString[commaIndex] == ',') break; commaIndex -= 1
}
// sanity check if path is actually enclosed with double-quote
if (commaIndex != 6 && inputString[commaIndex - 1] != '"') {
statusCode = TestDiskDrive.STATE_CODE_ILLEGAL_COMMAND
return
}
val pathStr = inputString.substring(6, if (commaIndex == 6) inputString.lastIndex else commaIndex - 1)
val driveNum =
if (commaIndex == 6) null else inputString.substring(commaIndex + 1, inputString.length).toInt()
val filePath = filterSuperRoot(sanitisePath(pathStr))
// TODO driveNum is for disk drives that may have two or more slots built; for testing purposes we'll ignore it
file = TevdFileDescriptor(DOM, filePath)
printdbg("file path: ${file.canonicalPath}, drive num: $driveNum")
if (openMode == 'R' && !file.exists()) {
printdbg("! file not found")
statusCode = TestDiskDrive.STATE_CODE_NO_SUCH_FILE_EXISTS
return
}
statusCode = TestDiskDrive.STATE_CODE_STANDBY
fileOpen = true
fileOpenMode = when (openMode) {
'W' -> 1
'A' -> 2
else -> -1
}
blockSendCount = 0
}
else if (inputString.startsWith("DELETE")) {
if (!fileOpen) {
statusCode = TestDiskDrive.STATE_CODE_NO_FILE_OPENED
return
}
try {
file.delete()
}
catch (e: SecurityException) {
statusCode = TestDiskDrive.STATE_CODE_SYSTEM_SECURITY_ERROR
return
}
catch (e1: IOException) {
statusCode = TestDiskDrive.STATE_CODE_SYSTEM_IO_ERROR
return
}
}
else if (inputString.startsWith("LISTFILES")) {
// TODO temporary behaviour to ignore any arguments
resetBuf()
if (!fileOpen) {
statusCode = TestDiskDrive.STATE_CODE_NO_FILE_OPENED
return
}
try {
if (file.isDirectory) {
file.listFiles()!!.forEachIndexed { index, lsfile ->
if (index != 0) messageComposeBuffer.write(0x1E)
messageComposeBuffer.write(if (lsfile.isDirectory) 0x11 else 0x12)
messageComposeBuffer.write(lsfile.name.toByteArray(VM.CHARSET))
}
statusCode = TestDiskDrive.STATE_CODE_STANDBY
}
else {
statusCode = TestDiskDrive.STATE_CODE_NOT_A_DIRECTORY
return
}
}
catch (e: SecurityException) {
statusCode = TestDiskDrive.STATE_CODE_SYSTEM_SECURITY_ERROR
return
}
catch (e1: IOException) {
statusCode = TestDiskDrive.STATE_CODE_SYSTEM_IO_ERROR
return
}
}
else if (inputString.startsWith("GETLEN")) {
// TODO temporary behaviour to ignore any arguments
resetBuf()
if (!fileOpen) {
statusCode = TestDiskDrive.STATE_CODE_NO_FILE_OPENED
return
}
messageComposeBuffer.write(getSizeStr().toByteArray(VM.CHARSET))
statusCode = TestDiskDrive.STATE_CODE_STANDBY
}
else if (inputString.startsWith("LIST")) {
// TODO temporary behaviour to ignore any arguments
resetBuf()
if (!fileOpen) {
statusCode = TestDiskDrive.STATE_CODE_NO_FILE_OPENED
return
}
messageComposeBuffer.write(getReadableLs().toByteArray(VM.CHARSET))
statusCode = TestDiskDrive.STATE_CODE_STANDBY
}
else if (inputString.startsWith("CLOSE")) {
fileOpen = false
fileOpenMode = -1
statusCode = TestDiskDrive.STATE_CODE_STANDBY
}
else if (inputString.startsWith("READ")) {
//readModeLength = inputString.substring(4 until inputString.length).toInt()
resetBuf()
if (file.isFile) {
try {
messageComposeBuffer.write(file.readBytes())
statusCode = TestDiskDrive.STATE_CODE_STANDBY
}
catch (e: IOException) {
statusCode = TestDiskDrive.STATE_CODE_SYSTEM_IO_ERROR
}
}
else {
statusCode = TestDiskDrive.STATE_CODE_OPERATION_NOT_PERMITTED
return
}
}
else if (inputString.startsWith("LOADBOOT")) {
var commaIndex = 0
while (commaIndex < inputString.length) {
if (inputString[commaIndex] == ',') break
commaIndex += 1
}
val driveNum = if (commaIndex >= inputString.length) null else commaIndex
// TODO driveNum is for disk drives that may have two or more slots built; for testing purposes we'll ignore it
val bootFile = TevdFileDescriptor(DOM, "!BOOTSEC")
if (!bootFile.exists()) {
statusCode = TestDiskDrive.STATE_CODE_NO_SUCH_FILE_EXISTS
return
}
try {
val retMsg = bootFile.getHeadBytes(BLOCK_SIZE)
recipient?.writeout(retMsg)
statusCode = TestDiskDrive.STATE_CODE_STANDBY
}
catch (e: IOException) {
statusCode = TestDiskDrive.STATE_CODE_SYSTEM_IO_ERROR
return
}
}
else if (inputString.startsWith("MKDIR")) {
if (!fileOpen) {
statusCode = TestDiskDrive.STATE_CODE_NO_FILE_OPENED
return
}
if (fileOpenMode < 1) {
statusCode = TestDiskDrive.STATE_CODE_OPERATION_NOT_PERMITTED
return
}
try {
val status = file.mkdir()
statusCode = if (status) 0 else 1
}
catch (e: SecurityException) {
statusCode = TestDiskDrive.STATE_CODE_SYSTEM_SECURITY_ERROR
}
}
else if (inputString.startsWith("MKFILE")) {
if (!fileOpen) {
statusCode = TestDiskDrive.STATE_CODE_NO_FILE_OPENED
return
}
if (fileOpenMode < 1) {
statusCode = TestDiskDrive.STATE_CODE_OPERATION_NOT_PERMITTED
return
}
try {
val f1 = file.createNewFile()
statusCode = if (f1) TestDiskDrive.STATE_CODE_STANDBY else TestDiskDrive.STATE_CODE_OPERATION_FAILED
return
}
catch (e: IOException) {
statusCode = TestDiskDrive.STATE_CODE_SYSTEM_IO_ERROR
}
catch (e1: SecurityException) {
statusCode = TestDiskDrive.STATE_CODE_SYSTEM_SECURITY_ERROR
}
}
else if (inputString.startsWith("TOUCH")) {
if (!fileOpen) {
statusCode = TestDiskDrive.STATE_CODE_NO_FILE_OPENED
return
}
if (fileOpenMode < 1) {
statusCode = TestDiskDrive.STATE_CODE_OPERATION_NOT_PERMITTED
return
}
try {
val f1 = file.setLastModified(vm.worldInterface.currentTimeInMills())
statusCode = if (f1) TestDiskDrive.STATE_CODE_STANDBY else TestDiskDrive.STATE_CODE_OPERATION_FAILED
return
}
catch (e: IOException) {
statusCode = TestDiskDrive.STATE_CODE_SYSTEM_IO_ERROR
}
catch (e1: SecurityException) {
statusCode = TestDiskDrive.STATE_CODE_SYSTEM_SECURITY_ERROR
}
}
else if (inputString.startsWith("WRITE")) {
if (!fileOpen) {
statusCode = TestDiskDrive.STATE_CODE_NO_FILE_OPENED
return
}
if (fileOpenMode < 0) {
statusCode = TestDiskDrive.STATE_CODE_OPERATION_NOT_PERMITTED
return
}
if (fileOpenMode == 1) { writeMode = true; appendMode = false }
else if (fileOpenMode == 2) { writeMode = false; appendMode = true }
writeModeLength = inputString.substring(5, inputString.length).toInt()
writeBuffer = ByteArray(writeModeLength)
writeBufferUsage = 0
statusCode = TestDiskDrive.STATE_CODE_STANDBY
}
else
statusCode = TestDiskDrive.STATE_CODE_ILLEGAL_COMMAND
}
}
private fun sanitisePath(s: String) = s.replace('\\','/').replace(Regex("""\?<>:\*\|"""),"-")
// applies a "cap" if the path attemps to access parent directory of the root
private fun filterSuperRoot(path: String): String {
if (path.isEmpty()) return path
var parentCount = 0
val paths = path.split('/')
val newPaths = ArrayList<String>()
paths.forEach {
if (it.isBlank() || it.isEmpty()) {
/*do nothing*/
}
else if (it == "..") {
parentCount -= -1
}
else if (it != ".") {
parentCount += 1
}
if (parentCount < -1) parentCount = -1
if (parentCount >= 0) {
newPaths.add(it)
}
}
return newPaths.joinToString("/")
}
private fun getReadableLs(): String {
val sb = StringBuilder()
val isRoot = (file.entryID == 0)
if (file.isFile) sb.append(file.name)
else {
sb.append("Current directory: ")
sb.append(if (isRoot) "(root)" else file.path)
sb.append('\n')
sb.append(".\n")
if (isRoot) sb.append("..\n")
// actual entries
file.listFiles()!!.forEach {
var filenameLen = it.name.length
sb.append(it.name)
if (it.isDirectory) {
sb.append("/")
filenameLen += 1
}
sb.append(" ".repeat(40 - filenameLen))
if (it.isFile) {
sb.append("${it.length()} B")
}
sb.append('\n')
}
}
return if (sb.last() == '\n') sb.substring(0, sb.lastIndex) else sb.toString()
}
private fun getSizeStr(): String {
val sb = StringBuilder()
if (file.isFile) sb.append(file.length())
else sb.append(file.listFiles()!!.size)
return sb.toString()
}
}

View File

@@ -0,0 +1,125 @@
package net.torvald.tsvm.peripheral
import net.torvald.terrarum.modulecomputers.virtualcomputer.tvd.*
import net.torvald.tsvm.VM
import java.io.IOException
/**
* Created by minjaesong on 2022-12-17.
*/
class TevdFileDescriptor(val DOM: VirtualDisk, _pathstr: String) {
val path = _pathstr.replace('/', '\\')
val vdPath = VDUtil.VDPath(path, VM.CHARSET)
val entryID: EntryID?
get() = VDUtil.getFile(DOM, vdPath)?.entryID
val canonicalPath: String
get() = path
val name: String
get() = path.substring(path.lastIndexOf('\\') + 1)
val nameBytes: ByteArray
get() = name.toByteArray(VM.CHARSET)
val isFile: Boolean
get() = TODO()
val isDirectory: Boolean
get() = TODO()
private var fileContent: EntryFile? = null
fun appendBytes(bytes: ByteArray) {
fileContent?.getContent()?.appendBytes(bytes)
}
fun writeBytes(bytes: ByteArray) {
fileContent?.replaceContent(ByteArray64.fromByteArray(bytes))
}
/**
* @param length how many bytes to read
* @return actual bytes read, the size may be less than `length` if the actual file size is smaller
*/
fun getHeadBytes64(length: Long): ByteArray64 {
if (fileContent == null) throw IOException("No such file or not a file")
return fileContent!!.getContent().sliceArray64(0L until length)
}
/**
* @param length how many bytes to read
* @return actual bytes read, the size may be less than `length` if the actual file size is smaller
*/
fun getHeadBytes(length: Int): ByteArray {
if (fileContent == null) throw IOException("No such file or not a file")
return fileContent!!.getContent().sliceArray(0 until length)
}
fun exists(): Boolean {
return VDUtil.getFile(DOM, vdPath) != null
}
fun delete() {
fileContent = null
}
fun length(): Long {
return if (isFile) fileContent?.getSizePure() ?: 0L
else -1L
}
fun listFiles(): Array<TevdFileDescriptor>? {
if (isFile) return null
entryID.let {
if (it == null) return null
return VDUtil.getDirectoryEntries(DOM, it).map {
TevdFileDescriptor(DOM, path + '\\' + it.getFilenameString(VM.CHARSET))
}.toTypedArray()
}
}
fun readBytes(): ByteArray {
if (isDirectory) throw RuntimeException("Not a file")
if (!exists()) throw IOException("File not found")
return VDUtil.getAsNormalFile(DOM, vdPath).getContent().toByteArray()
}
fun mkdir(): Boolean {
return try {
VDUtil.addDir(DOM, vdPath.getParent(), nameBytes)
true
}
catch (e: KotlinNullPointerException) {
false
}
}
fun createNewFile(): Boolean {
fileContent = EntryFile(ByteArray64())
val time_t = System.currentTimeMillis() / 1000
val newFile = DiskEntry(-1, -1, nameBytes, time_t, time_t, fileContent!!)
return try {
VDUtil.addFile(DOM, vdPath.getParent(), newFile)
true
}
catch (e: KotlinNullPointerException) {
false
}
}
fun setLastModified(newTime_t: Long): Boolean {
return VDUtil.getFile(DOM, vdPath).let {
if (it != null) {
it.modificationDate = newTime_t
true
}
else false
}
}
}