mirror of
https://github.com/curioustorvald/Terrarum.git
synced 2026-06-10 02:24:05 +09:00
799 lines
29 KiB
Kotlin
799 lines
29 KiB
Kotlin
package net.torvald.terrarum.savegame
|
|
|
|
import net.torvald.terrarum.serialise.toUint
|
|
import net.torvald.terrarum.serialise.toUlong
|
|
import java.io.*
|
|
import java.nio.charset.Charset
|
|
import java.util.*
|
|
import java.util.logging.Level
|
|
import java.util.zip.GZIPInputStream
|
|
import java.util.zip.GZIPOutputStream
|
|
import kotlin.collections.HashMap
|
|
import kotlin.experimental.and
|
|
|
|
/**
|
|
* Temporarily disabling on-disk compression; it somehow does not work, compress the files by yourself!
|
|
*
|
|
* Created by minjaesong on 2017-04-01.
|
|
*/
|
|
object VDUtil {
|
|
|
|
fun File.writeBytes64(array: net.torvald.terrarum.savegame.ByteArray64) {
|
|
array.writeToFile(this)
|
|
}
|
|
|
|
fun File.readBytes64(): net.torvald.terrarum.savegame.ByteArray64 {
|
|
val inbytes = net.torvald.terrarum.savegame.ByteArray64(this.length())
|
|
val inputStream = BufferedInputStream(FileInputStream(this))
|
|
var readInt = inputStream.read()
|
|
var readInCounter = 0L
|
|
while (readInt != -1) {
|
|
inbytes[readInCounter] = readInt.toByte()
|
|
readInCounter += 1
|
|
|
|
readInt = inputStream.read()
|
|
}
|
|
inputStream.close()
|
|
|
|
return inbytes
|
|
}
|
|
|
|
fun dumpToRealMachine(disk: VirtualDisk, outfile: File) {
|
|
outfile.writeBytes64(disk.serialize())
|
|
}
|
|
|
|
private const val DEBUG_PRINT_READ = false
|
|
|
|
/**
|
|
* 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): VirtualDisk {
|
|
val inbytes = infile.readBytes64()
|
|
|
|
|
|
|
|
if (magicMismatch(VirtualDisk.MAGIC, inbytes.sliceArray64(0L..3L).toByteArray()))
|
|
throw RuntimeException("Invalid Virtual Disk file!")
|
|
|
|
val diskSize = inbytes.sliceArray64(4L..9L).toInt48Big()
|
|
val diskName = inbytes.sliceArray(10..10 + 31) + inbytes.sliceArray(10+32+22..10+32+22+235)
|
|
val diskCRC = inbytes.sliceArray64(10L + 32..10L + 32 + 3).toIntBig() // to check with completed vdisk
|
|
val diskSpecVersion = inbytes[10L + 32 + 4]
|
|
val footers = inbytes.sliceArray64(10L+32+6..10L+32+21)
|
|
|
|
if (diskSpecVersion != specversion)
|
|
throw RuntimeException("Unsupported disk format version: current internal version is $specversion; the file's version is $diskSpecVersion")
|
|
|
|
val vdisk = VirtualDisk(diskSize, diskName, infile)
|
|
|
|
vdisk.__internalSetFooter__(footers)
|
|
|
|
//println("[VDUtil] currentUnixtime = $currentUnixtime")
|
|
|
|
val entryOffsetInfoForDebug = HashMap<EntryID, Long>()
|
|
|
|
var entryOffset = VirtualDisk.HEADER_SIZE
|
|
// not footer, entries
|
|
while (entryOffset < inbytes.size) {
|
|
//println("[VDUtil] entryOffset = $entryOffset")
|
|
// read and prepare all the shits
|
|
val entryOffsetStart = entryOffset
|
|
val entryID = inbytes.sliceArray64(entryOffset..entryOffset + 7).toLongBig()
|
|
val entryParentID = inbytes.sliceArray64(entryOffset + 8..entryOffset + 15).toLongBig()
|
|
val entryTypeFlag = inbytes[entryOffset + 16]
|
|
val entryCreationTime = inbytes.sliceArray64(entryOffset + 20..entryOffset + 25).toInt48Big()
|
|
val entryModifyTime = inbytes.sliceArray64(entryOffset + 26..entryOffset + 31).toInt48Big()
|
|
val entryCRC = inbytes.sliceArray64(entryOffset + 32..entryOffset + 35).toIntBig() // to check with completed entry
|
|
|
|
try {
|
|
val entryData = when (entryTypeFlag and 127) {
|
|
DiskEntry.NORMAL_FILE -> {
|
|
val filesize = inbytes.sliceArray64(entryOffset + DiskEntry.HEADER_SIZE..entryOffset + DiskEntry.HEADER_SIZE + 5).toInt48Big()
|
|
//println("[VDUtil] --> is file; filesize = $filesize")
|
|
inbytes.sliceArray64(entryOffset + DiskEntry.HEADER_SIZE + 6..entryOffset + DiskEntry.HEADER_SIZE + 5 + filesize)
|
|
}
|
|
DiskEntry.DIRECTORY -> {
|
|
val entryCount = inbytes.sliceArray64(entryOffset + DiskEntry.HEADER_SIZE..entryOffset + DiskEntry.HEADER_SIZE + 3).toIntBig()
|
|
//println("[VDUtil] --> is directory; entryCount = $entryCount")
|
|
inbytes.sliceArray64(entryOffset + DiskEntry.HEADER_SIZE + 4..entryOffset + DiskEntry.HEADER_SIZE + 3 + entryCount * 8)
|
|
}
|
|
DiskEntry.SYMLINK -> {
|
|
inbytes.sliceArray64(entryOffset + DiskEntry.HEADER_SIZE..entryOffset + DiskEntry.HEADER_SIZE + 7)
|
|
}
|
|
else -> throw RuntimeException("Unknown entry with type $entryTypeFlag at entryOffset $entryOffset")
|
|
}
|
|
|
|
if (DEBUG_PRINT_READ) {
|
|
println("[savegame.VDUtil] == Entry deserialise debugprint for entry ID $entryID (child of $entryParentID)")
|
|
println("Entry type flag: ${entryTypeFlag and 127}${if (entryTypeFlag < 0) "*" else ""}")
|
|
println("Entry raw contents bytes: (len: ${entryData.size})")
|
|
entryData.forEachIndexed { i, it ->
|
|
if (i > 0 && i % 8 == 0L) print(" ")
|
|
else if (i > 0 && i % 4 == 0L) print("_")
|
|
print(it.toInt().toHex().substring(6))
|
|
}; println()
|
|
}
|
|
|
|
|
|
// update entryOffset so that we can fetch next entry in the binary
|
|
entryOffset += DiskEntry.HEADER_SIZE + entryData.size + when (entryTypeFlag and 127) {
|
|
DiskEntry.NORMAL_FILE -> 6 // PLEASE DO REFER TO Spec.md
|
|
DiskEntry.DIRECTORY -> 4 // PLEASE DO REFER TO Spec.md
|
|
DiskEntry.SYMLINK -> 0 // PLEASE DO REFER TO Spec.md
|
|
else -> throw RuntimeException("Unknown entry with type $entryTypeFlag")
|
|
}
|
|
|
|
|
|
// check for the discard bit
|
|
if (entryTypeFlag > 0) {
|
|
|
|
// create entry
|
|
val diskEntry = DiskEntry(
|
|
entryID = entryID,
|
|
parentEntryID = entryParentID,
|
|
creationDate = entryCreationTime,
|
|
modificationDate = entryModifyTime,
|
|
contents = if (entryTypeFlag == DiskEntry.NORMAL_FILE) {
|
|
EntryFile(entryData)
|
|
} else if (entryTypeFlag == DiskEntry.DIRECTORY) {
|
|
|
|
val entryList = ArrayList<EntryID>()
|
|
|
|
(0 until entryData.size / 8).forEach { cnt ->
|
|
entryList.add(entryData.sliceArray64(8 * cnt until 8 * (cnt+1)).toLongBig())
|
|
}
|
|
|
|
entryList.sort()
|
|
|
|
EntryDirectory(entryList)
|
|
} else if (entryTypeFlag == DiskEntry.SYMLINK) {
|
|
EntrySymlink(entryData.toLongBig())
|
|
} else
|
|
throw RuntimeException("Unknown entry with type $entryTypeFlag")
|
|
)
|
|
|
|
// check CRC of entry
|
|
if (crcWarnLevel == Level.SEVERE || crcWarnLevel == Level.WARNING) {
|
|
|
|
// test print
|
|
if (DEBUG_PRINT_READ) {
|
|
val testbytez = diskEntry.contents.serialize()
|
|
val testbytes = testbytez
|
|
(diskEntry.contents as? EntryDirectory)?.forEach {
|
|
println("entry: ${it.toHex()}")
|
|
}
|
|
println("[savegame.VDUtil] bytes to calculate crc against:")
|
|
testbytes.forEachIndexed { i, it ->
|
|
if (i % 4 == 0L) print(" ")
|
|
print(it.toInt().toHex().substring(6))
|
|
}
|
|
println("\nCRC: " + testbytez.getCRC32().toHex())
|
|
}
|
|
// end of test print
|
|
|
|
val calculatedCRC = diskEntry.contents.serialize().getCRC32()
|
|
|
|
val crcMsg =
|
|
"CRC failed: stored value is ${entryCRC.toHex()}, but calculated value is ${calculatedCRC.toHex()}\n" +
|
|
"at file \"${diskIDtoReadableFilename(diskEntry.entryID, vdisk.saveKind)}\" (entry ID ${diskEntry.entryID})"
|
|
|
|
if (calculatedCRC != entryCRC) {
|
|
|
|
println("[savegame.VDUtil] CRC failed; entry info:\n$diskEntry")
|
|
|
|
if (crcWarnLevel == Level.SEVERE)
|
|
throw IOException(crcMsg)
|
|
else if (warningFunc != null)
|
|
warningFunc(crcMsg)
|
|
}
|
|
}
|
|
|
|
// add entry to disk
|
|
if (vdisk.entries[entryID] != null) {
|
|
println("[savegame.VDUtil] Overwriting existing entry ${entryID.toHex()} to new one; offset: ${entryOffsetInfoForDebug[entryID]} -> $entryOffsetStart; raw type flag: $entryTypeFlag)")
|
|
}
|
|
vdisk.entries[entryID] = diskEntry
|
|
entryOffsetInfoForDebug[entryID] = entryOffsetStart
|
|
}
|
|
else {
|
|
if (DEBUG_PRINT_READ) {
|
|
println("[savegame.VDUtil] Discarding entry ${entryID.toHex()} at offset $entryOffsetStart (raw type flag: $entryTypeFlag)")
|
|
}
|
|
}
|
|
}
|
|
catch (e: ArrayIndexOutOfBoundsException) {
|
|
System.err.println("An error occurred while reading a file (entryID: $entryID (${diskIDtoReadableFilename(entryID, vdisk.saveKind)}), typeFlag: $entryTypeFlag)")
|
|
System.err.println("Stack trace:")
|
|
e.printStackTrace()
|
|
break
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
|
|
fun isFile(disk: VirtualDisk, entryID: EntryID) = disk.entries[entryID]?.contents is EntryFile
|
|
fun isDirectory(disk: VirtualDisk, entryID: EntryID) = disk.entries[entryID]?.contents is EntryDirectory
|
|
fun isSymlink(disk: VirtualDisk, entryID: EntryID) = disk.entries[entryID]?.contents is EntrySymlink
|
|
|
|
/**
|
|
* Get list of entries of directory.
|
|
*/
|
|
fun getDirectoryEntries(disk: VirtualDisk, dirToSearch: DiskEntry): Array<DiskEntry> {
|
|
if (dirToSearch.contents !is EntryDirectory)
|
|
throw IllegalArgumentException("The entry is not directory")
|
|
|
|
val entriesList = ArrayList<DiskEntry>()
|
|
dirToSearch.contents.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)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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")
|
|
|
|
/**
|
|
* Fetch the file and returns a instance of normal file.
|
|
*/
|
|
fun getAsNormalFile(disk: VirtualDisk, entryIndex: EntryID) =
|
|
disk.entries[entryIndex]!!.getAsNormalFile(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, targetID: EntryID) {
|
|
disk.checkReadOnly()
|
|
|
|
val file = disk.entries[targetID]
|
|
|
|
if (file == null) {
|
|
throw FileNotFoundException("No such file to delete")
|
|
}
|
|
|
|
val parentID = file.parentEntryID
|
|
val parentDir = getAsDirectory(disk, parentID)
|
|
|
|
fun rollback() {
|
|
if (!disk.entries.contains(targetID)) {
|
|
disk.entries[targetID] = file
|
|
}
|
|
if (!parentDir.contains(targetID)) {
|
|
parentDir.add(targetID)
|
|
}
|
|
}
|
|
|
|
// check if directory "parentID" has "targetID" in the first place
|
|
if (!directoryContains(disk, parentID, targetID)) {
|
|
throw FileNotFoundException("No such file to delete")
|
|
}
|
|
else if (targetID == 0L) {
|
|
throw IOException("Cannot delete root file system")
|
|
}
|
|
else {
|
|
try {
|
|
// delete file record
|
|
disk.entries.remove(targetID)
|
|
// unlist file from parent directly
|
|
parentDir.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, fileID: EntryID, newID: EntryID, charset: Charset) {
|
|
val file = disk.entries[fileID]
|
|
|
|
if (file != null) {
|
|
file.entryID = newID
|
|
}
|
|
else {
|
|
throw FileNotFoundException()
|
|
}
|
|
}
|
|
/**
|
|
* Add file to the specified directory.
|
|
* The file will get new EntryID and its ParentID will be overwritten.
|
|
*/
|
|
fun addFile(disk: VirtualDisk, file: DiskEntry) {
|
|
disk.entries[file.entryID] = file
|
|
file.parentEntryID = 0
|
|
val dir = VDUtil.getAsDirectory(disk, 0)
|
|
if (!dir.contains(file.entryID)) dir.add(file.entryID)
|
|
}
|
|
|
|
fun randomBase62(length: Int): String {
|
|
val glyphs = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
|
|
val sb = StringBuilder()
|
|
|
|
kotlin.repeat(length) {
|
|
sb.append(glyphs[(Math.random() * glyphs.length).toInt()])
|
|
}
|
|
|
|
return sb.toString()
|
|
}
|
|
|
|
/**
|
|
* Add fully qualified DiskEntry to the disk, using file's own and its parent entryID.
|
|
*
|
|
* It's your job to ensure no ID collision.
|
|
*/
|
|
fun registerFile(disk: VirtualDisk, file: DiskEntry) {
|
|
disk.checkReadOnly()
|
|
disk.checkCapacity(file.serialisedSize)
|
|
|
|
VDUtil.getAsDirectory(disk, file.parentEntryID).add(file.entryID)
|
|
disk.entries[file.entryID] = file
|
|
}
|
|
|
|
/**
|
|
* Add file to the specified directory. ParentID of the file will be overwritten.
|
|
*/
|
|
fun addFile(disk: VirtualDisk, directoryID: EntryID, file: DiskEntry) {//}, compressTheFile: Boolean = false) {
|
|
disk.checkReadOnly()
|
|
disk.checkCapacity(file.serialisedSize)
|
|
|
|
try {
|
|
// generate new ID for the file
|
|
file.entryID = disk.generateUniqueID()
|
|
// add record to the directory
|
|
getAsDirectory(disk, directoryID).add(file.entryID)
|
|
|
|
// Gzip fat boy if marked as
|
|
/*if (compressTheFile && file.contents is EntryFile) {
|
|
val bo = ByteArray64GrowableOutputStream()
|
|
val zo = GZIPOutputStream(bo)
|
|
|
|
// zip
|
|
file.contents.bytes.forEach {
|
|
zo.write(it.toInt())
|
|
}
|
|
zo.flush(); zo.close()
|
|
|
|
val newContent = EntryFileCompressed(file.contents.bytes.size, bo.toByteArray64())
|
|
val newEntry = DiskEntry(
|
|
file.entryID, file.parentEntryID, file.filename, file.creationDate, file.modificationDate,
|
|
newContent
|
|
)
|
|
|
|
disk.entries[file.entryID] = newEntry
|
|
}
|
|
// just the add the boy to the house
|
|
else*/
|
|
disk.entries[file.entryID] = file
|
|
|
|
// make this boy recognise his new parent
|
|
file.parentEntryID = directoryID
|
|
}
|
|
catch (e: KotlinNullPointerException) {
|
|
throw FileNotFoundException("No such directory")
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Imports external file and returns corresponding DiskEntry.
|
|
*/
|
|
fun importFile(file: File, newID: EntryID, charset: Charset): DiskEntry {
|
|
if (file.isDirectory) {
|
|
throw IOException("The file is a directory")
|
|
}
|
|
|
|
return DiskEntry(
|
|
entryID = newID,
|
|
parentEntryID = 0, // placeholder
|
|
creationDate = currentUnixtime,
|
|
modificationDate = currentUnixtime,
|
|
contents = EntryFile(file.readBytes64())
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Export file on the virtual disk into real disk.
|
|
*/
|
|
fun exportFile(entryFile: EntryFile, outfile: File) {
|
|
outfile.createNewFile()
|
|
|
|
/*if (entryFile is EntryFileCompressed) {
|
|
entryFile.bytes.forEachBanks {
|
|
val fos = FileOutputStream(outfile)
|
|
val inflater = InflaterOutputStream(fos)
|
|
|
|
inflater.write(it)
|
|
inflater.flush()
|
|
inflater.close()
|
|
}
|
|
}
|
|
else*/
|
|
outfile.writeBytes64(entryFile.bytes)
|
|
}
|
|
|
|
|
|
/**
|
|
* Creates new disk with given name and capacity
|
|
*/
|
|
fun createNewDisk(diskSize: Long, diskName: String, charset: Charset): VirtualDisk {
|
|
val newdisk = VirtualDisk(diskSize, diskName.toEntryName(VirtualDisk.NAME_LENGTH, charset))
|
|
val rootDir = DiskEntry(
|
|
entryID = 0,
|
|
parentEntryID = 0,
|
|
creationDate = currentUnixtime,
|
|
modificationDate = currentUnixtime,
|
|
contents = EntryDirectory()
|
|
)
|
|
|
|
newdisk.entries[0] = rootDir
|
|
|
|
return newdisk
|
|
}
|
|
|
|
|
|
/**
|
|
* 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: Long) {
|
|
// if (this.usedBytes + newSize > this.capacity)
|
|
// throw IOException("Not enough space on the disk")
|
|
}
|
|
fun ByteArray64.toIntBig(): Int {
|
|
if (this.size != 4L)
|
|
throw UnsupportedOperationException("ByteArray is not Int")
|
|
|
|
var i = 0
|
|
var c = 0
|
|
this.forEach { byte -> i = i or byte.toUint().shl(24 - c * 8); c += 1 }
|
|
return i
|
|
}
|
|
fun ByteArray64.toLongBig(): Long {
|
|
if (this.size != 8L)
|
|
throw UnsupportedOperationException("ByteArray is not Long")
|
|
|
|
var i = 0L
|
|
var c = 0
|
|
this.forEach { byte -> i = i or byte.toUlong().shl(56 - c * 8); c += 1 }
|
|
return i
|
|
}
|
|
fun ByteArray64.toInt48Big(): Long {
|
|
if (this.size != 6L)
|
|
throw UnsupportedOperationException("ByteArray is not Long")
|
|
|
|
var i = 0L
|
|
var c = 0
|
|
this.forEach { byte -> i = i or byte.toUlong().shl(40 - c * 8); c += 1 }
|
|
return i
|
|
}
|
|
fun ByteArray64.toShortBig(): Short {
|
|
if (this.size != 2L)
|
|
throw UnsupportedOperationException("ByteArray is not Short")
|
|
|
|
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
|
|
}
|
|
|
|
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.contains(targetID)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Searches for disconnected nodes using its parent pointer.
|
|
* If the parent node is invalid, the node is considered orphan, and will be added
|
|
* to the list this function returns.
|
|
*
|
|
* @return List of orphan entries
|
|
*/
|
|
fun gcSearchOrphan(disk: VirtualDisk): List<EntryID> {
|
|
return disk.entries.filter { disk.entries[it.value.parentEntryID] == null }.keys.toList()
|
|
}
|
|
|
|
/**
|
|
* Searches for null-pointing entries (phantoms) within every directory.
|
|
*
|
|
* @return List of search results, which is Pair(directory that contains null pointer, null pointer)
|
|
*/
|
|
fun gcSearchPhantomBaby(disk: VirtualDisk): List<Pair<EntryID, EntryID>> {
|
|
// Pair<DirectoryID, ID of phantom in the directory>
|
|
val phantoms = ArrayList<Pair<EntryID, EntryID>>()
|
|
disk.entries.filter { it.value.contents is EntryDirectory }.values.forEach { directory ->
|
|
(directory.contents as EntryDirectory).forEach { dirEntryID ->
|
|
if (disk.entries[dirEntryID] == null) {
|
|
phantoms.add(Pair(directory.entryID, dirEntryID))
|
|
}
|
|
}
|
|
}
|
|
return phantoms
|
|
}
|
|
|
|
fun gcDumpOrphans(disk: VirtualDisk) {
|
|
try {
|
|
gcSearchOrphan(disk).forEach {
|
|
disk.entries.remove(it)
|
|
}
|
|
}
|
|
catch (e: Exception) {
|
|
e.printStackTrace()
|
|
throw InternalError("Aw, snap!")
|
|
}
|
|
}
|
|
|
|
fun gcDumpAll(disk: VirtualDisk) {
|
|
try {
|
|
gcSearchPhantomBaby(disk).forEach {
|
|
getAsDirectory(disk, it.first).remove(it.second)
|
|
}
|
|
gcSearchOrphan(disk).forEach {
|
|
disk.entries.remove(it)
|
|
}
|
|
}
|
|
catch (e: Exception) {
|
|
e.printStackTrace()
|
|
throw InternalError("Aw, snap!")
|
|
}
|
|
}
|
|
|
|
fun compress(ba: ByteArray64) = compress(ba.iterator())
|
|
fun compress(byteIterator: Iterator<Byte>): ByteArray64 {
|
|
val bo = ByteArray64GrowableOutputStream()
|
|
val zo = GZIPOutputStream(bo)
|
|
|
|
// zip
|
|
byteIterator.forEach {
|
|
zo.write(it.toInt())
|
|
}
|
|
zo.flush(); zo.close()
|
|
return bo.toByteArray64()
|
|
}
|
|
|
|
fun decompress(bytes: ByteArray64): ByteArray64 {
|
|
val unzipdBytes = ByteArray64()
|
|
val zi = GZIPInputStream(ByteArray64InputStream(bytes))
|
|
while (true) {
|
|
val byte = zi.read()
|
|
if (byte == -1) break
|
|
unzipdBytes.appendByte(byte.toByte())
|
|
}
|
|
zi.close()
|
|
return unzipdBytes
|
|
}
|
|
}
|
|
|
|
fun magicMismatch(magic: ByteArray, array: ByteArray): Boolean {
|
|
return !Arrays.equals(array, magic)
|
|
}
|
|
fun String.toEntryName(length: Int, charset: Charset): ByteArray {
|
|
val buffer = ByteArray64(length.toLong())
|
|
val stringByteArray = this.toByteArray(charset)
|
|
buffer.appendBytes(stringByteArray.sliceArray(0 until minOf(length, stringByteArray.size)))
|
|
return buffer.toByteArray()
|
|
}
|
|
fun ByteArray.toCanonicalString(charset: Charset): 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), charset)
|
|
}
|
|
|
|
fun ByteArray.toByteArray64(): ByteArray64 {
|
|
val array = ByteArray64(this.size.toLong())
|
|
this.forEachIndexed { index, byte ->
|
|
array[index.toLong()] = byte
|
|
}
|
|
return array
|
|
}
|
|
|
|
/**
|
|
* Writes String to the file
|
|
*
|
|
* Note: this FileWriter cannot write more than 2 GiB
|
|
*
|
|
* @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).toByteArray64()
|
|
newByteArray.forEach { newFileBuffer.add(it) }
|
|
}
|
|
else {
|
|
throw IOException()
|
|
}
|
|
}
|
|
|
|
override fun flush() {
|
|
if (!closed) {
|
|
val newByteArray = newFileBuffer.toByteArray()
|
|
|
|
if (!append) {
|
|
(fileEntry.contents as EntryFile).bytes = newByteArray.toByteArray64()
|
|
}
|
|
else {
|
|
val oldByteArray = (fileEntry.contents as EntryFile).bytes.toByteArray().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.bytes = newByteArray.toByteArray64()
|
|
}
|
|
|
|
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.toByteArray64()
|
|
}
|
|
else {
|
|
val oldByteArray = (fileEntry.contents as EntryFile).bytes.toByteArray().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.bytes = newByteArray.toByteArray64()
|
|
}
|
|
|
|
newFileBuffer = ArrayList<Byte>()
|
|
|
|
fileEntry.modificationDate = VDUtil.currentUnixtime
|
|
}
|
|
else {
|
|
throw IOException()
|
|
}
|
|
}
|
|
|
|
override fun close() {
|
|
flush()
|
|
closed = true
|
|
}
|
|
} |