GameWorld: adding "worldIndex"; more save/load stuffs

This commit is contained in:
minjaesong
2018-10-03 23:15:24 +09:00
parent b380fa7ce7
commit 071dc85b94
14 changed files with 1301 additions and 58 deletions

View File

@@ -1,18 +1,24 @@
package net.torvald.terrarum.modulecomputers.virtualcomputer.tvd
import net.torvald.terrarum.serialise.toLittleInt
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
/**
* Creates entry-to-offset tables to allow streaming from the disk, without storing whole VD file to the memory.
*
* Created by minjaesong on 2017-11-17.
*/
class DiskSkimmer(diskFile: File) {
class DiskSkimmer(private val diskFile: File) {
class EntryOffsetPair(val entryID: Int, val offset: Long)
val entryToOffsetTable = ArrayList<EntryOffsetPair>()
/**
* EntryID to Offset.
*
* Offset is where the header begins, so first 4 bytes are exactly the same as the EntryID.
*/
val entryToOffsetTable = HashMap<EntryID, Long>()
init {
@@ -64,7 +70,7 @@ class DiskSkimmer(diskFile: File) {
// fill up table
entryToOffsetTable.add(EntryOffsetPair(entryID, currentPosition))
entryToOffsetTable[entryID] = currentPosition
skipRead(4) // skip entryID of parent
@@ -88,15 +94,109 @@ class DiskSkimmer(diskFile: File) {
}
/**
* Using entryToOffsetTable, composes DiskEntry on the fly upon request.
* @return DiskEntry if the entry exists on the disk, `null` otherwise.
*/
fun requestFile(entryID: EntryID): DiskEntry? {
// FIXME untested
entryToOffsetTable[entryID].let { offset ->
if (offset == null)
return null
else {
val fis = FileInputStream(diskFile)
fis.skip(offset + 4) // get to the EntryHeader's parent directory area
val parent = fis.read(4).toLittleInt()
val fileFlag = fis.read(1)[0]
val filename = fis.read(256)
val creationTime = fis.read(6).toInt48()
val modifyTime = fis.read(6).toInt48()
val skip_crc = fis.read(4)
// get entry size // TODO future me, does this kind of comment helpful or redundant?
val entrySize = when (fileFlag) {
DiskEntry.NORMAL_FILE -> {
fis.read(6).toInt48()
}
DiskEntry.DIRECTORY -> {
fis.read(2).toUint16().toLong()
}
DiskEntry.SYMLINK -> 4L
else -> throw UnsupportedOperationException("Unsupported entry type: $fileFlag") // FIXME no support for compressed file
}
val entryContent = when (fileFlag) {
DiskEntry.NORMAL_FILE -> {
val byteArray = ByteArray64(entrySize)
// read one byte at a time
for (c in 0L until entrySize) {
byteArray[c] = fis.read().toByte()
}
EntryFile(byteArray)
}
DiskEntry.DIRECTORY -> {
val dirContents = ArrayList<EntryID>()
// read 4 bytes at a time
val bytesBuffer4 = ByteArray(4)
for (c in 0L until entrySize) {
fis.read(bytesBuffer4)
dirContents.add(bytesBuffer4.toIntBig())
}
EntryDirectory(dirContents)
}
DiskEntry.SYMLINK -> {
val target = fis.read(4).toIntBig()
EntrySymlink(target)
}
else -> throw UnsupportedOperationException("Unsupported entry type: $fileFlag") // FIXME no support for compressed file
}
return DiskEntry(entryID, parent, filename, creationTime, modifyTime, entryContent)
}
}
}
companion object {
fun InputStream.read(size: Int): ByteArray {
val ba = ByteArray(size)
this.read(ba)
return ba
}
}
private fun ByteArray.toUint16(): Int {
return this[0].toUint().shl(8) or
this[1].toUint()
}
private fun ByteArray.toIntBig(): Int {
return this[0].toUint().shl(24) or this[1].toUint().shl(16) or
this[2].toUint().shl(8) or this[2].toUint()
return this[0].toUint().shl(24) or
this[1].toUint().shl(16) or
this[2].toUint().shl(8) or
this[3].toUint()
}
private fun ByteArray.toInt48(): Long {
return this[0].toUlong().shl(56) or this[1].toUlong().shl(48) or
this[2].toUlong().shl(40) or this[3].toUlong().shl(32) or
this[4].toUlong().shl(24) or this[5].toUlong().shl(16) or
this[6].toUlong().shl(8) or this[7].toUlong()
return this[0].toUlong().shl(40) or
this[1].toUlong().shl(32) or
this[2].toUlong().shl(24) or
this[3].toUlong().shl(16) or
this[4].toUlong().shl(8) or
this[5].toUlong()
}
private fun ByteArray.toInt64(): Long {
return this[0].toUlong().shl(56) or
this[1].toUlong().shl(48) or
this[2].toUlong().shl(40) or
this[3].toUlong().shl(32) or
this[4].toUlong().shl(24) or
this[5].toUlong().shl(16) or
this[6].toUlong().shl(8) or
this[7].toUlong()
}
}

View File

@@ -0,0 +1,113 @@
# Terran Virtual Disk Image Format Specification
current specversion number: 0x03
## Changes
### 0x03
- Option to compress file entry
### 0x02
- 48-Bit filesize and timestamp (Max 256 TiB / 8.9 million years)
- 8 Reserved footer
### 0x01
**Note: this version were never released in public**
- Doubly Linked List instead of Singly
## Specs
* File structure
Header
IndexNumber
<entry>
IndexNumber
<entry>
IndexNumber
<entry>
...
Footer
* Order of the indices does not matter. Actual sorting is a job of the application.
* Endianness: Big
## Header
Uint8[4] Magic: TEVd
Int48 Disk size in bytes (max 256 TiB)
Uint8[32] Disk name
Int32 CRC-32
1. create list of arrays that contains CRC
2. put all the CRCs of entries
3. sort the list (here's the catch -- you will treat CRCs as SIGNED integer)
4. for elems on list: update crc with the elem (crc = calculateCRC(crc, elem))
Int8 Version
(Header size: 47 bytes)
## IndexNumber and Contents
<Entry Header>
<Actual Entry>
### Entry Header
Int32 EntryID (random Integer). This act as "jump" position for directory listing.
NOTE: Index 0 must be a root "Directory"; 0xFEFEFEFE is invalid (used as footer marker)
Int32 EntryID of parent directory
Int8 Flag for file or directory or symlink (cannot be negative)
0x01: Normal file, 0x02: Directory list, 0x03: Symlink
0x11: Compressed normal file
Uint8[256] File name in UTF-8
Int48 Creation date in real-life UNIX timestamp
Int48 Last modification date in real-life UNIX timestamp
Int32 CRC-32 of Actual Entry
(Header size: 281 bytes)
### Entry of File (Uncompressed)
Int48 File size in bytes (max 256 TiB)
<Bytes> Actual Contents
(Header size: 6 bytes)
### Entry of File (Compressed)
Int48 Size of compressed payload (max 256 TiB)
Int48 Size of uncompressed file (max 256 TiB)
<Bytes> Actual Contents, DEFLATEd payload
(Header size: 12 bytes)
### Entry of Directory
Uint16 Number of entries (normal files, other directories, symlinks)
<Int32s> Entry listing, contains IndexNumber
(Header size: 2 bytes)
### Entry of Symlink
Int32 Target IndexNumber
(Content size: 4 bytes)
## Footer
Uint8[4] 0xFE 0xFE 0xFE 0xFE (footer marker)
Int8 Disk properties flag 1
0b 7 6 5 4 3 2 1 0
0th bit: Readonly
Int8[7] Reserved, should be filled with zero
<optional footer if present>
Uint8[2] 0xFF 0x19 (EOF mark)

View File

@@ -792,18 +792,22 @@ object VDUtil {
/**
* Creates new zero-filled file with given name and size
*/
fun createNewBlankFile(disk: VirtualDisk, directoryID: EntryID, fileSize: Long, filename: String, charset: Charset) {
fun createNewBlankFile(disk: VirtualDisk, directoryID: EntryID, fileSize: Long, filename: String, charset: Charset): EntryID {
disk.checkReadOnly()
disk.checkCapacity(fileSize + DiskEntry.HEADER_SIZE + 4)
addFile(disk, directoryID, DiskEntry(
val newEntry = DiskEntry(
disk.generateUniqueID(),
directoryID,
filename.toEntryName(DiskEntry.NAME_LENGTH, charset = charset),
currentUnixtime,
currentUnixtime,
EntryFile(fileSize)
))
)
addFile(disk, directoryID, newEntry)
return newEntry.entryID
}

View File

@@ -0,0 +1,107 @@
package net.torvald.terrarum.virtualcomputer.tvd.finder
import java.awt.BorderLayout
import java.awt.GridLayout
import javax.swing.*
/**
* Created by SKYHi14 on 2017-04-01.
*/
object Popups {
val okCancel = arrayOf("OK", "Cancel")
}
class OptionDiskNameAndCap {
val name = JTextField(11)
val capacity = JSpinner(SpinnerNumberModel(
368640L.toJavaLong(),
0L.toJavaLong(),
(1L shl 38).toJavaLong(),
1L.toJavaLong()
)) // default 360 KiB, MAX 256 GiB
val mainPanel = JPanel()
val settingPanel = JPanel()
init {
mainPanel.layout = BorderLayout()
settingPanel.layout = GridLayout(2, 2, 2, 0)
//name.text = "Unnamed"
settingPanel.add(JLabel("Name (max 32 bytes)"))
settingPanel.add(name)
settingPanel.add(JLabel("Capacity (bytes)"))
settingPanel.add(capacity)
mainPanel.add(settingPanel, BorderLayout.CENTER)
mainPanel.add(JLabel("Set capacity to 0 to make the disk read-only"), BorderLayout.SOUTH)
}
/**
* returns either JOptionPane.OK_OPTION or JOptionPane.CANCEL_OPTION
*/
fun showDialog(title: String): Int {
return JOptionPane.showConfirmDialog(null, mainPanel,
title, JOptionPane.OK_CANCEL_OPTION)
}
}
fun kotlin.Long.toJavaLong() = java.lang.Long(this)
class OptionFileNameAndCap {
val name = JTextField(11)
val capacity = JSpinner(SpinnerNumberModel(
4096L.toJavaLong(),
0L.toJavaLong(),
((1L shl 48) - 1L).toJavaLong(),
1L.toJavaLong()
)) // default 360 KiB, MAX 256 TiB
val mainPanel = JPanel()
val settingPanel = JPanel()
init {
mainPanel.layout = BorderLayout()
settingPanel.layout = GridLayout(2, 2, 2, 0)
//name.text = "Unnamed"
settingPanel.add(JLabel("Name (max 32 bytes)"))
settingPanel.add(name)
settingPanel.add(JLabel("Capacity (bytes)"))
settingPanel.add(capacity)
mainPanel.add(settingPanel, BorderLayout.CENTER)
}
/**
* returns either JOptionPane.OK_OPTION or JOptionPane.CANCEL_OPTION
*/
fun showDialog(title: String): Int {
return JOptionPane.showConfirmDialog(null, mainPanel,
title, JOptionPane.OK_CANCEL_OPTION)
}
}
class OptionSize {
val capacity = JSpinner(SpinnerNumberModel(
368640L.toJavaLong(),
0L.toJavaLong(),
(1L shl 38).toJavaLong(),
1L.toJavaLong()
)) // default 360 KiB, MAX 256 GiB
val settingPanel = JPanel()
init {
settingPanel.add(JLabel("Size (bytes)"))
settingPanel.add(capacity)
}
/**
* returns either JOptionPane.OK_OPTION or JOptionPane.CANCEL_OPTION
*/
fun showDialog(title: String): Int {
return JOptionPane.showConfirmDialog(null, settingPanel,
title, JOptionPane.OK_CANCEL_OPTION)
}
}

View File

@@ -0,0 +1,843 @@
package net.torvald.terrarum.virtualcomputer.tvd.finder
import net.torvald.terrarum.modulecomputers.virtualcomputer.tvd.*
import java.awt.BorderLayout
import java.awt.Dimension
import java.awt.event.KeyEvent
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.nio.charset.Charset
import java.time.Instant
import java.time.format.DateTimeFormatter
import java.util.*
import java.util.logging.Level
import javax.swing.*
import javax.swing.table.AbstractTableModel
import javax.swing.ListSelectionModel
import javax.swing.text.DefaultCaret
/**
* Created by SKYHi14 on 2017-04-01.
*/
class VirtualDiskCracker(val sysCharset: Charset = Charsets.UTF_8) : JFrame() {
private val annoyHackers = true // Jar build settings. Intended for Terrarum proj.
private val PREVIEW_MAX_BYTES = 4L * 1024 // 4 kBytes
private val appName = "TerranVirtualDiskCracker"
private val copyright = "Copyright 2017 Torvald (minjaesong). Distributed under MIT license."
private val magicOpen = "I solemnly swear that I am up to no good."
private val magicSave = "Mischief managed."
private val annoyWhenLaunchMsg = "Type in following to get started:\n$magicOpen"
private val annoyWhenSaveMsg = "Type in following to save:\n$magicSave"
private val panelMain = JPanel()
private val menuBar = JMenuBar()
private val tableFiles: JTable
private val fileDesc = JTextArea()
private val diskInfo = JTextArea()
private val statBar = JLabel("Open a disk or create new to get started")
private var vdisk: VirtualDisk? = null
private var clipboard: DiskEntry? = null
private val labelPath = JLabel("(root)")
private var currentDirectoryEntries: Array<DiskEntry>? = null
private val directoryHierarchy = Stack<EntryID>(); init { directoryHierarchy.push(0) }
private fun gotoSubDirectory(id: EntryID) {
directoryHierarchy.push(id)
labelPath.text = vdisk!!.entries[id]!!.getFilenameString(sysCharset)
selectedFile = null
fileDesc.text = ""
updateDiskInfo()
}
val currentDirectory: EntryID
get() = directoryHierarchy.peek()
val upperDirectory: EntryID
get() = if (directoryHierarchy.lastIndex == 0) 0
else directoryHierarchy[directoryHierarchy.lastIndex - 1]
private fun gotoRoot() {
directoryHierarchy.removeAllElements()
directoryHierarchy.push(0)
selectedFile = null
fileDesc.text = ""
updateDiskInfo()
}
private fun gotoParent() {
if (directoryHierarchy.size > 1)
directoryHierarchy.pop()
selectedFile = null
fileDesc.text = ""
updateDiskInfo()
}
private var selectedFile: EntryID? = null
val tableColumns = arrayOf("Name", "Date Modified", "Size")
val tableParentRecord = arrayOf(arrayOf("..", "", ""))
init {
if (annoyHackers) {
val mantra = JOptionPane.showInputDialog(annoyWhenLaunchMsg)
if (mantra != magicOpen) {
System.exit(1)
}
}
panelMain.layout = BorderLayout()
this.defaultCloseOperation = JFrame.EXIT_ON_CLOSE
tableFiles = JTable(tableParentRecord, tableColumns)
tableFiles.addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent) {
val table = e.source as JTable
val row = table.rowAtPoint(e.point)
selectedFile = if (row > 0)
currentDirectoryEntries!![row - 1].entryID
else
null // clicked ".."
fileDesc.text = if (selectedFile != null) {
getFileInfoText(vdisk!!.entries[selectedFile!!]!!)
}
else
""
fileDesc.caretPosition = 0
// double click
if (e.clickCount == 2) {
if (row == 0) {
gotoParent()
}
else {
val record = currentDirectoryEntries!![row - 1]
if (record.contents is EntryDirectory) {
gotoSubDirectory(record.entryID)
}
}
}
}
})
tableFiles.selectionModel = object : DefaultListSelectionModel() {
init { selectionMode = ListSelectionModel.SINGLE_SELECTION }
override fun clearSelection() { } // required!
override fun removeSelectionInterval(index0: Int, index1: Int) { } // required!
override fun fireValueChanged(isAdjusting: Boolean) { } // required!
}
tableFiles.model = object : AbstractTableModel() {
override fun getRowCount(): Int {
return if (vdisk != null)
1 + (currentDirectoryEntries?.size ?: 0)
else 1
}
override fun getColumnCount() = tableColumns.size
override fun getColumnName(column: Int) = tableColumns[column]
override fun getValueAt(rowIndex: Int, columnIndex: Int): Any {
if (rowIndex == 0) {
return tableParentRecord[0][columnIndex]
}
else {
if (vdisk != null) {
val entry = currentDirectoryEntries!![rowIndex - 1]
return when(columnIndex) {
0 -> entry.getFilenameString(sysCharset)
1 -> Instant.ofEpochSecond(entry.modificationDate).
atZone(TimeZone.getDefault().toZoneId()).
format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
2 -> entry.getEffectiveSize()
else -> ""
}
}
else {
return ""
}
}
}
}
val menuFile = JMenu("File")
menuFile.mnemonic = KeyEvent.VK_F
menuFile.add("New Disk…").addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent?) {
try {
val makeNewDisk: Boolean
if (vdisk != null) {
makeNewDisk = confirmedDiscard()
}
else {
makeNewDisk = true
}
if (makeNewDisk) {
// inquire new size
val dialogBox = OptionDiskNameAndCap()
val confirmNew = JOptionPane.OK_OPTION == dialogBox.showDialog("Set Property of New Disk")
if (confirmNew) {
vdisk = VDUtil.createNewDisk(
(dialogBox.capacity.value as Long).toLong(),
dialogBox.name.text,
sysCharset
)
gotoRoot()
updateDiskInfo()
setStat("Disk created")
}
}
}
catch (e: Exception) {
e.printStackTrace()
popupError(e.toString())
}
}
})
menuFile.add("Open Disk…").addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent?) {
val makeNewDisk: Boolean
if (vdisk != null) {
makeNewDisk = confirmedDiscard()
}
else {
makeNewDisk = true
}
if (makeNewDisk) {
val fileChooser = JFileChooser()
fileChooser.showOpenDialog(null)
if (fileChooser.selectedFile != null) {
try {
vdisk = VDUtil.readDiskArchive(fileChooser.selectedFile, Level.WARNING, { popupWarning(it) }, sysCharset)
if (vdisk != null) {
gotoRoot()
updateDiskInfo()
setStat("Disk loaded")
}
}
catch (e: Exception) {
e.printStackTrace()
popupError(e.toString())
}
}
}
}
})
menuFile.addSeparator()
menuFile.add("Save Disk as…").addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent?) {
if (vdisk != null) {
if (annoyHackers) {
val mantra = JOptionPane.showInputDialog(annoyWhenSaveMsg)
if (mantra != magicSave) {
popupError("Nope!")
return
}
}
val fileChooser = JFileChooser()
fileChooser.showSaveDialog(null)
if (fileChooser.selectedFile != null) {
try {
VDUtil.dumpToRealMachine(vdisk!!, fileChooser.selectedFile)
setStat("Disk saved")
}
catch (e: Exception) {
e.printStackTrace()
popupError(e.toString())
}
}
}
}
})
menuBar.add(menuFile)
val menuEdit = JMenu("Edit")
menuEdit.mnemonic = KeyEvent.VK_E
menuEdit.add("New File…").addMouseListener(object: MouseAdapter() {
override fun mousePressed(e: MouseEvent?) {
if (vdisk != null) {
try {
val dialogBox = OptionFileNameAndCap()
val confirmNew = JOptionPane.OK_OPTION == dialogBox.showDialog("Set Property of New File")
if (confirmNew) {
if (VDUtil.nameExists(vdisk!!, dialogBox.name.text, currentDirectory, sysCharset)) {
popupError("The name already exists")
}
else {
VDUtil.createNewBlankFile(
vdisk!!,
currentDirectory,
(dialogBox.capacity.value as Long).toLong(),
dialogBox.name.text,
sysCharset
)
updateDiskInfo()
setStat("File created")
}
}
}
catch (e: Exception) {
e.printStackTrace()
popupError(e.toString())
}
}
}
})
menuEdit.add("New Directory…").addMouseListener(object: MouseAdapter() {
override fun mousePressed(e: MouseEvent?) {
if (vdisk != null) {
val newname = JOptionPane.showInputDialog("Enter a new directory name:")
if (newname != null) {
try {
if (VDUtil.nameExists(vdisk!!, newname, currentDirectory, sysCharset)) {
popupError("The name already exists")
}
else {
VDUtil.addDir(vdisk!!, currentDirectory, newname.toEntryName(DiskEntry.NAME_LENGTH, sysCharset))
updateDiskInfo()
setStat("Directory created")
}
}
catch (e: Exception) {
e.printStackTrace()
popupError(e.toString())
}
}
}
}
})
menuEdit.addSeparator()
menuEdit.add("Cut").addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent?) {
// copy
clipboard = vdisk!!.entries[selectedFile]
// delete
if (vdisk != null && selectedFile != null) {
try {
VDUtil.deleteFile(vdisk!!, selectedFile!!)
updateDiskInfo()
setStat("File deleted")
}
catch (e: Exception) {
e.printStackTrace()
popupError(e.toString())
}
}
}
})
menuEdit.add("Copy").addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent?) {
clipboard = vdisk!!.entries[selectedFile]
setStat("File copied")
}
})
menuEdit.add("Paste").addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent?) {
fun paste1(newname: ByteArray) {
try {
VDUtil.addFile(vdisk!!, currentDirectory, DiskEntry(// clone
vdisk!!.generateUniqueID(),
currentDirectory,
newname,
clipboard!!.creationDate,
clipboard!!.modificationDate,
clipboard!!.contents
))
updateDiskInfo()
setStat("File pasted")
}
catch (e: Exception) {
e.printStackTrace()
popupError(e.toString())
}
}
if (clipboard != null && vdisk != null) {
// check name collision. If it is, ask for new one
if (VDUtil.nameExists(vdisk!!, clipboard!!.getFilenameString(sysCharset), currentDirectory, sysCharset)) {
val newname = JOptionPane.showInputDialog("The name already exists. Enter a new name:")
if (newname != null) {
paste1(newname.toEntryName(DiskEntry.NAME_LENGTH, sysCharset))
}
}
else {
paste1(clipboard!!.filename)
}
}
}
})
menuEdit.add("Paste as Symbolic Link").addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent?) {
fun pasteSymbolic(newname: ByteArray) {
try {
// check if the original file is there in the first place
if (vdisk!!.entries[clipboard!!.entryID] != null) {
val entrySymlink = EntrySymlink(clipboard!!.entryID)
VDUtil.addFile(vdisk!!, currentDirectory, DiskEntry(
vdisk!!.generateUniqueID(),
currentDirectory,
newname,
VDUtil.currentUnixtime,
VDUtil.currentUnixtime,
entrySymlink
))
updateDiskInfo()
setStat("Symbolic link created")
}
else {
popupError("The orignal file is gone")
}
}
catch (e: Exception) {
e.printStackTrace()
popupError(e.toString())
}
}
if (clipboard != null && vdisk != null) {
// check name collision. If it is, ask for new one
if (VDUtil.nameExists(vdisk!!, clipboard!!.getFilenameString(sysCharset), currentDirectory, sysCharset)) {
val newname = JOptionPane.showInputDialog("The name already exists. Enter a new name:")
if (newname != null) {
pasteSymbolic(newname.toEntryName(DiskEntry.NAME_LENGTH, sysCharset))
}
}
else {
pasteSymbolic(clipboard!!.filename)
}
}
}
})
menuEdit.add("Delete").addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent?) {
if (vdisk != null && selectedFile != null) {
try {
VDUtil.deleteFile(vdisk!!, selectedFile!!)
updateDiskInfo()
setStat("File deleted")
}
catch (e: Exception) {
e.printStackTrace()
popupError(e.toString())
}
}
}
})
menuEdit.add("Rename…").addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent?) {
if (selectedFile != null) {
try {
val newname = JOptionPane.showInputDialog("Enter a new name:")
if (newname != null) {
if (VDUtil.nameExists(vdisk!!, newname, currentDirectory, sysCharset)) {
popupError("The name already exists")
}
else {
VDUtil.renameFile(vdisk!!, selectedFile!!, newname, sysCharset)
updateDiskInfo()
setStat("File renamed")
}
}
}
catch (e: Exception) {
e.printStackTrace()
popupError(e.toString())
}
}
}
})
menuEdit.add("Look Clipboard").addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent?) {
popupMessage(if (clipboard != null)
"${clipboard ?: "(bug found)"}"
else "(nothing)", "Clipboard"
)
}
})
menuEdit.addSeparator()
menuEdit.add("Import…").addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent?) {
if (vdisk != null) {
val fileChooser = JFileChooser()
fileChooser.fileSelectionMode = JFileChooser.FILES_AND_DIRECTORIES
fileChooser.isMultiSelectionEnabled = true
fileChooser.showOpenDialog(null)
if (fileChooser.selectedFiles.isNotEmpty()) {
try {
fileChooser.selectedFiles.forEach {
if (!it.isDirectory) {
val entry = VDUtil.importFile(it, vdisk!!.generateUniqueID(), sysCharset)
val newname: String?
if (VDUtil.nameExists(vdisk!!, entry.filename, currentDirectory)) {
newname = JOptionPane.showInputDialog("The name already exists. Enter a new name:")
}
else {
newname = entry.getFilenameString(sysCharset)
}
if (newname != null) {
entry.filename = newname.toEntryName(DiskEntry.NAME_LENGTH, sysCharset)
VDUtil.addFile(vdisk!!, currentDirectory, entry)
}
}
else {
val newname: String?
if (VDUtil.nameExists(vdisk!!, it.name.toEntryName(DiskEntry.NAME_LENGTH, sysCharset), currentDirectory)) {
newname = JOptionPane.showInputDialog("The name already exists. Enter a new name:")
}
else {
newname = it.name
}
if (newname != null) {
VDUtil.importDirRecurse(vdisk!!, it, currentDirectory, sysCharset, newname)
}
}
}
updateDiskInfo()
setStat("File added")
}
catch (e: Exception) {
e.printStackTrace()
popupError(e.toString())
}
}
fileChooser.isMultiSelectionEnabled = false
}
}
})
menuEdit.add("Export…").addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent?) {
if (vdisk != null) {
val file = vdisk!!.entries[selectedFile ?: currentDirectory]!!
val fileChooser = JFileChooser()
fileChooser.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
fileChooser.isMultiSelectionEnabled = false
fileChooser.showSaveDialog(null)
if (fileChooser.selectedFile != null) {
try {
val file = VDUtil.resolveIfSymlink(vdisk!!, file.entryID)
if (file.contents is EntryFile) {
VDUtil.exportFile(file.contents, fileChooser.selectedFile)
setStat("File exported")
}
else {
VDUtil.exportDirRecurse(vdisk!!, file.entryID, fileChooser.selectedFile, sysCharset)
setStat("Files exported")
}
}
catch (e: Exception) {
e.printStackTrace()
popupError(e.toString())
}
}
}
}
})
menuEdit.addSeparator()
menuEdit.add("Rename Disk…").addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent?) {
if (vdisk != null) {
try {
val newname = JOptionPane.showInputDialog("Enter a new disk name:")
if (newname != null) {
vdisk!!.diskName = newname.toEntryName(VirtualDisk.NAME_LENGTH, sysCharset)
updateDiskInfo()
setStat("Disk renamed")
}
}
catch (e: Exception) {
e.printStackTrace()
popupError(e.toString())
}
}
}
})
menuEdit.add("Resize Disk…").addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent?) {
if (vdisk != null) {
try {
val dialog = OptionSize()
val confirmed = dialog.showDialog("Input") == JOptionPane.OK_OPTION
if (confirmed) {
vdisk!!.capacity = (dialog.capacity.value as Long).toLong()
updateDiskInfo()
setStat("Disk resized")
}
}
catch (e: Exception) {
e.printStackTrace()
popupError(e.toString())
}
}
}
})
menuEdit.addSeparator()
menuEdit.add("Set/Unset Write Protection").addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent?) {
if (vdisk != null) {
try {
vdisk!!.isReadOnly = vdisk!!.isReadOnly.not()
updateDiskInfo()
setStat("Disk write protection ${if (vdisk!!.isReadOnly) "" else "dis"}engaged")
}
catch (e: Exception) {
e.printStackTrace()
popupError(e.toString())
}
}
}
})
menuBar.add(menuEdit)
val menuManage = JMenu("Manage")
menuManage.add("Report Orphans…").addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent?) {
if (vdisk != null) {
try {
val reports = VDUtil.gcSearchOrphan(vdisk!!)
val orphansCount = reports.size
val orphansSize = reports.map { vdisk!!.entries[it]!!.contents.getSizeEntry() }.sum()
val message = "Orphans count: $orphansCount\n" +
"Size: ${orphansSize.bytes()}"
popupMessage(message, "Orphans Report")
}
catch (e: Exception) {
e.printStackTrace()
popupError(e.toString())
}
}
}
})
menuManage.add("Report Phantoms…").addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent?) {
if (vdisk != null) {
try {
val reports = VDUtil.gcSearchPhantomBaby(vdisk!!)
val phantomsSize = reports.size
val message = "Phantoms count: $phantomsSize"
popupMessage(message, "Phantoms Report")
}
catch (e: Exception) {
e.printStackTrace()
popupError(e.toString())
}
}
}
})
menuManage.addSeparator()
menuManage.add("Remove Orphans").addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent?) {
if (vdisk != null) {
try {
val oldSize = vdisk!!.usedBytes
VDUtil.gcDumpOrphans(vdisk!!)
val newSize = vdisk!!.usedBytes
popupMessage("Saved ${(oldSize - newSize).bytes()}", "GC Report")
updateDiskInfo()
setStat("Orphan nodes removed")
}
catch (e: Exception) {
e.printStackTrace()
popupError(e.toString())
}
}
}
})
menuManage.add("Full Garbage Collect").addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent?) {
if (vdisk != null) {
try {
val oldSize = vdisk!!.usedBytes
VDUtil.gcDumpAll(vdisk!!)
val newSize = vdisk!!.usedBytes
popupMessage("Saved ${(oldSize - newSize).bytes()}", "GC Report")
updateDiskInfo()
setStat("Orphan nodes and null directory pointers removed")
}
catch (e: Exception) {
e.printStackTrace()
popupError(e.toString())
}
}
}
})
menuBar.add(menuManage)
val menuAbout = JMenu("About")
menuAbout.addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent?) {
popupMessage(copyright, "Copyright")
}
})
menuBar.add(menuAbout)
diskInfo.highlighter = null
diskInfo.text = "(Disk not loaded)"
diskInfo.preferredSize = Dimension(-1, 60)
fileDesc.highlighter = null
fileDesc.text = ""
fileDesc.caret.isVisible = false
(fileDesc.caret as DefaultCaret).updatePolicy = DefaultCaret.NEVER_UPDATE
val fileDescScroll = JScrollPane(fileDesc)
val tableFilesScroll = JScrollPane(tableFiles)
tableFilesScroll.size = Dimension(200, -1)
val panelFinder = JPanel(BorderLayout())
panelFinder.add(labelPath, BorderLayout.NORTH)
panelFinder.add(tableFilesScroll, BorderLayout.CENTER)
val panelFileDesc = JPanel(BorderLayout())
panelFileDesc.add(JLabel("Entry Information"), BorderLayout.NORTH)
panelFileDesc.add(fileDescScroll, BorderLayout.CENTER)
val filesSplit = JSplitPane(JSplitPane.HORIZONTAL_SPLIT, panelFinder, panelFileDesc)
filesSplit.resizeWeight = 0.714285
val panelDiskOp = JPanel(BorderLayout(2, 2))
panelDiskOp.add(filesSplit, BorderLayout.CENTER)
panelDiskOp.add(diskInfo, BorderLayout.SOUTH)
panelMain.add(menuBar, BorderLayout.NORTH)
panelMain.add(panelDiskOp, BorderLayout.CENTER)
panelMain.add(statBar, BorderLayout.SOUTH)
this.title = appName
this.add(panelMain)
this.setSize(700, 700)
this.isVisible = true
}
private fun confirmedDiscard() = 0 == JOptionPane.showOptionDialog(
null, // parent
"Any changes to current disk will be discarded. Continue?",
"Confirm Discard", // window title
JOptionPane.DEFAULT_OPTION, // option type
JOptionPane.WARNING_MESSAGE, // message type
null, // icon
Popups.okCancel, // options (provided by JOptionPane.OK_CANCEL_OPTION in this case)
Popups.okCancel[1] // default selection
)
private fun popupMessage(message: String, title: String = "") {
JOptionPane.showOptionDialog(
null,
message,
title,
JOptionPane.DEFAULT_OPTION,
JOptionPane.INFORMATION_MESSAGE,
null, null, null
)
}
private fun popupError(message: String, title: String = "Uh oh…") {
JOptionPane.showOptionDialog(
null,
message,
title,
JOptionPane.DEFAULT_OPTION,
JOptionPane.ERROR_MESSAGE,
null, null, null
)
}
private fun popupWarning(message: String, title: String = "Careful…") {
JOptionPane.showOptionDialog(
null,
message,
title,
JOptionPane.DEFAULT_OPTION,
JOptionPane.WARNING_MESSAGE,
null, null, null
)
}
private fun updateCurrentDirectory() {
currentDirectoryEntries = VDUtil.getDirectoryEntries(vdisk!!, currentDirectory)
}
private fun updateDiskInfo() {
val sb = StringBuilder()
directoryHierarchy.forEach {
sb.append(vdisk!!.entries[it]!!.getFilenameString(sysCharset))
sb.append('/')
}
sb.dropLast(1)
labelPath.text = sb.toString()
diskInfo.text = if (vdisk == null) "(Disk not loaded)" else getDiskInfoText(vdisk!!)
tableFiles.revalidate()
tableFiles.repaint()
updateCurrentDirectory()
}
private fun getDiskInfoText(disk: VirtualDisk): String {
return """Name: ${String(disk.diskName, sysCharset)}
Capacity: ${disk.capacity} bytes (${disk.usedBytes} bytes used, ${disk.capacity - disk.usedBytes} bytes free)
Write protected: ${disk.isReadOnly.toEnglish()}"""
}
private fun Boolean.toEnglish() = if (this) "Yes" else "No"
private fun getFileInfoText(file: DiskEntry): String {
return """Name: ${file.getFilenameString(sysCharset)}
Size: ${file.getEffectiveSize()}
Type: ${DiskEntry.getTypeString(file.contents)}
CRC: ${file.hashCode().toHex()}
EntryID: ${file.entryID}
ParentID: ${file.parentEntryID}""" + if (file.contents is EntryFile) """
Contents:
${String(file.contents.bytes.sliceArray64(0L..minOf(PREVIEW_MAX_BYTES, file.contents.bytes.size) - 1).toByteArray(), sysCharset)}""" else ""
}
private fun Long.bytes() = if (this == 1L) "1 byte" else "$this bytes"
private fun Int.entries() = if (this == 1) "1 entry" else "$this entries"
private fun DiskEntry.getEffectiveSize() = if (this.contents is EntryFile)
this.contents.getSizePure().bytes()
else if (this.contents is EntryDirectory)
this.contents.entryCount.entries()
else if (this.contents is EntrySymlink)
"(symlink)"
else
"n/a"
private fun setStat(message: String) {
statBar.text = message
}
}
fun main(args: Array<String>) {
VirtualDiskCracker(Charset.forName("CP437"))
}