diff --git a/.idea/libraries/TerranVirtualDisk.xml b/.idea/libraries/TerranVirtualDisk.xml index 2e09a48..e0e61d4 100644 --- a/.idea/libraries/TerranVirtualDisk.xml +++ b/.idea/libraries/TerranVirtualDisk.xml @@ -4,6 +4,8 @@ - + + + \ No newline at end of file diff --git a/lib/TerranVirtualDisk-src.jar b/lib/TerranVirtualDisk-src.jar index 962c2af..7626feb 100644 Binary files a/lib/TerranVirtualDisk-src.jar and b/lib/TerranVirtualDisk-src.jar differ diff --git a/lib/TerranVirtualDisk.jar b/lib/TerranVirtualDisk.jar index edb7db7..39a526d 100644 Binary files a/lib/TerranVirtualDisk.jar and b/lib/TerranVirtualDisk.jar differ diff --git a/tsvm_core/src/net/torvald/tsvm/peripheral/TestDiskDrive.kt b/tsvm_core/src/net/torvald/tsvm/peripheral/TestDiskDrive.kt index a00ff39..b246b7f 100644 --- a/tsvm_core/src/net/torvald/tsvm/peripheral/TestDiskDrive.kt +++ b/tsvm_core/src/net/torvald/tsvm/peripheral/TestDiskDrive.kt @@ -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() } diff --git a/tsvm_core/src/net/torvald/tsvm/peripheral/TevdDiskDrive.kt b/tsvm_core/src/net/torvald/tsvm/peripheral/TevdDiskDrive.kt index ca6d8bc..9c1b250 100644 --- a/tsvm_core/src/net/torvald/tsvm/peripheral/TevdDiskDrive.kt +++ b/tsvm_core/src/net/torvald/tsvm/peripheral/TevdDiskDrive.kt @@ -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() + 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() + } + } \ No newline at end of file diff --git a/tsvm_core/src/net/torvald/tsvm/peripheral/TevdFileDescriptor.kt b/tsvm_core/src/net/torvald/tsvm/peripheral/TevdFileDescriptor.kt new file mode 100644 index 0000000..c3928b5 --- /dev/null +++ b/tsvm_core/src/net/torvald/tsvm/peripheral/TevdFileDescriptor.kt @@ -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? { + 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 + } + } + + +} \ No newline at end of file